diff --git a/.circleci/config.yml b/.circleci/config.yml index 81eb7344f1f..1ddeab85e91 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -599,6 +599,17 @@ jobs: name: "Test" command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_sandbox_example.test.ts + e2e-singleton: + 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_singleton.test.ts + e2e-block-building: docker: - image: aztecprotocol/alpine-build-image @@ -1228,6 +1239,7 @@ workflows: # TODO(3458): Investigate intermittent failure # - e2e-slow-tree: *e2e_test - e2e-sandbox-example: *e2e_test + - e2e-singleton: *e2e_test - e2e-block-building: *e2e_test - e2e-nested-contract: *e2e_test - e2e-non-contract-account: *e2e_test @@ -1265,6 +1277,7 @@ workflows: - e2e-token-contract - e2e-blacklist-token-contract - e2e-sandbox-example + - e2e-singleton - e2e-block-building - e2e-nested-contract - e2e-non-contract-account diff --git a/docs/docs/dev_docs/contracts/syntax/storage/main.md b/docs/docs/dev_docs/contracts/syntax/storage/main.md index 0be4049fadd..b774ee0c682 100644 --- a/docs/docs/dev_docs/contracts/syntax/storage/main.md +++ b/docs/docs/dev_docs/contracts/syntax/storage/main.md @@ -306,7 +306,7 @@ To update the value of a `Singleton`, we can use the `replace` method. The metho An example of this is seen in a example card game, where we create a new note (a `CardNote`) containing some new data, and replace the current note with it: -#include_code state_vars-SingletonReplace /yarn-project/noir-contracts/contracts/docs_example_contract/src/actions.nr rust +#include_code state_vars-SingletonReplace /yarn-project/noir-contracts/contracts/docs_example_contract/src/main.nr rust If two people are trying to modify the Singleton at the same time, only one will succeed as we don't allow duplicate nullifiers! Developers should put in place appropriate access controls to avoid race conditions (unless a race is intended!). @@ -314,7 +314,7 @@ If two people are trying to modify the Singleton at the same time, only one will This function allows us to get the note of a Singleton, essentially reading the value. -#include_code state_vars-SingletonGet /yarn-project/noir-contracts/contracts/docs_example_contract/src/actions.nr rust +#include_code state_vars-SingletonGet /yarn-project/noir-contracts/contracts/docs_example_contract/src/main.nr rust #### Nullifying Note reads diff --git a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts index 77c58873e46..bc6baf42a99 100644 --- a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts @@ -250,18 +250,6 @@ describe('e2e_lending_contract', () => { .send(), ); }); - describe('failure cases', () => { - it('calling internal _deposit function directly', async () => { - // Try to call the internal `_deposit` function directly - // This should: - // - not change any storage values. - // - fail - - await expect( - lendingContract.methods._deposit(lendingAccount.address.toField(), 42n, collateralAsset.address).simulate(), - ).rejects.toThrow(); - }); - }); }); describe('Borrow', () => { diff --git a/yarn-project/end-to-end/src/e2e_singleton.test.ts b/yarn-project/end-to-end/src/e2e_singleton.test.ts new file mode 100644 index 00000000000..251b4e7a44f --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_singleton.test.ts @@ -0,0 +1,31 @@ +import { Fr, Wallet } from '@aztec/aztec.js'; +import { DocsExampleContract } from '@aztec/noir-contracts'; + +import { setup } from './fixtures/utils.js'; + +describe('e2e_singleton', () => { + let wallet: Wallet; + + let teardown: () => Promise; + let contract: DocsExampleContract; + + beforeAll(async () => { + ({ teardown, wallet } = await setup()); + contract = await DocsExampleContract.deploy(wallet).send().deployed(); + // sets card value to 1 and leader to sender. + await contract.methods.initialize_private(Fr.random(), 1).send().wait(); + }, 25_000); + + afterAll(() => teardown()); + + // Singleton tests: + it('can read singleton and replace/update it in the same call', async () => { + await expect(contract.methods.update_legendary_card(Fr.random(), 0).simulate()).rejects.toThrowError( + 'Assertion failed: can only update to higher value', + ); + + const newPoints = 3n; + await contract.methods.update_legendary_card(Fr.random(), newPoints).send().wait(); + expect((await contract.methods.get_leader().view()).points).toEqual(newPoints); + }); +}); diff --git a/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts b/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts index c17d0d7abb4..90b622b6115 100644 --- a/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts +++ b/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts @@ -166,7 +166,7 @@ function generateAbiStatement(name: string, artifactImportPath: string) { * @returns The corresponding ts code. */ export function generateTypescriptContractInterface(input: ContractArtifact, artifactImportPath?: string) { - const methods = input.functions.filter(f => f.name !== 'constructor').map(generateMethod); + const methods = input.functions.filter(f => f.name !== 'constructor' && !f.isInternal).map(generateMethod); const deploy = artifactImportPath && generateDeploy(input); const ctor = artifactImportPath && generateConstructor(input.name); const at = artifactImportPath && generateAt(input.name); diff --git a/yarn-project/noir-contracts/contracts/docs_example_contract/src/actions.nr b/yarn-project/noir-contracts/contracts/docs_example_contract/src/actions.nr deleted file mode 100644 index e827e1cdede..00000000000 --- a/yarn-project/noir-contracts/contracts/docs_example_contract/src/actions.nr +++ /dev/null @@ -1,24 +0,0 @@ -use dep::aztec::state_vars::{ - singleton::Singleton, -}; -use dep::std::option::Option; - -use crate::types::{ - card_note::{CardNote, CARD_NOTE_LEN}, -}; - -pub fn init_legendary_card(state_var: Singleton, card: &mut CardNote) { - state_var.initialize(card, Option::some(card.owner), true); -} - -// docs:start:state_vars-SingletonReplace -pub fn update_legendary_card(state_var: Singleton, card: &mut CardNote) { - state_var.replace(card, true); -} -// docs:end:state_vars-SingletonReplace - -// docs:start:state_vars-SingletonGet -pub fn get_legendary_card(state_var: Singleton) -> CardNote { - state_var.get_note(true) -} -// docs:end:state_vars-SingletonGet diff --git a/yarn-project/noir-contracts/contracts/docs_example_contract/src/main.nr b/yarn-project/noir-contracts/contracts/docs_example_contract/src/main.nr index 3287e4df905..6662f6de2f0 100644 --- a/yarn-project/noir-contracts/contracts/docs_example_contract/src/main.nr +++ b/yarn-project/noir-contracts/contracts/docs_example_contract/src/main.nr @@ -1,68 +1,126 @@ -mod actions; mod options; mod types; +// Following is a very simple game to show case use of singleton in as minimalistic way as possible +// It also serves as an e2e test that you can read and then replace the singleton in the same call +// (tests ordering in the circuit) + +// you have a card (singleton). Anyone can create a bigger card. Whoever is bigger will be the leader. +// it also has dummy methods and other examples used for documentation e.g. +// how to create custom notes, a custom struct for public state, a custom note that may be unencrypted +// also has `options.nr` which shows various ways of using `NoteGetterOptions` to query notes +// it also shows what our macros do behind the scenes! + contract DocsExample { + // how to import dependencies defined in your workspace use dep::aztec::protocol_types::{ + abis::function_selector::FunctionSelector, address::AztecAddress, }; use dep::aztec::{ - context::{PrivateContext, Context}, - state_vars::{ - map::Map, - singleton::Singleton, + note::{ + note_header::NoteHeader, + utils as note_utils, }, + context::{PrivateContext, PublicContext, Context}, + state_vars::{map::Map, public_state::PublicState,singleton::Singleton}, }; - use crate::actions; + // how to import methods from other files/folders within your workspace use crate::options::create_account_card_getter_options; use crate::types::{ card_note::{CardNote, CardNoteMethods, CARD_NOTE_LEN}, + leader::{Leader, LeaderSerializationMethods, LEADER_SERIALIZED_LEN}, }; struct Storage { + // Shows how to create a custom struct in Public + leader: PublicState, // docs:start:storage-singleton-declaration legendary_card: Singleton, // docs:end:storage-singleton-declaration + // just used for docs example to show how to create a singleton map. // docs:start:storage-map-singleton-declaration profiles: Map>, // docs:end:storage-map-singleton-declaration } - // docs:start:state_vars-MapSingleton impl Storage { fn init(context: Context) -> Self { Storage { + leader: PublicState::new( + context, + 1, + LeaderSerializationMethods, + ), // docs:start:start_vars_singleton - legendary_card: Singleton::new(context, 1, CardNoteMethods), + legendary_card: Singleton::new(context, 2, CardNoteMethods), // docs:end:start_vars_singleton - // highlight-next-line:state_vars-MapSingleton + // just used for docs example (not for game play): + // docs:start:state_vars-MapSingleton profiles: Map::new( context, - 2, + 3, |context, slot| { Singleton::new(context, slot, CardNoteMethods) }, ), + // docs:end:state_vars-MapSingleton } } } - // docs:end:state_vars-MapSingleton #[aztec(private)] - fn constructor(legendary_card_secret: Field) { - let mut legendary_card = CardNote::new(0, legendary_card_secret, AztecAddress::zero()); - actions::init_legendary_card(storage.legendary_card, &mut legendary_card); + fn constructor() {} + + #[aztec(private)] + // msg_sender() is 0 at deploy time. So created another function + fn initialize_private(randomness: Field, points: u8) { + let mut legendary_card = CardNote::new(points, randomness, context.msg_sender()); + // create and broadcast note + storage.legendary_card.initialize(&mut legendary_card, Option::none(), true); } #[aztec(private)] - fn update_legendary_card(new_points: u8, new_secret: Field) { - let owner = inputs.call_context.msg_sender; - let mut updated_card = CardNote::new(new_points, new_secret, owner); - actions::update_legendary_card(storage.legendary_card, &mut updated_card); + fn update_legendary_card(randomness: Field, points: u8) { + // Ensure `points` > current value + // Also serves as a e2e test that you can `get_note()` and then `replace()` + + // docs:start:state_vars-SingletonGet + let card = storage.legendary_card.get_note(true); + // docs:end:state_vars-SingletonGet + + assert(points > card.points, "can only update to higher value"); + let mut new_card = CardNote::new(points, randomness, context.msg_sender()); + // docs:start:state_vars-SingletonReplace + storage.legendary_card.replace(&mut new_card, true); + // docs:end:state_vars-SingletonReplace + + context.call_public_function( + context.this_address(), + FunctionSelector::from_signature("update_leader((Field),u8)"), + [context.msg_sender().to_field(), points as Field] + ); } - unconstrained fn get_legendary_card() -> pub CardNote { - actions::get_legendary_card(storage.legendary_card) + #[aztec(public)] + internal fn update_leader(account: AztecAddress, points: u8) { + let new_leader = Leader { account, points }; + storage.leader.write(new_leader); + } + + unconstrained fn get_leader() -> pub Leader { + storage.leader.read() + } + + // TODO: remove this placeholder once https://github.com/AztecProtocol/aztec-packages/issues/2918 is implemented + unconstrained fn compute_note_hash_and_nullifier( + contract_address: AztecAddress, + nonce: Field, + storage_slot: Field, + serialized_note: [Field; CARD_NOTE_LEN] + ) -> pub [Field; 4] { + let note_header = NoteHeader::new(contract_address, nonce, storage_slot); + note_utils::compute_note_hash_and_nullifier(CardNoteMethods, note_header, serialized_note) } /// Macro equivalence section @@ -128,21 +186,4 @@ contract DocsExample { // ************************************************************ } // docs:end:simple_macro_example_expanded - - // Cross chain messaging section - // Demonstrates a cross chain message - // docs:start:l1_to_l2_cross_chain_message - #[aztec(private)] - fn send_to_l1() {} - // docs:end:l1_to_l2_cross_chain_message - - // TODO: remove this placeholder once https://github.com/AztecProtocol/aztec-packages/issues/2918 is implemented - unconstrained fn compute_note_hash_and_nullifier( - contract_address: AztecAddress, - nonce: Field, - storage_slot: Field, - serialized_note: [Field; 0] - ) -> pub [Field; 4] { - [0, 0, 0, 0] - } } diff --git a/yarn-project/noir-contracts/contracts/docs_example_contract/src/options.nr b/yarn-project/noir-contracts/contracts/docs_example_contract/src/options.nr index 8fb26e6e7d7..9742345f8ca 100644 --- a/yarn-project/noir-contracts/contracts/docs_example_contract/src/options.nr +++ b/yarn-project/noir-contracts/contracts/docs_example_contract/src/options.nr @@ -6,6 +6,8 @@ use dep::aztec::protocol_types::{ use dep::aztec::note::note_getter_options::{NoteGetterOptions, Sort, SortOrder}; use dep::std::option::Option; +// Shows how to use NoteGetterOptions and query for notes. + // docs:start:state_vars-NoteGetterOptionsSelectSortOffset pub fn create_account_card_getter_options( account: AztecAddress, diff --git a/yarn-project/noir-contracts/contracts/docs_example_contract/src/types.nr b/yarn-project/noir-contracts/contracts/docs_example_contract/src/types.nr index 08035046df2..630d5c9b87e 100644 --- a/yarn-project/noir-contracts/contracts/docs_example_contract/src/types.nr +++ b/yarn-project/noir-contracts/contracts/docs_example_contract/src/types.nr @@ -1 +1,2 @@ mod card_note; +mod leader; diff --git a/yarn-project/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr b/yarn-project/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr index ba383755ca0..f185518c412 100644 --- a/yarn-project/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr +++ b/yarn-project/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr @@ -14,35 +14,37 @@ use dep::aztec::{ context::PrivateContext, }; +// Shows how to create a custom note + global CARD_NOTE_LEN: Field = 3; // docs:start:state_vars-CardNote struct CardNote { points: u8, - secret: Field, + randomness: Field, owner: AztecAddress, header: NoteHeader, } // docs:end:state_vars-CardNote impl CardNote { - pub fn new(points: u8, secret: Field, owner: AztecAddress) -> Self { + pub fn new(points: u8, randomness: Field, owner: AztecAddress) -> Self { CardNote { points, - secret, + randomness, owner, header: NoteHeader::empty(), } } pub fn serialize(self) -> [Field; CARD_NOTE_LEN] { - [self.points as Field, self.secret, self.owner.to_field()] + [self.points as Field, self.randomness, self.owner.to_field()] } pub fn deserialize(serialized_note: [Field; CARD_NOTE_LEN]) -> Self { CardNote { points: serialized_note[0] as u8, - secret: serialized_note[1], + randomness: serialized_note[1], owner: AztecAddress::from_field(serialized_note[2]), header: NoteHeader::empty(), } @@ -51,7 +53,7 @@ impl CardNote { pub fn compute_note_hash(self) -> Field { pedersen_hash([ self.points as Field, - self.secret, + self.randomness, self.owner.to_field(), ],0) } diff --git a/yarn-project/noir-contracts/contracts/docs_example_contract/src/types/leader.nr b/yarn-project/noir-contracts/contracts/docs_example_contract/src/types/leader.nr new file mode 100644 index 00000000000..ca034261ec0 --- /dev/null +++ b/yarn-project/noir-contracts/contracts/docs_example_contract/src/types/leader.nr @@ -0,0 +1,23 @@ +use dep::aztec::protocol_types::address::AztecAddress; +use dep::aztec::types::type_serialization::TypeSerializationInterface; + +// Shows how to create a custom struct in Public +struct Leader { + account: AztecAddress, + points: u8, +} + +global LEADER_SERIALIZED_LEN: Field = 2; + +fn deserialize(fields: [Field; LEADER_SERIALIZED_LEN]) -> Leader { + Leader { account: AztecAddress::from_field(fields[0]), points: fields[1] as u8 } +} + +fn serialize(leader: Leader) -> [Field; LEADER_SERIALIZED_LEN] { + [leader.account.to_field(), leader.points as Field] +} + +global LeaderSerializationMethods = TypeSerializationInterface { + deserialize, + serialize, +};