From 8f1b6c63624b873f4818cfb38ab9a9455680889d Mon Sep 17 00:00:00 2001 From: Nico Chamo Date: Sun, 24 May 2026 00:12:48 +0000 Subject: [PATCH 1/5] refactor(txe): normalize deploy and addAccount to oracle registry --- yarn-project/txe/src/index.ts | 203 +----------------- yarn-project/txe/src/oracle/interfaces.ts | 13 +- .../txe/src/oracle/txe_oracle_registry.ts | 19 ++ .../oracle/txe_oracle_top_level_context.ts | 45 +++- .../txe/src/oracle/txe_oracle_version.ts | 2 +- yarn-project/txe/src/rpc_translator.ts | 58 ++--- yarn-project/txe/src/txe_session.test.ts | 3 + yarn-project/txe/src/txe_session.ts | 20 +- .../txe/src/util/txe_artifact_resolver.ts | 166 ++++++++++++++ 9 files changed, 283 insertions(+), 246 deletions(-) create mode 100644 yarn-project/txe/src/util/txe_artifact_resolver.ts diff --git a/yarn-project/txe/src/index.ts b/yarn-project/txe/src/index.ts index d745b11df297..218943639ec0 100644 --- a/yarn-project/txe/src/index.ts +++ b/yarn-project/txe/src/index.ts @@ -1,61 +1,25 @@ -import { SchnorrAccountContractArtifact } from '@aztec/accounts/schnorr'; -import { type NoirCompiledContract, loadContractArtifact } from '@aztec/aztec.js/abi'; -import { AztecAddress } from '@aztec/aztec.js/addresses'; -import { - type ContractInstanceWithAddress, - getContractInstanceFromInstantiationParams, -} from '@aztec/aztec.js/contracts'; -import { Fr } from '@aztec/aztec.js/fields'; -import { PublicKeys, deriveKeys } from '@aztec/aztec.js/keys'; import { createSafeJsonRpcServer } from '@aztec/foundation/json-rpc/server'; import type { Logger } from '@aztec/foundation/log'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { protocolContractNames } from '@aztec/protocol-contracts'; import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle'; import { ContractStore } from '@aztec/pxe/server'; -import { computeArtifactHash } from '@aztec/stdlib/contract'; -import type { ContractArtifactWithHash } from '@aztec/stdlib/contract'; import type { ApiSchemaFor } from '@aztec/stdlib/schemas'; import { zodFor } from '@aztec/stdlib/schemas'; -import { createHash } from 'crypto'; -import { createReadStream } from 'fs'; -import { readFile, readdir } from 'fs/promises'; -import { join, parse } from 'path'; import { z } from 'zod'; import { type TXEOracleFunctionName, TXESession } from './txe_session.js'; import { type ForeignCallArgs, ForeignCallArgsSchema, - type ForeignCallArray, type ForeignCallResult, ForeignCallResultSchema, - type ForeignCallSingle, - addressFromSingle, - fromArray, - fromSingle, - toSingle, } from './util/encoding.js'; +import { TXEArtifactResolver } from './util/txe_artifact_resolver.js'; const sessions = new Map(); -/* - * TXE typically has to load the same contract artifacts over and over again for multiple tests, - * so we cache them here to avoid loading from disk repeatedly. - * - * The in-flight map coalesces concurrent requests for the same cache key so that - * computeArtifactHash (very expensive) is only run once even under parallelism. - */ -const TXEArtifactsCache = new Map< - string, - { artifact: ContractArtifactWithHash; instance: ContractInstanceWithAddress } ->(); -const TXEArtifactsCacheInFlight = new Map< - string, - Promise<{ artifact: ContractArtifactWithHash; instance: ContractInstanceWithAddress }> ->(); - type TXEForeignCallInput = { session_id: number; function: TXEOracleFunctionName; @@ -82,155 +46,19 @@ const TXEForeignCallInputSchema = zodFor()( class TXEDispatcher { private contractStore!: ContractStore; + private readonly artifactResolver = new TXEArtifactResolver(); constructor(private logger: Logger) {} - private fastHashFile(path: string) { - return new Promise(resolve => { - const fd = createReadStream(path); - const hash = createHash('sha1'); - hash.setEncoding('hex'); - - fd.on('end', function () { - hash.end(); - resolve(hash.read()); - }); - - fd.pipe(hash); - }); - } - - async #processDeployInputs({ inputs, root_path: rootPath, package_name: packageName }: TXEForeignCallInput) { - const [contractPath, initializer] = inputs.slice(0, 2).map(input => - fromArray(input as ForeignCallArray) - .map(char => String.fromCharCode(char.toNumber())) - .join(''), - ); - - const decodedArgs = fromArray(inputs[3] as ForeignCallArray); - const secret = fromSingle(inputs[4] as ForeignCallSingle); - const salt = fromSingle(inputs[5] as ForeignCallSingle); - const deployer = addressFromSingle(inputs[6] as ForeignCallSingle); - const publicKeys = secret.equals(Fr.ZERO) ? PublicKeys.default() : (await deriveKeys(secret)).publicKeys; - const publicKeysHash = await publicKeys.hash(); - - let artifactPath = ''; - const { dir: contractDirectory, base: contractFilename } = parse(contractPath); - if (contractDirectory) { - if (contractDirectory.includes('@')) { - // We're deploying a contract that belongs in a workspace - // env.deploy("../path/to/workspace/root@packageName/contractName") - const [workspace, pkg] = contractDirectory.split('@'); - const targetPath = join(rootPath, workspace, '/target'); - this.logger.debug(`Looking for compiled artifact in workspace ${targetPath}`); - artifactPath = join(targetPath, `${pkg}-${contractFilename}.json`); - } else { - // We're deploying a standalone external contract - // env.deploy("../path/to/contract/root/contractName") - const targetPath = join(rootPath, contractDirectory, '/target'); - this.logger.debug(`Looking for compiled artifact in ${targetPath}`); - [artifactPath] = (await readdir(targetPath)).filter(file => file.endsWith(`-${contractFilename}.json`)); - } - } else { - // We're deploying a local contract - // env.deploy("contractName") - artifactPath = join(rootPath, './target', `${packageName}-${contractFilename}.json`); - } - - const fileHash = await this.fastHashFile(artifactPath); - - const cacheKey = `${contractDirectory ?? ''}-${contractFilename}-${initializer}-${decodedArgs - .map(arg => arg.toString()) - .join('-')}-${publicKeysHash}-${salt}-${deployer}-${fileHash}`; - - let instance; - let artifact: ContractArtifactWithHash; - - if (TXEArtifactsCache.has(cacheKey)) { - this.logger.debug(`Using cached artifact for ${cacheKey}`); - ({ artifact, instance } = TXEArtifactsCache.get(cacheKey)!); - } else { - if (!TXEArtifactsCacheInFlight.has(cacheKey)) { - this.logger.debug(`Loading compiled artifact ${artifactPath}`); - const compute = async () => { - const artifactJSON = JSON.parse(await readFile(artifactPath, 'utf-8')) as NoirCompiledContract; - const artifactWithoutHash = loadContractArtifact(artifactJSON); - const computedArtifact: ContractArtifactWithHash = { - ...artifactWithoutHash, - // Artifact hash is *very* expensive to compute, so we do it here once - // and the TXE contract data provider can cache it - artifactHash: await computeArtifactHash(artifactWithoutHash), - }; - this.logger.debug( - `Deploy ${computedArtifact.name} with initializer ${initializer}(${decodedArgs}) and public keys hash ${publicKeysHash.toString()}`, - ); - const computedInstance = await getContractInstanceFromInstantiationParams(computedArtifact, { - constructorArgs: decodedArgs, - skipArgsDecoding: true, - salt, - publicKeys, - constructorArtifact: initializer ? initializer : undefined, - deployer, - }); - const result = { artifact: computedArtifact, instance: computedInstance }; - TXEArtifactsCache.set(cacheKey, result); - TXEArtifactsCacheInFlight.delete(cacheKey); - return result; - }; - TXEArtifactsCacheInFlight.set(cacheKey, compute()); - } - ({ artifact, instance } = await TXEArtifactsCacheInFlight.get(cacheKey)!); - } - - inputs.splice(0, 1, artifact, instance, toSingle(secret)); - } - - async #processAddAccountInputs({ inputs }: TXEForeignCallInput) { - const secret = fromSingle(inputs[0] as ForeignCallSingle); - - const cacheKey = `SchnorrAccountContract-${secret}`; - - let artifact: ContractArtifactWithHash; - let instance; - - if (TXEArtifactsCache.has(cacheKey)) { - this.logger.debug(`Using cached artifact for ${cacheKey}`); - ({ artifact, instance } = TXEArtifactsCache.get(cacheKey)!); - } else { - if (!TXEArtifactsCacheInFlight.has(cacheKey)) { - const compute = async () => { - const keys = await deriveKeys(secret); - const args = [keys.publicKeys.ivpkM.x, keys.publicKeys.ivpkM.y]; - const computedArtifact: ContractArtifactWithHash = { - ...SchnorrAccountContractArtifact, - // Artifact hash is *very* expensive to compute, so we do it here once - // and the TXE contract data provider can cache it - artifactHash: await computeArtifactHash(SchnorrAccountContractArtifact), - }; - const computedInstance = await getContractInstanceFromInstantiationParams(computedArtifact, { - constructorArgs: args, - skipArgsDecoding: true, - salt: Fr.ONE, - publicKeys: keys.publicKeys, - constructorArtifact: 'constructor', - deployer: AztecAddress.ZERO, - }); - const result = { artifact: computedArtifact, instance: computedInstance }; - TXEArtifactsCache.set(cacheKey, result); - TXEArtifactsCacheInFlight.delete(cacheKey); - return result; - }; - TXEArtifactsCacheInFlight.set(cacheKey, compute()); - } - ({ artifact, instance } = await TXEArtifactsCacheInFlight.get(cacheKey)!); - } - - inputs.splice(0, 0, artifact, instance); - } - // eslint-disable-next-line camelcase async resolve_foreign_call(callData: TXEForeignCallInput): Promise { - const { session_id: sessionId, function: functionName, inputs } = callData; + const { + session_id: sessionId, + function: functionName, + inputs, + root_path: rootPath, + package_name: packageName, + } = callData; this.logger.debug(`Calling ${functionName} on session ${sessionId}`); if (!sessions.has(sessionId)) { @@ -246,18 +74,7 @@ class TXEDispatcher { } this.logger.debug('Registered protocol contracts in shared contract store'); } - sessions.set(sessionId, await TXESession.init(this.contractStore)); - } - - switch (functionName) { - case 'aztec_txe_deploy': { - await this.#processDeployInputs(callData); - break; - } - case 'aztec_txe_addAccount': { - await this.#processAddAccountInputs(callData); - break; - } + sessions.set(sessionId, await TXESession.init(this.contractStore, this.artifactResolver, rootPath, packageName)); } return await sessions.get(sessionId)!.processFunction(functionName, inputs); diff --git a/yarn-project/txe/src/oracle/interfaces.ts b/yarn-project/txe/src/oracle/interfaces.ts index b39ebffc81e5..6b0a5a8c0bb9 100644 --- a/yarn-project/txe/src/oracle/interfaces.ts +++ b/yarn-project/txe/src/oracle/interfaces.ts @@ -1,6 +1,4 @@ -import type { ContractArtifact } from '@aztec/aztec.js/abi'; import { CompleteAddress } from '@aztec/aztec.js/addresses'; -import type { ContractInstanceWithAddress } from '@aztec/aztec.js/contracts'; import { TxHash } from '@aztec/aztec.js/tx'; import { BlockNumber } from '@aztec/foundation/branded-types'; import type { Fr } from '@aztec/foundation/curves/bn254'; @@ -59,9 +57,16 @@ export interface ITxeExecutionOracle { getNextBlockTimestamp(): Promise; advanceBlocksBy(blocks: number): Promise; advanceTimestampBy(duration: UInt64): void; - deploy(artifact: ContractArtifact, instance: ContractInstanceWithAddress, foreignSecret: Fr): Promise; + deploy( + contractPath: string, + initializer: string, + args: Fr[], + secret: Fr, + salt: Fr, + deployer: AztecAddress, + ): Promise; createAccount(secret: Fr): Promise; - addAccount(artifact: ContractArtifact, instance: ContractInstanceWithAddress, secret: Fr): Promise; + addAccount(secret: Fr): Promise; addAuthWitness(address: AztecAddress, messageHash: Fr): Promise; getLastBlockTimestamp(): Promise; getLastTxEffects(): Promise<{ diff --git a/yarn-project/txe/src/oracle/txe_oracle_registry.ts b/yarn-project/txe/src/oracle/txe_oracle_registry.ts index 4580e490b4bb..0a460f535271 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_registry.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_registry.ts @@ -22,6 +22,7 @@ import { ORACLE_REGISTRY, type OracleRegistryEntry, type ParamTypes, + STR, type TypeMapping, U32, makeEntry, @@ -188,11 +189,29 @@ export const TXE_ORACLE_REGISTRY = { params: [{ name: 'duration', type: BIGINT }], }), + aztec_txe_deploy: makeEntry({ + params: [ + { name: 'contractPath', type: STR }, + { name: 'initializer', type: STR }, + { name: 'argsLength', type: U32 }, + { name: 'args', type: ARRAY(FIELD) }, + { name: 'secret', type: FIELD }, + { name: 'salt', type: FIELD }, + { name: 'deployer', type: AZTEC_ADDRESS }, + ], + returnType: ARRAY(FIELD), + }), + aztec_txe_createAccount: makeEntry({ params: [{ name: 'secret', type: FIELD }], returnType: COMPLETE_ADDRESS, }), + aztec_txe_addAccount: makeEntry({ + params: [{ name: 'secret', type: FIELD }], + returnType: COMPLETE_ADDRESS, + }), + aztec_txe_addAuthWitness: makeEntry({ params: [ { name: 'address', type: AZTEC_ADDRESS }, diff --git a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts index f6158c5dc2c8..315aea76b9b0 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts @@ -85,6 +85,7 @@ import { ForkCheckpoint } from '@aztec/world-state'; import { DEFAULT_ADDRESS, MAX_PRIVATE_EVENTS_PER_TXE_QUERY, MAX_PRIVATE_EVENT_LEN } from '../constants.js'; import type { TXEStateMachine } from '../state_machine/index.js'; import type { TXEAccountStore } from '../util/txe_account_store.js'; +import type { TXEArtifactResolver } from '../util/txe_artifact_resolver.js'; import { TXEPublicContractDataSource } from '../util/txe_public_contract_data_source.js'; import { getSingleTxBlockRequestHash, insertTxEffectIntoWorldTrees, makeTXEBlock } from '../utils/block_creation.js'; import type { ITxeExecutionOracle } from './interfaces.js'; @@ -111,6 +112,9 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl private version: Fr, private chainId: Fr, private authwits: Map, + private readonly artifactResolver: TXEArtifactResolver, + private readonly rootPath: string, + private readonly packageName: string, ) { this.logger = createLogger('txe:top_level_context'); this.logger.debug('Entering Top Level Context'); @@ -245,7 +249,25 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl this.nextBlockTimestamp += duration; } - async deploy(artifact: ContractArtifact, instance: ContractInstanceWithAddress, secret: Fr) { + async deploy( + contractPath: string, + initializer: string, + args: Fr[], + secret: Fr, + salt: Fr, + deployer: AztecAddress, + ): Promise { + const { artifact, instance } = await this.artifactResolver.resolveDeployArtifact({ + rootPath: this.rootPath, + packageName: this.packageName, + contractPath, + initializer, + args, + secret, + salt, + deployer, + }); + // Emit deployment nullifier await this.mineBlock({ nullifiers: [ @@ -257,15 +279,32 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl }); if (!secret.equals(Fr.ZERO)) { - await this.addAccount(artifact, instance, secret); + await this.registerContractAndAddAccount(artifact, instance, secret); } else { await this.contractStore.addContractInstance(instance); await this.contractStore.addContractArtifact(artifact); this.logger.debug(`Deployed ${artifact.name} at ${instance.address}`); } + + return [ + instance.salt, + instance.deployer.toField(), + instance.currentContractClassId, + instance.initializationHash, + ...instance.publicKeys.toFields(), + ]; + } + + async addAccount(secret: Fr) { + const { artifact, instance } = await this.artifactResolver.resolveAccountArtifact(secret); + return this.registerContractAndAddAccount(artifact, instance, secret); } - async addAccount(artifact: ContractArtifact, instance: ContractInstanceWithAddress, secret: Fr) { + private async registerContractAndAddAccount( + artifact: ContractArtifact, + instance: ContractInstanceWithAddress, + secret: Fr, + ) { const partialAddress = await computePartialAddress(instance); this.logger.debug(`Deployed ${artifact.name} at ${instance.address}`); diff --git a/yarn-project/txe/src/oracle/txe_oracle_version.ts b/yarn-project/txe/src/oracle/txe_oracle_version.ts index 4f7a66e72849..72f3bf2d6b87 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_version.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_version.ts @@ -14,4 +14,4 @@ export const TXE_ORACLE_VERSION_MINOR = 1; * - TXE_ORACLE_VERSION_MAJOR (and reset MINOR to 0) for breaking changes, or * - TXE_ORACLE_VERSION_MINOR for additive changes (new oracle method added). */ -export const TXE_ORACLE_INTERFACE_HASH = 'e191c3083fae4aed4802d9fe8f2fbf3366dcbd058b2096c24be1110250f40db2'; +export const TXE_ORACLE_INTERFACE_HASH = '5c6740d14e8581af17d67583118bf92c905287c12d93ac77cf0b0745e5e915ad'; diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index a687e6d02087..b5ea8e42ec8d 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -1,20 +1,9 @@ -import type { ContractInstanceWithAddress } from '@aztec/aztec.js/contracts'; -import { Fr } from '@aztec/foundation/curves/bn254'; import type { IMiscOracle, IPrivateExecutionOracle, IUtilityExecutionOracle } from '@aztec/pxe/simulator'; -import type { ContractArtifact } from '@aztec/stdlib/abi'; import type { IAvmExecutionOracle, ITxeExecutionOracle } from './oracle/interfaces.js'; import { callTxeHandler } from './oracle/txe_oracle_registry.js'; import type { TXESessionStateHandler } from './txe_session.js'; -import { - type ForeignCallArgs, - type ForeignCallSingle, - addressFromSingle, - fromSingle, - toArray, - toForeignCallResult, - toSingle, -} from './util/encoding.js'; +import type { ForeignCallArgs } from './util/encoding.js'; export class UnavailableOracleError extends Error { constructor(oracleName: string) { @@ -185,25 +174,13 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_txe_deploy( - artifact: ContractArtifact, - instance: ContractInstanceWithAddress, - foreignSecret: ForeignCallSingle, - ) { - const secret = fromSingle(foreignSecret); - - await this.handlerAsTxe().deploy(artifact, instance, secret); - - return toForeignCallResult([ - toArray([ - instance.salt, - instance.deployer.toField(), - instance.currentContractClassId, - instance.initializationHash, - instance.immutablesHash, - ...instance.publicKeys.toFields(), - ]), - ]); + aztec_txe_deploy(...inputs: ForeignCallArgs) { + return callTxeHandler({ + oracle: 'aztec_txe_deploy', + inputs, + handler: ([contractPath, initializer, _, args, secret, salt, deployer]) => + this.handlerAsTxe().deploy(contractPath, initializer, args, secret, salt, deployer), + }); } // eslint-disable-next-line camelcase @@ -216,19 +193,12 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_txe_addAccount( - artifact: ContractArtifact, - instance: ContractInstanceWithAddress, - foreignSecret: ForeignCallSingle, - ) { - const secret = fromSingle(foreignSecret); - - const completeAddress = await this.handlerAsTxe().addAccount(artifact, instance, secret); - - return toForeignCallResult([ - toSingle(completeAddress.address), - ...completeAddress.publicKeys.toFields().map(toSingle), - ]); + aztec_txe_addAccount(...inputs: ForeignCallArgs) { + return callTxeHandler({ + oracle: 'aztec_txe_addAccount', + inputs, + handler: ([secret]) => this.handlerAsTxe().addAccount(secret), + }); } // eslint-disable-next-line camelcase diff --git a/yarn-project/txe/src/txe_session.test.ts b/yarn-project/txe/src/txe_session.test.ts index 3dd5676b9ea2..d4d01bc62124 100644 --- a/yarn-project/txe/src/txe_session.test.ts +++ b/yarn-project/txe/src/txe_session.test.ts @@ -26,6 +26,9 @@ describe('TXESession.processFunction', () => { new Fr(1), // chainId new Fr(1), // version 0n, // nextBlockTimestamp + {} as any, // artifactResolver + '', // rootPath + '', // packageName ); }); diff --git a/yarn-project/txe/src/txe_session.ts b/yarn-project/txe/src/txe_session.ts index abee63fc638d..51e58e0368ec 100644 --- a/yarn-project/txe/src/txe_session.ts +++ b/yarn-project/txe/src/txe_session.ts @@ -57,6 +57,7 @@ import { TXEArchiver } from './state_machine/archiver.js'; import { TXEStateMachine } from './state_machine/index.js'; import type { ForeignCallArgs, ForeignCallResult } from './util/encoding.js'; import { TXEAccountStore } from './util/txe_account_store.js'; +import type { TXEArtifactResolver } from './util/txe_artifact_resolver.js'; import { getSingleTxBlockRequestHash, insertTxEffectIntoWorldTrees, makeTXEBlock } from './utils/block_creation.js'; import { makeTxEffect } from './utils/tx_effect_creation.js'; @@ -220,9 +221,17 @@ export class TXESession implements TXESessionStateHandler { private chainId: Fr, private version: Fr, private nextBlockTimestamp: bigint, + private readonly artifactResolver: TXEArtifactResolver, + private readonly rootPath: string, + private readonly packageName: string, ) {} - static async init(contractStore: ContractStore) { + static async init( + contractStore: ContractStore, + artifactResolver: TXEArtifactResolver, + rootPath: string, + packageName: string, + ) { const store = await openTmpStore('txe-session'); const addressStore = new AddressStore(store); @@ -273,6 +282,9 @@ export class TXESession implements TXESessionStateHandler { version, chainId, new Map(), + artifactResolver, + rootPath, + packageName, ); await topLevelOracleHandler.advanceBlocksBy(1); @@ -295,6 +307,9 @@ export class TXESession implements TXESessionStateHandler { version, chainId, nextBlockTimestamp, + artifactResolver, + rootPath, + packageName, ); } @@ -471,6 +486,9 @@ export class TXESession implements TXESessionStateHandler { this.version, this.chainId, this.authwits, + this.artifactResolver, + this.rootPath, + this.packageName, ); this.state = { name: 'TOP_LEVEL' }; diff --git a/yarn-project/txe/src/util/txe_artifact_resolver.ts b/yarn-project/txe/src/util/txe_artifact_resolver.ts new file mode 100644 index 000000000000..0ba0cb243fdf --- /dev/null +++ b/yarn-project/txe/src/util/txe_artifact_resolver.ts @@ -0,0 +1,166 @@ +import { SchnorrAccountContractArtifact } from '@aztec/accounts/schnorr'; +import { type NoirCompiledContract, loadContractArtifact } from '@aztec/aztec.js/abi'; +import { AztecAddress } from '@aztec/aztec.js/addresses'; +import { + type ContractInstanceWithAddress, + getContractInstanceFromInstantiationParams, +} from '@aztec/aztec.js/contracts'; +import { Fr } from '@aztec/aztec.js/fields'; +import { PublicKeys, deriveKeys } from '@aztec/aztec.js/keys'; +import { createLogger } from '@aztec/foundation/log'; +import type { ContractArtifactWithHash } from '@aztec/stdlib/contract'; +import { computeArtifactHash } from '@aztec/stdlib/contract'; + +import { createHash } from 'crypto'; +import { createReadStream } from 'fs'; +import { readFile, readdir } from 'fs/promises'; +import { join, parse } from 'path'; + +export interface ResolvedArtifact { + artifact: ContractArtifactWithHash; + instance: ContractInstanceWithAddress; +} + +/** + * Resolves and caches contract artifacts and their associated instances. + * + * Artifact hash computation is expensive, so this service deduplicates both completed and in-flight computations + * across all TXE sessions. + */ +export class TXEArtifactResolver { + #cache = new Map>(); + #logger = createLogger('txe:artifact_resolver'); + + /** Resolves the Schnorr account contract artifact and instance for the given secret, caching the result. */ + resolveAccountArtifact(secret: Fr): Promise { + return this.#cached(`SchnorrAccountContract-${secret}`, () => this.#computeAccountArtifact(secret)); + } + + /** Resolves a contract artifact from disk by path, computes its instance, and caches the result. */ + async resolveDeployArtifact({ + rootPath, + packageName, + contractPath, + initializer, + args, + secret, + salt, + deployer, + }: { + rootPath: string; + packageName: string; + contractPath: string; + initializer: string; + args: Fr[]; + secret: Fr; + salt: Fr; + deployer: AztecAddress; + }): Promise { + const publicKeys = secret.equals(Fr.ZERO) ? PublicKeys.default() : (await deriveKeys(secret)).publicKeys; + const publicKeysHash = await publicKeys.hash(); + + const artifactPath = await this.#resolveArtifactPath(rootPath, packageName, contractPath); + const fileHash = await this.#fastHashFile(artifactPath); + + const { dir: contractDirectory, base: contractFilename } = parse(contractPath); + const cacheKey = `${contractDirectory ?? ''}-${contractFilename}-${initializer}-${args + .map(arg => arg.toString()) + .join('-')}-${publicKeysHash}-${salt}-${deployer}-${fileHash}`; + + return this.#cached(cacheKey, () => + this.#computeDeployArtifact(artifactPath, initializer, args, salt, publicKeys, publicKeysHash, deployer), + ); + } + + #cached(key: string, generator: () => Promise): Promise { + if (!this.#cache.has(key)) { + this.#cache.set(key, generator()); + } + return this.#cache.get(key)!; + } + + async #resolveArtifactPath(rootPath: string, packageName: string, contractPath: string): Promise { + const { dir: contractDirectory, base: contractFilename } = parse(contractPath); + if (contractDirectory) { + if (contractDirectory.includes('@')) { + // env.deploy("../path/to/workspace/root@packageName/contractName") + const [workspace, pkg] = contractDirectory.split('@'); + const targetPath = join(rootPath, workspace, '/target'); + this.#logger.debug(`Looking for compiled artifact in workspace ${targetPath}`); + return join(targetPath, `${pkg}-${contractFilename}.json`); + } else { + // env.deploy("../path/to/contract/root/contractName") + const targetPath = join(rootPath, contractDirectory, '/target'); + this.#logger.debug(`Looking for compiled artifact in ${targetPath}`); + const [artifactPath] = (await readdir(targetPath)).filter(file => file.endsWith(`-${contractFilename}.json`)); + return artifactPath; + } + } else { + // env.deploy("contractName") + return join(rootPath, './target', `${packageName}-${contractFilename}.json`); + } + } + + async #computeAccountArtifact(secret: Fr): Promise { + const keys = await deriveKeys(secret); + const args = [keys.publicKeys.masterIncomingViewingPublicKey.x, keys.publicKeys.masterIncomingViewingPublicKey.y]; + const artifact: ContractArtifactWithHash = { + ...SchnorrAccountContractArtifact, + artifactHash: await computeArtifactHash(SchnorrAccountContractArtifact), + }; + const instance = await getContractInstanceFromInstantiationParams(artifact, { + constructorArgs: args, + skipArgsDecoding: true, + salt: Fr.ONE, + publicKeys: keys.publicKeys, + constructorArtifact: 'constructor', + deployer: AztecAddress.ZERO, + }); + return { artifact, instance }; + } + + async #computeDeployArtifact( + artifactPath: string, + initializer: string, + args: Fr[], + salt: Fr, + publicKeys: PublicKeys, + publicKeysHash: Fr, + deployer: AztecAddress, + ): Promise { + this.#logger.debug(`Loading compiled artifact ${artifactPath}`); + const artifactJSON = JSON.parse(await readFile(artifactPath, 'utf-8')) as NoirCompiledContract; + const artifactWithoutHash = loadContractArtifact(artifactJSON); + const artifact: ContractArtifactWithHash = { + ...artifactWithoutHash, + artifactHash: await computeArtifactHash(artifactWithoutHash), + }; + this.#logger.debug( + `Deploy ${artifact.name} with initializer ${initializer}(${args}) and public keys hash ${publicKeysHash}`, + ); + const instance = await getContractInstanceFromInstantiationParams(artifact, { + constructorArgs: args, + skipArgsDecoding: true, + salt, + publicKeys, + constructorArtifact: initializer || undefined, + deployer, + }); + return { artifact, instance }; + } + + #fastHashFile(path: string): Promise { + return new Promise(resolve => { + const fd = createReadStream(path); + const hash = createHash('sha1'); + hash.setEncoding('hex'); + + fd.on('end', function () { + hash.end(); + resolve(hash.read()); + }); + + fd.pipe(hash); + }); + } +} From de203cc0b3c1efbd583cada09acd05af5bad1f48 Mon Sep 17 00:00:00 2001 From: Nico Chamo Date: Sun, 24 May 2026 00:27:25 +0000 Subject: [PATCH 2/5] refactor(txe): use private readonly props and # methods --- .../txe/src/util/txe_artifact_resolver.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/yarn-project/txe/src/util/txe_artifact_resolver.ts b/yarn-project/txe/src/util/txe_artifact_resolver.ts index 0ba0cb243fdf..9a2083be07eb 100644 --- a/yarn-project/txe/src/util/txe_artifact_resolver.ts +++ b/yarn-project/txe/src/util/txe_artifact_resolver.ts @@ -28,12 +28,12 @@ export interface ResolvedArtifact { * across all TXE sessions. */ export class TXEArtifactResolver { - #cache = new Map>(); - #logger = createLogger('txe:artifact_resolver'); + private readonly cache = new Map>(); + private readonly logger = createLogger('txe:artifact_resolver'); /** Resolves the Schnorr account contract artifact and instance for the given secret, caching the result. */ resolveAccountArtifact(secret: Fr): Promise { - return this.#cached(`SchnorrAccountContract-${secret}`, () => this.#computeAccountArtifact(secret)); + return this.#getOrCompute(`SchnorrAccountContract-${secret}`, () => this.#computeAccountArtifact(secret)); } /** Resolves a contract artifact from disk by path, computes its instance, and caches the result. */ @@ -67,16 +67,16 @@ export class TXEArtifactResolver { .map(arg => arg.toString()) .join('-')}-${publicKeysHash}-${salt}-${deployer}-${fileHash}`; - return this.#cached(cacheKey, () => + return this.#getOrCompute(cacheKey, () => this.#computeDeployArtifact(artifactPath, initializer, args, salt, publicKeys, publicKeysHash, deployer), ); } - #cached(key: string, generator: () => Promise): Promise { - if (!this.#cache.has(key)) { - this.#cache.set(key, generator()); + #getOrCompute(key: string, generator: () => Promise): Promise { + if (!this.cache.has(key)) { + this.cache.set(key, generator()); } - return this.#cache.get(key)!; + return this.cache.get(key)!; } async #resolveArtifactPath(rootPath: string, packageName: string, contractPath: string): Promise { @@ -86,12 +86,12 @@ export class TXEArtifactResolver { // env.deploy("../path/to/workspace/root@packageName/contractName") const [workspace, pkg] = contractDirectory.split('@'); const targetPath = join(rootPath, workspace, '/target'); - this.#logger.debug(`Looking for compiled artifact in workspace ${targetPath}`); + this.logger.debug(`Looking for compiled artifact in workspace ${targetPath}`); return join(targetPath, `${pkg}-${contractFilename}.json`); } else { // env.deploy("../path/to/contract/root/contractName") const targetPath = join(rootPath, contractDirectory, '/target'); - this.#logger.debug(`Looking for compiled artifact in ${targetPath}`); + this.logger.debug(`Looking for compiled artifact in ${targetPath}`); const [artifactPath] = (await readdir(targetPath)).filter(file => file.endsWith(`-${contractFilename}.json`)); return artifactPath; } @@ -128,14 +128,14 @@ export class TXEArtifactResolver { publicKeysHash: Fr, deployer: AztecAddress, ): Promise { - this.#logger.debug(`Loading compiled artifact ${artifactPath}`); + this.logger.debug(`Loading compiled artifact ${artifactPath}`); const artifactJSON = JSON.parse(await readFile(artifactPath, 'utf-8')) as NoirCompiledContract; const artifactWithoutHash = loadContractArtifact(artifactJSON); const artifact: ContractArtifactWithHash = { ...artifactWithoutHash, artifactHash: await computeArtifactHash(artifactWithoutHash), }; - this.#logger.debug( + this.logger.debug( `Deploy ${artifact.name} with initializer ${initializer}(${args}) and public keys hash ${publicKeysHash}`, ); const instance = await getContractInstanceFromInstantiationParams(artifact, { From 8c0027601e7361463b9daa09d307f21c78375139 Mon Sep 17 00:00:00 2001 From: Nico Chamo Date: Wed, 27 May 2026 22:58:14 +0000 Subject: [PATCH 3/5] fix(txe): migrate immutablesHash oracle and fix key accessor --- yarn-project/txe/src/oracle/interfaces.ts | 1 + .../txe/src/oracle/txe_oracle_public_context.ts | 4 ++++ .../txe/src/oracle/txe_oracle_registry.ts | 4 ++++ yarn-project/txe/src/rpc_translator.ts | 16 ++++++---------- .../txe/src/util/txe_artifact_resolver.ts | 2 +- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/yarn-project/txe/src/oracle/interfaces.ts b/yarn-project/txe/src/oracle/interfaces.ts index 6b0a5a8c0bb9..ec0f316391b0 100644 --- a/yarn-project/txe/src/oracle/interfaces.ts +++ b/yarn-project/txe/src/oracle/interfaces.ts @@ -39,6 +39,7 @@ export interface IAvmExecutionOracle { getContractInstanceDeployer(address: AztecAddress): Promise<{ member: Fr; exists: boolean }>; getContractInstanceClassId(address: AztecAddress): Promise<{ member: Fr; exists: boolean }>; getContractInstanceInitializationHash(address: AztecAddress): Promise<{ member: Fr; exists: boolean }>; + getContractInstanceImmutablesHash(address: AztecAddress): Promise<{ member: Fr; exists: boolean }>; returndataSize(): Promise; returndataCopy(rdOffset: number, copySize: number): Promise; call(l2Gas: number, daGas: number, address: AztecAddress, argsLength: number, args: Fr[]): Promise; diff --git a/yarn-project/txe/src/oracle/txe_oracle_public_context.ts b/yarn-project/txe/src/oracle/txe_oracle_public_context.ts index b5adea119658..73af8b5f4329 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_public_context.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_public_context.ts @@ -137,6 +137,10 @@ export class TXEOraclePublicContext implements IAvmExecutionOracle { return this.getContractInstanceMember(address, i => i.initializationHash); } + getContractInstanceImmutablesHash(address: AztecAddress): Promise<{ member: Fr; exists: boolean }> { + return this.getContractInstanceMember(address, i => i.immutablesHash); + } + private async getContractInstanceMember( address: AztecAddress, accessor: (instance: ContractInstanceWithAddress) => Fr, diff --git a/yarn-project/txe/src/oracle/txe_oracle_registry.ts b/yarn-project/txe/src/oracle/txe_oracle_registry.ts index 0a460f535271..a44eddb2ae68 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_registry.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_registry.ts @@ -364,6 +364,10 @@ export const TXE_ORACLE_REGISTRY = { params: [{ name: 'address', type: AZTEC_ADDRESS }], returnType: CONTRACT_INSTANCE_MEMBER, }), + aztec_avm_getContractInstanceImmutablesHash: makeEntry({ + params: [{ name: 'address', type: AZTEC_ADDRESS }], + returnType: CONTRACT_INSTANCE_MEMBER, + }), } satisfies Record; /** diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index b5ea8e42ec8d..36be2a53ddaa 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -821,16 +821,12 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - async aztec_avm_getContractInstanceImmutablesHash(foreignAddress: ForeignCallSingle) { - const address = addressFromSingle(foreignAddress); - - const instance = await this.handlerAsUtility().getContractInstance(address); - - return toForeignCallResult([ - toSingle(instance.immutablesHash), - // AVM requires an extra boolean indicating the instance was found - toSingle(new Fr(1)), - ]); + aztec_avm_getContractInstanceImmutablesHash(...inputs: ForeignCallArgs) { + return callTxeHandler({ + oracle: 'aztec_avm_getContractInstanceImmutablesHash', + inputs, + handler: ([address]) => this.handlerAsAvm().getContractInstanceImmutablesHash(address), + }); } // eslint-disable-next-line camelcase diff --git a/yarn-project/txe/src/util/txe_artifact_resolver.ts b/yarn-project/txe/src/util/txe_artifact_resolver.ts index 9a2083be07eb..309a2b5bf344 100644 --- a/yarn-project/txe/src/util/txe_artifact_resolver.ts +++ b/yarn-project/txe/src/util/txe_artifact_resolver.ts @@ -103,7 +103,7 @@ export class TXEArtifactResolver { async #computeAccountArtifact(secret: Fr): Promise { const keys = await deriveKeys(secret); - const args = [keys.publicKeys.masterIncomingViewingPublicKey.x, keys.publicKeys.masterIncomingViewingPublicKey.y]; + const args = [keys.publicKeys.ivpkM.x, keys.publicKeys.ivpkM.y]; const artifact: ContractArtifactWithHash = { ...SchnorrAccountContractArtifact, artifactHash: await computeArtifactHash(SchnorrAccountContractArtifact), From a2f8691b8d9217d01fadaa7f2973d5e9c17f693c Mon Sep 17 00:00:00 2001 From: Nico Chamo Date: Wed, 27 May 2026 23:04:16 +0000 Subject: [PATCH 4/5] fix(txe): update TXE oracle interface hash after adding immutablesHash --- yarn-project/txe/src/oracle/txe_oracle_version.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn-project/txe/src/oracle/txe_oracle_version.ts b/yarn-project/txe/src/oracle/txe_oracle_version.ts index 72f3bf2d6b87..d8f8eddab895 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_version.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_version.ts @@ -6,7 +6,7 @@ * The Noir counterparts are in `noir-projects/aztec-nr/aztec/src/test/helpers/txe_oracles.nr`. */ export const TXE_ORACLE_VERSION_MAJOR = 1; -export const TXE_ORACLE_VERSION_MINOR = 1; +export const TXE_ORACLE_VERSION_MINOR = 2; /** * This hash is computed from the TXE oracle interfaces (IAvmExecutionOracle and ITxeExecutionOracle) and is used to @@ -14,4 +14,4 @@ export const TXE_ORACLE_VERSION_MINOR = 1; * - TXE_ORACLE_VERSION_MAJOR (and reset MINOR to 0) for breaking changes, or * - TXE_ORACLE_VERSION_MINOR for additive changes (new oracle method added). */ -export const TXE_ORACLE_INTERFACE_HASH = '5c6740d14e8581af17d67583118bf92c905287c12d93ac77cf0b0745e5e915ad'; +export const TXE_ORACLE_INTERFACE_HASH = '9e5f6ad5fd170d1de5ddd417f19cce47b17382567e08360dc6a783154828e218'; From 4bd6e4107c989c4b2388331f025e6cd73b98749e Mon Sep 17 00:00:00 2001 From: Nico Chamo Date: Wed, 27 May 2026 23:29:58 +0000 Subject: [PATCH 5/5] fix(txe): add missing immutablesHash to deploy return value --- yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts index 315aea76b9b0..16052464dccb 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts @@ -291,6 +291,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl instance.deployer.toField(), instance.currentContractClassId, instance.initializationHash, + instance.immutablesHash, ...instance.publicKeys.toFields(), ]; }