@aztec/world-state 0.0.1-commit.fffb133c → 0.0.1-dev

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 (40) hide show
  1. package/dest/instrumentation/instrumentation.d.ts +1 -1
  2. package/dest/instrumentation/instrumentation.d.ts.map +1 -1
  3. package/dest/instrumentation/instrumentation.js +9 -2
  4. package/dest/native/fork_checkpoint.d.ts +7 -1
  5. package/dest/native/fork_checkpoint.d.ts.map +1 -1
  6. package/dest/native/fork_checkpoint.js +15 -3
  7. package/dest/native/merkle_trees_facade.d.ts +5 -5
  8. package/dest/native/merkle_trees_facade.d.ts.map +1 -1
  9. package/dest/native/merkle_trees_facade.js +9 -6
  10. package/dest/native/message.d.ts +17 -5
  11. package/dest/native/message.d.ts.map +1 -1
  12. package/dest/native/native_world_state.d.ts +5 -5
  13. package/dest/native/native_world_state.d.ts.map +1 -1
  14. package/dest/native/native_world_state.js +13 -9
  15. package/dest/native/native_world_state_instance.d.ts +3 -3
  16. package/dest/native/native_world_state_instance.d.ts.map +1 -1
  17. package/dest/native/native_world_state_instance.js +4 -4
  18. package/dest/synchronizer/config.d.ts +3 -3
  19. package/dest/synchronizer/config.d.ts.map +1 -1
  20. package/dest/synchronizer/config.js +6 -3
  21. package/dest/synchronizer/factory.d.ts +4 -3
  22. package/dest/synchronizer/factory.d.ts.map +1 -1
  23. package/dest/synchronizer/factory.js +5 -5
  24. package/dest/synchronizer/server_world_state_synchronizer.d.ts +4 -4
  25. package/dest/synchronizer/server_world_state_synchronizer.d.ts.map +1 -1
  26. package/dest/synchronizer/server_world_state_synchronizer.js +83 -19
  27. package/dest/test/utils.d.ts +1 -1
  28. package/dest/test/utils.d.ts.map +1 -1
  29. package/dest/test/utils.js +6 -1
  30. package/package.json +10 -10
  31. package/src/instrumentation/instrumentation.ts +9 -1
  32. package/src/native/fork_checkpoint.ts +19 -3
  33. package/src/native/merkle_trees_facade.ts +14 -7
  34. package/src/native/message.ts +18 -4
  35. package/src/native/native_world_state.ts +24 -11
  36. package/src/native/native_world_state_instance.ts +6 -4
  37. package/src/synchronizer/config.ts +7 -5
  38. package/src/synchronizer/factory.ts +7 -1
  39. package/src/synchronizer/server_world_state_synchronizer.ts +99 -21
  40. package/src/test/utils.ts +10 -1
@@ -4,9 +4,9 @@ import { fromEntries, padArrayEnd } from '@aztec/foundation/collection';
4
4
  import { Fr } from '@aztec/foundation/curves/bn254';
5
5
  import { EthAddress } from '@aztec/foundation/eth-address';
6
6
  import { tryRmDir } from '@aztec/foundation/fs';
7
- import { type Logger, createLogger } from '@aztec/foundation/log';
7
+ import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
8
8
  import type { L2Block } from '@aztec/stdlib/block';
