@aztec/sequencer-client 5.0.0-rc.1 → 5.0.0-rc.2

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 (35) hide show
  1. package/dest/config.js +1 -1
  2. package/dest/global_variable_builder/global_builder.d.ts +3 -16
  3. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  4. package/dest/global_variable_builder/global_builder.js +2 -25
  5. package/dest/publisher/config.d.ts +5 -1
  6. package/dest/publisher/config.d.ts.map +1 -1
  7. package/dest/publisher/config.js +11 -1
  8. package/dest/publisher/sequencer-publisher.d.ts +17 -4
  9. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  10. package/dest/publisher/sequencer-publisher.js +113 -20
  11. package/dest/sequencer/automine/automine_sequencer.d.ts +8 -3
  12. package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -1
  13. package/dest/sequencer/automine/automine_sequencer.js +16 -4
  14. package/dest/sequencer/checkpoint_proposal_job.d.ts +1 -1
  15. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  16. package/dest/sequencer/checkpoint_proposal_job.js +2 -3
  17. package/dest/sequencer/sequencer.d.ts +9 -7
  18. package/dest/sequencer/sequencer.d.ts.map +1 -1
  19. package/dest/sequencer/sequencer.js +46 -41
  20. package/dest/test/utils.d.ts +1 -1
  21. package/dest/test/utils.d.ts.map +1 -1
  22. package/dest/test/utils.js +1 -1
  23. package/package.json +27 -27
  24. package/src/config.ts +1 -1
  25. package/src/global_variable_builder/global_builder.ts +2 -34
  26. package/src/publisher/config.ts +22 -1
  27. package/src/publisher/sequencer-publisher.ts +108 -21
  28. package/src/sequencer/automine/automine_sequencer.ts +16 -4
  29. package/src/sequencer/checkpoint_proposal_job.ts +3 -5
  30. package/src/sequencer/sequencer.ts +52 -48
  31. package/src/test/utils.ts +1 -0
  32. package/dest/sequencer/chain_state_overrides.d.ts +0 -61
  33. package/dest/sequencer/chain_state_overrides.d.ts.map +0 -1
  34. package/dest/sequencer/chain_state_overrides.js +0 -98
  35. package/src/sequencer/chain_state_overrides.ts +0 -169
@@ -1,7 +1,12 @@
1
1
  import { type BlobClientConfig, blobClientConfigMapping } from '@aztec/blob-client/client/config';
2
2
  import { type L1ReaderConfig, l1ReaderConfigMappings } from '@aztec/ethereum/l1-reader';
3
3
  import { type L1TxUtilsConfig, l1TxUtilsConfigMappings } from '@aztec/ethereum/l1-tx-utils/config';
4
- import { type ConfigMappingsType, SecretValue, booleanConfigHelper } from '@aztec/foundation/config';
4
+ import {
5
+ type ConfigMappingsType,
6
+ SecretValue,
7
+ booleanConfigHelper,
8
+ numberConfigHelper,
9
+ } from '@aztec/foundation/config';
5
10
  import { EthAddress } from '@aztec/foundation/eth-address';
6
11
 
7
12
  import { parseEther } from 'viem';
@@ -90,6 +95,10 @@ export type SequencerPublisherConfig = L1TxUtilsConfig &
90
95
  fishermanMode?: boolean;
91
96
  sequencerPublisherAllowInvalidStates?: boolean;
92
97
  sequencerPublisherForwarderAddress?: EthAddress;
98
+ /** How long to wait for the previous L1 block before sending scheduled publisher txs anyway. */
99
+ sequencerPublisherPreviousL1BlockWaitTimeoutMs: number;
100
+ /** Poll interval while waiting for the previous L1 block before scheduled publisher txs. */
101
+ sequencerPublisherPreviousL1BlockWaitPollIntervalMs: number;
93
102
  /** Store for failed L1 transaction inputs (test networks only). Format: gs://bucket/path */
94
103
  l1TxFailedStore?: string;
95
104
  /** Min ETH balance below which a publisher gets funded. Undefined = funding disabled. */
@@ -170,6 +179,18 @@ export const sequencerPublisherConfigMappings: ConfigMappingsType<SequencerPubli
170
179
  description: 'Address of the forwarder contract to wrap all L1 transactions through (for testing purposes only)',
171
180
  parseEnv: (val: string) => EthAddress.fromString(val),
172
181
  },
