147: Refactor prod code after test refactoring r=da-kami a=da-kami

Follow up of https://github.com/comit-network/xmr-btc-swap/pull/144

Took a bit more than expected, but this is really neat now!
The commits should be well-contained for reviewing, but 00835baa15 is quite big. 

I changed the abstraction on the way - out methods are finally named `run` and `run_until` which both take a swap - which makes way more sense I think :)

Also had to change the abstraction layers in `testutils` and introduced `Test` which specifies the swap amounts (that would usually come from the commandline and should not live in the factory as they are irrelevant for resumed swaps).

Co-authored-by: Daniel Karzel <daniel@comit.network>
pull/148/head
bors[bot] 4 years ago committed by GitHub
commit 05669c749c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -14,20 +14,16 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use crate::cli::{Command, Options, Resume}; use crate::cli::{Command, Options, Resume};
use anyhow::{bail, Context, Result}; use anyhow::{Context, Result};
use libp2p::{core::Multiaddr, PeerId};
use prettytable::{row, Table}; use prettytable::{row, Table};
use rand::rngs::OsRng;
use std::sync::Arc; use std::sync::Arc;
use structopt::StructOpt; use structopt::StructOpt;
use swap::{ use swap::{
bitcoin, bitcoin,
config::Config, config::Config,
database::{Database, Swap}, database::Database,
monero, network, monero,
network::transport::build, protocol::{alice, bob, bob::SwapFactory, StartingBalances},
protocol::{alice, alice::AliceState, bob, bob::BobState},
seed::Seed,
trace::init_tracing, trace::init_tracing,
SwapAmounts, SwapAmounts,
}; };
@ -51,8 +47,7 @@ async fn main() -> Result<()> {
opt.data_dir opt.data_dir
); );
let data_dir = std::path::Path::new(opt.data_dir.as_str()).to_path_buf(); let data_dir = std::path::Path::new(opt.data_dir.as_str()).to_path_buf();
let db = let db_path = data_dir.join("database");
Database::open(data_dir.join("database").as_path()).context("Could not open database")?;
let seed = swap::config::seed::Seed::from_file_or_generate(&data_dir) let seed = swap::config::seed::Seed::from_file_or_generate(&data_dir)
.expect("Could not retrieve/initialize seed") .expect("Could not retrieve/initialize seed")
@ -67,7 +62,12 @@ async fn main() -> Result<()> {
send_monero, send_monero,
receive_bitcoin, receive_bitcoin,
} => { } => {
let (bitcoin_wallet, monero_wallet) = setup_wallets( let swap_amounts = SwapAmounts {
xmr: send_monero,
btc: receive_bitcoin,
};
let (bitcoin_wallet, monero_wallet, starting_balances) = setup_wallets(
bitcoind_url, bitcoind_url,
bitcoin_wallet_name.as_str(), bitcoin_wallet_name.as_str(),
monero_wallet_rpc_url, monero_wallet_rpc_url,
@ -75,50 +75,28 @@ async fn main() -> Result<()> {
) )
.await?; .await?;
let amounts = SwapAmounts {
btc: receive_bitcoin,
xmr: send_monero,
};
let alice_state = {
let rng = &mut OsRng;
let a = bitcoin::SecretKey::new_random(rng);
let s_a = cross_curve_dleq::Scalar::random(rng);
let v_a = monero::PrivateViewKey::new_random(rng);
let redeem_address = bitcoin_wallet.as_ref().new_address().await?;
let punish_address = redeem_address.clone();
let state0 = alice::state::State0::new(
a,
s_a,
v_a,
amounts.btc,
amounts.xmr,
config.bitcoin_cancel_timelock,
config.bitcoin_punish_timelock,
redeem_address,
punish_address,
);
AliceState::Started { amounts, state0 }
};
let swap_id = Uuid::new_v4(); let swap_id = Uuid::new_v4();
info!( info!(
"Swap sending {} and receiving {} started with ID {}", "Swap sending {} and receiving {} started with ID {}",
send_monero, receive_bitcoin, swap_id send_monero, receive_bitcoin, swap_id
); );
alice_swap( let alice_factory = alice::SwapFactory::new(
seed,
config,
swap_id, swap_id,
alice_state,
listen_addr,
bitcoin_wallet, bitcoin_wallet,
monero_wallet, monero_wallet,
config, starting_balances,
db, db_path,
seed, listen_addr,
) )
.await?; .await;
let (swap, mut event_loop) = alice_factory.new_swap_as_alice(swap_amounts).await?;
tokio::spawn(async move { event_loop.run().await });
alice::run(swap).await?;
} }
Command::BuyXmr { Command::BuyXmr {
alice_peer_id, alice_peer_id,
@ -129,7 +107,12 @@ async fn main() -> Result<()> {
send_bitcoin, send_bitcoin,
receive_monero, receive_monero,
} => { } => {
let (bitcoin_wallet, monero_wallet) = setup_wallets( let swap_amounts = SwapAmounts {
btc: send_bitcoin,
xmr: receive_monero,
};
let (bitcoin_wallet, monero_wallet, starting_balances) = setup_wallets(
bitcoind_url, bitcoind_url,
bitcoin_wallet_name.as_str(), bitcoin_wallet_name.as_str(),
monero_wallet_rpc_url, monero_wallet_rpc_url,
@ -137,47 +120,36 @@ async fn main() -> Result<()> {
) )
.await?; .await?;
let refund_address = bitcoin_wallet.new_address().await?;
let state0 = bob::state::State0::new(
&mut OsRng,
send_bitcoin,
receive_monero,
config.bitcoin_cancel_timelock,
config.bitcoin_punish_timelock,
refund_address,
config.monero_finality_confirmations,
);
let amounts = SwapAmounts {
btc: send_bitcoin,
xmr: receive_monero,
};
let bob_state = BobState::Started { state0, amounts };
let swap_id = Uuid::new_v4(); let swap_id = Uuid::new_v4();
info!( info!(
"Swap sending {} and receiving {} started with ID {}", "Swap sending {} and receiving {} started with ID {}",
send_bitcoin, receive_monero, swap_id send_bitcoin, receive_monero, swap_id
); );
bob_swap( let bob_factory = SwapFactory::new(
seed,
db_path,
swap_id, swap_id,
bob_state,
bitcoin_wallet, bitcoin_wallet,
monero_wallet, monero_wallet,
db, config,
alice_peer_id, starting_balances,
alice_addr, alice_addr,
seed, alice_peer_id,
) );
.await?; let (swap, event_loop) = bob_factory.new_swap_as_bob(swap_amounts).await?;
tokio::spawn(async move { event_loop.run().await });
bob::run(swap).await?;
} }
Command::History => { Command::History => {
let mut table = Table::new(); let mut table = Table::new();
table.add_row(row!["SWAP ID", "STATE"]); table.add_row(row!["SWAP ID", "STATE"]);
let db = Database::open(db_path.as_path()).context("Could not open database")?;
for (swap_id, state) in db.all()? { for (swap_id, state) in db.all()? {
table.add_row(row![swap_id, state]); table.add_row(row![swap_id, state]);
} }
@ -192,30 +164,29 @@ async fn main() -> Result<()> {
monero_wallet_rpc_url, monero_wallet_rpc_url,
listen_addr, listen_addr,
}) => { }) => {
let db_state = if let Swap::Alice(db_state) = db.get_state(swap_id)? { let (bitcoin_wallet, monero_wallet, starting_balances) = setup_wallets(
db_state
} else {
bail!("Swap {} is not sell xmr.", swap_id)
};
let (bitcoin_wallet, monero_wallet) = setup_wallets(
bitcoind_url, bitcoind_url,
bitcoin_wallet_name.as_str(), bitcoin_wallet_name.as_str(),
monero_wallet_rpc_url, monero_wallet_rpc_url,
config, config,
) )
.await?; .await?;
alice_swap(
let alice_factory = alice::SwapFactory::new(
seed,
config,
swap_id, swap_id,
db_state.into(),
listen_addr,
bitcoin_wallet, bitcoin_wallet,
monero_wallet, monero_wallet,
config, starting_balances,
db, db_path,
seed, listen_addr,
) )
.await?; .await;
let (swap, mut event_loop) = alice_factory.recover_alice_from_db().await?;
tokio::spawn(async move { event_loop.run().await });
alice::run(swap).await?;
} }
Command::Resume(Resume::BuyXmr { Command::Resume(Resume::BuyXmr {
swap_id, swap_id,
@ -225,30 +196,29 @@ async fn main() -> Result<()> {
alice_peer_id, alice_peer_id,
alice_addr, alice_addr,
}) => { }) => {
let db_state = if let Swap::Bob(db_state) = db.get_state(swap_id)? { let (bitcoin_wallet, monero_wallet, starting_balances) = setup_wallets(
db_state
} else {
bail!("Swap {} is not buy xmr.", swap_id)
};
let (bitcoin_wallet, monero_wallet) = setup_wallets(
bitcoind_url, bitcoind_url,
bitcoin_wallet_name.as_str(), bitcoin_wallet_name.as_str(),
monero_wallet_rpc_url, monero_wallet_rpc_url,
config, config,
) )
.await?; .await?;
bob_swap(
let bob_factory = SwapFactory::new(
seed,
db_path,
swap_id, swap_id,
db_state.into(),
bitcoin_wallet, bitcoin_wallet,
monero_wallet, monero_wallet,
db, config,
alice_peer_id, starting_balances,
alice_addr, alice_addr,
seed, alice_peer_id,
) );
.await?; let (swap, event_loop) = bob_factory.recover_bob_from_db().await?;
tokio::spawn(async move { event_loop.run().await });
bob::run(swap).await?;
} }
}; };
@ -260,7 +230,11 @@ async fn setup_wallets(
bitcoin_wallet_name: &str, bitcoin_wallet_name: &str,
monero_wallet_rpc_url: url::Url, monero_wallet_rpc_url: url::Url,
config: Config, config: Config,
) -> Result<(Arc<swap::bitcoin::Wallet>, Arc<swap::monero::Wallet>)> { ) -> Result<(
Arc<swap::bitcoin::Wallet>,
Arc<swap::monero::Wallet>,
StartingBalances,
)> {
let bitcoin_wallet = let bitcoin_wallet =
swap::bitcoin::Wallet::new(bitcoin_wallet_name, bitcoind_url, config.bitcoin_network) swap::bitcoin::Wallet::new(bitcoin_wallet_name, bitcoind_url, config.bitcoin_network)
.await?; .await?;
@ -279,68 +253,10 @@ async fn setup_wallets(
); );
let monero_wallet = Arc::new(monero_wallet); let monero_wallet = Arc::new(monero_wallet);
Ok((bitcoin_wallet, monero_wallet)) let starting_balances = StartingBalances {
} btc: bitcoin_balance,
#[allow(clippy::too_many_arguments)] xmr: monero_balance,
async fn alice_swap( };
swap_id: Uuid,
state: AliceState,
listen_addr: Multiaddr,
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
monero_wallet: Arc<swap::monero::Wallet>,
config: Config,
db: Database,
seed: Seed,
) -> Result<AliceState> {
let alice_behaviour = alice::Behaviour::new(network::Seed::new(seed));
let alice_peer_id = alice_behaviour.peer_id();
info!("Own Peer-ID: {}", alice_peer_id);
let alice_transport = build(alice_behaviour.identity())?;
let (mut event_loop, handle) =
alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen_addr)?;
let swap = alice::swap::swap(
state,
handle,
bitcoin_wallet.clone(),
monero_wallet.clone(),
config,
swap_id,
db,
);
tokio::spawn(async move { event_loop.run().await });
swap.await
}
#[allow(clippy::too_many_arguments)]
async fn bob_swap(
swap_id: Uuid,
state: BobState,
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
monero_wallet: Arc<swap::monero::Wallet>,
db: Database,
alice_peer_id: PeerId,
alice_addr: Multiaddr,
seed: Seed,
) -> Result<BobState> {
let bob_behaviour = bob::Behaviour::new(network::Seed::new(seed));
let bob_transport = build(bob_behaviour.identity())?;
let (event_loop, handle) =
bob::event_loop::EventLoop::new(bob_transport, bob_behaviour, alice_peer_id, alice_addr)?;
let swap = bob::swap::swap(
state,
handle,
db,
bitcoin_wallet.clone(),
monero_wallet.clone(),
OsRng,
swap_id,
);
tokio::spawn(event_loop.run()); Ok((bitcoin_wallet, monero_wallet, starting_balances))
swap.await
} }

