Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 10 additions & 193 deletions yarn-project/txe/src/index.ts
Original file line number Diff line number Diff line change
@@ -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<number, TXESession>();

/*
* 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;
Expand All @@ -82,155 +46,19 @@ const TXEForeignCallInputSchema = zodFor<TXEForeignCallInput>()(

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<ForeignCallResult> {
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)) {
Expand All @@ -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);
Expand Down
14 changes: 10 additions & 4 deletions yarn-project/txe/src/oracle/interfaces.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -41,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<Fr>;
returndataCopy(rdOffset: number, copySize: number): Promise<Fr[]>;
call(l2Gas: number, daGas: number, address: AztecAddress, argsLength: number, args: Fr[]): Promise<Fr[]>;
Expand All @@ -59,9 +58,16 @@ export interface ITxeExecutionOracle {
getNextBlockTimestamp(): Promise<UInt64>;
advanceBlocksBy(blocks: number): Promise<void>;
advanceTimestampBy(duration: UInt64): void;
deploy(artifact: ContractArtifact, instance: ContractInstanceWithAddress, foreignSecret: Fr): Promise<void>;
deploy(
contractPath: string,
initializer: string,
args: Fr[],
secret: Fr,
salt: Fr,
deployer: AztecAddress,
): Promise<Fr[]>;
createAccount(secret: Fr): Promise<CompleteAddress>;
addAccount(artifact: ContractArtifact, instance: ContractInstanceWithAddress, secret: Fr): Promise<CompleteAddress>;
addAccount(secret: Fr): Promise<CompleteAddress>;
addAuthWitness(address: AztecAddress, messageHash: Fr): Promise<void>;
getLastBlockTimestamp(): Promise<bigint>;
getLastTxEffects(): Promise<{
Expand Down
4 changes: 4 additions & 0 deletions yarn-project/txe/src/oracle/txe_oracle_public_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
23 changes: 23 additions & 0 deletions yarn-project/txe/src/oracle/txe_oracle_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
ORACLE_REGISTRY,
type OracleRegistryEntry,
type ParamTypes,
STR,
type TypeMapping,
U32,
makeEntry,
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -345,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<string, OracleRegistryEntry>;

/**
Expand Down
Loading
Loading