From 94760afaf66c9ca0884743559fbd8418d2c698e2 Mon Sep 17 00:00:00 2001 From: ale Date: Sun, 10 Mar 2024 18:36:25 +0100 Subject: [PATCH 01/12] add wasm_bindgen wrapper for encode_payment_address --- src/keys.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/keys.rs b/src/keys.rs index b06aac2..77e7ee9 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -71,7 +71,7 @@ pub fn encode_extsk(extsk: &ExtendedSpendingKey, is_testnet: bool) -> String { encoding::encode_extended_spending_key(enc_str, extsk) } -pub fn encode_payment_address(addr: &PaymentAddress, is_testnet: bool) -> String { +pub fn encode_payment_address_internal(addr: &PaymentAddress, is_testnet: bool) -> String { let enc_str: &str = if is_testnet { TEST_NETWORK.hrp_sapling_payment_address() } else { @@ -80,6 +80,23 @@ pub fn encode_payment_address(addr: &PaymentAddress, is_testnet: bool) -> String encoding::encode_payment_address(enc_str, addr) } +#[wasm_bindgen] +pub fn encode_payment_address( + is_testnet: bool, + ser_payment_address: &[u8], +) -> Result { + let enc_payment_address = encode_payment_address_internal( + &PaymentAddress::from_bytes( + &ser_payment_address + .try_into() + .map_err(|_| "Bad ser_payment_address")?, + ) + .ok_or("Failed to deserialize payment address")?, + is_testnet, + ); + Ok(serde_wasm_bindgen::to_value(&enc_payment_address)?) +} + pub fn decode_extended_full_viewing_key( enc_extfvk: &str, is_testnet: bool, @@ -136,7 +153,7 @@ pub fn generate_default_payment_address( decode_extended_full_viewing_key(&enc_extfvk, is_testnet).map_err(|e| e.to_string())?; let (def_index, def_address) = extfvk.to_diversifiable_full_viewing_key().default_address(); Ok(serde_wasm_bindgen::to_value(&NewAddress { - address: encode_payment_address(&def_address, is_testnet), + address: encode_payment_address_internal(&def_address, is_testnet), diversifier_index: def_index.0.to_vec(), })?) } @@ -163,7 +180,7 @@ pub fn generate_next_shielding_payment_address( .ok_or("No valid indeces left")?; // There are so many valid addresses this should never happen Ok(serde_wasm_bindgen::to_value(&NewAddress { - address: encode_payment_address(&address, is_testnet), + address: encode_payment_address_internal(&address, is_testnet), diversifier_index: new_index.0.to_vec(), })?) } From 010b3f8e8d7f6c39d8767dcc54b500b5acbd6f80 Mon Sep 17 00:00:00 2001 From: ale Date: Sun, 10 Mar 2024 18:38:06 +0100 Subject: [PATCH 02/12] add interface SimplifiedNote --- js/pivx_shield.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/js/pivx_shield.ts b/js/pivx_shield.ts index bf569b2..c66d16a 100644 --- a/js/pivx_shield.ts +++ b/js/pivx_shield.ts @@ -504,6 +504,11 @@ export interface Note { rseed: number[]; } +export interface SimplifiedNote { + recipient: string; + value: number; +} + export interface ShieldData { extfvk: string; lastProcessedBlock: number; From 8542f734b0845325078c836b65a839dd66fe564c Mon Sep 17 00:00:00 2001 From: ale Date: Sun, 10 Mar 2024 18:46:08 +0100 Subject: [PATCH 03/12] add getShieldAddressFromNote --- js/pivx_shield.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/js/pivx_shield.ts b/js/pivx_shield.ts index c66d16a..3691742 100644 --- a/js/pivx_shield.ts +++ b/js/pivx_shield.ts @@ -295,6 +295,28 @@ export class PIVXShield { this.lastProcessedBlock = block.height; } + + /** + * + * @param note - Note and its corresponding witness + * Generate the nullifier for a given pair note, witness + */ + async generateNullifierFromNote(note: [Note, String]) { + return await this.callWorker( + "get_nullifier_from_note", + note, + this.extfvk, + this.isTestnet, + ); + } + + private async getShieldAddressFromNote(note: Note) { + return await this.callWorker( + "encode_payment_address", + this.isTestnet, + note.recipient, + ); + } async addTransaction(hex: string, decryptOnly = false) { const res = await this.callWorker( "handle_transaction", From 7dfd77cc4fdd88fabe99e5257e402365ccb8960b Mon Sep 17 00:00:00 2001 From: ale Date: Sun, 10 Mar 2024 18:47:43 +0100 Subject: [PATCH 04/12] add decryptTransactionOutputs --- js/pivx_shield.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/js/pivx_shield.ts b/js/pivx_shield.ts index 3691742..1b149ba 100644 --- a/js/pivx_shield.ts +++ b/js/pivx_shield.ts @@ -317,6 +317,17 @@ export class PIVXShield { note.recipient, ); } + async decryptTransactionOutputs(hex: string) { + const res = await this.addTransaction(hex, true); + const simplifiedNotes = []; + for (const [note, _] of res) { + simplifiedNotes.push({ + value: note.value, + recipient: await this.getShieldAddressFromNote(note), + }); + } + return simplifiedNotes; + } async addTransaction(hex: string, decryptOnly = false) { const res = await this.callWorker( "handle_transaction", From cebc69ee96d51eec450a9c53cd8cd5b51310eeff Mon Sep 17 00:00:00 2001 From: ale Date: Wed, 6 Mar 2024 18:58:27 +0100 Subject: [PATCH 05/12] add get_nullifier_from_note --- src/transaction.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/transaction.rs b/src/transaction.rs index 029d86c..1b95ec2 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -268,6 +268,27 @@ pub fn remove_spent_notes( Ok(serde_wasm_bindgen::to_value(&unspent_notes)?) } +#[wasm_bindgen] +pub fn get_nullifier_from_note( + note_data: JsValue, + enc_extfvk: String, + is_testnet: bool, +) -> Result { + let extfvk = + decode_extended_full_viewing_key(&enc_extfvk, is_testnet).map_err(|e| e.to_string())?; + let nullif_key = extfvk + .to_diversifiable_full_viewing_key() + .to_nk(Scope::External); + let (note, hex_witness): (Note, String) = serde_wasm_bindgen::from_value(note_data)?; + let witness = Cursor::new(hex::decode(hex_witness).map_err(|e| e.to_string())?); + let path = IncrementalWitness::::read(witness) + .map_err(|_| "Cannot read witness from buffer")? + .path() + .ok_or("Cannot find witness path")?; + let ser_nullifiers = hex::encode(note.nf(&nullif_key, path.position).0); + Ok(serde_wasm_bindgen::to_value(&ser_nullifiers)?) +} + #[derive(Serialize, Deserialize)] pub struct JSTransaction { pub txid: String, From 3d6e6d48814e3cbb6fc63ce4727025f42a28bce9 Mon Sep 17 00:00:00 2001 From: ale Date: Wed, 6 Mar 2024 18:59:19 +0100 Subject: [PATCH 06/12] store in memory all nullifiers belonging to the wallet --- js/pivx_shield.ts | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/js/pivx_shield.ts b/js/pivx_shield.ts index 1b149ba..211c482 100644 --- a/js/pivx_shield.ts +++ b/js/pivx_shield.ts @@ -88,6 +88,14 @@ export class PIVXShield { */ private pendingUnspentNotes: Map = new Map(); + /** + * + * @private + * Map nullifier -> Note + * It contains all notes in the history of the wallet, both spent and unspent + */ + private mapNullifierNote: Map = new Map(); + private promises: Map< string, { res: (...args: any) => void; rej: (...args: any) => void } @@ -289,19 +297,28 @@ export class PIVXShield { ); } for (const tx of block.txs) { - await this.addTransaction(tx.hex); + const decryptedNotes = await this.addTransaction(tx.hex); + // Add all the decryptedNotes to the Nullifier->Note map + for (const note of decryptedNotes) { + const nullifier = await this.generateNullifierFromNote(note); + const simplifiedNote = { + value: note[0].value, + recipient: await this.getShieldAddressFromNote(note[0]), + }; + this.mapNullifierNote.set(nullifier, simplifiedNote); + } + // Delete the corresponding pending transaction this.pendingUnspentNotes.delete(tx.txid); } this.lastProcessedBlock = block.height; } - /** * * @param note - Note and its corresponding witness * Generate the nullifier for a given pair note, witness */ - async generateNullifierFromNote(note: [Note, String]) { + private async generateNullifierFromNote(note: [Note, String]) { return await this.callWorker( "get_nullifier_from_note", note, @@ -520,6 +537,14 @@ export class PIVXShield { this.unspentNotes = []; this.pendingSpentNotes = new Map(); this.pendingUnspentNotes = new Map(); + this.mapNullifierNote = new Map(); + } + /* + * @param nullifier - A shield spent nullifier + * @returns true if the provided nullifier belongs to the wallet + */ + isMyNullifier(nullifier: string) { + return this.mapNullifierNote.has(nullifier); } } From 32f90f31e9b5126ea2bc868038308c11d78e04e8 Mon Sep 17 00:00:00 2001 From: ale Date: Wed, 6 Mar 2024 19:01:48 +0100 Subject: [PATCH 07/12] save and load the nullifier map --- js/pivx_shield.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/js/pivx_shield.ts b/js/pivx_shield.ts index 211c482..c08a57e 100644 --- a/js/pivx_shield.ts +++ b/js/pivx_shield.ts @@ -256,6 +256,7 @@ export class PIVXShield { diversifierIndex: this.diversifierIndex, unspentNotes: this.unspentNotes, isTestnet: this.isTestnet, + mapNullifierNote: Object.fromEntries(this.mapNullifierNote), }); } /** @@ -281,6 +282,9 @@ export class PIVXShield { shieldData.lastProcessedBlock, shieldData.commitmentTree, ); + pivxShield.mapNullifierNote = new Map( + Object.entries(shieldData.mapNullifierNote), + ); pivxShield.diversifierIndex = shieldData.diversifierIndex; pivxShield.unspentNotes = shieldData.unspentNotes; return pivxShield; From 122f8c927a23ad04e9760ee660428edcaf4a83a5 Mon Sep 17 00:00:00 2001 From: ale Date: Wed, 6 Mar 2024 19:08:47 +0100 Subject: [PATCH 08/12] add getter nullifier -> Note --- js/pivx_shield.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/js/pivx_shield.ts b/js/pivx_shield.ts index c08a57e..a7be413 100644 --- a/js/pivx_shield.ts +++ b/js/pivx_shield.ts @@ -519,6 +519,13 @@ export class PIVXShield { return this.lastProcessedBlock; } + /** + * @param nullifier - A sapling nullifier + * @returns the Note corresponding to a given nullifier + */ + getNoteFromNullifier(nullifier: string) { + return this.mapNullifierNote.get(nullifier); + } /** * @returns sapling root */ @@ -543,13 +550,6 @@ export class PIVXShield { this.pendingUnspentNotes = new Map(); this.mapNullifierNote = new Map(); } - /* - * @param nullifier - A shield spent nullifier - * @returns true if the provided nullifier belongs to the wallet - */ - isMyNullifier(nullifier: string) { - return this.mapNullifierNote.has(nullifier); - } } export interface UTXO { From c450786b0dc65cff8d375c93fa30242946de3bac Mon Sep 17 00:00:00 2001 From: Alessandro Rezzi Date: Sun, 28 Apr 2024 10:39:14 +0200 Subject: [PATCH 09/12] return the list of txs belonging to the wallet when handling a block --- js/pivx_shield.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/js/pivx_shield.ts b/js/pivx_shield.ts index a7be413..b5687ea 100644 --- a/js/pivx_shield.ts +++ b/js/pivx_shield.ts @@ -293,15 +293,20 @@ export class PIVXShield { /** * Loop through the txs of a block and update useful shield data * @param block - block outputted from any PIVX node + * @returns list of transactions belonging to the wallet */ async handleBlock(block: Block) { + let walletTransactions : string[] = []; if (this.lastProcessedBlock > block.height) { throw new Error( "Blocks must be processed in a monotonically increasing order!", ); } for (const tx of block.txs) { - const decryptedNotes = await this.addTransaction(tx.hex); + const {belongToWallet, decryptedNotes} = await this.addTransaction(tx.hex); + if(belongToWallet){ + walletTransactions.push(tx.hex); + } // Add all the decryptedNotes to the Nullifier->Note map for (const note of decryptedNotes) { const nullifier = await this.generateNullifierFromNote(note); @@ -315,6 +320,7 @@ export class PIVXShield { this.pendingUnspentNotes.delete(tx.txid); } this.lastProcessedBlock = block.height; + return walletTransactions; } /** @@ -339,9 +345,9 @@ export class PIVXShield { ); } async decryptTransactionOutputs(hex: string) { - const res = await this.addTransaction(hex, true); + const { decryptedNotes } = await this.addTransaction(hex, true); const simplifiedNotes = []; - for (const [note, _] of res) { + for (const [note, _] of decryptedNotes) { simplifiedNotes.push({ value: note.value, recipient: await this.getShieldAddressFromNote(note), @@ -366,7 +372,15 @@ export class PIVXShield { await this.removeSpentNotes(res.nullifiers); } } - return res.decrypted_notes; + // Check if the transaction belongs to the wallet + let belongToWallet = res.decrypted_notes.length > 0; + for (const nullifier of res.nullifiers) { + if(belongToWallet){ + break + } + belongToWallet = belongToWallet || this.mapNullifierNote.has(nullifier); + } + return {belongToWallet, decryptedNotes: res.decrypted_notes}; } /** @@ -435,7 +449,7 @@ export class PIVXShield { if (useShieldInputs) { this.pendingSpentNotes.set(txid, nullifiers); } - const decryptedNewNotes = (await this.addTransaction(txhex, true)).filter( + const decryptedNewNotes = (await this.addTransaction(txhex, true)).decryptedNotes.filter( (note) => !this.unspentNotes.some( (note2) => JSON.stringify(note2[0]) === JSON.stringify(note[0]), From bdef58ca62e1dc6e39904c78a6547862723e5034 Mon Sep 17 00:00:00 2001 From: Alessandro Rezzi Date: Fri, 4 Oct 2024 14:33:40 +0200 Subject: [PATCH 10/12] Split addTransaction and decryptTransaction --- js/pivx_shield.ts | 54 ++++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/js/pivx_shield.ts b/js/pivx_shield.ts index b5687ea..bb3b0df 100644 --- a/js/pivx_shield.ts +++ b/js/pivx_shield.ts @@ -296,15 +296,18 @@ export class PIVXShield { * @returns list of transactions belonging to the wallet */ async handleBlock(block: Block) { - let walletTransactions : string[] = []; + let walletTransactions: string[] = []; if (this.lastProcessedBlock > block.height) { throw new Error( "Blocks must be processed in a monotonically increasing order!", ); } for (const tx of block.txs) { - const {belongToWallet, decryptedNotes} = await this.addTransaction(tx.hex); - if(belongToWallet){ + const { belongToWallet, decryptedNotes } = await this.decryptTransaction( + tx.hex, + ); + await this.addTransaction(tx.hex); + if (belongToWallet) { walletTransactions.push(tx.hex); } // Add all the decryptedNotes to the Nullifier->Note map @@ -345,7 +348,7 @@ export class PIVXShield { ); } async decryptTransactionOutputs(hex: string) { - const { decryptedNotes } = await this.addTransaction(hex, true); + const { decryptedNotes } = await this.decryptTransaction(hex); const simplifiedNotes = []; for (const [note, _] of decryptedNotes) { simplifiedNotes.push({ @@ -355,32 +358,41 @@ export class PIVXShield { } return simplifiedNotes; } - async addTransaction(hex: string, decryptOnly = false) { + async addTransaction(hex: string) { const res = await this.callWorker( "handle_transaction", this.commitmentTree, hex, this.extfvk, this.isTestnet, - decryptOnly ? [] : this.unspentNotes, + this.unspentNotes, ); - if (!decryptOnly) { - this.commitmentTree = res.commitment_tree; - this.unspentNotes = res.decrypted_notes; + this.commitmentTree = res.commitment_tree; + this.unspentNotes = res.decrypted_notes; - if (res.nullifiers.length > 0) { - await this.removeSpentNotes(res.nullifiers); - } + if (res.nullifiers.length > 0) { + await this.removeSpentNotes(res.nullifiers); } - // Check if the transaction belongs to the wallet + } + + async decryptTransaction(hex: string) { + const res = await this.callWorker( + "handle_transaction", + this.commitmentTree, + hex, + this.extfvk, + this.isTestnet, + [], + ); + // Check if the transaction belongs to the wallet: let belongToWallet = res.decrypted_notes.length > 0; for (const nullifier of res.nullifiers) { - if(belongToWallet){ - break + if (belongToWallet) { + break; } belongToWallet = belongToWallet || this.mapNullifierNote.has(nullifier); } - return {belongToWallet, decryptedNotes: res.decrypted_notes}; + return { belongToWallet, decryptedNotes: res.decrypted_notes }; } /** @@ -449,16 +461,10 @@ export class PIVXShield { if (useShieldInputs) { this.pendingSpentNotes.set(txid, nullifiers); } - const decryptedNewNotes = (await this.addTransaction(txhex, true)).decryptedNotes.filter( - (note) => - !this.unspentNotes.some( - (note2) => JSON.stringify(note2[0]) === JSON.stringify(note[0]), - ), - ); - + const { decryptedNotes } = await this.decryptTransaction(txhex); this.pendingUnspentNotes.set( txid, - decryptedNewNotes.map((n) => n[0]), + decryptedNotes.map((n) => n[0]), ); return { hex: txhex, From 328b790b5942692a94c1daec40ed6c3060af6a76 Mon Sep 17 00:00:00 2001 From: Alessandro Rezzi Date: Thu, 10 Oct 2024 11:32:58 +0200 Subject: [PATCH 11/12] Handle the case in which loaded data is incompatible with current version --- js/pivx_shield.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/js/pivx_shield.ts b/js/pivx_shield.ts index bb3b0df..b8ca209 100644 --- a/js/pivx_shield.ts +++ b/js/pivx_shield.ts @@ -283,11 +283,17 @@ export class PIVXShield { shieldData.commitmentTree, ); pivxShield.mapNullifierNote = new Map( - Object.entries(shieldData.mapNullifierNote), + Object.entries(shieldData.mapNullifierNote ?? {}), ); pivxShield.diversifierIndex = shieldData.diversifierIndex; pivxShield.unspentNotes = shieldData.unspentNotes; - return pivxShield; + + // Shield activity update: mapNullifierNote must be present in the shieldData + let success = true; + if (!shieldData.mapNullifierNote) { + success = false; + } + return { pivxShield, success }; } /** From 1f3aaa7f8d487bc69dabb0bf3704bc605112d559 Mon Sep 17 00:00:00 2001 From: Alessandro Rezzi Date: Thu, 10 Oct 2024 14:36:02 +0200 Subject: [PATCH 12/12] Add unit test coverage for get_nullifier_from_note --- src/transaction.rs | 16 +++++++++++++--- src/transaction/test.rs | 15 ++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/transaction.rs b/src/transaction.rs index 1b95ec2..7adc3f9 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -23,6 +23,7 @@ use pivx_primitives::transaction::components::{OutPoint, TxOut}; pub use pivx_primitives::transaction::fees::fixed::FeeRule; pub use pivx_primitives::transaction::Transaction; pub use pivx_primitives::zip32::AccountId; +use pivx_primitives::zip32::ExtendedFullViewingKey; pub use pivx_primitives::zip32::ExtendedSpendingKey; pub use pivx_primitives::zip32::Scope; pub use pivx_proofs::prover::LocalTxProver; @@ -276,17 +277,26 @@ pub fn get_nullifier_from_note( ) -> Result { let extfvk = decode_extended_full_viewing_key(&enc_extfvk, is_testnet).map_err(|e| e.to_string())?; + let (note, hex_witness): (Note, String) = serde_wasm_bindgen::from_value(note_data)?; + let ser_nullifiers = + get_nullifier_from_note_internal(extfvk, note, hex_witness).map_err(|e| e.to_string())?; + Ok(serde_wasm_bindgen::to_value(&ser_nullifiers)?) +} + +pub fn get_nullifier_from_note_internal( + extfvk: ExtendedFullViewingKey, + note: Note, + hex_witness: String, +) -> Result> { let nullif_key = extfvk .to_diversifiable_full_viewing_key() .to_nk(Scope::External); - let (note, hex_witness): (Note, String) = serde_wasm_bindgen::from_value(note_data)?; let witness = Cursor::new(hex::decode(hex_witness).map_err(|e| e.to_string())?); let path = IncrementalWitness::::read(witness) .map_err(|_| "Cannot read witness from buffer")? .path() .ok_or("Cannot find witness path")?; - let ser_nullifiers = hex::encode(note.nf(&nullif_key, path.position).0); - Ok(serde_wasm_bindgen::to_value(&ser_nullifiers)?) + Ok(hex::encode(note.nf(&nullif_key, path.position).0)) } #[derive(Serialize, Deserialize)] diff --git a/src/transaction/test.rs b/src/transaction/test.rs index 3965e8d..74c65b6 100644 --- a/src/transaction/test.rs +++ b/src/transaction/test.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::transaction::create_transaction_internal; +use crate::transaction::{create_transaction_internal, get_nullifier_from_note_internal}; use super::handle_transaction_internal; use either::Either; @@ -15,6 +15,7 @@ use pivx_primitives::sapling::value::NoteValue; use pivx_primitives::sapling::Node; use pivx_primitives::sapling::Note; use pivx_primitives::sapling::Rseed::BeforeZip212; +use pivx_primitives::zip32::Scope; use std::error::Error; use std::io::Cursor; @@ -92,7 +93,7 @@ pub async fn test_create_transaction() -> Result<(), Box> { path.write(&mut path_vec)?; let path = hex::encode(path_vec); let tx = create_transaction_internal( - Either::Left(vec![(note.clone(), path)]), + Either::Left(vec![(note.clone(), path.clone())]), &extended_spending_key, output, address, @@ -108,7 +109,15 @@ pub async fn test_create_transaction() -> Result<(), Box> { nullifier, "5269442d8022af933774f9f22775566d92089a151ba733f6d751f5bb65a7f56d" ); - // When we implement mempool, test that new notes work correctly + // Verify that get_nullifier_from_note_internal yields the same nullifier + assert_eq!( + get_nullifier_from_note_internal( + extended_spending_key.to_extended_full_viewing_key(), + note.clone(), + path + )?, + "5269442d8022af933774f9f22775566d92089a151ba733f6d751f5bb65a7f56d" + ); Ok(()) }