Merge pull request #54 from comit-network/bob-unhappy-paths

Punish Test
This commit is contained in:
rishflab 2020-12-09 15:51:16 +11:00 committed by GitHub
commit f88ed9183b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 882 additions and 683 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: 10000000
- name: Build binary - name: Build binary
run: | run: |

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,15 +87,75 @@ pub enum AliceState {
SafelyAborted, SafelyAborted,
} }
// State machine driver for swap execution impl fmt::Display for AliceState {
#[async_recursion] 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( pub async fn swap(
state: AliceState, state: AliceState,
swarm: Swarm,
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
monero_wallet: Arc<crate::monero::Wallet>,
config: Config,
) -> Result<(AliceState, Swarm)> {
run_until(
state,
is_complete,
swarm,
bitcoin_wallet,
monero_wallet,
config,
)
.await
}
pub fn is_complete(state: &AliceState) -> bool {
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
#[async_recursion]
pub async fn run_until(
state: AliceState,
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)> {
info!("Current state:{}", state);
if is_target_state(&state) {
Ok((state, swarm))
} else {
match state { match state {
AliceState::Started { AliceState::Started {
amounts, amounts,
@ -113,12 +174,13 @@ pub async fn swap(
) )
.await?; .await?;
swap( run_until(
AliceState::Negotiated { AliceState::Negotiated {
channel, channel,
amounts, amounts,
state3, state3,
}, },
is_target_state,
swarm, swarm,
bitcoin_wallet, bitcoin_wallet,
monero_wallet, monero_wallet,
@ -131,15 +193,17 @@ pub async fn swap(
channel, channel,
amounts, amounts,
} => { } => {
let _ = wait_for_locked_bitcoin(state3.tx_lock.txid(), bitcoin_wallet.clone(), config) let _ =
wait_for_locked_bitcoin(state3.tx_lock.txid(), bitcoin_wallet.clone(), config)
.await?; .await?;
swap( run_until(
AliceState::BtcLocked { AliceState::BtcLocked {
channel, channel,
amounts, amounts,
state3, state3,
}, },
is_target_state,
swarm, swarm,
bitcoin_wallet, bitcoin_wallet,
monero_wallet, monero_wallet,
@ -161,8 +225,9 @@ pub async fn swap(
) )
.await?; .await?;
swap( run_until(
AliceState::XmrLocked { state3 }, AliceState::XmrLocked { state3 },
is_target_state,
swarm, swarm,
bitcoin_wallet, bitcoin_wallet,
monero_wallet, monero_wallet,
@ -173,13 +238,19 @@ pub async fn swap(
AliceState::XmrLocked { state3 } => { AliceState::XmrLocked { state3 } => {
// Our Monero is locked, we need to go through the cancellation process if this // Our Monero is locked, we need to go through the cancellation process if this
// step fails // step fails
match wait_for_bitcoin_encrypted_signature(&mut swarm).await { match wait_for_bitcoin_encrypted_signature(
&mut swarm,
config.monero_max_finality_time,
)
.await
{
Ok(encrypted_signature) => { Ok(encrypted_signature) => {
swap( run_until(
AliceState::EncSignLearned { AliceState::EncSignLearned {
state3, state3,
encrypted_signature, encrypted_signature,
}, },
is_target_state,
swarm, swarm,
bitcoin_wallet, bitcoin_wallet,
monero_wallet, monero_wallet,
@ -188,8 +259,9 @@ pub async fn swap(
.await .await
} }
Err(_) => { Err(_) => {
swap( run_until(
AliceState::WaitingToCancel { state3 }, AliceState::WaitingToCancel { state3 },
is_target_state,
swarm, swarm,
bitcoin_wallet, bitcoin_wallet,
monero_wallet, monero_wallet,
@ -213,8 +285,9 @@ pub async fn swap(
) { ) {
Ok(tx) => tx, Ok(tx) => tx,
Err(_) => { Err(_) => {
return swap( return run_until(
AliceState::WaitingToCancel { state3 }, AliceState::WaitingToCancel { state3 },
is_target_state,
swarm, swarm,
bitcoin_wallet, bitcoin_wallet,
monero_wallet, monero_wallet,
@ -227,11 +300,16 @@ pub async fn swap(
// TODO(Franck): Error handling is delicate here. // TODO(Franck): Error handling is delicate here.
// If Bob sees this transaction he can redeem Monero // If Bob sees this transaction he can redeem Monero
// e.g. If the Bitcoin node is down then the user needs to take action. // 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) publish_bitcoin_redeem_transaction(
signed_tx_redeem,
bitcoin_wallet.clone(),
config,
)
.await?; .await?;
swap( run_until(
AliceState::BtcRedeemed, AliceState::BtcRedeemed,
is_target_state,
swarm, swarm,
bitcoin_wallet, bitcoin_wallet,
monero_wallet, monero_wallet,
@ -250,8 +328,9 @@ pub async fn swap(
) )
.await?; .await?;
swap( run_until(
AliceState::BtcCancelled { state3, tx_cancel }, AliceState::BtcCancelled { state3, tx_cancel },
is_target_state,
swarm, swarm,
bitcoin_wallet, bitcoin_wallet,
monero_wallet, monero_wallet,
@ -276,8 +355,9 @@ pub async fn swap(
// TODO(Franck): Review error handling // TODO(Franck): Review error handling
match published_refund_tx { match published_refund_tx {
None => { None => {
swap( run_until(
AliceState::BtcPunishable { tx_refund, state3 }, AliceState::BtcPunishable { tx_refund, state3 },
is_target_state,
swarm, swarm,
bitcoin_wallet.clone(), bitcoin_wallet.clone(),
monero_wallet, monero_wallet,
@ -286,12 +366,13 @@ pub async fn swap(
.await .await
} }
Some(published_refund_tx) => { Some(published_refund_tx) => {
swap( run_until(
AliceState::BtcRefunded { AliceState::BtcRefunded {
tx_refund, tx_refund,
published_refund_tx, published_refund_tx,
state3, state3,
}, },
is_target_state,
swarm, swarm,
bitcoin_wallet.clone(), bitcoin_wallet.clone(),
monero_wallet, monero_wallet,
@ -319,7 +400,7 @@ pub async fn swap(
.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(
@ -345,8 +426,9 @@ pub async fn swap(
match select(punish_tx_finalised, refund_tx_seen).await { match select(punish_tx_finalised, refund_tx_seen).await {
Either::Left(_) => { Either::Left(_) => {
swap( run_until(
AliceState::Punished, AliceState::Punished,
is_target_state,
swarm, swarm,
bitcoin_wallet.clone(), bitcoin_wallet.clone(),
monero_wallet, monero_wallet,
@ -355,12 +437,13 @@ pub async fn swap(
.await .await
} }
Either::Right((published_refund_tx, _)) => { Either::Right((published_refund_tx, _)) => {
swap( run_until(
AliceState::BtcRefunded { AliceState::BtcRefunded {
tx_refund, tx_refund,
published_refund_tx, published_refund_tx,
state3, state3,
}, },
is_target_state,
swarm, swarm,
bitcoin_wallet.clone(), bitcoin_wallet.clone(),
monero_wallet, monero_wallet,
@ -370,9 +453,10 @@ pub async fn swap(
} }
} }
} }
AliceState::XmrRefunded => Ok(AliceState::XmrRefunded), AliceState::XmrRefunded => Ok((AliceState::XmrRefunded, swarm)),
AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed), AliceState::BtcRedeemed => Ok((AliceState::BtcRedeemed, swarm)),
AliceState::Punished => Ok(AliceState::Punished), AliceState::Punished => Ok((AliceState::Punished, swarm)),
AliceState::SafelyAborted => Ok(AliceState::SafelyAborted), AliceState::SafelyAborted => Ok((AliceState::SafelyAborted, swarm)),
}
} }
} }

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,10 +33,73 @@ pub enum BobState {
SafelyAborted, SafelyAborted,
} }
// State machine driver for swap execution impl fmt::Display for BobState {
#[async_recursion] 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,
db: Database,
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
monero_wallet: Arc<crate::monero::Wallet>,
rng: R,
swap_id: Uuid,
) -> Result<BobState>
where
R: RngCore + CryptoRng + Send,
{
run_until(
state,
is_complete,
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
swap_id,
)
.await
}
pub fn is_complete(state: &BobState) -> bool {
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
#[allow(clippy::too_many_arguments)]
#[async_recursion]
pub async fn run_until<R>(
state: BobState,
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>,
@ -47,6 +110,10 @@ pub async fn swap<R>(
where where
R: RngCore + CryptoRng + Send, R: RngCore + CryptoRng + Send,
{ {
info!("Current state: {}", state);
if is_target_state(&state) {
Ok(state)
} else {
match state { match state {
BobState::Started { BobState::Started {
state0, state0,
@ -63,8 +130,9 @@ where
bitcoin_wallet.clone(), bitcoin_wallet.clone(),
) )
.await?; .await?;
swap( run_until(
BobState::Negotiated(state2, peer_id), BobState::Negotiated(state2, peer_id),
is_target_state,
swarm, swarm,
db, db,
bitcoin_wallet, bitcoin_wallet,
@ -78,8 +146,9 @@ where
// Alice and Bob have exchanged info // Alice and Bob have exchanged info
let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).await?; let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).await?;
// db.insert_latest_state(state); // db.insert_latest_state(state);
swap( run_until(
BobState::BtcLocked(state3, alice_peer_id), BobState::BtcLocked(state3, alice_peer_id),
is_target_state,
swarm, swarm,
db, db,
bitcoin_wallet, bitcoin_wallet,
@ -101,8 +170,9 @@ where
} }
other => panic!("unexpected event: {:?}", other), other => panic!("unexpected event: {:?}", other),
}; };
swap( run_until(
BobState::XmrLocked(state4, alice_peer_id), BobState::XmrLocked(state4, alice_peer_id),
is_target_state,
swarm, swarm,
db, db,
bitcoin_wallet, bitcoin_wallet,
@ -133,8 +203,9 @@ where
other => panic!("unexpected event: {:?}", other), other => panic!("unexpected event: {:?}", other),
}; };
swap( run_until(
BobState::EncSigSent(state, alice_peer_id), BobState::EncSigSent(state, alice_peer_id),
is_target_state,
swarm, swarm,
db, db,
bitcoin_wallet, bitcoin_wallet,
@ -151,8 +222,9 @@ where
tokio::select! { tokio::select! {
val = redeem_watcher => { val = redeem_watcher => {
swap( run_until(
BobState::BtcRedeemed(val?), BobState::BtcRedeemed(val?),
is_target_state,
swarm, swarm,
db, db,
bitcoin_wallet, bitcoin_wallet,
@ -169,8 +241,9 @@ where
state.submit_tx_cancel(bitcoin_wallet.as_ref()).await?; state.submit_tx_cancel(bitcoin_wallet.as_ref()).await?;
} }
swap( run_until(
BobState::Cancelled(state), BobState::Cancelled(state),
is_target_state,
swarm, swarm,
db, db,
bitcoin_wallet, bitcoin_wallet,
@ -186,8 +259,9 @@ where
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?;
swap( run_until(
BobState::XmrRedeemed, BobState::XmrRedeemed,
is_target_state,
swarm, swarm,
db, db,
bitcoin_wallet, bitcoin_wallet,
@ -197,69 +271,34 @@ where
) )
.await .await
} }
BobState::Cancelled(_state) => Ok(BobState::BtcRefunded), BobState::Cancelled(_state) => {
BobState::BtcRefunded => Ok(BobState::BtcRefunded), // Bob has cancelled the swap
BobState::Punished => Ok(BobState::Punished), // If <t2 Bob refunds
BobState::SafelyAborted => Ok(BobState::SafelyAborted), // if unimplemented!("<t2") {
BobState::XmrRedeemed => Ok(BobState::XmrRedeemed), // // 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)
}
}
} }
} }
// // State machine driver for recovery execution
// #[async_recursion]
// pub async fn abort(state: BobState, io: Io) -> Result<BobState> {
// match state {
// BobState::Started => {
// // Nothing has been commited by either party, abort swap.
// abort(BobState::SafelyAborted, io).await
// }
// BobState::Negotiated => {
// // Nothing has been commited by either party, abort swap.
// abort(BobState::SafelyAborted, io).await
// }
// BobState::BtcLocked => {
// // Bob has locked BTC and must refund it
// // Bob waits for alice to publish TxRedeem or t1
// if unimplemented!("TxRedeemSeen") {
// // Alice has redeemed revealing s_a
// abort(BobState::BtcRedeemed, io).await
// } else if unimplemented!("T1Elapsed") {
// // publish TxCancel or see if it has been published
// abort(BobState::Cancelled, io).await
// } else {
// Err(unimplemented!())
// }
// }
// BobState::XmrLocked => {
// // Alice has locked Xmr
// // Wait until t1
// if unimplemented!(">t1 and <t2") {
// // Bob publishes TxCancel
// abort(BobState::Cancelled, io).await
// } else {
// // >t2
// // submit TxCancel
// abort(BobState::Punished, io).await
// }
// }
// BobState::Cancelled => {
// // 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
// }
// }
// BobState::BtcRedeemed => {
// // Bob uses revealed s_a to redeem XMR
// abort(BobState::XmrRedeemed, io).await
// }
// BobState::BtcRefunded => Ok(BobState::BtcRefunded),
// BobState::Punished => Ok(BobState::Punished),
// BobState::SafelyAborted => Ok(BobState::SafelyAborted),
// BobState::XmrRedeemed => Ok(BobState::XmrRedeemed),
// }
// }

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;
@ -10,228 +10,71 @@ use swap::{
}; };
use tempfile::tempdir; use tempfile::tempdir;
use testcontainers::clients::Cli; use testcontainers::clients::Cli;
use tracing_subscriber::util::SubscriberInitExt as _;
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=10000000
#[ignore]
#[tokio::test] #[tokio::test]
async fn swap() { async fn happy_path() {
use tracing_subscriber::util::SubscriberInitExt as _;
let _guard = tracing_subscriber::fmt() let _guard = tracing_subscriber::fmt()
.with_env_filter("swap=info,xmr_btc=info") .with_env_filter("trace,hyper=warn")
.with_ansi(false)
.set_default(); .set_default();
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 btc_alice = bitcoin::Amount::ZERO;
let btc_bob = btc_to_swap * 10;
// this xmr value matches the logic of alice::calculate_amounts i.e. btc *
// 10_000 * 100
let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000);
let xmr_alice = xmr_to_swap * 10;
let xmr_bob = xmr_btc::monero::Amount::from_piconero(0);
let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9876" let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9876"
.parse() .parse()
.expect("failed to parse Alice's address"); .expect("failed to parse Alice's address");
let cli = Cli::default(); let (alice_state, alice_swarm, alice_btc_wallet, alice_xmr_wallet, alice_peer_id) = init_alice(
let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap(); &bitcoind,
dbg!(&bitcoind.node_url); &monero,
let _ = bitcoind.init(5).await; btc_to_swap,
btc_alice,
let btc = bitcoin::Amount::from_sat(1_000_000); xmr_to_swap,
let btc_alice = bitcoin::Amount::ZERO; xmr_alice,
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 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_multiaddr.clone(),
alice_transport, )
alice_behaviour, .await;
);
let db_dir = tempdir().unwrap(); let (bob_state, bob_swarm, bob_btc_wallet, bob_xmr_wallet, bob_db) = init_bob(
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, alice_peer_id,
rsp_rx, &bitcoind,
bob_transport, &monero,
bob_behaviour, btc_to_swap,
); btc_bob,
xmr_to_swap,
xmr_bob,
)
.await;
// automate the verification step by accepting any amounts sent over by Alice
rsp_tx.try_send(swap::Rsp::VerifiedAmounts).unwrap();
try_join(alice_swap, bob_swap).await.unwrap();
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,
xmr: xmr_btc::monero::Amount::from_piconero(xmr),
};
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,
@ -254,10 +97,219 @@ async fn happy_path_recursive_executor() {
assert_eq!( assert_eq!(
btc_alice_final, btc_alice_final,
btc_alice + btc - bitcoin::Amount::from_sat(bitcoin::TX_FEE) btc_alice + btc_to_swap - bitcoin::Amount::from_sat(bitcoin::TX_FEE)
); );
assert!(btc_bob_final <= btc_bob - btc); assert!(btc_bob_final <= btc_bob - btc_to_swap);
assert!(xmr_alice_final.as_piconero() <= xmr_alice - xmr); assert!(xmr_alice_final <= xmr_alice - xmr_to_swap);
assert_eq!(xmr_bob_final.as_piconero(), xmr_bob + xmr); assert_eq!(xmr_bob_final, xmr_bob + xmr_to_swap);
}
/// 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() {
let _guard = tracing_subscriber::fmt()
.with_env_filter("trace,hyper=warn")
.set_default();
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 = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000);
let bob_btc_starting_balance = btc_to_swap * 10;
let bob_xmr_starting_balance = xmr_btc::monero::Amount::from_piconero(0);
let alice_btc_starting_balance = bitcoin::Amount::ZERO;
let alice_xmr_starting_balance = xmr_to_swap * 10;
let alice_multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/9877"
.parse()
.expect("failed to parse Alice's address");
let (alice_state, alice_swarm, alice_btc_wallet, alice_xmr_wallet, alice_peer_id) = init_alice(
&bitcoind,
&monero,
btc_to_swap,
alice_btc_starting_balance,
xmr_to_swap,
alice_xmr_starting_balance,
alice_multiaddr.clone(),
)
.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_fut = alice::swap::swap(
alice_state,
alice_swarm,
alice_btc_wallet.clone(),
alice_xmr_wallet.clone(),
Config::regtest(),
);
// Wait until alice has locked xmr and bob h as locked btc
let ((alice_state, _), _bob_state) = try_join(alice_fut, bob_xmr_locked_fut).await.unwrap();
assert!(matches!(alice_state, 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: xmr_btc::monero::Amount,
xmr_starting_balance: xmr_btc::monero::Amount,
alice_multiaddr: Multiaddr,
) -> (
AliceState,
alice::Swarm,
Arc<swap::bitcoin::Wallet>,
Arc<swap::monero::Wallet>,
PeerId,
) {
monero
.init(vec![("alice", xmr_starting_balance.as_piconero())])
.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_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_swarm = alice::new_swarm(alice_multiaddr, alice_transport, alice_behaviour).unwrap();
(
alice_state,
alice_swarm,
alice_btc_wallet,
alice_xmr_wallet,
alice_peer_id,
)
}
#[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: xmr_btc::monero::Amount,
xmr_stating_balance: xmr_btc::monero::Amount,
) -> (
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.as_piconero())])
.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_to_swap,
};
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(
&mut OsRng,
btc_to_swap,
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)
} }

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,10 @@ 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,
// We apply a scaling factor (1.5) so that the swap is not aborted when the
// blockchain is slow
monero_max_finality_time: (*mainnet::MONERO_AVG_BLOCK_TIME).mul_f64(1.5)
* mainnet::MONERO_FINALITY_CONFIRMATIONS,
} }
} }
@ -22,6 +27,10 @@ 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,
// We apply a scaling factor (1.5) so that the swap is not aborted when the
// blockchain is slow
monero_max_finality_time: (*regtest::MONERO_AVG_BLOCK_TIME).mul_f64(1.5)
* regtest::MONERO_FINALITY_CONFIRMATIONS,
} }
} }
} }
@ -35,15 +44,23 @@ 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_FINALITY_CONFIRMATIONS: u32 = 15;
pub static MONERO_AVG_BLOCK_TIME: Lazy<Duration> = Lazy::new(|| Duration::from_secs(2 * 60));
} }
mod regtest { mod regtest {
use super::*; use super::*;
// In test, set to 5 seconds to fail fast // In test, we set a shorter time 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_FINALITY_CONFIRMATIONS: u32 = 1;
pub static MONERO_AVG_BLOCK_TIME: Lazy<Duration> = Lazy::new(|| Duration::from_secs(60));
} }

View File

@ -3,7 +3,7 @@ use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use rand::{CryptoRng, RngCore}; use rand::{CryptoRng, RngCore};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::ops::{Add, Sub}; use std::ops::{Add, Mul, Sub};
pub use curve25519_dalek::scalar::Scalar; pub use curve25519_dalek::scalar::Scalar;
pub use monero::*; pub use monero::*;
@ -97,6 +97,14 @@ impl Sub for Amount {
} }
} }
impl Mul<u64> for Amount {
type Output = Amount;
fn mul(self, rhs: u64) -> Self::Output {
Self(self.0 * rhs)
}
}
impl From<Amount> for u64 { impl From<Amount> for u64 {
fn from(from: Amount) -> u64 { fn from(from: Amount) -> u64 {
from.0 from.0