@aztec/sequencer-client 0.0.1-commit.b9865e97 → 0.0.1-commit.be03c316

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 (101) hide show
  1. package/README.md +27 -27
  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 +17 -12
  5. package/dest/config.d.ts +6 -2
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +23 -14
  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 +34 -6
  36. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  37. package/dest/publisher/sequencer-publisher.js +362 -61
  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 +199 -18
  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 +24 -16
  51. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  52. package/dest/sequencer/checkpoint_proposal_job.js +98 -90
  53. package/dest/sequencer/events.d.ts +16 -5
  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/requests_tracker.d.ts +22 -0
  59. package/dest/sequencer/requests_tracker.d.ts.map +1 -0
  60. package/dest/sequencer/requests_tracker.js +33 -0
  61. package/dest/sequencer/sequencer.d.ts +110 -36
  62. package/dest/sequencer/sequencer.d.ts.map +1 -1
  63. package/dest/sequencer/sequencer.js +329 -174
  64. package/dest/test/index.d.ts +3 -3
  65. package/dest/test/index.d.ts.map +1 -1
  66. package/dest/test/utils.d.ts +15 -1
  67. package/dest/test/utils.d.ts.map +1 -1
  68. package/dest/test/utils.js +18 -1
  69. package/package.json +30 -28
  70. package/src/client/sequencer-client.ts +20 -13
  71. package/src/config.ts +25 -12
  72. package/src/global_variable_builder/fee_predictor.ts +11 -1
  73. package/src/global_variable_builder/fee_provider.ts +27 -5
  74. package/src/global_variable_builder/global_builder.ts +2 -34
  75. package/src/index.ts +1 -10
  76. package/src/publisher/config.ts +22 -1
  77. package/src/publisher/l1_to_l2_messaging.ts +85 -0
  78. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +114 -7
  79. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +4 -3
  80. package/src/publisher/l1_tx_failed_store/index.ts +1 -1
  81. package/src/publisher/sequencer-publisher.ts +383 -72
  82. package/src/publisher/write_json.ts +78 -0
  83. package/src/sequencer/automine/README.md +18 -4
  84. package/src/sequencer/automine/automine_factory.ts +8 -1
  85. package/src/sequencer/automine/automine_sequencer.ts +221 -20
  86. package/src/sequencer/automine/index.ts +6 -0
  87. package/src/sequencer/checkpoint_proposal_job.ts +119 -100
  88. package/src/sequencer/events.ts +16 -4
  89. package/src/sequencer/index.ts +0 -2
  90. package/src/sequencer/requests_tracker.ts +43 -0
  91. package/src/sequencer/sequencer.ts +360 -187
  92. package/src/test/index.ts +2 -2
  93. package/src/test/utils.ts +33 -0
  94. package/dest/sequencer/chain_state_overrides.d.ts +0 -61
  95. package/dest/sequencer/chain_state_overrides.d.ts.map +0 -1
  96. package/dest/sequencer/chain_state_overrides.js +0 -98
  97. package/dest/sequencer/timetable.d.ts +0 -98
  98. package/dest/sequencer/timetable.d.ts.map +0 -1
  99. package/dest/sequencer/timetable.js +0 -227
  100. package/src/sequencer/chain_state_overrides.ts +0 -162
  101. package/src/sequencer/timetable.ts +0 -288