9
- import { DatabaseVersionManager } from '@aztec/stdlib/database-version';
9
+ import { DatabaseVersionManager } from '@aztec/stdlib/database-version/manager';
10
10
  import type {
11
11
  IndexedTreeId,
12
12
  MerkleTreeReadOperations,
@@ -52,7 +52,7 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
52
52
  protected constructor(
53
53
  protected instance: NativeWorldState,
54
54
  protected readonly worldStateInstrumentation: WorldStateInstrumentation,
55
- protected readonly log: Logger = createLogger('world-state:database'),
55
+ protected readonly log: Logger,
56
56
  private readonly cleanup = () => Promise.resolve(),
57
57
  ) {}
58
58
 
@@ -62,9 +62,10 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
62
62
  wsTreeMapSizes: WorldStateTreeMapSizes,
63
63
  prefilledPublicData: PublicDataTreeLeaf[] = [],
64
64
  instrumentation = new WorldStateInstrumentation(getTelemetryClient()),
65
- log = createLogger('world-state:database'),
65
+ bindings?: LoggerBindings,
66
66
  cleanup = () => Promise.resolve(),
67
67
  ): Promise<NativeWorldStateService> {
68
+ const log = createLogger('world-state:database', bindings);
68
69
  const worldStateDirectory = join(dataDir, WORLD_STATE_DIR);
69
70
  // Create a version manager to handle versioning
70
71
  const versionManager = new DatabaseVersionManager({
@@ -72,7 +73,9 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
72
73
  rollupAddress,
73
74
  dataDirectory: worldStateDirectory,
74
75
  onOpen: (dir: string) => {
75
- return Promise.resolve(new NativeWorldState(dir, wsTreeMapSizes, prefilledPublicData, instrumentation));
76
+ return Promise.resolve(
77
+ new NativeWorldState(dir, wsTreeMapSizes, prefilledPublicData, instrumentation, bindings),
78
+ );
76
79
  },
77
80
  });
78
81
 
@@ -93,8 +96,9 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
93
96
  cleanupTmpDir = true,
94
97
  prefilledPublicData: PublicDataTreeLeaf[] = [],
95
98
  instrumentation = new WorldStateInstrumentation(getTelemetryClient()),
99
+ bindings?: LoggerBindings,
96
100
  ): Promise<NativeWorldStateService> {
97
- const log = createLogger('world-state:database');
101
+ const log = createLogger('world-state:database', bindings);
98
102
  const dataDir = await mkdtemp(join(tmpdir(), 'aztec-world-state-'));
99
103
  const dbMapSizeKb = 10 * 1024 * 1024;
100
104
  const worldStateTreeMapSizes: WorldStateTreeMapSizes = {
@@ -116,7 +120,15 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
116
120
  }
117
121
  };
118
122
 
119
- return this.new(rollupAddress, dataDir, worldStateTreeMapSizes, prefilledPublicData, instrumentation, log, cleanup);
123
+ return this.new(
124
+ rollupAddress,
125
+ dataDir,
126
+ worldStateTreeMapSizes,
127
+ prefilledPublicData,
128
+ instrumentation,
129
+ bindings,
130
+ cleanup,
131
+ );
120
132
  }
121
133
 
122
134
  protected async init() {
@@ -135,9 +147,7 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
135
147
 
136
148
  // the initial header _must_ be the first element in the archive tree
137
149
  // if this assertion fails, check that the hashing done in Header in yarn-project matches the initial header hash done in world_state.cpp
138
- const indices = await committed.findLeafIndices(MerkleTreeId.ARCHIVE, [
139
- (await this.initialHeader.hash()).toField(),
140
- ]);
150
+ const indices = await committed.findLeafIndices(MerkleTreeId.ARCHIVE, [(await this.initialHeader.hash()).toFr()]);
141
151
  const initialHeaderIndex = indices[0];
142
152
  assert.strictEqual(initialHeaderIndex, 0n, 'Invalid initial archive state');
143
153
  }
@@ -222,7 +232,10 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
222
232
  WorldStateMessageType.SYNC_BLOCK,
