Skip to content

Commit 92c4a70

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 4f98ebb commit 92c4a70

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));
@@ -472,7 +508,7 @@ describe('L1Publisher integration', () => {
472508
checkpointConstants,
473509
l1ToL2Messages,
474510
previousCheckpointOutHashes,
475-
Fr.ZERO,
511+
previousInboxRollingHash,
476512
tempFork,
477513
);
478514

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

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

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

518554
describe('block building', () => {
519555
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.
556+
// This suite proposes consecutive checkpoints, each consuming the streaming-Inbox messages sent while it was
557+
// being built (AZIP-22 Fast Inbox), so real messages are genuinely consumed and validated on L1.
524558
await setup();
525559
});
526560

@@ -535,11 +569,24 @@ describe('L1Publisher integration', () => {
535569
'0x1647b194c649f5dd01d7c832f89b0f496043c9150797923ea89e93d5ac619a93',
536570
);
537571

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

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

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

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

589684
prevHeader = block.header;
590-
blockSource.getL1ToL2Messages.mockResolvedValueOnce(currentL1ToL2Messages);
591685

592686
const checkpointBlobFields = checkpoint.toBlobFields();
593687
const blockBlobs = await getBlobsPerL1Block(checkpointBlobFields);
@@ -615,7 +709,7 @@ describe('L1Publisher integration', () => {
615709
checkpoint,
616710
CommitteeAttestationsAndSigners.empty(getSignatureContext()),
617711
Signature.empty(),
618-
0n,
712+
bucketHint,
619713
);
620714
// Align chain time so the bundle simulate and the L1 send both run at the header's slot.
621715
await progressToSlot(BigInt(checkpoint.header.slotNumber));
@@ -661,7 +755,7 @@ describe('L1Publisher integration', () => {
661755
oracleInput: {
662756
feeAssetPriceModifier: 0n,
663757
},
664-
bucketHint: 0n,
758+
bucketHint,
665759
},
666760
CommitteeAttestationsAndSigners.packAttestations([]),
667761
[],

0 commit comments

Comments
 (0)