aere-research/research/specs/spec-zk-stack.md
Aere Network 6cb0140fae Republished from a clean root: the compiled artifact is gone from history, and the local line of work joins the sanitized public line
The public history carried kat/__pycache__/mlkem768_reference.cpython-314.pyc,
a compiled Python artifact embedding the operator's absolute local path. Text
secret scanners do not read compiled binaries, which is exactly how it slipped
through, and removing it from the tip would have left it reachable through the
old root commits. So this repository is republished from a single clean root.

This root also carries, from the previously unpublished line of work:
- corrected LICENSE year, LICENSING.md, VERIFY-POLICY.md, and
  CITATIONS-UNRESOLVED.md remeasured 2026-08-11 (101 paths, README aligned)
- O-018: run_consensus_verification.py ran 19 of 29 models and reported PASS;
  it now runs all 29, and computemarket_smt.py gains resolveByTimeout /
  reclaimUnsettled cases plus a negative control
- O-006: the word 'audited' removed from next to Bouncy Castle, twice, after a
  concurrent edit resurrected it
- O-014: prior art named and dated - Algorand's native falcon_verify shipped
  about ten months before AERE's precompiles; the primacy claim is withdrawn
  where it was implied
- bench/ scripts parametrized so they actually run for an outsider (the
  earlier textual sanitization left $STAGING unexpanded inside Python strings)
- AIP-2/AIP-3 errata with measured figures, spec remeasurements at 2026-08-01,
  and the spec-zk-stack retractions (owner is an operational key, not the
  Foundation; 'maximally sound' withdrawn; aggregator V1 deprecated)
The redacted bench-host environment files from the sanitized line are kept
exactly as published; the unredacted local variants are not carried.
2026-08-15 13:52:14 +03:00

47 KiB

AERE Network: Zero-Knowledge Verification Stack

Chain: AERE Network mainnet, chain ID 2800 (Hyperledger Besu QBFT). Status date: 2026-08-01. Every on-chain figure below was re-read from https://rpc.aere.network at head block 11,810,041 on that date. Source of truth for addresses: sdk-js/src/addresses.ts. Contract source: contracts/contracts/. Scope: the on-chain proof-verification layer. This covers SP1 (Groth16 / Plonk), RISC Zero, KZG / EIP-4844, Halo2, recursive proof aggregation, zkML, zk-KYC / zk-compliance, and the storage-proof coprocessor.

This document describes what each verifier proves, the exact on-chain path a proof takes to be accepted, and the honest trust boundary of each. It is deliberately conservative about claims: every capability is marked either live (deployed and demonstrated with an on-chain transaction recorded in the repo) or roadmap / in development.


0. Honesty preamble (read this first)

Six points frame everything below.

  1. This is an application / account-layer proof stack, not a consensus feature. AERE's validators sign classical QBFT; none of the verifiers here change how blocks are produced or finalized. The verifiers are ordinary EVM contracts that any account can call. They add verifiable computation on top of the chain, not inside consensus.

  2. The network is small and centralized today. AERE runs seven QBFT validators operated by a single operator, on a single client (Besu), at a measured mean block interval of 0.5175 s over the last 2,000 blocks. There is no external security audit of the AERE-authored contracts. On-chain usage of these verifiers is thin: the canonical AereProofRegistry reports programCount() = 1 and proofCount() = 1 at head. None of this is hidden; it is the baseline against which the engineering below should be judged.

  3. Address-citation discipline, corrected 2026-08-01. Addresses are printed verbatim only when they appear in the canonical registry sdk-js/src/addresses.ts. An earlier revision of this spec withheld the hex for five contracts (KZG, Halo2, the recursive aggregator, the zkML verifier, the state-root anchor) on the grounds that they were "pending canonical registration". That is no longer true and the omission is corrected here: all five are in the registry today, and their addresses are printed below. Withholding them stopped protecting anything and only kept integrators away from the canonical source. One contract in that group, AereHalo2ProofAnchor, really is still absent from the registry, and is marked as such.

  4. Deprecation is stated, not hidden. Several contracts in this stack have been superseded by corrected redeployments. Where that is so, this spec names the deprecated address, says what is wrong with it, and prints the replacement next to it. A reader who learns from us that something is superseded is better served than one who finds out from the chain.

  5. Ownership language. Where a contract is described as under an "operational deployment account", that is the literal measured state: owner() returns a deployment or operations account rather than the Foundation account 0x0243A4f47D44b40b65D33f20329dE20D00c6f3C3. These contracts are on the path to governance ownership. Nothing about which accounts those are, or what they could reach, belongs in a public spec.

  6. We do not claim uniqueness beyond what is demonstrable. Where a superiority framing is tempting, the honest statement is narrow: AERE hosts a multi-prover verification surface (SP1 plus RISC Zero plus a raw KZG precompile path plus a generated Halo2 verifier) behind a route-by-selector gateway, with a permissionless attestation registry on top. That is unusual to have all in one place; it is not a claim that no other chain can verify any one of these.


1. Architecture: route-by-selector, verify-then-record

Every proof system in the stack follows the same two-layer shape.

Layer 1, the verifier. A concrete verifier (SP1 Groth16, SP1 Plonk, a RISC Zero Groth16 verifier, a generated Halo2 verifier, or the EIP-4844 precompile) takes proof bytes plus public inputs and either returns cleanly or reverts. The canonical SP1 and RISC Zero verifiers follow the Succinct / RISC Zero convention: verifyProof / verify returns nothing and reverts on an invalid proof. AERE's wrapper contracts consistently try/catch that revert and translate it into a typed InvalidProof() error, so a caller never mistakes empty return-data for a failure.

Layer 2, routing and attestation. Concrete verifiers sit behind routers keyed on the first 4 bytes of the proof (a version selector):

  • SP1VerifierGateway routes an SP1 proof to the correct SP1 verifier version by its leading selector, refusing frozen routes.
  • RiscZeroVerifierRouter routes a RISC Zero seal by its leading selector to the matching zkVM-version verifier.

