Skip to content

Commit 516daa1

Browse files
committed
test(fast-inbox): rewrite l1_to_l2_inbox_drift for the streaming inbox (A-1384)
The test encoded the pre-flip protocol: it asserted an L1-to-L2 message had no membership witness during a proposed-chain drift and could not be consumed until the chain re-reached the message's insertion-time checkpoint. Post-flip messages carry a compact L1-assigned leaf index and stream into the tree in insertion order, so there is no per-message assigned checkpoint and the witness legitimately appears as the chain catches up. Rewrite the scenario to assert the genuinely regression-worthy post-flip invariants: after the rollup prunes the drifted chain, the message is still re-consumed on the new chain (bucket and rolling-hash state surviving the L2 reorg) from both private and public scope, and its witness leaf index still equals the L1-assigned global index. Drop the witness-absent and cannot-consume-early probes and the fragile proven-block destructure; retitle and refresh the scenario comments.
1 parent cfcfa75 commit 516daa1

1 file changed

Lines changed: 67 additions & 75 deletions

File tree

yarn-project/end-to-end/src/single-node/cross-chain/l1_to_l2_inbox_drift.test.ts

Lines changed: 67 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { generateClaimSecret } from '@aztec/aztec.js/ethereum';
33
import { Fr } from '@aztec/aztec.js/fields';
44
import type { Logger } from '@aztec/aztec.js/log';
55
import type { AztecNode } from '@aztec/aztec.js/node';
6-
import { TxExecutionResult } from '@aztec/aztec.js/tx';
76
import type { Wallet } from '@aztec/aztec.js/wallet';
87
import type { BlockNumber } from '@aztec/foundation/branded-types';
98
import { timesAsync } from '@aztec/foundation/collection';
@@ -19,11 +18,13 @@ import { type L1ToL2MessageScope, createL1ToL2MessageHelpers } from './message_t
1918

2019
jest.setTimeout(300_000);
2120

