@aztec/sequencer-client 0.0.1-commit.c2595eba → 0.0.1-commit.c2eed6949
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 -28
- 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 +22 -21
- 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.d.ts +33 -10
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +371 -57
- package/dest/sequencer/checkpoint_proposal_job.d.ts +39 -10
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +287 -167
- 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 +97 -15
- 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 +12 -12
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +45 -36
- package/dest/test/utils.d.ts +3 -3
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +5 -4
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +76 -23
- package/src/config.ts +65 -38
- package/src/global_variable_builder/global_builder.ts +23 -24
- 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.ts +371 -70
- package/src/sequencer/checkpoint_proposal_job.ts +392 -193
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/metrics.ts +106 -18
- package/src/sequencer/sequencer.ts +131 -94
- 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 +65 -53
- package/src/test/utils.ts +5 -2
|
@@ -436,21 +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';
|
|
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
|
-
import { unfreeze } from '@aztec/foundation/types';
|
|
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 { getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
451
450
|
import { Gas } from '@aztec/stdlib/gas';
|
|
451
|
+
import { InsufficientValidTxsError } from '@aztec/stdlib/interfaces/server';
|
|
452
452
|
import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
453
|
-
import { orderAttestations } from '@aztec/stdlib/p2p';
|
|
453
|
+
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
454
454
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
455
455
|
import { Attributes, trackSpan } from '@aztec/telemetry-client';
|
|
456
456
|
import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
|
|
@@ -462,7 +462,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
462
462
|
return {
|
|
463
463
|
// nullish operator needed for tests
|
|
464
464
|
[Attributes.COINBASE]: this.validatorClient.getCoinbaseForAttestor(this.attestorAddress)?.toString(),
|
|
465
|
-
[Attributes.SLOT_NUMBER]: this.
|
|
465
|
+
[Attributes.SLOT_NUMBER]: this.targetSlot
|
|
466
466
|
};
|
|
467
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');
|
|
468
468
|
/**
|
|
@@ -471,8 +471,10 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
471
471
|
* as well as enqueueing votes for slashing and governance proposals. This class is created from
|
|
472
472
|
* the Sequencer once the check for being the proposer for the slot has succeeded.
|
|
473
473
|
*/ export class CheckpointProposalJob {
|
|
474
|
-
|
|
475
|
-
|
|
474
|
+
slotNow;
|
|
475
|
+
targetSlot;
|
|
476
|
+
epochNow;
|
|
477
|
+
targetEpoch;
|
|
476
478
|
checkpointNumber;
|
|
477
479
|
syncedToBlockNumber;
|
|
478
480
|
proposer;
|
|
@@ -542,10 +544,12 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
542
544
|
], []));
|
|
543
545
|
}
|
|
544
546
|
log;
|
|
545
|
-
constructor(
|
|
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.
|
|
546
548
|
proposer, publisher, attestorAddress, invalidateCheckpoint, validatorClient, globalsBuilder, p2pClient, worldState, l1ToL2MessageSource, l2BlockSource, checkpointsBuilder, blockSink, l1Constants, config, timetable, slasherClient, epochCache, dateProvider, metrics, eventEmitter, setStateFn, tracer, bindings){
|
|
547
|
-
this.
|
|
548
|
-
this.
|
|
549
|
+
this.slotNow = slotNow;
|
|
550
|
+
this.targetSlot = targetSlot;
|
|
551
|
+
this.epochNow = epochNow;
|
|
552
|
+
this.targetEpoch = targetEpoch;
|
|
549
553
|
this.checkpointNumber = checkpointNumber;
|
|
550
554
|
this.syncedToBlockNumber = syncedToBlockNumber;
|
|
551
555
|
this.proposer = proposer;
|
|
@@ -573,9 +577,15 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
573
577
|
_initProto(this);
|
|
574
578
|
this.log = createLogger('sequencer:checkpoint-proposal', {
|
|
575
579
|
...bindings,
|
|
576
|
-
instanceId: `slot-${
|
|
580
|
+
instanceId: `slot-${this.slotNow}`
|
|
577
581
|
});
|
|
578
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;
|
|
588
|
+
}
|
|
579
589
|
/**
|
|
580
590
|
* Executes the checkpoint proposal job.
|
|
581
591
|
* Returns the published checkpoint if successful, undefined otherwise.
|
|
@@ -583,19 +593,37 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
583
593
|
// Enqueue governance and slashing votes (returns promises that will be awaited later)
|
|
584
594
|
// In fisherman mode, we simulate slashing but don't actually publish to L1
|
|
585
595
|
// These are constant for the whole slot, so we only enqueue them once
|
|
586
|
-
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();
|
|
587
597
|
// Build and propose the checkpoint. This will enqueue the request on the publisher if a checkpoint is built.
|
|
588
598
|
const checkpoint = await this.proposeCheckpoint();
|
|
589
599
|
// Wait until the voting promises have resolved, so all requests are enqueued (not sent)
|
|
590
600
|
await Promise.all(votesPromises);
|
|
591
601
|
if (checkpoint) {
|
|
592
|
-
this.metrics.
|
|
602
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
593
603
|
}
|
|
594
604
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
595
605
|
if (this.config.fishermanMode) {
|
|
596
606
|
await this.handleCheckpointEndAsFisherman(checkpoint);
|
|
597
607
|
return;
|
|
598
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
|
+
}
|
|
599
627
|
// Then send everything to L1
|
|
600
628
|
const l1Response = await this.publisher.sendRequests();
|
|
601
629
|
const proposedAction = l1Response?.successfulActions.find((a)=>a === 'propose');
|
|
@@ -627,25 +655,33 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
627
655
|
const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
|
|
628
656
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
|
|
629
657
|
// Start the checkpoint
|
|
630
|
-
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.
|
|
631
|
-
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');
|
|
632
667
|
// Enqueues checkpoint invalidation (constant for the whole slot)
|
|
633
668
|
if (this.invalidateCheckpoint && !this.config.skipInvalidateBlockAsProposer) {
|
|
634
669
|
this.publisher.enqueueInvalidateCheckpoint(this.invalidateCheckpoint);
|
|
635
670
|
}
|
|
636
671
|
// Create checkpoint builder for the slot
|
|
637
|
-
const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(coinbase, feeRecipient, this.
|
|
672
|
+
const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(coinbase, feeRecipient, this.targetSlot);
|
|
638
673
|
// Collect L1 to L2 messages for the checkpoint and compute their hash
|
|
639
674
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber);
|
|
640
675
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
641
676
|
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
642
|
-
const
|
|
643
|
-
|
|
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();
|
|
644
680
|
const fork = _ts_add_disposable_resource(env, await this.worldState.fork(this.syncedToBlockNumber, {
|
|
645
681
|
closeDelayMs: 12_000
|
|
646
|
-
}),
|
|
682
|
+
}), true);
|
|
647
683
|
// Create checkpoint builder for the entire slot
|
|
648
|
-
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(this.checkpointNumber, checkpointGlobalVariables, l1ToL2Messages, previousCheckpointOutHashes, fork, this.log.getBindings());
|
|
684
|
+
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(this.checkpointNumber, checkpointGlobalVariables, feeAssetPriceModifier, l1ToL2Messages, previousCheckpointOutHashes, fork, this.log.getBindings());
|
|
649
685
|
// Options for the validator client when creating block and checkpoint proposals
|
|
650
686
|
const blockProposalOptions = {
|
|
651
687
|
publishFullTxs: !!this.config.publishTxsWithProposals,
|
|
@@ -657,6 +693,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
657
693
|
};
|
|
658
694
|
let blocksInCheckpoint = [];
|
|
659
695
|
let blockPendingBroadcast = undefined;
|
|
696
|
+
const checkpointBuildTimer = new Timer();
|
|
660
697
|
try {
|
|
661
698
|
// Main loop: build blocks for the checkpoint
|
|
662
699
|
const result = await this.buildBlocksForCheckpoint(checkpointBuilder, checkpointGlobalVariables.timestamp, inHash, blockProposalOptions);
|
|
@@ -666,40 +703,55 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
666
703
|
// These errors are expected in HA mode, so we yield and let another HA node handle the slot
|
|
667
704
|
// The only distinction between the 2 errors is SlashingProtectionError throws when the payload is different,
|
|
668
705
|
// which is normal for block building (may have picked different txs)
|
|
669
|
-
if (err
|
|
670
|
-
this.log.info(`Checkpoint proposal for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
671
|
-
slot: this.slot,
|
|
672
|
-
signedByNode: err.signedByNode
|
|
673
|
-
});
|
|
674
|
-
return undefined;
|
|
675
|
-
}
|
|
676
|
-
if (err instanceof SlashingProtectionError) {
|
|
677
|
-
this.log.info(`Checkpoint proposal for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
678
|
-
slot: this.slot,
|
|
679
|
-
existingMessageHash: err.existingMessageHash,
|
|
680
|
-
attemptedMessageHash: err.attemptedMessageHash
|
|
681
|
-
});
|
|
706
|
+
if (this.handleHASigningError(err, 'Block proposal')) {
|
|
682
707
|
return undefined;
|
|
683
708
|
}
|
|
684
709
|
throw err;
|
|
685
710
|
}
|
|
686
711
|
if (blocksInCheckpoint.length === 0) {
|
|
687
|
-
this.log.warn(`No blocks were built for slot ${this.
|
|
688
|
-
slot: this.
|
|
712
|
+
this.log.warn(`No blocks were built for slot ${this.targetSlot}`, {
|
|
713
|
+
slot: this.targetSlot
|
|
689
714
|
});
|
|
690
715
|
this.eventEmitter.emit('checkpoint-empty', {
|
|
691
|
-
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
|
|
692
726
|
});
|
|
693
727
|
return undefined;
|
|
694
728
|
}
|
|
695
729
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
696
730
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
697
|
-
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.
|
|
731
|
+
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.targetSlot);
|
|
698
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()));
|
|
699
751
|
// Do not collect attestations nor publish to L1 in fisherman mode
|
|
700
752
|
if (this.config.fishermanMode) {
|
|
701
|
-
this.log.info(`Built checkpoint for slot ${this.
|
|
702
|
-
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,
|
|
703
755
|
checkpoint: checkpoint.header.toInspect(),
|
|
704
756
|
blocksBuilt: blocksInCheckpoint.length
|
|
705
757
|
});
|
|
@@ -713,10 +765,10 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
713
765
|
txs: blockPendingBroadcast.txs
|
|
714
766
|
};
|
|
715
767
|
// Create the checkpoint proposal and broadcast it
|
|
716
|
-
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);
|
|
717
769
|
const blockProposedAt = this.dateProvider.now();
|
|
718
770
|
await this.p2pClient.broadcastCheckpointProposal(proposal);
|
|
719
|
-
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.
|
|
771
|
+
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
|
|
720
772
|
const attestations = await this.waitForAttestations(proposal);
|
|
721
773
|
const blockAttestedAt = this.dateProvider.now();
|
|
722
774
|
this.metrics.recordCheckpointAttestationDelay(blockAttestedAt - blockProposedAt);
|
|
@@ -724,32 +776,28 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
724
776
|
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
725
777
|
let attestationsSignature;
|
|
726
778
|
try {
|
|
727
|
-
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer, this.
|
|
779
|
+
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer, this.targetSlot, this.checkpointNumber);
|
|
728
780
|
} catch (err) {
|
|
729
781
|
// We shouldn't really get here since we yield to another HA node
|
|
730
|
-
// as soon as we see these errors when creating block proposals.
|
|
731
|
-
if (err
|
|
732
|
-
this.log.info(`Attestations signature for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
733
|
-
slot: this.slot,
|
|
734
|
-
signedByNode: err.signedByNode
|
|
735
|
-
});
|
|
736
|
-
return undefined;
|
|
737
|
-
}
|
|
738
|
-
if (err instanceof SlashingProtectionError) {
|
|
739
|
-
this.log.info(`Attestations signature for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
740
|
-
slot: this.slot,
|
|
741
|
-
existingMessageHash: err.existingMessageHash,
|
|
742
|
-
attemptedMessageHash: err.attemptedMessageHash
|
|
743
|
-
});
|
|
782
|
+
// as soon as we see these errors when creating block or checkpoint proposals.
|
|
783
|
+
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
744
784
|
return undefined;
|
|
745
785
|
}
|
|
746
786
|
throw err;
|
|
747
787
|
}
|
|
748
788
|
// Enqueue publishing the checkpoint to L1
|
|
749
|
-
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.
|
|
789
|
+
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot);
|
|
750
790
|
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
751
|
-
const
|
|
752
|
-
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
|
+
}
|
|
753
801
|
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
754
802
|
txTimeoutAt,
|
|
755
803
|
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber
|
|
@@ -759,7 +807,8 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
759
807
|
env.error = e;
|
|
760
808
|
env.hasError = true;
|
|
761
809
|
} finally{
|
|
762
|
-
_ts_dispose_resources(env);
|
|
810
|
+
const result = _ts_dispose_resources(env);
|
|
811
|
+
if (result) await result;
|
|
763
812
|
}
|
|
764
813
|
} catch (err) {
|
|
765
814
|
if (err && (err instanceof DutyAlreadySignedError || err instanceof SlashingProtectionError)) {
|
|
@@ -776,19 +825,17 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
776
825
|
const blocksInCheckpoint = [];
|
|
777
826
|
const txHashesAlreadyIncluded = new Set();
|
|
778
827
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
779
|
-
// Remaining blob fields available for blocks (checkpoint end marker already subtracted)
|
|
780
|
-
let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
|
|
781
828
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
782
829
|
let blockPendingBroadcast = undefined;
|
|
783
830
|
while(true){
|
|
784
831
|
const blocksBuilt = blocksInCheckpoint.length;
|
|
785
|
-
const indexWithinCheckpoint = blocksBuilt;
|
|
832
|
+
const indexWithinCheckpoint = IndexWithinCheckpoint(blocksBuilt);
|
|
786
833
|
const blockNumber = BlockNumber(initialBlockNumber + blocksBuilt);
|
|
787
834
|
const secondsIntoSlot = this.getSecondsIntoSlot();
|
|
788
835
|
const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
|
|
789
836
|
if (!timingInfo.canStart) {
|
|
790
837
|
this.log.debug(`Not enough time left in slot to start another block`, {
|
|
791
|
-
slot: this.
|
|
838
|
+
slot: this.targetSlot,
|
|
792
839
|
blocksBuilt,
|
|
793
840
|
secondsIntoSlot
|
|
794
841
|
});
|
|
@@ -803,9 +850,9 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
803
850
|
buildDeadline: timingInfo.deadline ? new Date((this.getSlotStartBuildTimestamp() + timingInfo.deadline) * 1000) : undefined,
|
|
804
851
|
blockNumber,
|
|
805
852
|
indexWithinCheckpoint,
|
|
806
|
-
txHashesAlreadyIncluded
|
|
807
|
-
remainingBlobFields
|
|
853
|
+
txHashesAlreadyIncluded
|
|
808
854
|
});
|
|
855
|
+
// TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
|
|
809
856
|
if (!buildResult && timingInfo.isLastBlock) {
|
|
810
857
|
break;
|
|
811
858
|
} else if (!buildResult && timingInfo.deadline !== undefined) {
|
|
@@ -817,33 +864,23 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
817
864
|
} else if ('error' in buildResult) {
|
|
818
865
|
// If there was an error building the block, just exit the loop and give up the rest of the slot
|
|
819
866
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
820
|
-
this.log.warn(`Halting block building for slot ${this.
|
|
821
|
-
slot: this.
|
|
867
|
+
this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
|
|
868
|
+
slot: this.targetSlot,
|
|
822
869
|
blocksBuilt,
|
|
823
870
|
error: buildResult.error
|
|
824
871
|
});
|
|
825
872
|
}
|
|
826
873
|
break;
|
|
827
874
|
}
|
|
828
|
-
const { block, usedTxs
|
|
875
|
+
const { block, usedTxs } = buildResult;
|
|
829
876
|
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
877
|
usedTxs.forEach((tx)=>txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
843
|
-
// If this is the last block,
|
|
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.
|
|
844
880
|
if (timingInfo.isLastBlock) {
|
|
845
|
-
this.
|
|
846
|
-
|
|
881
|
+
await this.syncProposedBlockToArchiver(block);
|
|
882
|
+
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
|
|
883
|
+
slot: this.targetSlot,
|
|
847
884
|
blockNumber,
|
|
848
885
|
blocksBuilt
|
|
849
886
|
});
|
|
@@ -853,17 +890,22 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
853
890
|
};
|
|
854
891
|
break;
|
|
855
892
|
}
|
|
856
|
-
//
|
|
857
|
-
//
|
|
858
|
-
if
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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);
|
|
862
904
|
// Wait until the next block's start time
|
|
863
905
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
864
906
|
}
|
|
865
|
-
this.log.verbose(`Block building loop completed for slot ${this.
|
|
866
|
-
slot: this.
|
|
907
|
+
this.log.verbose(`Block building loop completed for slot ${this.targetSlot}`, {
|
|
908
|
+
slot: this.targetSlot,
|
|
867
909
|
blocksBuilt: blocksInCheckpoint.length
|
|
868
910
|
});
|
|
869
911
|
return {
|
|
@@ -871,92 +913,98 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
871
913
|
blockPendingBroadcast
|
|
872
914
|
};
|
|
873
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
|
+
}
|
|
874
923
|
/** 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.
|
|
924
|
+
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot);
|
|
876
925
|
this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, {
|
|
877
|
-
slot: this.
|
|
926
|
+
slot: this.targetSlot
|
|
878
927
|
});
|
|
879
928
|
await this.waitUntilTimeInSlot(nextSubslotStart);
|
|
880
929
|
}
|
|
881
930
|
/** 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.
|
|
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}`, {
|
|
884
933
|
...checkpointBuilder.getConstantData(),
|
|
885
934
|
...opts
|
|
886
935
|
});
|
|
887
936
|
try {
|
|
888
937
|
// Wait until we have enough txs to build the block
|
|
889
|
-
const minTxs = this.
|
|
890
|
-
const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
|
|
938
|
+
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
891
939
|
if (!canStartBuilding) {
|
|
892
|
-
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})`, {
|
|
893
941
|
blockNumber,
|
|
894
|
-
slot: this.
|
|
942
|
+
slot: this.targetSlot,
|
|
895
943
|
indexWithinCheckpoint
|
|
896
944
|
});
|
|
897
945
|
this.eventEmitter.emit('block-tx-count-check-failed', {
|
|
898
946
|
minTxs,
|
|
899
947
|
availableTxs,
|
|
900
|
-
slot: this.
|
|
948
|
+
slot: this.targetSlot
|
|
901
949
|
});
|
|
902
950
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
903
951
|
return undefined;
|
|
904
952
|
}
|
|
905
953
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
906
954
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
907
|
-
const pendingTxs = filter(this.p2pClient.
|
|
908
|
-
this.log.debug(`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.
|
|
909
|
-
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,
|
|
910
958
|
blockNumber,
|
|
911
959
|
indexWithinCheckpoint
|
|
912
960
|
});
|
|
913
|
-
this.setStateFn(SequencerState.CREATING_BLOCK, this.
|
|
914
|
-
//
|
|
915
|
-
|
|
916
|
-
|
|
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;
|
|
917
966
|
const blockBuilderOptions = {
|
|
918
967
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
|
923
974
|
};
|
|
924
|
-
// Actually build the block by executing txs
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
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);
|
|
928
979
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
929
|
-
await this.dropFailedTxsFromP2P(failedTxs);
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
if (!forceCreate && numTxs < minValidTxs) {
|
|
934
|
-
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})`, {
|
|
935
|
-
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,
|
|
936
984
|
blockNumber,
|
|
937
|
-
numTxs,
|
|
938
|
-
indexWithinCheckpoint
|
|
985
|
+
numTxs: buildResult.processedCount,
|
|
986
|
+
indexWithinCheckpoint,
|
|
987
|
+
minValidTxs
|
|
939
988
|
});
|
|
940
|
-
this.eventEmitter.emit('block-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
slot: this.slot
|
|
989
|
+
this.eventEmitter.emit('block-build-failed', {
|
|
990
|
+
reason: `Insufficient valid txs`,
|
|
991
|
+
slot: this.targetSlot
|
|
944
992
|
});
|
|
945
993
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
946
994
|
return undefined;
|
|
947
995
|
}
|
|
948
996
|
// Block creation succeeded, emit stats and metrics
|
|
997
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
|
|
949
998
|
const blockStats = {
|
|
950
999
|
eventName: 'l2-block-built',
|
|
951
1000
|
duration: blockBuildDuration,
|
|
952
1001
|
publicProcessDuration: publicProcessorDuration,
|
|
953
|
-
rollupCircuitsDuration: blockBuildingTimer.ms(),
|
|
954
1002
|
...block.getStats()
|
|
955
1003
|
};
|
|
956
1004
|
const blockHash = await block.hash();
|
|
957
1005
|
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.
|
|
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`, {
|
|
960
1008
|
blockHash,
|
|
961
1009
|
txHashes,
|
|
962
1010
|
manaPerSec,
|
|
@@ -964,22 +1012,22 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
964
1012
|
});
|
|
965
1013
|
this.eventEmitter.emit('block-proposed', {
|
|
966
1014
|
blockNumber: block.number,
|
|
967
|
-
slot: this.
|
|
1015
|
+
slot: this.targetSlot,
|
|
1016
|
+
buildSlot: this.slotNow
|
|
968
1017
|
});
|
|
969
|
-
this.metrics.recordBuiltBlock(blockBuildDuration,
|
|
1018
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
970
1019
|
return {
|
|
971
1020
|
block,
|
|
972
|
-
usedTxs
|
|
973
|
-
remainingBlobFields: maxBlobFieldsForTxs - usedTxBlobFields
|
|
1021
|
+
usedTxs
|
|
974
1022
|
};
|
|
975
1023
|
} catch (err) {
|
|
976
1024
|
this.eventEmitter.emit('block-build-failed', {
|
|
977
1025
|
reason: err.message,
|
|
978
|
-
slot: this.
|
|
1026
|
+
slot: this.targetSlot
|
|
979
1027
|
});
|
|
980
1028
|
this.log.error(`Error building block`, err, {
|
|
981
1029
|
blockNumber,
|
|
982
|
-
slot: this.
|
|
1030
|
+
slot: this.targetSlot
|
|
983
1031
|
});
|
|
984
1032
|
this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
|
|
985
1033
|
this.metrics.recordFailedBlock();
|
|
@@ -988,9 +1036,31 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
988
1036
|
};
|
|
989
1037
|
}
|
|
990
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
|
+
}
|
|
991
1060
|
/** Waits until minTxs are available on the pool for building a block. */ async waitForMinTxs(opts) {
|
|
992
|
-
const minTxs = this.config.minTxsPerBlock;
|
|
993
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;
|
|
994
1064
|
// Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
|
|
995
1065
|
const startBuildingDeadline = buildDeadline ? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000) : undefined;
|
|
996
1066
|
let availableTxs = await this.p2pClient.getPendingTxCount();
|
|
@@ -1000,22 +1070,24 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1000
1070
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
1001
1071
|
return {
|
|
1002
1072
|
canStartBuilding: false,
|
|
1003
|
-
availableTxs
|
|
1073
|
+
availableTxs,
|
|
1074
|
+
minTxs
|
|
1004
1075
|
};
|
|
1005
1076
|
}
|
|
1006
1077
|
// Wait a bit before checking again
|
|
1007
|
-
this.setStateFn(SequencerState.WAITING_FOR_TXS, this.
|
|
1008
|
-
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})`, {
|
|
1009
1080
|
blockNumber,
|
|
1010
|
-
slot: this.
|
|
1081
|
+
slot: this.targetSlot,
|
|
1011
1082
|
indexWithinCheckpoint
|
|
1012
1083
|
});
|
|
1013
|
-
await
|
|
1084
|
+
await this.waitForTxsPollingInterval();
|
|
1014
1085
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
1015
1086
|
}
|
|
1016
1087
|
return {
|
|
1017
1088
|
canStartBuilding: true,
|
|
1018
|
-
availableTxs
|
|
1089
|
+
availableTxs,
|
|
1090
|
+
minTxs
|
|
1019
1091
|
};
|
|
1020
1092
|
}
|
|
1021
1093
|
/**
|
|
@@ -1052,10 +1124,16 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1052
1124
|
try {
|
|
1053
1125
|
const attestations = await this.validatorClient.collectAttestations(proposal, numberOfRequiredAttestations, attestationDeadline);
|
|
1054
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
|
+
}
|
|
1055
1133
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
1056
|
-
const sorted = orderAttestations(
|
|
1134
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
1057
1135
|
// Manipulate the attestations if we've been configured to do so
|
|
1058
|
-
if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
|
|
1136
|
+
if (this.config.injectFakeAttestation || this.config.injectHighSValueAttestation || this.config.injectUnrecoverableSignatureAttestation || this.config.shuffleAttestationOrdering) {
|
|
1059
1137
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
1060
1138
|
}
|
|
1061
1139
|
return new CommitteeAttestationsAndSigners(sorted);
|
|
@@ -1072,7 +1150,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1072
1150
|
// Compute the proposer index in the committee, since we dont want to tweak it.
|
|
1073
1151
|
// Otherwise, the L1 rollup contract will reject the block outright.
|
|
1074
1152
|
const proposerIndex = Number(this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)));
|
|
1075
|
-
if (this.config.injectFakeAttestation) {
|
|
1153
|
+
if (this.config.injectFakeAttestation || this.config.injectHighSValueAttestation || this.config.injectUnrecoverableSignatureAttestation) {
|
|
1076
1154
|
// Find non-empty attestations that are not from the proposer
|
|
1077
1155
|
const nonProposerIndices = [];
|
|
1078
1156
|
for(let i = 0; i < attestations.length; i++){
|
|
@@ -1082,8 +1160,16 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1082
1160
|
}
|
|
1083
1161
|
if (nonProposerIndices.length > 0) {
|
|
1084
1162
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
1085
|
-
this.
|
|
1086
|
-
|
|
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
|
+
}
|
|
1087
1173
|
}
|
|
1088
1174
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
1089
1175
|
}
|
|
@@ -1092,14 +1178,25 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1092
1178
|
const shuffled = [
|
|
1093
1179
|
...attestations
|
|
1094
1180
|
];
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
];
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
+
}
|
|
1103
1200
|
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
1104
1201
|
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|
|
1105
1202
|
}
|
|
@@ -1112,7 +1209,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1112
1209
|
const failedTxData = failedTxs.map((fail)=>fail.tx);
|
|
1113
1210
|
const failedTxHashes = failedTxData.map((tx)=>tx.getTxHash());
|
|
1114
1211
|
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
1115
|
-
await this.p2pClient.
|
|
1212
|
+
await this.p2pClient.handleFailedExecution(failedTxHashes);
|
|
1116
1213
|
}
|
|
1117
1214
|
/**
|
|
1118
1215
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
@@ -1135,27 +1232,50 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1135
1232
|
/** Runs fee analysis and logs checkpoint outcome as fisherman */ async handleCheckpointEndAsFisherman(checkpoint) {
|
|
1136
1233
|
// Perform L1 fee analysis before clearing requests
|
|
1137
1234
|
// The callback is invoked asynchronously after the next block is mined
|
|
1138
|
-
const feeAnalysis = await this.publisher.analyzeL1Fees(this.
|
|
1235
|
+
const feeAnalysis = await this.publisher.analyzeL1Fees(this.targetSlot, (analysis)=>this.metrics.recordFishermanFeeAnalysis(analysis));
|
|
1139
1236
|
if (checkpoint) {
|
|
1140
|
-
this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.
|
|
1237
|
+
this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.targetSlot}`, {
|
|
1141
1238
|
...checkpoint.toCheckpointInfo(),
|
|
1142
1239
|
...checkpoint.getStats(),
|
|
1143
1240
|
feeAnalysisId: feeAnalysis?.id
|
|
1144
1241
|
});
|
|
1145
1242
|
} else {
|
|
1146
|
-
this.log.warn(`Validation block building FAILED for slot ${this.
|
|
1147
|
-
slot: this.
|
|
1243
|
+
this.log.warn(`Validation block building FAILED for slot ${this.targetSlot}`, {
|
|
1244
|
+
slot: this.targetSlot,
|
|
1148
1245
|
feeAnalysisId: feeAnalysis?.id
|
|
1149
1246
|
});
|
|
1150
|
-
this.metrics.
|
|
1247
|
+
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
1151
1248
|
}
|
|
1152
1249
|
this.publisher.clearPendingRequests();
|
|
1153
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
|
+
}
|
|
1154
1271
|
/** Waits until a specific time within the current slot */ async waitUntilTimeInSlot(targetSecondsIntoSlot) {
|
|
1155
1272
|
const slotStartTimestamp = this.getSlotStartBuildTimestamp();
|
|
1156
1273
|
const targetTimestamp = slotStartTimestamp + targetSecondsIntoSlot;
|
|
1157
1274
|
await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
|
|
1158
1275
|
}
|
|
1276
|
+
/** Waits the polling interval for transactions. Extracted for test overriding. */ async waitForTxsPollingInterval() {
|
|
1277
|
+
await sleep(TXS_POLLING_MS);
|
|
1278
|
+
}
|
|
1159
1279
|
getSlotStartBuildTimestamp() {
|
|
1160
1280
|
return getSlotStartBuildTimestamp(this.slot, this.l1Constants);
|
|
1161
1281
|
}
|