diff --git a/sn_client/src/audit/dag_crawling.rs b/sn_client/src/audit/dag_crawling.rs index 3a6ffc726a..ddc1ab7aa9 100644 --- a/sn_client/src/audit/dag_crawling.rs +++ b/sn_client/src/audit/dag_crawling.rs @@ -24,7 +24,7 @@ const SPENDS_PROCESSING_BUFFER_SIZE: usize = 4096; enum InternalGetNetworkSpend { Spend(Box), - DoubleSpend(Box, Box), + DoubleSpend(Vec), NotFound, Error(Error), } @@ -33,17 +33,18 @@ impl Client { pub async fn new_dag_with_genesis_only(&self) -> WalletResult { let genesis_addr = SpendAddress::from_unique_pubkey(&GENESIS_SPEND_UNIQUE_KEY); let mut dag = SpendDag::new(genesis_addr); - let genesis_spend = match self.get_spend_from_network(genesis_addr).await { - Ok(s) => s, - Err(Error::Network(NetworkError::DoubleSpendAttempt(spend1, spend2))) => { - let addr = spend1.address(); - println!("Double spend detected at Genesis: {addr:?}"); - dag.insert(genesis_addr, *spend2); - *spend1 + match self.get_spend_from_network(genesis_addr).await { + Ok(spend) => { + dag.insert(genesis_addr, spend); + } + Err(Error::Network(NetworkError::DoubleSpendAttempt(spends))) => { + println!("Double spend detected at Genesis: {genesis_addr:?}"); + for spend in spends.into_iter() { + dag.insert(genesis_addr, spend); + } } Err(e) => return Err(WalletError::FailedToGetSpend(e.to_string())), }; - dag.insert(genesis_addr, genesis_spend); Ok(dag) } @@ -163,7 +164,7 @@ impl Client { .map_err(|e| WalletError::SpendProcessing(e.to_string())); addrs_to_get.extend(for_further_track); } - InternalGetNetworkSpend::DoubleSpend(_s1, _s2) => { + InternalGetNetworkSpend::DoubleSpend(_spends) => { warn!("Detected double spend regarding {address:?}"); } InternalGetNetworkSpend::NotFound => { @@ -193,14 +194,27 @@ impl Client { let mut utxos = BTreeSet::new(); // get first spend - let first_spend = match self.crawl_spend(spend_addr).await { - InternalGetNetworkSpend::Spend(s) => *s, - InternalGetNetworkSpend::DoubleSpend(s1, s2) => { + let mut txs_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()]); + spend_processing - .send(*s2) + .send(spend) .await .map_err(|e| WalletError::SpendProcessing(e.to_string()))?; - *s1 + txs + } + InternalGetNetworkSpend::DoubleSpend(spends) => { + let mut txs = BTreeSet::new(); + for spend in spends.into_iter() { + txs.insert(spend.spend.spent_tx.clone()); + spend_processing + .send(spend) + .await + .map_err(|e| WalletError::SpendProcessing(e.to_string()))?; + } + txs } InternalGetNetworkSpend::NotFound => { // the cashnote was not spent yet, so it's an UTXO @@ -212,13 +226,8 @@ impl Client { return Err(WalletError::FailedToGetSpend(e.to_string())); } }; - spend_processing - .send(first_spend.clone()) - .await - .map_err(|e| WalletError::SpendProcessing(e.to_string()))?; // use iteration instead of recursion to avoid stack overflow - let mut txs_to_follow = BTreeSet::from_iter([first_spend.spend.spent_tx]); let mut known_tx = BTreeSet::new(); let mut gen: u32 = 0; let start = std::time::Instant::now(); @@ -260,18 +269,15 @@ impl Client { .await .map_err(|e| WalletError::SpendProcessing(e.to_string()))?; } - 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()); - spend_processing - .send(*s1.clone()) - .await - .map_err(|e| WalletError::SpendProcessing(e.to_string()))?; - spend_processing - .send(*s2.clone()) - .await - .map_err(|e| WalletError::SpendProcessing(e.to_string()))?; + 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()); + spend_processing + .send(s.clone()) + .await + .map_err(|e| WalletError::SpendProcessing(e.to_string()))?; + } } InternalGetNetworkSpend::NotFound => { info!("Reached UTXO at {addr:?}"); @@ -360,8 +366,8 @@ impl Client { InternalGetNetworkSpend::Spend(s) => { spends.insert(*s); } - InternalGetNetworkSpend::DoubleSpend(s1, s2) => { - spends.extend([*s1, *s2]); + InternalGetNetworkSpend::DoubleSpend(s) => { + spends.extend(s.into_iter()); } InternalGetNetworkSpend::NotFound => { return Err(WalletError::FailedToGetSpend(format!( @@ -498,9 +504,9 @@ impl Client { 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(Error::Network(NetworkError::DoubleSpendAttempt(spends))) => { + debug!("DAG crawling: got a double spend(s) of len {} at {spend_addr:?} on the network", spends.len()); + InternalGetNetworkSpend::DoubleSpend(spends) } Err(e) => { debug!( diff --git a/sn_networking/src/error.rs b/sn_networking/src/error.rs index cbaa4db731..103a79a9a3 100644 --- a/sn_networking/src/error.rs +++ b/sn_networking/src/error.rs @@ -138,8 +138,8 @@ pub enum NetworkError { // ---------- Spend Errors #[error("Spend not found: {0:?}")] NoSpendFoundInsideRecord(SpendAddress), - #[error("A double spend was detected. Two diverging signed spends: {0:?}, {1:?}")] - DoubleSpendAttempt(Box, Box), + #[error("Double spend(s) was detected. The signed spends are: {0:?}")] + DoubleSpendAttempt(Vec), // ---------- Store Error #[error("No Store Cost Responses")] diff --git a/sn_networking/src/event/kad.rs b/sn_networking/src/event/kad.rs index ec941a933e..8f72b5f3f7 100644 --- a/sn_networking/src/event/kad.rs +++ b/sn_networking/src/event/kad.rs @@ -9,6 +9,7 @@ use crate::{ driver::PendingGetClosestType, get_quorum_value, get_raw_signed_spends_from_record, GetRecordCfg, GetRecordError, NetworkError, Result, SwarmDriver, CLOSE_GROUP_SIZE, + MAX_DOUBLE_SPEND_ATTEMPTS_PER_RECORD, }; use itertools::Itertools; use libp2p::kad::{ @@ -414,7 +415,7 @@ impl SwarmDriver { info!("For record {pretty_key:?} task {query_id:?}, found split record for a spend, accumulated and sending them as a single record"); let accumulated_spends = accumulated_spends .into_iter() - .take(2) + .take(MAX_DOUBLE_SPEND_ATTEMPTS_PER_RECORD) .collect::>(); let bytes = try_serialize_record(&accumulated_spends, RecordKind::Spend)?; diff --git a/sn_networking/src/lib.rs b/sn_networking/src/lib.rs index df13d6cdaf..9df8c14e1a 100644 --- a/sn_networking/src/lib.rs +++ b/sn_networking/src/lib.rs @@ -87,6 +87,9 @@ pub const CLOSE_GROUP_SIZE: usize = 5; /// that a replication of the record shall be sent/accepted to/by the peer. pub const REPLICATION_PEERS_COUNT: usize = CLOSE_GROUP_SIZE + 2; +/// The maximum number of double spend attempts to store inside a record. +pub const MAX_DOUBLE_SPEND_ATTEMPTS_PER_RECORD: usize = 50; + /// Majority of a given group (i.e. > 1/2). #[inline] pub const fn close_group_majority() -> usize { diff --git a/sn_networking/src/transfers.rs b/sn_networking/src/transfers.rs index 15e739a595..e03dcff456 100644 --- a/sn_networking/src/transfers.rs +++ b/sn_networking/src/transfers.rs @@ -236,21 +236,22 @@ pub fn get_signed_spend_from_record( address: &SpendAddress, record: &Record, ) -> Result { - match get_raw_signed_spends_from_record(record)?.as_slice() { - [one, two, ..] => { - warn!("Found double spend for {address:?}"); - Err(NetworkError::DoubleSpendAttempt( - Box::new(one.to_owned()), - Box::new(two.to_owned()), - )) + let spends = get_raw_signed_spends_from_record(record)?; + match spends.as_slice() { + [] => { + error!("Found no spend for {address:?}"); + Err(NetworkError::NoSpendFoundInsideRecord(*address)) } [one] => { trace!("Spend get for address: {address:?} successful"); Ok(one.clone()) } - _ => { - trace!("Found no spend for {address:?}"); - Err(NetworkError::NoSpendFoundInsideRecord(*address)) + _double_spends => { + warn!( + "Found double spend(s) of len {} for {address:?}", + spends.len() + ); + Err(NetworkError::DoubleSpendAttempt(spends)) } } } diff --git a/sn_node/src/put_validation.rs b/sn_node/src/put_validation.rs index 931c827526..14b9abd0bf 100644 --- a/sn_node/src/put_validation.rs +++ b/sn_node/src/put_validation.rs @@ -8,7 +8,10 @@ use crate::{node::Node, quote::verify_quote_for_storecost, Error, Marker, Result}; use libp2p::kad::{Record, RecordKey}; -use sn_networking::{get_raw_signed_spends_from_record, GetRecordError, NetworkError}; +use sn_networking::{ + get_raw_signed_spends_from_record, GetRecordError, NetworkError, + MAX_DOUBLE_SPEND_ATTEMPTS_PER_RECORD, +}; use sn_protocol::{ messages::CmdOk, storage::{ @@ -363,7 +366,7 @@ impl Node { // validate the signed spends against the network and the local knowledge debug!("Validating spends for {pretty_key:?} with unique key: {unique_pubkey:?}"); - let (spend1, maybe_spend2) = match self + let validated_spends = match self .signed_spends_to_keep(spends_for_key.clone(), *unique_pubkey) .await { @@ -373,12 +376,11 @@ impl Node { return Err(e); } }; - let validated_spends = maybe_spend2 - .clone() - .map(|spend2| vec![spend1.clone(), spend2]) - .unwrap_or_else(|| vec![spend1.clone()]); - let len = validated_spends.len(); - debug!("Got {len} validated spends with key: {unique_pubkey:?} at {pretty_key:?}"); + + debug!( + "Got {} validated spends with key: {unique_pubkey:?} at {pretty_key:?}", + validated_spends.len() + ); // store the record into the local storage let record = Record { @@ -393,12 +395,9 @@ impl Node { ); // report double spends - if let Some(spend2) = maybe_spend2 { - warn!("Got a double spend for the Spend PUT with unique_pubkey {unique_pubkey}"); - return Err(NetworkError::DoubleSpendAttempt( - Box::new(spend1), - Box::new(spend2), - ))?; + if validated_spends.len() > 1 { + warn!("Got double spend(s) of len {} for the Spend PUT with unique_pubkey {unique_pubkey}", validated_spends.len()); + return Err(NetworkError::DoubleSpendAttempt(validated_spends))?; } self.record_metrics(Marker::ValidSpendRecordPutFromNetwork(&pretty_key)); @@ -627,7 +626,7 @@ impl Node { &self, signed_spends: Vec, unique_pubkey: UniquePubkey, - ) -> Result<(SignedSpend, Option)> { + ) -> Result> { let spend_addr = SpendAddress::from_unique_pubkey(&unique_pubkey); debug!( "Validating before storing spend at {spend_addr:?} with unique key: {unique_pubkey}" @@ -687,28 +686,22 @@ impl Node { } } - // return the single unique spend to store - match all_verified_spends + let verified_spends = all_verified_spends .into_iter() - .collect::>() - .as_slice() - { - [a] => { - debug!("Got a single valid spend for {unique_pubkey:?}"); - Ok((a.to_owned(), None)) - } - [a, b] => { - warn!("Got a double spend for {unique_pubkey:?}"); - Ok((a.to_owned(), Some(b.to_owned()))) - } - _ => { - debug!( - "No valid spends found while validating Spend PUT. Who is sending us garbage?" - ); - Err(Error::InvalidRequest(format!( - "Found no valid spends while validating Spend PUT for {unique_pubkey:?}" - ))) - } + .take(MAX_DOUBLE_SPEND_ATTEMPTS_PER_RECORD) + .collect::>(); + + if verified_spends.is_empty() { + debug!("No valid spends found while validating Spend PUT. Who is sending us garbage?"); + Err(Error::InvalidRequest(format!( + "Found no valid spends while validating Spend PUT for {unique_pubkey:?}" + ))) + } else if verified_spends.len() > 1 { + warn!("Got a double spend for {unique_pubkey:?}"); + Ok(verified_spends) + } else { + debug!("Got a single valid spend for {unique_pubkey:?}"); + Ok(verified_spends) } } }