Skip to content

Commit 6f3dab8

Browse files
sklppy88benesjan
andauthored
refactor: refactor key rotate and address comments from 6405 (#6450)
Addresses comments from #6405, refactors key rotate --------- Co-authored-by: Jan Beneš <janbenes1234@gmail.com>
1 parent 8c639b5 commit 6f3dab8

File tree

14 files changed

+145
-63
lines changed

14 files changed

+145
-63
lines changed

docs/docs/guides/js_apps/rotate_keys.md

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,6 @@ You will need to import these from Aztec.js:
2626

2727
## Rotate nullifier secret and public key
2828

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

31-
#include_code rotateMasterNullifierKey yarn-project/end-to-end/src/e2e_key_rotation.test.ts typescript
32-
33-
## Rotate public key
34-
35-
Connect to the key registry contract with your wallet.
36-
37-
#include_code keyRegistryWithB yarn-project/end-to-end/src/e2e_key_rotation.test.ts typescript
38-
39-
Then `rotate_npk_m` on the key registry contract to rotate the public key:
40-
41-
#include_code rotate_npk_m yarn-project/end-to-end/src/e2e_key_rotation.test.ts typescript
31+
#include_code rotateNullifierKeys yarn-project/end-to-end/src/e2e_key_rotation.test.ts typescript

noir-projects/noir-contracts/contracts/child_contract/src/main.nr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ contract Child {
6464
fn private_get_value(amount: Field, owner: AztecAddress) -> Field {
6565
let owner_npk_m_hash = get_npk_m_hash(&mut context, owner);
6666

67+
// TODO (#6312): This will break with key rotation. Fix this. Will not be able to find any notes after rotating key.
6768
let mut options = NoteGetterOptions::new();
6869
options = options.select(ValueNote::properties().value, amount, Option::none()).select(
6970
ValueNote::properties().npk_m_hash,

noir-projects/noir-contracts/contracts/docs_example_contract/src/options.nr

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub fn create_account_card_getter_options(
1111
account_npk_m_hash: Field,
1212
offset: u32
1313
) -> NoteGetterOptions<CardNote, CARD_NOTE_LEN, Field> {
14+
// TODO (#6312): This will break with key rotation. Fix this. Will not be able to find any notes after rotating key.
1415
let mut options = NoteGetterOptions::new();
1516
options.select(
1617
CardNote::properties().npk_m_hash,
@@ -26,6 +27,7 @@ pub fn create_exact_card_getter_options(
2627
secret: Field,
2728
account_npk_m_hash: Field
2829
) -> NoteGetterOptions<CardNote, CARD_NOTE_LEN, Field> {
30+
// TODO (#6312): This will break with key rotation. Fix this. Will not be able to find any notes after rotating key.
2931
let mut options = NoteGetterOptions::new();
3032
options.select(CardNote::properties().points, points as Field, Option::none()).select(CardNote::properties().randomness, secret, Option::none()).select(
3133
CardNote::properties().npk_m_hash,
@@ -54,6 +56,7 @@ pub fn filter_min_points(
5456

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

6568
// docs:start:state_vars-NoteGetterOptionsPickOne
6669
pub fn create_largest_account_card_getter_options(account_npk_m_hash: Field) -> NoteGetterOptions<CardNote, CARD_NOTE_LEN, Field> {
70+
// TODO (#6312): This will break with key rotation. Fix this. Will not be able to find any notes after rotating key.
6771
let mut options = NoteGetterOptions::new();
6872
options.select(
6973
CardNote::properties().npk_m_hash,

noir-projects/noir-contracts/contracts/stateful_test_contract/src/main.nr

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ contract StatefulTest {
4646
fn create_note_no_init_check(owner: AztecAddress, value: Field) {
4747
if (value != 0) {
4848
let loc = storage.notes.at(owner);
49-
// TODO (#6312): This will break with key rotation. Fix this. Will not be able to spend / increment any notes after rotating key.
5049
increment(loc, value, owner);
5150
}
5251
}

yarn-project/aztec.js/src/account/interface.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type AuthWitness, type CompleteAddress, type FunctionCall } from '@aztec/circuit-types';
22
import { type AztecAddress } from '@aztec/circuits.js';
3-
import { type Fr } from '@aztec/foundation/fields';
3+
import { type Fq, type Fr } from '@aztec/foundation/fields';
44

55
import { type ContractFunctionInteraction } from '../contract/contract_function_interaction.js';
66
import { type EntrypointInterface } from '../entrypoint/entrypoint.js';
@@ -51,4 +51,20 @@ export interface AccountInterface extends AuthWitnessProvider, EntrypointInterfa
5151
/** Returns the rollup version for this account */
5252
getVersion(): Fr;
5353
}
54+
55+
/**
56+
* Handler for interfacing with an account's ability to rotate its keys.
57+
*/
58+
export interface AccountKeyRotationInterface {
59+
/**
60+
* Rotates the account master nullifier key pair.
61+
* @param newNskM - The new master nullifier secret key we want to use.
62+
* @remarks - This function also calls the canonical key registry with the account's new derived master nullifier public key.
63+
* We are doing it this way to avoid user error, in the case that a user rotates their keys in the key registry,
64+
* but fails to do so in the key store. This leads to unspendable notes.
65+
*
66+
* 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.
67+
*/
68+
rotateNullifierKeys(newNskM: Fq): Promise<void>;
69+
}
5470
// docs:end:account-interface
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { type PXE } from '@aztec/circuit-types';
22

3-
import { type AccountInterface } from './interface.js';
3+
import { type AccountInterface, type AccountKeyRotationInterface } from './interface.js';
44

55
/**
66
* The wallet interface.
77
*/
8-
export type Wallet = AccountInterface & PXE;
8+
export type Wallet = AccountInterface & PXE & AccountKeyRotationInterface;

yarn-project/aztec.js/src/wallet/account_wallet.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type AuthWitness, type FunctionCall, type PXE, type TxExecutionRequest } from '@aztec/circuit-types';
2-
import { type AztecAddress, Fr } from '@aztec/circuits.js';
2+
import { AztecAddress, CANONICAL_KEY_REGISTRY_ADDRESS, Fq, Fr, derivePublicKeyFromSecretKey } from '@aztec/circuits.js';
33
import { type ABIParameterVisibility, type FunctionAbi, FunctionType } from '@aztec/foundation/abi';
44

55
import { type AccountInterface } from '../account/interface.js';
@@ -165,6 +165,30 @@ export class AccountWallet extends BaseWallet {
165165
return { isValidInPrivate, isValidInPublic };
166166
}
167167

168+
/**
169+
* Rotates the account master nullifier key pair.
170+
* @param newNskM - The new master nullifier secret key we want to use.
171+
* @remarks - This function also calls the canonical key registry with the account's new derived master nullifier public key.
172+
* We are doing it this way to avoid user error, in the case that a user rotates their keys in the key registry,
173+
* but fails to do so in the key store. This leads to unspendable notes.
174+
*
175+
* 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.
176+
*/
177+
public async rotateNullifierKeys(newNskM: Fq = Fq.random()): Promise<void> {
178+
// We rotate our secret key in the keystore first, because if the subsequent interaction fails, there are no bad side-effects.
179+
// 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
180+
// nullifier secret key to our wallet.
181+
await this.pxe.rotateNskM(this.getAddress(), newNskM);
182+
const interaction = new ContractFunctionInteraction(
183+
this,
184+
AztecAddress.fromBigInt(CANONICAL_KEY_REGISTRY_ADDRESS),
185+
this.getRotateNpkMAbi(),
186+
[this.getAddress(), derivePublicKeyFromSecretKey(newNskM), Fr.ZERO],
187+
);
188+
189+
await interaction.send().wait();
190+
}
191+
168192
/**
169193
* Returns a function interaction to cancel a message hash as authorized in this account.
170194
* @param messageHashOrIntent - The message or the caller and action to authorize/revoke
@@ -268,4 +292,39 @@ export class AccountWallet extends BaseWallet {
268292
returnTypes: [{ kind: 'array', length: 2, type: { kind: 'boolean' } }],
269293
};
270294
}
295+
296+
private getRotateNpkMAbi(): FunctionAbi {
297+
return {
298+
name: 'rotate_npk_m',
299+
isInitializer: false,
300+
functionType: FunctionType.PUBLIC,
301+
isInternal: false,
302+
isStatic: false,
303+
parameters: [
304+
{
305+
name: 'address',
306+
type: {
307+
fields: [{ name: 'inner', type: { kind: 'field' } }],
308+
kind: 'struct',
309+
path: 'authwit::aztec::protocol_types::address::aztec_address::AztecAddress',
310+
},
311+
visibility: 'private' as ABIParameterVisibility,
312+
},
313+
{
314+
name: 'new_npk_m',
315+
type: {
316+
fields: [
317+
{ name: 'x', type: { kind: 'field' } },
318+
{ name: 'y', type: { kind: 'field' } },
319+
],
320+
kind: 'struct',
321+
path: 'authwit::aztec::protocol_types::grumpkin_point::GrumpkinPoint',
322+
},
323+
visibility: 'private' as ABIParameterVisibility,
324+
},
325+
{ name: 'nonce', type: { kind: 'field' }, visibility: 'private' as ABIParameterVisibility },
326+
],
327+
returnTypes: [],
328+
};
329+
}
271330
}

yarn-project/aztec.js/src/wallet/base_wallet.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ export abstract class BaseWallet implements Wallet {
5454
},
5555
): Promise<AuthWitness>;
5656

57+
abstract rotateNullifierKeys(newNskM: Fq): Promise<void>;
58+
5759
getAddress() {
5860
return this.getCompleteAddress().address;
5961
}
@@ -69,8 +71,8 @@ export abstract class BaseWallet implements Wallet {
6971
registerAccount(secretKey: Fr, partialAddress: PartialAddress): Promise<CompleteAddress> {
7072
return this.pxe.registerAccount(secretKey, partialAddress);
7173
}
72-
rotateMasterNullifierKey(account: AztecAddress, secretKey: Fq): Promise<void> {
73-
return this.pxe.rotateMasterNullifierKey(account, secretKey);
74+
rotateNskM(address: AztecAddress, secretKey: Fq) {
75+
return this.pxe.rotateNskM(address, secretKey);
7476
}
7577
registerRecipient(account: CompleteAddress): Promise<void> {
7678
return this.pxe.registerRecipient(account);

yarn-project/aztec.js/src/wallet/signerless_wallet.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type AuthWitness, type PXE, type TxExecutionRequest } from '@aztec/circuit-types';
2-
import { type CompleteAddress, type Fr } from '@aztec/circuits.js';
2+
import { type CompleteAddress, type Fq, type Fr } from '@aztec/circuits.js';
33

44
import { DefaultEntrypoint } from '../entrypoint/default_entrypoint.js';
55
import { type EntrypointInterface, type ExecutionRequestInit } from '../entrypoint/entrypoint.js';
@@ -42,4 +42,8 @@ export class SignerlessWallet extends BaseWallet {
4242
createAuthWit(_messageHash: Fr): Promise<AuthWitness> {
4343
throw new Error('Method not implemented.');
4444
}
45+
46+
rotateNullifierKeys(_newNskM: Fq): Promise<void> {
47+
throw new Error('Method not implemented.');
48+
}
4549
}

yarn-project/circuit-types/src/interfaces/pxe.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,6 @@ export interface PXE {
6161
*/
6262
registerAccount(secretKey: Fr, partialAddress: PartialAddress): Promise<CompleteAddress>;
6363

64-
rotateMasterNullifierKey(account: AztecAddress, secretKey: Fq): Promise<void>;
65-
6664
/**
6765
* Registers a recipient in PXE. This is required when sending encrypted notes to
6866
* a user who hasn't deployed their account contract yet. Since their account is not deployed, their
@@ -107,6 +105,18 @@ export interface PXE {
107105
*/
108106
getRecipient(address: AztecAddress): Promise<CompleteAddress | undefined>;
109107

108+
/**
109+
* Rotates master nullifier keys.
110+
* @param address - The address of the account we want to rotate our key for.
111+
* @param newNskM - The new master nullifier secret key we want to use.
112+
* @remarks - One should not use this function directly without also calling the canonical key registry to rotate
113+
* the new master nullifier secret key's derived master nullifier public key.
114+
* Therefore, it is preferred to use rotateNullifierKeys on AccountWallet, as that handles the call to the Key Registry as well.
115+
*
116+
* 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.
117+
*/
118+
rotateNskM(address: AztecAddress, newNskM: Fq): Promise<void>;
119+
110120
/**
111121
* Registers a contract class in the PXE without registering any associated contract instance with it.
112122
*

0 commit comments

Comments
 (0)