@aztec/sequencer-client 5.0.0-private.20260319 → 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 (101) hide show
  1. package/README.md +281 -21
  2. package/dest/client/sequencer-client.d.ts +8 -3
  3. package/dest/client/sequencer-client.d.ts.map +1 -1
  4. package/dest/client/sequencer-client.js +17 -32
  5. package/dest/config.d.ts +10 -4
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +49 -25
  8. package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
  9. package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
  10. package/dest/global_variable_builder/fee_predictor.js +128 -0
  11. package/dest/global_variable_builder/fee_provider.d.ts +21 -0
  12. package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
  13. package/dest/global_variable_builder/fee_provider.js +67 -0
  14. package/dest/global_variable_builder/global_builder.d.ts +13 -26
  15. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  16. package/dest/global_variable_builder/global_builder.js +9 -67
  17. package/dest/global_variable_builder/index.d.ts +4 -2
  18. package/dest/global_variable_builder/index.d.ts.map +1 -1
  19. package/dest/global_variable_builder/index.js +2 -0
  20. package/dest/publisher/config.d.ts +19 -3
  21. package/dest/publisher/config.d.ts.map +1 -1
  22. package/dest/publisher/config.js +30 -5
  23. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +3 -4
  24. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
  25. package/dest/publisher/sequencer-bundle-simulator.d.ts +96 -0
  26. package/dest/publisher/sequencer-bundle-simulator.d.ts.map +1 -0
  27. package/dest/publisher/sequencer-bundle-simulator.js +198 -0
  28. package/dest/publisher/sequencer-publisher-factory.d.ts +3 -5
  29. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  30. package/dest/publisher/sequencer-publisher-factory.js +2 -3
  31. package/dest/publisher/sequencer-publisher.d.ts +84 -65
  32. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  33. package/dest/publisher/sequencer-publisher.js +403 -531
  34. package/dest/sequencer/automine/automine_factory.d.ts +56 -0
  35. package/dest/sequencer/automine/automine_factory.d.ts.map +1 -0
  36. package/dest/sequencer/automine/automine_factory.js +85 -0
  37. package/dest/sequencer/automine/automine_sequencer.d.ts +189 -0
  38. package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -0
  39. package/dest/sequencer/automine/automine_sequencer.js +689 -0
  40. package/dest/sequencer/automine/index.d.ts +3 -0
  41. package/dest/sequencer/automine/index.d.ts.map +1 -0
  42. package/dest/sequencer/automine/index.js +2 -0
  43. package/dest/sequencer/checkpoint_proposal_job.d.ts +73 -21
  44. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  45. package/dest/sequencer/checkpoint_proposal_job.js +709 -200
  46. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  47. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  48. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  49. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  50. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  51. package/dest/sequencer/checkpoint_voter.js +2 -5
  52. package/dest/sequencer/errors.d.ts +1 -8
  53. package/dest/sequencer/errors.d.ts.map +1 -1
  54. package/dest/sequencer/errors.js +0 -9
  55. package/dest/sequencer/events.d.ts +61 -4
  56. package/dest/sequencer/events.d.ts.map +1 -1
  57. package/dest/sequencer/metrics.d.ts +9 -10
  58. package/dest/sequencer/metrics.d.ts.map +1 -1
  59. package/dest/sequencer/metrics.js +34 -20
  60. package/dest/sequencer/sequencer.d.ts +117 -30
  61. package/dest/sequencer/sequencer.d.ts.map +1 -1
  62. package/dest/sequencer/sequencer.js +483 -158
  63. package/dest/sequencer/types.d.ts +2 -2
  64. package/dest/sequencer/types.d.ts.map +1 -1
  65. package/dest/test/index.d.ts +3 -3
  66. package/dest/test/index.d.ts.map +1 -1
  67. package/dest/test/utils.d.ts +15 -1
  68. package/dest/test/utils.d.ts.map +1 -1
  69. package/dest/test/utils.js +24 -7
  70. package/package.json +28 -27
  71. package/src/client/sequencer-client.ts +29 -41
  72. package/src/config.ts +58 -25
  73. package/src/global_variable_builder/README.md +44 -0
  74. package/src/global_variable_builder/fee_predictor.ts +172 -0
  75. package/src/global_variable_builder/fee_provider.ts +80 -0
  76. package/src/global_variable_builder/global_builder.ts +19 -89
  77. package/src/global_variable_builder/index.ts +3 -1
  78. package/src/publisher/config.ts +62 -7
  79. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
  80. package/src/publisher/sequencer-bundle-simulator.ts +254 -0
  81. package/src/publisher/sequencer-publisher-factory.ts +3 -6
  82. package/src/publisher/sequencer-publisher.ts +456 -578
  83. package/src/sequencer/automine/README.md +60 -0
  84. package/src/sequencer/automine/automine_factory.ts +152 -0
  85. package/src/sequencer/automine/automine_sequencer.ts +795 -0
  86. package/src/sequencer/automine/index.ts +6 -0
  87. package/src/sequencer/checkpoint_proposal_job.ts +817 -225
  88. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  89. package/src/sequencer/checkpoint_voter.ts +1 -12
  90. package/src/sequencer/errors.ts +0 -15
  91. package/src/sequencer/events.ts +65 -4
  92. package/src/sequencer/metrics.ts +43 -24
  93. package/src/sequencer/sequencer.ts +557 -175
  94. package/src/sequencer/types.ts +1 -1
  95. package/src/test/index.ts +2 -2
  96. package/src/test/utils.ts +60 -10
  97. package/dest/sequencer/timetable.d.ts +0 -88
  98. package/dest/sequencer/timetable.d.ts.map +0 -1
  99. package/dest/sequencer/timetable.js +0 -222
  100. package/src/sequencer/README.md +0 -531
  101. package/src/sequencer/timetable.ts +0 -283
