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: crude stable var implementation #4289

Merged
merged 3 commits into from
Jan 31, 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
8 changes: 4 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ jobs:
name: "Test"
command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_sandbox_example.test.ts

e2e-singleton:
e2e-state-vars:
docker:
- image: aztecprotocol/alpine-build-image
resource_class: small
Expand All @@ -619,7 +619,7 @@ jobs:
- *setup_env
- run:
name: "Test"
command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_singleton.test.ts
command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_state_vars.test.ts

e2e-block-building:
docker:
Expand Down Expand Up @@ -1240,7 +1240,7 @@ workflows:
# TODO(3458): Investigate intermittent failure
# - e2e-slow-tree: *e2e_test
- e2e-sandbox-example: *e2e_test
- e2e-singleton: *e2e_test
- e2e-state-vars: *e2e_test
- e2e-block-building: *e2e_test
- e2e-nested-contract: *e2e_test
- e2e-non-contract-account: *e2e_test
Expand Down Expand Up @@ -1278,7 +1278,7 @@ workflows:
- e2e-token-contract
- e2e-blacklist-token-contract
- e2e-sandbox-example
- e2e-singleton
- e2e-state-vars
- e2e-block-building
- e2e-nested-contract
- e2e-non-contract-account
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
SideEffect,
TxContext,
} from '@aztec/circuits.js';
import { computeUniqueCommitment, siloCommitment } from '@aztec/circuits.js/abis';
import { computePublicDataTreeLeafSlot, computeUniqueCommitment, siloCommitment } from '@aztec/circuits.js/abis';
import { Grumpkin } from '@aztec/circuits.js/barretenberg';
import { FunctionAbi, FunctionArtifact, countArgumentsSize } from '@aztec/foundation/abi';
import { AztecAddress } from '@aztec/foundation/aztec-address';
Expand Down Expand Up @@ -436,4 +436,29 @@ export class ClientExecutionContext extends ViewDataOracle {
startSideEffectCounter,
);
}

