Skip to content

feat: add tx effects tree root to the block header - #25132

Draft
spalladino wants to merge 15 commits into
nextfrom
spl/tx-effect-root
Draft

feat: add tx effects tree root to the block header#25132
spalladino wants to merge 15 commits into
nextfrom
spl/tx-effect-root

Conversation

@spalladino

@spalladino spalladino commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Context

Proving that a note (or any effect) belonged to a given tx from a block header currently requires replaying the checkpoint's sponge blob over every tx's effect data — up to 24,576 fields of preimage. This adds a per-block commitment so "tx X was included in block N and produced exactly effects E" is provable with a handful of hashes (≤16 worst case), revealing only the field of interest.

Design

Tx effect hash (structured, not flat). Each variable-length field of the tx effect is hashed on its own over its slice of the existing blob-field encoding (create_tx_blob_data / TxEffect.toBlobFields), at its actual runtime length — no new serialization format:

sub_<field>    = poseidon2(DOM_SEP__TX_EFFECT_FIELD_HASH, <field's blob-encoding slice>)
                 // note_hashes, nullifiers, l2_to_l1_msgs, public_data_writes,
                 // private_logs, public_logs, contract_class_logs; 0 if the field is empty

tx_effect_hash = poseidon2(DOM_SEP__TX_EFFECT_HASH,
                   [tx_start_marker, transaction_fee, ...the 7 sub-hashes])   // 9 fields

leaf           = poseidon2(DOM_SEP__TX_EFFECT_LEAF, [tx_hash, tx_effect_hash])

The tx_start_marker is the existing first blob field, which packs the revert code, every array count, and the total field count — so all lengths are bound and the 0 empty-field sentinel is unambiguous. Structuring the hash this way keeps selective proofs cheap: proving one note hash needs only the note-hashes slice as preimage, with the other six sub-hashes as opaque witness values.

Tree shape. The leaves are accumulated bottom-up through the existing rollup proving tree: tx base emits the leaf, tx merge combines pairwise with poseidon2(DOM_SEP__TX_EFFECTS_TREE, [left, right]), with no zero-skipping (there are no padding txs, so every leaf is a real tx). The result is the same greedily-filled wonky tree as the proving tree itself, whose shape is pinned in-circuit by assert_rollups_filled_greedily — a pure function of the tx count, so the prover has no freedom over it (e.g. 3 txs → H(H(l0,l1),l2), 5 txs → H(H(H(l0,l1),H(l2,l3)),l4)). An empty block has root 0; a single-tx block's root is the leaf, unhashed. Leaves and internal nodes use different domain separators, so no leaf/node second-preimage confusion is possible.

Header position. tx_effects_tree_root sits immediately after sponge_blob_hash (both are effect commitments), growing BLOCK_HEADER_LENGTH from 22 to 23:

last_archive | state | sponge_blob_hash | tx_effects_tree_root | global_variables | total_fees | total_mana_used

Approach

  • Leaf computed in the tx base circuits from the same per-field blob-encoding slices absorbed into the sponge blob (the slice builders were factored so both consumers share one definition); accumulated through tx merge; block root writes the value into the header. The root is deliberately not exposed in BlockRollupPublicInputs, checkpoint PIs, or L1 — it is committed via the header hash only, and TS recomputes it locally wherever headers are assembled (prover builders, archiver blob reconstruction, TXE).
  • TS mirrors: TxEffect.computeTxEffectHash/Leaf, Body.computeTxEffectsTreeRoot. Cross-language test vectors pin TS↔Noir (empty header hash, tx effect leaf) and C++↔Noir (genesis header hash).
  • The private-logs blob slice is built by an unconstrained hint verified with read-only accesses: constructing it in-circuit produced a bb RAM block with ~72k accesses, over the 65,535 small-range-constraint cap, aborting write_vk.

API changes

New node API for consuming the root: getTxEffectMembershipWitness(txHash) returns { blockNumber, root, leafIndex, siblingPath } — a variable-depth witness against BlockHeader.txEffectsTreeRoot. The archiver computes each block's leaves once at sync time and persists them (atomically with the block row; ARCHIVER_DB_VERSION 7 → 8), so answering a witness request only rebuilds the internal nodes from stored leaves, verifies the root against the stored header, and checks the requested tx's own leaf against the effects it serves. UnbalancedMerkleTreeCalculator gained an async-hasher factory (createAsync) for the poseidon tree.

Breaking changes

  • BLOCK_HEADER_LENGTH 22 → 23, so every tx hash, VK, contract artifact, and the genesis constants change. Oracle interface version 30 → 31 (the block header oracle return type grew); PXE data schema version 13 → 14 (pre-existing PXE DBs are re-initialized on next open). Migration note included.
  • Mainnet/testnet compatibility pins updated to the new VK tree root, protocol contracts hash, and genesis archive roots, following the precedent of previous protocol-breaking PRs.
  • The pinned protocol/mock circuit builds were removed (stale after this change). The standard-contracts pin is untouched and its bytecode is now ABI-incompatible (the pinned contracts predate the header change), so e2e cannot pass until the standard contracts are re-pinned and redeployed — that is a deliberate, separate human decision.
  • Circuit sample inputs (Prover.toml) updated; all fixtures in CI's execute list pass locally. private-kernel-init-simulated's fixture (not executed by CI) remains stale pending the e2e regeneration path.

Fixes A-1635

The tx-effects-tree-root change modifies the protocol types crate, which
invalidates both the protocol and mock circuit pinned builds.
Adds the structured tx-effect hash (per-field sub-hashes over the blob
encoding slices, 9-field outer hash, tx-hash-bound leaf), the tree node
accumulator, four domain separators, and the tx_effects_tree_root field
in BlockHeader. BLOCK_HEADER_LENGTH 22->23.
Tx base emits the tx-effect leaf in TxRollupPublicInputs, tx merge
accumulates pairwise without zero-skipping, and block root writes the
accumulated root into the new BlockHeader field (0 for empty blocks,
unhashed leaf for single-tx blocks).
Adds the zero field to the C++ genesis header preimage and updates the
hand-maintained GENESIS_BLOCK_HEADER_HASH and GENESIS_ARCHIVE_ROOT.
Runtime-offset writes into the 1088-field private-logs slice lowered to
a bb RAM block with ~72k accesses, over the 65535 small-range-constraint
cap, aborting write_vk for both tx base circuits. Build the slice in an
unconstrained hint and verify with dynamic reads (ROM), mirroring
create_tx_blob_data. Encoding unchanged.
The extra block header field grows every reset variant by 2-8k gates.
Adds txEffectsTreeRoot to BlockHeader and TxRollupPublicInputs, the
structured TxEffect hash/leaf helpers over the shared blob-slice
encodings, Body.computeTxEffectsTreeRoot, Noir conversions, and
cross-language test vectors (empty header hash and tx effect leaf
pinned between TS and Noir). Real assembly sites stay at zero until
the next commit wires them.
Prover header builders, the archiver blob reconstruction path, and TXE
block creation now derive the root from the block's tx effects; the
checkpoint orchestrator verifies the built header through a single
root-computing path.
Regenerates l1-contracts checkpoint fixtures and PXE store snapshots,
adds the missing txEffectsTreeRoot entry to the PXE block header oracle
wire mapping, and bumps ORACLE_VERSION_MAJOR since the block header
oracle return type grew.
The new block header field changes every VK, the protocol contracts
hash, and the genesis archive roots, and grows the persisted header
encoding, so pre-existing PXE databases are selected away via the
schema version bump.
…ount

Adds the new header/tx-rollup field to the committed Prover.toml sample
inputs (regenerated from a mock orchestrator run where the anchor header
hash is validated in-circuit) and rejects TxEffect construction with
more contract class logs than the protocol maximum.
@spalladino
spalladino marked this pull request as draft August 6, 2026 20:07
Adds getTxEffectMembershipWitness(txHash) to the node, backed by an
archiver resolver that rebuilds the block's tx effects tree, verifies
the root against the stored header, and returns the leaf index and
variable-depth sibling path. UnbalancedMerkleTreeCalculator gains an
async-hasher factory so the poseidon tree shares one shaping routine.
Computes each block's tx-effect leaves once when the block is stored
(atomically with the block row) instead of recomputing every leaf on
each membership witness request. The resolver rebuilds only the
internal nodes from the stored leaves and verifies the requested tx's
own leaf against the effects it serves. ARCHIVER_DB_VERSION 7->8.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant