From 8754a9931b7dcf6a802852173cf5f690e947ee92 Mon Sep 17 00:00:00 2001 From: rishflab Date: Tue, 29 Sep 2020 15:36:50 +1000 Subject: [PATCH] Execute Alice and Bob state machines concurrently Previously we were testing the protocol by manually driving Alice and Bob's state machines. This logic has now be moved to an async state transition function that can take any possible state as input. The state transition function is called in a loop until it returns the desired state. This allows use to interrupt midway through the protocol and perform refund and punish tests. This design was chosen over a generator based implementation because the the generator based implementation results in a impure state transition function that is difficult to reason about and prone to bugs. Test related code was extracted into the tests folder. The 2b and 4b states were renamed to be consistent with the rest. Macros were used to reduce code duplication when converting child states to their parent states and vice versa. Todos were added were neccessary. --- xmr-btc/Cargo.toml | 4 + xmr-btc/src/alice.rs | 263 +++++++++--- xmr-btc/src/alice/message.rs | 114 +++++ xmr-btc/src/bitcoin.rs | 5 - xmr-btc/src/bob.rs | 178 ++++++-- xmr-btc/src/bob/message.rs | 137 ++++++ xmr-btc/src/lib.rs | 365 +--------------- xmr-btc/src/monero.rs | 24 +- xmr-btc/src/transport.rs | 8 + xmr-btc/tests/e2e.rs | 389 ++++++++++++++++++ xmr-btc/tests/node.rs | 92 +++++ xmr-btc/tests/transport.rs | 56 +++ .../wallet.rs => tests/wallet/bitcoin.rs} | 9 +- xmr-btc/tests/wallet/mod.rs | 2 + .../wallet.rs => tests/wallet/monero.rs} | 68 +-- 15 files changed, 1211 insertions(+), 503 deletions(-) create mode 100644 xmr-btc/src/alice/message.rs create mode 100644 xmr-btc/src/bob/message.rs create mode 100644 xmr-btc/src/transport.rs create mode 100644 xmr-btc/tests/e2e.rs create mode 100644 xmr-btc/tests/node.rs create mode 100644 xmr-btc/tests/transport.rs rename xmr-btc/{src/bitcoin/wallet.rs => tests/wallet/bitcoin.rs} (94%) create mode 100644 xmr-btc/tests/wallet/mod.rs rename xmr-btc/{src/monero/wallet.rs => tests/wallet/monero.rs} (92%) diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index c49df15d..eeed8dae 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -17,6 +17,8 @@ monero = "0.9" rand = "0.7" sha2 = "0.9" thiserror = "1" +tokio = { version = "0.2", default-features = false, features = ["time"] } +tracing = "0.1" [dev-dependencies] base64 = "0.12" @@ -25,3 +27,5 @@ monero-harness = { path = "../monero-harness" } reqwest = { version = "0.10", default-features = false } testcontainers = "0.10" tokio = { version = "0.2", default-features = false, features = ["blocking", "macros", "rt-core", "time", "rt-threaded"] } +tracing-subscriber = "0.2.12" +tracing = "0.1" diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index 2e5c1d4f..f2aa9ad5 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -2,33 +2,178 @@ use anyhow::{anyhow, Result}; use ecdsa_fun::adaptor::{Adaptor, EncryptedSignature}; use rand::{CryptoRng, RngCore}; -use crate::{bitcoin, bitcoin::GetRawTransaction, bob, monero, monero::ImportOutput}; -use ecdsa_fun::{nonce::Deterministic, Signature}; +use crate::{ + bitcoin, + bitcoin::{BroadcastSignedTransaction, GetRawTransaction}, + bob, monero, + monero::{ImportOutput, Transfer}, + transport::SendReceive, +}; +use ecdsa_fun::nonce::Deterministic; use sha2::Sha256; +use std::convert::{TryFrom, TryInto}; -#[derive(Debug)] -pub struct Message0 { - pub(crate) A: bitcoin::PublicKey, - pub(crate) S_a_monero: monero::PublicKey, - pub(crate) S_a_bitcoin: bitcoin::PublicKey, - pub(crate) dleq_proof_s_a: cross_curve_dleq::Proof, - pub(crate) v_a: monero::PrivateViewKey, - pub(crate) redeem_address: bitcoin::Address, - pub(crate) punish_address: bitcoin::Address, +pub mod message; +pub use message::{Message, Message0, Message1, Message2, UnexpectedMessage}; + +pub async fn next_state< + 'a, + R: RngCore + CryptoRng, + B: GetRawTransaction + BroadcastSignedTransaction, + M: ImportOutput + Transfer, + T: SendReceive, +>( + bitcoin_wallet: &B, + monero_wallet: &M, + transport: &mut T, + state: State, + rng: &mut R, +) -> Result { + match state { + State::State0(state0) => { + transport + .send_message(state0.next_message(rng).into()) + .await?; + + let bob_message0: bob::Message0 = transport.receive_message().await?.try_into()?; + let state1 = state0.receive(bob_message0)?; + Ok(state1.into()) + } + State::State1(state1) => { + let bob_message1: bob::Message1 = transport.receive_message().await?.try_into()?; + let state2 = state1.receive(bob_message1); + let alice_message1: Message1 = state2.next_message(); + transport.send_message(alice_message1.into()).await?; + Ok(state2.into()) + } + State::State2(state2) => { + let bob_message2: bob::Message2 = transport.receive_message().await?.try_into()?; + let state3 = state2.receive(bob_message2)?; + tokio::time::delay_for(std::time::Duration::new(5, 0)).await; + Ok(state3.into()) + } + State::State3(state3) => { + tracing::info!("alice is watching for locked btc"); + let state4 = state3.watch_for_lock_btc(bitcoin_wallet).await?; + Ok(state4.into()) + } + State::State4(state4) => { + let state5 = state4.lock_xmr(monero_wallet).await?; + tracing::info!("alice has locked xmr"); + Ok(state5.into()) + } + State::State5(state5) => { + transport.send_message(state5.next_message().into()).await?; + // todo: pass in state4b as a parameter somewhere in this call to prevent the + // user from waiting for a message that wont be sent + let message3: bob::Message3 = transport.receive_message().await?.try_into()?; + let state6 = state5.receive(message3); + tracing::info!("alice has received bob message 3"); + tracing::info!("alice is redeeming btc"); + state6.redeem_btc(bitcoin_wallet).await.unwrap(); + Ok(state6.into()) + } + State::State6(state6) => Ok(state6.into()), + } } -#[derive(Debug)] -pub struct Message1 { - pub(crate) tx_cancel_sig: Signature, - pub(crate) tx_refund_encsig: EncryptedSignature, +#[derive(Debug, Clone)] +pub enum State { + State0(State0), + State1(State1), + State2(State2), + State3(State3), + State4(State4), + State5(State5), + State6(State6), } -#[derive(Debug)] -pub struct Message2 { - pub(crate) tx_lock_proof: monero::TransferProof, +// TODO: use macro or generics +pub fn is_state4(state: &State) -> bool { + match state { + State::State4 { .. } => true, + _ => false, + } +} +// TODO: use macro or generics +pub fn is_state5(state: &State) -> bool { + match state { + State::State5 { .. } => true, + _ => false, + } +} +// TODO: use macro or generics +pub fn is_state6(state: &State) -> bool { + match state { + State::State6 { .. } => true, + _ => false, + } } -#[derive(Debug)] +macro_rules! impl_try_from_parent_state { + ($type:ident) => { + impl TryFrom for $type { + type Error = anyhow::Error; + fn try_from(from: State) -> Result { + if let State::$type(state) = from { + Ok(state) + } else { + Err(anyhow!("Failed to convert parent state to child state")) + } + } + } + }; +} + +impl_try_from_parent_state!(State0); +impl_try_from_parent_state!(State1); +impl_try_from_parent_state!(State2); +impl_try_from_parent_state!(State3); +impl_try_from_parent_state!(State4); +impl_try_from_parent_state!(State5); +impl_try_from_parent_state!(State6); + +macro_rules! impl_from_child_state { + ($type:ident) => { + impl From<$type> for State { + fn from(from: $type) -> Self { + State::$type(from) + } + } + }; +} + +impl_from_child_state!(State0); +impl_from_child_state!(State1); +impl_from_child_state!(State2); +impl_from_child_state!(State3); +impl_from_child_state!(State4); +impl_from_child_state!(State5); +impl_from_child_state!(State6); + +impl State { + pub fn new( + rng: &mut R, + btc: bitcoin::Amount, + xmr: monero::Amount, + refund_timelock: u32, + punish_timelock: u32, + redeem_address: bitcoin::Address, + punish_address: bitcoin::Address, + ) -> Self { + Self::State0(State0::new( + rng, + btc, + xmr, + refund_timelock, + punish_timelock, + redeem_address, + punish_address, + )) + } +} + +#[derive(Debug, Clone)] pub struct State0 { a: bitcoin::SecretKey, s_a: cross_curve_dleq::Scalar, @@ -114,7 +259,7 @@ impl State0 { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct State1 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, @@ -152,7 +297,7 @@ impl State1 { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct State2 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, @@ -227,7 +372,7 @@ impl State2 { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct State3 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, @@ -252,10 +397,13 @@ impl State3 { where W: bitcoin::GetRawTransaction, { - let _ = bitcoin_wallet + tracing::info!("{}", self.tx_lock.txid()); + let tx = bitcoin_wallet .get_raw_transaction(self.tx_lock.txid()) .await?; + tracing::info!("{}", tx.txid()); + Ok(State4 { a: self.a, B: self.B, @@ -277,7 +425,7 @@ impl State3 { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct State4 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, @@ -298,7 +446,7 @@ pub struct State4 { } impl State4 { - pub async fn lock_xmr(self, monero_wallet: &W) -> Result<(State4b, monero::Amount)> + pub async fn lock_xmr(self, monero_wallet: &W) -> Result where W: monero::Transfer, { @@ -311,28 +459,26 @@ impl State4 { .transfer(S_a + S_b, self.v.public(), self.xmr) .await?; - Ok(( - State4b { - a: self.a, - B: self.B, - s_a: self.s_a, - S_b_monero: self.S_b_monero, - S_b_bitcoin: self.S_b_bitcoin, - 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_lock_proof, - tx_punish_sig_bob: self.tx_punish_sig_bob, - tx_cancel_sig_bob: self.tx_cancel_sig_bob, - }, - fee, - )) + Ok(State5 { + a: self.a, + B: self.B, + s_a: self.s_a, + S_b_monero: self.S_b_monero, + S_b_bitcoin: self.S_b_bitcoin, + 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_lock_proof, + tx_punish_sig_bob: self.tx_punish_sig_bob, + tx_cancel_sig_bob: self.tx_cancel_sig_bob, + lock_xmr_fee: fee, + }) } pub async fn punish( @@ -382,8 +528,8 @@ impl State4 { } } -#[derive(Debug)] -pub struct State4b { +#[derive(Debug, Clone)] +pub struct State5 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, s_a: cross_curve_dleq::Scalar, @@ -401,17 +547,18 @@ pub struct State4b { tx_lock_proof: monero::TransferProof, tx_punish_sig_bob: bitcoin::Signature, tx_cancel_sig_bob: bitcoin::Signature, + lock_xmr_fee: monero::Amount, } -impl State4b { +impl State5 { pub fn next_message(&self) -> Message2 { Message2 { tx_lock_proof: self.tx_lock_proof.clone(), } } - pub fn receive(self, msg: bob::Message3) -> State5 { - State5 { + pub fn receive(self, msg: bob::Message3) -> State6 { + State6 { a: self.a, B: self.B, s_a: self.s_a, @@ -428,6 +575,7 @@ impl State4b { tx_lock: self.tx_lock, tx_punish_sig_bob: self.tx_punish_sig_bob, tx_redeem_encsig: msg.tx_redeem_encsig, + lock_xmr_fee: self.lock_xmr_fee, } } @@ -469,8 +617,8 @@ impl State4b { } } -#[derive(Debug)] -pub struct State5 { +#[derive(Debug, Clone)] +pub struct State6 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, s_a: cross_curve_dleq::Scalar, @@ -487,9 +635,10 @@ pub struct State5 { tx_lock: bitcoin::TxLock, tx_punish_sig_bob: bitcoin::Signature, tx_redeem_encsig: EncryptedSignature, + lock_xmr_fee: monero::Amount, } -impl State5 { +impl State6 { pub async fn redeem_btc( &self, bitcoin_wallet: &W, @@ -513,4 +662,8 @@ impl State5 { Ok(()) } + + pub fn lock_xmr_fee(&self) -> monero::Amount { + self.lock_xmr_fee + } } diff --git a/xmr-btc/src/alice/message.rs b/xmr-btc/src/alice/message.rs new file mode 100644 index 00000000..5901a050 --- /dev/null +++ b/xmr-btc/src/alice/message.rs @@ -0,0 +1,114 @@ +use anyhow::Result; +use ecdsa_fun::adaptor::EncryptedSignature; + +use crate::{bitcoin, monero}; +use ecdsa_fun::Signature; + +use std::convert::TryFrom; + +#[derive(Debug)] +pub enum Message { + Message0(Message0), + Message1(Message1), + Message2(Message2), +} + +#[derive(Debug)] +pub struct Message0 { + pub(crate) A: bitcoin::PublicKey, + pub(crate) S_a_monero: monero::PublicKey, + pub(crate) S_a_bitcoin: bitcoin::PublicKey, + pub(crate) dleq_proof_s_a: cross_curve_dleq::Proof, + pub(crate) v_a: monero::PrivateViewKey, + pub(crate) redeem_address: bitcoin::Address, + pub(crate) punish_address: bitcoin::Address, +} + +#[derive(Debug)] +pub struct Message1 { + pub(crate) tx_cancel_sig: Signature, + pub(crate) tx_refund_encsig: EncryptedSignature, +} + +#[derive(Debug)] +pub struct Message2 { + pub(crate) tx_lock_proof: monero::TransferProof, +} + +impl From for Message { + fn from(m: Message0) -> Self { + Message::Message0(m) + } +} + +impl TryFrom for Message0 { + type Error = UnexpectedMessage; + + fn try_from(m: Message) -> Result { + match m { + Message::Message0(m) => Ok(m), + _ => Err(UnexpectedMessage { + expected_type: "Create0".to_string(), + received: m, + }), + } + } +} + +impl From for Message { + fn from(m: Message1) -> Self { + Message::Message1(m) + } +} + +impl TryFrom for Message1 { + type Error = UnexpectedMessage; + + fn try_from(m: Message) -> Result { + match m { + Message::Message1(m) => Ok(m), + _ => Err(UnexpectedMessage { + expected_type: "Create1".to_string(), + received: m, + }), + } + } +} + +impl From for Message { + fn from(m: Message2) -> Self { + Message::Message2(m) + } +} + +impl TryFrom for Message2 { + type Error = UnexpectedMessage; + + fn try_from(m: Message) -> Result { + match m { + Message::Message2(m) => Ok(m), + _ => Err(UnexpectedMessage { + expected_type: "Create2".to_string(), + received: m, + }), + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("expected message of type {expected_type}, got {received:?}")] +pub struct UnexpectedMessage { + expected_type: String, + received: Message, +} + +impl UnexpectedMessage { + pub fn new(received: Message) -> Self { + let expected_type = std::any::type_name::(); + + Self { + expected_type: expected_type.to_string(), + received, + } + } +} diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index 25a1b458..e903bb75 100644 --- a/xmr-btc/src/bitcoin.rs +++ b/xmr-btc/src/bitcoin.rs @@ -1,6 +1,4 @@ pub mod transactions; -#[cfg(test)] -pub mod wallet; use anyhow::{anyhow, bail, Result}; use async_trait::async_trait; @@ -28,9 +26,6 @@ pub use crate::bitcoin::transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxR pub use bitcoin::{Address, Amount, OutPoint, Txid}; pub use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; -#[cfg(test)] -pub use wallet::{make_wallet, Wallet}; - pub const TX_FEE: u64 = 10_000; #[derive(Debug, Clone)] diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 25fcfadb..62841c5b 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -1,7 +1,11 @@ use crate::{ alice, - bitcoin::{self, BuildTxLockPsbt, GetRawTransaction, TxCancel}, + bitcoin::{ + self, BroadcastSignedTransaction, BuildTxLockPsbt, GetRawTransaction, SignTxLock, TxCancel, + }, monero, + monero::{CheckTransfer, ImportOutput}, + transport::SendReceive, }; use anyhow::{anyhow, Result}; use ecdsa_fun::{ @@ -11,31 +15,132 @@ use ecdsa_fun::{ }; use rand::{CryptoRng, RngCore}; use sha2::Sha256; +use std::convert::{TryFrom, TryInto}; -#[derive(Debug)] -pub struct Message0 { - pub(crate) B: bitcoin::PublicKey, - pub(crate) S_b_monero: monero::PublicKey, - pub(crate) S_b_bitcoin: bitcoin::PublicKey, - pub(crate) dleq_proof_s_b: cross_curve_dleq::Proof, - pub(crate) v_b: monero::PrivateViewKey, - pub(crate) refund_address: bitcoin::Address, +pub mod message; +pub use message::{Message, Message0, Message1, Message2, Message3, UnexpectedMessage}; + +pub async fn next_state< + 'a, + R: RngCore + CryptoRng, + B: GetRawTransaction + SignTxLock + BuildTxLockPsbt + BroadcastSignedTransaction, + M: ImportOutput + CheckTransfer, + T: SendReceive, +>( + bitcoin_wallet: &B, + monero_wallet: &M, + transport: &mut T, + state: State, + rng: &mut R, +) -> Result { + match state { + State::State0(state0) => { + transport + .send_message(state0.next_message(rng).into()) + .await?; + let message0: alice::Message0 = transport.receive_message().await?.try_into()?; + let state1 = state0.receive(bitcoin_wallet, message0).await?; + Ok(state1.into()) + } + State::State1(state1) => { + transport.send_message(state1.next_message().into()).await?; + + let message1: alice::Message1 = transport.receive_message().await?.try_into()?; + let state2 = state1.receive(message1)?; + Ok(state2.into()) + } + State::State2(state2) => { + let message2 = state2.next_message(); + let state3 = state2.lock_btc(bitcoin_wallet).await?; + tracing::info!("bob has locked btc"); + transport.send_message(message2.into()).await?; + Ok(state3.into()) + } + State::State3(state3) => { + let message2: alice::Message2 = transport.receive_message().await?.try_into()?; + + let state4 = state3.watch_for_lock_xmr(monero_wallet, message2).await?; + tracing::info!("bob has seen that alice has locked xmr"); + Ok(state4.into()) + } + State::State4(state4) => { + transport.send_message(state4.next_message().into()).await?; + + tracing::info!("bob is watching for redeem_btc"); + tokio::time::delay_for(std::time::Duration::new(5, 0)).await; + let state5 = state4.watch_for_redeem_btc(bitcoin_wallet).await?; + tracing::info!("bob has seen that alice has redeemed btc"); + state5.claim_xmr(monero_wallet).await?; + tracing::info!("bob has claimed xmr"); + Ok(state5.into()) + } + State::State5(state5) => Ok(state5.into()), + } } #[derive(Debug)] -pub struct Message1 { - pub(crate) tx_lock: bitcoin::TxLock, +pub enum State { + State0(State0), + State1(State1), + State2(State2), + State3(State3), + State4(State4), + State5(State5), } -#[derive(Debug)] -pub struct Message2 { - pub(crate) tx_punish_sig: Signature, - pub(crate) tx_cancel_sig: Signature, +macro_rules! impl_try_from_parent_state { + ($type:ident) => { + impl TryFrom for $type { + type Error = anyhow::Error; + fn try_from(from: State) -> Result { + if let State::$type(state) = from { + Ok(state) + } else { + Err(anyhow!("Failed to convert parent state to child state")) + } + } + } + }; } -#[derive(Debug)] -pub struct Message3 { - pub(crate) tx_redeem_encsig: EncryptedSignature, +impl_try_from_parent_state!(State0); +impl_try_from_parent_state!(State1); +impl_try_from_parent_state!(State2); +impl_try_from_parent_state!(State3); +impl_try_from_parent_state!(State4); +impl_try_from_parent_state!(State5); + +macro_rules! impl_from_child_state { + ($type:ident) => { + impl From<$type> for State { + fn from(from: $type) -> Self { + State::$type(from) + } + } + }; +} + +impl_from_child_state!(State0); +impl_from_child_state!(State1); +impl_from_child_state!(State2); +impl_from_child_state!(State3); +impl_from_child_state!(State4); +impl_from_child_state!(State5); + +// TODO: use macro or generics +pub fn is_state5(state: &State) -> bool { + match state { + State::State5 { .. } => true, + _ => false, + } +} + +// TODO: use macro or generics +pub fn is_state3(state: &State) -> bool { + match state { + State::State3 { .. } => true, + _ => false, + } } #[derive(Debug)] @@ -126,7 +231,7 @@ impl State0 { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct State1 { A: bitcoin::PublicKey, b: bitcoin::SecretKey, @@ -189,7 +294,7 @@ impl State1 { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct State2 { A: bitcoin::PublicKey, b: bitcoin::SecretKey, @@ -228,17 +333,18 @@ impl State2 { } } - pub async fn lock_btc(self, bitcoin_wallet: &W) -> Result + pub async fn lock_btc(self, bitcoin_wallet: &W) -> Result where W: bitcoin::SignTxLock + bitcoin::BroadcastSignedTransaction, { let signed_tx_lock = bitcoin_wallet.sign_tx_lock(self.tx_lock.clone()).await?; + tracing::info!("{}", self.tx_lock.txid()); let _ = bitcoin_wallet .broadcast_signed_transaction(signed_tx_lock) .await?; - Ok(State2b { + Ok(State3 { A: self.A, b: self.b, s_b: self.s_b, @@ -260,7 +366,7 @@ impl State2 { } #[derive(Debug, Clone)] -pub struct State2b { +pub struct State3 { A: bitcoin::PublicKey, b: bitcoin::SecretKey, s_b: cross_curve_dleq::Scalar, @@ -279,8 +385,9 @@ pub struct State2b { tx_refund_encsig: EncryptedSignature, } -impl State2b { - pub async fn watch_for_lock_xmr(self, xmr_wallet: &W, msg: alice::Message2) -> Result +impl State3 { + // todo: loop until punish? timelock has expired + pub async fn watch_for_lock_xmr(self, xmr_wallet: &W, msg: alice::Message2) -> Result where W: monero::CheckTransfer, { @@ -293,7 +400,7 @@ impl State2b { .check_transfer(S, self.v.public(), msg.tx_lock_proof, self.xmr) .await?; - Ok(State3 { + Ok(State4 { A: self.A, b: self.b, s_b: self.s_b, @@ -359,15 +466,13 @@ impl State2b { } Ok(()) } - - #[cfg(test)] pub fn tx_lock_id(&self) -> bitcoin::Txid { self.tx_lock.txid() } } #[derive(Debug, Clone)] -pub struct State3 { +pub struct State4 { A: bitcoin::PublicKey, b: bitcoin::SecretKey, s_b: cross_curve_dleq::Scalar, @@ -386,7 +491,7 @@ pub struct State3 { tx_refund_encsig: EncryptedSignature, } -impl State3 { +impl State4 { pub fn next_message(&self) -> Message3 { let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address); let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin.clone(), tx_redeem.digest()); @@ -394,7 +499,7 @@ impl State3 { Message3 { tx_redeem_encsig } } - pub async fn watch_for_redeem_btc(self, bitcoin_wallet: &W) -> Result + pub async fn watch_for_redeem_btc(self, bitcoin_wallet: &W) -> Result where W: GetRawTransaction, { @@ -409,7 +514,7 @@ impl State3 { let s_a = monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order(s_a.to_bytes())); - Ok(State4 { + Ok(State5 { A: self.A, b: self.b, s_a, @@ -431,8 +536,8 @@ impl State3 { } } -#[derive(Debug)] -pub struct State4 { +#[derive(Debug, Clone)] +pub struct State5 { A: bitcoin::PublicKey, b: bitcoin::SecretKey, s_a: monero::PrivateKey, @@ -452,7 +557,7 @@ pub struct State4 { tx_cancel_sig: Signature, } -impl State4 { +impl State5 { pub async fn claim_xmr(&self, monero_wallet: &W) -> Result<()> where W: monero::ImportOutput, @@ -469,4 +574,7 @@ impl State4 { Ok(()) } + pub fn tx_lock_id(&self) -> bitcoin::Txid { + self.tx_lock.txid() + } } diff --git a/xmr-btc/src/bob/message.rs b/xmr-btc/src/bob/message.rs new file mode 100644 index 00000000..fe767a46 --- /dev/null +++ b/xmr-btc/src/bob/message.rs @@ -0,0 +1,137 @@ +use crate::{bitcoin, monero}; +use anyhow::Result; +use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; + +use std::convert::TryFrom; + +#[derive(Debug)] +pub enum Message { + Message0(Message0), + Message1(Message1), + Message2(Message2), + Message3(Message3), +} + +#[derive(Debug)] +pub struct Message0 { + pub(crate) B: bitcoin::PublicKey, + pub(crate) S_b_monero: monero::PublicKey, + pub(crate) S_b_bitcoin: bitcoin::PublicKey, + pub(crate) dleq_proof_s_b: cross_curve_dleq::Proof, + pub(crate) v_b: monero::PrivateViewKey, + pub(crate) refund_address: bitcoin::Address, +} + +#[derive(Debug)] +pub struct Message1 { + pub(crate) tx_lock: bitcoin::TxLock, +} + +#[derive(Debug)] +pub struct Message2 { + pub(crate) tx_punish_sig: Signature, + pub(crate) tx_cancel_sig: Signature, +} + +#[derive(Debug)] +pub struct Message3 { + pub(crate) tx_redeem_encsig: EncryptedSignature, +} + +impl From for Message { + fn from(m: Message0) -> Self { + Message::Message0(m) + } +} + +impl TryFrom for Message0 { + type Error = UnexpectedMessage; + + fn try_from(m: Message) -> Result { + match m { + Message::Message0(m) => Ok(m), + _ => Err(UnexpectedMessage { + expected_type: "Create0".to_string(), + received: m, + }), + } + } +} + +impl From for Message { + fn from(m: Message1) -> Self { + Message::Message1(m) + } +} + +impl TryFrom for Message1 { + type Error = UnexpectedMessage; + + fn try_from(m: Message) -> Result { + match m { + Message::Message1(m) => Ok(m), + _ => Err(UnexpectedMessage { + expected_type: "Create0".to_string(), + received: m, + }), + } + } +} + +impl From for Message { + fn from(m: Message2) -> Self { + Message::Message2(m) + } +} + +impl TryFrom for Message2 { + type Error = UnexpectedMessage; + + fn try_from(m: Message) -> Result { + match m { + Message::Message2(m) => Ok(m), + _ => Err(UnexpectedMessage { + expected_type: "Create0".to_string(), + received: m, + }), + } + } +} + +impl From for Message { + fn from(m: Message3) -> Self { + Message::Message3(m) + } +} + +impl TryFrom for Message3 { + type Error = UnexpectedMessage; + + fn try_from(m: Message) -> Result { + match m { + Message::Message3(m) => Ok(m), + _ => Err(UnexpectedMessage { + expected_type: "Create0".to_string(), + received: m, + }), + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("expected message of type {expected_type}, got {received:?}")] +pub struct UnexpectedMessage { + expected_type: String, + received: Message, +} + +impl UnexpectedMessage { + pub fn new(received: Message) -> Self { + let expected_type = std::any::type_name::(); + + Self { + expected_type: expected_type.to_string(), + received, + } + } +} diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index d4e66337..986d6434 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -18,367 +18,4 @@ pub mod alice; pub mod bitcoin; pub mod bob; pub mod monero; - -#[cfg(test)] -mod tests { - use crate::{ - alice, bitcoin, - bitcoin::{Amount, TX_FEE}, - bob, monero, - }; - use bitcoin_harness::Bitcoind; - use monero_harness::Monero; - use rand::rngs::OsRng; - use testcontainers::clients::Cli; - - const TEN_XMR: u64 = 10_000_000_000_000; - - pub async fn init_bitcoind(tc_client: &Cli) -> Bitcoind<'_> { - let bitcoind = Bitcoind::new(tc_client, "0.19.1").expect("failed to create bitcoind"); - let _ = bitcoind.init(5).await; - - bitcoind - } - - #[tokio::test] - async fn happy_path() { - let cli = Cli::default(); - let monero = Monero::new(&cli); - let bitcoind = init_bitcoind(&cli).await; - - // must be bigger than our hardcoded fee of 10_000 - let btc_amount = bitcoin::Amount::from_sat(10_000_000); - let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000); - - let fund_alice = TEN_XMR; - let fund_bob = 0; - monero.init(fund_alice, fund_bob).await.unwrap(); - - let alice_monero_wallet = monero::AliceWallet(&monero); - let bob_monero_wallet = monero::BobWallet(&monero); - - let alice_btc_wallet = bitcoin::Wallet::new("alice", &bitcoind.node_url) - .await - .unwrap(); - let bob_btc_wallet = bitcoin::make_wallet("bob", &bitcoind, btc_amount) - .await - .unwrap(); - - let alice_initial_btc_balance = alice_btc_wallet.balance().await.unwrap(); - let bob_initial_btc_balance = bob_btc_wallet.balance().await.unwrap(); - - let alice_initial_xmr_balance = alice_monero_wallet.0.get_balance_alice().await.unwrap(); - let bob_initial_xmr_balance = bob_monero_wallet.0.get_balance_bob().await.unwrap(); - - let redeem_address = alice_btc_wallet.new_address().await.unwrap(); - let punish_address = redeem_address.clone(); - let refund_address = bob_btc_wallet.new_address().await.unwrap(); - - let refund_timelock = 1; - let punish_timelock = 1; - - let alice_state0 = alice::State0::new( - &mut OsRng, - btc_amount, - xmr_amount, - refund_timelock, - punish_timelock, - redeem_address, - punish_address, - ); - let bob_state0 = bob::State0::new( - &mut OsRng, - btc_amount, - xmr_amount, - refund_timelock, - punish_timelock, - refund_address.clone(), - ); - - let alice_message0 = alice_state0.next_message(&mut OsRng); - let bob_message0 = bob_state0.next_message(&mut OsRng); - - let alice_state1 = alice_state0.receive(bob_message0).unwrap(); - let bob_state1 = bob_state0 - .receive(&bob_btc_wallet, alice_message0) - .await - .unwrap(); - - let bob_message1 = bob_state1.next_message(); - let alice_state2 = alice_state1.receive(bob_message1); - let alice_message1 = alice_state2.next_message(); - let bob_state2 = bob_state1.receive(alice_message1).unwrap(); - - let bob_message2 = bob_state2.next_message(); - let alice_state3 = alice_state2.receive(bob_message2).unwrap(); - - let bob_state2b = bob_state2.lock_btc(&bob_btc_wallet).await.unwrap(); - let lock_txid = bob_state2b.tx_lock_id(); - - let alice_state4 = alice_state3 - .watch_for_lock_btc(&alice_btc_wallet) - .await - .unwrap(); - - let (alice_state4b, lock_tx_monero_fee) = - alice_state4.lock_xmr(&alice_monero_wallet).await.unwrap(); - - let alice_message2 = alice_state4b.next_message(); - - let bob_state3 = bob_state2b - .watch_for_lock_xmr(&bob_monero_wallet, alice_message2) - .await - .unwrap(); - - let bob_message3 = bob_state3.next_message(); - let alice_state5 = alice_state4b.receive(bob_message3); - - alice_state5.redeem_btc(&alice_btc_wallet).await.unwrap(); - let bob_state4 = bob_state3 - .watch_for_redeem_btc(&bob_btc_wallet) - .await - .unwrap(); - - bob_state4.claim_xmr(&bob_monero_wallet).await.unwrap(); - - let alice_final_btc_balance = alice_btc_wallet.balance().await.unwrap(); - let bob_final_btc_balance = bob_btc_wallet.balance().await.unwrap(); - - let lock_tx_bitcoin_fee = bob_btc_wallet.transaction_fee(lock_txid).await.unwrap(); - - assert_eq!( - alice_final_btc_balance, - alice_initial_btc_balance + btc_amount - bitcoin::Amount::from_sat(bitcoin::TX_FEE) - ); - assert_eq!( - bob_final_btc_balance, - bob_initial_btc_balance - btc_amount - lock_tx_bitcoin_fee - ); - - let alice_final_xmr_balance = alice_monero_wallet.0.get_balance_alice().await.unwrap(); - bob_monero_wallet - .0 - .wait_for_bob_wallet_block_height() - .await - .unwrap(); - let bob_final_xmr_balance = bob_monero_wallet.0.get_balance_bob().await.unwrap(); - - assert_eq!( - alice_final_xmr_balance, - alice_initial_xmr_balance - u64::from(xmr_amount) - u64::from(lock_tx_monero_fee) - ); - assert_eq!( - bob_final_xmr_balance, - bob_initial_xmr_balance + u64::from(xmr_amount) - ); - } - - #[tokio::test] - async fn both_refund() { - let cli = Cli::default(); - let monero = Monero::new(&cli); - let bitcoind = init_bitcoind(&cli).await; - - // must be bigger than our hardcoded fee of 10_000 - let btc_amount = bitcoin::Amount::from_sat(10_000_000); - let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000); - - let alice_btc_wallet = bitcoin::Wallet::new("alice", &bitcoind.node_url) - .await - .unwrap(); - let bob_btc_wallet = bitcoin::make_wallet("bob", &bitcoind, btc_amount) - .await - .unwrap(); - - let fund_alice = TEN_XMR; - let fund_bob = 0; - - monero.init(fund_alice, fund_bob).await.unwrap(); - let alice_monero_wallet = monero::AliceWallet(&monero); - let bob_monero_wallet = monero::BobWallet(&monero); - - let alice_initial_btc_balance = alice_btc_wallet.balance().await.unwrap(); - let bob_initial_btc_balance = bob_btc_wallet.balance().await.unwrap(); - - let bob_initial_xmr_balance = bob_monero_wallet.0.get_balance_bob().await.unwrap(); - - let redeem_address = alice_btc_wallet.new_address().await.unwrap(); - let punish_address = redeem_address.clone(); - let refund_address = bob_btc_wallet.new_address().await.unwrap(); - - let refund_timelock = 1; - let punish_timelock = 1; - - let alice_state0 = alice::State0::new( - &mut OsRng, - btc_amount, - xmr_amount, - refund_timelock, - punish_timelock, - redeem_address, - punish_address, - ); - let bob_state0 = bob::State0::new( - &mut OsRng, - btc_amount, - xmr_amount, - refund_timelock, - punish_timelock, - refund_address.clone(), - ); - - let alice_message0 = alice_state0.next_message(&mut OsRng); - let bob_message0 = bob_state0.next_message(&mut OsRng); - - let alice_state1 = alice_state0.receive(bob_message0).unwrap(); - let bob_state1 = bob_state0 - .receive(&bob_btc_wallet, alice_message0) - .await - .unwrap(); - - let bob_message1 = bob_state1.next_message(); - let alice_state2 = alice_state1.receive(bob_message1); - let alice_message1 = alice_state2.next_message(); - let bob_state2 = bob_state1.receive(alice_message1).unwrap(); - - let bob_message2 = bob_state2.next_message(); - let alice_state3 = alice_state2.receive(bob_message2).unwrap(); - - let bob_state2b = bob_state2.lock_btc(&bob_btc_wallet).await.unwrap(); - - let alice_state4 = alice_state3 - .watch_for_lock_btc(&alice_btc_wallet) - .await - .unwrap(); - - let (alice_state4b, _lock_tx_monero_fee) = - alice_state4.lock_xmr(&alice_monero_wallet).await.unwrap(); - - bob_state2b.refund_btc(&bob_btc_wallet).await.unwrap(); - - alice_state4b - .refund_xmr(&alice_btc_wallet, &alice_monero_wallet) - .await - .unwrap(); - - let alice_final_btc_balance = alice_btc_wallet.balance().await.unwrap(); - let bob_final_btc_balance = bob_btc_wallet.balance().await.unwrap(); - - // lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal - // to TX_FEE - let lock_tx_bitcoin_fee = bob_btc_wallet - .transaction_fee(bob_state2b.tx_lock_id()) - .await - .unwrap(); - - assert_eq!(alice_final_btc_balance, alice_initial_btc_balance); - assert_eq!( - bob_final_btc_balance, - // The 2 * TX_FEE corresponds to tx_refund and tx_cancel. - bob_initial_btc_balance - Amount::from_sat(2 * TX_FEE) - lock_tx_bitcoin_fee - ); - - alice_monero_wallet - .0 - .wait_for_alice_wallet_block_height() - .await - .unwrap(); - let alice_final_xmr_balance = alice_monero_wallet.0.get_balance_alice().await.unwrap(); - let bob_final_xmr_balance = bob_monero_wallet.0.get_balance_bob().await.unwrap(); - - // Because we create a new wallet when claiming Monero, we can only assert on - // this new wallet owning all of `xmr_amount` after refund - assert_eq!(alice_final_xmr_balance, u64::from(xmr_amount)); - assert_eq!(bob_final_xmr_balance, bob_initial_xmr_balance); - } - - #[tokio::test] - async fn alice_punishes() { - let cli = Cli::default(); - let bitcoind = init_bitcoind(&cli).await; - - // must be bigger than our hardcoded fee of 10_000 - let btc_amount = bitcoin::Amount::from_sat(10_000_000); - let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000); - - let alice_btc_wallet = bitcoin::Wallet::new("alice", &bitcoind.node_url) - .await - .unwrap(); - let bob_btc_wallet = bitcoin::make_wallet("bob", &bitcoind, btc_amount) - .await - .unwrap(); - - let alice_initial_btc_balance = alice_btc_wallet.balance().await.unwrap(); - let bob_initial_btc_balance = bob_btc_wallet.balance().await.unwrap(); - - let redeem_address = alice_btc_wallet.new_address().await.unwrap(); - let punish_address = redeem_address.clone(); - let refund_address = bob_btc_wallet.new_address().await.unwrap(); - - let refund_timelock = 1; - let punish_timelock = 1; - - let alice_state0 = alice::State0::new( - &mut OsRng, - btc_amount, - xmr_amount, - refund_timelock, - punish_timelock, - redeem_address, - punish_address, - ); - let bob_state0 = bob::State0::new( - &mut OsRng, - btc_amount, - xmr_amount, - refund_timelock, - punish_timelock, - refund_address.clone(), - ); - - let alice_message0 = alice_state0.next_message(&mut OsRng); - let bob_message0 = bob_state0.next_message(&mut OsRng); - - let alice_state1 = alice_state0.receive(bob_message0).unwrap(); - let bob_state1 = bob_state0 - .receive(&bob_btc_wallet, alice_message0) - .await - .unwrap(); - - let bob_message1 = bob_state1.next_message(); - let alice_state2 = alice_state1.receive(bob_message1); - let alice_message1 = alice_state2.next_message(); - let bob_state2 = bob_state1.receive(alice_message1).unwrap(); - - let bob_message2 = bob_state2.next_message(); - let alice_state3 = alice_state2.receive(bob_message2).unwrap(); - - let bob_state2b = bob_state2.lock_btc(&bob_btc_wallet).await.unwrap(); - - let alice_state4 = alice_state3 - .watch_for_lock_btc(&alice_btc_wallet) - .await - .unwrap(); - - alice_state4.punish(&alice_btc_wallet).await.unwrap(); - - let alice_final_btc_balance = alice_btc_wallet.balance().await.unwrap(); - let bob_final_btc_balance = bob_btc_wallet.balance().await.unwrap(); - - // lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal - // to TX_FEE - let lock_tx_bitcoin_fee = bob_btc_wallet - .transaction_fee(bob_state2b.tx_lock_id()) - .await - .unwrap(); - - assert_eq!( - alice_final_btc_balance, - alice_initial_btc_balance + btc_amount - Amount::from_sat(2 * TX_FEE) - ); - assert_eq!( - bob_final_btc_balance, - bob_initial_btc_balance - btc_amount - lock_tx_bitcoin_fee - ); - } -} +pub mod transport; diff --git a/xmr-btc/src/monero.rs b/xmr-btc/src/monero.rs index b67caabd..aefe858d 100644 --- a/xmr-btc/src/monero.rs +++ b/xmr-btc/src/monero.rs @@ -1,6 +1,3 @@ -#[cfg(test)] -pub mod wallet; - use std::ops::Add; use anyhow::Result; @@ -16,9 +13,6 @@ pub fn random_private_key(rng: &mut R) -> PrivateKey { PrivateKey::from_scalar(scalar) } -#[cfg(test)] -pub use wallet::{AliceWallet, BobWallet}; - #[derive(Clone, Copy, Debug)] pub struct PrivateViewKey(PrivateKey); @@ -69,6 +63,9 @@ impl Amount { pub fn from_piconero(amount: u64) -> Self { Amount(amount) } + pub fn as_piconero(&self) -> u64 { + self.0 + } } impl From for u64 { @@ -83,8 +80,21 @@ pub struct TransferProof { tx_key: PrivateKey, } +impl TransferProof { + pub fn new(tx_hash: TxHash, tx_key: PrivateKey) -> Self { + Self { tx_hash, tx_key } + } + pub fn tx_hash(&self) -> TxHash { + self.tx_hash.clone() + } + pub fn tx_key(&self) -> PrivateKey { + self.tx_key + } +} + +// TODO: add constructor/ change String to fixed length byte array #[derive(Clone, Debug)] -pub struct TxHash(String); +pub struct TxHash(pub String); impl From for String { fn from(from: TxHash) -> Self { diff --git a/xmr-btc/src/transport.rs b/xmr-btc/src/transport.rs new file mode 100644 index 00000000..abf7eac5 --- /dev/null +++ b/xmr-btc/src/transport.rs @@ -0,0 +1,8 @@ +use anyhow::Result; +use async_trait::async_trait; + +#[async_trait] +pub trait SendReceive { + async fn send_message(&mut self, message: SendMsg) -> Result<()>; + async fn receive_message(&mut self) -> Result; +} diff --git a/xmr-btc/tests/e2e.rs b/xmr-btc/tests/e2e.rs new file mode 100644 index 00000000..bd40ed8a --- /dev/null +++ b/xmr-btc/tests/e2e.rs @@ -0,0 +1,389 @@ +use crate::{ + node::{AliceNode, BobNode}, + transport::Transport, +}; +use bitcoin_harness::Bitcoind; + +use monero_harness::Monero; +use rand::rngs::OsRng; + +use testcontainers::clients::Cli; +use tokio::sync::{ + mpsc, + mpsc::{Receiver, Sender}, +}; +use xmr_btc::{alice, bitcoin, bob, monero}; + +mod node; +mod transport; +mod wallet; + +const TEN_XMR: u64 = 10_000_000_000_000; +const RELATIVE_REFUND_TIMELOCK: u32 = 1; +const RELATIVE_PUNISH_TIMELOCK: u32 = 1; + +pub async fn init_bitcoind(tc_client: &Cli) -> Bitcoind<'_> { + let bitcoind = Bitcoind::new(tc_client, "0.19.1").expect("failed to create bitcoind"); + let _ = bitcoind.init(5).await; + + bitcoind +} + +pub struct InitialBalances { + alice_xmr: u64, + alice_btc: bitcoin::Amount, + bob_xmr: u64, + bob_btc: bitcoin::Amount, +} + +pub struct SwapAmounts { + xmr: monero::Amount, + btc: bitcoin::Amount, +} + +pub fn init_alice_and_bob_transports() -> ( + Transport, + Transport, +) { + let (a_sender, b_receiver): (Sender, Receiver) = + mpsc::channel(5); + let (b_sender, a_receiver): (Sender, Receiver) = mpsc::channel(5); + + let a_transport = Transport { + sender: a_sender, + receiver: a_receiver, + }; + + let b_transport = Transport { + sender: b_sender, + receiver: b_receiver, + }; + + (a_transport, b_transport) +} + +pub async fn init_test<'a>( + monero: &'a Monero<'a>, + bitcoind: &Bitcoind<'_>, +) -> ( + alice::State0, + bob::State0, + AliceNode<'a>, + BobNode<'a>, + InitialBalances, + SwapAmounts, +) { + // must be bigger than our hardcoded fee of 10_000 + let btc_amount = bitcoin::Amount::from_sat(10_000_000); + let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000); + + let swap_amounts = SwapAmounts { + xmr: xmr_amount, + btc: btc_amount, + }; + + let fund_alice = TEN_XMR; + let fund_bob = 0; + monero.init(fund_alice, fund_bob).await.unwrap(); + + let alice_monero_wallet = wallet::monero::AliceWallet(&monero); + let bob_monero_wallet = wallet::monero::BobWallet(&monero); + + let alice_btc_wallet = wallet::bitcoin::Wallet::new("alice", &bitcoind.node_url) + .await + .unwrap(); + let bob_btc_wallet = wallet::bitcoin::make_wallet("bob", &bitcoind, btc_amount) + .await + .unwrap(); + + let (alice_transport, bob_transport) = init_alice_and_bob_transports(); + + let alice = AliceNode::new(alice_transport, alice_btc_wallet, alice_monero_wallet); + + let bob = BobNode::new(bob_transport, bob_btc_wallet, bob_monero_wallet); + + let alice_initial_btc_balance = alice.bitcoin_wallet.balance().await.unwrap(); + let bob_initial_btc_balance = bob.bitcoin_wallet.balance().await.unwrap(); + + let alice_initial_xmr_balance = alice.monero_wallet.0.get_balance_alice().await.unwrap(); + let bob_initial_xmr_balance = bob.monero_wallet.0.get_balance_bob().await.unwrap(); + + let redeem_address = alice.bitcoin_wallet.new_address().await.unwrap(); + let punish_address = redeem_address.clone(); + let refund_address = bob.bitcoin_wallet.new_address().await.unwrap(); + + let alice_state0 = alice::State0::new( + &mut OsRng, + btc_amount, + xmr_amount, + RELATIVE_REFUND_TIMELOCK, + RELATIVE_PUNISH_TIMELOCK, + redeem_address.clone(), + punish_address.clone(), + ); + let bob_state0 = bob::State0::new( + &mut OsRng, + btc_amount, + xmr_amount, + RELATIVE_REFUND_TIMELOCK, + RELATIVE_PUNISH_TIMELOCK, + refund_address.clone(), + ); + let initial_balances = InitialBalances { + alice_xmr: alice_initial_xmr_balance, + alice_btc: alice_initial_btc_balance, + bob_xmr: bob_initial_xmr_balance, + bob_btc: bob_initial_btc_balance, + }; + ( + alice_state0, + bob_state0, + alice, + bob, + initial_balances, + swap_amounts, + ) +} + +#[cfg(test)] +mod tests { + use crate::{ + init_bitcoind, init_test, + node::{run_alice_until, run_bob_until}, + }; + + use futures::future; + use monero_harness::Monero; + 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}, + bob, + }; + + #[tokio::test] + async fn happy_path() { + let _guard = tracing_subscriber::fmt() + .with_env_filter("info") + .set_default(); + + let cli = Cli::default(); + let monero = Monero::new(&cli); + let bitcoind = init_bitcoind(&cli).await; + + let ( + alice_state0, + bob_state0, + mut alice_node, + mut bob_node, + initial_balances, + swap_amounts, + ) = init_test(&monero, &bitcoind).await; + + let (alice_state, bob_state) = future::try_join( + run_alice_until( + &mut alice_node, + alice_state0.into(), + alice::is_state6, + &mut OsRng, + ), + run_bob_until(&mut bob_node, bob_state0.into(), bob::is_state5, &mut OsRng), + ) + .await + .unwrap(); + + let alice_state6: alice::State6 = alice_state.try_into().unwrap(); + let bob_state5: bob::State5 = bob_state.try_into().unwrap(); + + let alice_final_btc_balance = alice_node.bitcoin_wallet.balance().await.unwrap(); + let bob_final_btc_balance = bob_node.bitcoin_wallet.balance().await.unwrap(); + + let lock_tx_bitcoin_fee = bob_node + .bitcoin_wallet + .transaction_fee(bob_state5.tx_lock_id()) + .await + .unwrap(); + + assert_eq!( + alice_final_btc_balance, + initial_balances.alice_btc + swap_amounts.btc + - bitcoin::Amount::from_sat(bitcoin::TX_FEE) + ); + assert_eq!( + bob_final_btc_balance, + initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee + ); + + let alice_final_xmr_balance = alice_node + .monero_wallet + .0 + .get_balance_alice() + .await + .unwrap(); + + bob_node + .monero_wallet + .0 + .wait_for_bob_wallet_block_height() + .await + .unwrap(); + + let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance_bob().await.unwrap(); + + assert_eq!( + alice_final_xmr_balance, + initial_balances.alice_xmr + - u64::from(swap_amounts.xmr) + - u64::from(alice_state6.lock_xmr_fee()) + ); + assert_eq!( + bob_final_xmr_balance, + initial_balances.bob_xmr + u64::from(swap_amounts.xmr) + ); + } + + #[tokio::test] + async fn both_refund() { + let _guard = tracing_subscriber::fmt() + .with_env_filter("info") + .set_default(); + + let cli = Cli::default(); + let monero = Monero::new(&cli); + let bitcoind = init_bitcoind(&cli).await; + + let ( + alice_state0, + bob_state0, + mut alice_node, + mut bob_node, + initial_balances, + swap_amounts, + ) = init_test(&monero, &bitcoind).await; + + let (alice_state, bob_state) = future::try_join( + run_alice_until( + &mut alice_node, + alice_state0.into(), + alice::is_state5, + &mut OsRng, + ), + run_bob_until(&mut bob_node, bob_state0.into(), bob::is_state3, &mut OsRng), + ) + .await + .unwrap(); + + let alice_state5: alice::State5 = alice_state.try_into().unwrap(); + let bob_state3: bob::State3 = bob_state.try_into().unwrap(); + + bob_state3 + .refund_btc(&bob_node.bitcoin_wallet) + .await + .unwrap(); + alice_state5 + .refund_xmr(&alice_node.bitcoin_wallet, &alice_node.monero_wallet) + .await + .unwrap(); + + let alice_final_btc_balance = alice_node.bitcoin_wallet.balance().await.unwrap(); + let bob_final_btc_balance = bob_node.bitcoin_wallet.balance().await.unwrap(); + + // lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal + // to TX_FEE + let lock_tx_bitcoin_fee = bob_node + .bitcoin_wallet + .transaction_fee(bob_state3.tx_lock_id()) + .await + .unwrap(); + + assert_eq!(alice_final_btc_balance, initial_balances.alice_btc); + assert_eq!( + bob_final_btc_balance, + // The 2 * TX_FEE corresponds to tx_refund and tx_cancel. + initial_balances.bob_btc - Amount::from_sat(2 * TX_FEE) - lock_tx_bitcoin_fee + ); + + alice_node + .monero_wallet + .0 + .wait_for_alice_wallet_block_height() + .await + .unwrap(); + let alice_final_xmr_balance = alice_node + .monero_wallet + .0 + .get_balance_alice() + .await + .unwrap(); + let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance_bob().await.unwrap(); + + // Because we create a new wallet when claiming Monero, we can only assert on + // this new wallet owning all of `xmr_amount` after refund + assert_eq!(alice_final_xmr_balance, u64::from(swap_amounts.xmr)); + assert_eq!(bob_final_xmr_balance, initial_balances.bob_xmr); + } + + #[tokio::test] + async fn alice_punishes() { + let _guard = tracing_subscriber::fmt() + .with_env_filter("info") + .set_default(); + + let cli = Cli::default(); + let monero = Monero::new(&cli); + let bitcoind = init_bitcoind(&cli).await; + + let ( + alice_state0, + bob_state0, + mut alice_node, + mut bob_node, + initial_balances, + swap_amounts, + ) = init_test(&monero, &bitcoind).await; + + let (alice_state, bob_state) = future::try_join( + run_alice_until( + &mut alice_node, + alice_state0.into(), + alice::is_state4, + &mut OsRng, + ), + run_bob_until(&mut bob_node, bob_state0.into(), bob::is_state3, &mut OsRng), + ) + .await + .unwrap(); + + let alice_state4: alice::State4 = alice_state.try_into().unwrap(); + let bob_state3: bob::State3 = bob_state.try_into().unwrap(); + + alice_state4 + .punish(&alice_node.bitcoin_wallet) + .await + .unwrap(); + + let alice_final_btc_balance = alice_node.bitcoin_wallet.balance().await.unwrap(); + let bob_final_btc_balance = bob_node.bitcoin_wallet.balance().await.unwrap(); + + // lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal + // to TX_FEE + let lock_tx_bitcoin_fee = bob_node + .bitcoin_wallet + .transaction_fee(bob_state3.tx_lock_id()) + .await + .unwrap(); + + assert_eq!( + alice_final_btc_balance, + initial_balances.alice_btc + swap_amounts.btc - Amount::from_sat(2 * TX_FEE) + ); + assert_eq!( + bob_final_btc_balance, + initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee + ); + } +} diff --git a/xmr-btc/tests/node.rs b/xmr-btc/tests/node.rs new file mode 100644 index 00000000..74351a64 --- /dev/null +++ b/xmr-btc/tests/node.rs @@ -0,0 +1,92 @@ +use crate::{transport::Transport, wallet}; +use anyhow::Result; +use rand::{CryptoRng, RngCore}; +use xmr_btc::{alice, bob}; + +// TODO: merge this with bob node +// This struct is responsible for I/O +pub struct AliceNode<'a> { + transport: Transport, + pub bitcoin_wallet: wallet::bitcoin::Wallet, + pub monero_wallet: wallet::monero::AliceWallet<'a>, +} + +impl<'a> AliceNode<'a> { + pub fn new( + transport: Transport, + bitcoin_wallet: wallet::bitcoin::Wallet, + monero_wallet: wallet::monero::AliceWallet<'a>, + ) -> AliceNode<'a> { + Self { + transport, + bitcoin_wallet, + monero_wallet, + } + } +} + +pub async fn run_alice_until<'a, R: RngCore + CryptoRng>( + alice: &mut AliceNode<'a>, + initial_state: alice::State, + is_state: fn(&alice::State) -> bool, + rng: &mut R, +) -> Result { + let mut result = initial_state; + loop { + result = alice::next_state( + &alice.bitcoin_wallet, + &alice.monero_wallet, + &mut alice.transport, + result, + rng, + ) + .await?; + if is_state(&result) { + return Ok(result); + } + } +} + +// TODO: merge this with alice node +// This struct is responsible for I/O +pub struct BobNode<'a> { + transport: Transport, + pub bitcoin_wallet: wallet::bitcoin::Wallet, + pub monero_wallet: wallet::monero::BobWallet<'a>, +} + +impl<'a> BobNode<'a> { + pub fn new( + transport: Transport, + bitcoin_wallet: wallet::bitcoin::Wallet, + monero_wallet: wallet::monero::BobWallet<'a>, + ) -> BobNode<'a> { + Self { + transport, + bitcoin_wallet, + monero_wallet, + } + } +} + +pub async fn run_bob_until<'a, R: RngCore + CryptoRng>( + bob: &mut BobNode<'a>, + initial_state: bob::State, + is_state: fn(&bob::State) -> bool, + rng: &mut R, +) -> Result { + let mut result = initial_state; + loop { + result = bob::next_state( + &bob.bitcoin_wallet, + &bob.monero_wallet, + &mut bob.transport, + result, + rng, + ) + .await?; + if is_state(&result) { + return Ok(result); + } + } +} diff --git a/xmr-btc/tests/transport.rs b/xmr-btc/tests/transport.rs new file mode 100644 index 00000000..b4be445d --- /dev/null +++ b/xmr-btc/tests/transport.rs @@ -0,0 +1,56 @@ +use anyhow::{anyhow, Result}; + +use async_trait::async_trait; +use tokio::{ + stream::StreamExt, + sync::mpsc::{Receiver, Sender}, +}; +use xmr_btc::{alice, bob, transport::SendReceive}; + +#[derive(Debug)] +pub struct Transport { + pub sender: Sender, + pub receiver: Receiver, +} + +#[async_trait] +impl SendReceive for Transport { + async fn send_message(&mut self, message: alice::Message) -> Result<()> { + let _ = self + .sender + .send(message) + .await + .map_err(|_| anyhow!("failed to send message"))?; + Ok(()) + } + + async fn receive_message(&mut self) -> Result { + let message = self + .receiver + .next() + .await + .ok_or_else(|| anyhow!("failed to receive message"))?; + Ok(message) + } +} + +#[async_trait] +impl SendReceive for Transport { + async fn send_message(&mut self, message: bob::Message) -> Result<()> { + let _ = self + .sender + .send(message) + .await + .map_err(|_| anyhow!("failed to send message"))?; + Ok(()) + } + + async fn receive_message(&mut self) -> Result { + let message = self + .receiver + .next() + .await + .ok_or_else(|| anyhow!("failed to receive message"))?; + Ok(message) + } +} diff --git a/xmr-btc/src/bitcoin/wallet.rs b/xmr-btc/tests/wallet/bitcoin.rs similarity index 94% rename from xmr-btc/src/bitcoin/wallet.rs rename to xmr-btc/tests/wallet/bitcoin.rs index b357d823..57d936b0 100644 --- a/xmr-btc/src/bitcoin/wallet.rs +++ b/xmr-btc/tests/wallet/bitcoin.rs @@ -1,6 +1,3 @@ -use crate::bitcoin::{ - BroadcastSignedTransaction, BuildTxLockPsbt, GetRawTransaction, SignTxLock, TxLock, -}; use anyhow::Result; use async_trait::async_trait; use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid}; @@ -8,6 +5,9 @@ use bitcoin_harness::{bitcoind_rpc::PsbtBase64, Bitcoind}; use reqwest::Url; use std::time::Duration; use tokio::time; +use xmr_btc::bitcoin::{ + BroadcastSignedTransaction, BuildTxLockPsbt, GetRawTransaction, SignTxLock, TxLock, +}; #[derive(Debug)] pub struct Wallet(pub bitcoin_harness::Wallet); @@ -109,7 +109,10 @@ impl BroadcastSignedTransaction for Wallet { #[async_trait] impl GetRawTransaction for Wallet { async fn get_raw_transaction(&self, txid: Txid) -> Result { + // TODO: put into loop instead of delaying + time::delay_for(Duration::from_millis(5000)).await; let tx = self.0.get_raw_transaction(txid).await?; + tracing::info!("{}", tx.txid()); Ok(tx) } diff --git a/xmr-btc/tests/wallet/mod.rs b/xmr-btc/tests/wallet/mod.rs new file mode 100644 index 00000000..c2b89100 --- /dev/null +++ b/xmr-btc/tests/wallet/mod.rs @@ -0,0 +1,2 @@ +pub mod bitcoin; +pub mod monero; diff --git a/xmr-btc/src/monero/wallet.rs b/xmr-btc/tests/wallet/monero.rs similarity index 92% rename from xmr-btc/src/monero/wallet.rs rename to xmr-btc/tests/wallet/monero.rs index 2834f134..6f47d3b4 100644 --- a/xmr-btc/src/monero/wallet.rs +++ b/xmr-btc/tests/wallet/monero.rs @@ -1,12 +1,12 @@ -use crate::monero::{ - Amount, CheckTransfer, ImportOutput, PrivateViewKey, PublicKey, PublicViewKey, Transfer, - TransferProof, TxHash, -}; use anyhow::{bail, Result}; use async_trait::async_trait; use monero::{Address, Network, PrivateKey}; use monero_harness::Monero; use std::str::FromStr; +use xmr_btc::monero::{ + Amount, CheckTransfer, ImportOutput, PrivateViewKey, PublicKey, PublicViewKey, Transfer, + TransferProof, TxHash, +}; #[derive(Debug)] pub struct AliceWallet<'c>(pub &'c Monero<'c>); @@ -24,7 +24,7 @@ impl Transfer for AliceWallet<'_> { let res = self .0 - .transfer_from_alice(amount.0, &destination_address.to_string()) + .transfer_from_alice(amount.as_piconero(), &destination_address.to_string()) .await?; let tx_hash = TxHash(res.tx_hash); @@ -32,7 +32,33 @@ impl Transfer for AliceWallet<'_> { let fee = Amount::from_piconero(res.fee); - Ok((TransferProof { tx_hash, tx_key }, fee)) + Ok((TransferProof::new(tx_hash, tx_key), fee)) + } +} + +#[async_trait] +impl ImportOutput for AliceWallet<'_> { + async fn import_output( + &self, + private_spend_key: PrivateKey, + private_view_key: PrivateViewKey, + ) -> Result<()> { + let public_spend_key = PublicKey::from_private_key(&private_spend_key); + let public_view_key = PublicKey::from_private_key(&private_view_key.into()); + + let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key); + + let _ = self + .0 + .alice_wallet_rpc_client() + .generate_from_keys( + &address.to_string(), + &private_spend_key.to_string(), + &PrivateKey::from(private_view_key).to_string(), + ) + .await?; + + Ok(()) } } @@ -54,8 +80,8 @@ impl CheckTransfer for BobWallet<'_> { let res = cli .check_tx_key( - &String::from(transfer_proof.tx_hash), - &transfer_proof.tx_key.to_string(), + &String::from(transfer_proof.tx_hash()), + &transfer_proof.tx_key().to_string(), &address.to_string(), ) .await?; @@ -97,29 +123,3 @@ impl ImportOutput for BobWallet<'_> { Ok(()) } } - -#[async_trait] -impl ImportOutput for AliceWallet<'_> { - async fn import_output( - &self, - private_spend_key: PrivateKey, - private_view_key: PrivateViewKey, - ) -> Result<()> { - let public_spend_key = PublicKey::from_private_key(&private_spend_key); - let public_view_key = PublicKey::from_private_key(&private_view_key.into()); - - let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key); - - let _ = self - .0 - .alice_wallet_rpc_client() - .generate_from_keys( - &address.to_string(), - &private_spend_key.to_string(), - &PrivateKey::from(private_view_key).to_string(), - ) - .await?; - - Ok(()) - } -}