/**
* Read the public storage data.
* @param startStorageSlot - The starting storage slot.
* @param numberOfElements - Number of elements to read from the starting storage slot.
*/
public async storageRead(startStorageSlot: Fr, numberOfElements: number): Promise<Fr[]> {
// TODO(#4320): This is a hack to work around not having directly access to the public data tree but
// still having access to the witnesses
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just expose the tree directly? Is it simply to get something to work before we decide to invest more time in this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look at #4320, it is because there need to be more plumbing in place. We straight up don't have access to the data through the node right now (in private execution).

const bn = await this.db.getBlockNumber();

const values = [];
for (let i = 0n; i < numberOfElements; i++) {
const storageSlot = new Fr(startStorageSlot.value + i);
const leafSlot = computePublicDataTreeLeafSlot(this.contractAddress, storageSlot);
const witness = await this.db.getPublicDataTreeWitness(bn, leafSlot);
if (!witness) {
throw new Error(`No witness for slot ${storageSlot.toString()}`);
}
const value = witness.leafPreimage.value;
this.log(`Oracle storage read: slot=${storageSlot.toString()} value=${value}`);
values.push(value);
}
return values;
}
}
18 changes: 13 additions & 5 deletions yarn-project/aztec-nr/aztec/src/history/public_value_inclusion.nr
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,20 @@ pub fn prove_public_value_inclusion(

// 4) Check that the witness matches the corresponding public_value
let preimage = witness.leaf_preimage;
if preimage.slot == public_value_leaf_slot {
assert_eq(preimage.value, value, "Public value does not match value in witness");

// Here we have two cases. Code based on same checks in `validate_public_data_reads` in `base_rollup_inputs`
// 1. The value is the same as the one in the witness
// 2. The value was never initialized and is zero
let is_less_than_slot = full_field_less_than(preimage.slot, public_value_leaf_slot);
let is_next_greater_than = full_field_less_than(public_value_leaf_slot, preimage.next_slot);
let is_max = ((preimage.next_index == 0) & (preimage.next_slot == 0));
let is_in_range = is_less_than_slot & (is_next_greater_than | is_max);

if is_in_range {
assert_eq(value, 0, "Non-existant public data leaf value is non-zero");
} else {
assert_eq(value, 0, "Got non-zero public value for non-existing slot");
assert(full_field_less_than(preimage.slot, public_value_leaf_slot), "Invalid witness range");
assert(full_field_less_than(public_value_leaf_slot, preimage.next_slot), "Invalid witness range");
assert_eq(preimage.slot, public_value_leaf_slot, "Public data slot don't match witness");
assert_eq(preimage.value, value, "Public value does not match the witness");
}

// 5) Prove that the leaf we validated is in the public data tree
Expand Down
1 change: 1 addition & 0 deletions yarn-project/aztec-nr/aztec/src/state_vars.nr
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ mod map;
mod public_state;
mod set;
mod singleton;
mod stable_public_state;
69 changes: 69 additions & 0 deletions yarn-project/aztec-nr/aztec/src/state_vars/stable_public_state.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use crate::context::{Context};
use crate::oracle::{
storage::{storage_read, storage_write},
};
use crate::history::public_value_inclusion::prove_public_value_inclusion;
use dep::std::option::Option;
use dep::protocol_types::traits::{Deserialize, Serialize};

struct StablePublicState<T>{
context: Context,
storage_slot: Field,
}

impl<T> StablePublicState<T> {
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.");
Self {
context,
storage_slot,
}
}

// Intended to be only called once.
pub fn initialize<T_SERIALIZED_LEN>(self, value: T) where T: Serialize<T_SERIALIZED_LEN> {
assert(self.context.private.is_none(), "Public state wrties only supported in public functions");
// TODO: Must throw if the storage slot is not empty -> cannot allow overwriting
// This is currently impractical, as public functions are never marked `is_contract_deployment`
// in the `call_context`, only private functions will have this flag set.
let fields = T::serialize(value);
storage_write(self.storage_slot, fields);
}

pub fn read_public<T_SERIALIZED_LEN>(self) -> T where T: Deserialize<T_SERIALIZED_LEN> {
assert(self.context.private.is_none(), "Public read only supported in public functions");
let fields = storage_read(self.storage_slot);
T::deserialize(fields)
}

pub fn read_private<T_SERIALIZED_LEN>(self) -> T where T: Deserialize<T_SERIALIZED_LEN> {
assert(self.context.public.is_none(), "Private read only supported in private functions");
let private_context = self.context.private.unwrap();

// Read the value from storage (using the public tree)
let fields = storage_read(self.storage_slot);

// TODO: The block_number here can be removed when using the current header in the membership proof.
let block_number = private_context.get_header().global_variables.block_number;

// Loop over the fields and prove their inclusion in the public tree
for i in 0..fields.len() {
// TODO: Update membership proofs to use current header (Requires #4179)
// Currently executing unnecessary computation:
// - a membership proof of the header(block_number) in the history
// - a membership proof of the value in the public tree of the header
prove_public_value_inclusion(
fields[i],
self.storage_slot + i,
block_number as u32,
(*private_context),
)
}
T::deserialize(fields)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,15 @@ describe('e2e_inclusion_proofs_contract', () => {
it('public value existence failure case', async () => {
// Choose random block number between first block and current block number to test archival node
const blockNumber = await getRandomBlockNumber();

const randomPublicValue = Fr.random();
await expect(
contract.methods.test_public_value_inclusion_proof(randomPublicValue, blockNumber).send().wait(),
).rejects.toThrow(/Public value does not match value in witness/);
).rejects.toThrow('Public value does not match the witness');
});

it('proves existence of uninitialized public value', async () => {
const blockNumber = await getRandomBlockNumber();
await contract.methods.test_public_unused_value_inclusion_proof(blockNumber).send().wait();
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DocsExampleContract } from '@aztec/noir-contracts';

import { setup } from './fixtures/utils.js';

describe('e2e_singleton', () => {
describe('e2e_state_vars', () => {
let wallet: Wallet;

let teardown: () => Promise<void>;
Expand All @@ -19,6 +19,24 @@ describe('e2e_singleton', () => {

afterAll(() => teardown());

describe('Stable Public State', () => {
it('private read of uninitialized stable', async () => {
const s = await contract.methods.get_stable().view();

const receipt2 = await contract.methods.match_stable(s.account, s.points).send().wait();
expect(receipt2.status).toEqual(TxStatus.MINED);
});

it('private read of initialized stable', async () => {
const receipt = await contract.methods.initialize_stable(1).send().wait();
expect(receipt.status).toEqual(TxStatus.MINED);
const s = await contract.methods.get_stable().view();

const receipt2 = await contract.methods.match_stable(s.account, s.points).send().wait();
expect(receipt2.status).toEqual(TxStatus.MINED);
}, 200_000);
});

describe('Singleton', () => {
it('fail to read uninitialized singleton', async () => {
expect(await contract.methods.is_legendary_initialized().view()).toEqual(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,14 @@ contract DocsExample {
address::AztecAddress,
};
use dep::aztec::{
oracle::{
debug_log::debug_log_format,
},
note::{
note_header::NoteHeader,
note_getter_options::{NoteGetterOptions, Comparator},
note_viewer_options::{NoteViewerOptions},
utils as note_utils,
},
context::{PrivateContext, PublicContext, Context},
state_vars::{map::Map, public_state::PublicState,singleton::Singleton, immutable_singleton::ImmutableSingleton, set::Set},
state_vars::{map::Map, public_state::PublicState,singleton::Singleton, immutable_singleton::ImmutableSingleton, set::Set, stable_public_state::StablePublicState},
};
// how to import methods from other files/folders within your workspace
use crate::options::create_account_card_getter_options;
Expand All @@ -49,6 +46,7 @@ contract DocsExample {
// docs:end:storage-map-singleton-declaration
test: Set<CardNote>,
imm_singleton: ImmutableSingleton<CardNote>,
stable_value: StablePublicState<Leader>,
}

impl Storage {
Expand All @@ -59,27 +57,47 @@ contract DocsExample {
1
),
// docs:start:start_vars_singleton
legendary_card: Singleton::new(context, 2),
legendary_card: Singleton::new(context, 3),
// docs:end:start_vars_singleton
// just used for docs example (not for game play):
// docs:start:state_vars-MapSingleton
profiles: Map::new(
context,
3,
4,
|context, slot| {
Singleton::new(context, slot)
},
),
// docs:end:state_vars-MapSingleton
test: Set::new(context, 4),
imm_singleton: ImmutableSingleton::new(context, 4),
test: Set::new(context, 5),
imm_singleton: ImmutableSingleton::new(context, 6),
stable_value: StablePublicState::new(context, 7),
}
}
}

#[aztec(private)]
fn constructor() {}

#[aztec(public)]
fn initialize_stable(points: u8) {
let mut new_leader = Leader { account: context.msg_sender(), points };
storage.stable_value.initialize(new_leader);
}

#[aztec(private)]
fn match_stable(account: AztecAddress, points: u8) {
let expected = Leader { account, points };
let read = storage.stable_value.read_private();

assert(read.account == expected.account, "Invalid account");
assert(read.points == expected.points, "Invalid points");
}

unconstrained fn get_stable() -> pub Leader {
storage.stable_value.read_public()
}

#[aztec(private)]
fn initialize_immutable_singleton(randomness: Field, points: u8) {
let mut new_card = CardNote::new(points, randomness, context.msg_sender());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ contract InclusionProofs {
struct Storage {
private_values: Map<AztecAddress, Set<ValueNote>>,
public_value: PublicState<Field>,
public_unused_value: PublicState<Field>,
}

impl Storage {
Expand All @@ -67,6 +68,10 @@ contract InclusionProofs {
context,
2, // Storage slot
),
public_unused_value: PublicState::new(
context,
3, // Storage slot
),
}
}
}
Expand Down Expand Up @@ -192,6 +197,17 @@ contract InclusionProofs {
// docs:end:prove_nullifier_inclusion
}

#[aztec(private)]
fn test_public_unused_value_inclusion_proof(block_number: u32 // The block at which we'll prove that the public value exists
) {
prove_public_value_inclusion(
0,
storage.public_unused_value.storage_slot,
block_number,
context
);
}

#[aztec(private)]
fn test_public_value_inclusion_proof(
public_value: Field,
Expand Down
Loading