diff --git a/Cargo.toml b/Cargo.toml index ff132087..3bd2d768 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "coinswap" version = "0.1.0" -authors = ["developers at citadel-tech"] +authors = ["Developers at Citadel-Tech"] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -24,7 +24,7 @@ openssl-sys = { version = "0.9.68", optional = true } #Empty default feature set, (helpful to generalise in github actions) [features] -default = ['tor', 'integration-test'] +default = ['tor'] # The following feature set is in response to the issue described at https://github.com/rust-lang/rust/issues/45599 # Only used for running the integration tests integration-test = [] diff --git a/README.md b/README.md index af48287e..95d43f18 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,14 @@ CoinSwap is a rust implementation of a variant of atomic-swap protocol, using HT * [Detailed design](https://gist.github.com/chris-belcher/9144bd57a91c194e332fb5ca371d0964) * [Developer's resources](/docs/dev-book.md) +## Dependencies + +Ensure you have the following dependency installed before compiling the project. + +```shell +sudo apt install build-essential automake libtool +``` + ## Build and Test The repo contains a fully automated integration testing framework on Bitcoin Regtest. The bitcoin binary used for testing is diff --git a/notes.md b/notes.md new file mode 100644 index 00000000..79e9d9f7 --- /dev/null +++ b/notes.md @@ -0,0 +1,2 @@ +# Install c-compiler essentials. +sudo apt install build-essential automake libtool diff --git a/src/bin/directory-cli.rs b/src/bin/directory-cli.rs index 7aea1001..4409a401 100644 --- a/src/bin/directory-cli.rs +++ b/src/bin/directory-cli.rs @@ -38,7 +38,7 @@ fn send_rpc_req(mut stream: TcpStream, req: RpcMsgReq) -> Result<(), DirectorySe let resp_bytes = read_message(&mut stream)?; let resp: RpcMsgResp = serde_cbor::from_slice(&resp_bytes).map_err(NetError::Cbor)?; - println!("{:?}", resp); + println!("{:#?}", resp); Ok(()) } diff --git a/src/bin/directoryd.rs b/src/bin/directoryd.rs index e568e1fd..d7ad7885 100644 --- a/src/bin/directoryd.rs +++ b/src/bin/directoryd.rs @@ -6,9 +6,7 @@ use coinswap::{ wallet::RPCConfig, }; -#[cfg(feature = "tor")] -use coinswap::tor::setup_mitosis; -use std::{path::PathBuf, str::FromStr, sync::Arc}; +use std::{path::PathBuf, sync::Arc}; #[derive(Parser)] #[clap(version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), @@ -22,7 +20,7 @@ struct Cli { name = "ADDRESS:PORT", long, short = 'r', - default_value = "127.0.0.1:18443" + default_value = "127.0.0.1:48332" )] pub(crate) rpc: String, /// Sets the rpc basic authentication. @@ -33,40 +31,33 @@ struct Cli { value_parser = parse_proxy_auth, default_value = "user:password", )] - pub(crate) auth: (String, String), - /// Sets the full node network, this should match with the network of the running node. - #[clap( - name = "rpc_network", - long, - default_value = "regtest", possible_values = &["regtest", "signet", "mainnet"] - )] - pub(crate) rpc_network: String, + pub auth: (String, String), } fn main() -> Result<(), DirectoryServerError> { setup_directory_logger(log::LevelFilter::Info); let args = Cli::parse(); - - let rpc_network = bitcoin::Network::from_str(&args.rpc_network).unwrap(); - let rpc_config = RPCConfig { url: args.rpc, auth: Auth::UserPass(args.auth.0, args.auth.1), - network: rpc_network, 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 connection_type = if cfg!(feature = "integration-test") { + ConnectionType::CLEARNET + } else { + ConnectionType::TOR + }; + + #[cfg(not(feature = "tor"))] + let connection_type = ConnectionType::CLEARNET; - let directory = Arc::new(DirectoryServer::new(args.data_directory, Some(conn_type))?); + let directory = Arc::new(DirectoryServer::new( + args.data_directory, + Some(connection_type), + )?); start_directory_server(directory, Some(rpc_config))?; diff --git a/src/bin/maker-cli.rs b/src/bin/maker-cli.rs index b16a968c..234fe80f 100644 --- a/src/bin/maker-cli.rs +++ b/src/bin/maker-cli.rs @@ -6,9 +6,16 @@ use coinswap::{ utill::{read_message, send_message, setup_maker_logger}, }; -/// maker-cli is a command line app to send RPC messages to maker server. +/// A simple command line app to operate the makerd server. +/// +/// The app works as a RPC client for makerd, useful to access the server, retrieve information, and manage server operations. +/// +/// For more detailed usage information, please refer: [maker demo doc link] +/// +/// This is early beta, and there are known and unknown bugs. Please report issues at: https://github.com/citadel-tech/coinswap/issues #[derive(Parser, Debug)] -#[clap(author, version, about, long_about = None)] +#[clap(version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), +author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] struct App { /// Sets the rpc-port of Makerd #[clap(long, short = 'p', default_value = "127.0.0.1:6103")] @@ -20,38 +27,53 @@ struct App { #[derive(Parser, Debug)] enum Commands { - /// Sends a Ping - Ping, - /// Returns a list of seed utxos - SeedUtxo, - /// Returns a list of swap coin utxos - SwapUtxo, - /// Returns a list of live contract utxos - ContractUtxo, - /// Returns a list of fidelity utxos - FidelityUtxo, - /// Returns the total seed balance - SeedBalance, - /// Returns the total swap coin balance - SwapBalance, - /// Returns the total live contract balance - ContractBalance, - /// Returns the total fidelity balance - FidelityBalance, - /// Gets a new address - NewAddress, - /// Send to an external address and returns the transaction hex. + /// Sends a ping to makerd. Will return a pong. + SendPing, + /// Lists all utxos in the wallet. Including fidelity bonds. + ListUtxo, + /// Lists utxos received from incoming swaps. + ListUtxoSwap, + /// Lists HTLC contract utxos. + ListUtxoContract, + /// Lists fidelity bond utxos. + ListUtxoFidelity, + /// Get total wallet balance, excluding Fidelity bonds. + GetBalance, + /// Get total balance received via incoming swaps. + GetBalanceSwap, + /// Get total balances of HTLC contract utxos. + GetBalanceContract, + /// Get total amount locked in fidelity bonds. + GetBalanceFidelity, + /// Gets a new bitcoin receiving address + GetNewAddress, + /// Send Bitcoin to an external address and returns the txid. SendToAddress { + /// Recipient's address. + #[clap(long, short = 't')] address: String, + /// Amount to send in sats + #[clap(long, short = 'a')] amount: u64, + /// Total fee to be paid in sats + #[clap(long, short = 'f')] fee: u64, }, - /// Returns the tor address - GetTorAddress, - /// Returns the data directory path - GetDataDir, - /// Stops the maker server + /// Show the server tor address + ShowTorAddress, + /// Show the data directory path + ShowDataDir, + /// Shutdown the makerd server Stop, + /// Redeems the fidelity bond if timelock is matured. Returns the txid of the spending transaction. + RedeemFidelity { + #[clap(long, short = 'i', default_value = "0")] + index: u32, + }, + /// Show all the fidelity bonds, current and previous, with an (index, {bond_proof, is_spent}) tupple. + ShowFidelity, + /// Sync the maker wallet with current blockchain state. + SyncWallet, } fn main() -> Result<(), MakerError> { @@ -61,34 +83,34 @@ fn main() -> Result<(), MakerError> { let stream = TcpStream::connect(cli.rpc_port)?; match cli.command { - Commands::Ping => { + Commands::SendPing => { send_rpc_req(stream, RpcMsgReq::Ping)?; } - Commands::ContractUtxo => { + Commands::ListUtxoContract => { send_rpc_req(stream, RpcMsgReq::ContractUtxo)?; } - Commands::ContractBalance => { + Commands::GetBalanceContract => { send_rpc_req(stream, RpcMsgReq::ContractBalance)?; } - Commands::FidelityBalance => { + Commands::GetBalanceFidelity => { send_rpc_req(stream, RpcMsgReq::FidelityBalance)?; } - Commands::FidelityUtxo => { + Commands::ListUtxoFidelity => { send_rpc_req(stream, RpcMsgReq::FidelityUtxo)?; } - Commands::SeedBalance => { - send_rpc_req(stream, RpcMsgReq::SeedBalance)?; + Commands::GetBalance => { + send_rpc_req(stream, RpcMsgReq::Balance)?; } - Commands::SeedUtxo => { - send_rpc_req(stream, RpcMsgReq::SeedUtxo)?; + Commands::ListUtxo => { + send_rpc_req(stream, RpcMsgReq::Utxo)?; } - Commands::SwapBalance => { + Commands::GetBalanceSwap => { send_rpc_req(stream, RpcMsgReq::SwapBalance)?; } - Commands::SwapUtxo => { + Commands::ListUtxoSwap => { send_rpc_req(stream, RpcMsgReq::SwapUtxo)?; } - Commands::NewAddress => { + Commands::GetNewAddress => { send_rpc_req(stream, RpcMsgReq::NewAddress)?; } Commands::SendToAddress { @@ -105,22 +127,31 @@ fn main() -> Result<(), MakerError> { }, )?; } - Commands::GetTorAddress => { + Commands::ShowTorAddress => { send_rpc_req(stream, RpcMsgReq::GetTorAddress)?; } - Commands::GetDataDir => { + Commands::ShowDataDir => { send_rpc_req(stream, RpcMsgReq::GetDataDir)?; } Commands::Stop => { send_rpc_req(stream, RpcMsgReq::Stop)?; } + Commands::RedeemFidelity { index } => { + send_rpc_req(stream, RpcMsgReq::RedeemFidelity(index))?; + } + Commands::ShowFidelity => { + send_rpc_req(stream, RpcMsgReq::ListFidelity)?; + } + Commands::SyncWallet => { + send_rpc_req(stream, RpcMsgReq::SyncWallet)?; + } } Ok(()) } fn send_rpc_req(mut stream: TcpStream, req: RpcMsgReq) -> Result<(), MakerError> { - stream.set_read_timeout(Some(Duration::from_secs(20)))?; + // stream.set_read_timeout(Some(Duration::from_secs(20)))?; stream.set_write_timeout(Some(Duration::from_secs(20)))?; send_message(&mut stream, &req)?; @@ -128,7 +159,11 @@ fn send_rpc_req(mut stream: TcpStream, req: RpcMsgReq) -> Result<(), MakerError> let response_bytes = read_message(&mut stream)?; let response: RpcMsgResp = serde_cbor::from_slice(&response_bytes)?; - println!("{}", response); + if matches!(response, RpcMsgResp::Pong) { + println!("success"); + } else { + println!("{}", response); + } Ok(()) } diff --git a/src/bin/makerd.rs b/src/bin/makerd.rs index b3167c59..495351a7 100644 --- a/src/bin/makerd.rs +++ b/src/bin/makerd.rs @@ -5,30 +5,36 @@ use coinswap::{ utill::{parse_proxy_auth, setup_maker_logger, ConnectionType}, wallet::RPCConfig, }; -use std::{path::PathBuf, str::FromStr, sync::Arc}; - -#[cfg(feature = "tor")] -use coinswap::tor::setup_mitosis; - -/// The Maker Server. +use std::{path::PathBuf, sync::Arc}; +/// Coinswap Maker Server +/// +/// The server requires a Bitcoin Core RPC connection running in Testnet4. It requires some starting balance, around 50,000 sats for Fidelity + Swap Liquidity (suggested 50,000 sats). +/// So topup with at least 0.001 BTC to start all the node processses. Suggested faucet: https://mempool.space/testnet4 +/// +/// All server process will start after the fidelity bond transaction confirms. This may take some time. Approx: 10 mins. +/// Once the bond confirms, the server starts listening for incoming swap requests. As it performs swaps for clients, it keeps earning fees. /// -/// This app starts the Maker server. -#[derive(Parser)] +/// The server is operated with the maker-cli app, for all basic wallet related operations. +/// +/// For more detailed usage information, please refer: [maker demo doc link] +/// +/// This is early beta, and there are known and unknown bugs. Please report issues at: https://github.com/citadel-tech/coinswap/issues +#[derive(Parser, Debug)] #[clap(version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] struct Cli { /// Optional DNS data directory. Default value : "~/.coinswap/maker" #[clap(long, short = 'd')] data_directory: Option, - /// Sets the full node address for rpc connection. + /// Bitcoin Core RPC network address. #[clap( name = "ADDRESS:PORT", long, short = 'r', - default_value = "127.0.0.1:18443" + default_value = "127.0.0.1:48332" )] - pub(crate) rpc: String, - /// Sets the rpc basic authentication. + pub rpc: String, + /// Bitcoin Core RPC authentication string (username, password). #[clap( name = "USER:PASSWD", short = 'a', @@ -36,16 +42,8 @@ struct Cli { value_parser = parse_proxy_auth, default_value = "user:password", )] - pub(crate) auth: (String, String), - /// Sets the full node network, this should match with the network of the running node. - #[clap( - name = "NETWORK", - long, - short = 'n', - default_value = "regtest", possible_values = &["regtest", "signet", "mainnet"] - )] - pub(crate) rpc_network: String, - /// Sets the maker wallet's name. If the wallet file already exists at data-directory, it will load that wallet. + pub auth: (String, String), + /// Optional wallet name. If the wallet exists, load the wallet, else create a new wallet with given name. Default: maker-wallet #[clap(name = "WALLET", long, short = 'w')] pub(crate) wallet_name: Option, } @@ -55,23 +53,27 @@ fn main() -> Result<(), MakerError> { let args = Cli::parse(); - let rpc_network = bitcoin::Network::from_str(&args.rpc_network).unwrap(); + let url = if cfg!(feature = "integration-test") { + "127.0.0.1:18443".to_owned() + } else { + args.rpc + }; let rpc_config = RPCConfig { - url: args.rpc, + url, auth: Auth::UserPass(args.auth.0, args.auth.1), - network: rpc_network, 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 connection_type = if cfg!(feature = "integration-test") { + ConnectionType::CLEARNET + } else { + ConnectionType::TOR + }; + + #[cfg(not(feature = "tor"))] + let connection_type = ConnectionType::CLEARNET; let maker = Arc::new(Maker::init( args.data_directory, @@ -80,7 +82,7 @@ fn main() -> Result<(), MakerError> { None, None, None, - Some(conn_type), + Some(connection_type), MakerBehavior::Normal, )?); diff --git a/src/bin/taker.rs b/src/bin/taker.rs index 3dd3d568..b180de9f 100644 --- a/src/bin/taker.rs +++ b/src/bin/taker.rs @@ -9,109 +9,126 @@ use coinswap::{ use log::LevelFilter; use std::{path::PathBuf, str::FromStr}; -/// taker-cli is a command line app to use taker client API's. +/// A simple command line app to operate as coinswap client. +/// +/// The app works as regular Bitcoin wallet with added capability to perform coinswaps. The app +/// requires a running Bitcoin Core node with RPC access. It currently only runs on Testnet4. +/// Suggested faucet for getting Testnet4 coins: https://mempool.space/testnet4 +/// +/// For more detailed usage information, please refer: [taker-cli demo doc link] +/// +/// This is early beta, and there are known and unknown bugs. Please report issues at: https://github.com/citadel-tech/coinswap/issues #[derive(Parser, Debug)] #[clap(version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] struct Cli { - /// Optional DNS data directory. Default value : "~/.coinswap/taker" + /// Optional data directory. Default value : "~/.coinswap/taker" #[clap(long, short = 'd')] data_directory: Option, - /// Sets the full node address for rpc connection. + + /// Bitcoin Core RPC address:port value #[clap( name = "ADDRESS:PORT", long, short = 'r', - default_value = "127.0.0.1:18443" + default_value = "127.0.0.1:48332" )] - pub(crate) rpc: String, - /// Sets the rpc basic authentication. + pub rpc: String, + + /// Bitcoin Core RPC authentication string. Ex: username:password #[clap(name="USER:PASSWORD",short='a',long, value_parser = parse_proxy_auth, default_value = "user:password")] - pub(crate) auth: (String, String), - /// Sets the full node network, this should match with the network of the running node. - #[clap( - long, - short = 'b', - default_value = "regtest", possible_values = &["regtest", "signet", "mainnet"] - )] - pub(crate) bitcoin_network: String, - /// Sets the taker wallet's name. If the wallet file already exists at data-directory, it will load that wallet. + pub auth: (String, String), + + /// Sets the taker wallet's name. If the wallet file already exists, it will load that wallet. Default: taker-wallet #[clap(name = "WALLET", long, short = 'w')] - pub(crate) wallet_name: Option, - /// Sets the verbosity level of logs. - /// Default: Determined by the command passed. - #[clap(long, short = 'v', possible_values = &["off", "error", "warn", "info", "debug", "trace"])] - pub(crate) verbosity: Option, - /// Sets the maker count to initiate coinswap with. - #[clap(name = "maker_count", default_value = "2")] - pub(crate) maker_count: usize, - /// Sets the send amount. - #[clap(name = "send_amount", default_value = "500000")] - pub(crate) send_amount: u64, - /// Sets the transaction count. - #[clap(name = "tx_count", default_value = "3")] - pub(crate) tx_count: u32, - /// List of sub commands to process various endpoints of taker cli app. + pub wallet_name: Option, + + /// Sets the verbosity level of debug.log file + #[clap(long, short = 'v', possible_values = &["off", "error", "warn", "info", "debug", "trace"], default_value = "info")] + pub verbosity: String, + + /// List of commands for various wallet operations #[clap(subcommand)] command: Commands, } #[derive(Parser, Debug)] enum Commands { - /// Returns a list of seed utxos - SeedUtxo, - /// Returns a list of swap coin utxos - SwapUtxo, - /// Returns a list of live contract utxos - ContractUtxo, - /// Returns the total seed balance - SeedBalance, - /// Returns the total swap coin balance - SwapBalance, - /// Returns the total live contract balance - ContractBalance, - /// Returns the total balance of taker wallet - TotalBalance, + // TODO: Design a better structure to display different utxos and balance groups. + /// Lists all currently spendable utxos + ListUtxo, + /// Lists all utxos received in incoming swaps + ListUtxoSwap, + /// Lists all HTLC utxos (if any) + ListUtxoContract, + /// Get the total spendable wallet balance (sats) + GetBalance, + /// Get the total balance received from swaps (sats) + GetBalanceSwap, + /// Get the total amount stuck in HTLC contracts (sats) + GetBalanceContract, /// Returns a new address GetNewAddress, /// Send to an external wallet address. SendToAddress { - #[clap(name = "address")] + /// Recipient's address. + #[clap(long, short = 't')] address: String, - /// Amount to be sent (in sats) - #[clap(name = "amount")] + /// Amount to send in sats + #[clap(long, short = 'a')] amount: u64, - /// Fee of a Tx(in sats) - #[clap(name = "fee")] + /// Mining fee to be paid in sats + #[clap(long, short = 'f')] fee: u64, }, - /// Sync the offer book - SyncOfferBook, + /// Update the offerbook with current market offers and display them + FetchOffers, + + // TODO: Also add ListOffers command to just list the current book. /// Initiate the coinswap process - DoCoinswap, + Coinswap { + /// Sets the maker count to swap with. Swapping with less than 2 makers is allowed to maintain client privacy. + /// Adding more makers in the swap will incure more swap fees. + #[clap(long, short = 'm', default_value = "2")] + makers: usize, + /// Sets the swap amount in sats. + #[clap(long, short = 'a', default_value = "20000")] + amount: u64, + // /// Sets how many new swap utxos to get. The swap amount will be randomly distrubted across the new utxos. + // /// Increasing this number also increases total swap fee. + // #[clap(long, short = 'u', default_value = "1")] + // utxos: u32, + }, + /// Recover from all failed swaps + Recover, } fn main() -> Result<(), TakerError> { let args = Cli::parse(); + setup_taker_logger(LevelFilter::from_str(&args.verbosity).unwrap()); - let rpc_network = bitcoin::Network::from_str(&args.bitcoin_network).unwrap(); - - let connection_type = ConnectionType::TOR; + let url = if cfg!(feature = "integration-test") { + "127.0.0.1:18443".to_owned() + } else { + args.rpc + }; let rpc_config = RPCConfig { - url: args.rpc, + url, auth: Auth::UserPass(args.auth.0, args.auth.1), - network: rpc_network, wallet_name: "random".to_string(), // we can put anything here as it will get updated in the init. }; - let swap_params = SwapParams { - send_amount: Amount::from_sat(args.send_amount), - maker_count: args.maker_count, - tx_count: args.tx_count, - required_confirms: REQUIRED_CONFIRMS, + #[cfg(feature = "tor")] + let connection_type = if cfg!(feature = "integration-test") { + ConnectionType::CLEARNET + } else { + ConnectionType::TOR }; + #[cfg(not(feature = "tor"))] + let connection_type = ConnectionType::CLEARNET; + let mut taker = Taker::init( args.data_directory.clone(), args.wallet_name.clone(), @@ -120,64 +137,44 @@ fn main() -> Result<(), TakerError> { Some(connection_type), )?; - // Determines the log level based on the verbosity argument or the command. - // - // If verbosity is provided, it converts the string to a `LevelFilter`. - // Otherwise, the log level is set based on the command. - let log_level = match args.verbosity { - Some(level) => LevelFilter::from_str(&level).unwrap(), - None => match args.command { - Commands::DoCoinswap | Commands::SyncOfferBook | Commands::SendToAddress { .. } => { - log::LevelFilter::Info - } - _ => log::LevelFilter::Off, - }, - }; - - setup_taker_logger(log_level); - match args.command { - Commands::SeedUtxo => { + Commands::ListUtxo => { let utxos: Vec = taker .get_wallet() - .list_descriptor_utxo_spend_info(None)? + .list_all_utxo_spend_info(None)? .iter() .map(|(l, _)| l.clone()) .collect(); - println!("{:?}", utxos); + println!("{:#?}", utxos); } - Commands::SwapUtxo => { + Commands::ListUtxoSwap => { let utxos: Vec = taker .get_wallet() .list_swap_coin_utxo_spend_info(None)? .iter() .map(|(l, _)| l.clone()) .collect(); - println!("{:?}", utxos); + println!("{:#?}", utxos); } - Commands::ContractUtxo => { + Commands::ListUtxoContract => { let utxos: Vec = taker .get_wallet() .list_live_contract_spend_info(None)? .iter() .map(|(l, _)| l.clone()) .collect(); - println!("{:?}", utxos); + println!("{:#?}", utxos); } - Commands::ContractBalance => { + Commands::GetBalanceContract => { let balance = taker.get_wallet().balance_live_contract(None)?; println!("{:?}", balance); } - Commands::SwapBalance => { + Commands::GetBalanceSwap => { let balance = taker.get_wallet().balance_swap_coins(None)?; println!("{:?}", balance); } - Commands::SeedBalance => { - let balance = taker.get_wallet().balance_descriptor_utxo(None)?; - println!("{:?}", balance); - } - Commands::TotalBalance => { - let balance = taker.get_wallet().balance()?; + Commands::GetBalance => { + let balance = taker.get_wallet().spendable_balance()?; println!("{:?}", balance); } Commands::GetNewAddress => { @@ -216,21 +213,33 @@ fn main() -> Result<(), TakerError> { &coins_to_spend, )?; - // Derive fee rate from given `fee` argument. - let calculated_fee_rate = fee / (tx.weight()); + let txid = taker.get_wallet().send_tx(&tx).unwrap(); - println!( - "transaction_hex : {:?}", - bitcoin::consensus::encode::serialize_hex(&tx) - ); - println!("Calculated FeeRate : {:#}", calculated_fee_rate); + println!("{}", txid); } - Commands::SyncOfferBook => { - taker.sync_offerbook()?; + Commands::FetchOffers => { + println!("Fetching offerdata from the market. use `tail -f /debug.log` to see progress."); + let offerbook = taker.fetch_offers()?; + println!("{:#?}", offerbook) } - Commands::DoCoinswap => { + Commands::Coinswap { makers, amount } => { + let swap_params = SwapParams { + send_amount: Amount::from_sat(amount), + maker_count: makers, + tx_count: 1, + required_confirms: REQUIRED_CONFIRMS, + }; + + println!("Starting coinswap with swap params : {:?}. use `tail -f /debug.log` to see progress.", swap_params); taker.do_coinswap(swap_params)?; + println!("succesfully completed coinswap!! Check `list-utxo` to see the new coins"); + } + + Commands::Recover => { + println!("Starting recovery. use `tail -f /debug.log` to see progress."); + taker.recover_from_swap()?; + println!("Recovery completed succesfully."); } } diff --git a/src/bin/tor.rs b/src/bin/tor.rs new file mode 100644 index 00000000..39ad7cc9 --- /dev/null +++ b/src/bin/tor.rs @@ -0,0 +1,27 @@ +use clap::Parser; + +/// wrapper binary to run the tor hidden service. +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct App { + /// the network port + #[clap(long, short = 'p')] + port: u16, + /// the socks port + #[clap(long, short = 's')] + socks_port: u16, + /// the base directory for hidden service + #[clap(long, short = 'd')] + base_dir: String, +} + +#[cfg(feature = "tor")] +fn main() -> Result<(), libtor::Error> { + let args = App::parse(); + coinswap::tor::start_tor(args.socks_port, args.port, args.base_dir) +} + +#[cfg(not(feature = "tor"))] +fn main() { + println!("Error: tor feature is needed to run this binary."); +} diff --git a/src/maker/api.rs b/src/maker/api.rs index 603b5dbf..0b059bf9 100644 --- a/src/maker/api.rs +++ b/src/maker/api.rs @@ -57,8 +57,8 @@ pub const RPC_PING_INTERVAL: Duration = Duration::from_secs(60); // 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); +/// Maker triggers the recovery mechanism, if Taker is idle for more than 30 mins. +pub const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60 * 30); /// The minimum difference in locktime (in blocks) between the incoming and outgoing swaps. /// @@ -95,12 +95,22 @@ pub const MIN_CONTRACT_REACTION_TIME: u16 = 20; /// - `total_fee` = 5,500 sats (5.5%) /// /// Fee rates are designed to asymptotically approach 5% of the swap amount as the swap amount increases.. +#[cfg(feature = "integration-test")] pub const BASE_FEE: u64 = 1000; +#[cfg(feature = "integration-test")] pub const AMOUNT_RELATIVE_FEE_PCT: f64 = 2.50; +#[cfg(feature = "integration-test")] pub const TIME_RELATIVE_FEE_PCT: f64 = 0.10; +#[cfg(not(feature = "integration-test"))] +pub const BASE_FEE: u64 = 100; +#[cfg(not(feature = "integration-test"))] +pub const AMOUNT_RELATIVE_FEE_PCT: f64 = 0.1; +#[cfg(not(feature = "integration-test"))] +pub const TIME_RELATIVE_FEE_PCT: f64 = 0.005; + /// Minimum Coinswap amount; makers will not accept amounts below this. -pub const MIN_SWAP_AMOUNT: u64 = 100000; +pub const MIN_SWAP_AMOUNT: u64 = 10_000; // What's the use of RefundLocktimeStep? @@ -158,14 +168,6 @@ pub(crate) struct ThreadPool { pub(crate) port: u16, } -impl Drop for ThreadPool { - fn drop(&mut self) { - if let Err(e) = self.join_all_threads() { - log::error!("Error joining threads in via drop: {:?}", e); - } - } -} - impl ThreadPool { pub(crate) fn new(port: u16) -> Self { Self { @@ -179,7 +181,7 @@ impl ThreadPool { threads.push(handle); } #[inline] - fn join_all_threads(&self) -> Result<(), MakerError> { + pub(crate) fn join_all_threads(&self) -> Result<(), MakerError> { let mut threads = self .threads .lock() @@ -190,6 +192,7 @@ impl ThreadPool { let mut joined_count = 0; while let Some(thread) = threads.pop() { let thread_name = thread.thread().name().unwrap().to_string(); + println!("joining thread: {}", thread_name); match thread.join() { Ok(_) => { @@ -238,22 +241,22 @@ pub struct Maker { impl Maker { /// Initializes a Maker structure. /// - /// This function sets up a Maker instance with configurable parameters. + /// This function sets up a Maker instance with configurable parameters. /// It handles the initialization of data directories, wallet files, and RPC configurations. /// /// ### Parameters: - /// - `data_dir`: - /// - `Some(value)`: Use the specified directory for storing data. - /// - `None`: Use the default data directory (e.g., for Linux: `~/.coinswap/maker`). - /// - `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`. + /// - `data_dir`: + /// - `Some(value)`: Use the specified directory for storing data. + /// - `None`: Use the default data directory (e.g., for Linux: `~/.coinswap/maker`). + /// - `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`. /// - If `rpc_config` = `None`: Use the default [`RPCConfig`] pub fn init( data_dir: Option, wallet_file_name: Option, rpc_config: Option, - port: Option, + network_port: Option, rpc_port: Option, socks_port: Option, connection_type: Option, @@ -286,8 +289,8 @@ impl Maker { // If config file doesn't exist, default config will be loaded. let mut config = MakerConfig::new(Some(&data_dir.join("config.toml")))?; - if let Some(port) = port { - config.port = port; + if let Some(port) = network_port { + config.network_port = port; } if let Some(rpc_port) = rpc_port { @@ -302,7 +305,7 @@ impl Maker { config.connection_type = connection_type; } - let port = config.port; + let port = config.network_port; config.write_to_file(&data_dir.join("config.toml"))?; @@ -522,7 +525,7 @@ pub(crate) fn check_for_broadcasted_contracts(maker: Arc) -> Result<(), M // Something is broadcasted. Report, Recover and Abort. log::warn!( "[{}] Contract txs broadcasted!! txid: {} Recovering from ongoing swaps.", - maker.config.port, + maker.config.network_port, txid ); // Extract Incoming and Outgoing contracts, and timelock spends of the contract transactions. @@ -551,7 +554,7 @@ pub(crate) fn check_for_broadcasted_contracts(maker: Arc) -> Result<(), M } else { log::warn!( "[{}] Outgoing contact signature not known. Not Broadcasting", - maker.config.port + maker.config.network_port ); } if let Ok(tx) = ic_sc.get_fully_signed_contract_tx() { @@ -559,7 +562,7 @@ pub(crate) fn check_for_broadcasted_contracts(maker: Arc) -> Result<(), M } else { log::warn!( "[{}] Incoming contact signature not known. Not Broadcasting", - maker.config.port + maker.config.network_port ); } } @@ -569,7 +572,7 @@ pub(crate) fn check_for_broadcasted_contracts(maker: Arc) -> Result<(), M let maker_clone = maker.clone(); log::info!( "[{}] Spawning recovery thread after seeing contracts in mempool", - maker.config.port + maker.config.network_port ); let handle = std::thread::Builder::new() .name("Swap recovery thread".to_string()) @@ -599,6 +602,46 @@ pub(crate) fn check_for_broadcasted_contracts(maker: Arc) -> Result<(), M Ok(()) } +/// Checks for swapcoins present in wallet store on reboot and starts recovery if found on bitcoind network. +/// +/// If any one of the is ever observed, run the recovery routine. +pub(crate) fn restore_broadcasted_contracts_on_reboot(maker: Arc) -> Result<(), MakerError> { + let (inc, out) = maker.wallet.read()?.find_unfinished_swapcoins(); + let mut outgoings = Vec::new(); + let mut incomings = Vec::new(); + // Extract Incoming and Outgoing contracts, and timelock spends of the contract transactions. + // fully signed. + for og_sc in out.iter() { + let contract_timelock = og_sc.get_timelock()?; + let next_internal_address = &maker.wallet.read()?.get_next_internal_addresses(1)?[0]; + let time_lock_spend = og_sc.create_timelock_spend(next_internal_address)?; + + let tx = og_sc.get_fully_signed_contract_tx()?; + outgoings.push(( + (og_sc.get_multisig_redeemscript(), tx), + (contract_timelock, time_lock_spend), + )); + } + + for ic_sc in inc.iter() { + let tx = ic_sc.get_fully_signed_contract_tx()?; + incomings.push((ic_sc.get_multisig_redeemscript(), tx)); + } + + // Spawn a separate thread to wait for contract maturity and broadcasting timelocked. + let maker_clone = maker.clone(); + let handle = std::thread::Builder::new() + .name("Swap recovery thread".to_string()) + .spawn(move || { + if let Err(e) = recover_from_swap(maker_clone, outgoings, incomings) { + log::error!("Failed to recover from swap due to: {:?}", e); + } + })?; + maker.thread_pool.add_thread(handle); + + Ok(()) +} + /// Check that if any Taker connection went idle. /// /// If a connection remains idle for more than idle timeout time, thats a potential DOS attack. @@ -627,18 +670,14 @@ pub(crate) fn check_for_idle_states(maker: Arc) -> Result<(), MakerError> let no_response_since = current_time.saturating_duration_since(*last_connected_time); - log::info!( - "[{}] No response from {} in {:?}", - maker.config.port, - ip, - no_response_since - ); + if no_response_since > conn_timeout { log::error!( - "[{}] Potential Dropped Connection from {}", - maker.config.port, - ip + "[{}] Potential Dropped Connection from taker. No response since : {} secs. Recovering from swap", + maker.config.network_port, + no_response_since.as_secs() ); + // Extract Incoming and Outgoing contracts, and timelock spends of the contract transactions. // fully signed. for (og_sc, ic_sc) in state @@ -663,7 +702,7 @@ pub(crate) fn check_for_idle_states(maker: Arc) -> Result<(), MakerError> let maker_clone = maker.clone(); log::info!( "[{}] Spawning recovery thread after Taker dropped", - maker.config.port + maker.config.network_port ); let handle = std::thread::Builder::new() .name("Swap Recovery Thread".to_string()) @@ -711,18 +750,14 @@ pub(crate) fn recover_from_swap( { log::info!( "[{}] Incoming Contract Already Broadcasted", - maker.config.port + maker.config.network_port ); } else { - maker - .wallet - .read()? - .rpc - .send_raw_transaction(&tx) - .map_err(WalletError::Rpc)?; + maker.wallet.read()?.send_tx(&tx)?; + log::info!( "[{}] Broadcasted Incoming Contract : {}", - maker.config.port, + maker.config.network_port, tx.compute_txid() ); } @@ -734,13 +769,11 @@ pub(crate) fn recover_from_swap( .expect("Incoming swapcoin expected"); log::info!( "[{}] Removed Incoming Swapcoin From Wallet, Contract Txid : {}", - maker.config.port, + maker.config.network_port, removed_incoming.contract_tx.compute_txid() ); } - maker.wallet.read()?.save_to_disk()?; - //broadcast all the outgoing contracts for ((_, tx), _) in outgoings.iter() { if maker @@ -752,27 +785,27 @@ pub(crate) fn recover_from_swap( { log::info!( "[{}] Outgoing Contract already broadcasted", - maker.config.port + maker.config.network_port ); } else { - maker - .wallet - .read()? - .rpc - .send_raw_transaction(tx) - .map_err(WalletError::Rpc)?; + let txid = maker.wallet.read()?.send_tx(tx)?; log::info!( "[{}] Broadcasted Outgoing Contract : {}", - maker.config.port, - tx.compute_txid() + maker.config.network_port, + txid ); } } + // Save the wallet here before going into the expensive loop. + maker.get_wallet().write()?.sync()?; + maker.get_wallet().read()?.save_to_disk()?; + log::info!("Wallet file synced and saved to disk."); + // Check for contract confirmations and broadcast timelocked transaction let mut timelock_boardcasted = Vec::new(); - loop { - for ((_, contract), (timelock, timelocked_tx)) in outgoings.iter() { + while !maker.shutdown.load(Relaxed) { + for ((outgoing_reedemscript, contract), (timelock, timelocked_tx)) in outgoings.iter() { // We have already broadcasted this tx, so skip if timelock_boardcasted.contains(&timelocked_tx) { continue; @@ -786,8 +819,8 @@ pub(crate) fn recover_from_swap( .get_raw_transaction_info(&contract.compute_txid(), None) { log::info!( - "[{}] Contract Tx : {}, reached confirmation : {:?}, Required Confirmation : {}", - maker.config.port, + "[{}] Contract Txid : {} reached confirmation : {:?}, Required Confirmation : {}", + maker.config.network_port, contract.compute_txid(), result.confirmations, timelock @@ -796,60 +829,53 @@ pub(crate) fn recover_from_swap( // Now the transaction is confirmed in a block, check for required maturity if confirmation > (*timelock as u32) { log::info!( - "[{}] Timelock maturity of {} blocks for Contract Tx is reached : {}", - maker.config.port, + "[{}] Timelock maturity of {} blocks reached for Contract Txid : {}", + maker.config.network_port, timelock, contract.compute_txid() ); log::info!( "[{}] Broadcasting timelocked tx: {}", - maker.config.port, + maker.config.network_port, timelocked_tx.compute_txid() ); - maker - .wallet - .read()? - .rpc - .send_raw_transaction(timelocked_tx) - .map_err(WalletError::Rpc)?; + maker.wallet.read()?.send_tx(timelocked_tx)?; timelock_boardcasted.push(timelocked_tx); + + let outgoing_removed = maker + .wallet + .write()? + .remove_outgoing_swapcoin(outgoing_reedemscript)? + .expect("outgoing swapcoin expected"); + + log::info!( + "[{}] Removed Outgoing Swapcoin from Wallet, Contract Txid: {}", + maker.config.network_port, + outgoing_removed.contract_tx.compute_txid() + ); + + log::info!("initializing Wallet Sync."); + { + let mut wallet_write = maker.wallet.write()?; + wallet_write.sync()?; + wallet_write.save_to_disk()?; + } + log::info!("Completed Wallet Sync."); } } } } - // Everything is broadcasted. Remove swapcoins from wallet - if timelock_boardcasted.len() == outgoings.len() { - for ((outgoing_reedemscript, _), _) in outgoings { - let outgoing_removed = maker - .wallet - .write()? - .remove_outgoing_swapcoin(&outgoing_reedemscript)? - .expect("outgoing swapcoin expected"); - log::info!( - "[{}] Removed Outgoing Swapcoin from Wallet, Contract Txid: {}", - maker.config.port, - outgoing_removed.contract_tx.compute_txid() - ); - } - log::info!("initializing Wallet Sync."); - { - let mut wallet_write = maker.wallet.write()?; - wallet_write.sync()?; - wallet_write.save_to_disk()?; - } - log::info!("Completed Wallet Sync."); - // For test, shutdown the maker at this stage. - #[cfg(feature = "integration-test")] - maker.shutdown.store(true, Relaxed); - return Ok(()); - } + #[cfg(feature = "integration-test")] + maker.shutdown.store(true, Relaxed); + // Sleep before next blockchain scan let block_lookup_interval = if cfg!(feature = "integration-test") { Duration::from_secs(10) } else { - Duration::from_secs(300) + Duration::from_secs(60) }; std::thread::sleep(block_lookup_interval); } + Ok(()) } diff --git a/src/maker/config.rs b/src/maker/config.rs index b1099dcc..ed0b9da2 100644 --- a/src/maker/config.rs +++ b/src/maker/config.rs @@ -13,7 +13,7 @@ use super::api::MIN_SWAP_AMOUNT; #[derive(Debug, Clone, PartialEq)] pub struct MakerConfig { /// Network listening port - pub port: u16, + pub network_port: u16, /// RPC listening port pub rpc_port: u16, /// Minimum Coinswap amount @@ -33,13 +33,19 @@ pub struct MakerConfig { impl Default for MakerConfig { fn default() -> Self { Self { - port: 6102, + network_port: 6102, rpc_port: 6103, min_swap_amount: MIN_SWAP_AMOUNT, socks_port: 19050, directory_server_address: "127.0.0.1:8080".to_string(), - fidelity_amount: 5_000_000, // 5 million sats - fidelity_timelock: 26_000, // Approx 6 months of blocks + #[cfg(feature = "integration-test")] + fidelity_amount: 5_000_000, // 0.05 BTC for tests + #[cfg(feature = "integration-test")] + fidelity_timelock: 26_000, // Approx 6 months of blocks for test + #[cfg(not(feature = "integration-test"))] + fidelity_amount: 50_000, // 50K sats for production + #[cfg(not(feature = "integration-test"))] + fidelity_timelock: 2160, // Approx 15 days of blocks in production connection_type: { #[cfg(feature = "tor")] { @@ -89,7 +95,7 @@ impl MakerConfig { ); Ok(MakerConfig { - port: parse_field(config_map.get("port"), default_config.port), + network_port: parse_field(config_map.get("network_port"), default_config.network_port), rpc_port: parse_field(config_map.get("rpc_port"), default_config.rpc_port), min_swap_amount: parse_field( config_map.get("min_swap_amount"), @@ -118,7 +124,7 @@ impl MakerConfig { // Method to serialize the MakerConfig into a TOML string and write it to a file pub(crate) fn write_to_file(&self, path: &Path) -> std::io::Result<()> { let toml_data = format!( - "port = {} + "network_port = {} rpc_port = {} min_swap_amount = {} socks_port = {} @@ -126,7 +132,7 @@ directory_server_address = {} fidelity_amount = {} fidelity_timelock = {} connection_type = {:?}", - self.port, + self.network_port, self.rpc_port, self.min_swap_amount, self.socks_port, @@ -168,11 +174,10 @@ mod tests { #[test] fn test_valid_config() { let contents = r#" - [maker_config] - port = 6102 + network_port = 6102 rpc_port = 6103 required_confirms = 1 - min_swap_amount = 100000 + min_swap_amount = 10000 socks_port = 19050 "#; let config_path = create_temp_config(contents, "valid_maker_config.toml"); @@ -187,16 +192,16 @@ mod tests { fn test_missing_fields() { let contents = r#" [maker_config] - port = 6103 + network_port = 6103 "#; let config_path = create_temp_config(contents, "missing_fields_maker_config.toml"); let config = MakerConfig::new(Some(&config_path)).unwrap(); remove_temp_config(&config_path); - assert_eq!(config.port, 6103); + assert_eq!(config.network_port, 6103); assert_eq!( MakerConfig { - port: 6102, + network_port: 6102, ..config }, MakerConfig::default() @@ -207,7 +212,7 @@ mod tests { fn test_incorrect_data_type() { let contents = r#" [maker_config] - port = "not_a_number" + network_port = "not_a_number" "#; let config_path = create_temp_config(contents, "incorrect_type_maker_config.toml"); let config = MakerConfig::new(Some(&config_path)).unwrap(); diff --git a/src/maker/handlers.rs b/src/maker/handlers.rs index 358223c8..f7599056 100644 --- a/src/maker/handlers.rs +++ b/src/maker/handlers.rs @@ -7,14 +7,13 @@ //! The file includes functions to validate and sign contract transactions, verify proof of funding, and handle unexpected recovery scenarios. //! Implements the core functionality for a Maker in a Bitcoin coinswap protocol. -use std::{net::IpAddr, sync::Arc, time::Instant}; +use std::{collections::HashMap, net::IpAddr, sync::Arc, time::Instant}; use bitcoin::{ hashes::Hash, secp256k1::{self, Secp256k1}, Amount, OutPoint, PublicKey, Transaction, Txid, }; -use bitcoind::bitcoincore_rpc::RpcApi; use super::{ api::{ @@ -234,19 +233,24 @@ impl Maker { acc + txinfo.funding_input_value.to_sat() }); - if total_funding_amount >= self.config.min_swap_amount - && total_funding_amount < self.wallet.read()?.store.offer_maxsize - { - log::info!( - "[{}] Total Funding Amount = {} | Funding Txids = {:?}", - self.config.port, - Amount::from_sat(total_funding_amount), - funding_txids - ); + log::info!( + "[{}] Total Funding Amount = {} | Funding Txids = {:?}", + self.config.network_port, + Amount::from_sat(total_funding_amount), + funding_txids + ); + + let max_size = self.wallet.read()?.store.offer_maxsize; + if total_funding_amount >= self.config.min_swap_amount && total_funding_amount <= max_size { Ok(MakerToTakerMessage::RespContractSigsForSender( ContractSigsForSender { sigs }, )) } else { + log::error!( + "Funding amount not within min/max limit, min {}, max {}", + self.config.min_swap_amount, + max_size + ); Err(MakerError::General("not enough funds")) } } @@ -268,7 +272,7 @@ impl Maker { let hashvalue = self.verify_proof_of_funding(&message)?; log::info!( "[{}] Validated Proof of Funding of receiving swap. Adding Incoming Swaps.", - self.config.port + self.config.network_port ); // Import transactions and addresses into Bitcoin core's wallet. @@ -328,11 +332,6 @@ impl Maker { .incoming_swapcoins .contains(&incoming_swapcoin) { - log::debug!( - "[{}] Incoming SwapCoins: {:?}", - self.config.port, - incoming_swapcoin - ); connection_state.incoming_swapcoins.push(incoming_swapcoin); } } @@ -405,7 +404,7 @@ impl Maker { log::info!( "[{}] Outgoing Funding Txids: {:?}.", - self.config.port, + self.config.network_port, my_funding_txes .iter() .map(|tx| tx.compute_txid()) @@ -414,7 +413,7 @@ impl Maker { log::info!( "[{}] Incoming Swap Amount = {} | Outgoing Swap Amount = {} | Coinswap Fee = {} | Refund Tx locktime (blocks) = {} | Total Funding Tx Mining Fees = {} |", - self.config.port, + self.config.network_port, Amount::from_sat(incoming_amount), Amount::from_sat(outgoing_amount), Amount::from_sat(act_coinswap_fees), @@ -513,18 +512,14 @@ impl Maker { let mut my_funding_txids = Vec::::new(); for my_funding_tx in &connection_state.pending_funding_txes { - let txid = self - .wallet - .read()? - .rpc - .send_raw_transaction(my_funding_tx) - .map_err(|e| MakerError::Wallet(e.into()))?; + let txid = self.wallet.read()?.send_tx(my_funding_tx)?; + assert_eq!(txid, my_funding_tx.compute_txid()); my_funding_txids.push(txid); } log::info!( "[{}] Outgoing Funding Txids: {:?}", - self.config.port, + self.config.network_port, my_funding_txids ); @@ -600,7 +595,7 @@ impl Maker { log::info!( "[{}] received preimage for hashvalue={}", - self.config.port, + self.config.network_port, hashvalue ); let mut swapcoin_private_keys = Vec::::new(); @@ -640,6 +635,11 @@ impl Maker { .expect("incoming swapcoin not found") .apply_privkey(swapcoin_private_key.key)?; } + + // Reset the connection state so watchtowers are not triggered. + let mut conn_state = self.connection_state.lock()?; + *conn_state = HashMap::default(); + log::info!("initializing Wallet Sync."); { let mut wallet_write = self.wallet.write()?; diff --git a/src/maker/rpc/messages.rs b/src/maker/rpc/messages.rs index 8a6b272b..c1baf2e6 100644 --- a/src/maker/rpc/messages.rs +++ b/src/maker/rpc/messages.rs @@ -1,9 +1,12 @@ -use std::fmt::Display; +use std::{collections::HashMap, fmt::Display}; +use bitcoin::Txid; use bitcoind::bitcoincore_rpc::json::ListUnspentResultEntry; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use crate::wallet::FidelityBond; + /// Enum representing RPC message requests. /// /// These messages are used for various operations in the Maker-rpc communication. @@ -12,17 +15,17 @@ use std::path::PathBuf; pub enum RpcMsgReq { /// Ping request to check connectivity. Ping, - /// Request to fetch UTXOs in the seed pool. - SeedUtxo, - /// Request to fetch UTXOs in the swap pool. + /// Request to fetch all utxos in the wallet. + Utxo, + /// Request to fetch only swap utxos in the wallet. SwapUtxo, /// Request to fetch UTXOs in the contract pool. ContractUtxo, /// Request to fetch UTXOs in the fidelity pool. FidelityUtxo, - /// Request to retrieve the total balance in the seed pool. - SeedBalance, - /// Request to retrieve the total balance in the swap pool. + /// Request to retreive the total spenable balance in wallet. + Balance, + /// Request to retrieve the total swap balance in wallet. SwapBalance, /// Request to retrieve the total balance in the contract pool. ContractBalance, @@ -45,6 +48,12 @@ pub enum RpcMsgReq { GetDataDir, /// Request to stop the Maker server. Stop, + /// Request to reddem a fidelity bond for a given index. + RedeemFidelity(u32), + /// Request to list all active and past fidelity bonds. + ListFidelity, + /// Request to sync the internal wallet with blockchain. + SyncWallet, } /// Enum representing RPC message responses. @@ -55,9 +64,9 @@ pub enum RpcMsgReq { pub enum RpcMsgResp { /// Response to a Ping request. Pong, - /// Response containing UTXOs in the seed pool. - SeedUtxoResp { - /// List of UTXOs in the seed pool. + /// Response containing all spendable UTXOs + UtxoResp { + /// List of spndable UTXOs in the wallet. utxos: Vec, }, /// Response containing UTXOs in the swap pool. @@ -93,6 +102,12 @@ pub enum RpcMsgResp { GetDataDirResp(PathBuf), /// Response indicating the server has been shut down. Shutdown, + /// Response with the fidelity spending txid. + FidelitySpend(Txid), + /// Response with the internal server error. + ServerError(String), + /// Response listing all current and past fidelity bonds. + ListBonds(HashMap), } impl Display for RpcMsgResp { @@ -104,14 +119,17 @@ impl Display for RpcMsgResp { Self::ContractBalanceResp(bal) => write!(f, "{} sats", bal), Self::SwapBalanceResp(bal) => write!(f, "{} sats", bal), Self::FidelityBalanceResp(bal) => write!(f, "{} sats", bal), - Self::SeedUtxoResp { utxos } => write!(f, "{:?}", utxos), - Self::SwapUtxoResp { utxos } => write!(f, "{:?}", utxos), - Self::FidelityUtxoResp { utxos } => write!(f, "{:?}", utxos), - Self::ContractUtxoResp { utxos } => write!(f, "{:?}", utxos), + Self::UtxoResp { utxos } => write!(f, "{:#?}", utxos), + Self::SwapUtxoResp { utxos } => write!(f, "{:#?}", utxos), + Self::FidelityUtxoResp { utxos } => write!(f, "{:#?}", utxos), + Self::ContractUtxoResp { utxos } => write!(f, "{:#?}", utxos), Self::SendToAddressResp(tx_hex) => write!(f, "{}", tx_hex), Self::GetTorAddressResp(addr) => write!(f, "{}", addr), Self::GetDataDirResp(path) => write!(f, "{}", path.display()), Self::Shutdown => write!(f, "Shutdown Initiated"), + Self::FidelitySpend(txid) => write!(f, "{}", txid), + Self::ServerError(e) => write!(f, "{}", e), + Self::ListBonds(v) => write!(f, "{:#?}", v), } } } diff --git a/src/maker/rpc/server.rs b/src/maker/rpc/server.rs index c5284dcc..07420780 100644 --- a/src/maker/rpc/server.rs +++ b/src/maker/rpc/server.rs @@ -22,12 +22,8 @@ fn handle_request(maker: &Arc, socket: &mut TcpStream) -> Result<(), Make let rpc_request: RpcMsgReq = serde_cbor::from_slice(&msg_bytes)?; log::info!("RPC request received: {:?}", rpc_request); - match rpc_request { - RpcMsgReq::Ping => { - if let Err(e) = send_message(socket, &RpcMsgResp::Pong) { - log::info!("Error sending RPC response {:?}", e); - }; - } + let resp = match rpc_request { + RpcMsgReq::Ping => RpcMsgResp::Pong, RpcMsgReq::ContractUtxo => { let utxos = maker .get_wallet() @@ -36,10 +32,7 @@ fn handle_request(maker: &Arc, socket: &mut TcpStream) -> Result<(), Make .iter() .map(|(l, _)| l.clone()) .collect::>(); - let resp = RpcMsgResp::ContractUtxoResp { utxos }; - if let Err(e) = send_message(socket, &resp) { - log::info!("Error sending RPC response {:?}", e); - }; + RpcMsgResp::ContractUtxoResp { utxos } } RpcMsgReq::FidelityUtxo => { let utxos = maker @@ -49,23 +42,17 @@ fn handle_request(maker: &Arc, socket: &mut TcpStream) -> Result<(), Make .iter() .map(|(l, _)| l.clone()) .collect::>(); - let resp = RpcMsgResp::FidelityUtxoResp { utxos }; - if let Err(e) = send_message(socket, &resp) { - log::info!("Error sending RPC response {:?}", e); - }; + RpcMsgResp::FidelityUtxoResp { utxos } } - RpcMsgReq::SeedUtxo => { + RpcMsgReq::Utxo => { let utxos = maker .get_wallet() .read()? - .list_descriptor_utxo_spend_info(None)? + .list_all_utxo_spend_info(None)? .iter() .map(|(l, _)| l.clone()) .collect::>(); - let resp = RpcMsgResp::SeedUtxoResp { utxos }; - if let Err(e) = send_message(socket, &resp) { - log::info!("Error sending RPC response {:?}", e); - }; + RpcMsgResp::UtxoResp { utxos } } RpcMsgReq::SwapUtxo => { let utxos = maker @@ -75,45 +62,27 @@ fn handle_request(maker: &Arc, socket: &mut TcpStream) -> Result<(), Make .iter() .map(|(l, _)| l.clone()) .collect::>(); - let resp = RpcMsgResp::SwapUtxoResp { utxos }; - if let Err(e) = send_message(socket, &resp) { - log::info!("Error sending RPC response {:?}", e); - }; + RpcMsgResp::SwapUtxoResp { utxos } } RpcMsgReq::ContractBalance => { let balance = maker.get_wallet().read()?.balance_live_contract(None)?; - let resp = RpcMsgResp::ContractBalanceResp(balance.to_sat()); - if let Err(e) = send_message(socket, &resp) { - log::info!("Error sending RPC response {:?}", e); - }; + RpcMsgResp::ContractBalanceResp(balance.to_sat()) } RpcMsgReq::FidelityBalance => { let balance = maker.get_wallet().read()?.balance_fidelity_bonds(None)?; - let resp = RpcMsgResp::FidelityBalanceResp(balance.to_sat()); - if let Err(e) = send_message(socket, &resp) { - log::info!("Error sending RPC response {:?}", e); - }; - } - RpcMsgReq::SeedBalance => { - let balance = maker.get_wallet().read()?.balance_descriptor_utxo(None)?; - let resp = RpcMsgResp::SeedBalanceResp(balance.to_sat()); - if let Err(e) = send_message(socket, &resp) { - log::info!("Error sending RPC response {:?}", e); - }; + RpcMsgResp::FidelityBalanceResp(balance.to_sat()) + } + RpcMsgReq::Balance => { + let balance = maker.get_wallet().read()?.spendable_balance()?; + RpcMsgResp::SeedBalanceResp(balance.to_sat()) } RpcMsgReq::SwapBalance => { let balance = maker.get_wallet().read()?.balance_swap_coins(None)?; - let resp = RpcMsgResp::SwapBalanceResp(balance.to_sat()); - if let Err(e) = send_message(socket, &resp) { - log::info!("Error sending RPC response {:?}", e); - }; + RpcMsgResp::SwapBalanceResp(balance.to_sat()) } RpcMsgReq::NewAddress => { let new_address = maker.get_wallet().write()?.get_next_external_address()?; - let resp = RpcMsgResp::NewAddressResp(new_address.to_string()); - if let Err(e) = send_message(socket, &resp) { - log::info!("Error sending RPC response {:?}", e); - }; + RpcMsgResp::NewAddressResp(new_address.to_string()) } RpcMsgReq::SendToAddress { address, @@ -137,46 +106,64 @@ fn handle_request(maker: &Arc, socket: &mut TcpStream) -> Result<(), Make let calculated_fee_rate = fee / (tx.weight()); log::info!("Calculated FeeRate : {:#}", calculated_fee_rate); - let resp = - RpcMsgResp::SendToAddressResp(bitcoin::consensus::encode::serialize_hex(&tx)); - if let Err(e) = send_message(socket, &resp) { - log::info!("Error sending RPC response {:?}", e); - }; + let txid = maker.get_wallet().read()?.send_tx(&tx)?; + + RpcMsgResp::SendToAddressResp(txid.to_string()) } RpcMsgReq::GetDataDir => { let path = maker.get_data_dir(); - let resp = RpcMsgResp::GetDataDirResp(path.clone()); - if let Err(e) = send_message(socket, &resp) { - log::info!("Error sending RPC response {:?}", e); - }; + RpcMsgResp::GetDataDirResp(path.clone()) } RpcMsgReq::GetTorAddress => { if maker.config.connection_type == ConnectionType::CLEARNET { - let resp = RpcMsgResp::GetTorAddressResp("Maker is not running on TOR".to_string()); - if let Err(e) = send_message(socket, &resp) { - log::info!("Error sending RPC response {:?}", e); - }; + RpcMsgResp::GetTorAddressResp("Maker is not running on TOR".to_string()) } else { - let maker_hs_path_str = - format!("/tmp/tor-rust-maker{}/hs-dir/hostname", maker.config.port); + let maker_hs_path_str = format!( + "/tmp/tor-rust-maker{}/hs-dir/hostname", + maker.config.network_port + ); let mut maker_file = File::open(maker_hs_path_str)?; let mut maker_onion_addr: String = String::new(); maker_file.read_to_string(&mut maker_onion_addr)?; maker_onion_addr.pop(); // Remove `\n` at the end. - let maker_address = format!("{}:{}", maker_onion_addr, maker.config.port); + let maker_address = format!("{}:{}", maker_onion_addr, maker.config.network_port); - let resp = RpcMsgResp::GetTorAddressResp(maker_address); - if let Err(e) = send_message(socket, &resp) { - log::info!("Error sending RPC response {:?}", e); - }; + RpcMsgResp::GetTorAddressResp(maker_address) } } RpcMsgReq::Stop => { maker.shutdown.store(true, Relaxed); - if let Err(e) = send_message(socket, &RpcMsgResp::Shutdown) { - log::info!("Error sending RPC response {:?}", e); - }; + RpcMsgResp::Shutdown + } + + RpcMsgReq::RedeemFidelity(index) => { + let txid = maker.get_wallet().write()?.redeem_fidelity(index)?; + RpcMsgResp::FidelitySpend(txid) } + RpcMsgReq::ListFidelity => { + let list = maker + .get_wallet() + .read()? + .get_fidelity_bonds() + .iter() + .map(|(i, (b, _, is_spent))| (*i, (b.clone(), *is_spent))) + .collect(); + + RpcMsgResp::ListBonds(list) + } + RpcMsgReq::SyncWallet => { + log::info!("Initializing wallet sync"); + if let Err(e) = maker.get_wallet().write()?.sync() { + RpcMsgResp::ServerError(format!("{:?}", e)) + } else { + log::info!("Completed wallet sync"); + RpcMsgResp::Pong + } + } + }; + + if let Err(e) = send_message(socket, &resp) { + log::error!("Error sending RPC response {:?}", e); } Ok(()) @@ -188,7 +175,7 @@ pub(crate) fn start_rpc_server(maker: Arc) -> Result<(), MakerError> { let listener = Arc::new(TcpListener::bind(&rpc_socket)?); log::info!( "[{}] RPC socket binding successful at {}", - maker.config.port, + maker.config.network_port, rpc_socket ); @@ -200,7 +187,16 @@ pub(crate) fn start_rpc_server(maker: Arc) -> Result<(), MakerError> { log::info!("Got RPC request from: {}", addr); stream.set_read_timeout(Some(Duration::from_secs(20)))?; stream.set_write_timeout(Some(Duration::from_secs(20)))?; - handle_request(&maker, &mut stream)?; + // Do not cause hard error if a rpc request fails + if let Err(e) = handle_request(&maker, &mut stream) { + log::error!("Error processing RPC Request: {:?}", e); + // Send the error back to client. + if let Err(e) = + send_message(&mut stream, &RpcMsgResp::ServerError(format!("{:?}", e))) + { + log::error!("Error sending RPC response {:?}", e); + }; + } } Err(e) => { diff --git a/src/maker/server.rs b/src/maker/server.rs index eb9f8023..c47df7d3 100644 --- a/src/maker/server.rs +++ b/src/maker/server.rs @@ -7,6 +7,8 @@ use std::{ io::ErrorKind, net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream}, + path::PathBuf, + process::Child, sync::{ atomic::{AtomicBool, Ordering::Relaxed}, Arc, @@ -15,15 +17,6 @@ use std::{ time::Duration, }; -#[cfg(feature = "tor")] -use std::io::Read; - -#[cfg(feature = "tor")] -use std::{ - fs, - path::{Path, PathBuf}, -}; - use bitcoin::{absolute::LockTime, Amount}; use bitcoind::bitcoincore_rpc::RpcApi; @@ -35,12 +28,15 @@ pub(crate) use super::{api::RPC_PING_INTERVAL, Maker}; use crate::{ error::NetError, maker::{ - api::{check_for_broadcasted_contracts, check_for_idle_states, ConnectionState}, + api::{ + check_for_broadcasted_contracts, check_for_idle_states, + restore_broadcasted_contracts_on_reboot, ConnectionState, + }, handlers::handle_message, rpc::start_rpc_server, }, protocol::messages::{DnsMetadata, DnsRequest, TakerToMakerMessage}, - utill::{read_message, send_message, ConnectionType, HEART_BEAT_INTERVAL}, + utill::{get_tor_addrs, read_message, send_message, ConnectionType, HEART_BEAT_INTERVAL}, wallet::WalletError, }; @@ -51,13 +47,6 @@ use crate::maker::error::MakerError; // Default values for Maker configurations pub(crate) const _DIRECTORY_SERVERS_REFRESH_INTERVAL_SECS: u64 = 60 * 60 * 12; // 12 Hours -pub(crate) const _IDLE_CONNECTION_TIMEOUT: u64 = 300; - -#[cfg(feature = "tor")] -type OptionalJoinHandle = Option>; - -#[cfg(not(feature = "tor"))] -type OptionalJoinHandle = Option<()>; /// Fetches the Maker and DNS address, and sends maker address to the DNS server. /// Depending upon ConnectionType and test/prod environment, different maker address and DNS addresses are returned. @@ -65,10 +54,9 @@ type OptionalJoinHandle = Option<()>; /// /// Tor thread is spawned only if ConnectionType=TOR and --feature=tor is enabled. /// Errors if ConncetionType=TOR but, the tor feature is not enabled. -fn network_bootstrap(maker: Arc) -> Result<(String, OptionalJoinHandle), MakerError> { - let maker_port = maker.config.port; - let mut tor_handle = None; - let (maker_address, dns_address) = match maker.config.connection_type { +fn network_bootstrap(maker: Arc) -> Result, MakerError> { + let maker_port = maker.config.network_port; + let (maker_address, dns_address, tor_handle) = match maker.config.connection_type { ConnectionType::CLEARNET => { let maker_address = format!("127.0.0.1:{}", maker_port); let dns_address = if cfg!(feature = "integration-test") { @@ -77,65 +65,81 @@ fn network_bootstrap(maker: Arc) -> Result<(String, OptionalJoinHandle), maker.config.directory_server_address.clone() }; - (maker_address, dns_address) + (maker_address, dns_address, None) } #[cfg(feature = "tor")] ConnectionType::TOR => { let maker_socks_port = maker.config.socks_port; - let tor_log_dir = format!("/tmp/tor-rust-maker{}/log", maker_port); + let tor_dir = maker.data_dir.join("tor"); + let tor_log_file = tor_dir.join("log"); - if Path::new(&tor_log_dir).exists() { - match fs::remove_file(&tor_log_dir) { - Ok(_) => log::info!( - "[{}] Previous Maker log file deleted successfully", - maker_port - ), - Err(_) => log::error!("[{}] Error deleting Maker log file", maker_port), + // Hard error if previous log file can't be removed, as monitor_log_for_completion doesn't work with existing file. + // Tell the user to manually delete the file and restart. + if tor_log_file.exists() { + if let Err(e) = std::fs::remove_file(&tor_log_file) { + log::error!( + "Error removing previous tor log. Please delete the file and restart. | {:?}", + tor_log_file + ); + return Err(e.into()); + } else { + log::info!("Previous tor log file deleted succesfully"); } } - tor_handle = Some(crate::tor::spawn_tor( + let tor_handle = Some(crate::tor::spawn_tor( maker_socks_port, maker_port, - format!("/tmp/tor-rust-maker{}", maker_port), - )); - thread::sleep(Duration::from_secs(10)); + tor_dir.to_str().unwrap().to_owned(), + )?); - if let Err(e) = monitor_log_for_completion(&PathBuf::from(tor_log_dir), "100%") { - log::error!("[{}] Error monitoring log file: {}", maker_port, e); - } - - log::info!("[{}] Maker tor is instantiated", maker_port); + log::info!( + "[{}] waiting for tor setup to compelte.", + maker.config.network_port + ); - let maker_hs_path_str = - format!("/tmp/tor-rust-maker{}/hs-dir/hostname", maker.config.port); - let mut maker_file = fs::File::open(maker_hs_path_str)?; - let mut maker_onion_addr: String = String::new(); - maker_file.read_to_string(&mut maker_onion_addr)?; + // TODO: move this function inside `spawn_tor` routine. ` + if let Err(e) = + monitor_log_for_completion(&tor_log_file, "Bootstrapped 100% (done): Done") + { + log::error!( + "[{}] Error monitoring log file {:?}. Remove the file and restart again. | {}", + maker_port, + tor_log_file, + e + ); + return Err(e.into()); + } - maker_onion_addr.pop(); // Remove `\n` at the end. + log::info!("[{}] tor setup complete!", maker_port); - let maker_address = format!("{}:{}", maker_onion_addr, maker.config.port); + let maker_onion_addr = get_tor_addrs(&tor_dir)?; + let maker_address = format!("{}:{}", maker_onion_addr, maker.config.network_port); let directory_onion_address = if cfg!(feature = "integration-test") { - let directory_hs_path_str = "/tmp/tor-rust-directory/hs-dir/hostname"; - let mut directory_file = fs::File::open(directory_hs_path_str)?; - let mut directory_onion_addr: String = String::new(); - - directory_file.read_to_string(&mut directory_onion_addr)?; - directory_onion_addr.pop(); // Remove `\n` at the end. + let directory_onion_addr = + get_tor_addrs(&PathBuf::from("/tmp/tor-rust-directory"))?; format!("{}:{}", directory_onion_addr, 8080) } else { maker.config.directory_server_address.clone() }; - (maker_address, directory_onion_address) + (maker_address, directory_onion_address, tor_handle) } }; + log::info!( + "[{}] Server is listening at {}", + maker.config.network_port, + maker_address + ); + setup_fidelity_bond(&maker, &maker_address)?; - maker.wallet.write()?.refresh_offer_maxsize_cache()?; + log::info!( + "Max offer size : {} sats", + maker.get_wallet().read()?.store.offer_maxsize + ); let proof = maker .highest_fidelity_proof @@ -153,37 +157,40 @@ fn network_bootstrap(maker: Arc) -> Result<(String, OptionalJoinHandle), metadata: dns_metadata, }; - // Keep trying until send is successful. - loop { - 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()), - }; + // Loop until shoutdown is initiated. + while !maker.shutdown.load(Relaxed) { + let stream = 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(HEART_BEAT_INTERVAL); - continue; - } + log::info!( + "[{}] Connecting to DNS: {}", + maker.config.network_port, + dns_address + ); + + let mut stream = match stream { + Ok(s) => s, + Err(e) => { + log::warn!( + "[{}] TCP connection error with directory, reattempting: {}", + maker_port, + e + ); + thread::sleep(HEART_BEAT_INTERVAL); + continue; } }; if let Err(e) = send_message(&mut stream, &request) { log::warn!( - "[{}] Failed to send maker address to directory, reattempting: {}", + "[{}] Failed to send our address to directory, reattempting: {}", maker_port, e ); @@ -192,26 +199,49 @@ fn network_bootstrap(maker: Arc) -> Result<(String, OptionalJoinHandle), }; log::info!( - "[{}] Successfully sent maker address to directory", - maker_port + "[{}] Successfully sent our address to dns at {}", + maker_port, + dns_address ); break; } - Ok((maker_address, tor_handle)) + Ok(tor_handle) } /// Checks if the wallet already has fidelity bonds. if not, create the first fidelity bond. fn setup_fidelity_bond(maker: &Arc, maker_address: &str) -> Result<(), MakerError> { let highest_index = maker.get_wallet().read()?.get_highest_fidelity_index()?; if let Some(i) = highest_index { + let wallet_read = maker.get_wallet().read()?; + let (bond, _, _) = wallet_read.get_fidelity_bonds().get(&i).unwrap(); + + let current_height = wallet_read + .rpc + .get_block_count() + .map_err(WalletError::Rpc)? as u32; + let highest_proof = maker .get_wallet() .read()? .generate_fidelity_proof(i, maker_address)?; + + log::info!( + "Highest bond at outpoint {} | index {} | Amount {:?} sats | Remaining Timelock for expiry : {:?} Blocks | Current Bond Value : {:?} sats", + highest_proof.bond.outpoint, + i, + bond.amount.to_sat(), + current_height - bond.lock_time.to_consensus_u32(), + wallet_read.calculate_bond_value(i)?.to_sat() + ); + log::info!("Bond amount : {:?}", bond.amount.to_sat()); + // TODO: work remainig + // log::info!("") + let mut proof = maker.highest_fidelity_proof.write()?; *proof = Some(highest_proof); } else { + // xxxxx // No bond in the wallet. Lets attempt to create one. let amount = Amount::from_sat(maker.config.fidelity_amount); let current_height = maker @@ -231,9 +261,13 @@ fn setup_fidelity_bond(maker: &Arc, maker_address: &str) -> Result<(), Ma let sleep_increment = 10; let mut sleep_multiplier = 0; - - log::info!("Fidelity value chosen = {:?} BTC", amount.to_btc()); - log::info!("Fidelity Tx fee = 1000 sats"); + log::info!("No active Fidelity Bonds found. Creating one."); + log::info!("Fidelity value chosen = {:?} sats", amount.to_sat()); + log::info!("Fidelity Tx fee = 300 sats"); + log::info!( + "Fidelity timelock {} blocks", + maker.config.fidelity_timelock + ); while !maker.shutdown.load(Relaxed) { sleep_multiplier += 1; @@ -258,7 +292,7 @@ fn setup_fidelity_bond(maker: &Arc, maker_address: &str) -> Result<(), Ma let amount = required - available; let addr = maker.get_wallet().write()?.get_next_external_address()?; - log::info!("Send at least {:.8} BTC to {:?} | If you send extra, that will be added to your swap balance", amount, addr); + log::info!("Send at least {:.8} BTC to {:?} | If you send extra, that will be added to your wallet balance", amount, addr); let total_sleep = sleep_increment * sleep_multiplier.min(10 * 60); log::info!("Next sync in {:?} secs", total_sleep); @@ -266,14 +300,17 @@ fn setup_fidelity_bond(maker: &Arc, maker_address: &str) -> Result<(), Ma } else { log::error!( "[{}] Fidelity Bond Creation failed: {:?}. Shutting Down Maker server", - maker.config.port, + maker.config.network_port, e ); return Err(e.into()); } } Ok(i) => { - log::info!("[{}] Successfully created fidelity bond", maker.config.port); + log::info!( + "[{}] Successfully created fidelity bond", + maker.config.network_port + ); let highest_proof = maker .get_wallet() .read()? @@ -281,7 +318,8 @@ fn setup_fidelity_bond(maker: &Arc, maker_address: &str) -> Result<(), Ma let mut proof = maker.highest_fidelity_proof.write()?; *proof = Some(highest_proof); - // save the wallet data to disk + // sync and save the wallet data to disk + maker.get_wallet().write()?.sync()?; maker.get_wallet().read()?.save_to_disk()?; break; } @@ -311,13 +349,19 @@ fn check_connection_with_core( } } if let Err(e) = maker.wallet.read()?.rpc.get_blockchain_info() { - log::info!( + log::error!( "[{}] RPC Connection failed. Reattempting {}", - maker.config.port, + maker.config.network_port, e ); rpc_ping_success = false; } else { + if !rpc_ping_success { + log::info!( + "[{}] Bitcoin Core RPC connection is live.", + maker.config.network_port + ); + } rpc_ping_success = true; } accepting_clients.store(rpc_ping_success, Relaxed); @@ -343,10 +387,11 @@ fn handle_client( Err(e) => { if let NetError::IO(e) = e { if e.kind() == ErrorKind::UnexpectedEof { - continue; + log::info!("[{}] Connection ended.", maker.config.network_port); + break; } else { // For any other errors, report them - log::error!("[{}] Net Error: {}", maker.config.port, e); + log::error!("[{}] Net Error: {}", maker.config.network_port, e); continue; } } @@ -354,14 +399,14 @@ fn handle_client( } let taker_msg: TakerToMakerMessage = serde_cbor::from_slice(&taker_msg_bytes)?; - log::info!("[{}] <=== {}", maker.config.port, taker_msg); + log::info!("[{}] <=== {}", maker.config.network_port, taker_msg); let reply = handle_message(&maker, &mut connection_state, taker_msg, client_addr.ip()); match reply { Ok(reply) => { if let Some(message) = reply { - log::info!("[{}] ===> {} ", maker.config.port, message); + log::info!("[{}] ===> {} ", maker.config.network_port, message); if let Err(e) = send_message(stream, &message) { log::error!("Closing due to IO error in sending message: {:?}", e); continue; @@ -374,13 +419,17 @@ fn handle_client( match &err { // Shutdown server if special behavior is set MakerError::SpecialBehaviour(sp) => { - log::error!("[{}] Maker Special Behavior : {:?}", maker.config.port, sp); + log::error!( + "[{}] Maker Special Behavior : {:?}", + maker.config.network_port, + sp + ); maker.shutdown.store(true, Relaxed); } e => { log::error!( "[{}] Internal message handling error occurred: {:?}", - maker.config.port, + maker.config.network_port, e ); } @@ -412,27 +461,17 @@ pub fn start_maker_server(maker: Arc) -> Result<(), MakerError> { // Initialize network connections. // Setup the wallet with fidelity bond. - let port = maker.config.port; + let port = maker.config.network_port; let network = maker.get_wallet().read()?.store.network; - let balance = maker.get_wallet().read()?.balance()?; - log::info!("[{}] Currency Network: {:?}", port, network); - log::info!("[{}] Total Wallet Balance: {:?}", port, balance); + let balance = maker.get_wallet().read()?.spendable_balance()?; + log::info!("[{}] Currency Network: {}", port, network); + log::info!("[{}] Total Wallet Balance: {}", port, balance); - let (maker_address, tor_thread) = network_bootstrap(maker.clone())?; + let _tor_thread = network_bootstrap(maker.clone())?; - let listener = - TcpListener::bind((Ipv4Addr::LOCALHOST, maker.config.port)).map_err(NetError::IO)?; - log::info!( - "[{}] Listening for client conns at: {}", - maker.config.port, - listener.local_addr()? - ); + let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, maker.config.network_port)) + .map_err(NetError::IO)?; listener.set_nonblocking(true)?; // Needed to not block a thread waiting for incoming connection. - log::info!( - "[{}] Maker Server Address: {}", - maker.config.port, - maker_address - ); // Global server Mutex, to switch on/off p2p network. let accepting_clients = Arc::new(AtomicBool::new(false)); @@ -508,7 +547,15 @@ pub fn start_maker_server(maker: Arc) -> Result<(), MakerError> { 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); + log::info!("[{}] Server Setup completed!! Use maker-cli to operate the server and the internal wallet.", maker.config.network_port); + } + + // Check if recovery is needed. + let (inc, out) = maker.wallet.read()?.find_unfinished_swapcoins(); + if !inc.is_empty() || !out.is_empty() { + log::info!("Incomplete swaps detected in the wallet. Starting recovery"); + let maker_clone = maker.clone(); + restore_broadcasted_contracts_on_reboot(maker_clone.clone())?; } // The P2P Client connection loop. @@ -519,9 +566,9 @@ pub fn start_maker_server(maker: Arc) -> Result<(), MakerError> { // Block client connections if accepting_client=false if !accepting_clients.load(Relaxed) { - log::debug!( - "[{}] Temporary failure in backend node. Not accepting swap request. Check your node if this error persists", - maker.config.port + log::warn!( + "[{}] Temporary failure in Bitcoin Core RPC.", + maker.config.network_port ); sleep(HEART_BEAT_INTERVAL); continue; @@ -529,16 +576,14 @@ pub fn start_maker_server(maker: Arc) -> Result<(), MakerError> { match listener.accept() { Ok((mut stream, client_addr)) => { - log::info!("[{}] Spawning Client Handler thread", maker.config.port); - let maker_for_handler = maker.clone(); - let client_handler_thread = thread::Builder::new() - .name("Client Handler Thread".to_string()) - .spawn(move || { - if let Err(e) = handle_client(maker_for_handler, &mut stream, client_addr) { - log::error!("[{}] Error Handling client request {:?}", port, e); - } - })?; - maker.thread_pool.add_thread(client_handler_thread); + log::info!( + "[{}] Received incoming connection", + maker.config.network_port + ); + + if let Err(e) = handle_client(maker, &mut stream, client_addr) { + log::error!("[{}] Error Handling client request {:?}", port, e); + } } Err(e) => { @@ -547,10 +592,9 @@ pub fn start_maker_server(maker: Arc) -> Result<(), MakerError> { } else { log::error!( "[{}] Error accepting incoming connection: {:?}", - maker.config.port, + maker.config.network_port, e ); - return Err(NetError::IO(e).into()); } } }; @@ -559,12 +603,17 @@ pub fn start_maker_server(maker: Arc) -> Result<(), MakerError> { } log::info!("[{}] Maker is shutting down.", port); + maker.thread_pool.join_all_threads()?; + #[cfg(feature = "tor")] - { - if maker.config.connection_type == ConnectionType::TOR && cfg!(feature = "tor") { - crate::tor::kill_tor_handles(tor_thread.expect("Tor thread expected")); + if let Some(mut tor_thread) = _tor_thread { + { + if maker.config.connection_type == ConnectionType::TOR && cfg!(feature = "tor") { + crate::tor::kill_tor_handles(&mut tor_thread); + } } } + log::info!("Shutdown wallet sync initiated."); maker.get_wallet().write()?.sync()?; log::info!("Shutdown wallet syncing completed."); diff --git a/src/market/directory.rs b/src/market/directory.rs index 065cf7c9..1be8d0a8 100644 --- a/src/market/directory.rs +++ b/src/market/directory.rs @@ -5,7 +5,6 @@ use bitcoin::{transaction::ParseOutPointError, OutPoint}; use bitcoind::bitcoincore_rpc::{self, Client, RpcApi}; -use std::collections::hash_map::Entry; use crate::{ market::rpc::start_rpc_server_thread, @@ -60,7 +59,9 @@ pub enum DirectoryServerError { /// /// This variant wraps a [`WalletError`] to capture issues arising during wallet-related operations. Wallet(WalletError), - /// Represents an error caused by a corrupted address file read. + /// Error indicating the address.dat file is corrupted. + /// + /// This can occur in case of incomplete shutdown or other ways a file can corrupt. AddressFileCorrupted(String), } @@ -118,7 +119,7 @@ pub struct DirectoryServer { /// RPC listening port pub rpc_port: u16, /// Network listening port - pub port: u16, + pub network_port: u16, /// Socks port pub socks_port: u16, /// Connection type @@ -127,7 +128,7 @@ pub struct DirectoryServer { pub data_dir: PathBuf, /// Shutdown flag to stop the directory server pub shutdown: AtomicBool, - /// A collection of maker addresses received from the Dns Server. + /// A store of all the received maker addresses indexed by fidelity bond outpoints. pub addresses: Arc>>, } @@ -135,7 +136,7 @@ impl Default for DirectoryServer { fn default() -> Self { Self { rpc_port: 4321, - port: 8080, + network_port: 8080, socks_port: 19060, connection_type: { #[cfg(feature = "tor")] @@ -217,7 +218,7 @@ impl DirectoryServer { Ok(DirectoryServer { rpc_port: parse_field(config_map.get("rpc_port"), default_dns.rpc_port), - port: parse_field(config_map.get("port"), default_dns.port), + network_port: parse_field(config_map.get("port"), default_dns.network_port), socks_port: parse_field(config_map.get("socks_port"), default_dns.socks_port), data_dir, shutdown: AtomicBool::new(false), @@ -234,22 +235,51 @@ impl DirectoryServer { &self, metadata: (String, OutPoint), ) -> Result<(), DirectoryServerError> { - match self.addresses.write()?.entry(metadata.1) { - Entry::Occupied(mut value) => { - log::info!("Maker Address Got Updated | Existing Address {} | New Address {} | Fidelity Outpoint {}", value.get(), metadata.0, metadata.1); - *value.get_mut() = metadata.0.clone(); - Ok(()) - } - Entry::Vacant(value) => { + let mut write_lock = self.addresses.write()?; + // Check if the value exists with a different key + if let Some(existing_key) = + write_lock + .iter() + .find_map(|(k, v)| if v == &metadata.0 { Some(*k) } else { None }) + { + // Update the fielity for the existing address + if existing_key != metadata.1 { log::info!( - "New Maker Address Added {} | Fidelity Outpoint {}", + "Fidelity update detected for address: {} | Old fidelity {} | New fidelity {}", metadata.0, + existing_key, metadata.1 ); - value.insert(metadata.0.clone()); - Ok(()) + write_lock.remove(&existing_key); + write_lock.insert(metadata.1, metadata.0); + } else { + log::info!("Maker data already exist for {}", metadata.0); } + } else if write_lock.contains_key(&metadata.1) { + // Update the address for the existing fidelity + if write_lock[&metadata.1] != metadata.0 { + let old_addr = write_lock + .insert(metadata.1, metadata.0.clone()) + .expect("value expected"); + log::info!( + "Address updated for fidelity: {} | old address {} | new address {}", + metadata.1, + old_addr, + metadata.0 + ); + } else { + log::info!("Maker data already exist for {}", metadata.0); + } + } else { + // Add a new entry if both fidelity and address are new + write_lock.insert(metadata.1, metadata.0.clone()); + log::info!( + "Added new maker info: Fidelity {} | Address {}", + metadata.1, + metadata.0 + ); } + Ok(()) } } @@ -352,6 +382,14 @@ pub fn start_directory_server( let rpc_client = bitcoincore_rpc::Client::try_from(&rpc_config)?; + // Stop early if bitcoin core connection is wrong + if let Err(e) = rpc_client.get_blockchain_info() { + log::error!("Cannot connect to bitcoin node {:?}", e); + return Err(e.into()); + } else { + log::info!("Bitcoin core connection successful"); + } + match directory.connection_type { ConnectionType::CLEARNET => {} #[cfg(feature = "tor")] @@ -361,34 +399,33 @@ pub fn start_directory_server( let tor_log_dir = "/tmp/tor-rust-directory/log"; if Path::new(tor_log_dir).exists() { match fs::remove_file(tor_log_dir) { - Ok(_) => log::info!("Previous directory log file deleted successfully"), - Err(_) => log::error!("Error deleting directory log file"), + Ok(_) => log::info!("Previous tor log file deleted successfully"), + Err(_) => log::error!("Error deleting tor log file"), } } let socks_port = directory.socks_port; - let tor_port = directory.port; + let network_port = directory.network_port; tor_handle = Some(crate::tor::spawn_tor( socks_port, - tor_port, + network_port, "/tmp/tor-rust-directory".to_string(), - )); + )?); - sleep(Duration::from_secs(10)); + log::info!("waiting for tor setup completion."); - if let Err(e) = monitor_log_for_completion(&PathBuf::from(tor_log_dir), "100%") { - log::error!("Error monitoring Directory log file: {}", e); + if let Err(e) = monitor_log_for_completion( + &PathBuf::from(tor_log_dir), + "Bootstrapped 100% (done): Done", + ) { + log::error!("Error monitoring tor log file: {}", e); } - log::info!("Directory tor is instantiated"); + log::info!("tor is ready!!"); let onion_addr = get_tor_addrs(&PathBuf::from("/tmp/tor-rust-directory"))?; - log::info!( - "Directory Server is listening at {}:{}", - onion_addr, - tor_port - ); + log::info!("DNS is listening at {}:{}", onion_addr, network_port); } } } @@ -407,16 +444,16 @@ pub fn start_directory_server( start_address_writer_thread(directory_clone) }); - let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, directory.port))?; + let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, directory.network_port))?; - // why we have not set it to non-blocking mode? while !directory.shutdown.load(Relaxed) { match listener.accept() { - Ok((mut stream, addrs)) => { - log::debug!("Incoming connection from : {}", addrs); + Ok((mut stream, _)) => { stream.set_read_timeout(Some(Duration::from_secs(60)))?; stream.set_write_timeout(Some(Duration::from_secs(60)))?; - handle_client(&mut stream, &directory.clone(), &rpc_client)?; + if let Err(e) = handle_client(&mut stream, &directory, &rpc_client) { + log::error!("Error accepting incoming connection: {:?}", e); + } } // If no connection received, check for shutdown or save addresses to disk @@ -440,8 +477,8 @@ pub fn start_directory_server( #[cfg(feature = "tor")] { - if let Some(handle) = tor_handle { - crate::tor::kill_tor_handles(handle); + if let Some(mut handle) = tor_handle { + crate::tor::kill_tor_handles(&mut handle); log::info!("Directory server and Tor instance terminated successfully"); } } @@ -474,6 +511,10 @@ fn handle_client( current_height, ) { Ok(_) => { + log::info!( + "Fidelity verification success from {}. Adding/updating to address data.", + metadata.url + ); directory.updated_address_map((metadata.url, metadata.proof.bond.outpoint))?; } Err(e) => { @@ -486,7 +527,7 @@ fn handle_client( } } DnsRequest::Get => { - log::info!("Received GET | From {}", stream.peer_addr()?); + log::info!("Received GET"); let addresses = directory.addresses.read()?; let response = addresses .iter() @@ -536,7 +577,7 @@ mod tests { let dns = DirectoryServer::new(Some(temp_dir.path().to_path_buf()), None).unwrap(); let default_dns = DirectoryServer::default(); - assert_eq!(dns.port, default_dns.port); + assert_eq!(dns.network_port, default_dns.network_port); assert_eq!(dns.socks_port, default_dns.socks_port); temp_dir.close().unwrap(); @@ -552,7 +593,7 @@ mod tests { create_temp_config(contents, &temp_dir); let dns = DirectoryServer::new(Some(temp_dir.path().to_path_buf()), None).unwrap(); - assert_eq!(dns.port, 8080); + assert_eq!(dns.network_port, 8080); assert_eq!(dns.socks_port, DirectoryServer::default().socks_port); temp_dir.close().unwrap(); @@ -569,7 +610,7 @@ mod tests { let dns = DirectoryServer::new(Some(temp_dir.path().to_path_buf()), None).unwrap(); let default_dns = DirectoryServer::default(); - assert_eq!(dns.port, default_dns.port); + assert_eq!(dns.network_port, default_dns.network_port); assert_eq!(dns.socks_port, default_dns.socks_port); temp_dir.close().unwrap(); @@ -581,7 +622,7 @@ mod tests { let dns = DirectoryServer::new(Some(temp_dir.path().to_path_buf()), None).unwrap(); let default_dns = DirectoryServer::default(); - assert_eq!(dns.port, default_dns.port); + assert_eq!(dns.network_port, default_dns.network_port); assert_eq!(dns.socks_port, default_dns.socks_port); temp_dir.close().unwrap(); diff --git a/src/market/rpc/server.rs b/src/market/rpc/server.rs index 1bc3e11d..119770a9 100644 --- a/src/market/rpc/server.rs +++ b/src/market/rpc/server.rs @@ -31,9 +31,7 @@ fn handle_request( .map(|(op, address)| (*op, address.clone())) .collect::>(), ); - if let Err(e) = send_message(socket, &resp) { - log::info!("Error sending RPC response {:?}", e); - }; + send_message(socket, &resp)?; } } @@ -56,14 +54,15 @@ pub fn start_rpc_server_thread( log::info!("Got RPC request from: {}", addr); stream.set_read_timeout(Some(Duration::from_secs(20)))?; stream.set_write_timeout(Some(Duration::from_secs(20)))?; - handle_request(&mut stream, directory.addresses.clone())?; + if let Err(e) = handle_request(&mut stream, directory.addresses.clone()) { + log::error!("Error handling RPC request: {:?}", e); + } } Err(e) => { if e.kind() == ErrorKind::WouldBlock { // do nothing } else { log::error!("Error accepting RPC connection: {:?}", e); - break; } } } diff --git a/src/protocol/messages.rs b/src/protocol/messages.rs index 78af7850..90e4ea8d 100644 --- a/src/protocol/messages.rs +++ b/src/protocol/messages.rs @@ -318,6 +318,7 @@ impl Display for MakerToTakerMessage { /// Metadata shared by the maker with the Directory Server for verifying authenticity. #[derive(Serialize, Deserialize, Debug)] +#[allow(private_interfaces)] pub struct DnsMetadata { /// The maker's URL. pub url: String, @@ -340,6 +341,7 @@ pub enum DnsRequest { Get, /// Dummy data used for integration tests. #[cfg(feature = "integration-test")] + /// Send a dummy, request, only used in integration tests Dummy { /// A dummy URL for testing. url: String, diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 17a84438..79e49b90 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -2,7 +2,7 @@ pub(crate) mod contract; pub mod error; -pub(crate) mod messages; +pub mod messages; pub(crate) use contract::Hash160; diff --git a/src/taker/api.rs b/src/taker/api.rs index 942b3431..9f07147d 100644 --- a/src/taker/api.rs +++ b/src/taker/api.rs @@ -10,8 +10,10 @@ use std::{ collections::{HashMap, HashSet}, + io::BufWriter, net::TcpStream, path::PathBuf, + process::Child, thread::sleep, time::{Duration, Instant}, }; @@ -19,9 +21,6 @@ use std::{ #[cfg(feature = "tor")] use std::io::Read; -#[cfg(feature = "tor")] -use std::{fs, path::Path}; - use bitcoind::bitcoincore_rpc::RpcApi; #[cfg(feature = "tor")] @@ -60,21 +59,34 @@ use crate::{ }, }; +#[cfg(feature = "tor")] +use crate::tor::kill_tor_handles; + // Default values for Taker configurations pub(crate) const REFUND_LOCKTIME: u16 = 20; pub(crate) const REFUND_LOCKTIME_STEP: u16 = 20; pub(crate) const FIRST_CONNECT_ATTEMPTS: u32 = 5; pub(crate) const FIRST_CONNECT_SLEEP_DELAY_SEC: u64 = 1; -pub(crate) const FIRST_CONNECT_ATTEMPT_TIMEOUT_SEC: u64 = 60; -pub(crate) const RECONNECT_ATTEMPTS: u32 = 3200; -pub(crate) const RECONNECT_SHORT_SLEEP_DELAY: u64 = 10; -pub(crate) const RECONNECT_LONG_SLEEP_DELAY: u64 = 60; -pub(crate) const SHORT_LONG_SLEEP_DELAY_TRANSITION: u32 = 60; -pub(crate) const RECONNECT_ATTEMPT_TIMEOUT_SEC: u64 = 300; +pub(crate) const FIRST_CONNECT_ATTEMPT_TIMEOUT_SEC: u64 = 30; + +// Tries reconnection by variable delay. +// First 10 attempts at 1 sec interval. +// Next 5 attempts by 30 sec interval. +// This is useful to cater for random network failure. +pub(crate) const RECONNECT_ATTEMPTS: u32 = 10; +pub(crate) const RECONNECT_SHORT_SLEEP_DELAY: u64 = 1; +pub(crate) const RECONNECT_LONG_SLEEP_DELAY: u64 = 5; +pub(crate) const SHORT_LONG_SLEEP_DELAY_TRANSITION: u32 = 30; +pub(crate) const TCP_TIMEOUT_SECONDS: u64 = 300; // TODO: Maker should decide this miner fee // This fee is used for both funding and contract txs. +#[cfg(feature = "integration-test")] pub(crate) const MINER_FEE: u64 = 1000; +/// This fee is used for both funding and contract txs. +#[cfg(not(feature = "integration-test"))] +pub(crate) const MINER_FEE: u64 = 300; // around 2 sats/vb for funding tx + /// Swap specific parameters. These are user's policy and can differ among swaps. /// SwapParams govern the criteria to find suitable set of makers from the offerbook. /// @@ -162,6 +174,29 @@ pub struct Taker { offerbook: OfferBook, ongoing_swap_state: OngoingSwapState, behavior: TakerBehavior, + tor_handle: Option, + data_dir: PathBuf, +} + +impl Drop for Taker { + fn drop(&mut self) { + log::info!("Shutting down taker."); + self.offerbook + .write_to_disk(&self.data_dir.join("offerbook.dat")) + .unwrap(); + log::info!("offerbook data saved to disk."); + self.wallet.save_to_disk().unwrap(); + log::info!("Wallet data saved to disk."); + + if !cfg!(feature = "tor") { + assert!(self.tor_handle.is_some(), "Tor handle should not exist") + } + + #[cfg(feature = "tor")] + if let Some(handle) = &mut self.tor_handle { + kill_tor_handles(handle); + } + } } impl Taker { @@ -169,16 +204,16 @@ impl Taker { /// Initializes a Maker structure. /// - /// This function sets up a Maker instance with configurable parameters. + /// This function sets up a Maker instance with configurable parameters. /// It handles the initialization of data directories, wallet files, and RPC configurations. /// /// ### Parameters: - /// - `data_dir`: - /// - `Some(value)`: Use the specified directory for storing data. - /// - `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`. + /// - `data_dir`: + /// - `Some(value)`: Use the specified directory for storing data. + /// - `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 `taker-wallet`. /// - If `rpc_config` = `None`: Use the default [`RPCConfig`] pub fn init( data_dir: Option, @@ -219,6 +254,31 @@ impl Taker { config.write_to_file(&data_dir.join("config.toml"))?; + // Load offerbook. If doesn't exists, creates fresh file. + let offerbook_path = data_dir.join("offerbook.dat"); + let offerbook = if offerbook_path.exists() { + // If read fails, recreate a fresh offerbook. + match OfferBook::read_from_disk(&offerbook_path) { + Ok(offerbook) => { + log::info!("Succesfully loaded offerbook at : {:?}", offerbook_path); + offerbook + } + Err(e) => { + log::error!("Offerbook data corrupted. Recreating. {:?}", e); + let empty_book = OfferBook::default(); + empty_book.write_to_disk(&offerbook_path)?; + empty_book + } + } + } else { + // Crewate a new offer book + let empty_book = OfferBook::default(); + let file = std::fs::File::create(&offerbook_path)?; + let writer = BufWriter::new(file); + serde_cbor::to_writer(writer, &empty_book)?; + empty_book + }; + log::info!("Initializing wallet sync"); wallet.sync()?; log::info!("Completed wallet sync"); @@ -226,9 +286,11 @@ impl Taker { Ok(Self { wallet, config, - offerbook: OfferBook::default(), + offerbook, ongoing_swap_state: OngoingSwapState::default(), behavior, + tor_handle: None, + data_dir, }) } @@ -244,48 +306,50 @@ impl Taker { /// Does the coinswap process pub fn do_coinswap(&mut self, swap_params: SwapParams) -> Result<(), TakerError> { - #[cfg(feature = "tor")] - let mut handle = None; + self.tor_handle = self.setup_tor()?; + self.send_coinswap(swap_params) + } + fn setup_tor(&self) -> Result, TakerError> { match self.config.connection_type { - ConnectionType::CLEARNET => {} + ConnectionType::CLEARNET => Ok(None), #[cfg(feature = "tor")] 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"), - Err(_) => log::error!("Error deleting taker log file "), + let tor_dir = self.data_dir.join("tor"); + let tor_log_file = tor_dir.join("log"); + + // Hard error if previous log file can't be removed, as monitor_log_for_completion doesn't work with existing file. + // Tell the user to manually delete the file and restart. + if tor_log_file.exists() { + if let Err(e) = std::fs::remove_file(&tor_log_file) { + log::error!( + "Error removing previous tor log. Please delet the file and restart. | {:?}", + tor_log_file + ); + return Err(e.into()); + } else { + log::info!("Previous tor log file deleted succesfully"); } } - handle = Some(crate::tor::spawn_tor( - taker_socks_port, - self.config.port, - "/tmp/tor-rust-taker".to_string(), - )); - - // wait for tor process to create a new log file. - std::thread::sleep(Duration::from_secs(3)); + let handle = Some(crate::tor::spawn_tor( + self.config.socks_port, + self.config.network_port, + tor_dir.to_str().unwrap().to_owned(), + )?); - if let Err(e) = monitor_log_for_completion(&PathBuf::from(tor_log_dir), "100%") { - log::error!("Error monitoring taker log file: {}\n Try removing the tor directory and retry", e); - return Err(TakerError::IO(e)); + if let Err(e) = + monitor_log_for_completion(&tor_log_file, "Bootstrapped 100% (done): Done") + { + log::error!("Error monitoring taker log file. Try removing the tor log file {:?} and try again. | {}", tor_log_file, e); + return Err(e.into()); } - } - } - self.send_coinswap(swap_params)?; + log::info!("tor is ready!"); - #[cfg(feature = "tor")] - { - if self.config.connection_type == ConnectionType::TOR && cfg!(feature = "tor") { - crate::tor::kill_tor_handles(handle.expect("tor handle expected")); + Ok(handle) } } - Ok(()) } /// Perform a coinswap round with given [SwapParams]. The Taker will try to perform swap with makers @@ -302,10 +366,21 @@ impl Taker { self.sync_offerbook()?; // Error early if hop_count > available good makers. - if swap_params.maker_count > self.offerbook.all_makers.len() { + if swap_params.maker_count > self.offerbook.all_good_makers().len() { + log::error!( + "Not enough makers in the offerbook. Required {}, avaialable {}", + swap_params.maker_count, + self.offerbook.all_good_makers().len() + ); return Err(TakerError::NotEnoughMakersInOfferBook); } + // Error early if less than 2 makers. + if swap_params.maker_count < 2 { + log::error!("Cannot swap with less than 2 makers"); + return Err(ProtocolError::General("Swap maker count < 2").into()); + } + // Generate new random preimage and initiate the first hop. let mut preimage = [0u8; 32]; OsRng.fill_bytes(&mut preimage); @@ -313,6 +388,20 @@ impl Taker { self.ongoing_swap_state.active_preimage = preimage; self.ongoing_swap_state.swap_params = swap_params; + let available = self.wallet.spendable_balance()?; + + // TODO: Make more exact estimate of swap cost and ensure balanbce. + // For now ensure at least swap_amount + 10000 is available. + let required = swap_params.send_amount + Amount::from_sat(1000); + if available < required { + let err = WalletError::InsufficientFund { + available: available.to_btc(), + required: required.to_btc(), + }; + log::error!("Not enough balance to cover swap : {:#?}", err); + return Err(err.into()); + } + // Try first hop. Abort if error happens. if let Err(e) = self.init_first_hop() { log::error!("Could not initiate first hop: {:?}", e); @@ -452,14 +541,8 @@ impl Taker { // Loop until we find a live maker who responded to our signature request. let (maker, funding_txs) = loop { - // Fail early if not enough good makers in the list to satisfy swap requirements. - let untried_maker_count = self.offerbook.get_all_untried().len(); - - if untried_maker_count < (self.ongoing_swap_state.swap_params.maker_count) { - log::error!("Not enough makers to satisfy swap requirements."); - return Err(TakerError::NotEnoughMakersInOfferBook); - } let maker = self.choose_next_maker()?.clone(); + log::info!("Choosing next maker: {}", maker.address); let (multisig_pubkeys, multisig_nonces, hashlock_pubkeys, hashlock_nonces) = generate_maker_keys( &maker.offer.tweakable_point, @@ -540,11 +623,7 @@ impl Taker { let funding_txids = funding_txs .iter() .map(|tx| { - let txid = self - .wallet - .rpc - .send_raw_transaction(tx) - .map_err(WalletError::Rpc)?; + let txid = self.wallet.send_tx(tx)?; log::info!("Funding Txid: {}", txid); assert_eq!(txid, tx.compute_txid()); Ok(txid) @@ -559,7 +638,6 @@ impl Taker { match self.watch_for_txs(&funding_txids) { Ok(stuffs) => { self.ongoing_swap_state.funding_txs.push(stuffs); - self.offerbook.add_good_maker(&maker); } Err(e) => { log::error!("Error: {:?}", e); @@ -833,8 +911,10 @@ impl Taker { Err(e) => { log::warn!( "Failed to connect to maker {} to send signatures and init next hop, \ - reattempting... error={:?}", + reattempting {} of {} | error={:?}", &maker_oa.address, + ii, + reconnect_attempts, e ); if ii <= reconnect_attempts { @@ -885,7 +965,7 @@ impl Taker { .into_inner(), }; - let reconnect_timeout = Duration::from_secs(RECONNECT_ATTEMPT_TIMEOUT_SEC); + let reconnect_timeout = Duration::from_secs(TCP_TIMEOUT_SECONDS); socket.set_read_timeout(Some(reconnect_timeout))?; socket.set_write_timeout(Some(reconnect_timeout))?; @@ -1020,10 +1100,7 @@ impl Taker { &next_peer_hashlock_keys_or_nonces, maker_refund_locktime, ) { - Ok(r) => { - self.offerbook.add_good_maker(&next_maker); - r - } + Ok(r) => r, Err(e) => { self.offerbook.add_bad_maker(&next_maker); log::info!( @@ -1386,8 +1463,10 @@ impl Taker { Err(e) => { log::warn!( "Failed to connect to maker {} to request signatures for receiver, \ - reattempting... error={:?}", + reattempting {} of {} | error={:?}", &maker_addr_str, + ii, + first_connect_attempts, e ); if ii <= first_connect_attempts { @@ -1422,7 +1501,7 @@ impl Taker { incoming_swapcoins: &[S], receivers_contract_txes: &[Transaction], ) -> Result { - let reconnect_time_out = Duration::from_secs(RECONNECT_ATTEMPT_TIMEOUT_SEC); + let reconnect_time_out = Duration::from_secs(TCP_TIMEOUT_SECONDS); // Configurable reconnection attempts for testing let reconnect_attempts = if cfg!(feature = "integration-test") { @@ -1463,8 +1542,10 @@ impl Taker { Err(e) => { log::warn!( "Failed to connect to maker {} to request signatures for receiver, \ - reattempting... error={:?}", + reattempting, {} of {} | error={:?}", &maker_addr_str, + ii, + reconnect_attempts, e ); if ii <= reconnect_attempts { @@ -1544,7 +1625,7 @@ impl Taker { .collect::>() }; - let reconnect_time_out = Duration::from_secs(RECONNECT_ATTEMPT_TIMEOUT_SEC); + let reconnect_time_out = Duration::from_secs(TCP_TIMEOUT_SECONDS); let mut ii = 0; @@ -1590,8 +1671,10 @@ impl Taker { Err(e) => { log::warn!( "Failed to connect to maker {} to settle coinswap, \ - reattempting... error={:?}", + reattempting {} of {} error={:?}", &maker_address.address, + ii, + reconnect_attempts, e ); if ii <= reconnect_attempts { @@ -1698,11 +1781,11 @@ impl Taker { // Ensure that we don't select a maker we are already swaping with. Ok(self .offerbook - .get_all_untried() + .all_good_makers() .iter() .find(|oa| { send_amount >= Amount::from_sat(oa.offer.min_size) - && send_amount < Amount::from_sat(oa.offer.max_size) + && send_amount <= Amount::from_sat(oa.offer.max_size) && !self .ongoing_swap_state .peer_infos @@ -1807,10 +1890,7 @@ impl Taker { { log::info!("Incoming Contract already broadacsted"); } else { - self.wallet - .rpc - .send_raw_transaction(contract_tx) - .map_err(WalletError::from)?; + self.wallet.send_tx(contract_tx)?; log::info!( "Broadcasted Incoming Contract. Removing from wallet. Contract Txid {}", contract_tx.compute_txid() @@ -1836,10 +1916,7 @@ impl Taker { { log::info!("Outgoing Contract already broadcasted"); } else { - self.wallet - .rpc - .send_raw_transaction(&contract_tx) - .map_err(WalletError::Rpc)?; + self.wallet.send_tx(&contract_tx)?; log::info!( "Broadcasted Outgoing Contract, Contract txid : {}", contract_tx.compute_txid() @@ -1855,12 +1932,17 @@ impl Taker { // Check for contract confirmations and broadcast timelocked transaction let mut timelock_boardcasted = Vec::new(); + // Save the wallet file here before going into the expensive loop. + self.wallet.sync()?; + self.wallet.save_to_disk()?; + log::info!("Wallet file synced and saved."); + // Start the loop to keep checking for timelock maturity, and spend from the contract asap. loop { // Break early if nothing to broadcast. // This happens only when init_first_hop() fails at `NotEnoughMakersInOfferBook` if outgoing_infos.is_empty() { - return Ok(()); + break; } for ((reedemscript, contract), (timelock, timelocked_tx)) in outgoing_infos.iter() { // We have already broadcasted this tx, so skip @@ -1892,10 +1974,7 @@ impl Taker { "Broadcasting timelocked tx: {}", timelocked_tx.compute_txid() ); - self.wallet - .rpc - .send_raw_transaction(timelocked_tx) - .map_err(WalletError::from)?; + self.wallet.send_tx(timelocked_tx)?; timelock_boardcasted.push(timelocked_tx); let outgoing_removed = self @@ -1906,19 +1985,23 @@ impl Taker { "Removed Outgoing Swapcoin from Wallet, Contract Txid: {}", outgoing_removed.contract_tx.compute_txid() ); + log::info!("Initializing Wallet sync and save"); + self.wallet.sync()?; + self.wallet.save_to_disk()?; + log::info!("Completed wallet sync and save"); } } } - // Everything is broadcasted. Clear the connectionstate and break the loop - if timelock_boardcasted.len() == outgoing_infos.len() { - log::info!("All outgoing contracts reedemed. Cleared ongoing swap state"); - self.clear_ongoing_swaps(); // This could be a bug if Taker is in middle of multiple swaps. For now we assume Taker will only do one swap at a time. - log::info!("Initializing Wallet sync and save"); - self.wallet.sync()?; - log::info!("Completed wallet sync and save"); - return Ok(()); - } } + + // Everything is broadcasted. Clear the connectionstate and break the loop + if timelock_boardcasted.len() == outgoing_infos.len() { + log::info!("All outgoing contracts reedemed. Cleared ongoing swap state"); + // TODO: Reevaluate this. + self.clear_ongoing_swaps(); // This could be a bug if Taker is in middle of multiple swaps. For now we assume Taker will only do one swap at a time. + break; + } + // Block wait time is varied between prod. and test builds. let block_wait_time = if cfg!(feature = "integration-test") { Duration::from_secs(10) @@ -1927,55 +2010,67 @@ impl Taker { }; std::thread::sleep(block_wait_time); } + log::info!("Recovery completed."); + + Ok(()) } /// Synchronizes the offer book with addresses obtained from directory servers and local configurations. pub fn sync_offerbook(&mut self) -> Result<(), TakerError> { - 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); - } - #[cfg(feature = "tor")] - ConnectionType::TOR => { - let directory_hs_path_str = - "/tmp/tor-rust-directory/hs-dir/hostname".to_string(); - 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(); - directory_address = format!("{}:{}", directory_onion_addr, 8080); + let dns_addr = match self.config.connection_type { + ConnectionType::CLEARNET => { + if cfg!(feature = "integration-test") { + format!("127.0.0.1:{}", 8080) + } else { + self.config.directory_server_address.clone() } } - } + #[cfg(feature = "tor")] + ConnectionType::TOR => { + let directory_hs_path_str = "/tmp/tor-rust-directory/hs-dir/hostname".to_string(); + let mut directory_file = std::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(); + format!("{}:{}", directory_onion_addr, 8080) + } + }; - let mut socks_port: Option = None; - #[cfg(feature = "tor")] - { - if self.config.connection_type == ConnectionType::TOR { - socks_port = Some(self.config.socks_port); + let socks_port = if cfg!(feature = "tor") { + if self.config.connection_type == ConnectionType::CLEARNET { + None + } else { + Some(self.config.socks_port) } - } + } else { + None + }; + + log::info!("Fetching addresses from DNS: {}", dns_addr); + let addresses_from_dns = - fetch_addresses_from_dns(socks_port, directory_address, self.config.connection_type)?; + match fetch_addresses_from_dns(socks_port, dns_addr, self.config.connection_type) { + Ok(dns_addrs) => dns_addrs, + Err(e) => { + log::error!("Could not connect to DNS Server: {:?}", e); + return Err(e); + } + }; - // Filter for new addresses only. - let know_addrs = self - .offerbook - .all_makers - .iter() - .map(|oa| oa.address.clone()) - .collect::>(); - let new_addrs = addresses_from_dns - .into_iter() - .filter(|addr| !know_addrs.contains(addr)) - .collect::>(); - let new_offers = fetch_offer_from_makers(new_addrs, &self.config)?; + // For now, ask offers from everyone, + // Because we don not have any smart update mechanism, not asking again could cause problem. + // if a maker changes their offer without changing tor address, the taker will not ask them again for updated offer. + // TODO: Add smarter update mechanism, where DNS would keep a flag for every update of maker offers and taker + // will selectively redownload the offer from those makers only. + // Further TODO: The Offer book needs to be restructured to store a unqiue value per fidelity bond. Similar to DNS. + let offers = fetch_offer_from_makers(addresses_from_dns, &self.config)?; + + // TODO: Use better logic to update offerbook than to just rewrite everything. + self.offerbook = OfferBook::default(); - for offer in new_offers { + for offer in offers { log::info!( - "Found New Offer from {}. Verifying Fidelity Proof", + "Found offer from {}. Verifying Fidelity Proof", offer.address.to_string() ); log::debug!("{:?}", offer); @@ -1996,4 +2091,12 @@ impl Taker { } Ok(()) } + + /// fetches only the offer data from DNS and returns the updated Offerbook. + /// Used for taker cli app, in `fetch-offers` command. + pub fn fetch_offers(&mut self) -> Result<&OfferBook, TakerError> { + self.tor_handle = self.setup_tor()?; + self.sync_offerbook()?; + Ok(&self.offerbook) + } } diff --git a/src/taker/config.rs b/src/taker/config.rs index ff2bb668..b23021f6 100644 --- a/src/taker/config.rs +++ b/src/taker/config.rs @@ -9,23 +9,21 @@ use std::{io, io::Write, path::Path}; /// Taker configuration with refund, connection, and sleep settings. #[derive(Debug, Clone, PartialEq)] pub struct TakerConfig { - /// Network listening port - pub port: u16, - /// Socks port + /// Network connection port + pub network_port: u16, + /// Socks proxy port used to connect TOR pub socks_port: u16, /// Directory server address (can be clearnet or onion) pub directory_server_address: String, /// Connection type pub connection_type: ConnectionType, - /// RPC port - pub rpc_port: u16, } impl Default for TakerConfig { fn default() -> Self { Self { - port: 8000, - socks_port: 19050, + network_port: 8000, + socks_port: 19070, directory_server_address: "directoryhiddenserviceaddress.onion:8080".to_string(), connection_type: { #[cfg(feature = "tor")] @@ -37,7 +35,6 @@ impl Default for TakerConfig { ConnectionType::CLEARNET } }, - rpc_port: 8081, } } } @@ -76,7 +73,7 @@ impl TakerConfig { ); Ok(TakerConfig { - port: parse_field(config_map.get("port"), default_config.port), + network_port: parse_field(config_map.get("network_port"), default_config.network_port), socks_port: parse_field(config_map.get("socks_port"), default_config.socks_port), directory_server_address: parse_field( config_map.get("directory_server_address"), @@ -86,23 +83,17 @@ impl TakerConfig { config_map.get("connection_type"), default_config.connection_type, ), - rpc_port: parse_field(config_map.get("rpc_port"), default_config.rpc_port), }) } // Method to manually serialize the Taker Config into a TOML string pub(crate) fn write_to_file(&self, path: &Path) -> std::io::Result<()> { let toml_data = format!( - "port = {} + "network_port = {} socks_port = {} -rpc_port = {} directory_server_address = {} connection_type = {:?}", - self.port, - self.socks_port, - self.rpc_port, - self.directory_server_address, - self.connection_type + self.network_port, self.socks_port, self.directory_server_address, self.connection_type ); std::fs::create_dir_all(path.parent().expect("Path should NOT be root!"))?; let mut file = std::fs::File::create(path)?; @@ -138,8 +129,8 @@ mod tests { #[test] fn test_valid_config() { let contents = r#" - port = 8000 - socks_port = 19050 + network_port = 8000 + socks_port = 19070 directory_server_address = directoryhiddenserviceaddress.onion:8080 connection_type = "TOR" rpc_port = 8081 diff --git a/src/taker/mod.rs b/src/taker/mod.rs index 6f788084..c4d4a110 100644 --- a/src/taker/mod.rs +++ b/src/taker/mod.rs @@ -4,7 +4,7 @@ //! simple request-response servers. The Taker handles all the necessary communications between one or many makers to route the swap across various makers. Description of //! protocol workflow is described in the [protocol between takers and makers](https://github.com/citadel-tech/coinswap/blob/master/docs/dev-book.md) -mod api; +pub mod api; mod config; pub mod error; pub(crate) mod offers; diff --git a/src/taker/offers.rs b/src/taker/offers.rs index 015ddf50..71115761 100644 --- a/src/taker/offers.rs +++ b/src/taker/offers.rs @@ -8,8 +8,10 @@ use std::{ convert::TryFrom, fmt, - io::Write, + fs::read, + io::{BufWriter, Write}, net::TcpStream, + path::Path, sync::mpsc, thread::{self, Builder}, }; @@ -28,7 +30,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)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct OfferAndAddress { pub(crate) offer: Offer, /// All maker addresses @@ -44,7 +46,7 @@ struct OnionAddress { } /// Enum representing maker addresses. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct MakerAddress(OnionAddress); impl MakerAddress { @@ -80,19 +82,24 @@ impl TryFrom<&mut TcpStream> for MakerAddress { /// An ephemeral Offerbook tracking good and bad makers. Currently, Offerbook is initiated /// at start of every swap. So good and bad maker list will ot be persisted. // TODO: Persist the offerbook in disk. -#[derive(Debug, Default)] -pub(crate) struct OfferBook { +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct OfferBook { pub(super) all_makers: Vec, - pub(super) good_makers: Vec, pub(super) bad_makers: Vec, } impl OfferBook { - /// Gets all untried offers. - pub(crate) fn get_all_untried(&self) -> Vec<&OfferAndAddress> { + // TODO: design a better offerbook: + // - unique key. + // - clear good-bad separations. + // - ranking system. + // - various categories of livelynesss, to smartly distribute try counts. + + /// Gets all "not-bad" offers. + pub fn all_good_makers(&self) -> Vec<&OfferAndAddress> { self.all_makers .iter() - .filter(|offer| !self.good_makers.contains(offer) && !self.bad_makers.contains(offer)) + .filter(|offer| !self.bad_makers.contains(offer)) .collect() } @@ -106,16 +113,6 @@ impl OfferBook { } } - /// Adds a good maker to the offer book. - pub(crate) fn add_good_maker(&mut self, good_maker: &OfferAndAddress) -> bool { - if !self.good_makers.contains(good_maker) { - self.good_makers.push(good_maker.clone()); - true - } else { - false - } - } - /// Adds a bad maker to the offer book. pub(crate) fn add_bad_maker(&mut self, bad_maker: &OfferAndAddress) -> bool { if !self.bad_makers.contains(bad_maker) { @@ -130,6 +127,39 @@ impl OfferBook { pub(crate) fn get_bad_makers(&self) -> Vec<&OfferAndAddress> { self.bad_makers.iter().collect() } + + /// Load existing file, updates it, writes it back (errors if path doesn't exist). + pub fn write_to_disk(&self, path: &Path) -> Result<(), TakerError> { + let wallet_file = std::fs::OpenOptions::new().write(true).open(path)?; + let writer = BufWriter::new(wallet_file); + Ok(serde_cbor::to_writer(writer, &self)?) + } + + /// Reads from a path (errors if path doesn't exist). + pub fn read_from_disk(path: &Path) -> Result { + //let wallet_file = File::open(path)?; + let mut reader = read(path)?; + let book = match serde_cbor::from_slice::(&reader) { + Ok(book) => book, + Err(e) => { + let err_string = format!("{:?}", e); + if err_string.contains("code: TrailingData") { + log::info!("Offerbook has trailing data, trying to restore"); + loop { + // pop the last byte and try again. + reader.pop(); + match serde_cbor::from_slice::(&reader) { + Ok(book) => break book, + Err(_) => continue, + } + } + } else { + return Err(e.into()); + } + } + }; + Ok(book) + } } /// Synchronizes the offer book with specific maker addresses. @@ -161,10 +191,6 @@ pub(crate) fn fetch_offer_from_makers( } for thread in thread_pool { - log::debug!( - "Joining thread : {}", - thread.thread().name().expect("thread names expected") - ); let join_result = thread.join(); if let Err(e) = join_result { @@ -175,20 +201,25 @@ pub(crate) fn fetch_offer_from_makers( } /// Retrieves advertised maker addresses from directory servers based on the specified network. -pub(crate) fn fetch_addresses_from_dns( - _socks_port: Option, - directory_server_address: String, +pub fn fetch_addresses_from_dns( + socks_port: Option, + dns_addr: String, connection_type: ConnectionType, ) -> Result, TakerError> { - // TODO: Make the communication in serde_encoded bytes. + if !cfg!(feature = "tor") { + assert!( + socks_port.is_none(), + "Cannot use socks port without tor feature" + ); + } loop { let mut stream = match connection_type { - ConnectionType::CLEARNET => TcpStream::connect(directory_server_address.as_str())?, + ConnectionType::CLEARNET => TcpStream::connect(dns_addr.as_str())?, #[cfg(feature = "tor")] ConnectionType::TOR => { - 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() + let socket_addrs = format!("127.0.0.1:{}", socks_port.expect("Tor port expected")); + Socks5Stream::connect(socket_addrs, dns_addr.as_str())?.into_inner() } }; @@ -197,9 +228,8 @@ pub(crate) fn fetch_addresses_from_dns( stream.set_nonblocking(false)?; stream.flush()?; - // Change datatype of number of makers to u32 from usize if let Err(e) = send_message(&mut stream, &DnsRequest::Get) { - log::warn!("Failed to send request. Retrying...{}", e); + log::error!("Failed to send request. Retrying...{}", e); thread::sleep(GLOBAL_PAUSE); continue; } diff --git a/src/taker/routines.rs b/src/taker/routines.rs index cc8da8cf..38af6311 100644 --- a/src/taker/routines.rs +++ b/src/taker/routines.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "tor")] use socks::Socks5Stream; -use std::{io::ErrorKind, net::TcpStream, thread::sleep, time::Duration}; +use std::{net::TcpStream, thread::sleep, time::Duration}; use crate::{ protocol::{ @@ -107,7 +107,6 @@ pub(crate) fn req_sigs_for_sender_once( maker_hashlock_nonces: &[SecretKey], locktime: u16, ) -> Result { - log::info!("Connecting to {}", socket.peer_addr()?); handshake_maker(socket)?; log::info!( "===> Sending ReqContractSigsForSender to {}", @@ -184,7 +183,6 @@ pub(crate) fn req_sigs_for_recvr_once( incoming_swapcoins: &[S], receivers_contract_txes: &[Transaction], ) -> Result { - log::info!("Connecting to {}", socket.peer_addr()?); handshake_maker(socket)?; let txs_info = incoming_swapcoins @@ -450,13 +448,14 @@ fn download_maker_offer_attempt_once( addr: &MakerAddress, config: &TakerConfig, ) -> Result { - let address = addr.to_string(); + let maker_addr = addr.to_string(); + log::info!("Attempting to download Offer from {}", maker_addr); let mut socket = match config.connection_type { - ConnectionType::CLEARNET => TcpStream::connect(address)?, + ConnectionType::CLEARNET => TcpStream::connect(&maker_addr)?, #[cfg(feature = "tor")] ConnectionType::TOR => Socks5Stream::connect( format!("127.0.0.1:{}", config.socks_port).as_str(), - address.as_ref(), + maker_addr.as_ref(), )? .into_inner(), }; @@ -481,6 +480,8 @@ fn download_maker_offer_attempt_once( } }; + log::info!("Got offer from : {} | {:?}", maker_addr, offer); + Ok(*offer) } @@ -494,31 +495,14 @@ pub(crate) fn download_maker_offer( ii += 1; 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!( - "Timeout for request offer from maker {}, reattempting...", - address - ); - continue; - } else { - log::error!( - "Timeout attempt exceeded for request offer from maker {}, ", - address - ); - return None; - } - } - } - Err(e) => { if ii <= FIRST_CONNECT_ATTEMPTS { log::warn!( - "Failed to request offer from maker {}, reattempting... error={:?}", + "Failed to request offer from maker {}, with error: {:?} reattempting {} of {}", address, - e + e, + ii, + FIRST_CONNECT_ATTEMPTS ); sleep(Duration::from_secs(FIRST_CONNECT_SLEEP_DELAY_SEC)); continue; diff --git a/src/tor.rs b/src/tor.rs index c1d49bf2..e11e2eb3 100644 --- a/src/tor.rs +++ b/src/tor.rs @@ -3,46 +3,75 @@ //! This module provides functionality for managing TOR instances using mitosis for spawning and //! handling processes. It includes utilities to initialize mitosis, spawn TOR processes, and //! gracefully terminate them. +use std::{ + io::{BufRead, BufReader}, + process::{Child, Command}, +}; + use libtor::{HiddenServiceVersion, LogDestination, LogLevel, Tor, TorAddress, TorFlag}; -use mitosis::JoinHandle; -/// Initializes mitosis for process spawning. -/// -/// This function sets up mitosis, preparing it for use in spawning processes. -pub fn setup_mitosis() { - mitosis::init(); +/// Used as the main function in tor binary +pub fn start_tor(socks_port: u16, port: u16, base_dir: String) -> Result<(), libtor::Error> { + let hs_string = format!("{}/hs-dir/", base_dir); + let data_dir = format!("{}/", base_dir); + let log_file = format!("{}/log", base_dir); + Tor::new() + .flag(TorFlag::DataDirectory(data_dir)) + .flag(TorFlag::LogTo( + LogLevel::Notice, + LogDestination::File(log_file), + )) + .flag(TorFlag::SocksPort(socks_port)) + .flag(TorFlag::HiddenServiceDir(hs_string)) + .flag(TorFlag::HiddenServiceVersion(HiddenServiceVersion::V3)) + .flag(TorFlag::HiddenServicePort( + TorAddress::Port(port), + None.into(), + )) + .start()?; + Ok(()) } -pub(crate) fn spawn_tor(socks_port: u16, port: u16, base_dir: String) -> JoinHandle<()> { - let handle = mitosis::spawn( - (socks_port, port, base_dir), - |(socks_port, port, base_dir)| { - let hs_string = format!("{}/hs-dir/", base_dir); - let data_dir = format!("{}/", base_dir); - let log_dir = format!("{}/log", base_dir); - let _handler = Tor::new() - .flag(TorFlag::DataDirectory(data_dir)) - .flag(TorFlag::LogTo( - LogLevel::Notice, - LogDestination::File(log_dir), - )) - .flag(TorFlag::SocksPort(socks_port)) - .flag(TorFlag::HiddenServiceDir(hs_string)) - .flag(TorFlag::HiddenServiceVersion(HiddenServiceVersion::V3)) - .flag(TorFlag::HiddenServicePort( - TorAddress::Port(port), - None.into(), - )) - .start(); - }, - ); - - handle +/// Used to programmatically spawn tor process in maker, taker, and dns. +pub fn spawn_tor(socks_port: u16, port: u16, base_dir: String) -> Result { + let mut tor_process = Command::new("./target/debug/tor") + .args([ + "-s", + &socks_port.to_string(), + "-p", + &port.to_string(), + "-d", + &base_dir, + ]) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn()?; + + let stdout = tor_process.stdout.take().unwrap(); + let stderr = tor_process.stderr.take().unwrap(); + + // Spawn threads to capture stdout and stderr. + std::thread::spawn(move || { + let reader = BufReader::new(stderr); + if let Some(line) = reader.lines().map_while(Result::ok).next() { + log::info!("{}", line); + } + }); + + std::thread::spawn(move || { + let reader = BufReader::new(stdout); + for line in reader.lines().map_while(Result::ok) { + log::info!("{}", line); + } + }); + + Ok(tor_process) } -pub(crate) fn kill_tor_handles(handle: JoinHandle<()>) { - match handle.kill() { +/// Kills all the tor processes. +pub fn kill_tor_handles(handle: &mut Child) { + match handle.kill().and_then(|_| handle.wait()) { Ok(_) => log::info!("Tor instance terminated successfully"), - Err(_) => log::error!("Error occurred while terminating tor instance"), + Err(e) => log::error!("Error occurred while terminating tor instance {:?}", e), }; } diff --git a/src/utill.rs b/src/utill.rs index 15bba671..f4a32fbb 100644 --- a/src/utill.rs +++ b/src/utill.rs @@ -146,24 +146,24 @@ pub(crate) fn get_dns_dir() -> PathBuf { /// log levels and configures log4rs with the specified filter level for fine-grained control /// of log verbosity. pub fn setup_taker_logger(filter: LevelFilter) { - env::set_var("RUST_LOG", "coinswap=info"); - let log_dir = get_taker_dir().join("debug.log"); - - let stdout = ConsoleAppender::builder().build(); - let file_appender = FileAppender::builder().build(log_dir).unwrap(); - - let config = Config::builder() - .appender(Appender::builder().build("stdout", Box::new(stdout))) - .appender(Appender::builder().build("file", Box::new(file_appender))) - .logger( - Logger::builder() - .appender("file") - .build("coinswap::taker", filter), - ) - .build(Root::builder().appender("stdout").build(filter)) - .unwrap(); + Once::new().call_once(|| { + //env::set_var("RUST_LOG", "coinswap=info"); + let log_dir = get_taker_dir().join("debug.log"); + + let file_appender = FileAppender::builder().build(log_dir).unwrap(); + + let config = Config::builder() + .appender(Appender::builder().build("file", Box::new(file_appender))) + .logger( + Logger::builder() + .appender("file") + .build("coinswap::taker", filter), + ) + .build(Root::builder().appender("file").build(filter)) + .unwrap(); - log4rs::init_config(config).unwrap(); + log4rs::init_config(config).unwrap(); + }) } /// Sets up the logger for the maker component. @@ -173,24 +173,26 @@ pub fn setup_taker_logger(filter: LevelFilter) { /// log levels and configures log4rs with the specified filter level for fine-grained control /// of log verbosity. pub fn setup_maker_logger(filter: LevelFilter) { - env::set_var("RUST_LOG", "coinswap=info"); - let log_dir = get_maker_dir().join("debug.log"); - - let stdout = ConsoleAppender::builder().build(); - let file_appender = FileAppender::builder().build(log_dir).unwrap(); - - let config = Config::builder() - .appender(Appender::builder().build("stdout", Box::new(stdout))) - .appender(Appender::builder().build("file", Box::new(file_appender))) - .logger( - Logger::builder() - .appender("file") - .build("coinswap::maker", filter), - ) - .build(Root::builder().appender("stdout").build(filter)) - .unwrap(); + Once::new().call_once(|| { + //env::set_var("RUST_LOG", "coinswap=info"); + let log_dir = get_maker_dir().join("debug.log"); + + let stdout = ConsoleAppender::builder().build(); + let file_appender = FileAppender::builder().build(log_dir).unwrap(); + + let config = Config::builder() + .appender(Appender::builder().build("stdout", Box::new(stdout))) + .appender(Appender::builder().build("file", Box::new(file_appender))) + .logger( + Logger::builder() + .appender("file") + .build("coinswap::maker", filter), + ) + .build(Root::builder().appender("stdout").build(filter)) + .unwrap(); - log4rs::init_config(config).unwrap(); + log4rs::init_config(config).unwrap(); + }) } /// Sets up the logger for the directory component. @@ -200,24 +202,26 @@ pub fn setup_maker_logger(filter: LevelFilter) { /// log levels and configures log4rs with the specified filter level for fine-grained control /// of log verbosity. pub fn setup_directory_logger(filter: LevelFilter) { - env::set_var("RUST_LOG", "coinswap=info"); - let log_dir = get_dns_dir().join("debug.log"); - - let stdout = ConsoleAppender::builder().build(); - let file_appender = FileAppender::builder().build(log_dir).unwrap(); - - let config = Config::builder() - .appender(Appender::builder().build("stdout", Box::new(stdout))) - .appender(Appender::builder().build("file", Box::new(file_appender))) - .logger( - Logger::builder() - .appender("file") - .build("coinswap::market", filter), - ) - .build(Root::builder().appender("stdout").build(filter)) - .unwrap(); + Once::new().call_once(|| { + //env::set_var("RUST_LOG", "coinswap=info"); + let log_dir = get_dns_dir().join("debug.log"); + + let stdout = ConsoleAppender::builder().build(); + let file_appender = FileAppender::builder().build(log_dir).unwrap(); + + let config = Config::builder() + .appender(Appender::builder().build("stdout", Box::new(stdout))) + .appender(Appender::builder().build("file", Box::new(file_appender))) + .logger( + Logger::builder() + .appender("file") + .build("coinswap::market", filter), + ) + .build(Root::builder().appender("stdout").build(filter)) + .unwrap(); - log4rs::init_config(config).unwrap(); + log4rs::init_config(config).unwrap(); + }) } /// Setup function that will only run once, even if called multiple times. @@ -364,14 +368,14 @@ pub(crate) fn get_hd_path_from_descriptor(descriptor: &str) -> Option<(&str, u32 &descriptor[open + 1..close] } else { // Debug log, because if it doesn't have path, its not an error. - log::debug!("Descriptor doesn't have path = {}", descriptor); + log::error!("Descriptor doesn't have path = {}", descriptor); return None; }; let path_chunks: Vec<&str> = path.split('/').collect(); if path_chunks.len() != 3 { // Debug log, because if it doesn't have path, its not an error. - log::debug!("Path is not a triplet. Path chunks = {:?}", path_chunks); + //log::warn!("Path is not a triplet. Path chunks = {:?}", path_chunks); return None; } @@ -429,29 +433,32 @@ pub(crate) fn parse_field(value: Option<&String>, default: /// Function to check if tor log contains a pattern pub(crate) fn monitor_log_for_completion(log_file: &Path, pattern: &str) -> io::Result<()> { + // TODO: Make this logic work for existing file with previous logs. let mut last_size = 0; loop { - let file = File::open(log_file)?; - let metadata = file.metadata()?; - let current_size = metadata.len(); - - if current_size != last_size { - let reader = io::BufReader::new(file); - let lines = reader.lines(); - - for line in lines { - if let Ok(line) = line { - if line.contains(pattern) { - log::info!("Tor instance bootstrapped"); - return Ok(()); + if log_file.exists() { + let file = File::open(log_file)?; + let metadata = file.metadata()?; + let current_size = metadata.len(); + + if current_size != last_size { + let reader = io::BufReader::new(file); + let lines = reader.lines(); + + for line in lines { + if let Ok(line) = line { + log::info!("{}", line); + if line.contains(pattern) { + return Ok(()); + } + } else { + return Err(io::Error::new(io::ErrorKind::Other, "Error reading line")); } - } else { - return Err(io::Error::new(io::ErrorKind::Other, "Error reading line")); } - } - last_size = current_size; + last_size = current_size; + } } thread::sleep(HEART_BEAT_INTERVAL); } @@ -580,19 +587,35 @@ pub(crate) fn verify_fidelity_checks( return Err(FidelityError::InvalidCertHash.into()); } - // Validate redeem script and corresponding address - let fidelity_redeem_script = fidelity_redeemscript(&proof.bond.lock_time, &proof.bond.pubkey); - let expected_address = Address::p2wsh( - fidelity_redeem_script.as_script(), + let networks = vec![ bitcoin::network::Network::Regtest, - ); + bitcoin::network::Network::Testnet, + bitcoin::network::Network::Testnet4, + bitcoin::network::Network::Bitcoin, + bitcoin::network::Network::Signet, + ]; + + let mut all_failed = true; + + for network in networks { + // Validate redeem script and corresponding address + let fidelity_redeem_script = + fidelity_redeemscript(&proof.bond.lock_time, &proof.bond.pubkey); + let expected_address = Address::p2wsh(fidelity_redeem_script.as_script(), network); - let derived_script_pubkey = expected_address.script_pubkey(); - let tx_out = tx - .tx_out(proof.bond.outpoint.vout as usize) - .map_err(|_| WalletError::General("Outputs index error".to_string()))?; + let derived_script_pubkey = expected_address.script_pubkey(); + let tx_out = tx + .tx_out(proof.bond.outpoint.vout as usize) + .map_err(|_| WalletError::General("Outputs index error".to_string()))?; + + if tx_out.script_pubkey == derived_script_pubkey { + all_failed = false; + break; // No need to continue checking once we find a successful match + } + } - if tx_out.script_pubkey != derived_script_pubkey { + // Only throw error if all checks fail + if all_failed { return Err(FidelityError::BondDoesNotExist.into()); } diff --git a/src/wallet/api.rs b/src/wallet/api.rs index 071b972c..db98954c 100644 --- a/src/wallet/api.rs +++ b/src/wallet/api.rs @@ -139,14 +139,17 @@ impl Wallet { /// /// The path should include the full path for a wallet file. /// If the wallet file doesn't exist it will create a new wallet file. - pub(crate) fn init(path: &Path, rpc_config: &RPCConfig) -> Result { + pub fn init(path: &Path, rpc_config: &RPCConfig) -> Result { + let rpc = Client::try_from(rpc_config)?; + let network = rpc.get_blockchain_info()?.chain; + // Generate Master key let master_key = { let mnemonic = Mnemonic::generate(12)?; let words = mnemonic.words().collect::>(); log::info!("Backup the Wallet Mnemonics. \n {:?}", words); let seed = mnemonic.to_entropy(); - Xpriv::new_master(rpc_config.network, &seed)? + Xpriv::new_master(network, &seed)? }; // Initialise wallet @@ -156,15 +159,9 @@ impl Wallet { .to_str() .expect("expected") .to_string(); - let rpc = Client::try_from(rpc_config)?; + let wallet_birthday = rpc.get_block_count()?; - let store = WalletStore::init( - file_name, - path, - rpc_config.network, - master_key, - Some(wallet_birthday), - )?; + let store = WalletStore::init(file_name, path, network, master_key, Some(wallet_birthday))?; Ok(Self { rpc, @@ -184,7 +181,18 @@ impl Wallet { ))); } let rpc = Client::try_from(rpc_config)?; - log::info!( + let network = rpc.get_blockchain_info()?.chain; + + // Check if the backend node is running on correct network. Or else hard error. + if store.network != network { + log::error!( + "Wallet file is created for {}, backend Bitcoin Core is running on {}", + store.network.to_string(), + network.to_string() + ); + return Err(WalletError::General("Wrong Bitcoin Network".to_string())); + } + log::debug!( "Loaded wallet file {} | External Index = {} | Incoming Swapcoins = {} | Outgoing Swapcoins = {}", store.file_name, store.external_index, @@ -272,11 +280,12 @@ impl Wallet { self.store.incoming_swapcoins.len() + self.store.outgoing_swapcoins.len() } - /// Calculates the total balance of the wallet, including swap coins, live contracts and fidelity bonds. - pub fn balance(&self) -> Result { + /// Calculates the total spendable balance of the wallet. Includes all utxos except the fidelity bond. + pub fn spendable_balance(&self) -> Result { Ok(self .list_all_utxo_spend_info(None)? .iter() + .filter(|(_, spend_info)| !matches!(spend_info, UTXOSpendInfo::FidelityBondCoin { .. })) .fold(Amount::ZERO, |a, (utxo, _)| a + utxo.amount)) } @@ -503,17 +512,23 @@ impl Wallet { &self, utxo: &ListUnspentResultEntry, ) -> Result, WalletError> { - if let Some(outgoing_swapcoin) = self.store.outgoing_swapcoins.get(&utxo.script_pub_key) { - if utxo.confirmations >= outgoing_swapcoin.get_timelock()?.into() { - return Ok(Some(UTXOSpendInfo::TimelockContract { - swapcoin_multisig_redeemscript: outgoing_swapcoin.get_multisig_redeemscript(), - input_value: utxo.amount, - })); - } - } else if let Some(incoming_swapcoin) = - self.store.incoming_swapcoins.get(&utxo.script_pub_key) + if let Some((_, outgoing_swapcoin)) = + self.store.outgoing_swapcoins.iter().find(|(_, og)| { + redeemscript_to_scriptpubkey(&og.contract_redeemscript).unwrap() + == utxo.script_pub_key + }) { - if incoming_swapcoin.is_hash_preimage_known() && utxo.confirmations >= 1 { + return Ok(Some(UTXOSpendInfo::TimelockContract { + swapcoin_multisig_redeemscript: outgoing_swapcoin.get_multisig_redeemscript(), + input_value: utxo.amount, + })); + } else if let Some((_, incoming_swapcoin)) = + self.store.incoming_swapcoins.iter().find(|(_, ig)| { + redeemscript_to_scriptpubkey(&ig.contract_redeemscript).unwrap() + == utxo.script_pub_key + }) + { + if incoming_swapcoin.is_hash_preimage_known() { return Ok(Some(UTXOSpendInfo::HashlockContract { swapcoin_multisig_redeemscript: incoming_swapcoin.get_multisig_redeemscript(), input_value: utxo.amount, @@ -593,7 +608,7 @@ impl Wallet { /// Returns a list all utxos with their spend info tracked by the wallet. /// Optionally takes in an Utxo list to reduce RPC calls. If None is given, the /// full list of utxo is fetched from core rpc. - pub(crate) fn list_all_utxo_spend_info( + pub fn list_all_utxo_spend_info( &self, utxos: Option<&Vec>, ) -> Result, WalletError> { @@ -1146,4 +1161,9 @@ impl Wallet { ); Ok(descriptors_to_import) } + + /// Uses internal RPC client to braodcast a transaction + pub fn send_tx(&self, tx: &Transaction) -> Result { + Ok(self.rpc.send_raw_transaction(tx)?) + } } diff --git a/src/wallet/fidelity.rs b/src/wallet/fidelity.rs index 570551f1..fab01e93 100644 --- a/src/wallet/fidelity.rs +++ b/src/wallet/fidelity.rs @@ -7,6 +7,7 @@ use std::{ use crate::{ protocol::messages::FidelityProof, + taker::api::MINER_FEE, utill::{redeemscript_to_scriptpubkey, verify_fidelity_checks}, wallet::{UTXOSpendInfo, Wallet}, }; @@ -174,8 +175,24 @@ impl Wallet { .iter() .filter_map(|(i, (_, _, is_spent))| { if !is_spent { - let value = self.calculate_bond_value(*i).unwrap(); - Some((i, value)) + match self.calculate_bond_value(*i) { + Ok(v) => { + log::info!("Fidelity Bond found | Index: {}, Value : {}", i, v); + Some((i, v)) + } + Err(e) => { + log::error!("Fidelity valuation failed for index {}: {:?} ", i, e); + if matches!( + e, + WalletError::Fidelity(FidelityError::BondLocktimeExpired) + ) { + log::info!( + "Use `maker-cli redeem-fildeity ` to redeem the bond" + ); + } + None + } + } } else { None } @@ -322,7 +339,7 @@ impl Wallet { } } - let fee = Amount::from_sat(1000); // TODO: Update this with the feerate + let fee = Amount::from_sat(MINER_FEE); // TODO: Update this with the feerate let total_input_amount = selected_utxo.iter().fold(Amount::ZERO, |acc, (unspet, _)| { acc.checked_add(unspet.amount) @@ -379,7 +396,7 @@ impl Wallet { .map(|(_, spend_info)| spend_info.clone()); self.sign_transaction(&mut tx, &mut input_info)?; - let txid = self.rpc.send_raw_transaction(&tx)?; + let txid = self.send_tx(&tx)?; let sleep_increment = 10; let mut sleep_multiplier = 0; @@ -400,6 +417,7 @@ impl Wallet { "Fidelity Transaction {} seen in mempool, waiting for confirmation.", txid ); + log::warn!("ATTENTION ! DO NOT SHUTDOWN THE MAKER UNTIL CONFIRMATION"); let total_sleep = sleep_increment * sleep_multiplier.min(10 * 60); // Caps at 1 Block interval i.e 10 mins log::info!("Next sync in {:?} secs", total_sleep); @@ -430,8 +448,8 @@ impl Wallet { } /// Redeem a Fidelity Bond. - /// This functions creates a spending transaction, signs and broadcasts it. - /// Upon confirmation it marks the bond as `spent` in the wallet data. + /// This functions creates a spending transaction from the fidelity bond, signs and broadcasts it. + /// Returns the txid of the spending tx, and mark the bond as spent. pub fn redeem_fidelity(&mut self, index: u32) -> Result { let (bond, _, is_spent) = self .store @@ -452,7 +470,7 @@ impl Wallet { }; // TODO take feerate as user input - let fee = Amount::from_sat(1000); + let fee = Amount::from_sat(MINER_FEE); let change_addr = &self.get_next_internal_addresses(1)?[0]; @@ -475,33 +493,11 @@ impl Wallet { self.sign_transaction(&mut tx, vec![utxo_spend_info].into_iter())?; - let txid = self.rpc.send_raw_transaction(&tx)?; + let txid = self.send_tx(&tx)?; - let sleep_increment = 10; - let mut sleep_multiplier = 0; - - loop { - sleep_multiplier += 1; - let get_tx_result = self.rpc.get_transaction(&txid, None)?; - if let Some(ht) = get_tx_result.info.blockheight { - log::info!( - "Redeem fidelity transaction {} confirmed at blockheight: {}", - txid, - ht - ); - break; - } else { - log::info!( - "Redeem fildelity transaction {} seen in mempool, waiting for confirmation.", - txid - ); + log::info!("Fidelity redeem transaction broadcasted. txid: {}", txid); - let total_sleep = sleep_increment * sleep_multiplier.min(10 * 60); // Caps at 1 Block interval i.e 10 mins - log::info!("Next sync in {:?} secs", total_sleep); - thread::sleep(Duration::from_secs(total_sleep)); - continue; - } - } + // No need to wait for confirmation as that will delay the rpc call. Just send back the txid. // mark is_spent { diff --git a/src/wallet/funding.rs b/src/wallet/funding.rs index 6276a6d1..cdd0227b 100644 --- a/src/wallet/funding.rs +++ b/src/wallet/funding.rs @@ -15,6 +15,8 @@ use bitcoind::bitcoincore_rpc::{json::CreateRawTransactionInput, RpcApi}; use bitcoin::secp256k1::rand::{rngs::OsRng, RngCore}; +use crate::taker::api::MINER_FEE; + use super::Wallet; use super::error::WalletError; @@ -385,7 +387,7 @@ impl Wallet { self.lock_unspendable_utxos()?; - let fee = Amount::from_sat(1000); + let fee = Amount::from_sat(MINER_FEE); let remaining = coinswap_amount; diff --git a/src/wallet/rpc.rs b/src/wallet/rpc.rs index 0cffd8d5..c264346f 100644 --- a/src/wallet/rpc.rs +++ b/src/wallet/rpc.rs @@ -1,12 +1,11 @@ //! Manages connection with a Bitcoin Core RPC. //! -use crate::utill::HEART_BEAT_INTERVAL; -use bitcoin::Network; +use std::{convert::TryFrom, thread}; + use bitcoind::bitcoincore_rpc::{Auth, Client, RpcApi}; use serde_json::{json, Value}; -use std::{convert::TryFrom, thread}; -use crate::wallet::api::KeychainKind; +use crate::{utill::HEART_BEAT_INTERVAL, wallet::api::KeychainKind}; use serde::Deserialize; @@ -19,8 +18,6 @@ pub struct RPCConfig { pub url: String, /// The bitcoin node authentication mechanism pub auth: Auth, - /// The network we are using (it will be checked the bitcoin node network matches this) - pub network: Network, /// The wallet name in the bitcoin node, derive this from the descriptor. pub wallet_name: String, } @@ -32,7 +29,6 @@ impl Default for RPCConfig { Self { url: RPC_HOSTPORT.to_string(), auth: Auth::UserPass("regtestrpcuser".to_string(), "regtestrpcpass".to_string()), - network: Network::Regtest, wallet_name: "random-wallet-name".to_string(), } } @@ -50,11 +46,6 @@ impl TryFrom<&RPCConfig> for Client { .as_str(), config.auth.clone(), )?; - if config.network != rpc.get_blockchain_info()?.chain { - return Err(WalletError::General( - "RPC Network not mathcing with RPCConfig".to_string(), - )); - } Ok(rpc) } } @@ -127,7 +118,7 @@ impl Wallet { .max(self.store.wallet_birthday.unwrap_or(0)); let node_synced = self.rpc.get_block_count()?; log::debug!( - "rescan_blockchain from:{} to:{}", + "Re-scanning Blockchain from:{} to:{}", last_synced_height, node_synced ); @@ -150,6 +141,7 @@ impl Wallet { let max_external_index = self.find_hd_next_index(KeychainKind::External)?; self.update_external_index(max_external_index)?; + self.refresh_offer_maxsize_cache()?; Ok(()) } diff --git a/src/wallet/storage.rs b/src/wallet/storage.rs index 6d3c7b3e..606ecfa6 100644 --- a/src/wallet/storage.rs +++ b/src/wallet/storage.rs @@ -6,8 +6,8 @@ use bitcoin::{bip32::Xpriv, Network, OutPoint, ScriptBuf}; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, - fs::{self, File}, - io::{BufReader, BufWriter}, + fs::{self, read, File}, + io::BufWriter, path::Path, }; @@ -84,9 +84,27 @@ impl WalletStore { /// Reads from a path (errors if path doesn't exist). pub(crate) fn read_from_disk(path: &Path) -> Result { - let wallet_file = File::open(path)?; - let reader = BufReader::new(wallet_file); - let store: Self = serde_cbor::from_reader(reader)?; + //let wallet_file = File::open(path)?; + let mut reader = read(path)?; + let store = match serde_cbor::from_slice::(&reader) { + Ok(store) => store, + Err(e) => { + let err_string = format!("{:?}", e); + if err_string.contains("code: TrailingData") { + log::info!("Wallet file has trailing data, trying to restore"); + loop { + // pop the last byte and try again. + reader.pop(); + match serde_cbor::from_slice::(&reader) { + Ok(store) => break store, + Err(_) => continue, + } + } + } else { + return Err(e.into()); + } + } + }; Ok(store) } } diff --git a/tests/maker_cli.rs b/tests/maker_cli.rs index 9130994f..eb419f48 100644 --- a/tests/maker_cli.rs +++ b/tests/maker_cli.rs @@ -1,7 +1,7 @@ //! Integration test for Maker CLI functionality. #![cfg(feature = "integration-test")] use bitcoin::{Address, Amount}; -use bitcoind::{bitcoincore_rpc::RpcApi, BitcoinD}; +use bitcoind::BitcoinD; use coinswap::utill::setup_logger; use std::{ fs, @@ -70,13 +70,15 @@ impl MakerCli { thread::spawn(move || { let reader = BufReader::new(stderr); if let Some(line) = reader.lines().map_while(Result::ok).next() { - let _ = stderr_sender.send(line); + println!("{}", line); + stderr_sender.send(line).unwrap(); } }); thread::spawn(move || { let reader = BufReader::new(stdout); for line in reader.lines().map_while(Result::ok) { + println!("{}", line); if stdout_sender.send(line).is_err() { break; } @@ -110,7 +112,7 @@ impl MakerCli { await_message(&stdout_recv, "Fidelity Transaction"); generate_blocks(&self.bitcoind, 1); await_message(&stdout_recv, "Successfully created fidelity bond"); - await_message(&stdout_recv, "Maker setup is ready"); + await_message(&stdout_recv, "Server Setup completed!!"); (stdout_recv, makerd_process) } @@ -146,31 +148,31 @@ fn test_maker_cli() { let (rx, mut makerd_proc) = maker_cli.start_makerd(); // Ping check - let ping_resp = maker_cli.execute_maker_cli(&["ping"]); + let ping_resp = maker_cli.execute_maker_cli(&["send-ping"]); await_message(&rx, "RPC request received: Ping"); - assert_eq!(ping_resp, "Pong"); + assert_eq!(ping_resp, "success"); // Data Dir check - let data_dir = maker_cli.execute_maker_cli(&["get-data-dir"]); + let data_dir = maker_cli.execute_maker_cli(&["show-data-dir"]); await_message(&rx, "RPC request received: GetDataDir"); assert!(data_dir.contains("/coinswap/maker")); - // Tor address check - let tor_addr = maker_cli.execute_maker_cli(&["get-tor-address"]); - await_message(&rx, "RPC request received: GetTorAddress"); - assert!(tor_addr.contains("onion:6102")); + // // Tor address check + // let tor_addr = maker_cli.execute_maker_cli(&["show-tor-address"]); + // await_message(&rx, "RPC request received: GetTorAddress"); + // assert!(tor_addr.contains("onion:6102")); // Initial Balance checks - let seed_balance = maker_cli.execute_maker_cli(&["seed-balance"]); - await_message(&rx, "RPC request received: SeedBalance"); + let seed_balance = maker_cli.execute_maker_cli(&["get-balance"]); + await_message(&rx, "RPC request received: Balance"); - let contract_balance = maker_cli.execute_maker_cli(&["contract-balance"]); + let contract_balance = maker_cli.execute_maker_cli(&["get-balance-contract"]); await_message(&rx, "RPC request received: ContractBalance"); - let fidelity_balance = maker_cli.execute_maker_cli(&["fidelity-balance"]); + let fidelity_balance = maker_cli.execute_maker_cli(&["get-balance-fidelity"]); await_message(&rx, "RPC request received: FidelityBalance"); - let swap_balance = maker_cli.execute_maker_cli(&["swap-balance"]); + let swap_balance = maker_cli.execute_maker_cli(&["get-balance-swap"]); await_message(&rx, "RPC request received: SwapBalance"); assert_eq!(seed_balance, "1000000 sats"); @@ -179,51 +181,57 @@ fn test_maker_cli() { assert_eq!(contract_balance, "0 sats"); // Initial UTXO checks - let seed_utxo = maker_cli.execute_maker_cli(&["seed-utxo"]); - await_message(&rx, "RPC request received: SeedUtxo"); + let all_utxos = maker_cli.execute_maker_cli(&["list-utxo"]); + await_message(&rx, "RPC request received: Utxo"); - let swap_utxo = maker_cli.execute_maker_cli(&["swap-utxo"]); + let swap_utxo = maker_cli.execute_maker_cli(&["list-utxo-swap"]); await_message(&rx, "RPC request received: SwapUtxo"); - let contract_utxo = maker_cli.execute_maker_cli(&["contract-utxo"]); + let contract_utxo = maker_cli.execute_maker_cli(&["list-utxo-contract"]); await_message(&rx, "RPC request received: ContractUtxo"); - let fidelity_utxo = maker_cli.execute_maker_cli(&["fidelity-utxo"]); + let fidelity_utxo = maker_cli.execute_maker_cli(&["list-utxo-fidelity"]); await_message(&rx, "RPC request received: FidelityUtxo"); // Validate UTXOs - assert_eq!(seed_utxo.matches("ListUnspentResultEntry").count(), 1); - assert!(seed_utxo.contains("amount: 1000000 SAT")); + assert_eq!(all_utxos.matches("ListUnspentResultEntry").count(), 2); + assert!(all_utxos.contains("amount: 1000000 SAT")); assert_eq!(fidelity_utxo.matches("ListUnspentResultEntry").count(), 1); assert!(fidelity_utxo.contains("amount: 5000000 SAT")); assert_eq!(swap_utxo.matches("ListUnspentResultEntry").count(), 0); assert_eq!(contract_utxo.matches("ListUnspentResultEntry").count(), 0); // Address check - derive and send to address -> - let address = maker_cli.execute_maker_cli(&["new-address"]); + let address = maker_cli.execute_maker_cli(&["get-new-address"]); await_message(&rx, "RPC request received: NewAddress"); assert!(Address::from_str(&address).is_ok()); - let tx_hex = maker_cli.execute_maker_cli(&["send-to-address", &address, "10000", "1000"]); - let tx: bitcoin::Transaction = bitcoin::consensus::encode::deserialize_hex(&tx_hex).unwrap(); - maker_cli.bitcoind.client.send_raw_transaction(&tx).unwrap(); + let _ = maker_cli.execute_maker_cli(&[ + "send-to-address", + "-t", + &address, + "-a", + "10000", + "-f", + "1000", + ]); generate_blocks(&maker_cli.bitcoind, 1); // Check balances + assert_eq!(maker_cli.execute_maker_cli(&["get-balance"]), "999000 sats"); assert_eq!( - maker_cli.execute_maker_cli(&["seed-balance"]), - "999000 sats" + maker_cli.execute_maker_cli(&["get-balance-contract"]), + "0 sats" ); - assert_eq!(maker_cli.execute_maker_cli(&["contract-balance"]), "0 sats"); assert_eq!( - maker_cli.execute_maker_cli(&["fidelity-balance"]), + maker_cli.execute_maker_cli(&["get-balance-fidelity"]), "5000000 sats" ); - assert_eq!(maker_cli.execute_maker_cli(&["swap-balance"]), "0 sats"); + assert_eq!(maker_cli.execute_maker_cli(&["get-balance-swap"]), "0 sats"); // Verify the seed UTXO count; other balance types remain unaffected when sending funds to an address. - let seed_utxo = maker_cli.execute_maker_cli(&["seed-utxo"]); - assert_eq!(seed_utxo.matches("ListUnspentResultEntry").count(), 2); + let seed_utxo = maker_cli.execute_maker_cli(&["list-utxo"]); + assert_eq!(seed_utxo.matches("ListUnspentResultEntry").count(), 3); // Shutdown check let stop = maker_cli.execute_maker_cli(&["stop"]); diff --git a/tests/standard_swap.rs b/tests/standard_swap.rs index e70194a7..51e5070c 100644 --- a/tests/standard_swap.rs +++ b/tests/standard_swap.rs @@ -28,11 +28,7 @@ fn test_standard_coinswap() { ((16102, Some(19052)), MakerBehavior::Normal), ]; - let connection_type = if cfg!(target_os = "macos") { - ConnectionType::CLEARNET - } else { - ConnectionType::TOR - }; + let connection_type = ConnectionType::CLEARNET; // Initiate test framework, Makers and a Taker with default behavior. let (test_framework, mut taker, makers, directory_server_instance, block_generation_handle) = diff --git a/tests/taker_cli.rs b/tests/taker_cli.rs index 9ead2e4b..a0f91543 100644 --- a/tests/taker_cli.rs +++ b/tests/taker_cli.rs @@ -1,5 +1,5 @@ #![cfg(feature = "integration-test")] -use bitcoin::{address::NetworkChecked, Address, Amount, Transaction}; +use bitcoin::{address::NetworkChecked, Address, Amount}; use bitcoind::{bitcoincore_rpc::RpcApi, tempfile::env::temp_dir, BitcoinD}; use std::{fs, path::PathBuf, process::Command, str::FromStr}; @@ -31,12 +31,7 @@ impl TakerCli { // Execute a cli-command fn execute(&self, cmd: &[&str]) -> String { - let mut args = vec![ - "--data-directory", - self.data_dir.to_str().unwrap(), - "--bitcoin-network", - "regtest", - ]; + let mut args = vec!["--data-directory", self.data_dir.to_str().unwrap()]; // RPC authentication (user:password) from the cookie file let cookie_file_path = &self.bitcoind.params.cookie_file; @@ -52,15 +47,6 @@ impl TakerCli { args.push("--WALLET"); args.push("test_wallet"); - // makers count - args.push("3"); - - // tx_count - args.push("3"); - - // fee_rate - args.push("1000"); - for arg in cmd { args.push(arg); } @@ -108,16 +94,14 @@ fn test_taker_cli() { generate_blocks(bitcoind, 10); // Assert that total_balance & seed_balance must be 3 BTC - let seed_balance = taker_cli.execute(&["seed-balance"]); - let total_balance = taker_cli.execute(&["total-balance"]); + let spendable_balance = taker_cli.execute(&["get-balance"]); - assert_eq!("300000000 SAT", seed_balance); - assert_eq!("300000000 SAT", total_balance); + assert_eq!("300000000 SAT", spendable_balance); // Assert that total no of seed-utxos are 3. - let seed_utxos = taker_cli.execute(&["seed-utxo"]); + let all_utxos = taker_cli.execute(&["list-utxo"]); - let no_of_seed_utxos = seed_utxos.matches("ListUnspentResultEntry {").count(); + let no_of_seed_utxos = all_utxos.matches("ListUnspentResultEntry {").count(); assert_eq!(3, no_of_seed_utxos); // Send 100,000 sats to a new address within the wallet, with a fee of 1,000 sats. @@ -125,40 +109,28 @@ fn test_taker_cli() { // get new external address let new_address = taker_cli.execute(&["get-new-address"]); - let response = taker_cli.execute(&["send-to-address", &new_address, "100000", "1000"]); - - // Extract Transaction hex string - let tx_hex_start = response.find("transaction_hex").unwrap() + "transaction_hex : \"".len(); - let tx_hex_end = response.find("\"\n").unwrap(); - - let tx_hex = &response[tx_hex_start..tx_hex_end]; - - // Extract FeeRate - let fee_rate_start = response.find("FeeRate").unwrap() + "FeeRate : ".len(); - let fee_rate_end = response.find(" sat").unwrap(); - - let _fee_rate = &response[fee_rate_start..fee_rate_end]; - // TODO: Determine if asserts are needed for the calculated fee rate. - - let tx: Transaction = bitcoin::consensus::encode::deserialize_hex(tx_hex).unwrap(); - - // broadcast signed transaction - bitcoind.client.send_raw_transaction(&tx).unwrap(); + let _ = taker_cli.execute(&[ + "send-to-address", + "-t", + &new_address, + "-a", + "100000", + "-f", + "1000", + ]); generate_blocks(bitcoind, 10); // Assert the total_amount & seed_amount must be initial (balance -fee) - let seed_balance = taker_cli.execute(&["seed-balance"]); - let total_balance = taker_cli.execute(&["total-balance"]); + let spendable_balance = taker_cli.execute(&["get-balance"]); // Since the amount is sent back to our wallet, the transaction fee is deducted from the balance. - assert_eq!("299999000 SAT", seed_balance); - assert_eq!("299999000 SAT", total_balance); + assert_eq!("299999000 SAT", spendable_balance); // Assert that no of seed utxos are 2 - let seed_utxos = taker_cli.execute(&["seed-utxo"]); + let all_utxos = taker_cli.execute(&["list-utxo"]); - let no_of_seed_utxos = seed_utxos.matches("ListUnspentResultEntry {").count(); + let no_of_seed_utxos = all_utxos.matches("ListUnspentResultEntry {").count(); assert_eq!(4, no_of_seed_utxos); bitcoind.client.stop().unwrap(); diff --git a/tests/test_framework/mod.rs b/tests/test_framework/mod.rs index 32242830..d51dcafd 100644 --- a/tests/test_framework/mod.rs +++ b/tests/test_framework/mod.rs @@ -118,12 +118,7 @@ pub(crate) fn start_dns(data_dir: &std::path::Path, bitcoind: &BitcoinD) -> proc let (stderr_sender, stderr_recv): (Sender, Receiver) = mpsc::channel(); - let mut args = vec![ - "--data-directory", - data_dir.to_str().unwrap(), - "--rpc_network", - "regtest", - ]; + let mut args = vec!["--data-directory", data_dir.to_str().unwrap()]; // RPC authentication (user:password) from the cookie file let cookie_file_path = Path::new(&bitcoind.params.cookie_file); @@ -404,9 +399,6 @@ impl TestFramework { Arc, JoinHandle<()>, ) { - if cfg!(feature = "tor") && connection_type == ConnectionType::TOR { - coinswap::tor::setup_mitosis(); - } setup_logger(log::LevelFilter::Info); // Setup directory let temp_dir = env::temp_dir().join("coinswap"); @@ -516,11 +508,9 @@ impl From<&TestFramework> for RPCConfig { fn from(value: &TestFramework) -> Self { let url = value.bitcoind.rpc_url().split_at(7).1.to_string(); let auth = Auth::CookieFile(value.bitcoind.params.cookie_file.clone()); - let network = value.bitcoind.client.get_blockchain_info().unwrap().chain; Self { url, auth, - network, ..Default::default() } }