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] 3 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)]
use crate::cli::{Command, Options, Resume};
use anyhow::{bail, Context, Result};
use libp2p::{core::Multiaddr, PeerId};
use anyhow::{Context, Result};
use prettytable::{row, Table};
use rand::rngs::OsRng;
use std::sync::Arc;
use structopt::StructOpt;
use swap::{
bitcoin,
config::Config,
database::{Database, Swap},
monero, network,
network::transport::build,
protocol::{alice, alice::AliceState, bob, bob::BobState},
seed::Seed,
database::Database,
monero,
protocol::{alice, bob, bob::SwapFactory, StartingBalances},
trace::init_tracing,
SwapAmounts,
};
@ -51,8 +47,7 @@ async fn main() -> Result<()> {
opt.data_dir
);
let data_dir = std::path::Path::new(opt.data_dir.as_str()).to_path_buf();
let db =
Database::open(data_dir.join("database").as_path()).context("Could not open database")?;
let db_path = data_dir.join("database");
let seed = swap::config::seed::Seed::from_file_or_generate(&data_dir)
.expect("Could not retrieve/initialize seed")
@ -67,7 +62,12 @@ async fn main() -> Result<()> {
send_monero,
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,
bitcoin_wallet_name.as_str(),
monero_wallet_rpc_url,
@ -75,50 +75,28 @@ async fn main() -> Result<()> {
)
.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();
info!(
"Swap sending {} and receiving {} started with ID {}",
send_monero, receive_bitcoin, swap_id
);
alice_swap(
let alice_factory = alice::SwapFactory::new(
seed,
config,
swap_id,
alice_state,
listen_addr,
bitcoin_wallet,
monero_wallet,
config,
db,
seed,
starting_balances,
db_path,
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 {
alice_peer_id,
@ -129,7 +107,12 @@ async fn main() -> Result<()> {
send_bitcoin,
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,
bitcoin_wallet_name.as_str(),
monero_wallet_rpc_url,
@ -137,47 +120,36 @@ async fn main() -> Result<()> {
)
.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();
info!(
"Swap sending {} and receiving {} started with ID {}",
send_bitcoin, receive_monero, swap_id
);
bob_swap(
let bob_factory = SwapFactory::new(
seed,
db_path,
swap_id,
bob_state,
bitcoin_wallet,
monero_wallet,
db,
alice_peer_id,
config,
starting_balances,
alice_addr,
seed,
)
.await?;
alice_peer_id,
);
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 => {
let mut table = Table::new();
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()? {
table.add_row(row![swap_id, state]);
}
@ -192,30 +164,29 @@ async fn main() -> Result<()> {
monero_wallet_rpc_url,
listen_addr,
}) => {
let db_state = if let Swap::Alice(db_state) = db.get_state(swap_id)? {
db_state
} else {
bail!("Swap {} is not sell xmr.", swap_id)
};
let (bitcoin_wallet, monero_wallet) = setup_wallets(
let (bitcoin_wallet, monero_wallet, starting_balances) = setup_wallets(
bitcoind_url,
bitcoin_wallet_name.as_str(),
monero_wallet_rpc_url,
config,
)
.await?;
alice_swap(
let alice_factory = alice::SwapFactory::new(
seed,
config,
swap_id,
db_state.into(),
listen_addr,
bitcoin_wallet,
monero_wallet,
config,
db,
seed,
starting_balances,
db_path,
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 {
swap_id,
@ -225,30 +196,29 @@ async fn main() -> Result<()> {
alice_peer_id,
alice_addr,
}) => {
let db_state = if let Swap::Bob(db_state) = db.get_state(swap_id)? {
db_state
} else {
bail!("Swap {} is not buy xmr.", swap_id)
};
let (bitcoin_wallet, monero_wallet) = setup_wallets(
let (bitcoin_wallet, monero_wallet, starting_balances) = setup_wallets(
bitcoind_url,
bitcoin_wallet_name.as_str(),
monero_wallet_rpc_url,
config,
)
.await?;
bob_swap(
let bob_factory = SwapFactory::new(
seed,
db_path,
swap_id,
db_state.into(),
bitcoin_wallet,
monero_wallet,
db,
alice_peer_id,
config,
starting_balances,
alice_addr,
seed,
)
.await?;
alice_peer_id,
);
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,
monero_wallet_rpc_url: url::Url,
config: Config,
) -> Result<(Arc<swap::bitcoin::Wallet>, Arc<swap::monero::Wallet>)> {
) -> Result<(
Arc<swap::bitcoin::Wallet>,
Arc<swap::monero::Wallet>,
StartingBalances,
)> {
let bitcoin_wallet =
swap::bitcoin::Wallet::new(bitcoin_wallet_name, bitcoind_url, config.bitcoin_network)
.await?;
@ -279,68 +253,10 @@ async fn setup_wallets(
);
let monero_wallet = Arc::new(monero_wallet);
Ok((bitcoin_wallet, monero_wallet))
}
#[allow(clippy::too_many_arguments)]
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,
);
let starting_balances = StartingBalances {
btc: bitcoin_balance,
xmr: monero_balance,
};
tokio::spawn(event_loop.run());
swap.await
Ok((bitcoin_wallet, monero_wallet, starting_balances))
}

@ -1,2 +1,8 @@
pub mod alice;
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.
//! Alice holds XMR and wishes receive BTC.
use anyhow::Result;
use libp2p::{
core::{identity::Keypair, Multiaddr},
request_response::ResponseChannel,
NetworkBehaviour, PeerId,
};
use anyhow::{bail, Result};
use libp2p::{request_response::ResponseChannel, NetworkBehaviour, PeerId};
use tracing::{debug, info};
use crate::{
bitcoin, database, monero,
network::{
peer_tracker::{self, PeerTracker},
request_response::AliceToBob,
transport::SwapTransport,
Seed, TokioExecutor,
Seed as NetworkSeed,
},
protocol::bob,
SwapAmounts,
@ -26,8 +22,16 @@ pub use self::{
message1::Message1,
message2::Message2,
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;
pub mod event_loop;
@ -39,29 +43,173 @@ pub mod state;
mod steps;
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(
listen: Multiaddr,
transport: SwapTransport,
behaviour: Behaviour,
) -> Result<Swarm> {
use anyhow::Context as _;
pub struct SwapFactory {
swap_id: Uuid,
identity: Keypair,
peer_id: PeerId,
db_path: PathBuf,
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,
))
}
pub fn peer_id(&self) -> PeerId {
self.peer_id.clone()
}
pub fn listen_address(&self) -> Multiaddr {
self.listen_address.clone()
}
}
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;
let mut swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone())
.executor(Box::new(TokioExecutor {
handle: tokio::runtime::Handle::current(),
}))
.build();
let amounts = SwapAmounts {
btc: btc_to_swap,
xmr: xmr_to_swap,
};
Swarm::listen_on(&mut swarm, listen.clone())
.with_context(|| format!("Address is not supported: {:#}", listen))?;
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,
);
tracing::info!("Initialized swarm: {}", local_peer_id);
Ok(AliceState::Started { amounts, state0 })
}
Ok(swarm)
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)]
@ -138,7 +286,7 @@ impl From<message3::OutEvent> for OutEvent {
}
/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice.
#[derive(NetworkBehaviour)]
#[derive(NetworkBehaviour, Default)]
#[behaviour(out_event = "OutEvent", event_process = false)]
#[allow(missing_debug_implementations)]
pub struct Behaviour {
@ -148,33 +296,9 @@ pub struct Behaviour {
message1: message1::Behaviour,
message2: message2::Behaviour,
message3: message3::Behaviour,
#[behaviour(ignore)]
identity: Keypair,
}
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.
pub fn send_amounts(&mut self, channel: ResponseChannel<AliceToBob>, amounts: SwapAmounts) {
let msg = AliceToBob::Amounts(amounts);

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

@ -15,18 +15,23 @@ use crate::{
bitcoin,
bitcoin::{TransactionBlockHeight, WatchForRawTransaction},
config::Config,
database::{Database, Swap},
database,
database::Database,
monero,
monero::CreateWalletForOutput,
protocol::alice::{
event_loop::EventLoopHandle,
steps::{
build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction,
extract_monero_private_key, lock_xmr, negotiate, publish_bitcoin_punish_transaction,
publish_bitcoin_redeem_transaction, publish_cancel_transaction,
wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin,
protocol::{
alice,
alice::{
event_loop::EventLoopHandle,
steps::{
build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction,
extract_monero_private_key, lock_xmr, negotiate,
publish_bitcoin_punish_transaction, publish_bitcoin_redeem_transaction,
publish_cancel_transaction, wait_for_bitcoin_encrypted_signature,
wait_for_bitcoin_refund, wait_for_locked_bitcoin,
},
AliceState,
},
AliceState,
},
ExpiredTimelocks,
};
@ -35,28 +40,6 @@ trait Rng: 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 {
matches!(
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
#[async_recursion]
#[allow(clippy::too_many_arguments)]
pub async fn run_until(
async fn run_until_internal(
state: AliceState,
is_target_state: fn(&AliceState) -> bool,
mut event_loop_handle: EventLoopHandle,
@ -110,9 +114,9 @@ pub async fn run_until(
};
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?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,
@ -153,9 +157,9 @@ pub async fn run_until(
};
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?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,
@ -194,9 +198,9 @@ pub async fn run_until(
};
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?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,
@ -232,9 +236,9 @@ pub async fn run_until(
};
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?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,
@ -270,9 +274,9 @@ pub async fn run_until(
let state = AliceState::CancelTimelockExpired { state3 };
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?;
return run_until(
return run_until_internal(
state,
is_target_state,
event_loop_handle,
@ -298,9 +302,9 @@ pub async fn run_until(
let state = AliceState::BtcRedeemed;
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?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,
@ -325,9 +329,9 @@ pub async fn run_until(
let state = AliceState::BtcCancelled { state3, tx_cancel };
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?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,
@ -358,10 +362,12 @@ pub async fn run_until(
None => {
let state = AliceState::BtcPunishable { tx_refund, state3 };
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?;
swap(
run_until_internal(
state,
is_target_state,
event_loop_handle,
bitcoin_wallet.clone(),
monero_wallet,
@ -382,9 +388,9 @@ pub async fn run_until(
let state = AliceState::BtcRefunded { spend_key, state3 };
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?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,
@ -407,7 +413,7 @@ pub async fn run_until(
let state = AliceState::XmrRefunded;
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?;
Ok(state)
}
@ -437,9 +443,9 @@ pub async fn run_until(
Either::Left(_) => {
let state = AliceState::BtcPunished;
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?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,
@ -461,9 +467,9 @@ pub async fn run_until(
)?;
let state = AliceState::BtcRefunded { spend_key, state3 };
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?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,

@ -1,19 +1,14 @@
//! Run an XMR/BTC swap in the role of Bob.
//! Bob holds BTC and wishes receive XMR.
use anyhow::Result;
use libp2p::{
core::{identity::Keypair, Multiaddr},
NetworkBehaviour, PeerId,
};
use anyhow::{bail, Result};
use libp2p::{core::Multiaddr, NetworkBehaviour, PeerId};
use tracing::{debug, info};
use crate::{
bitcoin,
bitcoin::EncryptedSignature,
network::{
peer_tracker::{self, PeerTracker},
transport::SwapTransport,
Seed, TokioExecutor,
},
database, monero, network,
network::peer_tracker::{self, PeerTracker},
protocol::{alice, bob},
SwapAmounts,
};
@ -26,8 +21,16 @@ pub use self::{
message2::Message2,
message3::Message3,
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;
pub mod event_loop;
@ -38,20 +41,169 @@ mod message3;
pub mod state;
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())?;
pub fn new_swarm(transport: SwapTransport, behaviour: Behaviour) -> Result<Swarm> {
let local_peer_id = behaviour.peer_id();
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 swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone())
.executor(Box::new(TokioExecutor {
handle: tokio::runtime::Handle::current(),
}))
.build();
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
)
};
info!("Initialized swarm with identity {}", local_peer_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(),
)?;
Ok(swarm)
Ok((
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,
))
}
}
async fn init_bob_state(
btc_to_swap: bitcoin::Amount,
xmr_to_swap: monero::Amount,
bob_btc_wallet: Arc<bitcoin::Wallet>,
config: Config,
) -> Result<BobState> {
let amounts = SwapAmounts {
btc: btc_to_swap,
xmr: xmr_to_swap,
};
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(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)]
@ -115,7 +267,7 @@ impl From<message3::OutEvent> for OutEvent {
}
/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob.
#[derive(NetworkBehaviour)]
#[derive(NetworkBehaviour, Default)]
#[behaviour(out_event = "OutEvent", event_process = false)]
#[allow(missing_debug_implementations)]
pub struct Behaviour {
@ -125,33 +277,9 @@ pub struct Behaviour {
message1: message1::Behaviour,
message2: message2::Behaviour,
message3: message3::Behaviour,
#[behaviour(ignore)]
identity: Keypair,
}
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`.
pub fn request_amounts(&mut self, alice: PeerId, btc: u64) {
let btc = ::bitcoin::Amount::from_sat(btc);
@ -189,19 +317,3 @@ impl Behaviour {
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(
transport: SwapTransport,
behaviour: Behaviour,
peer_id: PeerId,
alice_peer_id: PeerId,
alice_addr: Multiaddr,
) -> Result<(Self, EventLoopHandle)> {
let local_peer_id = behaviour.peer_id();
let mut swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id)
let mut swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, peer_id)
.executor(Box::new(TokioExecutor {
handle: tokio::runtime::Handle::current(),
}))

@ -1,6 +1,6 @@
use anyhow::{bail, Result};
use async_recursion::async_recursion;
use rand::{CryptoRng, RngCore};
use rand::{rngs::OsRng, CryptoRng, RngCore};
use std::sync::Arc;
use tokio::select;
use tracing::{debug, info};
@ -14,33 +14,6 @@ use crate::{
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 {
matches!(
state,
@ -63,10 +36,32 @@ pub fn is_encsig_sent(state: &BobState) -> bool {
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
#[allow(clippy::too_many_arguments)]
#[async_recursion]
pub async fn run_until<R>(
async fn run_until_internal<R>(
state: BobState,
is_target_state: fn(&BobState) -> bool,
mut event_loop_handle: EventLoopHandle,
@ -99,7 +94,7 @@ where
let state = BobState::Negotiated(state2);
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,
@ -120,7 +115,7 @@ where
let state = BobState::BtcLocked(state3);
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,
@ -185,7 +180,7 @@ where
};
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,
@ -226,7 +221,7 @@ where
};
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,
@ -261,7 +256,7 @@ where
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,
@ -282,7 +277,7 @@ where
};
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,
@ -307,7 +302,7 @@ where
db.insert_latest_state(swap_id, Swap::Bob(state.clone().into()))
.await?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,
@ -336,7 +331,7 @@ where
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
run_until_internal(
state,
is_target_state,
event_loop_handle,

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

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

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

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

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

@ -1,4 +1,3 @@
use rand::rngs::OsRng;
use swap::protocol::{alice, alice::AliceState, bob};
pub mod testutils;
@ -7,60 +6,33 @@ pub mod testutils;
/// then also refunds.
#[tokio::test]
async fn given_alice_restarts_after_xmr_is_locked_abort_swap() {
testutils::test(|alice_harness, bob_harness| async move {
let alice = alice_harness.new_alice().await;
let bob = bob_harness.new_bob().await;
testutils::setup_test(|ctx| async move {
let alice_swap = ctx.new_swap_as_alice().await;
let bob_swap = ctx.new_swap_as_bob().await;
let bob_swap = bob::swap(
bob.state,
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 bob = bob::run(bob_swap);
let bob_handle = tokio::spawn(bob);
let alice_state = alice::run_until(
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
.unwrap();
let alice_state = alice::run_until(alice_swap, alice::swap::is_xmr_locked)
.await
.unwrap();
assert!(matches!(alice_state, AliceState::XmrLocked {..}));
// 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
let alice = alice_harness.recover_alice_from_db().await;
assert!(matches!(alice.state, AliceState::XmrLocked {..}));
let alice_swap = ctx.recover_alice_from_db().await;
assert!(matches!(alice_swap.state, AliceState::XmrLocked {..}));
let alice_state = alice::swap(
alice.state,
alice.event_loop_handle,
alice.bitcoin_wallet.clone(),
alice.monero_wallet.clone(),
alice.config,
alice.swap_id,
alice.db,
)
.await
.unwrap();
let alice_state = alice::run(alice_swap).await.unwrap();
// 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!
// 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!
bob_harness.assert_refunded(bob_state.unwrap()).await;
alice_harness.assert_refunded(alice_state).await;
ctx.assert_bob_refunded(bob_state.unwrap()).await;
ctx.assert_alice_refunded(alice_state).await;
})
.await;
}

@ -2,17 +2,14 @@ use crate::testutils;
use bitcoin_harness::Bitcoind;
use futures::Future;
use get_port::get_port;
use libp2p::{core::Multiaddr, PeerId};
use libp2p::core::Multiaddr;
use monero_harness::{image, Monero};
use rand::rngs::OsRng;
use std::{path::PathBuf, sync::Arc};
use std::sync::Arc;
use swap::{
bitcoin,
config::Config,
database::Database,
monero, network,
network::transport::build,
protocol::{alice, alice::AliceState, bob, bob::BobState},
monero,
protocol::{alice, alice::AliceState, bob, bob::BobState, StartingBalances},
seed::Seed,
SwapAmounts,
};
@ -22,347 +19,150 @@ use tracing_core::dispatcher::DefaultGuard;
use tracing_log::LogTracer;
use uuid::Uuid;
pub async fn test<T, F>(testfn: T)
where
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 TestContext {
swap_amounts: SwapAmounts,
pub struct Alice {
pub state: AliceState,
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,
alice_swap_factory: alice::SwapFactory,
bob_swap_factory: bob::SwapFactory,
}
pub struct AliceHarness {
listen_address: Multiaddr,
peer_id: PeerId,
seed: Seed,
db_path: PathBuf,
swap_id: Uuid,
impl TestContext {
pub async fn new_swap_as_alice(&self) -> alice::Swap {
let (swap, mut event_loop) = self
.alice_swap_factory
.new_swap_as_alice(self.swap_amounts)
.await
.unwrap();
swap_amounts: SwapAmounts,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
config: Config,
starting_balances: StartingBalances,
}
tokio::spawn(async move { event_loop.run().await });
impl AliceHarness {
async fn new(
config: Config,
swap_amounts: SwapAmounts,
swap_id: Uuid,
monero: &Monero,
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
let network_seed = network::Seed::new(seed);
let identity = network_seed.derive_libp2p_identity();
let peer_id = PeerId::from(identity.public());
Self {
seed,
db_path,
listen_address,
peer_id,
swap_id,
swap_amounts,
bitcoin_wallet,
monero_wallet,
config,
starting_balances,
}
swap
}
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) =
init_alice_event_loop(self.listen_address.clone(), self.seed);
pub async fn new_swap_as_bob(&self) -> bob::Swap {
let (swap, event_loop) = self
.bob_swap_factory
.new_swap_as_bob(self.swap_amounts)
.await
.unwrap();
tokio::spawn(async move { event_loop.run().await });
let db = Database::open(self.db_path.as_path()).unwrap();
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,
}
swap
}
pub async fn recover_alice_from_db(&self) -> Alice {
// 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)
pub async fn recover_alice_from_db(&self) -> alice::Swap {
let (swap, mut event_loop) = self
.alice_swap_factory
.recover_alice_from_db()
.await
.unwrap();
// reopen the existing database
let db = Database::open(self.db_path.clone().as_path()).unwrap();
tokio::spawn(async move { event_loop.run().await });
let resume_state =
if let swap::database::Swap::Alice(state) = db.get_state(self.swap_id).unwrap() {
state.into()
} else {
unreachable!()
};
swap
}
let (mut event_loop, event_loop_handle) =
init_alice_event_loop(self.listen_address.clone(), self.seed);
pub async fn recover_bob_from_db(&self) -> bob::Swap {
let (swap, event_loop) = self.bob_swap_factory.recover_bob_from_db().await.unwrap();
tokio::spawn(async move { event_loop.run().await });
Alice {
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,
}
swap
}
pub async fn assert_redeemed(&self, state: AliceState) {
pub async fn assert_alice_redeemed(&self, state: AliceState) {
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!(
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)
);
let xmr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap();
assert!(xmr_balance_after_swap <= self.starting_balances.xmr - self.swap_amounts.xmr);
let xmr_balance_after_swap = self
.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));
let btc_balance_after_swap = self.bitcoin_wallet.as_ref().balance().await.unwrap();
assert_eq!(btc_balance_after_swap, self.starting_balances.btc);
let btc_balance_after_swap = self
.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
self.monero_wallet.as_ref().inner.refresh().await.unwrap();
let xmr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap();
self.alice_swap_factory
.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);
}
pub async fn assert_punished(&self, state: AliceState) {
pub async fn assert_alice_punished(&self, state: AliceState) {
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!(
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)
);
let xnr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap();
assert!(xnr_balance_after_swap <= self.starting_balances.xmr - self.swap_amounts.xmr);
}
pub fn peer_id(&self) -> PeerId {
self.peer_id.clone()
}
pub fn listen_address(&self) -> Multiaddr {
self.listen_address.clone()
}
}
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(),
let xmr_balance_after_swap = self
.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
);
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 {
tx_lock_id
} else {
@ -370,47 +170,75 @@ impl BobHarness {
};
let lock_tx_bitcoin_fee = self
.bob_swap_factory
.bitcoin_wallet
.transaction_fee(lock_tx_id)
.await
.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!(
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
self.monero_wallet.as_ref().inner.refresh().await.unwrap();
let xmr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap();
self.bob_swap_factory
.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!(
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 {
state4.tx_lock_id()
} else {
panic!("Bob in unexpected state");
};
let lock_tx_bitcoin_fee = self
.bob_swap_factory
.bitcoin_wallet
.transaction_fee(lock_tx_id)
.await
.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
== self.starting_balances.btc
== self.bob_swap_factory.starting_balances.btc
- lock_tx_bitcoin_fee
- bitcoin::Amount::from_sat(bitcoin::TX_FEE);
let bob_submitted_cancel = btc_balance_after_swap
== self.starting_balances.btc
== self.bob_swap_factory.starting_balances.btc
- lock_tx_bitcoin_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
assert!(alice_submitted_cancel || bob_submitted_cancel);
let xmr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap();
assert_eq!(xmr_balance_after_swap, self.starting_balances.xmr);
let xmr_balance_after_swap = self
.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 {
tx_lock_id
} else {
@ -430,26 +267,123 @@ impl BobHarness {
};
let lock_tx_bitcoin_fee = self
.bob_swap_factory
.bitcoin_wallet
.transaction_fee(lock_tx_id)
.await
.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!(
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();
assert_eq!(xmr_balance_after_swap, self.starting_balances.xmr);
let xmr_balance_after_swap = self
.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)]
struct StartingBalances {
pub xmr: monero::Amount,
pub btc: bitcoin::Amount,
pub async fn setup_test<T, F>(testfn: T)
where
T: Fn(TestContext) -> 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 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<'_>) {
@ -498,87 +432,6 @@ async fn init_wallets(
(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
#[allow(dead_code)]
struct Containers<'a> {

Loading…
Cancel
Save