@aztec/validator-client 0.0.1-commit.ffe5b04ea → 0.0.1-commit.fff30aa
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 +9 -10
- package/dest/checkpoint_builder.d.ts +10 -7
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +64 -41
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +1 -2
- package/dest/duties/validation_service.js +1 -1
- package/dest/factory.d.ts +7 -4
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +5 -5
- package/dest/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/key_store/ha_key_store.js +1 -1
- package/dest/metrics.d.ts +2 -2
- package/dest/metrics.d.ts.map +1 -1
- package/dest/proposal_handler.d.ts +94 -0
- package/dest/proposal_handler.d.ts.map +1 -0
- package/dest/{block_proposal_handler.js → proposal_handler.js} +313 -12
- package/dest/validator.d.ts +10 -23
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +44 -248
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +79 -52
- package/src/config.ts +1 -2
- package/src/duties/validation_service.ts +1 -1
- package/src/factory.ts +9 -4
- package/src/index.ts +1 -1
- package/src/key_store/ha_key_store.ts +1 -1
- package/src/metrics.ts +1 -1
- package/src/{block_proposal_handler.ts → proposal_handler.ts} +362 -13
- package/src/validator.ts +57 -270
- package/dest/block_proposal_handler.d.ts +0 -63
- package/dest/block_proposal_handler.d.ts.map +0 -1
|
@@ -1,23 +1,34 @@
|
|
|
1
|
+
import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
2
|
+
import { type Blob, encodeCheckpointBlobDataFromBlocks, getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
1
3
|
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
2
4
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
5
|
+
import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
|
|
3
6
|
import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
4
7
|
import { pick } from '@aztec/foundation/collection';
|
|
5
8
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
6
9
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
10
|
+
import type { LogData } from '@aztec/foundation/log';
|
|
7
11
|
import { createLogger } from '@aztec/foundation/log';
|
|
8
12
|
import { retryUntil } from '@aztec/foundation/retry';
|
|
9
13
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
10
14
|
import type { P2P, PeerId } from '@aztec/p2p';
|
|
11
15
|
import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
|
|
12
16
|
import type { BlockData, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
17
|
+
import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
13
18
|
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
14
19
|
import { Gas } from '@aztec/stdlib/gas';
|
|
15
20
|
import type { ITxProvider, ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
16
|
-
import {
|
|
17
|
-
|
|
21
|
+
import {
|
|
22
|
+
type L1ToL2MessageSource,
|
|
23
|
+
accumulateCheckpointOutHashes,
|
|
24
|
+
computeInHashFromL1ToL2Messages,
|
|
25
|
+
} from '@aztec/stdlib/messaging';
|
|
26
|
+
import type { BlockProposal, CheckpointProposalCore } from '@aztec/stdlib/p2p';
|
|
27
|
+
import { MerkleTreeId } from '@aztec/stdlib/trees';
|
|
18
28
|
import type { CheckpointGlobalVariables, FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
19
29
|
import {
|
|
20
30
|
ReExFailedTxsError,
|
|
31
|
+
ReExInitialStateMismatchError,
|
|
21
32
|
ReExStateMismatchError,
|
|
22
33
|
ReExTimeoutError,
|
|
23
34
|
TransactionsNotAvailableError,
|
|
@@ -30,6 +41,7 @@ import type { ValidatorMetrics } from './metrics.js';
|
|
|
30
41
|
export type BlockProposalValidationFailureReason =
|
|
31
42
|
| 'invalid_proposal'
|
|
32
43
|
| 'parent_block_not_found'
|
|
44
|
+
| 'block_source_not_synced'
|
|
33
45
|
| 'parent_block_wrong_slot'
|
|
34
46
|
| 'in_hash_mismatch'
|
|
35
47
|
| 'global_variables_mismatch'
|
|
@@ -37,6 +49,7 @@ export type BlockProposalValidationFailureReason =
|
|
|
37
49
|
| 'txs_not_available'
|
|
38
50
|
| 'state_mismatch'
|
|
39
51
|
| 'failed_txs'
|
|
52
|
+
| 'initial_state_mismatch'
|
|
40
53
|
| 'timeout'
|
|
41
54
|
| 'unknown_error';
|
|
42
55
|
|
|
@@ -62,11 +75,14 @@ export type BlockProposalValidationFailureResult = {
|
|
|
62
75
|
|
|
63
76
|
export type BlockProposalValidationResult = BlockProposalValidationSuccessResult | BlockProposalValidationFailureResult;
|
|
64
77
|
|
|
78
|
+
export type CheckpointProposalValidationResult = { isValid: true } | { isValid: false; reason: string };
|
|
79
|
+
|
|
65
80
|
type CheckpointComputationResult =
|
|
66
81
|
| { checkpointNumber: CheckpointNumber; reason?: undefined }
|
|
67
82
|
| { checkpointNumber?: undefined; reason: 'invalid_proposal' | 'global_variables_mismatch' };
|
|
68
83
|
|
|
69
|
-
|
|
84
|
+
/** Handles block and checkpoint proposals for both validator and non-validator nodes. */
|
|
85
|
+
export class ProposalHandler {
|
|
70
86
|
public readonly tracer: Tracer;
|
|
71
87
|
|
|
72
88
|
constructor(
|
|
@@ -78,21 +94,26 @@ export class BlockProposalHandler {
|
|
|
78
94
|
private blockProposalValidator: BlockProposalValidator,
|
|
79
95
|
private epochCache: EpochCache,
|
|
80
96
|
private config: ValidatorClientFullConfig,
|
|
97
|
+
private blobClient: BlobClientInterface,
|
|
81
98
|
private metrics?: ValidatorMetrics,
|
|
82
99
|
private dateProvider: DateProvider = new DateProvider(),
|
|
83
100
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
84
|
-
private log = createLogger('validator:
|
|
101
|
+
private log = createLogger('validator:proposal-handler'),
|
|
85
102
|
) {
|
|
86
103
|
if (config.fishermanMode) {
|
|
87
104
|
this.log = this.log.createChild('[FISHERMAN]');
|
|
88
105
|
}
|
|
89
|
-
this.tracer = telemetry.getTracer('
|
|
106
|
+
this.tracer = telemetry.getTracer('ProposalHandler');
|
|
90
107
|
}
|
|
91
108
|
|
|
92
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Registers non-validator handlers for block and checkpoint proposals on the p2p client.
|
|
111
|
+
* Block proposals are always registered. Checkpoint proposals are registered if the blob client can upload.
|
|
112
|
+
*/
|
|
113
|
+
register(p2pClient: P2P, shouldReexecute: boolean): ProposalHandler {
|
|
93
114
|
// Non-validator handler that processes or re-executes for monitoring but does not attest.
|
|
94
115
|
// Returns boolean indicating whether the proposal was valid.
|
|
95
|
-
const
|
|
116
|
+
const blockHandler = async (proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> => {
|
|
96
117
|
try {
|
|
97
118
|
const { slotNumber, blockNumber } = proposal;
|
|
98
119
|
const result = await this.handleBlockProposal(proposal, proposalSender, shouldReexecute);
|
|
@@ -119,7 +140,35 @@ export class BlockProposalHandler {
|
|
|
119
140
|
}
|
|
120
141
|
};
|
|
121
142
|
|
|
122
|
-
p2pClient.registerBlockProposalHandler(
|
|
143
|
+
p2pClient.registerBlockProposalHandler(blockHandler);
|
|
144
|
+
|
|
145
|
+
// Register checkpoint proposal handler if blob uploads are enabled and we are reexecuting
|
|
146
|
+
if (this.blobClient.canUpload() && shouldReexecute) {
|
|
147
|
+
const checkpointHandler = async (checkpoint: CheckpointProposalCore, _sender: PeerId) => {
|
|
148
|
+
try {
|
|
149
|
+
const proposalInfo = {
|
|
150
|
+
proposalSlotNumber: checkpoint.slotNumber,
|
|
151
|
+
archive: checkpoint.archive.toString(),
|
|
152
|
+
proposer: checkpoint.getSender()?.toString(),
|
|
153
|
+
};
|
|
154
|
+
const result = await this.handleCheckpointProposal(checkpoint, proposalInfo);
|
|
155
|
+
if (result.isValid) {
|
|
156
|
+
this.log.info(`Non-validator checkpoint proposal at slot ${checkpoint.slotNumber} handled`, proposalInfo);
|
|
157
|
+
} else {
|
|
158
|
+
this.log.warn(
|
|
159
|
+
`Non-validator checkpoint proposal at slot ${checkpoint.slotNumber} failed: ${result.reason}`,
|
|
160
|
+
proposalInfo,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
this.log.error('Error processing checkpoint proposal in non-validator handler', error);
|
|
165
|
+
}
|
|
166
|
+
// Non-validators don't attest
|
|
167
|
+
return undefined;
|
|
168
|
+
};
|
|
169
|
+
p2pClient.registerCheckpointProposalHandler(checkpointHandler);
|
|
170
|
+
}
|
|
171
|
+
|
|
123
172
|
return this;
|
|
124
173
|
}
|
|
125
174
|
|
|
@@ -138,7 +187,13 @@ export class BlockProposalHandler {
|
|
|
138
187
|
return { isValid: false, reason: 'invalid_proposal' };
|
|
139
188
|
}
|
|
140
189
|
|
|
141
|
-
const proposalInfo = {
|
|
190
|
+
const proposalInfo = {
|
|
191
|
+
...proposal.toBlockInfo(),
|
|
192
|
+
proposer: proposer.toString(),
|
|
193
|
+
blockNumber: undefined as BlockNumber | undefined,
|
|
194
|
+
checkpointNumber: undefined as CheckpointNumber | undefined,
|
|
195
|
+
};
|
|
196
|
+
|
|
142
197
|
this.log.info(`Processing proposal for slot ${slotNumber}`, {
|
|
143
198
|
...proposalInfo,
|
|
144
199
|
txHashes: proposal.txHashes.map(t => t.toString()),
|
|
@@ -152,7 +207,20 @@ export class BlockProposalHandler {
|
|
|
152
207
|
return { isValid: false, reason: 'invalid_proposal' };
|
|
153
208
|
}
|
|
154
209
|
|
|
155
|
-
//
|
|
210
|
+
// Ensure the block source is synced before checking for existing blocks,
|
|
211
|
+
// since a pending checkpoint prune may remove blocks we'd otherwise find.
|
|
212
|
+
// This affects mostly the block_number_already_exists check, since a pending
|
|
213
|
+
// checkpoint prune could remove a block that would conflict with this proposal.
|
|
214
|
+
// TODO(@Maddiaa0): This may break staggered slots.
|
|
215
|
+
const blockSourceSync = await this.waitForBlockSourceSync(slotNumber);
|
|
216
|
+
if (!blockSourceSync) {
|
|
217
|
+
this.log.warn(`Block source is not synced, skipping processing`, proposalInfo);
|
|
218
|
+
return { isValid: false, reason: 'block_source_not_synced' };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check that the parent proposal is a block we know, otherwise reexecution would fail.
|
|
222
|
+
// If we don't find it immediately, we keep retrying for a while; it may be we still
|
|
223
|
+
// need to process other block proposals to get to it.
|
|
156
224
|
const parentBlock = await this.getParentBlock(proposal);
|
|
157
225
|
if (parentBlock === undefined) {
|
|
158
226
|
this.log.warn(`Parent block for proposal not found, skipping processing`, proposalInfo);
|
|
@@ -174,6 +242,7 @@ export class BlockProposalHandler {
|
|
|
174
242
|
parentBlock === 'genesis'
|
|
175
243
|
? BlockNumber(INITIAL_L2_BLOCK_NUM)
|
|
176
244
|
: BlockNumber(parentBlock.header.getBlockNumber() + 1);
|
|
245
|
+
proposalInfo.blockNumber = blockNumber;
|
|
177
246
|
|
|
178
247
|
// Check that this block number does not exist already
|
|
179
248
|
const existingBlock = await this.blockSource.getBlockHeader(blockNumber);
|
|
@@ -189,7 +258,7 @@ export class BlockProposalHandler {
|
|
|
189
258
|
deadline: this.getReexecutionDeadline(slotNumber, config),
|
|
190
259
|
});
|
|
191
260
|
|
|
192
|
-
// If reexecution is disabled, bail. We
|
|
261
|
+
// If reexecution is disabled, bail. We were just interested in triggering tx collection.
|
|
193
262
|
if (!shouldReexecute) {
|
|
194
263
|
this.log.info(
|
|
195
264
|
`Received valid block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`,
|
|
@@ -204,6 +273,7 @@ export class BlockProposalHandler {
|
|
|
204
273
|
return { isValid: false, blockNumber, reason: checkpointResult.reason };
|
|
205
274
|
}
|
|
206
275
|
const checkpointNumber = checkpointResult.checkpointNumber;
|
|
276
|
+
proposalInfo.checkpointNumber = checkpointNumber;
|
|
207
277
|
|
|
208
278
|
// Check that I have the same set of l1ToL2Messages as the proposal
|
|
209
279
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
@@ -425,8 +495,48 @@ export class BlockProposalHandler {
|
|
|
425
495
|
return new Date(nextSlotTimestampSeconds * 1000);
|
|
426
496
|
}
|
|
427
497
|
|
|
428
|
-
|
|
429
|
-
|
|
498
|
+
/** Waits for the block source to sync L1 data up to at least the slot before the given one. */
|
|
499
|
+
private async waitForBlockSourceSync(slot: SlotNumber): Promise<boolean> {
|
|
500
|
+
const deadline = this.getReexecutionDeadline(slot, this.checkpointsBuilder.getConfig());
|
|
501
|
+
const timeoutMs = deadline.getTime() - this.dateProvider.now();
|
|
502
|
+
if (slot === 0) {
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Make a quick check before triggering an archiver sync
|
|
507
|
+
const syncedSlot = await this.blockSource.getSyncedL2SlotNumber();
|
|
508
|
+
if (syncedSlot !== undefined && syncedSlot + 1 >= slot) {
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
// Trigger an immediate sync of the block source, and wait until it reports being synced to the required slot
|
|
514
|
+
return await retryUntil(
|
|
515
|
+
async () => {
|
|
516
|
+
await this.blockSource.syncImmediate();
|
|
517
|
+
const syncedSlot = await this.blockSource.getSyncedL2SlotNumber();
|
|
518
|
+
return syncedSlot !== undefined && syncedSlot + 1 >= slot;
|
|
519
|
+
},
|
|
520
|
+
'wait for block source sync',
|
|
521
|
+
timeoutMs / 1000,
|
|
522
|
+
0.5,
|
|
523
|
+
);
|
|
524
|
+
} catch (err) {
|
|
525
|
+
if (err instanceof TimeoutError) {
|
|
526
|
+
this.log.warn(`Timed out waiting for block source to sync to slot ${slot}`);
|
|
527
|
+
return false;
|
|
528
|
+
} else {
|
|
529
|
+
throw err;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private getReexecuteFailureReason(err: any): BlockProposalValidationFailureReason {
|
|
535
|
+
if (err instanceof TransactionsNotAvailableError) {
|
|
536
|
+
return 'txs_not_available';
|
|
537
|
+
} else if (err instanceof ReExInitialStateMismatchError) {
|
|
538
|
+
return 'initial_state_mismatch';
|
|
539
|
+
} else if (err instanceof ReExStateMismatchError) {
|
|
430
540
|
return 'state_mismatch';
|
|
431
541
|
} else if (err instanceof ReExFailedTxsError) {
|
|
432
542
|
return 'failed_txs';
|
|
@@ -467,6 +577,13 @@ export class BlockProposalHandler {
|
|
|
467
577
|
await this.worldState.syncImmediate(parentBlockNumber);
|
|
468
578
|
await using fork = await this.worldState.fork(parentBlockNumber);
|
|
469
579
|
|
|
580
|
+
// Verify the fork's archive root matches the proposal's expected last archive.
|
|
581
|
+
// If they don't match, our world state synced to a different chain and reexecution would fail.
|
|
582
|
+
const forkArchiveRoot = new Fr((await fork.getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
583
|
+
if (!forkArchiveRoot.equals(proposal.blockHeader.lastArchive.root)) {
|
|
584
|
+
throw new ReExInitialStateMismatchError(proposal.blockHeader.lastArchive.root, forkArchiveRoot);
|
|
585
|
+
}
|
|
586
|
+
|
|
470
587
|
// Build checkpoint constants from proposal (excludes blockNumber which is per-block)
|
|
471
588
|
const constants: CheckpointGlobalVariables = {
|
|
472
589
|
chainId: new Fr(config.l1ChainId),
|
|
@@ -497,6 +614,8 @@ export class BlockProposalHandler {
|
|
|
497
614
|
? new Gas(this.config.validateMaxDABlockGas ?? Infinity, this.config.validateMaxL2BlockGas ?? Infinity)
|
|
498
615
|
: undefined;
|
|
499
616
|
const result = await checkpointBuilder.buildBlock(txs, blockNumber, blockHeader.globalVariables.timestamp, {
|
|
617
|
+
isBuildingProposal: false,
|
|
618
|
+
minValidTxs: 0,
|
|
500
619
|
deadline,
|
|
501
620
|
expectedEndState: blockHeader.state,
|
|
502
621
|
maxTransactions: this.config.validateMaxTxsPerBlock,
|
|
@@ -551,4 +670,234 @@ export class BlockProposalHandler {
|
|
|
551
670
|
totalManaUsed,
|
|
552
671
|
};
|
|
553
672
|
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Validates a checkpoint proposal and uploads blobs if configured.
|
|
676
|
+
* Used by both non-validator nodes (via register) and the validator client (via delegation).
|
|
677
|
+
*/
|
|
678
|
+
async handleCheckpointProposal(
|
|
679
|
+
proposal: CheckpointProposalCore,
|
|
680
|
+
proposalInfo: LogData,
|
|
681
|
+
): Promise<CheckpointProposalValidationResult> {
|
|
682
|
+
const proposer = proposal.getSender();
|
|
683
|
+
if (!proposer) {
|
|
684
|
+
this.log.warn(`Received checkpoint proposal with invalid signature for slot ${proposal.slotNumber}`);
|
|
685
|
+
return { isValid: false, reason: 'invalid_signature' };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
|
|
689
|
+
this.log.warn(
|
|
690
|
+
`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${proposal.slotNumber}`,
|
|
691
|
+
);
|
|
692
|
+
return { isValid: false, reason: 'invalid_fee_asset_price_modifier' };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const result = await this.validateCheckpointProposal(proposal, proposalInfo);
|
|
696
|
+
|
|
697
|
+
// Upload blobs to filestore if validation passed (fire and forget)
|
|
698
|
+
if (result.isValid) {
|
|
699
|
+
this.tryUploadBlobsForCheckpoint(proposal, proposalInfo);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return result;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Validates a checkpoint proposal by building the full checkpoint and comparing it with the proposal.
|
|
707
|
+
* @returns Validation result with isValid flag and reason if invalid.
|
|
708
|
+
*/
|
|
709
|
+
async validateCheckpointProposal(
|
|
710
|
+
proposal: CheckpointProposalCore,
|
|
711
|
+
proposalInfo: LogData,
|
|
712
|
+
): Promise<CheckpointProposalValidationResult> {
|
|
713
|
+
const slot = proposal.slotNumber;
|
|
714
|
+
|
|
715
|
+
// Timeout block syncing at the start of the next slot
|
|
716
|
+
const config = this.checkpointsBuilder.getConfig();
|
|
717
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
718
|
+
const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
|
|
719
|
+
|
|
720
|
+
// Wait for last block to sync by archive
|
|
721
|
+
let lastBlockHeader;
|
|
722
|
+
try {
|
|
723
|
+
lastBlockHeader = await retryUntil(
|
|
724
|
+
async () => {
|
|
725
|
+
await this.blockSource.syncImmediate();
|
|
726
|
+
return this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
727
|
+
},
|
|
728
|
+
`waiting for block with archive ${proposal.archive.toString()} for slot ${slot}`,
|
|
729
|
+
timeoutSeconds,
|
|
730
|
+
0.5,
|
|
731
|
+
);
|
|
732
|
+
} catch (err) {
|
|
733
|
+
if (err instanceof TimeoutError) {
|
|
734
|
+
this.log.warn(`Timed out waiting for block with archive matching checkpoint proposal`, proposalInfo);
|
|
735
|
+
return { isValid: false, reason: 'last_block_not_found' };
|
|
736
|
+
}
|
|
737
|
+
this.log.error(`Error fetching last block for checkpoint proposal`, err, proposalInfo);
|
|
738
|
+
return { isValid: false, reason: 'block_fetch_error' };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (!lastBlockHeader) {
|
|
742
|
+
this.log.warn(`Last block not found for checkpoint proposal`, proposalInfo);
|
|
743
|
+
return { isValid: false, reason: 'last_block_not_found' };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Get all full blocks for the slot and checkpoint
|
|
747
|
+
const blocks = await this.blockSource.getBlocksForSlot(slot);
|
|
748
|
+
if (blocks.length === 0) {
|
|
749
|
+
this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
|
|
750
|
+
return { isValid: false, reason: 'no_blocks_for_slot' };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Ensure the last block for this slot matches the archive in the checkpoint proposal
|
|
754
|
+
if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
|
|
755
|
+
this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
|
|
756
|
+
return { isValid: false, reason: 'last_block_archive_mismatch' };
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
|
|
760
|
+
...proposalInfo,
|
|
761
|
+
blockNumbers: blocks.map(b => b.number),
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
// Get checkpoint constants from first block
|
|
765
|
+
const firstBlock = blocks[0];
|
|
766
|
+
const constants = this.extractCheckpointConstants(firstBlock);
|
|
767
|
+
const checkpointNumber = firstBlock.checkpointNumber;
|
|
768
|
+
|
|
769
|
+
// Get L1-to-L2 messages for this checkpoint
|
|
770
|
+
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
771
|
+
|
|
772
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
773
|
+
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
774
|
+
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch))
|
|
775
|
+
.filter(c => c.checkpointNumber < checkpointNumber)
|
|
776
|
+
.map(c => c.checkpointOutHash);
|
|
777
|
+
|
|
778
|
+
// Fork world state at the block before the first block
|
|
779
|
+
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
780
|
+
const fork = await this.worldState.fork(parentBlockNumber);
|
|
781
|
+
|
|
782
|
+
try {
|
|
783
|
+
// Create checkpoint builder with all existing blocks
|
|
784
|
+
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
785
|
+
checkpointNumber,
|
|
786
|
+
constants,
|
|
787
|
+
proposal.feeAssetPriceModifier,
|
|
788
|
+
l1ToL2Messages,
|
|
789
|
+
previousCheckpointOutHashes,
|
|
790
|
+
fork,
|
|
791
|
+
blocks,
|
|
792
|
+
this.log.getBindings(),
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
// Complete the checkpoint to get computed values
|
|
796
|
+
const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
|
|
797
|
+
|
|
798
|
+
// Compare checkpoint header with proposal
|
|
799
|
+
if (!computedCheckpoint.header.equals(proposal.checkpointHeader)) {
|
|
800
|
+
this.log.warn(`Checkpoint header mismatch`, {
|
|
801
|
+
...proposalInfo,
|
|
802
|
+
computed: computedCheckpoint.header.toInspect(),
|
|
803
|
+
proposal: proposal.checkpointHeader.toInspect(),
|
|
804
|
+
});
|
|
805
|
+
return { isValid: false, reason: 'checkpoint_header_mismatch' };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Compare archive root with proposal
|
|
809
|
+
if (!computedCheckpoint.archive.root.equals(proposal.archive)) {
|
|
810
|
+
this.log.warn(`Archive root mismatch`, {
|
|
811
|
+
...proposalInfo,
|
|
812
|
+
computed: computedCheckpoint.archive.root.toString(),
|
|
813
|
+
proposal: proposal.archive.toString(),
|
|
814
|
+
});
|
|
815
|
+
return { isValid: false, reason: 'archive_mismatch' };
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Check that the accumulated epoch out hash matches the value in the proposal.
|
|
819
|
+
// The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
|
|
820
|
+
const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
821
|
+
const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
|
|
822
|
+
const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
|
|
823
|
+
if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
|
|
824
|
+
this.log.warn(`Epoch out hash mismatch`, {
|
|
825
|
+
proposalEpochOutHash: proposalEpochOutHash.toString(),
|
|
826
|
+
computedEpochOutHash: computedEpochOutHash.toString(),
|
|
827
|
+
checkpointOutHash: checkpointOutHash.toString(),
|
|
828
|
+
previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
|
|
829
|
+
...proposalInfo,
|
|
830
|
+
});
|
|
831
|
+
return { isValid: false, reason: 'out_hash_mismatch' };
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Final round of validations on the checkpoint, just in case.
|
|
835
|
+
try {
|
|
836
|
+
validateCheckpoint(computedCheckpoint, {
|
|
837
|
+
rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
|
|
838
|
+
maxDABlockGas: this.config.validateMaxDABlockGas,
|
|
839
|
+
maxL2BlockGas: this.config.validateMaxL2BlockGas,
|
|
840
|
+
maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
|
|
841
|
+
maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint,
|
|
842
|
+
});
|
|
843
|
+
} catch (err) {
|
|
844
|
+
this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
|
|
845
|
+
return { isValid: false, reason: 'checkpoint_validation_failed' };
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
849
|
+
return { isValid: true };
|
|
850
|
+
} finally {
|
|
851
|
+
await fork.close();
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** Extracts checkpoint global variables from a block. */
|
|
856
|
+
private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
|
|
857
|
+
const gv = block.header.globalVariables;
|
|
858
|
+
return {
|
|
859
|
+
chainId: gv.chainId,
|
|
860
|
+
version: gv.version,
|
|
861
|
+
slotNumber: gv.slotNumber,
|
|
862
|
+
timestamp: gv.timestamp,
|
|
863
|
+
coinbase: gv.coinbase,
|
|
864
|
+
feeRecipient: gv.feeRecipient,
|
|
865
|
+
gasFees: gv.gasFees,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/** Triggers blob upload for a checkpoint if the blob client can upload (fire and forget). */
|
|
870
|
+
protected tryUploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): void {
|
|
871
|
+
if (this.blobClient.canUpload()) {
|
|
872
|
+
void this.uploadBlobsForCheckpoint(proposal, proposalInfo);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/** Uploads blobs for a checkpoint to the filestore. */
|
|
877
|
+
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
878
|
+
try {
|
|
879
|
+
const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
880
|
+
if (!lastBlockHeader) {
|
|
881
|
+
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
|
|
886
|
+
if (blocks.length === 0) {
|
|
887
|
+
this.log.warn(`No blocks found for blob upload`, proposalInfo);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const blockBlobData = blocks.map(b => b.toBlockBlobData());
|
|
892
|
+
const blobFields = encodeCheckpointBlobDataFromBlocks(blockBlobData);
|
|
893
|
+
const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
|
|
894
|
+
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
895
|
+
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
896
|
+
...proposalInfo,
|
|
897
|
+
numBlobs: blobs.length,
|
|
898
|
+
});
|
|
899
|
+
} catch (err) {
|
|
900
|
+
this.log.warn(`Failed to upload blobs for checkpoint: ${err}`, proposalInfo);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
554
903
|
}
|