Skip to content

Commit ef0d467

Browse files
committed
feat!: make initialize_or_replace take an option
Follow up on #17017, this change allows for potentially cheaper circuits by not forcing the caller to compute the initial value (which might be expensive) unless needed. The multiple `Option` methods such as `unwrap_or`, `unwrap_or_else`, etc., make handling this simple. E.g. a counter becomes ```noir storage.counter.initialize_or_replace(|opt_current| UintNote::new( opt_current .map(|note| note.value) .unwrap_or(0) + 1 )); ```
1 parent 25c8b04 commit ef0d467

File tree

4 files changed

+33
-25
lines changed

4 files changed

+33
-25
lines changed

docs/docs/migration_notes.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,28 @@ Updating a note used to require reading it first (via `get_note`, which nullifie
5858
**Key points:**
5959

6060
1. `replace(self, new_note)` (old) → `replace(self, f)` (new), where `f` takes the current note and returns a transformed note.
61-
2. `initialize_or_replace(self, note)` (old) → `initialize_or_replace(self, init_note, f)` (new). Uninitialized variables use `init_note`, initialized ones pass the transform function to `replace`.
61+
2. `initialize_or_replace(self, note)` (old) → `initialize_or_replace(self, f)` (new), where `f` takes an `Option` with the current none, or `none` if uninitialized.
6262
3. Previous note is automatically nullified before the new note is inserted.
6363
4. `NoteEmission<Note>` still requires `.emit()` or `.discard()`.
6464

6565
**Example Migration:**
6666

6767
```diff
68+
- let current_note = storage.my_var.get_note();
69+
- let new_note = f(current_note);
6870
- storage.my_var.replace(new_note);
69-
+ let x: Field = 5;
70-
+ storage.my_var.replace(|note| UintNote::new(note.value + x));
71+
+ storage.my_var.replace(|current_note| f(current_note));
7172
```
7273

7374
```diff
74-
- storage.my_var.initialize_or_replace(init_note);
75-
+ storage.my_var.initialize_or_replace(init_note, |note| UintNote::new(note.value + 5));
75+
- storage.my_var.initialize_or_replace(new_note);
76+
+ storage.my_var.initialize_or_replace(|_| new_note);
77+
```
78+
79+
This makes it easy and efficient to handle both initialization and current value mutation via `initialize_or_replace`, e.g. if implementing a note that simply counts how many times it has been read:
80+
81+
```diff
82+
+ storage.my_var.initialize_or_replace(|opt_current: Option<Note>| opt_current.unwrap_or(0 /* initial value */) + 1);
7683
```
7784

7885

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

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,8 @@ where
253253
/// Reads the current note of a PrivateMutable state variable, nullifies it,
254254
/// and inserts a new note produced by a user-provided function.
255255
///
256-
/// This function implements a "read-and-replace" pattern for updating private state in Aztec.
257-
/// It first retrieves the current note, then nullifies it (marking it as spent),
256+
/// This function implements a "read-and-replace" pattern for updating private state
257+
/// in Aztec. It first retrieves the current note, then nullifies it (marking it as spent),
258258
/// and finally inserts a `new_note` produced by the user-provided function `f`.
259259
///
260260
/// This is conceptually similar to updating a variable in Ethereum smart contracts,
@@ -263,12 +263,13 @@ where
263263
///
264264
/// This function can only be called after the PrivateMutable has been initialized.
265265
/// If called on an uninitialized PrivateMutable, it will fail because there is
266-
/// no current note to replace.
266+
/// no current note to replace. If you don't know if the state variable has been
267+
/// initialized already, you can use `initialize_or_replace` to handle both cases.
267268
///
268269
/// ## Arguments
269270
///
270-
/// * `f` - A function that takes the current `Note` and returns a new `Note`.
271-
/// This allows you to transform the current note before it is reinserted.
271+
/// * `f` - A function that takes the current `Note` and returns a new `Note` that
272+
/// will replace it and become the "current value".
272273
///
273274
/// ## Returns
274275
///
@@ -322,8 +323,10 @@ where
322323
///
323324
/// ## Arguments
324325
///
325-
/// * `init_note` - The note to use if the PrivateMutable is uninitialized.
326-
/// * `transform_fn` - A function that takes the current `Note` and returns a new `Note` to replace it.
326+
/// * `f` - A function that takes an `Option` with the current `Note` and returns the `Note` to insert. This allows
327+
/// you to transform the current note before it is reinserted. The `Option` is `none` if the state variable
328+
/// was not initialized.
329+
///
327330
///
328331
/// ## Returns
329332
///
@@ -333,11 +336,7 @@ where
333336
/// the note, or `.discard()` to skip emission.
334337
/// See NoteEmission documentation for more details.
335338
///
336-
pub fn initialize_or_replace<Env>(
337-
self,
338-
init_note: Note,
339-
transform_fn: fn[Env](Note) -> Note,
340-
) -> NoteEmission<Note>
339+
pub fn initialize_or_replace<Env>(self, f: fn[Env](Option<Note>) -> Note) -> NoteEmission<Note>
341340
where
342341
Note: Packable,
343342
{
@@ -356,9 +355,9 @@ where
356355
unsafe { check_nullifier_exists(self.compute_initialization_nullifier()) };
357356

358357
if !is_initialized {
359-
self.initialize(init_note)
358+
self.initialize(f(Option::none()))
360359
} else {
361-
self.replace(transform_fn)
360+
self.replace(|note| f(Option::some(note)))
362361
}
363362
}
364363

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,9 @@ unconstrained fn initialize_or_replace_uninitialized() {
234234

235235
let init_note = MockNote::new(VALUE).build_note();
236236

237-
let emission = state_var.initialize_or_replace(init_note, |_: MockNote| {
238-
panic(f"") // This should not be called
237+
let emission = state_var.initialize_or_replace(|opt_current_note| {
238+
assert(opt_current_note.is_none());
239+
init_note
239240
});
240241

241242
assert_eq(emission.note, init_note);
@@ -303,8 +304,8 @@ unconstrained fn initialize_or_replace_initialized_settled() {
303304

304305
let replacement_value = VALUE + 1;
305306
let replacement_note = MockNote::new(replacement_value).build_note();
306-
let emission = state_var.initialize_or_replace(std::mem::zeroed(), |current_note| {
307-
assert_eq(current_note, note);
307+
let emission = state_var.initialize_or_replace(|opt_current_note| {
308+
assert_eq(opt_current_note.unwrap(), note);
308309
replacement_note
309310
});
310311

noir-projects/noir-contracts/contracts/app/app_subscription_contract/src/main.nr

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,12 @@ pub contract AppSubscription {
154154
&mut context,
155155
);
156156

157-
let subscription_note = SubscriptionNote::new(subscriber, expiry_block_number, tx_count);
158157
storage
159158
.subscriptions
160159
.at(subscriber)
161-
.initialize_or_replace(subscription_note, |_old| subscription_note)
160+
.initialize_or_replace(|_| {
161+
SubscriptionNote::new(subscriber, expiry_block_number, tx_count)
162+
})
162163
.emit(&mut context, subscriber, MessageDelivery.CONSTRAINED_ONCHAIN);
163164
}
164165

0 commit comments

Comments
 (0)