Skip to content

Commit

Permalink
feat: PublicImmutable impl (#4758)
Browse files Browse the repository at this point in the history
Fixes #4757
  • Loading branch information
benesjan authored Feb 29, 2024
1 parent 6582b09 commit 87c976b
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 15 deletions.
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

0 comments on commit 87c976b

Please sign in to comment.