@aztec/validator-client 6.0.0-nightly.20260812 → 6.0.0-nightly.20260821

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -64,7 +64,7 @@ function _ts_dispose_resources(env) {
64
64
  })(env);
65
65
  }
66
66
  import { encodeCheckpointBlobDataFromBlocks, getBlobsPerL1Block } from '@aztec/blob-lib';
67
- import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
67
+ import { INITIAL_L2_BLOCK_NUM, MAX_BLOCKS_PER_CHECKPOINT, MAX_L1_TO_L2_MSGS_PER_BLOCK, MAX_L1_TO_L2_MSGS_PER_CHECKPOINT } from '@aztec/constants';
68
68
  import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
69
69
  import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
70
70
  import { pick } from '@aztec/foundation/collection';
@@ -75,13 +75,14 @@ import { createLogger } from '@aztec/foundation/log';
75
75
  import { retryUntil } from '@aztec/foundation/retry';
76
76
  import { DateProvider, Timer } from '@aztec/foundation/timer';
77
77
  import { isErrorClass } from '@aztec/foundation/types';
78
- import { getPreviousCheckpointOutHashes, validateCheckpoint } from '@aztec/stdlib/checkpoint';
78
+ import { getPreviousCheckpointInboxRollingHash, getPreviousCheckpointOutHashes, validateCheckpoint } from '@aztec/stdlib/checkpoint';
79
79
  import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
80
80
  import { Gas } from '@aztec/stdlib/gas';
81
- import { accumulateCheckpointOutHashes, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
81
+ import { accumulateCheckpointOutHashes, getInboxCutoffTimestamp, isInboxConsumptionSufficient } from '@aztec/stdlib/messaging';
82
82
  import { MerkleTreeId } from '@aztec/stdlib/trees';
83
83
  import { InvalidBlockProposalTxsError, ReExFailedTxsError, ReExInitialStateMismatchError, ReExStateMismatchError, ReExTimeoutError, TransactionsNotAvailableError } from '@aztec/stdlib/validators';
84
84
  import { getTelemetryClient } from '@aztec/telemetry-client';
85
+ import { checkStreamingBlockProposalMetadata, getStreamingBlockBundle } from './streaming_inbox_checks.js';
85
86
  /**
86
87
  * Mapping from a checkpoint-proposal validation failure reason to the tracker outcome that
87
88
  * `handleCheckpointProposal` should record. `undefined` means do not record (signature
@@ -100,6 +101,7 @@ import { getTelemetryClient } from '@aztec/telemetry-client';
100
101
  checkpoint_header_mismatch: 'invalid',
101
102
  archive_mismatch: 'invalid',
102
103
  out_hash_mismatch: 'invalid',
104
+ inbox_consumption_insufficient: 'invalid',
103
105
  checkpoint_validation_failed: 'invalid'
104
106
  };
105
107
  const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
@@ -109,7 +111,6 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
109
111
  'global_variables_mismatch',
110
112
  'invalid_proposal',
111
113
  'parent_block_wrong_slot',
112
- 'in_hash_mismatch',
113
114
  'duplicate_txs',
114
115
  'invalid_embedded_txs'
115
116
  ];
@@ -126,6 +127,9 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
126
127
  ['checkpoint_validation_failed']: true,
127
128
  ['last_block_archive_mismatch']: true,
128
129
  // disabled
130
+ // Streaming Inbox last-block censorship: kept out of slashing while the streaming path is new; L1 `propose` is the
131
+ // authoritative reject (Rollup__UnconsumedInboxMessages).
132
+ ['inbox_consumption_insufficient']: false,
129
133
  ['invalid_signature']: false,
130
134
  ['last_block_not_found']: false,
131
135
  ['block_fetch_error']: false,
@@ -146,7 +150,6 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
146
150
  blockSource;
147
151
  l1ToL2MessageSource;
148
152
  txProvider;
149
- blockProposalValidator;
150
153
  epochCache;
151
154
  timetable;
152
155
  config;
@@ -165,13 +168,12 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
165
168
  checkpointProposalValidationFailureCallback;
166
169
  /** Slots at which a slashable invalid block or checkpoint proposal was observed. */ slotsWithInvalidProposals;
167
170
  /** Slots at which a proposal equivocation was observed; suppresses attested-to-invalid-proposal slashing. */ slotsWithProposalEquivocation;
168
- constructor(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, timetable, config, blobClient, reexecutionTracker, metrics, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator:proposal-handler')){
171
+ constructor(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, epochCache, timetable, config, blobClient, reexecutionTracker, metrics, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator:proposal-handler')){
169
172
  this.checkpointsBuilder = checkpointsBuilder;
170
173
  this.worldState = worldState;
171
174
  this.blockSource = blockSource;
172
175
  this.l1ToL2MessageSource = l1ToL2MessageSource;
173
176
  this.txProvider = txProvider;
174
- this.blockProposalValidator = blockProposalValidator;
175
177
  this.epochCache = epochCache;
176
178
  this.timetable = timetable;
177
179
  this.config = config;
@@ -337,7 +339,13 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
337
339
  p2pClient.registerAllNodesCheckpointProposalHandler(checkpointHandler);
338
340
  return this;
339
341
  }
340
- async handleBlockProposal(proposal, proposalSender, shouldReexecute) {
342
+ /**
343
+ * Processes a block proposal: collects its txs and, if requested, re-executes them to check the resulting
344
+ * block against the proposal. Expects the proposal to have already passed p2p ingress validation (signature
345
+ * context, signature, expected proposer, index within checkpoint, tx field checks, and the receive-window
346
+ * timeliness check) — none of those are re-applied here, and only deterministic properties of the payload
347
+ * are validated before processing.
348
+ */ async handleBlockProposal(proposal, proposalSender, shouldReexecute) {
341
349
  const slotNumber = proposal.slotNumber;
342
350
  const proposer = proposal.getSender();
343
351
  // Reject proposals with invalid signatures
@@ -358,16 +366,9 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
358
366
  ...proposalInfo,
359
367
  txHashes: proposal.txHashes.map((t)=>t.toString())
360
368
  });
361
- // Check that the proposal is from the current proposer, or the next proposer
362
- // This should have been handled by the p2p layer, but we double check here out of caution
363
- const validationResult = await this.blockProposalValidator.validate(proposal);
364
- if (validationResult.result !== 'accept') {
365
- this.log.warn(`Proposal is not valid, skipping processing`, proposalInfo);
366
- return {
367
- isValid: false,
368
- reason: 'invalid_proposal'
369
- };
370
- }
369
+ // The receive-window check from p2p ingress is deliberately not re-applied here: its outcome depends on
370
+ // the wall clock at evaluation time, so re-running it turned node-local processing latency into an
371
+ // invalid-proposal verdict against an honest proposer, which then fed the invalid-block slashing path.
371
372
  // A tx can only appear once in a block: the second copy would emit nullifiers already emitted by the
372
373
  // first. This is not a relaying-peer fault, so it passes gossip validation and is classified here as
373
374
  // proposer misbehavior. Tx collection also reconciles a deduplicated hash set against the full list,
@@ -440,9 +441,35 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
440
441
  reason: 'block_number_already_exists'
441
442
  };
442
443
  }