Because routing is by selector, a new prover version is added with a single addRoute / addVerifier call on the router. Downstream application contracts that hold only the router address need no migration when a new prover version ships. AERE's application verifiers (storage proof, zk-KYC, zkML, rollup validity, aggregator) all hold the SP1 gateway address, not a concrete verifier, exactly for this reason.

On top of the routers, AereProofRegistry records each successful verification as a permanent, event-indexed attestation, with permissionless program registration.


2. SP1 (Succinct Labs): Groth16 and Plonk

Contracts (canonical, in addresses.ts):

  • SP1VerifierGateway: 0x9ca479C8c52C0EbB4599319a36a5a017BCC70628
  • SP1VerifierGroth16_v6_1_0: 0xb5456d48bFdA70635c13b6CBE1Ad0310Dc0171aD (current production)
  • SP1VerifierGroth16_v6_0_0: 0xa9BD3020bC9a9614F9e2BC1618153c9fB1890ca6 (prior production, retained)
  • SP1VerifierPlonk_v6_1_0: 0x24a7a85E6D9A2b120F2730880bE7283dFB14d29B

What it proves. SP1 is a RISC-V zkVM. A guest Rust program is compiled to an ELF; its verification key (programVKey, a bytes32) binds that exact ELF. A proof attests that the guest ran to completion on some private witness and committed a specific publicValues byte-string. Groth16 gives constant-size (roughly 260 to 356-byte) proofs at about 300k gas to verify; Plonk avoids a per-circuit trusted setup at a somewhat higher verification cost. Both are routed by the same gateway.

Verification path. SP1VerifierGateway.verifyProof(programVKey, publicValues, proofBytes) reads bytes4(proofBytes[:4]) as the selector, looks up routes[selector], reverts RouteNotFound / RouteIsFrozen on a bad route, and otherwise delegates to the concrete verifier's verifyProof. The Groth16 route selector on chain 2800 is 0x4388a21c (the leading 4 bytes of the v6.1.0 Groth16 verifier's VERIFIER_HASH 0x4388a21c687fdd5f...); the Plonk verifier hash is 0x5a093a2f.... Ownership and trust boundary, measured 2026-08-01. The gateway is Ownable, and owner() returns an operational deployment account, not the Foundation account 0x0243A4f47D44b40b65D33f20329dE20D00c6f3C3. An earlier revision of this spec said "Ownable (Foundation)"; that was wrong, and the contract is on the path to governance ownership.

The same earlier revision drew a stronger conclusion than the code supports: it said the route owner "cannot forge a verification, because the actual cryptographic check happens in the immutable concrete verifier the route points at". Only the first half of that is true. Each concrete verifier is indeed immutable, so no one can change what a given verifier checks. But addRoute(bytes4,address) (selector 0x8c95ff1e in the live 1,746-byte gateway bytecode) and freezeRoute(bytes4) (0x814856f4) mean the route owner chooses which contract performs the check for a given selector. The honest statement is therefore:

A proof accepted through the gateway is exactly as sound as the concrete verifier the route currently points at. Immutability protects the verifier; it does not by itself constrain the route owner. An integrator who needs the stronger guarantee should hold the concrete verifier address (0xb5456d48…, 0x24a7a85E…) directly, or wait for the route to be frozen, or wait for the gateway to sit behind governance. Live routes measured at head: 0x4388a21c to 0xb5456d48…, 0x5a093a2f to 0x24a7a85E…, neither frozen.

This is a real limitation of the router pattern, not of this deployment alone, and the same reasoning applies to RiscZeroVerifierRouter in section 3.

Notes. A legacy v4.0.0-rc.3 SP1 verifier and registry were deployed on 2026-05-31 (recorded in contracts/deployments/zkverify-stack.json) and retain a historical Fibonacci fixture proof (registry proof id 0). Production traffic uses the v6.1.0 gateway above. Every AERE application verifier in this spec that says "SP1" routes through this one gateway.


3. RISC Zero zkVM: Groth16 receipts

Contracts (canonical, in addresses.ts):

  • RiscZeroVerifierRouter: 0x3f7015BC3290e63F7EC68ecF769b00aB296a249C (2,042 bytes; owner() measured at head returns an operational deployment account, not the Foundation account; moving to governance)
  • RiscZeroGroth16Verifier_DEPRECATED: 0x95cB30f3bdb3187f39203A9907bf707Aef07a1FD (do not use)
  • RiscZeroGroth16Verifier: 0xb6fD00D88Bf8B08d6371d2D98E0e239B89Ab5B9D (corrected)
  • RiscZeroVerifierRouter_Corrected: 0x62b96F7211F832f47d8Fc0D8317D5867B0Af43E5
  • AereProofRegistry_Corrected: 0x174F616E2048A71408E2491791ef77cd6913bbEf

What it proves. RISC Zero is a second RISC-V zkVM. A receipt attests that a guest with a given imageId produced a journal whose digest is journalDigest. RiscZeroVerifierRouter.verify(seal, imageId, journalDigest) routes by the seal's leading selector to the concrete Groth16 verifier, which checks the seal against RISC Zero's CONTROL_ROOT / BN254_CONTROL_ID circuit-binding constants (ControlID.sol) for a specific zkVM release (VERSION = "5.0.0-rc.1").

A real bug, honestly recorded. The originally deployed RISC Zero verifier (0x95cB...a1FD) was bootstrapped with a non-canonical control root (0xb1f640...488167) that matches no released risc0 version. That wrong root computes selector 0xc27d1bc0, so the router could never route a genuine risc0 5.0.0-rc.1 receipt (which carries selector 0xef6cb709): every real seal reverted SelectorUnknown. The fix, recorded in contracts/deployments/risc0-verifier-fix.json, deployed a corrected verifier (0xb6fD...5B9D) with the genuine control root 0x4ec911...bde5b04 and selector 0xef6cb709.

