Merge pull request #41 from rishflab/automated

Simplify swap and recovery execution
pull/58/head
rishflab 4 years ago committed by GitHub
commit a9bb4e6bed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,12 +7,14 @@ description = "XMR/BTC trustless atomic swaps."
[dependencies]
anyhow = "1"
async-recursion = "0.3.1"
async-trait = "0.1"
atty = "0.2"
backoff = { version = "0.2", features = ["tokio"] }
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" }
conquer-once = "0.3"
derivative = "2"
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat", "serde", "serialization"] }
futures = { version = "0.3", default-features = false }

@ -1,26 +1,5 @@
//! Run an XMR/BTC swap in the role of Alice.
//! Alice holds XMR and wishes receive BTC.
use anyhow::Result;
use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use genawaiter::GeneratorState;
use libp2p::{
core::{identity::Keypair, Multiaddr},
request_response::ResponseChannel,
NetworkBehaviour, PeerId,
};
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;
mod message1;
mod message2;
mod message3;
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
use crate::{
bitcoin,
@ -36,20 +15,42 @@ use crate::{
storage::Database,
SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
};
use anyhow::Result;
use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use genawaiter::GeneratorState;
use libp2p::{
core::{identity::Keypair, Multiaddr},
request_response::ResponseChannel,
NetworkBehaviour, PeerId,
};
use rand::rngs::OsRng;
use std::{sync::Arc, time::Duration};
use tokio::sync::Mutex;
use tracing::{debug, info, warn};
use uuid::Uuid;
use xmr_btc::{
alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0},
bitcoin::BroadcastSignedTransaction,
bob,
bob, cross_curve_dleq,
monero::{CreateWalletForOutput, Transfer},
};
mod amounts;
mod execution;
mod message0;
mod message1;
mod message2;
mod message3;
pub mod swap;
pub async fn swap(
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
db: Database,
listen: Multiaddr,
transport: SwapTransport,
behaviour: Alice,
behaviour: Behaviour,
) -> Result<()> {
struct Network {
swarm: Arc<Mutex<Swarm>>,
@ -128,8 +129,13 @@ pub async fn swap(
// TODO: Pass this in using <R: RngCore + CryptoRng>
let rng = &mut OsRng;
let a = bitcoin::SecretKey::new_random(rng);
let s_a = cross_curve_dleq::Scalar::random(rng);
let v_a = monero::PrivateViewKey::new_random(rng);
let state = State0::new(
rng,
a,
s_a,
v_a,
btc,
xmr,
REFUND_TIMELOCK,
@ -178,7 +184,7 @@ pub async fn swap(
};
let swap_id = Uuid::new_v4();
db.insert_latest_state(swap_id, state::Alice::Handshaken(state3.clone()).into())
db.insert_latest_state(swap_id, state::Alice::Negotiated(state3.clone()).into())
.await?;
info!("Handshake complete, we now have State3 for Alice.");
@ -272,9 +278,13 @@ pub async fn swap(
}
}
pub type Swarm = libp2p::Swarm<Alice>;
pub type Swarm = libp2p::Swarm<Behaviour>;
fn new_swarm(listen: Multiaddr, transport: SwapTransport, behaviour: Alice) -> Result<Swarm> {
pub fn new_swarm(
listen: Multiaddr,
transport: SwapTransport,
behaviour: Behaviour,
) -> Result<Swarm> {
use anyhow::Context as _;
let local_peer_id = behaviour.peer_id();
@ -297,6 +307,8 @@ fn new_swarm(listen: Multiaddr, transport: SwapTransport, behaviour: Alice) -> R
#[derive(Debug)]
pub enum OutEvent {
ConnectionEstablished(PeerId),
// TODO (Franck): Change this to get both amounts so parties can verify the amounts are
// expected early on.
Request(amounts::OutEvent), // Not-uniform with Bob on purpose, ready for adding Xmr event.
Message0(bob::Message0),
Message1 {
@ -362,7 +374,7 @@ impl From<message3::OutEvent> for OutEvent {
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "OutEvent", event_process = false)]
#[allow(missing_debug_implementations)]
pub struct Alice {
pub struct Behaviour {
pt: PeerTracker,
amounts: Amounts,
message0: Message0,
@ -373,7 +385,7 @@ pub struct Alice {
identity: Keypair,
}
impl Alice {
impl Behaviour {
pub fn identity(&self) -> Keypair {
self.identity.clone()
}
@ -389,6 +401,7 @@ impl Alice {
info!("Sent amounts response");
}
// TODO(Franck) remove
/// Message0 gets sent within the network layer using this state0.
pub fn set_state0(&mut self, state: State0) {
debug!("Set state 0");
@ -416,7 +429,7 @@ impl Alice {
}
}
impl Default for Alice {
impl Default for Behaviour {
fn default() -> Self {
let identity = Keypair::generate_ed25519();
@ -433,8 +446,8 @@ impl Default for Alice {
}
fn calculate_amounts(btc: ::bitcoin::Amount) -> SwapAmounts {
// TODO: Get this from an exchange.
// This value corresponds to 100 XMR per BTC
// TODO (Franck): This should instead verify that the received amounts matches
// the command line arguments This value corresponds to 100 XMR per BTC
const PICONERO_PER_SAT: u64 = 1_000_000;
let picos = btc.as_sat() * PICONERO_PER_SAT;

@ -0,0 +1,379 @@
use crate::{
alice::{amounts, OutEvent, Swarm},
bitcoin, monero,
network::request_response::AliceToBob,
SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
};
use anyhow::{bail, Context, Result};
use conquer_once::Lazy;
use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic};
use futures::{
future::{select, Either},
pin_mut,
};
use libp2p::request_response::ResponseChannel;
use sha2::Sha256;
use std::{sync::Arc, time::Duration};
use tokio::time::timeout;
use xmr_btc::{
alice,
alice::{State0, State3},
bitcoin::{
poll_until_block_height_is_gte, BlockHeight, BroadcastSignedTransaction,
EncryptedSignature, GetRawTransaction, TransactionBlockHeight, TxCancel, TxLock, TxRefund,
WaitForTransactionFinality, WatchForRawTransaction,
},
cross_curve_dleq,
monero::Transfer,
};
// For each step, we are giving Bob 10 minutes to act.
static BOB_TIME_TO_ACT: Lazy<Duration> = Lazy::new(|| Duration::from_secs(10 * 60));
// 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(
amounts: SwapAmounts,
a: bitcoin::SecretKey,
s_a: cross_curve_dleq::Scalar,
v_a: monero::PrivateViewKey,
swarm: &mut Swarm,
bitcoin_wallet: Arc<bitcoin::Wallet>,
) -> Result<(ResponseChannel<AliceToBob>, State3)> {
let event = timeout(*BOB_TIME_TO_ACT, swarm.next())
.await
.context("Failed to receive dial connection from Bob")?;
match event {
OutEvent::ConnectionEstablished(_bob_peer_id) => {}
other => bail!("Unexpected event received: {:?}", other),
}
let event = timeout(*BOB_TIME_TO_ACT, swarm.next())
.await
.context("Failed to receive amounts from Bob")?;
let (btc, channel) = match event {
OutEvent::Request(amounts::OutEvent::Btc { btc, channel }) => (btc, channel),
other => bail!("Unexpected event received: {:?}", other),
};
if btc != amounts.btc {
bail!(
"Bob proposed a different amount; got {}, expected: {}",
btc,
amounts.btc
);
}
// TODO: get an ack from libp2p2
swarm.send_amounts(channel, amounts);
let redeem_address = bitcoin_wallet.as_ref().new_address().await?;
let punish_address = redeem_address.clone();
let state0 = State0::new(
a,
s_a,
v_a,
amounts.btc,
amounts.xmr,
REFUND_TIMELOCK,
PUNISH_TIMELOCK,
redeem_address,
punish_address,
);
// TODO(Franck): Understand why this is needed.
swarm.set_state0(state0.clone());
let event = timeout(*BOB_TIME_TO_ACT, swarm.next())
.await
.context("Failed to receive message 0 from Bob")?;
let message0 = match event {
OutEvent::Message0(msg) => msg,
other => bail!("Unexpected event received: {:?}", other),
};
let state1 = state0.receive(message0)?;
let event = timeout(*BOB_TIME_TO_ACT, swarm.next())
.await
.context("Failed to receive message 1 from Bob")?;
let (msg, channel) = match event {
OutEvent::Message1 { msg, channel } => (msg, channel),
other => bail!("Unexpected event: {:?}", other),
};
let state2 = state1.receive(msg);
let message1 = state2.next_message();
swarm.send_message1(channel, message1);
let event = timeout(*BOB_TIME_TO_ACT, swarm.next())
.await
.context("Failed to receive message 2 from Bob")?;
let (msg, channel) = match event {
OutEvent::Message2 { msg, channel } => (msg, channel),
other => bail!("Unexpected event: {:?}", other),
};
let state3 = state2.receive(msg)?;
Ok((channel, state3))
}
pub async fn wait_for_locked_bitcoin<W>(
lock_bitcoin_txid: bitcoin::Txid,
bitcoin_wallet: Arc<W>,
) -> Result<()>
where
W: WatchForRawTransaction + WaitForTransactionFinality,
{
// We assume we will see Bob's transaction in the mempool first.
timeout(
*BOB_TIME_TO_ACT,
bitcoin_wallet.watch_for_raw_transaction(lock_bitcoin_txid),
)
.await
.context("Failed to find lock Bitcoin tx")?;
// // We saw the transaction in the mempool, waiting for it to be confirmed.
// bitcoin_wallet
// .wait_for_transaction_finality(lock_bitcoin_txid)
// .await;
Ok(())
}
pub async fn lock_xmr<W>(
channel: ResponseChannel<AliceToBob>,
amounts: SwapAmounts,
state3: State3,
swarm: &mut Swarm,
monero_wallet: Arc<W>,
) -> Result<()>
where
W: Transfer,
{
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey {
scalar: state3.s_a.into_ed25519(),
});
let public_spend_key = S_a + state3.S_b_monero;
let public_view_key = state3.v.public();
let (transfer_proof, _) = monero_wallet
.transfer(public_spend_key, public_view_key, amounts.xmr)
.await?;
// TODO(Franck): Wait for Monero to be confirmed once
swarm.send_message2(channel, alice::Message2 {
tx_lock_proof: transfer_proof,
});
Ok(())
}
pub async fn wait_for_bitcoin_encrypted_signature(swarm: &mut Swarm) -> Result<EncryptedSignature> {
let event = timeout(*MONERO_MAX_FINALITY_TIME, swarm.next())
.await
.context("Failed to receive Bitcoin encrypted signature from Bob")?;
match event {
OutEvent::Message3(msg) => Ok(msg.tx_redeem_encsig),
other => bail!(
"Expected Bob's Bitcoin redeem encrypted signature, got: {:?}",
other
),
}
}
pub fn build_bitcoin_redeem_transaction(
encrypted_signature: EncryptedSignature,
tx_lock: &TxLock,
a: bitcoin::SecretKey,
s_a: cross_curve_dleq::Scalar,
B: bitcoin::PublicKey,
redeem_address: &bitcoin::Address,
) -> Result<bitcoin::Transaction> {
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
let tx_redeem = bitcoin::TxRedeem::new(tx_lock, redeem_address);
bitcoin::verify_encsig(
B.clone(),
s_a.into_secp256k1().into(),
&tx_redeem.digest(),
&encrypted_signature,
)
.context("Invalid encrypted signature received")?;
let sig_a = a.sign(tx_redeem.digest());
let sig_b = adaptor.decrypt_signature(&s_a.into_secp256k1(), encrypted_signature);
let tx = tx_redeem
.add_signatures(&tx_lock, (a.public(), sig_a), (B, sig_b))
.context("sig_{a,b} are invalid for tx_redeem")?;
Ok(tx)
}
pub async fn publish_bitcoin_redeem_transaction<W>(
redeem_tx: bitcoin::Transaction,
bitcoin_wallet: Arc<W>,
) -> Result<()>
where
W: BroadcastSignedTransaction + WaitForTransactionFinality,
{
let _tx_id = bitcoin_wallet
.broadcast_signed_transaction(redeem_tx)
.await?;
// // TODO(Franck): Not sure if we wait for finality here or just mined
// bitcoin_wallet.wait_for_transaction_finality(tx_id).await;
Ok(())
}
pub async fn publish_cancel_transaction<W>(
tx_lock: TxLock,
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
refund_timelock: u32,
tx_cancel_sig_bob: bitcoin::Signature,
bitcoin_wallet: Arc<W>,
) -> Result<bitcoin::TxCancel>
where
W: GetRawTransaction + TransactionBlockHeight + BlockHeight + BroadcastSignedTransaction,
{
// First wait for t1 to expire
let tx_lock_height = bitcoin_wallet
.transaction_block_height(tx_lock.txid())
.await;
poll_until_block_height_is_gte(bitcoin_wallet.as_ref(), tx_lock_height + refund_timelock).await;
let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone());
// If Bob hasn't yet broadcasted the tx cancel, we do it
if bitcoin_wallet
.get_raw_transaction(tx_cancel.txid())
.await
.is_err()
{
// TODO(Franck): Maybe the cancel transaction is already mined, in this case,
// the broadcast will error out.
let sig_a = a.sign(tx_cancel.digest());
let sig_b = tx_cancel_sig_bob.clone();
let tx_cancel = 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");
// TODO(Franck): Error handling is delicate, why can't we broadcast?
bitcoin_wallet
.broadcast_signed_transaction(tx_cancel)
.await?;
// TODO(Franck): Wait until transaction is mined and returned mined
// block height
}
Ok(tx_cancel)
}
pub async fn wait_for_bitcoin_refund<W>(
tx_cancel: &TxCancel,
cancel_tx_height: u32,
punish_timelock: u32,
refund_address: &bitcoin::Address,
bitcoin_wallet: Arc<W>,
) -> Result<(bitcoin::TxRefund, Option<bitcoin::Transaction>)>
where
W: BlockHeight + WatchForRawTransaction,
{
let punish_timelock_expired =
poll_until_block_height_is_gte(bitcoin_wallet.as_ref(), cancel_tx_height + punish_timelock);
let tx_refund = bitcoin::TxRefund::new(tx_cancel, refund_address);
// TODO(Franck): This only checks the mempool, need to cater for the case where
// the transaction goes directly in a block
let seen_refund_tx = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid());
pin_mut!(punish_timelock_expired);
pin_mut!(seen_refund_tx);
match select(punish_timelock_expired, seen_refund_tx).await {
Either::Left(_) => Ok((tx_refund, None)),
Either::Right((published_refund_tx, _)) => Ok((tx_refund, Some(published_refund_tx))),
}
}
pub fn extract_monero_private_key(
published_refund_tx: bitcoin::Transaction,
tx_refund: TxRefund,
s_a: cross_curve_dleq::Scalar,
a: bitcoin::SecretKey,
S_b_bitcoin: bitcoin::PublicKey,
) -> Result<monero::PrivateKey> {
let s_a = monero::PrivateKey {
scalar: s_a.into_ed25519(),
};
let tx_refund_sig = tx_refund
.extract_signature_by_key(published_refund_tx, a.public())
.context("Failed to extract signature from Bitcoin refund tx")?;
let tx_refund_encsig = a.encsign(S_b_bitcoin.clone(), tx_refund.digest());
let s_b = bitcoin::recover(S_b_bitcoin, tx_refund_sig, tx_refund_encsig)
.context("Failed to recover Monero secret key from Bitcoin signature")?;
let s_b = monero::private_key_from_secp256k1_scalar(s_b.into());
let spend_key = s_a + s_b;
Ok(spend_key)
}
pub fn build_bitcoin_punish_transaction(
tx_lock: &TxLock,
refund_timelock: u32,
punish_address: &bitcoin::Address,
punish_timelock: u32,
tx_punish_sig_bob: bitcoin::Signature,
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
) -> Result<bitcoin::Transaction> {
let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone());
let tx_punish = bitcoin::TxPunish::new(&tx_cancel, &punish_address, punish_timelock);
let sig_a = a.sign(tx_punish.digest());
let sig_b = tx_punish_sig_bob;
let signed_tx_punish = tx_punish
.add_signatures(&tx_cancel, (a.public(), sig_a), (B, sig_b))
.expect("sig_{a,b} to be valid signatures for tx_cancel");
Ok(signed_tx_punish)
}
pub async fn publish_bitcoin_punish_transaction<W>(
punish_tx: bitcoin::Transaction,
bitcoin_wallet: Arc<W>,
) -> Result<bitcoin::Txid>
where
W: BroadcastSignedTransaction + WaitForTransactionFinality,
{
let txid = bitcoin_wallet
.broadcast_signed_transaction(punish_tx)
.await?;
// todo: enable this once trait is implemented
// bitcoin_wallet.wait_for_transaction_finality(txid).await;
Ok(txid)
}

@ -0,0 +1,351 @@
//! Run an XMR/BTC swap in the role of Alice.
//! Alice holds XMR and wishes receive BTC.
use crate::{
alice::{
execution::{
build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction,
extract_monero_private_key, lock_xmr, negotiate, publish_bitcoin_punish_transaction,
publish_bitcoin_redeem_transaction, publish_cancel_transaction,
wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin,
},
Swarm,
},
bitcoin,
bitcoin::EncryptedSignature,
monero,
network::request_response::AliceToBob,
SwapAmounts,
};
use anyhow::Result;
use async_recursion::async_recursion;
use futures::{
future::{select, Either},
pin_mut,
};
use libp2p::request_response::ResponseChannel;
use rand::{CryptoRng, RngCore};
use std::sync::Arc;
use xmr_btc::{
alice::State3,
bitcoin::{TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction},
cross_curve_dleq,
monero::CreateWalletForOutput,
};
trait Rng: RngCore + CryptoRng + Send {}
impl<T> Rng for T where T: RngCore + CryptoRng + Send {}
// The same data structure is used for swap execution and recovery.
// This allows for a seamless transition from a failed swap to recovery.
#[allow(clippy::large_enum_variant)]
pub enum AliceState {
Started {
amounts: SwapAmounts,
a: bitcoin::SecretKey,
s_a: cross_curve_dleq::Scalar,
v_a: monero::PrivateViewKey,
},
Negotiated {
channel: ResponseChannel<AliceToBob>,
amounts: SwapAmounts,
state3: State3,
},
BtcLocked {
channel: ResponseChannel<AliceToBob>,
amounts: SwapAmounts,
state3: State3,
},
XmrLocked {
state3: State3,
},
EncSignLearned {
state3: State3,
encrypted_signature: EncryptedSignature,
},
BtcRedeemed,
BtcCancelled {
state3: State3,
tx_cancel: TxCancel,
},
BtcRefunded {
tx_refund: TxRefund,
published_refund_tx: ::bitcoin::Transaction,
state3: State3,
},
BtcPunishable {
tx_refund: TxRefund,
state3: State3,
},
XmrRefunded,
WaitingToCancel {
state3: State3,
},
Punished,
SafelyAborted,
}
// State machine driver for swap execution
#[async_recursion]
pub async fn swap(
state: AliceState,
mut swarm: Swarm,
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
monero_wallet: Arc<crate::monero::Wallet>,
) -> Result<AliceState> {
match state {
AliceState::Started {
amounts,
a,
s_a,
v_a,
} => {
let (channel, state3) =
negotiate(amounts, a, s_a, v_a, &mut swarm, bitcoin_wallet.clone()).await?;
swap(
AliceState::Negotiated {
channel,
amounts,
state3,
},
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
AliceState::Negotiated {
state3,
channel,
amounts,
} => {
let _ = wait_for_locked_bitcoin(state3.tx_lock.txid(), bitcoin_wallet.clone()).await?;
swap(
AliceState::BtcLocked {
channel,
amounts,
state3,
},
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
AliceState::BtcLocked {
channel,
amounts,
state3,
} => {
lock_xmr(
channel,
amounts,
state3.clone(),
&mut swarm,
monero_wallet.clone(),
)
.await?;
swap(
AliceState::XmrLocked { state3 },
swarm,
bitcoin_wallet,
monero_wallet,
)
.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).await {
Ok(encrypted_signature) => {
swap(
AliceState::EncSignLearned {
state3,
encrypted_signature,
},
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
Err(_) => {
swap(
AliceState::WaitingToCancel { state3 },
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
}
}
AliceState::EncSignLearned {
state3,
encrypted_signature,
} => {
let signed_tx_redeem = match build_bitcoin_redeem_transaction(
encrypted_signature,
&state3.tx_lock,
state3.a.clone(),
state3.s_a,
state3.B.clone(),
&state3.redeem_address,
) {
Ok(tx) => tx,
Err(_) => {
return swap(
AliceState::WaitingToCancel { state3 },
swarm,
bitcoin_wallet,
monero_wallet,
)
.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()).await?;
swap(
AliceState::BtcRedeemed,
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
AliceState::WaitingToCancel { state3 } => {
let tx_cancel = publish_cancel_transaction(
state3.tx_lock.clone(),
state3.a.clone(),
state3.B.clone(),
state3.refund_timelock,
state3.tx_cancel_sig_bob.clone(),
bitcoin_wallet.clone(),
)
.await?;
swap(
AliceState::BtcCancelled { state3, tx_cancel },
swarm,
bitcoin_wallet,
monero_wallet,
)
.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 => {
swap(
AliceState::BtcPunishable { tx_refund, state3 },
swarm,
bitcoin_wallet.clone(),
monero_wallet,
)
.await
}
Some(published_refund_tx) => {
swap(
AliceState::BtcRefunded {
tx_refund,
published_refund_tx,
state3,
},
swarm,
bitcoin_wallet.clone(),
monero_wallet,
)
.await
}
}
}
AliceState::BtcRefunded {
tx_refund,
published_refund_tx,
state3,
} => {
let spend_key = extract_monero_private_key(
published_refund_tx,
tx_refund,
state3.s_a,
state3.a.clone(),
state3.S_b_bitcoin,
)?;
let view_key = state3.v;
monero_wallet
.create_and_load_wallet_for_output(spend_key, view_key)
.await?;
Ok(AliceState::XmrRefunded)
}
AliceState::BtcPunishable { tx_refund, state3 } => {
let signed_tx_punish = build_bitcoin_punish_transaction(
&state3.tx_lock,
state3.refund_timelock,
&state3.punish_address,
state3.punish_timelock,
state3.tx_punish_sig_bob.clone(),
state3.a.clone(),
state3.B.clone(),
)?;
let punish_tx_finalised =
publish_bitcoin_punish_transaction(signed_tx_punish, bitcoin_wallet.clone());
let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid());
pin_mut!(punish_tx_finalised);
pin_mut!(refund_tx_seen);
match select(punish_tx_finalised, refund_tx_seen).await {
Either::Left(_) => {
swap(
AliceState::Punished,
swarm,
bitcoin_wallet.clone(),
monero_wallet,
)
.await
}
Either::Right((published_refund_tx, _)) => {
swap(
AliceState::BtcRefunded {
tx_refund,
published_refund_tx,
state3,
},
swarm,
bitcoin_wallet.clone(),
monero_wallet,
)
.await
}
}
}
AliceState::XmrRefunded => Ok(AliceState::XmrRefunded),
AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed),
AliceState::Punished => Ok(AliceState::Punished),
AliceState::SafelyAborted => Ok(AliceState::SafelyAborted),
}
}

@ -15,17 +15,16 @@
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::{self, Alice},
bitcoin,
bob::{self, Bob},
alice, bitcoin, bob,
cli::Options,
monero,
network::transport::{build, build_tor, SwapTransport},
recover::recover,
storage::Database,
Cmd, Rsp, SwapAmounts,
};
use tracing::info;
@ -33,20 +32,12 @@ 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.
#[tokio::main]
async fn main() -> Result<()> {
let opt = Options::from_args();
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();
@ -59,7 +50,7 @@ async fn main() -> Result<()> {
} => {
info!("running swap node as Alice ...");
let behaviour = Alice::default();
let behaviour = alice::Behaviour::default();
let local_key_pair = behaviour.identity();
let (listen_addr, _ac, transport) = match tor_port {
@ -107,7 +98,7 @@ async fn main() -> Result<()> {
} => {
info!("running swap node as Bob ...");
let behaviour = Bob::default();
let behaviour = bob::Behaviour::default();
let local_key_pair = behaviour.identity();
let transport = match tor {
@ -187,7 +178,7 @@ async fn swap_as_alice(
db: Database,
addr: Multiaddr,
transport: SwapTransport,
behaviour: Alice,
behaviour: alice::Behaviour,
) -> Result<()> {
alice::swap(
bitcoin_wallet,
@ -207,7 +198,7 @@ async fn swap_as_bob(
sats: u64,
alice: Multiaddr,
transport: SwapTransport,
behaviour: Bob,
behaviour: bob::Behaviour,
) -> Result<()> {
let (cmd_tx, mut cmd_rx) = mpsc::channel(1);
let (mut rsp_tx, rsp_rx) = mpsc::channel(1);

@ -3,15 +3,15 @@ use std::time::Duration;
use anyhow::Result;
use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Transaction};
use bitcoin::util::psbt::PartiallySignedTransaction;
use bitcoin_harness::bitcoind_rpc::PsbtBase64;
use reqwest::Url;
use tokio::time;
use xmr_btc::bitcoin::{
BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TransactionBlockHeight,
WatchForRawTransaction,
};
pub use ::bitcoin::{Address, Transaction};
pub use xmr_btc::bitcoin::*;
pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600;
@ -85,21 +85,12 @@ impl SignTxLock for Wallet {
#[async_trait]
impl BroadcastSignedTransaction for Wallet {
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid> {
let txid = self.0.send_raw_transaction(transaction).await?;
// TODO: Instead of guessing how long it will take for the transaction to be
// mined we should ask bitcoind for the number of confirmations on `txid`
// give time for transaction to be mined
time::delay_for(Duration::from_millis(1100)).await;
Ok(txid)
Ok(self.0.send_raw_transaction(transaction).await?)
}
}
// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed
// to `ConstantBackoff`.
#[async_trait]
impl WatchForRawTransaction for Wallet {
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction {
@ -110,6 +101,14 @@ impl WatchForRawTransaction for Wallet {
}
}
#[async_trait]
impl GetRawTransaction for Wallet {
// todo: potentially replace with option
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
Ok(self.0.get_raw_transaction(txid).await?)
}
}
#[async_trait]
impl BlockHeight for Wallet {
async fn block_height(&self) -> u32 {
@ -146,3 +145,10 @@ impl TransactionBlockHeight for Wallet {
.expect("transient errors to be retried")
}
}
#[async_trait]
impl WaitForTransactionFinality for Wallet {
async fn wait_for_transaction_finality(&self, _txid: Txid) {
todo!()
}
}

@ -1,6 +1,7 @@
//! Run an XMR/BTC swap in the role of Bob.
//! Bob holds BTC and wishes receive XMR.
use anyhow::Result;
use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use futures::{
@ -16,10 +17,12 @@ use tracing::{debug, info, warn};
use uuid::Uuid;
mod amounts;
mod execution;
mod message0;
mod message1;
mod message2;
mod message3;
pub mod swap;
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
use crate::{
@ -51,7 +54,7 @@ pub async fn swap(
mut cmd_tx: Sender<Cmd>,
mut rsp_rx: Receiver<Rsp>,
transport: SwapTransport,
behaviour: Bob,
behaviour: Behaviour,
) -> Result<()> {
struct Network(Swarm);
@ -234,9 +237,9 @@ pub async fn swap(
}
}
pub type Swarm = libp2p::Swarm<Bob>;
pub type Swarm = libp2p::Swarm<Behaviour>;
fn new_swarm(transport: SwapTransport, behaviour: Bob) -> Result<Swarm> {
pub fn new_swarm(transport: SwapTransport, behaviour: Behaviour) -> Result<Swarm> {
let local_peer_id = behaviour.peer_id();
let swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone())
@ -315,7 +318,7 @@ impl From<message3::OutEvent> for OutEvent {
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "OutEvent", event_process = false)]
#[allow(missing_debug_implementations)]
pub struct Bob {
pub struct Behaviour {
pt: PeerTracker,
amounts: Amounts,
message0: Message0,
@ -326,7 +329,7 @@ pub struct Bob {
identity: Keypair,
}
impl Bob {
impl Behaviour {
pub fn identity(&self) -> Keypair {
self.identity.clone()
}
@ -373,8 +376,8 @@ impl Bob {
}
}
impl Default for Bob {
fn default() -> Bob {
impl Default for Behaviour {
fn default() -> Behaviour {
let identity = Keypair::generate_ed25519();
Self {

@ -0,0 +1,52 @@
use crate::{
bob::{OutEvent, Swarm},
SwapAmounts,
};
use anyhow::Result;
use libp2p::core::Multiaddr;
use rand::{CryptoRng, RngCore};
use std::sync::Arc;
use xmr_btc::bob::State2;
pub async fn negotiate<R>(
state0: xmr_btc::bob::State0,
amounts: SwapAmounts,
swarm: &mut Swarm,
addr: Multiaddr,
mut rng: R,
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
) -> Result<State2>
where
R: RngCore + CryptoRng + Send,
{
libp2p::Swarm::dial_addr(swarm, addr)?;
let alice = match swarm.next().await {
OutEvent::ConnectionEstablished(alice) => alice,
other => panic!("unexpected event: {:?}", other),
};
swarm.request_amounts(alice.clone(), amounts.btc.as_sat());
// todo: see if we can remove
let (_btc, _xmr) = match swarm.next().await {
OutEvent::Amounts(amounts) => (amounts.btc, amounts.xmr),
other => panic!("unexpected event: {:?}", other),
};
swarm.send_message0(alice.clone(), state0.next_message(&mut rng));
let state1 = match swarm.next().await {
OutEvent::Message0(msg) => state0.receive(bitcoin_wallet.as_ref(), msg).await?,
other => panic!("unexpected event: {:?}", other),
};
swarm.send_message1(alice.clone(), state1.next_message());
let state2 = match swarm.next().await {
OutEvent::Message1(msg) => state1.receive(msg)?,
other => panic!("unexpected event: {:?}", other),
};
swarm.send_message2(alice.clone(), state2.next_message());
Ok(state2)
}

@ -0,0 +1,265 @@
use crate::{
bob::{execution::negotiate, OutEvent, Swarm},
storage::Database,
SwapAmounts,
};
use anyhow::Result;
use async_recursion::async_recursion;
use libp2p::{core::Multiaddr, PeerId};
use rand::{CryptoRng, RngCore};
use std::sync::Arc;
use tracing::debug;
use uuid::Uuid;
use xmr_btc::bob::{self};
// The same data structure is used for swap execution and recovery.
// This allows for a seamless transition from a failed swap to recovery.
pub enum BobState {
Started {
state0: bob::State0,
amounts: SwapAmounts,
peer_id: PeerId,
addr: Multiaddr,
},
Negotiated(bob::State2, PeerId),
BtcLocked(bob::State3, PeerId),
XmrLocked(bob::State4, PeerId),
EncSigSent(bob::State4, PeerId),
BtcRedeemed(bob::State5),
Cancelled(bob::State4),
BtcRefunded,
XmrRedeemed,
Punished,
SafelyAborted,
}
// State machine driver for swap execution
#[async_recursion]
pub async fn swap<R>(
state: BobState,
mut swarm: Swarm,
db: Database,
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
monero_wallet: Arc<crate::monero::Wallet>,
mut rng: R,
swap_id: Uuid,
) -> Result<BobState>
where
R: RngCore + CryptoRng + Send,
{
match state {
BobState::Started {
state0,
amounts,
peer_id,
addr,
} => {
let state2 = negotiate(
state0,
amounts,
&mut swarm,
addr,
&mut rng,
bitcoin_wallet.clone(),
)
.await?;
swap(
BobState::Negotiated(state2, peer_id),
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
swap_id,
)
.await
}
BobState::Negotiated(state2, alice_peer_id) => {
// Alice and Bob have exchanged info
let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).await?;
// db.insert_latest_state(state);
swap(
BobState::BtcLocked(state3, alice_peer_id),
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
swap_id,
)
.await
}
// Bob has locked Btc
// Watch for Alice to Lock Xmr or for t1 to elapse
BobState::BtcLocked(state3, alice_peer_id) => {
// todo: watch until t1, not indefinetely
let state4 = match swarm.next().await {
OutEvent::Message2(msg) => {
state3
.watch_for_lock_xmr(monero_wallet.as_ref(), msg)
.await?
}
other => panic!("unexpected event: {:?}", other),
};
swap(
BobState::XmrLocked(state4, alice_peer_id),
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
swap_id,
)
.await
}
BobState::XmrLocked(state, alice_peer_id) => {
// Alice has locked Xmr
// Bob sends Alice his key
let tx_redeem_encsig = state.tx_redeem_encsig();
// Do we have to wait for a response?
// What if Alice fails to receive this? Should we always resend?
// todo: If we cannot dial Alice we should go to EncSigSent. Maybe dialing
// should happen in this arm?
swarm.send_message3(alice_peer_id.clone(), tx_redeem_encsig);
// Sadly we have to poll the swarm to get make sure the message is sent?
// 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 swarm.next().await {
OutEvent::Message3 => {
debug!("Got Message3 empty response");
}
other => panic!("unexpected event: {:?}", other),
};
swap(
BobState::EncSigSent(state, alice_peer_id),
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
swap_id,
)
.await
}
BobState::EncSigSent(state, ..) => {
// Watch for redeem
let redeem_watcher = state.watch_for_redeem_btc(bitcoin_wallet.as_ref());
let t1_timeout = state.wait_for_t1(bitcoin_wallet.as_ref());
tokio::select! {
val = redeem_watcher => {
swap(
BobState::BtcRedeemed(val?),
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
swap_id,
)
.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?;
}
swap(
BobState::Cancelled(state),
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
swap_id
)
.await
}
}
}
BobState::BtcRedeemed(state) => {
// Bob redeems XMR using revealed s_a
state.claim_xmr(monero_wallet.as_ref()).await?;
swap(
BobState::XmrRedeemed,
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
swap_id,
)
.await
}
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
// #[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),
// }
// }

@ -1,9 +1,12 @@
#![allow(non_snake_case)]
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display};
pub mod alice;
pub mod bitcoin;
pub mod bob;
pub mod cli;
pub mod monero;
pub mod network;
pub mod recover;
@ -11,8 +14,8 @@ pub mod state;
pub mod storage;
pub mod tor;
const REFUND_TIMELOCK: u32 = 10; // Relative timelock, this is number of blocks. TODO: What should it be?
const PUNISH_TIMELOCK: u32 = 10; // FIXME: What should this be?
pub const REFUND_TIMELOCK: u32 = 10; // Relative timelock, this is number of blocks. TODO: What should it be?
pub const PUNISH_TIMELOCK: u32 = 10; // FIXME: What should this be?
pub type Never = std::convert::Infallible;
@ -31,6 +34,7 @@ pub enum Rsp {
/// XMR/BTC swap amounts.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
// TODO(Franck): review necessity of this struct
pub struct SwapAmounts {
/// Amount of BTC to swap.
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]

@ -45,7 +45,7 @@ pub async fn alice_recover(
state: Alice,
) -> Result<()> {
match state {
Alice::Handshaken(_) | Alice::BtcLocked(_) | Alice::SwapComplete => {
Alice::Negotiated(_) | Alice::BtcLocked(_) | Alice::SwapComplete => {
info!("Nothing to do");
}
Alice::XmrLocked(state) => {

@ -12,7 +12,7 @@ pub enum Swap {
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub enum Alice {
Handshaken(alice::State3),
Negotiated(alice::State3),
BtcLocked(alice::State3),
XmrLocked(alice::State3),
BtcRedeemable {
@ -63,7 +63,7 @@ impl Display for Swap {
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::Negotiated(_) => 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"),

@ -2,17 +2,18 @@ use bitcoin_harness::Bitcoind;
use futures::{channel::mpsc, future::try_join};
use libp2p::Multiaddr;
use monero_harness::Monero;
use rand::rngs::OsRng;
use std::sync::Arc;
use swap::{alice, bob, network::transport::build, storage::Database};
use swap::{
alice, alice::swap::AliceState, bob, bob::swap::BobState, network::transport::build,
storage::Database, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
};
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
use uuid::Uuid;
use xmr_btc::{bitcoin, cross_curve_dleq};
#[ignore]
#[tokio::test]
async fn swap() {
use tracing_subscriber::util::SubscriberInitExt as _;
@ -69,7 +70,7 @@ async fn swap() {
));
let bob_xmr_wallet = Arc::new(swap::monero::Wallet(monero.wallet("bob").unwrap().client()));
let alice_behaviour = alice::Alice::default();
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();
@ -86,7 +87,7 @@ async fn swap() {
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_behaviour = bob::Behaviour::default();
let bob_transport = build(bob_behaviour.identity()).unwrap();
let bob_swap = bob::swap(
bob_btc_wallet.clone(),
@ -122,3 +123,139 @@ async fn swap() {
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 alice_swap = alice::swap::swap(
alice_state,
alice_swarm,
alice_btc_wallet.clone(),
alice_xmr_wallet.clone(),
);
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(
bob_state,
bob_swarm,
bob_db,
bob_btc_wallet.clone(),
bob_xmr_wallet.clone(),
OsRng,
Uuid::new_v4(),
);
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);
}

@ -416,8 +416,14 @@ impl State {
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
) -> Self {
let a = bitcoin::SecretKey::new_random(rng);
let s_a = cross_curve_dleq::Scalar::random(rng);
let v_a = monero::PrivateViewKey::new_random(rng);
Self::State0(State0::new(
rng,
a,
s_a,
v_a,
btc,
xmr,
refund_timelock,
@ -443,8 +449,11 @@ pub struct State0 {
}
impl State0 {
pub fn new<R: RngCore + CryptoRng>(
rng: &mut R,
#[allow(clippy::too_many_arguments)]
pub fn new(
a: bitcoin::SecretKey,
s_a: cross_curve_dleq::Scalar,
v_a: monero::PrivateViewKey,
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
@ -452,11 +461,6 @@ impl State0 {
redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
) -> Self {
let a = bitcoin::SecretKey::new_random(rng);
let s_a = cross_curve_dleq::Scalar::random(rng);
let v_a = monero::PrivateViewKey::new_random(rng);
Self {
a,
s_a,
@ -617,6 +621,7 @@ impl State2 {
S_b_monero: self.S_b_monero,
S_b_bitcoin: self.S_b_bitcoin,
v: self.v,
// TODO(Franck): Review if these amounts are actually needed
btc: self.btc,
xmr: self.xmr,
refund_timelock: self.refund_timelock,

@ -186,6 +186,11 @@ pub trait WatchForRawTransaction {
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction;
}
#[async_trait]
pub trait WaitForTransactionFinality {
async fn wait_for_transaction_finality(&self, txid: Txid);
}
#[async_trait]
pub trait BlockHeight {
async fn block_height(&self) -> u32;
@ -196,6 +201,16 @@ pub trait TransactionBlockHeight {
async fn transaction_block_height(&self, txid: Txid) -> u32;
}
#[async_trait]
pub trait WaitForBlockHeight {
async fn wait_for_block_height(&self, height: u32);
}
#[async_trait]
pub trait GetRawTransaction {
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction>;
}
pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result<SecretKey> {
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();

@ -32,7 +32,11 @@ use tokio::{sync::Mutex, time::timeout};
use tracing::error;
pub mod message;
use crate::monero::{CreateWalletForOutput, WatchForTransfer};
use crate::{
bitcoin::{BlockHeight, GetRawTransaction, TransactionBlockHeight},
monero::{CreateWalletForOutput, WatchForTransfer},
};
use ::bitcoin::{Transaction, Txid};
pub use message::{Message, Message0, Message1, Message2, Message3};
#[allow(clippy::large_enum_variant)]
@ -679,23 +683,23 @@ impl State3 {
}
}
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct State4 {
A: bitcoin::PublicKey,
b: bitcoin::SecretKey,
pub A: bitcoin::PublicKey,
pub b: bitcoin::SecretKey,
s_b: cross_curve_dleq::Scalar,
S_a_monero: monero::PublicKey,
S_a_bitcoin: bitcoin::PublicKey,
pub S_a_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
pub refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
pub redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
pub tx_lock: bitcoin::TxLock,
tx_cancel_sig_a: Signature,
tx_refund_encsig: EncryptedSignature,
}
@ -708,7 +712,77 @@ impl State4 {
Message3 { tx_redeem_encsig }
}
pub async fn watch_for_redeem_btc<W>(self, bitcoin_wallet: &W) -> Result<State5>
pub fn tx_redeem_encsig(&self) -> EncryptedSignature {
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
self.b.encsign(self.S_a_bitcoin.clone(), tx_redeem.digest())
}
pub async fn check_for_tx_cancel<W>(&self, bitcoin_wallet: &W) -> Result<Transaction>
where
W: GetRawTransaction,
{
let tx_cancel = bitcoin::TxCancel::new(
&self.tx_lock,
self.refund_timelock,
self.A.clone(),
self.b.public(),
);
// todo: check if this is correct
let sig_a = self.tx_cancel_sig_a.clone();
let sig_b = self.b.sign(tx_cancel.digest());
let tx_cancel = tx_cancel
.clone()
.add_signatures(
&self.tx_lock,
(self.A.clone(), sig_a),
(self.b.public(), sig_b),
)
.expect(
"sig_{a,b} to be valid signatures for
tx_cancel",
);
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
Ok(tx)
}
pub async fn submit_tx_cancel<W>(&self, bitcoin_wallet: &W) -> Result<Txid>
where
W: BroadcastSignedTransaction,
{
let tx_cancel = bitcoin::TxCancel::new(
&self.tx_lock,
self.refund_timelock,
self.A.clone(),
self.b.public(),
);
// todo: check if this is correct
let sig_a = self.tx_cancel_sig_a.clone();
let sig_b = self.b.sign(tx_cancel.digest());
let tx_cancel = tx_cancel
.clone()
.add_signatures(
&self.tx_lock,
(self.A.clone(), sig_a),
(self.b.public(), sig_b),
)
.expect(
"sig_{a,b} to be valid signatures for
tx_cancel",
);
let tx_id = bitcoin_wallet
.broadcast_signed_transaction(tx_cancel)
.await?;
Ok(tx_id)
}
pub async fn watch_for_redeem_btc<W>(&self, bitcoin_wallet: &W) -> Result<State5>
where
W: WatchForRawTransaction,
{
@ -725,25 +799,38 @@ impl State4 {
let s_a = monero::private_key_from_secp256k1_scalar(s_a.into());
Ok(State5 {
A: self.A,
b: self.b,
A: self.A.clone(),
b: self.b.clone(),
s_a,
s_b: self.s_b,
S_a_monero: self.S_a_monero,
S_a_bitcoin: self.S_a_bitcoin,
S_a_bitcoin: self.S_a_bitcoin.clone(),
v: self.v,
btc: self.btc,
xmr: self.xmr,
refund_timelock: self.refund_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
redeem_address: self.redeem_address,
punish_address: self.punish_address,
tx_lock: self.tx_lock,
tx_refund_encsig: self.tx_refund_encsig,
tx_cancel_sig: self.tx_cancel_sig_a,
refund_address: self.refund_address.clone(),
redeem_address: self.redeem_address.clone(),
punish_address: self.punish_address.clone(),
tx_lock: self.tx_lock.clone(),
tx_refund_encsig: self.tx_refund_encsig.clone(),
tx_cancel_sig: self.tx_cancel_sig_a.clone(),
})
}
pub async fn wait_for_t1<W>(&self, bitcoin_wallet: &W) -> Result<()>
where
W: WatchForRawTransaction + TransactionBlockHeight + BlockHeight,
{
let tx_id = self.tx_lock.txid();
let tx_lock_height = bitcoin_wallet.transaction_block_height(tx_id).await;
let t1_timeout =
poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + self.refund_timelock);
t1_timeout.await;
Ok(())
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]

@ -160,15 +160,23 @@ pub async fn init_test(
let punish_address = redeem_address.clone();
let refund_address = bob.bitcoin_wallet.new_address().await.unwrap();
let alice_state0 = xmr_btc::alice::State0::new(
&mut OsRng,
btc_amount,
xmr_amount,
refund_timelock.unwrap_or(RELATIVE_REFUND_TIMELOCK),
punish_timelock.unwrap_or(RELATIVE_PUNISH_TIMELOCK),
redeem_address.clone(),
punish_address.clone(),
);
let alice_state0 = {
let a = bitcoin::SecretKey::new_random(&mut OsRng);
let s_a = cross_curve_dleq::Scalar::random(&mut OsRng);
let v_a = monero::PrivateViewKey::new_random(&mut OsRng);
xmr_btc::alice::State0::new(
a,
s_a,
v_a,
btc_amount,
xmr_amount,
refund_timelock.unwrap_or(RELATIVE_REFUND_TIMELOCK),
punish_timelock.unwrap_or(RELATIVE_PUNISH_TIMELOCK),
redeem_address.clone(),
punish_address.clone(),
)
};
let bob_state0 = xmr_btc::bob::State0::new(
&mut OsRng,
btc_amount,

Loading…
Cancel
Save