@ -1,2 +1,8 @@
pub mod alice; pub mod alice;
pub mod bob; pub mod bob;
#[derive(Debug, Clone)]
pub struct StartingBalances {
pub xmr: crate::monero::Amount,
pub btc: bitcoin::Amount,
}

@ -1,19 +1,15 @@
//! Run an XMR/BTC swap in the role of Alice. //! Run an XMR/BTC swap in the role of Alice.
//! Alice holds XMR and wishes receive BTC. //! Alice holds XMR and wishes receive BTC.
use anyhow::Result; use anyhow::{bail, Result};
use libp2p::{ use libp2p::{request_response::ResponseChannel, NetworkBehaviour, PeerId};
core::{identity::Keypair, Multiaddr},
request_response::ResponseChannel,
NetworkBehaviour, PeerId,
};
use tracing::{debug, info}; use tracing::{debug, info};
use crate::{ use crate::{
bitcoin, database, monero,
network::{ network::{
peer_tracker::{self, PeerTracker}, peer_tracker::{self, PeerTracker},
request_response::AliceToBob, request_response::AliceToBob,
transport::SwapTransport, Seed as NetworkSeed,
Seed, TokioExecutor,
}, },
protocol::bob, protocol::bob,
SwapAmounts, SwapAmounts,
@ -26,8 +22,16 @@ pub use self::{
message1::Message1, message1::Message1,
message2::Message2, message2::Message2,
state::*, state::*,
swap::{run_until, swap}, swap::{run, run_until},
};
use crate::{
config::Config, database::Database, network::transport::build, protocol::StartingBalances,
seed::Seed,
}; };
use libp2p::{core::Multiaddr, identity::Keypair};
use rand::rngs::OsRng;
use std::{path::PathBuf, sync::Arc};
use uuid::Uuid;
mod amounts; mod amounts;
pub mod event_loop; pub mod event_loop;
@ -39,29 +43,173 @@ pub mod state;
mod steps; mod steps;
pub mod swap; pub mod swap;
pub type Swarm = libp2p::Swarm<Behaviour>; pub struct Swap {
pub state: AliceState,
pub event_loop_handle: EventLoopHandle,
pub bitcoin_wallet: Arc<bitcoin::Wallet>,
pub monero_wallet: Arc<monero::Wallet>,
pub config: Config,
pub swap_id: Uuid,
pub db: Database,
}
pub fn new_swarm( pub struct SwapFactory {
listen: Multiaddr, swap_id: Uuid,
transport: SwapTransport, identity: Keypair,
behaviour: Behaviour, peer_id: PeerId,
) -> Result<Swarm> { db_path: PathBuf,
use anyhow::Context as _; config: Config,
listen_address: Multiaddr,
pub bitcoin_wallet: Arc<bitcoin::Wallet>,
pub monero_wallet: Arc<monero::Wallet>,
pub starting_balances: StartingBalances,
}
impl SwapFactory {
#[allow(clippy::too_many_arguments)]
pub async fn new(
seed: Seed,
config: Config,
swap_id: Uuid,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
starting_balances: StartingBalances,
db_path: PathBuf,
listen_address: Multiaddr,
) -> Self {
let network_seed = NetworkSeed::new(seed);
let identity = network_seed.derive_libp2p_identity();
let peer_id = PeerId::from(identity.public());
Self {
swap_id,
identity,
peer_id,
db_path,
config,
listen_address,
bitcoin_wallet,
monero_wallet,
starting_balances,
}
}
pub async fn new_swap_as_alice(&self, swap_amounts: SwapAmounts) -> Result<(Swap, EventLoop)> {
let initial_state = init_alice_state(
swap_amounts.btc,
swap_amounts.xmr,
self.bitcoin_wallet.clone(),
self.config,
)
.await?;
let (event_loop, event_loop_handle) = init_alice_event_loop(
self.listen_address.clone(),
self.identity.clone(),
self.peer_id.clone(),
)?;
let db = Database::open(self.db_path.as_path())?;
Ok((
Swap {
event_loop_handle,
bitcoin_wallet: self.bitcoin_wallet.clone(),
monero_wallet: self.monero_wallet.clone(),
config: self.config,
db,
state: initial_state,
swap_id: self.swap_id,
},
event_loop,
))
}
pub async fn recover_alice_from_db(&self) -> Result<(Swap, EventLoop)> {
// reopen the existing database
let db = Database::open(self.db_path.clone().as_path())?;
let resume_state = if let database::Swap::Alice(state) = db.get_state(self.swap_id)? {
state.into()
} else {
bail!(
"Trying to load swap with id {} for the wrong direction.",
self.swap_id
)
};
let (event_loop, event_loop_handle) = init_alice_event_loop(
self.listen_address.clone(),
self.identity.clone(),
self.peer_id.clone(),
)?;
let local_peer_id = behaviour.peer_id(); Ok((
Swap {
state: resume_state,
event_loop_handle,
bitcoin_wallet: self.bitcoin_wallet.clone(),
monero_wallet: self.monero_wallet.clone(),
config: self.config,
swap_id: self.swap_id,
db,
},
event_loop,
))
}
let mut swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone()) pub fn peer_id(&self) -> PeerId {
.executor(Box::new(TokioExecutor { self.peer_id.clone()
handle: tokio::runtime::Handle::current(), }
}))
.build();
Swarm::listen_on(&mut swarm, listen.clone()) pub fn listen_address(&self) -> Multiaddr {
.with_context(|| format!("Address is not supported: {:#}", listen))?; self.listen_address.clone()
}
}
tracing::info!("Initialized swarm: {}", local_peer_id); async fn init_alice_state(
btc_to_swap: bitcoin::Amount,
xmr_to_swap: monero::Amount,
alice_btc_wallet: Arc<bitcoin::Wallet>,
config: Config,
) -> Result<AliceState> {
let rng = &mut OsRng;
Ok(swarm) let amounts = SwapAmounts {
btc: btc_to_swap,
xmr: xmr_to_swap,
};
let a = bitcoin::SecretKey::new_random(rng);
let s_a = cross_curve_dleq::Scalar::random(rng);
let v_a = monero::PrivateViewKey::new_random(rng);
let redeem_address = alice_btc_wallet.as_ref().new_address().await?;
let punish_address = redeem_address.clone();
let state0 = State0::new(
a,
s_a,
v_a,
amounts.btc,
amounts.xmr,
config.bitcoin_cancel_timelock,
config.bitcoin_punish_timelock,
redeem_address,
punish_address,
);
Ok(AliceState::Started { amounts, state0 })
}
fn init_alice_event_loop(
listen: Multiaddr,
identity: Keypair,
peer_id: PeerId,
) -> Result<(EventLoop, EventLoopHandle)> {
let alice_behaviour = Behaviour::default();
let alice_transport = build(identity)?;
EventLoop::new(alice_transport, alice_behaviour, listen, peer_id)
} }
#[derive(Debug)] #[derive(Debug)]
@ -138,7 +286,7 @@ impl From<message3::OutEvent> for OutEvent {
} }
/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice. /// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice.
#[derive(NetworkBehaviour)] #[derive(NetworkBehaviour, Default)]
#[behaviour(out_event = "OutEvent", event_process = false)] #[behaviour(out_event = "OutEvent", event_process = false)]
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct Behaviour { pub struct Behaviour {
@ -148,33 +296,9 @@ pub struct Behaviour {
message1: message1::Behaviour, message1: message1::Behaviour,
message2: message2::Behaviour, message2: message2::Behaviour,
message3: message3::Behaviour, message3: message3::Behaviour,
#[behaviour(ignore)]
identity: Keypair,
} }
impl Behaviour { impl Behaviour {
pub fn new(seed: Seed) -> Self {
let identity = seed.derive_libp2p_identity();
Self {
pt: PeerTracker::default(),
amounts: Amounts::default(),
message0: message0::Behaviour::default(),
message1: message1::Behaviour::default(),
message2: message2::Behaviour::default(),
message3: message3::Behaviour::default(),
identity,
}
}
pub fn identity(&self) -> Keypair {
self.identity.clone()
}
pub fn peer_id(&self) -> PeerId {
PeerId::from(self.identity.public())
}
/// Alice always sends her messages as a response to a request from Bob. /// Alice always sends her messages as a response to a request from Bob.
pub fn send_amounts(&mut self, channel: ResponseChannel<AliceToBob>, amounts: SwapAmounts) { pub fn send_amounts(&mut self, channel: ResponseChannel<AliceToBob>, amounts: SwapAmounts) {
let msg = AliceToBob::Amounts(amounts); let msg = AliceToBob::Amounts(amounts);

@ -148,10 +148,9 @@ impl EventLoop {
transport: SwapTransport, transport: SwapTransport,
behaviour: Behaviour, behaviour: Behaviour,
listen: Multiaddr, listen: Multiaddr,
peer_id: PeerId,
) -> Result<(Self, EventLoopHandle)> { ) -> Result<(Self, EventLoopHandle)> {
let local_peer_id = behaviour.peer_id(); let mut swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, peer_id)
let mut swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id)
.executor(Box::new(TokioExecutor { .executor(Box::new(TokioExecutor {
handle: tokio::runtime::Handle::current(), handle: tokio::runtime::Handle::current(),
})) }))
@ -249,8 +248,4 @@ impl EventLoop {
} }
} }
} }
pub fn peer_id(&self) -> PeerId {
self.swarm.peer_id()
}
} }