Why there is a "corrected" side stack, and the premise that turned out to be wrong. A parallel corrected router (0x62b9...43E5) and registry (0x174F...bbEf) were deployed to record a real RISC Zero proof: a risc0 5.0.0-rc.1 Groth16 factorization proof (guest proves knowledge of a nontrivial factorization of n = 11,917,497,200) was registered and recorded (submit tx 0xf9269dde...396e629, record id 0), with independent public-RPC read-back confirming accept-on-valid and VerificationFailed() on tampered journal or seal. That proof and that read-back are real and stand.

The reason given for building that side stack does not stand. An earlier revision of this spec said the account that built the fix "cannot addVerifier on the Foundation router". Measured at head, owner() on the canonical router 0x3f7015BC… returns that same operational deployment account, so it always could, and it since has. The side stack is therefore historical, not necessary. New integrations should use the canonical router and registry, not 0x62b9…43E5 / 0x174F…bbEf.

Canonical RISC Zero recording is LIVE, not roadmap (corrected 2026-08-01). An earlier revision listed one addVerifier(0xef6cb709, 0xb6fD…5B9D) as "staged but not yet executed", and called canonical R0 recording "roadmap". That transaction has been executed. Measured at head on https://rpc.aere.network:

  • RiscZeroVerifierRouter(0x3f7015BC…).getVerifier(0xef6cb709) returns 0xb6fD00D88Bf8B08d6371d2D98E0e239B89Ab5B9D
  • the verifiers(0xef6cb709) mapping returns the same address
  • AereProofRegistry(0x0A9b0967…).risc0Router() returns 0x3f7015BC3290e63F7EC68ecF769b00aB296a249C
  • the corrected verifier 0xb6fD00D8… carries 4,664 bytes of live code

So the canonical path from a genuine risc0 5.0.0-rc.1 seal to a recorded attestation in the canonical registry is connected end to end. What remains open is usage, not capability: programCount() and proofCount() on the canonical registry are both 1 at head, and that single record is an SP1 Fibonacci fixture, not a RISC Zero proof. Capability live, traffic zero.


4. Multi-prover attestation registry

Contract (canonical): AereProofRegistry: 0x0A9b09677DbE995ACfC0A28F0033e68F068517Ee (7,196 bytes; owner() measured at head returns an operational deployment account, not the Foundation account; moving to governance).

Measured usage, stated up front. The mechanism described below is real and the code is live. The traffic is not. At head, programCount() = 1 and proofCount() = 1. The single registered program is "SP1 Fibonacci (canonical)" and the single recorded proof is the fib(20) = 6765 fixture from block 2,151,318. Every other statement in this section describes a capability, not a volume.

Purpose. A single log that verifies and records proofs from both SP1 and RISC Zero. Its design goals:

  • Verify then record. submitSP1Proof calls ISP1Verifier(sp1Gateway).verifyProof(...) (reverts on invalid), then writes a ProofRecord and emits ProofVerified with the program key indexed. submitRiscZeroProof does the same through IRiscZeroVerifier(risc0Router).verify(...). Off-chain consumers subscribe to ProofVerified filtered by their program of interest.
  • Permissionless program registration. Anyone registers their own SP1 vkey or R0 imageId with a human-readable name plus URL (registerSP1Program / registerRiscZeroProgram). The Foundation can disableProgram a malicious program but cannot disable a legitimate one, and disabling never retroactively invalidates already-recorded proofs.
  • Free verdicts. verifySP1Only / verifyRiscZeroOnly are view functions that give the verdict (revert-on-invalid) without writing a record, for dApps that need the answer but not a permanent attestation.

Both immutables sp1Gateway and risc0Router are set at construction. Because the registry holds the gateway/router (not a concrete verifier), SP1 v7 or a new R0 verifier plug in upstream with no registry migration. The AereProofRegistryV2.sol source in the repo is an equivalent multi-prover registry design; the canonical live registry is the address above.


5. KZG / EIP-4844 point-evaluation verifier

Contract (canonical, in addresses.ts): AereKZGVerifier: 0x6596307BD8f54d9A91FE364EBC3e594F200AC862, 1,988 bytes of live code measured at head. KAT plus on-chain verify tx recorded in contracts/deployments/kzg-verifier.json. An earlier revision of this spec called this contract "deployed, not yet in addresses.ts" and withheld the hex; it has been in the canonical registry (line 386) and the omission is corrected here.

What it proves. That a blob polynomial P committed to by a 48-byte KZG commitment evaluates to y at point z, that is P(z) == y, using the EIP-4844 point-evaluation precompile at address 0x0A. This is the primitive underlying blob-data availability and any KZG-commitment scheme.

Verification path. verifyPointEvaluation(bytes input) forwards the raw 192-byte precompile input (versioned_hash(32) | z(32) | y(32) | commitment(48) | proof(48)) to 0x0A via staticcall. On a valid proof the precompile returns exactly 64 bytes: uint256(FIELD_ELEMENTS_PER_BLOB = 4096) | uint256(BLS_MODULUS). The contract enforces both the success flag and that canonical 64-byte return (rejecting any other length or constants), then records (sender, versionedHash, z, y, block, time) append-only. checkPointEvaluation is the gas-cheap view twin. Because precompiles have no code, the contract deliberately does no extcodesize check; correctness is enforced by the strict canonical-constant return, which no empty account or stray contract can satisfy by chance.

Live evidence. EIP-4844 blob support is active on chain 2800: the precompile was confirmed against the official go-ethereum known-answer vector (pointEvaluation1), and an on-chain verify transaction is recorded (gasUsed about 255,719 per the artifact). No owner, no admin, append-only.

Honest scope. This verifies a KZG opening; it does not by itself prove that a particular blob was posted for a particular transaction. QBFT produces no blobs (BLOBBASEFEE returns 0), so this is a verification primitive available to applications, not a data-availability layer the chain itself uses.


6. Halo2 (bn254 / KZG) verifier plus anchor

