@aztec/world-state 0.0.1-commit.b655e406 → 0.0.1-commit.b8a057fa

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 (80) hide show
  1. package/dest/index.d.ts +2 -1
  2. package/dest/index.d.ts.map +1 -1
  3. package/dest/index.js +1 -0
  4. package/dest/instrumentation/instrumentation.d.ts +4 -3
  5. package/dest/instrumentation/instrumentation.d.ts.map +1 -1
  6. package/dest/instrumentation/instrumentation.js +21 -50
  7. package/dest/native/bench_metrics.d.ts +1 -1
  8. package/dest/native/bench_metrics.d.ts.map +1 -1
  9. package/dest/native/fork_checkpoint.d.ts +7 -1
  10. package/dest/native/fork_checkpoint.d.ts.map +1 -1
  11. package/dest/native/fork_checkpoint.js +15 -3
  12. package/dest/native/index.d.ts +2 -1
  13. package/dest/native/index.d.ts.map +1 -1
  14. package/dest/native/index.js +1 -0
  15. package/dest/native/ipc_world_state_instance.d.ts +89 -0
  16. package/dest/native/ipc_world_state_instance.d.ts.map +1 -0
  17. package/dest/native/ipc_world_state_instance.js +661 -0
  18. package/dest/native/merkle_trees_facade.d.ts +18 -8
  19. package/dest/native/merkle_trees_facade.d.ts.map +1 -1
  20. package/dest/native/merkle_trees_facade.js +90 -111
  21. package/dest/native/message.d.ts +22 -228
  22. package/dest/native/message.d.ts.map +1 -1
  23. package/dest/native/message.js +14 -55
  24. package/dest/native/native_world_state.d.ts +30 -15
  25. package/dest/native/native_world_state.d.ts.map +1 -1
  26. package/dest/native/native_world_state.js +133 -66
  27. package/dest/native/native_world_state_instance.d.ts +56 -33
  28. package/dest/native/native_world_state_instance.d.ts.map +1 -1
  29. package/dest/native/native_world_state_instance.js +1 -182
  30. package/dest/native/world_state_operation.d.ts +7 -0
  31. package/dest/native/world_state_operation.d.ts.map +1 -0
  32. package/dest/native/world_state_operation.js +5 -0
  33. package/dest/synchronizer/config.d.ts +3 -5
  34. package/dest/synchronizer/config.d.ts.map +1 -1
  35. package/dest/synchronizer/config.js +15 -18
  36. package/dest/synchronizer/errors.d.ts +8 -1
  37. package/dest/synchronizer/errors.d.ts.map +1 -1
  38. package/dest/synchronizer/errors.js +8 -1
  39. package/dest/synchronizer/factory.d.ts +6 -5
  40. package/dest/synchronizer/factory.d.ts.map +1 -1
  41. package/dest/synchronizer/factory.js +7 -6
  42. package/dest/synchronizer/index.d.ts +2 -1
  43. package/dest/synchronizer/index.d.ts.map +1 -1
  44. package/dest/synchronizer/index.js +1 -0
  45. package/dest/synchronizer/server_world_state_synchronizer.d.ts +20 -32
  46. package/dest/synchronizer/server_world_state_synchronizer.d.ts.map +1 -1
  47. package/dest/synchronizer/server_world_state_synchronizer.js +181 -85
  48. package/dest/test/index.d.ts +1 -1
  49. package/dest/test/utils.d.ts +12 -5
  50. package/dest/test/utils.d.ts.map +1 -1
  51. package/dest/test/utils.js +59 -50
  52. package/dest/testing.d.ts +5 -4
  53. package/dest/testing.d.ts.map +1 -1
  54. package/dest/testing.js +15 -8
  55. package/dest/world-state-db/index.d.ts +1 -1
  56. package/dest/world-state-db/merkle_tree_db.d.ts +9 -19
  57. package/dest/world-state-db/merkle_tree_db.d.ts.map +1 -1
  58. package/package.json +16 -13
  59. package/src/index.ts +1 -0
  60. package/src/instrumentation/instrumentation.ts +23 -59
  61. package/src/native/fork_checkpoint.ts +19 -3
  62. package/src/native/index.ts +1 -0
  63. package/src/native/ipc_world_state_instance.ts +834 -0
  64. package/src/native/merkle_trees_facade.ts +118 -104
  65. package/src/native/message.ts +33 -305
  66. package/src/native/native_world_state.ts +179 -108
  67. package/src/native/native_world_state_instance.ts +94 -251
  68. package/src/native/world_state_operation.ts +33 -0
  69. package/src/synchronizer/config.ts +16 -23
  70. package/src/synchronizer/errors.ts +8 -0
  71. package/src/synchronizer/factory.ts +18 -11
  72. package/src/synchronizer/index.ts +1 -0
  73. package/src/synchronizer/server_world_state_synchronizer.ts +219 -111
  74. package/src/test/utils.ts +96 -92
  75. package/src/testing.ts +13 -11
  76. package/src/world-state-db/merkle_tree_db.ts +12 -19
  77. package/dest/native/world_state_ops_queue.d.ts +0 -19
  78. package/dest/native/world_state_ops_queue.d.ts.map +0 -1
  79. package/dest/native/world_state_ops_queue.js +0 -146
  80. package/src/native/world_state_ops_queue.ts +0 -190