444
+ // Streaming Inbox: run the metadata checks before committing to any network work. They are point lookups
445
+ // against our own Inbox view, so a proposal carrying a bucket reference that does not resolve locally is
446
+ // rejected without a proposer being able to make us spend the validation window collecting its txs.
447
+ const streamingMetadata = await this.checkStreamingBlockMetadata(proposal, blockNumber, parentBlock);
448
+ if (!streamingMetadata.accepted) {
449
+ this.log.warn(`Streaming Inbox block acceptance check failed, skipping processing`, {
450
+ reason: streamingMetadata.reason,
451
+ bucketRef: proposal.bucketRef?.toInspect(),
452
+ ...proposalInfo
453
+ });
454
+ return {
455
+ isValid: false,
456
+ blockNumber,
457
+ reason: streamingMetadata.reason
458
+ };
459
+ }
443
460
  // Collect txs from the proposal. We start doing this as early as possible,
444
461
  // and we do it even if we don't plan to re-execute the txs, so that we have them if another node needs them.
445
- const collected = await this.collectProposalTxs(proposal, blockNumber, proposalSender, proposalInfo);
462
+ // The block's message bundle is an independent read, so derive it concurrently with the collection.
463
+ const txsPromise = this.collectProposalTxs(proposal, blockNumber, proposalSender, proposalInfo);
464
+ const bundlePromise = getStreamingBlockBundle(this.l1ToL2MessageSource, streamingMetadata);
465
+ // Promise.all settles on the first rejection, so without a handler of its own the loser's later rejection
466
+ // would surface as an unhandled rejection. Awaiting below still observes whichever rejected first.
467
+ txsPromise.catch(()=>{});
468
+ bundlePromise.catch(()=>{});
469
+ const [collected, l1ToL2Messages] = await Promise.all([
470
+ txsPromise,
471
+ bundlePromise
472
+ ]);
446
473
  if (collected === 'invalid_embedded_txs') {
447
474
  return {
448
475
  isValid: false,
@@ -472,22 +499,6 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
472
499
  }
473
500
  const checkpointNumber = checkpointResult.checkpointNumber;
474
501
  proposalInfo.checkpointNumber = checkpointNumber;
475
- // Check that I have the same set of l1ToL2Messages as the proposal
476
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
477
- const computedInHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
478
- const proposalInHash = proposal.inHash;
479
- if (!computedInHash.equals(proposalInHash)) {
480
- this.log.warn(`L1 to L2 messages in hash mismatch, skipping processing`, {
481
- proposalInHash: proposalInHash.toString(),
482
- computedInHash: computedInHash.toString(),
483
- ...proposalInfo
484
- });
485
- return {
486
- isValid: false,
487
- blockNumber,
488
- reason: 'in_hash_mismatch'
489
- };
490
- }
491
502
  // Check that all of the transactions in the proposal are available
492
503
  if (missingTxs.length > 0) {
493
504
  this.log.warn(`Missing ${missingTxs.length} txs to process proposal`, {
@@ -512,11 +523,16 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
512
523
  pipeliningEnabled: true,
513
524
  log: this.log
514
525
  });
526
+ const previousInboxRollingHash = await getPreviousCheckpointInboxRollingHash({
527
+ blockSource: this.blockSource,
528
+ checkpointNumber,
529
+ log: this.log
530
+ });
515
531
  // Try re-executing the transactions in the proposal if needed
516
532
  let reexecutionResult;
517
533
  try {
518
534
  this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo);
519
- reexecutionResult = await this.reexecuteTransactions(proposal, blockNumber, checkpointNumber, txs, l1ToL2Messages, previousCheckpointOutHashes);
535
+ reexecutionResult = await this.reexecuteTransactions(proposal, blockNumber, checkpointNumber, txs, l1ToL2Messages, previousCheckpointOutHashes, previousInboxRollingHash);
520
536
  } catch (error) {
521
537
  this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
522
538
  const reason = this.getReexecuteFailureReason(error);
@@ -829,7 +845,99 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
829
845
  return 'unknown_error';
830
846
  }
831
847
  }
