-
Notifications
You must be signed in to change notification settings - Fork 307
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: process note logs in aztec-nr (#10651)
Closes #9576 This PR offloads the later stages of note log processing from PXE into aztec-nr. The plan is to take over at the decrypted log payload stage, and later on expand scope to also include decryption and (eventually) tagging indices management. Update: this now works, with some caveats. I'll leave some comments here re. current status in case anyone wants to take a look, and for me to resume work once I'm back. Contracts are now expected to have a `process_logs` function that will be called during note syncing. This function is not yet being autogenerated, but I did manually add it to SchnorrAccount and TokenContract for local testing - it should be fairly easy to autogenerate it. PXE still performs tagging index synchronization, fetches all relevant logs, decrypts them, and merges the public and private components when they are partial note logs. This will continue to be this way for a while: we'll tackle these problems in #10723 and #10724. However, past this point we delegate work to the contract.[^1] The contract performs nonce discovery and computes note hash and nullifier, and then calls a new PXE oracle called `deliverNote`. PXE validates that the note hash exists in the tree, and adds the note to its database. **Edit:** I now updated this section to remove all of the old relevant TS code. We no longer do nonce discovery in PXE. With this first step, PXE no longer needs to know about note type ids, which become exclusively an aztec-nr concept. It will continue to know about storage slots, but only because we index by them. More importantly however, this makes us quite ready to continue building on top of this work in order to fully move the other parts of the stack (notably decryption and partial notes) into the contract as well. [^1]: As of right now we're still doing all of the work in PXE, including payload destructuring and nonce discovery, but we discard the results and re-compute them in the contract. Changing this involves deleting a bunch of files and re-structuring some dataflows, and I haven't gotten round to it yet. We should do this in this PR. --------- Co-authored-by: Jan Beneš <[email protected]>
- Loading branch information
Showing
30 changed files
with
865 additions
and
498 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
use std::static_assert; | ||
|
||
use crate::{ | ||
context::unconstrained_context::UnconstrainedContext, note::note_header::NoteHeader, | ||
oracle::note_discovery::deliver_note, utils::array, | ||
}; | ||
|
||
use dep::protocol_types::{ | ||
address::AztecAddress, | ||
constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS}, | ||
hash::compute_note_hash_nonce, | ||
}; | ||
|
||
// We reserve two fields in the note log that are not part of the note content: one for the storage slot, and one for | ||
// the note type id. | ||
global NOTE_LOG_RESERVED_FIELDS: u32 = 2; | ||
pub global MAX_NOTE_SERIALIZED_LEN: u32 = PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_LOG_RESERVED_FIELDS; | ||
|
||
pub struct NoteHashesAndNullifier { | ||
pub note_hash: Field, | ||
pub unique_note_hash: Field, | ||
pub inner_nullifier: Field, | ||
} | ||
|
||
/// Processes a log given its plaintext by trying to find notes encoded in it. This process involves the discovery of | ||
/// the nonce of any such notes, which requires knowledge of the transaction hash in which the notes would've been | ||
/// created, along with the list of unique note hashes in said transaction. | ||
/// | ||
/// Additionally, this requires a `compute_note_hash_and_nullifier` lambda that is able to compute these values for any | ||
/// note in the contract given their contents. A typical implementation of such a function would look like this: | ||
/// | ||
/// ``` | ||
/// |serialized_note_content, note_header, note_type_id| { | ||
/// let hashes = if note_type_id == MyNoteType::get_note_type_id() { | ||
/// assert(serialized_note_content.len() == MY_NOTE_TYPE_SERIALIZATION_LENGTH); | ||
/// dep::aztec::note::utils::compute_note_hash_and_optionally_a_nullifier( | ||
/// MyNoteType::deserialize_content, | ||
/// note_header, | ||
/// true, | ||
/// serialized_note_content.storage(), | ||
/// ) | ||
/// } else { | ||
/// panic(f"Unknown note type id {note_type_id}") | ||
/// }; | ||
/// | ||
/// Option::some(dep::aztec::oracle::management::NoteHashesAndNullifier { | ||
/// note_hash: hashes[0], | ||
/// unique_note_hash: hashes[1], | ||
/// inner_nullifier: hashes[3], | ||
/// }) | ||
/// } | ||
/// ``` | ||
pub unconstrained fn do_process_log<Env>( | ||
context: UnconstrainedContext, | ||
log_plaintext: BoundedVec<Field, PRIVATE_LOG_SIZE_IN_FIELDS>, | ||
tx_hash: Field, | ||
unique_note_hashes_in_tx: BoundedVec<Field, MAX_NOTE_HASHES_PER_TX>, | ||
first_nullifier_in_tx: Field, | ||
recipient: AztecAddress, | ||
compute_note_hash_and_nullifier: fn[Env](BoundedVec<Field, MAX_NOTE_SERIALIZED_LEN>, NoteHeader, Field) -> Option<NoteHashesAndNullifier>, | ||
) { | ||
let (storage_slot, note_type_id, serialized_note_content) = | ||
destructure_log_plaintext(log_plaintext); | ||
|
||
// We need to find the note's nonce, which is the one that results in one of the unique note hashes from tx_hash | ||
for_each_in_bounded_vec( | ||
unique_note_hashes_in_tx, | ||
|expected_unique_note_hash, i| { | ||
let candidate_nonce = compute_note_hash_nonce(first_nullifier_in_tx, i); | ||
|
||
let header = NoteHeader::new(context.this_address(), candidate_nonce, storage_slot); | ||
|
||
// TODO(#11157): handle failed note_hash_and_nullifier computation | ||
let hashes = compute_note_hash_and_nullifier( | ||
serialized_note_content, | ||
header, | ||
note_type_id, | ||
) | ||
.unwrap(); | ||
|
||
if hashes.unique_note_hash == expected_unique_note_hash { | ||
// TODO(#10726): push these into a vec to deliver all at once instead of having one oracle call per note | ||
|
||
assert( | ||
deliver_note( | ||
context.this_address(), // TODO(#10727): allow other contracts to deliver notes | ||
storage_slot, | ||
candidate_nonce, | ||
serialized_note_content, | ||
hashes.note_hash, | ||
hashes.inner_nullifier, | ||
tx_hash, | ||
recipient, | ||
), | ||
"Failed to deliver note", | ||
); | ||
|
||
// We don't exit the loop - it is possible (though rare) for the exact same note content to be present | ||
// multiple times in the same transaction with different nonces. This typically doesn't happen due to | ||
// notes containing random values in order to hide their contents. | ||
} | ||
}, | ||
); | ||
} | ||
|
||
unconstrained fn destructure_log_plaintext( | ||
log_plaintext: BoundedVec<Field, PRIVATE_LOG_SIZE_IN_FIELDS>, | ||
) -> (Field, Field, BoundedVec<Field, MAX_NOTE_SERIALIZED_LEN>) { | ||
assert(log_plaintext.len() >= NOTE_LOG_RESERVED_FIELDS); | ||
|
||
// If NOTE_LOG_RESERVED_FIELDS is changed, causing the assertion below to fail, then the declarations for | ||
// `storage_slot` and `note_type_id` must be updated as well. | ||
static_assert( | ||
NOTE_LOG_RESERVED_FIELDS == 2, | ||
"unepxected value for NOTE_LOG_RESERVED_FIELDS", | ||
); | ||
let storage_slot = log_plaintext.get(0); | ||
let note_type_id = log_plaintext.get(1); | ||
|
||
let serialized_note_content = array::subbvec(log_plaintext, NOTE_LOG_RESERVED_FIELDS); | ||
|
||
(storage_slot, note_type_id, serialized_note_content) | ||
} | ||
|
||
fn for_each_in_bounded_vec<T, let MaxLen: u32, Env>( | ||
vec: BoundedVec<T, MaxLen>, | ||
f: fn[Env](T, u32) -> (), | ||
) { | ||
for i in 0..MaxLen { | ||
if i < vec.len() { | ||
f(vec.get_unchecked(i), i); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
pub mod constants; | ||
pub mod discovery; | ||
pub mod lifecycle; | ||
pub mod note_getter; | ||
pub mod note_getter_options; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.