diff --git a/docs/docs/developers/contracts/references/storage/public_state.md b/docs/docs/developers/contracts/references/storage/public_state.md index 4344c0b0d10..8a17b03842e 100644 --- a/docs/docs/developers/contracts/references/storage/public_state.md +++ b/docs/docs/developers/contracts/references/storage/public_state.md @@ -89,9 +89,34 @@ 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. + +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 + +### `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.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 new file mode 100644 index 00000000000..f3fc0828bd6 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr @@ -0,0 +1,56 @@ +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, + 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 can only be initialized from public functions" + ); + // 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 + + // 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 +} 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 16b15e6ab10..bdcaeaae60e 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 e528a39f047..de6e6b629db 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 @@ -20,7 +20,7 @@ contract DocsExample { note_viewer_options::{NoteViewerOptions}, 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; @@ -48,8 +48,13 @@ 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 { @@ -83,7 +88,10 @@ contract DocsExample { |context, slot| { PublicMutable::new(context, slot) } - )// docs:end:storage-minters-init + ), + // docs:end:storage-minters-init + // docs:start:storage-public-immutable + public_immutable: PublicImmutable::new(context, 9)// docs:end:storage-public-immutable } } } @@ -110,6 +118,20 @@ contract DocsExample { storage.shared_immutable.read_public() } + #[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 #[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);