Contracts: AereHalo2CubicVerifier (generated verifier) is canonical, in addresses.ts line 387: 0x414Cfe640B2770856d3D7262a7a3729f5c07dC55, 4,804 bytes of live code measured at head. Its companion AereHalo2ProofAnchor (record) is genuinely still absent from addresses.ts; its address, the proof hash and the on-chain verify tx are recorded in contracts/deployments/halo2-cubic.json. An earlier revision of this spec described both as pending registration; that was correct for the anchor and wrong for the verifier.

What it proves. The bound circuit is a real 4-row standard-PLONK circuit proving knowledge of a private x with x^3 + x + 5 == out, where out is the single public instance (demonstrated with out = 35). The verifier is generated by privacy-scaling-explorations/halo2-solidity-verifier (bn254 / KZG commitments, Bdfg21 / SHPLONK batch opening) with the verifying key embedded in bytecode.

Verification path. AereHalo2ProofAnchor.verifyAndRecord(proof, instances) calls IHalo2Verifier(VERIFIER).verifyProof(proof, instances); the generated verifier returns true and reverts on an invalid proof. On success the anchor records (sender, keccak256(proof), keccak256(instances), block, time) append-only. The verifier address is immutable; no owner, no admin. Per the artifact, on-chain verify gas is about 449,517 (proof 1152 bytes, revm gas about 300,144), with valid-accept and tampered-reject both demonstrated.

Honest scope: the SRS is a dev setup, not a ceremony. The KZG structured reference string was generated locally on the AERE build server (ParamsKZG::setup), not from a public trusted-setup ceremony. That is fine for demonstrating that AERE can host and verify a real Halo2 proof on-chain; it is not production-grade for a value-bearing circuit, where a multi-party ceremony SRS would be required. This is the single most important caveat on the Halo2 path and is stated plainly in the artifact.


7. Recursive proof aggregation

Contracts, and which one to integrate against. Both are in addresses.ts and both are live; they are not interchangeable.

Contract Address Live code Status
AereProofAggregator_DEPRECATED_UNENFORCED_INNER_VKEYS 0x6a260238890E740dB12b371E0C5d17a2470F84C5 4,531 bytes DEPRECATED. Do not integrate.
AereProofAggregatorV2 0x944dE720D6C9696Bdf10b6E2F00dd21ABB1A2b4C 4,663 bytes CANONICAL

Why V1 is deprecated, stated plainly. V1 counted how many folded inner programs were recognized and then recorded the aggregation regardless. It never enforced that every inner program was a registered one. The registry entry names it for exactly that: unenforced inner vkeys. V2 rejects an aggregation containing an unregistered inner program.

owner() on both returns the Foundation account 0x0243A4f47D44b40b65D33f20329dE20D00c6f3C3 (measured at head). Aggregation counts at head: V1 = 2, V2 = 1. Aggregation vkey, inner-program mappings and both V1 on-chain records are in contracts/deployments/proof-aggregator.json and proof-aggregator-scale.json.

What it proves. One SP1 Groth16 "proof of proofs." An aggregation guest recursively verified N inner SP1 proofs inside the zkVM via verify_sp1_proof, and committed a single composite digest binding all N (innerVKeyDigest, innerPublicValuesHash) pairs:

composite = sha256( u32_be(n) || (innerVKeyDigest_0 || innerPvHash_0) || ... || (innerVKeyDigest_{n-1} || innerPvHash_{n-1}) )

innerVKeyDigest_i is the KoalaBear (SP1Field) verifying-key digest the recursion verifier actually checked. Binding that, rather than the BN254 on-chain vkey, is the right thing to bind, because it is the digest the recursion actually verified against.

An earlier revision of this spec called the resulting on-chain check "maximally sound". Withdrawn. That phrase described V2's behaviour and was attached to V1's address. On V1 the composite binding above is enforced, so an aggregation cannot claim children it did not fold; but membership of each child in the registered-program set was only counted, never required. "Maximally sound" is not a property V1 has. It is the property V2 was built to add.

Verification path. recordAggregation(aggVKey, innerVKeyDigests[], innerPvHashes[], publicValues, proof):

  1. requires aggVKey is a Foundation-registered aggregation program;
  2. recomputes composite from the supplied inner arrays byte-for-byte with the guest and requires it equals the 32-byte publicValues;
  3. verifies the Groth16 proof through the SP1 gateway (selector 0x4388a21c);
  4. counts how many folded inner programs are recognized (registered via registerInnerProgram, which cross-maps each KoalaBear digest to its published on-chain BN254 vkey and a label) and records the aggregation.

Live evidence. Two real aggregations are recorded on chain 2800: a 3-proof fold (inner programs zkscreen, over18, zkml-mnist; record tx 0xde4c102a..., gasUsed about 387,858) and a 10-proof scale fold of distinct statements (record tx 0xd1fd4d60..., block 8930005, gasUsed about 393,844, recognized = 10/10). Note the gas to verify the aggregate on-chain is roughly flat (about 390k) whether folding 3 or 10 inner proofs; that constant-verification-cost property is the point of recursion. The proving side is not free: the scale artifact records about 526s to produce the aggregation Groth16 and about 988s of inner proving (measured, off-chain). Tampering (mutated proof or a dropped child) reverts.


8. zkML: private machine-learning inference

Contract (canonical, in addresses.ts line 392): AereZKMLVerifier: 0xf1BF15d5018a35D21FBB4Ec868f062DF7C06783c, 3,658 bytes of live code measured at head. Model vkey/hash, accuracy and the on-chain verify tx are in contracts/deployments/zkml-mnist-verifier.json. An earlier revision called this "deployed, not yet in addresses.ts" and withheld the hex; that is corrected here.

What it proves. That a registered ML model classified a private input without revealing that input. The reference model is a genuinely trained, quantized MNIST digit classifier (784 to 256 to 10 integer MLP, ReLU, argmax). The forward pass runs inside the SP1 zkVM on a private 28x28 image; only the predicted digit is made public.

