@aztec-labs/aztec-node 6.0.0-nightly.20260829

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 (83) hide show
  1. package/README.md +15 -0
  2. package/dest/aztec-node/block_response_helpers.d.ts +25 -0
  3. package/dest/aztec-node/block_response_helpers.d.ts.map +1 -0
  4. package/dest/aztec-node/block_response_helpers.js +112 -0
  5. package/dest/aztec-node/config.d.ts +52 -0
  6. package/dest/aztec-node/config.d.ts.map +1 -0
  7. package/dest/aztec-node/config.js +129 -0
  8. package/dest/aztec-node/node_metrics.d.ts +12 -0
  9. package/dest/aztec-node/node_metrics.d.ts.map +1 -0
  10. package/dest/aztec-node/node_metrics.js +36 -0
  11. package/dest/aztec-node/node_public_calls_simulator.d.ts +108 -0
  12. package/dest/aztec-node/node_public_calls_simulator.d.ts.map +1 -0
  13. package/dest/aztec-node/node_public_calls_simulator.js +380 -0
  14. package/dest/aztec-node/public_data_overrides.d.ts +13 -0
  15. package/dest/aztec-node/public_data_overrides.d.ts.map +1 -0
  16. package/dest/aztec-node/public_data_overrides.js +21 -0
  17. package/dest/aztec-node/register_node_rpc_handlers.d.ts +11 -0
  18. package/dest/aztec-node/register_node_rpc_handlers.d.ts.map +1 -0
  19. package/dest/aztec-node/register_node_rpc_handlers.js +48 -0
  20. package/dest/aztec-node/server.d.ts +281 -0
  21. package/dest/aztec-node/server.d.ts.map +1 -0
  22. package/dest/aztec-node/server.js +1235 -0
  23. package/dest/bin/index.d.ts +3 -0
  24. package/dest/bin/index.d.ts.map +1 -0
  25. package/dest/bin/index.js +57 -0
  26. package/dest/factory.d.ts +33 -0
  27. package/dest/factory.d.ts.map +1 -0
  28. package/dest/factory.js +539 -0
  29. package/dest/index.d.ts +5 -0
  30. package/dest/index.d.ts.map +1 -0
  31. package/dest/index.js +4 -0
  32. package/dest/modules/block_parameter.d.ts +25 -0
  33. package/dest/modules/block_parameter.d.ts.map +1 -0
  34. package/dest/modules/block_parameter.js +100 -0
  35. package/dest/modules/node_block_provider.d.ts +19 -0
  36. package/dest/modules/node_block_provider.d.ts.map +1 -0
  37. package/dest/modules/node_block_provider.js +112 -0
  38. package/dest/modules/node_tx_receipt.d.ts +24 -0
  39. package/dest/modules/node_tx_receipt.d.ts.map +1 -0
  40. package/dest/modules/node_tx_receipt.js +70 -0
  41. package/dest/modules/node_world_state_queries.d.ts +65 -0
  42. package/dest/modules/node_world_state_queries.d.ts.map +1 -0
  43. package/dest/modules/node_world_state_queries.js +270 -0
  44. package/dest/sentinel/config.d.ts +9 -0
  45. package/dest/sentinel/config.d.ts.map +1 -0
  46. package/dest/sentinel/config.js +39 -0
  47. package/dest/sentinel/factory.d.ts +11 -0
  48. package/dest/sentinel/factory.d.ts.map +1 -0
  49. package/dest/sentinel/factory.js +24 -0
  50. package/dest/sentinel/index.d.ts +3 -0
  51. package/dest/sentinel/index.d.ts.map +1 -0
  52. package/dest/sentinel/index.js +1 -0
  53. package/dest/sentinel/sentinel.d.ts +217 -0
  54. package/dest/sentinel/sentinel.d.ts.map +1 -0
  55. package/dest/sentinel/sentinel.js +551 -0
  56. package/dest/sentinel/store.d.ts +35 -0
  57. package/dest/sentinel/store.d.ts.map +1 -0
  58. package/dest/sentinel/store.js +182 -0
  59. package/dest/test/index.d.ts +31 -0
  60. package/dest/test/index.d.ts.map +1 -0
  61. package/dest/test/index.js +1 -0
  62. package/package.json +118 -0
  63. package/src/aztec-node/block_response_helpers.ts +161 -0
  64. package/src/aztec-node/config.ts +216 -0
  65. package/src/aztec-node/node_metrics.ts +49 -0
  66. package/src/aztec-node/node_public_calls_simulator.ts +437 -0
  67. package/src/aztec-node/public_data_overrides.ts +35 -0
  68. package/src/aztec-node/register_node_rpc_handlers.ts +45 -0
  69. package/src/aztec-node/server.ts +1155 -0
  70. package/src/bin/index.ts +77 -0
  71. package/src/factory.ts +704 -0
  72. package/src/index.ts +4 -0
  73. package/src/modules/block_parameter.ts +93 -0
  74. package/src/modules/node_block_provider.ts +149 -0
  75. package/src/modules/node_tx_receipt.ts +115 -0
  76. package/src/modules/node_world_state_queries.ts +373 -0
  77. package/src/sentinel/README.md +103 -0
  78. package/src/sentinel/config.ts +49 -0
  79. package/src/sentinel/factory.ts +46 -0
  80. package/src/sentinel/index.ts +8 -0
  81. package/src/sentinel/sentinel.ts +694 -0
  82. package/src/sentinel/store.ts +193 -0
  83. package/src/test/index.ts +32 -0
