@aztec/txe 0.0.1-commit.fffb133c → 0.0.1-dev
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/index.d.ts +1 -1
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +88 -54
- package/dest/oracle/interfaces.d.ts +35 -28
- package/dest/oracle/interfaces.d.ts.map +1 -1
- package/dest/oracle/txe_oracle_public_context.d.ts +13 -13
- package/dest/oracle/txe_oracle_public_context.d.ts.map +1 -1
- package/dest/oracle/txe_oracle_public_context.js +14 -15
- package/dest/oracle/txe_oracle_top_level_context.d.ts +33 -23
- package/dest/oracle/txe_oracle_top_level_context.d.ts.map +1 -1
- package/dest/oracle/txe_oracle_top_level_context.js +185 -71
- package/dest/rpc_translator.d.ts +135 -83
- package/dest/rpc_translator.d.ts.map +1 -1
- package/dest/rpc_translator.js +540 -178
- package/dest/state_machine/archiver.d.ts +3 -3
- package/dest/state_machine/archiver.d.ts.map +1 -1
- package/dest/state_machine/archiver.js +7 -7
- package/dest/state_machine/dummy_p2p_client.d.ts +16 -12
- package/dest/state_machine/dummy_p2p_client.d.ts.map +1 -1
- package/dest/state_machine/dummy_p2p_client.js +28 -16
- package/dest/state_machine/index.d.ts +7 -5
- package/dest/state_machine/index.d.ts.map +1 -1
- package/dest/state_machine/index.js +19 -10
- package/dest/state_machine/mock_epoch_cache.d.ts +3 -1
- package/dest/state_machine/mock_epoch_cache.d.ts.map +1 -1
- package/dest/state_machine/mock_epoch_cache.js +4 -0
- package/dest/state_machine/synchronizer.d.ts +5 -5
- package/dest/state_machine/synchronizer.d.ts.map +1 -1
- package/dest/state_machine/synchronizer.js +3 -3
- package/dest/txe_oracle_version.d.ts +10 -0
- package/dest/txe_oracle_version.d.ts.map +1 -0
- package/dest/txe_oracle_version.js +8 -0
- package/dest/txe_session.d.ts +67 -8
- package/dest/txe_session.d.ts.map +1 -1
- package/dest/txe_session.js +175 -32
- package/dest/util/encoding.d.ts +43 -41
- package/dest/util/encoding.d.ts.map +1 -1
- package/dest/util/txe_public_contract_data_source.d.ts +2 -3
- package/dest/util/txe_public_contract_data_source.d.ts.map +1 -1
- package/dest/util/txe_public_contract_data_source.js +5 -22
- package/dest/utils/block_creation.d.ts +1 -1
- package/dest/utils/block_creation.d.ts.map +1 -1
- package/dest/utils/block_creation.js +3 -1
- package/package.json +15 -15
- package/src/index.ts +89 -52
- package/src/oracle/interfaces.ts +43 -34
- package/src/oracle/txe_oracle_public_context.ts +14 -16
- package/src/oracle/txe_oracle_top_level_context.ts +231 -132
- package/src/rpc_translator.ts +673 -208
- package/src/state_machine/archiver.ts +7 -5
- package/src/state_machine/dummy_p2p_client.ts +40 -22
- package/src/state_machine/index.ts +30 -10
- package/src/state_machine/mock_epoch_cache.ts +5 -0
- package/src/state_machine/synchronizer.ts +4 -4
- package/src/txe_oracle_version.ts +9 -0
- package/src/txe_session.ts +270 -93
- package/src/util/txe_public_contract_data_source.ts +10 -36
- package/src/utils/block_creation.ts +3 -1
- package/dest/util/txe_contract_store.d.ts +0 -12
- package/dest/util/txe_contract_store.d.ts.map +0 -1
- package/dest/util/txe_contract_store.js +0 -22
- package/src/util/txe_contract_store.ts +0 -36
package/src/txe_session.ts
CHANGED
|
@@ -3,10 +3,12 @@ import { Fr } from '@aztec/foundation/curves/bn254';
|
|
|
3
3
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
4
4
|
import { KeyStore } from '@aztec/key-store';
|
|
5
5
|
import { openTmpStore } from '@aztec/kv-store/lmdb-v2';
|
|
6
|
-
import type { ProtocolContract } from '@aztec/protocol-contracts';
|
|
7
6
|
import {
|
|
8
7
|
AddressStore,
|
|
8
|
+
AnchorBlockStore,
|
|
9
|
+
CapsuleService,
|
|
9
10
|
CapsuleStore,
|
|
11
|
+
ContractStore,
|
|
10
12
|
JobCoordinator,
|
|
11
13
|
NoteService,
|
|
12
14
|
NoteStore,
|
|
@@ -36,11 +38,11 @@ import {
|
|
|
36
38
|
import { FunctionCall, FunctionSelector, FunctionType } from '@aztec/stdlib/abi';
|
|
37
39
|
import type { AuthWitness } from '@aztec/stdlib/auth-witness';
|
|
38
40
|
import { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
39
|
-
import { GasSettings } from '@aztec/stdlib/gas';
|
|
41
|
+
import type { GasSettings } from '@aztec/stdlib/gas';
|
|
40
42
|
import { computeProtocolNullifier } from '@aztec/stdlib/hash';
|
|
41
43
|
import { PrivateContextInputs } from '@aztec/stdlib/kernel';
|
|
42
44
|
import { makeGlobalVariables } from '@aztec/stdlib/testing';
|
|
43
|
-
import { CallContext, GlobalVariables, TxContext } from '@aztec/stdlib/tx';
|
|
45
|
+
import { CallContext, GlobalVariables, OFFCHAIN_MESSAGE_IDENTIFIER, TxContext } from '@aztec/stdlib/tx';
|
|
44
46
|
|
|
45
47
|
import { z } from 'zod';
|
|
46
48
|
|
|
@@ -49,10 +51,11 @@ import type { IAvmExecutionOracle, ITxeExecutionOracle } from './oracle/interfac
|
|
|
49
51
|
import { TXEOraclePublicContext } from './oracle/txe_oracle_public_context.js';
|
|
50
52
|
import { TXEOracleTopLevelContext } from './oracle/txe_oracle_top_level_context.js';
|
|
51
53
|
import { RPCTranslator } from './rpc_translator.js';
|
|
54
|
+
import { TXEArchiver } from './state_machine/archiver.js';
|
|
52
55
|
import { TXEStateMachine } from './state_machine/index.js';
|
|
56
|
+
import { TXE_ORACLE_VERSION_MAJOR, TXE_ORACLE_VERSION_MINOR } from './txe_oracle_version.js';
|
|
53
57
|
import type { ForeignCallArgs, ForeignCallResult } from './util/encoding.js';
|
|
54
58
|
import { TXEAccountStore } from './util/txe_account_store.js';
|
|
55
|
-
import { TXEContractStore } from './util/txe_contract_store.js';
|
|
56
59
|
import { getSingleTxBlockRequestHash, insertTxEffectIntoWorldTrees, makeTXEBlock } from './utils/block_creation.js';
|
|
57
60
|
import { makeTxEffect } from './utils/tx_effect_creation.js';
|
|
58
61
|
|
|
@@ -107,10 +110,80 @@ export type TXEOracleFunctionName = Exclude<
|
|
|
107
110
|
>;
|
|
108
111
|
|
|
109
112
|
export interface TXESessionStateHandler {
|
|
113
|
+
/** Records the TXE oracle version reported by the Noir test code for diagnostics. */
|
|
114
|
+
setTxeOracleVersion(version: { major: number; minor: number }): void;
|
|
115
|
+
|
|
110
116
|
enterTopLevelState(): Promise<void>;
|
|
111
117
|
enterPublicState(contractAddress?: AztecAddress): Promise<void>;
|
|
112
|
-
enterPrivateState(
|
|
118
|
+
enterPrivateState(
|
|
119
|
+
contractAddress: AztecAddress | undefined,
|
|
120
|
+
anchorBlockNumber: BlockNumber | undefined,
|
|
121
|
+
gasSettings: GasSettings,
|
|
122
|
+
): Promise<PrivateContextInputs>;
|
|
113
123
|
enterUtilityState(contractAddress?: AztecAddress): Promise<void>;
|
|
124
|
+
|
|
125
|
+
// TODO(F-335): Exposing the job info is abstraction breakage - drop the following 2 functions.
|
|
126
|
+
cycleJob(): Promise<string>;
|
|
127
|
+
getCurrentJob(): string;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Runs an executor-style top-level call (private/public call, utility execution) with last-call tracking.
|
|
131
|
+
*/
|
|
132
|
+
withTopLevelCallTracking<T>(work: () => Promise<{ result: T; txHash?: Fr }>): Promise<T>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Captures a raw offchain effect payload for consumption from test environment. Called by the `emit_offchain_effect`
|
|
136
|
+
* oracle handler whenever a contract function emits an offchain message, at any call depth.
|
|
137
|
+
*/
|
|
138
|
+
recordOffchainEffect(data: Fr[]): void;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Returns the raw offchain effect payloads emitted by the last top-level call. Each payload follows the protocol
|
|
142
|
+
* convention documented on `OFFCHAIN_MESSAGE_IDENTIFIER`, i.e. `[identifier, recipient, ...ciphertext]`. Decoding into
|
|
143
|
+
* `OffchainMessage` structs happens on the Noir side of the test helper. Marks the buffer as queried so the
|
|
144
|
+
* unqueried-messages warning doesn't fire on the next reset.
|
|
145
|
+
*/
|
|
146
|
+
getLastCallOffchainEffects(): { effects: Fr[][] };
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Returns the context of the last top-level call: its tx hash (`Fr.ZERO` if the call was tx-less) and the anchor
|
|
150
|
+
* block timestamp captured at the start of the call. Does *not* mark the buffer as queried — context reads are
|
|
151
|
+
* metadata, not effect consumption.
|
|
152
|
+
*/
|
|
153
|
+
getLastCallContext(): { txHash: Fr; anchorBlockTimestamp: bigint };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Session state tracking the most recently completed top-level call: the offchain effect buffer it produced, and the
|
|
158
|
+
* call's context (tx hash + anchor block timestamp). The context is refreshed on every top-level call, independently
|
|
159
|
+
* of whether the call produced offchain effects.
|
|
160
|
+
*/
|
|
161
|
+
interface LastCallState {
|
|
162
|
+
/**
|
|
163
|
+
* Raw offchain effect payloads emitted by the currently-executing (or most recently completed) top-level call. Wiped
|
|
164
|
+
* at the start of every top-level entry point, appended to on every `emit_offchain_effect` oracle invocation.
|
|
165
|
+
*/
|
|
166
|
+
offchainEffects: Fr[][];
|
|
167
|
+
/**
|
|
168
|
+
* Tracks whether the test has queried `effects` since the last reset. If a new top-level call clobbers the buffer
|
|
169
|
+
* without it being queried first, any accumulated messages are lost and we emit a warning so tests don't silently
|
|
170
|
+
* drop delivery.
|
|
171
|
+
*/
|
|
172
|
+
queried: boolean;
|
|
173
|
+
/**
|
|
174
|
+
* Tx hash of the most recently completed top-level call, or `Fr.ZERO` if the call was tx-less (context setters,
|
|
175
|
+
* utility execution). Populated by call executor handlers after execution completes.
|
|
176
|
+
*/
|
|
177
|
+
txHash: Fr;
|
|
178
|
+
/**
|
|
179
|
+
* Anchor block timestamp of the most recently completed top-level call, captured from the anchor block header that
|
|
180
|
+
* was active when the call started. Populated by call executor handlers after execution completes.
|
|
181
|
+
*/
|
|
182
|
+
anchorBlockTimestamp: bigint;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function emptyLastCallState(): LastCallState {
|
|
186
|
+
return { offchainEffects: [], queried: false, txHash: Fr.ZERO, anchorBlockTimestamp: 0n };
|
|
114
187
|
}
|
|
115
188
|
|
|
116
189
|
/**
|
|
@@ -120,6 +193,8 @@ export interface TXESessionStateHandler {
|
|
|
120
193
|
export class TXESession implements TXESessionStateHandler {
|
|
121
194
|
private state: SessionState = { name: 'TOP_LEVEL' };
|
|
122
195
|
private authwits: Map<string, AuthWitness> = new Map();
|
|
196
|
+
private lastCallInfo: LastCallState = emptyLastCallState();
|
|
197
|
+
private txeOracleVersion: { major: number; minor: number } | undefined;
|
|
123
198
|
|
|
124
199
|
constructor(
|
|
125
200
|
private logger: Logger,
|
|
@@ -129,7 +204,7 @@ export class TXESession implements TXESessionStateHandler {
|
|
|
129
204
|
| IPrivateExecutionOracle
|
|
130
205
|
| IAvmExecutionOracle
|
|
131
206
|
| ITxeExecutionOracle,
|
|
132
|
-
private contractStore:
|
|
207
|
+
private contractStore: ContractStore,
|
|
133
208
|
private noteStore: NoteStore,
|
|
134
209
|
private keyStore: KeyStore,
|
|
135
210
|
private addressStore: AddressStore,
|
|
@@ -146,12 +221,11 @@ export class TXESession implements TXESessionStateHandler {
|
|
|
146
221
|
private nextBlockTimestamp: bigint,
|
|
147
222
|
) {}
|
|
148
223
|
|
|
149
|
-
static async init(
|
|
224
|
+
static async init(contractStore: ContractStore) {
|
|
150
225
|
const store = await openTmpStore('txe-session');
|
|
151
226
|
|
|
152
227
|
const addressStore = new AddressStore(store);
|
|
153
228
|
const privateEventStore = new PrivateEventStore(store);
|
|
154
|
-
const contractStore = new TXEContractStore(store);
|
|
155
229
|
const noteStore = new NoteStore(store);
|
|
156
230
|
const senderTaggingStore = new SenderTaggingStore(store);
|
|
157
231
|
const recipientTaggingStore = new RecipientTaggingStore(store);
|
|
@@ -170,13 +244,9 @@ export class TXESession implements TXESessionStateHandler {
|
|
|
170
244
|
noteStore,
|
|
171
245
|
]);
|
|
172
246
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
await contractStore.addContractInstance(instance);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const stateMachine = await TXEStateMachine.create(store);
|
|
247
|
+
const archiver = new TXEArchiver(store);
|
|
248
|
+
const anchorBlockStore = new AnchorBlockStore(store);
|
|
249
|
+
const stateMachine = await TXEStateMachine.create(archiver, anchorBlockStore, contractStore, noteStore);
|
|
180
250
|
|
|
181
251
|
const nextBlockTimestamp = BigInt(Math.floor(new Date().getTime() / 1000));
|
|
182
252
|
const version = new Fr(await stateMachine.node.getVersion());
|
|
@@ -184,6 +254,8 @@ export class TXESession implements TXESessionStateHandler {
|
|
|
184
254
|
|
|
185
255
|
const initialJobId = jobCoordinator.beginJob();
|
|
186
256
|
|
|
257
|
+
const logger = createLogger('txe:session');
|
|
258
|
+
|
|
187
259
|
const topLevelOracleHandler = new TXEOracleTopLevelContext(
|
|
188
260
|
stateMachine,
|
|
189
261
|
contractStore,
|
|
@@ -196,16 +268,15 @@ export class TXESession implements TXESessionStateHandler {
|
|
|
196
268
|
senderAddressBookStore,
|
|
197
269
|
capsuleStore,
|
|
198
270
|
privateEventStore,
|
|
199
|
-
initialJobId,
|
|
200
271
|
nextBlockTimestamp,
|
|
201
272
|
version,
|
|
202
273
|
chainId,
|
|
203
274
|
new Map(),
|
|
204
275
|
);
|
|
205
|
-
await topLevelOracleHandler.
|
|
276
|
+
await topLevelOracleHandler.advanceBlocksBy(1);
|
|
206
277
|
|
|
207
278
|
return new TXESession(
|
|
208
|
-
|
|
279
|
+
logger,
|
|
209
280
|
stateMachine,
|
|
210
281
|
topLevelOracleHandler,
|
|
211
282
|
contractStore,
|
|
@@ -244,7 +315,28 @@ export class TXESession implements TXESessionStateHandler {
|
|
|
244
315
|
return translator[validatedFunctionName](...inputs);
|
|
245
316
|
} catch (error) {
|
|
246
317
|
if (error instanceof z.ZodError) {
|
|
247
|
-
|
|
318
|
+
let versionHint: string;
|
|
319
|
+
if (!this.txeOracleVersion) {
|
|
320
|
+
versionHint =
|
|
321
|
+
' The test appears to use an older version of Aztec.nr that does not' +
|
|
322
|
+
' support test environment oracle versioning. Update Aztec.nr to a compatible version.' +
|
|
323
|
+
' See https://docs.aztec.network/errors/12';
|
|
324
|
+
} else if (this.txeOracleVersion.minor > TXE_ORACLE_VERSION_MINOR) {
|
|
325
|
+
versionHint =
|
|
326
|
+
` The test uses Aztec.nr test oracle version` +
|
|
327
|
+
` ${this.txeOracleVersion.major}.${this.txeOracleVersion.minor}, but this test environment` +
|
|
328
|
+
` only supports up to ${TXE_ORACLE_VERSION_MAJOR}.${TXE_ORACLE_VERSION_MINOR}.` +
|
|
329
|
+
` Upgrade the Aztec CLI to a compatible version.` +
|
|
330
|
+
` See https://docs.aztec.network/errors/12`;
|
|
331
|
+
} else {
|
|
332
|
+
versionHint =
|
|
333
|
+
` The test's oracle version (${this.txeOracleVersion.major}.${this.txeOracleVersion.minor})` +
|
|
334
|
+
` is compatible with this test environment` +
|
|
335
|
+
` (${TXE_ORACLE_VERSION_MAJOR}.${TXE_ORACLE_VERSION_MINOR}), so this oracle should be` +
|
|
336
|
+
` available. This is an unexpected error, please report it.` +
|
|
337
|
+
` See https://docs.aztec.network/errors/13`;
|
|
338
|
+
}
|
|
339
|
+
throw new Error(`Unknown oracle '${functionName}'.${versionHint}`);
|
|
248
340
|
} else if (error instanceof Error) {
|
|
249
341
|
throw new Error(
|
|
250
342
|
`Execution error while processing function ${functionName} in state ${this.state.name}: ${error.message}`,
|
|
@@ -257,6 +349,66 @@ export class TXESession implements TXESessionStateHandler {
|
|
|
257
349
|
}
|
|
258
350
|
}
|
|
259
351
|
|
|
352
|
+
getCurrentJob(): string {
|
|
353
|
+
return this.currentJobId;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Commits the current job and begins a new one. Returns the new job ID. */
|
|
357
|
+
async cycleJob(): Promise<string> {
|
|
358
|
+
await this.jobCoordinator.commitJob(this.currentJobId);
|
|
359
|
+
this.currentJobId = this.jobCoordinator.beginJob();
|
|
360
|
+
return this.currentJobId;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private resetLastCall(): void {
|
|
364
|
+
const notQueriedMessageCount = this.lastCallInfo.queried
|
|
365
|
+
? 0
|
|
366
|
+
: this.lastCallInfo.offchainEffects.filter(payload => payload[0]?.equals(OFFCHAIN_MESSAGE_IDENTIFIER)).length;
|
|
367
|
+
if (notQueriedMessageCount > 0) {
|
|
368
|
+
this.logger.warn(
|
|
369
|
+
`Dropping ${notQueriedMessageCount} unqueried offchain message(s) from the previous top-level call. ` +
|
|
370
|
+
`To deliver them, call \`env.offchain_messages()\` and forward the result to the recipient contract's ` +
|
|
371
|
+
`\`offchain_receive\` utility before issuing another top-level call. To intentionally discard, assign ` +
|
|
372
|
+
`to \`let _ = env.offchain_messages()\` to silence this warning.`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
this.lastCallInfo = emptyLastCallState();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
recordOffchainEffect(data: Fr[]): void {
|
|
379
|
+
this.lastCallInfo.offchainEffects.push(data);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private setLastCallContext(txHash: Fr, anchorBlockTimestamp: bigint): void {
|
|
383
|
+
this.lastCallInfo.txHash = txHash;
|
|
384
|
+
this.lastCallInfo.anchorBlockTimestamp = anchorBlockTimestamp;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async withTopLevelCallTracking<T>(work: () => Promise<{ result: T; txHash?: Fr }>): Promise<T> {
|
|
388
|
+
this.resetLastCall();
|
|
389
|
+
// Capture the anchor *before* `work` runs: private/public executor calls mine a new block as a
|
|
390
|
+
// side effect, and that block's timestamp should not be attributed to this call's anchor.
|
|
391
|
+
const anchorBlockTimestamp = (await this.stateMachine.node.getBlockHeader('latest'))!.globalVariables.timestamp;
|
|
392
|
+
const { result, txHash } = await work();
|
|
393
|
+
this.setLastCallContext(txHash ?? Fr.ZERO, anchorBlockTimestamp);
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
getLastCallOffchainEffects(): { effects: Fr[][] } {
|
|
398
|
+
this.lastCallInfo.queried = true;
|
|
399
|
+
return { effects: this.lastCallInfo.offchainEffects };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
getLastCallContext(): { txHash: Fr; anchorBlockTimestamp: bigint } {
|
|
403
|
+
const { txHash, anchorBlockTimestamp } = this.lastCallInfo;
|
|
404
|
+
return { txHash, anchorBlockTimestamp };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
setTxeOracleVersion(version: { major: number; minor: number }): void {
|
|
408
|
+
this.txeOracleVersion = version;
|
|
409
|
+
this.logger.debug(`Test compiled with test oracle version ${version.major}.${version.minor}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
260
412
|
async enterTopLevelState() {
|
|
261
413
|
switch (this.state.name) {
|
|
262
414
|
case 'PRIVATE': {
|
|
@@ -280,8 +432,7 @@ export class TXESession implements TXESessionStateHandler {
|
|
|
280
432
|
}
|
|
281
433
|
|
|
282
434
|
// Commit all staged stores from the job that was just completed, then begin a new job
|
|
283
|
-
await this.
|
|
284
|
-
this.currentJobId = this.jobCoordinator.beginJob();
|
|
435
|
+
await this.cycleJob();
|
|
285
436
|
|
|
286
437
|
this.oracleHandler = new TXEOracleTopLevelContext(
|
|
287
438
|
this.stateMachine,
|
|
@@ -295,7 +446,6 @@ export class TXESession implements TXESessionStateHandler {
|
|
|
295
446
|
this.senderAddressBookStore,
|
|
296
447
|
this.capsuleStore,
|
|
297
448
|
this.privateEventStore,
|
|
298
|
-
this.currentJobId,
|
|
299
449
|
this.nextBlockTimestamp,
|
|
300
450
|
this.version,
|
|
301
451
|
this.chainId,
|
|
@@ -308,21 +458,21 @@ export class TXESession implements TXESessionStateHandler {
|
|
|
308
458
|
|
|
309
459
|
async enterPrivateState(
|
|
310
460
|
contractAddress: AztecAddress = DEFAULT_ADDRESS,
|
|
311
|
-
anchorBlockNumber
|
|
461
|
+
anchorBlockNumber: BlockNumber | undefined,
|
|
462
|
+
gasSettings: GasSettings,
|
|
312
463
|
): Promise<PrivateContextInputs> {
|
|
313
464
|
this.exitTopLevelState();
|
|
314
|
-
|
|
315
|
-
await new NoteService(
|
|
316
|
-
this.noteStore,
|
|
317
|
-
this.stateMachine.node,
|
|
318
|
-
this.stateMachine.anchorBlockStore,
|
|
319
|
-
this.currentJobId,
|
|
320
|
-
).syncNoteNullifiers(contractAddress);
|
|
465
|
+
this.resetLastCall();
|
|
321
466
|
|
|
322
467
|
// Private execution has two associated block numbers: the anchor block (i.e. the historical block that is used to
|
|
323
468
|
// build the proof), and the *next* block, i.e. the one we'll create once the execution ends, and which will contain
|
|
324
469
|
// a single transaction with the effects of what was done in the test.
|
|
325
470
|
const anchorBlock = await this.stateMachine.node.getBlockHeader(anchorBlockNumber ?? 'latest');
|
|
471
|
+
|
|
472
|
+
await new NoteService(this.noteStore, this.stateMachine.node, anchorBlock!, this.currentJobId).syncNoteNullifiers(
|
|
473
|
+
contractAddress,
|
|
474
|
+
await this.keyStore.getAccounts(),
|
|
475
|
+
);
|
|
326
476
|
const latestBlock = await this.stateMachine.node.getBlockHeader('latest');
|
|
327
477
|
|
|
328
478
|
const nextBlockGlobalVariables = makeGlobalVariables(undefined, {
|
|
@@ -338,30 +488,34 @@ export class TXESession implements TXESessionStateHandler {
|
|
|
338
488
|
const taggingIndexCache = new ExecutionTaggingIndexCache();
|
|
339
489
|
|
|
340
490
|
const utilityExecutor = this.utilityExecutorForContractSync(anchorBlock);
|
|
341
|
-
this.oracleHandler = new PrivateExecutionOracle(
|
|
342
|
-
Fr.ZERO,
|
|
343
|
-
new TxContext(this.chainId, this.version,
|
|
344
|
-
new CallContext(AztecAddress.ZERO, contractAddress, FunctionSelector.empty(), false),
|
|
345
|
-
anchorBlock!,
|
|
491
|
+
this.oracleHandler = new PrivateExecutionOracle({
|
|
492
|
+
argsHash: Fr.ZERO,
|
|
493
|
+
txContext: new TxContext(this.chainId, this.version, gasSettings),
|
|
494
|
+
callContext: new CallContext(AztecAddress.ZERO, contractAddress, FunctionSelector.empty(), false),
|
|
495
|
+
anchorBlockHeader: anchorBlock!,
|
|
346
496
|
utilityExecutor,
|
|
347
|
-
[],
|
|
348
|
-
[],
|
|
349
|
-
new HashedValuesCache(),
|
|
497
|
+
authWitnesses: [],
|
|
498
|
+
capsules: [],
|
|
499
|
+
executionCache: new HashedValuesCache(),
|
|
350
500
|
noteCache,
|
|
351
501
|
taggingIndexCache,
|
|
352
|
-
this.contractStore,
|
|
353
|
-
this.noteStore,
|
|
354
|
-
this.keyStore,
|
|
355
|
-
this.addressStore,
|
|
356
|
-
this.stateMachine.node,
|
|
357
|
-
this.
|
|
358
|
-
this.
|
|
359
|
-
this.
|
|
360
|
-
this.
|
|
361
|
-
this.
|
|
362
|
-
this.
|
|
363
|
-
this.
|
|
364
|
-
|
|
502
|
+
contractStore: this.contractStore,
|
|
503
|
+
noteStore: this.noteStore,
|
|
504
|
+
keyStore: this.keyStore,
|
|
505
|
+
addressStore: this.addressStore,
|
|
506
|
+
aztecNode: this.stateMachine.node,
|
|
507
|
+
senderTaggingStore: this.senderTaggingStore,
|
|
508
|
+
recipientTaggingStore: this.recipientTaggingStore,
|
|
509
|
+
senderAddressBookStore: this.senderAddressBookStore,
|
|
510
|
+
capsuleService: new CapsuleService(this.capsuleStore, await this.keyStore.getAccounts()),
|
|
511
|
+
privateEventStore: this.privateEventStore,
|
|
512
|
+
contractSyncService: this.stateMachine.contractSyncService,
|
|
513
|
+
l2TipsStore: this.stateMachine.node,
|
|
514
|
+
jobId: this.currentJobId,
|
|
515
|
+
scopes: await this.keyStore.getAccounts(),
|
|
516
|
+
messageContextService: this.stateMachine.messageContextService,
|
|
517
|
+
simulator: new WASMSimulator(),
|
|
518
|
+
});
|
|
365
519
|
|
|
366
520
|
// We store the note and tagging index caches fed into the PrivateExecutionOracle (along with some other auxiliary
|
|
367
521
|
// data) in order to refer to it later, mimicking the way this object is used by the ContractFunctionSimulator. The
|
|
@@ -371,17 +525,22 @@ export class TXESession implements TXESessionStateHandler {
|
|
|
371
525
|
this.state = { name: 'PRIVATE', nextBlockGlobalVariables, noteCache, taggingIndexCache };
|
|
372
526
|
this.logger.debug(`Entered state ${this.state.name}`);
|
|
373
527
|
|
|
528
|
+
// Record the *resolved* anchor's timestamp — if the caller pinned the anchor to a past block
|
|
529
|
+
// via `anchorBlockNumber`, "latest" would be the wrong anchor for offchain-message semantics.
|
|
530
|
+
this.setLastCallContext(Fr.ZERO, anchorBlock!.globalVariables.timestamp);
|
|
531
|
+
|
|
374
532
|
return (this.oracleHandler as PrivateExecutionOracle).getPrivateContextInputs();
|
|
375
533
|
}
|
|
376
534
|
|
|
377
535
|
async enterPublicState(contractAddress?: AztecAddress) {
|
|
378
536
|
this.exitTopLevelState();
|
|
537
|
+
this.resetLastCall();
|
|
379
538
|
|
|
380
539
|
// The PublicContext will create a block with a single transaction in it, containing the effects of what was done in
|
|
381
540
|
// the test. The block therefore gets the *next* block number and timestamp.
|
|
382
|
-
const
|
|
541
|
+
const latestHeader = (await this.stateMachine.node.getBlockHeader('latest'))!;
|
|
383
542
|
const globalVariables = makeGlobalVariables(undefined, {
|
|
384
|
-
blockNumber: BlockNumber(
|
|
543
|
+
blockNumber: BlockNumber(latestHeader.globalVariables.blockNumber + 1),
|
|
385
544
|
timestamp: this.nextBlockTimestamp,
|
|
386
545
|
version: this.version,
|
|
387
546
|
chainId: this.chainId,
|
|
@@ -396,10 +555,16 @@ export class TXESession implements TXESessionStateHandler {
|
|
|
396
555
|
|
|
397
556
|
this.state = { name: 'PUBLIC' };
|
|
398
557
|
this.logger.debug(`Entered state ${this.state.name}`);
|
|
558
|
+
|
|
559
|
+
// Public state is anchored at the latest block.
|
|
560
|
+
this.setLastCallContext(Fr.ZERO, latestHeader.globalVariables.timestamp);
|
|
399
561
|
}
|
|
400
562
|
|
|
401
563
|
async enterUtilityState(contractAddress: AztecAddress = DEFAULT_ADDRESS) {
|
|
402
564
|
this.exitTopLevelState();
|
|
565
|
+
this.resetLastCall();
|
|
566
|
+
|
|
567
|
+
const anchorBlockHeader = await this.stateMachine.anchorBlockStore.getBlockHeader();
|
|
403
568
|
|
|
404
569
|
// There is no automatic message discovery and contract-driven syncing process in inlined private or utility
|
|
405
570
|
// contexts, which means that known nullifiers are also not searched for, since it is during the tagging sync that
|
|
@@ -409,32 +574,38 @@ export class TXESession implements TXESessionStateHandler {
|
|
|
409
574
|
await new NoteService(
|
|
410
575
|
this.noteStore,
|
|
411
576
|
this.stateMachine.node,
|
|
412
|
-
|
|
577
|
+
anchorBlockHeader,
|
|
413
578
|
this.currentJobId,
|
|
414
|
-
).syncNoteNullifiers(contractAddress);
|
|
415
|
-
|
|
416
|
-
const anchorBlockHeader = await this.stateMachine.anchorBlockStore.getBlockHeader();
|
|
579
|
+
).syncNoteNullifiers(contractAddress, await this.keyStore.getAccounts());
|
|
417
580
|
|
|
418
|
-
this.oracleHandler = new UtilityExecutionOracle(
|
|
581
|
+
this.oracleHandler = new UtilityExecutionOracle({
|
|
419
582
|
contractAddress,
|
|
420
|
-
[],
|
|
421
|
-
[],
|
|
583
|
+
authWitnesses: [],
|
|
584
|
+
capsules: [],
|
|
422
585
|
anchorBlockHeader,
|
|
423
|
-
this.contractStore,
|
|
424
|
-
this.noteStore,
|
|
425
|
-
this.keyStore,
|
|
426
|
-
this.addressStore,
|
|
427
|
-
this.stateMachine.node,
|
|
428
|
-
this.
|
|
429
|
-
this.
|
|
430
|
-
this.
|
|
431
|
-
this.
|
|
432
|
-
this.
|
|
433
|
-
this.
|
|
434
|
-
|
|
586
|
+
contractStore: this.contractStore,
|
|
587
|
+
noteStore: this.noteStore,
|
|
588
|
+
keyStore: this.keyStore,
|
|
589
|
+
addressStore: this.addressStore,
|
|
590
|
+
aztecNode: this.stateMachine.node,
|
|
591
|
+
recipientTaggingStore: this.recipientTaggingStore,
|
|
592
|
+
senderAddressBookStore: this.senderAddressBookStore,
|
|
593
|
+
capsuleService: new CapsuleService(this.capsuleStore, await this.keyStore.getAccounts()),
|
|
594
|
+
privateEventStore: this.privateEventStore,
|
|
595
|
+
messageContextService: this.stateMachine.messageContextService,
|
|
596
|
+
contractSyncService: this.stateMachine.contractSyncService,
|
|
597
|
+
l2TipsStore: this.stateMachine.node,
|
|
598
|
+
jobId: this.currentJobId,
|
|
599
|
+
scopes: await this.keyStore.getAccounts(),
|
|
600
|
+
simulator: new WASMSimulator(),
|
|
601
|
+
utilityExecutor: this.utilityExecutorForContractSync(anchorBlockHeader),
|
|
602
|
+
});
|
|
435
603
|
|
|
436
604
|
this.state = { name: 'UTILITY' };
|
|
437
605
|
this.logger.debug(`Entered state ${this.state.name}`);
|
|
606
|
+
|
|
607
|
+
// Utility state anchors at whatever the anchor block store is pointing to (tracked as latest).
|
|
608
|
+
this.setLastCallContext(Fr.ZERO, anchorBlockHeader.globalVariables.timestamp);
|
|
438
609
|
}
|
|
439
610
|
|
|
440
611
|
private exitTopLevelState() {
|
|
@@ -444,8 +615,8 @@ export class TXESession implements TXESessionStateHandler {
|
|
|
444
615
|
|
|
445
616
|
// Note that while all public and private contexts do is build a single block that we then process when exiting
|
|
446
617
|
// those, the top level context performs a large number of actions not captured in the following 'close' call. Among
|
|
447
|
-
// others, it will create empty blocks (via `
|
|
448
|
-
// `
|
|
618
|
+
// others, it will create empty blocks (via `advanceBlocksBy` and `deploy`), create blocks with transactions via
|
|
619
|
+
// `privateCallNewFlow` and `publicCallNewFlow`, add accounts to PXE via `addAccount`, etc. This is a
|
|
449
620
|
// slight inconsistency in the working model of this class, but is not too bad.
|
|
450
621
|
// TODO: it's quite unfortunate that we need to capture the authwits created to later pass them again when the top
|
|
451
622
|
// level context is re-created. This is because authwits create a temporary utility context that'd otherwise reset
|
|
@@ -499,31 +670,37 @@ export class TXESession implements TXESessionStateHandler {
|
|
|
499
670
|
}
|
|
500
671
|
|
|
501
672
|
private utilityExecutorForContractSync(anchorBlock: any) {
|
|
502
|
-
return async (call: FunctionCall) => {
|
|
673
|
+
return async (call: FunctionCall, scopes: AztecAddress[]) => {
|
|
503
674
|
const entryPointArtifact = await this.contractStore.getFunctionArtifactWithDebugMetadata(call.to, call.selector);
|
|
504
675
|
if (entryPointArtifact.functionType !== FunctionType.UTILITY) {
|
|
505
676
|
throw new Error(`Cannot run ${entryPointArtifact.functionType} function as utility`);
|
|
506
677
|
}
|
|
507
678
|
|
|
508
679
|
try {
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
[],
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
this.
|
|
516
|
-
this.
|
|
517
|
-
this.
|
|
518
|
-
this.
|
|
519
|
-
this.stateMachine.
|
|
520
|
-
this.recipientTaggingStore,
|
|
521
|
-
this.senderAddressBookStore,
|
|
522
|
-
this.capsuleStore,
|
|
523
|
-
this.privateEventStore,
|
|
524
|
-
this.
|
|
525
|
-
|
|
526
|
-
|
|
680
|
+
const simulator = new WASMSimulator();
|
|
681
|
+
const oracle = new UtilityExecutionOracle({
|
|
682
|
+
contractAddress: call.to,
|
|
683
|
+
authWitnesses: [],
|
|
684
|
+
capsules: [],
|
|
685
|
+
anchorBlockHeader: anchorBlock!,
|
|
686
|
+
contractStore: this.contractStore,
|
|
687
|
+
noteStore: this.noteStore,
|
|
688
|
+
keyStore: this.keyStore,
|
|
689
|
+
addressStore: this.addressStore,
|
|
690
|
+
aztecNode: this.stateMachine.node,
|
|
691
|
+
recipientTaggingStore: this.recipientTaggingStore,
|
|
692
|
+
senderAddressBookStore: this.senderAddressBookStore,
|
|
693
|
+
capsuleService: new CapsuleService(this.capsuleStore, scopes),
|
|
694
|
+
privateEventStore: this.privateEventStore,
|
|
695
|
+
messageContextService: this.stateMachine.messageContextService,
|
|
696
|
+
contractSyncService: this.stateMachine.contractSyncService,
|
|
697
|
+
l2TipsStore: this.stateMachine.node,
|
|
698
|
+
jobId: this.currentJobId,
|
|
699
|
+
scopes,
|
|
700
|
+
simulator,
|
|
701
|
+
utilityExecutor: this.utilityExecutorForContractSync(anchorBlock),
|
|
702
|
+
});
|
|
703
|
+
await simulator
|
|
527
704
|
.executeUserCircuit(toACVMWitness(0, call.args), entryPointArtifact, new Oracle(oracle).toACIRCallback())
|
|
528
705
|
.catch((err: Error) => {
|
|
529
706
|
err.message = resolveAssertionMessageFromError(err, entryPointArtifact);
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import { BlockNumber } from '@aztec/foundation/branded-types';
|
|
2
2
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
3
3
|
import type { ContractStore } from '@aztec/pxe/server';
|
|
4
|
-
import { type ContractArtifact, FunctionSelector
|
|
4
|
+
import { type ContractArtifact, FunctionSelector } from '@aztec/stdlib/abi';
|
|
5
5
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
6
|
-
import {
|
|
7
|
-
type ContractClassPublic,
|
|
8
|
-
type ContractDataSource,
|
|
9
|
-
type ContractInstanceWithAddress,
|
|
10
|
-
computePrivateFunctionsRoot,
|
|
11
|
-
computePublicBytecodeCommitment,
|
|
12
|
-
getContractClassPrivateFunctionFromArtifact,
|
|
13
|
-
} from '@aztec/stdlib/contract';
|
|
6
|
+
import type { ContractClassPublic, ContractDataSource, ContractInstanceWithAddress } from '@aztec/stdlib/contract';
|
|
14
7
|
|
|
15
8
|
export class TXEPublicContractDataSource implements ContractDataSource {
|
|
16
|
-
#privateFunctionsRoot: Map<string, Buffer> = new Map();
|
|
17
9
|
constructor(
|
|
18
10
|
private blockNumber: BlockNumber,
|
|
19
11
|
private contractStore: ContractStore,
|
|
@@ -24,42 +16,24 @@ export class TXEPublicContractDataSource implements ContractDataSource {
|
|
|
24
16
|
}
|
|
25
17
|
|
|
26
18
|
async getContractClass(id: Fr): Promise<ContractClassPublic | undefined> {
|
|
27
|
-
const contractClass = await this.contractStore.
|
|
19
|
+
const contractClass = await this.contractStore.getContractClassWithPreimage(id);
|
|
28
20
|
if (!contractClass) {
|
|
29
21
|
return;
|
|
30
22
|
}
|
|
31
|
-
const artifact = await this.contractStore.getContractArtifact(id);
|
|
32
|
-
if (!artifact) {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let privateFunctionsRoot;
|
|
37
|
-
if (!this.#privateFunctionsRoot.has(id.toString())) {
|
|
38
|
-
const privateFunctions = await Promise.all(
|
|
39
|
-
artifact.functions
|
|
40
|
-
.filter(fn => fn.functionType === FunctionType.PRIVATE)
|
|
41
|
-
.map(fn => getContractClassPrivateFunctionFromArtifact(fn)),
|
|
42
|
-
);
|
|
43
|
-
privateFunctionsRoot = await computePrivateFunctionsRoot(privateFunctions);
|
|
44
|
-
this.#privateFunctionsRoot.set(id.toString(), privateFunctionsRoot.toBuffer());
|
|
45
|
-
} else {
|
|
46
|
-
privateFunctionsRoot = Fr.fromBuffer(this.#privateFunctionsRoot.get(id.toString())!);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
23
|
return {
|
|
50
|
-
id,
|
|
51
|
-
artifactHash: contractClass
|
|
52
|
-
packedBytecode: contractClass
|
|
53
|
-
privateFunctionsRoot,
|
|
54
|
-
version: contractClass
|
|
24
|
+
id: contractClass.id,
|
|
25
|
+
artifactHash: contractClass.artifactHash,
|
|
26
|
+
packedBytecode: contractClass.packedBytecode,
|
|
27
|
+
privateFunctionsRoot: contractClass.privateFunctionsRoot,
|
|
28
|
+
version: contractClass.version,
|
|
55
29
|
privateFunctions: [],
|
|
56
30
|
utilityFunctions: [],
|
|
57
31
|
};
|
|
58
32
|
}
|
|
59
33
|
|
|
60
34
|
async getBytecodeCommitment(id: Fr): Promise<Fr | undefined> {
|
|
61
|
-
const contractClass = await this.contractStore.
|
|
62
|
-
return contractClass
|
|
35
|
+
const contractClass = await this.contractStore.getContractClassWithPreimage(id);
|
|
36
|
+
return contractClass?.publicBytecodeCommitment;
|
|
63
37
|
}
|
|
64
38
|
|
|
65
39
|
async getContract(address: AztecAddress): Promise<ContractInstanceWithAddress | undefined> {
|
|
@@ -87,7 +87,9 @@ export async function makeTXEBlock(
|
|
|
87
87
|
const newArchiveInfo = await worldTrees.getTreeInfo(MerkleTreeId.ARCHIVE);
|
|
88
88
|
const newArchive = new AppendOnlyTreeSnapshot(new Fr(newArchiveInfo.root), Number(newArchiveInfo.size));
|
|
89
89
|
|
|
90
|
-
// L2Block requires checkpointNumber and indexWithinCheckpoint
|
|
90
|
+
// L2Block requires checkpointNumber and indexWithinCheckpoint.
|
|
91
|
+
// TXE uses 1-block-per-checkpoint for testing simplicity, so we can use block number as checkpoint number.
|
|
92
|
+
// This uses the deprecated fromBlockNumber method intentionally for the TXE testing environment.
|
|
91
93
|
const checkpointNumber = CheckpointNumber.fromBlockNumber(globalVariables.blockNumber);
|
|
92
94
|
const indexWithinCheckpoint = IndexWithinCheckpoint(0);
|
|
93
95
|
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { ContractArtifact } from '@aztec/aztec.js/abi';
|
|
2
|
-
import { Fr } from '@aztec/aztec.js/fields';
|
|
3
|
-
import { ContractStore } from '@aztec/pxe/server';
|
|
4
|
-
export type ContractArtifactWithHash = ContractArtifact & {
|
|
5
|
-
artifactHash: Fr;
|
|
6
|
-
};
|
|
7
|
-
export declare class TXEContractStore extends ContractStore {
|
|
8
|
-
#private;
|
|
9
|
-
addContractArtifact(id: Fr, artifact: ContractArtifact | ContractArtifactWithHash): Promise<void>;
|
|
10
|
-
getContractArtifact(contractClassId: Fr): Promise<ContractArtifact | ContractArtifactWithHash | undefined>;
|
|
11
|
-
}
|
|
12
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHhlX2NvbnRyYWN0X3N0b3JlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvdXRpbC90eGVfY29udHJhY3Rfc3RvcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUM1RCxPQUFPLEVBQUUsRUFBRSxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDNUMsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBRWxELE1BQU0sTUFBTSx3QkFBd0IsR0FBRyxnQkFBZ0IsR0FBRztJQUFFLFlBQVksRUFBRSxFQUFFLENBQUE7Q0FBRSxDQUFDO0FBTy9FLHFCQUFhLGdCQUFpQixTQUFRLGFBQWE7O0lBRzNCLG1CQUFtQixDQUN2QyxFQUFFLEVBQUUsRUFBRSxFQUNOLFFBQVEsRUFBRSxnQkFBZ0IsR0FBRyx3QkFBd0IsR0FDcEQsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUtmO0lBRXFCLG1CQUFtQixDQUN2QyxlQUFlLEVBQUUsRUFBRSxHQUNsQixPQUFPLENBQUMsZ0JBQWdCLEdBQUcsd0JBQXdCLEdBQUcsU0FBUyxDQUFDLENBUWxFO0NBQ0YifQ==
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"txe_contract_store.d.ts","sourceRoot":"","sources":["../../src/util/txe_contract_store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,EAAE,EAAE,MAAM,wBAAwB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,MAAM,MAAM,wBAAwB,GAAG,gBAAgB,GAAG;IAAE,YAAY,EAAE,EAAE,CAAA;CAAE,CAAC;AAO/E,qBAAa,gBAAiB,SAAQ,aAAa;;IAG3B,mBAAmB,CACvC,EAAE,EAAE,EAAE,EACN,QAAQ,EAAE,gBAAgB,GAAG,wBAAwB,GACpD,OAAO,CAAC,IAAI,CAAC,CAKf;IAEqB,mBAAmB,CACvC,eAAe,EAAE,EAAE,GAClB,OAAO,CAAC,gBAAgB,GAAG,wBAAwB,GAAG,SAAS,CAAC,CAQlE;CACF"}
|