Model binding, and which call path actually enforces it. Each model is registered by the Foundation with programVKey (binds the exact guest ELF, which embeds the quantized weights), modelHash (keccak256 of the weight blob, recomputed inside the zkVM), numClasses, and a measured accuracyBps.

Only the stateful path enforces registration. The free view path does not. This is the single most important integration caveat on this contract and an earlier revision of this spec did not state it. In the live source:

  • submitInference / verifyAndRecord check that the model is registered, and bind chainId, user and modelHash as described below. Use this path if you need "a registered model produced this classification".
  • the public verify(bytes32, bytes, bytes) twin checks only that publicValues is 128 bytes long and that the Groth16 proof is valid, then decodes the class. It does not consult the model registry, does not check chainId, does not check user, does not check modelHash, and records nothing. The contract's own source comment says as much.

So verify answers "some SP1 program with this vkey produced this output", not "the registered model classified this input". An integrator who calls verify and reports "registered model" to a user is over-claiming. If you only have the free path available, treat the vkey you pass in as the thing you are trusting, and pin it yourself. The reference model's measured accuracy is 97.98% (9798 of 10000 correct on the MNIST test set; float baseline 98.01%), recorded on-chain for transparency.

Verification path. submitInference(programVKey, publicValues, proof) requires the model is registered, requires publicValues is exactly 128 bytes = abi.encode(chainId, user, modelHash, predictedClass), verifies the SP1 proof through the gateway, then binds chainId == block.chainid, user == msg.sender, modelHash == registered model, and predictedClass < numClasses, and records the user's latest verified prediction. verify(...) is a stateless view twin. Per the artifact, the reference inference verified on-chain at gasUsed about 326,492; the guest runs about 5.22M cycles using the SP1 keccak precompile to hash the 407,592-byte weight blob (24.5M cycles without it).

Honest scope. This proves inference integrity for one small, quantized model on one dataset. It is a real, measured classifier, not a toy with hand-picked weights (it supersedes an earlier 4-to-8-to-3 toy). It is not a general zkML framework and makes no claim about large models.


9. zk-KYC / zk-compliance

Three related contracts implement "prove a compliance property without putting PII on-chain."

9.1 AereZKScreen (canonical)

Contract: AereZKScreen: 0x3A097A459FD26aC79573aCB5adB51430e473C2f1 (v3; 3,625 bytes live). owner() measured at head returns the Foundation account 0x0243A4f47D44b40b65D33f20329dE20D00c6f3C3, so "Foundation-owned" is accurate here and was re-verified rather than assumed.

The over-18 attribute program is REGISTERED and live (measured 2026-08-01). Other AERE documents have described on-chain registration of the over-18 program as "not yet done, pending a single Foundation signature", and said that submitting an over-18 proof would revert as an unknown program. Measured, that is no longer the case. Reading programs(0x005aa94ac711bc65d0755b3a94f3622a6e8c76e2cdd708c29869587f633343c5) on the live contract returns registered = true with a non-zero authorizedRoot (0xb3b4d506…40b304) and a description recorded on-chain that reads, in part, "SP1 Groth16 zero-knowledge proof that an issuer-attested birth year implies age 18+ (born <= 2008), without revealing the date of birth. No PII on-chain." Because the authorized root is set as well as the registration, the root-binding check described below is armed, not bypassed. The capability is shipped. Usage of it is a separate question and remains thin.

What it proves. That a user passed an off-chain compliance screen (sanctions check, residency / accredited-investor / MiCA-jurisdiction attestation) inside an SP1 zkVM, anchoring only a boolean-style clearance. dApps gate access with isCleared(user, programVKey, minTimestamp); no PII touches the chain.

Verification path plus the critical soundness fix. submitProof(programVKey, publicValues, proof) requires the program is registered, requires publicValues is exactly 128 bytes = abi.encode(chainId, user, attestedAt, extraDataHash), verifies the SP1 proof, then enforces: chainId matches, user == msg.sender, attestedAt <= now, and the anti-downgrade rule attestedAt > clearedAt[user][program]. The root-binding check is the soundness core: each program binds a Foundation-set authorizedRoot, and the proof's extraDataHash must equal that root. Without it, a prover could prove membership in an allowlist of their own making (an "allowlist" containing only themselves), a total break of the compliance claim. The version history in addresses.ts records that earlier deployments had exactly this class of hole (v1 0xE9da9c... dead; v2 0x140572... had a self-clear hole); v3 (the address above) is the root-binding fix, and adds owner setAuthorizedRoot rotation and per-user revoke.

Consumer obligations (documented in-source). dApps MUST pass a real minTimestamp (for example now - freshnessWindow), never 0, and MUST treat clearance as necessary-not-sufficient, AND-ing it with a live sanctions check for sanctions-sensitive flows.

9.2 AereCompliancePool SP1 verifier (canonical)

Contracts: AereCompliancePoolSP1Verifier: 0xE2D3fa91b680E835c971761ba75Fde0204AEF95E; consumed by AereCompliancePoolV2: 0xB144c923572E5Ac1B6B961C4ccfec36917173465.

What it proves. A Privacy-Pools-style compliant withdrawal: that a note is a member of a deposit Merkle root and an approved association root, with a spent-nullifier, recipient, fee, and refund, all without linking the deposit to the withdrawal. The SP1 guest lives at zk-circuits/compliance-pool/program; its vkey is the verifier's immutable PROGRAM_VKEY (0x00aa183a... per addresses.ts).

Verification path. verify(bytes proof, uint256[8] pubs) re-abi-encodes the 8 public inputs [poolId, depositRoot, associationRoot, nullifierHash, recipient, feeRecipient, refund, fee] into the exact struct the guest commits, then staticcalls ISP1Verifier.verifyProof(PROGRAM_VKEY, publicValues, proof) and returns the success bool to fit the pool's IPrivacyPoolVerifier interface. It is a drop-in production replacement for the test MockPrivacyPoolVerifier. The pool itself (AereCompliancePoolV2) is sanctions-gated plus Travel-Rule-bound and, per addresses.ts, currently holds 0 deposits (fresh).

