Skip to content
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type { Fr } from '@aztec/foundation/curves/bn254';
import { FieldReader } from '@aztec/foundation/serialize';
import { AuthorizationSelector, FunctionSelector } from '@aztec/stdlib/abi';
import { computeInnerAuthWitHash } from '@aztec/stdlib/auth-witness';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import { computeVarArgsHash } from '@aztec/stdlib/hash';

/**
* An authwit request for a function call. Includes the preimage of the data
* to be signed, as opposed of just the inner hash.
*/
export class CallAuthorizationRequest {
constructor(
private constructor(
/**
* The selector of the authwit type, used to identify it
* when emitted from `emit_offchain_effect`oracle.
Expand Down Expand Up @@ -38,6 +40,26 @@ export class CallAuthorizationRequest {
public args: Fr[],
) {}

/** Validates that innerHash and argsHash are consistent with the provided preimage fields. */
private async validate(): Promise<void> {
const expectedArgsHash = await computeVarArgsHash(this.args);
if (!expectedArgsHash.equals(this.argsHash)) {
throw new Error(
`CallAuthorizationRequest argsHash mismatch: expected ${expectedArgsHash.toString()}, got ${this.argsHash.toString()}`,
);
}
const expectedInnerHash = await computeInnerAuthWitHash([
this.msgSender.toField(),
this.functionSelector.toField(),
this.argsHash,
]);
if (!expectedInnerHash.equals(this.innerHash)) {
throw new Error(
`CallAuthorizationRequest innerHash mismatch: expected ${expectedInnerHash.toString()}, got ${this.innerHash.toString()}`,
);
}
}

static getSelector(): Promise<AuthorizationSelector> {
return AuthorizationSelector.fromSignature('CallAuthorization((Field),(u32),Field)');
}
Expand All @@ -51,13 +73,15 @@ export class CallAuthorizationRequest {
`Invalid authorization selector for CallAuthwit: expected ${expectedSelector.toString()}, got ${selector.toString()}`,
);
}
return new CallAuthorizationRequest(
const request = new CallAuthorizationRequest(
selector,
reader.readField(),
AztecAddress.fromField(reader.readField()),
FunctionSelector.fromField(reader.readField()),
reader.readField(),
reader.readFieldArray(reader.remainingFields()),
);
await request.validate();
return request;
}
}
37 changes: 16 additions & 21 deletions yarn-project/cli-wallet/src/utils/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ import { AccountManager, type Aliased, type SimulateOptions } from '@aztec/aztec
import type { DefaultAccountEntrypointOptions } from '@aztec/entrypoints/account';
import { Fr } from '@aztec/foundation/curves/bn254';
import type { LogFn } from '@aztec/foundation/log';
import type { AccessScopes, NotesFilter } from '@aztec/pxe/client/lazy';
import type { NotesFilter } from '@aztec/pxe/client/lazy';
import type { PXEConfig } from '@aztec/pxe/config';
import type { PXE } from '@aztec/pxe/server';
import { createPXE, getPXEConfig } from '@aztec/pxe/server';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import { deriveSigningKey } from '@aztec/stdlib/keys';
import { NoteDao } from '@aztec/stdlib/note';
import type { TxProvingResult, TxSimulationResult } from '@aztec/stdlib/tx';
import type { SimulationOverrides, TxProvingResult, TxSimulationResult } from '@aztec/stdlib/tx';
import { ExecutionPayload, mergeExecutionPayloads } from '@aztec/stdlib/tx';
import { BaseWallet, type FeeOptions } from '@aztec/wallet-sdk/base-wallet';
import { BaseWallet, type SimulateViaEntrypointOptions } from '@aztec/wallet-sdk/base-wallet';

import type { WalletDB } from '../storage/wallet_db.js';
import type { AccountType } from './constants.js';
Expand Down Expand Up @@ -224,21 +224,19 @@ export class CLIWallet extends BaseWallet {
*/
protected override async simulateViaEntrypoint(
executionPayload: ExecutionPayload,
from: AztecAddress,
feeOptions: FeeOptions,
scopes: AccessScopes,
skipTxValidation?: boolean,
skipFeeEnforcement?: boolean,
opts: SimulateViaEntrypointOptions,
): Promise<TxSimulationResult> {
if (from.equals(AztecAddress.ZERO)) {
return super.simulateViaEntrypoint(
executionPayload,
from,
feeOptions,
scopes,
skipTxValidation,
skipFeeEnforcement,
);
const { from, feeOptions, scopes } = opts;
let overrides: SimulationOverrides | undefined;
let fromAccount: Account;
if (!from.equals(AztecAddress.ZERO)) {
const { account, instance, artifact } = await this.getFakeAccountDataFor(from);
fromAccount = account;
overrides = {
contracts: { [from.toString()]: { instance, artifact } },
};
} else {
fromAccount = await this.getAccountFromAddress(from);
}

const feeExecutionPayload = await feeOptions.walletFeePaymentMethod?.getExecutionPayload();
Expand All @@ -251,7 +249,6 @@ export class CLIWallet extends BaseWallet {
? mergeExecutionPayloads([feeExecutionPayload, executionPayload])
: executionPayload;

const { account: fromAccount, instance, artifact } = await this.getFakeAccountDataFor(from);
const chainInfo = await this.getChainInfo();
const txRequest = await fromAccount.createTxExecutionRequest(
finalExecutionPayload,
Expand All @@ -263,9 +260,7 @@ export class CLIWallet extends BaseWallet {
simulatePublic: true,
skipFeeEnforcement: true,
skipTxValidation: true,
overrides: {
contracts: { [from.toString()]: { instance, artifact } },
},
overrides,
scopes,
});
}
Expand Down
54 changes: 33 additions & 21 deletions yarn-project/end-to-end/src/e2e_kernelless_simulation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { PendingNoteHashesContract } from '@aztec/noir-test-contracts.js/Pending
import { type AbiDecoded, decodeFromAbi, getFunctionArtifact } from '@aztec/stdlib/abi';
import { computeOuterAuthWitHash } from '@aztec/stdlib/auth-witness';

import { jest } from '@jest/globals';

import { deployToken, mintTokensToPrivate } from './fixtures/token_utils.js';
import { setup } from './fixtures/utils.js';
import type { TestWallet } from './test-wallet/test_wallet.js';
Expand Down Expand Up @@ -108,7 +110,7 @@ describe('Kernelless simulation', () => {
nonceForAuthwits,
);

wallet.enableSimulatedSimulations();
wallet.setSimulationMode('kernelless-override');

const { offchainEffects } = await addLiquidityInteraction.simulate({
from: liquidityProviderAddress,
Expand Down Expand Up @@ -216,7 +218,7 @@ describe('Kernelless simulation', () => {
).resolves.toBeDefined();
});

it('produces matching gas estimates between kernelless and with-kernels simulation', async () => {
it('produces matching gas estimates and fee payer between kernelless and with-kernels simulation', async () => {
const swapperBalancesBefore = await getWalletBalances(swapperAddress);
const ammBalancesBefore = await getAmmBalances();

Expand All @@ -236,27 +238,27 @@ describe('Kernelless simulation', () => {
nonceForAuthwits,
);

wallet.enableSimulatedSimulations();
const swapKernellessGas = (
await swapExactTokensInteraction.simulate({
from: swapperAddress,
includeMetadata: true,
})
).estimatedGas!;
const simulateTxSpy = jest.spyOn(wallet, 'simulateTx');

wallet.setSimulationMode('kernelless-override');
const kernellessResult = await swapExactTokensInteraction.simulate({
from: swapperAddress,
includeMetadata: true,
});
const swapKernellessGas = kernellessResult.estimatedGas!;

const swapAuthwit = await wallet.createAuthWit(swapperAddress, {
caller: amm.address,
action: token0.methods.transfer_to_public(swapperAddress, amm.address, amountIn, nonceForAuthwits),
});

wallet.disableSimulatedSimulations();
const swapWithKernelsGas = (
await swapExactTokensInteraction.simulate({
from: swapperAddress,
includeMetadata: true,
authWitnesses: [swapAuthwit],
})
).estimatedGas!;
wallet.setSimulationMode('full');
const withKernelsResult = await swapExactTokensInteraction.simulate({
from: swapperAddress,
includeMetadata: true,
authWitnesses: [swapAuthwit],
});
const swapWithKernelsGas = withKernelsResult.estimatedGas!;

logger.info(`Kernelless gas: L2=${swapKernellessGas.gasLimits.l2Gas} DA=${swapKernellessGas.gasLimits.daGas}`);
logger.info(
Expand All @@ -265,6 +267,16 @@ describe('Kernelless simulation', () => {

expect(swapKernellessGas.gasLimits.daGas).toEqual(swapWithKernelsGas.gasLimits.daGas);
expect(swapKernellessGas.gasLimits.l2Gas).toEqual(swapWithKernelsGas.gasLimits.l2Gas);

expect(simulateTxSpy).toHaveBeenCalledTimes(2);
const kernellessTxResult = await (simulateTxSpy.mock.results[0].value as ReturnType<typeof wallet.simulateTx>);
const withKernelsTxResult = await (simulateTxSpy.mock.results[1].value as ReturnType<typeof wallet.simulateTx>);
const kernellessFeePayer = kernellessTxResult.publicInputs.feePayer;
const withKernelsFeePayer = withKernelsTxResult.publicInputs.feePayer;
expect(kernellessFeePayer).toEqual(withKernelsFeePayer);
expect(kernellessFeePayer).toEqual(swapperAddress);

simulateTxSpy.mockRestore();
});
});

Expand All @@ -288,15 +300,15 @@ describe('Kernelless simulation', () => {
await pendingNoteHashesContract.methods.get_then_nullify_note.selector(),
);

wallet.enableSimulatedSimulations();
wallet.setSimulationMode('kernelless-override');
const kernellessGas = (
await interaction.simulate({
from: adminAddress,
includeMetadata: true,
})
).estimatedGas!;

wallet.disableSimulatedSimulations();
wallet.setSimulationMode('full');
const withKernelsGas = (
await interaction.simulate({
from: adminAddress,
Expand Down Expand Up @@ -325,14 +337,14 @@ describe('Kernelless simulation', () => {
const mintAmount = 100n;

// Insert a note with real kernels so it lands on-chain
wallet.disableSimulatedSimulations();
wallet.setSimulationMode('full');
await pendingNoteHashesContract.methods.insert_note(mintAmount, adminAddress, adminAddress).send({
from: adminAddress,
});

// Kernelless simulation of reading + nullifying that settled note produces a settled
// read request that gets verified against the note hash tree at the anchor block
wallet.enableSimulatedSimulations();
wallet.setSimulationMode('kernelless-override');
await expect(
pendingNoteHashesContract.methods.get_then_nullify_note(mintAmount, adminAddress).simulate({
from: adminAddress,
Expand Down
70 changes: 29 additions & 41 deletions yarn-project/end-to-end/src/test-wallet/test_wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ import { AccountManager, type SendOptions } from '@aztec/aztec.js/wallet';
import type { DefaultAccountEntrypointOptions } from '@aztec/entrypoints/account';
import { Fq, Fr } from '@aztec/foundation/curves/bn254';
import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin';
import type { AccessScopes, NotesFilter } from '@aztec/pxe/client/lazy';
import type { NotesFilter } from '@aztec/pxe/client/lazy';
import { type PXEConfig, getPXEConfig } from '@aztec/pxe/config';
import { PXE, type PXECreationOptions, createPXE } from '@aztec/pxe/server';
import { AuthWitness } from '@aztec/stdlib/auth-witness';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import { getContractInstanceFromInstantiationParams } from '@aztec/stdlib/contract';
import { deriveSigningKey } from '@aztec/stdlib/keys';
import type { NoteDao } from '@aztec/stdlib/note';
import type { BlockHeader, TxHash, TxReceipt, TxSimulationResult } from '@aztec/stdlib/tx';
import type { BlockHeader, SimulationOverrides, TxHash, TxReceipt, TxSimulationResult } from '@aztec/stdlib/tx';
import { ExecutionPayload, mergeExecutionPayloads } from '@aztec/stdlib/tx';
import { BaseWallet, type FeeOptions } from '@aztec/wallet-sdk/base-wallet';
import { BaseWallet, type SimulateViaEntrypointOptions } from '@aztec/wallet-sdk/base-wallet';

import { AztecNodeProxy, ProvenTx } from './utils.js';

Expand Down Expand Up @@ -125,21 +125,15 @@ export class TestWallet extends BaseWallet {
protected accounts: Map<string, Account> = new Map();

/**
* Toggle for running "simulated simulations" when calling simulateTx.
*
* When this flag is true, simulateViaEntrypoint constructs a request using a fake account
* (and accepts contract overrides on the input) and the PXE emulates kernel effects without
* generating kernel witnesses. When false, simulateViaEntrypoint defers to the standard
* simulation path via the real account entrypoint.
* Controls how the test wallet simulates transactions:
* - `kernelless`: Skips kernel circuits but uses the real account contract. Default.
* - `kernelless-override`: Skips kernels and replaces the account with a stub that doesn't do authwit validation.
* - `full`: Uses real kernel circuits and real account contracts. Slow!
*/
private simulatedSimulations = false;
private simulationMode: 'kernelless' | 'kernelless-override' | 'full' = 'kernelless';

enableSimulatedSimulations() {
this.simulatedSimulations = true;
}

disableSimulatedSimulations() {
this.simulatedSimulations = false;
setSimulationMode(mode: 'kernelless' | 'kernelless-override' | 'full') {
this.simulationMode = mode;
}

setMinFeePadding(value?: number) {
Expand Down Expand Up @@ -220,27 +214,24 @@ export class TestWallet extends BaseWallet {
return account.createAuthWit(intentInnerHash, chainInfo);
}

/**
* Override simulateViaEntrypoint to use fake accounts for kernelless simulation
* when simulatedSimulations is enabled. Otherwise falls through to the real entrypoint path.
*/
protected override async simulateViaEntrypoint(
executionPayload: ExecutionPayload,
from: AztecAddress,
feeOptions: FeeOptions,
scopes: AccessScopes,
skipTxValidation?: boolean,
skipFeeEnforcement?: boolean,
opts: SimulateViaEntrypointOptions,
): Promise<TxSimulationResult> {
if (!this.simulatedSimulations) {
return super.simulateViaEntrypoint(
executionPayload,
from,
feeOptions,
scopes,
skipTxValidation,
skipFeeEnforcement,
);
const { from, feeOptions, scopes, skipTxValidation, skipFeeEnforcement } = opts;
const skipKernels = this.simulationMode !== 'full';
const useOverride = this.simulationMode === 'kernelless-override' && !from.equals(AztecAddress.ZERO);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this maybe be an error, or should we instead take an enum or something? It feels weird to specify override but then not get overrides.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going away with NO_FROM


let overrides: SimulationOverrides | undefined;
let fromAccount: Account;
if (useOverride) {
const { account, instance, artifact } = await this.getFakeAccountDataFor(from);
fromAccount = account;
overrides = {
contracts: { [from.toString()]: { instance, artifact } },
};
} else {
fromAccount = await this.getAccountFromAddress(from);
}

const feeExecutionPayload = await feeOptions.walletFeePaymentMethod?.getExecutionPayload();
Expand All @@ -252,22 +243,19 @@ export class TestWallet extends BaseWallet {
const finalExecutionPayload = feeExecutionPayload
? mergeExecutionPayloads([feeExecutionPayload, executionPayload])
: executionPayload;
const { account: fromAccount, instance, artifact } = await this.getFakeAccountDataFor(from);
const chainInfo = await this.getChainInfo();
const txRequest = await fromAccount.createTxExecutionRequest(
finalExecutionPayload,
feeOptions.gasSettings,
chainInfo,
executionOptions,
);
const contractOverrides = {
[from.toString()]: { instance, artifact },
};
return this.pxe.simulateTx(txRequest, {
simulatePublic: true,
skipFeeEnforcement: true,
skipTxValidation: true,
overrides: { contracts: contractOverrides },
skipKernels,
skipFeeEnforcement,
skipTxValidation,
overrides,
scopes,
});
}
Expand Down
Loading
Loading