@aztec/world-state 0.0.1-commit.b2a5d0dd1 → 0.0.1-commit.b3d3157a
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.
- package/dest/instrumentation/instrumentation.d.ts +4 -3
- package/dest/instrumentation/instrumentation.d.ts.map +1 -1
- package/dest/instrumentation/instrumentation.js +4 -9
- package/dest/native/ipc_world_state_instance.d.ts +87 -0
- package/dest/native/ipc_world_state_instance.d.ts.map +1 -0
- package/dest/native/ipc_world_state_instance.js +660 -0
- package/dest/native/merkle_trees_facade.d.ts +6 -3
- package/dest/native/merkle_trees_facade.d.ts.map +1 -1
- package/dest/native/merkle_trees_facade.js +53 -113
- package/dest/native/message.d.ts +14 -229
- package/dest/native/message.d.ts.map +1 -1
- package/dest/native/message.js +0 -42
- package/dest/native/native_world_state.d.ts +15 -5
- package/dest/native/native_world_state.d.ts.map +1 -1
- package/dest/native/native_world_state.js +97 -51
- package/dest/native/native_world_state_instance.d.ts +58 -41
- package/dest/native/native_world_state_instance.d.ts.map +1 -1
- package/dest/native/native_world_state_instance.js +5 -203
- package/dest/native/world_state_operation.d.ts +7 -0
- package/dest/native/world_state_operation.d.ts.map +1 -0
- package/dest/native/world_state_operation.js +5 -0
- package/dest/synchronizer/config.d.ts +1 -1
- package/dest/synchronizer/config.d.ts.map +1 -1
- package/dest/synchronizer/config.js +9 -10
- package/dest/synchronizer/factory.d.ts +3 -3
- package/dest/synchronizer/factory.d.ts.map +1 -1
- package/dest/synchronizer/factory.js +5 -5
- package/dest/synchronizer/server_world_state_synchronizer.d.ts +1 -1
- package/dest/synchronizer/server_world_state_synchronizer.d.ts.map +1 -1
- package/dest/synchronizer/server_world_state_synchronizer.js +16 -13
- package/dest/testing.js +1 -1
- package/package.json +14 -10
- package/src/instrumentation/instrumentation.ts +6 -18
- package/src/native/ipc_world_state_instance.ts +830 -0
- package/src/native/merkle_trees_facade.ts +76 -108
- package/src/native/message.ts +13 -296
- package/src/native/native_world_state.ts +113 -79
- package/src/native/native_world_state_instance.ts +96 -284
- package/src/native/world_state_operation.ts +33 -0
- package/src/synchronizer/config.ts +14 -10
- package/src/synchronizer/factory.ts +8 -7
- package/src/synchronizer/server_world_state_synchronizer.ts +11 -14
- package/src/testing.ts +1 -1
- package/dest/native/world_state_ops_queue.d.ts +0 -19
- package/dest/native/world_state_ops_queue.d.ts.map +0 -1
- package/dest/native/world_state_ops_queue.js +0 -146
- package/src/native/world_state_ops_queue.ts +0 -190
|
@@ -3,7 +3,6 @@ import { BlockNumber } from '@aztec/foundation/branded-types';
|
|
|
3
3
|
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
|
-
import { tryRmDir } from '@aztec/foundation/fs';
|
|
7
6
|
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
|
|
8
7
|
import type { L2Block } from '@aztec/stdlib/block';
|
|
9
8
|
import { DatabaseVersionManager } from '@aztec/stdlib/database-version/manager';
|
|
@@ -19,16 +18,16 @@ import { EMPTY_GENESIS_DATA, type GenesisData, WorldStateRevision } from '@aztec
|
|
|
19
18
|
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
20
19
|
|
|
21
20
|
import assert from 'assert/strict';
|
|
22
|
-
import { mkdtemp, rm } from 'fs/promises';
|
|
21
|
+
import { mkdir, mkdtemp, rm } from 'fs/promises';
|
|
23
22
|
import { tmpdir } from 'os';
|
|
24
23
|
import { join } from 'path';
|
|
25
24
|
|
|
26
25
|
import { WorldStateInstrumentation } from '../instrumentation/instrumentation.js';
|
|
27
26
|
import type { WorldStateTreeMapSizes } from '../synchronizer/factory.js';
|
|
28
27
|
import type { MerkleTreeAdminDatabase as MerkleTreeDatabase } from '../world-state-db/merkle_tree_db.js';
|
|
28
|
+
import { IpcWorldState } from './ipc_world_state_instance.js';
|
|
29
29
|
import { MerkleTreesFacade, MerkleTreesForkFacade, serializeLeaf } from './merkle_trees_facade.js';
|
|
30
30
|
import {
|
|
31
|
-
WorldStateMessageType,
|
|
32
31
|
type WorldStateStatusFull,
|
|
33
32
|
type WorldStateStatusSummary,
|
|
34
33
|
blockStateReference,
|
|
@@ -36,7 +35,7 @@ import {
|
|
|
36
35
|
sanitizeSummary,
|
|
37
36
|
treeStateReferenceToSnapshot,
|
|
38
37
|
} from './message.js';
|
|
39
|
-
import {
|
|
38
|
+
import type { NativeWorldStateInstance } from './native_world_state_instance.js';
|
|
40
39
|
|
|
41
40
|
// The current version of the world state database schema
|
|
42
41
|
// Increment this when making incompatible changes to the database schema
|
|
@@ -50,11 +49,13 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
|
|
|
50
49
|
private cachedStatusSummary: WorldStateStatusSummary | undefined;
|
|
51
50
|
|
|
52
51
|
protected constructor(
|
|
53
|
-
protected instance:
|
|
52
|
+
protected instance: NativeWorldStateInstance,
|
|
54
53
|
protected readonly worldStateInstrumentation: WorldStateInstrumentation,
|
|
55
54
|
protected readonly log: Logger,
|
|
56
55
|
private readonly genesis: GenesisData = EMPTY_GENESIS_DATA,
|
|
57
56
|
private readonly cleanup = () => Promise.resolve(),
|
|
57
|
+
/** Factory to recreate a fresh IpcWorldState after clear(). */
|
|
58
|
+
private readonly recreateInstance?: () => Promise<NativeWorldStateInstance>,
|
|
58
59
|
) {}
|
|
59
60
|
|
|
60
61
|
static async new(
|
|
@@ -66,20 +67,31 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
|
|
|
66
67
|
bindings?: LoggerBindings,
|
|
67
68
|
cleanup = () => Promise.resolve(),
|
|
68
69
|
): Promise<NativeWorldStateService> {
|
|
70
|
+
for (const [key, value] of Object.entries(wsTreeMapSizes)) {
|
|
71
|
+
if (value <= 0) {
|
|
72
|
+
throw new Error(`Map size must be a positive number, got ${value} for ${key}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
const log = createLogger('world-state:database', bindings);
|
|
70
77
|
const worldStateDirectory = join(dataDir, WORLD_STATE_DIR);
|
|
71
|
-
|
|
78
|
+
|
|
72
79
|
const versionManager = new DatabaseVersionManager({
|
|
73
80
|
schemaVersion: WORLD_STATE_DB_VERSION,
|
|
74
81
|
rollupAddress,
|
|
75
82
|
dataDirectory: worldStateDirectory,
|
|
76
|
-
onOpen: (dir
|
|
77
|
-
return Promise.resolve(new NativeWorldState(dir, wsTreeMapSizes, genesis, instrumentation, bindings));
|
|
78
|
-
},
|
|
83
|
+
onOpen: dir => IpcWorldState.spawn(dir, wsTreeMapSizes, genesis, instrumentation, bindings),
|
|
79
84
|
});
|
|
80
85
|
|
|
81
86
|
const [instance] = await versionManager.open();
|
|
82
|
-
|
|
87
|
+
|
|
88
|
+
const recreateInstance = async () => {
|
|
89
|
+
await rm(worldStateDirectory, { recursive: true, force: true, maxRetries: 3 });
|
|
90
|
+
await mkdir(worldStateDirectory, { recursive: true });
|
|
91
|
+
return IpcWorldState.spawn(worldStateDirectory, wsTreeMapSizes, genesis, instrumentation, bindings);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const worldState = new this(instance, instrumentation, log, genesis, cleanup, recreateInstance);
|
|
83
95
|
try {
|
|
84
96
|
await worldState.init();
|
|
85
97
|
} catch (e) {
|
|
@@ -91,7 +103,6 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
|
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
static async tmp(
|
|
94
|
-
rollupAddress = EthAddress.ZERO,
|
|
95
106
|
cleanupTmpDir = true,
|
|
96
107
|
genesis: GenesisData = EMPTY_GENESIS_DATA,
|
|
97
108
|
instrumentation = new WorldStateInstrumentation(getTelemetryClient()),
|
|
@@ -109,7 +120,8 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
|
|
|
109
120
|
};
|
|
110
121
|
log.debug(`Created temporary world state database at: ${dataDir} with tree map size: ${dbMapSizeKb}`);
|
|
111
122
|
|
|
112
|
-
|
|
123
|
+
const instance = await IpcWorldState.spawn(dataDir, worldStateTreeMapSizes, genesis, instrumentation, bindings);
|
|
124
|
+
|
|
113
125
|
const cleanup = async () => {
|
|
114
126
|
if (cleanupTmpDir) {
|
|
115
127
|
await rm(dataDir, { recursive: true, force: true, maxRetries: 3 });
|
|
@@ -119,7 +131,43 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
|
|
|
119
131
|
}
|
|
120
132
|
};
|
|
121
133
|
|
|
122
|
-
|
|
134
|
+
const recreateInstance = async () => {
|
|
135
|
+
await rm(dataDir, { recursive: true, force: true, maxRetries: 3 });
|
|
136
|
+
await mkdir(dataDir, { recursive: true });
|
|
137
|
+
return IpcWorldState.spawn(dataDir, worldStateTreeMapSizes, genesis, instrumentation, bindings);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const worldState = new this(instance, instrumentation, log, genesis, cleanup, recreateInstance);
|
|
141
|
+
try {
|
|
142
|
+
await worldState.init();
|
|
143
|
+
} catch (e) {
|
|
144
|
+
log.error(`Error initializing tmp world state: ${e}`);
|
|
145
|
+
throw e;
|
|
146
|
+
}
|
|
147
|
+
return worldState;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static ephemeral(
|
|
151
|
+
genesis: GenesisData = EMPTY_GENESIS_DATA,
|
|
152
|
+
instrumentation = new WorldStateInstrumentation(getTelemetryClient()),
|
|
153
|
+
bindings?: LoggerBindings,
|
|
154
|
+
): Promise<NativeWorldStateService> {
|
|
155
|
+
return this.tmp(/*cleanupTmpDir=*/ true, genesis, instrumentation, bindings);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
static async fromIpc(
|
|
159
|
+
wsdbBackend: ConstructorParameters<typeof IpcWorldState>[0],
|
|
160
|
+
instrumentation = new WorldStateInstrumentation(getTelemetryClient()),
|
|
161
|
+
bindings?: LoggerBindings,
|
|
162
|
+
genesis: GenesisData = EMPTY_GENESIS_DATA,
|
|
163
|
+
cleanup = () => Promise.resolve(),
|
|
164
|
+
recreateInstance?: () => Promise<NativeWorldStateInstance>,
|
|
165
|
+
): Promise<NativeWorldStateService> {
|
|
166
|
+
const log = createLogger('world-state:database', bindings);
|
|
167
|
+
const instance = new IpcWorldState(wsdbBackend, instrumentation, bindings);
|
|
168
|
+
const worldState = new this(instance, instrumentation, log, genesis, cleanup, recreateInstance);
|
|
169
|
+
await worldState.init();
|
|
170
|
+
return worldState;
|
|
123
171
|
}
|
|
124
172
|
|
|
125
173
|
protected async init() {
|
|
@@ -143,11 +191,24 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
|
|
|
143
191
|
assert.strictEqual(initialHeaderIndex, 0n, 'Invalid initial archive state');
|
|
144
192
|
}
|
|
145
193
|
|
|
146
|
-
public async clear() {
|
|
194
|
+
public async clear(): Promise<void> {
|
|
195
|
+
if (!this.recreateInstance) {
|
|
196
|
+
throw new Error('clear() is not available for externally-managed IPC backends');
|
|
197
|
+
}
|
|
198
|
+
this.log.warn('Clearing world state: shutting down WSDB, deleting data, and recreating');
|
|
147
199
|
await this.instance.close();
|
|
148
200
|
this.cachedStatusSummary = undefined;
|
|
149
|
-
|
|
150
|
-
|
|
201
|
+
this.instance = await this.recreateInstance();
|
|
202
|
+
await this.init();
|
|
203
|
+
this.log.info('World state cleared and reinitialized from genesis');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Returns the IPC path of the underlying IPC backend, if available. */
|
|
207
|
+
public getIpcPath(): string {
|
|
208
|
+
if (this.instance instanceof IpcWorldState) {
|
|
209
|
+
return this.instance.getIpcPath();
|
|
210
|
+
}
|
|
211
|
+
throw new Error('getIpcPath() is only available with IPC world state');
|
|
151
212
|
}
|
|
152
213
|
|
|
153
214
|
public getCommitted(): MerkleTreeReadOperations {
|
|
@@ -166,16 +227,15 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
|
|
|
166
227
|
blockNumber?: BlockNumber,
|
|
167
228
|
opts: { closeDelayMs?: number } = {},
|
|
168
229
|
): Promise<MerkleTreeWriteOperations> {
|
|
169
|
-
const
|
|
230
|
+
const forkId = await this.instance.createFork({
|
|
170
231
|
latest: blockNumber === undefined,
|
|
171
232
|
blockNumber: blockNumber ?? BlockNumber.ZERO,
|
|
172
|
-
canonical: true,
|
|
173
233
|
});
|
|
174
234
|
return new MerkleTreesForkFacade(
|
|
175
235
|
this.instance,
|
|
176
236
|
this.initialHeader!,
|
|
177
237
|
new WorldStateRevision(
|
|
178
|
-
/*forkId=*/
|
|
238
|
+
/*forkId=*/ forkId,
|
|
179
239
|
/* blockNumber=*/ WorldStateRevision.LATEST,
|
|
180
240
|
/* includeUncommitted=*/ true,
|
|
181
241
|
),
|
|
@@ -219,30 +279,33 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
|
|
|
219
279
|
});
|
|
220
280
|
|
|
221
281
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
canonical: true,
|
|
233
|
-
},
|
|
234
|
-
this.sanitizeAndCacheSummaryFromFull.bind(this),
|
|
235
|
-
this.deleteCachedSummary.bind(this),
|
|
236
|
-
);
|
|
282
|
+
const status = await this.instance.syncBlock({
|
|
283
|
+
blockNumber: l2Block.number,
|
|
284
|
+
blockHeaderHash: (await l2Block.hash()).toBuffer(),
|
|
285
|
+
paddedL1ToL2Messages: paddedL1ToL2Messages.map(serializeLeaf),
|
|
286
|
+
paddedNoteHashes: paddedNoteHashes.map(serializeLeaf),
|
|
287
|
+
paddedNullifiers: paddedNullifiers.map(serializeLeaf),
|
|
288
|
+
publicDataWrites: publicDataWrites.map(serializeLeaf),
|
|
289
|
+
blockStateRef: blockStateReference(l2Block.header.state),
|
|
290
|
+
});
|
|
291
|
+
return this.sanitizeAndCacheSummaryFromFull(status);
|
|
237
292
|
} catch (err) {
|
|
293
|
+
this.deleteCachedSummary();
|
|
238
294
|
this.worldStateInstrumentation.incCriticalErrors('synch_pending_block');
|
|
239
295
|
throw err;
|
|
240
296
|
}
|
|
241
297
|
}
|
|
242
298
|
|
|
243
299
|
public async close(): Promise<void> {
|
|
244
|
-
|
|
245
|
-
|
|
300
|
+
try {
|
|
301
|
+
await this.instance.close();
|
|
302
|
+
} finally {
|
|
303
|
+
await this.cleanup();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async [Symbol.asyncDispose](): Promise<void> {
|
|
308
|
+
await this.close();
|
|
246
309
|
}
|
|
247
310
|
|
|
248
311
|
private async buildInitialHeader(): Promise<BlockHeader> {
|
|
@@ -265,7 +328,7 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
|
|
|
265
328
|
return sanitized;
|
|
266
329
|
}
|
|
267
330
|
|
|
268
|
-
private deleteCachedSummary(
|
|
331
|
+
private deleteCachedSummary() {
|
|
269
332
|
this.cachedStatusSummary = undefined;
|
|
270
333
|
}
|
|
271
334
|
|
|
@@ -276,16 +339,9 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
|
|
|
276
339
|
*/
|
|
277
340
|
public async setFinalized(toBlockNumber: BlockNumber) {
|
|
278
341
|
try {
|
|
279
|
-
await this.instance.
|
|
280
|
-
WorldStateMessageType.FINALIZE_BLOCKS,
|
|
281
|
-
{
|
|
282
|
-
toBlockNumber,
|
|
283
|
-
canonical: true,
|
|
284
|
-
},
|
|
285
|
-
this.sanitizeAndCacheSummary.bind(this),
|
|
286
|
-
this.deleteCachedSummary.bind(this),
|
|
287
|
-
);
|
|
342
|
+
this.sanitizeAndCacheSummary(await this.instance.finalizeBlocks(toBlockNumber));
|
|
288
343
|
} catch (err) {
|
|
344
|
+
this.deleteCachedSummary();
|
|
289
345
|
this.worldStateInstrumentation.incCriticalErrors('finalize_block');
|
|
290
346
|
throw err;
|
|
291
347
|
}
|
|
@@ -299,16 +355,9 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
|
|
|
299
355
|
*/
|
|
300
356
|
public async removeHistoricalBlocks(toBlockNumber: BlockNumber) {
|
|
301
357
|
try {
|
|
302
|
-
return await this.instance.
|
|
303
|
-
WorldStateMessageType.REMOVE_HISTORICAL_BLOCKS,
|
|
304
|
-
{
|
|
305
|
-
toBlockNumber,
|
|
306
|
-
canonical: true,
|
|
307
|
-
},
|
|
308
|
-
this.sanitizeAndCacheSummaryFromFull.bind(this),
|
|
309
|
-
this.deleteCachedSummary.bind(this),
|
|
310
|
-
);
|
|
358
|
+
return this.sanitizeAndCacheSummaryFromFull(await this.instance.removeHistoricalBlocks(toBlockNumber));
|
|
311
359
|
} catch (err) {
|
|
360
|
+
this.deleteCachedSummary();
|
|
312
361
|
this.worldStateInstrumentation.incCriticalErrors('prune_historical_block');
|
|
313
362
|
throw err;
|
|
314
363
|
}
|
|
@@ -321,16 +370,9 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
|
|
|
321
370
|
*/
|
|
322
371
|
public async unwindBlocks(toBlockNumber: BlockNumber) {
|
|
323
372
|
try {
|
|
324
|
-
return await this.instance.
|
|
325
|
-
WorldStateMessageType.UNWIND_BLOCKS,
|
|
326
|
-
{
|
|
327
|
-
toBlockNumber,
|
|
328
|
-
canonical: true,
|
|
329
|
-
},
|
|
330
|
-
this.sanitizeAndCacheSummaryFromFull.bind(this),
|
|
331
|
-
this.deleteCachedSummary.bind(this),
|
|
332
|
-
);
|
|
373
|
+
return this.sanitizeAndCacheSummaryFromFull(await this.instance.unwindBlocks(toBlockNumber));
|
|
333
374
|
} catch (err) {
|
|
375
|
+
this.deleteCachedSummary();
|
|
334
376
|
this.worldStateInstrumentation.incCriticalErrors('prune_pending_block');
|
|
335
377
|
throw err;
|
|
336
378
|
}
|
|
@@ -340,11 +382,7 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
|
|
|
340
382
|
if (this.cachedStatusSummary !== undefined) {
|
|
341
383
|
return { ...this.cachedStatusSummary };
|
|
342
384
|
}
|
|
343
|
-
return await this.instance.
|
|
344
|
-
WorldStateMessageType.GET_STATUS,
|
|
345
|
-
{ canonical: true },
|
|
346
|
-
this.sanitizeAndCacheSummary.bind(this),
|
|
347
|
-
);
|
|
385
|
+
return this.sanitizeAndCacheSummary(await this.instance.getStatus());
|
|
348
386
|
}
|
|
349
387
|
|
|
350
388
|
updateLeaf<ID extends IndexedTreeId>(
|
|
@@ -356,14 +394,14 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
|
|
|
356
394
|
}
|
|
357
395
|
|
|
358
396
|
private async getInitialStateReference(): Promise<StateReference> {
|
|
359
|
-
const
|
|
397
|
+
const state = await this.instance.getInitialStateReference();
|
|
360
398
|
|
|
361
399
|
return new StateReference(
|
|
362
|
-
treeStateReferenceToSnapshot(
|
|
400
|
+
treeStateReferenceToSnapshot(state[MerkleTreeId.L1_TO_L2_MESSAGE_TREE]),
|
|
363
401
|
new PartialStateReference(
|
|
364
|
-
treeStateReferenceToSnapshot(
|
|
365
|
-
treeStateReferenceToSnapshot(
|
|
366
|
-
treeStateReferenceToSnapshot(
|
|
402
|
+
treeStateReferenceToSnapshot(state[MerkleTreeId.NOTE_HASH_TREE]),
|
|
403
|
+
treeStateReferenceToSnapshot(state[MerkleTreeId.NULLIFIER_TREE]),
|
|
404
|
+
treeStateReferenceToSnapshot(state[MerkleTreeId.PUBLIC_DATA_TREE]),
|
|
367
405
|
),
|
|
368
406
|
);
|
|
369
407
|
}
|
|
@@ -372,11 +410,7 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
|
|
|
372
410
|
dstPath: string,
|
|
373
411
|
compact: boolean = true,
|
|
374
412
|
): Promise<Record<Exclude<SnapshotDataKeys, 'archiver'>, string>> {
|
|
375
|
-
await this.instance.
|
|
376
|
-
dstPath,
|
|
377
|
-
compact,
|
|
378
|
-
canonical: true,
|
|
379
|
-
});
|
|
413
|
+
await this.instance.copyStores(dstPath, compact);
|
|
380
414
|
return fromEntries(NATIVE_WORLD_STATE_DBS.map(([name, dir]) => [name, join(dstPath, dir, 'data.mdb')] as const));
|
|
381
415
|
}
|
|
382
416
|
}
|