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,10 +14,10 @@ import {
1314} from '@aztec/blob-lib' ;
1415import {
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' ;
2122import { EpochCache } from '@aztec/epoch-cache' ;
2223import { createEthereumChain } from '@aztec/ethereum/chain' ;
@@ -47,7 +48,7 @@ import { retryUntil } from '@aztec/foundation/retry';
4748import { sleep } from '@aztec/foundation/sleep' ;
4849import { hexToBuffer } from '@aztec/foundation/string' ;
4950import { TestDateProvider } from '@aztec/foundation/timer' ;
50- import { RollupAbi } from '@aztec/l1-artifacts' ;
51+ import { InboxAbi , RollupAbi } from '@aztec/l1-artifacts' ;
5152import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree' ;
5253import { ProtocolContractsList , protocolContractsHash } from '@aztec/protocol-contracts' ;
5354import { LightweightCheckpointBuilder } from '@aztec/prover-client/light' ;
@@ -86,7 +87,7 @@ import { NativeWorldStateService, ServerWorldStateSynchronizer, type WorldStateC
8687
8788import { beforeEach , describe , expect , it , jest } from '@jest/globals' ;
8889import { 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' ;
9091import { type PrivateKeyAccount , privateKeyToAccount } from 'viem/accounts' ;
9192import { 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.
112113const 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).
115117const numberOfConsecutiveBlocks = 3 ;
116118
117119jest . 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 ( / ^ 0 x c d 6 f 4 2 3 3 / ) ,
1055+ returnData : expect . stringMatching ( / ^ 0 x e d 1 f 7 b b 5 / ) ,
9961056 } ) ,
9971057 ) ;
9981058 } ) ;
0 commit comments