9.3 AereAIProof (provenance, not zk)

Contract: AereAIProof: 0xFf92c669AbF4C1DAE31eBFCC017764036d9D97e6.

Included here for completeness because it is often grouped with the compliance/attestation set, but it is signature-based, not zero-knowledge. Providers register a modelId with a signing key and anchor EIP-712-signed (modelId, inputHash, outputHash, requesterHash, timestamp, nonce) statements. Only hashes are stored (no raw prompts or completions). It gives tamper-evident AI-inference provenance (useful for agentic AERE402 flows and regulated AI audit logs) with a per-model monotonic nonce, a rotate-invalidates-old-nonce-space watermark, and ECDSA.recover against the current signer. It proves who signed what, not a hidden computation.


10. Storage-proof coprocessor

Contracts, and which one to integrate against. All four are in addresses.ts; two of them are deprecated.

Contract Address Live code Status
AereStorageProofVerifier_DEPRECATED_POISONABLE 0xF9a1A183bEb3147D88dA5927301683fEbFb9362E 1,710 bytes DEPRECATED. Do not read getProven() as canonical.
AereStorageProofVerifierV2 0x487021f9aE018657B7335ADC6Fc254717751a1Dd 3,160 bytes CANONICAL
AereStateRootAnchor_DEPRECATED_WIRED_TO_POISONABLE_V1 0x78b40a983E89c91Aefd8A62Be709bDF25ABB57cb see registry DEPRECATED
AereStateRootAnchorV2 0x0b959402283c0D2B9db4FfD7823f99782462e488 2,652 bytes CANONICAL

An earlier revision of this spec called 0xF9a1A183… "canonical, immutable, no owner". Two of those three words are still true and the important one is not. It is immutable and it has no owner. It is not canonical: the canonical registry has named it _DEPRECATED_POISONABLE and says literally "DO NOT read getProven() as canonical; repoint to V2".

Why V1 is deprecated, stated plainly. V1's stored record is keyed on (blockNumber, account, slot) but the record it writes carries whatever stateRoot the proof asserted. Because blockNumber is only metadata to the verifier (see the canonicity gap below), anyone can submit a genuine proof against a non-canonical root and thereby overwrite that key with a value that was never in the canonical chain. The record is poisonable. V2 closes this by binding to the canonicity anchor: AereStorageProofVerifierV2.CANONICAL_ANCHOR() returns 0x0b959402283c0D2B9db4FfD7823f99782462e488, verified in both directions at head.

Everything below describes the shared proof mechanism, which is the same in both versions. Integrate it through V2.

What it proves. That, at a given state root R, an account A's storage slot S held value V. The SP1 guest (aere-storage-proof-program) RLP-decodes and hash-chains the real eth_getProof account-proof and storage-proof nodes of AERE's Merkle-Patricia state trie: it walks stateRoot to storageRoot, then storageRoot to value, panicking on any node whose keccak256 does not match its parent reference. No Foundation-seeded side tree is involved.

Verification path. Public values are exactly 192 bytes = abi.encode(chainId, blockNumber, stateRoot, account, slot, value). submitProof verifies through the SP1 gateway (PROGRAM_VKEY = 0x0015e599...b659), enforces chainId == block.chainid, and records keccak256(blockNumber, account, slot) to {stateRoot, value, at}. submitProofWithExpectedRoot additionally reverts unless the proof's stateRoot == expectedStateRoot, moving the canonicity check to the call site. verify is a stateless view twin.

Live evidence. A first real proof is recorded: AereTreasury (0x687933...6119) slot 0 (its owner) equals the Foundation (0x0243A4...f3C3) at block 8915939, against canonical state root 0x515fce04...4a92f3 (confirmed equal to eth_getBlockByNumber(8915939).stateRoot); submit tx 0xe541c52b...6c54, gasUsed 348,345. Tampered proof or public values revert.

(An earlier revision added "under the EIP-7825 per-transaction cap of 16,777,216" here. That comparison is withdrawn as NOT MEASURED: we have not confirmed the per-transaction cap by reading the live chain. An eth_call carrying gas 0x2000000, which is 33,554,432 and therefore above the stated cap, succeeded without error, and eth_estimateGas does not discriminate. The only conclusive probe would be a transaction above the cap, which we do not send. What we did measure is that the header gasLimit at head is 0x1fffffffffffff = 9,007,199,254,740,991, so a 16,777,216 cap does not come from the block limit and cannot be inferred from it. The 348,345 figure itself is measured and stands on its own.)

Honest scope: the canonicity gap, and how the anchor closes it. The verifier alone proves "V is slot S of A in the trie whose root is stateRoot"; it does not prove that stateRoot is the canonical root of blockNumber (blockNumber is metadata). The consumer must establish canonicity independently. AereStateRootAnchorV2 (0x0b959402…) is the building block for that: it recovers a canonical block hash strictly from the BLOCKHASH opcode (never a caller argument), checks a caller-supplied RLP header against it, and records the header's state root as an anchored, trust-minimized value. BLOCKHASH is live but serves only the last 256 blocks, about 132 s at the measured 0.5175 s block interval.

EIP-2935 IS deployed on chain 2800. Correction of 2026-08-01. An earlier revision of this spec stated twice that "the EIP-2935 history contract is not deployed on chain 2800, so the extended roughly 8191-block window is unavailable". That is false, and it is false in the direction that under-sells the chain: we said we lacked a capability we have. Measured at head on https://rpc.aere.network:

  • eth_getCode at 0x0000F90827F1C53a10cb7A02335B175320002935 returns 83 bytes. The contract is there. It went live with the AerePQC fork at block 9,189,161.
  • the serve window was measured end to end by reading the contract directly with a 32-byte block number. Offsets of 1, 255, 256 and 8,190 blocks behind head each returned exactly the canonical block hash reported by eth_getBlockByNumber for that height. An offset of 8,191 and anything beyond it reverts. That is the 8,191-entry history ring behaving exactly as specified, with one slot held by the in-flight block.

