11import type { ArchiverDataSource } from '@aztec/archiver' ;
2+ import { MockL1ToL2MessageSource } from '@aztec/archiver/test' ;
23import { AztecAddress } from '@aztec/aztec.js/addresses' ;
34import { Fr } from '@aztec/aztec.js/fields' ;
45import { createLogger } from '@aztec/aztec.js/log' ;
@@ -13,6 +14,8 @@ import {
1314} from '@aztec/blob-lib' ;
1415import {
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';
4750import { sleep } from '@aztec/foundation/sleep' ;
4851import { hexToBuffer } from '@aztec/foundation/string' ;
4952import { TestDateProvider } from '@aztec/foundation/timer' ;
50- import { RollupAbi } from '@aztec/l1-artifacts' ;
53+ import { InboxAbi , RollupAbi } from '@aztec/l1-artifacts' ;
5154import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree' ;
5255import { ProtocolContractsList , protocolContractsHash } from '@aztec/protocol-contracts' ;
5356import { LightweightCheckpointBuilder } from '@aztec/prover-client/light' ;
@@ -86,11 +89,12 @@ import { NativeWorldStateService, ServerWorldStateSynchronizer, type WorldStateC
8689
8790import { beforeEach , describe , expect , it , jest } from '@jest/globals' ;
8891import { 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' ;
9093import { type PrivateKeyAccount , privateKeyToAccount } from 'viem/accounts' ;
9194import { foundry } from 'viem/chains' ;
9295
9396import { type SequencerClientConfig , getConfigEnvVars } from '../config.js' ;
97+ import { selectInboxBucketForBlock } from '../sequencer/inbox_bucket_selector.js' ;
9498import { sendL1ToL2Message } from './l1_to_l2_messaging.js' ;
9599import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js' ;
96100import { 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.
112116const 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).
115120const numberOfConsecutiveBlocks = 3 ;
116121
117122jest . 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