@@ -0,0 +1,437 @@
1
+ import { MAX_L1_TO_L2_MSGS_PER_BLOCK, MAX_L1_TO_L2_MSGS_PER_CHECKPOINT } from '@aztec-labs/constants';
2
+ import { PROPOSER_PIPELINING_SLOT_OFFSET } from '@aztec-labs/epoch-cache';
3
+ import type { EpochCacheInterface } from '@aztec-labs/epoch-cache';
4
+ import {
5
+ type RollupContract,
6
+ SimulationOverridesBuilder,
7
+ type SimulationOverridesPlan,
8
+ } from '@aztec-labs/ethereum/contracts';
9
+ import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec-labs/foundation/branded-types';
10
+ import { compactArray } from '@aztec-labs/foundation/collection';
11
+ import { EthAddress } from '@aztec-labs/foundation/eth-address';
12
+ import { BadRequestError } from '@aztec-labs/foundation/json-rpc';
13
+ import { type Logger, createLogger } from '@aztec-labs/foundation/log';
14
+ import { DateProvider } from '@aztec-labs/foundation/timer';
15
+ import { type InboxBucketSource, selectInboxBucketForBlock } from '@aztec-labs/sequencer-client';
16
+ import { type AvmSimulator, PublicContractsDB, PublicProcessorFactory } from '@aztec-labs/simulator/server';
17
+ import { CollectionLimitsConfig, PublicSimulatorConfig } from '@aztec-labs/stdlib/avm';
18
+ import { AztecAddress } from '@aztec-labs/stdlib/aztec-address';
19
+ import type { L2BlockSource, L2Tips } from '@aztec-labs/stdlib/block';
20
+ import { type ProposedCheckpointData, buildCheckpointSimulationOverridesPlan } from '@aztec-labs/stdlib/checkpoint';
21
+ import type { ContractDataSource } from '@aztec-labs/stdlib/contract';
22
+ import type { MerkleTreeWriteOperations, WorldStateSynchronizer } from '@aztec-labs/stdlib/interfaces/server';
23
+ import {
24
+ type L1ToL2MessageSource,
25
+ appendL1ToL2MessagesToTree,
26
+ getInboxCutoffTimestamp,
27
+ } from '@aztec-labs/stdlib/messaging';
28
+ import type { CoordinationSignatureContext } from '@aztec-labs/stdlib/p2p';
29
+ import { MerkleTreeId } from '@aztec-labs/stdlib/trees';
30
+ import {
31
+ type GlobalVariableBuilder,
32
+ GlobalVariables,
33
+ PublicSimulationOutput,
34
+ type SimulationOverrides,
35
+ type Tx,
36
+ } from '@aztec-labs/stdlib/tx';
37
+ import { type TelemetryClient, getTelemetryClient } from '@aztec-labs/telemetry-client';
38
+
39
+ import { applyPublicDataOverrides } from './public_data_overrides.js';
40
+
41
+ /** Inbox queries the simulator needs to predict the message bundle the next block would consume. */
42
+ type SimulatorInboxSource = InboxBucketSource & Pick<L1ToL2MessageSource, 'getInboxBucketByTotalMsgCount'>;
43
+
44
+ /** Config fields the simulator needs — a narrow subset of `AztecNodeConfig`. */
45
+ export interface NodePublicCallsSimulatorConfig {
46
+ /** Maximum total gas limit accepted for an incoming simulation. */
47
+ rpcSimulatePublicMaxGasLimit: number;
48
+ /** Maximum number of debug-log memory reads collected during simulation. */
49
+ rpcSimulatePublicMaxDebugLogMemoryReads: number;
50
+ }
51
+
52
+ /** Dependencies required to build a {@link NodePublicCallsSimulator}. */
53
+ export interface NodePublicCallsSimulatorDeps {
54
+ blockSource: L2BlockSource;
55
+ worldStateSynchronizer: WorldStateSynchronizer;
56
+ /** Inbox bucket queries, used to predict the L1-to-L2 messages the next block will consume. */
57
+ l1ToL2MessageSource: SimulatorInboxSource;
58
+ contractDataSource: ContractDataSource;
59
+ globalVariableBuilder: GlobalVariableBuilder;
60
+ /**
61
+ * Rollup contract used to build the fee-relevant L1 state overrides when opening a new checkpoint.
62
+ * Only needed when a proposed parent checkpoint exists (pipelining) or the pending chain is invalid;
63
+ * may be omitted in environments that never reach those states (e.g. TXE). When omitted, those paths
64
+ * degrade to a pinned-tips plan (non-pipelined fees) instead.
65
+ */
66
+ rollupContract?: RollupContract;
67
+ epochCache: EpochCacheInterface;
68
+ signatureContext: CoordinationSignatureContext;
69
+ config: NodePublicCallsSimulatorConfig;
70
+ /**
71
+ * AVM execution backend the public processor drives to run public calls. Optional because unit/TXE nodes
72
+ * that never call {@link simulate} are constructed without one; asserted at the simulation call site.
73
+ */
74
+ avmSimulator?: AvmSimulator;
75
+ telemetry?: TelemetryClient;
76
+ log?: Logger;
77
+ }
78
+
79
+ /**
80
+ * Simulates the public part of a transaction against a fresh world-state fork.
81
+ *
82
+ * Extracted from `AztecNodeService` so the slot/globals selection can be unit-tested without
83
+ * standing up the whole node, and to keep `server.ts` smaller.
84
+ *
85
+ * The simulator picks globals in one of two ways, mirroring how the sequencer builds the next block:
86
+ * - **When the next block continues an in-progress checkpoint** (the latest proposed block is ahead of
87
+ * the proposed-checkpoint frontier): every block in a checkpoint shares the same
88
+ * `CheckpointGlobalVariables`, so we copy the latest proposed block's globals verbatim and only
89
+ * bump the block number. No L1 calls.
90
+ * - **When the next block opens a new checkpoint** (the latest proposed block coincides with the
91
+ * proposed-checkpoint frontier): we compute fresh globals for the slot the next block will land in,
92
+ * applying the same `SimulationOverridesPlan` the sequencer applies so the simulated mana min fee
93
+ * matches what the sequencer will write into the block header.
94
+ *
95
+ * Either way it also predicts the L1-to-L2 message bundle the next block would consume and appends it
96
+ * to the fork, so a transaction consuming a message that is in the Inbox but not yet in a block
97
+ * simulates against the state it will actually run in.
98
+ */
99
+ export class NodePublicCallsSimulator {
100
+ private readonly blockSource: L2BlockSource;
101
+ private readonly worldStateSynchronizer: WorldStateSynchronizer;
102
+ private readonly l1ToL2MessageSource: SimulatorInboxSource;
103
+ private readonly contractDataSource: ContractDataSource;
104
+ private readonly globalVariableBuilder: GlobalVariableBuilder;
105
+ private readonly rollupContract: RollupContract | undefined;
106
+ private readonly epochCache: EpochCacheInterface;
107
+ private readonly signatureContext: CoordinationSignatureContext;
108
+ private readonly config: NodePublicCallsSimulatorConfig;
109
+ private readonly avmSimulator?: AvmSimulator;
110
+ private readonly telemetry: TelemetryClient;
111
+ private readonly log: Logger;
112
+ private readonly dateProvider = new DateProvider();
113
+
114
+ constructor(deps: NodePublicCallsSimulatorDeps) {
115
+ this.blockSource = deps.blockSource;
116
+ this.worldStateSynchronizer = deps.worldStateSynchronizer;
117
+ this.l1ToL2MessageSource = deps.l1ToL2MessageSource;
118
+ this.contractDataSource = deps.contractDataSource;
119
+ this.globalVariableBuilder = deps.globalVariableBuilder;
120
+ this.rollupContract = deps.rollupContract;
121
+ this.epochCache = deps.epochCache;
122
+ this.signatureContext = deps.signatureContext;
123
+ this.config = deps.config;
124
+ this.avmSimulator = deps.avmSimulator;
125
+ this.telemetry = deps.telemetry ?? getTelemetryClient();
126
+ this.log = deps.log ?? createLogger('node:public-calls-simulator');
127
+ }
128
+
129
+ /**
130
+ * Simulates the public part of a transaction with the current state.
131
+ * @param tx - The transaction to simulate.
132
+ * @param skipFeeEnforcement - If true, fee enforcement is skipped.
133
+ * @param overrides - Optional pre-simulation overrides applied to the ephemeral fork and contract DB.
134
+ */
135
+ public async simulate(
136
+ tx: Tx,
137
+ skipFeeEnforcement = false,
138
+ overrides?: SimulationOverrides,
139
+ ): Promise<PublicSimulationOutput> {
140
+ // Check total gas limit for simulation
141
+ const gasSettings = tx.data.constants.txContext.gasSettings;
142
+ const txGasLimit = gasSettings.gasLimits.l2Gas;
143
+ const teardownGasLimit = gasSettings.teardownGasLimits.l2Gas;
144
+ if (txGasLimit + teardownGasLimit > this.config.rpcSimulatePublicMaxGasLimit) {
145
+ throw new BadRequestError(
146
+ `Transaction total gas limit ${
147
+ txGasLimit + teardownGasLimit
148
+ } (${txGasLimit} + ${teardownGasLimit}) exceeds maximum gas limit ${
149
+ this.config.rpcSimulatePublicMaxGasLimit
150
+ } for simulation`,
151
+ );
152
+ }
153
+
154
+ const txHash = tx.getTxHash();
155
+ const [l2Tips, proposedCheckpointData] = await Promise.all([
156
+ this.blockSource.getL2Tips(),
157
+ this.blockSource.getProposedCheckpointData(),
158
+ ]);
159
+ const latestBlockNumber = l2Tips.proposed.number;
160
+ const blockNumber = BlockNumber.add(latestBlockNumber, 1);
161
+
162
+ // Terminating block of the proposed-checkpoint frontier. `getProposedCheckpointData()` returns the
163
+ // leading proposed (not-yet-L1-confirmed) checkpoint, whose last block is `startBlock + blockCount
164
+ // - 1`; with no proposed checkpoint the frontier coincides with the checkpointed tip.
165
+ const proposedCheckpointLastBlock = proposedCheckpointData
166
+ ? BlockNumber.add(proposedCheckpointData.startBlock, proposedCheckpointData.blockCount - 1)
167
+ : l2Tips.checkpointed.block.number;
168
+
169
+ // The next block continues the in-progress checkpoint when the latest proposed block is ahead of
170
+ // the proposed-checkpoint terminating block; it opens a new checkpoint when they coincide.
171
+ const atCheckpointBoundary = proposedCheckpointLastBlock === l2Tips.proposed.number;
172
+
173
+ const { globalVariables: newGlobalVariables } = atCheckpointBoundary
174
+ ? await this.buildGlobalVariablesForNewCheckpoint(l2Tips, proposedCheckpointData, blockNumber)
175
+ : { globalVariables: await this.copyGlobalVariablesFromLatestProposedBlock(latestBlockNumber, blockNumber) };
176
+
177
+ if (!this.avmSimulator) {
178
+ throw new Error('NodePublicCallsSimulator.simulate requires an AVM simulator, but none was configured');
179
+ }
180
+ const publicProcessorFactory = new PublicProcessorFactory(
181
+ this.contractDataSource,
182
+ this.avmSimulator,
183
+ this.dateProvider,
184
+ this.telemetry,
185
+ this.log.getBindings(),
186
+ );
187
+
188
+ this.log.verbose(`Simulating public calls for tx ${txHash}`, {
189
+ globalVariables: newGlobalVariables.toInspect(),
190
+ txHash,
191
+ blockNumber,
192
+ atCheckpointBoundary,
193
+ });
194
+
195
+ // Ensure world-state has caught up with the latest block we loaded from the archiver
196
+ await this.worldStateSynchronizer.syncImmediate(latestBlockNumber);
197
+
198
+ // Request a new fork of the world state at the latest block number, then apply the next block's predicted
199
+ // L1-to-L2 message bundle and any caller overrides to it before simulation.
200
+ await using merkleTreeFork = await this.worldStateSynchronizer.fork(latestBlockNumber);
201
+
202
+ await this.appendPredictedL1ToL2Messages(merkleTreeFork, {
203
+ slotNumber: newGlobalVariables.slotNumber,
204
+ checkpointStartBlock: atCheckpointBoundary ? undefined : proposedCheckpointLastBlock,
205
+ });
206
+
207
+ await applyPublicDataOverrides(merkleTreeFork, overrides?.publicStorage);
208
+
209
+ const config = PublicSimulatorConfig.from({
210
+ skipFeeEnforcement,
211
+ collectDebugLogs: true,
212
+ collectHints: false,
213
+ collectCallMetadata: true,
214
+ collectStatistics: false,
215
+ collectionLimits: CollectionLimitsConfig.from({
216
+ maxDebugLogMemoryReads: this.config.rpcSimulatePublicMaxDebugLogMemoryReads,
217
+ }),
218
+ });
219
+
220
+ const contractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings());
221
+ if (overrides?.contracts) {
222
+ contractsDB.addContracts(Object.values(overrides.contracts).map(({ instance }) => instance));
223
+ }
224
+ const processor = publicProcessorFactory.create(merkleTreeFork, newGlobalVariables, config, contractsDB);
225
+
226
+ // REFACTOR: Consider merging ProcessReturnValues into ProcessedTx
227
+ const [processedTxs, failedTxs, _usedTxs, returns, debugLogs] = await processor.process([tx]);
228
+ // REFACTOR: Consider returning the error rather than throwing
229
+ if (failedTxs.length) {
230
+ this.log.warn(`Simulated tx ${txHash} fails: ${failedTxs[0].error}`, { txHash });
231
+ throw failedTxs[0].error;
232
+ }
233
+
234
+ const [processedTx] = processedTxs;
235
+ return new PublicSimulationOutput(
236
+ processedTx.revertReason,
237
+ processedTx.globalVariables,
238
+ processedTx.txEffect,
239
+ returns,
240
+ processedTx.gasUsed,
241
+ debugLogs,
242
+ );
243
+ }
244
+
245
+ /**
246
+ * Appends the L1-to-L2 message bundle the next block would consume to the simulation fork, so a transaction
247
+ * consuming a message that has reached the Inbox but no block yet simulates against the state it will run in.
248
+ * Runs the same bucket selection the sequencer runs (lag eligibility plus the per-block and per-checkpoint caps),
249
+ * treating the next block as non-final: the censorship cutoff only widens consumption on a checkpoint's last
250
+ * block, and the node cannot know whether the next block is it.
251
+ *
252
+ * Best-effort. Any failure — Inbox buckets not synced yet, a torn archiver snapshot — leaves the fork at the tip
253
+ * state, which is what the transaction sees if the next block consumes nothing.
254
+ */
255
+ private async appendPredictedL1ToL2Messages(
256
+ fork: MerkleTreeWriteOperations,
257
+ opts: {
258
+ /** Slot the next block lands in; anchors the censorship cutoff. */
259
+ slotNumber: SlotNumber;
260
+ /** Last block of the checkpoint the next block extends; undefined when the next block opens a checkpoint. */
261
+ checkpointStartBlock: BlockNumber | undefined;
262
+ },
263
+ ): Promise<void> {
264
+ try {
265
+ const parentTotalMsgCount = (await fork.getTreeInfo(MerkleTreeId.L1_TO_L2_MESSAGE_TREE)).size;
266
+ const parentBucket = await this.l1ToL2MessageSource.getInboxBucketByTotalMsgCount(parentTotalMsgCount);
267
+ if (parentBucket === undefined) {
268
+ this.log.debug(`Inbox bucket at message total ${parentTotalMsgCount} not synced; simulating against the tip`, {
269
+ parentTotalMsgCount,
270
+ });
271
+ return;
272
+ }
273
+
274
+ // Origin of the per-checkpoint cap: the total consumed as of the checkpoint's parent. A block extending an
275
+ // in-progress checkpoint reads it off that checkpoint's parent block; a block opening one starts from the tip.
276
+ const checkpointStartTotalMsgCount =
277
+ opts.checkpointStartBlock === undefined
278
+ ? parentTotalMsgCount
279
+ : await this.getConsumedMessageTotal(opts.checkpointStartBlock);
280
+ if (checkpointStartTotalMsgCount === undefined) {
281
+ this.log.debug(`Block ${opts.checkpointStartBlock} has no header on this node; simulating against the tip`);
282
+ return;
283
+ }
284
+
285
+ const l1Constants = this.epochCache.getL1Constants();
286
+ const selection = await selectInboxBucketForBlock({
287
+ messageSource: this.l1ToL2MessageSource,
288
+ now: BigInt(Math.floor(this.dateProvider.now() / 1000)),
289
+ minBucketAgeSeconds: BigInt(l1Constants.ethereumSlotDuration),
290
+ parent: { seq: parentBucket.seq, totalMsgCount: parentBucket.totalMsgCount },
291
+ checkpointStartTotalMsgCount,
292
+ perBlockCap: MAX_L1_TO_L2_MSGS_PER_BLOCK,
293
+ perCheckpointCap: MAX_L1_TO_L2_MSGS_PER_CHECKPOINT,
294
+ isLastBlock: false,
295
+ cutoffTimestamp: getInboxCutoffTimestamp(opts.slotNumber, l1Constants),
296
+ });
297
+ if (!selection.consume || selection.bundle.length === 0) {
298
+ return;
299
+ }
300
+
301
+ await appendL1ToL2MessagesToTree(fork, selection.bundle);
302
+ this.log.debug(`Appended ${selection.bundle.length} predicted L1-to-L2 messages to the simulation fork`, {
303
+ bucketSeq: selection.bucket.seq,
304
+ messageCount: selection.bundle.length,
305
+ });
306
+ } catch (err) {
307
+ this.log.verbose(`Could not predict the next block's L1-to-L2 messages, simulating against the tip: ${err}`);
308
+ }
309
+ }
310
+
311
+ /** Cumulative Inbox message total consumed as of `blockNumber`, i.e. its L1-to-L2 message tree leaf count. */
312
+ private async getConsumedMessageTotal(blockNumber: BlockNumber): Promise<bigint | undefined> {
313
+ if (blockNumber === BlockNumber.ZERO) {
314
+ return 0n;
315
+ }
316
+ const block = await this.blockSource.getBlockData({ number: blockNumber });
317
+ return block === undefined ? undefined : BigInt(block.header.state.l1ToL2MessageTree.nextAvailableLeafIndex);
318
+ }
319
+
320
+ /**
321
+ * Continues an in-progress checkpoint: the next block extends the checkpoint the latest proposed
322
+ * block belongs to. Every block in a checkpoint shares the same `CheckpointGlobalVariables`, so the
323
+ * next block's globals are the latest proposed block's globals with only the block number bumped —
324
+ * including the proposer's real coinbase/feeRecipient. No L1 reads happen here.
325
+ *
326
+ * A missing header means the archiver reported a proposed tip via `getL2Tips` but no longer has its
327
+ * data (a torn snapshot). We throw a transient/retryable error rather than treating the next block as
328
+ * opening a new checkpoint, whose globals would be built for the wrong slot.
329
+ */
330
+ private async copyGlobalVariablesFromLatestProposedBlock(
331
+ latestBlockNumber: BlockNumber,
332
+ blockNumber: BlockNumber,
333
+ ): Promise<GlobalVariables> {
334
+ const latestBlockData = await this.blockSource.getBlockData({ number: latestBlockNumber });
335
+ if (!latestBlockData) {
336
+ throw new Error(
337
+ `Cannot simulate public calls: latest proposed block ${latestBlockNumber} has no header on this node ` +
338
+ `(torn archiver snapshot); retry`,
339
+ );
340
+ }
341
+ return GlobalVariables.from({ ...latestBlockData.header.globalVariables, blockNumber });
342
+ }
343
+
344
+ /**
345
+ * Opens a new checkpoint: the next block is the first of a fresh checkpoint. Picks the slot the next
346
+ * block will land in, mirroring the sequencer, and builds the same `SimulationOverridesPlan` the
347
+ * sequencer applies so the simulated mana min fee matches what the sequencer will write into the
348
+ * block header. Coinbase and fee recipient stay zero (we cannot know the future proposer's payout
349
+ * addresses), unlike continuing an in-progress checkpoint which inherits the real ones from the
350
+ * proposed header.
351
+ */
352
+ private async buildGlobalVariablesForNewCheckpoint(
353
+ l2Tips: L2Tips,
354
+ proposedCheckpointData: ProposedCheckpointData | undefined,
355
+ blockNumber: BlockNumber,
356
+ ): Promise<{ globalVariables: GlobalVariables }> {
357
+ const checkpointedCheckpointNumber = l2Tips.checkpointed.checkpoint.number;
358
+
359
+ const targetSlot = this.computeTargetSlot(proposedCheckpointData);
360
+ const plan = await this.buildSimulationOverridesPlan(proposedCheckpointData, checkpointedCheckpointNumber);
361
+
362
+ const checkpointGlobalVariables = await this.globalVariableBuilder.buildCheckpointGlobalVariables(
363
+ EthAddress.ZERO,
364
+ AztecAddress.ZERO,
365
+ targetSlot,
366
+ plan,
367
+ );
368
+
369
+ return {
370
+ globalVariables: GlobalVariables.from({ blockNumber, ...checkpointGlobalVariables }),
371
+ };
372
+ }
373
+
374
+ /**
375
+ * Slot the next block will land in. The first term is the sequencer's exact formula
376
+ * (`getEpochAndSlotInNextL1Slot().slot + PROPOSER_PIPELINING_SLOT_OFFSET`). The `max` with
377
+ * `proposedCheckpointSlot + 1` is an RPC-side approximation of the next build: when a proposed
378
+ * checkpoint is gossiped before its L1 slot starts, the next build (once its wall clock arrives)
379
+ * will target `parentSlot + 1`. The sequencer never advances its own target past wall clock — it
380
+ * just declines to build — so this is a prediction of inclusion globals, not literal sequencer
381
+ * behavior. The parent slot comes from the proposed checkpoint header so the slot and the
382
+ * overrides plan cannot derive from different snapshots.
383
+ */
384
+ private computeTargetSlot(proposedCheckpointData: ProposedCheckpointData | undefined): SlotNumber {
385
+ const slotFromNextL1Timestamp =
386
+ this.epochCache.getEpochAndSlotInNextL1Slot().slot + PROPOSER_PIPELINING_SLOT_OFFSET;
387
+ const slotAfterProposedCheckpoint = proposedCheckpointData
388
+ ? proposedCheckpointData.header.slotNumber + 1
389
+ : undefined;
390
+ return SlotNumber(Math.max(...compactArray([slotFromNextL1Timestamp, slotAfterProposedCheckpoint])));
391
+ }
392
+
393
+ /**
394
+ * Builds the chain-state overrides plan the simulator passes to `buildCheckpointGlobalVariables`,
395
+ * mirroring the sequencer (which always pins tips to neutralize prunes). When pipelining, the plan
396
+ * carries the proposed parent's archive, temp-checkpoint-log cell, and locally-derived fee header.
397
+ *
398
+ * Both the pipelining and invalid-pending-chain paths need a rollup contract for the L1 fee reads.
399
+ * Environments that omit it (e.g. TXE, which never has a proposed checkpoint and whose pending chain
400
+ * is always valid) fall back to pinning both pending and proven tips to the checkpointed tip, which
401
+ * neutralizes prunes in fee computation at the cost of non-pipelined fees.
402
+ */
403
+ private async buildSimulationOverridesPlan(
404
+ proposedCheckpointData: ProposedCheckpointData | undefined,
405
+ checkpointedCheckpointNumber: CheckpointNumber,
406
+ ): Promise<SimulationOverridesPlan | undefined> {
407
+ const rollup = this.rollupContract;
408
+ if (rollup) {
409
+ if (proposedCheckpointData) {
410
+ return buildCheckpointSimulationOverridesPlan({
411
+ checkpointNumber: CheckpointNumber(proposedCheckpointData.checkpointNumber + 1),
412
+ proposedCheckpointData,
413
+ checkpointedCheckpointNumber,
414
+ rollup,
415
+ signatureContext: this.signatureContext,
416
+ log: this.log,
417
+ });
418
+ }
419
+
420
+ const validationStatus = await this.blockSource.getPendingChainValidationStatus();
421
+ if (!validationStatus.valid) {
422
+ return buildCheckpointSimulationOverridesPlan({
423
+ checkpointNumber: CheckpointNumber(checkpointedCheckpointNumber + 1),
424
+ invalidateToPendingCheckpointNumber: CheckpointNumber(validationStatus.checkpoint.checkpointNumber - 1),
425
+ checkpointedCheckpointNumber,
426
+ rollup,
427
+ signatureContext: this.signatureContext,
428
+ log: this.log,
429
+ });
430
+ }
431
+ }
432
+
433
+ return new SimulationOverridesBuilder()
434
+ .withChainTips({ pending: checkpointedCheckpointNumber, proven: checkpointedCheckpointNumber })
435
+ .build();
436
+ }
437
+ }
@@ -0,0 +1,35 @@
1
+ import { PublicDataWrite } from '@aztec-labs/stdlib/avm';
2
+ import { computePublicDataTreeLeafSlot } from '@aztec-labs/stdlib/hash';
3
+ import type { PublicStorageOverride } from '@aztec-labs/stdlib/interfaces/client';
4
+ import type { MerkleTreeWriteOperations } from '@aztec-labs/stdlib/interfaces/server';
5
+ import { MerkleTreeId } from '@aztec-labs/stdlib/trees';
6
+
7
+ /**
8
+ * Injects public-state overrides into an (ephemeral) world-state fork before simulation.
9
+ *
10
+ * Each override is written via the same `sequentialInsert` path the public processor
11
+ * uses during real transaction execution, so low-leaf updates and root coherence are
12
+ * handled identically for both simulation and proof generation.
13
+ *
14
+ * Writes never reach committed world state — the fork is thrown away after simulation.
15
+ */
16
+ export async function applyPublicDataOverrides(
17
+ fork: MerkleTreeWriteOperations,
18
+ publicStorage: PublicStorageOverride[] | undefined,
19
+ ): Promise<void> {
20
+ if (!publicStorage?.length) {
21
+ return;
22
+ }
23
+
24
+ const writes = await Promise.all(
25
+ publicStorage.map(async o => {
26
+ const leafSlot = await computePublicDataTreeLeafSlot(o.contract, o.slot);
27
+ return new PublicDataWrite(leafSlot, o.value);
28
+ }),
29
+ );
30
+
31
+ await fork.sequentialInsert(
32
+ MerkleTreeId.PUBLIC_DATA_TREE,
33
+ writes.map(w => w.toBuffer()),
34
+ );
35
+ }
@@ -0,0 +1,45 @@
1
+ import type { NamespacedApiHandlers, StatusCheckFn } from '@aztec-labs/foundation/json-rpc/server';
2
+ import {
3
+ AztecNodeAdminApiSchema,
4
+ AztecNodeApiSchema,
5
+ AztecNodeDebugApiSchema,
6
+ } from '@aztec-labs/stdlib/interfaces/client';
7
+ import { type P2PApi, P2PApiSchema } from '@aztec-labs/stdlib/interfaces/server';
8
+
9
+ import type { AztecNodeService } from './server.js';
10
+
11
+ /**
12
+ * Health check for the p2p component: reports whether p2p is enabled and how many peers are connected, and fails
13
+ * when p2p is enabled but connected to fewer peers than the given minimum. A minimum of zero never fails.
14
+ */
15
+ function makeP2PHealthCheck(p2p: P2PApi, minPeers: number): StatusCheckFn {
16
+ return async () => {
17
+ const { enabled, connectedPeers } = await p2p.getP2PConnectivity();
18
+ return { healthy: !enabled || connectedPeers >= minPeers, details: { enabled, connectedPeers } };
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Registers the Aztec node RPC handlers (`aztec_*`, `aztecAdmin_*`, and optionally `aztecDebug_*`), along with the
24
+ * legacy pre-v5 namespaces (`node_*`, `nodeAdmin_*`, `nodeDebug_*`, `p2p_*`) for backwards compatibility.
25
+ */
26
+ // TODO: Legacy support for node, nodeAdmin, nodeDebug, p2p namespaces. New namespaces introduced in v5. Remove on future release. A-1169
27
+ export function registerAztecNodeRpcHandlers(
28
+ node: AztecNodeService,
29
+ services: NamespacedApiHandlers,
30
+ adminServices?: NamespacedApiHandlers,
31
+ options: { debug?: boolean; p2pHealthMinPeers?: number } = {},
32
+ ): void {
33
+ const p2p = node.getP2P();
34
+ services.aztec = [node, AztecNodeApiSchema];
35
+ services.node = services.aztec;
36
+ services.p2p = [p2p, P2PApiSchema, makeP2PHealthCheck(p2p, options.p2pHealthMinPeers ?? 0)];
37
+ if (adminServices) {
38
+ adminServices.aztecAdmin = [node, AztecNodeAdminApiSchema];
39
+ adminServices.nodeAdmin = adminServices.aztecAdmin;
40
+ }
41
+ if (options.debug) {
42
+ services.aztecDebug = [node, AztecNodeDebugApiSchema];
43
+ services.nodeDebug = services.aztecDebug;
44
+ }
45
+ }