|
| 1 | +use crate::{ |
| 2 | + capsules::CapsuleArray, |
| 3 | + discovery::{ |
| 4 | + ComputeNoteHashAndNullifier, |
| 5 | + nonce_discovery::{attempt_note_nonce_discovery, DiscoveredNoteInfo}, |
| 6 | + private_logs::MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN, |
| 7 | + }, |
| 8 | + oracle::note_discovery::{deliver_note, get_log_by_tag}, |
| 9 | + utils::array, |
| 10 | +}; |
| 11 | + |
| 12 | +use dep::protocol_types::{ |
| 13 | + address::AztecAddress, |
| 14 | + constants::PUBLIC_LOG_DATA_SIZE_IN_FIELDS, |
| 15 | + debug_log::debug_log_format, |
| 16 | + traits::{Deserialize, Serialize, ToField}, |
| 17 | +}; |
| 18 | + |
| 19 | +/// The slot in the PXE capsules where we store a `CapsuleArray` of `DeliveredPendingPartialNote`. |
| 20 | +// TODO(#11630): come up with some sort of slot allocation scheme. |
| 21 | +pub global DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_CAPSULES_SLOT: Field = 77; |
| 22 | + |
| 23 | +/// Public logs contain an extra field at the beginning with the address of the contract that emitted them, and partial |
| 24 | +/// notes emit their completion tag in the log, resulting in the first two fields in the public log not being part of |
| 25 | +/// the packed public content. |
| 26 | +// TODO(#10273): improve how contract log siloing is handled |
| 27 | +pub global NON_PACKED_CONTENT_FIELDS_IN_PUBLIC_LOG: u32 = 2; |
| 28 | + |
| 29 | +/// The maximum length of the packed representation of public fields in a partial note. This is limited by public log |
| 30 | +/// size and extra fields in the log (e.g. the tag). |
| 31 | +pub global MAX_PUBLIC_PARTIAL_NOTE_PACKED_CONTENT_LENGTH: u32 = |
| 32 | + PUBLIC_LOG_DATA_SIZE_IN_FIELDS - NON_PACKED_CONTENT_FIELDS_IN_PUBLIC_LOG; |
| 33 | + |
| 34 | +/// A partial note that was delivered but is still pending completion. Contains the information necessary to find the |
| 35 | +/// log that will complete it and lead to a note being discovered and delivered. |
| 36 | +#[derive(Serialize, Deserialize)] |
| 37 | +pub(crate) struct DeliveredPendingPartialNote { |
| 38 | + pub(crate) note_completion_log_tag: Field, |
| 39 | + pub(crate) storage_slot: Field, |
| 40 | + pub(crate) note_type_id: Field, |
| 41 | + pub(crate) packed_private_note_content: BoundedVec<Field, MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN>, |
| 42 | + pub(crate) recipient: AztecAddress, |
| 43 | +} |
| 44 | + |
| 45 | +/// Searches for public logs that would result in the completion of pending partial notes, ultimately resulting in the |
| 46 | +/// notes being delivered to PXE if completed. |
| 47 | +pub unconstrained fn fetch_and_process_public_partial_note_completion_logs<Env>( |
| 48 | + contract_address: AztecAddress, |
| 49 | + compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier<Env>, |
| 50 | +) { |
| 51 | + let pending_partial_notes = CapsuleArray::at( |
| 52 | + contract_address, |
| 53 | + DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_CAPSULES_SLOT, |
| 54 | + ); |
| 55 | + |
| 56 | + debug_log_format( |
| 57 | + "{} pending partial notes", |
| 58 | + [pending_partial_notes.len() as Field], |
| 59 | + ); |
| 60 | + |
| 61 | + let mut i = &mut 0; |
| 62 | + whyle( |
| 63 | + || *i < pending_partial_notes.len(), |
| 64 | + || { |
| 65 | + let pending_partial_note: DeliveredPendingPartialNote = pending_partial_notes.get(*i); |
| 66 | + |
| 67 | + let maybe_log = get_log_by_tag(pending_partial_note.note_completion_log_tag); |
| 68 | + if maybe_log.is_none() { |
| 69 | + debug_log_format( |
| 70 | + "Found no completion logs for partial note #{}", |
| 71 | + [(*i) as Field], |
| 72 | + ); |
| 73 | + *i += 1 as u32; |
| 74 | + // Note that we're not removing the pending partial note from the PXE DB, so we will continue searching |
| 75 | + // for this tagged log when performing note discovery in the future until we either find it or the entry |
| 76 | + // is somehow removed from the PXE DB. |
| 77 | + } else { |
| 78 | + debug_log_format("Completion log found for partial note #{}", [(*i) as Field]); |
| 79 | + let log = maybe_log.unwrap(); |
| 80 | + |
| 81 | + // Public logs have an extra field at the beginning with the contract address, which we use to verify |
| 82 | + // that we're getting the logs from the expected contract. |
| 83 | + // TODO(#10273): improve how contract log siloing is handled |
| 84 | + assert_eq( |
| 85 | + log.log_content.get(0), |
| 86 | + contract_address.to_field(), |
| 87 | + "Got a public log emitted by a different contract", |
| 88 | + ); |
| 89 | + |
| 90 | + // Public fields are assumed to all be placed at the end of the packed representation, so we combine the |
| 91 | + // private and public packed fields (i.e. the contents of the log sans the extra fields) to get the |
| 92 | + // complete packed content. |
| 93 | + let packed_public_note_content: BoundedVec<_, MAX_PUBLIC_PARTIAL_NOTE_PACKED_CONTENT_LENGTH> = |
| 94 | + array::subbvec(log.log_content, NON_PACKED_CONTENT_FIELDS_IN_PUBLIC_LOG); |
| 95 | + let complete_packed_note_content = array::append( |
| 96 | + pending_partial_note.packed_private_note_content, |
| 97 | + packed_public_note_content, |
| 98 | + ); |
| 99 | + |
| 100 | + let discovered_notes = attempt_note_nonce_discovery( |
| 101 | + log.unique_note_hashes_in_tx, |
| 102 | + log.first_nullifier_in_tx, |
| 103 | + compute_note_hash_and_nullifier, |
| 104 | + contract_address, |
| 105 | + pending_partial_note.storage_slot, |
| 106 | + pending_partial_note.note_type_id, |
| 107 | + complete_packed_note_content, |
| 108 | + ); |
| 109 | + |
| 110 | + debug_log_format( |
| 111 | + "Discovered {0} notes for partial note {1}", |
| 112 | + [discovered_notes.len() as Field, (*i) as Field], |
| 113 | + ); |
| 114 | + |
| 115 | + array::for_each_in_bounded_vec( |
| 116 | + discovered_notes, |
| 117 | + |discovered_note: DiscoveredNoteInfo, _| { |
| 118 | + // TODO:(#10728): decide how to handle notes that fail delivery. This could be due to e.g. a |
| 119 | + // temporary node connectivity issue - is simply throwing good enough here? |
| 120 | + assert( |
| 121 | + deliver_note( |
| 122 | + contract_address, |
| 123 | + pending_partial_note.storage_slot, |
| 124 | + discovered_note.nonce, |
| 125 | + complete_packed_note_content, |
| 126 | + discovered_note.note_hash, |
| 127 | + discovered_note.inner_nullifier, |
| 128 | + log.tx_hash, |
| 129 | + pending_partial_note.recipient, |
| 130 | + ), |
| 131 | + "Failed to deliver note", |
| 132 | + ); |
| 133 | + }, |
| 134 | + ); |
| 135 | + |
| 136 | + // Because there is only a single log for a given tag, once we've processed the tagged log then we |
| 137 | + // simply delete the pending work entry, regardless of whether it was actually completed or not. |
| 138 | + // TODO(#11627): only remove the pending entry if we actually process a log that results in the note |
| 139 | + // being completed. |
| 140 | + pending_partial_notes.remove(*i); |
| 141 | + } |
| 142 | + }, |
| 143 | + ); |
| 144 | +} |
| 145 | + |
| 146 | +/// Custom version of a while loop, calls `body` repeatedly until `condition` returns false. To be removed once Noir |
| 147 | +/// supports looping in unconstrained code. |
| 148 | +fn whyle<Env, Env2>(condition: fn[Env]() -> bool, body: fn[Env2]() -> ()) { |
| 149 | + if condition() { |
| 150 | + body(); |
| 151 | + whyle(condition, body); |
| 152 | + } |
| 153 | +} |
0 commit comments