832
- async reexecuteTransactions(proposal, blockNumber, checkpointNumber, txs, l1ToL2Messages, previousCheckpointOutHashes) {
848
+ /**
849
+ * Runs the streaming-Inbox per-block metadata checks for a block proposal, returning the bucket range its message
850
+ * bundle derives from or a rejection reason. The parent block's consumed total and the checkpoint's starting total
851
+ * are derived from L1-to-L2 tree leaf counts; a parent whose count does not sit on a bucket boundary is rejected
852
+ * inside {@link checkStreamingBlockProposalMetadata}.
853
+ */ async checkStreamingBlockMetadata(proposal, blockNumber, parentBlock) {
854
+ const parentTotalMsgCount = this.getConsumedMsgTotal(parentBlock);
855
+ const checkpointStartTotalMsgCount = await this.resolveCheckpointStartTotal(blockNumber, proposal.indexWithinCheckpoint, parentTotalMsgCount);
856
+ if (checkpointStartTotalMsgCount === undefined) {
857
+ // The block before the checkpoint's first block has not synced locally, so the per-checkpoint cap origin is
858
+ // unavailable: treat as an unknown local view. There is no bounded wait for the missing block yet.
859
+ return {
860
+ accepted: false,
861
+ reason: 'bucket_unknown'
862
+ };
863
+ }
864
+ const nowSeconds = BigInt(Math.floor(this.dateProvider.now() / 1000));
865
+ return checkStreamingBlockProposalMetadata({
866
+ messageSource: this.l1ToL2MessageSource,
867
+ bucketRef: proposal.bucketRef,
868
+ parentTotalMsgCount,
869
+ checkpointStartTotalMsgCount,
870
+ nowSeconds,
871
+ minBucketAgeSeconds: this.epochCache.getL1Constants().ethereumSlotDuration,
872
+ perBlockCap: MAX_L1_TO_L2_MSGS_PER_BLOCK,
873
+ perCheckpointCap: MAX_L1_TO_L2_MSGS_PER_CHECKPOINT
874
+ });
875
+ }
876
+ /** A block's L1-to-L2 message tree leaf count: the cumulative Inbox message count it consumed through. */ blockLeafCount(block) {
877
+ return BigInt(block.header.state.l1ToL2MessageTree.nextAvailableLeafIndex);
878
+ }
879
+ /** The cumulative Inbox message count consumed through a block: its L1-to-L2 tree leaf count (0 at genesis). */ getConsumedMsgTotal(block) {
880
+ return block === 'genesis' ? 0n : this.blockLeafCount(block);
881
+ }
882
+ /**
883
+ * The cumulative Inbox message count consumed as of the parent checkpoint (the per-checkpoint cap origin). For a
884
+ * checkpoint's first block this is the parent block's total; for a later block it is the leaf count of the block
885
+ * before the checkpoint's first block. Returns undefined when that block has not synced locally.
886
+ */ resolveCheckpointStartTotal(blockNumber, indexWithinCheckpoint, parentTotalMsgCount) {
887
+ return indexWithinCheckpoint === 0 ? Promise.resolve(parentTotalMsgCount) : this.getPreBlockConsumedTotal(blockNumber - indexWithinCheckpoint);
888
+ }
889
+ /**
890
+ * The cumulative Inbox message count consumed as of the block immediately before `firstBlockNumber` (its L1-to-L2
891
+ * tree leaf count): 0 when that block is genesis, undefined when it has not synced locally.
892
+ */ async getPreBlockConsumedTotal(firstBlockNumber) {
893
+ const preBlockNumber = firstBlockNumber - 1;
894
+ if (preBlockNumber < INITIAL_L2_BLOCK_NUM) {
895
+ return 0n;
896
+ }
897
+ const preBlock = await this.blockSource.getBlockData({
898
+ number: BlockNumber(preBlockNumber)
899
+ });
900
+ return preBlock === undefined ? undefined : this.blockLeafCount(preBlock);
901
+ }
902
+ /**
903
+ * Enforces the streaming-Inbox last-block minimum-consumption (censorship) rule for a checkpoint, mirroring
904
+ * `ProposeLib.validateInboxConsumption`: the first bucket the checkpoint left unconsumed must be absent, past the
905
+ * cutoff, or a cap-escape. Returns true (sufficient) when the checkpoint's consumption cannot be resolved against
906
+ * the local Inbox view, deferring to L1 `propose` as the authoritative reject.
907
+ */ async isLastBlockConsumptionSufficient(slot, blocks) {
908
+ const lastBlockTotal = this.blockLeafCount(blocks[blocks.length - 1]);
909
+ const checkpointStartTotal = await this.getPreBlockConsumedTotal(blocks[0].number);
910
+ const lastConsumedBucket = await this.l1ToL2MessageSource.getInboxBucketByTotalMsgCount(lastBlockTotal);
911
+ if (checkpointStartTotal === undefined || lastConsumedBucket === undefined) {
912
+ return true;
913
+ }
914
+ const nextBucket = await this.l1ToL2MessageSource.getInboxBucket(lastConsumedBucket.seq + 1n);
915
+ const cutoffTimestamp = getInboxCutoffTimestamp(slot, this.epochCache.getL1Constants());
916
+ return isInboxConsumptionSufficient({
917
+ nextBucket,
918
+ cutoffTimestamp,
919
+ checkpointStartTotalMsgCount: checkpointStartTotal,
920
+ perCheckpointCap: MAX_L1_TO_L2_MSGS_PER_CHECKPOINT
921
+ });
922
+ }
923
+ /**
924
+ * Derives the ordered list of L1-to-L2 messages a checkpoint consumed across its blocks, from the Inbox buckets
925
+ * between the parent checkpoint's consumed position and the checkpoint's last block. Empty when
926
+ * the checkpoint consumed nothing or its consumption cannot be resolved against the local Inbox view.
927
+ */ async deriveCheckpointConsumedMessages(blocks) {
928
+ const checkpointStartTotal = await this.getPreBlockConsumedTotal(blocks[0].number);
929
+ const lastBlockTotal = this.blockLeafCount(blocks[blocks.length - 1]);
930
+ if (checkpointStartTotal === undefined || lastBlockTotal <= checkpointStartTotal) {
931
+ return [];
932
+ }
933
+ const startBucket = await this.l1ToL2MessageSource.getInboxBucketByTotalMsgCount(checkpointStartTotal);
934
+ const endBucket = await this.l1ToL2MessageSource.getInboxBucketByTotalMsgCount(lastBlockTotal);
935
+ if (startBucket === undefined || endBucket === undefined) {
936
+ return [];
937
+ }
938
+ return this.l1ToL2MessageSource.getL1ToL2MessagesBetweenBuckets(startBucket.seq, endBucket.seq);
939
+ }
940
+ async reexecuteTransactions(proposal, blockNumber, checkpointNumber, txs, l1ToL2Messages, previousCheckpointOutHashes, previousInboxRollingHash) {
833
941
  const env = {
834
942
  stack: [],
835
943
  error: void 0,
@@ -869,8 +977,9 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
869
977
  feeRecipient: blockHeader.globalVariables.feeRecipient,
870
978
  gasFees: blockHeader.globalVariables.gasFees
871
979
  };
872
- // Create checkpoint builder with prior blocks
873
- const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, 0n, l1ToL2Messages, previousCheckpointOutHashes, fork, priorBlocks, this.log.getBindings());
980
+ // Create checkpoint builder with prior blocks. The messages the prior blocks consumed are not needed: this path
981
+ // only re-executes and compares the new block, never completing the checkpoint (whose rolling hash they seed).
982
+ const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, 0n, [], previousCheckpointOutHashes, previousInboxRollingHash, fork, priorBlocks, this.log.getBindings());
874
983
  // Build the new block
