A Python reference verifier (are_verify.py) and vector generators
(are_generate.py, are_real_vectors.py, are_extra_vectors.py) for the Attested-Read Envelope spec
(../specs/ARE/README.md). The verifier implements all
10 steps of §Verification Algorithm in order and returns ACCEPT or
REJECT@stepN.
v0.5 upgraded the suite from MINIMAL-preset fixtures to mainnet fidelity: a
real hexary Merkle-Patricia verifier, the full 17-field ExecutionPayloadHeader,
and a real-data vector built from a live mainnet eth_getProof + a real
Altair/Fulu LightClient finality_update + bootstrap (a real 512-member sync
committee and a real aggregate BLS signature). v0.6 then closed the last two
coverage gaps (deep historical_summaries ancestor + provider_sig) and added a
measured benchmark harness. See What changed in v0.6 and What changed in
v0.5.
python3 -m venv .venv
.venv/bin/pip install -r ../requirements.txt
cd reference
../.venv/bin/python are_generate.py # (re)generate the MINIMAL vectors
../.venv/bin/python are_real_vectors.py # (re)build accept_mainnet_real.json from ../vectors/real-data/
../.venv/bin/python are_extra_vectors.py # (re)build the deep-ancestor + provider_sig vectors
../.venv/bin/python run_vectors.py # MUST print "10/10 passed"
../.venv/bin/python benchmark.py # measured timings (prints a Markdown table)v0.6 discharges the two coverage caveats v0.5 left open, and adds the missing benchmark harness:
- Deep
historical_summariesancestor path (step 7b) now vector-locked. The v0.5 verifier had aREJECT@step7stub on the deep branch. It is now a real check: it composes thehistorical_summariesgeneralized index (are_constants.historical_summaries_leaf_gindex) and verifies a real SHA-256 Merkle branch of the read block root againstfinalized_header.state_root. New accepting vectoraccept_deep_ancestor.json(finalized−read gap = 20000 > 8192 → deep path; depth-41 branch). Tampering the proof rejects at step 7. provider_sig(step 10) now vector-locked with REAL ed25519. The v0.5 verifier had aREJECT@step10stub. Step 10 now resolves the verifying key via an independent trust path (VerifierConfig.resolve_provider_key, never the envelope), and verifies a real ed25519 (RFC 8032, pycryptodome) signature overhash_tree_root(envelope_without_provider_sig)(are_verify.envelope_signing_root). New accepting vectoraccept_provider_sig.json(dispute/audit mode) + bonus rejectreject_bad_provider_sig.json(tampered sig → REJECT@step10).- Benchmarks (
benchmark.py) — measured, environment stated; see the table below. Replaces the v0.5 "indicative only" note.
Suite is now 10/10. secp256k1 (sig_alg=1) remains declared-but-unimplemented
in this reference (pycryptodome has no secp256k1; no new dependency added) — an
honesty note, not a soundness gap (step-10 control flow is identical via ed25519).
Run python benchmark.py to reproduce. Captured environment:
Apple M2 · 8 GB RAM · Python 3.12.2 · macOS 26.5 (arm64) · py_ecc pure-Python BLS12-381 · native CPython. (WASM/native-blst not measured — see note.)
| Operation | Median | Notes |
|---|---|---|
(a) BLS FastAggregateVerify, 512-committee |
3818.89 ms | real mainnet aggregate, 510/512 participants, py_ecc pure-Python |
| (b) hexary MPT account+storage verify | 0.07 ms | real WETH eth_getProof (9 account + 7 storage nodes) |
(c) SSZ hash_tree_root(anchor) |
0.07 ms | full ConsensusAnchor incl. 17-field exec header |
| (d) full end-to-end envelope verify | 3783.90 ms | all 10 steps; dominated by (a) |
| Regime | Per-read cost | Meaning |
|---|---|---|
| Clustered (anchor reused) | 0.07 ms | BLS anchor verified once; each additional read at the same block pays only MPT |
| Scattered (distinct blocks) | 3818.96 ms | one BLS aggregate verify per distinct block + its MPT |
Clustered batching is ~56000× cheaper per read than scattered: the BLS verify (the dominant cost) is amortized across all reads sharing one anchor — the quantitative justification for the spec's "default to batch envelopes" SHOULD.
The pure-Python
py_eccBLS verify dominates and is an UPPER BOUND. A production verifier reusing an audited light-client core (Helios,blstnative or WASM) verifies the same aggregate in single-digit milliseconds. Native-blst and WASM figures are future work and are deliberately not fabricated here.
Per the task's two acceptable routes, this suite uses route (1) — real mainnet
data (the gold standard), not the full-synthetic fallback. Every byte in
accept_mainnet_real.json comes from live mainnet, captured once and preserved
under ../vectors/real-data/ for reproducibility:
| Input | Source | Captured fact |
|---|---|---|
eth_getProof (WETH account + storage slot 0) |
ethereum-rpc.publicnode.com |
account proof (9 hexary nodes) + storage proof (7 nodes) at exec block 25404693 |
LightClient finality_update |
ethereum-beacon-api.publicnode.com |
attested slot 14640721, finalized slot 14640640, signature slot 14640722, real sync_aggregate (510/512), full execution payloads, finality_branch |
LightClient bootstrap |
ethereum-beacon-api.publicnode.com (block root 0xb0cfd5f1…31a7) |
the real 512 current_sync_committee pubkeys (the trusted config) |
- Source EL state root:
0xf6c792621f2a4df8b83abcaf1c72aff30c571fcfb14533ebefd5327b2b53f2a1(exec block 25404693). The real WETH account proof roots to exactly this root. - Fork: Fulu,
current_version = 0x06000000, sync-committee period 1787. - No hash is ever fabricated. The real-data vector contains only bytes that were on mainnet; the verifier accepts them end-to-end.
The v0.4 caveats named three simplifications that blocked draft. All three are
now discharged:
-
REAL hexary Merkle-Patricia verifier (
are_mpt.py). The v0.4 single-account collapsed trie is removed._walk()is a genuine EIP-1186 walk: it follows thekeccak256(key)nibble path through branch (17-item), extension (2-item, even/odd hex-prefix), and leaf (2-item) nodes, dereferencing each child node by its keccak hash against the proof set, and handling inlined (<32-byte) nodes. It verifies a real mainnet WETH account proof and storage-slot proof. Exclusion verifies a genuine empty-branch-slot or diverging-leaf/extension terminus; a truncated/hash-inconsistent proof raises and is rejected (never silently treated as absence). The MINIMAL fixtures now build a real 2-leaf hexary trie and verify through the same walker — there is no longer any "simplified MPT" code path. -
FULL 17-field
ExecutionPayloadHeader(are_ssz.py). Replaces the v0.4 reduced 4-leaf model. All Deneb+ fields (parent_hash,fee_recipient,state_root,receipts_root,logs_bloomasByteVector[256],prev_randao,block_number,gas_limit,gas_used,timestamp,extra_dataasByteList[32],base_fee_per_gasasuint256,block_hash,transactions_root,withdrawals_root,blob_gas_used,excess_blob_gas) are merkleized with real SHA-256. Itshash_tree_rootmatches the real beaconbody_rootvia the realexecution_branchatEXECUTION_PAYLOAD_GINDEX = 25. -
MAINNET preset —
SYNC_COMMITTEE_SIZE = 512, real BLS aggregation (are_constants.py,are_real_vectors.py). The real-data vector verifies the real aggregate signature over 510 of the 512 real committee pubkeys withpy_eccFastAggregateVerify, the real Fulufork_version 0x06000000(fromepoch_of(signature_slot − 1)), the realgenesis_validators_root, and the realfinality_branchatFINALIZED_ROOT_GINDEX = 169(Electra+/Fulu). The verifier now selects the finalized-root gindex by the attested header's fork (105 pre-Electra, 169 Electra+).
The recorded intermediates for the mainnet vector:
signing_root = 0x000fd44e62c984d7c201807dc607688818fdca4716cadbd827ce34b393551af0,
bound_state_root = 0xf6c792621f2a4df8b83abcaf1c72aff30c571fcfb14533ebefd5327b2b53f2a1.
- BLS12-381, Ethereum
G2ProofOfPossessionciphersuiteBLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_(viapy_ecc). G1 pubkeys, G2 signatures,FastAggregateVerify. Mainnet vector = a real 510/512 aggregate. - Keccak-256 (Ethereum variant, via
pycryptodome) + RLP for a real hexary EIP-1186 account/storage proof walk. - SSZ
hash_tree_rootover SHA-256 for all beacon objects, the fullExecutionPayloadHeader, theConsensusAnchor, the signing domain (compute_domain/compute_signing_root/ForkData/SigningData), and every Merkle branch (verified by generalized index;merkleizeuses correct zero-subtree padding for any limit). - Real mainnet
genesis_validators_root0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95, real fork schedule (Altair…Fulu), real WETH proof, real sync committee.
No hash is ever fabricated. Every parent node in every Merkle tree is a real SHA-256 (consensus) or Keccak-256 (MPT) reduction of its children.
These do not affect the cryptographic soundness exercised by the mainnet vector; they are coverage/honesty notes:
- The two MINIMAL accept/reject fixtures use seeded synthetic Merkle siblings
for
execution_branch/finality_branch/ancestor_proof(there is no full beacon state to derive true siblings from). The root each branch proves to is still the real SHA-256 reduction along the gindex path, andverify_merkle_branchis the real consensus check — the mainnet vector uses entirely real branches. - MINIMAL preset =
SYNC_COMMITTEE_SIZE = 32. Kept as a fast smoke test (BLS over 32 keys is quick); the mainnet vector exercises the real 512. - Deep-
historical_summariesancestor path (step 7b) is now vector-locked byaccept_deep_ancestor.json, BUT that vector is a full-fidelity seeded synthetic at the Deneb preset (real Keccak/SSZ/BLS, real SHA-256historical_summariesbranch; only the outerBeaconStatesiblings are seeded random bytes). A real-mainnet deep proof is not reachable: the public beacon node blocks/eth/v2/debug/beacon/states(full-state download), the only source of truehistorical_summariessiblings. This matches the fidelity tier of the near-ancestoraccept_balance_finalized.json. No hash is fabricated — every parent is a real SHA-256 reduction along the real gindex. provider_sig(step 10) is vector-locked byaccept_provider_sig.jsonwith real ed25519 (RFC 8032).secp256k1(sig_alg=1) is declared by the spec but not implemented in this reference (no secp256k1 in pycryptodome; no new dependency added) — a coverage note, not a soundness gap.
| File | Preset | Expected | Exercises |
|---|---|---|---|
accept_mainnet_real.json |
MAINNET | ACCEPT |
REAL FINALIZED WETH balance + storage slot 0; real 512-committee aggregate (510 participants), real hexary MPT, full ExecutionPayloadHeader, real finality_branch @169 |
accept_balance.json |
MINIMAL | ACCEPT |
OPTIMISTIC balance read; records signing_root + bound_state_root; 25/32 participants |
accept_balance_finalized.json |
MINIMAL | ACCEPT |
FINALIZED, near-ancestor state.block_roots path |
accept_deep_ancestor.json |
MINIMAL | ACCEPT |
FINALIZED, deep-ancestor state.historical_summaries path (gap 20000 > 8192); real SHA-256 depth-41 branch |
accept_provider_sig.json |
MINIMAL | ACCEPT |
step 10: real ed25519 provider_sig over htr(envelope_without_provider_sig), independent key resolution, dispute mode |
reject_quorum_too_low.json |
MINIMAL | REJECT@step3 |
16/32 participation (2·16 == 32, not > 32) |
reject_bad_bls.json |
MINIMAL | REJECT@step4 |
quorum met, corrupted aggregate signature |
reject_unproven_absence.json |
MINIMAL | REJECT@step8 |
presence==0 for an address that resolves to an empty branch slot (proven absent) |
reject_mixed_root_batch.json |
MINIMAL | REJECT@step9 |
batch read declaring a state_root != bound_state_root |
reject_bad_provider_sig.json |
MINIMAL | REJECT@step10 |
tampered ed25519 provider_sig rejected in dispute mode |
Each vector is {description, preset, envelope, anchor, expected}. Rejects carry
the failing step in expected.result.
The MINIMAL vectors derive all randomness from random.seed(42) (committee keys
- synthetic siblings), so re-running
are_generate.pyis byte-reproducible. The MAINNET vector is data-reproducible:are_real_vectors.pyrebuilds it deterministically from the fixed real inputs in../vectors/real-data/(no RNG; real bytes in, verified vector out).
are_constants.py— MINIMAL/MAINNET presets, real fork schedule (Altair…Fulu), gindices, slot/epoch helpers.are_ssz.py— SSZ merkleization (zero-subtree padding),BeaconBlockHeader, full 17-fieldExecutionPayloadHeader,verify_merkle_branch.are_mpt.py— Keccak-256, RLP, real hexary MPT walk + synthetic 2-leaf trie builders for MINIMAL.are_bls.py— BLS sign /FastAggregateVerify/ aggregate.are_sig.py—provider_sig(step 10): real ed25519 sign/verify (RFC 8032).are_verify.py— the 10-step verifier + domain/signing-root +envelope_signing_root.are_codec.py— vector JSON (de)serialization (full exec header).are_generate.py— seeded MINIMAL vector generator (the original 6).are_real_vectors.py— MAINNET real-data vector generator (consumes../vectors/real-data/).are_extra_vectors.py— deep-ancestor + provider_sig vector generator (v0.6).benchmark.py— measured timing harness (BLS/MPT/SSZ/e2e, clustered-vs-scattered).run_vectors.py— loads all vectors (both presets), asserts expected == actual.