22-
// L1→L2 messaging via Inbox: inbox checkpoint drift after a rollup reorg. Uses CrossChainMessagingTest
21+
// L1→L2 messaging via Inbox: an L1→L2 message survives a rollup prune. Uses CrossChainMessagingTest
2322
// (prod sequencer, pipelining preset: ethSlot=4s, aztecSlot=12s, inboxLag=2, minTxsPerBlock=1,
24-
// aztecProofSubmissionEpochs=2, aztecEpochDuration=4) with EpochTestSettler for auto-proving and
25-
// CrossChainTestHarness for L1↔L2 token portal bridging. The drift scenario runs over private and
26-
// public scope via it.each, all sharing one node stood up once in beforeAll.
23+
// aztecProofSubmissionEpochs=2, aztecEpochDuration=4) with EpochTestSettler for auto-proving. Post-flip
24+
// (AZIP-22) messages carry a compact L1-assigned index and stream in by insertion order rather than
25+
// pinning to a checkpoint, so a message inserted while the proposed chain drifts must still be
26+
// re-consumed with a stable index after the chain prunes back. Runs over private and public scope via
27+
// it.each, sharing one node stood up once in beforeAll.
2728
describe('single-node/cross-chain/l1_to_l2_inbox_drift', () => {
2829
let t: CrossChainMessagingTest;
2930

@@ -35,7 +36,6 @@ describe('single-node/cross-chain/l1_to_l2_inbox_drift', () => {
3536

3637
let sendMessageToL2: ReturnType<typeof createL1ToL2MessageHelpers>['sendMessageToL2'];
3738
let advanceBlock: ReturnType<typeof createL1ToL2MessageHelpers>['advanceBlock'];
38-
let waitForMessageFetched: ReturnType<typeof createL1ToL2MessageHelpers>['waitForMessageFetched'];
3939
let waitForMessageReady: ReturnType<typeof createL1ToL2MessageHelpers>['waitForMessageReady'];
4040

4141
// Whether explicit mark-as-proven calls are honored. The inbox-drift scenario flips this to
@@ -73,7 +73,7 @@ describe('single-node/cross-chain/l1_to_l2_inbox_drift', () => {
7373
({ logger: log, wallet, user1Address, aztecNode } = t);
7474
({ contract: testContract } = await TestContract.deploy(wallet).send({ from: user1Address }));
7575

76-
({ sendMessageToL2, advanceBlock, waitForMessageFetched, waitForMessageReady } = createL1ToL2MessageHelpers({
76+
({ sendMessageToL2, advanceBlock, waitForMessageReady } = createL1ToL2MessageHelpers({
7777
t,
7878
aztecNode,
7979
wallet,
@@ -127,108 +127,100 @@ describe('single-node/cross-chain/l1_to_l2_inbox_drift', () => {
127127
}
128128
};
129129

130-
// Inbox checkpoint number can drift on two scenarios: if the rollup reorgs and rolls back its own
131-
// checkpoint number, or if the inbox receives too many messages and they are inserted faster than
132-
// they are consumed. In this test, we mine several checkpoints without marking them as proven until
133-
// we can trigger a reorg, and then wait until the message can be processed to consume it.
134-
const canConsumeMessageAfterInboxDrift = async (scope: L1ToL2MessageScope) => {
130+
// Post-flip (AZIP-22), L1→L2 messages carry a compact, L1-assigned global leaf index and are streamed
131+
// into the L2 tree in insertion order (subject to inbox lag and per-block/checkpoint caps) rather than
132+
// being pinned to a fixed checkpoint. This scenario stresses that message state survives an L2 reorg:
133+
// we let the proposed chain drift by mining several unproven checkpoints, insert a message during the
134+
// drift, then force the rollup to prune back to the pre-drift block. After the prune, the message must
135+
// still be re-consumed on the new chain and its witness leaf index must remain the L1-assigned index.
136+
const canConsumeMessageAfterRollupPrune = async (scope: L1ToL2MessageScope) => {
135137
// Stop the background epoch test settler so the drift scenario below can proceed without
136138
// an auto-prover racing it.
137139
await t.epochTestSettler?.stop();
138140

139141
// Reset the L1 proof window by marking the current pending tip as proven, so L1's prune
140-
// deadline doesn't fire mid-test before we finish mining the 4 drift checkpoints below.
142+
// deadline doesn't fire mid-test before we finish mining the drift checkpoints below.
141143
await markAsProven();
142144

143-
// Stop proving
145+
// Snapshot the block we will prune back to, then stop proving so the proposed chain can drift.
144146
const lastProven = await aztecNode.getBlockNumber();
145-
const [checkpointedProvenBlock] = await aztecNode.getBlocks(lastProven, 1, {
146-
includeL1PublishInfo: true,
147-
includeAttestations: true,
148-
onlyCheckpointed: true,
149-
});
150-
log.warn(`Stopping proof submission at checkpoint ${checkpointedProvenBlock.checkpointNumber} to allow drift`);
147+
log.warn(`Stopping proof submission at block ${lastProven} to allow drift`);
151148
markProvenEnabled = false;
152149

153150
// Mine several checkpoints to ensure drift
154151
log.warn(`Mining blocks to allow drift`);
155152
await timesAsync(4, advanceCheckpoint);
156153

157-
// Generate and send the message to the L1 contract
154+
// Generate and send the message to the L1 contract during the drift
158155
log.warn(`Sending L1 to L2 message`);
159156
const [secret, secretHash] = await generateClaimSecret();
160157
const message = { recipient: testContract.address, content: Fr.random(), secretHash };
161158
const { msgHash, globalLeafIndex } = await sendMessageToL2(message);
162159

163-
// Wait until the Aztec node has synced it
164-
const msgCheckpointNumber = await waitForMessageFetched(msgHash);
165-
log.warn(`Message synced for checkpoint ${msgCheckpointNumber}`);
166-
expect(checkpointedProvenBlock.checkpointNumber + 4).toBeLessThan(msgCheckpointNumber);
167-
168-
// And keep mining until we prune back to the original block number. Now the "waiting for two blocks"
169-
// strategy for the message to be ready to use shouldn't work, since the lastProven block is more than
170-
// two blocks behind the message block. This is the scenario we want to test.
160+
// The drift's current L1 pending checkpoint. The node prunes its local view as soon as the proof
161+
// window lapses, but L1 only commits the prune when the rebuilt chain proposes its first checkpoint
162+
// on top of the pre-drift block. Wait for that on-chain commit — L1's pending tip dropping back
163+
// below the drift — before re-enabling proving: marking proven while the drift is still the pending
164+
// tip would pin the drift proven and wedge the rebuild forever with Rollup__InvalidArchive.
165+
const driftPendingCheckpoint = (await t.cheatCodes.rollup.getTips()).pending;
166+
167+
// Keep mining until the rollup prunes the drifted proposed chain back to the pre-drift block. L1's
168+
// pending tip dropping below the drift is the unambiguous signal that the on-chain prune committed
169+
// (a new checkpoint was proposed on top of the pre-drift block); the node's faster local prune is
170+
// not, which is why we gate on L1 here rather than on the node's block number.
171171
log.warn(`Waiting until we prune back to ${lastProven}`);
172172
await retryUntil(
173-
async () =>
174-
(await aztecNode.getBlockNumber().then(b => b === lastProven || b === lastProven + 1)) ||
175-
(await tryAdvanceBlock()),
173+
async () => {
174+
await tryAdvanceBlock();
175+
return (await t.cheatCodes.rollup.getTips()).pending < driftPendingCheckpoint;
176+
},
176177
'wait for prune',
177178
180,
178179
);
179-
// The drift condition has been established. Re-enable explicit proving so the catch-up blocks
180-
// below are not pruned a second time before the message checkpoint becomes ready.
180+
// The prune has committed on L1. Re-enable explicit proving so the catch-up chain that re-consumes
181+
// the message is not pruned a second time before we can consume it — safe now that L1's pending tip
182+
// is the rebuilt chain rather than the drift.
181183
markProvenEnabled = true;
182184
await markAsProven();
183185

184-
// Check that there is no witness yet
185-
expect(await aztecNode.getL1ToL2MessageMembershipWitness('latest', msgHash)).toBeUndefined();
186-
187-
// Define L2 function to consume the message
188-
const consume = () => getConsumeMethod(scope)(message.content, secret, t.ethAccount, globalLeafIndex);
186+
// Post-flip invariant: the compact-indexed message survives the L2 prune and is re-consumed on the
187+
// new chain within a reasonable window (its bucket and rolling-hash state must persist across the
188+
// reorg), becoming consumable from the requested scope.
189+
await waitForMessageReady(msgHash, scope);
189190

190-
// Wait until the message is ready to be consumed, checking that it cannot be consumed beforehand
191-
await waitForMessageReady(msgHash, scope, async () => {
192-
if (scope === 'private') {
193-
// On private, we simulate the tx locally and check that we get a missing message error, then we advance to the next block
194-
await expect(() => consume().simulate({ from: user1Address })).rejects.toThrow(/No L1 to L2 message found/);
195-
await tryAdvanceBlock();
196-
} else {
197-
// In public it is harder to determine when a message becomes consumable.
198-
// We send a transaction, this advances the chain and the message MIGHT be consumed in the new block.
199-
// If it does get consumed then we check that the block contains the message.
200-
// If it fails we check that the block doesn't contain the message
201-
const { receipt } = await consume().send({ from: user1Address, wait: { dontThrowOnRevert: true } });
202-
if (receipt.executionResult === TxExecutionResult.SUCCESS) {
203-
// The consume tx must not succeed before the message checkpoint. It can land in a later
204-
// checkpoint if the node catches up between the readiness poll and the tx being built.
205-
const block = await aztecNode.getBlock(receipt.blockNumber!);
206-
expect(block).toBeDefined();
207-
expect(block!.checkpointNumber).toBeGreaterThanOrEqual(msgCheckpointNumber);
208-
} else {
209-
expect(receipt.executionResult).toEqual(TxExecutionResult.REVERTED);
191+
// The PXE is anchored to the checkpointed tip (syncChainTip: 'checkpointed'), so the re-consumed
192+
// message only becomes visible to the consume simulation once its block is checkpointed — not merely
193+
// proposed at 'latest'. Drive the rebuilt chain forward (advanceBlock marks each step proven so it
194+
// is not pruned again) until the checkpointed tip covers the block that re-consumed the message.
195+
const messageBlock = await aztecNode.getBlockNumber();
196+
await retryUntil(
197+
async () => {
198+
if ((await aztecNode.getBlockNumber('checkpointed')) >= messageBlock) {
199+
return true;
210200
}
211-
}
212-
await markAsProven();
213-
});
201+
await tryAdvanceBlock();
202+
return false;
203+
},
204+
'wait for message block to be checkpointed',
205+
180,
206+
);
214207

215-
// Verify the membership witness is available for creating the tx (private-land only)
216-
if (scope === 'private') {
217-
const [messageIndex] = (await aztecNode.getL1ToL2MessageMembershipWitness('latest', msgHash))!;
218-
expect(messageIndex).toEqual(globalLeafIndex.toBigInt());
219-
// And consume the message for private, public was already consumed.
220-
await consume().send({ from: user1Address });
221-
}
208+
// The witness leaf index is the L1-assigned compact global index and must be stable across the prune.
209+
const [messageIndex] = (await aztecNode.getL1ToL2MessageMembershipWitness('latest', msgHash))!;
210+
expect(messageIndex).toEqual(globalLeafIndex.toBigInt());
211+
212+
// The message is consumable on L2 from the requested scope.
213+
const consume = () => getConsumeMethod(scope)(message.content, secret, t.ethAccount, globalLeafIndex);
214+
await consume().send({ from: user1Address });
222215
};
223216

224-
// Mines four checkpoints without proving, inserting an L1→L2 message after the drift, then
225-
// triggers a rollup prune back to the pre-drift block. Verifies the message can be consumed only
226-
// after the chain re-syncs to the message's checkpoint, not before, from both private and public
227-
// scope (public uses a send+dontThrowOnRevert loop to probe when the message becomes consumable).
217+
// Mines several unproven checkpoints, inserts an L1→L2 message during the drift, then triggers a
218+
// rollup prune back to the pre-drift block and verifies the message is still re-consumed on the new
219+
// chain — with its L1-assigned witness index intact — from both private and public scope.
228220
it.each(['private', 'public'] as const)(
229-
'can consume L1 to L2 message in %s after inbox drifts away from the rollup',
221+
'consumes an L1 to L2 message after a rollup prune drops the drifted chain (%s)',
230222
async scope => {
231-
await canConsumeMessageAfterInboxDrift(scope);
223+
await canConsumeMessageAfterRollupPrune(scope);
232224
},
233225
);
234226
});

0 commit comments

Comments
 (0)