@aztec/validator-client 0.0.1-commit.2448fdb → 0.0.1-commit.2606882
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 +10 -11
- package/dest/checkpoint_builder.js +1 -1
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +17 -11
- package/dest/duties/validation_service.d.ts +12 -13
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +32 -38
- package/dest/factory.d.ts +4 -1
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +8 -3
- package/dest/metrics.d.ts +5 -1
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +12 -0
- package/dest/proposal_handler.d.ts +54 -14
- package/dest/proposal_handler.d.ts.map +1 -1
- package/dest/proposal_handler.js +369 -149
- package/dest/validator.d.ts +33 -12
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +267 -91
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +1 -1
- package/src/config.ts +17 -10
- package/src/duties/validation_service.ts +51 -47
- package/src/factory.ts +11 -0
- package/src/metrics.ts +18 -0
- package/src/proposal_handler.ts +440 -182
- package/src/validator.ts +341 -116
package/src/proposal_handler.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import type { Archiver } from '@aztec/archiver';
|
|
1
2
|
import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
2
3
|
import { type Blob, encodeCheckpointBlobDataFromBlocks, getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
3
4
|
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
4
|
-
import type
|
|
5
|
+
import { type EpochCache, PROPOSER_PIPELINING_SLOT_OFFSET } from '@aztec/epoch-cache';
|
|
5
6
|
import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
BlockNumber,
|
|
9
|
+
CheckpointNumber,
|
|
10
|
+
type CheckpointProposalHash,
|
|
11
|
+
SlotNumber,
|
|
12
|
+
} from '@aztec/foundation/branded-types';
|
|
7
13
|
import { pick } from '@aztec/foundation/collection';
|
|
8
14
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
9
15
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
@@ -14,8 +20,9 @@ import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
|
14
20
|
import type { P2P, PeerId } from '@aztec/p2p';
|
|
15
21
|
import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
|
|
16
22
|
import type { BlockData, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
23
|
+
import type { CheckpointReexecutionTracker, ReexecutionOutcome } from '@aztec/stdlib/checkpoint';
|
|
24
|
+
import { getPreviousCheckpointOutHashes, validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
25
|
+
import { getEpochAtSlot, getLastL1SlotTimestampForL2Slot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
19
26
|
import { Gas } from '@aztec/stdlib/gas';
|
|
20
27
|
import type { ITxProvider, ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
21
28
|
import {
|
|
@@ -23,7 +30,7 @@ import {
|
|
|
23
30
|
accumulateCheckpointOutHashes,
|
|
24
31
|
computeInHashFromL1ToL2Messages,
|
|
25
32
|
} from '@aztec/stdlib/messaging';
|
|
26
|
-
import type { BlockProposal, CheckpointProposalCore } from '@aztec/stdlib/p2p';
|
|
33
|
+
import type { BlockProposal, CheckpointAttestation, CheckpointProposalCore } from '@aztec/stdlib/p2p';
|
|
27
34
|
import { MerkleTreeId } from '@aztec/stdlib/trees';
|
|
28
35
|
import type { CheckpointGlobalVariables, FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
29
36
|
import {
|
|
@@ -39,9 +46,9 @@ import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
|
39
46
|
import type { ValidatorMetrics } from './metrics.js';
|
|
40
47
|
|
|
41
48
|
export type BlockProposalValidationFailureReason =
|
|
49
|
+
| 'invalid_signature'
|
|
42
50
|
| 'invalid_proposal'
|
|
43
51
|
| 'parent_block_not_found'
|
|
44
|
-
| 'block_source_not_synced'
|
|
45
52
|
| 'parent_block_wrong_slot'
|
|
46
53
|
| 'in_hash_mismatch'
|
|
47
54
|
| 'global_variables_mismatch'
|
|
@@ -51,6 +58,8 @@ export type BlockProposalValidationFailureReason =
|
|
|
51
58
|
| 'failed_txs'
|
|
52
59
|
| 'initial_state_mismatch'
|
|
53
60
|
| 'timeout'
|
|
61
|
+
| 'block_proposal_beyond_checkpoint'
|
|
62
|
+
| 'checkpoint_proposal_equivocation'
|
|
54
63
|
| 'unknown_error';
|
|
55
64
|
|
|
56
65
|
type ReexecuteTransactionsResult = {
|
|
@@ -75,16 +84,96 @@ export type BlockProposalValidationFailureResult = {
|
|
|
75
84
|
|
|
76
85
|
export type BlockProposalValidationResult = BlockProposalValidationSuccessResult | BlockProposalValidationFailureResult;
|
|
77
86
|
|
|
78
|
-
export type
|
|
87
|
+
export type CheckpointProposalValidationFailureReason =
|
|
88
|
+
| 'invalid_signature'
|
|
89
|
+
| 'invalid_fee_asset_price_modifier'
|
|
90
|
+
| 'last_block_not_found'
|
|
91
|
+
| 'block_fetch_error'
|
|
92
|
+
| 'checkpoint_already_published'
|
|
93
|
+
| 'no_blocks_for_slot'
|
|
94
|
+
| 'last_block_archive_mismatch'
|
|
95
|
+
| 'too_many_blocks_in_checkpoint'
|
|
96
|
+
| 'checkpoint_header_mismatch'
|
|
97
|
+
| 'archive_mismatch'
|
|
98
|
+
| 'out_hash_mismatch'
|
|
99
|
+
| 'checkpoint_validation_failed';
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Mapping from a checkpoint-proposal validation failure reason to the tracker outcome that
|
|
103
|
+
* `handleCheckpointProposal` should record. `undefined` means do not record (signature
|
|
104
|
+
* couldn't be verified, or the checkpoint is already on L1 so the question is moot).
|
|
105
|
+
*/
|
|
106
|
+
/* eslint-disable camelcase */
|
|
107
|
+
const CHECKPOINT_VALIDATION_REASON_TO_OUTCOME: Record<
|
|
108
|
+
CheckpointProposalValidationFailureReason,
|
|
109
|
+
ReexecutionOutcome | undefined
|
|
110
|
+
> = {
|
|
111
|
+
invalid_signature: undefined,
|
|
112
|
+
invalid_fee_asset_price_modifier: 'invalid',
|
|
113
|
+
checkpoint_already_published: undefined,
|
|
114
|
+
last_block_not_found: 'unvalidated',
|
|
115
|
+
block_fetch_error: 'unvalidated',
|
|
116
|
+
no_blocks_for_slot: 'unvalidated',
|
|
117
|
+
last_block_archive_mismatch: 'invalid',
|
|
118
|
+
too_many_blocks_in_checkpoint: 'invalid',
|
|
119
|
+
checkpoint_header_mismatch: 'invalid',
|
|
120
|
+
archive_mismatch: 'invalid',
|
|
121
|
+
out_hash_mismatch: 'invalid',
|
|
122
|
+
checkpoint_validation_failed: 'invalid',
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export type CheckpointProposalValidationSuccessResult = {
|
|
126
|
+
isValid: true;
|
|
127
|
+
checkpointNumber: CheckpointNumber;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export type CheckpointProposalValidationFailureResult = {
|
|
131
|
+
isValid: false;
|
|
132
|
+
reason: CheckpointProposalValidationFailureReason;
|
|
133
|
+
checkpointNumber?: CheckpointNumber;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export type CheckpointProposalValidationResult =
|
|
137
|
+
| CheckpointProposalValidationSuccessResult
|
|
138
|
+
| CheckpointProposalValidationFailureResult;
|
|
139
|
+
|
|
140
|
+
export type CheckpointProposalValidationFailureCallback = (
|
|
141
|
+
proposal: CheckpointProposalCore,
|
|
142
|
+
result: CheckpointProposalValidationFailureResult,
|
|
143
|
+
proposalInfo: LogData,
|
|
144
|
+
) => void | Promise<void>;
|
|
79
145
|
|
|
80
146
|
type CheckpointComputationResult =
|
|
81
147
|
| { checkpointNumber: CheckpointNumber; reason?: undefined }
|
|
82
148
|
| { checkpointNumber?: undefined; reason: 'invalid_proposal' | 'global_variables_mismatch' };
|
|
83
149
|
|
|
150
|
+
type BlockProposalSlotValidationResult =
|
|
151
|
+
| { isValid: true }
|
|
152
|
+
| { isValid: false; reason: 'block_proposal_beyond_checkpoint' | 'checkpoint_proposal_equivocation' };
|
|
153
|
+
|
|
84
154
|
/** Handles block and checkpoint proposals for both validator and non-validator nodes. */
|
|
85
155
|
export class ProposalHandler {
|
|
86
156
|
public readonly tracer: Tracer;
|
|
87
157
|
|
|
158
|
+
/** Cached last checkpoint validation result to avoid double-validation on validator nodes.
|
|
159
|
+
* Keyed by signed-payload hash so two proposals at the same (slot, archive) but with a
|
|
160
|
+
* different `feeAssetPriceModifier` (or any other signed field) are validated independently. */
|
|
161
|
+
private lastCheckpointValidationResult?: {
|
|
162
|
+
payloadHash: CheckpointProposalHash;
|
|
163
|
+
result: CheckpointProposalValidationResult;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** Archiver reference for setting proposed checkpoints (pipelining). Set via register(). */
|
|
167
|
+
private archiver?: Pick<Archiver, 'addProposedCheckpoint'>;
|
|
168
|
+
|
|
169
|
+
/** Returns current validator addresses for own-proposal detection. Set via register(). */
|
|
170
|
+
private getOwnValidatorAddresses?: () => string[];
|
|
171
|
+
|
|
172
|
+
/** P2P proposal pool access for deciding when retained proposals should block archiver processing. */
|
|
173
|
+
private p2pClient?: Pick<P2P, 'getProposalsForSlot'>;
|
|
174
|
+
|
|
175
|
+
private checkpointProposalValidationFailureCallback?: CheckpointProposalValidationFailureCallback;
|
|
176
|
+
|
|
88
177
|
constructor(
|
|
89
178
|
private checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
90
179
|
private worldState: WorldStateSynchronizer,
|
|
@@ -95,6 +184,7 @@ export class ProposalHandler {
|
|
|
95
184
|
private epochCache: EpochCache,
|
|
96
185
|
private config: ValidatorClientFullConfig,
|
|
97
186
|
private blobClient: BlobClientInterface,
|
|
187
|
+
private reexecutionTracker: CheckpointReexecutionTracker,
|
|
98
188
|
private metrics?: ValidatorMetrics,
|
|
99
189
|
private dateProvider: DateProvider = new DateProvider(),
|
|
100
190
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
@@ -106,11 +196,47 @@ export class ProposalHandler {
|
|
|
106
196
|
this.tracer = telemetry.getTracer('ProposalHandler');
|
|
107
197
|
}
|
|
108
198
|
|
|
199
|
+
public updateConfig(config: Partial<ValidatorClientFullConfig>): void {
|
|
200
|
+
this.config = { ...this.config, ...config };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public setCheckpointProposalValidationFailureCallback(callback?: CheckpointProposalValidationFailureCallback): void {
|
|
204
|
+
this.checkpointProposalValidationFailureCallback = callback;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Records the proposer's own checkpoint proposal as a `valid` outcome in the re-execution
|
|
209
|
+
* tracker. Without this, the node's own checkpoint proposals never flow through
|
|
210
|
+
* `handleCheckpointProposal` (proposers don't validate their own proposals), so its sentinel
|
|
211
|
+
* sees no outcome for slots where it was the proposer and reports itself as inactive.
|
|
212
|
+
*
|
|
213
|
+
* `archive` should be the locally-computed archive (NOT the broadcast archive, which may have
|
|
214
|
+
* been deliberately corrupted in tests via `broadcastInvalidBlockProposal` /
|
|
215
|
+
* `broadcastInvalidCheckpointProposalOnly`). Recording the local archive correctly models the
|
|
216
|
+
* proposer's own view of its own work.
|
|
217
|
+
*/
|
|
218
|
+
public recordOwnCheckpointProposalAsValid(slot: SlotNumber, archive: Fr, checkpointNumber: CheckpointNumber): void {
|
|
219
|
+
this.reexecutionTracker.recordOutcome(slot, archive, 'valid', checkpointNumber);
|
|
220
|
+
}
|
|
221
|
+
|
|
109
222
|
/**
|
|
110
|
-
* Registers
|
|
111
|
-
*
|
|
223
|
+
* Registers handlers for block and checkpoint proposals on the p2p client.
|
|
224
|
+
* Records the p2p client so validation can inspect retained proposals.
|
|
225
|
+
* Block proposals are registered for non-validator nodes (validators register their own enhanced handler).
|
|
226
|
+
* The all-nodes checkpoint proposal handler is always registered for validation, caching, and pipelining.
|
|
227
|
+
* @param archiver - Archiver reference for setting proposed checkpoints (pipelining)
|
|
228
|
+
* @param getOwnValidatorAddresses - Returns current validator addresses for own-proposal detection
|
|
112
229
|
*/
|
|
113
|
-
register(
|
|
230
|
+
register(
|
|
231
|
+
p2pClient: P2P,
|
|
232
|
+
shouldReexecute: boolean,
|
|
233
|
+
archiver?: Pick<Archiver, 'addProposedCheckpoint'>,
|
|
234
|
+
getOwnValidatorAddresses?: () => string[],
|
|
235
|
+
): ProposalHandler {
|
|
236
|
+
this.p2pClient = p2pClient;
|
|
237
|
+
this.archiver = archiver;
|
|
238
|
+
this.getOwnValidatorAddresses = getOwnValidatorAddresses;
|
|
239
|
+
|
|
114
240
|
// Non-validator handler that processes or re-executes for monitoring but does not attest.
|
|
115
241
|
// Returns boolean indicating whether the proposal was valid.
|
|
116
242
|
const blockHandler = async (proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> => {
|
|
@@ -142,32 +268,64 @@ export class ProposalHandler {
|
|
|
142
268
|
|
|
143
269
|
p2pClient.registerBlockProposalHandler(blockHandler);
|
|
144
270
|
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
271
|
+
// All-nodes checkpoint proposal handler: validates, caches, and sets proposed checkpoint for pipelining.
|
|
272
|
+
// Runs for all nodes (validators and non-validators). Validators get the cached result in the
|
|
273
|
+
// validator-specific callback (attestToCheckpointProposal) which runs after this one.
|
|
274
|
+
const checkpointHandler = async (
|
|
275
|
+
proposal: CheckpointProposalCore,
|
|
276
|
+
_sender: PeerId,
|
|
277
|
+
): Promise<CheckpointAttestation[] | undefined> => {
|
|
278
|
+
try {
|
|
279
|
+
const pipeliningTimer = new Timer();
|
|
280
|
+
const proposalInfo: LogData = {
|
|
281
|
+
slot: proposal.slotNumber,
|
|
282
|
+
archive: proposal.archive.toString(),
|
|
283
|
+
proposer: proposal.getSender()?.toString(),
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
if (this.config.skipCheckpointProposalValidation) {
|
|
287
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposal.slotNumber}`, proposalInfo);
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (await this.epochCache.isEscapeHatchOpenAtSlot(proposal.slotNumber)) {
|
|
292
|
+
this.log.warn(
|
|
293
|
+
`Escape hatch open for slot ${proposal.slotNumber}, skipping checkpoint proposal validation`,
|
|
294
|
+
proposalInfo,
|
|
295
|
+
);
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// For own proposals, skip validation and return: the proposer already built and validated the
|
|
300
|
+
// checkpoint, and the sequencer's checkpoint proposal job pushed the proposed checkpoint to the
|
|
301
|
+
// archiver from local data before broadcasting. Gossipsub doesn't echo our own messages back, so
|
|
302
|
+
// this branch is normally unreachable — it remains as defense if an own proposal arrives by some
|
|
303
|
+
// other path.
|
|
304
|
+
const proposer = proposal.getSender();
|
|
305
|
+
const ownAddresses = this.getOwnValidatorAddresses?.();
|
|
306
|
+
const isOwnProposal = proposer && ownAddresses?.some(addr => addr === proposer.toString());
|
|
307
|
+
|
|
308
|
+
if (isOwnProposal) {
|
|
309
|
+
this.log.debug(`Skipping validation for own checkpoint proposal at slot ${proposal.slotNumber}`);
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const result = await this.handleCheckpointProposal(proposal, proposalInfo);
|
|
314
|
+
if (!result.isValid) {
|
|
315
|
+
await this.checkpointProposalValidationFailureCallback?.(proposal, result, proposalInfo);
|
|
316
|
+
} else if (this.archiver) {
|
|
317
|
+
const set = await this.setProposedCheckpointFromValidation(proposal);
|
|
318
|
+
if (set) {
|
|
319
|
+
this.metrics?.recordCheckpointProposalToPipelinedStateDuration(pipeliningTimer.ms());
|
|
162
320
|
}
|
|
163
|
-
} catch (error) {
|
|
164
|
-
this.log.error('Error processing checkpoint proposal in non-validator handler', error);
|
|
165
321
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
}
|
|
322
|
+
} catch (err) {
|
|
323
|
+
this.log.warn(`Error handling checkpoint proposal for slot ${proposal.slotNumber}`, { err });
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
p2pClient.registerAllNodesCheckpointProposalHandler(checkpointHandler);
|
|
171
329
|
|
|
172
330
|
return this;
|
|
173
331
|
}
|
|
@@ -184,7 +342,7 @@ export class ProposalHandler {
|
|
|
184
342
|
// Reject proposals with invalid signatures
|
|
185
343
|
if (!proposer) {
|
|
186
344
|
this.log.warn(`Received proposal with invalid signature for slot ${slotNumber}`);
|
|
187
|
-
return { isValid: false, reason: '
|
|
345
|
+
return { isValid: false, reason: 'invalid_signature' };
|
|
188
346
|
}
|
|
189
347
|
|
|
190
348
|
const proposalInfo = {
|
|
@@ -207,17 +365,20 @@ export class ProposalHandler {
|
|
|
207
365
|
return { isValid: false, reason: 'invalid_proposal' };
|
|
208
366
|
}
|
|
209
367
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return { isValid: false, reason: 'block_source_not_synced' };
|
|
368
|
+
const retainedSlotValidation = await this.validateNewBlockInSlot(proposal);
|
|
369
|
+
if (!retainedSlotValidation.isValid) {
|
|
370
|
+
this.log.info(`Block proposal conflicts with retained proposals, skipping archiver processing`, {
|
|
371
|
+
...proposalInfo,
|
|
372
|
+
indexWithinCheckpoint: proposal.indexWithinCheckpoint,
|
|
373
|
+
reason: retainedSlotValidation.reason,
|
|
374
|
+
});
|
|
375
|
+
return { isValid: false, blockNumber: proposal.blockNumber, reason: retainedSlotValidation.reason };
|
|
219
376
|
}
|
|
220
377
|
|
|
378
|
+
// The proposer builds ahead of L1 submission under pipelining, so the block source won't have
|
|
379
|
+
// synced to the proposed slot yet. We deliberately do not wait for it to sync here, to avoid
|
|
380
|
+
// eating into the attestation window.
|
|
381
|
+
|
|
221
382
|
// Check that the parent proposal is a block we know, otherwise reexecution would fail.
|
|
222
383
|
// If we don't find it immediately, we keep retrying for a while; it may be we still
|
|
223
384
|
// need to process other block proposals to get to it.
|
|
@@ -245,7 +406,7 @@ export class ProposalHandler {
|
|
|
245
406
|
proposalInfo.blockNumber = blockNumber;
|
|
246
407
|
|
|
247
408
|
// Check that this block number does not exist already
|
|
248
|
-
const existingBlock = await this.blockSource.
|
|
409
|
+
const existingBlock = await this.blockSource.getBlockData({ number: blockNumber });
|
|
249
410
|
if (existingBlock) {
|
|
250
411
|
this.log.warn(`Block number ${blockNumber} already exists, skipping processing`, proposalInfo);
|
|
251
412
|
return { isValid: false, blockNumber, reason: 'block_number_already_exists' };
|
|
@@ -258,6 +419,9 @@ export class ProposalHandler {
|
|
|
258
419
|
deadline: this.getReexecutionDeadline(slotNumber, config),
|
|
259
420
|
});
|
|
260
421
|
|
|
422
|
+
// Record the tx-collection outcome on the re-execution tracker
|
|
423
|
+
this.reexecutionTracker.recordTxsCollected(slotNumber, proposal.indexWithinCheckpoint, missingTxs.length === 0);
|
|
424
|
+
|
|
261
425
|
// If reexecution is disabled, bail. We were just interested in triggering tx collection.
|
|
262
426
|
if (!shouldReexecute) {
|
|
263
427
|
this.log.info(
|
|
@@ -294,11 +458,18 @@ export class ProposalHandler {
|
|
|
294
458
|
return { isValid: false, blockNumber, reason: 'txs_not_available' };
|
|
295
459
|
}
|
|
296
460
|
|
|
297
|
-
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
461
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch.
|
|
462
|
+
// Mirror the proposer-side fallback: under pipelining the immediately-preceding cp may not
|
|
463
|
+
// yet be on L1, in which case the helper grafts the locally-known proposed cp's outHash.
|
|
298
464
|
const epoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
|
|
299
|
-
const previousCheckpointOutHashes =
|
|
300
|
-
|
|
301
|
-
|
|
465
|
+
const previousCheckpointOutHashes = await getPreviousCheckpointOutHashes({
|
|
466
|
+
blockSource: this.blockSource,
|
|
467
|
+
epoch,
|
|
468
|
+
checkpointNumber,
|
|
469
|
+
l1Constants: this.epochCache.getL1Constants(),
|
|
470
|
+
pipeliningEnabled: true,
|
|
471
|
+
log: this.log,
|
|
472
|
+
});
|
|
302
473
|
|
|
303
474
|
// Try re-executing the transactions in the proposal if needed
|
|
304
475
|
let reexecutionResult;
|
|
@@ -320,7 +491,7 @@ export class ProposalHandler {
|
|
|
320
491
|
|
|
321
492
|
// If we succeeded, push this block into the archiver (unless disabled)
|
|
322
493
|
if (reexecutionResult?.block && this.config.skipPushProposedBlocksToArchiver === false) {
|
|
323
|
-
await this.blockSource.addBlock(reexecutionResult
|
|
494
|
+
await this.blockSource.addBlock(reexecutionResult.block);
|
|
324
495
|
}
|
|
325
496
|
|
|
326
497
|
this.log.info(
|
|
@@ -331,9 +502,28 @@ export class ProposalHandler {
|
|
|
331
502
|
return { isValid: true, blockNumber, reexecutionResult };
|
|
332
503
|
}
|
|
333
504
|
|
|
505
|
+
private async validateNewBlockInSlot(blockProposal: BlockProposal): Promise<BlockProposalSlotValidationResult> {
|
|
506
|
+
if (!this.p2pClient) {
|
|
507
|
+
return { isValid: true };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const { blockProposals, checkpointProposals } = await this.p2pClient.getProposalsForSlot(blockProposal.slotNumber);
|
|
511
|
+
|
|
512
|
+
if (checkpointProposals.length === 0) {
|
|
513
|
+
return { isValid: true };
|
|
514
|
+
} else if (checkpointProposals.length > 1) {
|
|
515
|
+
return { isValid: false, reason: 'checkpoint_proposal_equivocation' };
|
|
516
|
+
} else {
|
|
517
|
+
const checkpointProposal = checkpointProposals[0];
|
|
518
|
+
const terminalBlock = blockProposals.find(block => block.archive.equals(checkpointProposal.archive));
|
|
519
|
+
return terminalBlock !== undefined && blockProposal.indexWithinCheckpoint > terminalBlock.indexWithinCheckpoint
|
|
520
|
+
? { isValid: false, reason: 'block_proposal_beyond_checkpoint' }
|
|
521
|
+
: { isValid: true };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
334
525
|
private async getParentBlock(proposal: BlockProposal): Promise<'genesis' | BlockData | undefined> {
|
|
335
526
|
const parentArchive = proposal.blockHeader.lastArchive.root;
|
|
336
|
-
const slot = proposal.slotNumber;
|
|
337
527
|
const config = this.checkpointsBuilder.getConfig();
|
|
338
528
|
const { genesisArchiveRoot } = await this.blockSource.getGenesisValues();
|
|
339
529
|
|
|
@@ -341,17 +531,18 @@ export class ProposalHandler {
|
|
|
341
531
|
return 'genesis';
|
|
342
532
|
}
|
|
343
533
|
|
|
344
|
-
const deadline = this.getReexecutionDeadline(
|
|
534
|
+
const deadline = this.getReexecutionDeadline(proposal.slotNumber, config);
|
|
345
535
|
const currentTime = this.dateProvider.now();
|
|
346
536
|
const timeoutDurationMs = deadline.getTime() - currentTime;
|
|
347
537
|
|
|
348
538
|
try {
|
|
349
539
|
return (
|
|
350
|
-
(await this.blockSource.
|
|
540
|
+
(await this.blockSource.getBlockData({ archive: parentArchive })) ??
|
|
351
541
|
(timeoutDurationMs <= 0
|
|
352
542
|
? undefined
|
|
353
543
|
: await retryUntil(
|
|
354
|
-
() =>
|
|
544
|
+
() =>
|
|
545
|
+
this.blockSource.syncImmediate().then(() => this.blockSource.getBlockData({ archive: parentArchive })),
|
|
355
546
|
'force archiver sync',
|
|
356
547
|
timeoutDurationMs / 1000,
|
|
357
548
|
0.5,
|
|
@@ -490,47 +681,17 @@ export class ProposalHandler {
|
|
|
490
681
|
return undefined;
|
|
491
682
|
}
|
|
492
683
|
|
|
493
|
-
private getReexecutionDeadline(
|
|
494
|
-
|
|
684
|
+
private getReexecutionDeadline(
|
|
685
|
+
slotNumber: SlotNumber,
|
|
686
|
+
config: { l1GenesisTime: bigint; slotDuration: number },
|
|
687
|
+
): Date {
|
|
688
|
+
// Under proposer pipelining, the proposal slot may be ahead of wall clock time.
|
|
689
|
+
// Reexecution budgets should still be bounded by the current slot we are in now.
|
|
690
|
+
const wallclockSlot = slotNumber - PROPOSER_PIPELINING_SLOT_OFFSET;
|
|
691
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(wallclockSlot + 1), config));
|
|
495
692
|
return new Date(nextSlotTimestampSeconds * 1000);
|
|
496
693
|
}
|
|
497
694
|
|
|
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
695
|
private getReexecuteFailureReason(err: any): BlockProposalValidationFailureReason {
|
|
535
696
|
if (err instanceof TransactionsNotAvailableError) {
|
|
536
697
|
return 'txs_not_available';
|
|
@@ -560,7 +721,7 @@ export class ProposalHandler {
|
|
|
560
721
|
// If we do not have all of the transactions, then we should fail
|
|
561
722
|
if (txs.length !== txHashes.length) {
|
|
562
723
|
const foundTxHashes = txs.map(tx => tx.getTxHash());
|
|
563
|
-
const missingTxHashes = txHashes.filter(txHash => !foundTxHashes.
|
|
724
|
+
const missingTxHashes = txHashes.filter(txHash => !foundTxHashes.some(h => h.equals(txHash)));
|
|
564
725
|
throw new TransactionsNotAvailableError(missingTxHashes);
|
|
565
726
|
}
|
|
566
727
|
|
|
@@ -672,27 +833,55 @@ export class ProposalHandler {
|
|
|
672
833
|
}
|
|
673
834
|
|
|
674
835
|
/**
|
|
675
|
-
* Validates a checkpoint proposal and uploads blobs if configured.
|
|
676
|
-
*
|
|
836
|
+
* Validates a checkpoint proposal, caches the result, and uploads blobs if configured.
|
|
837
|
+
* Returns a cached result if the same proposal (archive + slot) was already validated.
|
|
838
|
+
* Used by both the all-nodes callback (via register) and the validator client (via delegation).
|
|
677
839
|
*/
|
|
678
840
|
async handleCheckpointProposal(
|
|
679
841
|
proposal: CheckpointProposalCore,
|
|
680
842
|
proposalInfo: LogData,
|
|
681
843
|
): Promise<CheckpointProposalValidationResult> {
|
|
844
|
+
const slot = proposal.slotNumber;
|
|
845
|
+
const payloadHash = proposal.getPayloadHash();
|
|
846
|
+
|
|
847
|
+
// Check cache: same signed-payload hash means we already validated this exact proposal.
|
|
848
|
+
if (this.lastCheckpointValidationResult && this.lastCheckpointValidationResult.payloadHash === payloadHash) {
|
|
849
|
+
this.log.debug(`Returning cached validation result for checkpoint proposal at slot ${slot}`, proposalInfo);
|
|
850
|
+
return this.lastCheckpointValidationResult.result;
|
|
851
|
+
}
|
|
852
|
+
|
|
682
853
|
const proposer = proposal.getSender();
|
|
854
|
+
let result: CheckpointProposalValidationResult;
|
|
683
855
|
if (!proposer) {
|
|
684
856
|
this.log.warn(`Received checkpoint proposal with invalid signature for slot ${proposal.slotNumber}`);
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
|
|
857
|
+
result = { isValid: false as const, reason: 'invalid_signature' };
|
|
858
|
+
} else if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
|
|
689
859
|
this.log.warn(
|
|
690
860
|
`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${proposal.slotNumber}`,
|
|
691
861
|
);
|
|
692
|
-
|
|
862
|
+
result = { isValid: false, reason: 'invalid_fee_asset_price_modifier' };
|
|
863
|
+
} else {
|
|
864
|
+
result = await this.validateCheckpointProposal(proposal, proposalInfo);
|
|
693
865
|
}
|
|
694
866
|
|
|
695
|
-
|
|
867
|
+
this.lastCheckpointValidationResult = { payloadHash, result };
|
|
868
|
+
|
|
869
|
+
// Record the outcome on the re-execution tracker.
|
|
870
|
+
const outcome = result.isValid ? ('valid' as const) : CHECKPOINT_VALIDATION_REASON_TO_OUTCOME[result.reason];
|
|
871
|
+
if (outcome !== undefined) {
|
|
872
|
+
this.reexecutionTracker.recordOutcome(slot, proposal.archive, outcome, result.checkpointNumber);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Drop tracker entries for checkpoints that have reached L1 finality.
|
|
876
|
+
try {
|
|
877
|
+
const tips = await this.blockSource.getL2Tips();
|
|
878
|
+
const finalizedCheckpointNumber = tips.finalized.checkpoint.number;
|
|
879
|
+
if (finalizedCheckpointNumber > 0) {
|
|
880
|
+
this.reexecutionTracker.removeBefore(CheckpointNumber(finalizedCheckpointNumber + 1));
|
|
881
|
+
}
|
|
882
|
+
} catch (err) {
|
|
883
|
+
this.log.error(`Error pruning reexecution tracker`, err, proposalInfo);
|
|
884
|
+
}
|
|
696
885
|
|
|
697
886
|
// Upload blobs to filestore if validation passed (fire and forget)
|
|
698
887
|
if (result.isValid) {
|
|
@@ -712,18 +901,24 @@ export class ProposalHandler {
|
|
|
712
901
|
): Promise<CheckpointProposalValidationResult> {
|
|
713
902
|
const slot = proposal.slotNumber;
|
|
714
903
|
|
|
715
|
-
//
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
904
|
+
// Block-sync deadline = the L1 publish deadline, i.e. the latest moment the proposer can submit
|
|
905
|
+
// this checkpoint and still have it land on L1 in the target slot. That is 12s (one Ethereum
|
|
906
|
+
// slot) before the last L1 block of the target slot, which is later than the target-slot start
|
|
907
|
+
// used for block re-execution. Keeping validation/attestation alive until then lets validators
|
|
908
|
+
// keep attesting right up to the proposer's real publish cutoff.
|
|
909
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
910
|
+
const publishDeadlineSeconds =
|
|
911
|
+
Number(getLastL1SlotTimestampForL2Slot(slot, l1Constants)) - l1Constants.ethereumSlotDuration;
|
|
912
|
+
const deadline = new Date(publishDeadlineSeconds * 1000);
|
|
913
|
+
const timeoutSeconds = Math.max(1, Math.floor((deadline.getTime() - this.dateProvider.now()) / 1000));
|
|
719
914
|
|
|
720
915
|
// Wait for last block to sync by archive
|
|
721
|
-
let
|
|
916
|
+
let lastBlockData;
|
|
722
917
|
try {
|
|
723
|
-
|
|
918
|
+
lastBlockData = await retryUntil(
|
|
724
919
|
async () => {
|
|
725
920
|
await this.blockSource.syncImmediate();
|
|
726
|
-
return this.blockSource.
|
|
921
|
+
return await this.blockSource.getBlockData({ archive: proposal.archive });
|
|
727
922
|
},
|
|
728
923
|
`waiting for block with archive ${proposal.archive.toString()} for slot ${slot}`,
|
|
729
924
|
timeoutSeconds,
|
|
@@ -738,22 +933,54 @@ export class ProposalHandler {
|
|
|
738
933
|
return { isValid: false, reason: 'block_fetch_error' };
|
|
739
934
|
}
|
|
740
935
|
|
|
741
|
-
if (!
|
|
936
|
+
if (!lastBlockData) {
|
|
742
937
|
this.log.warn(`Last block not found for checkpoint proposal`, proposalInfo);
|
|
743
938
|
return { isValid: false, reason: 'last_block_not_found' };
|
|
744
939
|
}
|
|
745
940
|
|
|
941
|
+
// Refuse to attest if the block's enclosing checkpoint has already been published to L1.
|
|
942
|
+
const existingCheckpoint = await this.blockSource.getCheckpointData({ number: lastBlockData.checkpointNumber });
|
|
943
|
+
if (existingCheckpoint) {
|
|
944
|
+
this.log.warn(`Refusing to attest to checkpoint proposal whose checkpoint is already on L1`, {
|
|
945
|
+
...proposalInfo,
|
|
946
|
+
checkpointNumber: lastBlockData.checkpointNumber,
|
|
947
|
+
});
|
|
948
|
+
return {
|
|
949
|
+
isValid: false,
|
|
950
|
+
reason: 'checkpoint_already_published',
|
|
951
|
+
checkpointNumber: lastBlockData.checkpointNumber,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
746
955
|
// Get all full blocks for the slot and checkpoint
|
|
747
956
|
const blocks = await this.blockSource.getBlocksForSlot(slot);
|
|
748
957
|
if (blocks.length === 0) {
|
|
749
958
|
this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
|
|
750
|
-
return { isValid: false, reason: 'no_blocks_for_slot' };
|
|
959
|
+
return { isValid: false, reason: 'no_blocks_for_slot', checkpointNumber: lastBlockData.checkpointNumber };
|
|
751
960
|
}
|
|
752
961
|
|
|
753
962
|
// Ensure the last block for this slot matches the archive in the checkpoint proposal
|
|
754
963
|
if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
|
|
755
964
|
this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
|
|
756
|
-
return {
|
|
965
|
+
return {
|
|
966
|
+
isValid: false,
|
|
967
|
+
reason: 'last_block_archive_mismatch',
|
|
968
|
+
checkpointNumber: lastBlockData.checkpointNumber,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const maxBlocksPerCheckpoint = this.config.maxBlocksPerCheckpoint;
|
|
973
|
+
if (maxBlocksPerCheckpoint !== undefined && blocks.length > maxBlocksPerCheckpoint) {
|
|
974
|
+
this.log.warn(`Checkpoint proposal exceeds maxBlocksPerCheckpoint`, {
|
|
975
|
+
...proposalInfo,
|
|
976
|
+
blocksInProposal: blocks.length,
|
|
977
|
+
maxBlocksPerCheckpoint,
|
|
978
|
+
});
|
|
979
|
+
return {
|
|
980
|
+
isValid: false,
|
|
981
|
+
reason: 'too_many_blocks_in_checkpoint',
|
|
982
|
+
checkpointNumber: lastBlockData.checkpointNumber,
|
|
983
|
+
};
|
|
757
984
|
}
|
|
758
985
|
|
|
759
986
|
this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
|
|
@@ -769,87 +996,90 @@ export class ProposalHandler {
|
|
|
769
996
|
// Get L1-to-L2 messages for this checkpoint
|
|
770
997
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
771
998
|
|
|
772
|
-
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
999
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch.
|
|
1000
|
+
// See note on the analogous block-proposal site: the helper handles pipelining lag.
|
|
773
1001
|
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
774
|
-
const previousCheckpointOutHashes =
|
|
775
|
-
|
|
776
|
-
|
|
1002
|
+
const previousCheckpointOutHashes = await getPreviousCheckpointOutHashes({
|
|
1003
|
+
blockSource: this.blockSource,
|
|
1004
|
+
epoch,
|
|
1005
|
+
checkpointNumber,
|
|
1006
|
+
l1Constants: this.epochCache.getL1Constants(),
|
|
1007
|
+
pipeliningEnabled: true,
|
|
1008
|
+
log: this.log,
|
|
1009
|
+
});
|
|
777
1010
|
|
|
778
1011
|
// Fork world state at the block before the first block
|
|
779
1012
|
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
780
|
-
|
|
1013
|
+
await using fork = await this.checkpointsBuilder.getFork(parentBlockNumber);
|
|
781
1014
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
);
|
|
1015
|
+
// Create checkpoint builder with all existing blocks
|
|
1016
|
+
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
1017
|
+
checkpointNumber,
|
|
1018
|
+
constants,
|
|
1019
|
+
proposal.feeAssetPriceModifier,
|
|
1020
|
+
l1ToL2Messages,
|
|
1021
|
+
previousCheckpointOutHashes,
|
|
1022
|
+
fork,
|
|
1023
|
+
blocks,
|
|
1024
|
+
this.log.getBindings(),
|
|
1025
|
+
);
|
|
794
1026
|
|
|
795
|
-
|
|
796
|
-
|
|
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
|
-
}
|
|
1027
|
+
// Complete the checkpoint to get computed values
|
|
1028
|
+
const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
|
|
807
1029
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1030
|
+
// Compare checkpoint header with proposal
|
|
1031
|
+
if (!computedCheckpoint.header.equals(proposal.checkpointHeader)) {
|
|
1032
|
+
this.log.warn(`Checkpoint header mismatch`, {
|
|
1033
|
+
...proposalInfo,
|
|
1034
|
+
computed: computedCheckpoint.header.toInspect(),
|
|
1035
|
+
proposal: proposal.checkpointHeader.toInspect(),
|
|
1036
|
+
});
|
|
1037
|
+
return { isValid: false, reason: 'checkpoint_header_mismatch', checkpointNumber };
|
|
1038
|
+
}
|
|
817
1039
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
checkpointOutHash: checkpointOutHash.toString(),
|
|
828
|
-
previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
|
|
829
|
-
...proposalInfo,
|
|
830
|
-
});
|
|
831
|
-
return { isValid: false, reason: 'out_hash_mismatch' };
|
|
832
|
-
}
|
|
1040
|
+
// Compare archive root with proposal
|
|
1041
|
+
if (!computedCheckpoint.archive.root.equals(proposal.archive)) {
|
|
1042
|
+
this.log.warn(`Archive root mismatch`, {
|
|
1043
|
+
...proposalInfo,
|
|
1044
|
+
computed: computedCheckpoint.archive.root.toString(),
|
|
1045
|
+
proposal: proposal.archive.toString(),
|
|
1046
|
+
});
|
|
1047
|
+
return { isValid: false, reason: 'archive_mismatch', checkpointNumber };
|
|
1048
|
+
}
|
|
833
1049
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
}
|
|
1050
|
+
// Check that the accumulated epoch out hash matches the value in the proposal.
|
|
1051
|
+
// The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
|
|
1052
|
+
const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
1053
|
+
const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
|
|
1054
|
+
const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
|
|
1055
|
+
if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
|
|
1056
|
+
this.log.warn(`Epoch out hash mismatch`, {
|
|
1057
|
+
proposalEpochOutHash: proposalEpochOutHash.toString(),
|
|
1058
|
+
computedEpochOutHash: computedEpochOutHash.toString(),
|
|
1059
|
+
checkpointOutHash: checkpointOutHash.toString(),
|
|
1060
|
+
previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
|
|
1061
|
+
...proposalInfo,
|
|
1062
|
+
});
|
|
1063
|
+
return { isValid: false, reason: 'out_hash_mismatch', checkpointNumber };
|
|
1064
|
+
}
|
|
847
1065
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1066
|
+
// Final round of validations on the checkpoint, just in case.
|
|
1067
|
+
try {
|
|
1068
|
+
validateCheckpoint(computedCheckpoint, {
|
|
1069
|
+
rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
|
|
1070
|
+
maxDABlockGas: this.config.validateMaxDABlockGas,
|
|
1071
|
+
maxL2BlockGas: this.config.validateMaxL2BlockGas,
|
|
1072
|
+
maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
|
|
1073
|
+
maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint,
|
|
1074
|
+
});
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
|
|
1077
|
+
return { isValid: false, reason: 'checkpoint_validation_failed', checkpointNumber };
|
|
852
1078
|
}
|
|
1079
|
+
|
|
1080
|
+
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
1081
|
+
|
|
1082
|
+
return { isValid: true, checkpointNumber };
|
|
853
1083
|
}
|
|
854
1084
|
|
|
855
1085
|
/** Extracts checkpoint global variables from a block. */
|
|
@@ -876,7 +1106,7 @@ export class ProposalHandler {
|
|
|
876
1106
|
/** Uploads blobs for a checkpoint to the filestore. */
|
|
877
1107
|
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
878
1108
|
try {
|
|
879
|
-
const lastBlockHeader = await this.blockSource.
|
|
1109
|
+
const lastBlockHeader = (await this.blockSource.getBlockData({ archive: proposal.archive }))?.header;
|
|
880
1110
|
if (!lastBlockHeader) {
|
|
881
1111
|
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
882
1112
|
return;
|
|
@@ -900,4 +1130,32 @@ export class ProposalHandler {
|
|
|
900
1130
|
this.log.warn(`Failed to upload blobs for checkpoint: ${err}`, proposalInfo);
|
|
901
1131
|
}
|
|
902
1132
|
}
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* Derives proposed checkpoint data from validated blocks and sets it on the archiver.
|
|
1136
|
+
* Used after successful validation of a foreign proposal.
|
|
1137
|
+
* Does not retry since we already waited for the block during validation.
|
|
1138
|
+
*/
|
|
1139
|
+
private async setProposedCheckpointFromValidation(proposal: CheckpointProposalCore): Promise<boolean> {
|
|
1140
|
+
if (!this.archiver) {
|
|
1141
|
+
return false;
|
|
1142
|
+
}
|
|
1143
|
+
const blockData = await this.blockSource.getBlockData({ archive: proposal.archive });
|
|
1144
|
+
if (!blockData) {
|
|
1145
|
+
this.log.debug(`Block data not found for checkpoint proposal archive, cannot set proposed checkpoint`, {
|
|
1146
|
+
archive: proposal.archive.toString(),
|
|
1147
|
+
});
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
await this.archiver.addProposedCheckpoint({
|
|
1152
|
+
header: proposal.checkpointHeader,
|
|
1153
|
+
checkpointNumber: blockData.checkpointNumber,
|
|
1154
|
+
startBlock: BlockNumber(blockData.header.getBlockNumber() - blockData.indexWithinCheckpoint),
|
|
1155
|
+
blockCount: blockData.indexWithinCheckpoint + 1,
|
|
1156
|
+
totalManaUsed: proposal.checkpointHeader.totalManaUsed.toBigInt(),
|
|
1157
|
+
feeAssetPriceModifier: proposal.feeAssetPriceModifier,
|
|
1158
|
+
});
|
|
1159
|
+
return true;
|
|
1160
|
+
}
|
|
903
1161
|
}
|