Skip to content

Commit

Permalink
feat: crude stable var implementation (AztecProtocol#4289)
Browse files Browse the repository at this point in the history
Initial crude implementation for AztecProtocol#4130.
The way I am getting a hold of the public values through the oracle in here is an abomination. AztecProtocol#4320 is created to fix this.
  • Loading branch information
LHerskind committed Jan 31, 2024
1 parent 1faead5 commit 5f9eee4
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 21 deletions.
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
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

0 comments on commit 5f9eee4

Please sign in to comment.