@aztec/world-state 0.0.1-commit.9ef841308 → 0.0.1-commit.a5db02d

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 (50) hide show
  1. package/dest/native/ipc_world_state_instance.d.ts +50 -0
  2. package/dest/native/ipc_world_state_instance.d.ts.map +1 -0
  3. package/dest/native/ipc_world_state_instance.js +635 -0
  4. package/dest/native/merkle_trees_facade.d.ts +5 -2
  5. package/dest/native/merkle_trees_facade.d.ts.map +1 -1
  6. package/dest/native/merkle_trees_facade.js +24 -7
  7. package/dest/native/message.d.ts +6 -3
  8. package/dest/native/message.d.ts.map +1 -1
  9. package/dest/native/native_world_state.d.ts +31 -5
  10. package/dest/native/native_world_state.d.ts.map +1 -1
  11. package/dest/native/native_world_state.js +83 -27
  12. package/dest/native/native_world_state_instance.d.ts +5 -4
  13. package/dest/native/native_world_state_instance.d.ts.map +1 -1
  14. package/dest/native/native_world_state_instance.js +45 -25
  15. package/dest/native/world_state_ops_queue.js +5 -5
  16. package/dest/synchronizer/config.d.ts +1 -1
  17. package/dest/synchronizer/config.d.ts.map +1 -1
  18. package/dest/synchronizer/config.js +9 -10
  19. package/dest/synchronizer/errors.d.ts +8 -1
  20. package/dest/synchronizer/errors.d.ts.map +1 -1
  21. package/dest/synchronizer/errors.js +8 -1
  22. package/dest/synchronizer/factory.d.ts +4 -4
  23. package/dest/synchronizer/factory.d.ts.map +1 -1
  24. package/dest/synchronizer/factory.js +7 -6
  25. package/dest/synchronizer/index.d.ts +2 -1
  26. package/dest/synchronizer/index.d.ts.map +1 -1
  27. package/dest/synchronizer/index.js +1 -0
  28. package/dest/synchronizer/server_world_state_synchronizer.d.ts +11 -6
  29. package/dest/synchronizer/server_world_state_synchronizer.d.ts.map +1 -1
  30. package/dest/synchronizer/server_world_state_synchronizer.js +64 -39
  31. package/dest/test/utils.d.ts +1 -1
  32. package/dest/test/utils.d.ts.map +1 -1
  33. package/dest/test/utils.js +6 -1
  34. package/dest/testing.d.ts +4 -3
  35. package/dest/testing.d.ts.map +1 -1
  36. package/dest/testing.js +22 -7
  37. package/package.json +12 -10
  38. package/src/native/ipc_world_state_instance.ts +725 -0
  39. package/src/native/merkle_trees_facade.ts +27 -9
  40. package/src/native/message.ts +5 -2
  41. package/src/native/native_world_state.ts +101 -34
  42. package/src/native/native_world_state_instance.ts +55 -28
  43. package/src/native/world_state_ops_queue.ts +5 -5
  44. package/src/synchronizer/config.ts +14 -10
  45. package/src/synchronizer/errors.ts +8 -0
  46. package/src/synchronizer/factory.ts +12 -10
  47. package/src/synchronizer/index.ts +1 -0
  48. package/src/synchronizer/server_world_state_synchronizer.ts +70 -35
  49. package/src/test/utils.ts +10 -1
  50. package/src/testing.ts +19 -10
@@ -2,7 +2,7 @@ import type { LoggerBindings } from '@aztec/foundation/log';
2
2
  import type { L2BlockSource } from '@aztec/stdlib/block';
3
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,4 +1,3 @@
1
- import { GENESIS_BLOCK_HEADER_HASH, INITIAL_CHECKPOINT_NUMBER, INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
2
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';
@@ -6,15 +5,13 @@ import { promiseWithResolvers } from '@aztec/foundation/promise';
6
5
  import { elapsed } from '@aztec/foundation/timer';
7
6
  import {
8
7
  type BlockHash,
9
- GENESIS_CHECKPOINT_HEADER_HASH,
8
+ EventDrivenL2BlockStream,
10
9
  type L2Block,
11
- type L2BlockId,
12
10
  type L2BlockSource,
13
- L2BlockStream,
14
11
  type L2BlockStreamEvent,
15
12
  type L2BlockStreamEventHandler,
16
13
  type L2BlockStreamLocalDataProvider,
17
- type L2Tips,
14
+ type LocalChainTips,
18
15
  } from '@aztec/stdlib/block';
19
16
  import {
20
17
  WorldStateRunningState,
@@ -51,7 +48,7 @@ export class ServerWorldStateSynchronizer
51
48
  private currentState: WorldStateRunningState = WorldStateRunningState.IDLE;
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
@@ -81,6 +78,46 @@ export class ServerWorldStateSynchronizer
81
78
  return this.merkleTreeDb.getSnapshot(blockNumber);
82
79
  }
83
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
+
84
121
  public fork(blockNumber?: BlockNumber, opts?: { closeDelayMs?: number }): Promise<MerkleTreeWriteOperations> {
85
122
  return this.merkleTreeDb.fork(blockNumber, opts);
86
123
  }
@@ -123,9 +160,9 @@ export class ServerWorldStateSynchronizer
123
160
  return this.syncPromise.promise;
124
161
  }
125
162
 
