diff --git a/.gitignore b/.gitignore index 088ba6ba..d89bb47f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,148 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/rust,clion+all,emacs +# Edit at https://www.toptal.com/developers/gitignore?templates=rust,clion+all,emacs + +### CLion+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### CLion+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Emacs ### +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile + +# directory configuration +.dir-locals.el + +# network security +/network-security.data + + +### Rust ### # Generated by Cargo # will have compiled files and executables /target/ @@ -8,3 +153,5 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + +# End of https://www.toptal.com/developers/gitignore/api/rust,clion+all,emacs diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index c49df15d..0160696c 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -17,11 +17,15 @@ monero = "0.9" rand = "0.7" sha2 = "0.9" thiserror = "1" +tracing = "0.1" [dev-dependencies] base64 = "0.12" bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "d402b36d3d6406150e3bfb71492ff4a0a7cb290e" } +futures = "0.3" 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 = "0.1" +tracing-subscriber = "0.2" diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index 2e5c1d4f..13917d3e 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -1,31 +1,129 @@ +use crate::{ + bitcoin, + bitcoin::{BroadcastSignedTransaction, WatchForRawTransaction}, + bob, monero, + monero::{CreateWalletForOutput, Transfer}, + transport::{ReceiveMessage, SendMessage}, +}; use anyhow::{anyhow, Result}; -use ecdsa_fun::adaptor::{Adaptor, EncryptedSignature}; +use ecdsa_fun::{ + adaptor::{Adaptor, EncryptedSignature}, + nonce::Deterministic, +}; use rand::{CryptoRng, RngCore}; - -use crate::{bitcoin, bitcoin::GetRawTransaction, bob, monero, monero::ImportOutput}; -use ecdsa_fun::{nonce::Deterministic, Signature}; use sha2::Sha256; +use std::convert::{TryFrom, TryInto}; + +pub mod message; +pub use message::{Message, Message0, Message1, Message2}; + +pub async fn next_state< + R: RngCore + CryptoRng, + B: WatchForRawTransaction + BroadcastSignedTransaction, + M: CreateWalletForOutput + Transfer, + T: SendMessage + ReceiveMessage, +>( + 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?; -#[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, + let bob_message0 = transport.receive_message().await?.try_into()?; + let state1 = state0.receive(bob_message0)?; + Ok(state1.into()) + } + State::State1(state1) => { + let bob_message1 = transport.receive_message().await?.try_into()?; + let state2 = state1.receive(bob_message1); + let alice_message1 = state2.next_message(); + transport.send_message(alice_message1.into()).await?; + Ok(state2.into()) + } + State::State2(state2) => { + let bob_message2 = transport.receive_message().await?.try_into()?; + let state3 = state2.receive(bob_message2)?; + 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 = 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?; + Ok(state6.into()) + } + State::State6(state6) => Ok(state6.into()), + } } +#[allow(clippy::large_enum_variant)] #[derive(Debug)] -pub struct Message1 { - pub(crate) tx_cancel_sig: Signature, - pub(crate) tx_refund_encsig: EncryptedSignature, +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, +impl_try_from_parent_enum!(State0, State); +impl_try_from_parent_enum!(State1, State); +impl_try_from_parent_enum!(State2, State); +impl_try_from_parent_enum!(State3, State); +impl_try_from_parent_enum!(State4, State); +impl_try_from_parent_enum!(State5, State); +impl_try_from_parent_enum!(State6, State); + +impl_from_child_enum!(State0, State); +impl_from_child_enum!(State1, State); +impl_from_child_enum!(State2, State); +impl_from_child_enum!(State3, State); +impl_from_child_enum!(State4, State); +impl_from_child_enum!(State5, State); +impl_from_child_enum!(State6, State); + +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)] @@ -250,12 +348,15 @@ pub struct State3 { impl State3 { pub async fn watch_for_lock_btc(self, bitcoin_wallet: &W) -> Result where - W: bitcoin::GetRawTransaction, + W: bitcoin::WatchForRawTransaction, { - let _ = bitcoin_wallet - .get_raw_transaction(self.tx_lock.txid()) + tracing::info!("watching for lock btc with txid: {}", self.tx_lock.txid()); + let tx = bitcoin_wallet + .watch_for_raw_transaction(self.tx_lock.txid()) .await?; + tracing::info!("tx lock seen with txid: {}", tx.txid()); + Ok(State4 { a: self.a, B: self.B, @@ -298,7 +399,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 +412,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( @@ -383,7 +482,7 @@ impl State4 { } #[derive(Debug)] -pub struct State4b { +pub struct State5 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, s_a: cross_curve_dleq::Scalar, @@ -401,17 +500,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,14 +528,15 @@ 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, } } // watch for refund on btc, recover s_b and refund xmr pub async fn refund_xmr(self, bitcoin_wallet: &B, monero_wallet: &M) -> Result<()> where - B: GetRawTransaction, - M: ImportOutput, + B: WatchForRawTransaction, + M: CreateWalletForOutput, { let tx_cancel = bitcoin::TxCancel::new( &self.tx_lock, @@ -448,7 +549,9 @@ impl State4b { let tx_refund_encsig = self.a.encsign(self.S_b_bitcoin.clone(), tx_refund.digest()); - let tx_refund_candidate = bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?; + let tx_refund_candidate = bitcoin_wallet + .watch_for_raw_transaction(tx_refund.txid()) + .await?; let tx_refund_sig = tx_refund.extract_signature_by_key(tx_refund_candidate, self.a.public())?; @@ -462,7 +565,7 @@ impl State4b { // NOTE: This actually generates and opens a new wallet, closing the currently // open one. monero_wallet - .import_output(monero::PrivateKey::from_scalar(s), self.v) + .create_and_load_wallet_for_output(monero::PrivateKey::from_scalar(s), self.v) .await?; Ok(()) @@ -470,7 +573,7 @@ impl State4b { } #[derive(Debug)] -pub struct State5 { +pub struct State6 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, s_a: cross_curve_dleq::Scalar, @@ -487,9 +590,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 +617,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..7c95604b --- /dev/null +++ b/xmr-btc/src/alice/message.rs @@ -0,0 +1,42 @@ +use anyhow::Result; +use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; +use std::convert::TryFrom; + +use crate::{bitcoin, monero}; + +#[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_try_from_parent_enum!(Message0, Message); +impl_try_from_parent_enum!(Message1, Message); +impl_try_from_parent_enum!(Message2, Message); + +impl_from_child_enum!(Message0, Message); +impl_from_child_enum!(Message1, Message); +impl_from_child_enum!(Message2, Message); diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index 25a1b458..5bad1f9d 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; @@ -10,6 +8,7 @@ use bitcoin::{ util::psbt::PartiallySignedTransaction, SigHash, Transaction, }; +pub use bitcoin::{Address, Amount, OutPoint, Txid}; use ecdsa_fun::{ adaptor::Adaptor, fun::{ @@ -19,17 +18,13 @@ use ecdsa_fun::{ nonce::Deterministic, ECDSA, }; +pub use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; use miniscript::{Descriptor, Segwitv0}; use rand::{CryptoRng, RngCore}; use sha2::Sha256; use std::str::FromStr; pub use crate::bitcoin::transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund}; -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; @@ -193,8 +188,8 @@ pub trait BroadcastSignedTransaction { } #[async_trait] -pub trait GetRawTransaction { - async fn get_raw_transaction(&self, txid: Txid) -> Result; +pub trait WatchForRawTransaction { + async fn watch_for_raw_transaction(&self, txid: Txid) -> Result; } pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result { diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 25fcfadb..5db4465f 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -1,7 +1,12 @@ use crate::{ alice, - bitcoin::{self, BuildTxLockPsbt, GetRawTransaction, TxCancel}, + bitcoin::{ + self, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxCancel, + WatchForRawTransaction, + }, monero, + monero::{CheckTransfer, CreateWalletForOutput}, + transport::{ReceiveMessage, SendMessage}, }; use anyhow::{anyhow, Result}; use ecdsa_fun::{ @@ -11,32 +16,88 @@ use ecdsa_fun::{ }; use rand::{CryptoRng, RngCore}; use sha2::Sha256; +use std::convert::{TryFrom, TryInto}; + +pub mod message; +pub use message::{Message, Message0, Message1, Message2, Message3}; + +pub async fn next_state< + R: RngCore + CryptoRng, + B: WatchForRawTransaction + SignTxLock + BuildTxLockPsbt + BroadcastSignedTransaction, + M: CreateWalletForOutput + CheckTransfer, + T: SendMessage + ReceiveMessage, +>( + 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 = 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?; -#[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, + let 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 = 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"); + 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, -} +impl_try_from_parent_enum!(State0, State); +impl_try_from_parent_enum!(State1, State); +impl_try_from_parent_enum!(State2, State); +impl_try_from_parent_enum!(State3, State); +impl_try_from_parent_enum!(State4, State); +impl_try_from_parent_enum!(State5, State); -#[derive(Debug)] -pub struct Message3 { - pub(crate) tx_redeem_encsig: EncryptedSignature, -} +impl_from_child_enum!(State0, State); +impl_from_child_enum!(State1, State); +impl_from_child_enum!(State2, State); +impl_from_child_enum!(State3, State); +impl_from_child_enum!(State4, State); +impl_from_child_enum!(State5, State); #[derive(Debug)] pub struct State0 { @@ -228,17 +289,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, @@ -259,8 +321,8 @@ impl State2 { } } -#[derive(Debug, Clone)] -pub struct State2b { +#[derive(Debug)] +pub struct State3 { A: bitcoin::PublicKey, b: bitcoin::SecretKey, s_b: cross_curve_dleq::Scalar, @@ -279,8 +341,8 @@ 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 { + pub async fn watch_for_lock_xmr(self, xmr_wallet: &W, msg: alice::Message2) -> Result where W: monero::CheckTransfer, { @@ -293,7 +355,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 +421,13 @@ impl State2b { } Ok(()) } - - #[cfg(test)] pub fn tx_lock_id(&self) -> bitcoin::Txid { self.tx_lock.txid() } } -#[derive(Debug, Clone)] -pub struct State3 { +#[derive(Debug)] +pub struct State4 { A: bitcoin::PublicKey, b: bitcoin::SecretKey, s_b: cross_curve_dleq::Scalar, @@ -386,7 +446,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,14 +454,16 @@ 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, + W: WatchForRawTransaction, { 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()); - let tx_redeem_candidate = bitcoin_wallet.get_raw_transaction(tx_redeem.txid()).await?; + let tx_redeem_candidate = bitcoin_wallet + .watch_for_raw_transaction(tx_redeem.txid()) + .await?; let tx_redeem_sig = tx_redeem.extract_signature_by_key(tx_redeem_candidate, self.b.public())?; @@ -409,7 +471,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, @@ -432,7 +494,7 @@ impl State3 { } #[derive(Debug)] -pub struct State4 { +pub struct State5 { A: bitcoin::PublicKey, b: bitcoin::SecretKey, s_a: monero::PrivateKey, @@ -452,10 +514,10 @@ 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, + W: monero::CreateWalletForOutput, { let s_b = monero::PrivateKey { scalar: self.s_b.into_ed25519(), @@ -465,8 +527,13 @@ impl State4 { // NOTE: This actually generates and opens a new wallet, closing the currently // open one. - monero_wallet.import_output(s, self.v).await?; + monero_wallet + .create_and_load_wallet_for_output(s, self.v) + .await?; 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..de02b7a5 --- /dev/null +++ b/xmr-btc/src/bob/message.rs @@ -0,0 +1,48 @@ +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_try_from_parent_enum!(Message0, Message); +impl_try_from_parent_enum!(Message1, Message); +impl_try_from_parent_enum!(Message2, Message); +impl_try_from_parent_enum!(Message3, Message); + +impl_from_child_enum!(Message0, Message); +impl_from_child_enum!(Message1, Message); +impl_from_child_enum!(Message2, Message); +impl_from_child_enum!(Message3, Message); diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index d4e66337..790cb477 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -14,371 +14,39 @@ #![forbid(unsafe_code)] #![allow(non_snake_case)] -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); +#[macro_use] +mod utils { + + macro_rules! impl_try_from_parent_enum { + ($type:ident, $parent:ident) => { + impl TryFrom<$parent> for $type { + type Error = anyhow::Error; + fn try_from(from: $parent) -> Result { + if let $parent::$type(inner) = from { + Ok(inner) + } else { + Err(anyhow::anyhow!( + "Failed to convert parent state to child state" + )) + } + } + } + }; } - #[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 - ); + macro_rules! impl_from_child_enum { + ($type:ident, $parent:ident) => { + impl From<$type> for $parent { + fn from(from: $type) -> Self { + $parent::$type(from) + } + } + }; } } + +pub mod alice; +pub mod bitcoin; +pub mod bob; +pub mod monero; +pub mod transport; diff --git a/xmr-btc/src/monero.rs b/xmr-btc/src/monero.rs index b67caabd..459fa708 100644 --- a/xmr-btc/src/monero.rs +++ b/xmr-btc/src/monero.rs @@ -1,14 +1,9 @@ -#[cfg(test)] -pub mod wallet; - -use std::ops::Add; - use anyhow::Result; use async_trait::async_trait; -use rand::{CryptoRng, RngCore}; - pub use curve25519_dalek::scalar::Scalar; pub use monero::{Address, PrivateKey, PublicKey}; +use rand::{CryptoRng, RngCore}; +use std::ops::Add; pub fn random_private_key(rng: &mut R) -> PrivateKey { let scalar = Scalar::random(rng); @@ -16,9 +11,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 +61,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 +78,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 { @@ -114,8 +122,8 @@ pub trait CheckTransfer { } #[async_trait] -pub trait ImportOutput { - async fn import_output( +pub trait CreateWalletForOutput { + async fn create_and_load_wallet_for_output( &self, private_spend_key: PrivateKey, private_view_key: PrivateViewKey, diff --git a/xmr-btc/src/transport.rs b/xmr-btc/src/transport.rs new file mode 100644 index 00000000..f71992fb --- /dev/null +++ b/xmr-btc/src/transport.rs @@ -0,0 +1,12 @@ +use anyhow::Result; +use async_trait::async_trait; + +#[async_trait] +pub trait SendMessage { + async fn send_message(&mut self, message: SendMsg) -> Result<()>; +} + +#[async_trait] +pub trait ReceiveMessage { + 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..fb2ddeec --- /dev/null +++ b/xmr-btc/tests/e2e.rs @@ -0,0 +1,398 @@ +use crate::harness::wallet; +use bitcoin_harness::Bitcoind; +use harness::{ + node::{AliceNode, BobNode}, + transport::Transport, +}; +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 harness; + +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, + ); + 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, + ) +} + +mod tests { + use crate::{ + harness, + harness::node::{run_alice_until, run_bob_until}, + init_bitcoind, init_test, + }; + 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(), + harness::alice::is_state6, + &mut OsRng, + ), + run_bob_until( + &mut bob_node, + bob_state0.into(), + harness::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(); + + 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_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 + ); + + 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(), + harness::alice::is_state5, + &mut OsRng, + ), + run_bob_until( + &mut bob_node, + bob_state0.into(), + harness::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(); + + 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(); + + 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 + ); + + // 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(), + harness::alice::is_state4, + &mut OsRng, + ), + run_bob_until( + &mut bob_node, + bob_state0.into(), + harness::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/harness/mod.rs b/xmr-btc/tests/harness/mod.rs new file mode 100644 index 00000000..4f2cba46 --- /dev/null +++ b/xmr-btc/tests/harness/mod.rs @@ -0,0 +1,36 @@ +pub mod node; +pub mod transport; +pub mod wallet; + +pub mod bob { + use xmr_btc::bob::State; + + // TODO: use macro or generics + pub fn is_state5(state: &State) -> bool { + matches!(state, State::State5 { .. }) + } + + // TODO: use macro or generics + pub fn is_state3(state: &State) -> bool { + matches!(state, State::State3 { .. }) + } +} + +pub mod alice { + use xmr_btc::alice::State; + + // TODO: use macro or generics + pub fn is_state4(state: &State) -> bool { + matches!(state, State::State4 { .. }) + } + + // TODO: use macro or generics + pub fn is_state5(state: &State) -> bool { + matches!(state, State::State5 { .. }) + } + + // TODO: use macro or generics + pub fn is_state6(state: &State) -> bool { + matches!(state, State::State6 { .. }) + } +} diff --git a/xmr-btc/tests/harness/node.rs b/xmr-btc/tests/harness/node.rs new file mode 100644 index 00000000..88ee0a6a --- /dev/null +++ b/xmr-btc/tests/harness/node.rs @@ -0,0 +1,92 @@ +use crate::harness::{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/harness/transport.rs b/xmr-btc/tests/harness/transport.rs new file mode 100644 index 00000000..1c912b02 --- /dev/null +++ b/xmr-btc/tests/harness/transport.rs @@ -0,0 +1,45 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use tokio::{ + stream::StreamExt, + sync::mpsc::{Receiver, Sender}, +}; +use xmr_btc::transport::{ReceiveMessage, SendMessage}; + +#[derive(Debug)] +pub struct Transport { + pub sender: Sender, + pub receiver: Receiver, +} + +#[async_trait] +impl SendMessage for Transport +where + SendMsg: Send + Sync, + RecvMsg: std::marker::Send, +{ + async fn send_message(&mut self, message: SendMsg) -> Result<()> { + let _ = self + .sender + .send(message) + .await + .map_err(|_| anyhow!("failed to send message"))?; + Ok(()) + } +} + +#[async_trait] +impl ReceiveMessage for Transport +where + SendMsg: std::marker::Send, + RecvMsg: Send + Sync, +{ + 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/harness/wallet/bitcoin.rs similarity index 87% rename from xmr-btc/src/bitcoin/wallet.rs rename to xmr-btc/tests/harness/wallet/bitcoin.rs index b357d823..524baaa4 100644 --- a/xmr-btc/src/bitcoin/wallet.rs +++ b/xmr-btc/tests/harness/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, SignTxLock, TxLock, WatchForRawTransaction, +}; #[derive(Debug)] pub struct Wallet(pub bitcoin_harness::Wallet); @@ -107,10 +107,13 @@ impl BroadcastSignedTransaction for Wallet { } #[async_trait] -impl GetRawTransaction for Wallet { - async fn get_raw_transaction(&self, txid: Txid) -> Result { - let tx = self.0.get_raw_transaction(txid).await?; - - Ok(tx) +impl WatchForRawTransaction for Wallet { + async fn watch_for_raw_transaction(&self, txid: Txid) -> Result { + loop { + if let Ok(tx) = self.0.get_raw_transaction(txid).await { + return Ok(tx); + } + time::delay_for(Duration::from_millis(200)).await; + } } } diff --git a/xmr-btc/tests/harness/wallet/mod.rs b/xmr-btc/tests/harness/wallet/mod.rs new file mode 100644 index 00000000..c2b89100 --- /dev/null +++ b/xmr-btc/tests/harness/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/harness/wallet/monero.rs similarity index 83% rename from xmr-btc/src/monero/wallet.rs rename to xmr-btc/tests/harness/wallet/monero.rs index 2834f134..ca5e9039 100644 --- a/xmr-btc/src/monero/wallet.rs +++ b/xmr-btc/tests/harness/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, CreateWalletForOutput, 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 CreateWalletForOutput for AliceWallet<'_> { + async fn create_and_load_wallet_for_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?; @@ -73,8 +99,8 @@ impl CheckTransfer for BobWallet<'_> { } #[async_trait] -impl ImportOutput for BobWallet<'_> { - async fn import_output( +impl CreateWalletForOutput for BobWallet<'_> { + async fn create_and_load_wallet_for_output( &self, private_spend_key: PrivateKey, private_view_key: PrivateViewKey, @@ -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(()) - } -}