@@ -1,19 +1,17 @@
1
- import { L1_TO_L2_MSG_SUBTREE_HEIGHT } from '@aztec/constants';
2
- import { SHA256Trunc } from '@aztec/foundation/crypto';
3
- import type { Fr } from '@aztec/foundation/fields';
1
+ import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
2
+ import type { Fr } from '@aztec/foundation/curves/bn254';
4
3
  import { type Logger, createLogger } from '@aztec/foundation/log';
5
4
  import { promiseWithResolvers } from '@aztec/foundation/promise';
6
5
  import { elapsed } from '@aztec/foundation/timer';
7
- import { MerkleTreeCalculator } from '@aztec/foundation/trees';
8
- import type {
9
- L2Block,
10
- L2BlockId,
11
- L2BlockSource,
12
- L2BlockStream,
13
- L2BlockStreamEvent,
14
- L2BlockStreamEventHandler,
15
- L2BlockStreamLocalDataProvider,
16
- L2Tips,
6
+ import {
7
+ type BlockHash,
8
+ EventDrivenL2BlockStream,
9
+ type L2Block,
10
+ type L2BlockSource,
11
+ type L2BlockStreamEvent,
12
+ type L2BlockStreamEventHandler,
13
+ type L2BlockStreamLocalDataProvider,
14
+ type LocalChainTips,
17
15
  } from '@aztec/stdlib/block';
18
16
  import {
19
17
  WorldStateRunningState,
@@ -25,7 +23,7 @@ import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
25
23
  import type { SnapshotDataKeys } from '@aztec/stdlib/snapshots';
26
24
  import type { L2BlockHandledStats } from '@aztec/stdlib/stats';
27
25
  import { MerkleTreeId, type MerkleTreeReadOperations, type MerkleTreeWriteOperations } from '@aztec/stdlib/trees';
28
- import { TraceableL2BlockStream, getTelemetryClient } from '@aztec/telemetry-client';
26
+ import { getTelemetryClient } from '@aztec/telemetry-client';
29
27
 
30
28
  import { WorldStateInstrumentation } from '../instrumentation/instrumentation.js';
31
29
  import type { WorldStateStatusFull } from '../native/message.js';
@@ -45,17 +43,16 @@ export class ServerWorldStateSynchronizer
45
43
  {
46
44
  private readonly merkleTreeCommitted: MerkleTreeReadOperations;
47
45
 
48
- private latestBlockNumberAtStart = 0;
46
+ private latestBlockNumberAtStart = BlockNumber.ZERO;
49
47
  private historyToKeep: number | undefined;
50
48
  private currentState: WorldStateRunningState = WorldStateRunningState.IDLE;
51
- private latestBlockHashQuery: { blockNumber: number; hash: string | undefined } | undefined = undefined;
52
49
 
53
50
  private syncPromise = promiseWithResolvers<void>();
54
- protected blockStream: L2BlockStream | undefined;
51
+ protected blockStream: EventDrivenL2BlockStream | undefined;
55
52
 
56
53
  // WorldState doesn't track the proven block number, it only tracks the latest tips of the pending chain and the finalized chain
57
54
  // store the proven block number here, in the synchronizer, so that we don't end up spamming the logs with 'chain-proved' events
58
- private provenBlockNumber: bigint | undefined;
55
+ private provenBlockNumber: BlockNumber | undefined;
59
56
 
60
57
  constructor(
61
58
  private readonly merkleTreeDb: MerkleTreeAdminDatabase,
@@ -65,7 +62,7 @@ export class ServerWorldStateSynchronizer
65
62
  private readonly log: Logger = createLogger('world_state'),
66
63
  ) {
67
64
  this.merkleTreeCommitted = this.merkleTreeDb.getCommitted();
68
- this.historyToKeep = config.worldStateBlockHistory < 1 ? undefined : config.worldStateBlockHistory;
65
+ this.historyToKeep = config.worldStateCheckpointHistory < 1 ? undefined : config.worldStateCheckpointHistory;
69
66
  this.log.info(
70
67
  `Created world state synchroniser with block history of ${
71
68
  this.historyToKeep === undefined ? 'infinity' : this.historyToKeep
@@ -77,12 +74,52 @@ export class ServerWorldStateSynchronizer
77
74
  return this.merkleTreeDb.getCommitted();
78
75
  }
79
76
 
80
- public getSnapshot(blockNumber: number): MerkleTreeReadOperations {
77
+ public getSnapshot(blockNumber: BlockNumber): MerkleTreeReadOperations {
81
78
  return this.merkleTreeDb.getSnapshot(blockNumber);
82
79
  }
83
80
 
84
- public fork(blockNumber?: number): Promise<MerkleTreeWriteOperations> {
85
- return this.merkleTreeDb.fork(blockNumber);
81
+ public async getVerifiedSnapshot(blockNumber: BlockNumber, blockHash: BlockHash): Promise<MerkleTreeReadOperations> {
82
+ const snapshot = this.merkleTreeDb.getSnapshot(blockNumber);
83
+ // Block 0's snapshot is the pre-genesis archive view (size 0), so archive leaf 0 is not visible from it;
84
+ // verify against the initial header hash instead. For later blocks, read archive leaf `blockNumber` from the
85
+ // snapshot's own view so the exact handle we return is validated against the requested fork.
86
+ const actualHash =
87
+ blockNumber === BlockNumber.ZERO
88
+ ? (await this.merkleTreeCommitted.getInitialHeader().hash()).toString()
89
+ : (await snapshot.getLeafValue(MerkleTreeId.ARCHIVE, BigInt(blockNumber)))?.toString();
90
+
91
+ if (actualHash === undefined) {
92
+ // A missing archive leaf means either the block's history has been pruned away (permanent: the block predates
93
+ // the oldest historical block kept by world state) or a reorg flipped the fork between the sync and this read
94
+ // (transient). Only the latter is worth retrying, so it alone surfaces as WorldStateSynchronizerError.
95
+ const { oldestHistoricalBlock } = await this.merkleTreeDb.getStatusSummary();
96
+ if (blockNumber < oldestHistoricalBlock) {
97
+ throw new Error(
98
+ `Unable to find leaf for block ${blockNumber} in the archive tree: world state history has been pruned to block ${oldestHistoricalBlock}`,
99
+ );
100
+ }
101
+ throw new WorldStateSynchronizerError(`Unable to read block hash at block ${blockNumber} to verify snapshot`, {
102
+ cause: { reason: 'block_not_available', targetBlockNumber: blockNumber },
103
+ });
104
+ }
105
+ if (actualHash !== blockHash.toString()) {
106
+ throw new WorldStateSynchronizerError(
107
+ `Block hash mismatch at block ${blockNumber} (expected ${blockHash} but got ${actualHash})`,
108
+ {
109
+ cause: {
110
+ reason: 'block_hash_mismatch',
111
+ targetBlockNumber: blockNumber,
112
+ expectedHash: blockHash.toString(),
113
+ actualHash,
114
+ },
115
+ },
116
+ );
117
+ }
118
+ return snapshot;
119
+ }
120
+
121
+ public fork(blockNumber?: BlockNumber, opts?: { closeDelayMs?: number }): Promise<MerkleTreeWriteOperations> {
122
+ return this.merkleTreeDb.fork(blockNumber, opts);
86
123
  }
87
124
 
88
125
  public backupTo(dstPath: string, compact?: boolean): Promise<Record<Exclude<SnapshotDataKeys, 'archiver'>, string>> {
@@ -102,9 +139,7 @@ export class ServerWorldStateSynchronizer
102
139
  }
103
140
 
104
141
  // Get the current latest block number
105
- this.latestBlockNumberAtStart = await (this.config.worldStateProvenBlocksOnly
106
- ? this.l2BlockSource.getProvenBlockNumber()
107
- : this.l2BlockSource.getBlockNumber());
142
+ this.latestBlockNumberAtStart = BlockNumber(await this.l2BlockSource.getBlockNumber());
108
143
 
109
144
  const blockToDownloadFrom = (await this.getLatestBlockNumber()) + 1;
110
145
 
@@ -125,13 +160,12 @@ export class ServerWorldStateSynchronizer
125
160
  return this.syncPromise.promise;
126
161
  }
127
162
 
128
- protected createBlockStream(): L2BlockStream {
129
- const tracer = this.instrumentation.telemetry.getTracer('WorldStateL2BlockStream');
163
+ protected createBlockStream(): EventDrivenL2BlockStream {
130
164
  const logger = createLogger('world-state:block_stream');
131
- return new TraceableL2BlockStream(this.l2BlockSource, this, this, tracer, 'WorldStateL2BlockStream', logger, {
132
- proven: this.config.worldStateProvenBlocksOnly,
165
+ return new EventDrivenL2BlockStream(this.l2BlockSource, this, this, logger, {
133
166
  pollIntervalMS: this.config.worldStateBlockCheckIntervalMS,
134
167
  batchSize: this.config.worldStateBlockRequestBatchSize,
168
+ ignoreCheckpoints: true,
135
169
  });
136
170
  }
137
171
 
@@ -147,10 +181,10 @@ export class ServerWorldStateSynchronizer
147
181
  public async status(): Promise<WorldStateSynchronizerStatus> {
148
182
  const summary = await this.merkleTreeDb.getStatusSummary();
149
183
  const status: WorldStateSyncStatus = {
150
- latestBlockNumber: Number(summary.unfinalizedBlockNumber),
151
- latestBlockHash: (await this.getL2BlockHash(Number(summary.unfinalizedBlockNumber))) ?? '',
152
- finalizedBlockNumber: Number(summary.finalizedBlockNumber),
153
- oldestHistoricBlockNumber: Number(summary.oldestHistoricalBlock),
184
+ latestBlockNumber: summary.unfinalizedBlockNumber,
185
+ latestBlockHash: (await this.getL2BlockHash(summary.unfinalizedBlockNumber)) ?? '',
186
+ finalizedBlockNumber: summary.finalizedBlockNumber,
187
+ oldestHistoricBlockNumber: summary.oldestHistoricalBlock,
154
188
  treesAreSynched: summary.treesAreSynched,
155
189
  };
156
190
  return {
@@ -160,7 +194,7 @@ export class ServerWorldStateSynchronizer
160
194
  }
161
195
 
162
196
  public async getLatestBlockNumber() {
163
- return (await this.getL2Tips()).latest.number;
197
+ return (await this.getL2Tips()).proposed.number;
164
198
  }
165
199
 
166
200
  public async stopSync() {
@@ -181,10 +215,10 @@ export class ServerWorldStateSynchronizer
181
215
  /**
182
216
  * Forces an immediate sync.
183
217
  * @param targetBlockNumber - The target block number that we must sync to. Will download unproven blocks if needed to reach it.
184
- * @param skipThrowIfTargetNotReached - Whether to skip throwing if the target block number is not reached.
218
+ * @param blockHash - If provided, verifies the block at targetBlockNumber matches this hash. On mismatch, triggers a resync (reorg detection).
185
219
  * @returns A promise that resolves with the block number the world state was synced to
186
220
  */
187
- public async syncImmediate(targetBlockNumber?: number, skipThrowIfTargetNotReached?: boolean): Promise<number> {
221
+ public async syncImmediate(targetBlockNumber?: BlockNumber, blockHash?: BlockHash): Promise<BlockNumber> {
188
222
  if (this.currentState !== WorldStateRunningState.RUNNING) {
189
223
  throw new Error(`World State is not running. Unable to perform sync.`);
190
224
  }
@@ -196,13 +230,25 @@ export class ServerWorldStateSynchronizer
196
230
  // If we have been given a block number to sync to and we have reached that number then return
197
231
  const currentBlockNumber = await this.getLatestBlockNumber();
198
232
  if (targetBlockNumber !== undefined && targetBlockNumber <= currentBlockNumber) {
199
- return currentBlockNumber;
233
+ if (blockHash === undefined) {
234
+ return currentBlockNumber;
235
+ }
236
+
237
+ // If a block hash was provided, verify we're on the expected fork
238
+ const currentHash = await this.getL2BlockHash(targetBlockNumber);
239
+ if (currentHash === blockHash.toString()) {
240
+ return currentBlockNumber;
241
+ }
242
+ // Hash mismatch: a reorg may have occurred, fall through to trigger sync
243
+ this.log.debug(
244
+ `World state block hash mismatch at ${targetBlockNumber} (expected ${blockHash}, got ${currentHash}). Triggering resync.`,
245
+ );
200
246
  }
201
247
  this.log.debug(`World State at ${currentBlockNumber} told to sync to ${targetBlockNumber ?? 'latest'}`);
202
248
 
203
249
  // If the archiver is behind the target block, force an archiver sync
204
250
  if (targetBlockNumber) {
205
- const archiverLatestBlock = await this.l2BlockSource.getBlockNumber();
251
+ const archiverLatestBlock = BlockNumber(await this.l2BlockSource.getBlockNumber());
206
252
  if (archiverLatestBlock < targetBlockNumber) {
207
253
  this.log.debug(`Archiver is at ${archiverLatestBlock} behind target block ${targetBlockNumber}.`);
208
254
  await this.l2BlockSource.syncImmediate();
@@ -214,7 +260,7 @@ export class ServerWorldStateSynchronizer
214
260
 
215
261
  // If we have been given a block number to sync to and we have not reached that number then fail
216
262
  const updatedBlockNumber = await this.getLatestBlockNumber();
217
- if (!skipThrowIfTargetNotReached && targetBlockNumber !== undefined && targetBlockNumber > updatedBlockNumber) {
263
+ if (targetBlockNumber !== undefined && targetBlockNumber > updatedBlockNumber) {
218
264
  throw new WorldStateSynchronizerError(
219
265
  `Unable to sync to block number ${targetBlockNumber} (last synced is ${updatedBlockNumber})`,
220
266
  {
@@ -228,35 +274,62 @@ export class ServerWorldStateSynchronizer
228
274
  );
229
275
  }
230
276
 
277
+ // If a block hash was provided, verify we're on the expected fork after syncing, throw otherwise
278
+ if (blockHash !== undefined && targetBlockNumber !== undefined) {
279
+ const updatedHash = await this.getL2BlockHash(targetBlockNumber);
280
+ if (updatedHash !== blockHash.toString()) {
281
+ throw new WorldStateSynchronizerError(
282
+ `Block hash mismatch at block ${targetBlockNumber} (expected ${blockHash} but got ${updatedHash})`,
283
+ {
284
+ cause: {
285
+ reason: 'block_hash_mismatch',
286
+ targetBlockNumber,
287
+ expectedHash: blockHash.toString(),
288
+ actualHash: updatedHash,
289
+ },
290
+ },
291
+ );
292
+ }
293
+ }
294
+
231
295
  return updatedBlockNumber;
232
296
  }
233
297
 
234
298
  /** Returns the L2 block hash for a given number. Used by the L2BlockStream for detecting reorgs. */
235
- public async getL2BlockHash(number: number): Promise<string | undefined> {
236
- if (number === 0) {
299
+ public async getL2BlockHash(number: BlockNumber): Promise<string | undefined> {
300
+ if (number === BlockNumber.ZERO) {
237
301
  return (await this.merkleTreeCommitted.getInitialHeader().hash()).toString();
238
302
  }
239
- if (this.latestBlockHashQuery?.hash === undefined || number !== this.latestBlockHashQuery.blockNumber) {
240
- this.latestBlockHashQuery = {
241
- hash: await this.merkleTreeCommitted
242
- .getLeafValue(MerkleTreeId.ARCHIVE, BigInt(number))
243
- .then(leaf => leaf?.toString()),
244
- blockNumber: number,
245
- };
246
- }
247
- return this.latestBlockHashQuery.hash;
303
+ return this.merkleTreeCommitted.getLeafValue(MerkleTreeId.ARCHIVE, BigInt(number)).then(leaf => leaf?.toString());
248
304
  }
249
305
 
250
- /** Returns the latest L2 block number for each tip of the chain (latest, proven, finalized). */
251
- public async getL2Tips(): Promise<L2Tips> {
306
+ /**
307
+ * Returns the proposed, proven, and finalized block tips of the chain. World state drives its block stream with
308
+ * `ignoreCheckpoints`, so it does not track checkpointed blocks or checkpoints and omits `checkpointed` from the tips
309
+ * it reports.
310
+ */
311
+ public async getL2Tips(): Promise<LocalChainTips> {
252
312
  const status = await this.merkleTreeDb.getStatusSummary();
253
- const unfinalizedBlockHash = await this.getL2BlockHash(Number(status.unfinalizedBlockNumber));
254
- const latestBlockId: L2BlockId = { number: Number(status.unfinalizedBlockNumber), hash: unfinalizedBlockHash! };
255
-
313
+ const unfinalizedBlockHashPromise = this.getL2BlockHash(status.unfinalizedBlockNumber);
314
+ const finalizedBlockHashPromise = this.getL2BlockHash(status.finalizedBlockNumber);
315
+
316
+ const provenBlockNumber = this.provenBlockNumber ?? status.finalizedBlockNumber;
317
+ const provenBlockHashPromise =
318
+ this.provenBlockNumber === undefined ? finalizedBlockHashPromise : this.getL2BlockHash(this.provenBlockNumber);
319
+
320
+ const [unfinalizedBlockHash, finalizedBlockHash, provenBlockHash] = await Promise.all([
321
+ unfinalizedBlockHashPromise,
322
+ finalizedBlockHashPromise,
323
+ provenBlockHashPromise,
324
+ ]);
256
325
  return {
257
- latest: latestBlockId,
258
- finalized: { number: Number(status.finalizedBlockNumber), hash: '' },
259
- proven: { number: Number(this.provenBlockNumber ?? status.finalizedBlockNumber), hash: '' }, // TODO(palla/reorg): Using finalized as proven for now
326
+ proposed: { number: status.unfinalizedBlockNumber, hash: unfinalizedBlockHash },
327
+ finalized: {
328
+ block: { number: status.finalizedBlockNumber, hash: finalizedBlockHash },
329
+ },
330
+ proven: {
331
+ block: { number: provenBlockNumber, hash: provenBlockHash },
332
+ },
260
333
  };
261
334
  }
262
335
 
@@ -264,7 +337,7 @@ export class ServerWorldStateSynchronizer
264
337
  public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise<void> {
265
338
  switch (event.type) {
266
339
  case 'blocks-added':
267
- await this.handleL2Blocks(event.blocks.map(b => b.block));
340
+ await this.handleL2Blocks(event.blocks);
268
341
  break;
269
342
  case 'chain-pruned':
270
343
  await this.handleChainPruned(event.block.number);
@@ -275,6 +348,15 @@ export class ServerWorldStateSynchronizer
275
348
  case 'chain-finalized':
276
349
  await this.handleChainFinalized(event.block.number);
277
350
  break;
351
+ // World state runs in block mode with ignoreCheckpoints: it tracks tips via blocks-added/pruned/proven/finalized
352
+ // and ignores the thin tip events (it never anchors on them).
353
+ case 'chain-proposed':
354
+ case 'chain-checkpointed':
355
+ break;
356
+ default: {
357
+ const _: never = event;
358
+ break;
359
+ }
278
360
  }
279
361
  }
280
362
 
@@ -284,21 +366,31 @@ export class ServerWorldStateSynchronizer
284
366
  * @returns Whether the block handled was produced by this same node.
285
367
  */
286
368
  private async handleL2Blocks(l2Blocks: L2Block[]) {
287
- this.log.trace(`Handling L2 blocks ${l2Blocks[0].number} to ${l2Blocks.at(-1)!.number}`);
369
+ this.log.debug(`Handling L2 blocks ${l2Blocks[0].number} to ${l2Blocks.at(-1)!.number}`);
370
+
371
+ // Fetch the L1->L2 messages for the first block in a checkpoint.
372
+ const messagesForBlocks = new Map<BlockNumber, Fr[]>();
373
+ await Promise.all(
374
+ l2Blocks
375
+ .filter(b => b.indexWithinCheckpoint === 0)
376
+ .map(async block => {
377
+ const l1ToL2Messages = await this.l2BlockSource.getL1ToL2Messages(block.checkpointNumber);
378
+ messagesForBlocks.set(block.number, l1ToL2Messages);
379
+ }),
380
+ );
288
381
 
289
- const messagePromises = l2Blocks.map(block => this.l2BlockSource.getL1ToL2Messages(block.number));
290
- const l1ToL2Messages: Fr[][] = await Promise.all(messagePromises);
291
382
  let updateStatus: WorldStateStatusFull | undefined = undefined;
292
-
293
- for (let i = 0; i < l2Blocks.length; i++) {
294
- const [duration, result] = await elapsed(() => this.handleL2Block(l2Blocks[i], l1ToL2Messages[i]));
295
- this.log.info(`World state updated with L2 block ${l2Blocks[i].number}`, {
383
+ for (const block of l2Blocks) {
384
+ const [duration, result] = await elapsed(() =>
385
+ this.handleL2Block(block, messagesForBlocks.get(block.number) ?? []),
386
+ );
387
+ this.log.info(`World state updated with L2 block ${block.number}`, {
296
388
  eventName: 'l2-block-handled',
297
389
  duration,
298
- unfinalizedBlockNumber: result.summary.unfinalizedBlockNumber,
299
- finalizedBlockNumber: result.summary.finalizedBlockNumber,
300
- oldestHistoricBlock: result.summary.oldestHistoricalBlock,
301
- ...l2Blocks[i].getStats(),
390
+ unfinalizedBlockNumber: BigInt(result.summary.unfinalizedBlockNumber),
391
+ finalizedBlockNumber: BigInt(result.summary.finalizedBlockNumber),
392
+ oldestHistoricBlock: BigInt(result.summary.oldestHistoricalBlock),
393
+ ...block.getStats(),
302
394
  } satisfies L2BlockHandledStats);
303
395
  updateStatus = result;
304
396
  }
@@ -315,17 +407,12 @@ export class ServerWorldStateSynchronizer
315
407
  * @returns Whether the block handled was produced by this same node.
316
408
  */
317
409
  private async handleL2Block(l2Block: L2Block, l1ToL2Messages: Fr[]): Promise<WorldStateStatusFull> {
318
- // First we check that the L1 to L2 messages hash to the block inHash.
319
- // Note that we cannot optimize this check by checking the root of the subtree after inserting the messages
320
- // to the real L1_TO_L2_MESSAGE_TREE (like we do in merkleTreeDb.handleL2BlockAndMessages(...)) because that
321
- // tree uses pedersen and we don't have access to the converted root.
322
- await this.verifyMessagesHashToInHash(l1ToL2Messages, l2Block.header.contentCommitment.inHash);
323
-
324
- // If the above check succeeds, we can proceed to handle the block.
325
- this.log.trace(`Pushing L2 block ${l2Block.number} to merkle tree db `, {
410
+ this.log.debug(`Pushing L2 block ${l2Block.number} to merkle tree db `, {
326
411
  blockNumber: l2Block.number,
327
412
  blockHash: await l2Block.hash().then(h => h.toString()),
328
413
  l1ToL2Messages: l1ToL2Messages.map(msg => msg.toString()),
414
+ blockHeader: l2Block.header.toInspect(),
415
+ blockStats: l2Block.getStats(),
329
416
  });
330
417
  const result = await this.merkleTreeDb.handleL2BlockAndMessages(l2Block, l1ToL2Messages);
331
418
 
@@ -337,32 +424,73 @@ export class ServerWorldStateSynchronizer
337
424
  return result;
338
425
  }
339
426
 
340
- private async handleChainFinalized(blockNumber: number) {
427
+ private async handleChainFinalized(blockNumber: BlockNumber) {
341
428
  this.log.verbose(`Finalized chain is now at block ${blockNumber}`);
342
- const summary = await this.merkleTreeDb.setFinalized(BigInt(blockNumber));
429
+ // If the finalized block number is older than the oldest available block in world state,
430
+ // skip entirely. The finalized block number can jump backwards (e.g. when the finalization
431
+ // heuristic changes) and try to read block data that has already been pruned. When this
432
+ // happens, there is nothing useful to do — the native world state is already finalized
433
+ // past this point and pruning has already happened.
434
+ const currentSummary = await this.merkleTreeDb.getStatusSummary();
435
+ if (blockNumber < currentSummary.oldestHistoricalBlock || blockNumber < 1) {
436
+ this.log.trace(
437
+ `Finalized block ${blockNumber} is older than the oldest available block ${currentSummary.oldestHistoricalBlock}. Skipping.`,
438
+ );
439
+ return;
440
+ }
441
+ const summary = await this.merkleTreeDb.setFinalized(blockNumber);
343
442
  if (this.historyToKeep === undefined) {
344
443
  return;
345
444
  }
346
- const newHistoricBlock = summary.finalizedBlockNumber - BigInt(this.historyToKeep) + 1n;
347
- if (newHistoricBlock <= 1) {
445
+ const finalisedBlockData = await this.l2BlockSource.getBlockData({ number: summary.finalizedBlockNumber });
446
+ if (finalisedBlockData === undefined) {
447
+ this.log.warn(
448
+ `Failed to retrieve checkpointed block for finalized block number: ${summary.finalizedBlockNumber}`,
449
+ );
450
+ return;
451
+ }
452
+ // Compute the required historic checkpoint number
453
+ const newHistoricCheckpointNumber = finalisedBlockData.checkpointNumber - this.historyToKeep + 1;
454
+ if (newHistoricCheckpointNumber <= 1) {
455
+ return;
456
+ }
457
+ // Retrieve the historic checkpoint
458
+ const historicCheckpoint = await this.l2BlockSource.getCheckpoint({
459
+ number: CheckpointNumber(newHistoricCheckpointNumber),
460
+ });
461
+ if (!historicCheckpoint) {
462
+ this.log.warn(`Failed to retrieve checkpoint number ${newHistoricCheckpointNumber} from Archiver`);
463
+ return;
464
+ }
465
+ if (historicCheckpoint.checkpoint.blocks.length === 0 || historicCheckpoint.checkpoint.blocks[0] === undefined) {
466
+ this.log.warn(`Retrieved checkpoint number ${newHistoricCheckpointNumber} has no blocks!`);
348
467
  return;
349
468
  }
350
- this.log.verbose(`Pruning historic blocks to ${newHistoricBlock}`);
351
- const status = await this.merkleTreeDb.removeHistoricalBlocks(newHistoricBlock);
469
+ // Find the block at the start of the checkpoint and remove blocks up to this one
470
+ const newHistoricBlock = historicCheckpoint.checkpoint.blocks[0];
471
+ if (newHistoricBlock.number <= currentSummary.oldestHistoricalBlock) {
472
+ this.log.debug(
473
+ `Historic block ${newHistoricBlock.number} is not newer than oldest available ${currentSummary.oldestHistoricalBlock}. Skipping prune.`,
474
+ );
475
+ return;
476
+ }
477
+ this.log.verbose(`Pruning historic blocks to ${newHistoricBlock.number}`);
478
+ const status = await this.merkleTreeDb.removeHistoricalBlocks(BlockNumber(newHistoricBlock.number));
352
479
  this.log.debug(`World state summary `, status.summary);
353
480
  }
354
481
 
355
- private handleChainProven(blockNumber: number) {
356
- this.provenBlockNumber = BigInt(blockNumber);
482
+ private handleChainProven(blockNumber: BlockNumber) {
483
+ this.provenBlockNumber = blockNumber;
357
484
  this.log.debug(`Proven chain is now at block ${blockNumber}`);
358
485
  return Promise.resolve();
359
486
  }
360
487
 
361
- private async handleChainPruned(blockNumber: number) {
362
- this.log.warn(`Chain pruned to block ${blockNumber}`);
363
- const status = await this.merkleTreeDb.unwindBlocks(BigInt(blockNumber));
364
- this.latestBlockHashQuery = undefined;
365
- this.provenBlockNumber = undefined;
488
+ private async handleChainPruned(blockNumber: BlockNumber) {
489
+ this.log.info(`Chain pruned to block ${blockNumber}`);
490
+ const status = await this.merkleTreeDb.unwindBlocks(blockNumber);
491
+ if (this.provenBlockNumber !== undefined && this.provenBlockNumber > blockNumber) {
492
+ this.provenBlockNumber = undefined;
493
+ }
366
494
  this.instrumentation.updateWorldStateMetrics(status);
367
495
  }
368
496
 
@@ -374,24 +502,4 @@ export class ServerWorldStateSynchronizer
374
502
  this.currentState = newState;
375
503
  this.log.debug(`Moved to state ${WorldStateRunningState[this.currentState]}`);
376
504
  }
377
-
378
- /**
379
- * Verifies that the L1 to L2 messages hash to the block inHash.
380
- * @param l1ToL2Messages - The L1 to L2 messages for the block.
381
- * @param inHash - The inHash of the block.
382
- * @throws If the L1 to L2 messages do not hash to the block inHash.
383
- */
384
- protected async verifyMessagesHashToInHash(l1ToL2Messages: Fr[], inHash: Fr) {
385
- const treeCalculator = await MerkleTreeCalculator.create(
386
- L1_TO_L2_MSG_SUBTREE_HEIGHT,
387
- Buffer.alloc(32),
388
- (lhs, rhs) => Promise.resolve(new SHA256Trunc().hash(lhs, rhs)),
389
- );
390
-
391
- const root = await treeCalculator.computeTreeRoot(l1ToL2Messages.map(msg => msg.toBuffer()));
392
-
393
- if (!root.equals(inHash.toBuffer())) {
394
- throw new Error('Obtained L1 to L2 messages failed to be hashed to the block inHash');
395
- }
396
- }
397
505
  }