diff --git a/build_manifest.yml b/build_manifest.yml index f955b467221..bdb1f1361e9 100644 --- a/build_manifest.yml +++ b/build_manifest.yml @@ -166,5 +166,6 @@ docs: - ^.*.ts$ - ^.release-please-manifest.json$ - ^.*/noir-version.json$ + - ^.*.nr$ dependencies: - - yarn-project + - yarn-project \ No newline at end of file diff --git a/docs/docs/dev_docs/contracts/syntax/storage.md b/docs/docs/dev_docs/contracts/syntax/storage.md index 63dca65f2ba..4bac605a736 100644 --- a/docs/docs/dev_docs/contracts/syntax/storage.md +++ b/docs/docs/dev_docs/contracts/syntax/storage.md @@ -71,7 +71,7 @@ This function behaves similarly for both private and public maps. An example cou Above, we are specifying that we want to get the storage in the Map `at` the `msg_sender()`, read the value stored and check that `msg_sender()` is indeed a minter. Doing a similar operation in Solidity code would look like: ```solidity -require(minters[msg.sender] == 1, "caller is not minter"); +require(minters[msg.sender], "caller is not minter"); ``` ## Public State Variables @@ -141,7 +141,7 @@ In this case, specifying that we are dealing with a map of Fields, and that it s This would be similar to the following in solidity: ```solidity -mapping(address => uint256) internal minters; +mapping(address => bool) internal minters; ``` ### `read` @@ -159,7 +159,7 @@ For our `admin` example from earlier, this could be used as follows to check tha #include_code read_admin /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr rust -#### Reading from out `minters` example +#### Reading from our `minters` example As we saw in the Map earlier, a very similar operation can be done to perform a lookup in a map. diff --git a/docs/docs/dev_docs/tutorials/writing_dapp/contract_deployment.md b/docs/docs/dev_docs/tutorials/writing_dapp/contract_deployment.md index 83ddeebce38..9155fd48be1 100644 --- a/docs/docs/dev_docs/tutorials/writing_dapp/contract_deployment.md +++ b/docs/docs/dev_docs/tutorials/writing_dapp/contract_deployment.md @@ -32,7 +32,7 @@ The `Token` contract also requires two helper files. Copy-them too: Create `contracts/token/types.nr` and copy-paste the following: -#include_code token_types_all yarn-project/noir-contracts/src/contracts/token_contract/src/types.nr rust +#include_code token_types_all yarn-project/noir-contracts/src/contracts/token_contract/src/types/transparent_note.nr rust Finally, create `contracts/token/util.nr` and copy-paste the following: diff --git a/yarn-project/aztec-nr/aztec/src/types/address.nr b/yarn-project/aztec-nr/aztec/src/types/address.nr index 6849767b8d0..79a8f1bd343 100644 --- a/yarn-project/aztec-nr/aztec/src/types/address.nr +++ b/yarn-project/aztec-nr/aztec/src/types/address.nr @@ -9,6 +9,10 @@ impl AztecAddress { } } + fn eq(self: Self, other: Self) -> bool { + self.address == other.address + } + fn serialize(self: Self) -> [Field; 1] { [self.address] } diff --git a/yarn-project/aztec-nr/aztec/src/types/type_serialization.nr b/yarn-project/aztec-nr/aztec/src/types/type_serialization.nr index b45dcd7c855..b1bb9651ba6 100644 --- a/yarn-project/aztec-nr/aztec/src/types/type_serialization.nr +++ b/yarn-project/aztec-nr/aztec/src/types/type_serialization.nr @@ -1,6 +1,7 @@ mod bool_serialization; mod field_serialization; mod u32_serialization; +mod aztec_address_serialization; /** * Before Noir supports traits, a way of specifying the serialization and deserialization methods for a type. diff --git a/yarn-project/aztec-nr/aztec/src/types/type_serialization/aztec_address_serialization.nr b/yarn-project/aztec-nr/aztec/src/types/type_serialization/aztec_address_serialization.nr new file mode 100644 index 00000000000..8ce68ec513f --- /dev/null +++ b/yarn-project/aztec-nr/aztec/src/types/type_serialization/aztec_address_serialization.nr @@ -0,0 +1,17 @@ +use crate::types::type_serialization::TypeSerializationInterface; +use crate::types::address::AztecAddress; + +global AZTEC_ADDRESS_SERIALIZED_LEN: Field = 1; + +fn deserialize(fields: [Field; AZTEC_ADDRESS_SERIALIZED_LEN]) -> AztecAddress { + AztecAddress::new(fields[0]) +} + +fn serialize(value: AztecAddress) -> [Field; AZTEC_ADDRESS_SERIALIZED_LEN] { + [value.address] +} + +global AztecAddressSerializationMethods = TypeSerializationInterface { + deserialize, + serialize, +}; \ No newline at end of file diff --git a/yarn-project/aztec-nr/safe-math/src/safe_u120.nr b/yarn-project/aztec-nr/safe-math/src/safe_u120.nr index 5feeb3cca23..a36c05ac3cd 100644 --- a/yarn-project/aztec-nr/safe-math/src/safe_u120.nr +++ b/yarn-project/aztec-nr/safe-math/src/safe_u120.nr @@ -41,6 +41,22 @@ impl SafeU120 { self.value == other.value } + fn lt(self: Self, other: Self) -> bool { + self.value < other.value + } + + fn le(self: Self, other: Self) -> bool { + self.value <= other.value + } + + fn gt(self: Self, other: Self) -> bool { + self.value > other.value + } + + fn ge(self: Self, other: Self) -> bool { + self.value >= other.value + } + fn sub( self: Self, b: Self, @@ -139,6 +155,44 @@ fn test_eq() { assert(a.eq(b)); } +#[test] +fn test_lt() { + let a = SafeU120::new(1); + let b = SafeU120::new(2); + assert(a.lt(b)); + assert(b.lt(a) == false); +} + + +#[test] +fn test_le() { + let a = SafeU120::new(2); + let b = SafeU120::new(2); + let c = SafeU120::new(5); + assert(a.le(b)); + assert(a.le(c)); + assert(c.le(a) == false); +} + +#[test] +fn test_gt() { + let a = SafeU120::new(1); + let b = SafeU120::new(2); + assert(b.gt(a)); + assert(a.gt(b) == false); +} + + +#[test] +fn test_ge() { + let a = SafeU120::new(2); + let b = SafeU120::new(2); + let c = SafeU120::new(5); + assert(a.ge(b)); + assert(a.ge(c) == false); + assert(c.ge(a)); +} + #[test(should_fail)] fn test_init_too_large() { let b = SafeU120::max().value as Field + 1; // max + 1 diff --git a/yarn-project/end-to-end/src/e2e_token_contract.test.ts b/yarn-project/end-to-end/src/e2e_token_contract.test.ts index 578c0a3e291..a910d8bd6e4 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract.test.ts @@ -18,7 +18,7 @@ const hashPayload = async (payload: Fr[]) => { ); }; -const TIMEOUT = 60_000; +const TIMEOUT = 90_000; describe('e2e_token_contract', () => { jest.setTimeout(TIMEOUT); diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr index f9871004b29..2cf48a52994 100644 --- a/yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr @@ -16,12 +16,6 @@ contract Token { use dep::safe_math::SafeU120; - use dep::value_note::{ - balance_utils, - utils::{increment, decrement}, - value_note::{VALUE_NOTE_LEN, ValueNoteMethods, ValueNote}, - }; - use dep::aztec::{ note::{ note_header::NoteHeader, @@ -29,35 +23,41 @@ contract Token { }, context::{PrivateContext, PublicContext, Context}, state_vars::{map::Map, public_state::PublicState, set::Set}, - types::type_serialization::field_serialization::{ - FieldSerializationMethods, FIELD_SERIALIZED_LEN, + types::type_serialization::{ + field_serialization::{FieldSerializationMethods, FIELD_SERIALIZED_LEN}, + bool_serialization::{BoolSerializationMethods, BOOL_SERIALIZED_LEN}, + aztec_address_serialization::{AztecAddressSerializationMethods, AZTEC_ADDRESS_SERIALIZED_LEN}, }, types::address::{AztecAddress}, oracle::compute_selector::compute_selector, auth::{assert_valid_message_for, assert_valid_public_message_for} }; - use crate::types::{TransparentNote, TransparentNoteMethods, TRANSPARENT_NOTE_LEN}; + use crate::types::{ + transparent_note::{TransparentNote, TransparentNoteMethods, TRANSPARENT_NOTE_LEN}, + token_note::{TokenNote, TokenNoteMethods, TOKEN_NOTE_LEN}, + balances_map::{BalancesMap}, + safe_u120_serialization::{SafeU120SerializationMethods, SAFE_U120_SERIALIZED_LEN} + }; use crate::util::{compute_message_hash}; // docs:end::imports // docs:start:storage_struct struct Storage { - // docs:start:storage_admin - admin: PublicState, + admin: PublicState, // docs:end:storage_admin // docs:start:storage_minters - minters: Map>, + minters: Map>, // docs:end:storage_minters // docs:start:storage_balances - balances: Map>, + balances: BalancesMap, // docs:end:storage_balances - total_supply: PublicState, + total_supply: PublicState, // docs:start:storage_pending_shields pending_shields: Set, // docs:end:storage_pending_shields - public_balances: Map>, + public_balances: Map>, } // docs:end:storage_struct @@ -69,7 +69,7 @@ contract Token { admin: PublicState::new( context, 1, - FieldSerializationMethods, + AztecAddressSerializationMethods, ), // docs:end:storage_admin_init // docs:start:storage_minters_init @@ -80,22 +80,16 @@ contract Token { PublicState::new( context, slot, - FieldSerializationMethods, + BoolSerializationMethods, ) }, ), // docs:end:storage_minters_init - balances: Map::new( - context, - 3, - |context, slot| { - Set::new(context, slot, ValueNoteMethods) - }, - ), + balances: BalancesMap::new(context, 3), total_supply: PublicState::new( context, 4, - FieldSerializationMethods, + SafeU120SerializationMethods, ), // docs:start:storage_pending_shields_init pending_shields: Set::new(context, 5, TransparentNoteMethods), @@ -107,7 +101,7 @@ contract Token { PublicState::new( context, slot, - FieldSerializationMethods, + SafeU120SerializationMethods, ) }, ), @@ -130,9 +124,9 @@ contract Token { fn set_admin( new_admin: AztecAddress, ) { - assert(storage.admin.read() == context.msg_sender(), "caller is not admin"); + assert(storage.admin.read().eq(AztecAddress::new(context.msg_sender())), "caller is not admin"); // docs:start:write_admin - storage.admin.write(new_admin.address); + storage.admin.write(new_admin); // docs:end:write_admin } // docs:end:set_admin @@ -144,10 +138,10 @@ contract Token { approve: bool, ) { // docs:start:read_admin - assert(storage.admin.read() == context.msg_sender(), "caller is not admin"); + assert(storage.admin.read().eq(AztecAddress::new(context.msg_sender())), "caller is not admin"); // docs:end:read_admin // docs:start:write_minter - storage.minters.at(minter.address).write(approve as Field); + storage.minters.at(minter.address).write(approve); // docs:end:write_minter } // docs:end:set_minter @@ -159,14 +153,14 @@ contract Token { amount: Field, ) -> Field { // docs:start:read_minter - assert(storage.minters.at(context.msg_sender()).read() == 1, "caller is not minter"); + assert(storage.minters.at(context.msg_sender()).read(), "caller is not minter"); // docs:end:read_minter let amount = SafeU120::new(amount); - let new_balance = SafeU120::new(storage.public_balances.at(to.address).read()).add(amount); - let supply = SafeU120::new(storage.total_supply.read()).add(amount); + let new_balance = storage.public_balances.at(to.address).read().add(amount); + let supply = storage.total_supply.read().add(amount); - storage.public_balances.at(to.address).write(new_balance.value as Field); - storage.total_supply.write(supply.value as Field); + storage.public_balances.at(to.address).write(new_balance); + storage.total_supply.write(supply); 1 } // docs:end:mint_public @@ -177,12 +171,12 @@ contract Token { amount: Field, secret_hash: Field, ) -> Field { - assert(storage.minters.at(context.msg_sender()).read() == 1, "caller is not minter"); + assert(storage.minters.at(context.msg_sender()).read(), "caller is not minter"); let pending_shields = storage.pending_shields; let mut note = TransparentNote::new(amount, secret_hash); - let supply = SafeU120::new(storage.total_supply.read()).add(SafeU120::new(amount)); + let supply = storage.total_supply.read().add(SafeU120::new(amount)); - storage.total_supply.write(supply.value as Field); + storage.total_supply.write(supply); // docs:start:insert_from_public pending_shields.insert_from_public(&mut note); // docs:end:insert_from_public @@ -208,12 +202,12 @@ contract Token { } let amount = SafeU120::new(amount); - let from_balance = SafeU120::new(storage.public_balances.at(from.address).read()).sub(amount); + let from_balance = storage.public_balances.at(from.address).read().sub(amount); let pending_shields = storage.pending_shields; let mut note = TransparentNote::new(amount.value as Field, secret_hash); - storage.public_balances.at(from.address).write(from_balance.value as Field); + storage.public_balances.at(from.address).write(from_balance); pending_shields.insert_from_public(&mut note); 1 } @@ -236,11 +230,11 @@ contract Token { } let amount = SafeU120::new(amount); - let from_balance = SafeU120::new(storage.public_balances.at(from.address).read()).sub(amount); - storage.public_balances.at(from.address).write(from_balance.value as Field); + let from_balance = storage.public_balances.at(from.address).read().sub(amount); + storage.public_balances.at(from.address).write(from_balance); - let to_balance = SafeU120::new(storage.public_balances.at(to.address).read()).add(amount); - storage.public_balances.at(to.address).write(to_balance.value as Field); + let to_balance = storage.public_balances.at(to.address).read().add(amount); + storage.public_balances.at(to.address).write(to_balance); 1 } @@ -262,11 +256,11 @@ contract Token { } let amount = SafeU120::new(amount); - let from_balance = SafeU120::new(storage.public_balances.at(from.address).read()).sub(amount); - storage.public_balances.at(from.address).write(from_balance.value as Field); + let from_balance = storage.public_balances.at(from.address).read().sub(amount); + storage.public_balances.at(from.address).write(from_balance); - let new_supply = SafeU120::new(storage.total_supply.read()).sub(amount); - storage.total_supply.write(new_supply.value as Field); + let new_supply = storage.total_supply.read().sub(amount); + storage.total_supply.write(new_supply); 1 } @@ -280,13 +274,13 @@ contract Token { secret: Field, ) -> Field { let pending_shields = storage.pending_shields; - let balance = storage.balances.at(to.address); let mut public_note = TransparentNote::new_from_secret(amount, secret); // docs:start:assert_contains_and_remove_publicly_created pending_shields.assert_contains_and_remove_publicly_created(&mut public_note); // docs:end:assert_contains_and_remove_publicly_created - increment(balance, amount, to.address); + + storage.balances.at(to).add(SafeU120::new(amount)); 1 } @@ -308,8 +302,7 @@ contract Token { assert(nonce == 0, "invalid nonce"); } - let from_balance = storage.balances.at(from.address); - decrement(from_balance, amount, from.address); + storage.balances.at(from).sub(SafeU120::new(amount)); let selector = compute_selector("_increase_public_balance((Field),Field)"); let _void = context.call_public_function(context.this_address(), selector, [to.address, amount]); @@ -334,11 +327,9 @@ contract Token { assert(nonce == 0, "invalid nonce"); } - let from_balance = storage.balances.at(from.address); - let to_balance = storage.balances.at(to.address); - - decrement(from_balance, amount, from.address); - increment(to_balance, amount, to.address); + let amount = SafeU120::new(amount); + storage.balances.at(from).sub(amount); + storage.balances.at(to).add(amount); 1 } @@ -359,9 +350,7 @@ contract Token { assert(nonce == 0, "invalid nonce"); } - let from_balance = storage.balances.at(from.address); - - decrement(from_balance, amount, from.address); + storage.balances.at(from).sub(SafeU120::new(amount)); let selector = compute_selector("_reduce_total_supply(Field)"); let _void = context.call_public_function(context.this_address(), selector, [amount]); @@ -380,8 +369,8 @@ contract Token { fn _initialize( new_admin: AztecAddress, ) { - storage.admin.write(new_admin.address); - storage.minters.at(new_admin.address).write(1); + storage.admin.write(new_admin); + storage.minters.at(new_admin.address).write(true); } // docs:end:initialize @@ -393,8 +382,8 @@ contract Token { to: AztecAddress, amount: Field, ) { - let new_balance = SafeU120::new(storage.public_balances.at(to.address).read()).add(SafeU120::new(amount)); - storage.public_balances.at(to.address).write(new_balance.value as Field); + let new_balance = storage.public_balances.at(to.address).read().add(SafeU120::new(amount)); + storage.public_balances.at(to.address).write(new_balance); } // docs:end:increase_public_balance @@ -404,8 +393,8 @@ contract Token { amount: Field, ) { // Only to be called from burn. - let new_supply = SafeU120::new(storage.total_supply.read()).sub(SafeU120::new(amount)); - storage.total_supply.write(new_supply.value as Field); + let new_supply = storage.total_supply.read().sub(SafeU120::new(amount)); + storage.total_supply.write(new_supply); } // docs:end:reduce_total_supply @@ -413,7 +402,7 @@ contract Token { // docs:start:admin unconstrained fn admin() -> Field { - storage.admin.read() + storage.admin.read().address } // docs:end:admin @@ -421,31 +410,29 @@ contract Token { unconstrained fn is_minter( minter: AztecAddress, ) -> bool { - storage.minters.at(minter.address).read() as bool + storage.minters.at(minter.address).read() } // docs:end:is_minter // docs:start:total_supply - unconstrained fn total_supply() -> Field { - storage.total_supply.read() + unconstrained fn total_supply() -> u120 { + storage.total_supply.read().value } // docs:end:total_supply // docs:start:balance_of_private unconstrained fn balance_of_private( owner: AztecAddress, - ) -> Field { - let owner_balance = storage.balances.at(owner.address); - - balance_utils::get_balance(owner_balance) + ) -> u120 { + storage.balances.at(owner).balance_of().value } // docs:end:balance_of_private // docs:start:balance_of_public unconstrained fn balance_of_public( owner: AztecAddress, - ) -> Field { - storage.public_balances.at(owner.address).read() + ) -> u120 { + storage.public_balances.at(owner.address).read().value } // docs:end:balance_of_public @@ -456,12 +443,12 @@ contract Token { // Computes note hash and nullifier. // Note 1: Needs to be defined by every contract producing logs. // Note 2: Having it in all the contracts gives us the ability to compute the note hash and nullifier differently for different kind of notes. - unconstrained fn compute_note_hash_and_nullifier(contract_address: Field, nonce: Field, storage_slot: Field, preimage: [Field; VALUE_NOTE_LEN]) -> [Field; 4] { + unconstrained fn compute_note_hash_and_nullifier(contract_address: Field, nonce: Field, storage_slot: Field, preimage: [Field; TOKEN_NOTE_LEN]) -> [Field; 4] { let note_header = NoteHeader { contract_address, nonce, storage_slot }; if (storage_slot == 5) { note_utils::compute_note_hash_and_nullifier(TransparentNoteMethods, note_header, preimage) } else { - note_utils::compute_note_hash_and_nullifier(ValueNoteMethods, note_header, preimage) + note_utils::compute_note_hash_and_nullifier(TokenNoteMethods, note_header, preimage) } } // docs:end:compute_note_hash_and_nullifier diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/src/types.nr b/yarn-project/noir-contracts/src/contracts/token_contract/src/types.nr index 8b7e250cec9..1f4883a54c6 100644 --- a/yarn-project/noir-contracts/src/contracts/token_contract/src/types.nr +++ b/yarn-project/noir-contracts/src/contracts/token_contract/src/types.nr @@ -1,130 +1,5 @@ -// docs:start:token_types_all -use dep::std::hash::pedersen; -use dep::std::hash::pedersen_with_separator; -use dep::aztec::note::{ - note_header::NoteHeader, - note_interface::NoteInterface, - utils::compute_siloed_note_hash, -}; -use dep::aztec::constants_gen::GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET; - -global TRANSPARENT_NOTE_LEN: Field = 2; - - -// Transparent note represents a note that is created in the clear (public execution), -// but can only be spent by those that know the preimage of the "secret_hash" -struct TransparentNote { - amount: Field, - secret_hash: Field, - // the fields below are not serialized/deserialized - secret: Field, - header: NoteHeader, -} - -impl TransparentNote { - - // CONSTRUCTORS - - fn new(amount: Field, secret_hash: Field) -> Self { - TransparentNote { - amount: amount, - secret_hash: secret_hash, - secret: 0, - header: NoteHeader::empty(), - } - } - - // new oracle call primitive - // get me the secret corresponding to this hash - fn new_from_secret(amount: Field, secret: Field) -> Self { - TransparentNote { - amount: amount, - secret_hash: TransparentNote::compute_secret_hash(secret), - secret: secret, - header: NoteHeader::empty(), - } - } - - - // STANDARD NOTE_INTERFACE FUNCTIONS - - fn serialize(self) -> [Field; TRANSPARENT_NOTE_LEN] { - [self.amount, self.secret_hash] - } - - fn deserialize(preimage: [Field; TRANSPARENT_NOTE_LEN]) -> Self { - TransparentNote { - amount: preimage[0], - secret_hash: preimage[1], - secret: 0, - header: NoteHeader::empty(), - } - } - - fn compute_note_hash(self) -> Field { - // TODO(#1205) Should use a non-zero generator index. - dep::std::hash::pedersen([ - self.amount, - self.secret_hash, - ])[0] - } - - fn compute_nullifier(self) -> Field { - // TODO(https://github.com/AztecProtocol/aztec-packages/issues/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. - pedersen([self.secret, siloed_note_hash])[0] - } - - fn set_header(&mut self, header: NoteHeader) { - self.header = header; - } - - - // CUSTOM FUNCTIONS FOR THIS NOTE TYPE - - fn compute_secret_hash(secret: Field) -> Field { - // TODO(#1205) This is probably not the right index to use - pedersen_with_separator([secret], GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET)[0] - } - - fn knows_secret(self, secret: Field) { - let hash = TransparentNote::compute_secret_hash(secret); - assert(self.secret_hash == hash); - } -} - -fn deserialize(preimage: [Field; TRANSPARENT_NOTE_LEN]) -> TransparentNote { - TransparentNote::deserialize(preimage) -} - -fn serialize(note: TransparentNote) -> [Field; TRANSPARENT_NOTE_LEN] { - note.serialize() -} - -fn compute_note_hash(note: TransparentNote) -> Field { - note.compute_note_hash() -} - -fn compute_nullifier(note: TransparentNote) -> Field { - note.compute_nullifier() -} - -fn get_header(note: TransparentNote) -> NoteHeader { - note.header -} - -fn set_header(note: &mut TransparentNote, header: NoteHeader) { - note.set_header(header) -} - -global TransparentNoteMethods = NoteInterface { - deserialize, - serialize, - compute_note_hash, - compute_nullifier, - get_header, - set_header, -}; -// docs:end:token_types_all \ No newline at end of file +mod transparent_note; +mod balance_set; +mod balances_map; +mod token_note; +mod safe_u120_serialization; \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/src/types/balance_set.nr b/yarn-project/noir-contracts/src/contracts/token_contract/src/types/balance_set.nr new file mode 100644 index 00000000000..3789e62c96a --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/token_contract/src/types/balance_set.nr @@ -0,0 +1,132 @@ +use dep::std::option::Option; +use dep::safe_math::SafeU120; +use dep::aztec::{ + context::Context, + constants_gen::MAX_READ_REQUESTS_PER_CALL, + state_vars::set::Set, + log::emit_encrypted_log, + types::address::AztecAddress, +}; +use dep::aztec::note::{ + note_getter::view_notes, + note_getter_options::{NoteGetterOptions, SortOrder}, + note_viewer_options::NoteViewerOptions +}; +use dep::aztec::note::{ + note_header::NoteHeader, + 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. +// Does not require spending key, but only knowledge. +// Spending key requirement should be enforced by the contract using this. +struct BalanceSet { + context: Context, + owner: AztecAddress, + set: Set +} + +impl BalanceSet { + fn new(context: Context, owner: AztecAddress, storage_slot: Field) -> Self { + assert(storage_slot != 0, "Storage slot 0 not allowed. Storage slots must start from 1."); + let set = Set { + context, + storage_slot, + note_interface: TokenNoteMethods, + }; + Self { + context, + owner, + set, + } + } + + unconstrained fn balance_of(self: Self) -> SafeU120 { + self.balance_of_with_offset(0) + } + + unconstrained fn balance_of_with_offset(self: Self, offset: u32) -> SafeU120 { + // Same as SafeU120::new(0), but fewer constraints because no check. + let mut balance = SafeU120::min(); + // docs:start:view_notes + let options = NoteViewerOptions::new().set_offset(offset); + let opt_notes = self.set.view_notes(options); + // docs:end:view_notes + let len = opt_notes.len(); + for i in 0..len { + if opt_notes[i].is_some() { + balance = balance.add(opt_notes[i].unwrap_unchecked().amount); + } + } + if (opt_notes[len - 1].is_some()) { + balance = balance.add(self.balance_of_with_offset(offset + opt_notes.len() as u32)); + } + + balance + } + + fn add(self: Self, addend: SafeU120) { + let mut addend_note = TokenNote::new(addend, self.owner); + + // docs:start:insert + self.set.insert(&mut addend_note); + // docs:end:insert + + addend_note.emit_encrypted( + self.context.private.unwrap(), + self.set.storage_slot + ); + } + + fn sub(self: Self, subtrahend: SafeU120) { + // docs:start:get_notes + let options = NoteGetterOptions::with_filter(filter_notes_min_sum, subtrahend); + let maybe_notes = self.set.get_notes(options); + // docs:end:get_notes + + let mut minuend: SafeU120 = SafeU120::min(); + for i in 0..maybe_notes.len() { + if maybe_notes[i].is_some() { + let note = maybe_notes[i].unwrap_unchecked(); + + // Removes the note from the owner's set of notes. + // This will call the the `compute_nullifer` function of the `token_note` + // which require knowledge of the secret key (currently the users encryption key). + // The contract logic must ensure that the spending key is used as well. + // docs:start:remove + self.set.remove(note); + // docs:end:remove + + minuend = minuend.add(note.amount); + } + } + + // This is to provide a nicer error msg, + // without it minuend-subtrahend would still catch it, but more generic error then. + // without the == true, it includes 'minuend.ge(subtrahend)' as part of the error. + assert(minuend.ge(subtrahend) == true, "Balance too low"); + + self.add(minuend.sub(subtrahend)); + } +} + +fn filter_notes_min_sum(notes: [Option; MAX_READ_REQUESTS_PER_CALL], min_sum: SafeU120) -> [Option; MAX_READ_REQUESTS_PER_CALL] { + let mut selected = [Option::none(); MAX_READ_REQUESTS_PER_CALL]; + let mut sum = SafeU120::min(); + for i in 0..notes.len() { + if notes[i].is_some() & sum.lt(min_sum) { + let note = notes[i].unwrap_unchecked(); + selected[i] = Option::some(note); + sum = sum.add(note.amount); + } + } + selected +} \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/src/types/balances_map.nr b/yarn-project/noir-contracts/src/contracts/token_contract/src/types/balances_map.nr new file mode 100644 index 00000000000..fef342cbea7 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/token_contract/src/types/balances_map.nr @@ -0,0 +1,27 @@ +use dep::aztec::context::{PrivateContext, PublicContext, Context}; +use dep::aztec::types::address::AztecAddress; +use dep::std::option::Option; +use crate::types::balance_set::BalanceSet; + +struct BalancesMap { + context: Context, + storage_slot: Field, +} + +impl BalancesMap { + fn new( + context: Context, + storage_slot: Field, + ) -> Self { + assert(storage_slot != 0, "Storage slot 0 not allowed. Storage slots must start from 1."); + Self { + context, + storage_slot, + } + } + + fn at(self, owner: AztecAddress) -> BalanceSet { + let derived_storage_slot = dep::std::hash::pedersen([self.storage_slot, owner.address])[0]; + BalanceSet::new(self.context, owner, derived_storage_slot) + } +} diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/src/types/safe_u120_serialization.nr b/yarn-project/noir-contracts/src/contracts/token_contract/src/types/safe_u120_serialization.nr new file mode 100644 index 00000000000..f4f3ef84d23 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/token_contract/src/types/safe_u120_serialization.nr @@ -0,0 +1,18 @@ +use dep::aztec::types::type_serialization::TypeSerializationInterface; +use dep::safe_math::SafeU120; + +global SAFE_U120_SERIALIZED_LEN: Field = 1; + +// This is safe when reading from storage IF only correct safeu120 was written to storage +fn deserializeU120(fields: [Field; SAFE_U120_SERIALIZED_LEN]) -> SafeU120 { + SafeU120{value: fields[0] as u120} +} + +fn serializeU120(value: SafeU120) -> [Field; SAFE_U120_SERIALIZED_LEN] { + [value.value as Field] +} + +global SafeU120SerializationMethods = TypeSerializationInterface { + deserialize: deserializeU120, + serialize: serializeU120, +}; \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/src/types/token_note.nr b/yarn-project/noir-contracts/src/contracts/token_contract/src/types/token_note.nr new file mode 100644 index 00000000000..1322f248332 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/token_contract/src/types/token_note.nr @@ -0,0 +1,143 @@ +use dep::aztec::{ + note::{ + note_header::NoteHeader, + note_interface::NoteInterface, + utils::compute_note_hash_for_read_or_nullify, + }, + context::PrivateContext, + constants_gen::MAX_READ_REQUESTS_PER_CALL, + state_vars::set::Set, + log::emit_encrypted_log +}; +use dep::aztec::types::address::AztecAddress; +use dep::aztec::oracle::{ + rand::rand, + get_secret_key::get_secret_key, + get_public_key::get_public_key, +}; + +use dep::safe_math::SafeU120; +use dep::std::option::Option; + +global TOKEN_NOTE_LEN: Field = 3; // 3 plus a header. + +struct TokenNote { + // the amount of tokens in the note + amount: SafeU120, + // the provider of secrets for the nullifier. The owner (recipient) to ensure that the note + // can be privately spent. When nullifier secret and encryption private key is same + // we can simply use the owner for this one. + owner: AztecAddress, + // randomness of the note to hide contents. + randomness: Field, + // the note header (contract_address, nonce, storage_slot) + // included in the note such that it becomes part of encrypted logs for later use. + header: NoteHeader, +} + +impl TokenNote { + fn new(amount: SafeU120, owner: AztecAddress) -> Self { + Self { + amount, + owner, + randomness: rand(), + header: NoteHeader::empty(), + } + } + + fn serialize(self) -> [Field; TOKEN_NOTE_LEN] { + [self.amount.value as Field, self.owner.address, self.randomness] + } + + fn deserialize(preimage: [Field; TOKEN_NOTE_LEN]) -> Self { + Self { + amount: SafeU120::new(preimage[0]), + owner: AztecAddress::new(preimage[1]), + randomness: preimage[2], + header: NoteHeader::empty(), + } + } + + fn compute_note_hash(self) -> Field { + // TODO(#1205) Should use a non-zero generator index. + dep::std::hash::pedersen([ + self.amount.value as Field, + self.owner.address as Field, + self.randomness, + ])[0] + } + + // docs:start:nullifier + fn compute_nullifier(self) -> Field { + let note_hash_for_nullify = compute_note_hash_for_read_or_nullify(TokenNoteMethods, self); + let secret = get_secret_key(self.owner.address); + // TODO(#1205) Should use a non-zero generator index. + dep::std::hash::pedersen([ + note_hash_for_nullify, + secret.low, + secret.high, + ])[0] + } + // docs:end:nullifier + + fn set_header(&mut self, header: NoteHeader) { + self.header = header; + } + + + fn emit_encrypted( + self: &mut Self, + context: &mut PrivateContext, + storage_slot: Field, + ) { + // We only bother inserting the note if non-empty to save funds on gas. + if !self.amount.is_zero() { + // docs:start:encrypted + let application_contract_address = (*context).this_address(); + let encryption_pub_key = get_public_key(self.owner.address); + let encrypted_data = (*self).serialize(); + + emit_encrypted_log( + context, + application_contract_address, + storage_slot, + encryption_pub_key, + encrypted_data, + ); + // docs:end:encrypted + } + } +} + +fn deserialize(preimage: [Field; TOKEN_NOTE_LEN]) -> TokenNote { + TokenNote::deserialize(preimage) +} + +fn serialize(note: TokenNote) -> [Field; TOKEN_NOTE_LEN] { + note.serialize() +} + +fn compute_note_hash(note: TokenNote) -> Field { + note.compute_note_hash() +} + +fn compute_nullifier(note: TokenNote) -> Field { + note.compute_nullifier() +} + +fn get_header(note: TokenNote) -> NoteHeader { + note.header +} + +fn set_header(note: &mut TokenNote, header: NoteHeader) { + note.set_header(header) +} + +global TokenNoteMethods = NoteInterface { + deserialize, + serialize, + compute_note_hash, + compute_nullifier, + get_header, + set_header, +}; \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/token_contract/src/types/transparent_note.nr b/yarn-project/noir-contracts/src/contracts/token_contract/src/types/transparent_note.nr new file mode 100644 index 00000000000..ae32796f2bd --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/token_contract/src/types/transparent_note.nr @@ -0,0 +1,129 @@ +// docs:start:token_types_all +use dep::std::hash::pedersen; +use dep::std::hash::pedersen_with_separator; +use dep::aztec::note::{ + note_header::NoteHeader, + note_interface::NoteInterface, + utils::compute_siloed_note_hash, +}; +use dep::aztec::constants_gen::GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET; + +global TRANSPARENT_NOTE_LEN: Field = 2; + +// Transparent note represents a note that is created in the clear (public execution), +// but can only be spent by those that know the preimage of the "secret_hash" +struct TransparentNote { + amount: Field, + secret_hash: Field, + // the secret is just here for ease of use and won't be (de)serialized + secret: Field, + // header is just here to satisfy the NoteInterface + header: NoteHeader, +} + +impl TransparentNote { + + // CONSTRUCTORS + + fn new(amount: Field, secret_hash: Field) -> Self { + TransparentNote { + amount: amount, + secret_hash: secret_hash, + secret: 0, + header: NoteHeader::empty(), + } + } + + // new oracle call primitive + // get me the secret corresponding to this hash + fn new_from_secret(amount: Field, secret: Field) -> Self { + TransparentNote { + amount: amount, + secret_hash: TransparentNote::compute_secret_hash(secret), + secret: secret, + header: NoteHeader::empty(), + } + } + + + // STANDARD NOTE_INTERFACE FUNCTIONS + + fn serialize(self) -> [Field; TRANSPARENT_NOTE_LEN] { + [self.amount, self.secret_hash] + } + + fn deserialize(preimage: [Field; TRANSPARENT_NOTE_LEN]) -> Self { + TransparentNote { + amount: preimage[0], + secret_hash: preimage[1], + secret: 0, + header: NoteHeader::empty(), + } + } + + fn compute_note_hash(self) -> Field { + // TODO(#1205) Should use a non-zero generator index. + dep::std::hash::pedersen([ + self.amount, + self.secret_hash, + ])[0] + } + + fn compute_nullifier(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. + pedersen([self.secret, siloed_note_hash])[0] + } + + fn set_header(&mut self, header: NoteHeader) { + self.header = header; + } + + + // CUSTOM FUNCTIONS FOR THIS NOTE TYPE + + fn compute_secret_hash(secret: Field) -> Field { + // TODO(#1205) This is probably not the right index to use + pedersen_with_separator([secret], GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET)[0] + } + + fn knows_secret(self, secret: Field) { + let hash = TransparentNote::compute_secret_hash(secret); + assert(self.secret_hash == hash); + } +} + +fn deserialize(preimage: [Field; TRANSPARENT_NOTE_LEN]) -> TransparentNote { + TransparentNote::deserialize(preimage) +} + +fn serialize(note: TransparentNote) -> [Field; TRANSPARENT_NOTE_LEN] { + note.serialize() +} + +fn compute_note_hash(note: TransparentNote) -> Field { + note.compute_note_hash() +} + +fn compute_nullifier(note: TransparentNote) -> Field { + note.compute_nullifier() +} + +fn get_header(note: TransparentNote) -> NoteHeader { + note.header +} + +fn set_header(note: &mut TransparentNote, header: NoteHeader) { + note.set_header(header) +} + +global TransparentNoteMethods = NoteInterface { + deserialize, + serialize, + compute_note_hash, + compute_nullifier, + get_header, + set_header, +}; +// docs:end:token_types_all \ No newline at end of file