Skip to content

Commit

Permalink
feat: implement partial token notes
Browse files Browse the repository at this point in the history
  • Loading branch information
alexghr committed Mar 4, 2024
1 parent 14e4824 commit 328097b
Show file tree
Hide file tree
Showing 13 changed files with 393 additions and 18 deletions.
13 changes: 13 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,17 @@ jobs:
command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_fees.test.ts
aztec_manifest_key: end-to-end

e2e-partial-token-notes:
docker:
- image: aztecprotocol/alpine-build-image
resource_class: small
steps:
- *checkout
- *setup_env
- run:
name: "Test"
command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_partial_token_notes.test.ts
aztec_manifest_key: end-to-end
e2e-dapp-subscription:
docker:
- image: aztecprotocol/alpine-build-image
Expand Down Expand Up @@ -1508,6 +1519,7 @@ workflows:
- e2e-avm-simulator: *e2e_test
- e2e-fees: *e2e_test
- e2e-dapp-subscription: *e2e_test
- e2e-partial-token-notes: *e2e_test
- pxe: *e2e_test
- cli-docs-sandbox: *e2e_test
- e2e-docs-examples: *e2e_test
Expand Down Expand Up @@ -1555,6 +1567,7 @@ workflows:
- e2e-avm-simulator
- e2e-fees
- e2e-dapp-subscription
- e2e-partial-token-notes
- pxe
- boxes-vanilla
- boxes-react
Expand Down
50 changes: 47 additions & 3 deletions noir-projects/aztec-nr/aztec/src/note/lifecycle.nr
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::context::{PrivateContext, PublicContext};
use crate::note::{
note_header::NoteHeader, note_interface::NoteInterface,
utils::{compute_note_hash_for_insertion, compute_note_hash_for_consumption}
utils::{compute_note_hash_for_insertion, compute_note_hash_for_consumption, compute_partial_note_hash}
};
use crate::oracle::notes::{notify_created_note, notify_nullified_note};

Expand All @@ -13,7 +13,7 @@ pub fn create_note<Note, N>(
) where Note: NoteInterface<N> {
let contract_address = (*context).this_address();

let header = NoteHeader { contract_address, storage_slot, nonce: 0, is_transient: true };
let header = NoteHeader { contract_address, storage_slot, nonce: 0, is_transient: true, partial_note_hash: 0 };
// TODO: change this to note.setHeader(header) once https://github.com/noir-lang/noir/issues/4095 is fixed
Note::set_header(note, header);
// As `is_transient` is true, this will compute the inner note hsah
Expand All @@ -38,14 +38,58 @@ pub fn create_note<Note, N>(
}
}

pub fn create_partial_note<Note, N>(
context: &mut PrivateContext,
storage_slot: Field,
note: &mut Note,
broadcast: bool
) -> Field where Note: NoteInterface<N> {
let contract_address = (*context).this_address();

let mut header = NoteHeader { contract_address, storage_slot, nonce: 0, is_transient: true, partial_note_hash: 0 };
// TODO: change this to note.setHeader(header) once https://github.com/noir-lang/noir/issues/4095 is fixed
Note::set_header(note, header);
// As `is_transient` is true, this will compute the inner note hsah
let partial_note_hash = compute_partial_note_hash(*note);
header.partial_note_hash = partial_note_hash;

// have to set the header again so that the partial_note_hash is updated
Note::set_header(note, header);

if broadcast {
Note::broadcast(*note, context, storage_slot);
}

partial_note_hash
}

pub fn complete_partial_note_from_public<Note, N>(
context: &mut PublicContext,
partial_note_hash: Field,
note: &mut Note
) where Note: NoteInterface<N> {
let contract_address = (*context).this_address();

let header = NoteHeader { contract_address, storage_slot: 0, nonce: 0, is_transient: true, partial_note_hash };
// TODO: change this to note.setHeader(header) once
Note::set_header(note, header);
// As `is_transient` is true, this will compute the inner note hsah
let note_hash = compute_note_hash_for_insertion(*note);

// No need to broadcast.
// Whoever created the partial note "should" already have the missing info ncessary to complete the note in their PXE.
// Also broadcsating would require the note's storage slot.
context.push_new_note_hash(note_hash);
}

