From 8b6a0b497b84dc6c1e202a32843547a092abe45a Mon Sep 17 00:00:00 2001 From: Thunkar <5404052+Thunkar@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:29:03 +0000 Subject: [PATCH] feat: wallet routes public view calls directly through node for simulations Optimization suggested by @olehmisar and implemented slightly differently here. Reading public information from wallets is way faster AND parallelizable if we go directly to the node. This PR assembles a fake Tx object with only PublicCalls, that the node will accept and simulate in batches of 32. Co-authored-by: Charlie <5764343+charlielye@users.noreply.github.com> Co-authored-by: Gregorio Juliana Co-authored-by: Nicolas Chamo Co-authored-by: benesjan <13470840+benesjan@users.noreply.github.com> Co-authored-by: mverzilli <651693+mverzilli@users.noreply.github.com> Co-authored-by: mverzilli Co-authored-by: thunkar --- yarn-project/aztec.js/src/api/wallet.ts | 1 - .../aztec.js/src/contract/batch_call.test.ts | 25 +- .../contract/contract_function_interaction.ts | 19 +- .../fee_juice_payment_method_with_claim.ts | 10 +- .../src/fee/private_fee_payment_method.ts | 14 +- .../src/fee/public_fee_payment_method.ts | 16 +- .../aztec.js/src/fee/sponsored_fee_payment.ts | 6 +- .../aztec.js/src/wallet/wallet.test.ts | 15 +- yarn-project/aztec.js/src/wallet/wallet.ts | 20 +- .../end-to-end/src/e2e_fees/failures.test.ts | 14 +- .../entrypoints/src/account_entrypoint.ts | 20 +- .../src/default_multi_call_entrypoint.ts | 20 +- yarn-project/foundation/eslint.config.docs.js | 11 + yarn-project/foundation/eslint.config.js | 2 +- .../contract_function_simulator.ts | 4 +- .../oracle/oracle_version_is_checked.test.ts | 6 +- .../oracle/private_execution.test.ts | 10 +- .../oracle/utility_execution.test.ts | 6 +- yarn-project/pxe/src/debug/pxe_debug_utils.ts | 7 +- yarn-project/pxe/src/pxe.ts | 16 +- .../storage/contract_store/contract_store.ts | 8 +- yarn-project/stdlib/src/abi/function_call.ts | 28 ++- .../test-wallet/src/wallet/test_wallet.ts | 2 +- .../oracle/txe_oracle_top_level_context.ts | 20 +- .../src/base-wallet/base_wallet.test.ts | 185 +++++++++----- .../wallet-sdk/src/base-wallet/base_wallet.ts | 74 +++++- .../wallet-sdk/src/base-wallet/index.ts | 1 + .../wallet-sdk/src/base-wallet/utils.ts | 229 ++++++++++++++++++ 28 files changed, 586 insertions(+), 203 deletions(-) create mode 100644 yarn-project/wallet-sdk/src/base-wallet/utils.ts diff --git a/yarn-project/aztec.js/src/api/wallet.ts b/yarn-project/aztec.js/src/api/wallet.ts index 79448cf21842..ca9fdc0c1d75 100644 --- a/yarn-project/aztec.js/src/api/wallet.ts +++ b/yarn-project/aztec.js/src/api/wallet.ts @@ -17,7 +17,6 @@ export { type ContractClassMetadata, AppCapabilitiesSchema, WalletCapabilitiesSchema, - FunctionCallSchema, ExecutionPayloadSchema, GasSettingsOptionSchema, WalletSimulationFeeOptionSchema, diff --git a/yarn-project/aztec.js/src/contract/batch_call.test.ts b/yarn-project/aztec.js/src/contract/batch_call.test.ts index 1b2fe3b32abb..30efb33cf40e 100644 --- a/yarn-project/aztec.js/src/contract/batch_call.test.ts +++ b/yarn-project/aztec.js/src/contract/batch_call.test.ts @@ -1,5 +1,5 @@ import { Fr } from '@aztec/foundation/curves/bn254'; -import { FunctionSelector, FunctionType } from '@aztec/stdlib/abi'; +import { FunctionCall, FunctionSelector, FunctionType } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { ExecutionPayload, TxSimulationResult, UtilitySimulationResult } from '@aztec/stdlib/tx'; @@ -8,7 +8,6 @@ import { type MockProxy, mock } from 'jest-mock-extended'; import type { Wallet } from '../wallet/wallet.js'; import { BatchCall } from './batch_call.js'; -// eslint-disable-next-line jsdoc/require-jsdoc function createUtilityExecutionPayload( functionName: string, args: Fr[], @@ -16,16 +15,16 @@ function createUtilityExecutionPayload( ): ExecutionPayload { return new ExecutionPayload( [ - { + FunctionCall.from({ name: functionName, to: contractAddress, selector: FunctionSelector.random(), type: FunctionType.UTILITY, - isStatic: true, hideMsgSender: false, + isStatic: true, args, returnTypes: [{ kind: 'field' }], - }, + }), ], [], [], @@ -34,7 +33,6 @@ function createUtilityExecutionPayload( ); } -// eslint-disable-next-line jsdoc/require-jsdoc function createPrivateExecutionPayload( functionName: string, args: Fr[], @@ -43,16 +41,16 @@ function createPrivateExecutionPayload( ): ExecutionPayload { return new ExecutionPayload( [ - { + FunctionCall.from({ name: functionName, to: contractAddress, selector: FunctionSelector.random(), type: FunctionType.PRIVATE, - isStatic: false, hideMsgSender: false, + isStatic: false, args, returnTypes: Array(numReturnValues).fill({ kind: 'field' }), - }, + }), ], [], [], @@ -61,7 +59,6 @@ function createPrivateExecutionPayload( ); } -// eslint-disable-next-line jsdoc/require-jsdoc function createPublicExecutionPayload( functionName: string, args: Fr[], @@ -69,16 +66,16 @@ function createPublicExecutionPayload( ): ExecutionPayload { return new ExecutionPayload( [ - { + FunctionCall.from({ name: functionName, to: contractAddress, selector: FunctionSelector.random(), type: FunctionType.PUBLIC, - isStatic: false, hideMsgSender: false, + isStatic: false, args, returnTypes: [{ kind: 'field' }], - }, + }), ], [], [], @@ -272,7 +269,7 @@ describe('BatchCall', () => { batchCall = new BatchCall(wallet, [payload]); const feePayload = createPrivateExecutionPayload('payFee', [Fr.random()], await AztecAddress.random()); - // eslint-disable-next-line jsdoc/require-jsdoc + const mockPaymentMethod = mock<{ getExecutionPayload: () => Promise }>(); mockPaymentMethod.getExecutionPayload.mockResolvedValue(feePayload); 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 ef27ab7fa14c..5b50b936b5b0 100644 --- a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts +++ b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts @@ -1,4 +1,11 @@ -import { type FunctionAbi, FunctionSelector, FunctionType, decodeFromAbi, encodeArguments } from '@aztec/stdlib/abi'; +import { + type FunctionAbi, + FunctionCall, + FunctionSelector, + FunctionType, + decodeFromAbi, + encodeArguments, +} from '@aztec/stdlib/abi'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { type Capsule, type HashedValues, type TxProfileResult, collectOffchainEffects } from '@aztec/stdlib/tx'; @@ -43,16 +50,16 @@ export class ContractFunctionInteraction extends BaseContractInteraction { */ public async getFunctionCall() { const args = encodeArguments(this.functionDao, this.args); - return { + return FunctionCall.from({ name: this.functionDao.name, - args, + to: this.contractAddress, selector: await FunctionSelector.fromNameAndParameters(this.functionDao.name, this.functionDao.parameters), type: this.functionDao.functionType, - to: this.contractAddress, - isStatic: this.functionDao.isStatic, hideMsgSender: false /** Only set to `true` for enqueued public function calls */, + isStatic: this.functionDao.isStatic, + args, returnTypes: this.functionDao.returnTypes, - }; + }); } /** diff --git a/yarn-project/aztec.js/src/fee/fee_juice_payment_method_with_claim.ts b/yarn-project/aztec.js/src/fee/fee_juice_payment_method_with_claim.ts index bb9e4e20f73b..78d495dfe1e3 100644 --- a/yarn-project/aztec.js/src/fee/fee_juice_payment_method_with_claim.ts +++ b/yarn-project/aztec.js/src/fee/fee_juice_payment_method_with_claim.ts @@ -1,6 +1,6 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { ProtocolContractAddress } from '@aztec/protocol-contracts'; -import { FunctionSelector, FunctionType } from '@aztec/stdlib/abi'; +import { FunctionCall, FunctionSelector, FunctionType } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { GasSettings } from '@aztec/stdlib/gas'; import { ExecutionPayload } from '@aztec/stdlib/tx'; @@ -27,10 +27,11 @@ export class FeeJuicePaymentMethodWithClaim implements FeePaymentMethod { return new ExecutionPayload( [ - { - to: ProtocolContractAddress.FeeJuice, + FunctionCall.from({ name: 'claim_and_end_setup', + to: ProtocolContractAddress.FeeJuice, selector, + type: FunctionType.PRIVATE, hideMsgSender: false, isStatic: false, args: [ @@ -40,8 +41,7 @@ export class FeeJuicePaymentMethodWithClaim implements FeePaymentMethod { new Fr(this.claim.messageLeafIndex), ], returnTypes: [], - type: FunctionType.PRIVATE, - }, + }), ], [], [], 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 241533fa5dd0..891c52f79a64 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 @@ -1,5 +1,5 @@ import { Fr } from '@aztec/foundation/curves/bn254'; -import { type FunctionAbi, FunctionSelector, FunctionType, decodeFromAbi } from '@aztec/stdlib/abi'; +import { type FunctionAbi, FunctionCall, FunctionSelector, FunctionType, decodeFromAbi } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { GasSettings } from '@aztec/stdlib/gas'; import { ExecutionPayload } from '@aztec/stdlib/tx'; @@ -102,21 +102,21 @@ export class PrivateFeePaymentMethod implements FeePaymentMethod { const witness = await this.wallet.createAuthWit(this.sender, { caller: this.paymentContract, - call: { + call: FunctionCall.from({ name: 'transfer_to_public', - args: [this.sender.toField(), this.paymentContract.toField(), maxFee, txNonce], + to: await this.getAsset(), selector: await FunctionSelector.fromSignature('transfer_to_public((Field),(Field),u128,Field)'), type: FunctionType.PRIVATE, hideMsgSender: false, isStatic: false, - to: await this.getAsset(), + args: [this.sender.toField(), this.paymentContract.toField(), maxFee, txNonce], returnTypes: [], - }, + }), }); return new ExecutionPayload( [ - { + FunctionCall.from({ name: 'fee_entrypoint_private', to: this.paymentContract, selector: await FunctionSelector.fromSignature('fee_entrypoint_private(u128,Field)'), @@ -125,7 +125,7 @@ export class PrivateFeePaymentMethod implements FeePaymentMethod { isStatic: false, args: [maxFee, txNonce], returnTypes: [], - }, + }), ], [witness], [], 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 ae405491a49b..2847a40f1dea 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 @@ -1,5 +1,5 @@ import { Fr } from '@aztec/foundation/curves/bn254'; -import { type FunctionAbi, FunctionSelector, FunctionType, decodeFromAbi } from '@aztec/stdlib/abi'; +import { type FunctionAbi, FunctionCall, FunctionSelector, FunctionType, decodeFromAbi } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { GasSettings } from '@aztec/stdlib/gas'; import { ExecutionPayload } from '@aztec/stdlib/tx'; @@ -94,16 +94,16 @@ export class PublicFeePaymentMethod implements FeePaymentMethod { const intent = { caller: this.paymentContract, - call: { + call: FunctionCall.from({ name: 'transfer_in_public', - args: [this.sender.toField(), this.paymentContract.toField(), maxFee, txNonce], + to: await this.getAsset(), selector: await FunctionSelector.fromSignature('transfer_in_public((Field),(Field),u128,Field)'), type: FunctionType.PUBLIC, - isStatic: false, hideMsgSender: false /** The target function performs an authwit check, so msg_sender is needed */, - to: await this.getAsset(), + isStatic: false, + args: [this.sender.toField(), this.paymentContract.toField(), maxFee, txNonce], returnTypes: [], - }, + }), }; const setPublicAuthWitInteraction = await SetPublicAuthwitContractInteraction.create( @@ -116,7 +116,7 @@ export class PublicFeePaymentMethod implements FeePaymentMethod { return new ExecutionPayload( [ ...(await setPublicAuthWitInteraction.request()).calls, - { + FunctionCall.from({ name: 'fee_entrypoint_public', to: this.paymentContract, selector: await FunctionSelector.fromSignature('fee_entrypoint_public(u128,Field)'), @@ -125,7 +125,7 @@ export class PublicFeePaymentMethod implements FeePaymentMethod { isStatic: false, args: [maxFee, txNonce], returnTypes: [], - }, + }), ], [], [], diff --git a/yarn-project/aztec.js/src/fee/sponsored_fee_payment.ts b/yarn-project/aztec.js/src/fee/sponsored_fee_payment.ts index d272ba0b709d..64464636bb9a 100644 --- a/yarn-project/aztec.js/src/fee/sponsored_fee_payment.ts +++ b/yarn-project/aztec.js/src/fee/sponsored_fee_payment.ts @@ -1,5 +1,5 @@ import type { FeePaymentMethod } from '@aztec/aztec.js/fee'; -import { FunctionSelector, FunctionType } from '@aztec/stdlib/abi'; +import { FunctionCall, FunctionSelector, FunctionType } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { GasSettings } from '@aztec/stdlib/gas'; import { ExecutionPayload } from '@aztec/stdlib/tx'; @@ -22,7 +22,7 @@ export class SponsoredFeePaymentMethod implements FeePaymentMethod { async getExecutionPayload(): Promise { return new ExecutionPayload( [ - { + FunctionCall.from({ name: 'sponsor_unconditionally', to: this.paymentContract, selector: await FunctionSelector.fromSignature('sponsor_unconditionally()'), @@ -31,7 +31,7 @@ export class SponsoredFeePaymentMethod implements FeePaymentMethod { isStatic: false, args: [], returnTypes: [], - }, + }), ], [], [], diff --git a/yarn-project/aztec.js/src/wallet/wallet.test.ts b/yarn-project/aztec.js/src/wallet/wallet.test.ts index 91299597ea31..578f04066697 100644 --- a/yarn-project/aztec.js/src/wallet/wallet.test.ts +++ b/yarn-project/aztec.js/src/wallet/wallet.test.ts @@ -3,7 +3,7 @@ import { BlockNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { type JsonRpcTestContext, createJsonRpcTestSetup } from '@aztec/foundation/json-rpc/test'; import type { ContractArtifact, EventMetadataDefinition } from '@aztec/stdlib/abi'; -import { EventSelector, FunctionSelector, FunctionType } from '@aztec/stdlib/abi'; +import { EventSelector, FunctionCall, FunctionSelector, FunctionType } from '@aztec/stdlib/abi'; import { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { BlockHash } from '@aztec/stdlib/block'; @@ -164,16 +164,16 @@ describe('WalletSchema', () => { }); it('simulateUtility', async () => { - const call = { + const call = FunctionCall.from({ name: 'testFunction', to: await AztecAddress.random(), selector: FunctionSelector.fromField(new Fr(1)), type: FunctionType.UTILITY, - isStatic: false, hideMsgSender: false, + isStatic: false, args: [Fr.random()], returnTypes: [], - }; + }); const result = await context.client.simulateUtility(call, [AuthWitness.random()]); expect(result).toBeInstanceOf(UtilitySimulationResult); }); @@ -272,16 +272,16 @@ describe('WalletSchema', () => { profileMode: 'gates', }; - const call = { + const call = FunctionCall.from({ name: 'testFunction', to: address3, selector: FunctionSelector.fromField(new Fr(1)), type: FunctionType.UTILITY, - isStatic: false, hideMsgSender: false, + isStatic: false, args: [Fr.random()], returnTypes: [], - }; + }); const mockInstance: ContractInstanceWithAddress = { address: address2, @@ -355,7 +355,6 @@ describe('WalletSchema', () => { }); }); -// eslint-disable-next-line jsdoc/require-jsdoc class MockWallet implements Wallet { getChainInfo(): Promise { return Promise.resolve({ diff --git a/yarn-project/aztec.js/src/wallet/wallet.ts b/yarn-project/aztec.js/src/wallet/wallet.ts index a0f4f69961b0..a85cf012a726 100644 --- a/yarn-project/aztec.js/src/wallet/wallet.ts +++ b/yarn-project/aztec.js/src/wallet/wallet.ts @@ -7,8 +7,7 @@ import { type ContractArtifact, ContractArtifactSchema, type EventMetadataDefinition, - type FunctionCall, - FunctionType, + FunctionCall, } from '@aztec/stdlib/abi'; import { AuthWitness } from '@aztec/stdlib/auth-witness'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; @@ -257,19 +256,8 @@ export type Wallet = { batch(methods: T): Promise>; }; -export const FunctionCallSchema = z.object({ - name: z.string(), - to: schemas.AztecAddress, - selector: schemas.FunctionSelector, - type: z.nativeEnum(FunctionType), - isStatic: z.boolean(), - hideMsgSender: z.boolean(), - args: z.array(schemas.Fr), - returnTypes: z.array(AbiTypeSchema), -}); - export const ExecutionPayloadSchema = z.object({ - calls: z.array(FunctionCallSchema), + calls: z.array(FunctionCall.schema), authWitnesses: z.array(AuthWitness.schema), capsules: z.array(Capsule.schema), extraHashedArgs: z.array(HashedValues.schema), @@ -326,7 +314,7 @@ export const MessageHashOrIntentSchema = z.union([ z.object({ consumer: schemas.AztecAddress, innerHash: schemas.Fr }), z.object({ caller: schemas.AztecAddress, - call: FunctionCallSchema, + call: FunctionCall.schema, }), ]); @@ -522,7 +510,7 @@ const WalletMethodSchemas = { simulateTx: z.function().args(ExecutionPayloadSchema, SimulateOptionsSchema).returns(TxSimulationResult.schema), simulateUtility: z .function() - .args(FunctionCallSchema, optional(z.array(AuthWitness.schema))) + .args(FunctionCall.schema, optional(z.array(AuthWitness.schema))) .returns(UtilitySimulationResult.schema), profileTx: z.function().args(ExecutionPayloadSchema, ProfileOptionsSchema).returns(TxProfileResult.schema), sendTx: z diff --git a/yarn-project/end-to-end/src/e2e_fees/failures.test.ts b/yarn-project/end-to-end/src/e2e_fees/failures.test.ts index 7b58234b9ba7..42394548cda7 100644 --- a/yarn-project/end-to-end/src/e2e_fees/failures.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/failures.test.ts @@ -8,7 +8,7 @@ import { TxExecutionResult } from '@aztec/aztec.js/tx'; import type { Wallet } from '@aztec/aztec.js/wallet'; import type { FPCContract } from '@aztec/noir-contracts.js/FPC'; import type { TokenContract as BananaCoin } from '@aztec/noir-contracts.js/Token'; -import { FunctionType } from '@aztec/stdlib/abi'; +import { FunctionCall, FunctionType } from '@aztec/stdlib/abi'; import { Gas, GasSettings } from '@aztec/stdlib/gas'; import { ExecutionPayload } from '@aztec/stdlib/tx'; @@ -334,16 +334,16 @@ class BuggedSetupFeePaymentMethod extends PublicFeePaymentMethod { this.sender, { caller: this.paymentContract, - call: { + call: FunctionCall.from({ name: 'transfer_in_public', - args: [this.sender.toField(), this.paymentContract.toField(), maxFee, authwitNonce], + to: asset, selector: await FunctionSelector.fromSignature('transfer_in_public((Field),(Field),u128,Field)'), type: FunctionType.PUBLIC, hideMsgSender: false /** the target function performs an authwit, so msg_sender is needed */, isStatic: false, - to: asset, + args: [this.sender.toField(), this.paymentContract.toField(), maxFee, authwitNonce], returnTypes: [], - }, + }), }, true, ); @@ -351,7 +351,7 @@ class BuggedSetupFeePaymentMethod extends PublicFeePaymentMethod { return new ExecutionPayload( [ ...(await setPublicAuthWitInteraction.request()).calls, - { + FunctionCall.from({ name: 'fee_entrypoint_public', to: this.paymentContract, selector: await FunctionSelector.fromSignature('fee_entrypoint_public(u128,Field)'), @@ -360,7 +360,7 @@ class BuggedSetupFeePaymentMethod extends PublicFeePaymentMethod { isStatic: false, args: [tooMuchFee, authwitNonce], returnTypes: [], - }, + }), ], [], [], diff --git a/yarn-project/entrypoints/src/account_entrypoint.ts b/yarn-project/entrypoints/src/account_entrypoint.ts index 0ad631dd0707..114910fa342a 100644 --- a/yarn-project/entrypoints/src/account_entrypoint.ts +++ b/yarn-project/entrypoints/src/account_entrypoint.ts @@ -90,16 +90,16 @@ export class DefaultAccountEntrypoint implements EntrypointInterface { const callData = await this.#buildEntrypointCallData(exec, options); // Build the entrypoint function call - const entrypointCall = new FunctionCall( - callData.abi.name, - this.address, - callData.functionSelector, - callData.abi.functionType, - false, - callData.abi.isStatic, - callData.encodedArgs, - callData.abi.returnTypes, - ); + const entrypointCall = FunctionCall.from({ + name: callData.abi.name, + to: this.address, + selector: callData.functionSelector, + type: callData.abi.functionType, + hideMsgSender: false, + isStatic: callData.abi.isStatic, + args: callData.encodedArgs, + returnTypes: callData.abi.returnTypes, + }); return new ExecutionPayload( [entrypointCall], diff --git a/yarn-project/entrypoints/src/default_multi_call_entrypoint.ts b/yarn-project/entrypoints/src/default_multi_call_entrypoint.ts index 4897117f6616..60c905a415b0 100644 --- a/yarn-project/entrypoints/src/default_multi_call_entrypoint.ts +++ b/yarn-project/entrypoints/src/default_multi_call_entrypoint.ts @@ -40,16 +40,16 @@ export class DefaultMultiCallEntrypoint implements EntrypointInterface { async wrapExecutionPayload(exec: ExecutionPayload, _options?: any): Promise { const { authWitnesses, capsules, extraHashedArgs } = exec; const callData = await this.#buildEntrypointCallData(exec); - const entrypointCall = new FunctionCall( - callData.abi.name, - this.address, - callData.functionSelector, - callData.abi.functionType, - false, - callData.abi.isStatic, - callData.encodedArgs, - callData.abi.returnTypes, - ); + const entrypointCall = FunctionCall.from({ + name: callData.abi.name, + to: this.address, + selector: callData.functionSelector, + type: callData.abi.functionType, + hideMsgSender: false, + isStatic: callData.abi.isStatic, + args: callData.encodedArgs, + returnTypes: callData.abi.returnTypes, + }); return new ExecutionPayload( [entrypointCall], diff --git a/yarn-project/foundation/eslint.config.docs.js b/yarn-project/foundation/eslint.config.docs.js index 7f8261752c32..c440482d21bd 100644 --- a/yarn-project/foundation/eslint.config.docs.js +++ b/yarn-project/foundation/eslint.config.docs.js @@ -63,4 +63,15 @@ export default [ 'jsdoc/require-returns': 'off', }, }, + { + files: ['**/*.test.ts'], + rules: { + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-description': 'off', + 'jsdoc/require-param': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-param-name': 'off', + 'tsdoc/syntax': 'off', + }, + }, ]; diff --git a/yarn-project/foundation/eslint.config.js b/yarn-project/foundation/eslint.config.js index 6cd96887b931..a2e031075604 100644 --- a/yarn-project/foundation/eslint.config.js +++ b/yarn-project/foundation/eslint.config.js @@ -119,7 +119,7 @@ export default [ }, }), { - files: ['*.test.ts'], + files: ['**/*.test.ts'], rules: { 'jsdoc/require-jsdoc': 'off', }, diff --git a/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts b/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts index 506502fc4ee3..08a7877ceed3 100644 --- a/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts +++ b/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts @@ -359,7 +359,7 @@ class OrderedSideEffect { */ export async function generateSimulatedProvingResult( privateExecutionResult: PrivateExecutionResult, - contractStore: ContractStore, + debugFunctionNameGetter: (contractAddress: AztecAddress, functionSelector: FunctionSelector) => Promise, minRevertibleSideEffectCounterOverride?: number, ): Promise> { const siloedNoteHashes: OrderedSideEffect[] = []; @@ -440,7 +440,7 @@ export async function generateSimulatedProvingResult( : execution.publicInputs.publicTeardownCallRequest; executionSteps.push({ - functionName: await contractStore.getDebugFunctionName( + functionName: await debugFunctionNameGetter( execution.publicInputs.callContext.contractAddress, execution.publicInputs.callContext.functionSelector, ), diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts index ae2845d81fb0..07fdb05cc591 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts @@ -165,16 +165,16 @@ describe('Oracle Version Check test suite', () => { contractStore.getFunctionArtifact.mockResolvedValue(utilityFunctionArtifact); // Form the execution request for the utility function - const execRequest: FunctionCall = { + const execRequest = FunctionCall.from({ name: utilityFunctionArtifact.name, to: contractAddress, selector: FunctionSelector.empty(), type: FunctionType.UTILITY, - isStatic: false, hideMsgSender: false, + isStatic: false, args: encodeArguments(utilityFunctionArtifact, []), returnTypes: utilityFunctionArtifact.returnTypes, - }; + }); // Call the utility function await acirSimulator.runUtility(execRequest, [], anchorBlockHeader, [], 'test'); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts index eb8c261c1206..9ed5fcd4a4e5 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts @@ -27,7 +27,7 @@ import { TestContractArtifact } from '@aztec/noir-test-contracts.js/Test'; import { WASMSimulator } from '@aztec/simulator/client'; import { type ContractArtifact, - type FunctionCall, + FunctionCall, FunctionSelector, encodeArguments, getFunctionArtifact, @@ -464,16 +464,16 @@ describe('Private Execution test suite', () => { throw new Error(`Contract not found: ${to}`); } const functionArtifact = getFunctionArtifactByName(contract, functionName); - return { + return FunctionCall.from({ name: functionArtifact.name, - args: encodeArguments(functionArtifact, args), + to, selector: await FunctionSelector.fromNameAndParameters(functionArtifact.name, functionArtifact.parameters), type: functionArtifact.functionType, - to, hideMsgSender: false, isStatic: functionArtifact.isStatic, + args: encodeArguments(functionArtifact, args), returnTypes: functionArtifact.returnTypes, - }; + }); }); capsuleStore.loadCapsule.mockImplementation((_, __) => Promise.resolve(null)); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts index 08fc33d0ffd9..2cbc72824c2e 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts @@ -178,16 +178,16 @@ describe('Utility Execution test suite', () => { capsuleStore.loadCapsule.mockImplementation((_, __) => Promise.resolve(null)); - const execRequest: FunctionCall = { + const execRequest = FunctionCall.from({ name: artifact.name, to: contractAddress, selector: FunctionSelector.empty(), type: FunctionType.UTILITY, - isStatic: false, hideMsgSender: false, + isStatic: false, args: encodeArguments(artifact, [owner]), returnTypes: artifact.returnTypes, - }; + }); const result = await acirSimulator.runUtility(execRequest, [], anchorBlockHeader, [], 'test-job-id'); diff --git a/yarn-project/pxe/src/debug/pxe_debug_utils.ts b/yarn-project/pxe/src/debug/pxe_debug_utils.ts index 153bb7f6ae56..f99739367d56 100644 --- a/yarn-project/pxe/src/debug/pxe_debug_utils.ts +++ b/yarn-project/pxe/src/debug/pxe_debug_utils.ts @@ -2,7 +2,7 @@ import type { FunctionCall } from '@aztec/stdlib/abi'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { NoteDao, NotesFilter } from '@aztec/stdlib/note'; -import type { BlockHeader, ContractOverrides } from '@aztec/stdlib/tx'; +import type { ContractOverrides } from '@aztec/stdlib/tx'; import type { BlockSynchronizer } from '../block_synchronizer/block_synchronizer.js'; import type { ContractFunctionSimulator } from '../contract_function_simulator/contract_function_simulator.js'; @@ -81,11 +81,6 @@ export class PXEDebugUtils { }); } - /** Returns the block header up to which the PXE has synced. */ - public getSyncedBlockHeader(): Promise { - return this.anchorBlockStore.getBlockHeader(); - } - /** * Triggers a sync of the PXE with the node. * Blocks until the sync is complete. diff --git a/yarn-project/pxe/src/pxe.ts b/yarn-project/pxe/src/pxe.ts index d9992d77201e..3412ca96644f 100644 --- a/yarn-project/pxe/src/pxe.ts +++ b/yarn-project/pxe/src/pxe.ts @@ -33,6 +33,7 @@ import type { PrivateKernelTailCircuitPublicInputs, } from '@aztec/stdlib/kernel'; import { + BlockHeader, type ContractOverrides, type InTx, PrivateExecutionResult, @@ -430,6 +431,19 @@ export class PXE { // Public API + /** + * Returns the block header up to which the PXE has synced. + * @returns The synced block header + */ + public getSyncedBlockHeader(): Promise { + return this.anchorBlockStore.getBlockHeader(); + } + + /** + * Returns the contract instance for a given address, if it's registered in the PXE. + * @param address - The contract address. + * @returns The contract instance if found, undefined otherwise. + */ public getContractInstance(address: AztecAddress): Promise { return this.contractStore.getContractInstance(address); } @@ -881,7 +895,7 @@ export class PXE { if (skipKernels) { ({ publicInputs, executionSteps } = await generateSimulatedProvingResult( privateExecutionResult, - this.contractStore, + (addr, sel) => this.contractStore.getDebugFunctionName(addr, sel), )); } else { // Kernel logic, plus proving of all private functions and kernels. diff --git a/yarn-project/pxe/src/storage/contract_store/contract_store.ts b/yarn-project/pxe/src/storage/contract_store/contract_store.ts index 3d46bd08266e..6b2453850499 100644 --- a/yarn-project/pxe/src/storage/contract_store/contract_store.ts +++ b/yarn-project/pxe/src/storage/contract_store/contract_store.ts @@ -316,15 +316,15 @@ export class ContractStore { throw new Error(`Unknown function ${functionName} in contract ${contract.name}.`); } - return { + return FunctionCall.from({ name: functionDao.name, - args: encodeArguments(functionDao, args), + to, selector: await FunctionSelector.fromNameAndParameters(functionDao.name, functionDao.parameters), type: functionDao.functionType, - to, hideMsgSender: false, isStatic: functionDao.isStatic, + args: encodeArguments(functionDao, args), returnTypes: functionDao.returnTypes, - }; + }); } } diff --git a/yarn-project/stdlib/src/abi/function_call.ts b/yarn-project/stdlib/src/abi/function_call.ts index db898c44cfab..158ef0502d99 100644 --- a/yarn-project/stdlib/src/abi/function_call.ts +++ b/yarn-project/stdlib/src/abi/function_call.ts @@ -1,8 +1,11 @@ import type { Fr } from '@aztec/foundation/curves/bn254'; import type { FieldsOf } from '@aztec/foundation/types'; +import { z } from 'zod'; + import { AztecAddress } from '../aztec-address/index.js'; -import { type AbiType, FunctionType } from './abi.js'; +import { schemas } from '../schemas/index.js'; +import { type AbiType, AbiTypeSchema, FunctionType } from './abi.js'; import { FunctionSelector } from './function_selector.js'; /** A request to call a function on a contract. */ @@ -43,12 +46,31 @@ export class FunctionCall { return new FunctionCall(...FunctionCall.getFields(fields)); } + static get schema() { + return z + .object({ + name: z.string(), + to: schemas.AztecAddress, + selector: schemas.FunctionSelector, + type: z.nativeEnum(FunctionType), + isStatic: z.boolean(), + hideMsgSender: z.boolean(), + args: z.array(schemas.Fr), + returnTypes: z.array(AbiTypeSchema), + }) + .transform(FunctionCall.from); + } + + public isPublicStatic(): boolean { + return this.type === FunctionType.PUBLIC && this.isStatic; + } + /** * Creates an empty function call. * @returns an empty function call. */ public static empty() { - return { + return FunctionCall.from({ name: '', to: AztecAddress.ZERO, selector: FunctionSelector.empty(), @@ -57,6 +79,6 @@ export class FunctionCall { isStatic: false, args: [], returnTypes: [], - }; + }); } } diff --git a/yarn-project/test-wallet/src/wallet/test_wallet.ts b/yarn-project/test-wallet/src/wallet/test_wallet.ts index 27a5aa7ed4b8..b320c4ee63ef 100644 --- a/yarn-project/test-wallet/src/wallet/test_wallet.ts +++ b/yarn-project/test-wallet/src/wallet/test_wallet.ts @@ -284,7 +284,7 @@ export abstract class BaseTestWallet extends BaseWallet { /** Returns the block header up to which the wallet has synced. */ getSyncedBlockHeader(): Promise { - return this.pxe.debug.getSyncedBlockHeader(); + return this.pxe.getSyncedBlockHeader(); } /** 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 dd4dc504916b..aaf5f7303b81 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 @@ -411,7 +411,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl // We pass the non-zero minRevertibleSideEffectCounter to make sure the side effects are split correctly. const { publicInputs } = await generateSimulatedProvingResult( result, - this.contractStore, + (addr, sel) => this.contractStore.getDebugFunctionName(addr, sel), minRevertibleSideEffectCounter, ); @@ -684,16 +684,16 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl this.jobId, ); - const call = new FunctionCall( - artifact.name, - targetContractAddress, - functionSelector, - FunctionType.UTILITY, - false, - false, + const call = FunctionCall.from({ + name: artifact.name, + to: targetContractAddress, + selector: functionSelector, + type: FunctionType.UTILITY, + hideMsgSender: false, + isStatic: false, args, - [], - ); + returnTypes: [], + }); return this.executeUtilityCall(call); } diff --git a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.test.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.test.ts index b1563a2e0cd9..ac3983ce72ae 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.test.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.test.ts @@ -1,109 +1,178 @@ import type { Account } from '@aztec/aztec.js/account'; import type { AztecNode } from '@aztec/aztec.js/node'; -import type { Aliased, PrivateEvent } from '@aztec/aztec.js/wallet'; +import type { Aliased } from '@aztec/aztec.js/wallet'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { TokenContract, type Transfer } from '@aztec/noir-contracts.js/Token'; import { PXE, type PackedPrivateEvent } from '@aztec/pxe/server'; +import { FunctionCall, FunctionSelector, FunctionType } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { BlockHash } from '@aztec/stdlib/block'; -import { TxHash } from '@aztec/stdlib/tx'; +import type { NodeInfo } from '@aztec/stdlib/contract'; +import { Gas, GasFees } from '@aztec/stdlib/gas'; +import { PrivateKernelTailCircuitPublicInputs } from '@aztec/stdlib/kernel'; +import { + BlockHeader, + ExecutionPayload, + GlobalVariables, + NestedProcessReturnValues, + PrivateExecutionResult, + TxEffect, + TxHash, + TxSimulationResult, +} from '@aztec/stdlib/tx'; import { type MockProxy, mock } from 'jest-mock-extended'; import { BaseWallet } from './base_wallet.js'; -/** - * Just a construct to test BaseWallet - */ class BasicWallet extends BaseWallet { + mockAccount = mock(); + constructor(pxe: PXE, node: AztecNode) { super(pxe, node); } protected override getAccountFromAddress(_address: AztecAddress): Promise { - throw new Error('Method not implemented.'); + return Promise.resolve(this.mockAccount); } + override getAccounts(): Promise[]> { throw new Error('Method not implemented.'); } } +async function makeFunctionCall(type: FunctionType, isStatic: boolean, name: string): Promise { + return FunctionCall.from({ + name, + to: await AztecAddress.random(), + selector: FunctionSelector.random(), + type, + hideMsgSender: false, + isStatic, + args: [Fr.random()], + returnTypes: [{ kind: 'field' as const }], + }); +} + describe('BaseWallet', () => { let pxe: MockProxy; let node: MockProxy; - // eslint-disable-next-line jsdoc/require-jsdoc - async function makeTransferEvent(amount: number): Promise { - return { - from: await AztecAddress.random(), - to: await AztecAddress.random(), - amount: BigInt(amount), + it('splits a mixed payload into optimized and entrypoint paths and merges results', async () => { + pxe = mock(); + node = mock(); + const wallet = new BasicWallet(pxe, node); + const from = await AztecAddress.random(); + + // Mixed payload: 2 leading public static calls + 1 private call + const balanceOf = await makeFunctionCall(FunctionType.PUBLIC, true, 'balanceOf'); + const totalSupply = await makeFunctionCall(FunctionType.PUBLIC, true, 'totalSupply'); + const transfer = await makeFunctionCall(FunctionType.PRIVATE, false, 'transfer'); + const payload = new ExecutionPayload([balanceOf, totalSupply, transfer], [], []); + + const optimizedRv0 = new NestedProcessReturnValues([new Fr(100)]); + const optimizedRv1 = new NestedProcessReturnValues([new Fr(200)]); + const normalRv0 = new NestedProcessReturnValues([new Fr(300)]); + + node.getCurrentMinFees.mockResolvedValue(new GasFees(2, 2)); + node.getNodeInfo.mockResolvedValue({ ...mock(), l1ChainId: 1, rollupVersion: 1 }); + pxe.getSyncedBlockHeader.mockResolvedValue(BlockHeader.empty()); + + wallet.mockAccount.createTxExecutionRequest.mockResolvedValue(mock()); + + // Mock node.simulatePublicCalls — called by simulateViaNode for the 2 optimized calls + const optimizedPublicOutput = { + revertReason: undefined, + globalVariables: GlobalVariables.empty(), + txEffect: TxEffect.empty(), + publicReturnValues: [optimizedRv0, optimizedRv1], + gasUsed: { totalGas: Gas.empty(), teardownGas: Gas.empty(), publicGas: Gas.empty(), billedGas: Gas.empty() }, }; - } - - // eslint-disable-next-line jsdoc/require-jsdoc - function encodeTransfer(transfer: Transfer): Fr[] { - return [ - (transfer.from as AztecAddress).toField(), - (transfer.to as AztecAddress).toField(), - new Fr(transfer.amount), - ]; - } - - // eslint-disable-next-line jsdoc/require-jsdoc - function privateEventFor(serial: Fr[]): PackedPrivateEvent { - return { - packedEvent: serial, - l2BlockHash: BlockHash.random(), - l2BlockNumber: BlockNumber(42), - txHash: TxHash.random(), - eventSelector: TokenContract.events.Transfer.eventSelector, + node.simulatePublicCalls.mockResolvedValue(optimizedPublicOutput); + + // Mock pxe.simulateTx — called by simulateViaEntrypoint for the private call + const normalPublicOutput = { + revertReason: undefined, + globalVariables: GlobalVariables.empty(), + txEffect: TxEffect.empty(), + publicReturnValues: [normalRv0], + gasUsed: { totalGas: Gas.empty(), teardownGas: Gas.empty(), publicGas: Gas.empty(), billedGas: Gas.empty() }, }; - } + const normalResult = new TxSimulationResult( + mock(), + mock(), + normalPublicOutput, + undefined, + ); + pxe.simulateTx.mockResolvedValue(normalResult); + + const result = await wallet.simulateTx(payload, { from }); + + // Both paths should have been called + expect(node.simulatePublicCalls).toHaveBeenCalled(); + expect(pxe.simulateTx).toHaveBeenCalled(); + + // Return values should be merged in order: optimized first, then normal + const rv = result.publicOutput!.publicReturnValues; + expect(rv).toHaveLength(3); + expect(rv[0]).toBe(optimizedRv0); + expect(rv[1]).toBe(optimizedRv1); + expect(rv[2]).toBe(normalRv0); + }); it('decodes private events', async () => { pxe = mock(); node = mock(); - const transfer1: Transfer = await makeTransferEvent(120); - const transfer2: Transfer = await makeTransferEvent(235); + async function makeTransferEvent(amount: number): Promise { + return { + from: await AztecAddress.random(), + to: await AztecAddress.random(), + amount: BigInt(amount), + }; + } - const transfer1Serialized: Fr[] = encodeTransfer(transfer1); - const transfer2Serialized: Fr[] = encodeTransfer(transfer2); + function encodeTransfer(t: Transfer): Fr[] { + return [(t.from as AztecAddress).toField(), (t.to as AztecAddress).toField(), new Fr(t.amount)]; + } - const packedPrivateEventTransfer1: PackedPrivateEvent = privateEventFor(transfer1Serialized); - const packedPrivateEventTransfer2: PackedPrivateEvent = privateEventFor(transfer2Serialized); + function privateEventFor(serial: Fr[]): PackedPrivateEvent { + return { + packedEvent: serial, + l2BlockHash: BlockHash.random(), + l2BlockNumber: BlockNumber(42), + txHash: TxHash.random(), + eventSelector: TokenContract.events.Transfer.eventSelector, + }; + } - const privateEventTransfer1: PrivateEvent = { - event: transfer1, - metadata: { - l2BlockNumber: packedPrivateEventTransfer1.l2BlockNumber, - l2BlockHash: packedPrivateEventTransfer1.l2BlockHash, - txHash: packedPrivateEventTransfer1.txHash, - }, - }; + const transfer1 = await makeTransferEvent(120); + const transfer2 = await makeTransferEvent(235); - const privateEventTransfer2: PrivateEvent = { - event: transfer2, - metadata: { - l2BlockNumber: packedPrivateEventTransfer2.l2BlockNumber, - l2BlockHash: packedPrivateEventTransfer2.l2BlockHash, - txHash: packedPrivateEventTransfer2.txHash, - }, - }; + const packed1 = privateEventFor(encodeTransfer(transfer1)); + const packed2 = privateEventFor(encodeTransfer(transfer2)); - pxe.getPrivateEvents.mockResolvedValue([packedPrivateEventTransfer1, packedPrivateEventTransfer2]); + pxe.getPrivateEvents.mockResolvedValue([packed1, packed2]); - const basicWallet = new BasicWallet(pxe, node); + const wallet = new BasicWallet(pxe, node); - const events = await basicWallet.getPrivateEvents(TokenContract.events.Transfer, { + const events = await wallet.getPrivateEvents(TokenContract.events.Transfer, { contractAddress: await AztecAddress.random(), fromBlock: BlockNumber(42), toBlock: BlockNumber(43), scopes: [await AztecAddress.random()], }); - expect(events).toEqual([privateEventTransfer1, privateEventTransfer2]); + expect(events).toEqual([ + { + event: transfer1, + metadata: { l2BlockNumber: packed1.l2BlockNumber, l2BlockHash: packed1.l2BlockHash, txHash: packed1.txHash }, + }, + { + event: transfer2, + metadata: { l2BlockNumber: packed2.l2BlockNumber, l2BlockHash: packed2.l2BlockHash, txHash: packed2.txHash }, + }, + ]); }); }); diff --git a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts index 3e847ab42cb4..7d80c3d31f7f 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts @@ -45,16 +45,18 @@ import { SimulationError } from '@aztec/stdlib/errors'; import { Gas, GasSettings } from '@aztec/stdlib/gas'; import { siloNullifier } from '@aztec/stdlib/hash'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; -import type { - TxExecutionRequest, - TxProfileResult, +import { + type TxExecutionRequest, + type TxProfileResult, TxSimulationResult, - UtilitySimulationResult, + type UtilitySimulationResult, } from '@aztec/stdlib/tx'; import { ExecutionPayload, mergeExecutionPayloads } from '@aztec/stdlib/tx'; import { inspect } from 'util'; +import { buildMergedSimulationResult, extractOptimizablePublicStaticCalls, simulateViaNode } from './utils.js'; + /** * Options to configure fee payment for a transaction */ @@ -282,17 +284,67 @@ export abstract class BaseWallet implements Wallet { return instance; } + /** + * Simulates calls through the standard PXE path (account entrypoint). + * @param executionPayload - The execution payload to simulate. + * @param from - The sender address. + * @param feeOptions - Fee options for the transaction. + * @param skipTxValidation - Whether to skip tx validation. + * @param skipFeeEnforcement - Whether to skip fee enforcement. + */ + protected async simulateViaEntrypoint( + executionPayload: ExecutionPayload, + from: AztecAddress, + feeOptions: FeeOptions, + skipTxValidation?: boolean, + skipFeeEnforcement?: boolean, + ) { + const txRequest = await this.createTxExecutionRequestFromPayloadAndFee(executionPayload, from, feeOptions); + return this.pxe.simulateTx(txRequest, true /* simulatePublic */, skipTxValidation, skipFeeEnforcement); + } + + /** + * Simulates a transaction, optimizing leading public static calls by running them directly + * on the node while sending the remaining calls through the standard PXE path. + * Return values from both paths are merged back in original call order. + * @param executionPayload - The execution payload to simulate. + * @param opts - Simulation options (from address, fee settings, etc.). + * @returns The merged simulation result. + */ async simulateTx(executionPayload: ExecutionPayload, opts: SimulateOptions): Promise { const feeOptions = opts.fee?.estimateGas ? await this.completeFeeOptionsForEstimation(opts.from, executionPayload.feePayer, opts.fee?.gasSettings) : await this.completeFeeOptions(opts.from, executionPayload.feePayer, opts.fee?.gasSettings); - const txRequest = await this.createTxExecutionRequestFromPayloadAndFee(executionPayload, opts.from, feeOptions); - return this.pxe.simulateTx( - txRequest, - true /* simulatePublic */, - opts?.skipTxValidation, - opts?.skipFeeEnforcement ?? true, - ); + const { optimizableCalls, remainingCalls } = extractOptimizablePublicStaticCalls(executionPayload); + const remainingPayload = { ...executionPayload, calls: remainingCalls }; + + const chainInfo = await this.getChainInfo(); + const blockHeader = await this.pxe.getSyncedBlockHeader(); + + const [optimizedResults, normalResult] = await Promise.all([ + optimizableCalls.length > 0 + ? simulateViaNode( + this.aztecNode, + optimizableCalls, + opts.from, + chainInfo, + feeOptions.gasSettings, + blockHeader, + opts.skipFeeEnforcement ?? true, + ) + : Promise.resolve([]), + remainingCalls.length > 0 + ? this.simulateViaEntrypoint( + remainingPayload, + opts.from, + feeOptions, + opts.skipTxValidation, + opts.skipFeeEnforcement ?? true, + ) + : Promise.resolve(null), + ]); + + return buildMergedSimulationResult(optimizedResults, normalResult); } async profileTx(executionPayload: ExecutionPayload, opts: ProfileOptions): Promise { diff --git a/yarn-project/wallet-sdk/src/base-wallet/index.ts b/yarn-project/wallet-sdk/src/base-wallet/index.ts index e25adf5f71c4..4b6718cae7ae 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/index.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/index.ts @@ -1 +1,2 @@ export { BaseWallet, type FeeOptions } from './base_wallet.js'; +export { simulateViaNode, buildMergedSimulationResult, extractOptimizablePublicStaticCalls } from './utils.js'; diff --git a/yarn-project/wallet-sdk/src/base-wallet/utils.ts b/yarn-project/wallet-sdk/src/base-wallet/utils.ts new file mode 100644 index 000000000000..adcafad98d94 --- /dev/null +++ b/yarn-project/wallet-sdk/src/base-wallet/utils.ts @@ -0,0 +1,229 @@ +import type { AztecNode } from '@aztec/aztec.js/node'; +import { MAX_ENQUEUED_CALLS_PER_CALL } from '@aztec/constants'; +import type { ChainInfo } from '@aztec/entrypoints/interfaces'; +import { makeTuple } from '@aztec/foundation/array'; +import { Fr } from '@aztec/foundation/curves/bn254'; +import type { Tuple } from '@aztec/foundation/serialize'; +import { generateSimulatedProvingResult } from '@aztec/pxe/simulator'; +import { type FunctionCall, FunctionSelector } from '@aztec/stdlib/abi'; +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { GasSettings } from '@aztec/stdlib/gas'; +import { + ClaimedLengthArray, + CountedPublicCallRequest, + PrivateCircuitPublicInputs, + PublicCallRequest, +} from '@aztec/stdlib/kernel'; +import { ChonkProof } from '@aztec/stdlib/proofs'; +import { + type BlockHeader, + type ExecutionPayload, + HashedValues, + PrivateCallExecutionResult, + PrivateExecutionResult, + PublicSimulationOutput, + Tx, + TxContext, + TxSimulationResult, +} from '@aztec/stdlib/tx'; + +/** + * Splits an execution payload into a leading prefix of public static calls + * (eligible for direct node simulation) and the remaining calls. + * + * Only a leading run of public static calls is eligible for optimization. + * Any non-public-static call may enqueue public state mutations + * (e.g. private calls can enqueue public calls), so all calls that follow + * must go through the normal simulation path to see the correct state. + * + */ +export function extractOptimizablePublicStaticCalls(payload: ExecutionPayload): { + /** Leading public static calls eligible for direct node simulation. */ + optimizableCalls: FunctionCall[]; + /** All remaining calls. */ + remainingCalls: FunctionCall[]; +} { + const splitIndex = payload.calls.findIndex(call => !call.isPublicStatic()); + const boundary = splitIndex === -1 ? payload.calls.length : splitIndex; + return { + optimizableCalls: payload.calls.slice(0, boundary), + remainingCalls: payload.calls.slice(boundary), + }; +} + +/** + * Simulates a batch of public static calls by bypassing account entrypoint and private execution, + * directly constructing a minimal Tx and calling node.simulatePublicCalls. + * + * @param node - The Aztec node to simulate on. + * @param publicStaticCalls - Array of public static function calls (max MAX_ENQUEUED_CALLS_PER_CALL). + * @param from - The account address making the calls. + * @param chainInfo - Chain information (chainId and version). + * @param gasSettings - Gas settings for the transaction. + * @param blockHeader - Block header to use as anchor. + * @param skipFeeEnforcement - Whether to skip fee enforcement during simulation. + * @returns TxSimulationResult with public return values. + */ +async function simulateBatchViaNode( + node: AztecNode, + publicStaticCalls: FunctionCall[], + from: AztecAddress, + chainInfo: ChainInfo, + gasSettings: GasSettings, + blockHeader: BlockHeader, + skipFeeEnforcement: boolean, +): Promise { + const txContext = new TxContext(chainInfo.chainId, chainInfo.version, gasSettings); + + const publicFunctionCalldata: HashedValues[] = []; + for (const call of publicStaticCalls) { + const calldata = await HashedValues.fromCalldata([call.selector.toField(), ...call.args]); + publicFunctionCalldata.push(calldata); + } + + const publicCallRequests = makeTuple(MAX_ENQUEUED_CALLS_PER_CALL, i => { + const call = publicStaticCalls[i]; + if (!call) { + return CountedPublicCallRequest.empty(); + } + const publicCallRequest = new PublicCallRequest(from, call.to, call.isStatic, publicFunctionCalldata[i]!.hash); + // Counter starts at 1 (minRevertibleSideEffectCounter) so all calls are revertible + return new CountedPublicCallRequest(publicCallRequest, i + 1); + }); + + const publicCallRequestsArray: ClaimedLengthArray = + new ClaimedLengthArray( + publicCallRequests as Tuple, + publicStaticCalls.length, + ); + + const publicInputs = PrivateCircuitPublicInputs.from({ + ...PrivateCircuitPublicInputs.empty(), + anchorBlockHeader: blockHeader, + txContext: txContext, + publicCallRequests: publicCallRequestsArray, + startSideEffectCounter: new Fr(0), + endSideEffectCounter: new Fr(publicStaticCalls.length + 1), + }); + + // Minimal entrypoint structure — no real private execution, just public call requests + const emptyEntrypoint = new PrivateCallExecutionResult( + Buffer.alloc(0), + Buffer.alloc(0), + new Map(), + publicInputs, + [], + new Map(), + [], + [], + [], + [], + [], + ); + + const privateResult = new PrivateExecutionResult(emptyEntrypoint, Fr.random(), publicFunctionCalldata); + + const provingResult = await generateSimulatedProvingResult( + privateResult, + (_contractAddress: AztecAddress, _functionSelector: FunctionSelector) => Promise.resolve(''), + 1, // minRevertibleSideEffectCounter + ); + + provingResult.publicInputs.feePayer = from; + + const tx = await Tx.create({ + data: provingResult.publicInputs, + chonkProof: ChonkProof.empty(), + contractClassLogFields: [], + publicFunctionCalldata: publicFunctionCalldata, + }); + + const publicOutput = await node.simulatePublicCalls(tx, skipFeeEnforcement); + + if (publicOutput.revertReason) { + throw publicOutput.revertReason; + } + + return new TxSimulationResult(privateResult, provingResult.publicInputs, publicOutput, undefined); +} + +/** + * Simulates public static calls by splitting them into batches of MAX_ENQUEUED_CALLS_PER_CALL + * and sending each batch directly to the node. + * + * @param node - The Aztec node to simulate on. + * @param publicStaticCalls - Array of public static function calls to optimize. + * @param from - The account address making the calls. + * @param chainInfo - Chain information (chainId and version). + * @param gasSettings - Gas settings for the transaction. + * @param blockHeader - Block header to use as anchor. + * @param skipFeeEnforcement - Whether to skip fee enforcement during simulation. + * @returns Array of TxSimulationResult, one per batch. + */ +export async function simulateViaNode( + node: AztecNode, + publicStaticCalls: FunctionCall[], + from: AztecAddress, + chainInfo: ChainInfo, + gasSettings: GasSettings, + blockHeader: BlockHeader, + skipFeeEnforcement: boolean = true, +): Promise { + const batches: FunctionCall[][] = []; + + for (let i = 0; i < publicStaticCalls.length; i += MAX_ENQUEUED_CALLS_PER_CALL) { + batches.push(publicStaticCalls.slice(i, i + MAX_ENQUEUED_CALLS_PER_CALL)); + } + + const results: TxSimulationResult[] = []; + + for (const batch of batches) { + const result = await simulateBatchViaNode( + node, + batch, + from, + chainInfo, + gasSettings, + blockHeader, + skipFeeEnforcement, + ); + results.push(result); + } + + return results; +} + +/** + * Merges simulation results from the optimized (public static) and normal paths. + * Since optimized calls are always a leading prefix, return values are simply + * concatenated: optimized first, then normal. + * Stats are taken from the normal result only (the optimized path doesn't produce them). + * + * @param optimizedResults - Results from optimized public static call batches. + * @param normalResult - Result from normal simulation (null if all calls were optimized). + * @returns A single TxSimulationResult with return values in original call order. + */ +export function buildMergedSimulationResult( + optimizedResults: TxSimulationResult[], + normalResult: TxSimulationResult | null, +): TxSimulationResult { + const optimizedReturnValues = optimizedResults.flatMap(r => r.publicOutput?.publicReturnValues ?? []); + const normalReturnValues = normalResult?.publicOutput?.publicReturnValues ?? []; + const allReturnValues = [...optimizedReturnValues, ...normalReturnValues]; + + const baseResult = normalResult ?? optimizedResults[0]; + + const mergedPublicOutput: PublicSimulationOutput | undefined = baseResult.publicOutput + ? { + ...baseResult.publicOutput, + publicReturnValues: allReturnValues, + } + : undefined; + + return new TxSimulationResult( + baseResult.privateExecutionResult, + baseResult.publicInputs, + mergedPublicOutput, + normalResult?.stats, + ); +}