@aztec/sequencer-client 0.0.1-commit.f5d02921e → 0.0.1-commit.f7ea82942
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/dest/client/sequencer-client.d.ts +3 -1
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +3 -4
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +8 -11
- 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 -9
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +3 -41
- 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/publisher/config.d.ts +1 -1
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +2 -2
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +3 -4
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.d.ts +1 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +0 -1
- package/dest/publisher/sequencer-publisher.d.ts +16 -35
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +60 -92
- package/dest/sequencer/chain_state_overrides.d.ts +25 -0
- package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
- package/dest/sequencer/chain_state_overrides.js +39 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts +27 -22
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +294 -155
- 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 +6 -1
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +7 -10
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +27 -20
- package/dest/sequencer/sequencer.d.ts +5 -2
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +31 -23
- package/dest/sequencer/timetable.d.ts +14 -1
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +45 -36
- package/package.json +27 -27
- package/src/client/sequencer-client.ts +5 -7
- package/src/config.ts +8 -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 +7 -54
- package/src/global_variable_builder/index.ts +2 -0
- package/src/publisher/config.ts +6 -4
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
- package/src/publisher/sequencer-publisher-factory.ts +0 -3
- package/src/publisher/sequencer-publisher.ts +106 -152
- package/src/sequencer/README.md +83 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +365 -205
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/events.ts +5 -0
- package/src/sequencer/metrics.ts +35 -25
- package/src/sequencer/sequencer.ts +44 -26
- package/src/sequencer/timetable.ts +57 -45
|
@@ -436,13 +436,12 @@ function _apply_decs_2203_r(targetClass, memberDecs, classDecs, parentClass) {
|
|
|
436
436
|
return (_apply_decs_2203_r = applyDecs2203RFactory())(targetClass, memberDecs, classDecs, parentClass);
|
|
437
437
|
}
|
|
438
438
|
var _dec, _dec1, _dec2, _dec3, _dec4, _dec5, _dec6, _dec7, _initProto;
|
|
439
|
-
import { RollupContract } from '@aztec/ethereum/contracts';
|
|
440
439
|
import { BlockNumber, CheckpointNumber, IndexWithinCheckpoint } from '@aztec/foundation/branded-types';
|
|
441
440
|
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
442
441
|
import { flipSignature, generateRecoverableSignature, generateUnrecoverableSignature } from '@aztec/foundation/crypto/secp256k1-signer';
|
|
443
|
-
import { Signature } from '@aztec/foundation/eth-signature';
|
|
444
442
|
import { filter } from '@aztec/foundation/iterator';
|
|
445
443
|
import { createLogger } from '@aztec/foundation/log';
|
|
444
|
+
import { retryUntil } from '@aztec/foundation/retry';
|
|
446
445
|
import { sleep, sleepUntil } from '@aztec/foundation/sleep';
|
|
447
446
|
import { Timer } from '@aztec/foundation/timer';
|
|
448
447
|
import { isErrorClass, unfreeze } from '@aztec/foundation/types';
|
|
@@ -456,6 +455,7 @@ import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
|
456
455
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
457
456
|
import { Attributes, trackSpan } from '@aztec/telemetry-client';
|
|
458
457
|
import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
|
|
458
|
+
import { buildPipelinedParentSimulationOverridesPlan, buildSubmissionSimulationOverridesPlan } from './chain_state_overrides.js';
|
|
459
459
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
460
460
|
import { SequencerInterruptedError } from './errors.js';
|
|
461
461
|
import { SequencerState } from './utils.js';
|
|
@@ -497,6 +497,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
497
497
|
epochCache;
|
|
498
498
|
dateProvider;
|
|
499
499
|
metrics;
|
|
500
|
+
checkpointMetrics;
|
|
500
501
|
eventEmitter;
|
|
501
502
|
setStateFn;
|
|
502
503
|
tracer;
|
|
@@ -547,9 +548,9 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
547
548
|
}
|
|
548
549
|
log;
|
|
549
550
|
/** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */ pendingL1Submission;
|
|
550
|
-
/**
|
|
551
|
+
/** Pipelined parent chain state used while building and later submitting this checkpoint. */ pipelinedParentSimulationOverridesPlan;
|
|
551
552
|
constructor(slotNow, targetSlot, targetEpoch, checkpointNumber, syncedToBlockNumber, // TODO(palla/mbps): Can we remove the proposer in favor of attestorAddress? Need to check fisherman-node flows.
|
|
552
|
-
proposer, publisher, attestorAddress, invalidateCheckpoint, validatorClient, globalsBuilder, p2pClient, worldState, l1ToL2MessageSource, l2BlockSource, checkpointsBuilder, blockSink, l1Constants, config, timetable, slasherClient, epochCache, dateProvider, metrics, eventEmitter, setStateFn, tracer, bindings, proposedCheckpointData){
|
|
553
|
+
proposer, publisher, attestorAddress, invalidateCheckpoint, validatorClient, globalsBuilder, p2pClient, worldState, l1ToL2MessageSource, l2BlockSource, checkpointsBuilder, blockSink, l1Constants, config, timetable, slasherClient, epochCache, dateProvider, metrics, checkpointMetrics, eventEmitter, setStateFn, tracer, bindings, proposedCheckpointData){
|
|
553
554
|
this.slotNow = slotNow;
|
|
554
555
|
this.targetSlot = targetSlot;
|
|
555
556
|
this.targetEpoch = targetEpoch;
|
|
@@ -574,6 +575,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
574
575
|
this.epochCache = epochCache;
|
|
575
576
|
this.dateProvider = dateProvider;
|
|
576
577
|
this.metrics = metrics;
|
|
578
|
+
this.checkpointMetrics = checkpointMetrics;
|
|
577
579
|
this.eventEmitter = eventEmitter;
|
|
578
580
|
this.setStateFn = setStateFn;
|
|
579
581
|
this.tracer = tracer;
|
|
@@ -590,74 +592,102 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
590
592
|
}
|
|
591
593
|
/**
|
|
592
594
|
* Executes the checkpoint proposal job.
|
|
593
|
-
* Builds blocks,
|
|
594
|
-
*
|
|
595
|
+
* Builds blocks, assembles checkpoint, and broadcasts the proposal (blocking).
|
|
596
|
+
* Attestation collection, signing, and L1 submission are backgrounded so the
|
|
597
|
+
* work loop can return to IDLE immediately for consecutive slot proposals.
|
|
595
598
|
* Returns the built checkpoint if successful, undefined otherwise.
|
|
596
599
|
*/ async execute() {
|
|
597
600
|
// Enqueue governance and slashing votes (returns promises that will be awaited later)
|
|
598
601
|
// In fisherman mode, we simulate slashing but don't actually publish to L1
|
|
599
602
|
// These are constant for the whole slot, so we only enqueue them once
|
|
600
603
|
const votesPromises = new CheckpointVoter(this.targetSlot, this.publisher, this.attestorAddress, this.validatorClient, this.slasherClient, this.l1Constants, this.config, this.metrics, this.log).enqueueVotes();
|
|
601
|
-
// Build
|
|
602
|
-
//
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
604
|
+
// Build blocks, assemble checkpoint, and broadcast proposal (BLOCKING).
|
|
605
|
+
// Returns after broadcast — attestation collection is deferred.
|
|
606
|
+
const broadcast = await this.proposeCheckpoint();
|
|
607
|
+
if (!broadcast) {
|
|
608
|
+
await Promise.all(votesPromises);
|
|
609
|
+
// Still submit votes even without a checkpoint
|
|
610
|
+
if (!this.config.fishermanMode) {
|
|
611
|
+
this.pendingL1Submission = this.publisher.sendRequestsAt(this.dateProvider.nowAsDate()).then(()=>{});
|
|
612
|
+
}
|
|
613
|
+
return undefined;
|
|
609
614
|
}
|
|
615
|
+
const { checkpoint } = broadcast;
|
|
616
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
610
617
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
611
618
|
if (this.config.fishermanMode) {
|
|
612
619
|
await this.handleCheckpointEndAsFisherman(checkpoint);
|
|
613
|
-
return;
|
|
620
|
+
return checkpoint;
|
|
614
621
|
}
|
|
615
|
-
//
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
+
// Background the attestation → signing → L1 pipeline so the work loop is unblocked
|
|
623
|
+
this.pendingL1Submission = this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises);
|
|
624
|
+
// Return the built checkpoint immediately — the work loop is now unblocked
|
|
625
|
+
return checkpoint;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Background pipeline: collects attestations, signs them, enqueues the checkpoint, and submits to L1.
|
|
629
|
+
* Runs as a fire-and-forget task stored in `pendingL1Submission` so the work loop is unblocked.
|
|
630
|
+
*/ async waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises) {
|
|
631
|
+
const { checkpoint } = broadcast;
|
|
632
|
+
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
633
|
+
try {
|
|
634
|
+
// Wait for all votes actions, enqueued at the beginning, to resolve
|
|
635
|
+
await Promise.all(votesPromises);
|
|
636
|
+
// Try to collect attestations from the committee
|
|
637
|
+
const signedAttestations = await this.getSignedCommitteeAttestations(broadcast);
|
|
638
|
+
// If pipelining, wait for the previous checkpoint to land on L1 before submitting,
|
|
639
|
+
// so we can check it matches the proposed checkpoint we used as parent, and has valid attestations.
|
|
640
|
+
if (signedAttestations && (!isPipelining || await this.waitForValidParentCheckpointOnL1())) {
|
|
641
|
+
await this.enqueueCheckpointForSubmission({
|
|
642
|
+
checkpoint,
|
|
643
|
+
...signedAttestations
|
|
644
|
+
});
|
|
622
645
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
646
|
+
// If we failed to collect attestations, at least check if we need to issue an invalidation
|
|
647
|
+
// Note that if we are not pipelining, we enqueued the invalidation at the beginning
|
|
648
|
+
if (!signedAttestations && isPipelining && await this.waitForSyncedL2SlotNumber(this.slotNow)) {
|
|
649
|
+
const validationStatus = await this.l2BlockSource.getPendingChainValidationStatus();
|
|
650
|
+
if (!validationStatus.valid) {
|
|
651
|
+
this.log.warn(`Checkpoint ${validationStatus.checkpoint.checkpointNumber} has invalid attestations, enqueuing invalidation in spite of attestation collection failure`, {
|
|
652
|
+
checkpoint: validationStatus.checkpoint,
|
|
653
|
+
reason: validationStatus.reason
|
|
654
|
+
});
|
|
655
|
+
await this.enqueueInvalidation(validationStatus);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Send whatever was enqueued: votes + (propose | invalidation | nothing).
|
|
659
|
+
// Compute the earliest time to submit: pipeline slot start when pipelining, now otherwise.
|
|
660
|
+
const submitAfter = isPipelining ? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000) : new Date(this.dateProvider.now());
|
|
661
|
+
const l1Response = await this.publisher.sendRequestsAt(submitAfter);
|
|
631
662
|
const proposedAction = l1Response?.successfulActions.find((a)=>a === 'propose');
|
|
632
663
|
if (proposedAction) {
|
|
633
664
|
this.eventEmitter.emit('checkpoint-published', {
|
|
634
665
|
checkpoint: this.checkpointNumber,
|
|
635
666
|
slot: this.targetSlot
|
|
636
667
|
});
|
|
637
|
-
const coinbase = checkpoint
|
|
668
|
+
const coinbase = checkpoint.header.coinbase;
|
|
638
669
|
await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
|
|
639
|
-
} else
|
|
670
|
+
} else {
|
|
640
671
|
this.eventEmitter.emit('checkpoint-publish-failed', {
|
|
641
672
|
...l1Response,
|
|
642
673
|
slot: this.targetSlot
|
|
643
674
|
});
|
|
644
|
-
if (
|
|
675
|
+
if (isPipelining) {
|
|
645
676
|
this.metrics.recordPipelineDiscard();
|
|
646
677
|
}
|
|
647
678
|
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
this.eventEmitter.emit('checkpoint-publish-failed', {
|
|
652
|
-
slot: this.targetSlot
|
|
653
|
-
});
|
|
654
|
-
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
655
|
-
this.metrics.recordPipelineDiscard();
|
|
656
|
-
}
|
|
679
|
+
} catch (err) {
|
|
680
|
+
if (err instanceof SequencerInterruptedError) {
|
|
681
|
+
return;
|
|
657
682
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
683
|
+
this.log.error(`Background attestation/L1 pipeline failed for slot ${this.targetSlot}`, err);
|
|
684
|
+
this.eventEmitter.emit('checkpoint-publish-failed', {
|
|
685
|
+
slot: this.targetSlot
|
|
686
|
+
});
|
|
687
|
+
if (isPipelining) {
|
|
688
|
+
this.metrics.recordPipelineDiscard();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
661
691
|
}
|
|
662
692
|
/** Enqueues the checkpoint for L1 submission. Called after pipeline sleep in execute(). */ async enqueueCheckpointForSubmission(result) {
|
|
663
693
|
const { checkpoint, attestations, attestationsSignature } = result;
|
|
@@ -673,12 +703,135 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
673
703
|
return;
|
|
674
704
|
}
|
|
675
705
|
}
|
|
706
|
+
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
707
|
+
const submissionSimulationOverridesPlan = buildSubmissionSimulationOverridesPlan({
|
|
708
|
+
pipelinedParentPlan: this.pipelinedParentSimulationOverridesPlan,
|
|
709
|
+
invalidateToPendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
710
|
+
lastArchiveRoot: checkpoint.header.lastArchiveRoot,
|
|
711
|
+
pipeliningEnabled: isPipelining
|
|
712
|
+
});
|
|
676
713
|
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
677
714
|
txTimeoutAt,
|
|
678
|
-
|
|
679
|
-
|
|
715
|
+
...submissionSimulationOverridesPlan ? {
|
|
716
|
+
simulationOverridesPlan: submissionSimulationOverridesPlan
|
|
717
|
+
} : {}
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Wait until the archiver syncs past the given L2 slot number.
|
|
722
|
+
* The deadline is the end of `this.targetSlot`, beyond which any pipelined work would miss its
|
|
723
|
+
* L1 submission window and is no longer useful.
|
|
724
|
+
*/ async waitForSyncedL2SlotNumber(waitForSlot) {
|
|
725
|
+
const targetSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
|
|
726
|
+
const targetSlotEndMs = (targetSlotStart + this.l1Constants.slotDuration) * 1000;
|
|
727
|
+
const syncDelayTolerance = this.l1Constants.ethereumSlotDuration * 2 * 1000;
|
|
728
|
+
const timeoutSeconds = Math.max(0.1, (targetSlotEndMs + syncDelayTolerance - this.dateProvider.now()) / 1000);
|
|
729
|
+
try {
|
|
730
|
+
return await retryUntil(async ()=>{
|
|
731
|
+
const syncedSlot = await this.l2BlockSource.getSyncedL2SlotNumber();
|
|
732
|
+
return syncedSlot !== undefined && syncedSlot >= waitForSlot;
|
|
733
|
+
}, `archiver sync past slot ${waitForSlot}`, timeoutSeconds, 0.2);
|
|
734
|
+
} catch {
|
|
735
|
+
this.log.warn(`Archiver did not sync L1 past slot ${waitForSlot} before slot ${this.targetSlot} expired, discarding pipelined work`, {
|
|
736
|
+
checkpointNumber: this.checkpointNumber
|
|
737
|
+
});
|
|
738
|
+
this.emitPipelinedCheckpointDiscarded('archiver-sync-timeout');
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Waits for the parent checkpoint to land on L1 before submitting a pipelined checkpoint.
|
|
744
|
+
* Polls until the archiver has synced L1 past the parent's slot, then verifies:
|
|
745
|
+
* - If we built on a proposed parent: it must have landed on L1 with matching hash and valid attestations.
|
|
746
|
+
* - If we built without a proposed parent: no new checkpoint must have appeared for that slot.
|
|
747
|
+
* If the parent has invalid attestations, enqueues an invalidation. Returns whether to proceed with the proposal.
|
|
748
|
+
*/ async waitForValidParentCheckpointOnL1() {
|
|
749
|
+
const parentCheckpointNumber = CheckpointNumber(this.checkpointNumber - 1);
|
|
750
|
+
// Wait until archiver has synced L1 past the parent's slot (slotNow)
|
|
751
|
+
if (!await this.waitForSyncedL2SlotNumber(this.slotNow)) {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
const tips = await this.l2BlockSource.getL2Tips();
|
|
755
|
+
const checkpointedNumber = tips.checkpointed.checkpoint.number;
|
|
756
|
+
// We built on top of a proposed checkpoint. Verify it landed on L1 as expected.
|
|
757
|
+
if (this.proposedCheckpointData) {
|
|
758
|
+
// After syncing from L1 we see the chain tip has invalid attestations. This means the parent checkpoint was posted
|
|
759
|
+
// with invalid attestations, or it built on top of something with invalid attestations and didnt invalidate them.
|
|
760
|
+
// Either way, we thought our parent would be valid, so we have to throw away our work. But at least we'll try and
|
|
761
|
+
// invalidate on L1 so we clean up the chain for the next proposer. And we'll slash them, but that's handled elsewhere.
|
|
762
|
+
const validationStatus = await this.l2BlockSource.getPendingChainValidationStatus();
|
|
763
|
+
if (!validationStatus.valid) {
|
|
764
|
+
this.log.warn(`Parent checkpoint ${parentCheckpointNumber} has invalid attestations, discarding pipelined work`, {
|
|
765
|
+
checkpointNumber: this.checkpointNumber,
|
|
766
|
+
reason: validationStatus.reason
|
|
767
|
+
});
|
|
768
|
+
this.emitPipelinedCheckpointDiscarded('parent-invalid-attestations');
|
|
769
|
+
await this.enqueueInvalidation(validationStatus);
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
// The pending chain is valid. But did the parent checkpoint land on L1 at all?
|
|
773
|
+
if (checkpointedNumber < parentCheckpointNumber) {
|
|
774
|
+
this.log.warn(`Parent checkpoint ${parentCheckpointNumber} did not land on L1, discarding pipelined work`, {
|
|
775
|
+
checkpointNumber: this.checkpointNumber,
|
|
776
|
+
checkpointedNumber
|
|
777
|
+
});
|
|
778
|
+
this.emitPipelinedCheckpointDiscarded('parent-not-on-l1');
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
// It landed. But is it the one we were expecting?
|
|
782
|
+
const expectedHash = this.proposedCheckpointData.header.hash().toString();
|
|
783
|
+
if (tips.checkpointed.checkpoint.hash !== expectedHash) {
|
|
784
|
+
this.log.warn(`Parent checkpoint ${parentCheckpointNumber} hash mismatch on L1, discarding pipelined work`, {
|
|
785
|
+
checkpointNumber: this.checkpointNumber,
|
|
786
|
+
expectedHash,
|
|
787
|
+
actualHash: tips.checkpointed.checkpoint.hash
|
|
788
|
+
});
|
|
789
|
+
this.emitPipelinedCheckpointDiscarded('parent-hash-mismatch');
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
return true;
|
|
793
|
+
} else {
|
|
794
|
+
// We didn't see a proposed checkpoint at build time, so we built on checkpointed parent from two slots ago.
|
|
795
|
+
// But if a new checkpoint for the previous slot appeared on L1 in the meantime, our checkpoint assumed the wrong parent,
|
|
796
|
+
// so we have to discard our work. This can happen if we're somehow cut off from p2p and fail to see the checkpoint
|
|
797
|
+
// proposal for the previous slot.
|
|
798
|
+
if (checkpointedNumber > parentCheckpointNumber) {
|
|
799
|
+
this.log.warn(`Unexpected checkpoint ${checkpointedNumber} landed on L1 after we built on top of parent ${parentCheckpointNumber}, discarding pipelined work`, {
|
|
800
|
+
checkpointNumber: this.checkpointNumber,
|
|
801
|
+
checkpointedNumber
|
|
802
|
+
});
|
|
803
|
+
this.emitPipelinedCheckpointDiscarded('unexpected-parent-appeared');
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/** Emits the pipelined-checkpoint-discarded event and records the metric. */ emitPipelinedCheckpointDiscarded(reason) {
|
|
810
|
+
this.metrics.recordPipelineParentCheckpointMismatch(reason);
|
|
811
|
+
this.eventEmitter.emit('pipelined-checkpoint-discarded', {
|
|
812
|
+
slot: this.targetSlot,
|
|
813
|
+
checkpointNumber: this.checkpointNumber,
|
|
814
|
+
reason
|
|
680
815
|
});
|
|
681
816
|
}
|
|
817
|
+
/** Simulates and enqueues an invalidation request for the invalid parent checkpoint. */ async enqueueInvalidation(validationStatus) {
|
|
818
|
+
if (this.config.skipInvalidateBlockAsProposer) {
|
|
819
|
+
this.log.warn(`Skipping checkpoint invalidation as proposer due to test configuration`);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const invalidateRequest = await this.publisher.simulateInvalidateCheckpoint(validationStatus);
|
|
823
|
+
if (invalidateRequest) {
|
|
824
|
+
const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
|
|
825
|
+
const txTimeoutAt = new Date((submissionSlotStart + this.l1Constants.slotDuration) * 1000);
|
|
826
|
+
this.publisher.enqueueInvalidateCheckpoint(invalidateRequest, {
|
|
827
|
+
txTimeoutAt
|
|
828
|
+
});
|
|
829
|
+
} else {
|
|
830
|
+
this.log.info(`Invalidation simulation returned undefined, checkpoint may have been removed already`, {
|
|
831
|
+
checkpointNumber: this.checkpointNumber
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
682
835
|
async proposeCheckpoint() {
|
|
683
836
|
try {
|
|
684
837
|
const env = {
|
|
@@ -687,6 +840,15 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
687
840
|
hasError: false
|
|
688
841
|
};
|
|
689
842
|
try {
|
|
843
|
+
const now = this.dateProvider.now();
|
|
844
|
+
if (this.epochCache.isProposerPipeliningEnabled() && this.proposedCheckpointData) {
|
|
845
|
+
// Measure against the wall-clock slot whose build window we are currently using.
|
|
846
|
+
// In pipelining mode `targetSlot` is intentionally one slot ahead, which makes the
|
|
847
|
+
// target-slot boundary a full slot away from the actual build start time.
|
|
848
|
+
const slotBoundaryMs = Number(getTimestampForSlot(this.slotNow, this.l1Constants)) * 1000;
|
|
849
|
+
this.checkpointMetrics.recordPipelinedCheckpointBuildStartOffsetFromSlotBoundary(now - slotBoundaryMs);
|
|
850
|
+
}
|
|
851
|
+
this.checkpointMetrics.startCheckpointTiming(now);
|
|
690
852
|
// Get operator configured coinbase and fee recipient for this attestor
|
|
691
853
|
const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
|
|
692
854
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
|
|
@@ -708,15 +870,13 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
708
870
|
// When pipelining, force the proposed checkpoint number and fee header to our parent so the
|
|
709
871
|
// fee computation sees the same chain tip that L1 will see once the previous pipelined checkpoint lands.
|
|
710
872
|
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
forceProposedFeeHeader: this.computedForceProposedFeeHeader
|
|
719
|
-
});
|
|
873
|
+
this.pipelinedParentSimulationOverridesPlan = isPipelining ? await buildPipelinedParentSimulationOverridesPlan({
|
|
874
|
+
checkpointNumber: this.checkpointNumber,
|
|
875
|
+
proposedCheckpointData: this.proposedCheckpointData,
|
|
876
|
+
rollup: this.publisher.rollupContract,
|
|
877
|
+
log: this.log
|
|
878
|
+
}) : undefined;
|
|
879
|
+
const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(coinbase, feeRecipient, this.targetSlot, this.pipelinedParentSimulationOverridesPlan);
|
|
720
880
|
// Collect L1 to L2 messages for the checkpoint and compute their hash
|
|
721
881
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber);
|
|
722
882
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
@@ -794,8 +954,8 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
794
954
|
return undefined;
|
|
795
955
|
}
|
|
796
956
|
// Record checkpoint-level build metrics
|
|
797
|
-
this.
|
|
798
|
-
//
|
|
957
|
+
this.checkpointMetrics.recordCheckpointBuild(checkpointBuildTimer.ms(), blocksInCheckpoint.length, checkpoint.getStats().txCount, Number(checkpoint.header.totalManaUsed.toBigInt()));
|
|
958
|
+
// In fisherman mode, return the checkpoint without broadcasting or collecting attestations
|
|
799
959
|
if (this.config.fishermanMode) {
|
|
800
960
|
this.log.info(`Built checkpoint for slot ${this.targetSlot} with ${blocksInCheckpoint.length} blocks. ` + `Skipping proposal in fisherman mode.`, {
|
|
801
961
|
slot: this.targetSlot,
|
|
@@ -803,44 +963,23 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
803
963
|
blocksBuilt: blocksInCheckpoint.length
|
|
804
964
|
});
|
|
805
965
|
this.metrics.recordCheckpointSuccess();
|
|
966
|
+
// Return a broadcast result with a dummy proposal — fisherman mode skips attestation collection
|
|
806
967
|
return {
|
|
807
968
|
checkpoint,
|
|
808
|
-
|
|
809
|
-
|
|
969
|
+
proposal: undefined,
|
|
970
|
+
blockProposedAt: this.dateProvider.now()
|
|
810
971
|
};
|
|
811
972
|
}
|
|
812
|
-
// Include the block pending broadcast in the checkpoint proposal if any
|
|
813
|
-
const lastBlock = blockPendingBroadcast && {
|
|
814
|
-
blockHeader: blockPendingBroadcast.block.header,
|
|
815
|
-
indexWithinCheckpoint: blockPendingBroadcast.block.indexWithinCheckpoint,
|
|
816
|
-
txs: blockPendingBroadcast.txs
|
|
817
|
-
};
|
|
818
973
|
// Create the checkpoint proposal and broadcast it
|
|
819
|
-
const proposal = await this.validatorClient.createCheckpointProposal(checkpoint.header, checkpoint.archive.root, feeAssetPriceModifier,
|
|
974
|
+
const proposal = await this.validatorClient.createCheckpointProposal(checkpoint.header, checkpoint.archive.root, this.checkpointNumber, feeAssetPriceModifier, blockPendingBroadcast, this.proposer, checkpointProposalOptions);
|
|
820
975
|
const blockProposedAt = this.dateProvider.now();
|
|
821
976
|
await this.p2pClient.broadcastCheckpointProposal(proposal);
|
|
822
|
-
this.
|
|
823
|
-
|
|
824
|
-
const blockAttestedAt = this.dateProvider.now();
|
|
825
|
-
this.metrics.recordCheckpointAttestationDelay(blockAttestedAt - blockProposedAt);
|
|
826
|
-
// Proposer must sign over the attestations before pushing them to L1
|
|
827
|
-
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
828
|
-
let attestationsSignature;
|
|
829
|
-
try {
|
|
830
|
-
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer, this.targetSlot, this.checkpointNumber);
|
|
831
|
-
} catch (err) {
|
|
832
|
-
// We shouldn't really get here since we yield to another HA node
|
|
833
|
-
// as soon as we see these errors when creating block or checkpoint proposals.
|
|
834
|
-
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
835
|
-
return undefined;
|
|
836
|
-
}
|
|
837
|
-
throw err;
|
|
838
|
-
}
|
|
839
|
-
// Return the result for the caller to enqueue after the pipeline sleep
|
|
977
|
+
this.checkpointMetrics.noteCheckpointBroadcast(this.dateProvider.now());
|
|
978
|
+
// Return immediately after broadcast — attestation collection happens in the background
|
|
840
979
|
return {
|
|
841
980
|
checkpoint,
|
|
842
|
-
|
|
843
|
-
|
|
981
|
+
proposal,
|
|
982
|
+
blockProposedAt
|
|
844
983
|
};
|
|
845
984
|
} catch (e) {
|
|
846
985
|
env.error = e;
|
|
@@ -891,17 +1030,19 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
891
1030
|
indexWithinCheckpoint,
|
|
892
1031
|
txHashesAlreadyIncluded
|
|
893
1032
|
});
|
|
894
|
-
//
|
|
895
|
-
if (
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1033
|
+
// If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
|
|
1034
|
+
if ('failure' in buildResult) {
|
|
1035
|
+
// If this was the last subslot, or we're running with a single block per slot, we're done
|
|
1036
|
+
if (timingInfo.isLastBlock || timingInfo.deadline === undefined) {
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
// Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
|
|
899
1040
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
900
1041
|
continue;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1042
|
+
}
|
|
1043
|
+
// If there was an error building the block, we just exit the loop and give up the rest of the slot.
|
|
1044
|
+
// We don't want to risk building more blocks if something went wrong.
|
|
1045
|
+
if ('error' in buildResult) {
|
|
905
1046
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
906
1047
|
this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
|
|
907
1048
|
slot: this.targetSlot,
|
|
@@ -912,32 +1053,29 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
912
1053
|
break;
|
|
913
1054
|
}
|
|
914
1055
|
const { block, usedTxs } = buildResult;
|
|
1056
|
+
this.checkpointMetrics.noteCheckpointBlockBuilt(this.dateProvider.now(), {
|
|
1057
|
+
isFirstBlock: blocksBuilt === 0,
|
|
1058
|
+
isLastBlock: timingInfo.isLastBlock
|
|
1059
|
+
});
|
|
915
1060
|
blocksInCheckpoint.push(block);
|
|
916
1061
|
usedTxs.forEach((tx)=>txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
917
|
-
//
|
|
918
|
-
|
|
1062
|
+
// Sign the block proposal. This will throw if HA signing fails.
|
|
1063
|
+
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
1064
|
+
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal,
|
|
1065
|
+
// so we avoid polluting our archive with a block that would fail.
|
|
1066
|
+
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
|
|
1067
|
+
// If this throws, we abort the entire checkpoint.
|
|
1068
|
+
await this.syncProposedBlockToArchiver(block);
|
|
1069
|
+
// If this is the last block, do not broadcast it, since it will be included in the checkpoint proposal.
|
|
919
1070
|
if (timingInfo.isLastBlock) {
|
|
920
|
-
await this.syncProposedBlockToArchiver(block);
|
|
921
1071
|
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
|
|
922
1072
|
slot: this.targetSlot,
|
|
923
1073
|
blockNumber,
|
|
924
1074
|
blocksBuilt
|
|
925
1075
|
});
|
|
926
|
-
blockPendingBroadcast =
|
|
927
|
-
block,
|
|
928
|
-
txs: usedTxs
|
|
929
|
-
};
|
|
1076
|
+
blockPendingBroadcast = proposal;
|
|
930
1077
|
break;
|
|
931
1078
|
}
|
|
932
|
-
// Broadcast the block proposal (unless we're in fisherman mode) unless the block is the last one,
|
|
933
|
-
// in which case we'll broadcast it along with the checkpoint at the end of the loop.
|
|
934
|
-
// Note that we only send the block to the archiver if we manage to create the proposal, so if there's
|
|
935
|
-
// a HA error we don't pollute our archiver with a block that won't make it to the chain.
|
|
936
|
-
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
937
|
-
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal.
|
|
938
|
-
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
|
|
939
|
-
// If this throws, we abort the entire checkpoint.
|
|
940
|
-
await this.syncProposedBlockToArchiver(block);
|
|
941
1079
|
// Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
|
|
942
1080
|
proposal && await this.p2pClient.broadcastProposal(proposal);
|
|
943
1081
|
// Wait until the next block's start time
|
|
@@ -957,7 +1095,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
957
1095
|
this.log.info(`Skipping block proposal for block ${block.number} in fisherman mode`);
|
|
958
1096
|
return Promise.resolve(undefined);
|
|
959
1097
|
}
|
|
960
|
-
return this.validatorClient.createBlockProposal(block.header, block.indexWithinCheckpoint, inHash, block.archive.root, usedTxs, this.proposer, blockProposalOptions);
|
|
1098
|
+
return this.validatorClient.createBlockProposal(block.header, this.checkpointNumber, block.indexWithinCheckpoint, inHash, block.archive.root, usedTxs, this.proposer, blockProposalOptions);
|
|
961
1099
|
}
|
|
962
1100
|
/** Sleeps until it is time to produce the next block in the slot */ async waitUntilNextSubslot(nextSubslotStart) {
|
|
963
1101
|
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot);
|
|
@@ -987,7 +1125,9 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
987
1125
|
slot: this.targetSlot
|
|
988
1126
|
});
|
|
989
1127
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
990
|
-
return
|
|
1128
|
+
return {
|
|
1129
|
+
failure: 'insufficient-txs'
|
|
1130
|
+
};
|
|
991
1131
|
}
|
|
992
1132
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
993
1133
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
@@ -1030,7 +1170,9 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1030
1170
|
slot: this.targetSlot
|
|
1031
1171
|
});
|
|
1032
1172
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
1033
|
-
return
|
|
1173
|
+
return {
|
|
1174
|
+
failure: 'insufficient-valid-txs'
|
|
1175
|
+
};
|
|
1034
1176
|
}
|
|
1035
1177
|
// Block creation succeeded, emit stats and metrics
|
|
1036
1178
|
const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
|
|
@@ -1056,7 +1198,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1056
1198
|
slot: this.targetSlot,
|
|
1057
1199
|
buildSlot: this.slotNow
|
|
1058
1200
|
});
|
|
1059
|
-
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
1201
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe(), this.targetSlot);
|
|
1060
1202
|
return {
|
|
1061
1203
|
block,
|
|
1062
1204
|
usedTxs
|
|
@@ -1131,6 +1273,30 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1131
1273
|
minTxs
|
|
1132
1274
|
};
|
|
1133
1275
|
}
|
|
1276
|
+
async getSignedCommitteeAttestations(broadcast) {
|
|
1277
|
+
const { proposal, blockProposedAt } = broadcast;
|
|
1278
|
+
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
|
|
1279
|
+
const attestations = await this.waitForAttestations(proposal);
|
|
1280
|
+
if (!attestations) {
|
|
1281
|
+
return undefined;
|
|
1282
|
+
}
|
|
1283
|
+
this.checkpointMetrics.recordCheckpointAttestationDelay(this.dateProvider.now() - blockProposedAt);
|
|
1284
|
+
// Proposer must sign over the attestations before pushing them to L1
|
|
1285
|
+
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
1286
|
+
try {
|
|
1287
|
+
const attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer, this.targetSlot, this.checkpointNumber);
|
|
1288
|
+
return {
|
|
1289
|
+
attestations,
|
|
1290
|
+
attestationsSignature
|
|
1291
|
+
};
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
this.log.error(`Error signing attestations for checkpoint proposal at slot ${proposal.slotNumber}`, err);
|
|
1297
|
+
return undefined;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1134
1300
|
/**
|
|
1135
1301
|
* Waits for enough attestations to be collected via p2p.
|
|
1136
1302
|
* This is run after all blocks for the checkpoint have been built.
|
|
@@ -1154,16 +1320,16 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1154
1320
|
const numberOfRequiredAttestations = computeQuorum(committee.length);
|
|
1155
1321
|
if (this.config.skipCollectingAttestations) {
|
|
1156
1322
|
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
1157
|
-
const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
|
|
1323
|
+
const attestations = await this.validatorClient?.collectOwnAttestations(proposal, this.checkpointNumber);
|
|
1158
1324
|
return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
|
|
1159
1325
|
}
|
|
1160
|
-
const attestationTimeAllowed = this.config.enforceTimeTable ? this.timetable.
|
|
1326
|
+
const attestationTimeAllowed = this.config.enforceTimeTable ? this.timetable.getCheckpointAttestationDeadline() : this.l1Constants.slotDuration;
|
|
1161
1327
|
const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
|
|
1162
1328
|
this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
|
|
1163
1329
|
const collectAttestationsTimer = new Timer();
|
|
1164
1330
|
let collectedAttestationsCount = 0;
|
|
1165
1331
|
try {
|
|
1166
|
-
const attestations = await this.validatorClient.collectAttestations(proposal, numberOfRequiredAttestations, attestationDeadline);
|
|
1332
|
+
const attestations = await this.validatorClient.collectAttestations(proposal, numberOfRequiredAttestations, attestationDeadline, this.checkpointNumber);
|
|
1167
1333
|
collectedAttestationsCount = attestations.length;
|
|
1168
1334
|
// Trim attestations to minimum required to save L1 calldata gas
|
|
1169
1335
|
const localAddresses = this.validatorClient.getValidatorAddresses();
|
|
@@ -1181,8 +1347,11 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1181
1347
|
} catch (err) {
|
|
1182
1348
|
if (err && err instanceof AttestationTimeoutError) {
|
|
1183
1349
|
collectedAttestationsCount = err.collectedCount;
|
|
1350
|
+
this.log.error(`Timeout while waiting for attestations for checkpoint proposal at slot ${proposal.slotNumber} (collected ${collectedAttestationsCount}/${numberOfRequiredAttestations})`, err);
|
|
1351
|
+
} else {
|
|
1352
|
+
this.log.error(`Error collecting attestations for checkpoint proposal at slot ${proposal.slotNumber}`, err);
|
|
1184
1353
|
}
|
|
1185
|
-
|
|
1354
|
+
return undefined;
|
|
1186
1355
|
} finally{
|
|
1187
1356
|
this.metrics.recordCollectedAttestations(collectedAttestationsCount, collectAttestationsTimer.ms());
|
|
1188
1357
|
}
|
|
@@ -1256,8 +1425,12 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1256
1425
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
1257
1426
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
1258
1427
|
* would never receive its own block without this explicit sync.
|
|
1428
|
+
*
|
|
1429
|
+
* In fisherman mode we skip this push: the fisherman builds blocks locally for validation
|
|
1430
|
+
* and fee analysis only, and pushing them to the archiver causes spurious reorg cascades
|
|
1431
|
+
* whenever the real proposer's block arrives from L1.
|
|
1259
1432
|
*/ async syncProposedBlockToArchiver(block) {
|
|
1260
|
-
if (this.config.skipPushProposedBlocksToArchiver) {
|
|
1433
|
+
if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
|
|
1261
1434
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
1262
1435
|
blockNumber: block.number,
|
|
1263
1436
|
slot: block.header.globalVariables.slotNumber
|
|
@@ -1309,40 +1482,6 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1309
1482
|
}
|
|
1310
1483
|
return false;
|
|
1311
1484
|
}
|
|
1312
|
-
/**
|
|
1313
|
-
* In times of congestion we need to simulate using the correct fee header override for the previous block
|
|
1314
|
-
* We calculate the correct fee header values.
|
|
1315
|
-
*
|
|
1316
|
-
* If we are in block 1, or the checkpoint we are querying does not exist, we return undefined. However
|
|
1317
|
-
* If we are pipelining - where this function is called, the grandparentCheckpointNumber should always exist
|
|
1318
|
-
* @param parentCheckpointNumber
|
|
1319
|
-
* @returns
|
|
1320
|
-
*/ async computeForceProposedFeeHeader(parentCheckpointNumber) {
|
|
1321
|
-
if (!this.proposedCheckpointData) {
|
|
1322
|
-
return undefined;
|
|
1323
|
-
}
|
|
1324
|
-
const rollup = this.publisher.rollupContract;
|
|
1325
|
-
const grandparentCheckpointNumber = CheckpointNumber(this.checkpointNumber - 2);
|
|
1326
|
-
try {
|
|
1327
|
-
const [grandparentCheckpoint, manaTarget] = await Promise.all([
|
|
1328
|
-
rollup.getCheckpoint(grandparentCheckpointNumber),
|
|
1329
|
-
rollup.getManaTarget()
|
|
1330
|
-
]);
|
|
1331
|
-
if (!grandparentCheckpoint || !grandparentCheckpoint.feeHeader) {
|
|
1332
|
-
this.log.error(`Grandparent checkpoint or its feeHeader is undefined for checkpointNumber=${grandparentCheckpointNumber.toString()}`);
|
|
1333
|
-
return undefined;
|
|
1334
|
-
} else {
|
|
1335
|
-
const parentFeeHeader = RollupContract.computeChildFeeHeader(grandparentCheckpoint.feeHeader, this.proposedCheckpointData.totalManaUsed, this.proposedCheckpointData.feeAssetPriceModifier, manaTarget);
|
|
1336
|
-
return {
|
|
1337
|
-
checkpointNumber: parentCheckpointNumber,
|
|
1338
|
-
feeHeader: parentFeeHeader
|
|
1339
|
-
};
|
|
1340
|
-
}
|
|
1341
|
-
} catch (err) {
|
|
1342
|
-
this.log.error(`Failed to fetch grandparent checkpoint or mana target for checkpointNumber=${grandparentCheckpointNumber.toString()}: ${err}`);
|
|
1343
|
-
return undefined;
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
1485
|
/** Waits until a specific time within the current slot */ async waitUntilTimeInSlot(targetSecondsIntoSlot) {
|
|
1347
1486
|
const slotStartTimestamp = this.getSlotStartBuildTimestamp();
|
|
1348
1487
|
const targetTimestamp = slotStartTimestamp + targetSecondsIntoSlot;
|