From c186543bf0f8dc715d932405da825cce2e81b3a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Tue, 14 May 2024 16:30:39 +0200 Subject: [PATCH] feat: demonstrating use of nsk_app to check nullification (#6362) --- yarn-project/accounts/src/single_key/index.ts | 5 +- .../accounts/src/testing/configuration.ts | 9 ++- .../accounts/src/testing/create_account.ts | 26 ++++--- .../end-to-end/src/e2e_key_registry.test.ts | 67 ++++++++++++++++++- yarn-project/end-to-end/src/shared/browser.ts | 5 +- .../pxe/src/pxe_service/pxe_service.ts | 4 -- 6 files changed, 89 insertions(+), 27 deletions(-) diff --git a/yarn-project/accounts/src/single_key/index.ts b/yarn-project/accounts/src/single_key/index.ts index f29ed18a7e6..72bb8d7c1de 100644 --- a/yarn-project/accounts/src/single_key/index.ts +++ b/yarn-project/accounts/src/single_key/index.ts @@ -7,8 +7,7 @@ import { AccountManager, type Salt } from '@aztec/aztec.js/account'; import { type AccountWallet, getWallet } from '@aztec/aztec.js/wallet'; import { type GrumpkinPrivateKey, type PXE } from '@aztec/circuit-types'; -import { type AztecAddress, type Fr, GeneratorIndex } from '@aztec/circuits.js'; -import { sha512ToGrumpkinScalar } from '@aztec/foundation/crypto'; +import { type AztecAddress, type Fr, deriveMasterIncomingViewingSecretKey } from '@aztec/circuits.js'; import { SingleKeyAccountContract } from './account_contract.js'; @@ -23,7 +22,7 @@ export { SchnorrSingleKeyAccountContractArtifact as SingleKeyAccountContractArti * @param salt - Deployment salt. */ export function getSingleKeyAccount(pxe: PXE, secretKey: Fr, salt?: Salt): AccountManager { - const encryptionPrivateKey = sha512ToGrumpkinScalar([secretKey, GeneratorIndex.IVSK_M]); + const encryptionPrivateKey = deriveMasterIncomingViewingSecretKey(secretKey); return new AccountManager(pxe, secretKey, new SingleKeyAccountContract(encryptionPrivateKey), salt); } diff --git a/yarn-project/accounts/src/testing/configuration.ts b/yarn-project/accounts/src/testing/configuration.ts index b5a1d7586fc..bb58e05c3cf 100644 --- a/yarn-project/accounts/src/testing/configuration.ts +++ b/yarn-project/accounts/src/testing/configuration.ts @@ -1,8 +1,7 @@ import { generatePublicKey } from '@aztec/aztec.js'; import { type AccountWalletWithSecretKey } from '@aztec/aztec.js/wallet'; import { type PXE } from '@aztec/circuit-types'; -import { GeneratorIndex } from '@aztec/circuits.js/constants'; -import { sha512ToGrumpkinScalar } from '@aztec/foundation/crypto'; +import { deriveMasterIncomingViewingSecretKey, deriveSigningKey } from '@aztec/circuits.js/keys'; import { Fr } from '@aztec/foundation/fields'; import { getSchnorrAccount } from '../schnorr/index.js'; @@ -14,7 +13,7 @@ export const INITIAL_TEST_SECRET_KEYS = [ ]; export const INITIAL_TEST_ENCRYPTION_KEYS = INITIAL_TEST_SECRET_KEYS.map(secretKey => - sha512ToGrumpkinScalar([secretKey, GeneratorIndex.IVSK_M]), + 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; @@ -43,14 +42,14 @@ export async function getDeployedTestAccountsWallets(pxe: PXE): Promise { - const initialEncryptionKey = sha512ToGrumpkinScalar([initialSecretKey, GeneratorIndex.IVSK_M]); + const initialEncryptionKey = deriveMasterIncomingViewingSecretKey(initialSecretKey); const publicKey = generatePublicKey(initialEncryptionKey); return ( registeredAccounts.find(registered => registered.publicKeys.masterIncomingViewingPublicKey.equals(publicKey)) != undefined ); }).map(secretKey => { - const signingKey = sha512ToGrumpkinScalar([secretKey, GeneratorIndex.IVSK_M]); + const signingKey = deriveSigningKey(secretKey); // TODO(#5726): use actual salt here instead of hardcoding Fr.ZERO return getSchnorrAccount(pxe, secretKey, signingKey, Fr.ZERO).getWallet(); }), diff --git a/yarn-project/accounts/src/testing/create_account.ts b/yarn-project/accounts/src/testing/create_account.ts index 262fb29e07b..5f9903165da 100644 --- a/yarn-project/accounts/src/testing/create_account.ts +++ b/yarn-project/accounts/src/testing/create_account.ts @@ -1,7 +1,6 @@ import { type AccountWalletWithSecretKey } from '@aztec/aztec.js/wallet'; import { type PXE } from '@aztec/circuit-types'; -import { Fr, GeneratorIndex } from '@aztec/circuits.js'; -import { sha512ToGrumpkinScalar } from '@aztec/foundation/crypto'; +import { Fr, deriveSigningKey } from '@aztec/circuits.js'; import { getSchnorrAccount } from '../schnorr/index.js'; @@ -12,7 +11,7 @@ import { getSchnorrAccount } from '../schnorr/index.js'; */ export function createAccount(pxe: PXE): Promise { const secretKey = Fr.random(); - const signingKey = sha512ToGrumpkinScalar([secretKey, GeneratorIndex.IVSK_M]); + const signingKey = deriveSigningKey(secretKey); return getSchnorrAccount(pxe, secretKey, signingKey).waitSetup(); } @@ -20,16 +19,27 @@ export function createAccount(pxe: PXE): Promise { * Creates a given number of random accounts using the Schnorr account wallet. * @param pxe - PXE. * @param numberOfAccounts - How many accounts to create. + * @param secrets - Optional array of secrets to use for the accounts. If empty, random secrets will be generated. + * @throws If the secrets array is not empty and does not have the same length as the number of accounts. * @returns The created account wallets. */ -export async function createAccounts(pxe: PXE, numberOfAccounts = 1): Promise { +export async function createAccounts( + pxe: PXE, + numberOfAccounts = 1, + secrets: Fr[] = [], +): Promise { const accounts = []; + if (secrets.length == 0) { + secrets = Array.from({ length: numberOfAccounts }, () => Fr.random()); + } else if (secrets.length > 0 && secrets.length !== numberOfAccounts) { + throw new Error('Secrets array must be empty or have the same length as the number of accounts'); + } + // Prepare deployments - for (let i = 0; i < numberOfAccounts; ++i) { - const secretKey = Fr.random(); - const signingKey = sha512ToGrumpkinScalar([secretKey, GeneratorIndex.IVSK_M]); - const account = getSchnorrAccount(pxe, secretKey, signingKey); + for (const secret of secrets) { + const signingKey = deriveSigningKey(secret); + const account = getSchnorrAccount(pxe, secret, signingKey); // Unfortunately the function below is not stateless and we call it here because it takes a long time to run and // the results get stored within the account object. By calling it here we increase the probability of all the // accounts being deployed in the same block because it makes the deploy() method basically instant. diff --git a/yarn-project/end-to-end/src/e2e_key_registry.test.ts b/yarn-project/end-to-end/src/e2e_key_registry.test.ts index d0c47faa187..2d347a553fd 100644 --- a/yarn-project/end-to-end/src/e2e_key_registry.test.ts +++ b/yarn-project/end-to-end/src/e2e_key_registry.test.ts @@ -1,5 +1,16 @@ -import { type AccountWallet, AztecAddress, Fr, type PXE } from '@aztec/aztec.js'; -import { CompleteAddress, Point, PublicKeys } from '@aztec/circuits.js'; +import { createAccounts } from '@aztec/accounts/testing'; +import { type AccountWallet, AztecAddress, type AztecNode, Fr, type L2Block, type PXE } from '@aztec/aztec.js'; +import { + CompleteAddress, + GeneratorIndex, + INITIAL_L2_BLOCK_NUM, + Point, + PublicKeys, + computeAppNullifierSecretKey, + deriveMasterNullifierSecretKey, +} from '@aztec/circuits.js'; +import { siloNullifier } from '@aztec/circuits.js/hash'; +import { poseidon2Hash } from '@aztec/foundation/crypto'; import { KeyRegistryContract, TestContract } from '@aztec/noir-contracts.js'; import { getCanonicalKeyRegistryAddress } from '@aztec/protocol-contracts/key-registry'; @@ -15,6 +26,7 @@ describe('Key Registry', () => { let keyRegistry: KeyRegistryContract; let pxe: PXE; + let aztecNode: AztecNode; let testContract: TestContract; jest.setTimeout(TIMEOUT); @@ -25,7 +37,7 @@ describe('Key Registry', () => { const account = CompleteAddress.random(); beforeAll(async () => { - ({ teardown, pxe, wallets } = await setup(3)); + ({ aztecNode, teardown, pxe, wallets } = await setup(2)); keyRegistry = await KeyRegistryContract.at(getCanonicalKeyRegistryAddress(), wallets[0]); testContract = await TestContract.deploy(wallets[0]).send().deployed(); @@ -208,4 +220,53 @@ describe('Key Registry', () => { .wait(); }); }); + + describe('using nsk_app to detect nullification', () => { + // This test checks that it possible to detect that a note has been nullified just by using nsk_app. Note that this + // only works for non-transient note as transient notes never emit a note hash which makes it impossible to brute + // force their nullifier. This makes this scheme a bit useless in practice. + it('nsk_app and contract address are enough to detect note nullification', async () => { + const secret = Fr.random(); + const [account] = await createAccounts(pxe, 1, [secret]); + + const masterNullifierSecretKey = deriveMasterNullifierSecretKey(secret); + const nskApp = computeAppNullifierSecretKey(masterNullifierSecretKey, testContract.address); + + const noteValue = 5; + const noteOwner = account.getAddress(); + const noteStorageSlot = 12; + + await testContract.methods.call_create_note(noteValue, noteOwner, noteStorageSlot).send().wait(); + + expect(await getNumNullifiedNotes(nskApp, testContract.address)).toEqual(0); + + await testContract.methods.call_destroy_note(noteStorageSlot).send().wait(); + + expect(await getNumNullifiedNotes(nskApp, testContract.address)).toEqual(1); + }); + + const getNumNullifiedNotes = async (nskApp: Fr, contractAddress: AztecAddress) => { + // 1. Get all the note hashes + const blocks = await aztecNode.getBlocks(INITIAL_L2_BLOCK_NUM, 1000); + const noteHashes = blocks.flatMap((block: L2Block) => + block.body.txEffects.flatMap(txEffect => txEffect.noteHashes), + ); + // 2. Get all the seen nullifiers + const nullifiers = blocks.flatMap((block: L2Block) => + block.body.txEffects.flatMap(txEffect => txEffect.nullifiers), + ); + // 3. Derive all the possible nullifiers using nskApp + const derivedNullifiers = noteHashes.map(noteHash => { + const innerNullifier = poseidon2Hash([noteHash, nskApp, GeneratorIndex.NOTE_NULLIFIER]); + return siloNullifier(contractAddress, innerNullifier); + }); + // 4. Count the number of derived nullifiers that are in the nullifiers array + return derivedNullifiers.reduce((count, derived) => { + if (nullifiers.some(nullifier => nullifier.equals(derived))) { + count++; + } + return count; + }, 0); + }; + }); }); diff --git a/yarn-project/end-to-end/src/shared/browser.ts b/yarn-project/end-to-end/src/shared/browser.ts index 0aec2e8b7b9..d9af1477328 100644 --- a/yarn-project/end-to-end/src/shared/browser.ts +++ b/yarn-project/end-to-end/src/shared/browser.ts @@ -218,7 +218,6 @@ export const browserTestSuite = ( createPXEClient, getSchnorrAccount, Contract, - deriveKeys, Fr, ExtendedNote, Note, @@ -248,11 +247,9 @@ export const browserTestSuite = ( knownAccounts.push(newAccount); } const owner = knownAccounts[0]; - // TODO(#5726): this is messy, maybe we should expose publicKeysHash on account - const publicKeysHash = deriveKeys(INITIAL_TEST_SECRET_KEYS[0]).publicKeys.hash(); const ownerAddress = owner.getAddress(); const tx = new DeployMethod( - publicKeysHash, + owner.getCompleteAddress().publicKeys.hash(), owner, TokenContractArtifact, (a: AztecJs.AztecAddress) => Contract.at(a, TokenContractArtifact, owner), diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index b7de7347716..7b604c40a2f 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -804,10 +804,6 @@ export class PXEService implements PXE { return Promise.resolve(this.synchronizer.getSyncStatus()); } - public getKeyStore() { - return this.keyStore; - } - public async isContractClassPubliclyRegistered(id: Fr): Promise { return !!(await this.node.getContractClass(id)); }