Merge pull request #36 from comit-network/recovery

Recover from a failed swap
pull/45/head
rishflab 4 years ago committed by GitHub
commit e7682a42a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

4
.gitignore vendored

@ -1,4 +1,3 @@
# Created by https://www.toptal.com/developers/gitignore/api/rust,clion+all,emacs
# Edit at https://www.toptal.com/developers/gitignore?templates=rust,clion+all,emacs
@ -154,4 +153,7 @@ Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# sled DB directory generated during local development
.swap-db/
# End of https://www.toptal.com/developers/gitignore/api/rust,clion+all,emacs

@ -14,19 +14,22 @@ base64 = "0.12"
bitcoin = { version = "0.23", features = ["rand", "use-serde"] } # TODO: Upgrade other crates in this repo to use this version.
bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "3be644cd9512c157d3337a189298b8257ed54d04" }
derivative = "2"
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat", "serde", "serialization"] }
futures = { version = "0.3", default-features = false }
genawaiter = "0.99.1"
libp2p = { version = "0.29", default-features = false, features = ["tcp-tokio", "yamux", "mplex", "dns", "noise", "request-response"] }
libp2p-tokio-socks5 = "0.4"
log = { version = "0.4", features = ["serde"] }
monero = "0.9"
monero = { version = "0.9", features = ["serde_support"] }
monero-harness = { path = "../monero-harness" }
prettytable-rs = "0.8"
rand = "0.7"
reqwest = { version = "0.10", default-features = false, features = ["socks"] }
serde = { version = "1", features = ["derive"] }
serde_cbor = "0.11"
serde_derive = "1.0"
serde_json = "1"
sha2 = "0.9"
sled = "0.34"
structopt = "0.3"
tempfile = "3"
@ -39,6 +42,7 @@ tracing-futures = { version = "0.2", features = ["std-future", "futures-03"] }
tracing-log = "0.1"
tracing-subscriber = { version = "0.2", default-features = false, features = ["fmt", "ansi", "env-filter"] }
url = "2.1"
uuid = { version = "0.8", features = ["serde", "v4"] }
void = "1"
xmr-btc = { path = "../xmr-btc" }

