@aztec/sequencer-client 0.0.1-commit.6d3c34e → 0.0.1-commit.7035c9bd6
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 +12 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +56 -17
- 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 +2 -4
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +7 -6
- package/dest/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/publisher/config.d.ts +35 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +106 -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 +30 -10
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +362 -56
- package/dest/sequencer/checkpoint_proposal_job.d.ts +42 -11
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +322 -122
- package/dest/sequencer/checkpoint_voter.d.ts +3 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +34 -10
- package/dest/sequencer/events.d.ts +2 -1
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/index.d.ts +1 -2
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +0 -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 +43 -20
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +151 -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 +23 -19
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +67 -38
- package/dest/test/utils.d.ts +8 -8
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +12 -11
- package/package.json +29 -28
- package/src/client/sequencer-client.ts +77 -18
- package/src/config.ts +66 -41
- package/src/global_variable_builder/global_builder.ts +6 -5
- package/src/index.ts +1 -6
- package/src/publisher/config.ts +121 -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 +360 -69
- package/src/sequencer/checkpoint_proposal_job.ts +449 -142
- package/src/sequencer/checkpoint_voter.ts +32 -7
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/index.ts +0 -1
- package/src/sequencer/metrics.ts +138 -32
- package/src/sequencer/sequencer.ts +200 -91
- 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 +122 -78
- package/src/test/utils.ts +24 -14
- package/dest/sequencer/block_builder.d.ts +0 -26
- package/dest/sequencer/block_builder.d.ts.map +0 -1
- package/dest/sequencer/block_builder.js +0 -129
- package/src/sequencer/block_builder.ts +0 -216
|
@@ -436,21 +436,24 @@ 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 { BlockNumber } from '@aztec/foundation/branded-types';
|
|
439
|
+
import { BlockNumber, IndexWithinCheckpoint } from '@aztec/foundation/branded-types';
|
|
441
440
|
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
442
|
-
import {
|
|
441
|
+
import { flipSignature, generateRecoverableSignature, generateUnrecoverableSignature } from '@aztec/foundation/crypto/secp256k1-signer';
|
|
443
442
|
import { filter } from '@aztec/foundation/iterator';
|
|
443
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
444
444
|
import { sleep, sleepUntil } from '@aztec/foundation/sleep';
|
|
445
445
|
import { Timer } from '@aztec/foundation/timer';
|
|
446
|
-
import { unfreeze } from '@aztec/foundation/types';
|
|
446
|
+
import { isErrorClass, unfreeze } from '@aztec/foundation/types';
|
|
447
447
|
import { CommitteeAttestationsAndSigners, MaliciousCommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
|
|
448
|
-
import {
|
|
448
|
+
import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
449
|
+
import { getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
449
450
|
import { Gas } from '@aztec/stdlib/gas';
|
|
451
|
+
import { InsufficientValidTxsError } from '@aztec/stdlib/interfaces/server';
|
|
450
452
|
import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
451
|
-
import { orderAttestations } from '@aztec/stdlib/p2p';
|
|
453
|
+
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
452
454
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
453
455
|
import { Attributes, trackSpan } from '@aztec/telemetry-client';
|
|
456
|
+
import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
|
|
454
457
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
455
458
|
import { SequencerInterruptedError } from './errors.js';
|
|
456
459
|
import { SequencerState } from './utils.js';
|
|
@@ -459,7 +462,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
459
462
|
return {
|
|
460
463
|
// nullish operator needed for tests
|
|
461
464
|
[Attributes.COINBASE]: this.validatorClient.getCoinbaseForAttestor(this.attestorAddress)?.toString(),
|
|
462
|
-
[Attributes.SLOT_NUMBER]: this.
|
|
465
|
+
[Attributes.SLOT_NUMBER]: this.targetSlot
|
|
463
466
|
};
|
|
464
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');
|
|
465
468
|
/**
|
|
@@ -468,7 +471,10 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
468
471
|
* as well as enqueueing votes for slashing and governance proposals. This class is created from
|
|
469
472
|
* the Sequencer once the check for being the proposer for the slot has succeeded.
|
|
470
473
|
*/ export class CheckpointProposalJob {
|
|
471
|
-
|
|
474
|
+
slotNow;
|
|
475
|
+
targetSlot;
|
|
476
|
+
epochNow;
|
|
477
|
+
targetEpoch;
|
|
472
478
|
checkpointNumber;
|
|
473
479
|
syncedToBlockNumber;
|
|
474
480
|
proposer;
|
|
@@ -480,6 +486,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
480
486
|
p2pClient;
|
|
481
487
|
worldState;
|
|
482
488
|
l1ToL2MessageSource;
|
|
489
|
+
l2BlockSource;
|
|
483
490
|
checkpointsBuilder;
|
|
484
491
|
blockSink;
|
|
485
492
|
l1Constants;
|
|
@@ -491,7 +498,6 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
491
498
|
metrics;
|
|
492
499
|
eventEmitter;
|
|
493
500
|
setStateFn;
|
|
494
|
-
log;
|
|
495
501
|
tracer;
|
|
496
502
|
static{
|
|
497
503
|
({ e: [_initProto] } = _apply_decs_2203_r(this, [
|
|
@@ -537,9 +543,13 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
537
543
|
]
|
|
538
544
|
], []));
|
|
539
545
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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;
|
|
543
553
|
this.checkpointNumber = checkpointNumber;
|
|
544
554
|
this.syncedToBlockNumber = syncedToBlockNumber;
|
|
545
555
|
this.proposer = proposer;
|
|
@@ -551,6 +561,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
551
561
|
this.p2pClient = p2pClient;
|
|
552
562
|
this.worldState = worldState;
|
|
553
563
|
this.l1ToL2MessageSource = l1ToL2MessageSource;
|
|
564
|
+
this.l2BlockSource = l2BlockSource;
|
|
554
565
|
this.checkpointsBuilder = checkpointsBuilder;
|
|
555
566
|
this.blockSink = blockSink;
|
|
556
567
|
this.l1Constants = l1Constants;
|
|
@@ -562,9 +573,18 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
562
573
|
this.metrics = metrics;
|
|
563
574
|
this.eventEmitter = eventEmitter;
|
|
564
575
|
this.setStateFn = setStateFn;
|
|
565
|
-
this.log = log;
|
|
566
576
|
this.tracer = tracer;
|
|
567
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;
|
|
568
588
|
}
|
|
569
589
|
/**
|
|
570
590
|
* Executes the checkpoint proposal job.
|
|
@@ -573,19 +593,37 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
573
593
|
// Enqueue governance and slashing votes (returns promises that will be awaited later)
|
|
574
594
|
// In fisherman mode, we simulate slashing but don't actually publish to L1
|
|
575
595
|
// These are constant for the whole slot, so we only enqueue them once
|
|
576
|
-
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();
|
|
577
597
|
// Build and propose the checkpoint. This will enqueue the request on the publisher if a checkpoint is built.
|
|
578
598
|
const checkpoint = await this.proposeCheckpoint();
|
|
579
599
|
// Wait until the voting promises have resolved, so all requests are enqueued (not sent)
|
|
580
600
|
await Promise.all(votesPromises);
|
|
581
601
|
if (checkpoint) {
|
|
582
|
-
this.metrics.
|
|
602
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
583
603
|
}
|
|
584
604
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
585
605
|
if (this.config.fishermanMode) {
|
|
586
606
|
await this.handleCheckpointEndAsFisherman(checkpoint);
|
|
587
607
|
return;
|
|
588
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
|
+
}
|
|
589
627
|
// Then send everything to L1
|
|
590
628
|
const l1Response = await this.publisher.sendRequests();
|
|
591
629
|
const proposedAction = l1Response?.successfulActions.find((a)=>a === 'propose');
|
|
@@ -617,22 +655,33 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
617
655
|
const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
|
|
618
656
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
|
|
619
657
|
// Start the checkpoint
|
|
620
|
-
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.
|
|
621
|
-
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');
|
|
622
667
|
// Enqueues checkpoint invalidation (constant for the whole slot)
|
|
623
668
|
if (this.invalidateCheckpoint && !this.config.skipInvalidateBlockAsProposer) {
|
|
624
669
|
this.publisher.enqueueInvalidateCheckpoint(this.invalidateCheckpoint);
|
|
625
670
|
}
|
|
626
671
|
// Create checkpoint builder for the slot
|
|
627
|
-
const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(coinbase, feeRecipient, this.
|
|
672
|
+
const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(coinbase, feeRecipient, this.targetSlot);
|
|
628
673
|
// Collect L1 to L2 messages for the checkpoint and compute their hash
|
|
629
674
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber);
|
|
630
675
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
676
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
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();
|
|
631
680
|
const fork = _ts_add_disposable_resource(env, await this.worldState.fork(this.syncedToBlockNumber, {
|
|
632
681
|
closeDelayMs: 12_000
|
|
633
|
-
}),
|
|
682
|
+
}), true);
|
|
634
683
|
// Create checkpoint builder for the entire slot
|
|
635
|
-
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(this.checkpointNumber, checkpointGlobalVariables, l1ToL2Messages, fork);
|
|
684
|
+
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(this.checkpointNumber, checkpointGlobalVariables, feeAssetPriceModifier, l1ToL2Messages, previousCheckpointOutHashes, fork, this.log.getBindings());
|
|
636
685
|
// Options for the validator client when creating block and checkpoint proposals
|
|
637
686
|
const blockProposalOptions = {
|
|
638
687
|
publishFullTxs: !!this.config.publishTxsWithProposals,
|
|
@@ -642,25 +691,67 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
642
691
|
publishFullTxs: !!this.config.publishTxsWithProposals,
|
|
643
692
|
broadcastInvalidCheckpointProposal: this.config.broadcastInvalidBlockProposal
|
|
644
693
|
};
|
|
645
|
-
|
|
646
|
-
|
|
694
|
+
let blocksInCheckpoint = [];
|
|
695
|
+
let blockPendingBroadcast = undefined;
|
|
696
|
+
const checkpointBuildTimer = new Timer();
|
|
697
|
+
try {
|
|
698
|
+
// Main loop: build blocks for the checkpoint
|
|
699
|
+
const result = await this.buildBlocksForCheckpoint(checkpointBuilder, checkpointGlobalVariables.timestamp, inHash, blockProposalOptions);
|
|
700
|
+
blocksInCheckpoint = result.blocksInCheckpoint;
|
|
701
|
+
blockPendingBroadcast = result.blockPendingBroadcast;
|
|
702
|
+
} catch (err) {
|
|
703
|
+
// These errors are expected in HA mode, so we yield and let another HA node handle the slot
|
|
704
|
+
// The only distinction between the 2 errors is SlashingProtectionError throws when the payload is different,
|
|
705
|
+
// which is normal for block building (may have picked different txs)
|
|
706
|
+
if (this.handleHASigningError(err, 'Block proposal')) {
|
|
707
|
+
return undefined;
|
|
708
|
+
}
|
|
709
|
+
throw err;
|
|
710
|
+
}
|
|
647
711
|
if (blocksInCheckpoint.length === 0) {
|
|
648
|
-
this.log.warn(`No blocks were built for slot ${this.
|
|
649
|
-
slot: this.
|
|
712
|
+
this.log.warn(`No blocks were built for slot ${this.targetSlot}`, {
|
|
713
|
+
slot: this.targetSlot
|
|
650
714
|
});
|
|
651
715
|
this.eventEmitter.emit('checkpoint-empty', {
|
|
652
|
-
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
|
|
653
726
|
});
|
|
654
727
|
return undefined;
|
|
655
728
|
}
|
|
656
729
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
657
730
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
658
|
-
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.
|
|
731
|
+
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.targetSlot);
|
|
659
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()));
|
|
660
751
|
// Do not collect attestations nor publish to L1 in fisherman mode
|
|
661
752
|
if (this.config.fishermanMode) {
|
|
662
|
-
this.log.info(`Built checkpoint for slot ${this.
|
|
663
|
-
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,
|
|
664
755
|
checkpoint: checkpoint.header.toInspect(),
|
|
665
756
|
blocksBuilt: blocksInCheckpoint.length
|
|
666
757
|
});
|
|
@@ -674,21 +765,39 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
674
765
|
txs: blockPendingBroadcast.txs
|
|
675
766
|
};
|
|
676
767
|
// Create the checkpoint proposal and broadcast it
|
|
677
|
-
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);
|
|
678
769
|
const blockProposedAt = this.dateProvider.now();
|
|
679
770
|
await this.p2pClient.broadcastCheckpointProposal(proposal);
|
|
680
|
-
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.
|
|
771
|
+
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
|
|
681
772
|
const attestations = await this.waitForAttestations(proposal);
|
|
682
773
|
const blockAttestedAt = this.dateProvider.now();
|
|
683
774
|
this.metrics.recordCheckpointAttestationDelay(blockAttestedAt - blockProposedAt);
|
|
684
775
|
// Proposer must sign over the attestations before pushing them to L1
|
|
685
776
|
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
686
|
-
|
|
777
|
+
let attestationsSignature;
|
|
778
|
+
try {
|
|
779
|
+
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer, this.targetSlot, this.checkpointNumber);
|
|
780
|
+
} catch (err) {
|
|
781
|
+
// We shouldn't really get here since we yield to another HA node
|
|
782
|
+
// as soon as we see these errors when creating block or checkpoint proposals.
|
|
783
|
+
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
784
|
+
return undefined;
|
|
785
|
+
}
|
|
786
|
+
throw err;
|
|
787
|
+
}
|
|
687
788
|
// Enqueue publishing the checkpoint to L1
|
|
688
|
-
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.
|
|
789
|
+
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot);
|
|
689
790
|
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
690
|
-
const
|
|
691
|
-
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
|
+
}
|
|
692
801
|
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
693
802
|
txTimeoutAt,
|
|
694
803
|
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber
|
|
@@ -698,9 +807,14 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
698
807
|
env.error = e;
|
|
699
808
|
env.hasError = true;
|
|
700
809
|
} finally{
|
|
701
|
-
_ts_dispose_resources(env);
|
|
810
|
+
const result = _ts_dispose_resources(env);
|
|
811
|
+
if (result) await result;
|
|
702
812
|
}
|
|
703
813
|
} catch (err) {
|
|
814
|
+
if (err && (err instanceof DutyAlreadySignedError || err instanceof SlashingProtectionError)) {
|
|
815
|
+
// swallow this error. It's already been logged by a function deeper in the stack
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
704
818
|
this.log.error(`Error building checkpoint at slot ${this.slot}`, err);
|
|
705
819
|
return undefined;
|
|
706
820
|
}
|
|
@@ -715,13 +829,13 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
715
829
|
let blockPendingBroadcast = undefined;
|
|
716
830
|
while(true){
|
|
717
831
|
const blocksBuilt = blocksInCheckpoint.length;
|
|
718
|
-
const indexWithinCheckpoint = blocksBuilt;
|
|
832
|
+
const indexWithinCheckpoint = IndexWithinCheckpoint(blocksBuilt);
|
|
719
833
|
const blockNumber = BlockNumber(initialBlockNumber + blocksBuilt);
|
|
720
834
|
const secondsIntoSlot = this.getSecondsIntoSlot();
|
|
721
835
|
const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
|
|
722
836
|
if (!timingInfo.canStart) {
|
|
723
837
|
this.log.debug(`Not enough time left in slot to start another block`, {
|
|
724
|
-
slot: this.
|
|
838
|
+
slot: this.targetSlot,
|
|
725
839
|
blocksBuilt,
|
|
726
840
|
secondsIntoSlot
|
|
727
841
|
});
|
|
@@ -738,6 +852,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
738
852
|
indexWithinCheckpoint,
|
|
739
853
|
txHashesAlreadyIncluded
|
|
740
854
|
});
|
|
855
|
+
// TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
|
|
741
856
|
if (!buildResult && timingInfo.isLastBlock) {
|
|
742
857
|
break;
|
|
743
858
|
} else if (!buildResult && timingInfo.deadline !== undefined) {
|
|
@@ -749,8 +864,8 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
749
864
|
} else if ('error' in buildResult) {
|
|
750
865
|
// If there was an error building the block, just exit the loop and give up the rest of the slot
|
|
751
866
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
752
|
-
this.log.warn(`Halting block building for slot ${this.
|
|
753
|
-
slot: this.
|
|
867
|
+
this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
|
|
868
|
+
slot: this.targetSlot,
|
|
754
869
|
blocksBuilt,
|
|
755
870
|
error: buildResult.error
|
|
756
871
|
});
|
|
@@ -759,14 +874,13 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
759
874
|
}
|
|
760
875
|
const { block, usedTxs } = buildResult;
|
|
761
876
|
blocksInCheckpoint.push(block);
|
|
762
|
-
|
|
763
|
-
//
|
|
764
|
-
//
|
|
765
|
-
await this.syncProposedBlockToArchiver(block);
|
|
766
|
-
// 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.
|
|
767
880
|
if (timingInfo.isLastBlock) {
|
|
768
|
-
this.
|
|
769
|
-
|
|
881
|
+
await this.syncProposedBlockToArchiver(block);
|
|
882
|
+
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
|
|
883
|
+
slot: this.targetSlot,
|
|
770
884
|
blockNumber,
|
|
771
885
|
blocksBuilt
|
|
772
886
|
});
|
|
@@ -776,17 +890,22 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
776
890
|
};
|
|
777
891
|
break;
|
|
778
892
|
}
|
|
779
|
-
//
|
|
780
|
-
//
|
|
781
|
-
if
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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);
|
|
785
904
|
// Wait until the next block's start time
|
|
786
905
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
787
906
|
}
|
|
788
|
-
this.log.verbose(`Block building loop completed for slot ${this.
|
|
789
|
-
slot: this.
|
|
907
|
+
this.log.verbose(`Block building loop completed for slot ${this.targetSlot}`, {
|
|
908
|
+
slot: this.targetSlot,
|
|
790
909
|
blocksBuilt: blocksInCheckpoint.length
|
|
791
910
|
});
|
|
792
911
|
return {
|
|
@@ -794,89 +913,98 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
794
913
|
blockPendingBroadcast
|
|
795
914
|
};
|
|
796
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
|
+
}
|
|
797
923
|
/** Sleeps until it is time to produce the next block in the slot */ async waitUntilNextSubslot(nextSubslotStart) {
|
|
798
|
-
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.
|
|
924
|
+
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot);
|
|
799
925
|
this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, {
|
|
800
|
-
slot: this.
|
|
926
|
+
slot: this.targetSlot
|
|
801
927
|
});
|
|
802
928
|
await this.waitUntilTimeInSlot(nextSubslotStart);
|
|
803
929
|
}
|
|
804
930
|
/** Builds a single block. Called from the main block building loop. */ async buildSingleBlock(checkpointBuilder, opts) {
|
|
805
931
|
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } = opts;
|
|
806
|
-
this.log.verbose(`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.
|
|
932
|
+
this.log.verbose(`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot}`, {
|
|
807
933
|
...checkpointBuilder.getConstantData(),
|
|
808
934
|
...opts
|
|
809
935
|
});
|
|
810
936
|
try {
|
|
811
937
|
// Wait until we have enough txs to build the block
|
|
812
|
-
const minTxs = this.
|
|
813
|
-
const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
|
|
938
|
+
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
814
939
|
if (!canStartBuilding) {
|
|
815
|
-
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})`, {
|
|
816
941
|
blockNumber,
|
|
817
|
-
slot: this.
|
|
942
|
+
slot: this.targetSlot,
|
|
818
943
|
indexWithinCheckpoint
|
|
819
944
|
});
|
|
820
945
|
this.eventEmitter.emit('block-tx-count-check-failed', {
|
|
821
946
|
minTxs,
|
|
822
947
|
availableTxs,
|
|
823
|
-
slot: this.
|
|
948
|
+
slot: this.targetSlot
|
|
824
949
|
});
|
|
825
950
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
826
951
|
return undefined;
|
|
827
952
|
}
|
|
828
953
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
829
954
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
830
|
-
const pendingTxs = filter(this.p2pClient.
|
|
831
|
-
this.log.debug(`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.
|
|
832
|
-
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,
|
|
833
958
|
blockNumber,
|
|
834
959
|
indexWithinCheckpoint
|
|
835
960
|
});
|
|
836
|
-
this.setStateFn(SequencerState.CREATING_BLOCK, this.
|
|
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;
|
|
837
966
|
const blockBuilderOptions = {
|
|
838
967
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
|
843
974
|
};
|
|
844
|
-
// Actually build the block by executing txs
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
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);
|
|
848
979
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
849
|
-
await this.dropFailedTxsFromP2P(failedTxs);
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
if (!forceCreate && numTxs < minValidTxs) {
|
|
854
|
-
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})`, {
|
|
855
|
-
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,
|
|
856
984
|
blockNumber,
|
|
857
|
-
numTxs,
|
|
858
|
-
indexWithinCheckpoint
|
|
985
|
+
numTxs: buildResult.processedCount,
|
|
986
|
+
indexWithinCheckpoint,
|
|
987
|
+
minValidTxs
|
|
859
988
|
});
|
|
860
|
-
this.eventEmitter.emit('block-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
slot: this.slot
|
|
989
|
+
this.eventEmitter.emit('block-build-failed', {
|
|
990
|
+
reason: `Insufficient valid txs`,
|
|
991
|
+
slot: this.targetSlot
|
|
864
992
|
});
|
|
865
993
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
866
994
|
return undefined;
|
|
867
995
|
}
|
|
868
996
|
// Block creation succeeded, emit stats and metrics
|
|
997
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
|
|
869
998
|
const blockStats = {
|
|
870
999
|
eventName: 'l2-block-built',
|
|
871
1000
|
duration: blockBuildDuration,
|
|
872
1001
|
publicProcessDuration: publicProcessorDuration,
|
|
873
|
-
rollupCircuitsDuration: blockBuildingTimer.ms(),
|
|
874
1002
|
...block.getStats()
|
|
875
1003
|
};
|
|
876
1004
|
const blockHash = await block.hash();
|
|
877
1005
|
const txHashes = block.body.txEffects.map((tx)=>tx.txHash);
|
|
878
|
-
const manaPerSec =
|
|
879
|
-
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`, {
|
|
880
1008
|
blockHash,
|
|
881
1009
|
txHashes,
|
|
882
1010
|
manaPerSec,
|
|
@@ -884,9 +1012,10 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
884
1012
|
});
|
|
885
1013
|
this.eventEmitter.emit('block-proposed', {
|
|
886
1014
|
blockNumber: block.number,
|
|
887
|
-
slot: this.
|
|
1015
|
+
slot: this.targetSlot,
|
|
1016
|
+
buildSlot: this.slotNow
|
|
888
1017
|
});
|
|
889
|
-
this.metrics.recordBuiltBlock(blockBuildDuration,
|
|
1018
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
890
1019
|
return {
|
|
891
1020
|
block,
|
|
892
1021
|
usedTxs
|
|
@@ -894,11 +1023,11 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
894
1023
|
} catch (err) {
|
|
895
1024
|
this.eventEmitter.emit('block-build-failed', {
|
|
896
1025
|
reason: err.message,
|
|
897
|
-
slot: this.
|
|
1026
|
+
slot: this.targetSlot
|
|
898
1027
|
});
|
|
899
1028
|
this.log.error(`Error building block`, err, {
|
|
900
1029
|
blockNumber,
|
|
901
|
-
slot: this.
|
|
1030
|
+
slot: this.targetSlot
|
|
902
1031
|
});
|
|
903
1032
|
this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
|
|
904
1033
|
this.metrics.recordFailedBlock();
|
|
@@ -907,9 +1036,31 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
907
1036
|
};
|
|
908
1037
|
}
|
|
909
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
|
+
}
|
|
910
1060
|
/** Waits until minTxs are available on the pool for building a block. */ async waitForMinTxs(opts) {
|
|
911
|
-
const minTxs = this.config.minTxsPerBlock;
|
|
912
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;
|
|
913
1064
|
// Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
|
|
914
1065
|
const startBuildingDeadline = buildDeadline ? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000) : undefined;
|
|
915
1066
|
let availableTxs = await this.p2pClient.getPendingTxCount();
|
|
@@ -919,22 +1070,24 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
919
1070
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
920
1071
|
return {
|
|
921
1072
|
canStartBuilding: false,
|
|
922
|
-
availableTxs
|
|
1073
|
+
availableTxs,
|
|
1074
|
+
minTxs
|
|
923
1075
|
};
|
|
924
1076
|
}
|
|
925
1077
|
// Wait a bit before checking again
|
|
926
|
-
this.setStateFn(SequencerState.WAITING_FOR_TXS, this.
|
|
927
|
-
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})`, {
|
|
928
1080
|
blockNumber,
|
|
929
|
-
slot: this.
|
|
1081
|
+
slot: this.targetSlot,
|
|
930
1082
|
indexWithinCheckpoint
|
|
931
1083
|
});
|
|
932
|
-
await
|
|
1084
|
+
await this.waitForTxsPollingInterval();
|
|
933
1085
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
934
1086
|
}
|
|
935
1087
|
return {
|
|
936
1088
|
canStartBuilding: true,
|
|
937
|
-
availableTxs
|
|
1089
|
+
availableTxs,
|
|
1090
|
+
minTxs
|
|
938
1091
|
};
|
|
939
1092
|
}
|
|
940
1093
|
/**
|
|
@@ -964,17 +1117,23 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
964
1117
|
return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
|
|
965
1118
|
}
|
|
966
1119
|
const attestationTimeAllowed = this.config.enforceTimeTable ? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_CHECKPOINT) : this.l1Constants.slotDuration;
|
|
967
|
-
const attestationDeadline = new Date(this.
|
|
1120
|
+
const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
|
|
968
1121
|
this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
|
|
969
1122
|
const collectAttestationsTimer = new Timer();
|
|
970
1123
|
let collectedAttestationsCount = 0;
|
|
971
1124
|
try {
|
|
972
1125
|
const attestations = await this.validatorClient.collectAttestations(proposal, numberOfRequiredAttestations, attestationDeadline);
|
|
973
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
|
+
}
|
|
974
1133
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
975
|
-
const sorted = orderAttestations(
|
|
1134
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
976
1135
|
// Manipulate the attestations if we've been configured to do so
|
|
977
|
-
if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
|
|
1136
|
+
if (this.config.injectFakeAttestation || this.config.injectHighSValueAttestation || this.config.injectUnrecoverableSignatureAttestation || this.config.shuffleAttestationOrdering) {
|
|
978
1137
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
979
1138
|
}
|
|
980
1139
|
return new CommitteeAttestationsAndSigners(sorted);
|
|
@@ -991,7 +1150,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
991
1150
|
// Compute the proposer index in the committee, since we dont want to tweak it.
|
|
992
1151
|
// Otherwise, the L1 rollup contract will reject the block outright.
|
|
993
1152
|
const proposerIndex = Number(this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)));
|
|
994
|
-
if (this.config.injectFakeAttestation) {
|
|
1153
|
+
if (this.config.injectFakeAttestation || this.config.injectHighSValueAttestation || this.config.injectUnrecoverableSignatureAttestation) {
|
|
995
1154
|
// Find non-empty attestations that are not from the proposer
|
|
996
1155
|
const nonProposerIndices = [];
|
|
997
1156
|
for(let i = 0; i < attestations.length; i++){
|
|
@@ -1001,8 +1160,16 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1001
1160
|
}
|
|
1002
1161
|
if (nonProposerIndices.length > 0) {
|
|
1003
1162
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
1004
|
-
this.
|
|
1005
|
-
|
|
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
|
+
}
|
|
1006
1173
|
}
|
|
1007
1174
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
1008
1175
|
}
|
|
@@ -1011,14 +1178,25 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1011
1178
|
const shuffled = [
|
|
1012
1179
|
...attestations
|
|
1013
1180
|
];
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
];
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
+
}
|
|
1022
1200
|
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
1023
1201
|
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|
|
1024
1202
|
}
|
|
@@ -1031,14 +1209,13 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1031
1209
|
const failedTxData = failedTxs.map((fail)=>fail.tx);
|
|
1032
1210
|
const failedTxHashes = failedTxData.map((tx)=>tx.getTxHash());
|
|
1033
1211
|
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
1034
|
-
await this.p2pClient.
|
|
1212
|
+
await this.p2pClient.handleFailedExecution(failedTxHashes);
|
|
1035
1213
|
}
|
|
1036
1214
|
/**
|
|
1037
1215
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
1038
1216
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
1039
1217
|
* would never receive its own block without this explicit sync.
|
|
1040
1218
|
*/ async syncProposedBlockToArchiver(block) {
|
|
1041
|
-
// TODO(palla/mbps): Change default to false once block sync is stable.
|
|
1042
1219
|
if (this.config.skipPushProposedBlocksToArchiver !== false) {
|
|
1043
1220
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
1044
1221
|
blockNumber: block.number,
|
|
@@ -1055,27 +1232,50 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
|
|
|
1055
1232
|
/** Runs fee analysis and logs checkpoint outcome as fisherman */ async handleCheckpointEndAsFisherman(checkpoint) {
|
|
1056
1233
|
// Perform L1 fee analysis before clearing requests
|
|
1057
1234
|
// The callback is invoked asynchronously after the next block is mined
|
|
1058
|
-
const feeAnalysis = await this.publisher.analyzeL1Fees(this.
|
|
1235
|
+
const feeAnalysis = await this.publisher.analyzeL1Fees(this.targetSlot, (analysis)=>this.metrics.recordFishermanFeeAnalysis(analysis));
|
|
1059
1236
|
if (checkpoint) {
|
|
1060
|
-
this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.
|
|
1237
|
+
this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.targetSlot}`, {
|
|
1061
1238
|
...checkpoint.toCheckpointInfo(),
|
|
1062
1239
|
...checkpoint.getStats(),
|
|
1063
1240
|
feeAnalysisId: feeAnalysis?.id
|
|
1064
1241
|
});
|
|
1065
1242
|
} else {
|
|
1066
|
-
this.log.warn(`Validation block building FAILED for slot ${this.
|
|
1067
|
-
slot: this.
|
|
1243
|
+
this.log.warn(`Validation block building FAILED for slot ${this.targetSlot}`, {
|
|
1244
|
+
slot: this.targetSlot,
|
|
1068
1245
|
feeAnalysisId: feeAnalysis?.id
|
|
1069
1246
|
});
|
|
1070
|
-
this.metrics.
|
|
1247
|
+
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
1071
1248
|
}
|
|
1072
1249
|
this.publisher.clearPendingRequests();
|
|
1073
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
|
+
}
|
|
1074
1271
|
/** Waits until a specific time within the current slot */ async waitUntilTimeInSlot(targetSecondsIntoSlot) {
|
|
1075
1272
|
const slotStartTimestamp = this.getSlotStartBuildTimestamp();
|
|
1076
1273
|
const targetTimestamp = slotStartTimestamp + targetSecondsIntoSlot;
|
|
1077
1274
|
await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
|
|
1078
1275
|
}
|
|
1276
|
+
/** Waits the polling interval for transactions. Extracted for test overriding. */ async waitForTxsPollingInterval() {
|
|
1277
|
+
await sleep(TXS_POLLING_MS);
|
|
1278
|
+
}
|
|
1079
1279
|
getSlotStartBuildTimestamp() {
|
|
1080
1280
|
return getSlotStartBuildTimestamp(this.slot, this.l1Constants);
|
|
1081
1281
|
}
|