@aztec/sequencer-client 0.0.1-commit.3469e52 → 0.0.1-commit.35158ae7e
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 +15 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +60 -26
- package/dest/config.d.ts +26 -7
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +47 -30
- package/dest/global_variable_builder/global_builder.d.ts +14 -10
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +24 -23
- 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 +47 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +121 -42
- package/dest/publisher/index.d.ts +2 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
- package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/index.js +2 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +27 -2
- package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +12 -4
- package/dest/publisher/sequencer-publisher.d.ts +33 -10
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +374 -57
- package/dest/sequencer/checkpoint_proposal_job.d.ts +41 -12
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +295 -165
- package/dest/sequencer/events.d.ts +2 -1
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +21 -5
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +122 -30
- package/dest/sequencer/sequencer.d.ts +30 -15
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +95 -82
- package/dest/sequencer/timetable.d.ts +4 -6
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +7 -11
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +3 -5
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +18 -15
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +63 -40
- package/dest/test/utils.d.ts +8 -8
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +10 -9
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +76 -23
- package/src/config.ts +66 -41
- package/src/global_variable_builder/global_builder.ts +25 -26
- package/src/global_variable_builder/index.ts +1 -1
- package/src/publisher/config.ts +153 -43
- package/src/publisher/index.ts +3 -0
- package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
- package/src/publisher/l1_tx_failed_store/index.ts +3 -0
- package/src/publisher/sequencer-publisher-factory.ts +38 -6
- package/src/publisher/sequencer-publisher-metrics.ts +7 -3
- package/src/publisher/sequencer-publisher.ts +376 -70
- package/src/sequencer/checkpoint_proposal_job.ts +409 -201
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/metrics.ts +138 -32
- package/src/sequencer/sequencer.ts +132 -95
- package/src/sequencer/timetable.ts +13 -12
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +93 -65
- package/src/test/utils.ts +22 -13
|
@@ -436,20 +436,21 @@ 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 {
|
|
440
|
-
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
|
|
441
|
-
import { BlockNumber } from '@aztec/foundation/branded-types';
|
|
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';
|
|
443
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
445
444
|
import { sleep, sleepUntil } from '@aztec/foundation/sleep';
|
|
446
445
|
import { Timer } from '@aztec/foundation/timer';
|
|
447
|
-
import { unfreeze } from '@aztec/foundation/types';
|
|
446
|
+
import { isErrorClass, unfreeze } from '@aztec/foundation/types';
|
|
448
447
|
import { CommitteeAttestationsAndSigners, MaliciousCommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
|
|
449
|
-
import {
|
|
448
|
+
import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
449
|
+
import { getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
450
450
|
import { Gas } from '@aztec/stdlib/gas';
|
|
451
|
+
import { InsufficientValidTxsError } from '@aztec/stdlib/interfaces/server';
|
|
451
452
|
import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
452
|
-
import { orderAttestations } from '@aztec/stdlib/p2p';
|
|
453
|
+
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
453
454
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
454
455
|
import { Attributes, trackSpan } from '@aztec/telemetry-client';
|
|
455
456
|
import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
|
|
@@ -461,7 +462,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
461
462
|
return {
|
|
462
463
|
// nullish operator needed for tests
|
|
463
464
|
[Attributes.COINBASE]: this.validatorClient.getCoinbaseForAttestor(this.attestorAddress)?.toString(),
|
|
464
|
-
[Attributes.SLOT_NUMBER]: this.
|
|
465
|
+
[Attributes.SLOT_NUMBER]: this.targetSlot
|
|
465
466
|
};
|
|
466
467
|
}), _dec2 = trackSpan('CheckpointProposalJob.buildBlocksForCheckpoint'), _dec3 = trackSpan('CheckpointProposalJob.waitUntilNextSubslot'), _dec4 = trackSpan('CheckpointProposalJob.buildSingleBlock'), _dec5 = trackSpan('CheckpointProposalJob.waitForMinTxs'), _dec6 = trackSpan('CheckpointProposalJob.waitForAttestations'), _dec7 = trackSpan('CheckpointProposalJob.waitUntilTimeInSlot');
|
|
467
468
|
/**
|
|
@@ -470,8 +471,10 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
470
471
|
* as well as enqueueing votes for slashing and governance proposals. This class is created from
|
|
471
472
|
* the Sequencer once the check for being the proposer for the slot has succeeded.
|
|
472
473
|
*/ export class CheckpointProposalJob {
|
|
473
|
-
|
|
474
|
-
|
|
474
|
+
slotNow;
|
|
475
|
+
targetSlot;
|
|
476
|
+
epochNow;
|
|
477
|
+
targetEpoch;
|
|
475
478
|
checkpointNumber;
|
|
476
479
|
syncedToBlockNumber;
|
|
477
480
|
proposer;
|
|
@@ -495,7 +498,6 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
495
498
|
metrics;
|
|
496
499
|
eventEmitter;
|
|
497
500
|
setStateFn;
|
|
498
|
-
log;
|
|
499
501
|
tracer;
|
|
500
502
|
static{
|
|
501
503
|
({ e: [_initProto] } = _apply_decs_2203_r(this, [
|
|
@@ -541,10 +543,13 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
541
543
|
]
|
|
542
544
|
], []));
|
|
543
545
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
this.
|
|
546
|
+
log;
|
|
547
|
+
constructor(slotNow, targetSlot, epochNow, targetEpoch, checkpointNumber, syncedToBlockNumber, // TODO(palla/mbps): Can we remove the proposer in favor of attestorAddress? Need to check fisherman-node flows.
|
|
548
|
+
proposer, publisher, attestorAddress, invalidateCheckpoint, validatorClient, globalsBuilder, p2pClient, worldState, l1ToL2MessageSource, l2BlockSource, checkpointsBuilder, blockSink, l1Constants, config, timetable, slasherClient, epochCache, dateProvider, metrics, eventEmitter, setStateFn, tracer, bindings){
|
|
549
|
+
this.slotNow = slotNow;
|
|
550
|
+
this.targetSlot = targetSlot;
|
|
551
|
+
this.epochNow = epochNow;
|
|
552
|
+
this.targetEpoch = targetEpoch;
|
|
548
553
|
this.checkpointNumber = checkpointNumber;
|
|
549
554
|
this.syncedToBlockNumber = syncedToBlockNumber;
|
|
550
555
|
this.proposer = proposer;
|
|
@@ -568,9 +573,18 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
568
573
|
this.metrics = metrics;
|
|
569
574
|
this.eventEmitter = eventEmitter;
|
|
570
575
|
this.setStateFn = setStateFn;
|
|
571
|
-
this.log = log;
|
|
572
576
|
this.tracer = tracer;
|
|
573
577
|
_initProto(this);
|
|
578
|
+
this.log = createLogger('sequencer:checkpoint-proposal', {
|
|
579
|
+
...bindings,
|
|
580
|
+
instanceId: `slot-${this.slotNow}`
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
/** The wall-clock slot during which the proposer builds. */ get slot() {
|
|
584
|
+
return this.slotNow;
|
|
585
|
+
}
|
|
586
|
+
/** The wall-clock epoch. */ get epoch() {
|
|
587
|
+
return this.epochNow;
|
|
574
588
|
}
|
|
575
589
|
/**
|
|
576
590
|
* Executes the checkpoint proposal job.
|
|
@@ -579,19 +593,37 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
579
593
|
// Enqueue governance and slashing votes (returns promises that will be awaited later)
|
|
580
594
|
// In fisherman mode, we simulate slashing but don't actually publish to L1
|
|
581
595
|
// These are constant for the whole slot, so we only enqueue them once
|
|
582
|
-
const votesPromises = new CheckpointVoter(this.
|
|
596
|
+
const votesPromises = new CheckpointVoter(this.targetSlot, this.publisher, this.attestorAddress, this.validatorClient, this.slasherClient, this.l1Constants, this.config, this.metrics, this.log).enqueueVotes();
|
|
583
597
|
// Build and propose the checkpoint. This will enqueue the request on the publisher if a checkpoint is built.
|
|
584
598
|
const checkpoint = await this.proposeCheckpoint();
|
|
585
599
|
// Wait until the voting promises have resolved, so all requests are enqueued (not sent)
|
|
586
600
|
await Promise.all(votesPromises);
|
|
587
601
|
if (checkpoint) {
|
|
588
|
-
this.metrics.
|
|
602
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
589
603
|
}
|
|
590
604
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
591
605
|
if (this.config.fishermanMode) {
|
|
592
606
|
await this.handleCheckpointEndAsFisherman(checkpoint);
|
|
593
607
|
return;
|
|
594
608
|
}
|
|
609
|
+
// If pipelining, wait until the submission slot so L1 recognizes the pipelined proposer
|
|
610
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
611
|
+
const submissionSlotTimestamp = getTimestampForSlot(this.targetSlot, this.l1Constants) - BigInt(this.l1Constants.ethereumSlotDuration);
|
|
612
|
+
this.log.info(`Waiting until submission slot ${this.targetSlot} for L1 submission`, {
|
|
613
|
+
slot: this.slot,
|
|
614
|
+
submissionSlot: this.targetSlot,
|
|
615
|
+
submissionSlotTimestamp
|
|
616
|
+
});
|
|
617
|
+
await sleepUntil(new Date(Number(submissionSlotTimestamp) * 1000), this.dateProvider.nowAsDate());
|
|
618
|
+
// After waking, verify the parent checkpoint wasn't pruned during the sleep.
|
|
619
|
+
// We check L1's pending tip directly instead of canProposeAt, which also validates the proposer
|
|
620
|
+
// identity and would fail because the timestamp resolves to a different slot's proposer.
|
|
621
|
+
const l1Tips = await this.publisher.rollupContract.getTips();
|
|
622
|
+
if (l1Tips.pending < this.checkpointNumber - 1) {
|
|
623
|
+
this.log.warn(`Parent checkpoint was pruned during pipelining sleep (L1 pending=${l1Tips.pending}, expected>=${this.checkpointNumber - 1}), skipping L1 submission for checkpoint ${this.checkpointNumber}`);
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
595
627
|
// Then send everything to L1
|
|
596
628
|
const l1Response = await this.publisher.sendRequests();
|
|
597
629
|
const proposedAction = l1Response?.successfulActions.find((a)=>a === 'propose');
|
|
@@ -623,25 +655,33 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
623
655
|
const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
|
|
624
656
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
|
|
625
657
|
// Start the checkpoint
|
|
626
|
-
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.
|
|
627
|
-
this.
|
|
658
|
+
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.targetSlot);
|
|
659
|
+
this.log.info(`Starting checkpoint proposal`, {
|
|
660
|
+
buildSlot: this.slot,
|
|
661
|
+
submissionSlot: this.targetSlot,
|
|
662
|
+
pipelining: this.epochCache.isProposerPipeliningEnabled(),
|
|
663
|
+
proposer: this.proposer?.toString(),
|
|
664
|
+
coinbase: coinbase.toString()
|
|
665
|
+
});
|
|
666
|
+
this.metrics.incOpenSlot(this.targetSlot, this.proposer?.toString() ?? 'unknown');
|
|
628
667
|
// Enqueues checkpoint invalidation (constant for the whole slot)
|
|
629
668
|
if (this.invalidateCheckpoint && !this.config.skipInvalidateBlockAsProposer) {
|
|
630
669
|
this.publisher.enqueueInvalidateCheckpoint(this.invalidateCheckpoint);
|
|
631
670
|
}
|
|
632
671
|
// Create checkpoint builder for the slot
|
|
633
|
-
const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(coinbase, feeRecipient, this.
|
|
672
|
+
const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(coinbase, feeRecipient, this.targetSlot);
|
|
634
673
|
// Collect L1 to L2 messages for the checkpoint and compute their hash
|
|
635
674
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber);
|
|
636
675
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
637
676
|
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
638
|
-
const
|
|
639
|
-
|
|
677
|
+
const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.targetEpoch)).filter((c)=>c.checkpointNumber < this.checkpointNumber).map((c)=>c.checkpointOutHash);
|
|
678
|
+
// Get the fee asset price modifier from the oracle
|
|
679
|
+
const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
|
|
640
680
|
const fork = _ts_add_disposable_resource(env, await this.worldState.fork(this.syncedToBlockNumber, {
|
|
641
681
|
closeDelayMs: 12_000
|
|
642
|
-
}),
|
|
682
|
+
}), true);
|
|
643
683
|
// Create checkpoint builder for the entire slot
|
|
644
|
-
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(this.checkpointNumber, checkpointGlobalVariables, l1ToL2Messages, previousCheckpointOutHashes, fork);
|
|
684
|
+
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(this.checkpointNumber, checkpointGlobalVariables, feeAssetPriceModifier, l1ToL2Messages, previousCheckpointOutHashes, fork, this.log.getBindings());
|
|
645
685
|
// Options for the validator client when creating block and checkpoint proposals
|
|
646
686
|
const blockProposalOptions = {
|
|
647
687
|
publishFullTxs: !!this.config.publishTxsWithProposals,
|
|
@@ -653,6 +693,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
653
693
|
};
|
|
654
694
|
let blocksInCheckpoint = [];
|
|
655
695
|
let blockPendingBroadcast = undefined;
|
|
696
|
+
const checkpointBuildTimer = new Timer();
|
|
656
697
|
try {
|
|
657
698
|
// Main loop: build blocks for the checkpoint
|
|
658
699
|
const result = await this.buildBlocksForCheckpoint(checkpointBuilder, checkpointGlobalVariables.timestamp, inHash, blockProposalOptions);
|
|
@@ -662,40 +703,55 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
662
703
|
// These errors are expected in HA mode, so we yield and let another HA node handle the slot
|
|
663
704
|
// The only distinction between the 2 errors is SlashingProtectionError throws when the payload is different,
|
|
664
705
|
// which is normal for block building (may have picked different txs)
|
|
665
|
-
if (err
|
|
666
|
-
this.log.info(`Checkpoint proposal for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
667
|
-
slot: this.slot,
|
|
668
|
-
signedByNode: err.signedByNode
|
|
669
|
-
});
|
|
670
|
-
return undefined;
|
|
671
|
-
}
|
|
672
|
-
if (err instanceof SlashingProtectionError) {
|
|
673
|
-
this.log.info(`Checkpoint proposal for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
674
|
-
slot: this.slot,
|
|
675
|
-
existingMessageHash: err.existingMessageHash,
|
|
676
|
-
attemptedMessageHash: err.attemptedMessageHash
|
|
677
|
-
});
|
|
706
|
+
if (this.handleHASigningError(err, 'Block proposal')) {
|
|
678
707
|
return undefined;
|
|
679
708
|
}
|
|
680
709
|
throw err;
|
|
681
710
|
}
|
|
682
711
|
if (blocksInCheckpoint.length === 0) {
|
|
683
|
-
this.log.warn(`No blocks were built for slot ${this.
|
|
684
|
-
slot: this.
|
|
712
|
+
this.log.warn(`No blocks were built for slot ${this.targetSlot}`, {
|
|
713
|
+
slot: this.targetSlot
|
|
685
714
|
});
|
|
686
715
|
this.eventEmitter.emit('checkpoint-empty', {
|
|
687
|
-
slot: this.
|
|
716
|
+
slot: this.targetSlot
|
|
717
|
+
});
|
|
718
|
+
return undefined;
|
|
719
|
+
}
|
|
720
|
+
const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
|
|
721
|
+
if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
|
|
722
|
+
this.log.warn(`Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`, {
|
|
723
|
+
slot: this.targetSlot,
|
|
724
|
+
blocksBuilt: blocksInCheckpoint.length,
|
|
725
|
+
minBlocksForCheckpoint
|
|
688
726
|
});
|
|
689
727
|
return undefined;
|
|
690
728
|
}
|
|
691
729
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
692
730
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
693
|
-
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.
|
|
731
|
+
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.targetSlot);
|
|
694
732
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
733
|
+
// Final validation: per-block limits are only checked if the operator set them explicitly.
|
|
734
|
+
// Otherwise, checkpoint-level budgets were already enforced by the redistribution logic.
|
|
735
|
+
try {
|
|
736
|
+
validateCheckpoint(checkpoint, {
|
|
737
|
+
rollupManaLimit: this.l1Constants.rollupManaLimit,
|
|
738
|
+
maxL2BlockGas: this.config.maxL2BlockGas,
|
|
739
|
+
maxDABlockGas: this.config.maxDABlockGas,
|
|
740
|
+
maxTxsPerBlock: this.config.maxTxsPerBlock,
|
|
741
|
+
maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint
|
|
742
|
+
});
|
|
743
|
+
} catch (err) {
|
|
744
|
+
this.log.error(`Built an invalid checkpoint at slot ${this.slot} (skipping proposal)`, err, {
|
|
745
|
+
checkpoint: checkpoint.header.toInspect()
|
|
746
|
+
});
|
|
747
|
+
return undefined;
|
|
748
|
+
}
|
|
749
|
+
// Record checkpoint-level build metrics
|
|
750
|
+
this.metrics.recordCheckpointBuild(checkpointBuildTimer.ms(), blocksInCheckpoint.length, checkpoint.getStats().txCount, Number(checkpoint.header.totalManaUsed.toBigInt()));
|
|
695
751
|
// Do not collect attestations nor publish to L1 in fisherman mode
|
|
696
752
|
if (this.config.fishermanMode) {
|
|
697
|
-
this.log.info(`Built checkpoint for slot ${this.
|
|
698
|
-
slot: this.
|
|
753
|
+
this.log.info(`Built checkpoint for slot ${this.targetSlot} with ${blocksInCheckpoint.length} blocks. ` + `Skipping proposal in fisherman mode.`, {
|
|
754
|
+
slot: this.targetSlot,
|
|
699
755
|
checkpoint: checkpoint.header.toInspect(),
|
|
700
756
|
blocksBuilt: blocksInCheckpoint.length
|
|
701
757
|
});
|
|
@@ -709,10 +765,10 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
709
765
|
txs: blockPendingBroadcast.txs
|
|
710
766
|
};
|
|
711
767
|
// Create the checkpoint proposal and broadcast it
|
|
712
|
-
const proposal = await this.validatorClient.createCheckpointProposal(checkpoint.header, checkpoint.archive.root, lastBlock, this.proposer, checkpointProposalOptions);
|
|
768
|
+
const proposal = await this.validatorClient.createCheckpointProposal(checkpoint.header, checkpoint.archive.root, feeAssetPriceModifier, lastBlock, this.proposer, checkpointProposalOptions);
|
|
713
769
|
const blockProposedAt = this.dateProvider.now();
|
|
714
770
|
await this.p2pClient.broadcastCheckpointProposal(proposal);
|
|
715
|
-
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.
|
|
771
|
+
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
|
|
716
772
|
const attestations = await this.waitForAttestations(proposal);
|
|
717
773
|
const blockAttestedAt = this.dateProvider.now();
|
|
718
774
|
this.metrics.recordCheckpointAttestationDelay(blockAttestedAt - blockProposedAt);
|
|
@@ -720,32 +776,28 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
720
776
|
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
721
777
|
let attestationsSignature;
|
|
722
778
|
try {
|
|
723
|
-
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer, this.
|
|
779
|
+
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer, this.targetSlot, this.checkpointNumber);
|
|
724
780
|
} catch (err) {
|
|
725
781
|
// We shouldn't really get here since we yield to another HA node
|
|
726
|
-
// as soon as we see these errors when creating block proposals.
|
|
727
|
-
if (err
|
|
728
|
-
this.log.info(`Attestations signature for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
729
|
-
slot: this.slot,
|
|
730
|
-
signedByNode: err.signedByNode
|
|
731
|
-
});
|
|
732
|
-
return undefined;
|
|
733
|
-
}
|
|
734
|
-
if (err instanceof SlashingProtectionError) {
|
|
735
|
-
this.log.info(`Attestations signature for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
736
|
-
slot: this.slot,
|
|
737
|
-
existingMessageHash: err.existingMessageHash,
|
|
738
|
-
attemptedMessageHash: err.attemptedMessageHash
|
|
739
|
-
});
|
|
782
|
+
// as soon as we see these errors when creating block or checkpoint proposals.
|
|
783
|
+
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
740
784
|
return undefined;
|
|
741
785
|
}
|
|
742
786
|
throw err;
|
|
743
787
|
}
|
|
744
788
|
// Enqueue publishing the checkpoint to L1
|
|
745
|
-
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.
|
|
789
|
+
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot);
|
|
746
790
|
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
747
|
-
const
|
|
748
|
-
const txTimeoutAt = new Date((
|
|
791
|
+
const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
|
|
792
|
+
const txTimeoutAt = new Date((submissionSlotStart + aztecSlotDuration) * 1000);
|
|
793
|
+
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
794
|
+
if (this.config.skipPublishingCheckpointsPercent !== undefined && this.config.skipPublishingCheckpointsPercent > 0) {
|
|
795
|
+
const result = Math.max(0, randomInt(100));
|
|
796
|
+
if (result < this.config.skipPublishingCheckpointsPercent) {
|
|
797
|
+
this.log.warn(`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`);
|
|
798
|
+
return checkpoint;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
749
801
|
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
750
802
|
txTimeoutAt,
|
|
751
803
|
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber
|
|
@@ -755,7 +807,8 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
755
807
|
env.error = e;
|
|
756
808
|
env.hasError = true;
|
|
757
809
|
} finally{
|
|
758
|
-
_ts_dispose_resources(env);
|
|
810
|
+
const result = _ts_dispose_resources(env);
|
|
811
|
+
if (result) await result;
|
|
759
812
|
}
|
|
760
813
|
} catch (err) {
|
|
761
814
|
if (err && (err instanceof DutyAlreadySignedError || err instanceof SlashingProtectionError)) {
|
|
@@ -772,19 +825,17 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
772
825
|
const blocksInCheckpoint = [];
|
|
773
826
|
const txHashesAlreadyIncluded = new Set();
|
|
774
827
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
775
|
-
// Remaining blob fields available for blocks (checkpoint end marker already subtracted)
|
|
776
|
-
let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
|
|
777
828
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
778
829
|
let blockPendingBroadcast = undefined;
|
|
779
830
|
while(true){
|
|
780
831
|
const blocksBuilt = blocksInCheckpoint.length;
|
|
781
|
-
const indexWithinCheckpoint = blocksBuilt;
|
|
832
|
+
const indexWithinCheckpoint = IndexWithinCheckpoint(blocksBuilt);
|
|
782
833
|
const blockNumber = BlockNumber(initialBlockNumber + blocksBuilt);
|
|
783
834
|
const secondsIntoSlot = this.getSecondsIntoSlot();
|
|
784
835
|
const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
|
|
785
836
|
if (!timingInfo.canStart) {
|
|
786
837
|
this.log.debug(`Not enough time left in slot to start another block`, {
|
|
787
|
-
slot: this.
|
|
838
|
+
slot: this.targetSlot,
|
|
788
839
|
blocksBuilt,
|
|
789
840
|
secondsIntoSlot
|
|
790
841
|
});
|
|
@@ -799,9 +850,9 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
799
850
|
buildDeadline: timingInfo.deadline ? new Date((this.getSlotStartBuildTimestamp() + timingInfo.deadline) * 1000) : undefined,
|
|
800
851
|
blockNumber,
|
|
801
852
|
indexWithinCheckpoint,
|
|
802
|
-
txHashesAlreadyIncluded
|
|
803
|
-
remainingBlobFields
|
|
853
|
+
txHashesAlreadyIncluded
|
|
804
854
|
});
|
|
855
|
+
// TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
|
|
805
856
|
if (!buildResult && timingInfo.isLastBlock) {
|
|
806
857
|
break;
|
|
807
858
|
} else if (!buildResult && timingInfo.deadline !== undefined) {
|
|
@@ -813,26 +864,23 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
813
864
|
} else if ('error' in buildResult) {
|
|
814
865
|
// If there was an error building the block, just exit the loop and give up the rest of the slot
|
|
815
866
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
816
|
-
this.log.warn(`Halting block building for slot ${this.
|
|
817
|
-
slot: this.
|
|
867
|
+
this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
|
|
868
|
+
slot: this.targetSlot,
|
|
818
869
|
blocksBuilt,
|
|
819
870
|
error: buildResult.error
|
|
820
871
|
});
|
|
821
872
|
}
|
|
822
873
|
break;
|
|
823
874
|
}
|
|
824
|
-
const { block, usedTxs
|
|
875
|
+
const { block, usedTxs } = buildResult;
|
|
825
876
|
blocksInCheckpoint.push(block);
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
//
|
|
829
|
-
// Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
|
|
830
|
-
// Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
|
|
831
|
-
await this.syncProposedBlockToArchiver(block);
|
|
832
|
-
// If this is the last block, exit the loop now so we start collecting attestations
|
|
877
|
+
usedTxs.forEach((tx)=>txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
878
|
+
// If this is the last block, sync it to the archiver and exit the loop
|
|
879
|
+
// so we can build the checkpoint and start collecting attestations.
|
|
833
880
|
if (timingInfo.isLastBlock) {
|
|
834
|
-
this.
|
|
835
|
-
|
|
881
|
+
await this.syncProposedBlockToArchiver(block);
|
|
882
|
+
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
|
|
883
|
+
slot: this.targetSlot,
|
|
836
884
|
blockNumber,
|
|
837
885
|
blocksBuilt
|
|
838
886
|
});
|
|
@@ -842,17 +890,22 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
842
890
|
};
|
|
843
891
|
break;
|
|
844
892
|
}
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
if
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
893
|
+
// Broadcast the block proposal (unless we're in fisherman mode) unless the block is the last one,
|
|
894
|
+
// in which case we'll broadcast it along with the checkpoint at the end of the loop.
|
|
895
|
+
// Note that we only send the block to the archiver if we manage to create the proposal, so if there's
|
|
896
|
+
// a HA error we don't pollute our archiver with a block that won't make it to the chain.
|
|
897
|
+
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
898
|
+
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal.
|
|
899
|
+
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
|
|
900
|
+
// If this throws, we abort the entire checkpoint.
|
|
901
|
+
await this.syncProposedBlockToArchiver(block);
|
|
902
|
+
// Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
|
|
903
|
+
proposal && await this.p2pClient.broadcastProposal(proposal);
|
|
851
904
|
// Wait until the next block's start time
|
|
852
905
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
853
906
|
}
|
|
854
|
-
this.log.verbose(`Block building loop completed for slot ${this.
|
|
855
|
-
slot: this.
|
|
907
|
+
this.log.verbose(`Block building loop completed for slot ${this.targetSlot}`, {
|
|
908
|
+
slot: this.targetSlot,
|
|
856
909
|
blocksBuilt: blocksInCheckpoint.length
|
|
857
910
|
});
|
|
858
911
|
return {
|
|
@@ -860,92 +913,98 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
860
913
|
blockPendingBroadcast
|
|
861
914
|
};
|
|
862
915
|
}
|
|
916
|
+
/** Creates a block proposal for a given block via the validator client (unless in fisherman mode) */ createBlockProposal(block, inHash, usedTxs, blockProposalOptions) {
|
|
917
|
+
if (this.config.fishermanMode) {
|
|
918
|
+
this.log.info(`Skipping block proposal for block ${block.number} in fisherman mode`);
|
|
919
|
+
return Promise.resolve(undefined);
|
|
920
|
+
}
|
|
921
|
+
return this.validatorClient.createBlockProposal(block.header, block.indexWithinCheckpoint, inHash, block.archive.root, usedTxs, this.proposer, blockProposalOptions);
|
|
922
|
+
}
|
|
863
923
|
/** Sleeps until it is time to produce the next block in the slot */ async waitUntilNextSubslot(nextSubslotStart) {
|
|
864
|
-
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.
|
|
924
|
+
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot);
|
|
865
925
|
this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, {
|
|
866
|
-
slot: this.
|
|
926
|
+
slot: this.targetSlot
|
|
867
927
|
});
|
|
868
928
|
await this.waitUntilTimeInSlot(nextSubslotStart);
|
|
869
929
|
}
|
|
870
930
|
/** Builds a single block. Called from the main block building loop. */ async buildSingleBlock(checkpointBuilder, opts) {
|
|
871
|
-
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded
|
|
872
|
-
this.log.verbose(`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.
|
|
931
|
+
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } = opts;
|
|
932
|
+
this.log.verbose(`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot}`, {
|
|
873
933
|
...checkpointBuilder.getConstantData(),
|
|
874
934
|
...opts
|
|
875
935
|
});
|
|
876
936
|
try {
|
|
877
937
|
// Wait until we have enough txs to build the block
|
|
878
|
-
const minTxs = this.
|
|
879
|
-
const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
|
|
938
|
+
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
880
939
|
if (!canStartBuilding) {
|
|
881
|
-
this.log.warn(`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.
|
|
940
|
+
this.log.warn(`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (got ${availableTxs} txs but needs ${minTxs})`, {
|
|
882
941
|
blockNumber,
|
|
883
|
-
slot: this.
|
|
942
|
+
slot: this.targetSlot,
|
|
884
943
|
indexWithinCheckpoint
|
|
885
944
|
});
|
|
886
945
|
this.eventEmitter.emit('block-tx-count-check-failed', {
|
|
887
946
|
minTxs,
|
|
888
947
|
availableTxs,
|
|
889
|
-
slot: this.
|
|
948
|
+
slot: this.targetSlot
|
|
890
949
|
});
|
|
891
950
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
892
951
|
return undefined;
|
|
893
952
|
}
|
|
894
953
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
895
954
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
896
|
-
const pendingTxs = filter(this.p2pClient.
|
|
897
|
-
this.log.debug(`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.
|
|
898
|
-
slot: this.
|
|
955
|
+
const pendingTxs = filter(this.p2pClient.iterateEligiblePendingTxs(), (tx)=>!txHashesAlreadyIncluded.has(tx.txHash.toString()));
|
|
956
|
+
this.log.debug(`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot} with ${availableTxs} available txs`, {
|
|
957
|
+
slot: this.targetSlot,
|
|
899
958
|
blockNumber,
|
|
900
959
|
indexWithinCheckpoint
|
|
901
960
|
});
|
|
902
|
-
this.setStateFn(SequencerState.CREATING_BLOCK, this.
|
|
903
|
-
//
|
|
904
|
-
|
|
905
|
-
|
|
961
|
+
this.setStateFn(SequencerState.CREATING_BLOCK, this.targetSlot);
|
|
962
|
+
// Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
|
|
963
|
+
// by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
|
|
964
|
+
// minValidTxs is passed into the builder so it can reject the block *before* updating state.
|
|
965
|
+
const minValidTxs = forceCreate ? 0 : this.config.minValidTxsPerBlock ?? minTxs;
|
|
906
966
|
const blockBuilderOptions = {
|
|
907
967
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
968
|
+
maxBlockGas: this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined ? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity) : undefined,
|
|
969
|
+
deadline: buildDeadline,
|
|
970
|
+
isBuildingProposal: true,
|
|
971
|
+
minValidTxs,
|
|
972
|
+
maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
|
|
973
|
+
perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier
|
|
912
974
|
};
|
|
913
|
-
// Actually build the block by executing txs
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
const
|
|
975
|
+
// Actually build the block by executing txs. The builder throws InsufficientValidTxsError
|
|
976
|
+
// if the number of successfully processed txs is below minValidTxs, ensuring state is not
|
|
977
|
+
// updated for blocks that will be discarded.
|
|
978
|
+
const buildResult = await this.buildSingleBlockWithCheckpointBuilder(checkpointBuilder, pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
|
|
917
979
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
918
|
-
await this.dropFailedTxsFromP2P(failedTxs);
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
if (!forceCreate && numTxs < minValidTxs) {
|
|
923
|
-
this.log.warn(`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed (got ${numTxs} but required ${minValidTxs})`, {
|
|
924
|
-
slot: this.slot,
|
|
980
|
+
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
981
|
+
if (buildResult.status === 'insufficient-valid-txs') {
|
|
982
|
+
this.log.warn(`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.targetSlot} has too few valid txs to be proposed`, {
|
|
983
|
+
slot: this.targetSlot,
|
|
925
984
|
blockNumber,
|
|
926
|
-
numTxs,
|
|
927
|
-
indexWithinCheckpoint
|
|
985
|
+
numTxs: buildResult.processedCount,
|
|
986
|
+
indexWithinCheckpoint,
|
|
987
|
+
minValidTxs
|
|
928
988
|
});
|
|
929
|
-
this.eventEmitter.emit('block-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
slot: this.slot
|
|
989
|
+
this.eventEmitter.emit('block-build-failed', {
|
|
990
|
+
reason: `Insufficient valid txs`,
|
|
991
|
+
slot: this.targetSlot
|
|
933
992
|
});
|
|
934
993
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
935
994
|
return undefined;
|
|
936
995
|
}
|
|
937
996
|
// Block creation succeeded, emit stats and metrics
|
|
997
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
|
|
938
998
|
const blockStats = {
|
|
939
999
|
eventName: 'l2-block-built',
|
|
940
1000
|
duration: blockBuildDuration,
|
|
941
1001
|
publicProcessDuration: publicProcessorDuration,
|
|
942
|
-
rollupCircuitsDuration: blockBuildingTimer.ms(),
|
|
943
1002
|
...block.getStats()
|
|
944
1003
|
};
|
|
945
1004
|
const blockHash = await block.hash();
|
|
946
1005
|
const txHashes = block.body.txEffects.map((tx)=>tx.txHash);
|
|
947
|
-
const manaPerSec =
|
|
948
|
-
this.log.info(`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.
|
|
1006
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
1007
|
+
this.log.info(`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot} with ${numTxs} txs`, {
|
|
949
1008
|
blockHash,
|
|
950
1009
|
txHashes,
|
|
951
1010
|
manaPerSec,
|
|
@@ -953,22 +1012,22 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
953
1012
|
});
|
|
954
1013
|
this.eventEmitter.emit('block-proposed', {
|
|
955
1014
|
blockNumber: block.number,
|
|
956
|
-
slot: this.
|
|
1015
|
+
slot: this.targetSlot,
|
|
1016
|
+
buildSlot: this.slotNow
|
|
957
1017
|
});
|
|
958
|
-
this.metrics.recordBuiltBlock(blockBuildDuration,
|
|
1018
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
959
1019
|
return {
|
|
960
1020
|
block,
|
|
961
|
-
usedTxs
|
|
962
|
-
remainingBlobFields: maxBlobFieldsForTxs - usedTxBlobFields
|
|
1021
|
+
usedTxs
|
|
963
1022
|
};
|
|
964
1023
|
} catch (err) {
|
|
965
1024
|
this.eventEmitter.emit('block-build-failed', {
|
|
966
1025
|
reason: err.message,
|
|
967
|
-
slot: this.
|
|
1026
|
+
slot: this.targetSlot
|
|
968
1027
|
});
|
|
969
1028
|
this.log.error(`Error building block`, err, {
|
|
970
1029
|
blockNumber,
|
|
971
|
-
slot: this.
|
|
1030
|
+
slot: this.targetSlot
|
|
972
1031
|
});
|
|
973
1032
|
this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
|
|
974
1033
|
this.metrics.recordFailedBlock();
|
|
@@ -977,9 +1036,31 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
977
1036
|
};
|
|
978
1037
|
}
|
|
979
1038
|
}
|
|
1039
|
+
/** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */ async buildSingleBlockWithCheckpointBuilder(checkpointBuilder, pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions) {
|
|
1040
|
+
try {
|
|
1041
|
+
const workTimer = new Timer();
|
|
1042
|
+
const result = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
|
|
1043
|
+
const blockBuildDuration = workTimer.ms();
|
|
1044
|
+
return {
|
|
1045
|
+
...result,
|
|
1046
|
+
blockBuildDuration,
|
|
1047
|
+
status: 'success'
|
|
1048
|
+
};
|
|
1049
|
+
} catch (err) {
|
|
1050
|
+
if (isErrorClass(err, InsufficientValidTxsError)) {
|
|
1051
|
+
return {
|
|
1052
|
+
failedTxs: err.failedTxs,
|
|
1053
|
+
processedCount: err.processedCount,
|
|
1054
|
+
status: 'insufficient-valid-txs'
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
throw err;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
980
1060
|
/** Waits until minTxs are available on the pool for building a block. */ async waitForMinTxs(opts) {
|
|
981
|
-
const minTxs = this.config.minTxsPerBlock;
|
|
982
1061
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
1062
|
+
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
1063
|
+
const minTxs = indexWithinCheckpoint > 0 && this.config.minTxsPerBlock === 0 ? 1 : this.config.minTxsPerBlock;
|
|
983
1064
|
// Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
|
|
984
1065
|
const startBuildingDeadline = buildDeadline ? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000) : undefined;
|
|
985
1066
|
let availableTxs = await this.p2pClient.getPendingTxCount();
|
|
@@ -989,22 +1070,24 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
989
1070
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
990
1071
|
return {
|
|
991
1072
|
canStartBuilding: false,
|
|
992
|
-
availableTxs
|
|
1073
|
+
availableTxs,
|
|
1074
|
+
minTxs
|
|
993
1075
|
};
|
|
994
1076
|
}
|
|
995
1077
|
// Wait a bit before checking again
|
|
996
|
-
this.setStateFn(SequencerState.WAITING_FOR_TXS, this.
|
|
997
|
-
this.log.verbose(`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.
|
|
1078
|
+
this.setStateFn(SequencerState.WAITING_FOR_TXS, this.targetSlot);
|
|
1079
|
+
this.log.verbose(`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (have ${availableTxs} but need ${minTxs})`, {
|
|
998
1080
|
blockNumber,
|
|
999
|
-
slot: this.
|
|
1081
|
+
slot: this.targetSlot,
|
|
1000
1082
|
indexWithinCheckpoint
|
|
1001
1083
|
});
|
|
1002
|
-
await
|
|
1084
|
+
await this.waitForTxsPollingInterval();
|
|
1003
1085
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
1004
1086
|
}
|
|
1005
1087
|
return {
|
|
1006
1088
|
canStartBuilding: true,
|
|
1007
|
-
availableTxs
|
|
1089
|
+
availableTxs,
|
|
1090
|
+
minTxs
|
|
1008
1091
|
};
|
|
1009
1092
|
}
|
|
1010
1093
|
/**
|
|
@@ -1034,17 +1117,23 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1034
1117
|
return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
|
|
1035
1118
|
}
|
|
1036
1119
|
const attestationTimeAllowed = this.config.enforceTimeTable ? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_CHECKPOINT) : this.l1Constants.slotDuration;
|
|
1037
|
-
const attestationDeadline = new Date(this.
|
|
1120
|
+
const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
|
|
1038
1121
|
this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
|
|
1039
1122
|
const collectAttestationsTimer = new Timer();
|
|
1040
1123
|
let collectedAttestationsCount = 0;
|
|
1041
1124
|
try {
|
|
1042
1125
|
const attestations = await this.validatorClient.collectAttestations(proposal, numberOfRequiredAttestations, attestationDeadline);
|
|
1043
1126
|
collectedAttestationsCount = attestations.length;
|
|
1127
|
+
// Trim attestations to minimum required to save L1 calldata gas
|
|
1128
|
+
const localAddresses = this.validatorClient.getValidatorAddresses();
|
|
1129
|
+
const trimmed = trimAttestations(attestations, numberOfRequiredAttestations, this.attestorAddress, localAddresses);
|
|
1130
|
+
if (trimmed.length < attestations.length) {
|
|
1131
|
+
this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
|
|
1132
|
+
}
|
|
1044
1133
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
1045
|
-
const sorted = orderAttestations(
|
|
1134
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
1046
1135
|
// Manipulate the attestations if we've been configured to do so
|
|
1047
|
-
if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
|
|
1136
|
+
if (this.config.injectFakeAttestation || this.config.injectHighSValueAttestation || this.config.injectUnrecoverableSignatureAttestation || this.config.shuffleAttestationOrdering) {
|
|
1048
1137
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
1049
1138
|
}
|
|
1050
1139
|
return new CommitteeAttestationsAndSigners(sorted);
|
|
@@ -1061,7 +1150,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1061
1150
|
// Compute the proposer index in the committee, since we dont want to tweak it.
|
|
1062
1151
|
// Otherwise, the L1 rollup contract will reject the block outright.
|
|
1063
1152
|
const proposerIndex = Number(this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)));
|
|
1064
|
-
if (this.config.injectFakeAttestation) {
|
|
1153
|
+
if (this.config.injectFakeAttestation || this.config.injectHighSValueAttestation || this.config.injectUnrecoverableSignatureAttestation) {
|
|
1065
1154
|
// Find non-empty attestations that are not from the proposer
|
|
1066
1155
|
const nonProposerIndices = [];
|
|
1067
1156
|
for(let i = 0; i < attestations.length; i++){
|
|
@@ -1071,8 +1160,16 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1071
1160
|
}
|
|
1072
1161
|
if (nonProposerIndices.length > 0) {
|
|
1073
1162
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
1074
|
-
this.
|
|
1075
|
-
|
|
1163
|
+
if (this.config.injectHighSValueAttestation) {
|
|
1164
|
+
this.log.warn(`Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
|
|
1165
|
+
unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
|
|
1166
|
+
} else if (this.config.injectUnrecoverableSignatureAttestation) {
|
|
1167
|
+
this.log.warn(`Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
|
|
1168
|
+
unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
|
|
1169
|
+
} else {
|
|
1170
|
+
this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
|
|
1171
|
+
unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
|
|
1172
|
+
}
|
|
1076
1173
|
}
|
|
1077
1174
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
1078
1175
|
}
|
|
@@ -1081,14 +1178,25 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1081
1178
|
const shuffled = [
|
|
1082
1179
|
...attestations
|
|
1083
1180
|
];
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
];
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1181
|
+
// Find two non-proposer positions that both have non-empty signatures to swap.
|
|
1182
|
+
// This ensures the bitmap doesn't change, so the MaliciousCommitteeAttestationsAndSigners
|
|
1183
|
+
// signers array stays correctly aligned with L1's committee reconstruction.
|
|
1184
|
+
const swappable = [];
|
|
1185
|
+
for(let k = 0; k < shuffled.length; k++){
|
|
1186
|
+
if (!shuffled[k].signature.isEmpty() && k !== proposerIndex) {
|
|
1187
|
+
swappable.push(k);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (swappable.length >= 2) {
|
|
1191
|
+
const [i, j] = [
|
|
1192
|
+
swappable[0],
|
|
1193
|
+
swappable[1]
|
|
1194
|
+
];
|
|
1195
|
+
[shuffled[i], shuffled[j]] = [
|
|
1196
|
+
shuffled[j],
|
|
1197
|
+
shuffled[i]
|
|
1198
|
+
];
|
|
1199
|
+
}
|
|
1092
1200
|
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
1093
1201
|
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|
|
1094
1202
|
}
|
|
@@ -1101,14 +1209,13 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1101
1209
|
const failedTxData = failedTxs.map((fail)=>fail.tx);
|
|
1102
1210
|
const failedTxHashes = failedTxData.map((tx)=>tx.getTxHash());
|
|
1103
1211
|
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
1104
|
-
await this.p2pClient.
|
|
1212
|
+
await this.p2pClient.handleFailedExecution(failedTxHashes);
|
|
1105
1213
|
}
|
|
1106
1214
|
/**
|
|
1107
1215
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
1108
1216
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
1109
1217
|
* would never receive its own block without this explicit sync.
|
|
1110
1218
|
*/ async syncProposedBlockToArchiver(block) {
|
|
1111
|
-
// TODO(palla/mbps): Change default to false once block sync is stable.
|
|
1112
1219
|
if (this.config.skipPushProposedBlocksToArchiver !== false) {
|
|
1113
1220
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
1114
1221
|
blockNumber: block.number,
|
|
@@ -1125,27 +1232,50 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1125
1232
|
/** Runs fee analysis and logs checkpoint outcome as fisherman */ async handleCheckpointEndAsFisherman(checkpoint) {
|
|
1126
1233
|
// Perform L1 fee analysis before clearing requests
|
|
1127
1234
|
// The callback is invoked asynchronously after the next block is mined
|
|
1128
|
-
const feeAnalysis = await this.publisher.analyzeL1Fees(this.
|
|
1235
|
+
const feeAnalysis = await this.publisher.analyzeL1Fees(this.targetSlot, (analysis)=>this.metrics.recordFishermanFeeAnalysis(analysis));
|
|
1129
1236
|
if (checkpoint) {
|
|
1130
|
-
this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.
|
|
1237
|
+
this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.targetSlot}`, {
|
|
1131
1238
|
...checkpoint.toCheckpointInfo(),
|
|
1132
1239
|
...checkpoint.getStats(),
|
|
1133
1240
|
feeAnalysisId: feeAnalysis?.id
|
|
1134
1241
|
});
|
|
1135
1242
|
} else {
|
|
1136
|
-
this.log.warn(`Validation block building FAILED for slot ${this.
|
|
1137
|
-
slot: this.
|
|
1243
|
+
this.log.warn(`Validation block building FAILED for slot ${this.targetSlot}`, {
|
|
1244
|
+
slot: this.targetSlot,
|
|
1138
1245
|
feeAnalysisId: feeAnalysis?.id
|
|
1139
1246
|
});
|
|
1140
|
-
this.metrics.
|
|
1247
|
+
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
1141
1248
|
}
|
|
1142
1249
|
this.publisher.clearPendingRequests();
|
|
1143
1250
|
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Helper to handle HA double-signing errors. Returns true if the error was handled (caller should yield).
|
|
1253
|
+
*/ handleHASigningError(err, errorContext) {
|
|
1254
|
+
if (err instanceof DutyAlreadySignedError) {
|
|
1255
|
+
this.log.info(`${errorContext} for slot ${this.targetSlot} already signed by another HA node, yielding`, {
|
|
1256
|
+
slot: this.targetSlot,
|
|
1257
|
+
signedByNode: err.signedByNode
|
|
1258
|
+
});
|
|
1259
|
+
return true;
|
|
1260
|
+
}
|
|
1261
|
+
if (err instanceof SlashingProtectionError) {
|
|
1262
|
+
this.log.info(`${errorContext} for slot ${this.targetSlot} blocked by slashing protection, yielding`, {
|
|
1263
|
+
slot: this.targetSlot,
|
|
1264
|
+
existingMessageHash: err.existingMessageHash,
|
|
1265
|
+
attemptedMessageHash: err.attemptedMessageHash
|
|
1266
|
+
});
|
|
1267
|
+
return true;
|
|
1268
|
+
}
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1144
1271
|
/** Waits until a specific time within the current slot */ async waitUntilTimeInSlot(targetSecondsIntoSlot) {
|
|
1145
1272
|
const slotStartTimestamp = this.getSlotStartBuildTimestamp();
|
|
1146
1273
|
const targetTimestamp = slotStartTimestamp + targetSecondsIntoSlot;
|
|
1147
1274
|
await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
|
|
1148
1275
|
}
|
|
1276
|
+
/** Waits the polling interval for transactions. Extracted for test overriding. */ async waitForTxsPollingInterval() {
|
|
1277
|
+
await sleep(TXS_POLLING_MS);
|
|
1278
|
+
}
|
|
1149
1279
|
getSlotStartBuildTimestamp() {
|
|
1150
1280
|
return getSlotStartBuildTimestamp(this.slot, this.l1Constants);
|
|
1151
1281
|
}
|