Skip to content

Commit 5bab044

Browse files
authored
feat: SinglePrivateMutable (#18824)
Fixes F-187 Just like `PrivateMutable` but allows only for 1 instance per contract instead of 1 instance per contract-user pair. That 1 instance invariant is achieved by having a nullifier that has only the storage slot in it's preimage. This currently leaks that initialization happened - tackling this is a TODO.
2 parents fd2b3a1 + 1b23a3c commit 5bab044

File tree

7 files changed

+780
-22
lines changed

7 files changed

+780
-22
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod owned;
44
pub mod private_immutable;
55
pub mod single_private_immutable;
66
pub mod private_mutable;
7+
pub mod single_private_mutable;
78
pub mod public_immutable;
89
pub mod public_mutable;
910
pub mod private_set;
@@ -22,5 +23,6 @@ pub use crate::state_vars::private_set::PrivateSet;
2223
pub use crate::state_vars::public_immutable::PublicImmutable;
2324
pub use crate::state_vars::public_mutable::PublicMutable;
2425
pub use crate::state_vars::single_private_immutable::SinglePrivateImmutable;
26+
pub use crate::state_vars::single_private_mutable::SinglePrivateMutable;
2527
pub use crate::state_vars::single_use_claim::SingleUseClaim;
2628
pub use crate::state_vars::state_variable::StateVariable;
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
use crate::{
2+
context::{PrivateContext, UtilityContext},
3+
keys::getters::{get_nsk_app, get_public_keys},
4+
note::{
5+
lifecycle::{create_note, destroy_note_unsafe},
6+
note_getter::{get_note, view_note},
7+
note_interface::{NoteHash, NoteType},
8+
note_message::NoteMessage,
9+
},
10+
oracle::notes::check_nullifier_exists,
11+
state_vars::state_variable::StateVariable,
12+
};
13+
14+
use protocol_types::{
15+
address::AztecAddress,
16+
constants::GENERATOR_INDEX__INITIALIZATION_NULLIFIER,
17+
hash::poseidon2_hash_with_separator,
18+
traits::{Hash, Packable},
19+
};
20+
21+
mod test;
22+
23+
/// A state variable that holds a single private value that can be changed (unlike
24+
/// [crate::state_vars::private_mutable::PrivateMutable], which holds one private value _per account_ - hence
25+
/// the name 'single').
26+
///
27+
/// Because this private value has no semantic owner, it is up to the application to determine which accounts will
28+
/// learn of its existence via [crate::note::note_message::NoteMessage::deliver_to].
29+
///
30+
/// # Usage
31+
/// Unlike [crate::state_vars::private_immutable::PrivateImmutable] which is "owned" (requiring wrapping in an
32+
/// [crate::state_vars::owned::Owned] state variable), SinglePrivateMutable is used directly in storage:
33+
///
34+
/// ```noir
35+
/// #[storage]
36+
/// struct Storage<Context> {
37+
/// your_variable: SinglePrivateMutable<YourNote, Context>,
38+
/// }
39+
/// ```
40+
///
41+
/// Reading from a SinglePrivateMutable nullifies the current note, which restricts the use of this state variable to
42+
/// situations where race conditions are not a concern. This is commonly the case when dealing with admin-only
43+
/// functions.
44+
/// Given that this state variable works with private state it makes sense to use it only when we need the value
45+
/// to be known by a single individual or a closed set of parties.
46+
///
47+
/// # Examples
48+
///
49+
/// ## Account contract with signing key rotation
50+
///
51+
/// An account contract's signing key can be modeled as a SinglePrivateMutable<PublicKeyNote>. The "current value" of
52+
/// this state variable holds the active signing key. When the account owner wishes to update the signing key, they
53+
/// invoke the `replace` function, providing a new public key note as input.
54+
///
55+
/// ## Private Token
56+
///
57+
/// A private token's admin and total supply can be stored in two single private mutable state variables:
58+
/// ```noir
59+
/// #[storage]
60+
/// struct Storage<Context> {
61+
/// admin: SinglePrivateMutable<AddressNote, Context>,
62+
/// total_supply: SinglePrivateMutable<UintNote, Context>,
63+
/// balances: Owned<BalanceSet<Context>, Context>,
64+
/// }
65+
/// ```
66+
///
67+
/// When the mint or burn functions are invoked, we check that the message sender corresponds to the current value of
68+
/// the admin state variable. Then we update the total supply and balances. The resulting note message can then be
69+
/// delivered to an account whose encryption keys are known to a closed set of parties. With this approach, only
70+
/// the admin can modify the total supply of the token, but anyone with the relevant encryption keys can audit it.
71+
///
72+
///
73+
/// # Requirements
74+
///
75+
/// The contract that holds this state variable must have keys associated with it. This is because the initialization
76+
/// nullifier includes the contract's nullifying secret key (nsk) in its preimage. This is expected to not ever be a
77+
/// problem because the contracts that use SinglePrivateMutable generally have keys associated with them (account
78+
/// contracts or escrow contracts).
79+
///
80+
/// # Warning
81+
///
82+
/// The methods of this state variable that replace the underlying note return a [NoteMessage] that allows you to
83+
/// decide what method of note message delivery to use. Keep in mind that unless the caller of the relevant function is
84+
/// incentivized to deliver the note you should use constrained delivery or you are at a risk of a malicious actor
85+
/// bricking the contract.
86+
pub struct SinglePrivateMutable<Note, Context> {
87+
context: Context,
88+
storage_slot: Field,
89+
}
90+
91+
impl<Note, Context> StateVariable<1, Context> for SinglePrivateMutable<Note, Context> {
92+
fn new(context: Context, storage_slot: Field) -> Self {
93+
assert(storage_slot != 0, "Storage slot 0 not allowed. Storage slots must start from 1.");
94+
Self { context, storage_slot }
95+
}
96+
97+
fn get_storage_slot(self) -> Field {
98+
self.storage_slot
99+
}
100+
}
101+
102+
impl<Note, Context> SinglePrivateMutable<Note, Context> {
103+
/// Computes the initialization nullifier using the provided secret.
104+
fn compute_initialization_nullifier(self, secret: Field) -> Field {
105+
poseidon2_hash_with_separator(
106+
[self.storage_slot, secret],
107+
GENERATOR_INDEX__INITIALIZATION_NULLIFIER,
108+
)
109+
}
110+
}
111+
112+
impl<Note> SinglePrivateMutable<Note, &mut PrivateContext>
113+
where
114+
Note: NoteType + NoteHash,
115+
{
116+
/// Computes the nullifier that will be created when this SinglePrivateMutable is first initialized.
117+
///
118+
/// This function is primarily used internally by the `initialize` and `initialize_or_replace` methods, but may also
119+
/// be useful for contracts that need to check if a SinglePrivateMutable has been initialized.
120+
fn get_initialization_nullifier(self) -> Field {
121+
let contract_address = self.context.this_address();
122+
let contract_npk_m = get_public_keys(contract_address).npk_m;
123+
let contract_npk_m_hash = contract_npk_m.hash();
124+
let secret = self.context.request_nsk_app(contract_npk_m_hash);
125+
self.compute_initialization_nullifier(secret)
126+
}
127+
128+
/// Initializes a SinglePrivateMutable state variable instance with its first `note` and returns a [NoteMessage]
129+
/// that allows you to decide what method of note message delivery to use for the new note.
130+
///
131+
/// This function can only be called once per SinglePrivateMutable. Subsequent calls will fail because the
132+
/// initialization nullifier will already exist.
133+
pub fn initialize(self, note: Note, owner: AztecAddress) -> NoteMessage<Note>
134+
where
135+
Note: Packable,
136+
{
137+
// Nullify the storage slot.
138+
let nullifier = self.get_initialization_nullifier();
139+
self.context.push_nullifier(nullifier);
140+
141+
create_note(self.context, owner, self.storage_slot, note)
142+
}
143+
144+
/// Reads the current note of a SinglePrivateMutable state variable, nullifies it, and inserts a new note for
145+
/// `new_owner` produced by the provided function `f`.
146+
///
147+
/// This function returns a [NoteMessage] that allows you to decide what method of note message delivery to use for
148+
/// the new note.
149+
///
150+
/// This function implements a "read-and-replace" pattern for updating private state in Aztec. It first retrieves
151+
/// the current note, then nullifies it (marking it as spent), and finally inserts a `new_note` produced by the
152+
/// user-provided function `f`. The function `f` takes the current note and returns a new note that will replace the
153+
/// current note and become the "current value".
154+
///
155+
/// This function can only be called after the SinglePrivateMutable has been initialized. If called on an
156+
/// uninitialized SinglePrivateMutable, it will fail because there is no current note to replace. If you don't know
157+
/// if the state variable has been initialized already, you can use `initialize_or_replace` to handle both cases.
158+
///
159+
/// The nullification of the previous note ensures that it cannot be used again, maintaining the invariant that a
160+
/// SinglePrivateMutable has exactly one current note.
161+
pub fn replace<Env>(
162+
self,
163+
f: fn[Env](Note) -> Note,
164+
new_owner: AztecAddress,
165+
) -> NoteMessage<Note>
166+
where
167+
Note: Packable,
168+
{
169+
let (prev_retrieved_note, note_hash_read) =
170+
get_note(self.context, Option::none(), self.storage_slot);
171+
172+
// Nullify previous note.
173+
destroy_note_unsafe(self.context, prev_retrieved_note, note_hash_read);
174+
175+
let new_note = f(prev_retrieved_note.note);
176+
177+
// Add replacement note.
178+
create_note(self.context, new_owner, self.storage_slot, new_note)
179+
}
180+
181+
/// Initializes the SinglePrivateMutable if it's uninitialized, or replaces the current note using a transform
182+
/// function `f` while the new note is owned by `new_owner`. The function `f` takes an `Option` with the current
183+
/// `Note` and returns the `Note` to insert. The `Option` is `none` if the state variable was not initialized.
184+
///
185+
/// This function returns a [NoteMessage] that allows you to decide what method of note message delivery to use
186+
/// for the new note.
187+
pub fn initialize_or_replace<Env>(
188+
self,
189+
f: fn[Env](Option<Note>) -> Note,
190+
new_owner: AztecAddress,
191+
) -> NoteMessage<Note>
192+
where
193+
Note: Packable,
194+
{
195+
// Safety: `check_nullifier_exists` is an unconstrained function - we can constrain a true value
196+
// by providing an inclusion proof of the nullifier, but cannot constrain a false value since
197+
// a non-inclusion proof would only be valid if done in public.
198+
// Ultimately, this is not an issue given that we'll either:
199+
// - initialize the state variable, which would fail if it was already initialized due to the duplicate
200+
// nullifier, or
201+
// - replace the current value, which would fail if it was not initialized since we wouldn't be able
202+
// to produce an inclusion proof for the current note
203+
// This means that an honest oracle will assist the prover to produce a valid proof, while a malicious
204+
// oracle (i.e. one that returns an incorrect value for is_initialized) will simply fail to produce
205+
// a proof.
206+
let is_initialized = unsafe { check_nullifier_exists(self.get_initialization_nullifier()) };
207+
208+
let emission_new_note = if !is_initialized {
209+
self.initialize(f(Option::none()), new_owner).get_new_note()
210+
} else {
211+
self.replace(|note| f(Option::some(note)), new_owner).get_new_note()
212+
};
213+
214+
NoteMessage::new(emission_new_note, self.context)
215+
}
216+
217+
/// Reads the current note of a SinglePrivateMutable state variable instance. The read is performed by nullifying
218+
/// the current note and inserting a new note with the same value. By nullifying the current note, we ensure that
219+
/// we're reading the latest note.
220+
///
221+
/// This function returns a [NoteMessage] that allows you to decide what method of note message delivery to use
222+
/// for the new note.
223+
pub fn get_note(self) -> NoteMessage<Note>
224+
where
225+
Note: Packable,
226+
{
227+
let mut (retrieved_note, note_hash_read) =
228+
get_note(self.context, Option::none(), self.storage_slot);
229+
230+
destroy_note_unsafe(self.context, retrieved_note, note_hash_read);
231+
232+
create_note(
233+
self.context,
234+
retrieved_note.owner,
235+
self.storage_slot,
236+
retrieved_note.note,
237+
)
238+
}
239+
}
240+
241+
impl<Note> SinglePrivateMutable<Note, UtilityContext>
242+
where
243+
Note: NoteType + NoteHash + Eq,
244+
{
245+
/// Computes the nullifier that will be created when this SinglePrivateMutable is first initialized.
246+
unconstrained fn get_initialization_nullifier(self) -> Field {
247+
let contract_address = self.context.this_address();
248+
let contract_npk_m = get_public_keys(contract_address).npk_m;
249+
let contract_npk_m_hash = contract_npk_m.hash();
250+
let secret = get_nsk_app(contract_npk_m_hash);
251+
self.compute_initialization_nullifier(secret)
252+
}
253+
254+
/// Returns whether this SinglePrivateImmutable has been initialized.
255+
pub unconstrained fn is_initialized(self) -> bool {
256+
let nullifier = self.get_initialization_nullifier();
257+
check_nullifier_exists(nullifier)
258+
}
259+
260+
/// Returns the current note in this SinglePrivateMutable.
261+
pub unconstrained fn view_note(self) -> Note
262+
where
263+
Note: Packable,
264+
{
265+
// The note owner is set to none rather than msg_sender(), which means that anyone with access to this note in
266+
// the PXE can read it.
267+
view_note(Option::none(), self.storage_slot).note
268+
}
269+
}

0 commit comments

Comments
 (0)