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

refactor: refactor key rotate and address comments from 6405 #6450

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7d4ace4
Initial
sklppy88 May 16, 2024
a49681c
Addressing comments
sklppy88 May 16, 2024
9b1eb35
Addressing comments
sklppy88 May 16, 2024
034948d
Comment
sklppy88 May 16, 2024
f6902d0
test
sklppy88 May 16, 2024
3d10f31
Merge branch 'master' into ek/refactor/refactor-key-rotate-call-and-a…
sklppy88 May 17, 2024
0e738f7
One way
sklppy88 May 17, 2024
5ac568e
Comments
sklppy88 May 17, 2024
49652b5
Add
sklppy88 May 17, 2024
50a3909
Fmt
sklppy88 May 17, 2024
29b70fd
comment
sklppy88 May 17, 2024
06fb183
Missing
sklppy88 May 17, 2024
7853a03
Apply Jan's suggestions
sklppy88 May 17, 2024
c9e0a05
Add to interface
sklppy88 May 17, 2024
b206b7c
Comment
sklppy88 May 17, 2024
c602171
Revert "Add to interface"
sklppy88 May 17, 2024
d33e54f
Revert "Revert "Add to interface""
sklppy88 May 17, 2024
a3a503d
Fix
sklppy88 May 17, 2024
eaaf567
Fix
sklppy88 May 17, 2024
11b9e89
Merge branch 'master' into ek/refactor/refactor-key-rotate-call-and-a…
sklppy88 May 17, 2024
f595765
Format
sklppy88 May 17, 2024
41b99f4
Comment
sklppy88 May 17, 2024
6e9a0d3
Fix
sklppy88 May 17, 2024
7309988
Comment
sklppy88 May 17, 2024
6777c35
Merge branch 'master' into ek/refactor/refactor-key-rotate-call-and-a…
sklppy88 May 17, 2024
0d2bdd3
Merge branch 'master' into ek/refactor/refactor-key-rotate-call-and-a…
sklppy88 May 17, 2024
42a0551
Merge branch 'master' into ek/refactor/refactor-key-rotate-call-and-a…
sklppy88 May 18, 2024
e26e04b
Merge branch 'master' into ek/refactor/refactor-key-rotate-call-and-a…
sklppy88 May 20, 2024
924b901
Fix issues from merge
sklppy88 May 20, 2024
6044733
Merge branch 'master' into ek/refactor/refactor-key-rotate-call-and-a…
sklppy88 May 20, 2024
3378ac0
Merge branch 'master' into ek/refactor/refactor-key-rotate-call-and-a…
sklppy88 May 21, 2024
62ccede
Comments
sklppy88 May 21, 2024
cb500ec
Format
sklppy88 May 21, 2024
7bbed9e
Merge fixes
sklppy88 May 21, 2024
0225544
Merge branch 'master' into ek/refactor/refactor-key-rotate-call-and-a…
sklppy88 May 21, 2024
0fecfc2
Merge branch 'master' into ek/refactor/refactor-key-rotate-call-and-a…
sklppy88 May 21, 2024
3e9e018
Merge branch 'master' into ek/refactor/refactor-key-rotate-call-and-a…
sklppy88 May 21, 2024
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
14 changes: 2 additions & 12 deletions docs/docs/guides/js_apps/rotate_keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,6 @@ You will need to import these from Aztec.js:

## Rotate nullifier secret and public key

Call `rotateMasterNullifierKey` on the PXE to rotate the secret key.
Call `rotateNullifierKeys` on the AccountWallet to rotate the secret key in the PXE and call the key registry with the new derived public key.

#include_code rotateMasterNullifierKey yarn-project/end-to-end/src/e2e_key_rotation.test.ts typescript

## Rotate public key

Connect to the key registry contract with your wallet.

#include_code keyRegistryWithB yarn-project/end-to-end/src/e2e_key_rotation.test.ts typescript

Then `rotate_npk_m` on the key registry contract to rotate the public key:

