From 5cc9f1fb11645af9d7adf5c2824284c24684eb99 Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 26 Feb 2024 10:26:40 +0000 Subject: [PATCH 1/5] feat: PublicImmutable impl --- .../aztec/src/state_vars/public_immutable.nr | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr b/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr new file mode 100644 index 00000000000..bbda1e0f509 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr @@ -0,0 +1,46 @@ +use crate::context::{Context}; +use crate::oracle::storage::storage_read; +use crate::oracle::storage::storage_write; +use dep::std::option::Option; +use dep::protocol_types::traits::{Deserialize, Serialize}; +use crate::state_vars::storage::Storage; + +// docs:start:public_immutable_struct +struct PublicImmutable { + context: Context, + storage_slot: Field, +} +// docs:end:public_immutable_struct + +impl Storage for PublicImmutable {} + +impl PublicImmutable { + // docs:start:public_immutable_struct_new + pub fn new( + // Note: Passing the contexts to new(...) just to have an interface compatible with a Map. + context: Context, + storage_slot: Field + ) -> Self { + assert(storage_slot != 0, "Storage slot 0 not allowed. Storage slots must start from 1."); + PublicImmutable { context, storage_slot } + } + // docs:end:public_immutable_struct_new + + // docs:start:public_immutable_struct_write + pub fn initialize(self, value: T) where T: Serialize { + assert( + self.context.private.is_none(), "PublicImmutable initialization only supported in public functions" + ); + let fields = T::serialize(value); + storage_write(self.storage_slot, fields); + } + // docs:end:public_immutable_struct_write + + // docs:start:public_immutable_struct_read + pub fn read(self) -> T where T: Deserialize { + assert(self.context.private.is_none(), "PublicImmutable reads only supported in public functions"); + let fields = storage_read(self.storage_slot); + T::deserialize(fields) + } + // docs:end:public_immutable_struct_read +} From 9edf1cb235cb33ef5eac81bd252fc711dcd929eb Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 27 Feb 2024 09:41:04 +0000 Subject: [PATCH 2/5] WIP --- .../aztec-nr/aztec/src/state_vars.nr | 2 ++ .../aztec/src/state_vars/public_immutable.nr | 27 ++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/state_vars.nr b/noir-projects/aztec-nr/aztec/src/state_vars.nr index 4201723eb07..f10b2ac487e 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars.nr @@ -1,6 +1,7 @@ mod map; mod private_immutable; mod private_mutable; +mod public_immutable; mod public_mutable; mod private_set; mod shared_immutable; @@ -9,6 +10,7 @@ mod storage; use crate::state_vars::map::Map; use crate::state_vars::private_immutable::PrivateImmutable; use crate::state_vars::private_mutable::PrivateMutable; +use crate::state_vars::public_immutable::PublicImmutable; use crate::state_vars::public_mutable::PublicMutable; use crate::state_vars::private_set::PrivateSet; use crate::state_vars::shared_immutable::SharedImmutable; diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr b/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr index bbda1e0f509..17dd73efed6 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr @@ -1,9 +1,5 @@ -use crate::context::{Context}; -use crate::oracle::storage::storage_read; -use crate::oracle::storage::storage_write; -use dep::std::option::Option; -use dep::protocol_types::traits::{Deserialize, Serialize}; -use crate::state_vars::storage::Storage; +use crate::{context::Context, oracle::{storage::{storage_read, storage_write}}, state_vars::storage::Storage}; +use dep::protocol_types::{constants::INITIALIZATION_SLOT_SEPARATOR, traits::{Deserialize, Serialize}}; // docs:start:public_immutable_struct struct PublicImmutable { @@ -29,10 +25,23 @@ impl PublicImmutable { // docs:start:public_immutable_struct_write pub fn initialize(self, value: T) where T: Serialize { assert( - self.context.private.is_none(), "PublicImmutable initialization only supported in public functions" + self.context.private.is_none(), "PublicImmutable can only be initialized from public functions" ); - let fields = T::serialize(value); - storage_write(self.storage_slot, fields); + // TODO(#4738): Uncomment the following assert + // assert( + // self.context.public.unwrap_unchecked().is_deployment(), "PublicImmutable can only be initialized during contract deployment" + // ); + + // We check that the struct is not yet initialized by checking if the initialization slot is 0 + let initialization_slot = INITIALIZATION_SLOT_SEPARATOR + self.storage_slot; + let fields_read: [Field; 1] = storage_read(initialization_slot); + assert(fields_read[0] == 0, "PublicImmutable already initialized"); + + // We populate the initialization slot with a non-zero value to indicate that the struct is initialized + storage_write(initialization_slot, [0xdead]); + + let fields_write = T::serialize(value); + storage_write(self.storage_slot, fields_write); } // docs:end:public_immutable_struct_write From 255b56ae9ffaecffa7a18259b9b79a18191a2c2d Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 27 Feb 2024 10:06:06 +0000 Subject: [PATCH 3/5] testing PublicImmutable --- .../docs_example_contract/src/main.nr | 55 ++++--------------- .../end-to-end/src/e2e_state_vars.test.ts | 38 +++++++++---- 2 files changed, 38 insertions(+), 55 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr b/noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr index 9e246f32e80..ae8665438c5 100644 --- a/noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr @@ -25,7 +25,7 @@ contract DocsExample { utils as note_utils, }, context::{PrivateContext, PublicContext, Context}, - state_vars::{Map, PublicMutable, PrivateMutable, PrivateImmutable, PrivateSet, SharedImmutable}, + state_vars::{Map, PublicMutable, PublicImmutable, PrivateMutable, PrivateImmutable, PrivateSet, SharedImmutable}, }; // how to import methods from other files/folders within your workspace use crate::options::create_account_card_getter_options; @@ -56,48 +56,7 @@ contract DocsExample { // docs:start:storage-minters-declaration minters: Map>, // docs:end:storage-minters-declaration - } - - impl Storage { - fn init(context: Context) -> Self { - Storage { - // docs:start:storage-leader-init - leader: PublicMutable::new( - context, - 1 - ), - // docs:end:storage-leader-init - // docs:start:start_vars_private_mutable - legendary_card: PrivateMutable::new(context, 3), - // docs:end:start_vars_private_mutable - // just used for docs example (not for game play): - // docs:start:state_vars-MapPrivateMutable - profiles: Map::new( - context, - 4, - |context, slot| { - PrivateMutable::new(context, slot) - }, - ), - // docs:end:state_vars-MapPrivateMutable - // docs:start:storage-set-init - set: PrivateSet::new(context, 5), - // docs:end:storage-set-init - private_immutable: PrivateImmutable::new(context, 6), - // docs:start:storage-shared-immutable - shared_immutable: SharedImmutable::new(context, 7), - // docs:end:storage-shared-immutable - // docs:start:storage-minters-init - minters: Map::new( - context, - 8, - |context, slot| { - PublicMutable::new(context, slot) - }, - ), - // docs:end:storage-minters-init - } - } + public_immutable: PublicImmutable, } #[aztec(private)] @@ -122,6 +81,16 @@ contract DocsExample { storage.shared_immutable.read_public() } + #[aztec(public)] + fn initialize_public_immutable(points: u8) { + let mut new_leader = Leader { account: context.msg_sender(), points }; + storage.public_immutable.initialize(new_leader); + } + + unconstrained fn get_public_immutable() -> pub Leader { + storage.public_immutable.read() + } + // docs:start:initialize-private-mutable #[aztec(private)] fn initialize_private_immutable(randomness: Field, points: u8) { diff --git a/yarn-project/end-to-end/src/e2e_state_vars.test.ts b/yarn-project/end-to-end/src/e2e_state_vars.test.ts index 6365d289bc4..79d84e5da15 100644 --- a/yarn-project/end-to-end/src/e2e_state_vars.test.ts +++ b/yarn-project/end-to-end/src/e2e_state_vars.test.ts @@ -1,4 +1,4 @@ -import { TxStatus, Wallet } from '@aztec/aztec.js'; +import { Wallet } from '@aztec/aztec.js'; import { DocsExampleContract } from '@aztec/noir-contracts.js'; import { setup } from './fixtures/utils.js'; @@ -23,17 +23,15 @@ describe('e2e_state_vars', () => { it('private read of uninitialized SharedImmutable', async () => { const s = await contract.methods.get_shared_immutable().view(); - const receipt2 = await contract.methods.match_shared_immutable(s.account, s.points).send().wait(); - expect(receipt2.status).toEqual(TxStatus.MINED); + // Send the transaction and wait for it to be mined (wait function throws if the tx is not mined) + await contract.methods.match_shared_immutable(s.account, s.points).send().wait(); }); it('private read of initialized SharedImmutable', async () => { - const receipt = await contract.methods.initialize_shared_immutable(1).send().wait(); - expect(receipt.status).toEqual(TxStatus.MINED); + await contract.methods.initialize_shared_immutable(1).send().wait(); const s = await contract.methods.get_shared_immutable().view(); - const receipt2 = await contract.methods.match_shared_immutable(s.account, s.points).send().wait(); - expect(receipt2.status).toEqual(TxStatus.MINED); + await contract.methods.match_shared_immutable(s.account, s.points).send().wait(); }, 200_000); it('initializing SharedImmutable the second time should fail', async () => { @@ -45,6 +43,26 @@ describe('e2e_state_vars', () => { }, 100_000); }); + describe('PublicImmutable', () => { + it('initialize and read public immutable', async () => { + const numPoints = 1n; + + await contract.methods.initialize_public_immutable(numPoints).send().wait(); + const p = await contract.methods.get_public_immutable().view(); + + expect(p.account).toEqual(wallet.getCompleteAddress().address); + expect(p.points).toEqual(numPoints); + }, 200_000); + + it('initializing PublicImmutable the second time should fail', async () => { + // Jest executes the tests sequentially and the first call to initialize_public_immutable was executed + // in the previous test, so the call bellow should fail. + await expect(contract.methods.initialize_public_immutable(1).send().wait()).rejects.toThrowError( + "Assertion failed: PublicImmutable already initialized 'fields_read[0] == 0'", + ); + }, 100_000); + }); + describe('PrivateMutable', () => { it('fail to read uninitialized PrivateMutable', async () => { expect(await contract.methods.is_legendary_initialized().view()).toEqual(false); @@ -53,8 +71,8 @@ describe('e2e_state_vars', () => { it('initialize PrivateMutable', async () => { expect(await contract.methods.is_legendary_initialized().view()).toEqual(false); + // Send the transaction and wait for it to be mined (wait function throws if the tx is not mined) const receipt = await contract.methods.initialize_private(RANDOMNESS, POINTS).send().wait(); - expect(receipt.status).toEqual(TxStatus.MINED); const tx = await wallet.getTx(receipt.txHash); expect(tx?.newNoteHashes.length).toEqual(1); @@ -80,7 +98,6 @@ describe('e2e_state_vars', () => { expect(await contract.methods.is_legendary_initialized().view()).toEqual(true); const noteBefore = await contract.methods.get_legendary_card().view(); const receipt = await contract.methods.update_legendary_card(RANDOMNESS, POINTS).send().wait(); - expect(receipt.status).toEqual(TxStatus.MINED); const tx = await wallet.getTx(receipt.txHash); expect(tx?.newNoteHashes.length).toEqual(1); @@ -105,7 +122,6 @@ describe('e2e_state_vars', () => { .update_legendary_card(RANDOMNESS + 2n, POINTS + 1n) .send() .wait(); - expect(receipt.status).toEqual(TxStatus.MINED); const tx = await wallet.getTx(receipt.txHash); expect(tx?.newNoteHashes.length).toEqual(1); // 1 for the tx, another for the nullifier of the previous note @@ -120,7 +136,6 @@ describe('e2e_state_vars', () => { expect(await contract.methods.is_legendary_initialized().view()).toEqual(true); const noteBefore = await contract.methods.get_legendary_card().view(); const receipt = await contract.methods.increase_legendary_points().send().wait(); - expect(receipt.status).toEqual(TxStatus.MINED); const tx = await wallet.getTx(receipt.txHash); expect(tx?.newNoteHashes.length).toEqual(1); // 1 for the tx, another for the nullifier of the previous note @@ -141,7 +156,6 @@ describe('e2e_state_vars', () => { it('initialize PrivateImmutable', async () => { expect(await contract.methods.is_priv_imm_initialized().view()).toEqual(false); const receipt = await contract.methods.initialize_private_immutable(RANDOMNESS, POINTS).send().wait(); - expect(receipt.status).toEqual(TxStatus.MINED); const tx = await wallet.getTx(receipt.txHash); expect(tx?.newNoteHashes.length).toEqual(1); From c0f75b8a928706f46d5a82dcdb704ad3dc35e990 Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 27 Feb 2024 10:23:09 +0000 Subject: [PATCH 4/5] WIP --- .../references/storage/public_state.md | 35 +++++++++++- .../aztec/src/state_vars/public_immutable.nr | 1 + .../aztec/src/state_vars/shared_immutable.nr | 1 + .../docs_example_contract/src/main.nr | 53 +++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) diff --git a/docs/docs/developers/contracts/references/storage/public_state.md b/docs/docs/developers/contracts/references/storage/public_state.md index 4344c0b0d10..8660ccc43ff 100644 --- a/docs/docs/developers/contracts/references/storage/public_state.md +++ b/docs/docs/developers/contracts/references/storage/public_state.md @@ -89,9 +89,42 @@ We have a `write` method on the `PublicMutable` struct that takes the value to w --- +## Public Immutable + +`PublicImmutable` is a type that can be written once during a contract deployment and read later on from public only. + +This makes the immutable public variables useful for stuff that you would usually have in `immutable` values in solidity. For example this can be the name of a token or its number of decimals. + +Just like the `PublicMutable` it is generic over the variable type `T`. The type `MUST` implement Serialize and Deserialize traits. + +You can find the details of `PublicImmutable` in the implementation [here](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr). + +### `new` + +Is done exactly like the `PublicMutable` struct, but with the `PublicImmutable` struct. + +#include_code storage-public-immutable-declaration /noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr rust + +#include_code storage-public-immutable /noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr rust + +### `initialize` + +#include_code initialize_public_immutable /noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr rust + +:::warning Should only be called as part of the deployment. +If this is called outside the deployment transaction multiple values could be used down the line, potentially breaking the contract. + +Currently this is not constrained as we are in the middle of changing deployments. +::: + +### `read` + +Reading the value is just like `PublicMutable`. +#include_code read_public_immutable /noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr rust + ## Shared Immutable -`SharedImmutable` (formerly known as `StablePublicState`) is a special type that can be read from both public and private! +`SharedImmutable` (formerly known as `StablePublicState`) is a type which is very similar to `PublicImmutable` but with an addition of a private getter (can be read from private). Since private execution is based on historical data, the user can pick ANY of its prior values to read from. This is why it `MUST` not be updated after the contract is deployed. The variable should be initialized at the constructor and then never changed. diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr b/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr index 17dd73efed6..f3fc0828bd6 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr @@ -1,6 +1,7 @@ use crate::{context::Context, oracle::{storage::{storage_read, storage_write}}, state_vars::storage::Storage}; use dep::protocol_types::{constants::INITIALIZATION_SLOT_SEPARATOR, traits::{Deserialize, Serialize}}; +// Just like SharedImmutable but without the ability to read from private functions. // docs:start:public_immutable_struct struct PublicImmutable { context: Context, diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/shared_immutable.nr b/noir-projects/aztec-nr/aztec/src/state_vars/shared_immutable.nr index 8720a8f58c5..4f84f6d859a 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/shared_immutable.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/shared_immutable.nr @@ -4,6 +4,7 @@ use crate::{ }; use dep::protocol_types::{constants::INITIALIZATION_SLOT_SEPARATOR, traits::{Deserialize, Serialize}}; +// Just like PublicImmutable but with the ability to read from private functions. struct SharedImmutable{ context: Context, storage_slot: Field, diff --git a/noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr b/noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr index ae8665438c5..6d4e485bd96 100644 --- a/noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr @@ -56,7 +56,56 @@ contract DocsExample { // docs:start:storage-minters-declaration minters: Map>, // docs:end:storage-minters-declaration + // docs:start:storage-public-immutable-declaration public_immutable: PublicImmutable, + // docs:end:storage-public-immutable-declaration + } + + // Note: The following is no longer necessary to implement manually as our macros do this for us. It is left here + // for documentation purposes only. + impl Storage { + fn init(context: Context) -> Self { + Storage { + // docs:start:storage-leader-init + leader: PublicMutable::new( + context, + 1 + ), + // docs:end:storage-leader-init + // docs:start:start_vars_private_mutable + legendary_card: PrivateMutable::new(context, 3), + // docs:end:start_vars_private_mutable + // just used for docs example (not for game play): + // docs:start:state_vars-MapPrivateMutable + profiles: Map::new( + context, + 4, + |context, slot| { + PrivateMutable::new(context, slot) + }, + ), + // docs:end:state_vars-MapPrivateMutable + // docs:start:storage-set-init + set: PrivateSet::new(context, 5), + // docs:end:storage-set-init + private_immutable: PrivateImmutable::new(context, 6), + // docs:start:storage-shared-immutable + shared_immutable: SharedImmutable::new(context, 7), + // docs:end:storage-shared-immutable + // docs:start:storage-minters-init + minters: Map::new( + context, + 8, + |context, slot| { + PublicMutable::new(context, slot) + }, + ), + // docs:end:storage-minters-init + // docs:start:storage-public-immutable + public_immutable: PublicImmutable::new(context, 9), + // docs:end:storage-public-immutable + } + } } #[aztec(private)] @@ -83,12 +132,16 @@ contract DocsExample { #[aztec(public)] fn initialize_public_immutable(points: u8) { + // docs:start:initialize_public_immutable let mut new_leader = Leader { account: context.msg_sender(), points }; storage.public_immutable.initialize(new_leader); + // docs:end:initialize_public_immutable } unconstrained fn get_public_immutable() -> pub Leader { + // docs:start:read_public_immutable storage.public_immutable.read() + // docs:end:read_public_immutable } // docs:start:initialize-private-mutable From 215bafb70bca16e4657a9e723d7f2abe2ba77f1b Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 29 Feb 2024 08:19:37 +0000 Subject: [PATCH 5/5] docs fixes --- .../contracts/references/storage/public_state.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/docs/developers/contracts/references/storage/public_state.md b/docs/docs/developers/contracts/references/storage/public_state.md index 8660ccc43ff..8a17b03842e 100644 --- a/docs/docs/developers/contracts/references/storage/public_state.md +++ b/docs/docs/developers/contracts/references/storage/public_state.md @@ -93,8 +93,6 @@ We have a `write` method on the `PublicMutable` struct that takes the value to w `PublicImmutable` is a type that can be written once during a contract deployment and read later on from public only. -This makes the immutable public variables useful for stuff that you would usually have in `immutable` values in solidity. For example this can be the name of a token or its number of decimals. - Just like the `PublicMutable` it is generic over the variable type `T`. The type `MUST` implement Serialize and Deserialize traits. You can find the details of `PublicImmutable` in the implementation [here](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr). @@ -111,12 +109,6 @@ Is done exactly like the `PublicMutable` struct, but with the `PublicImmutable` #include_code initialize_public_immutable /noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr rust -:::warning Should only be called as part of the deployment. -If this is called outside the deployment transaction multiple values could be used down the line, potentially breaking the contract. - -Currently this is not constrained as we are in the middle of changing deployments. -::: - ### `read` Reading the value is just like `PublicMutable`.