diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ea64ba1..7d1fcc79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,10 +110,13 @@ jobs: happy_path, happy_path_restart_bob_after_xmr_locked, happy_path_restart_bob_before_xmr_locked, + happy_path_restart_alice_after_xmr_locked, bob_refunds_using_cancel_and_refund_command, bob_refunds_using_cancel_and_refund_command_timelock_not_expired, bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force, - punish + punish, + alice_punishes_after_restart_punish_timelock_expired, + alice_refunds_after_restart_bob_refunded ] runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 81e7a2ac..f96cedf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- The `resume` command of the `swap` CLI no longer require the `--seller-peer-id` parameter. + This information is now saved in the database. + ### Added - A changelog file. +- Automatic resume of unfinished swaps for the `asb` upon startup. + Unfinished swaps from earlier versions will be skipped. ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 99880a98..fbbec2b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1552,6 +1552,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.7" @@ -2547,7 +2556,7 @@ checksum = "32d3ebd75ac2679c2af3a92246639f9fcc8a442ee420719cc4fe195b98dd5fa3" dependencies = [ "bytes 1.0.1", "heck", - "itertools", + "itertools 0.9.0", "log 0.4.14", "multimap", "petgraph", @@ -2564,7 +2573,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "169a15f3008ecb5160cba7d37bcd690a7601b6d30cfb87a117d45e59d52af5d4" dependencies = [ "anyhow", - "itertools", + "itertools 0.9.0", "proc-macro2", "quote", "syn", @@ -3522,6 +3531,7 @@ dependencies = [ "futures", "get-port", "hyper 0.14.5", + "itertools 0.10.0", "libp2p", "libp2p-async-await", "miniscript", diff --git a/bors.toml b/bors.toml index 294fc31b..6d83fcd2 100644 --- a/bors.toml +++ b/bors.toml @@ -8,9 +8,12 @@ status = [ "test (macos-latest)", "docker_tests (happy_path)", "docker_tests (happy_path_restart_bob_after_xmr_locked)", + "docker_tests (happy_path_restart_alice_after_xmr_locked)", "docker_tests (happy_path_restart_bob_before_xmr_locked)", "docker_tests (bob_refunds_using_cancel_and_refund_command)", "docker_tests (bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force)", "docker_tests (bob_refunds_using_cancel_and_refund_command_timelock_not_expired)", - "docker_tests (punish)" + "docker_tests (punish)", + "docker_tests (alice_punishes_after_restart_punish_timelock_expired)", + "docker_tests (alice_refunds_after_restart_bob_refunded)" ] diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 09014eba..d36589dc 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -25,6 +25,7 @@ dialoguer = "0.8" directories-next = "2" ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", features = ["libsecp_compat", "serde"] } futures = { version = "0.3", default-features = false } +itertools = "0.10" libp2p = { version = "0.36", default-features = false, features = ["tcp-tokio", "yamux", "mplex", "dns-tokio", "noise", "request-response"] } libp2p-async-await = { git = "https://github.com/comit-network/rust-libp2p-async-await" } miniscript = { version = "5", features = ["serde"] } diff --git a/swap/src/asb/fixed_rate.rs b/swap/src/asb/fixed_rate.rs index b78da380..bf42be53 100644 --- a/swap/src/asb/fixed_rate.rs +++ b/swap/src/asb/fixed_rate.rs @@ -1,6 +1,6 @@ use crate::asb::Rate; -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub struct FixedRate(Rate); impl FixedRate { diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index fb48a148..d77bf271 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -130,7 +130,7 @@ async fn main() -> Result<()> { table.add_row(row!["SWAP ID", "STATE"]); - for (swap_id, state) in db.all()? { + for (swap_id, state) in db.all_alice()? { table.add_row(row![swap_id, state]); } diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index c1d6721f..ec731880 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -21,7 +21,7 @@ use std::sync::Arc; use std::time::Duration; use structopt::StructOpt; use swap::bitcoin::{Amount, TxLock}; -use swap::cli::command::{AliceConnectParams, Arguments, Command, Data, MoneroParams}; +use swap::cli::command::{AliceMultiaddress, Arguments, Command, Data, MoneroParams}; use swap::database::Database; use swap::env::{Config, GetConfig}; use swap::network::quote::BidQuote; @@ -81,9 +81,9 @@ async fn main() -> Result<()> { match args.cmd { Command::BuyXmr { - connect_params: - AliceConnectParams { - peer_id: alice_peer_id, + alice_peer_id, + alice_multi_addr: + AliceMultiaddress { multiaddr: alice_addr, }, monero_params: @@ -131,9 +131,12 @@ async fn main() -> Result<()> { ) .await?; + let swap_id = Uuid::new_v4(); + db.insert_peer_id(swap_id, alice_peer_id).await?; + let swap = Builder::new( db, - Uuid::new_v4(), + swap_id, bitcoin_wallet.clone(), Arc::new(monero_wallet), env_config, @@ -158,7 +161,7 @@ async fn main() -> Result<()> { table.add_row(row!["SWAP ID", "STATE"]); - for (swap_id, state) in db.all()? { + for (swap_id, state) in db.all_bob()? { table.add_row(row![swap_id, state]); } @@ -167,9 +170,8 @@ async fn main() -> Result<()> { } Command::Resume { swap_id, - connect_params: - AliceConnectParams { - peer_id: alice_peer_id, + alice_multi_addr: + AliceMultiaddress { multiaddr: alice_addr, }, monero_params: @@ -189,6 +191,7 @@ async fn main() -> Result<()> { init_monero_wallet(data_dir, monero_daemon_host, env_config).await?; let bitcoin_wallet = Arc::new(bitcoin_wallet); + let alice_peer_id = db.get_peer_id(swap_id)?; let mut swarm = swarm::new::(&seed)?; swarm.add_address(alice_peer_id, alice_addr); diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index 47c8b9f1..6e66736e 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -37,8 +37,15 @@ pub struct Arguments { pub enum Command { /// Start a XMR for BTC swap BuyXmr { + #[structopt( + long = "seller-peer-id", + default_value = DEFAULT_ALICE_PEER_ID, + help = "The peer id of a specific swap partner can be optionally provided" + )] + alice_peer_id: PeerId, + #[structopt(flatten)] - connect_params: AliceConnectParams, + alice_multi_addr: AliceMultiaddress, #[structopt(long = "electrum-rpc", help = "Provide the Bitcoin Electrum RPC URL", @@ -60,7 +67,7 @@ pub enum Command { swap_id: Uuid, #[structopt(flatten)] - connect_params: AliceConnectParams, + alice_multi_addr: AliceMultiaddress, #[structopt(long = "electrum-rpc", help = "Provide the Bitcoin Electrum RPC URL", @@ -108,14 +115,7 @@ pub enum Command { } #[derive(structopt::StructOpt, Debug)] -pub struct AliceConnectParams { - #[structopt( - long = "seller-peer-id", - default_value = DEFAULT_ALICE_PEER_ID, - help = "The peer id of a specific swap partner can be optionally provided" - )] - pub peer_id: PeerId, - +pub struct AliceMultiaddress { #[structopt( long = "seller-addr", default_value = DEFAULT_ALICE_MULTIADDR, diff --git a/swap/src/database.rs b/swap/src/database.rs index 98b7f092..a6839544 100644 --- a/swap/src/database.rs +++ b/swap/src/database.rs @@ -2,10 +2,13 @@ pub use alice::Alice; pub use bob::Bob; use anyhow::{anyhow, bail, Context, Result}; +use itertools::Itertools; +use libp2p::PeerId; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::fmt::Display; use std::path::Path; +use std::str::FromStr; use uuid::Uuid; mod alice; @@ -38,16 +41,34 @@ impl Display for Swap { } } +#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)] +#[error("Not in the role of Alice")] +struct NotAlice; + +#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)] +#[error("Not in the role of Bob")] +struct NotBob; + impl Swap { + pub fn try_into_alice(self) -> Result { + match self { + Swap::Alice(alice) => Ok(alice), + Swap::Bob(_) => bail!(NotAlice), + } + } + pub fn try_into_bob(self) -> Result { match self { Swap::Bob(bob) => Ok(bob), - Swap::Alice(_) => bail!("Swap instance is not Bob"), + Swap::Alice(_) => bail!(NotBob), } } } -pub struct Database(sled::Db); +pub struct Database { + swaps: sled::Tree, + peers: sled::Tree, +} impl Database { pub fn open(path: &Path) -> Result { @@ -56,22 +77,51 @@ impl Database { let db = sled::open(path).with_context(|| format!("Could not open the DB at {:?}", path))?; - Ok(Database(db)) + let swaps = db.open_tree("swaps")?; + let peers = db.open_tree("peers")?; + + Ok(Database { swaps, peers }) + } + + pub async fn insert_peer_id(&self, swap_id: Uuid, peer_id: PeerId) -> Result<()> { + let peer_id_str = peer_id.to_string(); + + let key = serialize(&swap_id)?; + let value = serialize(&peer_id_str).context("Could not serialize peer-id")?; + + self.peers.insert(key, value)?; + + self.peers + .flush_async() + .await + .map(|_| ()) + .context("Could not flush db") + } + + pub fn get_peer_id(&self, swap_id: Uuid) -> Result { + let key = serialize(&swap_id)?; + + let encoded = self + .peers + .get(&key)? + .ok_or_else(|| anyhow!("No peer-id found for swap id {} in database", swap_id))?; + + let peer_id: String = deserialize(&encoded).context("Could not deserialize peer-id")?; + Ok(PeerId::from_str(peer_id.as_str())?) } pub async fn insert_latest_state(&self, swap_id: Uuid, state: Swap) -> Result<()> { let key = serialize(&swap_id)?; let new_value = serialize(&state).context("Could not serialize new state value")?; - let old_value = self.0.get(&key)?; + let old_value = self.swaps.get(&key)?; - self.0 + self.swaps .compare_and_swap(key, old_value, Some(new_value)) .context("Could not write in the DB")? .context("Stored swap somehow changed, aborting saving")?; - // TODO: see if this can be done through sled config - self.0 + self.swaps .flush_async() .await .map(|_| ()) @@ -82,7 +132,7 @@ impl Database { let key = serialize(&swap_id)?; let encoded = self - .0 + .swaps .get(&key)? .ok_or_else(|| anyhow!("Swap with id {} not found in database", swap_id))?; @@ -90,22 +140,42 @@ impl Database { Ok(state) } - pub fn all(&self) -> Result> { - self.0 - .iter() - .map(|item| match item { - Ok((key, value)) => { - let swap_id = deserialize::(&key); - let swap = deserialize::(&value).context("Failed to deserialize swap"); + pub fn all_alice(&self) -> Result> { + self.all_alice_iter().collect() + } - match (swap_id, swap) { - (Ok(swap_id), Ok(swap)) => Ok((swap_id, swap)), - (Ok(_), Err(err)) => Err(err), - _ => bail!("Failed to deserialize swap"), - } - } - Err(err) => Err(err).context("Failed to retrieve swap from DB"), - }) + fn all_alice_iter(&self) -> impl Iterator> { + self.all_swaps_iter().map(|item| { + let (swap_id, swap) = item?; + Ok((swap_id, swap.try_into_alice()?)) + }) + } + + pub fn all_bob(&self) -> Result> { + self.all_bob_iter().collect() + } + + fn all_bob_iter(&self) -> impl Iterator> { + self.all_swaps_iter().map(|item| { + let (swap_id, swap) = item?; + Ok((swap_id, swap.try_into_bob()?)) + }) + } + + fn all_swaps_iter(&self) -> impl Iterator> { + self.swaps.iter().map(|item| { + let (key, value) = item.context("Failed to retrieve swap from DB")?; + + let swap_id = deserialize::(&key)?; + let swap = deserialize::(&value).context("Failed to deserialize swap")?; + + Ok((swap_id, swap)) + }) + } + + pub fn unfinished_alice(&self) -> Result> { + self.all_alice_iter() + .filter_ok(|(_swap_id, alice)| !matches!(alice, Alice::Done(_))) .collect() } } @@ -187,26 +257,106 @@ mod tests { } #[tokio::test] - async fn can_fetch_all_keys() { + async fn all_swaps_as_alice() { let db_dir = tempfile::tempdir().unwrap(); let db = Database::open(db_dir.path()).unwrap(); - let state_1 = Swap::Alice(Alice::Done(AliceEndState::BtcPunished)); - let swap_id_1 = Uuid::new_v4(); - db.insert_latest_state(swap_id_1, state_1.clone()) + let alice_state = Alice::Done(AliceEndState::BtcPunished); + let alice_swap = Swap::Alice(alice_state.clone()); + let alice_swap_id = Uuid::new_v4(); + db.insert_latest_state(alice_swap_id, alice_swap) .await - .expect("Failed to save second state"); + .expect("Failed to save alice state 1"); - let state_2 = Swap::Bob(Bob::Done(BobEndState::SafelyAborted)); - let swap_id_2 = Uuid::new_v4(); - db.insert_latest_state(swap_id_2, state_2.clone()) + let alice_swaps = db.all_alice().unwrap(); + assert_eq!(alice_swaps.len(), 1); + assert!(alice_swaps.contains(&(alice_swap_id, alice_state))); + + let bob_state = Bob::Done(BobEndState::SafelyAborted); + let bob_swap = Swap::Bob(bob_state); + let bob_swap_id = Uuid::new_v4(); + db.insert_latest_state(bob_swap_id, bob_swap) .await - .expect("Failed to save first state"); + .expect("Failed to save bob state 1"); - let swaps = db.all().unwrap(); + let err = db.all_alice().unwrap_err(); - assert_eq!(swaps.len(), 2); - assert!(swaps.contains(&(swap_id_1, state_1))); - assert!(swaps.contains(&(swap_id_2, state_2))); + assert_eq!(err.downcast_ref::().unwrap(), &NotAlice); + } + + #[tokio::test] + async fn all_swaps_as_bob() { + let db_dir = tempfile::tempdir().unwrap(); + let db = Database::open(db_dir.path()).unwrap(); + + let bob_state = Bob::Done(BobEndState::SafelyAborted); + let bob_swap = Swap::Bob(bob_state.clone()); + let bob_swap_id = Uuid::new_v4(); + db.insert_latest_state(bob_swap_id, bob_swap) + .await + .expect("Failed to save bob state 1"); + + let bob_swaps = db.all_bob().unwrap(); + assert_eq!(bob_swaps.len(), 1); + assert!(bob_swaps.contains(&(bob_swap_id, bob_state))); + + let alice_state = Alice::Done(AliceEndState::BtcPunished); + let alice_swap = Swap::Alice(alice_state); + let alice_swap_id = Uuid::new_v4(); + db.insert_latest_state(alice_swap_id, alice_swap) + .await + .expect("Failed to save alice state 1"); + + let err = db.all_bob().unwrap_err(); + + assert_eq!(err.downcast_ref::().unwrap(), &NotBob); + } + + #[tokio::test] + async fn can_save_swap_state_and_peer_id_with_same_swap_id() -> Result<()> { + let db_dir = tempfile::tempdir().unwrap(); + let db = Database::open(db_dir.path()).unwrap(); + + let alice_id = Uuid::new_v4(); + let alice_state = Alice::Done(AliceEndState::BtcPunished); + let alice_swap = Swap::Alice(alice_state); + let peer_id = PeerId::random(); + + db.insert_latest_state(alice_id, alice_swap.clone()).await?; + db.insert_peer_id(alice_id, peer_id).await?; + + let loaded_swap = db.get_state(alice_id)?; + let loaded_peer_id = db.get_peer_id(alice_id)?; + + assert_eq!(alice_swap, loaded_swap); + assert_eq!(peer_id, loaded_peer_id); + + Ok(()) + } + + #[tokio::test] + async fn test_reopen_db() -> Result<()> { + let db_dir = tempfile::tempdir().unwrap(); + let alice_id = Uuid::new_v4(); + let alice_state = Alice::Done(AliceEndState::BtcPunished); + let alice_swap = Swap::Alice(alice_state); + + let peer_id = PeerId::random(); + + { + let db = Database::open(db_dir.path()).unwrap(); + db.insert_latest_state(alice_id, alice_swap.clone()).await?; + db.insert_peer_id(alice_id, peer_id).await?; + } + + let db = Database::open(db_dir.path()).unwrap(); + + let loaded_swap = db.get_state(alice_id)?; + let loaded_peer_id = db.get_peer_id(alice_id)?; + + assert_eq!(alice_swap, loaded_swap); + assert_eq!(peer_id, loaded_peer_id); + + Ok(()) } } diff --git a/swap/src/database/alice.rs b/swap/src/database/alice.rs index b2424fba..b76e1f85 100644 --- a/swap/src/database/alice.rs +++ b/swap/src/database/alice.rs @@ -1,6 +1,6 @@ use crate::bitcoin::EncryptedSignature; use crate::monero; -use crate::monero::monero_private_key; +use crate::monero::{monero_private_key, TransferProof}; use crate::protocol::alice; use crate::protocol::alice::AliceState; use ::bitcoin::hashes::core::fmt::Display; @@ -18,29 +18,45 @@ pub enum Alice { BtcLocked { state3: alice::State3, }, + XmrLockTransactionSent { + monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, + state3: alice::State3, + }, XmrLocked { monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, + state3: alice::State3, + }, + XmrLockTransferProofSent { + monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, state3: alice::State3, }, EncSigLearned { monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, encrypted_signature: EncryptedSignature, state3: alice::State3, }, CancelTimelockExpired { monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, state3: alice::State3, }, BtcCancelled { monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, state3: alice::State3, }, BtcPunishable { monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, state3: alice::State3, }, BtcRefunded { monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, state3: alice::State3, #[serde(with = "monero_private_key")] spend_key: monero::PrivateKey, @@ -65,54 +81,82 @@ impl From<&AliceState> for Alice { AliceState::BtcLocked { state3 } => Alice::BtcLocked { state3: state3.as_ref().clone(), }, + AliceState::XmrLockTransactionSent { + monero_wallet_restore_blockheight, + transfer_proof, + state3, + } => Alice::XmrLockTransactionSent { + monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight, + transfer_proof: transfer_proof.clone(), + state3: state3.as_ref().clone(), + }, AliceState::XmrLocked { monero_wallet_restore_blockheight, + transfer_proof, state3, } => Alice::XmrLocked { monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight, + transfer_proof: transfer_proof.clone(), + state3: state3.as_ref().clone(), + }, + AliceState::XmrLockTransferProofSent { + monero_wallet_restore_blockheight, + transfer_proof, + state3, + } => Alice::XmrLockTransferProofSent { + monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight, + transfer_proof: transfer_proof.clone(), state3: state3.as_ref().clone(), }, AliceState::EncSigLearned { monero_wallet_restore_blockheight, + transfer_proof, state3, encrypted_signature, } => Alice::EncSigLearned { monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight, + transfer_proof: transfer_proof.clone(), state3: state3.as_ref().clone(), encrypted_signature: *encrypted_signature.clone(), }, AliceState::BtcRedeemed => Alice::Done(AliceEndState::BtcRedeemed), AliceState::BtcCancelled { monero_wallet_restore_blockheight, + transfer_proof, state3, - .. } => Alice::BtcCancelled { monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight, + transfer_proof: transfer_proof.clone(), state3: state3.as_ref().clone(), }, AliceState::BtcRefunded { monero_wallet_restore_blockheight, + transfer_proof, spend_key, state3, } => Alice::BtcRefunded { monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight, + transfer_proof: transfer_proof.clone(), spend_key: *spend_key, state3: state3.as_ref().clone(), }, AliceState::BtcPunishable { monero_wallet_restore_blockheight, + transfer_proof, state3, - .. } => Alice::BtcPunishable { monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight, + transfer_proof: transfer_proof.clone(), state3: state3.as_ref().clone(), }, AliceState::XmrRefunded => Alice::Done(AliceEndState::XmrRefunded), AliceState::CancelTimelockExpired { monero_wallet_restore_blockheight, + transfer_proof, state3, } => Alice::CancelTimelockExpired { monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight, + transfer_proof: transfer_proof.clone(), state3: state3.as_ref().clone(), }, AliceState::BtcPunished => Alice::Done(AliceEndState::BtcPunished), @@ -130,50 +174,80 @@ impl From for AliceState { Alice::BtcLocked { state3 } => AliceState::BtcLocked { state3: Box::new(state3), }, + Alice::XmrLockTransactionSent { + monero_wallet_restore_blockheight, + transfer_proof, + state3, + } => AliceState::XmrLockTransactionSent { + monero_wallet_restore_blockheight, + transfer_proof, + state3: Box::new(state3), + }, Alice::XmrLocked { monero_wallet_restore_blockheight, + transfer_proof, state3, } => AliceState::XmrLocked { monero_wallet_restore_blockheight, + transfer_proof, + state3: Box::new(state3), + }, + Alice::XmrLockTransferProofSent { + monero_wallet_restore_blockheight, + transfer_proof, + state3, + } => AliceState::XmrLockTransferProofSent { + monero_wallet_restore_blockheight, + transfer_proof, state3: Box::new(state3), }, Alice::EncSigLearned { monero_wallet_restore_blockheight, + transfer_proof, state3: state, encrypted_signature, } => AliceState::EncSigLearned { monero_wallet_restore_blockheight, + transfer_proof, state3: Box::new(state), encrypted_signature: Box::new(encrypted_signature), }, Alice::CancelTimelockExpired { monero_wallet_restore_blockheight, + transfer_proof, state3, } => AliceState::CancelTimelockExpired { monero_wallet_restore_blockheight, + transfer_proof, state3: Box::new(state3), }, Alice::BtcCancelled { monero_wallet_restore_blockheight, + transfer_proof, state3, } => AliceState::BtcCancelled { monero_wallet_restore_blockheight, + transfer_proof, state3: Box::new(state3), }, Alice::BtcPunishable { monero_wallet_restore_blockheight, + transfer_proof, state3, } => AliceState::BtcPunishable { monero_wallet_restore_blockheight, + transfer_proof, state3: Box::new(state3), }, Alice::BtcRefunded { monero_wallet_restore_blockheight, - state3, + transfer_proof, spend_key, + state3, } => AliceState::BtcRefunded { monero_wallet_restore_blockheight, + transfer_proof, spend_key, state3: Box::new(state3), }, @@ -192,7 +266,11 @@ impl Display for Alice { match self { Alice::Started { .. } => write!(f, "Started"), Alice::BtcLocked { .. } => f.write_str("Bitcoin locked"), + Alice::XmrLockTransactionSent { .. } => f.write_str("Monero lock transaction sent"), Alice::XmrLocked { .. } => f.write_str("Monero locked"), + Alice::XmrLockTransferProofSent { .. } => { + f.write_str("Monero lock transfer proof sent") + } Alice::CancelTimelockExpired { .. } => f.write_str("Cancel timelock is expired"), Alice::BtcCancelled { .. } => f.write_str("Bitcoin cancel transaction published"), Alice::BtcPunishable { .. } => f.write_str("Bitcoin punishable"), diff --git a/swap/src/protocol/alice/event_loop.rs b/swap/src/protocol/alice/event_loop.rs index 24948741..2ed686fe 100644 --- a/swap/src/protocol/alice/event_loop.rs +++ b/swap/src/protocol/alice/event_loop.rs @@ -84,6 +84,43 @@ where // terminate forever. self.send_transfer_proof.push(future::pending().boxed()); + let unfinished_swaps = match self.db.unfinished_alice() { + Ok(unfinished_swaps) => unfinished_swaps, + Err(_) => { + tracing::error!("Failed to load unfinished swaps"); + return; + } + }; + + for (swap_id, state) in unfinished_swaps { + let peer_id = match self.db.get_peer_id(swap_id) { + Ok(peer_id) => peer_id, + Err(_) => { + tracing::warn!(%swap_id, "Resuming swap skipped because no peer-id found for swap in database"); + continue; + } + }; + + let handle = self.new_handle(peer_id); + + let swap = Swap { + event_loop_handle: handle, + bitcoin_wallet: self.bitcoin_wallet.clone(), + monero_wallet: self.monero_wallet.clone(), + env_config: self.env_config, + db: self.db.clone(), + state: state.into(), + swap_id, + }; + + match self.swap_sender.send(swap).await { + Ok(_) => tracing::info!(%swap_id, "Resuming swap"), + Err(_) => { + tracing::warn!(%swap_id, "Failed to resume swap because receiver has been dropped") + } + } + } + loop { tokio::select! { swarm_event = self.swarm.next_event() => { @@ -264,8 +301,18 @@ where swap_id, }; - if let Err(error) = self.swap_sender.send(swap).await { - tracing::warn!(%swap_id, "Swap cannot be spawned: {}", error); + // TODO: Consider adding separate components for start/rsume of swaps + + // swaps save peer id so we can resume + match self.db.insert_peer_id(swap_id, bob_peer_id).await { + Ok(_) => { + if let Err(error) = self.swap_sender.send(swap).await { + tracing::warn!(%swap_id, "Swap cannot be spawned: {}", error); + } + } + Err(error) => { + tracing::warn!(%swap_id, "Unable to save peer-id, swap cannot be spawned: {}", error); + } } } diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index fe0f5679..4a6194fb 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -22,32 +22,48 @@ pub enum AliceState { BtcLocked { state3: Box, }, + XmrLockTransactionSent { + monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, + state3: Box, + }, XmrLocked { monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, + state3: Box, + }, + XmrLockTransferProofSent { + monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, state3: Box, }, EncSigLearned { monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, encrypted_signature: Box, state3: Box, }, BtcRedeemed, BtcCancelled { monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, state3: Box, }, BtcRefunded { monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, spend_key: monero::PrivateKey, state3: Box, }, BtcPunishable { monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, state3: Box, }, XmrRefunded, CancelTimelockExpired { monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, state3: Box, }, BtcPunished, @@ -59,7 +75,11 @@ impl fmt::Display for AliceState { match self { AliceState::Started { .. } => write!(f, "started"), AliceState::BtcLocked { .. } => write!(f, "btc is locked"), + AliceState::XmrLockTransactionSent { .. } => write!(f, "xmr lock transaction sent"), AliceState::XmrLocked { .. } => write!(f, "xmr is locked"), + AliceState::XmrLockTransferProofSent { .. } => { + write!(f, "xmr lock transfer proof sent") + } AliceState::EncSigLearned { .. } => write!(f, "encrypted signature is learned"), AliceState::BtcRedeemed => write!(f, "btc is redeemed"), AliceState::BtcCancelled { .. } => write!(f, "btc is cancelled"), diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index dc732bf7..a26474fb 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -5,39 +5,25 @@ use crate::env::Config; use crate::protocol::alice; use crate::protocol::alice::event_loop::EventLoopHandle; use crate::protocol::alice::AliceState; +use crate::protocol::alice::AliceState::XmrLockTransferProofSent; use crate::{bitcoin, database, monero}; use anyhow::{bail, Context, Result}; -use rand::{CryptoRng, RngCore}; use tokio::select; use tokio::time::timeout; use tracing::{error, info}; -trait Rng: RngCore + CryptoRng + Send {} - -impl Rng for T where T: RngCore + CryptoRng + Send {} - -pub fn is_complete(state: &AliceState) -> bool { - matches!( - state, - AliceState::XmrRefunded - | AliceState::BtcRedeemed - | AliceState::BtcPunished - | AliceState::SafelyAborted - ) -} - pub async fn run(swap: alice::Swap) -> Result { - run_until(swap, is_complete).await + run_until(swap, |_| false).await } -#[tracing::instrument(name = "swap", skip(swap,is_target_state), fields(id = %swap.swap_id))] +#[tracing::instrument(name = "swap", skip(swap,exit_early), fields(id = %swap.swap_id))] pub async fn run_until( mut swap: alice::Swap, - is_target_state: fn(&AliceState) -> bool, + exit_early: fn(&AliceState) -> bool, ) -> Result { let mut current_state = swap.state; - while !is_target_state(¤t_state) { + while !is_complete(¤t_state) && !exit_early(¤t_state) { current_state = next_state( current_state, &mut swap.event_loop_handle, @@ -89,38 +75,74 @@ async fn next_state( } } AliceState::BtcLocked { state3 } => { - // Record the current monero wallet block height so we don't have to scan from - // block 0 for scenarios where we create a refund wallet. - let monero_wallet_restore_blockheight = monero_wallet.block_height().await?; + match state3.expired_timelocks(bitcoin_wallet).await? { + ExpiredTimelocks::None => { + // Record the current monero wallet block height so we don't have to scan from + // block 0 for scenarios where we create a refund wallet. + let monero_wallet_restore_blockheight = monero_wallet.block_height().await?; - let transfer_proof = monero_wallet - .transfer(state3.lock_xmr_transfer_request()) - .await?; + let transfer_proof = monero_wallet + .transfer(state3.lock_xmr_transfer_request()) + .await?; - monero_wallet - .watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof.clone(), 1)) - .await?; - - // TODO: Waiting for XMR confirmations should be done in a separate - // state! We have to record that Alice has already sent the transaction. - // Otherwise Alice might publish the lock tx twice! - - event_loop_handle - .send_transfer_proof(transfer_proof.clone()) - .await?; - - monero_wallet - .watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof, 10)) - .await?; - - AliceState::XmrLocked { - state3, - monero_wallet_restore_blockheight, + AliceState::XmrLockTransactionSent { + monero_wallet_restore_blockheight, + transfer_proof, + state3, + } + } + _ => AliceState::SafelyAborted, } } - AliceState::XmrLocked { - state3, + AliceState::XmrLockTransactionSent { monero_wallet_restore_blockheight, + transfer_proof, + state3, + } => match state3.expired_timelocks(bitcoin_wallet).await? { + ExpiredTimelocks::None => { + monero_wallet + .watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof.clone(), 1)) + .await?; + + AliceState::XmrLocked { + monero_wallet_restore_blockheight, + transfer_proof, + state3, + } + } + _ => AliceState::CancelTimelockExpired { + monero_wallet_restore_blockheight, + transfer_proof, + state3, + }, + }, + + AliceState::XmrLocked { + monero_wallet_restore_blockheight, + transfer_proof, + state3, + } => match state3.expired_timelocks(bitcoin_wallet).await? { + ExpiredTimelocks::None => { + event_loop_handle + .send_transfer_proof(transfer_proof.clone()) + .await?; + + XmrLockTransferProofSent { + monero_wallet_restore_blockheight, + transfer_proof, + state3, + } + } + _ => AliceState::CancelTimelockExpired { + monero_wallet_restore_blockheight, + transfer_proof, + state3, + }, + }, + AliceState::XmrLockTransferProofSent { + monero_wallet_restore_blockheight, + transfer_proof, + state3, } => { let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; @@ -128,32 +150,36 @@ async fn next_state( ExpiredTimelocks::None => { select! { _ = tx_lock_status.wait_until_confirmed_with(state3.cancel_timelock) => { - AliceState::CancelTimelockExpired { - state3, - monero_wallet_restore_blockheight, + AliceState::CancelTimelockExpired { + monero_wallet_restore_blockheight, + transfer_proof, + state3, } } enc_sig = event_loop_handle.recv_encrypted_signature() => { tracing::info!("Received encrypted signature"); AliceState::EncSigLearned { - state3, - encrypted_signature: Box::new(enc_sig?), monero_wallet_restore_blockheight, + transfer_proof, + encrypted_signature: Box::new(enc_sig?), + state3, } } } } _ => AliceState::CancelTimelockExpired { - state3, monero_wallet_restore_blockheight, + transfer_proof, + state3, }, } } AliceState::EncSigLearned { - state3, - encrypted_signature, monero_wallet_restore_blockheight, + transfer_proof, + encrypted_signature, + state3, } => match state3.expired_timelocks(bitcoin_wallet).await? { ExpiredTimelocks::None => { let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; @@ -172,8 +198,9 @@ async fn next_state( .await?; AliceState::CancelTimelockExpired { - state3, monero_wallet_restore_blockheight, + transfer_proof, + state3, } } }, @@ -184,20 +211,23 @@ async fn next_state( .await?; AliceState::CancelTimelockExpired { - state3, monero_wallet_restore_blockheight, + transfer_proof, + state3, } } } } _ => AliceState::CancelTimelockExpired { - state3, monero_wallet_restore_blockheight, + transfer_proof, + state3, }, }, AliceState::CancelTimelockExpired { - state3, monero_wallet_restore_blockheight, + transfer_proof, + state3, } => { let transaction = state3.signed_cancel_transaction()?; @@ -219,13 +249,15 @@ async fn next_state( } AliceState::BtcCancelled { - state3, monero_wallet_restore_blockheight, + transfer_proof, + state3, } } AliceState::BtcCancelled { - state3, monero_wallet_restore_blockheight, + transfer_proof, + state3, } => { let tx_refund_status = bitcoin_wallet.subscribe_to(state3.tx_refund()).await; let tx_cancel_status = bitcoin_wallet.subscribe_to(state3.tx_cancel()).await; @@ -238,26 +270,35 @@ async fn next_state( let spend_key = state3.extract_monero_private_key(published_refund_tx)?; AliceState::BtcRefunded { + monero_wallet_restore_blockheight, + transfer_proof, spend_key, state3, - monero_wallet_restore_blockheight, } } _ = tx_cancel_status.wait_until_confirmed_with(state3.punish_timelock) => { AliceState::BtcPunishable { - state3, monero_wallet_restore_blockheight, + transfer_proof, + state3, } } } } AliceState::BtcRefunded { + monero_wallet_restore_blockheight, + transfer_proof, spend_key, state3, - monero_wallet_restore_blockheight, } => { let view_key = state3.v; + // Ensure that the XMR to be refunded are spendable by awaiting 10 confirmations + // on the lock transaction + monero_wallet + .watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof, 10)) + .await?; + monero_wallet .create_from(spend_key, view_key, monero_wallet_restore_blockheight) .await?; @@ -265,8 +306,9 @@ async fn next_state( AliceState::XmrRefunded } AliceState::BtcPunishable { - state3, monero_wallet_restore_blockheight, + transfer_proof, + state3, } => { let signed_tx_punish = state3.signed_punish_transaction()?; @@ -301,9 +343,10 @@ async fn next_state( let spend_key = state3.extract_monero_private_key(published_refund_tx)?; AliceState::BtcRefunded { + monero_wallet_restore_blockheight, + transfer_proof, spend_key, state3, - monero_wallet_restore_blockheight, } } } @@ -314,3 +357,13 @@ async fn next_state( AliceState::SafelyAborted => AliceState::SafelyAborted, }) } + +fn is_complete(state: &AliceState) -> bool { + matches!( + state, + AliceState::XmrRefunded + | AliceState::BtcRedeemed + | AliceState::BtcPunished + | AliceState::SafelyAborted + ) +} diff --git a/swap/src/protocol/bob/cancel.rs b/swap/src/protocol/bob/cancel.rs index ea9f2d9d..652abfa0 100644 --- a/swap/src/protocol/bob/cancel.rs +++ b/swap/src/protocol/bob/cancel.rs @@ -26,7 +26,14 @@ pub async fn cancel( BobState::XmrLocked(state4) => state4.cancel(), BobState::EncSigSent(state4) => state4.cancel(), BobState::CancelTimelockExpired(state6) => state6, - _ => bail!( + BobState::Started { .. } + | BobState::ExecutionSetupDone(_) + | BobState::BtcRedeemed(_) + | BobState::BtcCancelled(_) + | BobState::BtcRefunded(_) + | BobState::XmrRedeemed { .. } + | BobState::BtcPunished { .. } + | BobState::SafelyAborted => bail!( "Cannot cancel swap {} because it is in state {} which is not refundable.", swap_id, state diff --git a/swap/src/protocol/bob/refund.rs b/swap/src/protocol/bob/refund.rs index 4fa96778..cb00ce57 100644 --- a/swap/src/protocol/bob/refund.rs +++ b/swap/src/protocol/bob/refund.rs @@ -24,7 +24,13 @@ pub async fn refund( BobState::EncSigSent(state4) => state4.cancel(), BobState::CancelTimelockExpired(state6) => state6, BobState::BtcCancelled(state6) => state6, - _ => bail!( + BobState::Started { .. } + | BobState::ExecutionSetupDone(_) + | BobState::BtcRedeemed(_) + | BobState::BtcRefunded(_) + | BobState::XmrRedeemed { .. } + | BobState::BtcPunished { .. } + | BobState::SafelyAborted => bail!( "Cannot refund swap {} because it is in state {} which is not refundable.", swap_id, state diff --git a/swap/tests/alice_punishes_after_restart_punish_timelock_expired.rs b/swap/tests/alice_punishes_after_restart_punish_timelock_expired.rs new file mode 100644 index 00000000..5ede52a3 --- /dev/null +++ b/swap/tests/alice_punishes_after_restart_punish_timelock_expired.rs @@ -0,0 +1,62 @@ +pub mod harness; + +use harness::alice_run_until::is_xmr_lock_transaction_sent; +use harness::bob_run_until::is_btc_locked; +use harness::FastPunishConfig; +use swap::protocol::alice::AliceState; +use swap::protocol::bob::BobState; +use swap::protocol::{alice, bob}; + +/// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice +/// the encsig and fail to refund or redeem. Alice punishes. +#[tokio::test] +async fn alice_punishes_after_restart_if_punish_timelock_expired() { + harness::setup_test(FastPunishConfig, |mut ctx| async move { + let (bob_swap, bob_join_handle) = ctx.bob_swap().await; + let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); + + let alice_swap = ctx.alice_next_swap().await; + let alice_bitcoin_wallet = alice_swap.bitcoin_wallet.clone(); + + let alice_swap = tokio::spawn(alice::run_until(alice_swap, is_xmr_lock_transaction_sent)); + + let bob_state = bob_swap.await??; + assert!(matches!(bob_state, BobState::BtcLocked { .. })); + + let alice_state = alice_swap.await??; + + // Ensure punish timelock is expired + if let AliceState::XmrLockTransactionSent { state3, .. } = alice_state { + alice_bitcoin_wallet + .subscribe_to(state3.tx_lock) + .await + .wait_until_confirmed_with(state3.punish_timelock) + .await?; + } else { + panic!( + "\ + Alice in unexpected state {}", + alice_state + ); + } + + ctx.restart_alice().await; + let alice_swap = ctx.alice_next_swap().await; + let alice_swap = tokio::spawn(alice::run(alice_swap)); + + let alice_state = alice_swap.await??; + ctx.assert_alice_punished(alice_state).await; + + // Restart Bob after Alice punished to ensure Bob transitions to + // punished and does not run indefinitely + let (bob_swap, _) = ctx.stop_and_resume_bob_from_db(bob_join_handle).await; + assert!(matches!(bob_swap.state, BobState::BtcLocked { .. })); + + let bob_state = bob::run(bob_swap).await?; + + ctx.assert_bob_punished(bob_state).await; + + Ok(()) + }) + .await; +} diff --git a/swap/tests/alice_refunds_after_restart_bob_refunded.rs b/swap/tests/alice_refunds_after_restart_bob_refunded.rs new file mode 100644 index 00000000..0c341b7d --- /dev/null +++ b/swap/tests/alice_refunds_after_restart_bob_refunded.rs @@ -0,0 +1,38 @@ +pub mod harness; + +use harness::alice_run_until::is_xmr_lock_transaction_sent; +use harness::FastCancelConfig; +use swap::protocol::alice::AliceState; +use swap::protocol::{alice, bob}; + +/// Bob locks Btc and Alice locks Xmr. Alice does not act so Bob refunds. +/// Eventually Alice comes back online and refunds as well. +#[tokio::test] +async fn alice_refunds_after_restart_if_bob_already_refunded() { + harness::setup_test(FastCancelConfig, |mut ctx| async move { + let (bob_swap, _) = ctx.bob_swap().await; + let bob_swap = tokio::spawn(bob::run(bob_swap)); + + let alice_swap = ctx.alice_next_swap().await; + let alice_swap = tokio::spawn(alice::run_until(alice_swap, is_xmr_lock_transaction_sent)); + + let bob_state = bob_swap.await??; + ctx.assert_bob_refunded(bob_state).await; + + let alice_state = alice_swap.await??; + assert!(matches!( + alice_state, + AliceState::XmrLockTransactionSent { .. } + )); + + ctx.restart_alice().await; + let alice_swap = ctx.alice_next_swap().await; + let alice_swap = tokio::spawn(alice::run(alice_swap)); + + let alice_state = alice_swap.await??; + ctx.assert_alice_refunded(alice_state).await; + + Ok(()) + }) + .await; +} diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command.rs index f9a7a41e..a12d5724 100644 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command.rs +++ b/swap/tests/bob_refunds_using_cancel_and_refund_command.rs @@ -19,7 +19,7 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() { let (bob_swap, bob_join_handle) = ctx.stop_and_resume_bob_from_db(bob_join_handle).await; - // Ensure Bob's timelock is expired + // Ensure cancel timelock is expired if let BobState::BtcLocked(state3) = bob_swap.state.clone() { bob_swap .bitcoin_wallet diff --git a/swap/tests/happy_path_restart_alice_after_xmr_locked.rs b/swap/tests/happy_path_restart_alice_after_xmr_locked.rs new file mode 100644 index 00000000..97a6f04a --- /dev/null +++ b/swap/tests/happy_path_restart_alice_after_xmr_locked.rs @@ -0,0 +1,40 @@ +pub mod harness; + +use harness::alice_run_until::is_xmr_lock_transaction_sent; +use harness::SlowCancelConfig; +use swap::protocol::alice::AliceState; +use swap::protocol::{alice, bob}; + +#[tokio::test] +async fn given_alice_restarts_after_xmr_is_locked_resume_swap() { + harness::setup_test(SlowCancelConfig, |mut ctx| async move { + let (bob_swap, _) = ctx.bob_swap().await; + let bob_swap = tokio::spawn(bob::run(bob_swap)); + + let alice_swap = ctx.alice_next_swap().await; + let alice_swap = tokio::spawn(alice::run_until(alice_swap, is_xmr_lock_transaction_sent)); + + let alice_state = alice_swap.await??; + + assert!(matches!( + alice_state, + AliceState::XmrLockTransactionSent { .. } + )); + + ctx.restart_alice().await; + let alice_swap = ctx.alice_next_swap().await; + assert!(matches!( + alice_swap.state, + AliceState::XmrLockTransactionSent { .. } + )); + + let alice_state = alice::run(alice_swap).await?; + ctx.assert_alice_redeemed(alice_state).await; + + let bob_state = bob_swap.await??; + ctx.assert_bob_redeemed(bob_state).await; + + Ok(()) + }) + .await; +} diff --git a/swap/tests/harness/mod.rs b/swap/tests/harness/mod.rs index 8fafe985..ad0eb1e8 100644 --- a/swap/tests/harness/mod.rs +++ b/swap/tests/harness/mod.rs @@ -29,8 +29,9 @@ use tempfile::tempdir; use testcontainers::clients::Cli; use testcontainers::{Container, Docker, RunArgs}; use tokio::sync::mpsc; +use tokio::sync::mpsc::Receiver; use tokio::task::JoinHandle; -use tokio::time::interval; +use tokio::time::{interval, timeout}; use tracing_subscriber::util::SubscriberInitExt; use url::Url; use uuid::Uuid; @@ -79,24 +80,40 @@ impl BobParams { } } -pub struct BobEventLoopJoinHandle(JoinHandle<()>); +pub struct BobApplicationHandle(JoinHandle<()>); -impl BobEventLoopJoinHandle { +impl BobApplicationHandle { pub fn abort(&self) { self.0.abort() } } -pub struct AliceEventLoopJoinHandle(JoinHandle<()>); +pub struct AliceApplicationHandle { + handle: JoinHandle<()>, + peer_id: PeerId, +} + +impl AliceApplicationHandle { + pub fn abort(&self) { + self.handle.abort() + } +} pub struct TestContext { + env_config: Config, + btc_amount: bitcoin::Amount, xmr_amount: monero::Amount, + alice_seed: Seed, + alice_db_path: PathBuf, + alice_listen_address: Multiaddr, + alice_starting_balances: StartingBalances, alice_bitcoin_wallet: Arc, alice_monero_wallet: Arc, alice_swap_handle: mpsc::Receiver, + alice_handle: AliceApplicationHandle, bob_params: BobParams, bob_starting_balances: StartingBalances, @@ -105,11 +122,30 @@ pub struct TestContext { } impl TestContext { - pub async fn alice_next_swap(&mut self) -> alice::Swap { - self.alice_swap_handle.recv().await.unwrap() + pub async fn restart_alice(&mut self) { + self.alice_handle.abort(); + + let (alice_handle, alice_swap_handle) = start_alice( + &self.alice_seed, + self.alice_db_path.clone(), + self.alice_listen_address.clone(), + self.env_config, + self.alice_bitcoin_wallet.clone(), + self.alice_monero_wallet.clone(), + ); + + self.alice_handle = alice_handle; + self.alice_swap_handle = alice_swap_handle; } - pub async fn bob_swap(&mut self) -> (bob::Swap, BobEventLoopJoinHandle) { + pub async fn alice_next_swap(&mut self) -> alice::Swap { + timeout(Duration::from_secs(10), self.alice_swap_handle.recv()) + .await + .expect("No Alice swap within 10 seconds, aborting because this test is waiting for a swap forever...") + .unwrap() + } + + pub async fn bob_swap(&mut self) -> (bob::Swap, BobApplicationHandle) { let (event_loop, event_loop_handle) = self.bob_params.new_eventloop().unwrap(); let swap = self @@ -123,13 +159,13 @@ impl TestContext { let join_handle = tokio::spawn(event_loop.run()); - (swap, BobEventLoopJoinHandle(join_handle)) + (swap, BobApplicationHandle(join_handle)) } pub async fn stop_and_resume_bob_from_db( &mut self, - join_handle: BobEventLoopJoinHandle, - ) -> (bob::Swap, BobEventLoopJoinHandle) { + join_handle: BobApplicationHandle, + ) -> (bob::Swap, BobApplicationHandle) { join_handle.abort(); let (event_loop, event_loop_handle) = self.bob_params.new_eventloop().unwrap(); @@ -144,7 +180,7 @@ impl TestContext { let join_handle = tokio::spawn(event_loop.run()); - (swap, BobEventLoopJoinHandle(join_handle)) + (swap, BobApplicationHandle(join_handle)) } pub async fn assert_alice_redeemed(&mut self, state: AliceState) { @@ -462,20 +498,12 @@ where btc: bitcoin::Amount::ZERO, }; - let port = get_port().expect("Failed to find a free port"); - - let alice_listen_address: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", port) - .parse() - .expect("failed to parse Alice's address"); - let electrs_rpc_port = containers .electrs .get_host_port(harness::electrs::RPC_PORT) .expect("Could not map electrs rpc port"); let alice_seed = Seed::random().unwrap(); - let bob_seed = Seed::random().unwrap(); - let (alice_bitcoin_wallet, alice_monero_wallet) = init_test_wallets( MONERO_WALLET_NAME_ALICE, containers.bitcoind_url.clone(), @@ -483,16 +511,27 @@ where alice_starting_balances.clone(), tempdir().unwrap().path(), electrs_rpc_port, - alice_seed, + &alice_seed, env_config, ) .await; - let db_path = tempdir().unwrap(); - let alice_db = Arc::new(Database::open(db_path.path()).unwrap()); + let alice_listen_port = get_port().expect("Failed to find a free port"); + let alice_listen_address: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", alice_listen_port) + .parse() + .expect("failed to parse Alice's address"); - let alice_seed = Seed::random().unwrap(); + let alice_db_path = tempdir().unwrap().into_path(); + let (alice_handle, alice_swap_handle) = start_alice( + &alice_seed, + alice_db_path.clone(), + alice_listen_address.clone(), + env_config, + alice_bitcoin_wallet.clone(), + alice_monero_wallet.clone(), + ); + let bob_seed = Seed::random().unwrap(); let bob_starting_balances = StartingBalances { xmr: monero::Amount::ZERO, btc: btc_amount * 10, @@ -505,47 +544,34 @@ where bob_starting_balances.clone(), tempdir().unwrap().path(), electrs_rpc_port, - bob_seed, + &bob_seed, env_config, ) .await; - let mut alice_swarm = swarm::new::(&alice_seed).unwrap(); - Swarm::listen_on(&mut alice_swarm, alice_listen_address.clone()).unwrap(); - - let (alice_event_loop, alice_swap_handle) = alice::EventLoop::new( - alice_swarm, - env_config, - alice_bitcoin_wallet.clone(), - alice_monero_wallet.clone(), - alice_db, - FixedRate::default(), - bitcoin::Amount::ONE_BTC, - ) - .unwrap(); - - let alice_peer_id = alice_event_loop.peer_id(); - - tokio::spawn(alice_event_loop.run()); - let bob_params = BobParams { seed: Seed::random().unwrap(), db_path: tempdir().unwrap().path().to_path_buf(), swap_id: Uuid::new_v4(), bitcoin_wallet: bob_bitcoin_wallet.clone(), monero_wallet: bob_monero_wallet.clone(), - alice_address: alice_listen_address, - alice_peer_id, + alice_address: alice_listen_address.clone(), + alice_peer_id: alice_handle.peer_id, env_config, }; let test = TestContext { + env_config, btc_amount, xmr_amount, + alice_seed, + alice_db_path, + alice_listen_address, alice_starting_balances, alice_bitcoin_wallet, alice_monero_wallet, alice_swap_handle, + alice_handle, bob_params, bob_starting_balances, bob_bitcoin_wallet, @@ -555,6 +581,36 @@ where testfn(test).await.unwrap() } +fn start_alice( + seed: &Seed, + db_path: PathBuf, + listen_address: Multiaddr, + env_config: Config, + bitcoin_wallet: Arc, + monero_wallet: Arc, +) -> (AliceApplicationHandle, Receiver) { + let db = Arc::new(Database::open(db_path.as_path()).unwrap()); + + let mut swarm = swarm::new::(&seed).unwrap(); + Swarm::listen_on(&mut swarm, listen_address).unwrap(); + + let (event_loop, swap_handle) = alice::EventLoop::new( + swarm, + env_config, + bitcoin_wallet, + monero_wallet, + db, + FixedRate::default(), + bitcoin::Amount::ONE_BTC, + ) + .unwrap(); + + let peer_id = event_loop.peer_id(); + let handle = tokio::spawn(event_loop.run()); + + (AliceApplicationHandle { handle, peer_id }, swap_handle) +} + fn random_prefix() -> String { use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; @@ -714,7 +770,7 @@ async fn init_test_wallets( starting_balances: StartingBalances, datadir: &Path, electrum_rpc_port: u16, - seed: Seed, + seed: &Seed, env_config: Config, ) -> (Arc, Arc) { monero @@ -790,8 +846,8 @@ struct Containers<'a> { pub mod alice_run_until { use swap::protocol::alice::AliceState; - pub fn is_xmr_locked(state: &AliceState) -> bool { - matches!(state, AliceState::XmrLocked { .. }) + pub fn is_xmr_lock_transaction_sent(state: &AliceState) -> bool { + matches!(state, AliceState::XmrLockTransactionSent { .. }) } pub fn is_encsig_learned(state: &AliceState) -> bool { @@ -835,7 +891,7 @@ pub struct FastCancelConfig; impl GetConfig for FastCancelConfig { fn get_config() -> Config { Config { - bitcoin_cancel_timelock: CancelTimelock::new(1), + bitcoin_cancel_timelock: CancelTimelock::new(10), ..env::Regtest::get_config() } } @@ -846,8 +902,8 @@ pub struct FastPunishConfig; impl GetConfig for FastPunishConfig { fn get_config() -> Config { Config { - bitcoin_cancel_timelock: CancelTimelock::new(1), - bitcoin_punish_timelock: PunishTimelock::new(1), + bitcoin_cancel_timelock: CancelTimelock::new(10), + bitcoin_punish_timelock: PunishTimelock::new(10), ..env::Regtest::get_config() } }