diff --git a/boxes/boxes/react/src/config.ts b/boxes/boxes/react/src/config.ts index b0fcd398f1ee..89a2a9e7050d 100644 --- a/boxes/boxes/react/src/config.ts +++ b/boxes/boxes/react/src/config.ts @@ -21,7 +21,7 @@ export class PrivateEnv { ); } - await wallet.createSchnorrAccount(accountData.secret, accountData.salt, accountData.signingKey); + await wallet.createSchnorrInitializerlessAccount(accountData.secret, accountData.salt, accountData.signingKey); this.wallet = wallet; this.defaultAccountAddress = accountData.address; } diff --git a/boxes/boxes/vite/src/config.ts b/boxes/boxes/vite/src/config.ts index a5dfc2a4cdee..4b28c6dad8d4 100644 --- a/boxes/boxes/vite/src/config.ts +++ b/boxes/boxes/vite/src/config.ts @@ -1,4 +1,4 @@ -import { getInitialTestAccountsData } from "@aztec/accounts/testing"; +import { getInitialTestAccountsData } from '@aztec/accounts/testing'; import { AztecAddress } from '@aztec/aztec.js/addresses'; import { Wallet } from '@aztec/aztec.js/wallet'; import { EmbeddedWallet } from '@aztec/wallets/embedded'; @@ -10,22 +10,18 @@ export class PrivateEnv { constructor() {} async init() { - const nodeURL = process.env.AZTEC_NODE_URL ?? "http://localhost:8080"; + const nodeURL = process.env.AZTEC_NODE_URL ?? 'http://localhost:8080'; const wallet = await EmbeddedWallet.create(nodeURL); const [accountData] = await getInitialTestAccountsData(); if (!accountData) { console.error( - "Account not found. Please connect the app to a testing environment with deployed and funded test accounts.", + 'Account not found. Please connect the app to a testing environment with deployed and funded test accounts.', ); } - await wallet.createSchnorrAccount( - accountData.secret, - accountData.salt, - accountData.signingKey, - ); + await wallet.createSchnorrInitializerlessAccount(accountData.secret, accountData.salt, accountData.signingKey); this.wallet = wallet; this.defaultAccountAddress = accountData.address; } diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index 3410cc18587f..2da21d8aac75 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,26 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### [Aztec.js] Prefunded local network test accounts are now initializerless + +The genesis-funded test accounts in the local network (sandbox), returned by `getInitialTestAccountsData()`, are now initializerless Schnorr accounts (`schnorr_initializerless`). An initializerless account has no onchain deployment transaction: its address commits to the signing public key (through `immutables_hash`) and its contract state is materialized locally in the PXE, so these accounts are usable right away. + +Because their address is derived differently from a regular Schnorr account, register them with `createSchnorrInitializerlessAccount` rather than `createSchnorrAccount`. + +**Migration:** + +```diff +const [alice] = await getInitialTestAccountsData(); +- await wallet.createSchnorrAccount(alice.secret, alice.salt); ++ await wallet.createSchnorrInitializerlessAccount(alice.secret, alice.salt); +``` + +### [Aztec.js] `AccountContract` interface adds `getImmutablesHash()` + +The `AccountContract` interface now declares `getImmutablesHash(): Promise`, which returns the hash of the account's immutable instantiation parameters committed into its address, or `undefined` if the feature is not used. `getAccountContractAddress()` and `AccountManager.create()` call it to derive the address when an `immutablesHash` is not passed explicitly, so the address of an initializerless account is now resolved from the contract itself. + +Account contracts that extend `DefaultAccountContract` inherit a default implementation that returns `undefined` and need no changes. + ### [Aztec.nr] `messages::message_delivery` module moved to `messages::delivery` The `message_delivery` module has been renamed to `delivery`. Update imports accordingly: @@ -100,7 +120,7 @@ After the `auth_registry`, `public_checks`, and `multi_call_entrypoint` demotion ### [Aztec.nr] `multi_call_entrypoint` demoted from protocol contract -`multi_call_entrypoint` is no longer a protocol contract; its address is derived from its artifact rather than hardcoded at `6`, and PXE no longer auto-registers it. It is now a standard contract that PXE *preloads*: both `createPXE` and `EmbeddedWallet` preload the standard MultiCallEntrypoint automatically (and `EmbeddedWallet` additionally preloads `AuthRegistry`). **If you use the standard PXE or `EmbeddedWallet`, no changes are needed** — multicall keeps working out of the box. +`multi_call_entrypoint` is no longer a protocol contract; its address is derived from its artifact rather than hardcoded at `6`, and PXE no longer auto-registers it. It is now a standard contract that PXE _preloads_: both `createPXE` and `EmbeddedWallet` preload the standard MultiCallEntrypoint automatically (and `EmbeddedWallet` additionally preloads `AuthRegistry`). **If you use the standard PXE or `EmbeddedWallet`, no changes are needed** — multicall keeps working out of the box. To preload a different set of standard contracts (for example to also preload `PublicChecks`, which is not preloaded by default), a wallet or app passes its own `preloadedContractsProvider` through the wallet's PXE options: @@ -121,7 +141,7 @@ const wallet = await EmbeddedWallet.create(node, { }); ``` -The provider *replaces* the default list (it is not additive), so include every standard contract you want available. +The provider _replaces_ the default list (it is not additive), so include every standard contract you want available. ### [Aztec.nr] `public_checks` demoted from protocol contract diff --git a/docs/docs-words.txt b/docs/docs-words.txt index 9b08f9f45c73..13553bce3d78 100644 --- a/docs/docs-words.txt +++ b/docs/docs-words.txt @@ -63,6 +63,7 @@ colour Colour CapsuleStore initializerless +immutables colums Commiting comsumer diff --git a/docs/examples/ts/aave_bridge/index.ts b/docs/examples/ts/aave_bridge/index.ts index 2d7a46053cfb..07e3a94eaca7 100644 --- a/docs/examples/ts/aave_bridge/index.ts +++ b/docs/examples/ts/aave_bridge/index.ts @@ -36,7 +36,7 @@ const node = createAztecNodeClient( await waitForNode(node); const aztecWallet = await EmbeddedWallet.create(node, { ephemeral: true }); const [accData] = await getInitialTestAccountsData(); -const account = await aztecWallet.createSchnorrAccount( +const account = await aztecWallet.createSchnorrInitializerlessAccount( accData.secret, accData.salt, accData.signingKey, @@ -279,7 +279,10 @@ console.log("Block proven!\n"); // Compute the membership witness using the message hash and the L2 tx hash. // The node picks the smallest partial-proof root that covers the tx's checkpoint. -const witness = await node.getL2ToL1MembershipWitness(exitReceipt.txHash, msgLeaf); +const witness = await node.getL2ToL1MembershipWitness( + exitReceipt.txHash, + msgLeaf, +); const epoch = witness!.epochNumber; const numCheckpointsInEpoch = witness!.numCheckpointsInEpoch; diff --git a/docs/examples/ts/aztecjs_advanced/index.ts b/docs/examples/ts/aztecjs_advanced/index.ts index fbcdcdb029db..40109b6ed676 100644 --- a/docs/examples/ts/aztecjs_advanced/index.ts +++ b/docs/examples/ts/aztecjs_advanced/index.ts @@ -26,7 +26,7 @@ const testAccounts = await getInitialTestAccountsData(); const [aliceAddress, bobAddress] = await Promise.all( testAccounts.slice(0, 2).map(async (account) => { return ( - await wallet.createSchnorrAccount( + await wallet.createSchnorrInitializerlessAccount( account.secret, account.salt, account.signingKey, diff --git a/docs/examples/ts/aztecjs_authwit/index.ts b/docs/examples/ts/aztecjs_authwit/index.ts index 565333a38b6d..4d7c06294539 100644 --- a/docs/examples/ts/aztecjs_authwit/index.ts +++ b/docs/examples/ts/aztecjs_authwit/index.ts @@ -16,7 +16,7 @@ const testAccounts = await getInitialTestAccountsData(); const [aliceAddress, bobAddress] = await Promise.all( testAccounts.slice(0, 2).map(async (account) => { return ( - await wallet.createSchnorrAccount( + await wallet.createSchnorrInitializerlessAccount( account.secret, account.salt, account.signingKey, diff --git a/docs/examples/ts/aztecjs_connection/index.ts b/docs/examples/ts/aztecjs_connection/index.ts index 4f2aeb2add75..4d4dca73efdf 100644 --- a/docs/examples/ts/aztecjs_connection/index.ts +++ b/docs/examples/ts/aztecjs_connection/index.ts @@ -24,7 +24,7 @@ const testAccounts = await getInitialTestAccountsData(); const [aliceAddress, bobAddress] = await Promise.all( testAccounts.slice(0, 2).map(async (account) => { return ( - await wallet.createSchnorrAccount( + await wallet.createSchnorrInitializerlessAccount( account.secret, account.salt, account.signingKey, @@ -158,7 +158,10 @@ import { FeeJuicePaymentMethodWithClaim } from "@aztec/aztec.js/fee"; // claim is from the bridgeTokensPublic step above // Create a payment method that claims the bridged Fee Juice and uses it to pay -const bridgePaymentMethod = new FeeJuicePaymentMethodWithClaim(feeJuiceAccount.address, claim); +const bridgePaymentMethod = new FeeJuicePaymentMethodWithClaim( + feeJuiceAccount.address, + claim, +); // Use it to pay for any transaction; here we deploy the account in one step const deployMethodBridged = await feeJuiceAccount.getDeployMethod(); @@ -175,5 +178,10 @@ const metadata = await wallet.getContractMetadata(newAccount.address); console.log("Account deployed:", metadata.initializationStatus); // docs:end:verify_account_deployment -const feeJuiceMetadata = await wallet.getContractMetadata(feeJuiceAccount.address); -console.log("Fee Juice account deployed:", feeJuiceMetadata.initializationStatus); +const feeJuiceMetadata = await wallet.getContractMetadata( + feeJuiceAccount.address, +); +console.log( + "Fee Juice account deployed:", + feeJuiceMetadata.initializationStatus, +); diff --git a/docs/examples/ts/aztecjs_getting_started/index.ts b/docs/examples/ts/aztecjs_getting_started/index.ts index 2338115e1e4b..db238d59d1e3 100644 --- a/docs/examples/ts/aztecjs_getting_started/index.ts +++ b/docs/examples/ts/aztecjs_getting_started/index.ts @@ -6,8 +6,8 @@ const nodeUrl = process.env.AZTEC_NODE_URL ?? "http://localhost:8080"; const wallet = await EmbeddedWallet.create(nodeUrl, { ephemeral: true }); const [alice, bob] = await getInitialTestAccountsData(); -await wallet.createSchnorrAccount(alice.secret, alice.salt); -await wallet.createSchnorrAccount(bob.secret, bob.salt); +await wallet.createSchnorrInitializerlessAccount(alice.secret, alice.salt); +await wallet.createSchnorrInitializerlessAccount(bob.secret, bob.salt); // docs:end:setup // docs:start:deploy diff --git a/docs/examples/ts/aztecjs_kernelless_simulation/index.ts b/docs/examples/ts/aztecjs_kernelless_simulation/index.ts index c23a6222ebb8..b9b15f05846b 100644 --- a/docs/examples/ts/aztecjs_kernelless_simulation/index.ts +++ b/docs/examples/ts/aztecjs_kernelless_simulation/index.ts @@ -20,7 +20,7 @@ const testAccounts = await getInitialTestAccountsData(); const [aliceAddress, bobAddress] = await Promise.all( testAccounts.slice(0, 2).map(async (account) => { return ( - await wallet.createSchnorrAccount( + await wallet.createSchnorrInitializerlessAccount( account.secret, account.salt, account.signingKey, diff --git a/docs/examples/ts/aztecjs_testing/index.ts b/docs/examples/ts/aztecjs_testing/index.ts index a7da6e351b20..07a77c47e164 100644 --- a/docs/examples/ts/aztecjs_testing/index.ts +++ b/docs/examples/ts/aztecjs_testing/index.ts @@ -16,13 +16,21 @@ let token: TokenContract; // beforeAll equivalent - setup async function setup() { - const node = createAztecNodeClient(process.env.AZTEC_NODE_URL ?? "http://localhost:8080"); + const node = createAztecNodeClient( + process.env.AZTEC_NODE_URL ?? "http://localhost:8080", + ); await waitForNode(node); wallet = await EmbeddedWallet.create(node, { ephemeral: true }); const testAccounts = await getInitialTestAccountsData(); [aliceAddress, bobAddress] = await Promise.all( testAccounts.slice(0, 2).map(async (account) => { - return (await wallet.createSchnorrAccount(account.secret, account.salt, account.signingKey)).address; + return ( + await wallet.createSchnorrInitializerlessAccount( + account.secret, + account.salt, + account.signingKey, + ) + ).address; }), ); @@ -61,7 +69,9 @@ async function testTransferTokens() { .send({ from: aliceAddress }); // Transfer to bob using public transfer - await token.methods.transfer_in_public(aliceAddress, bobAddress, 100n, 0n).send({ from: aliceAddress }); + await token.methods + .transfer_in_public(aliceAddress, bobAddress, 100n, 0n) + .send({ from: aliceAddress }); const { result: aliceBalance } = await token.methods .balance_of_public(aliceAddress) diff --git a/docs/examples/ts/bob_token_contract/index.ts b/docs/examples/ts/bob_token_contract/index.ts index 6ce79a32c124..7f929e090d71 100644 --- a/docs/examples/ts/bob_token_contract/index.ts +++ b/docs/examples/ts/bob_token_contract/index.ts @@ -58,18 +58,19 @@ async function main() { const [giggleWalletData, aliceWalletData, bobClinicWalletData] = await getInitialTestAccountsData(); - const giggleAccountManager = await wallet.createSchnorrAccount( + const giggleAccountManager = await wallet.createSchnorrInitializerlessAccount( giggleWalletData.secret, giggleWalletData.salt, ); - const aliceAccountManager = await wallet.createSchnorrAccount( + const aliceAccountManager = await wallet.createSchnorrInitializerlessAccount( aliceWalletData.secret, aliceWalletData.salt, ); - const bobClinicAccountManager = await wallet.createSchnorrAccount( - bobClinicWalletData.secret, - bobClinicWalletData.salt, - ); + const bobClinicAccountManager = + await wallet.createSchnorrInitializerlessAccount( + bobClinicWalletData.secret, + bobClinicWalletData.salt, + ); const giggleAddress = giggleAccountManager.address; const aliceAddress = aliceAccountManager.address; diff --git a/docs/examples/ts/example_swap/index.ts b/docs/examples/ts/example_swap/index.ts index 5a81e6308b88..5cc0838f523b 100644 --- a/docs/examples/ts/example_swap/index.ts +++ b/docs/examples/ts/example_swap/index.ts @@ -34,7 +34,7 @@ const node = createAztecNodeClient(nodeUrl); await waitForNode(node); const wallet = await EmbeddedWallet.create(node, { ephemeral: true }); const [accData] = await getInitialTestAccountsData(); -const account = await wallet.createSchnorrAccount( +const account = await wallet.createSchnorrInitializerlessAccount( accData.secret, accData.salt, accData.signingKey, @@ -445,7 +445,10 @@ const exitMsgLeaf = computeL2ToL1MessageHash({ // docs:start:consume_l1_messages_witnesses // The node picks the smallest partial-proof root that covers each tx's checkpoint. -const exitWitness = await node.getL2ToL1MembershipWitness(swapReceipt.txHash, exitMsgLeaf); +const exitWitness = await node.getL2ToL1MembershipWitness( + swapReceipt.txHash, + exitMsgLeaf, +); const exitSiblingPath = exitWitness!.siblingPath .toBufferArray() .map((buf: Buffer) => `0x${buf.toString("hex")}` as `0x${string}`); @@ -495,7 +498,10 @@ const swapMsgLeaf = computeL2ToL1MessageHash({ chainId: new Fr(foundry.id), }); -const swapWitness = await node.getL2ToL1MembershipWitness(swapReceipt.txHash, swapMsgLeaf); +const swapWitness = await node.getL2ToL1MembershipWitness( + swapReceipt.txHash, + swapMsgLeaf, +); const swapSiblingPath = swapWitness!.siblingPath .toBufferArray() .map((buf: Buffer) => `0x${buf.toString("hex")}` as `0x${string}`); diff --git a/docs/examples/ts/token_bridge/index.ts b/docs/examples/ts/token_bridge/index.ts index 4f9e4c1b4301..993a39498569 100644 --- a/docs/examples/ts/token_bridge/index.ts +++ b/docs/examples/ts/token_bridge/index.ts @@ -28,7 +28,7 @@ console.log("Setting up L2...\n"); const node = createAztecNodeClient("http://localhost:8080"); const aztecWallet = await EmbeddedWallet.create(node); const [accData] = await getInitialTestAccountsData(); -const account = await aztecWallet.createSchnorrAccount( +const account = await aztecWallet.createSchnorrInitializerlessAccount( accData.secret, accData.salt, ); @@ -304,7 +304,10 @@ console.log("Block proven!\n"); // Compute the membership witness using the message hash and the L2 tx hash. // The node picks the smallest partial-proof root that covers the tx's checkpoint. -const witness = await node.getL2ToL1MembershipWitness(exitReceipt.txHash, msgLeaf); +const witness = await node.getL2ToL1MembershipWitness( + exitReceipt.txHash, + msgLeaf, +); const epoch = witness!.epochNumber; const numCheckpointsInEpoch = witness!.numCheckpointsInEpoch; console.log(` Epoch for block ${exitReceipt.blockNumber}: ${epoch}`); diff --git a/noir-projects/noir-contracts/contracts/account/schnorr_initializerless_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/account/schnorr_initializerless_account_contract/src/main.nr index cf49229cf20b..49db1a6ec3d3 100644 --- a/noir-projects/noir-contracts/contracts/account/schnorr_initializerless_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/account/schnorr_initializerless_account_contract/src/main.nr @@ -21,25 +21,32 @@ unconstrained fn no_sync( #[aztec(AztecConfig::new().custom_sync_state(crate::no_sync))] pub contract SchnorrInitializerlessAccount { use aztec::{ - authwit::{account::AccountActions, entrypoint::app::AppPayload}, + authwit::{ + account::AccountActions, + auth::{compute_authwit_message_hash, compute_authwit_nullifier}, + entrypoint::app::AppPayload, + }, context::PrivateContext, macros::functions::{allow_phase_change, external, view}, - oracle::{auth_witness::get_auth_witness, capsules::{load, store}, get_contract_instance::get_contract_instance}, - protocol::{hash::{poseidon2_hash, poseidon2_hash_bytes}, traits::{Deserialize, Serialize}}, + oracle::{ + auth_witness::get_auth_witness, + capsules::{load, store}, + get_contract_instance::get_contract_instance, + get_nullifier_membership_witness::get_low_nullifier_membership_witness, + }, + protocol::{ + address::AztecAddress, + hash::{compute_siloed_nullifier, poseidon2_hash, poseidon2_hash_bytes}, + traits::{Deserialize, Serialize}, + }, }; - - #[derive(Eq, Serialize, Deserialize)] - pub struct PublicKey { - pub x: Field, - pub y: Field, - } + use std::embedded_curve_ops::EmbeddedCurvePoint; global PUB_KEY_SLOT: Field = comptime { poseidon2_hash_bytes("INITIALIZERLESS_ACCOUNT_PUB_KEY".as_bytes()) }; #[external("utility")] - unconstrained fn constructor(public_key: PublicKey) { - let serialized_pub_key = public_key.serialize(); - let expected = poseidon2_hash(public_key.serialize()); + unconstrained fn constructor(signing_pub_key_x: Field, signing_pub_key_y: Field) { + let expected = poseidon2_hash([signing_pub_key_x, signing_pub_key_y]); let instance = get_contract_instance(self.address); assert_eq( expected, @@ -47,7 +54,12 @@ pub contract SchnorrInitializerlessAccount { "Public key hash does not match the immutables hash, refusing to store", ); - store(self.address, PUB_KEY_SLOT, serialized_pub_key, self.address); + store( + self.address, + PUB_KEY_SLOT, + [signing_pub_key_x, signing_pub_key_y], + self.address, + ); } #[external("private")] @@ -68,7 +80,7 @@ pub contract SchnorrInitializerlessAccount { fn is_valid_impl(context: &mut PrivateContext, outer_hash: Field) -> bool { // Safety: The public key inside the capsule is checked to match the immutables_hash of this instance let public_key = unsafe { load(context.this_address(), PUB_KEY_SLOT, context.this_address()) } - .map(|data| PublicKey::deserialize(data)) + .map(|data| EmbeddedCurvePoint::deserialize(data)) .unwrap_or_else(|| panic( "Public key was not stored in the Private eXecution Environment. Please call `constructor` first", )); @@ -86,8 +98,46 @@ pub contract SchnorrInitializerlessAccount { std::embedded_curve_ops::EmbeddedCurveScalar::new(limbs[2], limbs[3]), ); + schnorr::verify_signature(public_key, signature, outer_hash) + } + + /// @notice Helper function to check validity of private authwitnesses + /// @param consumer The address of the consumer of the message + /// @param message_hash The message hash of the message to check the validity + /// @return True if the message_hash can be consumed, false otherwise + #[external("utility")] + unconstrained fn lookup_validity(consumer: AztecAddress, inner_hash: Field) -> bool { + // Safety: The public key inside the capsule is checked to match the immutables_hash of this instance + let public_key = load(self.address, PUB_KEY_SLOT, self.address) + .map(|data| EmbeddedCurvePoint::deserialize(data)) + .unwrap_or_else(|| panic( + "Public key was not stored in the Private eXecution Environment. Please call `constructor` first", + )); + + let message_hash = compute_authwit_message_hash( + consumer, + self.context.chain_id(), + self.context.version(), + inner_hash, + ); + + let limbs: [Field; 4] = get_auth_witness(message_hash); + let signature = ( + std::embedded_curve_ops::EmbeddedCurveScalar::new(limbs[0], limbs[1]), + std::embedded_curve_ops::EmbeddedCurveScalar::new(limbs[2], limbs[3]), + ); let pub_key = std::embedded_curve_ops::EmbeddedCurvePoint { x: public_key.x, y: public_key.y }; + let valid_in_private = schnorr::verify_signature(pub_key, signature, message_hash); + + // Compute the nullifier and check if it is spent + // This will BLINDLY TRUST the oracle, but the oracle is us, and + // it is not as part of execution of the contract, so we are good. + let nullifier = compute_authwit_nullifier(self.address, inner_hash); + let siloed_nullifier = compute_siloed_nullifier(consumer, nullifier); + let (low_leaf_preimage, _witness) = + get_low_nullifier_membership_witness(self.context.block_header(), siloed_nullifier); + let is_spent = low_leaf_preimage.nullifier == siloed_nullifier; - schnorr::verify_signature(pub_key, signature, outer_hash) + !is_spent & valid_in_private } } diff --git a/yarn-project/accounts/src/defaults/account_contract.ts b/yarn-project/accounts/src/defaults/account_contract.ts index c4c6a10b857b..f97928166d1e 100644 --- a/yarn-project/accounts/src/defaults/account_contract.ts +++ b/yarn-project/accounts/src/defaults/account_contract.ts @@ -1,5 +1,6 @@ import { type Account, type AccountContract, type AuthWitnessProvider, BaseAccount } from '@aztec/aztec.js/account'; import { DefaultAccountEntrypoint } from '@aztec/entrypoints/account'; +import type { Fr } from '@aztec/foundation/curves/bn254'; import type { ContractArtifact } from '@aztec/stdlib/abi'; import type { CompleteAddress } from '@aztec/stdlib/contract'; @@ -22,6 +23,14 @@ export abstract class DefaultAccountContract implements AccountContract { constructor() {} + /** + * Accounts without immutables (the default) contribute to their address via an on-chain + * initializer instead. Account contracts that commit immutables into their address override this. + */ + getImmutablesHash(): Promise { + return Promise.resolve(undefined); + } + getAccount(completeAddress: CompleteAddress): Account { const authWitnessProvider = this.getAuthWitnessProvider(completeAddress); return new BaseAccount( diff --git a/yarn-project/accounts/src/schnorr/account_contract.ts b/yarn-project/accounts/src/schnorr/account_contract.ts index 45fbf9658489..8e7a770ade9c 100644 --- a/yarn-project/accounts/src/schnorr/account_contract.ts +++ b/yarn-project/accounts/src/schnorr/account_contract.ts @@ -14,12 +14,25 @@ import { DefaultAccountContract } from '../defaults/account_contract.js'; * can be implemented with or without lazy loading. */ export abstract class SchnorrBaseAccountContract extends DefaultAccountContract { - constructor(private signingPrivateKey: GrumpkinScalar) { + constructor(protected signingPrivateKey: GrumpkinScalar) { super(); } - async getInitializationFunctionAndArgs() { - const signingPublicKey = await new Schnorr().computePublicKey(this.signingPrivateKey); + /** The Grumpkin public key this account verifies signatures against. */ + getSigningPublicKey() { + return new Schnorr().computePublicKey(this.signingPrivateKey); + } + + async getInitializationFunctionAndArgs(): Promise< + | { + /** The name of the function used to initialize the contract */ + constructorName: string; + /** The args to the function used to initialize the contract */ + constructorArgs: any[]; + } + | undefined + > { + const signingPublicKey = await this.getSigningPublicKey(); return { constructorName: 'constructor', constructorArgs: [signingPublicKey.x, signingPublicKey.y] }; } diff --git a/yarn-project/accounts/src/schnorr/initializerless/index.ts b/yarn-project/accounts/src/schnorr/initializerless/index.ts index e9d467160a8c..2c1df1e3e24d 100644 --- a/yarn-project/accounts/src/schnorr/initializerless/index.ts +++ b/yarn-project/accounts/src/schnorr/initializerless/index.ts @@ -6,6 +6,7 @@ */ import { getAccountContractAddress } from '@aztec/aztec.js/account'; import type { AztecAddress } from '@aztec/aztec.js/addresses'; +import { poseidon2Hash } from '@aztec/foundation/crypto/poseidon'; import { Fr } from '@aztec/foundation/curves/bn254'; import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import type { ContractArtifact } from '@aztec/stdlib/abi'; @@ -30,6 +31,15 @@ export class SchnorrInitializerlessAccountContract extends SchnorrBaseAccountCon super(signingPrivateKey); } + override getInitializationFunctionAndArgs() { + return Promise.resolve(undefined); + } + + override async getImmutablesHash(): Promise { + const signingPublicKey = await this.getSigningPublicKey(); + return poseidon2Hash([signingPublicKey.x, signingPublicKey.y]); + } + override getContractArtifact(): Promise { return Promise.resolve(SchnorrInitializerlessAccountContractArtifact); } diff --git a/yarn-project/accounts/src/schnorr/initializerless/lazy.ts b/yarn-project/accounts/src/schnorr/initializerless/lazy.ts index 2627078dc230..f8c8cbdfba91 100644 --- a/yarn-project/accounts/src/schnorr/initializerless/lazy.ts +++ b/yarn-project/accounts/src/schnorr/initializerless/lazy.ts @@ -5,6 +5,7 @@ * @packageDocumentation */ import { getAccountContractAddress } from '@aztec/aztec.js/account'; +import { poseidon2Hash } from '@aztec/foundation/crypto/poseidon'; import { Fr } from '@aztec/foundation/curves/bn254'; import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import type { ContractArtifact } from '@aztec/stdlib/abi'; @@ -38,6 +39,15 @@ export class SchnorrInitializerlessAccountContract extends SchnorrBaseAccountCon super(signingPrivateKey); } + override getInitializationFunctionAndArgs() { + return Promise.resolve(undefined); + } + + override async getImmutablesHash(): Promise { + const signingPublicKey = await this.getSigningPublicKey(); + return poseidon2Hash([signingPublicKey.x, signingPublicKey.y]); + } + override getContractArtifact(): Promise { return getSchnorrInitializerlessAccountContractArtifact(); } diff --git a/yarn-project/accounts/src/testing/configuration.ts b/yarn-project/accounts/src/testing/configuration.ts index 731332bccec7..0255e9861b4e 100644 --- a/yarn-project/accounts/src/testing/configuration.ts +++ b/yarn-project/accounts/src/testing/configuration.ts @@ -17,6 +17,11 @@ export const INITIAL_TEST_SIGNING_KEYS = INITIAL_TEST_ENCRYPTION_KEYS; export const INITIAL_TEST_ACCOUNT_SALTS = [Fr.ZERO, Fr.ZERO, Fr.ZERO]; +/** + * The schnorr account contract variant a test account uses. + */ +export type InitialAccountType = 'schnorr' | 'schnorr_initializerless'; + /** * Data for generating an initial account. */ @@ -37,4 +42,8 @@ export interface InitialAccountData { * Address of the schnorr account contract. */ address: AztecAddress; + /** + * Account contract variant. + */ + type?: InitialAccountType; } diff --git a/yarn-project/accounts/src/testing/index.ts b/yarn-project/accounts/src/testing/index.ts index 549e99311797..d74871b74869 100644 --- a/yarn-project/accounts/src/testing/index.ts +++ b/yarn-project/accounts/src/testing/index.ts @@ -4,8 +4,10 @@ * @packageDocumentation */ import { Fr } from '@aztec/aztec.js/fields'; +import type { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import { deriveSigningKey } from '@aztec/stdlib/keys'; +import { getSchnorrInitializerlessAccountContractAddress } from '../schnorr/initializerless/index.js'; import { getSchnorrAccountContractAddress } from '../schnorr/private_immutable/index.js'; import { INITIAL_TEST_ACCOUNT_SALTS, @@ -13,6 +15,7 @@ import { INITIAL_TEST_SECRET_KEYS, INITIAL_TEST_SIGNING_KEYS, type InitialAccountData, + type InitialAccountType, } from './configuration.js'; export { @@ -21,8 +24,16 @@ export { INITIAL_TEST_SECRET_KEYS, INITIAL_TEST_SIGNING_KEYS, type InitialAccountData, + type InitialAccountType, } from './configuration.js'; +/** Derives the account contract address for the given type */ +function getTestAccountAddress(type: InitialAccountType, secret: Fr, salt: Fr, signingKey?: GrumpkinScalar) { + return type === 'schnorr' + ? getSchnorrAccountContractAddress(secret, salt, signingKey) + : getSchnorrInitializerlessAccountContractAddress(secret, salt, signingKey); +} + /** * Gets the basic information for initial test accounts. */ @@ -32,7 +43,8 @@ export function getInitialTestAccountsData(): Promise { secret, signingKey: INITIAL_TEST_ENCRYPTION_KEYS[i], salt: INITIAL_TEST_ACCOUNT_SALTS[i], - address: await getSchnorrAccountContractAddress( + type: 'schnorr_initializerless' as const, + address: await getSchnorrInitializerlessAccountContractAddress( secret, INITIAL_TEST_ACCOUNT_SALTS[i], INITIAL_TEST_SIGNING_KEYS[i], @@ -42,9 +54,12 @@ export function getInitialTestAccountsData(): Promise { } /** - * Generate a fixed amount of random schnorr account contract instance. + * Generate a fixed amount of random schnorr account contract instances of the given type */ -export async function generateSchnorrAccounts(numberOfAccounts: number): Promise> { +export async function generateSchnorrAccounts( + numberOfAccounts: number, + type: InitialAccountType = 'schnorr_initializerless', +): Promise { const secrets = Array.from({ length: numberOfAccounts }, () => Fr.random()); return await Promise.all( secrets.map(async secret => { @@ -53,7 +68,8 @@ export async function generateSchnorrAccounts(numberOfAccounts: number): Promise secret, signingKey: deriveSigningKey(secret), salt, - address: await getSchnorrAccountContractAddress(secret, salt), + type, + address: await getTestAccountAddress(type, secret, salt), }; }), ); diff --git a/yarn-project/accounts/src/testing/lazy.ts b/yarn-project/accounts/src/testing/lazy.ts index 94a27eb6fc1b..6884ac5c736b 100644 --- a/yarn-project/accounts/src/testing/lazy.ts +++ b/yarn-project/accounts/src/testing/lazy.ts @@ -4,8 +4,10 @@ * @packageDocumentation */ import { Fr } from '@aztec/aztec.js/fields'; +import type { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import { deriveSigningKey } from '@aztec/stdlib/keys'; +import { getSchnorrInitializerlessAccountContractAddress } from '../schnorr/initializerless/lazy.js'; import { getSchnorrAccountContractAddress } from '../schnorr/private_immutable/lazy.js'; import { INITIAL_TEST_ACCOUNT_SALTS, @@ -13,10 +15,18 @@ import { INITIAL_TEST_SECRET_KEYS, INITIAL_TEST_SIGNING_KEYS, type InitialAccountData, + type InitialAccountType, } from './configuration.js'; export { INITIAL_TEST_ACCOUNT_SALTS, INITIAL_TEST_SECRET_KEYS } from './configuration.js'; +/** Derives the account contract address for the given type */ +function getTestAccountAddress(type: InitialAccountType, secret: Fr, salt: Fr, signingKey?: GrumpkinScalar) { + return type === 'schnorr' + ? getSchnorrAccountContractAddress(secret, salt, signingKey) + : getSchnorrInitializerlessAccountContractAddress(secret, salt, signingKey); +} + /** * Gets the basic information for initial test accounts. */ @@ -26,7 +36,8 @@ export function getInitialTestAccountsData(): Promise { secret, signingKey: INITIAL_TEST_ENCRYPTION_KEYS[i], salt: INITIAL_TEST_ACCOUNT_SALTS[i], - address: await getSchnorrAccountContractAddress( + type: 'schnorr_initializerless' as const, + address: await getSchnorrInitializerlessAccountContractAddress( secret, INITIAL_TEST_ACCOUNT_SALTS[i], INITIAL_TEST_SIGNING_KEYS[i], @@ -36,9 +47,12 @@ export function getInitialTestAccountsData(): Promise { } /** - * Generate a fixed amount of random schnorr account contract instance. + * Generate a fixed amount of random schnorr account contract instances of the given type */ -export async function generateSchnorrAccounts(numberOfAccounts: number): Promise { +export async function generateSchnorrAccounts( + numberOfAccounts: number, + type: InitialAccountType = 'schnorr_initializerless', +): Promise { const secrets = Array.from({ length: numberOfAccounts }, () => Fr.random()); return await Promise.all( secrets.map(async secret => { @@ -47,7 +61,8 @@ export async function generateSchnorrAccounts(numberOfAccounts: number): Promise secret, signingKey: deriveSigningKey(secret), salt, - address: await getSchnorrAccountContractAddress(secret, salt), + type, + address: await getTestAccountAddress(type, secret, salt), }; }), ); diff --git a/yarn-project/aztec.js/src/account/account_contract.ts b/yarn-project/aztec.js/src/account/account_contract.ts index 384ba8ab8642..f75c501ad8ba 100644 --- a/yarn-project/aztec.js/src/account/account_contract.ts +++ b/yarn-project/aztec.js/src/account/account_contract.ts @@ -30,6 +30,13 @@ export interface AccountContract { | undefined >; + /** + * The hash of this account's immutable instantiation params, committed into its address. Returns + * undefined for accounts that have no immutables (these are instead deployed via an on-chain + * initializer, which contributes to the address through its initialization hash). + */ + getImmutablesHash(): Promise; + /** * Returns the account implementation for this account contract given an instance at the provided address. * The account is responsible for assembling tx requests given requested function calls, and @@ -47,9 +54,14 @@ export interface AccountContract { } /** - * Compute the address of an account contract from secret and salt. + * Compute the address of an account contract from secret, salt and optional immutables hash */ -export async function getAccountContractAddress(accountContract: AccountContract, secret: Fr, salt: Fr) { +export async function getAccountContractAddress( + accountContract: AccountContract, + secret: Fr, + salt: Fr, + immutablesHash?: Fr, +) { const { publicKeys } = await deriveKeys(secret); const { constructorName, constructorArgs } = (await accountContract.getInitializationFunctionAndArgs()) ?? { constructorName: undefined, @@ -61,6 +73,7 @@ export async function getAccountContractAddress(accountContract: AccountContract constructorArgs, salt, publicKeys, + immutablesHash: immutablesHash ?? (await accountContract.getImmutablesHash()), }); return instance.address; } diff --git a/yarn-project/aztec.js/src/wallet/account_manager.ts b/yarn-project/aztec.js/src/wallet/account_manager.ts index 0cfa9a50eb1d..fca04c280532 100644 --- a/yarn-project/aztec.js/src/wallet/account_manager.ts +++ b/yarn-project/aztec.js/src/wallet/account_manager.ts @@ -62,7 +62,7 @@ export class AccountManager { salt, publicKeys, deployer: opts?.deployer, - immutablesHash: opts?.immutablesHash, + immutablesHash: opts?.immutablesHash ?? (await accountContract.getImmutablesHash()), }); return new AccountManager(wallet, secretKey, accountContract, instance); diff --git a/yarn-project/aztec/src/examples/token.ts b/yarn-project/aztec/src/examples/token.ts index 1e7e2077910d..fddb6704492f 100644 --- a/yarn-project/aztec/src/examples/token.ts +++ b/yarn-project/aztec/src/examples/token.ts @@ -21,10 +21,10 @@ async function main() { const wallet = await EmbeddedWallet.create(node); - // During local network setup we deploy a few accounts. Below we add them to our wallet. + // During local network setup we create a few initializerless accounts. Below we add them to our wallet. const [aliceInitialAccountData, bobInitialAccountData] = await getInitialTestAccountsData(); - await wallet.createSchnorrAccount(aliceInitialAccountData.secret, aliceInitialAccountData.salt); - await wallet.createSchnorrAccount(bobInitialAccountData.secret, bobInitialAccountData.salt); + await wallet.createSchnorrInitializerlessAccount(aliceInitialAccountData.secret, aliceInitialAccountData.salt); + await wallet.createSchnorrInitializerlessAccount(bobInitialAccountData.secret, bobInitialAccountData.salt); const alice = aliceInitialAccountData.address; const bob = bobInitialAccountData.address; diff --git a/yarn-project/aztec/src/local-network/local-network.ts b/yarn-project/aztec/src/local-network/local-network.ts index 8612fbde91d0..c8b6dd36459b 100644 --- a/yarn-project/aztec/src/local-network/local-network.ts +++ b/yarn-project/aztec/src/local-network/local-network.ts @@ -30,7 +30,7 @@ import { initTelemetryClient, } from '@aztec/telemetry-client'; import { EmbeddedWallet } from '@aztec/wallets/embedded'; -import { deployFundedSchnorrAccounts } from '@aztec/wallets/testing'; +import { createFundedInitializerlessAccounts } from '@aztec/wallets/testing'; import { getGenesisValues } from '@aztec/world-state/testing'; import { type Hex, createPublicClient, fallback, http as httpViemTransport } from 'viem'; @@ -251,7 +251,7 @@ export async function createLocalNetwork(config: Partial = { }); userLog('Setting up funded test accounts...'); - const accountManagers = await deployFundedSchnorrAccounts(wallet, initialAccounts, setupWaitOpts); + const accountManagers = await createFundedInitializerlessAccounts(wallet, initialAccounts); const accLogs = await createAccountLogs(accountManagers, wallet); userLog(accLogs.join('')); diff --git a/yarn-project/bot/src/factory.ts b/yarn-project/bot/src/factory.ts index aa04f1c75dbd..788dc69de8c3 100644 --- a/yarn-project/bot/src/factory.ts +++ b/yarn-project/bot/src/factory.ts @@ -1,5 +1,4 @@ import { getInitialTestAccountsData } from '@aztec/accounts/testing'; -import { NO_FROM } from '@aztec/aztec.js/account'; import { AztecAddress } from '@aztec/aztec.js/addresses'; import { BatchCall, @@ -17,20 +16,17 @@ import { createLogger } from '@aztec/aztec.js/log'; import { waitForL1ToL2MessageReady } from '@aztec/aztec.js/messaging'; import { waitForTx } from '@aztec/aztec.js/node'; import { getFeeJuiceBalance } from '@aztec/aztec.js/utils'; -import { ContractInitializationStatus } from '@aztec/aztec.js/wallet'; import { createEthereumChain } from '@aztec/ethereum/chain'; import { createExtendedL1Client } from '@aztec/ethereum/client'; import { RollupContract } from '@aztec/ethereum/contracts'; import type { ExtendedViemWalletClient } from '@aztec/ethereum/types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; -import { Timer } from '@aztec/foundation/timer'; import { AMMContract } from '@aztec/noir-contracts.js/AMM'; import { PrivateTokenContract } from '@aztec/noir-contracts.js/PrivateToken'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import { TestContract } from '@aztec/noir-test-contracts.js/Test'; import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; -import { GasFees, GasSettings, ManaUsageEstimate } from '@aztec/stdlib/gas'; import type { AztecNode, AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; import { deriveSigningKey } from '@aztec/stdlib/keys'; import { EmbeddedWallet } from '@aztec/wallets/embedded'; @@ -43,7 +39,6 @@ import { getBalances, getPrivateBalance, isStandardTokenContract } from './utils const MINT_BALANCE = 1e12; const MIN_BALANCE = 1e3; const FEE_JUICE_TOP_UP_THRESHOLD = 100n * 10n ** 18n; -const FEE_JUICE_TOP_UP_TARGET = 10_000n * 10n ** 18n; export class BotFactory { private log = createLogger('bot'); @@ -73,8 +68,8 @@ export class BotFactory { }> { const defaultAccountAddress = await this.setupAccount(); const recipient = (await this.wallet.createSchnorrAccount(Fr.random(), Fr.random())).address; - const token = await this.setupTokenWithOptionalEarlyRefuel(defaultAccountAddress); - await this.ensureFeeJuiceBalance(defaultAccountAddress, token); + await this.ensureFeeJuiceBalance(defaultAccountAddress); + const token = await this.setupToken(defaultAccountAddress); await this.mintTokens(token, defaultAccountAddress); return { wallet: this.wallet, defaultAccountAddress, token, node: this.aztecNode, recipient }; } @@ -88,13 +83,8 @@ export class BotFactory { node: AztecNode; }> { const defaultAccountAddress = await this.setupAccount(); - const token0 = await this.setupTokenContractWithOptionalEarlyRefuel( - defaultAccountAddress, - this.config.tokenSalt, - 'BotToken0', - 'BOT0', - ); - await this.ensureFeeJuiceBalance(defaultAccountAddress, token0); + await this.ensureFeeJuiceBalance(defaultAccountAddress); + const token0 = await this.setupTokenContract(defaultAccountAddress, this.config.tokenSalt, 'BotToken0', 'BOT0'); const token1 = await this.setupTokenContract(defaultAccountAddress, this.config.tokenSalt, 'BotToken1', 'BOT1'); const liquidityToken = await this.setupTokenContract( defaultAccountAddress, @@ -129,6 +119,7 @@ export class BotFactory { rollupVersion: bigint; }> { const defaultAccountAddress = await this.setupAccount(); + await this.ensureFeeJuiceBalance(defaultAccountAddress); // Create L1 client (same pattern as bridgeL1FeeJuice) const l1RpcUrls = this.config.l1RpcUrls; @@ -147,7 +138,7 @@ export class BotFactory { const rollupContract = new RollupContract(l1Client, l1ContractAddresses.rollupAddress.toString()); const rollupVersion = await rollupContract.getVersion(); - // Deploy TestContract + // Deploy TestContract (pays from the standing balance funded above). const contract = await this.setupTestContract(defaultAccountAddress); // Recover any pending messages from store (clean up stale ones first) @@ -210,48 +201,16 @@ export class BotFactory { } } - private async setupAccountWithPrivateKey(secret: Fr) { - const salt = this.config.senderSalt ?? Fr.ONE; - const signingKey = deriveSigningKey(secret); - const accountManager = await this.wallet.createSchnorrAccount(secret, salt, signingKey); - const metadata = await this.wallet.getContractMetadata(accountManager.address); - if (metadata.initializationStatus === ContractInitializationStatus.INITIALIZED) { - this.log.info(`Account at ${accountManager.address.toString()} already initialized`); - const timer = new Timer(); - const address = accountManager.address; - this.log.info(`Account at ${address} registered. duration=${timer.ms()}`); - await this.store.deleteBridgeClaim(address); - return address; - } else { - const address = accountManager.address; - this.log.info(`Deploying account at ${address}`); - - const claim = await this.getOrCreateBridgeClaim(address); - - const paymentMethod = new FeeJuicePaymentMethodWithClaim(accountManager.address, claim); - const deployMethod = await accountManager.getDeployMethod(); - - await this.withNoMinTxsPerBlock(async () => { - const { txHash } = await deployMethod.send({ - from: NO_FROM, - fee: { paymentMethod }, - wait: NO_WAIT, - }); - this.log.info(`Sent tx for account deployment with hash ${txHash.toString()}`); - return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds }); - }); - this.log.info(`Account deployed at ${address}`); - - // Clean up the consumed bridge claim - await this.store.deleteBridgeClaim(address); - - return accountManager.address; - } - } - + /** + * Keyless fallback for tests and local dev: reuses the first genesis test account, whose address is + * pre-funded with fee juice via `initialFundedAccounts`. The test accounts are initializerless, so this + * must create an initializerless account for the address to match the funded one. Production bots set a + * sender private key and fund the resulting initializerless account from L1 instead; see + * setupAccountWithPrivateKey. + */ private async setupTestAccount() { const [initialAccountData] = await getInitialTestAccountsData(); - const accountManager = await this.wallet.createSchnorrAccount( + const accountManager = await this.wallet.createSchnorrInitializerlessAccount( initialAccountData.secret, initialAccountData.salt, initialAccountData.signingKey, @@ -259,62 +218,11 @@ export class BotFactory { return accountManager.address; } - /** - * Setup token and refuel first: if the token already exists (restart scenario), - * run ensureFeeJuiceBalance before any step that might need fee juice. When deploying, - * use a bridge claim if balance is below threshold. - */ - private async setupTokenWithOptionalEarlyRefuel(sender: AztecAddress): Promise { - const token = await this.getTokenInstance(sender); - const address = token.address; - const metadata = await this.wallet.getContractMetadata(address); - if (metadata.isContractPublished) { - this.log.info(`Token at ${address.toString()} already deployed, refueling before setup`); - await this.ensureFeeJuiceBalance(sender, token); - } - return this.setupToken(sender); - } - - /** - * Setup token0 for AMM with refuel-first behaviour when token already exists. - */ - private async setupTokenContractWithOptionalEarlyRefuel( - deployer: AztecAddress, - salt: Fr, - name: string, - ticker: string, - decimals = 18, - ): Promise { - const deploy = TokenContract.deploy(this.wallet, deployer, name, ticker, decimals, { salt, universalDeploy: true }); - const instance = await deploy.getInstance(); - const metadata = await this.wallet.getContractMetadata(instance.address); - if (metadata.isContractPublished) { - this.log.info(`Token ${name} at ${instance.address.toString()} already deployed, refueling before setup`); - const token = TokenContract.at(instance.address, this.wallet); - await this.ensureFeeJuiceBalance(deployer, token); - } - return this.setupTokenContract(deployer, salt, name, ticker, decimals); - } - - private async getTokenInstance(sender: AztecAddress): Promise { - const salt = this.config.tokenSalt; - if (this.config.contract === SupportedTokenContracts.TokenContract) { - const deploy = TokenContract.deploy(this.wallet, sender, 'BotToken', 'BOT', 18, { salt, universalDeploy: true }); - const instance = await deploy.getInstance(); - return TokenContract.at(instance.address, this.wallet); - } - if (this.config.contract === SupportedTokenContracts.PrivateTokenContract) { - const tokenSecretKey = Fr.random(); - const tokenPublicKeys = (await deriveKeys(tokenSecretKey)).publicKeys; - const deploy = PrivateTokenContract.deploy(this.wallet, MINT_BALANCE, sender, { - salt, - universalDeploy: true, - publicKeys: tokenPublicKeys, - }); - const instance = await deploy.getInstance(); - return PrivateTokenContract.at(instance.address, this.wallet); - } - throw new Error(`Unsupported token contract type: ${this.config.contract}`); + private async setupAccountWithPrivateKey(secret: Fr) { + const salt = this.config.senderSalt ?? Fr.ONE; + const signingKey = deriveSigningKey(secret); + const accountManager = await this.wallet.createSchnorrInitializerlessAccount(secret, salt, signingKey); + return accountManager.address; } /** @@ -511,66 +419,39 @@ export class BotFactory { if (metadata.isContractPublished) { this.log.info(`Contract ${name} at ${address.toString()} already deployed`); await deploy.register(); - } else { - const sender = deployOpts.from === NO_FROM ? undefined : deployOpts.from; - const balance = sender ? await getFeeJuiceBalance(sender, this.aztecNode) : 0n; - const useClaim = - sender && - balance < FEE_JUICE_TOP_UP_THRESHOLD && - this.config.feePaymentMethod === 'fee_juice' && - !!this.config.l1RpcUrls?.length; - const mnemonicOrPrivateKey = this.config.l1PrivateKey?.getValue() ?? this.config.l1Mnemonic?.getValue(); - - if (useClaim && mnemonicOrPrivateKey) { - const claim = await this.getOrCreateBridgeClaim(sender!); - const paymentMethod = new FeeJuicePaymentMethodWithClaim(sender!, claim); - const { estimatedGas } = await deploy.simulate({ ...deployOpts, fee: { estimateGas: true, paymentMethod } }); - const maxFeesPerGas = (await this.getMinFees()).mul(1 + this.config.minFeePadding); - const gasSettings = GasSettings.from({ - ...estimatedGas!, - maxFeesPerGas, - maxPriorityFeesPerGas: GasFees.empty(), - }); - await this.withNoMinTxsPerBlock(async () => { - const { txHash } = await deploy.send({ ...deployOpts, fee: { gasSettings, paymentMethod }, wait: NO_WAIT }); - this.log.info( - `Sent contract ${name} deploy tx ${txHash.toString()} (using bridge claim, balance was ${balance})`, - ); - return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds }); - }); - await this.store.deleteBridgeClaim(sender!); - } else { - const { estimatedGas } = await deploy.simulate({ ...deployOpts, fee: { estimateGas: true } }); - this.log.info(`Deploying contract ${name} at ${address.toString()}`, { estimatedGas }); - await this.withNoMinTxsPerBlock(async () => { - const { txHash } = await deploy.send({ ...deployOpts, fee: { gasSettings: estimatedGas }, wait: NO_WAIT }); - this.log.info(`Sent contract ${name} setup tx with hash ${txHash.toString()}`); - return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds }); - }); - } + return instance; } + + // Setup always runs ensureFeeJuiceBalance before any deploy, so the account pays from its standing + // balance here. No manual gas estimation: the embedded wallet simulates before sending and derives + // the gas limits and padded maxFeesPerGas itself. + this.log.info(`Deploying contract ${name} at ${address.toString()}`); + await this.withNoMinTxsPerBlock(async () => { + const { txHash } = await deploy.send({ ...deployOpts, wait: NO_WAIT }); + this.log.info(`Sent contract ${name} deploy tx ${txHash.toString()}`); + return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds }); + }); + return instance; } + /** True when the config allows bridging fee juice from L1 (fee_juice mode, an L1 RPC, and an L1 key). */ + private isL1BridgingConfigured(): boolean { + const mnemonicOrPrivateKey = this.config.l1PrivateKey?.getValue() ?? this.config.l1Mnemonic?.getValue(); + return this.config.feePaymentMethod === 'fee_juice' && !!this.config.l1RpcUrls?.length && !!mnemonicOrPrivateKey; + } + /** - * Mints private and public tokens for the sender if their balance is below the minimum. - * @param token - Token contract. - */ - /** - * Ensures the account has sufficient fee juice by bridging from L1 if balance is below threshold. - * Bridges repeatedly until balance reaches the target (10k FJ). - * Used on startup/restart to top up when the account has run out after previous runs. + * Ensures the account holds enough fee juice before any other setup step. The account starts empty + * (initializerless accounts have no deployment tx) and the runtime loop pays fees from this balance and + * never refuels itself, so every flow funds the account up front. Bridges claims from L1 and consumes + * each with a claim-only tx until the balance clears the threshold, working from a zero (fresh run) or + * drained (restart) balance. Each bridge mints a fixed amount well above the threshold, so this is a + * single bridge in practice. No-op when L1 bridging is not configured or the balance is already above + * the threshold. */ - private async ensureFeeJuiceBalance( - account: AztecAddress, - token: TokenContract | PrivateTokenContract, - ): Promise { - const { feePaymentMethod, l1RpcUrls } = this.config; - if (feePaymentMethod !== 'fee_juice' || !l1RpcUrls?.length) { - return; - } - const mnemonicOrPrivateKey = this.config.l1PrivateKey?.getValue() ?? this.config.l1Mnemonic?.getValue(); - if (!mnemonicOrPrivateKey) { + private async ensureFeeJuiceBalance(account: AztecAddress): Promise { + if (!this.isL1BridgingConfigured()) { return; } @@ -580,36 +461,21 @@ export class BotFactory { return; } - this.log.info( - `Fee juice balance ${balance} below threshold ${FEE_JUICE_TOP_UP_THRESHOLD}, bridging from L1 until ${FEE_JUICE_TOP_UP_TARGET}`, - ); - const maxFeesPerGas = (await this.getMinFees()).mul(1 + this.config.minFeePadding); - const minimalInteraction = isStandardTokenContract(token) - ? token.methods.transfer_in_public(account, account, 0n, 0) - : token.methods.transfer(0n, account, account); + this.log.info(`Fee juice balance ${balance} below threshold ${FEE_JUICE_TOP_UP_THRESHOLD}, bridging from L1`); - while (balance < FEE_JUICE_TOP_UP_TARGET) { - const claim = await this.bridgeL1FeeJuice(account); + while (balance < FEE_JUICE_TOP_UP_THRESHOLD) { + // Persist the claim before consuming it: if the top-up tx fails or the bot crashes mid-loop, the + // next run reuses the pending claim instead of bridging again (and wasting the bridged funds). + const claim = await this.getOrCreateBridgeClaim(account); const paymentMethod = new FeeJuicePaymentMethodWithClaim(account, claim); - const { estimatedGas } = await minimalInteraction.simulate({ - from: account, - fee: { estimateGas: true, paymentMethod }, - }); - const gasSettings = GasSettings.from({ - ...estimatedGas!, - maxFeesPerGas, - maxPriorityFeesPerGas: GasFees.empty(), - }); await this.withNoMinTxsPerBlock(async () => { - const { txHash } = await minimalInteraction.send({ - from: account, - fee: { gasSettings, paymentMethod }, - wait: NO_WAIT, - }); + const executionPayload = await paymentMethod.getExecutionPayload(); + const { txHash } = await this.wallet.sendTx(executionPayload, { from: account, wait: NO_WAIT }); this.log.info(`Sent fee juice top-up tx ${txHash.toString()}`); return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds }); }); + await this.store.deleteBridgeClaim(account); balance = await getFeeJuiceBalance(account, this.aztecNode); this.log.info(`Fee juice balance after top-up: ${balance}`); } @@ -661,17 +527,14 @@ export class BotFactory { } /** - * Gets or creates a bridge claim for the recipient. - * Checks if a claim already exists in the store and reuses it if valid. - * Only creates a new bridge if fee juice balance is below threshold. + * Returns a usable bridge claim for the recipient, reusing a persisted one when its L1→L2 message is + * still available (resuming a top-up that failed or crashed before the claim was consumed) and bridging + * a fresh claim otherwise. The caller deletes the claim from the store once it has been consumed. */ private async getOrCreateBridgeClaim(recipient: AztecAddress): Promise { - // Check if we have an existing claim in the store const existingClaim = await this.store.getBridgeClaim(recipient); if (existingClaim) { this.log.info(`Found existing bridge claim for ${recipient.toString()}, checking validity...`); - - // Check if the message is ready on L2 try { const messageHash = Fr.fromHexString(existingClaim.claim.messageHash); await this.withNoMinTxsPerBlock(() => @@ -688,7 +551,6 @@ export class BotFactory { const claim = await this.bridgeL1FeeJuice(recipient); await this.store.saveBridgeClaim(recipient, claim); - return claim; } @@ -723,19 +585,6 @@ export class BotFactory { return claim as L2AmountClaim; } - /** Returns worst-case min fees across predicted slots, with fallback to current min fees. */ - private async getMinFees(): Promise { - try { - const predicted = await this.aztecNode.getPredictedMinFees(ManaUsageEstimate.Limit); - if (predicted.length === 0) { - return this.aztecNode.getCurrentMinFees(); - } - return predicted.reduce((worst, fees) => (fees.feePerL2Gas > worst.feePerL2Gas ? fees : worst)); - } catch { - return this.aztecNode.getCurrentMinFees(); - } - } - private async withNoMinTxsPerBlock(fn: () => Promise): Promise { if (!this.aztecNodeAdmin || !this.config.flushSetupTransactions) { this.log.verbose(`No node admin client or flushing not requested (not setting minTxsPerBlock to 0)`); diff --git a/yarn-project/cli-wallet/src/cmds/create_account.ts b/yarn-project/cli-wallet/src/cmds/create_account.ts index 0bf8ad99a8d5..6b4487267bb5 100644 --- a/yarn-project/cli-wallet/src/cmds/create_account.ts +++ b/yarn-project/cli-wallet/src/cmds/create_account.ts @@ -71,7 +71,9 @@ export async function createAccount( let txHash: TxHash | undefined; let txReceipt: TxReceipt | undefined; - if (!registerOnly) { + // Initializerless accounts have no deployment tx — creating one only registers it locally — so there is + // nothing to deploy on-chain. + if (!registerOnly && accountType !== 'schnorr_initializerless') { const { paymentMethod, gasSettings } = await feeOpts.toUserFeeOptions(aztecNode, wallet, address); const delegatedDeployment = deployer && !account.address.equals(deployer); diff --git a/yarn-project/cli-wallet/src/cmds/import_test_accounts.ts b/yarn-project/cli-wallet/src/cmds/import_test_accounts.ts index e6302e5aa10f..f531b91ed1c4 100644 --- a/yarn-project/cli-wallet/src/cmds/import_test_accounts.ts +++ b/yarn-project/cli-wallet/src/cmds/import_test_accounts.ts @@ -16,7 +16,11 @@ export async function importTestAccounts(wallet: CLIWallet, db: WalletDB, json: const secret = testAccounts[i].secret; const salt = new Fr(account.salt); const address = account.address; - await db.storeAccount(address, { type: 'schnorr', secretKey: secret, salt, alias, publicKey: undefined }, log); + await db.storeAccount( + address, + { type: 'schnorr_initializerless', secretKey: secret, salt, alias, publicKey: undefined }, + log, + ); if (json) { out[alias] = { diff --git a/yarn-project/cli-wallet/src/utils/constants.ts b/yarn-project/cli-wallet/src/utils/constants.ts index ab747206179a..ce5184c51584 100644 --- a/yarn-project/cli-wallet/src/utils/constants.ts +++ b/yarn-project/cli-wallet/src/utils/constants.ts @@ -1,4 +1,10 @@ export const MIN_FEE_PADDING = 0.5; -export const AccountTypes = ['schnorr', 'ecdsasecp256r1', 'ecdsasecp256r1ssh', 'ecdsasecp256k1'] as const; +export const AccountTypes = [ + 'schnorr', + 'schnorr_initializerless', + 'ecdsasecp256r1', + 'ecdsasecp256r1ssh', + 'ecdsasecp256k1', +] as const; export type AccountType = (typeof AccountTypes)[number]; diff --git a/yarn-project/cli-wallet/src/utils/wallet.ts b/yarn-project/cli-wallet/src/utils/wallet.ts index e4ff836941de..74c1d047f97a 100644 --- a/yarn-project/cli-wallet/src/utils/wallet.ts +++ b/yarn-project/cli-wallet/src/utils/wallet.ts @@ -1,10 +1,15 @@ import { EcdsaRAccountContract, EcdsaRSSHAccountContract } from '@aztec/accounts/ecdsa'; import { StubEcdsaAccountContractArtifact, createStubEcdsaAccount } from '@aztec/accounts/ecdsa/stub'; -import { SchnorrAccountContract } from '@aztec/accounts/schnorr'; +import { SchnorrAccountContract, SchnorrInitializerlessAccountContract } from '@aztec/accounts/schnorr'; import { StubSchnorrAccountContractArtifact, createStubSchnorrAccount } from '@aztec/accounts/schnorr/stub'; import { getIdentities } from '@aztec/accounts/utils'; import { type Account, type AccountContract, NO_FROM } from '@aztec/aztec.js/account'; -import { type InteractionFeeOptions, getContractClassFromArtifact, getGasLimits } from '@aztec/aztec.js/contracts'; +import { + ContractFunctionInteraction, + type InteractionFeeOptions, + getContractClassFromArtifact, + getGasLimits, +} from '@aztec/aztec.js/contracts'; import type { AztecNode } from '@aztec/aztec.js/node'; import { AccountManager, type Aliased, type SimulateOptions } from '@aztec/aztec.js/wallet'; import { TxSimulationResultWithAppOffset } from '@aztec/aztec.js/wallet'; @@ -77,6 +82,7 @@ export class CLIWallet extends BaseWallet { await this.pxe.registerContractClass(StubEcdsaAccountContractArtifact); this.stubClassIds.set('schnorr', schnorrClassId); + this.stubClassIds.set('schnorr_initializerless', schnorrClassId); this.stubClassIds.set('ecdsasecp256k1', ecdsaClassId); this.stubClassIds.set('ecdsasecp256r1', ecdsaClassId); this.stubClassIds.set('ecdsasecp256r1ssh', ecdsaClassId); @@ -152,6 +158,20 @@ export class CLIWallet extends BaseWallet { await this.registerContract(instance, artifact, secret); this.accountCache.set(accountManager.address.toString(), await accountManager.getAccount()); + + // Initializerless accounts have no deployment tx; their address commits to the signing public key + // (via the contract's immutablesHash, resolved by AccountManager.create) and the constructor's + // storage writes are materialized locally via a simulated "store" call here. + if (contract instanceof SchnorrInitializerlessAccountContract) { + const constructorAbi = artifact.functions.find(f => f.name === 'constructor'); + if (!constructorAbi) { + throw new Error('Could not create SchnorrInitializerlessAccount: constructor ABI not found'); + } + const { x, y } = await contract.getSigningPublicKey(); + const storeCall = new ContractFunctionInteraction(this, instance.address, constructorAbi, [x, y]); + await storeCall.simulate({ from: instance.address }); + } + return accountManager; } @@ -179,6 +199,14 @@ export class CLIWallet extends BaseWallet { account = await this.createAccount(secretKey, salt, new SchnorrAccountContract(deriveSigningKey(secretKey))); break; } + case 'schnorr_initializerless': { + account = await this.createAccount( + secretKey, + salt, + new SchnorrInitializerlessAccountContract(deriveSigningKey(secretKey)), + ); + break; + } case 'ecdsasecp256r1': { account = await this.createAccount( secretKey, @@ -229,7 +257,9 @@ export class CLIWallet extends BaseWallet { } const { type } = await this.db!.retrieveAccount(address); const stubAccount = - type === 'schnorr' ? createStubSchnorrAccount(originalAddress) : createStubEcdsaAccount(originalAddress); + type === 'schnorr' || type === 'schnorr_initializerless' + ? createStubSchnorrAccount(originalAddress) + : createStubEcdsaAccount(originalAddress); const stubClassId = this.stubClassIds.get(type); if (!stubClassId) { throw new Error( diff --git a/yarn-project/cli/src/cmds/infrastructure/setup_l2_contract.ts b/yarn-project/cli/src/cmds/infrastructure/setup_l2_contract.ts index bd74d49765e9..0625ca237bc2 100644 --- a/yarn-project/cli/src/cmds/infrastructure/setup_l2_contract.ts +++ b/yarn-project/cli/src/cmds/infrastructure/setup_l2_contract.ts @@ -8,7 +8,7 @@ import { jsonStringify } from '@aztec/foundation/json-rpc'; import type { LogFn } from '@aztec/foundation/log'; import { ProtocolContractAddress } from '@aztec/protocol-contracts'; import { EmbeddedWallet } from '@aztec/wallets/embedded'; -import { deployFundedSchnorrAccounts } from '@aztec/wallets/testing'; +import { createFundedInitializerlessAccounts } from '@aztec/wallets/testing'; export async function setupL2Contracts(nodeUrl: string, testAccounts: boolean, json: boolean, log: LogFn) { const waitOpts: WaitOpts = { @@ -23,16 +23,16 @@ export async function setupL2Contracts(nodeUrl: string, testAccounts: boolean, j const node = createAztecNodeClient(nodeUrl); const wallet = await EmbeddedWallet.create(node); - let deployedAccountManagers: AccountManager[] = []; + let accountManagers: AccountManager[] = []; if (testAccounts) { - log('setupL2Contracts: Deploying test accounts...'); + log('setupL2Contracts: Creating test accounts...'); const initialAccountsData = await getInitialTestAccountsData(); - deployedAccountManagers = await deployFundedSchnorrAccounts(wallet, initialAccountsData, waitOpts); + accountManagers = await createFundedInitializerlessAccounts(wallet, initialAccountsData); } if (json) { const toPrint: Record = { ...ProtocolContractAddress }; - deployedAccountManagers.forEach((a, i) => { + accountManagers.forEach((a, i) => { toPrint[`testAccount${i}`] = a.address; }); log(JSON.stringify(toPrint, null, 2)); diff --git a/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts b/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts index 74ba3a4439d7..3a282a6d8b64 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts @@ -29,7 +29,7 @@ import { GasSettings } from '@aztec/stdlib/gas'; import { deriveSigningKey } from '@aztec/stdlib/keys'; import { AUTOMINE_E2E_OPTS, MNEMONIC, getPaddedMaxFeesPerGas } from '../../fixtures/fixtures.js'; -import { type EndToEndContext, type SetupOptions, deployAccounts, setup, teardown } from '../../fixtures/setup.js'; +import { type EndToEndContext, type SetupOptions, setup, teardown } from '../../fixtures/setup.js'; import { mintTokensToPrivate } from '../../fixtures/token_utils.js'; import { setupSponsoredFPC } from '../../fixtures/utils.js'; import { CrossChainTestHarness } from '../../shared/cross_chain_test_harness.js'; @@ -135,10 +135,9 @@ export class ClientFlowsBenchmark { this.logger.info('Setting up subsystems from fresh'); // Token allowlist entries are test-only: FPC-based fee payment with custom tokens won't work on mainnet alpha. const tokenAllowList = await getTokenAllowedSetupFunctions(); - this.context = await setup(0, { + this.context = await setup(2, { ...this.setupOptions, fundSponsoredFPC: true, - skipAccountDeployment: true, l1ContractsArgs: this.setupOptions, txPublicSetupAllowListExtend: [...(this.setupOptions.txPublicSetupAllowListExtend ?? []), ...tokenAllowList], }); @@ -203,15 +202,7 @@ export class ClientFlowsBenchmark { async applyInitialAccounts() { this.logger.info('Applying initial accounts setup'); - const { deployedAccounts } = await deployAccounts( - 2, - this.logger, - )({ - wallet: this.context.wallet, - initialFundedAccounts: this.context.initialFundedAccounts, - }); - - const [{ address: adminAddress }, { address: sequencerAddress }] = deployedAccounts; + const [adminAddress, sequencerAddress] = this.context.accounts; this.adminWallet = this.context.wallet; this.aztecNode = this.context.aztecNodeService; diff --git a/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts b/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts index 0853af29b175..04c20b23ca60 100644 --- a/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts +++ b/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts @@ -1,4 +1,4 @@ -import type { InitialAccountData } from '@aztec/accounts/testing'; +import { type InitialAccountData, generateSchnorrAccounts } from '@aztec/accounts/testing'; import type { ContractInstanceWithAddress } from '@aztec/aztec.js/contracts'; import { computeSecretHash } from '@aztec/aztec.js/crypto'; import type { AztecNode } from '@aztec/aztec.js/node'; @@ -46,7 +46,7 @@ describe('Aztec persistence', () => { let ownerAddress: AztecAddress; // Data of the funded accounts that can deploy themselves. - let initialFundedAccounts: InitialAccountData[]; + let additionallyFundedAccounts: InitialAccountData[]; // a directory where data will be persisted by components // passing this through to the Node or PXE will control whether they use persisted data or not @@ -62,16 +62,18 @@ describe('Aztec persistence', () => { dataDirectory = await mkdtemp(join(tmpdir(), 'aztec-node-')); const initialContext = await setup( - 1, - { ...PIPELINING_SETUP_OPTS, dataDirectory, numberOfInitialFundedAccounts: 3 }, + 0, + { ...PIPELINING_SETUP_OPTS, dataDirectory, additionallyFundedAccounts: await generateSchnorrAccounts(3) }, { dataDirectory }, ); aztecNode = initialContext.aztecNode; deployL1ContractsValues = initialContext.deployL1ContractsValues; - initialFundedAccounts = initialContext.initialFundedAccounts; + additionallyFundedAccounts = initialContext.additionallyFundedAccounts; wallet = initialContext.wallet; - owner = initialFundedAccounts[0]; + owner = additionallyFundedAccounts[0]; ownerAddress = owner.address; + // The owner is funded but not created by setup; create it (initializerless) so it can transact. + await wallet.createSchnorrInitializerlessAccount(owner.secret, owner.salt, owner.signingKey); const { contract, instance } = await TokenBlacklistContract.deploy(wallet, ownerAddress).send({ from: ownerAddress, @@ -121,7 +123,7 @@ describe('Aztec persistence', () => { () => setup( 0, - { ...PIPELINING_SETUP_OPTS, dataDirectory, deployL1ContractsValues, initialFundedAccounts }, + { ...PIPELINING_SETUP_OPTS, dataDirectory, deployL1ContractsValues, additionallyFundedAccounts }, { dataDirectory }, ), 60_000, @@ -129,7 +131,8 @@ describe('Aztec persistence', () => { [ // ie our PXE was restarted, data kept intact and now connects to a "new" Node. Initial synch will synch from scratch 'when starting a PXE with an existing database, connected to a Node with database synched from scratch', - () => setup(0, { ...PIPELINING_SETUP_OPTS, deployL1ContractsValues, initialFundedAccounts }, { dataDirectory }), + () => + setup(0, { ...PIPELINING_SETUP_OPTS, deployL1ContractsValues, additionallyFundedAccounts }, { dataDirectory }), 120_000, ], ])('%s', (_, contextSetup, timeout) => { @@ -137,7 +140,7 @@ describe('Aztec persistence', () => { beforeEach(async () => { context = await contextSetup(); - await context.wallet.createSchnorrAccount(owner.secret, owner.salt, owner.signingKey); + await context.wallet.createSchnorrInitializerlessAccount(owner.secret, owner.salt, owner.signingKey); contract = TokenBlacklistContract.at(contractAddress, wallet); }, timeout); @@ -193,8 +196,8 @@ describe('Aztec persistence', () => { }); it('allows spending of private notes', async () => { - const account = initialFundedAccounts[1]; // Not the owner account. - const otherAccount = await context.wallet.createSchnorrAccount(account.secret, account.salt); + const account = additionallyFundedAccounts[1]; // Not the owner account. + const otherAccount = await context.wallet.createSchnorrInitializerlessAccount(account.secret, account.salt); const otherAddress = otherAccount.address; const { result: initialOwnerBalance } = await contract.methods @@ -217,13 +220,14 @@ describe('Aztec persistence', () => { [ // ie. I'm setting up a new full node, sync from scratch and restore wallets/notes 'when starting the Node and PXE with empty databases', - () => setup(0, { ...PIPELINING_SETUP_OPTS, deployL1ContractsValues, initialFundedAccounts }, {}), + () => setup(0, { ...PIPELINING_SETUP_OPTS, deployL1ContractsValues, additionallyFundedAccounts }, {}), 120_000, ], [ // ie. I'm setting up a new PXE, restore wallets/notes from a Node 'when starting a PXE with an empty database connected to a Node with an existing database', - () => setup(0, { ...PIPELINING_SETUP_OPTS, dataDirectory, deployL1ContractsValues, initialFundedAccounts }, {}), + () => + setup(0, { ...PIPELINING_SETUP_OPTS, dataDirectory, deployL1ContractsValues, additionallyFundedAccounts }, {}), 120_000, ], ])('%s', (_, contextSetup, timeout) => { @@ -269,8 +273,8 @@ describe('Aztec persistence', () => { it('pxe restores notes after registering the owner', async () => { await context.wallet.registerContract(contractInstance, TokenBlacklistContract.artifact); - const account = initialFundedAccounts[0]; - await context.wallet.createSchnorrAccount(account.secret, account.salt); + const account = additionallyFundedAccounts[0]; + await context.wallet.createSchnorrInitializerlessAccount(account.secret, account.salt); const contract = TokenBlacklistContract.at(contractAddress, context.wallet); // check that notes total more than 0 so that this test isn't dependent on run order @@ -299,8 +303,8 @@ describe('Aztec persistence', () => { await temporaryContext.wallet.registerContract(contractInstance, TokenBlacklistContract.artifact); - const account = initialFundedAccounts[0]; - await context.wallet.createSchnorrAccount(account.secret, account.salt); + const account = additionallyFundedAccounts[0]; + await context.wallet.createSchnorrInitializerlessAccount(account.secret, account.salt); const contract = TokenBlacklistContract.at(contractAddress, context.wallet); @@ -324,8 +328,8 @@ describe('Aztec persistence', () => { beforeEach(async () => { context = await setup(0, { ...PIPELINING_SETUP_OPTS, dataDirectory, deployL1ContractsValues }, { dataDirectory }); - const account = initialFundedAccounts[0]; - await context.wallet.createSchnorrAccount(account.secret, account.salt); + const account = additionallyFundedAccounts[0]; + await context.wallet.createSchnorrInitializerlessAccount(account.secret, account.salt); contract = TokenBlacklistContract.at(contractAddress, context.wallet); }, 120_000); diff --git a/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts b/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts index 49552eab25cf..861060b5a3e1 100644 --- a/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts +++ b/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts @@ -5,7 +5,7 @@ * and Web3Signer for remote signing. Verifies that blocks are produced, * attestations are signed, and no double-signing occurs. */ -import type { InitialAccountData } from '@aztec/accounts/testing'; +import { type InitialAccountData, generateSchnorrAccounts } from '@aztec/accounts/testing'; import { type AztecNodeConfig, AztecNodeService } from '@aztec/aztec-node'; import { NO_FROM } from '@aztec/aztec.js/account'; import { AztecAddress, EthAddress } from '@aztec/aztec.js/addresses'; @@ -74,7 +74,7 @@ describe('HA Full Setup', () => { let aztecNode: AztecNode; let config: AztecNodeConfig; let teardown: () => Promise; - let initialFundedAccounts: InitialAccountData[]; + let additionallyFundedAccounts: InitialAccountData[]; let dateProvider: TestDateProvider; let genesis: GenesisData | undefined; @@ -152,12 +152,12 @@ describe('HA Full Setup', () => { wallet, aztecNode, config, - initialFundedAccounts, + additionallyFundedAccounts, dateProvider, deployL1ContractsValues, genesis, } = await setup( - 1, + 0, { ...PIPELINING_SETUP_OPTS, initialValidators, @@ -167,10 +167,14 @@ describe('HA Full Setup', () => { sequencerPollingIntervalMS: 200, worldStateBlockCheckIntervalMS: 200, blockCheckIntervalMS: 200, + // We deploy this account ourselves later, so fund it as a regular (deployable) schnorr account. + additionallyFundedAccounts: await generateSchnorrAccounts(1, 'schnorr'), startProverNode: true, // Disable validation on this node disableValidator: true, - skipAccountDeployment: true, + // This node cannot build blocks (validation is disabled and the committee's HA nodes start later), + // so setup must not wait for a block past genesis. + advancePastGenesis: false, // Enable P2P for transaction gossip p2pEnabled: true, // Enable slashing for testing governance + slashing vote coordination @@ -291,7 +295,7 @@ describe('HA Full Setup', () => { // Now deploy the account - blocks can be built by the HA nodes logger.info('Deploying test account now that validators are running'); - const accountData = initialFundedAccounts[0]; + const accountData = additionallyFundedAccounts[0]; const accountManager = await wallet.createSchnorrAccount( accountData.secret, accountData.salt, diff --git a/yarn-project/end-to-end/src/e2e_2_pxes.test.ts b/yarn-project/end-to-end/src/e2e_2_pxes.test.ts index 22274e82c59f..2c88438732db 100644 --- a/yarn-project/end-to-end/src/e2e_2_pxes.test.ts +++ b/yarn-project/end-to-end/src/e2e_2_pxes.test.ts @@ -1,4 +1,4 @@ -import type { InitialAccountData } from '@aztec/accounts/testing'; +import { type InitialAccountData, generateSchnorrAccounts } from '@aztec/accounts/testing'; import { NO_FROM } from '@aztec/aztec.js/account'; import { AztecAddress } from '@aztec/aztec.js/addresses'; import { Fr } from '@aztec/aztec.js/fields'; @@ -24,7 +24,7 @@ describe('e2e_2_pxes', () => { let walletB: TestWallet; let accountAAddress: AztecAddress; let accountBAddress: AztecAddress; - let initialFundedAccounts: InitialAccountData[]; + let additionallyFundedAccounts: InitialAccountData[]; let logger: Logger; let teardownA: () => Promise; let teardownB: () => Promise; @@ -48,18 +48,22 @@ describe('e2e_2_pxes', () => { beforeEach(async () => { ({ aztecNode, - initialFundedAccounts, + additionallyFundedAccounts, wallet: walletA, accounts: [accountAAddress], logger, teardown: teardownA, - } = await setup(1, { ...AUTOMINE_E2E_OPTS, numberOfInitialFundedAccounts: 3 })); + // accountA is the default initializerless account; accountB/C are created+deployed from these. + } = await setup(1, { + ...AUTOMINE_E2E_OPTS, + additionallyFundedAccounts: await generateSchnorrAccounts(3, 'schnorr'), + })); ({ wallet: walletB, address: accountBAddress, teardown: teardownB, - } = await setupSecondaryPXE(aztecNode, initialFundedAccounts, 1, 'pxe-b')); + } = await setupSecondaryPXE(aztecNode, additionallyFundedAccounts, 1, 'pxe-b')); await walletA.registerSender(accountBAddress, 'accountB'); await walletB.registerSender(accountAAddress, 'accountA'); @@ -188,7 +192,7 @@ describe('e2e_2_pxes', () => { const transferAmount2 = 323n; // setup an account that is shared across PXEs - const sharedAccount = initialFundedAccounts[2]; + const sharedAccount = additionallyFundedAccounts[2]; const sharedAccountOnAManager = await walletA.createSchnorrAccount(sharedAccount.secret, sharedAccount.salt); const sharedAccountOnADeployMethod = await sharedAccountOnAManager.getDeployMethod(); await sharedAccountOnADeployMethod.send({ from: NO_FROM }); @@ -232,7 +236,7 @@ describe('e2e_2_pxes', () => { wallet: walletC, address: accountCAddress, teardown: teardownC, - } = await setupSecondaryPXE(aztecNode, initialFundedAccounts, 2, 'pxe-c'); + } = await setupSecondaryPXE(aztecNode, additionallyFundedAccounts, 2, 'pxe-c'); await walletC.registerContract(instance, TokenContract.artifact); // Transfer from A to C 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 74a85e77875c..4635f190c272 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 @@ -1,5 +1,5 @@ import { EcdsaKAccountContract } from '@aztec/accounts/ecdsa'; -import { SchnorrAccountContract } from '@aztec/accounts/schnorr'; +import { SchnorrAccountContract, SchnorrInitializerlessAccountContract } from '@aztec/accounts/schnorr'; import { type Account, type AccountContract, @@ -63,14 +63,14 @@ const itShouldBehaveLikeAnAccountContract = ( ({ logger, teardown, aztecNode } = await setup(0, { ...AUTOMINE_E2E_OPTS, - initialFundedAccounts: [accountData], + additionallyFundedAccounts: [accountData], })); wallet = await TestWalletInternals.create(aztecNode); const accountManager = await wallet.createAccount({ secret, contract, salt }); completeAddress = await accountManager.getCompleteAddress(); + if (await accountManager.hasInitializer()) { - // The account is pre-funded and can pay for its own fee. const deployMethod = await accountManager.getDeployMethod(); await deployMethod.send({ from: NO_FROM }); } @@ -113,6 +113,10 @@ describe('e2e_account_contracts', () => { itShouldBehaveLikeAnAccountContract(() => new SchnorrAccountContract(GrumpkinScalar.random())); }); + describe('schnorr initializerless account', () => { + itShouldBehaveLikeAnAccountContract(() => new SchnorrInitializerlessAccountContract(GrumpkinScalar.random())); + }); + describe('ecdsa stored-key account', () => { itShouldBehaveLikeAnAccountContract(() => new EcdsaKAccountContract(randomBytes(32))); }); diff --git a/yarn-project/end-to-end/src/e2e_authwit.test.ts b/yarn-project/end-to-end/src/e2e_authwit.test.ts index bc31ce0e99d2..546cd7fa60d6 100644 --- a/yarn-project/end-to-end/src/e2e_authwit.test.ts +++ b/yarn-project/end-to-end/src/e2e_authwit.test.ts @@ -10,12 +10,7 @@ import { jest } from '@jest/globals'; import { sendThroughAuthwitProxy } from './fixtures/authwit_proxy.js'; import { AUTOMINE_E2E_OPTS, DUPLICATE_NULLIFIER_ERROR } from './fixtures/fixtures.js'; -import { - type EndToEndContext, - ensureAccountContractsPublished, - ensureAuthRegistryPublished, - setup, -} from './fixtures/utils.js'; +import { type EndToEndContext, ensureAuthRegistryPublished, setup } from './fixtures/utils.js'; import type { TestWallet } from './test-wallet/test_wallet.js'; const TIMEOUT = 300_000; @@ -37,7 +32,6 @@ describe('e2e_authwit_tests', () => { wallet, accounts: [account1Address, account2Address], } = await setup(2, { ...AUTOMINE_E2E_OPTS })); - await ensureAccountContractsPublished(wallet, [account1Address, account2Address]); await ensureAuthRegistryPublished(wallet, account1Address); ({ contract: auth } = await AuthWitTestContract.deploy(wallet).send({ from: account1Address })); diff --git a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts index 9b344c1a424b..554e336ea56a 100644 --- a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts +++ b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts @@ -10,7 +10,7 @@ import { AvmTestContract } from '@aztec/noir-test-contracts.js/AvmTest'; import { jest } from '@jest/globals'; import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; -import { ensureAccountContractsPublished, setup } from './fixtures/utils.js'; +import { setup } from './fixtures/utils.js'; const TIMEOUT = 600_000; @@ -29,7 +29,6 @@ describe('e2e_avm_simulator', () => { aztecNode, accounts: [defaultAccountAddress], } = await setup(1, { ...AUTOMINE_E2E_OPTS })); - await ensureAccountContractsPublished(wallet, [defaultAccountAddress]); }); afterAll(() => teardown()); diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts index 8065156fa0a8..1639de4c38d3 100644 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts @@ -17,9 +17,7 @@ import { jest } from '@jest/globals'; import { type EndToEndContext, type SetupOptions, - deployAccounts, ensureAuthRegistryPublished, - publicDeployAccounts, setup, teardown, } from '../fixtures/setup.js'; @@ -96,28 +94,14 @@ export class BlacklistTokenContractTest { // proxy deploys exceed the original window. jest.setTimeout(600_000); - this.logger.info('Deploying 3 accounts'); - const { deployedAccounts } = await deployAccounts( - 3, - this.logger, - )({ - wallet: this.context.wallet, - initialFundedAccounts: this.context.initialFundedAccounts, - }); - this.cheatCodes = this.context.cheatCodes; this.aztecNode = this.context.aztecNodeService; this.sequencer = this.context.sequencer!; this.wallet = this.context.wallet; - this.adminAddress = deployedAccounts[0].address; - this.otherAddress = deployedAccounts[1].address; - this.blacklistedAddress = deployedAccounts[2].address; + [this.adminAddress, this.otherAddress, this.blacklistedAddress] = this.context.accounts; this.logger.info('Setting up blacklist token contract'); await ensureAuthRegistryPublished(this.wallet, this.adminAddress); - // Create the token contract state. - this.logger.verbose(`Public deploy accounts...`); - await publicDeployAccounts(this.wallet, [this.adminAddress, this.otherAddress, this.blacklistedAddress]); this.logger.verbose(`Deploying TokenContract...`); ({ contract: this.asset } = await TokenBlacklistContract.deploy(this.wallet, this.adminAddress).send({ @@ -157,10 +141,9 @@ export class BlacklistTokenContractTest { async setup(opts: Partial = {}) { this.logger.info('Setting up fresh context'); - this.context = await setup(0, { + this.context = await setup(3, { ...opts, fundSponsoredFPC: true, - skipAccountDeployment: true, }); await this.applyBaseSetup(); } diff --git a/yarn-project/end-to-end/src/e2e_block_building.test.ts b/yarn-project/end-to-end/src/e2e_block_building.test.ts index b5b2645777e0..a66733f457f7 100644 --- a/yarn-project/end-to-end/src/e2e_block_building.test.ts +++ b/yarn-project/end-to-end/src/e2e_block_building.test.ts @@ -1,3 +1,4 @@ +import { generateSchnorrAccounts } from '@aztec/accounts/testing'; import { NO_FROM } from '@aztec/aztec.js/account'; import { AztecAddress } from '@aztec/aztec.js/addresses'; import { BatchCall, ContractFunctionInteraction, type DeployOptions, NO_WAIT } from '@aztec/aztec.js/contracts'; @@ -514,11 +515,15 @@ describe('e2e_block_building', () => { // Regression for https://github.com/AztecProtocol/aztec-packages/issues/7537 it('sends a tx on the first block', async () => { - const context = await setup(0, { ...PIPELINING_SETUP_OPTS, minTxsPerBlock: 0, numberOfInitialFundedAccounts: 1 }); + const context = await setup(0, { + ...PIPELINING_SETUP_OPTS, + minTxsPerBlock: 0, + additionallyFundedAccounts: await generateSchnorrAccounts(1, 'schnorr'), + }); ({ teardown, logger, aztecNode, wallet } = context); await sleep(1000); - const [accountData] = context.initialFundedAccounts; + const [accountData] = context.additionallyFundedAccounts; const accountManager = await (wallet as TestWallet).createSchnorrAccount(accountData.secret, accountData.salt); const deployMethod = await accountManager.getDeployMethod(); @@ -563,7 +568,7 @@ describe('e2e_block_building', () => { // which translates in an incorrect end state for world state. We can easily detect this by checking whether the nullifier // tree next available leaf index is a multiple of 64. it('clears up all nullifiers if tx processing fails', async () => { - const context = await setup(1, { ...PIPELINING_SETUP_OPTS, minTxsPerBlock: 1, numberOfInitialFundedAccounts: 1 }); + const context = await setup(1, { ...PIPELINING_SETUP_OPTS, minTxsPerBlock: 1 }); ({ teardown, logger, diff --git a/yarn-project/end-to-end/src/e2e_bot.test.ts b/yarn-project/end-to-end/src/e2e_bot.test.ts index 936a08a27000..514746bea348 100644 --- a/yarn-project/end-to-end/src/e2e_bot.test.ts +++ b/yarn-project/end-to-end/src/e2e_bot.test.ts @@ -1,9 +1,7 @@ import { getInitialTestAccountsData } from '@aztec/accounts/testing'; -import { NO_FROM } from '@aztec/aztec.js/account'; import { Fr } from '@aztec/aztec.js/fields'; import type { AztecNode } from '@aztec/aztec.js/node'; import { MinedTxReceipt, type TxReceipt } from '@aztec/aztec.js/tx'; -import { DeployAccountMethod } from '@aztec/aztec.js/wallet'; import type { CheatCodes } from '@aztec/aztec/testing'; import { AmmBot, @@ -40,7 +38,7 @@ describe('e2e_bot', () => { const setupResult = await setup(0, { ...PIPELINING_SETUP_OPTS, aztecProofSubmissionEpochs: 640, - initialFundedAccounts: [botAccount], + additionallyFundedAccounts: [botAccount], }); ({ teardown, @@ -50,9 +48,7 @@ describe('e2e_bot', () => { config: { l1RpcUrls }, } = setupResult); wallet = await EmbeddedWallet.create(aztecNode, { ephemeral: true }); - const accountManager = await wallet.createSchnorrAccount(botAccount.secret, botAccount.salt, botAccount.signingKey); - const deployMethod = await accountManager.getDeployMethod(); - await deployMethod.send({ from: NO_FROM }); + await wallet.createSchnorrInitializerlessAccount(botAccount.secret, botAccount.salt, botAccount.signingKey); }); afterAll(() => teardown()); @@ -151,19 +147,19 @@ describe('e2e_bot', () => { }; { - using deploy = jest.spyOn(DeployAccountMethod.prototype, 'send'); - - deploy.mockImplementation(() => { + using sendTx = jest.spyOn(EmbeddedWallet.prototype, 'sendTx'); + // Fail the fee juice top-up tx, which runs after the bridge claim has been persisted. + sendTx.mockImplementation(() => { throw new Error('test error'); }); await expect(Bot.create(config, wallet, aztecNode, aztecNodeAdmin, store)).rejects.toThrow('test error'); - expect(deploy).toHaveBeenCalledOnce(); expect(saveSpy).toHaveBeenCalledOnce(); } { saveSpy.mockClear(); + // The persisted claim is reused for the top-up, so no new claim is bridged or saved. await expect(Bot.create(config, wallet, aztecNode, aztecNodeAdmin, store)).resolves.toBeDefined(); expect(saveSpy).not.toHaveBeenCalled(); } @@ -193,8 +189,8 @@ describe('e2e_bot', () => { }; { - using deploy = jest.spyOn(DeployAccountMethod.prototype, 'send'); - deploy.mockImplementation(() => { + using sendTx = jest.spyOn(EmbeddedWallet.prototype, 'sendTx'); + sendTx.mockImplementation(() => { throw new Error('test error'); }); await expect(Bot.create(config, wallet, aztecNode, aztecNodeAdmin, store)).rejects.toThrow('test error'); @@ -203,7 +199,8 @@ describe('e2e_bot', () => { { saveSpy.mockClear(); - // same private key, but different salt derives a different L2 address + // same private key, but different salt derives a different L2 address, so the persisted claim does + // not apply and a fresh claim is bridged and saved config.senderSalt = config.senderSalt!.add(Fr.ONE); await expect(Bot.create(config, wallet, aztecNode, aztecNodeAdmin, store)).resolves.toBeDefined(); expect(saveSpy).toHaveBeenCalledOnce(); diff --git a/yarn-project/end-to-end/src/e2e_card_game.test.ts b/yarn-project/end-to-end/src/e2e_card_game.test.ts index 5c6741a707b5..1c9b9a00653a 100644 --- a/yarn-project/end-to-end/src/e2e_card_game.test.ts +++ b/yarn-project/end-to-end/src/e2e_card_game.test.ts @@ -1,3 +1,4 @@ +import { generateSchnorrAccounts } from '@aztec/accounts/testing'; import { AztecAddress } from '@aztec/aztec.js/addresses'; import type { GrumpkinScalar } from '@aztec/aztec.js/fields'; import { computeAppNullifierHidingKey, deriveMasterNullifierHidingKey } from '@aztec/aztec.js/keys'; @@ -11,6 +12,7 @@ import { jest } from '@jest/globals'; import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; import { setup } from './fixtures/utils.js'; +import type { TestWallet } from './test-wallet/test_wallet.js'; /* eslint-disable camelcase */ @@ -60,7 +62,7 @@ describe('e2e_card_game', () => { let logger: Logger; let teardown: () => Promise; - let wallet: Wallet; + let wallet: TestWallet; let masterNullifierHidingKeys: GrumpkinScalar[]; let firstPlayer: AztecAddress; @@ -87,14 +89,18 @@ describe('e2e_card_game', () => { }; beforeAll(async () => { - const context = await setup(3, { ...AUTOMINE_E2E_OPTS }); + // This test derives nullifier-hiding keys from the players' secrets, so we provide and create the + // accounts ourselves. + const players = await generateSchnorrAccounts(3); + const context = await setup(0, { ...AUTOMINE_E2E_OPTS, additionallyFundedAccounts: players }); ({ logger, teardown, wallet } = context); - [firstPlayer, secondPlayer, thirdPlayer] = context.accounts; + for (const player of players) { + await wallet.createSchnorrInitializerlessAccount(player.secret, player.salt, player.signingKey); + } + [firstPlayer, secondPlayer, thirdPlayer] = players.map(p => p.address); - masterNullifierHidingKeys = context.initialFundedAccounts.map(({ secret }) => - deriveMasterNullifierHidingKey(secret), - ); + masterNullifierHidingKeys = players.map(({ secret }) => deriveMasterNullifierHidingKey(secret)); }); beforeEach(async () => { diff --git a/yarn-project/end-to-end/src/e2e_circuit_recorder.test.ts b/yarn-project/end-to-end/src/e2e_circuit_recorder.test.ts index e806f61dfe0c..8d3e65140dad 100644 --- a/yarn-project/end-to-end/src/e2e_circuit_recorder.test.ts +++ b/yarn-project/end-to-end/src/e2e_circuit_recorder.test.ts @@ -1,4 +1,5 @@ import { MAX_APPS_PER_KERNEL } from '@aztec/constants'; +import { ChildContract } from '@aztec/noir-test-contracts.js/Child'; import fs from 'fs/promises'; import path from 'path'; @@ -16,8 +17,14 @@ describe('Circuit Recorder', () => { // Set recording directory env var - this will activate the circuit recorder process.env.CIRCUIT_RECORD_DIR = RECORD_DIR; - // Run setup which deploys an account contract and runs kernels - const { teardown } = await setup(1, { ...AUTOMINE_E2E_OPTS }); + // setup creates an initializerless account, which has no deployment tx. Deploying a contract from + // it exercises the account entrypoint (a user circuit) and the private kernels (protocol circuits) + const { + teardown, + wallet, + accounts: [accountAddress], + } = await setup(1, { ...AUTOMINE_E2E_OPTS }); + await ChildContract.deploy(wallet).send({ from: accountAddress }); // Check recording directory exists const dirExists = await fs.stat(RECORD_DIR).then( @@ -26,20 +33,20 @@ describe('Circuit Recorder', () => { ); expect(dirExists).toBe(true); - // Check recording file of a user circuit (contract circuit) exists and has expected content + // Check recording file of a user circuit (the account contract entrypoint) exists and has expected content { const files = await fs.readdir(RECORD_DIR); expect(files.length).toBeGreaterThan(0); - const recordingFile = files.find(f => f.startsWith('SchnorrAccount_constructor')); + const recordingFile = files.find(f => f.startsWith('SchnorrInitializerlessAccount_entrypoint')); expect(recordingFile).toBeDefined(); const recordingContent = await fs.readFile(path.join(RECORD_DIR, recordingFile!), 'utf8'); const recording = JSON.parse(recordingContent); expect(recording).toMatchObject({ - circuitName: 'SchnorrAccount', - functionName: 'constructor', + circuitName: 'SchnorrInitializerlessAccount', + functionName: 'entrypoint', inputs: expect.any(Object), oracleCalls: expect.any(Array), }); diff --git a/yarn-project/end-to-end/src/e2e_contract_updates.test.ts b/yarn-project/end-to-end/src/e2e_contract_updates.test.ts index ab2aa16ac1ee..0780a440151c 100644 --- a/yarn-project/end-to-end/src/e2e_contract_updates.test.ts +++ b/yarn-project/end-to-end/src/e2e_contract_updates.test.ts @@ -1,9 +1,8 @@ -import { getSchnorrAccountContractAddress } from '@aztec/accounts/schnorr'; +import { getSchnorrInitializerlessAccountContractAddress } from '@aztec/accounts/schnorr'; import { fastForwardContractUpdate, getContractClassFromArtifact } from '@aztec/aztec.js/contracts'; import { publishContractClass } from '@aztec/aztec.js/deployment'; import { Fr } from '@aztec/aztec.js/fields'; import type { AztecNode } from '@aztec/aztec.js/node'; -import type { Wallet } from '@aztec/aztec.js/wallet'; import type { CheatCodes } from '@aztec/aztec/testing'; import { MINIMUM_UPDATE_DELAY, UPDATED_CLASS_IDS_SLOT } from '@aztec/constants'; import { UpdatableContract } from '@aztec/noir-test-contracts.js/Updatable'; @@ -23,6 +22,7 @@ import { PublicDataTreeLeaf } from '@aztec/stdlib/trees'; import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; import { setup } from './fixtures/utils.js'; +import type { TestWallet } from './test-wallet/test_wallet.js'; // Set the update delay in genesis data so it's feasible to test in an e2e test. // The protocol enforces `MINIMUM_UPDATE_DELAY` (600 seconds, see constants.gen.ts), so we use that @@ -35,7 +35,7 @@ const INITIAL_UPDATABLE_CONTRACT_VALUE = 1n; const UPDATED_CONTRACT_PUBLIC_VALUE = 27n; describe('e2e_contract_updates', () => { - let wallet: Wallet; + let wallet: TestWallet; let defaultAccountAddress: AztecAddress; let teardown: () => Promise; let contract: UpdatableContract; @@ -80,29 +80,26 @@ describe('e2e_contract_updates', () => { const senderPrivateKey = Fr.random(); const signingKey = deriveSigningKey(senderPrivateKey); const salt = Fr.ONE; - const initialFundedAccounts = [ - { - secret: senderPrivateKey, - signingKey, - salt, - address: await getSchnorrAccountContractAddress(senderPrivateKey, salt, signingKey), - }, - ]; + // Use a deterministic initializerless account whose address we know before setup, so the scheduled + // delay can be seeded in genesis public data for it. We fund it and create it ourselves below. + const account = { + secret: senderPrivateKey, + signingKey, + salt, + type: 'schnorr_initializerless' as const, + address: await getSchnorrInitializerlessAccountContractAddress(senderPrivateKey, salt, signingKey), + }; + defaultAccountAddress = account.address; const constructorArgs = [INITIAL_UPDATABLE_CONTRACT_VALUE]; - const genesisPublicData = await setupScheduledDelay(constructorArgs, salt, initialFundedAccounts[0].address); - - ({ - aztecNode, - teardown, - wallet, - accounts: [defaultAccountAddress], - cheatCodes, - } = await setup(1, { + const genesisPublicData = await setupScheduledDelay(constructorArgs, salt, account.address); + + ({ aztecNode, teardown, wallet, cheatCodes } = await setup(0, { ...AUTOMINE_E2E_OPTS, genesisPublicData, - initialFundedAccounts, + additionallyFundedAccounts: [account], })); + await wallet.createSchnorrInitializerlessAccount(account.secret, account.salt, account.signingKey); ({ contract, instance } = await UpdatableContract.deploy(wallet, constructorArgs[0], { salt }).send({ from: defaultAccountAddress, diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts index 8b1555a732f3..fd2bae106b54 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts @@ -28,9 +28,7 @@ import { MNEMONIC } from '../fixtures/fixtures.js'; import { type EndToEndContext, type SetupOptions, - deployAccounts, ensureAuthRegistryPublished, - publicDeployAccounts, setup, teardown, } from '../fixtures/setup.js'; @@ -97,12 +95,11 @@ export class CrossChainMessagingTest { // Recompute requireEpochProven from the merged options so per-call startProverNode is honored. this.requireEpochProven = opts.startProverNode ?? this.setupOptions.startProverNode ?? false; this.context = await setup( - 0, + 3, { ...this.setupOptions, ...opts, fundSponsoredFPC: true, - skipAccountDeployment: true, l1ContractsArgs: { ...this.deployL1ContractsArgs, ...opts.l1ContractsArgs }, // `advanceToEpochProven` warps anvil's L1 clock forward by up to a full epoch in one // step. The prover-node tracks L1 time via `dateProvider.setTime(...)`, so any @@ -176,24 +173,12 @@ export class CrossChainMessagingTest { await this.epochTestSettler.start(); } - // Deploy 3 accounts - this.logger.info('Applying 3_accounts setup'); - const { deployedAccounts } = await deployAccounts( - 3, - this.logger, - )({ - wallet: this.context.wallet, - initialFundedAccounts: this.context.initialFundedAccounts, - }); - [this.ownerAddress, this.user1Address, this.user2Address] = deployedAccounts.map(a => a.address); + [this.ownerAddress, this.user1Address, this.user2Address] = this.context.accounts; // Set up cross chain messaging this.logger.info('Applying e2e_cross_chain_messaging setup'); await ensureAuthRegistryPublished(this.wallet, this.ownerAddress); - // Create the token contract state. - this.logger.verbose(`Public deploy accounts...`); - await publicDeployAccounts(this.wallet, [this.ownerAddress, this.user1Address, this.user2Address]); this.l1Client = createExtendedL1Client(this.aztecNodeConfig.l1RpcUrls, MNEMONIC); diff --git a/yarn-project/end-to-end/src/e2e_custom_message.test.ts b/yarn-project/end-to-end/src/e2e_custom_message.test.ts index 34fa17f1c901..464894af2ee3 100644 --- a/yarn-project/end-to-end/src/e2e_custom_message.test.ts +++ b/yarn-project/end-to-end/src/e2e_custom_message.test.ts @@ -8,7 +8,7 @@ import { CustomMessageContract, type MultiLogEvent } from '@aztec/noir-test-cont import { jest } from '@jest/globals'; import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; -import { ensureAccountContractsPublished, setup } from './fixtures/utils.js'; +import { setup } from './fixtures/utils.js'; const TIMEOUT = 300_000; @@ -26,7 +26,6 @@ describe('CustomMessage - Multi-Log Pattern', () => { wallet, accounts: [account], } = await setup(1, { ...AUTOMINE_E2E_OPTS })); - await ensureAccountContractsPublished(wallet, [account]); ({ contract } = await CustomMessageContract.deploy(wallet).send({ from: account })); }); diff --git a/yarn-project/end-to-end/src/e2e_debug_trace.test.ts b/yarn-project/end-to-end/src/e2e_debug_trace.test.ts index a9784e6aca9f..fbb3282245a2 100644 --- a/yarn-project/end-to-end/src/e2e_debug_trace.test.ts +++ b/yarn-project/end-to-end/src/e2e_debug_trace.test.ts @@ -120,7 +120,7 @@ describe('e2e_debug_trace_transaction', () => { } }); - expect(await aztecNode.getBlockNumber()).toBeGreaterThanOrEqual(2); + expect(await aztecNode.getBlockNumber()).toBeGreaterThanOrEqual(1); // The current config requires at least 1 tx per block, so the block number won't be increasing @@ -242,7 +242,7 @@ describe('e2e_debug_trace_transaction', () => { } }); - expect(await aztecNode.getBlockNumber()).toBeGreaterThanOrEqual(2); + expect(await aztecNode.getBlockNumber()).toBeGreaterThanOrEqual(1); const numBlocksToMine = 3; const startBlockNumber = await aztecNode.getBlockNumber(); diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_test.ts index aeaf3da2d6fe..abf70734c12d 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_test.ts @@ -9,7 +9,7 @@ import type { Wallet } from '@aztec/aztec.js/wallet'; import type { StatefulTestContract } from '@aztec/noir-test-contracts.js/StatefulTest'; import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; -import { type EndToEndContext, type SetupOptions, deployAccounts, setup, teardown } from '../fixtures/setup.js'; +import { type EndToEndContext, type SetupOptions, setup, teardown } from '../fixtures/setup.js'; import type { TestWallet } from '../test-wallet/test_wallet.js'; export class DeployTest { @@ -26,15 +26,14 @@ export class DeployTest { async setup(opts: Partial = {}) { this.logger.info('Setting up test environment'); - this.context = await setup(0, { + this.context = await setup(1, { ...opts, fundSponsoredFPC: true, - skipAccountDeployment: true, }); this.aztecNode = this.context.aztecNodeService; this.wallet = this.context.wallet; this.aztecNodeAdmin = this.context.aztecNodeService; - await this.applyInitialAccount(); + this.defaultAccountAddress = this.context.accounts[0]; return this; } @@ -42,18 +41,6 @@ export class DeployTest { await teardown(this.context); } - private async applyInitialAccount() { - this.logger.info('Applying initial account setup'); - const { deployedAccounts } = await deployAccounts( - 1, - this.logger, - )({ - wallet: this.context.wallet, - initialFundedAccounts: this.context.initialFundedAccounts, - }); - this.defaultAccountAddress = deployedAccounts[0].address; - } - async registerContract( wallet: Wallet, contractArtifact: ContractArtifactClass, diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.parallel.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.parallel.test.ts index 03df1e414ad5..bdca7c7cdd20 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.parallel.test.ts @@ -327,7 +327,12 @@ describe('e2e_epochs/epochs_mbps', () => { }); it('builds multiple blocks per slot with L1 to L2 messages', async () => { - await setupTest({ syncChainTip: 'proposed', minTxsPerBlock: 1, maxTxsPerBlock: 1 }); + // L1→L2 messages only become ready once the chain advances `inboxLag` checkpoints past where they + // were inboxed, and a checkpoint only advances when a block is built in a new slot. With + // skipInitialSequencer the chain won't move on its own, and a one-shot burst of filler txs lands + // within a single checkpoint — so let the sequencer keep building (empty) blocks each slot to drive + // the chain forward until the messages are ready. + await setupTest({ syncChainTip: 'proposed', minTxsPerBlock: 0, maxTxsPerBlock: 1, buildCheckpointIfEmpty: true }); // Start sequencers first, then deploy cross-chain contract (needs running sequencer to mine). await Promise.all(nodes.map(n => n.getSequencer()!.start())); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts index b172be919708..315c7a5b7718 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts @@ -161,7 +161,7 @@ export class EpochsTestContext { exitDelaySeconds: DefaultL1ContractsConfig.exitDelaySeconds, slasherEnabled: false, ...opts, - ...(hardcodedAccountData ? { initialFundedAccounts: [hardcodedAccountData], numberOfAccounts: 0 } : {}), + ...(hardcodedAccountData ? { additionallyFundedAccounts: [hardcodedAccountData], numberOfAccounts: 0 } : {}), }, // Use checkpointed chain tip for PXE by default to avoid issues with blocks being dropped due to pruned anchor blocks. // Can be overridden via opts.pxeOpts. @@ -219,7 +219,7 @@ export class EpochsTestContext { /** * Computes InitialAccountData for a SchnorrHardcodedKeyAccountContract. * This contract has a hardcoded signing key and no initializer, so it can be used without - * on-chain deployment. Pass the returned data in `initialFundedAccounts` so the address + * on-chain deployment. Pass the returned data in `additionallyFundedAccounts` so the address * gets funded with fee juice in genesis. */ public static async getHardcodedAccountData(secret: Fr, salt: Fr): Promise { diff --git a/yarn-project/end-to-end/src/e2e_event_logs.test.ts b/yarn-project/end-to-end/src/e2e_event_logs.test.ts index 0afbd83f49f5..ce1410241cd8 100644 --- a/yarn-project/end-to-end/src/e2e_event_logs.test.ts +++ b/yarn-project/end-to-end/src/e2e_event_logs.test.ts @@ -18,7 +18,7 @@ import { import { jest } from '@jest/globals'; import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; -import { ensureAccountContractsPublished, setup } from './fixtures/utils.js'; +import { setup } from './fixtures/utils.js'; const TIMEOUT = 300_000; @@ -44,9 +44,6 @@ describe('Logs', () => { logger: log, } = await setup(2, { ...AUTOMINE_E2E_OPTS })); - log.warn(`Setup complete, checking account contracts published`); - await ensureAccountContractsPublished(wallet, [account1Address, account2Address]); - log.warn(`Deploying test contract`); ({ contract: testLogContract } = await TestLogContract.deploy(wallet).send({ from: account1Address })); }); diff --git a/yarn-project/end-to-end/src/e2e_event_only.test.ts b/yarn-project/end-to-end/src/e2e_event_only.test.ts index dda2710f7d06..e2d0a79b7ef1 100644 --- a/yarn-project/end-to-end/src/e2e_event_only.test.ts +++ b/yarn-project/end-to-end/src/e2e_event_only.test.ts @@ -7,7 +7,7 @@ import { EventOnlyContract, type TestEvent } from '@aztec/noir-test-contracts.js import { jest } from '@jest/globals'; import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; -import { ensureAccountContractsPublished, setup } from './fixtures/utils.js'; +import { setup } from './fixtures/utils.js'; const TIMEOUT = 300_000; @@ -26,7 +26,6 @@ describe('EventOnly', () => { wallet, accounts: [defaultAccountAddress], } = await setup(1, { ...AUTOMINE_E2E_OPTS })); - await ensureAccountContractsPublished(wallet, [defaultAccountAddress]); ({ contract: eventOnlyContract } = await EventOnlyContract.deploy(wallet).send({ from: defaultAccountAddress })); }); diff --git a/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts b/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts index b05bf4ce0edf..a8b3b341e0d4 100644 --- a/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts @@ -151,6 +151,7 @@ describe('e2e_fees account_init', () => { const paymentMethod = new PublicFeePaymentMethod(bananaFPC.address, bobsAddress, wallet, gasSettings); const { receipt: tx } = await bobsDeployMethod.send({ from: NO_FROM, + skipClassPublication: false, skipInstancePublication: false, fee: { paymentMethod }, }); diff --git a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts index e722ce0ec332..d3070051c968 100644 --- a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts @@ -27,9 +27,7 @@ import { MNEMONIC, getPaddedMaxFeesPerGas } from '../fixtures/fixtures.js'; import { type EndToEndContext, type SetupOptions, - deployAccounts, ensureAuthRegistryPublished, - publicDeployAccounts, setup, teardown, } from '../fixtures/setup.js'; @@ -109,12 +107,11 @@ export class FeesTest { this.logger.verbose('Setting up fresh context...'); // Token allowlist entries are test-only: FPC-based fee payment with custom tokens won't work on mainnet alpha. const tokenAllowList = await getTokenAllowedSetupFunctions(); - this.context = await setup(0, { + this.context = await setup(this.numberOfAccounts, { startProverNode: true, ...this.setupOptions, ...opts, fundSponsoredFPC: true, - skipAccountDeployment: true, l1ContractsArgs: { ...this.setupOptions }, txPublicSetupAllowListExtend: [...(this.setupOptions.txPublicSetupAllowListExtend ?? []), ...tokenAllowList], }); @@ -179,7 +176,6 @@ export class FeesTest { public async applyBaseSetup() { await this.applyInitialAccounts(); await this.applyEnsureAuthRegistryPublished(); - await this.applyPublicDeployAccounts(); await this.applySetupFeeJuice(); await this.applyDeployBananaToken(); } @@ -192,14 +188,6 @@ export class FeesTest { async applyInitialAccounts() { this.logger.info('Applying initial accounts setup'); - const { deployedAccounts } = await deployAccounts( - this.numberOfAccounts, - this.logger, - )({ - wallet: this.context.wallet, - initialFundedAccounts: this.context.initialFundedAccounts, - }); - this.wallet = this.context.wallet; this.aztecNode = this.context.aztecNodeService; this.aztecNodeAdmin = this.context.aztecNodeService; @@ -207,7 +195,7 @@ export class FeesTest { maxFeesPerGas: await getPaddedMaxFeesPerGas(this.aztecNode), }); this.cheatCodes = this.context.cheatCodes; - this.accounts = deployedAccounts.map(a => a.address); + this.accounts = this.context.accounts; this.accounts.forEach((a, i) => this.logger.verbose(`Account ${i} address: ${a}`)); [this.aliceAddress, this.bobAddress, this.sequencerAddress] = this.accounts.slice(0, 3); @@ -218,11 +206,6 @@ export class FeesTest { this.feeJuiceContract = FeeJuiceContract.at(canonicalFeeJuice.address, this.wallet); } - async applyPublicDeployAccounts() { - this.logger.info('Applying public deploy accounts setup'); - await publicDeployAccounts(this.wallet, this.accounts); - } - async applySetupFeeJuice() { this.logger.info('Applying fee juice setup'); diff --git a/yarn-project/end-to-end/src/e2e_genesis_timestamp.test.ts b/yarn-project/end-to-end/src/e2e_genesis_timestamp.test.ts index 1543890e7c59..d9a96bb69f8d 100644 --- a/yarn-project/end-to-end/src/e2e_genesis_timestamp.test.ts +++ b/yarn-project/end-to-end/src/e2e_genesis_timestamp.test.ts @@ -1,3 +1,4 @@ +import { generateSchnorrAccounts } from '@aztec/accounts/testing'; import { NO_FROM } from '@aztec/aztec.js/account'; import { createLogger } from '@aztec/aztec.js/log'; import { retryUntil } from '@aztec/foundation/retry'; @@ -12,19 +13,28 @@ describe('e2e_genesis_timestamp', () => { const logger = createLogger('e2e:genesis_timestamp'); beforeEach(async () => { - // Skip account deployment and configure PXE to sync its anchor only to proven blocks so its + // Configure PXE to sync its anchor only to proven blocks so its // anchor lags behind proposed blocks. Under AUTOMINE_E2E_OPTS the AnvilTestWatcher is disabled // and the AutomineSequencer never marks blocks as proven on its own, so without a prover node // the proven tip stays at genesis for the duration of the test. - context = await setup(0, { ...AUTOMINE_E2E_OPTS, skipAccountDeployment: true }, { syncChainTip: 'proven' }); + context = await setup( + 0, + { + ...AUTOMINE_E2E_OPTS, + advancePastGenesis: false, + // This test proves genesis-anchored account deployment txs, so it needs deployable accounts + additionallyFundedAccounts: await generateSchnorrAccounts(2, 'schnorr'), + }, + { syncChainTip: 'proven' }, + ); }); afterEach(() => context.teardown()); // Creates and proves a tx, and asserts it's anchored to the genesis block const proveTxAnchoredToGenesis = async (accountIndex = 0) => { - const { wallet, initialFundedAccounts } = context; - const { secret, salt, signingKey } = initialFundedAccounts[accountIndex]; + const { wallet, additionallyFundedAccounts } = context; + const { secret, salt, signingKey } = additionallyFundedAccounts[accountIndex]; const accountManager = await wallet.createSchnorrAccount(secret, salt, signingKey); const deployMethod = await accountManager.getDeployMethod(); const provenTx = await proveInteraction(wallet, deployMethod, { diff --git a/yarn-project/end-to-end/src/e2e_keys.test.ts b/yarn-project/end-to-end/src/e2e_keys.test.ts index 0890b1c721ed..a12b02623c2c 100644 --- a/yarn-project/end-to-end/src/e2e_keys.test.ts +++ b/yarn-project/end-to-end/src/e2e_keys.test.ts @@ -1,8 +1,7 @@ -import type { InitialAccountData } from '@aztec/accounts/testing'; +import { generateSchnorrAccounts } from '@aztec/accounts/testing'; import type { AztecAddress } from '@aztec/aztec.js/addresses'; import { Fr } from '@aztec/aztec.js/fields'; import type { AztecNode } from '@aztec/aztec.js/node'; -import type { Wallet } from '@aztec/aztec.js/wallet'; import { DomainSeparator, INITIAL_L2_BLOCK_NUM } from '@aztec/constants'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { poseidon2HashWithSeparator } from '@aztec/foundation/crypto/poseidon'; @@ -21,6 +20,7 @@ import { jest } from '@jest/globals'; import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; import { setup } from './fixtures/utils.js'; +import type { TestWallet } from './test-wallet/test_wallet.js'; const TIMEOUT = 300_000; @@ -33,22 +33,18 @@ describe('Keys', () => { let testContract: TestContract; let secret: Fr; - let wallet: Wallet; + let wallet: TestWallet; let defaultAccountAddress: AztecAddress; beforeAll(async () => { - let initialFundedAccounts: InitialAccountData[]; - ({ - aztecNode, - teardown, - wallet, - accounts: [defaultAccountAddress], - initialFundedAccounts, - } = await setup(1, { ...AUTOMINE_E2E_OPTS })); + // This test needs the account's secret, so we provide and create the account ourselves. + const [account] = await generateSchnorrAccounts(1); + ({ aztecNode, teardown, wallet } = await setup(0, { ...AUTOMINE_E2E_OPTS, additionallyFundedAccounts: [account] })); + await wallet.createSchnorrInitializerlessAccount(account.secret, account.salt, account.signingKey); + defaultAccountAddress = account.address; + secret = account.secret; ({ contract: testContract } = await TestContract.deploy(wallet).send({ from: defaultAccountAddress })); - - secret = initialFundedAccounts[0].secret; }); afterAll(() => teardown()); diff --git a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts index b03736c4f1f5..a629781d0c83 100644 --- a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts @@ -15,7 +15,7 @@ import { afterAll, jest } from '@jest/globals'; import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; import type { EndToEndContext } from './fixtures/setup.js'; import { mintTokensToPrivate } from './fixtures/token_utils.js'; -import { ensureAccountContractsPublished, ensureAuthRegistryPublished, setup } from './fixtures/utils.js'; +import { ensureAuthRegistryPublished, setup } from './fixtures/utils.js'; import { LendingAccount, LendingSimulator, TokenSimulator } from './simulators/index.js'; import type { TestWallet } from './test-wallet/test_wallet.js'; @@ -93,7 +93,6 @@ describe('e2e_lending_contract', () => { accounts: [defaultAccountAddress], } = ctx); ({ lendingContract, priceFeedContract, collateralAsset, stableCoin } = await deployContracts()); - await ensureAccountContractsPublished(wallet, [defaultAccountAddress]); await ensureAuthRegistryPublished(wallet, defaultAccountAddress); const rollup = new RollupContract( diff --git a/yarn-project/end-to-end/src/e2e_multi_eoa.test.ts b/yarn-project/end-to-end/src/e2e_multi_eoa.test.ts index 727bd706883a..088006a88200 100644 --- a/yarn-project/end-to-end/src/e2e_multi_eoa.test.ts +++ b/yarn-project/end-to-end/src/e2e_multi_eoa.test.ts @@ -95,6 +95,16 @@ describe('e2e_multi_eoa', () => { sequencer = sequencerClient! as TestSequencerClient; publisherManager = sequencer.publisherManager; aztecNodeAdmin = maybeAztecNodeAdmin!; + + // Initializerless accounts deploy nothing during setup, so the chain sits at the single empty + // genesis block (one publisher used). Send a couple of txs from the default account so the + // sequencer publishes more blocks across rotated publishers. + for (let i = 0; i < 2; i++) { + await StatefulTestContract.deploy(wallet, defaultAccountAddress, 0, { + salt: Fr.random(), + deployer: defaultAccountAddress, + }).send({ from: defaultAccountAddress }); + } }); beforeEach(async () => { diff --git a/yarn-project/end-to-end/src/e2e_multi_validator/e2e_multi_validator_node.test.ts b/yarn-project/end-to-end/src/e2e_multi_validator/e2e_multi_validator_node.test.ts index 5b662846434c..368766eb8f9b 100644 --- a/yarn-project/end-to-end/src/e2e_multi_validator/e2e_multi_validator_node.test.ts +++ b/yarn-project/end-to-end/src/e2e_multi_validator/e2e_multi_validator_node.test.ts @@ -1,4 +1,4 @@ -import type { InitialAccountData } from '@aztec/accounts/testing'; +import { type InitialAccountData, generateSchnorrAccounts } from '@aztec/accounts/testing'; import type { Archiver } from '@aztec/archiver'; import type { AztecNodeConfig, AztecNodeService } from '@aztec/aztec-node'; import { NO_FROM } from '@aztec/aztec.js/account'; @@ -42,7 +42,7 @@ describe('e2e_multi_validator_node', () => { let teardown: () => Promise; let wallet: TestWallet; let ownerAddress: AztecAddress; - let initialFundedAccounts: InitialAccountData[]; + let additionallyFundedAccounts: InitialAccountData[]; let aztecNode: AztecNode; let config: AztecNodeConfig; let logger: Logger; @@ -75,9 +75,9 @@ describe('e2e_multi_validator_node', () => { }); const { aztecSlotDuration: _aztecSlotDuration } = getL1ContractsConfigEnvVars(); - ({ teardown, logger, wallet, initialFundedAccounts, aztecNode, config, deployL1ContractsValues, cheatCodes } = + ({ teardown, logger, wallet, additionallyFundedAccounts, aztecNode, config, deployL1ContractsValues, cheatCodes } = await setup( - 1, + 0, { ...PIPELINING_SETUP_OPTS, initialValidators, @@ -87,7 +87,8 @@ describe('e2e_multi_validator_node', () => { sequencerPollingIntervalMS: 200, worldStateBlockCheckIntervalMS: 200, blockCheckIntervalMS: 200, - skipAccountDeployment: true, + // We deploy this account ourselves later, so fund it as a regular (deployable) schnorr account. + additionallyFundedAccounts: await generateSchnorrAccounts(1, 'schnorr'), }, { syncChainTip: 'checkpointed' }, )); @@ -114,7 +115,7 @@ describe('e2e_multi_validator_node', () => { }); const deployOwnerAccount = async () => { - const accountData = initialFundedAccounts[0]; + const accountData = additionallyFundedAccounts[0]; const accountManager = await wallet.createSchnorrAccount( accountData.secret, accountData.salt, diff --git a/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts b/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts index b8948b1c5f46..ba3b05b5d82a 100644 --- a/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts +++ b/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts @@ -1,16 +1,16 @@ -import { getSchnorrAccountContractAddress } from '@aztec/accounts/schnorr'; +import { getSchnorrInitializerlessAccountContractAddress } from '@aztec/accounts/schnorr'; import { AztecAddress } from '@aztec/aztec.js/addresses'; import { Fr, GrumpkinScalar } from '@aztec/aztec.js/fields'; import type { Logger } from '@aztec/aztec.js/log'; -import type { Wallet } from '@aztec/aztec.js/wallet'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; import { deployToken, expectTokenBalance } from './fixtures/token_utils.js'; import { setup } from './fixtures/utils.js'; +import type { TestWallet } from './test-wallet/test_wallet.js'; describe('e2e_multiple_accounts_1_enc_key', () => { - let wallet: Wallet; + let wallet: TestWallet; let accounts: AztecAddress[] = []; let logger: Logger; let teardown: () => Promise; @@ -24,26 +24,33 @@ describe('e2e_multiple_accounts_1_enc_key', () => { // A shared secret for all accounts. const secret = Fr.random(); - const initialFundedAccounts = await Promise.all( + // These accounts share one encryption key but use different signing keys, so we build and create them + // ourselves (the default setup accounts each use a distinct secret). They are initializerless. + const accountsData = await Promise.all( Array.from({ length: numAccounts }).map(async () => { // A different signing key for each account. const signingKey = GrumpkinScalar.random(); const salt = Fr.random(); - const address = await getSchnorrAccountContractAddress(secret, salt, signingKey); + const address = await getSchnorrInitializerlessAccountContractAddress(secret, salt, signingKey); return { secret, signingKey, salt, + type: 'schnorr_initializerless' as const, address, }; }), ); - ({ teardown, logger, wallet, accounts } = await setup(numAccounts, { + ({ teardown, logger, wallet } = await setup(0, { ...AUTOMINE_E2E_OPTS, - initialFundedAccounts, + additionallyFundedAccounts: accountsData, })); - logger.info('Account contracts deployed'); + for (const a of accountsData) { + await wallet.createSchnorrInitializerlessAccount(a.secret, a.salt, a.signingKey); + } + accounts = accountsData.map(a => a.address); + logger.info('Account contracts created'); ({ contract: token } = await deployToken(wallet, accounts[0], initialBalance, logger)); }); diff --git a/yarn-project/end-to-end/src/e2e_nested_contract/nested_contract_test.ts b/yarn-project/end-to-end/src/e2e_nested_contract/nested_contract_test.ts index d68dcc808b16..fb586da297fe 100644 --- a/yarn-project/end-to-end/src/e2e_nested_contract/nested_contract_test.ts +++ b/yarn-project/end-to-end/src/e2e_nested_contract/nested_contract_test.ts @@ -5,14 +5,7 @@ import type { Wallet } from '@aztec/aztec.js/wallet'; import { ChildContract } from '@aztec/noir-test-contracts.js/Child'; import { ParentContract } from '@aztec/noir-test-contracts.js/Parent'; -import { - type EndToEndContext, - type SetupOptions, - deployAccounts, - publicDeployAccounts, - setup, - teardown as teardownSubsystems, -} from '../fixtures/setup.js'; +import { type EndToEndContext, type SetupOptions, setup, teardown as teardownSubsystems } from '../fixtures/setup.js'; export class NestedContractTest { context!: EndToEndContext; @@ -31,34 +24,15 @@ export class NestedContractTest { this.logger = createLogger(`e2e:e2e_nested_contract:${testName}`); } - /** - * Applies base setup by deploying accounts and publicly deploying them. - */ - async applyBaseSetup() { - this.logger.info('Deploying accounts'); - const { deployedAccounts } = await deployAccounts( - this.numberOfAccounts, - this.logger, - )({ - wallet: this.context.wallet, - initialFundedAccounts: this.context.initialFundedAccounts, - }); - this.wallet = this.context.wallet; - [{ address: this.defaultAccountAddress }] = deployedAccounts; - this.aztecNode = this.context.aztecNodeService; - - this.logger.info('Public deploy accounts'); - await publicDeployAccounts(this.wallet, [this.defaultAccountAddress]); - } - async setup(opts: Partial = {}) { this.logger.info('Setting up fresh subsystems'); - this.context = await setup(0, { + this.context = await setup(this.numberOfAccounts, { ...opts, fundSponsoredFPC: true, - skipAccountDeployment: true, }); - await this.applyBaseSetup(); + this.wallet = this.context.wallet; + this.defaultAccountAddress = this.context.accounts[0]; + this.aztecNode = this.context.aztecNodeService; } async teardown() { diff --git a/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts b/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts index 3ed31f0279e9..c222a06dd5b9 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts @@ -1,6 +1,5 @@ import { type InitialAccountData, getInitialTestAccountsData } from '@aztec/accounts/testing'; import type { AztecNodeService } from '@aztec/aztec-node'; -import { NO_FROM } from '@aztec/aztec.js/account'; import { EthAddress } from '@aztec/aztec.js/addresses'; import { waitForProven } from '@aztec/aztec.js/contracts'; import { generateClaimSecret } from '@aztec/aztec.js/ethereum'; @@ -291,11 +290,10 @@ describe('e2e_p2p_add_rollup', () => { { ...getPXEConfig(), proverEnabled: false, syncChainTip: 'checkpointed' }, { loggerActorLabel: 'pxe-bridge' }, ); - const aliceAccountManager = await wallet.createSchnorrAccount(aliceAccount.secret, aliceAccount.salt); - const aliceDeploymethod = await aliceAccountManager.getDeployMethod(); - await aliceDeploymethod.send({ - from: NO_FROM, - }); + const aliceAccountManager = await wallet.createSchnorrInitializerlessAccount( + aliceAccount.secret, + aliceAccount.salt, + ); const aliceAddress = aliceAccountManager.address; @@ -433,7 +431,7 @@ describe('e2e_p2p_add_rollup', () => { await bridging( nodes[0], - t.ctx.initialFundedAccounts[0], + t.ctx.additionallyFundedAccounts[0], t.ctx.deployL1ContractsValues.l1Client, t.ctx.deployL1ContractsValues.l1ContractAddresses, BigInt(t.ctx.aztecNodeConfig.rollupVersion), diff --git a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts index e912de390a05..8c3691b6c026 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts @@ -125,7 +125,6 @@ export class P2PNetworkTest { slasherEnabled: initialValidatorConfig.slasherEnabled ?? true, aztecTargetCommitteeSize: numberOfValidators, metricsPort: metricsPort, - numberOfInitialFundedAccounts: 2, startProverNode, }; @@ -368,17 +367,16 @@ export class P2PNetworkTest { address: await getAccountContractAddress(contract, secret, salt), }; - // Generate regular Schnorr accounts for tests that need deployable accounts (e.g. add_rollup). - const regularAccounts = await generateSchnorrAccounts(this.setupOptions.numberOfInitialFundedAccounts ?? 2); + // Generate funded (initializerless) Schnorr accounts for tests that create accounts from them (e.g. add_rollup). + const fundedAccounts = await generateSchnorrAccounts(2); this.context = await setup( 0, { ...this.setupOptions, fundSponsoredFPC: true, - skipAccountDeployment: true, skipInitialSequencer: true, - initialFundedAccounts: [...regularAccounts, this.hardcodedAccountData], + additionallyFundedAccounts: [...fundedAccounts, this.hardcodedAccountData], slasherEnabled: this.setupOptions.slasherEnabled ?? this.deployL1ContractsArgs.slasherEnabled ?? false, aztecTargetCommitteeSize: 0, l1ContractsArgs: this.deployL1ContractsArgs, @@ -389,7 +387,7 @@ export class P2PNetworkTest { this.ctx = this.context; const sponsoredFPCAddress = await getSponsoredFPCAddress(); - const initialFundedAccounts = [...this.context.initialFundedAccounts.map(a => a.address), sponsoredFPCAddress]; + const initialFundedAccounts = [...this.context.additionallyFundedAccounts.map(a => a.address), sponsoredFPCAddress]; const { genesis } = await getGenesisValues( initialFundedAccounts, diff --git a/yarn-project/end-to-end/src/e2e_public_testnet/e2e_public_testnet_transfer.test.ts b/yarn-project/end-to-end/src/e2e_public_testnet/e2e_public_testnet_transfer.test.ts index a8eb16753b3f..f128bb6c8984 100644 --- a/yarn-project/end-to-end/src/e2e_public_testnet/e2e_public_testnet_transfer.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_testnet/e2e_public_testnet_transfer.test.ts @@ -29,10 +29,9 @@ describe(`deploys and transfers a private only token`, () => { const chain = chainId == sepolia.id ? sepolia : foundry; // Not the best way of doing this. let accounts: AztecAddress[]; ({ logger, teardown, wallet, accounts } = await setup( - 2, // Deploy 2 accounts. + 2, // Create + fund 2 accounts. { ...PIPELINING_SETUP_OPTS, - numberOfInitialFundedAccounts: 2, // Fund 2 accounts. stateLoad: undefined, }, {}, diff --git a/yarn-project/end-to-end/src/e2e_sequencer_config.test.ts b/yarn-project/end-to-end/src/e2e_sequencer_config.test.ts index bf91c467acab..39ee841ea63d 100644 --- a/yarn-project/end-to-end/src/e2e_sequencer_config.test.ts +++ b/yarn-project/end-to-end/src/e2e_sequencer_config.test.ts @@ -1,5 +1,4 @@ import { getInitialTestAccountsData } from '@aztec/accounts/testing'; -import { NO_FROM } from '@aztec/aztec.js/account'; import type { AztecNode } from '@aztec/aztec.js/node'; import type { TxReceipt } from '@aztec/aztec.js/tx'; import { Bot, type BotConfig, BotStore, getBotDefaultConfig } from '@aztec/bot'; @@ -39,7 +38,7 @@ describe('e2e_sequencer_config', () => { ...PIPELINING_SETUP_OPTS, maxL2BlockGas: manaTarget * 2, manaTarget: BigInt(manaTarget), - initialFundedAccounts: [botAccount], + additionallyFundedAccounts: [botAccount], })); config = { ...getBotDefaultConfig(), @@ -51,13 +50,7 @@ describe('e2e_sequencer_config', () => { minFeePadding: PIPELINED_FEE_PADDING, }; wallet = await EmbeddedWallet.create(aztecNode, { ephemeral: true }); - const accountManager = await wallet.createSchnorrAccount( - botAccount.secret, - botAccount.salt, - botAccount.signingKey, - ); - const deployMethod = await accountManager.getDeployMethod(); - await deployMethod.send({ from: NO_FROM }); + await wallet.createSchnorrInitializerlessAccount(botAccount.secret, botAccount.salt, botAccount.signingKey); bot = await Bot.create(config, wallet, aztecNode, undefined, new BotStore(await openTmpStore('bot'))); }); diff --git a/yarn-project/end-to-end/src/e2e_synching.test.ts b/yarn-project/end-to-end/src/e2e_synching.test.ts index ed4cfb10adbe..c7541d76ecbc 100644 --- a/yarn-project/end-to-end/src/e2e_synching.test.ts +++ b/yarn-project/end-to-end/src/e2e_synching.test.ts @@ -31,7 +31,7 @@ * checkpointCount: 10, txCount: 36, complexity: PublicTransfer: {"numberOfBlocks":18, "syncTime":21.340179460525512} * checkpointCount: 10, txCount: 9, complexity: Spam: {"numberOfBlocks":17, "syncTime":49.40888188171387} */ -import type { InitialAccountData } from '@aztec/accounts/testing'; +import { type InitialAccountData, generateSchnorrAccounts } from '@aztec/accounts/testing'; import { createArchiver } from '@aztec/archiver'; import { AztecNodeService } from '@aztec/aztec-node'; import { NO_FROM } from '@aztec/aztec.js/account'; @@ -338,13 +338,15 @@ describe('e2e_synching', () => { aztecNode, wallet, accounts: [defaultAccountAddress], - initialFundedAccounts, + additionallyFundedAccounts, cheatCodes, } = await setup(1, { ...PIPELINING_SETUP_OPTS, l1StartTime: START_TIME, l2StartTime: START_TIME + 200 * ETHEREUM_SLOT_DURATION, - numberOfInitialFundedAccounts: variant.txCount + 1, + // These accounts are created+deployed as regular schnorr accounts (see deployAccounts), so fund them + // at their regular addresses. + additionallyFundedAccounts: await generateSchnorrAccounts(variant.txCount + 1, 'schnorr'), }); variant.setWallet(wallet); @@ -368,7 +370,7 @@ describe('e2e_synching', () => { sequencer?.updateConfig({ minTxsPerBlock: variant.txCount, maxTxsPerBlock: variant.txCount }); // The setup will mint tokens (private and public) - const accountsToBeDeployed = initialFundedAccounts.slice(1); // The first one has been deployed in setup. + const accountsToBeDeployed = additionallyFundedAccounts.slice(1); // The first one has been deployed in setup. await variant.setup(accountsToBeDeployed); for (let i = 0; i < variant.checkpointCount; i++) { @@ -421,12 +423,12 @@ describe('e2e_synching', () => { sequencer, watcher, wallet, - initialFundedAccounts, + additionallyFundedAccounts, dateProvider, } = await setup(0, { ...PIPELINING_SETUP_OPTS, l1StartTime: START_TIME, - numberOfInitialFundedAccounts: 10, + additionallyFundedAccounts: await generateSchnorrAccounts(10, 'schnorr'), }); await (aztecNode as any).stop(); @@ -493,7 +495,7 @@ describe('e2e_synching', () => { } await alternativeSync( - { deployL1ContractsValues, cheatCodes, config, logger, initialFundedAccounts, wallet }, + { deployL1ContractsValues, cheatCodes, config, logger, additionallyFundedAccounts, wallet }, variant, ); @@ -569,7 +571,9 @@ describe('e2e_synching', () => { const { wallet } = await setupPXEAndGetWallet(aztecNode!); variant.setWallet(wallet); - const defaultAccountAddress = (await variant.deployAccounts(opts.initialFundedAccounts!.slice(0, 1)))[0]; + const defaultAccountAddress = ( + await variant.deployAccounts(opts.additionallyFundedAccounts!.slice(0, 1)) + )[0]; contracts.push( ( diff --git a/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts b/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts index b9db38db8573..7fe1047e2cff 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts @@ -10,9 +10,7 @@ import { jest } from '@jest/globals'; import { type EndToEndContext, type SetupOptions, - deployAccounts, ensureAuthRegistryPublished, - publicDeployAccounts, setup, teardown, } from '../fixtures/setup.js'; @@ -71,23 +69,12 @@ export class TokenContractTest { // Adding a timeout of 2 minutes in here such that it is propagated to the underlying tests jest.setTimeout(120_000); - this.logger.info('Applying base setup - deploying 3 accounts'); - const { deployedAccounts } = await deployAccounts( - 3, - this.logger, - )({ - wallet: this.context.wallet, - initialFundedAccounts: this.context.initialFundedAccounts, - }); - this.node = this.context.aztecNodeService; this.wallet = this.context.wallet; - [this.adminAddress, this.account1Address, this.account2Address] = deployedAccounts.map(acc => acc.address); + [this.adminAddress, this.account1Address, this.account2Address] = this.context.accounts; this.logger.info('Applying base setup - deploying token contract'); await ensureAuthRegistryPublished(this.wallet, this.adminAddress); - this.logger.verbose(`Public deploy accounts...`); - await publicDeployAccounts(this.wallet, [this.adminAddress, this.account1Address]); this.logger.verbose(`Deploying TokenContract...`); ({ contract: this.asset } = await TokenContract.deploy( @@ -125,11 +112,10 @@ export class TokenContractTest { } async setup(opts: Partial = {}) { - this.context = await setup(0, { + this.context = await setup(3, { ...opts, metricsPort: this.metricsPort, fundSponsoredFPC: true, - skipAccountDeployment: true, }); if (this.shouldApplyBaseSetup) { diff --git a/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts b/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts index 906aa5b9ec20..83242518d906 100644 --- a/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts +++ b/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts @@ -1,4 +1,4 @@ -import type { InitialAccountData } from '@aztec/accounts/testing'; +import { type InitialAccountData, generateSchnorrAccounts } from '@aztec/accounts/testing'; import { AztecNodeService } from '@aztec/aztec-node'; import { AztecAddress, EthAddress } from '@aztec/aztec.js/addresses'; import { type Logger, createLogger } from '@aztec/aztec.js/log'; @@ -25,10 +25,8 @@ import { getBBConfig } from './get_bb_config.js'; import { type EndToEndContext, type SetupOptions, - deployAccounts, getPrivateKeyFromIndex, getSponsoredFPCAddress, - publicDeployAccounts, setup, setupPXEAndGetWallet, teardown, @@ -54,7 +52,7 @@ export class FullProverTest { wallet!: TestWallet; provenWallet!: TestWallet; accounts: AztecAddress[] = []; - deployedAccounts!: InitialAccountData[]; + fundedAccounts!: InitialAccountData[]; fakeProofsAsset!: TokenContract; fakeProofsAssetInstance!: ContractInstanceWithAddress; tokenSim!: TokenSimulator; @@ -89,20 +87,13 @@ export class FullProverTest { * Applies base setup: deploys 2 accounts and token contract. */ private async applyBaseSetup() { - this.logger.info('Applying base setup: deploying accounts'); - const { deployedAccounts } = await deployAccounts( - 2, - this.logger, - )({ - wallet: this.context.wallet, - initialFundedAccounts: this.context.initialFundedAccounts, - }); - this.deployedAccounts = deployedAccounts; - this.accounts = deployedAccounts.map(a => a.address); + this.logger.info('Applying base setup: registering funded accounts'); + this.fundedAccounts = this.context.additionallyFundedAccounts; + this.accounts = this.fundedAccounts.map(a => a.address); this.wallet = this.context.wallet; - - this.logger.info('Applying base setup: publicly deploying accounts'); - await publicDeployAccounts(this.wallet, this.accounts.slice(0, 2)); + for (const { secret, salt, signingKey } of this.fundedAccounts) { + await this.wallet.createSchnorrInitializerlessAccount(secret, salt, signingKey); + } this.logger.info('Applying base setup: deploying token contract'); const { contract: asset, instance } = await TokenContract.deploy( @@ -132,7 +123,7 @@ export class FullProverTest { startProverNode: true, coinbase: this.coinbase, fundSponsoredFPC: true, - skipAccountDeployment: true, + additionallyFundedAccounts: await generateSchnorrAccounts(2), l1ContractsArgs: { realVerifier: this.realProofs }, }); @@ -197,8 +188,13 @@ export class FullProverTest { await provenWallet.registerContract(this.fakeProofsAssetInstance, TokenContract.artifact); for (let i = 0; i < 2; i++) { - await provenWallet.createSchnorrAccount(this.deployedAccounts[i].secret, this.deployedAccounts[i].salt); - await this.wallet.createSchnorrAccount(this.deployedAccounts[i].secret, this.deployedAccounts[i].salt); + // Mirror the initializerless funded accounts (created via createFundedAccounts) in both wallets so + // their addresses match the genesis-funded ones. + await provenWallet.createSchnorrInitializerlessAccount( + this.fundedAccounts[i].secret, + this.fundedAccounts[i].salt, + ); + await this.wallet.createSchnorrInitializerlessAccount(this.fundedAccounts[i].secret, this.fundedAccounts[i].salt); } const asset = TokenContract.at(this.fakeProofsAsset.address, provenWallet); @@ -226,7 +222,7 @@ export class FullProverTest { this.logger.verbose('Starting prover node'); const sponsoredFPCAddress = await getSponsoredFPCAddress(); const { genesis } = await getGenesisValues( - this.context.initialFundedAccounts.map(a => a.address).concat(sponsoredFPCAddress), + this.context.additionallyFundedAccounts.map(a => a.address).concat(sponsoredFPCAddress), undefined, undefined, this.context.genesis!.genesisTimestamp, diff --git a/yarn-project/end-to-end/src/fixtures/setup.ts b/yarn-project/end-to-end/src/fixtures/setup.ts index 94b1934ed123..6ee3942fccc0 100644 --- a/yarn-project/end-to-end/src/fixtures/setup.ts +++ b/yarn-project/end-to-end/src/fixtures/setup.ts @@ -1,17 +1,7 @@ -import { SchnorrAccountContractArtifact } from '@aztec/accounts/schnorr'; import { type InitialAccountData, generateSchnorrAccounts } from '@aztec/accounts/testing'; import { type AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '@aztec/aztec-node'; -import { NO_FROM } from '@aztec/aztec.js/account'; import { AztecAddress, EthAddress } from '@aztec/aztec.js/addresses'; -import { - BatchCall, - type ContractFunctionInteraction, - type ContractMethod, - type DeployOptions, - type InteractionWaitOptions, - getContractClassFromArtifact, - waitForProven, -} from '@aztec/aztec.js/contracts'; +import type { ContractMethod } from '@aztec/aztec.js/contracts'; import { publishContractClass, publishInstance } from '@aztec/aztec.js/deployment'; import { Fr } from '@aztec/aztec.js/fields'; import { type Logger, createLogger } from '@aztec/aztec.js/log'; @@ -66,7 +56,7 @@ import { initTelemetryClient, } from '@aztec/telemetry-client'; import { BenchmarkTelemetryClient } from '@aztec/telemetry-client/bench'; -import { deployFundedSchnorrAccounts } from '@aztec/wallets/testing'; +import { createFundedInitializerlessAccounts } from '@aztec/wallets/testing'; import { getGenesisValues } from '@aztec/world-state/testing'; import fs from 'fs/promises'; @@ -165,10 +155,11 @@ export type SetupOptions = { deployL1ContractsValues?: DeployAztecL1ContractsReturnType; /** Initial fee juice for default accounts */ initialAccountFeeJuice?: Fr; - /** Number of initial accounts funded with fee juice */ - numberOfInitialFundedAccounts?: number; - /** Data of the initial funded accounts */ - initialFundedAccounts?: InitialAccountData[]; + /** + * Extra accounts to fund at genesis beyond the `numberOfAccounts` initializerless accounts that setup + * creates. Setup funds these but does NOT create or deploy them — the test creates/deploys them itself + */ + additionallyFundedAccounts?: InitialAccountData[]; /** An initial set of validators */ initialValidators?: (Operator & { privateKey: `0x${string}` })[]; /** Anvil Start time */ @@ -207,8 +198,11 @@ export type SetupOptions = { zkPassportArgs?: ZKPassportArgs; /** Whether to fund the sponsored FPC in genesis (defaults to false). */ fundSponsoredFPC?: boolean; - /** Whether to skip deploying accounts during setup (legacy behavior for tests using deployAccounts helper). */ - skipAccountDeployment?: boolean; + /** + * Whether to advance the chain past genesis by mining an empty block during setup (defaults to true). + * Set to false for tests that must observe the chain at genesis (block 0). + */ + advancePastGenesis?: boolean; /** L1 contracts deployment arguments. */ l1ContractsArgs?: Partial; /** Wallet minimum fee padding multiplier */ @@ -243,8 +237,8 @@ export type EndToEndContext = { config: AztecNodeConfig; /** The Aztec Node configuration (alias for config for backward compatibility). */ aztecNodeConfig: AztecNodeConfig; - /** The data for the initial funded accounts. */ - initialFundedAccounts: InitialAccountData[]; + /** Data for the extra accounts funded at genesis but not created by setup (the test creates/deploys them). */ + additionallyFundedAccounts: InitialAccountData[]; /** The wallet to be used. */ wallet: TestWallet; /** The wallets to be used. */ @@ -408,11 +402,12 @@ export async function setup( config.coinbase = EthAddress.fromString(publisherHdAccount.address); } - // Determine which addresses to fund in genesis - const initialFundedAccounts = - opts.initialFundedAccounts ?? - (await generateSchnorrAccounts(opts.numberOfInitialFundedAccounts ?? Math.max(numberOfAccounts, 10))); - const addressesToFund = initialFundedAccounts.map(a => a.address); + // The accounts setup creates itself: `numberOfAccounts` initializerless accounts, generated here and + // funded at genesis so they are immediately usable. + const defaultAccounts = await generateSchnorrAccounts(numberOfAccounts); + // Extra accounts the test wants funded at genesis but will create/deploy itself. + const additionallyFundedAccounts = opts.additionallyFundedAccounts ?? []; + const addressesToFund = [...defaultAccounts, ...additionallyFundedAccounts].map(a => a.address); // Optionally fund the sponsored FPC if (opts.fundSponsoredFPC) { @@ -539,24 +534,14 @@ export async function setup( // Transactions built against the genesis state must be included in block 1, otherwise they are dropped. // To avoid test failures from dropped transactions, we ensure progression beyond genesis before proceeding. - // For account deployments, we set minTxsPerBlock=1 and deploy accounts sequentially for guaranteed success. - // If no accounts need deployment, we await an empty block to confirm network progression. const originalMinTxsPerBlock = config.minTxsPerBlock; if (originalMinTxsPerBlock === undefined) { throw new Error('minTxsPerBlock is undefined in e2e test setup'); } - // Whether we're deploying accounts (and therefore need reliable block inclusion past genesis) - const shouldDeployAccounts = numberOfAccounts > 0 && !opts.skipAccountDeployment; - // Only set minTxsPerBlock=0 if we need an empty block (no accounts at all, not skipped deployment) - const needsEmptyBlock = numberOfAccounts === 0 && !opts.skipAccountDeployment; - // Pipelining is always on: the proposer builds during slot N-1 for slot N. A tx submitted at - // slot N start arrives after that build, so forcing minTxsPerBlock=1 would stall the chain on - // alternating slots -- hence empty checkpoints are allowed (minTxsPerBlock=0) for account - // deployment. Automine is unaffected: its runBuild clamps mempool builds to - // Math.max(minTxsPerBlock ?? 1, 1) and still requires minValidTxs: 1. - const accountsDeployMinTxs = 0; - config.minTxsPerBlock = shouldDeployAccounts ? accountsDeployMinTxs : needsEmptyBlock ? 0 : originalMinTxsPerBlock; + // Allow an empty checkpoint so the empty block can be built; leave untouched when not advancing. + const advancePastGenesis = (opts.advancePastGenesis ?? true) && !opts.skipInitialSequencer; + config.minTxsPerBlock = advancePastGenesis ? 0 : originalMinTxsPerBlock; config.p2pEnabled = opts.mockGossipSubNetwork || config.p2pEnabled; config.p2pIp = opts.p2pIp ?? config.p2pIp ?? '127.0.0.1'; @@ -651,18 +636,18 @@ export async function setup( let accounts: AztecAddress[] = []; - if (opts.skipInitialSequencer) { - logger.info('Sequencer not started on initial node, skipping block progression'); - } else if (shouldDeployAccounts) { - logger.info( - `${numberOfAccounts} accounts are being deployed. Reliably progressing past genesis by waiting for the accounts to be deployed`, - ); - const accountsData = initialFundedAccounts.slice(0, numberOfAccounts); - const accountManagers = await deployFundedSchnorrAccounts(wallet, accountsData); - accounts = accountManagers.map(accountManager => accountManager.address); - } else if (needsEmptyBlock) { - logger.info('No accounts are being deployed, waiting for an empty block 1 to be mined'); - // AutomineSequencer only builds on tx arrival; explicitly request an empty block. + // Create the default accounts. They are initializerless, so this is a PXE-side operation (registration + // + a simulated store call) with no on-chain tx, independent of the sequencer. + if (numberOfAccounts > 0) { + logger.info(`Creating ${numberOfAccounts} initializerless test accounts`); + await createFundedInitializerlessAccounts(wallet, defaultAccounts); + accounts = defaultAccounts.map(a => a.address); + } + + // Advancing past genesis needs a running sequencer to build the empty block; advancePastGenesis is + // already false when skipInitialSequencer is set. + if (advancePastGenesis) { + logger.info('Mining an empty block to progress past genesis'); const automine = aztecNodeService.getAutomineSequencer(); if (automine) { await automine.buildEmptyBlock(); @@ -670,20 +655,15 @@ export async function setup( while ((await aztecNodeService.getBlockNumber()) === 0) { await sleep(2000); } + } else if (opts.skipInitialSequencer) { + logger.info('Sequencer not started on initial node, skipping block progression'); } - // If skipAccountDeployment is true, we don't deploy or wait - tests will handle account deployment later // Now we restore the original minTxsPerBlock setting if we changed it. if (sequencerClient && config.minTxsPerBlock !== originalMinTxsPerBlock) { sequencerClient.getSequencer().updateConfig({ minTxsPerBlock: originalMinTxsPerBlock }); } - if (initialFundedAccounts.length < numberOfAccounts) { - throw new Error( - `Unable to deploy ${numberOfAccounts} accounts. Only ${initialFundedAccounts.length} accounts were funded.`, - ); - } - const teardown = async () => { try { await tryStop(wallet, logger); @@ -724,7 +704,7 @@ export async function setup( aztecNodeConfig: config, dateProvider, deployL1ContractsValues, - initialFundedAccounts, + additionallyFundedAccounts, logger, mockGossipSubNetwork, genesis, @@ -948,100 +928,6 @@ export async function ensureHandshakeRegistryPublished(wallet: Wallet, from: Azt await wallet.registerContract(instance, HandshakeRegistryArtifact); } -/** - * Registers the contract class used for test accounts and publicly deploys the instances requested. - * Use this when you need to make a public call to an account contract, such as for requesting a public authwit. - */ -export async function ensureAccountContractsPublished(wallet: Wallet, accountsToDeploy: AztecAddress[]) { - const accountsAndAddresses = await Promise.all( - accountsToDeploy.map(async address => { - return { - address, - deployed: (await wallet.getContractMetadata(address)).isContractPublished, - }; - }), - ); - const instances = ( - await Promise.all( - accountsAndAddresses - .filter(({ deployed }) => !deployed) - .map(({ address }) => wallet.getContractMetadata(address)), - ) - ).map(contractMetadata => contractMetadata.instance); - const contractClass = await getContractClassFromArtifact(SchnorrAccountContractArtifact); - if (!(await wallet.getContractClassMetadata(contractClass.id)).isContractClassPubliclyRegistered) { - await (await publishContractClass(wallet, SchnorrAccountContractArtifact)).send({ from: accountsToDeploy[0] }); - } - const requests = instances.map(instance => publishInstance(wallet, instance!)); - const batch = new BatchCall(wallet, requests); - await batch.send({ from: accountsToDeploy[0] }); -} - -/** - * Helper function to deploy accounts. - * Returns deployed account data that can be used by tests. - */ -export const deployAccounts = - (numberOfAccounts: number, logger: Logger, deployOptions?: Partial>) => - async ({ wallet, initialFundedAccounts }: { wallet: TestWallet; initialFundedAccounts: InitialAccountData[] }) => { - if (initialFundedAccounts.length < numberOfAccounts) { - throw new Error(`Cannot deploy more than ${initialFundedAccounts.length} initial accounts.`); - } - - logger.verbose('Deploying accounts funded with fee juice...'); - const deployedAccounts = initialFundedAccounts.slice(0, numberOfAccounts); - // Serial due to https://github.com/AztecProtocol/aztec-packages/issues/12045 - for (let i = 0; i < deployedAccounts.length; i++) { - const accountManager = await wallet.createSchnorrAccount( - deployedAccounts[i].secret, - deployedAccounts[i].salt, - deployedAccounts[i].signingKey, - ); - const deployMethod = await accountManager.getDeployMethod(); - await deployMethod.send({ - from: NO_FROM, - skipClassPublication: i !== 0, // Publish the contract class at most once. - ...deployOptions, - }); - } - - return { deployedAccounts }; - }; - -/** - * Registers the contract class used for test accounts and publicly deploys the instances requested. - * Use this when you need to make a public call to an account contract, such as for requesting a public authwit. - */ -export async function publicDeployAccounts( - wallet: Wallet, - accountsToDeploy: AztecAddress[], - waitUntilProven = false, - node?: AztecNode, -) { - const instances = (await Promise.all(accountsToDeploy.map(account => wallet.getContractMetadata(account)))).map( - metadata => metadata.instance, - ); - - const contractClass = await getContractClassFromArtifact(SchnorrAccountContractArtifact); - const alreadyRegistered = (await wallet.getContractClassMetadata(contractClass.id)).isContractClassPubliclyRegistered; - - const calls: ContractFunctionInteraction[] = await Promise.all([ - ...(!alreadyRegistered ? [publishContractClass(wallet, SchnorrAccountContractArtifact)] : []), - ...instances.map(instance => publishInstance(wallet, instance!)), - ]); - - const batch = new BatchCall(wallet, calls); - - const { receipt: txReceipt } = await batch.send({ from: accountsToDeploy[0] }); - if (waitUntilProven) { - if (!node) { - throw new Error('Need to provide an AztecNode to wait for proven.'); - } else { - await waitForProven(node, txReceipt); - } - } -} - /** * Destroys the current context. */ diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index a5f1907e229b..b1605928f266 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -7,8 +7,6 @@ export { type EndToEndContext, type SetupOptions, createAndSyncProverNode, - deployAccounts, - ensureAccountContractsPublished, ensureAuthRegistryPublished, ensurePublicChecksPublished, expectMapping, @@ -18,7 +16,6 @@ export { getPrivateKeyFromIndex, getSponsoredFPCAddress, getSponsoredFPCInstance, - publicDeployAccounts, registerSponsoredFPC, setup, setupPXEAndGetWallet, diff --git a/yarn-project/end-to-end/src/forward-compatibility/wallet_service.ts b/yarn-project/end-to-end/src/forward-compatibility/wallet_service.ts index 674ef390f4db..4394bdb8c053 100644 --- a/yarn-project/end-to-end/src/forward-compatibility/wallet_service.ts +++ b/yarn-project/end-to-end/src/forward-compatibility/wallet_service.ts @@ -58,7 +58,9 @@ async function main() { // incompatible with Node.js import attribute enforcement. const testAccountsData = await getInitialTestAccountsData(); const accounts = await Promise.all( - testAccountsData.map(({ secret, salt, signingKey }) => wallet.createSchnorrAccount(secret, salt, signingKey)), + testAccountsData.map(({ secret, salt, signingKey }) => + wallet.createSchnorrInitializerlessAccount(secret, salt, signingKey), + ), ); // Register and deploy the 4th account. diff --git a/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts b/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts index 1969c871b7c1..086018c7b0a6 100644 --- a/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts +++ b/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts @@ -21,11 +21,7 @@ import { computeL2ToL1MessageHash } from '@aztec/stdlib/hash'; import { jest } from '@jest/globals'; import { type GetContractReturnType, getContract, parseEther, toFunctionSelector } from 'viem'; -import { - type EndToEndContext, - ensureAccountContractsPublished, - ensureAuthRegistryPublished, -} from '../fixtures/utils.js'; +import { type EndToEndContext, ensureAuthRegistryPublished } from '../fixtures/utils.js'; import type { TestWallet } from '../test-wallet/test_wallet.js'; import { CrossChainTestHarness } from './cross_chain_test_harness.js'; @@ -101,7 +97,6 @@ export const uniswapL1L2TestSuite = ( version = Number(await rollup.getVersion()); ownerEthAddress = EthAddress.fromString((await l1Client.getAddresses())[0]); - await ensureAccountContractsPublished(wallet, [ownerAddress, sponsorAddress]); await ensureAuthRegistryPublished(wallet, ownerAddress); logger.info('Deploying DAI Portal, initializing and deploying l2 contract...'); 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 bb0662ad9640..f6b4389e34d4 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,6 +1,6 @@ import { EcdsaKAccountContract, EcdsaRAccountContract } from '@aztec/accounts/ecdsa'; import { StubEcdsaAccountContractArtifact, createStubEcdsaAccount } from '@aztec/accounts/ecdsa/stub'; -import { SchnorrAccountContract } from '@aztec/accounts/schnorr'; +import { SchnorrAccountContract, SchnorrInitializerlessAccountContract } from '@aztec/accounts/schnorr'; import { StubSchnorrAccountContractArtifact, createStubSchnorrAccount } from '@aztec/accounts/schnorr/stub'; import { type Account, type AccountContract, NO_FROM } from '@aztec/aztec.js/account'; import type { CompleteAddress } from '@aztec/aztec.js/addresses'; @@ -13,6 +13,7 @@ import { isContractFunctionInteractionCallIntent, lookupValidity, } from '@aztec/aztec.js/authorization'; +import { ContractFunctionInteraction } from '@aztec/aztec.js/contracts'; import type { AztecNode } from '@aztec/aztec.js/node'; import { AccountManager, type SendOptions } from '@aztec/aztec.js/wallet'; import { TxSimulationResultWithAppOffset } from '@aztec/aztec.js/wallet'; @@ -96,6 +97,16 @@ export class TestWallet extends BaseWallet { return this.createAccount({ secret, salt, type: 'schnorr', contract: new SchnorrAccountContract(signingKey) }); } + createSchnorrInitializerlessAccount(secret: Fr, salt: Fr, signingKey?: Fq): Promise { + signingKey = signingKey ?? deriveSigningKey(secret); + return this.createAccount({ + secret, + salt, + type: 'schnorr_initializerless', + contract: new SchnorrInitializerlessAccountContract(signingKey), + }); + } + createECDSARAccount(secret: Fr, salt: Fr, signingKey: Buffer): Promise { return this.createAccount({ secret, @@ -131,6 +142,8 @@ export class TestWallet extends BaseWallet { await this.pxe.registerContractClass(StubEcdsaAccountContractArtifact); this.stubClassIds.set('schnorr', schnorrClassId); + // Initializerless accounts share the schnorr stub class for kernelless simulation. + this.stubClassIds.set('schnorr_initializerless', schnorrClassId); this.stubClassIds.set('ecdsasecp256k1', ecdsaClassId); this.stubClassIds.set('ecdsasecp256r1', ecdsaClassId); } @@ -178,7 +191,8 @@ export class TestWallet extends BaseWallet { } private getStubAccountFor(address: AztecAddress, completeAddress: CompleteAddress) { - return this.getTypeFor(address) === 'schnorr' + const type = this.getTypeFor(address); + return type === 'schnorr' || type === 'schnorr_initializerless' ? createStubSchnorrAccount(completeAddress) : createStubEcdsaAccount(completeAddress); } @@ -221,6 +235,10 @@ export class TestWallet extends BaseWallet { const type = accountData?.type ?? 'schnorr'; const contract = accountData?.contract ?? new SchnorrAccountContract(GrumpkinScalar.random()); + // Initializerless accounts have no deployment tx: the address commits to the signing public key + // (via the contract's immutablesHash, resolved by AccountManager.create) and the constructor's + // storage writes are materialized locally via a simulated "store" call below. + // Mirrors EmbeddedWallet.createAccountInternal. const accountManager = await AccountManager.create(this, secret, contract, { salt }); const instance = accountManager.getInstance(); @@ -231,6 +249,16 @@ export class TestWallet extends BaseWallet { const address = accountManager.address.toString(); this.accounts.set(address, { account: await accountManager.getAccount(), type }); + if (contract instanceof SchnorrInitializerlessAccountContract) { + const constructorAbi = artifact.functions.find(f => f.name === 'constructor'); + if (!constructorAbi) { + throw new Error('Could not create SchnorrInitializerlessAccount: constructor ABI not found'); + } + const { x, y } = await contract.getSigningPublicKey(); + const storeCall = new ContractFunctionInteraction(this, instance.address, constructorAbi, [x, y]); + await storeCall.simulate({ from: instance.address }); + } + return accountManager; } 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 c10ebfc25375..cff7f0f89afe 100644 --- a/yarn-project/wallets/src/embedded/account-contract-providers/bundle.ts +++ b/yarn-project/wallets/src/embedded/account-contract-providers/bundle.ts @@ -1,6 +1,6 @@ import { EcdsaKAccountContract, EcdsaRAccountContract } from '@aztec/accounts/ecdsa'; import { StubEcdsaAccountContractArtifact, createStubEcdsaAccount } from '@aztec/accounts/ecdsa/stub'; -import { SchnorrAccountContract } from '@aztec/accounts/schnorr'; +import { SchnorrAccountContract, SchnorrInitializerlessAccountContract } from '@aztec/accounts/schnorr'; import { StubSchnorrAccountContractArtifact, createStubSchnorrAccount } from '@aztec/accounts/schnorr/stub'; import type { Account, AccountContract } from '@aztec/aztec.js/account'; import type { Fq } from '@aztec/foundation/curves/bn254'; @@ -19,6 +19,10 @@ export class BundleAccountContractsProvider implements AccountContractsProvider return Promise.resolve(new SchnorrAccountContract(signingKey)); } + getSchnorrInitializerlessAccountContract(signingKey: Fq): Promise { + return Promise.resolve(new SchnorrInitializerlessAccountContract(signingKey)); + } + getEcdsaRAccountContract(signingKey: Buffer): Promise { return Promise.resolve(new EcdsaRAccountContract(signingKey)); } 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 0edcd9007c94..bec3d9c5d20f 100644 --- a/yarn-project/wallets/src/embedded/account-contract-providers/lazy.ts +++ b/yarn-project/wallets/src/embedded/account-contract-providers/lazy.ts @@ -16,6 +16,11 @@ export class LazyAccountContractsProvider implements AccountContractsProvider { return new SchnorrAccountContract(signingKey); } + async getSchnorrInitializerlessAccountContract(signingKey: Fq): Promise { + const { SchnorrInitializerlessAccountContract } = await import('@aztec/accounts/schnorr/lazy'); + return new SchnorrInitializerlessAccountContract(signingKey); + } + async getEcdsaRAccountContract(signingKey: Buffer): Promise { const { EcdsaRAccountContract } = await import('@aztec/accounts/ecdsa/lazy'); return new EcdsaRAccountContract(signingKey); 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 b1b546e160e4..0c189c7b3634 100644 --- a/yarn-project/wallets/src/embedded/account-contract-providers/types.ts +++ b/yarn-project/wallets/src/embedded/account-contract-providers/types.ts @@ -13,6 +13,7 @@ import type { AccountType } from '../wallet_db.js'; */ export interface AccountContractsProvider { getSchnorrAccountContract(signingKey: Fq): Promise; + getSchnorrInitializerlessAccountContract(signingKey: Fq): Promise; getEcdsaRAccountContract(signingKey: Buffer): Promise; getEcdsaKAccountContract(signingKey: Buffer): Promise; getStubAccountContractArtifact(type: AccountType): Promise; diff --git a/yarn-project/wallets/src/embedded/embedded_wallet.ts b/yarn-project/wallets/src/embedded/embedded_wallet.ts index 1e30d72533ea..56e3d2e3acb0 100644 --- a/yarn-project/wallets/src/embedded/embedded_wallet.ts +++ b/yarn-project/wallets/src/embedded/embedded_wallet.ts @@ -1,6 +1,7 @@ import { type Account, NO_FROM } from '@aztec/aztec.js/account'; import { CallAuthorizationRequest } from '@aztec/aztec.js/authorization'; import { + ContractFunctionInteraction, type InteractionWaitOptions, NO_WAIT, type SendReturn, @@ -19,6 +20,8 @@ import type { import { AccountManager, TxSimulationResultWithAppOffset } from '@aztec/aztec.js/wallet'; import type { DefaultAccountEntrypointOptions } from '@aztec/entrypoints/account'; import { DefaultEntrypoint } from '@aztec/entrypoints/default'; +import { poseidon2Hash } from '@aztec/foundation/crypto/poseidon'; +import { Schnorr } from '@aztec/foundation/crypto/schnorr'; import { Fq, Fr } from '@aztec/foundation/curves/bn254'; import type { Logger } from '@aztec/foundation/log'; import type { AztecAsyncKVStore } from '@aztec/kv-store'; @@ -284,6 +287,7 @@ export class EmbeddedWallet extends BaseWallet { await this.pxe.registerContractClass(ecdsaArtifact); this.stubClassIds.set('schnorr', schnorrClassId); + this.stubClassIds.set('schnorr_initializerless', schnorrClassId); this.stubClassIds.set('ecdsasecp256k1', ecdsaClassId); this.stubClassIds.set('ecdsasecp256r1', ecdsaClassId); } @@ -388,11 +392,19 @@ export class EmbeddedWallet extends BaseWallet { signingKey: Buffer, ): Promise { let contract; + let immutablesHash; + let publicKey; switch (type) { case 'schnorr': { contract = await this.accountContracts.getSchnorrAccountContract(Fq.fromBuffer(signingKey)); break; } + case 'schnorr_initializerless': { + contract = await this.accountContracts.getSchnorrInitializerlessAccountContract(Fq.fromBuffer(signingKey)); + publicKey = await new Schnorr().computePublicKey(Fq.fromBuffer(signingKey)); + immutablesHash = await poseidon2Hash([publicKey.x, publicKey.y]); + break; + } case 'ecdsasecp256k1': { contract = await this.accountContracts.getEcdsaKAccountContract(signingKey); break; @@ -406,17 +418,29 @@ export class EmbeddedWallet extends BaseWallet { } } - const accountManager = await AccountManager.create(this, secret, contract, { salt }); + const accountManager = await AccountManager.create(this, secret, contract, { salt, immutablesHash }); const instance = accountManager.getInstance(); const existingInstance = await this.pxe.getContractInstance(instance.address); if (!existingInstance) { const existingArtifact = await this.pxe.getContractArtifact(instance.currentContractClassId); + const artifact = existingArtifact ?? (await accountManager.getAccountContract().getContractArtifact()); await this.registerContract( instance, !existingArtifact ? await accountManager.getAccountContract().getContractArtifact() : undefined, accountManager.getSecretKey(), ); + if (type === 'schnorr_initializerless') { + const constructor = artifact.functions.find(f => f.name === 'constructor'); + if (!constructor) { + throw new Error('Could not create SchnorrInitializerlessAccountContract: constructor ABI not found'); + } + const storeCall = new ContractFunctionInteraction(this, instance.address, constructor, [ + publicKey!.x, + publicKey!.y, + ]); + await storeCall.simulate({ from: instance.address }); + } } return accountManager; } @@ -438,6 +462,11 @@ export class EmbeddedWallet extends BaseWallet { return this.createAndStoreAccount(alias ?? '', 'schnorr', secret, salt, sk.toBuffer()); } + createSchnorrInitializerlessAccount(secret: Fr, salt: Fr, signingKey?: Fq, alias?: string): Promise { + const sk = signingKey ?? deriveSigningKey(secret); + return this.createAndStoreAccount(alias ?? '', 'schnorr_initializerless', secret, salt, sk.toBuffer()); + } + createECDSARAccount(secret: Fr, salt: Fr, signingKey: Buffer, alias?: string): Promise { return this.createAndStoreAccount(alias ?? '', 'ecdsasecp256r1', secret, salt, signingKey); } diff --git a/yarn-project/wallets/src/embedded/wallet_db.ts b/yarn-project/wallets/src/embedded/wallet_db.ts index b32a84ac4d43..e6d8e1a47491 100644 --- a/yarn-project/wallets/src/embedded/wallet_db.ts +++ b/yarn-project/wallets/src/embedded/wallet_db.ts @@ -4,7 +4,7 @@ import type { LogFn } from '@aztec/foundation/log'; import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -export const AccountTypes = ['schnorr', 'ecdsasecp256r1', 'ecdsasecp256k1'] as const; +export const AccountTypes = ['schnorr', 'schnorr_initializerless', 'ecdsasecp256r1', 'ecdsasecp256k1'] as const; export type AccountType = (typeof AccountTypes)[number]; function accountKey(field: string, address: AztecAddress | string): string { diff --git a/yarn-project/wallets/src/testing.ts b/yarn-project/wallets/src/testing.ts index 3f391caa9fe1..791ed6028944 100644 --- a/yarn-project/wallets/src/testing.ts +++ b/yarn-project/wallets/src/testing.ts @@ -1,32 +1,25 @@ import type { InitialAccountData } from '@aztec/accounts/testing'; import { getInitialTestAccountsData } from '@aztec/accounts/testing/lazy'; -import { NO_FROM } from '@aztec/aztec.js/account'; -import type { WaitOpts } from '@aztec/aztec.js/contracts'; import type { AccountManager } from '@aztec/aztec.js/wallet'; import type { Fq, Fr } from '@aztec/foundation/curves/bn254'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; interface WalletWithSchnorrAccounts { - createSchnorrAccount(secret: Fr, salt: Fr, signingKey?: Fq, alias?: string): Promise; + createSchnorrInitializerlessAccount(secret: Fr, salt: Fr, signingKey?: Fq, alias?: string): Promise; } -export async function deployFundedSchnorrAccounts( +/** + * Creates the given (genesis-funded) test accounts as initializerless schnorr accounts. Initializerless + * accounts need no deployment tx — creating one registers the instance and materializes its immutable keys + * locally — so the accounts are usable as soon as they are created, funded via genesis at their addresses. + */ +export async function createFundedInitializerlessAccounts( wallet: WalletWithSchnorrAccounts, accountsData: InitialAccountData[], - waitOptions?: WaitOpts, ) { const accountManagers = []; - // Serial due to https://github.com/AztecProtocol/aztec-packages/issues/12045 - for (let i = 0; i < accountsData.length; i++) { - const { secret, salt, signingKey } = accountsData[i]; - const accountManager = await wallet.createSchnorrAccount(secret, salt, signingKey); - const deployMethod = await accountManager.getDeployMethod(); - await deployMethod.send({ - from: NO_FROM, - skipClassPublication: i !== 0, - wait: waitOptions, - }); - accountManagers.push(accountManager); + for (const { secret, salt, signingKey } of accountsData) { + accountManagers.push(await wallet.createSchnorrInitializerlessAccount(secret, salt, signingKey)); } return accountManagers; } @@ -37,7 +30,8 @@ export async function registerInitialLocalNetworkAccountsInWallet( const testAccounts = await getInitialTestAccountsData(); return Promise.all( testAccounts.map(async account => { - return (await wallet.createSchnorrAccount(account.secret, account.salt, account.signingKey)).address; + return (await wallet.createSchnorrInitializerlessAccount(account.secret, account.salt, account.signingKey)) + .address; }), ); }