Skip to content

feat: tx effects tree including node - #25381

Merged
AztecBot merged 1 commit into
nextfrom
ir/tx-effects-tree-node
Sep 7, 2026
Merged

feat: tx effects tree including node#25381
AztecBot merged 1 commit into
nextfrom
ir/tx-effects-tree-node

Conversation

@IlyasRidhuan

@IlyasRidhuan IlyasRidhuan commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

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.

@IlyasRidhuan
IlyasRidhuan force-pushed the ir/tx-effects-tree-node branch 2 times, most recently from 3fca92d to 46461d8 Compare September 2, 2026 09:25
@IlyasRidhuan
IlyasRidhuan removed the request for review from LeilaWang September 2, 2026 09:55
@IlyasRidhuan
IlyasRidhuan force-pushed the ir/tx-effects-tree-node branch from 46461d8 to 680a1ba Compare September 3, 2026 08:59
@IlyasRidhuan
IlyasRidhuan requested review from iAmMichaelConnor and removed request for charlielye September 3, 2026 09:26
@IlyasRidhuan
IlyasRidhuan changed the base branch from ir/tx-effects-tree to next September 3, 2026 09:27
@IlyasRidhuan IlyasRidhuan changed the title feat: tx effects tree node edition feat: tx effects tree including node Sep 3, 2026
@IlyasRidhuan
IlyasRidhuan force-pushed the ir/tx-effects-tree-node branch from 680a1ba to 9e30066 Compare September 4, 2026 09:40
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)*
@AztecBot
AztecBot force-pushed the ir/tx-effects-tree-node branch from 12770d8 to 94b2edb Compare September 4, 2026 15:19
@AztecBot
AztecBot enabled auto-merge September 4, 2026 15:19
@AztecBot
AztecBot added this pull request to the merge queue Sep 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 4, 2026
@IlyasRidhuan
IlyasRidhuan force-pushed the ir/tx-effects-tree-node branch from 94b2edb to acef4fc Compare September 6, 2026 14:55
@IlyasRidhuan
IlyasRidhuan added this pull request to the merge queue Sep 7, 2026
@github-merge-queue
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
AztecBot force-pushed the ir/tx-effects-tree-node branch from f2118d7 to ae2aace Compare September 7, 2026 13:12
@AztecBot
AztecBot enabled auto-merge September 7, 2026 13:12
@AztecBot
AztecBot added this pull request to the merge queue Sep 7, 2026
Merged via the queue into next with commit 5961faa Sep 7, 2026
16 checks passed
@AztecBot
AztecBot deleted the ir/tx-effects-tree-node branch September 7, 2026 14:03
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.

3 participants