Skip to content

Commit 37a23d1

Browse files
committed
test(fast-inbox): drive l1 publisher inbox consumption through the streaming bucket selector (A-1387)
The A-1387 rewrite that removed inHash left this test's inbox handling half-adapted: it hardcoded previousInboxRollingHash to Fr.ZERO and proposed with bucketHint 0n while consuming real L1->L2 messages, so message-consuming checkpoints reverted on L1 with Rollup__InvalidInboxRollingHash / UnconsumedInboxMessages. INBOX_LAG_SECONDS is an L1-time censorship cutoff (not whole checkpoints), so no checkpoint-depth shift register can reproduce the aged bucket set. Mirror the real Inbox buckets into the test's MockL1ToL2MessageSource, then reuse the production selectInboxBucketForBlock (which mirrors ProposeLib.validateInboxConsumption) to pick exactly the buckets each checkpoint must consume, deriving the consumed bundle, the propose bucket hint, and the header rolling hash from that one selection so header, world state, and L1 agree. Matches the A-1388 rewrite of this file so the segment rebase dissolves to zero.
1 parent df47af3 commit 37a23d1

1 file changed

Lines changed: 118 additions & 24 deletions

File tree

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

Lines changed: 118 additions & 24 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,6 +14,8 @@ import {
1314
} from '@aztec/blob-lib';
1415
import {
1516
GENESIS_ARCHIVE_ROOT,
17+
INBOX_LAG_SECONDS,
18+
MAX_L1_TO_L2_MSGS_PER_BLOCK,
1619
MAX_L1_TO_L2_MSGS_PER_CHECKPOINT,
1720
MAX_NULLIFIERS_PER_TX,
1821
MAX_PROCESSABLE_L2_GAS,
@@ -47,7 +50,7 @@ import { retryUntil } from '@aztec/foundation/retry';
4750
import { sleep } from '@aztec/foundation/sleep';
4851
import { hexToBuffer } from '@aztec/foundation/string';
4952
import { TestDateProvider } from '@aztec/foundation/timer';
50-
import { RollupAbi } from '@aztec/l1-artifacts';
53+
import { InboxAbi, RollupAbi } from '@aztec/l1-artifacts';
5154
import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree';
5255
import { ProtocolContractsList, protocolContractsHash } from '@aztec/protocol-contracts';
5356
import { LightweightCheckpointBuilder } from '@aztec/prover-client/light';
@@ -86,11 +89,12 @@ import { NativeWorldStateService, ServerWorldStateSynchronizer, type WorldStateC
8689

8790
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
8891
import { type MockProxy, mock } from 'jest-mock-extended';
89-
import { type Address, encodeFunctionData, getAbiItem, getAddress, multicall3Abi } from 'viem';
92+
import { type Address, encodeFunctionData, getAbiItem, getAddress, getContract, multicall3Abi } from 'viem';
9093
import { type PrivateKeyAccount, privateKeyToAccount } from 'viem/accounts';
9194
import { foundry } from 'viem/chains';
9295

9396
import { type SequencerClientConfig, getConfigEnvVars } from '../config.js';
97+
import { selectInboxBucketForBlock } from '../sequencer/inbox_bucket_selector.js';
9498
import { sendL1ToL2Message } from './l1_to_l2_messaging.js';
9599
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
96100
import { SequencerPublisher } from './sequencer-publisher.js';
@@ -111,7 +115,8 @@ const logger = createLogger('integration_l1_publisher');
111115
// depending on @aztec/aztec-node, which would create a sequencer-client <-> aztec-node cycle.
112116
const config: SequencerClientConfig & L1ContractsConfig = { ...getL1ContractsConfigEnvVars(), ...getConfigEnvVars() };
113117

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

117122
jest.setTimeout(1000000);
@@ -138,6 +143,11 @@ describe('L1Publisher integration', () => {
138143

139144
let builderDb: NativeWorldStateService;
140145

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

@@ -274,6 +284,21 @@ describe('L1Publisher integration', () => {
274284
checkpointNumber: CheckpointNumber.ZERO,
275285
indexWithinCheckpoint: IndexWithinCheckpoint(0),
276286
};
287+
// Seed the genesis sentinel bucket (seq 0, no messages) so the world-state synchronizer can resolve a
288+
// totalMsgCount of 0 to a bucket when reconstructing the first block's message bundle.
289+
messageSource = new MockL1ToL2MessageSource(0);
290+
messageSource.setInboxBucket(
291+
{
292+
seq: 0n,
293+
inboxRollingHash: Fr.ZERO,
294+
totalMsgCount: 0n,
295+
timestamp: 0n,
296+
msgCount: 0,
297+
lastMessageIndex: 0n,
298+
isOpen: false,
299+
},
300+
[],
301+
);
277302
blockSource = mock<ArchiverDataSource>({
278303
getBlocks(query: BlocksQuery) {
279304
if (!('from' in query)) {
@@ -349,6 +374,14 @@ describe('L1Publisher integration', () => {
349374
getBlockNumber(): Promise<BlockNumber> {
350375
return Promise.resolve(BlockNumber(blocks.at(-1)?.number ?? BlockNumber.ZERO));
351376
},
377+
// Streaming L1->L2 message reconstruction (AZIP-22 Fast Inbox): the world-state synchronizer resolves each
378+
// block's consumed message bundle from the Inbox buckets registered per published block in buildAndPublishBlock.
379+
getInboxBucketByTotalMsgCount(totalMsgCount: bigint) {
380+
return messageSource.getInboxBucketByTotalMsgCount(totalMsgCount);
381+
},
382+
getL1ToL2MessagesBetweenBuckets(fromExclusive: bigint, toInclusive: bigint) {
383+
return messageSource.getL1ToL2MessagesBetweenBuckets(fromExclusive, toInclusive);
384+
},
352385
});
353386

354387
const worldStateConfig: WorldStateConfig = {
@@ -444,13 +477,16 @@ describe('L1Publisher integration', () => {
444477

445478
/**
446479
* Build a checkpoint with a single block using the LightweightCheckpointBuilder.
447-
* This properly computes all checkpoint header fields (blobsHash, blockHeadersHash, inHash, epochOutHash, etc.)
480+
* This properly computes all checkpoint header fields (blobsHash, blockHeadersHash, inboxRollingHash,
481+
* epochOutHash, etc.). `previousInboxRollingHash` is the previous checkpoint's rolling hash (zero at genesis), so
482+
* the header's `inboxRollingHash` continues the on-chain Inbox chain over `l1ToL2Messages`.
448483
*/
449484
const buildCheckpoint = async (
450485
globalVariables: GlobalVariables,
451486
txs: ProcessedTx[],
452487
l1ToL2Messages: Fr[],
453488
previousCheckpointOutHashes: Fr[] = [],
489+
previousInboxRollingHash: Fr = Fr.ZERO,
454490
): Promise<Checkpoint> => {
455491
await worldStateSynchronizer.syncImmediate();
456492
const tempFork = await worldStateSynchronizer.fork(BlockNumber(globalVariables.blockNumber - 1));
@@ -471,7 +507,7 @@ describe('L1Publisher integration', () => {
471507
checkpointNumber,
472508
checkpointConstants,
473509
previousCheckpointOutHashes,
474-
Fr.ZERO,
510+
previousInboxRollingHash,
475511
tempFork,
476512
);
477513

@@ -485,7 +521,8 @@ describe('L1Publisher integration', () => {
485521
const buildSingleCheckpoint = async (
486522
opts: { l1ToL2Messages?: Fr[]; blockNumber?: BlockNumber; slot?: SlotNumber } = {},
487523
) => {
488-
const l1ToL2Messages = opts.l1ToL2Messages ?? new Array(MAX_L1_TO_L2_MSGS_PER_CHECKPOINT).fill(Fr.ZERO);
524+
// By default a single checkpoint consumes no Inbox messages (bucketHint 0 against the genesis bucket).
525+
const l1ToL2Messages = opts.l1ToL2Messages ?? [];
489526

490527
const txs = await Promise.all([makeProcessedTx(0x1000), makeProcessedTx(0x2000)]);
491528
const ts = (await l1Client.getBlock()).timestamp;
@@ -502,7 +539,6 @@ describe('L1Publisher integration', () => {
502539
new GasFees(0, await rollup.getManaMinFeeAt(timestamp, true)),
503540
);
504541
const checkpoint = await buildCheckpoint(globalVariables, txs, l1ToL2Messages);
505-
blockSource.getL1ToL2Messages.mockResolvedValueOnce(l1ToL2Messages);
506542
return { checkpoint, l1ToL2Messages };
507543
};
508544

@@ -516,10 +552,8 @@ describe('L1Publisher integration', () => {
516552

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

@@ -534,11 +568,24 @@ describe('L1Publisher integration', () => {
534568
'0x1647b194c649f5dd01d7c832f89b0f496043c9150797923ea89e93d5ac619a93',
535569
);
536570

537-
// The deployed rollup consumes L1->L2 messages with a lag of `inboxLag` checkpoints: a message
538-
// inserted while building checkpoint N is only consumable at checkpoint N + inboxLag. Model that
539-
// with a shift register so a real message is genuinely consumed and validated on L1.
540-
const inboxLag = getL1ContractsConfigEnvVars().inboxLag;
541-
const messagesInFlight: Fr[][] = Array.from({ length: inboxLag }, () => []);
571+
// Streaming Inbox consumption (AZIP-22 Fast Inbox): the L1 Rollup only lets a checkpoint consume Inbox buckets
572+
// that have aged past the censorship cutoff (`toTimestamp(slot - 1) - INBOX_LAG_SECONDS`), measured in L1 time,
573+
// not whole checkpoints. Each checkpoint mirrors the real Inbox buckets into messageSource, then reuses the
574+
// production `selectInboxBucketForBlock` (which mirrors `ProposeLib.validateInboxConsumption`) to pick exactly
575+
// the buckets it must consume, deriving the consumed bundle, the propose bucket hint, and the header rolling
576+
// hash from that one selection so header, world state, and L1 agree by construction.
577+
const inbox = getContract({
578+
address: getAddress(l1ContractAddresses.inboxAddress.toString()),
579+
abi: InboxAbi,
580+
client: l1Client,
581+
});
582+
// Every message sent to the Inbox, in insertion order, so each bucket's leaves can be mirrored into messageSource.
583+
const allSentMessages: Fr[] = [];
584+
let mirroredThroughSeq = 0n;
585+
let mirroredThroughTotal = 0n;
586+
// The last Inbox bucket this checkpoint chain has consumed through; genesis sentinel to start.
587+
let parent = { seq: 0n, totalMsgCount: 0n };
588+
let previousInboxRollingHash = Fr.ZERO;
542589
const blobFieldsPerCheckpoint: Fr[][] = [];
543590
// The below batched blob is used for testing different epochs with 1..numberOfConsecutiveBlocks blocks on L1.
544591
// For real usage, always collect ALL epoch blobs first then call .batch().
@@ -553,10 +600,29 @@ describe('L1Publisher integration', () => {
553600
for (let j = 0; j < l1ToL2Content.length; j++) {
554601
sentThisCheckpoint.push(await sendToL2(l1ToL2Content[j], recipientAddress));
555602
}
556-
557-
// Consume the messages sent inboxLag checkpoints ago, then enqueue this checkpoint's messages.
558-
const currentL1ToL2Messages = messagesInFlight.shift()!;
559-
messagesInFlight.push(sentThisCheckpoint);
603+
allSentMessages.push(...sentThisCheckpoint);
604+
605+
// Mirror the Inbox's new buckets (seq, timestamp, rolling hash, totals) and their leaves into messageSource,
606+
// so the selector, the world-state synchronizer, and L1 all read the same bucket state.
607+
const currentBucketSeq = await inbox.read.getCurrentBucketSeq();
608+
for (let seq = mirroredThroughSeq + 1n; seq <= currentBucketSeq; seq++) {
609+
const bucket = await inbox.read.getBucket([seq]);
610+
const bucketMessages = allSentMessages.slice(Number(mirroredThroughTotal), Number(bucket.totalMsgCount));
611+
messageSource.setInboxBucket(
612+
{
613+
seq,
614+
inboxRollingHash: Fr.fromString(bucket.rollingHash),
615+
totalMsgCount: bucket.totalMsgCount,
616+
timestamp: bucket.timestamp,
617+
msgCount: Number(bucket.msgCount),
618+
lastMessageIndex: bucket.totalMsgCount - 1n,
619+
isOpen: seq === currentBucketSeq,
620+
},
621+
bucketMessages,
622+
);
623+
mirroredThroughTotal = bucket.totalMsgCount;
624+
}
625+
mirroredThroughSeq = currentBucketSeq;
560626

561627
// Ensure that each transaction has unique (non-intersecting nullifier values)
562628
const totalNullifiersPerBlock = 4 * MAX_NULLIFIERS_PER_TX;
@@ -579,14 +645,42 @@ describe('L1Publisher integration', () => {
579645
new GasFees(0, await rollup.getManaMinFeeAt(timestamp, true)),
580646
);
581647

582-
const checkpoint = await buildCheckpoint(globalVariables, txs, currentL1ToL2Messages);
648+
// Reuse the production streaming selector to pick the buckets this single-block (hence last-block) checkpoint
649+
// must consume, then derive the consumed bundle, the propose bucket hint, and the rolling-hash cursor from
650+
// that one selection so the header, world state, and L1 all agree.
651+
const buildFrameStart = await rollup.getTimestampForSlot(SlotNumber(slot - 1));
652+
const cutoffTimestamp = buildFrameStart - BigInt(INBOX_LAG_SECONDS);
653+
const selection = await selectInboxBucketForBlock({
654+
messageSource,
655+
now: buildFrameStart,
656+
lagSeconds: BigInt(INBOX_LAG_SECONDS),
657+
parent,
658+
checkpointStartTotalMsgCount: parent.totalMsgCount,
659+
perBlockCap: MAX_L1_TO_L2_MSGS_PER_BLOCK,
660+
perCheckpointCap: MAX_L1_TO_L2_MSGS_PER_CHECKPOINT,
661+
isLastBlock: true,
662+
cutoffTimestamp,
663+
});
664+
const currentL1ToL2Messages = selection.consume ? selection.bundle : [];
665+
const bucketHint = selection.consume ? selection.bucket.seq : parent.seq;
666+
667+
const checkpoint = await buildCheckpoint(
668+
globalVariables,
669+
txs,
670+
currentL1ToL2Messages,
671+
[],
672+
previousInboxRollingHash,
673+
);
674+
previousInboxRollingHash = checkpoint.header.inboxRollingHash;
583675
const block = checkpoint.blocks[0];
676+
if (selection.consume) {
677+
parent = { seq: selection.bucket.seq, totalMsgCount: selection.bucket.totalMsgCount };
678+
}
584679

585680
const totalManaUsed = txs.reduce((acc, tx) => acc.add(new Fr(tx.gasUsed.billedGas.l2Gas)), Fr.ZERO);
586681
expect(totalManaUsed.toBigInt()).toEqual(block.header.totalManaUsed.toBigInt());
587682

588683
prevHeader = block.header;
589-
blockSource.getL1ToL2Messages.mockResolvedValueOnce(currentL1ToL2Messages);
590684

591685
const checkpointBlobFields = checkpoint.toBlobFields();
592686
const blockBlobs = await getBlobsPerL1Block(checkpointBlobFields);
@@ -614,7 +708,7 @@ describe('L1Publisher integration', () => {
614708
checkpoint,
615709
CommitteeAttestationsAndSigners.empty(getSignatureContext()),
616710
Signature.empty(),
617-
0n,
711+
bucketHint,
618712
);
619713
// Align chain time so the bundle simulate and the L1 send both run at the header's slot.
620714
await progressToSlot(BigInt(checkpoint.header.slotNumber));
@@ -660,7 +754,7 @@ describe('L1Publisher integration', () => {
660754
oracleInput: {
661755
feeAssetPriceModifier: 0n,
662756
},
663-
bucketHint: 0n,
757+
bucketHint,
664758
},
665759
CommitteeAttestationsAndSigners.packAttestations([]),
666760
[],

0 commit comments

Comments
 (0)