From f594eba3d9de02f29f5eee9d05e08e09673acbae Mon Sep 17 00:00:00 2001 From: Roland Sherwin Date: Mon, 25 Mar 2024 00:47:30 +0530 Subject: [PATCH] feat(transfers): enable client to check if a quote has expired --- sn_client/src/files.rs | 2 +- sn_client/src/folders.rs | 2 +- sn_client/src/register.rs | 3 +- sn_client/src/wallet.rs | 228 ++++++++++++++++++----- sn_node/src/error.rs | 6 +- sn_node/src/quote.rs | 14 +- sn_transfers/src/lib.rs | 4 +- sn_transfers/src/wallet.rs | 2 +- sn_transfers/src/wallet/data_payments.rs | 15 +- sn_transfers/src/wallet/error.rs | 3 + 10 files changed, 213 insertions(+), 66 deletions(-) diff --git a/sn_client/src/files.rs b/sn_client/src/files.rs index fc42f9467b..972147b084 100644 --- a/sn_client/src/files.rs +++ b/sn_client/src/files.rs @@ -126,7 +126,7 @@ impl FilesApi { trace!("Client upload started for chunk: {chunk_addr:?}"); let wallet_client = self.wallet()?; - let (payment, payee) = wallet_client.get_recent_payment_for_addr(&chunk_addr)?; + let (payment, payee) = wallet_client.get_non_expired_payment_for_addr(&chunk_addr)?; debug!( "{:?} payments for chunk: {chunk_addr:?} to {payee:?}: {payment:?}", diff --git a/sn_client/src/folders.rs b/sn_client/src/folders.rs index 408903b6cb..21feab02a6 100644 --- a/sn_client/src/folders.rs +++ b/sn_client/src/folders.rs @@ -187,7 +187,7 @@ impl FoldersApi { } } - let payment_info = wallet_client.get_recent_payment_for_addr(&self.as_net_addr())?; + let payment_info = wallet_client.get_non_expired_payment_for_addr(&self.as_net_addr())?; self.register .sync(&mut wallet_client, verify_store, Some(payment_info)) diff --git a/sn_client/src/register.rs b/sn_client/src/register.rs index d105c32bcf..6cc38c6690 100644 --- a/sn_client/src/register.rs +++ b/sn_client/src/register.rs @@ -531,7 +531,8 @@ impl ClientRegister { royalties_fees = payment_result.royalty_fees; // Get payment proofs needed to publish the Register - let (payment, payee) = wallet_client.get_recent_payment_for_addr(&net_addr)?; + let (payment, payee) = + wallet_client.get_non_expired_payment_for_addr(&net_addr)?; debug!("payments found: {payment:?}"); payment_info = Some((payment, payee)); } diff --git a/sn_client/src/wallet.rs b/sn_client/src/wallet.rs index 4eabdd1e72..a089ca82e1 100644 --- a/sn_client/src/wallet.rs +++ b/sn_client/src/wallet.rs @@ -135,7 +135,9 @@ impl WalletClient { self.wallet.unconfirmed_spend_requests() } - /// Returns the Cached Payment for a provided NetworkAddress. + /// Returns the most recent cached Payment for a provided NetworkAddress. This function does not check if the + /// quote has expired or not. Use get_non_expired_payment_for_addr if you want to get a non expired one. + /// /// If multiple payments have been made to the same address, then we pick the last one as it is the most recent. /// /// # Arguments @@ -167,29 +169,94 @@ impl WalletClient { &self, address: &NetworkAddress, ) -> WalletResult<(Payment, PeerId)> { - match &address.as_xorname() { - Some(xorname) => { - let payment_details = self - .wallet - .get_recent_cached_payment_for_xorname(xorname) - .ok_or(WalletError::NoPaymentForAddress(*xorname))?; - let payment = payment_details.to_payment(); - debug!("Payment retrieved for {xorname:?} from wallet: {payment:?}"); - info!("Payment retrieved for {xorname:?} from wallet"); - let peer_id = PeerId::from_bytes(&payment_details.peer_id_bytes) - .map_err(|_| WalletError::NoPaymentForAddress(*xorname))?; - - Ok((payment, peer_id)) + let xorname = address + .as_xorname() + .ok_or(WalletError::InvalidAddressType)?; + let mut payment_details = self + .wallet + .get_all_cached_payment_for_xorname(&xorname) + .ok_or(WalletError::NoPaymentForAddress(xorname))?; + + let payment_detail = payment_details + .pop() + .ok_or(WalletError::NoPaymentForAddress(xorname))?; + + let payment = payment_detail.to_payment(); + debug!("Payment retrieved for {xorname:?} from wallet: {payment:?}"); + info!("Payment retrieved for {xorname:?} from wallet"); + let peer_id = PeerId::from_bytes(&payment_detail.peer_id_bytes) + .map_err(|_| WalletError::NoPaymentForAddress(xorname))?; + + Ok((payment, peer_id)) + } + + /// Returns the most recent non expired cached Payment for a provided NetworkAddress. + /// + /// # Arguments + /// * `address` - The [`NetworkAddress`]. + /// + /// # Example + /// ```no_run + /// // Getting the payment for an address using a random PeerId + /// # use sn_client::{Client, WalletClient, Error}; + /// # use tempfile::TempDir; + /// # use bls::SecretKey; + /// # use sn_transfers::{HotWallet, MainSecretKey}; + /// # #[tokio::main] + /// # async fn main() -> Result<(),Error>{ + /// # use std::io::Bytes; + /// # let client = Client::new(SecretKey::random(), None, false, None, None).await?; + /// # let tmp_path = TempDir::new()?.path().to_owned(); + /// # let mut wallet = HotWallet::load_from_path(&tmp_path,Some(MainSecretKey::new(SecretKey::random())))?; + /// use libp2p_identity::PeerId; + /// use sn_protocol::NetworkAddress; + /// + /// let mut wallet_client = WalletClient::new(client, wallet); + /// let network_address = NetworkAddress::from_peer(PeerId::random()); + /// let payment = wallet_client.get_non_expired_payment_for_addr(&network_address)?; + /// # Ok(()) + /// # } + /// ``` + pub fn get_non_expired_payment_for_addr( + &self, + address: &NetworkAddress, + ) -> WalletResult<(Payment, PeerId)> { + let xorname = address + .as_xorname() + .ok_or(WalletError::InvalidAddressType)?; + let mut payment_details = self + .wallet + .get_all_cached_payment_for_xorname(&xorname) + .ok_or(WalletError::NoPaymentForAddress(xorname))?; + + // find a non expired quote + let payment_detail = loop { + if let Some(payment_detail) = payment_details.pop() { + if payment_detail.quote.has_expired() { + continue; + } else { + break payment_detail; + } + } else { + return Err(WalletError::QuoteExpired(xorname)); } - None => Err(WalletError::InvalidAddressType), - } + }; + + let payment = payment_detail.to_payment(); + debug!("Payment retrieved for {xorname:?} from wallet: {payment:?}"); + info!("Payment retrieved for {xorname:?} from wallet"); + let peer_id = PeerId::from_bytes(&payment_detail.peer_id_bytes) + .map_err(|_| WalletError::NoPaymentForAddress(xorname))?; + + Ok((payment, peer_id)) } /// Returns the all cached Payment for a provided NetworkAddress. /// /// # Arguments - /// # Arguments /// * `address` - The [`NetworkAddress`]. + /// * `non_expired` - If set to true, we return all the payments that have not expired. An error is returned if + /// all the payments have expired. /// /// # Example /// ```no_run @@ -217,35 +284,106 @@ impl WalletClient { &self, address: &NetworkAddress, ) -> WalletResult> { - match &address.as_xorname() { - Some(xorname) => { - let payment_details = self - .wallet - .get_all_cached_payment_for_xorname(xorname) - .ok_or(WalletError::NoPaymentForAddress(*xorname))?; - let payments = payment_details - .into_iter() - .map(|details| { - let payment = details.to_payment(); - match PeerId::from_bytes(&details.peer_id_bytes) { - Ok(peer_id) => Ok((payment, peer_id)), - Err(_) => Err(WalletError::NoPaymentForAddress(*xorname)), - } - }) - .collect::>>()?; - debug!( - "{} Payment retrieved for {xorname:?} from wallet: {payments:?}", - payments.len() - ); - info!( - "{} Payment retrieved for {xorname:?} from wallet", - payments.len() - ); - - Ok(payments) - } - None => Err(WalletError::InvalidAddressType), + let xorname = address + .as_xorname() + .ok_or(WalletError::InvalidAddressType)?; + let payment_details = self + .wallet + .get_all_cached_payment_for_xorname(&xorname) + .ok_or(WalletError::NoPaymentForAddress(xorname))?; + + let payments = payment_details + .into_iter() + .map(|details| { + let payment = details.to_payment(); + + match PeerId::from_bytes(&details.peer_id_bytes) { + Ok(peer_id) => Ok((payment, peer_id)), + Err(_) => Err(WalletError::NoPaymentForAddress(xorname)), + } + }) + .collect::>>()?; + + debug!( + "{} Payment retrieved for {xorname:?} from wallet: {payments:?}", + payments.len() + ); + info!( + "{} Payment retrieved for {xorname:?} from wallet", + payments.len() + ); + + Ok(payments) + } + + /// Returns the all cached Payment for a provided NetworkAddress that have not expired yet. + /// + /// # Arguments + /// * `address` - The [`NetworkAddress`]. + /// + /// # Example + /// ```no_run + /// // Getting the payment for an address using a random PeerId + /// # use sn_client::{Client, WalletClient, Error}; + /// # use tempfile::TempDir; + /// # use bls::SecretKey; + /// # use sn_transfers::{HotWallet, MainSecretKey}; + /// # #[tokio::main] + /// # async fn main() -> Result<(),Error>{ + /// # use std::io::Bytes; + /// # let client = Client::new(SecretKey::random(), None, false, None, None).await?; + /// # let tmp_path = TempDir::new()?.path().to_owned(); + /// # let mut wallet = HotWallet::load_from_path(&tmp_path,Some(MainSecretKey::new(SecretKey::random())))?; + /// use libp2p_identity::PeerId; + /// use sn_protocol::NetworkAddress; + /// + /// let mut wallet_client = WalletClient::new(client, wallet); + /// let network_address = NetworkAddress::from_peer(PeerId::random()); + /// let payments = wallet_client.get_all_non_expired_payments_for_addr(&network_address)?; + /// # Ok(()) + /// # } + /// ``` + pub fn get_all_non_expired_payments_for_addr( + &self, + address: &NetworkAddress, + ) -> WalletResult> { + let xorname = address + .as_xorname() + .ok_or(WalletError::InvalidAddressType)?; + let payment_details = self + .wallet + .get_all_cached_payment_for_xorname(&xorname) + .ok_or(WalletError::NoPaymentForAddress(xorname))?; + + let payments = payment_details + .into_iter() + .filter_map(|details| { + if details.quote.has_expired() { + return None; + } + let payment = details.to_payment(); + + match PeerId::from_bytes(&details.peer_id_bytes) { + Ok(peer_id) => Some(Ok((payment, peer_id))), + Err(_) => Some(Err(WalletError::NoPaymentForAddress(xorname))), + } + }) + .collect::>>()?; + + if payments.is_empty() { + return Err(WalletError::QuoteExpired(xorname)); } + + debug!( + "{} Payment retrieved for {xorname:?} from wallet: {payments:?}", + payments.len() + ); + info!( + "{} Payment retrieved for {xorname:?} from wallet", + payments.len() + ); + + Ok(payments) } /// Remove the payment for a given network address from disk. diff --git a/sn_node/src/error.rs b/sn_node/src/error.rs index 77f4e463f3..d7bc2d09ab 100644 --- a/sn_node/src/error.rs +++ b/sn_node/src/error.rs @@ -6,7 +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 sn_protocol::PrettyPrintRecordKey; +use sn_protocol::{NetworkAddress, PrettyPrintRecordKey}; use sn_transfers::{NanoTokens, WalletError}; use thiserror::Error; @@ -55,8 +55,8 @@ pub enum Error { InvalidQuoteContent, #[error("The payment quote's signature is invalid")] InvalidQuoteSignature, - #[error("The payment quote expired")] - QuoteExpired, + #[error("The payment quote expired for {0:?}")] + QuoteExpired(NetworkAddress), /// Payment proof received has no inputs #[error( "Payment proof received with record:{0:?}. No payment for our node in its transaction" diff --git a/sn_node/src/quote.rs b/sn_node/src/quote.rs index 0933b9f7ca..2ab47962a3 100644 --- a/sn_node/src/quote.rs +++ b/sn_node/src/quote.rs @@ -11,9 +11,6 @@ use sn_networking::Network; use sn_protocol::{error::Error as ProtocolError, NetworkAddress}; use sn_transfers::{NanoTokens, PaymentQuote}; -/// The time in seconds that a quote is valid for -const QUOTE_EXPIRATION_SECS: u64 = 3600; - impl Node { pub(crate) fn create_quote_for_storecost( network: &Network, @@ -51,14 +48,9 @@ impl Node { return Err(Error::InvalidQuoteContent); } - // check time - let now = std::time::SystemTime::now(); - let dur_s = match now.duration_since(quote.timestamp) { - Ok(t) => t.as_secs(), - Err(_) => return Err(Error::InvalidQuoteContent), - }; - if dur_s > QUOTE_EXPIRATION_SECS { - return Err(Error::QuoteExpired); + // check if the quote has expired + if quote.has_expired() { + return Err(Error::QuoteExpired(address.clone())); } // check sig diff --git a/sn_transfers/src/lib.rs b/sn_transfers/src/lib.rs index 95b7f3e953..12893a3921 100644 --- a/sn_transfers/src/lib.rs +++ b/sn_transfers/src/lib.rs @@ -31,9 +31,9 @@ pub use genesis::{ get_faucet_data_dir, is_genesis_parent_tx, is_genesis_spend, load_genesis_wallet, Error as GenesisError, GENESIS_CASHNOTE, GENESIS_CASHNOTE_SK, NETWORK_ROYALTIES_PK, }; -pub use wallet::bls_secret_from_hex; pub use wallet::{ - Error as WalletError, HotWallet, Payment, PaymentQuote, Result as WalletResult, WatchOnlyWallet, + bls_secret_from_hex, Error as WalletError, HotWallet, Payment, PaymentQuote, + Result as WalletResult, WatchOnlyWallet, QUOTE_EXPIRATION_SECS, }; // re-export crates used in our public API diff --git a/sn_transfers/src/wallet.rs b/sn_transfers/src/wallet.rs index 16afd2cc1f..ace8f3a180 100644 --- a/sn_transfers/src/wallet.rs +++ b/sn_transfers/src/wallet.rs @@ -63,7 +63,7 @@ use crate::{NanoTokens, UniquePubkey}; use wallet_file::wallet_file_name; pub use self::{ - data_payments::{Payment, PaymentQuote}, + data_payments::{Payment, PaymentQuote, QUOTE_EXPIRATION_SECS}, error::{Error, Result}, hot_wallet::HotWallet, keys::bls_secret_from_hex, diff --git a/sn_transfers/src/wallet/data_payments.rs b/sn_transfers/src/wallet/data_payments.rs index c316ec5fff..f2533905bb 100644 --- a/sn_transfers/src/wallet/data_payments.rs +++ b/sn_transfers/src/wallet/data_payments.rs @@ -6,11 +6,13 @@ // 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::{MainPubkey, NanoTokens, Transfer}; use serde::{Deserialize, Serialize}; use std::time::SystemTime; use xor_name::XorName; -use crate::{MainPubkey, NanoTokens, Transfer}; +/// The time in seconds that a quote is valid for +pub const QUOTE_EXPIRATION_SECS: u64 = 3600; #[derive(Clone, Serialize, Deserialize, Eq, PartialEq, custom_debug::Debug)] pub struct Payment { @@ -94,6 +96,17 @@ impl PaymentQuote { bytes } + /// Returns true) if the quote has not yet expired + pub fn has_expired(&self) -> bool { + let now = std::time::SystemTime::now(); + + let dur_s = match now.duration_since(self.timestamp) { + Ok(dur) => dur.as_secs(), + Err(_) => return true, + }; + dur_s > QUOTE_EXPIRATION_SECS + } + /// test utility to create a dummy quote pub fn test_dummy(xorname: XorName, cost: NanoTokens) -> Self { Self { diff --git a/sn_transfers/src/wallet/error.rs b/sn_transfers/src/wallet/error.rs index 36d98267af..b9c341bef6 100644 --- a/sn_transfers/src/wallet/error.rs +++ b/sn_transfers/src/wallet/error.rs @@ -67,6 +67,9 @@ pub enum Error { /// No cached payment found for address #[error("No ongoing payment found for address {0:?}")] NoPaymentForAddress(XorName), + /// The payment Quote has expired. + #[error("The payment quote made for {0:?} has expired")] + QuoteExpired(XorName), /// DAG error #[error("DAG error: {0}")]