#include_code rotate_npk_m yarn-project/end-to-end/src/e2e_key_rotation.test.ts typescript
#include_code rotateNullifierKeys yarn-project/end-to-end/src/e2e_key_rotation.test.ts typescript
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ contract Child {
fn private_get_value(amount: Field, owner: AztecAddress) -> Field {
let owner_npk_m_hash = get_npk_m_hash(&mut context, owner);

// TODO (#6312): This will break with key rotation. Fix this. Will not be able to find any notes after rotating key.
let mut options = NoteGetterOptions::new();
options = options.select(ValueNote::properties().value, amount, Option::none()).select(
ValueNote::properties().npk_m_hash,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub fn create_account_card_getter_options(
account_npk_m_hash: Field,
offset: u32
) -> NoteGetterOptions<CardNote, CARD_NOTE_LEN, Field> {
// TODO (#6312): This will break with key rotation. Fix this. Will not be able to find any notes after rotating key.
let mut options = NoteGetterOptions::new();
options.select(
CardNote::properties().npk_m_hash,
Expand All @@ -26,6 +27,7 @@ pub fn create_exact_card_getter_options(
secret: Field,
account_npk_m_hash: Field
) -> NoteGetterOptions<CardNote, CARD_NOTE_LEN, Field> {
// TODO (#6312): This will break with key rotation. Fix this. Will not be able to find any notes after rotating key.
let mut options = NoteGetterOptions::new();
options.select(CardNote::properties().points, points as Field, Option::none()).select(CardNote::properties().randomness, secret, Option::none()).select(
CardNote::properties().npk_m_hash,
Expand Down Expand Up @@ -54,6 +56,7 @@ pub fn filter_min_points(

// docs:start:state_vars-NoteGetterOptionsFilter
pub fn create_account_cards_with_min_points_getter_options(account_npk_m_hash: Field, min_points: u8) -> NoteGetterOptions<CardNote, CARD_NOTE_LEN, u8> {
// TODO (#6312): This will break with key rotation. Fix this. Will not be able to find any notes after rotating key.
NoteGetterOptions::with_filter(filter_min_points, min_points).select(
CardNote::properties().npk_m_hash,
account_npk_m_hash,
Expand All @@ -64,6 +67,7 @@ pub fn create_account_cards_with_min_points_getter_options(account_npk_m_hash: F

// docs:start:state_vars-NoteGetterOptionsPickOne
pub fn create_largest_account_card_getter_options(account_npk_m_hash: Field) -> NoteGetterOptions<CardNote, CARD_NOTE_LEN, Field> {
// TODO (#6312): This will break with key rotation. Fix this. Will not be able to find any notes after rotating key.
let mut options = NoteGetterOptions::new();
options.select(
CardNote::properties().npk_m_hash,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ contract StatefulTest {
fn create_note_no_init_check(owner: AztecAddress, value: Field) {
if (value != 0) {
let loc = storage.notes.at(owner);
// TODO (#6312): This will break with key rotation. Fix this. Will not be able to spend / increment any notes after rotating key.
increment(loc, value, owner);
}
}
Expand Down
18 changes: 17 additions & 1 deletion yarn-project/aztec.js/src/account/interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type AuthWitness, type CompleteAddress, type FunctionCall } from '@aztec/circuit-types';
import { type AztecAddress } from '@aztec/circuits.js';
import { type Fr } from '@aztec/foundation/fields';
import { type Fq, type Fr } from '@aztec/foundation/fields';

import { type ContractFunctionInteraction } from '../contract/contract_function_interaction.js';
import { type EntrypointInterface } from '../entrypoint/entrypoint.js';
Expand Down Expand Up @@ -51,4 +51,20 @@ export interface AccountInterface extends AuthWitnessProvider, EntrypointInterfa
/** Returns the rollup version for this account */
getVersion(): Fr;
}

/**
* Handler for interfacing with an account's ability to rotate its keys.
*/
export interface AccountKeyRotationInterface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be similar to the authwitprovider? KeyRotationInterface and then have the usual AccountInterface also implement that?

So it is something that it possible from all of the accounts, so might be similar to the authwit in that sense 🤷

Copy link
Contributor Author

@sklppy88 sklppy88 May 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I like it right now as this makes AccountInterface requires access to the PXE / key store and we'd have to change a lot. Probably would be better refactored when we figure out exactly how we want our classes to look like here because there is a lot of jank as is.

/**
* Rotates the account master nullifier key pair.
* @param newNskM - The new master nullifier secret key we want to use.
* @remarks - This function also calls the canonical key registry with the account's new derived master nullifier 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 master nullifier public key, provided we have the master nullifier secret key for it.
*/
rotateNullifierKeys(newNskM: Fq): Promise<void>;
}
// docs:end:account-interface
4 changes: 2 additions & 2 deletions yarn-project/aztec.js/src/account/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { type PXE } from '@aztec/circuit-types';

import { type AccountInterface } from './interface.js';
import { type AccountInterface, type AccountKeyRotationInterface } from './interface.js';

/**
* The wallet interface.
*/
export type Wallet = AccountInterface & PXE;
export type Wallet = AccountInterface & PXE & AccountKeyRotationInterface;
61 changes: 60 additions & 1 deletion yarn-project/aztec.js/src/wallet/account_wallet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type AuthWitness, type FunctionCall, type PXE, type TxExecutionRequest } from '@aztec/circuit-types';
import { type AztecAddress, Fr } from '@aztec/circuits.js';
import { AztecAddress, CANONICAL_KEY_REGISTRY_ADDRESS, Fq, Fr, 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 @@ -165,6 +165,30 @@ export class AccountWallet extends BaseWallet {
return { isValidInPrivate, isValidInPublic };
}

/**
* Rotates the account master nullifier key pair.
* @param newNskM - The new master nullifier secret key we want to use.
* @remarks - This function also calls the canonical key registry with the account's new derived master nullifier 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 master nullifier public key, provided we have the master nullifier secret key for it.
*/
public async rotateNullifierKeys(newNskM: Fq = 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the cases that it happens a lot it might use a lot of storage. But it can quite easily be "cleaned" later by looping over the keys and simply checking which either maps the address preimage or that the matching public was in the registry at some point, but don't think it is meaningful for us to really look into now 🤷

// 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.
await this.pxe.rotateNskM(this.getAddress(), newNskM);
const interaction = new ContractFunctionInteraction(
this,
AztecAddress.fromBigInt(CANONICAL_KEY_REGISTRY_ADDRESS),
this.getRotateNpkMAbi(),
[this.getAddress(), derivePublicKeyFromSecretKey(newNskM), 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 @@ -268,4 +292,39 @@ export class AccountWallet extends BaseWallet {
returnTypes: [{ kind: 'array', length: 2, type: { kind: 'boolean' } }],
};
}

private getRotateNpkMAbi(): FunctionAbi {
return {
name: 'rotate_npk_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_npk_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: [],
};
}
}
6 changes: 4 additions & 2 deletions yarn-project/aztec.js/src/wallet/base_wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export abstract class BaseWallet implements Wallet {
},
): Promise<AuthWitness>;

abstract rotateNullifierKeys(newNskM: Fq): Promise<void>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is abstract so this can still implement wallet


getAddress() {
return this.getCompleteAddress().address;
}
Expand All @@ -69,8 +71,8 @@ export abstract class BaseWallet implements Wallet {
registerAccount(secretKey: Fr, partialAddress: PartialAddress): Promise<CompleteAddress> {
return this.pxe.registerAccount(secretKey, partialAddress);
}
rotateMasterNullifierKey(account: AztecAddress, secretKey: Fq): Promise<void> {
return this.pxe.rotateMasterNullifierKey(account, secretKey);
rotateNskM(address: AztecAddress, secretKey: Fq) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is gross but here because wallet implements PXE.

return this.pxe.rotateNskM(address, secretKey);
}
registerRecipient(account: CompleteAddress): Promise<void> {
return this.pxe.registerRecipient(account);
Expand Down
6 changes: 5 additions & 1 deletion yarn-project/aztec.js/src/wallet/signerless_wallet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type AuthWitness, type PXE, type TxExecutionRequest } from '@aztec/circuit-types';
import { type CompleteAddress, type Fr } from '@aztec/circuits.js';
import { type CompleteAddress, type Fq, type Fr } from '@aztec/circuits.js';

import { DefaultEntrypoint } from '../entrypoint/default_entrypoint.js';
import { type EntrypointInterface, type ExecutionRequestInit } from '../entrypoint/entrypoint.js';
Expand Down Expand Up @@ -42,4 +42,8 @@ export class SignerlessWallet extends BaseWallet {
createAuthWit(_messageHash: Fr): Promise<AuthWitness> {
throw new Error('Method not implemented.');
}

rotateNullifierKeys(_newNskM: Fq): Promise<void> {
throw new Error('Method not implemented.');
}
}
14 changes: 12 additions & 2 deletions yarn-project/circuit-types/src/interfaces/pxe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ export interface PXE {
*/
registerAccount(secretKey: Fr, partialAddress: PartialAddress): Promise<CompleteAddress>;

rotateMasterNullifierKey(account: AztecAddress, secretKey: Fq): Promise<void>;

/**
* Registers a recipient in PXE. This is required when sending encrypted notes to
* a user who hasn't deployed their account contract yet. Since their account is not deployed, their
Expand Down Expand Up @@ -107,6 +105,18 @@ export interface PXE {
*/
getRecipient(address: AztecAddress): Promise<CompleteAddress | undefined>;

/**
* 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.
*/
rotateNskM(address: AztecAddress, newNskM: Fq): Promise<void>;

/**
* Registers a contract class in the PXE without registering any associated contract instance with it.
*
Expand Down
21 changes: 8 additions & 13 deletions yarn-project/end-to-end/src/e2e_key_rotation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@ import {
} from '@aztec/aztec.js';
// docs:start:imports
import { type PublicKey, derivePublicKeyFromSecretKey } from '@aztec/circuits.js';
import { KeyRegistryContract } from '@aztec/noir-contracts.js';
// docs:end:imports
import { TestContract, TokenContract } from '@aztec/noir-contracts.js';
import { getCanonicalKeyRegistryAddress } from '@aztec/protocol-contracts/key-registry';

// docs:end:imports
import { jest } from '@jest/globals';

import { expectsNumOfNoteEncryptedLogsInTheLastBlockToBe, setup, setupPXEService } from './fixtures/utils.js';
Expand All @@ -40,7 +38,6 @@ describe('e2e_key_rotation', () => {
let teardownA: () => Promise<void>;
let teardownB: () => Promise<void>;

let keyRegistryWithB: KeyRegistryContract;
let testContract: TestContract;
let contractWithWalletA: TokenContract;
let contractWithWalletB: TokenContract;
Expand All @@ -59,10 +56,8 @@ describe('e2e_key_rotation', () => {
} = await setup(1));

({ pxe: pxeB, teardown: teardownB } = await setupPXEService(aztecNode, {}, undefined, true));
// docs:start:keyRegistryWithB
[walletB] = await createAccounts(pxeB, 1);
keyRegistryWithB = await KeyRegistryContract.at(getCanonicalKeyRegistryAddress(), walletB);
// docs:end:keyRegistryWithB

// We deploy test and token contracts
testContract = await TestContract.deploy(walletA).send().deployed();
const tokenInstance = await deployTokenContract(initialBalance, walletA.getAddress(), pxeA);
Expand Down Expand Up @@ -181,12 +176,12 @@ describe('e2e_key_rotation', () => {
const newNskM = Fq.random();
newNpkM = derivePublicKeyFromSecretKey(newNskM);
// docs:end:create_keys
// docs:start:rotateMasterNullifierKey
await pxeB.rotateMasterNullifierKey(walletB.getAddress(), newNskM);
// docs:end:rotateMasterNullifierKey
// docs:start:rotate_npk_m
await keyRegistryWithB.methods.rotate_npk_m(walletB.getAddress(), newNpkM, 0).send().wait();
// docs:end:rotate_npk_m

// docs:start:rotateNullifierKeys
// This function saves the new nullifier secret key for the account in our PXE,
// and calls the key registry with the derived nullifier public key.
await walletB.rotateNullifierKeys(newNskM);
// docs:end:rotateNullifierKeys
await crossDelay();
}

Expand Down
8 changes: 6 additions & 2 deletions yarn-project/key-store/src/key_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export class KeyStore {
}

// Now we iterate over the public keys in the buffer to find the one that matches the hash
const numKeys = pkMsBuffer.byteLength / Point.SIZE_IN_BYTES;
const numKeys = this.#calculateNumKeys(pkMsBuffer, Point);
for (; keyIndexInBuffer < numKeys; keyIndexInBuffer++) {
const foundPkM = Point.fromBuffer(
pkMsBuffer.subarray(keyIndexInBuffer * Point.SIZE_IN_BYTES, (keyIndexInBuffer + 1) * Point.SIZE_IN_BYTES),
Expand Down Expand Up @@ -289,7 +289,7 @@ export class KeyStore {
);
}

const numKeys = secretKeysBuffer.byteLength / GrumpkinScalar.SIZE_IN_BYTES;
const numKeys = this.#calculateNumKeys(secretKeysBuffer, GrumpkinScalar);
for (let i = 0; i < numKeys; i++) {
const foundSk = GrumpkinScalar.fromBuffer(
secretKeysBuffer.subarray(i * GrumpkinScalar.SIZE_IN_BYTES, (i + 1) * GrumpkinScalar.SIZE_IN_BYTES),
Expand Down Expand Up @@ -396,4 +396,8 @@ export class KeyStore {

await this.#keys.set(key, serializeToBuffer([currentValue, value]));
}

#calculateNumKeys(buf: Buffer, T: typeof Point | typeof Fq) {
return buf.byteLength / T.SIZE_IN_BYTES;
}
}
10 changes: 5 additions & 5 deletions yarn-project/pxe/src/pxe_service/pxe_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
import { computeNoteHashNonce, siloNullifier } from '@aztec/circuits.js/hash';
import { type ContractArtifact, type DecodedReturn, FunctionSelector, encodeArguments } from '@aztec/foundation/abi';
import { arrayNonEmptyLength, padArrayEnd } from '@aztec/foundation/collection';
import { Fq, Fr } from '@aztec/foundation/fields';
import { type Fq, Fr } from '@aztec/foundation/fields';
import { SerialQueue } from '@aztec/foundation/fifo';
import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log';
import { Timer } from '@aztec/foundation/timer';
Expand Down Expand Up @@ -161,6 +161,10 @@ export class PXEService implements PXE {
return this.db.getAuthWitness(messageHash);
}

async rotateNskM(account: AztecAddress, secretKey: Fq): Promise<void> {
await this.keyStore.rotateMasterNullifierKey(account, secretKey);
}

public addCapsule(capsule: Fr[]) {
return this.db.addCapsule(capsule);
}
Expand Down Expand Up @@ -193,10 +197,6 @@ export class PXEService implements PXE {
return accountCompleteAddress;
}

public async rotateMasterNullifierKey(account: AztecAddress, secretKey: Fq = Fq.random()): Promise<void> {
await this.keyStore.rotateMasterNullifierKey(account, secretKey);
}

public async getRegisteredAccounts(): Promise<CompleteAddress[]> {
// Get complete addresses of both the recipients and the accounts
const completeAddresses = await this.db.getCompleteAddresses();
Expand Down
Loading
Loading