AZIP-14: Multiple Roots per Epoch in Outbox#33
Open
spalladino wants to merge 1 commit into
Open
Conversation
This AZIP modifies the L1 `Outbox` contract so that each epoch can accumulate multiple L2-to-L1 message roots rather than overwriting a single root every time a partial epoch proof is submitted. The nullifier bitmap that tracks consumed messages remains a single bitmap per epoch and is shared across all roots of that epoch. This eliminates a race condition in which a user's L1 exit transaction, built against a partial-proof root, reverts because a later proof for the same epoch has overwritten that root before the user's transaction is mined.
spalladino
added a commit
to AztecProtocol/aztec-packages
that referenced
this pull request
May 21, 2026
Implements the spec in AztecProtocol/governance#33. The Outbox now stores an ordered list of roots per epoch instead of overwriting on each insert, while keeping a single nullifier bitmap per epoch shared across all roots. This eliminates the race where a later partial-proof root overwrites an earlier one and invalidates a user's pending L1 exit transaction. API changes: - consume(message, epoch, rootIndex, leafIndex, path) - getRootData(epoch, rootIndex) - RootAdded(epoch, rootIndex, root) - getRootCount(epoch) (new view) All in-repo L1 callers (TokenPortal, UniswapPortal, and tests) are updated. Off-chain TS bindings and yarn-project consumers are intentionally out of scope; they are tracked as follow-up work in the AZIP.
spalladino
added a commit
to AztecProtocol/aztec-packages
that referenced
this pull request
May 22, 2026
Implements the spec in AztecProtocol/governance#33. The L1 Outbox stores up to MAX_CHECKPOINTS_PER_EPOCH roots per epoch, keyed by the partial-proof depth (numCheckpointsInEpoch, 1-indexed). The nullifier bitmap is shared across all roots of an epoch, so a message consumed against one partial proof cannot be replayed against a later extending proof. This removes the race where a user's pending L1 exit reverted because a later proof overwrote the root the witness was built against. L1 contracts - Outbox.insert(epoch, numCheckpointsInEpoch, root) appends into a fixed-size `bytes32[MAX_CHECKPOINTS_PER_EPOCH]` keyed by depth - 1. - Outbox.consume(message, epoch, numCheckpointsInEpoch, leafIndex, path) selects the root slot by depth. - New view getRoots(epoch) returns the full fixed array; getRootData gains the depth parameter. - TokenPortal/UniswapPortal updated to thread numCheckpointsInEpoch. Off-chain / yarn-project - OutboxContract wrapper exposes getRoots and the new consume/getRootData signatures; getEpochRootStorageSlot computes `keccak256(epoch || 0) + (numCheckpointsInEpoch - 1)`. - stdlib computeL2ToL1MembershipWitness now takes an OutboxRootsReader, queries getRoots, picks the smallest covering depth, slices messagesInEpoch to that K before building the tree, cross-checks against the on-chain root, and returns numCheckpointsInEpoch in the witness. - aztec.js portal_manager and end-to-end harness/tests thread the new parameter through. Tests that previously computed the witness before the proof landed now resolve the epoch via getTxReceipt before advancing. - RollupCheatCodes.insertOutbox + EpochTestSettler thread numCheckpointsInEpoch so the local-network settler still works.
spalladino
added a commit
to AztecProtocol/aztec-packages
that referenced
this pull request
May 22, 2026
Implements the spec in AztecProtocol/governance#33. The L1 Outbox stores up to MAX_CHECKPOINTS_PER_EPOCH roots per epoch, keyed by the partial-proof depth (numCheckpointsInEpoch, 1-indexed). The nullifier bitmap is shared across all roots of an epoch, so a message consumed against one partial proof cannot be replayed against a later extending proof. This removes the race where a user's pending L1 exit reverted because a later proof overwrote the root the witness was built against. L1 contracts - Outbox.insert(epoch, numCheckpointsInEpoch, root) appends into a fixed-size `bytes32[MAX_CHECKPOINTS_PER_EPOCH]` keyed by depth - 1. - Outbox.consume(message, epoch, numCheckpointsInEpoch, leafIndex, path) selects the root slot by depth. - New view getRoots(epoch) returns the full fixed array; getRootData gains the depth parameter. - TokenPortal/UniswapPortal updated to thread numCheckpointsInEpoch. Off-chain / yarn-project - OutboxContract wrapper exposes getRoots and the new consume/getRootData signatures; getEpochRootStorageSlot computes `keccak256(epoch || 0) + (numCheckpointsInEpoch - 1)`. - stdlib computeL2ToL1MembershipWitness now takes an OutboxRootsReader, queries getRoots, picks the smallest covering depth, slices messagesInEpoch to that K before building the tree, cross-checks against the on-chain root, and returns numCheckpointsInEpoch in the witness. - aztec.js portal_manager and end-to-end harness/tests thread the new parameter through. Tests that previously computed the witness before the proof landed now resolve the epoch via getTxReceipt before advancing. - RollupCheatCodes.insertOutbox + EpochTestSettler thread numCheckpointsInEpoch so the local-network settler still works.
spalladino
added a commit
to AztecProtocol/aztec-packages
that referenced
this pull request
May 22, 2026
Implements the spec in AztecProtocol/governance#33. The L1 Outbox stores up to MAX_CHECKPOINTS_PER_EPOCH roots per epoch, keyed by the partial-proof depth (numCheckpointsInEpoch, 1-indexed). The nullifier bitmap is shared across all roots of an epoch, so a message consumed against one partial proof cannot be replayed against a later extending proof. This removes the race where a user's pending L1 exit reverted because a later proof overwrote the root the witness was built against. L1 contracts - Outbox.insert(epoch, numCheckpointsInEpoch, root) appends into a fixed-size `bytes32[MAX_CHECKPOINTS_PER_EPOCH]` keyed by depth - 1. - Outbox.consume(message, epoch, numCheckpointsInEpoch, leafIndex, path) selects the root slot by depth. - New view getRoots(epoch) returns the full fixed array; getRootData gains the depth parameter. - TokenPortal/UniswapPortal updated to thread numCheckpointsInEpoch. Off-chain / yarn-project - OutboxContract wrapper exposes getRoots and the new consume/getRootData signatures; getEpochRootStorageSlot computes `keccak256(epoch || 0) + (numCheckpointsInEpoch - 1)`. - stdlib computeL2ToL1MembershipWitness now takes an OutboxRootsReader, queries getRoots, picks the smallest covering depth, slices messagesInEpoch to that K before building the tree, cross-checks against the on-chain root, and returns numCheckpointsInEpoch in the witness. - aztec.js portal_manager and end-to-end harness/tests thread the new parameter through. Tests that previously computed the witness before the proof landed now resolve the epoch via getTxReceipt before advancing. - RollupCheatCodes.insertOutbox + EpochTestSettler thread numCheckpointsInEpoch so the local-network settler still works.
spalladino
added a commit
to AztecProtocol/aztec-packages
that referenced
this pull request
May 22, 2026
Implements the spec in AztecProtocol/governance#33. The L1 Outbox stores up to MAX_CHECKPOINTS_PER_EPOCH roots per epoch, keyed by the partial-proof depth (numCheckpointsInEpoch, 1-indexed). The nullifier bitmap is shared across all roots of an epoch, so a message consumed against one partial proof cannot be replayed against a later extending proof. This removes the race where a user's pending L1 exit reverted because a later proof overwrote the root the witness was built against. L1 contracts - Outbox.insert(epoch, numCheckpointsInEpoch, root) appends into a fixed-size `bytes32[MAX_CHECKPOINTS_PER_EPOCH]` keyed by depth - 1. - Outbox.consume(message, epoch, numCheckpointsInEpoch, leafIndex, path) selects the root slot by depth. - New view getRoots(epoch) returns the full fixed array; getRootData gains the depth parameter. - TokenPortal/UniswapPortal updated to thread numCheckpointsInEpoch. Off-chain / yarn-project - OutboxContract wrapper exposes getRoots and the new consume/getRootData signatures; getEpochRootStorageSlot computes `keccak256(epoch || 0) + (numCheckpointsInEpoch - 1)`. - stdlib computeL2ToL1MembershipWitness now takes an OutboxRootsReader, queries getRoots, picks the smallest covering depth, slices messagesInEpoch to that K before building the tree, cross-checks against the on-chain root, and returns numCheckpointsInEpoch in the witness. - aztec.js portal_manager and end-to-end harness/tests thread the new parameter through. Tests that previously computed the witness before the proof landed now resolve the epoch via getTxReceipt before advancing. - RollupCheatCodes.insertOutbox + EpochTestSettler thread numCheckpointsInEpoch so the local-network settler still works.
spalladino
added a commit
to AztecProtocol/aztec-packages
that referenced
this pull request
May 22, 2026
Implements the spec in AztecProtocol/governance#33. The L1 Outbox stores up to MAX_CHECKPOINTS_PER_EPOCH roots per epoch, keyed by the partial-proof depth (numCheckpointsInEpoch, 1-indexed). The nullifier bitmap is shared across all roots of an epoch, so a message consumed against one partial proof cannot be replayed against a later extending proof. This removes the race where a user's pending L1 exit reverted because a later proof overwrote the root the witness was built against. L1 contracts - Outbox.insert(epoch, numCheckpointsInEpoch, root) appends into a fixed-size `bytes32[MAX_CHECKPOINTS_PER_EPOCH]` keyed by depth - 1. - Outbox.consume(message, epoch, numCheckpointsInEpoch, leafIndex, path) selects the root slot by depth. - New view getRoots(epoch) returns the full fixed array; getRootData gains the depth parameter. - TokenPortal/UniswapPortal updated to thread numCheckpointsInEpoch. Off-chain / yarn-project - OutboxContract wrapper exposes getRoots and the new consume/getRootData signatures; getEpochRootStorageSlot computes `keccak256(epoch || 0) + (numCheckpointsInEpoch - 1)`. - stdlib computeL2ToL1MembershipWitness now takes an OutboxRootsReader, queries getRoots, picks the smallest covering depth, slices messagesInEpoch to that K before building the tree, cross-checks against the on-chain root, and returns numCheckpointsInEpoch in the witness. - aztec.js portal_manager and end-to-end harness/tests thread the new parameter through. Tests that previously computed the witness before the proof landed now resolve the epoch via getTxReceipt before advancing. - RollupCheatCodes.insertOutbox + EpochTestSettler thread numCheckpointsInEpoch so the local-network settler still works.
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 AZIP modifies the L1
Outboxcontract so that each epoch can accumulate multiple L2-to-L1 message roots rather than overwriting a single root every time a partial epoch proof is submitted. The nullifier bitmap that tracks consumed messages remains a single bitmap per epoch and is shared across all roots of that epoch. This eliminates a race condition in which a user's L1 exit transaction, built against a partial-proof root, reverts because a later proof for the same epoch has overwritten that root before the user's transaction is mined.