@aztec/sequencer-client 0.0.1-commit.993d240 → 0.0.1-commit.9a89641

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.
Files changed (106) hide show
  1. package/README.md +40 -41
  2. package/dest/client/sequencer-client.d.ts +16 -3
  3. package/dest/client/sequencer-client.d.ts.map +1 -1
  4. package/dest/client/sequencer-client.js +18 -14
  5. package/dest/config.d.ts +9 -4
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +29 -15
  8. package/dest/global_variable_builder/fee_predictor.d.ts +1 -1
  9. package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -1
  10. package/dest/global_variable_builder/fee_predictor.js +11 -1
  11. package/dest/global_variable_builder/fee_provider.d.ts +1 -1
  12. package/dest/global_variable_builder/fee_provider.d.ts.map +1 -1
  13. package/dest/global_variable_builder/fee_provider.js +27 -5
  14. package/dest/global_variable_builder/global_builder.d.ts +3 -16
  15. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  16. package/dest/global_variable_builder/global_builder.js +2 -25
  17. package/dest/index.d.ts +2 -2
  18. package/dest/index.d.ts.map +1 -1
  19. package/dest/index.js +1 -1
  20. package/dest/publisher/config.d.ts +5 -1
  21. package/dest/publisher/config.d.ts.map +1 -1
  22. package/dest/publisher/config.js +11 -1
  23. package/dest/publisher/l1_to_l2_messaging.d.ts +21 -0
  24. package/dest/publisher/l1_to_l2_messaging.d.ts.map +1 -0
  25. package/dest/publisher/l1_to_l2_messaging.js +70 -0
  26. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +48 -8
  27. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
  28. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +68 -1
  29. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +2 -2
  30. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -1
  31. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +4 -2
  32. package/dest/publisher/l1_tx_failed_store/index.d.ts +2 -2
  33. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -1
  34. package/dest/publisher/l1_tx_failed_store/index.js +1 -0
  35. package/dest/publisher/sequencer-publisher.d.ts +39 -10
  36. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  37. package/dest/publisher/sequencer-publisher.js +403 -75
  38. package/dest/publisher/write_json.d.ts +11 -0
  39. package/dest/publisher/write_json.d.ts.map +1 -0
  40. package/dest/publisher/write_json.js +57 -0
  41. package/dest/sequencer/automine/automine_factory.d.ts +5 -3
  42. package/dest/sequencer/automine/automine_factory.d.ts.map +1 -1
  43. package/dest/sequencer/automine/automine_factory.js +2 -1
  44. package/dest/sequencer/automine/automine_sequencer.d.ts +43 -5
  45. package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -1
  46. package/dest/sequencer/automine/automine_sequencer.js +209 -19
  47. package/dest/sequencer/automine/index.d.ts +3 -0
  48. package/dest/sequencer/automine/index.d.ts.map +1 -0
  49. package/dest/sequencer/automine/index.js +2 -0
  50. package/dest/sequencer/checkpoint_proposal_job.d.ts +28 -15
  51. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  52. package/dest/sequencer/checkpoint_proposal_job.js +158 -117
  53. package/dest/sequencer/events.d.ts +16 -4
  54. package/dest/sequencer/events.d.ts.map +1 -1
  55. package/dest/sequencer/index.d.ts +1 -3
  56. package/dest/sequencer/index.d.ts.map +1 -1
  57. package/dest/sequencer/index.js +0 -2
  58. package/dest/sequencer/missing_committee.d.ts +72 -0
  59. package/dest/sequencer/missing_committee.d.ts.map +1 -0
  60. package/dest/sequencer/missing_committee.js +139 -0
  61. package/dest/sequencer/requests_tracker.d.ts +22 -0
  62. package/dest/sequencer/requests_tracker.d.ts.map +1 -0
  63. package/dest/sequencer/requests_tracker.js +33 -0
  64. package/dest/sequencer/sequencer.d.ts +113 -28
  65. package/dest/sequencer/sequencer.d.ts.map +1 -1
  66. package/dest/sequencer/sequencer.js +352 -162
  67. package/dest/test/index.d.ts +3 -3
  68. package/dest/test/index.d.ts.map +1 -1
  69. package/dest/test/utils.d.ts +15 -1
  70. package/dest/test/utils.d.ts.map +1 -1
  71. package/dest/test/utils.js +18 -1
  72. package/package.json +29 -28
  73. package/src/client/sequencer-client.ts +20 -14
  74. package/src/config.ts +32 -16
  75. package/src/global_variable_builder/fee_predictor.ts +11 -1
  76. package/src/global_variable_builder/fee_provider.ts +27 -5
  77. package/src/global_variable_builder/global_builder.ts +2 -34
  78. package/src/index.ts +1 -10
  79. package/src/publisher/config.ts +22 -1
  80. package/src/publisher/l1_to_l2_messaging.ts +85 -0
  81. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +114 -7
  82. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +4 -3
  83. package/src/publisher/l1_tx_failed_store/index.ts +1 -1
  84. package/src/publisher/sequencer-publisher.ts +430 -86
  85. package/src/publisher/write_json.ts +78 -0
  86. package/src/sequencer/README.md +16 -6
  87. package/src/sequencer/automine/README.md +18 -4
  88. package/src/sequencer/automine/automine_factory.ts +8 -1
  89. package/src/sequencer/automine/automine_sequencer.ts +229 -21
  90. package/src/sequencer/automine/index.ts +6 -0
  91. package/src/sequencer/checkpoint_proposal_job.ts +177 -132
  92. package/src/sequencer/events.ts +16 -3
  93. package/src/sequencer/index.ts +0 -2
  94. package/src/sequencer/missing_committee.ts +192 -0
  95. package/src/sequencer/requests_tracker.ts +43 -0
  96. package/src/sequencer/sequencer.ts +382 -176
  97. package/src/test/index.ts +2 -2
  98. package/src/test/utils.ts +33 -0
  99. package/dest/sequencer/chain_state_overrides.d.ts +0 -61
  100. package/dest/sequencer/chain_state_overrides.d.ts.map +0 -1
  101. package/dest/sequencer/chain_state_overrides.js +0 -98
  102. package/dest/sequencer/timetable.d.ts +0 -101
  103. package/dest/sequencer/timetable.d.ts.map +0 -1
  104. package/dest/sequencer/timetable.js +0 -231
  105. package/src/sequencer/chain_state_overrides.ts +0 -162
  106. package/src/sequencer/timetable.ts +0 -295
