@aztec/sequencer-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 +281 -21
- package/dest/client/sequencer-client.d.ts +4 -3
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +11 -11
- package/dest/config.d.ts +4 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +35 -14
- package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
- package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
- package/dest/global_variable_builder/fee_predictor.js +128 -0
- package/dest/global_variable_builder/fee_provider.d.ts +21 -0
- package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
- package/dest/global_variable_builder/fee_provider.js +58 -0
- package/dest/global_variable_builder/global_builder.d.ts +4 -10
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +5 -40
- package/dest/global_variable_builder/index.d.ts +3 -1
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/global_variable_builder/index.js +2 -0
- package/dest/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/publisher/config.d.ts +19 -3
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +25 -5
- package/dest/publisher/index.d.ts +2 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
- package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +58 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/index.js +2 -0
- package/dest/publisher/sequencer-bundle-simulator.d.ts +96 -0
- package/dest/publisher/sequencer-bundle-simulator.d.ts.map +1 -0
- package/dest/publisher/sequencer-bundle-simulator.js +198 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts +3 -5
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +16 -3
- package/dest/publisher/sequencer-publisher.d.ts +77 -66
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +410 -398
- package/dest/sequencer/automine/automine_factory.d.ts +54 -0
- package/dest/sequencer/automine/automine_factory.d.ts.map +1 -0
- package/dest/sequencer/automine/automine_factory.js +84 -0
- package/dest/sequencer/automine/automine_sequencer.d.ts +151 -0
- package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -0
- package/dest/sequencer/automine/automine_sequencer.js +509 -0
- package/dest/sequencer/chain_state_overrides.d.ts +61 -0
- package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
- package/dest/sequencer/chain_state_overrides.js +98 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts +58 -10
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +736 -191
- package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
- package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
- package/dest/sequencer/events.d.ts +48 -2
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/index.d.ts +3 -1
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +2 -0
- package/dest/sequencer/metrics.d.ts +13 -10
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +45 -20
- package/dest/sequencer/sequencer.d.ts +59 -12
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +344 -105
- package/dest/sequencer/timetable.d.ts +11 -1
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +41 -36
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +0 -2
- package/dest/test/utils.d.ts +1 -1
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +7 -6
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +19 -20
- package/src/config.ts +40 -11
- package/src/global_variable_builder/README.md +44 -0
- package/src/global_variable_builder/fee_predictor.ts +172 -0
- package/src/global_variable_builder/fee_provider.ts +75 -0
- package/src/global_variable_builder/global_builder.ts +10 -46
- package/src/global_variable_builder/index.ts +2 -0
- package/src/index.ts +10 -1
- package/src/publisher/config.ts +49 -6
- package/src/publisher/index.ts +3 -0
- package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +57 -0
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
- package/src/publisher/l1_tx_failed_store/index.ts +3 -0
- package/src/publisher/sequencer-bundle-simulator.ts +254 -0
- package/src/publisher/sequencer-publisher-factory.ts +18 -6
- package/src/publisher/sequencer-publisher.ts +485 -474
- package/src/sequencer/README.md +162 -450
- package/src/sequencer/automine/README.md +46 -0
- package/src/sequencer/automine/automine_factory.ts +145 -0
- package/src/sequencer/automine/automine_sequencer.ts +595 -0
- package/src/sequencer/chain_state_overrides.ts +162 -0
- package/src/sequencer/checkpoint_proposal_job.ts +857 -201
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/events.ts +51 -2
- package/src/sequencer/index.ts +2 -0
- package/src/sequencer/metrics.ts +57 -24
- package/src/sequencer/sequencer.ts +421 -118
- package/src/sequencer/timetable.ts +49 -44
- package/src/sequencer/types.ts +1 -1
- package/src/test/mock_checkpoint_builder.ts +0 -2
- package/src/test/utils.ts +28 -10
|
@@ -372,22 +372,23 @@ function _apply_decs_2203_r(targetClass, memberDecs, classDecs, parentClass) {
|
|
|
372
372
|
}
|
|
373
373
|
var _dec, _dec1, _dec2, _dec3, _initProto;
|
|
374
374
|
import { getKzg } from '@aztec/blob-lib';
|
|
375
|
-
import {
|
|
375
|
+
import { PROPOSER_PIPELINING_SLOT_OFFSET } from '@aztec/epoch-cache';
|
|
376
376
|
import { NoCommitteeError } from '@aztec/ethereum/contracts';
|
|
377
|
-
import {
|
|
377
|
+
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
378
378
|
import { merge, omit, pick } from '@aztec/foundation/collection';
|
|
379
|
-
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
380
379
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
381
380
|
import { createLogger } from '@aztec/foundation/log';
|
|
382
381
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
383
|
-
import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
382
|
+
import { getEpochAtSlot, getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
384
383
|
import { SequencerConfigSchema } from '@aztec/stdlib/interfaces/server';
|
|
385
384
|
import { pickFromSchema } from '@aztec/stdlib/schemas';
|
|
386
|
-
import {
|
|
385
|
+
import { MIN_EXECUTION_TIME } from '@aztec/stdlib/timetable';
|
|
387
386
|
import { Attributes, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
388
387
|
import EventEmitter from 'node:events';
|
|
389
388
|
import { DefaultSequencerConfig } from '../config.js';
|
|
389
|
+
import { buildCheckpointSimulationOverridesPlan } from './chain_state_overrides.js';
|
|
390
390
|
import { CheckpointProposalJob } from './checkpoint_proposal_job.js';
|
|
391
|
+
import { CheckpointProposalJobMetrics } from './checkpoint_proposal_job_metrics.js';
|
|
391
392
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
392
393
|
import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
|
|
393
394
|
import { SequencerMetrics } from './metrics.js';
|
|
@@ -448,32 +449,45 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
448
449
|
}
|
|
449
450
|
runningPromise;
|
|
450
451
|
state;
|
|
452
|
+
stateSlotNumber;
|
|
453
|
+
stateEnteredAtMs;
|
|
451
454
|
metrics;
|
|
455
|
+
checkpointProposalJobMetrics;
|
|
456
|
+
stateLog;
|
|
452
457
|
/** The last slot for which we attempted to perform our voting duties with degraded block production */ lastSlotForFallbackVote;
|
|
453
458
|
/** The last slot for which we logged "no committee" warning, to avoid spam */ lastSlotForNoCommitteeWarning;
|
|
454
459
|
/** The last slot for which we triggered a checkpoint proposal job, to prevent duplicate attempts. */ lastSlotForCheckpointProposalJob;
|
|
455
460
|
/** Last successful checkpoint proposed */ lastCheckpointProposed;
|
|
456
461
|
/** The last epoch for which we logged strategy comparison in fisherman mode. */ lastEpochForStrategyComparison;
|
|
462
|
+
/** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */ lastCheckpointProposalJob;
|
|
457
463
|
/** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */ timetable;
|
|
458
464
|
/** Config for the sequencer */ config;
|
|
465
|
+
signatureContext;
|
|
459
466
|
constructor(publisherFactory, validatorClient, globalsBuilder, p2pClient, worldState, slasherClient, l2BlockSource, l1ToL2MessageSource, checkpointsBuilder, l1Constants, dateProvider, epochCache, rollupContract, config, telemetry = getTelemetryClient(), log = createLogger('sequencer')){
|
|
460
|
-
super(), this.publisherFactory = publisherFactory, this.validatorClient = validatorClient, this.globalsBuilder = globalsBuilder, this.p2pClient = p2pClient, this.worldState = worldState, this.slasherClient = slasherClient, this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.checkpointsBuilder = checkpointsBuilder, this.l1Constants = l1Constants, this.dateProvider = dateProvider, this.epochCache = epochCache, this.rollupContract = rollupContract, this.telemetry = telemetry, this.log = log, this.state = (_initProto(this), SequencerState.STOPPED), this.config = DefaultSequencerConfig;
|
|
467
|
+
super(), this.publisherFactory = publisherFactory, this.validatorClient = validatorClient, this.globalsBuilder = globalsBuilder, this.p2pClient = p2pClient, this.worldState = worldState, this.slasherClient = slasherClient, this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.checkpointsBuilder = checkpointsBuilder, this.l1Constants = l1Constants, this.dateProvider = dateProvider, this.epochCache = epochCache, this.rollupContract = rollupContract, this.telemetry = telemetry, this.log = log, this.state = (_initProto(this), SequencerState.STOPPED), this.stateEnteredAtMs = performance.now(), this.config = DefaultSequencerConfig;
|
|
468
|
+
this.stateLog = log.createChild('state');
|
|
461
469
|
// Add [FISHERMAN] prefix to logger if in fisherman mode
|
|
462
470
|
if (config.fishermanMode) {
|
|
463
471
|
this.log = log.createChild('[FISHERMAN]');
|
|
464
472
|
}
|
|
473
|
+
this.signatureContext = {
|
|
474
|
+
chainId: config.l1ChainId,
|
|
475
|
+
rollupAddress: config.rollupAddress
|
|
476
|
+
};
|
|
465
477
|
this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
|
|
478
|
+
this.checkpointProposalJobMetrics = new CheckpointProposalJobMetrics(telemetry);
|
|
466
479
|
this.updateConfig(config);
|
|
467
480
|
}
|
|
468
481
|
/** Updates sequencer config by the defined values and updates the timetable */ updateConfig(config) {
|
|
469
482
|
const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
|
|
470
483
|
this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
|
|
471
484
|
this.config = merge(this.config, filteredConfig);
|
|
485
|
+
const p2pPropagationTime = this.config.attestationPropagationTime;
|
|
472
486
|
this.timetable = new SequencerTimetable({
|
|
473
487
|
ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
|
|
474
488
|
aztecSlotDuration: this.aztecSlotDuration,
|
|
475
489
|
l1PublishingTime: this.l1PublishingTime,
|
|
476
|
-
p2pPropagationTime
|
|
490
|
+
p2pPropagationTime,
|
|
477
491
|
blockDurationMs: this.config.blockDurationMs,
|
|
478
492
|
enforce: this.config.enforceTimeTable
|
|
479
493
|
}, this.metrics, this.log);
|
|
@@ -497,8 +511,10 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
497
511
|
this.setState(SequencerState.STOPPING, undefined, {
|
|
498
512
|
force: true
|
|
499
513
|
});
|
|
500
|
-
this.
|
|
514
|
+
this.lastCheckpointProposalJob?.interrupt();
|
|
515
|
+
await this.publisherFactory.stopAll();
|
|
501
516
|
await this.runningPromise?.stop();
|
|
517
|
+
await this.lastCheckpointProposalJob?.awaitPendingSubmission();
|
|
502
518
|
this.setState(SequencerState.STOPPED, undefined, {
|
|
503
519
|
force: true
|
|
504
520
|
});
|
|
@@ -508,20 +524,19 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
508
524
|
try {
|
|
509
525
|
await this.work();
|
|
510
526
|
} catch (err) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
//
|
|
516
|
-
|
|
517
|
-
SequencerState.INITIALIZING_CHECKPOINT,
|
|
518
|
-
SequencerState.PROPOSER_CHECK
|
|
519
|
-
].includes(err.proposedState) ? 'debug' : 'warn';
|
|
520
|
-
this.log[logLvl](err.message, {
|
|
527
|
+
if (err instanceof SequencerTooSlowError && [
|
|
528
|
+
SequencerState.INITIALIZING_CHECKPOINT,
|
|
529
|
+
SequencerState.PROPOSER_CHECK
|
|
530
|
+
].includes(err.proposedState)) {
|
|
531
|
+
// No need to alert if we just didn't get to start in time
|
|
532
|
+
this.log.debug(err.message, {
|
|
521
533
|
now: this.dateProvider.nowInSeconds()
|
|
522
534
|
});
|
|
523
535
|
} else {
|
|
524
536
|
// Re-throw other errors
|
|
537
|
+
this.emit('checkpoint-error', {
|
|
538
|
+
error: err
|
|
539
|
+
});
|
|
525
540
|
throw err;
|
|
526
541
|
}
|
|
527
542
|
} finally{
|
|
@@ -542,40 +557,62 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
542
557
|
* - Submit checkpoint
|
|
543
558
|
*/ async work() {
|
|
544
559
|
this.setState(SequencerState.SYNCHRONIZING, undefined);
|
|
545
|
-
const { slot,
|
|
560
|
+
const { slot, targetSlot, epoch, targetEpoch, ts, nowSeconds } = this.getSlotContextInNextL1Slot();
|
|
546
561
|
// Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
|
|
547
|
-
const checkpointProposalJob = await this.prepareCheckpointProposal(epoch,
|
|
562
|
+
const checkpointProposalJob = await this.prepareCheckpointProposal(slot, targetSlot, epoch, targetEpoch, ts, nowSeconds);
|
|
548
563
|
if (!checkpointProposalJob) {
|
|
549
564
|
return;
|
|
550
565
|
}
|
|
566
|
+
// Track the job so we can await its pending L1 submission during shutdown
|
|
567
|
+
this.lastCheckpointProposalJob = checkpointProposalJob;
|
|
551
568
|
// Execute the checkpoint proposal job
|
|
552
569
|
const checkpoint = await checkpointProposalJob.execute();
|
|
553
570
|
// Update last checkpoint proposed (currently unused)
|
|
554
571
|
if (checkpoint) {
|
|
555
572
|
this.lastCheckpointProposed = checkpoint;
|
|
556
573
|
}
|
|
557
|
-
// Log fee strategy comparison if on fisherman
|
|
558
|
-
if (this.config.fishermanMode && (this.lastEpochForStrategyComparison === undefined ||
|
|
559
|
-
this.logStrategyComparison(
|
|
560
|
-
this.lastEpochForStrategyComparison =
|
|
574
|
+
// Log fee strategy comparison if on fisherman (uses target epoch since we mirror the proposer's perspective)
|
|
575
|
+
if (this.config.fishermanMode && (this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)) {
|
|
576
|
+
this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
|
|
577
|
+
this.lastEpochForStrategyComparison = targetEpoch;
|
|
561
578
|
}
|
|
562
579
|
return checkpoint;
|
|
563
580
|
}
|
|
581
|
+
/** Returns slot and target slot from a single clock snapshot. */ getSlotContextInNextL1Slot() {
|
|
582
|
+
const { slot, ts, nowSeconds, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
583
|
+
const targetSlot = SlotNumber(slot + PROPOSER_PIPELINING_SLOT_OFFSET);
|
|
584
|
+
return {
|
|
585
|
+
slot,
|
|
586
|
+
targetSlot,
|
|
587
|
+
epoch,
|
|
588
|
+
targetEpoch: getEpochAtSlot(targetSlot, this.l1Constants),
|
|
589
|
+
ts,
|
|
590
|
+
nowSeconds
|
|
591
|
+
};
|
|
592
|
+
}
|
|
564
593
|
/**
|
|
565
594
|
* Prepares the checkpoint proposal by performing all necessary checks and setup.
|
|
566
595
|
* This is the initial step in the main loop.
|
|
567
596
|
* @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
|
|
568
|
-
*/ async prepareCheckpointProposal(epoch,
|
|
569
|
-
// Check we have not already processed this slot (cheapest check)
|
|
597
|
+
*/ async prepareCheckpointProposal(slot, targetSlot, epoch, targetEpoch, ts, nowSeconds) {
|
|
598
|
+
// Check we have not already processed this target slot (cheapest check)
|
|
570
599
|
// We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
|
|
571
600
|
// running against actual time (eg when we use sandbox-style automining)
|
|
572
|
-
if (this.lastSlotForCheckpointProposalJob && this.lastSlotForCheckpointProposalJob >=
|
|
573
|
-
this.log.trace(`
|
|
601
|
+
if (this.lastSlotForCheckpointProposalJob && this.lastSlotForCheckpointProposalJob >= targetSlot && this.config.enforceTimeTable) {
|
|
602
|
+
this.log.trace(`Target slot ${targetSlot} has already been processed`);
|
|
574
603
|
return undefined;
|
|
575
604
|
}
|
|
576
|
-
// But if we have already proposed for this slot,
|
|
577
|
-
if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >=
|
|
578
|
-
this.log.trace(`Slot ${
|
|
605
|
+
// But if we have already proposed for this slot, then we definitely have to skip it, automining or not
|
|
606
|
+
if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= targetSlot) {
|
|
607
|
+
this.log.trace(`Slot ${targetSlot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`);
|
|
608
|
+
return undefined;
|
|
609
|
+
}
|
|
610
|
+
// Test-only: skip proposing for explicitly paused slots. Attestation paths run in the validator
|
|
611
|
+
// client and are not gated by this hook, so paused proposers still attest to others' proposals.
|
|
612
|
+
if (this.config.pauseProposingForSlots?.some((s)=>s === targetSlot)) {
|
|
613
|
+
this.log.warn(`Skipping proposal for paused slot ${targetSlot} (test-only pauseProposingForSlots hook)`, {
|
|
614
|
+
targetSlot
|
|
615
|
+
});
|
|
579
616
|
return undefined;
|
|
580
617
|
}
|
|
581
618
|
// Check all components are synced to latest as seen by the archiver (queries all subsystems)
|
|
@@ -586,19 +623,22 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
586
623
|
if (!syncedTo) {
|
|
587
624
|
await this.tryVoteWhenSyncFails({
|
|
588
625
|
slot,
|
|
626
|
+
targetSlot,
|
|
589
627
|
ts
|
|
590
628
|
});
|
|
591
629
|
return undefined;
|
|
592
630
|
}
|
|
593
|
-
// If escape hatch is open for
|
|
631
|
+
// If escape hatch is open for the target epoch, do not start checkpoint proposal work and do not attempt invalidations.
|
|
594
632
|
// Still perform governance/slashing voting (as proposer) once per slot.
|
|
595
|
-
|
|
633
|
+
// When pipelining, we check the target epoch (slot+1's epoch) since that's the epoch we're building for.
|
|
634
|
+
const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(targetEpoch);
|
|
596
635
|
if (isEscapeHatchOpen) {
|
|
597
636
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
598
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
637
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
599
638
|
if (canPropose) {
|
|
600
639
|
await this.tryVoteWhenEscapeHatchOpen({
|
|
601
640
|
slot,
|
|
641
|
+
targetSlot,
|
|
602
642
|
proposer
|
|
603
643
|
});
|
|
604
644
|
} else {
|
|
@@ -613,24 +653,32 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
613
653
|
// Next checkpoint follows from the last synced one
|
|
614
654
|
const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
|
|
615
655
|
const logCtx = {
|
|
616
|
-
|
|
656
|
+
nowSeconds,
|
|
617
657
|
syncedToL2Slot: syncedTo.syncedL2Slot,
|
|
618
658
|
slot,
|
|
659
|
+
targetSlot,
|
|
619
660
|
slotTs: ts,
|
|
620
661
|
checkpointNumber,
|
|
621
662
|
isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex')
|
|
622
663
|
};
|
|
623
|
-
// Check that we are a proposer for the
|
|
664
|
+
// Check that we are a proposer for the target slot.
|
|
624
665
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
625
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
666
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
626
667
|
// If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
|
|
627
668
|
if (!canPropose) {
|
|
628
669
|
await this.considerInvalidatingCheckpoint(syncedTo, slot);
|
|
629
670
|
return undefined;
|
|
630
671
|
}
|
|
631
|
-
//
|
|
632
|
-
|
|
633
|
-
|
|
672
|
+
// Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
|
|
673
|
+
// confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
|
|
674
|
+
const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
|
|
675
|
+
if (checkpointNumber > confirmedCkpt + 2) {
|
|
676
|
+
this.log.verbose(`Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`);
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
679
|
+
// Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
|
|
680
|
+
if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
|
|
681
|
+
this.log.warn(`Cannot propose block at target slot ${targetSlot} since that slot was taken by block ${syncedTo.blockNumber}`, {
|
|
634
682
|
...logCtx,
|
|
635
683
|
block: syncedTo.blockData.header.toInspect()
|
|
636
684
|
});
|
|
@@ -644,18 +692,84 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
644
692
|
const proposerForPublisher = this.config.fishermanMode ? undefined : proposer;
|
|
645
693
|
const { attestorAddress, publisher } = await this.publisherFactory.create(proposerForPublisher);
|
|
646
694
|
this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
|
|
647
|
-
//
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
695
|
+
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need).
|
|
696
|
+
// Only simulate invalidation when there's no proposed parent, since we assume the proposed parent
|
|
697
|
+
// will invalidate the currently invalid checkpoint on L1.
|
|
698
|
+
const invalidateCheckpoint = syncedTo.hasProposedCheckpoint || syncedTo.pendingChainValidationStatus.valid ? undefined : await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
|
|
699
|
+
// Determine the correct archive and L1 state overrides for the canProposeAt check.
|
|
700
|
+
// The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
|
|
701
|
+
// When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
|
|
702
|
+
let archiveForCheck = syncedTo.archive;
|
|
703
|
+
if (syncedTo.hasProposedCheckpoint) {
|
|
704
|
+
this.metrics.recordPipelineDepth(syncedTo.checkpointNumber - syncedTo.checkpointedCheckpointNumber);
|
|
705
|
+
this.log.verbose(`Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber}) for target slot ${targetSlot}`, {
|
|
706
|
+
targetSlot,
|
|
707
|
+
parentCheckpointNumber: CheckpointNumber(checkpointNumber - 1)
|
|
708
|
+
});
|
|
709
|
+
// Match what L1 will see at archives[pending] once the proposed parent lands: the parent's
|
|
710
|
+
// own archive root from the gossiped proposal. `syncedTo.archive` is the world-state-local
|
|
711
|
+
// view and can transiently diverge from the proposed parent (e.g. before the proposed
|
|
712
|
+
// parent's blocks have been applied locally); diverging here would cause the canProposeAt
|
|
713
|
+
// override to set archives[pending] to one value while we present another for comparison.
|
|
714
|
+
archiveForCheck = syncedTo.proposedCheckpointData.archive.root;
|
|
715
|
+
} else if (invalidateCheckpoint) {
|
|
716
|
+
// After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
|
|
717
|
+
// exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
|
|
718
|
+
archiveForCheck = invalidateCheckpoint.lastArchive;
|
|
719
|
+
this.metrics.recordPipelineDepth(0);
|
|
720
|
+
} else {
|
|
721
|
+
this.metrics.recordPipelineDepth(0);
|
|
651
722
|
}
|
|
652
|
-
//
|
|
653
|
-
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
|
|
723
|
+
// Build the simulation plan: pending/proven override from pipelining or invalidation (or the
|
|
724
|
+
// current snapshot when neither applies, to short-circuit any pending prune in simulation),
|
|
725
|
+
// plus the parent checkpoint cell and fee header when pipelining.
|
|
726
|
+
const simulationOverridesPlan = await buildCheckpointSimulationOverridesPlan({
|
|
727
|
+
checkpointNumber,
|
|
728
|
+
proposedCheckpointData: syncedTo.hasProposedCheckpoint ? syncedTo.proposedCheckpointData : undefined,
|
|
729
|
+
invalidateToPendingCheckpointNumber: invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
730
|
+
checkpointedCheckpointNumber: syncedTo.checkpointedCheckpointNumber,
|
|
731
|
+
rollup: this.rollupContract,
|
|
732
|
+
signatureContext: this.signatureContext,
|
|
733
|
+
log: this.log
|
|
734
|
+
});
|
|
735
|
+
// The plan always pins both pending/proven (to short-circuit `canPruneAtTime` in simulation),
|
|
736
|
+
// so `provenOverride` always reflects the assumed proven checkpoint we are pinning the
|
|
737
|
+
// simulation to. We additionally warn when the pin is load-bearing — i.e. when a prune would
|
|
738
|
+
// actually fire at the target slot without it — so observers can spot "we are building
|
|
739
|
+
// optimistically across a pruning boundary" in the logs.
|
|
740
|
+
const provenOverride = simulationOverridesPlan?.chainTipsOverride?.proven;
|
|
741
|
+
if (provenOverride !== undefined && await this.l2BlockSource.isPruneDueAtSlot(targetSlot)) {
|
|
742
|
+
this.log.warn(`Assuming proof for epoch ending at checkpoint ${provenOverride} lands by target slot ${targetSlot}`, {
|
|
743
|
+
checkpointNumber,
|
|
744
|
+
slot,
|
|
745
|
+
targetSlot,
|
|
746
|
+
provenOverride
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
this.emit('preparing-checkpoint', {
|
|
750
|
+
targetSlot,
|
|
751
|
+
checkpointNumber,
|
|
752
|
+
hadProposedParent: syncedTo.hasProposedCheckpoint,
|
|
753
|
+
provenOverride,
|
|
754
|
+
simulatedPending: simulationOverridesPlan?.chainTipsOverride?.pending
|
|
755
|
+
});
|
|
756
|
+
const canProposeCheck = await publisher.canProposeAt(archiveForCheck, proposer ?? EthAddress.ZERO, simulationOverridesPlan);
|
|
757
|
+
const proposeContext = {
|
|
758
|
+
hasProposedCheckpoint: syncedTo.hasProposedCheckpoint,
|
|
759
|
+
proposedCheckpointNumber: syncedTo.proposedCheckpointData?.checkpointNumber,
|
|
760
|
+
checkpointedCheckpointNumber: syncedTo.checkpointedCheckpointNumber,
|
|
761
|
+
isInvalidating: !!invalidateCheckpoint,
|
|
762
|
+
invalidatingCheckpointNumber: invalidateCheckpoint?.checkpointNumber,
|
|
763
|
+
archiveForCheck: archiveForCheck.toString(),
|
|
764
|
+
overridePendingCheckpointNumber: simulationOverridesPlan?.chainTipsOverride?.pending,
|
|
765
|
+
overrideArchive: simulationOverridesPlan?.pendingCheckpointState?.archive,
|
|
766
|
+
overrideFeeHeader: simulationOverridesPlan?.pendingCheckpointState?.feeHeader
|
|
767
|
+
};
|
|
657
768
|
if (canProposeCheck === undefined) {
|
|
658
|
-
this.log.warn(`Cannot propose checkpoint ${checkpointNumber} at slot ${slot} due to failed rollup contract check`,
|
|
769
|
+
this.log.warn(`Cannot propose checkpoint ${checkpointNumber} at slot ${slot} due to failed rollup contract check`, {
|
|
770
|
+
...logCtx,
|
|
771
|
+
...proposeContext
|
|
772
|
+
});
|
|
659
773
|
this.emit('proposer-rollup-check-failed', {
|
|
660
774
|
reason: 'Rollup contract check failed',
|
|
661
775
|
slot
|
|
@@ -663,11 +777,12 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
663
777
|
this.metrics.recordCheckpointPrecheckFailed('rollup_contract_check_failed');
|
|
664
778
|
return undefined;
|
|
665
779
|
}
|
|
666
|
-
if (canProposeCheck.slot !==
|
|
667
|
-
this.log.warn(`Cannot propose block due to slot mismatch with rollup contract (this can be caused by a clock out of sync). Expected slot ${
|
|
780
|
+
if (canProposeCheck.slot !== targetSlot) {
|
|
781
|
+
this.log.warn(`Cannot propose block due to slot mismatch with rollup contract (this can be caused by a clock out of sync). Expected slot ${targetSlot} but got ${canProposeCheck.slot}.`, {
|
|
668
782
|
...logCtx,
|
|
783
|
+
...proposeContext,
|
|
669
784
|
rollup: canProposeCheck,
|
|
670
|
-
expectedSlot:
|
|
785
|
+
expectedSlot: targetSlot
|
|
671
786
|
});
|
|
672
787
|
this.emit('proposer-rollup-check-failed', {
|
|
673
788
|
reason: 'Slot mismatch',
|
|
@@ -679,6 +794,7 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
679
794
|
if (canProposeCheck.checkpointNumber !== checkpointNumber) {
|
|
680
795
|
this.log.warn(`Cannot propose due to block mismatch with rollup contract (this can be caused by a pending archiver sync). Expected checkpoint ${checkpointNumber} but got ${canProposeCheck.checkpointNumber}.`, {
|
|
681
796
|
...logCtx,
|
|
797
|
+
...proposeContext,
|
|
682
798
|
rollup: canProposeCheck,
|
|
683
799
|
expectedSlot: slot
|
|
684
800
|
});
|
|
@@ -689,17 +805,20 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
689
805
|
this.metrics.recordCheckpointPrecheckFailed('block_number_mismatch');
|
|
690
806
|
return undefined;
|
|
691
807
|
}
|
|
692
|
-
this.lastSlotForCheckpointProposalJob =
|
|
693
|
-
await this.p2pClient.prepareForSlot(
|
|
694
|
-
this.log.info(`Preparing checkpoint proposal ${checkpointNumber}
|
|
808
|
+
this.lastSlotForCheckpointProposalJob = targetSlot;
|
|
809
|
+
await this.p2pClient.prepareForSlot(targetSlot);
|
|
810
|
+
this.log.info(`Preparing checkpoint proposal ${checkpointNumber} for target slot ${targetSlot} during wall-clock slot ${slot}`, {
|
|
695
811
|
...logCtx,
|
|
812
|
+
...proposeContext,
|
|
696
813
|
proposer
|
|
697
814
|
});
|
|
698
815
|
// Create and return the checkpoint proposal job
|
|
699
|
-
return this.createCheckpointProposalJob(
|
|
816
|
+
return this.createCheckpointProposalJob(slot, targetSlot, targetEpoch, checkpointNumber, syncedTo.blockNumber, syncedTo.checkpointedCheckpointNumber, proposer, publisher, attestorAddress, invalidateCheckpoint, syncedTo.proposedCheckpointData);
|
|
700
817
|
}
|
|
701
|
-
createCheckpointProposalJob(
|
|
702
|
-
return new CheckpointProposalJob(
|
|
818
|
+
createCheckpointProposalJob(slot, targetSlot, targetEpoch, checkpointNumber, syncedToBlockNumber, checkpointedCheckpointNumber, proposer, publisher, attestorAddress, invalidateCheckpoint, proposedCheckpointData) {
|
|
819
|
+
return new CheckpointProposalJob(slot, targetSlot, targetEpoch, checkpointNumber, syncedToBlockNumber, checkpointedCheckpointNumber, proposer, publisher, attestorAddress, invalidateCheckpoint, this.validatorClient, this.globalsBuilder, this.p2pClient, this.worldState, this.l1ToL2MessageSource, this.l2BlockSource, this.checkpointsBuilder, this.l2BlockSource, this.l1Constants, this.signatureContext, this.config, this.timetable, this.slasherClient, this.epochCache, this.dateProvider, this.metrics, this.checkpointProposalJobMetrics.createRecorder(), this, (state, slot, timeReferenceSlot)=>this.setState(state, slot, {
|
|
820
|
+
timeReferenceSlot
|
|
821
|
+
}), this.tracer, this.log.getBindings(), proposedCheckpointData);
|
|
703
822
|
}
|
|
704
823
|
/**
|
|
705
824
|
* Returns the current sequencer state.
|
|
@@ -709,8 +828,9 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
709
828
|
/**
|
|
710
829
|
* Internal helper for setting the sequencer state and checks if we have enough time left in the slot to transition to the new state.
|
|
711
830
|
* @param proposedState - The new state to transition to.
|
|
712
|
-
* @param slotNumber - The
|
|
831
|
+
* @param slotNumber - The logical slot associated with the new state.
|
|
713
832
|
* @param force - Whether to force the transition even if the sequencer is stopped.
|
|
833
|
+
* @param timeReferenceSlot - The slot to use for deadline checks when it differs from the logical slot.
|
|
714
834
|
*/ setState(proposedState, slotNumber, opts = {}) {
|
|
715
835
|
if (this.state === SequencerState.STOPPING && proposedState !== SequencerState.STOPPED && !opts.force) {
|
|
716
836
|
this.log.warn(`Cannot set sequencer to ${proposedState} as it is stopping.`);
|
|
@@ -721,25 +841,44 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
721
841
|
return;
|
|
722
842
|
}
|
|
723
843
|
let secondsIntoSlot = undefined;
|
|
724
|
-
|
|
725
|
-
|
|
844
|
+
const timeReferenceSlot = opts.timeReferenceSlot ?? slotNumber;
|
|
845
|
+
if (timeReferenceSlot !== undefined) {
|
|
846
|
+
secondsIntoSlot = this.getSecondsIntoSlot(timeReferenceSlot);
|
|
726
847
|
this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
|
|
727
848
|
}
|
|
849
|
+
const oldState = this.state;
|
|
850
|
+
const oldStateSlotNumber = this.stateSlotNumber;
|
|
851
|
+
const stateChanged = proposedState !== oldState;
|
|
852
|
+
const transitionAtMs = performance.now();
|
|
853
|
+
const stateDurationMs = transitionAtMs - this.stateEnteredAtMs;
|
|
728
854
|
const boringStates = [
|
|
729
855
|
SequencerState.IDLE,
|
|
730
856
|
SequencerState.SYNCHRONIZING
|
|
731
857
|
];
|
|
732
|
-
const logLevel = boringStates.includes(proposedState) && boringStates.includes(
|
|
733
|
-
this.
|
|
858
|
+
const logLevel = boringStates.includes(proposedState) && boringStates.includes(oldState) ? 'trace' : 'debug';
|
|
859
|
+
this.stateLog[logLevel](`Transitioning from ${oldState} to ${proposedState}`, {
|
|
860
|
+
oldState,
|
|
861
|
+
newState: proposedState,
|
|
734
862
|
slotNumber,
|
|
735
|
-
|
|
863
|
+
timeReferenceSlot,
|
|
864
|
+
stateSlotNumber: oldStateSlotNumber,
|
|
865
|
+
secondsIntoSlot,
|
|
866
|
+
...stateChanged && {
|
|
867
|
+
stateDurationMs: Math.ceil(stateDurationMs)
|
|
868
|
+
}
|
|
736
869
|
});
|
|
737
870
|
this.emit('state-changed', {
|
|
738
|
-
oldState
|
|
871
|
+
oldState,
|
|
739
872
|
newState: proposedState,
|
|
740
873
|
secondsIntoSlot,
|
|
741
|
-
slot: slotNumber
|
|
874
|
+
slot: slotNumber,
|
|
875
|
+
timeReferenceSlot
|
|
742
876
|
});
|
|
877
|
+
if (stateChanged) {
|
|
878
|
+
this.metrics.recordStateDuration(stateDurationMs, oldState);
|
|
879
|
+
this.stateEnteredAtMs = transitionAtMs;
|
|
880
|
+
this.stateSlotNumber = slotNumber;
|
|
881
|
+
}
|
|
743
882
|
this.state = proposedState;
|
|
744
883
|
}
|
|
745
884
|
/**
|
|
@@ -763,71 +902,142 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
763
902
|
number: syncSummary.latestBlockNumber,
|
|
764
903
|
hash: syncSummary.latestBlockHash
|
|
765
904
|
})),
|
|
766
|
-
this.l2BlockSource.getL2Tips().then((t)=>
|
|
905
|
+
this.l2BlockSource.getL2Tips().then((t)=>({
|
|
906
|
+
proposed: t.proposed,
|
|
907
|
+
checkpointed: t.checkpointed,
|
|
908
|
+
proposedCheckpoint: t.proposedCheckpoint
|
|
909
|
+
})),
|
|
767
910
|
this.p2pClient.getStatus().then((p2p)=>p2p.syncedToL2Block),
|
|
768
|
-
this.l1ToL2MessageSource.getL2Tips().then((t)=>
|
|
769
|
-
|
|
911
|
+
this.l1ToL2MessageSource.getL2Tips().then((t)=>({
|
|
912
|
+
proposed: t.proposed,
|
|
913
|
+
checkpointed: t.checkpointed
|
|
914
|
+
})),
|
|
915
|
+
this.l2BlockSource.getPendingChainValidationStatus(),
|
|
916
|
+
this.l2BlockSource.getProposedCheckpointData()
|
|
770
917
|
]);
|
|
771
|
-
const [worldState,
|
|
772
|
-
|
|
773
|
-
// as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
|
|
774
|
-
// TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
|
|
775
|
-
const result = l2BlockSource.number === 0 && worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0 || worldState.hash === l2BlockSource.hash && p2p.hash === l2BlockSource.hash && l1ToL2MessageSource.hash === l2BlockSource.hash;
|
|
918
|
+
const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] = syncedBlocks;
|
|
919
|
+
const result = worldState.hash === l2Tips.proposed.hash && p2p.hash === l2Tips.proposed.hash && l1ToL2MessageSourceTips.proposed.hash === l2Tips.proposed.hash && l1ToL2MessageSourceTips.checkpointed.block.hash === l2Tips.checkpointed.block.hash && l1ToL2MessageSourceTips.checkpointed.checkpoint.hash === l2Tips.checkpointed.checkpoint.hash;
|
|
776
920
|
if (!result) {
|
|
777
921
|
this.log.debug(`Sequencer sync check failed`, {
|
|
778
922
|
worldState,
|
|
779
|
-
l2BlockSource,
|
|
923
|
+
l2BlockSource: l2Tips.proposed,
|
|
780
924
|
p2p,
|
|
781
|
-
|
|
925
|
+
l1ToL2MessageSourceTips
|
|
782
926
|
});
|
|
783
927
|
return undefined;
|
|
784
928
|
}
|
|
785
|
-
// Special case for genesis state
|
|
786
929
|
const blockNumber = worldState.number;
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
930
|
+
const blockData = await this.l2BlockSource.getBlockData({
|
|
931
|
+
number: blockNumber
|
|
932
|
+
});
|
|
933
|
+
if (!blockData) {
|
|
934
|
+
this.log.warn(`Sequencer sync check failed: failed to get L2 block data ${blockNumber} from the archiver`, {
|
|
935
|
+
blockNumber,
|
|
936
|
+
l2Tips,
|
|
937
|
+
syncedL2Slot,
|
|
938
|
+
...args
|
|
939
|
+
});
|
|
940
|
+
return undefined;
|
|
941
|
+
}
|
|
942
|
+
// Refuse to build a checkpoint on top of a proposed block whose enclosing checkpoint was never
|
|
943
|
+
// proposed. Under pipelining we may have received and reexecuted such a block locally — advancing
|
|
944
|
+
// our world-state tip past the checkpointed tip — while the proposing node never published the
|
|
945
|
+
// matching proposed checkpoint (e.g. it crashed before assembling it). Building on this orphan block
|
|
946
|
+
// would fork the chain off a tip no other node can follow. The archiver prunes these orphan blocks
|
|
947
|
+
// once their build slot ends; this guard is the correctness barrier during the grace window before.
|
|
948
|
+
if (blockData.checkpointNumber > l2Tips.checkpointed.checkpoint.number && (l2Tips.proposedCheckpoint.checkpoint.number !== blockData.checkpointNumber || proposedCheckpointData?.checkpointNumber !== blockData.checkpointNumber)) {
|
|
949
|
+
const logCtx = {
|
|
950
|
+
blockCheckpointNumber: blockData.checkpointNumber,
|
|
951
|
+
checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
|
|
952
|
+
proposedCheckpointTipNumber: l2Tips.proposedCheckpoint.checkpoint.number,
|
|
953
|
+
proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber,
|
|
954
|
+
blockNumber: blockData.header.getBlockNumber(),
|
|
955
|
+
blockSlot: blockData.header.getSlot(),
|
|
793
956
|
syncedL2Slot,
|
|
794
|
-
|
|
957
|
+
...args
|
|
795
958
|
};
|
|
959
|
+
// Under pipelining the block proposal for a checkpoint leads its checkpoint proposal by up to one
|
|
960
|
+
// slot, so a world-state tip sitting in an as-yet-unproposed checkpoint is the expected steady state
|
|
961
|
+
// until that checkpoint is due. Only treat it as abnormal — and warn — once the checkpoint is overdue
|
|
962
|
+
// by the same deadline the archiver uses to prune the orphan block (see pruneOrphanProposedBlocks).
|
|
963
|
+
// Before then this is normal pipelining and we wait it out quietly.
|
|
964
|
+
if (this.isProposedCheckpointOverdue(blockData.header.getSlot())) {
|
|
965
|
+
this.log.warn(`Sequencer sync check failed: proposed block has no matching proposed checkpoint`, logCtx);
|
|
966
|
+
} else {
|
|
967
|
+
this.log.debug(`Waiting for proposed checkpoint to catch up with reexecuted block`, logCtx);
|
|
968
|
+
}
|
|
969
|
+
return undefined;
|
|
796
970
|
}
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
971
|
+
const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
|
|
972
|
+
// The l2Tips and proposedCheckpointData reads above come from independent archiver snapshots
|
|
973
|
+
// (a JS-side tips cache vs. a direct store read on `#proposedCheckpoints`). A concurrent archiver
|
|
974
|
+
// write that mutates both can be observed split, leaving us with `hasProposedCheckpoint=true` but
|
|
975
|
+
// no proposedCheckpointData (or one whose number doesn't match the tip). Refuse to proceed in that
|
|
976
|
+
// window — the next checkSync tick will see a coherent snapshot.
|
|
977
|
+
if (hasProposedCheckpoint && (!proposedCheckpointData || proposedCheckpointData.checkpointNumber !== l2Tips.proposedCheckpoint.checkpoint.number)) {
|
|
978
|
+
this.log.warn(`Sequencer sync check failed: inconsistent proposed-checkpoint state`, {
|
|
979
|
+
proposedCheckpointTipNumber: l2Tips.proposedCheckpoint.checkpoint.number,
|
|
980
|
+
checkpointedTipNumber: l2Tips.checkpointed.checkpoint.number,
|
|
981
|
+
proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber,
|
|
982
|
+
syncedL2Slot,
|
|
983
|
+
...args
|
|
984
|
+
});
|
|
985
|
+
return undefined;
|
|
986
|
+
}
|
|
987
|
+
// Check that the proposed checkpoint is indeed the parent of the checkpoint we'll be building
|
|
988
|
+
// The checkpoint number to build is derived as blockData.checkpointNumber + 1
|
|
989
|
+
if (proposedCheckpointData && proposedCheckpointData.checkpointNumber !== blockData.checkpointNumber) {
|
|
990
|
+
this.log.warn(`Sequencer sync check failed: proposed checkpoint number mismatch`, {
|
|
991
|
+
proposedCheckpointNumber: proposedCheckpointData.checkpointNumber,
|
|
992
|
+
blockCheckpointNumber: blockData.checkpointNumber,
|
|
993
|
+
syncedL2Slot,
|
|
994
|
+
...args
|
|
995
|
+
});
|
|
801
996
|
return undefined;
|
|
802
997
|
}
|
|
803
998
|
return {
|
|
804
999
|
blockData,
|
|
805
1000
|
blockNumber: blockData.header.getBlockNumber(),
|
|
806
1001
|
checkpointNumber: blockData.checkpointNumber,
|
|
1002
|
+
checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
|
|
807
1003
|
archive: blockData.archive.root,
|
|
1004
|
+
hasProposedCheckpoint,
|
|
1005
|
+
proposedCheckpointData,
|
|
808
1006
|
syncedL2Slot,
|
|
809
1007
|
pendingChainValidationStatus
|
|
810
1008
|
};
|
|
811
1009
|
}
|
|
812
1010
|
/**
|
|
1011
|
+
* Whether the enclosing checkpoint of a reexecuted block is overdue: past the deadline by which a
|
|
1012
|
+
* well-behaved proposer should have published it. Mirrors the archiver's orphan-prune deadline (the
|
|
1013
|
+
* start of the slot after the block's build slot, plus a grace period) so the sequencer only warns
|
|
1014
|
+
* about a missing proposed checkpoint once the archiver itself would prune the orphan block. The grace
|
|
1015
|
+
* is derived from the block build duration the same way the archiver defaults it at node wiring.
|
|
1016
|
+
*/ isProposedCheckpointOverdue(blockSlot) {
|
|
1017
|
+
const expectedBySlot = SlotNumber(Number(blockSlot) - PROPOSER_PIPELINING_SLOT_OFFSET + 1);
|
|
1018
|
+
const graceSeconds = this.config.blockDurationMs !== undefined ? Math.ceil(this.config.blockDurationMs / 1000) : MIN_EXECUTION_TIME;
|
|
1019
|
+
const expectedByTime = getTimestampForSlot(expectedBySlot, this.l1Constants) + BigInt(graceSeconds);
|
|
1020
|
+
return BigInt(this.dateProvider.nowInSeconds()) >= expectedByTime;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
813
1023
|
* Checks if we are the proposer for the next slot.
|
|
814
1024
|
* @returns True if we can propose, and the proposer address (undefined if anyone can propose)
|
|
815
|
-
*/ async checkCanPropose(
|
|
1025
|
+
*/ async checkCanPropose(targetSlot) {
|
|
816
1026
|
let proposer;
|
|
817
1027
|
try {
|
|
818
|
-
proposer = await this.epochCache.getProposerAttesterAddressInSlot(
|
|
1028
|
+
proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
|
|
819
1029
|
} catch (e) {
|
|
820
1030
|
if (e instanceof NoCommitteeError) {
|
|
821
|
-
if (this.lastSlotForNoCommitteeWarning !==
|
|
822
|
-
this.lastSlotForNoCommitteeWarning =
|
|
823
|
-
this.log.warn(`Cannot propose at
|
|
1031
|
+
if (this.lastSlotForNoCommitteeWarning !== targetSlot) {
|
|
1032
|
+
this.lastSlotForNoCommitteeWarning = targetSlot;
|
|
1033
|
+
this.log.warn(`Cannot propose at target slot ${targetSlot} since the committee does not exist on L1`);
|
|
824
1034
|
}
|
|
825
1035
|
return [
|
|
826
1036
|
false,
|
|
827
1037
|
undefined
|
|
828
1038
|
];
|
|
829
1039
|
}
|
|
830
|
-
this.log.error(`Error getting proposer for slot ${
|
|
1040
|
+
this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
|
|
831
1041
|
return [
|
|
832
1042
|
false,
|
|
833
1043
|
undefined
|
|
@@ -850,7 +1060,8 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
850
1060
|
const validatorAddresses = this.validatorClient.getValidatorAddresses();
|
|
851
1061
|
const weAreProposer = validatorAddresses.some((addr)=>addr.equals(proposer));
|
|
852
1062
|
if (!weAreProposer) {
|
|
853
|
-
this.log.debug(`Cannot propose at slot ${
|
|
1063
|
+
this.log.debug(`Cannot propose at target slot ${targetSlot} since we are not a proposer`, {
|
|
1064
|
+
targetSlot,
|
|
854
1065
|
validatorAddresses,
|
|
855
1066
|
proposer
|
|
856
1067
|
});
|
|
@@ -859,6 +1070,10 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
859
1070
|
proposer
|
|
860
1071
|
];
|
|
861
1072
|
}
|
|
1073
|
+
this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
|
|
1074
|
+
targetSlot,
|
|
1075
|
+
proposer
|
|
1076
|
+
});
|
|
862
1077
|
return [
|
|
863
1078
|
true,
|
|
864
1079
|
proposer
|
|
@@ -868,7 +1083,7 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
868
1083
|
* Tries to vote on slashing actions and governance when the sync check fails but we're past the max time for initializing a proposal.
|
|
869
1084
|
* This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
|
|
870
1085
|
*/ async tryVoteWhenSyncFails(args) {
|
|
871
|
-
const { slot } = args;
|
|
1086
|
+
const { slot, targetSlot } = args;
|
|
872
1087
|
// Prevent duplicate attempts in the same slot
|
|
873
1088
|
if (this.lastSlotForFallbackVote === slot) {
|
|
874
1089
|
this.log.trace(`Already attempted to vote in slot ${slot} (skipping)`);
|
|
@@ -891,7 +1106,7 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
891
1106
|
maxAllowedTime
|
|
892
1107
|
});
|
|
893
1108
|
// Check if we're a proposer or proposal is open
|
|
894
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
1109
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
895
1110
|
if (!canPropose) {
|
|
896
1111
|
this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, {
|
|
897
1112
|
slot,
|
|
@@ -907,8 +1122,8 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
907
1122
|
attestorAddress,
|
|
908
1123
|
slot
|
|
909
1124
|
});
|
|
910
|
-
// Enqueue governance and slashing votes
|
|
911
|
-
const voter = new CheckpointVoter(
|
|
1125
|
+
// Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
|
|
1126
|
+
const voter = new CheckpointVoter(targetSlot, publisher, attestorAddress, this.validatorClient, this.slasherClient, this.l1Constants, this.config, this.metrics, this.log);
|
|
912
1127
|
const votesPromises = voter.enqueueVotes();
|
|
913
1128
|
const votes = await Promise.all(votesPromises);
|
|
914
1129
|
if (votes.every((p)=>!p)) {
|
|
@@ -918,13 +1133,21 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
918
1133
|
this.log.info(`Voting in slot ${slot} despite sync failure`, {
|
|
919
1134
|
slot
|
|
920
1135
|
});
|
|
921
|
-
|
|
1136
|
+
// Votes are EIP-712-signed for `targetSlot` (the pipelined slot in which the multicall is
|
|
1137
|
+
// expected to mine). Delay submission to the start of `targetSlot` so the tx mines in the
|
|
1138
|
+
// slot the votes were signed for. We fire-and-forget so we don't block the sequencer's
|
|
1139
|
+
// work loop while waiting for the target slot to start.
|
|
1140
|
+
void publisher.sendRequestsAt(targetSlot).catch((err)=>{
|
|
1141
|
+
this.log.error(`Failed to publish votes despite sync failure for slot ${slot}`, err, {
|
|
1142
|
+
slot
|
|
1143
|
+
});
|
|
1144
|
+
});
|
|
922
1145
|
}
|
|
923
1146
|
/**
|
|
924
1147
|
* Tries to vote on slashing actions and governance proposals when escape hatch is open.
|
|
925
1148
|
* This allows the sequencer to participate in voting without performing checkpoint proposal work.
|
|
926
1149
|
*/ async tryVoteWhenEscapeHatchOpen(args) {
|
|
927
|
-
const { slot, proposer } = args;
|
|
1150
|
+
const { slot, targetSlot, proposer } = args;
|
|
928
1151
|
// Prevent duplicate attempts in the same slot
|
|
929
1152
|
if (this.lastSlotForFallbackVote === slot) {
|
|
930
1153
|
this.log.trace(`Already attempted to vote in slot ${slot} (escape hatch open, skipping)`);
|
|
@@ -935,9 +1158,14 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
935
1158
|
const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
|
|
936
1159
|
this.log.debug(`Escape hatch open for slot ${slot}, attempting vote-only actions`, {
|
|
937
1160
|
slot,
|
|
1161
|
+
targetSlot,
|
|
938
1162
|
attestorAddress
|
|
939
1163
|
});
|
|
940
|
-
|
|
1164
|
+
// Under proposer pipelining, the multicall is expected to mine in `targetSlot` (slot + 1).
|
|
1165
|
+
// Governance and slashing votes are EIP-712-signed against the slot they will mine in, and the
|
|
1166
|
+
// L1 contract checks `msg.sender == getCurrentProposer()` using the mining slot. So we must
|
|
1167
|
+
// sign for `targetSlot` and delay submission to the start of `targetSlot`.
|
|
1168
|
+
const voter = new CheckpointVoter(targetSlot, publisher, attestorAddress, this.validatorClient, this.slasherClient, this.l1Constants, this.config, this.metrics, this.log);
|
|
941
1169
|
const votesPromises = voter.enqueueVotes();
|
|
942
1170
|
const votes = await Promise.all(votesPromises);
|
|
943
1171
|
if (votes.every((p)=>!p)) {
|
|
@@ -945,9 +1173,20 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
|
|
|
945
1173
|
return;
|
|
946
1174
|
}
|
|
947
1175
|
this.log.info(`Voting in slot ${slot} (escape hatch open)`, {
|
|
948
|
-
slot
|
|
1176
|
+
slot,
|
|
1177
|
+
targetSlot
|
|
1178
|
+
});
|
|
1179
|
+
// Votes are EIP-712-signed for `targetSlot`. Delay submission to the start of `targetSlot` so
|
|
1180
|
+
// the multicall mines in the slot the votes were signed for; otherwise the L1 contract reads
|
|
1181
|
+
// `signaler = getCurrentProposer()` against the wrong slot and signature verification fails
|
|
1182
|
+
// silently inside Multicall3. Fire-and-forget so we don't block the sequencer's work loop while
|
|
1183
|
+
// waiting for the target slot to start, mirroring tryVoteWhenSyncFails.
|
|
1184
|
+
void publisher.sendRequestsAt(targetSlot).catch((err)=>{
|
|
1185
|
+
this.log.error(`Failed to publish escape-hatch votes for slot ${slot}`, err, {
|
|
1186
|
+
slot,
|
|
1187
|
+
targetSlot
|
|
1188
|
+
});
|
|
949
1189
|
});
|
|
950
|
-
await publisher.sendRequests();
|
|
951
1190
|
}
|
|
952
1191
|
/**
|
|
953
1192
|
* Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
|