@ -15,19 +15,24 @@ use crate::{
bitcoin, bitcoin,
bitcoin::{TransactionBlockHeight, WatchForRawTransaction}, bitcoin::{TransactionBlockHeight, WatchForRawTransaction},
config::Config, config::Config,
database::{Database, Swap}, database,
database::Database,
monero, monero,
monero::CreateWalletForOutput, monero::CreateWalletForOutput,
protocol::alice::{ protocol::{
alice,
alice::{
event_loop::EventLoopHandle, event_loop::EventLoopHandle,
steps::{ steps::{
build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction, build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction,
extract_monero_private_key, lock_xmr, negotiate, publish_bitcoin_punish_transaction, extract_monero_private_key, lock_xmr, negotiate,
publish_bitcoin_redeem_transaction, publish_cancel_transaction, publish_bitcoin_punish_transaction, publish_bitcoin_redeem_transaction,
wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin, publish_cancel_transaction, wait_for_bitcoin_encrypted_signature,
wait_for_bitcoin_refund, wait_for_locked_bitcoin,
}, },
AliceState, AliceState,
}, },
},
ExpiredTimelocks, ExpiredTimelocks,
}; };
@ -35,28 +40,6 @@ trait Rng: RngCore + CryptoRng + Send {}
impl<T> Rng for T where T: RngCore + CryptoRng + Send {} impl<T> Rng for T where T: RngCore + CryptoRng + Send {}
pub async fn swap(
state: AliceState,
event_loop_handle: EventLoopHandle,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
config: Config,
swap_id: Uuid,
db: Database,
) -> Result<AliceState> {
run_until(
state,
is_complete,
event_loop_handle,
bitcoin_wallet,
monero_wallet,
config,
swap_id,
db,
)
.await
}
pub fn is_complete(state: &AliceState) -> bool { pub fn is_complete(state: &AliceState) -> bool {
matches!( matches!(
state, state,
@ -81,10 +64,31 @@ pub fn is_encsig_learned(state: &AliceState) -> bool {
) )
} }
pub async fn run(swap: alice::Swap) -> Result<AliceState> {
run_until(swap, is_complete).await
}
pub async fn run_until(
swap: alice::Swap,
is_target_state: fn(&AliceState) -> bool,
) -> Result<AliceState> {
run_until_internal(
swap.state,
is_target_state,
swap.event_loop_handle,
swap.bitcoin_wallet,
swap.monero_wallet,
swap.config,
swap.swap_id,
swap.db,
)
.await
}
// State machine driver for swap execution // State machine driver for swap execution
#[async_recursion] #[async_recursion]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub async fn run_until( async fn run_until_internal(
state: AliceState, state: AliceState,
is_target_state: fn(&AliceState) -> bool, is_target_state: fn(&AliceState) -> bool,
mut event_loop_handle: EventLoopHandle, mut event_loop_handle: EventLoopHandle,
@ -110,9 +114,9 @@ pub async fn run_until(
}; };
let db_state = (&state).into(); let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state)) db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?; .await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,
@ -153,9 +157,9 @@ pub async fn run_until(
}; };
let db_state = (&state).into(); let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state)) db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?; .await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,
@ -194,9 +198,9 @@ pub async fn run_until(
}; };
let db_state = (&state).into(); let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state)) db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?; .await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,
@ -232,9 +236,9 @@ pub async fn run_until(
}; };
let db_state = (&state).into(); let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state)) db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?; .await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,
@ -270,9 +274,9 @@ pub async fn run_until(
let state = AliceState::CancelTimelockExpired { state3 }; let state = AliceState::CancelTimelockExpired { state3 };
let db_state = (&state).into(); let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state)) db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?; .await?;
return run_until( return run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,
@ -298,9 +302,9 @@ pub async fn run_until(
let state = AliceState::BtcRedeemed; let state = AliceState::BtcRedeemed;
let db_state = (&state).into(); let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state)) db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?; .await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,
@ -325,9 +329,9 @@ pub async fn run_until(
let state = AliceState::BtcCancelled { state3, tx_cancel }; let state = AliceState::BtcCancelled { state3, tx_cancel };
let db_state = (&state).into(); let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state)) db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?; .await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,
@ -358,10 +362,12 @@ pub async fn run_until(
None => { None => {
let state = AliceState::BtcPunishable { tx_refund, state3 }; let state = AliceState::BtcPunishable { tx_refund, state3 };
let db_state = (&state).into(); let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state)) db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?; .await?;
swap(
run_until_internal(
state, state,
is_target_state,
event_loop_handle, event_loop_handle,
bitcoin_wallet.clone(), bitcoin_wallet.clone(),
monero_wallet, monero_wallet,
@ -382,9 +388,9 @@ pub async fn run_until(
let state = AliceState::BtcRefunded { spend_key, state3 }; let state = AliceState::BtcRefunded { spend_key, state3 };
let db_state = (&state).into(); let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state)) db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?; .await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,
@ -407,7 +413,7 @@ pub async fn run_until(
let state = AliceState::XmrRefunded; let state = AliceState::XmrRefunded;
let db_state = (&state).into(); let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state)) db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?; .await?;
Ok(state) Ok(state)
} }
@ -437,9 +443,9 @@ pub async fn run_until(
Either::Left(_) => { Either::Left(_) => {
let state = AliceState::BtcPunished; let state = AliceState::BtcPunished;
let db_state = (&state).into(); let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state)) db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?; .await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,
@ -461,9 +467,9 @@ pub async fn run_until(
)?; )?;
let state = AliceState::BtcRefunded { spend_key, state3 }; let state = AliceState::BtcRefunded { spend_key, state3 };
let db_state = (&state).into(); let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state)) db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?; .await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,