@@ -0,0 +1,78 @@
1
+ import { AztecAddress } from '@aztec/aztec.js/addresses';
2
+ import { Fr } from '@aztec/aztec.js/fields';
3
+ import { BatchedBlob, Blob, getEthBlobEvaluationInputs, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
4
+ import { EthAddress } from '@aztec/foundation/eth-address';
5
+ import { L2Block } from '@aztec/stdlib/block';
6
+ import { CheckpointHeader } from '@aztec/stdlib/rollup';
7
+
8
+ import { writeFile } from 'fs/promises';
9
+
10
+ const AZTEC_GENERATE_TEST_DATA = !!process.env.AZTEC_GENERATE_TEST_DATA;
11
+
12
+ /**
13
+ * Creates a json object that can be used to test the solidity contract.
14
+ * The json object must be put into
15
+ */
16
+ export async function writeJson(
17
+ fileName: string,
18
+ checkpointHeader: CheckpointHeader,
19
+ block: L2Block,
20
+ l1ToL2Content: Fr[],
21
+ blobs: Blob[],
22
+ batchedBlob: BatchedBlob,
23
+ recipientAddress: AztecAddress,
24
+ deployerAddress: `0x${string}`,
25
+ ): Promise<void> {
26
+ if (!AZTEC_GENERATE_TEST_DATA) {
27
+ return;
28
+ }
29
+ // Path relative to the package.json in the end-to-end folder
30
+ const path = `../../l1-contracts/test/fixtures/${fileName}.json`;
31
+
32
+ const asHex = (value: Fr | Buffer | EthAddress | AztecAddress, size = 64) => {
33
+ const buffer = Buffer.isBuffer(value) ? value : value.toBuffer();
34
+ return `0x${buffer.toString('hex').padStart(size, '0')}`;
35
+ };
36
+
37
+ const jsonObject = {
38
+ populate: {
39
+ l1ToL2Content: l1ToL2Content.map(value => asHex(value)),
40
+ recipient: asHex(recipientAddress.toField()),
41
+ sender: deployerAddress,
42
+ },
43
+ messages: {
44
+ l2ToL1Messages: block.body.txEffects.flatMap(txEffect => txEffect.l2ToL1Msgs).map(value => asHex(value)),
45
+ },
46
+ checkpoint: {
47
+ // The json formatting in forge is a bit brittle, so we convert Fr to a number in the few values below.
48
+ // This should not be a problem for testing as long as the values are not larger than u32.
49
+ archive: asHex(block.archive.root),
50
+ blobCommitments: getPrefixedEthBlobCommitments(blobs),
51
+ batchedBlobInputs: getEthBlobEvaluationInputs(batchedBlob),
52
+ checkpointNumber: block.number,
53
+ body: `0x${block.body.toBuffer().toString('hex')}`,
54
+ header: {
55
+ lastArchiveRoot: asHex(checkpointHeader.lastArchiveRoot),
56
+ blockHeadersHash: asHex(checkpointHeader.blockHeadersHash),
57
+ blobsHash: asHex(checkpointHeader.blobsHash),
58
+ inHash: asHex(checkpointHeader.inHash),
59
+ outHash: asHex(checkpointHeader.epochOutHash),
60
+ slotNumber: Number(checkpointHeader.slotNumber),
61
+ timestamp: Number(checkpointHeader.timestamp),
62
+ coinbase: asHex(checkpointHeader.coinbase, 40),
63
+ feeRecipient: asHex(checkpointHeader.feeRecipient),
64
+ gasFees: {
65
+ feePerDaGas: Number(checkpointHeader.gasFees.feePerDaGas),
66
+ feePerL2Gas: Number(checkpointHeader.gasFees.feePerL2Gas),
67
+ },
68
+ totalManaUsed: checkpointHeader.totalManaUsed.toNumber(),
69
+ accumulatedFees: checkpointHeader.accumulatedFees.toNumber(),
70
+ },
71
+ headerHash: asHex(checkpointHeader.hash()),
72
+ numTxs: block.body.txEffects.length,
73
+ },
74
+ };
75
+
76
+ const output = JSON.stringify(jsonObject, null, 2);
77
+ await writeFile(path, output, 'utf8');
78
+ }
@@ -19,11 +19,11 @@ Compared to the production `Sequencer`:
19
19
  - No validator orchestration, attestation collection, or P2P proposal gossip.
20
20
  - No slashing, no governance votes, no `SequencerEvents`.
21
21
 
22
- Consumers (archiver, world-state, `EpochTestSettler`) observe L1 and the archiver tip directly rather than listening for sequencer events.
22
+ Consumers (archiver, world-state) observe L1 and the archiver tip directly rather than listening for sequencer events.
23
23
 
24
24
  ## Serial-queue invariant
25
25
 
26
- Every operation — mempool-driven block builds, explicit empty-block builds, time warps, and reorgs — is serialized through a single `SerialQueue`. They never interleave.
26
+ Every operation — mempool-driven block builds, explicit empty-block builds, time warps, reorgs, and synthetic epoch proving — is serialized through a single `SerialQueue`. They never interleave.
27
27
 
28
28
  Public entry points:
29
29
 
@@ -32,15 +32,29 @@ Public entry points:
32
32
  | `buildIfPending()` | Enqueues a mempool-driven build. Coalesces bursts into one job. |
33
33
  | `buildEmptyBlock()` | Enqueues a forced empty-block build. |
34
34
  | `warpTo(ts)` / `warpBy(delta)` | Advances L1 time to a slot boundary. |
35
+ | `prove(upToCheckpoint?)` | Synthetically proves epochs up to a checkpoint (default: the latest checkpointed): writes the epoch out hashes into the L1 Outbox so L2-to-L1 messages become consumable, then advances the proven tip. No real proof. Clamps to the checkpointed tip and no-ops when already proven. |
35
36
  | `revertToCheckpoint(n)` | Rolls L1 back to the block that published checkpoint `n`, then resets archiver, world-state, and P2P pool. |
36
37
  | `syncPoint()` | Awaits the queue reaching idle. |
37
38
 
39
+ ## Time control
40
+
41
+ The AutomineSequencer owns L1 time in the local network (replacing the deleted `AnvilTestWatcher`). It builds and publishes each checkpoint at the next aztec-slot boundary, and `warpTo` / `warpBy` advance the clock by publishing an empty checkpoint at the target slot. Before every build, warp, and prove it reconciles the injected `TestDateProvider` to the latest *mined* L1 timestamp, so node-side consumers of `dateProvider.now()` stay aligned with L1 even when an unrelated L1 tx mines a block between our builds. It never advances the clock to the pending, un-mined timestamp.
42
+
43
+ ## Publish-failure recovery
44
+
45
+ A failed propose mines no checkpoint on L1 (it reverts inside the multicall or is never sent), so recovery is purely local — there is **no L1 reorg**. The optimistic archiver insert (the proposed block plus its proposed checkpoint) is rolled back via `archiver.removeUncheckpointedBlocksAfter`, which removes the uncheckpointed block and evicts the proposed checkpoint that referenced it. `p2pClient.sync()` then observes the lowered proposed tip and returns the block's txs to the pending pool, `worldState.syncImmediate()` drops any applied effects, and the L1 nonce is reset. The build is not retried inline; the mempool poller re-enqueues one once the txs are back in the pool.
46
+
38
47
  ## Entry points
39
48
 
40
49
  **Factory** — `createAutomineSequencer` in `automine_factory.ts` wires up all dependencies (publisher manager, keystore, cheat codes, etc.), starts the `PublisherManager`, and returns an unstarted `AutomineSequencer`. The caller (`AztecNodeService.createAndSync` in `aztec-node/src/aztec-node/server.ts`) invokes `AutomineSequencer.start()` separately. It is called by the full node when `aztecTargetCommitteeSize == 0`.
41
50
 
42
51
  **Test fixture** — `AUTOMINE_E2E_OPTS` in `end-to-end/src/fixtures/fixtures.ts` is the test-side entry point. Pass it to `setup()` to get a node + `AutomineSequencer` instead of the production sequencer stack.
43
52
 
44
- ## Epoch proving caveat
53
+ ## Epoch proving
54
+
55
+ There is no real prover in the automine setup, so epochs are settled synthetically: the epoch out hash is written into the L1 Outbox via cheat codes and the rollup's proven tip is advanced — the local-network equivalent of an epoch proof landing on L1. The grouping/out-hash logic lives in the shared `settleEpochOutbox` helper in `@aztec/prover-client/test`, used by both proving drivers below.
56
+
57
+ Who drives proving depends on the `automineEnableProveEpoch` config flag:
45
58
 
46
- Epoch proving remains manual under `AUTOMINE_E2E_OPTS`. The e2e `setup()` fixture does NOT wire an `EpochTestSettler` that observer is only attached in `local-network.ts`. Tests that cross epoch boundaries must therefore advance the proven anchor explicitly via `cheatCodes.rollup.markAsProven(...)`. See `e2e_lending_contract.test.ts` (which calls `progressSlots` in `simulators/lending_simulator.ts`) and `e2e_pruned_blocks.test.ts` for real examples.
59
+ - **Local network / sandbox** (`automineEnableProveEpoch: true`): the AutomineSequencer runs an auto-prove loop that calls `prove()` as checkpoints land, through the same serial queue as its builds. This replaces the standalone `EpochTestSettler`, which used to race the build loop. The loop also reconciles the clock on each tick.
60
+ - **e2e tests** (`AUTOMINE_E2E_OPTS`, flag off): proving is manual so tests stay deterministic. Cross-epoch tests advance the proven anchor explicitly via `node.prove(...)`, `cheatCodes.rollup.markAsProven(...)`, or a hand-driven `EpochTestSettler`. See `e2e_pruned_blocks.test.ts` and `e2e_epochs/epochs_partial_proof_multi_root.test.ts` for real examples.
@@ -40,9 +40,14 @@ export type CreateAutomineSequencerArgs = {
40
40
  worldStateSynchronizer: WorldStateSynchronizer;
41
41
  archiver: L2BlockSource &
42
42
  L1ToL2MessageSource &
43
- Pick<Archiver, 'rollbackTo' | 'addBlock' | 'addProposedCheckpoint' | 'syncImmediate'>;
43
+ Pick<
44
+ Archiver,
45
+ 'rollbackTo' | 'addBlock' | 'addProposedCheckpoint' | 'syncImmediate' | 'removeUncheckpointedBlocksAfter'
46
+ >;
44
47
  p2pClient: P2P & Pick<ConcreteP2PClient, 'sync'>;
45
48
  l1Constants: { l1GenesisTime: bigint; slotDuration: number; ethereumSlotDuration: number; rollupManaLimit: number };
49
+ /** When true, run the auto-settle / clock-reconcile loop (local-network only). */
50
+ autoSettle?: boolean;
46
51
  log: Logger;
47
52
  };
48
53
 
@@ -71,6 +76,7 @@ export async function createAutomineSequencer({
71
76
  archiver,
72
77
  p2pClient,
73
78
  l1Constants,
79
+ autoSettle,
74
80
  log,
75
81
  }: CreateAutomineSequencerArgs): Promise<AutomineSequencer> {
76
82
  const publisherManager = new PublisherManager(l1TxUtils, getPublisherConfigFromSequencerConfig(config), {
@@ -136,6 +142,7 @@ export async function createAutomineSequencer({
136
142
  config,
137
143
  archiver,
138
144
  l1TxUtils: reorgResetL1TxUtils,
145
+ autoSettle,
139
146
  stopExtras: () => publisherManager.stop(),
140
147
  log: log.createChild('automine-sequencer'),
141
148
  });
@@ -1,7 +1,7 @@
1
1
  import type { Archiver } from '@aztec/archiver';
2
2
  import type { L1TxUtils } from '@aztec/ethereum/l1-tx-utils';
3
- import type { EthCheatCodes } from '@aztec/ethereum/test';
4
- import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
3
+ import { type EthCheatCodes, RollupCheatCodes } from '@aztec/ethereum/test';
4
+ import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
5
5
  import type { EthAddress } from '@aztec/foundation/eth-address';
6
6
  import { Signature } from '@aztec/foundation/eth-signature';
7
7
  import { type Logger, createLogger } from '@aztec/foundation/log';
@@ -10,6 +10,7 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
10
10
  import type { TestDateProvider } from '@aztec/foundation/timer';
11
11
  import { isErrorClass } from '@aztec/foundation/types';
12
12
  import type { P2PClient as ConcreteP2PClient, P2P } from '@aztec/p2p';
13
+ import { settleEpochOutbox } from '@aztec/prover-client/test';
13
14
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
14
15
  import { CommitteeAttestationsAndSigners, type L2Block, type L2BlockSource } from '@aztec/stdlib/block';
15
16
  import { getPreviousCheckpointOutHashes } from '@aztec/stdlib/checkpoint';
@@ -65,7 +66,10 @@ export type AutomineSequencerDeps = {
65
66
  * Archiver used to push locally-built blocks and proposed checkpoints into the in-memory
66
67
  * store, force an immediate L1 sync after publishing, and roll back state during reorgs.
67
68
  */
68
- archiver: Pick<Archiver, 'rollbackTo' | 'addBlock' | 'addProposedCheckpoint' | 'syncImmediate'>;
69
+ archiver: Pick<
70
+ Archiver,
71
+ 'rollbackTo' | 'addBlock' | 'addProposedCheckpoint' | 'syncImmediate' | 'removeUncheckpointedBlocksAfter'
72
+ >;
69
73
  /** L1 tx utils whose cached nonces must be reset after an L1 reorg. */
70
74
  l1TxUtils: Pick<L1TxUtils, 'resetNonce'>[];
71
75
  /**
@@ -75,6 +79,14 @@ export type AutomineSequencerDeps = {
75
79
  stopExtras?: () => Promise<void>;
76
80
  /** How often to poll the mempool for new txs while running. Defaults to 50ms. */
77
81
  pollIntervalMs?: number;
82
+ /**
83
+ * When true, run a loop that synthetically settles epochs (writes outbox roots and advances the
84
+ * proven tip) as checkpoints land, replacing the standalone `EpochTestSettler`. Local-network only;
85
+ * e2e tests leave this off and drive proving explicitly.
86
+ */
87
+ autoSettle?: boolean;
88
+ /** How often the auto-settle / clock-reconcile loop runs while {@link autoSettle} is enabled. Defaults to 200ms. */
89
+ settlePollIntervalMs?: number;
78
90
  log?: Logger;
79
91
  };
80
92
 
@@ -106,6 +118,8 @@ export class AutomineSequencer {
106
118
  private stopped = false;
107
119
  private paused = false;
108
120
  private mempoolPoller?: RunningPromise;
121
+ /** Loop that settles epochs and reconciles the clock while {@link AutomineSequencerDeps.autoSettle} is set. */
122
+ private settler?: RunningPromise;
109
123
  private publisher?: SequencerPublisher;
110
124
  private attestorAddress?: EthAddress;
111
125
 
@@ -115,6 +129,9 @@ export class AutomineSequencer {
115
129
  /** Last L2 slot we published a checkpoint for (-1 means none yet). */
116
130
  private lastBuiltSlot: number = -1;
117
131
 
132
+ /** Lazily-built cheat codes for synthetic epoch settlement (outbox roots + proven tip). */
133
+ private rollupCheatCodes?: RollupCheatCodes;
134
+
118
135
  constructor(deps: AutomineSequencerDeps) {
119
136
  this.deps = deps;
120
137
  this.log = deps.log ?? createLogger('sequencer:automine');
@@ -122,6 +139,14 @@ export class AutomineSequencer {
122
139
  this.pollIntervalMs = deps.pollIntervalMs ?? 50;
123
140
  }
124
141
 
142
+ /** Cheat codes for the rollup, built from the same EthCheatCodes the sequencer uses for time control. */
143
+ private getRollupCheatCodes(): RollupCheatCodes {
144
+ this.rollupCheatCodes ??= new RollupCheatCodes(this.deps.ethCheatCodes, {
145
+ rollupAddress: this.deps.config.rollupAddress,
146
+ });
147
+ return this.rollupCheatCodes;
148
+ }
149
+
125
150
  /**
126
151
  * Starts the sequencer. Switches anvil into automine mode (no interval mining),
127
152
  * acquires a publisher, and begins polling the mempool for pending txs.
@@ -149,6 +174,13 @@ export class AutomineSequencer {
149
174
  // notification. Tests can also call `buildIfPending()` directly to skip the poll wait.
150
175
  this.mempoolPoller = new RunningPromise(() => this.maybeEnqueueBuild(), this.log, this.pollIntervalMs);
151
176
  this.mempoolPoller.start();
177
+
178
+ // Local-network only: settle epochs and reconcile the clock to L1 as checkpoints land. Both run
179
+ // through the same serial queue as builds/warps, so they never interleave with a checkpoint build.
180
+ if (this.deps.autoSettle) {
181
+ this.settler = new RunningPromise(() => this.maybeSettle(), this.log, this.deps.settlePollIntervalMs ?? 200);
182
+ this.settler.start();
183
+ }
152
184
  }
153
185
 
154
186
  /**
@@ -163,6 +195,7 @@ export class AutomineSequencer {
163
195
  this.stopped = true;
164
196
  this.running = false;
165
197
  await this.mempoolPoller?.stop();
198
+ await this.settler?.stop();
166
199
  await this.queue.end();
167
200
  await this.deps.stopExtras?.();
168
201
  this.log.info('AutomineSequencer stopped');
@@ -241,14 +274,22 @@ export class AutomineSequencer {
241
274
  /**
242
275
  * Warps L1 timestamp to `targetTimestampSec`. Rounded up to the next aztec-slot
243
276
  * boundary so the next build lands on a fresh slot. Atomic with respect to builds —
244
- * 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}).
245
279
  */
246
280
  public warpTo(targetTimestampSec: number): Promise<void> {
247
281
  return this.queue.put(() => this.runWarp(targetTimestampSec));
248
282
  }
249
283
 
250
- /** 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
+ */
251
289
  public warpBy(deltaSec: number): Promise<void> {
290
+ if (deltaSec <= 0) {
291
+ throw new Error(`warpL2TimeAtLeastBy: duration must be positive, got ${deltaSec} seconds.`);
292
+ }
252
293
  return this.queue.put(async () => {
253
294
  const current = await this.deps.ethCheatCodes.lastBlockTimestamp();
254
295
  await this.runWarp(current + deltaSec);
@@ -271,14 +312,57 @@ export class AutomineSequencer {
271
312
  return this.queue.syncPoint();
272
313
  }
273
314
 
315
+ /**
316
+ * Synthetically "proves" the L2 chain up to `upToCheckpoint` (default: the latest checkpointed
317
+ * checkpoint). For every epoch newly covered — including a partial final epoch — the epoch out hash
318
+ * is written into the L1 Outbox so the L2-to-L1 messages in those checkpoints become consumable,
319
+ * then the rollup's proven tip is advanced to the target. There is no real proof; this is the
320
+ * local-network equivalent of an epoch proof landing on L1.
321
+ *
322
+ * Clamps the target down to the latest checkpointed checkpoint and no-ops when it is already proven.
323
+ * Runs inside the serial queue so it never interleaves with a build or warp.
324
+ *
325
+ * @returns The proven checkpoint after the call (the target, or the existing proven tip on no-op).
326
+ */
327
+ public prove(upToCheckpoint?: CheckpointNumber): Promise<CheckpointNumber> {
328
+ return this.queue.put(() => this.runProve(upToCheckpoint));
329
+ }
330
+
331
+ /**
332
+ * Auto-settle tick (local-network only). Proving up to the latest checkpointed checkpoint also
333
+ * reconciles the clock at the head of {@link runProve}, so this single tick keeps both the
334
+ * proven tip and the date provider current even when no build is happening.
335
+ */
336
+ private async maybeSettle(): Promise<void> {
337
+ if (!this.running) {
338
+ return;
339
+ }
340
+ try {
341
+ await this.prove();
342
+ } catch (err) {
343
+ this.log.warn('Automine auto-settle tick failed', { err: err instanceof Error ? err.message : String(err) });
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Advances the injected date provider to the latest *mined* L1 timestamp when it has fallen behind
349
+ * (e.g. an unrelated L1 tx mined a block between our builds). Never advances to the pending, un-mined
350
+ * timestamp. Keeps node-side consumers of `dateProvider.now()` aligned with L1 without our own builds.
351
+ */
352
+ private async reconcileDateProvider(): Promise<void> {
353
+ const lastTsMs = (await this.deps.ethCheatCodes.lastBlockTimestamp()) * 1000;
354
+ if (lastTsMs > this.deps.dateProvider.now()) {
355
+ this.deps.dateProvider.setTime(lastTsMs);
356
+ }
357
+ }
358
+
274
359
  /** Called from the mempool poller. Enqueues a build if there are pending txs. */
275
360
  private async maybeEnqueueBuild(): Promise<void> {
276
361
  if (!this.running || this.paused || this.buildQueued) {
277
362
  return;
278
363
  }
279
364
  try {
280
- const pending = await this.deps.p2pClient.getPendingTxCount();
281
- if (pending > 0) {
365
+ if (await this.deps.p2pClient.hasEligiblePendingTxs(1)) {
282
366
  // Fire-and-forget; the build result is delivered via `buildIfPending()` callers,
283
367
  // not via the poller.
284
368
  void this.buildIfPending().catch(err => {
@@ -290,19 +374,27 @@ export class AutomineSequencer {
290
374
  }
291
375
  }
292
376
 
293
- /** Builds one checkpoint with a single block, publishes it, syncs the date provider. */
377
+ /**
378
+ * Builds and publishes one checkpoint, returning the built block — or `undefined` when there was
379
+ * nothing to build or the propose failed. A failed propose mines no checkpoint on L1 (it reverts
380
+ * inside the multicall or is never sent), so recovery is purely local: the optimistic archiver insert
381
+ * is rolled back, the block's txs return to the pending pool, and the L1 nonce is reset. No L1 reorg
382
+ * is performed, and the build is not retried inline — once the txs are back in the pending pool the
383
+ * mempool poller re-enqueues a build on its next tick (see {@link maybeEnqueueBuild}).
384
+ */
294
385
  private async runBuild({ allowEmpty }: { allowEmpty: boolean }): Promise<L2Block | undefined> {
295
386
  if (!this.running || !this.publisher || !this.attestorAddress) {
296
387
  return undefined;
297
388
  }
389
+ await this.reconcileDateProvider();
298
390
 
299
- const txCount = await this.deps.p2pClient.getPendingTxCount();
300
- // For mempool-driven builds, wait for at least `minTxsPerBlock` pending txs (or 1 if not set)
391
+ // For mempool-driven builds, wait for at least `minTxsPerBlock` age-eligible txs (or 1 if not set)
301
392
  // before building. This mirrors the production sequencer's `waitForMinTxs` behavior, and is
302
393
  // required for tests that bundle multiple txs into one block via `setConfig({ minTxsPerBlock })`.
303
- // Explicit empty-block / warp paths pass `allowEmpty: true` and bypass this gate.
394
+ // Explicit empty-block / warp paths pass `allowEmpty: true`, giving minRequired 0, which
395
+ // hasEligiblePendingTxs treats as always satisfied so the gate is bypassed.
304
396
  const minRequired = allowEmpty ? 0 : Math.max(this.deps.config.minTxsPerBlock ?? 1, 1);
305
- if (txCount < minRequired) {
397
+ if (!(await this.deps.p2pClient.hasEligiblePendingTxs(minRequired))) {
306
398
  return undefined;
307
399
  }
308
400
 
@@ -324,7 +416,10 @@ export class AutomineSequencer {
324
416
  await this.deps.ethCheatCodes.setNextBlockTimestamp(slotBoundaryTs);
325
417
  }
326
418
 
327
- const tips = await this.deps.l2BlockSource.getL2Tips();
419
+ const [tips, proposedCheckpoint] = await Promise.all([
420
+ this.deps.l2BlockSource.getL2Tips(),
421
+ this.deps.l2BlockSource.getProposedCheckpointData(),
422
+ ]);
328
423
  const syncedToBlockNumber = tips.proposed.number;
329
424
 
330
425
  // Ensure world state has processed the archiver's tip before forking. Without this,
@@ -334,7 +429,8 @@ export class AutomineSequencer {
334
429
  await this.deps.worldState.syncImmediate(BlockNumber(syncedToBlockNumber));
335
430
 
336
431
  const nextBlockNumber = BlockNumber(syncedToBlockNumber + 1);
337
- const checkpointNumber = CheckpointNumber(tips.proposedCheckpoint.checkpoint.number + 1);
432
+ const parentCheckpointNumber = proposedCheckpoint?.checkpointNumber ?? tips.checkpointed.checkpoint.number;
433
+ const checkpointNumber = CheckpointNumber(parentCheckpointNumber + 1);
338
434
  const targetEpoch = getEpochAtSlot(SlotNumber(targetSlot), this.deps.l1Constants);
339
435
 
340
436
  this.log.verbose(`Building automine checkpoint`, {
@@ -342,7 +438,6 @@ export class AutomineSequencer {
342
438
  blockNumber: nextBlockNumber,
343
439
  slot: targetSlot,
344
440
  slotTimestamp: slotBoundaryTs,
345
- txCount,
346
441
  allowEmpty,
347
442
  });
348
443
 
@@ -377,7 +472,8 @@ export class AutomineSequencer {
377
472
  this.log.getBindings(),
378
473
  );
379
474
 
380
- const pendingTxs = this.deps.p2pClient.iterateEligiblePendingTxs();
475
+ // Block building only executes txs, and automine publishes straight to L1, so the proofs are never needed.
476
+ const pendingTxs = this.deps.p2pClient.iterateEligiblePendingTxs({ includeProof: false });
381
477
 
382
478
  const buildResult = await this.tryBuildBlock(
383
479
  checkpointBuilder,
@@ -419,15 +515,16 @@ export class AutomineSequencer {
419
515
  // offset — automine is the deliberate non-pipelined exception and builds/publishes in place.
420
516
  const result = await this.publisher.sendRequests(SlotNumber(targetSlot));
421
517
 
422
- const successful = result?.successfulActions?.find(a => a === 'propose');
423
- if (!successful) {
424
- this.log.error('Propose action did not succeed under automine', {
518
+ const proposeSucceeded = !!result?.successfulActions?.some(action => action === 'propose');
519
+ if (!proposeSucceeded) {
520
+ this.log.warn('Automine propose did not succeed; rolled back optimistic insert, will rebuild from the poller', {
425
521
  slot: targetSlot,
426
522
  checkpointNumber,
427
523
  successful: result?.successfulActions,
428
524
  failed: result?.failedActions,
429
525
  });
430
- throw new Error(`AutomineSequencer: propose did not succeed for slot ${targetSlot}`);
526
+ await this.rollbackOptimisticInsert(syncedToBlockNumber);
527
+ return undefined;
431
528
  }
432
529
 
433
530
  // Force one full L1-sync cycle synchronously. The local addBlock/addProposedCheckpoint
@@ -439,6 +536,17 @@ export class AutomineSequencer {
439
536
  const newL1Ts = await this.deps.ethCheatCodes.lastBlockTimestamp();
440
537
  this.deps.dateProvider.setTime(newL1Ts * 1000);
441
538
 
539
+ // A successful propose is validated on-chain against the mined L1 block's slot, so the mined slot
540
+ // should equal our target; warn (without rolling back — the checkpoint is on L1) if it ever differs.
541
+ const minedSlot = Number(getSlotAtTimestamp(BigInt(newL1Ts), this.deps.l1Constants));
542
+ if (minedSlot !== targetSlot) {
543
+ this.log.warn(`Automine checkpoint mined in slot ${minedSlot} but targeted ${targetSlot}`, {
544
+ minedSlot,
545
+ targetSlot,
546
+ checkpointNumber,
547
+ });
548
+ }
549
+
442
550
  this.lastBuiltSlot = targetSlot;
443
551
 
444
552
  this.log.verbose(`Automine checkpoint published`, {
@@ -452,6 +560,23 @@ export class AutomineSequencer {
452
560
  return buildResult.block;
453
561
  }
454
562
 
563
+ /**
564
+ * Undoes an optimistic archiver insert (uncheckpointed block + proposed checkpoint) without reorging
565
+ * L1: removes the uncheckpointed block and evicts the proposed checkpoint that referenced it, so the
566
+ * proposed tip drops back to `toBlockNumber`. `p2pClient.sync()` then observes the lowered tip and
567
+ * restores the block's txs to the pending pool; `worldState.syncImmediate()` drops any applied effects;
568
+ * and the L1 nonce is reset (a reverted-but-mined propose consumes one) so the build can be retried.
569
+ *
570
+ * Note: `archiver.rollbackTo` is NOT usable here — it is checkpoint-granular and no-ops on a
571
+ * proposed-only tip (the inserted checkpoint is not yet in the checkpointed set).
572
+ */
573
+ private async rollbackOptimisticInsert(toBlockNumber: BlockNumber): Promise<void> {
574
+ await this.deps.archiver.removeUncheckpointedBlocksAfter(toBlockNumber);
575
+ await this.deps.p2pClient.sync();
576
+ await this.deps.worldState.syncImmediate();
577
+ this.deps.l1TxUtils.forEach(utils => utils.resetNonce());
578
+ }
579
+
455
580
  /**
456
581
  * Warps L1 timestamp to (or past) `targetTimestampSec`, rounded up to the next aztec-slot
457
582
  * boundary, by queuing an empty-checkpoint build at that slot. Mines exactly one L1 block
@@ -459,6 +584,7 @@ export class AutomineSequencer {
459
584
  * the mined block lands on the slot boundary.
460
585
  */
461
586
  private async runWarp(targetTimestampSec: number): Promise<void> {
587
+ await this.reconcileDateProvider();
462
588
  const currentL1Ts = await this.deps.ethCheatCodes.lastBlockTimestamp();
463
589
  if (targetTimestampSec <= currentL1Ts) {
464
590
  this.log.debug(`Warp target ${targetTimestampSec} is not in the future of current L1 ts ${currentL1Ts}`);
@@ -536,6 +662,81 @@ export class AutomineSequencer {
536
662
  });
537
663
  }
538
664
 
665
+ /**
666
+ * Writes outbox roots for every epoch newly covered up to `maybeCheckpoint` and advances the proven
667
+ * tip. Settles each fully-covered epoch in full and the final epoch only up to the target checkpoint
668
+ * (a partial epoch, which the AZIP-14 Outbox supports via per-`numCheckpointsInEpoch` roots).
669
+ */
670
+ private async runProve(upToCheckpoint?: CheckpointNumber): Promise<CheckpointNumber> {
671
+ await this.reconcileDateProvider();
672
+ const rollupCheatCodes = this.getRollupCheatCodes();
673
+ const tips = await this.deps.l2BlockSource.getL2Tips();
674
+ const checkpointedTip = tips.checkpointed.checkpoint.number;
675
+
676
+ // Never prove beyond what the archiver has actually checkpointed; default to that tip.
677
+ const target = CheckpointNumber(Math.min(upToCheckpoint ?? checkpointedTip, checkpointedTip));
678
+
679
+ const { proven } = await rollupCheatCodes.getTips();
680
+ if (target <= proven) {
681
+ this.log.debug(`Checkpoint ${target} already proven`, { target, proven, checkpointedTip });
682
+ return proven;
683
+ }
684
+
685
+ const startEpoch = await this.getEpochOfCheckpoint(CheckpointNumber(proven + 1));
686
+ const endEpoch = await this.getEpochOfCheckpoint(target);
687
+ if (startEpoch === undefined || endEpoch === undefined) {
688
+ this.log.warn(`Cannot resolve epoch range to prove up to checkpoint ${target}`, {
689
+ target,
690
+ proven,
691
+ startEpoch,
692
+ endEpoch,
693
+ });
694
+ return proven;
695
+ }
696
+
697
+ for (let epoch = startEpoch; epoch <= endEpoch; epoch++) {
698
+ const lastCovered = await settleEpochOutbox({
699
+ rollupCheatCodes,
700
+ l2BlockSource: this.deps.l2BlockSource,
701
+ epoch: EpochNumber(epoch),
702
+ maxCheckpoint: epoch === endEpoch ? target : undefined,
703
+ log: this.log,
704
+ });
705
+ if (lastCovered === undefined) {
706
+ // An epoch in (proven, target] with no checkpointed blocks — expected when warps skip a whole
707
+ // epoch. Logged so it's distinguishable from the archiver failing to serve the epoch's blocks.
708
+ this.log.debug(`No checkpointed blocks to settle for epoch ${epoch} while proving to ${target}`, {
709
+ epoch,
710
+ target,
711
+ });
712
+ }
713
+ }
714
+
715
+ await rollupCheatCodes.markAsProven(target);
716
+ // Settlement is a direct L1 storage write that mines no block, unlike a real epoch proof landing
717
+ // on L1. The archiver's L1 sync short-circuits while the L1 block hash is unchanged, so it would
718
+ // never re-read the proven tip until the next build/warp mines a block. Mine one L1 block so the
719
+ // block hash advances, then force an immediate sync that observes the new proven checkpoint. Use
720
+ // evmMine (not mineEmptyBlock): mineEmptyBlock drops+re-adds the mempool non-atomically, which can
721
+ // silently lose a test's concurrently-submitted direct L1 tx. evm_mine just includes whatever is
722
+ // pending, which is fine here — we only need the block hash to advance.
723
+ await this.deps.ethCheatCodes.evmMine();
724
+ await this.deps.archiver.syncImmediate();
725
+
726
+ this.log.verbose(`Proved up to checkpoint ${target}`, { target, proven, startEpoch, endEpoch });
727
+ return target;
728
+ }
729
+
730
+ /** Resolves the epoch a checkpoint belongs to from its slot, or undefined if the archiver lacks it. */
731
+ private async getEpochOfCheckpoint(checkpointNumber: CheckpointNumber): Promise<number | undefined> {
732
+ const checkpointData = await this.deps.l2BlockSource.getCheckpointData({ number: checkpointNumber });
733
+ if (!checkpointData) {
734
+ return undefined;
735
+ }
736
+ const slot = SlotNumber(Number(checkpointData.header.slotNumber));
737
+ return Number(getEpochAtSlot(slot, this.deps.l1Constants));
738
+ }
739
+
539
740
  /**
540
741
  * Wraps `checkpointBuilder.buildBlock` with the failed-tx handling shared by both error
541
742
  * and success paths: drops the failed txs from the P2P mempool, and returns `undefined`
@@ -0,0 +1,6 @@
1
+ export {
2
+ AutomineSequencer,
3
+ type AutomineSequencerConstants,
4
+ type AutomineSequencerDeps,
5
+ } from './automine_sequencer.js';
6
+ export { createAutomineSequencer, type CreateAutomineSequencerArgs } from './automine_factory.js';