@@ -435,19 +435,20 @@ function applyDecs2203RFactory() {
435
435
  function _apply_decs_2203_r(targetClass, memberDecs, classDecs, parentClass) {
436
436
  return (_apply_decs_2203_r = applyDecs2203RFactory())(targetClass, memberDecs, classDecs, parentClass);
437
437
  }
438
- var _dec, _dec1, _dec2, _dec3, _dec4, _dec5, _dec6, _dec7, _initProto;
439
- import { BlockNumber, CheckpointNumber, IndexWithinCheckpoint } from '@aztec/foundation/branded-types';
438
+ var _dec, _dec1, _dec2, _dec3, _dec4, _dec5, _dec6, _initProto;
439
+ import { PROPOSER_PIPELINING_SLOT_OFFSET } from '@aztec/epoch-cache';
440
+ import { BlockNumber, CheckpointNumber, IndexWithinCheckpoint, SlotNumber } from '@aztec/foundation/branded-types';
440
441
  import { randomInt } from '@aztec/foundation/crypto/random';
441
442
  import { flipSignature, generateRecoverableSignature, generateUnrecoverableSignature } from '@aztec/foundation/crypto/secp256k1-signer';
443
+ import { InterruptError, TimeoutError } from '@aztec/foundation/error';
442
444
  import { filter } from '@aztec/foundation/iterator';
443
445
  import { createLogger } from '@aztec/foundation/log';
444
- import { retryUntil } from '@aztec/foundation/retry';
445
- import { sleep, sleepUntil } from '@aztec/foundation/sleep';
446
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
446
447
  import { Timer } from '@aztec/foundation/timer';
447
448
  import { isErrorClass, unfreeze } from '@aztec/foundation/types';
448
- import { CommitteeAttestationsAndSigners, MaliciousCommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
449
- import { getPreviousCheckpointOutHashes, validateCheckpoint } from '@aztec/stdlib/checkpoint';
450
- import { computeQuorum, getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
449
+ import { CommitteeAttestationsAndSigners, MaliciousCommitteeAttestationsAndSigners, MaliciousYParityCommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
450
+ import { buildCheckpointSimulationOverridesPlan, getPreviousCheckpointOutHashes, validateCheckpoint } from '@aztec/stdlib/checkpoint';
451
+ import { computeQuorum, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
451
452
  import { Gas } from '@aztec/stdlib/gas';
452
453
  import { InsufficientValidTxsError } from '@aztec/stdlib/interfaces/server';
453
454
  import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
@@ -455,25 +456,24 @@ import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
455
456
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
456
457
  import { Attributes, trackSpan } from '@aztec/telemetry-client';
457
458
  import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
458
- import { buildCheckpointSimulationOverridesPlan } from './chain_state_overrides.js';
459
459
  import { CheckpointVoter } from './checkpoint_voter.js';
460
460
  import { SequencerInterruptedError } from './errors.js';
461
461
  import { SequencerState } from './utils.js';
462
462
  /** How much time to sleep while waiting for min transactions to accumulate for a block */ const TXS_POLLING_MS = 500;
463
+ const ARCHIVER_SYNC_POLLING_MS = 200;
463
464
  _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('CheckpointProposalJob.proposeCheckpoint', function() {
464
465
  return {
465
466
  // nullish operator needed for tests
466
467
  [Attributes.COINBASE]: this.validatorClient.getCoinbaseForAttestor(this.attestorAddress)?.toString(),
467
468
  [Attributes.SLOT_NUMBER]: this.targetSlot
468
469
  };
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');
470
+ }), _dec2 = trackSpan('CheckpointProposalJob.buildBlocksForCheckpoint'), _dec3 = trackSpan('CheckpointProposalJob.waitUntilNextSubslot'), _dec4 = trackSpan('CheckpointProposalJob.buildSingleBlock'), _dec5 = trackSpan('CheckpointProposalJob.waitForMinTxs'), _dec6 = trackSpan('CheckpointProposalJob.waitForAttestations');
470
471
  /**
471
472
  * Handles the execution of a checkpoint proposal after the initial preparation phase.
472
473
  * This includes building blocks, collecting attestations, and publishing the checkpoint to L1,
473
474
  * as well as enqueueing votes for slashing and governance proposals. This class is created from
474
475
  * the Sequencer once the check for being the proposer for the slot has succeeded.
475
476
  */ export class CheckpointProposalJob {
476
- slotNow;
477
477
  targetSlot;
478
478
  targetEpoch;
479
479
  checkpointNumber;
@@ -501,6 +501,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
501
501
  metrics;
502
502
  checkpointMetrics;
503
503
  eventEmitter;
504
+ pendingRequests;
504
505
  setStateFn;
505
506
  tracer;
506
507
  proposedCheckpointData;
@@ -540,17 +541,13 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
540
541
  _dec6,
541
542
  2,
542
543
  "waitForAttestations"
543
- ],
544
- [
545
- _dec7,
546
- 2,
547
- "waitUntilTimeInSlot"
548
544
  ]
549
545
  ], []));
550
546
  }
551
547
  log;
552
548
  checkpointEventLog;
553
- /** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */ pendingL1Submission;
549
+ interruptibleSleep;
550
+ interrupted;
554
551
  /**
555
552
  * Chain state overrides built once per slot in proposeCheckpoint after the checkpoint is
556
553
  * complete. Carries the pending parent override (archive + slot + fee header) for pipelining,
@@ -560,9 +557,10 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
560
557
  getSignatureContext() {
561
558
  return this.signatureContext;
562
559
  }
563
- constructor(slotNow, targetSlot, targetEpoch, checkpointNumber, syncedToBlockNumber, checkpointedCheckpointNumber, // TODO(palla/mbps): Can we remove the proposer in favor of attestorAddress? Need to check fisherman-node flows.
564
- proposer, publisher, attestorAddress, invalidateCheckpoint, validatorClient, globalsBuilder, p2pClient, worldState, l1ToL2MessageSource, l2BlockSource, checkpointsBuilder, blockSink, l1Constants, signatureContext, config, timetable, slasherClient, epochCache, dateProvider, metrics, checkpointMetrics, eventEmitter, setStateFn, tracer, bindings, proposedCheckpointData){
565
- this.slotNow = slotNow;
560
+ constructor(targetSlot, targetEpoch, checkpointNumber, syncedToBlockNumber, checkpointedCheckpointNumber, // TODO(palla/mbps): Can we remove the proposer in favor of attestorAddress? Need to check fisherman-node flows.
561
+ proposer, publisher, attestorAddress, invalidateCheckpoint, validatorClient, globalsBuilder, p2pClient, worldState, l1ToL2MessageSource, l2BlockSource, checkpointsBuilder, blockSink, l1Constants, signatureContext, config, timetable, slasherClient, epochCache, dateProvider, metrics, checkpointMetrics, eventEmitter, // Shared with the owning sequencer, which drains it during shutdown; the fire-and-forget L1
562
+ // submission this job backgrounds is tracked here rather than in a job-local tracker.
563
+ pendingRequests, setStateFn, tracer, bindings, proposedCheckpointData){
566
564
  this.targetSlot = targetSlot;
567
565
  this.targetEpoch = targetEpoch;
568
566
  this.checkpointNumber = checkpointNumber;
@@ -590,22 +588,54 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
590
588
  this.metrics = metrics;
591
589
  this.checkpointMetrics = checkpointMetrics;
592
590
  this.eventEmitter = eventEmitter;
591
+ this.pendingRequests = pendingRequests;
593
592
  this.setStateFn = setStateFn;
594
593
  this.tracer = tracer;
595
594
  this.proposedCheckpointData = proposedCheckpointData;
596
- _initProto(this);
595
+ this.interruptibleSleep = (_initProto(this), new InterruptibleSleep());
596
+ this.interrupted = false;
597
597
  this.log = createLogger('sequencer:checkpoint-proposal', {
598
598
  ...bindings,
599
- instanceId: `slot-${this.slotNow}`
599
+ instanceId: `slot-${this.getBuildSlot()}`
600
600
  });
601
601
  this.checkpointEventLog = createLogger('sequencer:checkpoint-events', {
602
602
  ...bindings,
603
- instanceId: `slot-${this.slotNow}`
603
+ instanceId: `slot-${this.getBuildSlot()}`
604
604
  });
605
605
  }
606
- /** Awaits the pending L1 submission if one is in progress. Call during shutdown. */ async awaitPendingSubmission() {
607
- this.log.info('Awaiting pending L1 payload submission');
608
- await this.pendingL1Submission;
606
+ /**
607
+ * The wall-clock slot during which this job builds, i.e. the slot one before {@link targetSlot} under
608
+ * proposer pipelining. Also the slot of the parent checkpoint this job builds on top of.
609
+ */ getBuildSlot() {
610
+ return SlotNumber(this.targetSlot - PROPOSER_PIPELINING_SLOT_OFFSET);
611
+ }
612
+ /**
613
+ * Sets the sequencer state for this job, reporting the target slot the checkpoint is being proposed for
614
+ * (not the wall-clock build slot). The slot is informational on the event payload/metrics; the job knows
615
+ * its own target slot, so callers only pass the state.
616
+ */ setState(state) {
617
+ this.setStateFn(state, this.targetSlot);
618
+ }
619
+ /** Interrupts job-owned waits, including the publisher's send-at-slot sleep, so shutdown can finish. */ interrupt() {
620
+ this.interrupted = true;
621
+ this.interruptibleSleep.interrupt(true);
622
+ this.publisher.interrupt();
623
+ }
624
+ async awaitInterruptibleSleep(ms) {
625
+ if (this.interrupted) {
626
+ throw new SequencerInterruptedError();
627
+ }
628
+ if (ms <= 0) {
629
+ return;
630
+ }
631
+ try {
632
+ await this.interruptibleSleep.sleep(ms);
633
+ } catch (err) {
634
+ if (err instanceof InterruptError) {
635
+ throw new SequencerInterruptedError();
636
+ }
637
+ throw err;
638
+ }
609
639
  }
610
640
  logCheckpointEvent(eventName, message, fields) {
611
641
  this.checkpointEventLog.debug(message, {
@@ -636,7 +666,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
636
666
  // signature verification to fail silently inside Multicall3. Delay submission to the
637
667
  // start of `targetSlot` so the tx mines in the slot the vote was signed for.
638
668
  if (!this.config.fishermanMode) {
639
- this.pendingL1Submission = this.publisher.sendRequestsAt(this.targetSlot).then(()=>{});
669
+ this.pendingRequests.trackRequest(this.publisher.sendRequestsAt(this.targetSlot), ()=>this.interrupt());
640
670
  }
641
671
  return undefined;
642
672
  }
@@ -648,32 +678,30 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
648
678
  return checkpoint;
649
679
  }
650
680
  // Background the attestation → signing → L1 pipeline so the work loop is unblocked
651
- this.pendingL1Submission = this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises);
681
+ this.pendingRequests.trackRequest(this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises), ()=>this.interrupt());
652
682
  // Return the built checkpoint immediately — the work loop is now unblocked
653
683
  return checkpoint;
654
684
  }
