diff --git a/sn_cli/src/bin/subcommands/wallet/wo_wallet.rs b/sn_cli/src/bin/subcommands/wallet/wo_wallet.rs index 7a955956d1..73396b9949 100644 --- a/sn_cli/src/bin/subcommands/wallet/wo_wallet.rs +++ b/sn_cli/src/bin/subcommands/wallet/wo_wallet.rs @@ -304,7 +304,7 @@ async fn broadcast_signed_spends( let transfer = OfflineTransfer::from_transaction(signed_spends, tx, 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.created_cash_notes[..] { + let cash_note = match &transfer.cash_notes_for_recipient[..] { [cashnote] => cashnote.to_hex()?, [_multiple, ..] => bail!("Multiple CashNotes were returned from the transaction when only one was expected. This is a BUG."), [] =>bail!("No CashNotes were built from the Tx.") diff --git a/sn_client/src/audit.rs b/sn_client/src/audit.rs index 6eac78efa5..aa1482db40 100644 --- a/sn_client/src/audit.rs +++ b/sn_client/src/audit.rs @@ -7,261 +7,12 @@ // permissions and limitations relating to use of the SAFE Network Software. mod dag_error; +mod spend_check; mod spend_dag; mod spend_dag_building; +#[cfg(test)] +mod tests; + pub use dag_error::{DagError, SpendFault}; pub use spend_dag::{SpendDag, SpendDagGet}; - -use super::{ - error::{Error, Result}, - Client, -}; - -use futures::future::join_all; -use sn_networking::{target_arch::Instant, GetRecordError, NetworkError}; -use sn_transfers::{Hash, SignedSpend, SpendAddress, WalletError, WalletResult}; -use std::{collections::BTreeSet, iter::Iterator}; - -impl Client { - /// Verify that a spend is valid on the network. - /// Optionally verify its ancestors as well, all the way to genesis (might take a LONG time) - /// - /// Prints progress on stdout. - /// - /// When verifying all the way back to genesis, it only verifies Spends that are ancestors of the given Spend, - /// ignoring all other branches. - /// - /// This is how the DAG it follows could look like: - /// ```text - /// ... -- - /// \ - /// ... ---- ... -- - /// \ \ - /// Spend0 -> Spend1 -----> Spend2 ---> Spend5 ---> Spend2 ---> Genesis - /// \ / - /// ---> Spend3 ---> Spend6 -> - /// \ / - /// -> Spend4 -> - /// / - /// ... - /// - /// depth0 depth1 depth2 depth3 depth4 depth5 - /// ``` - /// - /// This function will return an error if any spend in the way is invalid. - pub async fn verify_spend_at(&self, addr: SpendAddress, to_genesis: bool) -> WalletResult<()> { - let first_spend = self - .get_spend_from_network(addr) - .await - .map_err(|err| WalletError::CouldNotVerifyTransfer(err.to_string()))?; - - if !to_genesis { - return Ok(()); - } - - // use iteration instead of recursion to avoid stack overflow - let mut txs_to_verify = BTreeSet::from_iter([first_spend.spend.parent_tx]); - let mut depth = 0; - let mut verified_tx = BTreeSet::new(); - let start = Instant::now(); - - while !txs_to_verify.is_empty() { - let mut next_gen_tx = 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} - Verifying parent Tx : {parent_tx_hash:?} with inputs: {addrs_to_verify:?}"); - - // get all parent spends in parallel - let tasks: Vec<_> = addrs_to_verify - .clone() - .map(|a| self.get_spend_from_network(a)) - .collect(); - let spends = join_all(tasks).await - .into_iter() - .zip(addrs_to_verify.into_iter()) - .map(|(maybe_spend, a)| - maybe_spend.map_err(|err| WalletError::CouldNotVerifyTransfer(format!("at depth {depth} - Failed to get spend {a:?} from network for parent Tx {parent_tx_hash:?}: {err}")))) - .collect::>>()?; - debug!( - "Depth {depth} - Got {:?} spends for parent Tx: {parent_tx_hash:?}", - spends.len() - ); - trace!("Spends for {parent_tx_hash:?} - {spends:?}"); - - // check if we reached the genesis Tx - if parent_tx == sn_transfers::GENESIS_CASHNOTE.src_tx - && spends - .iter() - .all(|s| s.spend.unique_pubkey == sn_transfers::GENESIS_CASHNOTE.id) - && spends.len() == 1 - { - debug!("Depth {depth} - Reached genesis Tx on one branch: {parent_tx_hash:?}"); - verified_tx.insert(parent_tx_hash); - continue; - } - - // verify tx with those spends - parent_tx - .verify_against_inputs_spent(&spends) - .map_err(|err| WalletError::CouldNotVerifyTransfer(format!("at depth {depth} - Failed to verify parent Tx {parent_tx_hash:?}: {err}")))?; - verified_tx.insert(parent_tx_hash); - debug!("Depth {depth} - Verified parent Tx: {parent_tx_hash:?}"); - - // add new parent spends to next gen - next_gen_tx.extend(spends.into_iter().map(|s| s.spend.parent_tx)); - } - - // only verify parents we haven't already verified - txs_to_verify = next_gen_tx - .into_iter() - .filter(|tx| !verified_tx.contains(&tx.hash())) - .collect(); - - depth += 1; - let elapsed = start.elapsed(); - let n = verified_tx.len(); - println!("Now at depth {depth} - Verified {n} transactions in {elapsed:?}"); - } - - let elapsed = start.elapsed(); - let n = verified_tx.len(); - println!("Verified all the way to genesis! Through {depth} generations, verifying {n} transactions in {elapsed:?}"); - Ok(()) - } - - /// This function does the opposite of verify_spend. - /// It recursively follows the descendants of a Spend, all the way to unspent Transaction Outputs (UTXOs). - /// - /// Prints progress on stdout - /// - /// Starting from Genesis, this amounts to Auditing the entire currency. - /// This is how the DAG it follows could look like: - /// - /// ```text - /// -> Spend7 ---> UTXO_11 - /// / - /// Genesis -> Spend1 -----> Spend2 ---> Spend5 ---> UTXO_10 - /// \ - /// ---> Spend3 ---> Spend6 ---> UTXO_9 - /// \ - /// -> Spend4 ---> UTXO_8 - /// - /// gen0 gen1 gen2 gen3 - /// - /// ``` - /// - /// This function will return the UTXOs (Spend addresses not spent yet) - /// Future calls to this function could start from those UTXOs to avoid - /// re-checking all previously checked branches. - pub async fn follow_spend( - &self, - spend_addr: SpendAddress, - ) -> WalletResult> { - let first_spend = self - .get_spend_from_network(spend_addr) - .await - .map_err(|err| WalletError::CouldNotVerifyTransfer(err.to_string()))?; - println!("Generation 0 - Found first spend: {spend_addr:#?}"); - - // use iteration instead of recursion to avoid stack overflow - let mut txs_to_follow = BTreeSet::from_iter([first_spend.spend.spent_tx]); - let mut all_utxos = BTreeSet::new(); - let mut verified_tx = BTreeSet::new(); - let mut gen = 0; - let start = Instant::now(); - - while !txs_to_follow.is_empty() { - let mut next_gen_tx = BTreeSet::new(); - let mut next_gen_spends = BTreeSet::new(); - let mut next_gen_utxos = BTreeSet::new(); - - 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)); - debug!("Gen {gen} - Following descendant Tx : {descendant_tx_hash:?} with outputs: {addrs_to_follow:?}"); - - // get all descendant spends in parallel - let tasks: Vec<_> = addrs_to_follow - .clone() - .map(|a| self.get_spend_from_network(a)) - .collect(); - let spends_res = join_all(tasks) - .await - .into_iter() - .zip(addrs_to_follow) - .collect::>(); - - // split spends into utxos and spends - let (utxos, spends) = split_utxos_and_spends(spends_res, gen, descendant_tx_hash)?; - debug!("Gen {gen} - Got {:?} spends and {:?} utxos for descendant Tx: {descendant_tx_hash:?}", spends.len(), utxos.len()); - trace!("Spends for {descendant_tx_hash:?} - {spends:?}"); - next_gen_utxos.extend(utxos); - next_gen_spends.extend( - spends - .iter() - .map(|s| SpendAddress::from_unique_pubkey(&s.spend.unique_pubkey)), - ); - - // add new descendant spends to next gen - next_gen_tx.extend(spends.into_iter().map(|s| s.spend.spent_tx)); - } - - // print stats - gen += 1; - let elapsed = start.elapsed(); - let u = next_gen_utxos.len(); - let s = next_gen_spends.len(); - println!("Generation {gen} - Found {u} UTXOs and {s} Spends in {elapsed:?}"); - debug!("Generation {gen} - UTXOs: {:#?}", next_gen_utxos); - debug!("Generation {gen} - Spends: {:#?}", next_gen_spends); - all_utxos.extend(next_gen_utxos); - - // only verify tx we haven't already verified - verified_tx.extend(txs_to_follow.iter().map(|tx| tx.hash())); - txs_to_follow = next_gen_tx - .into_iter() - .filter(|tx| !verified_tx.contains(&tx.hash())) - .collect(); - } - - let elapsed = start.elapsed(); - let n = all_utxos.len(); - let tx = verified_tx.len(); - println!("Finished auditing! Through {gen} generations, found {n} UTXOs and verified {tx} Transactions in {elapsed:?}"); - Ok(all_utxos) - } -} - -fn split_utxos_and_spends( - spends_res: Vec<(Result, SpendAddress)>, - gen: usize, - descendant_tx_hash: Hash, -) -> WalletResult<(Vec, Vec)> { - let mut utxos = Vec::new(); - let mut spends = Vec::new(); - - for (res, addr) in spends_res { - match res { - Ok(spend) => { - spends.push(spend); - } - Err(Error::Network(NetworkError::GetRecordError(GetRecordError::RecordNotFound))) => { - utxos.push(addr); - } - Err(err) => { - warn!("Error while following spend {addr:?}: {err}"); - return Err(WalletError::CouldNotVerifyTransfer(format!("at gen {gen} - Failed to get spend {addr:?} from network for descendant Tx {descendant_tx_hash:?}: {err}"))); - } - } - } - - Ok((utxos, spends)) -} diff --git a/sn_client/src/audit/dag_error.rs b/sn_client/src/audit/dag_error.rs index d553ed22ee..6fb79953fd 100644 --- a/sn_client/src/audit/dag_error.rs +++ b/sn_client/src/audit/dag_error.rs @@ -28,23 +28,20 @@ pub enum DagError { pub enum SpendFault { #[error("Double Spend at {0:?}")] DoubleSpend(SpendAddress), - #[error("Spend at {addr:?} has missing ancestors at {invalid_ancestor:?}")] + #[error("Spend at {addr:?} has a missing ancestor at {ancestor:?}, until this ancestor is added to the DAG, it cannot be verified")] MissingAncestry { addr: SpendAddress, - invalid_ancestor: SpendAddress, + ancestor: SpendAddress, }, - #[error("Spend at {addr:?} has invalid ancestors at {invalid_ancestor:?}")] - InvalidAncestry { + #[error( + "Spend at {addr:?} has a double spent ancestor at {ancestor:?}, making it unspendable" + )] + DoubleSpentAncestor { addr: SpendAddress, - invalid_ancestor: SpendAddress, + ancestor: SpendAddress, }, #[error("Invalid transaction for spend at {0:?}: {1}")] InvalidTransaction(SpendAddress, String), - #[error("Spend at {addr:?} has an unknown ancestor at {ancestor_addr:?}, until this ancestor is added to the DAG, it cannot be verified")] - UnknownAncestor { - addr: SpendAddress, - ancestor_addr: SpendAddress, - }, #[error("Poisoned ancestry for spend at {0:?}: {1}")] PoisonedAncestry(SpendAddress, String), #[error("Spend at {addr:?} does not descend from given source: {src:?}")] @@ -69,9 +66,8 @@ impl SpendFault { match self { SpendFault::DoubleSpend(addr) | SpendFault::MissingAncestry { addr, .. } - | SpendFault::InvalidAncestry { addr, .. } + | SpendFault::DoubleSpentAncestor { addr, .. } | SpendFault::InvalidTransaction(addr, _) - | SpendFault::UnknownAncestor { addr, .. } | SpendFault::PoisonedAncestry(addr, _) | SpendFault::OrphanSpend { addr, .. } => *addr, } diff --git a/sn_client/src/audit/spend_check.rs b/sn_client/src/audit/spend_check.rs new file mode 100644 index 0000000000..e2df4a4a7b --- /dev/null +++ b/sn_client/src/audit/spend_check.rs @@ -0,0 +1,260 @@ +// 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 crate::{ + error::{Error, Result}, + Client, +}; + +use futures::future::join_all; +use sn_networking::{target_arch::Instant, GetRecordError, NetworkError}; +use sn_transfers::{Hash, SignedSpend, SpendAddress, WalletError, WalletResult}; +use std::{collections::BTreeSet, iter::Iterator}; + +impl Client { + /// Verify that a spend is valid on the network. + /// Optionally verify its ancestors as well, all the way to genesis (might take a LONG time) + /// + /// Prints progress on stdout. + /// + /// When verifying all the way back to genesis, it only verifies Spends that are ancestors of the given Spend, + /// ignoring all other branches. + /// + /// This is how the DAG it follows could look like: + /// ```text + /// ... -- + /// \ + /// ... ---- ... -- + /// \ \ + /// Spend0 -> Spend1 -----> Spend2 ---> Spend5 ---> Spend2 ---> Genesis + /// \ / + /// ---> Spend3 ---> Spend6 -> + /// \ / + /// -> Spend4 -> + /// / + /// ... + /// + /// depth0 depth1 depth2 depth3 depth4 depth5 + /// ``` + /// + /// This function will return an error if any spend in the way is invalid. + pub async fn verify_spend_at(&self, addr: SpendAddress, to_genesis: bool) -> WalletResult<()> { + let first_spend = self + .get_spend_from_network(addr) + .await + .map_err(|err| WalletError::CouldNotVerifyTransfer(err.to_string()))?; + + if !to_genesis { + return Ok(()); + } + + // use iteration instead of recursion to avoid stack overflow + let mut txs_to_verify = BTreeSet::from_iter([first_spend.spend.parent_tx]); + let mut depth = 0; + let mut verified_tx = BTreeSet::new(); + let start = Instant::now(); + + while !txs_to_verify.is_empty() { + let mut next_gen_tx = 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} - Verifying parent Tx : {parent_tx_hash:?} with inputs: {addrs_to_verify:?}"); + + // get all parent spends in parallel + let tasks: Vec<_> = addrs_to_verify + .clone() + .map(|a| self.get_spend_from_network(a)) + .collect(); + let spends = join_all(tasks).await + .into_iter() + .zip(addrs_to_verify.into_iter()) + .map(|(maybe_spend, a)| + maybe_spend.map_err(|err| WalletError::CouldNotVerifyTransfer(format!("at depth {depth} - Failed to get spend {a:?} from network for parent Tx {parent_tx_hash:?}: {err}")))) + .collect::>>()?; + debug!( + "Depth {depth} - Got {:?} spends for parent Tx: {parent_tx_hash:?}", + spends.len() + ); + trace!("Spends for {parent_tx_hash:?} - {spends:?}"); + + // check if we reached the genesis Tx + if parent_tx == sn_transfers::GENESIS_CASHNOTE.src_tx + && spends + .iter() + .all(|s| s.spend.unique_pubkey == sn_transfers::GENESIS_CASHNOTE.id) + && spends.len() == 1 + { + debug!("Depth {depth} - Reached genesis Tx on one branch: {parent_tx_hash:?}"); + verified_tx.insert(parent_tx_hash); + continue; + } + + // verify tx with those spends + parent_tx + .verify_against_inputs_spent(&spends) + .map_err(|err| WalletError::CouldNotVerifyTransfer(format!("at depth {depth} - Failed to verify parent Tx {parent_tx_hash:?}: {err}")))?; + verified_tx.insert(parent_tx_hash); + debug!("Depth {depth} - Verified parent Tx: {parent_tx_hash:?}"); + + // add new parent spends to next gen + next_gen_tx.extend(spends.into_iter().map(|s| s.spend.parent_tx)); + } + + // only verify parents we haven't already verified + txs_to_verify = next_gen_tx + .into_iter() + .filter(|tx| !verified_tx.contains(&tx.hash())) + .collect(); + + depth += 1; + let elapsed = start.elapsed(); + let n = verified_tx.len(); + println!("Now at depth {depth} - Verified {n} transactions in {elapsed:?}"); + } + + let elapsed = start.elapsed(); + let n = verified_tx.len(); + println!("Verified all the way to genesis! Through {depth} generations, verifying {n} transactions in {elapsed:?}"); + Ok(()) + } + + /// This function does the opposite of verify_spend. + /// It recursively follows the descendants of a Spend, all the way to unspent Transaction Outputs (UTXOs). + /// + /// Prints progress on stdout + /// + /// Starting from Genesis, this amounts to Auditing the entire currency. + /// This is how the DAG it follows could look like: + /// + /// ```text + /// -> Spend7 ---> UTXO_11 + /// / + /// Genesis -> Spend1 -----> Spend2 ---> Spend5 ---> UTXO_10 + /// \ + /// ---> Spend3 ---> Spend6 ---> UTXO_9 + /// \ + /// -> Spend4 ---> UTXO_8 + /// + /// gen0 gen1 gen2 gen3 + /// + /// ``` + /// + /// This function will return the UTXOs (Spend addresses not spent yet) + /// Future calls to this function could start from those UTXOs to avoid + /// re-checking all previously checked branches. + pub async fn follow_spend( + &self, + spend_addr: SpendAddress, + ) -> WalletResult> { + let first_spend = self + .get_spend_from_network(spend_addr) + .await + .map_err(|err| WalletError::CouldNotVerifyTransfer(err.to_string()))?; + println!("Generation 0 - Found first spend: {spend_addr:#?}"); + + // use iteration instead of recursion to avoid stack overflow + let mut txs_to_follow = BTreeSet::from_iter([first_spend.spend.spent_tx]); + let mut all_utxos = BTreeSet::new(); + let mut verified_tx = BTreeSet::new(); + let mut gen = 0; + let start = Instant::now(); + + while !txs_to_follow.is_empty() { + let mut next_gen_tx = BTreeSet::new(); + let mut next_gen_spends = BTreeSet::new(); + let mut next_gen_utxos = BTreeSet::new(); + + 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)); + debug!("Gen {gen} - Following descendant Tx : {descendant_tx_hash:?} with outputs: {addrs_to_follow:?}"); + + // get all descendant spends in parallel + let tasks: Vec<_> = addrs_to_follow + .clone() + .map(|a| self.get_spend_from_network(a)) + .collect(); + let spends_res = join_all(tasks) + .await + .into_iter() + .zip(addrs_to_follow) + .collect::>(); + + // split spends into utxos and spends + let (utxos, spends) = split_utxos_and_spends(spends_res, gen, descendant_tx_hash)?; + debug!("Gen {gen} - Got {:?} spends and {:?} utxos for descendant Tx: {descendant_tx_hash:?}", spends.len(), utxos.len()); + trace!("Spends for {descendant_tx_hash:?} - {spends:?}"); + next_gen_utxos.extend(utxos); + next_gen_spends.extend( + spends + .iter() + .map(|s| SpendAddress::from_unique_pubkey(&s.spend.unique_pubkey)), + ); + + // add new descendant spends to next gen + next_gen_tx.extend(spends.into_iter().map(|s| s.spend.spent_tx)); + } + + // print stats + gen += 1; + let elapsed = start.elapsed(); + let u = next_gen_utxos.len(); + let s = next_gen_spends.len(); + println!("Generation {gen} - Found {u} UTXOs and {s} Spends in {elapsed:?}"); + debug!("Generation {gen} - UTXOs: {:#?}", next_gen_utxos); + debug!("Generation {gen} - Spends: {:#?}", next_gen_spends); + all_utxos.extend(next_gen_utxos); + + // only verify tx we haven't already verified + verified_tx.extend(txs_to_follow.iter().map(|tx| tx.hash())); + txs_to_follow = next_gen_tx + .into_iter() + .filter(|tx| !verified_tx.contains(&tx.hash())) + .collect(); + } + + let elapsed = start.elapsed(); + let n = all_utxos.len(); + let tx = verified_tx.len(); + println!("Finished auditing! Through {gen} generations, found {n} UTXOs and verified {tx} Transactions in {elapsed:?}"); + Ok(all_utxos) + } +} + +fn split_utxos_and_spends( + spends_res: Vec<(Result, SpendAddress)>, + gen: usize, + descendant_tx_hash: Hash, +) -> WalletResult<(Vec, Vec)> { + let mut utxos = Vec::new(); + let mut spends = Vec::new(); + + for (res, addr) in spends_res { + match res { + Ok(spend) => { + spends.push(spend); + } + Err(Error::Network(NetworkError::GetRecordError(GetRecordError::RecordNotFound))) => { + utxos.push(addr); + } + Err(err) => { + warn!("Error while following spend {addr:?}: {err}"); + return Err(WalletError::CouldNotVerifyTransfer(format!("at gen {gen} - Failed to get spend {addr:?} from network for descendant Tx {descendant_tx_hash:?}: {err}"))); + } + } + } + + Ok((utxos, spends)) +} diff --git a/sn_client/src/audit/spend_dag.rs b/sn_client/src/audit/spend_dag.rs index 2d63d90b3f..b11d99a424 100644 --- a/sn_client/src/audit/spend_dag.rs +++ b/sn_client/src/audit/spend_dag.rs @@ -126,7 +126,7 @@ impl SpendDag { ); node_idx } - // or upgrade existing utxo to spend + // or upgrade a known but not gathered entry to spend Some(DagEntry::NotGatheredYet(idx)) => { self.spends .insert(spend_addr, DagEntry::Spend(Box::new(spend.clone()), idx)); @@ -324,42 +324,54 @@ impl SpendDag { } /// helper that returns the direct ancestors of a given spend - fn get_ancestor_spends( + /// along with any faults detected + /// On error returns the address of the missing ancestor + fn get_direct_ancestors( &self, spend: &SignedSpend, - ) -> Result, SpendFault> { + ) -> Result<(BTreeSet, BTreeSet), SpendAddress> { 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); match self.spends.get(&ancestor_addr) { Some(DagEntry::Spend(ancestor_spend, _)) => { ancestors.insert(*ancestor_spend.clone()); } - Some(DagEntry::NotGatheredYet(_)) => { - warn!("UnknownAncestor: ancestor {ancestor_addr:?} was not gathered yet for spend {spend:?}"); - return Err(SpendFault::UnknownAncestor { - addr, - ancestor_addr, - }); - } - Some(DagEntry::DoubleSpend(_)) => { - warn!("InvalidAncestry: DoubleSpend ancestor {ancestor_addr:?} for spend {spend:?}"); - return Err(SpendFault::InvalidAncestry { - addr, - invalid_ancestor: ancestor_addr, - }); + Some(DagEntry::NotGatheredYet(_)) | None => { + warn!("Direct ancestor of {spend:?} at {ancestor_addr:?} is missing"); + return Err(ancestor_addr); } - None => { - warn!("MissingAncestry: ancestor {ancestor_addr:?} is unknown for spend {spend:?}"); - return Err(SpendFault::MissingAncestry { + Some(DagEntry::DoubleSpend(multiple_ancestors)) => { + debug!("Direct ancestor for spend {spend:?} at {ancestor_addr:?} is a double spend"); + faults.insert(SpendFault::DoubleSpentAncestor { addr, - invalid_ancestor: ancestor_addr, + ancestor: ancestor_addr, }); + let actual_ancestor: Vec<_> = multiple_ancestors + .iter() + .filter(|(s, _)| s.spend.spent_tx.hash() == spend.spend.parent_tx.hash()) + .map(|(s, _)| s.clone()) + .collect(); + match actual_ancestor.as_slice() { + [ancestor_spend] => { + debug!("Direct ancestor of {spend:?} at {ancestor_addr:?} is a double spend but one of those match our parent_tx hash, using it for verification"); + ancestors.insert(ancestor_spend.clone()); + } + [ancestor1, _ancestor2, ..] => { + warn!("Direct ancestor of {spend:?} at {ancestor_addr:?} is a double spend and mutliple match our parent_tx hash, using the first one for verification"); + ancestors.insert(ancestor1.clone()); + } + [] => { + warn!("Direct ancestor of {spend:?} at {ancestor_addr:?} is a double spend and none of them match the spend parent_tx, which means the parent for this spend is missing!"); + return Err(ancestor_addr); + } + } } } } - Ok(ancestors) + Ok((ancestors, faults)) } /// helper that returns all the descendants (recursively all the way to UTXOs) of a given spend @@ -543,35 +555,34 @@ impl SpendDag { } // get the ancestors of this spend - let ancestor_spends = match self.get_ancestor_spends(spend) { + let (ancestor_spends, faults) = match self.get_direct_ancestors(spend) { Ok(a) => a, - Err(fault) => { - debug!("Failed to get ancestor spends of {addr:?}: {fault}"); - recorded_faults.insert(fault.clone()); - - // if ancestry is invalid, poison all the descendants - let poison = format!("ancestry issue: {fault}"); + Err(missing_ancestor) => { + debug!("Failed to get ancestor spends of {addr:?} as ancestor at {missing_ancestor:?} is missing"); + recorded_faults.insert(SpendFault::MissingAncestry { + addr, + ancestor: missing_ancestor, + }); + + let poison = format!("missing ancestor at: {missing_ancestor:?}"); let descendants_faults = self.poison_all_descendants(spend, poison)?; recorded_faults.extend(descendants_faults); return Ok(recorded_faults); } }; + recorded_faults.extend(faults); // verify the tx - match spend + if let Err(e) = spend .spend .parent_tx .verify_against_inputs_spent(&ancestor_spends) { - Ok(_) => (), - Err(e) => { - recorded_faults.insert(SpendFault::InvalidTransaction(addr, format!("{e}"))); - - // mark this spend's descendants as poisoned if tx is invalid - let poison = format!("ancestor transaction was poisoned at: {addr:?}: {e}"); - let descendants_faults = self.poison_all_descendants(spend, poison)?; - recorded_faults.extend(descendants_faults); - } + 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}"); + let descendants_faults = self.poison_all_descendants(spend, poison)?; + recorded_faults.extend(descendants_faults); } Ok(recorded_faults) diff --git a/sn_client/src/audit/spend_dag_building.rs b/sn_client/src/audit/spend_dag_building.rs index 0a269b62c2..6fb7737523 100644 --- a/sn_client/src/audit/spend_dag_building.rs +++ b/sn_client/src/audit/spend_dag_building.rs @@ -6,8 +6,7 @@ // 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::{Client, SpendDag}; -use crate::Error; +use crate::{Client, Error, SpendDag}; use futures::{future::join_all, StreamExt}; use sn_networking::{GetRecordError, NetworkError}; diff --git a/sn_client/src/audit/tests/mod.rs b/sn_client/src/audit/tests/mod.rs new file mode 100644 index 0000000000..5a9d28149e --- /dev/null +++ b/sn_client/src/audit/tests/mod.rs @@ -0,0 +1,87 @@ +// 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. + +mod setup; + +use std::collections::BTreeSet; + +use setup::MockNetwork; + +use eyre::Result; + +use crate::{SpendDag, SpendFault}; + +#[test] +fn test_spend_dag_verify_valid_simple() -> Result<()> { + let mut net = MockNetwork::genesis()?; + let genesis = net.genesis_spend; + + let owner1 = net.new_pk_with_balance(100)?; + let owner2 = net.new_pk_with_balance(0)?; + let owner3 = net.new_pk_with_balance(0)?; + let owner4 = net.new_pk_with_balance(0)?; + let owner5 = net.new_pk_with_balance(0)?; + let owner6 = net.new_pk_with_balance(0)?; + + net.send(&owner1, &owner2, 100)?; + net.send(&owner2, &owner3, 100)?; + net.send(&owner3, &owner4, 100)?; + net.send(&owner4, &owner5, 100)?; + net.send(&owner5, &owner6, 100)?; + + let mut dag = SpendDag::new(genesis); + for spend in net.spends { + dag.insert(spend.address(), spend.clone()); + } + + assert_eq!(dag.verify(&genesis), Ok(BTreeSet::new())); + Ok(()) +} + +#[test] +fn test_spend_dag_double_spend_detection() -> Result<()> { + let mut net = MockNetwork::genesis()?; + let genesis = net.genesis_spend; + + let owner1 = net.new_pk_with_balance(100)?; + let owner2a = net.new_pk_with_balance(0)?; + let owner2b = net.new_pk_with_balance(0)?; + + let cn_to_reuse = net + .wallets + .get(&owner1) + .expect("owner1 wallet to exist") + .cn + .clone(); + let spend1_addr = net.send(&owner1, &owner2a, 100)?; + net.wallets + .get_mut(&owner1) + .expect("owner1 wallet to still exist") + .cn = cn_to_reuse; + let spend2_addr = net.send(&owner1, &owner2b, 100)?; + + let mut dag = SpendDag::new(genesis); + for spend in net.spends { + dag.insert(spend.address(), spend.clone()); + } + dag.record_faults(&genesis)?; + + assert_eq!( + spend1_addr, spend2_addr, + "both spends should be at the same address" + ); + assert_eq!(spend1_addr.len(), 1, "there should only be one spend"); + let double_spent = spend1_addr.first().expect("spend1_addr to have an element"); + let expected = BTreeSet::from_iter([SpendFault::DoubleSpend(*double_spent)]); + assert_eq!( + dag.get_spend_faults(double_spent), + expected, + "DAG should have detected double spend" + ); + Ok(()) +} diff --git a/sn_client/src/audit/tests/setup.rs b/sn_client/src/audit/tests/setup.rs new file mode 100644 index 0000000000..cdc7caff80 --- /dev/null +++ b/sn_client/src/audit/tests/setup.rs @@ -0,0 +1,152 @@ +// 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 std::collections::{BTreeMap, BTreeSet}; + +use bls::SecretKey; +use eyre::{eyre, Result}; +use sn_transfers::{ + CashNote, DerivationIndex, Hash, MainPubkey, MainSecretKey, NanoTokens, OfflineTransfer, + SignedSpend, SpendAddress, GENESIS_CASHNOTE, GENESIS_CASHNOTE_SK, +}; +use xor_name::XorName; + +pub struct MockWallet { + pub sk: MainSecretKey, + pub cn: Vec, +} + +pub struct MockNetwork { + pub genesis_spend: SpendAddress, + pub spends: BTreeSet, + pub wallets: BTreeMap, +} + +impl MockNetwork { + pub fn genesis() -> Result { + let mut rng = rand::thread_rng(); + let placeholder = SpendAddress::new(XorName::random(&mut rng)); + let mut net = MockNetwork { + genesis_spend: placeholder, + spends: BTreeSet::new(), + wallets: BTreeMap::new(), + }; + + // create genesis wallet + let genesis_cn = GENESIS_CASHNOTE.clone(); + let genesis_sk = MainSecretKey::new( + SecretKey::from_hex(GENESIS_CASHNOTE_SK) + .map_err(|e| eyre!("failed to parse genesis pk: {e}"))?, + ); + let genesis_pk = genesis_sk.main_pubkey(); + net.wallets.insert( + genesis_pk, + MockWallet { + sk: genesis_sk, + cn: vec![genesis_cn], + }, + ); + + // spend genesis + let everything = GENESIS_CASHNOTE + .value() + .map_err(|e| eyre!("invalid genesis cashnote: {e}"))? + .as_nano(); + let spent_addrs = net + .send(&genesis_pk, &genesis_pk, everything) + .map_err(|e| eyre!("failed to send genesis: {e}"))?; + net.genesis_spend = match spent_addrs.as_slice() { + [one] => *one, + _ => { + return Err(eyre!( + "Expected Genesis spend to be unique but got {spent_addrs:?}" + )) + } + }; + + Ok(net) + } + + pub fn new_pk_with_balance(&mut self, balance: u64) -> Result { + let owner = MainSecretKey::new(SecretKey::random()); + let owner_pk = owner.main_pubkey(); + self.wallets.insert( + owner_pk, + MockWallet { + sk: owner, + cn: Vec::new(), + }, + ); + + if balance > 0 { + let genesis_pk = GENESIS_CASHNOTE.main_pubkey(); + self.send(genesis_pk, &owner_pk, balance) + .map_err(|e| eyre!("failed to get money from genesis: {e}"))?; + } + Ok(owner_pk) + } + + pub fn send( + &mut self, + from: &MainPubkey, + to: &MainPubkey, + amount: u64, + ) -> Result> { + let mut rng = rand::thread_rng(); + let from_wallet = self + .wallets + .get(from) + .ok_or_else(|| eyre!("from wallet not found: {from:?}"))?; + let to_wallet = self + .wallets + .get(to) + .ok_or_else(|| eyre!("to wallet not found: {to:?}"))?; + + // perform offline transfer + let cash_notes_with_keys = from_wallet + .cn + .clone() + .into_iter() + .map(|cn| Ok((cn.clone(), Some(cn.derived_key(&from_wallet.sk)?)))) + .collect::>() + .map_err(|e| eyre!("could not get cashnotes for transfer: {e}"))?; + let recipient = vec![( + NanoTokens::from(amount), + to_wallet.sk.main_pubkey(), + DerivationIndex::random(&mut rng), + )]; + let transfer = OfflineTransfer::new( + cash_notes_with_keys, + recipient, + from_wallet.sk.main_pubkey(), + Hash::default(), + ) + .map_err(|e| eyre!("failed to create transfer: {}", e))?; + let spends = transfer.all_spend_requests; + + // update wallets + let mut updated_from_wallet_cns = from_wallet.cn.clone(); + updated_from_wallet_cns.retain(|cn| { + !spends + .iter() + .any(|s| s.unique_pubkey() == &cn.unique_pubkey()) + }); + updated_from_wallet_cns.extend(transfer.change_cash_note); + self.wallets + .entry(*from) + .and_modify(|w| w.cn = updated_from_wallet_cns); + self.wallets + .entry(*to) + .and_modify(|w| w.cn.extend(transfer.cash_notes_for_recipient)); + + // update network spends + let spent_addrs = spends.iter().map(|s| s.address()).collect(); + self.spends.extend(spends); + Ok(spent_addrs) + } +} diff --git a/sn_node/tests/double_spend.rs b/sn_node/tests/double_spend.rs index d861e2b5d8..990eb0cff7 100644 --- a/sn_node/tests/double_spend.rs +++ b/sn_node/tests/double_spend.rs @@ -73,8 +73,8 @@ async fn cash_note_transfer_double_spend_fail() -> Result<()> { // check the CashNotes, it should fail info!("Verifying the transfers from first wallet..."); - let cash_notes_for_2: Vec<_> = transfer_to_2.created_cash_notes.clone(); - let cash_notes_for_3: Vec<_> = transfer_to_3.created_cash_notes.clone(); + let cash_notes_for_2: Vec<_> = transfer_to_2.cash_notes_for_recipient.clone(); + let cash_notes_for_3: Vec<_> = transfer_to_3.cash_notes_for_recipient.clone(); let could_err1 = client.verify_cashnote(&cash_notes_for_2[0]).await; let could_err2 = client.verify_cashnote(&cash_notes_for_3[0]).await; @@ -125,7 +125,7 @@ async fn genesis_double_spend_fail() -> Result<()> { assert!(res.is_ok()); // put the bad cashnote in the first wallet - first_wallet.deposit_and_store_to_disk(&transfer.created_cash_notes)?; + first_wallet.deposit_and_store_to_disk(&transfer.cash_notes_for_recipient)?; // now try to spend this illegitimate cashnote (direct descendant of double spent genesis) let (genesis_cashnote_and_others, exclusive_access) = first_wallet.available_cash_notes()?; diff --git a/sn_transfers/benches/reissue.rs b/sn_transfers/benches/reissue.rs index ead6cde1e1..b2c8cfc900 100644 --- a/sn_transfers/benches/reissue.rs +++ b/sn_transfers/benches/reissue.rs @@ -115,12 +115,12 @@ fn bench_reissue_100_to_1(c: &mut Criterion) { // prepare to send all of those cashnotes back to our starting_main_key let total_amount = offline_transfer - .created_cash_notes + .cash_notes_for_recipient .iter() .map(|cn| cn.value().unwrap().as_nano()) .sum(); let many_cashnotes = offline_transfer - .created_cash_notes + .cash_notes_for_recipient .into_iter() .map(|cn| { let derivation_index = cn.derivation_index(); diff --git a/sn_transfers/src/transfers/offline_transfer.rs b/sn_transfers/src/transfers/offline_transfer.rs index d1f478ffa4..ca44440740 100644 --- a/sn_transfers/src/transfers/offline_transfer.rs +++ b/sn_transfers/src/transfers/offline_transfer.rs @@ -31,7 +31,7 @@ pub struct OfflineTransfer { /// The cash_notes that were created containing /// the tokens sent to respective recipient. #[debug(skip)] - pub created_cash_notes: Vec, + pub cash_notes_for_recipient: Vec, /// The cash_note holding surplus tokens after /// spending the necessary input cash_notes. #[debug(skip)] @@ -70,7 +70,7 @@ impl OfflineTransfer { Ok(Self { tx, - created_cash_notes, + cash_notes_for_recipient: created_cash_notes, change_cash_note, all_spend_requests: signed_spends.into_iter().collect(), }) @@ -345,7 +345,7 @@ fn create_offline_transfer_with( Ok(OfflineTransfer { tx, - created_cash_notes, + 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 bcf23d97ae..7d738952d1 100644 --- a/sn_transfers/src/wallet/hot_wallet.rs +++ b/sn_transfers/src/wallet/hot_wallet.rs @@ -335,7 +335,7 @@ impl HotWallet { reason_hash, )?; - let created_cash_notes = transfer.created_cash_notes.clone(); + let created_cash_notes = transfer.cash_notes_for_recipient.clone(); self.update_local_wallet(transfer, exclusive_access)?; @@ -354,7 +354,7 @@ impl HotWallet { let transfer = OfflineTransfer::from_transaction(signed_spends, tx, change_id, output_details)?; - let created_cash_notes = transfer.created_cash_notes.clone(); + let created_cash_notes = transfer.cash_notes_for_recipient.clone(); trace!("Trying to lock wallet to get available cash_notes..."); // lock and load from disk to make sure we're up to date and others can't modify the wallet concurrently @@ -436,14 +436,14 @@ impl HotWallet { )?; trace!( "local_send_storage_payment created offline_transfer with {} cashnotes in {:?}", - offline_transfer.created_cash_notes.len(), + offline_transfer.cash_notes_for_recipient.len(), start.elapsed() ); let start = Instant::now(); // cache transfer payments in the wallet let mut cashnotes_to_use: HashSet = offline_transfer - .created_cash_notes + .cash_notes_for_recipient .iter() .cloned() .collect();