diff --git a/sn_auditor/src/dag_db.rs b/sn_auditor/src/dag_db.rs index 98a11e4a0c..94759c7497 100644 --- a/sn_auditor/src/dag_db.rs +++ b/sn_auditor/src/dag_db.rs @@ -82,7 +82,7 @@ impl SpendDagDb { let (spend_type, spends) = match spend { SpendDagGet::SpendNotFound => ("SpendNotFound", vec![]), - SpendDagGet::SpendIsAnUtxo => ("SpendIsAnUtxo", vec![]), + SpendDagGet::SpendKeyExists => ("SpendKeyExists", vec![]), SpendDagGet::DoubleSpend(vs) => ("DoubleSpend", vs), SpendDagGet::Spend(s) => ("Spend", vec![*s]), }; @@ -176,7 +176,7 @@ impl SpendDagDb { let mut w_handle = dag_ref .write() .map_err(|e| eyre!("Failed to get write lock: {e}"))?; - w_handle.merge(other); + w_handle.merge(other)?; Ok(()) } } @@ -218,17 +218,31 @@ fn dag_to_svg(dag: &SpendDag) -> Result> { // - makes spends clickable // - spend address reveals on hover // - marks poisoned spends as red +// - marks UTXOs and unknown ancestors as yellow // - just pray it works on windows fn quick_edit_svg(svg: Vec, dag: &SpendDag) -> Result> { let mut str = String::from_utf8(svg).map_err(|err| eyre!("Failed svg conversion: {err}"))?; let spend_addrs: Vec<_> = dag.all_spends().iter().map(|s| s.address()).collect(); let utxo_addrs = dag.get_utxos(); + let unknown_parents = dag.get_unknown_parents(); + let all_addrs = spend_addrs + .iter() + .chain(utxo_addrs.iter()) + .chain(unknown_parents.iter()); - for addr in spend_addrs.iter().chain(utxo_addrs.iter()) { + for addr in all_addrs { let addr_hex = addr.to_hex().to_string(); let is_fault = !dag.get_spend_faults(addr).is_empty(); - let colour = if is_fault { "red" } else { "none" }; + let is_known_but_not_gathered = matches!(dag.get_spend(addr), SpendDagGet::SpendKeyExists); + let colour = if is_fault { + "red" + } else if is_known_but_not_gathered { + "yellow" + } else { + "none" + }; + let link = format!(""); let idxs = dag.get_spend_indexes(addr); for i in idxs { diff --git a/sn_client/src/audit.rs b/sn_client/src/audit.rs index 8f81eb2669..6eac78efa5 100644 --- a/sn_client/src/audit.rs +++ b/sn_client/src/audit.rs @@ -20,7 +20,7 @@ use super::{ use futures::future::join_all; use sn_networking::{target_arch::Instant, GetRecordError, NetworkError}; -use sn_transfers::{SignedSpend, SpendAddress, WalletError, WalletResult}; +use sn_transfers::{Hash, SignedSpend, SpendAddress, WalletError, WalletResult}; use std::{collections::BTreeSet, iter::Iterator}; impl Client { @@ -73,17 +73,19 @@ impl Client { 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:?}"); + 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 - .into_iter() + .clone() .map(|a| self.get_spend_from_network(a)) .collect(); let spends = join_all(tasks).await .into_iter() - .collect::>>() - .map_err(|err| WalletError::CouldNotVerifyTransfer(format!("at depth {depth} - Failed to get spends from network for parent Tx {parent_tx_hash:?}: {err}")))?; + .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() @@ -184,7 +186,7 @@ impl Client { .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:?}"); + 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 @@ -198,8 +200,7 @@ impl Client { .collect::>(); // split spends into utxos and spends - let (utxos, spends) = split_utxos_and_spends(spends_res) - .map_err(|err| WalletError::CouldNotVerifyTransfer(format!("at gen {gen} - Failed to get spends from network for descendant Tx {descendant_tx_hash:?}: {err}")))?; + 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); @@ -241,7 +242,9 @@ impl Client { fn split_utxos_and_spends( spends_res: Vec<(Result, SpendAddress)>, -) -> Result<(Vec, Vec)> { + gen: usize, + descendant_tx_hash: Hash, +) -> WalletResult<(Vec, Vec)> { let mut utxos = Vec::new(); let mut spends = Vec::new(); @@ -254,8 +257,8 @@ fn split_utxos_and_spends( utxos.push(addr); } Err(err) => { - warn!("Error while following spends: {err}"); - return 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}"))); } } } diff --git a/sn_client/src/audit/dag_error.rs b/sn_client/src/audit/dag_error.rs index 7423ffdb47..d553ed22ee 100644 --- a/sn_client/src/audit/dag_error.rs +++ b/sn_client/src/audit/dag_error.rs @@ -40,6 +40,11 @@ pub enum SpendFault { }, #[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:?}")] @@ -66,6 +71,7 @@ impl SpendFault { | SpendFault::MissingAncestry { addr, .. } | SpendFault::InvalidAncestry { addr, .. } | SpendFault::InvalidTransaction(addr, _) + | SpendFault::UnknownAncestor { addr, .. } | SpendFault::PoisonedAncestry(addr, _) | SpendFault::OrphanSpend { addr, .. } => *addr, } diff --git a/sn_client/src/audit/spend_dag.rs b/sn_client/src/audit/spend_dag.rs index 197f1d73a8..2d63d90b3f 100644 --- a/sn_client/src/audit/spend_dag.rs +++ b/sn_client/src/audit/spend_dag.rs @@ -46,7 +46,7 @@ type DagIndex = usize; /// Internal Dag entry type #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] enum DagEntry { - Utxo(DagIndex), + NotGatheredYet(DagIndex), DoubleSpend(Vec<(SignedSpend, DagIndex)>), Spend(Box, DagIndex), } @@ -54,7 +54,7 @@ enum DagEntry { impl DagEntry { fn indexes(&self) -> Vec { match self { - DagEntry::Utxo(idx) => vec![*idx], + DagEntry::NotGatheredYet(idx) => vec![*idx], DagEntry::DoubleSpend(spends) => spends.iter().map(|(_, idx)| *idx).collect(), DagEntry::Spend(_, idx) => vec![*idx], } @@ -64,7 +64,7 @@ impl DagEntry { match self { DagEntry::Spend(spend, _) => vec![&**spend], DagEntry::DoubleSpend(spends) => spends.iter().map(|(s, _)| s).collect(), - DagEntry::Utxo(_) => vec![], + DagEntry::NotGatheredYet(_) => vec![], } } } @@ -74,8 +74,8 @@ impl DagEntry { pub enum SpendDagGet { /// Spend does not exist in the DAG SpendNotFound, - /// Spend is an UTXO, meaning it was not spent yet but its ancestors exist - SpendIsAnUtxo, + /// Spend key is refered to by known spends but does not exist in the DAG yet + SpendKeyExists, /// Spend is a double spend DoubleSpend(Vec), /// Spend is in the DAG @@ -127,7 +127,7 @@ impl SpendDag { node_idx } // or upgrade existing utxo to spend - Some(DagEntry::Utxo(idx)) => { + Some(DagEntry::NotGatheredYet(idx)) => { self.spends .insert(spend_addr, DagEntry::Spend(Box::new(spend.clone()), idx)); NodeIndex::new(idx) @@ -168,7 +168,7 @@ impl SpendDag { // add descendant if not already in dag let spends_at_addr = self.spends.entry(descendant_addr).or_insert_with(|| { let node_idx = self.dag.add_node(descendant_addr); - DagEntry::Utxo(node_idx.index()) + DagEntry::NotGatheredYet(node_idx.index()) }); // link to descendant @@ -192,7 +192,7 @@ impl SpendDag { // add ancestor if not already in dag let spends_at_addr = self.spends.entry(ancestor_addr).or_insert_with(|| { let node_idx = self.dag.add_node(ancestor_addr); - DagEntry::Utxo(node_idx.index()) + DagEntry::NotGatheredYet(node_idx.index()) }); // link to ancestor @@ -206,6 +206,27 @@ impl SpendDag { true } + /// Get the unknown parents: all the addresses that are refered to as parents by other spends + /// but don't have parents themselves. + /// Those Spends must exist somewhere on the Network, we just haven't gathered them yet. + pub fn get_unknown_parents(&self) -> BTreeSet { + let mut sources = BTreeSet::new(); + for node_index in self.dag.node_indices() { + if !self + .dag + .neighbors_directed(node_index, petgraph::Direction::Incoming) + .any(|_| true) + { + let utxo_addr = self.dag[node_index]; + sources.insert(utxo_addr); + } + } + sources + } + + /// Get the UTXOs: all the addresses that are refered to as children by other spends + /// but that don't have children themselves. + /// Those will eventually exist on the Network as the address is spent by their owners. pub fn get_utxos(&self) -> BTreeSet { let mut leaves = BTreeSet::new(); for node_index in self.dag.node_indices() { @@ -226,18 +247,19 @@ impl SpendDag { } /// Merges the given dag into ours - pub fn merge(&mut self, sub_dag: SpendDag) { + pub fn merge(&mut self, sub_dag: SpendDag) -> Result<(), DagError> { + let source = self.source(); info!( "Merging sub DAG starting at {:?} into our DAG with source {:?}", sub_dag.source(), - self.source() + source ); for (addr, spends) in sub_dag.spends { - // only add spends to the dag, ignoring utxos + // only add spends to the dag, ignoring utxos and not yet gathered relatives // utxos will be added automatically as their ancestors are added // edges are updated by the insert method match spends { - DagEntry::Utxo(_) => continue, + DagEntry::NotGatheredYet(_) => continue, DagEntry::DoubleSpend(spends) => { for (spend, _) in spends { self.insert(addr, spend); @@ -249,15 +271,15 @@ impl SpendDag { } } - // merge errors - self.faults.extend(sub_dag.faults); + // recompute faults + self.record_faults(&source) } /// Get the spend at a given address pub fn get_spend(&self, addr: &SpendAddress) -> SpendDagGet { match self.spends.get(addr) { None => SpendDagGet::SpendNotFound, - Some(DagEntry::Utxo(_)) => SpendDagGet::SpendIsAnUtxo, + Some(DagEntry::NotGatheredYet(_)) => SpendDagGet::SpendKeyExists, Some(DagEntry::DoubleSpend(spends)) => { SpendDagGet::DoubleSpend(spends.iter().map(|(s, _)| s.clone()).collect()) } @@ -314,11 +336,11 @@ impl SpendDag { Some(DagEntry::Spend(ancestor_spend, _)) => { ancestors.insert(*ancestor_spend.clone()); } - Some(DagEntry::Utxo(_)) => { - warn!("InvalidAncestry: SpendIsAnUtxo ancestor {ancestor_addr:?} for spend {spend:?}"); - return Err(SpendFault::InvalidAncestry { + Some(DagEntry::NotGatheredYet(_)) => { + warn!("UnknownAncestor: ancestor {ancestor_addr:?} was not gathered yet for spend {spend:?}"); + return Err(SpendFault::UnknownAncestor { addr, - invalid_ancestor: ancestor_addr, + ancestor_addr, }); } Some(DagEntry::DoubleSpend(_)) => { @@ -329,10 +351,11 @@ impl SpendDag { }); } None => { + warn!("MissingAncestry: ancestor {ancestor_addr:?} is unknown for spend {spend:?}"); return Err(SpendFault::MissingAncestry { addr, invalid_ancestor: ancestor_addr, - }) + }); } } } @@ -345,8 +368,8 @@ impl SpendDag { let mut to_traverse = BTreeSet::from_iter(vec![addr]); while let Some(current_addr) = to_traverse.pop_first() { // get the spend at this address - let indexes = match self.spends.get(current_addr) { - Some(entry) => entry.indexes(), + let dag_entry = match self.spends.get(current_addr) { + Some(entry) => entry, None => { warn!("Incoherent DAG, missing descendant spend when expecting one at: {current_addr:?}"); return Err(DagError::IncoherentDag( @@ -355,6 +378,14 @@ impl SpendDag { )); } }; + let (spends, indexes) = (dag_entry.spends(), dag_entry.indexes()); + + // get descendants via Tx data + let descendants_via_tx: BTreeSet = spends + .into_iter() + .flat_map(|s| s.spend.spent_tx.outputs.to_vec()) + .map(|o| SpendAddress::from_unique_pubkey(&o.unique_pubkey)) + .collect(); // get descendants via DAG let descendants_via_dag: BTreeSet<&SpendAddress> = indexes @@ -366,24 +397,17 @@ impl SpendDag { }) .collect(); - // get descendants via Tx data - let descendants_via_tx: BTreeSet = self - .spends - .get(current_addr) - .map(|entry| entry.spends()) - .unwrap_or_default() - .into_iter() - .flat_map(|s| s.spend.spent_tx.outputs.to_vec()) - .map(|o| SpendAddress::from_unique_pubkey(&o.unique_pubkey)) - .collect(); - // report inconsistencies if descendants_via_dag != descendants_via_tx.iter().collect() { - 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:?}") - )); + 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:?}") + )); + } } // continue traversal @@ -430,6 +454,7 @@ impl SpendDag { pub fn record_faults(&mut self, source: &SpendAddress) -> Result<(), DagError> { let faults = self.verify(source)?; + self.faults.clear(); for f in faults { self.faults.entry(f.spend_address()).or_default().insert(f); } @@ -520,9 +545,14 @@ impl SpendDag { // get the ancestors of this spend let ancestor_spends = match self.get_ancestor_spends(spend) { Ok(a) => a, - Err(e) => { - debug!("Failed to get ancestor spends of: {addr:?} {e}"); - recorded_faults.insert(e); + 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}"); + let descendants_faults = self.poison_all_descendants(spend, poison)?; + recorded_faults.extend(descendants_faults); return Ok(recorded_faults); } }; diff --git a/sn_client/src/audit/spend_dag_building.rs b/sn_client/src/audit/spend_dag_building.rs index c81c53e0eb..0a269b62c2 100644 --- a/sn_client/src/audit/spend_dag_building.rs +++ b/sn_client/src/audit/spend_dag_building.rs @@ -7,7 +7,7 @@ // permissions and limitations relating to use of the SAFE Network Software. use super::{Client, SpendDag}; -use crate::{Error, Result}; +use crate::Error; use futures::{future::join_all, StreamExt}; use sn_networking::{GetRecordError, NetworkError}; @@ -93,7 +93,7 @@ impl Client { info!("Reached UTXO at {addr:?}"); } Err(err) => { - error!("Could not verify transfer at {addr:?}: {err:?}"); + error!("Failed to get spend at {addr:?} during DAG collection: {err:?}"); } } } @@ -175,7 +175,7 @@ impl Client { 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:?}"); + debug!("Depth {depth} - checking parent Tx : {parent_tx_hash:?} with inputs: {addrs_to_verify:?}"); // get all parent spends in parallel let tasks: Vec<_> = addrs_to_verify @@ -184,8 +184,10 @@ impl Client { .collect(); let spends = join_all(tasks).await .into_iter() - .collect::>>() - .map_err(|err| WalletError::FailedToGetSpend(format!("at depth {depth} - Failed to get spends from network for parent Tx {parent_tx_hash:?}: {err}")))?; + .zip(addrs_to_verify.clone()) + .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() @@ -259,7 +261,8 @@ impl Client { dag: &mut SpendDag, max_depth: Option, ) -> WalletResult<()> { - info!("Gathering spend DAG from utxos..."); + let main_dag_src = dag.source(); + info!("Expanding spend DAG with source: {main_dag_src:?} from utxos..."); let utxos = dag.get_utxos(); let mut stream = futures::stream::iter(utxos.into_iter()) @@ -271,9 +274,14 @@ impl Client { while let Some((res, addr)) = stream.next().await { match res { - Ok(d) => dag.merge(d), + Ok(sub_dag) => { + debug!("Gathered sub DAG from: {addr:?}"); + if let Err(e) = dag.merge(sub_dag) { + warn!("Failed to merge sub dag from {addr:?} into dag: {e}"); + } + } Err(e) => warn!("Failed to gather sub dag from {addr:?}: {e}"), - } + }; } dag.record_faults(&dag.source())