@@ -0,0 +1,689 @@
1
+ function _ts_add_disposable_resource(env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() {
16
+ try {
17
+ inner.call(this);
18
+ } catch (e) {
19
+ return Promise.reject(e);
20
+ }
21
+ };
22
+ env.stack.push({
23
+ value: value,
24
+ dispose: dispose,
25
+ async: async
26
+ });
27
+ } else if (async) {
28
+ env.stack.push({
29
+ async: true
30
+ });
31
+ }
32
+ return value;
33
+ }
34
+ function _ts_dispose_resources(env) {
35
+ var _SuppressedError = typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
36
+ var e = new Error(message);
37
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
38
+ };
39
+ return (_ts_dispose_resources = function _ts_dispose_resources(env) {
40
+ function fail(e) {
41
+ env.error = env.hasError ? new _SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
42
+ env.hasError = true;
43
+ }
44
+ var r, s = 0;
45
+ function next() {
46
+ while(r = env.stack.pop()){
47
+ try {
48
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
49
+ if (r.dispose) {
50
+ var result = r.dispose.call(r.value);
51
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
52
+ fail(e);
53
+ return next();
54
+ });
55
+ } else s |= 1;
56
+ } catch (e) {
57
+ fail(e);
58
+ }
59
+ }
60
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
61
+ if (env.hasError) throw env.error;
62
+ }
63
+ return next();
64
+ })(env);
65
+ }
66
+ import { RollupCheatCodes } from '@aztec/ethereum/test';
67
+ import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
68
+ import { Signature } from '@aztec/foundation/eth-signature';
69
+ import { createLogger } from '@aztec/foundation/log';
70
+ import { SerialQueue } from '@aztec/foundation/queue';
71
+ import { RunningPromise } from '@aztec/foundation/running-promise';
72
+ import { isErrorClass } from '@aztec/foundation/types';
73
+ import { settleEpochOutbox } from '@aztec/prover-client/test';
74
+ import { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
75
+ import { getPreviousCheckpointOutHashes } from '@aztec/stdlib/checkpoint';
76
+ import { getEpochAtSlot, getSlotAtTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
77
+ import { InsufficientValidTxsError } from '@aztec/stdlib/interfaces/server';
78
+ /**
79
+ * Minimal, deterministic, queue-driven sequencer for e2e tests that don't exercise
80
+ * block-building or consensus (e.g. e2e_token, e2e_amm, e2e_authwit).
81
+ *
82
+ * Differences from the production `Sequencer`:
83
+ * - No proposer-turn check (single sequencer).
84
+ * - No sync check, no pipelining, no validator orchestration, no attestations,
85
+ * no slashing, no votes, no P2P proposal gossip, no timetable enforcement.
86
+ * - No event emission — consumers (archiver, world-state, `EpochTestSettler`) observe
87
+ * L1 and the archiver tip rather than sequencer events.
88
+ * - All test-driven time control (warp / mine empty block) goes through a single
89
+ * serial queue alongside mempool-driven builds; the three never interleave.
90
+ *
91
+ * Requires `aztecTargetCommitteeSize == 0` on the deployed rollup so that the
92
+ * L1 `verifyProposer` / `verifyAttestations` short-circuits accept an empty
93
+ * `CommitteeAttestationsAndSigners` (see
94
+ * `l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol:244`).
95
+ */ export class AutomineSequencer {
96
+ log;
97
+ queue;
98
+ pollIntervalMs;
99
+ deps;
100
+ running = false;
101
+ stopped = false;
102
+ paused = false;
103
+ mempoolPoller;
104
+ /** Loop that settles epochs and reconciles the clock while {@link AutomineSequencerDeps.autoSettle} is set. */ settler;
105
+ publisher;
106
+ attestorAddress;
107
+ /** Set while a mempool-driven build is queued but not yet run; used to coalesce. */ buildQueued = undefined;
108
+ /** Last L2 slot we published a checkpoint for (-1 means none yet). */ lastBuiltSlot = -1;
109
+ /** Lazily-built cheat codes for synthetic epoch settlement (outbox roots + proven tip). */ rollupCheatCodes;
110
+ constructor(deps){
111
+ this.deps = deps;
112
+ this.log = deps.log ?? createLogger('sequencer:automine');
113
+ this.queue = new SerialQueue();
114
+ this.pollIntervalMs = deps.pollIntervalMs ?? 50;
115
+ }
116
+ /** Cheat codes for the rollup, built from the same EthCheatCodes the sequencer uses for time control. */ getRollupCheatCodes() {
117
+ this.rollupCheatCodes ??= new RollupCheatCodes(this.deps.ethCheatCodes, {
118
+ rollupAddress: this.deps.config.rollupAddress
119
+ });
120
+ return this.rollupCheatCodes;
121
+ }
122
+ /**
123
+ * Starts the sequencer. Switches anvil into automine mode (no interval mining),
124
+ * acquires a publisher, and begins polling the mempool for pending txs.
125
+ */ async start() {
126
+ if (this.running) {
127
+ return;
128
+ }
129
+ this.running = true;
130
+ await this.deps.ethCheatCodes.setIntervalMining(0, {
131
+ silent: true
132
+ });
133
+ await this.deps.ethCheatCodes.setAutomine(true, {
134
+ silent: true
135
+ });
136
+ const { publisher, attestorAddress } = await this.deps.publisherFactory.create();
137
+ this.publisher = publisher;
138
+ this.attestorAddress = attestorAddress;
139
+ this.log.info(`AutomineSequencer started`, {
140
+ publisher: this.publisher.getSenderAddress().toString(),
141
+ attestor: this.attestorAddress.toString()
142
+ });
143
+ this.queue.start();
144
+ // Fallback poller that triggers a build if we discover pending txs without an explicit
145
+ // notification. Tests can also call `buildIfPending()` directly to skip the poll wait.
146
+ this.mempoolPoller = new RunningPromise(()=>this.maybeEnqueueBuild(), this.log, this.pollIntervalMs);
147
+ this.mempoolPoller.start();
148
+ // Local-network only: settle epochs and reconcile the clock to L1 as checkpoints land. Both run
149
+ // through the same serial queue as builds/warps, so they never interleave with a checkpoint build.
150
+ if (this.deps.autoSettle) {
151
+ this.settler = new RunningPromise(()=>this.maybeSettle(), this.log, this.deps.settlePollIntervalMs ?? 200);
152
+ this.settler.start();
153
+ }
154
+ }
155
+ /**
156
+ * Stops the sequencer. Drains the queue, unsubscribes the mempool poller, and runs any
157
+ * registered {@link AutomineSequencerDeps.stopExtras} hook (e.g. the PublisherManager's
158
+ * funding loop). Idempotent: subsequent calls return without re-running cleanup.
159
+ */ async stop() {
160
+ if (this.stopped) {
161
+ return;
162
+ }
163
+ this.stopped = true;
164
+ this.running = false;
165
+ await this.mempoolPoller?.stop();
166
+ await this.settler?.stop();
167
+ await this.queue.end();
168
+ await this.deps.stopExtras?.();
169
+ this.log.info('AutomineSequencer stopped');
170
+ }
171
+ /**
172
+ * Enqueues a mempool-driven build. Coalesces consecutive calls so a burst of new txs
173
+ * collapses into one build job. Returns the built block (or undefined if nothing was built
174
+ * or the sequencer is paused).
175
+ */ buildIfPending() {
176
+ if (!this.running || this.paused) {
177
+ return Promise.resolve(undefined);
178
+ }
179
+ if (this.buildQueued) {
180
+ // A build is already queued; coalesce. The pending build will pick up the new txs.
181
+ return this.buildQueued;
182
+ }
183
+ this.buildQueued = this.queue.put(async ()=>{
184
+ try {
185
+ return await this.runBuild({
186
+ allowEmpty: false
187
+ });
188
+ } finally{
189
+ this.buildQueued = undefined;
190
+ }
191
+ });
192
+ return this.buildQueued;
193
+ }
194
+ /**
195
+ * Pauses mempool-driven block production. Pending txs accumulate in the pool without being
196
+ * mined. Explicit test-harness operations (`buildEmptyBlock`, `warpTo`, `revertToCheckpoint`)
197
+ * continue to work; this only gates the mempool poller and `buildIfPending`.
198
+ */ pause() {
199
+ if (this.paused) {
200
+ return;
201
+ }
202
+ this.paused = true;
203
+ this.log.info('AutomineSequencer paused');
204
+ }
205
+ /** Resumes mempool-driven block production after a previous {@link pause}. */ resume() {
206
+ if (!this.paused) {
207
+ return;
208
+ }
209
+ this.paused = false;
210
+ this.log.info('AutomineSequencer resumed');
211
+ }
212
+ /**
213
+ * Updates the in-memory sequencer config. Mirrors {@link SequencerClient.updateConfig} so that
214
+ * `AztecNode.setConfig` (e.g. tests bumping `minTxsPerBlock` to bundle multiple txs into one
215
+ * block) propagates to the AutomineSequencer's gating logic in {@link runBuild}.
216
+ */ updateConfig(config) {
217
+ Object.assign(this.deps.config, config);
218
+ }
219
+ /** Returns a snapshot of the current sequencer config (used by pause/resume bookkeeping). */ getConfig() {
220
+ return this.deps.config;
221
+ }
222
+ /** Enqueues an empty-block build. Resolves to the built block. */ buildEmptyBlock() {
223
+ return this.queue.put(async ()=>{
224
+ const block = await this.runBuild({
225
+ allowEmpty: true
226
+ });
227
+ if (!block) {
228
+ throw new Error('buildEmptyBlock: runBuild returned undefined');
229
+ }
230
+ return block;
231
+ });
232
+ }
233
+ /**
234
+ * Warps L1 timestamp to `targetTimestampSec`. Rounded up to the next aztec-slot
235
+ * boundary so the next build lands on a fresh slot. Atomic with respect to builds —
236
+ * the queue ensures no build is in flight while the warp executes. A no-op when the
237
+ * target is already at or behind the current L1 time (see {@link runWarp}).
238
+ */ warpTo(targetTimestampSec) {
239
+ return this.queue.put(()=>this.runWarp(targetTimestampSec));
240
+ }
241
+ /**
242
+ * Warps L1 timestamp forward by `deltaSec` seconds from the current L1 time, rounded up to the next
243
+ * aztec-slot boundary. Throws if `deltaSec` is not positive (warping "by" a non-positive amount is a
244
+ * caller bug — unlike {@link warpTo}, which no-ops on a past target).
245
+ */ warpBy(deltaSec) {
246
+ if (deltaSec <= 0) {
247
+ throw new Error(`warpL2TimeAtLeastBy: duration must be positive, got ${deltaSec} seconds.`);
248
+ }
249
+ return this.queue.put(async ()=>{
250
+ const current = await this.deps.ethCheatCodes.lastBlockTimestamp();
251
+ await this.runWarp(current + deltaSec);
252
+ });
253
+ }
254
+ /**
255
+ * Reorgs L1 so that every L1 block strictly after the one that published
256
+ * `targetCheckpoint` is removed. The archiver, world-state, and date provider
257
+ * are all brought back in sync before the promise resolves.
258
+ *
259
+ * Runs inside the serial queue so it never interleaves with a build or warp.
260
+ */ revertToCheckpoint(targetCheckpoint) {
261
+ return this.queue.put(()=>this.runRevert(targetCheckpoint));
262
+ }
263
+ /** Awaits the queue draining to a fully idle state. */ syncPoint() {
264
+ return this.queue.syncPoint();
265
+ }
266
+ /**
267
+ * Synthetically "proves" the L2 chain up to `upToCheckpoint` (default: the latest checkpointed
268
+ * checkpoint). For every epoch newly covered — including a partial final epoch — the epoch out hash
269
+ * is written into the L1 Outbox so the L2-to-L1 messages in those checkpoints become consumable,
270
+ * then the rollup's proven tip is advanced to the target. There is no real proof; this is the
271
+ * local-network equivalent of an epoch proof landing on L1.
272
+ *
273
+ * Clamps the target down to the latest checkpointed checkpoint and no-ops when it is already proven.
274
+ * Runs inside the serial queue so it never interleaves with a build or warp.
275
+ *
276
+ * @returns The proven checkpoint after the call (the target, or the existing proven tip on no-op).
277
+ */ prove(upToCheckpoint) {
278
+ return this.queue.put(()=>this.runProve(upToCheckpoint));
279
+ }
280
+ /**
281
+ * Auto-settle tick (local-network only). Proving up to the latest checkpointed checkpoint also
282
+ * reconciles the clock at the head of {@link runProve}, so this single tick keeps both the
283
+ * proven tip and the date provider current even when no build is happening.
284
+ */ async maybeSettle() {
285
+ if (!this.running) {
286
+ return;
287
+ }
288
+ try {
289
+ await this.prove();
290
+ } catch (err) {
291
+ this.log.warn('Automine auto-settle tick failed', {
292
+ err: err instanceof Error ? err.message : String(err)
293
+ });
294
+ }
295
+ }
296
+ /**
297
+ * Advances the injected date provider to the latest *mined* L1 timestamp when it has fallen behind
298
+ * (e.g. an unrelated L1 tx mined a block between our builds). Never advances to the pending, un-mined
299
+ * timestamp. Keeps node-side consumers of `dateProvider.now()` aligned with L1 without our own builds.
300
+ */ async reconcileDateProvider() {
301
+ const lastTsMs = await this.deps.ethCheatCodes.lastBlockTimestamp() * 1000;
302
+ if (lastTsMs > this.deps.dateProvider.now()) {
303
+ this.deps.dateProvider.setTime(lastTsMs);
304
+ }
305
+ }
306
+ /** Called from the mempool poller. Enqueues a build if there are pending txs. */ async maybeEnqueueBuild() {
307
+ if (!this.running || this.paused || this.buildQueued) {
308
+ return;
309
+ }
310
+ try {
311
+ const pending = await this.deps.p2pClient.getPendingTxCount();
312
+ if (pending > 0) {
313
+ // Fire-and-forget; the build result is delivered via `buildIfPending()` callers,
314
+ // not via the poller.
315
+ void this.buildIfPending().catch((err)=>{
316
+ this.log.error('Mempool-driven build failed', err);
317
+ });
318
+ }
319
+ } catch (err) {
320
+ this.log.warn('Failed to poll mempool', {
321
+ err: err instanceof Error ? err.message : String(err)
322
+ });
323
+ }
324
+ }
325
+ /**
326
+ * Builds and publishes one checkpoint, returning the built block — or `undefined` when there was
327
+ * nothing to build or the propose failed. A failed propose mines no checkpoint on L1 (it reverts
328
+ * inside the multicall or is never sent), so recovery is purely local: the optimistic archiver insert
329
+ * is rolled back, the block's txs return to the pending pool, and the L1 nonce is reset. No L1 reorg
330
+ * is performed, and the build is not retried inline — once the txs are back in the pending pool the
331
+ * mempool poller re-enqueues a build on its next tick (see {@link maybeEnqueueBuild}).
332
+ */ async runBuild({ allowEmpty }) {
333
+ const env = {
334
+ stack: [],
335
+ error: void 0,
336
+ hasError: false
337
+ };
338
+ try {
339
+ if (!this.running || !this.publisher || !this.attestorAddress) {
340
+ return undefined;
341
+ }
342
+ await this.reconcileDateProvider();
343
+ const txCount = await this.deps.p2pClient.getPendingTxCount();
344
+ // For mempool-driven builds, wait for at least `minTxsPerBlock` pending txs (or 1 if not set)
345
+ // before building. This mirrors the production sequencer's `waitForMinTxs` behavior, and is
346
+ // required for tests that bundle multiple txs into one block via `setConfig({ minTxsPerBlock })`.
347
+ // Explicit empty-block / warp paths pass `allowEmpty: true` and bypass this gate.
348
+ const minRequired = allowEmpty ? 0 : Math.max(this.deps.config.minTxsPerBlock ?? 1, 1);
349
+ if (txCount < minRequired) {
350
+ return undefined;
351
+ }
352
+ // Decide target slot from the pending block's timestamp — picks up any prior
353
+ // `setNextBlockTimestamp` call (e.g. queued by runWarp) instead of assuming +1 over
354
+ // the last mined block.
355
+ const pendingBlockTs = await this.deps.ethCheatCodes.nextBlockTimestamp();
356
+ let targetSlot = Number(getSlotAtTimestamp(BigInt(pendingBlockTs), this.deps.l1Constants));
357
+ if (targetSlot <= this.lastBuiltSlot) {
358
+ // Pending block doesn't reach a new slot yet; advance to the next slot we own.
359
+ targetSlot = this.lastBuiltSlot + 1;
360
+ }
361
+ const slotBoundaryTs = Number(getTimestampForSlot(SlotNumber(targetSlot), this.deps.l1Constants));
362
+ // Pre-set anvil's next block timestamp only if the slot boundary is past what's already
363
+ // scheduled. setNextBlockTimestamp rejects values not strictly greater than the last block's
364
+ // timestamp, and pendingBlockTs is always > lastBlockTimestamp, so this guard is sufficient.
365
+ if (slotBoundaryTs > pendingBlockTs) {
366
+ await this.deps.ethCheatCodes.setNextBlockTimestamp(slotBoundaryTs);
367
+ }
368
+ const [tips, proposedCheckpoint] = await Promise.all([
369
+ this.deps.l2BlockSource.getL2Tips(),
370
+ this.deps.l2BlockSource.getProposedCheckpointData()
371
+ ]);
372
+ const syncedToBlockNumber = tips.proposed.number;
373
+ // Ensure world state has processed the archiver's tip before forking. Without this,
374
+ // world state may still be at the previous block (since it syncs asynchronously from
375
+ // the archiver), and `fork(syncedToBlockNumber)` would fail with
376
+ // "Unable to initialize from future block".
377
+ await this.deps.worldState.syncImmediate(BlockNumber(syncedToBlockNumber));
378
+ const nextBlockNumber = BlockNumber(syncedToBlockNumber + 1);
379
+ const parentCheckpointNumber = proposedCheckpoint?.checkpointNumber ?? tips.checkpointed.checkpoint.number;
380
+ const checkpointNumber = CheckpointNumber(parentCheckpointNumber + 1);
381
+ const targetEpoch = getEpochAtSlot(SlotNumber(targetSlot), this.deps.l1Constants);
382
+ this.log.verbose(`Building automine checkpoint`, {
383
+ checkpointNumber,
384
+ blockNumber: nextBlockNumber,
385
+ slot: targetSlot,
386
+ slotTimestamp: slotBoundaryTs,
387
+ txCount,
388
+ allowEmpty
389
+ });
390
+ const checkpointGlobals = await this.deps.globalsBuilder.buildCheckpointGlobalVariables(this.deps.coinbase, this.deps.feeRecipient, SlotNumber(targetSlot));
391
+ const l1ToL2Messages = await this.deps.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
392
+ const previousCheckpointOutHashes = await getPreviousCheckpointOutHashes({
393
+ blockSource: this.deps.l2BlockSource,
394
+ epoch: targetEpoch,
395
+ checkpointNumber,
396
+ l1Constants: this.deps.l1Constants,
397
+ pipeliningEnabled: false,
398
+ log: this.log
399
+ });
400
+ const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
401
+ const fork = _ts_add_disposable_resource(env, await this.deps.worldState.fork(syncedToBlockNumber, {
402
+ closeDelayMs: 0
403
+ }), true);
404
+ const checkpointBuilder = await this.deps.checkpointsBuilder.startCheckpoint(checkpointNumber, checkpointGlobals, feeAssetPriceModifier, l1ToL2Messages, previousCheckpointOutHashes, fork, this.log.getBindings());
405
+ // Block building only executes txs, and automine publishes straight to L1, so the proofs are never needed.
406
+ const pendingTxs = this.deps.p2pClient.iterateEligiblePendingTxs({
407
+ includeProof: false
408
+ });
409
+ const buildResult = await this.tryBuildBlock(checkpointBuilder, pendingTxs, nextBlockNumber, checkpointGlobals.timestamp, allowEmpty, checkpointNumber);
410
+ if (!buildResult) {
411
+ return undefined;
412
+ }
413
+ const checkpoint = await checkpointBuilder.completeCheckpoint();
414
+ // Empty CommitteeAttestationsAndSigners is accepted on-chain when committee size is 0.
415
+ const emptyAttestations = CommitteeAttestationsAndSigners.empty(this.deps.signatureContext);
416
+ const emptyAttestationsSignature = Signature.empty();
417
+ // Push the block and proposed checkpoint into the archiver locally BEFORE publishing to L1.
418
+ // This avoids racing the archiver's L1 polling: if the L1 publish happened first, polling
419
+ // could surface the checkpoint and reject our subsequent local push as duplicate. Pushing
420
+ // first means the archiver already has the proposed entry when L1 polling fires; the L1
421
+ // sync path then promotes the existing proposed checkpoint via promoteProposedToCheckpointed
422
+ // rather than re-adding it.
423
+ await this.deps.archiver.addBlock(buildResult.block);
424
+ await this.deps.archiver.addProposedCheckpoint({
425
+ header: checkpoint.header,
426
+ checkpointNumber,
427
+ startBlock: BlockNumber(buildResult.block.number),
428
+ blockCount: 1,
429
+ totalManaUsed: checkpoint.header.totalManaUsed.toBigInt(),
430
+ feeAssetPriceModifier
431
+ });
432
+ await this.publisher.enqueueProposeCheckpoint(checkpoint, emptyAttestations, emptyAttestationsSignature);
433
+ // Automine publishes synchronously in the current slot via `sendRequests`. It must NOT use the
434
+ // production `sendRequestsAt` (or `canProposeAt`), which always apply the one-slot pipelining
435
+ // offset — automine is the deliberate non-pipelined exception and builds/publishes in place.
436
+ const result = await this.publisher.sendRequests(SlotNumber(targetSlot));
437
+ const proposeSucceeded = !!result?.successfulActions?.some((action)=>action === 'propose');
438
+ if (!proposeSucceeded) {
439
+ this.log.warn('Automine propose did not succeed; rolled back optimistic insert, will rebuild from the poller', {
440
+ slot: targetSlot,
441
+ checkpointNumber,
442
+ successful: result?.successfulActions,
443
+ failed: result?.failedActions
444
+ });
445
+ await this.rollbackOptimisticInsert(syncedToBlockNumber);
446
+ return undefined;
447
+ }
448
+ // Force one full L1-sync cycle synchronously. The local addBlock/addProposedCheckpoint
449
+ // above advances the proposed tip, but tips.checkpointed and the L1->L2 inbox tree state
450
+ // only advance when the archiver observes the L1-confirmed checkpoint via its sync loop.
451
+ await this.deps.archiver.syncImmediate();
452
+ // Sync the date provider to the L1 block timestamp we just mined.
453
+ const newL1Ts = await this.deps.ethCheatCodes.lastBlockTimestamp();
454
+ this.deps.dateProvider.setTime(newL1Ts * 1000);
455
+ // A successful propose is validated on-chain against the mined L1 block's slot, so the mined slot
456
+ // should equal our target; warn (without rolling back — the checkpoint is on L1) if it ever differs.
457
+ const minedSlot = Number(getSlotAtTimestamp(BigInt(newL1Ts), this.deps.l1Constants));
458
+ if (minedSlot !== targetSlot) {
459
+ this.log.warn(`Automine checkpoint mined in slot ${minedSlot} but targeted ${targetSlot}`, {
460
+ minedSlot,
461
+ targetSlot,
462
+ checkpointNumber
463
+ });
464
+ }
465
+ this.lastBuiltSlot = targetSlot;
466
+ this.log.verbose(`Automine checkpoint published`, {
467
+ checkpointNumber,
468
+ blockNumber: nextBlockNumber,
469
+ slot: targetSlot,
470
+ l1Timestamp: newL1Ts,
471
+ txCount: buildResult.numTxs
472
+ });
473
+ return buildResult.block;
474
+ } catch (e) {
475
+ env.error = e;
476
+ env.hasError = true;
477
+ } finally{
478
+ const result = _ts_dispose_resources(env);
479
+ if (result) await result;
480
+ }
481
+ }
482
+ /**
483
+ * Undoes an optimistic archiver insert (uncheckpointed block + proposed checkpoint) without reorging
484
+ * L1: removes the uncheckpointed block and evicts the proposed checkpoint that referenced it, so the
485
+ * proposed tip drops back to `toBlockNumber`. `p2pClient.sync()` then observes the lowered tip and
486
+ * restores the block's txs to the pending pool; `worldState.syncImmediate()` drops any applied effects;
487
+ * and the L1 nonce is reset (a reverted-but-mined propose consumes one) so the build can be retried.
488
+ *
489
+ * Note: `archiver.rollbackTo` is NOT usable here — it is checkpoint-granular and no-ops on a
490
+ * proposed-only tip (the inserted checkpoint is not yet in the checkpointed set).
491
+ */ async rollbackOptimisticInsert(toBlockNumber) {
492
+ await this.deps.archiver.removeUncheckpointedBlocksAfter(toBlockNumber);
493
+ await this.deps.p2pClient.sync();
494
+ await this.deps.worldState.syncImmediate();
495
+ this.deps.l1TxUtils.forEach((utils)=>utils.resetNonce());
496
+ }
497
+ /**
498
+ * Warps L1 timestamp to (or past) `targetTimestampSec`, rounded up to the next aztec-slot
499
+ * boundary, by queuing an empty-checkpoint build at that slot. Mines exactly one L1 block
500
+ * (the propose tx auto-mined under anvil's automine mode), with the timestamp pre-set so
501
+ * the mined block lands on the slot boundary.
502
+ */ async runWarp(targetTimestampSec) {
503
+ await this.reconcileDateProvider();
504
+ const currentL1Ts = await this.deps.ethCheatCodes.lastBlockTimestamp();
505
+ if (targetTimestampSec <= currentL1Ts) {
506
+ this.log.debug(`Warp target ${targetTimestampSec} is not in the future of current L1 ts ${currentL1Ts}`);
507
+ return;
508
+ }
509
+ // Round up to the next aztec-slot boundary so the next build naturally lands on a new slot.
510
+ const targetSlot = Number(getSlotAtTimestamp(BigInt(targetTimestampSec), this.deps.l1Constants));
511
+ const slotBoundaryTs = Number(getTimestampForSlot(SlotNumber(targetSlot + 1), this.deps.l1Constants));
512
+ // Queue the next L1 block at the slot boundary timestamp, then build (and publish) an
513
+ // empty L2 checkpoint. The propose tx auto-mines a single L1 block at slotBoundaryTs,
514
+ // and `runBuild` syncs the date provider to the new L1 timestamp.
515
+ await this.deps.ethCheatCodes.setNextBlockTimestamp(slotBoundaryTs);
516
+ await this.runBuild({
517
+ allowEmpty: true
518
+ });
519
+ this.log.verbose(`Warped L1 to slot boundary`, {
520
+ slot: targetSlot + 1,
521
+ timestamp: slotBoundaryTs
522
+ });
523
+ }
524
+ /**
525
+ * Rolls L1 back to the block that published `targetCheckpoint`, drops the archiver's
526
+ * in-memory state to match, and resets internal slot bookkeeping.
527
+ */ async runRevert(targetCheckpoint) {
528
+ const checkpointData = await this.deps.l2BlockSource.getCheckpointData({
529
+ number: CheckpointNumber(targetCheckpoint)
530
+ });
531
+ if (!checkpointData) {
532
+ throw new Error(`AutomineSequencer: checkpoint ${targetCheckpoint} not found in archiver`);
533
+ }
534
+ const targetL1Block = Number(checkpointData.l1.blockNumber);
535
+ this.log.verbose(`Reverting to checkpoint ${targetCheckpoint}`, {
536
+ targetCheckpoint,
537
+ targetL1Block,
538
+ checkpointSlot: checkpointData.header.slotNumber
539
+ });
540
+ // Roll the archiver back to the last block of targetCheckpoint before the L1 reorg,
541
+ // since the archiver needs to fetch the target checkpoint's L1 block hash during rollback.
542
+ const lastBlockInCheckpoint = BlockNumber(checkpointData.startBlock + checkpointData.blockCount - 1);
543
+ await this.deps.archiver.rollbackTo(lastBlockInCheckpoint);
544
+ // Force the P2P block stream to run one cycle immediately so it processes the
545
+ // chain-pruned event triggered by the archiver rollback above. Without this, the
546
+ // P2P pool may not have restored rolled-back txs to pending by the time the next
547
+ // build runs.
548
+ await this.deps.p2pClient.sync();
549
+ // Force world-state to process the archiver's prune event immediately, so the next build
550
+ // doesn't try to insert nullifiers that were already in the pruned checkpoints.
551
+ await this.deps.worldState.syncImmediate();
552
+ // Remove all L1 blocks strictly after the target checkpoint's publish block so that
553
+ // the propose txs for later checkpoints are gone from L1. We use reorg(depth) directly
554
+ // to keep targetL1Block itself as the new chain tip.
555
+ const currentL1Block = await this.deps.ethCheatCodes.publicClient.getBlockNumber();
556
+ const depth = Number(currentL1Block) - targetL1Block;
557
+ if (depth > 0) {
558
+ await this.deps.ethCheatCodes.reorg(depth);
559
+ }
560
+ // anvil_rollback re-queues the rolled-back txs into the mempool. Clear them so they
561
+ // don't get re-mined, then reset the publisher nonce tracker so the next propose tx
562
+ // uses the correct nonce for the post-reorg chain state.
563
+ await this.deps.ethCheatCodes.rpcCall('anvil_dropAllTransactions', []);
564
+ this.deps.l1TxUtils.forEach((utils)=>utils.resetNonce());
565
+ // Reset slot bookkeeping so the next build picks up at the correct slot.
566
+ this.lastBuiltSlot = Number(checkpointData.header.slotNumber);
567
+ this.log.verbose(`Reverted to checkpoint ${targetCheckpoint}`, {
568
+ targetCheckpoint,
569
+ targetL1Block
570
+ });
571
+ }
572
+ /**
573
+ * Writes outbox roots for every epoch newly covered up to `maybeCheckpoint` and advances the proven
574
+ * tip. Settles each fully-covered epoch in full and the final epoch only up to the target checkpoint
575
+ * (a partial epoch, which the AZIP-14 Outbox supports via per-`numCheckpointsInEpoch` roots).
576
+ */ async runProve(upToCheckpoint) {
577
+ await this.reconcileDateProvider();
578
+ const rollupCheatCodes = this.getRollupCheatCodes();
579
+ const tips = await this.deps.l2BlockSource.getL2Tips();
580
+ const checkpointedTip = tips.checkpointed.checkpoint.number;
581
+ // Never prove beyond what the archiver has actually checkpointed; default to that tip.
582
+ const target = CheckpointNumber(Math.min(upToCheckpoint ?? checkpointedTip, checkpointedTip));
583
+ const { proven } = await rollupCheatCodes.getTips();
584
+ if (target <= proven) {
585
+ this.log.debug(`Checkpoint ${target} already proven`, {
586
+ target,
587
+ proven,
588
+ checkpointedTip
589
+ });
590
+ return proven;
591
+ }
592
+ const startEpoch = await this.getEpochOfCheckpoint(CheckpointNumber(proven + 1));
593
+ const endEpoch = await this.getEpochOfCheckpoint(target);
594
+ if (startEpoch === undefined || endEpoch === undefined) {
595
+ this.log.warn(`Cannot resolve epoch range to prove up to checkpoint ${target}`, {
596
+ target,
597
+ proven,
598
+ startEpoch,
599
+ endEpoch
600
+ });
601
+ return proven;
602
+ }
603
+ for(let epoch = startEpoch; epoch <= endEpoch; epoch++){
604
+ const lastCovered = await settleEpochOutbox({
605
+ rollupCheatCodes,
606
+ l2BlockSource: this.deps.l2BlockSource,
607
+ epoch: EpochNumber(epoch),
608
+ maxCheckpoint: epoch === endEpoch ? target : undefined,
609
+ log: this.log
610
+ });
611
+ if (lastCovered === undefined) {
612
+ // An epoch in (proven, target] with no checkpointed blocks — expected when warps skip a whole
613
+ // epoch. Logged so it's distinguishable from the archiver failing to serve the epoch's blocks.
614
+ this.log.debug(`No checkpointed blocks to settle for epoch ${epoch} while proving to ${target}`, {
615
+ epoch,
616
+ target
617
+ });
618
+ }
619
+ }
620
+ await rollupCheatCodes.markAsProven(target);
621
+ // Settlement is a direct L1 storage write that mines no block, unlike a real epoch proof landing
622
+ // on L1. The archiver's L1 sync short-circuits while the L1 block hash is unchanged, so it would
623
+ // never re-read the proven tip until the next build/warp mines a block. Mine one empty L1 block so
624
+ // the block hash advances, then force an immediate sync that observes the new proven checkpoint.
625
+ await this.deps.ethCheatCodes.mineEmptyBlock();
626
+ await this.deps.archiver.syncImmediate();
627
+ this.log.verbose(`Proved up to checkpoint ${target}`, {
628
+ target,
629
+ proven,
630
+ startEpoch,
631
+ endEpoch
632
+ });
633
+ return target;
634
+ }
635
+ /** Resolves the epoch a checkpoint belongs to from its slot, or undefined if the archiver lacks it. */ async getEpochOfCheckpoint(checkpointNumber) {
636
+ const checkpointData = await this.deps.l2BlockSource.getCheckpointData({
637
+ number: checkpointNumber
638
+ });
639
+ if (!checkpointData) {
640
+ return undefined;
641
+ }
642
+ const slot = SlotNumber(Number(checkpointData.header.slotNumber));
643
+ return Number(getEpochAtSlot(slot, this.deps.l1Constants));
644
+ }
645
+ /**
646
+ * Wraps `checkpointBuilder.buildBlock` with the failed-tx handling shared by both error
647
+ * and success paths: drops the failed txs from the P2P mempool, and returns `undefined`
648
+ * when `InsufficientValidTxsError` aborts the build (so the caller skips publishing).
649
+ */ async tryBuildBlock(checkpointBuilder, pendingTxs, nextBlockNumber, timestamp, allowEmpty, checkpointNumber) {
650
+ let buildResult;
651
+ try {
652
+ buildResult = await checkpointBuilder.buildBlock(pendingTxs, nextBlockNumber, timestamp, {
653
+ maxTransactions: this.deps.config.maxTxsPerBlock,
654
+ // Allow empty for explicit-empty builds; require at least 1 valid tx otherwise.
655
+ minValidTxs: allowEmpty ? 0 : 1,
656
+ isBuildingProposal: true,
657
+ maxBlocksPerCheckpoint: 1,
658
+ perBlockAllocationMultiplier: 1
659
+ });
660
+ } catch (err) {
661
+ // Mirrors production's checkpoint_proposal_job: if every pending tx failed execution and
662
+ // we didn't reach minValidTxs, drop the failed txs from the mempool so they don't block
663
+ // the poller forever, then abort this build with no checkpoint published.
664
+ if (isErrorClass(err, InsufficientValidTxsError)) {
665
+ await this.dropFailedTxsFromP2P(err.failedTxs);
666
+ this.log.verbose(`AutomineSequencer: insufficient valid txs, skipping build`, {
667
+ checkpointNumber,
668
+ processedCount: err.processedCount,
669
+ minRequired: err.minRequired,
670
+ failedCount: err.failedTxs.length
671
+ });
672
+ return undefined;
673
+ }
674
+ throw err;
675
+ }
676
+ // Drop any txs that failed execution but didn't trigger InsufficientValidTxsError, so we
677
+ // don't re-pick them up on the next build.
678
+ await this.dropFailedTxsFromP2P(buildResult.failedTxs);
679
+ return buildResult;
680
+ }
681
+ /** Removes txs that failed execution from the P2P mempool so they don't get retried. */ async dropFailedTxsFromP2P(failedTxs) {
682
+ if (failedTxs.length === 0) {
683
+ return;
684
+ }
685
+ const failedTxHashes = failedTxs.map((fail)=>fail.tx.getTxHash());
686
+ this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
687
+ await this.deps.p2pClient.handleFailedExecution(failedTxHashes);
688
+ }
689
+ }