Skip to content

Commit a17dce8

Browse files
committed
test(fast-inbox): back-port the l1 publisher integration test to streaming inbox (A-1384)
The flip makes world-state sync reconstruct L1-to-L2 messages per block from the Inbox, and propose validates the checkpoint's inboxRollingHash against a bucket hint. Back the blockSource mock with MockL1ToL2MessageSource, register a bucket per block from the real on-chain currentBucketSeq, chain the previous checkpoint's rolling hash, and derive the propose bucket hint from the mock. Updates the propose-revert assertion to the streaming Rollup__InvalidInboxRollingHash selector (a-1384 already validates rolling hash, not inHash).
1 parent 8502637 commit a17dce8

1 file changed

Lines changed: 95 additions & 35 deletions

File tree

yarn-project/sequencer-client/src/publisher/l1_publisher.integration.test.ts

Lines changed: 95 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ArchiverDataSource } from '@aztec/archiver';
2+
import { MockL1ToL2MessageSource } from '@aztec/archiver/test';
23
import { AztecAddress } from '@aztec/aztec.js/addresses';
34
import { Fr } from '@aztec/aztec.js/fields';
45
import { createLogger } from '@aztec/aztec.js/log';
@@ -13,10 +14,10 @@ import {
1314
} from '@aztec/blob-lib';
1415
import {
1516
GENESIS_ARCHIVE_ROOT,
17+
MAX_L1_TO_L2_MSGS_PER_CHECKPOINT,
1618
MAX_NULLIFIERS_PER_TX,
1719
MAX_PROCESSABLE_L2_GAS,
1820
MAX_TX_DA_GAS,
19-
NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP,
2021
} from '@aztec/constants';
2122
import { EpochCache } from '@aztec/epoch-cache';
2223
import { createEthereumChain } from '@aztec/ethereum/chain';
@@ -47,7 +48,7 @@ import { retryUntil } from '@aztec/foundation/retry';
4748
import { sleep } from '@aztec/foundation/sleep';
4849
import { hexToBuffer } from '@aztec/foundation/string';
4950
import { TestDateProvider } from '@aztec/foundation/timer';
50-
import { RollupAbi } from '@aztec/l1-artifacts';
51+
import { InboxAbi, RollupAbi } from '@aztec/l1-artifacts';
5152
import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree';
5253
import { ProtocolContractsList, protocolContractsHash } from '@aztec/protocol-contracts';
5354
import { LightweightCheckpointBuilder } from '@aztec/prover-client/light';
@@ -86,7 +87,7 @@ import { NativeWorldStateService, ServerWorldStateSynchronizer, type WorldStateC
8687

8788
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
8889
import { type MockProxy, mock } from 'jest-mock-extended';
89-
import { type Address, encodeFunctionData, getAbiItem, getAddress, multicall3Abi } from 'viem';
90+
import { type Address, encodeFunctionData, getAbiItem, getAddress, getContract, multicall3Abi } from 'viem';
9091
import { type PrivateKeyAccount, privateKeyToAccount } from 'viem/accounts';
9192
import { foundry } from 'viem/chains';
9293

@@ -111,7 +112,8 @@ const logger = createLogger('integration_l1_publisher');
111112
// depending on @aztec/aztec-node, which would create a sequencer-client <-> aztec-node cycle.
112113
const config: SequencerClientConfig & L1ContractsConfig = { ...getL1ContractsConfigEnvVars(), ...getConfigEnvVars() };
113114

114-
// Must exceed the inbox lag (network default 2) so at least one checkpoint consumes a real L1->L2 message.
115+
// Several consecutive checkpoints, each consuming the L1->L2 messages sent while it was being built, so real
116+
// messages are genuinely consumed and validated on L1 (AZIP-22 Fast Inbox).
115117
const numberOfConsecutiveBlocks = 3;
116118

117119
jest.setTimeout(1000000);
@@ -138,6 +140,11 @@ describe('L1Publisher integration', () => {
138140

139141
let builderDb: NativeWorldStateService;
140142

143+
// Backs the blockSource mock's streaming L1->L2 message queries. The world-state synchronizer reconstructs each
144+
// block's consumed message bundle from Inbox buckets (AZIP-22 Fast Inbox) when it syncs a block back, so the test
145+
// registers one bucket per published block here (see buildAndPublishBlock).
146+
let messageSource: MockL1ToL2MessageSource;
147+
141148
// The header of the last block
142149
let prevHeader: BlockHeader;
143150

@@ -274,6 +281,21 @@ describe('L1Publisher integration', () => {
274281
checkpointNumber: CheckpointNumber.ZERO,
275282
indexWithinCheckpoint: IndexWithinCheckpoint(0),
276283
};
284+
// Seed the genesis sentinel bucket (seq 0, no messages) so the world-state synchronizer can resolve a
285+
// totalMsgCount of 0 to a bucket when reconstructing the first block's message bundle.
286+
messageSource = new MockL1ToL2MessageSource(0);
287+
messageSource.setInboxBucket(
288+
{
289+
seq: 0n,
290+
inboxRollingHash: Fr.ZERO,
291+
totalMsgCount: 0n,
292+
timestamp: 0n,
293+
msgCount: 0,
294+
lastMessageIndex: 0n,
295+
isOpen: false,
296+
},
297+
[],
298+
);
277299
blockSource = mock<ArchiverDataSource>({
278300
getBlocks(query: BlocksQuery) {
279301
if (!('from' in query)) {
@@ -349,6 +371,14 @@ describe('L1Publisher integration', () => {
349371
getBlockNumber(): Promise<BlockNumber> {
350372
return Promise.resolve(BlockNumber(blocks.at(-1)?.number ?? BlockNumber.ZERO));
351373
},
374+
// Streaming L1->L2 message reconstruction (AZIP-22 Fast Inbox): the world-state synchronizer resolves each
375+
// block's consumed message bundle from the Inbox buckets registered per published block in buildAndPublishBlock.
376+
getInboxBucketByTotalMsgCount(totalMsgCount: bigint) {
377+
return messageSource.getInboxBucketByTotalMsgCount(totalMsgCount);
378+
},
379+
getL1ToL2MessagesBetweenBuckets(fromExclusive: bigint, toInclusive: bigint) {
380+
return messageSource.getL1ToL2MessagesBetweenBuckets(fromExclusive, toInclusive);
381+
},
352382
});
353383

354384
const worldStateConfig: WorldStateConfig = {
@@ -444,13 +474,16 @@ describe('L1Publisher integration', () => {
444474

445475
/**
446476
* Build a checkpoint with a single block using the LightweightCheckpointBuilder.
447-
* This properly computes all checkpoint header fields (blobsHash, blockHeadersHash, inHash, epochOutHash, etc.)
477+
* This properly computes all checkpoint header fields (blobsHash, blockHeadersHash, inHash, inboxRollingHash,
478+
* epochOutHash, etc.). `previousInboxRollingHash` is the previous checkpoint's rolling hash (zero at genesis), so
479+
* the header's `inboxRollingHash` continues the on-chain Inbox chain over `l1ToL2Messages`.
448480
*/
449481
const buildCheckpoint = async (
450482
globalVariables: GlobalVariables,
451483
txs: ProcessedTx[],
452484
l1ToL2Messages: Fr[],
453485
previousCheckpointOutHashes: Fr[] = [],
486+
previousInboxRollingHash: Fr = Fr.ZERO,
454487
): Promise<Checkpoint> => {
455488
await worldStateSynchronizer.syncImmediate();
456489
const tempFork = await worldStateSynchronizer.fork(BlockNumber(globalVariables.blockNumber - 1));
@@ -472,7 +505,7 @@ describe('L1Publisher integration', () => {
472505
checkpointConstants,
473506
l1ToL2Messages,
474507
previousCheckpointOutHashes,
475-
Fr.ZERO,
508+
previousInboxRollingHash,
476509
tempFork,
477510
);
478511

@@ -486,7 +519,8 @@ describe('L1Publisher integration', () => {
486519
const buildSingleCheckpoint = async (
487520
opts: { l1ToL2Messages?: Fr[]; blockNumber?: BlockNumber; slot?: SlotNumber } = {},
488521
) => {
489-
const l1ToL2Messages = opts.l1ToL2Messages ?? new Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(Fr.ZERO);
522+
// By default a single checkpoint consumes no Inbox messages (bucketHint 0 against the genesis bucket).
523+
const l1ToL2Messages = opts.l1ToL2Messages ?? [];
490524

491525
const txs = await Promise.all([makeProcessedTx(0x1000), makeProcessedTx(0x2000)]);
492526
const ts = (await l1Client.getBlock()).timestamp;
@@ -503,7 +537,6 @@ describe('L1Publisher integration', () => {
503537
new GasFees(0, await rollup.getManaMinFeeAt(timestamp, true)),
504538
);
505539
const checkpoint = await buildCheckpoint(globalVariables, txs, l1ToL2Messages);
506-
blockSource.getL1ToL2Messages.mockResolvedValueOnce(l1ToL2Messages);
507540
return { checkpoint, l1ToL2Messages };
508541
};
509542

@@ -517,10 +550,8 @@ describe('L1Publisher integration', () => {
517550

518551
describe('block building', () => {
519552
beforeEach(async () => {
520-
// This suite proposes consecutive checkpoints and models the inbox lag by hand (a checkpoint
521-
// consumes the L1->L2 messages sent inboxLag checkpoints earlier -- see the shift register in
522-
// buildAndPublishBlock). It inherits the network default lag and proposes enough checkpoints
523-
// that a real message is actually consumed and validated on L1.
553+
// This suite proposes consecutive checkpoints, each consuming the streaming-Inbox messages sent while it was
554+
// being built (AZIP-22 Fast Inbox), so real messages are genuinely consumed and validated on L1.
524555
await setup();
525556
});
526557

@@ -535,30 +566,33 @@ describe('L1Publisher integration', () => {
535566
'0x1647b194c649f5dd01d7c832f89b0f496043c9150797923ea89e93d5ac619a93',
536567
);
537568

538-
// The deployed rollup consumes L1->L2 messages with a lag of `inboxLag` checkpoints: a message
539-
// inserted while building checkpoint N is only consumable at checkpoint N + inboxLag. Model that
540-
// with a shift register so a real message is genuinely consumed and validated on L1.
541-
const inboxLag = getL1ContractsConfigEnvVars().inboxLag;
542-
const messagesInFlight: Fr[][] = Array.from({ length: inboxLag }, () => []);
569+
// Streaming Inbox consumption (AZIP-22 Fast Inbox): each checkpoint consumes every message sent so far, so its
570+
// header rolling hash continues the previous checkpoint's and matches the Inbox's current bucket. Consuming
571+
// through the newest bucket trivially satisfies the mandatory-consumption assert. A checkpoint's messages are
572+
// sent one per L1 block, so they span several Inbox buckets; the checkpoint's consumed bucket is the newest,
573+
// whose sequence we read straight off the deployed Inbox and mirror into messageSource so the propose bucket
574+
// hint can be looked up by the checkpoint's cumulative message count.
575+
const inbox = getContract({
576+
address: getAddress(l1ContractAddresses.inboxAddress.toString()),
577+
abi: InboxAbi,
578+
client: l1Client,
579+
});
580+
let previousInboxRollingHash = Fr.ZERO;
543581
const blobFieldsPerCheckpoint: Fr[][] = [];
544582
// The below batched blob is used for testing different epochs with 1..numberOfConsecutiveBlocks blocks on L1.
545583
// For real usage, always collect ALL epoch blobs first then call .batch().
546584
let currentBatch: BatchedBlob | undefined;
547585

548586
for (let i = 0; i < numberOfConsecutiveBlocks; i++) {
549-
// With just one l1 client (serial sending) this takes too much time to send NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP
587+
// With just one l1 client (serial sending) this takes too much time to send MAX_L1_TO_L2_MSGS_PER_CHECKPOINT
550588
// and causes a chain prune
551-
const l1ToL2Content = range(Math.min(16, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP), 128 * i + 1 + 0x400).map(fr);
589+
const l1ToL2Content = range(Math.min(16, MAX_L1_TO_L2_MSGS_PER_CHECKPOINT), 128 * i + 1 + 0x400).map(fr);
552590

553-
const sentThisCheckpoint: Fr[] = [];
591+
const currentL1ToL2Messages: Fr[] = [];
554592
for (let j = 0; j < l1ToL2Content.length; j++) {
555-
sentThisCheckpoint.push(await sendToL2(l1ToL2Content[j], recipientAddress));
593+
currentL1ToL2Messages.push(await sendToL2(l1ToL2Content[j], recipientAddress));
556594
}
557595

558-
// Consume the messages sent inboxLag checkpoints ago, then enqueue this checkpoint's messages.
559-
const currentL1ToL2Messages = messagesInFlight.shift()!;
560-
messagesInFlight.push(sentThisCheckpoint);
561-
562596
// Ensure that each transaction has unique (non-intersecting nullifier values)
563597
const totalNullifiersPerBlock = 4 * MAX_NULLIFIERS_PER_TX;
564598
const txs = await timesParallel(numTxs, txIndex =>
@@ -580,14 +614,39 @@ describe('L1Publisher integration', () => {
580614
new GasFees(0, await rollup.getManaMinFeeAt(timestamp, true)),
581615
);
582616

583-
const checkpoint = await buildCheckpoint(globalVariables, txs, currentL1ToL2Messages);
617+
const checkpoint = await buildCheckpoint(
618+
globalVariables,
619+
txs,
620+
currentL1ToL2Messages,
621+
[],
622+
previousInboxRollingHash,
623+
);
624+
previousInboxRollingHash = checkpoint.header.inboxRollingHash;
584625
const block = checkpoint.blocks[0];
585626

627+
// Mirror the Inbox's newest bucket into messageSource so the world-state synchronizer can rebuild this
628+
// block's consumed bundle when it syncs the block back on the next iteration, and so the propose bucket hint
629+
// resolves to the sequence whose rolling hash the checkpoint header committed to. One bucket per block keyed
630+
// by its cumulative L1->L2 tree leaf count -- exactly the value world-state looks the bucket up by.
631+
const currentBucketSeq = await inbox.read.getCurrentBucketSeq();
632+
const cumulativeMsgCount = BigInt(block.header.state.l1ToL2MessageTree.nextAvailableLeafIndex);
633+
messageSource.setInboxBucket(
634+
{
635+
seq: currentBucketSeq,
636+
inboxRollingHash: checkpoint.header.inboxRollingHash,
637+
totalMsgCount: cumulativeMsgCount,
638+
timestamp,
639+
msgCount: currentL1ToL2Messages.length,
640+
lastMessageIndex: cumulativeMsgCount - 1n,
641+
isOpen: false,
642+
},
643+
currentL1ToL2Messages,
644+
);
645+
586646
const totalManaUsed = txs.reduce((acc, tx) => acc.add(new Fr(tx.gasUsed.billedGas.l2Gas)), Fr.ZERO);
587647
expect(totalManaUsed.toBigInt()).toEqual(block.header.totalManaUsed.toBigInt());
588648

589649
prevHeader = block.header;
590-
blockSource.getL1ToL2Messages.mockResolvedValueOnce(currentL1ToL2Messages);
591650

592651
const checkpointBlobFields = checkpoint.toBlobFields();
593652
const blockBlobs = await getBlobsPerL1Block(checkpointBlobFields);
@@ -611,11 +670,13 @@ describe('L1Publisher integration', () => {
611670
deployerAccount.address,
612671
);
613672

673+
// The checkpoint consumed everything sent so far, so its consumed bucket is the Inbox's newest.
674+
const bucketHint = (await messageSource.getInboxBucketByTotalMsgCount(cumulativeMsgCount))!.seq;
614675
await publisher.enqueueProposeCheckpoint(
615676
checkpoint,
616677
CommitteeAttestationsAndSigners.empty(getSignatureContext()),
617678
Signature.empty(),
618-
0n,
679+
bucketHint,
619680
);
620681
// Align chain time so the bundle simulate and the L1 send both run at the header's slot.
621682
await progressToSlot(BigInt(checkpoint.header.slotNumber));
@@ -661,7 +722,7 @@ describe('L1Publisher integration', () => {
661722
oracleInput: {
662723
feeAssetPriceModifier: 0n,
663724
},
664-
bucketHint: 0n,
725+
bucketHint,
665726
},
666727
CommitteeAttestationsAndSigners.packAttestations([]),
667728
[],
@@ -969,10 +1030,9 @@ describe('L1Publisher integration', () => {
9691030
});
9701031

9711032
it(`shows propose custom errors if tx simulation fails`, async () => {
972-
// Set up different l1-to-l2 messages than the ones on the inbox, so this submission reverts because the
973-
// INBOX.consume does not match the header.inHash and we get a Rollup__BlobHash that is not caught by
974-
// validateHeader before.
975-
const l1ToL2Messages = new Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(new Fr(1n));
1033+
// Set up different l1-to-l2 messages than the ones on the inbox, so the checkpoint's inboxRollingHash does not
1034+
// match the referenced Inbox bucket and the submission reverts at the streaming-consumption check.
1035+
const l1ToL2Messages = new Array(MAX_L1_TO_L2_MSGS_PER_CHECKPOINT).fill(new Fr(1n));
9761036
const { checkpoint } = await buildSingleCheckpoint({ l1ToL2Messages });
9771037

9781038
// Enqueue no longer simulates per action — the bundle simulate at send time drops the
@@ -987,12 +1047,12 @@ describe('L1Publisher integration', () => {
9871047
await progressToSlot(BigInt(checkpoint.header.slotNumber));
9881048
const result = await publisher.sendRequests();
9891049
expect(result).toBeUndefined();
990-
// 0xcd6f4233 == Rollup__InvalidInHash selector
1050+
// 0xed1f7bb5 == Rollup__InvalidInboxRollingHash selector
9911051
expect(loggerWarnSpy).toHaveBeenCalledWith(
9921052
'Bundle entry dropped: action reverted in sim',
9931053
expect.objectContaining({
9941054
action: 'propose',
995-
returnData: expect.stringMatching(/^0xcd6f4233/),
1055+
returnData: expect.stringMatching(/^0xed1f7bb5/),
9961056
}),
9971057
);
9981058
});

0 commit comments

Comments
 (0)