|
| 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