875
984
  const deadline = this.getReexecutionDeadline(slot);
876
985
  const maxBlockGas = this.config.validateMaxL2BlockGas !== undefined || this.config.validateMaxDABlockGas !== undefined ? new Gas(this.config.validateMaxDABlockGas ?? Infinity, this.config.validateMaxL2BlockGas ?? Infinity) : undefined;
@@ -880,7 +989,8 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
880
989
  deadline,
881
990
  expectedEndState: blockHeader.state,
882
991
  maxTransactions: this.config.validateMaxTxsPerBlock,
883
- maxBlockGas
992
+ maxBlockGas,
993
+ l1ToL2Messages
884
994
  });
885
995
  const { block, failedTxs } = result;
886
996
  const numFailedTxs = failedTxs.length;
@@ -934,6 +1044,8 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
934
1044
  * Validates a checkpoint proposal, caches the result, and uploads blobs if configured.
935
1045
  * Returns a cached result if the same proposal (archive + slot) was already validated.
936
1046
  * Used by both the all-nodes callback (via register) and the validator client (via delegation).
1047
+ * Expects the proposal to have already passed p2p ingress validation (expected proposer and receive-window
1048
+ * timeliness); only deterministic properties of the signed payload are checked here.
937
1049
  */ async handleCheckpointProposal(proposal, proposalInfo) {
938
1050
  const slot = proposal.slotNumber;
939
1051
  const payloadHash = proposal.getPayloadHash();
@@ -1069,9 +1181,12 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
1069
1181
  checkpointNumber: lastBlockData.checkpointNumber
1070
1182
  };
1071
1183
  }
1072
- // Note this condition should never trigger, since we dont process block proposals that exceed indexWithinCheckpoint
1073
- const maxBlocksPerCheckpoint = this.config.maxBlocksPerCheckpoint;
1074
- if (maxBlocksPerCheckpoint !== undefined && blocks.length > maxBlocksPerCheckpoint) {
1184
+ // The checkpoint root circuit caps how many blocks a checkpoint may contain, and L1 cannot check it when the
1185
+ // checkpoint is proposed (`ProposedHeader` carries no block count, only `blockHeadersHash`). An over-cap
1186
+ // checkpoint would therefore be accepted on L1 and then be unprovable, forcing an epoch reorg, so validators
1187
+ // reject it before attesting. An operator-configured limit may only tighten the protocol cap, never relax it.
1188
+ const maxBlocksPerCheckpoint = Math.min(this.config.maxBlocksPerCheckpoint ?? MAX_BLOCKS_PER_CHECKPOINT, MAX_BLOCKS_PER_CHECKPOINT);
1189
+ if (blocks.length > maxBlocksPerCheckpoint) {
1075
1190
  this.log.warn(`Checkpoint proposal exceeds maxBlocksPerCheckpoint`, {
1076
1191
  ...proposalInfo,
1077
1192
  blocksInProposal: blocks.length,
@@ -1091,8 +1206,23 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
1091
1206
  const firstBlock = blocks[0];
1092
1207
  const constants = this.extractCheckpointConstants(firstBlock);
1093
1208
  const checkpointNumber = firstBlock.checkpointNumber;
1094
- // Get L1-to-L2 messages for this checkpoint
1095
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
1209
+ // Streaming Inbox: on the last block of a checkpoint, enforce the minimum-consumption
1210
+ // (censorship) rule before attesting. Reject (no attestation) if a mandatory bucket was left unconsumed.
1211
+ if (!await this.isLastBlockConsumptionSufficient(slot, blocks)) {
1212
+ this.log.warn(`Streaming Inbox last-block censorship check failed, refusing to attest`, {
1213
+ ...proposalInfo,
1214
+ checkpointNumber
1215
+ });
1216
+ return {
1217
+ isValid: false,
1218
+ reason: 'inbox_consumption_insufficient',
1219
+ checkpointNumber
1220
+ };
1221
+ }
1222
+ // Derive the checkpoint's consumed L1-to-L2 message list from the Inbox buckets between the parent checkpoint's
1223
+ // consumed position and the last block's (compact indexing). The messages are already in the db from per-block
1224
+ // validation; this list only drives the checkpoint's rolling-hash recomputation in completeCheckpoint.
1225
+ const l1ToL2Messages = await this.deriveCheckpointConsumedMessages(blocks);
1096
1226
  // Collect the out hashes of all the checkpoints before this one in the same epoch.
1097
1227
  // See note on the analogous block-proposal site: the helper handles pipelining lag.
1098
1228
  const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
@@ -1104,6 +1234,11 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
1104
1234
  pipeliningEnabled: true,
1105
1235
  log: this.log
1106
1236
  });
1237
+ const previousInboxRollingHash = await getPreviousCheckpointInboxRollingHash({
1238
+ blockSource: this.blockSource,
1239
+ checkpointNumber,
1240
+ log: this.log
1241
+ });
1107
1242
  // Fork world state at the block before the first block. getFork syncs world state to the parent block
1108
1243
  // first (see its doc): the block source (archiver) can already hold the block while world state still
1109
1244
  // trails it by one, and forking a not-yet-applied block throws a raw tree error that would otherwise
@@ -1149,7 +1284,7 @@ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
1149
1284
  };
1150
1285
  }
