@aztec/aztec-node 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 (48) hide show
  1. package/dest/aztec-node/config.d.ts +3 -1
  2. package/dest/aztec-node/config.d.ts.map +1 -1
  3. package/dest/aztec-node/config.js +5 -0
  4. package/dest/aztec-node/node_public_calls_simulator.d.ts +90 -0
  5. package/dest/aztec-node/node_public_calls_simulator.d.ts.map +1 -0
  6. package/dest/aztec-node/node_public_calls_simulator.js +346 -0
  7. package/dest/aztec-node/server.d.ts +68 -65
  8. package/dest/aztec-node/server.d.ts.map +1 -1
  9. package/dest/aztec-node/server.js +153 -1161
  10. package/dest/bin/index.js +2 -2
  11. package/dest/factory.d.ts +33 -0
  12. package/dest/factory.d.ts.map +1 -0
  13. package/dest/factory.js +496 -0
  14. package/dest/index.d.ts +2 -1
  15. package/dest/index.d.ts.map +1 -1
  16. package/dest/index.js +1 -0
  17. package/dest/modules/block_parameter.d.ts +25 -0
  18. package/dest/modules/block_parameter.d.ts.map +1 -0
  19. package/dest/modules/block_parameter.js +100 -0
  20. package/dest/modules/node_block_provider.d.ts +19 -0
  21. package/dest/modules/node_block_provider.d.ts.map +1 -0
  22. package/dest/modules/node_block_provider.js +112 -0
  23. package/dest/modules/node_tx_receipt.d.ts +24 -0
  24. package/dest/modules/node_tx_receipt.d.ts.map +1 -0
  25. package/dest/modules/node_tx_receipt.js +70 -0
  26. package/dest/modules/node_world_state_queries.d.ts +61 -0
  27. package/dest/modules/node_world_state_queries.d.ts.map +1 -0
  28. package/dest/modules/node_world_state_queries.js +257 -0
  29. package/dest/sentinel/factory.d.ts +3 -3
  30. package/dest/sentinel/factory.d.ts.map +1 -1
  31. package/dest/sentinel/factory.js +8 -1
  32. package/dest/sentinel/sentinel.d.ts +21 -21
  33. package/dest/sentinel/sentinel.d.ts.map +1 -1
  34. package/dest/sentinel/sentinel.js +27 -47
  35. package/package.json +28 -27
  36. package/src/aztec-node/config.ts +7 -0
  37. package/src/aztec-node/node_public_calls_simulator.ts +383 -0
  38. package/src/aztec-node/server.ts +221 -1342
  39. package/src/bin/index.ts +7 -2
  40. package/src/factory.ts +656 -0
  41. package/src/index.ts +1 -0
  42. package/src/modules/block_parameter.ts +93 -0
  43. package/src/modules/node_block_provider.ts +149 -0
  44. package/src/modules/node_tx_receipt.ts +115 -0
  45. package/src/modules/node_world_state_queries.ts +360 -0
  46. package/src/sentinel/README.md +3 -3
  47. package/src/sentinel/factory.ts +15 -3
  48. package/src/sentinel/sentinel.ts +34 -72
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/aztec-node",
3
- "version": "5.0.0-rc.1",
3
+ "version": "5.0.0-rc.2",
4
4
  "main": "dest/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -65,32 +65,33 @@
65
65
  ]
66
66
  },