The consequence for this section is that the extended lookback is available, not roadmap. AereHistoryStateRootAnchorV2 (0x4C1F9daD515c7990b65F9833960CA4315216330C, 2,915 bytes, addresses.ts line 391) is the deployed consumer of it, and its HISTORY_STORAGE_ADDRESS() returns 0x0000F90827F1C53a10cb7A02335B175320002935, read from chain.

One open item on that anchor, stated because we found it. AereHistoryStateRootAnchorV2.VERIFIER() returns 0xF9a1A183bEb3147D88dA5927301683fEbFb9362E, which is the storage-proof verifier this same section marks deprecated. The extended-lookback anchor is therefore wired to the deprecated verifier. Do not treat that combination as a canonical record path until it is repointed. This is a real loose end and it is ours.


11. Rollup validity anchor

Contract: AereRollupValidity: 0x38772063572DF94E90351e44ccbBEefD5F497fbd (immutable, no owner).

What it proves. An SP1 Groth16 validity proof that AERE's deterministic rollup executor transitioned prevRoot to postRoot by applying a batch committed as batchHash. Unlike an optimistic rollup with a challenge window, an epoch recorded here is final the instant the proof verifies.

Verification path. Public values are exactly 160 bytes = abi.encode(chainId, prevRoot, postRoot, batchHash, numTxns). submitEpoch(publicValues, proof) verifies through the SP1 gateway (PROGRAM_VKEY = 0x00c39737...84ad, selector 0x4388a21c), binds chainId, enforces canonical-chain continuity prevRoot == latestRoot, then records the epoch and advances latestRoot. Immutables SP1_VERIFIER, PROGRAM_VKEY, GENESIS_ROOT are fixed at deploy; submission is permissionless and gated only by the proof plus continuity, so anyone can advance the tip and no one can fork it. Epoch 0 is recorded from a real proof (submit tx 0xd4f8d858...12a4b0, block 8880188; latestRoot = 0x256fa277...25e3ac, epochCount = 1), verified against the production aere-block-stm executor's state root for the same 5-tx batch.

Honest scope. This proves the state transition of AERE's current bounded-VM executor: a fixed transaction set (Transfer / Sweep / Increment / AmmSwap over a balance/storage map), not arbitrary EVM bytecode. It is a real validity proof of that executor. Nothing built on this contract should imply it proves a full EVM.

Update, 2026-08-01. An earlier revision described replacing the guest VM with a real EVM (revm) inside the zkVM as "a separate multi-quarter effort" still ahead of us. That work shipped. AereEVMValidity 0x1f2CB0ebDBb21500e99868DbF1dB3abCcDd7DF26 (2,095 bytes live) and AereEVMValidityBatch 0x49D30a5999eA5b7f6eFf12196AB006316683Ff3C (2,776 bytes live) each report provenCount() = 1 at head, and the canonical-bound successors AereEVMValidityV2 0x4884ad66… (2,852 bytes) and AereEVMValidityBatchV2 0xe154B899… (3,425 bytes) are live. They are the subject of spec-full-evm-validity.md. The bounded-VM anchor described in this section remains what it is; the full-EVM path is a different, live contract, and anyone reaching for one should know the other exists.


12. Consolidated status and address table

Canonical (verbatim from sdk-js/src/addresses.ts):

Subsystem Contract Address Status
SP1 routing SP1VerifierGateway 0x9ca479C8c52C0EbB4599319a36a5a017BCC70628 Live
SP1 Groth16 (prod) SP1VerifierGroth16_v6_1_0 0xb5456d48bFdA70635c13b6CBE1Ad0310Dc0171aD Live
SP1 Groth16 (prior) SP1VerifierGroth16_v6_0_0 0xa9BD3020bC9a9614F9e2BC1618153c9fB1890ca6 Live, retained
SP1 Plonk SP1VerifierPlonk_v6_1_0 0x24a7a85E6D9A2b120F2730880bE7283dFB14d29B Live
RISC Zero routing RiscZeroVerifierRouter 0x3f7015BC3290e63F7EC68ecF769b00aB296a249C Live; route 0xef6cb709 is set to 0xb6fD00D8… (measured)
RISC Zero Groth16 (corrected) RiscZeroGroth16Verifier 0xb6fD00D88Bf8B08d6371d2D98E0e239B89Ab5B9D Live
RISC Zero Groth16 (bad root) RiscZeroGroth16Verifier_DEPRECATED 0x95cB30f3bdb3187f39203A9907bf707Aef07a1FD Deprecated, do not use
RISC Zero corrected router RiscZeroVerifierRouter_Corrected 0x62b96F7211F832f47d8Fc0D8317D5867B0Af43E5 Live
Multi-prover registry AereProofRegistry 0x0A9b09677DbE995ACfC0A28F0033e68F068517Ee Live, R0 path connected; programCount 1, proofCount 1
Historical side registry AereProofRegistry_Corrected 0x174F616E2048A71408E2491791ef77cd6913bbEf Live (recorded R0 proof id 0). Historical; use the canonical registry
Storage-proof coprocessor AereStorageProofVerifierV2 0x487021f9aE018657B7335ADC6Fc254717751a1Dd Live, CANONICAL
Storage-proof coprocessor V1 AereStorageProofVerifier_DEPRECATED_POISONABLE 0xF9a1A183bEb3147D88dA5927301683fEbFb9362E Deprecated, record is poisonable. Do not use
Storage-proof canonicity anchor AereStateRootAnchorV2 0x0b959402283c0D2B9db4FfD7823f99782462e488 Live, CANONICAL
Extended-lookback anchor (EIP-2935) AereHistoryStateRootAnchorV2 0x4C1F9daD515c7990b65F9833960CA4315216330C Live; VERIFIER() still points at the deprecated V1
zk-KYC screen AereZKScreen 0x3A097A459FD26aC79573aCB5adB51430e473C2f1 Live (v3, root-binding)
Compliance-pool SP1 verifier AereCompliancePoolSP1Verifier 0xE2D3fa91b680E835c971761ba75Fde0204AEF95E Live
Compliance pool AereCompliancePoolV2 0xB144c923572E5Ac1B6B961C4ccfec36917173465 Live (0 deposits)
AI-inference provenance (signed, not zk) AereAIProof 0xFf92c669AbF4C1DAE31eBFCC017764036d9D97e6 Live
Rollup validity anchor AereRollupValidity 0x38772063572DF94E90351e44ccbBEefD5F497fbd Live (bounded VM)

