Skip to content

Commit 1879ac1

Browse files
authored
docs(aztec-nr): document partial note completion trust model (#24816)
## Summary - Documents the trust model of partial note completion on `PartialUintNote` and `PartialNFTNote`: - The validity commitment only proves that the contract created the partial note designating `completer`. - The storage slot and value/token id are trusted arguments, not bound by the commitment. - The completer is not authenticated by the check itself, so contracts must pass `msg_sender()` as `completer`. - Adds a WARNING that completion is not single-use: the designated completer can complete the same partial note any number of times, so contracts must make every completion independently paid for or authorized in the completing function (as the token and NFT contracts already do). - Fixes doc overclaims that said the validity commitment verifies the storage slot / state variable.
1 parent f245aed commit 1879ac1

2 files changed

Lines changed: 39 additions & 16 deletions

File tree

noir-projects/aztec-nr/uint-note/src/uint_note.nr

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -172,15 +172,27 @@ pub struct PartialUintNote {
172172

173173
impl PartialUintNote {
174174
/// Completes the partial note, creating a new note that can be used like any other UintNote.
175+
///
176+
/// The validity commitment only proves that this contract created this exact partial note designating `completer`
177+
/// (see [`compute_validity_commitment`](Self::compute_validity_commitment)). The `storage_slot` and `value`
178+
/// arguments are not bound by it, so the caller is trusted to provide correct values for them. Nothing checks
179+
/// that the completer is the entity performing the call. Contracts must authenticate the caller themselves,
180+
/// typically by passing `msg_sender()` as `completer`.
181+
///
182+
/// WARNING: completion is not single-use. Nothing prevents the completer from completing the same partial note
183+
/// multiple times, inserting a new note hash each time. Every completion must therefore be independently paid for
184+
/// or authorized in the completing function itself (e.g. by debiting the completer's balance on each call), never
185+
/// granting value at creation to be redeemed at completion. Completing more than once is otherwise harmless to
186+
/// the contract, but wasteful for the completer. The recipient only discovers the first completion, so anything
187+
/// carried by further ones is lost.
175188
pub fn complete(self, context: PublicContext, completer: AztecAddress, storage_slot: Field, value: u128) {
176189
// A note with a value of zero is valid, but we cannot currently complete a partial note with such a value
177190
// because this will result in the completion log having its last field set to 0. Public logs currently do not
178191
// track their length, and so trailing zeros are simply trimmed. This results in the completion log missing its
179192
// last field (the value), and note discovery failing. TODO(#11636): remove this
180193
assert(value != 0, "Cannot complete a PartialUintNote with a value of 0");
181194

182-
// We verify that the partial note we're completing is valid (i.e. completer is correct, it uses the correct
183-
// state variable's storage slot, and it is internally consistent).
195+
// We verify that `completer` matches the completer designated when this contract created this partial note.
184196
let validity_commitment = self.compute_validity_commitment(completer);
185197
// Safety: we're using the existence of the nullifier as proof of the contract having validated the partial
186198
// note's preimage, which is safe.
@@ -209,8 +221,7 @@ impl PartialUintNote {
209221
storage_slot: Field,
210222
value: u128,
211223
) {
212-
// We verify that the partial note we're completing is valid (i.e. completer is correct, it uses the correct
213-
// state variable's storage slot, and it is internally consistent).
224+
// We verify that `completer` matches the completer designated when this contract created this partial note.
214225
let validity_commitment = self.compute_validity_commitment(completer);
215226

216227
let siloed_validity_commitment = compute_siloed_nullifier(context.this_address(), validity_commitment);
@@ -230,11 +241,11 @@ impl PartialUintNote {
230241
context.push_note_hash(self.compute_complete_note_hash(storage_slot, value));
231242
}
232243

233-
/// Computes a validity commitment for this partial note. The commitment cryptographically binds the note's private
234-
/// data with the designated completer address. When the note is later completed in public execution, we can load
235-
/// this commitment from the nullifier tree and verify that both the partial note (e.g. that the storage slot
236-
/// corresponds to the correct owner, and that we're using the correct state variable) and completer are
237-
/// legitimate.
244+
/// Computes a validity commitment for this partial note.
245+
///
246+
/// The commitment cryptographically binds the note's private content with the designated completer address. When
247+
/// the note is later completed, the commitment is looked up in the nullifier tree to verify that this contract
248+
/// created this exact partial note designating that completer.
238249
pub fn compute_validity_commitment(self, completer: AztecAddress) -> Field {
239250
poseidon2_hash_with_separator(
240251
[self.commitment, completer.to_field()],

noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,19 @@ pub struct PartialNFTNote {
166166

167167
impl PartialNFTNote {
168168
/// Completes the partial note, creating a new note that can be used like any other NFTNote.
169+
///
170+
/// The validity commitment only proves that this contract created this exact partial note designating `completer`
171+
/// (see [`compute_validity_commitment`](Self::compute_validity_commitment)). The `storage_slot` and `token_id`
172+
/// arguments are not bound by it, so the caller is trusted to provide correct values for them. Nothing checks
173+
/// that the completer is the entity performing the call. Contracts must authenticate the caller themselves,
174+
/// typically by passing `msg_sender()` as `completer`.
175+
///
176+
/// WARNING: completion is not single-use. Nothing prevents the completer from completing the same partial note
177+
/// multiple times, inserting a new note hash each time. Every completion must therefore be independently paid for
178+
/// or authorized in the completing function itself (e.g. by asserting that the completer publicly owns the NFT and
179+
/// clearing its ownership on each call), never granting value at creation to be redeemed at completion. Completing
180+
/// more than once is otherwise harmless to the contract, but wasteful for the completer. The recipient only
181+
/// discovers the first completion, so anything carried by further ones is lost.
169182
pub fn complete(self, context: PublicContext, completer: AztecAddress, storage_slot: Field, token_id: Field) {
170183
// A note with a value of zero is valid, but we cannot currently complete a partial note with such a value
171184
// because this will result in the completion log having its last field set to 0. Public logs currently do not
@@ -174,8 +187,7 @@ impl PartialNFTNote {
174187
// TODO(#11636): remove this
175188
assert(token_id != 0, "Cannot complete a PartialNFTNote with a value of 0");
176189

177-
// We verify that the partial note we're completing is valid (i.e. completer is correct, it uses the correct
178-
// state variable's storage slot, and it is internally consistent).
190+
// We verify that `completer` matches the completer designated when this contract created this partial note.
179191
let validity_commitment = self.compute_validity_commitment(completer);
180192
// Safety: we're using the existence of the nullifier as proof of the contract having validated the partial
181193
// note's preimage, which is safe.
@@ -195,11 +207,11 @@ impl PartialNFTNote {
195207
context.push_note_hash(self.compute_complete_note_hash(storage_slot, token_id));
196208
}
197209

198-
/// Computes a validity commitment for this partial note. The commitment cryptographically binds the note's private
199-
/// data with the designated completer address. When the note is later completed in public execution, we can load
200-
/// this commitment from the nullifier tree and verify that both the partial note (e.g. that the storage slot
201-
/// corresponds to the correct owner, and that we're using the correct state variable) and completer are
202-
/// legitimate.
210+
/// Computes a validity commitment for this partial note.
211+
///
212+
/// The commitment cryptographically binds the note's private content with the designated completer address. When
213+
/// the note is later completed, the commitment is looked up in the nullifier tree to verify that this contract
214+
/// created this exact partial note designating that completer.
203215
pub fn compute_validity_commitment(self, completer: AztecAddress) -> Field {
204216
poseidon2_hash_with_separator(
205217
[self.commitment, completer.to_field()],

0 commit comments

Comments
 (0)