@aztec/world-state 0.0.1-commit.993d240 → 0.0.1-commit.9a89641

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 (65) 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 +4 -9
  7. package/dest/native/index.d.ts +2 -1
  8. package/dest/native/index.d.ts.map +1 -1
  9. package/dest/native/index.js +1 -0
  10. package/dest/native/ipc_world_state_instance.d.ts +61 -22
  11. package/dest/native/ipc_world_state_instance.d.ts.map +1 -1
  12. package/dest/native/ipc_world_state_instance.js +530 -488
  13. package/dest/native/merkle_trees_facade.d.ts +4 -3
  14. package/dest/native/merkle_trees_facade.d.ts.map +1 -1
  15. package/dest/native/merkle_trees_facade.js +37 -109
  16. package/dest/native/message.d.ts +14 -229
  17. package/dest/native/message.d.ts.map +1 -1
  18. package/dest/native/message.js +0 -42
  19. package/dest/native/native_world_state.d.ts +14 -28
  20. package/dest/native/native_world_state.d.ts.map +1 -1
  21. package/dest/native/native_world_state.js +108 -99
  22. package/dest/native/native_world_state_instance.d.ts +56 -43
  23. package/dest/native/native_world_state_instance.d.ts.map +1 -1
  24. package/dest/native/native_world_state_instance.js +1 -210
  25. package/dest/native/world_state_operation.d.ts +7 -0
  26. package/dest/native/world_state_operation.d.ts.map +1 -0
  27. package/dest/native/world_state_operation.js +5 -0
  28. package/dest/synchronizer/errors.d.ts +8 -1
  29. package/dest/synchronizer/errors.d.ts.map +1 -1
  30. package/dest/synchronizer/errors.js +8 -1
  31. package/dest/synchronizer/factory.d.ts +1 -1
  32. package/dest/synchronizer/factory.d.ts.map +1 -1
  33. package/dest/synchronizer/factory.js +1 -1
  34. package/dest/synchronizer/index.d.ts +2 -1
  35. package/dest/synchronizer/index.d.ts.map +1 -1
  36. package/dest/synchronizer/index.js +1 -0
  37. package/dest/synchronizer/server_world_state_synchronizer.d.ts +11 -6
  38. package/dest/synchronizer/server_world_state_synchronizer.d.ts.map +1 -1
  39. package/dest/synchronizer/server_world_state_synchronizer.js +55 -43
  40. package/dest/test/utils.d.ts +1 -1
  41. package/dest/test/utils.d.ts.map +1 -1
  42. package/dest/test/utils.js +6 -1
  43. package/dest/testing.d.ts +2 -2
  44. package/dest/testing.d.ts.map +1 -1
  45. package/dest/testing.js +15 -4
  46. package/package.json +11 -10
  47. package/src/index.ts +1 -0
  48. package/src/instrumentation/instrumentation.ts +6 -18
  49. package/src/native/index.ts +1 -0
  50. package/src/native/ipc_world_state_instance.ts +625 -495
  51. package/src/native/merkle_trees_facade.ts +59 -104
  52. package/src/native/message.ts +13 -296
  53. package/src/native/native_world_state.ts +136 -153
  54. package/src/native/native_world_state_instance.ts +94 -291
  55. package/src/native/world_state_operation.ts +33 -0
  56. package/src/synchronizer/errors.ts +8 -0
  57. package/src/synchronizer/factory.ts +0 -1
  58. package/src/synchronizer/index.ts +1 -0
  59. package/src/synchronizer/server_world_state_synchronizer.ts +63 -30
  60. package/src/test/utils.ts +10 -1
  61. package/src/testing.ts +14 -4
  62. package/dest/native/world_state_ops_queue.d.ts +0 -19
  63. package/dest/native/world_state_ops_queue.d.ts.map +0 -1
  64. package/dest/native/world_state_ops_queue.js +0 -146
  65. package/src/native/world_state_ops_queue.ts +0 -190
