feat: tx effects tree including node - #25381
Merged
Merged
Conversation
IlyasRidhuan
force-pushed
the
ir/tx-effects-tree
branch
from
September 2, 2026 07:58
cf49c08 to
2272bf7
Compare
IlyasRidhuan
force-pushed
the
ir/tx-effects-tree-node
branch
2 times, most recently
from
September 2, 2026 09:25
3fca92d to
46461d8
Compare
IlyasRidhuan
force-pushed
the
ir/tx-effects-tree-node
branch
from
September 3, 2026 08:59
46461d8 to
680a1ba
Compare
IlyasRidhuan
requested review from
iAmMichaelConnor
and removed request for
charlielye
September 3, 2026 09:26
IlyasRidhuan
force-pushed
the
ir/tx-effects-tree-node
branch
from
September 4, 2026 09:40
680a1ba to
9e30066
Compare
IlyasRidhuan
added a commit
that referenced
this pull request
Sep 4, 2026
## Human-written subsection - Some renaming for clarity, because I was getting confused. - Some significant constraint improvements (re-using hashing that was already being done) - It rolled-in a constraint optimisation (and code simplification) that removes a call to an inefficient/complex call to an unconstrained function. - Added some `<=` constraints on some array lengths (which already happen in the kernels, but belt and braces sounds good since it's very cheap). - More tests (lots more adversarial tests) ## Circuit change size **This PR changes 534 actual production Noir code lines relative to #25381: 226 additions and 308 deletions across 14 circuit source files.** This excludes dedicated test files, inline test modules, blank lines, and comment-only lines. Under conventional diff accounting, which includes production comments and blank lines, the same non-test Noir diff is 679 changed lines: 278 additions and 401 deletions. Neither figure counts TypeScript/Labs code, patch-container lines, fixtures, `Prover.toml`, or any test code. The net deletion is chiefly the removed maximum-sized flat blob hint and its field-by-field validation loops. ## Pretty diagrams https://gist.github.com/AztecBot/7d8a176090d75364f01beda1783f50c3 ## Context This is a stacked review-fix PR against [#25381](#25381), not an alternative implementation of the tx-effects tree feature. The base PR adds a structured tx-effects-tree commitment, carries its accumulating root through the recursive rollups, commits the final per-block root in `BlockHeader`, and adds the Labs/node membership API. This PR keeps that design and addresses the constraint cost, duplicated encoding path, local soundness assumptions, terminology, and test gaps found during review. Ilyas rebased the six base commits onto `next` while this stack was under review. All six base commits match their previous versions one-for-one under `git range-diff`. This stack is rebased onto the new [#25381](#25381) head `9e30066ab1537c1e7e38ce2abc9a44e032f5c24b`; all 24 commits in this stack are patch-identical to their pre-rebase versions. ## What changes relative to [aztec-packages#25381](#25381) The per-change figures below are isolated A/B measurements and are not additive because the optimizations overlap. The combined, end-to-end saving is **162,881 private tx-base gates and 246,682 public tx-base gates**. ### 1. Save 48,112 private / 95,920 public tx-base gates with canonical variable-length Poseidon2 **Constraint saving: 48,112 private tx-base gates and 95,920 public tx-base gates in the isolated in-situ A/B.** `poseidon2_hash_subarray_with_separator` now delegates to the Poseidon library's constrained variable-length hash: ```noir let inputs_with_separator = [separator.to_field()].concat(input); poseidon::poseidon2::Poseidon2::hash(inputs_with_separator, in_len + 1) ``` The separator remains the first absorbed field and `in_len + 1` remains the length-bound IV input, so this is value-equivalent to the previous hand-rolled sponge path. It avoids a second protocol-local implementation of the same primitive and is substantially cheaper for the maximum-sized category arrays used here. Prefix-length equivalence is tested for lengths 0 through 8, in addition to the existing fixed-length comparison. ### 2. Save 126,811 gates per tx-base by reusing the already-constrained contract-class-log hash **Constraint saving: 126,811 gates on each private and public tx-base circuit in the isolated A/B.** The base PR re-hashes the roughly 3,000-field contract-class-log array when constructing the tx-effect category commitment, even though tx-base has already: - verified the kernel-provided padded log hash against those fields; - verified the contract address and log length; and - required the unused tail to be zero. This PR threads that already-verified hash from both private/public tx-effect builders into the composer. The contract-class-log category preimage becomes: ```text [contract_address, padded_contract_class_log_hash] ``` rather than re-hashing the full padded log inside the tx-effects commitment. The tx start marker still binds the runtime log length. Consequently, the new preimage binds the same constrained data while avoiding a duplicate maximum-sized Poseidon computation. The reused hash is carried from the private/public tx-effect builders to the commitment composer as composer-owned data. ### 3. Save 22,532 private / 58,465 public tx-base gates by removing the whole-transaction blob hint **Constraint saving: 22,532 private tx-base gates and 58,465 public tx-base gates in the exact sound-alternative A/B.** The base implementation constructs a maximum-sized, concatenated tx-blob array in an unconstrained function and then checks that prover-supplied duplicate field-by-field against `TxEffect` before feeding it to the DA sponge. This PR computes a small `TxBlobDataSections` value once and shares it between the two consumers: - the DA sponge absorbs the already-constrained fields/sections in canonical serialization order; and - the tx-effects-tree leaf hashes the same category sections. The absorbed field stream remains exactly: ```text tx start marker, tx hash, fee, note hashes, nullifiers, L2-to-L1 messages, public-data writes, private logs, public logs, contract-class logs ``` Therefore this does **not** change blob encoding, blob boundaries, checkpoint replay, or the final `SpongeBlob` value. It removes only the duplicate maximum-sized representation and the constraints needed to prove that duplicate equal to the source arrays. `tx_blob_data.nr` looks large textually because the old monolithic hint and its repeated validation loops are deleted. The effective change is 169 additions / 267 deletions (net −98 lines). The replacement consists of: - the section carrier and one place for total-length/start-marker bookkeeping; - direct fixed-index encoders for public-data writes and the single contract-class log; - the existing private-log compaction hint, now isolated and explicitly validated; and - adversarial tests for that remaining hint. The usual unconstrained-arrangement optimization still applies to private logs, whose runtime offsets would otherwise form an impractically large RAM block; this PR retains that hint and validates every used field. ### 4. Constrain log lengths where they are consumed The base code relied on terminal-kernel validation to ensure private-log and contract-class-log lengths did not exceed their fixed arrays. These lengths drive runtime offsets in the blob/category encoders, so that assumption is now enforced locally before the offsets are advanced. For the remaining unconstrained private-log compaction hint, validation checks: - every included log length; - every included field; - the number of included logs; and - the final encoded field count. This prevents oversized lengths or a malicious hint from skipping unconstrained positions that could reach either the sponge or category commitment. ### 5. Make accumulation and commitment terminology precise The base PR's names obscured which commitment layer a value belonged to, and in one case called a multi-field category a “field.” This PR renames each layer consistently in Noir and Labs: - `tx_effect_field_hash` / `compute_tx_effect_field_hash` / `DOM_SEP__TX_EFFECT_FIELD_HASH` become `tx_effect_category_hash` / `compute_tx_effect_category_hash` / `DOM_SEP__TX_EFFECT_CATEGORY_HASH`. The function hashes all fields in one variable-length category—note hashes, nullifiers, messages, writes, or one of the log categories—not one field. “Category” states both the content and its role as one of seven fixed positions in the next preimage. - `field_hashes` becomes `tx_effect_category_hashes`. These are the seven hashes for one tx's seven effect categories, so the expanded name removes both the “one field” implication and uncertainty about what the array belongs to. - `compute_tx_effect_hash_inputs` becomes `compute_tx_effect_categories_hash_inputs`. It prepares the marker and seven category hashes consumed by the categories-hash layer; it does not prepare a generic serialization or the final tree leaf. - `tx_effect_hash` / `compute_tx_effect_hash` / `computeTxEffectHash` / `DOM_SEP__TX_EFFECT_HASH` become `tx_effect_categories_hash` / `compute_tx_effect_categories_hash` / `computeTxEffectCategoriesHash` / `DOM_SEP__TX_EFFECT_CATEGORIES_HASH`. The output is specifically the hash of the seven category hashes plus the tx start marker and fee. Calling it the generic “tx effect hash” hid that structure and was easy to confuse with either `tx_hash` or the complete leaf. - `tx_effect_leaf` / `compute_tx_effect_leaf` / `computeTxEffectLeaf` / `DOM_SEP__TX_EFFECT_LEAF` become `tx_effects_tree_leaf` / `compute_tx_effects_tree_leaf` / `computeTxEffectsTreeLeaf` / `DOM_SEP__TX_EFFECTS_TREE_LEAF`. The value is not merely a leaf-shaped hash of a `TxEffect`; it is explicitly the leaf type of the tx-effects tree and is domain-separated from that tree's internal nodes. - `TxRollupPublicInputs.tx_effects_tree_root` becomes `TxRollupPublicInputs.accumulated_tx_effects_tree_root`. At tx-base the field contains one `tx_effects_tree_leaf`; each recursive tx-merge replaces it with the node hash of its two child subtree roots. It is therefore an accumulating leaf/subtree value until block-root finishes the fold. `BlockHeader.tx_effects_tree_root` deliberately keeps its name because that field does contain the final root for the whole block. - `NUM_TX_EFFECT_FIELD_HASHES` becomes `NUM_TX_EFFECT_CATEGORIES`, and `NUM_TX_EFFECT_HASH_FIELDS` becomes `TX_EFFECT_CATEGORIES_HASH_PREIMAGE_LENGTH`. The first constant counts semantic categories, so it follows the existing `NUM_` convention; the second counts fields in one hash preimage, so it follows the existing `_LENGTH` convention. Both move beside the other cross-language protocol constants. In lifecycle form: ```text tx-base: tx_effects_tree_leaf -> TxRollupPublicInputs.accumulated_tx_effects_tree_root tx-merge: left/right accumulated roots -> accumulated_tx_effects_tree_root block-root: final accumulated root -> BlockHeader.tx_effects_tree_root ``` The renamed domain separators intentionally change their derived numeric values. Noir and TypeScript are updated together, and the new values are cross-pinned. This is being done before the feature lands; there is no compatibility promise to the unmerged values from the base branch. Comments now distinguish this per-block tree from state trees and from the checkpoint-wide blob sponge. They also document the variable-depth consumer rule: a membership verifier must recompute/validate the leaf under the leaf domain separator rather than accept an untrusted bare node value as a leaf. ### 6. Expand positive, boundary, adversarial, and cross-language coverage New or strengthened tests cover: - all seven category commitments and scalar bindings (`tx_hash`, fee, revert code); - equal-count cross-category swaps; - empty categories and empty effects; - every short variable-length prefix against the fixed-length Poseidon result; - maximum-sized note hashes, nullifiers, messages, public-data writes, private logs, public logs, and contract-class logs; - wrong private-log hint fields and lengths; - dropped/duplicated logs and under/overstated log counts; - oversized private/class-log lengths; - unchanged flat blob field order versus section-by-section sponge absorption; - reverted and non-reverted public transactions; - canonical greedy tx-tree shapes for 2, 3, 4, 5, 6, and 7 transactions; - leaf/node domain separation, including zero children; and - Noir/TypeScript commitment pins for small, empty, maximum-size, and three-transaction fixtures. The Labs changes are carried as four normal patch-series commits (`0006`–`0009`) because this repository consumes the Labs tree through the pinned submodule/patch mechanism. Those patch files are containers for the TypeScript mirror and tests, not four additional runtime implementations. ## Constraint impact **Overall, this PR removes 162,881 private tx-base gates (5.79%) and 246,682 public tx-base gates (5.24%) relative to the rebased [aztec-packages#25381](#25381) head.** Measured with the same pinned `nargo` (`1.0.0-beta.26+40d6574`) and the same `bb-avm` binary/UltraHonk IPA flags on both sides: | Circuit | #25381 rebased head | This PR | Delta | | --- | ---: | ---: | ---: | | private tx-base ACIR opcodes | 313,057 | 239,795 | −73,262 (−23.40%) | | private tx-base gates | 2,813,175 | 2,650,294 | **−162,881 (−5.79%)** | | public tx-base ACIR opcodes | 400,738 | 252,104 | −148,634 (−37.09%) | | public tx-base gates | 4,706,865 | 4,460,183 | **−246,682 (−5.24%)** | | tx-merge gates | — | 1,489,439 | unchanged by the rebase/refactor | | block-root gates | — | 1,552,608 | unchanged by the rebase/refactor | | single-tx block-root gates | — | 771,829 | unchanged by the rebase/refactor | Relative to `next` before the tx-effects-tree feature, the final remaining tx-base cost is approximately +20.6k private gates (+0.78%) and +102.7k public gates (+2.36%). That remaining cost is the structured, independently openable category commitment the feature intentionally adds, rather than duplicate computation. These are deterministic circuit-size measurements, not local wall-clock proving benchmarks. ## Compatibility and deliberate changes - **Blob encoding and `SpongeBlob` output:** unchanged. - **Tx tree shape and greedy recursive accumulation:** unchanged. - **`BlockHeader` layout relative to #25381:** unchanged. - **Tx-effects leaf/root values relative to #25381:** intentionally changed because of the renamed domain separators and the class-log category preimage; Noir and TypeScript are updated and cross-pinned together. - **Historic membership semantics:** unchanged: the block header commits the per-block root, the header hash is committed into the archive tree, and consumers must validate both the leaf preimage and the relevant historical header/archive membership. ## Verification performed after rebasing ```text git range-diff (old #25381 six commits vs rebased six commits): 6/6 exact git range-diff (old review stack vs rebased stack): 24/24 exact git diff --check: passed nargo fmt --check --package types: passed nargo fmt --check --package rollup_lib: passed nargo test --package types --no-fuzz: 438 passed; 3 fuzz tests deliberately skipped nargo test --package rollup_lib --no-fuzz tx_effects_tree_root: 10 passed nargo execute rollup_tx_base_private: passed nargo execute rollup_tx_base_public: passed nargo execute rollup_tx_merge: passed nargo execute rollup_block_root: passed nargo execute rollup_block_root_single_tx: passed labs-patches lifecycle: 14/14 checks passed labs patch-series application check: passed ``` The full repository/Labs TypeScript matrices are left to CI; local Labs dependencies are not installed in this checkout. ## Review notes and scope The associated audit-invariant updates are tracked separately in [AztecProtocol/claudebox#2445](AztecProtocol/claudebox#2445). That document records the tx membership, effect-field binding, canonical tree shape/domain separation, historical header/archive authentication, and Noir/TypeScript conformance obligations introduced by the base feature. --- *Created by [claudebox](https://claudebox.work/v2/sessions/c852026bbea6f24b/jobs/53) · group: `slackbot` · requested by Mike (@iAmMichaelConnor) · [Slack thread](https://aztecfoundation.slack.com/archives/D0B2N7W1WJD/p1788347586185419?thread_ts=1788347586.185419&cid=D0B2N7W1WJD)*
iAmMichaelConnor
approved these changes
Sep 4, 2026
AztecBot
force-pushed
the
ir/tx-effects-tree-node
branch
from
September 4, 2026 15:19
12770d8 to
94b2edb
Compare
AztecBot
enabled auto-merge
September 4, 2026 15:19
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Sep 4, 2026
IlyasRidhuan
force-pushed
the
ir/tx-effects-tree-node
branch
from
September 6, 2026 14:55
94b2edb to
acef4fc
Compare
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Sep 7, 2026
This is basically the PR from #25332 but with additional node code. Large diffs as a result of regen patches -------------- Adds a tx effects unbalanced tree to the rollup, enabling more efficient proofs of tx effects. Today, proving "tx X was included in this block and produced effects E" means replaying the whole checkpoint's blob sponge. With this root, it takes a single membership proof against one header field. ### Summary #### Structured leaf hash - Each variable-length field of a TxEffect (note hashes, nullifiers, l2l1 msgs, public data writes, private logs, public logs, contract class logs) is hashed independently over that field's portion of the existing blob encoding. - The tx start marker, transaction fee, and the seven field sub-hashes (from above) combine into a tx effect hash; - This hash is combined with the tx hash to form the leaf (compute_tx_effect_leaf). #### Wiring - tx_base emits the leaf as TxRollupPublicInputs.tx_effects_tree_root - tx_merge combines children via accumulate_tx_effects_tree_root - block_root writes the result into BlockHeader.tx_effects_tree_root. **Blob encoding refactor.** - The per-field blob-slice builders (create_public_data_writes_blob_fields, create_private_logs_blob_fields, create_contract_class_logs_blob_fields) are extracted out of `tx_blob_data.nr` so the same formats are used for the blob encoding and the new per-field hashes ### Breaking / versioning - BlockHeader and TxRollupPublicInputs both grow by one field (`tx_effects_tree_root`) - Genesis header hash and archive root are recomputed (header serialization changed). - ORACLE_VERSION_MAJOR 30 → 31 (noir-contracts and aztec-nr), TXE_ORACLE_VERSION_MAJOR 8 → 9. - C++ world_state.cpp adds the matching zero field when hashing the initial (genesis) block header.
AztecBot
force-pushed
the
ir/tx-effects-tree-node
branch
from
September 7, 2026 13:12
f2118d7 to
ae2aace
Compare
AztecBot
enabled auto-merge
September 7, 2026 13:12
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is basically the PR from #25332 but with additional node code. Large diffs as a result of regen patches
Adds a tx effects unbalanced tree to the rollup, enabling more efficient proofs of tx effects.
Today, proving "tx X was included in this block and produced effects E" means replaying the whole checkpoint's blob sponge. With this root, it takes a single membership proof against one header field.
Summary
Structured leaf hash
Wiring
Blob encoding refactor.
tx_blob_data.nrso the same formats are used for the blob encoding and the new per-field hashesBreaking / versioning
tx_effects_tree_root)