@aztec/world-state 0.0.1-commit.f2ce05ee → 0.0.1-commit.f5a9928

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 (57) hide show
  1. package/dest/native/fork_checkpoint.d.ts +7 -1
  2. package/dest/native/fork_checkpoint.d.ts.map +1 -1
  3. package/dest/native/fork_checkpoint.js +15 -3
  4. package/dest/native/ipc_world_state_instance.d.ts +50 -0
  5. package/dest/native/ipc_world_state_instance.d.ts.map +1 -0
  6. package/dest/native/ipc_world_state_instance.js +635 -0
  7. package/dest/native/merkle_trees_facade.d.ts +9 -6
  8. package/dest/native/merkle_trees_facade.d.ts.map +1 -1
  9. package/dest/native/merkle_trees_facade.js +33 -13
  10. package/dest/native/message.d.ts +17 -6
  11. package/dest/native/message.d.ts.map +1 -1
  12. package/dest/native/native_world_state.d.ts +31 -5
  13. package/dest/native/native_world_state.d.ts.map +1 -1
  14. package/dest/native/native_world_state.js +83 -27
  15. package/dest/native/native_world_state_instance.d.ts +5 -4
  16. package/dest/native/native_world_state_instance.d.ts.map +1 -1
  17. package/dest/native/native_world_state_instance.js +46 -26
  18. package/dest/native/world_state_ops_queue.js +5 -5
  19. package/dest/synchronizer/config.d.ts +3 -3
  20. package/dest/synchronizer/config.d.ts.map +1 -1
  21. package/dest/synchronizer/config.js +15 -13
  22. package/dest/synchronizer/errors.d.ts +8 -1
  23. package/dest/synchronizer/errors.d.ts.map +1 -1
  24. package/dest/synchronizer/errors.js +8 -1
  25. package/dest/synchronizer/factory.d.ts +5 -5
  26. package/dest/synchronizer/factory.d.ts.map +1 -1
  27. package/dest/synchronizer/factory.js +7 -6
  28. package/dest/synchronizer/index.d.ts +2 -1
  29. package/dest/synchronizer/index.d.ts.map +1 -1
  30. package/dest/synchronizer/index.js +1 -0
  31. package/dest/synchronizer/server_world_state_synchronizer.d.ts +13 -8
  32. package/dest/synchronizer/server_world_state_synchronizer.d.ts.map +1 -1
  33. package/dest/synchronizer/server_world_state_synchronizer.js +128 -44
  34. package/dest/test/utils.d.ts +1 -1
  35. package/dest/test/utils.d.ts.map +1 -1
  36. package/dest/test/utils.js +6 -1
  37. package/dest/testing.d.ts +4 -3
  38. package/dest/testing.d.ts.map +1 -1
  39. package/dest/testing.js +22 -7
  40. package/dest/world-state-db/merkle_tree_db.d.ts +1 -10
  41. package/dest/world-state-db/merkle_tree_db.d.ts.map +1 -1
  42. package/package.json +12 -11
  43. package/src/native/fork_checkpoint.ts +19 -3
  44. package/src/native/ipc_world_state_instance.ts +725 -0
  45. package/src/native/merkle_trees_facade.ts +41 -16
  46. package/src/native/message.ts +18 -5
  47. package/src/native/native_world_state.ts +101 -34
  48. package/src/native/native_world_state_instance.ts +57 -30
  49. package/src/native/world_state_ops_queue.ts +5 -5
  50. package/src/synchronizer/config.ts +21 -15
  51. package/src/synchronizer/errors.ts +8 -0
  52. package/src/synchronizer/factory.ts +13 -11
  53. package/src/synchronizer/index.ts +1 -0
  54. package/src/synchronizer/server_world_state_synchronizer.ts +148 -40
  55. package/src/test/utils.ts +10 -1
  56. package/src/testing.ts +19 -10
  57. package/src/world-state-db/merkle_tree_db.ts +0 -10
@@ -1,4 +1,9 @@
1
- import { type ConfigMappingsType, getConfigFromMappings, numberConfigHelper } from '@aztec/foundation/config';
1
+ import {
2
+ type ConfigMappingsType,
3
+ getConfigFromMappings,
4
+ numberConfigHelper,
5
+ optionalNumberConfigHelper,
6
+ } from '@aztec/foundation/config';
2
7
 
