diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index 0f876dd56034..4129c0e52f54 100644 --- a/noir-projects/noir-contracts/Nargo.toml +++ b/noir-projects/noir-contracts/Nargo.toml @@ -4,7 +4,8 @@ members = [ "contracts/account/ecdsa_r_account_contract", "contracts/account/schnorr_account_contract", "contracts/account/schnorr_hardcoded_account_contract", - "contracts/account/simulated_account_contract", + "contracts/account/simulated_schnorr_account_contract", + "contracts/account/simulated_ecdsa_account_contract", "contracts/app/amm_contract", "contracts/app/app_subscription_contract", "contracts/app/auth_contract", diff --git a/noir-projects/noir-contracts/contracts/account/simulated_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/account/simulated_account_contract/src/main.nr deleted file mode 100644 index f6d70cde52ed..000000000000 --- a/noir-projects/noir-contracts/contracts/account/simulated_account_contract/src/main.nr +++ /dev/null @@ -1,52 +0,0 @@ -use aztec::macros::aztec; - -// Account contract that does not perform any authentication, typically used to override a *real* account contract -// in a simulation. This avoids the need of generating real signatures during simulation. -// It will also consider all authwits sent to it as valid and emit the hash in an offchain effect -// for later verification. In order to properly mimic our account contract it imports the PublickKeyNote -// struct so the #[aztec] macro can generate a `sync_state` implementation that can process the *real* -// public key of the overridden contract -#[aztec] -pub contract SimulatedAccount { - use aztec::{ - authwit::{account::AccountActions, auth::IS_VALID_SELECTOR, entrypoint::app::AppPayload}, - context::PrivateContext, - macros::functions::{allow_phase_change, external, view}, - oracle::notes::set_sender_for_tags, - }; - - // @dev: If you globally change the entrypoint signature don't forget to update account_entrypoint.ts (specifically `getEntrypointAbi()`) - #[external("private")] - #[allow_phase_change] - fn entrypoint(app_payload: AppPayload, fee_payment_method: u8, cancellable: bool) { - // Safety: The sender for tags is only used to compute unconstrained shared secrets for emitting logs. - // Since this value is only used for unconstrained tagging and not for any constrained logic, - // it is safe to set from a constrained context. - unsafe { set_sender_for_tags(self.address) }; - - let actions = AccountActions::init(self.context, is_valid_impl); - actions.entrypoint(app_payload, fee_payment_method, cancellable); - } - - #[external("private")] - #[view] - fn verify_private_authwit(inner_hash: Field) -> Field { - IS_VALID_SELECTOR - } - - #[contract_library_method] - fn is_valid_impl(_context: &mut PrivateContext, _outer_hash: Field) -> bool { - true - } - - // This contract override exists solely to enable simulation without requiring users to sign anything. Therefore, - // this function should never be called - this contract should never be used to sync the state of the original - // contract! Doing so could result in an undefined behavior. - #[external("utility")] - unconstrained fn sync_state() { - assert( - false, - "BUG ALERT: sync_state on a simulated account contract should never be triggered.", - ); - } -} diff --git a/noir-projects/noir-contracts/contracts/account/simulated_account_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/Nargo.toml similarity index 76% rename from noir-projects/noir-contracts/contracts/account/simulated_account_contract/Nargo.toml rename to noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/Nargo.toml index 3a2a16d624f3..71998b27abbb 100644 --- a/noir-projects/noir-contracts/contracts/account/simulated_account_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/Nargo.toml @@ -1,5 +1,5 @@ [package] -name = "simulated_account_contract" +name = "simulated_ecdsa_account_contract" authors = [""] compiler_version = ">=0.25.0" type = "contract" diff --git a/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/src/main.nr new file mode 100644 index 000000000000..b27a1d6f5a0e --- /dev/null +++ b/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/src/main.nr @@ -0,0 +1,80 @@ +use aztec::macros::aztec; + +// Stub account contract for ECDSA accounts (both secp256k1 and secp256r1) used during simulation. +// Matches the constructor signature of EcdsaKAccount / EcdsaRAccount so that deployment +// simulations using this stub as an override do not fail on selector lookup. +// See simulated_account_contract for the base stub without a constructor. +#[aztec] +pub contract SimulatedEcdsaAccount { + use aztec::{ + authwit::{account::AccountActions, auth::IS_VALID_SELECTOR, entrypoint::app::AppPayload}, + context::PrivateContext, + macros::functions::{allow_phase_change, external, view}, + messages::encoding::MESSAGE_CIPHERTEXT_LEN, + oracle::{notes::set_sender_for_tags, random::random}, + }; + + // Stub constructor matching the EcdsaKAccount / EcdsaRAccount constructor signature. + // Does NOT use #[initializer] so that the macro does not inject + // assert_initialization_matches_address_preimage_private, which would fail during kernelless + // simulation because the stub instance has a different initialization hash than the real account. + // Emits the same shape of side effects as the real constructor (one nullifier for + // SinglePrivateImmutable initialization, one note hash for the key note, and one private + // log tied to that note hash) so that gas estimation produces accurate results. + #[external("private")] + fn constructor(_signing_pub_key_x: [u8; 32], _signing_pub_key_y: [u8; 32]) { + // Safety: Random seeds are only used to produce dummy side effects that match the shape of + // the real EcdsaKAccount / EcdsaRAccount constructor. The values are never constrained or + // used for any security purpose. + let seed = unsafe { random() }; + + // Emit the initialization nullifier for the signing_public_key SinglePrivateImmutable. + self.context.push_nullifier(seed); + + // Emit the note hash for the signing key note. + self.context.push_note_hash(seed + 1); + + // Emit a private log tied to the note hash, matching the length of a real note delivery + // log (MESSAGE_CIPHERTEXT_LEN fields). The signing key note is a SinglePrivateImmutable + // and is therefore never nullified, so in practice the log will never be squashed. We + // pass the note hash counter anyway for correctness. + let dummy_log: [Field; MESSAGE_CIPHERTEXT_LEN] = [seed + 2; MESSAGE_CIPHERTEXT_LEN]; + self.context.emit_raw_note_log_unsafe( + seed + 3, + dummy_log, + MESSAGE_CIPHERTEXT_LEN, + self.context.side_effect_counter, + ); + } + + // @dev: If you globally change the entrypoint signature don't forget to update account_entrypoint.ts + #[external("private")] + #[allow_phase_change] + fn entrypoint(app_payload: AppPayload, fee_payment_method: u8, cancellable: bool) { + // Safety: The sender for tags is only used to compute unconstrained shared secrets for + // emitting logs. It is not used in any constrained logic, so it is safe to set here. + unsafe { set_sender_for_tags(self.address) }; + + let actions = AccountActions::init(self.context, is_valid_impl); + actions.entrypoint(app_payload, fee_payment_method, cancellable); + } + + #[external("private")] + #[view] + fn verify_private_authwit(inner_hash: Field) -> Field { + IS_VALID_SELECTOR + } + + #[contract_library_method] + fn is_valid_impl(_context: &mut PrivateContext, _outer_hash: Field) -> bool { + true + } + + #[external("utility")] + unconstrained fn sync_state() { + assert( + false, + "BUG ALERT: sync_state on a simulated account contract should never be triggered.", + ); + } +} diff --git a/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/Nargo.toml new file mode 100644 index 000000000000..f2ebb5cadf60 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "simulated_schnorr_account_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../../aztec-nr/aztec" } diff --git a/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/src/main.nr new file mode 100644 index 000000000000..4ac523076a6e --- /dev/null +++ b/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/src/main.nr @@ -0,0 +1,80 @@ +use aztec::macros::aztec; + +// Stub account contract for Schnorr accounts used during simulation. +// Matches the constructor signature of SchnorrAccount so that deployment +// simulations using this stub as an override do not fail on selector lookup. +// See simulated_account_contract for the base stub without a constructor. +#[aztec] +pub contract SimulatedSchnorrAccount { + use aztec::{ + authwit::{account::AccountActions, auth::IS_VALID_SELECTOR, entrypoint::app::AppPayload}, + context::PrivateContext, + macros::functions::{allow_phase_change, external, view}, + messages::encoding::MESSAGE_CIPHERTEXT_LEN, + oracle::{notes::set_sender_for_tags, random::random}, + }; + + // Stub constructor matching the SchnorrAccount constructor signature. + // Does NOT use #[initializer] so that the macro does not inject + // assert_initialization_matches_address_preimage_private, which would fail during kernelless + // simulation because the stub instance has a different initialization hash than the real account. + // Emits the same shape of side effects as the real constructor (one nullifier for + // SinglePrivateImmutable initialization, one note hash for the key note, and one private + // log tied to that note hash) so that gas estimation produces accurate results. + #[external("private")] + fn constructor(_signing_pub_key_x: Field, _signing_pub_key_y: Field) { + // Safety: Random seeds are only used to produce dummy side effects that match the shape of + // the real SchnorrAccount constructor. The values are never constrained or used for any + // security purpose. + let seed = unsafe { random() }; + + // Emit the initialization nullifier for the signing_public_key SinglePrivateImmutable. + self.context.push_nullifier(seed); + + // Emit the note hash for the signing key note. + self.context.push_note_hash(seed + 1); + + // Emit a private log tied to the note hash, matching the length of a real note delivery + // log (MESSAGE_CIPHERTEXT_LEN fields). The signing key note is a SinglePrivateImmutable + // and is therefore never nullified, so in practice the log will never be squashed. We + // pass the note hash counter anyway for correctness. + let dummy_log: [Field; MESSAGE_CIPHERTEXT_LEN] = [seed + 2; MESSAGE_CIPHERTEXT_LEN]; + self.context.emit_raw_note_log_unsafe( + seed + 3, + dummy_log, + MESSAGE_CIPHERTEXT_LEN, + self.context.side_effect_counter, + ); + } + + // @dev: If you globally change the entrypoint signature don't forget to update account_entrypoint.ts + #[external("private")] + #[allow_phase_change] + fn entrypoint(app_payload: AppPayload, fee_payment_method: u8, cancellable: bool) { + // Safety: The sender for tags is only used to compute unconstrained shared secrets for + // emitting logs. It is not used in any constrained logic, so it is safe to set here. + unsafe { set_sender_for_tags(self.address) }; + + let actions = AccountActions::init(self.context, is_valid_impl); + actions.entrypoint(app_payload, fee_payment_method, cancellable); + } + + #[external("private")] + #[view] + fn verify_private_authwit(inner_hash: Field) -> Field { + IS_VALID_SELECTOR + } + + #[contract_library_method] + fn is_valid_impl(_context: &mut PrivateContext, _outer_hash: Field) -> bool { + true + } + + #[external("utility")] + unconstrained fn sync_state() { + assert( + false, + "BUG ALERT: sync_state on a simulated account contract should never be triggered.", + ); + } +} diff --git a/yarn-project/accounts/package.json b/yarn-project/accounts/package.json index 01f6ed385e54..1cfdc55fb2c8 100644 --- a/yarn-project/accounts/package.json +++ b/yarn-project/accounts/package.json @@ -10,8 +10,10 @@ "./ecdsa/lazy": "./dest/ecdsa/lazy.js", "./schnorr": "./dest/schnorr/index.js", "./schnorr/lazy": "./dest/schnorr/lazy.js", - "./stub": "./dest/stub/index.js", - "./stub/lazy": "./dest/stub/lazy.js", + "./stub/schnorr": "./dest/stub/schnorr/index.js", + "./stub/schnorr/lazy": "./dest/stub/schnorr/lazy.js", + "./stub/ecdsa": "./dest/stub/ecdsa/index.js", + "./stub/ecdsa/lazy": "./dest/stub/ecdsa/lazy.js", "./testing": "./dest/testing/index.js", "./testing/lazy": "./dest/testing/lazy.js", "./copy-cat": "./dest/copy_cat/index.js", diff --git a/yarn-project/accounts/scripts/copy-contracts.sh b/yarn-project/accounts/scripts/copy-contracts.sh index 16dfcd9b3325..c8babc835659 100755 --- a/yarn-project/accounts/scripts/copy-contracts.sh +++ b/yarn-project/accounts/scripts/copy-contracts.sh @@ -2,7 +2,7 @@ set -euo pipefail mkdir -p ./artifacts -contracts=(schnorr_account_contract-SchnorrAccount ecdsa_k_account_contract-EcdsaKAccount ecdsa_r_account_contract-EcdsaRAccount simulated_account_contract-SimulatedAccount ) +contracts=(schnorr_account_contract-SchnorrAccount ecdsa_k_account_contract-EcdsaKAccount ecdsa_r_account_contract-EcdsaRAccount simulated_schnorr_account_contract-SimulatedSchnorrAccount simulated_ecdsa_account_contract-SimulatedEcdsaAccount ) decl=$(cat < { - return Promise.resolve(StubAccountContractArtifact); - } -} - -/** - * Creates a stub account that impersonates the one with the provided originalAddress. - * @param originalAddress - The address of the account to stub - * @returns A stub account that can be used for kernelless simulations - */ -export function createStubAccount(originalAddress: CompleteAddress) { - const accountContract = new StubAccountContract(); - const authWitnessProvider = accountContract.getAuthWitnessProvider(originalAddress); - return new BaseAccount( - new DefaultAccountEntrypoint(originalAddress.address, authWitnessProvider), - authWitnessProvider, - originalAddress, - ); -} diff --git a/yarn-project/accounts/src/stub/lazy.ts b/yarn-project/accounts/src/stub/lazy.ts deleted file mode 100644 index 4332560b0c81..000000000000 --- a/yarn-project/accounts/src/stub/lazy.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { BaseAccount } from '@aztec/aztec.js/account'; -import type { CompleteAddress } from '@aztec/aztec.js/addresses'; -import { DefaultAccountEntrypoint } from '@aztec/entrypoints/account'; -import type { ContractArtifact } from '@aztec/stdlib/abi'; -import { loadContractArtifact } from '@aztec/stdlib/abi'; - -import { StubBaseAccountContract } from './account_contract.js'; - -/** - * Lazily loads the contract artifact - * @returns The contract artifact for the Stub account contract - */ -export async function getStubAccountContractArtifact() { - // Cannot assert this import as it's incompatible with bundlers like vite - // https://github.com/vitejs/vite/issues/19095#issuecomment-2566074352 - // Even if now supported by al major browsers, the MIME type is replaced with - // "text/javascript" - // In the meantime, this lazy import is INCOMPATIBLE WITH NODEJS - const { default: StubAccountContractJson } = await import('../../artifacts/SimulatedAccount.json'); - return loadContractArtifact(StubAccountContractJson); -} - -/** - * Account contract that authenticates transactions using Stub signatures - * verified against a Grumpkin public key stored in an immutable encrypted note. - * Lazily loads the contract artifact - */ -export class StubAccountContract extends StubBaseAccountContract { - constructor() { - super(); - } - - override getContractArtifact(): Promise { - return getStubAccountContractArtifact(); - } -} - -/** - * Creates a stub account that impersonates the one with the provided originalAddress. - * @param originalAddress - The address of the account to stub - * @returns A stub account that can be used for kernelless simulations - */ -export function createStubAccount(originalAddress: CompleteAddress) { - const accountContract = new StubAccountContract(); - const authWitnessProvider = accountContract.getAuthWitnessProvider(originalAddress); - return new BaseAccount( - new DefaultAccountEntrypoint(originalAddress.address, authWitnessProvider), - authWitnessProvider, - originalAddress, - ); -} diff --git a/yarn-project/accounts/src/stub/schnorr/index.ts b/yarn-project/accounts/src/stub/schnorr/index.ts new file mode 100644 index 000000000000..640343cd0c5b --- /dev/null +++ b/yarn-project/accounts/src/stub/schnorr/index.ts @@ -0,0 +1,30 @@ +import { BaseAccount } from '@aztec/aztec.js/account'; +import type { CompleteAddress } from '@aztec/aztec.js/addresses'; +import { DefaultAccountEntrypoint } from '@aztec/entrypoints/account'; +import { loadContractArtifact } from '@aztec/stdlib/abi'; +import type { NoirCompiledContract } from '@aztec/stdlib/noir'; + +import SimulatedSchnorrAccountJson from '../../../artifacts/SimulatedSchnorrAccount.json' with { type: 'json' }; +import { StubBaseAccountContract } from '../account_contract.js'; + +export const StubSchnorrAccountContractArtifact = loadContractArtifact( + SimulatedSchnorrAccountJson as NoirCompiledContract, +); + +/** Stub account contract for Schnorr accounts. Eagerly loads the contract artifact. */ +export class StubSchnorrAccountContract extends StubBaseAccountContract { + override getContractArtifact() { + return Promise.resolve(StubSchnorrAccountContractArtifact); + } +} + +/** Creates a Schnorr stub account that impersonates the one with the provided address. */ +export function createStubSchnorrAccount(originalAddress: CompleteAddress) { + const accountContract = new StubSchnorrAccountContract(); + const authWitnessProvider = accountContract.getAuthWitnessProvider(originalAddress); + return new BaseAccount( + new DefaultAccountEntrypoint(originalAddress.address, authWitnessProvider), + authWitnessProvider, + originalAddress, + ); +} diff --git a/yarn-project/accounts/src/stub/schnorr/lazy.ts b/yarn-project/accounts/src/stub/schnorr/lazy.ts new file mode 100644 index 000000000000..413a42f3692b --- /dev/null +++ b/yarn-project/accounts/src/stub/schnorr/lazy.ts @@ -0,0 +1,34 @@ +import { BaseAccount } from '@aztec/aztec.js/account'; +import type { CompleteAddress } from '@aztec/aztec.js/addresses'; +import { DefaultAccountEntrypoint } from '@aztec/entrypoints/account'; +import { loadContractArtifact } from '@aztec/stdlib/abi'; + +import { StubBaseAccountContract } from '../account_contract.js'; + +/** + * Lazily loads the Schnorr stub contract artifact (browser-compatible). + */ +export async function getStubSchnorrAccountContractArtifact() { + // Cannot assert this import as it's incompatible with bundlers like vite + // https://github.com/vitejs/vite/issues/19095#issuecomment-2566074352 + const { default: json } = await import('../../../artifacts/SimulatedSchnorrAccount.json'); + return loadContractArtifact(json); +} + +/** Stub account contract for Schnorr accounts. Lazily loads the contract artifact. */ +export class StubSchnorrAccountContract extends StubBaseAccountContract { + override getContractArtifact() { + return getStubSchnorrAccountContractArtifact(); + } +} + +/** Creates a Schnorr stub account that impersonates the one with the provided address. */ +export function createStubSchnorrAccount(originalAddress: CompleteAddress) { + const accountContract = new StubSchnorrAccountContract(); + const authWitnessProvider = accountContract.getAuthWitnessProvider(originalAddress); + return new BaseAccount( + new DefaultAccountEntrypoint(originalAddress.address, authWitnessProvider), + authWitnessProvider, + originalAddress, + ); +} diff --git a/yarn-project/cli-wallet/src/utils/wallet.ts b/yarn-project/cli-wallet/src/utils/wallet.ts index dc799905dff2..44329c0ed830 100644 --- a/yarn-project/cli-wallet/src/utils/wallet.ts +++ b/yarn-project/cli-wallet/src/utils/wallet.ts @@ -1,6 +1,7 @@ import { EcdsaRAccountContract, EcdsaRSSHAccountContract } from '@aztec/accounts/ecdsa'; import { SchnorrAccountContract } from '@aztec/accounts/schnorr'; -import { StubAccountContractArtifact, createStubAccount } from '@aztec/accounts/stub'; +import { StubEcdsaAccountContractArtifact, createStubEcdsaAccount } from '@aztec/accounts/stub/ecdsa'; +import { StubSchnorrAccountContractArtifact, createStubSchnorrAccount } from '@aztec/accounts/stub/schnorr'; import { getIdentities } from '@aztec/accounts/utils'; import { type Account, type AccountContract, NO_FROM } from '@aztec/aztec.js/account'; import { @@ -200,15 +201,12 @@ export class CLIWallet extends BaseWallet { if (!contractInstance) { throw new Error(`No contract instance found for address: ${originalAddress.address}`); } - const stubAccount = createStubAccount(originalAddress); - const instance = await getContractInstanceFromInstantiationParams(StubAccountContractArtifact, { - salt: Fr.random(), - }); - return { - account: stubAccount, - instance, - artifact: StubAccountContractArtifact, - }; + const { type } = await this.db!.retrieveAccount(address); + const isSchnorr = type === 'schnorr'; + const artifact = isSchnorr ? StubSchnorrAccountContractArtifact : StubEcdsaAccountContractArtifact; + const stubAccount = isSchnorr ? createStubSchnorrAccount(originalAddress) : createStubEcdsaAccount(originalAddress); + const instance = await getContractInstanceFromInstantiationParams(artifact, { salt: Fr.random() }); + return { account: stubAccount, instance, artifact }; } override async simulateTx( @@ -237,7 +235,8 @@ export class CLIWallet extends BaseWallet { executionPayload: ExecutionPayload, opts: SimulateViaEntrypointOptions, ): Promise { - const { from, feeOptions, scopes } = opts; + const { from, feeOptions, additionalScopes } = opts; + const scopes = this.scopesFrom(from, additionalScopes); const feeExecutionPayload = await feeOptions.walletFeePaymentMethod?.getExecutionPayload(); const finalExecutionPayload = feeExecutionPayload ? mergeExecutionPayloads([feeExecutionPayload, executionPayload]) diff --git a/yarn-project/end-to-end/src/e2e_account_contracts.test.ts b/yarn-project/end-to-end/src/e2e_account_contracts.test.ts index f501ec18c8ef..d48af0ff7fe3 100644 --- a/yarn-project/end-to-end/src/e2e_account_contracts.test.ts +++ b/yarn-project/end-to-end/src/e2e_account_contracts.test.ts @@ -31,7 +31,8 @@ export class TestWalletInternals extends TestWallet { } replaceAccountAt(account: Account, address: AztecAddress) { - this.accounts.set(address.toString(), account); + const existing = this.accounts.get(address.toString()); + this.accounts.set(address.toString(), { account, type: existing!.type }); } } diff --git a/yarn-project/end-to-end/src/test-wallet/test_wallet.ts b/yarn-project/end-to-end/src/test-wallet/test_wallet.ts index 0dbd8d2b2d68..8541c349f028 100644 --- a/yarn-project/end-to-end/src/test-wallet/test_wallet.ts +++ b/yarn-project/end-to-end/src/test-wallet/test_wallet.ts @@ -1,7 +1,9 @@ import { EcdsaKAccountContract, EcdsaRAccountContract } from '@aztec/accounts/ecdsa'; import { SchnorrAccountContract } from '@aztec/accounts/schnorr'; -import { StubAccountContractArtifact, createStubAccount } from '@aztec/accounts/stub'; +import { StubEcdsaAccountContractArtifact, createStubEcdsaAccount } from '@aztec/accounts/stub/ecdsa'; +import { StubSchnorrAccountContractArtifact, createStubSchnorrAccount } from '@aztec/accounts/stub/schnorr'; import { type Account, type AccountContract, NO_FROM } from '@aztec/aztec.js/account'; +import type { CompleteAddress } from '@aztec/aztec.js/addresses'; import { type CallIntent, type ContractFunctionInteractionCallIntent, @@ -36,6 +38,7 @@ import { } from '@aztec/stdlib/tx'; import { ExecutionPayload, mergeExecutionPayloads } from '@aztec/stdlib/tx'; import { BaseWallet, type SimulateViaEntrypointOptions } from '@aztec/wallet-sdk/base-wallet'; +import type { AccountType } from '@aztec/wallets/embedded'; import { AztecNodeProxy, ProvenTx } from './utils.js'; @@ -45,6 +48,7 @@ import { AztecNodeProxy, ProvenTx } from './utils.js'; export interface AccountData { secret: Fr; salt: Fr; + type?: AccountType; contract: AccountContract; } @@ -85,30 +89,25 @@ export class TestWallet extends BaseWallet { createSchnorrAccount(secret: Fr, salt: Fr, signingKey?: Fq): Promise { signingKey = signingKey ?? deriveSigningKey(secret); - const accountData = { - secret, - salt, - contract: new SchnorrAccountContract(signingKey), - }; - return this.createAccount(accountData); + return this.createAccount({ secret, salt, type: 'schnorr', contract: new SchnorrAccountContract(signingKey) }); } createECDSARAccount(secret: Fr, salt: Fr, signingKey: Buffer): Promise { - const accountData = { + return this.createAccount({ secret, salt, + type: 'ecdsasecp256r1', contract: new EcdsaRAccountContract(signingKey), - }; - return this.createAccount(accountData); + }); } createECDSAKAccount(secret: Fr, salt: Fr, signingKey: Buffer): Promise { - const accountData = { + return this.createAccount({ secret, salt, + type: 'ecdsasecp256k1', contract: new EcdsaKAccountContract(signingKey), - }; - return this.createAccount(accountData); + }); } /** @@ -131,20 +130,40 @@ export class TestWallet extends BaseWallet { ); } - const stubInstance = await getContractInstanceFromInstantiationParams(StubAccountContractArtifact, { + const stubArtifact = this.getStubArtifactFor(address); + const stubConstructorArgs = + this.getTypeFor(address) === 'schnorr' ? [Fr.ZERO, Fr.ZERO] : [Buffer.alloc(32), Buffer.alloc(32)]; + const stubInstance = await getContractInstanceFromInstantiationParams(stubArtifact, { salt: Fr.random(), + constructorArgs: stubConstructorArgs, }); contracts[address.toString()] = { instance: stubInstance, - artifact: StubAccountContractArtifact, + artifact: stubArtifact, }; } return contracts; } - protected accounts: Map = new Map(); + protected accounts: Map = new Map(); + + private getTypeFor(address: AztecAddress): AccountType { + return this.accounts.get(address.toString())?.type ?? 'schnorr'; + } + + private getStubArtifactFor(address: AztecAddress) { + return this.getTypeFor(address) === 'schnorr' + ? StubSchnorrAccountContractArtifact + : StubEcdsaAccountContractArtifact; + } + + private getStubAccountFor(address: AztecAddress, completeAddress: CompleteAddress) { + return this.getTypeFor(address) === 'schnorr' + ? createStubSchnorrAccount(completeAddress) + : createStubEcdsaAccount(completeAddress); + } /** * Controls how the test wallet simulates transactions: @@ -163,22 +182,25 @@ export class TestWallet extends BaseWallet { } protected getAccountFromAddress(address: AztecAddress): Promise { - const account = this.accounts.get(address?.toString() ?? ''); + const entry = this.accounts.get(address?.toString() ?? ''); - if (!account) { + if (!entry) { throw new Error(`Account not found in wallet for address: ${address}`); } - return Promise.resolve(account); + return Promise.resolve(entry.account); } getAccounts() { - return Promise.resolve(Array.from(this.accounts.values()).map(acc => ({ alias: '', item: acc.getAddress() }))); + return Promise.resolve( + Array.from(this.accounts.values()).map(entry => ({ alias: '', item: entry.account.getAddress() })), + ); } async createAccount(accountData?: AccountData): Promise { const secret = accountData?.secret ?? Fr.random(); const salt = accountData?.salt ?? Fr.random(); + const type = accountData?.type ?? 'schnorr'; const contract = accountData?.contract ?? new SchnorrAccountContract(GrumpkinScalar.random()); const accountManager = await AccountManager.create(this, secret, contract, salt); @@ -188,7 +210,8 @@ export class TestWallet extends BaseWallet { await this.registerContract(instance, artifact, secret); - this.accounts.set(accountManager.address.toString(), await accountManager.getAccount()); + const address = accountManager.address.toString(); + this.accounts.set(address, { account: await accountManager.getAccount(), type }); return accountManager; } @@ -235,7 +258,8 @@ export class TestWallet extends BaseWallet { executionPayload: ExecutionPayload, opts: SimulateViaEntrypointOptions, ): Promise { - const { from, feeOptions, scopes, skipTxValidation, skipFeeEnforcement } = opts; + const { from, feeOptions, additionalScopes, skipTxValidation, skipFeeEnforcement } = opts; + const scopes = this.scopesFrom(from, additionalScopes); const skipKernels = this.simulationMode !== 'full'; const useOverride = this.simulationMode === 'kernelless-override'; @@ -248,7 +272,7 @@ export class TestWallet extends BaseWallet { let overrides: SimulationOverrides | undefined; let txRequest: TxExecutionRequest; if (useOverride) { - const accountOverrides = await this.buildAccountOverrides(this.scopesFrom(from, opts.additionalScopes)); + const accountOverrides = await this.buildAccountOverrides(scopes); overrides = new SimulationOverrides(accountOverrides); } @@ -259,8 +283,7 @@ export class TestWallet extends BaseWallet { let fromAccount: Account; if (useOverride) { const originalAccount = await this.getAccountFromAddress(from); - const completeAddress = originalAccount.getCompleteAddress(); - fromAccount = createStubAccount(completeAddress); + fromAccount = this.getStubAccountFor(from, originalAccount.getCompleteAddress()); } else { fromAccount = await this.getAccountFromAddress(from); } diff --git a/yarn-project/pxe/src/contract_function_simulator/proxied_contract_data_source.ts b/yarn-project/pxe/src/contract_function_simulator/proxied_contract_data_source.ts index d825ef65c730..f3edf78718f8 100644 --- a/yarn-project/pxe/src/contract_function_simulator/proxied_contract_data_source.ts +++ b/yarn-project/pxe/src/contract_function_simulator/proxied_contract_data_source.ts @@ -29,6 +29,7 @@ export class ProxiedContractStoreFactory { } instance.currentContractClassId = realInstance.currentContractClassId; instance.originalContractClassId = realInstance.originalContractClassId; + instance.initializationHash = realInstance.initializationHash; return instance; } else { return target.getContractInstance(address); @@ -47,6 +48,9 @@ export class ProxiedContractStoreFactory { return fn; } } + throw new Error( + `Function with selector ${selector} not found in stub artifact for overridden contract at ${contractAddress}. The stub does not implement this function.`, + ); } else { return target.getFunctionArtifact(contractAddress, selector); } @@ -64,6 +68,9 @@ export class ProxiedContractStoreFactory { return fn; } } + throw new Error( + `Function with selector ${selector} not found in stub artifact for overridden contract at ${contractAddress}. The stub does not implement this function.`, + ); } else { return target.getFunctionArtifactWithDebugMetadata(contractAddress, selector); } @@ -78,6 +85,6 @@ export class ProxiedContractStoreFactory { } } }, - }); + }) satisfies ContractStore; } } 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 3008f3fd0b86..bf7422b9fb2f 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts @@ -87,8 +87,6 @@ export type SimulateViaEntrypointOptions = Pick< > & { /** Fee options for the entrypoint */ feeOptions: FeeOptions; - /** Scopes to use for the simulation */ - scopes: AztecAddress[]; }; /** Options for `completeFeeOptions`. */ @@ -319,7 +317,7 @@ export abstract class BaseWallet implements Wallet { simulatePublic: true, skipTxValidation: opts.skipTxValidation, skipFeeEnforcement: opts.skipFeeEnforcement, - scopes: opts.scopes, + scopes: this.scopesFrom(opts.from, opts.additionalScopes), }); const appCallOffset = await this.computeAppCallOffset(opts.from, opts.feeOptions); return TxSimulationResultWithAppOffset.fromResultAndOffset(result, appCallOffset); @@ -388,7 +386,7 @@ export abstract class BaseWallet implements Wallet { ? this.simulateViaEntrypoint(remainingPayload, { from: opts.from, feeOptions, - scopes: this.scopesFrom(opts.from, opts.additionalScopes), + additionalScopes: opts.additionalScopes, skipTxValidation: opts.skipTxValidation, skipFeeEnforcement: opts.skipFeeEnforcement ?? true, }) diff --git a/yarn-project/wallets/src/embedded/account-contract-providers/bundle.ts b/yarn-project/wallets/src/embedded/account-contract-providers/bundle.ts index 8e4a49f12e9b..c854dd077e97 100644 --- a/yarn-project/wallets/src/embedded/account-contract-providers/bundle.ts +++ b/yarn-project/wallets/src/embedded/account-contract-providers/bundle.ts @@ -1,12 +1,14 @@ import { EcdsaKAccountContract, EcdsaRAccountContract } from '@aztec/accounts/ecdsa'; import { SchnorrAccountContract } from '@aztec/accounts/schnorr'; -import { StubAccountContractArtifact, createStubAccount } from '@aztec/accounts/stub'; +import { StubEcdsaAccountContractArtifact, createStubEcdsaAccount } from '@aztec/accounts/stub/ecdsa'; +import { StubSchnorrAccountContractArtifact, createStubSchnorrAccount } from '@aztec/accounts/stub/schnorr'; import type { Account, AccountContract } from '@aztec/aztec.js/account'; import type { Fq } from '@aztec/foundation/curves/bn254'; import { getCanonicalMultiCallEntrypoint } from '@aztec/protocol-contracts/multi-call-entrypoint'; import type { ContractArtifact } from '@aztec/stdlib/abi'; import type { CompleteAddress, ContractInstanceWithAddress } from '@aztec/stdlib/contract'; +import type { AccountType } from '../wallet_db.js'; import type { AccountContractsProvider } from './types.js'; /** @@ -26,12 +28,12 @@ export class BundleAccountContractsProvider implements AccountContractsProvider return Promise.resolve(new EcdsaKAccountContract(signingKey)); } - getStubAccountContractArtifact(): Promise { - return Promise.resolve(StubAccountContractArtifact); + getStubAccountContractArtifact(type: AccountType): Promise { + return Promise.resolve(type === 'schnorr' ? StubSchnorrAccountContractArtifact : StubEcdsaAccountContractArtifact); } - createStubAccount(address: CompleteAddress): Promise { - return Promise.resolve(createStubAccount(address)); + createStubAccount(address: CompleteAddress, type: AccountType): Promise { + return Promise.resolve(type === 'schnorr' ? createStubSchnorrAccount(address) : createStubEcdsaAccount(address)); } getMulticallContract(): Promise<{ instance: ContractInstanceWithAddress; artifact: ContractArtifact }> { diff --git a/yarn-project/wallets/src/embedded/account-contract-providers/lazy.ts b/yarn-project/wallets/src/embedded/account-contract-providers/lazy.ts index 1da707e921f6..501212d7f7b5 100644 --- a/yarn-project/wallets/src/embedded/account-contract-providers/lazy.ts +++ b/yarn-project/wallets/src/embedded/account-contract-providers/lazy.ts @@ -4,6 +4,7 @@ import { getCanonicalMultiCallEntrypoint } from '@aztec/protocol-contracts/multi import type { ContractArtifact } from '@aztec/stdlib/abi'; import type { CompleteAddress, ContractInstanceWithAddress } from '@aztec/stdlib/contract'; +import type { AccountType } from '../wallet_db.js'; import type { AccountContractsProvider } from './types.js'; /** @@ -26,14 +27,24 @@ export class LazyAccountContractsProvider implements AccountContractsProvider { return new EcdsaKAccountContract(signingKey); } - async getStubAccountContractArtifact(): Promise { - const { getStubAccountContractArtifact } = await import('@aztec/accounts/stub/lazy'); - return getStubAccountContractArtifact(); + async getStubAccountContractArtifact(type: AccountType): Promise { + if (type === 'schnorr') { + const { getStubSchnorrAccountContractArtifact } = await import('@aztec/accounts/stub/schnorr/lazy'); + return getStubSchnorrAccountContractArtifact(); + } else { + const { getStubEcdsaAccountContractArtifact } = await import('@aztec/accounts/stub/ecdsa/lazy'); + return getStubEcdsaAccountContractArtifact(); + } } - async createStubAccount(address: CompleteAddress): Promise { - const { createStubAccount } = await import('@aztec/accounts/stub/lazy'); - return createStubAccount(address); + async createStubAccount(address: CompleteAddress, type: AccountType): Promise { + if (type === 'schnorr') { + const { createStubSchnorrAccount } = await import('@aztec/accounts/stub/schnorr/lazy'); + return createStubSchnorrAccount(address); + } else { + const { createStubEcdsaAccount } = await import('@aztec/accounts/stub/ecdsa/lazy'); + return createStubEcdsaAccount(address); + } } getMulticallContract(): Promise<{ instance: ContractInstanceWithAddress; artifact: ContractArtifact }> { diff --git a/yarn-project/wallets/src/embedded/account-contract-providers/types.ts b/yarn-project/wallets/src/embedded/account-contract-providers/types.ts index a99b5c504268..62441a21ce1d 100644 --- a/yarn-project/wallets/src/embedded/account-contract-providers/types.ts +++ b/yarn-project/wallets/src/embedded/account-contract-providers/types.ts @@ -3,6 +3,8 @@ import type { Fq } from '@aztec/foundation/curves/bn254'; import type { ContractArtifact } from '@aztec/stdlib/abi'; import type { CompleteAddress, ContractInstanceWithAddress } from '@aztec/stdlib/contract'; +import type { AccountType } from '../wallet_db.js'; + /** * Provides account contract implementations and stub accounts for the EmbeddedWallet. * Two implementations exist: @@ -13,7 +15,7 @@ export interface AccountContractsProvider { getSchnorrAccountContract(signingKey: Fq): Promise; getEcdsaRAccountContract(signingKey: Buffer): Promise; getEcdsaKAccountContract(signingKey: Buffer): Promise; - getStubAccountContractArtifact(): Promise; + getStubAccountContractArtifact(type: AccountType): Promise; getMulticallContract(): Promise<{ instance: ContractInstanceWithAddress; artifact: ContractArtifact }>; - createStubAccount(address: CompleteAddress): Promise; + createStubAccount(address: CompleteAddress, type: AccountType): Promise; } diff --git a/yarn-project/wallets/src/embedded/embedded_wallet.ts b/yarn-project/wallets/src/embedded/embedded_wallet.ts index e5cbef12e2c2..10914e0915f7 100644 --- a/yarn-project/wallets/src/embedded/embedded_wallet.ts +++ b/yarn-project/wallets/src/embedded/embedded_wallet.ts @@ -130,7 +130,7 @@ export class EmbeddedWallet extends BaseWallet { const simulationResult = await this.simulateViaEntrypoint(executionPayload, { from: opts.from, feeOptions, - scopes: this.scopesFrom(opts.from, opts.additionalScopes), + additionalScopes: opts.additionalScopes, skipTxValidation: true, }); @@ -184,17 +184,19 @@ export class EmbeddedWallet extends BaseWallet { /** * Builds contract overrides for all provided addresses by replacing their account contracts with stub implementations. + * Uses a type-specific stub artifact so that the stub's constructor selector matches the real account's constructor. */ protected async buildAccountOverrides(addresses: AztecAddress[]): Promise { const accounts = await this.getAccounts(); const contracts: ContractOverrides = {}; - const stubArtifact = await this.accountContracts.getStubAccountContractArtifact(); - const filtered = accounts.filter(acc => addresses.some(addr => addr.equals(acc.item))); for (const account of filtered) { const address = account.item; + const { type } = await this.walletDB.retrieveAccount(address); + const stubArtifact = await this.accountContracts.getStubAccountContractArtifact(type); + const originalAccount = await this.getAccountFromAddress(address); const completeAddress = originalAccount.getCompleteAddress(); const contractInstance = await this.pxe.getContractInstance(completeAddress.address); @@ -204,8 +206,10 @@ export class EmbeddedWallet extends BaseWallet { ); } + const stubConstructorArgs = type === 'schnorr' ? [Fr.ZERO, Fr.ZERO] : [Buffer.alloc(32), Buffer.alloc(32)]; const stubInstance = await getContractInstanceFromInstantiationParams(stubArtifact, { salt: Fr.random(), + constructorArgs: stubConstructorArgs, }); contracts[address.toString()] = { @@ -226,7 +230,8 @@ export class EmbeddedWallet extends BaseWallet { executionPayload: ExecutionPayload, opts: SimulateViaEntrypointOptions, ): Promise { - const { from, feeOptions, scopes, skipTxValidation, skipFeeEnforcement } = opts; + const { from, feeOptions, additionalScopes, skipTxValidation, skipFeeEnforcement } = opts; + const scopes = this.scopesFrom(from, additionalScopes); const feeExecutionPayload = await feeOptions.walletFeePaymentMethod?.getExecutionPayload(); const finalExecutionPayload = feeExecutionPayload @@ -234,7 +239,7 @@ export class EmbeddedWallet extends BaseWallet { : executionPayload; const chainInfo = await this.getChainInfo(); - const accountOverrides = await this.buildAccountOverrides(this.scopesFrom(from, opts.additionalScopes)); + const accountOverrides = await this.buildAccountOverrides(scopes); const overrides = new SimulationOverrides(accountOverrides); let txRequest: TxExecutionRequest; @@ -242,9 +247,10 @@ export class EmbeddedWallet extends BaseWallet { const entrypoint = new DefaultEntrypoint(); txRequest = await entrypoint.createTxExecutionRequest(finalExecutionPayload, feeOptions.gasSettings, chainInfo); } else { + const { type } = await this.walletDB.retrieveAccount(from); const originalAccount = await this.getAccountFromAddress(from); const completeAddress = originalAccount.getCompleteAddress(); - const account = await this.accountContracts.createStubAccount(completeAddress); + const account = await this.accountContracts.createStubAccount(completeAddress, type); const executionOptions: DefaultAccountEntrypointOptions = { txNonce: Fr.random(), cancellable: this.cancellableTransactions,