Skip to content

fix: handle missing nullifiers in nonce discovery#21328

Closed
nchamo wants to merge 8 commits intomerge-train/fairiesfrom
fix/nonce-discovery-optional-nullifier
Closed

fix: handle missing nullifiers in nonce discovery#21328
nchamo wants to merge 8 commits intomerge-train/fairiesfrom
fix/nonce-discovery-optional-nullifier

Conversation

@nchamo
Copy link
Contributor

@nchamo nchamo commented Mar 10, 2026

Summary

  • compute_note_hash_and_nullifier now returns Option instead of panicking when note hash computation fails (e.g. unknown note type)
  • Notes with matching hashes but missing nullifiers (e.g. unavailable nullifier secret key) are skipped with a warning log instead of panicking

Closes #11157
Closes F-344

@nchamo nchamo requested a review from nventuro as a code owner March 10, 2026 18:06
@nchamo nchamo self-assigned this Mar 10, 2026
@nchamo nchamo requested a review from benesjan March 12, 2026 16:14
Copy link
Contributor

@nventuro nventuro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will have conflicts with #21438. Sorry about that.

Comment on lines +53 to +55
// matches the note hash at the array index we're currently processing. This computation may fail, in which
// case we skip the candidate.
let maybe_hashes = compute_note_hash_and_nullifier(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not the computation that fails, it's that the contract may not know what to do. Examples include it not recognizing the note_type_id, or the packed_note not having the correct length.

We should log a warning mentioning that we attempted to process note data during discovery but failed to compute its hash.

// and calls to the application could result in invalid transactions (with duplicate nullifiers).
// This is not a concern because an application already has more direct means of making a call to
// it fail the transaction.
// TODO(F-265): consider the case for external notes even when nullifier is not computable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TODO should go on the else

@benesjan benesjan removed their request for review March 13, 2026 05:18
@benesjan
Copy link
Contributor

Nico's comments are not addressed yet and there are conflicts so not reviewing again for now.

Comment on lines +118 to +131
if discovered_notes.len() == 0 {
if had_hash_failure {
aztecnr_warn_log_format!(
"Could not compute note hash for note type {0} on contract {1}, discarding note",
)(
[note_type_id, contract_address.to_field()],
);
}
if had_nullifier_failure {
aztecnr_warn_log_format!(
"Could not compute nullifier for note type {0} on contract {1}, discarding note",
)(
[note_type_id, contract_address.to_field()],
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like @benesjan mentioned before, we were emitting warnings inside the loop. The problem is that Option::none could be returned for reasons unrelated to the nonce, so we would end up logging the warning for every nonce. So I moved this here

@nchamo nchamo requested review from benesjan and nventuro March 13, 2026 14:45
Comment on lines +118 to +130
if discovered_notes.len() == 0 {
if had_hash_failure {
aztecnr_warn_log_format!(
"Could not compute note hash for note type {0} on contract {1}, discarding note",
)(
[note_type_id, contract_address.to_field()],
);
}
if had_nullifier_failure {
aztecnr_warn_log_format!(
"Could not compute nullifier for note type {0} on contract {1}, discarding note",
)(
[note_type_id, contract_address.to_field()],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, not sure. This is the second time we run into this - might as well do it properly if we're going to be doing so much error handling.

We could have a local implementation of Result (Noir doesn't have it yet), which is the same as Option except the none case has a type:

struct Result<T, E> {
  value: T;
  error: E;
  is_ok: bool;
}

impl<T, E> Result<T, E> {
   fn ok(value: T) -> Self {
       Self { value, error: std::mem::zeroed(), is_ok: true }
   }

   fn err(error: E) -> Self {
       Self { value: std::mem::zeroed(), error,  is_ok: false }
   }
  
   fn is_ok(self) -> bool {
     ..
   }

   fn is_err(self) -> bool {
     ..
   }
  
    fn and_then(self) -> bool {
     ..
   }
}

See https://doc.rust-lang.org/std/result/ and https://doc.rust-lang.org/std/result/enum.Result.html.

wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it but seems like something that should be in noir::std. Shall we ping Noir people if they are willing to add it?

@nchamo once this discussion gets resolved feel free to re-request review from me (just trying to keep my review work clean by un-requesting when it doesn't make sense for me to imminently review). Thanks

@benesjan benesjan removed their request for review March 16, 2026 06:31
@nventuro
Copy link
Contributor

#21639 now accidentally also overlaps with this.

@nchamo
Copy link
Contributor Author

nchamo commented Mar 17, 2026

@nventuro , closing this since it will be handled in #21639

@nchamo nchamo closed this Mar 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants