diff --git a/.test_patterns.yml b/.test_patterns.yml index 5cf3381c0307..03dcd7e7a6db 100644 --- a/.test_patterns.yml +++ b/.test_patterns.yml @@ -166,6 +166,7 @@ tests: # boxes - regex: "boxes/scripts/run_test.sh vite" + error_regex: "Test timeout of 90000ms exceeded." owners: - *grego diff --git a/playground/src/components/contract/contract.tsx b/playground/src/components/contract/contract.tsx index 699dc8e54374..d8f5112736d4 100644 --- a/playground/src/components/contract/contract.tsx +++ b/playground/src/components/contract/contract.tsx @@ -218,7 +218,8 @@ export function ContractComponent() { setIsWorking(true); let result; try { - const call = currentContract.methods[fnName](...parameters[fnName]); + const fnParameters = parameters[fnName] ?? []; + const call = currentContract.methods[fnName](...fnParameters); result = await call.simulate(); setSimulationResults({ diff --git a/yarn-project/aztec.js/src/account_manager/deploy_account_method.ts b/yarn-project/aztec.js/src/account_manager/deploy_account_method.ts index ad7349dcd49e..51571a979089 100644 --- a/yarn-project/aztec.js/src/account_manager/deploy_account_method.ts +++ b/yarn-project/aztec.js/src/account_manager/deploy_account_method.ts @@ -1,11 +1,10 @@ -import type { AuthWitnessProvider } from '@aztec/entrypoints/interfaces'; import { - EncodedAppEntrypointPayload, - EncodedFeeEntrypointPayload, - ExecutionPayload, + EncodedAppEntrypointCalls, + EncodedCallsForEntrypoint, computeCombinedPayloadHash, -} from '@aztec/entrypoints/payload'; -import { mergeExecutionPayloads } from '@aztec/entrypoints/utils'; +} from '@aztec/entrypoints/encoding'; +import type { AuthWitnessProvider } from '@aztec/entrypoints/interfaces'; +import { ExecutionPayload, mergeExecutionPayloads } from '@aztec/entrypoints/payload'; import { type ContractArtifact, type FunctionArtifact, getFunctionArtifactByName } from '@aztec/stdlib/abi'; import type { PublicKeys } from '@aztec/stdlib/keys'; @@ -51,22 +50,33 @@ export class DeployAccountMethod extends DeployMethod { if (options.fee && this.#feePaymentArtifact) { const { address } = await this.getInstance(); - const emptyAppPayload = await EncodedAppEntrypointPayload.fromAppExecution([]); + const emptyAppCalls = await EncodedAppEntrypointCalls.fromAppExecution([]); const fee = await this.getDefaultFeeOptions(options.fee); - const feePayload = await EncodedFeeEntrypointPayload.fromFeeOptions(address, fee); - const args = [emptyAppPayload, feePayload, false]; + // Get the execution payload for the fee, it includes the calls and potentially authWitnesses + const { calls: feeCalls, authWitnesses: feeAuthwitnesses } = await fee.paymentMethod.getExecutionPayload( + fee.gasSettings, + ); + // Encode the calls for the fee + const feePayer = await fee.paymentMethod.getFeePayer(fee.gasSettings); + const isFeePayer = feePayer.equals(address); + const feeEncodedCalls = await EncodedCallsForEntrypoint.fromFeeCalls(feeCalls, isFeePayer); + + // Get the entrypoint args + const args = [emptyAppCalls, feeEncodedCalls, false]; + + // Compute the authwitness required to verify the combined payload + const combinedPayloadAuthWitness = await this.#authWitnessProvider.createAuthWit( + await computeCombinedPayloadHash(emptyAppCalls, feeEncodedCalls), + ); const call = new ContractFunctionInteraction( this.wallet, address, this.#feePaymentArtifact, args, - [ - await this.#authWitnessProvider.createAuthWit(await computeCombinedPayloadHash(emptyAppPayload, feePayload)), - ...feePayload.authWitnesses, - ], + [combinedPayloadAuthWitness, ...feeAuthwitnesses], [], - [...emptyAppPayload.hashedArguments, ...feePayload.hashedArguments], + [...emptyAppCalls.hashedArguments, ...feeEncodedCalls.hashedArguments], ); exec = mergeExecutionPayloads([exec, await call.request()]); diff --git a/yarn-project/aztec.js/src/contract/base_contract_interaction.ts b/yarn-project/aztec.js/src/contract/base_contract_interaction.ts index 3e4b13fe5b0d..7344268a026f 100644 --- a/yarn-project/aztec.js/src/contract/base_contract_interaction.ts +++ b/yarn-project/aztec.js/src/contract/base_contract_interaction.ts @@ -4,7 +4,7 @@ import type { Fr } from '@aztec/foundation/fields'; import { createLogger } from '@aztec/foundation/log'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import { GasSettings } from '@aztec/stdlib/gas'; -import type { Capsule, HashedValues, TxExecutionRequest, TxProvingResult } from '@aztec/stdlib/tx'; +import type { Capsule, TxExecutionRequest, TxProvingResult } from '@aztec/stdlib/tx'; import { FeeJuicePaymentMethod } from '../fee/fee_juice_payment_method.js'; import type { Wallet } from '../wallet/wallet.js'; @@ -12,11 +12,21 @@ import { getGasLimits } from './get_gas_limits.js'; import { ProvenTx } from './proven_tx.js'; import { SentTx } from './sent_tx.js'; +/** + * Represents the options to configure a request from a contract interaction. + * Allows specifying additional auth witnesses and capsules to use during execution + */ +export type RequestMethodOptions = { + /** Extra authwits to use during execution */ + authWitnesses?: AuthWitness[]; + /** Extra capsules to use during execution */ + capsules?: Capsule[]; +}; + /** * Represents options for calling a (constrained) function in a contract. - * Allows the user to specify the sender address and nonce for a transaction. */ -export type SendMethodOptions = { +export type SendMethodOptions = RequestMethodOptions & { /** Wether to skip the simulation of the public part of the transaction. */ skipPublicSimulation?: boolean; /** The fee options for the transaction. */ @@ -25,10 +35,6 @@ export type SendMethodOptions = { nonce?: Fr; /** Whether the transaction can be cancelled. If true, an extra nullifier will be emitted: H(nonce, GENERATOR_INDEX__TX_NULLIFIER) */ cancellable?: boolean; - /** Authwits to use in the simulation */ - authWitnesses?: AuthWitness[]; - /** Capsules to use in the simulation */ - capsules?: Capsule[]; }; /** @@ -42,7 +48,6 @@ export abstract class BaseContractInteraction { protected wallet: Wallet, protected authWitnesses: AuthWitness[] = [], protected capsules: Capsule[] = [], - protected extraHashedValues: HashedValues[] = [], ) {} /** @@ -58,7 +63,7 @@ export abstract class BaseContractInteraction { * @param options - An optional object containing additional configuration for the transaction. * @returns An execution request wrapped in promise. */ - public abstract request(options?: SendMethodOptions): Promise; + public abstract request(options?: RequestMethodOptions): Promise; /** * Creates a transaction execution request, simulates and proves it. Differs from .prove in @@ -153,8 +158,8 @@ export abstract class BaseContractInteraction { */ protected async getFeeOptions( executionPayload: ExecutionPayload, - fee?: UserFeeOptions, - options?: TxExecutionOptions, + fee: UserFeeOptions = {}, + options: TxExecutionOptions, ): Promise { // docs:end:getFeeOptions const defaultFeeOptions = await this.getDefaultFeeOptions(fee); @@ -165,7 +170,7 @@ export abstract class BaseContractInteraction { let gasSettings = defaultFeeOptions.gasSettings; if (fee?.estimateGas) { const feeForEstimation: FeeOptions = { paymentMethod, gasSettings }; - const txRequest = await this.wallet.createTxExecutionRequest(executionPayload, feeForEstimation, options ?? {}); + const txRequest = await this.wallet.createTxExecutionRequest(executionPayload, feeForEstimation, options); const simulationResult = await this.wallet.simulateTx( txRequest, true /*simulatePublic*/, @@ -185,25 +190,4 @@ export abstract class BaseContractInteraction { return { gasSettings, paymentMethod }; } - - /** - * Return all authWitnesses added for this interaction. - */ - public getAuthWitnesses() { - return this.authWitnesses; - } - - /** - * Return all capsules added for this contract interaction. - */ - public getCapsules() { - return this.capsules; - } - - /** - * Return all extra hashed values added for this contract interaction. - */ - public getExtraHashedValues() { - return this.extraHashedValues; - } } diff --git a/yarn-project/aztec.js/src/contract/batch_call.ts b/yarn-project/aztec.js/src/contract/batch_call.ts index 34835460cff1..e0844f93c414 100644 --- a/yarn-project/aztec.js/src/contract/batch_call.ts +++ b/yarn-project/aztec.js/src/contract/batch_call.ts @@ -1,10 +1,14 @@ import type { ExecutionPayload } from '@aztec/entrypoints/payload'; -import { mergeExecutionPayloads } from '@aztec/entrypoints/utils'; +import { mergeExecutionPayloads } from '@aztec/entrypoints/payload'; import { type FunctionCall, FunctionType, decodeFromAbi } from '@aztec/stdlib/abi'; import type { TxExecutionRequest } from '@aztec/stdlib/tx'; import type { Wallet } from '../wallet/wallet.js'; -import { BaseContractInteraction, type SendMethodOptions } from './base_contract_interaction.js'; +import { + BaseContractInteraction, + type RequestMethodOptions, + type SendMethodOptions, +} from './base_contract_interaction.js'; import type { SimulateMethodOptions } from './contract_function_interaction.js'; /** A batch of function calls to be sent as a single transaction through a wallet. */ @@ -30,11 +34,11 @@ export class BatchCall extends BaseContractInteraction { /** * Returns an execution request that represents this operation. - * @param _options - (ignored) An optional object containing additional configuration for the transaction. - * @returns An execution request wrapped in promise. + * @param options - An optional object containing additional configuration for the request generation. + * @returns An execution payload wrapped in promise. */ - public async request(_options: SendMethodOptions = {}): Promise { - const requests = await this.getRequests(); + public async request(options: RequestMethodOptions = {}): Promise { + const requests = await this.getRequests(options); return mergeExecutionPayloads(requests); } @@ -48,7 +52,7 @@ export class BatchCall extends BaseContractInteraction { * @returns The result of the transaction as returned by the contract function. */ public async simulate(options: SimulateMethodOptions = {}): Promise { - const { indexedExecutionPayloads, unconstrained } = (await this.getRequests()).reduce<{ + const { indexedExecutionPayloads, unconstrained } = (await this.getRequests(options)).reduce<{ /** Keep track of the number of private calls to retrieve the return values */ privateIndex: 0; /** Keep track of the number of public calls to retrieve the return values */ @@ -76,9 +80,9 @@ export class BatchCall extends BaseContractInteraction { const payloads = indexedExecutionPayloads.map(([request]) => request); const requestWithoutFee = mergeExecutionPayloads(payloads); - const { fee: userFee } = options; - const fee = await this.getFeeOptions(requestWithoutFee, userFee); - const txRequest = await this.wallet.createTxExecutionRequest(requestWithoutFee, fee, {}); + const { fee: userFee, nonce, cancellable } = options; + const fee = await this.getFeeOptions(requestWithoutFee, userFee, {}); + const txRequest = await this.wallet.createTxExecutionRequest(requestWithoutFee, fee, { nonce, cancellable }); const unconstrainedCalls = unconstrained.map( async ([call, index]) => @@ -113,21 +117,7 @@ export class BatchCall extends BaseContractInteraction { return results; } - /** - * Return all authWitnesses added for this interaction. - */ - public override getAuthWitnesses() { - return [this.authWitnesses, ...this.calls.map(c => c.getAuthWitnesses())].flat(); - } - - /** - * Return all capsules added for this interaction. - */ - public override getCapsules() { - return [this.capsules, ...this.calls.map(c => c.getCapsules())].flat(); - } - - private async getRequests() { - return await Promise.all(this.calls.map(c => c.request())); + private async getRequests(options: RequestMethodOptions = {}) { + return await Promise.all(this.calls.map(c => c.request(options))); } } diff --git a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts index 2f986eff8a34..cb39a37438fd 100644 --- a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts +++ b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts @@ -4,9 +4,12 @@ import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { Capsule, HashedValues, TxExecutionRequest, TxProfileResult } from '@aztec/stdlib/tx'; -import { FeeJuicePaymentMethod } from '../fee/fee_juice_payment_method.js'; import type { Wallet } from '../wallet/wallet.js'; -import { BaseContractInteraction, type SendMethodOptions } from './base_contract_interaction.js'; +import { + BaseContractInteraction, + type RequestMethodOptions, + type SendMethodOptions, +} from './base_contract_interaction.js'; export type { SendMethodOptions }; @@ -15,33 +18,31 @@ export type { SendMethodOptions }; * Allows specifying the address from which the view method should be called. * Disregarded for simulation of public functions */ -export type ProfileMethodOptions = Pick & { +export type ProfileMethodOptions = Pick< + SendMethodOptions, + 'authWitnesses' | 'capsules' | 'fee' | 'nonce' | 'cancellable' +> & { /** Whether to return gates information or the bytecode/witnesses. */ profileMode: 'gates' | 'execution-steps' | 'full'; /** The sender's Aztec address. */ from?: AztecAddress; - /** Authwits to use in the simulation */ - authWitnesses?: AuthWitness[]; - /** Capsules to use in the simulation */ - capsules?: Capsule[]; }; /** * Represents the options for simulating a contract function interaction. - * Allows specifying the address from which the view method should be called. + * Allows specifying the address from which the method should be called. * Disregarded for simulation of public functions */ -export type SimulateMethodOptions = Pick & { +export type SimulateMethodOptions = Pick< + SendMethodOptions, + 'authWitnesses' | 'capsules' | 'fee' | 'nonce' | 'cancellable' +> & { /** The sender's Aztec address. */ from?: AztecAddress; /** Simulate without checking for the validity of the resulting transaction, e.g. whether it emits any existing nullifiers. */ skipTxValidation?: boolean; /** Whether to ensure the fee payer is not empty and has enough balance to pay for the fee. */ skipFeeEnforcement?: boolean; - /** Authwits to use in the simulation */ - authWitnesses?: AuthWitness[]; - /** Capsules to use in the simulation */ - capsules?: Capsule[]; }; /** @@ -56,9 +57,9 @@ export class ContractFunctionInteraction extends BaseContractInteraction { protected args: any[], authWitnesses: AuthWitness[] = [], capsules: Capsule[] = [], - extraHashedValues: HashedValues[] = [], + private extraHashedArgs: HashedValues[] = [], ) { - super(wallet, authWitnesses, capsules, extraHashedValues); + super(wallet, authWitnesses, capsules); if (args.some(arg => arg === undefined || arg === null)) { throw new Error('All function interaction arguments must be defined and not null. Received: ' + args); } @@ -88,10 +89,10 @@ export class ContractFunctionInteraction extends BaseContractInteraction { /** * Returns an execution request that represents this operation. * Can be used as a building block for constructing batch requests. - * @param options - An optional object containing additional configuration for the transaction. - * @returns An execution request wrapped in promise. + * @param options - An optional object containing additional configuration for the request generation. + * @returns An execution payload wrapped in promise. */ - public async request(options: SendMethodOptions = {}): Promise { + public async request(options: RequestMethodOptions = {}): Promise { // docs:end:request const args = encodeArguments(this.functionDao, this.args); const calls = [ @@ -110,7 +111,7 @@ export class ContractFunctionInteraction extends BaseContractInteraction { calls, this.authWitnesses.concat(authWitnesses ?? []), this.capsules.concat(capsules ?? []), - this.extraHashedValues, + this.extraHashedArgs, ); } @@ -136,9 +137,7 @@ export class ContractFunctionInteraction extends BaseContractInteraction { ); } - const fee = options.fee ?? { paymentMethod: new FeeJuicePaymentMethod(AztecAddress.ZERO) }; - const { authWitnesses, capsules } = options; - const txRequest = await this.create({ fee, authWitnesses, capsules }); + const txRequest = await this.create(options); const simulatedTx = await this.wallet.simulateTx( txRequest, true /* simulatePublic */, @@ -180,4 +179,34 @@ export class ContractFunctionInteraction extends BaseContractInteraction { const txRequest = await this.create({ fee, authWitnesses, capsules }); return await this.wallet.profileTx(txRequest, options.profileMode, options?.from); } + + /** + * Augments this ContractFunctionInteraction with additional metadata, such as authWitnesses, capsules, and extraHashedArgs. + * This is useful when creating a "batteries included" interaction, such as registering a contract class with its associated + * capsule instead of having the user provide them externally. + * @param options - An object containing the metadata to add to the interaction + * @returns A new ContractFunctionInteraction with the added metadata, but calling the same original function in the same manner + */ + public with({ + authWitnesses = [], + capsules = [], + extraHashedArgs = [], + }: { + /** The authWitnesses to add to the interaction */ + authWitnesses?: AuthWitness[]; + /** The capsules to add to the interaction */ + capsules?: Capsule[]; + /** The extra hashed args to add to the interaction */ + extraHashedArgs?: HashedValues[]; + }): ContractFunctionInteraction { + return new ContractFunctionInteraction( + this.wallet, + this.contractAddress, + this.functionDao, + this.args, + this.authWitnesses.concat(authWitnesses), + this.capsules.concat(capsules), + this.extraHashedArgs.concat(extraHashedArgs), + ); + } } diff --git a/yarn-project/aztec.js/src/contract/deploy_method.ts b/yarn-project/aztec.js/src/contract/deploy_method.ts index 14ae30097f6a..41b55c71c715 100644 --- a/yarn-project/aztec.js/src/contract/deploy_method.ts +++ b/yarn-project/aztec.js/src/contract/deploy_method.ts @@ -1,5 +1,5 @@ import type { ExecutionPayload } from '@aztec/entrypoints/payload'; -import { mergeExecutionPayloads } from '@aztec/entrypoints/utils'; +import { mergeExecutionPayloads } from '@aztec/entrypoints/payload'; import type { Fr } from '@aztec/foundation/fields'; import { type ContractArtifact, type FunctionAbi, type FunctionArtifact, getInitializer } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; @@ -125,9 +125,9 @@ export class DeployMethod extends Bas } /** - * Returns calls for registration of the class and deployment of the instance, depending on the provided options. + * Returns the execution payload for registration of the class and deployment of the instance, depending on the provided options. * @param options - Deployment options. - * @returns A function call array with potentially requests to the class registerer and instance deployer. + * @returns An execution payload with potentially calls (and bytecode capsule) to the class registerer and instance deployer. */ protected async getDeploymentExecutionPayload(options: DeployOptions = {}): Promise { const calls: ExecutionPayload[] = []; diff --git a/yarn-project/aztec.js/src/deployment/broadcast_function.ts b/yarn-project/aztec.js/src/deployment/broadcast_function.ts index e37af7e96081..6bfd4815a1fa 100644 --- a/yarn-project/aztec.js/src/deployment/broadcast_function.ts +++ b/yarn-project/aztec.js/src/deployment/broadcast_function.ts @@ -15,7 +15,7 @@ import { } from '@aztec/stdlib/contract'; import { Capsule } from '@aztec/stdlib/tx'; -import { ContractFunctionInteraction } from '../contract/contract_function_interaction.js'; +import type { ContractFunctionInteraction } from '../contract/contract_function_interaction.js'; import { getRegistererContract } from '../contract/protocol_contracts.js'; import type { Wallet } from '../wallet/index.js'; @@ -59,16 +59,12 @@ export async function broadcastPrivateFunction( const vkHash = await computeVerificationKeyHash(privateFunctionArtifact); const registerer = await getRegistererContract(wallet); - const broadcastFunctionArtifact = registerer.artifact.functions.find(f => f.name === 'broadcast_private_function')!; const bytecode = bufferAsFields( privateFunctionArtifact.bytecode, MAX_PACKED_BYTECODE_SIZE_PER_PRIVATE_FUNCTION_IN_FIELDS, ); - const fn = new ContractFunctionInteraction( - wallet, - registerer.address, - broadcastFunctionArtifact!, - [ + return registerer.methods + .broadcast_private_function( contractClass.id, artifactMetadataHash, unconstrainedFunctionsArtifactTreeRoot, @@ -78,18 +74,16 @@ export async function broadcastPrivateFunction( artifactTreeLeafIndex, // eslint-disable-next-line camelcase { selector, metadata_hash: functionMetadataHash, vk_hash: vkHash }, - ], - [], - [ - new Capsule( - ProtocolContractAddress.ContractClassRegisterer, - new Fr(REGISTERER_CONTRACT_BYTECODE_CAPSULE_SLOT), - bytecode, - ), - ], - ); - - return fn; + ) + .with({ + capsules: [ + new Capsule( + ProtocolContractAddress.ContractClassRegisterer, + new Fr(REGISTERER_CONTRACT_BYTECODE_CAPSULE_SLOT), + bytecode, + ), + ], + }); } /** @@ -128,19 +122,12 @@ export async function broadcastUnconstrainedFunction( } = await createUnconstrainedFunctionMembershipProof(selector, artifact); const registerer = await getRegistererContract(wallet); - const broadcastFunctionArtifact = registerer.artifact.functions.find( - f => f.name === 'broadcast_unconstrained_function', - ); const bytecode = bufferAsFields( unconstrainedFunctionArtifact.bytecode, MAX_PACKED_BYTECODE_SIZE_PER_PRIVATE_FUNCTION_IN_FIELDS, ); - - const fn = new ContractFunctionInteraction( - wallet, - registerer.address, - broadcastFunctionArtifact!, - [ + return registerer.methods + .broadcast_unconstrained_function( contractClass.id, artifactMetadataHash, privateFunctionsArtifactTreeRoot, @@ -148,16 +135,14 @@ export async function broadcastUnconstrainedFunction( artifactTreeLeafIndex, // eslint-disable-next-line camelcase { selector, metadata_hash: functionMetadataHash }, - ], - [], - [ - new Capsule( - ProtocolContractAddress.ContractClassRegisterer, - new Fr(REGISTERER_CONTRACT_BYTECODE_CAPSULE_SLOT), - bytecode, - ), - ], - ); - - return fn; + ) + .with({ + capsules: [ + new Capsule( + ProtocolContractAddress.ContractClassRegisterer, + new Fr(REGISTERER_CONTRACT_BYTECODE_CAPSULE_SLOT), + bytecode, + ), + ], + }); } diff --git a/yarn-project/aztec.js/src/deployment/register_class.ts b/yarn-project/aztec.js/src/deployment/register_class.ts index ae49f8d7d441..eddd4759f392 100644 --- a/yarn-project/aztec.js/src/deployment/register_class.ts +++ b/yarn-project/aztec.js/src/deployment/register_class.ts @@ -5,7 +5,7 @@ import { type ContractArtifact, bufferAsFields } from '@aztec/stdlib/abi'; import { getContractClassFromArtifact } from '@aztec/stdlib/contract'; import { Capsule } from '@aztec/stdlib/tx'; -import { ContractFunctionInteraction } from '../contract/contract_function_interaction.js'; +import type { ContractFunctionInteraction } from '../contract/contract_function_interaction.js'; import { getRegistererContract } from '../contract/protocol_contracts.js'; import type { Wallet } from '../wallet/index.js'; @@ -17,21 +17,15 @@ export async function registerContractClass( const { artifactHash, privateFunctionsRoot, publicBytecodeCommitment, packedBytecode } = await getContractClassFromArtifact(artifact); const registerer = await getRegistererContract(wallet); - const functionArtifact = registerer.artifact.functions.find(f => f.name === 'register'); + const encodedBytecode = bufferAsFields(packedBytecode, MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS); - const interaction = new ContractFunctionInteraction( - wallet, - registerer.address, - functionArtifact!, - [artifactHash, privateFunctionsRoot, publicBytecodeCommitment], - [], - [ + return registerer.methods.register(artifactHash, privateFunctionsRoot, publicBytecodeCommitment).with({ + capsules: [ new Capsule( ProtocolContractAddress.ContractClassRegisterer, new Fr(REGISTERER_CONTRACT_BYTECODE_CAPSULE_SLOT), encodedBytecode, ), ], - ); - return interaction; + }); } diff --git a/yarn-project/aztec.js/src/entrypoint/default_multi_call_entrypoint.ts b/yarn-project/aztec.js/src/entrypoint/default_multi_call_entrypoint.ts index 94479349d79c..785a06e8dc2d 100644 --- a/yarn-project/aztec.js/src/entrypoint/default_multi_call_entrypoint.ts +++ b/yarn-project/aztec.js/src/entrypoint/default_multi_call_entrypoint.ts @@ -1,6 +1,6 @@ +import { EncodedCallsForEntrypoint } from '@aztec/entrypoints/encoding'; import type { EntrypointInterface, FeeOptions } from '@aztec/entrypoints/interfaces'; -import { EncodedExecutionPayloadForEntrypoint, ExecutionPayload } from '@aztec/entrypoints/payload'; -import { mergeAndEncodeExecutionPayloads } from '@aztec/entrypoints/utils'; +import { ExecutionPayload } from '@aztec/entrypoints/payload'; import { ProtocolContractAddress } from '@aztec/protocol-contracts'; import { type FunctionAbi, FunctionSelector, encodeArguments } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; @@ -17,25 +17,25 @@ export class DefaultMultiCallEntrypoint implements EntrypointInterface { ) {} async createTxExecutionRequest(exec: ExecutionPayload, fee: FeeOptions): Promise { - const { calls, authWitnesses: userAuthWitnesses = [], capsules: userCapsules = [], extraHashedValues = [] } = exec; - const encodedPayload = await EncodedExecutionPayloadForEntrypoint.fromAppExecution(calls); - const abi = this.getEntrypointAbi(); - const entrypointHashedArgs = await HashedValues.fromValues(encodeArguments(abi, [encodedPayload])); + // Initial request with calls, authWitnesses and capsules + const { calls, authWitnesses, capsules, extraHashedArgs } = exec; - const encodedExecutionPayload = await mergeAndEncodeExecutionPayloads([encodedPayload], { - extraHashedArgs: [entrypointHashedArgs, ...extraHashedValues], - extraAuthWitnesses: userAuthWitnesses, - extraCapsules: userCapsules, - }); + // Encode the calls + const encodedCalls = await EncodedCallsForEntrypoint.fromAppExecution(calls); + + // Obtain the entrypoint hashed args, built from the encoded calls + const abi = this.getEntrypointAbi(); + const entrypointHashedArgs = await HashedValues.fromValues(encodeArguments(abi, [encodedCalls])); + // Assemble the tx request const txRequest = TxExecutionRequest.from({ firstCallArgsHash: entrypointHashedArgs.hash, origin: this.address, functionSelector: await FunctionSelector.fromNameAndParameters(abi.name, abi.parameters), txContext: new TxContext(this.chainId, this.version, fee.gasSettings), - argsOfCalls: encodedExecutionPayload.hashedArguments, - authWitnesses: encodedExecutionPayload.authWitnesses, - capsules: encodedExecutionPayload.capsules, + argsOfCalls: [...encodedCalls.hashedArguments, entrypointHashedArgs, ...extraHashedArgs], + authWitnesses, + capsules, }); return Promise.resolve(txRequest); diff --git a/yarn-project/aztec.js/src/fee/private_fee_payment_method.ts b/yarn-project/aztec.js/src/fee/private_fee_payment_method.ts index 871234648fd4..76eec682386e 100644 --- a/yarn-project/aztec.js/src/fee/private_fee_payment_method.ts +++ b/yarn-project/aztec.js/src/fee/private_fee_payment_method.ts @@ -40,29 +40,34 @@ export class PrivateFeePaymentMethod implements FeePaymentMethod { if (!this.assetPromise) { // We use the utility method to avoid a signature because this function could be triggered // before the associated account is deployed. - this.assetPromise = simulateWithoutSignature(this.wallet, this.paymentContract, { - name: 'get_accepted_asset', - functionType: FunctionType.PRIVATE, - isInternal: false, - isStatic: false, - parameters: [], - returnTypes: [ - { - kind: 'struct', - path: 'authwit::aztec::protocol_types::address::aztec_address::AztecAddress', - fields: [ - { - name: 'inner', - type: { - kind: 'field', + this.assetPromise = simulateWithoutSignature( + this.wallet, + this.paymentContract, + { + name: 'get_accepted_asset', + functionType: FunctionType.PRIVATE, + isInternal: false, + isStatic: false, + parameters: [], + returnTypes: [ + { + kind: 'struct', + path: 'authwit::aztec::protocol_types::address::aztec_address::AztecAddress', + fields: [ + { + name: 'inner', + type: { + kind: 'field', + }, }, - }, - ], - }, - ], - errorTypes: {}, - isInitializer: false, - }) as Promise; + ], + }, + ], + errorTypes: {}, + isInitializer: false, + }, + [], + ) as Promise; } return this.assetPromise!; } diff --git a/yarn-project/aztec.js/src/fee/public_fee_payment_method.ts b/yarn-project/aztec.js/src/fee/public_fee_payment_method.ts index 8c7a5724dfd6..790b913b4be3 100644 --- a/yarn-project/aztec.js/src/fee/public_fee_payment_method.ts +++ b/yarn-project/aztec.js/src/fee/public_fee_payment_method.ts @@ -33,29 +33,34 @@ export class PublicFeePaymentMethod implements FeePaymentMethod { if (!this.assetPromise) { // We use the utility method to avoid a signature because this function could be triggered // before the associated account is deployed. - this.assetPromise = simulateWithoutSignature(this.wallet, this.paymentContract, { - name: 'get_accepted_asset', - functionType: FunctionType.PRIVATE, - isInternal: false, - isStatic: false, - parameters: [], - returnTypes: [ - { - kind: 'struct', - path: 'authwit::aztec::protocol_types::address::aztec_address::AztecAddress', - fields: [ - { - name: 'inner', - type: { - kind: 'field', + this.assetPromise = simulateWithoutSignature( + this.wallet, + this.paymentContract, + { + name: 'get_accepted_asset', + functionType: FunctionType.PRIVATE, + isInternal: false, + isStatic: false, + parameters: [], + returnTypes: [ + { + kind: 'struct', + path: 'authwit::aztec::protocol_types::address::aztec_address::AztecAddress', + fields: [ + { + name: 'inner', + type: { + kind: 'field', + }, }, - }, - ], - }, - ], - errorTypes: {}, - isInitializer: false, - }) as Promise; + ], + }, + ], + errorTypes: {}, + isInitializer: false, + }, + [], + ) as Promise; } return this.assetPromise!; } diff --git a/yarn-project/aztec.js/src/fee/utils.ts b/yarn-project/aztec.js/src/fee/utils.ts index 9961ea56dc56..df1e4ecb26cc 100644 --- a/yarn-project/aztec.js/src/fee/utils.ts +++ b/yarn-project/aztec.js/src/fee/utils.ts @@ -12,10 +12,16 @@ import { FeeJuicePaymentMethod } from './fee_juice_payment_method.js'; * @param wallet - The wallet to use for the simulation. * @param contractAddress - The address of the contract to call. * @param abi - The ABI of the function to simulate. + * @param args - The arguments to pass to the function. * @returns The return values of the function call. */ -export async function simulateWithoutSignature(wallet: Wallet, contractAddress: AztecAddress, abi: FunctionAbi) { - const interaction = new ContractFunctionInteraction(wallet, contractAddress, abi, []); +export async function simulateWithoutSignature( + wallet: Wallet, + contractAddress: AztecAddress, + abi: FunctionAbi, + args: any[], +) { + const interaction = new ContractFunctionInteraction(wallet, contractAddress, abi, args); const request = await interaction.request(); const maxFeesPerGas = (await wallet.getCurrentBaseFees()).mul(1.5); diff --git a/yarn-project/end-to-end/src/e2e_amm.test.ts b/yarn-project/end-to-end/src/e2e_amm.test.ts index 892ca108540b..22a40717cda0 100644 --- a/yarn-project/end-to-end/src/e2e_amm.test.ts +++ b/yarn-project/end-to-end/src/e2e_amm.test.ts @@ -127,9 +127,10 @@ describe('AMM', () => { const addLiquidityInteraction = amm .withWallet(liquidityProvider) - .methods.add_liquidity(amount0Max, amount1Max, amount0Min, amount1Min, nonceForAuthwits); + .methods.add_liquidity(amount0Max, amount1Max, amount0Min, amount1Min, nonceForAuthwits) + .with({ authWitnesses: [token0Authwit, token1Authwit] }); await capturePrivateExecutionStepsIfEnvSet('amm-add-liquidity', addLiquidityInteraction); - await addLiquidityInteraction.send({ authWitnesses: [token0Authwit, token1Authwit] }).wait(); + await addLiquidityInteraction.send().wait(); const ammBalancesAfter = await getAmmBalances(); const lpBalancesAfter = await getWalletBalances(liquidityProvider); @@ -237,9 +238,10 @@ describe('AMM', () => { const swapExactTokensInteraction = amm .withWallet(swapper) - .methods.swap_exact_tokens_for_tokens(token0.address, token1.address, amountIn, amountOutMin, nonceForAuthwits); + .methods.swap_exact_tokens_for_tokens(token0.address, token1.address, amountIn, amountOutMin, nonceForAuthwits) + .with({ authWitnesses: [swapAuthwit] }); await capturePrivateExecutionStepsIfEnvSet('amm-swap-exact-tokens', swapExactTokensInteraction); - await swapExactTokensInteraction.send({ authWitnesses: [swapAuthwit] }).wait(); + await swapExactTokensInteraction.send().wait(); // We know exactly how many tokens we're supposed to get because we know nobody else interacted with the AMM // before we did. diff --git a/yarn-project/entrypoints/package.json b/yarn-project/entrypoints/package.json index a1067853c861..f9fbc81ca1a2 100644 --- a/yarn-project/entrypoints/package.json +++ b/yarn-project/entrypoints/package.json @@ -10,7 +10,7 @@ "./default": "./dest/default_entrypoint.js", "./interfaces": "./dest/interfaces.js", "./payload": "./dest/payload.js", - "./utils": "./dest/utils.js" + "./encoding": "./dest/encoding.js" }, "typedocOptions": { "entryPoints": [ diff --git a/yarn-project/entrypoints/src/account_entrypoint.ts b/yarn-project/entrypoints/src/account_entrypoint.ts index 8cc3301995f2..6ab6bed806e2 100644 --- a/yarn-project/entrypoints/src/account_entrypoint.ts +++ b/yarn-project/entrypoints/src/account_entrypoint.ts @@ -3,9 +3,9 @@ import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { HashedValues, TxContext, TxExecutionRequest } from '@aztec/stdlib/tx'; import { DEFAULT_CHAIN_ID, DEFAULT_VERSION } from './constants.js'; +import { EncodedCallsForEntrypoint, computeCombinedPayloadHash } from './encoding.js'; import type { AuthWitnessProvider, EntrypointInterface, FeeOptions, TxExecutionOptions } from './interfaces.js'; -import { EncodedExecutionPayloadForEntrypoint, ExecutionPayload, computeCombinedPayloadHash } from './payload.js'; -import { mergeAndEncodeExecutionPayloads } from './utils.js'; +import { ExecutionPayload } from './payload.js'; /** * Implementation for an entrypoint interface that follows the default entrypoint signature @@ -24,34 +24,46 @@ export class DefaultAccountEntrypoint implements EntrypointInterface { fee: FeeOptions, options: TxExecutionOptions, ): Promise { - const { calls, authWitnesses: userAuthWitnesses = [], capsules: userCapsules = [] } = exec; + // Initial request with calls, authWitnesses and capsules + const { calls, authWitnesses, capsules, extraHashedArgs } = exec; + // Global tx options const { cancellable, nonce } = options; - const appPayload = await EncodedExecutionPayloadForEntrypoint.fromAppExecution(calls, nonce); - const feePayload = await EncodedExecutionPayloadForEntrypoint.fromFeeOptions(this.address, fee); + // Encode the calls for the app + const appEncodedCalls = await EncodedCallsForEntrypoint.fromAppExecution(calls, nonce); + // Get the execution payload for the fee, it includes the calls and potentially authWitnesses + const { calls: feeCalls, authWitnesses: feeAuthwitnesses } = await fee.paymentMethod.getExecutionPayload( + fee.gasSettings, + ); + // Encode the calls for the fee + const feePayer = await fee.paymentMethod.getFeePayer(fee.gasSettings); + const isFeePayer = feePayer.equals(this.address); + const feeEncodedCalls = await EncodedCallsForEntrypoint.fromFeeCalls(feeCalls, isFeePayer); + // Obtain the entrypoint hashed args, built from the app and fee encoded calls const abi = this.getEntrypointAbi(); const entrypointHashedArgs = await HashedValues.fromValues( - encodeArguments(abi, [appPayload, feePayload, !!cancellable]), + encodeArguments(abi, [appEncodedCalls, feeEncodedCalls, !!cancellable]), ); + // Generate the combined payload auth witness, by signing the hash of the combined payload const combinedPayloadAuthWitness = await this.auth.createAuthWit( - await computeCombinedPayloadHash(appPayload, feePayload), + await computeCombinedPayloadHash(appEncodedCalls, feeEncodedCalls), ); - const encodedExecutionPayload = await mergeAndEncodeExecutionPayloads([appPayload, feePayload], { - extraHashedArgs: [entrypointHashedArgs], - extraAuthWitnesses: [combinedPayloadAuthWitness, ...userAuthWitnesses], - extraCapsules: userCapsules, - }); - + // Assemble the tx request const txRequest = TxExecutionRequest.from({ firstCallArgsHash: entrypointHashedArgs.hash, origin: this.address, functionSelector: await FunctionSelector.fromNameAndParameters(abi.name, abi.parameters), txContext: new TxContext(this.chainId, this.version, fee.gasSettings), - argsOfCalls: encodedExecutionPayload.hashedArguments, - authWitnesses: encodedExecutionPayload.authWitnesses, - capsules: encodedExecutionPayload.capsules, + argsOfCalls: [ + ...appEncodedCalls.hashedArguments, + ...feeEncodedCalls.hashedArguments, + entrypointHashedArgs, + ...extraHashedArgs, + ], + authWitnesses: [...authWitnesses, ...feeAuthwitnesses, combinedPayloadAuthWitness], + capsules, }); return txRequest; diff --git a/yarn-project/entrypoints/src/dapp_entrypoint.ts b/yarn-project/entrypoints/src/dapp_entrypoint.ts index 10747dd1ffad..282c97a67a87 100644 --- a/yarn-project/entrypoints/src/dapp_entrypoint.ts +++ b/yarn-project/entrypoints/src/dapp_entrypoint.ts @@ -5,9 +5,9 @@ import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { HashedValues, TxContext, TxExecutionRequest } from '@aztec/stdlib/tx'; import { DEFAULT_CHAIN_ID, DEFAULT_VERSION } from './constants.js'; +import { EncodedCallsForEntrypoint } from './encoding.js'; import type { AuthWitnessProvider, EntrypointInterface, FeeOptions, TxExecutionOptions } from './interfaces.js'; -import { EncodedExecutionPayloadForEntrypoint, ExecutionPayload } from './payload.js'; -import { mergeAndEncodeExecutionPayloads } from './utils.js'; +import { ExecutionPayload } from './payload.js'; /** * Implementation for an entrypoint interface that follows the default entrypoint signature @@ -25,19 +25,26 @@ export class DefaultDappEntrypoint implements EntrypointInterface { async createTxExecutionRequest( exec: ExecutionPayload, fee: FeeOptions, - _options: TxExecutionOptions, + options: TxExecutionOptions, ): Promise { - const { calls, authWitnesses: userAuthWitnesses = [], capsules: userCapsules = [] } = exec; + if (options.nonce || options.cancellable !== undefined) { + throw new Error('TxExecutionOptions are not supported in DappEntrypoint'); + } + // Initial request with calls, authWitnesses and capsules + const { calls, authWitnesses, capsules, extraHashedArgs } = exec; if (calls.length !== 1) { throw new Error(`Expected exactly 1 function call, got ${calls.length}`); } - const encodedPayload = await EncodedExecutionPayloadForEntrypoint.fromFunctionCalls(calls); + // Encode the function call the dapp is ultimately going to invoke + const encodedCalls = await EncodedCallsForEntrypoint.fromFunctionCalls(calls); + // Obtain the entrypoint hashed args, built from the function call and the user's address const abi = this.getEntrypointAbi(); - const entrypointHashedArgs = await HashedValues.fromValues( - encodeArguments(abi, [encodedPayload, this.userAddress]), - ); + const entrypointHashedArgs = await HashedValues.fromValues(encodeArguments(abi, [encodedCalls, this.userAddress])); + + // Construct an auth witness for the entrypoint, by signing the hash of the action to perform + // (the dapp calls a function on the user's behalf) const functionSelector = await FunctionSelector.fromNameAndParameters(abi.name, abi.parameters); // Default msg_sender for entrypoints is now Fr.max_value rather than 0 addr (see #7190 & #7404) const innerHash = await computeInnerAuthWitHash([ @@ -52,22 +59,17 @@ export class DefaultDappEntrypoint implements EntrypointInterface { innerHash, ); - const authWitness = await this.userAuthWitnessProvider.createAuthWit(outerHash); - - const executionPayload = await mergeAndEncodeExecutionPayloads([encodedPayload], { - extraAuthWitnesses: [authWitness, ...userAuthWitnesses], - extraHashedArgs: [entrypointHashedArgs], - extraCapsules: userCapsules, - }); + const entypointAuthwitness = await this.userAuthWitnessProvider.createAuthWit(outerHash); + // Assemble the tx request const txRequest = TxExecutionRequest.from({ firstCallArgsHash: entrypointHashedArgs.hash, origin: this.dappEntrypointAddress, functionSelector, txContext: new TxContext(this.chainId, this.version, fee.gasSettings), - argsOfCalls: executionPayload.hashedArguments, - authWitnesses: executionPayload.authWitnesses, - capsules: executionPayload.capsules, + argsOfCalls: [...encodedCalls.hashedArguments, entrypointHashedArgs, ...extraHashedArgs], + authWitnesses: [entypointAuthwitness, ...authWitnesses], + capsules, }); return txRequest; diff --git a/yarn-project/entrypoints/src/default_entrypoint.ts b/yarn-project/entrypoints/src/default_entrypoint.ts index 240a4dd46d06..9085999c7c01 100644 --- a/yarn-project/entrypoints/src/default_entrypoint.ts +++ b/yarn-project/entrypoints/src/default_entrypoint.ts @@ -13,9 +13,13 @@ export class DefaultEntrypoint implements EntrypointInterface { async createTxExecutionRequest( exec: ExecutionPayload, fee: FeeOptions, - _options: TxExecutionOptions, + options: TxExecutionOptions, ): Promise { - const { calls, authWitnesses = [], capsules = [] } = exec; + if (options.nonce || options.cancellable !== undefined) { + throw new Error('TxExecutionOptions are not supported in DefaultEntrypoint'); + } + // Initial request with calls, authWitnesses and capsules + const { calls, authWitnesses, capsules, extraHashedArgs } = exec; if (calls.length > 1) { throw new Error(`Expected a single call, got ${calls.length}`); @@ -23,19 +27,20 @@ export class DefaultEntrypoint implements EntrypointInterface { const call = calls[0]; + // Hash the arguments for the function call const hashedArguments = [await HashedValues.fromValues(call.args)]; if (call.type !== FunctionType.PRIVATE) { throw new Error('Public entrypoints are not allowed'); } - const txContext = new TxContext(this.chainId, this.protocolVersion, fee.gasSettings); + // Assemble the tx request return new TxExecutionRequest( call.to, call.selector, hashedArguments[0].hash, - txContext, - [...hashedArguments], + new TxContext(this.chainId, this.protocolVersion, fee.gasSettings), + [...hashedArguments, ...extraHashedArgs], authWitnesses, capsules, ); diff --git a/yarn-project/entrypoints/src/encoding.ts b/yarn-project/entrypoints/src/encoding.ts new file mode 100644 index 000000000000..df7bf6055edc --- /dev/null +++ b/yarn-project/entrypoints/src/encoding.ts @@ -0,0 +1,225 @@ +import { GeneratorIndex } from '@aztec/constants'; +import { padArrayEnd } from '@aztec/foundation/collection'; +import { poseidon2HashWithSeparator } from '@aztec/foundation/crypto'; +import { Fr } from '@aztec/foundation/fields'; +import type { Tuple } from '@aztec/foundation/serialize'; +import { FunctionCall, FunctionType } from '@aztec/stdlib/abi'; +import { HashedValues } from '@aztec/stdlib/tx'; + +// These must match the values defined in: +// - noir-projects/aztec-nr/aztec/src/entrypoint/app.nr +const APP_MAX_CALLS = 4; +// - and noir-projects/aztec-nr/aztec/src/entrypoint/fee.nr +const FEE_MAX_CALLS = 2; + +/* eslint-disable camelcase */ +/** Encoded function call for an Aztec entrypoint */ +export type EncodedFunctionCall = { + /** Arguments hash for the call */ + args_hash: Fr; + /** Selector of the function to call */ + function_selector: Fr; + /** Address of the contract to call */ + target_address: Fr; + /** Whether the function is public or private */ + is_public: boolean; + /** Whether the function can alter state */ + is_static: boolean; +}; +/* eslint-enable camelcase */ + +/** Type that represents function calls ready to be sent to a circuit for execution */ +export type EncodedCalls = { + /** Function calls in the expected format (Noir's convention) */ + encodedFunctionCalls: EncodedFunctionCall[]; + /** The hashed args for the call, ready to be injected in the execution cache */ + hashedArguments: HashedValues[]; +}; + +/** + * Entrypoints derive their arguments from the calls that they'll ultimate make. + * This utility class helps in creating the payload for the entrypoint by taking into + * account how the calls are encoded and hashed. + * */ +export abstract class EncodedCallsForEntrypoint implements EncodedCalls { + constructor( + /** Function calls in the expected format (Noir's convention) */ + public encodedFunctionCalls: EncodedFunctionCall[], + /** The hashed args for the call, ready to be injected in the execution cache */ + public hashedArguments: HashedValues[], + /** The index of the generator to use for hashing */ + public generatorIndex: number, + /** The nonce for the payload, used to emit a nullifier identifying the call */ + public nonce: Fr, + ) {} + + /* eslint-disable camelcase */ + /** + * The function calls to execute. This uses snake_case naming so that it is compatible with Noir encoding + * @internal + */ + get function_calls() { + return this.encodedFunctionCalls; + } + /* eslint-enable camelcase */ + + /** + * Serializes the payload to an array of fields + * @returns The fields of the payload + */ + abstract toFields(): Fr[]; + + /** + * Hashes the payload + * @returns The hash of the payload + */ + hash() { + return poseidon2HashWithSeparator(this.toFields(), this.generatorIndex); + } + + /** Serializes the function calls to an array of fields. */ + protected functionCallsToFields() { + return this.encodedFunctionCalls.flatMap(call => [ + call.args_hash, + call.function_selector, + call.target_address, + new Fr(call.is_public), + new Fr(call.is_static), + ]); + } + + /** + * Encodes a set of function calls for a dapp entrypoint + * @param functionCalls - The function calls to execute + * @returns The encoded calls + */ + static async fromFunctionCalls(functionCalls: FunctionCall[]) { + const encoded = await encode(functionCalls); + return new EncodedAppEntrypointCalls(encoded.encodedFunctionCalls, encoded.hashedArguments, 0, Fr.random()); + } + + /** + * Encodes the functions for the app-portion of a transaction from a set of function calls and a nonce + * @param functionCalls - The function calls to execute + * @param nonce - The nonce for the payload, used to emit a nullifier identifying the call + * @returns The encoded calls + */ + static async fromAppExecution( + functionCalls: FunctionCall[] | Tuple, + nonce = Fr.random(), + ) { + if (functionCalls.length > APP_MAX_CALLS) { + throw new Error(`Expected at most ${APP_MAX_CALLS} function calls, got ${functionCalls.length}`); + } + const paddedCalls = padArrayEnd(functionCalls, FunctionCall.empty(), APP_MAX_CALLS); + const encoded = await encode(paddedCalls); + return new EncodedAppEntrypointCalls( + encoded.encodedFunctionCalls, + encoded.hashedArguments, + GeneratorIndex.SIGNATURE_PAYLOAD, + nonce, + ); + } + + /** + * Creates an encoded set of functions to pay the fee for a transaction + * @param functionCalls - The calls generated by the payment method + * @param isFeePayer - Whether the sender should be appointed as fee payer + * @returns The encoded calls + */ + static async fromFeeCalls( + functionCalls: FunctionCall[] | Tuple, + isFeePayer: boolean, + ) { + const paddedCalls = padArrayEnd(functionCalls, FunctionCall.empty(), FEE_MAX_CALLS); + const encoded = await encode(paddedCalls); + return new EncodedFeeEntrypointCalls( + encoded.encodedFunctionCalls, + encoded.hashedArguments, + GeneratorIndex.FEE_PAYLOAD, + Fr.random(), + isFeePayer, + ); + } +} + +/** Encoded calls for app phase execution. */ +export class EncodedAppEntrypointCalls extends EncodedCallsForEntrypoint { + constructor( + encodedFunctionCalls: EncodedFunctionCall[], + hashedArguments: HashedValues[], + generatorIndex: number, + nonce: Fr, + ) { + super(encodedFunctionCalls, hashedArguments, generatorIndex, nonce); + } + + override toFields(): Fr[] { + return [...this.functionCallsToFields(), this.nonce]; + } +} + +/** Encoded calls for fee payment */ +export class EncodedFeeEntrypointCalls extends EncodedCallsForEntrypoint { + #isFeePayer: boolean; + + constructor( + encodedFunctionCalls: EncodedFunctionCall[], + hashedArguments: HashedValues[], + generatorIndex: number, + nonce: Fr, + isFeePayer: boolean, + ) { + super(encodedFunctionCalls, hashedArguments, generatorIndex, nonce); + this.#isFeePayer = isFeePayer; + } + + override toFields(): Fr[] { + return [...this.functionCallsToFields(), this.nonce, new Fr(this.#isFeePayer)]; + } + + /* eslint-disable camelcase */ + /** Whether the sender should be appointed as fee payer. */ + get is_fee_payer() { + return this.#isFeePayer; + } + /* eslint-enable camelcase */ +} + +/** + * Computes a hash of a combined set of app and fee calls. + * @param appCalls - A set of app calls. + * @param feeCalls - A set of calls used to pay fees. + * @returns A hash of a combined call set. + */ +export async function computeCombinedPayloadHash( + appPayload: EncodedAppEntrypointCalls, + feePayload: EncodedFeeEntrypointCalls, +): Promise { + return poseidon2HashWithSeparator( + [await appPayload.hash(), await feePayload.hash()], + GeneratorIndex.COMBINED_PAYLOAD, + ); +} + +/** Encodes FunctionCalls for execution, following Noir's convention */ +export async function encode(calls: FunctionCall[]): Promise { + const hashedArguments: HashedValues[] = []; + for (const call of calls) { + hashedArguments.push(await HashedValues.fromValues(call.args)); + } + + /* eslint-disable camelcase */ + const encodedFunctionCalls: EncodedFunctionCall[] = calls.map((call, index) => ({ + args_hash: hashedArguments[index].hash, + function_selector: call.selector.toField(), + target_address: call.to.toField(), + is_public: call.type == FunctionType.PUBLIC, + is_static: call.isStatic, + })); + + return { + encodedFunctionCalls, + hashedArguments, + }; +} diff --git a/yarn-project/entrypoints/src/index.ts b/yarn-project/entrypoints/src/index.ts index da04efb9b608..2c31758ff989 100644 --- a/yarn-project/entrypoints/src/index.ts +++ b/yarn-project/entrypoints/src/index.ts @@ -10,4 +10,4 @@ export * from './account_entrypoint.js'; export * from './dapp_entrypoint.js'; export * from './interfaces.js'; export * from './default_entrypoint.js'; -export * from './utils.js'; +export * from './encoding.js'; diff --git a/yarn-project/entrypoints/src/interfaces.ts b/yarn-project/entrypoints/src/interfaces.ts index fe8055f45fbd..be25be4d1a1c 100644 --- a/yarn-project/entrypoints/src/interfaces.ts +++ b/yarn-project/entrypoints/src/interfaces.ts @@ -7,22 +7,6 @@ import type { TxExecutionRequest } from '@aztec/stdlib/tx'; import type { ExecutionPayload } from './payload.js'; -/* eslint-disable camelcase */ -/** Encoded function call for an Aztec entrypoint */ -export type EncodedFunctionCall = { - /** Arguments hash for the call */ - args_hash: Fr; - /** Selector of the function to call */ - function_selector: Fr; - /** Address of the contract to call */ - target_address: Fr; - /** Whether the function is public or private */ - is_public: boolean; - /** Whether the function can alter state */ - is_static: boolean; -}; -/* eslint-enable camelcase */ - /** * General options for the tx execution. */ @@ -33,7 +17,10 @@ export type TxExecutionOptions = { nonce?: Fr; }; -/** Creates transaction execution requests out of a set of function calls. */ +/** + * Creates transaction execution requests out of a set of function calls, a fee payment method and + * general options for the transaction + */ export interface EntrypointInterface { /** * Generates an execution request out of set of function calls. diff --git a/yarn-project/entrypoints/src/payload.ts b/yarn-project/entrypoints/src/payload.ts index 41e566c07690..963e6c5cb7e8 100644 --- a/yarn-project/entrypoints/src/payload.ts +++ b/yarn-project/entrypoints/src/payload.ts @@ -1,22 +1,11 @@ -import { GeneratorIndex } from '@aztec/constants'; -import { padArrayEnd } from '@aztec/foundation/collection'; -import { poseidon2HashWithSeparator } from '@aztec/foundation/crypto'; -import { Fr } from '@aztec/foundation/fields'; -import type { Tuple } from '@aztec/foundation/serialize'; -import { FunctionCall, FunctionType } from '@aztec/stdlib/abi'; +import { FunctionCall } from '@aztec/stdlib/abi'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; -import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { Capsule, HashedValues } from '@aztec/stdlib/tx'; -import type { EncodedFunctionCall, FeeOptions } from './interfaces.js'; - -// These must match the values defined in: -// - noir-projects/aztec-nr/aztec/src/entrypoint/app.nr -const APP_MAX_CALLS = 4; -// - and noir-projects/aztec-nr/aztec/src/entrypoint/fee.nr -const FEE_MAX_CALLS = 2; - -/** Represents data necessary to execute a list of function calls successfully */ +/** + * Represents data necessary to perform an action in the network successfully. + * This class can be considered Aztec's "minimal execution unit". + * */ export class ExecutionPayload { public constructor( /** The function calls to be executed. */ @@ -26,231 +15,21 @@ export class ExecutionPayload { /** Data passed through an oracle for this execution. */ public capsules: Capsule[], /** Extra hashed values to be injected in the execution cache */ - public extraHashedValues: HashedValues[] = [], + public extraHashedArgs: HashedValues[] = [], ) {} static empty() { return new ExecutionPayload([], [], []); } - - /** Encodes the payload for execution, following Noir's convention */ - public async encode(): Promise { - const hashedArguments: HashedValues[] = []; - for (const call of this.calls) { - hashedArguments.push(await HashedValues.fromValues(call.args)); - } - - /* eslint-disable camelcase */ - const encodedFunctionCalls: EncodedFunctionCall[] = this.calls.map((call, index) => ({ - args_hash: hashedArguments[index].hash, - function_selector: call.selector.toField(), - target_address: call.to.toField(), - is_public: call.type == FunctionType.PUBLIC, - is_static: call.isStatic, - })); - - return { - encodedFunctionCalls, - hashedArguments: [...hashedArguments, ...this.extraHashedValues], - authWitnesses: this.authWitnesses, - capsules: this.capsules, - function_calls: encodedFunctionCalls, - }; - /* eslint-enable camelcase */ - } -} - -/** Representation of the encoded payload for execution */ -export type EncodedExecutionPayload = Omit & { - /** Function calls in the expected format (Noir's convention) */ - encodedFunctionCalls: EncodedFunctionCall[]; - /** The hashed args for the call, ready to be injected in the execution cache */ - hashedArguments: HashedValues[]; - /* eslint-disable camelcase */ - /** - * The function calls to execute. This uses snake_case naming so that it is compatible with Noir encoding - * */ - get function_calls(): EncodedFunctionCall[]; - /* eslint-enable camelcase */ -}; - -/** Represents the ExecutionPayload after encoding for the entrypoint to execute */ -export abstract class EncodedExecutionPayloadForEntrypoint implements EncodedExecutionPayload { - constructor( - /** Function calls in the expected format (Noir's convention) */ - public encodedFunctionCalls: EncodedFunctionCall[], - /** The hashed args for the call, ready to be injected in the execution cache */ - public hashedArguments: HashedValues[], - /** Any transient auth witnesses needed for this execution */ - public authWitnesses: AuthWitness[], - /** Data passed through an oracle for this execution. */ - public capsules: Capsule[], - /** The index of the generator to use for hashing */ - public generatorIndex: number, - /** The nonce for the payload, used to emit a nullifier identifying the call */ - public nonce: Fr, - ) {} - - /* eslint-disable camelcase */ - /** - * The function calls to execute. This uses snake_case naming so that it is compatible with Noir encoding - * @internal - */ - get function_calls() { - return this.encodedFunctionCalls; - } - /* eslint-enable camelcase */ - - /** - * Serializes the payload to an array of fields - * @returns The fields of the payload - */ - abstract toFields(): Fr[]; - - /** - * Hashes the payload - * @returns The hash of the payload - */ - hash() { - return poseidon2HashWithSeparator(this.toFields(), this.generatorIndex); - } - - /** Serializes the function calls to an array of fields. */ - protected functionCallsToFields() { - return this.encodedFunctionCalls.flatMap(call => [ - call.args_hash, - call.function_selector, - call.target_address, - new Fr(call.is_public), - new Fr(call.is_static), - ]); - } - - /** - * Creates an execution payload for a dapp from a set of function calls - * @param functionCalls - The function calls to execute - * @returns The execution payload - */ - static async fromFunctionCalls(functionCalls: FunctionCall[]) { - const encoded = await new ExecutionPayload(functionCalls, [], []).encode(); - return new EncodedAppEntrypointPayload( - encoded.encodedFunctionCalls, - encoded.hashedArguments, - [], - [], - 0, - Fr.random(), - ); - } - - /** - * Creates an execution payload for the app-portion of a transaction from a set of function calls - * @param functionCalls - The function calls to execute - * @param nonce - The nonce for the payload, used to emit a nullifier identifying the call - * @returns The execution payload - */ - static async fromAppExecution(functionCalls: FunctionCall[] | Tuple, nonce = Fr.random()) { - if (functionCalls.length > APP_MAX_CALLS) { - throw new Error(`Expected at most ${APP_MAX_CALLS} function calls, got ${functionCalls.length}`); - } - const paddedCalls = padArrayEnd(functionCalls, FunctionCall.empty(), APP_MAX_CALLS); - const encoded = await new ExecutionPayload(paddedCalls, [], []).encode(); - return new EncodedAppEntrypointPayload( - encoded.encodedFunctionCalls, - encoded.hashedArguments, - [], - [], - GeneratorIndex.SIGNATURE_PAYLOAD, - nonce, - ); - } - - /** - * Creates an execution payload to pay the fee for a transaction - * @param sender - The address sending this payload - * @param feeOpts - The fee payment options - * @returns The execution payload - */ - static async fromFeeOptions(sender: AztecAddress, feeOpts?: FeeOptions) { - const { calls, authWitnesses } = (await feeOpts?.paymentMethod.getExecutionPayload(feeOpts?.gasSettings)) ?? { - calls: [], - authWitnesses: [], - }; - const feePayer = await feeOpts?.paymentMethod.getFeePayer(feeOpts?.gasSettings); - const isFeePayer = !!feePayer && feePayer.equals(sender); - const paddedCalls = padArrayEnd(calls, FunctionCall.empty(), FEE_MAX_CALLS); - const encoded = await new ExecutionPayload(paddedCalls, authWitnesses, []).encode(); - return new EncodedFeeEntrypointPayload( - encoded.encodedFunctionCalls, - encoded.hashedArguments, - encoded.authWitnesses, - [], - GeneratorIndex.FEE_PAYLOAD, - Fr.random(), - isFeePayer, - ); - } -} - -/** Entrypoint payload for app phase execution. */ -export class EncodedAppEntrypointPayload extends EncodedExecutionPayloadForEntrypoint { - constructor( - encodedFunctionCalls: EncodedFunctionCall[], - hashedArguments: HashedValues[], - authWitnesses: AuthWitness[], - capsules: Capsule[], - generatorIndex: number, - nonce: Fr, - ) { - super(encodedFunctionCalls, hashedArguments, authWitnesses, capsules, generatorIndex, nonce); - } - - override toFields(): Fr[] { - return [...this.functionCallsToFields(), this.nonce]; - } -} - -/** Entrypoint payload for fee payment to be run during setup phase. */ -export class EncodedFeeEntrypointPayload extends EncodedExecutionPayloadForEntrypoint { - #isFeePayer: boolean; - - constructor( - encodedFunctionCalls: EncodedFunctionCall[], - hashedArguments: HashedValues[], - authWitnesses: AuthWitness[], - capsules: Capsule[], - generatorIndex: number, - nonce: Fr, - isFeePayer: boolean, - ) { - super(encodedFunctionCalls, hashedArguments, authWitnesses, capsules, generatorIndex, nonce); - this.#isFeePayer = isFeePayer; - } - - override toFields(): Fr[] { - return [...this.functionCallsToFields(), this.nonce, new Fr(this.#isFeePayer)]; - } - - /* eslint-disable camelcase */ - /** Whether the sender should be appointed as fee payer. */ - get is_fee_payer() { - return this.#isFeePayer; - } - /* eslint-enable camelcase */ } /** - * Computes a hash of a combined payload. - * @param appPayload - An app payload. - * @param feePayload - A fee payload. - * @returns A hash of a combined payload. + * Merges an array ExecutionPayloads combining their calls, authWitnesses, capsules and extraArgHashes. */ -export async function computeCombinedPayloadHash( - appPayload: EncodedAppEntrypointPayload, - feePayload: EncodedFeeEntrypointPayload, -): Promise { - return poseidon2HashWithSeparator( - [await appPayload.hash(), await feePayload.hash()], - GeneratorIndex.COMBINED_PAYLOAD, - ); +export function mergeExecutionPayloads(requests: ExecutionPayload[]): ExecutionPayload { + const calls = requests.map(r => r.calls).flat(); + const combinedAuthWitnesses = requests.map(r => r.authWitnesses ?? []).flat(); + const combinedCapsules = requests.map(r => r.capsules ?? []).flat(); + const combinedextraHashedArgs = requests.map(r => r.extraHashedArgs ?? []).flat(); + return new ExecutionPayload(calls, combinedAuthWitnesses, combinedCapsules, combinedextraHashedArgs); } diff --git a/yarn-project/entrypoints/src/utils.ts b/yarn-project/entrypoints/src/utils.ts deleted file mode 100644 index 7f019efc32d3..000000000000 --- a/yarn-project/entrypoints/src/utils.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { AuthWitness } from '@aztec/stdlib/auth-witness'; -import { type Capsule, HashedValues } from '@aztec/stdlib/tx'; - -import { type EncodedExecutionPayload, ExecutionPayload } from './payload.js'; - -/** - * Merges an array ExecutionPayloads combining their calls, authWitnesses and capsules - */ -export function mergeExecutionPayloads(requests: ExecutionPayload[]): ExecutionPayload { - const calls = requests.map(r => r.calls).flat(); - const combinedAuthWitnesses = requests.map(r => r.authWitnesses ?? []).flat(); - const combinedCapsules = requests.map(r => r.capsules ?? []).flat(); - const combinedExtraHashedValues = requests.map(r => r.extraHashedValues ?? []).flat(); - return new ExecutionPayload(calls, combinedAuthWitnesses, combinedCapsules, combinedExtraHashedValues); -} - -/** - * Merges an array of mixed ExecutionPayloads and EncodedExecutionPayloads and adds a nonce and cancellable flags, - * in order to create a single EncodedExecutionPayload. - */ -export async function mergeAndEncodeExecutionPayloads( - requests: (ExecutionPayload | EncodedExecutionPayload)[], - { - extraHashedArgs, - extraAuthWitnesses, - extraCapsules, - }: { - /** Extra hashed args to be added to the resulting payload (e.g: app_payload and fee_payload args in entrypoint calls) */ - extraHashedArgs?: HashedValues[]; - /** Extra authwitnesses to be added to the resulting payload */ - extraAuthWitnesses?: AuthWitness[]; - /** Extra capsules to be added to the resulting payload */ - extraCapsules?: Capsule[]; - } = { extraAuthWitnesses: [], extraCapsules: [], extraHashedArgs: [] }, -): Promise { - const isEncoded = (value: ExecutionPayload | EncodedExecutionPayload): value is EncodedExecutionPayload => - 'encodedFunctionCalls' in value; - const encoded = ( - await Promise.all( - requests.map(r => { - if (!isEncoded(r)) { - return new ExecutionPayload(r.calls, r.authWitnesses, r.capsules).encode(); - } else { - return r; - } - }), - ) - ).flat(); - const encodedFunctionCalls = encoded.map(r => r.encodedFunctionCalls).flat(); - const combinedAuthWitnesses = encoded - .map(r => r.authWitnesses ?? []) - .flat() - .concat(extraAuthWitnesses ?? []); - const hashedArguments = encoded - .map(r => r.hashedArguments ?? []) - .flat() - .concat(extraHashedArgs ?? []); - const combinedCapsules = encoded - .map(r => r.capsules ?? []) - .flat() - .concat(extraCapsules ?? []); - return { - encodedFunctionCalls, - authWitnesses: combinedAuthWitnesses, - hashedArguments, - capsules: combinedCapsules, - /* eslint-disable camelcase */ - function_calls: encodedFunctionCalls, - /* eslint-enable camelcase */ - }; -}