Skip to content

Commit

Permalink
feat(docs): Key rotation / owner -> nullifier key docs (#6538)
Browse files Browse the repository at this point in the history
  • Loading branch information
catmcgee authored May 20, 2024
1 parent c3b573e commit 2453ba8
Show file tree
Hide file tree
Showing 12 changed files with 110 additions and 18 deletions.
4 changes: 2 additions & 2 deletions docs/docs/getting_started/aztecnr-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Map is a private state variable that functions like a dictionary, relating Field

`value_note`

Notes are fundamental to how Aztec manages privacy. A note is a privacy-preserving representation of an amount of tokens associated with an address, while encrypting the amount and owner. In this contract, we are using the `value_note` library. This is a type of note interface for storing a single Field, eg a balance - or, in our case, a counter.
Notes are fundamental to how Aztec manages privacy. A note is a privacy-preserving representation of an amount of tokens associated with a nullifier key (that can be owned by an owner), while encrypting the amount. In this contract, we are using the `value_note` library. This is a type of note interface for storing a single Field, eg a balance - or, in our case, a counter.

We are also using `balance_utils` from this import, a useful library that allows us to utilize value notes as if they are simple balances.

Expand Down Expand Up @@ -128,7 +128,7 @@ The `increment` function works very similarly to the `constructor`, but instead

## Prevent double spending

Because our counters are private, the network can't directly verify if a note was spent or not, which could lead to double-spending. To solve this, we use a nullifier - a unique identifier generated from each spent note and its owner. Although this isn't really an issue in this simple smart contract, Aztec injects a special function called `compute_note_hash_and_nullifier` to determine these values for any given note produced by this contract.
Because our counters are private, the network can't directly verify if a note was spent or not, which could lead to double-spending. To solve this, we use a nullifier - a unique identifier generated from each spent note and its nullifier key. Although this isn't really an issue in this simple smart contract, Aztec injects a special function called `compute_note_hash_and_nullifier` to determine these values for any given note produced by this contract.

## Getting a counter

Expand Down
41 changes: 41 additions & 0 deletions docs/docs/guides/js_apps/rotate_keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: How to Rotate Nullifier Keys
---

This guide explains how to rotate nullifer secret and public keys using Aztec.js. To learn more about key rotation, read the [concepts section](../../aztec/concepts/accounts/keys.md#key-rotation).

## Prerequisites

You should have a wallet whose keys you want to rotate. You can learn how to create wallets from [this guide](./create_account.md).

You should also have a PXE initialized.

## Relevant imports

You will need to import these from Aztec.js:

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

## Create nullifier secret and public key

`newNskM` = new master nullifier secret key

`newNpkM` = new master nullifier public key (type `PublicKey`)

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

## Rotate nullifier secret and public key

Call `rotateMasterNullifierKey` on the PXE to rotate the secret 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
2 changes: 1 addition & 1 deletion docs/docs/guides/js_apps/test_contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ Private state in the Aztec Network is represented via sets of [private notes](/a

#include_code value-note-def noir-projects/aztec-nr/value-note/src/value_note.nr rust

We can query the Private eXecution Environment (PXE) for all notes encrypted for a given user in a contract slot. For this example, we'll get all notes encrypted for the `owner` user that are stored on the token contract address and on the slot we calculated earlier. To calculate the actual balance, we extract the `value` of each note, which is the first element, and sum them up.
We can query the Private eXecution Environment (PXE) for all notes encrypted to a given owner in a contract slot. For this example, we'll get all notes encrypted for the `owner` user that are stored on the token contract address and on the slot we calculated earlier. To calculate the actual balance, we extract the `value` of each note, which is the first element, and sum them up.

#include_code private-storage /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
title: Key Rotation
---

## Prerequisite reading

- [Keys](../../../../aztec/concepts/accounts/keys.md)

## Introduction

It is possible for users to rotate their keys, which can be helpful if some of their keys are leaked.

Because of this, notes are associated with their `nullifier key` rather than any sort of 'owner' address.

It is still possible to nullify the notes with the old nullifier key even after the key rotation.

## Things to consider

- 'Owner' is arbitrary - as long as you know the nullifier secret, you can nullify a note
- Consider how key rotation can affect account contracts, eg you can add additional security checks for who or how the key rotation is called

## Glossary

- `npk_m_hash`: master nullifying public key hash
- `nsk_app`: app nullifying secret key - the app-specific NSK (learn more about app-scoped keys [here](../../../../aztec/concepts/accounts/keys.md#scoped-keys))
- `nsk_hash`: nullifying secret key hash
- `ivpk_m`: incoming view public key (master) (learn more about IVPKs [here](../../../../aztec/concepts/accounts/keys.md#keys))
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Encrypted events can only be emitted by private functions and are encrypted usin
For this reason it is necessary to register a recipient in the Private Execution Environment (PXE) before encrypting the events for them.

First we need to get a hold of recipient's [complete address](#complete-address).
Bellow are some ways how we could instantiate it after getting the information in a string form from a recipient:
Below are some ways how we could instantiate it after getting the information in a string form from a recipient:

#include_code instantiate-complete-address /yarn-project/circuits.js/src/structs/complete_address.test.ts rust

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The history library allows you to prove any of the following at a given block he
Using this library, you can check that specific notes or nullifiers were part of Aztec network state at specific blocks. This can be useful for things such as:

- Verifying a minimum timestamp from a private context
- Checking eligibility based on historical events (e.g. for an airdrop by proving that you owned a note)
- Checking eligibility based on historical events (e.g. for an airdrop by proving that you knew the nullifier key for a note)
- Verifying historic ownership / relinquishing of assets
- Proving existence of a value in public data tree at a given contract slot
- Proving that a contract was deployed in a given block with some parameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ As a convenience, the outer [note/utils.nr](https://github.com/AztecProtocol/azt
Serialization/deserialization of content is used to convert between the Note's variables and a generic array of Field elements. The Field type is understood and used by lower level crypographic libraries.
This is analogous to the encoding/decoding between variables and bytes in solidity.

For example in ValueNote, the `serialize_content` function simply returns: the value, owner address (as a field) and the note randomness; as an array of Field elements.
For example in ValueNote, the `serialize_content` function simply returns: the value, nullifying public key hash (as a field) and the note randomness; as an array of Field elements.

### Value as a sum of Notes
We recall that multiple notes are associated with a "slot" (or ID), and so the value of a numerical note (like ValueNote) is the sum of each note's value.
Expand Down
12 changes: 12 additions & 0 deletions docs/docs/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,20 @@ struct TokenNote {
}
```

Creating a token note and adding it to storage now looks like this:

```diff
- let mut note = ValueNote::new(new_value, owner);
- storage.a_private_value.insert(&mut note, true);
+ let owner_npk_m_hash = get_npk_m_hash(&mut context, owner);
+ let owner_ivpk_m = get_ivpk_m(&mut context, owner);
+ let mut note = ValueNote::new(new_value, owner_npk_m_hash);
+ storage.a_private_value.insert(&mut note, true, owner_ivpk_m);
```

Computing the nullifier similarly changes to use this master nullifying public key hash.


## 0.40.0

### [Aztec.nr] Debug logging
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,9 @@ The first value of `.select` and `.sort` is the index of a field in a note type.

#include_code state_vars-CardNote /noir-projects/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr rust

The indices are: 0 for `points`, 1 for `secret`, and 2 for `owner`.
The indices are: 0 for `points`, 1 for `randomness`, and 2 for `npk_m_hash`.

In the example, `.select(2, account_address, Option::none())` matches the 2nd field of `CardNote`, which is `owner`, and returns the cards whose `owner` field equals `account_address`, equality is the comparator used because because including no comparator (the third argument) defaults to using the equality comparator. The current possible values of Comparator are specified in the Note Getter Options implementation linked above.
In the example, `.select(2, account_address, Option::none())` matches the 2nd field of `CardNote`, which is `npk_m_hash`, and returns the cards whose `npk_m_hash` field equals `account_npk_m_hash`, equality is the comparator used because because including no comparator (the third argument) defaults to using the equality comparator. The current possible values of Comparator are specified in the Note Getter Options implementation linked above.

`.sort(0, SortOrder.DESC)` sorts the 0th field of `CardNote`, which is `points`, in descending order.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Your file structure should look something like this:

The file `main.nr` will soon turn into our smart contract!

We will need the Aztec library to create this contract. In your `Nargo.toml` you should see `[dependencies]` - paste this bellow it.
We will need the Aztec library to create this contract. In your `Nargo.toml` you should see `[dependencies]` - paste this below it.

```toml
[dependencies]
Expand Down Expand Up @@ -126,6 +126,10 @@ The first thing we do here is assert that the vote has not ended.

The code after the assertion will only run if the assertion is true. In this snippet, we read the current vote tally at the voteId, add 1 to it, and write this new number to the voteId. The `Field` element allows us to use `+` to add to an integer.

:::danger
Note that due to [key rotation](../../aztec/concepts/accounts/keys.md#key-rotation), it would be possible for a user to rotate their nullifier secret key and be able to vote again. Refer to [common patterns](../../guides/smart_contracts/writing_contracts/common_patterns/key_rotation.md) for more information
:::

## Getting the number of votes

We will create a function that anyone can call that will return the number of votes at a given vote Id. Paste this in your contract:
Expand Down
10 changes: 5 additions & 5 deletions docs/docs/tutorials/contract_tutorials/token_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ For more detail on execution contexts, see [Contract Communication](/aztec/conce

We are also importing types from a `types.nr` file, which imports types from the `types` folder. You can view them [here](https://github.com/AztecProtocol/aztec-packages/tree/#include_aztec_version/noir-projects/noir-contracts/contracts/token_contract/src).

The main thing to note from this types folder is the `TransparentNote` definition. This defines how the contract moves value from the public domain into the private domain. It is similar to the `value_note` that we imported, but with some modifications namely, instead of a defined `owner`, it allows anyone that can produce the pre-image to the stored `secret_hash` to spend the note.
The main thing to note from this types folder is the `TransparentNote` definition. This defines how the contract moves value from the public domain into the private domain. It is similar to the `value_note` that we imported, but with some modifications namely, instead of a defined nullifier key, it allows anyone that can produce the pre-image to the stored `secret_hash` to spend the note.

### Note on private state

Expand All @@ -233,7 +233,7 @@ Reading through the storage variables:

- `admin` an Aztec address stored in public state.
- `minters` is a mapping of Aztec addresses in public state. This will store whether an account is an approved minter on the contract.
- `balances` is a mapping of private balances. Private balances are stored in a `PrivateSet` of `ValueNote`s. The balance is the sum of all of an account's `ValueNote`s.
- `balances` is a mapping of private balances. Private balances are stored in a `PrivateSet` of `TokenNote`s. The balance is the sum of all of an account's `TokenNote`s.
- `total_supply` is an unsigned integer (max 128 bit value) stored in public state and represents the total number of tokens minted.
- `pending_shields` is a `PrivateSet` of `TransparentNote`s stored in private state. What is stored publicly is a set of commitments to `TransparentNote`s.
- `public_balances` is a mapping of Aztec addresses in public state and represents the publicly viewable balances of accounts.
Expand Down Expand Up @@ -337,7 +337,7 @@ Storage is referenced as `storage.variable`.

#### `redeem_shield`

This private function enables an account to move tokens from a `TransparentNote` in the `pending_shields` mapping to any Aztec account as a `ValueNote` in private `balances`.
This private function enables an account to move tokens from a `TransparentNote` in the `pending_shields` mapping to a `TokenNote` in private `balances`. The `TokenNote` will be associated with a nullifier key, so any account that knows this key can spend this note.

Going through the function logic, first the `secret_hash` is generated from the given secret. This ensures that only the entity possessing the secret can use it to redeem the note. Following this, a `TransparentNote` is retrieved from the set, using the provided amount and secret. The note is subsequently removed from the set, allowing it to be redeemed only once. The recipient's private balance is then increased using the `increment` helper function from the `value_note` [library](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/value-note/src/utils.nr).

Expand All @@ -347,7 +347,7 @@ The function returns `1` to indicate successful execution.

#### `unshield`

This private function enables un-shielding of private `ValueNote`s stored in `balances` to any Aztec account's `public_balance`.
This private function enables un-shielding of private `TokenNote`s stored in `balances` to any Aztec account's `public_balance`.

After initializing storage, the function checks that the `msg_sender` is authorized to spend tokens. See [the Authorizing token spends section](#authorizing-token-spends) above for more detail--the only difference being that `assert_valid_message_for` is modified to work specifically in the private context. After the authorization check, the sender's private balance is decreased using the `decrement` helper function for the `value_note` library. Then it stages a public function call on this contract ([`_increase_public_balance`](#_increase_public_balance)) to be executed in the [public execution phase](#execution-contexts) of transaction execution. `_increase_public_balance` is marked as an `internal` function, so can only be called by this token contract.

Expand Down Expand Up @@ -411,7 +411,7 @@ A getter function for checking the token `total_supply`.

#### `balance_of_private`

A getter function for checking the private balance of the provided Aztec account. Note that the [Private Execution Environment (PXE)](https://github.com/AztecProtocol/aztec-packages/tree/#include_aztec_version/yarn-project/pxe) must have access to the `owner`s decryption keys in order to decrypt their notes.
A getter function for checking the private balance of the provided Aztec account. Note that the [Private Execution Environment (PXE)](https://github.com/AztecProtocol/aztec-packages/tree/#include_aztec_version/yarn-project/pxe) must have `ivsk_app` ([app-specific secret key](../../aztec/concepts/accounts/keys.md#scoped-keys)) in order to decrypt the notes.

#include_code balance_of_private /noir-projects/noir-contracts/contracts/token_contract/src/main.nr rust

Expand Down
16 changes: 12 additions & 4 deletions yarn-project/end-to-end/src/e2e_key_rotation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import {
computeSecretHash,
retryUntil,
} from '@aztec/aztec.js';
// docs:start:imports
import { type PublicKey, derivePublicKeyFromSecretKey } from '@aztec/circuits.js';
import { KeyRegistryContract, TestContract, TokenContract } from '@aztec/noir-contracts.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';

import { jest } from '@jest/globals';
Expand Down Expand Up @@ -56,10 +59,10 @@ 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 @@ -174,11 +177,16 @@ describe('e2e_key_rotation', () => {
// 3. Rotates B key
let newNpkM: PublicKey;
{
// docs:start:create_keys
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
await crossDelay();
}

Expand Down

0 comments on commit 2453ba8

Please sign in to comment.