@@ -1,301 +1,104 @@
1
- import {
2
- ARCHIVE_HEIGHT,
3
- DomainSeparator,
4
- L1_TO_L2_MSG_TREE_HEIGHT,
5
- MAX_NULLIFIERS_PER_TX,
6
- MAX_TOTAL_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX,
7
- NOTE_HASH_TREE_HEIGHT,
8
- NULLIFIER_TREE_HEIGHT,
9
- PUBLIC_DATA_TREE_HEIGHT,
10
- } from '@aztec/constants';
11
- import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
12
- import { NativeWorldState as BaseNativeWorldState, MsgpackChannel } from '@aztec/native';
13
- import { MerkleTreeId } from '@aztec/stdlib/trees';
14
- import { EMPTY_GENESIS_DATA, type GenesisData } from '@aztec/stdlib/world-state';
15
-
16
- import assert from 'assert';
17
- import { cpus } from 'os';
18
-
19
- import type { WorldStateInstrumentation } from '../instrumentation/instrumentation.js';
20
- import type { WorldStateTreeMapSizes } from '../synchronizer/factory.js';
21
- import {
22
- WorldStateMessageType,
23
- type WorldStateRequest,
24
- type WorldStateRequestCategories,
25
- type WorldStateResponse,
26
- isWithCanonical,
27
- isWithForkId,
28
- isWithRevision,
1
+ import type { BlockNumber } from '@aztec/foundation/branded-types';
2
+ import type { Fr } from '@aztec/foundation/curves/bn254';
3
+ import type { IndexedTreeId, TreeInfo } from '@aztec/stdlib/interfaces/server';
4
+ import type { MerkleTreeId } from '@aztec/stdlib/trees';
5
+ import type { WorldStateRevision } from '@aztec/stdlib/world-state';
6
+
7
+ import type {
8
+ BlockStateReference,
9
+ SerializedBatchInsertionResult,
10
+ SerializedIndexedLeaf,
11
+ SerializedLeafValue,
12
+ SerializedSequentialInsertionResult,
13
+ SerializedSiblingPathAndIndex,
14
+ TreeStateReference,
15
+ WorldStateStatusFull,
16
+ WorldStateStatusSummary,
29
17
  } from './message.js';
30
- import { WorldStateOpsQueue } from './world_state_ops_queue.js';
31
-
32
- const MAX_WORLD_STATE_THREADS = +(process.env.HARDWARE_CONCURRENCY || '16');
33
18
 
19
+ /** Backend-agnostic handle to a running aztec-wsdb world state, accessed by the TS layer. */
34
20
  export interface NativeWorldStateInstance {
35
- call<T extends WorldStateMessageType>(
36
- messageType: T,
37
- body: WorldStateRequest[T] & WorldStateRequestCategories,
38
- ): Promise<WorldStateResponse[T]>;
39
- // TODO(dbanks12): this returns any type, but we should strongly type it
40
- getHandle(): any;
41
- }
42
-
43
- /**
44
- * Strongly-typed interface to access the WorldState class in the native world_state_napi module.
45
- */
46
- export class NativeWorldState implements NativeWorldStateInstance {
47
- private open = true;
48
-
49
- // We maintain a map of queue to fork
50
- private queues = new Map<number, WorldStateOpsQueue>();
51
-
52
- private instance: MsgpackChannel<WorldStateMessageType, WorldStateRequest, WorldStateResponse>;
53
-
54
- /** Creates a new native WorldState instance */
55
- constructor(
56
- private readonly dataDir: string,
57
- private readonly wsTreeMapSizes: WorldStateTreeMapSizes,
58
- private readonly genesis: GenesisData = EMPTY_GENESIS_DATA,
59
- private readonly instrumentation: WorldStateInstrumentation,
60
- bindings?: LoggerBindings,
61
- private readonly log: Logger = createLogger('world-state:database', bindings),
62
- private readonly ephemeral: boolean = false,
63
- ) {
64
- const threads = Math.min(cpus().length, MAX_WORLD_STATE_THREADS);
65
- log.info(
66
- `Creating world state data store at directory ${dataDir} with map sizes ${JSON.stringify(
67
- wsTreeMapSizes,
68
- )} and ${threads} threads (ephemeral=${ephemeral}).`,
69
- );
70
- const prefilledPublicDataBufferArray = genesis.prefilledPublicData.map(d => [
71
- d.slot.toBuffer(),
72
- d.value.toBuffer(),
73
- ]);
74
- const ws = new BaseNativeWorldState(
75
- dataDir,
76
- {
77
- [MerkleTreeId.NULLIFIER_TREE]: NULLIFIER_TREE_HEIGHT,
78
- [MerkleTreeId.NOTE_HASH_TREE]: NOTE_HASH_TREE_HEIGHT,
79
- [MerkleTreeId.PUBLIC_DATA_TREE]: PUBLIC_DATA_TREE_HEIGHT,
80
- [MerkleTreeId.L1_TO_L2_MESSAGE_TREE]: L1_TO_L2_MSG_TREE_HEIGHT,
81
- [MerkleTreeId.ARCHIVE]: ARCHIVE_HEIGHT,
82
- },
83
- {
84
- [MerkleTreeId.NULLIFIER_TREE]: 2 * MAX_NULLIFIERS_PER_TX,
85
- [MerkleTreeId.PUBLIC_DATA_TREE]: 2 * MAX_TOTAL_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX,
86
- },
87
- prefilledPublicDataBufferArray,
88
- DomainSeparator.BLOCK_HEADER_HASH,
89
- Number(genesis.genesisTimestamp),
90
- {
91
- [MerkleTreeId.NULLIFIER_TREE]: wsTreeMapSizes.nullifierTreeMapSizeKb,
92
- [MerkleTreeId.NOTE_HASH_TREE]: wsTreeMapSizes.noteHashTreeMapSizeKb,
93
- [MerkleTreeId.PUBLIC_DATA_TREE]: wsTreeMapSizes.publicDataTreeMapSizeKb,
94
- [MerkleTreeId.L1_TO_L2_MESSAGE_TREE]: wsTreeMapSizes.messageTreeMapSizeKb,
95
- [MerkleTreeId.ARCHIVE]: wsTreeMapSizes.archiveTreeMapSizeKb,
96
- },
97
- threads,
98
- ephemeral,
99
- );
100
- this.instance = new MsgpackChannel(ws);
101
- // Manually create the queue for the canonical fork
102
- this.queues.set(0, new WorldStateOpsQueue());
103
- }
104
-
105
- public getDataDir() {
106
- return this.dataDir;
107
- }
108
-
109
- public clone() {
110
- return new NativeWorldState(
111
- this.dataDir,
112
- this.wsTreeMapSizes,
113
- this.genesis,
114
- this.instrumentation,
115
- this.log.getBindings(),
116
- this.log,
117
- this.ephemeral,
118
- );
119
- }
120
-
121
- /**
122
- * Gets the native WorldState handle from the underlying native instance.
123
- * We call the getHandle() method on the native WorldState to get a NAPI External
124
- * that wraps the underlying C++ WorldState pointer.
125
- * @returns The NAPI External handle to the native WorldState instance,since
126
- * the NAPI external type is opaque, we return any (we could also use an opaque symbol type)
127
- */
128
- public getHandle(): any {
129
- const worldStateWrapper = (this.instance as any).dest;
130
-
131
- if (!worldStateWrapper) {
132
- throw new Error('No WorldStateWrapper found');
133
- }
134
-
135
- if (typeof worldStateWrapper.getHandle !== 'function') {
136
- throw new Error('WorldStateWrapper does not have getHandle method');
137
- }
138
-
139
- // Call getHandle() to get the NAPI External
140
- try {
141
- return worldStateWrapper.getHandle();
142
- } catch (error) {
143
- this.log.error('Failed to get native WorldState handle', error);
144
- }
145
- }
21
+ getTreeInfo(treeId: MerkleTreeId, revision: WorldStateRevision): Promise<TreeInfo>;
22
+ getStateReference(revision: WorldStateRevision): Promise<Record<number, TreeStateReference>>;
23
+ getInitialStateReference(): Promise<Record<number, TreeStateReference>>;
24
+ getLeafValue(
25
+ treeId: MerkleTreeId,
26
+ revision: WorldStateRevision,
27
+ leafIndex: bigint,
28
+ ): Promise<SerializedLeafValue | undefined>;
29
+ getLeafPreimage(
30
+ treeId: IndexedTreeId,
31
+ revision: WorldStateRevision,
32
+ leafIndex: bigint,
33
+ ): Promise<SerializedIndexedLeaf | undefined>;
34
+ getSiblingPath(treeId: MerkleTreeId, revision: WorldStateRevision, leafIndex: bigint): Promise<Buffer[]>;
35
+ getBlockNumbersForLeafIndices(
36
+ treeId: MerkleTreeId,
37
+ revision: WorldStateRevision,
38
+ leafIndices: bigint[],
39
+ ): Promise<(bigint | undefined)[]>;
40
+ findLeafIndices(
41
+ treeId: MerkleTreeId,
42
+ revision: WorldStateRevision,
43
+ leaves: SerializedLeafValue[],
44
+ startIndex: bigint,
45
+ ): Promise<(bigint | undefined)[]>;
46
+ findLowLeaf(
47
+ treeId: IndexedTreeId,
48
+ revision: WorldStateRevision,
49
+ key: Fr,
50
+ ): Promise<{ index: bigint; alreadyPresent: boolean }>;
51
+ findSiblingPaths(
52
+ treeId: MerkleTreeId,
53
+ revision: WorldStateRevision,
54
+ leaves: SerializedLeafValue[],
55
+ ): Promise<(SerializedSiblingPathAndIndex | undefined)[]>;
56
+ updateArchive(forkId: number, blockStateRef: BlockStateReference, blockHeaderHash: Buffer): Promise<void>;
57
+ appendLeaves(treeId: MerkleTreeId, forkId: number, leaves: SerializedLeafValue[]): Promise<void>;
58
+ batchInsert(
59
+ treeId: IndexedTreeId,
60
+ forkId: number,
61
+ leaves: SerializedLeafValue[],
62
+ subtreeDepth: number,
63
+ ): Promise<SerializedBatchInsertionResult>;
64
+ sequentialInsert(
65
+ treeId: IndexedTreeId,
66
+ forkId: number,
67
+ leaves: SerializedLeafValue[],
68
+ ): Promise<SerializedSequentialInsertionResult>;
69
+ syncBlock(input: {
70
+ blockNumber: BlockNumber;
71
+ blockStateRef: BlockStateReference;
72
+ blockHeaderHash: Buffer;
73
+ expectedArchiveRoot: Buffer;
74
+ expectedPreviousArchiveRoot: Buffer;
75
+ paddedNoteHashes: readonly SerializedLeafValue[];
76
+ paddedL1ToL2Messages: readonly SerializedLeafValue[];
77
+ paddedNullifiers: readonly SerializedLeafValue[];
78
+ publicDataWrites: readonly SerializedLeafValue[];
79
+ }): Promise<WorldStateStatusFull>;
80
+ createFork(input: { latest: boolean; blockNumber: BlockNumber }): Promise<number>;
81
+ deleteFork(forkId: number): Promise<void>;
82
+ finalizeBlocks(toBlockNumber: BlockNumber): Promise<WorldStateStatusSummary>;
83
+ unwindBlocks(toBlockNumber: BlockNumber): Promise<WorldStateStatusFull>;
84
+ removeHistoricalBlocks(toBlockNumber: BlockNumber): Promise<WorldStateStatusFull>;
85
+ getStatus(): Promise<WorldStateStatusSummary>;
86
+ createCheckpoint(forkId: number): Promise<number>;
87
+ commitCheckpoint(forkId: number): Promise<void>;
88
+ revertCheckpoint(forkId: number): Promise<void>;
89
+ commitAllCheckpoints(forkId: number, depth: number): Promise<void>;
90
+ revertAllCheckpoints(forkId: number, depth: number): Promise<void>;
91
+ copyStores(dstPath: string, compact: boolean): Promise<void>;
146
92
 
147
93
  /**
148
- * Sends a message to the native instance and returns the response.
149
- * @param messageType - The type of message to send
150
- * @param body - The message body
151
- * @param responseHandler - A callback accepting the response, executed on the job queue
152
- * @param errorHandler - A callback called on request error, executed on the job queue
153
- * @returns The response to the message
94
+ * IPC path the underlying aztec-wsdb process listens on. The C++ AVM uses this to attach to the
95
+ * same world state instance the TS layer is using.
154
96
  */
155
- public async call<T extends WorldStateMessageType>(
156
- messageType: T,
157
- body: WorldStateRequest[T] & WorldStateRequestCategories,
158
- // allows for the pre-processing of responses on the job queue before being passed back
159
- responseHandler = (response: WorldStateResponse[T]): WorldStateResponse[T] => response,
160
- errorHandler = (_: string) => {},
161
- ): Promise<WorldStateResponse[T]> {
162
- // Here we determine which fork the request is being executed against and whether it requires uncommitted data
163
- // We use the fork Id to select the appropriate request queue and the uncommitted data flag to pass to the queue
164
- let forkId = -1;
165
- // We assume it includes uncommitted unless explicitly told otherwise
166
- let committedOnly = false;
167
-
168
- // Canonical requests ALWAYS go against the canonical fork
169
- // These include things like block syncs/unwinds etc
170
- // These requests don't contain a fork ID
171
- if (isWithCanonical(body)) {
172
- forkId = 0;
173
- } else if (isWithForkId(body)) {
174
- forkId = body.forkId;
175
- } else if (isWithRevision(body)) {
176
- forkId = body.revision.forkId;
177
- committedOnly = body.revision.includeUncommitted === false;
178
- } else {
179
- const _: never = body;
180
- throw new Error(`Unable to determine forkId for message=${WorldStateMessageType[messageType]}`);
181
- }
182
-
183
- // Get the queue or create a new one
184
- let requestQueue = this.queues.get(forkId);
185
- if (requestQueue === undefined) {
186
- requestQueue = new WorldStateOpsQueue();
187
- this.queues.set(forkId, requestQueue);
188
- }
189
-
190
- // Enqueue the request and wait for the response. The per-fork queue is cleaned up in `finally` even on
191
- // error, so the JS-side queues map cannot outlive the native fork (e.g. when the native fork was already
192
- // destroyed by an unwind/historical-prune and DELETE_FORK rejects with "Fork not found").
193
- try {
194
- const response = await requestQueue.execute(
195
- async () => {
196
- assert.notEqual(messageType, WorldStateMessageType.CLOSE, 'Use close() to close the native instance');
197
- assert.equal(this.open, true, 'Native instance is closed');
198
- let response: WorldStateResponse[T];
199
- try {
200
- response = await this._sendMessage(messageType, body);
201
- } catch (error: any) {
202
- errorHandler(error.message);
203
- throw error;
204
- }
205
- return responseHandler(response);
206
- },
207
- messageType,
208
- committedOnly,
209
- );
210
- return response;
211
- } finally {
212
- if (messageType === WorldStateMessageType.DELETE_FORK) {
213
- await requestQueue.stop();
214
- this.queues.delete(forkId);
215
- }
216
- }
217
- }
97
+ getIpcPath(): string;
218
98
 
219
99
  /**
220
- * Stops the native instance.
100
+ * Shut down the world state instance. Cancels any in-flight queues, closes the IPC channel, and
101
+ * terminates the underlying aztec-wsdb process. Idempotent.
221
102
  */
222
- public async close(): Promise<void> {
223
- if (!this.open) {
224
- return;
225
- }
226
- this.open = false;
227
- const queue = this.queues.get(0)!;
228
-
229
- await queue.execute(
230
- async () => {
231
- await this._sendMessage(WorldStateMessageType.CLOSE, { canonical: true });
232
- },
233
- WorldStateMessageType.CLOSE,
234
- false,
235
- );
236
- await queue.stop();
237
- }
238
-
239
- private async _sendMessage<T extends WorldStateMessageType>(
240
- messageType: T,
241
- body: WorldStateRequest[T] & WorldStateRequestCategories,
242
- ): Promise<WorldStateResponse[T]> {
243
- let logMetadata: Record<string, any> = {};
244
-
245
- if (body) {
246
- if ('treeId' in body) {
247
- logMetadata['treeId'] = MerkleTreeId[body.treeId];
248
- }
249
-
250
- if ('revision' in body) {
251
- logMetadata = { ...logMetadata, ...body.revision };
252
- }
253
-
254
- if ('forkId' in body) {
255
- logMetadata['forkId'] = body.forkId;
256
- }
257
-
258
- if ('blockNumber' in body) {
259
- logMetadata['blockNumber'] = body.blockNumber;
260
- }
261
-
262
- if ('toBlockNumber' in body) {
263
- logMetadata['toBlockNumber'] = body.toBlockNumber;
264
- }
265
-
266
- if ('leafIndex' in body) {
267
- logMetadata['leafIndex'] = body.leafIndex;
268
- }
269
-
270
- if ('blockHeaderHash' in body) {
271
- logMetadata['blockHeaderHash'] = '0x' + body.blockHeaderHash.toString('hex');
272
- }
273
-
274
- if ('leaves' in body) {
275
- logMetadata['leavesCount'] = body.leaves.length;
276
- }
277
-
278
- // sync operation
279
- if ('paddedNoteHashes' in body) {
280
- logMetadata['notesCount'] = body.paddedNoteHashes.length;
281
- logMetadata['nullifiersCount'] = body.paddedNullifiers.length;
282
- logMetadata['l1ToL2MessagesCount'] = body.paddedL1ToL2Messages.length;
283
- logMetadata['publicDataWritesCount'] = body.publicDataWrites.length;
284
- }
285
- }
286
-
287
- try {
288
- const { duration, response } = await this.instance.sendMessage(messageType, body);
289
- this.log.trace(`Call ${WorldStateMessageType[messageType]} took (ms)`, {
290
- duration,
291
- ...logMetadata,
292
- });
293
-
294
- this.instrumentation.recordRoundTrip(duration.totalUs, messageType);
295
- return response;
296
- } catch (error) {
297
- this.log.error(`Call ${WorldStateMessageType[messageType]} failed: ${error}`, error, logMetadata);
298
- throw error;
299
- }
300
- }
103
+ close(): Promise<void>;
301
104
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Names of the world-state operations, used as labels for instrumentation and
3
+ * logging. (Read/write ordering is enforced server-side by the wsdb scheduler,
4
+ * so there is no longer a client-side operation queue.)
5
+ */
6
+ export type WorldStateOperationName =
7
+ | 'getTreeInfo'
8
+ | 'getStateReference'
9
+ | 'getInitialStateReference'
10
+ | 'getLeafValue'
11
+ | 'getLeafPreimage'
12
+ | 'getSiblingPath'
13
+ | 'getBlockNumbersForLeafIndices'
14
+ | 'findLeafIndices'
15
+ | 'findLowLeaf'
16
+ | 'findSiblingPaths'
17
+ | 'appendLeaves'
18
+ | 'batchInsert'
19
+ | 'sequentialInsert'
20
+ | 'updateArchive'
21
+ | 'syncBlock'
22
+ | 'createFork'
23
+ | 'deleteFork'
24
+ | 'finalizeBlocks'
25
+ | 'unwindBlocks'
26
+ | 'removeHistoricalBlocks'
27
+ | 'getStatus'
28
+ | 'createCheckpoint'
29
+ | 'commitCheckpoint'
30
+ | 'revertCheckpoint'
31
+ | 'commitAllCheckpoints'
32
+ | 'revertAllCheckpoints'
33
+ | 'copyStores';
@@ -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
  }
@@ -73,7 +73,6 @@ export async function createWorldState(
73
73
  bindings,
74
74
  )
75
75
  : await NativeWorldStateService.tmp(
76
- config.rollupAddress,
77
76
  !['true', '1'].includes(process.env.DEBUG_WORLD_STATE!),
78
77
  genesis,
79
78
  instrumentation,
@@ -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 { INITIAL_CHECKPOINT_NUMBER } 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,30 +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
- const initialBlockHash = (await this.merkleTreeCommitted.getInitialHeader().hash()).toString();
291
325
  return {
292
- proposed: latestBlockId,
293
- checkpointed: {
294
- block: { number: BlockNumber.ZERO, hash: initialBlockHash },
295
- checkpoint: { number: INITIAL_CHECKPOINT_NUMBER, hash: genesisCheckpointHeaderHash },
296
- },
297
- proposedCheckpoint: {
298
- block: { number: BlockNumber.ZERO, hash: initialBlockHash },
299
- checkpoint: { number: INITIAL_CHECKPOINT_NUMBER, hash: genesisCheckpointHeaderHash },
300
- },
326
+ proposed: { number: status.unfinalizedBlockNumber, hash: unfinalizedBlockHash },
301
327
  finalized: {
302
- block: { number: status.finalizedBlockNumber, hash: finalizedBlockHash ?? '' },
303
- checkpoint: { number: INITIAL_CHECKPOINT_NUMBER, hash: genesisCheckpointHeaderHash },
328
+ block: { number: status.finalizedBlockNumber, hash: finalizedBlockHash },
304
329
  },
305
330
  proven: {
306
- block: { number: provenBlockNumber, hash: provenBlockHash ?? '' },
307
- checkpoint: { number: INITIAL_CHECKPOINT_NUMBER, hash: genesisCheckpointHeaderHash },
331
+ block: { number: provenBlockNumber, hash: provenBlockHash },
308
332
  },
309
333
  };
310
334
  }
@@ -324,6 +348,15 @@ export class ServerWorldStateSynchronizer
324
348
  case 'chain-finalized':
325
349
  await this.handleChainFinalized(event.block.number);
326
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
+ }
327
360
  }
328
361
  }
329
362
 
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);