diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a548a8f..5ea64ba1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,4 +127,3 @@ jobs: run: cargo test --package swap --all-features --test ${{ matrix.test_name }} "" env: MONERO_ADDITIONAL_SLEEP_PERIOD: 60000 - RUST_MIN_STACK: 16777216 # 16 MB. Default is 8MB. This is fine as in tests we start 2 programs: Alice and Bob. diff --git a/Cargo.lock b/Cargo.lock index 604d3e4f..f4bbf4df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cddc5f91628367664cc7c69714ff08deee8a3efc54623011c772544d7b2767" +checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" [[package]] name = "arrayref" @@ -136,17 +136,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "async-recursion" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "async-trait" version = "0.1.48" @@ -1408,9 +1397,9 @@ checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" [[package]] name = "hyper" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8e946c2b1349055e0b72ae281b238baf1a3ea7307c7e9f9d64673bdd9c26ac7" +checksum = "8bf09f61b52cfcf4c00de50df88ae423d6c02354e385a86341133b5338630ad1" dependencies = [ "bytes", "futures-channel", @@ -1423,7 +1412,7 @@ dependencies = [ "httpdate", "itoa", "pin-project 1.0.5", - "socket2 0.3.19", + "socket2 0.4.0", "tokio", "tower-service", "tracing", @@ -2015,7 +2004,6 @@ dependencies = [ "testcontainers 0.12.0", "tokio", "tracing", - "tracing-log", "tracing-subscriber", ] @@ -3433,7 +3421,6 @@ version = "0.3.0" dependencies = [ "anyhow", "async-compression", - "async-recursion", "async-trait", "atty", "backoff", @@ -3484,7 +3471,6 @@ dependencies = [ "toml", "tracing", "tracing-futures", - "tracing-log", "tracing-subscriber", "url", "uuid", @@ -3874,6 +3860,7 @@ dependencies = [ "thread_local", "tracing", "tracing-core", + "tracing-log", ] [[package]] diff --git a/monero-harness/Cargo.toml b/monero-harness/Cargo.toml index 26df8af3..fa57f1a9 100644 --- a/monero-harness/Cargo.toml +++ b/monero-harness/Cargo.toml @@ -14,5 +14,4 @@ spectral = "0.6" testcontainers = "0.12" tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "time", "macros"] } tracing = "0.1" -tracing-log = "0.1" -tracing-subscriber = { version = "0.2", default-features = false, features = ["fmt", "ansi", "env-filter"] } +tracing-subscriber = { version = "0.2", default-features = false, features = ["fmt", "ansi", "env-filter", "tracing-log"] } diff --git a/monero-harness/tests/monerod.rs b/monero-harness/tests/monerod.rs index 7e816264..40901b50 100644 --- a/monero-harness/tests/monerod.rs +++ b/monero-harness/tests/monerod.rs @@ -1,15 +1,15 @@ -use crate::testutils::init_tracing; use monero_harness::Monero; use spectral::prelude::*; use std::time::Duration; use testcontainers::clients::Cli; use tokio::time; - -mod testutils; +use tracing_subscriber::util::SubscriberInitExt; #[tokio::test] async fn init_miner_and_mine_to_miner_address() { - let _guard = init_tracing(); + let _guard = tracing_subscriber::fmt() + .with_env_filter("warn,test=debug,monero_harness=debug,monero_rpc=debug") + .set_default(); let tc = Cli::default(); let (monero, _monerod_container) = Monero::new(&tc, vec![]).await.unwrap(); diff --git a/monero-harness/tests/testutils/mod.rs b/monero-harness/tests/testutils/mod.rs deleted file mode 100644 index 0f4f34f3..00000000 --- a/monero-harness/tests/testutils/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -use tracing::subscriber::DefaultGuard; -use tracing_log::LogTracer; - -/// Utility function to initialize logging in the test environment. -/// Note that you have to keep the `_guard` in scope after calling in test: -/// -/// ```rust -/// let _guard = init_tracing(); -/// ``` -pub fn init_tracing() -> DefaultGuard { - // converts all log records into tracing events - // Note: Make sure to initialize without unwrapping, otherwise this causes - // trouble when running multiple tests. - let _ = LogTracer::init(); - - let global_filter = tracing::Level::WARN; - let test_filter = tracing::Level::DEBUG; - let monero_harness_filter = tracing::Level::DEBUG; - let monero_rpc_filter = tracing::Level::DEBUG; - - use tracing_subscriber::util::SubscriberInitExt as _; - tracing_subscriber::fmt() - .with_env_filter(format!( - "{},test={},monero_harness={},monero_rpc={}", - global_filter, test_filter, monero_harness_filter, monero_rpc_filter, - )) - .set_default() -} diff --git a/monero-harness/tests/wallet.rs b/monero-harness/tests/wallet.rs index 3314341a..15fb3698 100644 --- a/monero-harness/tests/wallet.rs +++ b/monero-harness/tests/wallet.rs @@ -1,15 +1,15 @@ -use crate::testutils::init_tracing; use monero_harness::{Monero, MoneroWalletRpc}; use spectral::prelude::*; use std::time::Duration; use testcontainers::clients::Cli; use tokio::time::sleep; - -mod testutils; +use tracing_subscriber::util::SubscriberInitExt; #[tokio::test] async fn fund_transfer_and_check_tx_key() { - let _guard = init_tracing(); + let _guard = tracing_subscriber::fmt() + .with_env_filter("warn,test=debug,monero_harness=debug,monero_rpc=debug") + .set_default(); let fund_alice: u64 = 1_000_000_000_000; let fund_bob = 0; diff --git a/monero-rpc/src/rpc/wallet.rs b/monero-rpc/src/rpc/wallet.rs index 71607299..abad399b 100644 --- a/monero-rpc/src/rpc/wallet.rs +++ b/monero-rpc/src/rpc/wallet.rs @@ -488,7 +488,7 @@ struct CheckTxKeyParams { #[derive(Clone, Copy, Debug, Deserialize)] pub struct CheckTxKey { - pub confirmations: u32, + pub confirmations: u64, pub received: u64, } diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 969cff2f..b66a4f3e 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -11,7 +11,6 @@ name = "swap" [dependencies] anyhow = "1" async-compression = { version = "0.3", features = ["bzip2", "tokio"] } -async-recursion = "0.3" async-trait = "0.1" atty = "0.2" backoff = { version = "0.3", features = ["tokio"] } @@ -53,8 +52,7 @@ tokio-util = { version = "0.6", features = ["io"] } toml = "0.5" tracing = { version = "0.1", features = ["attributes"] } tracing-futures = { version = "0.2", features = ["std-future", "futures-03"] } -tracing-log = "0.1" -tracing-subscriber = { version = "0.2", default-features = false, features = ["fmt", "ansi", "env-filter", "chrono"] } +tracing-subscriber = { version = "0.2", default-features = false, features = ["fmt", "ansi", "env-filter", "chrono", "tracing-log"] } url = { version = "2", features = ["serde"] } uuid = { version = "0.8", features = ["serde", "v4"] } void = "1" diff --git a/swap/src/env.rs b/swap/src/env.rs index d60ae858..e020faea 100644 --- a/swap/src/env.rs +++ b/swap/src/env.rs @@ -12,7 +12,7 @@ pub struct Config { pub bitcoin_punish_timelock: PunishTimelock, pub bitcoin_network: bitcoin::Network, pub monero_avg_block_time: Duration, - pub monero_finality_confirmations: u32, + pub monero_finality_confirmations: u64, pub monero_network: monero::Network, } diff --git a/swap/src/monero/wallet.rs b/swap/src/monero/wallet.rs index 4c643356..0f9490c0 100644 --- a/swap/src/monero/wallet.rs +++ b/swap/src/monero/wallet.rs @@ -11,7 +11,6 @@ use std::str::FromStr; use std::time::Duration; use tokio::sync::Mutex; use tokio::time::Interval; -use tracing::{debug, info}; use url::Url; #[derive(Debug)] @@ -34,9 +33,9 @@ impl Wallet { "Unable to create Monero wallet, please ensure that the monero-wallet-rpc is available", )?; - debug!("Created Monero wallet {}", name); + tracing::debug!("Created Monero wallet {}", name); } else { - debug!("Opened Monero wallet {}", name); + tracing::debug!("Opened Monero wallet {}", name); } Self::connect(client, name, env_config).await @@ -271,7 +270,7 @@ pub struct WatchRequest { pub public_spend_key: PublicKey, pub public_view_key: PublicViewKey, pub transfer_proof: TransferProof, - pub conf_target: u32, + pub conf_target: u64, pub expected: Amount, } @@ -280,14 +279,16 @@ async fn wait_for_confirmations( fetch_tx: impl Fn(String) -> Fut, mut check_interval: Interval, expected: Amount, - conf_target: u32, + conf_target: u64, ) -> Result<(), InsufficientFunds> where Fut: Future>, { - let mut seen_confirmations = 0u32; + let mut seen_confirmations = 0u64; while seen_confirmations < conf_target { + check_interval.tick().await; // tick() at the beginning of the loop so every `continue` tick()s as well + let tx = match fetch_tx(txid.clone()).await { Ok(proof) => proof, Err(error) => { @@ -310,10 +311,8 @@ where if tx.confirmations > seen_confirmations { seen_confirmations = tx.confirmations; - info!(%txid, "Monero lock tx has {} out of {} confirmations", tx.confirmations, conf_target); + tracing::info!(%txid, "Monero lock tx has {} out of {} confirmations", tx.confirmations, conf_target); } - - check_interval.tick().await; } Ok(()) @@ -323,7 +322,7 @@ where mod tests { use super::*; use monero_rpc::wallet::CheckTxKey; - use std::sync::atomic::{AtomicU32, Ordering}; + use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; use std::sync::Arc; #[tokio::test] @@ -364,9 +363,9 @@ mod tests { #[tokio::test] async fn visual_log_check() { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); - const MAX_REQUESTS: u32 = 20; + const MAX_REQUESTS: u64 = 20; - let requests = Arc::new(AtomicU32::new(0)); + let requests = Arc::new(AtomicU64::new(0)); let result = wait_for_confirmations( String::from("TXID"), diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index 3b035a41..e0ff69df 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -361,7 +361,7 @@ impl State3 { pub fn lock_xmr_watch_request( &self, transfer_proof: TransferProof, - conf_target: u32, + conf_target: u64, ) -> WatchRequest { let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: self.s_a }); diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 4a767c79..bf84bbea 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -1,7 +1,6 @@ //! Run an XMR/BTC swap in the role of Alice. //! Alice holds XMR and wishes receive BTC. use crate::bitcoin::{ExpiredTimelocks, TxRedeem}; -use crate::database::Database; use crate::env::Config; use crate::monero_ext::ScalarExt; use crate::protocol::alice; @@ -9,13 +8,10 @@ use crate::protocol::alice::event_loop::EventLoopHandle; use crate::protocol::alice::AliceState; use crate::{bitcoin, database, monero}; use anyhow::{bail, Context, Result}; -use async_recursion::async_recursion; use rand::{CryptoRng, RngCore}; -use std::sync::Arc; use tokio::select; use tokio::time::timeout; use tracing::{error, info}; -use uuid::Uuid; trait Rng: RngCore + CryptoRng + Send {} @@ -37,41 +33,40 @@ pub async fn run(swap: alice::Swap) -> Result { #[tracing::instrument(name = "swap", skip(swap,is_target_state), fields(id = %swap.swap_id))] pub async fn run_until( - swap: alice::Swap, + mut swap: alice::Swap, is_target_state: fn(&AliceState) -> bool, ) -> Result { - run_until_internal( - swap.state, - is_target_state, - swap.event_loop_handle, - swap.bitcoin_wallet, - swap.monero_wallet, - swap.env_config, - swap.swap_id, - swap.db, - ) - .await -} + let mut current_state = swap.state; -// State machine driver for swap execution -#[async_recursion] -#[allow(clippy::too_many_arguments)] -async fn run_until_internal( - state: AliceState, - is_target_state: fn(&AliceState) -> bool, - mut event_loop_handle: EventLoopHandle, - bitcoin_wallet: Arc, - monero_wallet: Arc, - env_config: Config, - swap_id: Uuid, - db: Arc, -) -> Result { - info!("Current state: {}", state); - if is_target_state(&state) { - return Ok(state); + while !is_target_state(¤t_state) { + current_state = next_state( + current_state, + &mut swap.event_loop_handle, + swap.bitcoin_wallet.as_ref(), + swap.monero_wallet.as_ref(), + &swap.env_config, + ) + .await?; + + let db_state = (¤t_state).into(); + swap.db + .insert_latest_state(swap.swap_id, database::Swap::Alice(db_state)) + .await?; } - let new_state = match state { + Ok(current_state) +} + +async fn next_state( + state: AliceState, + event_loop_handle: &mut EventLoopHandle, + bitcoin_wallet: &bitcoin::Wallet, + monero_wallet: &monero::Wallet, + env_config: &Config, +) -> Result { + info!("Current state: {}", state); + + Ok(match state { AliceState::Started { state3 } => { timeout( env_config.bob_time_to_act, @@ -121,10 +116,10 @@ async fn run_until_internal( AliceState::XmrLocked { state3, monero_wallet_restore_blockheight, - } => match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { + } => match state3.expired_timelocks(bitcoin_wallet).await? { ExpiredTimelocks::None => { select! { - _ = state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => { + _ = state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet) => { AliceState::CancelTimelockExpired { state3, monero_wallet_restore_blockheight, @@ -150,7 +145,7 @@ async fn run_until_internal( state3, encrypted_signature, monero_wallet_restore_blockheight, - } => match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { + } => match state3.expired_timelocks(bitcoin_wallet).await? { ExpiredTimelocks::None => { match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).complete( *encrypted_signature, @@ -168,7 +163,7 @@ async fn run_until_internal( Err(e) => { error!("Publishing the redeem transaction failed with {}, attempting to wait for cancellation now. If you restart the application before the timelock is expired publishing the redeem transaction will be retried.", e); state3 - .wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) + .wait_for_cancel_timelock_to_expire(bitcoin_wallet) .await?; AliceState::CancelTimelockExpired { @@ -180,7 +175,7 @@ async fn run_until_internal( Err(e) => { error!("Constructing the redeem transaction failed with {}, attempting to wait for cancellation now.", e); state3 - .wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) + .wait_for_cancel_timelock_to_expire(bitcoin_wallet) .await?; AliceState::CancelTimelockExpired { @@ -336,20 +331,5 @@ async fn run_until_internal( AliceState::BtcRedeemed => AliceState::BtcRedeemed, AliceState::BtcPunished => AliceState::BtcPunished, AliceState::SafelyAborted => AliceState::SafelyAborted, - }; - - let db_state = (&new_state).into(); - db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) - .await?; - run_until_internal( - new_state, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - env_config, - swap_id, - db, - ) - .await + }) } diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index b2703a51..1ccfc476 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -83,7 +83,7 @@ pub struct State0 { cancel_timelock: CancelTimelock, punish_timelock: PunishTimelock, refund_address: bitcoin::Address, - min_monero_confirmations: u32, + min_monero_confirmations: u64, } impl State0 { @@ -94,7 +94,7 @@ impl State0 { cancel_timelock: CancelTimelock, punish_timelock: PunishTimelock, refund_address: bitcoin::Address, - min_monero_confirmations: u32, + min_monero_confirmations: u64, ) -> Self { let b = bitcoin::SecretKey::new_random(rng); @@ -185,7 +185,7 @@ pub struct State1 { redeem_address: bitcoin::Address, punish_address: bitcoin::Address, tx_lock: bitcoin::TxLock, - min_monero_confirmations: u32, + min_monero_confirmations: u64, } impl State1 { @@ -245,7 +245,7 @@ pub struct State2 { tx_lock: bitcoin::TxLock, tx_cancel_sig_a: Signature, tx_refund_encsig: bitcoin::EncryptedSignature, - min_monero_confirmations: u32, + min_monero_confirmations: u64, } impl State2 { @@ -302,7 +302,7 @@ pub struct State3 { tx_lock: bitcoin::TxLock, tx_cancel_sig_a: Signature, tx_refund_encsig: bitcoin::EncryptedSignature, - min_monero_confirmations: u32, + min_monero_confirmations: u64, } impl State3 { @@ -485,23 +485,17 @@ pub struct State5 { s_b: monero::Scalar, v: monero::PrivateViewKey, tx_lock: bitcoin::TxLock, - monero_wallet_restore_blockheight: BlockHeight, + pub monero_wallet_restore_blockheight: BlockHeight, } impl State5 { - pub async fn claim_xmr(&self, monero_wallet: &monero::Wallet) -> Result<()> { + pub fn xmr_keys(&self) -> (monero::PrivateKey, monero::PrivateViewKey) { let s_b = monero::PrivateKey { scalar: self.s_b }; - let s = self.s_a + s_b; - // NOTE: This actually generates and opens a new wallet, closing the currently - // open one. - monero_wallet - .create_from_and_load(s, self.v, self.monero_wallet_restore_blockheight) - .await?; - - Ok(()) + (s, self.v) } + pub fn tx_lock_id(&self) -> bitcoin::Txid { self.tx_lock.txid() } diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index cb264750..ac0ef989 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -1,17 +1,14 @@ use crate::bitcoin::ExpiredTimelocks; -use crate::database::{Database, Swap}; +use crate::database::Swap; use crate::env::Config; use crate::protocol::bob; use crate::protocol::bob::event_loop::EventLoopHandle; use crate::protocol::bob::state::*; use crate::{bitcoin, monero}; use anyhow::{bail, Context, Result}; -use async_recursion::async_recursion; use rand::rngs::OsRng; -use std::sync::Arc; use tokio::select; use tracing::trace; -use uuid::Uuid; pub fn is_complete(state: &BobState) -> bool { matches!( @@ -29,49 +26,48 @@ pub async fn run(swap: bob::Swap) -> Result { } pub async fn run_until( - swap: bob::Swap, + mut swap: bob::Swap, is_target_state: fn(&BobState) -> bool, ) -> Result { - run_until_internal( - swap.state, - is_target_state, - swap.event_loop_handle, - swap.db, - swap.bitcoin_wallet, - swap.monero_wallet, - swap.swap_id, - swap.env_config, - swap.receive_monero_address, - ) - .await + let mut current_state = swap.state; + + while !is_target_state(¤t_state) { + current_state = next_state( + current_state, + &mut swap.event_loop_handle, + swap.bitcoin_wallet.as_ref(), + swap.monero_wallet.as_ref(), + &swap.env_config, + swap.receive_monero_address, + ) + .await?; + + let db_state = current_state.clone().into(); + swap.db + .insert_latest_state(swap.swap_id, Swap::Bob(db_state)) + .await?; + } + + Ok(current_state) } -// State machine driver for swap execution -#[allow(clippy::too_many_arguments)] -#[async_recursion] -async fn run_until_internal( +async fn next_state( state: BobState, - is_target_state: fn(&BobState) -> bool, - mut event_loop_handle: EventLoopHandle, - db: Database, - bitcoin_wallet: Arc, - monero_wallet: Arc, - swap_id: Uuid, - env_config: Config, + event_loop_handle: &mut EventLoopHandle, + bitcoin_wallet: &bitcoin::Wallet, + monero_wallet: &monero::Wallet, + env_config: &Config, receive_monero_address: monero::Address, ) -> Result { trace!("Current state: {}", state); - if is_target_state(&state) { - return Ok(state); - } - let new_state = match state { + Ok(match state { BobState::Started { btc_amount } => { let bitcoin_refund_address = bitcoin_wallet.new_address().await?; let state2 = request_price_and_setup( btc_amount, - &mut event_loop_handle, + event_loop_handle, env_config, bitcoin_refund_address, ) @@ -93,10 +89,10 @@ async fn run_until_internal( // Bob has locked Btc // Watch for Alice to Lock Xmr or for cancel timelock to elapse BobState::BtcLocked(state3) => { - if let ExpiredTimelocks::None = state3.current_epoch(bitcoin_wallet.as_ref()).await? { + if let ExpiredTimelocks::None = state3.current_epoch(bitcoin_wallet).await? { let transfer_proof_watcher = event_loop_handle.recv_transfer_proof(); let cancel_timelock_expires = - state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); + state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet); // Record the current monero wallet block height so we don't have to scan from // block 0 once we create the redeem wallet. @@ -133,7 +129,7 @@ async fn run_until_internal( lock_transfer_proof, monero_wallet_restore_blockheight, } => { - if let ExpiredTimelocks::None = state.current_epoch(bitcoin_wallet.as_ref()).await? { + if let ExpiredTimelocks::None = state.current_epoch(bitcoin_wallet).await? { let watch_request = state.lock_xmr_watch_request(lock_transfer_proof); select! { @@ -142,13 +138,13 @@ async fn run_until_internal( Ok(()) => BobState::XmrLocked(state.xmr_locked(monero_wallet_restore_blockheight)), Err(e) => { tracing::warn!("Waiting for refund because insufficient Monero have been locked! {}", e); - state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()).await?; + state.wait_for_cancel_timelock_to_expire(bitcoin_wallet).await?; BobState::CancelTimelockExpired(state.cancel()) }, } } - _ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => { + _ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet) => { BobState::CancelTimelockExpired(state.cancel()) } } @@ -157,7 +153,7 @@ async fn run_until_internal( } } BobState::XmrLocked(state) => { - if let ExpiredTimelocks::None = state.expired_timelock(bitcoin_wallet.as_ref()).await? { + if let ExpiredTimelocks::None = state.expired_timelock(bitcoin_wallet).await? { // Alice has locked Xmr // Bob sends Alice his key @@ -165,7 +161,7 @@ async fn run_until_internal( _ = event_loop_handle.send_encrypted_signature(state.tx_redeem_encsig()) => { BobState::EncSigSent(state) }, - _ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => { + _ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet) => { BobState::CancelTimelockExpired(state.cancel()) } } @@ -174,12 +170,12 @@ async fn run_until_internal( } } BobState::EncSigSent(state) => { - if let ExpiredTimelocks::None = state.expired_timelock(bitcoin_wallet.as_ref()).await? { + if let ExpiredTimelocks::None = state.expired_timelock(bitcoin_wallet).await? { select! { - state5 = state.watch_for_redeem_btc(bitcoin_wallet.as_ref()) => { + state5 = state.watch_for_redeem_btc(bitcoin_wallet) => { BobState::BtcRedeemed(state5?) }, - _ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => { + _ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet) => { BobState::CancelTimelockExpired(state.cancel()) } } @@ -188,8 +184,13 @@ async fn run_until_internal( } } BobState::BtcRedeemed(state) => { - // Bob redeems XMR using revealed s_a - state.claim_xmr(monero_wallet.as_ref()).await?; + let (spend_key, view_key) = state.xmr_keys(); + + // NOTE: This actually generates and opens a new wallet, closing the currently + // open one. + monero_wallet + .create_from_and_load(spend_key, view_key, state.monero_wallet_restore_blockheight) + .await?; // Ensure that the generated wallet is synced so we have a proper balance monero_wallet.refresh().await?; @@ -205,26 +206,22 @@ async fn run_until_internal( } } BobState::CancelTimelockExpired(state4) => { - if state4 - .check_for_tx_cancel(bitcoin_wallet.as_ref()) - .await - .is_err() - { - state4.submit_tx_cancel(bitcoin_wallet.as_ref()).await?; + if state4.check_for_tx_cancel(bitcoin_wallet).await.is_err() { + state4.submit_tx_cancel(bitcoin_wallet).await?; } BobState::BtcCancelled(state4) } BobState::BtcCancelled(state) => { // Bob has cancelled the swap - match state.expired_timelock(bitcoin_wallet.as_ref()).await? { + match state.expired_timelock(bitcoin_wallet).await? { ExpiredTimelocks::None => { bail!( "Internal error: canceled state reached before cancel timelock was expired" ); } ExpiredTimelocks::Cancel => { - state.refund_btc(bitcoin_wallet.as_ref()).await?; + state.refund_btc(bitcoin_wallet).await?; BobState::BtcRefunded(state) } ExpiredTimelocks::Punish => BobState::BtcPunished { @@ -236,28 +233,13 @@ async fn run_until_internal( BobState::BtcPunished { tx_lock_id } => BobState::BtcPunished { tx_lock_id }, BobState::SafelyAborted => BobState::SafelyAborted, BobState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id }, - }; - - let db_state = new_state.clone().into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - run_until_internal( - new_state, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - swap_id, - env_config, - receive_monero_address, - ) - .await + }) } pub async fn request_price_and_setup( btc: bitcoin::Amount, event_loop_handle: &mut EventLoopHandle, - env_config: Config, + env_config: &Config, bitcoin_refund_address: bitcoin::Address, ) -> Result { let xmr = event_loop_handle.request_spot_price(btc).await?; diff --git a/swap/src/trace.rs b/swap/src/trace.rs index 2c80de97..70728fd6 100644 --- a/swap/src/trace.rs +++ b/swap/src/trace.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use tracing_log::LogTracer; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::FmtSubscriber; @@ -8,9 +7,6 @@ pub fn init_tracing(level: LevelFilter) -> Result<()> { return Ok(()); } - // We want upstream library log messages, just only at Info level. - LogTracer::init_with_filter(tracing_log::log::LevelFilter::Info)?; - let is_terminal = atty::is(atty::Stream::Stderr); let builder = FmtSubscriber::builder() diff --git a/swap/tests/testutils/mod.rs b/swap/tests/testutils/mod.rs index fb442c2d..7df968ed 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -2,13 +2,16 @@ mod bitcoind; mod electrs; use crate::testutils; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; +use async_trait::async_trait; use bitcoin_harness::{BitcoindRpcApi, Client}; use futures::Future; use get_port::get_port; use libp2p::core::Multiaddr; use libp2p::{PeerId, Swarm}; use monero_harness::{image, Monero}; +use std::cmp::Ordering; +use std::fmt; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; @@ -28,8 +31,7 @@ use testcontainers::{Container, Docker, RunArgs}; use tokio::sync::mpsc; use tokio::task::JoinHandle; use tokio::time::interval; -use tracing::dispatcher::DefaultGuard; -use tracing_log::LogTracer; +use tracing_subscriber::util::SubscriberInitExt; use url::Url; use uuid::Uuid; @@ -148,108 +150,83 @@ impl TestContext { pub async fn assert_alice_redeemed(&mut self, state: AliceState) { assert!(matches!(state, AliceState::BtcRedeemed)); - self.alice_bitcoin_wallet.sync().await.unwrap(); + assert_eventual_balance( + self.alice_bitcoin_wallet.as_ref(), + Ordering::Equal, + self.alice_redeemed_btc_balance(), + ) + .await + .unwrap(); - let btc_balance_after_swap = self.alice_bitcoin_wallet.as_ref().balance().await.unwrap(); - assert_eq!( - btc_balance_after_swap, - self.alice_starting_balances.btc + self.btc_amount - - bitcoin::Amount::from_sat(bitcoin::TX_FEE) - ); - - let xmr_balance_after_swap = self - .alice_monero_wallet - .as_ref() - .get_balance() - .await - .unwrap(); - assert!( - xmr_balance_after_swap <= self.alice_starting_balances.xmr - self.xmr_amount, - "{} !< {} - {}", - xmr_balance_after_swap, - self.alice_starting_balances.xmr, - self.xmr_amount - ); + assert_eventual_balance( + self.alice_monero_wallet.as_ref(), + Ordering::Less, + self.alice_redeemed_xmr_balance(), + ) + .await + .unwrap(); } pub async fn assert_alice_refunded(&mut self, state: AliceState) { assert!(matches!(state, AliceState::XmrRefunded)); - self.alice_bitcoin_wallet.sync().await.unwrap(); - - let btc_balance_after_swap = self.alice_bitcoin_wallet.as_ref().balance().await.unwrap(); - assert_eq!(btc_balance_after_swap, self.alice_starting_balances.btc); - - // Ensure that Alice's balance is refreshed as we use a newly created wallet - self.alice_monero_wallet.as_ref().refresh().await.unwrap(); - let xmr_balance_after_swap = self - .alice_monero_wallet - .as_ref() - .get_balance() - .await - .unwrap(); + assert_eventual_balance( + self.alice_bitcoin_wallet.as_ref(), + Ordering::Equal, + self.alice_refunded_btc_balance(), + ) + .await + .unwrap(); // Alice pays fees - comparison does not take exact lock fee into account - assert!( - xmr_balance_after_swap > self.alice_starting_balances.xmr - self.xmr_amount, - "{} > {} - {}", - xmr_balance_after_swap, - self.alice_starting_balances.xmr, - self.xmr_amount - ); + assert_eventual_balance( + self.alice_monero_wallet.as_ref(), + Ordering::Greater, + self.alice_refunded_xmr_balance(), + ) + .await + .unwrap(); } pub async fn assert_alice_punished(&self, state: AliceState) { assert!(matches!(state, AliceState::BtcPunished)); - self.alice_bitcoin_wallet.sync().await.unwrap(); + assert_eventual_balance( + self.alice_bitcoin_wallet.as_ref(), + Ordering::Equal, + self.alice_punished_btc_balance(), + ) + .await + .unwrap(); - let btc_balance_after_swap = self.alice_bitcoin_wallet.as_ref().balance().await.unwrap(); - assert_eq!( - btc_balance_after_swap, - self.alice_starting_balances.btc + self.btc_amount - - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE) - ); - - let xmr_balance_after_swap = self - .alice_monero_wallet - .as_ref() - .get_balance() - .await - .unwrap(); - assert!(xmr_balance_after_swap <= self.alice_starting_balances.xmr - self.xmr_amount); + assert_eventual_balance( + self.alice_monero_wallet.as_ref(), + Ordering::Less, + self.alice_punished_xmr_balance(), + ) + .await + .unwrap(); } pub async fn assert_bob_redeemed(&self, state: BobState) { - self.bob_bitcoin_wallet.sync().await.unwrap(); - - let lock_tx_id = if let BobState::XmrRedeemed { tx_lock_id } = state { - tx_lock_id - } else { - panic!("Bob in not in xmr redeemed state: {:?}", state); - }; - - let lock_tx_bitcoin_fee = self - .bob_bitcoin_wallet - .transaction_fee(lock_tx_id) - .await - .unwrap(); - - let btc_balance_after_swap = self.bob_bitcoin_wallet.as_ref().balance().await.unwrap(); - assert_eq!( - btc_balance_after_swap, - self.bob_starting_balances.btc - self.btc_amount - lock_tx_bitcoin_fee - ); + assert_eventual_balance( + self.bob_bitcoin_wallet.as_ref(), + Ordering::Equal, + self.bob_redeemed_btc_balance(state).await.unwrap(), + ) + .await + .unwrap(); // unload the generated wallet by opening the original wallet self.bob_monero_wallet.re_open().await.unwrap(); - // refresh the original wallet to make sure the balance is caught up - self.bob_monero_wallet.refresh().await.unwrap(); - // Ensure that Bob's balance is refreshed as we use a newly created wallet - self.bob_monero_wallet.as_ref().refresh().await.unwrap(); - let xmr_balance_after_swap = self.bob_monero_wallet.as_ref().get_balance().await.unwrap(); - assert!(xmr_balance_after_swap > self.bob_starting_balances.xmr); + assert_eventual_balance( + self.bob_monero_wallet.as_ref(), + Ordering::Greater, + self.bob_redeemed_xmr_balance(), + ) + .await + .unwrap(); } pub async fn assert_bob_refunded(&self, state: BobState) { @@ -266,7 +243,7 @@ impl TestContext { .await .unwrap(); - let btc_balance_after_swap = self.bob_bitcoin_wallet.as_ref().balance().await.unwrap(); + let btc_balance_after_swap = self.bob_bitcoin_wallet.balance().await.unwrap(); let alice_submitted_cancel = btc_balance_after_swap == self.bob_starting_balances.btc @@ -282,33 +259,181 @@ impl TestContext { // Since we cannot be sure who submitted it we have to assert accordingly assert!(alice_submitted_cancel || bob_submitted_cancel); - let xmr_balance_after_swap = self.bob_monero_wallet.as_ref().get_balance().await.unwrap(); - assert_eq!(xmr_balance_after_swap, self.bob_starting_balances.xmr); + assert_eventual_balance( + self.bob_monero_wallet.as_ref(), + Ordering::Equal, + self.bob_refunded_xmr_balance(), + ) + .await + .unwrap(); } pub async fn assert_bob_punished(&self, state: BobState) { - self.bob_bitcoin_wallet.sync().await.unwrap(); + assert_eventual_balance( + self.bob_bitcoin_wallet.as_ref(), + Ordering::Equal, + self.bob_punished_btc_balance(state).await.unwrap(), + ) + .await + .unwrap(); + + assert_eventual_balance( + self.bob_monero_wallet.as_ref(), + Ordering::Equal, + self.bob_punished_xmr_balance(), + ) + .await + .unwrap(); + } + + fn alice_redeemed_xmr_balance(&self) -> monero::Amount { + self.alice_starting_balances.xmr - self.xmr_amount + } + + fn alice_redeemed_btc_balance(&self) -> bitcoin::Amount { + self.alice_starting_balances.btc + self.btc_amount + - bitcoin::Amount::from_sat(bitcoin::TX_FEE) + } + + fn bob_redeemed_xmr_balance(&self) -> monero::Amount { + self.bob_starting_balances.xmr + } + + async fn bob_redeemed_btc_balance(&self, state: BobState) -> Result { + self.bob_bitcoin_wallet.sync().await?; + + let lock_tx_id = if let BobState::XmrRedeemed { tx_lock_id } = state { + tx_lock_id + } else { + bail!("Bob in not in xmr redeemed state: {:?}", state); + }; + + let lock_tx_bitcoin_fee = self.bob_bitcoin_wallet.transaction_fee(lock_tx_id).await?; + + Ok(self.bob_starting_balances.btc - self.btc_amount - lock_tx_bitcoin_fee) + } + + fn alice_refunded_xmr_balance(&self) -> monero::Amount { + self.alice_starting_balances.xmr - self.xmr_amount + } + + fn alice_refunded_btc_balance(&self) -> bitcoin::Amount { + self.alice_starting_balances.btc + } + + fn bob_refunded_xmr_balance(&self) -> monero::Amount { + self.bob_starting_balances.xmr + } + + fn alice_punished_xmr_balance(&self) -> monero::Amount { + self.alice_starting_balances.xmr - self.xmr_amount + } + + fn alice_punished_btc_balance(&self) -> bitcoin::Amount { + self.alice_starting_balances.btc + self.btc_amount + - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE) + } + + fn bob_punished_xmr_balance(&self) -> monero::Amount { + self.bob_starting_balances.xmr + } + + async fn bob_punished_btc_balance(&self, state: BobState) -> Result { + self.bob_bitcoin_wallet.sync().await?; let lock_tx_id = if let BobState::BtcPunished { tx_lock_id } = state { tx_lock_id } else { - panic!("Bob in not in btc punished state: {:?}", state); + bail!("Bob in not in btc punished state: {:?}", state); }; - let lock_tx_bitcoin_fee = self - .bob_bitcoin_wallet - .transaction_fee(lock_tx_id) - .await - .unwrap(); + let lock_tx_bitcoin_fee = self.bob_bitcoin_wallet.transaction_fee(lock_tx_id).await?; - let btc_balance_after_swap = self.bob_bitcoin_wallet.as_ref().balance().await.unwrap(); - assert_eq!( - btc_balance_after_swap, - self.bob_starting_balances.btc - self.btc_amount - lock_tx_bitcoin_fee + Ok(self.bob_starting_balances.btc - self.btc_amount - lock_tx_bitcoin_fee) + } +} + +async fn assert_eventual_balance( + wallet: &impl Wallet, + ordering: Ordering, + expected: A, +) -> Result<()> { + let ordering_str = match ordering { + Ordering::Less => "less than", + Ordering::Equal => "equal to", + Ordering::Greater => "greater than", + }; + + let mut current_balance = wallet.get_balance().await?; + + let assertion = async { + while current_balance.partial_cmp(&expected).unwrap() != ordering { + tokio::time::sleep(Duration::from_millis(500)).await; + + wallet.refresh().await?; + current_balance = wallet.get_balance().await?; + } + + tracing::debug!( + "Assertion successful! Balance {} is {} {}", + current_balance, + ordering_str, + expected ); - let xmr_balance_after_swap = self.bob_monero_wallet.as_ref().get_balance().await.unwrap(); - assert_eq!(xmr_balance_after_swap, self.bob_starting_balances.xmr); + Result::<_, anyhow::Error>::Ok(()) + }; + + let timeout = Duration::from_secs(10); + + tokio::time::timeout(timeout, assertion) + .await + .with_context(|| { + format!( + "Expected balance to be {} {} after at most {}s but was {}", + ordering_str, + expected, + timeout.as_secs(), + current_balance + ) + })??; + + Ok(()) +} + +#[async_trait] +trait Wallet { + type Amount; + + async fn refresh(&self) -> Result<()>; + async fn get_balance(&self) -> Result; +} + +#[async_trait] +impl Wallet for monero::Wallet { + type Amount = monero::Amount; + + async fn refresh(&self) -> Result<()> { + self.refresh().await?; + + Ok(()) + } + + async fn get_balance(&self) -> Result { + self.get_balance().await + } +} + +#[async_trait] +impl Wallet for bitcoin::Wallet { + type Amount = bitcoin::Amount; + + async fn refresh(&self) -> Result<()> { + self.sync().await + } + + async fn get_balance(&self) -> Result { + self.balance().await } } @@ -320,7 +445,10 @@ where { let cli = Cli::default(); - let _guard = init_tracing(); + let _guard = tracing_subscriber::fmt() + .with_env_filter("warn,swap=debug,monero_harness=debug,monero_rpc=info,bitcoin_harness=info,testcontainers=info") + .with_test_writer() + .set_default(); let env_config = C::get_config(); @@ -659,24 +787,6 @@ struct Containers<'a> { electrs: Container<'a, Cli, electrs::Electrs>, } -/// Utility function to initialize logging in the test environment. -/// Note that you have to keep the `_guard` in scope after calling in test: -/// -/// ```rust -/// let _guard = init_tracing(); -/// ``` -pub fn init_tracing() -> DefaultGuard { - // converts all log records into tracing events - // Note: Make sure to initialize without unwrapping, otherwise this causes - // trouble when running multiple tests. - let _ = LogTracer::init(); - - use tracing_subscriber::util::SubscriberInitExt as _; - tracing_subscriber::fmt() - .with_env_filter("warn,swap=debug,monero_harness=debug,monero_rpc=info,bitcoin_harness=info,testcontainers=info") - .set_default() -} - pub mod alice_run_until { use swap::protocol::alice::AliceState;