Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: incoming viewing key roation #6883

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ contract KeyRegistry {
npk_m_y_registry.schedule_value_change(new_npk_m.y);
}

#[aztec(public)]
fn rotate_ivpk_m(address: AztecAddress, new_ivpk_m: GrumpkinPoint, nonce: Field) {
if (!address.eq(context.msg_sender())) {
assert_current_call_valid_authwit_public(&mut context, address);
} else {
assert(nonce == 0, "invalid nonce");
}

let ivpk_m_x_registry = storage.ivpk_m_x_registry.at(address);
let ivpk_m_y_registry = storage.ivpk_m_y_registry.at(address);
ivpk_m_x_registry.schedule_value_change(new_ivpk_m.x);
ivpk_m_y_registry.schedule_value_change(new_ivpk_m.y);
}

#[aztec(public)]
fn register(address: AztecAddress, partial_address: PartialAddress, keys: PublicKeys) {
let computed_address = AztecAddress::compute(keys.hash(), partial_address);
Expand Down
67 changes: 65 additions & 2 deletions yarn-project/aztec.js/src/wallet/account_wallet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { type AuthWitness, type FunctionCall, type PXE, type TxExecutionRequest } from '@aztec/circuit-types';
import { AztecAddress, CANONICAL_KEY_REGISTRY_ADDRESS, Fq, Fr, derivePublicKeyFromSecretKey } from '@aztec/circuits.js';
import {
AztecAddress,
CANONICAL_KEY_REGISTRY_ADDRESS,
Fq,
Fr,
type GrumpkinPrivateKey,
derivePublicKeyFromSecretKey,
} from '@aztec/circuits.js';
import { type ABIParameterVisibility, type FunctionAbi, FunctionType } from '@aztec/foundation/abi';

import { type AccountInterface } from '../account/interface.js';
Expand Down Expand Up @@ -174,7 +181,7 @@ export class AccountWallet extends BaseWallet {
*
* This does not hinder our ability to spend notes tied to a previous master nullifier public key, provided we have the master nullifier secret key for it.
*/
public async rotateNullifierKeys(newNskM: Fq = Fq.random()): Promise<void> {
public async rotateNullifierKeys(newNskM: GrumpkinPrivateKey = Fq.random()): Promise<void> {
// We rotate our secret key in the keystore first, because if the subsequent interaction fails, there are no bad side-effects.
// If vice versa (the key registry is called first), but the call to the PXE fails, we will end up in a situation with unspendable notes, as we have not committed our
// nullifier secret key to our wallet.
Expand All @@ -189,6 +196,27 @@ export class AccountWallet extends BaseWallet {
await interaction.send().wait();
}

/**
* Rotates the account incoming viewing key pair.
* @param newIvskM - The new incoming viewing key secret key we want to use.
* @remarks - This function also calls the canonical key registry with the account's new derived incoming viewing public key.
* We are doing it this way to avoid user error, in the case that a user rotates their keys in the key registry,
* but fails to do so in the key store. This leads to unspendable notes.
*
* This does not hinder our ability to spend notes tied to a previous incoming viewing public key, provided we have the incoming viewing secret key for it.
*/
public async rotateIncomingViewingKeys(newIvskM: GrumpkinPrivateKey = Fq.random()): Promise<void> {
await this.pxe.rotateIvskM(this.getAddress(), newIvskM);
const interaction = new ContractFunctionInteraction(
this,
AztecAddress.fromBigInt(CANONICAL_KEY_REGISTRY_ADDRESS),
this.getRotateIvpkMAbi(),
[this.getAddress(), derivePublicKeyFromSecretKey(newIvskM), Fr.ZERO],
);

await interaction.send().wait();
}

/**
* Returns a function interaction to cancel a message hash as authorized in this account.
* @param messageHashOrIntent - The message or the caller and action to authorize/revoke
Expand Down Expand Up @@ -327,4 +355,39 @@ export class AccountWallet extends BaseWallet {
returnTypes: [],
};
}

private getRotateIvpkMAbi(): FunctionAbi {
return {
name: 'rotate_ivpk_m',
isInitializer: false,
functionType: FunctionType.PUBLIC,
isInternal: false,
isStatic: false,
parameters: [
{
name: 'address',
type: {
fields: [{ name: 'inner', type: { kind: 'field' } }],
kind: 'struct',
path: 'authwit::aztec::protocol_types::address::aztec_address::AztecAddress',
},
visibility: 'private' as ABIParameterVisibility,
},
{
name: 'new_ivpk_m',
type: {
fields: [
{ name: 'x', type: { kind: 'field' } },
{ name: 'y', type: { kind: 'field' } },
],
kind: 'struct',
path: 'authwit::aztec::protocol_types::grumpkin_point::GrumpkinPoint',
},
visibility: 'private' as ABIParameterVisibility,
},
{ name: 'nonce', type: { kind: 'field' }, visibility: 'private' as ABIParameterVisibility },
],
returnTypes: [],
};
}
}
3 changes: 3 additions & 0 deletions yarn-project/aztec.js/src/wallet/base_wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ export abstract class BaseWallet implements Wallet {
rotateNskM(address: AztecAddress, secretKey: Fq) {
return this.pxe.rotateNskM(address, secretKey);
}
rotateIvskM(address: AztecAddress, secretKey: Fq) {
return this.pxe.rotateIvskM(address, secretKey);
}
registerRecipient(account: CompleteAddress): Promise<void> {
return this.pxe.registerRecipient(account);
}
Expand Down
26 changes: 19 additions & 7 deletions yarn-project/circuit-types/src/interfaces/pxe.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { type AztecAddress, type CompleteAddress, type Fq, type Fr, type PartialAddress } from '@aztec/circuits.js';
import {
type AztecAddress,
type CompleteAddress,
type Fr,
type GrumpkinPrivateKey,
type PartialAddress,
} from '@aztec/circuits.js';
import { type ContractArtifact } from '@aztec/foundation/abi';
import {
type ContractClassWithId,
Expand Down Expand Up @@ -114,13 +120,19 @@ export interface PXE {
* Rotates master nullifier keys.
* @param address - The address of the account we want to rotate our key for.
* @param newNskM - The new master nullifier secret key we want to use.
* @remarks - One should not use this function directly without also calling the canonical key registry to rotate
* the new master nullifier secret key's derived master nullifier public key.
* Therefore, it is preferred to use rotateNullifierKeys on AccountWallet, as that handles the call to the Key Registry as well.
*
* This does not hinder our ability to spend notes tied to a previous master nullifier public key, provided we have the master nullifier secret key for it.
* @remarks - Has to be called along with rotate_npk_m on key registry contract. Instead of this function call
* rotateNullifierKeys on AccountWallet to handle the call to the Key Registry as well.
*/
rotateNskM(address: AztecAddress, newNskM: GrumpkinPrivateKey): Promise<void>;

/**
* Rotates incoming viewing keys.
* @param address - The address of the account we want to rotate our key for.
* @param newIvskM - The new incoming viewing key secret key we want to use.
* @remarks - Has to be called along with rotate_ivpk_m on key registry contract. Instead of this function call
* rotateIncomingViewingKeys on AccountWallet to handle the call to the Key Registry as well.
*/
rotateNskM(address: AztecAddress, newNskM: Fq): Promise<void>;
rotateIvskM(address: AztecAddress, newIvskM: GrumpkinPrivateKey): Promise<void>;

/**
* Registers a contract class in the PXE without registering any associated contract instance with it.
Expand Down
4 changes: 3 additions & 1 deletion yarn-project/end-to-end/src/e2e_key_rotation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ describe('e2e_key_rotation', () => {
await contract.methods.redeem_shield(recipient, balance, secret).send().wait();
};

it(`Rotates keys and uses them`, async () => {
it(`Rotates nullifier keys and uses them`, async () => {
// 1. We check that setup set initial balances as expected
await expectTokenBalance(walletA, tokenAddress, walletA.getAddress(), initialBalance);
await expectTokenBalance(walletB, tokenAddress, walletB.getAddress(), 0n);
Expand Down Expand Up @@ -233,4 +233,6 @@ describe('e2e_key_rotation', () => {
await expectTokenBalance(walletB, tokenAddress, walletB.getAddress(), 0n);
}
}, 600_000);

it(`Rotates incoming viewing keys`, async () => {});
});
33 changes: 33 additions & 0 deletions yarn-project/key-store/src/__snapshots__/key_store.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`KeyStore key rotation tests 1`] = `"0x1a8a9a1d91cbb353d8df4f1bbfd0283f7fc63766f671edd9443a1270a7b2a954"`;

exports[`KeyStore key rotation tests 2`] = `"0x296e42f1039b62290372d608fcab55b00a3f96c1c8aa347b2a830639c5a12757"`;

exports[`KeyStore key rotation tests 3`] = `"0x019f2a705b68683f1d86da639a543411fa779af41896c3920d0c2d5226c686dd"`;

exports[`KeyStore key rotation tests 4`] = `"0x117445c8819c06b9a0889e5cce1f550e32ec6993c23f57bc9fc5cda05df520ae"`;

exports[`KeyStore key rotation tests 5`] = `"0x1a8a9a1d91cbb353d8df4f1bbfd0283f7fc63766f671edd9443a1270a7b2a954"`;

exports[`KeyStore key rotation tests 6`] = `"0x21e3ca4bc7ae2b5e9fe343f4eec5c0aa7391857333821a4b0a1c7d4cb0055bf0"`;

exports[`KeyStore key rotation tests 7`] = `"0x0900aea4825d057e5bc916063a535520a7c6283740eaf218cd6961b10cba46fd"`;

exports[`KeyStore key rotation tests 8`] = `"0x27ccbe41ff5f33fa78348533da9d4a79e8fea8805771e61748ea42be4202f168"`;

exports[`KeyStore key rotation tests 9`] = `"0x1a8a9a1d91cbb353d8df4f1bbfd0283f7fc63766f671edd9443a1270a7b2a954"`;

exports[`KeyStore key rotation tests 10`] = `"0x1895c1aa4f807cc8429c4d73a7bcb366653fea18f2b35f12589c599398733e7b"`;

exports[`KeyStore key rotation tests 11`] = `"0x28e0f5e42af72adfa2aa1f7c64c34dc9589104e4a3bc0ac0d63f21a6780d8250"`;

exports[`KeyStore key rotation tests 12`] = `"0x0f00f1ac34f2e4ba49d25cc57b4d0ed45a1cc2b55b292e1fb09b157887153f8e"`;

exports[`KeyStore key rotation tests 13`] = `"0x1a8a9a1d91cbb353d8df4f1bbfd0283f7fc63766f671edd9443a1270a7b2a954"`;

exports[`KeyStore key rotation tests 14`] = `"0x105baf9ca58f8a5bff283642c423f3f1d46107975f05a751abc7e8b51728277f"`;

exports[`KeyStore key rotation tests 15`] = `"0x2fb411ed0ba7bf7b44963b1c36a1de17546da2ebee4564d9cc62486c3325149e"`;

exports[`KeyStore key rotation tests 16`] = `"0x1c0fb3a8709e6ea1a9c67b3b499b5e6d0471cb38a6c824319805aeaca0e201df"`;
88 changes: 41 additions & 47 deletions yarn-project/key-store/src/key_store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import {
AztecAddress,
Fq,
Fr,
computeAppNullifierSecretKey,
type KeyPrefix,
computeAppSecretKey,
deriveKeys,
derivePublicKeyFromSecretKey,
KEY_PREFIXES,
} from '@aztec/circuits.js';
import { openTmpStore } from '@aztec/kv-store/utils';

Expand Down Expand Up @@ -89,82 +91,74 @@ describe('KeyStore', () => {
);
});

it('nullifier key rotation tests', async () => {
it.each(KEY_PREFIXES)('key rotation tests', async keyPrefix => {
const keyStore = new KeyStore(openTmpStore());

// Arbitrary fixed values
const sk = new Fr(8923n);
const partialAddress = new Fr(243523n);

const { address: accountAddress } = await keyStore.addAccount(sk, partialAddress);
expect(accountAddress.toString()).toMatchInlineSnapshot(
`"0x1a8a9a1d91cbb353d8df4f1bbfd0283f7fc63766f671edd9443a1270a7b2a954"`,
);
expect(accountAddress.toString()).toMatchSnapshot();

// Arbitrary fixed values
const newMasterNullifierSecretKeys = [new Fq(420n), new Fq(69n), new Fq(42069n)];
const newDerivedMasterNullifierPublicKeys = [
derivePublicKeyFromSecretKey(newMasterNullifierSecretKeys[0]),
derivePublicKeyFromSecretKey(newMasterNullifierSecretKeys[1]),
derivePublicKeyFromSecretKey(newMasterNullifierSecretKeys[2]),
const newMasterSecretKeys = [new Fq(420n), new Fq(69n), new Fq(42069n)];
const newDerivedMasterPublicKeys = [
derivePublicKeyFromSecretKey(newMasterSecretKeys[0]),
derivePublicKeyFromSecretKey(newMasterSecretKeys[1]),
derivePublicKeyFromSecretKey(newMasterSecretKeys[2]),
];

const newComputedMasterNullifierPublicKeyHashes = [
newDerivedMasterNullifierPublicKeys[0].hash(),
newDerivedMasterNullifierPublicKeys[1].hash(),
newDerivedMasterNullifierPublicKeys[2].hash(),
const newComputedMasterPublicKeyHashes = [
newDerivedMasterPublicKeys[0].hash(),
newDerivedMasterPublicKeys[1].hash(),
newDerivedMasterPublicKeys[2].hash(),
];

// We rotate our nullifier key
await keyStore.rotateMasterNullifierKey(accountAddress, newMasterNullifierSecretKeys[0]);
await keyStore.rotateMasterNullifierKey(accountAddress, newMasterNullifierSecretKeys[1]);
await keyStore.rotateMasterNullifierKey(accountAddress, newMasterNullifierSecretKeys[2]);
// We rotate our key
await keyStore.rotateMasterKey(accountAddress, keyPrefix, newMasterSecretKeys[0]);
await keyStore.rotateMasterKey(accountAddress, keyPrefix, newMasterSecretKeys[1]);
await keyStore.rotateMasterKey(accountAddress, keyPrefix, newMasterSecretKeys[2]);

// We make sure we can get master nullifier public keys with master nullifier public key hashes
const { pkM: masterNullifierPublicKey2 } = await keyStore.getKeyValidationRequest(
newComputedMasterNullifierPublicKeyHashes[2],
// We make sure we can get master public keys with master public key hashes
const { pkM: masterPublicKey2 } = await keyStore.getKeyValidationRequest(
newComputedMasterPublicKeyHashes[2],
AztecAddress.random(), // Address is random because we are not interested in the app secret key here
);
expect(masterNullifierPublicKey2).toEqual(newDerivedMasterNullifierPublicKeys[2]);
const { pkM: masterNullifierPublicKey1 } = await keyStore.getKeyValidationRequest(
newComputedMasterNullifierPublicKeyHashes[1],
expect(masterPublicKey2).toEqual(newDerivedMasterPublicKeys[2]);
const { pkM: masterPublicKey1 } = await keyStore.getKeyValidationRequest(
newComputedMasterPublicKeyHashes[1],
AztecAddress.random(), // Address is random because we are not interested in the app secret key here
);
expect(masterNullifierPublicKey1).toEqual(newDerivedMasterNullifierPublicKeys[1]);
const { pkM: masterNullifierPublicKey0 } = await keyStore.getKeyValidationRequest(
newComputedMasterNullifierPublicKeyHashes[0],
expect(masterPublicKey1).toEqual(newDerivedMasterPublicKeys[1]);
const { pkM: masterPublicKey0 } = await keyStore.getKeyValidationRequest(
newComputedMasterPublicKeyHashes[0],
AztecAddress.random(), // Address is random because we are not interested in the app secret key here
);
expect(masterNullifierPublicKey0).toEqual(newDerivedMasterNullifierPublicKeys[0]);
expect(masterPublicKey0).toEqual(newDerivedMasterPublicKeys[0]);

// Arbitrary app contract address
const appAddress = AztecAddress.fromBigInt(624n);

// We make sure we can get app nullifier secret keys with master nullifier public key hashes
const { skApp: appNullifierSecretKey0 } = await keyStore.getKeyValidationRequest(
newComputedMasterNullifierPublicKeyHashes[0],
// We make sure we can get app secret keys with master public key hashes
const { skApp: appSecretKey0 } = await keyStore.getKeyValidationRequest(
newComputedMasterPublicKeyHashes[0],
appAddress,
);
expect(appNullifierSecretKey0.toString()).toMatchInlineSnapshot(
`"0x296e42f1039b62290372d608fcab55b00a3f96c1c8aa347b2a830639c5a12757"`,
);
const { skApp: appNullifierSecretKey1 } = await keyStore.getKeyValidationRequest(
newComputedMasterNullifierPublicKeyHashes[1],
expect(appSecretKey0.toString()).toMatchSnapshot();
const { skApp: appSecretKey1 } = await keyStore.getKeyValidationRequest(
newComputedMasterPublicKeyHashes[1],
appAddress,
);
expect(appNullifierSecretKey1.toString()).toMatchInlineSnapshot(
`"0x019f2a705b68683f1d86da639a543411fa779af41896c3920d0c2d5226c686dd"`,
);
const { skApp: appNullifierSecretKey2 } = await keyStore.getKeyValidationRequest(
newComputedMasterNullifierPublicKeyHashes[2],
expect(appSecretKey1.toString()).toMatchSnapshot();
const { skApp: appSecretKey2 } = await keyStore.getKeyValidationRequest(
newComputedMasterPublicKeyHashes[2],
appAddress,
);
expect(appNullifierSecretKey2.toString()).toMatchInlineSnapshot(
`"0x117445c8819c06b9a0889e5cce1f550e32ec6993c23f57bc9fc5cda05df520ae"`,
);
expect(appSecretKey2.toString()).toMatchSnapshot();

expect(appNullifierSecretKey0).toEqual(computeAppNullifierSecretKey(newMasterNullifierSecretKeys[0], appAddress));
expect(appNullifierSecretKey1).toEqual(computeAppNullifierSecretKey(newMasterNullifierSecretKeys[1], appAddress));
expect(appNullifierSecretKey2).toEqual(computeAppNullifierSecretKey(newMasterNullifierSecretKeys[2], appAddress));
expect(appSecretKey0).toEqual(computeAppSecretKey(newMasterSecretKeys[0], appAddress, keyPrefix));
expect(appSecretKey1).toEqual(computeAppSecretKey(newMasterSecretKeys[1], appAddress, keyPrefix));
expect(appSecretKey2).toEqual(computeAppSecretKey(newMasterSecretKeys[2], appAddress, keyPrefix));
});
});
26 changes: 11 additions & 15 deletions yarn-project/key-store/src/key_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type PublicKey } from '@aztec/circuit-types';
import {
AztecAddress,
CompleteAddress,
Fq,
type Fq,
Fr,
GeneratorIndex,
type GrumpkinPrivateKey,
Expand Down Expand Up @@ -307,28 +307,24 @@ export class KeyStore {
}

/**
* Rotates the master nullifier key for the specified account.
*
* @dev This function updates the secret and public keys associated with the account.
* It appends a new secret key to the existing secret keys, derives the
* corresponding public key, and updates the stored keys accordingly.
*
* @param account - The account address for which the master nullifier key is being rotated.
* @param newSecretKey - (Optional) A new secret key of type Fq. If not provided, a random key is generated.
* @throws If the account does not have existing nullifier secret keys or public keys.
* Rotates the master key defined by key prefix for the specified account.
* @param account - The account address for which the master key is being rotated.
* @param keyPrefix - The key prefix of the master key to rotate.
* @param newSecretKey - A new secret key to rotate to.
* @throws If the account does not have existing secret keys or public keys.
* @returns A Promise that resolves when the key rotation is complete.
*/
public async rotateMasterNullifierKey(account: AztecAddress, newSecretKey: Fq = Fq.random()) {
public async rotateMasterKey(account: AztecAddress, keyPrefix: KeyPrefix, newSecretKey: GrumpkinPrivateKey) {
// We append the secret key to the array of secret keys
await this.#appendValue(`${account.toString()}-nsk_m`, newSecretKey);
await this.#appendValue(`${account.toString()}-${keyPrefix}sk_m`, newSecretKey);

// Now we derive the public key from the new secret key and append it to the buffer of original public keys
const newPublicKey = derivePublicKeyFromSecretKey(newSecretKey);
await this.#appendValue(`${account.toString()}-npk_m`, newPublicKey);
await this.#appendValue(`${account.toString()}-${keyPrefix}pk_m`, newPublicKey);

// At last we store npk_m_hash under `account-npk_m_hash` key to be able to obtain address and key prefix
// At last we store pk_m_hash under `account-pk_m_hash` key to be able to obtain address and key prefix
// using the #getKeyPrefixAndAccount function later on
await this.#appendValue(`${account.toString()}-npk_m_hash`, newPublicKey.hash());
await this.#appendValue(`${account.toString()}-${keyPrefix}pk_m_hash`, newPublicKey.hash());
}

/**
Expand Down
Loading
Loading