diff --git a/src/maker/api.rs b/src/maker/api.rs index 3780d7e0..4dfbf015 100644 --- a/src/maker/api.rs +++ b/src/maker/api.rs @@ -16,10 +16,9 @@ use std::{ use bip39::Mnemonic; use bitcoin::{ - absolute::LockTime, ecdsa::Signature, secp256k1::{self, Secp256k1}, - Amount, OutPoint, PublicKey, ScriptBuf, Transaction, + OutPoint, PublicKey, ScriptBuf, Transaction, }; use bitcoind::bitcoincore_rpc::RpcApi; use std::time::Duration; @@ -239,25 +238,6 @@ impl Maker { &self.wallet } - /// Generates Fidelity bond from existing utxos - /// Errors if not enough balance - pub fn create_fidelity_bond(&self) -> Result<(), MakerError> { - let mut wallet = self.wallet.write()?; - log::info!("Creating Fidelity Bond."); - let fidelity_index = wallet.create_fidelity( - Amount::from_sat(self.config.fidelity_value), - LockTime::from_height(self.config.fidelity_timelock).unwrap(), - )?; - - log::info!("Created new fidelity bond at index: {} ", fidelity_index); - let bond = wallet - .get_fidelity_bonds() - .get(&fidelity_index) - .expect("bond expected"); - log::info!("Bond: {:?}", bond); - Ok(()) - } - /// Checks consistency of the [ProofOfFunding] message and return the Hashvalue /// used in hashlock transaction. pub fn verify_proof_of_funding(&self, message: &ProofOfFunding) -> Result { diff --git a/src/maker/server.rs b/src/maker/server.rs index 2a16ab28..e955cb91 100644 --- a/src/maker/server.rs +++ b/src/maker/server.rs @@ -30,7 +30,7 @@ use crate::{ }, protocol::messages::TakerToMakerMessage, utill::{monitor_log_for_completion, read_message, send_message, ConnectionType}, - wallet::{FidelityError, WalletError}, + wallet::WalletError, }; use crate::maker::error::MakerError; @@ -228,10 +228,10 @@ fn setup_fidelity_bond(maker: &Arc, maker_address: &str) -> Result<(), Ma // Wait for sufficient fund to create fidelity bond. // Hard error if fidelity still can't be created. Err(e) => { - if let WalletError::Fidelity(FidelityError::InsufficientFund { + if let WalletError::InsufficientFund { available, required, - }) = e + } = e { log::warn!("Insufficient fund to create fidelity bond."); let amount = required - available; @@ -380,6 +380,7 @@ fn handle_client( // The main Maker Server process. pub fn start_maker_server(maker: Arc) -> Result<(), MakerError> { + log::info!("Starting Maker Server"); // Initialize network connections. let (maker_address, tor_thread) = network_bootstrap(maker.clone())?; let port = maker.config.port; @@ -414,62 +415,62 @@ pub fn start_maker_server(maker: Arc) -> Result<(), MakerError> { // All thread handles are stored in the thread_pool, which are all joined at server shutdown. let mut thread_pool = Vec::new(); - // 1. Bitcoin Core Connection checker thread. - // Ensures that Bitcoin Core connection is live. - // If not, it will block p2p connections until Core works again. - let maker_clone = maker.clone(); - let acc_client_clone = accepting_clients.clone(); - let conn_check_thread: thread::JoinHandle> = thread::Builder::new() - .name("Bitcoin Core Connection Checker Thread".to_string()) - .spawn(move || { - log::info!("[{}] Spawning Bitcoin Core connection checker thread", port); - check_connection_with_core(maker_clone, acc_client_clone) - })?; - thread_pool.push(conn_check_thread); - - // 2. Idle Client connection checker thread. - // This threads check idelness of peer in live swaps. - // And takes recovery measure if the peer seems to have disappeared in middlle of a swap. - let maker_clone = maker.clone(); - let idle_conn_check_thread = thread::Builder::new() - .name("Idle Client Checker Thread".to_string()) - .spawn(move || { - log::info!( - "[{}] Spawning Client connection status checker thread", - port - ); - check_for_idle_states(maker_clone.clone()) - })?; - thread_pool.push(idle_conn_check_thread); - - // 3. Watchtower thread. - // This thread checks for broadcasted contract transactions, which usually means violation of the protocol. - // When contract transaction detected in mempool it will attempt recovery. - // This can get triggered even when contracts of adjacent hops are published. Implying the whole swap route is disrupted. - let maker_clone = maker.clone(); - let contract_watcher_thread = thread::Builder::new() - .name("Contract Watcher Thread".to_string()) - .spawn(move || { - log::info!("[{}] Spawning contract-watcher thread", port); - check_for_broadcasted_contracts(maker_clone.clone()) - })?; - thread_pool.push(contract_watcher_thread); - - // 4: The RPC server thread. - // User for responding back to `maker-cli` apps. - let maker_clone = maker.clone(); - let rpc_thread = thread::Builder::new() - .name("RPC Thread".to_string()) - .spawn(move || { - log::info!("[{}] Spawning RPC server", port); - start_rpc_server(maker_clone) - })?; - - thread_pool.push(rpc_thread); - - maker.setup_complete()?; - - log::info!("[{}] Maker setup is ready", maker.config.port); + if !*maker.shutdown.read()? { + // 1. Bitcoin Core Connection checker thread. + // Ensures that Bitcoin Core connection is live. + // If not, it will block p2p connections until Core works again. + let maker_clone = maker.clone(); + let acc_client_clone = accepting_clients.clone(); + let conn_check_thread: thread::JoinHandle> = thread::Builder::new() + .name("Bitcoin Core Connection Checker Thread".to_string()) + .spawn(move || { + log::info!("[{}] Spawning Bitcoin Core connection checker thread", port); + check_connection_with_core(maker_clone, acc_client_clone) + })?; + thread_pool.push(conn_check_thread); + + // 2. Idle Client connection checker thread. + // This threads check idelness of peer in live swaps. + // And takes recovery measure if the peer seems to have disappeared in middlle of a swap. + let maker_clone = maker.clone(); + let idle_conn_check_thread = thread::Builder::new() + .name("Idle Client Checker Thread".to_string()) + .spawn(move || { + log::info!( + "[{}] Spawning Client connection status checker thread", + port + ); + check_for_idle_states(maker_clone.clone()) + })?; + thread_pool.push(idle_conn_check_thread); + + // 3. Watchtower thread. + // This thread checks for broadcasted contract transactions, which usually means violation of the protocol. + // When contract transaction detected in mempool it will attempt recovery. + // This can get triggered even when contracts of adjacent hops are published. Implying the whole swap route is disrupted. + let maker_clone = maker.clone(); + let contract_watcher_thread = thread::Builder::new() + .name("Contract Watcher Thread".to_string()) + .spawn(move || { + log::info!("[{}] Spawning contract-watcher thread", port); + check_for_broadcasted_contracts(maker_clone.clone()) + })?; + thread_pool.push(contract_watcher_thread); + + // 4: The RPC server thread. + // User for responding back to `maker-cli` apps. + let maker_clone = maker.clone(); + let rpc_thread = thread::Builder::new() + .name("RPC Thread".to_string()) + .spawn(move || { + log::info!("[{}] Spawning RPC server", port); + start_rpc_server(maker_clone) + })?; + + thread_pool.push(rpc_thread); + maker.setup_complete()?; + log::info!("[{}] Maker setup is ready", maker.config.port); + } // The P2P Client connection loop. // Each client connection will spawn a new handler thread, which is added back in the global thread_pool. @@ -548,6 +549,6 @@ pub fn start_maker_server(maker: Arc) -> Result<(), MakerError> { log::info!("Shutdown wallet syncing completed."); maker.get_wallet().read()?.save_to_disk()?; log::info!("Wallet file saved to disk."); - + log::info!("Maker Server is shut down successfully"); Ok(()) } diff --git a/src/wallet/api.rs b/src/wallet/api.rs index 41606147..8935649d 100644 --- a/src/wallet/api.rs +++ b/src/wallet/api.rs @@ -952,12 +952,9 @@ impl Wallet { /// Refreshes the offer maximum size cache based on the current wallet's unspent transaction outputs (UTXOs). pub fn refresh_offer_maxsize_cache(&mut self) -> Result<(), WalletError> { - let all_utxos = self.get_all_utxo()?; - let mut utxos = self.list_descriptor_utxo_spend_info(Some(&all_utxos))?; - let mut swap_coin_utxo = self.list_swap_coin_utxo_spend_info(Some(&all_utxos))?; - utxos.append(&mut swap_coin_utxo); - let balance: Amount = utxos.iter().fold(Amount::ZERO, |acc, u| acc + u.0.amount); - self.store.offer_maxsize = balance.to_sat(); + let swap_balance = self.balance_swap_coins(None)?; + let seed_balance = self.balance_descriptor_utxo(None)?; + self.store.offer_maxsize = (seed_balance + swap_balance).to_sat(); Ok(()) } diff --git a/src/wallet/direct_send.rs b/src/wallet/direct_send.rs index 4b3fb6f3..92026bd8 100644 --- a/src/wallet/direct_send.rs +++ b/src/wallet/direct_send.rs @@ -117,13 +117,10 @@ impl Wallet { if let SendAmount::Amount(a) = send_amount { if a + fee > total_input_value { - return Err(WalletError::Protocol(format!( - - "Insufficient funds: Required (send_amount + fee): {} sats | Available: {} sats | Deficit: {} sats.", - (a + fee).to_sat(), - total_input_value.to_sat(), - (a + fee - total_input_value).to_sat(), - ))); + return Err(WalletError::InsufficientFund { + available: total_input_value.to_sat(), + required: (a + fee).to_sat(), + }); } } diff --git a/src/wallet/error.rs b/src/wallet/error.rs index b4c0819b..3193b4c7 100644 --- a/src/wallet/error.rs +++ b/src/wallet/error.rs @@ -17,6 +17,7 @@ pub enum WalletError { Locktime(bitcoin::blockdata::locktime::absolute::ConversionError), Secp(bitcoin::secp256k1::Error), Consensus(String), + InsufficientFund { available: u64, required: u64 }, } impl From for WalletError { diff --git a/src/wallet/fidelity.rs b/src/wallet/fidelity.rs index fe659693..84436610 100644 --- a/src/wallet/fidelity.rs +++ b/src/wallet/fidelity.rs @@ -5,6 +5,11 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; +use crate::{ + protocol::messages::FidelityProof, + utill::redeemscript_to_scriptpubkey, + wallet::{UTXOSpendInfo, Wallet}, +}; use bitcoin::{ absolute::LockTime, bip32::{ChildNumber, DerivationPath}, @@ -19,12 +24,6 @@ use bitcoin::{ use bitcoind::bitcoincore_rpc::RpcApi; use serde::{Deserialize, Serialize}; -use crate::{ - protocol::messages::FidelityProof, - utill::redeemscript_to_scriptpubkey, - wallet::{UTXOSpendInfo, Wallet}, -}; - use super::WalletError; // To (strongly) disincentivize Sybil behavior, the value assessment of the bond @@ -46,11 +45,9 @@ const FIDELITY_DERIVATION_PATH: &str = "m/84'/0'/0'/2"; #[derive(Debug)] pub enum FidelityError { WrongScriptType, - BondAlreadyExists(u32), BondDoesNotExist, BondAlreadySpent, CertExpired, - InsufficientFund { available: u64, required: u64 }, General(String), } @@ -323,11 +320,10 @@ impl Wallet { }); if total_input_amount < amount { - return Err((FidelityError::InsufficientFund { + return Err(WalletError::InsufficientFund { available: total_input_amount.to_sat(), required: amount.to_sat(), - }) - .into()); + }); } let change_amount = total_input_amount.checked_sub(amount + fee); diff --git a/tests/fidelity.rs b/tests/fidelity.rs index 6f965d3d..de1f74ca 100644 --- a/tests/fidelity.rs +++ b/tests/fidelity.rs @@ -8,25 +8,21 @@ use coinswap::{ mod test_framework; use test_framework::*; -use std::{thread, time::Duration}; +use std::{assert_eq, thread, time::Duration}; -/// Test Fidelity Transactions +/// Test Fidelity Bond Creation and Redemption /// -/// These tests covers -/// - Creation -/// - Redemption -/// - Valuations of Fidelity Bonds. +/// This test covers the full lifecycle of Fidelity Bonds, including creation, valuation, and redemption: /// -/// Fidelity Bonds can be created either via running the maker server or by calling the `create_fidelity()` API -/// on the wallet. Both of them are performed here. At the start of the maker server it will try to create a fidelity -/// bond with value and timelock provided in the configuration (default: value = 5_000_000 sats, locktime = 100 block). -/// -/// Maker server will error if not enough balance is present to create fidelity bond. -/// A custom fidelity bond can be create using the `create_fidelity()` API. +/// - The Maker starts with insufficient funds to create a fidelity bond (0.04 BTC), +/// triggering log messages requesting more funds. +/// - Once provided with sufficient funds (1 BTC), the Maker creates the first fidelity bond (0.05 BTC). +/// - A second fidelity bond (0.08 BTC) is created and its higher value is verified. +/// - The test simulates bond maturity by advancing the blockchain height and redeems them sequentially, +/// verifying correct balances and proper bond status updates after redemption. #[test] fn test_fidelity() { // ---- Setup ---- - let makers_config_map = [((6102, None), MakerBehavior::Normal)]; let (test_framework, _, makers, directory_server_instance) = TestFramework::init( @@ -40,8 +36,8 @@ fn test_fidelity() { // ----- Test ----- - // Give insufficient fund to maker and start the server. - // This should return Error of Insufficient fund. + // Provide insufficient funds to the maker and start the server. + // This will continuously log about insufficient funds and request 0.01 BTC to create a fidelity bond. let maker_addrs = maker .get_wallet() .write() @@ -49,115 +45,154 @@ fn test_fidelity() { .get_next_external_address() .unwrap(); test_framework.send_to_address(&maker_addrs, Amount::from_btc(0.04).unwrap()); + test_framework.generate_blocks(1); let maker_clone = maker.clone(); + let maker_thread = thread::spawn(move || start_maker_server(maker_clone)); - thread::sleep(Duration::from_secs(20)); + thread::sleep(Duration::from_secs(12)); maker.shutdown().unwrap(); let _ = maker_thread.join().unwrap(); - // TODO: Assert that fund request for fidelity is printed in the log. + // TODO: Assert that a request for fidelity funds is printed in the logs. *maker.shutdown.write().unwrap() = false; - // Give Maker more funds and check fidelity bond is created at the restart of server. - test_framework.send_to_address(&maker_addrs, Amount::from_btc(0.04).unwrap()); + // Provide the maker with more funds. + test_framework.send_to_address(&maker_addrs, Amount::ONE_BTC); test_framework.generate_blocks(1); let maker_clone = maker.clone(); + let maker_thread = thread::spawn(move || start_maker_server(maker_clone)); - thread::sleep(Duration::from_secs(20)); + thread::sleep(Duration::from_secs(1)); maker.shutdown().unwrap(); - let success = maker_thread.join().unwrap(); - - assert!(success.is_ok()); + let _ = maker_thread.join().unwrap(); - // Check fidelity bond created correctly - let first_conf_height = { + // Verify that the fidelity bond is created correctly. + let first_maturity_height = { let wallet_read = maker.get_wallet().read().unwrap(); - let (index, bond, is_spent) = wallet_read + + // Get the index of the bond with the highest value, + // which should be 0 as there is only one fidelity bond. + let highest_bond_index = wallet_read.get_highest_fidelity_index().unwrap().unwrap(); + assert_eq!(highest_bond_index, 0); + + let bond_value = wallet_read + .calculate_bond_value(highest_bond_index) + .unwrap(); + assert_eq!(bond_value, Amount::from_sat(550)); + + let (bond, _, is_spent) = wallet_read .get_fidelity_bonds() - .iter() - .map(|(i, (b, _, is_spent))| (i, b, is_spent)) - .next() + .get(&highest_bond_index) .unwrap(); - assert_eq!(*index, 0); + assert_eq!(bond.amount, Amount::from_sat(5000000)); assert!(!is_spent); - bond.conf_height + + bond.lock_time.to_consensus_u32() }; - // Create another fidelity bond of 1000000 sats - let second_conf_height = { + // Create another fidelity bond of 0.08 BTC and validate it. + let second_maturity_height = { let mut wallet_write = maker.get_wallet().write().unwrap(); + let index = wallet_write .create_fidelity( - Amount::from_sat(1000000), - LockTime::from_height((test_framework.get_block_count() as u32) + 100).unwrap(), + Amount::from_sat(8000000), + LockTime::from_height((test_framework.get_block_count() as u32) + 150).unwrap(), ) .unwrap(); - assert_eq!(index, 1); - let (bond, _, is_spent) = wallet_write - .get_fidelity_bonds() - .get(&index) - .expect("bond expected"); - assert_eq!(bond.amount, Amount::from_sat(1000000)); + + // Since this bond has a larger amount than the first, it should now be the highest value bond. + let highest_bond_index = wallet_write.get_highest_fidelity_index().unwrap().unwrap(); + assert_eq!(highest_bond_index, index); + + let bond_value = wallet_write.calculate_bond_value(index).unwrap(); + assert_eq!(bond_value, Amount::from_sat(1801)); + + let (bond, _, is_spent) = wallet_write.get_fidelity_bonds().get(&index).unwrap(); + assert_eq!(bond.amount, Amount::from_sat(8000000)); assert!(!is_spent); - bond.conf_height + + bond.lock_time.to_consensus_u32() }; - // Check the balances + // Verify balances { - let wallet = maker.get_wallet().read().unwrap(); - let all_utxos = wallet.get_all_utxo().unwrap(); - let normal_balance = wallet.balance_descriptor_utxo(Some(&all_utxos)).unwrap() - + wallet.balance_swap_coins(Some(&all_utxos)).unwrap(); - assert_eq!(normal_balance.to_sat(), 1998000); + let mut wallet_write = maker.get_wallet().write().unwrap(); + + // Sync the wallet to get accurate balances. + wallet_write.sync().unwrap(); + + let fidelity_balance = wallet_write.balance_fidelity_bonds(None).unwrap(); + let seed_balance = wallet_write.balance_descriptor_utxo(None).unwrap(); + + assert_eq!(fidelity_balance.to_sat(), 13000000); + assert_eq!(seed_balance.to_sat(), 90998000); } - let (first_maturity_heigh, second_maturity_height) = - (first_conf_height + 100, second_conf_height + 100); + // Wait for the bonds to mature, redeem them, and validate the process. + let mut required_height = first_maturity_height; - // Wait for maturity and then redeem the bonds loop { let current_height = test_framework.get_block_count() as u32; - let required_height = first_maturity_heigh.max(second_maturity_height); + if current_height < required_height { log::info!( - "Waiting for maturity. Current height {}, required height: {}", + "Waiting for bond maturity. Current height: {}, required height: {}", current_height, required_height ); + thread::sleep(Duration::from_secs(10)); - continue; } else { - log::info!("Fidelity is matured. sending redemption transactions"); let mut wallet_write = maker.get_wallet().write().unwrap(); - let indexes = wallet_write - .get_fidelity_bonds() - .keys() - .cloned() - .collect::>(); - for i in indexes { - wallet_write.redeem_fidelity(i).unwrap(); + + if required_height == first_maturity_height { + log::info!("First Fidelity Bond is matured. Sending redemption transaction"); + + let _ = wallet_write.redeem_fidelity(0).unwrap(); + + log::info!("First Fidelity Bond is successfully redeemed."); + + // The second bond should now be the highest value bond. + let highest_bond_index = + wallet_write.get_highest_fidelity_index().unwrap().unwrap(); + assert_eq!(highest_bond_index, 1); + + // Wait for the second bond to mature. + required_height = second_maturity_height; + } else { + log::info!("Second Fidelity Bond is matured. sending redemption transactions"); + + let _ = wallet_write.redeem_fidelity(1).unwrap(); + + log::info!("Second Fidelity Bond is successfully redeemed."); + + // There should now be no unspent bonds left. + let index = wallet_write.get_highest_fidelity_index().unwrap(); + assert_eq!(index, None); + break; } - break; } } - // Check the balances again + // Verify the balances again after all bonds are redeemed. { - let wallet = maker.get_wallet().read().unwrap(); - let all_utxos = wallet.get_all_utxo().unwrap(); - let normal_balance = wallet.balance_descriptor_utxo(Some(&all_utxos)).unwrap() - + wallet.balance_swap_coins(Some(&all_utxos)).unwrap(); - assert_eq!(normal_balance.to_sat(), 7996000); + let wallet_read = maker.get_wallet().read().unwrap(); + let fidelity_balance = wallet_read.balance_fidelity_bonds(None).unwrap(); + let seed_balance = wallet_read.balance_descriptor_utxo(None).unwrap(); + + assert_eq!(fidelity_balance.to_sat(), 0); + assert_eq!(seed_balance.to_sat(), 103996000); } - // stop directory server + // Stop the directory server. let _ = directory_server_instance.shutdown(); thread::sleep(Duration::from_secs(10));