pub fn create_note_hash_from_public<Note, N>(
context: &mut PublicContext,
storage_slot: Field,
note: &mut Note
) where Note: NoteInterface<N> {
let contract_address = (*context).this_address();

let header = NoteHeader { contract_address, storage_slot, nonce: 0, is_transient: true };
let header = NoteHeader { contract_address, storage_slot, nonce: 0, is_transient: true, partial_note_hash: 0 };
// TODO: change this to note.setHeader(header) once https://github.com/noir-lang/noir/issues/4095 is fixed
Note::set_header(note, header);
let note_hash = compute_note_hash_for_insertion(*note);
Expand Down
11 changes: 9 additions & 2 deletions noir-projects/aztec-nr/aztec/src/note/note_header.nr
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,23 @@ struct NoteHeader {
// TODO(https://github.com/AztecProtocol/aztec-packages/issues/1386)
// Remove this and check the nonce to see whether a note is transient or not.
is_transient: bool,

// this will be non-zero if this note was partially built at a previous time
partial_note_hash: Field,
}

impl Empty for NoteHeader {
fn empty() -> Self {
NoteHeader { contract_address: AztecAddress::zero(), nonce: 0, storage_slot: 0, is_transient: false }
NoteHeader { contract_address: AztecAddress::zero(), nonce: 0, storage_slot: 0, is_transient: false, partial_note_hash: 0 }
}
}

impl NoteHeader {
pub fn new(contract_address: AztecAddress, nonce: Field, storage_slot: Field) -> Self {
NoteHeader { contract_address, nonce, storage_slot, is_transient: false }
NoteHeader { contract_address, nonce, storage_slot, is_transient: false, partial_note_hash: 0 }
}

pub fn is_partial(self) -> bool {
self.partial_note_hash != 0
}
}
9 changes: 7 additions & 2 deletions noir-projects/aztec-nr/aztec/src/note/utils.nr
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,20 @@ fn compute_unique_hash(nonce: Field, siloed_note_hash: Field) -> Field {
pedersen_hash(inputs, GENERATOR_INDEX__UNIQUE_NOTE_HASH)
}

fn compute_partial_note_hash<Note, N>(note: Note) -> Field where Note: NoteInterface<N> {
pub fn compute_partial_note_hash<Note, N>(note: Note) -> Field where Note: NoteInterface<N> {
let header = note.get_header();
let inner_content_hash = note.compute_inner_note_content_hash();
pedersen_hash([header.storage_slot, inner_content_hash], 0)
}

fn compute_non_siloed_note_hash<Note, N>(note: Note) -> Field where Note: NoteInterface<N> {
let header = note.get_header();
let partial_note_hash = compute_partial_note_hash(note);
let mut partial_note_hash = header.partial_note_hash;

if partial_note_hash == 0 {
partial_note_hash = compute_partial_note_hash(note);
}

let outer_content_hash = note.compute_outer_note_content_hash();

// TODO(#1205) Do we need a generator index here?
Expand Down
2 changes: 1 addition & 1 deletion noir-projects/aztec-nr/aztec/src/oracle/notes.nr
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ unconstrained pub fn get_notes<Note, N, M, S, NS>(
let read_offset: u64 = return_header_length + i * (N + extra_preimage_length);
let nonce = fields[read_offset];
let is_transient = fields[read_offset + 1] as bool;
let header = NoteHeader { contract_address, nonce, storage_slot, is_transient };
let header = NoteHeader { contract_address, nonce, storage_slot, is_transient, partial_note_hash: 0 };
let serialized_note = arr_copy_slice(fields, [0; N], read_offset + 2);
let mut note = Note::deserialize_content(serialized_note);
// TODO: change this to note.setHeader(header) once https://github.com/noir-lang/noir/issues/4095 is fixed
Expand Down
15 changes: 14 additions & 1 deletion noir-projects/aztec-nr/aztec/src/state_vars/private_set.nr
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use dep::protocol_types::{
};
use crate::context::{PrivateContext, PublicContext, Context};
use crate::note::{
lifecycle::{create_note, create_note_hash_from_public, destroy_note},
lifecycle::{create_note, create_partial_note, create_note_hash_from_public, destroy_note},
note_getter::{get_notes, view_notes}, note_getter_options::NoteGetterOptions,
note_header::NoteHeader, note_interface::NoteInterface, note_viewer_options::NoteViewerOptions,
utils::compute_note_hash_for_consumption
Expand Down Expand Up @@ -39,6 +39,19 @@ impl<Note> PrivateSet<Note> {
}
// docs:end:insert

pub fn insert_partial<N>(
self,
note: &mut Note,
broadcast: bool
) -> Field where Note: NoteInterface<N> {
create_partial_note(
self.context.private.unwrap(),
self.storage_slot,
note,
broadcast
)
}

// docs:start:insert_from_public
pub fn insert_from_public<N>(self, note: &mut Note) where Note: NoteInterface<N> {
create_note_hash_from_public(self.context.public.unwrap(), self.storage_slot, note);
Expand Down
88 changes: 85 additions & 3 deletions noir-projects/noir-contracts/contracts/token_contract/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,23 @@ contract Token {
use dep::compressed_string::FieldCompressedString;

use dep::aztec::{
note::{note_getter_options::NoteGetterOptions, note_header::NoteHeader, utils as note_utils},
note::{
note_getter_options::NoteGetterOptions, note_header::NoteHeader, utils as note_utils,
lifecycle::{complete_partial_note_from_public}
},
hash::{compute_secret_hash}, state_vars::{Map, PublicMutable, SharedImmutable, PrivateSet},
protocol_types::{abis::function_selector::FunctionSelector, address::AztecAddress}
protocol_types::{abis::function_selector::FunctionSelector, address::AztecAddress},
log::{emit_unencrypted_log, emit_unencrypted_log_from_private}
};

// docs:start:import_authwit
use dep::authwit::{auth::{assert_current_call_valid_authwit, assert_current_call_valid_authwit_public}};
// docs:end:import_authwit

use crate::types::{transparent_note::TransparentNote, token_note::{TokenNote, TOKEN_NOTE_LEN}, balances_map::BalancesMap};
use crate::types::{
transparent_note::TransparentNote, token_note::{TokenNote, TOKEN_NOTE_LEN},
balances_map::BalancesMap, partial_note::{compute_partial_notes_pair_hash, PartialNotesHash}
};
// docs:end::imports

// docs:start:storage_struct
Expand All @@ -48,6 +55,7 @@ contract Token {
name: SharedImmutable<FieldCompressedString>,
// docs:start:storage_decimals
decimals: SharedImmutable<u8>,
partial_notes: Map<PartialNotesHash, PublicMutable<bool>>,
// docs:end:storage_decimals
}
// docs:end:storage_struct
Expand Down Expand Up @@ -380,5 +388,79 @@ contract Token {
storage.public_balances.at(owner).read().to_integer()
}
// docs:end:balance_of_public

#[aztec(private)]
fn split_to_partial_notes(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) -> [Field; 2] {
if (!from.eq(context.msg_sender())) {
assert_current_call_valid_authwit(&mut context, from);
} else {
assert(nonce == 0, "invalid nonce");
}

storage.balances.sub(from, U128::from_integer(amount));

let partial_note_0 = storage.balances.add_partial(from);
let partial_note_1 = storage.balances.add_partial(to);

let partial_note_hash_0 = partial_note_0.get_header().partial_note_hash;
let partial_note_hash_1 = partial_note_1.get_header().partial_note_hash;

let hash = compute_partial_notes_pair_hash(
partial_note_hash_0,
partial_note_hash_1,
context.msg_sender(),
amount
);

context.call_public_function(
context.this_address(),
FunctionSelector::from_signature("auth_partial_notes((Field))"),
[hash.to_field()]
);

// HACK: emit private data as unencrypted logs until PXE can track partial notes
// (the partial note is emitted as an encrypted log but the PXE drops it because its not in the tree)
emit_unencrypted_log_from_private(
&mut context,
[partial_note_0.randomness, partial_note_1.randomness]
);

emit_unencrypted_log_from_private(
&mut context,
[partial_note_0.get_header().storage_slot, partial_note_1.get_header().storage_slot]
);

// emit as unencrypted for e2e testing as return values are not accessible there
emit_unencrypted_log_from_private(&mut context, [partial_note_hash_0, partial_note_hash_1]);

[partial_note_hash_0, partial_note_hash_1]
}

#[aztec(public)]
internal fn auth_partial_notes(hash: PartialNotesHash) {
storage.partial_notes.at(hash).write(true);
}

#[aztec(public)]
fn complete_partial_notes(partial_note_hashes: [Field; 2], amounts: [Field; 2]) {
let hash = compute_partial_notes_pair_hash(
partial_note_hashes[0],
partial_note_hashes[1],
context.msg_sender(),
amounts[0] + amounts[1]
);

assert(storage.partial_notes.at(hash).read(), "Partial notes not authorized");
// only completable once
context.push_new_nullifier(hash.to_field(), 0);

let mut token_note_0 = TokenNote::new_from_public(U128::from_integer(amounts[0]));
complete_partial_note_from_public(&mut context, partial_note_hashes[0], &mut token_note_0);

let mut token_note_1 = TokenNote::new_from_public(U128::from_integer(amounts[1]));
complete_partial_note_from_public(&mut context, partial_note_hashes[1], &mut token_note_1);

emit_unencrypted_log(&mut context, amounts);
}
}
// docs:end:token_all
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod transparent_note;
mod balances_map;
mod token_note;
mod partial_note;
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ impl<T> BalancesMap<T> {
balance
}

pub fn add_partial<T_SERIALIZED_LEN>(self: Self, owner: AztecAddress) -> T where T: NoteInterface<T_SERIALIZED_LEN> + OwnedNote {
let mut addend_note = T::new(U128::empty(), owner);
let _partial_note_hash = self.map.at(owner).insert_partial(&mut addend_note, true);
// TODO once PXE tracks partial notes, just return the hash instead of the whole note
addend_note
}

pub fn add<T_SERIALIZED_LEN>(
self: Self,
owner: AztecAddress,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use dep::aztec::protocol_types::{hash::pedersen_hash, traits::{ToField, Serialize, Deserialize}, address::AztecAddress};

// helper struct to constrain a pair of partial notes to add up to a known amount
// it's a struct so that it implements ToField and can be used as a map key
struct PartialNotesHash {
inner: Field
}

impl ToField for PartialNotesHash {
fn to_field(self) -> Field {
self.inner
}
}

impl Serialize<1> for PartialNotesHash {
fn serialize(self) -> [Field; 1] {
[self.inner]
}
}

impl Deserialize<1> for PartialNotesHash {
fn deserialize(data: [Field; 1]) -> PartialNotesHash {
PartialNotesHash {
inner: data[0]
}
}
}

pub fn compute_partial_notes_pair_hash(
partial_note_1: Field,
partial_note_2: Field,
completer: AztecAddress,
amount: Field
) -> PartialNotesHash {
let inner = pedersen_hash(
[partial_note_1, partial_note_2, completer.to_field(), amount],
0
);

PartialNotesHash { inner }
}
Loading

0 comments on commit 328097b

Please sign in to comment.