diff --git a/.circleci/config.yml b/.circleci/config.yml index cf0b8686481..0fae211493d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 @@ -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: @@ -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 @@ -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 diff --git a/yarn-project/acir-simulator/src/client/client_execution_context.ts b/yarn-project/acir-simulator/src/client/client_execution_context.ts index 73f9d346792..f3d684f4ef0 100644 --- a/yarn-project/acir-simulator/src/client/client_execution_context.ts +++ b/yarn-project/acir-simulator/src/client/client_execution_context.ts @@ -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'; @@ -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 { + // 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; + } } diff --git a/yarn-project/aztec-nr/aztec/src/history/public_value_inclusion.nr b/yarn-project/aztec-nr/aztec/src/history/public_value_inclusion.nr index 62c65bfd858..ad14fb77cb3 100644 --- a/yarn-project/aztec-nr/aztec/src/history/public_value_inclusion.nr +++ b/yarn-project/aztec-nr/aztec/src/history/public_value_inclusion.nr @@ -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 diff --git a/yarn-project/aztec-nr/aztec/src/state_vars.nr b/yarn-project/aztec-nr/aztec/src/state_vars.nr index e1e813891ff..d8213bb1ef0 100644 --- a/yarn-project/aztec-nr/aztec/src/state_vars.nr +++ b/yarn-project/aztec-nr/aztec/src/state_vars.nr @@ -3,3 +3,4 @@ mod map; mod public_state; mod set; mod singleton; +mod stable_public_state; diff --git a/yarn-project/aztec-nr/aztec/src/state_vars/stable_public_state.nr b/yarn-project/aztec-nr/aztec/src/state_vars/stable_public_state.nr new file mode 100644 index 00000000000..013f059aef0 --- /dev/null +++ b/yarn-project/aztec-nr/aztec/src/state_vars/stable_public_state.nr @@ -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{ + context: Context, + storage_slot: Field, +} + +impl StablePublicState { + 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(self, value: T) where T: Serialize { + 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(self) -> T where T: Deserialize { + 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(self) -> T where T: Deserialize { + 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) + } + +} diff --git a/yarn-project/end-to-end/src/e2e_inclusion_proofs_contract.test.ts b/yarn-project/end-to-end/src/e2e_inclusion_proofs_contract.test.ts index 287d3fc27f1..87821e95d11 100644 --- a/yarn-project/end-to-end/src/e2e_inclusion_proofs_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_inclusion_proofs_contract.test.ts @@ -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(); }); }); diff --git a/yarn-project/end-to-end/src/e2e_singleton.test.ts b/yarn-project/end-to-end/src/e2e_state_vars.test.ts similarity index 89% rename from yarn-project/end-to-end/src/e2e_singleton.test.ts rename to yarn-project/end-to-end/src/e2e_state_vars.test.ts index 6c1407ef053..0ad3a75a04e 100644 --- a/yarn-project/end-to-end/src/e2e_singleton.test.ts +++ b/yarn-project/end-to-end/src/e2e_state_vars.test.ts @@ -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; @@ -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); diff --git a/yarn-project/noir-contracts/contracts/docs_example_contract/src/main.nr b/yarn-project/noir-contracts/contracts/docs_example_contract/src/main.nr index 9300ef77ecd..a87b7c960cc 100644 --- a/yarn-project/noir-contracts/contracts/docs_example_contract/src/main.nr +++ b/yarn-project/noir-contracts/contracts/docs_example_contract/src/main.nr @@ -18,9 +18,6 @@ contract DocsExample { address::AztecAddress, }; use dep::aztec::{ - oracle::{ - debug_log::debug_log_format, - }, note::{ note_header::NoteHeader, note_getter_options::{NoteGetterOptions, Comparator}, @@ -28,7 +25,7 @@ contract DocsExample { 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; @@ -49,6 +46,7 @@ contract DocsExample { // docs:end:storage-map-singleton-declaration test: Set, imm_singleton: ImmutableSingleton, + stable_value: StablePublicState, } impl Storage { @@ -59,20 +57,21 @@ 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), } } } @@ -80,6 +79,25 @@ contract DocsExample { #[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()); diff --git a/yarn-project/noir-contracts/contracts/inclusion_proofs_contract/src/main.nr b/yarn-project/noir-contracts/contracts/inclusion_proofs_contract/src/main.nr index 12a3a797029..37517063ad3 100644 --- a/yarn-project/noir-contracts/contracts/inclusion_proofs_contract/src/main.nr +++ b/yarn-project/noir-contracts/contracts/inclusion_proofs_contract/src/main.nr @@ -51,6 +51,7 @@ contract InclusionProofs { struct Storage { private_values: Map>, public_value: PublicState, + public_unused_value: PublicState, } impl Storage { @@ -67,6 +68,10 @@ contract InclusionProofs { context, 2, // Storage slot ), + public_unused_value: PublicState::new( + context, + 3, // Storage slot + ), } } } @@ -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,