223
233
  {
224
234
  blockNumber: l2Block.number,
225
- blockHeaderHash: await l2Block.hash(),
235
+ blockHeaderHash: (await l2Block.hash()).toBuffer(),
236
+ // Forwarded so the native sync verifies the archive root against canonical and rejects a divergent tree.
237
+ expectedArchiveRoot: l2Block.archive.root.toBuffer(),
238
+ expectedPreviousArchiveRoot: l2Block.header.lastArchive.root.toBuffer(),
226
239
  paddedL1ToL2Messages: paddedL1ToL2Messages.map(serializeLeaf),
227
240
  paddedNoteHashes: paddedNoteHashes.map(serializeLeaf),
228
241
  paddedNullifiers: paddedNullifiers.map(serializeLeaf),
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ARCHIVE_HEIGHT,
3
- GeneratorIndex,
3
+ DomainSeparator,
4
4
  L1_TO_L2_MSG_TREE_HEIGHT,
5
5
  MAX_NULLIFIERS_PER_TX,
6
6
  MAX_TOTAL_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX,
@@ -8,7 +8,7 @@ import {
8
8
  NULLIFIER_TREE_HEIGHT,
9
9
  PUBLIC_DATA_TREE_HEIGHT,
10
10
  } from '@aztec/constants';
11
- import { type Logger, createLogger } from '@aztec/foundation/log';
11
+ import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
12
12
  import { NativeWorldState as BaseNativeWorldState, MsgpackChannel } from '@aztec/native';
13
13
  import { MerkleTreeId } from '@aztec/stdlib/trees';
14
14
  import type { PublicDataTreeLeaf } from '@aztec/stdlib/trees';
@@ -57,7 +57,8 @@ export class NativeWorldState implements NativeWorldStateInstance {
57
57
  private readonly wsTreeMapSizes: WorldStateTreeMapSizes,
58
58
  private readonly prefilledPublicData: PublicDataTreeLeaf[] = [],
59
59
  private readonly instrumentation: WorldStateInstrumentation,
60
- private readonly log: Logger = createLogger('world-state:database'),
60
+ bindings?: LoggerBindings,
61
+ private readonly log: Logger = createLogger('world-state:database', bindings),
61
62
  ) {
62
63
  const threads = Math.min(cpus().length, MAX_WORLD_STATE_THREADS);
63
64
  log.info(
@@ -80,7 +81,7 @@ export class NativeWorldState implements NativeWorldStateInstance {
80
81
  [MerkleTreeId.PUBLIC_DATA_TREE]: 2 * MAX_TOTAL_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX,
81
82
  },
82
83
  prefilledPublicDataBufferArray,
83
- GeneratorIndex.BLOCK_HASH,
84
+ DomainSeparator.BLOCK_HEADER_HASH,
84
85
  {
85
86
  [MerkleTreeId.NULLIFIER_TREE]: wsTreeMapSizes.nullifierTreeMapSizeKb,
86
87
  [MerkleTreeId.NOTE_HASH_TREE]: wsTreeMapSizes.noteHashTreeMapSizeKb,
@@ -105,6 +106,7 @@ export class NativeWorldState implements NativeWorldStateInstance {
105
106
  this.wsTreeMapSizes,
106
107
  this.prefilledPublicData,
107
108
  this.instrumentation,
109
+ this.log.getBindings(),
108
110
  this.log,
109
111
  );
110
112
  }
@@ -29,8 +29,8 @@ export interface WorldStateConfig {
29
29
  /** Optional directory for the world state DB, if unspecified will default to the general data directory */
30
30
  worldStateDataDirectory?: string;
31
31
 
32
- /** The number of historic blocks to maintain */
33
- worldStateBlockHistory: number;
32
+ /** The number of historic checkpoints worth of blocks to maintain */
33
+ worldStateCheckpointHistory: number;
34
34
  }
35
35
 
36
36
  export const worldStateConfigMappings: ConfigMappingsType<WorldStateConfig> = {
@@ -84,9 +84,11 @@ export const worldStateConfigMappings: ConfigMappingsType<WorldStateConfig> = {
84
84
  env: 'WS_DATA_DIRECTORY',
85
85
  description: 'Optional directory for the world state database',
86
86
  },
87
- worldStateBlockHistory: {
88
- env: 'WS_NUM_HISTORIC_BLOCKS',
89
- description: 'The number of historic blocks to maintain. Values less than 1 mean all history is maintained',
87
+ worldStateCheckpointHistory: {
88
+ env: 'WS_NUM_HISTORIC_CHECKPOINTS',
89
+ description:
90
+ 'The number of historic checkpoints worth of blocks to maintain. Values less than 1 mean all history is maintained',
91
+ fallback: ['WS_NUM_HISTORIC_BLOCKS'],
90
92
  ...numberConfigHelper(64),
91
93
  },
92
94
  };
@@ -1,3 +1,4 @@
1
+ import type { LoggerBindings } from '@aztec/foundation/log';
1
2
  import type { DataStoreConfig } from '@aztec/kv-store/config';
2
3
  import type { L2BlockSource } from '@aztec/stdlib/block';
3
4
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
@@ -22,9 +23,10 @@ export async function createWorldStateSynchronizer(
22
23
  l2BlockSource: L2BlockSource & L1ToL2MessageSource,
23
24
  prefilledPublicData: PublicDataTreeLeaf[] = [],
24
25
  client: TelemetryClient = getTelemetryClient(),
26
+ bindings?: LoggerBindings,
25
27
  ) {
26
28
  const instrumentation = new WorldStateInstrumentation(client);
27
- const merkleTrees = await createWorldState(config, prefilledPublicData, instrumentation);
29
+ const merkleTrees = await createWorldState(config, prefilledPublicData, instrumentation, bindings);
28
30
  return new ServerWorldStateSynchronizer(merkleTrees, l2BlockSource, config, instrumentation);
29
31
  }
30
32
 
@@ -42,6 +44,7 @@ export async function createWorldState(
42
44
  Pick<DataStoreConfig, 'dataDirectory' | 'dataStoreMapSizeKb' | 'l1Contracts'>,
43
45
  prefilledPublicData: PublicDataTreeLeaf[] = [],
44
46
  instrumentation: WorldStateInstrumentation = new WorldStateInstrumentation(getTelemetryClient()),
47
+ bindings?: LoggerBindings,
45
48
  ) {
46
49
  const dataDirectory = config.worldStateDataDirectory ?? config.dataDirectory;
47
50
  const dataStoreMapSizeKb = config.worldStateDbMapSizeKb ?? config.dataStoreMapSizeKb;
@@ -65,11 +68,14 @@ export async function createWorldState(
65
68
  wsTreeMapSizes,
66
69
  prefilledPublicData,
67
70
  instrumentation,
71
+ bindings,
68
72
  )
69
73
  : await NativeWorldStateService.tmp(
70
74
  config.l1Contracts.rollupAddress,
71
75
  !['true', '1'].includes(process.env.DEBUG_WORLD_STATE!),
72
76
  prefilledPublicData,
77
+ instrumentation,
78
+ bindings,
73
79
  );
74
80
 
75
81
  return merkleTrees;
@@ -1,10 +1,11 @@
1
- import { GENESIS_BLOCK_HEADER_HASH, INITIAL_L2_BLOCK_NUM, INITIAL_L2_CHECKPOINT_NUM } from '@aztec/constants';
2
- import { BlockNumber } from '@aztec/foundation/branded-types';
1
+ import { GENESIS_BLOCK_HEADER_HASH, INITIAL_CHECKPOINT_NUMBER, INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
2
+ import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
3
3
  import type { Fr } from '@aztec/foundation/curves/bn254';
4
4
  import { type Logger, createLogger } from '@aztec/foundation/log';
5
5
  import { promiseWithResolvers } from '@aztec/foundation/promise';
6
6
  import { elapsed } from '@aztec/foundation/timer';
7
7
  import {
8
+ type BlockHash,
8
9
  GENESIS_CHECKPOINT_HEADER_HASH,
9
10
  type L2Block,
10
11
  type L2BlockId,
@@ -64,7 +65,7 @@ export class ServerWorldStateSynchronizer
64
65
  private readonly log: Logger = createLogger('world_state'),
65
66
  ) {
66
67
  this.merkleTreeCommitted = this.merkleTreeDb.getCommitted();
67
- this.historyToKeep = config.worldStateBlockHistory < 1 ? undefined : config.worldStateBlockHistory;
68
+ this.historyToKeep = config.worldStateCheckpointHistory < 1 ? undefined : config.worldStateCheckpointHistory;
68
69
  this.log.info(
69
70
  `Created world state synchroniser with block history of ${
70
71
  this.historyToKeep === undefined ? 'infinity' : this.historyToKeep
@@ -177,13 +178,10 @@ export class ServerWorldStateSynchronizer
177
178
  /**
178
179
  * Forces an immediate sync.
179
180
  * @param targetBlockNumber - The target block number that we must sync to. Will download unproven blocks if needed to reach it.
180
- * @param skipThrowIfTargetNotReached - Whether to skip throwing if the target block number is not reached.
181
+ * @param blockHash - If provided, verifies the block at targetBlockNumber matches this hash. On mismatch, triggers a resync (reorg detection).
181
182
  * @returns A promise that resolves with the block number the world state was synced to
182
183
  */
183
- public async syncImmediate(
184
- targetBlockNumber?: BlockNumber,
185
- skipThrowIfTargetNotReached?: boolean,
186
- ): Promise<BlockNumber> {
184
+ public async syncImmediate(targetBlockNumber?: BlockNumber, blockHash?: BlockHash): Promise<BlockNumber> {
187
185
  if (this.currentState !== WorldStateRunningState.RUNNING) {
188
186
  throw new Error(`World State is not running. Unable to perform sync.`);
189
187
  }
@@ -195,7 +193,19 @@ export class ServerWorldStateSynchronizer
195
193
  // If we have been given a block number to sync to and we have reached that number then return
196
194
  const currentBlockNumber = await this.getLatestBlockNumber();
197
195
  if (targetBlockNumber !== undefined && targetBlockNumber <= currentBlockNumber) {
198
- return currentBlockNumber;
196
+ if (blockHash === undefined) {
197
+ return currentBlockNumber;
198
+ }
199
+
200
+ // If a block hash was provided, verify we're on the expected fork
201
+ const currentHash = await this.getL2BlockHash(targetBlockNumber);
202
+ if (currentHash === blockHash.toString()) {
203
+ return currentBlockNumber;
204
+ }
205
+ // Hash mismatch: a reorg may have occurred, fall through to trigger sync
206
+ this.log.debug(
207
+ `World state block hash mismatch at ${targetBlockNumber} (expected ${blockHash}, got ${currentHash}). Triggering resync.`,
208
+ );
199
209
  }
200
210
  this.log.debug(`World State at ${currentBlockNumber} told to sync to ${targetBlockNumber ?? 'latest'}`);
201
211
 
@@ -213,7 +223,7 @@ export class ServerWorldStateSynchronizer
213
223
 
214
224
  // If we have been given a block number to sync to and we have not reached that number then fail
215
225
  const updatedBlockNumber = await this.getLatestBlockNumber();
216
- if (!skipThrowIfTargetNotReached && targetBlockNumber !== undefined && targetBlockNumber > updatedBlockNumber) {
226
+ if (targetBlockNumber !== undefined && targetBlockNumber > updatedBlockNumber) {
217
227
  throw new WorldStateSynchronizerError(
218
228
  `Unable to sync to block number ${targetBlockNumber} (last synced is ${updatedBlockNumber})`,
219
229
  {
@@ -227,6 +237,24 @@ export class ServerWorldStateSynchronizer
227
237
  );
228
238
  }
229
239
 
240
+ // If a block hash was provided, verify we're on the expected fork after syncing, throw otherwise
241
+ if (blockHash !== undefined && targetBlockNumber !== undefined) {
242
+ const updatedHash = await this.getL2BlockHash(targetBlockNumber);
243
+ if (updatedHash !== blockHash.toString()) {
244
+ throw new WorldStateSynchronizerError(
245
+ `Block hash mismatch at block ${targetBlockNumber} (expected ${blockHash} but got ${updatedHash})`,
246
+ {
247
+ cause: {
248
+ reason: 'block_hash_mismatch',
249
+ targetBlockNumber,
250
+ expectedHash: blockHash.toString(),
251
+ actualHash: updatedHash,
252
+ },
253
+ },
254
+ );
255
+ }
256
+ }
257
+
230
258
  return updatedBlockNumber;
231
259
  }
232
260
 
@@ -263,15 +291,15 @@ export class ServerWorldStateSynchronizer
263
291
  proposed: latestBlockId,
264
292
  checkpointed: {
265
293
  block: { number: INITIAL_L2_BLOCK_NUM, hash: GENESIS_BLOCK_HEADER_HASH.toString() },
266
- checkpoint: { number: INITIAL_L2_CHECKPOINT_NUM, hash: genesisCheckpointHeaderHash },
294
+ checkpoint: { number: INITIAL_CHECKPOINT_NUMBER, hash: genesisCheckpointHeaderHash },
267
295
  },
268
296
  finalized: {
269
297
  block: { number: status.finalizedBlockNumber, hash: finalizedBlockHash ?? '' },
270
- checkpoint: { number: INITIAL_L2_CHECKPOINT_NUM, hash: genesisCheckpointHeaderHash },
298
+ checkpoint: { number: INITIAL_CHECKPOINT_NUMBER, hash: genesisCheckpointHeaderHash },
271
299
  },
272
300
  proven: {
273
301
  block: { number: provenBlockNumber, hash: provenBlockHash ?? '' },
274
- checkpoint: { number: INITIAL_L2_CHECKPOINT_NUM, hash: genesisCheckpointHeaderHash },
302
+ checkpoint: { number: INITIAL_CHECKPOINT_NUMBER, hash: genesisCheckpointHeaderHash },
275
303
  },
276
304
  };
277
305
  }
@@ -300,7 +328,7 @@ export class ServerWorldStateSynchronizer
300
328
  * @returns Whether the block handled was produced by this same node.
301
329
  */
302
330
  private async handleL2Blocks(l2Blocks: L2Block[]) {
303
- this.log.trace(`Handling L2 blocks ${l2Blocks[0].number} to ${l2Blocks.at(-1)!.number}`);
331
+ this.log.debug(`Handling L2 blocks ${l2Blocks[0].number} to ${l2Blocks.at(-1)!.number}`);
304
332
 
305
333
  // Fetch the L1->L2 messages for the first block in a checkpoint.
306
334
  const messagesForBlocks = new Map<BlockNumber, Fr[]>();
@@ -341,10 +369,12 @@ export class ServerWorldStateSynchronizer
341
369
  * @returns Whether the block handled was produced by this same node.
342
370
  */
343
371
  private async handleL2Block(l2Block: L2Block, l1ToL2Messages: Fr[]): Promise<WorldStateStatusFull> {
344
- this.log.trace(`Pushing L2 block ${l2Block.number} to merkle tree db `, {
372
+ this.log.debug(`Pushing L2 block ${l2Block.number} to merkle tree db `, {
345
373
  blockNumber: l2Block.number,
346
374
  blockHash: await l2Block.hash().then(h => h.toString()),
347
375
  l1ToL2Messages: l1ToL2Messages.map(msg => msg.toString()),
376
+ blockHeader: l2Block.header.toInspect(),
377
+ blockStats: l2Block.getStats(),
348
378
  });
349
379
  const result = await this.merkleTreeDb.handleL2BlockAndMessages(l2Block, l1ToL2Messages);
350
380
 
@@ -357,17 +387,65 @@ export class ServerWorldStateSynchronizer
357
387
  }
358
388
 
359
389
  private async handleChainFinalized(blockNumber: BlockNumber) {
360
- this.log.verbose(`Finalized chain is now at block ${blockNumber}`);
390
+ this.log.verbose(`Updating world state finalized chain to block ${blockNumber}`);
391
+ // If the finalized block number is older than the oldest available block in world state,
392
+ // skip entirely. The finalized block number can jump backwards (e.g. when the finalization
393
+ // heuristic changes) and try to read block data that has already been pruned. When this
394
+ // happens, there is nothing useful to do — the native world state is already finalized
395
+ // past this point and pruning has already happened.
396
+ const currentSummary = await this.merkleTreeDb.getStatusSummary();
397
+ if (blockNumber < currentSummary.oldestHistoricalBlock || blockNumber < 1) {
398
+ this.log.trace(
399
+ `Finalized block ${blockNumber} is older than the oldest available block ${currentSummary.oldestHistoricalBlock}. Skipping.`,
400
+ );
401
+ return;
402
+ }
361
403
  const summary = await this.merkleTreeDb.setFinalized(blockNumber);
404
+ this.log.info(`World state finalized chain updated`, {
405
+ finalizedBlockNumber: summary.finalizedBlockNumber,
406
+ unfinalizedBlockNumber: summary.unfinalizedBlockNumber,
407
+ oldestHistoricalBlock: summary.oldestHistoricalBlock,
408
+ });
362
409
  if (this.historyToKeep === undefined) {
363
410
  return;
364
411
  }
365
- const newHistoricBlock = summary.finalizedBlockNumber - this.historyToKeep + 1;
366
- if (newHistoricBlock <= 1) {
412
+ // Get the checkpointed block for the finalized block number
413
+ const finalisedCheckpoint = await this.l2BlockSource.getCheckpointedBlock(summary.finalizedBlockNumber);
414
+ if (finalisedCheckpoint === undefined) {
415
+ this.log.warn(
416
+ `Failed to retrieve checkpointed block for finalized block number: ${summary.finalizedBlockNumber}`,
417
+ );
418
+ return;
419
+ }
420
+ // Compute the required historic checkpoint number
421
+ const newHistoricCheckpointNumber = finalisedCheckpoint.checkpointNumber - this.historyToKeep + 1;
422
+ if (newHistoricCheckpointNumber <= 1) {
423
+ return;
424
+ }
425
+ // Retrieve the historic checkpoint
426
+ const historicCheckpoints = await this.l2BlockSource.getCheckpoints(
427
+ CheckpointNumber(newHistoricCheckpointNumber),
428
+ 1,
429
+ );
430
+ if (historicCheckpoints.length === 0 || historicCheckpoints[0] === undefined) {
431
+ this.log.warn(`Failed to retrieve checkpoint number ${newHistoricCheckpointNumber} from Archiver`);
432
+ return;
433
+ }
434
+ const historicCheckpoint = historicCheckpoints[0];
435
+ if (historicCheckpoint.checkpoint.blocks.length === 0 || historicCheckpoint.checkpoint.blocks[0] === undefined) {
436
+ this.log.warn(`Retrieved checkpoint number ${newHistoricCheckpointNumber} has no blocks!`);
437
+ return;
438
+ }
439
+ // Find the block at the start of the checkpoint and remove blocks up to this one
440
+ const newHistoricBlock = historicCheckpoint.checkpoint.blocks[0];
441
+ if (newHistoricBlock.number <= currentSummary.oldestHistoricalBlock) {
442
+ this.log.debug(
443
+ `Historic block ${newHistoricBlock.number} is not newer than oldest available ${currentSummary.oldestHistoricalBlock}. Skipping prune.`,
444
+ );
367
445
  return;
368
446
  }
369
- this.log.verbose(`Pruning historic blocks to ${newHistoricBlock}`);
370
- const status = await this.merkleTreeDb.removeHistoricalBlocks(BlockNumber(newHistoricBlock));
447
+ this.log.verbose(`Pruning historic blocks to ${newHistoricBlock.number}`);
448
+ const status = await this.merkleTreeDb.removeHistoricalBlocks(BlockNumber(newHistoricBlock.number));
371
449
  this.log.debug(`World state summary `, status.summary);
372
450
  }
373
451
 
@@ -378,7 +456,7 @@ export class ServerWorldStateSynchronizer
378
456
  }
379
457
 
380
458
  private async handleChainPruned(blockNumber: BlockNumber) {
381
- this.log.warn(`Chain pruned to block ${blockNumber}`);
459
+ this.log.info(`Chain pruned to block ${blockNumber}`);
382
460
  const status = await this.merkleTreeDb.unwindBlocks(blockNumber);
383
461
  this.provenBlockNumber = undefined;
384
462
  this.instrumentation.updateWorldStateMetrics(status);
package/src/test/utils.ts CHANGED
@@ -60,7 +60,16 @@ export async function updateBlockState(block: L2Block, l1ToL2Messages: Fr[], for
60
60
  await Promise.all([publicDataInsert, nullifierInsert, noteHashInsert, messageInsert]);
61
61
 
62
62
  const state = await fork.getStateReference();
63
- block.header = BlockHeader.from({ ...block.header, state });
63
+
64
+ // Capture the archive root *before* appending this block so the header's lastArchive chains off the previous block.
65
+ // sync_block now verifies lastArchive against the committed archive root, so a block carrying an unchained value
66
+ // (e.g. the random lastArchive from L2Block.random) would be rejected as a divergence from the canonical chain.
67
+ const previousArchive = await fork.getTreeInfo(MerkleTreeId.ARCHIVE);
68
+ block.header = BlockHeader.from({
69
+ ...block.header,
70
+ state,
71
+ lastArchive: new AppendOnlyTreeSnapshot(Fr.fromBuffer(previousArchive.root), Number(previousArchive.size)),
72
+ });
64
73
  await fork.updateArchive(block.header);
65
74
 
66
75
  const archiveState = await fork.getTreeInfo(MerkleTreeId.ARCHIVE);