67
67
  "dependencies": {
68
- "@aztec/archiver": "5.0.0-rc.1",
69
- "@aztec/bb-prover": "5.0.0-rc.1",
70
- "@aztec/blob-client": "5.0.0-rc.1",
71
- "@aztec/blob-lib": "5.0.0-rc.1",
72
- "@aztec/constants": "5.0.0-rc.1",
73
- "@aztec/epoch-cache": "5.0.0-rc.1",
74
- "@aztec/ethereum": "5.0.0-rc.1",
75
- "@aztec/foundation": "5.0.0-rc.1",
76
- "@aztec/kv-store": "5.0.0-rc.1",
77
- "@aztec/l1-artifacts": "5.0.0-rc.1",
78
- "@aztec/node-keystore": "5.0.0-rc.1",
79
- "@aztec/node-lib": "5.0.0-rc.1",
80
- "@aztec/noir-protocol-circuits-types": "5.0.0-rc.1",
81
- "@aztec/p2p": "5.0.0-rc.1",
82
- "@aztec/protocol-contracts": "5.0.0-rc.1",
83
- "@aztec/prover-client": "5.0.0-rc.1",
84
- "@aztec/prover-node": "5.0.0-rc.1",
85
- "@aztec/sequencer-client": "5.0.0-rc.1",
86
- "@aztec/simulator": "5.0.0-rc.1",
87
- "@aztec/slasher": "5.0.0-rc.1",
88
- "@aztec/standard-contracts": "5.0.0-rc.1",
89
- "@aztec/stdlib": "5.0.0-rc.1",
90
- "@aztec/telemetry-client": "5.0.0-rc.1",
91
- "@aztec/validator-client": "5.0.0-rc.1",
92
- "@aztec/validator-ha-signer": "5.0.0-rc.1",
93
- "@aztec/world-state": "5.0.0-rc.1",
68
+ "@aztec/archiver": "5.0.0-rc.2",
69
+ "@aztec/bb-prover": "5.0.0-rc.2",
70
+ "@aztec/bb.js": "5.0.0-rc.2",
71
+ "@aztec/blob-client": "5.0.0-rc.2",
72
+ "@aztec/blob-lib": "5.0.0-rc.2",
73
+ "@aztec/constants": "5.0.0-rc.2",
74
+ "@aztec/epoch-cache": "5.0.0-rc.2",
75
+ "@aztec/ethereum": "5.0.0-rc.2",
76
+ "@aztec/foundation": "5.0.0-rc.2",
77
+ "@aztec/kv-store": "5.0.0-rc.2",
78
+ "@aztec/l1-artifacts": "5.0.0-rc.2",
79
+ "@aztec/node-keystore": "5.0.0-rc.2",
80
+ "@aztec/node-lib": "5.0.0-rc.2",
81
+ "@aztec/noir-protocol-circuits-types": "5.0.0-rc.2",
82
+ "@aztec/p2p": "5.0.0-rc.2",
83
+ "@aztec/protocol-contracts": "5.0.0-rc.2",
84
+ "@aztec/prover-client": "5.0.0-rc.2",
85
+ "@aztec/prover-node": "5.0.0-rc.2",
86
+ "@aztec/sequencer-client": "5.0.0-rc.2",
87
+ "@aztec/simulator": "5.0.0-rc.2",
88
+ "@aztec/slasher": "5.0.0-rc.2",
89
+ "@aztec/standard-contracts": "5.0.0-rc.2",
90
+ "@aztec/stdlib": "5.0.0-rc.2",
91
+ "@aztec/telemetry-client": "5.0.0-rc.2",
92
+ "@aztec/validator-client": "5.0.0-rc.2",
93
+ "@aztec/validator-ha-signer": "5.0.0-rc.2",
94
+ "@aztec/world-state": "5.0.0-rc.2",
94
95
  "koa": "^2.16.1",
95
96
  "koa-router": "^13.1.1",
96
97
  "tslib": "^2.4.0",
@@ -60,6 +60,8 @@ export type AztecNodeConfig = ArchiverConfig &
60
60
  debugForceTxProofVerification: boolean;
61
61
  /** Whether to enable the prover node as a subsystem. */
62
62
  enableProverNode: boolean;
63
+ /** Whether to run the slashing watchers to collect offenses even if not a validator. */
64
+ enableOffenseCollection: boolean;
63
65
  /**
64
66
  * Test-only: use the deterministic AutomineSequencer instead of the production Sequencer.
65
67
  * Requires `aztecTargetCommitteeSize === 0` on the deployed rollup and anvil-backed L1.
@@ -111,6 +113,11 @@ export const aztecNodeConfigMappings: ConfigMappingsType<AztecNodeConfig> = {
111
113
  description: 'Whether to enable the prover node as a subsystem.',
112
114
  ...booleanConfigHelper(false),
113
115
  },
116
+ enableOffenseCollection: {
117
+ env: 'OFFENSE_COLLECTION_ENABLED',
118
+ description: 'Whether to run the slashing watchers to collect offenses even if not a validator.',
119
+ ...booleanConfigHelper(false),
120
+ },
114
121
  useAutomineSequencer: {
115
122
  env: 'USE_AUTOMINE_SEQUENCER',
116
123
  description: 'Test-only: use AutomineSequencer instead of the production Sequencer.',
@@ -0,0 +1,383 @@
1
+ import { L1ToL2MessagesNotReadyError } from '@aztec/archiver';
2
+ import { PROPOSER_PIPELINING_SLOT_OFFSET } from '@aztec/epoch-cache';
3
+ import type { EpochCacheInterface } from '@aztec/epoch-cache';
4
+ import {
5
+ type RollupContract,
6
+ SimulationOverridesBuilder,
7
+ type SimulationOverridesPlan,
8
+ } from '@aztec/ethereum/contracts';
9
+ import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
10
+ import { compactArray } from '@aztec/foundation/collection';
11
+ import { Fr } from '@aztec/foundation/curves/bn254';
12
+ import { EthAddress } from '@aztec/foundation/eth-address';
13
+ import { BadRequestError } from '@aztec/foundation/json-rpc';
14
+ import { type Logger, createLogger } from '@aztec/foundation/log';
15
+ import { DateProvider } from '@aztec/foundation/timer';
16
+ import { isErrorClass } from '@aztec/foundation/types';
17
+ import { PublicContractsDB, PublicProcessorFactory } from '@aztec/simulator/server';
18
+ import { CollectionLimitsConfig, PublicSimulatorConfig } from '@aztec/stdlib/avm';
19
+ import { AztecAddress } from '@aztec/stdlib/aztec-address';
20
+ import type { L2BlockSource, L2Tips } from '@aztec/stdlib/block';
21
+ import { type ProposedCheckpointData, buildCheckpointSimulationOverridesPlan } from '@aztec/stdlib/checkpoint';
22
+ import type { ContractDataSource } from '@aztec/stdlib/contract';
23
+ import type { WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
24
+ import { type L1ToL2MessageSource, appendL1ToL2MessagesToTree } from '@aztec/stdlib/messaging';
25
+ import type { CoordinationSignatureContext } from '@aztec/stdlib/p2p';
26
+ import {
27
+ type GlobalVariableBuilder,
28
+ GlobalVariables,
29
+ PublicSimulationOutput,
30
+ type SimulationOverrides,
31
+ type Tx,
32
+ } from '@aztec/stdlib/tx';
33
+ import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
34
+
35
+ import { applyPublicDataOverrides } from './public_data_overrides.js';
36
+
37
+ /** Config fields the simulator needs — a narrow subset of `AztecNodeConfig`. */
38
+ export interface NodePublicCallsSimulatorConfig {
39
+ /** Maximum total gas limit accepted for an incoming simulation. */
40
+ rpcSimulatePublicMaxGasLimit: number;
41
+ /** Maximum number of debug-log memory reads collected during simulation. */
42
+ rpcSimulatePublicMaxDebugLogMemoryReads: number;
43
+ }
44
+
45
+ /** Dependencies required to build a {@link NodePublicCallsSimulator}. */
46
+ export interface NodePublicCallsSimulatorDeps {
47
+ blockSource: L2BlockSource;
48
+ worldStateSynchronizer: WorldStateSynchronizer;
49
+ l1ToL2MessageSource: L1ToL2MessageSource;
50
+ contractDataSource: ContractDataSource;
51
+ globalVariableBuilder: GlobalVariableBuilder;
52
+ /**
53
+ * Rollup contract used to build the fee-relevant L1 state overrides when opening a new checkpoint.
54
+ * Only needed when a proposed parent checkpoint exists (pipelining) or the pending chain is invalid;
55
+ * may be omitted in environments that never reach those states (e.g. TXE). When omitted, those paths
56
+ * degrade to a pinned-tips plan (non-pipelined fees) instead.
57
+ */
58
+ rollupContract?: RollupContract;
59
+ epochCache: EpochCacheInterface;
60
+ signatureContext: CoordinationSignatureContext;
61
+ config: NodePublicCallsSimulatorConfig;
62
+ telemetry?: TelemetryClient;
63
+ log?: Logger;
64
+ }
65
+
66
+ /**
67
+ * Simulates the public part of a transaction against a fresh world-state fork.
68
+ *
69
+ * Extracted from `AztecNodeService` so the slot/globals selection can be unit-tested without
70
+ * standing up the whole node, and to keep `server.ts` smaller.
71
+ *
72
+ * The simulator picks globals in one of two ways, mirroring how the sequencer builds the next block:
73
+ * - **When the next block continues an in-progress checkpoint** (the latest proposed block is ahead of
74
+ * the proposed-checkpoint frontier): every block in a checkpoint shares the same
75
+ * `CheckpointGlobalVariables`, so we copy the latest proposed block's globals verbatim and only
76
+ * bump the block number. No L1 calls, no L1-to-L2 message insertion.
77
+ * - **When the next block opens a new checkpoint** (the latest proposed block coincides with the
78
+ * proposed-checkpoint frontier): we compute fresh globals for the slot the next block will land in,
79
+ * applying the same `SimulationOverridesPlan` the sequencer applies so the simulated mana min fee
80
+ * matches what the sequencer will write into the block header.
81
+ */
82
+ export class NodePublicCallsSimulator {
83
+ private readonly blockSource: L2BlockSource;
84
+ private readonly worldStateSynchronizer: WorldStateSynchronizer;
85
+ private readonly l1ToL2MessageSource: L1ToL2MessageSource;
86
+ private readonly contractDataSource: ContractDataSource;
87
+ private readonly globalVariableBuilder: GlobalVariableBuilder;
88
+ private readonly rollupContract: RollupContract | undefined;
89
+ private readonly epochCache: EpochCacheInterface;
90
+ private readonly signatureContext: CoordinationSignatureContext;
91
+ private readonly config: NodePublicCallsSimulatorConfig;
92
+ private readonly telemetry: TelemetryClient;
93
+ private readonly log: Logger;
94
+
95
+ constructor(deps: NodePublicCallsSimulatorDeps) {
96
+ this.blockSource = deps.blockSource;
97
+ this.worldStateSynchronizer = deps.worldStateSynchronizer;
98
+ this.l1ToL2MessageSource = deps.l1ToL2MessageSource;
99
+ this.contractDataSource = deps.contractDataSource;
100
+ this.globalVariableBuilder = deps.globalVariableBuilder;
101
+ this.rollupContract = deps.rollupContract;
102
+ this.epochCache = deps.epochCache;
103
+ this.signatureContext = deps.signatureContext;
104
+ this.config = deps.config;
105
+ this.telemetry = deps.telemetry ?? getTelemetryClient();
106
+ this.log = deps.log ?? createLogger('node:public-calls-simulator');
107
+ }
108
+
109
+ /**
110
+ * Simulates the public part of a transaction with the current state.
111
+ * @param tx - The transaction to simulate.
112
+ * @param skipFeeEnforcement - If true, fee enforcement is skipped.
113
+ * @param overrides - Optional pre-simulation overrides applied to the ephemeral fork and contract DB.
114
+ */
115
+ public async simulate(
116
+ tx: Tx,
117
+ skipFeeEnforcement = false,
118
+ overrides?: SimulationOverrides,
119
+ ): Promise<PublicSimulationOutput> {
120
+ // Check total gas limit for simulation
121
+ const gasSettings = tx.data.constants.txContext.gasSettings;
122
+ const txGasLimit = gasSettings.gasLimits.l2Gas;
123
+ const teardownGasLimit = gasSettings.teardownGasLimits.l2Gas;
124
+ if (txGasLimit + teardownGasLimit > this.config.rpcSimulatePublicMaxGasLimit) {
125
+ throw new BadRequestError(
126
+ `Transaction total gas limit ${
127
+ txGasLimit + teardownGasLimit
128
+ } (${txGasLimit} + ${teardownGasLimit}) exceeds maximum gas limit ${
129
+ this.config.rpcSimulatePublicMaxGasLimit
130
+ } for simulation`,
131
+ );
132
+ }
133
+
134
+ const txHash = tx.getTxHash();
135
+ const [l2Tips, proposedCheckpointData] = await Promise.all([
136
+ this.blockSource.getL2Tips(),
137
+ this.blockSource.getProposedCheckpointData(),
138
+ ]);
139
+ const latestBlockNumber = l2Tips.proposed.number;
140
+ const blockNumber = BlockNumber.add(latestBlockNumber, 1);
141
+
142
+ // Terminating block of the proposed-checkpoint frontier. `getProposedCheckpointData()` returns the
143
+ // leading proposed (not-yet-L1-confirmed) checkpoint, whose last block is `startBlock + blockCount
144
+ // - 1`; with no proposed checkpoint the frontier coincides with the checkpointed tip.
145
+ const proposedCheckpointLastBlock = proposedCheckpointData
146
+ ? BlockNumber.add(proposedCheckpointData.startBlock, proposedCheckpointData.blockCount - 1)
147
+ : l2Tips.checkpointed.block.number;
148
+
149
+ // The next block continues the in-progress checkpoint when the latest proposed block is ahead of
150
+ // the proposed-checkpoint terminating block; it opens a new checkpoint when they coincide.
151
+ const atCheckpointBoundary = proposedCheckpointLastBlock === l2Tips.proposed.number;
152
+
153
+ // `targetCheckpoint` is the checkpoint whose L1-to-L2 messages must be inserted into the fork
154
+ // before simulation. Only set when opening a new checkpoint, where the next block is its first block.
155
+ const { globalVariables: newGlobalVariables, targetCheckpoint } = atCheckpointBoundary
156
+ ? await this.buildGlobalVariablesForNewCheckpoint(l2Tips, proposedCheckpointData, blockNumber)
157
+ : { globalVariables: await this.copyGlobalVariablesFromLatestProposedBlock(latestBlockNumber, blockNumber) };
158
+
159
+ const publicProcessorFactory = new PublicProcessorFactory(
160
+ this.contractDataSource,
161
+ new DateProvider(),
162
+ this.telemetry,
163
+ this.log.getBindings(),
164
+ );
165
+
166
+ this.log.verbose(`Simulating public calls for tx ${txHash}`, {
167
+ globalVariables: newGlobalVariables.toInspect(),
168
+ txHash,
169
+ blockNumber,
170
+ atCheckpointBoundary,
171
+ });
172
+
173
+ // Ensure world-state has caught up with the latest block we loaded from the archiver
174
+ await this.worldStateSynchronizer.syncImmediate(latestBlockNumber);
175
+
176
+ const nextCheckpointMessages = await this.getNextCheckpointMessages(targetCheckpoint);
177
+
178
+ // Request a new fork of the world state at the latest block number, and apply any overrides and next checkpoint messages to it before simulation
179
+ await using merkleTreeFork = await this.worldStateSynchronizer.fork(latestBlockNumber);
180
+
181
+ if (nextCheckpointMessages !== undefined) {
182
+ this.log.debug(
183
+ `Appending ${nextCheckpointMessages.length} L1-to-L2 messages to the world state tree for the next checkpoint`,
184
+ { checkpointNumber: targetCheckpoint },
185
+ );
186
+ await appendL1ToL2MessagesToTree(merkleTreeFork, nextCheckpointMessages);
187
+ }
188
+
189
+ await applyPublicDataOverrides(merkleTreeFork, overrides?.publicStorage);
190
+
191
+ const config = PublicSimulatorConfig.from({
192
+ skipFeeEnforcement,
193
+ collectDebugLogs: true,
194
+ collectHints: false,
195
+ collectCallMetadata: true,
196
+ collectStatistics: false,
197
+ collectionLimits: CollectionLimitsConfig.from({
198
+ maxDebugLogMemoryReads: this.config.rpcSimulatePublicMaxDebugLogMemoryReads,
199
+ }),
200
+ });
201
+
202
+ const contractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings());
203
+ if (overrides?.contracts) {
204
+ contractsDB.addContracts(Object.values(overrides.contracts).map(({ instance }) => instance));
205
+ }
206
+ const processor = publicProcessorFactory.create(merkleTreeFork, newGlobalVariables, config, contractsDB);
207
+
208
+ // REFACTOR: Consider merging ProcessReturnValues into ProcessedTx
209
+ const [processedTxs, failedTxs, _usedTxs, returns, debugLogs] = await processor.process([tx]);
210
+ // REFACTOR: Consider returning the error rather than throwing
211
+ if (failedTxs.length) {
212
+ this.log.warn(`Simulated tx ${txHash} fails: ${failedTxs[0].error}`, { txHash });
213
+ throw failedTxs[0].error;
214
+ }
215
+
216
+ const [processedTx] = processedTxs;
217
+ return new PublicSimulationOutput(
218
+ processedTx.revertReason,
219
+ processedTx.globalVariables,
220
+ processedTx.txEffect,
221
+ returns,
222
+ processedTx.gasUsed,
223
+ debugLogs,
224
+ );
225
+ }
226
+
227
+ /**
228
+ * Fetches the next checkpoint's L1-to-L2 messages to insert into the fork before simulation. Only set
229
+ * when opening a new checkpoint; when continuing an in-progress checkpoint the ongoing checkpoint's
230
+ * messages were already applied when its first block synced, so inserting here would double-count them
231
+ * — which is why a missing header for the latest proposed block throws rather than falling through to
232
+ * this path. A not-ready or failed fetch degrades to simulating without the messages rather than
233
+ * failing the request.
234
+ */
235
+ private async getNextCheckpointMessages(targetCheckpoint: CheckpointNumber | undefined): Promise<Fr[] | undefined> {
236
+ if (targetCheckpoint === undefined) {
237
+ return undefined;
238
+ }
239
+ try {
240
+ return await this.l1ToL2MessageSource.getL1ToL2Messages(targetCheckpoint);
241
+ } catch (err) {
242
+ if (isErrorClass(err, L1ToL2MessagesNotReadyError)) {
243
+ this.log.warn(
244
+ `L1-to-L2 messages for checkpoint ${targetCheckpoint} are not ready yet (simulating without them)`,
245
+ { checkpointNumber: targetCheckpoint },
246
+ );
247
+ } else {
248
+ this.log.error(
249
+ `Failed to get L1-to-L2 messages for checkpoint ${targetCheckpoint} (simulating without them)`,
250
+ err,
251
+ { checkpointNumber: targetCheckpoint },
252
+ );
253
+ }
254
+ return undefined;
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Continues an in-progress checkpoint: the next block extends the checkpoint the latest proposed
260
+ * block belongs to. Every block in a checkpoint shares the same `CheckpointGlobalVariables`, so the
261
+ * next block's globals are the latest proposed block's globals with only the block number bumped —
262
+ * including the proposer's real coinbase/feeRecipient. No L1 reads and no L1-to-L2 message insertion
263
+ * happen here.
264
+ *
265
+ * A missing header means the archiver reported a proposed tip via `getL2Tips` but no longer has its
266
+ * data (a torn snapshot). We throw a transient/retryable error rather than treating the next block as
267
+ * opening a new checkpoint: the fork at `latestBlockNumber` already contains the ongoing checkpoint's
268
+ * L1-to-L2 messages, so inserting the next checkpoint's messages would append them a second time.
269
+ */
270
+ private async copyGlobalVariablesFromLatestProposedBlock(
271
+ latestBlockNumber: BlockNumber,
272
+ blockNumber: BlockNumber,
273
+ ): Promise<GlobalVariables> {
274
+ const latestBlockData = await this.blockSource.getBlockData({ number: latestBlockNumber });
275
+ if (!latestBlockData) {
276
+ throw new Error(
277
+ `Cannot simulate public calls: latest proposed block ${latestBlockNumber} has no header on this node ` +
278
+ `(torn archiver snapshot); retry`,
279
+ );
280
+ }
281
+ return GlobalVariables.from({ ...latestBlockData.header.globalVariables, blockNumber });
282
+ }
283
+
284
+ /**
285
+ * Opens a new checkpoint: the next block is the first of a fresh checkpoint. Picks the slot the next
286
+ * block will land in, mirroring the sequencer, and builds the same `SimulationOverridesPlan` the
287
+ * sequencer applies so the simulated mana min fee matches what the sequencer will write into the
288
+ * block header. Coinbase and fee recipient stay zero (we cannot know the future proposer's payout
289
+ * addresses), unlike continuing an in-progress checkpoint which inherits the real ones from the
290
+ * proposed header. Returns the target checkpoint so the caller inserts that checkpoint's L1-to-L2
291
+ * messages into the fork.
292
+ */
293
+ private async buildGlobalVariablesForNewCheckpoint(
294
+ l2Tips: L2Tips,
295
+ proposedCheckpointData: ProposedCheckpointData | undefined,
296
+ blockNumber: BlockNumber,
297
+ ): Promise<{ globalVariables: GlobalVariables; targetCheckpoint: CheckpointNumber }> {
298
+ const checkpointedCheckpointNumber = l2Tips.checkpointed.checkpoint.number;
299
+ // The new checkpoint sits on top of the proposed one when pipelining, otherwise on the
300
+ // checkpointed tip. The target slot and the overrides plan both derive from the single
301
+ // `proposedCheckpointData` read, so they cannot disagree about the proposed parent.
302
+ const proposedCheckpointNumber = proposedCheckpointData?.checkpointNumber ?? checkpointedCheckpointNumber;
303
+
304
+ const targetSlot = this.computeTargetSlot(proposedCheckpointData);
305
+ const plan = await this.buildSimulationOverridesPlan(proposedCheckpointData, checkpointedCheckpointNumber);
306
+
307
+ const checkpointGlobalVariables = await this.globalVariableBuilder.buildCheckpointGlobalVariables(
308
+ EthAddress.ZERO,
309
+ AztecAddress.ZERO,
310
+ targetSlot,
311
+ plan,
312
+ );
313
+
314
+ return {
315
+ globalVariables: GlobalVariables.from({ blockNumber, ...checkpointGlobalVariables }),
316
+ targetCheckpoint: CheckpointNumber(proposedCheckpointNumber + 1),
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Slot the next block will land in. The first term is the sequencer's exact formula
322
+ * (`getEpochAndSlotInNextL1Slot().slot + PROPOSER_PIPELINING_SLOT_OFFSET`). The `max` with
323
+ * `proposedCheckpointSlot + 1` is an RPC-side approximation of the next build: when a proposed
324
+ * checkpoint is gossiped before its L1 slot starts, the next build (once its wall clock arrives)
325
+ * will target `parentSlot + 1`. The sequencer never advances its own target past wall clock — it
326
+ * just declines to build — so this is a prediction of inclusion globals, not literal sequencer
327
+ * behavior. The parent slot comes from the proposed checkpoint header so the slot and the
328
+ * overrides plan cannot derive from different snapshots.
329
+ */
330
+ private computeTargetSlot(proposedCheckpointData: ProposedCheckpointData | undefined): SlotNumber {
331
+ const slotFromNextL1Timestamp =
332
+ this.epochCache.getEpochAndSlotInNextL1Slot().slot + PROPOSER_PIPELINING_SLOT_OFFSET;
333
+ const slotAfterProposedCheckpoint = proposedCheckpointData
334
+ ? proposedCheckpointData.header.slotNumber + 1
335
+ : undefined;
336
+ return SlotNumber(Math.max(...compactArray([slotFromNextL1Timestamp, slotAfterProposedCheckpoint])));
337
+ }
338
+
339
+ /**
340
+ * Builds the chain-state overrides plan the simulator passes to `buildCheckpointGlobalVariables`,
341
+ * mirroring the sequencer (which always pins tips to neutralize prunes). When pipelining, the plan
342
+ * carries the proposed parent's archive, temp-checkpoint-log cell, and locally-derived fee header.
343
+ *
344
+ * Both the pipelining and invalid-pending-chain paths need a rollup contract for the L1 fee reads.
345
+ * Environments that omit it (e.g. TXE, which never has a proposed checkpoint and whose pending chain
346
+ * is always valid) fall back to pinning both pending and proven tips to the checkpointed tip, which
347
+ * neutralizes prunes in fee computation at the cost of non-pipelined fees.
348
+ */
349
+ private async buildSimulationOverridesPlan(
350
+ proposedCheckpointData: ProposedCheckpointData | undefined,
351
+ checkpointedCheckpointNumber: CheckpointNumber,
352
+ ): Promise<SimulationOverridesPlan | undefined> {
353
+ const rollup = this.rollupContract;
354
+ if (rollup) {
355
+ if (proposedCheckpointData) {
356
+ return buildCheckpointSimulationOverridesPlan({
357
+ checkpointNumber: CheckpointNumber(proposedCheckpointData.checkpointNumber + 1),
358
+ proposedCheckpointData,
359
+ checkpointedCheckpointNumber,
360
+ rollup,
361
+ signatureContext: this.signatureContext,
362
+ log: this.log,
363
+ });
364
+ }
365
+
366
+ const validationStatus = await this.blockSource.getPendingChainValidationStatus();
367
+ if (!validationStatus.valid) {
368
+ return buildCheckpointSimulationOverridesPlan({
369
+ checkpointNumber: CheckpointNumber(checkpointedCheckpointNumber + 1),
370
+ invalidateToPendingCheckpointNumber: CheckpointNumber(validationStatus.checkpoint.checkpointNumber - 1),
371
+ checkpointedCheckpointNumber,
372
+ rollup,
373
+ signatureContext: this.signatureContext,
374
+ log: this.log,
375
+ });
376
+ }
377
+ }
378
+
379
+ return new SimulationOverridesBuilder()
380
+ .withChainTips({ pending: checkpointedCheckpointNumber, proven: checkpointedCheckpointNumber })
381
+ .build();
382
+ }
383
+ }