182
+ sequencerPublisherPreviousL1BlockWaitTimeoutMs: {
183
+ env: `SEQ_PUBLISHER_PREVIOUS_L1_BLOCK_WAIT_TIMEOUT_MS`,
184
+ description:
185
+ 'How long to wait for the previous L1 block before sending scheduled publisher txs anyway, in milliseconds.',
186
+ ...numberConfigHelper(8_000),
187
+ },
188
+ sequencerPublisherPreviousL1BlockWaitPollIntervalMs: {
189
+ env: `SEQ_PUBLISHER_PREVIOUS_L1_BLOCK_WAIT_POLL_INTERVAL_MS`,
190
+ description:
191
+ 'Poll interval while waiting for the previous L1 block before scheduled publisher txs, in milliseconds.',
192
+ ...numberConfigHelper(500),
193
+ },
173
194
  l1TxFailedStore: {
174
195
  env: 'L1_TX_FAILED_STORE',
175
196
  description: 'Store for failed L1 transaction inputs (test networks only). Format: gs://bucket/path',
@@ -117,6 +117,7 @@ type L1ProcessArgs = {
117
117
  export const Actions = [
118
118
  'invalidate-by-invalid-attestation',
119
119
  'invalidate-by-insufficient-attestations',
120
+ 'prune',
120
121
  'propose',
121
122
  'governance-signal',
122
123
  'vote-offenses',
@@ -177,6 +178,8 @@ export class SequencerPublisher {
177
178
  protected log: Logger;
178
179
  protected ethereumSlotDuration: bigint;
179
180
  protected aztecSlotDuration: bigint;
181
+ private readonly previousL1BlockWaitTimeoutMs: number;
182
+ private readonly previousL1BlockWaitPollIntervalMs: number;
180
183
 
181
184
  /** Date provider for wall-clock time. */
182
185
  private readonly dateProvider: DateProvider;
@@ -205,7 +208,13 @@ export class SequencerPublisher {
205
208
  protected requests: RequestWithExpiry[] = [];
206
209
 
207
210
  constructor(
208
- private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
211
+ private config: Pick<
212
+ SequencerPublisherConfig,
213
+ | 'fishermanMode'
214
+ | 'l1TxFailedStore'
215
+ | 'sequencerPublisherPreviousL1BlockWaitTimeoutMs'
216
+ | 'sequencerPublisherPreviousL1BlockWaitPollIntervalMs'
217
+ > &
209
218
  Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
210
219
  deps: {
211
220
  telemetry?: TelemetryClient;
@@ -225,6 +234,8 @@ export class SequencerPublisher {
225
234
  this.log = deps.log ?? createLogger('sequencer:publisher');
226
235
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
227
236
  this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
237
+ this.previousL1BlockWaitTimeoutMs = config.sequencerPublisherPreviousL1BlockWaitTimeoutMs;
238
+ this.previousL1BlockWaitPollIntervalMs = config.sequencerPublisherPreviousL1BlockWaitPollIntervalMs;
228
239
  this.dateProvider = deps.dateProvider;
229
240
  this.epochCache = deps.epochCache;
230
241
  this.lastActions = deps.lastActions;
@@ -617,34 +628,69 @@ export class SequencerPublisher {
617
628
 
618
629
  /*
619
630
  * Schedules sending all enqueued requests at (or after) the start of the given L2 slot.
620
- * Sleeps until one L1 slot before the L2 slot boundary so the tx has a chance of being
621
- * picked up by the first L1 block of the L2 slot.
622
- * NB: there is a known correctness risk — being included in the L1 block right before the
623
- * L2 slot starts would revert propose with HeaderLib__InvalidSlotNumber.
624
- * Uses InterruptibleSleep so it can be cancelled via interrupt().
625
631
  */
626
632
  public async sendRequestsAt(targetSlot: SlotNumber): Promise<SendRequestsResult | undefined> {
627
- const l1Constants = this.epochCache.getL1Constants();
628
- // Start of the target L2 slot, in ms (getTimestampForSlot returns seconds).
629
- const startOfTargetSlotMs = Number(getTimestampForSlot(targetSlot, l1Constants)) * 1000;
630
- // Aim to be in the mempool one L1 slot before the L2 slot starts, so we have a chance of
631
- // being picked up by the first L1 block of the L2 slot.
632
- const submitAfterMs = startOfTargetSlotMs - Number(this.ethereumSlotDuration) * 1000;
633
+ await this.waitForTargetSlot(targetSlot);
633
634
  if (this.interrupted) {
634
635
  return undefined;
635
636
  }
636
- const sleepMs = submitAfterMs - this.dateProvider.now();
637
- if (sleepMs > 0) {
638
- this.log.debug(`Sleeping ${sleepMs}ms before sending requests`, {
639
- targetSlot,
640
- submitAfterMs,
641
- });
637
+
638
+ return this.sendRequests(targetSlot);
639
+ }
640
+
641
+ /**
642
+ * Sleeps until one L1 slot before the L2 slot boundary, and then waits for that L1 block
643
+ * to be mined, so we don't risk being included in it. If that block never gets mined after
644
+ * a timeout, we assume it got skipped on L1, so we send the tx anyway.
645
+ */
646
+ private async waitForTargetSlot(targetSlot: SlotNumber): Promise<void> {
647
+ const l1Constants = this.epochCache.getL1Constants();
648
+ const nowInSeconds = this.dateProvider.nowInSeconds();
649
+ const startOfTargetSlotTs = getTimestampForSlot(targetSlot, l1Constants);
650
+ const previousL1BlockTs = startOfTargetSlotTs - this.ethereumSlotDuration;
651
+ const waitDeadlineTs = previousL1BlockTs + BigInt(this.previousL1BlockWaitTimeoutMs / 1000);
652
+ const logCtx = { targetSlot, startOfTargetSlotTs, nowInSeconds, previousL1BlockTs, waitDeadlineTs };
653
+
654
+ // Check if we are already past time
655
+ if (nowInSeconds >= startOfTargetSlotTs) {
656
+ this.log.verbose(`Target slot ${targetSlot} already started, sending requests immediately`, logCtx);
657
+ return;
658
+ }
659
+
660
+ // Otherwise we wait
661
+ this.log.debug(`Waiting for slot ${targetSlot} before sending requests`, logCtx);
662
+
663
+ // Wait until previous L1 block timestamp first
664
+ const sleepMs = (Number(previousL1BlockTs) - nowInSeconds) * 1000;
665
+ if (sleepMs > 0 && !this.interrupted) {
666
+ this.log.trace(`Sleeping ${sleepMs}ms before waiting for previous L1 block`, logCtx);
642
667
  await this.interruptibleSleep.sleep(sleepMs);
643
668
  }
644
- if (this.interrupted) {
645
- return undefined;
669
+
670
+ // Then loop until we see the previous L1 block, so we know that we cannot be included in it.
671
+ // We time out after a while, once we are sure that that block is skipped in L1.
672
+ while (!this.interrupted) {
673
+ try {
674
+ const nowInSeconds = this.dateProvider.nowInSeconds();
675
+ logCtx.nowInSeconds = nowInSeconds;
676
+
677
+ if (nowInSeconds >= waitDeadlineTs) {
678
+ this.log.warn(`Timed out waiting for previous L1 block before sending requests, proceeding`, logCtx);
679
+ return;
680
+ }
681
+
682
+ const latestBlockTs = await this.l1TxUtils.getBlock().then(b => b.timestamp);
683
+ if (latestBlockTs >= previousL1BlockTs) {
684
+ this.log.debug(`Previous L1 block mined, proceeding to send requests`, { ...logCtx, latestBlockTs });
685
+ return;
686
+ }
687
+ this.log.trace(`Previous L1 block not mined yet, continuing to wait`, { ...logCtx, latestBlockTs });
688
+ } catch (err) {
689
+ this.log.error(`Error while waiting for previous L1 block before sending requests; retrying`, err, logCtx);
690
+ } finally {
691
+ await this.interruptibleSleep.sleep(this.previousL1BlockWaitPollIntervalMs);
692
+ }
646
693
  }
647
- return this.sendRequests(targetSlot);
648
694
  }
649
695
 
650
696
  private callbackBundledTransactions(
@@ -1015,6 +1061,47 @@ export class SequencerPublisher {
1015
1061
  );
1016
1062
  }
1017
1063
 
1064
+ /**
1065
+ * Enqueues a `prune()` transaction if the rollup is prunable at the given slot's L1 timestamp.
1066
+ * `prune()` is permissionless and idempotent — if the chain is no longer prunable by send time the
1067
+ * bundle simulation usually drops the entry; on a node without `eth_simulateV1` the bundle is sent
1068
+ * as-is and the prune reverts `Rollup__NothingToPrune` inside `aggregate3(allowFailure: true)`
1069
+ * (a failed action, never a whole-tx revert). Used by the failed-sync fallback so a stuck pending
1070
+ * chain (e.g. bad data blocking sync) can be wound back to recover.
1071
+ * @returns true if a prune request was enqueued, false otherwise.
1072
+ */
1073
+ public async enqueuePruneIfPrunable(slotNumber: SlotNumber): Promise<boolean> {
1074
+ if (this.lastActions['prune'] === slotNumber) {
1075
+ this.log.debug(`Skipping duplicate prune for slot ${slotNumber}`, { slotNumber });
1076
+ return false;
1077
+ }
1078
+ // Use the SAME timestamp the bundle simulator overrides block.timestamp with at send time
1079
+ // (sequencer-bundle-simulator.ts) so this upfront check and the send-time sim agree. Slot-start
1080
+ // and last-L1-slot both fall within the same L2 slot (and epoch, which is what `canPruneAtTime`
1081
+ // derives), so they agree today; matching the simulator keeps it robust if the contract ever uses
1082
+ // the timestamp more granularly.
1083
+ const ts = getLastL1SlotTimestampForL2Slot(slotNumber, this.epochCache.getL1Constants());
1084
+ const canPrune = await this.rollupContract.canPruneAtTime(ts).catch(err => {
1085
+ this.log.error(`Failed to check canPruneAtTime for slot ${slotNumber}`, err, { slotNumber });
1086
+ return false;
1087
+ });
1088
+ if (!canPrune) {
1089
+ this.log.debug(`Rollup not prunable at slot ${slotNumber}`, { slotNumber });
1090
+ return false;
1091
+ }
1092
+ const request: L1TxRequest = {
1093
+ to: this.rollupContract.address,
1094
+ data: encodeFunctionData({ abi: RollupAbi, functionName: 'prune', args: [] }),
1095
+ };
1096
+ this.log.info(`Enqueuing rollup prune for slot ${slotNumber}`, { slotNumber });
1097
+ return this.enqueueRequest(
1098
+ 'prune',
1099
+ request,
1100
+ { address: this.rollupContract.address, abi: RollupAbi, eventName: 'PrunedPending' },
1101
+ slotNumber,
1102
+ );
1103
+ }
1104
+
1018
1105
  /** Enqueues all slashing actions as returned by the slasher client. */
1019
1106
  public async enqueueSlashingActions(
1020
1107
  actions: ProposerSlashAction[],
@@ -274,14 +274,22 @@ export class AutomineSequencer {
274
274
  /**
275
275
  * Warps L1 timestamp to `targetTimestampSec`. Rounded up to the next aztec-slot
276
276
  * boundary so the next build lands on a fresh slot. Atomic with respect to builds —
277
- * the queue ensures no build is in flight while the warp executes.
277
+ * the queue ensures no build is in flight while the warp executes. A no-op when the
278
+ * target is already at or behind the current L1 time (see {@link runWarp}).
278
279
  */
279
280
  public warpTo(targetTimestampSec: number): Promise<void> {
280
281
  return this.queue.put(() => this.runWarp(targetTimestampSec));
281
282
  }
282
283
 
283
- /** Warps L1 timestamp forward by `deltaSec` seconds from the current L1 time. */
284
+ /**
285
+ * Warps L1 timestamp forward by `deltaSec` seconds from the current L1 time, rounded up to the next
286
+ * aztec-slot boundary. Throws if `deltaSec` is not positive (warping "by" a non-positive amount is a
287
+ * caller bug — unlike {@link warpTo}, which no-ops on a past target).
288
+ */
284
289
  public warpBy(deltaSec: number): Promise<void> {
290
+ if (deltaSec <= 0) {
291
+ throw new Error(`warpL2TimeAtLeastBy: duration must be positive, got ${deltaSec} seconds.`);
292
+ }
285
293
  return this.queue.put(async () => {
286
294
  const current = await this.deps.ethCheatCodes.lastBlockTimestamp();
287
295
  await this.runWarp(current + deltaSec);
@@ -409,7 +417,10 @@ export class AutomineSequencer {
409
417
  await this.deps.ethCheatCodes.setNextBlockTimestamp(slotBoundaryTs);
410
418
  }
411
419
 
412
- const tips = await this.deps.l2BlockSource.getL2Tips();
420
+ const [tips, proposedCheckpoint] = await Promise.all([
421
+ this.deps.l2BlockSource.getL2Tips(),
422
+ this.deps.l2BlockSource.getProposedCheckpointData(),
423
+ ]);
413
424
  const syncedToBlockNumber = tips.proposed.number;
414
425
 
415
426
  // Ensure world state has processed the archiver's tip before forking. Without this,
@@ -419,7 +430,8 @@ export class AutomineSequencer {
419
430
  await this.deps.worldState.syncImmediate(BlockNumber(syncedToBlockNumber));
420
431
 
421
432
  const nextBlockNumber = BlockNumber(syncedToBlockNumber + 1);
422
- const checkpointNumber = CheckpointNumber(tips.proposedCheckpoint.checkpoint.number + 1);
433
+ const parentCheckpointNumber = proposedCheckpoint?.checkpointNumber ?? tips.checkpointed.checkpoint.number;
434
+ const checkpointNumber = CheckpointNumber(parentCheckpointNumber + 1);
423
435
  const targetEpoch = getEpochAtSlot(SlotNumber(targetSlot), this.deps.l1Constants);
424
436
 
425
437
  this.log.verbose(`Building automine checkpoint`, {
@@ -37,6 +37,7 @@ import {
37
37
  import {
38
38
  type Checkpoint,
39
39
  type ProposedCheckpointData,
40
+ buildCheckpointSimulationOverridesPlan,
40
41
  getPreviousCheckpointOutHashes,
41
42
  validateCheckpoint,
42
43
  } from '@aztec/stdlib/checkpoint';
@@ -68,7 +69,6 @@ import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validato
68
69
 
69
70
  import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
70
71
  import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
71
- import { buildCheckpointSimulationOverridesPlan } from './chain_state_overrides.js';
72
72
  import type { CheckpointProposalJobMetricsRecorder } from './checkpoint_proposal_job_metrics.js';
73
73
  import { CheckpointVoter } from './checkpoint_voter.js';
74
74
  import { SequencerInterruptedError } from './errors.js';
@@ -398,9 +398,7 @@ export class CheckpointProposalJob implements Traceable {
398
398
  }
399
399
  }
400
400
 
401
- await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
402
- txTimeoutAt,
403
- });
401
+ await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, { txTimeoutAt });
404
402
  }
405
403
 
406
404
  /**
@@ -1062,7 +1060,7 @@ export class CheckpointProposalJob implements Traceable {
1062
1060
  availableTxs,
1063
1061
  minTxs,
1064
1062
  });
1065
- this.log.warn(
1063
+ this.log.verbose(
1066
1064
  `Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (got ${availableTxs} txs but needs ${minTxs})`,
1067
1065
  {
1068
1066
  reason: 'insufficient_txs',
@@ -18,7 +18,11 @@ import type {
18
18
  ProposedCheckpointSink,
19
19
  ValidateCheckpointResult,
20
20
  } from '@aztec/stdlib/block';
21
- import type { Checkpoint, ProposedCheckpointData } from '@aztec/stdlib/checkpoint';
21
+ import {
22
+ type Checkpoint,
23
+ type ProposedCheckpointData,
24
+ buildCheckpointSimulationOverridesPlan,
25
+ } from '@aztec/stdlib/checkpoint';
22
26
  import type { ChainConfig } from '@aztec/stdlib/config';
23
27
  import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
24
28
  import {
@@ -45,7 +49,6 @@ import { DefaultSequencerConfig } from '../config.js';
45
49
  import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
46
50
  import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
47
51
  import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
48
- import { buildCheckpointSimulationOverridesPlan } from './chain_state_overrides.js';
49
52
  import { CheckpointProposalJob } from './checkpoint_proposal_job.js';
50
53
  import { CheckpointProposalJobMetrics } from './checkpoint_proposal_job_metrics.js';
51
54
  import { CheckpointVoter } from './checkpoint_voter.js';
@@ -85,8 +88,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
85
88
  private checkpointProposalJobMetrics: CheckpointProposalJobMetrics;
86
89
  private readonly stateLog: Logger;
87
90
 
88
- /** The last slot for which we attempted to perform our voting duties with degraded block production */
89
- private lastSlotForFallbackVote: SlotNumber | undefined;
91
+ /** The last slot for which we attempted to perform our fallback duties (votes and/or prune) with degraded block production */
92
+ private lastSlotForFallbackAction: SlotNumber | undefined;
90
93
 
91
94
  /** The (checkpoint, slot) of the last invalidation request we successfully simulated, to prevent
92
95
  * re-simulating and re-submitting the same invalidation across the many ticks within a single slot. */
@@ -452,7 +455,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
452
455
  // We are the proposer and the escape hatch is closed: now run the full sync check before building.
453
456
  const syncedTo = await this.checkSync({ ts, slot });
454
457
  if (!syncedTo) {
455
- await this.tryVoteWhenCannotBuild({ slot, targetSlot });
458
+ await this.tryVoteAndPruneWhenCannotBuild({ slot, targetSlot });
456
459
  return undefined;
457
460
  }
458
461
 
@@ -472,7 +475,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
472
475
  });
473
476
  // Mark the slot as attempted so a deadline abort is not retried within the same slot. Vote-only actions
474
477
  // still need to run because sync can succeed even when it is too late to start building a checkpoint.
475
- await this.tryVoteWhenCannotBuild({ slot, targetSlot });
478
+ await this.tryVoteAndPruneWhenCannotBuild({ slot, targetSlot });
476
479
  this.lastSlotForCheckpointProposalJob = targetSlot;
477
480
  return undefined;
478
481
  }
@@ -798,9 +801,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
798
801
  number: syncSummary.latestBlockNumber,
799
802
  hash: syncSummary.latestBlockHash,
800
803
  })),
801
- this.l2BlockSource
802
- .getL2Tips()
803
- .then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
804
+ this.l2BlockSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
804
805
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
805
806
  this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
806
807
  this.l2BlockSource.getPendingChainValidationStatus(),
@@ -845,16 +846,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
845
846
  // matching proposed checkpoint (e.g. it crashed before assembling it). Building on this orphan block
846
847
  // would fork the chain off a tip no other node can follow. The archiver prunes these orphan blocks
847
848
  // once their build slot ends; this guard is the correctness barrier during the grace window before.
849
+ // `getProposedCheckpointData()` returns the latest proposed checkpoint payload, which is always
850
+ // the leading one (a proposed entry is only stored beyond the confirmed frontier and is deleted
851
+ // on confirmation). It carries no tip, so there is no tip-vs-payload split read to reconcile.
848
852
  if (
849
853
  blockData.checkpointNumber > l2Tips.checkpointed.checkpoint.number &&
850
- (l2Tips.proposedCheckpoint.checkpoint.number !== blockData.checkpointNumber ||
851
- proposedCheckpointData?.checkpointNumber !== blockData.checkpointNumber)
854
+ proposedCheckpointData?.checkpointNumber !== blockData.checkpointNumber
852
855
  ) {
853
856
  const logCtx = {
854
857
  blockCheckpointNumber: blockData.checkpointNumber,
855
858
  checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
856
- proposedCheckpointTipNumber: l2Tips.proposedCheckpoint.checkpoint.number,
857
- proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber,
859
+ proposedCheckpointTipNumber: proposedCheckpointData?.checkpointNumber,
858
860
  blockNumber: blockData.header.getBlockNumber(),
859
861
  blockSlot: blockData.header.getSlot(),
860
862
  syncedL2Slot,
@@ -865,27 +867,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
865
867
  return undefined;
866
868
  }
867
869
 
868
- const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
869
-
870
- // The l2Tips and proposedCheckpointData reads above come from independent archiver snapshots
871
- // (a JS-side tips cache vs. a direct store read on `#proposedCheckpoints`). A concurrent archiver
872
- // write that mutates both can be observed split, leaving us with `hasProposedCheckpoint=true` but
873
- // no proposedCheckpointData (or one whose number doesn't match the tip). Refuse to proceed in that
874
- // window — the next checkSync tick will see a coherent snapshot.
875
- if (
876
- hasProposedCheckpoint &&
877
- (!proposedCheckpointData ||
878
- proposedCheckpointData.checkpointNumber !== l2Tips.proposedCheckpoint.checkpoint.number)
879
- ) {
880
- this.log.warn(`Sequencer sync check failed: inconsistent proposed-checkpoint state`, {
881
- proposedCheckpointTipNumber: l2Tips.proposedCheckpoint.checkpoint.number,
882
- checkpointedTipNumber: l2Tips.checkpointed.checkpoint.number,
883
- proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber,
884
- syncedL2Slot,
885
- ...args,
886
- });
887
- return undefined;
888
- }
870
+ const hasProposedCheckpoint = proposedCheckpointData !== undefined;
889
871
 
890
872
  // Check that the proposed checkpoint is indeed the parent of the checkpoint we'll be building
891
873
  // The checkpoint number to build is derived as blockData.checkpointNumber + 1
@@ -962,16 +944,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
962
944
  }
963
945
 
964
946
  /**
965
- * Tries to vote on slashing actions and governance when we cannot build and are past the block-building window.
966
- * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
947
+ * Tries to vote on slashing actions and governance and to prune when we cannot build and are past the
948
+ * block-building window. This allows the sequencer to participate in governance/slashing votes even when it
949
+ * cannot build blocks, and to prune the pending chain so it can recover from bad data that is blocking sync.
967
950
  */
968
- @trackSpan('Sequencer.tryVoteWhenCannotBuild', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
969
- protected async tryVoteWhenCannotBuild(args: { slot: SlotNumber; targetSlot: SlotNumber }): Promise<void> {
951
+ @trackSpan('Sequencer.tryVoteAndPruneWhenCannotBuild', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
952
+ protected async tryVoteAndPruneWhenCannotBuild(args: { slot: SlotNumber; targetSlot: SlotNumber }): Promise<void> {
970
953
  const { slot, targetSlot } = args;
971
954
 
972
955
  // Prevent duplicate attempts in the same slot
973
- if (this.lastSlotForFallbackVote === slot) {
974
- this.log.trace(`Already attempted to vote in slot ${slot} (skipping)`);
956
+ if (this.lastSlotForFallbackAction === slot) {
957
+ this.log.trace(`Already attempted fallback actions in slot ${slot} (skipping)`);
975
958
  return;
976
959
  }
977
960
 
@@ -994,7 +977,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
994
977
  }
995
978
 
996
979
  // Mark this slot as attempted
997
- this.lastSlotForFallbackVote = slot;
980
+ this.lastSlotForFallbackAction = slot;
998
981
 
999
982
  // Get a publisher for voting
1000
983
  const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
@@ -1019,21 +1002,42 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
1019
1002
  const votesPromises = voter.enqueueVotes();
1020
1003
  const votes = await Promise.all(votesPromises);
1021
1004
 
1022
- if (votes.every(p => !p)) {
1023
- this.log.debug(`No votes to enqueue for slot ${slot}`);
1005
+ // Even if we cannot build, try to prune so a stuck pending chain (e.g. bad data blocking sync) can
1006
+ // recover. prune() is permissionless, so it rides the same fallback multicall as the votes.
1007
+ const pruneEnqueued = await this.tryEnqueuePruneIfPrunable(targetSlot, publisher);
1008
+
1009
+ // Bail if nothing to do
1010
+ if (votes.every(p => !p) && !pruneEnqueued) {
1011
+ this.log.debug(`Nothing to enqueue for slot ${slot} (no votes, not prunable)`);
1024
1012
  return;
1025
1013
  }
1026
1014
 
1027
- this.log.info(`Voting in slot ${slot} despite sync failure`, { slot });
1015
+ const [governanceVoteEnqueued, slashingVoteEnqueued] = votes;
1016
+ this.log.info(`Submitting fallback requests in slot ${slot} despite sync failure`, {
1017
+ slot,
1018
+ pruneEnqueued,
1019
+ governanceVoteEnqueued: !!governanceVoteEnqueued,
1020
+ slashingVoteEnqueued: !!slashingVoteEnqueued,
1021
+ });
1022
+
1028
1023
  // Votes are EIP-712-signed for `targetSlot` (the pipelined slot in which the multicall is
1029
1024
  // expected to mine). Delay submission to the start of `targetSlot` so the tx mines in the
1030
1025
  // slot the votes were signed for. We fire-and-forget so we don't block the sequencer's
1031
1026
  // work loop while waiting for the target slot to start.
1032
1027
  void publisher.sendRequestsAt(targetSlot).catch(err => {
1033
- this.log.error(`Failed to publish votes despite sync failure for slot ${slot}`, err, { slot });
1028
+ this.log.error(`Failed to publish fallback requests despite sync failure for slot ${slot}`, err, { slot });
1034
1029
  });
1035
1030
  }
1036
1031
 
1032
+ private async tryEnqueuePruneIfPrunable(targetSlot: SlotNumber, publisher: SequencerPublisher): Promise<boolean> {
1033
+ try {
1034
+ return await publisher.enqueuePruneIfPrunable(targetSlot);
1035
+ } catch (err) {
1036
+ this.log.error(`Failed to enqueue rollup prune for slot ${targetSlot}`, err, { targetSlot });
1037
+ return false;
1038
+ }
1039
+ }
1040
+
1037
1041
  /**
1038
1042
  * Tries to vote on slashing actions and governance proposals when escape hatch is open.
1039
1043
  * This allows the sequencer to participate in voting without performing checkpoint proposal work.
@@ -1047,13 +1051,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
1047
1051
  const { slot, targetSlot, proposer } = args;
1048
1052
 
1049
1053
  // Prevent duplicate attempts in the same slot
1050
- if (this.lastSlotForFallbackVote === slot) {
1054
+ if (this.lastSlotForFallbackAction === slot) {
1051
1055
  this.log.trace(`Already attempted to vote in slot ${slot} (escape hatch open, skipping)`);
1052
1056
  return;
1053
1057
  }
1054
1058
 
1055
1059
  // Mark this slot as attempted
1056
- this.lastSlotForFallbackVote = slot;
1060
+ this.lastSlotForFallbackAction = slot;
1057
1061
 
1058
1062
  const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
1059
1063
 
@@ -1092,7 +1096,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
1092
1096
  // the multicall mines in the slot the votes were signed for; otherwise the L1 contract reads
1093
1097
  // `signaler = getCurrentProposer()` against the wrong slot and signature verification fails
1094
1098
  // silently inside Multicall3. Fire-and-forget so we don't block the sequencer's work loop while
1095
- // waiting for the target slot to start, mirroring tryVoteWhenCannotBuild.
1099
+ // waiting for the target slot to start, mirroring tryVoteAndPruneWhenCannotBuild.
1096
1100
  void publisher.sendRequestsAt(targetSlot).catch(err => {
1097
1101
  this.log.error(`Failed to publish escape-hatch votes for slot ${slot}`, err, { slot, targetSlot });
1098
1102
  });
package/src/test/utils.ts CHANGED
@@ -129,6 +129,7 @@ function createCheckpointHeaderFromBlock(block: L2Block): CheckpointHeader {
129
129
  gv.feeRecipient,
130
130
  gv.gasFees,
131
131
  block.header.totalManaUsed,
132
+ block.header.totalFees,
132
133
  );
133
134
  }
134
135
 
@@ -1,61 +0,0 @@
1
- import { type FeeHeader, RollupContract, type SimulationOverridesPlan } from '@aztec/ethereum/contracts';
2
- import { CheckpointNumber } from '@aztec/foundation/branded-types';
3
- import type { Logger } from '@aztec/foundation/log';
4
- import { type ProposedCheckpointData } from '@aztec/stdlib/checkpoint';
5
- import type { CoordinationSignatureContext } from '@aztec/stdlib/p2p';
6
- type CheckpointSimulationOverridesPlanInput = {
7
- /** Target rollup contract. */
8
- rollup: RollupContract;
9
- /** Checkpoint number to be proposed. */
10
- checkpointNumber: CheckpointNumber;
11
- /** Logger instance. */
12
- log: Logger;
13
- /**
14
- * The proposed parent checkpoint when pipelining. Its `checkpointNumber` must equal
15
- * `checkpointNumber - 1`; the helper enforces this. Mutually exclusive with
16
- * `invalidateToPendingCheckpointNumber`.
17
- */
18
- proposedCheckpointData?: ProposedCheckpointData;
19
- /**
20
- * The pending checkpoint number we'll end up at after invalidation lands. Mutually exclusive
21
- * with `proposedCheckpointData`.
22
- */
23
- invalidateToPendingCheckpointNumber?: CheckpointNumber;
24
- /**
25
- * The real on-chain pending checkpoint number (typically `syncedTo.checkpointedCheckpointNumber`).
26
- * Used as the snapshot we pin both `pending` and `proven` to avoid prunes in simulation.
27
- */
28
- checkpointedCheckpointNumber: CheckpointNumber;
29
- /**
30
- * Chain-level consensus signature context. Used to recompute the parent's `payloadDigest` for the
31
- * pipelined simulation override so it matches what `propose` will write into `tempCheckpointLogs[parent]`
32
- * once the parent lands.
33
- */
34
- signatureContext: CoordinationSignatureContext;
35
- };
36
- /**
37
- * Builds the SimulationOverridesPlan describing the simulated L1 rollup state for a checkpoint's
38
- * enqueue-time simulations: `canProposeAt` (in Sequencer.doWork) and the propose-related sims
39
- * (validateBlockHeader, simulateProposeTx). The plan reflects "as if our pipelined parent
40
- * checkpoint has landed and any required invalidation has executed" — the gap that needs to be
41
- * bridged at enqueue time.
42
- *
43
- * Pipelining (`proposedCheckpointData`) and invalidation (`invalidateToPendingCheckpointNumber`)
44
- * are mutually exclusive; passing both throws.
45
- */
46
- export declare function buildCheckpointSimulationOverridesPlan(input: CheckpointSimulationOverridesPlanInput): Promise<SimulationOverridesPlan | undefined>;
47
- type PipelinedParentFeeHeaderInput = {
48
- checkpointNumber: CheckpointNumber;
49
- proposedCheckpointData: ProposedCheckpointData;
50
- rollup: RollupContract;
51
- log: Logger;
52
- };
53
- /**
54
- * Derives the pending parent fee header used during pipelined proposal simulation. Returns
55
- * `undefined` only when no grandparent exists (i.e. the proposed parent is the genesis
56
- * checkpoint); all other failure modes (missing grandparent state, missing fee header, RPC
57
- * errors) throw so callers don't silently desync the fee-header override.
58
- */
59
- export declare function computePipelinedParentFeeHeader(input: PipelinedParentFeeHeaderInput): Promise<FeeHeader | undefined>;
60
- export {};
61
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2hhaW5fc3RhdGVfb3ZlcnJpZGVzLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvc2VxdWVuY2VyL2NoYWluX3N0YXRlX292ZXJyaWRlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsS0FBSyxTQUFTLEVBQ2QsY0FBYyxFQUVkLEtBQUssdUJBQXVCLEVBQzdCLE1BQU0sMkJBQTJCLENBQUM7QUFDbkMsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDbkUsT0FBTyxLQUFLLEVBQUUsTUFBTSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDcEQsT0FBTyxFQUFFLEtBQUssc0JBQXNCLEVBQWtDLE1BQU0sMEJBQTBCLENBQUM7QUFDdkcsT0FBTyxLQUFLLEVBQUUsNEJBQTRCLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUV0RSxLQUFLLHNDQUFzQyxHQUFHO0lBQzVDLDhCQUE4QjtJQUM5QixNQUFNLEVBQUUsY0FBYyxDQUFDO0lBQ3ZCLHdDQUF3QztJQUN4QyxnQkFBZ0IsRUFBRSxnQkFBZ0IsQ0FBQztJQUNuQyx1QkFBdUI7SUFDdkIsR0FBRyxFQUFFLE1BQU0sQ0FBQztJQUNaOzs7O09BSUc7SUFDSCxzQkFBc0IsQ0FBQyxFQUFFLHNCQUFzQixDQUFDO0lBQ2hEOzs7T0FHRztJQUNILG1DQUFtQyxDQUFDLEVBQUUsZ0JBQWdCLENBQUM7SUFDdkQ7OztPQUdHO0lBQ0gsNEJBQTRCLEVBQUUsZ0JBQWdCLENBQUM7SUFDL0M7Ozs7T0FJRztJQUNILGdCQUFnQixFQUFFLDRCQUE0QixDQUFDO0NBQ2hELENBQUM7QUFFRjs7Ozs7Ozs7O0dBU0c7QUFDSCx3QkFBc0Isc0NBQXNDLENBQzFELEtBQUssRUFBRSxzQ0FBc0MsR0FDNUMsT0FBTyxDQUFDLHVCQUF1QixHQUFHLFNBQVMsQ0FBQyxDQXNEOUM7QUFxQkQsS0FBSyw2QkFBNkIsR0FBRztJQUNuQyxnQkFBZ0IsRUFBRSxnQkFBZ0IsQ0FBQztJQUNuQyxzQkFBc0IsRUFBRSxzQkFBc0IsQ0FBQztJQUMvQyxNQUFNLEVBQUUsY0FBYyxDQUFDO0lBQ3ZCLEdBQUcsRUFBRSxNQUFNLENBQUM7Q0FDYixDQUFDO0FBRUY7Ozs7O0dBS0c7QUFDSCx3QkFBc0IsK0JBQStCLENBQ25ELEtBQUssRUFBRSw2QkFBNkIsR0FDbkMsT0FBTyxDQUFDLFNBQVMsR0FBRyxTQUFTLENBQUMsQ0F3QmhDIn0=
@@ -1 +0,0 @@
1
- {"version":3,"file":"chain_state_overrides.d.ts","sourceRoot":"","sources":["../../src/sequencer/chain_state_overrides.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EACd,cAAc,EAEd,KAAK,uBAAuB,EAC7B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,KAAK,sBAAsB,EAAkC,MAAM,0BAA0B,CAAC;AACvG,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AAEtE,KAAK,sCAAsC,GAAG;IAC5C,8BAA8B;IAC9B,MAAM,EAAE,cAAc,CAAC;IACvB,wCAAwC;IACxC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;IAChD;;;OAGG;IACH,mCAAmC,CAAC,EAAE,gBAAgB,CAAC;IACvD;;;OAGG;IACH,4BAA4B,EAAE,gBAAgB,CAAC;IAC/C;;;;OAIG;IACH,gBAAgB,EAAE,4BAA4B,CAAC;CAChD,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAsB,sCAAsC,CAC1D,KAAK,EAAE,sCAAsC,GAC5C,OAAO,CAAC,uBAAuB,GAAG,SAAS,CAAC,CAsD9C;AAqBD,KAAK,6BAA6B,GAAG;IACnC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,sBAAsB,EAAE,sBAAsB,CAAC;IAC/C,MAAM,EAAE,cAAc,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,+BAA+B,CACnD,KAAK,EAAE,6BAA6B,GACnC,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAwBhC"}