Skip to content

Commit

Permalink
feat!: nullifier key (AztecProtocol#4166)
Browse files Browse the repository at this point in the history
- Rename oracle call `get_secret_key` to `get_nullifier_secret_key`.
- Add new method `request_nullifier_secret_key(account: Address)` to
`PrivateContext`, which returns a nullifier secret key for an account,
and also pushes a request for the kernel circuit to verify the keys.
- Changes to `NoteInterface`:
- Change `compute_nullifier()` to `compute_nullifier(private_context:
PrivateContext)`. If a secret is used in creating a nullifier, it must
be fetched via
`private_context.request_nullifier_secret_key(account_address)`.
- Add new method `compute_nullifier_without_context()`. This will be
used in unconstrained functions where the private context is not
available, and using an unverified nullifier keys won't affect other
users. For example, it's used in `compute_note_hash_and_nullifier` to
compute values for the user's own notes.
- Add new apis to `KeyStore`:
  - `getNullifierSecretKey(pubKey: PublicKey)`
  - `getNullifierPublicKey(pubKey: PublicKey)`
- `getSiloedNullifierSecretKey(pubKey: PublicKey, contractAddress:
AztecAddress)`
  • Loading branch information
LeilaWang authored Jan 22, 2024
1 parent 5006228 commit 7c07665
Show file tree
Hide file tree
Showing 55 changed files with 561 additions and 769 deletions.
6 changes: 0 additions & 6 deletions boxes/token/src/contracts/src/types/balance_set.nr
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@ use dep::aztec::note::{
note_interface::NoteInterface,
utils::compute_note_hash_for_read_or_nullify,
};
use dep::aztec::oracle::{
rand::rand,
get_secret_key::get_secret_key,
get_public_key::get_public_key,
};

use crate::types::token_note::{TokenNote, TOKEN_NOTE_LEN, TokenNoteMethods};

// A set implementing standard manipulation of balances.
Expand Down
26 changes: 21 additions & 5 deletions boxes/token/src/contracts/src/types/token_note.nr
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use dep::aztec::{
};
use dep::aztec::oracle::{
rand::rand,
get_secret_key::get_secret_key,
nullifier_key::get_nullifier_secret_key,
get_public_key::get_public_key,
};
use dep::safe_math::SafeU120;
Expand Down Expand Up @@ -72,9 +72,9 @@ impl TokenNote {
}

// docs:start:nullifier
pub fn compute_nullifier(self) -> Field {
pub fn compute_nullifier(self, context: &mut PrivateContext) -> Field {
let note_hash_for_nullify = compute_note_hash_for_read_or_nullify(TokenNoteMethods, self);
let secret = get_secret_key(self.owner);
let secret = context.request_nullifier_secret_key(self.owner);
// TODO(#1205) Should use a non-zero generator index.
pedersen_hash([
note_hash_for_nullify,
Expand All @@ -84,6 +84,17 @@ impl TokenNote {
}
// docs:end:nullifier

pub fn compute_nullifier_without_context(self) -> Field {
let note_hash_for_nullify = compute_note_hash_for_read_or_nullify(TokenNoteMethods, self);
let secret = get_nullifier_secret_key(self.owner);
// TODO(#1205) Should use a non-zero generator index.
pedersen_hash([
note_hash_for_nullify,
secret.low,
secret.high,
],0)
}

pub fn set_header(&mut self, header: NoteHeader) {
self.header = header;
}
Expand Down Expand Up @@ -116,8 +127,12 @@ fn compute_note_hash(note: TokenNote) -> Field {
note.compute_note_hash()
}

fn compute_nullifier(note: TokenNote) -> Field {
note.compute_nullifier()
fn compute_nullifier(note: TokenNote, context: &mut PrivateContext) -> Field {
note.compute_nullifier(context)
}

fn compute_nullifier_without_context(note: TokenNote) -> Field {
note.compute_nullifier_without_context()
}

fn get_header(note: TokenNote) -> NoteHeader {
Expand All @@ -138,6 +153,7 @@ global TokenNoteMethods = NoteInterface {
serialize,
compute_note_hash,
compute_nullifier,
compute_nullifier_without_context,
get_header,
set_header,
broadcast,
Expand Down
15 changes: 12 additions & 3 deletions boxes/token/src/contracts/src/types/transparent_note.nr
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ impl TransparentNote {
],0)
}

pub fn compute_nullifier(self) -> Field {
pub fn compute_nullifier(self, _context: &mut PrivateContext) -> Field {
self.compute_nullifier_without_context()
}

pub fn compute_nullifier_without_context(self) -> Field {
// TODO(#1386): should use `compute_note_hash_for_read_or_nullify` once public functions inject nonce!
let siloed_note_hash = compute_siloed_note_hash(TransparentNoteMethods, self);
// TODO(#1205) Should use a non-zero generator index.
Expand Down Expand Up @@ -102,8 +106,12 @@ fn compute_note_hash(note: TransparentNote) -> Field {
note.compute_note_hash()
}

fn compute_nullifier(note: TransparentNote) -> Field {
note.compute_nullifier()
fn compute_nullifier(note: TransparentNote, context: &mut PrivateContext) -> Field {
note.compute_nullifier(context)
}

fn compute_nullifier_without_context(note: TransparentNote) -> Field {
note.compute_nullifier_without_context()
}

fn get_header(note: TransparentNote) -> NoteHeader {
Expand All @@ -123,6 +131,7 @@ global TransparentNoteMethods = NoteInterface {
serialize,
compute_note_hash,
compute_nullifier,
compute_nullifier_without_context,
get_header,
set_header,
broadcast,
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/concepts/foundation/accounts/keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Note that any accounts you own that have been added to the PXE are automatically

In addition to deriving encryption keys, the privacy master key is used for deriving nullifier secrets. Whenever a private note is consumed, a nullifier deterministically derived from it is emitted. This mechanisms prevents double-spends, since nullifiers are checked by the protocol to be unique. Now, in order to preserve privacy, a third party should not be able to link a note commitment to its nullifier - this link is enforced by the note implementation. Therefore, calculating the nullifier for a note requires a secret from its owner.

An application in Aztec.nr can request a secret from the current user for computing the nullifier of a note via the `get_secret_key` oracle call:
An application in Aztec.nr can request a secret from the current user for computing the nullifier of a note via the `request_nullifier_secret_key` api:

#include_code nullifier /yarn-project/aztec-nr/value-note/src/value_note.nr rust

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Inside this, paste these imports:
We are using various utils within the Aztec library:

- `context` - exposes things such as the contract address, msg_sender, etc
- `oracle::get_secret_key` - get your secret key to help us create a randomized nullifier
- `context.request_nullifier_secret_key` - get your secret key to help us create a randomized nullifier
- `FunctionSelector::from_signature` - compute a function selector from signature so we can call functions from other functions
- `state_vars::{ map::Map, public_state::PublicState, }` - we will use a Map to store the votes (key = voteId, value = number of votes), and PublicState to hold our public values that we mentioned earlier
- `types::type_serialization::{..}` - various serialization methods for defining how to use these types
Expand Down
12 changes: 8 additions & 4 deletions yarn-project/acir-simulator/src/acvm/oracle/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ export class Oracle {
return toACVMField(packed);
}

async getSecretKey([publicKeyX]: ACVMField[], [publicKeyY]: ACVMField[]): Promise<ACVMField[]> {
const publicKey = new Point(fromACVMField(publicKeyX), fromACVMField(publicKeyY));
const secretKey = await this.typedOracle.getSecretKey(publicKey);
return [toACVMField(secretKey.low), toACVMField(secretKey.high)];
async getNullifierKeyPair([accountAddress]: ACVMField[]): Promise<ACVMField[]> {
const { publicKey, secretKey } = await this.typedOracle.getNullifierKeyPair(fromACVMField(accountAddress));
return [
toACVMField(publicKey.x),
toACVMField(publicKey.y),
toACVMField(secretKey.high),
toACVMField(secretKey.low),
];
}

async getPublicKeyAndPartialAddress([address]: ACVMField[]) {
Expand Down
20 changes: 17 additions & 3 deletions yarn-project/acir-simulator/src/acvm/oracle/typed_oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,25 @@ import {
PublicKey,
UnencryptedL2Log,
} from '@aztec/circuit-types';
import { BlockHeader, PrivateCallStackItem, PublicCallRequest } from '@aztec/circuits.js';
import { BlockHeader, GrumpkinPrivateKey, PrivateCallStackItem, PublicCallRequest } from '@aztec/circuits.js';
import { FunctionSelector } from '@aztec/foundation/abi';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { EthAddress } from '@aztec/foundation/eth-address';
import { Fr, GrumpkinScalar } from '@aztec/foundation/fields';
import { Fr } from '@aztec/foundation/fields';

/**
* A pair of public key and secret key.
*/
export interface KeyPair {
/**
* Public key.
*/
publicKey: PublicKey;
/**
* Secret Key.
*/
secretKey: GrumpkinPrivateKey;
}

/**
* Information about a note needed during execution.
Expand Down Expand Up @@ -76,7 +90,7 @@ export abstract class TypedOracle {
throw new Error('Not available.');
}

getSecretKey(_owner: PublicKey): Promise<GrumpkinScalar> {
getNullifierKeyPair(_accountAddress: AztecAddress): Promise<KeyPair> {
throw new Error('Not available.');
}

Expand Down
18 changes: 9 additions & 9 deletions yarn-project/acir-simulator/src/client/db_oracle.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { L2Block, MerkleTreeId, NullifierMembershipWitness, PublicDataWitness } from '@aztec/circuit-types';
import { BlockHeader, CompleteAddress, GrumpkinPrivateKey, PublicKey } from '@aztec/circuits.js';
import { BlockHeader, CompleteAddress } from '@aztec/circuits.js';
import { FunctionArtifactWithDebugMetadata, FunctionSelector } from '@aztec/foundation/abi';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { EthAddress } from '@aztec/foundation/eth-address';
import { Fr } from '@aztec/foundation/fields';

import { NoteData } from '../acvm/index.js';
import { KeyPair, NoteData } from '../acvm/index.js';
import { CommitmentsDB } from '../public/db.js';

/**
Expand Down Expand Up @@ -43,16 +43,16 @@ export interface DBOracle extends CommitmentsDB {
popCapsule(): Promise<Fr[]>;

/**
* Retrieve the secret key associated with a specific public key.
* Retrieve the nullifier key pair associated with a specific account.
* The function only allows access to the secret keys of the transaction creator,
* and throws an error if the address does not match the public key address of the key pair.
* and throws an error if the address does not match the account address of the key pair.
*
* @param contractAddress - The contract address. Ignored here. But we might want to return different keys for different contracts.
* @param pubKey - The public key of an account.
* @returns A Promise that resolves to the secret key.
* @throws An Error if the input address does not match the public key address of the key pair.
* @param accountAddress - The account address.
* @param contractAddress - The contract address.
* @returns A Promise that resolves to the nullifier key pair.
* @throws An Error if the input address does not match the account address of the key pair.
*/
getSecretKey(contractAddress: AztecAddress, pubKey: PublicKey): Promise<GrumpkinPrivateKey>;
getNullifierKeyPair(accountAddress: AztecAddress, contractAddress: AztecAddress): Promise<KeyPair>;

/**
* Retrieves a set of notes stored in the database for a given contract address and storage slot.
Expand Down
57 changes: 48 additions & 9 deletions yarn-project/acir-simulator/src/client/private_execution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {
MAX_NEW_COMMITMENTS_PER_CALL,
NOTE_HASH_TREE_HEIGHT,
PublicCallRequest,
PublicKey,
TxContext,
computeNullifierSecretKey,
computeSiloedNullifierSecretKey,
derivePublicKey,
nonEmptySideEffects,
sideEffectArrayToValueArray,
} from '@aztec/circuits.js';
Expand Down Expand Up @@ -52,6 +54,7 @@ import { default as levelup } from 'levelup';
import { type MemDown, default as memdown } from 'memdown';
import { getFunctionSelector } from 'viem';

import { KeyPair } from '../acvm/index.js';
import { buildL1ToL2Message } from '../test/utils.js';
import { computeSlotForMapping } from '../utils.js';
import { DBOracle } from './db_oracle.js';
Expand All @@ -75,6 +78,8 @@ describe('Private Execution test suite', () => {
let recipient: AztecAddress;
let ownerCompleteAddress: CompleteAddress;
let recipientCompleteAddress: CompleteAddress;
let ownerNullifierKeyPair: KeyPair;
let recipientNullifierKeyPair: KeyPair;

const treeHeights: { [name: string]: number } = {
noteHash: NOTE_HASH_TREE_HEIGHT,
Expand Down Expand Up @@ -157,18 +162,36 @@ describe('Private Execution test suite', () => {

owner = ownerCompleteAddress.address;
recipient = recipientCompleteAddress.address;

const ownerNullifierSecretKey = computeNullifierSecretKey(ownerPk);
ownerNullifierKeyPair = {
secretKey: ownerNullifierSecretKey,
publicKey: derivePublicKey(ownerNullifierSecretKey),
};

const recipientNullifierSecretKey = computeNullifierSecretKey(recipientPk);
recipientNullifierKeyPair = {
secretKey: recipientNullifierSecretKey,
publicKey: derivePublicKey(recipientNullifierSecretKey),
};
});

beforeEach(() => {
oracle = mock<DBOracle>();
oracle.getSecretKey.mockImplementation((contractAddress: AztecAddress, pubKey: PublicKey) => {
if (pubKey.equals(ownerCompleteAddress.publicKey)) {
return Promise.resolve(ownerPk);
oracle.getNullifierKeyPair.mockImplementation((accountAddress: AztecAddress, contractAddress: AztecAddress) => {
if (accountAddress.equals(ownerCompleteAddress.address)) {
return Promise.resolve({
publicKey: ownerNullifierKeyPair.publicKey,
secretKey: computeSiloedNullifierSecretKey(ownerNullifierKeyPair.secretKey, contractAddress),
});
}
if (pubKey.equals(recipientCompleteAddress.publicKey)) {
return Promise.resolve(recipientPk);
if (accountAddress.equals(recipientCompleteAddress.address)) {
return Promise.resolve({
publicKey: recipientNullifierKeyPair.publicKey,
secretKey: computeSiloedNullifierSecretKey(recipientNullifierKeyPair.secretKey, contractAddress),
});
}
throw new Error(`Unknown address ${pubKey}`);
throw new Error(`Unknown address ${accountAddress}`);
});
oracle.getBlockHeader.mockResolvedValue(blockHeader);

Expand Down Expand Up @@ -659,7 +682,15 @@ describe('Private Execution test suite', () => {
expect(gotNoteValue).toEqual(amountToTransfer);

const nullifier = result.callStackItem.publicInputs.newNullifiers[0];
const expectedNullifier = hashFields([innerNoteHash, ownerPk.low, ownerPk.high]);
const siloedNullifierSecretKey = computeSiloedNullifierSecretKey(
ownerNullifierKeyPair.secretKey,
contractAddress,
);
const expectedNullifier = hashFields([
innerNoteHash,
siloedNullifierSecretKey.low,
siloedNullifierSecretKey.high,
]);
expect(nullifier.value).toEqual(expectedNullifier);
});

Expand Down Expand Up @@ -732,7 +763,15 @@ describe('Private Execution test suite', () => {
expect(gotNoteValue).toEqual(amountToTransfer);

const nullifier = execGetThenNullify.callStackItem.publicInputs.newNullifiers[0];
const expectedNullifier = hashFields([innerNoteHash, ownerPk.low, ownerPk.high]);
const siloedNullifierSecretKey = computeSiloedNullifierSecretKey(
ownerNullifierKeyPair.secretKey,
contractAddress,
);
const expectedNullifier = hashFields([
innerNoteHash,
siloedNullifierSecretKey.low,
siloedNullifierSecretKey.high,
]);
expect(nullifier.value).toEqual(expectedNullifier);

// check that the last get_notes call return no note
Expand Down
24 changes: 14 additions & 10 deletions yarn-project/acir-simulator/src/client/simulator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { computeUniqueCommitment, siloCommitment } from '@aztec/circuits.js/abis
import { ABIParameterVisibility, FunctionArtifactWithDebugMetadata, getFunctionArtifact } from '@aztec/foundation/abi';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { pedersenHash } from '@aztec/foundation/crypto';
import { Fr, GrumpkinScalar } from '@aztec/foundation/fields';
import { Fr, GrumpkinScalar, Point } from '@aztec/foundation/fields';
import { TokenContractArtifact } from '@aztec/noir-contracts/Token';

import { MockProxy, mock } from 'jest-mock-extended';
Expand All @@ -15,20 +15,20 @@ import { AcirSimulator } from './simulator.js';
describe('Simulator', () => {
let oracle: MockProxy<DBOracle>;
let simulator: AcirSimulator;
let ownerCompleteAddress: CompleteAddress;
let owner: AztecAddress;
const ownerPk = GrumpkinScalar.fromString('2dcc5485a58316776299be08c78fa3788a1a7961ae30dc747fb1be17692a8d32');
const ownerCompleteAddress = CompleteAddress.fromPrivateKeyAndPartialAddress(ownerPk, Fr.random());
const owner = ownerCompleteAddress.address;
const ownerNullifierSecretKey = GrumpkinScalar.random();
const ownerNullifierPublicKey = Point.random();

const hashFields = (data: Fr[]) => Fr.fromBuffer(pedersenHash(data.map(f => f.toBuffer())));

beforeAll(() => {
ownerCompleteAddress = CompleteAddress.fromPrivateKeyAndPartialAddress(ownerPk, Fr.random());
owner = ownerCompleteAddress.address;
});

beforeEach(() => {
oracle = mock<DBOracle>();
oracle.getSecretKey.mockResolvedValue(ownerPk);
oracle.getNullifierKeyPair.mockResolvedValue({
secretKey: ownerNullifierSecretKey,
publicKey: ownerNullifierPublicKey,
});
oracle.getCompleteAddress.mockResolvedValue(ownerCompleteAddress);

simulator = new AcirSimulator(oracle);
Expand All @@ -50,7 +50,11 @@ describe('Simulator', () => {
const innerNoteHash = hashFields([storageSlot, valueNoteHash]);
const siloedNoteHash = siloCommitment(contractAddress, innerNoteHash);
const uniqueSiloedNoteHash = computeUniqueCommitment(nonce, siloedNoteHash);
const innerNullifier = hashFields([uniqueSiloedNoteHash, ownerPk.low, ownerPk.high]);
const innerNullifier = hashFields([
uniqueSiloedNoteHash,
ownerNullifierSecretKey.low,
ownerNullifierSecretKey.high,
]);

const result = await simulator.computeNoteHashAndNullifier(contractAddress, nonce, storageSlot, note);

Expand Down
4 changes: 2 additions & 2 deletions yarn-project/acir-simulator/src/client/simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,15 @@ export class AcirSimulator {
const extendedNoteItems = note.items.concat(Array(maxNoteFields - note.items.length).fill(Fr.ZERO));

const execRequest: FunctionCall = {
to: AztecAddress.ZERO,
to: contractAddress,
functionData: FunctionData.empty(),
args: encodeArguments(artifact, [contractAddress, nonce, storageSlot, extendedNoteItems]),
};

const [innerNoteHash, siloedNoteHash, uniqueSiloedNoteHash, innerNullifier] = (await this.runUnconstrained(
execRequest,
artifact,
AztecAddress.ZERO,
contractAddress,
)) as bigint[];

return {
Expand Down
Loading

0 comments on commit 7c07665

Please sign in to comment.