Add alice punish test

Use reusable test init functions for happy path test

Extract tracing setup to reusable function

Move test initialization to seperate functions

Increase stack size in CI

Fix monero max finality time

Force Bob swarm polling to send message 2

Run Bob state to xmr_locked in punish test to force the sending of
message2. Previously Bob state was run until btc_locked. Although
this was the right thing to do, message2 was not being sent as the
swarm was not polled in btc_locked. Alice punish test passes.

Add info logging to executor
This commit is contained in:
rishflab 2020-12-02 12:36:47 +11:00
parent 5fef68322a
commit c91e9652aa
8 changed files with 731 additions and 540 deletions

View File

@ -104,6 +104,7 @@ jobs:
run: cargo test --workspace --all-features run: cargo test --workspace --all-features
env: env:
MONERO_ADDITIONAL_SLEEP_PERIOD: 60000 MONERO_ADDITIONAL_SLEEP_PERIOD: 60000
RUST_MIN_STACK: 100000000000
- name: Build binary - name: Build binary
run: | run: |

1
Cargo.lock generated
View File

@ -3369,7 +3369,6 @@ dependencies = [
"bitcoin", "bitcoin",
"bitcoin-harness", "bitcoin-harness",
"conquer-once", "conquer-once",
"conquer-once",
"derivative", "derivative",
"ecdsa_fun", "ecdsa_fun",
"futures", "futures",

View File

@ -5,7 +5,6 @@ use crate::{
SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
}; };
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use conquer_once::Lazy;
use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic};
use futures::{ use futures::{
future::{select, Either}, future::{select, Either},
@ -29,13 +28,6 @@ use xmr_btc::{
monero::Transfer, monero::Transfer,
}; };
// The maximum we assume we need to wait from the moment the monero transaction
// is mined to the moment it reaches finality. We set 15 confirmations for now
// (based on Kraken). 1.5 multiplier in case the blockchain is slower than
// usually. Average of 2 minutes block time
static MONERO_MAX_FINALITY_TIME: Lazy<Duration> =
Lazy::new(|| Duration::from_secs_f64(15f64 * 1.5 * 2f64 * 60f64));
pub async fn negotiate( pub async fn negotiate(
amounts: SwapAmounts, amounts: SwapAmounts,
a: bitcoin::SecretKey, a: bitcoin::SecretKey,
@ -180,8 +172,11 @@ where
Ok(()) Ok(())
} }
pub async fn wait_for_bitcoin_encrypted_signature(swarm: &mut Swarm) -> Result<EncryptedSignature> { pub async fn wait_for_bitcoin_encrypted_signature(
let event = timeout(*MONERO_MAX_FINALITY_TIME, swarm.next()) swarm: &mut Swarm,
timeout_duration: Duration,
) -> Result<EncryptedSignature> {
let event = timeout(timeout_duration, swarm.next())
.await .await
.context("Failed to receive Bitcoin encrypted signature from Bob")?; .context("Failed to receive Bitcoin encrypted signature from Bob")?;

View File

@ -24,7 +24,8 @@ use futures::{
}; };
use libp2p::request_response::ResponseChannel; use libp2p::request_response::ResponseChannel;
use rand::{CryptoRng, RngCore}; use rand::{CryptoRng, RngCore};
use std::sync::Arc; use std::{fmt, sync::Arc};
use tracing::info;
use xmr_btc::{ use xmr_btc::{
alice::State3, alice::State3,
bitcoin::{TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction}, bitcoin::{TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction},
@ -86,328 +87,377 @@ pub enum AliceState {
SafelyAborted, SafelyAborted,
} }
pub async fn swap<R>( impl fmt::Display for AliceState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AliceState::Started { .. } => write!(f, "started"),
AliceState::Negotiated { .. } => write!(f, "negotiated"),
AliceState::BtcLocked { .. } => write!(f, "btc_locked"),
AliceState::XmrLocked { .. } => write!(f, "xmr_locked"),
AliceState::EncSignLearned { .. } => write!(f, "encsig_sent"),
AliceState::BtcRedeemed => write!(f, "btc_redeemed"),
AliceState::BtcCancelled { .. } => write!(f, "btc_cancelled"),
AliceState::BtcRefunded { .. } => write!(f, "btc_refunded"),
AliceState::Punished => write!(f, "punished"),
AliceState::SafelyAborted => write!(f, "safely_aborted"),
AliceState::BtcPunishable { .. } => write!(f, "btc_punishable"),
AliceState::XmrRefunded => write!(f, "xmr_refunded"),
AliceState::WaitingToCancel { .. } => write!(f, "waiting_to_cancel"),
}
}
}
pub async fn swap(
state: AliceState, state: AliceState,
swarm: Swarm, swarm: Swarm,
bitcoin_wallet: Arc<crate::bitcoin::Wallet>, bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
monero_wallet: Arc<crate::monero::Wallet>, monero_wallet: Arc<crate::monero::Wallet>,
) -> Result<AliceState> config: Config,
where ) -> Result<(AliceState, Swarm)> {
R: RngCore + CryptoRng + Send, run_until(
{ state,
run_until(state, is_complete, swarm, bitcoin_wallet, monero_wallet).await is_complete,
swarm,
bitcoin_wallet,
monero_wallet,
config,
)
.await
} }
// TODO: use macro or generics // TODO: use macro or generics
pub fn is_complete(state: &AliceState) -> bool { pub fn is_complete(state: &AliceState) -> bool {
matches!(state, AliceState::XmrRefunded| AliceState::BtcRedeemed | AliceState::Punished | AliceState::SafelyAborted) matches!(
state,
AliceState::XmrRefunded
| AliceState::BtcRedeemed
| AliceState::Punished
| AliceState::SafelyAborted
)
} }
pub fn is_xmr_locked(state: &AliceState) -> bool {
matches!(
state,
AliceState::XmrLocked{..}
)
}
// State machine driver for swap execution // State machine driver for swap execution
#[async_recursion] #[async_recursion]
pub async fn run_until( pub async fn run_until(
state: AliceState, state: AliceState,
is_state: fn(&AliceState) -> bool, is_target_state: fn(&AliceState) -> bool,
mut swarm: Swarm, mut swarm: Swarm,
bitcoin_wallet: Arc<crate::bitcoin::Wallet>, bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
monero_wallet: Arc<crate::monero::Wallet>, monero_wallet: Arc<crate::monero::Wallet>,
config: Config, config: Config,
) -> Result<AliceState> { ) -> Result<(AliceState, Swarm)> {
if is_state(&state) { info!("{}", state);
Ok(state) if is_target_state(&state) {
Ok((state, swarm))
} else { } else {
match state { match state {
AliceState::Started { AliceState::Started {
amounts,
a,
s_a,
v_a,
} => {
let (channel, state3) = negotiate(
amounts, amounts,
a, a,
s_a, s_a,
v_a, v_a,
&mut swarm, } => {
bitcoin_wallet.clone(), let (channel, state3) = negotiate(
config,
)
.await?;
run_until(
AliceState::Negotiated {
channel,
amounts, amounts,
state3, a,
}, s_a,
is_state, v_a,
swarm, &mut swarm,
bitcoin_wallet, bitcoin_wallet.clone(),
monero_wallet, config,
config, )
)
.await
}
AliceState::Negotiated {
state3,
channel,
amounts,
} => {
let _ = wait_for_locked_bitcoin(state3.tx_lock.txid(), bitcoin_wallet.clone(), config)
.await?; .await?;
run_until( run_until(
AliceState::BtcLocked { AliceState::Negotiated {
channel, channel,
amounts, amounts,
state3, state3,
}, },
is_state, is_target_state,
swarm, swarm,
bitcoin_wallet, bitcoin_wallet,
monero_wallet, monero_wallet,
config, config,
) )
.await .await
} }
AliceState::BtcLocked { AliceState::Negotiated {
channel, state3,
amounts,
state3,
} => {
lock_xmr(
channel, channel,
amounts, amounts,
state3.clone(), } => {
&mut swarm, let _ =
monero_wallet.clone(), wait_for_locked_bitcoin(state3.tx_lock.txid(), bitcoin_wallet.clone(), config)
) .await?;
.await?;
run_until( run_until(
AliceState::XmrLocked { state3 }, AliceState::BtcLocked {
is_state, channel,
swarm, amounts,
bitcoin_wallet, state3,
monero_wallet, },
config, is_target_state,
) swarm,
.await bitcoin_wallet,
} monero_wallet,
AliceState::XmrLocked { state3 } => { config,
// Our Monero is locked, we need to go through the cancellation process if this )
// step fails .await
match wait_for_bitcoin_encrypted_signature(&mut swarm).await { }
Ok(encrypted_signature) => { AliceState::BtcLocked {
run_until( channel,
AliceState::EncSignLearned { amounts,
state3, state3,
encrypted_signature, } => {
}, lock_xmr(
is_state, channel,
swarm, amounts,
bitcoin_wallet, state3.clone(),
monero_wallet, &mut swarm,
config, monero_wallet.clone(),
) )
.await .await?;
}
Err(_) => { run_until(
run_until( AliceState::XmrLocked { state3 },
AliceState::WaitingToCancel { state3 }, is_target_state,
is_state, swarm,
swarm, bitcoin_wallet,
bitcoin_wallet, monero_wallet,
monero_wallet, config,
config, )
) .await
.await }
AliceState::XmrLocked { state3 } => {
// Our Monero is locked, we need to go through the cancellation process if this
// step fails
match wait_for_bitcoin_encrypted_signature(
&mut swarm,
config.monero_max_finality_time,
)
.await
{
Ok(encrypted_signature) => {
run_until(
AliceState::EncSignLearned {
state3,
encrypted_signature,
},
is_target_state,
swarm,
bitcoin_wallet,
monero_wallet,
config,
)
.await
}
Err(_) => {
run_until(
AliceState::WaitingToCancel { state3 },
is_target_state,
swarm,
bitcoin_wallet,
monero_wallet,
config,
)
.await
}
} }
} }
} AliceState::EncSignLearned {
AliceState::EncSignLearned { state3,
state3,
encrypted_signature,
} => {
let signed_tx_redeem = match build_bitcoin_redeem_transaction(
encrypted_signature, encrypted_signature,
&state3.tx_lock, } => {
state3.a.clone(), let signed_tx_redeem = match build_bitcoin_redeem_transaction(
state3.s_a, encrypted_signature,
state3.B, &state3.tx_lock,
&state3.redeem_address, state3.a.clone(),
) { state3.s_a,
Ok(tx) => tx, state3.B,
Err(_) => { &state3.redeem_address,
return run_until( ) {
AliceState::WaitingToCancel { state3 }, Ok(tx) => tx,
is_state, Err(_) => {
swarm, return run_until(
bitcoin_wallet, AliceState::WaitingToCancel { state3 },
monero_wallet, is_target_state,
config, swarm,
) bitcoin_wallet,
monero_wallet,
config,
)
.await;
}
};
// TODO(Franck): Error handling is delicate here.
// If Bob sees this transaction he can redeem Monero
// e.g. If the Bitcoin node is down then the user needs to take action.
publish_bitcoin_redeem_transaction(
signed_tx_redeem,
bitcoin_wallet.clone(),
config,
)
.await?;
run_until(
AliceState::BtcRedeemed,
is_target_state,
swarm,
bitcoin_wallet,
monero_wallet,
config,
)
.await
}
AliceState::WaitingToCancel { state3 } => {
let tx_cancel = publish_cancel_transaction(
state3.tx_lock.clone(),
state3.a.clone(),
state3.B,
state3.refund_timelock,
state3.tx_cancel_sig_bob.clone(),
bitcoin_wallet.clone(),
)
.await?;
run_until(
AliceState::BtcCancelled { state3, tx_cancel },
is_target_state,
swarm,
bitcoin_wallet,
monero_wallet,
config,
)
.await
}
AliceState::BtcCancelled { state3, tx_cancel } => {
let tx_cancel_height = bitcoin_wallet
.transaction_block_height(tx_cancel.txid())
.await; .await;
}
};
// TODO(Franck): Error handling is delicate here. let (tx_refund, published_refund_tx) = wait_for_bitcoin_refund(
// If Bob sees this transaction he can redeem Monero &tx_cancel,
// e.g. If the Bitcoin node is down then the user needs to take action. tx_cancel_height,
publish_bitcoin_redeem_transaction(signed_tx_redeem, bitcoin_wallet.clone(), config) state3.punish_timelock,
&state3.refund_address,
bitcoin_wallet.clone(),
)
.await?; .await?;
run_until( // TODO(Franck): Review error handling
AliceState::BtcRedeemed, match published_refund_tx {
is_state, None => {
swarm, run_until(
bitcoin_wallet, AliceState::BtcPunishable { tx_refund, state3 },
monero_wallet, is_target_state,
config, swarm,
) bitcoin_wallet.clone(),
.await monero_wallet,
} config,
AliceState::WaitingToCancel { state3 } => { )
let tx_cancel = publish_cancel_transaction( .await
state3.tx_lock.clone(), }
state3.a.clone(), Some(published_refund_tx) => {
state3.B, run_until(
state3.refund_timelock, AliceState::BtcRefunded {
state3.tx_cancel_sig_bob.clone(), tx_refund,
bitcoin_wallet.clone(), published_refund_tx,
) state3,
.await?; },
is_target_state,
run_until( swarm,
AliceState::BtcCancelled { state3, tx_cancel }, bitcoin_wallet.clone(),
is_state, monero_wallet,
swarm, config,
bitcoin_wallet, )
monero_wallet, .await
config, }
)
.await
}
AliceState::BtcCancelled { state3, tx_cancel } => {
let tx_cancel_height = bitcoin_wallet
.transaction_block_height(tx_cancel.txid())
.await;
let (tx_refund, published_refund_tx) = wait_for_bitcoin_refund(
&tx_cancel,
tx_cancel_height,
state3.punish_timelock,
&state3.refund_address,
bitcoin_wallet.clone(),
)
.await?;
// TODO(Franck): Review error handling
match published_refund_tx {
None => {
run_until(
AliceState::BtcPunishable { tx_refund, state3 },
is_state,
swarm,
bitcoin_wallet.clone(),
monero_wallet,
config,
)
.await
}
Some(published_refund_tx) => {
run_until(
AliceState::BtcRefunded {
tx_refund,
published_refund_tx,
state3,
},
is_state,
swarm,
bitcoin_wallet.clone(),
monero_wallet,
config,
)
.await
} }
} }
} AliceState::BtcRefunded {
AliceState::BtcRefunded {
tx_refund,
published_refund_tx,
state3,
} => {
let spend_key = extract_monero_private_key(
published_refund_tx,
tx_refund, tx_refund,
state3.s_a, published_refund_tx,
state3.a.clone(), state3,
state3.S_b_bitcoin, } => {
)?; let spend_key = extract_monero_private_key(
let view_key = state3.v; published_refund_tx,
tx_refund,
state3.s_a,
state3.a.clone(),
state3.S_b_bitcoin,
)?;
let view_key = state3.v;
monero_wallet monero_wallet
.create_and_load_wallet_for_output(spend_key, view_key) .create_and_load_wallet_for_output(spend_key, view_key)
.await?; .await?;
Ok(AliceState::XmrRefunded) Ok((AliceState::XmrRefunded, swarm))
} }
AliceState::BtcPunishable { tx_refund, state3 } => { AliceState::BtcPunishable { tx_refund, state3 } => {
let signed_tx_punish = build_bitcoin_punish_transaction( let signed_tx_punish = build_bitcoin_punish_transaction(
&state3.tx_lock, &state3.tx_lock,
state3.refund_timelock, state3.refund_timelock,
&state3.punish_address, &state3.punish_address,
state3.punish_timelock, state3.punish_timelock,
state3.tx_punish_sig_bob.clone(), state3.tx_punish_sig_bob.clone(),
state3.a.clone(), state3.a.clone(),
state3.B, state3.B,
)?; )?;
let punish_tx_finalised = publish_bitcoin_punish_transaction( let punish_tx_finalised = publish_bitcoin_punish_transaction(
signed_tx_punish, signed_tx_punish,
bitcoin_wallet.clone(), bitcoin_wallet.clone(),
config, config,
); );
let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid());
pin_mut!(punish_tx_finalised); pin_mut!(punish_tx_finalised);
pin_mut!(refund_tx_seen); pin_mut!(refund_tx_seen);
match select(punish_tx_finalised, refund_tx_seen).await { match select(punish_tx_finalised, refund_tx_seen).await {
Either::Left(_) => { Either::Left(_) => {
run_until( run_until(
AliceState::Punished, AliceState::Punished,
is_state, is_target_state,
swarm, swarm,
bitcoin_wallet.clone(), bitcoin_wallet.clone(),
monero_wallet, monero_wallet,
config, config,
) )
.await .await
} }
Either::Right((published_refund_tx, _)) => { Either::Right((published_refund_tx, _)) => {
run_until( run_until(
AliceState::BtcRefunded { AliceState::BtcRefunded {
tx_refund, tx_refund,
published_refund_tx, published_refund_tx,
state3, state3,
}, },
is_state, is_target_state,
swarm, swarm,
bitcoin_wallet.clone(), bitcoin_wallet.clone(),
monero_wallet, monero_wallet,
config, config,
) )
.await .await
}
} }
} }
AliceState::XmrRefunded => Ok((AliceState::XmrRefunded, swarm)),
AliceState::BtcRedeemed => Ok((AliceState::BtcRedeemed, swarm)),
AliceState::Punished => Ok((AliceState::Punished, swarm)),
AliceState::SafelyAborted => Ok((AliceState::SafelyAborted, swarm)),
} }
AliceState::XmrRefunded => Ok(AliceState::XmrRefunded),
AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed),
AliceState::Punished => Ok(AliceState::Punished),
AliceState::SafelyAborted => Ok(AliceState::SafelyAborted),
}
} }
} }