@ -1,19 +1,14 @@
//! Run an XMR/BTC swap in the role of Bob. //! Run an XMR/BTC swap in the role of Bob.
//! Bob holds BTC and wishes receive XMR. //! Bob holds BTC and wishes receive XMR.
use anyhow::Result; use anyhow::{bail, Result};
use libp2p::{ use libp2p::{core::Multiaddr, NetworkBehaviour, PeerId};
core::{identity::Keypair, Multiaddr},
NetworkBehaviour, PeerId,
};
use tracing::{debug, info}; use tracing::{debug, info};
use crate::{ use crate::{
bitcoin,
bitcoin::EncryptedSignature, bitcoin::EncryptedSignature,
network::{ database, monero, network,
peer_tracker::{self, PeerTracker}, network::peer_tracker::{self, PeerTracker},
transport::SwapTransport,
Seed, TokioExecutor,
},
protocol::{alice, bob}, protocol::{alice, bob},
SwapAmounts, SwapAmounts,
}; };
@ -26,8 +21,16 @@ pub use self::{
message2::Message2, message2::Message2,
message3::Message3, message3::Message3,
state::*, state::*,
swap::{run_until, swap}, swap::{run, run_until},
};
use crate::{
config::Config, database::Database, network::transport::build, protocol::StartingBalances,
seed::Seed,
}; };
use libp2p::identity::Keypair;
use rand::rngs::OsRng;
use std::{path::PathBuf, sync::Arc};
use uuid::Uuid;
mod amounts; mod amounts;
pub mod event_loop; pub mod event_loop;
@ -38,20 +41,169 @@ mod message3;
pub mod state; pub mod state;
pub mod swap; pub mod swap;
pub type Swarm = libp2p::Swarm<Behaviour>; pub struct Swap {
pub state: BobState,
pub event_loop_handle: bob::EventLoopHandle,
pub db: Database,
pub bitcoin_wallet: Arc<bitcoin::Wallet>,
pub monero_wallet: Arc<monero::Wallet>,
pub swap_id: Uuid,
}
pub struct SwapFactory {
swap_id: Uuid,
identity: Keypair,
peer_id: PeerId,
db_path: PathBuf,
config: Config,
alice_connect_address: Multiaddr,
alice_connect_peer_id: PeerId,
pub bitcoin_wallet: Arc<bitcoin::Wallet>,
pub monero_wallet: Arc<monero::Wallet>,
pub starting_balances: StartingBalances,
}
impl SwapFactory {
#[allow(clippy::too_many_arguments)]
pub fn new(
seed: Seed,
db_path: PathBuf,
swap_id: Uuid,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
config: Config,
starting_balances: StartingBalances,
alice_connect_address: Multiaddr,
alice_connect_peer_id: PeerId,
) -> Self {
let identity = network::Seed::new(seed).derive_libp2p_identity();
let peer_id = identity.public().into_peer_id();
Self {
swap_id,
identity,
peer_id,
db_path,
config,
alice_connect_address,
alice_connect_peer_id,
bitcoin_wallet,
monero_wallet,
starting_balances,
}
}
pub async fn new_swap_as_bob(
&self,
swap_amounts: SwapAmounts,
) -> Result<(bob::Swap, bob::EventLoop)> {
let initial_state = init_bob_state(
swap_amounts.btc,
swap_amounts.xmr,
self.bitcoin_wallet.clone(),
self.config,
)
.await?;
let (event_loop, event_loop_handle) = init_bob_event_loop(
self.identity.clone(),
self.peer_id.clone(),
self.alice_connect_peer_id.clone(),
self.alice_connect_address.clone(),
)?;
let db = Database::open(self.db_path.as_path())?;
Ok((
Swap {
state: initial_state,
event_loop_handle,
db,
bitcoin_wallet: self.bitcoin_wallet.clone(),
monero_wallet: self.monero_wallet.clone(),
swap_id: self.swap_id,
},
event_loop,
))
}
pub async fn recover_bob_from_db(&self) -> Result<(bob::Swap, bob::EventLoop)> {
// reopen the existing database
let db = Database::open(self.db_path.clone().as_path())?;
let resume_state = if let database::Swap::Bob(state) = db.get_state(self.swap_id)? {
state.into()
} else {
bail!(
"Trying to load swap with id {} for the wrong direction.",
self.swap_id
)
};
let (event_loop, event_loop_handle) = init_bob_event_loop(
self.identity.clone(),
self.peer_id.clone(),
self.alice_connect_peer_id.clone(),
self.alice_connect_address.clone(),
)?;
pub fn new_swarm(transport: SwapTransport, behaviour: Behaviour) -> Result<Swarm> { Ok((
let local_peer_id = behaviour.peer_id(); Swap {
state: resume_state,
event_loop_handle,
db,
bitcoin_wallet: self.bitcoin_wallet.clone(),
monero_wallet: self.monero_wallet.clone(),
swap_id: self.swap_id,
},
event_loop,
))
}
}
let swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone()) async fn init_bob_state(
.executor(Box::new(TokioExecutor { btc_to_swap: bitcoin::Amount,
handle: tokio::runtime::Handle::current(), xmr_to_swap: monero::Amount,
})) bob_btc_wallet: Arc<bitcoin::Wallet>,
.build(); config: Config,
) -> Result<BobState> {
let amounts = SwapAmounts {
btc: btc_to_swap,
xmr: xmr_to_swap,
};
info!("Initialized swarm with identity {}", local_peer_id); let refund_address = bob_btc_wallet.new_address().await?;
let state0 = bob::State0::new(
&mut OsRng,
btc_to_swap,
xmr_to_swap,
config.bitcoin_cancel_timelock,
config.bitcoin_punish_timelock,
refund_address,
config.monero_finality_confirmations,
);
Ok(swarm) Ok(BobState::Started { state0, amounts })
}
fn init_bob_event_loop(
identity: Keypair,
peer_id: PeerId,
alice_peer_id: PeerId,
alice_addr: Multiaddr,
) -> Result<(bob::event_loop::EventLoop, bob::event_loop::EventLoopHandle)> {
let bob_behaviour = bob::Behaviour::default();
let bob_transport = build(identity)?;
bob::event_loop::EventLoop::new(
bob_transport,
bob_behaviour,
peer_id,
alice_peer_id,
alice_addr,
)
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -115,7 +267,7 @@ impl From<message3::OutEvent> for OutEvent {
} }
/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob. /// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob.
#[derive(NetworkBehaviour)] #[derive(NetworkBehaviour, Default)]
#[behaviour(out_event = "OutEvent", event_process = false)] #[behaviour(out_event = "OutEvent", event_process = false)]
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct Behaviour { pub struct Behaviour {
@ -125,33 +277,9 @@ pub struct Behaviour {
message1: message1::Behaviour, message1: message1::Behaviour,
message2: message2::Behaviour, message2: message2::Behaviour,
message3: message3::Behaviour, message3: message3::Behaviour,
#[behaviour(ignore)]
identity: Keypair,
} }
impl Behaviour { impl Behaviour {
pub fn new(seed: Seed) -> Self {
let identity = seed.derive_libp2p_identity();
Self {
pt: PeerTracker::default(),
amounts: Amounts::default(),
message0: message0::Behaviour::default(),
message1: message1::Behaviour::default(),
message2: message2::Behaviour::default(),
message3: message3::Behaviour::default(),
identity,
}
}
pub fn identity(&self) -> Keypair {
self.identity.clone()
}
pub fn peer_id(&self) -> PeerId {
PeerId::from(self.identity.public())
}
/// Sends a message to Alice to get current amounts based on `btc`. /// Sends a message to Alice to get current amounts based on `btc`.
pub fn request_amounts(&mut self, alice: PeerId, btc: u64) { pub fn request_amounts(&mut self, alice: PeerId, btc: u64) {
let btc = ::bitcoin::Amount::from_sat(btc); let btc = ::bitcoin::Amount::from_sat(btc);
@ -189,19 +317,3 @@ impl Behaviour {
self.pt.add_address(peer_id, address) self.pt.add_address(peer_id, address)
} }
} }
impl Default for Behaviour {
fn default() -> Behaviour {
let identity = Keypair::generate_ed25519();
Self {
pt: PeerTracker::default(),
amounts: Amounts::default(),
message0: message0::Behaviour::default(),
message1: message1::Behaviour::default(),
message2: message2::Behaviour::default(),
message3: message3::Behaviour::default(),
identity,
}
}
}

@ -131,12 +131,11 @@ impl EventLoop {
pub fn new( pub fn new(
transport: SwapTransport, transport: SwapTransport,
behaviour: Behaviour, behaviour: Behaviour,
peer_id: PeerId,
alice_peer_id: PeerId, alice_peer_id: PeerId,
alice_addr: Multiaddr, alice_addr: Multiaddr,
) -> Result<(Self, EventLoopHandle)> { ) -> Result<(Self, EventLoopHandle)> {
let local_peer_id = behaviour.peer_id(); let mut swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, peer_id)
let mut swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id)
.executor(Box::new(TokioExecutor { .executor(Box::new(TokioExecutor {
handle: tokio::runtime::Handle::current(), handle: tokio::runtime::Handle::current(),
})) }))

@ -1,6 +1,6 @@
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use async_recursion::async_recursion; use async_recursion::async_recursion;
use rand::{CryptoRng, RngCore}; use rand::{rngs::OsRng, CryptoRng, RngCore};
use std::sync::Arc; use std::sync::Arc;
use tokio::select; use tokio::select;
use tracing::{debug, info}; use tracing::{debug, info};
@ -14,33 +14,6 @@ use crate::{
ExpiredTimelocks, SwapAmounts, ExpiredTimelocks, SwapAmounts,
}; };
// TODO(Franck): Make this a method on a struct
#[allow(clippy::too_many_arguments)]
pub async fn swap<R>(
state: BobState,
event_loop_handle: EventLoopHandle,
db: Database,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
rng: R,
swap_id: Uuid,
) -> Result<BobState>
where
R: RngCore + CryptoRng + Send,
{
run_until(
state,
is_complete,
event_loop_handle,
db,
bitcoin_wallet,
monero_wallet,
rng,
swap_id,
)
.await
}
pub fn is_complete(state: &BobState) -> bool { pub fn is_complete(state: &BobState) -> bool {
matches!( matches!(
state, state,
@ -63,10 +36,32 @@ pub fn is_encsig_sent(state: &BobState) -> bool {
matches!(state, BobState::EncSigSent(..)) matches!(state, BobState::EncSigSent(..))
} }
#[allow(clippy::too_many_arguments)]
pub async fn run(swap: bob::Swap) -> Result<BobState> {
run_until(swap, is_complete).await
}
pub async fn run_until(
swap: bob::Swap,
is_target_state: fn(&BobState) -> bool,
) -> Result<BobState> {
run_until_internal(
swap.state,
is_target_state,
swap.event_loop_handle,
swap.db,
swap.bitcoin_wallet,
swap.monero_wallet,
OsRng,
swap.swap_id,
)
.await
}
// State machine driver for swap execution // State machine driver for swap execution
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[async_recursion] #[async_recursion]
pub async fn run_until<R>( async fn run_until_internal<R>(
state: BobState, state: BobState,
is_target_state: fn(&BobState) -> bool, is_target_state: fn(&BobState) -> bool,
mut event_loop_handle: EventLoopHandle, mut event_loop_handle: EventLoopHandle,
@ -99,7 +94,7 @@ where
let state = BobState::Negotiated(state2); let state = BobState::Negotiated(state2);
let db_state = state.clone().into(); let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,
@ -120,7 +115,7 @@ where
let state = BobState::BtcLocked(state3); let state = BobState::BtcLocked(state3);
let db_state = state.clone().into(); let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,
@ -185,7 +180,7 @@ where
}; };
let db_state = state.clone().into(); let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,
@ -226,7 +221,7 @@ where
}; };
let db_state = state.clone().into(); let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,
@ -261,7 +256,7 @@ where
let db_state = state.clone().into(); let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,
@ -282,7 +277,7 @@ where
}; };
let db_state = state.clone().into(); let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,
@ -307,7 +302,7 @@ where
db.insert_latest_state(swap_id, Swap::Bob(state.clone().into())) db.insert_latest_state(swap_id, Swap::Bob(state.clone().into()))
.await?; .await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,
@ -336,7 +331,7 @@ where
let db_state = state.clone().into(); let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until( run_until_internal(
state, state,
is_target_state, is_target_state,
event_loop_handle, event_loop_handle,

@ -1,4 +1,3 @@
use rand::rngs::OsRng;
use swap::protocol::{alice, bob}; use swap::protocol::{alice, bob};
use tokio::join; use tokio::join;
@ -8,33 +7,17 @@ pub mod testutils;
#[tokio::test] #[tokio::test]
async fn happy_path() { async fn happy_path() {
testutils::test(|alice_harness, bob_harness| async move { testutils::setup_test(|ctx| async move {
let alice = alice_harness.new_alice().await; let alice_swap = ctx.new_swap_as_alice().await;
let bob = bob_harness.new_bob().await; let bob_swap = ctx.new_swap_as_bob().await;
let alice_swap = alice::swap( let alice = alice::run(alice_swap);
alice.state,
alice.event_loop_handle,
alice.bitcoin_wallet.clone(),
alice.monero_wallet.clone(),
alice.config,
alice.swap_id,
alice.db,
);
let bob_swap = bob::swap( let bob = bob::run(bob_swap);
bob.state, let (alice_state, bob_state) = join!(alice, bob);
bob.event_loop_handle,
bob.db,
bob.bitcoin_wallet.clone(),
bob.monero_wallet.clone(),
OsRng,
bob.swap_id,
);
let (alice_state, bob_state) = join!(alice_swap, bob_swap);
alice_harness.assert_redeemed(alice_state.unwrap()).await; ctx.assert_alice_redeemed(alice_state.unwrap()).await;
bob_harness.assert_redeemed(bob_state.unwrap()).await; ctx.assert_bob_redeemed(bob_state.unwrap()).await;
}) })
.await; .await;
} }

@ -1,58 +1,30 @@
use rand::rngs::OsRng;
use swap::protocol::{alice, alice::AliceState, bob}; use swap::protocol::{alice, alice::AliceState, bob};
pub mod testutils; pub mod testutils;
#[tokio::test] #[tokio::test]
async fn given_alice_restarts_after_encsig_is_learned_resume_swap() { async fn given_alice_restarts_after_encsig_is_learned_resume_swap() {
testutils::test(|alice_harness, bob_harness| async move { testutils::setup_test(|ctx| async move {
let alice = alice_harness.new_alice().await; let alice_swap = ctx.new_swap_as_alice().await;
let bob = bob_harness.new_bob().await; let bob_swap = ctx.new_swap_as_bob().await;
let bob_swap = bob::swap( let bob = bob::run(bob_swap);
bob.state, let bob_handle = tokio::spawn(bob);
bob.event_loop_handle,
bob.db, let alice_state = alice::run_until(alice_swap, alice::swap::is_encsig_learned)
bob.bitcoin_wallet.clone(),
bob.monero_wallet.clone(),
OsRng,
bob.swap_id,
);
let bob_swap_handle = tokio::spawn(bob_swap);
let alice_state = alice::run_until(
alice.state,
alice::swap::is_encsig_learned,
alice.event_loop_handle,
alice.bitcoin_wallet.clone(),
alice.monero_wallet.clone(),
alice.config,
alice.swap_id,
alice.db,
)
.await .await
.unwrap(); .unwrap();
assert!(matches!(alice_state, AliceState::EncSigLearned {..})); assert!(matches!(alice_state, AliceState::EncSigLearned {..}));
let alice = alice_harness.recover_alice_from_db().await; let alice_swap = ctx.recover_alice_from_db().await;
assert!(matches!(alice.state, AliceState::EncSigLearned {..})); assert!(matches!(alice_swap.state, AliceState::EncSigLearned {..}));
let alice_state = alice::swap( let alice_state = alice::run(alice_swap).await.unwrap();
alice.state,
alice.event_loop_handle,
alice.bitcoin_wallet.clone(),
alice.monero_wallet.clone(),
alice.config,
alice.swap_id,
alice.db,
)
.await
.unwrap();
alice_harness.assert_redeemed(alice_state).await; ctx.assert_alice_redeemed(alice_state).await;
let bob_state = bob_swap_handle.await.unwrap(); let bob_state = bob_handle.await.unwrap();
bob_harness.assert_redeemed(bob_state.unwrap()).await ctx.assert_bob_redeemed(bob_state.unwrap()).await
}) })
.await; .await;
} }

@ -1,59 +1,31 @@
use rand::rngs::OsRng;
use swap::protocol::{alice, bob, bob::BobState}; use swap::protocol::{alice, bob, bob::BobState};
pub mod testutils; pub mod testutils;
#[tokio::test] #[tokio::test]
async fn given_bob_restarts_after_encsig_is_sent_resume_swap() { async fn given_bob_restarts_after_encsig_is_sent_resume_swap() {
testutils::test(|alice_harness, bob_harness| async move { testutils::setup_test(|ctx| async move {
let alice = alice_harness.new_alice().await; let alice_swap = ctx.new_swap_as_alice().await;
let bob = bob_harness.new_bob().await; let bob_swap = ctx.new_swap_as_bob().await;
let alice_swap = alice::swap( let alice = alice::run(alice_swap);
alice.state, let alice_handle = tokio::spawn(alice);
alice.event_loop_handle,
alice.bitcoin_wallet.clone(), let bob_state = bob::run_until(bob_swap, bob::swap::is_encsig_sent)
alice.monero_wallet.clone(),
alice.config,
alice.swap_id,
alice.db,
);
let alice_swap_handle = tokio::spawn(alice_swap);
let bob_state = bob::run_until(
bob.state,
bob::swap::is_encsig_sent,
bob.event_loop_handle,
bob.db,
bob.bitcoin_wallet.clone(),
bob.monero_wallet.clone(),
OsRng,
bob.swap_id,
)
.await .await
.unwrap(); .unwrap();
assert!(matches!(bob_state, BobState::EncSigSent {..})); assert!(matches!(bob_state, BobState::EncSigSent {..}));
let bob = bob_harness.recover_bob_from_db().await; let bob_swap = ctx.recover_bob_from_db().await;
assert!(matches!(bob.state, BobState::EncSigSent {..})); assert!(matches!(bob_swap.state, BobState::EncSigSent {..}));
let bob_state = bob::swap( let bob_state = bob::run(bob_swap).await.unwrap();
bob.state,
bob.event_loop_handle,
bob.db,
bob.bitcoin_wallet.clone(),
bob.monero_wallet.clone(),
OsRng,
bob.swap_id,
)
.await
.unwrap();
bob_harness.assert_redeemed(bob_state).await; ctx.assert_bob_redeemed(bob_state).await;
let alice_state = alice_swap_handle.await.unwrap(); let alice_state = alice_handle.await.unwrap();
alice_harness.assert_redeemed(alice_state.unwrap()).await; ctx.assert_alice_redeemed(alice_state.unwrap()).await;
}) })
.await; .await;
} }

@ -1,59 +1,32 @@
use rand::rngs::OsRng; use swap::protocol::{
use swap::protocol::{alice, bob, bob::BobState}; alice, bob,
bob::{swap::is_xmr_locked, BobState},
};
pub mod testutils; pub mod testutils;
#[tokio::test] #[tokio::test]
async fn given_bob_restarts_after_xmr_is_locked_resume_swap() { async fn given_bob_restarts_after_xmr_is_locked_resume_swap() {
testutils::test(|alice_harness, bob_harness| async move { testutils::setup_test(|ctx| async move {
let alice = alice_harness.new_alice().await; let alice_swap = ctx.new_swap_as_alice().await;
let bob = bob_harness.new_bob().await; let bob_swap = ctx.new_swap_as_bob().await;
let alice_swap = alice::swap( let alice_handle = alice::run(alice_swap);
alice.state, let alice_swap_handle = tokio::spawn(alice_handle);
alice.event_loop_handle,
alice.bitcoin_wallet.clone(), let bob_state = bob::run_until(bob_swap, is_xmr_locked).await.unwrap();
alice.monero_wallet.clone(),
alice.config,
alice.swap_id,
alice.db,
);
let alice_swap_handle = tokio::spawn(alice_swap);
let bob_state = bob::run_until(
bob.state,
bob::swap::is_xmr_locked,
bob.event_loop_handle,
bob.db,
bob.bitcoin_wallet.clone(),
bob.monero_wallet.clone(),
OsRng,
bob.swap_id,
)
.await
.unwrap();
assert!(matches!(bob_state, BobState::XmrLocked {..})); assert!(matches!(bob_state, BobState::XmrLocked {..}));
let bob = bob_harness.recover_bob_from_db().await; let bob_swap = ctx.recover_bob_from_db().await;
assert!(matches!(bob.state, BobState::XmrLocked {..})); assert!(matches!(bob_swap.state, BobState::XmrLocked {..}));
let bob_state = bob::swap( let bob_state = bob::run(bob_swap).await.unwrap();
bob.state,
bob.event_loop_handle,
bob.db,
bob.bitcoin_wallet.clone(),
bob.monero_wallet.clone(),
OsRng,
bob.swap_id,
)
.await
.unwrap();
bob_harness.assert_redeemed(bob_state).await; ctx.assert_bob_redeemed(bob_state).await;
let alice_state = alice_swap_handle.await.unwrap(); let alice_state = alice_swap_handle.await.unwrap();
alice_harness.assert_redeemed(alice_state.unwrap()).await; ctx.assert_alice_redeemed(alice_state.unwrap()).await;
}) })
.await; .await;
} }

@ -1,5 +1,7 @@
use rand::rngs::OsRng; use swap::protocol::{
use swap::protocol::{alice, bob, bob::BobState}; alice, bob,
bob::{swap::is_btc_locked, BobState},
};
pub mod testutils; pub mod testutils;
@ -7,57 +9,28 @@ pub mod testutils;
/// the encsig and fail to refund or redeem. Alice punishes. /// the encsig and fail to refund or redeem. Alice punishes.
#[tokio::test] #[tokio::test]
async fn alice_punishes_if_bob_never_acts_after_fund() { async fn alice_punishes_if_bob_never_acts_after_fund() {
testutils::test(|alice_harness, bob_harness| async move { testutils::setup_test(|ctx| async move {
let alice = alice_harness.new_alice().await; let alice_swap = ctx.new_swap_as_alice().await;
let bob = bob_harness.new_bob().await; let bob_swap = ctx.new_swap_as_bob().await;
let alice_swap = alice::swap( let alice = alice::run(alice_swap);
alice.state, let alice_handle = tokio::spawn(alice);
alice.event_loop_handle,
alice.bitcoin_wallet.clone(), let bob_state = bob::run_until(bob_swap, is_btc_locked).await.unwrap();
alice.monero_wallet.clone(),
alice.config,
alice.swap_id,
alice.db,
);
let alice_swap_handle = tokio::spawn(alice_swap);
let bob_state = bob::run_until(
bob.state,
bob::swap::is_btc_locked,
bob.event_loop_handle,
bob.db,
bob.bitcoin_wallet.clone(),
bob.monero_wallet.clone(),
OsRng,
bob.swap_id,
)
.await
.unwrap();
assert!(matches!(bob_state, BobState::BtcLocked {..})); assert!(matches!(bob_state, BobState::BtcLocked {..}));
let alice_state = alice_swap_handle.await.unwrap(); let alice_state = alice_handle.await.unwrap();
alice_harness.assert_punished(alice_state.unwrap()).await; ctx.assert_alice_punished(alice_state.unwrap()).await;
// Restart Bob after Alice punished to ensure Bob transitions to // Restart Bob after Alice punished to ensure Bob transitions to
// punished and does not run indefinitely // punished and does not run indefinitely
let bob = bob_harness.recover_bob_from_db().await; let bob_swap = ctx.recover_bob_from_db().await;
assert!(matches!(bob.state, BobState::BtcLocked {..})); assert!(matches!(bob_swap.state, BobState::BtcLocked {..}));
let bob_state = bob::swap( let bob_state = bob::run(bob_swap).await.unwrap();
bob.state,
bob.event_loop_handle, ctx.assert_bob_punished(bob_state).await;
bob.db,
bob.bitcoin_wallet.clone(),
bob.monero_wallet.clone(),
OsRng,
bob.swap_id,
)
.await
.unwrap();
bob_harness.assert_punished(bob_state).await;
}) })
.await; .await;
} }

@ -1,4 +1,3 @@
use rand::rngs::OsRng;
use swap::protocol::{alice, alice::AliceState, bob}; use swap::protocol::{alice, alice::AliceState, bob};
pub mod testutils; pub mod testutils;
@ -7,60 +6,33 @@ pub mod testutils;
/// then also refunds. /// then also refunds.
#[tokio::test] #[tokio::test]
async fn given_alice_restarts_after_xmr_is_locked_abort_swap() { async fn given_alice_restarts_after_xmr_is_locked_abort_swap() {
testutils::test(|alice_harness, bob_harness| async move { testutils::setup_test(|ctx| async move {
let alice = alice_harness.new_alice().await; let alice_swap = ctx.new_swap_as_alice().await;
let bob = bob_harness.new_bob().await; let bob_swap = ctx.new_swap_as_bob().await;
let bob_swap = bob::swap( let bob = bob::run(bob_swap);
bob.state, let bob_handle = tokio::spawn(bob);
bob.event_loop_handle,
bob.db,
bob.bitcoin_wallet.clone(),
bob.monero_wallet.clone(),
OsRng,
bob.swap_id,
);
let bob_swap_handle = tokio::spawn(bob_swap);
let alice_state = alice::run_until( let alice_state = alice::run_until(alice_swap, alice::swap::is_xmr_locked)
alice.state,
alice::swap::is_xmr_locked,
alice.event_loop_handle,
alice.bitcoin_wallet.clone(),
alice.monero_wallet.clone(),
alice.config,
alice.swap_id,
alice.db,
)
.await .await
.unwrap(); .unwrap();
assert!(matches!(alice_state, AliceState::XmrLocked {..})); assert!(matches!(alice_state, AliceState::XmrLocked {..}));
// Alice does not act, Bob refunds // Alice does not act, Bob refunds
let bob_state = bob_swap_handle.await.unwrap(); let bob_state = bob_handle.await.unwrap();
// Once bob has finished Alice is restarted and refunds as well // Once bob has finished Alice is restarted and refunds as well
let alice = alice_harness.recover_alice_from_db().await; let alice_swap = ctx.recover_alice_from_db().await;
assert!(matches!(alice.state, AliceState::XmrLocked {..})); assert!(matches!(alice_swap.state, AliceState::XmrLocked {..}));
let alice_state = alice::swap( let alice_state = alice::run(alice_swap).await.unwrap();
alice.state,
alice.event_loop_handle,
alice.bitcoin_wallet.clone(),
alice.monero_wallet.clone(),
alice.config,
alice.swap_id,
alice.db,
)
.await
.unwrap();
// TODO: The test passes like this, but the assertion should be done after Bob // TODO: The test passes like this, but the assertion should be done after Bob
// refunded, not at the end because this can cause side-effects! // refunded, not at the end because this can cause side-effects!
// We have to properly wait for the refund tx's finality inside the assertion, // We have to properly wait for the refund tx's finality inside the assertion,
// which requires storing the refund_tx_id in the the state! // which requires storing the refund_tx_id in the the state!
bob_harness.assert_refunded(bob_state.unwrap()).await; ctx.assert_bob_refunded(bob_state.unwrap()).await;
alice_harness.assert_refunded(alice_state).await; ctx.assert_alice_refunded(alice_state).await;
}) })
.await; .await;
} }

@ -2,17 +2,14 @@ use crate::testutils;
use bitcoin_harness::Bitcoind; use bitcoin_harness::Bitcoind;
use futures::Future; use futures::Future;
use get_port::get_port; use get_port::get_port;
use libp2p::{core::Multiaddr, PeerId}; use libp2p::core::Multiaddr;
use monero_harness::{image, Monero}; use monero_harness::{image, Monero};
use rand::rngs::OsRng; use std::sync::Arc;
use std::{path::PathBuf, sync::Arc};
use swap::{ use swap::{
bitcoin, bitcoin,
config::Config, config::Config,
database::Database, monero,
monero, network, protocol::{alice, alice::AliceState, bob, bob::BobState, StartingBalances},
network::transport::build,
protocol::{alice, alice::AliceState, bob, bob::BobState},
seed::Seed, seed::Seed,
SwapAmounts, SwapAmounts,
}; };
@ -22,347 +19,150 @@ use tracing_core::dispatcher::DefaultGuard;
use tracing_log::LogTracer; use tracing_log::LogTracer;
use uuid::Uuid; use uuid::Uuid;
pub async fn test<T, F>(testfn: T) pub struct TestContext {
where swap_amounts: SwapAmounts,
T: Fn(AliceHarness, BobHarness) -> F,
F: Future<Output = ()>,
{
let cli = Cli::default();
let _guard = init_tracing();
let (monero, containers) = testutils::init_containers(&cli).await;
let swap_amounts = SwapAmounts {
btc: bitcoin::Amount::from_sat(1_000_000),
xmr: monero::Amount::from_piconero(1_000_000_000_000),
};
let config = Config::regtest();
let alice_starting_balances = StartingBalances {
xmr: swap_amounts.xmr * 10,
btc: bitcoin::Amount::ZERO,
};
let alice_harness = AliceHarness::new(
config,
swap_amounts,
Uuid::new_v4(),
&monero,
&containers.bitcoind,
alice_starting_balances,
)
.await;
let bob_starting_balances = StartingBalances {
xmr: monero::Amount::ZERO,
btc: swap_amounts.btc * 10,
};
let bob_harness = BobHarness::new(
config,
swap_amounts,
Uuid::new_v4(),
&monero,
&containers.bitcoind,
bob_starting_balances,
alice_harness.listen_address(),
alice_harness.peer_id(),
)
.await;
testfn(alice_harness, bob_harness).await
}
pub struct Alice { alice_swap_factory: alice::SwapFactory,
pub state: AliceState, bob_swap_factory: bob::SwapFactory,
pub event_loop_handle: alice::EventLoopHandle,
pub bitcoin_wallet: Arc<bitcoin::Wallet>,
pub monero_wallet: Arc<monero::Wallet>,
pub config: Config,
pub swap_id: Uuid,
pub db: Database,
} }
pub struct AliceHarness { impl TestContext {
listen_address: Multiaddr, pub async fn new_swap_as_alice(&self) -> alice::Swap {
peer_id: PeerId, let (swap, mut event_loop) = self
.alice_swap_factory
.new_swap_as_alice(self.swap_amounts)
.await
.unwrap();
seed: Seed, tokio::spawn(async move { event_loop.run().await });
db_path: PathBuf,
swap_id: Uuid,
swap_amounts: SwapAmounts, swap
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
config: Config,
starting_balances: StartingBalances,
} }
impl AliceHarness { pub async fn new_swap_as_bob(&self) -> bob::Swap {
async fn new( let (swap, event_loop) = self
config: Config, .bob_swap_factory
swap_amounts: SwapAmounts, .new_swap_as_bob(self.swap_amounts)
swap_id: Uuid, .await
monero: &Monero, .unwrap();
bitcoind: &Bitcoind<'_>,
starting_balances: StartingBalances,
) -> Self {
let port = get_port().expect("Failed to find a free port");
let listen_address: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", port)
.parse()
.expect("failed to parse Alice's address");
let seed = Seed::random().unwrap();
let db_path = tempdir().unwrap().path().to_path_buf();
let (bitcoin_wallet, monero_wallet) =
init_wallets("alice", bitcoind, monero, starting_balances.clone(), config).await;
// TODO: This should be done by changing the production code tokio::spawn(async move { event_loop.run().await });
let network_seed = network::Seed::new(seed);
let identity = network_seed.derive_libp2p_identity();
let peer_id = PeerId::from(identity.public());
Self { swap
seed,
db_path,
listen_address,
peer_id,
swap_id,
swap_amounts,
bitcoin_wallet,
monero_wallet,
config,
starting_balances,
} }
}
pub async fn new_alice(&self) -> Alice {
let initial_state = init_alice_state(
self.swap_amounts.btc,
self.swap_amounts.xmr,
self.bitcoin_wallet.clone(),
self.config,
)
.await;
let (mut event_loop, event_loop_handle) = pub async fn recover_alice_from_db(&self) -> alice::Swap {
init_alice_event_loop(self.listen_address.clone(), self.seed); let (swap, mut event_loop) = self
.alice_swap_factory
.recover_alice_from_db()
.await
.unwrap();
tokio::spawn(async move { event_loop.run().await }); tokio::spawn(async move { event_loop.run().await });
let db = Database::open(self.db_path.as_path()).unwrap(); swap
Alice {
event_loop_handle,
bitcoin_wallet: self.bitcoin_wallet.clone(),
monero_wallet: self.monero_wallet.clone(),
config: self.config,
db,
state: initial_state,
swap_id: self.swap_id,
}
} }
pub async fn recover_alice_from_db(&self) -> Alice { pub async fn recover_bob_from_db(&self) -> bob::Swap {
// TODO: "simulated restart" issues: let (swap, event_loop) = self.bob_swap_factory.recover_bob_from_db().await.unwrap();
// - create new wallets instead of reusing (hard because of container
// lifetimes)
// - consider aborting the old event loop (currently just keeps running)
// reopen the existing database
let db = Database::open(self.db_path.clone().as_path()).unwrap();
let resume_state =
if let swap::database::Swap::Alice(state) = db.get_state(self.swap_id).unwrap() {
state.into()
} else {
unreachable!()
};
let (mut event_loop, event_loop_handle) =
init_alice_event_loop(self.listen_address.clone(), self.seed);
tokio::spawn(async move { event_loop.run().await }); tokio::spawn(async move { event_loop.run().await });
Alice { swap
state: resume_state,
event_loop_handle,
bitcoin_wallet: self.bitcoin_wallet.clone(),
monero_wallet: self.monero_wallet.clone(),
config: self.config,
swap_id: self.swap_id,
db,
}
} }
pub async fn assert_redeemed(&self, state: AliceState) { pub async fn assert_alice_redeemed(&self, state: AliceState) {
assert!(matches!(state, AliceState::BtcRedeemed)); assert!(matches!(state, AliceState::BtcRedeemed));
let btc_balance_after_swap = self.bitcoin_wallet.as_ref().balance().await.unwrap(); let btc_balance_after_swap = self
.alice_swap_factory
.bitcoin_wallet
.as_ref()
.balance()
.await
.unwrap();
assert_eq!( assert_eq!(
btc_balance_after_swap, btc_balance_after_swap,
self.starting_balances.btc + self.swap_amounts.btc self.alice_swap_factory.starting_balances.btc + self.swap_amounts.btc
- bitcoin::Amount::from_sat(bitcoin::TX_FEE) - bitcoin::Amount::from_sat(bitcoin::TX_FEE)
); );
let xmr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap(); let xmr_balance_after_swap = self
assert!(xmr_balance_after_swap <= self.starting_balances.xmr - self.swap_amounts.xmr); .alice_swap_factory
.monero_wallet
.as_ref()
.get_balance()
.await
.unwrap();
assert!(
xmr_balance_after_swap
<= self.alice_swap_factory.starting_balances.xmr - self.swap_amounts.xmr
);
} }
pub async fn assert_refunded(&self, state: AliceState) { pub async fn assert_alice_refunded(&self, state: AliceState) {
assert!(matches!(state, AliceState::XmrRefunded)); assert!(matches!(state, AliceState::XmrRefunded));
let btc_balance_after_swap = self.bitcoin_wallet.as_ref().balance().await.unwrap(); let btc_balance_after_swap = self
assert_eq!(btc_balance_after_swap, self.starting_balances.btc); .alice_swap_factory
.bitcoin_wallet
.as_ref()
.balance()
.await
.unwrap();
assert_eq!(
btc_balance_after_swap,
self.alice_swap_factory.starting_balances.btc
);
// Ensure that Alice's balance is refreshed as we use a newly created wallet // Ensure that Alice's balance is refreshed as we use a newly created wallet
self.monero_wallet.as_ref().inner.refresh().await.unwrap(); self.alice_swap_factory
let xmr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap(); .monero_wallet
.as_ref()
.inner
.refresh()
.await
.unwrap();
let xmr_balance_after_swap = self
.alice_swap_factory
.monero_wallet
.as_ref()
.get_balance()
.await
.unwrap();
assert_eq!(xmr_balance_after_swap, self.swap_amounts.xmr); assert_eq!(xmr_balance_after_swap, self.swap_amounts.xmr);
} }
pub async fn assert_punished(&self, state: AliceState) { pub async fn assert_alice_punished(&self, state: AliceState) {
assert!(matches!(state, AliceState::BtcPunished)); assert!(matches!(state, AliceState::BtcPunished));
let btc_balance_after_swap = self.bitcoin_wallet.as_ref().balance().await.unwrap(); let btc_balance_after_swap = self
.alice_swap_factory
.bitcoin_wallet
.as_ref()
.balance()
.await
.unwrap();
assert_eq!( assert_eq!(
btc_balance_after_swap, btc_balance_after_swap,
self.starting_balances.btc + self.swap_amounts.btc self.alice_swap_factory.starting_balances.btc + self.swap_amounts.btc
- bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE) - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE)
); );
let xnr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap(); let xmr_balance_after_swap = self
assert!(xnr_balance_after_swap <= self.starting_balances.xmr - self.swap_amounts.xmr); .alice_swap_factory
} .monero_wallet
.as_ref()
pub fn peer_id(&self) -> PeerId { .get_balance()
self.peer_id.clone() .await
} .unwrap();
assert!(
pub fn listen_address(&self) -> Multiaddr { xmr_balance_after_swap
self.listen_address.clone() <= self.alice_swap_factory.starting_balances.xmr - self.swap_amounts.xmr
}
}
pub struct Bob {
pub state: BobState,
pub event_loop_handle: bob::EventLoopHandle,
pub db: Database,
pub bitcoin_wallet: Arc<bitcoin::Wallet>,
pub monero_wallet: Arc<monero::Wallet>,
pub swap_id: Uuid,
}
pub struct BobHarness {
db_path: PathBuf,
swap_id: Uuid,
swap_amounts: SwapAmounts,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
config: Config,
starting_balances: StartingBalances,
alice_connect_address: Multiaddr,
alice_connect_peer_id: PeerId,
}
impl BobHarness {
#[allow(clippy::too_many_arguments)]
async fn new(
config: Config,
swap_amounts: SwapAmounts,
swap_id: Uuid,
monero: &Monero,
bitcoind: &Bitcoind<'_>,
starting_balances: StartingBalances,
alice_connect_address: Multiaddr,
alice_connect_peer_id: PeerId,
) -> Self {
let db_path = tempdir().unwrap().path().to_path_buf();
let (bitcoin_wallet, monero_wallet) =
init_wallets("bob", bitcoind, monero, starting_balances.clone(), config).await;
Self {
db_path,
swap_id,
swap_amounts,
bitcoin_wallet,
monero_wallet,
config,
starting_balances,
alice_connect_address,
alice_connect_peer_id,
}
}
pub async fn new_bob(&self) -> Bob {
let initial_state = init_bob_state(
self.swap_amounts.btc,
self.swap_amounts.xmr,
self.bitcoin_wallet.clone(),
self.config,
)
.await;
let (event_loop, event_loop_handle) = init_bob_event_loop(
self.alice_connect_peer_id.clone(),
self.alice_connect_address.clone(),
);
tokio::spawn(async move { event_loop.run().await });
let db = Database::open(self.db_path.as_path()).unwrap();
Bob {
state: initial_state,
event_loop_handle,
db,
bitcoin_wallet: self.bitcoin_wallet.clone(),
monero_wallet: self.monero_wallet.clone(),
swap_id: self.swap_id,
}
}
pub async fn recover_bob_from_db(&self) -> Bob {
// TODO: "simulated restart" issues:
// - create new wallets instead of reusing (hard because of container
// lifetimes)
// - consider aborting the old event loop (currently just keeps running)
// reopen the existing database
let db = Database::open(self.db_path.clone().as_path()).unwrap();
let resume_state =
if let swap::database::Swap::Bob(state) = db.get_state(self.swap_id).unwrap() {
state.into()
} else {
unreachable!()
};
let (event_loop, event_loop_handle) = init_bob_event_loop(
self.alice_connect_peer_id.clone(),
self.alice_connect_address.clone(),
); );
tokio::spawn(async move { event_loop.run().await });
Bob {
state: resume_state,
event_loop_handle,
db,
bitcoin_wallet: self.bitcoin_wallet.clone(),
monero_wallet: self.monero_wallet.clone(),
swap_id: self.swap_id,
}
} }
pub async fn assert_redeemed(&self, state: BobState) { pub async fn assert_bob_redeemed(&self, state: BobState) {
let lock_tx_id = if let BobState::XmrRedeemed { tx_lock_id } = state { let lock_tx_id = if let BobState::XmrRedeemed { tx_lock_id } = state {
tx_lock_id tx_lock_id
} else { } else {
@ -370,47 +170,75 @@ impl BobHarness {
}; };
let lock_tx_bitcoin_fee = self let lock_tx_bitcoin_fee = self
.bob_swap_factory
.bitcoin_wallet .bitcoin_wallet
.transaction_fee(lock_tx_id) .transaction_fee(lock_tx_id)
.await .await
.unwrap(); .unwrap();
let btc_balance_after_swap = self.bitcoin_wallet.as_ref().balance().await.unwrap(); let btc_balance_after_swap = self
.bob_swap_factory
.bitcoin_wallet
.as_ref()
.balance()
.await
.unwrap();
assert_eq!( assert_eq!(
btc_balance_after_swap, btc_balance_after_swap,
self.starting_balances.btc - self.swap_amounts.btc - lock_tx_bitcoin_fee self.bob_swap_factory.starting_balances.btc
- self.swap_amounts.btc
- lock_tx_bitcoin_fee
); );
// Ensure that Bob's balance is refreshed as we use a newly created wallet // Ensure that Bob's balance is refreshed as we use a newly created wallet
self.monero_wallet.as_ref().inner.refresh().await.unwrap(); self.bob_swap_factory
let xmr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap(); .monero_wallet
.as_ref()
.inner
.refresh()
.await
.unwrap();
let xmr_balance_after_swap = self
.bob_swap_factory
.monero_wallet
.as_ref()
.get_balance()
.await
.unwrap();
assert_eq!( assert_eq!(
xmr_balance_after_swap, xmr_balance_after_swap,
self.starting_balances.xmr + self.swap_amounts.xmr self.bob_swap_factory.starting_balances.xmr + self.swap_amounts.xmr
); );
} }
pub async fn assert_refunded(&self, state: BobState) { pub async fn assert_bob_refunded(&self, state: BobState) {
let lock_tx_id = if let BobState::BtcRefunded(state4) = state { let lock_tx_id = if let BobState::BtcRefunded(state4) = state {
state4.tx_lock_id() state4.tx_lock_id()
} else { } else {
panic!("Bob in unexpected state"); panic!("Bob in unexpected state");
}; };
let lock_tx_bitcoin_fee = self let lock_tx_bitcoin_fee = self
.bob_swap_factory
.bitcoin_wallet .bitcoin_wallet
.transaction_fee(lock_tx_id) .transaction_fee(lock_tx_id)
.await .await
.unwrap(); .unwrap();
let btc_balance_after_swap = self.bitcoin_wallet.as_ref().balance().await.unwrap(); let btc_balance_after_swap = self
.bob_swap_factory
.bitcoin_wallet
.as_ref()
.balance()
.await
.unwrap();
let alice_submitted_cancel = btc_balance_after_swap let alice_submitted_cancel = btc_balance_after_swap
== self.starting_balances.btc == self.bob_swap_factory.starting_balances.btc
- lock_tx_bitcoin_fee - lock_tx_bitcoin_fee
- bitcoin::Amount::from_sat(bitcoin::TX_FEE); - bitcoin::Amount::from_sat(bitcoin::TX_FEE);
let bob_submitted_cancel = btc_balance_after_swap let bob_submitted_cancel = btc_balance_after_swap
== self.starting_balances.btc == self.bob_swap_factory.starting_balances.btc
- lock_tx_bitcoin_fee - lock_tx_bitcoin_fee
- bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE); - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE);
@ -418,11 +246,20 @@ impl BobHarness {
// Since we cannot be sure who submitted it we have to assert accordingly // Since we cannot be sure who submitted it we have to assert accordingly
assert!(alice_submitted_cancel || bob_submitted_cancel); assert!(alice_submitted_cancel || bob_submitted_cancel);
let xmr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap(); let xmr_balance_after_swap = self
assert_eq!(xmr_balance_after_swap, self.starting_balances.xmr); .bob_swap_factory
.monero_wallet
.as_ref()
.get_balance()
.await
.unwrap();
assert_eq!(
xmr_balance_after_swap,
self.bob_swap_factory.starting_balances.xmr
);
} }
pub async fn assert_punished(&self, state: BobState) { pub async fn assert_bob_punished(&self, state: BobState) {
let lock_tx_id = if let BobState::BtcPunished { tx_lock_id } = state { let lock_tx_id = if let BobState::BtcPunished { tx_lock_id } = state {
tx_lock_id tx_lock_id
} else { } else {
@ -430,26 +267,123 @@ impl BobHarness {
}; };
let lock_tx_bitcoin_fee = self let lock_tx_bitcoin_fee = self
.bob_swap_factory
.bitcoin_wallet .bitcoin_wallet
.transaction_fee(lock_tx_id) .transaction_fee(lock_tx_id)
.await .await
.unwrap(); .unwrap();
let btc_balance_after_swap = self.bitcoin_wallet.as_ref().balance().await.unwrap(); let btc_balance_after_swap = self
.bob_swap_factory
.bitcoin_wallet
.as_ref()
.balance()
.await
.unwrap();
assert_eq!( assert_eq!(
btc_balance_after_swap, btc_balance_after_swap,
self.starting_balances.btc - self.swap_amounts.btc - lock_tx_bitcoin_fee self.bob_swap_factory.starting_balances.btc
- self.swap_amounts.btc
- lock_tx_bitcoin_fee
); );
let xmr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap(); let xmr_balance_after_swap = self
assert_eq!(xmr_balance_after_swap, self.starting_balances.xmr); .bob_swap_factory
.monero_wallet
.as_ref()
.get_balance()
.await
.unwrap();
assert_eq!(
xmr_balance_after_swap,
self.bob_swap_factory.starting_balances.xmr
);
} }
} }
#[derive(Debug, Clone)] pub async fn setup_test<T, F>(testfn: T)
struct StartingBalances { where
pub xmr: monero::Amount, T: Fn(TestContext) -> F,
pub btc: bitcoin::Amount, F: Future<Output = ()>,
{
let cli = Cli::default();
let _guard = init_tracing();
let (monero, containers) = testutils::init_containers(&cli).await;
let swap_amounts = SwapAmounts {
btc: bitcoin::Amount::from_sat(1_000_000),
xmr: monero::Amount::from_piconero(1_000_000_000_000),
};
let config = Config::regtest();
let alice_starting_balances = StartingBalances {
xmr: swap_amounts.xmr * 10,
btc: bitcoin::Amount::ZERO,
};
let port = get_port().expect("Failed to find a free port");
let listen_address: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", port)
.parse()
.expect("failed to parse Alice's address");
let (alice_bitcoin_wallet, alice_monero_wallet) = init_wallets(
"alice",
&containers.bitcoind,
&monero,
alice_starting_balances.clone(),
config,
)
.await;
let alice_swap_factory = alice::SwapFactory::new(
Seed::random().unwrap(),
config,
Uuid::new_v4(),
alice_bitcoin_wallet,
alice_monero_wallet,
alice_starting_balances,
tempdir().unwrap().path().to_path_buf(),
listen_address,
)
.await;
let bob_starting_balances = StartingBalances {
xmr: monero::Amount::ZERO,
btc: swap_amounts.btc * 10,
};
let (bob_bitcoin_wallet, bob_monero_wallet) = init_wallets(
"bob",
&containers.bitcoind,
&monero,
bob_starting_balances.clone(),
config,
)
.await;
let bob_swap_factory = bob::SwapFactory::new(
Seed::random().unwrap(),
tempdir().unwrap().path().to_path_buf(),
Uuid::new_v4(),
bob_bitcoin_wallet,
bob_monero_wallet,
config,
bob_starting_balances,
alice_swap_factory.listen_address(),
alice_swap_factory.peer_id(),
);
let test = TestContext {
swap_amounts,
alice_swap_factory,
bob_swap_factory,
};
testfn(test).await
} }
async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) { async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) {
@ -498,87 +432,6 @@ async fn init_wallets(
(btc_wallet, xmr_wallet) (btc_wallet, xmr_wallet)
} }
async fn init_alice_state(
btc_to_swap: bitcoin::Amount,
xmr_to_swap: monero::Amount,
alice_btc_wallet: Arc<bitcoin::Wallet>,
config: Config,
) -> AliceState {
let rng = &mut OsRng;
let amounts = SwapAmounts {
btc: btc_to_swap,
xmr: xmr_to_swap,
};
let a = bitcoin::SecretKey::new_random(rng);
let s_a = cross_curve_dleq::Scalar::random(rng);
let v_a = monero::PrivateViewKey::new_random(rng);
let redeem_address = alice_btc_wallet.as_ref().new_address().await.unwrap();
let punish_address = redeem_address.clone();
let state0 = alice::State0::new(
a,
s_a,
v_a,
amounts.btc,
amounts.xmr,
config.bitcoin_cancel_timelock,
config.bitcoin_punish_timelock,
redeem_address,
punish_address,
);
AliceState::Started { amounts, state0 }
}
fn init_alice_event_loop(
listen: Multiaddr,
seed: Seed,
) -> (
alice::event_loop::EventLoop,
alice::event_loop::EventLoopHandle,
) {
let alice_behaviour = alice::Behaviour::new(network::Seed::new(seed));
let alice_transport = build(alice_behaviour.identity()).unwrap();
alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen).unwrap()
}
async fn init_bob_state(
btc_to_swap: bitcoin::Amount,
xmr_to_swap: monero::Amount,
bob_btc_wallet: Arc<bitcoin::Wallet>,
config: Config,
) -> BobState {
let amounts = SwapAmounts {
btc: btc_to_swap,
xmr: xmr_to_swap,
};
let refund_address = bob_btc_wallet.new_address().await.unwrap();
let state0 = bob::State0::new(
&mut OsRng,
btc_to_swap,
xmr_to_swap,
config.bitcoin_cancel_timelock,
config.bitcoin_punish_timelock,
refund_address,
config.monero_finality_confirmations,
);
BobState::Started { state0, amounts }
}
fn init_bob_event_loop(
alice_peer_id: PeerId,
alice_addr: Multiaddr,
) -> (bob::event_loop::EventLoop, bob::event_loop::EventLoopHandle) {
let seed = Seed::random().unwrap();
let bob_behaviour = bob::Behaviour::new(network::Seed::new(seed));
let bob_transport = build(bob_behaviour.identity()).unwrap();
bob::event_loop::EventLoop::new(bob_transport, bob_behaviour, alice_peer_id, alice_addr)
.unwrap()
}
// This is just to keep the containers alive // This is just to keep the containers alive
#[allow(dead_code)] #[allow(dead_code)]
struct Containers<'a> { struct Containers<'a> {

Loading…
Cancel
Save