126
- protected createBlockStream(): L2BlockStream {
163
+ protected createBlockStream(): EventDrivenL2BlockStream {
127
164
  const logger = createLogger('world-state:block_stream');
128
- return new L2BlockStream(this.l2BlockSource, this, this, logger, {
165
+ return new EventDrivenL2BlockStream(this.l2BlockSource, this, this, logger, {
129
166
  pollIntervalMS: this.config.worldStateBlockCheckIntervalMS,
130
167
  batchSize: this.config.worldStateBlockRequestBatchSize,
131
168
  ignoreCheckpoints: true,
@@ -266,8 +303,12 @@ export class ServerWorldStateSynchronizer
266
303
  return this.merkleTreeCommitted.getLeafValue(MerkleTreeId.ARCHIVE, BigInt(number)).then(leaf => leaf?.toString());
267
304
  }
268
305
 
269
- /** Returns the latest L2 block number for each tip of the chain (latest, proven, finalized). */
270
- 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> {
271
312
  const status = await this.merkleTreeDb.getStatusSummary();
272
313
  const unfinalizedBlockHashPromise = this.getL2BlockHash(status.unfinalizedBlockNumber);
273
314
  const finalizedBlockHashPromise = this.getL2BlockHash(status.finalizedBlockNumber);
@@ -281,25 +322,13 @@ export class ServerWorldStateSynchronizer
281
322
  finalizedBlockHashPromise,
282
323
  provenBlockHashPromise,
283
324
  ]);
284
- const latestBlockId: L2BlockId = { number: status.unfinalizedBlockNumber, hash: unfinalizedBlockHash! };
285
-
286
- // World state doesn't track checkpointed blocks or checkpoints themselves.
287
- // but we use a block stream so we need to provide 'local' L2Tips.
288
- // We configure the block stream to ignore checkpoints and set checkpoint values to genesis here.
289
- const genesisCheckpointHeaderHash = GENESIS_CHECKPOINT_HEADER_HASH.toString();
290
325
  return {
291
- proposed: latestBlockId,
292
- checkpointed: {
293
- block: { number: INITIAL_L2_BLOCK_NUM, hash: GENESIS_BLOCK_HEADER_HASH.toString() },
294
- checkpoint: { number: INITIAL_CHECKPOINT_NUMBER, hash: genesisCheckpointHeaderHash },
295
- },
326
+ proposed: { number: status.unfinalizedBlockNumber, hash: unfinalizedBlockHash },
296
327
  finalized: {
297
- block: { number: status.finalizedBlockNumber, hash: finalizedBlockHash ?? '' },
298
- checkpoint: { number: INITIAL_CHECKPOINT_NUMBER, hash: genesisCheckpointHeaderHash },
328
+ block: { number: status.finalizedBlockNumber, hash: finalizedBlockHash },
299
329
  },
300
330
  proven: {
301
- block: { number: provenBlockNumber, hash: provenBlockHash ?? '' },
302
- checkpoint: { number: INITIAL_CHECKPOINT_NUMBER, hash: genesisCheckpointHeaderHash },
331
+ block: { number: provenBlockNumber, hash: provenBlockHash },
303
332
  },
304
333
  };
305
334
  }
@@ -319,6 +348,15 @@ export class ServerWorldStateSynchronizer
319
348
  case 'chain-finalized':
320
349
  await this.handleChainFinalized(event.block.number);
321
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
+ }
322
360
  }
323
361
  }
324
362
 
@@ -404,29 +442,26 @@ export class ServerWorldStateSynchronizer
404
442
  if (this.historyToKeep === undefined) {
405
443
  return;
406
444
  }
407
- // Get the checkpointed block for the finalized block number
408
- const finalisedCheckpoint = await this.l2BlockSource.getCheckpointedBlock(summary.finalizedBlockNumber);
409
- if (finalisedCheckpoint === undefined) {
445
+ const finalisedBlockData = await this.l2BlockSource.getBlockData({ number: summary.finalizedBlockNumber });
446
+ if (finalisedBlockData === undefined) {
410
447
  this.log.warn(
411
448
  `Failed to retrieve checkpointed block for finalized block number: ${summary.finalizedBlockNumber}`,
412
449
  );
413
450
  return;
414
451
  }
415
452
  // Compute the required historic checkpoint number
416
- const newHistoricCheckpointNumber = finalisedCheckpoint.checkpointNumber - this.historyToKeep + 1;
453
+ const newHistoricCheckpointNumber = finalisedBlockData.checkpointNumber - this.historyToKeep + 1;
417
454
  if (newHistoricCheckpointNumber <= 1) {
418
455
  return;
419
456
  }
420
457
  // Retrieve the historic checkpoint
421
- const historicCheckpoints = await this.l2BlockSource.getCheckpoints(
422
- CheckpointNumber(newHistoricCheckpointNumber),
423
- 1,
424
- );
425
- if (historicCheckpoints.length === 0 || historicCheckpoints[0] === undefined) {
458
+ const historicCheckpoint = await this.l2BlockSource.getCheckpoint({
459
+ number: CheckpointNumber(newHistoricCheckpointNumber),
460
+ });
461
+ if (!historicCheckpoint) {
426
462
  this.log.warn(`Failed to retrieve checkpoint number ${newHistoricCheckpointNumber} from Archiver`);
427
463
  return;
428
464
  }
429
- const historicCheckpoint = historicCheckpoints[0];
430
465
  if (historicCheckpoint.checkpoint.blocks.length === 0 || historicCheckpoint.checkpoint.blocks[0] === undefined) {
431
466
  this.log.warn(`Retrieved checkpoint number ${newHistoricCheckpointNumber} has no blocks!`);
432
467
  return;
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
  }