Make the factory code usable in production

- Introduce Test abstraction instead of tow harnesses, move test specific data into Test
- Change the abstraction from actors to swap, because we are creating swaps, not actors
- rename actor::swap  to run, because we are running a swap
This commit is contained in:
Daniel Karzel 2021-01-18 19:56:43 +11:00
parent e4795fa4ee
commit 8bf467b550
12 changed files with 568 additions and 564 deletions

View File

@ -300,15 +300,17 @@ async fn alice_swap(
let (mut event_loop, handle) =
alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen_addr)?;
let swap = alice::swap::swap(
let swap = alice::Swap {
state,
handle,
bitcoin_wallet.clone(),
monero_wallet.clone(),
event_loop_handle: handle,
bitcoin_wallet,
monero_wallet,
config,
swap_id,
db,
);
};
let swap = alice::swap::run(swap);
tokio::spawn(async move { event_loop.run().await });
swap.await
@ -331,15 +333,16 @@ async fn bob_swap(
let (event_loop, handle) =
bob::event_loop::EventLoop::new(bob_transport, bob_behaviour, alice_peer_id, alice_addr)?;
let swap = bob::swap::swap(
let swap = bob::Swap {
state,
handle,
event_loop_handle: handle,
db,
bitcoin_wallet.clone(),
monero_wallet.clone(),
OsRng,
bitcoin_wallet,
monero_wallet,
swap_id,
);
};
let swap = bob::swap::run(swap);
tokio::spawn(event_loop.run());
swap.await

View File

@ -9,6 +9,7 @@ use libp2p::{
use tracing::{debug, info};
use crate::{
bitcoin, monero,
network::{
peer_tracker::{self, PeerTracker},
request_response::AliceToBob,
@ -26,8 +27,11 @@ pub use self::{
message1::Message1,
message2::Message2,
state::*,
swap::{run_until, swap},
swap::{run, run_until},
};
use crate::{config::Config, database::Database};
use std::sync::Arc;
use uuid::Uuid;
mod amounts;
pub mod event_loop;
@ -39,6 +43,16 @@ pub mod state;
mod steps;
pub mod swap;
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 type Swarm = libp2p::Swarm<Behaviour>;
pub fn new_swarm(

View File

@ -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> {
do_run_until(
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 do_run_until(
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(
do_run_until(
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(
do_run_until(
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(
do_run_until(
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(
do_run_until(
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 do_run_until(
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(
do_run_until(
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(
do_run_until(
state,
is_target_state,
event_loop_handle,
@ -358,10 +362,10 @@ 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?;
run_until(
do_run_until(
state,
is_target_state,
event_loop_handle,
@ -384,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(
do_run_until(
state,
is_target_state,
event_loop_handle,
@ -409,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)
}
@ -439,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(
do_run_until(
state,
is_target_state,
event_loop_handle,
@ -463,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(
do_run_until(
state,
is_target_state,
event_loop_handle,

View File

@ -8,7 +8,9 @@ use libp2p::{
use tracing::{debug, info};
use crate::{
bitcoin,
bitcoin::EncryptedSignature,
monero,
network::{
peer_tracker::{self, PeerTracker},
transport::SwapTransport,
@ -26,8 +28,11 @@ pub use self::{
message2::Message2,
message3::Message3,
state::*,
swap::{run_until, swap},
swap::{run, run_until},
};
use crate::database::Database;
use std::sync::Arc;
use uuid::Uuid;
mod amounts;
pub mod event_loop;
@ -38,6 +43,15 @@ mod message3;
pub mod state;
pub mod swap;
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 type Swarm = libp2p::Swarm<Behaviour>;
pub fn new_swarm(transport: SwapTransport, behaviour: Behaviour) -> Result<Swarm> {

View File

@ -13,33 +13,7 @@ use crate::{
protocol::bob::{self, event_loop::EventLoopHandle, state::*},
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
}
use ecdsa_fun::fun::rand_core::OsRng;
pub fn is_complete(state: &BobState) -> bool {
matches!(
@ -63,10 +37,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> {
do_run_until(
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 do_run_until<R>(
state: BobState,
is_target_state: fn(&BobState) -> bool,
mut event_loop_handle: EventLoopHandle,
@ -99,7 +95,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(
do_run_until(
state,
is_target_state,
event_loop_handle,
@ -120,7 +116,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(
do_run_until(
state,
is_target_state,
event_loop_handle,
@ -185,7 +181,7 @@ where
};
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
do_run_until(
state,
is_target_state,
event_loop_handle,
@ -226,7 +222,7 @@ where
};
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
do_run_until(
state,
is_target_state,
event_loop_handle,
@ -261,7 +257,7 @@ where
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
do_run_until(
state,
is_target_state,
event_loop_handle,
@ -282,7 +278,7 @@ where
};
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
do_run_until(
state,
is_target_state,
event_loop_handle,
@ -307,7 +303,7 @@ where
db.insert_latest_state(swap_id, Swap::Bob(state.clone().into()))
.await?;
run_until(
do_run_until(
state,
is_target_state,
event_loop_handle,
@ -336,7 +332,7 @@ where
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until(
do_run_until(
state,
is_target_state,
event_loop_handle,

View File

@ -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::init(|test| async move {
let alice_swap = test.new_swap_as_alice().await;
let bob_swap = test.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;
test.assert_alice_redeemed(alice_state.unwrap()).await;
test.assert_bob_redeemed(bob_state.unwrap()).await;
})
.await;
}

View File

@ -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;
testutils::init(|test| async move {
let alice_swap = test.new_swap_as_alice().await;
let bob_swap = test.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_encsig_learned,
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_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_swap = test.recover_alice_from_db().await;
assert!(matches!(alice_swap.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();
let alice_state = alice::run(alice_swap).await.unwrap();
alice_harness.assert_redeemed(alice_state).await;
test.assert_alice_redeemed(alice_state).await;
let bob_state = bob_swap_handle.await.unwrap();
bob_harness.assert_redeemed(bob_state.unwrap()).await
let bob_state = bob_handle.await.unwrap();
test.assert_bob_redeemed(bob_state.unwrap()).await
})
.await;
}

View File

@ -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;
testutils::init(|test| async move {
let alice_swap = test.new_swap_as_alice().await;
let bob_swap = test.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_swap_handle = tokio::spawn(alice_swap);
let alice = alice::run(alice_swap);
let alice_handle = tokio::spawn(alice);
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();
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_swap = test.recover_bob_from_db().await;
assert!(matches!(bob_swap.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();
let bob_state = bob::run(bob_swap).await.unwrap();
bob_harness.assert_redeemed(bob_state).await;
test.assert_bob_redeemed(bob_state).await;
let alice_state = alice_swap_handle.await.unwrap();
alice_harness.assert_redeemed(alice_state.unwrap()).await;
let alice_state = alice_handle.await.unwrap();
test.assert_alice_redeemed(alice_state.unwrap()).await;
})
.await;
}

View File

@ -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;
testutils::init(|test| async move {
let alice_swap = test.new_swap_as_alice().await;
let bob_swap = test.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_swap_handle = tokio::spawn(alice_swap);
let alice_handle = alice::run(alice_swap);
let alice_swap_handle = tokio::spawn(alice_handle);
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();
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 = test.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;
test.assert_bob_redeemed(bob_state).await;
let alice_state = alice_swap_handle.await.unwrap();
alice_harness.assert_redeemed(alice_state.unwrap()).await;
test.assert_alice_redeemed(alice_state.unwrap()).await;
})
.await;
}

View File

@ -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;
testutils::init(|test| async move {
let alice_swap = test.new_swap_as_alice().await;
let bob_swap = test.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_swap_handle = tokio::spawn(alice_swap);
let alice = alice::run(alice_swap);
let alice_handle = tokio::spawn(alice);
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();
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();
test.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_swap = test.recover_bob_from_db().await;
assert!(matches!(bob_swap.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();
let bob_state = bob::run(bob_swap).await.unwrap();
bob_harness.assert_punished(bob_state).await;
test.assert_bob_punished(bob_state).await;
})
.await;
}

View File

@ -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::init(|test| async move {
let alice_swap = test.new_swap_as_alice().await;
let bob_swap = test.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 = test.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;
test.assert_bob_refunded(bob_state.unwrap()).await;
test.assert_alice_refunded(alice_state).await;
})
.await;
}

View File

@ -22,9 +22,285 @@ use tracing_core::dispatcher::DefaultGuard;
use tracing_log::LogTracer;
use uuid::Uuid;
pub async fn test<T, F>(testfn: T)
pub struct Test {
swap_amounts: SwapAmounts,
alice_swap_factory: AliceSwapFactory,
bob_swap_factory: BobSwapFactory,
}
impl Test {
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;
tokio::spawn(async move { event_loop.run().await });
swap
}
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;
tokio::spawn(async move { event_loop.run().await });
swap
}
pub async fn recover_alice_from_db(&self) -> alice::Swap {
let (swap, mut event_loop) = self.alice_swap_factory.recover_alice_from_db().await;
tokio::spawn(async move { event_loop.run().await });
swap
}
pub async fn recover_bob_from_db(&self) -> bob::Swap {
let (swap, event_loop) = self.bob_swap_factory.recover_bob_from_db().await;
tokio::spawn(async move { event_loop.run().await });
swap
}
pub async fn assert_alice_redeemed(&self, state: AliceState) {
assert!(matches!(state, AliceState::BtcRedeemed));
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 + self.swap_amounts.btc
- bitcoin::Amount::from_sat(bitcoin::TX_FEE)
);
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_alice_refunded(&self, state: AliceState) {
assert!(matches!(state, AliceState::XmrRefunded));
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.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_alice_punished(&self, state: AliceState) {
assert!(matches!(state, AliceState::BtcPunished));
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 + self.swap_amounts.btc
- bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE)
);
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_bob_redeemed(&self, state: BobState) {
let lock_tx_id = if let BobState::XmrRedeemed { tx_lock_id } = state {
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
.bob_swap_factory
.bitcoin_wallet
.as_ref()
.balance()
.await
.unwrap();
assert_eq!(
btc_balance_after_swap,
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.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.bob_swap_factory.starting_balances.xmr + self.swap_amounts.xmr
);
}
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
.bob_swap_factory
.bitcoin_wallet
.as_ref()
.balance()
.await
.unwrap();
let alice_submitted_cancel = btc_balance_after_swap
== 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.bob_swap_factory.starting_balances.btc
- lock_tx_bitcoin_fee
- bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE);
// The cancel tx can be submitted by both Alice and Bob.
// Since we cannot be sure who submitted it we have to assert accordingly
assert!(alice_submitted_cancel || bob_submitted_cancel);
let xmr_balance_after_swap = self
.bob_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_bob_punished(&self, state: BobState) {
let lock_tx_id = if let BobState::BtcPunished { tx_lock_id } = state {
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
.bob_swap_factory
.bitcoin_wallet
.as_ref()
.balance()
.await
.unwrap();
assert_eq!(
btc_balance_after_swap,
self.bob_swap_factory.starting_balances.btc
- self.swap_amounts.btc
- lock_tx_bitcoin_fee
);
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 init<T, F>(testfn: T)
where
T: Fn(AliceHarness, BobHarness) -> F,
T: Fn(Test) -> F,
F: Future<Output = ()>,
{
let cli = Cli::default();
@ -44,9 +320,8 @@ where
xmr: swap_amounts.xmr * 10,
btc: bitcoin::Amount::ZERO,
};
let alice_harness = AliceHarness::new(
let alice_swap_factory = AliceSwapFactory::new(
config,
swap_amounts,
Uuid::new_v4(),
&monero,
&containers.bitcoind,
@ -59,32 +334,27 @@ where
btc: swap_amounts.btc * 10,
};
let bob_harness = BobHarness::new(
let bob_swap_factory = BobSwapFactory::new(
config,
swap_amounts,
Uuid::new_v4(),
&monero,
&containers.bitcoind,
bob_starting_balances,
alice_harness.listen_address(),
alice_harness.peer_id(),
alice_swap_factory.listen_address(),
alice_swap_factory.peer_id(),
)
.await;
testfn(alice_harness, bob_harness).await
let test = Test {
swap_amounts,
alice_swap_factory,
bob_swap_factory,
};
testfn(test).await
}
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,
}
pub struct AliceHarness {
pub struct AliceSwapFactory {
listen_address: Multiaddr,
peer_id: PeerId,
@ -92,17 +362,15 @@ pub struct AliceHarness {
db_path: PathBuf,
swap_id: Uuid,
swap_amounts: SwapAmounts,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
config: Config,
starting_balances: StartingBalances,
}
impl AliceHarness {
impl AliceSwapFactory {
async fn new(
config: Config,
swap_amounts: SwapAmounts,
swap_id: Uuid,
monero: &Monero,
bitcoind: &Bitcoind<'_>,
@ -132,7 +400,6 @@ impl AliceHarness {
listen_address,
peer_id,
swap_id,
swap_amounts,
bitcoin_wallet,
monero_wallet,
config,
@ -140,39 +407,37 @@ impl AliceHarness {
}
}
pub async fn new_alice(&self) -> Alice {
pub async fn new_swap_as_alice(
&self,
swap_amounts: SwapAmounts,
) -> (alice::Swap, alice::EventLoop) {
let initial_state = init_alice_state(
self.swap_amounts.btc,
self.swap_amounts.xmr,
swap_amounts.btc,
swap_amounts.xmr,
self.bitcoin_wallet.clone(),
self.config,
)
.await;
let (mut event_loop, event_loop_handle) =
let (event_loop, event_loop_handle) =
init_alice_event_loop(self.listen_address.clone(), self.seed);
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,
}
(
alice::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) -> 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, alice::EventLoop) {
// reopen the existing database
let db = Database::open(self.db_path.clone().as_path()).unwrap();
@ -183,60 +448,21 @@ impl AliceHarness {
unreachable!()
};
let (mut event_loop, event_loop_handle) =
let (event_loop, event_loop_handle) =
init_alice_event_loop(self.listen_address.clone(), self.seed);
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,
}
}
pub async fn assert_redeemed(&self, state: AliceState) {
assert!(matches!(state, AliceState::BtcRedeemed));
let btc_balance_after_swap = self.bitcoin_wallet.as_ref().balance().await.unwrap();
assert_eq!(
btc_balance_after_swap,
self.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);
}
pub async fn assert_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);
// 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();
assert_eq!(xmr_balance_after_swap, self.swap_amounts.xmr);
}
pub async fn assert_punished(&self, state: AliceState) {
assert!(matches!(state, AliceState::BtcPunished));
let btc_balance_after_swap = self.bitcoin_wallet.as_ref().balance().await.unwrap();
assert_eq!(
btc_balance_after_swap,
self.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);
(
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,
},
event_loop,
)
}
pub fn peer_id(&self) -> PeerId {
@ -248,20 +474,10 @@ impl AliceHarness {
}
}
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 {
pub struct BobSwapFactory {
db_path: PathBuf,
swap_id: Uuid,
swap_amounts: SwapAmounts,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
config: Config,
@ -271,11 +487,10 @@ pub struct BobHarness {
alice_connect_peer_id: PeerId,
}
impl BobHarness {
impl BobSwapFactory {
#[allow(clippy::too_many_arguments)]
async fn new(
config: Config,
swap_amounts: SwapAmounts,
swap_id: Uuid,
monero: &Monero,
bitcoind: &Bitcoind<'_>,
@ -291,7 +506,6 @@ impl BobHarness {
Self {
db_path,
swap_id,
swap_amounts,
bitcoin_wallet,
monero_wallet,
config,
@ -301,10 +515,10 @@ impl BobHarness {
}
}
pub async fn new_bob(&self) -> Bob {
pub async fn new_swap_as_bob(&self, swap_amounts: SwapAmounts) -> (bob::Swap, bob::EventLoop) {
let initial_state = init_bob_state(
self.swap_amounts.btc,
self.swap_amounts.xmr,
swap_amounts.btc,
swap_amounts.xmr,
self.bitcoin_wallet.clone(),
self.config,
)
@ -315,26 +529,22 @@ impl BobHarness {
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,
}
(
bob::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) -> 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)
pub async fn recover_bob_from_db(&self) -> (bob::Swap, bob::EventLoop) {
// reopen the existing database
let db = Database::open(self.db_path.clone().as_path()).unwrap();
@ -350,99 +560,17 @@ impl BobHarness {
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) {
let lock_tx_id = if let BobState::XmrRedeemed { tx_lock_id } = state {
tx_lock_id
} else {
panic!("Bob in unexpected state");
};
let lock_tx_bitcoin_fee = self
.bitcoin_wallet
.transaction_fee(lock_tx_id)
.await
.unwrap();
let btc_balance_after_swap = self.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
);
// 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();
assert_eq!(
xmr_balance_after_swap,
self.starting_balances.xmr + self.swap_amounts.xmr
);
}
pub async fn assert_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
.bitcoin_wallet
.transaction_fee(lock_tx_id)
.await
.unwrap();
let btc_balance_after_swap = self.bitcoin_wallet.as_ref().balance().await.unwrap();
let alice_submitted_cancel = btc_balance_after_swap
== self.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
- lock_tx_bitcoin_fee
- bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE);
// The cancel tx can be submitted by both Alice and Bob.
// 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);
}
pub async fn assert_punished(&self, state: BobState) {
let lock_tx_id = if let BobState::BtcPunished { tx_lock_id } = state {
tx_lock_id
} else {
panic!("Bob in unexpected state");
};
let lock_tx_bitcoin_fee = self
.bitcoin_wallet
.transaction_fee(lock_tx_id)
.await
.unwrap();
let btc_balance_after_swap = self.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
);
let xmr_balance_after_swap = self.monero_wallet.as_ref().get_balance().await.unwrap();
assert_eq!(xmr_balance_after_swap, self.starting_balances.xmr);
(
bob::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,
)
}
}