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(()) - } -}