@ -13,6 +13,7 @@ use rand::rngs::OsRng;
use std::{sync::Arc, time::Duration};
use tokio::sync::Mutex;
use tracing::{debug, info, warn};
use uuid::Uuid;
mod amounts;
mod message0;
@ -31,6 +32,8 @@ use crate::{
transport::SwapTransport,
TokioExecutor,
},
state,
storage::Database,
SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
};
use xmr_btc::{
@ -43,6 +46,7 @@ use xmr_btc::{
pub async fn swap(
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
db: Database,
listen: Multiaddr,
transport: SwapTransport,
behaviour: Alice,
@ -71,7 +75,7 @@ pub async fn swap(
// to `ConstantBackoff`.
#[async_trait]
impl ReceiveBitcoinRedeemEncsig for Network {
async fn receive_bitcoin_redeem_encsig(&mut self) -> xmr_btc::bitcoin::EncryptedSignature {
async fn receive_bitcoin_redeem_encsig(&mut self) -> bitcoin::EncryptedSignature {
#[derive(Debug)]
struct UnexpectedMessage;
@ -173,6 +177,10 @@ pub async fn swap(
other => panic!("Unexpected event: {:?}", other),
};
let swap_id = Uuid::new_v4();
db.insert_latest_state(swap_id, state::Alice::Handshaken(state3.clone()).into())
.await?;
info!("Handshake complete, we now have State3 for Alice.");
let network = Arc::new(Mutex::new(Network {
@ -183,7 +191,7 @@ pub async fn swap(
let mut action_generator = action_generator(
network.clone(),
bitcoin_wallet.clone(),
state3,
state3.clone(),
TX_LOCK_MINE_TIMEOUT,
);
@ -198,33 +206,68 @@ pub async fn swap(
public_spend_key,
public_view_key,
}) => {
db.insert_latest_state(swap_id, state::Alice::BtcLocked(state3.clone()).into())
.await?;
let (transfer_proof, _) = monero_wallet
.transfer(public_spend_key, public_view_key, amount)
.await?;
db.insert_latest_state(swap_id, state::Alice::XmrLocked(state3.clone()).into())
.await?;
let mut guard = network.as_ref().lock().await;
guard.send_message2(transfer_proof).await;
info!("Sent transfer proof");
}
GeneratorState::Yielded(Action::RedeemBtc(tx)) => {
db.insert_latest_state(
swap_id,
state::Alice::BtcRedeemable {
state: state3.clone(),
redeem_tx: tx.clone(),
}
.into(),
)
.await?;
let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?;
}
GeneratorState::Yielded(Action::CancelBtc(tx)) => {
let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?;
}
GeneratorState::Yielded(Action::PunishBtc(tx)) => {
db.insert_latest_state(swap_id, state::Alice::BtcPunishable(state3.clone()).into())
.await?;
let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?;
}
GeneratorState::Yielded(Action::CreateMoneroWalletForOutput {
spend_key,
view_key,
}) => {
db.insert_latest_state(
swap_id,
state::Alice::BtcRefunded {
state: state3.clone(),
spend_key,
view_key,
}
.into(),
)
.await?;
monero_wallet
.create_and_load_wallet_for_output(spend_key, view_key)
.await?;
}
GeneratorState::Complete(()) => return Ok(()),
GeneratorState::Complete(()) => {
db.insert_latest_state(swap_id, state::Alice::SwapComplete.into())
.await?;
return Ok(());
}
}
}
}

@ -8,13 +8,13 @@ use bitcoin_harness::bitcoind_rpc::PsbtBase64;
use reqwest::Url;
use tokio::time;
use xmr_btc::bitcoin::{
Amount, BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock,
TransactionBlockHeight, TxLock, Txid, WatchForRawTransaction,
BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TransactionBlockHeight,
WatchForRawTransaction,
};
pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600;
pub use xmr_btc::bitcoin::*;
// This is cut'n'paste from xmr_btc/tests/harness/wallet/bitcoin.rs
pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600;
#[derive(Debug)]
pub struct Wallet(pub bitcoin_harness::Wallet);

@ -13,6 +13,7 @@ use rand::rngs::OsRng;
use std::{process, sync::Arc, time::Duration};
use tokio::sync::Mutex;
use tracing::{debug, info, warn};
use uuid::Uuid;
mod amounts;
mod message0;
@ -22,14 +23,15 @@ mod message3;
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
use crate::{
bitcoin,
bitcoin::TX_LOCK_MINE_TIMEOUT,
bitcoin::{self, TX_LOCK_MINE_TIMEOUT},
monero,
network::{
peer_tracker::{self, PeerTracker},
transport::SwapTransport,
TokioExecutor,
},
state,
storage::Database,
Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
};
use xmr_btc::{
@ -43,6 +45,7 @@ use xmr_btc::{
pub async fn swap(
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
db: Database,
btc: u64,
addr: Multiaddr,
mut cmd_tx: Sender<Cmd>,
@ -141,6 +144,10 @@ pub async fn swap(
other => panic!("unexpected event: {:?}", other),
};
let swap_id = Uuid::new_v4();
db.insert_latest_state(swap_id, state::Bob::Handshaken(state2.clone()).into())
.await?;
swarm.send_message2(alice.clone(), state2.next_message());
info!("Handshake complete");
@ -151,7 +158,7 @@ pub async fn swap(
network.clone(),
monero_wallet.clone(),
bitcoin_wallet.clone(),
state2,
state2.clone(),
TX_LOCK_MINE_TIMEOUT,
);
@ -160,20 +167,29 @@ pub async fn swap(
info!("Resumed execution of generator, got: {:?}", state);
// TODO: Protect against transient errors
// TODO: Ignore transaction-already-in-block-chain errors
match state {
GeneratorState::Yielded(bob::Action::LockBtc(tx_lock)) => {
let signed_tx_lock = bitcoin_wallet.sign_tx_lock(tx_lock).await?;
let _ = bitcoin_wallet
.broadcast_signed_transaction(signed_tx_lock)
.await?;
db.insert_latest_state(swap_id, state::Bob::BtcLocked(state2.clone()).into())
.await?;
}
GeneratorState::Yielded(bob::Action::SendBtcRedeemEncsig(tx_redeem_encsig)) => {
db.insert_latest_state(swap_id, state::Bob::XmrLocked(state2.clone()).into())
.await?;
let mut guard = network.as_ref().lock().await;
guard.0.send_message3(alice.clone(), tx_redeem_encsig);
info!("Sent Bitcoin redeem encsig");
// TODO: Does Bob need to wait for Alice to send an empty response, or can we
// just continue?
// FIXME: Having to wait for Alice's response here is a big problem, because
// we're stuck if she doesn't send her response back. I believe this is
// currently necessary, so we may have to rework this and/or how we use libp2p
match guard.0.next().shared().await {
OutEvent::Message3 => {
debug!("Got Message3 empty response");
@ -185,21 +201,35 @@ pub async fn swap(
spend_key,
view_key,
}) => {
db.insert_latest_state(swap_id, state::Bob::BtcRedeemed(state2.clone()).into())
.await?;
monero_wallet
.create_and_load_wallet_for_output(spend_key, view_key)
.await?;
}
GeneratorState::Yielded(bob::Action::CancelBtc(tx_cancel)) => {
db.insert_latest_state(swap_id, state::Bob::BtcRefundable(state2.clone()).into())
.await?;
let _ = bitcoin_wallet
.broadcast_signed_transaction(tx_cancel)
.await?;
}
GeneratorState::Yielded(bob::Action::RefundBtc(tx_refund)) => {
db.insert_latest_state(swap_id, state::Bob::BtcRefundable(state2.clone()).into())
.await?;
let _ = bitcoin_wallet
.broadcast_signed_transaction(tx_refund)
.await?;
}
GeneratorState::Complete(()) => return Ok(()),
GeneratorState::Complete(()) => {
db.insert_latest_state(swap_id, state::Bob::SwapComplete.into())
.await?;
return Ok(());
}
}
}
}

@ -1,5 +1,6 @@
use libp2p::core::Multiaddr;
use url::Url;
use uuid::Uuid;
#[derive(structopt::StructOpt, Debug)]
#[structopt(name = "xmr-btc-swap", about = "Trustless XMR BTC swaps")]
@ -8,7 +9,7 @@ pub enum Options {
#[structopt(default_value = "http://127.0.0.1:8332", long = "bitcoind")]
bitcoind_url: Url,
#[structopt(default_value = "http://127.0.0.1:18083", long = "monerod")]
#[structopt(default_value = "http://127.0.0.1:18083/json_rpc", long = "monerod")]
monerod_url: Url,
#[structopt(default_value = "/ip4/127.0.0.1/tcp/9876", long = "listen-addr")]
@ -27,10 +28,21 @@ pub enum Options {
#[structopt(default_value = "http://127.0.0.1:8332", long = "bitcoind")]
bitcoind_url: Url,
#[structopt(default_value = "http://127.0.0.1:18083", long = "monerod")]
#[structopt(default_value = "http://127.0.0.1:18083/json_rpc", long = "monerod")]
monerod_url: Url,
#[structopt(long = "tor")]
tor: bool,
},
History,
Recover {
#[structopt(required = true)]
swap_id: Uuid,
#[structopt(default_value = "http://127.0.0.1:8332", long = "bitcoind")]
bitcoind_url: Url,
#[structopt(default_value = "http://127.0.0.1:18083/json_rpc", long = "monerod")]
monerod_url: Url,
},
}

@ -6,6 +6,8 @@ pub mod bitcoin;
pub mod bob;
pub mod monero;
pub mod network;
pub mod recover;
pub mod state;
pub mod storage;
pub mod tor;
@ -32,10 +34,10 @@ pub enum Rsp {
pub struct SwapAmounts {
/// Amount of BTC to swap.
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
pub btc: ::bitcoin::Amount,
pub btc: bitcoin::Amount,
/// Amount of XMR to swap.
#[serde(with = "xmr_btc::serde::monero_amount")]
pub xmr: xmr_btc::monero::Amount,
pub xmr: monero::Amount,
}
// TODO: Display in XMR and BTC (not picos and sats).

@ -16,23 +16,28 @@ use anyhow::Result;
use futures::{channel::mpsc, StreamExt};
use libp2p::Multiaddr;
use log::LevelFilter;
use prettytable::{row, Table};
use std::{io, io::Write, process, sync::Arc};
use structopt::StructOpt;
use swap::{
alice,
alice::Alice,
bitcoin, bob,
bob::Bob,
alice::{self, Alice},
bitcoin,
bob::{self, Bob},
monero,
network::transport::{build, build_tor, SwapTransport},
recover::recover,
Cmd, Rsp, SwapAmounts,
};
use tracing::info;
#[macro_use]
extern crate prettytable;
mod cli;
mod trace;
use cli::Options;
use swap::storage::Database;
// TODO: Add root seed file instead of generating new seed each run.
@ -42,6 +47,9 @@ async fn main() -> Result<()> {
trace::init_tracing(LevelFilter::Debug)?;
// This currently creates the directory if it's not there in the first place
let db = Database::open(std::path::Path::new("./.swap-db/")).unwrap();
match opt {
Options::Alice {
bitcoind_url,
@ -83,6 +91,7 @@ async fn main() -> Result<()> {
swap_as_alice(
bitcoin_wallet,
monero_wallet,
db,
listen_addr,
transport,
behaviour,
@ -116,6 +125,7 @@ async fn main() -> Result<()> {
swap_as_bob(
bitcoin_wallet,
monero_wallet,
db,
satoshis,
alice_addr,
transport,
@ -123,6 +133,31 @@ async fn main() -> Result<()> {
)
.await?;
}
Options::History => {
let mut table = Table::new();
table.add_row(row!["SWAP ID", "STATE"]);
for (swap_id, state) in db.all()? {
table.add_row(row![swap_id, state]);
}
// Print the table to stdout
table.printstd();
}
Options::Recover {
swap_id,
bitcoind_url,
monerod_url,
} => {
let state = db.get_state(swap_id)?;
let bitcoin_wallet = bitcoin::Wallet::new("bob", bitcoind_url)
.await
.expect("failed to create bitcoin wallet");
let monero_wallet = monero::Wallet::new(monerod_url);
recover(bitcoin_wallet, monero_wallet, state).await?;
}
}
Ok(())
@ -149,16 +184,26 @@ async fn create_tor_service(
async fn swap_as_alice(
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
monero_wallet: Arc<swap::monero::Wallet>,
db: Database,
addr: Multiaddr,
transport: SwapTransport,
behaviour: Alice,
) -> Result<()> {
alice::swap(bitcoin_wallet, monero_wallet, addr, transport, behaviour).await
alice::swap(
bitcoin_wallet,
monero_wallet,
db,
addr,
transport,
behaviour,
)
.await
}
async fn swap_as_bob(
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
monero_wallet: Arc<swap::monero::Wallet>,
db: Database,
sats: u64,
alice: Multiaddr,
transport: SwapTransport,
@ -169,6 +214,7 @@ async fn swap_as_bob(
tokio::spawn(bob::swap(
bitcoin_wallet,
monero_wallet,
db,
sats,
alice,
cmd_tx,

@ -1,15 +1,11 @@
use anyhow::Result;
use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use monero::{Address, Network, PrivateKey};
use monero_harness::rpc::wallet;
use std::{str::FromStr, time::Duration};
use url::Url;
pub use xmr_btc::monero::{
Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicKey, PublicViewKey,
Transfer, TransferProof, TxHash, WatchForTransfer, *,
};
pub use xmr_btc::monero::*;
pub struct Wallet(pub wallet::Client);

@ -0,0 +1,481 @@
//! This module is used to attempt to recover an unfinished swap.
//!
//! Recovery is only supported for certain states and the strategy followed is
//! to perform the simplest steps that require no further action from the
//! counterparty.
//!
//! The quality of this module is bad because there is a lot of code
//! duplication, both within the module and with respect to
//! `xmr_btc/src/{alice,bob}.rs`. In my opinion, a better approach to support
//! swap recovery would be through the `action_generator`s themselves, but this
//! was deemed too complicated for the time being.
use crate::{
bitcoin, monero,
monero::CreateWalletForOutput,
state::{Alice, Bob, Swap},
};
use anyhow::Result;
use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic};
use futures::{
future::{select, Either},
pin_mut,
};
use sha2::Sha256;
use tracing::info;
use xmr_btc::bitcoin::{
poll_until_block_height_is_gte, BroadcastSignedTransaction, TransactionBlockHeight,
WatchForRawTransaction,
};
pub async fn recover(
bitcoin_wallet: bitcoin::Wallet,
monero_wallet: monero::Wallet,
state: Swap,
) -> Result<()> {
match state {
Swap::Alice(state) => alice_recover(bitcoin_wallet, monero_wallet, state).await,
Swap::Bob(state) => bob_recover(bitcoin_wallet, monero_wallet, state).await,
}
}
pub async fn alice_recover(
bitcoin_wallet: bitcoin::Wallet,
monero_wallet: monero::Wallet,
state: Alice,
) -> Result<()> {
match state {
Alice::Handshaken(_) | Alice::BtcLocked(_) | Alice::SwapComplete => {
info!("Nothing to do");
}
Alice::XmrLocked(state) => {
info!("Monero still locked up");
let tx_cancel = bitcoin::TxCancel::new(
&state.tx_lock,
state.refund_timelock,
state.a.public(),
state.B.clone(),
);
info!("Checking if the Bitcoin cancel transaction has been published");
if bitcoin_wallet
.0
.get_raw_transaction(tx_cancel.txid())
.await
.is_err()
{
info!("Bitcoin cancel transaction not yet published");
let tx_lock_height = bitcoin_wallet
.transaction_block_height(state.tx_lock.txid())
.await;
poll_until_block_height_is_gte(
&bitcoin_wallet,
tx_lock_height + state.refund_timelock,
)
.await;
let sig_a = state.a.sign(tx_cancel.digest());
let sig_b = state.tx_cancel_sig_bob.clone();
let tx_cancel = tx_cancel
.clone()
.add_signatures(
&state.tx_lock,
(state.a.public(), sig_a),
(state.B.clone(), sig_b),
)
.expect("sig_{a,b} to be valid signatures for tx_cancel");
// TODO: We should not fail if the transaction is already on the blockchain
bitcoin_wallet
.broadcast_signed_transaction(tx_cancel)
.await?;
}
info!("Confirmed that Bitcoin cancel transaction is on the blockchain");
let tx_cancel_height = bitcoin_wallet
.transaction_block_height(tx_cancel.txid())
.await;
let poll_until_bob_can_be_punished = poll_until_block_height_is_gte(
&bitcoin_wallet,
tx_cancel_height + state.punish_timelock,
);
pin_mut!(poll_until_bob_can_be_punished);
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address);
info!("Waiting for either Bitcoin refund or punish timelock");
match select(
bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()),
poll_until_bob_can_be_punished,
)
.await
{
Either::Left((tx_refund_published, ..)) => {
info!("Found Bitcoin refund transaction");
let s_a = monero::PrivateKey {
scalar: state.s_a.into_ed25519(),
};
let tx_refund_sig = tx_refund
.extract_signature_by_key(tx_refund_published, state.a.public())?;
let tx_refund_encsig = state
.a
.encsign(state.S_b_bitcoin.clone(), tx_refund.digest());
let s_b = bitcoin::recover(state.S_b_bitcoin, tx_refund_sig, tx_refund_encsig)?;
let s_b = monero::PrivateKey::from_scalar(
monero::Scalar::from_bytes_mod_order(s_b.to_bytes()),
);
monero_wallet
.create_and_load_wallet_for_output(s_a + s_b, state.v)
.await?;
info!("Successfully refunded monero");
}
Either::Right(_) => {
info!("Punish timelock reached, attempting to punish Bob");
let tx_punish = bitcoin::TxPunish::new(
&tx_cancel,
&state.punish_address,
state.punish_timelock,
);
let sig_a = state.a.sign(tx_punish.digest());
let sig_b = state.tx_punish_sig_bob.clone();
let sig_tx_punish = tx_punish.add_signatures(
&tx_cancel,
(state.a.public(), sig_a),
(state.B.clone(), sig_b),
)?;
bitcoin_wallet
.broadcast_signed_transaction(sig_tx_punish)
.await?;
info!("Successfully punished Bob's inactivity by taking bitcoin");
}
};
}
Alice::BtcRedeemable { redeem_tx, state } => {
info!("Have the means to redeem the Bitcoin");
let tx_lock_height = bitcoin_wallet
.transaction_block_height(state.tx_lock.txid())
.await;
let block_height = bitcoin_wallet.0.block_height().await?;
let refund_absolute_expiry = tx_lock_height + state.refund_timelock;
info!("Checking refund timelock");
if block_height < refund_absolute_expiry {
info!("Safe to redeem");
bitcoin_wallet
.broadcast_signed_transaction(redeem_tx)
.await?;
info!("Successfully redeemed bitcoin");
} else {
info!("Refund timelock reached");
let tx_cancel = bitcoin::TxCancel::new(
&state.tx_lock,
state.refund_timelock,
state.a.public(),
state.B.clone(),
);
info!("Checking if the Bitcoin cancel transaction has been published");
if bitcoin_wallet
.0
.get_raw_transaction(tx_cancel.txid())
.await
.is_err()
{
let sig_a = state.a.sign(tx_cancel.digest());
let sig_b = state.tx_cancel_sig_bob.clone();
let tx_cancel = tx_cancel
.clone()
.add_signatures(
&state.tx_lock,
(state.a.public(), sig_a),
(state.B.clone(), sig_b),
)
.expect("sig_{a,b} to be valid signatures for tx_cancel");
// TODO: We should not fail if the transaction is already on the blockchain
bitcoin_wallet
.broadcast_signed_transaction(tx_cancel)
.await?;
}
info!("Confirmed that Bitcoin cancel transaction is on the blockchain");
let tx_cancel_height = bitcoin_wallet
.transaction_block_height(tx_cancel.txid())
.await;
let poll_until_bob_can_be_punished = poll_until_block_height_is_gte(
&bitcoin_wallet,
tx_cancel_height + state.punish_timelock,
);
pin_mut!(poll_until_bob_can_be_punished);
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address);
info!("Waiting for either Bitcoin refund or punish timelock");
match select(
bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()),
poll_until_bob_can_be_punished,
)
.await
{
Either::Left((tx_refund_published, ..)) => {
info!("Found Bitcoin refund transaction");
let s_a = monero::PrivateKey {
scalar: state.s_a.into_ed25519(),
};
let tx_refund_sig = tx_refund
.extract_signature_by_key(tx_refund_published, state.a.public())?;
let tx_refund_encsig = state
.a
.encsign(state.S_b_bitcoin.clone(), tx_refund.digest());
let s_b =
bitcoin::recover(state.S_b_bitcoin, tx_refund_sig, tx_refund_encsig)?;
let s_b = monero::PrivateKey::from_scalar(
monero::Scalar::from_bytes_mod_order(s_b.to_bytes()),
);
monero_wallet
.create_and_load_wallet_for_output(s_a + s_b, state.v)
.await?;
info!("Successfully refunded monero");
}
Either::Right(_) => {
info!("Punish timelock reached, attempting to punish Bob");
let tx_punish = bitcoin::TxPunish::new(
&tx_cancel,
&state.punish_address,
state.punish_timelock,
);
let sig_a = state.a.sign(tx_punish.digest());
let sig_b = state.tx_punish_sig_bob.clone();
let sig_tx_punish = tx_punish.add_signatures(
&tx_cancel,
(state.a.public(), sig_a),
(state.B.clone(), sig_b),
)?;
bitcoin_wallet
.broadcast_signed_transaction(sig_tx_punish)
.await?;
info!("Successfully punished Bob's inactivity by taking bitcoin");
}
};
}
}
Alice::BtcPunishable(state) => {
info!("Punish timelock reached, attempting to punish Bob");
let tx_cancel = bitcoin::TxCancel::new(
&state.tx_lock,
state.refund_timelock,
state.a.public(),
state.B.clone(),
);
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address);
info!("Checking if Bitcoin has already been refunded");
// TODO: Protect against transient errors so that we can correctly decide if the
// bitcoin has been refunded
match bitcoin_wallet.0.get_raw_transaction(tx_refund.txid()).await {
Ok(tx_refund_published) => {
info!("Bitcoin already refunded");
let s_a = monero::PrivateKey {
scalar: state.s_a.into_ed25519(),
};
let tx_refund_sig = tx_refund
.extract_signature_by_key(tx_refund_published, state.a.public())?;
let tx_refund_encsig = state
.a
.encsign(state.S_b_bitcoin.clone(), tx_refund.digest());
let s_b = bitcoin::recover(state.S_b_bitcoin, tx_refund_sig, tx_refund_encsig)?;
let s_b = monero::PrivateKey::from_scalar(
monero::Scalar::from_bytes_mod_order(s_b.to_bytes()),
);
monero_wallet
.create_and_load_wallet_for_output(s_a + s_b, state.v)
.await?;
info!("Successfully refunded monero");
}
Err(_) => {
info!("Bitcoin not yet refunded");
let tx_punish = bitcoin::TxPunish::new(
&tx_cancel,
&state.punish_address,
state.punish_timelock,
);
let sig_a = state.a.sign(tx_punish.digest());
let sig_b = state.tx_punish_sig_bob.clone();
let sig_tx_punish = tx_punish.add_signatures(
&tx_cancel,
(state.a.public(), sig_a),
(state.B.clone(), sig_b),
)?;
bitcoin_wallet
.broadcast_signed_transaction(sig_tx_punish)
.await?;
info!("Successfully punished Bob's inactivity by taking bitcoin");
}
}
}
Alice::BtcRefunded {
view_key,
spend_key,
..
} => {
info!("Bitcoin was refunded, attempting to refund monero");
monero_wallet
.create_and_load_wallet_for_output(spend_key, view_key)
.await?;
info!("Successfully refunded monero");
}
};
Ok(())
}
pub async fn bob_recover(
bitcoin_wallet: crate::bitcoin::Wallet,
monero_wallet: crate::monero::Wallet,
state: Bob,
) -> Result<()> {
match state {
Bob::Handshaken(_) | Bob::SwapComplete => {
info!("Nothing to do");
}
Bob::BtcLocked(state) | Bob::XmrLocked(state) | Bob::BtcRefundable(state) => {
info!("Bitcoin may still be locked up, attempting to refund");
let tx_cancel = bitcoin::TxCancel::new(
&state.tx_lock,
state.refund_timelock,
state.A.clone(),
state.b.public(),
);
info!("Checking if the Bitcoin cancel transaction has been published");
if bitcoin_wallet
.0
.get_raw_transaction(tx_cancel.txid())
.await
.is_err()
{
info!("Bitcoin cancel transaction not yet published");
let tx_lock_height = bitcoin_wallet
.transaction_block_height(state.tx_lock.txid())
.await;
poll_until_block_height_is_gte(
&bitcoin_wallet,
tx_lock_height + state.refund_timelock,
)
.await;
let sig_a = state.tx_cancel_sig_a.clone();
let sig_b = state.b.sign(tx_cancel.digest());
let tx_cancel = tx_cancel
.clone()
.add_signatures(
&state.tx_lock,
(state.A.clone(), sig_a),
(state.b.public(), sig_b),
)
.expect("sig_{a,b} to be valid signatures for tx_cancel");
// TODO: We should not fail if the transaction is already on the blockchain
bitcoin_wallet
.broadcast_signed_transaction(tx_cancel)
.await?;
}
info!("Confirmed that Bitcoin cancel transaction is on the blockchain");
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address);
let signed_tx_refund = {
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
let sig_a = adaptor
.decrypt_signature(&state.s_b.into_secp256k1(), state.tx_refund_encsig.clone());
let sig_b = state.b.sign(tx_refund.digest());
tx_refund
.add_signatures(
&tx_cancel,
(state.A.clone(), sig_a),
(state.b.public(), sig_b),
)
.expect("sig_{a,b} to be valid signatures for tx_refund")
};
// TODO: Check if Bitcoin has already been punished and provide a useful error
// message/log to the user if so
bitcoin_wallet
.broadcast_signed_transaction(signed_tx_refund)
.await?;
info!("Successfully refunded bitcoin");
}
Bob::BtcRedeemed(state) => {
info!("Bitcoin was redeemed, attempting to redeem monero");
let tx_redeem = bitcoin::TxRedeem::new(&state.tx_lock, &state.redeem_address);
let tx_redeem_published = bitcoin_wallet
.0
.get_raw_transaction(tx_redeem.txid())
.await?;
let tx_redeem_encsig = state
.b
.encsign(state.S_a_bitcoin.clone(), tx_redeem.digest());
let tx_redeem_sig =
tx_redeem.extract_signature_by_key(tx_redeem_published, state.b.public())?;
let s_a = bitcoin::recover(state.S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)?;
let s_a = monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order(
s_a.to_bytes(),
));
let s_b = monero::PrivateKey {
scalar: state.s_b.into_ed25519(),
};
monero_wallet
.create_and_load_wallet_for_output(s_a + s_b, state.v)
.await?;
info!("Successfully redeemed monero")
}
};
Ok(())
}

@ -0,0 +1,88 @@
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use xmr_btc::{alice, bob, monero, serde::monero_private_key};
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub enum Swap {
Alice(Alice),
Bob(Bob),
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub enum Alice {
Handshaken(alice::State3),
BtcLocked(alice::State3),
XmrLocked(alice::State3),
BtcRedeemable {
state: alice::State3,
redeem_tx: bitcoin::Transaction,
},
BtcPunishable(alice::State3),
BtcRefunded {
state: alice::State3,
#[serde(with = "monero_private_key")]
spend_key: monero::PrivateKey,
view_key: monero::PrivateViewKey,
},
SwapComplete,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub enum Bob {
Handshaken(bob::State2),
BtcLocked(bob::State2),
XmrLocked(bob::State2),
BtcRedeemed(bob::State2),
BtcRefundable(bob::State2),
SwapComplete,
}
impl From<Alice> for Swap {
fn from(from: Alice) -> Self {
Swap::Alice(from)
}
}
impl From<Bob> for Swap {
fn from(from: Bob) -> Self {
Swap::Bob(from)
}
}
impl Display for Swap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Swap::Alice(alice) => Display::fmt(alice, f),
Swap::Bob(bob) => Display::fmt(bob, f),
}
}
}
impl Display for Alice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Alice::Handshaken(_) => f.write_str("Handshake complete"),
Alice::BtcLocked(_) => f.write_str("Bitcoin locked"),
Alice::XmrLocked(_) => f.write_str("Monero locked"),
Alice::BtcRedeemable { .. } => f.write_str("Bitcoin redeemable"),
Alice::BtcPunishable(_) => f.write_str("Bitcoin punishable"),
Alice::BtcRefunded { .. } => f.write_str("Monero refundable"),
Alice::SwapComplete => f.write_str("Swap complete"),
}
}
}
impl Display for Bob {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Bob::Handshaken(_) => f.write_str("Handshake complete"),
Bob::BtcLocked(_) | Bob::XmrLocked(_) | Bob::BtcRefundable(_) => {
f.write_str("Bitcoin refundable")
}
Bob::BtcRedeemed(_) => f.write_str("Monero redeemable"),
Bob::SwapComplete => f.write_str("Swap complete"),
}
}
}

@ -1,62 +1,68 @@
use anyhow::{anyhow, Context, Result};
use crate::state::Swap;
use anyhow::{anyhow, bail, Context, Result};
use serde::{de::DeserializeOwned, Serialize};
use std::path::Path;
use uuid::Uuid;
pub struct Database<T>
where
T: Serialize + DeserializeOwned,
{
db: sled::Db,
_marker: std::marker::PhantomData<T>,
}
impl<T> Database<T>
where
T: Serialize + DeserializeOwned,
{
// TODO: serialize using lazy/one-time initlisation
const LAST_STATE_KEY: &'static str = "latest_state";
pub struct Database(sled::Db);
impl Database {
pub fn open(path: &Path) -> Result<Self> {
let db =
sled::open(path).with_context(|| format!("Could not open the DB at {:?}", path))?;
Ok(Database {
db,
_marker: Default::default(),
})
Ok(Database(db))
}
pub async fn insert_latest_state(&self, state: &T) -> Result<()> {
let key = serialize(&Self::LAST_STATE_KEY)?;
pub async fn insert_latest_state(&self, swap_id: Uuid, state: Swap) -> Result<()> {
let key = serialize(&swap_id)?;
let new_value = serialize(&state).context("Could not serialize new state value")?;
let old_value = self.db.get(&key)?;
let old_value = self.0.get(&key)?;
self.db
self.0
.compare_and_swap(key, old_value, Some(new_value))
.context("Could not write in the DB")?
.context("Stored swap somehow changed, aborting saving")?;
// TODO: see if this can be done through sled config
self.db
self.0
.flush_async()
.await
.map(|_| ())
.context("Could not flush db")
}
pub fn get_latest_state(&self) -> anyhow::Result<T> {
let key = serialize(&Self::LAST_STATE_KEY)?;
pub fn get_state(&self, swap_id: Uuid) -> anyhow::Result<Swap> {
let key = serialize(&swap_id)?;
let encoded = self
.db
.0
.get(&key)?
.ok_or_else(|| anyhow!("State does not exist {:?}", key))?;
let state = deserialize(&encoded).context("Could not deserialize state")?;
Ok(state)
}
pub fn all(&self) -> Result<Vec<(Uuid, Swap)>> {
self.0
.iter()
.map(|item| match item {
Ok((key, value)) => {
let swap_id = deserialize::<Uuid>(&key);
let swap = deserialize::<Swap>(&value).context("failed to deserialize swap");
match (swap_id, swap) {
(Ok(swap_id), Ok(swap)) => Ok((swap_id, swap)),
(Ok(_), Err(err)) => Err(err),
_ => bail!("failed to deserialize swap"),
}
}
Err(err) => Err(err).context("failed to retrieve swap from DB"),
})
.collect()
}
}
pub fn serialize<T>(t: &T) -> anyhow::Result<Vec<u8>>
@ -75,86 +81,87 @@ where
#[cfg(test)]
mod tests {
#![allow(non_snake_case)]
use crate::state::{Alice, Bob};
use super::*;
use bitcoin::SigHash;
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use xmr_btc::{cross_curve_dleq, monero, serde::monero_private_key};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct TestState {
A: xmr_btc::bitcoin::PublicKey,
a: xmr_btc::bitcoin::SecretKey,
s_a: cross_curve_dleq::Scalar,
#[serde(with = "monero_private_key")]
s_b: monero::PrivateKey,
S_a_monero: ::monero::PublicKey,
S_a_bitcoin: xmr_btc::bitcoin::PublicKey,
v: xmr_btc::monero::PrivateViewKey,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
btc: ::bitcoin::Amount,
xmr: xmr_btc::monero::Amount,
refund_timelock: u32,
refund_address: ::bitcoin::Address,
transaction: ::bitcoin::Transaction,
tx_punish_sig: xmr_btc::bitcoin::Signature,
#[tokio::test]
async fn can_write_and_read_to_multiple_keys() {
let db_dir = tempfile::tempdir().unwrap();
let db = Database::open(db_dir.path()).unwrap();
let state_1 = Swap::Alice(Alice::SwapComplete);
let swap_id_1 = Uuid::new_v4();
db.insert_latest_state(swap_id_1, state_1.clone())
.await
.expect("Failed to save second state");
let state_2 = Swap::Bob(Bob::SwapComplete);
let swap_id_2 = Uuid::new_v4();
db.insert_latest_state(swap_id_2, state_2.clone())
.await
.expect("Failed to save first state");
let recovered_1 = db
.get_state(swap_id_1)
.expect("Failed to recover first state");
let recovered_2 = db
.get_state(swap_id_2)
.expect("Failed to recover second state");
assert_eq!(recovered_1, state_1);
assert_eq!(recovered_2, state_2);
}
#[tokio::test]
async fn recover_state_from_db() {
async fn can_write_twice_to_one_key() {
let db_dir = tempfile::tempdir().unwrap();
let db = Database::open(db_dir.path()).unwrap();
let a = xmr_btc::bitcoin::SecretKey::new_random(&mut OsRng);
let s_a = cross_curve_dleq::Scalar::random(&mut OsRng);
let s_b = monero::PrivateKey::from_scalar(monero::Scalar::random(&mut OsRng));
let v_a = xmr_btc::monero::PrivateViewKey::new_random(&mut OsRng);
let S_a_monero = monero::PublicKey::from_private_key(&monero::PrivateKey {
scalar: s_a.into_ed25519(),
});
let S_a_bitcoin = s_a.into_secp256k1().into();
let tx_punish_sig = a.sign(SigHash::default());
let state = TestState {
A: a.public(),
a,
s_b,
s_a,
S_a_monero,
S_a_bitcoin,
v: v_a,
btc: ::bitcoin::Amount::from_sat(100),
xmr: xmr_btc::monero::Amount::from_piconero(1000),
refund_timelock: 0,
refund_address: ::bitcoin::Address::from_str("1L5wSMgerhHg8GZGcsNmAx5EXMRXSKR3He")
.unwrap(),
transaction: ::bitcoin::Transaction {
version: 0,
lock_time: 0,
input: vec![::bitcoin::TxIn::default()],
output: vec![::bitcoin::TxOut::default()],
},
tx_punish_sig,
};
db.insert_latest_state(&state)
let state = Swap::Alice(Alice::SwapComplete);
let swap_id = Uuid::new_v4();
db.insert_latest_state(swap_id, state.clone())
.await
.expect("Failed to save state the first time");
let recovered: TestState = db
.get_latest_state()
let recovered = db
.get_state(swap_id)
.expect("Failed to recover state the first time");
// We insert and recover twice to ensure database implementation allows the
// caller to write to an existing key
db.insert_latest_state(&recovered)
db.insert_latest_state(swap_id, recovered)
.await
.expect("Failed to save state the second time");
let recovered: TestState = db
.get_latest_state()
let recovered = db
.get_state(swap_id)
.expect("Failed to recover state the second time");
assert_eq!(state, recovered);
assert_eq!(recovered, state);
}
#[tokio::test]
async fn can_fetch_all_keys() {
let db_dir = tempfile::tempdir().unwrap();
let db = Database::open(db_dir.path()).unwrap();
let state_1 = Swap::Alice(Alice::SwapComplete);
let swap_id_1 = Uuid::new_v4();
db.insert_latest_state(swap_id_1, state_1.clone())
.await
.expect("Failed to save second state");
let state_2 = Swap::Bob(Bob::SwapComplete);
let swap_id_2 = Uuid::new_v4();
db.insert_latest_state(swap_id_2, state_2.clone())
.await
.expect("Failed to save first state");
let swaps = db.all().unwrap();
assert_eq!(swaps.len(), 2);
assert!(swaps.contains(&(swap_id_1, state_1)));
assert!(swaps.contains(&(swap_id_2, state_2)));
}
}

@ -14,7 +14,10 @@ pub fn init_tracing(level: log::LevelFilter) -> anyhow::Result<()> {
let is_terminal = atty::is(Stream::Stdout);
let subscriber = FmtSubscriber::builder()
.with_env_filter(format!("swap={}", level))
.with_env_filter(format!(
"swap={},xmr_btc={},monero_harness={}",
level, level, level
))
.with_ansi(is_terminal)
.finish();

@ -1,117 +1,124 @@
#[cfg(not(feature = "tor"))]
mod e2e_test {
use bitcoin_harness::Bitcoind;
use futures::{channel::mpsc, future::try_join};
use libp2p::Multiaddr;
use monero_harness::Monero;
use std::sync::Arc;
use swap::{alice, bob, network::transport::build};
use testcontainers::clients::Cli;
use tracing_subscriber::util::SubscriberInitExt;
#[tokio::test]
async fn swap() {
let _guard = tracing_subscriber::fmt()
.with_env_filter(
"swap=debug,xmr_btc=debug,hyper=off,reqwest=off,monero_harness=info,testcontainers=info,libp2p=debug",
)
use bitcoin_harness::Bitcoind;
use futures::{channel::mpsc, future::try_join};
use libp2p::Multiaddr;
use monero_harness::Monero;
use std::sync::Arc;
use swap::{alice, bob, network::transport::build, storage::Database};
use tempfile::tempdir;
use testcontainers::clients::Cli;
use xmr_btc::bitcoin;
// NOTE: For some reason running these tests overflows the stack. In order to
// mitigate this run them with:
//
// RUST_MIN_STACK=100000000 cargo test
#[tokio::test]
async fn swap() {
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();
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 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 (monero, _container) = Monero::new(&cli, Some("swap_".to_string()), vec![
"alice".to_string(),
"bob".to_string(),
])
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();
monero
.init(vec![("alice", xmr_alice), ("bob", xmr_bob)])
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::Alice::default();
let alice_transport = build(alice_behaviour.identity()).unwrap();
let alice_swap = alice::swap(
alice_btc_wallet.clone(),
alice_xmr_wallet.clone(),
alice_multiaddr.clone(),
alice_transport,
alice_behaviour,
);
let (cmd_tx, mut _cmd_rx) = mpsc::channel(1);
let (mut rsp_tx, rsp_rx) = mpsc::channel(1);
let bob_behaviour = bob::Bob::default();
let bob_transport = build(bob_behaviour.identity()).unwrap();
let bob_swap = bob::swap(
bob_btc_wallet.clone(),
bob_xmr_wallet.clone(),
btc.as_sat(),
alice_multiaddr,
cmd_tx,
rsp_rx,
bob_transport,
bob_behaviour,
);
// 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(xmr_btc::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);
}
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::Alice::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::Bob::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,
cmd_tx,
rsp_rx,
bob_transport,
bob_behaviour,
);
// 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);
}

@ -13,7 +13,6 @@ mod tor_test {
onion::TorSecretKeyV3,
utils::{run_tor, AutoKillChild},
};
use tracing_subscriber::util::SubscriberInitExt;
async fn hello_world(
_req: hyper::Request<hyper::Body>,
@ -76,10 +75,6 @@ mod tor_test {
#[tokio::test]
async fn test_tor_control_port() -> Result<()> {
let _guard = tracing_subscriber::fmt()
.with_env_filter("info")
.set_default();
// start tmp tor
let (_child, control_port, proxy_port, _tmp_torrc) = run_tmp_tor()?;

@ -97,7 +97,7 @@ where
#[derive(Debug)]
enum SwapFailed {
BeforeBtcLock(Reason),
AfterXmrLock { tx_lock_height: u32, reason: Reason },
AfterXmrLock(Reason),
}
/// Reason why the swap has failed.
@ -114,9 +114,7 @@ where
#[derive(Debug)]
enum RefundFailed {
BtcPunishable {
tx_cancel_was_published: bool,
},
BtcPunishable,
/// Could not find Alice's signature on the refund transaction witness
/// stack.
BtcRefundSignature,
@ -167,12 +165,7 @@ where
.await
{
Either::Left((encsig, _)) => encsig,
Either::Right(_) => {
return Err(SwapFailed::AfterXmrLock {
reason: Reason::BtcExpired,
tx_lock_height,
})
}
Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)),
};
tracing::debug!("select returned redeem encsig from message");
@ -191,10 +184,7 @@ where
&tx_redeem.digest(),
&tx_redeem_encsig,
)
.map_err(|_| SwapFailed::AfterXmrLock {
reason: Reason::InvalidEncryptedSignature,
tx_lock_height,
})?;
.map_err(|_| SwapFailed::AfterXmrLock(Reason::InvalidEncryptedSignature))?;
let sig_a = a.sign(tx_redeem.digest());
let sig_b =
@ -217,12 +207,7 @@ where
.await
{
Either::Left(_) => {}
Either::Right(_) => {
return Err(SwapFailed::AfterXmrLock {
reason: Reason::BtcExpired,
tx_lock_height,
})
}
Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)),
};
Ok(())
@ -233,19 +218,8 @@ where
error!("swap failed: {:?}", err);
}
if let Err(SwapFailed::AfterXmrLock {
reason: Reason::BtcExpired,
tx_lock_height,
}) = swap_result
{
if let Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)) = swap_result {
let refund_result: Result<(), RefundFailed> = async {
let poll_until_bob_can_be_punished = poll_until_block_height_is_gte(
bitcoin_client.as_ref(),
tx_lock_height + refund_timelock + punish_timelock,
)
.shared();
pin_mut!(poll_until_bob_can_be_punished);
let tx_cancel =
bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone());
let signed_tx_cancel = {
@ -260,19 +234,19 @@ where
co.yield_(Action::CancelBtc(signed_tx_cancel)).await;
match select(
bitcoin_client.watch_for_raw_transaction(tx_cancel.txid()),
poll_until_bob_can_be_punished.clone(),
bitcoin_client
.watch_for_raw_transaction(tx_cancel.txid())
.await;
let tx_cancel_height = bitcoin_client
.transaction_block_height(tx_cancel.txid())
.await;
let poll_until_bob_can_be_punished = poll_until_block_height_is_gte(
bitcoin_client.as_ref(),
tx_cancel_height + punish_timelock,
)
.await
{
Either::Left(_) => {}
Either::Right(_) => {
return Err(RefundFailed::BtcPunishable {
tx_cancel_was_published: false,
})
}
};
.shared();
pin_mut!(poll_until_bob_can_be_punished);
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address);
let tx_refund_published = match select(
@ -282,11 +256,7 @@ where
.await
{
Either::Left((tx, _)) => tx,
Either::Right(_) => {
return Err(RefundFailed::BtcPunishable {
tx_cancel_was_published: true,
});
}
Either::Right(_) => return Err(RefundFailed::BtcPunishable),
};
let s_a = monero::PrivateKey {
@ -321,32 +291,9 @@ where
// transaction with his refund transaction. Alice would then need to carry on
// with the refund on Monero. Doing so may be too verbose with the current,
// linear approach. A different design may be required
if let Err(RefundFailed::BtcPunishable {
tx_cancel_was_published,
}) = refund_result
{
if let Err(RefundFailed::BtcPunishable) = refund_result {
let tx_cancel =
bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone());
if !tx_cancel_was_published {
let tx_cancel_txid = tx_cancel.txid();
let signed_tx_cancel = {
let sig_a = a.sign(tx_cancel.digest());
let sig_b = tx_cancel_sig_bob;
tx_cancel
.clone()
.add_signatures(&tx_lock, (a.public(), sig_a), (B.clone(), sig_b))
.expect("sig_{a,b} to be valid signatures for tx_cancel")
};
co.yield_(Action::CancelBtc(signed_tx_cancel)).await;
let _ = bitcoin_client
.watch_for_raw_transaction(tx_cancel_txid)
.await;
}
let tx_punish =
bitcoin::TxPunish::new(&tx_cancel, &punish_address, punish_timelock);
let tx_punish_txid = tx_punish.txid();
@ -684,7 +631,7 @@ impl State2 {
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct State3 {
pub a: bitcoin::SecretKey,
pub B: bitcoin::PublicKey,

@ -2,12 +2,7 @@ pub mod transactions;
use anyhow::{anyhow, bail, Result};
use async_trait::async_trait;
use bitcoin::{
hashes::{hex::ToHex, Hash},
secp256k1,
util::psbt::PartiallySignedTransaction,
SigHash,
};
use bitcoin::hashes::{hex::ToHex, Hash};
use ecdsa_fun::{adaptor::Adaptor, fun::Point, nonce::Deterministic, ECDSA};
use miniscript::{Descriptor, Segwitv0};
use rand::{CryptoRng, RngCore};
@ -15,9 +10,9 @@ use serde::{Deserialize, Serialize};
use sha2::Sha256;
use std::str::FromStr;
pub use crate::bitcoin::transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund};
pub use bitcoin::{Address, Amount, OutPoint, Transaction, Txid};
pub use bitcoin::{util::psbt::PartiallySignedTransaction, *};
pub use ecdsa_fun::{adaptor::EncryptedSignature, fun::Scalar, Signature};
pub use transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund};
pub const TX_FEE: u64 = 10_000;

@ -495,7 +495,7 @@ impl State1 {
}
}
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct State2 {
pub A: bitcoin::PublicKey,
pub b: bitcoin::SecretKey,
@ -507,10 +507,10 @@ pub struct State2 {
btc: bitcoin::Amount,
pub xmr: monero::Amount,
pub refund_timelock: u32,
punish_timelock: u32,
pub punish_timelock: u32,
pub refund_address: bitcoin::Address,
pub redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
pub punish_address: bitcoin::Address,
pub tx_lock: bitcoin::TxLock,
pub tx_cancel_sig_a: Signature,
pub tx_refund_encsig: EncryptedSignature,

@ -1,12 +1,13 @@
use crate::serde::monero_private_key;
use anyhow::Result;
use async_trait::async_trait;
pub use curve25519_dalek::scalar::Scalar;
pub use monero::{Address, PrivateKey, PublicKey};
use rand::{CryptoRng, RngCore};
use serde::{Deserialize, Serialize};
use std::ops::{Add, Sub};
pub use curve25519_dalek::scalar::Scalar;
pub use monero::*;
pub const MIN_CONFIRMATIONS: u32 = 10;
pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {

@ -13,7 +13,6 @@ mod tests {
use rand::rngs::OsRng;
use std::convert::TryInto;
use testcontainers::clients::Cli;
use tracing_subscriber::util::SubscriberInitExt;
use xmr_btc::{
alice, bitcoin,
bitcoin::{Amount, TX_FEE},
@ -22,10 +21,6 @@ mod tests {
#[tokio::test]
async fn happy_path() {
let _guard = tracing_subscriber::fmt()
.with_env_filter("info")
.set_default();
let cli = Cli::default();
let (monero, _container) = Monero::new(&cli, Some("hp".to_string()), vec![
"alice".to_string(),
@ -101,10 +96,6 @@ mod tests {
#[tokio::test]
async fn both_refund() {
let _guard = tracing_subscriber::fmt()
.with_env_filter("info")
.set_default();
let cli = Cli::default();
let (monero, _container) = Monero::new(&cli, Some("br".to_string()), vec![
"alice".to_string(),
@ -182,10 +173,6 @@ mod tests {
#[tokio::test]
async fn alice_punishes() {
let _guard = tracing_subscriber::fmt()
.with_env_filter("info")
.set_default();
let cli = Cli::default();
let (monero, _containers) = Monero::new(&cli, Some("ap".to_string()), vec![
"alice".to_string(),

@ -1,12 +1,11 @@
use anyhow::Result;
use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use monero::{Address, Network, PrivateKey};
use monero_harness::rpc::wallet;
use std::{str::FromStr, time::Duration};
use xmr_btc::monero::{
Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicKey, PublicViewKey,
Transfer, TransferProof, TxHash, WatchForTransfer,
Address, Amount, CreateWalletForOutput, InsufficientFunds, Network, PrivateKey, PrivateViewKey,
PublicKey, PublicViewKey, Transfer, TransferProof, TxHash, WatchForTransfer,
};
pub struct Wallet(pub wallet::Client);

@ -20,7 +20,7 @@ use tokio::sync::Mutex;
use tracing::info;
use xmr_btc::{
alice::{self, ReceiveBitcoinRedeemEncsig},
bitcoin::{BroadcastSignedTransaction, EncryptedSignature, SignTxLock},
bitcoin::{self, BroadcastSignedTransaction, EncryptedSignature, SignTxLock},
bob::{self, ReceiveTransferProof},
monero::{CreateWalletForOutput, Transfer, TransferProof},
};
@ -309,8 +309,7 @@ async fn on_chain_happy_path() {
assert_eq!(
alice_final_btc_balance,
initial_balances.alice_btc + swap_amounts.btc
- bitcoin::Amount::from_sat(xmr_btc::bitcoin::TX_FEE)
initial_balances.alice_btc + swap_amounts.btc - bitcoin::Amount::from_sat(bitcoin::TX_FEE)
);
assert_eq!(
bob_final_btc_balance,
@ -411,7 +410,7 @@ async fn on_chain_both_refund_if_alice_never_redeems() {
bob_final_btc_balance,
// The 2 * TX_FEE corresponds to tx_refund and tx_cancel.
initial_balances.bob_btc
- bitcoin::Amount::from_sat(2 * xmr_btc::bitcoin::TX_FEE)
- bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE)
- lock_tx_bitcoin_fee
);
@ -508,7 +507,7 @@ async fn on_chain_alice_punishes_if_bob_never_acts_after_fund() {
assert_eq!(
alice_final_btc_balance,
initial_balances.alice_btc + swap_amounts.btc
- bitcoin::Amount::from_sat(2 * xmr_btc::bitcoin::TX_FEE)
- bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE)
);
assert_eq!(
bob_final_btc_balance,

Loading…
Cancel
Save