Skip to content

Commit

Permalink
refactor: spend verification error management
Browse files Browse the repository at this point in the history
  • Loading branch information
grumbach committed May 29, 2024
1 parent 19f8a07 commit 78effff
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 44 deletions.
3 changes: 1 addition & 2 deletions sn_auditor/src/dag_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,7 @@ pub async fn new_dag_with_genesis_only(client: &Client) -> Result<SpendDag> {
let mut dag = SpendDag::new(genesis_addr);
let genesis_spend = match client.get_spend_from_network(genesis_addr).await {
Ok(s) => s,
Err(ClientError::Network(NetworkError::DoubleSpendAttempt(spend1, spend2)))
| Err(ClientError::DoubleSpend(_, spend1, spend2)) => {
Err(ClientError::Network(NetworkError::DoubleSpendAttempt(spend1, spend2))) => {
let addr = spend1.address();
println!("Double spend detected at Genesis: {addr:?}");
dag.insert(genesis_addr, *spend2);
Expand Down
133 changes: 94 additions & 39 deletions sn_client/src/audit/spend_dag_building.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,28 @@ use sn_networking::{GetRecordError, NetworkError};
use sn_transfers::{SignedSpend, SpendAddress, WalletError, WalletResult};
use std::collections::BTreeSet;

enum InternalGetNetworkSpend {
Spend(Box<SignedSpend>),
DoubleSpend(Box<SignedSpend>, Box<SignedSpend>),
NotFound,
Error(Error),
}

impl Client {
/// Builds a SpendDag from a given SpendAddress recursively following descendants all the way to UTxOs
/// Started from Genesis this gives the entire SpendDag of the Network at a certain point in time
/// Once the DAG collected, optionally verifies and records errors in the DAG
///
/// ```text
/// -> Spend7 ---> UTXO_11
/// /
/// Genesis -> Spend1 -----> Spend2 ---> Spend5 ---> UTXO_10
/// \
/// ---> Spend3 ---> Spend6 ---> UTXO_9
/// \
/// -> Spend4 ---> UTXO_8
///
/// ```
pub async fn spend_dag_build_from(
&self,
spend_addr: SpendAddress,
Expand All @@ -27,21 +45,18 @@ impl Client {
let mut dag = SpendDag::new(spend_addr);

// get first spend
let first_spend = match self.get_spend_from_network(spend_addr).await {
Ok(s) => s,
Err(Error::Network(NetworkError::GetRecordError(GetRecordError::RecordNotFound))) => {
let first_spend = match self.crawl_spend(spend_addr).await {
InternalGetNetworkSpend::Spend(s) => *s,
InternalGetNetworkSpend::DoubleSpend(s1, s2) => {
dag.insert(spend_addr, *s2);
*s1
}
InternalGetNetworkSpend::NotFound => {
// the cashnote was not spent yet, so it's an UTXO
info!("UTXO at {spend_addr:?}");
return Ok(dag);
}
Err(Error::Network(NetworkError::DoubleSpendAttempt(s1, s2))) => {
// the cashnote was spent twice, take it into account and continue
info!("Double spend at {spend_addr:?}");
dag.insert(spend_addr, *s2);
*s1
}
Err(e) => {
warn!("Failed to get spend at {spend_addr:?}: {e}");
InternalGetNetworkSpend::Error(e) => {
return Err(WalletError::FailedToGetSpend(e.to_string()));
}
};
Expand Down Expand Up @@ -71,10 +86,7 @@ impl Client {

// get all spends in parallel
let mut stream = futures::stream::iter(addrs.clone())
// For crawling, a special fetch policy is deployed to improve the performance:
// 1, Expect `majority` copies as it is a `Spend`;
// 2, But don't retry as most will be `UTXO` which won't be found.
.map(|a| async move { (self.crawl_spend_from_network(a).await, a) })
.map(|a| async move { (self.crawl_spend(a).await, a) })
.buffer_unordered(crate::MAX_CONCURRENT_TASKS);
info!(
"Gen {gen} - Getting {} spends from {} txs in batches of: {}",
Expand All @@ -84,20 +96,22 @@ impl Client {
);

// insert spends in the dag as they are collected
while let Some((res, addr)) = stream.next().await {
match res {
Ok(spend) => {
info!("Fetched spend {addr:?} from network.");
dag.insert(addr, spend.clone());
while let Some((get_spend, addr)) = stream.next().await {
match get_spend {
InternalGetNetworkSpend::Spend(spend) => {
next_gen_tx.insert(spend.spend.spent_tx.clone());
dag.insert(addr, *spend);
}
Err(Error::Network(NetworkError::GetRecordError(
GetRecordError::RecordNotFound,
))) => {
info!("Reached UTXO at {addr:?}");
InternalGetNetworkSpend::DoubleSpend(s1, s2) => {
info!("Fetched double spend at {addr:?} from network, following both...");
next_gen_tx.insert(s1.spend.spent_tx.clone());
next_gen_tx.insert(s2.spend.spent_tx.clone());
dag.insert(addr, *s1);
dag.insert(addr, *s2);
}
Err(err) => {
error!("Failed to get spend at {addr:?} during DAG collection: {err:?}");
InternalGetNetworkSpend::NotFound => info!("Reached UTXO at {addr:?}"),
InternalGetNetworkSpend::Error(err) => {
error!("Failed to get spend at {addr:?} during DAG collection: {err:?}")
}
}
}
Expand Down Expand Up @@ -186,35 +200,49 @@ impl Client {
// get all parent spends in parallel
let tasks: Vec<_> = addrs_to_verify
.clone()
.map(|a| self.crawl_spend_from_network(a))
.map(|a| self.crawl_spend(a))
.collect();
let spends = join_all(tasks).await
let mut spends = BTreeSet::new();
for (spend_get, a) in join_all(tasks)
.await
.into_iter()
.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::<WalletResult<BTreeSet<_>>>()?;
debug!(
"Depth {depth} - Got {:?} spends for parent Tx: {parent_tx_hash:?}",
spends.len()
);
{
match spend_get {
InternalGetNetworkSpend::Spend(s) => {
spends.insert(*s);
}
InternalGetNetworkSpend::DoubleSpend(s1, s2) => {
spends.extend([*s1, *s2]);
}
InternalGetNetworkSpend::NotFound => {
return Err(WalletError::FailedToGetSpend(format!(
"Missing ancestor spend at {a:?}"
)))
}
InternalGetNetworkSpend::Error(e) => {
return Err(WalletError::FailedToGetSpend(format!(
"Failed to get ancestor spend at {a:?}: {e}"
)))
}
}
}
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)
&& spends.len() == 1
{
debug!("Depth {depth} - reached genesis Tx on one branch: {parent_tx_hash:?}");
known_txs.insert(parent_tx_hash);
continue;
}

known_txs.insert(parent_tx_hash);
debug!("Depth {depth} - Verified parent Tx: {parent_tx_hash:?}");

// 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();
Expand Down Expand Up @@ -314,4 +342,31 @@ impl Client {
self.spend_dag_continue_from(dag, utxos, max_depth, verify)
.await
}

/// Internal get spend helper for DAG purposes
/// For crawling, a special fetch policy is deployed to improve the performance:
/// 1. Expect `majority` copies as it is a `Spend`;
/// 2. But don't retry as most will be `UTXO` which won't be found.
async fn crawl_spend(&self, spend_addr: SpendAddress) -> InternalGetNetworkSpend {
match self.crawl_spend_from_network(spend_addr).await {
Ok(s) => {
debug!("DAG crawling: fetched spend {spend_addr:?} from network");
InternalGetNetworkSpend::Spend(Box::new(s))
}
Err(Error::Network(NetworkError::GetRecordError(GetRecordError::RecordNotFound))) => {
debug!("DAG crawling: spend at {spend_addr:?} not found on the network");
InternalGetNetworkSpend::NotFound
}
Err(Error::Network(NetworkError::DoubleSpendAttempt(s1, s2))) => {
debug!("DAG crawling: got a double spend at {spend_addr:?} on the network");
InternalGetNetworkSpend::DoubleSpend(s1, s2)
}
Err(e) => {
debug!(
"DAG crawling: got an error for spend at {spend_addr:?} on the network: {e}"
);
InternalGetNetworkSpend::Error(e)
}
}
}
}
3 changes: 0 additions & 3 deletions sn_client/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ use crate::UploadSummary;
use super::ClientEvent;
use sn_protocol::NetworkAddress;
use sn_registers::{Entry, EntryHash};
use sn_transfers::{SignedSpend, SpendAddress};
use std::collections::BTreeSet;
use thiserror::Error;
use tokio::time::Duration;
Expand Down Expand Up @@ -68,8 +67,6 @@ pub enum Error {
/// A general error when verifying a transfer validity in the network.
#[error("Failed to verify transfer validity in the network {0}")]
CouldNotVerifyTransfer(String),
#[error("Double spend detected at address: {0:?}")]
DoubleSpend(SpendAddress, Box<SignedSpend>, Box<SignedSpend>),
#[error("Invalid DAG")]
InvalidDag,
#[error("Serialization error: {0:?}")]
Expand Down

0 comments on commit 78effff

Please sign in to comment.