3
8
  /** World State synchronizer configuration values. */
4
9
  export interface WorldStateConfig {
@@ -29,54 +34,53 @@ export interface WorldStateConfig {
29
34
  /** Optional directory for the world state DB, if unspecified will default to the general data directory */
30
35
  worldStateDataDirectory?: string;
31
36
 
32
- /** The number of historic blocks to maintain */
33
- worldStateBlockHistory: number;
37
+ /** The number of historic checkpoints worth of blocks to maintain */
38
+ worldStateCheckpointHistory: number;
34
39
  }
35
40
 
36
41
  export const worldStateConfigMappings: ConfigMappingsType<WorldStateConfig> = {
37
42
  worldStateBlockCheckIntervalMS: {
38
43
  env: 'WS_BLOCK_CHECK_INTERVAL_MS',
39
- parseEnv: (val: string) => +val,
40
- defaultValue: 100,
44
+ ...numberConfigHelper(100),
41
45
  description: 'The frequency in which to check.',
42
46
  },
43
47
  worldStateBlockRequestBatchSize: {
44
48
  env: 'WS_BLOCK_REQUEST_BATCH_SIZE',
45
- parseEnv: (val: string | undefined) => (val ? +val : undefined),
49
+ ...optionalNumberConfigHelper(),
46
50
  description: 'Size of the batch for each get-blocks request from the synchronizer to the archiver.',
47
51
  },
48
52
  worldStateDbMapSizeKb: {
49
53
  env: 'WS_DB_MAP_SIZE_KB',
50
- parseEnv: (val: string | undefined) => (val ? +val : undefined),
54
+ ...optionalNumberConfigHelper(),
51
55
  description: 'The maximum possible size of the world state DB in KB. Overwrites the general dataStoreMapSizeKb.',
52
56
  },
53
57
  archiveTreeMapSizeKb: {
54
58
  env: 'ARCHIVE_TREE_MAP_SIZE_KB',
55
- parseEnv: (val: string | undefined) => (val ? +val : undefined),
59
+ ...optionalNumberConfigHelper(),
56
60
  description:
57
61
  'The maximum possible size of the world state archive tree in KB. Overwrites the general worldStateDbMapSizeKb.',
58
62
  },
59
63
  nullifierTreeMapSizeKb: {
60
64
  env: 'NULLIFIER_TREE_MAP_SIZE_KB',
61
- parseEnv: (val: string | undefined) => (val ? +val : undefined),
65
+ ...optionalNumberConfigHelper(),
62
66
  description:
63
67
  'The maximum possible size of the world state nullifier tree in KB. Overwrites the general worldStateDbMapSizeKb.',
64
68
  },
65
69
  noteHashTreeMapSizeKb: {
66
70
  env: 'NOTE_HASH_TREE_MAP_SIZE_KB',
67
- parseEnv: (val: string | undefined) => (val ? +val : undefined),
71
+ ...optionalNumberConfigHelper(),
68
72
  description:
69
73
  'The maximum possible size of the world state note hash tree in KB. Overwrites the general worldStateDbMapSizeKb.',
70
74
  },
71
75
  messageTreeMapSizeKb: {
72
76
  env: 'MESSAGE_TREE_MAP_SIZE_KB',
73
- parseEnv: (val: string | undefined) => (val ? +val : undefined),
77
+ ...optionalNumberConfigHelper(),
74
78
  description:
75
79
  'The maximum possible size of the world state message tree in KB. Overwrites the general worldStateDbMapSizeKb.',
76
80
  },
77
81
  publicDataTreeMapSizeKb: {
78
82
  env: 'PUBLIC_DATA_TREE_MAP_SIZE_KB',
79
- parseEnv: (val: string | undefined) => (val ? +val : undefined),
83
+ ...optionalNumberConfigHelper(),
80
84
  description:
81
85
  'The maximum possible size of the world state public data tree in KB. Overwrites the general worldStateDbMapSizeKb.',
82
86
  },
@@ -84,9 +88,11 @@ export const worldStateConfigMappings: ConfigMappingsType<WorldStateConfig> = {
84
88
  env: 'WS_DATA_DIRECTORY',
85
89
  description: 'Optional directory for the world state database',
86
90
  },
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',
91
+ worldStateCheckpointHistory: {
92
+ env: 'WS_NUM_HISTORIC_CHECKPOINTS',
93
+ description:
94
+ 'The number of historic checkpoints worth of blocks to maintain. Values less than 1 mean all history is maintained',
95
+ fallback: ['WS_NUM_HISTORIC_BLOCKS'],
90
96
  ...numberConfigHelper(64),
91
97
  },
92
98
  };
@@ -1,5 +1,13 @@
1
+ /**
2
+ * Thrown by {@link ServerWorldStateSynchronizer.syncImmediate} when world state cannot be synced to the requested
3
+ * target: either the block is not available from the block source (`block_not_available`, e.g. it was pruned away)
4
+ * or the synced block does not match the requested hash (`block_hash_mismatch`, i.e. a reorg). Both causes are
5
+ * transient from the caller's perspective: re-resolving the query against the current chain and retrying may succeed
6
+ * or produce a more precise error.
7
+ */
1
8
  export class WorldStateSynchronizerError extends Error {
2
9
  constructor(message: string, options?: ErrorOptions) {
3
10
  super(message, options);
11
+ this.name = 'WorldStateSynchronizerError';
4
12
  }
5
13
  }
@@ -1,8 +1,8 @@
1
1
  import type { LoggerBindings } from '@aztec/foundation/log';
2
- import type { DataStoreConfig } from '@aztec/kv-store/config';
3
2
  import type { L2BlockSource } from '@aztec/stdlib/block';
3
+ import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
4
4
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
5
- import type { PublicDataTreeLeaf } from '@aztec/stdlib/trees';
5
+ import { EMPTY_GENESIS_DATA, type GenesisData, isGenesisData } from '@aztec/stdlib/world-state';
6
6
  import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
7
7
 
8
8
  import { WorldStateInstrumentation } from '../instrumentation/instrumentation.js';
@@ -21,12 +21,14 @@ export interface WorldStateTreeMapSizes {
21
21
  export async function createWorldStateSynchronizer(
22
22
  config: WorldStateConfig & DataStoreConfig,
23
23
  l2BlockSource: L2BlockSource & L1ToL2MessageSource,
24
- prefilledPublicData: PublicDataTreeLeaf[] = [],
24
+ genesisOrNativeWorldState: GenesisData | NativeWorldStateService,
25
25
  client: TelemetryClient = getTelemetryClient(),
26
26
  bindings?: LoggerBindings,
27
27
  ) {
28
28
  const instrumentation = new WorldStateInstrumentation(client);
29
- const merkleTrees = await createWorldState(config, prefilledPublicData, instrumentation, bindings);
29
+ const merkleTrees = isGenesisData(genesisOrNativeWorldState)
30
+ ? await createWorldState(config, genesisOrNativeWorldState, instrumentation, bindings)
31
+ : genesisOrNativeWorldState;
30
32
  return new ServerWorldStateSynchronizer(merkleTrees, l2BlockSource, config, instrumentation);
31
33
  }
32
34
 
@@ -41,8 +43,8 @@ export async function createWorldState(
41
43
  | 'messageTreeMapSizeKb'
42
44
  | 'publicDataTreeMapSizeKb'
43
45
  > &
44
- Pick<DataStoreConfig, 'dataDirectory' | 'dataStoreMapSizeKb' | 'l1Contracts'>,
45
- prefilledPublicData: PublicDataTreeLeaf[] = [],
46
+ Pick<DataStoreConfig, 'dataDirectory' | 'dataStoreMapSizeKb' | 'rollupAddress'>,
47
+ genesis: GenesisData = EMPTY_GENESIS_DATA,
46
48
  instrumentation: WorldStateInstrumentation = new WorldStateInstrumentation(getTelemetryClient()),
47
49
  bindings?: LoggerBindings,
48
50
  ) {
@@ -56,24 +58,24 @@ export async function createWorldState(
56
58
  publicDataTreeMapSizeKb: config.publicDataTreeMapSizeKb ?? dataStoreMapSizeKb,
57
59
  };
58
60
 
59
- if (!config.l1Contracts?.rollupAddress) {
61
+ if (!config.rollupAddress) {
60
62
  throw new Error('Rollup address is required to create a world state synchronizer.');
61
63
  }
62
64
 
63
65
  // If a data directory is provided in config, then create a persistent store.
64
66
  const merkleTrees = dataDirectory
65
67
  ? await NativeWorldStateService.new(
66
- config.l1Contracts.rollupAddress,
68
+ config.rollupAddress,
67
69
  dataDirectory,
68
70
  wsTreeMapSizes,
69
- prefilledPublicData,
71
+ genesis,
70
72
  instrumentation,
71
73
  bindings,
72
74
  )
73
75
  : await NativeWorldStateService.tmp(
74
- config.l1Contracts.rollupAddress,
76
+ config.rollupAddress,
75
77
  !['true', '1'].includes(process.env.DEBUG_WORLD_STATE!),
76
- prefilledPublicData,
78
+ genesis,
77
79
  instrumentation,
78
80
  bindings,
79
81
  );
@@ -1,2 +1,3 @@
1
1
  export * from './server_world_state_synchronizer.js';
2
2
  export * from './factory.js';
3
+ export * from './errors.js';
@@ -1,19 +1,17 @@
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 { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
3
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
6
  import {
8
- GENESIS_CHECKPOINT_HEADER_HASH,
7
+ type BlockHash,
8
+ EventDrivenL2BlockStream,
9
9
  type L2Block,
10
- type L2BlockId,
11
10
  type L2BlockSource,
12
- L2BlockStream,
13
11
  type L2BlockStreamEvent,
14
12
  type L2BlockStreamEventHandler,
15
13
  type L2BlockStreamLocalDataProvider,
16
- type L2Tips,
14
+ type LocalChainTips,
17
15
  } from '@aztec/stdlib/block';
18
16
  import {
19
17
  WorldStateRunningState,
@@ -50,7 +48,7 @@ export class ServerWorldStateSynchronizer
50
48
  private currentState: WorldStateRunningState = WorldStateRunningState.IDLE;
51
49
 
52
50
  private syncPromise = promiseWithResolvers<void>();
53
- protected blockStream: L2BlockStream | undefined;
51
+ protected blockStream: EventDrivenL2BlockStream | undefined;
54
52
 
55
53
  // WorldState doesn't track the proven block number, it only tracks the latest tips of the pending chain and the finalized chain
56
54
  // store the proven block number here, in the synchronizer, so that we don't end up spamming the logs with 'chain-proved' events
@@ -64,7 +62,7 @@ export class ServerWorldStateSynchronizer
64
62
  private readonly log: Logger = createLogger('world_state'),
65
63
  ) {
66
64
  this.merkleTreeCommitted = this.merkleTreeDb.getCommitted();
67
- this.historyToKeep = config.worldStateBlockHistory < 1 ? undefined : config.worldStateBlockHistory;
65
+ this.historyToKeep = config.worldStateCheckpointHistory < 1 ? undefined : config.worldStateCheckpointHistory;
68
66
  this.log.info(
69
67
  `Created world state synchroniser with block history of ${
70
68
  this.historyToKeep === undefined ? 'infinity' : this.historyToKeep
@@ -80,6 +78,46 @@ export class ServerWorldStateSynchronizer
80
78
  return this.merkleTreeDb.getSnapshot(blockNumber);
81
79
  }
82
80
 
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
+
83
121
  public fork(blockNumber?: BlockNumber, opts?: { closeDelayMs?: number }): Promise<MerkleTreeWriteOperations> {
84
122
  return this.merkleTreeDb.fork(blockNumber, opts);
85
123
  }
@@ -122,9 +160,9 @@ export class ServerWorldStateSynchronizer
122
160
  return this.syncPromise.promise;
123
161
  }
124
162
 
125
- protected createBlockStream(): L2BlockStream {
163
+ protected createBlockStream(): EventDrivenL2BlockStream {
126
164
  const logger = createLogger('world-state:block_stream');
127
- return new L2BlockStream(this.l2BlockSource, this, this, logger, {
165
+ return new EventDrivenL2BlockStream(this.l2BlockSource, this, this, logger, {
128
166
  pollIntervalMS: this.config.worldStateBlockCheckIntervalMS,
129
167
  batchSize: this.config.worldStateBlockRequestBatchSize,
130
168
  ignoreCheckpoints: true,
@@ -177,13 +215,10 @@ export class ServerWorldStateSynchronizer
177
215
  /**
178
216
  * Forces an immediate sync.
179
217
  * @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.
218
+ * @param blockHash - If provided, verifies the block at targetBlockNumber matches this hash. On mismatch, triggers a resync (reorg detection).
181
219
  * @returns A promise that resolves with the block number the world state was synced to
182
220
  */
183
- public async syncImmediate(
184
- targetBlockNumber?: BlockNumber,
185
- skipThrowIfTargetNotReached?: boolean,
186
- ): Promise<BlockNumber> {
221
+ public async syncImmediate(targetBlockNumber?: BlockNumber, blockHash?: BlockHash): Promise<BlockNumber> {
187
222
  if (this.currentState !== WorldStateRunningState.RUNNING) {
188
223
  throw new Error(`World State is not running. Unable to perform sync.`);
189
224
  }
@@ -195,7 +230,19 @@ export class ServerWorldStateSynchronizer
195
230
  // If we have been given a block number to sync to and we have reached that number then return
196
231
  const currentBlockNumber = await this.getLatestBlockNumber();
197
232
  if (targetBlockNumber !== undefined && targetBlockNumber <= currentBlockNumber) {
198
- 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
+ );
199
246
  }
200
247
  this.log.debug(`World State at ${currentBlockNumber} told to sync to ${targetBlockNumber ?? 'latest'}`);
201
248
 
@@ -213,7 +260,7 @@ export class ServerWorldStateSynchronizer
213
260
 
214
261
  // If we have been given a block number to sync to and we have not reached that number then fail
215
262
  const updatedBlockNumber = await this.getLatestBlockNumber();
216
- if (!skipThrowIfTargetNotReached && targetBlockNumber !== undefined && targetBlockNumber > updatedBlockNumber) {
263
+ if (targetBlockNumber !== undefined && targetBlockNumber > updatedBlockNumber) {
217
264
  throw new WorldStateSynchronizerError(
218
265
  `Unable to sync to block number ${targetBlockNumber} (last synced is ${updatedBlockNumber})`,
219
266
  {
@@ -227,6 +274,24 @@ export class ServerWorldStateSynchronizer
227
274
  );
228
275
  }
229
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
+
230
295
  return updatedBlockNumber;
231
296
  }
232
297
 
@@ -238,8 +303,12 @@ export class ServerWorldStateSynchronizer
238
303
  return this.merkleTreeCommitted.getLeafValue(MerkleTreeId.ARCHIVE, BigInt(number)).then(leaf => leaf?.toString());
239
304
  }
240
305
 
241
- /** Returns the latest L2 block number for each tip of the chain (latest, proven, finalized). */
242
- 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> {
243
312
  const status = await this.merkleTreeDb.getStatusSummary();
244
313
  const unfinalizedBlockHashPromise = this.getL2BlockHash(status.unfinalizedBlockNumber);
245
314
  const finalizedBlockHashPromise = this.getL2BlockHash(status.finalizedBlockNumber);
@@ -253,25 +322,13 @@ export class ServerWorldStateSynchronizer
253
322
  finalizedBlockHashPromise,
254
323
  provenBlockHashPromise,
255
324
  ]);
256
- const latestBlockId: L2BlockId = { number: status.unfinalizedBlockNumber, hash: unfinalizedBlockHash! };
257
-
258
- // World state doesn't track checkpointed blocks or checkpoints themselves.
259
- // but we use a block stream so we need to provide 'local' L2Tips.
260
- // We configure the block stream to ignore checkpoints and set checkpoint values to genesis here.
261
- const genesisCheckpointHeaderHash = GENESIS_CHECKPOINT_HEADER_HASH.toString();
262
325
  return {
263
- proposed: latestBlockId,
264
- checkpointed: {
265
- block: { number: INITIAL_L2_BLOCK_NUM, hash: GENESIS_BLOCK_HEADER_HASH.toString() },
266
- checkpoint: { number: INITIAL_L2_CHECKPOINT_NUM, hash: genesisCheckpointHeaderHash },
267
- },
326
+ proposed: { number: status.unfinalizedBlockNumber, hash: unfinalizedBlockHash },
268
327
  finalized: {
269
- block: { number: status.finalizedBlockNumber, hash: finalizedBlockHash ?? '' },
270
- checkpoint: { number: INITIAL_L2_CHECKPOINT_NUM, hash: genesisCheckpointHeaderHash },
328
+ block: { number: status.finalizedBlockNumber, hash: finalizedBlockHash },
271
329
  },
272
330
  proven: {
273
- block: { number: provenBlockNumber, hash: provenBlockHash ?? '' },
274
- checkpoint: { number: INITIAL_L2_CHECKPOINT_NUM, hash: genesisCheckpointHeaderHash },
331
+ block: { number: provenBlockNumber, hash: provenBlockHash },
275
332
  },
276
333
  };
277
334
  }
@@ -291,6 +348,15 @@ export class ServerWorldStateSynchronizer
291
348
  case 'chain-finalized':
292
349
  await this.handleChainFinalized(event.block.number);
293
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
+ }
294
360
  }
295
361
  }
296
362
 
@@ -360,16 +426,56 @@ export class ServerWorldStateSynchronizer
360
426
 
361
427
  private async handleChainFinalized(blockNumber: BlockNumber) {
362
428
  this.log.verbose(`Finalized chain is now at block ${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
+ }
363
441
  const summary = await this.merkleTreeDb.setFinalized(blockNumber);
364
442
  if (this.historyToKeep === undefined) {
365
443
  return;
366
444
  }
367
- const newHistoricBlock = summary.finalizedBlockNumber - this.historyToKeep + 1;
368
- 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!`);
369
467
  return;
370
468
  }
371
- this.log.verbose(`Pruning historic blocks to ${newHistoricBlock}`);
372
- const status = await this.merkleTreeDb.removeHistoricalBlocks(BlockNumber(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));
373
479
  this.log.debug(`World state summary `, status.summary);
374
480
  }
375
481
 
@@ -380,9 +486,11 @@ export class ServerWorldStateSynchronizer
380
486
  }
381
487
 
382
488
  private async handleChainPruned(blockNumber: BlockNumber) {
383
- this.log.warn(`Chain pruned to block ${blockNumber}`);
489
+ this.log.info(`Chain pruned to block ${blockNumber}`);
384
490
  const status = await this.merkleTreeDb.unwindBlocks(blockNumber);
385
- this.provenBlockNumber = undefined;
491
+ if (this.provenBlockNumber !== undefined && this.provenBlockNumber > blockNumber) {
492
+ this.provenBlockNumber = undefined;
493
+ }
386
494
  this.instrumentation.updateWorldStateMetrics(status);
387
495
  }
388
496
 
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);
package/src/testing.ts CHANGED
@@ -3,22 +3,24 @@ import { Fr } from '@aztec/foundation/curves/bn254';
3
3
  import { computeFeePayerBalanceLeafSlot } from '@aztec/protocol-contracts/fee-juice';
4
4
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
5
5
  import { MerkleTreeId, PublicDataTreeLeaf } from '@aztec/stdlib/trees';
6
+ import type { GenesisData } from '@aztec/stdlib/world-state';
6
7
 
7
8
  import { NativeWorldStateService } from './native/index.js';
8
9
 
9
- async function generateGenesisValues(prefilledPublicData: PublicDataTreeLeaf[]) {
10
- if (!prefilledPublicData.length) {
10
+ async function generateGenesisValues(genesis: GenesisData) {
11
+ // The GENESIS_ARCHIVE_ROOT constant reflects the canonical empty genesis (no public data, no prefilled nullifiers,
12
+ // timestamp 0), so we can only short-circuit when this genesis adds none of those on top.
13
+ if (!genesis.prefilledPublicData.length && genesis.genesisTimestamp === 0n && !genesis.prefilledNullifiers?.length) {
11
14
  return {
12
15
  genesisArchiveRoot: new Fr(GENESIS_ARCHIVE_ROOT),
13
16
  };
14
17
  }
15
18
 
16
- // Create a temporary world state to compute the genesis values.
17
- const ws = await NativeWorldStateService.tmp(
18
- undefined /* rollupAddress */,
19
- true /* cleanupTmpDir */,
20
- prefilledPublicData,
21
- );
19
+ // Compute the genesis values on a throwaway world state. The archive root derives deterministically from the
20
+ // prefilled public data, the prefilled nullifiers, and the genesis timestamp, so the fsync-off ephemeral store (no
21
+ // version manager, no crash-recoverability) produces an identical root while skipping the fsync overhead that `tmp`
22
+ // pays. close() removes the tmpdir.
23
+ const ws = await NativeWorldStateService.ephemeral(genesis);
22
24
  const genesisArchiveRoot = new Fr((await ws.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
23
25
  await ws.close();
24
26
 
@@ -33,6 +35,8 @@ export async function getGenesisValues(
33
35
  initialAccounts: AztecAddress[],
34
36
  initialAccountFeeJuice = defaultInitialAccountFeeJuice,
35
37
  genesisPublicData: PublicDataTreeLeaf[] = [],
38
+ genesisTimestamp: bigint = 0n,
39
+ prefilledNullifiers: Fr[] = [],
36
40
  ) {
37
41
  // Top up the accounts with fee juice.
38
42
  let prefilledPublicData = await Promise.all(
@@ -46,11 +50,16 @@ export async function getGenesisValues(
46
50
 
47
51
  prefilledPublicData.sort((a, b) => (b.slot.lt(a.slot) ? 1 : -1));
48
52
 
49
- const { genesisArchiveRoot } = await generateGenesisValues(prefilledPublicData);
53
+ // The indexed nullifier tree requires its prefilled leaves to be unique and strictly increasing, so sort ascending
54
+ // here (a copy, to avoid mutating the caller's array) rather than relying on the caller's ordering.
55
+ const sortedNullifiers = [...prefilledNullifiers].sort((a, b) => (a.toBigInt() < b.toBigInt() ? -1 : 1));
56
+
57
+ const genesis: GenesisData = { prefilledPublicData, prefilledNullifiers: sortedNullifiers, genesisTimestamp };
58
+ const { genesisArchiveRoot } = await generateGenesisValues(genesis);
50
59
 
51
60
  return {
52
61
  genesisArchiveRoot,
53
- prefilledPublicData,
62
+ genesis,
54
63
  fundingNeeded: BigInt(initialAccounts.length) * initialAccountFeeJuice.toBigInt(),
55
64
  };
56
65
  }
@@ -1,14 +1,12 @@
1
1
  import { MAX_NULLIFIERS_PER_TX, MAX_TOTAL_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX } from '@aztec/constants';
2
2
  import type { BlockNumber } from '@aztec/foundation/branded-types';
3
3
  import type { Fr } from '@aztec/foundation/curves/bn254';
4
- import type { IndexedTreeSnapshot, TreeSnapshot } from '@aztec/merkle-tree';
5
4
  import type { L2Block } from '@aztec/stdlib/block';
6
5
  import type {
7
6
  ForkMerkleTreeOperations,
8
7
  MerkleTreeReadOperations,
9
8
  ReadonlyWorldStateAccess,
10
9
  } from '@aztec/stdlib/interfaces/server';
11
- import type { MerkleTreeId } from '@aztec/stdlib/trees';
12
10
 
13
11
  import type { WorldStateStatusFull, WorldStateStatusSummary } from '../native/message.js';
14
12
 
@@ -31,14 +29,6 @@ export const INITIAL_NULLIFIER_TREE_SIZE = 2 * MAX_NULLIFIERS_PER_TX;
31
29
 
32
30
  export const INITIAL_PUBLIC_DATA_TREE_SIZE = 2 * MAX_TOTAL_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX;
33
31
 
34
- export type TreeSnapshots = {
35
- [MerkleTreeId.NULLIFIER_TREE]: IndexedTreeSnapshot;
36
- [MerkleTreeId.NOTE_HASH_TREE]: TreeSnapshot<Fr>;
37
- [MerkleTreeId.PUBLIC_DATA_TREE]: IndexedTreeSnapshot;
38
- [MerkleTreeId.L1_TO_L2_MESSAGE_TREE]: TreeSnapshot<Fr>;
39
- [MerkleTreeId.ARCHIVE]: TreeSnapshot<Fr>;
40
- };
41
-
42
32
  export interface MerkleTreeAdminDatabase extends ForkMerkleTreeOperations, ReadonlyWorldStateAccess {
43
33
  /**
44
34
  * Handles a single L2 block (i.e. Inserts the new note hashes into the merkle tree).