@aztec/validator-client 0.0.1-commit.b9865e97 → 0.0.1-commit.be03c316
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 +2 -0
- package/dest/checkpoint_builder.d.ts +6 -4
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +16 -8
- package/dest/config.d.ts +9 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +9 -1
- package/dest/factory.d.ts +1 -1
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +10 -3
- package/dest/key_store/web3signer_key_store.d.ts +10 -2
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
- package/dest/key_store/web3signer_key_store.js +32 -41
- package/dest/proposal_handler.d.ts +37 -6
- package/dest/proposal_handler.d.ts.map +1 -1
- package/dest/proposal_handler.js +182 -47
- package/dest/validator.d.ts +11 -5
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +51 -42
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +16 -7
- package/src/config.ts +16 -3
- package/src/factory.ts +9 -1
- package/src/key_store/web3signer_key_store.ts +43 -59
- package/src/proposal_handler.ts +203 -50
- package/src/validator.ts +61 -46
package/src/proposal_handler.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Archiver } from '@aztec/archiver';
|
|
|
2
2
|
import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
3
3
|
import { type Blob, encodeCheckpointBlobDataFromBlocks, getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
4
4
|
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
5
|
-
import {
|
|
5
|
+
import type { EpochCache } from '@aztec/epoch-cache';
|
|
6
6
|
import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
|
|
7
7
|
import {
|
|
8
8
|
BlockNumber,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { pick } from '@aztec/foundation/collection';
|
|
14
14
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
15
15
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
16
|
+
import { FifoSet } from '@aztec/foundation/fifo-set';
|
|
16
17
|
import type { LogData } from '@aztec/foundation/log';
|
|
17
18
|
import { createLogger } from '@aztec/foundation/log';
|
|
18
19
|
import { retryUntil } from '@aztec/foundation/retry';
|
|
@@ -22,7 +23,7 @@ import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
|
|
|
22
23
|
import type { BlockData, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
23
24
|
import type { CheckpointReexecutionTracker, ReexecutionOutcome } from '@aztec/stdlib/checkpoint';
|
|
24
25
|
import { getPreviousCheckpointOutHashes, validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
25
|
-
import { getEpochAtSlot
|
|
26
|
+
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
26
27
|
import { Gas } from '@aztec/stdlib/gas';
|
|
27
28
|
import type { ITxProvider, ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
28
29
|
import {
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
computeInHashFromL1ToL2Messages,
|
|
32
33
|
} from '@aztec/stdlib/messaging';
|
|
33
34
|
import type { BlockProposal, CheckpointAttestation, CheckpointProposalCore } from '@aztec/stdlib/p2p';
|
|
35
|
+
import type { ConsensusTimetable } from '@aztec/stdlib/timetable';
|
|
34
36
|
import { MerkleTreeId } from '@aztec/stdlib/trees';
|
|
35
37
|
import type { CheckpointGlobalVariables, FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
36
38
|
import {
|
|
@@ -151,7 +153,49 @@ type BlockProposalSlotValidationResult =
|
|
|
151
153
|
| { isValid: true }
|
|
152
154
|
| { isValid: false; reason: 'block_proposal_beyond_checkpoint' | 'checkpoint_proposal_equivocation' };
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
|
|
157
|
+
|
|
158
|
+
/** Block-proposal validation failures that constitute a slashable invalid-block offense. */
|
|
159
|
+
export const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT: BlockProposalValidationFailureReason[] = [
|
|
160
|
+
'state_mismatch',
|
|
161
|
+
'failed_txs',
|
|
162
|
+
'global_variables_mismatch',
|
|
163
|
+
'invalid_proposal',
|
|
164
|
+
'parent_block_wrong_slot',
|
|
165
|
+
'in_hash_mismatch',
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
/** Checkpoint-proposal validation failures that constitute a slashable invalid-checkpoint offense. */
|
|
169
|
+
export const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT: Record<
|
|
170
|
+
CheckpointProposalValidationFailureReason,
|
|
171
|
+
boolean
|
|
172
|
+
> = {
|
|
173
|
+
// enabled
|
|
174
|
+
['invalid_fee_asset_price_modifier']: true,
|
|
175
|
+
['checkpoint_header_mismatch']: true,
|
|
176
|
+
// These late mismatches should normally be caught by earlier checks, but if reached after validating the local
|
|
177
|
+
// checkpoint inputs, the proposer-signed payload disagrees with deterministic recomputation.
|
|
178
|
+
['archive_mismatch']: true,
|
|
179
|
+
['out_hash_mismatch']: true,
|
|
180
|
+
['no_blocks_for_slot']: true,
|
|
181
|
+
['too_many_blocks_in_checkpoint']: true,
|
|
182
|
+
['checkpoint_validation_failed']: true,
|
|
183
|
+
['last_block_archive_mismatch']: true,
|
|
184
|
+
|
|
185
|
+
// disabled
|
|
186
|
+
['invalid_signature']: false,
|
|
187
|
+
['last_block_not_found']: false,
|
|
188
|
+
['block_fetch_error']: false,
|
|
189
|
+
['checkpoint_already_published']: false,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Handles block and checkpoint proposals for both validator and non-validator nodes. Also tracks which slots
|
|
194
|
+
* had a slashable invalid proposal or a proposal equivocation, exposing them via the
|
|
195
|
+
* `InvalidProposalSlotSource` interface consumed by the attested-invalid-proposal slashing watcher. The
|
|
196
|
+
* tracking is populated as a side effect of validating/re-executing proposals, so any node that re-executes
|
|
197
|
+
* proposals (the default) can serve it — not only validators.
|
|
198
|
+
*/
|
|
155
199
|
export class ProposalHandler {
|
|
156
200
|
public readonly tracer: Tracer;
|
|
157
201
|
|
|
@@ -164,7 +208,7 @@ export class ProposalHandler {
|
|
|
164
208
|
};
|
|
165
209
|
|
|
166
210
|
/** Archiver reference for setting proposed checkpoints (pipelining). Set via register(). */
|
|
167
|
-
private archiver?: Pick<Archiver, 'addProposedCheckpoint'>;
|
|
211
|
+
private archiver?: Pick<Archiver, 'addProposedCheckpoint' | 'getProposedCheckpointData'>;
|
|
168
212
|
|
|
169
213
|
/** Returns current validator addresses for own-proposal detection. Set via register(). */
|
|
170
214
|
private getOwnValidatorAddresses?: () => string[];
|
|
@@ -174,6 +218,12 @@ export class ProposalHandler {
|
|
|
174
218
|
|
|
175
219
|
private checkpointProposalValidationFailureCallback?: CheckpointProposalValidationFailureCallback;
|
|
176
220
|
|
|
221
|
+
/** Slots at which a slashable invalid block or checkpoint proposal was observed. */
|
|
222
|
+
private readonly slotsWithInvalidProposals = FifoSet.withLimit<SlotNumber>(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
|
|
223
|
+
|
|
224
|
+
/** Slots at which a proposal equivocation was observed; suppresses attested-to-invalid-proposal slashing. */
|
|
225
|
+
private readonly slotsWithProposalEquivocation = FifoSet.withLimit<SlotNumber>(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
|
|
226
|
+
|
|
177
227
|
constructor(
|
|
178
228
|
private checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
179
229
|
private worldState: WorldStateSynchronizer,
|
|
@@ -182,6 +232,7 @@ export class ProposalHandler {
|
|
|
182
232
|
private txProvider: ITxProvider,
|
|
183
233
|
private blockProposalValidator: BlockProposalValidator,
|
|
184
234
|
private epochCache: EpochCache,
|
|
235
|
+
private timetable: ConsensusTimetable,
|
|
185
236
|
private config: ValidatorClientFullConfig,
|
|
186
237
|
private blobClient: BlobClientInterface,
|
|
187
238
|
private reexecutionTracker: CheckpointReexecutionTracker,
|
|
@@ -219,6 +270,26 @@ export class ProposalHandler {
|
|
|
219
270
|
this.reexecutionTracker.recordOutcome(slot, archive, 'valid', checkpointNumber);
|
|
220
271
|
}
|
|
221
272
|
|
|
273
|
+
/** Whether a slashable invalid block or checkpoint proposal was observed at the given slot (InvalidProposalSlotSource). */
|
|
274
|
+
public hasInvalidProposals(slotNumber: SlotNumber): boolean {
|
|
275
|
+
return this.slotsWithInvalidProposals.has(slotNumber);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Whether a proposal equivocation was observed at the given slot (InvalidProposalSlotSource). */
|
|
279
|
+
public hasProposalEquivocation(slotNumber: SlotNumber): boolean {
|
|
280
|
+
return this.slotsWithProposalEquivocation.has(slotNumber);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Records a slot as having a slashable invalid proposal, for offense observers (sentinel/slasher watchers). */
|
|
284
|
+
public markInvalidProposalSlot(slotNumber: SlotNumber): void {
|
|
285
|
+
this.slotsWithInvalidProposals.add(slotNumber);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Records a slot as having a proposal equivocation, which suppresses attested-to-invalid-proposal slashing. */
|
|
289
|
+
public markProposalEquivocation(slotNumber: SlotNumber): void {
|
|
290
|
+
this.slotsWithProposalEquivocation.add(slotNumber);
|
|
291
|
+
}
|
|
292
|
+
|
|
222
293
|
/**
|
|
223
294
|
* Registers handlers for block and checkpoint proposals on the p2p client.
|
|
224
295
|
* Records the p2p client so validation can inspect retained proposals.
|
|
@@ -230,7 +301,7 @@ export class ProposalHandler {
|
|
|
230
301
|
register(
|
|
231
302
|
p2pClient: P2P,
|
|
232
303
|
shouldReexecute: boolean,
|
|
233
|
-
archiver?: Pick<Archiver, 'addProposedCheckpoint'>,
|
|
304
|
+
archiver?: Pick<Archiver, 'addProposedCheckpoint' | 'getProposedCheckpointData'>,
|
|
234
305
|
getOwnValidatorAddresses?: () => string[],
|
|
235
306
|
): ProposalHandler {
|
|
236
307
|
this.p2pClient = p2pClient;
|
|
@@ -254,6 +325,18 @@ export class ProposalHandler {
|
|
|
254
325
|
});
|
|
255
326
|
return true;
|
|
256
327
|
} else {
|
|
328
|
+
// Track invalid proposals / equivocations so offense observers (the attested-invalid-proposal
|
|
329
|
+
// watcher) work on non-validator nodes too. Validators populate these via their own handlers.
|
|
330
|
+
// Skip invalid-proposal marking while the escape hatch is open, matching the validator path,
|
|
331
|
+
// which intentionally disables invalid-block slashing then.
|
|
332
|
+
if (result.reason === 'checkpoint_proposal_equivocation') {
|
|
333
|
+
this.markProposalEquivocation(slotNumber);
|
|
334
|
+
} else if (
|
|
335
|
+
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(result.reason) &&
|
|
336
|
+
!(await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber))
|
|
337
|
+
) {
|
|
338
|
+
this.markInvalidProposalSlot(slotNumber);
|
|
339
|
+
}
|
|
257
340
|
this.log.warn(
|
|
258
341
|
`Non-validator block proposal ${blockNumber} at slot ${slotNumber} failed processing with ${result.reason}`,
|
|
259
342
|
{ blockNumber: result.blockNumber, slotNumber, reason: result.reason },
|
|
@@ -268,6 +351,11 @@ export class ProposalHandler {
|
|
|
268
351
|
|
|
269
352
|
p2pClient.registerBlockProposalHandler(blockHandler);
|
|
270
353
|
|
|
354
|
+
// p2p detects duplicate (equivocated) proposals without routing them through the handlers above, so mark
|
|
355
|
+
// the slot as equivocated here. This suppresses false-positive attested-to-invalid-proposal slashing on
|
|
356
|
+
// non-validator offense collectors. Validators overwrite this with their own richer handler.
|
|
357
|
+
p2pClient.registerDuplicateProposalCallback(info => this.markProposalEquivocation(info.slot));
|
|
358
|
+
|
|
271
359
|
// All-nodes checkpoint proposal handler: validates, caches, and sets proposed checkpoint for pipelining.
|
|
272
360
|
// Runs for all nodes (validators and non-validators). Validators get the cached result in the
|
|
273
361
|
// validator-specific callback (attestToCheckpointProposal) which runs after this one.
|
|
@@ -296,25 +384,35 @@ export class ProposalHandler {
|
|
|
296
384
|
return undefined;
|
|
297
385
|
}
|
|
298
386
|
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
387
|
+
// A proposal is "own" when it was signed by a validator key this node also owns. The true local
|
|
388
|
+
// proposer already built, validated, and stored this checkpoint before broadcasting, so a matching
|
|
389
|
+
// proposed checkpoint is already in its archiver — skip the redundant re-validation. An HA peer that
|
|
390
|
+
// shares the proposer's keys sees the same "own" proposal over gossip but never built it, so it has
|
|
391
|
+
// nothing stored; it falls through to the normal validate-and-persist path below to hydrate the
|
|
392
|
+
// proposed-checkpoint metadata it needs to build the next slot on top of this checkpoint.
|
|
304
393
|
const proposer = proposal.getSender();
|
|
305
394
|
const ownAddresses = this.getOwnValidatorAddresses?.();
|
|
306
395
|
const isOwnProposal = proposer && ownAddresses?.some(addr => addr === proposer.toString());
|
|
307
396
|
|
|
308
397
|
if (isOwnProposal) {
|
|
309
|
-
|
|
310
|
-
|
|
398
|
+
const existing = await this.archiver?.getProposedCheckpointData({ slot: proposal.slotNumber });
|
|
399
|
+
if (existing?.archive.root.equals(proposal.archive)) {
|
|
400
|
+
this.log.debug(`Skipping sync for existing own checkpoint proposal at slot ${proposal.slotNumber}`);
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
311
403
|
}
|
|
312
404
|
|
|
313
405
|
const result = await this.handleCheckpointProposal(proposal, proposalInfo);
|
|
314
406
|
if (!result.isValid) {
|
|
407
|
+
// Track invalid checkpoint proposals so offense observers (the attested-invalid-proposal watcher)
|
|
408
|
+
// work on non-validator nodes too. This handler runs for all nodes; validators also mark via the
|
|
409
|
+
// failure callback below (idempotent).
|
|
410
|
+
if (SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT[result.reason]) {
|
|
411
|
+
this.markInvalidProposalSlot(proposal.slotNumber);
|
|
412
|
+
}
|
|
315
413
|
await this.checkpointProposalValidationFailureCallback?.(proposal, result, proposalInfo);
|
|
316
414
|
} else if (this.archiver) {
|
|
317
|
-
const set = await this.
|
|
415
|
+
const set = await this.setProposedCheckpoint(proposal);
|
|
318
416
|
if (set) {
|
|
319
417
|
this.metrics?.recordCheckpointProposalToPipelinedStateDuration(pipeliningTimer.ms());
|
|
320
418
|
}
|
|
@@ -337,7 +435,6 @@ export class ProposalHandler {
|
|
|
337
435
|
): Promise<BlockProposalValidationResult> {
|
|
338
436
|
const slotNumber = proposal.slotNumber;
|
|
339
437
|
const proposer = proposal.getSender();
|
|
340
|
-
const config = this.checkpointsBuilder.getConfig();
|
|
341
438
|
|
|
342
439
|
// Reject proposals with invalid signatures
|
|
343
440
|
if (!proposer) {
|
|
@@ -405,8 +502,12 @@ export class ProposalHandler {
|
|
|
405
502
|
: BlockNumber(parentBlock.header.getBlockNumber() + 1);
|
|
406
503
|
proposalInfo.blockNumber = blockNumber;
|
|
407
504
|
|
|
408
|
-
// Check that this block number does not exist already
|
|
409
|
-
|
|
505
|
+
// Check that this block number does not exist already. During a reorg the archiver can still hold a
|
|
506
|
+
// stale block at this number (a different archive, about to be pruned) while the proposal carries the
|
|
507
|
+
// rebuilt replacement; resolveExistingBlockAtNumber waits for the local prune in that case so the
|
|
508
|
+
// rebuilt block is processed in time to attest, rather than being permanently dropped on a bare
|
|
509
|
+
// number collision.
|
|
510
|
+
const existingBlock = await this.resolveExistingBlockAtNumber(blockNumber, proposal.archive, slotNumber);
|
|
410
511
|
if (existingBlock) {
|
|
411
512
|
this.log.warn(`Block number ${blockNumber} already exists, skipping processing`, proposalInfo);
|
|
412
513
|
return { isValid: false, blockNumber, reason: 'block_number_already_exists' };
|
|
@@ -416,7 +517,7 @@ export class ProposalHandler {
|
|
|
416
517
|
// 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.
|
|
417
518
|
const { txs, missingTxs } = await this.txProvider.getTxsForBlockProposal(proposal, blockNumber, {
|
|
418
519
|
pinnedPeer: proposalSender,
|
|
419
|
-
deadline: this.getReexecutionDeadline(slotNumber
|
|
520
|
+
deadline: this.getReexecutionDeadline(slotNumber),
|
|
420
521
|
});
|
|
421
522
|
|
|
422
523
|
// Record the tx-collection outcome on the re-execution tracker
|
|
@@ -490,7 +591,7 @@ export class ProposalHandler {
|
|
|
490
591
|
}
|
|
491
592
|
|
|
492
593
|
// If we succeeded, push this block into the archiver (unless disabled)
|
|
493
|
-
if (reexecutionResult?.block && this.config.skipPushProposedBlocksToArchiver
|
|
594
|
+
if (reexecutionResult?.block && !this.config.skipPushProposedBlocksToArchiver) {
|
|
494
595
|
await this.blockSource.addBlock(reexecutionResult.block);
|
|
495
596
|
}
|
|
496
597
|
|
|
@@ -524,16 +625,14 @@ export class ProposalHandler {
|
|
|
524
625
|
|
|
525
626
|
private async getParentBlock(proposal: BlockProposal): Promise<'genesis' | BlockData | undefined> {
|
|
526
627
|
const parentArchive = proposal.blockHeader.lastArchive.root;
|
|
527
|
-
const config = this.checkpointsBuilder.getConfig();
|
|
528
628
|
const { genesisArchiveRoot } = await this.blockSource.getGenesisValues();
|
|
529
629
|
|
|
530
630
|
if (parentArchive.equals(genesisArchiveRoot)) {
|
|
531
631
|
return 'genesis';
|
|
532
632
|
}
|
|
533
633
|
|
|
534
|
-
const deadline = this.getReexecutionDeadline(proposal.slotNumber
|
|
535
|
-
const
|
|
536
|
-
const timeoutDurationMs = deadline.getTime() - currentTime;
|
|
634
|
+
const deadline = this.getReexecutionDeadline(proposal.slotNumber);
|
|
635
|
+
const timeoutDurationMs = deadline.getTime() - this.dateProvider.now();
|
|
537
636
|
|
|
538
637
|
try {
|
|
539
638
|
return (
|
|
@@ -544,7 +643,7 @@ export class ProposalHandler {
|
|
|
544
643
|
() =>
|
|
545
644
|
this.blockSource.syncImmediate().then(() => this.blockSource.getBlockData({ archive: parentArchive })),
|
|
546
645
|
'force archiver sync',
|
|
547
|
-
|
|
646
|
+
{ deadline, dateProvider: this.dateProvider },
|
|
548
647
|
0.5,
|
|
549
648
|
))
|
|
550
649
|
);
|
|
@@ -558,6 +657,63 @@ export class ProposalHandler {
|
|
|
558
657
|
}
|
|
559
658
|
}
|
|
560
659
|
|
|
660
|
+
/**
|
|
661
|
+
* Resolves whether a block genuinely already exists at `blockNumber`. Returns the existing block only if
|
|
662
|
+
* it is a true duplicate of the proposal (matching archive). During a reorg the archiver can still hold a
|
|
663
|
+
* stale fork at this number (different archive) that is about to be pruned; in that case this forces L1
|
|
664
|
+
* sync and waits, bounded by the re-execution deadline, for the prune to land, then returns `undefined` so
|
|
665
|
+
* the rebuilt block proposal can be processed in time to attest. If the prune does not complete before the
|
|
666
|
+
* deadline it returns the stale block, so the caller falls back to the safe `block_number_already_exists`
|
|
667
|
+
* rejection.
|
|
668
|
+
*/
|
|
669
|
+
private async resolveExistingBlockAtNumber(
|
|
670
|
+
blockNumber: BlockNumber,
|
|
671
|
+
proposalArchive: Fr,
|
|
672
|
+
slotNumber: SlotNumber,
|
|
673
|
+
): Promise<BlockData | undefined> {
|
|
674
|
+
const existingBlock = await this.blockSource.getBlockData({ number: blockNumber });
|
|
675
|
+
if (!existingBlock || existingBlock.archive.root.equals(proposalArchive)) {
|
|
676
|
+
return existingBlock;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// A different block already occupies this number: it may be a stale fork being pruned during a reorg, not a
|
|
680
|
+
// genuine duplicate. Wait for the local prune rather than permanently rejecting the proposal.
|
|
681
|
+
const deadline = this.getReexecutionDeadline(slotNumber);
|
|
682
|
+
if (deadline.getTime() - this.dateProvider.now() <= 0) {
|
|
683
|
+
return existingBlock;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
this.log.warn(`Block number ${blockNumber} already exists, awaiting potential prune`, {
|
|
687
|
+
blockNumber,
|
|
688
|
+
existingArchive: existingBlock.archive.root.toString(),
|
|
689
|
+
proposalArchive: proposalArchive.toString(),
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
try {
|
|
693
|
+
const { block } = await retryUntil(
|
|
694
|
+
async () => {
|
|
695
|
+
await this.blockSource.syncImmediate();
|
|
696
|
+
const block = await this.blockSource.getBlockData({ number: blockNumber });
|
|
697
|
+
// Resolve once the existing block is gone (pruned) or has been replaced by one matching the
|
|
698
|
+
// proposal — the same condition as the early return above. A matching block is returned so the
|
|
699
|
+
// caller still treats it as a genuine duplicate; an `undefined` (pruned) block lets the proposal
|
|
700
|
+
// be processed. Wrap in an object so the `undefined` case is still a truthy retry result.
|
|
701
|
+
return block === undefined || block.archive.root.equals(proposalArchive) ? { block } : undefined;
|
|
702
|
+
},
|
|
703
|
+
`prune of stale block ${blockNumber}`,
|
|
704
|
+
{ deadline, dateProvider: this.dateProvider },
|
|
705
|
+
0.5,
|
|
706
|
+
);
|
|
707
|
+
return block;
|
|
708
|
+
} catch (err) {
|
|
709
|
+
if (err instanceof TimeoutError) {
|
|
710
|
+
this.log.warn(`Timed out waiting for stale block ${blockNumber} to be pruned`, { blockNumber });
|
|
711
|
+
return existingBlock;
|
|
712
|
+
}
|
|
713
|
+
throw err;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
561
717
|
private computeCheckpointNumber(
|
|
562
718
|
proposal: BlockProposal,
|
|
563
719
|
parentBlock: 'genesis' | BlockData,
|
|
@@ -681,15 +837,14 @@ export class ProposalHandler {
|
|
|
681
837
|
return undefined;
|
|
682
838
|
}
|
|
683
839
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
return new Date(nextSlotTimestampSeconds * 1000);
|
|
840
|
+
/**
|
|
841
|
+
* Hard re-execution/validation deadline for any block or checkpoint proposal targeting `slotNumber`:
|
|
842
|
+
* the single consensus `attestation_deadline` (`target_slot_start + S - 2E`). This is the latest the
|
|
843
|
+
* checkpoint can land on L1 in the target slot; all nodes agree on it. Loosened from the previous
|
|
844
|
+
* next-wall-clock-slot-boundary bound (see the timetable spec / refactor notes).
|
|
845
|
+
*/
|
|
846
|
+
private getReexecutionDeadline(slotNumber: SlotNumber): Date {
|
|
847
|
+
return new Date(this.timetable.getAttestationDeadline(slotNumber) * 1000);
|
|
693
848
|
}
|
|
694
849
|
|
|
695
850
|
private getReexecuteFailureReason(err: any): BlockProposalValidationFailureReason {
|
|
@@ -769,7 +924,7 @@ export class ProposalHandler {
|
|
|
769
924
|
);
|
|
770
925
|
|
|
771
926
|
// Build the new block
|
|
772
|
-
const deadline = this.getReexecutionDeadline(slot
|
|
927
|
+
const deadline = this.getReexecutionDeadline(slot);
|
|
773
928
|
const maxBlockGas =
|
|
774
929
|
this.config.validateMaxL2BlockGas !== undefined || this.config.validateMaxDABlockGas !== undefined
|
|
775
930
|
? new Gas(this.config.validateMaxDABlockGas ?? Infinity, this.config.validateMaxL2BlockGas ?? Infinity)
|
|
@@ -901,18 +1056,15 @@ export class ProposalHandler {
|
|
|
901
1056
|
): Promise<CheckpointProposalValidationResult> {
|
|
902
1057
|
const slot = proposal.slotNumber;
|
|
903
1058
|
|
|
904
|
-
// Block-sync deadline = the
|
|
905
|
-
// this checkpoint and still have it land on L1 in
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
const timeoutSeconds = Math.max(1, Math.floor((deadline.getTime() - this.dateProvider.now()) / 1000));
|
|
914
|
-
|
|
915
|
-
// Wait for last block to sync by archive
|
|
1059
|
+
// Block-sync/validation deadline = the single consensus attestation_deadline (target_slot_start + S
|
|
1060
|
+
// - 2E): the latest moment the proposer can submit this checkpoint and still have it land on L1 in
|
|
1061
|
+
// the target slot. Keeping validation/attestation alive until then lets validators keep attesting
|
|
1062
|
+
// right up to the proposer's real publish cutoff.
|
|
1063
|
+
const deadline = this.getReexecutionDeadline(slot);
|
|
1064
|
+
|
|
1065
|
+
// Wait for last block to sync by archive. The deadline is passed to retryUntil as an absolute date so
|
|
1066
|
+
// the remaining budget is derived from the date provider; a deadline already in the past times out
|
|
1067
|
+
// after a single attempt instead of looping (the immediate-timeout semantics of the deadline overload).
|
|
916
1068
|
let lastBlockData;
|
|
917
1069
|
try {
|
|
918
1070
|
lastBlockData = await retryUntil(
|
|
@@ -921,7 +1073,7 @@ export class ProposalHandler {
|
|
|
921
1073
|
return await this.blockSource.getBlockData({ archive: proposal.archive });
|
|
922
1074
|
},
|
|
923
1075
|
`waiting for block with archive ${proposal.archive.toString()} for slot ${slot}`,
|
|
924
|
-
|
|
1076
|
+
{ deadline, dateProvider: this.dateProvider },
|
|
925
1077
|
0.5,
|
|
926
1078
|
);
|
|
927
1079
|
} catch (err) {
|
|
@@ -969,6 +1121,7 @@ export class ProposalHandler {
|
|
|
969
1121
|
};
|
|
970
1122
|
}
|
|
971
1123
|
|
|
1124
|
+
// Note this condition should never trigger, since we dont process block proposals that exceed indexWithinCheckpoint
|
|
972
1125
|
const maxBlocksPerCheckpoint = this.config.maxBlocksPerCheckpoint;
|
|
973
1126
|
if (maxBlocksPerCheckpoint !== undefined && blocks.length > maxBlocksPerCheckpoint) {
|
|
974
1127
|
this.log.warn(`Checkpoint proposal exceeds maxBlocksPerCheckpoint`, {
|
|
@@ -1132,11 +1285,11 @@ export class ProposalHandler {
|
|
|
1132
1285
|
}
|
|
1133
1286
|
|
|
1134
1287
|
/**
|
|
1135
|
-
* Derives proposed checkpoint data from validated blocks and sets it on the archiver
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1288
|
+
* Derives proposed checkpoint data from validated blocks and sets it on the archiver, so this node can
|
|
1289
|
+
* pipeline building on top of the checkpoint. Does not retry, since validation already waited for the
|
|
1290
|
+
* last block to sync.
|
|
1138
1291
|
*/
|
|
1139
|
-
private async
|
|
1292
|
+
private async setProposedCheckpoint(proposal: CheckpointProposalCore): Promise<boolean> {
|
|
1140
1293
|
if (!this.archiver) {
|
|
1141
1294
|
return false;
|
|
1142
1295
|
}
|