Formerly withheld, now printed. An earlier revision of this spec listed the six rows below as "deployed but NOT yet in the canonical registry" and deliberately omitted their hex. Five of the six have been in addresses.ts; the policy of omission was protecting nothing and hiding the canonical source from integrators. Code sizes measured at head.

Subsystem Contract Address Registry Live code Status
KZG / EIP-4844 AereKZGVerifier 0x6596307BD8f54d9A91FE364EBC3e594F200AC862 addresses.ts:386 1,988 B Live
Halo2 verifier AereHalo2CubicVerifier 0x414Cfe640B2770856d3D7262a7a3729f5c07dC55 addresses.ts:387 4,804 B Live, dev SRS, not a ceremony
Halo2 anchor AereHalo2ProofAnchor see contracts/deployments/halo2-cubic.json absent not measured Deployed, genuinely still unregistered
Recursive aggregation AereProofAggregatorV2 0x944dE720D6C9696Bdf10b6E2F00dd21ABB1A2b4C addresses.ts:396 4,663 B Live, CANONICAL
Recursive aggregation V1 AereProofAggregator_DEPRECATED_UNENFORCED_INNER_VKEYS 0x6a260238890E740dB12b371E0C5d17a2470F84C5 addresses.ts:395 4,531 B Deprecated. Do not use
zkML AereZKMLVerifier 0xf1BF15d5018a35D21FBB4Ec868f062DF7C06783c addresses.ts:392 3,658 B Live, 97.98% MNIST; free verify path checks no registration
State-root anchor AereStateRootAnchorV2 0x0b959402283c0D2B9db4FfD7823f99782462e488 addresses.ts:390 2,652 B Live, 256-block BLOCKHASH window
Extended-lookback anchor AereHistoryStateRootAnchorV2 0x4C1F9daD515c7990b65F9833960CA4315216330C addresses.ts:391 2,915 B Live, 8,191-block EIP-2935 ring

13. Roadmap summary (explicit "not done yet")

Four items that this list carried on 2026-07-11 have since been done, and are moved out of "not done yet" into the closed list below rather than quietly deleted.

Still not done:

  • Halo2 production SRS: replace the local dev SRS with a multi-party ceremony SRS before any value-bearing Halo2 circuit. This remains the single most important caveat in the stack.
  • Register AereHalo2ProofAnchor in sdk-js/src/addresses.ts. It is the one contract in this spec that is genuinely deployed and genuinely unregistered.
  • Repoint AereHistoryStateRootAnchorV2.VERIFIER() away from the deprecated 0xF9a1A183… and onto AereStorageProofVerifierV2 0x487021f9…, so the extended-lookback path is not wired to a poisonable record.
  • Freeze or govern the verifier routes. While addRoute / addVerifier remain available to a route owner, "verified through the gateway" is only as strong as the current route (section 2). Freezing the production routes, or moving the routers behind governance, converts that from a trust assumption into a guarantee.
  • Move the routers and the canonical proof registry off the operational deployment account and onto governance. Measured today, owner() on 0x9ca479C8…, 0x3f7015BC… and 0x0A9b0967… is not the Foundation account.
  • Usage. The canonical registry has one program and one proof. Everything in this spec is capability; almost none of it is traffic. We would rather say that than let a mechanism description read as a volume claim.

Closed since the previous revision, with the measurement that closed it:

  • Canonical RISC Zero recording, staged and unsigned DONE. getVerifier(0xef6cb709) on the canonical router returns 0xb6fD00D8…; AereProofRegistry.risc0Router() returns the canonical router. Section 3.
  • Promote KZG / Halo2 verifier / aggregator / zkML / state-root anchor into addresses.ts DONE. All five are in the registry; their addresses are printed in section 12.
  • Historical-state canonicity blocked because EIP-2935 is not deployed The premise was false. EIP-2935 is live at 0x0000F908…2935 with 83 bytes of code, and the 8,191-block ring was measured end to end. Section 10.
  • Full validity rollup: replace the bounded-VM executor guest with revm inside the zkVM DONE. AereEVMValidity 0x1f2CB0ebDBb21500e99868DbF1dB3abCcDd7DF26 (2,095 bytes) and AereEVMValidityBatch 0x49D30a5999eA5b7f6eFf12196AB006316683Ff3C (2,776 bytes) are live with provenCount() = 1 each, and the QBFT-canonical-bound successors AereEVMValidityV2 0x4884ad6617e320735b65D31915d1aBC884768D9B (2,852 bytes) and AereEVMValidityBatchV2 0xe154B8993FB54dB4418e03ef4a04894f29E690aE (3,425 bytes) are live as well. See spec-full-evm-validity.md. Section 11's "honest next step" paragraph is updated accordingly.

Decentralization baseline, unchanged: seven validators, one operator, one client, no external audit, thin usage. That is what we are, it is published, and it is the frame for everything above.


Every address printed above is verbatim from sdk-js/src/addresses.ts, with one exception that is marked as absent from it (AereHalo2ProofAnchor). Copying the hex correctly is not enough: where the registry marks an entry deprecated, this spec now carries that label too, because an earlier revision copied addresses faithfully and left their status behind. Performance figures (gas, cycles, accuracy, proving times) are quoted from repo deployment artifacts and are measured single-run values, not benchmarked averages. Code sizes, owners, route targets, counters and the EIP-2935 window were re-read from https://rpc.aere.network on 2026-08-01 at head block 11,810,041.