Skip to content

AZIP-14: Multiple Roots per Epoch in Outbox#33

Open
spalladino wants to merge 1 commit into
AztecProtocol:mainfrom
spalladino:spl/azip-14
Open

AZIP-14: Multiple Roots per Epoch in Outbox#33
spalladino wants to merge 1 commit into
AztecProtocol:mainfrom
spalladino:spl/azip-14

Conversation

@spalladino
Copy link
Copy Markdown
Contributor

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 spalladino requested a review from a team May 20, 2026 13:36
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.
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