Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: PublicImmutable impl #4758

Merged
merged 7 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions noir-projects/aztec-nr/aztec/src/state_vars.nr
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod map;
mod private_immutable;
mod private_mutable;
mod public_immutable;
mod public_mutable;
mod private_set;
mod shared_immutable;
Expand All @@ -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;
Expand Down
56 changes: 56 additions & 0 deletions noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr
Original file line number Diff line number Diff line change
@@ -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<T> {
context: Context,
storage_slot: Field,
}
// docs:end:public_immutable_struct

impl<T> Storage<T> for PublicImmutable<T> {}

impl<T> PublicImmutable<T> {
// 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<T_SERIALIZED_LEN>(self, value: T) where T: Serialize<T_SERIALIZED_LEN> {
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<T_SERIALIZED_LEN>(self) -> T where T: Deserialize<T_SERIALIZED_LEN> {
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>{
context: Context,
storage_slot: Field,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -48,8 +48,13 @@ contract DocsExample {
// docs:start:storage-minters-declaration
minters: Map<AztecAddress, PublicMutable<bool>>,
// docs:end:storage-minters-declaration
// docs:start:storage-public-immutable-declaration
public_immutable: PublicImmutable<Leader>,
// 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 {
Expand Down Expand Up @@ -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
}
}
}
Expand All @@ -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) {
Expand Down
38 changes: 26 additions & 12 deletions yarn-project/end-to-end/src/e2e_state_vars.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 () => {
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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);
Expand Down
Loading