Skip to content

Commit

Permalink
feat: delivering partial fields via unencrypted logs (#8725)
Browse files Browse the repository at this point in the history
Partial fields get delivered via unencrypted logs and NoteProcessor then looks for them if it fails to find note hash.
  • Loading branch information
benesjan authored Oct 4, 2024
1 parent 7f40411 commit 8a59b17
Show file tree
Hide file tree
Showing 19 changed files with 573 additions and 271 deletions.
7 changes: 5 additions & 2 deletions noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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() });

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
32 changes: 32 additions & 0 deletions noir-projects/noir-contracts/contracts/nft_contract/src/main.nr
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod types;
mod test;
mod hacky_partial_note;

use dep::aztec::macros::aztec;

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
23 changes: 2 additions & 21 deletions yarn-project/end-to-end/src/e2e_nft.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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(),
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion yarn-project/pxe/src/database/deferred_note_dao.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,6 +14,7 @@ export const randomDeferredNoteDao = ({
noteTypeId = NoteSelector.random(),
noteHashes = [Fr.random(), Fr.random()],
dataStartIndexForTx = randomInt(100),
unencryptedLogs = UnencryptedTxL2Logs.random(1, 1),
}: Partial<DeferredNoteDao> = {}) => {
return new DeferredNoteDao(
publicKey,
Expand All @@ -24,6 +25,7 @@ export const randomDeferredNoteDao = ({
txHash,
noteHashes,
dataStartIndexForTx,
unencryptedLogs,
);
};

Expand Down
6 changes: 5 additions & 1 deletion yarn-project/pxe/src/database/deferred_note_dao.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 {
Expand All @@ -38,6 +40,7 @@ export class DeferredNoteDao {
this.txHash,
new Vector(this.noteHashes),
this.dataStartIndexForTx,
this.unencryptedLogs,
);
}
static fromBuffer(buffer: Buffer | BufferReader) {
Expand All @@ -51,6 +54,7 @@ export class DeferredNoteDao {
reader.readObject(TxHash),
reader.readVector(Fr),
reader.readNumber(),
reader.readObject(UnencryptedTxL2Logs),
);
}
}
25 changes: 24 additions & 1 deletion yarn-project/pxe/src/database/incoming_note_dao.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand Down Expand Up @@ -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,
Expand Down
24 changes: 23 additions & 1 deletion yarn-project/pxe/src/database/outgoing_note_dao.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand Down Expand Up @@ -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,
Expand Down
23 changes: 18 additions & 5 deletions yarn-project/pxe/src/note_processor/note_processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand All @@ -318,6 +330,7 @@ export class NoteProcessor {
dataStartIndexForTx,
excludedIndices,
this.log,
unencryptedLogs,
);

if (isIncoming) {
Expand Down
Loading

0 comments on commit 8a59b17

Please sign in to comment.