View File

@ -7,8 +7,8 @@ use anyhow::Result;
use async_recursion::async_recursion; use async_recursion::async_recursion;
use libp2p::{core::Multiaddr, PeerId}; use libp2p::{core::Multiaddr, PeerId};
use rand::{CryptoRng, RngCore}; use rand::{CryptoRng, RngCore};
use std::sync::Arc; use std::{fmt, sync::Arc};
use tracing::debug; use tracing::{debug, info};
use uuid::Uuid; use uuid::Uuid;
use xmr_btc::bob::{self}; use xmr_btc::bob::{self};
@ -33,6 +33,24 @@ pub enum BobState {
SafelyAborted, SafelyAborted,
} }
impl fmt::Display for BobState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BobState::Started { .. } => write!(f, "started"),
BobState::Negotiated(..) => write!(f, "negotiated"),
BobState::BtcLocked(..) => write!(f, "btc_locked"),
BobState::XmrLocked(..) => write!(f, "xmr_locked"),
BobState::EncSigSent(..) => write!(f, "encsig_sent"),
BobState::BtcRedeemed(_) => write!(f, "btc_redeemed"),
BobState::Cancelled(_) => write!(f, "cancelled"),
BobState::BtcRefunded => write!(f, "btc_refunded"),
BobState::XmrRedeemed => write!(f, "xmr_redeemed"),
BobState::Punished => write!(f, "punished"),
BobState::SafelyAborted => write!(f, "safely_aborted"),
}
}
}
pub async fn swap<R>( pub async fn swap<R>(
state: BobState, state: BobState,
swarm: Swarm, swarm: Swarm,
@ -42,22 +60,47 @@ pub async fn swap<R>(
rng: R, rng: R,
swap_id: Uuid, swap_id: Uuid,
) -> Result<BobState> ) -> Result<BobState>
where where
R: RngCore + CryptoRng + Send, R: RngCore + CryptoRng + Send,
{ {
run_until(state, is_complete, swarm, db, bitcoin_wallet, monero_wallet, rng, swap_id).await run_until(
state,
is_complete,
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
swap_id,
)
.await
} }
// TODO: use macro or generics // TODO: use macro or generics
pub fn is_complete(state: &BobState) -> bool { pub fn is_complete(state: &BobState) -> bool {
matches!(state, BobState::BtcRefunded| BobState::XmrRedeemed | BobState::Punished | BobState::SafelyAborted) matches!(
state,
BobState::BtcRefunded
| BobState::XmrRedeemed
| BobState::Punished
| BobState::SafelyAborted
)
}
pub fn is_btc_locked(state: &BobState) -> bool {
matches!(state, BobState::BtcLocked(..))
}
pub fn is_xmr_locked(state: &BobState) -> bool {
matches!(state, BobState::XmrLocked(..))
} }
// State machine driver for swap execution // State machine driver for swap execution
#[allow(clippy::too_many_arguments)]
#[async_recursion] #[async_recursion]
pub async fn run_until<R>( pub async fn run_until<R>(
state: BobState, state: BobState,
is_state: fn(&BobState) -> bool, is_target_state: fn(&BobState) -> bool,
mut swarm: Swarm, mut swarm: Swarm,
db: Database, db: Database,
bitcoin_wallet: Arc<crate::bitcoin::Wallet>, bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
@ -68,7 +111,8 @@ pub async fn run_until<R>(
where where
R: RngCore + CryptoRng + Send, R: RngCore + CryptoRng + Send,
{ {
if is_state(&state) { info!("{}", state);
if is_target_state(&state) {
Ok(state) Ok(state)
} else { } else {
match state { match state {
@ -86,10 +130,10 @@ where
&mut rng, &mut rng,
bitcoin_wallet.clone(), bitcoin_wallet.clone(),
) )
.await?; .await?;
run_until( run_until(
BobState::Negotiated(state2, peer_id), BobState::Negotiated(state2, peer_id),
is_state, is_target_state,
swarm, swarm,
db, db,
bitcoin_wallet, bitcoin_wallet,
@ -97,7 +141,7 @@ where
rng, rng,
swap_id, swap_id,
) )
.await .await
} }
BobState::Negotiated(state2, alice_peer_id) => { BobState::Negotiated(state2, alice_peer_id) => {
// Alice and Bob have exchanged info // Alice and Bob have exchanged info
@ -105,7 +149,7 @@ where
// db.insert_latest_state(state); // db.insert_latest_state(state);
run_until( run_until(
BobState::BtcLocked(state3, alice_peer_id), BobState::BtcLocked(state3, alice_peer_id),
is_state, is_target_state,
swarm, swarm,
db, db,
bitcoin_wallet, bitcoin_wallet,
@ -113,7 +157,7 @@ where
rng, rng,
swap_id, swap_id,
) )
.await .await
} }
// Bob has locked Btc // Bob has locked Btc
// Watch for Alice to Lock Xmr or for t1 to elapse // Watch for Alice to Lock Xmr or for t1 to elapse
@ -129,7 +173,7 @@ where
}; };
run_until( run_until(
BobState::XmrLocked(state4, alice_peer_id), BobState::XmrLocked(state4, alice_peer_id),
is_state, is_target_state,
swarm, swarm,
db, db,
bitcoin_wallet, bitcoin_wallet,
@ -137,7 +181,7 @@ where
rng, rng,
swap_id, swap_id,
) )
.await .await
} }
BobState::XmrLocked(state, alice_peer_id) => { BobState::XmrLocked(state, alice_peer_id) => {
// Alice has locked Xmr // Alice has locked Xmr
@ -162,7 +206,7 @@ where
run_until( run_until(
BobState::EncSigSent(state, alice_peer_id), BobState::EncSigSent(state, alice_peer_id),
is_state, is_target_state,
swarm, swarm,
db, db,
bitcoin_wallet, bitcoin_wallet,
@ -170,7 +214,7 @@ where
rng, rng,
swap_id, swap_id,
) )
.await .await
} }
BobState::EncSigSent(state, ..) => { BobState::EncSigSent(state, ..) => {
// Watch for redeem // Watch for redeem
@ -178,47 +222,47 @@ where
let t1_timeout = state.wait_for_t1(bitcoin_wallet.as_ref()); let t1_timeout = state.wait_for_t1(bitcoin_wallet.as_ref());
tokio::select! { tokio::select! {
val = redeem_watcher => { val = redeem_watcher => {
run_until( run_until(
BobState::BtcRedeemed(val?), BobState::BtcRedeemed(val?),
is_state, is_target_state,
swarm, swarm,
db, db,
bitcoin_wallet, bitcoin_wallet,
monero_wallet, monero_wallet,
rng, rng,
swap_id, swap_id,
) )
.await .await
}
_ = t1_timeout => {
// Check whether TxCancel has been published.
// We should not fail if the transaction is already on the blockchain
if state.check_for_tx_cancel(bitcoin_wallet.as_ref()).await.is_err() {
state.submit_tx_cancel(bitcoin_wallet.as_ref()).await?;
} }
_ = t1_timeout => {
// Check whether TxCancel has been published.
// We should not fail if the transaction is already on the blockchain
if state.check_for_tx_cancel(bitcoin_wallet.as_ref()).await.is_err() {
state.submit_tx_cancel(bitcoin_wallet.as_ref()).await?;
}
run_until( run_until(
BobState::Cancelled(state), BobState::Cancelled(state),
is_state, is_target_state,
swarm, swarm,
db, db,
bitcoin_wallet, bitcoin_wallet,
monero_wallet, monero_wallet,
rng, rng,
swap_id swap_id
) )
.await .await
}
} }
} }
}
BobState::BtcRedeemed(state) => { BobState::BtcRedeemed(state) => {
// Bob redeems XMR using revealed s_a // Bob redeems XMR using revealed s_a
state.claim_xmr(monero_wallet.as_ref()).await?; state.claim_xmr(monero_wallet.as_ref()).await?;
run_until( run_until(
BobState::XmrRedeemed, BobState::XmrRedeemed,
is_state, is_target_state,
swarm, swarm,
db, db,
bitcoin_wallet, bitcoin_wallet,
@ -226,21 +270,40 @@ where
rng, rng,
swap_id, swap_id,
) )
.await .await
}
BobState::Cancelled(_state) => {
// Bob has cancelled the swap
// If <t2 Bob refunds
// if unimplemented!("<t2") {
// // Submit TxRefund
// abort(BobState::BtcRefunded, io).await
// } else {
// // Bob failed to refund in time and has been punished
// abort(BobState::Punished, io).await
// }
Ok(BobState::BtcRefunded)
}
BobState::BtcRefunded => {
info!("btc refunded");
Ok(BobState::BtcRefunded)
}
BobState::Punished => {
info!("punished");
Ok(BobState::Punished)
}
BobState::SafelyAborted => {
info!("safely aborted");
Ok(BobState::SafelyAborted)
}
BobState::XmrRedeemed => {
info!("xmr redeemed");
Ok(BobState::XmrRedeemed)
} }
BobState::Cancelled(_state) => Ok(BobState::BtcRefunded),
BobState::BtcRefunded => Ok(BobState::BtcRefunded),
BobState::Punished => Ok(BobState::Punished),
BobState::SafelyAborted => Ok(BobState::SafelyAborted),
BobState::XmrRedeemed => Ok(BobState::XmrRedeemed),
} }
} }
} }
// // State machine driver for recovery execution // // State machine driver for recovery execution
// #[async_recursion] // #[async_recursion]
// pub async fn abort(state: BobState, io: Io) -> Result<BobState> { // pub async fn abort(state: BobState, io: Io) -> Result<BobState> {

