Skip to content

Commit 94c0812

Browse files
authored
feat: assert initialization when reading PublicImmutable from private and utility context (#19957)
Fixes #15703 Summary - Adds initialization check when reading `PublicImmutable` from private and utility contexts by asserting the initialization nullifier exists - Updates documentation to explain how initialization verification works differently in private vs public contexts - Fixes test imports and expected error messages
2 parents 593df5b + 9a0a417 commit 94c0812

File tree

3 files changed

+47
-23
lines changed

3 files changed

+47
-23
lines changed

noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
use crate::{context::{PrivateContext, PublicContext, UtilityContext}, state_vars::StateVariable, utils::WithHash};
1+
use crate::{
2+
context::{PrivateContext, PublicContext, UtilityContext},
3+
nullifier::utils::compute_nullifier_existence_request,
4+
oracle::nullifiers::check_nullifier_exists,
5+
state_vars::StateVariable,
6+
utils::WithHash,
7+
};
28
use crate::protocol::{
39
constants::DOM_SEP__INITIALIZATION_NULLIFIER, hash::poseidon2_hash_with_separator, traits::Packable,
410
};
511

12+
mod test;
13+
614
/// Immutable public values.
715
///
816
/// This is one of the most basic public state variables. It is similar to an `immutable` or `constant` Solidity state
@@ -249,9 +257,15 @@ impl<T> PublicImmutable<T, UtilityContext> {
249257
where
250258
T: Packable + Eq,
251259
{
252-
// TODO(#15703): this fn should fail if the variable is not initialized
260+
assert(self.is_initialized(), "Trying to read from uninitialized PublicImmutable");
253261
WithHash::utility_public_storage_read(self.context, self.storage_slot)
254262
}
263+
264+
/// Returns true if the `PublicImmutable` has been initialized.
265+
pub unconstrained fn is_initialized(self) -> bool {
266+
let nullifier = self.compute_initialization_inner_nullifier();
267+
check_nullifier_exists(nullifier)
268+
}
255269
}
256270

257271
impl<T> PublicImmutable<T, &mut PrivateContext> {
@@ -270,10 +284,11 @@ impl<T> PublicImmutable<T, &mut PrivateContext> {
270284
///
271285
/// ## Cost
272286
///
273-
/// A historical public storage read at the anchor block is performed for a single storage slot, **regardless of
274-
/// `T`'s packed length**. This is because [PublicImmutable::initialize] stores not just the value but also its
275-
/// hash: this function obtains the preimage from an oracle and proves that it matches the hash from public
276-
/// storage.
287+
/// A nullifier existence request is pushed to the context, which will be verified by the kernel circuit.
288+
/// Additionally, a historical public storage read at the anchor block is performed for a single storage slot,
289+
/// **regardless of `T`'s packed length**. This is because [PublicImmutable::initialize] stores not just the value
290+
/// but also its hash: this function obtains the preimage from an oracle and proves that it matches the hash from
291+
/// public storage.
277292
///
278293
/// Because of this reason it is convenient to group together all of a contract's public immutable values that are
279294
/// read privately in a single type `T`:
@@ -302,7 +317,17 @@ impl<T> PublicImmutable<T, &mut PrivateContext> {
302317
where
303318
T: Packable + Eq,
304319
{
305-
// TODO(#15703): this fn should fail if the variable is not initialized
320+
let nullifier = self.compute_initialization_inner_nullifier();
321+
322+
// Safety: We use this check to be able to test this function works properly on synthetic envs
323+
// like TXE. We assert the returned value only to provide a clear error message. The actual
324+
// constrained check that the nullifier exists is done below with `assert_nullifier_exists`
325+
// We should improve our synthetic envs because this check forces an unnecesary roundtrip
326+
let nullifier_exists = unsafe { check_nullifier_exists(nullifier) };
327+
assert(nullifier_exists, "Trying to read from uninitialized PublicImmutable");
328+
329+
let nullifier_existence_request = compute_nullifier_existence_request(nullifier, self.context.this_address());
330+
self.context.assert_nullifier_exists(nullifier_existence_request);
306331
WithHash::historical_public_storage_read(
307332
self.context.get_anchor_block_header(),
308333
self.context.this_address(),

noir-projects/aztec-nr/aztec/src/state_vars/public_immutable/test.nr

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
use crate::{context::{PrivateContext, PublicContext, UtilityContext}, state_vars::public_immutable::PublicImmutable};
1+
use crate::{
2+
context::{PrivateContext, PublicContext, UtilityContext},
3+
state_vars::{public_immutable::PublicImmutable, StateVariable},
4+
};
25
use crate::test::{helpers::test_environment::TestEnvironment, mocks::MockStruct};
3-
use std::mem::zeroed;
46

57
global storage_slot: Field = 7;
68

@@ -99,25 +101,23 @@ unconstrained fn read_uninitialized_public_fails() {
99101
});
100102
}
101103

102-
// TODO(#15703): this test should be made to fail
103-
#[test]
104-
unconstrained fn read_uninitialized_private_returns_default() {
104+
#[test(should_fail_with = "Trying to read from uninitialized PublicImmutable")]
105+
unconstrained fn read_uninitialized_private_fails() {
105106
let env = TestEnvironment::new();
106107

107108
env.private_context(|context| {
108109
let state_var = in_private(context);
109-
assert_eq(state_var.read(), zeroed());
110+
let _ = state_var.read();
110111
});
111112
}
112113

113-
// TODO(#15703): this test should be made to fail
114-
#[test]
115-
unconstrained fn read_uninitialized_utility_returns_default() {
114+
#[test(should_fail_with = "Trying to read from uninitialized PublicImmutable")]
115+
unconstrained fn read_uninitialized_utility_fails() {
116116
let env = TestEnvironment::new();
117117

118118
env.utility_context(|context| {
119119
let state_var = in_utility(context);
120-
assert_eq(state_var.read(), zeroed());
120+
let _ = state_var.read();
121121
});
122122
}
123123

yarn-project/end-to-end/src/e2e_state_vars.test.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,10 @@ describe('e2e_state_vars', () => {
3939
afterAll(() => teardown());
4040

4141
describe('PublicImmutable', () => {
42-
it('private read of uninitialized PublicImmutable', async () => {
43-
const s = await contract.methods.get_public_immutable().simulate({ from: defaultAccountAddress });
44-
45-
// Send the transaction and wait for it to be mined (wait function throws if the tx is not mined)
46-
await contract.methods.match_public_immutable(s.account, s.value).send({ from: defaultAccountAddress });
42+
it('private read of uninitialized PublicImmutable should fail', async () => {
43+
await expect(
44+
contract.methods.get_public_immutable_constrained_private().simulate({ from: defaultAccountAddress }),
45+
).rejects.toThrow('Trying to read from uninitialized PublicImmutable');
4746
});
4847

4948
it('initialize and read PublicImmutable', async () => {
@@ -57,7 +56,7 @@ describe('e2e_state_vars', () => {
5756
expect(read).toEqual({ account: defaultAccountAddress, value: read.value });
5857
});
5958

60-
it('private read of PublicImmutable', async () => {
59+
it('private read of initialized PublicImmutable', async () => {
6160
// Reads the value using a utility function checking the return values with:
6261
// 1. A constrained private function that reads it directly
6362
// 2. A constrained private function that calls another private function that reads.

0 commit comments

Comments
 (0)