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 ) ) ;
@@ -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