From c75ea371b692b95ee0cf825b59a1b7dd1c28e64a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Tue, 30 Jun 2026 22:15:25 +0000 Subject: [PATCH 01/11] feat!: stop deriving signing key from privacy keys --- .../src/schnorr/initializerless/index.ts | 16 ++++++------ .../src/schnorr/initializerless/lazy.ts | 16 ++++++------ .../src/schnorr/private_immutable/index.ts | 16 ++++++------ .../src/schnorr/private_immutable/lazy.ts | 16 ++++++------ .../accounts/src/testing/configuration.ts | 8 ++++-- yarn-project/accounts/src/testing/index.ts | 26 +++++++++---------- yarn-project/accounts/src/testing/lazy.ts | 26 +++++++++---------- yarn-project/accounts/src/utils/index.ts | 1 + .../accounts/src/utils/key_derivation.test.ts | 18 +++++++++++++ .../accounts/src/utils/key_derivation.ts | 15 +++++++++++ .../src/e2e_contract_updates.test.ts | 2 +- .../e2e_multiple_accounts_1_enc_key.test.ts | 2 +- .../forward-compatibility/wallet_service.ts | 4 +-- 13 files changed, 102 insertions(+), 64 deletions(-) create mode 100644 yarn-project/accounts/src/utils/key_derivation.test.ts create mode 100644 yarn-project/accounts/src/utils/key_derivation.ts diff --git a/yarn-project/accounts/src/schnorr/initializerless/index.ts b/yarn-project/accounts/src/schnorr/initializerless/index.ts index 2c1df1e3e24d..58312c7a0633 100644 --- a/yarn-project/accounts/src/schnorr/initializerless/index.ts +++ b/yarn-project/accounts/src/schnorr/initializerless/index.ts @@ -11,10 +11,10 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import type { ContractArtifact } from '@aztec/stdlib/abi'; import { loadContractArtifact } from '@aztec/stdlib/abi'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; import type { NoirCompiledContract } from '@aztec/stdlib/noir'; import SchnorrInitializerlessAccountContractJson from '../../../artifacts/SchnorrInitializerlessAccount.json' with { type: 'json' }; +import { deriveSecretKeyFromSigningKey } from '../../utils/key_derivation.js'; import { SchnorrBaseAccountContract } from '../account_contract.js'; export const SchnorrInitializerlessAccountContractArtifact = loadContractArtifact( @@ -47,16 +47,16 @@ export class SchnorrInitializerlessAccountContract extends SchnorrBaseAccountCon /** * Compute the address of a schnorr account contract. - * @param secret - A seed for deriving the signing key and public keys. + * @param signingPrivateKey - The account's signing private key. * @param salt - The contract address salt. - * @param signingPrivateKey - A specific signing private key that's not derived from the secret. + * @param secretKey - Seed for the account's privacy keys. Derived from the signing key when omitted. */ export async function getSchnorrInitializerlessAccountContractAddress( - secret: Fr, + signingPrivateKey: GrumpkinScalar, salt: Fr, - signingPrivateKey?: GrumpkinScalar, + secretKey?: Fr, ): Promise { - const signingKey = signingPrivateKey ?? deriveSigningKey(secret); - const accountContract = new SchnorrInitializerlessAccountContract(signingKey); - return await getAccountContractAddress(accountContract, secret, salt); + const resolvedSecretKey = secretKey ?? (await deriveSecretKeyFromSigningKey(signingPrivateKey)); + const accountContract = new SchnorrInitializerlessAccountContract(signingPrivateKey); + return await getAccountContractAddress(accountContract, resolvedSecretKey, salt); } diff --git a/yarn-project/accounts/src/schnorr/initializerless/lazy.ts b/yarn-project/accounts/src/schnorr/initializerless/lazy.ts index f8c8cbdfba91..2476b672748c 100644 --- a/yarn-project/accounts/src/schnorr/initializerless/lazy.ts +++ b/yarn-project/accounts/src/schnorr/initializerless/lazy.ts @@ -11,8 +11,8 @@ import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import type { ContractArtifact } from '@aztec/stdlib/abi'; import { loadContractArtifact } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; +import { deriveSecretKeyFromSigningKey } from '../../utils/key_derivation.js'; import { SchnorrBaseAccountContract } from '../account_contract.js'; /** @@ -55,16 +55,16 @@ export class SchnorrInitializerlessAccountContract extends SchnorrBaseAccountCon /** * Compute the address of a schnorr account contract. - * @param secret - A seed for deriving the signing key and public keys. + * @param signingPrivateKey - The account's signing private key. * @param salt - The contract address salt. - * @param signingPrivateKey - A specific signing private key that's not derived from the secret. + * @param secretKey - Seed for the account's privacy keys. Derived from the signing key when omitted. */ export async function getSchnorrInitializerlessAccountContractAddress( - secret: Fr, + signingPrivateKey: GrumpkinScalar, salt: Fr, - signingPrivateKey?: GrumpkinScalar, + secretKey?: Fr, ): Promise { - const signingKey = signingPrivateKey ?? deriveSigningKey(secret); - const accountContract = new SchnorrInitializerlessAccountContract(signingKey); - return await getAccountContractAddress(accountContract, secret, salt); + const resolvedSecretKey = secretKey ?? (await deriveSecretKeyFromSigningKey(signingPrivateKey)); + const accountContract = new SchnorrInitializerlessAccountContract(signingPrivateKey); + return await getAccountContractAddress(accountContract, resolvedSecretKey, salt); } diff --git a/yarn-project/accounts/src/schnorr/private_immutable/index.ts b/yarn-project/accounts/src/schnorr/private_immutable/index.ts index 63d57997d5a9..fd844e7c2cc8 100644 --- a/yarn-project/accounts/src/schnorr/private_immutable/index.ts +++ b/yarn-project/accounts/src/schnorr/private_immutable/index.ts @@ -10,10 +10,10 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import type { ContractArtifact } from '@aztec/stdlib/abi'; import { loadContractArtifact } from '@aztec/stdlib/abi'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; import type { NoirCompiledContract } from '@aztec/stdlib/noir'; import SchnorrAccountContractJson from '../../../artifacts/SchnorrAccount.json' with { type: 'json' }; +import { deriveSecretKeyFromSigningKey } from '../../utils/key_derivation.js'; import { SchnorrBaseAccountContract } from '../account_contract.js'; export const SchnorrAccountContractArtifact = loadContractArtifact(SchnorrAccountContractJson as NoirCompiledContract); @@ -35,16 +35,16 @@ export class SchnorrAccountContract extends SchnorrBaseAccountContract { /** * Compute the address of a schnorr account contract. - * @param secret - A seed for deriving the signing key and public keys. + * @param signingPrivateKey - The account's signing private key. * @param salt - The contract address salt. - * @param signingPrivateKey - A specific signing private key that's not derived from the secret. + * @param secretKey - Seed for the account's privacy keys. Derived from the signing key when omitted. */ export async function getSchnorrAccountContractAddress( - secret: Fr, + signingPrivateKey: GrumpkinScalar, salt: Fr, - signingPrivateKey?: GrumpkinScalar, + secretKey?: Fr, ): Promise { - const signingKey = signingPrivateKey ?? deriveSigningKey(secret); - const accountContract = new SchnorrAccountContract(signingKey); - return await getAccountContractAddress(accountContract, secret, salt); + const resolvedSecretKey = secretKey ?? (await deriveSecretKeyFromSigningKey(signingPrivateKey)); + const accountContract = new SchnorrAccountContract(signingPrivateKey); + return await getAccountContractAddress(accountContract, resolvedSecretKey, salt); } diff --git a/yarn-project/accounts/src/schnorr/private_immutable/lazy.ts b/yarn-project/accounts/src/schnorr/private_immutable/lazy.ts index 468354a29d8d..077762d213a9 100644 --- a/yarn-project/accounts/src/schnorr/private_immutable/lazy.ts +++ b/yarn-project/accounts/src/schnorr/private_immutable/lazy.ts @@ -10,8 +10,8 @@ import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import type { ContractArtifact } from '@aztec/stdlib/abi'; import { loadContractArtifact } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; +import { deriveSecretKeyFromSigningKey } from '../../utils/key_derivation.js'; import { SchnorrBaseAccountContract } from '../account_contract.js'; /** @@ -45,16 +45,16 @@ export class SchnorrAccountContract extends SchnorrBaseAccountContract { /** * Compute the address of a schnorr account contract. - * @param secret - A seed for deriving the signing key and public keys. + * @param signingPrivateKey - The account's signing private key. * @param salt - The contract address salt. - * @param signingPrivateKey - A specific signing private key that's not derived from the secret. + * @param secretKey - Seed for the account's privacy keys. Derived from the signing key when omitted. */ export async function getSchnorrAccountContractAddress( - secret: Fr, + signingPrivateKey: GrumpkinScalar, salt: Fr, - signingPrivateKey?: GrumpkinScalar, + secretKey?: Fr, ): Promise { - const signingKey = signingPrivateKey ?? deriveSigningKey(secret); - const accountContract = new SchnorrAccountContract(signingKey); - return await getAccountContractAddress(accountContract, secret, salt); + const resolvedSecretKey = secretKey ?? (await deriveSecretKeyFromSigningKey(signingPrivateKey)); + const accountContract = new SchnorrAccountContract(signingPrivateKey); + return await getAccountContractAddress(accountContract, resolvedSecretKey, salt); } diff --git a/yarn-project/accounts/src/testing/configuration.ts b/yarn-project/accounts/src/testing/configuration.ts index 0255e9861b4e..021afa360d46 100644 --- a/yarn-project/accounts/src/testing/configuration.ts +++ b/yarn-project/accounts/src/testing/configuration.ts @@ -12,8 +12,12 @@ export const INITIAL_TEST_SECRET_KEYS = [ export const INITIAL_TEST_ENCRYPTION_KEYS = INITIAL_TEST_SECRET_KEYS.map(secretKey => deriveMasterIncomingViewingSecretKey(secretKey), ); -// TODO(#5837): come up with a standard signing key derivation scheme instead of using ivsk_m as signing keys here -export const INITIAL_TEST_SIGNING_KEYS = INITIAL_TEST_ENCRYPTION_KEYS; + +export const INITIAL_TEST_SIGNING_KEYS = [ + GrumpkinScalar.fromHexString('0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'), + GrumpkinScalar.fromHexString('202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e'), + GrumpkinScalar.fromHexString('404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e'), +]; export const INITIAL_TEST_ACCOUNT_SALTS = [Fr.ZERO, Fr.ZERO, Fr.ZERO]; diff --git a/yarn-project/accounts/src/testing/index.ts b/yarn-project/accounts/src/testing/index.ts index d74871b74869..b39167a9594e 100644 --- a/yarn-project/accounts/src/testing/index.ts +++ b/yarn-project/accounts/src/testing/index.ts @@ -4,14 +4,13 @@ * @packageDocumentation */ import { Fr } from '@aztec/aztec.js/fields'; -import type { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; +import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import { getSchnorrInitializerlessAccountContractAddress } from '../schnorr/initializerless/index.js'; import { getSchnorrAccountContractAddress } from '../schnorr/private_immutable/index.js'; +import { deriveSecretKeyFromSigningKey } from '../utils/key_derivation.js'; import { INITIAL_TEST_ACCOUNT_SALTS, - INITIAL_TEST_ENCRYPTION_KEYS, INITIAL_TEST_SECRET_KEYS, INITIAL_TEST_SIGNING_KEYS, type InitialAccountData, @@ -28,10 +27,10 @@ export { } from './configuration.js'; /** Derives the account contract address for the given type */ -function getTestAccountAddress(type: InitialAccountType, secret: Fr, salt: Fr, signingKey?: GrumpkinScalar) { +function getTestAccountAddress(type: InitialAccountType, signingKey: GrumpkinScalar, salt: Fr, secret?: Fr) { return type === 'schnorr' - ? getSchnorrAccountContractAddress(secret, salt, signingKey) - : getSchnorrInitializerlessAccountContractAddress(secret, salt, signingKey); + ? getSchnorrAccountContractAddress(signingKey, salt, secret) + : getSchnorrInitializerlessAccountContractAddress(signingKey, salt, secret); } /** @@ -41,13 +40,13 @@ export function getInitialTestAccountsData(): Promise { return Promise.all( INITIAL_TEST_SECRET_KEYS.map(async (secret, i) => ({ secret, - signingKey: INITIAL_TEST_ENCRYPTION_KEYS[i], + signingKey: INITIAL_TEST_SIGNING_KEYS[i], salt: INITIAL_TEST_ACCOUNT_SALTS[i], type: 'schnorr_initializerless' as const, address: await getSchnorrInitializerlessAccountContractAddress( - secret, - INITIAL_TEST_ACCOUNT_SALTS[i], INITIAL_TEST_SIGNING_KEYS[i], + INITIAL_TEST_ACCOUNT_SALTS[i], + secret, ), })), ); @@ -60,16 +59,17 @@ export async function generateSchnorrAccounts( numberOfAccounts: number, type: InitialAccountType = 'schnorr_initializerless', ): Promise { - const secrets = Array.from({ length: numberOfAccounts }, () => Fr.random()); + const signingKeys = Array.from({ length: numberOfAccounts }, () => GrumpkinScalar.random()); return await Promise.all( - secrets.map(async secret => { + signingKeys.map(async signingKey => { const salt = Fr.random(); + const secret = await deriveSecretKeyFromSigningKey(signingKey); return { secret, - signingKey: deriveSigningKey(secret), + signingKey, salt, type, - address: await getTestAccountAddress(type, secret, salt), + address: await getTestAccountAddress(type, signingKey, salt, secret), }; }), ); diff --git a/yarn-project/accounts/src/testing/lazy.ts b/yarn-project/accounts/src/testing/lazy.ts index 6884ac5c736b..c386aa97f9d4 100644 --- a/yarn-project/accounts/src/testing/lazy.ts +++ b/yarn-project/accounts/src/testing/lazy.ts @@ -4,14 +4,13 @@ * @packageDocumentation */ import { Fr } from '@aztec/aztec.js/fields'; -import type { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; +import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import { getSchnorrInitializerlessAccountContractAddress } from '../schnorr/initializerless/lazy.js'; import { getSchnorrAccountContractAddress } from '../schnorr/private_immutable/lazy.js'; +import { deriveSecretKeyFromSigningKey } from '../utils/key_derivation.js'; import { INITIAL_TEST_ACCOUNT_SALTS, - INITIAL_TEST_ENCRYPTION_KEYS, INITIAL_TEST_SECRET_KEYS, INITIAL_TEST_SIGNING_KEYS, type InitialAccountData, @@ -21,10 +20,10 @@ import { 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) { +function getTestAccountAddress(type: InitialAccountType, signingKey: GrumpkinScalar, salt: Fr, secret?: Fr) { return type === 'schnorr' - ? getSchnorrAccountContractAddress(secret, salt, signingKey) - : getSchnorrInitializerlessAccountContractAddress(secret, salt, signingKey); + ? getSchnorrAccountContractAddress(signingKey, salt, secret) + : getSchnorrInitializerlessAccountContractAddress(signingKey, salt, secret); } /** @@ -34,13 +33,13 @@ export function getInitialTestAccountsData(): Promise { return Promise.all( INITIAL_TEST_SECRET_KEYS.map(async (secret, i) => ({ secret, - signingKey: INITIAL_TEST_ENCRYPTION_KEYS[i], + signingKey: INITIAL_TEST_SIGNING_KEYS[i], salt: INITIAL_TEST_ACCOUNT_SALTS[i], type: 'schnorr_initializerless' as const, address: await getSchnorrInitializerlessAccountContractAddress( - secret, - INITIAL_TEST_ACCOUNT_SALTS[i], INITIAL_TEST_SIGNING_KEYS[i], + INITIAL_TEST_ACCOUNT_SALTS[i], + secret, ), })), ); @@ -53,16 +52,17 @@ export async function generateSchnorrAccounts( numberOfAccounts: number, type: InitialAccountType = 'schnorr_initializerless', ): Promise { - const secrets = Array.from({ length: numberOfAccounts }, () => Fr.random()); + const signingKeys = Array.from({ length: numberOfAccounts }, () => GrumpkinScalar.random()); return await Promise.all( - secrets.map(async secret => { + signingKeys.map(async signingKey => { const salt = Fr.random(); + const secret = await deriveSecretKeyFromSigningKey(signingKey); return { secret, - signingKey: deriveSigningKey(secret), + signingKey, salt, type, - address: await getTestAccountAddress(type, secret, salt), + address: await getTestAccountAddress(type, signingKey, salt, secret), }; }), ); diff --git a/yarn-project/accounts/src/utils/index.ts b/yarn-project/accounts/src/utils/index.ts index ba7c543a0645..56824f63ea66 100644 --- a/yarn-project/accounts/src/utils/index.ts +++ b/yarn-project/accounts/src/utils/index.ts @@ -1 +1,2 @@ +export * from './key_derivation.js'; export * from './ssh_agent.js'; diff --git a/yarn-project/accounts/src/utils/key_derivation.test.ts b/yarn-project/accounts/src/utils/key_derivation.test.ts new file mode 100644 index 000000000000..3cbd046626fa --- /dev/null +++ b/yarn-project/accounts/src/utils/key_derivation.test.ts @@ -0,0 +1,18 @@ +import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; + +import { deriveSecretKeyFromSigningKey } from './key_derivation.js'; + +describe('deriveSecretKeyFromSigningKey', () => { + it('derives a deterministic, signing-key-specific secret key', async () => { + const signingKey = GrumpkinScalar.fromHexString('0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'); + const secretKey = await deriveSecretKeyFromSigningKey(signingKey); + // The value feeds the account address, so it is locked here to catch any silent change to the derivation. + expect(secretKey.toString()).toEqual('0x21a8894e479037a29ac0dfd569f9adab7fa4a2a215f7f1884b30df383e54b55b'); + + expect((await deriveSecretKeyFromSigningKey(signingKey)).toString()).toEqual(secretKey.toString()); + const otherSigningKey = GrumpkinScalar.fromHexString( + '202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e', + ); + expect((await deriveSecretKeyFromSigningKey(otherSigningKey)).toString()).not.toEqual(secretKey.toString()); + }); +}); diff --git a/yarn-project/accounts/src/utils/key_derivation.ts b/yarn-project/accounts/src/utils/key_derivation.ts new file mode 100644 index 000000000000..5446a9bec272 --- /dev/null +++ b/yarn-project/accounts/src/utils/key_derivation.ts @@ -0,0 +1,15 @@ +import { poseidon2Hash } from '@aztec/foundation/crypto/poseidon'; +import { sha256ToField } from '@aztec/foundation/crypto/sha256'; +import type { Fr } from '@aztec/foundation/curves/bn254'; +import type { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; + +const SIGNING_KEY_TO_SECRET_KEY_SEPARATOR = sha256ToField([Buffer.from('@aztec/accounts/signing_key_to_secret_key')]); + +/** + * Derives the privacy secret key (the seed for the viewing/nullifier keyset that PXE holds) from the account's signing + * key. The signing key is the ownership root the user controls; the privacy keyset hangs off it through this one-way + * hash, so a value handled by PXE can never be used to recover the signing key. + */ +export function deriveSecretKeyFromSigningKey(signingKey: GrumpkinScalar): Promise { + return poseidon2Hash([SIGNING_KEY_TO_SECRET_KEY_SEPARATOR, signingKey.hi, signingKey.lo]); +} 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 4092c1fce247..6530060f0e55 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 @@ -93,7 +93,7 @@ describe('e2e_contract_updates', () => { signingKey, salt, type: 'schnorr_initializerless' as const, - address: await getSchnorrInitializerlessAccountContractAddress(senderPrivateKey, salt, signingKey), + address: await getSchnorrInitializerlessAccountContractAddress(signingKey, salt, senderPrivateKey), }; defaultAccountAddress = account.address; 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 7e6014e9f378..c0aae518aa09 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 @@ -34,7 +34,7 @@ describe('e2e_multiple_accounts_1_enc_key', () => { // A different signing key for each account. const signingKey = GrumpkinScalar.random(); const salt = Fr.random(); - const address = await getSchnorrInitializerlessAccountContractAddress(secret, salt, signingKey); + const address = await getSchnorrInitializerlessAccountContractAddress(signingKey, salt, secret); return { secret, signingKey, 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 a0fc4bb8c692..6a3e9cb17271 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 @@ -33,9 +33,9 @@ async function main() { const extraAccountSalt = Fr.ZERO; const extraAccountSigningKey = GrumpkinScalar.random(); const extraAccountAddress = await getSchnorrAccountContractAddress( - extraAccountSecret, - extraAccountSalt, extraAccountSigningKey, + extraAccountSalt, + extraAccountSecret, ); logger.info('Starting wallet service...', { l1RpcUrls }); From 1944a30bbb8a7e4a4a9d26868e640a27729354e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 1 Jul 2026 15:57:44 +0000 Subject: [PATCH 02/11] feat(cli-wallet)!: root accounts on the signing key Renames the account flag from --secret-key to --signing-key: the signing key is now the account's root and the privacy secret is derived from it, instead of deriving the signing key from the privacy secret. SSH accounts keep an independent random secret since their key is external. The wallet DB persists both keys. Also splits the ambiguous createOrRetrieveAccount into separate createAccount and retrieveAccount, and wires up the previously-unhandled ecdsasecp256k1 account type. BREAKING CHANGE: --secret-key is renamed to --signing-key and account addresses change. --- .../cli-wallet/src/cmds/create_account.ts | 32 ++-- .../cli-wallet/src/cmds/deploy_account.ts | 2 +- .../src/cmds/import_test_accounts.ts | 9 +- yarn-project/cli-wallet/src/cmds/index.ts | 20 ++- .../cli-wallet/src/storage/wallet_db.ts | 26 ++- .../cli-wallet/src/utils/options/options.ts | 10 +- yarn-project/cli-wallet/src/utils/wallet.ts | 155 ++++++++++-------- yarn-project/cli/src/utils/commands.ts | 25 +++ 8 files changed, 179 insertions(+), 100 deletions(-) diff --git a/yarn-project/cli-wallet/src/cmds/create_account.ts b/yarn-project/cli-wallet/src/cmds/create_account.ts index b883d23c0e15..9b610fb5b00f 100644 --- a/yarn-project/cli-wallet/src/cmds/create_account.ts +++ b/yarn-project/cli-wallet/src/cmds/create_account.ts @@ -1,3 +1,4 @@ +import { deriveSecretKeyFromSigningKey } from '@aztec/accounts/utils'; import { NO_FROM } from '@aztec/aztec.js/account'; import { AztecAddress } from '@aztec/aztec.js/addresses'; import { NO_WAIT } from '@aztec/aztec.js/contracts'; @@ -5,6 +6,7 @@ import { type AztecNode, waitForTx } from '@aztec/aztec.js/node'; import type { DeployAccountOptions } from '@aztec/aztec.js/wallet'; import { prettyPrintJSON } from '@aztec/cli/cli-utils'; import { Fr } from '@aztec/foundation/curves/bn254'; +import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import type { LogFn, Logger } from '@aztec/foundation/log'; import { type TxHash, type TxReceipt, TxStatus } from '@aztec/stdlib/tx'; @@ -18,7 +20,7 @@ export async function createAccount( wallet: CLIWallet, aztecNode: AztecNode, accountType: AccountType, - secretKey: Fr | undefined, + signingKey: GrumpkinScalar | undefined, salt: Fr | undefined, publicKey: string | undefined, alias: string | undefined, @@ -35,15 +37,17 @@ export async function createAccount( debugLogger: Logger, log: LogFn, ) { - secretKey ??= Fr.random(); - - const account = await wallet.createOrRetrieveAccount( - undefined /* address, we don't have it yet */, - secretKey, - accountType, - salt, - publicKey, - ); + let secretKey: Fr; + if (accountType === 'ecdsasecp256r1ssh') { + // SSH accounts sign with a key held in the agent, and so we cannot derive their privacy secret key from it. Insted + // we pick a random value. + secretKey = Fr.random(); + } else { + signingKey ??= GrumpkinScalar.random(); + secretKey = await deriveSecretKeyFromSigningKey(signingKey); + } + + const account = await wallet.createAccount(accountType, signingKey, secretKey, salt, publicKey); const instanceSalt = account.getInstance().salt; const { address, publicKeys, partialAddress } = await account.getCompleteAddress(); @@ -51,6 +55,9 @@ export async function createAccount( if (json) { out.address = address; out.publicKey = publicKeys; + if (signingKey) { + out.signingKey = signingKey; + } if (secretKey) { out.secretKey = secretKey; } @@ -61,6 +68,9 @@ export async function createAccount( log(`\nNew account:\n`); log(`Address: ${address.toString()}`); log(`Public key: ${publicKeys.toString()}`); + if (signingKey) { + log(`Signing key: ${signingKey.toString()}`); + } if (secretKey) { log(`Secret key: ${secretKey.toString()}`); } @@ -162,5 +172,5 @@ export async function createAccount( } } - return { alias, address, secretKey, salt: instanceSalt }; + return { alias, address, signingKey, secretKey, salt: instanceSalt }; } diff --git a/yarn-project/cli-wallet/src/cmds/deploy_account.ts b/yarn-project/cli-wallet/src/cmds/deploy_account.ts index 5177e426fd3e..240d4d5180ae 100644 --- a/yarn-project/cli-wallet/src/cmds/deploy_account.ts +++ b/yarn-project/cli-wallet/src/cmds/deploy_account.ts @@ -30,7 +30,7 @@ export async function deployAccount( ) { const out: Record = {}; - const account = await wallet.createOrRetrieveAccount(address); + const account = await wallet.retrieveAccount(address); const { partialAddress, publicKeys } = await account.getCompleteAddress(); const { initializationHash, salt } = account.getInstance(); 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 f531b91ed1c4..b8fdfd6d2300 100644 --- a/yarn-project/cli-wallet/src/cmds/import_test_accounts.ts +++ b/yarn-project/cli-wallet/src/cmds/import_test_accounts.ts @@ -18,7 +18,14 @@ export async function importTestAccounts(wallet: CLIWallet, db: WalletDB, json: const address = account.address; await db.storeAccount( address, - { type: 'schnorr_initializerless', secretKey: secret, salt, alias, publicKey: undefined }, + { + type: 'schnorr_initializerless', + signingKey: account.signingKey, + secretKey: secret, + salt, + alias, + publicKey: undefined, + }, log, ); diff --git a/yarn-project/cli-wallet/src/cmds/index.ts b/yarn-project/cli-wallet/src/cmds/index.ts index d6ca67c97ecb..b9d362bc93e0 100644 --- a/yarn-project/cli-wallet/src/cmds/index.ts +++ b/yarn-project/cli-wallet/src/cmds/index.ts @@ -4,7 +4,7 @@ import { ETHEREUM_HOSTS, PRIVATE_KEY, addOptions, - createSecretKeyOption, + createSigningKeyOption, l1ChainIdOption, parseBigint, parseFieldFromHexString, @@ -24,7 +24,7 @@ import { ARTIFACT_DESCRIPTION, CLIFeeArgs, aliasedAddressParser, - aliasedSecretKeyParser, + aliasedSigningKeyParser, aliasedTxHashParser, artifactPathFromPromiseOrAlias, artifactPathParser, @@ -96,8 +96,8 @@ export function injectCommands( 'Public key that identifies a private signing key stored outside of the wallet. Used for ECDSA SSH accounts over the secp256r1 curve.', ) .addOption( - createSecretKeyOption('Secret key for account. Uses random by default.', false, sk => - aliasedSecretKeyParser(sk, db), + createSigningKeyOption('Signing key for account. Uses random by default.', false, sk => + aliasedSigningKeyParser(sk, db), ).conflicts('public-key'), ) .addOption(createAliasOption('Alias for the account. Used for easy reference in subsequent commands.', !db)) @@ -124,7 +124,7 @@ export function injectCommands( const { type, from: parsedFromAddress, - secretKey, + signingKey, salt, wait, waitForStatus: waitForStatusStr, @@ -156,7 +156,7 @@ export function injectCommands( wallet, node, type, - secretKey, + signingKey, salt, publicKey, alias, @@ -174,8 +174,8 @@ export function injectCommands( log, ); if (db) { - const { address, alias, secretKey, salt } = accountCreationResult; - await db.storeAccount(address, { type, secretKey, salt, alias, publicKey }, log); + const { address, alias, signingKey, secretKey, salt } = accountCreationResult; + await db.storeAccount(address, { type, signingKey, secretKey, salt, alias, publicKey }, log); } }); @@ -399,7 +399,9 @@ export function injectCommands( .addOption(createContractAddressOption(db)) .addOption(createArtifactOption(db)) .addOption( - createSecretKeyOption("The sender's secret key", !db, sk => aliasedSecretKeyParser(sk, db)).conflicts('account'), + createSigningKeyOption("The sender's signing key", !db, sk => aliasedSigningKeyParser(sk, db)).conflicts( + 'account', + ), ) .addOption(createAuthwitnessOption('Authorization witness to use for the simulation', !db, db)) .addOption(createAccountOption('Alias or address of the account to simulate from', !db, db)) diff --git a/yarn-project/cli-wallet/src/storage/wallet_db.ts b/yarn-project/cli-wallet/src/storage/wallet_db.ts index fbb9b8eb70dd..e4e4af4a52c4 100644 --- a/yarn-project/cli-wallet/src/storage/wallet_db.ts +++ b/yarn-project/cli-wallet/src/storage/wallet_db.ts @@ -1,4 +1,5 @@ import { Fr } from '@aztec/foundation/curves/bn254'; +import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import type { LogFn } from '@aztec/foundation/log'; import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; @@ -88,13 +89,28 @@ export class WalletDB { address: AztecAddress, { type, + signingKey, secretKey, salt, alias, publicKey, - }: { type: AccountType; secretKey: Fr; salt: Fr; alias: string | undefined; publicKey: string | undefined }, + }: { + type: AccountType; + signingKey?: GrumpkinScalar; + secretKey?: Fr; + salt: Fr; + alias: string | undefined; + publicKey: string | undefined; + }, log: LogFn, ) { + // Even though the privacy secret key is sometimes derived from the signing key, we store it in the database + // regardless as that is not always the case. Local-key accounts also store their signing key, while external-key + // accounts such as SSH store only the privacy secret. + if (!secretKey) { + throw new Error('Cannot store account without a secret key'); + } + let publicSigningKey: Buffer | undefined; if (type === 'ecdsasecp256r1ssh' && publicKey) { publicSigningKey = extractECDSAPublicKeyFromBase64String(publicKey); @@ -106,6 +122,9 @@ export class WalletDB { } await this.#accounts.set(`${address.toString()}:type`, Buffer.from(type)); await this.#accounts.set(`${address.toString()}:sk`, secretKey.toBuffer()); + if (signingKey) { + await this.#accounts.set(`${address.toString()}:signing`, signingKey.toBuffer()); + } await this.#accounts.set(`${address.toString()}:salt`, salt.toBuffer()); if (publicSigningKey) { await this.#accounts.set(`${address.toString()}:publicSigningKey`, publicSigningKey); @@ -226,10 +245,13 @@ export class WalletDB { if (!secretKeyBuffer) { throw new Error(`Could not find ${address}:sk. Account "${address.toString}" does not exist on this wallet.`); } + const signingKeyBuffer = await this.#accounts.getAsync(`${address.toString()}:signing`); const secretKey = Fr.fromBuffer(secretKeyBuffer); + // External-key accounts (e.g. SSH) store no signing key. + const signingKey = signingKeyBuffer ? GrumpkinScalar.fromBuffer(signingKeyBuffer) : undefined; const salt = Fr.fromBuffer((await this.#accounts.getAsync(`${address.toString()}:salt`))!); const type = (await this.#accounts.getAsync(`${address.toString()}:type`))!.toString('utf8') as AccountType; - return { address, secretKey, salt, type }; + return { address, signingKey, secretKey, salt, type }; } async storeAlias(type: AliasType, key: string, value: Buffer, log: LogFn) { diff --git a/yarn-project/cli-wallet/src/utils/options/options.ts b/yarn-project/cli-wallet/src/utils/options/options.ts index 795234b93bd2..cf871daf9553 100644 --- a/yarn-project/cli-wallet/src/utils/options/options.ts +++ b/yarn-project/cli-wallet/src/utils/options/options.ts @@ -1,5 +1,5 @@ import { TxHash } from '@aztec/aztec.js/tx'; -import { parseAztecAddress, parseSecretKey, parseTxHash } from '@aztec/cli/utils'; +import { parseAztecAddress, parseSigningKey, parseTxHash } from '@aztec/cli/utils'; import { AuthWitness } from '@aztec/stdlib/auth-witness'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; @@ -64,13 +64,13 @@ export function aliasedAddressParser(defaultPrefix: AliasType, address: string, } } -export function aliasedSecretKeyParser(sk: string, db?: WalletDB) { +export function aliasedSigningKeyParser(sk: string, db?: WalletDB) { if (sk.startsWith('0x')) { - return parseSecretKey(sk); + return parseSigningKey(sk); } else { - const prefixed = `${sk.startsWith('accounts') ? '' : 'accounts'}:${sk.endsWith(':sk') ? sk : `${sk}:sk`}`; + const prefixed = `${sk.startsWith('accounts') ? '' : 'accounts'}:${sk.endsWith(':signing') ? sk : `${sk}:signing`}`; const rawSk = db ? db.tryRetrieveAlias(prefixed) : sk; - return parseSecretKey(rawSk); + return parseSigningKey(rawSk); } } diff --git a/yarn-project/cli-wallet/src/utils/wallet.ts b/yarn-project/cli-wallet/src/utils/wallet.ts index ffb0832a1110..7d9ffa73824a 100644 --- a/yarn-project/cli-wallet/src/utils/wallet.ts +++ b/yarn-project/cli-wallet/src/utils/wallet.ts @@ -1,4 +1,4 @@ -import { EcdsaRAccountContract, EcdsaRSSHAccountContract } from '@aztec/accounts/ecdsa'; +import { EcdsaKAccountContract, EcdsaRAccountContract, EcdsaRSSHAccountContract } from '@aztec/accounts/ecdsa'; import { StubEcdsaAccountContractArtifact, createStubEcdsaAccount } from '@aztec/accounts/ecdsa/stub'; import { SchnorrAccountContract, SchnorrInitializerlessAccountContract } from '@aztec/accounts/schnorr'; import { StubSchnorrAccountContractArtifact, createStubSchnorrAccount } from '@aztec/accounts/schnorr/stub'; @@ -15,6 +15,7 @@ import { TxSimulationResultWithAppOffset } from '@aztec/aztec.js/wallet'; import type { DefaultAccountEntrypointOptions } from '@aztec/entrypoints/account'; import { DefaultEntrypoint } from '@aztec/entrypoints/default'; import { Fr } from '@aztec/foundation/curves/bn254'; +import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import type { LogFn } from '@aztec/foundation/log'; import type { NotesFilter } from '@aztec/pxe/client/lazy'; import type { PXEConfig } from '@aztec/pxe/config'; @@ -23,7 +24,6 @@ import { createPXE, getPXEConfig } from '@aztec/pxe/server'; import { getStandardAuthRegistry } from '@aztec/standard-contracts/auth-registry'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { Gas, GasUsed } from '@aztec/stdlib/gas'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; import { NoteDao } from '@aztec/stdlib/note'; import type { SimulationOverrides, TxExecutionRequest, TxProvingResult } from '@aztec/stdlib/tx'; import { ExecutionPayload, mergeExecutionPayloads } from '@aztec/stdlib/tx'; @@ -152,7 +152,7 @@ export class CLIWallet extends BaseWallet { if (this.accountCache.has(address.toString())) { return this.accountCache.get(address.toString())!; } else { - const accountManager = await this.createOrRetrieveAccount(address); + const accountManager = await this.retrieveAccount(address); account = await accountManager.getAccount(); } @@ -162,7 +162,87 @@ export class CLIWallet extends BaseWallet { return account; } - private async createAccount(secret: Fr, salt: Fr, contract: AccountContract): Promise { + /** + * Creates an account from freshly supplied keys. SSH accounts sign with a key held in the agent (resolved from + * `publicKey`), every other type is rooted on `signingKey`, with `secretKey` as its privacy secret. + */ + async createAccount( + type: AccountType, + signingKey: GrumpkinScalar | undefined, + secretKey: Fr | undefined, + salt: Fr = Fr.ZERO, + publicKey?: string, + ): Promise { + const publicSigningKey = + type === 'ecdsasecp256r1ssh' ? await this.resolveSshPublicSigningKey(publicKey) : undefined; + return this.buildAccount(type, salt, signingKey, secretKey, publicSigningKey); + } + + /** + * Retrieves a previously stored account by address, loading its keys, type and salt from the wallet database. + */ + async retrieveAccount(address: AztecAddress): Promise { + if (!this.db) { + throw new Error('Cannot retrieve an account without a wallet database'); + } + const { type, signingKey, secretKey, salt } = await this.db.retrieveAccount(address); + const publicSigningKey = + type === 'ecdsasecp256r1ssh' ? await this.db.retrieveAccountMetadata(address, 'publicSigningKey') : undefined; + return this.buildAccount(type, salt, signingKey, secretKey, publicSigningKey); + } + + private async buildAccount( + type: AccountType, + salt: Fr, + signingKey: GrumpkinScalar | undefined, + secretKey: Fr | undefined, + publicSigningKey: Buffer | undefined, + ): Promise { + switch (type) { + case 'schnorr': + case 'schnorr_initializerless': + case 'ecdsasecp256r1': + case 'ecdsasecp256k1': { + if (!signingKey || !secretKey) { + throw new Error('Cannot build account without signing key and secret key'); + } + const contract = + type === 'schnorr' + ? new SchnorrAccountContract(signingKey) + : type === 'schnorr_initializerless' + ? new SchnorrInitializerlessAccountContract(signingKey) + : type === 'ecdsasecp256r1' + ? new EcdsaRAccountContract(signingKey.toBuffer()) + : new EcdsaKAccountContract(signingKey.toBuffer()); + return await this.materializeAccount(secretKey, salt, contract); + } + case 'ecdsasecp256r1ssh': { + if (!secretKey || !publicSigningKey) { + throw new Error('Cannot build SSH account without secret key and public signing key'); + } + return await this.materializeAccount(secretKey, salt, new EcdsaRSSHAccountContract(publicSigningKey)); + } + default: { + throw new Error(`Unsupported account type: ${type}`); + } + } + } + + private async resolveSshPublicSigningKey(publicKey: string | undefined): Promise { + if (!publicKey) { + throw new Error('Public key must be provided for ECDSA SSH account'); + } + const identities = await getIdentities(); + const foundIdentity = identities.find( + identity => identity.type === 'ecdsa-sha2-nistp256' && identity.publicKey === publicKey, + ); + if (!foundIdentity) { + throw new Error(`Identity for public key ${publicKey} not found in the SSH agent`); + } + return extractECDSAPublicKeyFromBase64String(foundIdentity.publicKey); + } + + private async materializeAccount(secret: Fr, salt: Fr, contract: AccountContract): Promise { const accountManager = await AccountManager.create(this, secret, contract, { salt }); const instance = accountManager.getInstance(); @@ -187,73 +267,6 @@ export class CLIWallet extends BaseWallet { return accountManager; } - async createOrRetrieveAccount( - address?: AztecAddress, - secretKey?: Fr, - type: AccountType = 'schnorr', - salt?: Fr, - publicKey?: string, - ): Promise { - let account; - - salt ??= Fr.ZERO; - - if (this.db && address) { - ({ type, secretKey, salt } = await this.db.retrieveAccount(address)); - } - - if (!secretKey) { - throw new Error('Cannot retrieve/create wallet without secret key'); - } - - switch (type) { - case 'schnorr': { - 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, - salt, - new EcdsaRAccountContract(deriveSigningKey(secretKey).toBuffer()), - ); - break; - } - case 'ecdsasecp256r1ssh': { - let publicSigningKey; - if (this.db && address) { - publicSigningKey = await this.db.retrieveAccountMetadata(address, 'publicSigningKey'); - } else if (publicKey) { - const identities = await getIdentities(); - const foundIdentity = identities.find( - identity => identity.type === 'ecdsa-sha2-nistp256' && identity.publicKey === publicKey, - ); - if (!foundIdentity) { - throw new Error(`Identity for public key ${publicKey} not found in the SSH agent`); - } - publicSigningKey = extractECDSAPublicKeyFromBase64String(foundIdentity.publicKey); - } else { - throw new Error('Public key must be provided for ECDSA SSH account'); - } - account = await this.createAccount(secretKey, salt, new EcdsaRSSHAccountContract(publicSigningKey)); - break; - } - default: { - throw new Error(`Unsupported account type: ${type}`); - } - } - - return account; - } - /** * Creates a stub account that impersonates the given address, allowing kernelless simulations * to bypass the account's authorization mechanisms via contract overrides. diff --git a/yarn-project/cli/src/utils/commands.ts b/yarn-project/cli/src/utils/commands.ts index f4c35cc816a3..35c2c0fde4b3 100644 --- a/yarn-project/cli/src/utils/commands.ts +++ b/yarn-project/cli/src/utils/commands.ts @@ -1,4 +1,5 @@ import { Fr } from '@aztec/foundation/curves/bn254'; +import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import { EthAddress } from '@aztec/foundation/eth-address'; import type { LogFn } from '@aztec/foundation/log'; import type { PXE } from '@aztec/pxe/server'; @@ -60,6 +61,16 @@ export const createSecretKeyOption = ( .argParser(argsParser ?? parseSecretKey) .makeOptionMandatory(mandatory); +export const createSigningKeyOption = ( + description: string, + mandatory: boolean, + argsParser?: (value: string, previous: GrumpkinScalar) => GrumpkinScalar, +) => + new Option('-sk, --signing-key ', description) + .env('SIGNING_KEY') + .argParser(argsParser ?? parseSigningKey) + .makeOptionMandatory(mandatory); + export const logJson = (log: LogFn) => (obj: object) => log(JSON.stringify(obj, null, 2)); /** @@ -370,6 +381,20 @@ export function parseSecretKey(secretKey: string): Fr { } } +/** + * Parses an account signing key from a string. + * @param signingKey - A string + * @returns A signing key + * @throws InvalidArgumentError if the input string is not valid. + */ +export function parseSigningKey(signingKey: string): GrumpkinScalar { + try { + return GrumpkinScalar.fromHexString(signingKey); + } catch { + throw new InvalidArgumentError(`Invalid signing key: ${signingKey}`); + } +} + /** * Parses a field from a string. * @param field - A string representing the field. From 514b06c17b6f1874a24515bbe68c5901af9fa4e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 1 Jul 2026 15:57:50 +0000 Subject: [PATCH 03/11] fix(wallets): use the schnorr stub for schnorr_initializerless accounts The embedded stub providers routed schnorr_initializerless accounts to the ECDSA stub, disagreeing with stubClassIds which registers them under the schnorr stub class. --- .../src/embedded/account-contract-providers/bundle.ts | 6 ++++-- .../wallets/src/embedded/account-contract-providers/lazy.ts | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) 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 cff7f0f89afe..5fd3575c6289 100644 --- a/yarn-project/wallets/src/embedded/account-contract-providers/bundle.ts +++ b/yarn-project/wallets/src/embedded/account-contract-providers/bundle.ts @@ -32,10 +32,12 @@ export class BundleAccountContractsProvider implements AccountContractsProvider } getStubAccountContractArtifact(type: AccountType): Promise { - return Promise.resolve(type === 'schnorr' ? StubSchnorrAccountContractArtifact : StubEcdsaAccountContractArtifact); + const isSchnorr = type === 'schnorr' || type === 'schnorr_initializerless'; + return Promise.resolve(isSchnorr ? StubSchnorrAccountContractArtifact : StubEcdsaAccountContractArtifact); } createStubAccount(address: CompleteAddress, type: AccountType): Promise { - return Promise.resolve(type === 'schnorr' ? createStubSchnorrAccount(address) : createStubEcdsaAccount(address)); + const isSchnorr = type === 'schnorr' || type === 'schnorr_initializerless'; + return Promise.resolve(isSchnorr ? createStubSchnorrAccount(address) : createStubEcdsaAccount(address)); } } 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 bec3d9c5d20f..3d18f4a76d0e 100644 --- a/yarn-project/wallets/src/embedded/account-contract-providers/lazy.ts +++ b/yarn-project/wallets/src/embedded/account-contract-providers/lazy.ts @@ -32,7 +32,7 @@ export class LazyAccountContractsProvider implements AccountContractsProvider { } async getStubAccountContractArtifact(type: AccountType): Promise { - if (type === 'schnorr') { + if (type === 'schnorr' || type === 'schnorr_initializerless') { const { getStubSchnorrAccountContractArtifact } = await import('@aztec/accounts/schnorr/stub/lazy'); return getStubSchnorrAccountContractArtifact(); } else { @@ -42,7 +42,7 @@ export class LazyAccountContractsProvider implements AccountContractsProvider { } async createStubAccount(address: CompleteAddress, type: AccountType): Promise { - if (type === 'schnorr') { + if (type === 'schnorr' || type === 'schnorr_initializerless') { const { createStubSchnorrAccount } = await import('@aztec/accounts/schnorr/stub/lazy'); return createStubSchnorrAccount(address); } else { From 8b8bb0cf4786a553e923cfe060f9cb9c56f01716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 1 Jul 2026 16:59:26 +0000 Subject: [PATCH 04/11] feat(wallets)!: require an explicit signing key for schnorr test accounts Removes the deriveSigningKey(secret) fallback from the embedded wallet and TestWallet createSchnorr* methods so the signing key is always supplied explicitly, and threads it through the worker registerAccount RPC. Callers pass their fixture's signing key or a fresh random one. BREAKING CHANGE: createSchnorrAccount/createSchnorrInitializerlessAccount now require a signing key argument. --- yarn-project/aztec/src/examples/token.ts | 12 ++++++++++-- yarn-project/bot/src/factory.ts | 4 +++- .../src/composed/e2e_persistence.test.ts | 12 ++++++++---- yarn-project/end-to-end/src/e2e_2_pxes.test.ts | 9 +++++++-- .../end-to-end/src/e2e_block_building.test.ts | 6 +++++- .../src/e2e_constrained_delivery.test.ts | 1 + .../end-to-end/src/e2e_p2p/add_rollup.test.ts | 1 + .../end-to-end/src/e2e_synching.test.ts | 2 +- .../end-to-end/src/fixtures/e2e_prover_test.ts | 7 ++++++- .../src/spartan/block_capacity.test.ts | 12 ++++-------- .../end-to-end/src/spartan/n_tps.test.ts | 12 ++++-------- .../end-to-end/src/spartan/n_tps_prove.test.ts | 12 ++++-------- .../src/spartan/setup_test_wallets.ts | 18 ++++++++++++------ .../end-to-end/src/test-wallet/test_wallet.ts | 7 ++----- .../src/test-wallet/wallet_worker_script.ts | 6 +++--- .../src/test-wallet/worker_wallet.ts | 6 +++--- .../src/test-wallet/worker_wallet_schema.ts | 2 +- .../wallets/src/embedded/embedded_wallet.ts | 11 ++++------- yarn-project/wallets/src/testing.ts | 2 +- 19 files changed, 80 insertions(+), 62 deletions(-) diff --git a/yarn-project/aztec/src/examples/token.ts b/yarn-project/aztec/src/examples/token.ts index fddb6704492f..4cd126301ce2 100644 --- a/yarn-project/aztec/src/examples/token.ts +++ b/yarn-project/aztec/src/examples/token.ts @@ -23,8 +23,16 @@ async function main() { // During local network setup we create a few initializerless accounts. Below we add them to our wallet. const [aliceInitialAccountData, bobInitialAccountData] = await getInitialTestAccountsData(); - await wallet.createSchnorrInitializerlessAccount(aliceInitialAccountData.secret, aliceInitialAccountData.salt); - await wallet.createSchnorrInitializerlessAccount(bobInitialAccountData.secret, bobInitialAccountData.salt); + await wallet.createSchnorrInitializerlessAccount( + aliceInitialAccountData.secret, + aliceInitialAccountData.salt, + aliceInitialAccountData.signingKey, + ); + await wallet.createSchnorrInitializerlessAccount( + bobInitialAccountData.secret, + bobInitialAccountData.salt, + bobInitialAccountData.signingKey, + ); const alice = aliceInitialAccountData.address; const bob = bobInitialAccountData.address; diff --git a/yarn-project/bot/src/factory.ts b/yarn-project/bot/src/factory.ts index d720ccdb83a2..ac4da66fe876 100644 --- a/yarn-project/bot/src/factory.ts +++ b/yarn-project/bot/src/factory.ts @@ -21,6 +21,7 @@ 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 { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import { EthAddress } from '@aztec/foundation/eth-address'; import { AMMContract } from '@aztec/noir-contracts.js/AMM'; import { PrivateTokenContract } from '@aztec/noir-contracts.js/PrivateToken'; @@ -69,7 +70,8 @@ export class BotFactory { recipient: AztecAddress; }> { const defaultAccountAddress = await this.setupAccount(); - const recipient = (await this.wallet.createSchnorrAccount(Fr.random(), Fr.random())).address; + const recipient = (await this.wallet.createSchnorrAccount(Fr.random(), Fr.random(), GrumpkinScalar.random())) + .address; await this.ensureFeeJuiceBalance(defaultAccountAddress); const token = await this.setupToken(defaultAccountAddress); await this.mintTokens(token, defaultAccountAddress); 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 2073a2b6b710..69d4f020c053 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 @@ -200,7 +200,11 @@ describe('Aztec persistence', () => { it('allows spending of private notes', async () => { const account = additionallyFundedAccounts[1]; // Not the owner account. - const otherAccount = await context.wallet.createSchnorrInitializerlessAccount(account.secret, account.salt); + const otherAccount = await context.wallet.createSchnorrInitializerlessAccount( + account.secret, + account.salt, + account.signingKey, + ); const otherAddress = otherAccount.address; const { result: initialOwnerBalance } = await contract.methods @@ -277,7 +281,7 @@ describe('Aztec persistence', () => { await context.wallet.registerContract(contractInstance, TokenBlacklistContract.artifact); const account = additionallyFundedAccounts[0]; - await context.wallet.createSchnorrInitializerlessAccount(account.secret, account.salt); + await context.wallet.createSchnorrInitializerlessAccount(account.secret, account.salt, account.signingKey); const contract = TokenBlacklistContract.at(contractAddress, context.wallet); // check that notes total more than 0 so that this test isn't dependent on run order @@ -307,7 +311,7 @@ describe('Aztec persistence', () => { await temporaryContext.wallet.registerContract(contractInstance, TokenBlacklistContract.artifact); const account = additionallyFundedAccounts[0]; - await context.wallet.createSchnorrInitializerlessAccount(account.secret, account.salt); + await context.wallet.createSchnorrInitializerlessAccount(account.secret, account.salt, account.signingKey); const contract = TokenBlacklistContract.at(contractAddress, context.wallet); @@ -332,7 +336,7 @@ describe('Aztec persistence', () => { beforeEach(async () => { context = await setup(0, { ...PIPELINING_SETUP_OPTS, dataDirectory, deployL1ContractsValues }, { dataDirectory }); const account = additionallyFundedAccounts[0]; - await context.wallet.createSchnorrInitializerlessAccount(account.secret, account.salt); + await context.wallet.createSchnorrInitializerlessAccount(account.secret, account.salt, account.signingKey); contract = TokenBlacklistContract.at(contractAddress, context.wallet); }, 120_000); 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 c0ae9f5d1f69..3e0d1f610a03 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 @@ -44,6 +44,7 @@ describe('e2e_2_pxes', () => { const accountManager = await wallet.createSchnorrAccount( fundedAccounts[accountIndex].secret, fundedAccounts[accountIndex].salt, + fundedAccounts[accountIndex].signingKey, ); const deployMethod = await accountManager.getDeployMethod(); await deployMethod.send({ from: NO_FROM }); @@ -212,13 +213,17 @@ describe('e2e_2_pxes', () => { // setup an account that is shared across PXEs const sharedAccount = additionallyFundedAccounts[2]; - const sharedAccountOnAManager = await walletA.createSchnorrAccount(sharedAccount.secret, sharedAccount.salt); + const sharedAccountOnAManager = await walletA.createSchnorrAccount( + sharedAccount.secret, + sharedAccount.salt, + sharedAccount.signingKey, + ); const sharedAccountOnADeployMethod = await sharedAccountOnAManager.getDeployMethod(); await sharedAccountOnADeployMethod.send({ from: NO_FROM }); const sharedAccountAddress = sharedAccountOnAManager.address; // Register the shared account on walletB. - await walletB.createSchnorrAccount(sharedAccount.secret, sharedAccount.salt); + await walletB.createSchnorrAccount(sharedAccount.secret, sharedAccount.salt, sharedAccount.signingKey); // deploy the contract on PXE A const { contract: token, instance } = await deployToken(walletA, accountAAddress, initialBalance, logger); 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 eb52b960940f..11cc5550ced2 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 @@ -562,7 +562,11 @@ describe('e2e_block_building', () => { const [accountData] = context.additionallyFundedAccounts; - const accountManager = await (wallet as TestWallet).createSchnorrAccount(accountData.secret, accountData.salt); + const accountManager = await (wallet as TestWallet).createSchnorrAccount( + accountData.secret, + accountData.salt, + accountData.signingKey, + ); const deployMethod = await accountManager.getDeployMethod(); await deployMethod.send({ from: NO_FROM, diff --git a/yarn-project/end-to-end/src/e2e_constrained_delivery.test.ts b/yarn-project/end-to-end/src/e2e_constrained_delivery.test.ts index 5ee4f32ef33a..6e2b9803acd2 100644 --- a/yarn-project/end-to-end/src/e2e_constrained_delivery.test.ts +++ b/yarn-project/end-to-end/src/e2e_constrained_delivery.test.ts @@ -199,6 +199,7 @@ describe('cross-PXE constrained delivery', () => { const recipientAccount = await walletRecipient.createSchnorrAccount( additionallyFundedAccounts[0].secret, additionallyFundedAccounts[0].salt, + additionallyFundedAccounts[0].signingKey, ); await (await recipientAccount.getDeployMethod()).send({ from: NO_FROM }); recipient = recipientAccount.address; 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 c677bbe2f9a4..6d6d2e99aaf8 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 @@ -301,6 +301,7 @@ describe('e2e_p2p_add_rollup', () => { const aliceAccountManager = await wallet.createSchnorrInitializerlessAccount( aliceAccount.secret, aliceAccount.salt, + aliceAccount.signingKey, ); const aliceAddress = aliceAccountManager.address; 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 55030ee90604..402a4c3eba27 100644 --- a/yarn-project/end-to-end/src/e2e_synching.test.ts +++ b/yarn-project/end-to-end/src/e2e_synching.test.ts @@ -153,7 +153,7 @@ class TestVariant { async deployAccounts(accounts: InitialAccountData[]) { // Create accounts such that we can send from many to not have colliding nullifiers const managers = await Promise.all( - accounts.map(account => this.wallet.createSchnorrAccount(account.secret, account.salt)), + accounts.map(account => this.wallet.createSchnorrAccount(account.secret, account.salt, account.signingKey)), ); await Promise.all( managers.map(async m => { 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 b30afad6e913..9407df8ef96a 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 @@ -191,8 +191,13 @@ export class FullProverTest { await provenWallet.createSchnorrInitializerlessAccount( this.fundedAccounts[i].secret, this.fundedAccounts[i].salt, + this.fundedAccounts[i].signingKey, + ); + await this.wallet.createSchnorrInitializerlessAccount( + this.fundedAccounts[i].secret, + this.fundedAccounts[i].salt, + this.fundedAccounts[i].signingKey, ); - await this.wallet.createSchnorrInitializerlessAccount(this.fundedAccounts[i].secret, this.fundedAccounts[i].salt); } const asset = TokenContract.at(this.fakeProofsAsset.address, provenWallet); diff --git a/yarn-project/end-to-end/src/spartan/block_capacity.test.ts b/yarn-project/end-to-end/src/spartan/block_capacity.test.ts index 8a3fcbc5c3e2..b83092761982 100644 --- a/yarn-project/end-to-end/src/spartan/block_capacity.test.ts +++ b/yarn-project/end-to-end/src/spartan/block_capacity.test.ts @@ -9,12 +9,12 @@ import { asyncPool } from '@aztec/foundation/async-pool'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { times } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; +import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import { createLogger } from '@aztec/foundation/log'; import { retryUntil } from '@aztec/foundation/retry'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import { BenchmarkingContract } from '@aztec/noir-test-contracts.js/Benchmarking'; import { GasFees } from '@aztec/stdlib/gas'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; import { Tx } from '@aztec/stdlib/tx'; import { jest } from '@jest/globals'; @@ -108,14 +108,10 @@ describe('block capacity benchmark', () => { wallets.map(async wallet => { const secret = Fr.random(); const salt = Fr.random(); - const address = await wallet.registerAccount(secret, salt); + const signingKey = GrumpkinScalar.random(); + const address = await wallet.registerAccount(secret, salt, signingKey); await registerSponsoredFPC(wallet); - const manager = await AccountManager.create( - wallet, - secret, - new SchnorrAccountContract(deriveSigningKey(secret)), - { salt }, - ); + const manager = await AccountManager.create(wallet, secret, new SchnorrAccountContract(signingKey), { salt }); const deployMethod = await manager.getDeployMethod(); await deployMethod.send({ from: NO_FROM, diff --git a/yarn-project/end-to-end/src/spartan/n_tps.test.ts b/yarn-project/end-to-end/src/spartan/n_tps.test.ts index 13d2cd7dabe0..44362527367e 100644 --- a/yarn-project/end-to-end/src/spartan/n_tps.test.ts +++ b/yarn-project/end-to-end/src/spartan/n_tps.test.ts @@ -10,13 +10,13 @@ import { BlockNumber } from '@aztec/foundation/branded-types'; import { times, timesParallel } from '@aztec/foundation/collection'; import { randomBigInt } from '@aztec/foundation/crypto/random'; import { Fr } from '@aztec/foundation/curves/bn254'; +import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import { type Logger, createLogger } from '@aztec/foundation/log'; import { RunningPromise } from '@aztec/foundation/promise'; import { retryUntil } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; import { BenchmarkingContract } from '@aztec/noir-test-contracts.js/Benchmarking'; import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; import { TopicType } from '@aztec/stdlib/p2p'; import { Tx, TxHash, TxStatus } from '@aztec/stdlib/tx'; import { getGasLimits } from '@aztec/wallet-sdk/base-wallet'; @@ -319,14 +319,10 @@ describe('sustained N TPS test', () => { wallets.map(async wallet => { const secret = Fr.random(); const salt = Fr.random(); - const address = await wallet.registerAccount(secret, salt); + const signingKey = GrumpkinScalar.random(); + const address = await wallet.registerAccount(secret, salt, signingKey); await registerSponsoredFPC(wallet); - const manager = await AccountManager.create( - wallet, - secret, - new SchnorrAccountContract(deriveSigningKey(secret)), - { salt }, - ); + const manager = await AccountManager.create(wallet, secret, new SchnorrAccountContract(signingKey), { salt }); const deployMethod = await manager.getDeployMethod(); // Explicit gas estimation: BaseWallet's fallback bakes ~196_608 daGas into deploys, which exceeds // the proposer's per-block fair-share daGas (~94k at 10 blocks/checkpoint diff --git a/yarn-project/end-to-end/src/spartan/n_tps_prove.test.ts b/yarn-project/end-to-end/src/spartan/n_tps_prove.test.ts index 7bc23728af59..89d091ee3ef7 100644 --- a/yarn-project/end-to-end/src/spartan/n_tps_prove.test.ts +++ b/yarn-project/end-to-end/src/spartan/n_tps_prove.test.ts @@ -11,13 +11,13 @@ import { EthCheatCodesWithState } from '@aztec/ethereum/test'; import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { timesParallel } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; +import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import { type Logger, createLogger } from '@aztec/foundation/log'; import { retryUntil } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; import { DateProvider, Timer } from '@aztec/foundation/timer'; import { AvmGadgetsTestContract } from '@aztec/noir-test-contracts.js/AvmGadgetsTest'; import { GasFees } from '@aztec/stdlib/gas'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; import { Tx, TxHash } from '@aztec/stdlib/tx'; import { jest } from '@jest/globals'; @@ -312,14 +312,10 @@ describe(`prove ${TARGET_TPS}TPS test`, () => { wallets.map(async wallet => { const secret = Fr.random(); const salt = Fr.random(); - const address = await wallet.registerAccount(secret, salt); + const signingKey = GrumpkinScalar.random(); + const address = await wallet.registerAccount(secret, salt, signingKey); await registerSponsoredFPC(wallet); - const manager = await AccountManager.create( - wallet, - secret, - new SchnorrAccountContract(deriveSigningKey(secret)), - { salt }, - ); + const manager = await AccountManager.create(wallet, secret, new SchnorrAccountContract(signingKey), { salt }); const deployMethod = await manager.getDeployMethod(); await deployMethod.send({ from: NO_FROM, diff --git a/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts b/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts index 985920f898df..7ab7de69d3ab 100644 --- a/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts +++ b/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts @@ -86,8 +86,10 @@ export async function deploySponsoredTestAccountsWithTokens( numberOfFundedWallets = 1, ): Promise { const [recipient, ...funded] = await generateSchnorrAccounts(numberOfFundedWallets + 1); - const recipientAccount = await wallet.createSchnorrAccount(recipient.secret, recipient.salt); - const fundedAccounts = await Promise.all(funded.map(a => wallet.createSchnorrAccount(a.secret, a.salt))); + const recipientAccount = await wallet.createSchnorrAccount(recipient.secret, recipient.salt, recipient.signingKey); + const fundedAccounts = await Promise.all( + funded.map(a => wallet.createSchnorrAccount(a.secret, a.salt, a.signingKey)), + ); await registerSponsoredFPC(wallet); @@ -233,8 +235,10 @@ export async function deploySponsoredTestAccounts( opts?: { estimateGas?: boolean }, ): Promise { const [recipient, ...funded] = await generateSchnorrAccounts(numberOfFundedWallets + 1); - const recipientAccount = await wallet.createSchnorrAccount(recipient.secret, recipient.salt); - const fundedAccounts = await Promise.all(funded.map(a => wallet.createSchnorrAccount(a.secret, a.salt))); + const recipientAccount = await wallet.createSchnorrAccount(recipient.secret, recipient.salt, recipient.signingKey); + const fundedAccounts = await Promise.all( + funded.map(a => wallet.createSchnorrAccount(a.secret, a.salt, a.signingKey)), + ); await registerSponsoredFPC(wallet); @@ -278,8 +282,10 @@ export async function deployTestAccountsWithTokens( const wallet = await TestWallet.create(aztecNode); const [recipient, ...funded] = await generateSchnorrAccounts(numberOfFundedWallets + 1); - const recipientAccount = await wallet.createSchnorrAccount(recipient.secret, recipient.salt); - const fundedAccounts = await Promise.all(funded.map(a => wallet.createSchnorrAccount(a.secret, a.salt))); + const recipientAccount = await wallet.createSchnorrAccount(recipient.secret, recipient.salt, recipient.signingKey); + const fundedAccounts = await Promise.all( + funded.map(a => wallet.createSchnorrAccount(a.secret, a.salt, a.signingKey)), + ); const claims = await Promise.all( fundedAccounts.map(a => bridgeL1FeeJuice(l1RpcUrls, mnemonicOrPrivateKey, aztecNode, a.address, undefined, logger)), 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 f6b4389e34d4..5e3f75f7865a 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 @@ -27,7 +27,6 @@ import { PXE, type PXECreationOptions, createPXE } from '@aztec/pxe/server'; import { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { getContractClassFromArtifact } from '@aztec/stdlib/contract'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; import type { NoteDao } from '@aztec/stdlib/note'; import { type BlockHeader, @@ -92,13 +91,11 @@ export class TestWallet extends BaseWallet { this.nodeRef.updateTargetNode(node); } - createSchnorrAccount(secret: Fr, salt: Fr, signingKey?: Fq): Promise { - signingKey = signingKey ?? deriveSigningKey(secret); + createSchnorrAccount(secret: Fr, salt: Fr, signingKey: Fq): Promise { return this.createAccount({ secret, salt, type: 'schnorr', contract: new SchnorrAccountContract(signingKey) }); } - createSchnorrInitializerlessAccount(secret: Fr, salt: Fr, signingKey?: Fq): Promise { - signingKey = signingKey ?? deriveSigningKey(secret); + createSchnorrInitializerlessAccount(secret: Fr, salt: Fr, signingKey: Fq): Promise { return this.createAccount({ secret, salt, diff --git a/yarn-project/end-to-end/src/test-wallet/wallet_worker_script.ts b/yarn-project/end-to-end/src/test-wallet/wallet_worker_script.ts index de104bc4a964..67b540bb6fa0 100644 --- a/yarn-project/end-to-end/src/test-wallet/wallet_worker_script.ts +++ b/yarn-project/end-to-end/src/test-wallet/wallet_worker_script.ts @@ -3,7 +3,7 @@ import type { SendOptions } from '@aztec/aztec.js/wallet'; import { BackendType, BarretenbergSync } from '@aztec/bb.js'; import { jsonStringify } from '@aztec/foundation/json-rpc'; import { createLogger } from '@aztec/foundation/log'; -import type { ApiSchema, Fr } from '@aztec/foundation/schemas'; +import type { ApiSchema, Fq, Fr } from '@aztec/foundation/schemas'; import { getSchemaParameters, parseWithOptionals, schemaHasMethod } from '@aztec/foundation/schemas'; import { NodeListener, TransportServer } from '@aztec/foundation/transport'; import { ExecutionPayload, Tx } from '@aztec/stdlib/tx'; @@ -36,8 +36,8 @@ try { provenTx.publicFunctionCalldata, ); }, - registerAccount: async (secret: Fr, salt: Fr) => { - const manager = await wallet.createSchnorrAccount(secret, salt); + registerAccount: async (secret: Fr, salt: Fr, signingKey: Fq) => { + const manager = await wallet.createSchnorrAccount(secret, salt, signingKey); return manager.address; }, }; diff --git a/yarn-project/end-to-end/src/test-wallet/worker_wallet.ts b/yarn-project/end-to-end/src/test-wallet/worker_wallet.ts index df93dceafde1..d59d17f344bc 100644 --- a/yarn-project/end-to-end/src/test-wallet/worker_wallet.ts +++ b/yarn-project/end-to-end/src/test-wallet/worker_wallet.ts @@ -18,7 +18,7 @@ import type { WalletCapabilities, } from '@aztec/aztec.js/wallet'; import type { ChainInfo } from '@aztec/entrypoints/interfaces'; -import type { Fr } from '@aztec/foundation/curves/bn254'; +import type { Fq, Fr } from '@aztec/foundation/curves/bn254'; import { jsonStringify } from '@aztec/foundation/json-rpc'; import { createLogger } from '@aztec/foundation/log'; import { promiseWithResolvers } from '@aztec/foundation/promise'; @@ -194,8 +194,8 @@ export class WorkerWallet implements Wallet { } /** Registers an account inside the worker's TestWallet, populating its accounts map. */ - registerAccount(secret: Fr, salt: Fr): Promise { - return this.call('registerAccount', secret, salt); + registerAccount(secret: Fr, salt: Fr, signingKey: Fq): Promise { + return this.call('registerAccount', secret, salt, signingKey); } createAuthWit(from: AztecAddress, messageHashOrIntent: IntentInnerHash | CallIntent): Promise { diff --git a/yarn-project/end-to-end/src/test-wallet/worker_wallet_schema.ts b/yarn-project/end-to-end/src/test-wallet/worker_wallet_schema.ts index ad6239219822..d7b4a73a57b3 100644 --- a/yarn-project/end-to-end/src/test-wallet/worker_wallet_schema.ts +++ b/yarn-project/end-to-end/src/test-wallet/worker_wallet_schema.ts @@ -10,5 +10,5 @@ import { z } from 'zod'; export const WorkerWalletSchema: ApiSchema = { ...WalletSchema, proveTx: z.function({ input: z.tuple([ExecutionPayloadSchema, SendOptionsSchema]), output: Tx.schema }), - registerAccount: z.function({ input: z.tuple([schemas.Fr, schemas.Fr]), output: AztecAddress.schema }), + registerAccount: z.function({ input: z.tuple([schemas.Fr, schemas.Fr, schemas.Fq]), output: AztecAddress.schema }), }; diff --git a/yarn-project/wallets/src/embedded/embedded_wallet.ts b/yarn-project/wallets/src/embedded/embedded_wallet.ts index ef195efbed97..29f07ccd5dc2 100644 --- a/yarn-project/wallets/src/embedded/embedded_wallet.ts +++ b/yarn-project/wallets/src/embedded/embedded_wallet.ts @@ -31,7 +31,6 @@ import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { type ContractInstanceWithAddress, getContractClassFromArtifact } from '@aztec/stdlib/contract'; import { GasSettings } from '@aztec/stdlib/gas'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; import { type ContractOverrides, ExecutionPayload, @@ -459,14 +458,12 @@ export class EmbeddedWallet extends BaseWallet { return accountManager; } - createSchnorrAccount(secret: Fr, salt: Fr, signingKey?: Fq, alias?: string): Promise { - const sk = signingKey ?? deriveSigningKey(secret); - return this.createAndStoreAccount(alias ?? '', 'schnorr', secret, salt, sk.toBuffer()); + createSchnorrAccount(secret: Fr, salt: Fr, signingKey: Fq, alias?: string): Promise { + return this.createAndStoreAccount(alias ?? '', 'schnorr', secret, salt, signingKey.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()); + createSchnorrInitializerlessAccount(secret: Fr, salt: Fr, signingKey: Fq, alias?: string): Promise { + return this.createAndStoreAccount(alias ?? '', 'schnorr_initializerless', secret, salt, signingKey.toBuffer()); } createECDSARAccount(secret: Fr, salt: Fr, signingKey: Buffer, alias?: string): Promise { diff --git a/yarn-project/wallets/src/testing.ts b/yarn-project/wallets/src/testing.ts index 791ed6028944..25967613a8d7 100644 --- a/yarn-project/wallets/src/testing.ts +++ b/yarn-project/wallets/src/testing.ts @@ -5,7 +5,7 @@ import type { Fq, Fr } from '@aztec/foundation/curves/bn254'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; interface WalletWithSchnorrAccounts { - createSchnorrInitializerlessAccount(secret: Fr, salt: Fr, signingKey?: Fq, alias?: string): Promise; + createSchnorrInitializerlessAccount(secret: Fr, salt: Fr, signingKey: Fq, alias?: string): Promise; } /** From 68ef2f48c292b736a88ba9351acafa1650dd87a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 1 Jul 2026 17:05:14 +0000 Subject: [PATCH 05/11] refactor!: delete deriveSigningKey Removes the last usages of deriveSigningKey (which derived the account signing key from the privacy secret) and deletes the function, closing out #5837. The bot now treats its configured senderPrivateKey as the signing key and derives the privacy secret from it; the remaining test callers pass an explicit random signing key. BREAKING CHANGE: deriveSigningKey is removed; the bot's account address changes for a given senderPrivateKey. --- yarn-project/bot/src/factory.ts | 10 +++++++--- .../src/bench/client_flows/client_flows_benchmark.ts | 4 ++-- .../end-to-end/src/e2e_account_contracts.test.ts | 3 +-- .../end-to-end/src/e2e_contract_updates.test.ts | 5 ++--- .../end-to-end/src/spartan/mempool_limit.test.ts | 1 - yarn-project/stdlib/src/keys/derivation.ts | 5 ----- 6 files changed, 12 insertions(+), 16 deletions(-) diff --git a/yarn-project/bot/src/factory.ts b/yarn-project/bot/src/factory.ts index ac4da66fe876..53eb611c7ae1 100644 --- a/yarn-project/bot/src/factory.ts +++ b/yarn-project/bot/src/factory.ts @@ -1,4 +1,5 @@ import { getInitialTestAccountsData } from '@aztec/accounts/testing'; +import { deriveSecretKeyFromSigningKey } from '@aztec/accounts/utils'; import { AztecAddress } from '@aztec/aztec.js/addresses'; import { BatchCall, @@ -30,7 +31,6 @@ import { TestContract } from '@aztec/noir-test-contracts.js/Test'; import type { BlockTag } from '@aztec/stdlib/block'; import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; import type { AztecNode, AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; import { EmbeddedWallet } from '@aztec/wallets/embedded'; import { type BotConfig, SupportedTokenContracts } from './config.js'; @@ -223,9 +223,13 @@ export class BotFactory { return accountManager.address; } - private async setupAccountWithPrivateKey(secret: Fr) { + private async setupAccountWithPrivateKey(privateKey: Fr) { const salt = this.config.senderSalt ?? Fr.ONE; - const signingKey = deriveSigningKey(secret); + // The configured private key is the account's signing key. Reinterpret its bytes as a Grumpkin scalar (always + // valid, since the BN254 scalar field is smaller than Grumpkin's) and derive the privacy secret from it, keeping + // the signing key independent of anything PXE holds. + const signingKey = GrumpkinScalar.fromBuffer(privateKey.toBuffer()); + const secret = await deriveSecretKeyFromSigningKey(signingKey); const accountManager = await this.wallet.createSchnorrInitializerlessAccount(secret, salt, signingKey); return accountManager.address; } 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 c8b423f302c6..09fd5117f54b 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 @@ -13,6 +13,7 @@ import { deployL1Contract } from '@aztec/ethereum/deploy-l1-contract'; import { ChainMonitor } from '@aztec/ethereum/test'; import { randomBytes } from '@aztec/foundation/crypto/random'; import { Fr } from '@aztec/foundation/curves/bn254'; +import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import { EthAddress } from '@aztec/foundation/eth-address'; import { TestERC20Abi } from '@aztec/l1-artifacts/TestERC20Abi'; import { TestERC20Bytecode } from '@aztec/l1-artifacts/TestERC20Bytecode'; @@ -26,7 +27,6 @@ import { getCanonicalFeeJuice } from '@aztec/protocol-contracts/fee-juice'; import { type PXEConfig, getPXEConfig } from '@aztec/pxe/server'; import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; import { Gas, GasSettings } from '@aztec/stdlib/gas'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; import { AUTOMINE_E2E_OPTS, @@ -191,7 +191,7 @@ export class ClientFlowsBenchmark { let benchysPrivateSigningKey; if (type === 'schnorr') { - benchysPrivateSigningKey = deriveSigningKey(benchysSecret); + benchysPrivateSigningKey = GrumpkinScalar.random(); return wallet.createSchnorrAccount(benchysSecret, salt, benchysPrivateSigningKey); } else if (type === 'ecdsar1') { benchysPrivateSigningKey = randomBytes(32); 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 a082d3dfb8da..8a93f2c44cb8 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 @@ -15,7 +15,6 @@ import { DefaultAccountEntrypoint } from '@aztec/entrypoints/account'; import { randomBytes } from '@aztec/foundation/crypto/random'; import { ChildContract } from '@aztec/noir-test-contracts.js/Child'; import { createPXE, getPXEConfig } from '@aztec/pxe/server'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; import { setup } from './fixtures/utils.js'; @@ -54,7 +53,7 @@ const itShouldBehaveLikeAnAccountContract = ( beforeAll(async () => { const secret = Fr.random(); const salt = Fr.random(); - const signingKey = deriveSigningKey(secret); + const signingKey = GrumpkinScalar.random(); const contract = getAccountContract(signingKey); const address = await getAccountContractAddress(contract, secret, salt); const accountData = { 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 6530060f0e55..831ce820e034 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,7 +1,7 @@ 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 { Fr, GrumpkinScalar } from '@aztec/aztec.js/fields'; import type { AztecNode } from '@aztec/aztec.js/node'; import type { CheatCodes } from '@aztec/aztec/testing'; import { MINIMUM_UPDATE_DELAY, UPDATED_CLASS_IDS_SLOT } from '@aztec/constants'; @@ -17,7 +17,6 @@ import { } from '@aztec/stdlib/delayed-public-mutable'; import { computePublicDataTreeLeafSlot, deriveStorageSlotInMap } from '@aztec/stdlib/hash'; import type { AztecNodeDebug } from '@aztec/stdlib/interfaces/client'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; import { PublicDataTreeLeaf } from '@aztec/stdlib/trees'; import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; @@ -84,7 +83,7 @@ describe('e2e_contract_updates', () => { beforeEach(async () => { const senderPrivateKey = Fr.random(); - const signingKey = deriveSigningKey(senderPrivateKey); + const signingKey = GrumpkinScalar.random(); const salt = Fr.ONE; // 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. diff --git a/yarn-project/end-to-end/src/spartan/mempool_limit.test.ts b/yarn-project/end-to-end/src/spartan/mempool_limit.test.ts index 92dcbba12e20..9622d66312a3 100644 --- a/yarn-project/end-to-end/src/spartan/mempool_limit.test.ts +++ b/yarn-project/end-to-end/src/spartan/mempool_limit.test.ts @@ -13,7 +13,6 @@ // createAztecNodeAdminClient, // createAztecNodeClient, // } from '@aztec/stdlib/interfaces/client'; -// import { deriveSigningKey } from '@aztec/stdlib/keys'; // import { makeTracedFetch } from '@aztec/telemetry-client'; import { AztecAddress } from '@aztec/aztec.js/addresses'; import { SponsoredFeePaymentMethod } from '@aztec/aztec.js/fee'; diff --git a/yarn-project/stdlib/src/keys/derivation.ts b/yarn-project/stdlib/src/keys/derivation.ts index 88cdb05cf270..3dd2af8cc0b2 100644 --- a/yarn-project/stdlib/src/keys/derivation.ts +++ b/yarn-project/stdlib/src/keys/derivation.ts @@ -47,11 +47,6 @@ export function deriveMasterFallbackSecretKey(secretKey: Fr): GrumpkinScalar { return sha512ToGrumpkinScalar([secretKey, DomainSeparator.FBSK_M]); } -export function deriveSigningKey(secretKey: Fr): GrumpkinScalar { - // TODO(#5837): come up with a standard signing key derivation scheme instead of using ivsk_m as signing keys here - return sha512ToGrumpkinScalar([secretKey, DomainSeparator.IVSK_M]); -} - export function computePreaddress(publicKeysHash: Fr, partialAddress: Fr) { return poseidon2HashWithSeparator([publicKeysHash, partialAddress], DomainSeparator.CONTRACT_ADDRESS_V2); } From 6f453755b85c521626f91fb688aab60abaf9247a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 1 Jul 2026 17:40:45 +0000 Subject: [PATCH 06/11] refactor(cli): remove the now-unused --secret-key option cli-wallet switched to --signing-key, leaving createSecretKeyOption and parseSecretKey without any callers, so drop them. Also removes a now-redundant comment in the bot factory. --- yarn-project/bot/src/factory.ts | 3 --- yarn-project/cli/src/utils/commands.ts | 24 ------------------------ 2 files changed, 27 deletions(-) diff --git a/yarn-project/bot/src/factory.ts b/yarn-project/bot/src/factory.ts index 53eb611c7ae1..a4785ac02106 100644 --- a/yarn-project/bot/src/factory.ts +++ b/yarn-project/bot/src/factory.ts @@ -225,9 +225,6 @@ export class BotFactory { private async setupAccountWithPrivateKey(privateKey: Fr) { const salt = this.config.senderSalt ?? Fr.ONE; - // The configured private key is the account's signing key. Reinterpret its bytes as a Grumpkin scalar (always - // valid, since the BN254 scalar field is smaller than Grumpkin's) and derive the privacy secret from it, keeping - // the signing key independent of anything PXE holds. const signingKey = GrumpkinScalar.fromBuffer(privateKey.toBuffer()); const secret = await deriveSecretKeyFromSigningKey(signingKey); const accountManager = await this.wallet.createSchnorrInitializerlessAccount(secret, salt, signingKey); diff --git a/yarn-project/cli/src/utils/commands.ts b/yarn-project/cli/src/utils/commands.ts index 35c2c0fde4b3..9677e2995cff 100644 --- a/yarn-project/cli/src/utils/commands.ts +++ b/yarn-project/cli/src/utils/commands.ts @@ -51,16 +51,6 @@ export const l1ChainIdOption = new Option('-c, --l1-chain-id ', 'Chain I return parsedValue; }); -export const createSecretKeyOption = ( - description: string, - mandatory: boolean, - argsParser?: (value: string, previous: Fr) => Fr, -) => - new Option('-sk, --secret-key ', description) - .env('SECRET_KEY') - .argParser(argsParser ?? parseSecretKey) - .makeOptionMandatory(mandatory); - export const createSigningKeyOption = ( description: string, mandatory: boolean, @@ -367,20 +357,6 @@ export function parsePartialAddress(address: string): Fr { } } -/** - * Parses a secret key from a string. - * @param privateKey - A string - * @returns A secret key - * @throws InvalidArgumentError if the input string is not valid. - */ -export function parseSecretKey(secretKey: string): Fr { - try { - return Fr.fromHexString(secretKey); - } catch { - throw new InvalidArgumentError(`Invalid encryption secret key: ${secretKey}`); - } -} - /** * Parses an account signing key from a string. * @param signingKey - A string From 47f454e08101866138d7c21192a49978e78c7e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 1 Jul 2026 17:40:45 +0000 Subject: [PATCH 07/11] docs: update account and CLI docs for the signing-key changes Adds migration notes for the signing-key decoupling, the aztec-wallet --secret-key -> --signing-key rename, and the bot senderPrivateKey change. Corrects the create-account guide and the wallet-extension tutorial (which taught the removed deriveSigningKey), and updates the type-checked examples to pass an explicit signing key. --- .../docs/aztec-js/how_to_create_account.md | 8 ++-- .../docs/resources/migration_notes.md | 42 +++++++++++++++++++ .../wallet-extension/04-accounts.md | 14 +++---- docs/examples/ts/aztecjs_connection/index.ts | 7 +++- .../ts/aztecjs_getting_started/index.ts | 12 +++++- docs/examples/ts/bob_token_contract/index.ts | 3 ++ .../ts/recursive_verification/index.ts | 8 +++- docs/examples/ts/token_bridge/index.ts | 1 + .../scripts/deploy-and-interact.ts | 18 +++++--- 9 files changed, 89 insertions(+), 24 deletions(-) diff --git a/docs/docs-developers/docs/aztec-js/how_to_create_account.md b/docs/docs-developers/docs/aztec-js/how_to_create_account.md index 5f9fcfe0a43e..35aedc81106c 100644 --- a/docs/docs-developers/docs/aztec-js/how_to_create_account.md +++ b/docs/docs-developers/docs/aztec-js/how_to_create_account.md @@ -20,14 +20,14 @@ yarn add @aztec/aztec.js@#include_version_without_prefix @aztec/wallets@#include ## Create a new account -Using the [`wallet` from the connection guide](./how_to_connect_to_local_network.md), call `createSchnorrAccount` to create a new account with a random secret and salt: +Using the [`wallet` from the connection guide](./how_to_connect_to_local_network.md), call `createSchnorrAccount` to create a new account with a random secret, salt, and signing key: #include_code create_account /docs/examples/ts/aztecjs_connection/index.ts typescript -The secret is used to derive the account's encryption keys, and the salt ensures address uniqueness. The signing key is automatically derived from the secret. +The secret derives the account's encryption keys, the signing key authenticates its transactions, and the salt ensures address uniqueness. The signing key is provided independently and is not derived from the secret: it is an ownership key, so keep it separate from the encryption secret that your PXE holds. -:::warning Store your secret and salt -Save the `secret` and `salt` values securely. You need both to recover access to your account. If you lose them, you will permanently lose access to the account and any assets it holds. +:::warning Store your secret, salt, and signing key +Save the `secret`, `salt`, and `signingKey` values securely. You need all three to recover access to your account. If you lose them, you will permanently lose access to the account and any assets it holds. ::: ## Deploy the account diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index 73ba5a8d1f76..6d74e1be9ef8 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,48 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### [Aztec.js] Account signing keys are no longer derived from the privacy secret + +Schnorr account signing keys used to be derived from the account's privacy secret (via the now-removed `deriveSigningKey`), which meant the ownership key could be reconstructed from a value the PXE holds. The relationship is now reversed: the signing key is the root, and the privacy secret is derived from it with `deriveSecretKeyFromSigningKey` (exported from `@aztec/accounts/utils`). + +As a result: + +- `deriveSigningKey` is removed. +- `getSchnorrAccountContractAddress` and `getSchnorrInitializerlessAccountContractAddress` now take the signing key first and an optional secret: `(signingPrivateKey, salt, secretKey?)`. When `secretKey` is omitted it is derived from the signing key. +- The embedded wallet's `createSchnorrAccount` and `createSchnorrInitializerlessAccount` now require an explicit signing key argument. + +**Migration:** + +```diff +- import { deriveSigningKey } from '@aztec/stdlib/keys'; +- const signingKey = deriveSigningKey(secret); +- const address = await getSchnorrAccountContractAddress(secret, salt, signingKey); ++ import { GrumpkinScalar } from '@aztec/aztec.js/fields'; ++ const signingKey = GrumpkinScalar.random(); // supply your own signing key ++ const address = await getSchnorrAccountContractAddress(signingKey, salt, secret); +``` + +**Impact**: Account addresses change, since both the signing key and the privacy secret feed the address. Code that derived the signing key from the secret, or passed the secret first to the address helpers, no longer compiles. + +### [CLI] `aztec-wallet` `--secret-key` is renamed to `--signing-key` + +`aztec-wallet` accounts are now rooted on their signing key rather than on a privacy secret. The `--secret-key` option (on `create-account` and `simulate`) is renamed to `--signing-key`, and the `SECRET_KEY` environment variable to `SIGNING_KEY`. When creating an account, a random signing key is generated by default and the privacy secret is derived from it. + +**Migration:** + +```diff +- aztec-wallet create-account --secret-key 0x... ++ aztec-wallet create-account --signing-key 0x... +``` + +**Impact**: The value you save and restore for an account is now its signing key, and account addresses change, so existing wallet databases and funded addresses are not carried over. + +### [Bot] `senderPrivateKey` is now the account signing key + +The transaction bot previously derived its account's signing key from the configured `senderPrivateKey`. That value is now used directly as the signing key, with the privacy secret derived from it. + +**Impact**: The bot's account address changes for a given `senderPrivateKey`, so the account must be re-funded at its new address. + ### [PXE] Browser KV-store default is now SQLite-OPFS; the IndexedDB entrypoint moved and will be deprecated The browser PXE data store and the embedded wallet (`@aztec/wallets`) now persist to SQLite-OPFS instead of IndexedDB by default. The recommended way to obtain the browser backend is `@aztec/kv-store/sqlite-opfs`. diff --git a/docs/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md b/docs/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md index 6589e3b8999d..3d412393e454 100644 --- a/docs/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md +++ b/docs/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md @@ -24,23 +24,21 @@ An Aztec account has: ## Key Derivation -The wallet uses the standard derivation from `@aztec/stdlib/keys`: +The wallet generates a random secret for the account's privacy keys and a separate, independent signing key for transaction authorization. The signing key is an ownership key, so it is kept independent of the privacy secret rather than derived from it (deriving it from the secret would let anything holding the secret, such as the PXE, reconstruct the ownership key): ```typescript -import { deriveSigningKey } from '@aztec/stdlib/keys'; +import { Fr, GrumpkinScalar } from '@aztec/aztec.js/fields'; -// Generate a random secret +// Generate a random privacy secret and an independent signing key const secret = Fr.random(); - -// Derive the signing key -const signingKey = deriveSigningKey(secret); +const signingKey = GrumpkinScalar.random(); ``` -The derivation uses SHA-512 with domain separators to derive different keys: +The nullifier, viewing, and tagging keys are derived from the secret with SHA-512 and domain separators, while the signing key is supplied to the account contract directly: ```typescript // From stdlib/src/keys/derivation.ts -export function deriveSigningKey(secretKey: Fr): GrumpkinScalar { +export function deriveMasterIncomingViewingSecretKey(secretKey: Fr): GrumpkinScalar { return sha512ToGrumpkinScalar([secretKey, GeneratorIndex.IVSK_M]); } diff --git a/docs/examples/ts/aztecjs_connection/index.ts b/docs/examples/ts/aztecjs_connection/index.ts index 46f37945f601..059b19a24e2f 100644 --- a/docs/examples/ts/aztecjs_connection/index.ts +++ b/docs/examples/ts/aztecjs_connection/index.ts @@ -45,11 +45,12 @@ console.log(`Alice's fee juice balance: ${aliceBalance}`); // docs:end:check_fee_juice // docs:start:create_account -import { Fr } from "@aztec/aztec.js/fields"; +import { Fr, GrumpkinScalar } from "@aztec/aztec.js/fields"; const secret = Fr.random(); const salt = Fr.random(); -const newAccount = await wallet.createSchnorrAccount(secret, salt); +const signingKey = GrumpkinScalar.random(); +const newAccount = await wallet.createSchnorrAccount(secret, salt, signingKey); console.log("New account address:", newAccount.address.toString()); // docs:end:create_account @@ -87,9 +88,11 @@ await deployMethod.send({ // can coexist in one example; in your own code, pick whichever name fits. const feeJuiceSecret = Fr.random(); const feeJuiceSalt = Fr.random(); +const feeJuiceSigningKey = GrumpkinScalar.random(); const feeJuiceAccount = await wallet.createSchnorrAccount( feeJuiceSecret, feeJuiceSalt, + feeJuiceSigningKey, ); // docs:end:create_fee_juice_account diff --git a/docs/examples/ts/aztecjs_getting_started/index.ts b/docs/examples/ts/aztecjs_getting_started/index.ts index db238d59d1e3..3b2d9a836728 100644 --- a/docs/examples/ts/aztecjs_getting_started/index.ts +++ b/docs/examples/ts/aztecjs_getting_started/index.ts @@ -6,8 +6,16 @@ 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.createSchnorrInitializerlessAccount(alice.secret, alice.salt); -await wallet.createSchnorrInitializerlessAccount(bob.secret, bob.salt); +await wallet.createSchnorrInitializerlessAccount( + alice.secret, + alice.salt, + alice.signingKey, +); +await wallet.createSchnorrInitializerlessAccount( + bob.secret, + bob.salt, + bob.signingKey, +); // docs:end:setup // docs:start:deploy diff --git a/docs/examples/ts/bob_token_contract/index.ts b/docs/examples/ts/bob_token_contract/index.ts index 7f929e090d71..ac4234e9dc1a 100644 --- a/docs/examples/ts/bob_token_contract/index.ts +++ b/docs/examples/ts/bob_token_contract/index.ts @@ -61,15 +61,18 @@ async function main() { const giggleAccountManager = await wallet.createSchnorrInitializerlessAccount( giggleWalletData.secret, giggleWalletData.salt, + giggleWalletData.signingKey, ); const aliceAccountManager = await wallet.createSchnorrInitializerlessAccount( aliceWalletData.secret, aliceWalletData.salt, + aliceWalletData.signingKey, ); const bobClinicAccountManager = await wallet.createSchnorrInitializerlessAccount( bobClinicWalletData.secret, bobClinicWalletData.salt, + bobClinicWalletData.signingKey, ); const giggleAddress = giggleAccountManager.address; diff --git a/docs/examples/ts/recursive_verification/index.ts b/docs/examples/ts/recursive_verification/index.ts index b8ce5d30bb36..80170f392567 100644 --- a/docs/examples/ts/recursive_verification/index.ts +++ b/docs/examples/ts/recursive_verification/index.ts @@ -6,7 +6,7 @@ import { SponsoredFPCContract } from "@aztec/noir-contracts.js/SponsoredFPC"; import { ValueNotEqualContract } from "./artifacts/ValueNotEqual.js"; import { EmbeddedWallet } from "@aztec/wallets/embedded"; import { NO_FROM } from "@aztec/aztec.js/account"; -import { Fr } from "@aztec/aztec.js/fields"; +import { Fr, GrumpkinScalar } from "@aztec/aztec.js/fields"; import assert from "node:assert"; import fs from "node:fs"; @@ -47,7 +47,11 @@ async function main() { // Step 1: Setup wallet and create account // Accounts in Aztec are smart contracts (account abstraction) const wallet = await setupWallet(); - const manager = await wallet.createSchnorrAccount(Fr.random(), Fr.random()); + const manager = await wallet.createSchnorrAccount( + Fr.random(), + Fr.random(), + GrumpkinScalar.random(), + ); // Deploy the account contract const deployMethod = await manager.getDeployMethod(); diff --git a/docs/examples/ts/token_bridge/index.ts b/docs/examples/ts/token_bridge/index.ts index 993a39498569..18f3508ac046 100644 --- a/docs/examples/ts/token_bridge/index.ts +++ b/docs/examples/ts/token_bridge/index.ts @@ -31,6 +31,7 @@ const [accData] = await getInitialTestAccountsData(); const account = await aztecWallet.createSchnorrInitializerlessAccount( accData.secret, accData.salt, + accData.signingKey, ); console.log(`Account: ${account.address.toString()}\n`); diff --git a/docs/examples/webapp-tutorial/scripts/deploy-and-interact.ts b/docs/examples/webapp-tutorial/scripts/deploy-and-interact.ts index cd3aac7eaa2e..cef9c56dd8b2 100644 --- a/docs/examples/webapp-tutorial/scripts/deploy-and-interact.ts +++ b/docs/examples/webapp-tutorial/scripts/deploy-and-interact.ts @@ -8,15 +8,21 @@ 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); -console.log("Accounts ready:", alice.address.toString(), bob.address.toString()); +await wallet.createSchnorrAccount(alice.secret, alice.salt, alice.signingKey); +await wallet.createSchnorrAccount(bob.secret, bob.salt, bob.signingKey); +console.log( + "Accounts ready:", + alice.address.toString(), + bob.address.toString(), +); // docs:end:script-setup // docs:start:script-deploy -const { contract } = await PodRacingContract.deploy(wallet, alice.address).send({ - from: alice.address, -}); +const { contract } = await PodRacingContract.deploy(wallet, alice.address).send( + { + from: alice.address, + }, +); console.log("Contract deployed at:", contract.address.toString()); // docs:end:script-deploy From 14c77ce7bb847961917bbb6aa0b49df92fda79b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 1 Jul 2026 17:56:12 +0000 Subject: [PATCH 08/11] docs: update wallet-extension example to drop deriveSigningKey --- .../wallet-extension/04-accounts.md | 11 +-- .../test-extension/src/account-utils.ts | 9 +-- .../test-extension/src/aztec-imports.ts | 71 ++++++++++--------- .../test-extension/src/wallet/wallet-impl.ts | 46 ++++++------ 4 files changed, 74 insertions(+), 63 deletions(-) diff --git a/docs/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md b/docs/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md index 3d412393e454..dbc6d8d08407 100644 --- a/docs/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md +++ b/docs/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md @@ -24,17 +24,18 @@ An Aztec account has: ## Key Derivation -The wallet generates a random secret for the account's privacy keys and a separate, independent signing key for transaction authorization. The signing key is an ownership key, so it is kept independent of the privacy secret rather than derived from it (deriving it from the secret would let anything holding the secret, such as the PXE, reconstruct the ownership key): +The wallet generates a random signing key as the account's root and derives the privacy secret from it: ```typescript -import { Fr, GrumpkinScalar } from '@aztec/aztec.js/fields'; +import { GrumpkinScalar } from '@aztec/aztec.js/fields'; +import { deriveSecretKeyFromSigningKey } from '@aztec/accounts/utils'; -// Generate a random privacy secret and an independent signing key -const secret = Fr.random(); +// The signing key is the account's root; the privacy secret is derived from it const signingKey = GrumpkinScalar.random(); +const secret = await deriveSecretKeyFromSigningKey(signingKey); ``` -The nullifier, viewing, and tagging keys are derived from the secret with SHA-512 and domain separators, while the signing key is supplied to the account contract directly: +The nullifier, viewing, and tagging keys are then derived from that secret with SHA-512 and domain separators, while the signing key is supplied to the account contract directly: ```typescript // From stdlib/src/keys/derivation.ts diff --git a/docs/examples/webapp-tutorial/test-extension/src/account-utils.ts b/docs/examples/webapp-tutorial/test-extension/src/account-utils.ts index 5aaf733f8bb6..489dbe482a79 100644 --- a/docs/examples/webapp-tutorial/test-extension/src/account-utils.ts +++ b/docs/examples/webapp-tutorial/test-extension/src/account-utils.ts @@ -5,7 +5,7 @@ * account creation, deployment, and registration. */ -import { getAztecCore } from './aztec-imports'; +import { getAztecCore } from "./aztec-imports"; /** * Derives keys and instantiates a Schnorr account contract from a secret and salt. @@ -21,17 +21,18 @@ import { getAztecCore } from './aztec-imports'; export async function instantiateAccount(secret: string, salt: string) { const { Fr, + GrumpkinScalar, deriveKeys, - deriveSigningKey, + deriveSecretKeyFromSigningKey, SchnorrAccountContract, getContractInstanceFromInstantiationParams, } = await getAztecCore(); - const secretFr = Fr.fromString(secret); + const signingKey = GrumpkinScalar.fromString(secret); const saltFr = Fr.fromString(salt); + const secretFr = await deriveSecretKeyFromSigningKey(signingKey); const { publicKeys } = await deriveKeys(secretFr); - const signingKey = deriveSigningKey(secretFr); const accountContract = new SchnorrAccountContract(signingKey); const initInfo = await accountContract.getInitializationFunctionAndArgs(); diff --git a/docs/examples/webapp-tutorial/test-extension/src/aztec-imports.ts b/docs/examples/webapp-tutorial/test-extension/src/aztec-imports.ts index 99c8795c1029..510b5c25919f 100644 --- a/docs/examples/webapp-tutorial/test-extension/src/aztec-imports.ts +++ b/docs/examples/webapp-tutorial/test-extension/src/aztec-imports.ts @@ -14,29 +14,30 @@ /** Core imports needed for account operations (key derivation, contract setup) */ export interface AztecCoreImports { - Fr: typeof import('@aztec/aztec.js/fields').Fr; - AztecAddress: typeof import('@aztec/aztec.js/addresses').AztecAddress; - deriveKeys: typeof import('@aztec/stdlib/keys').deriveKeys; - deriveSigningKey: typeof import('@aztec/stdlib/keys').deriveSigningKey; - SchnorrAccountContract: typeof import('@aztec/accounts/schnorr/lazy').SchnorrAccountContract; - getContractInstanceFromInstantiationParams: typeof import('@aztec/aztec.js/contracts').getContractInstanceFromInstantiationParams; - AccountManager: typeof import('@aztec/aztec.js/wallet').AccountManager; + Fr: typeof import("@aztec/aztec.js/fields").Fr; + GrumpkinScalar: typeof import("@aztec/aztec.js/fields").GrumpkinScalar; + AztecAddress: typeof import("@aztec/aztec.js/addresses").AztecAddress; + deriveKeys: typeof import("@aztec/stdlib/keys").deriveKeys; + deriveSecretKeyFromSigningKey: typeof import("@aztec/accounts/utils").deriveSecretKeyFromSigningKey; + SchnorrAccountContract: typeof import("@aztec/accounts/schnorr/lazy").SchnorrAccountContract; + getContractInstanceFromInstantiationParams: typeof import("@aztec/aztec.js/contracts").getContractInstanceFromInstantiationParams; + AccountManager: typeof import("@aztec/aztec.js/wallet").AccountManager; } /** Additional imports for the wallet runtime (BaseWallet, serialization) */ export interface AztecWalletImports extends AztecCoreImports { - BaseWallet: typeof import('@aztec/wallet-sdk/base-wallet').BaseWallet; - SignerlessAccount: typeof import('@aztec/aztec.js/account').SignerlessAccount; - WalletSchema: typeof import('@aztec/aztec.js/wallet').WalletSchema; - jsonStringify: typeof import('@aztec/foundation/json-rpc').jsonStringify; - schemaHasMethod: typeof import('@aztec/foundation/schemas').schemaHasMethod; + BaseWallet: typeof import("@aztec/wallet-sdk/base-wallet").BaseWallet; + SignerlessAccount: typeof import("@aztec/aztec.js/account").SignerlessAccount; + WalletSchema: typeof import("@aztec/aztec.js/wallet").WalletSchema; + jsonStringify: typeof import("@aztec/foundation/json-rpc").jsonStringify; + schemaHasMethod: typeof import("@aztec/foundation/schemas").schemaHasMethod; } /** Deploy-specific imports (fee payment, SponsoredFPC) */ export interface AztecDeployImports extends AztecCoreImports { - SponsoredFeePaymentMethod: typeof import('@aztec/aztec.js/fee').SponsoredFeePaymentMethod; - SponsoredFPCContract: typeof import('@aztec/noir-contracts.js/SponsoredFPC').SponsoredFPCContract; - SPONSORED_FPC_SALT: typeof import('@aztec/constants').SPONSORED_FPC_SALT; + SponsoredFeePaymentMethod: typeof import("@aztec/aztec.js/fee").SponsoredFeePaymentMethod; + SponsoredFPCContract: typeof import("@aztec/noir-contracts.js/SponsoredFPC").SponsoredFPCContract; + SPONSORED_FPC_SALT: typeof import("@aztec/constants").SPONSORED_FPC_SALT; } let coreCache: AztecCoreImports | null = null; @@ -50,22 +51,26 @@ let deployCache: AztecDeployImports | null = null; export async function getAztecCore(): Promise { if (coreCache) return coreCache; - const [fields, addresses, keys, schnorr, contracts, wallet] = await Promise.all([ - import('@aztec/aztec.js/fields'), - import('@aztec/aztec.js/addresses'), - import('@aztec/stdlib/keys'), - import('@aztec/accounts/schnorr/lazy'), - import('@aztec/aztec.js/contracts'), - import('@aztec/aztec.js/wallet'), - ]); + const [fields, addresses, keys, accountUtils, schnorr, contracts, wallet] = + await Promise.all([ + import("@aztec/aztec.js/fields"), + import("@aztec/aztec.js/addresses"), + import("@aztec/stdlib/keys"), + import("@aztec/accounts/utils"), + import("@aztec/accounts/schnorr/lazy"), + import("@aztec/aztec.js/contracts"), + import("@aztec/aztec.js/wallet"), + ]); coreCache = { Fr: fields.Fr, + GrumpkinScalar: fields.GrumpkinScalar, AztecAddress: addresses.AztecAddress, deriveKeys: keys.deriveKeys, - deriveSigningKey: keys.deriveSigningKey, + deriveSecretKeyFromSigningKey: accountUtils.deriveSecretKeyFromSigningKey, SchnorrAccountContract: schnorr.SchnorrAccountContract, - getContractInstanceFromInstantiationParams: contracts.getContractInstanceFromInstantiationParams, + getContractInstanceFromInstantiationParams: + contracts.getContractInstanceFromInstantiationParams, AccountManager: wallet.AccountManager, }; @@ -81,11 +86,11 @@ export async function getAztecWallet(): Promise { const [core, bw, account, walletMod, jsonRpc, schemas] = await Promise.all([ getAztecCore(), - import('@aztec/wallet-sdk/base-wallet'), - import('@aztec/aztec.js/account'), - import('@aztec/aztec.js/wallet'), - import('@aztec/foundation/json-rpc'), - import('@aztec/foundation/schemas'), + import("@aztec/wallet-sdk/base-wallet"), + import("@aztec/aztec.js/account"), + import("@aztec/aztec.js/wallet"), + import("@aztec/foundation/json-rpc"), + import("@aztec/foundation/schemas"), ]); walletCache = { @@ -109,9 +114,9 @@ export async function getAztecDeploy(): Promise { const [core, fee, sponsoredFpc, constants] = await Promise.all([ getAztecCore(), - import('@aztec/aztec.js/fee'), - import('@aztec/noir-contracts.js/SponsoredFPC'), - import('@aztec/constants'), + import("@aztec/aztec.js/fee"), + import("@aztec/noir-contracts.js/SponsoredFPC"), + import("@aztec/constants"), ]); deployCache = { diff --git a/docs/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts b/docs/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts index 2d1bd3637d16..0f288e5b80ff 100644 --- a/docs/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts +++ b/docs/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts @@ -4,17 +4,17 @@ * With the crossOriginIsolated monkey-patch, Barretenberg WASM works in Chrome * extension offscreen documents. This allows full cryptographic operations: * - Fr.random() for secure random field elements - * - deriveKeys() and deriveSigningKey() for key derivation + * - deriveKeys() and deriveSecretKeyFromSigningKey() for key derivation * - Address computation with SchnorrAccountContract * * All encryption uses a non-extractable CryptoKey — the raw password * is never stored or passed around after initial derivation. (#2) */ -import { Fr } from '@aztec/aztec.js/fields'; +import { Fr, GrumpkinScalar } from "@aztec/aztec.js/fields"; -import { instantiateAccount } from '../account-utils'; -import type { PublicAccountInfo } from '../shared-types'; +import { instantiateAccount } from "../account-utils"; +import type { PublicAccountInfo } from "../shared-types"; import { type StoredAccount, getStoredAccounts, @@ -25,16 +25,17 @@ import { markAccountDeployed, getActiveAccount, setActiveAccount, -} from './storage'; -import { log } from '../config'; +} from "./storage"; +import { log } from "../config"; /** * Generates a new secret and salt for account creation using real Aztec primitives. * Uses Fr.random() which is cryptographically secure via Barretenberg. */ export function generateSecret(): { secret: string; salt: string } { - log.debug('[wallet-manager] Generating new secret and salt with Fr.random()...'); - const secret = Fr.random().toString(); + log.debug("[wallet-manager] Generating new signing key and salt..."); + // The account's root is its signing key; the privacy secret is derived from it in instantiateAccount. + const secret = GrumpkinScalar.random().toString(); const salt = Fr.random().toString(); return { secret, salt }; } @@ -43,10 +44,13 @@ export function generateSecret(): { secret: string; salt: string } { * Computes the account address from a secret and salt. * Runs in the extension offscreen document where Barretenberg works. */ -export async function computeAddress(secretHex: string, saltHex: string): Promise { - log.debug('[wallet-manager] Computing address...'); +export async function computeAddress( + secretHex: string, + saltHex: string, +): Promise { + log.debug("[wallet-manager] Computing address..."); const { instance } = await instantiateAccount(secretHex, saltHex); - log.debug('[wallet-manager] Computed address:', instance.address.toString()); + log.debug("[wallet-manager] Computed address:", instance.address.toString()); return instance.address.toString(); } @@ -57,9 +61,9 @@ export async function computeAddress(secretHex: string, saltHex: string): Promis // docs:start:create-new-account export async function createAccount( masterKey: CryptoKey, - alias: string = '' + alias: string = "", ): Promise<{ address: string; secret: string; salt: string }> { - log.debug('[wallet-manager] Creating account...'); + log.debug("[wallet-manager] Creating account..."); const { secret, salt } = generateSecret(); const address = await computeAddress(secret, salt); @@ -69,10 +73,10 @@ export async function createAccount( const currentActive = await getActiveAccount(); if (!currentActive) { await setActiveAccount(address); - log.debug('[wallet-manager] Set as active account (first account)'); + log.debug("[wallet-manager] Set as active account (first account)"); } - log.debug('[wallet-manager] Account created:', address); + log.debug("[wallet-manager] Account created:", address); return { address, secret, salt }; } // docs:end:create-new-account @@ -86,9 +90,9 @@ export async function storeAccount( secret: string, salt: string, masterKey: CryptoKey, - alias: string = '' + alias: string = "", ): Promise { - log.debug('[wallet-manager] Storing account:', address); + log.debug("[wallet-manager] Storing account:", address); const { encrypted, iv } = await encryptWithKey(secret, masterKey); @@ -102,7 +106,7 @@ export async function storeAccount( }; await saveAccount(storedAccount); - log.debug('[wallet-manager] Account stored successfully'); + log.debug("[wallet-manager] Account stored successfully"); } /** @@ -111,7 +115,7 @@ export async function storeAccount( */ export async function getAccountSecret( address: string, - masterKey: CryptoKey + masterKey: CryptoKey, ): Promise<{ secret: string; salt: string } | null> { const stored = await getStoredAccount(address); if (!stored) { @@ -121,7 +125,7 @@ export async function getAccountSecret( const secret = await decryptWithKey( stored.encryptedSecret, stored.iv, - masterKey + masterKey, ); return { @@ -135,7 +139,7 @@ export async function getAccountSecret( */ export async function getAccounts(): Promise { const accounts = await getStoredAccounts(); - return accounts.map(acc => ({ + return accounts.map((acc) => ({ address: acc.address, alias: acc.alias, isDeployed: acc.isDeployed, From 353b3d52022ae4ede60582b4db3633327159107c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 2 Jul 2026 18:30:32 +0000 Subject: [PATCH 09/11] fix: address review feedback - Rename SECRET_KEY to SIGNING_KEY in docker-compose.yml cli service - Fix docs snippet to use DomainSeparator instead of removed GeneratorIndex - Clear error when retrieving accounts stored by older aztec-wallet versions --- docker-compose.yml | 2 +- .../js_tutorials/wallet-extension/04-accounts.md | 4 ++-- .../cli-wallet/src/cmds/create_account.ts | 2 +- yarn-project/cli-wallet/src/utils/wallet.ts | 6 ++++++ .../end-to-end/src/spartan/mempool_limit.test.ts | 16 ---------------- 5 files changed, 10 insertions(+), 20 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6eb3b0d77981..df0a586f6321 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -72,7 +72,7 @@ services: environment: AZTEC_NODE_URL: http://node:8080 NODE_NO_WARNINGS: 1 - SECRET_KEY: + SIGNING_KEY: ETHEREUM_HOSTS: profiles: - cli diff --git a/docs/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md b/docs/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md index ce3cd4b631fe..5ce2dc37841e 100644 --- a/docs/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md +++ b/docs/docs-developers/docs/tutorials/js_tutorials/wallet-extension/04-accounts.md @@ -40,11 +40,11 @@ The nullifier, viewing, and tagging keys are then derived from that secret with ```typescript // From stdlib/src/keys/derivation.ts export function deriveMasterIncomingViewingSecretKey(secretKey: Fr): GrumpkinScalar { - return sha512ToGrumpkinScalar([secretKey, GeneratorIndex.IVSK_M]); + return sha512ToGrumpkinScalar([secretKey, DomainSeparator.IVSK_M]); } export function deriveMasterNullifierHidingSecretKey(secretKey: Fr): GrumpkinScalar { - return sha512ToGrumpkinScalar([secretKey, GeneratorIndex.NHK_M]); + return sha512ToGrumpkinScalar([secretKey, DomainSeparator.NHK_M]); } ``` diff --git a/yarn-project/cli-wallet/src/cmds/create_account.ts b/yarn-project/cli-wallet/src/cmds/create_account.ts index 9b610fb5b00f..e409cbfb4618 100644 --- a/yarn-project/cli-wallet/src/cmds/create_account.ts +++ b/yarn-project/cli-wallet/src/cmds/create_account.ts @@ -39,7 +39,7 @@ export async function createAccount( ) { let secretKey: Fr; if (accountType === 'ecdsasecp256r1ssh') { - // SSH accounts sign with a key held in the agent, and so we cannot derive their privacy secret key from it. Insted + // SSH accounts sign with a key held in the agent, and so we cannot derive their privacy secret key from it. Instead // we pick a random value. secretKey = Fr.random(); } else { diff --git a/yarn-project/cli-wallet/src/utils/wallet.ts b/yarn-project/cli-wallet/src/utils/wallet.ts index 7d9ffa73824a..51cbebe28545 100644 --- a/yarn-project/cli-wallet/src/utils/wallet.ts +++ b/yarn-project/cli-wallet/src/utils/wallet.ts @@ -186,6 +186,12 @@ export class CLIWallet extends BaseWallet { throw new Error('Cannot retrieve an account without a wallet database'); } const { type, signingKey, secretKey, salt } = await this.db.retrieveAccount(address); + if (type !== 'ecdsasecp256r1ssh' && !signingKey) { + throw new Error( + `Account ${address} has no stored signing key: it was created with an older version of aztec-wallet and is ` + + `incompatible with this one. Create a new account to continue.`, + ); + } const publicSigningKey = type === 'ecdsasecp256r1ssh' ? await this.db.retrieveAccountMetadata(address, 'publicSigningKey') : undefined; return this.buildAccount(type, salt, signingKey, secretKey, publicSigningKey); diff --git a/yarn-project/end-to-end/src/spartan/mempool_limit.test.ts b/yarn-project/end-to-end/src/spartan/mempool_limit.test.ts index 9622d66312a3..baba481c623a 100644 --- a/yarn-project/end-to-end/src/spartan/mempool_limit.test.ts +++ b/yarn-project/end-to-end/src/spartan/mempool_limit.test.ts @@ -1,19 +1,3 @@ -// import { getSchnorrAccount } from '@aztec/accounts/schnorr'; -// import { AztecAddress } from '@aztec/aztec.js/addresses'; -// import type { InteractionFeeOptions } from '@aztec/entrypoints/interfaces'; -// import { asyncPool } from '@aztec/foundation/async-pool'; -// import { times } from '@aztec/foundation/collection'; -// import { Agent, makeUndiciFetch } from '@aztec/foundation/json-rpc/undici'; -// import { createLogger } from '@aztec/foundation/log'; -// import { TokenContract } from '@aztec/noir-contracts.js/Token'; -// import { createPXE } from '@aztec/pxe/server'; -// import { -// type AztecNode, -// type AztecNodeAdmin, -// createAztecNodeAdminClient, -// createAztecNodeClient, -// } from '@aztec/stdlib/interfaces/client'; -// import { makeTracedFetch } from '@aztec/telemetry-client'; import { AztecAddress } from '@aztec/aztec.js/addresses'; import { SponsoredFeePaymentMethod } from '@aztec/aztec.js/fee'; import { Fr } from '@aztec/aztec.js/fields'; From 3de013dd6440f6d4e738ef3f966bc88049798d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 2 Jul 2026 20:30:30 +0000 Subject: [PATCH 10/11] fix imports --- .../src/components/navbar/components/WalletHub.tsx | 4 +--- .../src/wallet/components/CreateAccountDialog.tsx | 14 +++++++------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/playground/src/components/navbar/components/WalletHub.tsx b/playground/src/components/navbar/components/WalletHub.tsx index b214559e132e..d7d3dbaa9fa9 100644 --- a/playground/src/components/navbar/components/WalletHub.tsx +++ b/playground/src/components/navbar/components/WalletHub.tsx @@ -22,7 +22,6 @@ import { useContext, useEffect, useState, useRef } from 'react'; import { EmbeddedWallet } from '@aztec/wallets/embedded'; import { WebLogger } from '../../../utils/web_logger'; import { getInitialTestAccountsData } from '@aztec/accounts/testing/lazy'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; import { AztecAddress } from '@aztec/aztec.js/addresses'; import { type DeployOptions, DeployMethod } from '@aztec/aztec.js/contracts'; import { AztecContext } from '../../../aztecContext'; @@ -93,8 +92,7 @@ async function discoverTestAccounts(wallet: EmbeddedWallet) { for (let i = 0; i < testAccountData.length; i++) { const accountData = testAccountData[i]; - const sk = deriveSigningKey(accountData.secret); - await wallet.createSchnorrAccount(accountData.secret, accountData.salt, sk, `test${i}`); + await wallet.createSchnorrAccount(accountData.secret, accountData.salt, accountData.signingKey, `test${i}`); } } diff --git a/playground/src/wallet/components/CreateAccountDialog.tsx b/playground/src/wallet/components/CreateAccountDialog.tsx index 021db24b59c6..b4eca600a3bf 100644 --- a/playground/src/wallet/components/CreateAccountDialog.tsx +++ b/playground/src/wallet/components/CreateAccountDialog.tsx @@ -1,13 +1,13 @@ import { AztecAddress } from '@aztec/aztec.js/addresses'; import { type DeployOptions, DeployMethod } from '@aztec/aztec.js/contracts'; -import { Fr } from '@aztec/aztec.js/fields'; +import { Fr, GrumpkinScalar } from '@aztec/aztec.js/fields'; import type { DeployAccountOptions } from '@aztec/aztec.js/wallet'; import Button from '@mui/material/Button'; import Dialog from '@mui/material/Dialog'; import DialogTitle from '@mui/material/DialogTitle'; import TextField from '@mui/material/TextField'; import { useState } from 'react'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; +import { deriveSecretKeyFromSigningKey } from '@aztec/accounts/utils'; import FormControl from '@mui/material/FormControl'; import Select from '@mui/material/Select'; import MenuItem from '@mui/material/MenuItem'; @@ -49,20 +49,20 @@ export function CreateAccountDialog({ try { const salt = Fr.random(); let accountManager; - let signingKey; switch (type) { case 'schnorr': { - signingKey = deriveSigningKey(secretKey); - accountManager = await wallet.createAndStoreAccount(alias, 'schnorr', secretKey, salt, signingKey); + const signingKey = GrumpkinScalar.random(); + const secret = await deriveSecretKeyFromSigningKey(signingKey); + accountManager = await wallet.createAndStoreAccount(alias, 'schnorr', secret, salt, signingKey.toBuffer()); break; } case 'ecdsasecp256r1': { - signingKey = randomBytes(32); + const signingKey = randomBytes(32); accountManager = await wallet.createAndStoreAccount(alias, 'ecdsasecp256r1', secretKey, salt, signingKey); break; } case 'ecdsasecp256k1': { - signingKey = randomBytes(32); + const signingKey = randomBytes(32); accountManager = await wallet.createAndStoreAccount(alias, 'ecdsasecp256k1', secretKey, salt, signingKey); break; } From b36a9aa304eed1415f542aec4047f17156d60d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 2 Jul 2026 21:54:55 +0000 Subject: [PATCH 11/11] fix import --- yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts index 004232d4bbf1..543a21e5f54b 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts @@ -358,7 +358,7 @@ export abstract class BaseWallet implements Wallet { if (artifact) { await this.pxe.registerContractClass(artifact); } - await this.pxe.registerContract(instance); + const instanceAddress = await this.pxe.registerContract(instance); if (secretKeyOrKeys) { // PXE never receives the account seed (from which the message-signing/fallback secret keys could be re-derived): @@ -372,9 +372,9 @@ export abstract class BaseWallet implements Wallet { ? await deriveKeys(secretKeyOrKeys) : await deriveKeysFromMasterSecretKeys(secretKeyOrKeys); const { address } = await this.pxe.registerAccount(derivedKeys, await computePartialAddress(instance)); - if (!address.equals(instance.address)) { + if (!address.equals(instanceAddress)) { throw new Error( - `Registered account address ${address.toString()} does not match contract instance address ${instance.address.toString()}: the provided keys do not correspond to this account.`, + `Registered account address ${address.toString()} does not match contract instance address ${instanceAddress.toString()}: the provided keys do not correspond to this account.`, ); } }