@aztec/validator-client 6.0.0-nightly.20260810 → 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.
- package/README.md +5 -5
- package/dest/checkpoint_builder.d.ts +6 -3
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +14 -10
- package/dest/duties/validation_service.d.ts +3 -3
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +2 -3
- package/dest/factory.d.ts +2 -2
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +1 -13
- package/dest/metrics.d.ts +1 -1
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +0 -1
- package/dest/proposal_handler.d.ts +32 -10
- package/dest/proposal_handler.d.ts.map +1 -1
- package/dest/proposal_handler.js +181 -46
- package/dest/streaming_inbox_checks.d.ts +103 -0
- package/dest/streaming_inbox_checks.d.ts.map +1 -0
- package/dest/streaming_inbox_checks.js +112 -0
- package/dest/validator.d.ts +6 -6
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +5 -18
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +25 -11
- package/src/duties/validation_service.ts +3 -3
- package/src/factory.ts +1 -13
- package/src/metrics.ts +0 -1
- package/src/proposal_handler.ts +229 -40
- package/src/streaming_inbox_checks.ts +198 -0
- package/src/validator.ts +10 -22
package/dest/proposal_handler.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1073
|
-
|
|
1074
|
-
|
|
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
|
-
//
|
|
1095
|
-
|
|
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
|
+
}
|