View File

@ -1,6 +1,6 @@
use bitcoin_harness::Bitcoind; use bitcoin_harness::Bitcoind;
use futures::{channel::mpsc, future::try_join}; use futures::future::try_join;
use libp2p::Multiaddr; use libp2p::{Multiaddr, PeerId};
use monero_harness::Monero; use monero_harness::Monero;
use rand::rngs::OsRng; use rand::rngs::OsRng;
use std::sync::Arc; use std::sync::Arc;
@ -11,25 +11,20 @@ use swap::{
use tempfile::tempdir; use tempfile::tempdir;
use testcontainers::clients::Cli; use testcontainers::clients::Cli;
use uuid::Uuid; use uuid::Uuid;
use xmr_btc::{bitcoin, cross_curve_dleq}; use xmr_btc::{bitcoin, config::Config, cross_curve_dleq};
/// Run the following tests with RUST_MIN_STACK=100000000000
#[ignore]
#[tokio::test] #[tokio::test]
async fn swap() { async fn happy_path() {
use tracing_subscriber::util::SubscriberInitExt as _; init_tracing();
let _guard = tracing_subscriber::fmt()
.with_env_filter("swap=info,xmr_btc=info")
.with_ansi(false)
.set_default();
let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9876"
.parse()
.expect("failed to parse Alice's address");
let cli = Cli::default(); let cli = Cli::default();
let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap(); let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap();
dbg!(&bitcoind.node_url);
let _ = bitcoind.init(5).await; let _ = bitcoind.init(5).await;
let (monero, _container) =
Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()])
.await
.unwrap();
let btc = bitcoin::Amount::from_sat(1_000_000); let btc = bitcoin::Amount::from_sat(1_000_000);
let btc_alice = bitcoin::Amount::ZERO; let btc_alice = bitcoin::Amount::ZERO;
@ -41,197 +36,35 @@ async fn swap() {
let xmr_alice = xmr * 10; let xmr_alice = xmr * 10;
let xmr_bob = 0; let xmr_bob = 0;
let alice_btc_wallet = Arc::new( let (
swap::bitcoin::Wallet::new("alice", bitcoind.node_url.clone()) alice_state,
.await alice_swarm,
.unwrap(), alice_btc_wallet,
); alice_xmr_wallet,
let bob_btc_wallet = Arc::new( alice_peer_id,
swap::bitcoin::Wallet::new("bob", bitcoind.node_url.clone())
.await
.unwrap(),
);
bitcoind
.mint(bob_btc_wallet.0.new_address().await.unwrap(), btc_bob)
.await
.unwrap();
let (monero, _container) =
Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()])
.await
.unwrap();
monero
.init(vec![("alice", xmr_alice), ("bob", xmr_bob)])
.await
.unwrap();
let alice_xmr_wallet = Arc::new(swap::monero::Wallet(
monero.wallet("alice").unwrap().client(),
));
let bob_xmr_wallet = Arc::new(swap::monero::Wallet(monero.wallet("bob").unwrap().client()));
let alice_behaviour = alice::Behaviour::default();
let alice_transport = build(alice_behaviour.identity()).unwrap();
let db = Database::open(std::path::Path::new("../.swap-db/")).unwrap();
let alice_swap = alice::swap(
alice_btc_wallet.clone(),
alice_xmr_wallet.clone(),
db,
alice_multiaddr.clone(),
alice_transport,
alice_behaviour,
);
let db_dir = tempdir().unwrap();
let db = Database::open(db_dir.path()).unwrap();
let (cmd_tx, mut _cmd_rx) = mpsc::channel(1);
let (mut rsp_tx, rsp_rx) = mpsc::channel(1);
let bob_behaviour = bob::Behaviour::default();
let bob_transport = build(bob_behaviour.identity()).unwrap();
let bob_swap = bob::swap(
bob_btc_wallet.clone(),
bob_xmr_wallet.clone(),
db,
btc.as_sat(),
alice_multiaddr, alice_multiaddr,
cmd_tx, ) = init_alice(&bitcoind, &monero, btc, btc_alice, xmr, xmr_alice).await;
rsp_rx,
bob_transport,
bob_behaviour,
);
// automate the verification step by accepting any amounts sent over by Alice let (bob_state, bob_swarm, bob_btc_wallet, bob_xmr_wallet, bob_db) = init_bob(
rsp_tx.try_send(swap::Rsp::VerifiedAmounts).unwrap(); alice_multiaddr,
alice_peer_id,
try_join(alice_swap, bob_swap).await.unwrap(); &bitcoind,
&monero,
let btc_alice_final = alice_btc_wallet.as_ref().balance().await.unwrap();
let btc_bob_final = bob_btc_wallet.as_ref().balance().await.unwrap();
let xmr_alice_final = alice_xmr_wallet.as_ref().get_balance().await.unwrap();
bob_xmr_wallet.as_ref().0.refresh().await.unwrap();
let xmr_bob_final = bob_xmr_wallet.as_ref().get_balance().await.unwrap();
assert_eq!(
btc_alice_final,
btc_alice + btc - bitcoin::Amount::from_sat(bitcoin::TX_FEE)
);
assert!(btc_bob_final <= btc_bob - btc);
assert!(xmr_alice_final.as_piconero() <= xmr_alice - xmr);
assert_eq!(xmr_bob_final.as_piconero(), xmr_bob + xmr);
}
#[tokio::test]
async fn happy_path_recursive_executor() {
use tracing_subscriber::util::SubscriberInitExt as _;
let _guard = tracing_subscriber::fmt()
.with_env_filter("swap=info,xmr_btc=info")
.with_ansi(false)
.set_default();
let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9876"
.parse()
.expect("failed to parse Alice's address");
let cli = Cli::default();
let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap();
dbg!(&bitcoind.node_url);
let _ = bitcoind.init(5).await;
let btc = bitcoin::Amount::from_sat(1_000_000);
let btc_alice = bitcoin::Amount::ZERO;
let btc_bob = btc * 10;
// this xmr value matches the logic of alice::calculate_amounts i.e. btc *
// 10_000 * 100
let xmr = 1_000_000_000_000;
let xmr_alice = xmr * 10;
let xmr_bob = 0;
let alice_btc_wallet = Arc::new(
swap::bitcoin::Wallet::new("alice", bitcoind.node_url.clone())
.await
.unwrap(),
);
let bob_btc_wallet = Arc::new(
swap::bitcoin::Wallet::new("bob", bitcoind.node_url.clone())
.await
.unwrap(),
);
bitcoind
.mint(bob_btc_wallet.0.new_address().await.unwrap(), btc_bob)
.await
.unwrap();
let (monero, _container) =
Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()])
.await
.unwrap();
monero
.init(vec![("alice", xmr_alice), ("bob", xmr_bob)])
.await
.unwrap();
let alice_xmr_wallet = Arc::new(swap::monero::Wallet(
monero.wallet("alice").unwrap().client(),
));
let bob_xmr_wallet = Arc::new(swap::monero::Wallet(monero.wallet("bob").unwrap().client()));
let amounts = SwapAmounts {
btc, btc,
xmr: xmr_btc::monero::Amount::from_piconero(xmr), btc_bob,
}; xmr,
xmr_bob,
)
.await;
let alice_behaviour = alice::Behaviour::default();
let alice_peer_id = alice_behaviour.peer_id().clone();
let alice_transport = build(alice_behaviour.identity()).unwrap();
let rng = &mut OsRng;
let alice_state = {
let a = bitcoin::SecretKey::new_random(rng);
let s_a = cross_curve_dleq::Scalar::random(rng);
let v_a = xmr_btc::monero::PrivateViewKey::new_random(rng);
AliceState::Started {
amounts,
a,
s_a,
v_a,
}
};
let alice_swarm =
alice::new_swarm(alice_multiaddr.clone(), alice_transport, alice_behaviour).unwrap();
let config = xmr_btc::config::Config::regtest();
let alice_swap = alice::swap::swap( let alice_swap = alice::swap::swap(
alice_state, alice_state,
alice_swarm, alice_swarm,
alice_btc_wallet.clone(), alice_btc_wallet.clone(),
alice_xmr_wallet.clone(), alice_xmr_wallet.clone(),
config, Config::regtest(),
); );
let bob_db_dir = tempdir().unwrap();
let bob_db = Database::open(bob_db_dir.path()).unwrap();
let bob_behaviour = bob::Behaviour::default();
let bob_transport = build(bob_behaviour.identity()).unwrap();
let refund_address = bob_btc_wallet.new_address().await.unwrap();
let state0 = xmr_btc::bob::State0::new(
rng,
btc,
xmr_btc::monero::Amount::from_piconero(xmr),
REFUND_TIMELOCK,
PUNISH_TIMELOCK,
refund_address,
);
let bob_state = BobState::Started {
state0,
amounts,
peer_id: alice_peer_id,
addr: alice_multiaddr,
};
let bob_swarm = bob::new_swarm(bob_transport, bob_behaviour).unwrap();
let bob_swap = bob::swap::swap( let bob_swap = bob::swap::swap(
bob_state, bob_state,
bob_swarm, bob_swarm,
@ -261,3 +94,241 @@ async fn happy_path_recursive_executor() {
assert!(xmr_alice_final.as_piconero() <= xmr_alice - xmr); assert!(xmr_alice_final.as_piconero() <= xmr_alice - xmr);
assert_eq!(xmr_bob_final.as_piconero(), xmr_bob + xmr); assert_eq!(xmr_bob_final.as_piconero(), xmr_bob + xmr);
} }
/// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice
/// the encsig and fail to refund or redeem. Alice punishes.
#[tokio::test]
async fn alice_punishes_if_bob_never_acts_after_fund() {
init_tracing();
let cli = Cli::default();
let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap();
let _ = bitcoind.init(5).await;
let (monero, _container) =
Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()])
.await
.unwrap();
let btc_to_swap = bitcoin::Amount::from_sat(1_000_000);
let xmr_to_swap = 1_000_000_000_000;
let bob_btc_starting_balance = btc_to_swap * 10;
let bob_xmr_starting_balance = 0;
let alice_btc_starting_balance = bitcoin::Amount::ZERO;
let alice_xmr_starting_balance = xmr_to_swap * 10;
let (
alice_state,
alice_swarm,
alice_btc_wallet,
alice_xmr_wallet,
alice_peer_id,
alice_multiaddr,
) = init_alice(
&bitcoind,
&monero,
btc_to_swap,
alice_btc_starting_balance,
xmr_to_swap,
alice_xmr_starting_balance,
)
.await;
let (bob_state, bob_swarm, bob_btc_wallet, bob_xmr_wallet, bob_db) = init_bob(
alice_multiaddr,
alice_peer_id,
&bitcoind,
&monero,
btc_to_swap,
bob_btc_starting_balance,
xmr_to_swap,
bob_xmr_starting_balance,
)
.await;
let bob_xmr_locked_fut = bob::swap::run_until(
bob_state,
bob::swap::is_xmr_locked,
bob_swarm,
bob_db,
bob_btc_wallet.clone(),
bob_xmr_wallet.clone(),
OsRng,
Uuid::new_v4(),
);
let alice_xmr_locked_fut = alice::swap::run_until(
alice_state,
alice::swap::is_xmr_locked,
alice_swarm,
alice_btc_wallet.clone(),
alice_xmr_wallet.clone(),
Config::regtest(),
);
// Wait until alice has locked xmr and bob has locked btc
let ((alice_state, alice_swarm), _bob_state) =
try_join(alice_xmr_locked_fut, bob_xmr_locked_fut)
.await
.unwrap();
let (punished, _) = alice::swap::swap(
alice_state,
alice_swarm,
alice_btc_wallet.clone(),
alice_xmr_wallet.clone(),
Config::regtest(),
)
.await
.unwrap();
assert!(matches!(punished, AliceState::Punished));
// todo: Add balance assertions
}
#[allow(clippy::too_many_arguments)]
async fn init_alice(
bitcoind: &Bitcoind<'_>,
monero: &Monero,
btc_to_swap: bitcoin::Amount,
_btc_starting_balance: bitcoin::Amount,
xmr_to_swap: u64,
xmr_starting_balance: u64,
) -> (
AliceState,
alice::Swarm,
Arc<swap::bitcoin::Wallet>,
Arc<swap::monero::Wallet>,
PeerId,
Multiaddr,
) {
monero
.init(vec![("alice", xmr_starting_balance)])
.await
.unwrap();
let alice_xmr_wallet = Arc::new(swap::monero::Wallet(
monero.wallet("alice").unwrap().client(),
));
let alice_btc_wallet = Arc::new(
swap::bitcoin::Wallet::new("alice", bitcoind.node_url.clone())
.await
.unwrap(),
);
let amounts = SwapAmounts {
btc: btc_to_swap,
xmr: xmr_btc::monero::Amount::from_piconero(xmr_to_swap),
};
let alice_behaviour = alice::Behaviour::default();
let alice_peer_id = alice_behaviour.peer_id();
let alice_transport = build(alice_behaviour.identity()).unwrap();
let rng = &mut OsRng;
let alice_state = {
let a = bitcoin::SecretKey::new_random(rng);
let s_a = cross_curve_dleq::Scalar::random(rng);
let v_a = xmr_btc::monero::PrivateViewKey::new_random(rng);
AliceState::Started {
amounts,
a,
s_a,
v_a,
}
};
let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9876"
.parse()
.expect("failed to parse Alice's address");
let alice_swarm =
alice::new_swarm(alice_multiaddr.clone(), alice_transport, alice_behaviour).unwrap();
(
alice_state,
alice_swarm,
alice_btc_wallet,
alice_xmr_wallet,
alice_peer_id,
alice_multiaddr,
)
}
#[allow(clippy::too_many_arguments)]
async fn init_bob(
alice_multiaddr: Multiaddr,
alice_peer_id: PeerId,
bitcoind: &Bitcoind<'_>,
monero: &Monero,
btc_to_swap: bitcoin::Amount,
btc_starting_balance: bitcoin::Amount,
xmr_to_swap: u64,
xmr_stating_balance: u64,
) -> (
BobState,
bob::Swarm,
Arc<swap::bitcoin::Wallet>,
Arc<swap::monero::Wallet>,
Database,
) {
let bob_btc_wallet = Arc::new(
swap::bitcoin::Wallet::new("bob", bitcoind.node_url.clone())
.await
.unwrap(),
);
bitcoind
.mint(
bob_btc_wallet.0.new_address().await.unwrap(),
btc_starting_balance,
)
.await
.unwrap();
monero
.init(vec![("bob", xmr_stating_balance)])
.await
.unwrap();
let bob_xmr_wallet = Arc::new(swap::monero::Wallet(monero.wallet("bob").unwrap().client()));
let amounts = SwapAmounts {
btc: btc_to_swap,
xmr: xmr_btc::monero::Amount::from_piconero(xmr_to_swap),
};
let rng = &mut OsRng;
let bob_db_dir = tempdir().unwrap();
let bob_db = Database::open(bob_db_dir.path()).unwrap();
let bob_behaviour = bob::Behaviour::default();
let bob_transport = build(bob_behaviour.identity()).unwrap();
let refund_address = bob_btc_wallet.new_address().await.unwrap();
let state0 = xmr_btc::bob::State0::new(
rng,
btc_to_swap,
xmr_btc::monero::Amount::from_piconero(xmr_to_swap),
REFUND_TIMELOCK,
PUNISH_TIMELOCK,
refund_address,
);
let bob_state = BobState::Started {
state0,
amounts,
peer_id: alice_peer_id,
addr: alice_multiaddr,
};
let bob_swarm = bob::new_swarm(bob_transport, bob_behaviour).unwrap();
(bob_state, bob_swarm, bob_btc_wallet, bob_xmr_wallet, bob_db)
}
fn init_tracing() {
use tracing_subscriber::util::SubscriberInitExt as _;
let _guard = tracing_subscriber::fmt()
.with_env_filter("swap=info,xmr_btc=info")
.with_ansi(false)
.set_default();
}

