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