diff --git a/sn_auditor/src/dag_db.rs b/sn_auditor/src/dag_db.rs index 0ed400dd05..641f0ef116 100644 --- a/sn_auditor/src/dag_db.rs +++ b/sn_auditor/src/dag_db.rs @@ -334,7 +334,7 @@ impl SpendDagDb { ) { let mut beta_tracking = self.beta_tracking.write().await; beta_tracking.processed_spends += 1; - beta_tracking.total_accumulated_utxo += spend.spend.spent_tx.outputs.len() as u64; + beta_tracking.total_accumulated_utxo += spend.spend.descendants.len() as u64; beta_tracking.total_on_track_utxo += utxos_for_further_track; // check for beta rewards reason @@ -347,7 +347,7 @@ impl SpendDagDb { // add to local rewards let addr = spend.address(); - let amount = spend.spend.amount; + let amount = spend.spend.amount(); let beta_participants_read = self.beta_participants.read().await; if let Some(user_name) = beta_participants_read.get(&user_name_hash) { diff --git a/sn_cli/src/bin/subcommands/wallet.rs b/sn_cli/src/bin/subcommands/wallet.rs index 9a497e38c9..03855f299b 100644 --- a/sn_cli/src/bin/subcommands/wallet.rs +++ b/sn_cli/src/bin/subcommands/wallet.rs @@ -53,18 +53,22 @@ impl WalletApiHelper { spend.spend.unique_pubkey, address.to_hex() ); - println!("reason {:?}, amount {}, inputs: {}, outputs: {}, royalties: {}, {:?} - {:?}", - spend.spend.reason, spend.spend.amount, spend.spend.spent_tx.inputs.len(), spend.spend.spent_tx.outputs.len(), - spend.spend.network_royalties.len(), spend.spend.spent_tx.inputs, spend.spend.spent_tx.outputs); + println!( + "reason {:?}, amount {}, inputs: {}, outputs: {}", + spend.spend.reason, + spend.spend.amount(), + spend.spend.ancestors.len(), + spend.spend.descendants.len() + ); println!("Inputs in hex str:"); - for input in spend.spend.spent_tx.inputs.iter() { - let address = SpendAddress::from_unique_pubkey(&input.unique_pubkey); + for input in spend.spend.ancestors.iter() { + let address = SpendAddress::from_unique_pubkey(input); println!("Input spend {}", address.to_hex()); } - println!("parent_tx inputs in hex str:"); - for input in spend.spend.parent_tx.inputs.iter() { - let address = SpendAddress::from_unique_pubkey(&input.unique_pubkey); - println!("parent_tx input spend {}", address.to_hex()); + println!("Outputs in hex str:"); + for (output, (amount, purpose)) in spend.spend.descendants.iter() { + let address = SpendAddress::from_unique_pubkey(output); + println!("Output {} with {amount} and {purpose:?}", address.to_hex()); } } println!("Available cash notes are:"); @@ -91,10 +95,9 @@ impl WalletApiHelper { let cash_notes = vec![cash_note.clone()]; let spent_unique_pubkeys: BTreeSet<_> = cash_note - .parent_tx - .inputs + .parent_spends .iter() - .map(|input| input.unique_pubkey()) + .map(|spend| spend.unique_pubkey()) .collect(); match self { diff --git a/sn_cli/src/bin/subcommands/wallet/hot_wallet.rs b/sn_cli/src/bin/subcommands/wallet/hot_wallet.rs index db51964612..f3ccb54be3 100644 --- a/sn_cli/src/bin/subcommands/wallet/hot_wallet.rs +++ b/sn_cli/src/bin/subcommands/wallet/hot_wallet.rs @@ -312,28 +312,15 @@ fn sign_transaction(tx: &str, root_dir: &Path, force: bool) -> Result<()> { let unsigned_transfer: UnsignedTransfer = rmp_serde::from_slice(&hex::decode(tx)?)?; println!("The unsigned transaction has been successfully decoded:"); - let mut spent_tx = None; for (i, (spend, _)) in unsigned_transfer.spends.iter().enumerate() { println!("\nSpending input #{i}:"); println!("\tKey: {}", spend.unique_pubkey.to_hex()); - println!("\tAmount: {}", spend.amount); - if let Some(ref tx) = spent_tx { - if tx != &spend.spent_tx { - bail!("Transaction seems corrupted, not all Spends (inputs) refer to the same transaction"); - } - } else { - spent_tx = Some(spend.spent_tx.clone()); - } - } + println!("\tAmount: {}", spend.amount()); - if let Some(ref tx) = spent_tx { - for (i, output) in tx.outputs.iter().enumerate() { - println!("\nOutput #{i}:"); - println!("\tKey: {}", output.unique_pubkey.to_hex()); - println!("\tAmount: {}", output.amount); + for (descendant, (amount, _purpose)) in spend.descendants.iter() { + println!("\tOutput Key: {}", descendant.to_hex()); + println!("\tAmount: {amount}"); } - } else { - bail!("Transaction is corrupted, no transaction information found."); } if !force { @@ -352,7 +339,7 @@ fn sign_transaction(tx: &str, root_dir: &Path, force: bool) -> Result<()> { let signed_spends = wallet.sign(unsigned_transfer.spends); for signed_spend in signed_spends.iter() { - if let Err(err) = signed_spend.verify(signed_spend.spent_tx_hash()) { + if let Err(err) = signed_spend.verify() { bail!("Signature or transaction generated is invalid: {err:?}"); } } diff --git a/sn_cli/src/bin/subcommands/wallet/wo_wallet.rs b/sn_cli/src/bin/subcommands/wallet/wo_wallet.rs index 4b1b52bd81..0825980db4 100644 --- a/sn_cli/src/bin/subcommands/wallet/wo_wallet.rs +++ b/sn_cli/src/bin/subcommands/wallet/wo_wallet.rs @@ -27,6 +27,12 @@ use std::{ }; use walkdir::WalkDir; +type SignedTx = ( + BTreeSet, + BTreeMap, + UniquePubkey, +); + // Please do not remove the blank lines in these doc comments. // They are used for inserting line breaks when the help menu is rendered in the UI. #[derive(Parser, Debug)] @@ -249,42 +255,24 @@ async fn broadcast_signed_spends( verify_store: bool, force: bool, ) -> Result<()> { - let (signed_spends, output_details, change_id): ( - BTreeSet, - BTreeMap, - UniquePubkey, - ) = rmp_serde::from_slice(&hex::decode(signed_tx)?)?; + let (signed_spends, output_details, change_id): SignedTx = + rmp_serde::from_slice(&hex::decode(signed_tx)?)?; println!("The signed transaction has been successfully decoded:"); - let mut transaction = None; for (i, signed_spend) in signed_spends.iter().enumerate() { println!("\nSpending input #{i}:"); println!("\tKey: {}", signed_spend.unique_pubkey().to_hex()); println!("\tAmount: {}", signed_spend.token()); - let linked_tx = signed_spend.spent_tx(); - if let Some(ref tx) = transaction { - if tx != &linked_tx { - bail!("Transaction seems corrupted, not all Spends (inputs) refer to the same transaction"); - } - } else { - transaction = Some(linked_tx); - } - if let Err(err) = signed_spend.verify(signed_spend.spent_tx_hash()) { + if let Err(err) = signed_spend.verify() { bail!("Transaction is invalid: {err:?}"); } - } - let tx = if let Some(tx) = transaction { - for (i, output) in tx.outputs.iter().enumerate() { - println!("\nOutput #{i}:"); - println!("\tKey: {}", output.unique_pubkey.to_hex()); - println!("\tAmount: {}", output.amount); + for (descendant, (amount, _purpose)) in signed_spend.spend.descendants.iter() { + println!("\tOutput Key: {}", descendant.to_hex()); + println!("\tAmount: {amount}"); } - tx - } else { - bail!("Transaction is corrupted, no transaction information found."); - }; + } if !force { println!( @@ -301,7 +289,7 @@ async fn broadcast_signed_spends( } println!("Broadcasting the transaction to the network..."); - let transfer = OfflineTransfer::from_transaction(signed_spends, tx, change_id, output_details)?; + let transfer = OfflineTransfer::from_transaction(signed_spends, change_id, output_details)?; // return the first CashNote (assuming there is only one because we only sent to one recipient) let cash_note = match &transfer.cash_notes_for_recipient[..] { diff --git a/sn_client/src/api.rs b/sn_client/src/api.rs index 0d78dea614..da220b1811 100644 --- a/sn_client/src/api.rs +++ b/sn_client/src/api.rs @@ -1016,7 +1016,7 @@ impl Client { } // check spend - match signed_spend.verify(signed_spend.spent_tx_hash()) { + match signed_spend.verify() { Ok(()) => { trace!("Verified signed spend got from network for {address:?}"); Ok(signed_spend.clone()) diff --git a/sn_client/src/audit/dag_crawling.rs b/sn_client/src/audit/dag_crawling.rs index ddc1ab7aa9..96a2592742 100644 --- a/sn_client/src/audit/dag_crawling.rs +++ b/sn_client/src/audit/dag_crawling.rs @@ -11,8 +11,8 @@ use crate::{Client, Error, SpendDag}; use futures::{future::join_all, StreamExt}; use sn_networking::{GetRecordError, NetworkError}; use sn_transfers::{ - SignedSpend, SpendAddress, SpendReason, WalletError, WalletResult, - DEFAULT_NETWORK_ROYALTIES_PK, GENESIS_SPEND_UNIQUE_KEY, NETWORK_ROYALTIES_PK, + OutputPurpose, SignedSpend, SpendAddress, SpendReason, UniquePubkey, WalletError, WalletResult, + GENESIS_SPEND_UNIQUE_KEY, }; use std::{ collections::{BTreeMap, BTreeSet}, @@ -91,7 +91,7 @@ impl Client { ); dag.insert(addr, spend.clone()); if let Some(sender) = &spend_processing { - let outputs = spend.spend.spent_tx.outputs.len() as u64; + let outputs = spend.spend.descendants.len() as u64; sender .send((spend, outputs)) .await @@ -194,27 +194,27 @@ impl Client { let mut utxos = BTreeSet::new(); // get first spend - let mut txs_to_follow = match self.crawl_spend(spend_addr).await { + let mut descendants_to_follow = match self.crawl_spend(spend_addr).await { InternalGetNetworkSpend::Spend(spend) => { let spend = *spend; - let txs = BTreeSet::from_iter([spend.spend.spent_tx.clone()]); + let descendants_to_follow = spend.spend.descendants.clone(); spend_processing .send(spend) .await .map_err(|e| WalletError::SpendProcessing(e.to_string()))?; - txs + descendants_to_follow } InternalGetNetworkSpend::DoubleSpend(spends) => { - let mut txs = BTreeSet::new(); + let mut descendants_to_follow = BTreeMap::new(); for spend in spends.into_iter() { - txs.insert(spend.spend.spent_tx.clone()); + descendants_to_follow.extend(spend.spend.descendants.clone()); spend_processing .send(spend) .await .map_err(|e| WalletError::SpendProcessing(e.to_string()))?; } - txs + descendants_to_follow } InternalGetNetworkSpend::NotFound => { // the cashnote was not spent yet, so it's an UTXO @@ -228,24 +228,19 @@ impl Client { }; // use iteration instead of recursion to avoid stack overflow - let mut known_tx = BTreeSet::new(); + let mut known_descendants: BTreeSet = BTreeSet::new(); let mut gen: u32 = 0; let start = std::time::Instant::now(); - while !txs_to_follow.is_empty() { - let mut next_gen_tx = BTreeSet::new(); + while !descendants_to_follow.is_empty() { + let mut next_gen_descendants = BTreeMap::new(); // list up all descendants let mut addrs = vec![]; - for descendant_tx in txs_to_follow.iter() { - let descendant_tx_hash = descendant_tx.hash(); - let descendant_keys = descendant_tx - .outputs - .iter() - .map(|output| output.unique_pubkey); - let addrs_to_follow = descendant_keys.map(|k| SpendAddress::from_unique_pubkey(&k)); - info!("Gen {gen} - Following descendant Tx : {descendant_tx_hash:?}"); - addrs.extend(addrs_to_follow); + for (descendant, (_amount, _purpose)) in descendants_to_follow.iter() { + let addrs_to_follow = SpendAddress::from_unique_pubkey(descendant); + info!("Gen {gen} - Following descendant : {descendant:?}"); + addrs.push(addrs_to_follow); } // get all spends in parallel @@ -255,7 +250,7 @@ impl Client { info!( "Gen {gen} - Getting {} spends from {} txs in batches of: {}", addrs.len(), - txs_to_follow.len(), + descendants_to_follow.len(), crate::MAX_CONCURRENT_TASKS, ); @@ -263,7 +258,7 @@ impl Client { while let Some((get_spend, addr)) = stream.next().await { match get_spend { InternalGetNetworkSpend::Spend(spend) => { - next_gen_tx.insert(spend.spend.spent_tx.clone()); + next_gen_descendants.extend(spend.spend.descendants.clone()); spend_processing .send(*spend.clone()) .await @@ -272,7 +267,7 @@ impl Client { InternalGetNetworkSpend::DoubleSpend(spends) => { info!("Fetched double spend(s) of len {} at {addr:?} from network, following all of them.", spends.len()); for s in spends.into_iter() { - next_gen_tx.insert(s.spend.spent_tx.clone()); + next_gen_descendants.extend(s.spend.descendants.clone()); spend_processing .send(s.clone()) .await @@ -289,11 +284,13 @@ impl Client { } } - // only follow tx we haven't already gathered - known_tx.extend(txs_to_follow.iter().map(|tx| tx.hash())); - txs_to_follow = next_gen_tx + // only follow descendants we haven't already gathered + let followed_descendants: BTreeSet = + descendants_to_follow.keys().copied().collect(); + known_descendants.extend(followed_descendants); + descendants_to_follow = next_gen_descendants .into_iter() - .filter(|tx| !known_tx.contains(&tx.hash())) + .filter(|(key, (_, _))| !known_descendants.contains(key)) .collect(); // go on to next gen @@ -337,24 +334,22 @@ impl Client { } // use iteration instead of recursion to avoid stack overflow - let mut txs_to_verify = BTreeSet::from_iter([new_spend.spend.parent_tx]); + let mut ancestors_to_verify = new_spend.spend.ancestors.clone(); let mut depth = 0; - let mut known_txs = BTreeSet::new(); + let mut known_ancestors = BTreeSet::new(); let start = std::time::Instant::now(); - while !txs_to_verify.is_empty() { - let mut next_gen_tx = BTreeSet::new(); + while !ancestors_to_verify.is_empty() { + let mut next_gen_ancestors = BTreeSet::new(); - for parent_tx in txs_to_verify { - let parent_tx_hash = parent_tx.hash(); - let parent_keys = parent_tx.inputs.iter().map(|input| input.unique_pubkey); - let addrs_to_verify = parent_keys.map(|k| SpendAddress::from_unique_pubkey(&k)); - debug!("Depth {depth} - checking parent Tx : {parent_tx_hash:?} with inputs: {addrs_to_verify:?}"); + for ancestor in ancestors_to_verify { + let addrs_to_verify = vec![SpendAddress::from_unique_pubkey(&ancestor)]; + debug!("Depth {depth} - checking parent : {ancestor:?} - {addrs_to_verify:?}"); // get all parent spends in parallel let tasks: Vec<_> = addrs_to_verify - .clone() - .map(|a| self.crawl_spend(a)) + .iter() + .map(|a| self.crawl_spend(*a)) .collect(); let mut spends = BTreeSet::new(); for (spend_get, a) in join_all(tasks) @@ -382,47 +377,49 @@ impl Client { } } let spends_len = spends.len(); - debug!("Depth {depth} - Got {spends_len} spends for parent Tx: {parent_tx_hash:?}"); - trace!("Spends for {parent_tx_hash:?} - {spends:?}"); - - // check if we reached the genesis Tx - known_txs.insert(parent_tx_hash); - if parent_tx == *sn_transfers::GENESIS_CASHNOTE_PARENT_TX - && spends - .iter() - .all(|s| s.spend.unique_pubkey == *sn_transfers::GENESIS_SPEND_UNIQUE_KEY) + debug!("Depth {depth} - Got {spends_len} spends for parent: {addrs_to_verify:?}"); + trace!("Spends for {addrs_to_verify:?} - {spends:?}"); + + // check if we reached the genesis spend + known_ancestors.extend(addrs_to_verify.clone()); + if spends + .iter() + .all(|s| s.spend.unique_pubkey == *sn_transfers::GENESIS_SPEND_UNIQUE_KEY) && spends.len() == 1 { - debug!("Depth {depth} - reached genesis Tx on one branch: {parent_tx_hash:?}"); + debug!( + "Depth {depth} - reached genesis spend on one branch: {addrs_to_verify:?}" + ); continue; } // add spends to the dag for (spend, addr) in spends.clone().into_iter().zip(addrs_to_verify) { - let spend_parent_tx = spend.spend.parent_tx.clone(); - let is_new_spend = dag.insert(addr, spend); + let is_new_spend = dag.insert(addr, spend.clone()); // no need to check this spend's parents if it was already in the DAG if is_new_spend { - next_gen_tx.insert(spend_parent_tx); + next_gen_ancestors.extend(spend.spend.ancestors.clone()); } } } // only verify parents we haven't already verified - txs_to_verify = next_gen_tx + ancestors_to_verify = next_gen_ancestors .into_iter() - .filter(|tx| !known_txs.contains(&tx.hash())) + .filter(|ancestor| { + !known_ancestors.contains(&SpendAddress::from_unique_pubkey(ancestor)) + }) .collect(); depth += 1; let elapsed = start.elapsed(); - let n = known_txs.len(); + let n = known_ancestors.len(); info!("Now at depth {depth} - Collected spends from {n} transactions in {elapsed:?}"); } let elapsed = start.elapsed(); - let n = known_txs.len(); + let n = known_ancestors.len(); info!("Collected the DAG branch all the way to known spends or genesis! Through {depth} generations, collecting spends from {n} transactions in {elapsed:?}"); // verify the DAG @@ -521,33 +518,15 @@ impl Client { /// Helper function to analyze spend for beta_tracking optimization. /// returns the new_utxos that needs to be further tracked. fn beta_track_analyze_spend(spend: &SignedSpend) -> BTreeSet { - // Filter out royalty outputs - let royalty_pubkeys: BTreeSet<_> = spend - .spend - .network_royalties - .iter() - .map(|derivation_idx| NETWORK_ROYALTIES_PK.new_unique_pubkey(derivation_idx)) - .collect(); - let default_royalty_pubkeys: BTreeSet<_> = spend - .spend - .network_royalties - .iter() - .map(|derivation_idx| DEFAULT_NETWORK_ROYALTIES_PK.new_unique_pubkey(derivation_idx)) - .collect(); - let new_utxos: BTreeSet<_> = spend .spend - .spent_tx - .outputs + .descendants .iter() - .filter_map(|output| { - if default_royalty_pubkeys.contains(&output.unique_pubkey) { - return None; - } - if !royalty_pubkeys.contains(&output.unique_pubkey) { - Some(SpendAddress::from_unique_pubkey(&output.unique_pubkey)) - } else { + .filter_map(|(key, (_amount, purpose))| { + if let OutputPurpose::RoyaltyFee(_) = purpose { None + } else { + Some(SpendAddress::from_unique_pubkey(key)) } }) .collect(); @@ -558,7 +537,7 @@ fn beta_track_analyze_spend(spend: &SignedSpend) -> BTreeSet { } else { trace!( "Spend original has {} outputs, tracking {} of them.", - spend.spend.spent_tx.outputs.len(), + spend.spend.descendants.len(), new_utxos.len() ); new_utxos diff --git a/sn_client/src/audit/spend_dag.rs b/sn_client/src/audit/spend_dag.rs index 35f1c9b803..934aad7998 100644 --- a/sn_client/src/audit/spend_dag.rs +++ b/sn_client/src/audit/spend_dag.rs @@ -12,7 +12,8 @@ use petgraph::graph::{DiGraph, NodeIndex}; use petgraph::visit::EdgeRef; use serde::{Deserialize, Serialize}; use sn_transfers::{ - is_genesis_spend, CashNoteRedemption, Hash, NanoTokens, SignedSpend, SpendAddress, + is_genesis_spend, CashNoteRedemption, Hash, NanoTokens, OutputPurpose, SignedSpend, + SpendAddress, }; use std::{ collections::{BTreeMap, BTreeSet}, @@ -170,8 +171,8 @@ impl SpendDag { }; // link to descendants - for descendant in spend.spend.spent_tx.outputs.iter() { - let descendant_addr = SpendAddress::from_unique_pubkey(&descendant.unique_pubkey); + for (descendant, (amount, _purpose)) in spend.spend.descendants.iter() { + let descendant_addr = SpendAddress::from_unique_pubkey(descendant); // add descendant if not already in dag let spends_at_addr = self.spends.entry(descendant_addr).or_insert_with(|| { @@ -182,8 +183,7 @@ impl SpendDag { // link to descendant for idx in spends_at_addr.indexes() { let descendant_idx = NodeIndex::new(idx); - self.dag - .update_edge(new_node_idx, descendant_idx, descendant.amount); + self.dag.update_edge(new_node_idx, descendant_idx, *amount); } } @@ -194,8 +194,8 @@ impl SpendDag { // link to ancestors const PENDING_AMOUNT: NanoTokens = NanoTokens::from(0); - for ancestor in spend.spend.parent_tx.inputs.iter() { - let ancestor_addr = SpendAddress::from_unique_pubkey(&ancestor.unique_pubkey); + for ancestor in spend.spend.ancestors.iter() { + let ancestor_addr = SpendAddress::from_unique_pubkey(ancestor); // add ancestor if not already in dag let spends_at_addr = self.spends.entry(ancestor_addr).or_insert_with(|| { @@ -214,26 +214,28 @@ impl SpendDag { let ancestor_idx = NodeIndex::new(*idx); let ancestor_given_amount = ancestor_spend .spend - .spent_tx - .outputs + .descendants .iter() - .find(|o| o.unique_pubkey == spend.spend.unique_pubkey) - .map(|o| o.amount) + .find(|(descendant, (_amount, _purpose))| { + **descendant == spend.spend.unique_pubkey + }) + .map(|(_descendant, (amount, _purpose))| *amount) .unwrap_or(PENDING_AMOUNT); self.dag .update_edge(ancestor_idx, new_node_idx, ancestor_given_amount); } DagEntry::DoubleSpend(multiple_ancestors) => { for (ancestor_spend, ancestor_idx) in multiple_ancestors { - if ancestor_spend.spend.spent_tx.hash() == spend.spend.parent_tx.hash() { + if ancestor_spend.address() == spend.address() { let ancestor_idx = NodeIndex::new(*ancestor_idx); let ancestor_given_amount = ancestor_spend .spend - .spent_tx - .outputs + .descendants .iter() - .find(|o| o.unique_pubkey == spend.spend.unique_pubkey) - .map(|o| o.amount) + .find(|(descendant, (_amount, _purpose))| { + **descendant == spend.spend.unique_pubkey + }) + .map(|(_descendant, (amount, _purpose))| *amount) .unwrap_or(PENDING_AMOUNT); self.dag .update_edge(ancestor_idx, new_node_idx, ancestor_given_amount); @@ -308,7 +310,7 @@ impl SpendDag { format!("{sender_hash:?}") }; let holders = statistics.entry(sender).or_default(); - holders.push(signed_spend.spend.amount); + holders.push(signed_spend.spend.amount()); } } } @@ -404,9 +406,11 @@ impl SpendDag { let spends = self.all_spends(); let mut royalties = Vec::new(); for s in spends { - for derivation_idx in s.spend.network_royalties.iter() { - let spend_addr = SpendAddress::from_unique_pubkey(&s.spend.unique_pubkey); - royalties.push(CashNoteRedemption::new(*derivation_idx, spend_addr)); + for (_descendant, (_amount, purpose)) in s.spend.descendants.iter() { + if let OutputPurpose::RoyaltyFee(derivation_index) = purpose { + let spend_addr = SpendAddress::from_unique_pubkey(&s.spend.unique_pubkey); + royalties.push(CashNoteRedemption::new(*derivation_index, spend_addr)); + } } } Ok(royalties) @@ -439,8 +443,8 @@ impl SpendDag { let addr = spend.address(); let mut ancestors = BTreeSet::new(); let mut faults = BTreeSet::new(); - for input in spend.spend.parent_tx.inputs.iter() { - let ancestor_addr = SpendAddress::from_unique_pubkey(&input.unique_pubkey); + for ancestor in spend.spend.ancestors.iter() { + let ancestor_addr = SpendAddress::from_unique_pubkey(ancestor); match self.spends.get(&ancestor_addr) { Some(DagEntry::Spend(ancestor_spend, _)) => { ancestors.insert(*ancestor_spend.clone()); @@ -457,7 +461,7 @@ impl SpendDag { }); let actual_ancestor: Vec<_> = multiple_ancestors .iter() - .filter(|(s, _)| s.spend.spent_tx.hash() == spend.spend.parent_tx.hash()) + .filter(|(s, _)| s.address() == spend.address()) .map(|(s, _)| s.clone()) .collect(); match actual_ancestor.as_slice() { @@ -498,11 +502,11 @@ impl SpendDag { }; let (spends, indexes) = (dag_entry.spends(), dag_entry.indexes()); - // get descendants via Tx data - let descendants_via_tx: BTreeSet = spends + // get descendants via spend + let descendants_via_spend: BTreeSet = spends .into_iter() - .flat_map(|s| s.spend.spent_tx.outputs.to_vec()) - .map(|o| SpendAddress::from_unique_pubkey(&o.unique_pubkey)) + .flat_map(|s| s.spend.descendants.keys()) + .map(SpendAddress::from_unique_pubkey) .collect(); // get descendants via DAG @@ -516,14 +520,14 @@ impl SpendDag { .collect(); // report inconsistencies - if descendants_via_dag != descendants_via_tx.iter().collect() { + if descendants_via_dag != descendants_via_spend.iter().collect() { if matches!(dag_entry, DagEntry::NotGatheredYet(_)) { debug!("Spend at {current_addr:?} was not gathered yet and has children refering to it, continuing traversal through those children..."); } else { warn!("Incoherent DAG at: {current_addr:?}"); return Err(DagError::IncoherentDag( *current_addr, - format!("descendants via DAG: {descendants_via_dag:?} do not match descendants via TX: {descendants_via_tx:?}") + format!("descendants via DAG: {descendants_via_dag:?} do not match descendants via spend: {descendants_via_spend:?}") )); } } @@ -574,10 +578,11 @@ impl SpendDag { for spend in spends { let gathered_descendants = spend .spend - .spent_tx - .outputs + .descendants .iter() - .map(|o| SpendAddress::from_unique_pubkey(&o.unique_pubkey)) + .map(|(descendant, (_amount, _purpose))| { + SpendAddress::from_unique_pubkey(descendant) + }) .filter_map(|a| self.spends.get(&a)) .filter_map(|s| { if matches!(s, DagEntry::NotGatheredYet(_)) { @@ -663,8 +668,8 @@ impl SpendDag { recorded_faults.insert(SpendFault::DoubleSpend(*addr)); let direct_descendants: BTreeSet = spends .iter() - .flat_map(|s| s.spend.spent_tx.outputs.iter()) - .map(|o| SpendAddress::from_unique_pubkey(&o.unique_pubkey)) + .flat_map(|s| s.spend.descendants.keys()) + .map(SpendAddress::from_unique_pubkey) .collect(); debug!("Making the direct descendants of the double spend at {addr:?} as faulty: {direct_descendants:?}"); for a in direct_descendants.iter() { @@ -709,14 +714,11 @@ impl SpendDag { Ok(recorded_faults) } - /// Verifies a single transaction and returns resulting errors and DAG poisoning spread + /// Verifies a single spend and returns resulting errors and DAG poisoning spread fn verify_parent_tx(&self, spend: &SignedSpend) -> Result, DagError> { let addr = spend.address(); let mut recorded_faults = BTreeSet::new(); - debug!( - "Verifying transaction {} at: {addr:?}", - spend.spend.parent_tx.hash().to_hex() - ); + debug!("Verifying spend at: {addr:?}"); // skip if spend matches genesis if is_genesis_spend(spend) { @@ -743,11 +745,7 @@ impl SpendDag { recorded_faults.extend(faults); // verify the tx - if let Err(e) = spend - .spend - .parent_tx - .verify_against_inputs_spent(&ancestor_spends) - { + if let Err(e) = spend.verify_parent_spends(&ancestor_spends) { warn!("Parent Tx verfication failed for spend at: {addr:?}: {e}"); recorded_faults.insert(SpendFault::InvalidTransaction(addr, format!("{e}"))); let poison = format!("ancestor transaction was poisoned at: {addr:?}: {e}"); @@ -765,11 +763,11 @@ impl SpendDag { poison: String, ) -> Result, DagError> { let mut recorded_faults = BTreeSet::new(); - let spent_tx = spend.spent_tx(); - let direct_descendants = spent_tx - .outputs + let direct_descendants = spend + .spend + .descendants .iter() - .map(|o| SpendAddress::from_unique_pubkey(&o.unique_pubkey)) + .map(|(descendant, (_amount, _purpose))| SpendAddress::from_unique_pubkey(descendant)) .collect::>(); let mut all_descendants = direct_descendants .iter() diff --git a/sn_client/src/wallet.rs b/sn_client/src/wallet.rs index 50858604c9..f86eb3f615 100644 --- a/sn_client/src/wallet.rs +++ b/sn_client/src/wallet.rs @@ -17,7 +17,7 @@ use sn_networking::{GetRecordError, PayeeQuote}; use sn_protocol::NetworkAddress; use sn_transfers::{ CashNote, DerivationIndex, HotWallet, MainPubkey, NanoTokens, Payment, PaymentQuote, - SignedSpend, SpendAddress, Transaction, Transfer, UniquePubkey, WalletError, WalletResult, + SignedSpend, SpendAddress, Transfer, UniquePubkey, WalletError, WalletResult, }; use std::{ collections::{BTreeMap, BTreeSet}, @@ -344,14 +344,13 @@ impl WalletClient { async fn send_signed_spends( &mut self, signed_spends: BTreeSet, - tx: Transaction, change_id: UniquePubkey, - output_details: BTreeMap, + output_details: BTreeMap, verify_store: bool, ) -> WalletResult { let created_cash_notes = self.wallet - .prepare_signed_transfer(signed_spends, tx, change_id, output_details)?; + .prepare_signed_transfer(signed_spends, change_id, output_details)?; // send to network if let Err(error) = self @@ -707,10 +706,9 @@ impl WalletClient { .map(|s| { let parent_spends: BTreeSet<_> = s .spend - .parent_tx - .inputs + .ancestors .iter() - .map(|i| SpendAddress::from_unique_pubkey(&i.unique_pubkey)) + .map(SpendAddress::from_unique_pubkey) .collect(); (s.address(), parent_spends) }) @@ -1224,9 +1222,8 @@ pub async fn broadcast_signed_spends( from: HotWallet, client: &Client, signed_spends: BTreeSet, - tx: Transaction, change_id: UniquePubkey, - output_details: BTreeMap, + output_details: BTreeMap, verify_store: bool, ) -> WalletResult { let mut wallet_client = WalletClient::new(client.clone(), from); @@ -1241,7 +1238,7 @@ pub async fn broadcast_signed_spends( } let new_cash_note = wallet_client - .send_signed_spends(signed_spends, tx, change_id, output_details, verify_store) + .send_signed_spends(signed_spends, change_id, output_details, verify_store) .await .map_err(|err| { error!("Could not send signed spends, err: {err:?}"); diff --git a/sn_networking/src/spends.rs b/sn_networking/src/spends.rs index faff6e82c3..edff197a44 100644 --- a/sn_networking/src/spends.rs +++ b/sn_networking/src/spends.rs @@ -9,7 +9,7 @@ use crate::{Network, NetworkError, Result}; use futures::future::join_all; use sn_transfers::{is_genesis_spend, SignedSpend, SpendAddress, TransferError}; -use std::{collections::BTreeSet, iter::Iterator}; +use std::collections::BTreeSet; #[derive(Debug)] pub enum SpendVerificationOk { @@ -29,7 +29,7 @@ impl Network { let mut result = SpendVerificationOk::Valid; let unique_key = spend.unique_pubkey(); debug!("Verifying spend {unique_key}"); - spend.verify(spend.spent_tx_hash())?; + spend.verify()?; // genesis does not have parents so we end here if is_genesis_spend(spend) { @@ -38,12 +38,7 @@ impl Network { } // get its parents - let parent_keys = spend - .spend - .parent_tx - .inputs - .iter() - .map(|input| input.unique_pubkey); + let parent_keys = spend.spend.ancestors.clone(); let tasks: Vec<_> = parent_keys .map(|parent| async move { let spend = self diff --git a/sn_networking/src/transfers.rs b/sn_networking/src/transfers.rs index f8566511d8..0432854707 100644 --- a/sn_networking/src/transfers.rs +++ b/sn_networking/src/transfers.rs @@ -15,8 +15,8 @@ use sn_protocol::{ NetworkAddress, PrettyPrintRecordKey, }; use sn_transfers::{ - CashNote, CashNoteRedemption, DerivationIndex, HotWallet, MainPubkey, SignedSpend, Transaction, - Transfer, UniquePubkey, + CashNote, CashNoteRedemption, DerivationIndex, HotWallet, MainPubkey, SignedSpend, Transfer, + UniquePubkey, }; use std::collections::BTreeSet; use tokio::task::JoinSet; @@ -142,8 +142,6 @@ impl Network { .map_err(|e| NetworkError::InvalidTransfer(format!("{e}")))?; let _ = parent_spends.insert(signed_spend.clone()); } - let parent_txs: BTreeSet = - parent_spends.iter().map(|s| s.spent_tx()).collect(); // get our outputs from Tx let our_output_unique_pubkeys: Vec<(UniquePubkey, DerivationIndex)> = cashnote_redemptions @@ -156,64 +154,15 @@ impl Network { let mut our_output_cash_notes = Vec::new(); for (id, derivation_index) in our_output_unique_pubkeys.into_iter() { - let src_tx = parent_txs - .iter() - .find(|tx| tx.outputs.iter().any(|o| o.unique_pubkey() == &id)) - .ok_or(NetworkError::InvalidTransfer( - "None of the CashNoteRedemptions are destined to our key".to_string(), - ))? - .clone(); - let signed_spends: BTreeSet = parent_spends - .iter() - .filter(|s| s.spent_tx_hash() == src_tx.hash()) - .cloned() - .collect(); let cash_note = CashNote { unique_pubkey: id, - parent_tx: src_tx, - parent_spends: signed_spends, + parent_spends: parent_spends.clone(), main_pubkey, derivation_index, }; our_output_cash_notes.push(cash_note); } - // check Txs and parent spends are valid - trace!("Validating parent spends"); - for tx in parent_txs { - let tx_inputs_keys: Vec<_> = tx.inputs.iter().map(|i| i.unique_pubkey()).collect(); - - // get the missing inputs spends from the network - let mut tasks = JoinSet::new(); - for input_key in tx_inputs_keys { - if parent_spends.iter().any(|s| s.unique_pubkey() == input_key) { - continue; - } - let self_clone = self.clone(); - let addr = SpendAddress::from_unique_pubkey(input_key); - let _ = tasks.spawn(async move { self_clone.get_spend(addr).await }); - } - while let Some(result) = tasks.join_next().await { - let signed_spend = result - .map_err(|e| NetworkError::FailedToGetSpend(format!("{e}")))? - .map_err(|e| NetworkError::InvalidTransfer(format!("{e}")))?; - let _ = parent_spends.insert(signed_spend.clone()); - } - - // verify the Tx against the inputs spends - let input_spends: BTreeSet<_> = parent_spends - .iter() - .filter(|s| s.spent_tx_hash() == tx.hash()) - .cloned() - .collect(); - tx.verify_against_inputs_spent(&input_spends).map_err(|e| { - NetworkError::InvalidTransfer(format!( - "Payment parent Tx {:?} invalid: {e}", - tx.hash() - )) - })?; - } - Ok(our_output_cash_notes) } } diff --git a/sn_transfers/benches/reissue.rs b/sn_transfers/benches/reissue.rs index c8aae15664..ee7a3a3603 100644 --- a/sn_transfers/benches/reissue.rs +++ b/sn_transfers/benches/reissue.rs @@ -8,7 +8,7 @@ #![allow(clippy::from_iter_instead_of_collect, clippy::unwrap_used)] -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{criterion_group, criterion_main, Criterion}; use sn_transfers::{ create_first_cash_note_from_key, rng, CashNote, DerivationIndex, MainSecretKey, NanoTokens, OfflineTransfer, SpendReason, @@ -49,20 +49,12 @@ fn bench_reissue_1_to_100(c: &mut Criterion) { panic!("cashnote double spend"); }; } - let spent_tx = offline_transfer.tx; - let signed_spends: BTreeSet<_> = offline_transfer.all_spend_requests.into_iter().collect(); // bench verification - c.bench_function(&format!("reissue split 1 to {N_OUTPUTS}"), |b| { + c.bench_function(&format!("reissue split 1 to {N_OUTPUTS}"), |_b| { #[cfg(unix)] let guard = pprof::ProfilerGuard::new(100).unwrap(); - b.iter(|| { - black_box(spent_tx.clone()) - .verify_against_inputs_spent(&signed_spends) - .unwrap(); - }); - #[cfg(unix)] if let Ok(report) = guard.report().build() { let file = @@ -135,30 +127,19 @@ fn bench_reissue_100_to_1(c: &mut Criterion) { )]; // create transfer to merge all of the cashnotes into one - let many_to_one_transfer = OfflineTransfer::new( + let _many_to_one_transfer = OfflineTransfer::new( many_cashnotes, one_single_recipient, starting_main_key.main_pubkey(), SpendReason::default(), ) .expect("transfer to succeed"); - let merge_spent_tx = many_to_one_transfer.tx.clone(); - let signed_spends: Vec<_> = many_to_one_transfer - .all_spend_requests - .into_iter() - .collect(); // bench verification - c.bench_function(&format!("reissue merge {N_OUTPUTS} to 1"), |b| { + c.bench_function(&format!("reissue merge {N_OUTPUTS} to 1"), |_b| { #[cfg(unix)] let guard = pprof::ProfilerGuard::new(100).unwrap(); - b.iter(|| { - black_box(&merge_spent_tx) - .verify_against_inputs_spent(&signed_spends) - .unwrap(); - }); - #[cfg(unix)] if let Ok(report) = guard.report().build() { let file = diff --git a/sn_transfers/src/cashnotes.rs b/sn_transfers/src/cashnotes.rs index 526b5b783c..63743c252d 100644 --- a/sn_transfers/src/cashnotes.rs +++ b/sn_transfers/src/cashnotes.rs @@ -11,19 +11,21 @@ mod builder; mod cashnote; mod hash; mod nano; +mod output_purpose; mod signed_spend; mod spend_reason; mod transaction; mod unique_keys; pub(crate) use builder::{CashNoteBuilder, TransactionBuilder}; -pub(crate) use transaction::{Input, Output}; +pub(crate) use transaction::Input; pub use address::SpendAddress; pub use builder::UnsignedTransfer; pub use cashnote::CashNote; pub use hash::Hash; pub use nano::NanoTokens; +pub use output_purpose::OutputPurpose; pub use signed_spend::{SignedSpend, Spend}; pub use spend_reason::SpendReason; pub use transaction::Transaction; @@ -32,8 +34,31 @@ pub use unique_keys::{DerivationIndex, DerivedSecretKey, MainPubkey, MainSecretK #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::TransferError; - use transaction::Output; + use crate::{cashnotes::output_purpose::OutputPurpose, TransferError}; + + use std::collections::{BTreeMap, BTreeSet}; + + fn generate_parent_spends( + derived_sk: DerivedSecretKey, + amount: u64, + output: UniquePubkey, + ) -> BTreeSet { + let mut descendants = BTreeMap::new(); + let _ = descendants.insert(output, (NanoTokens::from(amount), OutputPurpose::default())); + let spend = Spend { + unique_pubkey: derived_sk.unique_pubkey(), + reason: SpendReason::default(), + ancestors: BTreeSet::new(), + descendants, + }; + let mut parent_spends = BTreeSet::new(); + let derived_key_sig = derived_sk.sign(&spend.to_bytes_for_signing()); + let _ = parent_spends.insert(SignedSpend { + spend, + derived_key_sig, + }); + parent_spends + } #[test] fn from_hex_should_deserialize_a_hex_encoded_string_to_a_cashnote() -> Result<(), TransferError> @@ -43,14 +68,16 @@ pub(crate) mod tests { let main_key = MainSecretKey::random_from_rng(&mut rng); let derivation_index = DerivationIndex::random(&mut rng); let derived_key = main_key.derive_key(&derivation_index); - let tx = Transaction { - inputs: vec![], - outputs: vec![Output::new(derived_key.unique_pubkey(), amount)], - }; + + let parent_spends = generate_parent_spends( + main_key.derive_key(&DerivationIndex::random(&mut rng)), + amount, + derived_key.unique_pubkey(), + ); + let cashnote = CashNote { unique_pubkey: derived_key.unique_pubkey(), - parent_tx: tx, - parent_spends: Default::default(), + parent_spends, main_pubkey: main_key.main_pubkey(), derivation_index, }; @@ -70,14 +97,16 @@ pub(crate) mod tests { let main_key = MainSecretKey::random_from_rng(&mut rng); let derivation_index = DerivationIndex::random(&mut rng); let derived_key = main_key.derive_key(&derivation_index); - let tx = Transaction { - inputs: vec![], - outputs: vec![Output::new(derived_key.unique_pubkey(), amount)], - }; + + let parent_spends = generate_parent_spends( + main_key.derive_key(&DerivationIndex::random(&mut rng)), + amount, + derived_key.unique_pubkey(), + ); + let cashnote = CashNote { unique_pubkey: derived_key.unique_pubkey(), - parent_tx: tx, - parent_spends: Default::default(), + parent_spends, main_pubkey: main_key.main_pubkey(), derivation_index, }; @@ -100,15 +129,15 @@ pub(crate) mod tests { let derivation_index = DerivationIndex::random(&mut rng); let derived_key = main_key.derive_key(&derivation_index); - let tx = Transaction { - inputs: vec![], - outputs: vec![Output::new(derived_key.unique_pubkey(), amount)], - }; + let parent_spends = generate_parent_spends( + main_key.derive_key(&DerivationIndex::random(&mut rng)), + amount, + derived_key.unique_pubkey(), + ); let cashnote = CashNote { unique_pubkey: derived_key.unique_pubkey(), - parent_tx: tx, - parent_spends: Default::default(), + parent_spends, main_pubkey: main_key.main_pubkey(), derivation_index, }; @@ -125,20 +154,13 @@ pub(crate) mod tests { #[test] fn test_cashnote_without_inputs_fails_verification() -> Result<(), TransferError> { let mut rng = crate::rng::from_seed([0u8; 32]); - let amount = 100; let main_key = MainSecretKey::random_from_rng(&mut rng); let derivation_index = DerivationIndex::random(&mut rng); let derived_key = main_key.derive_key(&derivation_index); - let tx = Transaction { - inputs: vec![], - outputs: vec![Output::new(derived_key.unique_pubkey(), amount)], - }; - let cashnote = CashNote { unique_pubkey: derived_key.unique_pubkey(), - parent_tx: tx, parent_spends: Default::default(), main_pubkey: main_key.main_pubkey(), derivation_index, diff --git a/sn_transfers/src/cashnotes/builder.rs b/sn_transfers/src/cashnotes/builder.rs index 946d8d85e0..29b1a05a1a 100644 --- a/sn_transfers/src/cashnotes/builder.rs +++ b/sn_transfers/src/cashnotes/builder.rs @@ -7,32 +7,26 @@ // permissions and limitations relating to use of the SAFE Network Software. use super::{ - spend_reason::SpendReason, - transaction::{Output, Transaction}, - CashNote, DerivationIndex, DerivedSecretKey, Input, MainPubkey, NanoTokens, SignedSpend, Spend, + output_purpose::OutputPurpose, spend_reason::SpendReason, transaction::Output, CashNote, + DerivationIndex, DerivedSecretKey, Input, MainPubkey, NanoTokens, SignedSpend, Spend, UniquePubkey, }; -use crate::{Result, TransferError}; +use crate::Result; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; -pub type InputSrcTx = Transaction; - /// Unsigned Transfer #[derive(custom_debug::Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct UnsignedTransfer { - /// This is the transaction where all the below - /// spends were made and cash_notes created. - pub tx: Transaction, /// The unsigned spends with their corresponding owner's key derivation index. pub spends: BTreeSet<(Spend, DerivationIndex)>, /// The cash_note holding surplus tokens after /// spending the necessary input cash_notes. pub change_id: UniquePubkey, /// Information for aggregating signed spends and generating the final CashNote outputs. - pub output_details: BTreeMap, + pub output_details: BTreeMap, } /// A builder to create a Transaction from @@ -41,22 +35,24 @@ pub struct UnsignedTransfer { pub struct TransactionBuilder { inputs: Vec, outputs: Vec, - input_details: BTreeMap, InputSrcTx, DerivationIndex)>, - output_details: BTreeMap, + input_details: + BTreeMap, DerivationIndex, UniquePubkey)>, + output_details: BTreeMap, } impl TransactionBuilder { - /// Add an input given a the Input, the input's derived_key and the input's src transaction + /// Add an input: + /// the input's derived_key and derivation_index, the spend contains the input as one of its output pub fn add_input( mut self, input: Input, derived_key: Option, - input_src_tx: InputSrcTx, derivation_index: DerivationIndex, + input_src_spend: UniquePubkey, ) -> Self { self.input_details.insert( *input.unique_pubkey(), - (derived_key, input_src_tx, derivation_index), + (derived_key, derivation_index, input_src_spend), ); self.inputs.push(input); self @@ -65,10 +61,17 @@ impl TransactionBuilder { /// Add an input given an iterator over the Input, the input's derived_key and the input's src transaction pub fn add_inputs( mut self, - inputs: impl IntoIterator, InputSrcTx, DerivationIndex)>, + inputs: impl IntoIterator< + Item = ( + Input, + Option, + DerivationIndex, + UniquePubkey, + ), + >, ) -> Self { - for (input, derived_key, input_src_tx, derivation_index) in inputs.into_iter() { - self = self.add_input(input, derived_key, input_src_tx, derivation_index); + for (input, derived_key, derivation_index, input_src_spend) in inputs.into_iter() { + self = self.add_input(input, derived_key, derivation_index, input_src_spend); } self } @@ -83,7 +86,7 @@ impl TransactionBuilder { let unique_pubkey = main_pubkey.new_unique_pubkey(&derivation_index); self.output_details - .insert(unique_pubkey, (main_pubkey, derivation_index)); + .insert(unique_pubkey, (main_pubkey, derivation_index, token)); let output = Output::new(unique_pubkey, token.as_nano()); self.outputs.push(output); @@ -102,27 +105,30 @@ impl TransactionBuilder { } /// Build the Transaction by signing the inputs. Return a CashNoteBuilder. - pub fn build( - self, - reason: SpendReason, - network_royalties: Vec, - ) -> CashNoteBuilder { - let spent_tx = Transaction { - inputs: self.inputs, - outputs: self.outputs, - }; + pub fn build(self, reason: SpendReason) -> CashNoteBuilder { let mut signed_spends = BTreeSet::new(); - for input in &spent_tx.inputs { - if let Some((Some(derived_key), input_src_tx, _)) = + + let mut descendants = BTreeMap::new(); + for output in self.outputs.iter() { + // TODO: use proper OutputPurpose + let _ = descendants.insert( + output.unique_pubkey, + (output.amount, OutputPurpose::default()), + ); + } + + for input in &self.inputs { + if let Some((Some(derived_key), _, input_src_spend)) = self.input_details.get(&input.unique_pubkey) { + let mut ancestors = BTreeSet::new(); + let _ = ancestors.insert(*input_src_spend); + let spend = Spend { unique_pubkey: *input.unique_pubkey(), - spent_tx: spent_tx.clone(), reason: reason.clone(), - amount: input.amount, - parent_tx: input_src_tx.clone(), - network_royalties: network_royalties.clone(), + ancestors, + descendants: descendants.clone(), }; let derived_key_sig = derived_key.sign(&spend.to_bytes_for_signing()); signed_spends.insert(SignedSpend { @@ -132,39 +138,42 @@ impl TransactionBuilder { } } - CashNoteBuilder::new(spent_tx, self.output_details, signed_spends) + CashNoteBuilder::new(self.output_details, signed_spends) } /// Build the UnsignedTransfer which contains the generated (unsigned) Spends. pub fn build_unsigned_transfer( self, reason: SpendReason, - network_royalties: Vec, change_id: UniquePubkey, ) -> Result { - let tx = Transaction { - inputs: self.inputs, - outputs: self.outputs, - }; + let mut descendants = BTreeMap::new(); + for output in self.outputs.iter() { + // TODO: use proper OutputPurpose + let _ = descendants.insert( + output.unique_pubkey, + (output.amount, OutputPurpose::default()), + ); + } let mut spends = BTreeSet::new(); - for input in &tx.inputs { - if let Some((_, input_src_tx, derivation_index)) = + for input in &self.inputs { + if let Some((_, derivation_index, input_src_spend)) = self.input_details.get(&input.unique_pubkey) { + let mut ancestors = BTreeSet::new(); + let _ = ancestors.insert(*input_src_spend); + let spend = Spend { unique_pubkey: *input.unique_pubkey(), - spent_tx: tx.clone(), reason: reason.clone(), - amount: input.amount, - parent_tx: input_src_tx.clone(), - network_royalties: network_royalties.clone(), + ancestors, + descendants: descendants.clone(), }; spends.insert((spend, *derivation_index)); } } Ok(UnsignedTransfer { - tx, spends, change_id, output_details: self.output_details, @@ -175,20 +184,17 @@ impl TransactionBuilder { /// A Builder for aggregating SignedSpends and generating the final CashNote outputs. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CashNoteBuilder { - pub spent_tx: Transaction, - pub output_details: BTreeMap, + pub output_details: BTreeMap, pub signed_spends: BTreeSet, } impl CashNoteBuilder { /// Create a new CashNoteBuilder. pub fn new( - spent_tx: Transaction, - output_details: BTreeMap, + output_details: BTreeMap, signed_spends: BTreeSet, ) -> Self { Self { - spent_tx, output_details, signed_spends, } @@ -200,16 +206,8 @@ impl CashNoteBuilder { self.signed_spends.iter().collect() } - /// Build the output CashNotes, verifying the transaction and SignedSpends. - /// - /// See TransactionVerifier::verify() for a description of - /// verifier requirements. + /// Build the output CashNotes pub fn build(self) -> Result> { - // Verify the tx, along with signed spends. - // Note that we do this just once for entire tx, not once per output CashNote. - self.spent_tx - .verify_against_inputs_spent(self.signed_spends.iter())?; - // Build output CashNotes. self.build_output_cashnotes() } @@ -221,26 +219,20 @@ impl CashNoteBuilder { // Private helper to build output CashNotes. fn build_output_cashnotes(self) -> Result> { - self.spent_tx - .outputs - .iter() - .map(|output| { - let (main_pubkey, derivation_index) = self - .output_details - .get(&output.unique_pubkey) - .ok_or(TransferError::UniquePubkeyNotFound)?; - - Ok(( + Ok(self + .output_details + .values() + .map(|(main_pubkey, derivation_index, amount)| { + ( CashNote { unique_pubkey: main_pubkey.new_unique_pubkey(derivation_index), - parent_tx: self.spent_tx.clone(), parent_spends: self.signed_spends.clone(), main_pubkey: *main_pubkey, derivation_index: *derivation_index, }, - output.amount, - )) + *amount, + ) }) - .collect() + .collect()) } } diff --git a/sn_transfers/src/cashnotes/cashnote.rs b/sn_transfers/src/cashnotes/cashnote.rs index cbf596dcc9..a3dcc38cd9 100644 --- a/sn_transfers/src/cashnotes/cashnote.rs +++ b/sn_transfers/src/cashnotes/cashnote.rs @@ -8,7 +8,7 @@ use super::{ DerivationIndex, DerivedSecretKey, Hash, MainPubkey, MainSecretKey, NanoTokens, SignedSpend, - Transaction, UniquePubkey, + UniquePubkey, }; use crate::{Result, TransferError}; @@ -61,9 +61,6 @@ pub struct CashNote { /// The unique public key of this CashNote. It is unique, and there can never /// be another CashNote with the same public key. It used in SignedSpends. pub unique_pubkey: UniquePubkey, - /// The transaction where this CashNote was created. - #[debug(skip)] - pub parent_tx: Transaction, /// The transaction's input's SignedSpends pub parent_spends: BTreeSet, /// This is the MainPubkey of the owner of this CashNote @@ -112,19 +109,20 @@ impl CashNote { /// Return the value in NanoTokens for this CashNote. pub fn value(&self) -> Result { - Ok(self - .parent_tx - .outputs - .iter() - .find(|o| &self.unique_pubkey() == o.unique_pubkey()) - .ok_or(TransferError::OutputNotFound)? - .amount) + let mut total_amount: u64 = 0; + for p in self.parent_spends.iter() { + if let Some(amount) = p.spend.get_output_amount(&self.unique_pubkey()) { + total_amount += amount.as_nano(); + } else { + return Err(TransferError::OutputNotFound); + } + } + Ok(NanoTokens::from(total_amount)) } /// Generate the hash of this CashNote pub fn hash(&self) -> Hash { let mut sha3 = Sha3::v256(); - sha3.update(self.parent_tx.hash().as_ref()); sha3.update(&self.main_pubkey.to_bytes()); sha3.update(&self.derivation_index.0); @@ -141,25 +139,17 @@ impl CashNote { /// /// A CashNote recipient should call this immediately upon receipt. /// - /// important: this will verify there is a matching transaction provided - /// for each SignedSpend, although this does not check if the CashNote has been spent. + /// important: this does not check if the CashNote has been spent. /// For that, one must query the spentbook nodes. /// /// Note that the spentbook nodes cannot perform this check. Only the CashNote /// recipient (private key holder) can. - /// - /// see TransactionVerifier::verify() for a description of - /// verifier requirements. pub fn verify(&self, main_key: &MainSecretKey) -> Result<(), TransferError> { - self.parent_tx - .verify_against_inputs_spent(self.parent_spends.iter())?; - let unique_pubkey = self.derived_key(main_key)?.unique_pubkey(); if !self - .parent_tx - .outputs + .parent_spends .iter() - .any(|o| unique_pubkey.eq(o.unique_pubkey())) + .all(|p| p.spend.get_output_amount(&unique_pubkey).is_some()) { return Err(TransferError::CashNoteCiphersNotPresentInTransactionOutput); } diff --git a/sn_transfers/src/cashnotes/output_purpose.rs b/sn_transfers/src/cashnotes/output_purpose.rs new file mode 100644 index 0000000000..ebdf4f41ff --- /dev/null +++ b/sn_transfers/src/cashnotes/output_purpose.rs @@ -0,0 +1,204 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use bls::{Ciphertext, PublicKey, SecretKey}; +use serde::{Deserialize, Serialize}; +use xor_name::XorName; + +use crate::{DerivationIndex, Hash, Result, TransferError}; + +const CUSTOM_OUTPUT_PURPOSE_SIZE: usize = 64; + +/// The attached metadata or reason for which a Spend was spent +#[derive(Default, Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum OutputPurpose { + #[default] + None, + /// Reference to network data + NetworkData(XorName), + /// Custom field for any application data + Custom(#[serde(with = "serde_bytes")] [u8; CUSTOM_OUTPUT_PURPOSE_SIZE]), + /// For Network Royalty Fee + RoyaltyFee(DerivationIndex), + + /// Beta only feature to track rewards + /// Discord username encrypted to the Foundation's pubkey with a random nonce + BetaRewardTracking(DiscordNameCipher), +} + +impl OutputPurpose { + pub fn hash(&self) -> Hash { + match self { + Self::None => Hash::default(), + Self::NetworkData(xor_name) => Hash::hash(xor_name), + Self::Custom(bytes) => Hash::hash(bytes), + Self::RoyaltyFee(derivation_index) => Hash::hash(&derivation_index.0), + Self::BetaRewardTracking(cypher) => Hash::hash(&cypher.cipher), + } + } + + pub fn create_reward_tracking_purpose(input_str: &str) -> Result { + let input_pk = crate::PAYMENT_FORWARD_PK.public_key(); + Ok(Self::BetaRewardTracking(DiscordNameCipher::create( + input_str, input_pk, + )?)) + } + + pub fn get_sender_hash(&self, sk: &SecretKey) -> Option { + match self { + Self::BetaRewardTracking(cypher) => { + if let Ok(hash) = cypher.decrypt_to_username_hash(sk) { + Some(hash) + } else { + error!("Failed to decrypt BetaRewardTracking"); + None + } + } + _ => None, + } + } +} + +const MAX_CIPHER_SIZE: usize = u8::MAX as usize; +const DERIVATION_INDEX_SIZE: usize = 32; +const HASH_SIZE: usize = 32; +const CHECK_SUM_SIZE: usize = 8; +const CONTENT_SIZE: usize = HASH_SIZE + DERIVATION_INDEX_SIZE; +const LIMIT_SIZE: usize = CONTENT_SIZE + CHECK_SUM_SIZE; +const CHECK_SUM: [u8; CHECK_SUM_SIZE] = [15; CHECK_SUM_SIZE]; + +/// Discord username encrypted to the Foundation's pubkey with a random nonce +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct DiscordNameCipher { + /// Length of the cipher, hard limited to MAX_U8 + len: u8, + /// Encrypted Discord username + #[serde(with = "serde_bytes")] + cipher: [u8; MAX_CIPHER_SIZE], +} + +/// Discord username hash and nonce +/// u256 hash + u256 nonce might be overkill (very big) +struct DiscordName { + hash: Hash, + nonce: DerivationIndex, + checksum: [u8; CHECK_SUM_SIZE], +} + +impl DiscordName { + fn new(user_name: &str) -> Self { + let rng = &mut rand::thread_rng(); + DiscordName { + hash: Hash::hash(user_name.as_bytes()), + nonce: DerivationIndex::random(rng), + checksum: CHECK_SUM, + } + } + + fn to_sized_bytes(&self) -> [u8; LIMIT_SIZE] { + let mut bytes: [u8; LIMIT_SIZE] = [0; LIMIT_SIZE]; + bytes[0..HASH_SIZE].copy_from_slice(self.hash.slice()); + bytes[HASH_SIZE..CONTENT_SIZE].copy_from_slice(&self.nonce.0); + bytes[CONTENT_SIZE..LIMIT_SIZE].copy_from_slice(&self.checksum); + bytes + } + + fn from_bytes(bytes: &[u8]) -> Result { + let mut hash_bytes = [0; HASH_SIZE]; + hash_bytes.copy_from_slice(&bytes[0..HASH_SIZE]); + let hash = Hash::from(hash_bytes.to_owned()); + let mut nonce_bytes = [0; DERIVATION_INDEX_SIZE]; + nonce_bytes.copy_from_slice(&bytes[HASH_SIZE..CONTENT_SIZE]); + let nonce = DerivationIndex(nonce_bytes.to_owned()); + + let mut checksum = [0; CHECK_SUM_SIZE]; + if bytes.len() < LIMIT_SIZE { + // Backward compatible, which will allow invalid key generate a random hash result + checksum = CHECK_SUM; + } else { + checksum.copy_from_slice(&bytes[CONTENT_SIZE..LIMIT_SIZE]); + if checksum != CHECK_SUM { + return Err(TransferError::InvalidDecryptionKey); + } + } + + Ok(Self { + hash, + nonce, + checksum, + }) + } +} + +impl DiscordNameCipher { + /// Create a new DiscordNameCipher from a Discord username + /// it is encrypted to the given pubkey + pub fn create(user_name: &str, encryption_pk: PublicKey) -> Result { + let discord_name = DiscordName::new(user_name); + let cipher = encryption_pk.encrypt(discord_name.to_sized_bytes()); + let bytes = cipher.to_bytes(); + if bytes.len() > MAX_CIPHER_SIZE { + return Err(TransferError::DiscordNameCipherTooBig); + } + let mut sized = [0; MAX_CIPHER_SIZE]; + sized[0..bytes.len()].copy_from_slice(&bytes); + Ok(Self { + len: bytes.len() as u8, + cipher: sized, + }) + } + + /// Recover a Discord username hash using the secret key it was encrypted to + pub fn decrypt_to_username_hash(&self, sk: &SecretKey) -> Result { + let cipher = Ciphertext::from_bytes(&self.cipher[0..self.len as usize])?; + let decrypted = sk + .decrypt(&cipher) + .ok_or(TransferError::UserNameDecryptFailed)?; + let discord_name = DiscordName::from_bytes(&decrypted)?; + Ok(discord_name.hash) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_discord_name_cyphering() { + let encryption_sk = SecretKey::random(); + let encryption_pk = encryption_sk.public_key(); + + let user_name = "JohnDoe#1234"; + let user_name_hash = Hash::hash(user_name.as_bytes()); + let cypher = + DiscordNameCipher::create(user_name, encryption_pk).expect("cypher creation failed"); + let recovered_hash = cypher + .decrypt_to_username_hash(&encryption_sk) + .expect("decryption failed"); + assert_eq!(user_name_hash, recovered_hash); + + let user_name2 = "JackMa#5678"; + let user_name_hash2 = Hash::hash(user_name2.as_bytes()); + let cypher = + DiscordNameCipher::create(user_name2, encryption_pk).expect("cypher creation failed"); + let recovered_hash = cypher + .decrypt_to_username_hash(&encryption_sk) + .expect("decryption failed"); + assert_eq!(user_name_hash2, recovered_hash); + + assert_ne!(user_name_hash, user_name_hash2); + + let encryption_wrong_pk = SecretKey::random().public_key(); + let cypher_wrong = DiscordNameCipher::create(user_name, encryption_wrong_pk) + .expect("cypher creation failed"); + assert_eq!( + Err(TransferError::InvalidDecryptionKey), + cypher_wrong.decrypt_to_username_hash(&encryption_sk) + ); + } +} diff --git a/sn_transfers/src/cashnotes/signed_spend.rs b/sn_transfers/src/cashnotes/signed_spend.rs index 6a43e297e3..c6b0a664a5 100644 --- a/sn_transfers/src/cashnotes/signed_spend.rs +++ b/sn_transfers/src/cashnotes/signed_spend.rs @@ -6,14 +6,17 @@ // KIND, either express or implied. Please review the Licences for the specific language governing // permissions and limitations relating to use of the SAFE Network Software. +use super::output_purpose::OutputPurpose; use super::spend_reason::SpendReason; -use super::{Hash, NanoTokens, Transaction, UniquePubkey}; -use crate::{DerivationIndex, Result, Signature, SpendAddress, TransferError}; +use super::{Hash, NanoTokens, UniquePubkey}; +use crate::{Result, Signature, SpendAddress, TransferError}; use custom_debug::Debug; use serde::{Deserialize, Serialize}; -use std::cmp::Ordering; -use std::collections::BTreeSet; +use std::{ + cmp::Ordering, + collections::{BTreeMap, BTreeSet}, +}; /// SignedSpend's are constructed when a CashNote is logged to the spentbook. #[derive(Debug, Clone, PartialOrd, Ord, Serialize, Deserialize)] @@ -36,24 +39,9 @@ impl SignedSpend { SpendAddress::from_unique_pubkey(&self.spend.unique_pubkey) } - /// Get the hash of the transaction this CashNote is spent in - pub fn spent_tx_hash(&self) -> Hash { - self.spend.spent_tx.hash() - } - - /// Get the transaction this CashNote is spent in - pub fn spent_tx(&self) -> Transaction { - self.spend.spent_tx.clone() - } - - /// Get the hash of the transaction this CashNote was created in - pub fn parent_tx_hash(&self) -> Hash { - self.spend.parent_tx.hash() - } - /// Get Nano - pub fn token(&self) -> &NanoTokens { - &self.spend.amount + pub fn token(&self) -> NanoTokens { + self.spend.amount() } /// Get reason. @@ -72,7 +60,6 @@ impl SignedSpend { /// Verify a SignedSpend /// /// Checks that - /// - the spend was indeed spent for the given Tx /// - it was signed by the DerivedSecretKey that owns the CashNote for this Spend /// - the signature is valid /// - its value didn't change between the two transactions it is involved in (creation and spending) @@ -80,62 +67,7 @@ impl SignedSpend { /// It does NOT check: /// - if the spend exists on the Network /// - the spend's parents and if they exist on the Network - pub fn verify(&self, spent_tx_hash: Hash) -> Result<()> { - // verify that input spent_tx_hash matches self.spent_tx_hash - if spent_tx_hash != self.spent_tx_hash() { - return Err(TransferError::TransactionHashMismatch( - spent_tx_hash, - self.spent_tx_hash(), - )); - } - - // check that the spend is an output of its parent tx - let parent_tx = &self.spend.parent_tx; - let unique_key = self.unique_pubkey(); - if !parent_tx - .outputs - .iter() - .any(|o| o.unique_pubkey() == unique_key) - { - return Err(TransferError::InvalidParentTx(format!( - "spend {unique_key} is not an output of the its parent tx: {parent_tx:?}" - ))); - } - - // check that the spend is an input of its spent tx - let spent_tx = &self.spend.spent_tx; - if !spent_tx - .inputs - .iter() - .any(|i| i.unique_pubkey() == unique_key) - { - return Err(TransferError::InvalidSpentTx(format!( - "spend {unique_key} is not an input of the its spent tx: {spent_tx:?}" - ))); - } - - // check that the value of the spend wasn't tampered with - let claimed_value = self.spend.amount; - let creation_value = self - .spend - .parent_tx - .outputs - .iter() - .find(|o| o.unique_pubkey == self.spend.unique_pubkey) - .map(|o| o.amount) - .unwrap_or(NanoTokens::zero()); - let spent_value = self - .spend - .spent_tx - .inputs - .iter() - .find(|i| i.unique_pubkey == self.spend.unique_pubkey) - .map(|i| i.amount) - .unwrap_or(NanoTokens::zero()); - if claimed_value != creation_value || creation_value != spent_value { - return Err(TransferError::InvalidSpendValue(*self.unique_pubkey())); - } - + pub fn verify(&self) -> Result<()> { // check signature // the spend is signed by the DerivedSecretKey // corresponding to the UniquePubkey of the CashNote being spent. @@ -152,8 +84,8 @@ impl SignedSpend { /// Verify the parents of this Spend, making sure the input parent_spends are ancestors of self. /// - Also handles the case of parent double spends. - /// - verifies that the parent_spends where spent in our spend's parent_tx. - /// - verifies the parent_tx against the parent_spends + /// - verifies that the parent_spends contains self as an output + /// - verifies the sum of total inputs equals to the sum of outputs pub fn verify_parent_spends<'a, T>(&self, parent_spends: T) -> Result<()> where T: IntoIterator> + Clone, @@ -161,17 +93,9 @@ impl SignedSpend { let unique_key = self.unique_pubkey(); trace!("Verifying parent_spends for {unique_key}"); - // Check that the parent where all spent to our parent_tx - let tx_our_cash_note_was_created_in = self.parent_tx_hash(); - let mut actual_parent_spends = BTreeSet::new(); - for parents in parent_spends.clone().into_iter() { - if parents.is_empty() { - error!("No parent spend provided for {unique_key}"); - return Err(TransferError::InvalidParentSpend( - "Parent is empty".to_string(), - )); - } - let parent_unique_key = parents + let mut total_inputs: u64 = 0; + for p in parent_spends { + let parent_unique_key = p .iter() .map(|p| *p.unique_pubkey()) .collect::>(); @@ -180,36 +104,19 @@ impl SignedSpend { return Err(TransferError::InvalidParentSpend("Invalid parent double spend. More than one unique_pubkey in the parent double spend.".to_string())); } - // if parent is a double spend, get the actual parent among the parent double spends - let actual_parent = parents - .iter() - .find(|p| p.spent_tx_hash() == tx_our_cash_note_was_created_in) - .cloned(); - - match actual_parent { - Some(actual_parent) => { - actual_parent_spends.insert(actual_parent); - } - None => { - let tx_parent_was_spent_in = parents - .iter() - .map(|p| p.spent_tx_hash()) - .collect::>(); - return Err(TransferError::InvalidParentSpend(format!( - "Parent spend was spent in another transaction. Expected: {tx_our_cash_note_was_created_in:?} Got: {tx_parent_was_spent_in:?}" - ))); - } + if let Some(amount) = p.spend.get_output_amount(unique_key) { + total_inputs += amount.as_nano(); + } else { + return Err(TransferError::InvalidParentSpend(format!( + "Parent spend {:?} doesn't contain self spend {unique_key:?} as one of its output", p.unique_pubkey() + ))); } } - // Here we check that the CashNote we're trying to spend was created in a valid tx - if let Err(e) = self - .spend - .parent_tx - .verify_against_inputs_spent(actual_parent_spends.iter()) - { + let total_outputs = self.token().as_nano(); + if total_outputs != total_inputs { return Err(TransferError::InvalidParentSpend(format!( - "Parent Tx verification failed: {e:?}" + "Parents total_inputs {total_inputs:?} doesn't match total_outputs {total_outputs:?}" ))); } @@ -235,25 +142,19 @@ impl std::hash::Hash for SignedSpend { } /// Represents the data to be signed by the DerivedSecretKey of the CashNote being spent. +/// The claimed `spend.unique_pubkey` must appears in the `ancestor` spends, and the total sum of amount +/// must be equal to the total sum of amount of all outputs (descendants) #[derive(custom_debug::Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Spend { /// UniquePubkey of input CashNote that this SignedSpend is proving to be spent. pub unique_pubkey: UniquePubkey, - /// The transaction that the input CashNote is being spent in (where it is an input) - #[debug(skip)] - pub spent_tx: Transaction, /// Reason why this CashNote was spent. #[debug(skip)] pub reason: SpendReason, - /// The amount of the input CashNote. - #[debug(skip)] - pub amount: NanoTokens, - /// The transaction that the input CashNote was created in (where it is an output) - #[debug(skip)] - pub parent_tx: Transaction, - /// Data to claim the Network Royalties (if any) from the Spend's descendants (outputs in spent_tx) - #[debug(skip)] - pub network_royalties: Vec, + /// Inputs (parent spends) of this spend + pub ancestors: BTreeSet, + /// Outputs of this spend + pub descendants: BTreeMap, } impl Spend { @@ -262,10 +163,15 @@ impl Spend { pub fn to_bytes_for_signing(&self) -> Vec { let mut bytes: Vec = Default::default(); bytes.extend(self.unique_pubkey.to_bytes()); - bytes.extend(self.spent_tx.hash().as_ref()); bytes.extend(self.reason.hash().as_ref()); - bytes.extend(self.amount.to_bytes()); - bytes.extend(self.parent_tx.hash().as_ref()); + for ancestor in self.ancestors.iter() { + bytes.extend(&ancestor.to_bytes()); + } + for (descendant, (amount, purpose)) in self.descendants.iter() { + bytes.extend(&descendant.to_bytes()); + bytes.extend(amount.to_bytes()); + bytes.extend(purpose.hash().as_ref()); + } bytes } @@ -273,6 +179,26 @@ impl Spend { pub fn hash(&self) -> Hash { Hash::hash(&self.to_bytes_for_signing()) } + + /// Returns the amount to be spent in this Spend + pub fn amount(&self) -> NanoTokens { + let amount: u64 = self + .descendants + .values() + .map(|(amount, _)| amount.as_nano()) + .sum(); + NanoTokens::from(amount) + } + + /// Returns the amount of a particual output target. + /// None if the target is not one of the outputs + pub fn get_output_amount(&self, target: &UniquePubkey) -> Option { + if let Some((amount, _)) = self.descendants.get(target) { + Some(*amount) + } else { + None + } + } } impl PartialOrd for Spend { diff --git a/sn_transfers/src/cashnotes/transaction.rs b/sn_transfers/src/cashnotes/transaction.rs index 7fa73c1a6f..a64977321e 100644 --- a/sn_transfers/src/cashnotes/transaction.rs +++ b/sn_transfers/src/cashnotes/transaction.rs @@ -205,10 +205,9 @@ impl Transaction { return Err(TransferError::UniquePubkeyNotUniqueInTx); } - // Verify that each signed spend is valid and was spent in this transaction - let spent_tx_hash = self.hash(); + // Verify that each signed spend is valid. for s in signed_spends { - s.verify(spent_tx_hash)?; + s.verify()?; } // Verify that the transaction is balanced diff --git a/sn_transfers/src/genesis.rs b/sn_transfers/src/genesis.rs index 8f87391df1..94000af753 100644 --- a/sn_transfers/src/genesis.rs +++ b/sn_transfers/src/genesis.rs @@ -10,8 +10,8 @@ use super::wallet::HotWallet; use crate::{ wallet::Result as WalletResult, CashNote, DerivationIndex, Input, MainPubkey, MainSecretKey, - NanoTokens, Output, SignedSpend, SpendReason, Transaction, TransactionBuilder, - TransferError as CashNoteError, UniquePubkey, + NanoTokens, SignedSpend, SpendReason, TransactionBuilder, TransferError as CashNoteError, + UniquePubkey, }; use bls::SecretKey; @@ -92,21 +92,6 @@ lazy_static! { pub static ref GENESIS_SPEND_UNIQUE_KEY: UniquePubkey = GENESIS_PK.new_unique_pubkey(&GENESIS_DERIVATION_INDEX); } -lazy_static! { - pub static ref GENESIS_CASHNOTE_PARENT_TX: Transaction = { - let mut tx = Transaction::empty(); - tx.inputs = vec![Input { - unique_pubkey: *GENESIS_SPEND_UNIQUE_KEY, - amount: NanoTokens::from(GENESIS_CASHNOTE_AMOUNT), - }]; - tx.outputs = vec![Output { - unique_pubkey: *GENESIS_SPEND_UNIQUE_KEY, - amount: NanoTokens::from(GENESIS_CASHNOTE_AMOUNT), - }]; - tx - }; -} - lazy_static! { pub static ref GENESIS_SK_STR: String = { let compile_time_key = option_env!("GENESIS_SK").unwrap_or(DEFAULT_LIVE_GENESIS_SK); @@ -145,18 +130,12 @@ pub fn get_genesis_sk() -> MainSecretKey { } } -/// Return if provided Transaction is genesis parent tx. -pub fn is_genesis_parent_tx(parent_tx: &Transaction) -> bool { - parent_tx == &*GENESIS_CASHNOTE_PARENT_TX -} - /// Return if provided Spend is genesis spend. pub fn is_genesis_spend(spend: &SignedSpend) -> bool { let bytes = spend.spend.to_bytes_for_signing(); spend.spend.unique_pubkey == *GENESIS_SPEND_UNIQUE_KEY && GENESIS_SPEND_UNIQUE_KEY.verify(&spend.derived_key_sig, bytes) - && is_genesis_parent_tx(&spend.spend.parent_tx) - && spend.spend.amount == NanoTokens::from(GENESIS_CASHNOTE_AMOUNT) + && spend.spend.amount() == NanoTokens::from(GENESIS_CASHNOTE_AMOUNT) } pub fn load_genesis_wallet() -> Result { @@ -214,7 +193,6 @@ pub fn create_first_cash_note_from_key( let derived_key = first_cash_note_key.derive_key(&GENESIS_DERIVATION_INDEX); // Use the same key as the input and output of Genesis Tx. - // The src tx is empty as this is the first CashNote. let genesis_input = Input { unique_pubkey: derived_key.unique_pubkey(), amount: NanoTokens::from(GENESIS_CASHNOTE_AMOUNT), @@ -225,16 +203,16 @@ pub fn create_first_cash_note_from_key( let cash_note_builder = TransactionBuilder::default() .add_input( genesis_input, - Some(derived_key), - Transaction::empty(), + Some(derived_key.clone()), GENESIS_DERIVATION_INDEX, + derived_key.unique_pubkey(), ) .add_output( NanoTokens::from(GENESIS_CASHNOTE_AMOUNT), main_pubkey, GENESIS_DERIVATION_INDEX, ) - .build(reason, vec![]); + .build(reason); // build the output CashNotes let output_cash_notes = cash_note_builder.build_without_verifying().map_err(|err| { @@ -299,7 +277,6 @@ mod tests { "genesis_cn.unique_pubkey: {:?}", genesis_cn.unique_pubkey().to_hex() ); - println!("genesis_cn.parent_tx: {:?}", genesis_cn.parent_tx.to_hex()); } } } diff --git a/sn_transfers/src/lib.rs b/sn_transfers/src/lib.rs index ab96cc12dd..bd74e9ca53 100644 --- a/sn_transfers/src/lib.rs +++ b/sn_transfers/src/lib.rs @@ -15,20 +15,20 @@ mod genesis; mod transfers; mod wallet; -pub(crate) use cashnotes::{Input, Output, TransactionBuilder}; +pub(crate) use cashnotes::{Input, TransactionBuilder}; /// Types used in the public API pub use cashnotes::{ CashNote, DerivationIndex, DerivedSecretKey, Hash, MainPubkey, MainSecretKey, NanoTokens, - SignedSpend, Spend, SpendAddress, SpendReason, Transaction, UniquePubkey, UnsignedTransfer, + OutputPurpose, SignedSpend, Spend, SpendAddress, SpendReason, Transaction, UniquePubkey, + UnsignedTransfer, }; pub use error::{Result, TransferError}; /// Utilities exposed pub use genesis::{ calculate_royalties_fee, create_first_cash_note_from_key, get_faucet_data_dir, get_genesis_sk, - is_genesis_parent_tx, is_genesis_spend, load_genesis_wallet, Error as GenesisError, - GENESIS_CASHNOTE, GENESIS_CASHNOTE_PARENT_TX, GENESIS_PK, GENESIS_SPEND_UNIQUE_KEY, - TOTAL_SUPPLY, + is_genesis_spend, load_genesis_wallet, Error as GenesisError, GENESIS_CASHNOTE, GENESIS_PK, + GENESIS_SPEND_UNIQUE_KEY, TOTAL_SUPPLY, }; pub use transfers::{CashNoteRedemption, OfflineTransfer, Transfer}; pub use wallet::{ diff --git a/sn_transfers/src/transfers/offline_transfer.rs b/sn_transfers/src/transfers/offline_transfer.rs index 9b6725645f..bb15207737 100644 --- a/sn_transfers/src/transfers/offline_transfer.rs +++ b/sn_transfers/src/transfers/offline_transfer.rs @@ -9,8 +9,7 @@ use crate::{ cashnotes::{CashNoteBuilder, UnsignedTransfer}, rng, CashNote, DerivationIndex, DerivedSecretKey, Input, MainPubkey, NanoTokens, Result, - SignedSpend, SpendReason, Transaction, TransactionBuilder, TransferError, UniquePubkey, - NETWORK_ROYALTIES_PK, + SignedSpend, SpendReason, TransactionBuilder, TransferError, UniquePubkey, }; use serde::{Deserialize, Serialize}; @@ -25,9 +24,6 @@ pub type CashNotesAndSecretKey = Vec<(CashNote, Option)>; /// of tokens from one or more cash_notes, into one or more new cash_notes. #[derive(custom_debug::Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct OfflineTransfer { - /// This is the transaction where all the below - /// spends were made and cash_notes created. - pub tx: Transaction, /// The cash_notes that were created containing /// the tokens sent to respective recipient. #[debug(skip)] @@ -43,12 +39,10 @@ pub struct OfflineTransfer { impl OfflineTransfer { pub fn from_transaction( signed_spends: BTreeSet, - tx: Transaction, change_id: UniquePubkey, - output_details: BTreeMap, + output_details: BTreeMap, ) -> Result { - let cash_note_builder = - CashNoteBuilder::new(tx.clone(), output_details, signed_spends.clone()); + let cash_note_builder = CashNoteBuilder::new(output_details, signed_spends.clone()); // Perform validations of input tx and signed spends, // as well as building the output CashNotes. @@ -69,7 +63,6 @@ impl OfflineTransfer { }); Ok(Self { - tx, cash_notes_for_recipient: created_cash_notes, change_cash_note, all_spend_requests: signed_spends.into_iter().collect(), @@ -154,18 +147,10 @@ pub fn create_unsigned_transfer( change: (change_amount, change_to), }; - // gather the network_royalties derivation indexes - let network_royalties: Vec = selected_inputs - .recipients - .iter() - .filter(|(_, main_pubkey, _)| *main_pubkey == *NETWORK_ROYALTIES_PK) - .map(|(_, _, derivation_index)| *derivation_index) - .collect(); - - let (tx_builder, _src_txs, change_id) = create_transaction_builder_with(selected_inputs)?; + let (tx_builder, change_id) = create_transaction_builder_with(selected_inputs)?; // Get the unsigned Spends. - tx_builder.build_unsigned_transfer(reason_hash, network_royalties, change_id) + tx_builder.build_unsigned_transfer(reason_hash, change_id) } /// Select the necessary number of cash_notes from those that we were passed. @@ -231,18 +216,13 @@ fn select_inputs( fn create_transaction_builder_with( selected_inputs: TransferInputs, -) -> Result<( - TransactionBuilder, - BTreeMap, - crate::UniquePubkey, -)> { +) -> Result<(TransactionBuilder, crate::UniquePubkey)> { let TransferInputs { change: (change, change_to), .. } = selected_inputs; let mut inputs = vec![]; - let mut src_txs = BTreeMap::new(); for (cash_note, derived_key) in selected_inputs.cash_notes_to_spend { let token = match cash_note.value() { Ok(token) => token, @@ -258,10 +238,9 @@ fn create_transaction_builder_with( inputs.push(( input, derived_key, - cash_note.parent_tx.clone(), cash_note.derivation_index, + cash_note.unique_pubkey(), )); - let _ = src_txs.insert(cash_note.unique_pubkey(), cash_note.parent_tx); } // Build the transaction and create change cash_note if needed @@ -275,7 +254,7 @@ fn create_transaction_builder_with( tx_builder = tx_builder.add_output(change, change_to, derivation_index); } - Ok((tx_builder, src_txs, change_id)) + Ok((tx_builder, change_id)) } /// The tokens of the input cash_notes will be transfered to the @@ -288,20 +267,10 @@ fn create_offline_transfer_with( selected_inputs: TransferInputs, input_reason: SpendReason, ) -> Result { - // gather the network_royalties derivation indexes - let network_royalties: Vec = selected_inputs - .recipients - .iter() - .filter(|(_, main_pubkey, _)| *main_pubkey == *NETWORK_ROYALTIES_PK) - .map(|(_, _, derivation_index)| *derivation_index) - .collect(); - - let (tx_builder, src_txs, change_id) = create_transaction_builder_with(selected_inputs)?; + let (tx_builder, change_id) = create_transaction_builder_with(selected_inputs)?; // Finalize the tx builder to get the cash_note builder. - let cash_note_builder = tx_builder.build(input_reason, network_royalties); - - let tx = cash_note_builder.spent_tx.clone(); + let cash_note_builder = tx_builder.build(input_reason); let signed_spends: BTreeMap<_, _> = cash_note_builder .signed_spends() @@ -309,17 +278,6 @@ fn create_offline_transfer_with( .map(|spend| (spend.unique_pubkey(), spend)) .collect(); - // We must have a source transaction for each signed spend (i.e. the tx where the cash_note was created). - // These are required to upload the spends to the network. - if !signed_spends - .iter() - .all(|(unique_pubkey, _)| src_txs.contains_key(*unique_pubkey)) - { - return Err(TransferError::CashNoteReissueFailed( - "Not all signed spends could be matched to a source cash_note transaction.".to_string(), - )); - } - let mut all_spend_requests = vec![]; for (_, signed_spend) in signed_spends.into_iter() { all_spend_requests.push(signed_spend.to_owned()); @@ -344,7 +302,6 @@ fn create_offline_transfer_with( }); Ok(OfflineTransfer { - tx, cash_notes_for_recipient: created_cash_notes, change_cash_note, all_spend_requests, diff --git a/sn_transfers/src/wallet/hot_wallet.rs b/sn_transfers/src/wallet/hot_wallet.rs index e3134c0465..75609a30c6 100644 --- a/sn_transfers/src/wallet/hot_wallet.rs +++ b/sn_transfers/src/wallet/hot_wallet.rs @@ -23,8 +23,8 @@ use crate::{ cashnotes::UnsignedTransfer, transfers::{CashNotesAndSecretKey, OfflineTransfer}, CashNote, CashNoteRedemption, DerivationIndex, DerivedSecretKey, MainPubkey, MainSecretKey, - NanoTokens, SignedSpend, Spend, SpendAddress, SpendReason, Transaction, Transfer, UniquePubkey, - WalletError, NETWORK_ROYALTIES_PK, + NanoTokens, SignedSpend, Spend, SpendAddress, SpendReason, Transfer, UniquePubkey, WalletError, + NETWORK_ROYALTIES_PK, }; use std::{ collections::{BTreeMap, BTreeSet, HashSet}, @@ -357,12 +357,10 @@ impl HotWallet { pub fn prepare_signed_transfer( &mut self, signed_spends: BTreeSet, - tx: Transaction, change_id: UniquePubkey, - output_details: BTreeMap, + output_details: BTreeMap, ) -> Result> { - let transfer = - OfflineTransfer::from_transaction(signed_spends, tx, change_id, output_details)?; + let transfer = OfflineTransfer::from_transaction(signed_spends, change_id, output_details)?; let created_cash_notes = transfer.cash_notes_for_recipient.clone(); @@ -590,10 +588,9 @@ impl HotWallet { ) -> Result<()> { // First of all, update client local state. let spent_unique_pubkeys: BTreeSet<_> = transfer - .tx - .inputs + .all_spend_requests .iter() - .map(|input| input.unique_pubkey()) + .map(|s| s.unique_pubkey()) .collect(); self.watchonly_wallet diff --git a/sn_transfers/src/wallet/watch_only.rs b/sn_transfers/src/wallet/watch_only.rs index 9eea240034..2ee2b25c6c 100644 --- a/sn_transfers/src/wallet/watch_only.rs +++ b/sn_transfers/src/wallet/watch_only.rs @@ -74,12 +74,7 @@ impl WatchOnlyWallet { let cash_notes = load_cash_notes_from_disk(&self.wallet_dir)?; let spent_unique_pubkeys: BTreeSet<_> = cash_notes .iter() - .flat_map(|cn| { - cn.parent_tx - .inputs - .iter() - .map(|input| input.unique_pubkey()) - }) + .flat_map(|cn| cn.parent_spends.iter().map(|s| s.unique_pubkey())) .collect(); self.deposit(&cash_notes)?; self.mark_notes_as_spent(spent_unique_pubkeys);