From 8a59b176545ba6d0eed434cba50c9d5c745cfd25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Fri, 4 Oct 2024 09:43:50 +0200 Subject: [PATCH] feat: delivering partial fields via unencrypted logs (#8725) Partial fields get delivered via unencrypted logs and NoteProcessor then looks for them if it fails to find note hash. --- .../aztec-nr/aztec/src/macros/notes/mod.nr | 7 +- .../nft_contract/src/hacky_partial_note.nr | 21 ++ .../contracts/nft_contract/src/main.nr | 32 +++ .../src/test/transfer_to_private.nr | 4 +- .../contracts/nft_contract/src/test/utils.nr | 4 +- yarn-project/end-to-end/src/e2e_nft.test.ts | 23 +- .../src/database/deferred_note_dao.test.ts | 4 +- .../pxe/src/database/deferred_note_dao.ts | 6 +- .../pxe/src/database/incoming_note_dao.ts | 25 +- .../pxe/src/database/outgoing_note_dao.ts | 24 +- .../pxe/src/note_processor/note_processor.ts | 23 +- .../src/note_processor/produce_note_dao.ts | 235 ------------------ .../utils/add_nullable_field_to_payload.ts | 67 +++++ .../utils/brute_force_note_info.ts | 82 ++++++ .../pxe/src/note_processor/utils/index.ts | 2 + .../note_processor/utils/produce_note_daos.ts | 114 +++++++++ .../utils/produce_note_daos_for_key.ts | 154 ++++++++++++ .../txe/src/txe_service/txe_service.ts | 13 + .../types/src/abi/contract_artifact.ts | 4 + 19 files changed, 573 insertions(+), 271 deletions(-) create mode 100644 noir-projects/noir-contracts/contracts/nft_contract/src/hacky_partial_note.nr delete mode 100644 yarn-project/pxe/src/note_processor/produce_note_dao.ts create mode 100644 yarn-project/pxe/src/note_processor/utils/add_nullable_field_to_payload.ts create mode 100644 yarn-project/pxe/src/note_processor/utils/brute_force_note_info.ts create mode 100644 yarn-project/pxe/src/note_processor/utils/index.ts create mode 100644 yarn-project/pxe/src/note_processor/utils/produce_note_daos.ts create mode 100644 yarn-project/pxe/src/note_processor/utils/produce_note_daos_for_key.ts diff --git a/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr index 21a852b5604..4ad2201fccb 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr @@ -156,8 +156,11 @@ pub(crate) comptime fn generate_note_export( fields: [(Quoted, u32, bool)] ) -> Quoted { let name = s.name(); - let global_export_name = f"{name}_EXPORTS".quoted_contents(); - let note_fields_name = f"{name}Fields".quoted_contents(); + let mut hasher = Poseidon2Hasher::default(); + s.as_type().hash(&mut hasher); + let hash = hasher.finish() as u32; + let global_export_name = f"{name}_{hash}_EXPORTS".quoted_contents(); + let note_fields_name = f"{name}Fields_{hash}".quoted_contents(); let note_name_as_str = name.as_str_quote(); let note_name_str_len = unquote!(quote { $note_name_as_str.as_bytes().len() }); diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/hacky_partial_note.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/hacky_partial_note.nr new file mode 100644 index 00000000000..cb39a0aaed0 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/hacky_partial_note.nr @@ -0,0 +1,21 @@ +use dep::aztec::{prelude::{NullifiableNote, PrivateContext}, macros::notes::note}; + +// TODO(benesjan): Once nuking this nuke hack in contract_artifact.ts +// This note has to have the same name as the standard `NFTNote` so that we get the same note type id. +#[note] +struct NFTNote { + // The nullifying public key hash is used with the nsk_app to ensure that the note can be privately spent. + npk_m_hash: Field, + // Randomness of the note to hide its contents + randomness: Field, +} + +impl NullifiableNote for NFTNote { + fn compute_nullifier(self, _context: &mut PrivateContext, _note_hash_for_nullify: Field) -> Field { + 0 + } + + unconstrained fn compute_nullifier_without_context(self) -> Field { + 0 + } +} diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr index cb72da92eec..98080f89570 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/main.nr @@ -1,5 +1,6 @@ mod types; mod test; +mod hacky_partial_note; use dep::aztec::macros::aztec; @@ -151,6 +152,34 @@ contract NFT { let to_note_slot = storage.private_nfts.at(to).storage_slot; let hiding_point = NFTNote::hiding_point().new(to_npk_m_hash, note_randomness, to_note_slot); + // ####### HACKY PARTIAL NOTE EMISSION CODE (to be replaced in a followup PR with proper macro support) ####### + // TODO(#8769): The following is a temporary partial notes delivery solution that should be eventually replaced. + let to_public_keys = get_public_keys(to); + let hacky_nft_note = crate::hacky_partial_note::NFTNote { + npk_m_hash: to_npk_m_hash, + randomness: note_randomness, + header: dep::aztec::prelude::NoteHeader::empty() + }; + // `from` cannot receive any logs here as that would leak information of who is the note recipient. For this + // reason we encrypt outgoing to `to`. + let plaintext = hacky_nft_note.to_be_bytes(to_note_slot); + let encrypted_log: [u8; 480] = dep::aztec::encrypted_logs::payload::compute_encrypted_log( + context.this_address(), + context.request_ovsk_app(to_public_keys.ovpk_m.hash()), + to_public_keys.ovpk_m, + to_public_keys.ivpk_m, + to, + plaintext + ); + let log_hash = dep::aztec::protocol_types::hash::sha256_to_field(encrypted_log); + + // Unfortunately we need to push a dummy note hash to the context here because a note log requires having + // a counter that corresponds to a note hash in the same call. + let note_hash_counter = context.side_effect_counter; + context.push_note_hash(5); + context.emit_raw_note_log(note_hash_counter, encrypted_log, log_hash); + // ####### END OF HACKY PARTIAL NOTE EMISSION CODE ####### + // We make the msg_sender/transfer_preparer part of the slot preimage to ensure he cannot interfere with // non-sender's slots let transfer_preparer_storage_slot_commitment: Field = pedersen_hash( @@ -208,6 +237,9 @@ contract NFT { let note_hash = hiding_point.finalize(token_id); context.push_note_hash(note_hash); + // We emit the `token_id` as unencrypted event such that the `NoteProcessor` can use it to reconstruct the note + context.emit_unencrypted_log(token_id); + // At last we reset public storage to zero to achieve the effect of transient storage - kernels will squash // the writes context.storage_write( diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr index aa8ad337253..6de1bb56ef7 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/test/transfer_to_private.nr @@ -48,8 +48,8 @@ unconstrained fn transfer_to_private_to_a_different_account() { NFT::at(nft_contract_address).finalize_transfer_to_private(token_id, transfer_preparer_storage_slot_commitment) ); - // Store the finalized note in the cache - let mut context = env.private(); + // TODO(#8771): We need to manually add the note because in the partial notes flow `notify_created_note_oracle` + // is not called and we don't have a `NoteProcessor` in TXE. let recipient_npk_m_hash = get_public_keys(recipient).npk_m.hash(); let private_nfts_recipient_slot = derive_storage_slot_in_map(NFT::storage_layout().private_nfts.slot, recipient); diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/test/utils.nr index 4e2aa858b50..fb03eb48d6a 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/test/utils.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/test/utils.nr @@ -70,8 +70,8 @@ unconstrained pub fn setup_mint_and_transfer_to_private(with_account_contracts: let finalize_transfer_to_private_call_interface = NFT::at(nft_contract_address).finalize_transfer_to_private(minted_token_id, transfer_preparer_storage_slot_commitment); env.call_public(finalize_transfer_to_private_call_interface); - // Store the finalized note in the cache - let mut context = env.private(); + // TODO(#8771): We need to manually add the note because in the partial notes flow `notify_created_note_oracle` + // is not called and we don't have a `NoteProcessor` in TXE. let owner_npk_m_hash = get_public_keys(owner).npk_m.hash(); let private_nfts_owner_slot = derive_storage_slot_in_map(NFT::storage_layout().private_nfts.slot, owner); diff --git a/yarn-project/end-to-end/src/e2e_nft.test.ts b/yarn-project/end-to-end/src/e2e_nft.test.ts index 36cba60c70b..f86fd42c733 100644 --- a/yarn-project/end-to-end/src/e2e_nft.test.ts +++ b/yarn-project/end-to-end/src/e2e_nft.test.ts @@ -1,5 +1,4 @@ -import { type AccountWallet, AztecAddress, BatchCall, ExtendedNote, Fr, Note } from '@aztec/aztec.js'; -import { deriveStorageSlotInMap } from '@aztec/circuits.js/hash'; +import { type AccountWallet, AztecAddress, BatchCall, Fr } from '@aztec/aztec.js'; import { pedersenHash } from '@aztec/foundation/crypto'; import { NFTContract } from '@aztec/noir-contracts.js'; @@ -72,7 +71,7 @@ describe('NFT', () => { TRANSIENT_STORAGE_SLOT_PEDERSEN_INDEX, ); - const { txHash, debugInfo } = await new BatchCall(user1Wallet, [ + const { debugInfo } = await new BatchCall(user1Wallet, [ nftContractAsUser1.methods .prepare_transfer_to_private(sender, recipient, noteRandomness, transientStorageSlotRandomness) .request(), @@ -86,24 +85,6 @@ describe('NFT', () => { const publicOwnerAfter = await nftContractAsUser1.methods.owner_of(TOKEN_ID).simulate(); expect(publicOwnerAfter).toEqual(AztecAddress.ZERO); - // TODO(#8238): Since we don't yet have a partial note delivery we have to manually add it to PXE - const nftNote = new Note([ - new Fr(TOKEN_ID), - user1Wallet.getCompleteAddress().publicKeys.masterNullifierPublicKey.hash(), - noteRandomness, - ]); - - await user1Wallet.addNote( - new ExtendedNote( - nftNote, - user1Wallet.getAddress(), - nftContractAsUser1.address, - deriveStorageSlotInMap(NFTContract.storage.private_nfts.slot, user1Wallet.getAddress()), - NFTContract.notes.NFTNote.id, - txHash, - ), - ); - // We should get 4 data writes setting values to 0 - 3 for note hiding point and 1 for public owner (we transfer // to private so public owner is set to 0). Ideally we would have here only 1 data write as the 4 values change // from zero to non-zero to zero in the tx and hence no write could be committed. This makes public writes diff --git a/yarn-project/pxe/src/database/deferred_note_dao.test.ts b/yarn-project/pxe/src/database/deferred_note_dao.test.ts index 542774a1165..90da662aa5c 100644 --- a/yarn-project/pxe/src/database/deferred_note_dao.test.ts +++ b/yarn-project/pxe/src/database/deferred_note_dao.test.ts @@ -1,4 +1,4 @@ -import { Note, randomTxHash } from '@aztec/circuit-types'; +import { Note, UnencryptedTxL2Logs, randomTxHash } from '@aztec/circuit-types'; import { AztecAddress, Fr, Point } from '@aztec/circuits.js'; import { NoteSelector } from '@aztec/foundation/abi'; import { randomInt } from '@aztec/foundation/crypto'; @@ -14,6 +14,7 @@ export const randomDeferredNoteDao = ({ noteTypeId = NoteSelector.random(), noteHashes = [Fr.random(), Fr.random()], dataStartIndexForTx = randomInt(100), + unencryptedLogs = UnencryptedTxL2Logs.random(1, 1), }: Partial = {}) => { return new DeferredNoteDao( publicKey, @@ -24,6 +25,7 @@ export const randomDeferredNoteDao = ({ txHash, noteHashes, dataStartIndexForTx, + unencryptedLogs, ); }; diff --git a/yarn-project/pxe/src/database/deferred_note_dao.ts b/yarn-project/pxe/src/database/deferred_note_dao.ts index bc5cfd455ba..1351b5b3b2d 100644 --- a/yarn-project/pxe/src/database/deferred_note_dao.ts +++ b/yarn-project/pxe/src/database/deferred_note_dao.ts @@ -1,4 +1,4 @@ -import { Note, TxHash } from '@aztec/circuit-types'; +import { Note, TxHash, UnencryptedTxL2Logs } from '@aztec/circuit-types'; import { AztecAddress, Fr, Point, type PublicKey, Vector } from '@aztec/circuits.js'; import { NoteSelector } from '@aztec/foundation/abi'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; @@ -26,6 +26,8 @@ export class DeferredNoteDao { public noteHashes: Fr[], /** The next available leaf index for the note hash tree for this transaction */ public dataStartIndexForTx: number, + /** Unencrypted logs for the transaction (used to complete partial notes) */ + public unencryptedLogs: UnencryptedTxL2Logs, ) {} toBuffer(): Buffer { @@ -38,6 +40,7 @@ export class DeferredNoteDao { this.txHash, new Vector(this.noteHashes), this.dataStartIndexForTx, + this.unencryptedLogs, ); } static fromBuffer(buffer: Buffer | BufferReader) { @@ -51,6 +54,7 @@ export class DeferredNoteDao { reader.readObject(TxHash), reader.readVector(Fr), reader.readNumber(), + reader.readObject(UnencryptedTxL2Logs), ); } } diff --git a/yarn-project/pxe/src/database/incoming_note_dao.ts b/yarn-project/pxe/src/database/incoming_note_dao.ts index cba1df864f6..88961240638 100644 --- a/yarn-project/pxe/src/database/incoming_note_dao.ts +++ b/yarn-project/pxe/src/database/incoming_note_dao.ts @@ -1,10 +1,12 @@ -import { Note, TxHash } from '@aztec/circuit-types'; +import { type L1NotePayload, Note, TxHash } from '@aztec/circuit-types'; import { AztecAddress, Fr, Point, type PublicKey } from '@aztec/circuits.js'; import { NoteSelector } from '@aztec/foundation/abi'; import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; import { type NoteData } from '@aztec/simulator'; +import { type NoteInfo } from '../note_processor/utils/index.js'; + /** * A note with contextual data which was decrypted as incoming. */ @@ -38,6 +40,27 @@ export class IncomingNoteDao implements NoteData { public ivpkM: PublicKey, ) {} + static fromPayloadAndNoteInfo( + payload: L1NotePayload, + noteInfo: NoteInfo, + dataStartIndexForTx: number, + ivpkM: PublicKey, + ) { + const noteHashIndexInTheWholeTree = BigInt(dataStartIndexForTx + noteInfo.noteHashIndex); + return new IncomingNoteDao( + payload.note, + payload.contractAddress, + payload.storageSlot, + payload.noteTypeId, + noteInfo.txHash, + noteInfo.nonce, + noteInfo.noteHash, + noteInfo.siloedNullifier, + noteHashIndexInTheWholeTree, + ivpkM, + ); + } + toBuffer(): Buffer { return serializeToBuffer([ this.note, diff --git a/yarn-project/pxe/src/database/outgoing_note_dao.ts b/yarn-project/pxe/src/database/outgoing_note_dao.ts index bfbd309ddf7..a2efe0375d4 100644 --- a/yarn-project/pxe/src/database/outgoing_note_dao.ts +++ b/yarn-project/pxe/src/database/outgoing_note_dao.ts @@ -1,9 +1,11 @@ -import { Note, TxHash } from '@aztec/circuit-types'; +import { type L1NotePayload, Note, TxHash } from '@aztec/circuit-types'; import { AztecAddress, Fr, Point, type PublicKey } from '@aztec/circuits.js'; import { NoteSelector } from '@aztec/foundation/abi'; import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; +import { type NoteInfo } from '../note_processor/utils/index.js'; + /** * A note with contextual data which was decrypted as outgoing. */ @@ -32,6 +34,26 @@ export class OutgoingNoteDao { public ovpkM: PublicKey, ) {} + static fromPayloadAndNoteInfo( + payload: L1NotePayload, + noteInfo: NoteInfo, + dataStartIndexForTx: number, + ivpkM: PublicKey, + ) { + const noteHashIndexInTheWholeTree = BigInt(dataStartIndexForTx + noteInfo.noteHashIndex); + return new OutgoingNoteDao( + payload.note, + payload.contractAddress, + payload.storageSlot, + payload.noteTypeId, + noteInfo.txHash, + noteInfo.nonce, + noteInfo.noteHash, + noteHashIndexInTheWholeTree, + ivpkM, + ); + } + toBuffer(): Buffer { return serializeToBuffer([ this.note, diff --git a/yarn-project/pxe/src/note_processor/note_processor.ts b/yarn-project/pxe/src/note_processor/note_processor.ts index ac9bf48b691..8a12764ff66 100644 --- a/yarn-project/pxe/src/note_processor/note_processor.ts +++ b/yarn-project/pxe/src/note_processor/note_processor.ts @@ -12,7 +12,7 @@ import { type IncomingNoteDao } from '../database/incoming_note_dao.js'; import { type PxeDatabase } from '../database/index.js'; import { type OutgoingNoteDao } from '../database/outgoing_note_dao.js'; import { getAcirSimulator } from '../simulator/index.js'; -import { produceNoteDaos } from './produce_note_dao.js'; +import { produceNoteDaos } from './utils/produce_note_daos.js'; /** * Contains all the decrypted data in this array so that we can later batch insert it all into the database. @@ -156,17 +156,19 @@ export class NoteProcessor { const payload = incomingNotePayload || outgoingNotePayload; - const txHash = block.body.txEffects[indexOfTxInABlock].txHash; + const txEffect = block.body.txEffects[indexOfTxInABlock]; const { incomingNote, outgoingNote, incomingDeferredNote, outgoingDeferredNote } = await produceNoteDaos( this.simulator, + this.db, incomingNotePayload ? this.ivpkM : undefined, outgoingNotePayload ? this.ovpkM : undefined, payload!, - txHash, + txEffect.txHash, noteHashes, dataStartIndexForTx, excludedIndices, this.log, + txEffect.unencryptedLogs, ); if (incomingNote) { @@ -296,8 +298,17 @@ export class NoteProcessor { const outgoingNotes: OutgoingNoteDao[] = []; for (const deferredNote of deferredNoteDaos) { - const { publicKey, note, contractAddress, storageSlot, noteTypeId, txHash, noteHashes, dataStartIndexForTx } = - deferredNote; + const { + publicKey, + note, + contractAddress, + storageSlot, + noteTypeId, + txHash, + noteHashes, + dataStartIndexForTx, + unencryptedLogs, + } = deferredNote; const payload = new L1NotePayload(note, contractAddress, storageSlot, noteTypeId); const isIncoming = publicKey.equals(this.ivpkM); @@ -310,6 +321,7 @@ export class NoteProcessor { const { incomingNote, outgoingNote } = await produceNoteDaos( this.simulator, + this.db, isIncoming ? this.ivpkM : undefined, isOutgoing ? this.ovpkM : undefined, payload, @@ -318,6 +330,7 @@ export class NoteProcessor { dataStartIndexForTx, excludedIndices, this.log, + unencryptedLogs, ); if (isIncoming) { diff --git a/yarn-project/pxe/src/note_processor/produce_note_dao.ts b/yarn-project/pxe/src/note_processor/produce_note_dao.ts deleted file mode 100644 index 5ffc396bc6c..00000000000 --- a/yarn-project/pxe/src/note_processor/produce_note_dao.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { type L1NotePayload, type TxHash } from '@aztec/circuit-types'; -import { Fr, type PublicKey } from '@aztec/circuits.js'; -import { computeNoteHashNonce, siloNullifier } from '@aztec/circuits.js/hash'; -import { type Logger } from '@aztec/foundation/log'; -import { type AcirSimulator, ContractNotFoundError } from '@aztec/simulator'; - -import { DeferredNoteDao } from '../database/deferred_note_dao.js'; -import { IncomingNoteDao } from '../database/incoming_note_dao.js'; -import { OutgoingNoteDao } from '../database/outgoing_note_dao.js'; - -/** - * Decodes a note from a transaction that we know was intended for us. - * Throws if we do not yet have the contract corresponding to the note in our database. - * Accepts a set of excluded indices, which are indices that have been assigned a note in the same tx. - * Inserts the index of the note into the excludedIndices set if the note is successfully decoded. - * - * @param ivpkM - The public counterpart to the secret key to be used in the decryption of incoming note logs. - * @param ovpkM - The public counterpart to the secret key to be used in the decryption of outgoing note logs. - * @param payload - An instance of l1NotePayload. - * @param txHash - The hash of the transaction that created the note. Equivalent to the first nullifier of the transaction. - * @param noteHashes - New note hashes in this transaction, one of which belongs to this note. - * @param dataStartIndexForTx - The next available leaf index for the note hash tree for this transaction. - * @param excludedIndices - Indices that have been assigned a note in the same tx. Notes in a tx can have the same l1NotePayload, we need to find a different index for each replicate. - * @param simulator - An instance of AcirSimulator. - * @returns An object containing the incoming, outgoing, and deferred notes. - */ -export async function produceNoteDaos( - simulator: AcirSimulator, - ivpkM: PublicKey | undefined, - ovpkM: PublicKey | undefined, - payload: L1NotePayload, - txHash: TxHash, - noteHashes: Fr[], - dataStartIndexForTx: number, - excludedIndices: Set, - log: Logger, -): Promise<{ - incomingNote: IncomingNoteDao | undefined; - outgoingNote: OutgoingNoteDao | undefined; - incomingDeferredNote: DeferredNoteDao | undefined; - outgoingDeferredNote: DeferredNoteDao | undefined; -}> { - if (!ivpkM && !ovpkM) { - throw new Error('Both ivpkM and ovpkM are undefined. Cannot create note.'); - } - - let incomingNote: IncomingNoteDao | undefined; - let outgoingNote: OutgoingNoteDao | undefined; - let incomingDeferredNote: DeferredNoteDao | undefined; - let outgoingDeferredNote: DeferredNoteDao | undefined; - - try { - if (ivpkM) { - const { noteHashIndex, nonce, noteHash, siloedNullifier } = await findNoteIndexAndNullifier( - simulator, - noteHashes, - txHash, - payload, - excludedIndices, - true, // For incoming we compute a nullifier (recipient of incoming is the party that nullifies). - ); - const index = BigInt(dataStartIndexForTx + noteHashIndex); - excludedIndices?.add(noteHashIndex); - - incomingNote = new IncomingNoteDao( - payload.note, - payload.contractAddress, - payload.storageSlot, - payload.noteTypeId, - txHash, - nonce, - noteHash, - siloedNullifier, - index, - ivpkM, - ); - } - } catch (e) { - if (e instanceof ContractNotFoundError) { - log.warn(e.message); - - if (ivpkM) { - incomingDeferredNote = new DeferredNoteDao( - ivpkM, - payload.note, - payload.contractAddress, - payload.storageSlot, - payload.noteTypeId, - txHash, - noteHashes, - dataStartIndexForTx, - ); - } - } else { - log.error(`Could not process note because of "${e}". Discarding note...`); - } - } - - try { - if (ovpkM) { - if (incomingNote) { - // Incoming note is defined meaning that this PXE has both the incoming and outgoing keys. We can skip computing - // note hash and note index since we already have them in the incoming note. - outgoingNote = new OutgoingNoteDao( - payload.note, - payload.contractAddress, - payload.storageSlot, - payload.noteTypeId, - txHash, - incomingNote.nonce, - incomingNote.noteHash, - incomingNote.index, - ovpkM, - ); - } else { - const { noteHashIndex, nonce, noteHash } = await findNoteIndexAndNullifier( - simulator, - noteHashes, - txHash, - payload, - excludedIndices, - false, // For outgoing we do not compute a nullifier. - ); - const index = BigInt(dataStartIndexForTx + noteHashIndex); - excludedIndices?.add(noteHashIndex); - outgoingNote = new OutgoingNoteDao( - payload.note, - payload.contractAddress, - payload.storageSlot, - payload.noteTypeId, - txHash, - nonce, - noteHash, - index, - ovpkM, - ); - } - } - } catch (e) { - if (e instanceof ContractNotFoundError) { - log.warn(e.message); - - if (ovpkM) { - outgoingDeferredNote = new DeferredNoteDao( - ovpkM, - payload.note, - payload.contractAddress, - payload.storageSlot, - payload.noteTypeId, - txHash, - noteHashes, - dataStartIndexForTx, - ); - } - } else { - log.error(`Could not process note because of "${e}". Discarding note...`); - } - } - - return { - incomingNote, - outgoingNote, - incomingDeferredNote, - outgoingDeferredNote, - }; -} - -/** - * Finds nonce, index, inner hash and siloed nullifier for a given note. - * @dev Finds the index in the note hash tree by computing the note hash with different nonce and see which hash for - * the current tx matches this value. - * @remarks This method assists in identifying spent notes in the note hash tree. - * @param siloedNoteHashes - Note hashes in the tx. One of them should correspond to the note we are looking for - * @param txHash - Hash of a tx the note was emitted in. - * @param l1NotePayload - The note payload. - * @param excludedIndices - Indices that have been assigned a note in the same tx. Notes in a tx can have the same - * l1NotePayload. We need to find a different index for each replicate. - * @param computeNullifier - A flag indicating whether to compute the nullifier or just return 0. - * @returns Nonce, index, inner hash and siloed nullifier for a given note. - * @throws If cannot find the nonce for the note. - */ -async function findNoteIndexAndNullifier( - simulator: AcirSimulator, - siloedNoteHashes: Fr[], - txHash: TxHash, - { contractAddress, storageSlot, noteTypeId, note }: L1NotePayload, - excludedIndices: Set, - computeNullifier: boolean, -) { - let noteHashIndex = 0; - let nonce: Fr | undefined; - let noteHash: Fr | undefined; - let siloedNoteHash: Fr | undefined; - let innerNullifier: Fr | undefined; - const firstNullifier = Fr.fromBuffer(txHash.toBuffer()); - - for (; noteHashIndex < siloedNoteHashes.length; ++noteHashIndex) { - if (excludedIndices.has(noteHashIndex)) { - continue; - } - - const siloedNoteHashFromTxEffect = siloedNoteHashes[noteHashIndex]; - if (siloedNoteHashFromTxEffect.equals(Fr.ZERO)) { - break; - } - - const expectedNonce = computeNoteHashNonce(firstNullifier, noteHashIndex); - ({ noteHash, siloedNoteHash, innerNullifier } = await simulator.computeNoteHashAndOptionallyANullifier( - contractAddress, - expectedNonce, - storageSlot, - noteTypeId, - computeNullifier, - note, - )); - - if (siloedNoteHashFromTxEffect.equals(siloedNoteHash)) { - nonce = expectedNonce; - break; - } - } - - if (!nonce) { - // NB: this used to warn the user that a decrypted log didn't match any notes. - // This was previously fine as we didn't chop transient note logs, but now we do (#1641 complete). - throw new Error('Cannot find a matching note hash for the note.'); - } - - return { - noteHashIndex, - nonce, - noteHash: noteHash!, - siloedNullifier: siloNullifier(contractAddress, innerNullifier!), - }; -} diff --git a/yarn-project/pxe/src/note_processor/utils/add_nullable_field_to_payload.ts b/yarn-project/pxe/src/note_processor/utils/add_nullable_field_to_payload.ts new file mode 100644 index 00000000000..478702f6e00 --- /dev/null +++ b/yarn-project/pxe/src/note_processor/utils/add_nullable_field_to_payload.ts @@ -0,0 +1,67 @@ +import { L1NotePayload, Note } from '@aztec/circuit-types'; +import { type Fr } from '@aztec/foundation/fields'; +import { ContractNotFoundError } from '@aztec/simulator'; + +import { type PxeDatabase } from '../../database/pxe_database.js'; + +/** + * Inserts publicly delivered nullable fields into the note payload. + * @param db - PXE database used to fetch contract instance and artifact. + * @param payload - Note payload to which nullable fields should be added. + * @param nullableFields - List of nullable fields to be added to the note payload. + * @returns Note payload with nullable fields added. + */ +export async function addNullableFieldsToPayload( + db: PxeDatabase, + payload: L1NotePayload, + nullableFields: Fr[], +): Promise { + const instance = await db.getContractInstance(payload.contractAddress); + if (!instance) { + throw new ContractNotFoundError( + `Could not find instance for ${payload.contractAddress.toString()}. This should never happen here as the partial notes flow should be triggered only for non-deferred notes.`, + ); + } + + const artifact = await db.getContractArtifact(instance.contractClassId); + if (!artifact) { + throw new Error( + `Could not find artifact for contract class ${instance.contractClassId.toString()}. This should never happen here as the partial notes flow should be triggered only for non-deferred notes.`, + ); + } + + const noteFields = Object.values(artifact.notes).find(note => note.id.equals(payload.noteTypeId))?.fields; + + if (!noteFields) { + throw new Error(`Could not find note fields for note type ${payload.noteTypeId.toString()}.`); + } + + // We sort note fields by index so that we can iterate over them in order. + noteFields.sort((a, b) => a.index - b.index); + + // Now we insert the nullable fields into the note based on its indices defined in the ABI. + const modifiedNoteItems = [...payload.note.items]; + let indexInNullable = 0; + for (let i = 0; i < noteFields.length; i++) { + const noteField = noteFields[i]; + if (noteField.nullable) { + if (i == noteFields.length - 1) { + // We are processing the last field so we simply insert the rest of the nullable fields at the end + modifiedNoteItems.push(...nullableFields.slice(indexInNullable)); + } else { + const noteFieldLength = noteFields[i + 1].index - noteField.index; + const nullableFieldsToInsert = nullableFields.slice(indexInNullable, indexInNullable + noteFieldLength); + indexInNullable += noteFieldLength; + // Now we insert the nullable fields at the note field index + modifiedNoteItems.splice(noteField.index, 0, ...nullableFieldsToInsert); + } + } + } + + return new L1NotePayload( + new Note(modifiedNoteItems), + payload.contractAddress, + payload.storageSlot, + payload.noteTypeId, + ); +} diff --git a/yarn-project/pxe/src/note_processor/utils/brute_force_note_info.ts b/yarn-project/pxe/src/note_processor/utils/brute_force_note_info.ts new file mode 100644 index 00000000000..f7515a61b25 --- /dev/null +++ b/yarn-project/pxe/src/note_processor/utils/brute_force_note_info.ts @@ -0,0 +1,82 @@ +import { type L1NotePayload, type TxHash } from '@aztec/circuit-types'; +import { computeNoteHashNonce, siloNullifier } from '@aztec/circuits.js/hash'; +import { Fr } from '@aztec/foundation/fields'; +import { type AcirSimulator } from '@aztec/simulator'; + +export interface NoteInfo { + noteHashIndex: number; + nonce: Fr; + noteHash: Fr; + siloedNullifier: Fr; + txHash: TxHash; +} + +/** + * Finds nonce, index, inner hash and siloed nullifier for a given note. + * @dev Finds the index in the note hash tree by computing the note hash with different nonce and see which hash for + * the current tx matches this value. + * @remarks This method assists in identifying spent notes in the note hash tree. + * @param siloedNoteHashes - Note hashes in the tx. One of them should correspond to the note we are looking for + * @param txHash - Hash of a tx the note was emitted in. + * @param l1NotePayload - The note payload. + * @param excludedIndices - Indices that have been assigned a note in the same tx. Notes in a tx can have the same + * l1NotePayload. We need to find a different index for each replicate. + * @param computeNullifier - A flag indicating whether to compute the nullifier or just return 0. + * @returns Nonce, index, inner hash and siloed nullifier for a given note. + * @throws If cannot find the nonce for the note. + */ +export async function bruteForceNoteInfo( + simulator: AcirSimulator, + siloedNoteHashes: Fr[], + txHash: TxHash, + { contractAddress, storageSlot, noteTypeId, note }: L1NotePayload, + excludedIndices: Set, + computeNullifier: boolean, +): Promise { + let noteHashIndex = 0; + let nonce: Fr | undefined; + let noteHash: Fr | undefined; + let siloedNoteHash: Fr | undefined; + let innerNullifier: Fr | undefined; + const firstNullifier = Fr.fromBuffer(txHash.toBuffer()); + + for (; noteHashIndex < siloedNoteHashes.length; ++noteHashIndex) { + if (excludedIndices.has(noteHashIndex)) { + continue; + } + + const siloedNoteHashFromTxEffect = siloedNoteHashes[noteHashIndex]; + if (siloedNoteHashFromTxEffect.equals(Fr.ZERO)) { + break; + } + + const expectedNonce = computeNoteHashNonce(firstNullifier, noteHashIndex); + ({ noteHash, siloedNoteHash, innerNullifier } = await simulator.computeNoteHashAndOptionallyANullifier( + contractAddress, + expectedNonce, + storageSlot, + noteTypeId, + computeNullifier, + note, + )); + + if (siloedNoteHashFromTxEffect.equals(siloedNoteHash)) { + nonce = expectedNonce; + break; + } + } + + if (!nonce) { + // NB: this used to warn the user that a decrypted log didn't match any notes. + // This was previously fine as we didn't chop transient note logs, but now we do (#1641 complete). + throw new Error('Cannot find a matching note hash for the note.'); + } + + return { + noteHashIndex, + nonce, + noteHash: noteHash!, + siloedNullifier: siloNullifier(contractAddress, innerNullifier!), + txHash, + }; +} diff --git a/yarn-project/pxe/src/note_processor/utils/index.ts b/yarn-project/pxe/src/note_processor/utils/index.ts new file mode 100644 index 00000000000..b272abb2e5c --- /dev/null +++ b/yarn-project/pxe/src/note_processor/utils/index.ts @@ -0,0 +1,2 @@ +export { produceNoteDaos } from './produce_note_daos.js'; +export { NoteInfo } from './brute_force_note_info.js'; diff --git a/yarn-project/pxe/src/note_processor/utils/produce_note_daos.ts b/yarn-project/pxe/src/note_processor/utils/produce_note_daos.ts new file mode 100644 index 00000000000..5198664efe2 --- /dev/null +++ b/yarn-project/pxe/src/note_processor/utils/produce_note_daos.ts @@ -0,0 +1,114 @@ +import { type L1NotePayload, type PublicKey, type TxHash, type UnencryptedTxL2Logs } from '@aztec/circuit-types'; +import { type Fr } from '@aztec/foundation/fields'; +import { type Logger } from '@aztec/foundation/log'; +import { type AcirSimulator } from '@aztec/simulator'; + +import { type DeferredNoteDao } from '../../database/deferred_note_dao.js'; +import { IncomingNoteDao } from '../../database/incoming_note_dao.js'; +import { OutgoingNoteDao } from '../../database/outgoing_note_dao.js'; +import { type PxeDatabase } from '../../database/pxe_database.js'; +import { produceNoteDaosForKey } from './produce_note_daos_for_key.js'; + +/** + * Decodes a note from a transaction that we know was intended for us. + * Throws if we do not yet have the contract corresponding to the note in our database. + * Accepts a set of excluded indices, which are indices that have been assigned a note in the same tx. + * Inserts the index of the note into the excludedIndices set if the note is successfully decoded. + * + * @param simulator - An instance of AcirSimulator. + * @param db - An instance of PxeDatabase. + * @param ivpkM - The public counterpart to the secret key to be used in the decryption of incoming note logs. + * @param ovpkM - The public counterpart to the secret key to be used in the decryption of outgoing note logs. + * @param payload - An instance of l1NotePayload. + * @param txHash - The hash of the transaction that created the note. Equivalent to the first nullifier of the transaction. + * @param noteHashes - New note hashes in this transaction, one of which belongs to this note. + * @param dataStartIndexForTx - The next available leaf index for the note hash tree for this transaction. + * @param excludedIndices - Indices that have been assigned a note in the same tx. Notes in a tx can have the same l1NotePayload, we need to find a different index for each replicate. + * @param logger - An instance of Logger. + * @param unencryptedLogs - Unencrypted logs for the transaction (used to complete partial notes). + * @returns An object containing the incoming, outgoing, and deferred notes. + */ +export async function produceNoteDaos( + simulator: AcirSimulator, + db: PxeDatabase, + ivpkM: PublicKey | undefined, + ovpkM: PublicKey | undefined, + payload: L1NotePayload, + txHash: TxHash, + noteHashes: Fr[], + dataStartIndexForTx: number, + excludedIndices: Set, + logger: Logger, + unencryptedLogs: UnencryptedTxL2Logs, +): Promise<{ + incomingNote: IncomingNoteDao | undefined; + outgoingNote: OutgoingNoteDao | undefined; + incomingDeferredNote: DeferredNoteDao | undefined; + outgoingDeferredNote: DeferredNoteDao | undefined; +}> { + // WARNING: This code is full of tech debt and will be refactored once we have final design of partial notes + // delivery. + if (!ivpkM && !ovpkM) { + throw new Error('Both ivpkM and ovpkM are undefined. Cannot create note.'); + } + + let incomingNote: IncomingNoteDao | undefined; + let outgoingNote: OutgoingNoteDao | undefined; + let incomingDeferredNote: DeferredNoteDao | undefined; + let outgoingDeferredNote: DeferredNoteDao | undefined; + + if (ivpkM) { + [incomingNote, incomingDeferredNote] = await produceNoteDaosForKey( + simulator, + db, + ivpkM, + payload, + txHash, + noteHashes, + dataStartIndexForTx, + excludedIndices, + logger, + unencryptedLogs, + IncomingNoteDao.fromPayloadAndNoteInfo, + ); + } + + if (ovpkM) { + if (incomingNote) { + // Incoming note is defined meaning that this PXE has both the incoming and outgoing keys. We can skip computing + // note hash and note index since we already have them in the incoming note. + outgoingNote = new OutgoingNoteDao( + payload.note, + payload.contractAddress, + payload.storageSlot, + payload.noteTypeId, + txHash, + incomingNote.nonce, + incomingNote.noteHash, + incomingNote.index, + ovpkM, + ); + } else { + [outgoingNote, outgoingDeferredNote] = await produceNoteDaosForKey( + simulator, + db, + ovpkM, + payload, + txHash, + noteHashes, + dataStartIndexForTx, + excludedIndices, + logger, + unencryptedLogs, + OutgoingNoteDao.fromPayloadAndNoteInfo, + ); + } + } + + return { + incomingNote, + outgoingNote, + incomingDeferredNote, + outgoingDeferredNote, + }; +} diff --git a/yarn-project/pxe/src/note_processor/utils/produce_note_daos_for_key.ts b/yarn-project/pxe/src/note_processor/utils/produce_note_daos_for_key.ts new file mode 100644 index 00000000000..3faa2b78aa2 --- /dev/null +++ b/yarn-project/pxe/src/note_processor/utils/produce_note_daos_for_key.ts @@ -0,0 +1,154 @@ +import { type L1NotePayload, type TxHash, UnencryptedTxL2Logs } from '@aztec/circuit-types'; +import { Fr, type PublicKey } from '@aztec/circuits.js'; +import { type Logger } from '@aztec/foundation/log'; +import { type AcirSimulator, ContractNotFoundError } from '@aztec/simulator'; + +import { DeferredNoteDao } from '../../database/deferred_note_dao.js'; +import { type PxeDatabase } from '../../database/pxe_database.js'; +import { addNullableFieldsToPayload } from './add_nullable_field_to_payload.js'; +import { type NoteInfo, bruteForceNoteInfo } from './brute_force_note_info.js'; + +export async function produceNoteDaosForKey( + simulator: AcirSimulator, + db: PxeDatabase, + pkM: PublicKey, + payload: L1NotePayload, + txHash: TxHash, + noteHashes: Fr[], + dataStartIndexForTx: number, + excludedIndices: Set, + logger: Logger, + unencryptedLogs: UnencryptedTxL2Logs, + daoConstructor: (payload: L1NotePayload, noteInfo: NoteInfo, dataStartIndexForTx: number, pkM: PublicKey) => T, +): Promise<[T | undefined, DeferredNoteDao | undefined]> { + let noteDao: T | undefined; + let deferredNoteDao: DeferredNoteDao | undefined; + + try { + const noteInfo = await bruteForceNoteInfo( + simulator, + noteHashes, + txHash, + payload, + excludedIndices, + true, // For incoming we compute a nullifier (recipient of incoming is the party that nullifies). + ); + excludedIndices?.add(noteInfo.noteHashIndex); + + noteDao = daoConstructor(payload, noteInfo, dataStartIndexForTx, pkM); + } catch (e) { + if (e instanceof ContractNotFoundError) { + logger.warn(e.message); + + deferredNoteDao = new DeferredNoteDao( + pkM, + payload.note, + payload.contractAddress, + payload.storageSlot, + payload.noteTypeId, + txHash, + noteHashes, + dataStartIndexForTx, + unencryptedLogs, + ); + } else if ( + (e as any).message.includes('failed to solve blackbox function: embedded_curve_add') || + (e as any).message.includes('Could not find key prefix.') + ) { + // TODO(#8769): This branch is a temporary partial notes delivery solution that should be eventually replaced. + // Both error messages above occur only when we are dealing with a partial note and are thrown when calling + // `note.compute_note_hash()` or `note.compute_nullifier_without_context()` + // in `compute_note_hash_and_optionally_a_nullifier` function. It occurs with partial notes because in the + // partial flow we receive a note log of a note that is missing some fields here and then we try to compute + // the note hash with MSM while some of the fields are zeroed out (or get a nsk for zero npk_m_hash). + noteDao = await handlePartialNote( + simulator, + db, + pkM, + payload, + txHash, + noteHashes, + dataStartIndexForTx, + excludedIndices, + logger, + unencryptedLogs, + daoConstructor, + ); + } else { + logger.error(`Could not process note because of "${e}". Discarding note...`); + } + } + + return [noteDao, deferredNoteDao]; +} + +async function handlePartialNote( + simulator: AcirSimulator, + db: PxeDatabase, + pkM: PublicKey, + payload: L1NotePayload, + txHash: TxHash, + noteHashes: Fr[], + dataStartIndexForTx: number, + excludedIndices: Set, + logger: Logger, + unencryptedLogs: UnencryptedTxL2Logs, + daoConstructor: (payload: L1NotePayload, noteInfo: NoteInfo, dataStartIndexForTx: number, pkM: PublicKey) => T, +): Promise { + let noteDao: T | undefined; + + for (const functionLogs of unencryptedLogs.functionLogs) { + for (const log of functionLogs.logs) { + const { data } = log; + // It is the expectation that partial notes will have the corresponding unencrypted log be multiple + // of Fr.SIZE_IN_BYTES as the nullable fields should be simply concatenated. + if (data.length % Fr.SIZE_IN_BYTES === 0) { + const nullableFields = []; + for (let i = 0; i < data.length; i += Fr.SIZE_IN_BYTES) { + const chunk = data.subarray(i, i + Fr.SIZE_IN_BYTES); + nullableFields.push(Fr.fromBuffer(chunk)); + } + + // We insert the nullable fields into the note and then we try to produce the note dao again + const payloadWithNullableFields = await addNullableFieldsToPayload(db, payload, nullableFields); + + let deferredNoteDao: DeferredNoteDao | undefined; + try { + [noteDao, deferredNoteDao] = await produceNoteDaosForKey( + simulator, + db, + pkM, + payloadWithNullableFields, + txHash, + noteHashes, + dataStartIndexForTx, + excludedIndices, + logger, + UnencryptedTxL2Logs.empty(), // We set unencrypted logs to empty to prevent infinite recursion. + daoConstructor, + ); + } catch (e) { + if (!(e as any).message.includes('Could not find key prefix.')) { + throw e; + } + } + + if (deferredNoteDao) { + // This should not happen as we should first get contract not found error before the blackbox func error. + throw new Error('Partial notes should never be deferred.'); + } + + if (noteDao) { + // We managed to complete the partial note so we terminate the search. + break; + } + } + } + } + + if (!noteDao) { + logger.error(`Partial note note found. Discarding note...`); + } + + return noteDao; +} diff --git a/yarn-project/txe/src/txe_service/txe_service.ts b/yarn-project/txe/src/txe_service/txe_service.ts index 425b56c423c..02cbba5ee6c 100644 --- a/yarn-project/txe/src/txe_service/txe_service.ts +++ b/yarn-project/txe/src/txe_service/txe_service.ts @@ -618,6 +618,7 @@ export class TXEService { _encryptedLog: ForeignCallSingle, _counter: ForeignCallSingle, ) { + // TODO(#8811): Implement return toForeignCallResult([]); } @@ -626,10 +627,12 @@ export class TXEService { _encryptedNote: ForeignCallArray, _counter: ForeignCallSingle, ) { + // TODO(#8811): Implement return toForeignCallResult([]); } emitEncryptedEventLog(_contractAddress: AztecAddress, _randomness: Fr, _encryptedEvent: Buffer, _counter: number) { + // TODO(#8811): Implement return toForeignCallResult([]); } @@ -750,4 +753,14 @@ export class TXEService { } return toForeignCallResult([toArray(witness)]); } + + emitUnencryptedLog(_contractAddress: ForeignCallSingle, _message: ForeignCallArray, _counter: ForeignCallSingle) { + // TODO(#8811): Implement + return toForeignCallResult([]); + } + + avmOpcodeEmitUnencryptedLog(_message: ForeignCallArray) { + // TODO(#8811): Implement + return toForeignCallResult([]); + } } diff --git a/yarn-project/types/src/abi/contract_artifact.ts b/yarn-project/types/src/abi/contract_artifact.ts index a474920eddf..3a02214798e 100644 --- a/yarn-project/types/src/abi/contract_artifact.ts +++ b/yarn-project/types/src/abi/contract_artifact.ts @@ -285,6 +285,10 @@ function getNoteTypes(input: NoirCompiledContract) { nullable: field.value.fields[1].value.value, }; }); + // TODO(benesjan): Nuke this once you nuke hacky_partial_note.nr + if (acc[name] && acc[name].fields.length > fields.length) { + return acc; + } acc[name] = { id: noteTypeId, typ: name,