diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index 5568ccbfc8c2..0595238903a3 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -69,6 +69,11 @@ pub global L1_TO_L2_MSG_SUBTREE_ROOT_SIBLING_PATH_LENGTH: u32 = pub global MAX_L1_TO_L2_MSGS_PER_BLOCK: u32 = 1024; // Cap on L1-to-L2 messages consumed by a single checkpoint. Semantically today's NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP. pub global MAX_L1_TO_L2_MSGS_PER_CHECKPOINT: u32 = 1024; +// Minimum bucket age, in seconds, at the start of a checkpoint's build frame for its consumption to be mandatory under +// the streaming Inbox censorship cutoff (AZIP-22 Fast Inbox). One L1 slot: validators cannot be required to act on +// buckets they may not yet have seen. L2 consensus policy consumed by the sequencer and validator (and mirrored by +// ProposeLib on L1); the protocol circuits do not read it. +pub global INBOX_LAG_SECONDS: u32 = 12; // Maximum number of subtrees a L2ToL1Msg unbalanced tree can have. Used when calculating the out hash of a tx. pub global MAX_L2_TO_L1_MSG_SUBTREES_PER_TX: u32 = 3; // ceil(log2(MAX_L2_TO_L1_MSGS_PER_TX)) diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 09c3eacc19cc..acd2de75317c 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -384,6 +384,7 @@ export type EnvVar = | 'FISHERMAN_MODE' | 'MAX_ALLOWED_ETH_CLIENT_DRIFT_SECONDS' | 'MAX_BLOCKS_PER_CHECKPOINT' + | 'STREAMING_INBOX' | 'LEGACY_BLS_CLI' | 'DEBUG_FORCE_TX_PROOF_VERIFICATION' | 'VALIDATOR_ALLOW_EPHEMERAL_SIGNING_PROTECTION' diff --git a/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.ts b/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.ts index 59176228a08c..8a14968a9288 100644 --- a/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.ts +++ b/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.ts @@ -70,9 +70,14 @@ export class LightweightCheckpointBuilder { db: MerkleTreeWriteOperations, bindings?: LoggerBindings, feeAssetPriceModifier: bigint = 0n, + // Streaming Inbox (AZIP-22 Fast Inbox): messages are inserted per block via `addBlock`, so `l1ToL2Messages` here + // is empty and the up-front checkpoint-wide insertion is skipped. + insertMessagesPerBlock: boolean = false, ): Promise { - // Insert l1-to-l2 messages into the tree. - await appendL1ToL2MessagesToTree(db, l1ToL2Messages); + // Insert l1-to-l2 messages into the tree (legacy flow: the whole checkpoint's messages up front). + if (!insertMessagesPerBlock) { + await appendL1ToL2MessagesToTree(db, l1ToL2Messages); + } return new LightweightCheckpointBuilder( checkpointNumber, @@ -172,7 +177,7 @@ export class LightweightCheckpointBuilder { public async addBlock( globalVariables: GlobalVariables, txs: ProcessedTx[], - opts: { insertTxsEffects?: boolean; expectedEndState?: StateReference } = {}, + opts: { insertTxsEffects?: boolean; expectedEndState?: StateReference; l1ToL2Messages?: Fr[] } = {}, ): Promise<{ block: L2Block; timings: Record }> { const timings: Record = {}; const isFirstBlock = this.blocks.length === 0; @@ -203,6 +208,20 @@ export class LightweightCheckpointBuilder { timings.insertSideEffects = msInsertSideEffects; } + // Streaming Inbox (AZIP-22 Fast Inbox): insert this block's L1-to-L2 message bundle before reading the end state, + // so the block header's L1-to-L2 tree snapshot reflects it. First-in-checkpoint bundles are padded to + // NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP (matching the legacy per-checkpoint insertion and the world-state + // synchronizer); non-first bundles are appended compactly. The logical (unpadded) messages are accumulated only + // once the block is fully built (below), so a mid-build failure does not pollute the checkpoint's inHash/rolling + // hash; the rolling hash and inHash are recomputed over them at checkpoint completion. + if (opts.l1ToL2Messages !== undefined) { + if (isFirstBlock) { + await appendL1ToL2MessagesToTree(this.db, opts.l1ToL2Messages); + } else { + await this.db.appendLeaves(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, opts.l1ToL2Messages); + } + } + const [msGetEndState, endState] = await elapsed(() => this.db.getStateReference()); timings.getEndState = msGetEndState; @@ -238,6 +257,12 @@ export class LightweightCheckpointBuilder { const block = new L2Block(newArchive, header, body, this.checkpointNumber, indexWithinCheckpoint); this.blocks.push(block); + // Accumulate the streaming bundle now that the block is fully built, so a mid-build throw above leaves the + // checkpoint's message list (and thus its inHash/rolling hash) consistent with the blocks actually built. + if (opts.l1ToL2Messages !== undefined) { + this.l1ToL2Messages.push(...opts.l1ToL2Messages); + } + const [msSpongeAbsorb] = await elapsed(() => this.spongeBlob.absorb(blockBlobFields)); timings.spongeAbsorb = msSpongeAbsorb; this.blobFields.push(...blockBlobFields); diff --git a/yarn-project/sequencer-client/src/config.ts b/yarn-project/sequencer-client/src/config.ts index 09c73d6625f2..90c22c71209d 100644 --- a/yarn-project/sequencer-client/src/config.ts +++ b/yarn-project/sequencer-client/src/config.ts @@ -68,6 +68,7 @@ export const DefaultSequencerConfig = { skipPushProposedBlocksToArchiver: false, skipPublishingCheckpointsPercent: 0, maxBlocksPerCheckpoint: DEFAULT_MAX_BLOCKS_PER_CHECKPOINT, + streamingInbox: false, } satisfies ResolvedSequencerConfig; /** diff --git a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts index 1dad18d8fdf8..13bbca84fdce 100644 --- a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts @@ -40,7 +40,7 @@ import { type ResolvedSequencerConfig, type WorldStateSynchronizer, } from '@aztec/stdlib/interfaces/server'; -import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging'; +import type { InboxBucket, L1ToL2MessageSource } from '@aztec/stdlib/messaging'; import { BlockProposal, CheckpointProposal, type CoordinationSignatureContext } from '@aztec/stdlib/p2p'; import { CheckpointHeader } from '@aztec/stdlib/rollup'; import type { ProposerTimetable, SubslotSelection } from '@aztec/stdlib/timetable'; @@ -1394,6 +1394,59 @@ describe('CheckpointProposalJob', () => { }); }); + describe('streaming inbox (flag on)', () => { + beforeEach(() => { + job.setTimetable(makeProposerTimetable({ l1Constants, blockDurationMs: 3000 })); + job.updateConfig({ streamingInbox: true }); + }); + + it('streams per-block bucket bundles, advances the reference, and skips the bulk message fetch', async () => { + jest + .spyOn(job.getTimetable(), 'selectNextSubslot') + .mockReturnValueOnce(subslot(10, 0, false)) + .mockReturnValueOnce(subslot(18, 1, true)) + .mockReturnValue(noSubslot()); + + // The archiver returns the newest synced bucket (seq 2) for any lag/cutoff lookup, so the first block + // consumes through it and the second block (cursor already at seq 2) consumes nothing and reuses the ref. + const bucket: InboxBucket = { + seq: 2n, + inboxRollingHash: new Fr(99), + totalMsgCount: 5n, + timestamp: 0n, + msgCount: 2, + lastMessageIndex: 4n, + }; + const bundle = Array.from({ length: 5 }, (_, i) => new Fr(i + 1)); + l1ToL2MessageSource.getLatestInboxBucketAtOrBefore.mockResolvedValue(bucket); + l1ToL2MessageSource.getL1ToL2MessagesBetweenBuckets.mockResolvedValue(bundle); + + const { lastBlock } = await setupMultipleBlocks(2, [2, 1]); + validatorClient.collectAttestations.mockResolvedValue(getAttestations(lastBlock)); + + const checkpoint = await job.executeAndAwait(); + + expect(checkpoint).toBeDefined(); + + // Streaming skips the bulk per-checkpoint fetch and tells the builder to insert messages per block. + expect(l1ToL2MessageSource.getL1ToL2Messages).not.toHaveBeenCalled(); + expect(checkpointsBuilder.startCheckpointCalls).toHaveLength(1); + expect(checkpointsBuilder.startCheckpointCalls[0].l1ToL2Messages).toEqual([]); + expect(checkpointsBuilder.startCheckpointCalls[0].insertMessagesPerBlock).toBe(true); + + // The first block consumes the selected bundle; the second consumes nothing. + expect(checkpointBuilder.buildBlockCalls).toHaveLength(2); + expect(checkpointBuilder.buildBlockCalls[0].opts.l1ToL2Messages).toEqual(bundle); + expect(checkpointBuilder.buildBlockCalls[1].opts.l1ToL2Messages).toEqual([]); + + // Both block proposals carry the selected bucket reference (the second reuses the first's). + const bucketRefArgs = validatorClient.createBlockProposal.mock.calls.map(call => call[8]); + expect(bucketRefArgs).toHaveLength(2); + expect(bucketRefArgs[0]?.bucketSeq).toBe(2n); + expect(bucketRefArgs[1]?.bucketSeq).toBe(2n); + }); + }); + describe('build single block', () => { it('does not build a block if not enough valid txs are collected', async () => { // We have enough txs, but not enough valid ones diff --git a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts index 6fa6212fb0f8..b8c9cf995d25 100644 --- a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts +++ b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts @@ -1,3 +1,4 @@ +import { INBOX_LAG_SECONDS, MAX_L1_TO_L2_MSGS_PER_BLOCK, MAX_L1_TO_L2_MSGS_PER_CHECKPOINT } from '@aztec/constants'; import { type EpochCache, PROPOSER_PIPELINING_SLOT_OFFSET } from '@aztec/epoch-cache'; import type { SimulationOverridesPlan } from '@aztec/ethereum/contracts'; import { @@ -51,7 +52,7 @@ import { type ResolvedSequencerConfig, type WorldStateSynchronizer, } from '@aztec/stdlib/interfaces/server'; -import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging'; +import { InboxBucketRef, type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging'; import type { BlockProposal, BlockProposalOptions, @@ -75,6 +76,11 @@ import type { CheckpointProposalJobMetricsRecorder } from './checkpoint_proposal import { CheckpointVoter } from './checkpoint_voter.js'; import { SequencerInterruptedError } from './errors.js'; import type { SequencerEvents } from './events.js'; +import { + type ConsumedBucketCursor, + type InboxBucketSelection, + selectInboxBucketForBlock, +} from './inbox_bucket_selector.js'; import type { SequencerMetrics } from './metrics.js'; import type { RequestsTracker } from './requests_tracker.js'; import type { SequencerRollupConstants } from './types.js'; @@ -98,6 +104,21 @@ type CheckpointProposalResult = { attestationsSignature: Signature; }; +/** + * Running state of streaming Inbox message selection across the blocks of one checkpoint (AZIP-22 Fast Inbox). + * Consumption starts from the parent checkpoint's last-consumed bucket and advances one block at a time. + */ +type StreamingCheckpointState = { + /** Cumulative Inbox message count consumed as of the parent checkpoint; the per-checkpoint cap origin (fixed). */ + checkpointStartTotalMsgCount: bigint; + /** The last bucket consumed so far (parent checkpoint's at the first block); advances as blocks consume. */ + parent: ConsumedBucketCursor; + /** Reference to the last consumed bucket; reused by blocks that consume nothing. */ + lastBucketRef: InboxBucketRef; + /** All message leaves consumed so far this checkpoint, in insertion order; drives the running `inHash`. */ + accumulatedMessages: Fr[]; +}; + /** * Handles the execution of a checkpoint proposal after the initial preparation phase. * This includes building blocks, collecting attestations, and publishing the checkpoint to L1, @@ -611,9 +632,14 @@ export class CheckpointProposalJob implements Traceable { this.checkpointSimulationOverridesPlan, ); - // Collect L1 to L2 messages for the checkpoint and compute their hash - const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber); - const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages); + // Collect L1 to L2 messages for the checkpoint and compute their hash. Under the streaming Inbox (AZIP-22 + // Fast Inbox) messages are selected per block instead, so the bulk fetch and up-front insertion are skipped + // and both start empty; the running values are computed block by block in the build loop. + const streamingInbox = this.config.streamingInbox; + const l1ToL2Messages = streamingInbox + ? [] + : await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber); + const inHash = streamingInbox ? Fr.ZERO : computeInHashFromL1ToL2Messages(l1ToL2Messages); // Collect the out hashes of all the checkpoints before this one in the same epoch. // Under pipelining the parent checkpoint may not be on L1 yet at build time, so the helper @@ -640,6 +666,12 @@ export class CheckpointProposalJob implements Traceable { log: this.log, }); + // Streaming Inbox: resolve where this checkpoint's consumption starts (the parent checkpoint's last-consumed + // bucket). Only the genesis base case is wired here; see resolveStreamingCheckpointStart. + const streamingState = streamingInbox + ? await this.resolveStreamingCheckpointStart(previousInboxRollingHash) + : undefined; + // Anchor the modifier to the predicted parent fee header: L1 will apply it against // that, not against the latest published checkpoint (which lags by one under pipelining). const predictedParentEthPerFeeAssetE12 = @@ -659,6 +691,7 @@ export class CheckpointProposalJob implements Traceable { previousInboxRollingHash, fork, this.log.getBindings(), + streamingInbox, ); // Options for the validator client when creating block and checkpoint proposals @@ -684,6 +717,7 @@ export class CheckpointProposalJob implements Traceable { checkpointGlobalVariables.timestamp, inHash, blockProposalOptions, + streamingState, ); blocksInCheckpoint = result.blocksInCheckpoint; blockPendingBroadcast = result.blockPendingBroadcast; @@ -875,6 +909,7 @@ export class CheckpointProposalJob implements Traceable { timestamp: bigint, inHash: Fr, blockProposalOptions: BlockProposalOptions, + streamingState?: StreamingCheckpointState, ): Promise<{ blocksInCheckpoint: L2Block[]; blockPendingBroadcast: BlockProposal | undefined; @@ -912,6 +947,18 @@ export class CheckpointProposalJob implements Traceable { break; } + // Streaming Inbox: select this block's message bundle against the current (not-yet-advanced) consumption + // cursor. The state is only advanced once the block builds successfully, so a failed build (retried in a + // later sub-slot) re-derives the bundle rather than losing it. The builder inserts the bundle and rolls it + // back with the fork on failure. The censorship cutoff floor must apply on whichever block ends the + // checkpoint, which includes the block that reaches the per-checkpoint block cap, not just the timetable's + // last sub-slot. + const isCheckpointFinalBlock = timingInfo.isLastBlock || blocksBuilt + 1 >= this.config.maxBlocksPerCheckpoint; + const selection = streamingState + ? await this.selectStreamingBundle(streamingState, isCheckpointFinalBlock, nowSeconds) + : undefined; + const streamingBundle = streamingState ? (selection && selection.consume ? selection.bundle : []) : undefined; + const buildResult = await this.buildSingleBlock(checkpointBuilder, { // Create all blocks with the same timestamp blockTimestamp: timestamp, @@ -921,6 +968,7 @@ export class CheckpointProposalJob implements Traceable { blockNumber, indexWithinCheckpoint, txHashesAlreadyIncluded, + l1ToL2Messages: streamingBundle, }); // If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot @@ -956,13 +1004,35 @@ export class CheckpointProposalJob implements Traceable { blocksInCheckpoint.push(block); usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString())); + // Streaming Inbox: the block built successfully, so advance the consumption cursor and derive this block's + // rolling-hash bucket reference and `inHash`. A block that consumed nothing reuses the parent bucket + // reference; the running `inHash` over the accumulated messages matches the checkpoint header's `inHash` + // once the last block is reached. + let blockInHash = inHash; + let blockBucketRef: InboxBucketRef | undefined = undefined; + if (streamingState && selection) { + if (selection.consume) { + streamingState.parent = { seq: selection.bucket.seq, totalMsgCount: selection.bucket.totalMsgCount }; + streamingState.lastBucketRef = InboxBucketRef.fromBucket(selection.bucket); + streamingState.accumulatedMessages.push(...selection.bundle); + } + blockBucketRef = streamingState.lastBucketRef; + blockInHash = computeInHashFromL1ToL2Messages(streamingState.accumulatedMessages); + } + // Sign the block proposal. This will throw if HA signing fails. - const proposal = await this.createBlockProposal(block, inHash, usedTxs, { - ...blockProposalOptions, - broadcastInvalidBlockProposal: - blockProposalOptions.broadcastInvalidBlockProposal || - block.indexWithinCheckpoint === this.config.invalidBlockProposalIndexWithinCheckpoint, - }); + const proposal = await this.createBlockProposal( + block, + blockInHash, + usedTxs, + { + ...blockProposalOptions, + broadcastInvalidBlockProposal: + blockProposalOptions.broadcastInvalidBlockProposal || + block.indexWithinCheckpoint === this.config.invalidBlockProposalIndexWithinCheckpoint, + }, + blockBucketRef, + ); // Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal, // so we avoid polluting our archive with a block that would fail. @@ -1005,6 +1075,7 @@ export class CheckpointProposalJob implements Traceable { inHash: Fr, usedTxs: Tx[], blockProposalOptions: BlockProposalOptions, + bucketRef?: InboxBucketRef, ): Promise { if (this.config.fishermanMode) { this.log.info(`Skipping block proposal for block ${block.number} in fisherman mode`); @@ -1019,9 +1090,59 @@ export class CheckpointProposalJob implements Traceable { usedTxs, this.proposer, blockProposalOptions, + bucketRef, ); } + /** + * Resolves where a streaming-Inbox checkpoint's consumption starts: the parent checkpoint's last-consumed bucket + * (AZIP-22 Fast Inbox). Only the genesis base case is wired: when the parent's rolling hash is zero, consumption + * begins from the start of the Inbox. Sourcing a non-genesis parent's bucket needs the consumed reference carried + * across checkpoints (there is no by-rolling-hash archiver lookup, and legacy parents do not sit on a bucket + * boundary); that is a flip-time concern. Pre-flip the flag is off, so this throw only fires if the flag is turned + * on against a non-genesis chain, where the outer catch turns it into a skipped (unsubmitted) proposal. + */ + private resolveStreamingCheckpointStart(previousInboxRollingHash: Fr): Promise { + if (!previousInboxRollingHash.isZero()) { + throw new Error( + `Streaming inbox: cannot source parent checkpoint bucket for non-genesis rolling hash ${previousInboxRollingHash} ` + + `(checkpoint ${this.checkpointNumber}); cross-checkpoint streaming is wired at the flip`, + ); + } + return Promise.resolve({ + checkpointStartTotalMsgCount: 0n, + parent: { seq: 0n, totalMsgCount: 0n }, + lastBucketRef: InboxBucketRef.empty(), + accumulatedMessages: [], + }); + } + + /** + * Selects this block's streaming-Inbox message bundle against the current consumption cursor, mirroring the L1 + * predicate in `ProposeLib.validateInboxConsumption` (AZIP-22 Fast Inbox). Does not mutate the cursor; the caller + * advances it only after the block builds successfully. + */ + private selectStreamingBundle( + state: StreamingCheckpointState, + isLastBlock: boolean, + nowSeconds: number, + ): Promise { + // Mirror ProposeLib's cutoff exactly: buildFrameStart = toTimestamp(slot - 1), cutoff = buildFrameStart - lag. + const buildFrameStart = getTimestampForSlot(SlotNumber(this.targetSlot - 1), this.l1Constants); + const cutoffTimestamp = buildFrameStart - BigInt(INBOX_LAG_SECONDS); + return selectInboxBucketForBlock({ + messageSource: this.l1ToL2MessageSource, + now: BigInt(Math.floor(nowSeconds)), + lagSeconds: BigInt(INBOX_LAG_SECONDS), + parent: state.parent, + checkpointStartTotalMsgCount: state.checkpointStartTotalMsgCount, + perBlockCap: MAX_L1_TO_L2_MSGS_PER_BLOCK, + perCheckpointCap: MAX_L1_TO_L2_MSGS_PER_CHECKPOINT, + isLastBlock, + cutoffTimestamp, + }); + } + /** * Sleeps until it is time to produce the next block in the slot. * @param nextSubslotStart - Absolute wall-clock timestamp in seconds of the previous sub-slot deadline. @@ -1046,12 +1167,21 @@ export class CheckpointProposalJob implements Traceable { indexWithinCheckpoint: IndexWithinCheckpoint; buildDeadline: Date | undefined; txHashesAlreadyIncluded: Set; + /** Streaming Inbox message bundle to insert into this block's L1-to-L2 tree; undefined in the legacy flow. */ + l1ToL2Messages?: Fr[]; }, ): Promise< { block: L2Block; usedTxs: Tx[] } | { failure: 'insufficient-txs' | 'insufficient-valid-txs' } | { error: Error } > { - const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } = - opts; + const { + blockTimestamp, + forceCreate, + blockNumber, + indexWithinCheckpoint, + buildDeadline, + txHashesAlreadyIncluded, + l1ToL2Messages, + } = opts; this.log.verbose( `Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot}`, @@ -1118,6 +1248,7 @@ export class CheckpointProposalJob implements Traceable { maxBlocksPerCheckpoint: this.timetable.getMaxBlocksPerCheckpoint(), perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier, perBlockDAAllocationMultiplier: this.config.perBlockDAAllocationMultiplier, + l1ToL2Messages, }; // Actually build the block by executing txs. The builder throws InsufficientValidTxsError diff --git a/yarn-project/sequencer-client/src/sequencer/inbox_bucket_selector.test.ts b/yarn-project/sequencer-client/src/sequencer/inbox_bucket_selector.test.ts new file mode 100644 index 000000000000..3e811527d29f --- /dev/null +++ b/yarn-project/sequencer-client/src/sequencer/inbox_bucket_selector.test.ts @@ -0,0 +1,274 @@ +import { Fr } from '@aztec/foundation/curves/bn254'; +import type { InboxBucket } from '@aztec/stdlib/messaging'; + +import { type InboxBucketSource, selectInboxBucketForBlock } from './inbox_bucket_selector.js'; + +/** A test bucket: its cumulative totals and leaves are derived from a running message count. */ +type TestBucketSpec = { seq: bigint; timestamp: bigint; msgCount: number }; + +/** + * Builds an in-memory {@link InboxBucketSource} from a list of bucket specs, mirroring the archiver's dense, + * timestamp-ordered buckets. Bucket seq 0 is the genesis sentinel (never a real bucket); real buckets start at 1. + */ +function makeSource(specs: TestBucketSpec[]): { + source: InboxBucketSource; + buckets: Map; + leaves: Fr[]; +} { + const leaves: Fr[] = []; + const buckets = new Map(); + let total = 0n; + for (const spec of specs) { + for (let i = 0; i < spec.msgCount; i++) { + leaves.push(new Fr(spec.seq * 1000n + BigInt(i))); + } + total += BigInt(spec.msgCount); + buckets.set(spec.seq, { + seq: spec.seq, + inboxRollingHash: new Fr(spec.seq), + totalMsgCount: total, + timestamp: spec.timestamp, + msgCount: spec.msgCount, + lastMessageIndex: total - 1n, + }); + } + + const ordered = [...buckets.values()].sort((a, b) => Number(a.seq - b.seq)); + const source: InboxBucketSource = { + getInboxBucket: (seq: bigint) => Promise.resolve(buckets.get(seq)), + getLatestInboxBucketAtOrBefore: (timestamp: bigint) => { + const eligible = ordered.filter(b => b.timestamp <= timestamp); + return Promise.resolve(eligible.length === 0 ? undefined : eligible[eligible.length - 1]); + }, + getL1ToL2MessagesBetweenBuckets: (fromExclusive: bigint, toInclusive: bigint) => { + const toBucket = buckets.get(toInclusive); + if (toBucket === undefined) { + return Promise.resolve([]); + } + let startIndex = 0n; + if (fromExclusive > 0n) { + const fromBucket = buckets.get(fromExclusive); + if (fromBucket === undefined) { + return Promise.resolve([]); + } + startIndex = fromBucket.lastMessageIndex + 1n; + } + return Promise.resolve(leaves.slice(Number(startIndex), Number(toBucket.lastMessageIndex + 1n))); + }, + }; + + return { source, buckets, leaves }; +} + +const GENESIS_PARENT = { seq: 0n, totalMsgCount: 0n }; + +// Pinned cross-layer values from A-1371-resolution §13: genesisTime=100000, slotDuration=36, INBOX_LAG_SECONDS=12. +const GENESIS_TIME = 100000n; +const SLOT_DURATION = 36n; +const LAG = 12n; +const cutoffForSlot = (slot: bigint) => GENESIS_TIME + (slot - 1n) * SLOT_DURATION - LAG; + +describe('selectInboxBucketForBlock', () => { + const baseInput = { + lagSeconds: LAG, + checkpointStartTotalMsgCount: 0n, + perBlockCap: 1024, + perCheckpointCap: 1024, + isLastBlock: false, + cutoffTimestamp: 0n, + }; + + it('consumes nothing from an empty Inbox', async () => { + const { source } = makeSource([]); + const result = await selectInboxBucketForBlock({ + ...baseInput, + messageSource: source, + now: 1_000_000n, + parent: GENESIS_PARENT, + }); + expect(result.consume).toBe(false); + }); + + it('picks the newest lag-eligible bucket and derives its bundle from genesis', async () => { + const { source } = makeSource([ + { seq: 1n, timestamp: 100n, msgCount: 3 }, + { seq: 2n, timestamp: 200n, msgCount: 2 }, + { seq: 3n, timestamp: 300n, msgCount: 4 }, + ]); + // now - lag = 250 -> newest bucket at-or-before 250 is seq 2. + const result = await selectInboxBucketForBlock({ + ...baseInput, + messageSource: source, + now: 250n + LAG, + parent: GENESIS_PARENT, + }); + expect(result).toMatchObject({ consume: true }); + if (result.consume) { + expect(result.bucket.seq).toBe(2n); + expect(result.bundle).toHaveLength(5); // buckets 1 (3) + 2 (2) + } + }); + + it('treats a bucket exactly lagSeconds old as eligible (inclusive lag boundary)', async () => { + const { source } = makeSource([{ seq: 1n, timestamp: 500n, msgCount: 1 }]); + // Bucket age exactly == lag: timestamp == now - lag. + const result = await selectInboxBucketForBlock({ + ...baseInput, + messageSource: source, + now: 500n + LAG, + parent: GENESIS_PARENT, + }); + expect(result.consume).toBe(true); + + // One second younger than the lag boundary: not yet eligible. + const tooYoung = await selectInboxBucketForBlock({ + ...baseInput, + messageSource: source, + now: 500n + LAG - 1n, + parent: GENESIS_PARENT, + }); + expect(tooYoung.consume).toBe(false); + }); + + it('walks back to the newest bucket that fits the per-block cap', async () => { + const { source } = makeSource([ + { seq: 1n, timestamp: 100n, msgCount: 200 }, + { seq: 2n, timestamp: 200n, msgCount: 200 }, + { seq: 3n, timestamp: 300n, msgCount: 200 }, + ]); + // Newest eligible is seq 3 (600 msgs from genesis), but perBlockCap=400 only fits through seq 2. + const result = await selectInboxBucketForBlock({ + ...baseInput, + messageSource: source, + now: 300n + LAG, + parent: GENESIS_PARENT, + perBlockCap: 400, + }); + expect(result).toMatchObject({ consume: true }); + if (result.consume) { + expect(result.bucket.seq).toBe(2n); + expect(result.bundle).toHaveLength(400); + } + }); + + it('accumulates the per-checkpoint cap across blocks', async () => { + const { source, buckets } = makeSource([ + { seq: 1n, timestamp: 100n, msgCount: 600 }, + { seq: 2n, timestamp: 200n, msgCount: 600 }, + ]); + // Block 1 already consumed bucket 1 (600 msgs). perCheckpointCap=1000 leaves only 400 headroom, but bucket 2 + // would bring the checkpoint total to 1200 > 1000, so block 2 consumes nothing (cap-escape). + const result = await selectInboxBucketForBlock({ + ...baseInput, + messageSource: source, + now: 200n + LAG, + parent: { seq: 1n, totalMsgCount: buckets.get(1n)!.totalMsgCount }, + checkpointStartTotalMsgCount: 0n, + perCheckpointCap: 1000, + }); + expect(result.consume).toBe(false); + }); + + it('advances across sub-slots as buckets arrive', async () => { + const { source } = makeSource([ + { seq: 1n, timestamp: 100n, msgCount: 2 }, + { seq: 2n, timestamp: 260n, msgCount: 3 }, + ]); + // Block 1 at now-lag=150: only bucket 1 eligible. + const first = await selectInboxBucketForBlock({ + ...baseInput, + messageSource: source, + now: 150n + LAG, + parent: GENESIS_PARENT, + }); + expect(first).toMatchObject({ consume: true }); + if (!first.consume) { + return; + } + expect(first.bucket.seq).toBe(1n); + + // Block 2, later sub-slot (now-lag=300): bucket 2 has now aged in; parent is block 1's bucket. + const second = await selectInboxBucketForBlock({ + ...baseInput, + messageSource: source, + now: 300n + LAG, + parent: { seq: first.bucket.seq, totalMsgCount: first.bucket.totalMsgCount }, + checkpointStartTotalMsgCount: 0n, + }); + expect(second).toMatchObject({ consume: true }); + if (second.consume) { + expect(second.bucket.seq).toBe(2n); + expect(second.bundle).toHaveLength(3); // only bucket 2's messages, not bucket 1's + expect(second.bundle).toEqual(await source.getL1ToL2MessagesBetweenBuckets(1n, 2n)); + } + }); + + it('applies the cutoff as a consumption floor on the last block', async () => { + // Bucket sits at the cutoff for slot 10 but is younger than now-lag, so a non-last block skips it. + const cutoff = cutoffForSlot(10n); + const { source } = makeSource([{ seq: 1n, timestamp: cutoff, msgCount: 5 }]); + + const nonLast = await selectInboxBucketForBlock({ + ...baseInput, + messageSource: source, + now: cutoff + LAG - 1n, // bucket is one second too young for lag eligibility + parent: GENESIS_PARENT, + isLastBlock: false, + cutoffTimestamp: cutoff, + }); + expect(nonLast.consume).toBe(false); + + const last = await selectInboxBucketForBlock({ + ...baseInput, + messageSource: source, + now: cutoff + LAG - 1n, + parent: GENESIS_PARENT, + isLastBlock: true, + cutoffTimestamp: cutoff, + }); + expect(last).toMatchObject({ consume: true }); + if (last.consume) { + expect(last.bucket.seq).toBe(1n); + } + }); + + it('makes a bucket exactly at the cutoff mandatory and one past it optional (§13 boundary)', async () => { + const cutoff = cutoffForSlot(10n); // 100312 + // A bucket AT the cutoff must be consumed by the last block; a bucket one second past it need not be. + const atCutoff = makeSource([{ seq: 1n, timestamp: cutoff, msgCount: 1 }]); + const pastCutoff = makeSource([{ seq: 1n, timestamp: cutoff + 1n, msgCount: 1 }]); + + const mustConsume = await selectInboxBucketForBlock({ + ...baseInput, + messageSource: atCutoff.source, + now: cutoff, // before lag would make it eligible, forcing reliance on the cutoff floor + parent: GENESIS_PARENT, + isLastBlock: true, + cutoffTimestamp: cutoff, + }); + expect(mustConsume.consume).toBe(true); + + const mayskip = await selectInboxBucketForBlock({ + ...baseInput, + messageSource: pastCutoff.source, + now: cutoff, + parent: GENESIS_PARENT, + isLastBlock: true, + cutoffTimestamp: cutoff, + }); + expect(mayskip.consume).toBe(false); + }); + + it('consumes nothing when even the first bucket past the parent exceeds the per-checkpoint cap (cap-escape)', async () => { + const { source } = makeSource([{ seq: 1n, timestamp: 100n, msgCount: 2000 }]); + const result = await selectInboxBucketForBlock({ + ...baseInput, + messageSource: source, + now: 100n + LAG, + parent: GENESIS_PARENT, + perBlockCap: 4096, + perCheckpointCap: 1024, + }); + expect(result.consume).toBe(false); + }); +}); diff --git a/yarn-project/sequencer-client/src/sequencer/inbox_bucket_selector.ts b/yarn-project/sequencer-client/src/sequencer/inbox_bucket_selector.ts new file mode 100644 index 000000000000..e56c4bcaef3f --- /dev/null +++ b/yarn-project/sequencer-client/src/sequencer/inbox_bucket_selector.ts @@ -0,0 +1,122 @@ +import type { Fr } from '@aztec/foundation/curves/bn254'; +import type { InboxBucket, L1ToL2MessageSource } from '@aztec/stdlib/messaging'; + +/** The subset of the archiver's Inbox-bucket queries the selector needs (AZIP-22 Fast Inbox). */ +export type InboxBucketSource = Pick< + L1ToL2MessageSource, + 'getInboxBucket' | 'getLatestInboxBucketAtOrBefore' | 'getL1ToL2MessagesBetweenBuckets' +>; + +/** + * The last-consumed Inbox bucket a block streams from. Only the sequence number and cumulative message count are + * needed: the sequence number bounds the derived bundle, and the count is the per-block/per-checkpoint cap origin. + * At a checkpoint's first block this is the parent checkpoint's last-consumed bucket; the genesis base case is + * `{ seq: 0, totalMsgCount: 0 }` (bundles derive from the start of the Inbox). + */ +export type ConsumedBucketCursor = Pick; + +/** Inputs to a single block's streaming Inbox-bucket selection. */ +export type SelectInboxBucketInput = { + /** Archiver Inbox-bucket queries. */ + messageSource: InboxBucketSource; + /** Wall-clock time of this sub-slot, in seconds; the lag-eligibility anchor. */ + now: bigint; + /** Minimum bucket age in seconds (`INBOX_LAG_SECONDS`) for a bucket to be lag-eligible this sub-slot. */ + lagSeconds: bigint; + /** The last bucket consumed by this checkpoint so far (parent checkpoint's at the first block). */ + parent: ConsumedBucketCursor; + /** Cumulative Inbox message count consumed as of the parent checkpoint; the per-checkpoint cap origin. */ + checkpointStartTotalMsgCount: bigint; + /** Maximum number of messages this block may consume. */ + perBlockCap: number; + /** Maximum number of messages the checkpoint may consume in total. */ + perCheckpointCap: number; + /** True on the checkpoint's final block, where the censorship cutoff becomes a consumption floor. */ + isLastBlock: boolean; + /** + * Censorship cutoff timestamp, `toTimestamp(slot - 1) - lagSeconds` (mirrors `ProposeLib.validateInboxConsumption`). + * Buckets opened at or before it are mandatory to consume by the checkpoint's last block. + */ + cutoffTimestamp: bigint; +}; + +/** Result of a block's streaming Inbox-bucket selection. */ +export type InboxBucketSelection = + | { + /** The block consumes messages, advancing to `bucket`. */ + consume: true; + /** The newest bucket this block consumes through. */ + bucket: InboxBucket; + /** The message leaves consumed this block, in insertion order (may be empty for an empty bucket). */ + bundle: Fr[]; + } + | { + /** The block consumes nothing; it reuses the parent bucket reference. */ + consume: false; + }; + +/** + * Selects the newest Inbox bucket a block streams from, mirroring the L1 consumption predicate in + * `ProposeLib.validateInboxConsumption` (AZIP-22 Fast Inbox). The policy, per block: + * + * 1. Pick the newest lag-eligible bucket: the newest bucket opened at or before `now - lagSeconds`. On the + * checkpoint's last block, also consider the cutoff bucket (newest opened at or before `cutoffTimestamp`) and take + * whichever is newer, so the checkpoint reaches the censorship floor even if the sub-slot lag preferred less. + * 2. If nothing is newer than the parent bucket, consume nothing. + * 3. Otherwise walk back from the candidate to the newest bucket whose consumption fits both the per-block cap + * (`bucket.totalMsgCount - parent.totalMsgCount`) and the per-checkpoint cap + * (`bucket.totalMsgCount - checkpointStartTotalMsgCount`). If even the first bucket past the parent overshoots the + * per-checkpoint cap, consume nothing — the L1 cap-escape (`ProposeLib` allows leaving a bucket unconsumed when + * consuming through it would exceed the per-checkpoint cap). + * + * The `<=` comparisons make a bucket exactly `lagSeconds` old lag-eligible and a bucket exactly at the cutoff + * mandatory, matching the strict `>` "past cutoff" test on L1 (`next.timestamp > cutoff` leaves it optional). + * + * A single bucket never exceeds the per-block cap by construction (the Inbox bucket size is at most the per-block cap), + * so per-block walk-back always lands on at least one bucket; only the per-checkpoint cap can force consuming nothing. + */ +export async function selectInboxBucketForBlock(input: SelectInboxBucketInput): Promise { + const { + messageSource, + now, + lagSeconds, + parent, + checkpointStartTotalMsgCount, + perBlockCap, + perCheckpointCap, + isLastBlock, + cutoffTimestamp, + } = input; + + let candidate = await messageSource.getLatestInboxBucketAtOrBefore(now - lagSeconds); + + if (isLastBlock) { + const cutoffBucket = await messageSource.getLatestInboxBucketAtOrBefore(cutoffTimestamp); + if (cutoffBucket !== undefined && (candidate === undefined || cutoffBucket.seq > candidate.seq)) { + candidate = cutoffBucket; + } + } + + if (candidate === undefined || candidate.seq <= parent.seq) { + return { consume: false }; + } + + const perBlockCapBig = BigInt(perBlockCap); + const perCheckpointCapBig = BigInt(perCheckpointCap); + + let selected: InboxBucket | undefined = candidate; + while (selected !== undefined && selected.seq > parent.seq) { + const blockCount = selected.totalMsgCount - parent.totalMsgCount; + const checkpointCount = selected.totalMsgCount - checkpointStartTotalMsgCount; + if (blockCount <= perBlockCapBig && checkpointCount <= perCheckpointCapBig) { + const bundle = await messageSource.getL1ToL2MessagesBetweenBuckets(parent.seq, selected.seq); + return { consume: true, bucket: selected, bundle }; + } + if (selected.seq - 1n <= parent.seq) { + break; + } + selected = await messageSource.getInboxBucket(selected.seq - 1n); + } + + return { consume: false }; +} diff --git a/yarn-project/sequencer-client/src/test/mock_checkpoint_builder.ts b/yarn-project/sequencer-client/src/test/mock_checkpoint_builder.ts index b7cfae439637..b5301c4a9cef 100644 --- a/yarn-project/sequencer-client/src/test/mock_checkpoint_builder.ts +++ b/yarn-project/sequencer-client/src/test/mock_checkpoint_builder.ts @@ -1,5 +1,6 @@ import { type BlockNumber, CheckpointNumber, IndexWithinCheckpoint } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; +import type { LoggerBindings } from '@aztec/foundation/log'; import { unfreeze } from '@aztec/foundation/types'; import { L2Block } from '@aztec/stdlib/block'; import { Checkpoint } from '@aztec/stdlib/checkpoint'; @@ -206,6 +207,7 @@ export class MockCheckpointsBuilder implements ICheckpointsBuilder { l1ToL2Messages: Fr[]; previousCheckpointOutHashes: Fr[]; feeAssetPriceModifier: bigint; + insertMessagesPerBlock: boolean | undefined; }> = []; public openCheckpointCalls: Array<{ checkpointNumber: CheckpointNumber; @@ -265,6 +267,8 @@ export class MockCheckpointsBuilder implements ICheckpointsBuilder { previousCheckpointOutHashes: Fr[], _previousInboxRollingHash: Fr, _fork: MerkleTreeWriteOperations, + _bindings?: LoggerBindings, + insertMessagesPerBlock?: boolean, ): Promise { this.startCheckpointCalls.push({ checkpointNumber, @@ -272,6 +276,7 @@ export class MockCheckpointsBuilder implements ICheckpointsBuilder { l1ToL2Messages, previousCheckpointOutHashes, feeAssetPriceModifier, + insertMessagesPerBlock, }); if (!this.checkpointBuilder) { diff --git a/yarn-project/stdlib/src/config/sequencer-config.ts b/yarn-project/stdlib/src/config/sequencer-config.ts index f08782c1ef54..874d2449bd20 100644 --- a/yarn-project/stdlib/src/config/sequencer-config.ts +++ b/yarn-project/stdlib/src/config/sequencer-config.ts @@ -1,5 +1,6 @@ import { type ConfigMappingsType, + booleanConfigHelper, floatConfigHelper, numberConfigHelper, optionalNumberConfigHelper, @@ -40,6 +41,7 @@ export const sharedSequencerConfigMappings: ConfigMappingsType< | 'checkpointProposalPrepareTime' | 'minBlockDuration' | 'maxBlocksPerCheckpoint' + | 'streamingInbox' > > = { blockDurationMs: { @@ -93,4 +95,12 @@ export const sharedSequencerConfigMappings: ConfigMappingsType< parseEnv: (val: string) => parseInt(val, 10), defaultValue: DEFAULT_MAX_BLOCKS_PER_CHECKPOINT, }, + streamingInbox: { + env: 'STREAMING_INBOX', + description: + 'Select L1-to-L2 messages per block from the streaming Inbox buckets (AZIP-22 Fast Inbox) instead of the whole ' + + "checkpoint's messages up front. Shared by the sequencer and validator, which must flip together. Default off: " + + 'pre-flip a checkpoint built with this on is expected to fail L1 submission.', + ...booleanConfigHelper(false), + }, }; diff --git a/yarn-project/stdlib/src/interfaces/block-builder.ts b/yarn-project/stdlib/src/interfaces/block-builder.ts index a925cfd2c08c..7846dba719b7 100644 --- a/yarn-project/stdlib/src/interfaces/block-builder.ts +++ b/yarn-project/stdlib/src/interfaces/block-builder.ts @@ -56,6 +56,12 @@ export type PublicProcessorLimits = { type BlockBuilderOptionsBase = PublicProcessorLimits & { /** Minimum number of successfully processed txs required. Block is rejected if fewer succeed. */ minValidTxs: number; + /** + * L1-to-L2 message leaves this block consumes, inserted into the fork's L1-to-L2 message tree before the block + * header is built (AZIP-22 Fast Inbox streaming). Omitted (or empty) in the legacy flow, where the whole + * checkpoint's messages are inserted up front at `startCheckpoint`. + */ + l1ToL2Messages?: Fr[]; }; /** Proposer mode: redistribution params are required. */ @@ -159,5 +165,8 @@ export interface ICheckpointsBuilder { previousInboxRollingHash: Fr, fork: MerkleTreeWriteOperations, bindings?: LoggerBindings, + // When true, the checkpoint's messages are inserted per block (via `buildBlock`'s `l1ToL2Messages`) rather than + // as one padded bundle up front, so `l1ToL2Messages` here must be empty (AZIP-22 Fast Inbox streaming). + insertMessagesPerBlock?: boolean, ): Promise; } diff --git a/yarn-project/stdlib/src/interfaces/configs.ts b/yarn-project/stdlib/src/interfaces/configs.ts index 14602960be57..28c24922a938 100644 --- a/yarn-project/stdlib/src/interfaces/configs.ts +++ b/yarn-project/stdlib/src/interfaces/configs.ts @@ -110,6 +110,13 @@ export interface SequencerConfig { expectedBlockProposalsPerSlot?: number; /** Have sequencer build and publish an empty checkpoint if there are no txs */ buildCheckpointIfEmpty?: boolean; + /** + * Select L1-to-L2 messages per block from the streaming Inbox buckets (AZIP-22 Fast Inbox), rather than consuming + * the whole checkpoint's messages up front. Default off: pre-flip, on-chain `propose` still enforces the legacy + * per-checkpoint consumption, so a checkpoint built with this on is expected to fail L1 submission. The sequencer + * and validator must flip together, so both read the same `STREAMING_INBOX` env var. + */ + streamingInbox?: boolean; /** Skip pushing proposed blocks to archiver (default: false) */ skipPushProposedBlocksToArchiver?: boolean; /** Minimum number of blocks required for a checkpoint proposal (test only, defaults to undefined = no minimum) */ @@ -174,6 +181,7 @@ export const SequencerConfigSchema = zodFor()( checkpointProposalSyncGraceSeconds: z.number().nonnegative().optional(), expectedBlockProposalsPerSlot: z.number().nonnegative().optional(), buildCheckpointIfEmpty: z.boolean().optional(), + streamingInbox: z.boolean().optional(), skipPushProposedBlocksToArchiver: z.boolean().optional(), minBlocksForCheckpoint: z.number().positive().optional(), skipPublishingCheckpointsPercent: z.number().gte(0).lte(100).optional(), diff --git a/yarn-project/validator-client/src/checkpoint_builder.ts b/yarn-project/validator-client/src/checkpoint_builder.ts index 801dd309e297..291298b3a574 100644 --- a/yarn-project/validator-client/src/checkpoint_builder.ts +++ b/yarn-project/validator-client/src/checkpoint_builder.ts @@ -131,9 +131,10 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { // Commit the fork checkpoint await forkCheckpoint.commit(); - // Add block to checkpoint + // Add block to checkpoint, inserting this block's streaming L1-to-L2 message bundle (if any) into the fork. const { block } = await this.checkpointBuilder.addBlock(globalVariables, processedTxs, { expectedEndState: opts.expectedEndState, + l1ToL2Messages: opts.l1ToL2Messages, }); this.contractsDB.commitCheckpoint(); @@ -322,6 +323,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { previousInboxRollingHash: Fr, fork: MerkleTreeWriteOperations, bindings?: LoggerBindings, + insertMessagesPerBlock: boolean = false, ): Promise { const stateReference = await fork.getStateReference(); const archiveTree = await fork.getTreeInfo(MerkleTreeId.ARCHIVE); @@ -344,6 +346,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { fork, bindings, feeAssetPriceModifier, + insertMessagesPerBlock, ); return new CheckpointBuilder( diff --git a/yarn-project/validator-client/src/duties/validation_service.ts b/yarn-project/validator-client/src/duties/validation_service.ts index 5fd753a7bdce..d5f2157d7a49 100644 --- a/yarn-project/validator-client/src/duties/validation_service.ts +++ b/yarn-project/validator-client/src/duties/validation_service.ts @@ -4,6 +4,7 @@ import type { EthAddress } from '@aztec/foundation/eth-address'; import type { Signature } from '@aztec/foundation/eth-signature'; import { createLogger } from '@aztec/foundation/log'; import { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block'; +import type { InboxBucketRef } from '@aztec/stdlib/messaging'; import { BlockProposal, type BlockProposalOptions, @@ -53,6 +54,7 @@ export class ValidationService { txs: Tx[], proposerAttesterAddress: EthAddress | undefined, options: BlockProposalOptions, + bucketRef?: InboxBucketRef, ): Promise { // For testing: change the new archive to trigger state_mismatch validation failure if (options.broadcastInvalidBlockProposal) { @@ -82,6 +84,7 @@ export class ValidationService { this.signatureContext, payloadSigner, txsSigner, + bucketRef, ); } diff --git a/yarn-project/validator-client/src/validator.ts b/yarn-project/validator-client/src/validator.ts index d81bc2f2fb66..97f8e6f9114e 100644 --- a/yarn-project/validator-client/src/validator.ts +++ b/yarn-project/validator-client/src/validator.ts @@ -31,7 +31,7 @@ import type { ValidatorClientFullConfig, WorldStateSynchronizer, } from '@aztec/stdlib/interfaces/server'; -import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging'; +import type { InboxBucketRef, L1ToL2MessageSource } from '@aztec/stdlib/messaging'; import { type BlockProposal, type BlockProposalOptions, @@ -922,6 +922,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter) txs: Tx[], proposerAddress: EthAddress | undefined, options: BlockProposalOptions = {}, + bucketRef?: InboxBucketRef, ): Promise { // Validate that we're not creating a proposal for an older or equal position if (this.lastProposedBlock) { @@ -953,6 +954,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter) broadcastInvalidBlockProposal: options.broadcastInvalidBlockProposal || this.config.broadcastInvalidBlockProposal, }, + bucketRef, ); this.lastProposedBlock = newProposal; return newProposal;