655
685
  /**
656
686
  * Background pipeline: collects attestations, signs them, enqueues the checkpoint, and submits to L1.
657
- * Runs as a fire-and-forget task stored in `pendingL1Submission` so the work loop is unblocked.
687
+ * Runs as a fire-and-forget task tracked in the sequencer's shared tracker so the work loop is unblocked.
658
688
  */ async waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises) {
659
689
  const { checkpoint } = broadcast;
660
- const isPipelining = this.epochCache.isProposerPipeliningEnabled();
661
690
  try {
662
691
  // Wait for all votes actions, enqueued at the beginning, to resolve
663
692
  await Promise.all(votesPromises);
664
693
  // Try to collect attestations from the committee
665
694
  const signedAttestations = await this.getSignedCommitteeAttestations(broadcast);
666
- // If pipelining, wait for the previous checkpoint to land on L1 before submitting,
667
- // so we can check it matches the proposed checkpoint we used as parent, and has valid attestations.
668
- if (signedAttestations && (!isPipelining || await this.waitForValidParentCheckpointOnL1())) {
695
+ // Wait for the previous checkpoint to land on L1 before submitting, so we can check it
696
+ // matches the proposed checkpoint we used as parent, and has valid attestations.
697
+ if (signedAttestations && await this.waitForValidParentCheckpointOnL1()) {
669
698
  await this.enqueueCheckpointForSubmission({
670
699
  checkpoint,
671
700
  ...signedAttestations
672
701
  });
673
702
  }
674
703
  // If we failed to collect attestations, at least check if we need to issue an invalidation
675
- // Note that if we are not pipelining, we enqueued the invalidation at the beginning
676
- if (!signedAttestations && isPipelining && await this.waitForSyncedL2SlotNumber(this.slotNow)) {
704
+ if (!signedAttestations && await this.waitForSyncedL2SlotNumber(this.getBuildSlot())) {
677
705
  const validationStatus = await this.l2BlockSource.getPendingChainValidationStatus();
678
706
  if (!validationStatus.valid) {
679
707
  this.log.warn(`Checkpoint ${validationStatus.checkpoint.checkpointNumber} has invalid attestations, enqueuing invalidation in spite of attestation collection failure`, {
@@ -722,9 +750,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
722
750
  ...l1Response,
723
751
  slot: this.targetSlot
724
752
  });
725
- if (isPipelining) {
726
- this.metrics.recordPipelineDiscard();
727
- }
753
+ this.metrics.recordPipelineDiscard();
728
754
  }
729
755
  } catch (err) {
730
756
  if (err instanceof SequencerInterruptedError) {
@@ -743,17 +769,20 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
743
769
  this.eventEmitter.emit('checkpoint-publish-failed', {
744
770
  slot: this.targetSlot
745
771
  });
746
- if (isPipelining) {
747
- this.metrics.recordPipelineDiscard();
748
- }
772
+ this.metrics.recordPipelineDiscard();
749
773
  }
750
774
  }
751
775
  /** Enqueues the checkpoint for L1 submission. Called after pipeline sleep in execute(). */ async enqueueCheckpointForSubmission(result) {
752
776
  const { checkpoint, attestations, attestationsSignature } = result;
753
- this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot);
754
- const aztecSlotDuration = this.l1Constants.slotDuration;
755
- const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
756
- const txTimeoutAt = new Date((submissionSlotStart + aztecSlotDuration) * 1000);
777
+ this.setState(SequencerState.PUBLISHING_CHECKPOINT);
778
+ // Latest L1 block the propose can still land in for the target slot: the last Ethereum block inside
779
+ // the target slot (`target_slot_start + S - E`). This is one ethereum slot later than
780
+ // `attestation_deadline` (= last_ethereum_block_in_target_slot - E), which bounds when validators must
781
+ // have signed, not when the proposer must have sent. Using the attestation deadline here is too tight:
782
+ // attestations are collected up to (and, when not enforcing, past) it, so the propose tx would be
783
+ // enqueued already expired and time out before it can mine.
784
+ const lastL1BlockInTargetSlot = Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) + this.l1Constants.slotDuration - this.l1Constants.ethereumSlotDuration;
785
+ const txTimeoutAt = new Date(lastL1BlockInTargetSlot * 1000);
757
786
  // If we have been configured to potentially skip publishing checkpoint then roll the dice here
758
787
  if (this.config.skipPublishingCheckpointsPercent !== undefined && this.config.skipPublishingCheckpointsPercent > 0) {
759
788
  const roll = Math.max(0, randomInt(100));
@@ -776,11 +805,21 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
776
805
  const syncDelayTolerance = this.l1Constants.ethereumSlotDuration * 2 * 1000;
777
806
  const timeoutSeconds = Math.max(0.1, (targetSlotEndMs + syncDelayTolerance - this.dateProvider.now()) / 1000);
778
807
  try {
779
- return await retryUntil(async ()=>{
808
+ const timer = new Timer();
809
+ while(true){
780
810
  const syncedSlot = await this.l2BlockSource.getSyncedL2SlotNumber();
781
- return syncedSlot !== undefined && syncedSlot >= waitForSlot;
782
- }, `archiver sync past slot ${waitForSlot}`, timeoutSeconds, 0.2);
783
- } catch {
811
+ if (syncedSlot !== undefined && syncedSlot >= waitForSlot) {
812
+ return true;
813
+ }
814
+ if (timeoutSeconds && timer.s() > timeoutSeconds) {
815
+ throw new TimeoutError(`Timeout awaiting archiver sync past slot ${waitForSlot}`);
816
+ }
817
+ await this.awaitInterruptibleSleep(ARCHIVER_SYNC_POLLING_MS);
818
+ }
819
+ } catch (err) {
820
+ if (err instanceof SequencerInterruptedError) {
821
+ throw err;
822
+ }
784
823
  this.log.warn(`Archiver did not sync L1 past slot ${waitForSlot} before slot ${this.targetSlot} expired, discarding pipelined work`, {
785
824
  checkpointNumber: this.checkpointNumber
786
825
  });
@@ -802,8 +841,8 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
802
841
  return true;
803
842
  }
804
843
  const parentCheckpointNumber = CheckpointNumber(this.checkpointNumber - 1);
805
- // Wait until archiver has synced L1 past the parent's slot (slotNow)
806
- if (!await this.waitForSyncedL2SlotNumber(this.slotNow)) {
844
+ // Wait until archiver has synced L1 past the parent's slot (the build slot, one before targetSlot)
845
+ if (!await this.waitForSyncedL2SlotNumber(this.getBuildSlot())) {
807
846
  return false;
808
847
  }
809
848
  const tips = await this.l2BlockSource.getL2Tips();
@@ -896,11 +935,11 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
896
935
  };
897
936
  try {
898
937
  const now = this.dateProvider.now();
899
- if (this.epochCache.isProposerPipeliningEnabled() && this.proposedCheckpointData) {
938
+ if (this.proposedCheckpointData) {
900
939
  // Measure against the wall-clock slot whose build window we are currently using.
901
940
  // In pipelining mode `targetSlot` is intentionally one slot ahead, which makes the
902
941
  // target-slot boundary a full slot away from the actual build start time.
903
- const slotBoundaryMs = Number(getTimestampForSlot(this.slotNow, this.l1Constants)) * 1000;
942
+ const slotBoundaryMs = Number(getTimestampForSlot(this.getBuildSlot(), this.l1Constants)) * 1000;
904
943
  this.checkpointMetrics.recordPipelinedCheckpointBuildStartOffsetFromSlotBoundary(now - slotBoundaryMs);
905
944
  }
906
945
  this.checkpointMetrics.startCheckpointTiming(now);
@@ -908,13 +947,12 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
908
947
  const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
909
948
  const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
910
949
  // Start the checkpoint
911
- this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.targetSlot);
950
+ this.setState(SequencerState.INITIALIZING_CHECKPOINT);
912
951
  this.logCheckpointEvent('slot-started', `Starting checkpoint proposal for slot ${this.targetSlot}`, {
913
- buildSlot: this.slotNow,
952
+ buildSlot: this.getBuildSlot(),
914
953
  submissionSlot: this.targetSlot,
915
954
  slot: this.targetSlot,
916
955
  checkpointNumber: this.checkpointNumber,
917
- pipelining: this.epochCache.isProposerPipeliningEnabled(),
918
956
  proposer: this.proposer?.toString(),
919
957
  attestorAddress: this.attestorAddress.toString(),
920
958
  publisherAddress: this.publisher.getSenderAddress().toString(),
@@ -929,10 +967,9 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
929
967
  // pending/archive/fee-header to "as if the proposed parent had landed", so both the
930
968
  // mana-min-fee simulation (in the globals builder) and the pre-broadcast
931
969
  // validateBlockHeader see the chain tip the eventual L1 send will see.
932
- const isPipelining = this.epochCache.isProposerPipeliningEnabled();
933
970
  this.checkpointSimulationOverridesPlan = await buildCheckpointSimulationOverridesPlan({
934
971
  checkpointNumber: this.checkpointNumber,
935
- proposedCheckpointData: isPipelining ? this.proposedCheckpointData : undefined,
972
+ proposedCheckpointData: this.proposedCheckpointData,
936
973
  invalidateToPendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
937
974
  checkpointedCheckpointNumber: this.checkpointedCheckpointNumber,
938
975
  rollup: this.publisher.rollupContract,
@@ -953,7 +990,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
953
990
  epoch: this.targetEpoch,
954
991
  checkpointNumber: this.checkpointNumber,
955
992
  l1Constants: this.epochCache.getL1Constants(),
956
- pipeliningEnabled: this.epochCache.isProposerPipeliningEnabled(),
993
+ pipeliningEnabled: true,
957
994
  proposedCheckpointData: this.proposedCheckpointData,
958
995
  log: this.log
959
996
  });
@@ -1028,7 +1065,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1028
1065
  }
1029
1066
  // Assemble and broadcast the checkpoint proposal, including the last block that was not
1030
1067
  // broadcasted yet, and wait to collect the committee attestations.
1031
- this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.targetSlot);
1068
+ this.setState(SequencerState.ASSEMBLING_CHECKPOINT);
1032
1069
  const checkpoint = await checkpointBuilder.completeCheckpoint();
1033
1070
  // Final validation: per-block limits are only checked if the operator set them explicitly.
1034
1071
  // Otherwise, checkpoint-level budgets were already enforced by the redistribution logic.
@@ -1048,7 +1085,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1048
1085
  reason: 'invalid_checkpoint',
1049
1086
  checkpoint: checkpoint.header.toInspect()
1050
1087
  });
1051
- this.log.error(`Built an invalid checkpoint at slot ${this.slotNow} (skipping proposal)`, err, {
1088
+ this.log.error(`Built an invalid checkpoint at slot ${this.targetSlot} (skipping proposal)`, err, {
1052
1089
  slot: this.targetSlot,
1053
1090
  checkpointNumber: this.checkpointNumber,
1054
1091
  blocksBuilt: blocksInCheckpoint.length,
@@ -1061,7 +1098,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1061
1098
  this.checkpointMetrics.recordCheckpointBuild(checkpointBuildTimer.ms(), blocksInCheckpoint.length, checkpoint.getStats().txCount, Number(checkpoint.header.totalManaUsed.toBigInt()));
1062
1099
  this.logCheckpointEvent('built', `Checkpoint built for slot ${this.targetSlot}`, {
1063
1100
  slot: this.targetSlot,
1064
- buildSlot: this.slotNow,
1101
+ buildSlot: this.getBuildSlot(),
1065
1102
  checkpointNumber: this.checkpointNumber,
1066
1103
  proposer: this.proposer?.toString(),
1067
1104
  attestorAddress: this.attestorAddress.toString(),
@@ -1166,13 +1203,13 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1166
1203
  });
1167
1204
  break;
1168
1205
  }
1169
- const secondsIntoSlot = this.getSecondsIntoSlot();
1170
- const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
1206
+ const nowSeconds = this.dateProvider.now() / 1000;
1207
+ const timingInfo = this.timetable.selectNextSubslot(this.targetSlot, nowSeconds);
1171
1208
  if (!timingInfo.canStart) {
1172
1209
  this.log.debug(`Not enough time left in slot to start another block`, {
1173
1210
  slot: this.targetSlot,
1174
1211
  blocksBuilt,
1175
- secondsIntoSlot
1212
+ nowSeconds
1176
1213
  });
1177
1214
  break;
1178
1215
  }
@@ -1181,16 +1218,15 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1181
1218
  blockTimestamp: timestamp,
1182
1219
  // Create an empty block if we haven't already and this is the last one
1183
1220
  forceCreate: timingInfo.isLastBlock && blocksBuilt === 0 && this.config.buildCheckpointIfEmpty,
1184
- // Build deadline is only set if we are enforcing the timetable
1185
- buildDeadline: timingInfo.deadline ? new Date((this.getSlotStartBuildTimestamp() + timingInfo.deadline) * 1000) : undefined,
1221
+ buildDeadline: new Date(timingInfo.deadline * 1000),
1186
1222
  blockNumber,
1187
1223
  indexWithinCheckpoint,
1188
1224
  txHashesAlreadyIncluded
1189
1225
  });
1190
1226
  // If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
1191
1227
  if ('failure' in buildResult) {
1192
- // If this was the last subslot, or we're running with a single block per slot, we're done
1193
- if (timingInfo.isLastBlock || timingInfo.deadline === undefined) {
1228
+ // If this was the last subslot, we're done.
1229
+ if (timingInfo.isLastBlock) {
1194
1230
  break;
1195
1231
  }
1196
1232
  // Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
@@ -1259,12 +1295,15 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1259
1295
  }
1260
1296
  return this.validatorClient.createBlockProposal(block.header, this.checkpointNumber, block.indexWithinCheckpoint, inHash, block.archive.root, usedTxs, this.proposer, blockProposalOptions);
1261
1297
  }
1262
- /** Sleeps until it is time to produce the next block in the slot */ async waitUntilNextSubslot(nextSubslotStart) {
1263
- this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot);
1264
- this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, {
1298
+ /**
1299
+ * Sleeps until it is time to produce the next block in the slot.
1300
+ * @param nextSubslotStart - Absolute wall-clock timestamp in seconds of the previous sub-slot deadline.
1301
+ */ async waitUntilNextSubslot(nextSubslotStart) {
1302
+ this.setState(SequencerState.WAITING_UNTIL_NEXT_BLOCK);
1303
+ this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s`, {
1265
1304
  slot: this.targetSlot
1266
1305
  });
1267
- await this.waitUntilTimeInSlot(nextSubslotStart);
1306
+ await this.awaitInterruptibleSleep(Math.max(0, nextSubslotStart * 1000 - this.dateProvider.now()));
1268
1307
  }
1269
1308
  /** Builds a single block. Called from the main block building loop. */ async buildSingleBlock(checkpointBuilder, opts) {
1270
1309
  const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } = opts;
@@ -1274,7 +1313,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1274
1313
  });
1275
1314
  try {
1276
1315
  // Wait until we have enough txs to build the block
1277
- const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
1316
+ const { canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
1278
1317
  if (!canStartBuilding) {
1279
1318
  this.logCheckpointEvent('block-build-failed', `Block build failed for slot ${this.targetSlot}`, {
1280
1319
  reason: 'insufficient_txs',
@@ -1282,21 +1321,18 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1282
1321
  slot: this.targetSlot,
1283
1322
  checkpointNumber: this.checkpointNumber,
1284
1323
  indexWithinCheckpoint,
1285
- availableTxs,
1286
1324
  minTxs
1287
1325
  });
1288
- this.log.warn(`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (got ${availableTxs} txs but needs ${minTxs})`, {
1326
+ this.log.verbose(`Not enough age-eligible txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (needs ${minTxs} eligible)`, {
1289
1327
  reason: 'insufficient_txs',
1290
1328
  blockNumber,
1291
1329
  slot: this.targetSlot,
1292
1330
  checkpointNumber: this.checkpointNumber,
1293
1331
  indexWithinCheckpoint,
1294
- availableTxs,
1295
1332
  minTxs
1296
1333
  });
1297
1334
  this.eventEmitter.emit('block-tx-count-check-failed', {
1298
1335
  minTxs,
1299
- availableTxs,
1300
1336
  slot: this.targetSlot
1301
1337
  });
1302
1338
  this.metrics.recordBlockProposalFailed('insufficient_txs');
@@ -1306,13 +1342,17 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1306
1342
  }
1307
1343
  // Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
1308
1344
  // just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
1309
- const pendingTxs = filter(this.p2pClient.iterateEligiblePendingTxs(), (tx)=>!txHashesAlreadyIncluded.has(tx.txHash.toString()));
1310
- this.log.debug(`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot} with ${availableTxs} available txs`, {
1345
+ // Block building only executes txs, so we skip loading their proofs unless these same tx objects get attached
1346
+ // to the broadcasted proposals via publishTxsWithProposals.
1347
+ const pendingTxs = filter(this.p2pClient.iterateEligiblePendingTxs({
1348
+ includeProof: !!this.config.publishTxsWithProposals
1349
+ }), (tx)=>!txHashesAlreadyIncluded.has(tx.txHash.toString()));
1350
+ this.log.debug(`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot}`, {
1311
1351
  slot: this.targetSlot,
1312
1352
  blockNumber,
1313
1353
  indexWithinCheckpoint
1314
1354
  });
1315
- this.setStateFn(SequencerState.CREATING_BLOCK, this.targetSlot);
1355
+ this.setState(SequencerState.CREATING_BLOCK);
1316
1356
  // Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
1317
1357
  // by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
1318
1358
  // minValidTxs is passed into the builder so it can reject the block *before* updating state.
@@ -1323,8 +1363,9 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1323
1363
  deadline: buildDeadline,
1324
1364
  isBuildingProposal: true,
1325
1365
  minValidTxs,
1326
- maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
1327
- perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier
1366
+ maxBlocksPerCheckpoint: this.timetable.getMaxBlocksPerCheckpoint(),
1367
+ perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
1368
+ perBlockDAAllocationMultiplier: this.config.perBlockDAAllocationMultiplier
1328
1369
  };
1329
1370
  // Actually build the block by executing txs. The builder throws InsufficientValidTxsError
1330
1371
  // if the number of successfully processed txs is below minValidTxs, ensuring state is not
@@ -1385,7 +1426,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1385
1426
  checkpointNumber: this.checkpointNumber,
1386
1427
  indexWithinCheckpoint: block.indexWithinCheckpoint,
1387
1428
  slot: this.targetSlot,
1388
- buildSlot: this.slotNow
1429
+ buildSlot: this.getBuildSlot()
1389
1430
  });
1390
1431
  this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe(), this.targetSlot);
1391
1432
  return {
@@ -1441,38 +1482,37 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1441
1482
  const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
1442
1483
  // We only allow a block with 0 txs in the first block of the checkpoint
1443
1484
  const minTxs = indexWithinCheckpoint > 0 && this.config.minTxsPerBlock === 0 ? 1 : this.config.minTxsPerBlock;
1444
- // Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
1445
- const startBuildingDeadline = buildDeadline ? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000) : undefined;
1446
- let availableTxs = await this.p2pClient.getPendingTxCount();
1447
- while(!forceCreate && availableTxs < minTxs){
1485
+ // Latest time to keep waiting for txs: wait_for_txs_deadline = block_build_deadline(k) - min_block_duration.
1486
+ const startBuildingDeadline = buildDeadline ? new Date(buildDeadline.getTime() - this.timetable.minBlockDuration * 1000) : undefined;
1487
+ // Gate on age-eligible txs so we don't start building on txs the builder would then filter out for being
1488
+ // too fresh. hasEligiblePendingTxs early-exits once minTxs are eligible instead of counting the whole pool.
1489
+ while(!forceCreate && !await this.p2pClient.hasEligiblePendingTxs(minTxs)){
1448
1490
  // If we're past deadline, or we have no deadline, give up
1449
1491
  const now = this.dateProvider.nowAsDate();
1450
1492
  if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
1451
1493
  return {
1452
1494
  canStartBuilding: false,
1453
- availableTxs,
1454
1495
  minTxs
1455
1496
  };
1456
1497
  }
1457
1498
  // Wait a bit before checking again
1458
- this.setStateFn(SequencerState.WAITING_FOR_TXS, this.targetSlot);
1459
- this.log.verbose(`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (have ${availableTxs} but need ${minTxs})`, {
1499
+ this.setState(SequencerState.WAITING_FOR_TXS);
1500
+ this.log.verbose(`Waiting for ${minTxs} age-eligible txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot}`, {
1460
1501
  blockNumber,
1461
1502
  slot: this.targetSlot,
1462
- indexWithinCheckpoint
1503
+ indexWithinCheckpoint,
1504
+ minTxs
1463
1505
  });
1464
1506
  await this.waitForTxsPollingInterval();
1465
- availableTxs = await this.p2pClient.getPendingTxCount();
1466
1507
  }
1467
1508
  return {
1468
1509
  canStartBuilding: true,
1469
- availableTxs,
1470
1510
  minTxs
1471
1511
  };
1472
1512
  }
1473
1513
  async getSignedCommitteeAttestations(broadcast) {
1474
1514
  const { proposal, blockProposedAt } = broadcast;
1475
- this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
1515
+ this.setState(SequencerState.COLLECTING_ATTESTATIONS);
1476
1516
  const attestations = await this.waitForAttestations(proposal);
1477
1517
  if (!attestations) {
1478
1518
  return undefined;
@@ -1523,9 +1563,10 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1523
1563
  });
1524
1564
  return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee), this.getSignatureContext());
1525
1565
  }
1526
- const attestationTimeAllowed = this.config.enforceTimeTable ? this.timetable.getCheckpointAttestationDeadline() : this.l1Constants.slotDuration;
1527
- const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
1528
- this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
1566
+ // Hard attestation-collection cutoff = the single consensus attestation_deadline (target_slot_start + S - 2E).
1567
+ const attestationDeadlineSeconds = this.timetable.getAttestationDeadline(this.targetSlot);
1568
+ const attestationDeadline = new Date(attestationDeadlineSeconds * 1000);
1569
+ this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, Math.max(0, attestationDeadline.getTime() - this.dateProvider.now()));
1529
1570
  const collectAttestationsTimer = new Timer();
1530
1571
  let collectedAttestationsCount = 0;
1531
1572
  try {
@@ -1543,7 +1584,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1543
1584
  submittedCount: trimmed.length
1544
1585
  });
1545
1586
  // Manipulate the attestations if we've been configured to do so
1546
- if (this.config.injectFakeAttestation || this.config.injectHighSValueAttestation || this.config.injectUnrecoverableSignatureAttestation || this.config.shuffleAttestationOrdering) {
1587
+ if (this.config.injectFakeAttestation || this.config.injectHighSValueAttestation || this.config.injectUnrecoverableSignatureAttestation || this.config.injectYParityAttestation || this.config.shuffleAttestationOrdering) {
1547
1588
  return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
1548
1589
  }
1549
1590
  return new CommitteeAttestationsAndSigners(sorted, this.getSignatureContext());
@@ -1615,6 +1656,14 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1615
1656
  }
1616
1657
  return new CommitteeAttestationsAndSigners(attestations, this.getSignatureContext());
1617
1658
  }
1659
+ if (this.config.injectYParityAttestation) {
1660
+ // Force every non-proposer signed slot's recovery byte to yParity (v ∈ {0, 1}) form in the packed L1
1661
+ // tuple, after packAttestations has canonicalized it. The proposer's own slot is left canonical so
1662
+ // propose() still passes verifyProposer. Models a malicious proposer landing a checkpoint L1 accepts
1663
+ // but that can never be proven (ECDSA.recover rejects v ∉ {27, 28}).
1664
+ this.log.warn(`Injecting yParity attestations in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
1665
+ return new MaliciousYParityCommitteeAttestationsAndSigners(attestations, proposerIndex, this.getSignatureContext());
1666
+ }
1618
1667
  if (this.config.shuffleAttestationOrdering) {
1619
1668
  this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
1620
1669
  const shuffled = [
@@ -1650,7 +1699,15 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1650
1699
  }
1651
1700
  const failedTxData = failedTxs.map((fail)=>fail.tx);
1652
1701
  const failedTxHashes = failedTxData.map((tx)=>tx.getTxHash());
1653
- this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
1702
+ const failures = failedTxs.map((fail)=>({
1703
+ txHash: fail.tx.getTxHash().toString(),
1704
+ reason: fail.error.message
1705
+ }));
1706
+ this.log.warn(`Dropping ${failedTxs.length} txs from mempool due to failures during block building for slot ${this.targetSlot}`, {
1707
+ slot: this.targetSlot,
1708
+ checkpointNumber: this.checkpointNumber,
1709
+ failures
1710
+ });
1654
1711
  await this.p2pClient.handleFailedExecution(failedTxHashes);
1655
1712
  }
1656
1713
  /**
@@ -1680,8 +1737,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1680
1737
  * tip advances locally. Gossip doesn't echo our own messages back, so without this the proposer
1681
1738
  * would never see its own proposed checkpoint and couldn't pipeline the next slot.
1682
1739
  *
1683
- * Only runs under proposer pipelining (the proposed tip is a pipelining-only concept) and is
1684
- * skipped whenever proposed blocks aren't pushed (`skipPushProposedBlocksToArchiver`, fisherman
1740
+ * Skipped whenever proposed blocks aren't pushed (`skipPushProposedBlocksToArchiver`, fisherman
1685
1741
  * mode): the archiver derives the checkpoint archive from its stored blocks, so without them the
1686
1742
  * push would fail. All blocks were already added (and awaited) during block building, so this
1687
1743
  * needs no retry — they are guaranteed present by the time we get here.
@@ -1689,9 +1745,6 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1689
1745
  if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
1690
1746
  return;
1691
1747
  }
1692
- if (!this.epochCache.isProposerPipeliningEnabled()) {
1693
- return;
1694
- }
1695
1748
  const startBlock = BlockNumber(this.syncedToBlockNumber + 1);
1696
1749
  this.log.debug(`Syncing proposed checkpoint ${this.checkpointNumber} to archiver`, {
1697
1750
  checkpointNumber: this.checkpointNumber,
@@ -1747,20 +1800,8 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1747
1800
  }
1748
1801
  return false;
1749
1802
  }
1750
- /** Waits until a specific time within the current slot */ async waitUntilTimeInSlot(targetSecondsIntoSlot) {
1751
- const slotStartTimestamp = this.getSlotStartBuildTimestamp();
1752
- const targetTimestamp = slotStartTimestamp + targetSecondsIntoSlot;
1753
- await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
1754
- }
1755
1803
  /** Waits the polling interval for transactions. Extracted for test overriding. */ async waitForTxsPollingInterval() {
1756
- await sleep(TXS_POLLING_MS);
1757
- }
1758
- getSlotStartBuildTimestamp() {
1759
- return getSlotStartBuildTimestamp(this.slotNow, this.l1Constants);
1760
- }
1761
- getSecondsIntoSlot() {
1762
- const slotStartTimestamp = this.getSlotStartBuildTimestamp();
1763
- return Number((this.dateProvider.now() / 1000 - slotStartTimestamp).toFixed(3));
1804
+ await this.awaitInterruptibleSleep(TXS_POLLING_MS);
1764
1805
  }
1765
1806
  getPublisher() {
1766
1807
  return this.publisher;