Skip to content

Commit fa66e7c

Browse files
authored
refactor(txe): normalize deploy and addAccount to oracle registry (#23536)
1 parent 3b5f8b6 commit fa66e7c

9 files changed

Lines changed: 340 additions & 245 deletions

yarn-project/txe/src/index.ts

Lines changed: 15 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,12 @@
1-
import { type NoirCompiledContract, loadContractArtifact } from '@aztec/aztec.js/abi';
2-
import { AztecAddress } from '@aztec/aztec.js/addresses';
3-
import {
4-
type ContractInstanceWithAddress,
5-
getContractInstanceFromInstantiationParams,
6-
} from '@aztec/aztec.js/contracts';
71
import { Fr } from '@aztec/aztec.js/fields';
8-
import { PublicKeys, deriveKeys } from '@aztec/aztec.js/keys';
92
import type { Logger } from '@aztec/foundation/log';
103
import { cloneEphemeralStoreFrom } from '@aztec/kv-store/lmdb-v2';
114
import type { ProtocolContractName } from '@aztec/protocol-contracts';
125
import { ContractStore } from '@aztec/pxe/client/lazy';
13-
import { computeArtifactHash } from '@aztec/stdlib/contract';
14-
import type { ContractArtifactWithHash } from '@aztec/stdlib/contract';
156
import type { ApiSchemaFor } from '@aztec/stdlib/schemas';
167
import { zodFor } from '@aztec/stdlib/schemas';
178

18-
import { createHash } from 'crypto';
19-
import { createReadStream } from 'fs';
20-
import { readFile, readdir } from 'fs/promises';
21-
import { join, parse } from 'path';
9+
import { join } from 'path';
2210
import { z } from 'zod';
2311

2412
// Side-effect import: registers the msgpackr Fr extension for the bundled `Fr` class. Must
@@ -28,64 +16,17 @@ import { type TXEOracleFunctionName, TXESession } from './txe_session.js';
2816
import {
2917
type ForeignCallArgs,
3018
ForeignCallArgsSchema,
31-
type ForeignCallArray,
3219
type ForeignCallResult,
3320
ForeignCallResultSchema,
34-
type ForeignCallSingle,
35-
addressFromSingle,
36-
fromArray,
37-
fromSingle,
38-
toSingle,
3921
} from './utils/encoding.js';
22+
import { TXEArtifactResolver } from './utils/txe_artifact_resolver.js';
4023

4124
// Protocol contracts TXE registers in its contract store. Only AuthRegistry is needed for the
4225
// current test suites; add a contract here if a lookup against a `0x000…00X` address fails.
4326
export const TXE_REQUIRED_PROTOCOL_CONTRACTS: ProtocolContractName[] = [];
4427

4528
const sessions = new Map<number, TXESession>();
4629

47-
/**
48-
* Cache + in-flight map pair. Lookup hits the cache, then awaits an in-flight `compute()` if one
49-
* exists, otherwise starts one and stores it. Guarantees `compute()` runs at most once per `key`
50-
* across concurrent callers, which matters because `computeArtifactHash` is expensive.
51-
*/
52-
class AsyncCache<K, V> {
53-
private readonly cache = new Map<K, V>();
54-
private readonly inFlight = new Map<K, Promise<V>>();
55-
56-
getOrCompute(key: K, compute: () => Promise<V>): Promise<V> {
57-
const cached = this.cache.get(key);
58-
if (cached !== undefined) {
59-
return Promise.resolve(cached);
60-
}
61-
let pending = this.inFlight.get(key);
62-
if (!pending) {
63-
pending = (async () => {
64-
try {
65-
const value = await compute();
66-
this.cache.set(key, value);
67-
return value;
68-
} finally {
69-
this.inFlight.delete(key);
70-
}
71-
})();
72-
this.inFlight.set(key, pending);
73-
}
74-
return pending;
75-
}
76-
}
77-
78-
// Full deploys (artifact + computed instance), keyed by the full deploy context (contract +
79-
// constructor args + publicKeys + salt + deployer). Hits on repeated identical deploys.
80-
const TXEDeploymentsCache = new AsyncCache<
81-
string,
82-
{ artifact: ContractArtifactWithHash; instance: ContractInstanceWithAddress }
83-
>();
84-
85-
// Loaded + hashed contract artifact, keyed by compiled-bytecode hash. Hits across deploys of the
86-
// same contract when constructor args / salt / deployer differ.
87-
const TXEArtifactsCache = new AsyncCache<string, ContractArtifactWithHash>();
88-
8930
export type TXEForeignCallInput = {
9031
session_id: number;
9132
function: TXEOracleFunctionName;
@@ -119,15 +60,16 @@ export interface TXEDispatcherOptions {
11960
*/
12061
contractStoreSourceDir: string;
12162
/**
122-
* Class id (hex) of the SchnorrAccount artifact pre-registered in the shared LMDB. When set,
123-
* `#processAddAccountInputs` looks the artifact up from the cloned store instead of
124-
* recomputing it via `getSchnorrAccountContractArtifact()` + `computeArtifactHash()`.
63+
* Class id (hex) of the SchnorrAccount artifact pre-registered in the shared LMDB. The
64+
* {@link TXEArtifactResolver} looks the artifact up from the cloned store via this class id
65+
* instead of recomputing it via `getSchnorrAccountContractArtifact()` + `computeArtifactHash()`.
12566
*/
12667
schnorrClassId: string;
12768
}
12869

12970
export class TXEDispatcher {
13071
private contractStore!: ContractStore;
72+
private artifactResolver!: TXEArtifactResolver;
13173
private readonly contractStoreSourceDir: string;
13274
private readonly schnorrClassId: Fr;
13375

@@ -156,146 +98,25 @@ export class TXEDispatcher {
15698
2,
15799
);
158100
this.contractStore = new ContractStore(kvStore);
101+
this.artifactResolver = new TXEArtifactResolver(this.contractStore, this.schnorrClassId);
159102
this.logger.debug('Cloned shared protocol-contracts store', { totalMs: Date.now() - t0 });
160103
}
161104

162-
private fastHashFile(path: string): Promise<string> {
163-
return new Promise(resolve => {
164-
const fd = createReadStream(path);
165-
const hash = createHash('sha1');
166-
hash.setEncoding('hex');
167-
168-
fd.on('end', function () {
169-
hash.end();
170-
resolve(hash.read() as string);
171-
});
172-
173-
fd.pipe(hash);
174-
});
175-
}
176-
177-
async #processDeployInputs({ inputs, root_path: rootPath, package_name: packageName }: TXEForeignCallInput) {
178-
const [contractPath, initializer] = inputs.slice(0, 2).map(input =>
179-
fromArray(input as ForeignCallArray)
180-
.map(char => String.fromCharCode(char.toNumber()))
181-
.join(''),
182-
);
183-
184-
const decodedArgs = fromArray(inputs[3] as ForeignCallArray);
185-
const secret = fromSingle(inputs[4] as ForeignCallSingle);
186-
const salt = fromSingle(inputs[5] as ForeignCallSingle);
187-
const deployer = addressFromSingle(inputs[6] as ForeignCallSingle);
188-
const publicKeys = secret.equals(Fr.ZERO) ? PublicKeys.default() : (await deriveKeys(secret)).publicKeys;
189-
const publicKeysHash = await publicKeys.hash();
190-
191-
let artifactPath = '';
192-
const { dir: contractDirectory, base: contractFilename } = parse(contractPath);
193-
if (contractDirectory) {
194-
if (contractDirectory.includes('@')) {
195-
// We're deploying a contract that belongs in a workspace
196-
// env.deploy("../path/to/workspace/root@packageName/contractName")
197-
const [workspace, pkg] = contractDirectory.split('@');
198-
const targetPath = join(rootPath, workspace, '/target');
199-
this.logger.debug(`Looking for compiled artifact in workspace ${targetPath}`);
200-
artifactPath = join(targetPath, `${pkg}-${contractFilename}.json`);
201-
} else {
202-
// We're deploying a standalone external contract
203-
// env.deploy("../path/to/contract/root/contractName")
204-
const targetPath = join(rootPath, contractDirectory, '/target');
205-
this.logger.debug(`Looking for compiled artifact in ${targetPath}`);
206-
[artifactPath] = (await readdir(targetPath)).filter(file => file.endsWith(`-${contractFilename}.json`));
207-
}
208-
} else {
209-
// We're deploying a local contract
210-
// env.deploy("contractName")
211-
artifactPath = join(rootPath, './target', `${packageName}-${contractFilename}.json`);
212-
}
213-
214-
const fileHash = await this.fastHashFile(artifactPath);
215-
216-
const cacheKey = `${contractDirectory ?? ''}-${contractFilename}-${initializer}-${decodedArgs
217-
.map(arg => arg.toString())
218-
.join('-')}-${publicKeysHash}-${salt}-${deployer}-${fileHash}`;
219-
220-
const { artifact, instance } = await TXEDeploymentsCache.getOrCompute(cacheKey, async () => {
221-
this.logger.debug(`Loading compiled artifact ${artifactPath}`);
222-
// Inner cache: artifact load + hash depends only on the compiled bytecode (`fileHash`), so
223-
// subsequent deploys of the same contract — regardless of constructor args / deployer /
224-
// salt — reuse the same `ContractArtifactWithHash`.
225-
const computedArtifact = await TXEArtifactsCache.getOrCompute(fileHash, async () => {
226-
const artifactJSON = JSON.parse(await readFile(artifactPath, 'utf-8')) as NoirCompiledContract;
227-
const artifactWithoutHash = loadContractArtifact(artifactJSON);
228-
return { ...artifactWithoutHash, artifactHash: await computeArtifactHash(artifactWithoutHash) };
229-
});
230-
this.logger.debug(
231-
`Deploy ${computedArtifact.name} with initializer ${initializer}(${decodedArgs}) and public keys hash ${publicKeysHash.toString()}`,
232-
);
233-
const computedInstance = await getContractInstanceFromInstantiationParams(computedArtifact, {
234-
constructorArgs: decodedArgs,
235-
skipArgsDecoding: true,
236-
salt,
237-
publicKeys,
238-
constructorArtifact: initializer ? initializer : undefined,
239-
deployer,
240-
});
241-
return { artifact: computedArtifact, instance: computedInstance };
242-
});
243-
244-
inputs.splice(0, 1, artifact, instance, toSingle(secret));
245-
}
246-
247-
async #processAddAccountInputs({ inputs }: TXEForeignCallInput) {
248-
const secret = fromSingle(inputs[0] as ForeignCallSingle);
249-
250-
const cacheKey = `SchnorrAccountContract-${secret}`;
251-
252-
const { artifact, instance } = await TXEDeploymentsCache.getOrCompute(cacheKey, async () => {
253-
const [artifactFromStore, classWithPreimage] = await Promise.all([
254-
this.contractStore.getContractArtifact(this.schnorrClassId),
255-
this.contractStore.getContractClassWithPreimage(this.schnorrClassId),
256-
]);
257-
if (!artifactFromStore || !classWithPreimage) {
258-
throw new Error(
259-
`SchnorrAccount not found in shared contract store at class id ${this.schnorrClassId.toString()}`,
260-
);
261-
}
262-
const computedArtifact = { ...artifactFromStore, artifactHash: classWithPreimage.artifactHash };
263-
const keys = await deriveKeys(secret);
264-
const args = [keys.publicKeys.ivpkM.x, keys.publicKeys.ivpkM.y];
265-
const computedInstance = await getContractInstanceFromInstantiationParams(computedArtifact, {
266-
constructorArgs: args,
267-
skipArgsDecoding: true,
268-
salt: Fr.ONE,
269-
publicKeys: keys.publicKeys,
270-
constructorArtifact: 'constructor',
271-
deployer: AztecAddress.ZERO,
272-
});
273-
return { artifact: computedArtifact, instance: computedInstance };
274-
});
275-
276-
inputs.splice(0, 0, artifact, instance);
277-
}
278-
279105
// eslint-disable-next-line camelcase
280106
async resolve_foreign_call(callData: TXEForeignCallInput): Promise<ForeignCallResult> {
281-
const { session_id: sessionId, function: functionName, inputs } = callData;
107+
const {
108+
session_id: sessionId,
109+
function: functionName,
110+
inputs,
111+
root_path: rootPath,
112+
package_name: packageName,
113+
} = callData;
282114
this.logger.debug(`Calling ${functionName} on session ${sessionId}`);
283115

284116
if (!sessions.has(sessionId)) {
285117
this.logger.debug(`Creating new session ${sessionId}`);
286118
await this.warmUp();
287-
sessions.set(sessionId, await TXESession.init(this.contractStore));
288-
}
289-
290-
switch (functionName) {
291-
case 'aztec_txe_deploy': {
292-
await this.#processDeployInputs(callData);
293-
break;
294-
}
295-
case 'aztec_txe_addAccount': {
296-
await this.#processAddAccountInputs(callData);
297-
break;
298-
}
119+
sessions.set(sessionId, await TXESession.init(this.contractStore, this.artifactResolver, rootPath, packageName));
299120
}
300121

301122
return await sessions.get(sessionId)!.processFunction(functionName, inputs);

yarn-project/txe/src/oracle/interfaces.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import type { ContractArtifact } from '@aztec/aztec.js/abi';
21
import { CompleteAddress } from '@aztec/aztec.js/addresses';
3-
import type { ContractInstanceWithAddress } from '@aztec/aztec.js/contracts';
42
import { TxHash } from '@aztec/aztec.js/tx';
53
import { BlockNumber } from '@aztec/foundation/branded-types';
64
import type { Fr } from '@aztec/foundation/curves/bn254';
@@ -60,9 +58,16 @@ export interface ITxeExecutionOracle {
6058
getNextBlockTimestamp(): Promise<UInt64>;
6159
advanceBlocksBy(blocks: number): Promise<void>;
6260
advanceTimestampBy(duration: UInt64): void;
63-
deploy(artifact: ContractArtifact, instance: ContractInstanceWithAddress, foreignSecret: Fr): Promise<void>;
61+
deploy(
62+
contractPath: string,
63+
initializer: string,
64+
args: Fr[],
65+
secret: Fr,
66+
salt: Fr,
67+
deployer: AztecAddress,
68+
): Promise<Fr[]>;
6469
createAccount(secret: Fr): Promise<CompleteAddress>;
65-
addAccount(artifact: ContractArtifact, instance: ContractInstanceWithAddress, secret: Fr): Promise<CompleteAddress>;
70+
addAccount(secret: Fr): Promise<CompleteAddress>;
6671
addAuthWitness(address: AztecAddress, messageHash: Fr): Promise<void>;
6772
getLastBlockTimestamp(): Promise<bigint>;
6873
getLastTxEffects(): Promise<{

yarn-project/txe/src/oracle/txe_oracle_registry.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
ORACLE_REGISTRY,
2323
type OracleRegistryEntry,
2424
type ParamTypes,
25+
STR,
2526
type TypeMapping,
2627
U32,
2728
makeEntry,
@@ -188,11 +189,29 @@ export const TXE_ORACLE_REGISTRY = {
188189
params: [{ name: 'duration', type: BIGINT }],
189190
}),
190191

192+
aztec_txe_deploy: makeEntry({
193+
params: [
194+
{ name: 'contractPath', type: STR },
195+
{ name: 'initializer', type: STR },
196+
{ name: 'argsLength', type: U32 },
197+
{ name: 'args', type: ARRAY(FIELD) },
198+
{ name: 'secret', type: FIELD },
199+
{ name: 'salt', type: FIELD },
200+
{ name: 'deployer', type: AZTEC_ADDRESS },
201+
],
202+
returnType: ARRAY(FIELD),
203+
}),
204+
191205
aztec_txe_createAccount: makeEntry({
192206
params: [{ name: 'secret', type: FIELD }],
193207
returnType: COMPLETE_ADDRESS,
194208
}),
195209

210+
aztec_txe_addAccount: makeEntry({
211+
params: [{ name: 'secret', type: FIELD }],
212+
returnType: COMPLETE_ADDRESS,
213+
}),
214+
196215
aztec_txe_addAuthWitness: makeEntry({
197216
params: [
198217
{ name: 'address', type: AZTEC_ADDRESS },

0 commit comments

Comments
 (0)