1151
1286
  // Create checkpoint builder with all existing blocks
1152
- const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, proposal.feeAssetPriceModifier, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks, this.log.getBindings());
1287
+ const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, proposal.feeAssetPriceModifier, l1ToL2Messages, previousCheckpointOutHashes, previousInboxRollingHash, fork, blocks, this.log.getBindings());
1153
1288
  // Complete the checkpoint to get computed values
1154
1289
  const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
1155
1290
  // Compare checkpoint header with proposal
@@ -0,0 +1,103 @@
1
+ import type { Fr } from '@aztec/foundation/curves/bn254';
2
+ import type { InboxBucket, InboxBucketRef, L1ToL2MessageSource } from '@aztec/stdlib/messaging';
3
+ /**
4
+ * Reason a streaming-Inbox block proposal fails the per-block acceptance checks. Follows the
5
+ * handler's existing `{ isValid, reason }` string style.
6
+ */
7
+ export type StreamingBlockCheckReason = 'bucket_unknown' | 'bucket_hash_mismatch' | 'parent_bucket_unresolved' | 'bucket_moves_backwards' | 'bucket_too_new' | 'bundle_over_block_cap' | 'checkpoint_over_msg_cap';
8
+ /** The subset of the archiver's Inbox-bucket queries the per-block streaming checks need. */
9
+ export type StreamingInboxBucketSource = Pick<L1ToL2MessageSource, 'getInboxBucket' | 'getInboxBucketByTotalMsgCount' | 'getL1ToL2MessagesBetweenBuckets'>;
10
+ /** Inputs to the per-block streaming Inbox metadata checks. */
11
+ export type StreamingBlockMetadataCheckInput = {
12
+ /** Archiver Inbox-bucket queries (resolved against this node's own Inbox view). */
13
+ messageSource: Pick<StreamingInboxBucketSource, 'getInboxBucket' | 'getInboxBucketByTotalMsgCount'>;
14
+ /** The proposal's bucket reference: `bucketSeq` is the lookup hint, `inboxRollingHash` the expected commitment. */
15
+ bucketRef: InboxBucketRef | undefined;
16
+ /** Cumulative Inbox message count consumed through the parent block (its L1-to-L2 tree leaf count; 0 at genesis). */
17
+ parentTotalMsgCount: bigint;
18
+ /** Cumulative Inbox message count consumed as of the parent checkpoint; the per-checkpoint cap origin. */
19
+ checkpointStartTotalMsgCount: bigint;
20
+ /** Validation-time wall clock in seconds; the lag-eligibility anchor. */
21
+ nowSeconds: bigint;
22
+ /**
23
+ * Minimum bucket age in seconds for a bucket to be lag-eligible: one configured Ethereum slot, the same value the
24
+ * sequencer's selection uses.
25
+ *
26
+ * Age in seconds is a proxy for L1 reorg depth, and only an exact one when no L1 slot is missed: with missed slots a
27
+ * bucket can be a full Ethereum slot old and still sit in the latest L1 block. A block-depth rule would be
28
+ * stronger — require at least one later L1 block to have synced — and is implementable without new L1 state, since
29
+ * the archiver records each bucket's `l1BlockNumber`.
30
+ */
31
+ minBucketAgeSeconds: number;
32
+ /** Maximum number of messages this block may consume (`MAX_L1_TO_L2_MSGS_PER_BLOCK`). */
33
+ perBlockCap: number;
34
+ /** Maximum number of messages the checkpoint may consume in total (`MAX_L1_TO_L2_MSGS_PER_CHECKPOINT`). */
35
+ perCheckpointCap: number;
36
+ };
37
+ /** Inputs to the full per-block streaming Inbox acceptance checks, metadata plus bundle derivation. */
38
+ export type StreamingBlockCheckInput = Omit<StreamingBlockMetadataCheckInput, 'messageSource'> & {
39
+ messageSource: StreamingInboxBucketSource;
40
+ };
41
+ /** The buckets a passing block proposal consumes between; the input to bundle derivation. */
42
+ export type StreamingBlockBucketRange = {
43
+ /** The bucket the block consumes through, resolved from this node's own Inbox view. */
44
+ bucket: InboxBucket;
45
+ /** The bucket the parent block consumed through. Equal to `bucket` when the block consumes nothing. */
46
+ parentBucket: InboxBucket;
47
+ };
48
+ /** Result of the per-block streaming Inbox metadata checks. */
49
+ export type StreamingBlockMetadataCheckResult = ({
50
+ /** Every metadata check passed; the block's bundle can be derived from the returned bucket range. */
51
+ accepted: true;
52
+ } & StreamingBlockBucketRange) | {
53
+ /** A check failed; `reason` mirrors the L1 acceptance condition that would have rejected the proposal. */
54
+ accepted: false;
55
+ reason: StreamingBlockCheckReason;
56
+ };
57
+ /** Result of the per-block streaming Inbox acceptance checks. */
58
+ export type StreamingBlockCheckResult = {
59
+ /** All checks passed; `bundle` is the message-leaf bundle this block consumes, for re-execution. */
60
+ accepted: true;
61
+ bundle: Fr[];
62
+ } | {
63
+ /** A check failed; `reason` mirrors the L1 acceptance condition that would have rejected the proposal. */
64
+ accepted: false;
65
+ reason: StreamingBlockCheckReason;
66
+ };
67
+ /**
68
+ * Runs the metadata half of the per-block acceptance checks a validator applies to a streaming block proposal:
69
+ * every check that is a bounded number of point lookups against the local Inbox view, with no message data read.
70
+ * Mirrors the L1 acceptance conditions:
71
+ *
72
+ * 1. **Exists**: the referenced bucket resolves in this node's own Inbox view, and its consensus rolling hash matches
73
+ * the reference. An unknown bucket is an immediate reject here (there is no bounded wait yet); a hash
74
+ * mismatch means the wire reference disagrees with the local bucket. The reference is trusted only as a `bucketSeq`
75
+ * lookup hint — timestamp and message counts are read from the locally resolved bucket, never from the wire.
76
+ * 2. **Moves forward**: the bucket's cumulative total is at least the parent block's, so consumption never rewinds.
77
+ * Equal totals mean the block consumes nothing (empty bundle).
78
+ * 3. **Not too new**: the bucket is at least `minBucketAgeSeconds` old at validation time
79
+ * (`timestamp <= now - minBucketAgeSeconds`, inclusive — a bucket exactly that old is eligible, matching L1's
80
+ * strict `>` "too new" test).
81
+ * 4. **Caps**: the per-block message count and the running per-checkpoint total fit their respective caps.
82
+ * 5. **Parent boundary**: the parent block's cumulative total sits on a bucket boundary, so the consumed range is
83
+ * well defined.
84
+ *
85
+ * Because this phase is cheap and needs nothing off the network, a caller can run it before committing to any
86
+ * expensive work on a proposal — notably before collecting the proposal's transactions over P2P.
87
+ *
88
+ * The reject branch is a single function so a future bounded wait can wrap `bucket_unknown`.
89
+ */
90
+ export declare function checkStreamingBlockProposalMetadata(input: StreamingBlockMetadataCheckInput): Promise<StreamingBlockMetadataCheckResult>;
91
+ /**
92
+ * Derives the message-leaf bundle a streaming block proposal consumes (for re-execution): the leaves between the
93
+ * parent bucket and the proposed one, bounded by the per-block cap that
94
+ * {@link checkStreamingBlockProposalMetadata} already enforced.
95
+ */
96
+ export declare function getStreamingBlockBundle(messageSource: Pick<StreamingInboxBucketSource, 'getL1ToL2MessagesBetweenBuckets'>, range: StreamingBlockBucketRange): Promise<Fr[]>;
97
+ /**
98
+ * Runs the per-block acceptance checks a validator applies to a streaming block proposal, and derives the
99
+ * message-leaf bundle the block consumes. Composes {@link checkStreamingBlockProposalMetadata} with
100
+ * {@link getStreamingBlockBundle} for callers that have no use for running the two phases separately.
101
+ */
102
+ export declare function checkStreamingBlockProposal(input: StreamingBlockCheckInput): Promise<StreamingBlockCheckResult>;
103
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RyZWFtaW5nX2luYm94X2NoZWNrcy5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL3N0cmVhbWluZ19pbmJveF9jaGVja3MudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLEVBQUUsRUFBRSxFQUFFLE1BQU0sZ0NBQWdDLENBQUM7QUFDekQsT0FBTyxLQUFLLEVBQUUsV0FBVyxFQUFFLGNBQWMsRUFBRSxtQkFBbUIsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBRWhHOzs7R0FHRztBQUNILE1BQU0sTUFBTSx5QkFBeUIsR0FDakMsZ0JBQWdCLEdBQ2hCLHNCQUFzQixHQUN0QiwwQkFBMEIsR0FDMUIsd0JBQXdCLEdBQ3hCLGdCQUFnQixHQUNoQix1QkFBdUIsR0FDdkIseUJBQXlCLENBQUM7QUFFOUIsNkZBQTZGO0FBQzdGLE1BQU0sTUFBTSwwQkFBMEIsR0FBRyxJQUFJLENBQzNDLG1CQUFtQixFQUNuQixnQkFBZ0IsR0FBRywrQkFBK0IsR0FBRyxpQ0FBaUMsQ0FDdkYsQ0FBQztBQUVGLCtEQUErRDtBQUMvRCxNQUFNLE1BQU0sZ0NBQWdDLEdBQUc7SUFDN0MsbUZBQW1GO0lBQ25GLGFBQWEsRUFBRSxJQUFJLENBQUMsMEJBQTBCLEVBQUUsZ0JBQWdCLEdBQUcsK0JBQStCLENBQUMsQ0FBQztJQUNwRyxtSEFBbUg7SUFDbkgsU0FBUyxFQUFFLGNBQWMsR0FBRyxTQUFTLENBQUM7SUFDdEMscUhBQXFIO0lBQ3JILG1CQUFtQixFQUFFLE1BQU0sQ0FBQztJQUM1QiwwR0FBMEc7SUFDMUcsNEJBQTRCLEVBQUUsTUFBTSxDQUFDO0lBQ3JDLHlFQUF5RTtJQUN6RSxVQUFVLEVBQUUsTUFBTSxDQUFDO0lBQ25COzs7Ozs7OztPQVFHO0lBQ0gsbUJBQW1CLEVBQUUsTUFBTSxDQUFDO0lBQzVCLHlGQUF5RjtJQUN6RixXQUFXLEVBQUUsTUFBTSxDQUFDO0lBQ3BCLDJHQUEyRztJQUMzRyxnQkFBZ0IsRUFBRSxNQUFNLENBQUM7Q0FDMUIsQ0FBQztBQUVGLHVHQUF1RztBQUN2RyxNQUFNLE1BQU0sd0JBQXdCLEdBQUcsSUFBSSxDQUFDLGdDQUFnQyxFQUFFLGVBQWUsQ0FBQyxHQUFHO0lBQy9GLGFBQWEsRUFBRSwwQkFBMEIsQ0FBQztDQUMzQyxDQUFDO0FBRUYsNkZBQTZGO0FBQzdGLE1BQU0sTUFBTSx5QkFBeUIsR0FBRztJQUN0Qyx1RkFBdUY7SUFDdkYsTUFBTSxFQUFFLFdBQVcsQ0FBQztJQUNwQix1R0FBdUc7SUFDdkcsWUFBWSxFQUFFLFdBQVcsQ0FBQztDQUMzQixDQUFDO0FBRUYsK0RBQStEO0FBQy9ELE1BQU0sTUFBTSxpQ0FBaUMsR0FDekMsQ0FBQztJQUNDLHFHQUFxRztJQUNyRyxRQUFRLEVBQUUsSUFBSSxDQUFDO0NBQ2hCLEdBQUcseUJBQXlCLENBQUMsR0FDOUI7SUFDRSwwR0FBMEc7SUFDMUcsUUFBUSxFQUFFLEtBQUssQ0FBQztJQUNoQixNQUFNLEVBQUUseUJBQXlCLENBQUM7Q0FDbkMsQ0FBQztBQUVOLGlFQUFpRTtBQUNqRSxNQUFNLE1BQU0seUJBQXlCLEdBQ2pDO0lBQ0Usb0dBQW9HO0lBQ3BHLFFBQVEsRUFBRSxJQUFJLENBQUM7SUFDZixNQUFNLEVBQUUsRUFBRSxFQUFFLENBQUM7Q0FDZCxHQUNEO0lBQ0UsMEdBQTBHO0lBQzFHLFFBQVEsRUFBRSxLQUFLLENBQUM7SUFDaEIsTUFBTSxFQUFFLHlCQUF5QixDQUFDO0NBQ25DLENBQUM7QUFFTjs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXNCRztBQUNILHdCQUFzQixtQ0FBbUMsQ0FDdkQsS0FBSyxFQUFFLGdDQUFnQyxHQUN0QyxPQUFPLENBQUMsaUNBQWlDLENBQUMsQ0F3RDVDO0FBRUQ7Ozs7R0FJRztBQUNILHdCQUFnQix1QkFBdUIsQ0FDckMsYUFBYSxFQUFFLElBQUksQ0FBQywwQkFBMEIsRUFBRSxpQ0FBaUMsQ0FBQyxFQUNsRixLQUFLLEVBQUUseUJBQXlCLEdBQy9CLE9BQU8sQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUtmO0FBRUQ7Ozs7R0FJRztBQUNILHdCQUFzQiwyQkFBMkIsQ0FBQyxLQUFLLEVBQUUsd0JBQXdCLEdBQUcsT0FBTyxDQUFDLHlCQUF5QixDQUFDLENBTXJIIn0=
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming_inbox_checks.d.ts","sourceRoot":"","sources":["../src/streaming_inbox_checks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gCAAgC,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAEhG;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GACjC,gBAAgB,GAChB,sBAAsB,GACtB,0BAA0B,GAC1B,wBAAwB,GACxB,gBAAgB,GAChB,uBAAuB,GACvB,yBAAyB,CAAC;AAE9B,6FAA6F;AAC7F,MAAM,MAAM,0BAA0B,GAAG,IAAI,CAC3C,mBAAmB,EACnB,gBAAgB,GAAG,+BAA+B,GAAG,iCAAiC,CACvF,CAAC;AAEF,+DAA+D;AAC/D,MAAM,MAAM,gCAAgC,GAAG;IAC7C,mFAAmF;IACnF,aAAa,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,GAAG,+BAA+B,CAAC,CAAC;IACpG,mHAAmH;IACnH,SAAS,EAAE,cAAc,GAAG,SAAS,CAAC;IACtC,qHAAqH;IACrH,mBAAmB,EAAE,MAAM,CAAC;IAC5B,0GAA0G;IAC1G,4BAA4B,EAAE,MAAM,CAAC;IACrC,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,mBAAmB,EAAE,MAAM,CAAC;IAC5B,yFAAyF;IACzF,WAAW,EAAE,MAAM,CAAC;IACpB,2GAA2G;IAC3G,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,uGAAuG;AACvG,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,gCAAgC,EAAE,eAAe,CAAC,GAAG;IAC/F,aAAa,EAAE,0BAA0B,CAAC;CAC3C,CAAC;AAEF,6FAA6F;AAC7F,MAAM,MAAM,yBAAyB,GAAG;IACtC,uFAAuF;IACvF,MAAM,EAAE,WAAW,CAAC;IACpB,uGAAuG;IACvG,YAAY,EAAE,WAAW,CAAC;CAC3B,CAAC;AAEF,+DAA+D;AAC/D,MAAM,MAAM,iCAAiC,GACzC,CAAC;IACC,qGAAqG;IACrG,QAAQ,EAAE,IAAI,CAAC;CAChB,GAAG,yBAAyB,CAAC,GAC9B;IACE,0GAA0G;IAC1G,QAAQ,EAAE,KAAK,CAAC;IAChB,MAAM,EAAE,yBAAyB,CAAC;CACnC,CAAC;AAEN,iEAAiE;AACjE,MAAM,MAAM,yBAAyB,GACjC;IACE,oGAAoG;IACpG,QAAQ,EAAE,IAAI,CAAC;IACf,MAAM,EAAE,EAAE,EAAE,CAAC;CACd,GACD;IACE,0GAA0G;IAC1G,QAAQ,EAAE,KAAK,CAAC;IAChB,MAAM,EAAE,yBAAyB,CAAC;CACnC,CAAC;AAEN;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,mCAAmC,CACvD,KAAK,EAAE,gCAAgC,GACtC,OAAO,CAAC,iCAAiC,CAAC,CAwD5C;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,IAAI,CAAC,0BAA0B,EAAE,iCAAiC,CAAC,EAClF,KAAK,EAAE,yBAAyB,GAC/B,OAAO,CAAC,EAAE,EAAE,CAAC,CAKf;AAED;;;;GAIG;AACH,wBAAsB,2BAA2B,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAMrH"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Runs the metadata half of the per-block acceptance checks a validator applies to a streaming block proposal:
3
+ * every check that is a bounded number of point lookups against the local Inbox view, with no message data read.
4
+ * Mirrors the L1 acceptance conditions:
5
+ *
6
+ * 1. **Exists**: the referenced bucket resolves in this node's own Inbox view, and its consensus rolling hash matches
7
+ * the reference. An unknown bucket is an immediate reject here (there is no bounded wait yet); a hash
8
+ * mismatch means the wire reference disagrees with the local bucket. The reference is trusted only as a `bucketSeq`
9
+ * lookup hint — timestamp and message counts are read from the locally resolved bucket, never from the wire.
10
+ * 2. **Moves forward**: the bucket's cumulative total is at least the parent block's, so consumption never rewinds.
11
+ * Equal totals mean the block consumes nothing (empty bundle).
12
+ * 3. **Not too new**: the bucket is at least `minBucketAgeSeconds` old at validation time
13
+ * (`timestamp <= now - minBucketAgeSeconds`, inclusive — a bucket exactly that old is eligible, matching L1's
14
+ * strict `>` "too new" test).
15
+ * 4. **Caps**: the per-block message count and the running per-checkpoint total fit their respective caps.
16
+ * 5. **Parent boundary**: the parent block's cumulative total sits on a bucket boundary, so the consumed range is
17
+ * well defined.
18
+ *
19
+ * Because this phase is cheap and needs nothing off the network, a caller can run it before committing to any
20
+ * expensive work on a proposal — notably before collecting the proposal's transactions over P2P.
21
+ *
22
+ * The reject branch is a single function so a future bounded wait can wrap `bucket_unknown`.
23
+ */ export async function checkStreamingBlockProposalMetadata(input) {
24
+ const { messageSource, bucketRef, parentTotalMsgCount, checkpointStartTotalMsgCount, nowSeconds, minBucketAgeSeconds, perBlockCap, perCheckpointCap } = input;
25
+ // A streaming proposal must carry a bucket reference to derive its bundle from.
26
+ if (bucketRef === undefined) {
27
+ return {
28
+ accepted: false,
29
+ reason: 'bucket_unknown'
30
+ };
31
+ }
32
+ // Check 1: exists in our own Inbox view, and the resolved rolling hash matches the reference.
33
+ const bucket = await messageSource.getInboxBucket(bucketRef.bucketSeq);
34
+ if (bucket === undefined) {
35
+ return {
36
+ accepted: false,
37
+ reason: 'bucket_unknown'
38
+ };
39
+ }
40
+ if (!bucket.inboxRollingHash.equals(bucketRef.inboxRollingHash)) {
41
+ return {
42
+ accepted: false,
43
+ reason: 'bucket_hash_mismatch'
44
+ };
45
+ }
46
+ // Check 2: consumption moves forward relative to the parent block.
47
+ if (bucket.totalMsgCount < parentTotalMsgCount) {
48
+ return {
49
+ accepted: false,
50
+ reason: 'bucket_moves_backwards'
51
+ };
52
+ }
53
+ // Check 3: the bucket is at least `minBucketAgeSeconds` old at validation time.
54
+ if (bucket.timestamp > nowSeconds - BigInt(minBucketAgeSeconds)) {
55
+ return {
56
+ accepted: false,
57
+ reason: 'bucket_too_new'
58
+ };
59
+ }
60
+ // Check 4a: the per-block message count fits the per-block cap.
61
+ const blockCount = bucket.totalMsgCount - parentTotalMsgCount;
62
+ if (blockCount > BigInt(perBlockCap)) {
63
+ return {
64
+ accepted: false,
65
+ reason: 'bundle_over_block_cap'
66
+ };
67
+ }
68
+ // Check 4b: the running per-checkpoint total fits the per-checkpoint cap.
69
+ const checkpointCount = bucket.totalMsgCount - checkpointStartTotalMsgCount;
70
+ if (checkpointCount > BigInt(perCheckpointCap)) {
71
+ return {
72
+ accepted: false,
73
+ reason: 'checkpoint_over_msg_cap'
74
+ };
75
+ }
76
+ // Check 5: the parent bucket is the one whose cumulative total equals the parent block's leaf count (messages are
77
+ // indexed compactly, with no padding); a parent whose count does not sit on a bucket boundary is unresolvable.
78
+ const parentBucket = await messageSource.getInboxBucketByTotalMsgCount(parentTotalMsgCount);
79
+ if (parentBucket === undefined) {
80
+ return {
81
+ accepted: false,
82
+ reason: 'parent_bucket_unresolved'
83
+ };
84
+ }
85
+ return {
86
+ accepted: true,
87
+ bucket,
88
+ parentBucket
89
+ };
90
+ }
91
+ /**
92
+ * Derives the message-leaf bundle a streaming block proposal consumes (for re-execution): the leaves between the
93
+ * parent bucket and the proposed one, bounded by the per-block cap that
94
+ * {@link checkStreamingBlockProposalMetadata} already enforced.
95
+ */ export function getStreamingBlockBundle(messageSource, range) {
96
+ const { bucket, parentBucket } = range;
97
+ return parentBucket.seq === bucket.seq ? Promise.resolve([]) : messageSource.getL1ToL2MessagesBetweenBuckets(parentBucket.seq, bucket.seq);
98
+ }
99
+ /**
100
+ * Runs the per-block acceptance checks a validator applies to a streaming block proposal, and derives the
101
+ * message-leaf bundle the block consumes. Composes {@link checkStreamingBlockProposalMetadata} with
102
+ * {@link getStreamingBlockBundle} for callers that have no use for running the two phases separately.
103
+ */ export async function checkStreamingBlockProposal(input) {
104
+ const metadata = await checkStreamingBlockProposalMetadata(input);
105
+ if (!metadata.accepted) {
106
+ return metadata;
107
+ }
108
+ return {
109
+ accepted: true,
110
+ bundle: await getStreamingBlockBundle(input.messageSource, metadata)
111
+ };
112
+ }