diff --git a/directory.toml b/directory.toml index 49112ba9..5d4cffe3 100644 --- a/directory.toml +++ b/directory.toml @@ -1,4 +1,8 @@ -[directory_config] +# Listening port port = 8080 +# Socks port socks_port = 19060 -connection_type = "tor" \ No newline at end of file +# Connection type +connection_type = TOR +# RPC listening port +rpc_port = 4321 \ No newline at end of file diff --git a/maker.toml b/maker.toml index ef34f8ba..b3505a7c 100644 --- a/maker.toml +++ b/maker.toml @@ -1,29 +1,16 @@ -[maker_config] # Listening port port = 6102 -# Time interval between connection checks -heart_beat_interval_secs = 3 -# Time interval to ping the RPC backend -rpc_ping_interval_secs = 60 -# Time interval to ping directory server -# 12 hours = 60*60*12 = 43200 -directory_servers_refresh_interval_secs = 43200 -# Time interval to close a connection if no response is received -idle_connection_timeout = 300 -# Absolute coinswap fee -absolute_fee_sats = 1000 -# Fee rate per swap amount in ppb. -amount_relative_fee_ppb = 10000000 -# Fee rate for timelocked contract in ppb -time_relative_fee_ppb = 100000 -# No of confirmation required for funding transaction -required_confirms = 1 -# Minimum timelock difference between contract transaction of two hops -min_contract_reaction_time = 48 -# Minimum coinswap amount size in sats -min_size = 10000 +# RPC listening port +rpc_port +# Minimum Coinswap amount +min_swap_amount = 100000 # Socks port -socks_part = 19050 -# Directory server onion address -directory_server_onion_address = "directoryhiddenserviceaddress.onion:8080" -connection_type = "tor" \ No newline at end of file +socks_port = 19050 +# Directory server address +directory_server_address = 127.0.0.1:8080 +# Fidelity Bond amount +fidelity_amount = 5000000 +# Fidelity Bond timelock in Block heights +fidelity_timelock = 26000 +# Connection type +connection_type = TOR \ No newline at end of file diff --git a/src/bin/directoryd.rs b/src/bin/directoryd.rs index 26a5f47b..5cf26de3 100644 --- a/src/bin/directoryd.rs +++ b/src/bin/directoryd.rs @@ -14,9 +14,6 @@ use std::{path::PathBuf, str::FromStr, sync::Arc}; #[clap(version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] struct Cli { - /// Optional network type. - #[clap(long, short = 'n', default_value = "tor", possible_values = &["tor", "clearnet"])] - network: String, /// Optional DNS data directory. Default value : "~/.coinswap/dns" #[clap(long, short = 'd')] data_directory: Option, @@ -53,8 +50,6 @@ fn main() -> Result<(), DirectoryServerError> { let rpc_network = bitcoin::Network::from_str(&args.rpc_network).unwrap(); - let conn_type = ConnectionType::from_str(&args.network)?; - let rpc_config = RPCConfig { url: args.rpc, auth: Auth::UserPass(args.auth.0, args.auth.1), @@ -62,12 +57,15 @@ fn main() -> Result<(), DirectoryServerError> { wallet_name: "random".to_string(), // we can put anything here as it will get updated in the init. }; + let conn_type = ConnectionType::TOR; + #[cfg(feature = "tor")] { if conn_type == ConnectionType::TOR { setup_mitosis(); } } + let directory = Arc::new(DirectoryServer::new(args.data_directory, Some(conn_type))?); start_directory_server(directory, Some(rpc_config))?; diff --git a/src/bin/makerd.rs b/src/bin/makerd.rs index 0d067e08..160f0c03 100644 --- a/src/bin/makerd.rs +++ b/src/bin/makerd.rs @@ -17,9 +17,6 @@ use coinswap::tor::setup_mitosis; #[clap(version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] struct Cli { - /// Optional Connection Network Type - #[clap(long, default_value = "tor", possible_values = &["tor", "clearnet"])] - network: String, /// Optional DNS data directory. Default value : "~/.coinswap/maker" #[clap(long, short = 'd')] data_directory: Option, @@ -60,8 +57,6 @@ fn main() -> Result<(), MakerError> { let rpc_network = bitcoin::Network::from_str(&args.rpc_network).unwrap(); - let conn_type = ConnectionType::from_str(&args.network)?; - let rpc_config = RPCConfig { url: args.rpc, auth: Auth::UserPass(args.auth.0, args.auth.1), @@ -69,6 +64,8 @@ fn main() -> Result<(), MakerError> { wallet_name: "random".to_string(), // we can put anything here as it will get updated in the init. }; + let conn_type = ConnectionType::TOR; + #[cfg(feature = "tor")] { if conn_type == ConnectionType::TOR { diff --git a/src/bin/taker.rs b/src/bin/taker.rs index 89f08ed1..c89d80bf 100644 --- a/src/bin/taker.rs +++ b/src/bin/taker.rs @@ -3,7 +3,7 @@ use bitcoind::bitcoincore_rpc::{json::ListUnspentResultEntry, Auth}; use clap::Parser; use coinswap::{ taker::{error::TakerError, SwapParams, Taker, TakerBehavior}, - utill::{parse_proxy_auth, setup_taker_logger, ConnectionType}, + utill::{parse_proxy_auth, setup_taker_logger, ConnectionType, REQUIRED_CONFIRMS}, wallet::{Destination, RPCConfig, SendAmount}, }; use log::LevelFilter; @@ -14,9 +14,6 @@ use std::{path::PathBuf, str::FromStr}; #[clap(version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] struct Cli { - /// Optional Connection Network Type - #[clap(long, default_value = "clearnet",short= 'c', possible_values = &["tor","clearnet"])] - connection_type: String, /// Optional DNS data directory. Default value : "~/.coinswap/taker" #[clap(long, short = 'd')] data_directory: Option, @@ -54,9 +51,6 @@ struct Cli { /// Sets the transaction count. #[clap(name = "tx_count", default_value = "3")] pub tx_count: u32, - /// Sets the required on-chain confirmations. - #[clap(name = "required_confirms", default_value = "1000")] - pub required_confirms: u64, /// List of sub commands to process various endpoints of taker cli app. #[clap(subcommand)] command: Commands, @@ -101,7 +95,8 @@ fn main() -> Result<(), TakerError> { let args = Cli::parse(); let rpc_network = bitcoin::Network::from_str(&args.bitcoin_network).unwrap(); - let connection_type = ConnectionType::from_str(&args.connection_type)?; + + let connection_type = ConnectionType::TOR; let rpc_config = RPCConfig { url: args.rpc, @@ -114,7 +109,7 @@ fn main() -> Result<(), TakerError> { send_amount: Amount::from_sat(args.send_amount), maker_count: args.maker_count, tx_count: args.tx_count, - required_confirms: args.required_confirms, + required_confirms: REQUIRED_CONFIRMS, }; let mut taker = Taker::init( diff --git a/src/maker/api.rs b/src/maker/api.rs index 17afc0e2..8e900f19 100644 --- a/src/maker/api.rs +++ b/src/maker/api.rs @@ -12,7 +12,10 @@ use crate::{ messages::{FidelityProof, ReqContractSigsForSender}, Hash160, }, - utill::{get_maker_dir, redeemscript_to_scriptpubkey, ConnectionType}, + utill::{ + get_maker_dir, redeemscript_to_scriptpubkey, ConnectionType, HEART_BEAT_INTERVAL, + REQUIRED_CONFIRMS, + }, wallet::{RPCConfig, SwapCoin, WalletSwapCoin}, }; use bitcoin::{ @@ -46,9 +49,60 @@ use crate::{ use super::{config::MakerConfig, error::MakerError}; -use crate::maker::server::{ - HEART_BEAT_INTERVAL_SECS, MIN_CONTRACT_REACTION_TIME, REQUIRED_CONFIRMS, -}; +/// Interval for health checks on a stable RPC connection with bitcoind. +pub const RPC_PING_INTERVAL: Duration = Duration::from_secs(60); + +// Currently we don't refresh address at DNS. The Maker only post it once at startup. +// If the address record gets deleted, or the DNS gets blasted, the Maker won't know. +// TODO: Make the maker repost their address to DNS once a day in spawned thread. +// pub const DIRECTORY_SERVERS_REFRESH_INTERVAL_SECS: u64 = Duartion::from_days(1); // Once a day. + +/// Maker triggers the recovery mechanism, if Taker is idle for more than 300 secs. +pub const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(300); + +/// The minimum difference in locktime (in blocks) between the incoming and outgoing swaps. +/// +/// This value specifies the reaction time, in blocks, available to a Maker +/// to claim the refund transaction in case of recovery. +/// +/// According to [BOLT #2](https://github.com/lightning/bolts/blob/aa5207aeaa32d841353dd2df3ce725a4046d528d/02-peer-protocol.md?plain=1#L1798), +/// the estimated minimum `cltv_expiry_delta` is 18 blocks. +/// To enhance safety, the default value is set to 20 blocks. +pub const MIN_CONTRACT_REACTION_TIME: u16 = 20; + +/// # Fee Parameters for Coinswap +/// +/// These parameters define the fees charged by Makers in a coinswap transaction. +/// +/// TODO: These parameters are currently hardcoded. Consider making them configurable for Makers in the future. +///p +/// - `BASE_FEE`: A fixed base fee charged by the Maker for providing its services +/// - `AMOUNT_RELATIVE_FEE_PCT`: A percentage fee based on the swap amount. +/// - `TIME_RELATIVE_FEE_PCT`: A percentage fee based on the refund locktime (duration the Maker must wait for a refund). +/// +/// The coinswap fee increases with both the swap amount and the refund locktime. +/// Refer to `REFUND_LOCKTIME` and `REFUND_LOCKTIME_STEP` in `taker::api.rs` for related parameters. +/// +/// ### Fee Calculation +/// The total fee for a swap is calculated as: +/// `total_fee = base_fee + (swap_amount * amount_relative_fee_pct) / 100 + (swap_amount * refund_locktime * time_relative_fee_pct) / 100` +/// +/// ### Example (Default Values) +/// For a swap amount of 100,000 sats and a refund locktime of 20 blocks: +/// - `base_fee` = 1,000 sats +/// - `amount_relative_fee` = (100,000 * 2.5) / 100 = 2,500 sats +/// - `time_relative_fee` = (100,000 * 20 * 0.1) / 100 = 2,000 sats +/// - `total_fee` = 5,500 sats (5.5%) +/// +/// Fee rates are designed to asymptotically approach 5% of the swap amount as the swap amount increases.. +pub const BASE_FEE: u64 = 1000; +pub const AMOUNT_RELATIVE_FEE_PCT: f64 = 2.50; +pub const TIME_RELATIVE_FEE_PCT: f64 = 0.10; + +/// Minimum Coinswap amount; makers will not accept amounts below this. +pub const MIN_SWAP_AMOUNT: u64 = 100000; + +// What's the use of RefundLocktimeStep? /// Used to configure the maker for testing purposes. #[derive(Debug, Clone, Copy)] @@ -278,7 +332,7 @@ impl Maker { // check that the new locktime is sufficently short enough compared to the // locktime in the provided funding tx let locktime = read_contract_locktime(&funding_info.contract_redeemscript)?; - if locktime - message.next_locktime < MIN_CONTRACT_REACTION_TIME { + if locktime - message.refund_locktime < MIN_CONTRACT_REACTION_TIME { return Err(MakerError::General( "Next hop locktime too close to current hop locktime", )); @@ -298,7 +352,7 @@ impl Maker { ) .map_err(WalletError::Rpc)? { - if txout.confirmations < (REQUIRED_CONFIRMS as u32) { + if txout.confirmations < REQUIRED_CONFIRMS { return Err(MakerError::General( "funding tx not confirmed to required depth", )); @@ -525,7 +579,7 @@ pub fn check_for_broadcasted_contracts(maker: Arc) -> Result<(), MakerErr } } // All locks are cleared here. - std::thread::sleep(Duration::from_secs(HEART_BEAT_INTERVAL_SECS)); + std::thread::sleep(HEART_BEAT_INTERVAL); } Ok(()) @@ -537,6 +591,13 @@ pub fn check_for_broadcasted_contracts(maker: Arc) -> Result<(), MakerErr /// Broadcast the contract transactions and claim funds via timelock. pub fn check_for_idle_states(maker: Arc) -> Result<(), MakerError> { let mut bad_ip = Vec::new(); + + let conn_timeout = if cfg!(feature = "integration-test") { + Duration::from_secs(60) + } else { + IDLE_CONNECTION_TIMEOUT + }; + loop { if maker.shutdown.load(Relaxed) { break; @@ -558,7 +619,7 @@ pub fn check_for_idle_states(maker: Arc) -> Result<(), MakerError> { ip, no_response_since ); - if no_response_since > std::time::Duration::from_secs(60) { + if no_response_since > conn_timeout { log::error!( "[{}] Potential Dropped Connection from {}", maker.config.port, @@ -610,7 +671,7 @@ pub fn check_for_idle_states(maker: Arc) -> Result<(), MakerError> { } } // All locks are cleared here - std::thread::sleep(Duration::from_secs(HEART_BEAT_INTERVAL_SECS)); + std::thread::sleep(HEART_BEAT_INTERVAL); } Ok(()) diff --git a/src/maker/config.rs b/src/maker/config.rs index 7e8ed124..c4486a64 100644 --- a/src/maker/config.rs +++ b/src/maker/config.rs @@ -3,11 +3,12 @@ use crate::utill::parse_toml; use std::{io, path::Path}; -use bitcoin::Amount; use std::io::Write; use crate::utill::{get_maker_dir, parse_field, ConnectionType}; +use super::api::MIN_SWAP_AMOUNT; + /// Maker Configuration, controlling various maker behavior. #[derive(Debug, Clone, PartialEq)] pub struct MakerConfig { @@ -15,19 +16,14 @@ pub struct MakerConfig { pub port: u16, /// RPC listening port pub rpc_port: u16, - /// Absolute coinswap fee - pub absolute_fee_sats: Amount, - /// Fee rate for timelocked contract in parts per billion (PPB). - /// Similar to `DEFAULT_AMOUNT_RELATIVE_FEE_PPB`, calculated as (amount * fee_ppb) / 1_000_000_000. - pub time_relative_fee_ppb: Amount, - /// Minimum timelock difference between contract transaction of two hops - pub min_size: u64, + /// Minimum Coinswap amount + pub min_swap_amount: u64, /// Socks port pub socks_port: u16, /// Directory server address (can be clearnet or onion) pub directory_server_address: String, - /// Fidelity Bond Value - pub fidelity_value: u64, + /// Fidelity Bond amount + pub fidelity_amount: u64, /// Fidelity Bond timelock in Block heights. pub fidelity_timelock: u32, /// Connection type @@ -39,13 +35,11 @@ impl Default for MakerConfig { Self { port: 6102, rpc_port: 6103, - absolute_fee_sats: Amount::from_sat(1000), - time_relative_fee_ppb: Amount::from_sat(100_000), - min_size: 10_000, + min_swap_amount: MIN_SWAP_AMOUNT, socks_port: 19050, directory_server_address: "127.0.0.1:8080".to_string(), - fidelity_value: 5_000_000, // 5 million sats - fidelity_timelock: 26_000, // Approx 6 months of blocks + fidelity_amount: 5_000_000, // 5 million sats + fidelity_timelock: 26_000, // Approx 6 months of blocks connection_type: { #[cfg(feature = "tor")] { @@ -97,24 +91,18 @@ impl MakerConfig { Ok(MakerConfig { port: parse_field(config_map.get("port"), default_config.port), rpc_port: parse_field(config_map.get("rpc_port"), default_config.rpc_port), - absolute_fee_sats: parse_field( - config_map.get("absolute_fee_sats"), - default_config.absolute_fee_sats, - ), - time_relative_fee_ppb: parse_field( - config_map.get("time_relative_fee_ppb"), - default_config.time_relative_fee_ppb, + min_swap_amount: parse_field( + config_map.get("min_swap_amount"), + default_config.min_swap_amount, ), - min_size: parse_field(config_map.get("min_size"), default_config.min_size), socks_port: parse_field(config_map.get("socks_port"), default_config.socks_port), directory_server_address: parse_field( config_map.get("directory_server_address"), default_config.directory_server_address, ), - - fidelity_value: parse_field( - config_map.get("fidelity_value"), - default_config.fidelity_value, + fidelity_amount: parse_field( + config_map.get("fidelity_amount"), + default_config.fidelity_amount, ), fidelity_timelock: parse_field( config_map.get("fidelity_timelock"), @@ -132,22 +120,18 @@ impl MakerConfig { let toml_data = format!( "port = {} rpc_port = {} -absolute_fee_sats = {} -time_relative_fee_ppb = {} -min_size = {} +min_swap_amount = {} socks_port = {} directory_server_address = {} -fidelity_value = {} +fidelity_amount = {} fidelity_timelock = {} connection_type = {:?}", self.port, self.rpc_port, - self.absolute_fee_sats, - self.time_relative_fee_ppb, - self.min_size, + self.min_swap_amount, self.socks_port, self.directory_server_address, - self.fidelity_value, + self.fidelity_amount, self.fidelity_timelock, self.connection_type, ); @@ -187,12 +171,8 @@ mod tests { [maker_config] port = 6102 rpc_port = 6103 - absolute_fee_sats = 1000 - amount_relative_fee_ppb = 10000000 - time_relative_fee_ppb = 100000 required_confirms = 1 - min_contract_reaction_time = 48 - min_size = 10000 + min_swap_amount = 100000 socks_port = 19050 "#; let config_path = create_temp_config(contents, "valid_maker_config.toml"); diff --git a/src/maker/handlers.rs b/src/maker/handlers.rs index dc6b32ff..425805fe 100644 --- a/src/maker/handlers.rs +++ b/src/maker/handlers.rs @@ -16,40 +16,33 @@ use bitcoin::{ }; use bitcoind::bitcoincore_rpc::RpcApi; -use crate::{ - maker::api::recover_from_swap, - protocol::{ - error::ProtocolError, - messages::{MakerHello, MultisigPrivkey, PrivKeyHandover}, - Hash160, +use super::{ + api::{ + recover_from_swap, ConnectionState, ExpectedMessage, Maker, MakerBehavior, + AMOUNT_RELATIVE_FEE_PCT, BASE_FEE, MIN_CONTRACT_REACTION_TIME, TIME_RELATIVE_FEE_PCT, }, - wallet::{WalletError, WalletSwapCoin}, + error::MakerError, }; use crate::{ - maker::{ - api::{ConnectionState, ExpectedMessage, Maker, MakerBehavior}, - error::MakerError, - }, protocol::{ contract::{ calculate_coinswap_fee, create_receivers_contract_tx, find_funding_output_index, - read_contract_locktime, read_hashvalue_from_contract, - read_pubkeys_from_multisig_redeemscript, FUNDING_TX_VBYTE_SIZE, + read_hashvalue_from_contract, read_pubkeys_from_multisig_redeemscript, }, + error::ProtocolError, messages::{ ContractSigsAsRecvrAndSender, ContractSigsForRecvr, ContractSigsForRecvrAndSender, - ContractSigsForSender, HashPreimage, MakerToTakerMessage, Offer, ProofOfFunding, - ReqContractSigsForRecvr, ReqContractSigsForSender, SenderContractTxInfo, - TakerToMakerMessage, + ContractSigsForSender, HashPreimage, MakerHello, MakerToTakerMessage, MultisigPrivkey, + Offer, PrivKeyHandover, ProofOfFunding, ReqContractSigsForRecvr, + ReqContractSigsForSender, SenderContractTxInfo, TakerToMakerMessage, }, + Hash160, }, - wallet::{IncomingSwapCoin, SwapCoin}, + utill::REQUIRED_CONFIRMS, + wallet::{IncomingSwapCoin, SwapCoin, WalletError, WalletSwapCoin}, }; -use crate::maker::server::{ - AMOUNT_RELATIVE_FEE_PPB, MIN_CONTRACT_REACTION_TIME, REQUIRED_CONFIRMS, -}; /// The Global Handle Message function. Takes in a [`Arc`] and handle messages /// according to a [ConnectionState]. pub fn handle_message( @@ -96,13 +89,13 @@ pub fn handle_message( let fidelity = maker.highest_fidelity_proof.read()?; let fidelity = fidelity.as_ref().expect("proof expected"); Some(MakerToTakerMessage::RespOffer(Box::new(Offer { - absolute_fee_sat: maker.config.absolute_fee_sats, - amount_relative_fee_ppb: AMOUNT_RELATIVE_FEE_PPB, - time_relative_fee_ppb: maker.config.time_relative_fee_ppb, + base_fee: BASE_FEE, + amount_relative_fee_pct: AMOUNT_RELATIVE_FEE_PCT, + time_relative_fee_pct: TIME_RELATIVE_FEE_PCT, required_confirms: REQUIRED_CONFIRMS, minimum_locktime: MIN_CONTRACT_REACTION_TIME, max_size, - min_size: maker.config.min_size, + min_size: maker.config.min_swap_amount, tweakable_point, fidelity: fidelity.clone(), }))) @@ -241,7 +234,7 @@ impl Maker { acc + txinfo.funding_input_value.to_sat() }); - if total_funding_amount >= self.config.min_size + if total_funding_amount >= self.config.min_swap_amount && total_funding_amount < self.wallet.read()?.store.offer_maxsize { log::info!( @@ -300,7 +293,7 @@ impl Maker { }, funding_output.value, &funding_info.contract_redeemscript, - Amount::from_sat(message.next_fee_rate), + Amount::from_sat(message.contract_feerate), )?; let (tweakable_privkey, _) = self.wallet.read()?.get_tweakable_keypair()?; @@ -359,19 +352,32 @@ impl Maker { })?; let calc_coinswap_fees = calculate_coinswap_fee( - self.config.absolute_fee_sats, - AMOUNT_RELATIVE_FEE_PPB, - self.config.time_relative_fee_ppb, - Amount::from_sat(incoming_amount), - REQUIRED_CONFIRMS, //time_in_blocks just 1 for now + incoming_amount, + message.refund_locktime, + BASE_FEE, + AMOUNT_RELATIVE_FEE_PCT, + TIME_RELATIVE_FEE_PCT, ); - let calc_funding_tx_fees = (FUNDING_TX_VBYTE_SIZE - * message.next_fee_rate - * (message.next_coinswap_info.len() as u64)) - / 1000; - - let outgoing_amount = incoming_amount - calc_coinswap_fees - calc_funding_tx_fees; + // NOTE: The `contract_feerate` currently represents the hardcoded `MINER_FEE` of a transaction, not the fee rate. + // This will remain unchanged to avoid modifying the structure of the [ProofOfFunding] message. + // Once issue https://github.com/citadel-tech/coinswap/issues/309 is resolved, + //`contract_feerate` will represent the actual fee rate instead of the `MINER_FEE`. + let calc_funding_tx_fees = + message.contract_feerate * (message.next_coinswap_info.len() as u64); + + // Check for overflow. If happens hard error. + // This can happen if the fee_rate for funding tx is very high and incoming_amount is very low. + // TODO: Ensure at Taker protocol that this never happens. + let outgoing_amount = if let Some(a) = + incoming_amount.checked_sub(calc_coinswap_fees + calc_funding_tx_fees) + { + a + } else { + return Err(MakerError::General( + "Fatal Error! Total swap fee is more than the swap amount. Failing the swap.", + )); + }; // Create outgoing coinswap of the next hop let (my_funding_txes, outgoing_swapcoins, act_funding_txs_fees) = { @@ -388,12 +394,14 @@ impl Maker { .map(|next_hop| next_hop.next_hashlock_pubkey) .collect::>(), hashvalue, - message.next_locktime, - Amount::from_sat(message.next_fee_rate), + message.refund_locktime, + Amount::from_sat(message.contract_feerate), )? }; - let act_coinswap_fees = incoming_amount - outgoing_amount - act_funding_txs_fees.to_sat(); + let act_coinswap_fees = incoming_amount + .checked_sub(outgoing_amount + act_funding_txs_fees.to_sat()) + .expect("This should not overflow as we just above."); log::info!( "[{}] Outgoing Funding Txids: {:?}.", @@ -403,27 +411,15 @@ impl Maker { .map(|tx| tx.compute_txid()) .collect::>() ); - log::debug!( - "[{}] Outgoing Swapcoins: {:?}.", - self.config.port, - outgoing_swapcoins - ); log::info!( - "incoming_amount = {} | incoming_locktime = {} | outgoing_amount = {} | outgoing_locktime = {}", + "[{}] Incoming Swap Amount = {} | Outgoing Swap Amount = {} | Coinswap Fee = {} | Refund Tx locktime (blocks) = {} | Total Funding Tx Mining Fees = {} |", + self.config.port, Amount::from_sat(incoming_amount), - read_contract_locktime( - &message.confirmed_funding_txes[0].contract_redeemscript - )?, Amount::from_sat(outgoing_amount), - message.next_locktime - ); - log::info!( - "Calculated Funding Txs Fees = {} | Actual Funding Txs Fees = {} | Calculated Swap Revenue = {} | Actual Swap Revenue = {}", - Amount::from_sat(calc_funding_tx_fees), - act_funding_txs_fees, - Amount::from_sat(calc_coinswap_fees), - Amount::from_sat(act_coinswap_fees) + Amount::from_sat(act_coinswap_fees), + message.refund_locktime, + act_funding_txs_fees ); connection_state.pending_funding_txes = my_funding_txes; diff --git a/src/maker/rpc/server.rs b/src/maker/rpc/server.rs index 4a8e2a32..76ee4acf 100644 --- a/src/maker/rpc/server.rs +++ b/src/maker/rpc/server.rs @@ -9,15 +9,14 @@ use std::{ use bitcoin::{Address, Amount}; +use super::messages::RpcMsgReq; use crate::{ maker::{error::MakerError, rpc::messages::RpcMsgResp, Maker}, - utill::{read_message, send_message, ConnectionType}, + utill::{read_message, send_message, ConnectionType, HEART_BEAT_INTERVAL}, wallet::{Destination, SendAmount}, }; use std::str::FromStr; -use super::messages::RpcMsgReq; - fn handle_request(maker: &Arc, socket: &mut TcpStream) -> Result<(), MakerError> { let msg_bytes = read_message(socket)?; let rpc_request: RpcMsgReq = serde_cbor::from_slice(&msg_bytes)?; @@ -206,16 +205,14 @@ pub fn start_rpc_server(maker: Arc) -> Result<(), MakerError> { Err(e) => { if e.kind() == ErrorKind::WouldBlock { - sleep(Duration::from_secs(3)); - continue; + // do nothing } else { log::error!("Error accepting RPC connection: {:?}", e); - return Err(e.into()); } } } - sleep(Duration::from_secs(3)); + sleep(HEART_BEAT_INTERVAL); } Ok(()) diff --git a/src/maker/server.rs b/src/maker/server.rs index ee2054ee..3cf7af5c 100644 --- a/src/maker/server.rs +++ b/src/maker/server.rs @@ -30,7 +30,7 @@ use bitcoind::bitcoincore_rpc::RpcApi; #[cfg(feature = "tor")] use socks::Socks5Stream; -pub use super::Maker; +pub use super::{api::RPC_PING_INTERVAL, Maker}; use crate::{ error::NetError, @@ -40,7 +40,9 @@ use crate::{ rpc::start_rpc_server, }, protocol::messages::TakerToMakerMessage, - utill::{read_message, send_message, ConnectionType, DnsMetadata, DnsRequest}, + utill::{ + read_message, send_message, ConnectionType, DnsMetadata, DnsRequest, HEART_BEAT_INTERVAL, + }, wallet::WalletError, }; @@ -49,18 +51,6 @@ use crate::utill::monitor_log_for_completion; use crate::maker::error::MakerError; -// Default values for Maker configurations -pub const HEART_BEAT_INTERVAL_SECS: u64 = 3; -pub const RPC_PING_INTERVAL_SECS: u64 = 60; -pub const _DIRECTORY_SERVERS_REFRESH_INTERVAL_SECS: u64 = 60 * 60 * 12; // 12 Hours -pub const _IDLE_CONNECTION_TIMEOUT: u64 = 300; -pub const REQUIRED_CONFIRMS: u64 = 1; -pub const MIN_CONTRACT_REACTION_TIME: u16 = 48; - -/// Fee rate per swap amount in parts per billion (PPB). -/// E.g., for 1 billion sats (0.01 BTC), a value of 10_000 would result in a 0.1% fee. -pub const AMOUNT_RELATIVE_FEE_PPB: Amount = Amount::from_sat(10_000_000); - #[cfg(feature = "tor")] type OptionalJoinHandle = Option>; @@ -163,36 +153,29 @@ fn network_bootstrap(maker: Arc) -> Result<(String, OptionalJoinHandle), // Keep trying until send is successful. loop { - let mut stream = match maker.config.connection_type { - ConnectionType::CLEARNET => match TcpStream::connect(&dns_address) { - Ok(s) => s, + let mut stream = loop { + let conn_result = match maker.config.connection_type { + ConnectionType::CLEARNET => TcpStream::connect(&dns_address), + + #[cfg(feature = "tor")] + ConnectionType::TOR => Socks5Stream::connect( + format!("127.0.0.1:{}", maker.config.socks_port), + dns_address.as_str(), + ) + .map(|stream| stream.into_inner()), + }; + + match conn_result { + Ok(stream) => break stream, Err(e) => { log::warn!( "[{}] TCP connection error with directory, reattempting: {}", maker_port, e ); - thread::sleep(Duration::from_secs(HEART_BEAT_INTERVAL_SECS)); + thread::sleep(HEART_BEAT_INTERVAL); continue; } - }, - #[cfg(feature = "tor")] - ConnectionType::TOR => { - match Socks5Stream::connect( - format!("127.0.0.1:{}", maker.config.socks_port), - dns_address.as_str(), - ) { - Ok(s) => s.into_inner(), - Err(e) => { - log::warn!( - "[{}] TCP connection error with directory, reattempting: {}", - maker_port, - e - ); - thread::sleep(Duration::from_secs(HEART_BEAT_INTERVAL_SECS)); - continue; - } - } } }; @@ -202,9 +185,7 @@ fn network_bootstrap(maker: Arc) -> Result<(String, OptionalJoinHandle), maker_port, e ); - - // Wait before reattempting - std::thread::sleep(std::time::Duration::from_secs(HEART_BEAT_INTERVAL_SECS)); + thread::sleep(HEART_BEAT_INTERVAL); continue; }; @@ -230,7 +211,7 @@ fn setup_fidelity_bond(maker: &Arc, maker_address: &str) -> Result<(), Ma *proof = Some(highest_proof); } else { // No bond in the wallet. Lets attempt to create one. - let amount = Amount::from_sat(maker.config.fidelity_value); + let amount = Amount::from_sat(maker.config.fidelity_amount); let current_height = maker .get_wallet() .read()? @@ -321,10 +302,10 @@ fn check_connection_with_core( // If connection is live, keep tring at rpc_ping_interval (60 sec). match rpc_ping_success { true => { - sleep(Duration::from_secs(RPC_PING_INTERVAL_SECS)); + sleep(RPC_PING_INTERVAL); } false => { - sleep(Duration::from_secs(HEART_BEAT_INTERVAL_SECS)); + sleep(HEART_BEAT_INTERVAL); } } if let Err(e) = maker.wallet.read()?.rpc.get_blockchain_info() { @@ -438,8 +419,6 @@ pub fn start_maker_server(maker: Arc) -> Result<(), MakerError> { maker_address ); - let heart_beat_interval = HEART_BEAT_INTERVAL_SECS; // All maker internal threads loops at this frequency. - // Global server Mutex, to switch on/off p2p network. let accepting_clients = Arc::new(AtomicBool::new(false)); @@ -512,7 +491,7 @@ pub fn start_maker_server(maker: Arc) -> Result<(), MakerError> { maker.thread_pool.add_thread(rpc_thread); - sleep(Duration::from_secs(heart_beat_interval)); // wait for 1 beat, to complete spawns of all the threads. + sleep(HEART_BEAT_INTERVAL); // wait for 1 beat, to complete spawns of all the threads. maker.is_setup_complete.store(true, Relaxed); log::info!("[{}] Maker setup is ready", maker.config.port); } @@ -522,7 +501,6 @@ pub fn start_maker_server(maker: Arc) -> Result<(), MakerError> { // This loop beats at `maker.config.heart_beat_interval_secs` while !maker.shutdown.load(Relaxed) { let maker = maker.clone(); // This clone is needed to avoid moving the Arc in each iterations. - let heart_beat_interval = HEART_BEAT_INTERVAL_SECS; // Block client connections if accepting_client=false if !accepting_clients.load(Relaxed) { @@ -530,7 +508,7 @@ pub fn start_maker_server(maker: Arc) -> Result<(), MakerError> { "[{}] Temporary failure in backend node. Not accepting swap request. Check your node if this error persists", maker.config.port ); - sleep(Duration::from_secs(heart_beat_interval)); + sleep(HEART_BEAT_INTERVAL); continue; } @@ -562,7 +540,7 @@ pub fn start_maker_server(maker: Arc) -> Result<(), MakerError> { } }; - sleep(Duration::from_secs(heart_beat_interval)); + sleep(HEART_BEAT_INTERVAL); } log::info!("[{}] Maker is shutting down.", port); diff --git a/src/market/directory.rs b/src/market/directory.rs index b4b58525..7d951fd5 100644 --- a/src/market/directory.rs +++ b/src/market/directory.rs @@ -9,7 +9,7 @@ use crate::{ market::rpc::start_rpc_server_thread, utill::{ get_dns_dir, parse_field, parse_toml, read_message, send_message, verify_fidelity_checks, - ConnectionType, DnsRequest, + ConnectionType, DnsRequest, HEART_BEAT_INTERVAL, }, wallet::{RPCConfig, WalletError}, }; @@ -343,6 +343,7 @@ pub fn start_directory_server( let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, directory.port))?; + // why we have not set it to non-blocking mode? while !directory.shutdown.load(Relaxed) { match listener.accept() { Ok((mut stream, addrs)) => { @@ -358,7 +359,7 @@ pub fn start_directory_server( } } - sleep(Duration::from_secs(3)); + sleep(HEART_BEAT_INTERVAL); } log::info!("Shutdown signal received. Stopping directory server."); diff --git a/src/market/rpc/server.rs b/src/market/rpc/server.rs index 4097cf72..4850a183 100644 --- a/src/market/rpc/server.rs +++ b/src/market/rpc/server.rs @@ -2,7 +2,7 @@ use super::{RpcMsgReq, RpcMsgResp}; use crate::{ error::NetError, market::directory::{AddressEntry, DirectoryServer, DirectoryServerError}, - utill::{read_message, send_message}, + utill::{read_message, send_message, HEART_BEAT_INTERVAL}, }; use std::{ collections::BTreeSet, @@ -12,7 +12,6 @@ use std::{ thread::sleep, time::Duration, }; - fn handle_request( socket: &mut TcpStream, address: Arc>>, @@ -59,15 +58,15 @@ pub fn start_rpc_server_thread( } Err(e) => { if e.kind() == ErrorKind::WouldBlock { - sleep(Duration::from_secs(3)); - continue; + // do nothing } else { log::error!("Error accepting RPC connection: {:?}", e); break; } } } - sleep(Duration::from_secs(3)); + // use heart_beat + sleep(HEART_BEAT_INTERVAL); } Ok(()) diff --git a/src/protocol/contract.rs b/src/protocol/contract.rs index e94e244b..53cc425e 100644 --- a/src/protocol/contract.rs +++ b/src/protocol/contract.rs @@ -54,16 +54,21 @@ const PUBKEY1_OFFSET: usize = 2; const PUBKEY2_OFFSET: usize = PUBKEY1_OFFSET + PUBKEY_LENGTH + 1; /// Calculate the coin swap fee based on various parameters. +/// swap_amount in sats, refund_locktime in blocks. pub fn calculate_coinswap_fee( - absolute_fee_sat: Amount, - amount_relative_fee_ppb: Amount, - time_relative_fee_ppb: Amount, - total_funding_amount: Amount, - time_in_blocks: u64, + // Should we consider value in Amount? + swap_amount: u64, + refund_locktime: u16, + base_fee: u64, + amt_rel_fee_pct: f64, + time_rel_fee_pct: f64, ) -> u64 { - absolute_fee_sat.to_sat() - + (total_funding_amount.to_sat() * amount_relative_fee_ppb.to_sat()) / 1_000_000_000 - + (time_in_blocks * time_relative_fee_ppb.to_sat()) / 1_000_000_000 + // swap_amount as f64 * refund_locktime as f64 -> can overflow inside f64? + let total_fee = base_fee as f64 + + (swap_amount as f64 * amt_rel_fee_pct) / 1_00.00 + + (swap_amount as f64 * refund_locktime as f64 * time_rel_fee_pct) / 1_00.00; + + total_fee.ceil() as u64 } /// Apply two signatures to a 2-of-2 multisig spend. @@ -1092,54 +1097,46 @@ mod test { #[test] fn calculate_coinswap_fee_normal() { // Test with typical values - let absolute_fee_sat = Amount::from_sat(1000); - let amount_relative_fee_ppb = Amount::from_sat(500_000_000); - let time_relative_fee_ppb = Amount::from_sat(200_000_000); - let total_funding_amount = Amount::from_sat(1_000_000_000); - let time_in_blocks = 100; + let base_fee_sat = 1000; + let amt_rel_fee_pct = 2.5; + let time_rel_fee_pct = 0.1; + let swap_amount = 100_000; + let refund_locktime = 20; - let expected_fee = 1000 - + (1_000_000_000 * 500_000_000) / 1_000_000_000 - + (100 * 200_000_000) / 1_000_000_000; + let expected_fee = 5500; let calculated_fee = calculate_coinswap_fee( - absolute_fee_sat, - amount_relative_fee_ppb, - time_relative_fee_ppb, - total_funding_amount, - time_in_blocks, + swap_amount, + refund_locktime, + base_fee_sat, + amt_rel_fee_pct, + time_rel_fee_pct, ); assert_eq!(calculated_fee, expected_fee); // Test with zero values assert_eq!( - calculate_coinswap_fee(Amount::ZERO, Amount::ZERO, Amount::ZERO, Amount::ZERO, 0), + calculate_coinswap_fee(swap_amount, refund_locktime, 0, 0.0, 0.0), 0 ); // Test with only the absolute fee being non-zero assert_eq!( - calculate_coinswap_fee( - Amount::from_sat(1000), - Amount::ZERO, - Amount::ZERO, - Amount::ZERO, - 0 - ), + calculate_coinswap_fee(swap_amount, refund_locktime, base_fee_sat, 0.0, 0.0), 1000 ); // Test with only the relative fees being non-zero assert_eq!( calculate_coinswap_fee( - Amount::ZERO, - Amount::from_sat(1_000_000_000), - Amount::from_sat(1_000_000_000), - Amount::from_sat(1000), - 10 + swap_amount, + refund_locktime, + 0, + amt_rel_fee_pct, + time_rel_fee_pct ), - 1010 + 4500 ); } @@ -1292,8 +1289,8 @@ mod test { next_hashlock_pubkey: pub_1, next_multisig_pubkey: pub_2, }], - next_locktime: u16::default(), - next_fee_rate: u64::default(), + refund_locktime: u16::default(), + contract_feerate: u64::default(), }; // case with same hash value @@ -1321,8 +1318,8 @@ mod test { next_hashlock_pubkey: pub_1, next_multisig_pubkey: pub_2, }], - next_locktime: u16::default(), - next_fee_rate: u64::default(), + refund_locktime: u16::default(), + contract_feerate: u64::default(), }; let hash_value_from_fn = check_hashvalues_are_equal(&funding_proof).unwrap_err(); diff --git a/src/protocol/messages.rs b/src/protocol/messages.rs index 400ea6ef..b3182431 100644 --- a/src/protocol/messages.rs +++ b/src/protocol/messages.rs @@ -142,8 +142,8 @@ pub struct ProofOfFunding { pub confirmed_funding_txes: Vec, // TODO: Directly use Vec of Pubkeys. pub next_coinswap_info: Vec, - pub next_locktime: u16, - pub next_fee_rate: u64, + pub refund_locktime: u16, + pub contract_feerate: u64, } /// Signatures required for an intermediate Maker to perform receiving and sending of coinswaps. @@ -170,14 +170,14 @@ pub struct HashPreimage { } /// Multisig Privatekeys used in the last step of coinswap to perform privatekey handover. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct MultisigPrivkey { pub multisig_redeemscript: ScriptBuf, pub key: SecretKey, } /// Message to perform the final Privatekey Handover. This is the last message of the Coinswap Protocol. -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize)] pub struct PrivKeyHandover { pub multisig_privkeys: Vec, } @@ -221,14 +221,14 @@ impl Display for TakerToMakerMessage { } /// Represents the initial handshake message sent from Maker to Taker. -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize)] pub struct MakerHello { pub protocol_version_min: u32, pub protocol_version_max: u32, } /// Contains proof data related to fidelity bond. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Hash)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct FidelityProof { pub bond: FidelityBond, pub cert_hash: Hash, @@ -236,12 +236,12 @@ pub struct FidelityProof { } /// Represents an offer in the context of the Coinswap protocol. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Hash)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Offer { - pub absolute_fee_sat: Amount, - pub amount_relative_fee_ppb: Amount, - pub time_relative_fee_ppb: Amount, - pub required_confirms: u64, + pub base_fee: u64, // base fee in sats + pub amount_relative_fee_pct: f64, // % fee on total amount + pub time_relative_fee_pct: f64, // amount * refund_locktime * TRF% = fees for locking the fund. + pub required_confirms: u32, pub minimum_locktime: u16, pub max_size: u64, pub min_size: u64, @@ -250,13 +250,13 @@ pub struct Offer { } /// Contract Tx signatures provided by a Sender of a Coinswap. -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize)] pub struct ContractSigsForSender { pub sigs: Vec, } /// Contract Tx and extra metadata from a Sender of a Coinswap -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize)] pub struct SenderContractTxInfo { pub contract_tx: Transaction, pub timelock_pubkey: PublicKey, @@ -268,7 +268,7 @@ pub struct SenderContractTxInfo { /// for the Maker as both Sender and Receiver of Coinswaps. /// /// This message is sent by a Maker after a [`ProofOfFunding`] has been received. -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize)] pub struct ContractSigsAsRecvrAndSender { /// Contract Tx by which this maker is receiving Coinswap. pub receivers_contract_txs: Vec, @@ -277,13 +277,13 @@ pub struct ContractSigsAsRecvrAndSender { } /// Contract Tx signatures a Maker sends as a Receiver of CoinSwap. -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize)] pub struct ContractSigsForRecvr { pub sigs: Vec, } /// All messages sent from Maker to Taker. -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize)] pub enum MakerToTakerMessage { /// Protocol Handshake. MakerHello(MakerHello), diff --git a/src/taker/api.rs b/src/taker/api.rs index 8f1d4141..59665673 100644 --- a/src/taker/api.rs +++ b/src/taker/api.rs @@ -12,7 +12,7 @@ use std::{ collections::{HashMap, HashSet}, net::TcpStream, path::PathBuf, - thread::{self, sleep}, + thread::sleep, time::{Duration, Instant}, }; @@ -61,8 +61,8 @@ use crate::{ }; // Default values for Taker configurations -pub const REFUND_LOCKTIME: u16 = 48; -pub const REFUND_LOCKTIME_STEP: u16 = 48; +pub const REFUND_LOCKTIME: u16 = 20; +pub const REFUND_LOCKTIME_STEP: u16 = 20; pub const FIRST_CONNECT_ATTEMPTS: u32 = 5; pub const FIRST_CONNECT_SLEEP_DELAY_SEC: u64 = 1; pub const FIRST_CONNECT_ATTEMPT_TIMEOUT_SEC: u64 = 60; @@ -89,7 +89,7 @@ pub struct SwapParams { pub tx_count: u32, // TODO: Following two should be moved to TakerConfig as global configuration. /// Confirmation count required for funding txs. - pub required_confirms: u64, + pub required_confirms: u32, } // Defines the Taker's position in the current ongoing swap. @@ -174,7 +174,7 @@ impl Taker { /// ### Parameters: /// - `data_dir`: /// - `Some(value)`: Use the specified directory for storing data. - /// - `None`: Use the default data directory (e.g., for Linux: `~/.coinswap/maker`). + /// - `None`: Use the default data directory (e.g., for Linux: `~/.coinswap/taker`). /// - `wallet_file_name`: /// - `Some(value)`: Attempt to load a wallet file named `value`. If it does not exist, a new wallet with the given name will be created. /// - `None`: Create a new wallet file with the default name `maker-wallet`. @@ -240,10 +240,6 @@ impl Taker { } pub fn do_coinswap(&mut self, swap_params: SwapParams) -> Result<(), TakerError> { - let tor_log_dir = "/tmp/tor-rust-taker/log".to_string(); - - let taker_port = self.config.port; - #[cfg(feature = "tor")] let mut handle = None; @@ -253,6 +249,7 @@ impl Taker { ConnectionType::TOR => { let taker_socks_port = self.config.socks_port; + let tor_log_dir = "/tmp/tor-rust-taker/log".to_string(); if Path::new(tor_log_dir.as_str()).exists() { match fs::remove_file(Path::new(tor_log_dir.clone().as_str())) { Ok(_) => log::info!("Previous taker log file deleted successfully"), @@ -262,19 +259,20 @@ impl Taker { handle = Some(crate::tor::spawn_tor( taker_socks_port, - taker_port, + self.config.port, "/tmp/tor-rust-taker".to_string(), )); - thread::sleep(Duration::from_secs(10)); + // wait for tor process to create a new log file. + std::thread::sleep(Duration::from_secs(3)); if let Err(e) = monitor_log_for_completion(&PathBuf::from(tor_log_dir), "100%") { - log::error!("Error monitoring taker log file: {}", e); + log::error!("Error monitoring taker log file: {}\n Try removing the tor directory and retry", e); + return Err(TakerError::IO(e)); } - - log::info!("Taker tor is instantiated"); } } + self.send_coinswap(swap_params)?; #[cfg(feature = "tor")] @@ -655,7 +653,7 @@ impl Taker { } } //TODO handle confirm<0 - if gettx.confirmations >= Some(required_confirmations as u32) { + if gettx.confirmations >= Some(required_confirmations) { txid_tx_map.insert( *txid, deserialize::(&gettx.hex).map_err(WalletError::from)?, @@ -957,18 +955,18 @@ impl Taker { log::info!("Fundix Txids: {:?}", funding_txids); // Struct for information related to the next peer - let next_maker_info = NextPeerInfoArgs { + let next_maker_info = NextMakerInfo { next_peer_multisig_pubkeys: next_peer_multisig_pubkeys.clone(), next_peer_hashlock_pubkeys: next_peer_hashlock_pubkeys.clone(), - next_maker_refund_locktime: maker_refund_locktime, - next_maker_fee_rate: Amount::from_sat(MINER_FEE), }; let this_maker_info = ThisMakerInfo { this_maker: this_maker.clone(), funding_tx_infos: funding_tx_infos.to_vec(), this_maker_contract_txs, + this_maker_refund_locktime: maker_refund_locktime, }; + let (contract_sigs_as_recvr_sender, next_swap_contract_redeemscripts) = send_proof_of_funding_and_init_next_hop( &mut socket, @@ -1699,7 +1697,7 @@ impl Taker { .get_all_untried() .iter() .find(|oa| { - send_amount > Amount::from_sat(oa.offer.min_size) + send_amount >= Amount::from_sat(oa.offer.min_size) && send_amount < Amount::from_sat(oa.offer.max_size) && !self .ongoing_swap_state @@ -1927,30 +1925,25 @@ impl Taker { /// Synchronizes the offer book with addresses obtained from directory servers and local configurations. pub fn sync_offerbook(&mut self) -> Result<(), TakerError> { - let directory_address = match self.config.connection_type { - ConnectionType::CLEARNET => { - let mut address = self.config.directory_server_address.clone(); - if cfg!(feature = "integration-test") { - address = format!("127.0.0.1:{}", 8080); + let mut directory_address = self.config.directory_server_address.clone(); + if cfg!(feature = "integration-test") { + match self.config.connection_type { + ConnectionType::CLEARNET => { + directory_address = format!("127.0.0.1:{}", 8080); } - address - } - #[cfg(feature = "tor")] - ConnectionType::TOR => { - let mut address = self.config.directory_server_address.clone(); - if cfg!(feature = "integration-test") { + #[cfg(feature = "tor")] + ConnectionType::TOR => { let directory_hs_path_str = "/tmp/tor-rust-directory/hs-dir/hostname".to_string(); - let directory_hs_path = PathBuf::from(directory_hs_path_str); - let mut directory_file = fs::File::open(directory_hs_path)?; + let mut directory_file = fs::File::open(directory_hs_path_str)?; let mut directory_onion_addr = String::new(); directory_file.read_to_string(&mut directory_onion_addr)?; directory_onion_addr.pop(); - address = format!("{}:{}", directory_onion_addr, 8080); + directory_address = format!("{}:{}", directory_onion_addr, 8080); } - address } - }; + } + let mut socks_port: Option = None; #[cfg(feature = "tor")] { diff --git a/src/taker/config.rs b/src/taker/config.rs index 4d25438b..57c521c0 100644 --- a/src/taker/config.rs +++ b/src/taker/config.rs @@ -162,7 +162,7 @@ mod tests { let config = TakerConfig::new(Some(&config_path)).unwrap(); remove_temp_config(&config_path); - assert_eq!(REFUND_LOCKTIME, 48); + assert_eq!(REFUND_LOCKTIME, 20); assert_eq!(config, TakerConfig::default()); } @@ -188,7 +188,7 @@ mod tests { let config_path = create_temp_config(contents, "different_data_taker_config.toml"); let config = TakerConfig::new(Some(&config_path)).unwrap(); remove_temp_config(&config_path); - assert_eq!(REFUND_LOCKTIME, 48); + assert_eq!(REFUND_LOCKTIME, 20); assert_eq!( TakerConfig { socks_port: 19051, // Configurable via TOML. diff --git a/src/taker/offers.rs b/src/taker/offers.rs index d256d382..beaa6b77 100644 --- a/src/taker/offers.rs +++ b/src/taker/offers.rs @@ -28,7 +28,7 @@ use crate::{ use super::{config::TakerConfig, error::TakerError, routines::download_maker_offer}; /// Represents an offer along with the corresponding maker address. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash)] +#[derive(Debug, Clone, PartialEq)] pub struct OfferAndAddress { pub offer: Offer, pub address: MakerAddress, @@ -142,9 +142,9 @@ pub fn fetch_offer_from_makers( let maker_addresses_len = maker_addresses.len(); for addr in maker_addresses { let offers_writer = offers_writer.clone(); - let taker_config: TakerConfig = config.clone(); + let taker_config = config.clone(); let thread = Builder::new() - .name(format!("maker_offer_fecth_thread_{}", addr)) + .name(format!("maker_offer_fetch_thread_{}", addr)) .spawn(move || -> Result<(), TakerError> { let offer = download_maker_offer(addr, taker_config); Ok(offers_writer.send(offer)?) @@ -152,7 +152,7 @@ pub fn fetch_offer_from_makers( thread_pool.push(thread); } - let mut result = Vec::::new(); + let mut result = Vec::new(); for _ in 0..maker_addresses_len { if let Some(offer_addr) = offers_reader.recv()? { result.push(offer_addr); @@ -165,8 +165,9 @@ pub fn fetch_offer_from_makers( thread.thread().name().expect("thread names expected") ); let join_result = thread.join(); + if let Err(e) = join_result { - log::error!("Error in internal thread: {:?}", e); + log::error!("Error while joining thread: {:?}", e); } } Ok(result) @@ -174,7 +175,7 @@ pub fn fetch_offer_from_makers( /// Retrieves advertised maker addresses from directory servers based on the specified network. pub fn fetch_addresses_from_dns( - socks_port: Option, + _socks_port: Option, directory_server_address: String, connection_type: ConnectionType, ) -> Result, TakerError> { @@ -185,7 +186,7 @@ pub fn fetch_addresses_from_dns( ConnectionType::CLEARNET => TcpStream::connect(directory_server_address.as_str())?, #[cfg(feature = "tor")] ConnectionType::TOR => { - let socket_addrs = format!("127.0.0.1:{}", socks_port.expect("Tor port expected")); + let socket_addrs = format!("127.0.0.1:{}", _socks_port.expect("Tor port expected")); Socks5Stream::connect(socket_addrs, directory_server_address.as_str())?.into_inner() } }; diff --git a/src/taker/routines.rs b/src/taker/routines.rs index 25ee1609..1ee71a09 100644 --- a/src/taker/routines.rs +++ b/src/taker/routines.rs @@ -2,7 +2,7 @@ //! //! It includes functions for handshaking, requesting contract signatures, sending proofs of funding, and downloading maker offers. //! It also defines structs for contract transactions and contract information. -//! Notable types include [ContractTransaction], [ContractsInfo], [ThisMakerInfo], and [NextPeerInfoArgs]. +//! Notable types include [ContractTransaction], [ContractsInfo], [ThisMakerInfo], and [NextMakerInfo]. //! It also handles downloading maker offers with retry mechanisms and implements the necessary message structures //! for communication between taker and maker. @@ -15,7 +15,7 @@ use crate::{ protocol::{ contract::{ calculate_coinswap_fee, create_contract_redeemscript, find_funding_output_index, - validate_contract_tx, FUNDING_TX_VBYTE_SIZE, + validate_contract_tx, }, error::ProtocolError, messages::{ @@ -27,6 +27,7 @@ use crate::{ }, Hash160, }, + taker::api::MINER_FEE, utill::{read_message, send_message, ConnectionType}, wallet::WalletError, }; @@ -245,22 +246,21 @@ pub struct ThisMakerInfo { pub this_maker: OfferAndAddress, pub funding_tx_infos: Vec, pub this_maker_contract_txs: Vec, + pub this_maker_refund_locktime: u16, } -// Type for information related to the next peer +// Type for information related to the next peer // why not next Maker? #[derive(Clone)] -pub struct NextPeerInfoArgs { +pub struct NextMakerInfo { pub next_peer_multisig_pubkeys: Vec, pub next_peer_hashlock_pubkeys: Vec, - pub next_maker_refund_locktime: u16, - pub next_maker_fee_rate: Amount, } /// [Internal] Send a Proof funding to the maker and init next hop. pub(crate) fn send_proof_of_funding_and_init_next_hop( socket: &mut TcpStream, tmi: ThisMakerInfo, - npi: NextPeerInfoArgs, + npi: NextMakerInfo, hashvalue: Hash160, ) -> Result<(ContractSigsAsRecvrAndSender, Vec), TakerError> { // Send POF @@ -279,8 +279,8 @@ pub(crate) fn send_proof_of_funding_and_init_next_hop( let pof_msg = TakerToMakerMessage::RespProofOfFunding(ProofOfFunding { confirmed_funding_txes: tmi.funding_tx_infos.clone(), next_coinswap_info, - next_locktime: npi.next_maker_refund_locktime, - next_fee_rate: npi.next_maker_fee_rate.to_sat(), + refund_locktime: tmi.this_maker_refund_locktime, + contract_feerate: MINER_FEE, }); send_message(socket, &pof_msg)?; @@ -337,18 +337,18 @@ pub(crate) fn send_proof_of_funding_and_init_next_hop( .iter() .map(|i| i.funding_amount) .sum::(); + let coinswap_fees = calculate_coinswap_fee( - tmi.this_maker.offer.absolute_fee_sat, - tmi.this_maker.offer.amount_relative_fee_ppb, - tmi.this_maker.offer.time_relative_fee_ppb, - Amount::from_sat(this_amount), - 1, //time_in_blocks just 1 for now + this_amount, + tmi.this_maker_refund_locktime, + tmi.this_maker.offer.base_fee, + tmi.this_maker.offer.amount_relative_fee_pct, + tmi.this_maker.offer.time_relative_fee_pct, ); - let miner_fees_paid_by_taker = (FUNDING_TX_VBYTE_SIZE - * npi.next_maker_fee_rate.to_sat() - * (npi.next_peer_multisig_pubkeys.len() as u64)) - / 1000; + + let miner_fees_paid_by_taker = (tmi.funding_tx_infos.len() as u64) * MINER_FEE; let calculated_next_amount = this_amount - coinswap_fees - miner_fees_paid_by_taker; + if Amount::from_sat(calculated_next_amount) != next_amount { return Err((ProtocolError::IncorrectFundingAmount { expected: Amount::from_sat(calculated_next_amount), @@ -356,12 +356,13 @@ pub(crate) fn send_proof_of_funding_and_init_next_hop( }) .into()); } + log::info!( - "this_amount={} coinswap_fees={} miner_fees_paid_by_taker={} next_amount={}", - this_amount, - coinswap_fees, + "Maker Received = {} | Maker is Forwarding = {} | Coinswap Fees = {} | Miner Fees paid by us = {} ", + Amount::from_sat(this_amount), + next_amount, + Amount::from_sat(coinswap_fees), miner_fees_paid_by_taker, - next_amount ); for ((receivers_contract_tx, contract_tx), contract_redeemscript) in @@ -394,7 +395,7 @@ pub(crate) fn send_proof_of_funding_and_init_next_hop( hashlock_pubkey, &senders_contract_tx_info.timelock_pubkey, &hashvalue, - &npi.next_maker_refund_locktime, + &tmi.this_maker_refund_locktime, ) }) .collect::>(); @@ -491,6 +492,7 @@ pub fn download_maker_offer(address: MakerAddress, config: TakerConfig) -> Optio match download_maker_offer_attempt_once(&address, &config) { Ok(offer) => return Some(OfferAndAddress { offer, address }), Err(TakerError::IO(e)) => { + // TODO: Think about here for better logic? if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut { if ii <= FIRST_CONNECT_ATTEMPTS { log::warn!( diff --git a/src/utill.rs b/src/utill.rs index f2149808..057f1020 100644 --- a/src/utill.rs +++ b/src/utill.rs @@ -55,9 +55,12 @@ pub const NET_TIMEOUT: Duration = Duration::from_secs(60); /// Used as delays on reattempting some network communications. pub const GLOBAL_PAUSE: Duration = Duration::from_secs(10); -/// Global heartbeat interval for internal server threads. +/// Global heartbeat interval used during waiting periods in critical situations. pub const HEART_BEAT_INTERVAL: Duration = Duration::from_secs(3); +/// Number of confirmation required funding transaction. +pub const REQUIRED_CONFIRMS: u32 = 1; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ConnectionType { #[cfg(feature = "tor")] @@ -94,6 +97,7 @@ pub fn get_tor_addrs(hs_dir: &Path) -> io::Result { let mut hostname_file = File::open(hostname_file_path).unwrap(); let mut tor_addrs: String = String::new(); hostname_file.read_to_string(&mut tor_addrs)?; + tor_addrs.pop(); // Remove `\n` at the end. Ok(tor_addrs) } @@ -421,7 +425,7 @@ pub fn monitor_log_for_completion(log_file: &Path, pattern: &str) -> io::Result< last_size = current_size; } - thread::sleep(Duration::from_secs(3)); + thread::sleep(HEART_BEAT_INTERVAL); } } @@ -587,17 +591,20 @@ mod tests { protocol_version_min: 1, protocol_version_max: 100, }); + thread::spawn(move || { let (mut socket, _) = listener.accept().unwrap(); let msg_bytes = read_message(&mut socket).unwrap(); let msg: MakerToTakerMessage = serde_cbor::from_slice(&msg_bytes).unwrap(); - assert_eq!( - msg, - MakerToTakerMessage::MakerHello(MakerHello { - protocol_version_min: 1, - protocol_version_max: 100 - }) - ); + + if let MakerToTakerMessage::MakerHello(hello) = msg { + assert!(hello.protocol_version_min == 1 && hello.protocol_version_max == 100); + } else { + panic!( + "Received Wrong Message: Expected MakerHello variant, Got: {:?}", + msg, + ); + } }); let mut stream = TcpStream::connect(address).unwrap(); diff --git a/src/wallet/rpc.rs b/src/wallet/rpc.rs index 056f7d2e..d0a1a7e8 100644 --- a/src/wallet/rpc.rs +++ b/src/wallet/rpc.rs @@ -1,10 +1,10 @@ //! Manages connection with a Bitcoin Core RPC. //! -use std::{convert::TryFrom, thread, time::Duration}; - +use crate::utill::HEART_BEAT_INTERVAL; use bitcoin::Network; use bitcoind::bitcoincore_rpc::{Auth, Client, RpcApi}; use serde_json::{json, Value}; +use std::{convert::TryFrom, thread}; use crate::wallet::api::KeychainKind; @@ -142,7 +142,7 @@ impl Wallet { Err(e) => { log::warn!("Sync Error, Retrying: {}", e); - thread::sleep(Duration::from_secs(3)); + thread::sleep(HEART_BEAT_INTERVAL); continue; } } diff --git a/taker.toml b/taker.toml index 79d94d63..af140627 100644 --- a/taker.toml +++ b/taker.toml @@ -1,39 +1,10 @@ -[taker_config] -# relatively low value for now so that its easier to test without having to wait too much -# right now only the very brave will try coinswap out on mainnet with non-trivial amounts - -#in blocks -refund_locktime = 48 -#in blocks -refund_locktime_step = 48 - -# first connect means the first time you're ever connecting, without having gotten any txes -# confirmed yet, so the taker will not be very persistent since there should be plenty of other -# makers out there -# but also it should allow for flaky connections, otherwise you exclude raspberry pi nodes running -# in people's closets, which are very important for decentralization - -first_connect_attempts = 5 -first_connect_sleep_delay_sec = 1 -first_connect_attempt_timeout_sec = 60 - -# reconnect means when connecting to a maker again after having already gotten txes confirmed -# as it would be a waste of miner fees to give up, the taker is coded to be very persistent -# taker will first attempt to connect with a short delay between attempts -# after that will attempt to connect with a longer delay between attempts -# these figures imply that taker will attempt to connect for just over 48 hours -# of course the user can ctrl+c before then if they give up themselves - -reconnect_attempts = 3200 -reconnect_short_sleep_delay = 10 -reconnect_long_sleep_delay = 60 -# after this many attempts, switch to sleeping longer -short_long_sleep_delay_transition = 60 -reconnect_attempt_timeout_sec = 300 - -# tor configuration -tor_port = 8000 -socks_port = 19050 -# Directory server onion address -directory_server_onion_address = "directoryhiddenserviceaddress.onion:8080" -connection_type = "tor" \ No newline at end of file +# Network listening port +port= 8000, +#Socks port +socks_port= 19070, +# Directory server address +directory_server_address=directoryhiddenserviceaddress.onion:8080 , +# Connection type +connection_type= TOR, +# RPC port +rpc_port= 8081, \ No newline at end of file diff --git a/tests/abort1.rs b/tests/abort1.rs index 84274b82..1de93f07 100644 --- a/tests/abort1.rs +++ b/tests/abort1.rs @@ -7,7 +7,12 @@ use coinswap::{ }; mod test_framework; use log::{info, warn}; -use std::{assert_eq, sync::atomic::Ordering::Relaxed, thread, time::Duration}; +use std::{ + assert_eq, + sync::{atomic::Ordering::Relaxed, Arc}, + thread, + time::Duration, +}; use test_framework::*; /// Abort 1: TAKER Drops After Full Setup. @@ -30,7 +35,7 @@ fn test_stop_taker_after_setup() { // Initiate test framework, Makers. // Taker has a special behavior DropConnectionAfterFullSetup. - let (test_framework, taker, makers, directory_server_instance, block_generation_handle) = + let (test_framework, mut taker, makers, directory_server_instance, block_generation_handle) = TestFramework::init( makers_config_map.into(), TakerBehavior::DropConnectionAfterFullSetup, @@ -39,79 +44,26 @@ fn test_stop_taker_after_setup() { warn!("Running Test: Taker Cheats on Everybody."); - let bitcoind = &test_framework.bitcoind; - - info!("Initiating Takers..."); - // Fund the Taker and Makers with 3 utxos of 0.05 btc each. - for _ in 0..3 { - let taker_address = taker - .write() - .unwrap() - .get_wallet_mut() - .get_next_external_address() - .unwrap(); - - send_to_address(bitcoind, &taker_address, Amount::from_btc(0.05).unwrap()); - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - } - - // Coins for fidelity creation - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - - // confirm balances - generate_blocks(bitcoind, 1); - - let mut all_utxos = taker.read().unwrap().get_wallet().get_all_utxo().unwrap(); + // Fund the Taker with 3 utxos of 0.05 btc each and do basic checks on the balance + let org_taker_spend_balance = fund_and_verify_taker( + &mut taker, + &test_framework.bitcoind, + 3, + Amount::from_btc(0.05).unwrap(), + ); - // Get the original balances - let org_taker_balance_fidelity = taker - .read() - .unwrap() - .get_wallet() - .balance_fidelity_bonds(Some(&all_utxos)) - .unwrap(); - let org_taker_balance_descriptor_utxo = taker - .read() - .unwrap() - .get_wallet() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let org_taker_balance_swap_coins = taker - .read() - .unwrap() - .get_wallet() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - let org_taker_balance_live_contract = taker - .read() - .unwrap() - .get_wallet() - .balance_live_contract(Some(&all_utxos)) - .unwrap(); - let org_taker_balance = org_taker_balance_descriptor_utxo + org_taker_balance_swap_coins; + // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. + let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); + fund_and_verify_maker( + makers_ref, + &test_framework.bitcoind, + 4, + Amount::from_btc(0.05).unwrap(), + ); - // ---- Start Servers and attempt Swap ---- + // Start the Maker Server threads + log::info!("Initiating Maker..."); - info!("Initiating Maker..."); - // Start the Maker server threads let maker_threads = makers .iter() .map(|maker| { @@ -122,203 +74,101 @@ fn test_stop_taker_after_setup() { }) .collect::>(); - // Start swap - // Makers take time to fully setup. - makers.iter().for_each(|maker| { - while !maker.is_setup_complete.load(Relaxed) { - log::info!("Waiting for maker setup completion"); - // Introduce a delay of 10 seconds to prevent write lock starvation. - thread::sleep(Duration::from_secs(10)); - continue; - } - }); + let org_maker_spend_balances = makers + .iter() + .map(|maker| { + while !maker.is_setup_complete.load(Relaxed) { + log::info!("Waiting for maker setup completion"); + // Introduce a delay of 10 seconds to prevent write lock starvation. + thread::sleep(Duration::from_secs(10)); + continue; + } - let swap_params = SwapParams { - send_amount: Amount::from_sat(500000), - maker_count: 2, - tx_count: 3, - required_confirms: 1, - }; + // Check balance after setting up maker server. + let wallet = maker.wallet.read().unwrap(); + let all_utxos = wallet.get_all_utxo().unwrap(); - info!("Initiating coinswap protocol"); + let seed_balance = wallet.balance_descriptor_utxo(Some(&all_utxos)).unwrap(); - // Calculate Original balance excluding fidelity bonds. - // Bonds are created automatically after spawning the maker server. - let org_maker_balances = makers - .iter() - .map(|maker| { - all_utxos = maker.get_wallet().read().unwrap().get_all_utxo().unwrap(); - let maker_balance_fidelity = maker - .get_wallet() - .read() - .unwrap() - .balance_fidelity_bonds(Some(&all_utxos)) - .unwrap(); - let maker_balance_descriptor_utxo = maker - .get_wallet() - .read() - .unwrap() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let maker_balance_swap_coins = maker - .get_wallet() - .read() - .unwrap() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - let maker_balance_live_contract = maker - .get_wallet() - .read() - .unwrap() - .balance_live_contract(Some(&all_utxos)) - .unwrap(); - assert_eq!(maker_balance_fidelity, Amount::from_btc(0.05).unwrap()); - assert_eq!( - maker_balance_descriptor_utxo, - Amount::from_btc(0.14999).unwrap() - ); - assert_eq!(maker_balance_swap_coins, Amount::from_btc(0.0).unwrap()); - assert_eq!(maker_balance_live_contract, Amount::from_btc(0.0).unwrap()); - maker_balance_descriptor_utxo + maker_balance_swap_coins + let fidelity_balance = wallet.balance_fidelity_bonds(Some(&all_utxos)).unwrap(); + + let swapcoin_balance = wallet.balance_swap_coins(Some(&all_utxos)).unwrap(); + + let live_contract_balance = wallet.balance_live_contract(Some(&all_utxos)).unwrap(); + + assert_eq!(seed_balance, Amount::from_btc(0.14999).unwrap()); + assert_eq!(fidelity_balance, Amount::from_btc(0.05).unwrap()); + assert_eq!(swapcoin_balance, Amount::ZERO); + assert_eq!(live_contract_balance, Amount::ZERO); + + seed_balance + swapcoin_balance }) .collect::>(); - // Spawn a Taker coinswap thread. - let taker_clone = taker.clone(); - let taker_thread = thread::spawn(move || { - taker_clone - .write() - .unwrap() - .do_coinswap(swap_params) - .unwrap(); - }); + // Initiate Coinswap + log::info!("Initiating coinswap protocol"); - // Wait for Taker swap thread to conclude. - taker_thread.join().unwrap(); + // Swap params for coinswap. + let swap_params = SwapParams { + send_amount: Amount::from_sat(500000), + maker_count: 2, + tx_count: 3, + required_confirms: 1, + }; + taker.do_coinswap(swap_params).unwrap(); - // Wait for Maker threads to conclude. + // After Swap is done, wait for maker threads to conclude. maker_threads .into_iter() .for_each(|thread| thread.join().unwrap()); - // ---- After Swap checks ---- + log::info!("All coinswaps processed successfully. Transaction complete."); + // Shutdown Directory Server directory_server_instance.shutdown.store(true, Relaxed); thread::sleep(Duration::from_secs(10)); - // Taker still has 6 swapcoins in its list - assert_eq!(taker.read().unwrap().get_wallet().get_swapcoins_count(), 6); - //Run Recovery script + // TODO: do something about this? warn!("Starting Taker recovery process"); - taker.write().unwrap().recover_from_swap().unwrap(); - - // All pending swapcoins are cleared now. - assert_eq!(taker.read().unwrap().get_wallet().get_swapcoins_count(), 0); - - all_utxos = taker.read().unwrap().get_wallet().get_all_utxo().unwrap(); - - // Check everybody looses mining fees of contract txs. - let taker_balance_fidelity = taker - .read() - .unwrap() - .get_wallet() - .balance_fidelity_bonds(Some(&all_utxos)) - .unwrap(); - let taker_balance_descriptor_utxo = taker - .read() - .unwrap() - .get_wallet() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let taker_balance_swap_coins = taker - .read() - .unwrap() - .get_wallet() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - let taker_balance_live_contract = taker - .read() - .unwrap() - .get_wallet() - .balance_live_contract(Some(&all_utxos)) - .unwrap(); - let taker_balance = taker_balance_descriptor_utxo + taker_balance_swap_coins; - - assert_eq!(org_taker_balance - taker_balance, Amount::from_sat(6768)); - assert_eq!(org_taker_balance_fidelity, Amount::from_btc(0.0).unwrap()); - assert_eq!( - org_taker_balance_descriptor_utxo, - Amount::from_btc(0.15).unwrap() - ); - assert_eq!(org_taker_balance_swap_coins, Amount::from_btc(0.0).unwrap()); - assert_eq!( - org_taker_balance_live_contract, - Amount::from_btc(0.0).unwrap() - ); - assert_eq!(taker_balance_fidelity, Amount::from_btc(0.0).unwrap()); - assert_eq!( - taker_balance_descriptor_utxo, - Amount::from_btc(0.14993232).unwrap() + taker.recover_from_swap().unwrap(); + + // ## Fee Tracking and Workflow: + // + // ### Fee Breakdown: + // + // +------------------+-------------------------+--------------------------+------------+----------------------------+-------------------+ + // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | + // +------------------+-------------------------+--------------------------+------------+----------------------------+-------------------+ + // | Taker | _ | 500,000 | _ | 3,000 | 3,000 | + // | Maker16102 | 500,000 | 463,500 | 33,500 | 3,000 | 36,500 | + // | Maker6102 | 463,500 | 438,642 | 21,858 | 3,000 | 24,858 | + // +------------------+-------------------------+--------------------------+------------+----------------------------+-------------------+ + // + // + // **Taker** => DropConnectionAfterFullSetup + // + // Participants regain their initial funding amounts but incur a total loss of **6,768 sats** + // due to mining fees (recovery + initial transaction fees). + // + // ### Recovery Fees Breakdown: + // + // +------------------+------------------------------------+---------------------+--------------------+----------------------------+ + // | Participant | Mining Fee for Contract txes (Sats) | Timelock Fee (Sats) | Funding Fee (Sats) | Total Recovery Fees (Sats) | + // +------------------+------------------------------------+---------------------+--------------------+----------------------------+ + // | Taker | 3,000 | 768 | 3,000 | 6,768 | + // | Maker16102 | 3,000 | 768 | 3,000 | 6,768 | + // | Maker6102 | 3,000 | 768 | 3,000 | 6,768 | + // +------------------+------------------------------------+---------------------+--------------------+----------------------------+ + // + verify_swap_results( + &taker, + &makers, + org_taker_spend_balance, + org_maker_spend_balances, ); - assert_eq!(taker_balance_swap_coins, Amount::from_btc(0.0).unwrap()); - assert_eq!(taker_balance_live_contract, Amount::from_btc(0.0).unwrap()); - - makers - .iter() - .zip(org_maker_balances.iter()) - .for_each(|(maker, org_balance)| { - all_utxos = maker.get_wallet().read().unwrap().get_all_utxo().unwrap(); - let maker_balance_fidelity = maker - .get_wallet() - .read() - .unwrap() - .balance_fidelity_bonds(Some(&all_utxos)) - .unwrap(); - let maker_balance_descriptor_utxo = maker - .get_wallet() - .read() - .unwrap() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let maker_balance_swap_coins = maker - .get_wallet() - .read() - .unwrap() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - let maker_balance_live_contract = maker - .get_wallet() - .read() - .unwrap() - .balance_live_contract(Some(&all_utxos)) - .unwrap(); - let new_balance = maker - .get_wallet() - .read() - .unwrap() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap() - + maker - .get_wallet() - .read() - .unwrap() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - - assert_eq!(*org_balance - new_balance, Amount::from_sat(6768)); - - assert_eq!(maker_balance_fidelity, Amount::from_btc(0.05).unwrap()); - assert_eq!( - maker_balance_descriptor_utxo, - Amount::from_btc(0.14992232).unwrap() - ); - assert_eq!(maker_balance_swap_coins, Amount::from_btc(0.0).unwrap()); - assert_eq!(maker_balance_live_contract, Amount::from_btc(0.0).unwrap()); - }); - info!("All checks successful. Terminating integration test case"); test_framework.stop(); diff --git a/tests/abort2_case1.rs b/tests/abort2_case1.rs index bb815857..9a207e74 100644 --- a/tests/abort2_case1.rs +++ b/tests/abort2_case1.rs @@ -1,16 +1,17 @@ #![cfg(feature = "integration-test")] use bitcoin::Amount; +use bitcoind::bitcoincore_rpc::RpcApi; use coinswap::{ maker::{start_maker_server, MakerBehavior}, taker::{SwapParams, TakerBehavior}, utill::ConnectionType, }; - +use std::sync::Arc; mod test_framework; -use test_framework::*; - +use coinswap::wallet::{Destination, SendAmount}; use log::{info, warn}; use std::{sync::atomic::Ordering::Relaxed, thread, time::Duration}; +use test_framework::*; /// ABORT 2: Maker Drops Before Setup /// This test demonstrates the situation where a Maker prematurely drops connections after doing @@ -25,14 +26,17 @@ fn test_abort_case_2_move_on_with_other_makers() { // 6102 is naughty. But theres enough good ones. let makers_config_map = [ - ((6102, None), MakerBehavior::CloseAtReqContractSigsForSender), - ((16102, None), MakerBehavior::Normal), + ((6102, None), MakerBehavior::Normal), + ( + (16102, None), + MakerBehavior::CloseAtReqContractSigsForSender, + ), ((26102, None), MakerBehavior::Normal), ]; // Initiate test framework, Makers. // Taker has normal behavior. - let (test_framework, taker, makers, directory_server_instance, block_generation_handle) = + let (test_framework, mut taker, makers, directory_server_instance, block_generation_handle) = TestFramework::init( makers_config_map.into(), TakerBehavior::Normal, @@ -45,49 +49,18 @@ fn test_abort_case_2_move_on_with_other_makers() { let bitcoind = &test_framework.bitcoind; - info!("Initiating Takers..."); - // Fund the Taker and Makers with 3 utxos of 0.05 btc each. - for _ in 0..3 { - let taker_address = taker - .write() - .unwrap() - .get_wallet_mut() - .get_next_external_address() - .unwrap(); - - send_to_address(bitcoind, &taker_address, Amount::from_btc(0.05).unwrap()); - - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - } - - // Coins for fidelity creation - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); + // Fund the Taker with 3 utxos of 0.05 btc each and do basic checks on the balance + let org_taker_spend_balance = + fund_and_verify_taker(&mut taker, bitcoind, 3, Amount::from_btc(0.05).unwrap()); - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); + // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. + let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); - // confirm balances - generate_blocks(bitcoind, 1); + fund_and_verify_maker(makers_ref, bitcoind, 4, Amount::from_btc(0.05).unwrap()); - // ---- Start Servers and attempt Swap ---- + // Start the Maker Server threads + log::info!("Initiating Maker..."); - info!("Initiating Maker..."); - // Start the Maker server threads let maker_threads = makers .iter() .map(|maker| { @@ -98,65 +71,164 @@ fn test_abort_case_2_move_on_with_other_makers() { }) .collect::>(); - // Start swap - // Makers take time to fully setup. - makers.iter().for_each(|maker| { - while !maker.is_setup_complete.load(Relaxed) { - log::info!("Waiting for maker setup completion"); - // Introduce a delay of 10 seconds to prevent write lock starvation. - thread::sleep(Duration::from_secs(10)); - continue; - } - }); + let org_maker_spend_balances = makers + .iter() + .map(|maker| { + while !maker.is_setup_complete.load(Relaxed) { + log::info!("Waiting for maker setup completion"); + // Introduce a delay of 10 seconds to prevent write lock starvation. + thread::sleep(Duration::from_secs(10)); + continue; + } + + // Check balance after setting up maker server. + let wallet = maker.wallet.read().unwrap(); + let all_utxos = wallet.get_all_utxo().unwrap(); + + let seed_balance = wallet.balance_descriptor_utxo(Some(&all_utxos)).unwrap(); + + let fidelity_balance = wallet.balance_fidelity_bonds(Some(&all_utxos)).unwrap(); + + let swapcoin_balance = wallet.balance_swap_coins(Some(&all_utxos)).unwrap(); + + let live_contract_balance = wallet.balance_live_contract(Some(&all_utxos)).unwrap(); + + assert_eq!(seed_balance, Amount::from_btc(0.14999).unwrap()); + assert_eq!(fidelity_balance, Amount::from_btc(0.05).unwrap()); + assert_eq!(swapcoin_balance, Amount::ZERO); + assert_eq!(live_contract_balance, Amount::ZERO); + + seed_balance + swapcoin_balance + }) + .collect::>(); + // Initiate Coinswap + log::info!("Initiating coinswap protocol"); + + // Swap params for coinswap. let swap_params = SwapParams { send_amount: Amount::from_sat(500000), maker_count: 2, tx_count: 3, required_confirms: 1, }; + taker.do_coinswap(swap_params).unwrap(); - info!("Initiating coinswap protocol"); - // Spawn a Taker coinswap thread. - let taker_clone = taker.clone(); - let taker_thread = thread::spawn(move || { - taker_clone - .write() - .unwrap() - .do_coinswap(swap_params) - .unwrap(); - }); - - // Wait for Taker swap thread to conclude. - taker_thread.join().unwrap(); - - // Wait for Maker threads to conclude. + // After Swap is done, wait for maker threads to conclude. makers .iter() .for_each(|maker| maker.shutdown.store(true, Relaxed)); + maker_threads .into_iter() .for_each(|thread| thread.join().unwrap()); - // ---- After Swap checks ---- + log::info!("All coinswaps processed successfully. Transaction complete."); + // Shutdown Directory Server directory_server_instance.shutdown.store(true, Relaxed); thread::sleep(Duration::from_secs(10)); - // TODO: Do balance assertions. - - // Maker might not get banned as Taker may not try 6102 for swap. If it does then check its 6102. - if !taker.read().unwrap().get_bad_makers().is_empty() { + // ----------------------Swap Completed Successfully----------------------------------------------------------- + + // +------------------------------------------------------------------------------------------------------+ + // | ## Fee Tracking and Workflow | + // +------------------------------------------------------------------------------------------------------+ + // | | + // | ### Assumptions: | + // | 1. **Taker connects to Maker16102 as the first Maker.** | + // | 2. **Workflow:** Taker → Maker16102 (`CloseAtReqContractSigsForSender`) → Maker6102 → Maker26102 → | + // | Taker. | + // | | + // | ### Fee Breakdown: | + // | | + // | +------------------+-------------------------+--------------------------+------------+----------------+| + // | | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining || + // | | | | | | Fees (Sats) || + // | +------------------+-------------------------+--------------------------+------------+----------------+| + // | | Taker | _ | 500,000 | _ | 3,000 || + // | | Maker16102 | _ | _ | _ | _ || + // | | Maker6102 | 500,000 | 463,500 | 33,500 | 3,000 || + // | | Maker26102 | 463,500 | 438,642 | 21,858 | 3,000 || + // | +------------------+-------------------------+--------------------------+------------+----------------+| + // | | + // | ### Final Outcomes | + // | | + // | #### Taker (Successful Coinswap): | + // | +-------------+------------------------------------------------------------------------------------+ | + // | | Participant | Coinswap Outcome (Sats) | | + // | +-------------+------------------------------------------------------------------------------------+ | + // | | Taker | 438,642 = 500,000 - (Total Fees for Maker16102 + Total Fees for Maker6102) | | + // | +-------------+------------------------------------------------------------------------------------+ | + // | | + // | #### Makers: | + // | +---------------+-----------------------------------------------------------------------------------+| + // | | Participant | Coinswap Outcome (Sats) | | + // | +---------------+-----------------------------------------------------------------------------------+| + // | | Maker16102 | 0 (Marked as a bad Maker by Taker) | | + // | | Maker6102 | 500,000 - 463,500 - 3,000 = +33,500 | | + // | | Maker26102 | 463,500 - 438,642 - 3,000 = +21,858 | | + // | +---------------+-----------------------------------------------------------------------------------+| + // | | + // +------------------------------------------------------------------------------------------------------+ + + // Maker might not get banned as Taker may not try 16102 for swap. If it does then check its 16102. + if !taker.get_bad_makers().is_empty() { assert_eq!( - format!("127.0.0.1:{}", 6102), - taker.read().unwrap().get_bad_makers()[0] - .address - .to_string() + format!("127.0.0.1:{}", 16102), + taker.get_bad_makers()[0].address.to_string() ); } + // After Swap checks: + verify_swap_results( + &taker, + &makers, + org_taker_spend_balance, + org_maker_spend_balances, + ); + + info!("Balance check successful."); + + // Check spending from swapcoins. + info!("Checking Spend from Swapcoin"); + + let taker_wallet_mut = taker.get_wallet_mut(); + + let swap_coins = taker_wallet_mut + .list_swap_coin_utxo_spend_info(None) + .unwrap(); + + let tx = taker_wallet_mut + .spend_from_wallet( + Amount::from_sat(1000), + SendAmount::Max, + Destination::Wallet, + &swap_coins, + ) + .unwrap(); + + assert_eq!( + tx.input.len(), + 3, + "Not all swap coin utxos got included in the spend transaction" + ); + + bitcoind.client.send_raw_transaction(&tx).unwrap(); + generate_blocks(bitcoind, 1); + + taker_wallet_mut.sync().unwrap(); + + let swap_coin_bal = taker_wallet_mut.balance_swap_coins(None).unwrap(); + let descriptor_bal = taker_wallet_mut.balance_descriptor_utxo(None).unwrap(); + + assert_eq!(swap_coin_bal, Amount::ZERO); + assert_eq!(descriptor_bal, Amount::from_btc(0.14934642).unwrap()); + + info!("All checks successful. Terminating integration test case"); + test_framework.stop(); block_generation_handle.join().unwrap(); diff --git a/tests/abort2_case2.rs b/tests/abort2_case2.rs index ad395e8e..29a71323 100644 --- a/tests/abort2_case2.rs +++ b/tests/abort2_case2.rs @@ -5,6 +5,7 @@ use coinswap::{ taker::{SwapParams, TakerBehavior}, utill::ConnectionType, }; +use std::sync::Arc; mod test_framework; use test_framework::*; @@ -39,71 +40,33 @@ fn test_abort_case_2_recover_if_no_makers_found() { // Initiate test framework, Makers. // Taker has normal behavior. - let (test_framework, taker, makers, directory_server_instance, block_generation_handle) = + let (test_framework, mut taker, makers, directory_server_instance, block_generation_handle) = TestFramework::init( makers_config_map.into(), TakerBehavior::Normal, ConnectionType::CLEARNET, ); - let bitcoind = &test_framework.bitcoind; - // Fund the Taker and Makers with 3 utxos of 0.05 btc each. - for _ in 0..3 { - let taker_address = taker - .write() - .unwrap() - .get_wallet_mut() - .get_next_external_address() - .unwrap(); - - send_to_address(bitcoind, &taker_address, Amount::from_btc(0.05).unwrap()); - - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - } - - // Coins for fidelity creation - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - - // confirm balances - generate_blocks(bitcoind, 1); - - let mut all_utxos = taker.read().unwrap().get_wallet().get_all_utxo().unwrap(); - - // Get the original balances - let org_taker_balance_descriptor_utxo = taker - .read() - .unwrap() - .get_wallet() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let org_taker_balance_swap_coins = taker - .read() - .unwrap() - .get_wallet() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); + // Fund the Taker with 3 utxos of 0.05 btc each and do basic checks on the balance + let org_taker_spend_balance = fund_and_verify_taker( + &mut taker, + &test_framework.bitcoind, + 3, + Amount::from_btc(0.05).unwrap(), + ); - let org_taker_balance = org_taker_balance_descriptor_utxo + org_taker_balance_swap_coins; + // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. + let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); + fund_and_verify_maker( + makers_ref, + &test_framework.bitcoind, + 4, + Amount::from_btc(0.05).unwrap(), + ); - // ---- Start Servers and attempt Swap ---- + // Start the Maker Server threads + log::info!("Initiating Maker..."); - // Start the Maker server threads let maker_threads = makers .iter() .map(|maker| { @@ -114,108 +77,116 @@ fn test_abort_case_2_recover_if_no_makers_found() { }) .collect::>(); - // Start swap - // Makers take time to fully setup. - makers.iter().for_each(|maker| { - while !maker.is_setup_complete.load(Relaxed) { - log::info!("Waiting for maker setup completion"); - // Introduce a delay of 10 seconds to prevent write lock starvation. - thread::sleep(Duration::from_secs(10)); - continue; - } - }); - - let swap_params = SwapParams { - send_amount: Amount::from_sat(500000), - maker_count: 2, - tx_count: 3, - required_confirms: 1, - }; - - // Calculate Original balance excluding fidelity bonds. - // Bonds are created automatically after spawning the maker server. - let org_maker_balances = makers + let org_maker_spend_balances = makers .iter() .map(|maker| { - all_utxos = maker.get_wallet().read().unwrap().get_all_utxo().unwrap(); - let maker_balance_fidelity = maker - .get_wallet() - .read() - .unwrap() - .balance_fidelity_bonds(Some(&all_utxos)) - .unwrap(); - let maker_balance_descriptor_utxo = maker - .get_wallet() - .read() - .unwrap() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let maker_balance_swap_coins = maker - .get_wallet() - .read() - .unwrap() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - let maker_balance_live_contract = maker - .get_wallet() - .read() - .unwrap() - .balance_live_contract(Some(&all_utxos)) - .unwrap(); + while !maker.is_setup_complete.load(Relaxed) { + log::info!("Waiting for maker setup completion"); + // Introduce a delay of 10 seconds to prevent write lock starvation. + thread::sleep(Duration::from_secs(10)); + continue; + } - assert_eq!(maker_balance_fidelity, Amount::from_btc(0.05).unwrap()); - assert_eq!( - maker_balance_descriptor_utxo, - Amount::from_btc(0.14999).unwrap() - ); - assert_eq!(maker_balance_swap_coins, Amount::from_btc(0.0).unwrap()); - assert_eq!(maker_balance_live_contract, Amount::from_btc(0.0).unwrap()); + // Check balance after setting up maker server. + let wallet = maker.wallet.read().unwrap(); + let all_utxos = wallet.get_all_utxo().unwrap(); + + let seed_balance = wallet.balance_descriptor_utxo(Some(&all_utxos)).unwrap(); + + let fidelity_balance = wallet.balance_fidelity_bonds(Some(&all_utxos)).unwrap(); + + let swapcoin_balance = wallet.balance_swap_coins(Some(&all_utxos)).unwrap(); + + let live_contract_balance = wallet.balance_live_contract(Some(&all_utxos)).unwrap(); - ( - maker_balance_fidelity, - maker_balance_descriptor_utxo, - maker_balance_swap_coins, - maker_balance_live_contract, - maker_balance_descriptor_utxo + maker_balance_swap_coins, - ) + assert_eq!(seed_balance, Amount::from_btc(0.14999).unwrap()); + assert_eq!(fidelity_balance, Amount::from_btc(0.05).unwrap()); + assert_eq!(swapcoin_balance, Amount::ZERO); + assert_eq!(live_contract_balance, Amount::ZERO); + + seed_balance + swapcoin_balance }) .collect::>(); - // Spawn a Taker coinswap thread. - let taker_clone = taker.clone(); - let taker_thread = thread::spawn(move || taker_clone.write().unwrap().do_coinswap(swap_params)); + // Initiate Coinswap + log::info!("Initiating coinswap protocol"); - // Wait for Taker swap thread to conclude. - // The whole swap can fail if 6102 happens to be the first peer. - // In that the swap isn't feasible, and user should modify SwapParams::maker_count. - if let Err(e) = taker_thread.join().unwrap() { + // Swap params for coinswap. + let swap_params = SwapParams { + send_amount: Amount::from_sat(500000), + maker_count: 2, + tx_count: 3, + required_confirms: 1, + }; + + if let Err(e) = taker.do_coinswap(swap_params) { assert_eq!(format!("{:?}", e), "NotEnoughMakersInOfferBook".to_string()); info!("Coinswap failed because the first maker rejected for signature"); } - // Wait for Maker threads to conclude. + // After Swap is done, wait for maker threads to conclude. makers .iter() .for_each(|maker| maker.shutdown.store(true, Relaxed)); + maker_threads .into_iter() .for_each(|thread| thread.join().unwrap()); - // ---- After Swap checks ---- + log::info!("All coinswaps processed successfully. Transaction complete."); + // Shutdown Directory Server directory_server_instance.shutdown.store(true, Relaxed); thread::sleep(Duration::from_secs(10)); + // -------- Fee Tracking and Workflow -------- + // + // Case 1: Maker6102 is the second maker, and the Taker recovers from an initiated swap. + // Workflow: Taker -> Maker16102 -> Maker6102 (CloseAtReqContractSigsForSender) + // + // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | + // |----------------|------------------------|-------------------------|------------|----------------------------|-------------------| + // | **Taker** | _ | 500,000 | _ | 3,000 | 3,000 | + // + // - Taker sends [ProofOfFunding] to Maker16102. + // - Maker16102 responds with [ReqContractSigsAsRecvrAndSender] to the Taker. + // - Taker forwards [ReqContractSigsForSender] to Maker6102, but Maker6102 does not respond, and the Taker recovers from the swap. + // + // Final Outcome for Taker (Recover from Swap): + // + // | Participant | Mining Fee for Contract txes (Sats) | Timelock Fee (Sats) | Funding Fee (Sats) | Total Recovery Fees (Sats) | + // |----------------|------------------------------------|---------------------|--------------------|----------------------------| + // | **Taker** | 3,000 | 768 | 3,000 | 6,768 | + // + // - The Taker regains their initial funding amounts but incurs a total loss of **6,768 sats** due to mining fees. + // + // Case 2: Maker6102 is the first maker. + // Workflow: Taker -> Maker6102 (CloseAtReqContractSigsForSender) + // + // - Taker creates unbroadcasted funding transactions and sends [ReqContractSigsForSender] to Maker6102. + // - Maker6102 does not respond, and the swap fails. + // + // Final Outcome for Taker: + // + // | Participant | Coinswap Outcome (Sats) | + // |----------------|--------------------------| + // | **Taker** | 0 | + // + // Final Outcome for Makers (In both cases): + // + // | Participant | Coinswap Outcome (Sats) | + // |----------------|------------------------------------------| + // | **Maker6102** | 0 (Marked as a bad maker by the Taker) | + // | **Maker16102** | 0 | + // Maker gets banned for being naughty. - match taker.read().unwrap().config.connection_type { + match taker.config.connection_type { ConnectionType::CLEARNET => { assert_eq!( format!("127.0.0.1:{}", 6102), - taker.read().unwrap().get_bad_makers()[0] - .address - .to_string() + taker.get_bad_makers()[0].address.to_string() ); } #[cfg(feature = "tor")] @@ -228,85 +199,18 @@ fn test_abort_case_2_recover_if_no_makers_found() { onion_addr.pop(); assert_eq!( format!("{}:{}", onion_addr, 6102), - taker.read().unwrap().get_bad_makers()[0] - .address - .to_string() + taker.get_bad_makers()[0].address.to_string() ); } } - all_utxos = taker.read().unwrap().get_wallet().get_all_utxo().unwrap(); - - // Assert that Taker burned the mining fees, - // Makers are fine. - - let new_taker_balance_descriptor_utxo = taker - .read() - .unwrap() - .get_wallet() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let new_taker_balance_swap_coins = taker - .read() - .unwrap() - .get_wallet() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - - let new_taker_balance = new_taker_balance_descriptor_utxo + new_taker_balance_swap_coins; - - // Balance will not differ if the first maker drops and swap doesn't take place. - // The recovery will happen only if the 2nd maker drops, which has 50% probabiltiy. - // Only do this assert if the balance differs, implying that the swap took place. - if new_taker_balance != org_taker_balance { - assert_eq!( - org_taker_balance - new_taker_balance, - Amount::from_sat(6768) - ); - } - makers - .iter() - .zip(org_maker_balances.iter()) - .for_each(|(maker, org_balance)| { - all_utxos = maker.get_wallet().read().unwrap().get_all_utxo().unwrap(); - let maker_balance_fidelity = maker - .get_wallet() - .read() - .unwrap() - .balance_fidelity_bonds(Some(&all_utxos)) - .unwrap(); - let maker_balance_descriptor_utxo = maker - .get_wallet() - .read() - .unwrap() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let maker_balance_swap_coins = maker - .get_wallet() - .read() - .unwrap() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - let maker_balance_live_contract = maker - .get_wallet() - .read() - .unwrap() - .balance_live_contract(Some(&all_utxos)) - .unwrap(); - - let new_balance = maker_balance_descriptor_utxo + maker_balance_swap_coins; - - assert_eq!(org_balance.4 - new_balance, Amount::from_sat(0)); - - assert_eq!(maker_balance_fidelity, Amount::from_btc(0.05).unwrap()); - assert_eq!( - maker_balance_descriptor_utxo, - Amount::from_btc(0.14999).unwrap() - ); - assert_eq!(maker_balance_swap_coins, Amount::from_btc(0.0).unwrap()); - assert_eq!(maker_balance_live_contract, Amount::from_btc(0.0).unwrap()); - }); - + // After Swap checks: + verify_swap_results( + &taker, + &makers, + org_taker_spend_balance, + org_maker_spend_balances, + ); test_framework.stop(); block_generation_handle.join().unwrap(); } diff --git a/tests/abort2_case3.rs b/tests/abort2_case3.rs index 50137e84..05d85f36 100644 --- a/tests/abort2_case3.rs +++ b/tests/abort2_case3.rs @@ -5,7 +5,7 @@ use coinswap::{ taker::SwapParams, utill::ConnectionType, }; - +use std::sync::Arc; mod test_framework; use coinswap::taker::TakerBehavior; use log::{info, warn}; @@ -33,7 +33,7 @@ fn maker_drops_after_sending_senders_sigs() { // Initiate test framework, Makers. // Taker has normal behavior. - let (test_framework, taker, makers, directory_server_instance, block_generation_handle) = + let (test_framework, mut taker, makers, directory_server_instance, block_generation_handle) = TestFramework::init( makers_config_map.into(), TakerBehavior::Normal, @@ -44,49 +44,26 @@ fn maker_drops_after_sending_senders_sigs() { "Running Test: Maker 6102 Closes after sending sender's signature. This is really bad. Recovery is the only option." ); - let bitcoind = &test_framework.bitcoind; - info!("Initiating Takers..."); - // Fund the Taker and Makers with 3 utxos of 0.05 btc each. - for _ in 0..3 { - let taker_address = taker - .write() - .unwrap() - .get_wallet_mut() - .get_next_external_address() - .unwrap(); - - send_to_address(bitcoind, &taker_address, Amount::from_btc(0.05).unwrap()); - - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - } - - // Coins for fidelity creation - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - - // confirm balances - generate_blocks(bitcoind, 1); + // Fund the Taker with 3 utxos of 0.05 btc each and do basic checks on the balance + let org_taker_spend_balance = fund_and_verify_taker( + &mut taker, + &test_framework.bitcoind, + 3, + Amount::from_btc(0.05).unwrap(), + ); - // ---- Start Servers and attempt Swap ---- + // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. + let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); + fund_and_verify_maker( + makers_ref, + &test_framework.bitcoind, + 4, + Amount::from_btc(0.05).unwrap(), + ); + // Start the Maker Server threads info!("Initiating Maker..."); - // Start the Maker server threads + let maker_threads = makers .iter() .map(|maker| { @@ -97,62 +74,126 @@ fn maker_drops_after_sending_senders_sigs() { }) .collect::>(); - // Start swap - // Makers take time to fully setup. - makers.iter().for_each(|maker| { - while !maker.is_setup_complete.load(Relaxed) { - log::info!("Waiting for maker setup completion"); - // Introduce a delay of 10 seconds to prevent write lock starvation. - thread::sleep(Duration::from_secs(10)); - continue; - } - }); + let org_maker_spend_balances = makers + .iter() + .map(|maker| { + while !maker.is_setup_complete.load(Relaxed) { + info!("Waiting for maker setup completion"); + // Introduce a delay of 10 seconds to prevent write lock starvation. + thread::sleep(Duration::from_secs(10)); + continue; + } + + // Check balance after setting up maker server. + let wallet = maker.wallet.read().unwrap(); + let all_utxos = wallet.get_all_utxo().unwrap(); + + let seed_balance = wallet.balance_descriptor_utxo(Some(&all_utxos)).unwrap(); + + let fidelity_balance = wallet.balance_fidelity_bonds(Some(&all_utxos)).unwrap(); + let swapcoin_balance = wallet.balance_swap_coins(Some(&all_utxos)).unwrap(); + + let live_contract_balance = wallet.balance_live_contract(Some(&all_utxos)).unwrap(); + + assert_eq!(seed_balance, Amount::from_btc(0.14999).unwrap()); + assert_eq!(fidelity_balance, Amount::from_btc(0.05).unwrap()); + assert_eq!(swapcoin_balance, Amount::ZERO); + assert_eq!(live_contract_balance, Amount::ZERO); + + seed_balance + swapcoin_balance + }) + .collect::>(); + + // Initiate Coinswap + info!("Initiating coinswap protocol"); + + // Swap params for coinswap. let swap_params = SwapParams { send_amount: Amount::from_sat(500000), maker_count: 2, tx_count: 3, required_confirms: 1, }; + taker.do_coinswap(swap_params).unwrap(); - info!("Initiating coinswap protocol"); - // Spawn a Taker coinswap thread. - let taker_clone = taker.clone(); - let taker_thread = thread::spawn(move || { - taker_clone - .write() - .unwrap() - .do_coinswap(swap_params) - .unwrap(); - }); - - // Wait for Taker swap thread to conclude. - taker_thread.join().unwrap(); - - // Wait for Maker threads to conclude. + // After Swap is done, wait for maker threads to conclude. makers .iter() .for_each(|maker| maker.shutdown.store(true, Relaxed)); + maker_threads .into_iter() .for_each(|thread| thread.join().unwrap()); - // ---- After Swap checks ---- + info!("All coinswaps processed successfully. Transaction complete."); + // Shutdown Directory Server directory_server_instance.shutdown.store(true, Relaxed); thread::sleep(Duration::from_secs(10)); - // TODO: Do balance asserts - // Maker gets banned for being naughty. - match taker.read().unwrap().config.connection_type { + // -------- Fee Tracking and Workflow -------- + // Case 1: Taker recovers from initiated swap with Maker6102 (CloseAtProofOfFunding) + + // + // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | + // |----------------|------------------------|-------------------------|------------|----------------------------|-------------------| + // | **Taker** | _ | 500,000 | _ | 3,000 | 3,000 | + + // - Taker sends [ReqContractSigsForSender] to Maker6102, Maker6102 responds with signatures. + // - Taker forwards [ProofOfFunding], but Maker6102 doesn't respond, leading to swap recovery. + + // + // Final Outcome for Taker (Recover from Swap): + // + // | Participant | Mining Fee for Contract txes (Sats) | Timelock Fee (Sats) | Funding Fee (Sats) | Total Recovery Fees (Sats) | + // |----------------|------------------------------------|---------------------|--------------------|----------------------------| + // | **Taker** | 3,000 | 768 | 3,000 | 6,768 | + + // Taker recovers initial funding but incurs 6,768 sats in mining fees. + + // + // Final Outcome for Makers (Case 1): + // + // | Participant | Coinswap Outcome (Sats) | + // |----------------|------------------------------------------| + // | **Maker6102** | 0 (Bad maker marked by Taker) | + // | **Maker16102** | 0 + + //------------------------------------------------------------------------------------------------------------------------------------------------------- + + // Case 2: Taker -> Maker16102 -> Maker6102 (CloseAtProofOfFunding) + + // + // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | + // |----------------|------------------------|-------------------------|------------|----------------------------|-------------------| + // | **Taker** | _ | 500,000 | _ | 3,000 | 3,000 | + // | **Maker16102** | 500,000 | 463,500 | 33,500 | 3,000 | 36,500 | + + // Maker6102 reaches CloseAtProofOfFunding state, Maker16102 and Taker regain funding but incur total loss of 6,768 sats. + + // + // Final Outcome for Maker6102: + // + // | Participant | Coinswap Outcome (Sats) | + // |----------------|------------------------------------------| + // | **Maker6102** | 0 (Bad maker marked by Taker) | + + // Final Outcome for Maker16102 and Taker: + // + // | Participant | Mining Fee for Contract txes (Sats) | Timelock Fee (Sats) | Funding Fee (Sats) | Total Recovery Fees (Sats) | + // |----------------|------------------------------------|---------------------|--------------------|----------------------------| + // | **Taker** | 3,000 | 768 | 3,000 | 6,768 | + // | **Maker16102** | 3,000 | 768 | 3,000 | 6,768 | + + // Maker6102 gets banned for being naughty. + match taker.config.connection_type { ConnectionType::CLEARNET => { assert_eq!( format!("127.0.0.1:{}", 6102), - taker.read().unwrap().get_bad_makers()[0] - .address - .to_string() + taker.get_bad_makers()[0].address.to_string() ); } #[cfg(feature = "tor")] @@ -165,13 +206,21 @@ fn maker_drops_after_sending_senders_sigs() { onion_addr.pop(); assert_eq!( format!("{}:{}", onion_addr, 6102), - taker.read().unwrap().get_bad_makers()[0] - .address - .to_string() + taker.get_bad_makers()[0].address.to_string() ); } } + // After Swap checks: + verify_swap_results( + &taker, + &makers, + org_taker_spend_balance, + org_maker_spend_balances, + ); + + info!("All checks successful. Terminating integration test case"); + test_framework.stop(); block_generation_handle.join().unwrap(); } diff --git a/tests/abort3_case1.rs b/tests/abort3_case1.rs index de41d4b3..fc9558f9 100644 --- a/tests/abort3_case1.rs +++ b/tests/abort3_case1.rs @@ -7,17 +7,21 @@ use coinswap::{ }; mod test_framework; -use test_framework::*; - use log::{info, warn}; use std::{ - fs::File, io::Read, path::PathBuf, sync::atomic::Ordering::Relaxed, thread, time::Duration, + fs::File, + io::Read, + path::PathBuf, + sync::{atomic::Ordering::Relaxed, Arc}, + thread, + time::Duration, }; +use test_framework::*; /// ABORT 3: Maker Drops After Setup /// Case 1: CloseAtContractSigsForRecvrAndSender /// -/// Maker closes connection after receiving a `ContractSigsForRecvrAndSender` and doesn't broadcasts it's funding txs. +/// Maker closes connection after receiving a `RespContractSigsForRecvrAndSender` and doesn't broadcasts it's funding txs. /// Taker wait until a timeout (10ses for test, 5mins for prod) and starts recovery after that. // This is problematic. Needs more detailed thought. #[test] @@ -35,7 +39,7 @@ fn abort3_case1_close_at_contract_sigs_for_recvr_and_sender() { // Initiate test framework, Makers. // Taker has normal behavior. - let (test_framework, taker, makers, directory_server_instance, block_generation_handle) = + let (test_framework, mut taker, makers, directory_server_instance, block_generation_handle) = TestFramework::init( makers_config_map.into(), TakerBehavior::Normal, @@ -44,48 +48,26 @@ fn abort3_case1_close_at_contract_sigs_for_recvr_and_sender() { warn!("Running Test: Maker closes connection after receiving a ContractSigsForRecvrAndSender"); - let bitcoind = &test_framework.bitcoind; - - info!("Initiating Takers..."); - // Fund the Taker and Makers with 3 utxos of 0.05 btc each. - for _ in 0..3 { - let taker_address = taker - .write() - .unwrap() - .get_wallet_mut() - .get_next_external_address() - .unwrap(); - - send_to_address(bitcoind, &taker_address, Amount::from_btc(0.05).unwrap()); - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - } - - // Coins for fidelity creation - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - - // confirm balances - generate_blocks(bitcoind, 1); - - // ---- Start Servers and attempt Swap ---- - + // Fund the Taker with 3 utxos of 0.05 btc each and do basic checks on the balance + let org_taker_spend_balance = fund_and_verify_taker( + &mut taker, + &test_framework.bitcoind, + 3, + Amount::from_btc(0.05).unwrap(), + ); + + // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. + let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); + fund_and_verify_maker( + makers_ref, + &test_framework.bitcoind, + 4, + Amount::from_btc(0.05).unwrap(), + ); + + // Start the Maker Server threads info!("Initiating Maker..."); - // Start the Maker server threads + let maker_threads = makers .iter() .map(|maker| { @@ -96,63 +78,126 @@ fn abort3_case1_close_at_contract_sigs_for_recvr_and_sender() { }) .collect::>(); - info!("Initiating coinswap protocol"); + // Makers take time to fully setup. + let org_maker_spend_balances = makers + .iter() + .map(|maker| { + while !maker.is_setup_complete.load(Relaxed) { + info!("Waiting for maker setup completion"); + // Introduce a delay of 10 seconds to prevent write lock starvation. + thread::sleep(Duration::from_secs(10)); + continue; + } - // Start swap + // Check balance after setting up maker server. + let wallet = maker.wallet.read().unwrap(); + let all_utxos = wallet.get_all_utxo().unwrap(); - // Makers take time to fully setup. - makers.iter().for_each(|maker| { - while !maker.is_setup_complete.load(Relaxed) { - log::info!("Waiting for maker setup completion"); - // Introduce a delay of 10 seconds to prevent write lock starvation. - thread::sleep(Duration::from_secs(10)); - continue; - } - }); + let seed_balance = wallet.balance_descriptor_utxo(Some(&all_utxos)).unwrap(); + let fidelity_balance = wallet.balance_fidelity_bonds(Some(&all_utxos)).unwrap(); + + let swapcoin_balance = wallet.balance_swap_coins(Some(&all_utxos)).unwrap(); + + let live_contract_balance = wallet.balance_live_contract(Some(&all_utxos)).unwrap(); + + assert_eq!(seed_balance, Amount::from_btc(0.14999).unwrap()); + assert_eq!(fidelity_balance, Amount::from_btc(0.05).unwrap()); + assert_eq!(swapcoin_balance, Amount::ZERO); + assert_eq!(live_contract_balance, Amount::ZERO); + + seed_balance + swapcoin_balance + }) + .collect::>(); + + // Initiate Coinswap + info!("Initiating coinswap protocol"); + + // Swap params for coinswap. let swap_params = SwapParams { send_amount: Amount::from_sat(500000), maker_count: 2, tx_count: 3, required_confirms: 1, }; + taker.do_coinswap(swap_params).unwrap(); - // Spawn a Taker coinswap thread. - let taker_clone = taker.clone(); - let taker_thread = thread::spawn(move || { - taker_clone - .write() - .unwrap() - .do_coinswap(swap_params) - .unwrap(); - }); - - // Wait for Taker swap thread to conclude. - taker_thread.join().unwrap(); - - // Wait for Maker threads to conclude. + // After Swap is done, wait for maker threads to conclude. makers .iter() .for_each(|maker| maker.shutdown.store(true, Relaxed)); + maker_threads .into_iter() .for_each(|thread| thread.join().unwrap()); - // ---- After Swap checks ---- + info!("All coinswaps processed successfully. Transaction complete."); + // Shutdown Directory Server directory_server_instance.shutdown.store(true, Relaxed); thread::sleep(Duration::from_secs(10)); - // TODO: Do balance asserts - // Maker gets banned for being naughty. - match taker.read().unwrap().config.connection_type { + // -------- Fee Tracking and Workflow -------- + // + // Case 1: Maker6102 is the First Maker, and the Taker recovers from an initiated swap. + // Workflow: Taker -> Maker6102 (CloseAtContractSigsForRecvrAndSender) + // + // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | + // |----------------|------------------------|-------------------------|------------|----------------------------|-------------------| + // | **Taker** | _ | 500,000 | _ | 3,000 | 3,000 | + // + // - Taker forwards [ProofOfFunding] to Maker6102, receives [ReqContractSigsAsRecvrAndSender]. + // - Maker6102 reaches CloseAtContractSigsForRecvrAndSender and doesn’t broadcast funding tx. + // - Taker recovers from the swap. + // + // Final Outcome for Taker (Recover from Swap): + // + // | Participant | Mining Fee for Contract txes (Sats) | Timelock Fee (Sats) | Funding Fee (Sats) | Total Recovery Fees (Sats) | + // |----------------|------------------------------------|---------------------|--------------------|----------------------------| + // | **Taker** | 3,000 | 768 | 3,000 | 6,768 | + // + // - Taker recovers funds but loses **6,768 sats** in mining fees. + // + // Final Outcome for Makers (Case 1): + // + // | Participant | Coinswap Outcome (Sats) | + // |----------------|------------------------------------------| + // | **Maker6102** | 0 (Marked as a bad maker by the Taker) | + // | **Maker16102** | 0 | + // + // Case 2: Maker6102 is the Second Maker. + // Workflow: Taker -> Maker16102 -> Maker6102 (CloseAtContractSigsForRecvrAndSender) + // + // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | + // |----------------|------------------------|-------------------------|------------|----------------------------|-------------------| + // | **Taker** | _ | 500,000 | _ | 3,000 | 3,000 | + // | **Maker16102** | 500,000 | 463,500 | 33,500 | 3,000 | 36,500 | + // + // - Maker6102 receives [ProofOfFunding] of Maker16102, sends [ReqContractSigsAsRecvrAndSender]. + // - Maker6102 reaches CloseAtContractSigsForRecvrAndSender and doesn’t broadcast funding tx. + // + // - After timeout, Taker and Maker16102 recover funds but lose **6,768 sats** each in fees. + // + // Final Outcome for Maker6102: + // + // | Participant | Coinswap Outcome (Sats) | + // |----------------|------------------------------------------| + // | **Maker6102** | 0 (Marked as a bad maker by the Taker) | + // + // Final Outcome for Maker16102 and Taker: + // + // | Participant | Mining Fee for Contract txes (Sats) | Timelock Fee (Sats) | Funding Fee (Sats) | Total Recovery Fees (Sats) | + // |----------------|------------------------------------|---------------------|--------------------|----------------------------| + // | **Taker** | 3,000 | 768 | 3,000 | 6,768 | + // | **Maker16102** | 3,000 | 768 | 3,000 | 6,768 | + + // Maker6102 gets banned for being naughty. + match taker.config.connection_type { ConnectionType::CLEARNET => { assert_eq!( format!("127.0.0.1:{}", 6102), - taker.read().unwrap().get_bad_makers()[0] - .address - .to_string() + taker.get_bad_makers()[0].address.to_string() ); } #[cfg(feature = "tor")] @@ -165,13 +210,21 @@ fn abort3_case1_close_at_contract_sigs_for_recvr_and_sender() { onion_addr.pop(); assert_eq!( format!("{}:{}", onion_addr, 6102), - taker.read().unwrap().get_bad_makers()[0] - .address - .to_string() + taker.get_bad_makers()[0].address.to_string() ); } } + // After Swap checks: + verify_swap_results( + &taker, + &makers, + org_taker_spend_balance, + org_maker_spend_balances, + ); + + info!("All checks successful. Terminating integration test case"); + test_framework.stop(); block_generation_handle.join().unwrap(); } diff --git a/tests/abort3_case2.rs b/tests/abort3_case2.rs index e657d588..a99f60f2 100644 --- a/tests/abort3_case2.rs +++ b/tests/abort3_case2.rs @@ -5,6 +5,7 @@ use coinswap::{ taker::{SwapParams, TakerBehavior}, utill::ConnectionType, }; +use std::sync::Arc; mod test_framework; use test_framework::*; @@ -32,7 +33,7 @@ fn abort3_case2_close_at_contract_sigs_for_recvr() { // Initiate test framework, Makers. // Taker has normal behavior. - let (test_framework, taker, makers, directory_server_instance, block_generation_handle) = + let (test_framework, mut taker, makers, directory_server_instance, block_generation_handle) = TestFramework::init( makers_config_map.into(), TakerBehavior::Normal, @@ -40,49 +41,27 @@ fn abort3_case2_close_at_contract_sigs_for_recvr() { ); warn!("Running Test: Maker closes connection after sending a ContractSigsForRecvr"); - let bitcoind = &test_framework.bitcoind; - - info!("Initiating Takers..."); - // Fund the Taker and Makers with 3 utxos of 0.05 btc each. - for _ in 0..3 { - let taker_address = taker - .write() - .unwrap() - .get_wallet_mut() - .get_next_external_address() - .unwrap(); - - send_to_address(bitcoind, &taker_address, Amount::from_btc(0.05).unwrap()); - - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - } - - // Coins for fidelity creation - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - - // confirm balances - generate_blocks(bitcoind, 1); - - // ---- Start Servers and attempt Swap ---- + // Fund the Taker with 3 utxos of 0.05 btc each and do basic checks on the balance + let org_taker_spend_balance = fund_and_verify_taker( + &mut taker, + &test_framework.bitcoind, + 3, + Amount::from_btc(0.05).unwrap(), + ); + + // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. + let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); + fund_and_verify_maker( + makers_ref, + &test_framework.bitcoind, + 4, + Amount::from_btc(0.05).unwrap(), + ); + + // Start the Maker Server threads info!("Initiating Maker..."); - // Start the Maker server threads + let maker_threads = makers .iter() .map(|maker| { @@ -93,63 +72,110 @@ fn abort3_case2_close_at_contract_sigs_for_recvr() { }) .collect::>(); - info!("Initiating coinswap protocol"); + // Makers take time to fully setup. + let org_maker_spend_balances = makers + .iter() + .map(|maker| { + while !maker.is_setup_complete.load(Relaxed) { + info!("Waiting for maker setup completion"); + // Introduce a delay of 10 seconds to prevent write lock starvation. + thread::sleep(Duration::from_secs(10)); + continue; + } - // Start swap + // Check balance after setting up maker server. + let wallet = maker.wallet.read().unwrap(); + let all_utxos = wallet.get_all_utxo().unwrap(); - // Makers take time to fully setup. - makers.iter().for_each(|maker| { - while !maker.is_setup_complete.load(Relaxed) { - log::info!("Waiting for maker setup completion"); - // Introduce a delay of 10 seconds to prevent write lock starvation. - thread::sleep(Duration::from_secs(10)); - continue; - } - }); + let seed_balance = wallet.balance_descriptor_utxo(Some(&all_utxos)).unwrap(); + + let fidelity_balance = wallet.balance_fidelity_bonds(Some(&all_utxos)).unwrap(); + + let swapcoin_balance = wallet.balance_swap_coins(Some(&all_utxos)).unwrap(); + let live_contract_balance = wallet.balance_live_contract(Some(&all_utxos)).unwrap(); + + assert_eq!(seed_balance, Amount::from_btc(0.14999).unwrap()); + assert_eq!(fidelity_balance, Amount::from_btc(0.05).unwrap()); + assert_eq!(swapcoin_balance, Amount::ZERO); + assert_eq!(live_contract_balance, Amount::ZERO); + + seed_balance + swapcoin_balance + }) + .collect::>(); + + // Initiate Coinswap + info!("Initiating coinswap protocol"); + + // Swap params for coinswap. let swap_params = SwapParams { send_amount: Amount::from_sat(500000), maker_count: 2, tx_count: 3, required_confirms: 1, }; + taker.do_coinswap(swap_params).unwrap(); - // Spawn a Taker coinswap thread. - let taker_clone = taker.clone(); - let taker_thread = thread::spawn(move || { - taker_clone - .write() - .unwrap() - .do_coinswap(swap_params) - .unwrap(); - }); - - // Wait for Taker swap thread to conclude. - taker_thread.join().unwrap(); - - // Wait for Maker threads to conclude. + // After Swap is done, wait for maker threads to conclude. makers .iter() .for_each(|maker| maker.shutdown.store(true, Relaxed)); + maker_threads .into_iter() .for_each(|thread| thread.join().unwrap()); - // ---- After Swap checks ---- + info!("All coinswaps processed successfully. Transaction complete."); + // Shutdown Directory Server directory_server_instance.shutdown.store(true, Relaxed); thread::sleep(Duration::from_secs(10)); - // TODO: Do balance asserts + // -------- Fee Tracking and Workflow -------- + // + // Case 1: Maker6102 is the First Maker. + // Workflow: Taker -> Maker6102 (CloseAtContractSigsForRecvr) -----> Maker16102 + // + // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | + // |----------------|------------------------|-------------------------|------------|----------------------------|-------------------| + // | **Taker** | _ | 500,000 | _ | 3,000 | 3,000 | + // | **Maker6102** | 500,000 | 463,500 | 33,500 | 3,000 | 36,500 | + // + // - Taker sends [ProofOfFunding] of Maker6102 to Maker16102, who replies with [ReqContractSigsForRecvrAndSender]. + // - Taker forwards [ReqContractSigsForRecvr] to Maker6102, but Maker6102 doesn't respond. + // - After a timeout, both Taker and Maker6102 recover from the swap, incurring losses. + // + // Final Outcome for Taker & Maker6102 (Recover from Swap): + // + // | Participant | Mining Fee for Contract txes (Sats) | Timelock Fee (Sats) | Funding Fee (Sats) | Total Recovery Fees (Sats) | + // |-----------------------------------------------------|------------------------------------|---------------------|--------------------|----------------------------| + // | **Taker** | 3,000 | 768 | 3,000 | 6,768 | + // | **Maker6102** (Marked as a bad maker by the Taker) | 3,000 | 768 | 3,000 | 6,768 | + // + // - Both **Taker** and **Maker6102** regain their initial funding amounts but incur a total loss of **6,768 sats** due to mining fees. + // + // Final Outcome for Maker16102: + // + // | Participant | Coinswap Outcome (Sats) | + // |----------------|------------------------------------------| + // | **Maker16102** | 0 | + // + // ------------------------------------------------------------------------------------------------------------------------ + // + // Case 2: Maker6102 is the Second Maker. + // Workflow: Taker -> Maker16102 -> Maker6102 (CloseAtContractSigsForRecvr) + // + // In this case, the Coinswap completes successfully since Maker6102, being the last maker, does not receive [ReqContractSigsForRecvr] from the Taker. + // + // The Fee balance would look like `standard_swap` IT for this case. + // Maker gets banned for being naughty. - match taker.read().unwrap().config.connection_type { + match taker.config.connection_type { ConnectionType::CLEARNET => { assert_eq!( format!("127.0.0.1:{}", 6102), - taker.read().unwrap().get_bad_makers()[0] - .address - .to_string() + taker.get_bad_makers()[0].address.to_string() ); } #[cfg(feature = "tor")] @@ -162,13 +188,21 @@ fn abort3_case2_close_at_contract_sigs_for_recvr() { onion_addr.pop(); assert_eq!( format!("{}:{}", onion_addr, 6102), - taker.read().unwrap().get_bad_makers()[0] - .address - .to_string() + taker.get_bad_makers()[0].address.to_string() ); } } + // After Swap checks: + verify_swap_results( + &taker, + &makers, + org_taker_spend_balance, + org_maker_spend_balances, + ); + + info!("All checks successful. Terminating integration test case"); + test_framework.stop(); block_generation_handle.join().unwrap(); } diff --git a/tests/abort3_case3.rs b/tests/abort3_case3.rs index e72f0b22..fd6848b7 100644 --- a/tests/abort3_case3.rs +++ b/tests/abort3_case3.rs @@ -5,14 +5,13 @@ use coinswap::{ taker::{SwapParams, TakerBehavior}, utill::ConnectionType, }; +use std::sync::Arc; mod test_framework; use test_framework::*; use log::{info, warn}; -use std::{ - fs::File, io::Read, path::PathBuf, sync::atomic::Ordering::Relaxed, thread, time::Duration, -}; +use std::{sync::atomic::Ordering::Relaxed, thread, time::Duration}; /// ABORT 3: Maker Drops After Setup /// Case 3: CloseAtHashPreimage @@ -32,7 +31,7 @@ fn abort3_case3_close_at_hash_preimage_handover() { // Initiate test framework, Makers. // Taker has normal behavior. - let (test_framework, taker, makers, directory_server_instance, block_generation_handle) = + let (test_framework, mut taker, makers, directory_server_instance, block_generation_handle) = TestFramework::init( makers_config_map.into(), TakerBehavior::Normal, @@ -40,47 +39,27 @@ fn abort3_case3_close_at_hash_preimage_handover() { ); warn!("Running Test: Maker closes conneciton at hash preimage handling"); - let bitcoind = &test_framework.bitcoind; - info!("Initiating Takers..."); - // Fund the Taker and Makers with 3 utxos of 0.05 btc each. - for _ in 0..3 { - let taker_address = taker - .write() - .unwrap() - .get_wallet_mut() - .get_next_external_address() - .unwrap(); - - send_to_address(bitcoind, &taker_address, Amount::from_btc(0.05).unwrap()); - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - } - - // Coins for fidelity creation - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - - // confirm balances - generate_blocks(bitcoind, 1); - - // ---- Start Servers and attempt Swap ---- + // Fund the Taker with 3 utxos of 0.05 btc each and do basic checks on the balance + let org_taker_spend_balance = fund_and_verify_taker( + &mut taker, + &test_framework.bitcoind, + 3, + Amount::from_btc(0.05).unwrap(), + ); + + // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. + let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); + fund_and_verify_maker( + makers_ref, + &test_framework.bitcoind, + 4, + Amount::from_btc(0.05).unwrap(), + ); + + // Start the Maker Server threads info!("Initiating Maker..."); - // Start the Maker server threads + let maker_threads = makers .iter() .map(|maker| { @@ -91,81 +70,106 @@ fn abort3_case3_close_at_hash_preimage_handover() { }) .collect::>(); - info!("Initiating coinswap protocol"); + // Makers take time to fully setup. + let org_maker_spend_balances = makers + .iter() + .map(|maker| { + while !maker.is_setup_complete.load(Relaxed) { + info!("Waiting for maker setup completion"); + // Introduce a delay of 10 seconds to prevent write lock starvation. + thread::sleep(Duration::from_secs(10)); + continue; + } - // Start swap + // Check balance after setting up maker server. + let wallet = maker.wallet.read().unwrap(); + let all_utxos = wallet.get_all_utxo().unwrap(); - // Makers take time to fully setup. - makers.iter().for_each(|maker| { - while !maker.is_setup_complete.load(Relaxed) { - log::info!("Waiting for maker setup completion"); - // Introduce a delay of 10 seconds to prevent write lock starvation. - thread::sleep(Duration::from_secs(10)); - continue; - } - }); + let seed_balance = wallet.balance_descriptor_utxo(Some(&all_utxos)).unwrap(); + + let fidelity_balance = wallet.balance_fidelity_bonds(Some(&all_utxos)).unwrap(); + + let swapcoin_balance = wallet.balance_swap_coins(Some(&all_utxos)).unwrap(); + + let live_contract_balance = wallet.balance_live_contract(Some(&all_utxos)).unwrap(); + assert_eq!(seed_balance, Amount::from_btc(0.14999).unwrap()); + assert_eq!(fidelity_balance, Amount::from_btc(0.05).unwrap()); + assert_eq!(swapcoin_balance, Amount::ZERO); + assert_eq!(live_contract_balance, Amount::ZERO); + + seed_balance + swapcoin_balance + }) + .collect::>(); + + // Initiate Coinswap + info!("Initiating coinswap protocol"); + + // Swap params for coinswap. let swap_params = SwapParams { send_amount: Amount::from_sat(500000), maker_count: 2, tx_count: 3, required_confirms: 1, }; + taker.do_coinswap(swap_params).unwrap(); - // Spawn a Taker coinswap thread. - let taker_clone = taker.clone(); - let taker_thread = thread::spawn(move || { - taker_clone - .write() - .unwrap() - .do_coinswap(swap_params) - .unwrap(); - }); - - // Wait for Taker swap thread to conclude. - taker_thread.join().unwrap(); - - // Wait for Maker threads to conclude. + // After Swap is done, wait for maker threads to conclude. makers .iter() .for_each(|maker| maker.shutdown.store(true, Relaxed)); + maker_threads .into_iter() .for_each(|thread| thread.join().unwrap()); - // ---- After Swap checks ---- + info!("All coinswaps processed successfully. Transaction complete."); + // Shutdown Directory Server directory_server_instance.shutdown.store(true, Relaxed); thread::sleep(Duration::from_secs(10)); - // TODO: Do balance asserts - // Maker gets banned for being naughty. - match taker.read().unwrap().config.connection_type { - ConnectionType::CLEARNET => { - assert_eq!( - format!("127.0.0.1:{}", 6102), - taker.read().unwrap().get_bad_makers()[0] - .address - .to_string() - ); - } - #[cfg(feature = "tor")] - ConnectionType::TOR => { - let onion_addr_path = - PathBuf::from(format!("/tmp/tor-rust-maker{}/hs-dir/hostname", 6102)); - let mut file = File::open(onion_addr_path).unwrap(); - let mut onion_addr: String = String::new(); - file.read_to_string(&mut onion_addr).unwrap(); - onion_addr.pop(); - assert_eq!( - format!("{}:{}", onion_addr, 6102), - taker.read().unwrap().get_bad_makers()[0] - .address - .to_string() - ); - } - } + //-------- Fee Tracking and Workflow:-------------------------------------------------------------------------- + // + // This fee scenario would occur in both cases whether Maker6102 is the first or last maker. + + // Case 1: Maker6102 is the first maker + // Workflow: Taker -> Maker6102(CloseAtHashPreimage) -> Maker16102 + // + // + // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | + // |----------------|------------------------|-------------------------|------------|----------------------------|-------------------| + // | **Taker** | _ | 500,000 | _ | 3,000 | 3,000 | + // | **Maker16102** | 500,000 | 463,500 | 33,500 | 3,000 | 36,500 | + // | **Maker6102** | 463,500 | 438,642 | 21,858 | 3,000 | 24,858 | + // + // Maker6102 => DropConnectionAfterFullSetup + // + // Participants regain their initial funding amounts but incur a total loss of **6,768 sats** + // due to mining fees (recovery + initial transaction fees). + // + // | Participant | Mining Fee for Contract txes (Sats) | Timelock Fee (Sats) | Funding Fee (Sats) | Total Recovery Fees (Sats) | + // |----------------|------------------------------------|---------------------|--------------------|----------------------------| + // | **Taker** | 3,000 | 768 | 3,000 | 6,768 | + // | **Maker16102** | 3,000 | 768 | 3,000 | 6,768 | + // | **Maker6102** | 3,000 | 768 | 3,000 | 6,768 | + // + // Case 2: Maker16102 is the last maker. + // Workflow: Taker -> Maker16102 -> Maker16102(CloseAtHashPreimage) + // + // Same as Case 1. + //----------------------------------------------------------------------------------------------------------------------------------------------- + + // After Swap checks: + verify_swap_results( + &taker, + &makers, + org_taker_spend_balance, + org_maker_spend_balances, + ); + + info!("All checks successful. Terminating integration test case"); test_framework.stop(); block_generation_handle.join().unwrap(); diff --git a/tests/dns.rs b/tests/dns.rs index 005a14c5..fc76fa29 100644 --- a/tests/dns.rs +++ b/tests/dns.rs @@ -3,7 +3,7 @@ use std::{io::Write, net::TcpStream, process::Command, thread, time::Duration}; mod test_framework; -use coinswap::utill::{ConnectionType, DnsRequest}; +use coinswap::utill::DnsRequest; use test_framework::{init_bitcoind, start_dns}; fn send_addresses(addresses: &[&str]) { @@ -56,7 +56,7 @@ fn test_dns() { let data_dir = temp_dir.join("dns"); - let mut process = start_dns(&data_dir, ConnectionType::CLEARNET, &bitcoind); + let mut process = start_dns(&data_dir, &bitcoind); let initial_addresses = vec!["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"]; send_addresses(&initial_addresses); @@ -67,7 +67,7 @@ fn test_dns() { process.kill().expect("Failed to kill directoryd process"); process.wait().unwrap(); - let mut process = start_dns(&data_dir, ConnectionType::CLEARNET, &bitcoind); + let mut process = start_dns(&data_dir, &bitcoind); let additional_addresses = vec!["127.0.0.1:8083", "127.0.0.1:8084"]; send_addresses(&additional_addresses); @@ -76,7 +76,7 @@ fn test_dns() { process.kill().expect("Failed to kill directoryd process"); process.wait().unwrap(); - let mut process = start_dns(&data_dir, ConnectionType::CLEARNET, &bitcoind); + let mut process = start_dns(&data_dir, &bitcoind); let all_addresses = vec![ "127.0.0.1:8080", "127.0.0.1:8081", diff --git a/tests/maker_cli.rs b/tests/maker_cli.rs index f07f4a53..9130994f 100644 --- a/tests/maker_cli.rs +++ b/tests/maker_cli.rs @@ -2,7 +2,7 @@ #![cfg(feature = "integration-test")] use bitcoin::{Address, Amount}; use bitcoind::{bitcoincore_rpc::RpcApi, BitcoinD}; -use coinswap::utill::{setup_logger, ConnectionType}; +use coinswap::utill::setup_logger; use std::{ fs, io::{BufRead, BufReader}, @@ -51,8 +51,6 @@ impl MakerCli { .args([ "--data-directory", self.data_dir.to_str().unwrap(), - "--network", - "clearnet", "-a", &rpc_auth, "-r", @@ -144,7 +142,7 @@ fn test_maker_cli() { let maker_cli = MakerCli::new(); let dns_dir = maker_cli.data_dir.parent().unwrap(); - let mut directoryd_proc = start_dns(dns_dir, ConnectionType::CLEARNET, &maker_cli.bitcoind); + let mut directoryd_proc = start_dns(dns_dir, &maker_cli.bitcoind); let (rx, mut makerd_proc) = maker_cli.start_makerd(); // Ping check @@ -160,7 +158,7 @@ fn test_maker_cli() { // Tor address check let tor_addr = maker_cli.execute_maker_cli(&["get-tor-address"]); await_message(&rx, "RPC request received: GetTorAddress"); - assert_eq!(tor_addr, "Maker is not running on TOR"); + assert!(tor_addr.contains("onion:6102")); // Initial Balance checks let seed_balance = maker_cli.execute_maker_cli(&["seed-balance"]); diff --git a/tests/malice1.rs b/tests/malice1.rs index 44262a16..ab34eda1 100644 --- a/tests/malice1.rs +++ b/tests/malice1.rs @@ -5,14 +5,13 @@ use coinswap::{ taker::{SwapParams, TakerBehavior}, utill::ConnectionType, }; +use std::sync::Arc; mod test_framework; use test_framework::*; use log::{info, warn}; -use std::{ - assert_eq, collections::BTreeSet, sync::atomic::Ordering::Relaxed, thread, time::Duration, -}; +use std::{assert_eq, sync::atomic::Ordering::Relaxed, thread, time::Duration}; /// Malice 1: Taker Broadcasts contract transactions prematurely. /// @@ -29,7 +28,7 @@ fn malice1_taker_broadcast_contract_prematurely() { // Initiate test framework, Makers. // Taker has normal behavior. - let (test_framework, taker, makers, directory_server_instance, block_generation_handle) = + let (test_framework, mut taker, makers, directory_server_instance, block_generation_handle) = TestFramework::init( makers_config_map.into(), TakerBehavior::BroadcastContractAfterFullSetup, @@ -38,78 +37,26 @@ fn malice1_taker_broadcast_contract_prematurely() { warn!("Running Test: Taker broadcasts contract transaction prematurely"); - let bitcoind = &test_framework.bitcoind; - - info!("Initiating Takers..."); - // Fund the Taker and Makers with 3 utxos of 0.05 btc each. - for _ in 0..3 { - let taker_address = taker - .write() - .unwrap() - .get_wallet_mut() - .get_next_external_address() - .unwrap(); - - send_to_address(bitcoind, &taker_address, Amount::from_btc(0.05).unwrap()); - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - } - - // Coins for fidelity creation - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - - // confirm balances - generate_blocks(bitcoind, 1); - - let mut all_utxos = taker.read().unwrap().get_wallet().get_all_utxo().unwrap(); - - // Get the original balances - let org_taker_balance_fidelity = taker - .read() - .unwrap() - .get_wallet() - .balance_fidelity_bonds(Some(&all_utxos)) - .unwrap(); - let org_taker_balance_descriptor_utxo = taker - .read() - .unwrap() - .get_wallet() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let org_taker_balance_swap_coins = taker - .read() - .unwrap() - .get_wallet() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - let org_taker_balance_live_contract = taker - .read() - .unwrap() - .get_wallet() - .balance_live_contract(Some(&all_utxos)) - .unwrap(); + // Fund the Taker with 3 utxos of 0.05 btc each and do basic checks on the balance + let org_taker_spend_balance = fund_and_verify_taker( + &mut taker, + &test_framework.bitcoind, + 3, + Amount::from_btc(0.05).unwrap(), + ); - let org_taker_balance = org_taker_balance_descriptor_utxo + org_taker_balance_swap_coins; + // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. + let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); + fund_and_verify_maker( + makers_ref, + &test_framework.bitcoind, + 4, + Amount::from_btc(0.05).unwrap(), + ); - // ---- Start Servers and attempt Swap ---- + // Start the Maker Server threads + log::info!("Initiating Maker..."); - info!("Initiating Maker..."); - // Start the Maker server threads let maker_threads = makers .iter() .map(|maker| { @@ -120,203 +67,93 @@ fn malice1_taker_broadcast_contract_prematurely() { }) .collect::>(); - info!("Initiating coinswap protocol"); - // Start swap - // Makers take time to fully setup. - makers.iter().for_each(|maker| { - while !maker.is_setup_complete.load(Relaxed) { - log::info!("Waiting for maker setup completion"); - // Introduce a delay of 10 seconds to prevent write lock starvation. - thread::sleep(Duration::from_secs(10)); - continue; - } - }); - - let swap_params = SwapParams { - send_amount: Amount::from_sat(500000), - maker_count: 2, - tx_count: 3, - required_confirms: 1, - }; - - // Calculate Original balance excluding fidelity bonds. - // Bonds are created automatically after spawning the maker server. - let org_maker_balances = makers + let org_maker_spend_balances = makers .iter() .map(|maker| { - all_utxos = maker.get_wallet().read().unwrap().get_all_utxo().unwrap(); - let maker_balance_fidelity = maker - .get_wallet() - .read() - .unwrap() - .balance_fidelity_bonds(Some(&all_utxos)) - .unwrap(); - let maker_balance_descriptor_utxo = maker - .get_wallet() - .read() - .unwrap() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let maker_balance_swap_coins = maker - .get_wallet() - .read() - .unwrap() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - let maker_balance_live_contract = maker - .get_wallet() - .read() - .unwrap() - .balance_live_contract(Some(&all_utxos)) - .unwrap(); + while !maker.is_setup_complete.load(Relaxed) { + log::info!("Waiting for maker setup completion"); + // Introduce a delay of 10 seconds to prevent write lock starvation. + thread::sleep(Duration::from_secs(10)); + continue; + } - assert_eq!(maker_balance_fidelity, Amount::from_btc(0.05).unwrap()); - assert_eq!( - maker_balance_descriptor_utxo, - Amount::from_btc(0.14999).unwrap() - ); - assert_eq!(maker_balance_swap_coins, Amount::from_btc(0.0).unwrap()); - assert_eq!(maker_balance_live_contract, Amount::from_btc(0.0).unwrap()); + // Check balance after setting up maker server. + let wallet = maker.wallet.read().unwrap(); + let all_utxos = wallet.get_all_utxo().unwrap(); - maker_balance_descriptor_utxo + maker_balance_swap_coins + let seed_balance = wallet.balance_descriptor_utxo(Some(&all_utxos)).unwrap(); + + let fidelity_balance = wallet.balance_fidelity_bonds(Some(&all_utxos)).unwrap(); + + let swapcoin_balance = wallet.balance_swap_coins(Some(&all_utxos)).unwrap(); + + let live_contract_balance = wallet.balance_live_contract(Some(&all_utxos)).unwrap(); + + assert_eq!(seed_balance, Amount::from_btc(0.14999).unwrap()); + assert_eq!(fidelity_balance, Amount::from_btc(0.05).unwrap()); + assert_eq!(swapcoin_balance, Amount::ZERO); + assert_eq!(live_contract_balance, Amount::ZERO); + + seed_balance + swapcoin_balance }) - .collect::>(); + .collect::>(); - // Spawn a Taker coinswap thread. - let taker_clone = taker.clone(); - let taker_thread = thread::spawn(move || { - taker_clone - .write() - .unwrap() - .do_coinswap(swap_params) - .unwrap(); - }); + // Initiate Coinswap + log::info!("Initiating coinswap protocol"); - // Wait for Taker swap thread to conclude. - taker_thread.join().unwrap(); + // Swap params for coinswap. + let swap_params = SwapParams { + send_amount: Amount::from_sat(500000), + maker_count: 2, + tx_count: 3, + required_confirms: 1, + }; + taker.do_coinswap(swap_params).unwrap(); - // Wait for Maker threads to conclude. + // After Swap is done, wait for maker threads to conclude. makers .iter() .for_each(|maker| maker.shutdown.store(true, Relaxed)); + maker_threads .into_iter() .for_each(|thread| thread.join().unwrap()); - // ---- After Swap checks ---- + log::info!("All coinswaps processed successfully. Transaction complete."); + // Shutdown Directory Server directory_server_instance.shutdown.store(true, Relaxed); thread::sleep(Duration::from_secs(10)); - let maker_balances = makers - .iter() - .map(|maker| { - all_utxos = maker.get_wallet().read().unwrap().get_all_utxo().unwrap(); - let maker_balance_fidelity = maker - .get_wallet() - .read() - .unwrap() - .balance_fidelity_bonds(Some(&all_utxos)) - .unwrap(); - let maker_balance_descriptor_utxo = maker - .get_wallet() - .read() - .unwrap() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let maker_balance_swap_coins = maker - .get_wallet() - .read() - .unwrap() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - let maker_balance_live_contract = maker - .get_wallet() - .read() - .unwrap() - .balance_live_contract(Some(&all_utxos)) - .unwrap(); - - assert_eq!(maker_balance_fidelity, Amount::from_btc(0.05).unwrap()); - assert_eq!( - maker_balance_descriptor_utxo, - Amount::from_btc(0.14992232).unwrap() - ); - assert_eq!(maker_balance_swap_coins, Amount::from_btc(0.0).unwrap()); - assert_eq!(maker_balance_live_contract, Amount::from_btc(0.0).unwrap()); - - maker_balance_descriptor_utxo + maker_balance_swap_coins - }) - .collect::>(); - - all_utxos = taker.read().unwrap().get_wallet().get_all_utxo().unwrap(); - - // Check everybody looses mining fees of contract txs. - let taker_balance_fidelity = taker - .read() - .unwrap() - .get_wallet() - .balance_fidelity_bonds(Some(&all_utxos)) - .unwrap(); - let taker_balance_descriptor_utxo = taker - .read() - .unwrap() - .get_wallet() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let taker_balance_swap_coins = taker - .read() - .unwrap() - .get_wallet() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - let taker_balance_live_contract = taker - .read() - .unwrap() - .get_wallet() - .balance_live_contract(Some(&all_utxos)) - .unwrap(); - - let taker_balance = taker_balance_descriptor_utxo + taker_balance_swap_coins; - - assert!(maker_balances.len() == 1); // The set only contains one element, - // assert_eq!(maker_balances.first().unwrap(), &Amount::from_sat(14994773)); - - // Everybody looses 4227 sats for contract transactions. - assert_eq!( - org_maker_balances - .first() - .unwrap() - .checked_sub(*maker_balances.first().unwrap()) - .unwrap(), - Amount::from_sat(6768) - ); - - assert_eq!(org_taker_balance_fidelity, Amount::from_btc(0.0).unwrap()); - assert_eq!( - org_taker_balance_descriptor_utxo, - Amount::from_btc(0.15).unwrap() - ); - assert_eq!( - org_taker_balance_live_contract, - Amount::from_btc(0.0).unwrap() - ); - assert_eq!(org_taker_balance_swap_coins, Amount::from_btc(0.0).unwrap()); - - assert_eq!(taker_balance_fidelity, Amount::from_btc(0.0).unwrap()); - assert_eq!( - taker_balance_descriptor_utxo, - Amount::from_btc(0.14993232).unwrap() - ); - assert_eq!(taker_balance_live_contract, Amount::from_btc(0.0).unwrap()); - assert_eq!(taker_balance_swap_coins, Amount::from_btc(0.0).unwrap()); - - assert_eq!( - org_taker_balance.checked_sub(taker_balance).unwrap(), - Amount::from_sat(6768) + //-------- Fee Tracking and Workflow:------------ + // + // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | + // |----------------|------------------------|-------------------------|------------|----------------------------|-------------------| + // | **Taker** | _ | 500,000 | _ | 3,000 | 3,000 | + // | **Maker16102** | 500,000 | 463,500 | 33,500 | 3,000 | 36,500 | + // | **Maker6102** | 463,500 | 438,642 | 21,858 | 3,000 | 24,858 | + // + // **Taker** => BroadcastContractAfterFullSetup + // + // Participants regain their initial funding amounts but incur a total loss of **6,768 sats** + // due to mining fees (recovery + initial transaction fees). + // + // | Participant | Mining Fee for Contract txes (Sats) | Timelock Fee (Sats) | Funding Fee (Sats) | Total Recovery Fees (Sats) | + // |----------------|------------------------------------|---------------------|--------------------|----------------------------| + // | **Taker** | 3,000 | 768 | 3,000 | 6,768 | + // | **Maker16102** | 3,000 | 768 | 3,000 | 6,768 | + // | **Maker6102** | 3,000 | 768 | 3,000 | 6,768 | + + // After Swap checks: + verify_swap_results( + &taker, + &makers, + org_taker_spend_balance, + org_maker_spend_balances, ); + info!("All checks successful. Terminating integration test case"); test_framework.stop(); block_generation_handle.join().unwrap(); diff --git a/tests/malice2.rs b/tests/malice2.rs index d71ba2f3..2b361247 100644 --- a/tests/malice2.rs +++ b/tests/malice2.rs @@ -5,11 +5,11 @@ use coinswap::{ taker::{SwapParams, TakerBehavior}, utill::ConnectionType, }; - +use std::sync::Arc; mod test_framework; use test_framework::*; -use std::{collections::BTreeSet, sync::atomic::Ordering::Relaxed, thread, time::Duration}; +use std::{sync::atomic::Ordering::Relaxed, thread, time::Duration}; /// Malice 2: Maker Broadcasts contract transactions prematurely. /// @@ -23,87 +23,39 @@ fn malice2_maker_broadcast_contract_prematurely() { // ---- Setup ---- let makers_config_map = [ - ((6102, None), MakerBehavior::Normal), - ((16102, None), MakerBehavior::BroadcastContractAfterSetup), + ((6102, None), MakerBehavior::BroadcastContractAfterSetup), + ((16102, None), MakerBehavior::Normal), ]; // Initiate test framework, Makers. // Taker has normal behavior. - let (test_framework, taker, makers, directory_server_instance, block_generation_handle) = + let (test_framework, mut taker, makers, directory_server_instance, block_generation_handle) = TestFramework::init( makers_config_map.into(), TakerBehavior::Normal, ConnectionType::CLEARNET, ); - let bitcoind = &test_framework.bitcoind; - - // Fund the Taker and Makers with 3 utxos of 0.05 btc each. - for _ in 0..3 { - let taker_address = taker - .write() - .unwrap() - .get_wallet_mut() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &taker_address, Amount::from_btc(0.05).unwrap()); - - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - } - - // Coins for fidelity creation - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - - // confirm balances - generate_blocks(bitcoind, 1); - - let mut all_utxos = taker.read().unwrap().get_wallet().get_all_utxo().unwrap(); - let org_taker_balance_fidelity = taker - .read() - .unwrap() - .get_wallet() - .balance_fidelity_bonds(Some(&all_utxos)) - .unwrap(); - let org_taker_balance_descriptor_utxo = taker - .read() - .unwrap() - .get_wallet() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let org_taker_balance_swap_coins = taker - .read() - .unwrap() - .get_wallet() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - let org_taker_balance_live_contract = taker - .read() - .unwrap() - .get_wallet() - .balance_live_contract(Some(&all_utxos)) - .unwrap(); + // Fund the Taker with 3 utxos of 0.05 btc each and do basic checks on the balance + let org_taker_spend_balance = fund_and_verify_taker( + &mut taker, + &test_framework.bitcoind, + 3, + Amount::from_btc(0.05).unwrap(), + ); - let org_taker_balance = org_taker_balance_descriptor_utxo + org_taker_balance_swap_coins; + // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. + let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); + fund_and_verify_maker( + makers_ref, + &test_framework.bitcoind, + 4, + Amount::from_btc(0.05).unwrap(), + ); - // ---- Start Servers and attempt Swap ---- + // Start the Maker Server threads + log::info!("Initiating Maker..."); - // Start the Maker server threads let maker_threads = makers .iter() .map(|maker| { @@ -114,201 +66,123 @@ fn malice2_maker_broadcast_contract_prematurely() { }) .collect::>(); - // Start swap - // Makers take time to fully setup. - makers.iter().for_each(|maker| { - while !maker.is_setup_complete.load(Relaxed) { - log::info!("Waiting for maker setup completion"); - // Introduce a delay of 10 seconds to prevent write lock starvation. - thread::sleep(Duration::from_secs(10)); - continue; - } - }); + let org_maker_spend_balances = makers + .iter() + .map(|maker| { + while !maker.is_setup_complete.load(Relaxed) { + log::info!("Waiting for maker setup completion"); + // Introduce a delay of 10 seconds to prevent write lock starvation. + thread::sleep(Duration::from_secs(10)); + continue; + } + + // Check balance after setting up maker server. + let wallet = maker.wallet.read().unwrap(); + let all_utxos = wallet.get_all_utxo().unwrap(); + let seed_balance = wallet.balance_descriptor_utxo(Some(&all_utxos)).unwrap(); + + let fidelity_balance = wallet.balance_fidelity_bonds(Some(&all_utxos)).unwrap(); + + let swapcoin_balance = wallet.balance_swap_coins(Some(&all_utxos)).unwrap(); + + let live_contract_balance = wallet.balance_live_contract(Some(&all_utxos)).unwrap(); + + assert_eq!(seed_balance, Amount::from_btc(0.14999).unwrap()); + assert_eq!(fidelity_balance, Amount::from_btc(0.05).unwrap()); + assert_eq!(swapcoin_balance, Amount::ZERO); + assert_eq!(live_contract_balance, Amount::ZERO); + + seed_balance + swapcoin_balance + }) + .collect::>(); + + // Initiate Coinswap + log::info!("Initiating coinswap protocol"); + + // Swap params for coinswap. let swap_params = SwapParams { send_amount: Amount::from_sat(500000), maker_count: 2, tx_count: 3, required_confirms: 1, }; + taker.do_coinswap(swap_params).unwrap(); - // Calculate Original balance excluding fidelity bonds. - // Bonds are created automatically after spawning the maker server. - let org_maker_balances = makers - .iter() - .map(|maker| { - all_utxos = maker.get_wallet().read().unwrap().get_all_utxo().unwrap(); - let maker_balance_fidelity = maker - .get_wallet() - .read() - .unwrap() - .balance_fidelity_bonds(Some(&all_utxos)) - .unwrap(); - let maker_balance_descriptor_utxo = maker - .get_wallet() - .read() - .unwrap() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let maker_balance_swap_coins = maker - .get_wallet() - .read() - .unwrap() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - let maker_balance_live_contract = maker - .get_wallet() - .read() - .unwrap() - .balance_live_contract(Some(&all_utxos)) - .unwrap(); - - assert_eq!(maker_balance_fidelity, Amount::from_btc(0.05).unwrap()); - assert_eq!( - maker_balance_descriptor_utxo, - Amount::from_btc(0.14999).unwrap() - ); - assert_eq!(maker_balance_swap_coins, Amount::from_btc(0.0).unwrap()); - assert_eq!(maker_balance_live_contract, Amount::from_btc(0.0).unwrap()); - maker_balance_descriptor_utxo + maker_balance_swap_coins - }) - .collect::>(); - - // Spawn a Taker coinswap thread. - let taker_clone = taker.clone(); - let taker_thread = thread::spawn(move || { - taker_clone - .write() - .unwrap() - .do_coinswap(swap_params) - .unwrap(); - }); - - // Wait for Taker swap thread to conclude. - taker_thread.join().unwrap(); - - // Wait for Maker threads to conclude. + // After Swap is done, wait for maker threads to conclude. makers .iter() .for_each(|maker| maker.shutdown.store(true, Relaxed)); + maker_threads .into_iter() .for_each(|thread| thread.join().unwrap()); - // ---- After Swap checks ---- + log::info!("All coinswaps processed successfully. Transaction complete."); + // Shutdown Directory Server directory_server_instance.shutdown.store(true, Relaxed); thread::sleep(Duration::from_secs(10)); - let maker_balances = makers - .iter() - .map(|maker| { - all_utxos = maker.get_wallet().read().unwrap().get_all_utxo().unwrap(); - let maker_balance_fidelity = maker - .get_wallet() - .read() - .unwrap() - .balance_fidelity_bonds(Some(&all_utxos)) - .unwrap(); - let maker_balance_descriptor_utxo = maker - .get_wallet() - .read() - .unwrap() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let maker_balance_swap_coins = maker - .get_wallet() - .read() - .unwrap() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - let maker_balance_live_contract = maker - .get_wallet() - .read() - .unwrap() - .balance_live_contract(Some(&all_utxos)) - .unwrap(); - - assert_eq!(maker_balance_fidelity, Amount::from_btc(0.05).unwrap()); - // If the first maker misbehaves, then the 2nd maker doesn't loose anything. - // as they haven't broadcasted their outgoing swap. - assert!( - maker_balance_descriptor_utxo == Amount::from_btc(0.14992232).unwrap() - || maker_balance_descriptor_utxo == Amount::from_btc(0.14999).unwrap() - ); - assert_eq!(maker_balance_swap_coins, Amount::from_btc(0.0).unwrap()); - assert_eq!(maker_balance_live_contract, Amount::from_btc(0.0).unwrap()); - - maker_balance_descriptor_utxo + maker_balance_swap_coins - }) - .collect::>(); - - all_utxos = taker.read().unwrap().get_wallet().get_all_utxo().unwrap(); - - let taker_balance_fidelity = taker - .read() - .unwrap() - .get_wallet() - .balance_fidelity_bonds(Some(&all_utxos)) - .unwrap(); - let taker_balance_descriptor_utxo = taker - .read() - .unwrap() - .get_wallet() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let taker_balance_swap_coins = taker - .read() - .unwrap() - .get_wallet() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - let taker_balance_live_contract = taker - .read() - .unwrap() - .get_wallet() - .balance_live_contract(Some(&all_utxos)) - .unwrap(); - - let taker_balance = taker_balance_descriptor_utxo + taker_balance_swap_coins; - - assert_eq!(org_taker_balance_fidelity, Amount::from_btc(0.0).unwrap()); - assert_eq!( - org_taker_balance_descriptor_utxo, - Amount::from_btc(0.15).unwrap() - ); - assert_eq!( - org_taker_balance_live_contract, - Amount::from_btc(0.0).unwrap() + // -------- Fee Tracking and Workflow -------- + // + // Case 1: Maker6102 is the First Maker. + // Workflow: Taker -> Maker6102 (BroadcastContractAfterSetup) -> Maker16102 + // + // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | + // |----------------|------------------------|-------------------------|------------|----------------------------|-------------------| + // | **Taker** | _ | 500,000 | _ | 3,000 | 3,000 | + // | **Maker6102** | 500,000 | 463,500 | 33,500 | 3,000 | 36,500 | + // + // Maker6102 => BroadcastContractAfterSetup + // + // Seeing those contract txes, the Taker recovers from the swap. + // Taker and Maker6102 recover funds but lose **6,768 sats** each in fees. + // + // Final Outcome for Taker & Maker6102: + // | Participant | Mining Fee for Contract txes (Sats) | Timelock Fee (Sats) | Funding Fee (Sats) | Total Recovery Fees (Sats) | + // |----------------|------------------------------------|---------------------|--------------------|----------------------------| + // | **Taker** | 3,000 | 768 | 3,000 | 6,768 | + // | **Maker6102** | 3,000 | 768 | 3,000 | 6,768 | + // + // Final Outcome for Maker16102: + // | Participant | Coinswap Outcome (Sats) | + // |----------------|--------------------------| + // | **Maker16102** | 0 | + // + // ------------------------------------------------------------------------------------------------------------------------ + // + // Case 2: Maker6102 is the Last Maker. + // Workflow: Taker -> Maker16102 -> Maker6102 (BroadcastContractAfterSetup) + // + // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | + // |----------------|------------------------|-------------------------|------------|----------------------------|-------------------| + // | **Taker** | _ | 500,000 | _ | 3,000 | 3,000 | + // | **Maker16102** | 500,000 | 463,500 | 33,500 | 3,000 | 36,500 | + // | **Maker6102** | 463,500 | 438,642 | 21,858 | 3,000 | 24,858 | + // + // Maker6102 => BroadcastContractAfterSetup + // + // Participants regain their initial funding amounts but incur a total loss of **6,768 sats** + // due to mining fees (recovery + initial transaction fees). + // + // | Participant | Mining Fee for Contract txes (Sats) | Timelock Fee (Sats) | Funding Fee (Sats) | Total Recovery Fees (Sats) | + // |----------------|------------------------------------|---------------------|--------------------|----------------------------| + // | **Taker** | 3,000 | 768 | 3,000 | 6,768 | + // | **Maker16102** | 3,000 | 768 | 3,000 | 6,768 | + // | **Maker6102** | 3,000 | 768 | 3,000 | 6,768 | + + // After Swap checks: + verify_swap_results( + &taker, + &makers, + org_taker_spend_balance, + org_maker_spend_balances, ); - assert_eq!(org_taker_balance_swap_coins, Amount::from_btc(0.0).unwrap()); - assert_eq!(taker_balance_fidelity, Amount::from_btc(0.0).unwrap()); - assert_eq!( - taker_balance_descriptor_utxo, - Amount::from_btc(0.14993232).unwrap() - ); - assert_eq!(taker_balance_live_contract, Amount::from_btc(0.0).unwrap()); - assert_eq!(taker_balance_swap_coins, Amount::from_btc(0.0).unwrap()); - - assert_eq!(*maker_balances.first().unwrap(), Amount::from_sat(14992232)); - - // Everybody looses 4227 sats for contract transactions. - assert_eq!( - org_maker_balances - .first() - .unwrap() - .checked_sub(*maker_balances.first().unwrap()) - .unwrap(), - Amount::from_sat(6768) - ); - - assert_eq!( - org_taker_balance.checked_sub(taker_balance).unwrap(), - Amount::from_sat(6768) - ); + log::info!("All checks successful. Terminating integration test case"); test_framework.stop(); block_generation_handle.join().unwrap(); diff --git a/tests/standard_swap.rs b/tests/standard_swap.rs index d7ee3165..e70194a7 100644 --- a/tests/standard_swap.rs +++ b/tests/standard_swap.rs @@ -6,6 +6,7 @@ use coinswap::{ utill::ConnectionType, wallet::{Destination, SendAmount}, }; +use std::sync::Arc; use bitcoind::bitcoincore_rpc::RpcApi; @@ -34,7 +35,7 @@ fn test_standard_coinswap() { }; // Initiate test framework, Makers and a Taker with default behavior. - let (test_framework, taker, makers, directory_server_instance, block_generation_handle) = + let (test_framework, mut taker, makers, directory_server_instance, block_generation_handle) = TestFramework::init( makers_config_map.into(), TakerBehavior::Normal, @@ -42,155 +43,19 @@ fn test_standard_coinswap() { ); warn!("Running Test: Standard Coinswap Procedure"); - let bitcoind = &test_framework.bitcoind; - info!("Initiating Takers..."); - // Fund the Taker and Makers with 3 utxos of 0.05 btc each. - for _ in 0..3 { - let taker_address = taker - .write() - .unwrap() - .get_wallet_mut() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &taker_address, Amount::from_btc(0.05).unwrap()); - - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - } - - // Coins for fidelity creation - makers.iter().for_each(|maker| { - let maker_addrs = maker - .get_wallet() - .write() - .unwrap() - .get_next_external_address() - .unwrap(); - send_to_address(bitcoind, &maker_addrs, Amount::from_btc(0.05).unwrap()); - }); - - // confirm balances - generate_blocks(bitcoind, 1); - // --- Basic Checks ---- - - // Assert external address index reached to 4. - assert_eq!(taker.read().unwrap().get_wallet().get_external_index(), &3); - makers.iter().for_each(|maker| { - let next_external_index = *maker.get_wallet().read().unwrap().get_external_index(); - assert_eq!(next_external_index, 4); - }); - - // Check if utxo list looks good. - // TODO: Assert other interesting things from the utxo list. - - let mut all_utxos = taker.read().unwrap().get_wallet().get_all_utxo().unwrap(); - - let taker_no_of_descriptor_utxo_unspent = taker - .read() - .unwrap() - .get_wallet() - .list_descriptor_utxo_spend_info(Some(&all_utxos)) - .unwrap() - .len(); - - let taker_no_of_fidelity_unspent = taker - .read() - .unwrap() - .get_wallet() - .list_fidelity_spend_info(Some(&all_utxos)) - .unwrap() - .len(); - let taker_no_of_swap_coin_unspent = taker - .read() - .unwrap() - .get_wallet() - .list_swap_coin_utxo_spend_info(Some(&all_utxos)) - .unwrap() - .len(); - - let taker_no_of_live_contract_unspent = taker - .read() - .unwrap() - .get_wallet() - .list_live_contract_spend_info(Some(&all_utxos)) - .unwrap() - .len(); - - assert_eq!(taker_no_of_descriptor_utxo_unspent, 3); - assert_eq!(taker_no_of_fidelity_unspent, 0); - assert_eq!(taker_no_of_swap_coin_unspent, 0); - assert_eq!(taker_no_of_live_contract_unspent, 0); - - makers.iter().for_each(|maker| { - all_utxos = maker.get_wallet().read().unwrap().get_all_utxo().unwrap(); - - let maker_no_of_descriptor_utxo_unspent = maker - .get_wallet() - .read() - .unwrap() - .list_descriptor_utxo_spend_info(Some(&all_utxos)) - .unwrap() - .len(); - - let maker_no_of_fidelity_unspent = maker - .get_wallet() - .read() - .unwrap() - .list_fidelity_spend_info(Some(&all_utxos)) - .unwrap() - .len(); - - let maker_no_of_swap_coin_unspent = maker - .get_wallet() - .read() - .unwrap() - .list_swap_coin_utxo_spend_info(Some(&all_utxos)) - .unwrap() - .len(); - - let maker_no_of_live_contract_unspent = maker - .get_wallet() - .read() - .unwrap() - .list_live_contract_spend_info(Some(&all_utxos)) - .unwrap() - .len(); - - assert_eq!(maker_no_of_descriptor_utxo_unspent, 4); - assert_eq!(maker_no_of_fidelity_unspent, 0); - assert_eq!(maker_no_of_swap_coin_unspent, 0); - assert_eq!(maker_no_of_live_contract_unspent, 0); - }); - - // Check locking non-wallet utxos worked. - taker - .read() - .unwrap() - .get_wallet() - .lock_unspendable_utxos() - .unwrap(); - makers.iter().for_each(|maker| { - maker - .get_wallet() - .read() - .unwrap() - .lock_unspendable_utxos() - .unwrap(); - }); - - // ---- Start Servers and attempt Swap ---- - - info!("Initiating Maker..."); - // Start the Maker server threads + // Fund the Taker with 3 utxos of 0.05 btc each and do basic checks on the balance + let org_taker_spend_balance = + fund_and_verify_taker(&mut taker, bitcoind, 3, Amount::from_btc(0.05).unwrap()); + + // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. + let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); + fund_and_verify_maker(makers_ref, bitcoind, 4, Amount::from_btc(0.05).unwrap()); + + // Start the Maker Server threads + log::info!("Initiating Maker..."); + let maker_threads = makers .iter() .map(|maker| { @@ -201,199 +66,106 @@ fn test_standard_coinswap() { }) .collect::>(); - // Start swap - // Makers take time to fully setup. - makers.iter().for_each(|maker| { - while !maker.is_setup_complete.load(Relaxed) { - log::info!("Waiting for maker setup completion"); - // Introduce a delay of 10 seconds to prevent write lock starvation. - thread::sleep(Duration::from_secs(10)); - continue; - } - }); + let org_maker_spend_balances = makers + .iter() + .map(|maker| { + while !maker.is_setup_complete.load(Relaxed) { + log::info!("Waiting for maker setup completion"); + // Introduce a delay of 10 seconds to prevent write lock starvation. + thread::sleep(Duration::from_secs(10)); + continue; + } + + // Check balance after setting up maker server. + let wallet = maker.wallet.read().unwrap(); + let all_utxos = wallet.get_all_utxo().unwrap(); + + let seed_balance = wallet.balance_descriptor_utxo(Some(&all_utxos)).unwrap(); + let fidelity_balance = wallet.balance_fidelity_bonds(Some(&all_utxos)).unwrap(); + + let swapcoin_balance = wallet.balance_swap_coins(Some(&all_utxos)).unwrap(); + + let live_contract_balance = wallet.balance_live_contract(Some(&all_utxos)).unwrap(); + + assert_eq!(seed_balance, Amount::from_btc(0.14999).unwrap()); + assert_eq!(fidelity_balance, Amount::from_btc(0.05).unwrap()); + assert_eq!(swapcoin_balance, Amount::ZERO); + assert_eq!(live_contract_balance, Amount::ZERO); + + seed_balance + swapcoin_balance + }) + .collect::>(); + + // Initiate Coinswap + log::info!("Initiating coinswap protocol"); + + // Swap params for coinswap. let swap_params = SwapParams { send_amount: Amount::from_sat(500000), maker_count: 2, tx_count: 3, required_confirms: 1, }; + taker.do_coinswap(swap_params).unwrap(); - info!("Initiating coinswap protocol"); - // Spawn a Taker coinswap thread. - let taker_clone = taker.clone(); - let taker_thread = thread::spawn(move || { - taker_clone - .write() - .unwrap() - .do_coinswap(swap_params) - .unwrap(); - }); - - // Wait for Taker swap thread to conclude. - taker_thread.join().unwrap(); - - // Wait for Maker threads to conclude. + // After Swap is done, wait for maker threads to conclude. makers .iter() .for_each(|maker| maker.shutdown.store(true, Relaxed)); + maker_threads .into_iter() .for_each(|thread| thread.join().unwrap()); - info!("All coinswaps processed successfully. Transaction complete."); + log::info!("All coinswaps processed successfully. Transaction complete."); + // Shutdown Directory Server directory_server_instance.shutdown.store(true, Relaxed); thread::sleep(Duration::from_secs(10)); - // ---- After Swap Asserts ---- - - info!("Final Balance Checks for process"); - // Check everybody hash 6 swapcoins. - assert_eq!(taker.read().unwrap().get_wallet().get_swapcoins_count(), 6); - makers.iter().for_each(|maker| { - let swapcoin_count = maker.get_wallet().read().unwrap().get_swapcoins_count(); - assert_eq!(swapcoin_count, 6); - }); - - // Check balances makes sense - all_utxos = taker.read().unwrap().get_wallet().get_all_utxo().unwrap(); - assert_eq!(all_utxos.len(), 12); - - let taker_spendable_bal = taker - .read() - .unwrap() - .get_wallet() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap() - + taker - .read() - .unwrap() - .get_wallet() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - - assert_eq!(taker_spendable_bal, Amount::from_btc(0.1498284).unwrap()); - - let taker_balance_fidelity = taker - .read() - .unwrap() - .get_wallet() - .balance_fidelity_bonds(Some(&all_utxos)) - .unwrap(); - let taker_balance_descriptor_utxo = taker - .read() - .unwrap() - .get_wallet() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let taker_balance_swap_coins = taker - .read() - .unwrap() - .get_wallet() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - let taker_balance_live_contract = taker - .read() - .unwrap() - .get_wallet() - .balance_live_contract(Some(&all_utxos)) - .unwrap(); - assert_eq!( - taker_balance_fidelity - + taker_balance_descriptor_utxo - + taker_balance_swap_coins - + taker_balance_live_contract, - Amount::from_btc(0.1498284).unwrap() - ); - assert_eq!( - taker_balance_descriptor_utxo, - Amount::from_btc(0.14497).unwrap() - ); - assert_eq!( - taker_balance_swap_coins, - Amount::from_btc(0.0048584).unwrap() + //-------- Fee Tracking and Workflow:------------ + // + // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | + // |----------------|------------------------|-------------------------|------------|----------------------------|-------------------| + // | **Taker** | _ | 500,000 | _ | 3,000 | 3,000 | + // | **Maker16102** | 500,000 | 463,500 | 33,500 | 3,000 | 36,500 | + // | **Maker6102** | 463,500 | 438,642 | 21,858 | 3,000 | 24,858 | + // + // ## 3. Final Outcome for Taker (Successful Coinswap): + // + // | Participant | Coinswap Outcome (Sats) | + // |---------------|---------------------------------------------------------------------------| + // | **Taker** | 438,642= 500,000 - (Total Fees for Maker16102 + Total Fees for Maker6102) | + // + // ## 4. Final Outcome for Makers: + // + // | Participant | Coinswap Outcome (Sats) | + // |----------------|-------------------------------------------------------------------| + // | **Maker16102** | 500,000 - 463,500 - 3,000 = +33,500 | + // | **Maker6102** | 465,384 - 438,642 - 3,000 = +21,858 | + + // After Swap Asserts + verify_swap_results( + &taker, + &makers, + org_taker_spend_balance, + org_maker_spend_balances, ); - assert_eq!(taker_balance_fidelity, Amount::from_btc(0.0).unwrap()); - assert_eq!(taker_balance_live_contract, Amount::from_btc(0.0).unwrap()); - - makers.iter().for_each(|maker| { - all_utxos = maker.get_wallet().read().unwrap().get_all_utxo().unwrap(); - assert_eq!(all_utxos.len(), 10); - - let maker_balance_fidelity = maker - .get_wallet() - .read() - .unwrap() - .balance_fidelity_bonds(Some(&all_utxos)) - .unwrap(); - let maker_balance_descriptor_utxo = maker - .get_wallet() - .read() - .unwrap() - .balance_descriptor_utxo(Some(&all_utxos)) - .unwrap(); - let maker_balance_swap_coins = maker - .get_wallet() - .read() - .unwrap() - .balance_swap_coins(Some(&all_utxos)) - .unwrap(); - let maker_balance_live_contract = maker - .get_wallet() - .read() - .unwrap() - .balance_live_contract(Some(&all_utxos)) - .unwrap(); - - let maker_total_balance = maker.get_wallet().read().unwrap().balance().unwrap(); - - assert!( - maker_total_balance == Amount::from_btc(0.20003044).unwrap() - || maker_total_balance == Amount::from_btc(0.20003116).unwrap(), - "maker total balance didn't match any of the expected values" - ); - - assert!( - maker_balance_descriptor_utxo == Amount::from_btc(0.14503116).unwrap() - || maker_balance_descriptor_utxo == Amount::from_btc(0.1451016).unwrap(), - "maker_balance_descriptor_utxo does not match any of the expected values" - ); - - assert!( - maker_balance_swap_coins == Amount::from_btc(0.00492884).unwrap() - || maker_balance_swap_coins == Amount::from_btc(0.005).unwrap(), - "maker_balance_swap_coins does not match any of the expected values" - ); - assert_eq!(maker_balance_fidelity, Amount::from_btc(0.05).unwrap()); - assert_eq!(maker_balance_live_contract, Amount::from_btc(0.0).unwrap()); - - let maker_spendable_balance = maker_balance_descriptor_utxo + maker_balance_swap_coins; - - assert!( - maker_spendable_balance == Amount::from_btc(0.15003116).unwrap() - || maker_spendable_balance == Amount::from_btc(0.15003044).unwrap(), - "maker spendable balance didn't match any of the expected values" - ); - }); info!("Balance check successful."); // Check spending from swapcoins. info!("Checking Spend from Swapcoin"); - let swap_coins = taker - .read() - .unwrap() - .get_wallet() + + let taker_wallet_mut = taker.get_wallet_mut(); + let swap_coins = taker_wallet_mut .list_swap_coin_utxo_spend_info(None) .unwrap(); - let tx = taker - .write() - .unwrap() - .get_wallet_mut() + let tx = taker_wallet_mut .spend_from_wallet( Amount::from_sat(1000), SendAmount::Max, @@ -411,18 +183,11 @@ fn test_standard_coinswap() { bitcoind.client.send_raw_transaction(&tx).unwrap(); generate_blocks(bitcoind, 1); - taker.write().unwrap().get_wallet_mut().sync().unwrap(); - - let taker_read = taker.read().unwrap(); - - let swap_coin_bal = taker_read.get_wallet().balance_swap_coins(None).unwrap(); - let descriptor_bal = taker_read - .get_wallet() - .balance_descriptor_utxo(None) - .unwrap(); + let swap_coin_bal = taker_wallet_mut.balance_swap_coins(None).unwrap(); + let descriptor_bal = taker_wallet_mut.balance_descriptor_utxo(None).unwrap(); assert_eq!(swap_coin_bal, Amount::ZERO); - assert_eq!(descriptor_bal, Amount::from_btc(0.1498184).unwrap()); + assert_eq!(descriptor_bal, Amount::from_btc(0.14934642).unwrap()); info!("All checks successful. Terminating integration test case"); diff --git a/tests/taker_cli.rs b/tests/taker_cli.rs index e6f71cc0..9ead2e4b 100644 --- a/tests/taker_cli.rs +++ b/tests/taker_cli.rs @@ -36,8 +36,6 @@ impl TakerCli { self.data_dir.to_str().unwrap(), "--bitcoin-network", "regtest", - "--connection-type", - "clearnet", ]; // RPC authentication (user:password) from the cookie file diff --git a/tests/test_framework/mod.rs b/tests/test_framework/mod.rs index 31bfdeb3..0041c819 100644 --- a/tests/test_framework/mod.rs +++ b/tests/test_framework/mod.rs @@ -11,14 +11,14 @@ //! The test data also includes the backend bitcoind data-directory, which is useful for observing the blockchain states after a swap. //! //! Checkout `tests/standard_swap.rs` for example of simple coinswap simulation test between 1 Taker and 2 Makers. +use bitcoin::Amount; use std::{ - collections::HashMap, env::{self, consts}, fs, path::{Path, PathBuf}, sync::{ atomic::{AtomicBool, Ordering::Relaxed}, - Arc, RwLock, + Arc, }, thread::{self, JoinHandle}, time::Duration, @@ -113,21 +113,14 @@ pub fn await_message(rx: &Receiver, expected_message: &str) { // Start the DNS server based on given connection type and considers data directory for the server. #[allow(dead_code)] -pub fn start_dns( - data_dir: &std::path::Path, - conn_type: ConnectionType, - bitcoind: &BitcoinD, -) -> process::Child { +pub fn start_dns(data_dir: &std::path::Path, bitcoind: &BitcoinD) -> process::Child { let (stdout_sender, stdout_recv): (Sender, Receiver) = mpsc::channel(); let (stderr_sender, stderr_recv): (Sender, Receiver) = mpsc::channel(); - let conn_type = format!("{}", conn_type); let mut args = vec![ "--data-directory", data_dir.to_str().unwrap(), - "--network", - &conn_type, "--rpc_network", "regtest", ]; @@ -184,6 +177,198 @@ pub fn start_dns( directoryd_process } +#[allow(dead_code)] +pub fn fund_and_verify_taker( + taker: &mut Taker, + bitcoind: &BitcoinD, + utxo_count: u32, + utxo_value: Amount, +) -> Amount { + log::info!("Funding Takers..."); + + // Fund the Taker with 3 utxos of 0.05 btc each. + for _ in 0..utxo_count { + let taker_address = taker.get_wallet_mut().get_next_external_address().unwrap(); + send_to_address(bitcoind, &taker_address, utxo_value); + } + + // confirm balances + generate_blocks(bitcoind, 1); + + //------Basic Checks----- + + let wallet = taker.get_wallet(); + // Assert external address index reached to 3. + assert_eq!(wallet.get_external_index(), &utxo_count); + + // Check if utxo list looks good. + // TODO: Assert other interesting things from the utxo list. + + let all_utxos = wallet.get_all_utxo().unwrap(); + + let seed_balance = wallet.balance_descriptor_utxo(Some(&all_utxos)).unwrap(); + + let fidelity_balance = wallet.balance_fidelity_bonds(Some(&all_utxos)).unwrap(); + + let swapcoin_balance = wallet.balance_swap_coins(Some(&all_utxos)).unwrap(); + + let live_contract_balance = wallet.balance_live_contract(Some(&all_utxos)).unwrap(); + + // TODO: Think about this: utxo_count*utxo_amt. + assert_eq!(seed_balance, Amount::from_btc(0.15).unwrap()); + assert_eq!(fidelity_balance, Amount::ZERO); + assert_eq!(swapcoin_balance, Amount::ZERO); + assert_eq!(live_contract_balance, Amount::ZERO); + + seed_balance + swapcoin_balance +} + +#[allow(dead_code)] +pub fn fund_and_verify_maker( + makers: Vec<&Maker>, + bitcoind: &BitcoinD, + utxo_count: u32, + utxo_value: Amount, +) { + // Fund the Maker with 4 utxos of 0.05 btc each. + + log::info!("Funding Makers..."); + + makers.iter().for_each(|&maker| { + // let wallet = maker..write().unwrap(); + let mut wallet_write = maker.wallet.write().unwrap(); + + for _ in 0..utxo_count { + let maker_addr = wallet_write.get_next_external_address().unwrap(); + send_to_address(bitcoind, &maker_addr, utxo_value); + } + }); + + // confirm balances + generate_blocks(bitcoind, 1); + + // --- Basic Checks ---- + makers.iter().for_each(|&maker| { + let wallet = maker.get_wallet().read().unwrap(); + // Assert external address index reached to 4. + assert_eq!(wallet.get_external_index(), &utxo_count); + + let all_utxos = wallet.get_all_utxo().unwrap(); + + let seed_balance = wallet.balance_descriptor_utxo(Some(&all_utxos)).unwrap(); + + let fidelity_balance = wallet.balance_fidelity_bonds(Some(&all_utxos)).unwrap(); + + let swapcoin_balance = wallet.balance_swap_coins(Some(&all_utxos)).unwrap(); + + let live_contract_balance = wallet.balance_live_contract(Some(&all_utxos)).unwrap(); + + // TODO: Think about this: utxo_count*utxo_amt. + assert_eq!(seed_balance, Amount::from_btc(0.20).unwrap()); + assert_eq!(fidelity_balance, Amount::ZERO); + assert_eq!(swapcoin_balance, Amount::ZERO); + assert_eq!(live_contract_balance, Amount::ZERO); + }); +} + +/// Verifies the results of a coinswap for the taker and makers after performing a swap. +#[allow(dead_code)] +pub fn verify_swap_results( + taker: &Taker, + makers: &[Arc], + org_taker_spend_balance: Amount, + org_maker_spend_balances: Vec, +) { + // Check Taker balances + { + let wallet = taker.get_wallet(); + let all_utxos = wallet.get_all_utxo().unwrap(); + let fidelity_balance = wallet.balance_fidelity_bonds(Some(&all_utxos)).unwrap(); + let seed_balance = wallet.balance_descriptor_utxo(Some(&all_utxos)).unwrap(); + let swapcoin_balance = wallet.balance_swap_coins(Some(&all_utxos)).unwrap(); + let live_contract_balance = wallet.balance_live_contract(Some(&all_utxos)).unwrap(); + + let spendable_balance = seed_balance + swapcoin_balance; + + assert!( + seed_balance == Amount::from_btc(0.14497).unwrap() // Successful coinswap + || seed_balance == Amount::from_btc(0.14993232).unwrap() // Recovery via timelock + || seed_balance == Amount::from_btc(0.15).unwrap(), // No spending + "Taker seed balance mismatch" + ); + + assert!( + swapcoin_balance == Amount::from_btc(0.00438642).unwrap() // Successful coinswap + || swapcoin_balance == Amount::ZERO, // Unsuccessful coinswap + "Taker swapcoin balance mismatch" + ); + + assert_eq!(live_contract_balance, Amount::ZERO); + assert_eq!(fidelity_balance, Amount::ZERO); + + // Check balance difference + let balance_diff = org_taker_spend_balance + .checked_sub(spendable_balance) + .unwrap(); + + assert!( + balance_diff == Amount::from_sat(64358) // Successful coinswap + || balance_diff == Amount::from_sat(6768) // Recovery via timelock + || balance_diff == Amount::ZERO, // No spending + "Taker spendable balance change mismatch" + ); + } + + // Check Maker balances + makers + .iter() + .zip(org_maker_spend_balances.iter()) + .for_each(|(maker, org_spend_balance)| { + let wallet = maker.get_wallet().read().unwrap(); + let all_utxos = wallet.get_all_utxo().unwrap(); + let fidelity_balance = wallet.balance_fidelity_bonds(Some(&all_utxos)).unwrap(); + let seed_balance = wallet.balance_descriptor_utxo(Some(&all_utxos)).unwrap(); + let swapcoin_balance = wallet.balance_swap_coins(Some(&all_utxos)).unwrap(); + let live_contract_balance = wallet.balance_live_contract(Some(&all_utxos)).unwrap(); + + let spendable_balance = seed_balance + swapcoin_balance; + + assert!( + seed_balance == Amount::from_btc(0.14557358).unwrap() // First maker on successful coinswap + || seed_balance == Amount::from_btc(0.14532500).unwrap() // Second maker on successful coinswap + || seed_balance == Amount::from_btc(0.14999).unwrap() // No spending + || seed_balance == Amount::from_btc(0.14992232).unwrap(), // Recovery via timelock + "Maker seed balance mismatch" + ); + + assert!( + swapcoin_balance == Amount::from_btc(0.005).unwrap() // First maker + || swapcoin_balance == Amount::from_btc(0.00463500).unwrap() // Second maker + || swapcoin_balance == Amount::ZERO, // No swap or funding tx missing + "Maker swapcoin balance mismatch" + ); + + assert_eq!(fidelity_balance, Amount::from_btc(0.05).unwrap()); + assert_eq!(live_contract_balance, Amount::ZERO); + + // Check spendable balance difference. + let balance_diff = match org_spend_balance.checked_sub(spendable_balance) { + None => spendable_balance.checked_sub(*org_spend_balance).unwrap(), // Successful swap as Makers balance increase by Coinswap fee. + Some(diff) => diff, // No spending or unsuccessful swap + }; + + assert!( + balance_diff == Amount::from_sat(33500) // First maker fee + || balance_diff == Amount::from_sat(21858) // Second maker fee + || balance_diff == Amount::ZERO // No spending + || balance_diff == Amount::from_sat(6768) // Recovery via timelock + || balance_diff == Amount::from_sat(466500) // TODO: Investigate this value + || balance_diff == Amount::from_sat(441642), // TODO: Investigate this value + "Maker spendable balance change mismatch" + ); + }); +} + /// The Test Framework. /// /// Handles initializing, operating and cleaning up of all backend processes. Bitcoind, Taker and Makers. @@ -208,12 +393,12 @@ impl TestFramework { /// If no bitcoind conf is provide a default value will be used. #[allow(clippy::type_complexity)] pub fn init( - makers_config_map: HashMap<(u16, Option), MakerBehavior>, + makers_config_map: Vec<((u16, Option), MakerBehavior)>, taker_behavior: TakerBehavior, connection_type: ConnectionType, ) -> ( Arc, - Arc>, + Taker, Vec>, Arc, JoinHandle<()>, @@ -258,16 +443,15 @@ impl TestFramework { // Create the Taker. let taker_rpc_config = rpc_config.clone(); - let taker = Arc::new(RwLock::new( - Taker::init( - Some(temp_dir.join("taker")), - None, - Some(taker_rpc_config), - taker_behavior, - Some(connection_type), - ) - .unwrap(), - )); + let taker = Taker::init( + Some(temp_dir.join("taker")), + None, + Some(taker_rpc_config), + taker_behavior, + Some(connection_type), + ) + .unwrap(); + let mut base_rpc_port = 3500; // Random port for RPC connection in tests. (Not used) // Create the Makers as per given configuration map. let makers = makers_config_map