View File

@ -799,6 +799,9 @@ impl State4 {
t1_timeout.await; t1_timeout.await;
Ok(()) Ok(())
} }
pub fn tx_lock_id(&self) -> bitcoin::Txid {
self.tx_lock.txid()
}
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]

View File

@ -6,6 +6,7 @@ pub struct Config {
pub bob_time_to_act: Duration, pub bob_time_to_act: Duration,
pub bitcoin_finality_confirmations: u32, pub bitcoin_finality_confirmations: u32,
pub bitcoin_avg_block_time: Duration, pub bitcoin_avg_block_time: Duration,
pub monero_max_finality_time: Duration,
} }
impl Config { impl Config {
@ -14,6 +15,7 @@ impl Config {
bob_time_to_act: *mainnet::BOB_TIME_TO_ACT, bob_time_to_act: *mainnet::BOB_TIME_TO_ACT,
bitcoin_finality_confirmations: mainnet::BITCOIN_FINALITY_CONFIRMATIONS, bitcoin_finality_confirmations: mainnet::BITCOIN_FINALITY_CONFIRMATIONS,
bitcoin_avg_block_time: *mainnet::BITCOIN_AVG_BLOCK_TIME, bitcoin_avg_block_time: *mainnet::BITCOIN_AVG_BLOCK_TIME,
monero_max_finality_time: *mainnet::MONERO_MAX_FINALITY_TIME,
} }
} }
@ -22,6 +24,7 @@ impl Config {
bob_time_to_act: *regtest::BOB_TIME_TO_ACT, bob_time_to_act: *regtest::BOB_TIME_TO_ACT,
bitcoin_finality_confirmations: regtest::BITCOIN_FINALITY_CONFIRMATIONS, bitcoin_finality_confirmations: regtest::BITCOIN_FINALITY_CONFIRMATIONS,
bitcoin_avg_block_time: *regtest::BITCOIN_AVG_BLOCK_TIME, bitcoin_avg_block_time: *regtest::BITCOIN_AVG_BLOCK_TIME,
monero_max_finality_time: *regtest::MONERO_MAX_FINALITY_TIME,
} }
} }
} }
@ -35,15 +38,21 @@ mod mainnet {
pub static BITCOIN_FINALITY_CONFIRMATIONS: u32 = 3; pub static BITCOIN_FINALITY_CONFIRMATIONS: u32 = 3;
pub static BITCOIN_AVG_BLOCK_TIME: Lazy<Duration> = Lazy::new(|| Duration::from_secs(10 * 60)); pub static BITCOIN_AVG_BLOCK_TIME: Lazy<Duration> = Lazy::new(|| Duration::from_secs(10 * 60));
pub static MONERO_MAX_FINALITY_TIME: Lazy<Duration> =
Lazy::new(|| Duration::from_secs_f64(15f64 * 1.5 * 2f64 * 60f64));
} }
mod regtest { mod regtest {
use super::*; use super::*;
// In test, set to 5 seconds to fail fast // In test, set to 5 seconds to fail fast
pub static BOB_TIME_TO_ACT: Lazy<Duration> = Lazy::new(|| Duration::from_secs(10)); pub static BOB_TIME_TO_ACT: Lazy<Duration> = Lazy::new(|| Duration::from_secs(30));
pub static BITCOIN_FINALITY_CONFIRMATIONS: u32 = 1; pub static BITCOIN_FINALITY_CONFIRMATIONS: u32 = 1;
pub static BITCOIN_AVG_BLOCK_TIME: Lazy<Duration> = Lazy::new(|| Duration::from_secs(5)); pub static BITCOIN_AVG_BLOCK_TIME: Lazy<Duration> = Lazy::new(|| Duration::from_secs(5));
pub static MONERO_MAX_FINALITY_TIME: Lazy<Duration> =
Lazy::new(|| Duration::from_secs_f64(60f64));
} }