@aztec/sequencer-client 0.0.1-commit.ec5f612 → 0.0.1-commit.ec7ac5448
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 +6 -1
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +49 -27
- package/dest/config.d.ts +25 -5
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +31 -22
- package/dest/global_variable_builder/global_builder.d.ts +15 -8
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +26 -26
- package/dest/global_variable_builder/index.d.ts +2 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/publisher/config.d.ts +13 -1
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +17 -2
- 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 +55 -43
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +180 -116
- 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 +24 -9
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +331 -211
- package/dest/sequencer/checkpoint_voter.d.ts +1 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +2 -5
- package/dest/sequencer/events.d.ts +2 -1
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +9 -2
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +23 -1
- package/dest/sequencer/sequencer.d.ts +28 -11
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +134 -74
- package/dest/sequencer/timetable.d.ts +17 -3
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +51 -43
- 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 +7 -9
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +39 -30
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +61 -28
- package/src/config.ts +39 -24
- package/src/global_variable_builder/global_builder.ts +30 -27
- package/src/global_variable_builder/index.ts +1 -1
- package/src/publisher/config.ts +32 -0
- package/src/publisher/sequencer-publisher-factory.ts +18 -6
- package/src/publisher/sequencer-publisher.ts +263 -167
- package/src/sequencer/README.md +83 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +429 -239
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/metrics.ts +29 -1
- package/src/sequencer/sequencer.ts +194 -81
- package/src/sequencer/timetable.ts +64 -52
- package/src/sequencer/types.ts +1 -1
- package/src/test/mock_checkpoint_builder.ts +51 -48
|
@@ -436,25 +436,25 @@ 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 { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
|
|
440
|
-
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
|
|
441
439
|
import { BlockNumber, IndexWithinCheckpoint } from '@aztec/foundation/branded-types';
|
|
442
440
|
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
443
|
-
import {
|
|
441
|
+
import { flipSignature, generateRecoverableSignature, generateUnrecoverableSignature } from '@aztec/foundation/crypto/secp256k1-signer';
|
|
444
442
|
import { filter } from '@aztec/foundation/iterator';
|
|
445
443
|
import { createLogger } from '@aztec/foundation/log';
|
|
446
444
|
import { sleep, sleepUntil } from '@aztec/foundation/sleep';
|
|
447
445
|
import { Timer } from '@aztec/foundation/timer';
|
|
448
446
|
import { isErrorClass, unfreeze } from '@aztec/foundation/types';
|
|
449
447
|
import { CommitteeAttestationsAndSigners, MaliciousCommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
|
|
450
|
-
import {
|
|
448
|
+
import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
449
|
+
import { computeQuorum, getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
451
450
|
import { Gas } from '@aztec/stdlib/gas';
|
|
452
|
-
import {
|
|
451
|
+
import { InsufficientValidTxsError } from '@aztec/stdlib/interfaces/server';
|
|
453
452
|
import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
454
453
|
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
455
454
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
456
455
|
import { Attributes, trackSpan } from '@aztec/telemetry-client';
|
|
457
456
|
import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
|
|
457
|
+
import { buildPipelinedParentSimulationOverridesPlan, buildSubmissionSimulationOverridesPlan } from './chain_state_overrides.js';
|
|
458
458
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
459
459
|
import { SequencerInterruptedError } from './errors.js';
|
|
460
460
|
import { SequencerState } from './utils.js';
|
|
@@ -463,7 +463,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
463
463
|
return {
|
|
464
464
|
// nullish operator needed for tests
|
|
465
465
|
[Attributes.COINBASE]: this.validatorClient.getCoinbaseForAttestor(this.attestorAddress)?.toString(),
|
|
466
|
-
[Attributes.SLOT_NUMBER]: this.
|
|
466
|
+
[Attributes.SLOT_NUMBER]: this.targetSlot
|
|
467
467
|
};
|
|
468
468
|
}), _dec2 = trackSpan('CheckpointProposalJob.buildBlocksForCheckpoint'), _dec3 = trackSpan('CheckpointProposalJob.waitUntilNextSubslot'), _dec4 = trackSpan('CheckpointProposalJob.buildSingleBlock'), _dec5 = trackSpan('CheckpointProposalJob.waitForMinTxs'), _dec6 = trackSpan('CheckpointProposalJob.waitForAttestations'), _dec7 = trackSpan('CheckpointProposalJob.waitUntilTimeInSlot');
|
|
469
469
|
/**
|
|
@@ -472,8 +472,9 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
472
472
|
* as well as enqueueing votes for slashing and governance proposals. This class is created from
|
|
473
473
|
* the Sequencer once the check for being the proposer for the slot has succeeded.
|
|
474
474
|
*/ export class CheckpointProposalJob {
|
|
475
|
-
|
|
476
|
-
|
|
475
|
+
slotNow;
|
|
476
|
+
targetSlot;
|
|
477
|
+
targetEpoch;
|
|
477
478
|
checkpointNumber;
|
|
478
479
|
syncedToBlockNumber;
|
|
479
480
|
proposer;
|
|
@@ -498,6 +499,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
498
499
|
eventEmitter;
|
|
499
500
|
setStateFn;
|
|
500
501
|
tracer;
|
|
502
|
+
proposedCheckpointData;
|
|
501
503
|
static{
|
|
502
504
|
({ e: [_initProto] } = _apply_decs_2203_r(this, [
|
|
503
505
|
[
|
|
@@ -543,10 +545,13 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
543
545
|
], []));
|
|
544
546
|
}
|
|
545
547
|
log;
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
548
|
+
/** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */ pendingL1Submission;
|
|
549
|
+
/** Pipelined parent chain state used while building and later submitting this checkpoint. */ pipelinedParentSimulationOverridesPlan;
|
|
550
|
+
constructor(slotNow, targetSlot, targetEpoch, checkpointNumber, syncedToBlockNumber, // TODO(palla/mbps): Can we remove the proposer in favor of attestorAddress? Need to check fisherman-node flows.
|
|
551
|
+
proposer, publisher, attestorAddress, invalidateCheckpoint, validatorClient, globalsBuilder, p2pClient, worldState, l1ToL2MessageSource, l2BlockSource, checkpointsBuilder, blockSink, l1Constants, config, timetable, slasherClient, epochCache, dateProvider, metrics, eventEmitter, setStateFn, tracer, bindings, proposedCheckpointData){
|
|
552
|
+
this.slotNow = slotNow;
|
|
553
|
+
this.targetSlot = targetSlot;
|
|
554
|
+
this.targetEpoch = targetEpoch;
|
|
550
555
|
this.checkpointNumber = checkpointNumber;
|
|
551
556
|
this.syncedToBlockNumber = syncedToBlockNumber;
|
|
552
557
|
this.proposer = proposer;
|
|
@@ -571,51 +576,139 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
571
576
|
this.eventEmitter = eventEmitter;
|
|
572
577
|
this.setStateFn = setStateFn;
|
|
573
578
|
this.tracer = tracer;
|
|
579
|
+
this.proposedCheckpointData = proposedCheckpointData;
|
|
574
580
|
_initProto(this);
|
|
575
581
|
this.log = createLogger('sequencer:checkpoint-proposal', {
|
|
576
582
|
...bindings,
|
|
577
|
-
instanceId: `slot-${
|
|
583
|
+
instanceId: `slot-${this.slotNow}`
|
|
578
584
|
});
|
|
579
585
|
}
|
|
586
|
+
/** Awaits the pending L1 submission if one is in progress. Call during shutdown. */ async awaitPendingSubmission() {
|
|
587
|
+
this.log.info('Awaiting pending L1 payload submission');
|
|
588
|
+
await this.pendingL1Submission;
|
|
589
|
+
}
|
|
580
590
|
/**
|
|
581
591
|
* Executes the checkpoint proposal job.
|
|
582
|
-
*
|
|
592
|
+
* Builds blocks, assembles checkpoint, and broadcasts the proposal (blocking).
|
|
593
|
+
* Attestation collection, signing, and L1 submission are backgrounded so the
|
|
594
|
+
* work loop can return to IDLE immediately for consecutive slot proposals.
|
|
595
|
+
* Returns the built checkpoint if successful, undefined otherwise.
|
|
583
596
|
*/ async execute() {
|
|
584
597
|
// Enqueue governance and slashing votes (returns promises that will be awaited later)
|
|
585
598
|
// In fisherman mode, we simulate slashing but don't actually publish to L1
|
|
586
599
|
// These are constant for the whole slot, so we only enqueue them once
|
|
587
|
-
const votesPromises = new CheckpointVoter(this.
|
|
588
|
-
// Build
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
600
|
+
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 blocks, assemble checkpoint, and broadcast proposal (BLOCKING).
|
|
602
|
+
// Returns after broadcast — attestation collection is deferred.
|
|
603
|
+
const broadcast = await this.proposeCheckpoint();
|
|
604
|
+
if (!broadcast) {
|
|
605
|
+
await Promise.all(votesPromises);
|
|
606
|
+
// Still submit votes even without a checkpoint
|
|
607
|
+
if (!this.config.fishermanMode) {
|
|
608
|
+
this.pendingL1Submission = this.publisher.sendRequestsAt(this.dateProvider.nowAsDate()).then(()=>{});
|
|
609
|
+
}
|
|
610
|
+
return undefined;
|
|
594
611
|
}
|
|
612
|
+
const { checkpoint } = broadcast;
|
|
613
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
595
614
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
596
615
|
if (this.config.fishermanMode) {
|
|
597
616
|
await this.handleCheckpointEndAsFisherman(checkpoint);
|
|
598
|
-
return;
|
|
617
|
+
return checkpoint;
|
|
599
618
|
}
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
619
|
+
// Background the attestation → signing → L1 pipeline so the work loop is unblocked
|
|
620
|
+
this.pendingL1Submission = this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises);
|
|
621
|
+
// Return the built checkpoint immediately — the work loop is now unblocked
|
|
622
|
+
return checkpoint;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Background pipeline: collects attestations, signs them, enqueues the checkpoint, and submits to L1.
|
|
626
|
+
* Runs as a fire-and-forget task stored in `pendingL1Submission` so the work loop is unblocked.
|
|
627
|
+
*/ async waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises) {
|
|
628
|
+
const { checkpoint, proposal, blockProposedAt } = broadcast;
|
|
629
|
+
try {
|
|
630
|
+
await Promise.all(votesPromises);
|
|
631
|
+
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
|
|
632
|
+
const attestations = await this.waitForAttestations(proposal);
|
|
633
|
+
this.metrics.recordCheckpointAttestationDelay(this.dateProvider.now() - blockProposedAt);
|
|
634
|
+
// Proposer must sign over the attestations before pushing them to L1
|
|
635
|
+
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
636
|
+
let attestationsSignature;
|
|
637
|
+
try {
|
|
638
|
+
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer, this.targetSlot, this.checkpointNumber);
|
|
639
|
+
} catch (err) {
|
|
640
|
+
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
throw err;
|
|
644
|
+
}
|
|
645
|
+
// Enqueue the checkpoint for L1 submission
|
|
646
|
+
await this.enqueueCheckpointForSubmission({
|
|
647
|
+
checkpoint,
|
|
648
|
+
attestations,
|
|
649
|
+
attestationsSignature
|
|
607
650
|
});
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
651
|
+
// Compute the earliest time to submit: pipeline slot start when pipelining, now otherwise.
|
|
652
|
+
const submitAfter = this.epochCache.isProposerPipeliningEnabled() ? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000) : new Date(this.dateProvider.now());
|
|
653
|
+
const l1Response = await this.publisher.sendRequestsAt(submitAfter);
|
|
654
|
+
const proposedAction = l1Response?.successfulActions.find((a)=>a === 'propose');
|
|
655
|
+
if (proposedAction) {
|
|
656
|
+
this.eventEmitter.emit('checkpoint-published', {
|
|
657
|
+
checkpoint: this.checkpointNumber,
|
|
658
|
+
slot: this.targetSlot
|
|
659
|
+
});
|
|
660
|
+
const coinbase = checkpoint.header.coinbase;
|
|
661
|
+
await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
|
|
662
|
+
} else {
|
|
663
|
+
this.eventEmitter.emit('checkpoint-publish-failed', {
|
|
664
|
+
...l1Response,
|
|
665
|
+
slot: this.targetSlot
|
|
666
|
+
});
|
|
667
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
668
|
+
this.metrics.recordPipelineDiscard();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
} catch (err) {
|
|
672
|
+
if (err instanceof SequencerInterruptedError) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
this.log.error(`Background attestation/L1 pipeline failed for slot ${this.targetSlot}`, err);
|
|
612
676
|
this.eventEmitter.emit('checkpoint-publish-failed', {
|
|
613
|
-
|
|
614
|
-
slot: this.slot
|
|
677
|
+
slot: this.targetSlot
|
|
615
678
|
});
|
|
616
|
-
|
|
679
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
680
|
+
this.metrics.recordPipelineDiscard();
|
|
681
|
+
}
|
|
617
682
|
}
|
|
618
683
|
}
|
|
684
|
+
/** Enqueues the checkpoint for L1 submission. Called after pipeline sleep in execute(). */ async enqueueCheckpointForSubmission(result) {
|
|
685
|
+
const { checkpoint, attestations, attestationsSignature } = result;
|
|
686
|
+
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot);
|
|
687
|
+
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
688
|
+
const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
|
|
689
|
+
const txTimeoutAt = new Date((submissionSlotStart + aztecSlotDuration) * 1000);
|
|
690
|
+
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
691
|
+
if (this.config.skipPublishingCheckpointsPercent !== undefined && this.config.skipPublishingCheckpointsPercent > 0) {
|
|
692
|
+
const roll = Math.max(0, randomInt(100));
|
|
693
|
+
if (roll < this.config.skipPublishingCheckpointsPercent) {
|
|
694
|
+
this.log.warn(`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${roll}`);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
699
|
+
const submissionSimulationOverridesPlan = buildSubmissionSimulationOverridesPlan({
|
|
700
|
+
pipelinedParentPlan: this.pipelinedParentSimulationOverridesPlan,
|
|
701
|
+
invalidateToPendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
702
|
+
lastArchiveRoot: checkpoint.header.lastArchiveRoot,
|
|
703
|
+
pipeliningEnabled: isPipelining
|
|
704
|
+
});
|
|
705
|
+
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
706
|
+
txTimeoutAt,
|
|
707
|
+
...submissionSimulationOverridesPlan ? {
|
|
708
|
+
simulationOverridesPlan: submissionSimulationOverridesPlan
|
|
709
|
+
} : {}
|
|
710
|
+
});
|
|
711
|
+
}
|
|
619
712
|
async proposeCheckpoint() {
|
|
620
713
|
try {
|
|
621
714
|
const env = {
|
|
@@ -628,19 +721,35 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
628
721
|
const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
|
|
629
722
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
|
|
630
723
|
// Start the checkpoint
|
|
631
|
-
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.
|
|
632
|
-
this.
|
|
724
|
+
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.targetSlot);
|
|
725
|
+
this.log.info(`Starting checkpoint proposal`, {
|
|
726
|
+
buildSlot: this.slotNow,
|
|
727
|
+
submissionSlot: this.targetSlot,
|
|
728
|
+
pipelining: this.epochCache.isProposerPipeliningEnabled(),
|
|
729
|
+
proposer: this.proposer?.toString(),
|
|
730
|
+
coinbase: coinbase.toString()
|
|
731
|
+
});
|
|
732
|
+
this.metrics.incOpenSlot(this.targetSlot, this.proposer?.toString() ?? 'unknown');
|
|
633
733
|
// Enqueues checkpoint invalidation (constant for the whole slot)
|
|
634
734
|
if (this.invalidateCheckpoint && !this.config.skipInvalidateBlockAsProposer) {
|
|
635
735
|
this.publisher.enqueueInvalidateCheckpoint(this.invalidateCheckpoint);
|
|
636
736
|
}
|
|
637
|
-
// Create checkpoint builder for the slot
|
|
638
|
-
|
|
737
|
+
// Create checkpoint builder for the slot.
|
|
738
|
+
// When pipelining, force the proposed checkpoint number and fee header to our parent so the
|
|
739
|
+
// fee computation sees the same chain tip that L1 will see once the previous pipelined checkpoint lands.
|
|
740
|
+
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
741
|
+
this.pipelinedParentSimulationOverridesPlan = isPipelining ? await buildPipelinedParentSimulationOverridesPlan({
|
|
742
|
+
checkpointNumber: this.checkpointNumber,
|
|
743
|
+
proposedCheckpointData: this.proposedCheckpointData,
|
|
744
|
+
rollup: this.publisher.rollupContract,
|
|
745
|
+
log: this.log
|
|
746
|
+
}) : undefined;
|
|
747
|
+
const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(coinbase, feeRecipient, this.targetSlot, this.pipelinedParentSimulationOverridesPlan);
|
|
639
748
|
// Collect L1 to L2 messages for the checkpoint and compute their hash
|
|
640
749
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber);
|
|
641
750
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
642
751
|
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
643
|
-
const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.
|
|
752
|
+
const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.targetEpoch)).filter((c)=>c.checkpointNumber < this.checkpointNumber).map((c)=>c.checkpointOutHash);
|
|
644
753
|
// Get the fee asset price modifier from the oracle
|
|
645
754
|
const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
|
|
646
755
|
const fork = _ts_add_disposable_resource(env, await this.worldState.fork(this.syncedToBlockNumber, {
|
|
@@ -675,18 +784,18 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
675
784
|
throw err;
|
|
676
785
|
}
|
|
677
786
|
if (blocksInCheckpoint.length === 0) {
|
|
678
|
-
this.log.warn(`No blocks were built for slot ${this.
|
|
679
|
-
slot: this.
|
|
787
|
+
this.log.warn(`No blocks were built for slot ${this.targetSlot}`, {
|
|
788
|
+
slot: this.targetSlot
|
|
680
789
|
});
|
|
681
790
|
this.eventEmitter.emit('checkpoint-empty', {
|
|
682
|
-
slot: this.
|
|
791
|
+
slot: this.targetSlot
|
|
683
792
|
});
|
|
684
793
|
return undefined;
|
|
685
794
|
}
|
|
686
795
|
const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
|
|
687
796
|
if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
|
|
688
797
|
this.log.warn(`Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`, {
|
|
689
|
-
slot: this.
|
|
798
|
+
slot: this.targetSlot,
|
|
690
799
|
blocksBuilt: blocksInCheckpoint.length,
|
|
691
800
|
minBlocksForCheckpoint
|
|
692
801
|
});
|
|
@@ -694,65 +803,51 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
694
803
|
}
|
|
695
804
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
696
805
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
697
|
-
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.
|
|
806
|
+
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.targetSlot);
|
|
698
807
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
808
|
+
// Final validation: per-block limits are only checked if the operator set them explicitly.
|
|
809
|
+
// Otherwise, checkpoint-level budgets were already enforced by the redistribution logic.
|
|
810
|
+
try {
|
|
811
|
+
validateCheckpoint(checkpoint, {
|
|
812
|
+
rollupManaLimit: this.l1Constants.rollupManaLimit,
|
|
813
|
+
maxL2BlockGas: this.config.maxL2BlockGas,
|
|
814
|
+
maxDABlockGas: this.config.maxDABlockGas,
|
|
815
|
+
maxTxsPerBlock: this.config.maxTxsPerBlock,
|
|
816
|
+
maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint
|
|
817
|
+
});
|
|
818
|
+
} catch (err) {
|
|
819
|
+
this.log.error(`Built an invalid checkpoint at slot ${this.slotNow} (skipping proposal)`, err, {
|
|
820
|
+
checkpoint: checkpoint.header.toInspect()
|
|
821
|
+
});
|
|
822
|
+
return undefined;
|
|
823
|
+
}
|
|
699
824
|
// Record checkpoint-level build metrics
|
|
700
825
|
this.metrics.recordCheckpointBuild(checkpointBuildTimer.ms(), blocksInCheckpoint.length, checkpoint.getStats().txCount, Number(checkpoint.header.totalManaUsed.toBigInt()));
|
|
701
|
-
//
|
|
826
|
+
// In fisherman mode, return the checkpoint without broadcasting or collecting attestations
|
|
702
827
|
if (this.config.fishermanMode) {
|
|
703
|
-
this.log.info(`Built checkpoint for slot ${this.
|
|
704
|
-
slot: this.
|
|
828
|
+
this.log.info(`Built checkpoint for slot ${this.targetSlot} with ${blocksInCheckpoint.length} blocks. ` + `Skipping proposal in fisherman mode.`, {
|
|
829
|
+
slot: this.targetSlot,
|
|
705
830
|
checkpoint: checkpoint.header.toInspect(),
|
|
706
831
|
blocksBuilt: blocksInCheckpoint.length
|
|
707
832
|
});
|
|
708
833
|
this.metrics.recordCheckpointSuccess();
|
|
709
|
-
|
|
834
|
+
// Return a broadcast result with a dummy proposal — fisherman mode skips attestation collection
|
|
835
|
+
return {
|
|
836
|
+
checkpoint,
|
|
837
|
+
proposal: undefined,
|
|
838
|
+
blockProposedAt: this.dateProvider.now()
|
|
839
|
+
};
|
|
710
840
|
}
|
|
711
|
-
// Include the block pending broadcast in the checkpoint proposal if any
|
|
712
|
-
const lastBlock = blockPendingBroadcast && {
|
|
713
|
-
blockHeader: blockPendingBroadcast.block.header,
|
|
714
|
-
indexWithinCheckpoint: blockPendingBroadcast.block.indexWithinCheckpoint,
|
|
715
|
-
txs: blockPendingBroadcast.txs
|
|
716
|
-
};
|
|
717
841
|
// Create the checkpoint proposal and broadcast it
|
|
718
|
-
const proposal = await this.validatorClient.createCheckpointProposal(checkpoint.header, checkpoint.archive.root, feeAssetPriceModifier,
|
|
842
|
+
const proposal = await this.validatorClient.createCheckpointProposal(checkpoint.header, checkpoint.archive.root, feeAssetPriceModifier, blockPendingBroadcast, this.proposer, checkpointProposalOptions);
|
|
719
843
|
const blockProposedAt = this.dateProvider.now();
|
|
720
844
|
await this.p2pClient.broadcastCheckpointProposal(proposal);
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
let attestationsSignature;
|
|
728
|
-
try {
|
|
729
|
-
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer, this.slot, this.checkpointNumber);
|
|
730
|
-
} catch (err) {
|
|
731
|
-
// We shouldn't really get here since we yield to another HA node
|
|
732
|
-
// as soon as we see these errors when creating block or checkpoint proposals.
|
|
733
|
-
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
734
|
-
return undefined;
|
|
735
|
-
}
|
|
736
|
-
throw err;
|
|
737
|
-
}
|
|
738
|
-
// Enqueue publishing the checkpoint to L1
|
|
739
|
-
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.slot);
|
|
740
|
-
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
741
|
-
const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
|
|
742
|
-
const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
|
|
743
|
-
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
744
|
-
if (this.config.skipPublishingCheckpointsPercent !== undefined && this.config.skipPublishingCheckpointsPercent > 0) {
|
|
745
|
-
const result = Math.max(0, randomInt(100));
|
|
746
|
-
if (result < this.config.skipPublishingCheckpointsPercent) {
|
|
747
|
-
this.log.warn(`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`);
|
|
748
|
-
return checkpoint;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
752
|
-
txTimeoutAt,
|
|
753
|
-
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber
|
|
754
|
-
});
|
|
755
|
-
return checkpoint;
|
|
845
|
+
// Return immediately after broadcast — attestation collection happens in the background
|
|
846
|
+
return {
|
|
847
|
+
checkpoint,
|
|
848
|
+
proposal,
|
|
849
|
+
blockProposedAt
|
|
850
|
+
};
|
|
756
851
|
} catch (e) {
|
|
757
852
|
env.error = e;
|
|
758
853
|
env.hasError = true;
|
|
@@ -765,7 +860,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
765
860
|
// swallow this error. It's already been logged by a function deeper in the stack
|
|
766
861
|
return undefined;
|
|
767
862
|
}
|
|
768
|
-
this.log.error(`Error building checkpoint at slot ${this.
|
|
863
|
+
this.log.error(`Error building checkpoint at slot ${this.targetSlot}`, err);
|
|
769
864
|
return undefined;
|
|
770
865
|
}
|
|
771
866
|
}
|
|
@@ -775,8 +870,6 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
775
870
|
const blocksInCheckpoint = [];
|
|
776
871
|
const txHashesAlreadyIncluded = new Set();
|
|
777
872
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
778
|
-
// Remaining blob fields available for blocks (checkpoint end marker already subtracted)
|
|
779
|
-
let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
|
|
780
873
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
781
874
|
let blockPendingBroadcast = undefined;
|
|
782
875
|
while(true){
|
|
@@ -787,7 +880,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
787
880
|
const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
|
|
788
881
|
if (!timingInfo.canStart) {
|
|
789
882
|
this.log.debug(`Not enough time left in slot to start another block`, {
|
|
790
|
-
slot: this.
|
|
883
|
+
slot: this.targetSlot,
|
|
791
884
|
blocksBuilt,
|
|
792
885
|
secondsIntoSlot
|
|
793
886
|
});
|
|
@@ -802,68 +895,57 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
802
895
|
buildDeadline: timingInfo.deadline ? new Date((this.getSlotStartBuildTimestamp() + timingInfo.deadline) * 1000) : undefined,
|
|
803
896
|
blockNumber,
|
|
804
897
|
indexWithinCheckpoint,
|
|
805
|
-
txHashesAlreadyIncluded
|
|
806
|
-
remainingBlobFields
|
|
898
|
+
txHashesAlreadyIncluded
|
|
807
899
|
});
|
|
808
|
-
//
|
|
809
|
-
if (
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
900
|
+
// If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
|
|
901
|
+
if ('failure' in buildResult) {
|
|
902
|
+
// If this was the last subslot, or we're running with a single block per slot, we're done
|
|
903
|
+
if (timingInfo.isLastBlock || timingInfo.deadline === undefined) {
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
// Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
|
|
813
907
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
814
908
|
continue;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
909
|
+
}
|
|
910
|
+
// If there was an error building the block, we just exit the loop and give up the rest of the slot.
|
|
911
|
+
// We don't want to risk building more blocks if something went wrong.
|
|
912
|
+
if ('error' in buildResult) {
|
|
819
913
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
820
|
-
this.log.warn(`Halting block building for slot ${this.
|
|
821
|
-
slot: this.
|
|
914
|
+
this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
|
|
915
|
+
slot: this.targetSlot,
|
|
822
916
|
blocksBuilt,
|
|
823
917
|
error: buildResult.error
|
|
824
918
|
});
|
|
825
919
|
}
|
|
826
920
|
break;
|
|
827
921
|
}
|
|
828
|
-
const { block, usedTxs
|
|
922
|
+
const { block, usedTxs } = buildResult;
|
|
829
923
|
blocksInCheckpoint.push(block);
|
|
830
|
-
// Update remaining blob fields for the next block
|
|
831
|
-
remainingBlobFields = newRemainingBlobFields;
|
|
832
|
-
// Sync the proposed block to the archiver to make it available
|
|
833
|
-
// Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
|
|
834
|
-
// Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
|
|
835
|
-
// Fire and forget - don't block the critical path, but log errors
|
|
836
|
-
this.syncProposedBlockToArchiver(block).catch((err)=>{
|
|
837
|
-
this.log.error(`Failed to sync proposed block ${block.number} to archiver`, {
|
|
838
|
-
blockNumber: block.number,
|
|
839
|
-
err
|
|
840
|
-
});
|
|
841
|
-
});
|
|
842
924
|
usedTxs.forEach((tx)=>txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
843
|
-
//
|
|
925
|
+
// Sign the block proposal. This will throw if HA signing fails.
|
|
926
|
+
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
927
|
+
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal,
|
|
928
|
+
// so we avoid polluting our archive with a block that would fail.
|
|
929
|
+
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
|
|
930
|
+
// If this throws, we abort the entire checkpoint.
|
|
931
|
+
await this.syncProposedBlockToArchiver(block);
|
|
932
|
+
// If this is the last block, do not broadcast it, since it will be included in the checkpoint proposal.
|
|
844
933
|
if (timingInfo.isLastBlock) {
|
|
845
|
-
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.
|
|
846
|
-
slot: this.
|
|
934
|
+
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
|
|
935
|
+
slot: this.targetSlot,
|
|
847
936
|
blockNumber,
|
|
848
937
|
blocksBuilt
|
|
849
938
|
});
|
|
850
|
-
blockPendingBroadcast =
|
|
851
|
-
block,
|
|
852
|
-
txs: usedTxs
|
|
853
|
-
};
|
|
939
|
+
blockPendingBroadcast = proposal;
|
|
854
940
|
break;
|
|
855
941
|
}
|
|
856
|
-
//
|
|
857
|
-
|
|
858
|
-
if (!this.config.fishermanMode) {
|
|
859
|
-
const proposal = await this.validatorClient.createBlockProposal(block.header, block.indexWithinCheckpoint, inHash, block.archive.root, usedTxs, this.proposer, blockProposalOptions);
|
|
860
|
-
await this.p2pClient.broadcastProposal(proposal);
|
|
861
|
-
}
|
|
942
|
+
// Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
|
|
943
|
+
proposal && await this.p2pClient.broadcastProposal(proposal);
|
|
862
944
|
// Wait until the next block's start time
|
|
863
945
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
864
946
|
}
|
|
865
|
-
this.log.verbose(`Block building loop completed for slot ${this.
|
|
866
|
-
slot: this.
|
|
947
|
+
this.log.verbose(`Block building loop completed for slot ${this.targetSlot}`, {
|
|
948
|
+
slot: this.targetSlot,
|
|
867
949
|
blocksBuilt: blocksInCheckpoint.length
|
|
868
950
|
});
|
|
869
951
|
return {
|
|
@@ -871,82 +953,92 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
871
953
|
blockPendingBroadcast
|
|
872
954
|
};
|
|
873
955
|
}
|
|
956
|
+
/** Creates a block proposal for a given block via the validator client (unless in fisherman mode) */ createBlockProposal(block, inHash, usedTxs, blockProposalOptions) {
|
|
957
|
+
if (this.config.fishermanMode) {
|
|
958
|
+
this.log.info(`Skipping block proposal for block ${block.number} in fisherman mode`);
|
|
959
|
+
return Promise.resolve(undefined);
|
|
960
|
+
}
|
|
961
|
+
return this.validatorClient.createBlockProposal(block.header, block.indexWithinCheckpoint, inHash, block.archive.root, usedTxs, this.proposer, blockProposalOptions);
|
|
962
|
+
}
|
|
874
963
|
/** Sleeps until it is time to produce the next block in the slot */ async waitUntilNextSubslot(nextSubslotStart) {
|
|
875
|
-
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.
|
|
964
|
+
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot);
|
|
876
965
|
this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, {
|
|
877
|
-
slot: this.
|
|
966
|
+
slot: this.targetSlot
|
|
878
967
|
});
|
|
879
968
|
await this.waitUntilTimeInSlot(nextSubslotStart);
|
|
880
969
|
}
|
|
881
970
|
/** Builds a single block. Called from the main block building loop. */ async buildSingleBlock(checkpointBuilder, opts) {
|
|
882
|
-
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded
|
|
883
|
-
this.log.verbose(`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.
|
|
971
|
+
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } = opts;
|
|
972
|
+
this.log.verbose(`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot}`, {
|
|
884
973
|
...checkpointBuilder.getConstantData(),
|
|
885
974
|
...opts
|
|
886
975
|
});
|
|
887
976
|
try {
|
|
888
977
|
// Wait until we have enough txs to build the block
|
|
889
|
-
const minTxs = this.
|
|
890
|
-
const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
|
|
978
|
+
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
891
979
|
if (!canStartBuilding) {
|
|
892
|
-
this.log.warn(`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.
|
|
980
|
+
this.log.warn(`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (got ${availableTxs} txs but needs ${minTxs})`, {
|
|
893
981
|
blockNumber,
|
|
894
|
-
slot: this.
|
|
982
|
+
slot: this.targetSlot,
|
|
895
983
|
indexWithinCheckpoint
|
|
896
984
|
});
|
|
897
985
|
this.eventEmitter.emit('block-tx-count-check-failed', {
|
|
898
986
|
minTxs,
|
|
899
987
|
availableTxs,
|
|
900
|
-
slot: this.
|
|
988
|
+
slot: this.targetSlot
|
|
901
989
|
});
|
|
902
990
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
903
|
-
return
|
|
991
|
+
return {
|
|
992
|
+
failure: 'insufficient-txs'
|
|
993
|
+
};
|
|
904
994
|
}
|
|
905
995
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
906
996
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
907
997
|
const pendingTxs = filter(this.p2pClient.iterateEligiblePendingTxs(), (tx)=>!txHashesAlreadyIncluded.has(tx.txHash.toString()));
|
|
908
|
-
this.log.debug(`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.
|
|
909
|
-
slot: this.
|
|
998
|
+
this.log.debug(`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot} with ${availableTxs} available txs`, {
|
|
999
|
+
slot: this.targetSlot,
|
|
910
1000
|
blockNumber,
|
|
911
1001
|
indexWithinCheckpoint
|
|
912
1002
|
});
|
|
913
|
-
this.setStateFn(SequencerState.CREATING_BLOCK, this.
|
|
914
|
-
//
|
|
915
|
-
|
|
916
|
-
|
|
1003
|
+
this.setStateFn(SequencerState.CREATING_BLOCK, this.targetSlot);
|
|
1004
|
+
// Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
|
|
1005
|
+
// by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
|
|
1006
|
+
// minValidTxs is passed into the builder so it can reject the block *before* updating state.
|
|
1007
|
+
const minValidTxs = forceCreate ? 0 : this.config.minValidTxsPerBlock ?? minTxs;
|
|
917
1008
|
const blockBuilderOptions = {
|
|
918
1009
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1010
|
+
maxBlockGas: this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined ? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity) : undefined,
|
|
1011
|
+
deadline: buildDeadline,
|
|
1012
|
+
isBuildingProposal: true,
|
|
1013
|
+
minValidTxs,
|
|
1014
|
+
maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
|
|
1015
|
+
perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier
|
|
923
1016
|
};
|
|
924
|
-
// Actually build the block by executing txs
|
|
1017
|
+
// Actually build the block by executing txs. The builder throws InsufficientValidTxsError
|
|
1018
|
+
// if the number of successfully processed txs is below minValidTxs, ensuring state is not
|
|
1019
|
+
// updated for blocks that will be discarded.
|
|
925
1020
|
const buildResult = await this.buildSingleBlockWithCheckpointBuilder(checkpointBuilder, pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
|
|
926
1021
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
927
1022
|
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
|
|
932
|
-
if (buildResult.status === 'no-valid-txs' || !forceCreate && numTxs < minValidTxs) {
|
|
933
|
-
this.log.warn(`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`, {
|
|
934
|
-
slot: this.slot,
|
|
1023
|
+
if (buildResult.status === 'insufficient-valid-txs') {
|
|
1024
|
+
this.log.warn(`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.targetSlot} has too few valid txs to be proposed`, {
|
|
1025
|
+
slot: this.targetSlot,
|
|
935
1026
|
blockNumber,
|
|
936
|
-
numTxs,
|
|
1027
|
+
numTxs: buildResult.processedCount,
|
|
937
1028
|
indexWithinCheckpoint,
|
|
938
|
-
minValidTxs
|
|
939
|
-
buildResult: buildResult.status
|
|
1029
|
+
minValidTxs
|
|
940
1030
|
});
|
|
941
1031
|
this.eventEmitter.emit('block-build-failed', {
|
|
942
1032
|
reason: `Insufficient valid txs`,
|
|
943
|
-
slot: this.
|
|
1033
|
+
slot: this.targetSlot
|
|
944
1034
|
});
|
|
945
1035
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
946
|
-
return
|
|
1036
|
+
return {
|
|
1037
|
+
failure: 'insufficient-valid-txs'
|
|
1038
|
+
};
|
|
947
1039
|
}
|
|
948
1040
|
// Block creation succeeded, emit stats and metrics
|
|
949
|
-
const {
|
|
1041
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
|
|
950
1042
|
const blockStats = {
|
|
951
1043
|
eventName: 'l2-block-built',
|
|
952
1044
|
duration: blockBuildDuration,
|
|
@@ -955,31 +1047,33 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
955
1047
|
};
|
|
956
1048
|
const blockHash = await block.hash();
|
|
957
1049
|
const txHashes = block.body.txEffects.map((tx)=>tx.txHash);
|
|
958
|
-
const manaPerSec =
|
|
959
|
-
this.log.info(`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.
|
|
1050
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
1051
|
+
this.log.info(`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot} with ${numTxs} txs`, {
|
|
960
1052
|
blockHash,
|
|
961
1053
|
txHashes,
|
|
962
1054
|
manaPerSec,
|
|
963
1055
|
...blockStats
|
|
964
1056
|
});
|
|
1057
|
+
// `slot` is the target/submission slot (may be one ahead when pipelining),
|
|
1058
|
+
// `buildSlot` is the wall-clock slot during which the block was actually built.
|
|
965
1059
|
this.eventEmitter.emit('block-proposed', {
|
|
966
1060
|
blockNumber: block.number,
|
|
967
|
-
slot: this.
|
|
1061
|
+
slot: this.targetSlot,
|
|
1062
|
+
buildSlot: this.slotNow
|
|
968
1063
|
});
|
|
969
|
-
this.metrics.recordBuiltBlock(blockBuildDuration,
|
|
1064
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe(), this.targetSlot);
|
|
970
1065
|
return {
|
|
971
1066
|
block,
|
|
972
|
-
usedTxs
|
|
973
|
-
remainingBlobFields: maxBlobFieldsForTxs - usedTxBlobFields
|
|
1067
|
+
usedTxs
|
|
974
1068
|
};
|
|
975
1069
|
} catch (err) {
|
|
976
1070
|
this.eventEmitter.emit('block-build-failed', {
|
|
977
1071
|
reason: err.message,
|
|
978
|
-
slot: this.
|
|
1072
|
+
slot: this.targetSlot
|
|
979
1073
|
});
|
|
980
1074
|
this.log.error(`Error building block`, err, {
|
|
981
1075
|
blockNumber,
|
|
982
|
-
slot: this.
|
|
1076
|
+
slot: this.targetSlot
|
|
983
1077
|
});
|
|
984
1078
|
this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
|
|
985
1079
|
this.metrics.recordFailedBlock();
|
|
@@ -988,7 +1082,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
988
1082
|
};
|
|
989
1083
|
}
|
|
990
1084
|
}
|
|
991
|
-
/** Uses the checkpoint builder to build a block, catching
|
|
1085
|
+
/** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */ async buildSingleBlockWithCheckpointBuilder(checkpointBuilder, pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions) {
|
|
992
1086
|
try {
|
|
993
1087
|
const workTimer = new Timer();
|
|
994
1088
|
const result = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
|
|
@@ -999,10 +1093,11 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
999
1093
|
status: 'success'
|
|
1000
1094
|
};
|
|
1001
1095
|
} catch (err) {
|
|
1002
|
-
if (isErrorClass(err,
|
|
1096
|
+
if (isErrorClass(err, InsufficientValidTxsError)) {
|
|
1003
1097
|
return {
|
|
1004
1098
|
failedTxs: err.failedTxs,
|
|
1005
|
-
|
|
1099
|
+
processedCount: err.processedCount,
|
|
1100
|
+
status: 'insufficient-valid-txs'
|
|
1006
1101
|
};
|
|
1007
1102
|
}
|
|
1008
1103
|
throw err;
|
|
@@ -1021,14 +1116,15 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1021
1116
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
1022
1117
|
return {
|
|
1023
1118
|
canStartBuilding: false,
|
|
1024
|
-
availableTxs
|
|
1119
|
+
availableTxs,
|
|
1120
|
+
minTxs
|
|
1025
1121
|
};
|
|
1026
1122
|
}
|
|
1027
1123
|
// Wait a bit before checking again
|
|
1028
|
-
this.setStateFn(SequencerState.WAITING_FOR_TXS, this.
|
|
1029
|
-
this.log.verbose(`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.
|
|
1124
|
+
this.setStateFn(SequencerState.WAITING_FOR_TXS, this.targetSlot);
|
|
1125
|
+
this.log.verbose(`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (have ${availableTxs} but need ${minTxs})`, {
|
|
1030
1126
|
blockNumber,
|
|
1031
|
-
slot: this.
|
|
1127
|
+
slot: this.targetSlot,
|
|
1032
1128
|
indexWithinCheckpoint
|
|
1033
1129
|
});
|
|
1034
1130
|
await this.waitForTxsPollingInterval();
|
|
@@ -1036,7 +1132,8 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1036
1132
|
}
|
|
1037
1133
|
return {
|
|
1038
1134
|
canStartBuilding: true,
|
|
1039
|
-
availableTxs
|
|
1135
|
+
availableTxs,
|
|
1136
|
+
minTxs
|
|
1040
1137
|
};
|
|
1041
1138
|
}
|
|
1042
1139
|
/**
|
|
@@ -1059,13 +1156,13 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1059
1156
|
committee
|
|
1060
1157
|
});
|
|
1061
1158
|
}
|
|
1062
|
-
const numberOfRequiredAttestations =
|
|
1159
|
+
const numberOfRequiredAttestations = computeQuorum(committee.length);
|
|
1063
1160
|
if (this.config.skipCollectingAttestations) {
|
|
1064
1161
|
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
1065
1162
|
const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
|
|
1066
1163
|
return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
|
|
1067
1164
|
}
|
|
1068
|
-
const attestationTimeAllowed = this.config.enforceTimeTable ? this.timetable.
|
|
1165
|
+
const attestationTimeAllowed = this.config.enforceTimeTable ? this.timetable.getCheckpointAttestationDeadline() : this.l1Constants.slotDuration;
|
|
1069
1166
|
const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
|
|
1070
1167
|
this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
|
|
1071
1168
|
const collectAttestationsTimer = new Timer();
|
|
@@ -1082,7 +1179,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1082
1179
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
1083
1180
|
const sorted = orderAttestations(trimmed, committee);
|
|
1084
1181
|
// Manipulate the attestations if we've been configured to do so
|
|
1085
|
-
if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
|
|
1182
|
+
if (this.config.injectFakeAttestation || this.config.injectHighSValueAttestation || this.config.injectUnrecoverableSignatureAttestation || this.config.shuffleAttestationOrdering) {
|
|
1086
1183
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
1087
1184
|
}
|
|
1088
1185
|
return new CommitteeAttestationsAndSigners(sorted);
|
|
@@ -1099,7 +1196,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1099
1196
|
// Compute the proposer index in the committee, since we dont want to tweak it.
|
|
1100
1197
|
// Otherwise, the L1 rollup contract will reject the block outright.
|
|
1101
1198
|
const proposerIndex = Number(this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)));
|
|
1102
|
-
if (this.config.injectFakeAttestation) {
|
|
1199
|
+
if (this.config.injectFakeAttestation || this.config.injectHighSValueAttestation || this.config.injectUnrecoverableSignatureAttestation) {
|
|
1103
1200
|
// Find non-empty attestations that are not from the proposer
|
|
1104
1201
|
const nonProposerIndices = [];
|
|
1105
1202
|
for(let i = 0; i < attestations.length; i++){
|
|
@@ -1109,8 +1206,16 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1109
1206
|
}
|
|
1110
1207
|
if (nonProposerIndices.length > 0) {
|
|
1111
1208
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
1112
|
-
this.
|
|
1113
|
-
|
|
1209
|
+
if (this.config.injectHighSValueAttestation) {
|
|
1210
|
+
this.log.warn(`Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
|
|
1211
|
+
unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
|
|
1212
|
+
} else if (this.config.injectUnrecoverableSignatureAttestation) {
|
|
1213
|
+
this.log.warn(`Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
|
|
1214
|
+
unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
|
|
1215
|
+
} else {
|
|
1216
|
+
this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
|
|
1217
|
+
unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
|
|
1218
|
+
}
|
|
1114
1219
|
}
|
|
1115
1220
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
1116
1221
|
}
|
|
@@ -1119,14 +1224,25 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1119
1224
|
const shuffled = [
|
|
1120
1225
|
...attestations
|
|
1121
1226
|
];
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
];
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1227
|
+
// Find two non-proposer positions that both have non-empty signatures to swap.
|
|
1228
|
+
// This ensures the bitmap doesn't change, so the MaliciousCommitteeAttestationsAndSigners
|
|
1229
|
+
// signers array stays correctly aligned with L1's committee reconstruction.
|
|
1230
|
+
const swappable = [];
|
|
1231
|
+
for(let k = 0; k < shuffled.length; k++){
|
|
1232
|
+
if (!shuffled[k].signature.isEmpty() && k !== proposerIndex) {
|
|
1233
|
+
swappable.push(k);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (swappable.length >= 2) {
|
|
1237
|
+
const [i, j] = [
|
|
1238
|
+
swappable[0],
|
|
1239
|
+
swappable[1]
|
|
1240
|
+
];
|
|
1241
|
+
[shuffled[i], shuffled[j]] = [
|
|
1242
|
+
shuffled[j],
|
|
1243
|
+
shuffled[i]
|
|
1244
|
+
];
|
|
1245
|
+
}
|
|
1130
1246
|
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
1131
1247
|
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|
|
1132
1248
|
}
|
|
@@ -1145,8 +1261,12 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1145
1261
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
1146
1262
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
1147
1263
|
* would never receive its own block without this explicit sync.
|
|
1264
|
+
*
|
|
1265
|
+
* In fisherman mode we skip this push: the fisherman builds blocks locally for validation
|
|
1266
|
+
* and fee analysis only, and pushing them to the archiver causes spurious reorg cascades
|
|
1267
|
+
* whenever the real proposer's block arrives from L1.
|
|
1148
1268
|
*/ async syncProposedBlockToArchiver(block) {
|
|
1149
|
-
if (this.config.skipPushProposedBlocksToArchiver
|
|
1269
|
+
if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
|
|
1150
1270
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
1151
1271
|
blockNumber: block.number,
|
|
1152
1272
|
slot: block.header.globalVariables.slotNumber
|
|
@@ -1162,16 +1282,16 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1162
1282
|
/** Runs fee analysis and logs checkpoint outcome as fisherman */ async handleCheckpointEndAsFisherman(checkpoint) {
|
|
1163
1283
|
// Perform L1 fee analysis before clearing requests
|
|
1164
1284
|
// The callback is invoked asynchronously after the next block is mined
|
|
1165
|
-
const feeAnalysis = await this.publisher.analyzeL1Fees(this.
|
|
1285
|
+
const feeAnalysis = await this.publisher.analyzeL1Fees(this.targetSlot, (analysis)=>this.metrics.recordFishermanFeeAnalysis(analysis));
|
|
1166
1286
|
if (checkpoint) {
|
|
1167
|
-
this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.
|
|
1287
|
+
this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.targetSlot}`, {
|
|
1168
1288
|
...checkpoint.toCheckpointInfo(),
|
|
1169
1289
|
...checkpoint.getStats(),
|
|
1170
1290
|
feeAnalysisId: feeAnalysis?.id
|
|
1171
1291
|
});
|
|
1172
1292
|
} else {
|
|
1173
|
-
this.log.warn(`Validation block building FAILED for slot ${this.
|
|
1174
|
-
slot: this.
|
|
1293
|
+
this.log.warn(`Validation block building FAILED for slot ${this.targetSlot}`, {
|
|
1294
|
+
slot: this.targetSlot,
|
|
1175
1295
|
feeAnalysisId: feeAnalysis?.id
|
|
1176
1296
|
});
|
|
1177
1297
|
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
@@ -1182,15 +1302,15 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1182
1302
|
* Helper to handle HA double-signing errors. Returns true if the error was handled (caller should yield).
|
|
1183
1303
|
*/ handleHASigningError(err, errorContext) {
|
|
1184
1304
|
if (err instanceof DutyAlreadySignedError) {
|
|
1185
|
-
this.log.info(`${errorContext} for slot ${this.
|
|
1186
|
-
slot: this.
|
|
1305
|
+
this.log.info(`${errorContext} for slot ${this.targetSlot} already signed by another HA node, yielding`, {
|
|
1306
|
+
slot: this.targetSlot,
|
|
1187
1307
|
signedByNode: err.signedByNode
|
|
1188
1308
|
});
|
|
1189
1309
|
return true;
|
|
1190
1310
|
}
|
|
1191
1311
|
if (err instanceof SlashingProtectionError) {
|
|
1192
|
-
this.log.info(`${errorContext} for slot ${this.
|
|
1193
|
-
slot: this.
|
|
1312
|
+
this.log.info(`${errorContext} for slot ${this.targetSlot} blocked by slashing protection, yielding`, {
|
|
1313
|
+
slot: this.targetSlot,
|
|
1194
1314
|
existingMessageHash: err.existingMessageHash,
|
|
1195
1315
|
attemptedMessageHash: err.attemptedMessageHash
|
|
1196
1316
|
});
|
|
@@ -1207,7 +1327,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1207
1327
|
await sleep(TXS_POLLING_MS);
|
|
1208
1328
|
}
|
|
1209
1329
|
getSlotStartBuildTimestamp() {
|
|
1210
|
-
return getSlotStartBuildTimestamp(this.
|
|
1330
|
+
return getSlotStartBuildTimestamp(this.slotNow, this.l1Constants);
|
|
1211
1331
|
}
|
|
1212
1332
|
getSecondsIntoSlot() {
|
|
1213
1333
|
const slotStartTimestamp = this.getSlotStartBuildTimestamp();
|