From 5daa3ea9a8a78d0333142c203a302d3634c42b93 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Mon, 12 Oct 2020 17:17:22 +1100 Subject: [PATCH 01/52] [WIP] Generate actions for Bob's on-chain protocol Mimics what @thomaseizinger did here [1] and here [2]. This has the advantage that the consumer has more freedom to execute `Action`s without having to implement particular traits. The error handling required inside this protocol-executing function is also reduced. As discussed with Thomas, for this approach to work well, the trait functions such as `receive_transfer_proof` should be infallible, and the implementer should be forced to hide IO errors behind a retry mechanism. All of these asynchronous calls need to be "raced" against the abort condition (determined by the `refund_timelock`), which is missing in the current state of the implementation. The initial handshake of the protocol has not been included here, because it may not be easy to integrate this approach with libp2p, but a couple of messages still need to exchanged. I need @tcharding to tell me if it's feasible/good to do it like this. [1] https://github.com/comit-network/comit-rs/blob/move-nectar-swap-to-comit/nectar/src/swap/comit/herc20_hbit.rs#L57-L184. [2] https://github.com/comit-network/comit-rs/blob/e584d2b14f3602e2657d09b989e8ea1d483a0626/nectar/src/swap.rs#L716-L751. --- xmr-btc/Cargo.toml | 1 + xmr-btc/src/bitcoin.rs | 6 +- xmr-btc/src/bob.rs | 26 +++---- xmr-btc/src/lib.rs | 148 ++++++++++++++++++++++++++++++++++++++ xmr-btc/tests/on_chain.rs | 67 +++++++++++++++++ 5 files changed, 232 insertions(+), 16 deletions(-) create mode 100644 xmr-btc/tests/on_chain.rs diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index 0160696c..35475ab1 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -12,6 +12,7 @@ cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", curve25519-dalek = "2" ecdsa_fun = { version = "0.3.1", features = ["libsecp_compat"] } ed25519-dalek = "1.0.0-pre.4" # Cannot be 1 because they depend on curve25519-dalek version 3 +genawaiter = "0.99.1" miniscript = "1" monero = "0.9" rand = "0.7" diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index 5bad1f9d..43622cb6 100644 --- a/xmr-btc/src/bitcoin.rs +++ b/xmr-btc/src/bitcoin.rs @@ -6,9 +6,8 @@ use bitcoin::{ hashes::{hex::ToHex, Hash}, secp256k1, util::psbt::PartiallySignedTransaction, - SigHash, Transaction, + SigHash, }; -pub use bitcoin::{Address, Amount, OutPoint, Txid}; use ecdsa_fun::{ adaptor::Adaptor, fun::{ @@ -18,13 +17,14 @@ 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, Transaction, Txid}; +pub use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; pub const TX_FEE: u64 = 10_000; diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index f3c38157..75c13bd3 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -255,22 +255,22 @@ impl State1 { #[derive(Debug)] pub struct State2 { - A: bitcoin::PublicKey, - b: bitcoin::SecretKey, - s_b: cross_curve_dleq::Scalar, - S_a_monero: monero::PublicKey, - S_a_bitcoin: bitcoin::PublicKey, - v: monero::PrivateViewKey, + pub A: bitcoin::PublicKey, + pub b: bitcoin::SecretKey, + pub s_b: cross_curve_dleq::Scalar, + pub S_a_monero: monero::PublicKey, + pub S_a_bitcoin: bitcoin::PublicKey, + pub v: monero::PrivateViewKey, btc: bitcoin::Amount, - xmr: monero::Amount, - refund_timelock: u32, + pub xmr: monero::Amount, + pub refund_timelock: u32, punish_timelock: u32, - refund_address: bitcoin::Address, - redeem_address: bitcoin::Address, + pub refund_address: bitcoin::Address, + pub redeem_address: bitcoin::Address, punish_address: bitcoin::Address, - tx_lock: bitcoin::TxLock, - tx_cancel_sig_a: Signature, - tx_refund_encsig: EncryptedSignature, + pub tx_lock: bitcoin::TxLock, + pub tx_cancel_sig_a: Signature, + pub tx_refund_encsig: EncryptedSignature, } impl State2 { diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 790cb477..a26269a9 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -50,3 +50,151 @@ pub mod bitcoin; pub mod bob; pub mod monero; pub mod transport; + +use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; +use genawaiter::sync::{Gen, GenBoxed}; +use sha2::Sha256; + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum Action { + LockBitcoin(bitcoin::TxLock), + SendBitcoinRedeemEncsig(bitcoin::EncryptedSignature), + CreateMoneroWalletForOutput { + spend_key: monero::PrivateKey, + view_key: monero::PrivateViewKey, + }, + RefundBitcoin { + tx_cancel: bitcoin::Transaction, + tx_refund: bitcoin::Transaction, + }, +} + +// TODO: This could be moved to the monero module +pub trait ReceiveTransferProof { + fn receive_transfer_proof(&self) -> monero::TransferProof; +} + +/// Perform the on-chain protocol to swap monero and bitcoin as Bob. +/// +/// This is called post handshake, after all the keys, addresses and most of the +/// signatures have been exchanged. +pub fn action_generator_bob( + network: &'static N, + monero_ledger: &'static M, + bitcoin_ledger: &'static B, + // TODO: Replace this with a new, slimmer struct? + bob::State2 { + A, + b, + s_b, + S_a_monero, + S_a_bitcoin, + v, + xmr, + refund_timelock, + redeem_address, + refund_address, + tx_lock, + tx_cancel_sig_a, + tx_refund_encsig, + .. + }: bob::State2, +) -> GenBoxed +where + N: ReceiveTransferProof + Send + Sync, + M: monero::CheckTransfer + Send + Sync, + B: bitcoin::WatchForRawTransaction + Send + Sync, +{ + Gen::new_boxed(|co| async move { + let swap_result: Result<(), ()> = { + co.yield_(Action::LockBitcoin(tx_lock.clone())).await; + + // the source of this could be the database, this layer doesn't care + let transfer_proof = network.receive_transfer_proof(); + + let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar( + s_b.into_ed25519(), + )); + let S = S_a_monero + S_b_monero; + + // TODO: We should require a specific number of confirmations on the lock + // transaction + monero_ledger + .check_transfer(S, v.public(), transfer_proof, xmr) + .await + .expect("TODO: implementor of this trait must make it infallible by retrying"); + + let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address); + let tx_redeem_encsig = b.encsign(S_a_bitcoin.clone(), tx_redeem.digest()); + + co.yield_(Action::SendBitcoinRedeemEncsig(tx_redeem_encsig.clone())) + .await; + + let tx_redeem_published = bitcoin_ledger + .watch_for_raw_transaction(tx_redeem.txid()) + .await + .expect("TODO: implementor of this trait must make it infallible by retrying"); + + // NOTE: If any of this fails, Bob will never be able to take the monero. + // Therefore, there is no way to handle these errors other than aborting + let tx_redeem_sig = tx_redeem + .extract_signature_by_key(tx_redeem_published, b.public()) + .expect("redeem transaction must include signature from us"); + let s_a = bitcoin::recover(S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig).expect( + "alice can only produce our signature by decrypting our encrypted signature", + ); + let s_a = monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order( + s_a.to_bytes(), + )); + + let s_b = monero::PrivateKey { + scalar: s_b.into_ed25519(), + }; + + co.yield_(Action::CreateMoneroWalletForOutput { + spend_key: s_a + s_b, + view_key: v, + }) + .await; + + Ok(()) + }; + + // NOTE: swap result should only be `Err` if we have reached the + // `refund_timelock`. Therefore, we should always yield the refund action + if swap_result.is_err() { + let tx_cancel = + bitcoin::TxCancel::new(&tx_lock, refund_timelock, A.clone(), b.public()); + let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address); + + let signed_tx_cancel = { + let sig_a = tx_cancel_sig_a.clone(); + let sig_b = b.sign(tx_cancel.digest()); + + tx_cancel + .clone() + .add_signatures(&tx_lock, (A.clone(), sig_a), (b.public(), sig_b)) + .expect("sig_{a,b} to be valid signatures for tx_cancel") + }; + + let signed_tx_refund = { + let adaptor = Adaptor::>::default(); + + let sig_a = + adaptor.decrypt_signature(&s_b.into_secp256k1(), tx_refund_encsig.clone()); + let sig_b = b.sign(tx_refund.digest()); + + tx_refund + .add_signatures(&tx_cancel, (A.clone(), sig_a), (b.public(), sig_b)) + .expect("sig_{a,b} to be valid signatures for tx_refund") + }; + + co.yield_(Action::RefundBitcoin { + tx_cancel: signed_tx_cancel, + tx_refund: signed_tx_refund, + }) + .await; + } + }) +} diff --git a/xmr-btc/tests/on_chain.rs b/xmr-btc/tests/on_chain.rs new file mode 100644 index 00000000..d8d9e59d --- /dev/null +++ b/xmr-btc/tests/on_chain.rs @@ -0,0 +1,67 @@ +mod harness; + +use anyhow::Result; +use genawaiter::GeneratorState; +use harness::wallet::{bitcoin, monero}; +use xmr_btc::{ + action_generator_bob, + bitcoin::{BroadcastSignedTransaction, SignTxLock}, + bob, + monero::CreateWalletForOutput, + Action, ReceiveTransferProof, +}; + +struct Network; + +impl ReceiveTransferProof for Network { + fn receive_transfer_proof(&self) -> xmr_btc::monero::TransferProof { + todo!("use libp2p") + } +} + +async fn swap_as_bob( + network: &'static Network, + monero_wallet: &'static monero::BobWallet<'static>, + bitcoin_wallet: &'static bitcoin::Wallet, + state: bob::State2, +) -> Result<()> { + let mut action_generator = action_generator_bob(network, monero_wallet, bitcoin_wallet, state); + + loop { + match action_generator.async_resume().await { + GeneratorState::Yielded(Action::LockBitcoin(tx_lock)) => { + let signed_tx_lock = bitcoin_wallet.sign_tx_lock(tx_lock).await?; + let _ = bitcoin_wallet + .broadcast_signed_transaction(signed_tx_lock) + .await?; + } + GeneratorState::Yielded(Action::SendBitcoinRedeemEncsig(_tx_redeem_encsig)) => { + todo!("use libp2p") + } + GeneratorState::Yielded(Action::CreateMoneroWalletForOutput { + spend_key, + view_key, + }) => { + monero_wallet + .create_and_load_wallet_for_output(spend_key, view_key) + .await?; + } + GeneratorState::Yielded(Action::RefundBitcoin { + tx_cancel, + tx_refund, + }) => { + let _ = bitcoin_wallet + .broadcast_signed_transaction(tx_cancel) + .await?; + + let _ = bitcoin_wallet + .broadcast_signed_transaction(tx_refund) + .await?; + } + GeneratorState::Complete(()) => return Ok(()), + } + } +} + +#[test] +fn on_chain_happy_path() {} From 15f7932f7f52ffc042136f867d0a051797a19fc8 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Thu, 15 Oct 2020 13:10:31 +1100 Subject: [PATCH 02/52] Replace monero::CheckTransfer with monero::WatchForTransfer Instead of checking once to see if Monero's `TxLock` has been published, the new trait should keep looking until the transaction has been found. The new trait also allows the caller to set an expected number of confirmations on the transaction. The implementation of the trait is currently part of test code, but it should be similar to what we will eventually do for an application. --- xmr-btc/Cargo.toml | 1 + xmr-btc/src/bob.rs | 14 +++-- xmr-btc/src/lib.rs | 15 ++++-- xmr-btc/src/monero.rs | 21 +++++--- xmr-btc/tests/harness/wallet/monero.rs | 72 +++++++++++++++++--------- 5 files changed, 84 insertions(+), 39 deletions(-) diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index 35475ab1..d82790f5 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -21,6 +21,7 @@ thiserror = "1" tracing = "0.1" [dev-dependencies] +backoff = { version = "0.2", features = ["tokio"] } base64 = "0.12" bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "d402b36d3d6406150e3bfb71492ff4a0a7cb290e" } futures = "0.3" diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 75c13bd3..2d4105d7 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -5,7 +5,7 @@ use crate::{ WatchForRawTransaction, }, monero, - monero::{CheckTransfer, CreateWalletForOutput}, + monero::{CreateWalletForOutput, WatchForTransfer}, transport::{ReceiveMessage, SendMessage}, }; use anyhow::{anyhow, Result}; @@ -27,7 +27,7 @@ pub use message::{Message, Message0, Message1, Message2, Message3}; pub async fn next_state< R: RngCore + CryptoRng, B: WatchForRawTransaction + SignTxLock + BuildTxLockPsbt + BroadcastSignedTransaction, - M: CreateWalletForOutput + CheckTransfer, + M: CreateWalletForOutput + WatchForTransfer, T: SendMessage + ReceiveMessage, >( bitcoin_wallet: &B, @@ -347,7 +347,7 @@ pub struct State3 { impl State3 { pub async fn watch_for_lock_xmr(self, xmr_wallet: &W, msg: alice::Message2) -> Result where - W: monero::CheckTransfer, + W: monero::WatchForTransfer, { let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar( self.s_b.into_ed25519(), @@ -355,7 +355,13 @@ impl State3 { let S = self.S_a_monero + S_b_monero; xmr_wallet - .check_transfer(S, self.v.public(), msg.tx_lock_proof, self.xmr) + .watch_for_transfer( + S, + self.v.public(), + msg.tx_lock_proof, + self.xmr, + monero::MIN_CONFIRMATIONS, + ) .await?; Ok(State4 { diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index a26269a9..f69f1a7a 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -103,11 +103,15 @@ pub fn action_generator_bob( ) -> GenBoxed where N: ReceiveTransferProof + Send + Sync, - M: monero::CheckTransfer + Send + Sync, + M: monero::WatchForTransfer + Send + Sync, B: bitcoin::WatchForRawTransaction + Send + Sync, { + enum SwapFailedRefund { + InsufficientXMR(monero::InsufficientFunds), + } + Gen::new_boxed(|co| async move { - let swap_result: Result<(), ()> = { + let swap_result: Result<(), SwapFailedRefund> = async { co.yield_(Action::LockBitcoin(tx_lock.clone())).await; // the source of this could be the database, this layer doesn't care @@ -121,9 +125,9 @@ where // TODO: We should require a specific number of confirmations on the lock // transaction monero_ledger - .check_transfer(S, v.public(), transfer_proof, xmr) + .watch_for_transfer(S, v.public(), transfer_proof, xmr, 10) .await - .expect("TODO: implementor of this trait must make it infallible by retrying"); + .map_err(|e| SwapFailedRefund::InsufficientXMR(e))?; let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address); let tx_redeem_encsig = b.encsign(S_a_bitcoin.clone(), tx_redeem.digest()); @@ -159,7 +163,8 @@ where .await; Ok(()) - }; + } + .await; // NOTE: swap result should only be `Err` if we have reached the // `refund_timelock`. Therefore, we should always yield the refund action diff --git a/xmr-btc/src/monero.rs b/xmr-btc/src/monero.rs index 459fa708..241666c7 100644 --- a/xmr-btc/src/monero.rs +++ b/xmr-btc/src/monero.rs @@ -1,10 +1,11 @@ -use anyhow::Result; use async_trait::async_trait; pub use curve25519_dalek::scalar::Scalar; pub use monero::{Address, PrivateKey, PublicKey}; use rand::{CryptoRng, RngCore}; use std::ops::Add; +pub const MIN_CONFIRMATIONS: u32 = 10; + pub fn random_private_key(rng: &mut R) -> PrivateKey { let scalar = Scalar::random(rng); @@ -107,18 +108,26 @@ pub trait Transfer { public_spend_key: PublicKey, public_view_key: PublicViewKey, amount: Amount, - ) -> Result<(TransferProof, Amount)>; + ) -> anyhow::Result<(TransferProof, Amount)>; } #[async_trait] -pub trait CheckTransfer { - async fn check_transfer( +pub trait WatchForTransfer { + async fn watch_for_transfer( &self, public_spend_key: PublicKey, public_view_key: PublicViewKey, transfer_proof: TransferProof, amount: Amount, - ) -> Result<()>; + expected_confirmations: u32, + ) -> Result<(), InsufficientFunds>; +} + +#[derive(Debug, Clone, Copy, thiserror::Error)] +#[error("transaction does not pay enough: expected {expected:?}, got {actual:?}")] +pub struct InsufficientFunds { + pub expected: Amount, + pub actual: Amount, } #[async_trait] @@ -127,5 +136,5 @@ pub trait CreateWalletForOutput { &self, private_spend_key: PrivateKey, private_view_key: PrivateViewKey, - ) -> Result<()>; + ) -> anyhow::Result<()>; } diff --git a/xmr-btc/tests/harness/wallet/monero.rs b/xmr-btc/tests/harness/wallet/monero.rs index ca5e9039..9faf7dd8 100644 --- a/xmr-btc/tests/harness/wallet/monero.rs +++ b/xmr-btc/tests/harness/wallet/monero.rs @@ -1,11 +1,12 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use async_trait::async_trait; +use backoff::{future::FutureOperation as _, ExponentialBackoff}; 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, + Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicKey, PublicViewKey, + Transfer, TransferProof, TxHash, WatchForTransfer, }; #[derive(Debug)] @@ -66,33 +67,56 @@ impl CreateWalletForOutput for AliceWallet<'_> { pub struct BobWallet<'c>(pub &'c Monero<'c>); #[async_trait] -impl CheckTransfer for BobWallet<'_> { - async fn check_transfer( +impl WatchForTransfer for BobWallet<'_> { + async fn watch_for_transfer( &self, public_spend_key: PublicKey, public_view_key: PublicViewKey, transfer_proof: TransferProof, - amount: Amount, - ) -> Result<()> { - let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key.into()); - - let cli = self.0.bob_wallet_rpc_client(); + expected_amount: Amount, + expected_confirmations: u32, + ) -> Result<(), InsufficientFunds> { + enum Error { + TxNotFound, + InsufficientConfirmations, + InsufficientFunds { expected: Amount, actual: Amount }, + } - let res = cli - .check_tx_key( - &String::from(transfer_proof.tx_hash()), - &transfer_proof.tx_key().to_string(), - &address.to_string(), - ) - .await?; + let wallet = self.0.bob_wallet_rpc_client(); + let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key.into()); - if res.received != u64::from(amount) { - bail!( - "tx_lock doesn't pay enough: expected {:?}, got {:?}", - res.received, - amount - ) - } + let res = (|| async { + // NOTE: Currently, this is conflating IO errors with the transaction not being + // in the blockchain yet, or not having enough confirmations on it. All these + // errors warrant a retry, but the strategy should probably differ per case + let proof = wallet + .check_tx_key( + &String::from(transfer_proof.tx_hash()), + &transfer_proof.tx_key().to_string(), + &address.to_string(), + ) + .await + .map_err(|_| backoff::Error::Transient(Error::TxNotFound))?; + + if proof.received != expected_amount.as_piconero() { + return Err(backoff::Error::Permanent(Error::InsufficientFunds { + expected: expected_amount, + actual: Amount::from_piconero(proof.received), + })); + } + + if proof.confirmations < expected_confirmations { + return Err(backoff::Error::Transient(Error::InsufficientConfirmations)); + } + + Ok(proof) + }) + .retry(ExponentialBackoff::default()) + .await; + + if let Err(Error::InsufficientFunds { expected, actual }) = res { + return Err(InsufficientFunds { expected, actual }); + }; Ok(()) } From 08be87747f8029943a41512a2d0427c6eb84e26e Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Thu, 15 Oct 2020 18:34:13 +1100 Subject: [PATCH 03/52] Configure exponential backoff so that it never stops retrying --- xmr-btc/tests/harness/wallet/monero.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/xmr-btc/tests/harness/wallet/monero.rs b/xmr-btc/tests/harness/wallet/monero.rs index 9faf7dd8..b5991152 100644 --- a/xmr-btc/tests/harness/wallet/monero.rs +++ b/xmr-btc/tests/harness/wallet/monero.rs @@ -111,7 +111,10 @@ impl WatchForTransfer for BobWallet<'_> { Ok(proof) }) - .retry(ExponentialBackoff::default()) + .retry(ExponentialBackoff { + max_elapsed_time: None, + ..Default::default() + }) .await; if let Err(Error::InsufficientFunds { expected, actual }) = res { From ba3011a9c94713fc8ae410491b04a59bed2b4703 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Thu, 15 Oct 2020 18:34:55 +1100 Subject: [PATCH 04/52] Trigger refund if the publication of Monero TxLock takes too long --- xmr-btc/Cargo.toml | 1 + xmr-btc/src/lib.rs | 52 +++++++++++++++++++------ xmr-btc/tests/harness/wallet/bitcoin.rs | 21 +++++++++- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index d82790f5..0b6cf7e7 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -12,6 +12,7 @@ cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", curve25519-dalek = "2" ecdsa_fun = { version = "0.3.1", features = ["libsecp_compat"] } ed25519-dalek = "1.0.0-pre.4" # Cannot be 1 because they depend on curve25519-dalek version 3 +futures = "0.3" genawaiter = "0.99.1" miniscript = "1" monero = "0.9" diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index f69f1a7a..a028f85b 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -51,7 +51,9 @@ pub mod bob; pub mod monero; pub mod transport; +use async_trait::async_trait; use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; +use futures::future::Either; use genawaiter::sync::{Gen, GenBoxed}; use sha2::Sha256; @@ -75,6 +77,11 @@ pub trait ReceiveTransferProof { fn receive_transfer_proof(&self) -> monero::TransferProof; } +#[async_trait] +pub trait MedianTime { + async fn median_time(&self) -> u32; +} + /// Perform the on-chain protocol to swap monero and bitcoin as Bob. /// /// This is called post handshake, after all the keys, addresses and most of the @@ -104,16 +111,31 @@ pub fn action_generator_bob( where N: ReceiveTransferProof + Send + Sync, M: monero::WatchForTransfer + Send + Sync, - B: bitcoin::WatchForRawTransaction + Send + Sync, + B: MedianTime + bitcoin::WatchForRawTransaction + Send + Sync, { - enum SwapFailedRefund { + enum SwapFailed { + TimelockReached, InsufficientXMR(monero::InsufficientFunds), } + async fn poll_until_bitcoin_time(bitcoin_client: &B, timestamp: u32) + where + B: MedianTime, + { + loop { + if bitcoin_client.median_time().await >= timestamp { + return; + } + } + } + Gen::new_boxed(|co| async move { - let swap_result: Result<(), SwapFailedRefund> = async { + let swap_result: Result<(), SwapFailed> = async { co.yield_(Action::LockBitcoin(tx_lock.clone())).await; + let poll_until_expiry = poll_until_bitcoin_time(bitcoin_ledger, refund_timelock); + futures::pin_mut!(poll_until_expiry); + // the source of this could be the database, this layer doesn't care let transfer_proof = network.receive_transfer_proof(); @@ -122,12 +144,22 @@ where )); let S = S_a_monero + S_b_monero; - // TODO: We should require a specific number of confirmations on the lock - // transaction - monero_ledger - .watch_for_transfer(S, v.public(), transfer_proof, xmr, 10) - .await - .map_err(|e| SwapFailedRefund::InsufficientXMR(e))?; + match futures::future::select( + monero_ledger.watch_for_transfer( + S, + v.public(), + transfer_proof, + xmr, + monero::MIN_CONFIRMATIONS, + ), + poll_until_expiry, + ) + .await + { + Either::Left((Err(e), _)) => return Err(SwapFailed::InsufficientXMR(e)), + Either::Right(_) => return Err(SwapFailed::TimelockReached), + _ => {} + } let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address); let tx_redeem_encsig = b.encsign(S_a_bitcoin.clone(), tx_redeem.digest()); @@ -166,8 +198,6 @@ where } .await; - // NOTE: swap result should only be `Err` if we have reached the - // `refund_timelock`. Therefore, we should always yield the refund action if swap_result.is_err() { let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, A.clone(), b.public()); diff --git a/xmr-btc/tests/harness/wallet/bitcoin.rs b/xmr-btc/tests/harness/wallet/bitcoin.rs index 524baaa4..723cd8fb 100644 --- a/xmr-btc/tests/harness/wallet/bitcoin.rs +++ b/xmr-btc/tests/harness/wallet/bitcoin.rs @@ -1,12 +1,16 @@ use anyhow::Result; use async_trait::async_trait; +use backoff::{future::FutureOperation as _, ExponentialBackoff}; use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid}; 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, +use xmr_btc::{ + bitcoin::{ + BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxLock, WatchForRawTransaction, + }, + MedianTime, }; #[derive(Debug)] @@ -117,3 +121,16 @@ impl WatchForRawTransaction for Wallet { } } } + +#[async_trait] +impl MedianTime for Wallet { + async fn median_time(&self) -> u32 { + (|| async { Ok(self.0.median_time().await?) }) + .retry(ExponentialBackoff { + max_elapsed_time: None, + ..Default::default() + }) + .await + .expect("transient errors to be retried") + } +} From df4ffb65c962e44f1447d537f34e31825111ea2e Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Thu, 15 Oct 2020 21:17:42 +1100 Subject: [PATCH 05/52] Make bitcoin::WatchForRawTransaction infallible And trigger refund if Alice's redeem transaction takes too long. --- xmr-btc/src/alice.rs | 4 ++-- xmr-btc/src/bitcoin.rs | 2 +- xmr-btc/src/bob.rs | 2 +- xmr-btc/src/lib.rs | 25 +++++++++++++++++-------- xmr-btc/tests/harness/wallet/bitcoin.rs | 15 ++++++++------- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index 3cfe819a..4684c1a4 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -356,7 +356,7 @@ impl State3 { 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?; + .await; tracing::info!("tx lock seen with txid: {}", tx.txid()); @@ -554,7 +554,7 @@ impl State5 { let tx_refund_candidate = bitcoin_wallet .watch_for_raw_transaction(tx_refund.txid()) - .await?; + .await; let tx_refund_sig = tx_refund.extract_signature_by_key(tx_refund_candidate, self.a.public())?; diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index 43622cb6..291c6ce8 100644 --- a/xmr-btc/src/bitcoin.rs +++ b/xmr-btc/src/bitcoin.rs @@ -189,7 +189,7 @@ pub trait BroadcastSignedTransaction { #[async_trait] pub trait WatchForRawTransaction { - async fn watch_for_raw_transaction(&self, txid: Txid) -> Result; + async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction; } 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 2d4105d7..55099ed6 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -472,7 +472,7 @@ impl State4 { let tx_redeem_candidate = bitcoin_wallet .watch_for_raw_transaction(tx_redeem.txid()) - .await?; + .await; let tx_redeem_sig = tx_redeem.extract_signature_by_key(tx_redeem_candidate, self.b.public())?; diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index a028f85b..f2ec7d7e 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -53,7 +53,10 @@ pub mod transport; use async_trait::async_trait; use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; -use futures::future::Either; +use futures::{ + future::{select, Either}, + FutureExt, +}; use genawaiter::sync::{Gen, GenBoxed}; use sha2::Sha256; @@ -133,7 +136,8 @@ where let swap_result: Result<(), SwapFailed> = async { co.yield_(Action::LockBitcoin(tx_lock.clone())).await; - let poll_until_expiry = poll_until_bitcoin_time(bitcoin_ledger, refund_timelock); + let poll_until_expiry = + poll_until_bitcoin_time(bitcoin_ledger, refund_timelock).shared(); futures::pin_mut!(poll_until_expiry); // the source of this could be the database, this layer doesn't care @@ -144,7 +148,7 @@ where )); let S = S_a_monero + S_b_monero; - match futures::future::select( + match select( monero_ledger.watch_for_transfer( S, v.public(), @@ -152,7 +156,7 @@ where xmr, monero::MIN_CONFIRMATIONS, ), - poll_until_expiry, + poll_until_expiry.clone(), ) .await { @@ -167,10 +171,15 @@ where co.yield_(Action::SendBitcoinRedeemEncsig(tx_redeem_encsig.clone())) .await; - let tx_redeem_published = bitcoin_ledger - .watch_for_raw_transaction(tx_redeem.txid()) - .await - .expect("TODO: implementor of this trait must make it infallible by retrying"); + let tx_redeem_published = match select( + bitcoin_ledger.watch_for_raw_transaction(tx_redeem.txid()), + poll_until_expiry, + ) + .await + { + Either::Left((tx, _)) => tx, + Either::Right(_) => return Err(SwapFailed::TimelockReached), + }; // NOTE: If any of this fails, Bob will never be able to take the monero. // Therefore, there is no way to handle these errors other than aborting diff --git a/xmr-btc/tests/harness/wallet/bitcoin.rs b/xmr-btc/tests/harness/wallet/bitcoin.rs index 723cd8fb..d31990e8 100644 --- a/xmr-btc/tests/harness/wallet/bitcoin.rs +++ b/xmr-btc/tests/harness/wallet/bitcoin.rs @@ -112,13 +112,14 @@ impl BroadcastSignedTransaction for Wallet { #[async_trait] 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; - } + async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction { + (|| async { Ok(self.0.get_raw_transaction(txid).await?) }) + .retry(ExponentialBackoff { + max_elapsed_time: None, + ..Default::default() + }) + .await + .expect("transient errors to be retried") } } From f0ecc62748a98b559b120b10a1bb640bf999a17a Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Thu, 15 Oct 2020 21:48:08 +1100 Subject: [PATCH 06/52] Check expiry before emitting first action --- xmr-btc/src/lib.rs | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index f2ec7d7e..3d6e80dc 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -55,7 +55,7 @@ use async_trait::async_trait; use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; use futures::{ future::{select, Either}, - FutureExt, + Future, FutureExt, }; use genawaiter::sync::{Gen, GenBoxed}; use sha2::Sha256; @@ -121,24 +121,33 @@ where InsufficientXMR(monero::InsufficientFunds), } - async fn poll_until_bitcoin_time(bitcoin_client: &B, timestamp: u32) - where - B: MedianTime, - { + async fn poll_until(condition_future: impl Future + Clone) { loop { - if bitcoin_client.median_time().await >= timestamp { + if condition_future.clone().await { return; } } } + async fn bitcoin_time_is_gte(bitcoin_client: &B, timestamp: u32) -> bool + where + B: MedianTime, + { + bitcoin_client.median_time().await >= timestamp + } + Gen::new_boxed(|co| async move { let swap_result: Result<(), SwapFailed> = async { + let btc_has_expired = bitcoin_time_is_gte(bitcoin_ledger, refund_timelock).shared(); + + if btc_has_expired.clone().await { + return Err(SwapFailed::TimelockReached); + } + co.yield_(Action::LockBitcoin(tx_lock.clone())).await; - let poll_until_expiry = - poll_until_bitcoin_time(bitcoin_ledger, refund_timelock).shared(); - futures::pin_mut!(poll_until_expiry); + let poll_until_btc_has_expired = poll_until(btc_has_expired).shared(); + futures::pin_mut!(poll_until_btc_has_expired); // the source of this could be the database, this layer doesn't care let transfer_proof = network.receive_transfer_proof(); @@ -156,7 +165,7 @@ where xmr, monero::MIN_CONFIRMATIONS, ), - poll_until_expiry.clone(), + poll_until_btc_has_expired.clone(), ) .await { @@ -173,7 +182,7 @@ where let tx_redeem_published = match select( bitcoin_ledger.watch_for_raw_transaction(tx_redeem.txid()), - poll_until_expiry, + poll_until_btc_has_expired, ) .await { From 4723626fc0048b5218d37f836721e468cf8e9bdd Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Thu, 15 Oct 2020 21:53:55 +1100 Subject: [PATCH 07/52] Suppress compiler noise in tests folder By having two modules that declare and use the `harness` module we ran into a situation where not all parts of the `harness` module were being used by both of these other test modules. This was producing annoying warnings. For now this can be solved by marking the `harness` module declaration as public. This has no real effect since modules under `/tests` cannot be depended on elsewhere, but it satisfies the compiler. It does mean that we will not hear about unused functions, but this change should be temporary and it's a very minor problem given that it's just test code. --- xmr-btc/tests/e2e.rs | 4 ++-- xmr-btc/tests/on_chain.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/xmr-btc/tests/e2e.rs b/xmr-btc/tests/e2e.rs index fb2ddeec..d280f26d 100644 --- a/xmr-btc/tests/e2e.rs +++ b/xmr-btc/tests/e2e.rs @@ -1,3 +1,5 @@ +pub mod harness; + use crate::harness::wallet; use bitcoin_harness::Bitcoind; use harness::{ @@ -13,8 +15,6 @@ use tokio::sync::{ }; 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; diff --git a/xmr-btc/tests/on_chain.rs b/xmr-btc/tests/on_chain.rs index d8d9e59d..20e937b7 100644 --- a/xmr-btc/tests/on_chain.rs +++ b/xmr-btc/tests/on_chain.rs @@ -1,4 +1,4 @@ -mod harness; +pub mod harness; use anyhow::Result; use genawaiter::GeneratorState; From 05766d3146ad24ec18160d0de4f84bc0be4f3600 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Fri, 16 Oct 2020 09:14:39 +1100 Subject: [PATCH 08/52] Add swap/ Add a binary crate `swap` that implements two nodes (Alice and Bob). With this applied we can start up a node for each role and do: - Bob: Requests current amounts using BTC is input - Alice: Responds with amounts - Bob: (mock) get user input to Ok the amounts ... continue with swap (TODO) --- Cargo.toml | 2 +- swap/Cargo.toml | 32 ++++++ swap/src/alice.rs | 141 ++++++++++++++++++++++++ swap/src/alice/messenger.rs | 135 +++++++++++++++++++++++ swap/src/bob.rs | 157 +++++++++++++++++++++++++++ swap/src/bob/messenger.rs | 117 ++++++++++++++++++++ swap/src/cli.rs | 14 +++ swap/src/lib.rs | 95 ++++++++++++++++ swap/src/main.rs | 96 ++++++++++++++++ swap/src/network.rs | 18 +++ swap/src/network/peer_tracker.rs | 148 +++++++++++++++++++++++++ swap/src/network/request_response.rs | 109 +++++++++++++++++++ swap/src/network/transport.rs | 53 +++++++++ swap/src/trace.rs | 25 +++++ 14 files changed, 1141 insertions(+), 1 deletion(-) create mode 100644 swap/Cargo.toml create mode 100644 swap/src/alice.rs create mode 100644 swap/src/alice/messenger.rs create mode 100644 swap/src/bob.rs create mode 100644 swap/src/bob/messenger.rs create mode 100644 swap/src/cli.rs create mode 100644 swap/src/lib.rs create mode 100644 swap/src/main.rs create mode 100644 swap/src/network.rs create mode 100644 swap/src/network/peer_tracker.rs create mode 100644 swap/src/network/request_response.rs create mode 100644 swap/src/network/transport.rs create mode 100644 swap/src/trace.rs diff --git a/Cargo.toml b/Cargo.toml index e0174381..9d880136 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["monero-harness", "xmr-btc"] +members = ["monero-harness", "xmr-btc", "swap"] diff --git a/swap/Cargo.toml b/swap/Cargo.toml new file mode 100644 index 00000000..cb22e3d1 --- /dev/null +++ b/swap/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "swap" +version = "0.1.0" +authors = ["CoBloX developers "] +edition = "2018" +description = "XMR/BTC trustless atomic swaps." + +[dependencies] +anyhow = "1" +async-trait = "0.1" +atty = "0.2" +bitcoin = "0.25" # TODO: Upgrade other crates in this repo to use this version. +derivative = "2" +futures = { version = "0.3", default-features = false } +libp2p = { version = "0.28", default-features = false, features = ["tcp-tokio", "yamux", "mplex", "dns", "noise", "request-response"] } +libp2p-tokio-socks5 = "0.3" +log = { version = "0.4", features = ["serde"] } +monero = "0.9" +rand = "0.7" +serde = { version = "1", features = ["derive"] } +serde_derive = "1.0" +serde_json = "1" +structopt = "0.3" +time = "0.2" +tokio = { version = "0.2", features = ["rt-threaded", "time", "macros", "sync"] } +tracing = { version = "0.1", features = ["attributes"] } +tracing-core = "0.1" +tracing-futures = { version = "0.2", features = ["std-future", "futures-03"] } +tracing-log = "0.1" +tracing-subscriber = { version = "0.2", default-features = false, features = ["fmt", "ansi", "env-filter"] } +void = "1" +xmr-btc = { path = "../xmr-btc" } \ No newline at end of file diff --git a/swap/src/alice.rs b/swap/src/alice.rs new file mode 100644 index 00000000..5addb61f --- /dev/null +++ b/swap/src/alice.rs @@ -0,0 +1,141 @@ +//! Run an XMR/BTC swap in the role of Alice. +//! Alice holds XMR and wishes receive BTC. +use anyhow::Result; +use libp2p::{ + core::{identity::Keypair, Multiaddr}, + request_response::ResponseChannel, + NetworkBehaviour, PeerId, +}; +use std::{thread, time::Duration}; +use tracing::{debug, warn}; + +mod messenger; + +use self::messenger::*; +use crate::{ + monero, + network::{ + peer_tracker::{self, PeerTracker}, + request_response::{AliceToBob, TIMEOUT}, + transport, TokioExecutor, + }, + Never, SwapParams, +}; + +pub type Swarm = libp2p::Swarm; + +pub async fn swap(listen: Multiaddr) -> Result<()> { + let mut swarm = new_swarm(listen)?; + + match swarm.next().await { + BehaviourOutEvent::Request(messenger::BehaviourOutEvent::Btc { btc, channel }) => { + debug!("Got request from Bob"); + let params = SwapParams { + btc, + // TODO: Do a real calculation. + xmr: monero::Amount::from_piconero(10), + }; + + let msg = AliceToBob::Amounts(params); + swarm.send(channel, msg); + } + other => panic!("unexpected event: {:?}", other), + } + + warn!("parking thread ..."); + thread::park(); + Ok(()) +} + +fn new_swarm(listen: Multiaddr) -> Result { + use anyhow::Context as _; + + let behaviour = Alice::default(); + + let local_key_pair = behaviour.identity(); + let local_peer_id = behaviour.peer_id(); + + let transport = transport::build(local_key_pair)?; + + let mut swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone()) + .executor(Box::new(TokioExecutor { + handle: tokio::runtime::Handle::current(), + })) + .build(); + + Swarm::listen_on(&mut swarm, listen.clone()) + .with_context(|| format!("Address is not supported: {:#}", listen))?; + + tracing::info!("Initialized swarm: {}", local_peer_id); + + Ok(swarm) +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum BehaviourOutEvent { + Request(messenger::BehaviourOutEvent), + ConnectionEstablished(PeerId), + Never, // FIXME: Why do we need this? +} + +impl From for BehaviourOutEvent { + fn from(_: Never) -> Self { + BehaviourOutEvent::Never + } +} + +impl From for BehaviourOutEvent { + fn from(event: messenger::BehaviourOutEvent) -> Self { + BehaviourOutEvent::Request(event) + } +} + +impl From for BehaviourOutEvent { + fn from(event: peer_tracker::BehaviourOutEvent) -> Self { + match event { + peer_tracker::BehaviourOutEvent::ConnectionEstablished(id) => { + BehaviourOutEvent::ConnectionEstablished(id) + } + } + } +} + +/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice. +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "BehaviourOutEvent", event_process = false)] +#[allow(missing_debug_implementations)] +pub struct Alice { + net: Messenger, + pt: PeerTracker, + #[behaviour(ignore)] + identity: Keypair, +} + +impl Alice { + pub fn identity(&self) -> Keypair { + self.identity.clone() + } + + pub fn peer_id(&self) -> PeerId { + PeerId::from(self.identity.public()) + } + + /// Alice always sends her messages as a response to a request from Bob. + pub fn send(&mut self, channel: ResponseChannel, msg: AliceToBob) { + self.net.send(channel, msg); + } +} + +impl Default for Alice { + fn default() -> Self { + let identity = Keypair::generate_ed25519(); + let timeout = Duration::from_secs(TIMEOUT); + + Self { + net: Messenger::new(timeout), + pt: PeerTracker::default(), + identity, + } + } +} diff --git a/swap/src/alice/messenger.rs b/swap/src/alice/messenger.rs new file mode 100644 index 00000000..62355006 --- /dev/null +++ b/swap/src/alice/messenger.rs @@ -0,0 +1,135 @@ +use anyhow::Result; +use libp2p::{ + request_response::{ + handler::RequestProtocol, ProtocolSupport, RequestId, RequestResponse, + RequestResponseConfig, RequestResponseEvent, RequestResponseMessage, ResponseChannel, + }, + swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters}, + NetworkBehaviour, PeerId, +}; +use std::{ + collections::VecDeque, + task::{Context, Poll}, + time::Duration, +}; +use tracing::{debug, error}; + +use crate::{ + bitcoin, + network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}, + Never, +}; + +#[derive(Debug)] +pub enum BehaviourOutEvent { + Btc { + btc: bitcoin::Amount, + channel: ResponseChannel, + }, +} + +/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob. +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "BehaviourOutEvent", poll_method = "poll")] +#[allow(missing_debug_implementations)] +pub struct Messenger { + rr: RequestResponse, + #[behaviour(ignore)] + events: VecDeque, +} + +impl Messenger { + pub fn new(timeout: Duration) -> Self { + let mut config = RequestResponseConfig::default(); + config.set_request_timeout(timeout); + + Self { + rr: RequestResponse::new( + Codec::default(), + vec![(Protocol, ProtocolSupport::Full)], + config, + ), + events: Default::default(), + } + } + + /// Alice always sends her messages as a response to a request from Bob. + pub fn send(&mut self, channel: ResponseChannel, msg: AliceToBob) { + self.rr.send_response(channel, msg); + } + + pub async fn request_amounts( + &mut self, + alice: PeerId, + btc: bitcoin::Amount, + ) -> Result { + let msg = BobToAlice::AmountsFromBtc(btc); + let id = self.rr.send_request(&alice, msg); + debug!("Request sent to: {}", alice); + + Ok(id) + } + + fn poll( + &mut self, + _: &mut Context<'_>, + _: &mut impl PollParameters, + ) -> Poll, BehaviourOutEvent>> { + if let Some(event) = self.events.pop_front() { + return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event)); + } + + Poll::Pending + } +} + +impl NetworkBehaviourEventProcess> for Messenger { + fn inject_event(&mut self, event: RequestResponseEvent) { + match event { + RequestResponseEvent::Message { + peer: _, + message: + RequestResponseMessage::Request { + request, + request_id: _, + channel, + }, + } => match request { + BobToAlice::AmountsFromBtc(btc) => self + .events + .push_back(BehaviourOutEvent::Btc { btc, channel }), + _ => panic!("unexpected request"), + }, + RequestResponseEvent::Message { + peer: _, + message: + RequestResponseMessage::Response { + response: _, + request_id: _, + }, + } => panic!("unexpected response"), + RequestResponseEvent::InboundFailure { + peer: _, + request_id: _, + error, + } => { + error!("Inbound failure: {:?}", error); + } + RequestResponseEvent::OutboundFailure { + peer: _, + request_id: _, + error, + } => { + error!("Outbound failure: {:?}", error); + } + } + } +} + +impl libp2p::swarm::NetworkBehaviourEventProcess<()> for Messenger { + fn inject_event(&mut self, _event: ()) {} +} + +impl libp2p::swarm::NetworkBehaviourEventProcess for Messenger { + fn inject_event(&mut self, _: Never) {} +} diff --git a/swap/src/bob.rs b/swap/src/bob.rs new file mode 100644 index 00000000..33e4520b --- /dev/null +++ b/swap/src/bob.rs @@ -0,0 +1,157 @@ +//! Run an XMR/BTC swap in the role of Bob. +//! Bob holds BTC and wishes receive XMR. +use anyhow::Result; +use futures::{ + channel::mpsc::{Receiver, Sender}, + StreamExt, +}; +use libp2p::{core::identity::Keypair, Multiaddr, NetworkBehaviour, PeerId}; +use std::{process, thread, time::Duration}; +use tracing::{debug, info, warn}; + +mod messenger; + +use self::messenger::*; +use crate::{ + bitcoin, + network::{ + peer_tracker::{self, PeerTracker}, + request_response::TIMEOUT, + transport, TokioExecutor, + }, + Cmd, Never, Rsp, +}; + +pub async fn swap( + btc: u64, + addr: Multiaddr, + mut cmd_tx: Sender, + mut rsp_rx: Receiver, +) -> Result<()> { + let mut swarm = new_swarm()?; + + libp2p::Swarm::dial_addr(&mut swarm, addr)?; + let id = match swarm.next().await { + BehaviourOutEvent::ConnectionEstablished(id) => id, + other => panic!("unexpected event: {:?}", other), + }; + info!("Connection established."); + + swarm.request_amounts(id, btc).await; + + match swarm.next().await { + BehaviourOutEvent::Response(messenger::BehaviourOutEvent::Amounts(p)) => { + debug!("Got response from Alice: {:?}", p); + let cmd = Cmd::VerifyAmounts(p); + cmd_tx.try_send(cmd)?; + let response = rsp_rx.next().await; + if response == Some(Rsp::Abort) { + info!("Amounts no good, aborting ..."); + process::exit(0); + } + info!("User verified amounts, continuing with swap ..."); + } + other => panic!("unexpected event: {:?}", other), + } + + warn!("parking thread ..."); + thread::park(); + Ok(()) +} + +pub type Swarm = libp2p::Swarm; + +fn new_swarm() -> Result { + let behaviour = Bob::default(); + + let local_key_pair = behaviour.identity(); + let local_peer_id = behaviour.peer_id(); + + let transport = transport::build(local_key_pair)?; + + let swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone()) + .executor(Box::new(TokioExecutor { + handle: tokio::runtime::Handle::current(), + })) + .build(); + + info!("Initialized swarm with identity {}", local_peer_id); + + Ok(swarm) +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum BehaviourOutEvent { + Response(messenger::BehaviourOutEvent), + ConnectionEstablished(PeerId), + Never, // FIXME: Why do we need this? +} + +impl From for BehaviourOutEvent { + fn from(_: Never) -> Self { + BehaviourOutEvent::Never + } +} + +impl From for BehaviourOutEvent { + fn from(event: messenger::BehaviourOutEvent) -> Self { + BehaviourOutEvent::Response(event) + } +} + +impl From for BehaviourOutEvent { + fn from(event: peer_tracker::BehaviourOutEvent) -> Self { + match event { + peer_tracker::BehaviourOutEvent::ConnectionEstablished(id) => { + BehaviourOutEvent::ConnectionEstablished(id) + } + } + } +} + +/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob. +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "BehaviourOutEvent", event_process = false)] +#[allow(missing_debug_implementations)] +pub struct Bob { + net: Messenger, + pt: PeerTracker, + #[behaviour(ignore)] + identity: Keypair, +} + +impl Bob { + pub fn identity(&self) -> Keypair { + self.identity.clone() + } + + pub fn peer_id(&self) -> PeerId { + PeerId::from(self.identity.public()) + } + + /// Sends a message to Alice to get current amounts based on `btc`. + pub async fn request_amounts(&mut self, alice: PeerId, btc: u64) { + let btc = bitcoin::Amount::from_sat(btc); + let _id = self.net.request_amounts(alice.clone(), btc).await; + debug!("Requesting amounts from: {}", alice); + } + + /// Returns Alice's peer id if we are connected. + pub fn peer_id_of_alice(&self) -> Option { + self.pt.counterparty() + } +} + +impl Default for Bob { + fn default() -> Bob { + let identity = Keypair::generate_ed25519(); + let timeout = Duration::from_secs(TIMEOUT); + + Self { + net: Messenger::new(timeout), + pt: PeerTracker::default(), + identity, + } + } +} diff --git a/swap/src/bob/messenger.rs b/swap/src/bob/messenger.rs new file mode 100644 index 00000000..e153addc --- /dev/null +++ b/swap/src/bob/messenger.rs @@ -0,0 +1,117 @@ +use anyhow::Result; +use libp2p::{ + request_response::{ + handler::RequestProtocol, ProtocolSupport, RequestId, RequestResponse, + RequestResponseConfig, RequestResponseEvent, RequestResponseMessage, + }, + swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters}, + NetworkBehaviour, PeerId, +}; +use std::{ + collections::VecDeque, + task::{Context, Poll}, + time::Duration, +}; +use tracing::{debug, error}; + +use crate::{ + bitcoin, + network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}, + Never, SwapParams, +}; + +#[derive(Debug)] +pub enum BehaviourOutEvent { + Amounts(SwapParams), +} + +/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob. +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "BehaviourOutEvent", poll_method = "poll")] +#[allow(missing_debug_implementations)] +pub struct Messenger { + rr: RequestResponse, + #[behaviour(ignore)] + events: VecDeque, +} + +impl Messenger { + pub fn new(timeout: Duration) -> Self { + let mut config = RequestResponseConfig::default(); + config.set_request_timeout(timeout); + + Self { + rr: RequestResponse::new( + Codec::default(), + vec![(Protocol, ProtocolSupport::Full)], + config, + ), + events: Default::default(), + } + } + + pub async fn request_amounts( + &mut self, + alice: PeerId, + btc: bitcoin::Amount, + ) -> Result { + debug!("Sending request ..."); + let msg = BobToAlice::AmountsFromBtc(btc); + let id = self.rr.send_request(&alice, msg); + debug!("Sent."); + + Ok(id) + } + + fn poll( + &mut self, + _: &mut Context<'_>, + _: &mut impl PollParameters, + ) -> Poll, BehaviourOutEvent>> { + if let Some(event) = self.events.pop_front() { + return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event)); + } + + Poll::Pending + } +} + +impl NetworkBehaviourEventProcess> for Messenger { + fn inject_event(&mut self, event: RequestResponseEvent) { + match event { + RequestResponseEvent::Message { + peer: _, + message: RequestResponseMessage::Request { .. }, + } => panic!("Bob should never get a request from Alice"), + RequestResponseEvent::Message { + peer: _, + message: + RequestResponseMessage::Response { + response, + request_id: _, + }, + } => match response { + AliceToBob::Amounts(p) => self.events.push_back(BehaviourOutEvent::Amounts(p)), + }, + + RequestResponseEvent::InboundFailure { .. } => { + panic!("Bob should never get a request from Alice, so should never get an InboundFailure"); + } + RequestResponseEvent::OutboundFailure { + peer: _, + request_id: _, + error, + } => { + error!("Outbound failure: {:?}", error); + } + } + } +} + +impl libp2p::swarm::NetworkBehaviourEventProcess<()> for Messenger { + fn inject_event(&mut self, _event: ()) {} +} + +impl libp2p::swarm::NetworkBehaviourEventProcess for Messenger { + fn inject_event(&mut self, _: Never) {} +} diff --git a/swap/src/cli.rs b/swap/src/cli.rs new file mode 100644 index 00000000..fd4f9a59 --- /dev/null +++ b/swap/src/cli.rs @@ -0,0 +1,14 @@ +#[derive(structopt::StructOpt, Debug)] +pub struct Options { + /// Run the swap as Alice. + #[structopt(long = "as-alice")] + pub as_alice: bool, + + /// Run the swap as Bob and try to swap this many XMR (in piconero). + #[structopt(long = "picos")] + pub piconeros: Option, + + /// Run the swap as Bob and try to swap this many BTC (in satoshi). + #[structopt(long = "sats")] + pub satoshis: Option, +} diff --git a/swap/src/lib.rs b/swap/src/lib.rs new file mode 100644 index 00000000..c5c22421 --- /dev/null +++ b/swap/src/lib.rs @@ -0,0 +1,95 @@ +use serde::{Deserialize, Serialize}; + +pub mod alice; +pub mod bob; +pub mod network; + +pub const ONE_BTC: u64 = 100_000_000; + +pub type Never = std::convert::Infallible; + +/// Commands sent from Bob to the main task. +#[derive(Debug)] +pub enum Cmd { + VerifyAmounts(SwapParams), +} + +/// Responses send from the main task back to Bob. +#[derive(Debug, PartialEq)] +pub enum Rsp { + Verified, + Abort, +} + +/// XMR/BTC swap parameters. +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub struct SwapParams { + /// Amount of BTC to swap. + pub btc: bitcoin::Amount, + /// Amount of XMR to swap. + pub xmr: monero::Amount, +} + +// FIXME: Amount modules are a quick hack so we can derive serde. + +pub mod monero { + use serde::{Deserialize, Serialize}; + use std::fmt; + + #[derive(Copy, Clone, Debug, Serialize, Deserialize)] + pub struct Amount(u64); + + impl Amount { + /// Create an [Amount] with piconero precision and the given number of + /// piconeros. + /// + /// A piconero (a.k.a atomic unit) is equal to 1e-12 XMR. + pub fn from_piconero(amount: u64) -> Self { + Amount(amount) + } + pub fn as_piconero(&self) -> u64 { + self.0 + } + } + + impl From for u64 { + fn from(from: Amount) -> u64 { + from.0 + } + } + + impl fmt::Display for Amount { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} piconeros", self.0) + } + } +} + +pub mod bitcoin { + use serde::{Deserialize, Serialize}; + use std::fmt; + + #[derive(Copy, Clone, Debug, Serialize, Deserialize)] + pub struct Amount(u64); + + impl Amount { + /// The zero amount. + pub const ZERO: Amount = Amount(0); + /// Exactly one satoshi. + pub const ONE_SAT: Amount = Amount(1); + /// Exactly one bitcoin. + pub const ONE_BTC: Amount = Amount(100_000_000); + + /// Create an [Amount] with satoshi precision and the given number of + /// satoshis. + pub fn from_sat(satoshi: u64) -> Amount { + Amount(satoshi) + } + } + + impl fmt::Display for Amount { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} satoshis", self.0) + } + } +} diff --git a/swap/src/main.rs b/swap/src/main.rs new file mode 100644 index 00000000..b95e5ba2 --- /dev/null +++ b/swap/src/main.rs @@ -0,0 +1,96 @@ +#![warn( + unused_extern_crates, + missing_debug_implementations, + missing_copy_implementations, + rust_2018_idioms, + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::fallible_impl_from, + clippy::cast_precision_loss, + clippy::cast_possible_wrap, + clippy::dbg_macro +)] +#![forbid(unsafe_code)] + +use anyhow::{bail, Result}; +use futures::{channel::mpsc, StreamExt}; +use libp2p::Multiaddr; +use log::LevelFilter; +use structopt::StructOpt; +use tracing::info; + +mod cli; +mod trace; + +use cli::Options; +use swap::{alice, bob, Cmd, Rsp, SwapParams}; + +// TODO: Add root seed file instead of generating new seed each run. + +// Alice's address and port until we have a config file. +pub const PORT: u16 = 9876; // Arbitrarily chosen. +pub const ADDR: &str = "127.0.0.1"; + +#[tokio::main] +async fn main() -> Result<()> { + let opt = Options::from_args(); + + trace::init_tracing(LevelFilter::Debug)?; + + let addr = format!("/ip4/{}/tcp/{}", ADDR, PORT); + let alice_addr: Multiaddr = addr.parse().expect("failed to parse Alice's address"); + + if opt.as_alice { + info!("running swap node as Alice ..."); + + if opt.piconeros.is_some() || opt.satoshis.is_some() { + bail!("Alice cannot set the amount to swap via the cli"); + } + + swap_as_alice(alice_addr).await?; + } else { + info!("running swap node as Bob ..."); + + match (opt.piconeros, opt.satoshis) { + (Some(_), Some(_)) => bail!("Please supply only a single amount to swap"), + (None, None) => bail!("Please supply an amount to swap"), + (Some(_picos), _) => todo!("support starting with picos"), + (None, Some(sats)) => { + swap_as_bob(sats, alice_addr).await?; + } + }; + } + + Ok(()) +} + +async fn swap_as_alice(addr: Multiaddr) -> Result<()> { + alice::swap(addr).await +} + +async fn swap_as_bob(sats: u64, addr: Multiaddr) -> Result<()> { + let (cmd_tx, mut cmd_rx) = mpsc::channel(1); + let (mut rsp_tx, rsp_rx) = mpsc::channel(1); + tokio::spawn(bob::swap(sats, addr, cmd_tx, rsp_rx)); + loop { + let read = cmd_rx.next().await; + match read { + Some(cmd) => match cmd { + Cmd::VerifyAmounts(p) => { + if verified(p) { + rsp_tx.try_send(Rsp::Verified)?; + } + } + }, + None => { + info!("Channel closed from other end"); + return Ok(()); + } + } + } +} + +fn verified(_p: SwapParams) -> bool { + // TODO: Read input from the shell. + true +} diff --git a/swap/src/network.rs b/swap/src/network.rs new file mode 100644 index 00000000..90bf44de --- /dev/null +++ b/swap/src/network.rs @@ -0,0 +1,18 @@ +use futures::prelude::*; +use libp2p::core::Executor; +use std::pin::Pin; +use tokio::runtime::Handle; + +pub mod peer_tracker; +pub mod request_response; +pub mod transport; + +pub struct TokioExecutor { + pub handle: Handle, +} + +impl Executor for TokioExecutor { + fn exec(&self, future: Pin + Send>>) { + let _ = self.handle.spawn(future); + } +} diff --git a/swap/src/network/peer_tracker.rs b/swap/src/network/peer_tracker.rs new file mode 100644 index 00000000..7b76563f --- /dev/null +++ b/swap/src/network/peer_tracker.rs @@ -0,0 +1,148 @@ +use futures::task::Context; +use libp2p::{ + core::{connection::ConnectionId, ConnectedPoint}, + swarm::{ + protocols_handler::DummyProtocolsHandler, NetworkBehaviour, NetworkBehaviourAction, + PollParameters, + }, + Multiaddr, PeerId, +}; +use std::{ + collections::{hash_map::Entry, HashMap, VecDeque}, + task::Poll, +}; + +#[derive(Debug)] +pub enum BehaviourOutEvent { + ConnectionEstablished(PeerId), +} + +/// A NetworkBehaviour that tracks connections to other peers. +#[derive(Default, Debug)] +pub struct PeerTracker { + connected_peers: HashMap>, + address_hints: HashMap>, + events: VecDeque, +} + +impl PeerTracker { + /// Returns an arbitrary connected counterparty. + /// This is useful if we are connected to a single other node. + pub fn counterparty(&self) -> Option { + // TODO: Refactor to use combinators. + if let Some((id, _)) = self.connected_peers().next() { + return Some(id); + } + None + } + + pub fn connected_peers(&self) -> impl Iterator)> { + self.connected_peers.clone().into_iter() + } + + /// Adds an address hint for the given peer id. The added address is + /// considered most recent and hence is added at the start of the list + /// because libp2p tries to connect with the first address first. + pub fn add_recent_address_hint(&mut self, id: PeerId, addr: Multiaddr) { + let old_addresses = self.address_hints.get_mut(&id); + + match old_addresses { + None => { + let mut hints = VecDeque::new(); + hints.push_back(addr); + self.address_hints.insert(id, hints); + } + Some(hints) => { + hints.push_front(addr); + } + } + } +} + +impl NetworkBehaviour for PeerTracker { + type ProtocolsHandler = DummyProtocolsHandler; + type OutEvent = BehaviourOutEvent; + + fn new_handler(&mut self) -> Self::ProtocolsHandler { + DummyProtocolsHandler::default() + } + + /// Note (from libp2p doc): + /// The addresses will be tried in the order returned by this function, + /// which means that they should be ordered by decreasing likelihood of + /// reachability. In other words, the first address should be the most + /// likely to be reachable. + fn addresses_of_peer(&mut self, peer: &PeerId) -> Vec { + let mut addresses: Vec = vec![]; + + // If we are connected then this address is most likely to be valid + if let Some(connected) = self.connected_peers.get(peer) { + for addr in connected.iter() { + addresses.push(addr.clone()) + } + } + + if let Some(hints) = self.address_hints.get(peer) { + for hint in hints { + addresses.push(hint.clone()); + } + } + + addresses + } + + fn inject_connected(&mut self, _: &PeerId) {} + + fn inject_disconnected(&mut self, _: &PeerId) {} + + fn inject_connection_established( + &mut self, + peer: &PeerId, + _: &ConnectionId, + point: &ConnectedPoint, + ) { + if let ConnectedPoint::Dialer { address } = point { + self.connected_peers + .entry(peer.clone()) + .or_default() + .push(address.clone()); + + self.events + .push_back(BehaviourOutEvent::ConnectionEstablished(peer.clone())); + } + } + + fn inject_connection_closed( + &mut self, + peer: &PeerId, + _: &ConnectionId, + point: &ConnectedPoint, + ) { + if let ConnectedPoint::Dialer { address } = point { + match self.connected_peers.entry(peer.clone()) { + Entry::Vacant(_) => {} + Entry::Occupied(mut entry) => { + let addresses = entry.get_mut(); + + if let Some(pos) = addresses.iter().position(|a| a == address) { + addresses.remove(pos); + } + } + } + } + } + + fn inject_event(&mut self, _: PeerId, _: ConnectionId, _: void::Void) {} + + fn poll( + &mut self, + _: &mut Context<'_>, + _: &mut impl PollParameters, + ) -> Poll> { + if let Some(event) = self.events.pop_front() { + return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event)); + } + + Poll::Pending + } +} diff --git a/swap/src/network/request_response.rs b/swap/src/network/request_response.rs new file mode 100644 index 00000000..c986e2a9 --- /dev/null +++ b/swap/src/network/request_response.rs @@ -0,0 +1,109 @@ +use async_trait::async_trait; +use futures::prelude::*; +use libp2p::{ + core::upgrade, + request_response::{ProtocolName, RequestResponseCodec}, +}; +use serde::{Deserialize, Serialize}; +use std::{fmt::Debug, io}; + +use crate::{bitcoin, monero, SwapParams}; + +/// Time to wait for a response back once we send a request. +pub const TIMEOUT: u64 = 3600; // One hour. + +/// Messages Bob sends to Alice. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum BobToAlice { + AmountsFromBtc(bitcoin::Amount), + AmountsFromXmr(monero::Amount), + /* TODO: How are we going to do this when the messages are not Clone? + * Msg(bob::Message), */ +} + +/// Messages Alice sends to Bob. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum AliceToBob { + Amounts(SwapParams), + /* TODO: How are we going to do this when the messages are not Clone? + * Msg(alice::Message) */ +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct Protocol; + +impl ProtocolName for Protocol { + fn protocol_name(&self) -> &[u8] { + b"/xmr/btc/1.0.0" + } +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Codec; + +#[async_trait] +impl RequestResponseCodec for Codec { + type Protocol = Protocol; + type Request = BobToAlice; + type Response = AliceToBob; + + async fn read_request(&mut self, _: &Self::Protocol, io: &mut T) -> io::Result + where + T: AsyncRead + Unpin + Send, + { + let message = upgrade::read_one(io, 1024) + .await + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + let mut de = serde_json::Deserializer::from_slice(&message); + let msg = BobToAlice::deserialize(&mut de)?; + + Ok(msg) + } + + async fn read_response( + &mut self, + _: &Self::Protocol, + io: &mut T, + ) -> io::Result + where + T: AsyncRead + Unpin + Send, + { + let message = upgrade::read_one(io, 1024) + .await + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + let mut de = serde_json::Deserializer::from_slice(&message); + let msg = AliceToBob::deserialize(&mut de)?; + + Ok(msg) + } + + async fn write_request( + &mut self, + _: &Self::Protocol, + io: &mut T, + req: Self::Request, + ) -> io::Result<()> + where + T: AsyncWrite + Unpin + Send, + { + let bytes = serde_json::to_vec(&req)?; + upgrade::write_one(io, &bytes).await?; + + Ok(()) + } + + async fn write_response( + &mut self, + _: &Self::Protocol, + io: &mut T, + res: Self::Response, + ) -> io::Result<()> + where + T: AsyncWrite + Unpin + Send, + { + let bytes = serde_json::to_vec(&res)?; + upgrade::write_one(io, &bytes).await?; + + Ok(()) + } +} diff --git a/swap/src/network/transport.rs b/swap/src/network/transport.rs new file mode 100644 index 00000000..60d9b3ce --- /dev/null +++ b/swap/src/network/transport.rs @@ -0,0 +1,53 @@ +use anyhow::Result; +use libp2p::{ + core::{ + either::EitherError, + identity, + muxing::StreamMuxerBox, + transport::{boxed::Boxed, timeout::TransportTimeoutError}, + upgrade::{SelectUpgrade, Version}, + Transport, UpgradeError, + }, + dns::{DnsConfig, DnsErr}, + mplex::MplexConfig, + noise::{self, NoiseConfig, NoiseError, X25519Spec}, + tcp::TokioTcpConfig, + yamux, PeerId, +}; +use std::{io, time::Duration}; + +/// Builds a libp2p transport with the following features: +/// - TcpConnection +/// - DNS name resolution +/// - authentication via noise +/// - multiplexing via yamux or mplex +pub fn build(id_keys: identity::Keypair) -> Result { + let dh_keys = noise::Keypair::::new().into_authentic(&id_keys)?; + let noise = NoiseConfig::xx(dh_keys).into_authenticated(); + + let tcp = TokioTcpConfig::new().nodelay(true); + let dns = DnsConfig::new(tcp)?; + + let transport = dns + .upgrade(Version::V1) + .authenticate(noise) + .multiplex(SelectUpgrade::new( + yamux::Config::default(), + MplexConfig::new(), + )) + .map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer))) + .timeout(Duration::from_secs(20)) + .boxed(); + + Ok(transport) +} + +pub type SwapTransport = Boxed< + (PeerId, StreamMuxerBox), + TransportTimeoutError< + EitherError< + EitherError, UpgradeError>, + UpgradeError>, + >, + >, +>; diff --git a/swap/src/trace.rs b/swap/src/trace.rs new file mode 100644 index 00000000..14854cd4 --- /dev/null +++ b/swap/src/trace.rs @@ -0,0 +1,25 @@ +use atty::{self, Stream}; +use log::LevelFilter; +use tracing::{info, subscriber}; +use tracing_log::LogTracer; +use tracing_subscriber::FmtSubscriber; + +pub fn init_tracing(level: log::LevelFilter) -> anyhow::Result<()> { + if level == LevelFilter::Off { + return Ok(()); + } + + // Upstream log filter. + LogTracer::init_with_filter(LevelFilter::Debug)?; + + let is_terminal = atty::is(Stream::Stdout); + let subscriber = FmtSubscriber::builder() + .with_env_filter(format!("swap={}", level)) + .with_ansi(is_terminal) + .finish(); + + subscriber::set_global_default(subscriber)?; + info!("Initialized tracing with level: {}", level); + + Ok(()) +} From 3492c46e7131209db511d3fb2205b28851dd0c67 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Fri, 16 Oct 2020 10:43:32 +1100 Subject: [PATCH 09/52] Verify amounts with user --- swap/src/alice.rs | 34 ++++++++++++++++------------------ swap/src/bob/messenger.rs | 4 +--- swap/src/lib.rs | 11 +++++++++-- swap/src/main.rs | 36 ++++++++++++++++++++++++++++++------ 4 files changed, 56 insertions(+), 29 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 5addb61f..80ddc09c 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -6,8 +6,8 @@ use libp2p::{ request_response::ResponseChannel, NetworkBehaviour, PeerId, }; -use std::{thread, time::Duration}; -use tracing::{debug, warn}; +use std::time::Duration; +use tracing::debug; mod messenger; @@ -27,24 +27,22 @@ pub type Swarm = libp2p::Swarm; pub async fn swap(listen: Multiaddr) -> Result<()> { let mut swarm = new_swarm(listen)?; - match swarm.next().await { - BehaviourOutEvent::Request(messenger::BehaviourOutEvent::Btc { btc, channel }) => { - debug!("Got request from Bob"); - let params = SwapParams { - btc, - // TODO: Do a real calculation. - xmr: monero::Amount::from_piconero(10), - }; - - let msg = AliceToBob::Amounts(params); - swarm.send(channel, msg); + loop { + match swarm.next().await { + BehaviourOutEvent::Request(messenger::BehaviourOutEvent::Btc { btc, channel }) => { + debug!("Got request from Bob to swap {}", btc); + let params = SwapParams { + btc, + // TODO: Do a real calculation. + xmr: monero::Amount::from_piconero(10), + }; + + let msg = AliceToBob::Amounts(params); + swarm.send(channel, msg); + } + other => panic!("unexpected event: {:?}", other), } - other => panic!("unexpected event: {:?}", other), } - - warn!("parking thread ..."); - thread::park(); - Ok(()) } fn new_swarm(listen: Multiaddr) -> Result { diff --git a/swap/src/bob/messenger.rs b/swap/src/bob/messenger.rs index e153addc..6d57804e 100644 --- a/swap/src/bob/messenger.rs +++ b/swap/src/bob/messenger.rs @@ -12,7 +12,7 @@ use std::{ task::{Context, Poll}, time::Duration, }; -use tracing::{debug, error}; +use tracing::error; use crate::{ bitcoin, @@ -55,10 +55,8 @@ impl Messenger { alice: PeerId, btc: bitcoin::Amount, ) -> Result { - debug!("Sending request ..."); let msg = BobToAlice::AmountsFromBtc(btc); let id = self.rr.send_request(&alice, msg); - debug!("Sent."); Ok(id) } diff --git a/swap/src/lib.rs b/swap/src/lib.rs index c5c22421..f9ba9a32 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; pub mod alice; pub mod bob; @@ -9,13 +10,13 @@ pub const ONE_BTC: u64 = 100_000_000; pub type Never = std::convert::Infallible; /// Commands sent from Bob to the main task. -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub enum Cmd { VerifyAmounts(SwapParams), } /// Responses send from the main task back to Bob. -#[derive(Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum Rsp { Verified, Abort, @@ -30,6 +31,12 @@ pub struct SwapParams { pub xmr: monero::Amount, } +impl Display for SwapParams { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} for {}", self.btc, self.xmr) + } +} + // FIXME: Amount modules are a quick hack so we can derive serde. pub mod monero { diff --git a/swap/src/main.rs b/swap/src/main.rs index b95e5ba2..fc2997ee 100644 --- a/swap/src/main.rs +++ b/swap/src/main.rs @@ -16,9 +16,9 @@ use anyhow::{bail, Result}; use futures::{channel::mpsc, StreamExt}; use libp2p::Multiaddr; use log::LevelFilter; +use std::{io, io::Write, process}; use structopt::StructOpt; use tracing::info; - mod cli; mod trace; @@ -77,8 +77,10 @@ async fn swap_as_bob(sats: u64, addr: Multiaddr) -> Result<()> { match read { Some(cmd) => match cmd { Cmd::VerifyAmounts(p) => { - if verified(p) { - rsp_tx.try_send(Rsp::Verified)?; + let rsp = verify(p); + rsp_tx.try_send(rsp)?; + if rsp == Rsp::Abort { + process::exit(0); } } }, @@ -90,7 +92,29 @@ async fn swap_as_bob(sats: u64, addr: Multiaddr) -> Result<()> { } } -fn verified(_p: SwapParams) -> bool { - // TODO: Read input from the shell. - true +fn verify(p: SwapParams) -> Rsp { + let mut s = String::new(); + println!("Got rate from Alice for XMR/BTC swap\n"); + println!("{}", p); + print!("Would you like to continue with this swap [y/N]: "); + let _ = io::stdout().flush(); + io::stdin() + .read_line(&mut s) + .expect("Did not enter a correct string"); + if let Some('\n') = s.chars().next_back() { + s.pop(); + } + if let Some('\r') = s.chars().next_back() { + s.pop(); + } + if !is_yes(&s) { + println!("No worries, try again later - Alice updates her rate regularly"); + return Rsp::Abort; + } + + Rsp::Verified +} + +fn is_yes(s: &str) -> bool { + matches!(s, "y" | "Y" | "yes" | "YES" | "Yes") } From 176b2195e3a5b387b8905c3789260ec015483e9d Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Fri, 16 Oct 2020 10:49:34 +1100 Subject: [PATCH 10/52] Make ReceiveTransferProof async and take &mut self --- xmr-btc/src/lib.rs | 7 ++++--- xmr-btc/tests/on_chain.rs | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 3d6e80dc..0430b567 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -76,8 +76,9 @@ pub enum Action { } // TODO: This could be moved to the monero module +#[async_trait] pub trait ReceiveTransferProof { - fn receive_transfer_proof(&self) -> monero::TransferProof; + async fn receive_transfer_proof(&mut self) -> monero::TransferProof; } #[async_trait] @@ -90,7 +91,7 @@ pub trait MedianTime { /// This is called post handshake, after all the keys, addresses and most of the /// signatures have been exchanged. pub fn action_generator_bob( - network: &'static N, + network: &'static mut N, monero_ledger: &'static M, bitcoin_ledger: &'static B, // TODO: Replace this with a new, slimmer struct? @@ -150,7 +151,7 @@ where futures::pin_mut!(poll_until_btc_has_expired); // the source of this could be the database, this layer doesn't care - let transfer_proof = network.receive_transfer_proof(); + let transfer_proof = network.receive_transfer_proof().await; let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar( s_b.into_ed25519(), diff --git a/xmr-btc/tests/on_chain.rs b/xmr-btc/tests/on_chain.rs index 20e937b7..a5781189 100644 --- a/xmr-btc/tests/on_chain.rs +++ b/xmr-btc/tests/on_chain.rs @@ -1,6 +1,7 @@ pub mod harness; use anyhow::Result; +use async_trait::async_trait; use genawaiter::GeneratorState; use harness::wallet::{bitcoin, monero}; use xmr_btc::{ @@ -13,14 +14,15 @@ use xmr_btc::{ struct Network; +#[async_trait] impl ReceiveTransferProof for Network { - fn receive_transfer_proof(&self) -> xmr_btc::monero::TransferProof { + async fn receive_transfer_proof(&mut self) -> xmr_btc::monero::TransferProof { todo!("use libp2p") } } async fn swap_as_bob( - network: &'static Network, + network: &'static mut Network, monero_wallet: &'static monero::BobWallet<'static>, bitcoin_wallet: &'static bitcoin::Wallet, state: bob::State2, From c1f6adc8ed8b5607fe05c57a64bee259a871041c Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Fri, 16 Oct 2020 10:54:12 +1100 Subject: [PATCH 11/52] Refund if Alice takes too long to prove that Monero has been locked --- xmr-btc/src/lib.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 0430b567..fc0cbb35 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -151,7 +151,16 @@ where futures::pin_mut!(poll_until_btc_has_expired); // the source of this could be the database, this layer doesn't care - let transfer_proof = network.receive_transfer_proof().await; + + let transfer_proof = match select( + network.receive_transfer_proof(), + poll_until_btc_has_expired.clone(), + ) + .await + { + Either::Left((proof, _)) => proof, + Either::Right(_) => return Err(SwapFailed::TimelockReached), + }; let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar( s_b.into_ed25519(), From 9f1bf72c7b5619d2baa9e6f8278949c9b6754275 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Fri, 16 Oct 2020 11:18:02 +1100 Subject: [PATCH 12/52] Only generate refund action after Bitcoin lock --- xmr-btc/src/lib.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index fc0cbb35..f1412366 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -118,7 +118,12 @@ where B: MedianTime + bitcoin::WatchForRawTransaction + Send + Sync, { enum SwapFailed { - TimelockReached, + BeforeBtcLock, + AfterBtcLock(Reason), + } + + enum Reason { + BtcExpired, InsufficientXMR(monero::InsufficientFunds), } @@ -142,7 +147,7 @@ where let btc_has_expired = bitcoin_time_is_gte(bitcoin_ledger, refund_timelock).shared(); if btc_has_expired.clone().await { - return Err(SwapFailed::TimelockReached); + return Err(SwapFailed::BeforeBtcLock); } co.yield_(Action::LockBitcoin(tx_lock.clone())).await; @@ -150,8 +155,6 @@ where let poll_until_btc_has_expired = poll_until(btc_has_expired).shared(); futures::pin_mut!(poll_until_btc_has_expired); - // the source of this could be the database, this layer doesn't care - let transfer_proof = match select( network.receive_transfer_proof(), poll_until_btc_has_expired.clone(), @@ -159,7 +162,7 @@ where .await { Either::Left((proof, _)) => proof, - Either::Right(_) => return Err(SwapFailed::TimelockReached), + Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), }; let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar( @@ -179,8 +182,10 @@ where ) .await { - Either::Left((Err(e), _)) => return Err(SwapFailed::InsufficientXMR(e)), - Either::Right(_) => return Err(SwapFailed::TimelockReached), + Either::Left((Err(e), _)) => { + return Err(SwapFailed::AfterBtcLock(Reason::InsufficientXMR(e))) + } + Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), _ => {} } @@ -197,7 +202,7 @@ where .await { Either::Left((tx, _)) => tx, - Either::Right(_) => return Err(SwapFailed::TimelockReached), + Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), }; // NOTE: If any of this fails, Bob will never be able to take the monero. @@ -226,7 +231,7 @@ where } .await; - if swap_result.is_err() { + if let Err(SwapFailed::AfterBtcLock(_)) = swap_result { let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, A.clone(), b.public()); let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address); From 7d307e581a842d674c067461bc74ca343fd2b818 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Fri, 16 Oct 2020 11:25:41 +1100 Subject: [PATCH 13/52] Fail the swap early if Bitcoin TxLock is never published This helps distinguish between the case where the refund timelock is reached before the bitcoin is locked and the case where the refund timelock is reached after the bitcoin is locked and before Alice sends over the transfer proof for locking up the monero. In the first case we can abort without doing anything, but in the second case we must instruct the caller to refund the bitcoin. --- xmr-btc/src/lib.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index f1412366..c24ed222 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -145,6 +145,8 @@ where Gen::new_boxed(|co| async move { let swap_result: Result<(), SwapFailed> = async { let btc_has_expired = bitcoin_time_is_gte(bitcoin_ledger, refund_timelock).shared(); + let poll_until_btc_has_expired = poll_until(btc_has_expired.clone()).shared(); + futures::pin_mut!(poll_until_btc_has_expired); if btc_has_expired.clone().await { return Err(SwapFailed::BeforeBtcLock); @@ -152,8 +154,15 @@ where co.yield_(Action::LockBitcoin(tx_lock.clone())).await; - let poll_until_btc_has_expired = poll_until(btc_has_expired).shared(); - futures::pin_mut!(poll_until_btc_has_expired); + match select( + bitcoin_ledger.watch_for_raw_transaction(tx_lock.txid()), + poll_until_btc_has_expired.clone(), + ) + .await + { + Either::Left(_) => {} + Either::Right(_) => return Err(SwapFailed::BeforeBtcLock), + } let transfer_proof = match select( network.receive_transfer_proof(), From 369770488f26f66e93180e3e1ee0a3a8057ad543 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Fri, 16 Oct 2020 11:31:16 +1100 Subject: [PATCH 14/52] Document Reason enum --- xmr-btc/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index c24ed222..8842e8df 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -122,8 +122,11 @@ where AfterBtcLock(Reason), } + /// Reason why the swap has failed. enum Reason { + /// The refund timelock has been reached. BtcExpired, + /// Alice did not lock up enough monero in the shared output. InsufficientXMR(monero::InsufficientFunds), } From e457130b6c78bd8ba94d4bef55caecbd363e20c5 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Fri, 16 Oct 2020 11:43:24 +1100 Subject: [PATCH 15/52] Bubble up unrecoverable errors instead of expecting This does introduce the ability of expressing incorrect combinations of the enums `SwapFailed` and `Reason`, but these are just internal to this function and it's terser that way. --- xmr-btc/src/lib.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 8842e8df..f07fa774 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -120,6 +120,7 @@ where enum SwapFailed { BeforeBtcLock, AfterBtcLock(Reason), + AfterBtcRedeem(Reason), } /// Reason why the swap has failed. @@ -128,6 +129,12 @@ where BtcExpired, /// Alice did not lock up enough monero in the shared output. InsufficientXMR(monero::InsufficientFunds), + /// Could not find Bob's signature on the redeem transaction witness + /// stack. + BtcRedeemSignature, + /// Could not recover secret `s_a` from Bob's redeem transaction + /// signature. + SecretRecovery, } async fn poll_until(condition_future: impl Future + Clone) { @@ -221,10 +228,9 @@ where // Therefore, there is no way to handle these errors other than aborting let tx_redeem_sig = tx_redeem .extract_signature_by_key(tx_redeem_published, b.public()) - .expect("redeem transaction must include signature from us"); - let s_a = bitcoin::recover(S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig).expect( - "alice can only produce our signature by decrypting our encrypted signature", - ); + .map_err(|_| SwapFailed::AfterBtcRedeem(Reason::BtcRedeemSignature))?; + let s_a = bitcoin::recover(S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig) + .map_err(|_| SwapFailed::AfterBtcRedeem(Reason::SecretRecovery))?; let s_a = monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order( s_a.to_bytes(), )); From 1251681cfdb2528477602c32194bdcac1f8314e9 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Fri, 16 Oct 2020 12:42:47 +1100 Subject: [PATCH 16/52] Calculate amounts from BTC --- swap/src/alice.rs | 39 ++++++++++++++++++++++++++++++--------- swap/src/lib.rs | 8 ++++++-- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 80ddc09c..aebd9b3f 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -13,7 +13,7 @@ mod messenger; use self::messenger::*; use crate::{ - monero, + bitcoin, monero, network::{ peer_tracker::{self, PeerTracker}, request_response::{AliceToBob, TIMEOUT}, @@ -31,14 +31,8 @@ pub async fn swap(listen: Multiaddr) -> Result<()> { match swarm.next().await { BehaviourOutEvent::Request(messenger::BehaviourOutEvent::Btc { btc, channel }) => { debug!("Got request from Bob to swap {}", btc); - let params = SwapParams { - btc, - // TODO: Do a real calculation. - xmr: monero::Amount::from_piconero(10), - }; - - let msg = AliceToBob::Amounts(params); - swarm.send(channel, msg); + let p = calculate_amounts(btc); + swarm.send(channel, AliceToBob::Amounts(p)); } other => panic!("unexpected event: {:?}", other), } @@ -137,3 +131,30 @@ impl Default for Alice { } } } + +fn calculate_amounts(btc: bitcoin::Amount) -> SwapParams { + const XMR_PER_BTC: u64 = 100; // TODO: Get this from an exchange. + + // XMR uses 12 zerose BTC uses 8. + let picos = (btc.as_sat() * 10000) * XMR_PER_BTC; + let xmr = monero::Amount::from_piconero(picos); + + SwapParams { btc, xmr } +} + +#[cfg(test)] +mod tests { + use super::*; + + const ONE_BTC: u64 = 100_000_000; + const HUNDRED_XMR: u64 = 100_000_000_000_000; + + #[test] + fn one_bitcoin_equals_a_hundred_moneroj() { + let btc = bitcoin::Amount::from_sat(ONE_BTC); + let want = monero::Amount::from_piconero(HUNDRED_XMR); + + let SwapParams { xmr: got, .. } = calculate_amounts(btc); + assert_eq!(got, want); + } +} diff --git a/swap/src/lib.rs b/swap/src/lib.rs index f9ba9a32..11f688d6 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -43,7 +43,7 @@ pub mod monero { use serde::{Deserialize, Serialize}; use std::fmt; - #[derive(Copy, Clone, Debug, Serialize, Deserialize)] + #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Amount(u64); impl Amount { @@ -76,7 +76,7 @@ pub mod bitcoin { use serde::{Deserialize, Serialize}; use std::fmt; - #[derive(Copy, Clone, Debug, Serialize, Deserialize)] + #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Amount(u64); impl Amount { @@ -92,6 +92,10 @@ pub mod bitcoin { pub fn from_sat(satoshi: u64) -> Amount { Amount(satoshi) } + + pub fn as_sat(&self) -> u64 { + self.0 + } } impl fmt::Display for Amount { From bae391ff9c5198e4017403db100607c1cc7207c0 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Fri, 16 Oct 2020 11:43:24 +1100 Subject: [PATCH 17/52] Bubble up unrecoverable errors instead of expecting This does introduce the ability of expressing incorrect combinations of the enums `SwapFailed` and `Reason`, but these are just internal to this function and it's terser that way. --- xmr-btc/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index f07fa774..99cc8405 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -224,8 +224,6 @@ where Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), }; - // NOTE: If any of this fails, Bob will never be able to take the monero. - // Therefore, there is no way to handle these errors other than aborting let tx_redeem_sig = tx_redeem .extract_signature_by_key(tx_redeem_published, b.public()) .map_err(|_| SwapFailed::AfterBtcRedeem(Reason::BtcRedeemSignature))?; From d813957c5a235648a1a2dc81a60008d1d66203a3 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Fri, 16 Oct 2020 14:02:47 +1100 Subject: [PATCH 18/52] Rename InsufficientXMR to InsufficientXmr --- xmr-btc/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 99cc8405..cb12d5a8 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -128,7 +128,7 @@ where /// The refund timelock has been reached. BtcExpired, /// Alice did not lock up enough monero in the shared output. - InsufficientXMR(monero::InsufficientFunds), + InsufficientXmr(monero::InsufficientFunds), /// Could not find Bob's signature on the redeem transaction witness /// stack. BtcRedeemSignature, @@ -202,7 +202,7 @@ where .await { Either::Left((Err(e), _)) => { - return Err(SwapFailed::AfterBtcLock(Reason::InsufficientXMR(e))) + return Err(SwapFailed::AfterBtcLock(Reason::InsufficientXmr(e))) } Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), _ => {} From 969ca63081795f6632eaecb474b97c61052bb54f Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Fri, 16 Oct 2020 14:06:25 +1100 Subject: [PATCH 19/52] Split RefundBitcoin into CancelBitcoin and RefundBitcoin --- xmr-btc/src/lib.rs | 18 +++++++----------- xmr-btc/tests/on_chain.rs | 8 +++----- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index cb12d5a8..5a5d95b9 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -69,10 +69,8 @@ pub enum Action { spend_key: monero::PrivateKey, view_key: monero::PrivateViewKey, }, - RefundBitcoin { - tx_cancel: bitcoin::Transaction, - tx_refund: bitcoin::Transaction, - }, + CancelBitcoin(bitcoin::Transaction), + RefundBitcoin(bitcoin::Transaction), } // TODO: This could be moved to the monero module @@ -250,8 +248,6 @@ where if let Err(SwapFailed::AfterBtcLock(_)) = swap_result { let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, A.clone(), b.public()); - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address); - let signed_tx_cancel = { let sig_a = tx_cancel_sig_a.clone(); let sig_b = b.sign(tx_cancel.digest()); @@ -262,7 +258,11 @@ where .expect("sig_{a,b} to be valid signatures for tx_cancel") }; + co.yield_(Action::CancelBitcoin(signed_tx_cancel)).await; + let signed_tx_refund = { + let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address); + let adaptor = Adaptor::>::default(); let sig_a = @@ -274,11 +274,7 @@ where .expect("sig_{a,b} to be valid signatures for tx_refund") }; - co.yield_(Action::RefundBitcoin { - tx_cancel: signed_tx_cancel, - tx_refund: signed_tx_refund, - }) - .await; + co.yield_(Action::RefundBitcoin(signed_tx_refund)).await; } }) } diff --git a/xmr-btc/tests/on_chain.rs b/xmr-btc/tests/on_chain.rs index a5781189..8de971c0 100644 --- a/xmr-btc/tests/on_chain.rs +++ b/xmr-btc/tests/on_chain.rs @@ -48,14 +48,12 @@ async fn swap_as_bob( .create_and_load_wallet_for_output(spend_key, view_key) .await?; } - GeneratorState::Yielded(Action::RefundBitcoin { - tx_cancel, - tx_refund, - }) => { + GeneratorState::Yielded(Action::CancelBitcoin(tx_cancel)) => { let _ = bitcoin_wallet .broadcast_signed_transaction(tx_cancel) .await?; - + } + GeneratorState::Yielded(Action::RefundBitcoin(tx_refund)) => { let _ = bitcoin_wallet .broadcast_signed_transaction(tx_refund) .await?; From 9d64f3fd2952fa5056ac5d7c06a9138b1b19fb43 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Fri, 16 Oct 2020 14:16:06 +1100 Subject: [PATCH 20/52] Watch the blockchain during Bitcoin refund --- xmr-btc/src/bitcoin/transactions.rs | 4 ++++ xmr-btc/src/lib.rs | 13 +++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/xmr-btc/src/bitcoin/transactions.rs b/xmr-btc/src/bitcoin/transactions.rs index 9eb7878d..1402c9cc 100644 --- a/xmr-btc/src/bitcoin/transactions.rs +++ b/xmr-btc/src/bitcoin/transactions.rs @@ -260,6 +260,10 @@ impl TxCancel { } } + pub fn txid(&self) -> Txid { + self.inner.txid() + } + pub fn digest(&self) -> SigHash { self.digest } diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 5a5d95b9..479f2a0a 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -248,6 +248,7 @@ where if let Err(SwapFailed::AfterBtcLock(_)) = swap_result { let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, A.clone(), b.public()); + let tx_cancel_txid = tx_cancel.txid(); let signed_tx_cancel = { let sig_a = tx_cancel_sig_a.clone(); let sig_b = b.sign(tx_cancel.digest()); @@ -260,9 +261,13 @@ where co.yield_(Action::CancelBitcoin(signed_tx_cancel)).await; - let signed_tx_refund = { - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address); + let _ = bitcoin_ledger + .watch_for_raw_transaction(tx_cancel_txid) + .await; + let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address); + let tx_refund_txid = tx_refund.txid(); + let signed_tx_refund = { let adaptor = Adaptor::>::default(); let sig_a = @@ -275,6 +280,10 @@ where }; co.yield_(Action::RefundBitcoin(signed_tx_refund)).await; + + let _ = bitcoin_ledger + .watch_for_raw_transaction(tx_refund_txid) + .await; } }) } From 55ea8f23e25d40592e5df8e4a48134e45985a213 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Fri, 16 Oct 2020 14:17:10 +1100 Subject: [PATCH 21/52] s/{monero,bitcoin}_ledger/{monero,bitcoin}_client/g --- xmr-btc/src/lib.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 479f2a0a..97e8944f 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -90,8 +90,8 @@ pub trait MedianTime { /// signatures have been exchanged. pub fn action_generator_bob( network: &'static mut N, - monero_ledger: &'static M, - bitcoin_ledger: &'static B, + monero_client: &'static M, + bitcoin_client: &'static B, // TODO: Replace this with a new, slimmer struct? bob::State2 { A, @@ -152,7 +152,7 @@ where Gen::new_boxed(|co| async move { let swap_result: Result<(), SwapFailed> = async { - let btc_has_expired = bitcoin_time_is_gte(bitcoin_ledger, refund_timelock).shared(); + let btc_has_expired = bitcoin_time_is_gte(bitcoin_client, refund_timelock).shared(); let poll_until_btc_has_expired = poll_until(btc_has_expired.clone()).shared(); futures::pin_mut!(poll_until_btc_has_expired); @@ -163,7 +163,7 @@ where co.yield_(Action::LockBitcoin(tx_lock.clone())).await; match select( - bitcoin_ledger.watch_for_raw_transaction(tx_lock.txid()), + bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), poll_until_btc_has_expired.clone(), ) .await @@ -188,7 +188,7 @@ where let S = S_a_monero + S_b_monero; match select( - monero_ledger.watch_for_transfer( + monero_client.watch_for_transfer( S, v.public(), transfer_proof, @@ -213,7 +213,7 @@ where .await; let tx_redeem_published = match select( - bitcoin_ledger.watch_for_raw_transaction(tx_redeem.txid()), + bitcoin_client.watch_for_raw_transaction(tx_redeem.txid()), poll_until_btc_has_expired, ) .await @@ -261,7 +261,7 @@ where co.yield_(Action::CancelBitcoin(signed_tx_cancel)).await; - let _ = bitcoin_ledger + let _ = bitcoin_client .watch_for_raw_transaction(tx_cancel_txid) .await; @@ -281,7 +281,7 @@ where co.yield_(Action::RefundBitcoin(signed_tx_refund)).await; - let _ = bitcoin_ledger + let _ = bitcoin_client .watch_for_raw_transaction(tx_refund_txid) .await; } From 7c99415cff18734e9cf7f1bca4e9041118e7dc55 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Fri, 16 Oct 2020 17:04:03 +1100 Subject: [PATCH 22/52] Write action_generator_alice --- xmr-btc/src/alice.rs | 30 +-- xmr-btc/src/bitcoin/transactions.rs | 4 + xmr-btc/src/lib.rs | 298 +++++++++++++++++++++++++++- xmr-btc/tests/on_chain.rs | 12 +- 4 files changed, 316 insertions(+), 28 deletions(-) diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index 4684c1a4..a8a1ea20 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -330,22 +330,22 @@ impl State2 { #[derive(Debug)] pub struct State3 { - a: bitcoin::SecretKey, - B: bitcoin::PublicKey, - s_a: cross_curve_dleq::Scalar, - S_b_monero: monero::PublicKey, - S_b_bitcoin: bitcoin::PublicKey, - v: monero::PrivateViewKey, + pub a: bitcoin::SecretKey, + pub B: bitcoin::PublicKey, + pub s_a: cross_curve_dleq::Scalar, + pub S_b_monero: monero::PublicKey, + pub S_b_bitcoin: bitcoin::PublicKey, + pub v: monero::PrivateViewKey, btc: bitcoin::Amount, - xmr: monero::Amount, - refund_timelock: u32, - punish_timelock: u32, - refund_address: bitcoin::Address, - redeem_address: bitcoin::Address, - punish_address: bitcoin::Address, - tx_lock: bitcoin::TxLock, - tx_punish_sig_bob: bitcoin::Signature, - tx_cancel_sig_bob: bitcoin::Signature, + pub xmr: monero::Amount, + pub refund_timelock: u32, + pub punish_timelock: u32, + pub refund_address: bitcoin::Address, + pub redeem_address: bitcoin::Address, + pub punish_address: bitcoin::Address, + pub tx_lock: bitcoin::TxLock, + pub tx_punish_sig_bob: bitcoin::Signature, + pub tx_cancel_sig_bob: bitcoin::Signature, } impl State3 { diff --git a/xmr-btc/src/bitcoin/transactions.rs b/xmr-btc/src/bitcoin/transactions.rs index 1402c9cc..c54448dc 100644 --- a/xmr-btc/src/bitcoin/transactions.rs +++ b/xmr-btc/src/bitcoin/transactions.rs @@ -463,6 +463,10 @@ impl TxPunish { } } + pub fn txid(&self) -> Txid { + self.inner.txid() + } + pub fn digest(&self) -> SigHash { self.digest } diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 97e8944f..a77c95bf 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -62,7 +62,7 @@ use sha2::Sha256; #[allow(clippy::large_enum_variant)] #[derive(Debug)] -pub enum Action { +pub enum BobAction { LockBitcoin(bitcoin::TxLock), SendBitcoinRedeemEncsig(bitcoin::EncryptedSignature), CreateMoneroWalletForOutput { @@ -109,7 +109,7 @@ pub fn action_generator_bob( tx_refund_encsig, .. }: bob::State2, -) -> GenBoxed +) -> GenBoxed where N: ReceiveTransferProof + Send + Sync, M: monero::WatchForTransfer + Send + Sync, @@ -160,7 +160,7 @@ where return Err(SwapFailed::BeforeBtcLock); } - co.yield_(Action::LockBitcoin(tx_lock.clone())).await; + co.yield_(BobAction::LockBitcoin(tx_lock.clone())).await; match select( bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), @@ -209,7 +209,7 @@ where let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address); let tx_redeem_encsig = b.encsign(S_a_bitcoin.clone(), tx_redeem.digest()); - co.yield_(Action::SendBitcoinRedeemEncsig(tx_redeem_encsig.clone())) + co.yield_(BobAction::SendBitcoinRedeemEncsig(tx_redeem_encsig.clone())) .await; let tx_redeem_published = match select( @@ -235,7 +235,7 @@ where scalar: s_b.into_ed25519(), }; - co.yield_(Action::CreateMoneroWalletForOutput { + co.yield_(BobAction::CreateMoneroWalletForOutput { spend_key: s_a + s_b, view_key: v, }) @@ -259,7 +259,7 @@ where .expect("sig_{a,b} to be valid signatures for tx_cancel") }; - co.yield_(Action::CancelBitcoin(signed_tx_cancel)).await; + co.yield_(BobAction::CancelBitcoin(signed_tx_cancel)).await; let _ = bitcoin_client .watch_for_raw_transaction(tx_cancel_txid) @@ -279,7 +279,7 @@ where .expect("sig_{a,b} to be valid signatures for tx_refund") }; - co.yield_(Action::RefundBitcoin(signed_tx_refund)).await; + co.yield_(BobAction::RefundBitcoin(signed_tx_refund)).await; let _ = bitcoin_client .watch_for_raw_transaction(tx_refund_txid) @@ -287,3 +287,287 @@ where } }) } + +#[derive(Debug)] +pub enum AliceAction { + // This action also includes proving to Bob that this has happened, given that our current + // protocol requires a transfer proof to verify that the coins have been locked on Monero + LockXmr { + amount: monero::Amount, + public_spend_key: monero::PublicKey, + public_view_key: monero::PublicViewKey, + }, + RedeemBtc(bitcoin::Transaction), + CreateMoneroWalletForOutput { + spend_key: monero::PrivateKey, + view_key: monero::PrivateViewKey, + }, + CancelBtc(bitcoin::Transaction), + PunishBtc(bitcoin::Transaction), +} + +// TODO: This could be moved to the bitcoin module +#[async_trait] +pub trait ReceiveBitcoinRedeemEncsig { + async fn receive_bitcoin_redeem_encsig(&mut self) -> bitcoin::EncryptedSignature; +} + +/// Perform the on-chain protocol to swap monero and bitcoin as Alice. +/// +/// This is called post handshake, after all the keys, addresses and most of the +/// signatures have been exchanged. +pub fn action_generator_alice( + network: &'static mut N, + _monero_client: &'static M, + bitcoin_client: &'static B, + // TODO: Replace this with a new, slimmer struct? + alice::State3 { + a, + B, + s_a, + S_b_monero, + S_b_bitcoin, + v, + xmr, + refund_timelock, + punish_timelock, + refund_address, + redeem_address, + punish_address, + tx_lock, + tx_punish_sig_bob, + tx_cancel_sig_bob, + .. + }: alice::State3, +) -> GenBoxed +where + N: ReceiveBitcoinRedeemEncsig + Send + Sync, + M: monero::WatchForTransfer + Send + Sync, + B: MedianTime + bitcoin::WatchForRawTransaction + Send + Sync, +{ + enum SwapFailed { + BeforeBtcLock, + AfterXmrLock(Reason), + } + + /// Reason why the swap has failed. + enum Reason { + /// The refund timelock has been reached. + BtcExpired, + } + + enum RefundFailed { + BtcPunishable { + tx_cancel_was_published: bool, + }, + /// Could not find Alice's signature on the refund transaction witness + /// stack. + BtcRefundSignature, + /// Could not recover secret `s_b` from Alice's refund transaction + /// signature. + SecretRecovery, + } + + async fn poll_until(condition_future: impl Future + Clone) { + loop { + if condition_future.clone().await { + return; + } + } + } + + async fn bitcoin_time_is_gte(bitcoin_client: &B, timestamp: u32) -> bool + where + B: MedianTime, + { + bitcoin_client.median_time().await >= timestamp + } + + Gen::new_boxed(|co| async move { + let swap_result: Result<(), SwapFailed> = async { + let btc_has_expired = bitcoin_time_is_gte(bitcoin_client, refund_timelock).shared(); + let poll_until_btc_has_expired = poll_until(btc_has_expired.clone()).shared(); + futures::pin_mut!(poll_until_btc_has_expired); + + match select( + bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), + poll_until_btc_has_expired.clone(), + ) + .await + { + Either::Left(_) => {} + Either::Right(_) => return Err(SwapFailed::BeforeBtcLock), + } + + let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { + scalar: s_a.into_ed25519(), + }); + + co.yield_(AliceAction::LockXmr { + amount: xmr, + public_spend_key: S_a + S_b_monero, + public_view_key: v.public(), + }) + .await; + + // TODO: Watch for LockXmr using watch-only wallet. Doing so will prevent Alice + // from cancelling/refunding unnecessarily. + + let tx_redeem_encsig = match select( + network.receive_bitcoin_redeem_encsig(), + poll_until_btc_has_expired.clone(), + ) + .await + { + Either::Left((encsig, _)) => encsig, + Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)), + }; + + let (signed_tx_redeem, tx_redeem_txid) = { + let adaptor = Adaptor::>::default(); + + let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address); + + let sig_a = a.sign(tx_redeem.digest()); + let sig_b = + adaptor.decrypt_signature(&s_a.into_secp256k1(), tx_redeem_encsig.clone()); + + let tx = tx_redeem + .add_signatures(&tx_lock, (a.public(), sig_a), (B.clone(), sig_b)) + .expect("sig_{a,b} to be valid signatures for tx_redeem"); + let txid = tx.txid(); + + (tx, txid) + }; + + co.yield_(AliceAction::RedeemBtc(signed_tx_redeem)).await; + + match select( + bitcoin_client.watch_for_raw_transaction(tx_redeem_txid), + poll_until_btc_has_expired, + ) + .await + { + Either::Left(_) => {} + Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)), + }; + + Ok(()) + } + .await; + + if let Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)) = swap_result { + let refund_result: Result<(), RefundFailed> = async { + let bob_can_be_punished = + bitcoin_time_is_gte(bitcoin_client, punish_timelock).shared(); + let poll_until_bob_can_be_punished = poll_until(bob_can_be_punished).shared(); + futures::pin_mut!(poll_until_bob_can_be_punished); + + let tx_cancel = + bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone()); + match select( + bitcoin_client.watch_for_raw_transaction(tx_cancel.txid()), + poll_until_bob_can_be_punished.clone(), + ) + .await + { + Either::Left(_) => {} + Either::Right(_) => { + return Err(RefundFailed::BtcPunishable { + tx_cancel_was_published: false, + }) + } + }; + + let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address); + let tx_refund_published = match select( + bitcoin_client.watch_for_raw_transaction(tx_refund.txid()), + poll_until_bob_can_be_punished, + ) + .await + { + Either::Left((tx, _)) => tx, + Either::Right(_) => { + return Err(RefundFailed::BtcPunishable { + tx_cancel_was_published: true, + }) + } + }; + + let s_a = monero::PrivateKey { + scalar: s_a.into_ed25519(), + }; + + let tx_refund_sig = tx_refund + .extract_signature_by_key(tx_refund_published, B.clone()) + .map_err(|_| RefundFailed::BtcRefundSignature)?; + let tx_refund_encsig = a.encsign(S_b_bitcoin.clone(), tx_refund.digest()); + + let s_b = bitcoin::recover(S_b_bitcoin, tx_refund_sig, tx_refund_encsig) + .map_err(|_| RefundFailed::SecretRecovery)?; + let s_b = monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order( + s_b.to_bytes(), + )); + + co.yield_(AliceAction::CreateMoneroWalletForOutput { + spend_key: s_a + s_b, + view_key: v, + }) + .await; + + Ok(()) + } + .await; + + // LIMITATION: When approaching the punish scenario, Bob could theoretically + // wake up in between Alice's publication of tx cancel and beat Alice's punish + // transaction with his refund transaction. Alice would then need to carry on + // with the refund on Monero. Doing so may be too verbose with the current, + // linear approach. A different design may be required + if let Err(RefundFailed::BtcPunishable { + tx_cancel_was_published, + }) = refund_result + { + let tx_cancel = + bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone()); + + if !tx_cancel_was_published { + let tx_cancel_txid = tx_cancel.txid(); + let signed_tx_cancel = { + let sig_a = a.sign(tx_cancel.digest()); + let sig_b = tx_cancel_sig_bob; + + tx_cancel + .clone() + .add_signatures(&tx_lock, (a.public(), sig_a), (B.clone(), sig_b)) + .expect("sig_{a,b} to be valid signatures for tx_cancel") + }; + + co.yield_(AliceAction::CancelBtc(signed_tx_cancel)).await; + + let _ = bitcoin_client + .watch_for_raw_transaction(tx_cancel_txid) + .await; + } + + let tx_punish = + bitcoin::TxPunish::new(&tx_cancel, &punish_address, punish_timelock); + let tx_punish_txid = tx_punish.txid(); + let signed_tx_punish = { + let sig_a = a.sign(tx_punish.digest()); + let sig_b = tx_punish_sig_bob; + + tx_punish + .add_signatures(&tx_cancel, (a.public(), sig_a), (B, sig_b)) + .expect("sig_{a,b} to be valid signatures for tx_cancel") + }; + + co.yield_(AliceAction::PunishBtc(signed_tx_punish)).await; + + let _ = bitcoin_client + .watch_for_raw_transaction(tx_punish_txid) + .await; + } + } + }) +} diff --git a/xmr-btc/tests/on_chain.rs b/xmr-btc/tests/on_chain.rs index 8de971c0..7d43252b 100644 --- a/xmr-btc/tests/on_chain.rs +++ b/xmr-btc/tests/on_chain.rs @@ -9,7 +9,7 @@ use xmr_btc::{ bitcoin::{BroadcastSignedTransaction, SignTxLock}, bob, monero::CreateWalletForOutput, - Action, ReceiveTransferProof, + BobAction, ReceiveTransferProof, }; struct Network; @@ -31,16 +31,16 @@ async fn swap_as_bob( loop { match action_generator.async_resume().await { - GeneratorState::Yielded(Action::LockBitcoin(tx_lock)) => { + GeneratorState::Yielded(BobAction::LockBitcoin(tx_lock)) => { let signed_tx_lock = bitcoin_wallet.sign_tx_lock(tx_lock).await?; let _ = bitcoin_wallet .broadcast_signed_transaction(signed_tx_lock) .await?; } - GeneratorState::Yielded(Action::SendBitcoinRedeemEncsig(_tx_redeem_encsig)) => { + GeneratorState::Yielded(BobAction::SendBitcoinRedeemEncsig(_tx_redeem_encsig)) => { todo!("use libp2p") } - GeneratorState::Yielded(Action::CreateMoneroWalletForOutput { + GeneratorState::Yielded(BobAction::CreateMoneroWalletForOutput { spend_key, view_key, }) => { @@ -48,12 +48,12 @@ async fn swap_as_bob( .create_and_load_wallet_for_output(spend_key, view_key) .await?; } - GeneratorState::Yielded(Action::CancelBitcoin(tx_cancel)) => { + GeneratorState::Yielded(BobAction::CancelBitcoin(tx_cancel)) => { let _ = bitcoin_wallet .broadcast_signed_transaction(tx_cancel) .await?; } - GeneratorState::Yielded(Action::RefundBitcoin(tx_refund)) => { + GeneratorState::Yielded(BobAction::RefundBitcoin(tx_refund)) => { let _ = bitcoin_wallet .broadcast_signed_transaction(tx_refund) .await?; From 55629838f4355984acb283349a6f304d7b5b8325 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Mon, 19 Oct 2020 10:44:25 +1100 Subject: [PATCH 23/52] Implement test function that calls action_generator_alice --- xmr-btc/src/lib.rs | 2 +- xmr-btc/tests/on_chain.rs | 85 ++++++++++++++++++++++++++++++++++----- 2 files changed, 75 insertions(+), 12 deletions(-) diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index a77c95bf..1b2b6e24 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -342,7 +342,7 @@ pub fn action_generator_alice( ) -> GenBoxed where N: ReceiveBitcoinRedeemEncsig + Send + Sync, - M: monero::WatchForTransfer + Send + Sync, + M: Send + Sync, B: MedianTime + bitcoin::WatchForRawTransaction + Send + Sync, { enum SwapFailed { diff --git a/xmr-btc/tests/on_chain.rs b/xmr-btc/tests/on_chain.rs index 7d43252b..d6bcaa86 100644 --- a/xmr-btc/tests/on_chain.rs +++ b/xmr-btc/tests/on_chain.rs @@ -2,27 +2,90 @@ pub mod harness; use anyhow::Result; use async_trait::async_trait; +use futures::{ + channel::mpsc::{Receiver, Sender}, + SinkExt, StreamExt, +}; use genawaiter::GeneratorState; use harness::wallet::{bitcoin, monero}; use xmr_btc::{ - action_generator_bob, - bitcoin::{BroadcastSignedTransaction, SignTxLock}, + action_generator_alice, action_generator_bob, alice, + bitcoin::{BroadcastSignedTransaction, EncryptedSignature, SignTxLock}, bob, - monero::CreateWalletForOutput, - BobAction, ReceiveTransferProof, + monero::{CreateWalletForOutput, Transfer, TransferProof}, + AliceAction, BobAction, ReceiveBitcoinRedeemEncsig, ReceiveTransferProof, }; -struct Network; +type AliceNetwork = Network; +type BobNetwork = Network; + +#[derive(Debug)] +struct Network { + // TODO: It is weird to use mpsc's in a situation where only one message is expected, but the + // ownership rules of Rust are making this painful + pub receiver: Receiver, +} + +#[async_trait] +impl ReceiveTransferProof for BobNetwork { + async fn receive_transfer_proof(&mut self) -> TransferProof { + self.receiver.next().await.unwrap() + } +} #[async_trait] -impl ReceiveTransferProof for Network { - async fn receive_transfer_proof(&mut self) -> xmr_btc::monero::TransferProof { - todo!("use libp2p") +impl ReceiveBitcoinRedeemEncsig for AliceNetwork { + async fn receive_bitcoin_redeem_encsig(&mut self) -> EncryptedSignature { + self.receiver.next().await.unwrap() + } +} + +async fn swap_as_alice( + network: &'static mut AliceNetwork, + // FIXME: It would be more intuitive to have a single network/transport struct instead of + // splitting into two, but Rust ownership rules make this tedious + mut sender: Sender, + monero_wallet: &'static monero::AliceWallet<'static>, + bitcoin_wallet: &'static bitcoin::Wallet, + state: alice::State3, +) -> Result<()> { + let mut action_generator = + action_generator_alice(network, monero_wallet, bitcoin_wallet, state); + + loop { + match action_generator.async_resume().await { + GeneratorState::Yielded(AliceAction::LockXmr { + amount, + public_spend_key, + public_view_key, + }) => { + let (transfer_proof, _) = monero_wallet + .transfer(public_spend_key, public_view_key, amount) + .await?; + + sender.send(transfer_proof).await.unwrap(); + } + GeneratorState::Yielded(AliceAction::RedeemBtc(tx)) + | GeneratorState::Yielded(AliceAction::CancelBtc(tx)) + | GeneratorState::Yielded(AliceAction::PunishBtc(tx)) => { + let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?; + } + GeneratorState::Yielded(AliceAction::CreateMoneroWalletForOutput { + spend_key, + view_key, + }) => { + monero_wallet + .create_and_load_wallet_for_output(spend_key, view_key) + .await?; + } + GeneratorState::Complete(()) => return Ok(()), + } } } async fn swap_as_bob( - network: &'static mut Network, + network: &'static mut BobNetwork, + mut sender: Sender, monero_wallet: &'static monero::BobWallet<'static>, bitcoin_wallet: &'static bitcoin::Wallet, state: bob::State2, @@ -37,8 +100,8 @@ async fn swap_as_bob( .broadcast_signed_transaction(signed_tx_lock) .await?; } - GeneratorState::Yielded(BobAction::SendBitcoinRedeemEncsig(_tx_redeem_encsig)) => { - todo!("use libp2p") + GeneratorState::Yielded(BobAction::SendBitcoinRedeemEncsig(tx_redeem_encsig)) => { + sender.send(tx_redeem_encsig).await.unwrap(); } GeneratorState::Yielded(BobAction::CreateMoneroWalletForOutput { spend_key, From 50ed74319f9b64d237f95f15128046ae153b35b4 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Tue, 20 Oct 2020 12:18:27 +1100 Subject: [PATCH 24/52] Simplify xmr-btc/tests Monero wallet - Make it the same for Alice and Bob. - Make it contain a wallet client instead of the `Monero` struct. Also: Remove `Container` from inside `Monero` struct. The caller of `new` can simply ensure that `Container` is not dropped to keep the container alive. This makes the `Monero` struct easier to work with, as it just holds the data necessary to create the different clients created during `init`, and does not have any lifetime restrictions. --- monero-harness/src/lib.rs | 80 +++++++------------------- monero-harness/tests/client.rs | 25 ++++---- monero-harness/tests/monerod.rs | 32 +---------- monero-harness/tests/wallet.rs | 25 ++++---- xmr-btc/tests/e2e.rs | 54 ++++++----------- xmr-btc/tests/harness/node.rs | 28 ++++----- xmr-btc/tests/harness/wallet/monero.rs | 47 +++------------ xmr-btc/tests/on_chain.rs | 4 +- 8 files changed, 88 insertions(+), 207 deletions(-) diff --git a/monero-harness/src/lib.rs b/monero-harness/src/lib.rs index 128d9d97..1d60ff13 100644 --- a/monero-harness/src/lib.rs +++ b/monero-harness/src/lib.rs @@ -48,18 +48,17 @@ const WAIT_WALLET_SYNC_MILLIS: u64 = 1000; /// Wallet sub-account indices. const ACCOUNT_INDEX_PRIMARY: u32 = 0; -#[derive(Debug)] -pub struct Monero<'c> { - pub docker: Container<'c, Cli, image::Monero>, - pub monerod_rpc_port: u16, - pub miner_wallet_rpc_port: u16, - pub alice_wallet_rpc_port: u16, - pub bob_wallet_rpc_port: u16, +#[derive(Copy, Clone, Debug)] +pub struct Monero { + monerod_rpc_port: u16, + miner_wallet_rpc_port: u16, + alice_wallet_rpc_port: u16, + bob_wallet_rpc_port: u16, } -impl<'c> Monero<'c> { +impl<'c> Monero { /// Starts a new regtest monero container. - pub fn new(cli: &'c Cli) -> Self { + pub fn new(cli: &'c Cli) -> (Self, Container<'c, Cli, image::Monero>) { let mut rng = rand::thread_rng(); let monerod_rpc_port: u16 = rng.gen_range(1024, u16::MAX); let miner_wallet_rpc_port: u16 = rng.gen_range(1024, u16::MAX); @@ -91,13 +90,15 @@ impl<'c> Monero<'c> { let docker = cli.run(image); println!("image ran"); - Self { + ( + Self { + monerod_rpc_port, + miner_wallet_rpc_port, + alice_wallet_rpc_port, + bob_wallet_rpc_port, + }, docker, - monerod_rpc_port, - miner_wallet_rpc_port, - alice_wallet_rpc_port, - bob_wallet_rpc_port, - } + ) } pub fn miner_wallet_rpc_client(&self) -> wallet::Client { @@ -156,21 +157,6 @@ impl<'c> Monero<'c> { Ok(()) } - /// Just create a wallet and start mining (you probably want `init()`). - pub async fn init_just_miner(&self, blocks: u32) -> Result<()> { - let wallet = self.miner_wallet_rpc_client(); - let monerod = self.monerod_rpc_client(); - - wallet.create_wallet("miner_wallet").await?; - let miner = self.get_address_miner().await?.address; - - let _ = monerod.generate_blocks(blocks, &miner).await?; - - let _ = tokio::spawn(mine(monerod.clone(), miner)); - - Ok(()) - } - async fn fund_account(&self, address: &str, miner: &str, funding: u64) -> Result<()> { let monerod = self.monerod_rpc_client(); @@ -208,64 +194,42 @@ impl<'c> Monero<'c> { } /// Get addresses for the primary account. - pub async fn get_address_miner(&self) -> Result { + async fn get_address_miner(&self) -> Result { let wallet = self.miner_wallet_rpc_client(); wallet.get_address(ACCOUNT_INDEX_PRIMARY).await } /// Get addresses for the Alice's account. - pub async fn get_address_alice(&self) -> Result { + async fn get_address_alice(&self) -> Result { let wallet = self.alice_wallet_rpc_client(); wallet.get_address(ACCOUNT_INDEX_PRIMARY).await } /// Get addresses for the Bob's account. - pub async fn get_address_bob(&self) -> Result { + async fn get_address_bob(&self) -> Result { let wallet = self.bob_wallet_rpc_client(); wallet.get_address(ACCOUNT_INDEX_PRIMARY).await } - /// Gets the balance of the wallet primary account. - pub async fn get_balance_primary(&self) -> Result { - let wallet = self.miner_wallet_rpc_client(); - wallet.get_balance(ACCOUNT_INDEX_PRIMARY).await - } - /// Gets the balance of Alice's account. - pub async fn get_balance_alice(&self) -> Result { + async fn get_balance_alice(&self) -> Result { let wallet = self.alice_wallet_rpc_client(); wallet.get_balance(ACCOUNT_INDEX_PRIMARY).await } /// Gets the balance of Bob's account. - pub async fn get_balance_bob(&self) -> Result { + async fn get_balance_bob(&self) -> Result { let wallet = self.bob_wallet_rpc_client(); wallet.get_balance(ACCOUNT_INDEX_PRIMARY).await } /// Transfers moneroj from the primary account. - pub async fn transfer_from_primary(&self, amount: u64, address: &str) -> Result { + async fn transfer_from_primary(&self, amount: u64, address: &str) -> Result { let wallet = self.miner_wallet_rpc_client(); wallet .transfer(ACCOUNT_INDEX_PRIMARY, amount, address) .await } - - /// Transfers moneroj from Alice's account. - pub async fn transfer_from_alice(&self, amount: u64, address: &str) -> Result { - let wallet = self.alice_wallet_rpc_client(); - wallet - .transfer(ACCOUNT_INDEX_PRIMARY, amount, address) - .await - } - - /// Transfers moneroj from Bob's account. - pub async fn transfer_from_bob(&self, amount: u64, address: &str) -> Result { - let wallet = self.bob_wallet_rpc_client(); - wallet - .transfer(ACCOUNT_INDEX_PRIMARY, amount, address) - .await - } } /// Mine a block ever BLOCK_TIME_SECS seconds. diff --git a/monero-harness/tests/client.rs b/monero-harness/tests/client.rs index 102b97b3..6a257a52 100644 --- a/monero-harness/tests/client.rs +++ b/monero-harness/tests/client.rs @@ -5,29 +5,24 @@ use testcontainers::clients::Cli; const ALICE_FUND_AMOUNT: u64 = 1_000_000_000_000; const BOB_FUND_AMOUNT: u64 = 0; -fn init_cli() -> Cli { - Cli::default() -} - -async fn init_monero(tc: &'_ Cli) -> Monero<'_> { - let monero = Monero::new(tc); - let _ = monero.init(ALICE_FUND_AMOUNT, BOB_FUND_AMOUNT).await; - - monero -} - #[tokio::test] async fn init_accounts_for_alice_and_bob() { - let cli = init_cli(); - let monero = init_monero(&cli).await; + let tc = Cli::default(); + let (monero, _container) = Monero::new(&tc); + monero + .init(ALICE_FUND_AMOUNT, BOB_FUND_AMOUNT) + .await + .unwrap(); let got_balance_alice = monero - .get_balance_alice() + .alice_wallet_rpc_client() + .get_balance(0) .await .expect("failed to get alice's balance"); let got_balance_bob = monero - .get_balance_bob() + .bob_wallet_rpc_client() + .get_balance(0) .await .expect("failed to get bob's balance"); diff --git a/monero-harness/tests/monerod.rs b/monero-harness/tests/monerod.rs index 52270db6..08465297 100644 --- a/monero-harness/tests/monerod.rs +++ b/monero-harness/tests/monerod.rs @@ -1,8 +1,6 @@ -use monero_harness::{rpc::monerod::Client, Monero}; +use monero_harness::Monero; use spectral::prelude::*; -use std::time::Duration; use testcontainers::clients::Cli; -use tokio::time; fn init_cli() -> Cli { Cli::default() @@ -11,8 +9,8 @@ fn init_cli() -> Cli { #[tokio::test] async fn connect_to_monerod() { let tc = init_cli(); - let monero = Monero::new(&tc); - let cli = Client::localhost(monero.monerod_rpc_port); + let (monero, _container) = Monero::new(&tc); + let cli = monero.monerod_rpc_client(); let header = cli .get_block_header_by_height(0) @@ -21,27 +19,3 @@ async fn connect_to_monerod() { assert_that!(header.height).is_equal_to(0); } - -#[tokio::test] -async fn miner_is_running_and_producing_blocks() { - let tc = init_cli(); - let monero = Monero::new(&tc); - let cli = Client::localhost(monero.monerod_rpc_port); - - monero - .init_just_miner(2) - .await - .expect("Failed to initialize"); - - // Only need 3 seconds since we mine a block every second but - // give it 5 just for good measure. - time::delay_for(Duration::from_secs(5)).await; - - // We should have at least 5 blocks by now. - let header = cli - .get_block_header_by_height(5) - .await - .expect("failed to get block"); - - assert_that!(header.height).is_equal_to(5); -} diff --git a/monero-harness/tests/wallet.rs b/monero-harness/tests/wallet.rs index 91ad7d06..bf77d625 100644 --- a/monero-harness/tests/wallet.rs +++ b/monero-harness/tests/wallet.rs @@ -1,24 +1,21 @@ -use monero_harness::{rpc::wallet::Client, Monero}; +use monero_harness::Monero; use spectral::prelude::*; use testcontainers::clients::Cli; #[tokio::test] async fn wallet_and_accounts() { let tc = Cli::default(); - let monero = Monero::new(&tc); - let miner_wallet = Client::localhost(monero.miner_wallet_rpc_port); + let (monero, _container) = Monero::new(&tc); + let cli = monero.miner_wallet_rpc_client(); println!("creating wallet ..."); - let _ = miner_wallet + let _ = cli .create_wallet("wallet") .await .expect("failed to create wallet"); - let got = miner_wallet - .get_balance(0) - .await - .expect("failed to get balance"); + let got = cli.get_balance(0).await.expect("failed to get balance"); let want = 0; assert_that!(got).is_equal_to(want); @@ -27,8 +24,8 @@ async fn wallet_and_accounts() { #[tokio::test] async fn create_account_and_retrieve_it() { let tc = Cli::default(); - let monero = Monero::new(&tc); - let cli = Client::localhost(monero.miner_wallet_rpc_port); + let (monero, _container) = Monero::new(&tc); + let cli = monero.miner_wallet_rpc_client(); let label = "Iron Man"; // This is intentionally _not_ Alice or Bob. @@ -61,18 +58,20 @@ async fn transfer_and_check_tx_key() { let fund_bob = 0; let tc = Cli::default(); - let monero = Monero::new(&tc); + let (monero, _container) = Monero::new(&tc); let _ = monero.init(fund_alice, fund_bob).await; let address_bob = monero - .get_address_bob() + .bob_wallet_rpc_client() + .get_address(0) .await .expect("failed to get Bob's address") .address; let transfer_amount = 100; let transfer = monero - .transfer_from_alice(transfer_amount, &address_bob) + .alice_wallet_rpc_client() + .transfer(0, transfer_amount, &address_bob) .await .expect("transfer failed"); diff --git a/xmr-btc/tests/e2e.rs b/xmr-btc/tests/e2e.rs index d280f26d..2eb3cf70 100644 --- a/xmr-btc/tests/e2e.rs +++ b/xmr-btc/tests/e2e.rs @@ -59,14 +59,14 @@ pub fn init_alice_and_bob_transports() -> ( (a_transport, b_transport) } -pub async fn init_test<'a>( - monero: &'a Monero<'a>, +pub async fn init_test( + monero: &Monero, bitcoind: &Bitcoind<'_>, ) -> ( alice::State0, bob::State0, - AliceNode<'a>, - BobNode<'a>, + AliceNode, + BobNode, InitialBalances, SwapAmounts, ) { @@ -83,8 +83,8 @@ pub async fn init_test<'a>( 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_monero_wallet = wallet::monero::Wallet(monero.alice_wallet_rpc_client()); + let bob_monero_wallet = wallet::monero::Wallet(monero.bob_wallet_rpc_client()); let alice_btc_wallet = wallet::bitcoin::Wallet::new("alice", &bitcoind.node_url) .await @@ -101,8 +101,8 @@ pub async fn init_test<'a>( 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 alice_initial_xmr_balance = alice.monero_wallet.0.get_balance(0).await.unwrap(); + let bob_initial_xmr_balance = bob.monero_wallet.0.get_balance(0).await.unwrap(); let redeem_address = alice.bitcoin_wallet.new_address().await.unwrap(); let punish_address = redeem_address.clone(); @@ -166,7 +166,7 @@ mod tests { .set_default(); let cli = Cli::default(); - let monero = Monero::new(&cli); + let (monero, _container) = Monero::new(&cli); let bitcoind = init_bitcoind(&cli).await; let ( @@ -207,21 +207,11 @@ mod tests { .await .unwrap(); - let alice_final_xmr_balance = alice_node - .monero_wallet - .0 - .get_balance_alice() - .await - .unwrap(); + let alice_final_xmr_balance = alice_node.monero_wallet.0.get_balance(0).await.unwrap(); - bob_node - .monero_wallet - .0 - .wait_for_bob_wallet_block_height() - .await - .unwrap(); + monero.wait_for_bob_wallet_block_height().await.unwrap(); - let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance_bob().await.unwrap(); + let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance(0).await.unwrap(); assert_eq!( alice_final_btc_balance, @@ -252,7 +242,7 @@ mod tests { .set_default(); let cli = Cli::default(); - let monero = Monero::new(&cli); + let (monero, _container) = Monero::new(&cli); let bitcoind = init_bitcoind(&cli).await; let ( @@ -304,19 +294,9 @@ mod tests { .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(); + monero.wait_for_alice_wallet_block_height().await.unwrap(); + let alice_final_xmr_balance = alice_node.monero_wallet.0.get_balance(0).await.unwrap(); + let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance(0).await.unwrap(); assert_eq!(alice_final_btc_balance, initial_balances.alice_btc); assert_eq!( @@ -338,7 +318,7 @@ mod tests { .set_default(); let cli = Cli::default(); - let monero = Monero::new(&cli); + let (monero, _container) = Monero::new(&cli); let bitcoind = init_bitcoind(&cli).await; let ( diff --git a/xmr-btc/tests/harness/node.rs b/xmr-btc/tests/harness/node.rs index 88ee0a6a..7b4bf207 100644 --- a/xmr-btc/tests/harness/node.rs +++ b/xmr-btc/tests/harness/node.rs @@ -5,18 +5,18 @@ use xmr_btc::{alice, bob}; // TODO: merge this with bob node // This struct is responsible for I/O -pub struct AliceNode<'a> { +pub struct AliceNode { transport: Transport, pub bitcoin_wallet: wallet::bitcoin::Wallet, - pub monero_wallet: wallet::monero::AliceWallet<'a>, + pub monero_wallet: wallet::monero::Wallet, } -impl<'a> AliceNode<'a> { +impl AliceNode { pub fn new( transport: Transport, bitcoin_wallet: wallet::bitcoin::Wallet, - monero_wallet: wallet::monero::AliceWallet<'a>, - ) -> AliceNode<'a> { + monero_wallet: wallet::monero::Wallet, + ) -> AliceNode { Self { transport, bitcoin_wallet, @@ -25,8 +25,8 @@ impl<'a> AliceNode<'a> { } } -pub async fn run_alice_until<'a, R: RngCore + CryptoRng>( - alice: &mut AliceNode<'a>, +pub async fn run_alice_until( + alice: &mut AliceNode, initial_state: alice::State, is_state: fn(&alice::State) -> bool, rng: &mut R, @@ -49,18 +49,18 @@ pub async fn run_alice_until<'a, R: RngCore + CryptoRng>( // TODO: merge this with alice node // This struct is responsible for I/O -pub struct BobNode<'a> { +pub struct BobNode { transport: Transport, pub bitcoin_wallet: wallet::bitcoin::Wallet, - pub monero_wallet: wallet::monero::BobWallet<'a>, + pub monero_wallet: wallet::monero::Wallet, } -impl<'a> BobNode<'a> { +impl BobNode { pub fn new( transport: Transport, bitcoin_wallet: wallet::bitcoin::Wallet, - monero_wallet: wallet::monero::BobWallet<'a>, - ) -> BobNode<'a> { + monero_wallet: wallet::monero::Wallet, + ) -> BobNode { Self { transport, bitcoin_wallet, @@ -69,8 +69,8 @@ impl<'a> BobNode<'a> { } } -pub async fn run_bob_until<'a, R: RngCore + CryptoRng>( - bob: &mut BobNode<'a>, +pub async fn run_bob_until( + bob: &mut BobNode, initial_state: bob::State, is_state: fn(&bob::State) -> bool, rng: &mut R, diff --git a/xmr-btc/tests/harness/wallet/monero.rs b/xmr-btc/tests/harness/wallet/monero.rs index b5991152..5bac71c7 100644 --- a/xmr-btc/tests/harness/wallet/monero.rs +++ b/xmr-btc/tests/harness/wallet/monero.rs @@ -2,18 +2,17 @@ use anyhow::Result; use async_trait::async_trait; use backoff::{future::FutureOperation as _, ExponentialBackoff}; use monero::{Address, Network, PrivateKey}; -use monero_harness::Monero; +use monero_harness::rpc::wallet; use std::str::FromStr; use xmr_btc::monero::{ Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicKey, PublicViewKey, Transfer, TransferProof, TxHash, WatchForTransfer, }; -#[derive(Debug)] -pub struct AliceWallet<'c>(pub &'c Monero<'c>); +pub struct Wallet(pub wallet::Client); #[async_trait] -impl Transfer for AliceWallet<'_> { +impl Transfer for Wallet { async fn transfer( &self, public_spend_key: PublicKey, @@ -25,7 +24,7 @@ impl Transfer for AliceWallet<'_> { let res = self .0 - .transfer_from_alice(amount.as_piconero(), &destination_address.to_string()) + .transfer(0, amount.as_piconero(), &destination_address.to_string()) .await?; let tx_hash = TxHash(res.tx_hash); @@ -38,7 +37,7 @@ impl Transfer for AliceWallet<'_> { } #[async_trait] -impl CreateWalletForOutput for AliceWallet<'_> { +impl CreateWalletForOutput for Wallet { async fn create_and_load_wallet_for_output( &self, private_spend_key: PrivateKey, @@ -51,7 +50,6 @@ impl CreateWalletForOutput for AliceWallet<'_> { let _ = self .0 - .alice_wallet_rpc_client() .generate_from_keys( &address.to_string(), &private_spend_key.to_string(), @@ -63,11 +61,8 @@ impl CreateWalletForOutput for AliceWallet<'_> { } } -#[derive(Debug)] -pub struct BobWallet<'c>(pub &'c Monero<'c>); - #[async_trait] -impl WatchForTransfer for BobWallet<'_> { +impl WatchForTransfer for Wallet { async fn watch_for_transfer( &self, public_spend_key: PublicKey, @@ -82,14 +77,14 @@ impl WatchForTransfer for BobWallet<'_> { InsufficientFunds { expected: Amount, actual: Amount }, } - let wallet = self.0.bob_wallet_rpc_client(); let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key.into()); let res = (|| async { // NOTE: Currently, this is conflating IO errors with the transaction not being // in the blockchain yet, or not having enough confirmations on it. All these // errors warrant a retry, but the strategy should probably differ per case - let proof = wallet + let proof = self + .0 .check_tx_key( &String::from(transfer_proof.tx_hash()), &transfer_proof.tx_key().to_string(), @@ -124,29 +119,3 @@ impl WatchForTransfer for BobWallet<'_> { Ok(()) } } - -#[async_trait] -impl CreateWalletForOutput for BobWallet<'_> { - 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 - .bob_wallet_rpc_client() - .generate_from_keys( - &address.to_string(), - &private_spend_key.to_string(), - &PrivateKey::from(private_view_key).to_string(), - ) - .await?; - - Ok(()) - } -} diff --git a/xmr-btc/tests/on_chain.rs b/xmr-btc/tests/on_chain.rs index d6bcaa86..77fe0410 100644 --- a/xmr-btc/tests/on_chain.rs +++ b/xmr-btc/tests/on_chain.rs @@ -45,7 +45,7 @@ async fn swap_as_alice( // FIXME: It would be more intuitive to have a single network/transport struct instead of // splitting into two, but Rust ownership rules make this tedious mut sender: Sender, - monero_wallet: &'static monero::AliceWallet<'static>, + monero_wallet: &'static monero::Wallet, bitcoin_wallet: &'static bitcoin::Wallet, state: alice::State3, ) -> Result<()> { @@ -86,7 +86,7 @@ async fn swap_as_alice( async fn swap_as_bob( network: &'static mut BobNetwork, mut sender: Sender, - monero_wallet: &'static monero::BobWallet<'static>, + monero_wallet: &'static monero::Wallet, bitcoin_wallet: &'static bitcoin::Wallet, state: bob::State2, ) -> Result<()> { From 40e97ffdc36e485589aa6475c16bb513d06ed93a Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Fri, 16 Oct 2020 12:44:53 +1100 Subject: [PATCH 25/52] Add todo to check amounts calculation --- swap/src/alice.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index aebd9b3f..4fae9003 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -132,6 +132,7 @@ impl Default for Alice { } } +// TODO: Check that this is correct. fn calculate_amounts(btc: bitcoin::Amount) -> SwapParams { const XMR_PER_BTC: u64 = 100; // TODO: Get this from an exchange. From 47eaa44f7676846fbbecd0523e7815c0d1fc6d14 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Fri, 16 Oct 2020 12:46:47 +1100 Subject: [PATCH 26/52] Remove unneeded From impl --- swap/src/alice.rs | 9 +-------- swap/src/bob.rs | 9 +-------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 4fae9003..b61c6e58 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -19,7 +19,7 @@ use crate::{ request_response::{AliceToBob, TIMEOUT}, transport, TokioExecutor, }, - Never, SwapParams, + SwapParams, }; pub type Swarm = libp2p::Swarm; @@ -68,13 +68,6 @@ fn new_swarm(listen: Multiaddr) -> Result { pub enum BehaviourOutEvent { Request(messenger::BehaviourOutEvent), ConnectionEstablished(PeerId), - Never, // FIXME: Why do we need this? -} - -impl From for BehaviourOutEvent { - fn from(_: Never) -> Self { - BehaviourOutEvent::Never - } } impl From for BehaviourOutEvent { diff --git a/swap/src/bob.rs b/swap/src/bob.rs index 33e4520b..8bef0b71 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -19,7 +19,7 @@ use crate::{ request_response::TIMEOUT, transport, TokioExecutor, }, - Cmd, Never, Rsp, + Cmd, Rsp, }; pub async fn swap( @@ -85,13 +85,6 @@ fn new_swarm() -> Result { pub enum BehaviourOutEvent { Response(messenger::BehaviourOutEvent), ConnectionEstablished(PeerId), - Never, // FIXME: Why do we need this? -} - -impl From for BehaviourOutEvent { - fn from(_: Never) -> Self { - BehaviourOutEvent::Never - } } impl From for BehaviourOutEvent { From aaf1363c05fa26119956a06b17bc71440b048488 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Tue, 20 Oct 2020 10:10:28 +1100 Subject: [PATCH 27/52] Refine peer tracker and amounts protocol We model the getting of amounts as a network behaviour even though conceptually it is a protocol. Refine/refactor the code a bit to make this more obvious. - Use `Amounts` instead of `Messenger` We only ever connect to a single peer, update peer tracker to reflect this. This is a single patch because the handling of the two network behaviours is a intertwined. - Only track one peer connection - Track the peer id and the multiaddr of the counterparty - Emit an event for connection established on Alice's side as well as Bob's side --- swap/src/alice.rs | 36 ++++--- swap/src/alice/{messenger.rs => amounts.rs} | 29 ++--- swap/src/bob.rs | 36 +++---- swap/src/bob/{messenger.rs => amounts.rs} | 26 ++--- swap/src/network/peer_tracker.rs | 112 +++++++------------- 5 files changed, 92 insertions(+), 147 deletions(-) rename swap/src/alice/{messenger.rs => amounts.rs} (82%) rename swap/src/bob/{messenger.rs => amounts.rs} (83%) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index b61c6e58..fde9daa3 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -9,9 +9,9 @@ use libp2p::{ use std::time::Duration; use tracing::debug; -mod messenger; +mod amounts; -use self::messenger::*; +use self::amounts::*; use crate::{ bitcoin, monero, network::{ @@ -29,12 +29,14 @@ pub async fn swap(listen: Multiaddr) -> Result<()> { loop { match swarm.next().await { - BehaviourOutEvent::Request(messenger::BehaviourOutEvent::Btc { btc, channel }) => { + OutEvent::ConnectionEstablished(id) => { + tracing::info!("Connection established with: {}", id); + } + OutEvent::Request(amounts::OutEvent::Btc { btc, channel }) => { debug!("Got request from Bob to swap {}", btc); let p = calculate_amounts(btc); swarm.send(channel, AliceToBob::Amounts(p)); } - other => panic!("unexpected event: {:?}", other), } } } @@ -65,22 +67,22 @@ fn new_swarm(listen: Multiaddr) -> Result { #[allow(clippy::large_enum_variant)] #[derive(Debug)] -pub enum BehaviourOutEvent { - Request(messenger::BehaviourOutEvent), +pub enum OutEvent { + Request(amounts::OutEvent), ConnectionEstablished(PeerId), } -impl From for BehaviourOutEvent { - fn from(event: messenger::BehaviourOutEvent) -> Self { - BehaviourOutEvent::Request(event) +impl From for OutEvent { + fn from(event: amounts::OutEvent) -> Self { + OutEvent::Request(event) } } -impl From for BehaviourOutEvent { - fn from(event: peer_tracker::BehaviourOutEvent) -> Self { +impl From for OutEvent { + fn from(event: peer_tracker::OutEvent) -> Self { match event { - peer_tracker::BehaviourOutEvent::ConnectionEstablished(id) => { - BehaviourOutEvent::ConnectionEstablished(id) + peer_tracker::OutEvent::ConnectionEstablished(id) => { + OutEvent::ConnectionEstablished(id) } } } @@ -88,10 +90,10 @@ impl From for BehaviourOutEvent { /// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice. #[derive(NetworkBehaviour)] -#[behaviour(out_event = "BehaviourOutEvent", event_process = false)] +#[behaviour(out_event = "OutEvent", event_process = false)] #[allow(missing_debug_implementations)] pub struct Alice { - net: Messenger, + amounts: Amounts, pt: PeerTracker, #[behaviour(ignore)] identity: Keypair, @@ -108,7 +110,7 @@ impl Alice { /// Alice always sends her messages as a response to a request from Bob. pub fn send(&mut self, channel: ResponseChannel, msg: AliceToBob) { - self.net.send(channel, msg); + self.amounts.send(channel, msg); } } @@ -118,7 +120,7 @@ impl Default for Alice { let timeout = Duration::from_secs(TIMEOUT); Self { - net: Messenger::new(timeout), + amounts: Amounts::new(timeout), pt: PeerTracker::default(), identity, } diff --git a/swap/src/alice/messenger.rs b/swap/src/alice/amounts.rs similarity index 82% rename from swap/src/alice/messenger.rs rename to swap/src/alice/amounts.rs index 62355006..dbe100b7 100644 --- a/swap/src/alice/messenger.rs +++ b/swap/src/alice/amounts.rs @@ -17,11 +17,10 @@ use tracing::{debug, error}; use crate::{ bitcoin, network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}, - Never, }; #[derive(Debug)] -pub enum BehaviourOutEvent { +pub enum OutEvent { Btc { btc: bitcoin::Amount, channel: ResponseChannel, @@ -30,15 +29,15 @@ pub enum BehaviourOutEvent { /// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob. #[derive(NetworkBehaviour)] -#[behaviour(out_event = "BehaviourOutEvent", poll_method = "poll")] +#[behaviour(out_event = "OutEvent", poll_method = "poll")] #[allow(missing_debug_implementations)] -pub struct Messenger { +pub struct Amounts { rr: RequestResponse, #[behaviour(ignore)] - events: VecDeque, + events: VecDeque, } -impl Messenger { +impl Amounts { pub fn new(timeout: Duration) -> Self { let mut config = RequestResponseConfig::default(); config.set_request_timeout(timeout); @@ -74,7 +73,7 @@ impl Messenger { &mut self, _: &mut Context<'_>, _: &mut impl PollParameters, - ) -> Poll, BehaviourOutEvent>> { + ) -> Poll, OutEvent>> { if let Some(event) = self.events.pop_front() { return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event)); } @@ -83,7 +82,7 @@ impl Messenger { } } -impl NetworkBehaviourEventProcess> for Messenger { +impl NetworkBehaviourEventProcess> for Amounts { fn inject_event(&mut self, event: RequestResponseEvent) { match event { RequestResponseEvent::Message { @@ -95,9 +94,9 @@ impl NetworkBehaviourEventProcess> channel, }, } => match request { - BobToAlice::AmountsFromBtc(btc) => self - .events - .push_back(BehaviourOutEvent::Btc { btc, channel }), + BobToAlice::AmountsFromBtc(btc) => { + self.events.push_back(OutEvent::Btc { btc, channel }) + } _ => panic!("unexpected request"), }, RequestResponseEvent::Message { @@ -125,11 +124,3 @@ impl NetworkBehaviourEventProcess> } } } - -impl libp2p::swarm::NetworkBehaviourEventProcess<()> for Messenger { - fn inject_event(&mut self, _event: ()) {} -} - -impl libp2p::swarm::NetworkBehaviourEventProcess for Messenger { - fn inject_event(&mut self, _: Never) {} -} diff --git a/swap/src/bob.rs b/swap/src/bob.rs index 8bef0b71..da1403bc 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -9,9 +9,9 @@ use libp2p::{core::identity::Keypair, Multiaddr, NetworkBehaviour, PeerId}; use std::{process, thread, time::Duration}; use tracing::{debug, info, warn}; -mod messenger; +mod amounts; -use self::messenger::*; +use self::amounts::*; use crate::{ bitcoin, network::{ @@ -32,7 +32,7 @@ pub async fn swap( libp2p::Swarm::dial_addr(&mut swarm, addr)?; let id = match swarm.next().await { - BehaviourOutEvent::ConnectionEstablished(id) => id, + OutEvent::ConnectionEstablished(id) => id, other => panic!("unexpected event: {:?}", other), }; info!("Connection established."); @@ -40,7 +40,7 @@ pub async fn swap( swarm.request_amounts(id, btc).await; match swarm.next().await { - BehaviourOutEvent::Response(messenger::BehaviourOutEvent::Amounts(p)) => { + OutEvent::Response(amounts::OutEvent::Amounts(p)) => { debug!("Got response from Alice: {:?}", p); let cmd = Cmd::VerifyAmounts(p); cmd_tx.try_send(cmd)?; @@ -82,22 +82,22 @@ fn new_swarm() -> Result { #[allow(clippy::large_enum_variant)] #[derive(Debug)] -pub enum BehaviourOutEvent { - Response(messenger::BehaviourOutEvent), +pub enum OutEvent { + Response(amounts::OutEvent), ConnectionEstablished(PeerId), } -impl From for BehaviourOutEvent { - fn from(event: messenger::BehaviourOutEvent) -> Self { - BehaviourOutEvent::Response(event) +impl From for OutEvent { + fn from(event: amounts::OutEvent) -> Self { + OutEvent::Response(event) } } -impl From for BehaviourOutEvent { - fn from(event: peer_tracker::BehaviourOutEvent) -> Self { +impl From for OutEvent { + fn from(event: peer_tracker::OutEvent) -> Self { match event { - peer_tracker::BehaviourOutEvent::ConnectionEstablished(id) => { - BehaviourOutEvent::ConnectionEstablished(id) + peer_tracker::OutEvent::ConnectionEstablished(id) => { + OutEvent::ConnectionEstablished(id) } } } @@ -105,10 +105,10 @@ impl From for BehaviourOutEvent { /// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob. #[derive(NetworkBehaviour)] -#[behaviour(out_event = "BehaviourOutEvent", event_process = false)] +#[behaviour(out_event = "OutEvent", event_process = false)] #[allow(missing_debug_implementations)] pub struct Bob { - net: Messenger, + amounts: Amounts, pt: PeerTracker, #[behaviour(ignore)] identity: Keypair, @@ -126,13 +126,13 @@ impl Bob { /// Sends a message to Alice to get current amounts based on `btc`. pub async fn request_amounts(&mut self, alice: PeerId, btc: u64) { let btc = bitcoin::Amount::from_sat(btc); - let _id = self.net.request_amounts(alice.clone(), btc).await; + let _id = self.amounts.request_amounts(alice.clone(), btc).await; debug!("Requesting amounts from: {}", alice); } /// Returns Alice's peer id if we are connected. pub fn peer_id_of_alice(&self) -> Option { - self.pt.counterparty() + self.pt.counterparty_peer_id() } } @@ -142,7 +142,7 @@ impl Default for Bob { let timeout = Duration::from_secs(TIMEOUT); Self { - net: Messenger::new(timeout), + amounts: Amounts::new(timeout), pt: PeerTracker::default(), identity, } diff --git a/swap/src/bob/messenger.rs b/swap/src/bob/amounts.rs similarity index 83% rename from swap/src/bob/messenger.rs rename to swap/src/bob/amounts.rs index 6d57804e..527fdb32 100644 --- a/swap/src/bob/messenger.rs +++ b/swap/src/bob/amounts.rs @@ -17,25 +17,25 @@ use tracing::error; use crate::{ bitcoin, network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}, - Never, SwapParams, + SwapParams, }; #[derive(Debug)] -pub enum BehaviourOutEvent { +pub enum OutEvent { Amounts(SwapParams), } /// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob. #[derive(NetworkBehaviour)] -#[behaviour(out_event = "BehaviourOutEvent", poll_method = "poll")] +#[behaviour(out_event = "OutEvent", poll_method = "poll")] #[allow(missing_debug_implementations)] -pub struct Messenger { +pub struct Amounts { rr: RequestResponse, #[behaviour(ignore)] - events: VecDeque, + events: VecDeque, } -impl Messenger { +impl Amounts { pub fn new(timeout: Duration) -> Self { let mut config = RequestResponseConfig::default(); config.set_request_timeout(timeout); @@ -65,7 +65,7 @@ impl Messenger { &mut self, _: &mut Context<'_>, _: &mut impl PollParameters, - ) -> Poll, BehaviourOutEvent>> { + ) -> Poll, OutEvent>> { if let Some(event) = self.events.pop_front() { return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event)); } @@ -74,7 +74,7 @@ impl Messenger { } } -impl NetworkBehaviourEventProcess> for Messenger { +impl NetworkBehaviourEventProcess> for Amounts { fn inject_event(&mut self, event: RequestResponseEvent) { match event { RequestResponseEvent::Message { @@ -89,7 +89,7 @@ impl NetworkBehaviourEventProcess> request_id: _, }, } => match response { - AliceToBob::Amounts(p) => self.events.push_back(BehaviourOutEvent::Amounts(p)), + AliceToBob::Amounts(p) => self.events.push_back(OutEvent::Amounts(p)), }, RequestResponseEvent::InboundFailure { .. } => { @@ -105,11 +105,3 @@ impl NetworkBehaviourEventProcess> } } } - -impl libp2p::swarm::NetworkBehaviourEventProcess<()> for Messenger { - fn inject_event(&mut self, _event: ()) {} -} - -impl libp2p::swarm::NetworkBehaviourEventProcess for Messenger { - fn inject_event(&mut self, _: Never) {} -} diff --git a/swap/src/network/peer_tracker.rs b/swap/src/network/peer_tracker.rs index 7b76563f..ca3a67e5 100644 --- a/swap/src/network/peer_tracker.rs +++ b/swap/src/network/peer_tracker.rs @@ -7,85 +7,56 @@ use libp2p::{ }, Multiaddr, PeerId, }; -use std::{ - collections::{hash_map::Entry, HashMap, VecDeque}, - task::Poll, -}; +use std::{collections::VecDeque, task::Poll}; #[derive(Debug)] -pub enum BehaviourOutEvent { +pub enum OutEvent { ConnectionEstablished(PeerId), } -/// A NetworkBehaviour that tracks connections to other peers. +/// A NetworkBehaviour that tracks connections to the counterparty. Although the +/// libp2p `NetworkBehaviour` abstraction encompasses connections to multiple +/// peers we only ever connect to a single counterparty. Peer Tracker tracks +/// that connection. #[derive(Default, Debug)] pub struct PeerTracker { - connected_peers: HashMap>, - address_hints: HashMap>, - events: VecDeque, + connected: Option<(PeerId, Multiaddr)>, + events: VecDeque, } impl PeerTracker { /// Returns an arbitrary connected counterparty. /// This is useful if we are connected to a single other node. - pub fn counterparty(&self) -> Option { - // TODO: Refactor to use combinators. - if let Some((id, _)) = self.connected_peers().next() { - return Some(id); + pub fn counterparty_peer_id(&self) -> Option { + if let Some((id, _)) = &self.connected { + return Some(id.clone()); } None } - pub fn connected_peers(&self) -> impl Iterator)> { - self.connected_peers.clone().into_iter() - } - - /// Adds an address hint for the given peer id. The added address is - /// considered most recent and hence is added at the start of the list - /// because libp2p tries to connect with the first address first. - pub fn add_recent_address_hint(&mut self, id: PeerId, addr: Multiaddr) { - let old_addresses = self.address_hints.get_mut(&id); - - match old_addresses { - None => { - let mut hints = VecDeque::new(); - hints.push_back(addr); - self.address_hints.insert(id, hints); - } - Some(hints) => { - hints.push_front(addr); - } + /// Returns an arbitrary connected counterparty. + /// This is useful if we are connected to a single other node. + pub fn counterparty_addr(&self) -> Option { + if let Some((_, addr)) = &self.connected { + return Some(addr.clone()); } + None } } impl NetworkBehaviour for PeerTracker { type ProtocolsHandler = DummyProtocolsHandler; - type OutEvent = BehaviourOutEvent; + type OutEvent = OutEvent; fn new_handler(&mut self) -> Self::ProtocolsHandler { DummyProtocolsHandler::default() } - /// Note (from libp2p doc): - /// The addresses will be tried in the order returned by this function, - /// which means that they should be ordered by decreasing likelihood of - /// reachability. In other words, the first address should be the most - /// likely to be reachable. - fn addresses_of_peer(&mut self, peer: &PeerId) -> Vec { + fn addresses_of_peer(&mut self, _: &PeerId) -> Vec { let mut addresses: Vec = vec![]; - // If we are connected then this address is most likely to be valid - if let Some(connected) = self.connected_peers.get(peer) { - for addr in connected.iter() { - addresses.push(addr.clone()) - } - } - - if let Some(hints) = self.address_hints.get(peer) { - for hint in hints { - addresses.push(hint.clone()); - } + if let Some(addr) = self.counterparty_addr() { + addresses.push(addr) } addresses @@ -101,35 +72,24 @@ impl NetworkBehaviour for PeerTracker { _: &ConnectionId, point: &ConnectedPoint, ) { - if let ConnectedPoint::Dialer { address } = point { - self.connected_peers - .entry(peer.clone()) - .or_default() - .push(address.clone()); - - self.events - .push_back(BehaviourOutEvent::ConnectionEstablished(peer.clone())); + match point { + ConnectedPoint::Dialer { address } => { + self.connected = Some((peer.clone(), address.clone())); + } + ConnectedPoint::Listener { + local_addr: _, + send_back_addr, + } => { + self.connected = Some((peer.clone(), send_back_addr.clone())); + } } + + self.events + .push_back(OutEvent::ConnectionEstablished(peer.clone())); } - fn inject_connection_closed( - &mut self, - peer: &PeerId, - _: &ConnectionId, - point: &ConnectedPoint, - ) { - if let ConnectedPoint::Dialer { address } = point { - match self.connected_peers.entry(peer.clone()) { - Entry::Vacant(_) => {} - Entry::Occupied(mut entry) => { - let addresses = entry.get_mut(); - - if let Some(pos) = addresses.iter().position(|a| a == address) { - addresses.remove(pos); - } - } - } - } + fn inject_connection_closed(&mut self, _: &PeerId, _: &ConnectionId, _: &ConnectedPoint) { + self.connected = None; } fn inject_event(&mut self, _: PeerId, _: ConnectionId, _: void::Void) {} From 08ec776daa8d6ab3203eaebe7139d3168ed99d3e Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Wed, 21 Oct 2020 13:01:10 +1100 Subject: [PATCH 28/52] Move monero/bitcoin modules to files --- swap/Cargo.toml | 2 +- swap/src/bitcoin.rs | 1 + swap/src/lib.rs | 71 ++-------------------------- swap/src/monero.rs | 30 ++++++++++++ swap/src/network/request_response.rs | 1 + 5 files changed, 36 insertions(+), 69 deletions(-) create mode 100644 swap/src/bitcoin.rs create mode 100644 swap/src/monero.rs diff --git a/swap/Cargo.toml b/swap/Cargo.toml index cb22e3d1..aa9973df 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -9,7 +9,7 @@ description = "XMR/BTC trustless atomic swaps." anyhow = "1" async-trait = "0.1" atty = "0.2" -bitcoin = "0.25" # TODO: Upgrade other crates in this repo to use this version. +bitcoin = { version = "0.25", features = ["rand", "use-serde"] } # TODO: Upgrade other crates in this repo to use this version. derivative = "2" futures = { version = "0.3", default-features = false } libp2p = { version = "0.28", default-features = false, features = ["tcp-tokio", "yamux", "mplex", "dns", "noise", "request-response"] } diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs new file mode 100644 index 00000000..70bed6cf --- /dev/null +++ b/swap/src/bitcoin.rs @@ -0,0 +1 @@ +pub use bitcoin::Amount; diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 11f688d6..9fded336 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -2,7 +2,9 @@ use serde::{Deserialize, Serialize}; use std::fmt::{self, Display}; pub mod alice; +pub mod bitcoin; pub mod bob; +pub mod monero; pub mod network; pub const ONE_BTC: u64 = 100_000_000; @@ -26,6 +28,7 @@ pub enum Rsp { #[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub struct SwapParams { /// Amount of BTC to swap. + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] pub btc: bitcoin::Amount, /// Amount of XMR to swap. pub xmr: monero::Amount, @@ -36,71 +39,3 @@ impl Display for SwapParams { write!(f, "{} for {}", self.btc, self.xmr) } } - -// FIXME: Amount modules are a quick hack so we can derive serde. - -pub mod monero { - use serde::{Deserialize, Serialize}; - use std::fmt; - - #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] - pub struct Amount(u64); - - impl Amount { - /// Create an [Amount] with piconero precision and the given number of - /// piconeros. - /// - /// A piconero (a.k.a atomic unit) is equal to 1e-12 XMR. - pub fn from_piconero(amount: u64) -> Self { - Amount(amount) - } - pub fn as_piconero(&self) -> u64 { - self.0 - } - } - - impl From for u64 { - fn from(from: Amount) -> u64 { - from.0 - } - } - - impl fmt::Display for Amount { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} piconeros", self.0) - } - } -} - -pub mod bitcoin { - use serde::{Deserialize, Serialize}; - use std::fmt; - - #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] - pub struct Amount(u64); - - impl Amount { - /// The zero amount. - pub const ZERO: Amount = Amount(0); - /// Exactly one satoshi. - pub const ONE_SAT: Amount = Amount(1); - /// Exactly one bitcoin. - pub const ONE_BTC: Amount = Amount(100_000_000); - - /// Create an [Amount] with satoshi precision and the given number of - /// satoshis. - pub fn from_sat(satoshi: u64) -> Amount { - Amount(satoshi) - } - - pub fn as_sat(&self) -> u64 { - self.0 - } - } - - impl fmt::Display for Amount { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} satoshis", self.0) - } - } -} diff --git a/swap/src/monero.rs b/swap/src/monero.rs new file mode 100644 index 00000000..940a1922 --- /dev/null +++ b/swap/src/monero.rs @@ -0,0 +1,30 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct Amount(u64); + +impl Amount { + /// Create an [Amount] with piconero precision and the given number of + /// piconeros. + /// + /// A piconero (a.k.a atomic unit) is equal to 1e-12 XMR. + pub fn from_piconero(amount: u64) -> Self { + Amount(amount) + } + pub fn as_piconero(&self) -> u64 { + self.0 + } +} + +impl From for u64 { + fn from(from: Amount) -> u64 { + from.0 + } +} + +impl fmt::Display for Amount { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} piconeros", self.0) + } +} diff --git a/swap/src/network/request_response.rs b/swap/src/network/request_response.rs index c986e2a9..deb19b73 100644 --- a/swap/src/network/request_response.rs +++ b/swap/src/network/request_response.rs @@ -15,6 +15,7 @@ pub const TIMEOUT: u64 = 3600; // One hour. /// Messages Bob sends to Alice. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum BobToAlice { + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] AmountsFromBtc(bitcoin::Amount), AmountsFromXmr(monero::Amount), /* TODO: How are we going to do this when the messages are not Clone? From f22729c5dac7dfa9cc09a0a58dfd48a95c5dc18d Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Wed, 21 Oct 2020 15:02:37 +1100 Subject: [PATCH 29/52] Derive Clone/ on initial states and messages --- xmr-btc/src/alice.rs | 4 ++-- xmr-btc/src/alice/message.rs | 6 +++--- xmr-btc/src/bob.rs | 4 ++-- xmr-btc/src/bob/message.rs | 10 +++++----- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index a8a1ea20..62d6385f 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -129,7 +129,7 @@ impl State { } } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct State0 { a: bitcoin::SecretKey, s_a: cross_curve_dleq::Scalar, @@ -215,7 +215,7 @@ impl State0 { } } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct State1 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, diff --git a/xmr-btc/src/alice/message.rs b/xmr-btc/src/alice/message.rs index 7c95604b..052731b1 100644 --- a/xmr-btc/src/alice/message.rs +++ b/xmr-btc/src/alice/message.rs @@ -11,7 +11,7 @@ pub enum Message { Message2(Message2), } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Message0 { pub(crate) A: bitcoin::PublicKey, pub(crate) S_a_monero: monero::PublicKey, @@ -22,13 +22,13 @@ pub struct Message0 { pub(crate) punish_address: bitcoin::Address, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Message1 { pub(crate) tx_cancel_sig: Signature, pub(crate) tx_refund_encsig: EncryptedSignature, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Message2 { pub(crate) tx_lock_proof: monero::TransferProof, } diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 55099ed6..2087a142 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -102,7 +102,7 @@ impl_from_child_enum!(State3, State); impl_from_child_enum!(State4, State); impl_from_child_enum!(State5, State); -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct State0 { b: bitcoin::SecretKey, s_b: cross_curve_dleq::Scalar, @@ -190,7 +190,7 @@ impl State0 { } } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct State1 { A: bitcoin::PublicKey, b: bitcoin::SecretKey, diff --git a/xmr-btc/src/bob/message.rs b/xmr-btc/src/bob/message.rs index de02b7a5..f45c3b92 100644 --- a/xmr-btc/src/bob/message.rs +++ b/xmr-btc/src/bob/message.rs @@ -3,7 +3,7 @@ use anyhow::Result; use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; use std::convert::TryFrom; -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum Message { Message0(Message0), Message1(Message1), @@ -11,7 +11,7 @@ pub enum Message { Message3(Message3), } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Message0 { pub(crate) B: bitcoin::PublicKey, pub(crate) S_b_monero: monero::PublicKey, @@ -21,18 +21,18 @@ pub struct Message0 { pub(crate) refund_address: bitcoin::Address, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Message1 { pub(crate) tx_lock: bitcoin::TxLock, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Message2 { pub(crate) tx_punish_sig: Signature, pub(crate) tx_cancel_sig: Signature, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Message3 { pub(crate) tx_redeem_encsig: EncryptedSignature, } From e7504de76c700b50b8a91a8e763b3ccfb8a20232 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Wed, 21 Oct 2020 14:41:50 +1100 Subject: [PATCH 30/52] Add state0 -> state1 messages --- swap/Cargo.toml | 6 +- swap/src/alice.rs | 94 ++++++++++++++++---- swap/src/alice/amounts.rs | 11 +-- swap/src/alice/message0.rs | 125 +++++++++++++++++++++++++++ swap/src/bitcoin.rs | 115 +++++++++++++++++++++++- swap/src/bob.rs | 99 ++++++++++++++++----- swap/src/bob/amounts.rs | 9 +- swap/src/bob/message0.rs | 97 +++++++++++++++++++++ swap/src/lib.rs | 15 +++- swap/src/main.rs | 26 ++++-- swap/src/monero.rs | 43 +++++---- swap/src/network/request_response.rs | 11 ++- 12 files changed, 556 insertions(+), 95 deletions(-) create mode 100644 swap/src/alice/message0.rs create mode 100644 swap/src/bob/message0.rs diff --git a/swap/Cargo.toml b/swap/Cargo.toml index aa9973df..2c217182 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -9,7 +9,10 @@ description = "XMR/BTC trustless atomic swaps." anyhow = "1" async-trait = "0.1" atty = "0.2" -bitcoin = { version = "0.25", features = ["rand", "use-serde"] } # TODO: Upgrade other crates in this repo to use this version. +backoff = { version = "0.2", features = ["tokio"] } +base64 = "0.12" +bitcoin = { version = "0.23", features = ["rand", "use-serde"] } # TODO: Upgrade other crates in this repo to use this version. +bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "d402b36d3d6406150e3bfb71492ff4a0a7cb290e" } derivative = "2" futures = { version = "0.3", default-features = false } libp2p = { version = "0.28", default-features = false, features = ["tcp-tokio", "yamux", "mplex", "dns", "noise", "request-response"] } @@ -17,6 +20,7 @@ libp2p-tokio-socks5 = "0.3" log = { version = "0.4", features = ["serde"] } monero = "0.9" rand = "0.7" +reqwest = { version = "0.10", default-features = false } serde = { version = "1", features = ["derive"] } serde_derive = "1.0" serde_json = "1" diff --git a/swap/src/alice.rs b/swap/src/alice.rs index fde9daa3..734cbed5 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -6,25 +6,35 @@ use libp2p::{ request_response::ResponseChannel, NetworkBehaviour, PeerId, }; -use std::time::Duration; +use rand::{CryptoRng, RngCore}; +use std::{thread, time::Duration}; use tracing::debug; mod amounts; +mod message0; -use self::amounts::*; +use self::{amounts::*, message0::*}; use crate::{ - bitcoin, monero, network::{ peer_tracker::{self, PeerTracker}, request_response::{AliceToBob, TIMEOUT}, transport, TokioExecutor, }, - SwapParams, + SwapParams, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; +use xmr_btc::{alice::State0, bob, monero}; pub type Swarm = libp2p::Swarm; -pub async fn swap(listen: Multiaddr) -> Result<()> { +pub async fn swap( + listen: Multiaddr, + rng: &mut R, + redeem_address: ::bitcoin::Address, + punish_address: ::bitcoin::Address, +) -> Result<()> { + let mut message0: Option = None; + let mut last_amounts: Option = None; + let mut swarm = new_swarm(listen)?; loop { @@ -35,10 +45,47 @@ pub async fn swap(listen: Multiaddr) -> Result<()> { OutEvent::Request(amounts::OutEvent::Btc { btc, channel }) => { debug!("Got request from Bob to swap {}", btc); let p = calculate_amounts(btc); + last_amounts = Some(p); swarm.send(channel, AliceToBob::Amounts(p)); } - } + OutEvent::Message0(msg) => { + debug!("Got message0 from Bob"); + // TODO: Do this in a more Rusty/functional way. + message0 = Some(msg); + break; + } + other => panic!("unexpected event: {:?}", other), + }; } + + let (xmr, btc) = match last_amounts { + Some(p) => (p.xmr, p.btc), + None => unreachable!("should have amounts by here"), + }; + + // FIXME: Too many `bitcoin` crates/modules. + let xmr = monero::Amount::from_piconero(xmr.as_piconero()); + let btc = ::bitcoin::Amount::from_sat(btc.as_sat()); + + let state0 = State0::new( + rng, + btc, + xmr, + REFUND_TIMELOCK, + PUNISH_TIMELOCK, + redeem_address, + punish_address, + ); + swarm.set_state0(state0.clone()); + + let state1 = match message0 { + Some(msg) => state0.receive(msg), + None => unreachable!("should have msg by here"), + }; + + tracing::warn!("parking thread ..."); + thread::park(); + Ok(()) } fn new_swarm(listen: Multiaddr) -> Result { @@ -68,8 +115,19 @@ fn new_swarm(listen: Multiaddr) -> Result { #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum OutEvent { - Request(amounts::OutEvent), ConnectionEstablished(PeerId), + Request(amounts::OutEvent), + Message0(bob::Message0), +} + +impl From for OutEvent { + fn from(event: peer_tracker::OutEvent) -> Self { + match event { + peer_tracker::OutEvent::ConnectionEstablished(id) => { + OutEvent::ConnectionEstablished(id) + } + } + } } impl From for OutEvent { @@ -78,12 +136,10 @@ impl From for OutEvent { } } -impl From for OutEvent { - fn from(event: peer_tracker::OutEvent) -> Self { +impl From for OutEvent { + fn from(event: message0::OutEvent) -> Self { match event { - peer_tracker::OutEvent::ConnectionEstablished(id) => { - OutEvent::ConnectionEstablished(id) - } + message0::OutEvent::Msg(msg) => OutEvent::Message0(msg), } } } @@ -93,8 +149,9 @@ impl From for OutEvent { #[behaviour(out_event = "OutEvent", event_process = false)] #[allow(missing_debug_implementations)] pub struct Alice { - amounts: Amounts, pt: PeerTracker, + amounts: Amounts, + message0: Message0, #[behaviour(ignore)] identity: Keypair, } @@ -112,6 +169,10 @@ impl Alice { pub fn send(&mut self, channel: ResponseChannel, msg: AliceToBob) { self.amounts.send(channel, msg); } + + pub fn set_state0(&mut self, state: State0) { + self.message0.set_state(state); + } } impl Default for Alice { @@ -120,15 +181,16 @@ impl Default for Alice { let timeout = Duration::from_secs(TIMEOUT); Self { - amounts: Amounts::new(timeout), pt: PeerTracker::default(), + amounts: Amounts::new(timeout), + message0: Message0::new(timeout), identity, } } } // TODO: Check that this is correct. -fn calculate_amounts(btc: bitcoin::Amount) -> SwapParams { +fn calculate_amounts(btc: ::bitcoin::Amount) -> SwapParams { const XMR_PER_BTC: u64 = 100; // TODO: Get this from an exchange. // XMR uses 12 zerose BTC uses 8. @@ -147,7 +209,7 @@ mod tests { #[test] fn one_bitcoin_equals_a_hundred_moneroj() { - let btc = bitcoin::Amount::from_sat(ONE_BTC); + let btc = ::bitcoin::Amount::from_sat(ONE_BTC); let want = monero::Amount::from_piconero(HUNDRED_XMR); let SwapParams { xmr: got, .. } = calculate_amounts(btc); diff --git a/swap/src/alice/amounts.rs b/swap/src/alice/amounts.rs index dbe100b7..fb4872bc 100644 --- a/swap/src/alice/amounts.rs +++ b/swap/src/alice/amounts.rs @@ -14,20 +14,17 @@ use std::{ }; use tracing::{debug, error}; -use crate::{ - bitcoin, - network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}, -}; +use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}; #[derive(Debug)] pub enum OutEvent { Btc { - btc: bitcoin::Amount, + btc: ::bitcoin::Amount, channel: ResponseChannel, }, } -/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob. +/// A `NetworkBehaviour` that represents getting the amounts of an XMR/BTC swap. #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", poll_method = "poll")] #[allow(missing_debug_implementations)] @@ -60,7 +57,7 @@ impl Amounts { pub async fn request_amounts( &mut self, alice: PeerId, - btc: bitcoin::Amount, + btc: ::bitcoin::Amount, ) -> Result { let msg = BobToAlice::AmountsFromBtc(btc); let id = self.rr.send_request(&alice, msg); diff --git a/swap/src/alice/message0.rs b/swap/src/alice/message0.rs new file mode 100644 index 00000000..c1631bb9 --- /dev/null +++ b/swap/src/alice/message0.rs @@ -0,0 +1,125 @@ +use anyhow::{bail, Result}; +use libp2p::{ + request_response::{ + handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig, + RequestResponseEvent, RequestResponseMessage, + }, + swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters}, + NetworkBehaviour, +}; +use rand::rngs::OsRng; +use std::{ + collections::VecDeque, + task::{Context, Poll}, + time::Duration, +}; +use tracing::error; + +use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}; +use xmr_btc::{alice::State0, bob}; + +#[derive(Debug)] +pub enum OutEvent { + Msg(bob::Message0), +} + +/// A `NetworkBehaviour` that represents getting the amounts of an XMR/BTC swap. +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "OutEvent", poll_method = "poll")] +#[allow(missing_debug_implementations)] +pub struct Message0 { + rr: RequestResponse, + #[behaviour(ignore)] + events: VecDeque, + #[behaviour(ignore)] + state: Option, +} + +impl Message0 { + pub fn new(timeout: Duration) -> Self { + let mut config = RequestResponseConfig::default(); + config.set_request_timeout(timeout); + + Self { + rr: RequestResponse::new( + Codec::default(), + vec![(Protocol, ProtocolSupport::Full)], + config, + ), + events: Default::default(), + state: None, + } + } + + pub fn set_state(&mut self, state: State0) -> Result<()> { + if self.state.is_some() { + bail!("Trying to set state a second time"); + } + self.state = Some(state); + + Ok(()) + } + + fn poll( + &mut self, + _: &mut Context<'_>, + _: &mut impl PollParameters, + ) -> Poll, OutEvent>> { + if let Some(event) = self.events.pop_front() { + return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event)); + } + + Poll::Pending + } +} + +impl NetworkBehaviourEventProcess> for Message0 { + fn inject_event(&mut self, event: RequestResponseEvent) { + match event { + RequestResponseEvent::Message { + peer: _, + message: + RequestResponseMessage::Request { + request, + request_id: _, + channel, + }, + } => match request { + BobToAlice::Message0(msg) => { + let response = match self.state { + None => panic!("No state, did you forget to set it?"), + Some(state) => { + // TODO: Get OsRng from somewhere? + AliceToBob::Message0(state.next_message(&mut OsRng)) + } + }; + self.rr.send_response(channel, response); + self.events.push_back(OutEvent::Msg(msg)); + } + _ => panic!("unexpected request"), + }, + RequestResponseEvent::Message { + peer: _, + message: + RequestResponseMessage::Response { + response: _, + request_id: _, + }, + } => panic!("unexpected response"), + RequestResponseEvent::InboundFailure { + peer: _, + request_id: _, + error, + } => { + error!("Inbound failure: {:?}", error); + } + RequestResponseEvent::OutboundFailure { + peer: _, + request_id: _, + error, + } => { + error!("Outbound failure: {:?}", error); + } + } + } +} diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 70bed6cf..1b3495ea 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -1 +1,114 @@ -pub use bitcoin::Amount; +use anyhow::Result; +use async_trait::async_trait; +use backoff::{future::FutureOperation as _, ExponentialBackoff}; +use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid}; +use bitcoin_harness::bitcoind_rpc::PsbtBase64; +use reqwest::Url; +use xmr_btc::{ + bitcoin::{ + BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxLock, WatchForRawTransaction, + }, + MedianTime, +}; + +#[derive(Debug)] +pub struct Wallet(pub bitcoin_harness::Wallet); + +impl Wallet { + pub async fn new(name: &str, url: &Url) -> Result { + let wallet = bitcoin_harness::Wallet::new(name, url.clone()).await?; + + Ok(Self(wallet)) + } + + pub async fn balance(&self) -> Result { + let balance = self.0.balance().await?; + Ok(balance) + } + + pub async fn new_address(&self) -> Result
{ + self.0.new_address().await.map_err(Into::into) + } + + pub async fn transaction_fee(&self, txid: Txid) -> Result { + let fee = self + .0 + .get_wallet_transaction(txid) + .await + .map(|res| bitcoin::Amount::from_btc(-res.fee))??; + + // FIXME: Handle re-export of bitcoin::Amount correctly. + let fee = Amount::from_sat(fee.as_sat()); + Ok(fee) + } +} + +#[async_trait] +impl BuildTxLockPsbt for Wallet { + async fn build_tx_lock_psbt( + &self, + output_address: Address, + output_amount: Amount, + ) -> Result { + let psbt = self.0.fund_psbt(output_address, output_amount).await?; + let as_hex = base64::decode(psbt)?; + + let psbt = bitcoin::consensus::deserialize(&as_hex)?; + + Ok(psbt) + } +} + +#[async_trait] +impl SignTxLock for Wallet { + async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result { + let psbt = PartiallySignedTransaction::from(tx_lock); + + let psbt = bitcoin::consensus::serialize(&psbt); + let as_base64 = base64::encode(psbt); + + let psbt = self.0.wallet_process_psbt(PsbtBase64(as_base64)).await?; + let PsbtBase64(signed_psbt) = PsbtBase64::from(psbt); + + let as_hex = base64::decode(signed_psbt)?; + let psbt: PartiallySignedTransaction = bitcoin::consensus::deserialize(&as_hex)?; + + let tx = psbt.extract_tx(); + + Ok(tx) + } +} + +#[async_trait] +impl BroadcastSignedTransaction for Wallet { + async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result { + let txid = self.0.send_raw_transaction(transaction).await?; + Ok(txid) + } +} + +#[async_trait] +impl WatchForRawTransaction for Wallet { + async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction { + (|| async { Ok(self.0.get_raw_transaction(txid).await?) }) + .retry(ExponentialBackoff { + max_elapsed_time: None, + ..Default::default() + }) + .await + .expect("transient errors to be retried") + } +} + +#[async_trait] +impl MedianTime for Wallet { + async fn median_time(&self) -> u32 { + (|| async { Ok(self.0.median_time().await?) }) + .retry(ExponentialBackoff { + max_elapsed_time: None, + ..Default::default() + }) + .await + .expect("transient errors to be retried") + } +} diff --git a/swap/src/bob.rs b/swap/src/bob.rs index da1403bc..41db2b36 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -6,42 +6,55 @@ use futures::{ StreamExt, }; use libp2p::{core::identity::Keypair, Multiaddr, NetworkBehaviour, PeerId}; +use rand::{CryptoRng, RngCore}; use std::{process, thread, time::Duration}; use tracing::{debug, info, warn}; mod amounts; +mod message0; -use self::amounts::*; +use self::{amounts::*, message0::*}; use crate::{ - bitcoin, network::{ peer_tracker::{self, PeerTracker}, request_response::TIMEOUT, transport, TokioExecutor, }, - Cmd, Rsp, + Cmd, Rsp, PUNISH_TIMELOCK, REFUND_TIMELOCK, +}; +use xmr_btc::{ + alice, + bitcoin::BuildTxLockPsbt, + bob::{self, State0}, }; -pub async fn swap( +pub async fn swap( btc: u64, addr: Multiaddr, mut cmd_tx: Sender, mut rsp_rx: Receiver, -) -> Result<()> { + rng: &mut R, + refund_address: ::bitcoin::Address, + wallet: &W, +) -> Result<()> +where + W: BuildTxLockPsbt, + R: RngCore + CryptoRng, +{ let mut swarm = new_swarm()?; libp2p::Swarm::dial_addr(&mut swarm, addr)?; - let id = match swarm.next().await { - OutEvent::ConnectionEstablished(id) => id, + let alice = match swarm.next().await { + OutEvent::ConnectionEstablished(alice) => alice, other => panic!("unexpected event: {:?}", other), }; info!("Connection established."); - swarm.request_amounts(id, btc).await; + swarm.request_amounts(alice.clone(), btc); - match swarm.next().await { - OutEvent::Response(amounts::OutEvent::Amounts(p)) => { - debug!("Got response from Alice: {:?}", p); + let (btc, xmr) = match swarm.next().await { + OutEvent::Amounts(amounts::OutEvent::Amounts(p)) => { + debug!("Got amounts from Alice: {:?}", p); let cmd = Cmd::VerifyAmounts(p); cmd_tx.try_send(cmd)?; let response = rsp_rx.next().await; @@ -49,10 +62,32 @@ pub async fn swap( info!("Amounts no good, aborting ..."); process::exit(0); } + info!("User verified amounts, continuing with swap ..."); + (p.btc, p.xmr) } other => panic!("unexpected event: {:?}", other), - } + }; + + // FIXME: Too many `bitcoin` crates/modules. + let xmr = xmr_btc::monero::Amount::from_piconero(xmr.as_piconero()); + let btc = ::bitcoin::Amount::from_sat(btc.as_sat()); + + let state0 = State0::new( + rng, + btc, + xmr, + REFUND_TIMELOCK, + PUNISH_TIMELOCK, + refund_address, + ); + swarm.send_message0(alice.clone(), state0.next_message(rng)); + let state1 = match swarm.next().await { + OutEvent::Message0(msg) => { + state0.receive(wallet, msg) // TODO: More graceful error handling. + } + other => panic!("unexpected event: {:?}", other), + }; warn!("parking thread ..."); thread::park(); @@ -83,14 +118,9 @@ fn new_swarm() -> Result { #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum OutEvent { - Response(amounts::OutEvent), ConnectionEstablished(PeerId), -} - -impl From for OutEvent { - fn from(event: amounts::OutEvent) -> Self { - OutEvent::Response(event) - } + Amounts(amounts::OutEvent), + Message0(alice::Message0), } impl From for OutEvent { @@ -103,13 +133,28 @@ impl From for OutEvent { } } +impl From for OutEvent { + fn from(event: amounts::OutEvent) -> Self { + OutEvent::Amounts(event) + } +} + +impl From for OutEvent { + fn from(event: message0::OutEvent) -> Self { + match event { + message0::OutEvent::Msg(msg) => OutEvent::Message0(msg), + } + } +} + /// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob. #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", event_process = false)] #[allow(missing_debug_implementations)] pub struct Bob { - amounts: Amounts, pt: PeerTracker, + amounts: Amounts, + message0: Message0, #[behaviour(ignore)] identity: Keypair, } @@ -124,12 +169,17 @@ impl Bob { } /// Sends a message to Alice to get current amounts based on `btc`. - pub async fn request_amounts(&mut self, alice: PeerId, btc: u64) { - let btc = bitcoin::Amount::from_sat(btc); - let _id = self.amounts.request_amounts(alice.clone(), btc).await; + pub fn request_amounts(&mut self, alice: PeerId, btc: u64) { + let btc = ::bitcoin::Amount::from_sat(btc); + let _id = self.amounts.request_amounts(alice.clone(), btc); debug!("Requesting amounts from: {}", alice); } + /// Sends Bob's first state message to Alice. + pub fn send_message0(&mut self, alice: PeerId, msg: bob::Message0) { + self.message0.send(alice, msg) + } + /// Returns Alice's peer id if we are connected. pub fn peer_id_of_alice(&self) -> Option { self.pt.counterparty_peer_id() @@ -142,8 +192,9 @@ impl Default for Bob { let timeout = Duration::from_secs(TIMEOUT); Self { - amounts: Amounts::new(timeout), pt: PeerTracker::default(), + amounts: Amounts::new(timeout), + message0: Message0::new(timeout), identity, } } diff --git a/swap/src/bob/amounts.rs b/swap/src/bob/amounts.rs index 527fdb32..6a329a00 100644 --- a/swap/src/bob/amounts.rs +++ b/swap/src/bob/amounts.rs @@ -15,7 +15,6 @@ use std::{ use tracing::error; use crate::{ - bitcoin, network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}, SwapParams, }; @@ -25,7 +24,7 @@ pub enum OutEvent { Amounts(SwapParams), } -/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob. +/// A `NetworkBehaviour` that represents getting the amounts of an XMR/BTC swap. #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", poll_method = "poll")] #[allow(missing_debug_implementations)] @@ -50,11 +49,7 @@ impl Amounts { } } - pub async fn request_amounts( - &mut self, - alice: PeerId, - btc: bitcoin::Amount, - ) -> Result { + pub fn request_amounts(&mut self, alice: PeerId, btc: ::bitcoin::Amount) -> Result { let msg = BobToAlice::AmountsFromBtc(btc); let id = self.rr.send_request(&alice, msg); diff --git a/swap/src/bob/message0.rs b/swap/src/bob/message0.rs new file mode 100644 index 00000000..e5d3d215 --- /dev/null +++ b/swap/src/bob/message0.rs @@ -0,0 +1,97 @@ +use libp2p::{ + request_response::{ + handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig, + RequestResponseEvent, RequestResponseMessage, + }, + swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters}, + NetworkBehaviour, PeerId, +}; +use std::{ + collections::VecDeque, + task::{Context, Poll}, + time::Duration, +}; +use tracing::error; + +use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}; +use xmr_btc::{alice, bob}; + +#[derive(Debug)] +pub enum OutEvent { + Msg(alice::Message0), +} + +/// A `NetworkBehaviour` that represents send/recv of message 0. +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "OutEvent", poll_method = "poll")] +#[allow(missing_debug_implementations)] +pub struct Message0 { + rr: RequestResponse, + #[behaviour(ignore)] + events: VecDeque, +} + +impl Message0 { + pub fn new(timeout: Duration) -> Self { + let mut config = RequestResponseConfig::default(); + config.set_request_timeout(timeout); + + Self { + rr: RequestResponse::new( + Codec::default(), + vec![(Protocol, ProtocolSupport::Full)], + config, + ), + events: Default::default(), + } + } + + pub fn send(&mut self, alice: PeerId, msg: bob::Message0) { + let msg = BobToAlice::Message0(msg); + let _id = self.rr.send_request(&alice, msg); + } + + fn poll( + &mut self, + _: &mut Context<'_>, + _: &mut impl PollParameters, + ) -> Poll, OutEvent>> { + if let Some(event) = self.events.pop_front() { + return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event)); + } + + Poll::Pending + } +} + +impl NetworkBehaviourEventProcess> for Message0 { + fn inject_event(&mut self, event: RequestResponseEvent) { + match event { + RequestResponseEvent::Message { + peer: _, + message: RequestResponseMessage::Request { .. }, + } => panic!("Bob should never get a request from Alice"), + RequestResponseEvent::Message { + peer: _, + message: + RequestResponseMessage::Response { + response, + request_id: _, + }, + } => match response { + AliceToBob::Message0(msg) => self.events.push_back(OutEvent::Msg(msg)), + }, + + RequestResponseEvent::InboundFailure { .. } => { + panic!("Bob should never get a request from Alice, so should never get an InboundFailure"); + } + RequestResponseEvent::OutboundFailure { + peer: _, + request_id: _, + error, + } => { + error!("Outbound failure: {:?}", error); + } + } + } +} diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 9fded336..9f98db70 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -9,6 +9,9 @@ pub mod network; pub const ONE_BTC: u64 = 100_000_000; +const REFUND_TIMELOCK: u32 = 10; // FIXME: What should this be? +const PUNISH_TIMELOCK: u32 = 20; // FIXME: What should this be? + pub type Never = std::convert::Infallible; /// Commands sent from Bob to the main task. @@ -29,13 +32,19 @@ pub enum Rsp { pub struct SwapParams { /// Amount of BTC to swap. #[serde(with = "::bitcoin::util::amount::serde::as_sat")] - pub btc: bitcoin::Amount, + pub btc: ::bitcoin::Amount, /// Amount of XMR to swap. - pub xmr: monero::Amount, + #[serde(with = "crate::monero::amount_serde")] + pub xmr: xmr_btc::monero::Amount, } impl Display for SwapParams { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} for {}", self.btc, self.xmr) + write!( + f, + "{} sats for {} piconeros", + self.btc.as_sat(), + self.xmr.as_piconero() + ) } } diff --git a/swap/src/main.rs b/swap/src/main.rs index fc2997ee..f796cb75 100644 --- a/swap/src/main.rs +++ b/swap/src/main.rs @@ -16,6 +16,7 @@ use anyhow::{bail, Result}; use futures::{channel::mpsc, StreamExt}; use libp2p::Multiaddr; use log::LevelFilter; +use rand::rngs::OsRng; use std::{io, io::Write, process}; use structopt::StructOpt; use tracing::info; @@ -38,7 +39,7 @@ async fn main() -> Result<()> { trace::init_tracing(LevelFilter::Debug)?; let addr = format!("/ip4/{}/tcp/{}", ADDR, PORT); - let alice_addr: Multiaddr = addr.parse().expect("failed to parse Alice's address"); + let alice: Multiaddr = addr.parse().expect("failed to parse Alice's address"); if opt.as_alice { info!("running swap node as Alice ..."); @@ -47,16 +48,23 @@ async fn main() -> Result<()> { bail!("Alice cannot set the amount to swap via the cli"); } - swap_as_alice(alice_addr).await?; + // TODO: Get these addresses from command line + let redeem = bitcoin::Address::default(); + let punish = bitcoin::Address::default(); + + swap_as_alice(alice, redeem, refund).await?; } else { info!("running swap node as Bob ..."); + // TODO: Get refund address from command line + let refund = bitcoin::Address::default(); + match (opt.piconeros, opt.satoshis) { (Some(_), Some(_)) => bail!("Please supply only a single amount to swap"), (None, None) => bail!("Please supply an amount to swap"), (Some(_picos), _) => todo!("support starting with picos"), (None, Some(sats)) => { - swap_as_bob(sats, alice_addr).await?; + swap_as_bob(sats, alice_addr, refund).await?; } }; } @@ -64,14 +72,18 @@ async fn main() -> Result<()> { Ok(()) } -async fn swap_as_alice(addr: Multiaddr) -> Result<()> { - alice::swap(addr).await +async fn swap_as_alice( + addr: Multiaddr, + redeem: bitcoin::Address::default(), + punish: bitcoin::Address::default(), +) -> Result<()> { + alice::swap(addr, OsRng, redeem, punish).await } -async fn swap_as_bob(sats: u64, addr: Multiaddr) -> Result<()> { +async fn swap_as_bob(sats: u64, addr: Multiaddr, refund: bitcoin::Address) -> Result<()> { let (cmd_tx, mut cmd_rx) = mpsc::channel(1); let (mut rsp_tx, rsp_rx) = mpsc::channel(1); - tokio::spawn(bob::swap(sats, addr, cmd_tx, rsp_rx)); + tokio::spawn(bob::swap(sats, addr, cmd_tx, rsp_rx, OsRng, refund)); loop { let read = cmd_rx.next().await; match read { diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 940a1922..43b571b8 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -1,30 +1,27 @@ -use serde::{Deserialize, Serialize}; -use std::fmt; +use serde::{de::Error, Deserialize, Deserializer, Serializer}; -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct Amount(u64); +use xmr_btc::monero::Amount; -impl Amount { - /// Create an [Amount] with piconero precision and the given number of - /// piconeros. - /// - /// A piconero (a.k.a atomic unit) is equal to 1e-12 XMR. - pub fn from_piconero(amount: u64) -> Self { - Amount(amount) - } - pub fn as_piconero(&self) -> u64 { - self.0 - } -} +pub mod amount_serde { + use super::*; + use std::str::FromStr; -impl From for u64 { - fn from(from: Amount) -> u64 { - from.0 + pub fn serialize(value: &Amount, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&value.as_piconero().to_string()) } -} -impl fmt::Display for Amount { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} piconeros", self.0) + pub fn deserialize<'de, D>(deserializer: D) -> Result>::Error> + where + D: Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + let value = + u64::from_str(value.as_str()).map_err(>::Error::custom)?; + let amount = Amount::from_piconero(value); + + Ok(amount) } } diff --git a/swap/src/network/request_response.rs b/swap/src/network/request_response.rs index deb19b73..4af3a222 100644 --- a/swap/src/network/request_response.rs +++ b/swap/src/network/request_response.rs @@ -7,7 +7,8 @@ use libp2p::{ use serde::{Deserialize, Serialize}; use std::{fmt::Debug, io}; -use crate::{bitcoin, monero, SwapParams}; +use crate::SwapParams; +use xmr_btc::{alice, bob, monero}; /// Time to wait for a response back once we send a request. pub const TIMEOUT: u64 = 3600; // One hour. @@ -16,18 +17,16 @@ pub const TIMEOUT: u64 = 3600; // One hour. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum BobToAlice { #[serde(with = "::bitcoin::util::amount::serde::as_sat")] - AmountsFromBtc(bitcoin::Amount), + AmountsFromBtc(::bitcoin::Amount), AmountsFromXmr(monero::Amount), - /* TODO: How are we going to do this when the messages are not Clone? - * Msg(bob::Message), */ + Message0(bob::Message0), } /// Messages Alice sends to Bob. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum AliceToBob { Amounts(SwapParams), - /* TODO: How are we going to do this when the messages are not Clone? - * Msg(alice::Message) */ + Message0(alice::Message0), } #[derive(Debug, Clone, Copy, Default)] From 04e1dca16afdb0834c8758ce0bc8eed3796812ec Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Wed, 21 Oct 2020 15:14:31 +1100 Subject: [PATCH 31/52] Fix dependency to cross-curve-dleq and ecdsa_fun --- xmr-btc/Cargo.toml | 4 ++-- xmr-btc/src/alice.rs | 2 +- xmr-btc/src/bob.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index 0b6cf7e7..3e1457de 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -8,9 +8,9 @@ edition = "2018" anyhow = "1" async-trait = "0.1" bitcoin = { version = "0.23", features = ["rand"] } -cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "a3e57a70d332b4ce9600663453b9bd02936d76bf" } +cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "49171f5e08473d46f951fb1fc338fe437d974d3c" } curve25519-dalek = "2" -ecdsa_fun = { version = "0.3.1", features = ["libsecp_compat"] } +ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat"] } ed25519-dalek = "1.0.0-pre.4" # Cannot be 1 because they depend on curve25519-dalek version 3 futures = "0.3" genawaiter = "0.99.1" diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index a8a1ea20..9b398464 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -188,7 +188,7 @@ impl State0 { pub fn receive(self, msg: bob::Message0) -> Result { msg.dleq_proof_s_b.verify( - &msg.S_b_bitcoin.clone().into(), + msg.S_b_bitcoin.clone().into(), msg.S_b_monero .point .decompress() diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 55099ed6..2b62c749 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -160,7 +160,7 @@ impl State0 { W: BuildTxLockPsbt, { msg.dleq_proof_s_a.verify( - &msg.S_a_bitcoin.clone().into(), + msg.S_a_bitcoin.clone().into(), msg.S_a_monero .point .decompress() From 8b48191e1cb70c168a0ed0f7b67372727d142b9b Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Wed, 21 Oct 2020 15:55:20 +1100 Subject: [PATCH 32/52] Increase Rust minimum stack size in CI --- .github/workflows/ci.yml | 1 + xmr-btc/tests/e2e.rs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 531def83..c92878bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,4 +76,5 @@ jobs: - name: Cargo test run: cargo test --workspace --all-features env: + RUST_MIN_STACK: 10000000 MONERO_ADDITIONAL_SLEEP_PERIOD: 60000 diff --git a/xmr-btc/tests/e2e.rs b/xmr-btc/tests/e2e.rs index 2eb3cf70..2b3bc461 100644 --- a/xmr-btc/tests/e2e.rs +++ b/xmr-btc/tests/e2e.rs @@ -142,6 +142,11 @@ pub async fn init_test( } mod tests { + // NOTE: For some reason running these tests overflows the stack. In order to + // mitigate this run them with: + // + // RUST_MIN_STACK=10000000 cargo test + use crate::{ harness, harness::node::{run_alice_until, run_bob_until}, From 917aabcc03e643d2674846ce197c00c47a9c088f Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Wed, 21 Oct 2020 16:04:41 +1100 Subject: [PATCH 33/52] Increase Rust minimum stack size a lot more --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c92878bc..e3a8d6fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,5 +76,5 @@ jobs: - name: Cargo test run: cargo test --workspace --all-features env: - RUST_MIN_STACK: 10000000 + RUST_MIN_STACK: 100000000 MONERO_ADDITIONAL_SLEEP_PERIOD: 60000 From d3a7689059f82e03eba4689cdac7819718961e71 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Wed, 21 Oct 2020 16:10:20 +1100 Subject: [PATCH 34/52] Update comment about minimum stack size --- xmr-btc/tests/e2e.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmr-btc/tests/e2e.rs b/xmr-btc/tests/e2e.rs index 2b3bc461..024fb1ca 100644 --- a/xmr-btc/tests/e2e.rs +++ b/xmr-btc/tests/e2e.rs @@ -145,7 +145,7 @@ mod tests { // NOTE: For some reason running these tests overflows the stack. In order to // mitigate this run them with: // - // RUST_MIN_STACK=10000000 cargo test + // RUST_MIN_STACK=100000000 cargo test use crate::{ harness, From 39afb4196b51bf3b284a64418e105e865d059048 Mon Sep 17 00:00:00 2001 From: rishflab Date: Wed, 14 Oct 2020 09:32:25 +1100 Subject: [PATCH 35/52] Save and recover protocol state from disk NOTE: This implementation saves secrets to disk! It is not secure. The storage API allows the caller to atomically record the state of the protocol. The user can retrieve this recorded state and re-commence the protocol from that point. The state is recorded using a hard coded key, causing it to overwrite the previously recorded state. This limitation means that this recovery mechanism should not be used in a program that simultaneously manages the execution of multiple swaps. An e2e test was added to show how to save, recover and resume protocol execution. This logic could also be integrated into the run_until functions to automate saving but was not included at this stage as protocol execution is currently under development. Serialisation and deserialisation was implemented on the states to allow the to be stored using the database. Currently the secret's are also being stored to disk but should be recovered from a seed or wallets. --- xmr-btc/Cargo.toml | 13 +- xmr-btc/src/alice.rs | 37 ++++- xmr-btc/src/bitcoin.rs | 5 +- xmr-btc/src/bitcoin/transactions.rs | 3 +- xmr-btc/src/bob.rs | 27 +++- xmr-btc/src/lib.rs | 1 + xmr-btc/src/monero.rs | 13 +- xmr-btc/src/serde.rs | 210 ++++++++++++++++++++++++++++ xmr-btc/tests/e2e.rs | 131 ++++++++++++++++- xmr-btc/tests/harness/mod.rs | 1 + xmr-btc/tests/harness/storage.rs | 159 +++++++++++++++++++++ 11 files changed, 571 insertions(+), 29 deletions(-) create mode 100644 xmr-btc/src/serde.rs create mode 100644 xmr-btc/tests/harness/storage.rs diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index c25060d0..75944532 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -7,14 +7,15 @@ edition = "2018" [dependencies] anyhow = "1" async-trait = "0.1" -bitcoin = { version = "0.23", features = ["rand"] } +bitcoin = { version = "0.23", features = ["rand", "serde"] } cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "49171f5e08473d46f951fb1fc338fe437d974d3c" } curve25519-dalek = "2" -ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat"] } -ed25519-dalek = "1.0.0-pre.4" # Cannot be 1 because they depend on curve25519-dalek version 3 -miniscript = "1" -monero = "0.9" +ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat", "serde", "serialization"] } +ed25519-dalek = { version = "1.0.0-pre.4", features = ["serde"] }# Cannot be 1 because they depend on curve25519-dalek version 3 +miniscript = { version = "1", features = ["serde"] } +monero = { version = "0.9", features = ["serde_support"] } rand = "0.7" +serde = { version = "1", features = ["derive"] } sha2 = "0.9" thiserror = "1" tracing = "0.1" @@ -25,6 +26,8 @@ bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = futures = "0.3" monero-harness = { path = "../monero-harness" } reqwest = { version = "0.10", default-features = false } +serde_cbor = "0.11" +sled = "0.34" testcontainers = "0.10" tokio = { version = "0.2", default-features = false, features = ["blocking", "macros", "rt-core", "time", "rt-threaded"] } tracing = "0.1" diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index 0bba27bb..8d07be01 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -3,6 +3,7 @@ use crate::{ bitcoin::{BroadcastSignedTransaction, WatchForRawTransaction}, bob, monero, monero::{CreateWalletForOutput, Transfer}, + serde::{bitcoin_amount, cross_curve_dleq_scalar, ecdsa_fun_signature}, transport::{ReceiveMessage, SendMessage}, }; use anyhow::{anyhow, Result}; @@ -11,6 +12,7 @@ use ecdsa_fun::{ nonce::Deterministic, }; use rand::{CryptoRng, RngCore}; +use serde::{Deserialize, Serialize}; use sha2::Sha256; use std::convert::{TryFrom, TryInto}; @@ -129,11 +131,13 @@ impl State { } } -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct State0 { a: bitcoin::SecretKey, + #[serde(with = "cross_curve_dleq_scalar")] s_a: cross_curve_dleq::Scalar, v_a: monero::PrivateViewKey, + #[serde(with = "bitcoin_amount")] btc: bitcoin::Amount, xmr: monero::Amount, refund_timelock: u32, @@ -215,14 +219,16 @@ impl State0 { } } -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct State1 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, + #[serde(with = "cross_curve_dleq_scalar")] s_a: cross_curve_dleq::Scalar, S_b_monero: monero::PublicKey, S_b_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, + #[serde(with = "bitcoin_amount")] btc: bitcoin::Amount, xmr: monero::Amount, refund_timelock: u32, @@ -253,14 +259,16 @@ impl State1 { } } -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct State2 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, + #[serde(with = "cross_curve_dleq_scalar")] s_a: cross_curve_dleq::Scalar, S_b_monero: monero::PublicKey, S_b_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, + #[serde(with = "bitcoin_amount")] btc: bitcoin::Amount, xmr: monero::Amount, refund_timelock: u32, @@ -328,14 +336,16 @@ impl State2 { } } -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct State3 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, + #[serde(with = "cross_curve_dleq_scalar")] s_a: cross_curve_dleq::Scalar, S_b_monero: monero::PublicKey, S_b_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, + #[serde(with = "bitcoin_amount")] btc: bitcoin::Amount, xmr: monero::Amount, refund_timelock: u32, @@ -344,7 +354,9 @@ pub struct State3 { redeem_address: bitcoin::Address, punish_address: bitcoin::Address, tx_lock: bitcoin::TxLock, + #[serde(with = "ecdsa_fun_signature")] tx_punish_sig_bob: bitcoin::Signature, + #[serde(with = "ecdsa_fun_signature")] tx_cancel_sig_bob: bitcoin::Signature, } @@ -381,14 +393,16 @@ impl State3 { } } -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct State4 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, + #[serde(with = "cross_curve_dleq_scalar")] s_a: cross_curve_dleq::Scalar, S_b_monero: monero::PublicKey, S_b_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, + #[serde(with = "bitcoin_amount")] btc: bitcoin::Amount, xmr: monero::Amount, refund_timelock: u32, @@ -397,7 +411,9 @@ pub struct State4 { redeem_address: bitcoin::Address, punish_address: bitcoin::Address, tx_lock: bitcoin::TxLock, + #[serde(with = "ecdsa_fun_signature")] tx_punish_sig_bob: bitcoin::Signature, + #[serde(with = "ecdsa_fun_signature")] tx_cancel_sig_bob: bitcoin::Signature, } @@ -484,14 +500,16 @@ impl State4 { } } -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct State5 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, + #[serde(with = "cross_curve_dleq_scalar")] s_a: cross_curve_dleq::Scalar, S_b_monero: monero::PublicKey, S_b_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, + #[serde(with = "bitcoin_amount")] btc: bitcoin::Amount, xmr: monero::Amount, refund_timelock: u32, @@ -501,7 +519,9 @@ pub struct State5 { punish_address: bitcoin::Address, tx_lock: bitcoin::TxLock, tx_lock_proof: monero::TransferProof, + #[serde(with = "ecdsa_fun_signature")] tx_punish_sig_bob: bitcoin::Signature, + #[serde(with = "ecdsa_fun_signature")] tx_cancel_sig_bob: bitcoin::Signature, lock_xmr_fee: monero::Amount, } @@ -575,14 +595,16 @@ impl State5 { } } -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct State6 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, + #[serde(with = "cross_curve_dleq_scalar")] s_a: cross_curve_dleq::Scalar, S_b_monero: monero::PublicKey, S_b_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, + #[serde(with = "bitcoin_amount")] btc: bitcoin::Amount, xmr: monero::Amount, refund_timelock: u32, @@ -591,6 +613,7 @@ pub struct State6 { redeem_address: bitcoin::Address, punish_address: bitcoin::Address, tx_lock: bitcoin::TxLock, + #[serde(with = "ecdsa_fun_signature")] tx_punish_sig_bob: bitcoin::Signature, tx_redeem_encsig: EncryptedSignature, lock_xmr_fee: monero::Amount, diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index 5bad1f9d..a0f323eb 100644 --- a/xmr-btc/src/bitcoin.rs +++ b/xmr-btc/src/bitcoin.rs @@ -21,6 +21,7 @@ use ecdsa_fun::{ pub use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; use miniscript::{Descriptor, Segwitv0}; use rand::{CryptoRng, RngCore}; +use serde::{Deserialize, Serialize}; use sha2::Sha256; use std::str::FromStr; @@ -28,7 +29,7 @@ pub use crate::bitcoin::transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxR pub const TX_FEE: u64 = 10_000; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct SecretKey { inner: Scalar, public: Point, @@ -83,7 +84,7 @@ impl SecretKey { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct PublicKey(Point); impl From for Point { diff --git a/xmr-btc/src/bitcoin/transactions.rs b/xmr-btc/src/bitcoin/transactions.rs index 9eb7878d..242e3a9e 100644 --- a/xmr-btc/src/bitcoin/transactions.rs +++ b/xmr-btc/src/bitcoin/transactions.rs @@ -8,9 +8,10 @@ use bitcoin::{ }; use ecdsa_fun::Signature; use miniscript::Descriptor; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct TxLock { inner: Transaction, output_descriptor: Descriptor<::bitcoin::PublicKey>, diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 57515f1a..3ec29f6e 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -6,6 +6,7 @@ use crate::{ }, monero, monero::{CheckTransfer, CreateWalletForOutput}, + serde::{bitcoin_amount, cross_curve_dleq_scalar, monero_private_key}, transport::{ReceiveMessage, SendMessage}, }; use anyhow::{anyhow, Result}; @@ -15,6 +16,7 @@ use ecdsa_fun::{ Signature, }; use rand::{CryptoRng, RngCore}; +use serde::{Deserialize, Serialize}; use sha2::Sha256; use std::convert::{TryFrom, TryInto}; @@ -102,11 +104,13 @@ impl_from_child_enum!(State3, State); impl_from_child_enum!(State4, State); impl_from_child_enum!(State5, State); -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct State0 { b: bitcoin::SecretKey, + #[serde(with = "cross_curve_dleq_scalar")] s_b: cross_curve_dleq::Scalar, v_b: monero::PrivateViewKey, + #[serde(with = "bitcoin_amount")] btc: bitcoin::Amount, xmr: monero::Amount, refund_timelock: u32, @@ -190,14 +194,16 @@ impl State0 { } } -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct State1 { A: bitcoin::PublicKey, b: bitcoin::SecretKey, + #[serde(with = "cross_curve_dleq_scalar")] s_b: cross_curve_dleq::Scalar, S_a_monero: monero::PublicKey, S_a_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, + #[serde(with = "bitcoin_amount")] btc: bitcoin::Amount, xmr: monero::Amount, refund_timelock: u32, @@ -253,14 +259,16 @@ impl State1 { } } -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct State2 { A: bitcoin::PublicKey, b: bitcoin::SecretKey, + #[serde(with = "cross_curve_dleq_scalar")] s_b: cross_curve_dleq::Scalar, S_a_monero: monero::PublicKey, S_a_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, + #[serde(with = "bitcoin_amount")] btc: bitcoin::Amount, xmr: monero::Amount, refund_timelock: u32, @@ -324,14 +332,16 @@ impl State2 { } } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub struct State3 { A: bitcoin::PublicKey, b: bitcoin::SecretKey, + #[serde(with = "cross_curve_dleq_scalar")] s_b: cross_curve_dleq::Scalar, S_a_monero: monero::PublicKey, S_a_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, + #[serde(with = "bitcoin_amount")] btc: bitcoin::Amount, xmr: monero::Amount, refund_timelock: u32, @@ -429,14 +439,16 @@ impl State3 { } } -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct State4 { A: bitcoin::PublicKey, b: bitcoin::SecretKey, + #[serde(with = "cross_curve_dleq_scalar")] s_b: cross_curve_dleq::Scalar, S_a_monero: monero::PublicKey, S_a_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, + #[serde(with = "bitcoin_amount")] btc: bitcoin::Amount, xmr: monero::Amount, refund_timelock: u32, @@ -496,15 +508,18 @@ impl State4 { } } -#[derive(Debug)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct State5 { A: bitcoin::PublicKey, b: bitcoin::SecretKey, + #[serde(with = "monero_private_key")] s_a: monero::PrivateKey, + #[serde(with = "cross_curve_dleq_scalar")] s_b: cross_curve_dleq::Scalar, S_a_monero: monero::PublicKey, S_a_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, + #[serde(with = "bitcoin_amount")] btc: bitcoin::Amount, xmr: monero::Amount, refund_timelock: u32, diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 790cb477..98cb3b6e 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -49,4 +49,5 @@ pub mod alice; pub mod bitcoin; pub mod bob; pub mod monero; +pub mod serde; pub mod transport; diff --git a/xmr-btc/src/monero.rs b/xmr-btc/src/monero.rs index 459fa708..b37dba25 100644 --- a/xmr-btc/src/monero.rs +++ b/xmr-btc/src/monero.rs @@ -1,8 +1,10 @@ +use crate::serde::monero_private_key; use anyhow::Result; use async_trait::async_trait; pub use curve25519_dalek::scalar::Scalar; pub use monero::{Address, PrivateKey, PublicKey}; use rand::{CryptoRng, RngCore}; +use serde::{Deserialize, Serialize}; use std::ops::Add; pub fn random_private_key(rng: &mut R) -> PrivateKey { @@ -11,8 +13,8 @@ pub fn random_private_key(rng: &mut R) -> PrivateKey { PrivateKey::from_scalar(scalar) } -#[derive(Clone, Copy, Debug)] -pub struct PrivateViewKey(PrivateKey); +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] +pub struct PrivateViewKey(#[serde(with = "monero_private_key")] PrivateKey); impl PrivateViewKey { pub fn new_random(rng: &mut R) -> Self { @@ -50,7 +52,7 @@ impl From for PublicKey { #[derive(Clone, Copy, Debug)] pub struct PublicViewKey(PublicKey); -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)] pub struct Amount(u64); impl Amount { @@ -72,9 +74,10 @@ impl From for u64 { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct TransferProof { tx_hash: TxHash, + #[serde(with = "monero_private_key")] tx_key: PrivateKey, } @@ -91,7 +94,7 @@ impl TransferProof { } // TODO: add constructor/ change String to fixed length byte array -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct TxHash(pub String); impl From for String { diff --git a/xmr-btc/src/serde.rs b/xmr-btc/src/serde.rs new file mode 100644 index 00000000..e64cfd10 --- /dev/null +++ b/xmr-btc/src/serde.rs @@ -0,0 +1,210 @@ +pub mod ecdsa_fun_signature { + use serde::{de, de::Visitor, Deserializer, Serializer}; + use std::{convert::TryFrom, fmt}; + + struct Bytes64Visitor; + + impl<'de> Visitor<'de> for Bytes64Visitor { + type Value = ecdsa_fun::Signature; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "a string containing 64 bytes") + } + + fn visit_bytes(self, s: &[u8]) -> Result + where + E: de::Error, + { + if let Ok(value) = <[u8; 64]>::try_from(s) { + let sig = ecdsa_fun::Signature::from_bytes(value) + .expect("bytes represent an integer greater than or equal to the curve order"); + Ok(sig) + } else { + Err(de::Error::invalid_length(s.len(), &self)) + } + } + } + + pub fn serialize(x: &ecdsa_fun::Signature, s: S) -> Result + where + S: Serializer, + { + s.serialize_bytes(&x.to_bytes()) + } + + pub fn deserialize<'de, D>( + deserializer: D, + ) -> Result>::Error> + where + D: Deserializer<'de>, + { + let sig = deserializer.deserialize_bytes(Bytes64Visitor)?; + Ok(sig) + } +} + +pub mod cross_curve_dleq_scalar { + use serde::{de, de::Visitor, Deserializer, Serializer}; + use std::{convert::TryFrom, fmt}; + + struct Bytes32Visitor; + + impl<'de> Visitor<'de> for Bytes32Visitor { + type Value = cross_curve_dleq::Scalar; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "a string containing 32 bytes") + } + + fn visit_bytes(self, s: &[u8]) -> Result + where + E: de::Error, + { + if let Ok(value) = <[u8; 32]>::try_from(s) { + Ok(cross_curve_dleq::Scalar::from(value)) + } else { + Err(de::Error::invalid_length(s.len(), &self)) + } + } + } + + pub fn serialize(x: &cross_curve_dleq::Scalar, s: S) -> Result + where + S: Serializer, + { + // Serialise as ed25519 because the inner bytes are private + // TODO: Open PR in cross_curve_dleq to allow accessing the inner bytes + s.serialize_bytes(&x.into_ed25519().to_bytes()) + } + + pub fn deserialize<'de, D>( + deserializer: D, + ) -> Result>::Error> + where + D: Deserializer<'de>, + { + let dleq = deserializer.deserialize_bytes(Bytes32Visitor)?; + Ok(dleq) + } +} + +pub mod monero_private_key { + use serde::{de, de::Visitor, Deserializer, Serializer}; + use std::fmt; + + struct BytesVisitor; + + impl<'de> Visitor<'de> for BytesVisitor { + type Value = monero::PrivateKey; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "a string containing 32 bytes") + } + + fn visit_bytes(self, s: &[u8]) -> Result + where + E: de::Error, + { + if let Ok(key) = monero::PrivateKey::from_slice(s) { + Ok(key) + } else { + Err(de::Error::invalid_length(s.len(), &self)) + } + } + } + + pub fn serialize(x: &monero::PrivateKey, s: S) -> Result + where + S: Serializer, + { + s.serialize_bytes(x.as_bytes()) + } + + pub fn deserialize<'de, D>( + deserializer: D, + ) -> Result>::Error> + where + D: Deserializer<'de>, + { + let key = deserializer.deserialize_bytes(BytesVisitor)?; + Ok(key) + } +} + +pub mod bitcoin_amount { + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(value: &bitcoin::Amount, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u64(value.as_sat()) + } + + pub fn deserialize<'de, D>( + deserializer: D, + ) -> Result>::Error> + where + D: Deserializer<'de>, + { + let value = u64::deserialize(deserializer)?; + let amount = bitcoin::Amount::from_sat(value); + + Ok(amount) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ::bitcoin::SigHash; + use curve25519_dalek::scalar::Scalar; + use rand::rngs::OsRng; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + pub struct CrossCurveDleqScalar( + #[serde(with = "cross_curve_dleq_scalar")] cross_curve_dleq::Scalar, + ); + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + pub struct ECDSAFunSignature(#[serde(with = "ecdsa_fun_signature")] ecdsa_fun::Signature); + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + pub struct MoneroPrivateKey(#[serde(with = "monero_private_key")] crate::monero::PrivateKey); + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + pub struct BitcoinAmount(#[serde(with = "bitcoin_amount")] ::bitcoin::Amount); + + #[test] + fn serde_cross_curv_dleq_scalar() { + let scalar = CrossCurveDleqScalar(cross_curve_dleq::Scalar::random(&mut OsRng)); + let encoded = serde_cbor::to_vec(&scalar).unwrap(); + let decoded: CrossCurveDleqScalar = serde_cbor::from_slice(&encoded).unwrap(); + assert_eq!(scalar, decoded); + } + + #[test] + fn serde_ecdsa_fun_sig() { + let secret_key = crate::bitcoin::SecretKey::new_random(&mut OsRng); + let sig = ECDSAFunSignature(secret_key.sign(SigHash::default())); + let encoded = serde_cbor::to_vec(&sig).unwrap(); + let decoded: ECDSAFunSignature = serde_cbor::from_slice(&encoded).unwrap(); + assert_eq!(sig, decoded); + } + + #[test] + fn serde_monero_private_key() { + let key = MoneroPrivateKey(monero::PrivateKey::from_scalar(Scalar::random(&mut OsRng))); + let encoded = serde_cbor::to_vec(&key).unwrap(); + let decoded: MoneroPrivateKey = serde_cbor::from_slice(&encoded).unwrap(); + assert_eq!(key, decoded); + } + #[test] + fn serde_bitcoin_amount() { + let amount = BitcoinAmount(::bitcoin::Amount::from_sat(100)); + let encoded = serde_cbor::to_vec(&amount).unwrap(); + let decoded: BitcoinAmount = serde_cbor::from_slice(&encoded).unwrap(); + assert_eq!(amount, decoded); + } +} diff --git a/xmr-btc/tests/e2e.rs b/xmr-btc/tests/e2e.rs index 41423f5d..6cf0e9dd 100644 --- a/xmr-btc/tests/e2e.rs +++ b/xmr-btc/tests/e2e.rs @@ -18,6 +18,8 @@ mod harness; const TEN_XMR: u64 = 10_000_000_000_000; const RELATIVE_REFUND_TIMELOCK: u32 = 1; const RELATIVE_PUNISH_TIMELOCK: u32 = 1; +const ALICE_TEST_DB_FOLDER: &str = "../target/e2e-test-alice-recover"; +const BOB_TEST_DB_FOLDER: &str = "../target/e2e-test-bob-recover"; pub async fn init_bitcoind(tc_client: &Cli) -> Bitcoind<'_> { let bitcoind = Bitcoind::new(tc_client, "0.19.1").expect("failed to create bitcoind"); @@ -61,7 +63,7 @@ pub fn init_alice_and_bob_transports() -> ( pub async fn init_test<'a>( monero: &'a Monero<'a>, - bitcoind: &Bitcoind<'_>, + bitcoind: &Bitcoind<'a>, ) -> ( alice::State0, bob::State0, @@ -150,12 +152,13 @@ mod tests { use crate::{ harness, harness::node::{run_alice_until, run_bob_until}, - init_bitcoind, init_test, + init_bitcoind, init_test, ALICE_TEST_DB_FOLDER, BOB_TEST_DB_FOLDER, }; use futures::future; use monero_harness::Monero; use rand::rngs::OsRng; - use std::convert::TryInto; + + use std::{convert::TryInto, path::Path}; use testcontainers::clients::Cli; use tracing_subscriber::util::SubscriberInitExt; use xmr_btc::{ @@ -400,4 +403,126 @@ mod tests { initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee ); } + + #[tokio::test] + async fn recover_protocol_state_from_db() { + 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_db = harness::storage::Database::open(Path::new(ALICE_TEST_DB_FOLDER)).unwrap(); + let bob_db = harness::storage::Database::open(Path::new(BOB_TEST_DB_FOLDER)).unwrap(); + + 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(); + + // save state to db + alice_db.insert_latest_state(&alice_state5).await.unwrap(); + bob_db.insert_latest_state(&bob_state3).await.unwrap(); + }; + + let (alice_state6, bob_state5) = { + // recover state from db + let alice_state5: alice::State5 = alice_db.get_latest_state().unwrap(); + let bob_state3: bob::State3 = bob_db.get_latest_state().unwrap(); + + let (alice_state, bob_state) = future::try_join( + run_alice_until( + &mut alice_node, + alice_state5.into(), + harness::alice::is_state6, + &mut OsRng, + ), + run_bob_until( + &mut bob_node, + bob_state3.into(), + harness::bob::is_state5, + &mut OsRng, + ), + ) + .await + .unwrap(); + + let alice_state: alice::State6 = alice_state.try_into().unwrap(); + let bob_state: bob::State5 = bob_state.try_into().unwrap(); + + (alice_state, bob_state) + }; + + 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) + ); + } } diff --git a/xmr-btc/tests/harness/mod.rs b/xmr-btc/tests/harness/mod.rs index 4f2cba46..d08a7291 100644 --- a/xmr-btc/tests/harness/mod.rs +++ b/xmr-btc/tests/harness/mod.rs @@ -1,4 +1,5 @@ pub mod node; +pub mod storage; pub mod transport; pub mod wallet; diff --git a/xmr-btc/tests/harness/storage.rs b/xmr-btc/tests/harness/storage.rs new file mode 100644 index 00000000..19b1b401 --- /dev/null +++ b/xmr-btc/tests/harness/storage.rs @@ -0,0 +1,159 @@ +use anyhow::{anyhow, Context, Result}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::path::Path; + +pub struct Database { + db: sled::Db, +} + +impl Database { + const LAST_STATE_KEY: &'static str = "latest_state"; + + pub fn open(path: &Path) -> Result { + let path = path + .to_str() + .ok_or_else(|| anyhow!("The path is not utf-8 valid: {:?}", path))?; + let db = sled::open(path).with_context(|| format!("Could not open the DB at {}", path))?; + + Ok(Database { db }) + } + + pub async fn insert_latest_state(&self, state: &T) -> Result<()> + where + T: Serialize + DeserializeOwned, + { + let key = serialize(&Self::LAST_STATE_KEY)?; + let new_value = serialize(&state).context("Could not serialize new state value")?; + + let old_value = self.db.get(&key)?; + + self.db + .compare_and_swap(key, old_value, Some(new_value)) + .context("Could not write in the DB")? + .context("Stored swap somehow changed, aborting saving")?; // let _ = + + self.db + .flush_async() + .await + .map(|_| ()) + .context("Could not flush db") + } + + pub fn get_latest_state(&self) -> anyhow::Result + where + T: DeserializeOwned, + { + let key = serialize(&Self::LAST_STATE_KEY)?; + + let encoded = self + .db + .get(&key)? + .ok_or_else(|| anyhow!("State does not exist {:?}", key))?; + + let state = deserialize(&encoded).context("Could not deserialize state")?; + Ok(state) + } +} + +pub fn serialize(t: &T) -> anyhow::Result> +where + T: Serialize, +{ + Ok(serde_cbor::to_vec(t)?) +} + +pub fn deserialize(v: &[u8]) -> anyhow::Result +where + T: DeserializeOwned, +{ + Ok(serde_cbor::from_slice(&v)?) +} + +#[cfg(test)] +mod tests { + #![allow(non_snake_case)] + use super::*; + use bitcoin::SigHash; + use curve25519_dalek::scalar::Scalar; + use ecdsa_fun::fun::rand_core::OsRng; + use std::str::FromStr; + use xmr_btc::serde::{ + bitcoin_amount, cross_curve_dleq_scalar, ecdsa_fun_signature, monero_private_key, + }; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + pub struct TestState { + A: xmr_btc::bitcoin::PublicKey, + a: xmr_btc::bitcoin::SecretKey, + #[serde(with = "cross_curve_dleq_scalar")] + s_a: ::cross_curve_dleq::Scalar, + #[serde(with = "monero_private_key")] + s_b: monero::PrivateKey, + S_a_monero: ::monero::PublicKey, + S_a_bitcoin: xmr_btc::bitcoin::PublicKey, + v: xmr_btc::monero::PrivateViewKey, + #[serde(with = "bitcoin_amount")] + btc: ::bitcoin::Amount, + xmr: xmr_btc::monero::Amount, + refund_timelock: u32, + refund_address: ::bitcoin::Address, + transaction: ::bitcoin::Transaction, + #[serde(with = "ecdsa_fun_signature")] + tx_punish_sig: xmr_btc::bitcoin::Signature, + } + + #[tokio::test] + async fn recover_state_from_db() { + let db = Database::open(Path::new("../target/test_recover.db")).unwrap(); + + let a = crate::bitcoin::SecretKey::new_random(&mut OsRng); + let s_a = cross_curve_dleq::Scalar::random(&mut OsRng); + let s_b = monero::PrivateKey::from_scalar(Scalar::random(&mut OsRng)); + let v_a = xmr_btc::monero::PrivateViewKey::new_random(&mut OsRng); + let S_a_monero = monero::PublicKey::from_private_key(&monero::PrivateKey { + scalar: s_a.into_ed25519(), + }); + let S_a_bitcoin = s_a.into_secp256k1().into(); + let tx_punish_sig = a.sign(SigHash::default()); + + let state = TestState { + A: a.public(), + a, + s_b, + s_a, + S_a_monero, + S_a_bitcoin, + v: v_a, + btc: ::bitcoin::Amount::from_sat(100), + xmr: crate::monero::Amount::from_piconero(1000), + refund_timelock: 0, + refund_address: ::bitcoin::Address::from_str("1L5wSMgerhHg8GZGcsNmAx5EXMRXSKR3He") + .unwrap(), + transaction: ::bitcoin::Transaction { + version: 0, + lock_time: 0, + input: vec![::bitcoin::TxIn::default()], + output: vec![::bitcoin::TxOut::default()], + }, + tx_punish_sig, + }; + + db.insert_latest_state(&state) + .await + .expect("Failed to save state the first time"); + let recovered: TestState = db + .get_latest_state() + .expect("Failed to recover state the first time"); + + // We insert and recover twice to ensure database implementation allows the + // caller to write to an existing key + db.insert_latest_state(&recovered) + .await + .expect("Failed to save state the second time"); + let recovered: TestState = db + .get_latest_state() + .expect("Failed to recover state the second time"); + + assert_eq!(state, recovered); + } +} From 2a07113074fda8964f76ef34c11175584b88243e Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 22 Oct 2020 08:28:51 +1100 Subject: [PATCH 36/52] Fix buld after merge of db branch --- xmr-btc/src/bob.rs | 2 +- xmr-btc/tests/e2e.rs | 20 +++++--------------- xmr-btc/tests/harness/storage.rs | 4 ++-- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index bf8780ea..d106e915 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -5,7 +5,7 @@ use crate::{ WatchForRawTransaction, }, monero, - monero::{CheckTransfer, CreateWalletForOutput, WatchForTransfer}, + monero::{CreateWalletForOutput, WatchForTransfer}, serde::{bitcoin_amount, cross_curve_dleq_scalar, monero_private_key}, transport::{ReceiveMessage, SendMessage}, }; diff --git a/xmr-btc/tests/e2e.rs b/xmr-btc/tests/e2e.rs index d2e5f550..84f0878a 100644 --- a/xmr-btc/tests/e2e.rs +++ b/xmr-btc/tests/e2e.rs @@ -62,7 +62,7 @@ pub fn init_alice_and_bob_transports() -> ( } pub async fn init_test<'a>( - monero: &'a Monero<'a>, + monero: &'a Monero, bitcoind: &Bitcoind<'a>, ) -> ( alice::State0, @@ -391,7 +391,7 @@ mod tests { .set_default(); let cli = Cli::default(); - let monero = Monero::new(&cli); + let (monero, _container) = Monero::new(&cli); let bitcoind = init_bitcoind(&cli).await; let alice_db = harness::storage::Database::open(Path::new(ALICE_TEST_DB_FOLDER)).unwrap(); let bob_db = harness::storage::Database::open(Path::new(BOB_TEST_DB_FOLDER)).unwrap(); @@ -468,21 +468,11 @@ mod tests { .await .unwrap(); - let alice_final_xmr_balance = alice_node - .monero_wallet - .0 - .get_balance_alice() - .await - .unwrap(); + let alice_final_xmr_balance = alice_node.monero_wallet.0.get_balance(0).await.unwrap(); - bob_node - .monero_wallet - .0 - .wait_for_bob_wallet_block_height() - .await - .unwrap(); + monero.wait_for_bob_wallet_block_height().await.unwrap(); - let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance_bob().await.unwrap(); + let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance(0).await.unwrap(); assert_eq!( alice_final_btc_balance, diff --git a/xmr-btc/tests/harness/storage.rs b/xmr-btc/tests/harness/storage.rs index 19b1b401..1f2947cb 100644 --- a/xmr-btc/tests/harness/storage.rs +++ b/xmr-btc/tests/harness/storage.rs @@ -106,7 +106,7 @@ mod tests { async fn recover_state_from_db() { let db = Database::open(Path::new("../target/test_recover.db")).unwrap(); - let a = crate::bitcoin::SecretKey::new_random(&mut OsRng); + let a = xmr_btc::bitcoin::SecretKey::new_random(&mut OsRng); let s_a = cross_curve_dleq::Scalar::random(&mut OsRng); let s_b = monero::PrivateKey::from_scalar(Scalar::random(&mut OsRng)); let v_a = xmr_btc::monero::PrivateViewKey::new_random(&mut OsRng); @@ -125,7 +125,7 @@ mod tests { S_a_bitcoin, v: v_a, btc: ::bitcoin::Amount::from_sat(100), - xmr: crate::monero::Amount::from_piconero(1000), + xmr: xmr_btc::monero::Amount::from_piconero(1000), refund_timelock: 0, refund_address: ::bitcoin::Address::from_str("1L5wSMgerhHg8GZGcsNmAx5EXMRXSKR3He") .unwrap(), From 7dffe4cf5f5d826cccd6d2bf987af0787e23ae0d Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 22 Oct 2020 09:18:57 +1100 Subject: [PATCH 37/52] Implement Clone on states --- xmr-btc/src/alice.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index 5d486161..860027fd 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -131,7 +131,7 @@ impl State { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct State0 { a: bitcoin::SecretKey, #[serde(with = "cross_curve_dleq_scalar")] @@ -219,7 +219,7 @@ impl State0 { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct State1 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, @@ -259,7 +259,7 @@ impl State1 { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct State2 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, @@ -336,7 +336,7 @@ impl State2 { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct State3 { pub a: bitcoin::SecretKey, pub B: bitcoin::PublicKey, @@ -393,7 +393,7 @@ impl State3 { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct State4 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, @@ -500,7 +500,7 @@ impl State4 { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct State5 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, @@ -595,7 +595,7 @@ impl State5 { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct State6 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, From bb946439433fa2385c74c8cd46614e2b9172e6bf Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 22 Oct 2020 09:58:22 +1100 Subject: [PATCH 38/52] Remove data from message until Message0 impl serde --- swap/Cargo.toml | 1 + swap/src/alice.rs | 19 ++++----- swap/src/alice/message0.rs | 18 +++++---- swap/src/bob.rs | 26 +++++++------ swap/src/bob/amounts.rs | 1 + swap/src/bob/message0.rs | 14 ++++--- swap/src/main.rs | 58 +++++++++++++++++++++------- swap/src/network/request_response.rs | 8 ++-- 8 files changed, 93 insertions(+), 52 deletions(-) diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 2c217182..7bfb32ea 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -32,5 +32,6 @@ tracing-core = "0.1" tracing-futures = { version = "0.2", features = ["std-future", "futures-03"] } tracing-log = "0.1" tracing-subscriber = { version = "0.2", default-features = false, features = ["fmt", "ansi", "env-filter"] } +url = "2.1" void = "1" xmr-btc = { path = "../xmr-btc" } \ No newline at end of file diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 734cbed5..ac46a8ba 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -32,7 +32,7 @@ pub async fn swap( redeem_address: ::bitcoin::Address, punish_address: ::bitcoin::Address, ) -> Result<()> { - let mut message0: Option = None; + let message0: Option = None; let mut last_amounts: Option = None; let mut swarm = new_swarm(listen)?; @@ -48,13 +48,12 @@ pub async fn swap( last_amounts = Some(p); swarm.send(channel, AliceToBob::Amounts(p)); } - OutEvent::Message0(msg) => { + OutEvent::Message0 => { debug!("Got message0 from Bob"); // TODO: Do this in a more Rusty/functional way. - message0 = Some(msg); + // message0 = Some(msg); break; } - other => panic!("unexpected event: {:?}", other), }; } @@ -78,9 +77,9 @@ pub async fn swap( ); swarm.set_state0(state0.clone()); - let state1 = match message0 { + let _state1 = match message0 { Some(msg) => state0.receive(msg), - None => unreachable!("should have msg by here"), + None => todo!("implement serde on Message0"), }; tracing::warn!("parking thread ..."); @@ -117,7 +116,8 @@ fn new_swarm(listen: Multiaddr) -> Result { pub enum OutEvent { ConnectionEstablished(PeerId), Request(amounts::OutEvent), - Message0(bob::Message0), + // Message0(bob::Message0), + Message0, } impl From for OutEvent { @@ -139,7 +139,8 @@ impl From for OutEvent { impl From for OutEvent { fn from(event: message0::OutEvent) -> Self { match event { - message0::OutEvent::Msg(msg) => OutEvent::Message0(msg), + // message0::OutEvent::Msg(msg) => OutEvent::Message0(msg), + message0::OutEvent::Msg => OutEvent::Message0, } } } @@ -171,7 +172,7 @@ impl Alice { } pub fn set_state0(&mut self, state: State0) { - self.message0.set_state(state); + let _ = self.message0.set_state(state); } } diff --git a/swap/src/alice/message0.rs b/swap/src/alice/message0.rs index c1631bb9..699a7114 100644 --- a/swap/src/alice/message0.rs +++ b/swap/src/alice/message0.rs @@ -7,7 +7,6 @@ use libp2p::{ swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters}, NetworkBehaviour, }; -use rand::rngs::OsRng; use std::{ collections::VecDeque, task::{Context, Poll}, @@ -16,11 +15,12 @@ use std::{ use tracing::error; use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}; -use xmr_btc::{alice::State0, bob}; +use xmr_btc::alice::State0; #[derive(Debug)] pub enum OutEvent { - Msg(bob::Message0), + // Msg(bob::Message0), + Msg, } /// A `NetworkBehaviour` that represents getting the amounts of an XMR/BTC swap. @@ -85,16 +85,18 @@ impl NetworkBehaviourEventProcess> channel, }, } => match request { - BobToAlice::Message0(msg) => { - let response = match self.state { + BobToAlice::Message0 => { + let response = match &self.state { None => panic!("No state, did you forget to set it?"), - Some(state) => { + Some(_state) => { // TODO: Get OsRng from somewhere? - AliceToBob::Message0(state.next_message(&mut OsRng)) + // AliceToBob::Message0(state.next_message(&mut OsRng)) + AliceToBob::Message0 } }; self.rr.send_response(channel, response); - self.events.push_back(OutEvent::Msg(msg)); + + self.events.push_back(OutEvent::Msg); } _ => panic!("unexpected request"), }, diff --git a/swap/src/bob.rs b/swap/src/bob.rs index 41db2b36..4504b3c9 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -6,7 +6,7 @@ use futures::{ StreamExt, }; use libp2p::{core::identity::Keypair, Multiaddr, NetworkBehaviour, PeerId}; -use rand::{CryptoRng, RngCore}; +use rand::rngs::OsRng; use std::{process, thread, time::Duration}; use tracing::{debug, info, warn}; @@ -23,23 +23,20 @@ use crate::{ Cmd, Rsp, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; use xmr_btc::{ - alice, bitcoin::BuildTxLockPsbt, bob::{self, State0}, }; -pub async fn swap( +pub async fn swap( btc: u64, addr: Multiaddr, mut cmd_tx: Sender, mut rsp_rx: Receiver, - rng: &mut R, refund_address: ::bitcoin::Address, - wallet: &W, + _wallet: W, ) -> Result<()> where - W: BuildTxLockPsbt, - R: RngCore + CryptoRng, + W: BuildTxLockPsbt + Send + Sync + 'static, { let mut swarm = new_swarm()?; @@ -73,6 +70,7 @@ where let xmr = xmr_btc::monero::Amount::from_piconero(xmr.as_piconero()); let btc = ::bitcoin::Amount::from_sat(btc.as_sat()); + let rng = &mut OsRng; let state0 = State0::new( rng, btc, @@ -82,9 +80,11 @@ where refund_address, ); swarm.send_message0(alice.clone(), state0.next_message(rng)); - let state1 = match swarm.next().await { - OutEvent::Message0(msg) => { - state0.receive(wallet, msg) // TODO: More graceful error handling. + let _state1 = match swarm.next().await { + OutEvent::Message0 => { + // state0.receive(wallet, msg) // TODO: More graceful error + // handling. + println!("TODO: receive after serde is done for Message0") } other => panic!("unexpected event: {:?}", other), }; @@ -120,7 +120,8 @@ fn new_swarm() -> Result { pub enum OutEvent { ConnectionEstablished(PeerId), Amounts(amounts::OutEvent), - Message0(alice::Message0), + // Message0(alice::Message0), + Message0, } impl From for OutEvent { @@ -142,7 +143,8 @@ impl From for OutEvent { impl From for OutEvent { fn from(event: message0::OutEvent) -> Self { match event { - message0::OutEvent::Msg(msg) => OutEvent::Message0(msg), + // message0::OutEvent::Msg(msg) => OutEvent::Message0(msg), + message0::OutEvent::Msg => OutEvent::Message0, } } } diff --git a/swap/src/bob/amounts.rs b/swap/src/bob/amounts.rs index 6a329a00..18071985 100644 --- a/swap/src/bob/amounts.rs +++ b/swap/src/bob/amounts.rs @@ -85,6 +85,7 @@ impl NetworkBehaviourEventProcess> }, } => match response { AliceToBob::Amounts(p) => self.events.push_back(OutEvent::Amounts(p)), + AliceToBob::Message0 => panic!("shouldn't get message0 here"), }, RequestResponseEvent::InboundFailure { .. } => { diff --git a/swap/src/bob/message0.rs b/swap/src/bob/message0.rs index e5d3d215..7aba6f23 100644 --- a/swap/src/bob/message0.rs +++ b/swap/src/bob/message0.rs @@ -14,11 +14,12 @@ use std::{ use tracing::error; use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}; -use xmr_btc::{alice, bob}; +use xmr_btc::bob; #[derive(Debug)] pub enum OutEvent { - Msg(alice::Message0), + // Msg(alice::Message0), + Msg, } /// A `NetworkBehaviour` that represents send/recv of message 0. @@ -46,8 +47,9 @@ impl Message0 { } } - pub fn send(&mut self, alice: PeerId, msg: bob::Message0) { - let msg = BobToAlice::Message0(msg); + pub fn send(&mut self, alice: PeerId, _msg: bob::Message0) { + // let msg = BobToAlice::Message0(msg); + let msg = BobToAlice::Message0; let _id = self.rr.send_request(&alice, msg); } @@ -79,7 +81,9 @@ impl NetworkBehaviourEventProcess> request_id: _, }, } => match response { - AliceToBob::Message0(msg) => self.events.push_back(OutEvent::Msg(msg)), + // AliceToBob::Message0(msg) => self.events.push_back(OutEvent::Msg(msg)), + AliceToBob::Message0 => self.events.push_back(OutEvent::Msg), + AliceToBob::Amounts(_) => panic!("shouldn't get amounts here"), }, RequestResponseEvent::InboundFailure { .. } => { diff --git a/swap/src/main.rs b/swap/src/main.rs index f796cb75..f5b60f42 100644 --- a/swap/src/main.rs +++ b/swap/src/main.rs @@ -20,17 +20,20 @@ use rand::rngs::OsRng; use std::{io, io::Write, process}; use structopt::StructOpt; use tracing::info; +use url::Url; + mod cli; mod trace; use cli::Options; -use swap::{alice, bob, Cmd, Rsp, SwapParams}; - +use swap::{alice, bitcoin::Wallet, bob, Cmd, Rsp, SwapParams}; +use xmr_btc::bitcoin::BuildTxLockPsbt; // TODO: Add root seed file instead of generating new seed each run. // Alice's address and port until we have a config file. pub const PORT: u16 = 9876; // Arbitrarily chosen. pub const ADDR: &str = "127.0.0.1"; +pub const BITCOIND_JSON_RPC_URL: &str = "127.0.0.1:8332"; #[tokio::main] async fn main() -> Result<()> { @@ -48,23 +51,40 @@ async fn main() -> Result<()> { bail!("Alice cannot set the amount to swap via the cli"); } - // TODO: Get these addresses from command line - let redeem = bitcoin::Address::default(); - let punish = bitcoin::Address::default(); - - swap_as_alice(alice, redeem, refund).await?; + let url = Url::parse(BITCOIND_JSON_RPC_URL).expect("failed to parse url"); + let bitcoin_wallet = Wallet::new("alice", &url) + .await + .expect("failed to create bitcoin wallet"); + + let redeem = bitcoin_wallet + .new_address() + .await + .expect("failed to get new redeem address"); + let punish = bitcoin_wallet + .new_address() + .await + .expect("failed to get new punish address"); + + swap_as_alice(alice.clone(), redeem, punish).await?; } else { info!("running swap node as Bob ..."); - // TODO: Get refund address from command line - let refund = bitcoin::Address::default(); + let url = Url::parse(BITCOIND_JSON_RPC_URL).expect("failed to parse url"); + let bitcoin_wallet = Wallet::new("bob", &url) + .await + .expect("failed to create bitcoin wallet"); + + let refund = bitcoin_wallet + .new_address() + .await + .expect("failed to get new address"); match (opt.piconeros, opt.satoshis) { (Some(_), Some(_)) => bail!("Please supply only a single amount to swap"), (None, None) => bail!("Please supply an amount to swap"), (Some(_picos), _) => todo!("support starting with picos"), (None, Some(sats)) => { - swap_as_bob(sats, alice_addr, refund).await?; + swap_as_bob(sats, alice, refund, bitcoin_wallet).await?; } }; } @@ -74,16 +94,24 @@ async fn main() -> Result<()> { async fn swap_as_alice( addr: Multiaddr, - redeem: bitcoin::Address::default(), - punish: bitcoin::Address::default(), + redeem: bitcoin::Address, + punish: bitcoin::Address, ) -> Result<()> { - alice::swap(addr, OsRng, redeem, punish).await + alice::swap(addr, &mut OsRng, redeem, punish).await } -async fn swap_as_bob(sats: u64, addr: Multiaddr, refund: bitcoin::Address) -> Result<()> { +async fn swap_as_bob( + sats: u64, + addr: Multiaddr, + refund: bitcoin::Address, + wallet: W, +) -> Result<()> +where + W: BuildTxLockPsbt + Send + Sync + 'static, +{ let (cmd_tx, mut cmd_rx) = mpsc::channel(1); let (mut rsp_tx, rsp_rx) = mpsc::channel(1); - tokio::spawn(bob::swap(sats, addr, cmd_tx, rsp_rx, OsRng, refund)); + tokio::spawn(bob::swap(sats, addr, cmd_tx, rsp_rx, refund, wallet)); loop { let read = cmd_rx.next().await; match read { diff --git a/swap/src/network/request_response.rs b/swap/src/network/request_response.rs index 4af3a222..86420bee 100644 --- a/swap/src/network/request_response.rs +++ b/swap/src/network/request_response.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use std::{fmt::Debug, io}; use crate::SwapParams; -use xmr_btc::{alice, bob, monero}; +use xmr_btc::monero; /// Time to wait for a response back once we send a request. pub const TIMEOUT: u64 = 3600; // One hour. @@ -19,14 +19,16 @@ pub enum BobToAlice { #[serde(with = "::bitcoin::util::amount::serde::as_sat")] AmountsFromBtc(::bitcoin::Amount), AmountsFromXmr(monero::Amount), - Message0(bob::Message0), + Message0, + // Message0(bob::Message0), } /// Messages Alice sends to Bob. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum AliceToBob { Amounts(SwapParams), - Message0(alice::Message0), + Message0, + // Message0(alice::Message0), } #[derive(Debug, Clone, Copy, Default)] From ad0d563d93c0d6103ce768713fbeadace0527040 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 22 Oct 2020 10:37:06 +1100 Subject: [PATCH 39/52] Use Normal secp256k1::Point --- xmr-btc/Cargo.toml | 2 +- xmr-btc/src/bitcoin.rs | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index 6aff2c90..62044dcb 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" anyhow = "1" async-trait = "0.1" bitcoin = { version = "0.23", features = ["rand", "serde"] } -cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "49171f5e08473d46f951fb1fc338fe437d974d3c" } +cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "3fc1726e4511d53a6cbcb91a42f479238eca688b" } curve25519-dalek = "2" ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat", "serde", "serialization"] } ed25519-dalek = { version = "1.0.0-pre.4", features = ["serde"] }# Cannot be 1 because they depend on curve25519-dalek version 3 diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index d3c96d15..f85c0271 100644 --- a/xmr-btc/src/bitcoin.rs +++ b/xmr-btc/src/bitcoin.rs @@ -10,10 +10,7 @@ use bitcoin::{ }; use ecdsa_fun::{ adaptor::Adaptor, - fun::{ - marker::{Jacobian, Mark}, - Point, Scalar, - }, + fun::{Point, Scalar}, nonce::Deterministic, ECDSA, }; @@ -87,9 +84,9 @@ impl SecretKey { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct PublicKey(Point); -impl From for Point { +impl From for Point { fn from(from: PublicKey) -> Self { - from.0.mark::() + from.0 } } From ad0d8d5713a2e0c1b0f358ceab2a3d44ff253c28 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 22 Oct 2020 11:04:49 +1100 Subject: [PATCH 40/52] Emit message0 out of NB (both parties) --- swap/src/alice.rs | 15 +++++------ swap/src/alice/message0.rs | 15 +++++------ swap/src/bob.rs | 15 +++++------ swap/src/bob/amounts.rs | 2 +- swap/src/bob/message0.rs | 13 ++++------ swap/src/network/request_response.rs | 10 ++++---- xmr-btc/src/alice/message.rs | 6 +++-- xmr-btc/src/bob/message.rs | 6 +++-- xmr-btc/src/serde.rs | 38 ++++++++++++++++++++++++++++ 9 files changed, 77 insertions(+), 43 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index ac46a8ba..d0cedd23 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -26,13 +26,14 @@ use xmr_btc::{alice::State0, bob, monero}; pub type Swarm = libp2p::Swarm; +#[allow(unused_assignments)] // Due to the mutable message0? pub async fn swap( listen: Multiaddr, rng: &mut R, redeem_address: ::bitcoin::Address, punish_address: ::bitcoin::Address, ) -> Result<()> { - let message0: Option = None; + let mut message0: Option = None; let mut last_amounts: Option = None; let mut swarm = new_swarm(listen)?; @@ -48,10 +49,10 @@ pub async fn swap( last_amounts = Some(p); swarm.send(channel, AliceToBob::Amounts(p)); } - OutEvent::Message0 => { + OutEvent::Message0(msg) => { debug!("Got message0 from Bob"); // TODO: Do this in a more Rusty/functional way. - // message0 = Some(msg); + message0 = Some(msg); break; } }; @@ -79,7 +80,7 @@ pub async fn swap( let _state1 = match message0 { Some(msg) => state0.receive(msg), - None => todo!("implement serde on Message0"), + None => panic!("should have the message by here"), }; tracing::warn!("parking thread ..."); @@ -116,8 +117,7 @@ fn new_swarm(listen: Multiaddr) -> Result { pub enum OutEvent { ConnectionEstablished(PeerId), Request(amounts::OutEvent), - // Message0(bob::Message0), - Message0, + Message0(bob::Message0), } impl From for OutEvent { @@ -139,8 +139,7 @@ impl From for OutEvent { impl From for OutEvent { fn from(event: message0::OutEvent) -> Self { match event { - // message0::OutEvent::Msg(msg) => OutEvent::Message0(msg), - message0::OutEvent::Msg => OutEvent::Message0, + message0::OutEvent::Msg(msg) => OutEvent::Message0(msg), } } } diff --git a/swap/src/alice/message0.rs b/swap/src/alice/message0.rs index 699a7114..ad62586c 100644 --- a/swap/src/alice/message0.rs +++ b/swap/src/alice/message0.rs @@ -7,6 +7,7 @@ use libp2p::{ swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters}, NetworkBehaviour, }; +use rand::rngs::OsRng; use std::{ collections::VecDeque, task::{Context, Poll}, @@ -15,12 +16,11 @@ use std::{ use tracing::error; use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}; -use xmr_btc::alice::State0; +use xmr_btc::{alice::State0, bob}; #[derive(Debug)] pub enum OutEvent { - // Msg(bob::Message0), - Msg, + Msg(bob::Message0), } /// A `NetworkBehaviour` that represents getting the amounts of an XMR/BTC swap. @@ -85,18 +85,17 @@ impl NetworkBehaviourEventProcess> channel, }, } => match request { - BobToAlice::Message0 => { + BobToAlice::Message0(msg) => { let response = match &self.state { None => panic!("No state, did you forget to set it?"), - Some(_state) => { + Some(state) => { // TODO: Get OsRng from somewhere? - // AliceToBob::Message0(state.next_message(&mut OsRng)) - AliceToBob::Message0 + AliceToBob::Message0(state.next_message(&mut OsRng)) } }; self.rr.send_response(channel, response); - self.events.push_back(OutEvent::Msg); + self.events.push_back(OutEvent::Msg(msg)); } _ => panic!("unexpected request"), }, diff --git a/swap/src/bob.rs b/swap/src/bob.rs index 4504b3c9..ed38cf87 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -23,6 +23,7 @@ use crate::{ Cmd, Rsp, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; use xmr_btc::{ + alice, bitcoin::BuildTxLockPsbt, bob::{self, State0}, }; @@ -33,7 +34,7 @@ pub async fn swap( mut cmd_tx: Sender, mut rsp_rx: Receiver, refund_address: ::bitcoin::Address, - _wallet: W, + wallet: W, ) -> Result<()> where W: BuildTxLockPsbt + Send + Sync + 'static, @@ -81,10 +82,8 @@ where ); swarm.send_message0(alice.clone(), state0.next_message(rng)); let _state1 = match swarm.next().await { - OutEvent::Message0 => { - // state0.receive(wallet, msg) // TODO: More graceful error - // handling. - println!("TODO: receive after serde is done for Message0") + OutEvent::Message0(msg) => { + state0.receive(&wallet, msg) // TODO: More graceful error handling. } other => panic!("unexpected event: {:?}", other), }; @@ -120,8 +119,7 @@ fn new_swarm() -> Result { pub enum OutEvent { ConnectionEstablished(PeerId), Amounts(amounts::OutEvent), - // Message0(alice::Message0), - Message0, + Message0(alice::Message0), } impl From for OutEvent { @@ -143,8 +141,7 @@ impl From for OutEvent { impl From for OutEvent { fn from(event: message0::OutEvent) -> Self { match event { - // message0::OutEvent::Msg(msg) => OutEvent::Message0(msg), - message0::OutEvent::Msg => OutEvent::Message0, + message0::OutEvent::Msg(msg) => OutEvent::Message0(msg), } } } diff --git a/swap/src/bob/amounts.rs b/swap/src/bob/amounts.rs index 18071985..4443a1e1 100644 --- a/swap/src/bob/amounts.rs +++ b/swap/src/bob/amounts.rs @@ -85,7 +85,7 @@ impl NetworkBehaviourEventProcess> }, } => match response { AliceToBob::Amounts(p) => self.events.push_back(OutEvent::Amounts(p)), - AliceToBob::Message0 => panic!("shouldn't get message0 here"), + AliceToBob::Message0(_) => panic!("shouldn't get message0 here"), }, RequestResponseEvent::InboundFailure { .. } => { diff --git a/swap/src/bob/message0.rs b/swap/src/bob/message0.rs index 7aba6f23..8e3a0ad8 100644 --- a/swap/src/bob/message0.rs +++ b/swap/src/bob/message0.rs @@ -14,12 +14,11 @@ use std::{ use tracing::error; use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}; -use xmr_btc::bob; +use xmr_btc::{alice, bob}; #[derive(Debug)] pub enum OutEvent { - // Msg(alice::Message0), - Msg, + Msg(alice::Message0), } /// A `NetworkBehaviour` that represents send/recv of message 0. @@ -47,9 +46,8 @@ impl Message0 { } } - pub fn send(&mut self, alice: PeerId, _msg: bob::Message0) { - // let msg = BobToAlice::Message0(msg); - let msg = BobToAlice::Message0; + pub fn send(&mut self, alice: PeerId, msg: bob::Message0) { + let msg = BobToAlice::Message0(msg); let _id = self.rr.send_request(&alice, msg); } @@ -81,8 +79,7 @@ impl NetworkBehaviourEventProcess> request_id: _, }, } => match response { - // AliceToBob::Message0(msg) => self.events.push_back(OutEvent::Msg(msg)), - AliceToBob::Message0 => self.events.push_back(OutEvent::Msg), + AliceToBob::Message0(msg) => self.events.push_back(OutEvent::Msg(msg)), AliceToBob::Amounts(_) => panic!("shouldn't get amounts here"), }, diff --git a/swap/src/network/request_response.rs b/swap/src/network/request_response.rs index 86420bee..b6e582c5 100644 --- a/swap/src/network/request_response.rs +++ b/swap/src/network/request_response.rs @@ -8,27 +8,27 @@ use serde::{Deserialize, Serialize}; use std::{fmt::Debug, io}; use crate::SwapParams; -use xmr_btc::monero; +use xmr_btc::{alice, bob, monero}; /// Time to wait for a response back once we send a request. pub const TIMEOUT: u64 = 3600; // One hour. /// Messages Bob sends to Alice. #[derive(Clone, Debug, Serialize, Deserialize)] +#[allow(clippy::large_enum_variant)] pub enum BobToAlice { #[serde(with = "::bitcoin::util::amount::serde::as_sat")] AmountsFromBtc(::bitcoin::Amount), AmountsFromXmr(monero::Amount), - Message0, - // Message0(bob::Message0), + Message0(bob::Message0), } /// Messages Alice sends to Bob. #[derive(Clone, Debug, Serialize, Deserialize)] +#[allow(clippy::large_enum_variant)] pub enum AliceToBob { Amounts(SwapParams), - Message0, - // Message0(alice::Message0), + Message0(alice::Message0), } #[derive(Debug, Clone, Copy, Default)] diff --git a/xmr-btc/src/alice/message.rs b/xmr-btc/src/alice/message.rs index 052731b1..ada3636f 100644 --- a/xmr-btc/src/alice/message.rs +++ b/xmr-btc/src/alice/message.rs @@ -1,8 +1,9 @@ use anyhow::Result; use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; +use serde::{Deserialize, Serialize}; use std::convert::TryFrom; -use crate::{bitcoin, monero}; +use crate::{bitcoin, monero, serde::cross_curve_dleq_proof}; #[derive(Debug)] pub enum Message { @@ -11,11 +12,12 @@ pub enum Message { Message2(Message2), } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Message0 { pub(crate) A: bitcoin::PublicKey, pub(crate) S_a_monero: monero::PublicKey, pub(crate) S_a_bitcoin: bitcoin::PublicKey, + #[serde(with = "cross_curve_dleq_proof")] pub(crate) dleq_proof_s_a: cross_curve_dleq::Proof, pub(crate) v_a: monero::PrivateViewKey, pub(crate) redeem_address: bitcoin::Address, diff --git a/xmr-btc/src/bob/message.rs b/xmr-btc/src/bob/message.rs index f45c3b92..465241f6 100644 --- a/xmr-btc/src/bob/message.rs +++ b/xmr-btc/src/bob/message.rs @@ -1,6 +1,7 @@ -use crate::{bitcoin, monero}; +use crate::{bitcoin, monero, serde::cross_curve_dleq_proof}; use anyhow::Result; use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; +use serde::{Deserialize, Serialize}; use std::convert::TryFrom; #[derive(Clone, Debug)] @@ -11,11 +12,12 @@ pub enum Message { Message3(Message3), } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Message0 { pub(crate) B: bitcoin::PublicKey, pub(crate) S_b_monero: monero::PublicKey, pub(crate) S_b_bitcoin: bitcoin::PublicKey, + #[serde(with = "cross_curve_dleq_proof")] pub(crate) dleq_proof_s_b: cross_curve_dleq::Proof, pub(crate) v_b: monero::PrivateViewKey, pub(crate) refund_address: bitcoin::Address, diff --git a/xmr-btc/src/serde.rs b/xmr-btc/src/serde.rs index e64cfd10..5e6e7052 100644 --- a/xmr-btc/src/serde.rs +++ b/xmr-btc/src/serde.rs @@ -88,6 +88,44 @@ pub mod cross_curve_dleq_scalar { } } +pub mod cross_curve_dleq_proof { + use serde::{de, de::Visitor, Deserializer, Serializer}; + use std::fmt; + + struct MyVisitor; + + impl<'de> Visitor<'de> for MyVisitor { + type Value = cross_curve_dleq::Proof; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "todo") + } + + fn visit_bytes(self, _s: &[u8]) -> Result + where + E: de::Error, + { + todo!("visit_bytes") + } + } + + pub fn serialize(_x: &cross_curve_dleq::Proof, _s: S) -> Result + where + S: Serializer, + { + todo!("serialize") + } + + pub fn deserialize<'de, D>( + _deserializer: D, + ) -> Result>::Error> + where + D: Deserializer<'de>, + { + todo!("deserialize") + } +} + pub mod monero_private_key { use serde::{de, de::Visitor, Deserializer, Serializer}; use std::fmt; From a0987ee2b885763bb63a6c0b00d76d69b9795615 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 22 Oct 2020 11:19:43 +1100 Subject: [PATCH 41/52] Use upstream serder for dleq Proof --- xmr-btc/Cargo.toml | 2 +- xmr-btc/src/alice/message.rs | 3 +-- xmr-btc/src/bob/message.rs | 3 +-- xmr-btc/src/serde.rs | 38 ------------------------------------ 4 files changed, 3 insertions(+), 43 deletions(-) diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index 62044dcb..c37df001 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" anyhow = "1" async-trait = "0.1" bitcoin = { version = "0.23", features = ["rand", "serde"] } -cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "3fc1726e4511d53a6cbcb91a42f479238eca688b" } +cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "1931c0436f259e1a1f53a4ec8acbbaaf614bd1e4", features = ["serde"] } curve25519-dalek = "2" ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat", "serde", "serialization"] } ed25519-dalek = { version = "1.0.0-pre.4", features = ["serde"] }# Cannot be 1 because they depend on curve25519-dalek version 3 diff --git a/xmr-btc/src/alice/message.rs b/xmr-btc/src/alice/message.rs index ada3636f..79b66986 100644 --- a/xmr-btc/src/alice/message.rs +++ b/xmr-btc/src/alice/message.rs @@ -3,7 +3,7 @@ use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; -use crate::{bitcoin, monero, serde::cross_curve_dleq_proof}; +use crate::{bitcoin, monero}; #[derive(Debug)] pub enum Message { @@ -17,7 +17,6 @@ pub struct Message0 { pub(crate) A: bitcoin::PublicKey, pub(crate) S_a_monero: monero::PublicKey, pub(crate) S_a_bitcoin: bitcoin::PublicKey, - #[serde(with = "cross_curve_dleq_proof")] pub(crate) dleq_proof_s_a: cross_curve_dleq::Proof, pub(crate) v_a: monero::PrivateViewKey, pub(crate) redeem_address: bitcoin::Address, diff --git a/xmr-btc/src/bob/message.rs b/xmr-btc/src/bob/message.rs index 465241f6..2826a545 100644 --- a/xmr-btc/src/bob/message.rs +++ b/xmr-btc/src/bob/message.rs @@ -1,4 +1,4 @@ -use crate::{bitcoin, monero, serde::cross_curve_dleq_proof}; +use crate::{bitcoin, monero}; use anyhow::Result; use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; use serde::{Deserialize, Serialize}; @@ -17,7 +17,6 @@ pub struct Message0 { pub(crate) B: bitcoin::PublicKey, pub(crate) S_b_monero: monero::PublicKey, pub(crate) S_b_bitcoin: bitcoin::PublicKey, - #[serde(with = "cross_curve_dleq_proof")] pub(crate) dleq_proof_s_b: cross_curve_dleq::Proof, pub(crate) v_b: monero::PrivateViewKey, pub(crate) refund_address: bitcoin::Address, diff --git a/xmr-btc/src/serde.rs b/xmr-btc/src/serde.rs index 5e6e7052..e64cfd10 100644 --- a/xmr-btc/src/serde.rs +++ b/xmr-btc/src/serde.rs @@ -88,44 +88,6 @@ pub mod cross_curve_dleq_scalar { } } -pub mod cross_curve_dleq_proof { - use serde::{de, de::Visitor, Deserializer, Serializer}; - use std::fmt; - - struct MyVisitor; - - impl<'de> Visitor<'de> for MyVisitor { - type Value = cross_curve_dleq::Proof; - - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(formatter, "todo") - } - - fn visit_bytes(self, _s: &[u8]) -> Result - where - E: de::Error, - { - todo!("visit_bytes") - } - } - - pub fn serialize(_x: &cross_curve_dleq::Proof, _s: S) -> Result - where - S: Serializer, - { - todo!("serialize") - } - - pub fn deserialize<'de, D>( - _deserializer: D, - ) -> Result>::Error> - where - D: Deserializer<'de>, - { - todo!("deserialize") - } -} - pub mod monero_private_key { use serde::{de, de::Visitor, Deserializer, Serializer}; use std::fmt; From 5395303a99b0e66a5380e8d14ba48f573d6950fc Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Thu, 22 Oct 2020 10:57:42 +1100 Subject: [PATCH 42/52] Test on-chain protocol happy path --- xmr-btc/Cargo.toml | 3 +- xmr-btc/src/bob.rs | 6 +- xmr-btc/src/lib.rs | 130 +++++++++++------- xmr-btc/src/monero.rs | 20 ++- xmr-btc/tests/e2e.rs | 170 ++---------------------- xmr-btc/tests/harness/mod.rs | 153 +++++++++++++++++++++ xmr-btc/tests/harness/wallet/bitcoin.rs | 47 +++++-- xmr-btc/tests/harness/wallet/monero.rs | 18 ++- xmr-btc/tests/on_chain.rs | 155 ++++++++++++++++++--- 9 files changed, 457 insertions(+), 245 deletions(-) diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index 3e1457de..4e22e421 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -19,12 +19,13 @@ monero = "0.9" rand = "0.7" sha2 = "0.9" thiserror = "1" +tokio = { version = "0.2", features = ["time"] } tracing = "0.1" [dev-dependencies] backoff = { version = "0.2", features = ["tokio"] } base64 = "0.12" -bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "d402b36d3d6406150e3bfb71492ff4a0a7cb290e" } +bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "7ff30a559ab57cc3aa71189e71433ef6b2a6c3a2" } futures = "0.3" monero-harness = { path = "../monero-harness" } reqwest = { version = "0.10", default-features = false } diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 2b62c749..51bd2a88 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -50,13 +50,15 @@ pub async fn next_state< let message1 = transport.receive_message().await?.try_into()?; let state2 = state1.receive(message1)?; + + let message2 = state2.next_message(); + transport.send_message(message2.into()).await?; 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) => { diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 1b2b6e24..c51ca999 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -59,6 +59,14 @@ use futures::{ }; use genawaiter::sync::{Gen, GenBoxed}; use sha2::Sha256; +use std::{sync::Arc, time::Duration}; +use tokio::time::timeout; +use tracing::error; + +// TODO: Replace this with something configurable, such as an function argument. +/// Time that Bob has to publish the Bitcoin lock transaction before both +/// parties will abort, in seconds. +const SECS_TO_ACT_BOB: u64 = 60; #[allow(clippy::large_enum_variant)] #[derive(Debug)] @@ -80,8 +88,13 @@ pub trait ReceiveTransferProof { } #[async_trait] -pub trait MedianTime { - async fn median_time(&self) -> u32; +pub trait BlockHeight { + async fn block_height(&self) -> u32; +} + +#[async_trait] +pub trait TransactionBlockHeight { + async fn transaction_block_height(&self, txid: bitcoin::Txid) -> u32; } /// Perform the on-chain protocol to swap monero and bitcoin as Bob. @@ -89,9 +102,9 @@ pub trait MedianTime { /// This is called post handshake, after all the keys, addresses and most of the /// signatures have been exchanged. pub fn action_generator_bob( - network: &'static mut N, - monero_client: &'static M, - bitcoin_client: &'static B, + mut network: N, + monero_client: Arc, + bitcoin_client: Arc, // TODO: Replace this with a new, slimmer struct? bob::State2 { A, @@ -111,10 +124,16 @@ pub fn action_generator_bob( }: bob::State2, ) -> GenBoxed where - N: ReceiveTransferProof + Send + Sync, - M: monero::WatchForTransfer + Send + Sync, - B: MedianTime + bitcoin::WatchForRawTransaction + Send + Sync, + N: ReceiveTransferProof + Send + Sync + 'static, + M: monero::WatchForTransfer + Send + Sync + 'static, + B: BlockHeight + + TransactionBlockHeight + + bitcoin::WatchForRawTransaction + + Send + + Sync + + 'static, { + #[derive(Debug)] enum SwapFailed { BeforeBtcLock, AfterBtcLock(Reason), @@ -122,6 +141,7 @@ where } /// Reason why the swap has failed. + #[derive(Debug)] enum Reason { /// The refund timelock has been reached. BtcExpired, @@ -140,37 +160,40 @@ where if condition_future.clone().await { return; } + + tokio::time::delay_for(std::time::Duration::from_secs(1)).await; } } - async fn bitcoin_time_is_gte(bitcoin_client: &B, timestamp: u32) -> bool + async fn bitcoin_block_height_is_gte(bitcoin_client: &B, n_blocks: u32) -> bool where - B: MedianTime, + B: BlockHeight, { - bitcoin_client.median_time().await >= timestamp + bitcoin_client.block_height().await >= n_blocks } Gen::new_boxed(|co| async move { let swap_result: Result<(), SwapFailed> = async { - let btc_has_expired = bitcoin_time_is_gte(bitcoin_client, refund_timelock).shared(); - let poll_until_btc_has_expired = poll_until(btc_has_expired.clone()).shared(); - futures::pin_mut!(poll_until_btc_has_expired); - - if btc_has_expired.clone().await { - return Err(SwapFailed::BeforeBtcLock); - } - co.yield_(BobAction::LockBitcoin(tx_lock.clone())).await; - match select( + timeout( + Duration::from_secs(SECS_TO_ACT_BOB), bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), - poll_until_btc_has_expired.clone(), ) .await - { - Either::Left(_) => {} - Either::Right(_) => return Err(SwapFailed::BeforeBtcLock), - } + .map(|tx| tx.txid()) + .map_err(|_| SwapFailed::BeforeBtcLock)?; + + let tx_lock_height = bitcoin_client + .transaction_block_height(tx_lock.txid()) + .await; + let btc_has_expired = bitcoin_block_height_is_gte( + bitcoin_client.as_ref(), + tx_lock_height + refund_timelock, + ) + .shared(); + let poll_until_btc_has_expired = poll_until(btc_has_expired).shared(); + futures::pin_mut!(poll_until_btc_has_expired); let transfer_proof = match select( network.receive_transfer_proof(), @@ -245,7 +268,9 @@ where } .await; - if let Err(SwapFailed::AfterBtcLock(_)) = swap_result { + if let Err(err @ SwapFailed::AfterBtcLock(_)) = swap_result { + error!("Swap failed, reason: {:?}", err); + let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, A.clone(), b.public()); let tx_cancel_txid = tx_cancel.txid(); @@ -316,10 +341,9 @@ pub trait ReceiveBitcoinRedeemEncsig { /// /// This is called post handshake, after all the keys, addresses and most of the /// signatures have been exchanged. -pub fn action_generator_alice( - network: &'static mut N, - _monero_client: &'static M, - bitcoin_client: &'static B, +pub fn action_generator_alice( + mut network: N, + bitcoin_client: Arc, // TODO: Replace this with a new, slimmer struct? alice::State3 { a, @@ -341,16 +365,22 @@ pub fn action_generator_alice( }: alice::State3, ) -> GenBoxed where - N: ReceiveBitcoinRedeemEncsig + Send + Sync, - M: Send + Sync, - B: MedianTime + bitcoin::WatchForRawTransaction + Send + Sync, + N: ReceiveBitcoinRedeemEncsig + Send + Sync + 'static, + B: BlockHeight + + TransactionBlockHeight + + bitcoin::WatchForRawTransaction + + Send + + Sync + + 'static, { + #[derive(Debug)] enum SwapFailed { BeforeBtcLock, AfterXmrLock(Reason), } /// Reason why the swap has failed. + #[derive(Debug)] enum Reason { /// The refund timelock has been reached. BtcExpired, @@ -373,31 +403,37 @@ where if condition_future.clone().await { return; } + + tokio::time::delay_for(std::time::Duration::from_secs(1)).await; } } - async fn bitcoin_time_is_gte(bitcoin_client: &B, timestamp: u32) -> bool + async fn bitcoin_block_height_is_gte(bitcoin_client: &B, n_blocks: u32) -> bool where - B: MedianTime, + B: BlockHeight, { - bitcoin_client.median_time().await >= timestamp + bitcoin_client.block_height().await >= n_blocks } Gen::new_boxed(|co| async move { let swap_result: Result<(), SwapFailed> = async { - let btc_has_expired = bitcoin_time_is_gte(bitcoin_client, refund_timelock).shared(); - let poll_until_btc_has_expired = poll_until(btc_has_expired.clone()).shared(); - futures::pin_mut!(poll_until_btc_has_expired); - - match select( + timeout( + Duration::from_secs(SECS_TO_ACT_BOB), bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), - poll_until_btc_has_expired.clone(), ) .await - { - Either::Left(_) => {} - Either::Right(_) => return Err(SwapFailed::BeforeBtcLock), - } + .map_err(|_| SwapFailed::BeforeBtcLock)?; + + let tx_lock_height = bitcoin_client + .transaction_block_height(tx_lock.txid()) + .await; + let btc_has_expired = bitcoin_block_height_is_gte( + bitcoin_client.as_ref(), + tx_lock_height + refund_timelock, + ) + .shared(); + let poll_until_btc_has_expired = poll_until(btc_has_expired).shared(); + futures::pin_mut!(poll_until_btc_has_expired); let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: s_a.into_ed25519(), @@ -459,7 +495,7 @@ where if let Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)) = swap_result { let refund_result: Result<(), RefundFailed> = async { let bob_can_be_punished = - bitcoin_time_is_gte(bitcoin_client, punish_timelock).shared(); + bitcoin_block_height_is_gte(bitcoin_client.as_ref(), punish_timelock).shared(); let poll_until_bob_can_be_punished = poll_until(bob_can_be_punished).shared(); futures::pin_mut!(poll_until_bob_can_be_punished); diff --git a/xmr-btc/src/monero.rs b/xmr-btc/src/monero.rs index 241666c7..f2459d04 100644 --- a/xmr-btc/src/monero.rs +++ b/xmr-btc/src/monero.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; pub use curve25519_dalek::scalar::Scalar; pub use monero::{Address, PrivateKey, PublicKey}; use rand::{CryptoRng, RngCore}; -use std::ops::Add; +use std::ops::{Add, Sub}; pub const MIN_CONFIRMATIONS: u32 = 10; @@ -51,7 +51,7 @@ impl From for PublicKey { #[derive(Clone, Copy, Debug)] pub struct PublicViewKey(PublicKey); -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] pub struct Amount(u64); impl Amount { @@ -67,6 +67,22 @@ impl Amount { } } +impl Add for Amount { + type Output = Amount; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl Sub for Amount { + type Output = Amount; + + fn sub(self, rhs: Self) -> Self::Output { + Self(self.0 - rhs.0) + } +} + impl From for u64 { fn from(from: Amount) -> u64 { from.0 diff --git a/xmr-btc/tests/e2e.rs b/xmr-btc/tests/e2e.rs index 024fb1ca..a7203ab0 100644 --- a/xmr-btc/tests/e2e.rs +++ b/xmr-btc/tests/e2e.rs @@ -1,156 +1,14 @@ pub mod harness; -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}; - -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( - monero: &Monero, - bitcoind: &Bitcoind<'_>, -) -> ( - alice::State0, - bob::State0, - AliceNode, - BobNode, - 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::Wallet(monero.alice_wallet_rpc_client()); - let bob_monero_wallet = wallet::monero::Wallet(monero.bob_wallet_rpc_client()); - - 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(0).await.unwrap(); - let bob_initial_xmr_balance = bob.monero_wallet.0.get_balance(0).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 { // NOTE: For some reason running these tests overflows the stack. In order to // mitigate this run them with: // // RUST_MIN_STACK=100000000 cargo test - use crate::{ - harness, - harness::node::{run_alice_until, run_bob_until}, - init_bitcoind, init_test, + use crate::harness::{ + self, init_bitcoind, init_test, + node::{run_alice_until, run_bob_until}, }; use futures::future; use monero_harness::Monero; @@ -181,7 +39,7 @@ mod tests { mut bob_node, initial_balances, swap_amounts, - ) = init_test(&monero, &bitcoind).await; + ) = init_test(&monero, &bitcoind, None, None).await; let (alice_state, bob_state) = future::try_join( run_alice_until( @@ -212,11 +70,11 @@ mod tests { .await .unwrap(); - let alice_final_xmr_balance = alice_node.monero_wallet.0.get_balance(0).await.unwrap(); + let alice_final_xmr_balance = alice_node.monero_wallet.get_balance().await.unwrap(); monero.wait_for_bob_wallet_block_height().await.unwrap(); - let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance(0).await.unwrap(); + let bob_final_xmr_balance = bob_node.monero_wallet.get_balance().await.unwrap(); assert_eq!( alice_final_btc_balance, @@ -230,13 +88,11 @@ mod tests { assert_eq!( alice_final_xmr_balance, - initial_balances.alice_xmr - - u64::from(swap_amounts.xmr) - - u64::from(alice_state6.lock_xmr_fee()) + initial_balances.alice_xmr - swap_amounts.xmr - alice_state6.lock_xmr_fee() ); assert_eq!( bob_final_xmr_balance, - initial_balances.bob_xmr + u64::from(swap_amounts.xmr) + initial_balances.bob_xmr + swap_amounts.xmr ); } @@ -257,7 +113,7 @@ mod tests { mut bob_node, initial_balances, swap_amounts, - ) = init_test(&monero, &bitcoind).await; + ) = init_test(&monero, &bitcoind, None, None).await; let (alice_state, bob_state) = future::try_join( run_alice_until( @@ -300,8 +156,8 @@ mod tests { .unwrap(); monero.wait_for_alice_wallet_block_height().await.unwrap(); - let alice_final_xmr_balance = alice_node.monero_wallet.0.get_balance(0).await.unwrap(); - let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance(0).await.unwrap(); + let alice_final_xmr_balance = alice_node.monero_wallet.get_balance().await.unwrap(); + let bob_final_xmr_balance = bob_node.monero_wallet.get_balance().await.unwrap(); assert_eq!(alice_final_btc_balance, initial_balances.alice_btc); assert_eq!( @@ -312,7 +168,7 @@ mod tests { // 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!(alice_final_xmr_balance, swap_amounts.xmr); assert_eq!(bob_final_xmr_balance, initial_balances.bob_xmr); } @@ -333,7 +189,7 @@ mod tests { mut bob_node, initial_balances, swap_amounts, - ) = init_test(&monero, &bitcoind).await; + ) = init_test(&monero, &bitcoind, None, None).await; let (alice_state, bob_state) = future::try_join( run_alice_until( diff --git a/xmr-btc/tests/harness/mod.rs b/xmr-btc/tests/harness/mod.rs index 4f2cba46..6e94edf2 100644 --- a/xmr-btc/tests/harness/mod.rs +++ b/xmr-btc/tests/harness/mod.rs @@ -5,6 +5,10 @@ pub mod wallet; pub mod bob { use xmr_btc::bob::State; + pub fn is_state2(state: &State) -> bool { + matches!(state, State::State2 { .. }) + } + // TODO: use macro or generics pub fn is_state5(state: &State) -> bool { matches!(state, State::State5 { .. }) @@ -19,6 +23,10 @@ pub mod bob { pub mod alice { use xmr_btc::alice::State; + pub fn is_state3(state: &State) -> bool { + matches!(state, State::State3 { .. }) + } + // TODO: use macro or generics pub fn is_state4(state: &State) -> bool { matches!(state, State::State4 { .. }) @@ -34,3 +42,148 @@ pub mod alice { matches!(state, State::State6 { .. }) } } + +use bitcoin_harness::Bitcoind; +use monero_harness::Monero; +use node::{AliceNode, BobNode}; +use rand::rngs::OsRng; +use testcontainers::clients::Cli; +use tokio::sync::{ + mpsc, + mpsc::{Receiver, Sender}, +}; +use transport::Transport; +use xmr_btc::{bitcoin, monero}; + +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 { + pub alice_xmr: monero::Amount, + pub alice_btc: bitcoin::Amount, + pub bob_xmr: monero::Amount, + pub bob_btc: bitcoin::Amount, +} + +pub struct SwapAmounts { + pub xmr: monero::Amount, + pub 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( + monero: &Monero, + bitcoind: &Bitcoind<'_>, + refund_timelock: Option, + punish_timelock: Option, +) -> ( + xmr_btc::alice::State0, + xmr_btc::bob::State0, + AliceNode, + BobNode, + 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::Wallet(monero.alice_wallet_rpc_client()); + let bob_monero_wallet = wallet::monero::Wallet(monero.bob_wallet_rpc_client()); + + 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.get_balance().await.unwrap(); + let bob_initial_xmr_balance = bob.monero_wallet.get_balance().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 = xmr_btc::alice::State0::new( + &mut OsRng, + btc_amount, + xmr_amount, + refund_timelock.unwrap_or(RELATIVE_REFUND_TIMELOCK), + punish_timelock.unwrap_or(RELATIVE_PUNISH_TIMELOCK), + redeem_address.clone(), + punish_address.clone(), + ); + let bob_state0 = xmr_btc::bob::State0::new( + &mut OsRng, + btc_amount, + xmr_amount, + refund_timelock.unwrap_or(RELATIVE_REFUND_TIMELOCK), + punish_timelock.unwrap_or(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, + ) +} diff --git a/xmr-btc/tests/harness/wallet/bitcoin.rs b/xmr-btc/tests/harness/wallet/bitcoin.rs index d31990e8..c39ba3f7 100644 --- a/xmr-btc/tests/harness/wallet/bitcoin.rs +++ b/xmr-btc/tests/harness/wallet/bitcoin.rs @@ -1,6 +1,6 @@ use anyhow::Result; use async_trait::async_trait; -use backoff::{future::FutureOperation as _, ExponentialBackoff}; +use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid}; use bitcoin_harness::{bitcoind_rpc::PsbtBase64, Bitcoind}; use reqwest::Url; @@ -10,7 +10,7 @@ use xmr_btc::{ bitcoin::{ BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxLock, WatchForRawTransaction, }, - MedianTime, + BlockHeight, TransactionBlockHeight, }; #[derive(Debug)] @@ -114,24 +114,45 @@ impl BroadcastSignedTransaction for Wallet { impl WatchForRawTransaction for Wallet { async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction { (|| async { Ok(self.0.get_raw_transaction(txid).await?) }) - .retry(ExponentialBackoff { - max_elapsed_time: None, - ..Default::default() - }) + .retry(ConstantBackoff::new(Duration::from_secs(1))) .await .expect("transient errors to be retried") } } #[async_trait] -impl MedianTime for Wallet { - async fn median_time(&self) -> u32 { - (|| async { Ok(self.0.median_time().await?) }) - .retry(ExponentialBackoff { - max_elapsed_time: None, - ..Default::default() - }) +impl BlockHeight for Wallet { + async fn block_height(&self) -> u32 { + (|| async { Ok(self.0.block_height().await?) }) + .retry(ConstantBackoff::new(Duration::from_secs(1))) .await .expect("transient errors to be retried") } } + +#[async_trait] +impl TransactionBlockHeight for Wallet { + async fn transaction_block_height(&self, txid: Txid) -> u32 { + #[derive(Debug)] + enum Error { + Io, + NotYetMined, + } + + (|| async { + let block_height = self + .0 + .transaction_block_height(txid) + .await + .map_err(|_| backoff::Error::Transient(Error::Io))?; + + let block_height = + block_height.ok_or_else(|| backoff::Error::Transient(Error::NotYetMined))?; + + Result::<_, backoff::Error>::Ok(block_height) + }) + .retry(ConstantBackoff::new(Duration::from_secs(1))) + .await + .expect("transient errors to be retried") + } +} diff --git a/xmr-btc/tests/harness/wallet/monero.rs b/xmr-btc/tests/harness/wallet/monero.rs index 5bac71c7..3cdc8a46 100644 --- a/xmr-btc/tests/harness/wallet/monero.rs +++ b/xmr-btc/tests/harness/wallet/monero.rs @@ -1,9 +1,9 @@ use anyhow::Result; use async_trait::async_trait; -use backoff::{future::FutureOperation as _, ExponentialBackoff}; +use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; use monero::{Address, Network, PrivateKey}; use monero_harness::rpc::wallet; -use std::str::FromStr; +use std::{str::FromStr, time::Duration}; use xmr_btc::monero::{ Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicKey, PublicViewKey, Transfer, TransferProof, TxHash, WatchForTransfer, @@ -11,6 +11,15 @@ use xmr_btc::monero::{ pub struct Wallet(pub wallet::Client); +impl Wallet { + /// Get the balance of the primary account. + pub async fn get_balance(&self) -> Result { + let amount = self.0.get_balance(0).await?; + + Ok(Amount::from_piconero(amount)) + } +} + #[async_trait] impl Transfer for Wallet { async fn transfer( @@ -106,10 +115,7 @@ impl WatchForTransfer for Wallet { Ok(proof) }) - .retry(ExponentialBackoff { - max_elapsed_time: None, - ..Default::default() - }) + .retry(ConstantBackoff::new(Duration::from_secs(1))) .await; if let Err(Error::InsufficientFunds { expected, actual }) = res { diff --git a/xmr-btc/tests/on_chain.rs b/xmr-btc/tests/on_chain.rs index 77fe0410..c2876fc3 100644 --- a/xmr-btc/tests/on_chain.rs +++ b/xmr-btc/tests/on_chain.rs @@ -1,13 +1,23 @@ pub mod harness; +use std::{convert::TryInto, sync::Arc}; + use anyhow::Result; use async_trait::async_trait; use futures::{ - channel::mpsc::{Receiver, Sender}, + channel::mpsc::{channel, Receiver, Sender}, + future::try_join, SinkExt, StreamExt, }; use genawaiter::GeneratorState; -use harness::wallet::{bitcoin, monero}; +use harness::{ + init_bitcoind, init_test, + node::{run_alice_until, run_bob_until}, +}; +use monero_harness::Monero; +use rand::rngs::OsRng; +use testcontainers::clients::Cli; +use tracing::info; use xmr_btc::{ action_generator_alice, action_generator_bob, alice, bitcoin::{BroadcastSignedTransaction, EncryptedSignature, SignTxLock}, @@ -20,10 +30,18 @@ type AliceNetwork = Network; type BobNetwork = Network; #[derive(Debug)] -struct Network { +struct Network { // TODO: It is weird to use mpsc's in a situation where only one message is expected, but the // ownership rules of Rust are making this painful - pub receiver: Receiver, + pub receiver: Receiver, +} + +impl Network { + pub fn new() -> (Network, Sender) { + let (sender, receiver) = channel(1); + + (Self { receiver }, sender) + } } #[async_trait] @@ -41,19 +59,22 @@ impl ReceiveBitcoinRedeemEncsig for AliceNetwork { } async fn swap_as_alice( - network: &'static mut AliceNetwork, + network: AliceNetwork, // FIXME: It would be more intuitive to have a single network/transport struct instead of // splitting into two, but Rust ownership rules make this tedious mut sender: Sender, - monero_wallet: &'static monero::Wallet, - bitcoin_wallet: &'static bitcoin::Wallet, + monero_wallet: &harness::wallet::monero::Wallet, + bitcoin_wallet: Arc, state: alice::State3, ) -> Result<()> { - let mut action_generator = - action_generator_alice(network, monero_wallet, bitcoin_wallet, state); + let mut action_generator = action_generator_alice(network, bitcoin_wallet.clone(), state); loop { - match action_generator.async_resume().await { + let state = action_generator.async_resume().await; + + info!("resumed execution of generator, got: {:?}", state); + + match state { GeneratorState::Yielded(AliceAction::LockXmr { amount, public_spend_key, @@ -84,16 +105,25 @@ async fn swap_as_alice( } async fn swap_as_bob( - network: &'static mut BobNetwork, + network: BobNetwork, mut sender: Sender, - monero_wallet: &'static monero::Wallet, - bitcoin_wallet: &'static bitcoin::Wallet, + monero_wallet: Arc, + bitcoin_wallet: Arc, state: bob::State2, ) -> Result<()> { - let mut action_generator = action_generator_bob(network, monero_wallet, bitcoin_wallet, state); + let mut action_generator = action_generator_bob( + network, + monero_wallet.clone(), + bitcoin_wallet.clone(), + state, + ); loop { - match action_generator.async_resume().await { + let state = action_generator.async_resume().await; + + info!("resumed execution of generator, got: {:?}", state); + + match state { GeneratorState::Yielded(BobAction::LockBitcoin(tx_lock)) => { let signed_tx_lock = bitcoin_wallet.sign_tx_lock(tx_lock).await?; let _ = bitcoin_wallet @@ -126,5 +156,96 @@ async fn swap_as_bob( } } -#[test] -fn on_chain_happy_path() {} +// NOTE: For some reason running these tests overflows the stack. In order to +// mitigate this run them with: +// +// RUST_MIN_STACK=100000000 cargo test + +#[tokio::test] +async fn on_chain_happy_path() { + let cli = Cli::default(); + let (monero, _container) = 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, Some(100), Some(100)).await; + + // run the handshake as part of the setup + let (alice_state, bob_state) = try_join( + run_alice_until( + &mut alice_node, + alice_state0.into(), + harness::alice::is_state3, + &mut OsRng, + ), + run_bob_until( + &mut bob_node, + bob_state0.into(), + harness::bob::is_state2, + &mut OsRng, + ), + ) + .await + .unwrap(); + let alice: alice::State3 = alice_state.try_into().unwrap(); + let bob: bob::State2 = bob_state.try_into().unwrap(); + let tx_lock_txid = bob.tx_lock.txid(); + + let alice_bitcoin_wallet = Arc::new(alice_node.bitcoin_wallet); + let bob_bitcoin_wallet = Arc::new(bob_node.bitcoin_wallet); + let alice_monero_wallet = Arc::new(alice_node.monero_wallet); + let bob_monero_wallet = Arc::new(bob_node.monero_wallet); + + let (alice_network, bob_sender) = Network::::new(); + let (bob_network, alice_sender) = Network::::new(); + + try_join( + swap_as_alice( + alice_network, + alice_sender, + &alice_monero_wallet.clone(), + alice_bitcoin_wallet.clone(), + alice, + ), + swap_as_bob( + bob_network, + bob_sender, + bob_monero_wallet.clone(), + bob_bitcoin_wallet.clone(), + bob, + ), + ) + .await + .unwrap(); + + let alice_final_btc_balance = alice_bitcoin_wallet.balance().await.unwrap(); + let bob_final_btc_balance = bob_bitcoin_wallet.balance().await.unwrap(); + + let lock_tx_bitcoin_fee = bob_bitcoin_wallet + .transaction_fee(tx_lock_txid) + .await + .unwrap(); + + let alice_final_xmr_balance = alice_monero_wallet.get_balance().await.unwrap(); + + monero.wait_for_bob_wallet_block_height().await.unwrap(); + let bob_final_xmr_balance = bob_monero_wallet.get_balance().await.unwrap(); + + assert_eq!( + alice_final_btc_balance, + initial_balances.alice_btc + swap_amounts.btc + - bitcoin::Amount::from_sat(xmr_btc::bitcoin::TX_FEE) + ); + assert_eq!( + bob_final_btc_balance, + initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee + ); + + // Getting the Monero LockTx fee is tricky in a clean way, I think checking this + // condition is sufficient + assert!(alice_final_xmr_balance <= initial_balances.alice_xmr - swap_amounts.xmr,); + assert_eq!( + bob_final_xmr_balance, + initial_balances.bob_xmr + swap_amounts.xmr + ); +} From 0f17ec076c09f064e62dd6ccac6d1c08f57b71a7 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 22 Oct 2020 13:23:12 +1100 Subject: [PATCH 43/52] Add message1 --- swap/src/alice.rs | 49 ++++++++++-- swap/src/alice/message0.rs | 2 +- swap/src/alice/message1.rs | 113 +++++++++++++++++++++++++++ swap/src/bob.rs | 40 ++++++++-- swap/src/bob/amounts.rs | 2 +- swap/src/bob/message0.rs | 2 +- swap/src/bob/message1.rs | 98 +++++++++++++++++++++++ swap/src/network/request_response.rs | 2 + xmr-btc/src/alice/message.rs | 2 +- xmr-btc/src/bob/message.rs | 2 +- 10 files changed, 296 insertions(+), 16 deletions(-) create mode 100644 swap/src/alice/message1.rs create mode 100644 swap/src/bob/message1.rs diff --git a/swap/src/alice.rs b/swap/src/alice.rs index d0cedd23..4ad7f196 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -12,8 +12,9 @@ use tracing::debug; mod amounts; mod message0; +mod message1; -use self::{amounts::*, message0::*}; +use self::{amounts::*, message0::*, message1::*}; use crate::{ network::{ peer_tracker::{self, PeerTracker}, @@ -47,7 +48,7 @@ pub async fn swap( debug!("Got request from Bob to swap {}", btc); let p = calculate_amounts(btc); last_amounts = Some(p); - swarm.send(channel, AliceToBob::Amounts(p)); + swarm.send_amounts(channel, p); } OutEvent::Message0(msg) => { debug!("Got message0 from Bob"); @@ -55,6 +56,7 @@ pub async fn swap( message0 = Some(msg); break; } + other => panic!("Unexpected event: {:?}", other), }; } @@ -78,11 +80,25 @@ pub async fn swap( ); swarm.set_state0(state0.clone()); - let _state1 = match message0 { - Some(msg) => state0.receive(msg), + let state1 = match message0 { + Some(msg) => state0.receive(msg).expect("failed to receive msg 0"), None => panic!("should have the message by here"), }; + let (state2, channel) = match swarm.next().await { + OutEvent::Message1 { msg, channel } => { + debug!("Got message1 from Bob"); + let state2 = state1.receive(msg); + (state2, channel) + } + other => panic!("Unexpected event: {:?}", other), + }; + + let msg = state2.next_message(); + swarm.send_message1(channel, msg); + + tracing::info!("handshake complete, we now have State2 for Alice."); + tracing::warn!("parking thread ..."); thread::park(); Ok(()) @@ -118,6 +134,10 @@ pub enum OutEvent { ConnectionEstablished(PeerId), Request(amounts::OutEvent), Message0(bob::Message0), + Message1 { + msg: bob::Message1, + channel: ResponseChannel, + }, } impl From for OutEvent { @@ -144,6 +164,14 @@ impl From for OutEvent { } } +impl From for OutEvent { + fn from(event: message1::OutEvent) -> Self { + match event { + message1::OutEvent::Msg { msg, channel } => OutEvent::Message1 { msg, channel }, + } + } +} + /// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice. #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", event_process = false)] @@ -152,6 +180,7 @@ pub struct Alice { pt: PeerTracker, amounts: Amounts, message0: Message0, + message1: Message1, #[behaviour(ignore)] identity: Keypair, } @@ -166,13 +195,22 @@ impl Alice { } /// Alice always sends her messages as a response to a request from Bob. - pub fn send(&mut self, channel: ResponseChannel, msg: AliceToBob) { + pub fn send_amounts(&mut self, channel: ResponseChannel, p: SwapParams) { + let msg = AliceToBob::Amounts(p); self.amounts.send(channel, msg); } pub fn set_state0(&mut self, state: State0) { let _ = self.message0.set_state(state); } + + pub fn send_message1( + &mut self, + channel: ResponseChannel, + msg: xmr_btc::alice::Message1, + ) { + self.message1.send(channel, msg) + } } impl Default for Alice { @@ -184,6 +222,7 @@ impl Default for Alice { pt: PeerTracker::default(), amounts: Amounts::new(timeout), message0: Message0::new(timeout), + message1: Message1::new(timeout), identity, } } diff --git a/swap/src/alice/message0.rs b/swap/src/alice/message0.rs index ad62586c..ec85d57a 100644 --- a/swap/src/alice/message0.rs +++ b/swap/src/alice/message0.rs @@ -23,7 +23,7 @@ pub enum OutEvent { Msg(bob::Message0), } -/// A `NetworkBehaviour` that represents getting the amounts of an XMR/BTC swap. +/// A `NetworkBehaviour` that represents send/recv of message 0. #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", poll_method = "poll")] #[allow(missing_debug_implementations)] diff --git a/swap/src/alice/message1.rs b/swap/src/alice/message1.rs new file mode 100644 index 00000000..4995e99a --- /dev/null +++ b/swap/src/alice/message1.rs @@ -0,0 +1,113 @@ +use libp2p::{ + request_response::{ + handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig, + RequestResponseEvent, RequestResponseMessage, ResponseChannel, + }, + swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters}, + NetworkBehaviour, +}; +use std::{ + collections::VecDeque, + task::{Context, Poll}, + time::Duration, +}; +use tracing::error; + +use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}; +use xmr_btc::bob; + +#[derive(Debug)] +pub enum OutEvent { + Msg { + /// Received message from Bob. + msg: bob::Message1, + /// Channel to send back Alice's message 1. + channel: ResponseChannel, + }, +} + +/// A `NetworkBehaviour` that represents send/recv of message 1. +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "OutEvent", poll_method = "poll")] +#[allow(missing_debug_implementations)] +pub struct Message1 { + rr: RequestResponse, + #[behaviour(ignore)] + events: VecDeque, +} + +impl Message1 { + pub fn new(timeout: Duration) -> Self { + let mut config = RequestResponseConfig::default(); + config.set_request_timeout(timeout); + + Self { + rr: RequestResponse::new( + Codec::default(), + vec![(Protocol, ProtocolSupport::Full)], + config, + ), + events: Default::default(), + } + } + + pub fn send(&mut self, channel: ResponseChannel, msg: xmr_btc::alice::Message1) { + let msg = AliceToBob::Message1(msg); + self.rr.send_response(channel, msg); + } + + fn poll( + &mut self, + _: &mut Context<'_>, + _: &mut impl PollParameters, + ) -> Poll, OutEvent>> { + if let Some(event) = self.events.pop_front() { + return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event)); + } + + Poll::Pending + } +} + +impl NetworkBehaviourEventProcess> for Message1 { + fn inject_event(&mut self, event: RequestResponseEvent) { + match event { + RequestResponseEvent::Message { + peer: _, + message: + RequestResponseMessage::Request { + request, + request_id: _, + channel, + }, + } => match request { + BobToAlice::Message1(msg) => { + self.events.push_back(OutEvent::Msg { msg, channel }); + } + other => panic!("unexpected request: {:?}", other), + }, + RequestResponseEvent::Message { + peer: _, + message: + RequestResponseMessage::Response { + response: _, + request_id: _, + }, + } => panic!("unexpected response"), + RequestResponseEvent::InboundFailure { + peer: _, + request_id: _, + error, + } => { + error!("Inbound failure: {:?}", error); + } + RequestResponseEvent::OutboundFailure { + peer: _, + request_id: _, + error, + } => { + error!("Outbound failure: {:?}", error); + } + } + } +} diff --git a/swap/src/bob.rs b/swap/src/bob.rs index ed38cf87..53eec7d0 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -8,12 +8,13 @@ use futures::{ use libp2p::{core::identity::Keypair, Multiaddr, NetworkBehaviour, PeerId}; use rand::rngs::OsRng; use std::{process, thread, time::Duration}; -use tracing::{debug, info, warn}; +use tracing::{debug, info}; mod amounts; mod message0; +mod message1; -use self::{amounts::*, message0::*}; +use self::{amounts::*, message0::*, message1::*}; use crate::{ network::{ peer_tracker::{self, PeerTracker}, @@ -80,15 +81,26 @@ where PUNISH_TIMELOCK, refund_address, ); + swarm.send_message0(alice.clone(), state0.next_message(rng)); - let _state1 = match swarm.next().await { + let state1 = match swarm.next().await { OutEvent::Message0(msg) => { - state0.receive(&wallet, msg) // TODO: More graceful error handling. + state0.receive(&wallet, msg).await? // TODO: More graceful error + // handling. } other => panic!("unexpected event: {:?}", other), }; - warn!("parking thread ..."); + swarm.send_message1(alice.clone(), state1.next_message()); + let _state2 = match swarm.next().await { + OutEvent::Message1(msg) => { + state1.receive(msg) // TODO: More graceful error handling. + } + other => panic!("unexpected event: {:?}", other), + }; + + info!("handshake complete, we now have State2 for Bob."); + thread::park(); Ok(()) } @@ -120,6 +132,7 @@ pub enum OutEvent { ConnectionEstablished(PeerId), Amounts(amounts::OutEvent), Message0(alice::Message0), + Message1(alice::Message1), } impl From for OutEvent { @@ -146,6 +159,14 @@ impl From for OutEvent { } } +impl From for OutEvent { + fn from(event: message1::OutEvent) -> Self { + match event { + message1::OutEvent::Msg(msg) => OutEvent::Message1(msg), + } + } +} + /// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob. #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", event_process = false)] @@ -154,6 +175,7 @@ pub struct Bob { pt: PeerTracker, amounts: Amounts, message0: Message0, + message1: Message1, #[behaviour(ignore)] identity: Keypair, } @@ -174,11 +196,16 @@ impl Bob { debug!("Requesting amounts from: {}", alice); } - /// Sends Bob's first state message to Alice. + /// Sends Bob's first message to Alice. pub fn send_message0(&mut self, alice: PeerId, msg: bob::Message0) { self.message0.send(alice, msg) } + /// Sends Bob's second message to Alice. + pub fn send_message1(&mut self, alice: PeerId, msg: bob::Message1) { + self.message1.send(alice, msg) + } + /// Returns Alice's peer id if we are connected. pub fn peer_id_of_alice(&self) -> Option { self.pt.counterparty_peer_id() @@ -194,6 +221,7 @@ impl Default for Bob { pt: PeerTracker::default(), amounts: Amounts::new(timeout), message0: Message0::new(timeout), + message1: Message1::new(timeout), identity, } } diff --git a/swap/src/bob/amounts.rs b/swap/src/bob/amounts.rs index 4443a1e1..6d7ed714 100644 --- a/swap/src/bob/amounts.rs +++ b/swap/src/bob/amounts.rs @@ -85,7 +85,7 @@ impl NetworkBehaviourEventProcess> }, } => match response { AliceToBob::Amounts(p) => self.events.push_back(OutEvent::Amounts(p)), - AliceToBob::Message0(_) => panic!("shouldn't get message0 here"), + other => panic!("unexpected response: {:?}", other), }, RequestResponseEvent::InboundFailure { .. } => { diff --git a/swap/src/bob/message0.rs b/swap/src/bob/message0.rs index 8e3a0ad8..fd20c841 100644 --- a/swap/src/bob/message0.rs +++ b/swap/src/bob/message0.rs @@ -80,7 +80,7 @@ impl NetworkBehaviourEventProcess> }, } => match response { AliceToBob::Message0(msg) => self.events.push_back(OutEvent::Msg(msg)), - AliceToBob::Amounts(_) => panic!("shouldn't get amounts here"), + other => panic!("unexpected response: {:?}", other), }, RequestResponseEvent::InboundFailure { .. } => { diff --git a/swap/src/bob/message1.rs b/swap/src/bob/message1.rs new file mode 100644 index 00000000..43d24850 --- /dev/null +++ b/swap/src/bob/message1.rs @@ -0,0 +1,98 @@ +use libp2p::{ + request_response::{ + handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig, + RequestResponseEvent, RequestResponseMessage, + }, + swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters}, + NetworkBehaviour, PeerId, +}; +use std::{ + collections::VecDeque, + task::{Context, Poll}, + time::Duration, +}; +use tracing::error; + +use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}; +use xmr_btc::{alice, bob}; + +#[derive(Debug)] +pub enum OutEvent { + Msg(alice::Message1), +} + +/// A `NetworkBehaviour` that represents send/recv of message 1. +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "OutEvent", poll_method = "poll")] +#[allow(missing_debug_implementations)] +pub struct Message1 { + rr: RequestResponse, + #[behaviour(ignore)] + events: VecDeque, +} + +impl Message1 { + pub fn new(timeout: Duration) -> Self { + let mut config = RequestResponseConfig::default(); + config.set_request_timeout(timeout); + + Self { + rr: RequestResponse::new( + Codec::default(), + vec![(Protocol, ProtocolSupport::Full)], + config, + ), + events: Default::default(), + } + } + + pub fn send(&mut self, alice: PeerId, msg: bob::Message1) { + let msg = BobToAlice::Message1(msg); + let _id = self.rr.send_request(&alice, msg); + } + + fn poll( + &mut self, + _: &mut Context<'_>, + _: &mut impl PollParameters, + ) -> Poll, OutEvent>> { + if let Some(event) = self.events.pop_front() { + return Poll::Ready(NetworkBehaviourAction::GenerateEvent(event)); + } + + Poll::Pending + } +} + +impl NetworkBehaviourEventProcess> for Message1 { + fn inject_event(&mut self, event: RequestResponseEvent) { + match event { + RequestResponseEvent::Message { + peer: _, + message: RequestResponseMessage::Request { .. }, + } => panic!("Bob should never get a request from Alice"), + RequestResponseEvent::Message { + peer: _, + message: + RequestResponseMessage::Response { + response, + request_id: _, + }, + } => match response { + AliceToBob::Message1(msg) => self.events.push_back(OutEvent::Msg(msg)), + other => panic!("unexpected response: {:?}", other), + }, + + RequestResponseEvent::InboundFailure { .. } => { + panic!("Bob should never get a request from Alice, so should never get an InboundFailure"); + } + RequestResponseEvent::OutboundFailure { + peer: _, + request_id: _, + error, + } => { + error!("Outbound failure: {:?}", error); + } + } + } +} diff --git a/swap/src/network/request_response.rs b/swap/src/network/request_response.rs index b6e582c5..1b45a9dd 100644 --- a/swap/src/network/request_response.rs +++ b/swap/src/network/request_response.rs @@ -21,6 +21,7 @@ pub enum BobToAlice { AmountsFromBtc(::bitcoin::Amount), AmountsFromXmr(monero::Amount), Message0(bob::Message0), + Message1(bob::Message1), } /// Messages Alice sends to Bob. @@ -29,6 +30,7 @@ pub enum BobToAlice { pub enum AliceToBob { Amounts(SwapParams), Message0(alice::Message0), + Message1(alice::Message1), } #[derive(Debug, Clone, Copy, Default)] diff --git a/xmr-btc/src/alice/message.rs b/xmr-btc/src/alice/message.rs index 79b66986..6bbaa9ac 100644 --- a/xmr-btc/src/alice/message.rs +++ b/xmr-btc/src/alice/message.rs @@ -23,7 +23,7 @@ pub struct Message0 { pub(crate) punish_address: bitcoin::Address, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Message1 { pub(crate) tx_cancel_sig: Signature, pub(crate) tx_refund_encsig: EncryptedSignature, diff --git a/xmr-btc/src/bob/message.rs b/xmr-btc/src/bob/message.rs index 2826a545..bfd977c6 100644 --- a/xmr-btc/src/bob/message.rs +++ b/xmr-btc/src/bob/message.rs @@ -22,7 +22,7 @@ pub struct Message0 { pub(crate) refund_address: bitcoin::Address, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Message1 { pub(crate) tx_lock: bitcoin::TxLock, } From ad006fae6a101a17ebc6cddf93561231f987be4a Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 22 Oct 2020 13:30:07 +1100 Subject: [PATCH 44/52] Do a bunch of cleanups --- swap/src/alice.rs | 25 +++++++++++++------------ swap/src/bob.rs | 1 + swap/src/bob/amounts.rs | 4 ++-- swap/src/lib.rs | 10 +++++----- swap/src/main.rs | 12 +++++++----- swap/src/network/peer_tracker.rs | 6 ++---- swap/src/network/request_response.rs | 4 ++-- 7 files changed, 32 insertions(+), 30 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 4ad7f196..6611ad43 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -6,7 +6,7 @@ use libp2p::{ request_response::ResponseChannel, NetworkBehaviour, PeerId, }; -use rand::{CryptoRng, RngCore}; +use rand::rngs::OsRng; use std::{thread, time::Duration}; use tracing::debug; @@ -21,21 +21,20 @@ use crate::{ request_response::{AliceToBob, TIMEOUT}, transport, TokioExecutor, }, - SwapParams, PUNISH_TIMELOCK, REFUND_TIMELOCK, + SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; use xmr_btc::{alice::State0, bob, monero}; pub type Swarm = libp2p::Swarm; #[allow(unused_assignments)] // Due to the mutable message0? -pub async fn swap( +pub async fn swap( listen: Multiaddr, - rng: &mut R, redeem_address: ::bitcoin::Address, punish_address: ::bitcoin::Address, ) -> Result<()> { let mut message0: Option = None; - let mut last_amounts: Option = None; + let mut last_amounts: Option = None; let mut swarm = new_swarm(listen)?; @@ -65,10 +64,12 @@ pub async fn swap( None => unreachable!("should have amounts by here"), }; - // FIXME: Too many `bitcoin` crates/modules. let xmr = monero::Amount::from_piconero(xmr.as_piconero()); + // TODO: This should be the Amount exported by xmr_btc. let btc = ::bitcoin::Amount::from_sat(btc.as_sat()); + // TODO: Pass this in using + let rng = &mut OsRng; let state0 = State0::new( rng, btc, @@ -195,8 +196,8 @@ impl Alice { } /// Alice always sends her messages as a response to a request from Bob. - pub fn send_amounts(&mut self, channel: ResponseChannel, p: SwapParams) { - let msg = AliceToBob::Amounts(p); + pub fn send_amounts(&mut self, channel: ResponseChannel, amounts: SwapAmounts) { + let msg = AliceToBob::Amounts(amounts); self.amounts.send(channel, msg); } @@ -228,15 +229,15 @@ impl Default for Alice { } } -// TODO: Check that this is correct. -fn calculate_amounts(btc: ::bitcoin::Amount) -> SwapParams { +fn calculate_amounts(btc: ::bitcoin::Amount) -> SwapAmounts { const XMR_PER_BTC: u64 = 100; // TODO: Get this from an exchange. + // TODO: Check that this is correct. // XMR uses 12 zerose BTC uses 8. let picos = (btc.as_sat() * 10000) * XMR_PER_BTC; let xmr = monero::Amount::from_piconero(picos); - SwapParams { btc, xmr } + SwapAmounts { btc, xmr } } #[cfg(test)] @@ -251,7 +252,7 @@ mod tests { let btc = ::bitcoin::Amount::from_sat(ONE_BTC); let want = monero::Amount::from_piconero(HUNDRED_XMR); - let SwapParams { xmr: got, .. } = calculate_amounts(btc); + let SwapAmounts { xmr: got, .. } = calculate_amounts(btc); assert_eq!(got, want); } } diff --git a/swap/src/bob.rs b/swap/src/bob.rs index 53eec7d0..d3422b5d 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -72,6 +72,7 @@ where let xmr = xmr_btc::monero::Amount::from_piconero(xmr.as_piconero()); let btc = ::bitcoin::Amount::from_sat(btc.as_sat()); + // TODO: Pass this in using let rng = &mut OsRng; let state0 = State0::new( rng, diff --git a/swap/src/bob/amounts.rs b/swap/src/bob/amounts.rs index 6d7ed714..fcdb74e0 100644 --- a/swap/src/bob/amounts.rs +++ b/swap/src/bob/amounts.rs @@ -16,12 +16,12 @@ use tracing::error; use crate::{ network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}, - SwapParams, + SwapAmounts, }; #[derive(Debug)] pub enum OutEvent { - Amounts(SwapParams), + Amounts(SwapAmounts), } /// A `NetworkBehaviour` that represents getting the amounts of an XMR/BTC swap. diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 9f98db70..99778b79 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -9,7 +9,7 @@ pub mod network; pub const ONE_BTC: u64 = 100_000_000; -const REFUND_TIMELOCK: u32 = 10; // FIXME: What should this be? +const REFUND_TIMELOCK: u32 = 10; // Relative timelock, this is number of blocks. TODO: What should it be? const PUNISH_TIMELOCK: u32 = 20; // FIXME: What should this be? pub type Never = std::convert::Infallible; @@ -17,7 +17,7 @@ pub type Never = std::convert::Infallible; /// Commands sent from Bob to the main task. #[derive(Clone, Copy, Debug)] pub enum Cmd { - VerifyAmounts(SwapParams), + VerifyAmounts(SwapAmounts), } /// Responses send from the main task back to Bob. @@ -27,9 +27,9 @@ pub enum Rsp { Abort, } -/// XMR/BTC swap parameters. +/// XMR/BTC swap amounts. #[derive(Copy, Clone, Debug, Serialize, Deserialize)] -pub struct SwapParams { +pub struct SwapAmounts { /// Amount of BTC to swap. #[serde(with = "::bitcoin::util::amount::serde::as_sat")] pub btc: ::bitcoin::Amount, @@ -38,7 +38,7 @@ pub struct SwapParams { pub xmr: xmr_btc::monero::Amount, } -impl Display for SwapParams { +impl Display for SwapAmounts { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, diff --git a/swap/src/main.rs b/swap/src/main.rs index f5b60f42..7f427a8d 100644 --- a/swap/src/main.rs +++ b/swap/src/main.rs @@ -16,7 +16,6 @@ use anyhow::{bail, Result}; use futures::{channel::mpsc, StreamExt}; use libp2p::Multiaddr; use log::LevelFilter; -use rand::rngs::OsRng; use std::{io, io::Write, process}; use structopt::StructOpt; use tracing::info; @@ -26,10 +25,13 @@ mod cli; mod trace; use cli::Options; -use swap::{alice, bitcoin::Wallet, bob, Cmd, Rsp, SwapParams}; +use swap::{alice, bitcoin::Wallet, bob, Cmd, Rsp, SwapAmounts}; use xmr_btc::bitcoin::BuildTxLockPsbt; + // TODO: Add root seed file instead of generating new seed each run. +// TODO: Remove all instances of the todo! macro +// TODO: Add a config file with these in it. // Alice's address and port until we have a config file. pub const PORT: u16 = 9876; // Arbitrarily chosen. pub const ADDR: &str = "127.0.0.1"; @@ -97,7 +99,7 @@ async fn swap_as_alice( redeem: bitcoin::Address, punish: bitcoin::Address, ) -> Result<()> { - alice::swap(addr, &mut OsRng, redeem, punish).await + alice::swap(addr, redeem, punish).await } async fn swap_as_bob( @@ -132,10 +134,10 @@ where } } -fn verify(p: SwapParams) -> Rsp { +fn verify(amounts: SwapAmounts) -> Rsp { let mut s = String::new(); println!("Got rate from Alice for XMR/BTC swap\n"); - println!("{}", p); + println!("{}", amounts); print!("Would you like to continue with this swap [y/N]: "); let _ = io::stdout().flush(); io::stdin() diff --git a/swap/src/network/peer_tracker.rs b/swap/src/network/peer_tracker.rs index ca3a67e5..08e97077 100644 --- a/swap/src/network/peer_tracker.rs +++ b/swap/src/network/peer_tracker.rs @@ -25,8 +25,7 @@ pub struct PeerTracker { } impl PeerTracker { - /// Returns an arbitrary connected counterparty. - /// This is useful if we are connected to a single other node. + /// Returns the peer id of counterparty if we are connected. pub fn counterparty_peer_id(&self) -> Option { if let Some((id, _)) = &self.connected { return Some(id.clone()); @@ -34,8 +33,7 @@ impl PeerTracker { None } - /// Returns an arbitrary connected counterparty. - /// This is useful if we are connected to a single other node. + /// Returns the multiaddr of counterparty if we are connected. pub fn counterparty_addr(&self) -> Option { if let Some((_, addr)) = &self.connected { return Some(addr.clone()); diff --git a/swap/src/network/request_response.rs b/swap/src/network/request_response.rs index 1b45a9dd..8e9f44b0 100644 --- a/swap/src/network/request_response.rs +++ b/swap/src/network/request_response.rs @@ -7,7 +7,7 @@ use libp2p::{ use serde::{Deserialize, Serialize}; use std::{fmt::Debug, io}; -use crate::SwapParams; +use crate::SwapAmounts; use xmr_btc::{alice, bob, monero}; /// Time to wait for a response back once we send a request. @@ -28,7 +28,7 @@ pub enum BobToAlice { #[derive(Clone, Debug, Serialize, Deserialize)] #[allow(clippy::large_enum_variant)] pub enum AliceToBob { - Amounts(SwapParams), + Amounts(SwapAmounts), Message0(alice::Message0), Message1(alice::Message1), } From 2059158dad9abf7a2bd31ad2a929442905f5ac42 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 22 Oct 2020 13:39:18 +1100 Subject: [PATCH 45/52] Bump to libp2p v29 --- swap/Cargo.toml | 4 ++-- swap/src/network/transport.rs | 21 +++++---------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 7bfb32ea..5efe6e57 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -15,8 +15,8 @@ bitcoin = { version = "0.23", features = ["rand", "use-serde"] } # TODO: Upgrade bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "d402b36d3d6406150e3bfb71492ff4a0a7cb290e" } derivative = "2" futures = { version = "0.3", default-features = false } -libp2p = { version = "0.28", default-features = false, features = ["tcp-tokio", "yamux", "mplex", "dns", "noise", "request-response"] } -libp2p-tokio-socks5 = "0.3" +libp2p = { version = "0.29", default-features = false, features = ["tcp-tokio", "yamux", "mplex", "dns", "noise", "request-response"] } +libp2p-tokio-socks5 = "0.4" log = { version = "0.4", features = ["serde"] } monero = "0.9" rand = "0.7" diff --git a/swap/src/network/transport.rs b/swap/src/network/transport.rs index 60d9b3ce..22392fd2 100644 --- a/swap/src/network/transport.rs +++ b/swap/src/network/transport.rs @@ -1,20 +1,18 @@ use anyhow::Result; use libp2p::{ core::{ - either::EitherError, identity, muxing::StreamMuxerBox, - transport::{boxed::Boxed, timeout::TransportTimeoutError}, + transport::Boxed, upgrade::{SelectUpgrade, Version}, - Transport, UpgradeError, + Transport, }, - dns::{DnsConfig, DnsErr}, + dns::DnsConfig, mplex::MplexConfig, - noise::{self, NoiseConfig, NoiseError, X25519Spec}, + noise::{self, NoiseConfig, X25519Spec}, tcp::TokioTcpConfig, yamux, PeerId, }; -use std::{io, time::Duration}; /// Builds a libp2p transport with the following features: /// - TcpConnection @@ -36,18 +34,9 @@ pub fn build(id_keys: identity::Keypair) -> Result { MplexConfig::new(), )) .map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer))) - .timeout(Duration::from_secs(20)) .boxed(); Ok(transport) } -pub type SwapTransport = Boxed< - (PeerId, StreamMuxerBox), - TransportTimeoutError< - EitherError< - EitherError, UpgradeError>, - UpgradeError>, - >, - >, ->; +pub type SwapTransport = Boxed<(PeerId, StreamMuxerBox)>; From 30298bdf1f12da2b03041cf09be23a664f12ce94 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 22 Oct 2020 13:48:30 +1100 Subject: [PATCH 46/52] Do some more cleanups --- swap/src/alice.rs | 1 - swap/src/main.rs | 8 ++++++-- swap/src/monero.rs | 8 ++++---- swap/src/network/request_response.rs | 3 +++ swap/src/network/transport.rs | 2 ++ 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 6611ad43..79c1060e 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -88,7 +88,6 @@ pub async fn swap( let (state2, channel) = match swarm.next().await { OutEvent::Message1 { msg, channel } => { - debug!("Got message1 from Bob"); let state2 = state1.receive(msg); (state2, channel) } diff --git a/swap/src/main.rs b/swap/src/main.rs index 7f427a8d..ce6ea418 100644 --- a/swap/src/main.rs +++ b/swap/src/main.rs @@ -104,7 +104,7 @@ async fn swap_as_alice( async fn swap_as_bob( sats: u64, - addr: Multiaddr, + alice: Multiaddr, refund: bitcoin::Address, wallet: W, ) -> Result<()> @@ -113,7 +113,8 @@ where { let (cmd_tx, mut cmd_rx) = mpsc::channel(1); let (mut rsp_tx, rsp_rx) = mpsc::channel(1); - tokio::spawn(bob::swap(sats, addr, cmd_tx, rsp_rx, refund, wallet)); + tokio::spawn(bob::swap(sats, alice, cmd_tx, rsp_rx, refund, wallet)); + loop { let read = cmd_rx.next().await; match read { @@ -139,16 +140,19 @@ fn verify(amounts: SwapAmounts) -> Rsp { println!("Got rate from Alice for XMR/BTC swap\n"); println!("{}", amounts); print!("Would you like to continue with this swap [y/N]: "); + let _ = io::stdout().flush(); io::stdin() .read_line(&mut s) .expect("Did not enter a correct string"); + if let Some('\n') = s.chars().next_back() { s.pop(); } if let Some('\r') = s.chars().next_back() { s.pop(); } + if !is_yes(&s) { println!("No worries, try again later - Alice updates her rate regularly"); return Rsp::Abort; diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 43b571b8..c9ca0d30 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -1,10 +1,10 @@ -use serde::{de::Error, Deserialize, Deserializer, Serializer}; - -use xmr_btc::monero::Amount; +//! Monero stuff, for now just serde. +// This has to be in a sub-module to use with serde derive. pub mod amount_serde { - use super::*; + use serde::{de::Error, Deserialize, Deserializer, Serializer}; use std::str::FromStr; + use xmr_btc::monero::Amount; pub fn serialize(value: &Amount, serializer: S) -> Result where diff --git a/swap/src/network/request_response.rs b/swap/src/network/request_response.rs index 8e9f44b0..b0061a9b 100644 --- a/swap/src/network/request_response.rs +++ b/swap/src/network/request_response.rs @@ -13,6 +13,9 @@ use xmr_btc::{alice, bob, monero}; /// Time to wait for a response back once we send a request. pub const TIMEOUT: u64 = 3600; // One hour. +// TODO: Think about whether there is a better way to do this, e.g., separate +// Codec for each Message and a macro that implements them. + /// Messages Bob sends to Alice. #[derive(Clone, Debug, Serialize, Deserialize)] #[allow(clippy::large_enum_variant)] diff --git a/swap/src/network/transport.rs b/swap/src/network/transport.rs index 22392fd2..19da6c8f 100644 --- a/swap/src/network/transport.rs +++ b/swap/src/network/transport.rs @@ -14,6 +14,8 @@ use libp2p::{ yamux, PeerId, }; +// TOOD: Add the tor transport builder. + /// Builds a libp2p transport with the following features: /// - TcpConnection /// - DNS name resolution From eed5e8e9a478ff465db41d867881972d6668df7a Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 22 Oct 2020 13:54:13 +1100 Subject: [PATCH 47/52] Move monero serde code to lib --- swap/src/lib.rs | 3 +-- swap/src/monero.rs | 27 --------------------------- xmr-btc/src/serde.rs | 35 ++++++++++++++++++++++++++++------- 3 files changed, 29 insertions(+), 36 deletions(-) delete mode 100644 swap/src/monero.rs diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 99778b79..ad67aca7 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -4,7 +4,6 @@ use std::fmt::{self, Display}; pub mod alice; pub mod bitcoin; pub mod bob; -pub mod monero; pub mod network; pub const ONE_BTC: u64 = 100_000_000; @@ -34,7 +33,7 @@ pub struct SwapAmounts { #[serde(with = "::bitcoin::util::amount::serde::as_sat")] pub btc: ::bitcoin::Amount, /// Amount of XMR to swap. - #[serde(with = "crate::monero::amount_serde")] + #[serde(with = "xmr_btc::serde::monero_amount")] pub xmr: xmr_btc::monero::Amount, } diff --git a/swap/src/monero.rs b/swap/src/monero.rs deleted file mode 100644 index c9ca0d30..00000000 --- a/swap/src/monero.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Monero stuff, for now just serde. - -// This has to be in a sub-module to use with serde derive. -pub mod amount_serde { - use serde::{de::Error, Deserialize, Deserializer, Serializer}; - use std::str::FromStr; - use xmr_btc::monero::Amount; - - pub fn serialize(value: &Amount, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&value.as_piconero().to_string()) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result>::Error> - where - D: Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - let value = - u64::from_str(value.as_str()).map_err(>::Error::custom)?; - let amount = Amount::from_piconero(value); - - Ok(amount) - } -} diff --git a/xmr-btc/src/serde.rs b/xmr-btc/src/serde.rs index e64cfd10..f8fa4ab9 100644 --- a/xmr-btc/src/serde.rs +++ b/xmr-btc/src/serde.rs @@ -132,23 +132,44 @@ pub mod monero_private_key { } pub mod bitcoin_amount { + use bitcoin::Amount; use serde::{Deserialize, Deserializer, Serializer}; - pub fn serialize(value: &bitcoin::Amount, serializer: S) -> Result + pub fn serialize(x: &Amount, s: S) -> Result where S: Serializer, { - serializer.serialize_u64(value.as_sat()) + s.serialize_u64(x.as_sat()) } - pub fn deserialize<'de, D>( - deserializer: D, - ) -> Result>::Error> + pub fn deserialize<'de, D>(deserializer: D) -> Result>::Error> + where + D: Deserializer<'de>, + { + let sats = u64::deserialize(deserializer)?; + let amount = Amount::from_sat(sats); + + Ok(amount) + } +} + +pub mod monero_amount { + use crate::monero::Amount; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(x: &Amount, s: S) -> Result + where + S: Serializer, + { + s.serialize_u64(x.as_piconero()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result>::Error> where D: Deserializer<'de>, { - let value = u64::deserialize(deserializer)?; - let amount = bitcoin::Amount::from_sat(value); + let picos = u64::deserialize(deserializer)?; + let amount = Amount::from_piconero(picos); Ok(amount) } From cc6107982600212906e978868f5605767142990e Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 22 Oct 2020 13:55:50 +1100 Subject: [PATCH 48/52] Use VerifiedAmounts --- swap/src/lib.rs | 5 +++-- swap/src/main.rs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/swap/src/lib.rs b/swap/src/lib.rs index ad67aca7..404e9177 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -19,10 +19,10 @@ pub enum Cmd { VerifyAmounts(SwapAmounts), } -/// Responses send from the main task back to Bob. +/// Responses sent from the main task back to Bob. #[derive(Clone, Copy, Debug, PartialEq)] pub enum Rsp { - Verified, + VerifiedAmounts, Abort, } @@ -37,6 +37,7 @@ pub struct SwapAmounts { pub xmr: xmr_btc::monero::Amount, } +// TODO: Display in XMR and BTC (not picos and sats). impl Display for SwapAmounts { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( diff --git a/swap/src/main.rs b/swap/src/main.rs index ce6ea418..5a62120d 100644 --- a/swap/src/main.rs +++ b/swap/src/main.rs @@ -158,7 +158,7 @@ fn verify(amounts: SwapAmounts) -> Rsp { return Rsp::Abort; } - Rsp::Verified + Rsp::VerifiedAmounts } fn is_yes(s: &str) -> bool { From 6e34f9c978fdfd5374afa34308cecb479b37bacd Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 22 Oct 2020 14:12:49 +1100 Subject: [PATCH 49/52] Use const TIMEOUT instead of an argument --- swap/src/alice.rs | 16 +++++++++------- swap/src/alice/amounts.rs | 34 +++++++++++++++++++--------------- swap/src/alice/message0.rs | 35 +++++++++++++++++++---------------- swap/src/alice/message1.rs | 33 ++++++++++++++++++--------------- swap/src/bob.rs | 27 ++++++++++++++------------- swap/src/bob/amounts.rs | 34 +++++++++++++++++++--------------- swap/src/bob/message0.rs | 33 ++++++++++++++++++--------------- swap/src/bob/message1.rs | 33 ++++++++++++++++++--------------- 8 files changed, 134 insertions(+), 111 deletions(-) diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 79c1060e..09423290 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -7,7 +7,7 @@ use libp2p::{ NetworkBehaviour, PeerId, }; use rand::rngs::OsRng; -use std::{thread, time::Duration}; +use std::thread; use tracing::debug; mod amounts; @@ -18,7 +18,7 @@ use self::{amounts::*, message0::*, message1::*}; use crate::{ network::{ peer_tracker::{self, PeerTracker}, - request_response::{AliceToBob, TIMEOUT}, + request_response::AliceToBob, transport, TokioExecutor, }, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, @@ -27,6 +27,7 @@ use xmr_btc::{alice::State0, bob, monero}; pub type Swarm = libp2p::Swarm; +// FIXME: This whole function is horrible, needs total re-write. #[allow(unused_assignments)] // Due to the mutable message0? pub async fn swap( listen: Multiaddr, @@ -132,7 +133,7 @@ fn new_swarm(listen: Multiaddr) -> Result { #[derive(Debug)] pub enum OutEvent { ConnectionEstablished(PeerId), - Request(amounts::OutEvent), + Request(amounts::OutEvent), // Not-uniform with Bob on purpose, ready for adding Xmr event. Message0(bob::Message0), Message1 { msg: bob::Message1, @@ -200,10 +201,12 @@ impl Alice { self.amounts.send(channel, msg); } + /// Message0 gets sent within the network layer using this state0. pub fn set_state0(&mut self, state: State0) { let _ = self.message0.set_state(state); } + /// Send Message1 to Bob in response to receiving his Message1. pub fn send_message1( &mut self, channel: ResponseChannel, @@ -216,13 +219,12 @@ impl Alice { impl Default for Alice { fn default() -> Self { let identity = Keypair::generate_ed25519(); - let timeout = Duration::from_secs(TIMEOUT); Self { pt: PeerTracker::default(), - amounts: Amounts::new(timeout), - message0: Message0::new(timeout), - message1: Message1::new(timeout), + amounts: Amounts::default(), + message0: Message0::default(), + message1: Message1::default(), identity, } } diff --git a/swap/src/alice/amounts.rs b/swap/src/alice/amounts.rs index fb4872bc..e48fccfb 100644 --- a/swap/src/alice/amounts.rs +++ b/swap/src/alice/amounts.rs @@ -14,7 +14,7 @@ use std::{ }; use tracing::{debug, error}; -use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}; +use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT}; #[derive(Debug)] pub enum OutEvent { @@ -35,20 +35,6 @@ pub struct Amounts { } impl Amounts { - pub fn new(timeout: Duration) -> Self { - let mut config = RequestResponseConfig::default(); - config.set_request_timeout(timeout); - - Self { - rr: RequestResponse::new( - Codec::default(), - vec![(Protocol, ProtocolSupport::Full)], - config, - ), - events: Default::default(), - } - } - /// Alice always sends her messages as a response to a request from Bob. pub fn send(&mut self, channel: ResponseChannel, msg: AliceToBob) { self.rr.send_response(channel, msg); @@ -79,6 +65,24 @@ impl Amounts { } } +impl Default for Amounts { + fn default() -> Self { + let timeout = Duration::from_secs(TIMEOUT); + + let mut config = RequestResponseConfig::default(); + config.set_request_timeout(timeout); + + Self { + rr: RequestResponse::new( + Codec::default(), + vec![(Protocol, ProtocolSupport::Full)], + config, + ), + events: Default::default(), + } + } +} + impl NetworkBehaviourEventProcess> for Amounts { fn inject_event(&mut self, event: RequestResponseEvent) { match event { diff --git a/swap/src/alice/message0.rs b/swap/src/alice/message0.rs index ec85d57a..77b8d5ab 100644 --- a/swap/src/alice/message0.rs +++ b/swap/src/alice/message0.rs @@ -15,7 +15,7 @@ use std::{ }; use tracing::error; -use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}; +use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT}; use xmr_btc::{alice::State0, bob}; #[derive(Debug)] @@ -36,21 +36,6 @@ pub struct Message0 { } impl Message0 { - pub fn new(timeout: Duration) -> Self { - let mut config = RequestResponseConfig::default(); - config.set_request_timeout(timeout); - - Self { - rr: RequestResponse::new( - Codec::default(), - vec![(Protocol, ProtocolSupport::Full)], - config, - ), - events: Default::default(), - state: None, - } - } - pub fn set_state(&mut self, state: State0) -> Result<()> { if self.state.is_some() { bail!("Trying to set state a second time"); @@ -73,6 +58,24 @@ impl Message0 { } } +impl Default for Message0 { + fn default() -> Self { + let timeout = Duration::from_secs(TIMEOUT); + let mut config = RequestResponseConfig::default(); + config.set_request_timeout(timeout); + + Self { + rr: RequestResponse::new( + Codec::default(), + vec![(Protocol, ProtocolSupport::Full)], + config, + ), + events: Default::default(), + state: None, + } + } +} + impl NetworkBehaviourEventProcess> for Message0 { fn inject_event(&mut self, event: RequestResponseEvent) { match event { diff --git a/swap/src/alice/message1.rs b/swap/src/alice/message1.rs index 4995e99a..bb4b368a 100644 --- a/swap/src/alice/message1.rs +++ b/swap/src/alice/message1.rs @@ -13,7 +13,7 @@ use std::{ }; use tracing::error; -use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}; +use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT}; use xmr_btc::bob; #[derive(Debug)] @@ -37,20 +37,6 @@ pub struct Message1 { } impl Message1 { - pub fn new(timeout: Duration) -> Self { - let mut config = RequestResponseConfig::default(); - config.set_request_timeout(timeout); - - Self { - rr: RequestResponse::new( - Codec::default(), - vec![(Protocol, ProtocolSupport::Full)], - config, - ), - events: Default::default(), - } - } - pub fn send(&mut self, channel: ResponseChannel, msg: xmr_btc::alice::Message1) { let msg = AliceToBob::Message1(msg); self.rr.send_response(channel, msg); @@ -111,3 +97,20 @@ impl NetworkBehaviourEventProcess> } } } + +impl Default for Message1 { + fn default() -> Self { + let timeout = Duration::from_secs(TIMEOUT); + let mut config = RequestResponseConfig::default(); + config.set_request_timeout(timeout); + + Self { + rr: RequestResponse::new( + Codec::default(), + vec![(Protocol, ProtocolSupport::Full)], + config, + ), + events: Default::default(), + } + } +} diff --git a/swap/src/bob.rs b/swap/src/bob.rs index d3422b5d..22c339ee 100644 --- a/swap/src/bob.rs +++ b/swap/src/bob.rs @@ -7,7 +7,7 @@ use futures::{ }; use libp2p::{core::identity::Keypair, Multiaddr, NetworkBehaviour, PeerId}; use rand::rngs::OsRng; -use std::{process, thread, time::Duration}; +use std::{process, thread}; use tracing::{debug, info}; mod amounts; @@ -18,10 +18,9 @@ use self::{amounts::*, message0::*, message1::*}; use crate::{ network::{ peer_tracker::{self, PeerTracker}, - request_response::TIMEOUT, transport, TokioExecutor, }, - Cmd, Rsp, PUNISH_TIMELOCK, REFUND_TIMELOCK, + Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, }; use xmr_btc::{ alice, @@ -29,6 +28,7 @@ use xmr_btc::{ bob::{self, State0}, }; +// FIXME: This whole function is horrible, needs total re-write. pub async fn swap( btc: u64, addr: Multiaddr, @@ -52,9 +52,9 @@ where swarm.request_amounts(alice.clone(), btc); let (btc, xmr) = match swarm.next().await { - OutEvent::Amounts(amounts::OutEvent::Amounts(p)) => { - debug!("Got amounts from Alice: {:?}", p); - let cmd = Cmd::VerifyAmounts(p); + OutEvent::Amounts(amounts) => { + debug!("Got amounts from Alice: {:?}", amounts); + let cmd = Cmd::VerifyAmounts(amounts); cmd_tx.try_send(cmd)?; let response = rsp_rx.next().await; if response == Some(Rsp::Abort) { @@ -63,7 +63,7 @@ where } info!("User verified amounts, continuing with swap ..."); - (p.btc, p.xmr) + (amounts.btc, amounts.xmr) } other => panic!("unexpected event: {:?}", other), }; @@ -131,7 +131,7 @@ fn new_swarm() -> Result { #[derive(Debug)] pub enum OutEvent { ConnectionEstablished(PeerId), - Amounts(amounts::OutEvent), + Amounts(SwapAmounts), Message0(alice::Message0), Message1(alice::Message1), } @@ -148,7 +148,9 @@ impl From for OutEvent { impl From for OutEvent { fn from(event: amounts::OutEvent) -> Self { - OutEvent::Amounts(event) + match event { + amounts::OutEvent::Amounts(amounts) => OutEvent::Amounts(amounts), + } } } @@ -216,13 +218,12 @@ impl Bob { impl Default for Bob { fn default() -> Bob { let identity = Keypair::generate_ed25519(); - let timeout = Duration::from_secs(TIMEOUT); Self { pt: PeerTracker::default(), - amounts: Amounts::new(timeout), - message0: Message0::new(timeout), - message1: Message1::new(timeout), + amounts: Amounts::default(), + message0: Message0::default(), + message1: Message1::default(), identity, } } diff --git a/swap/src/bob/amounts.rs b/swap/src/bob/amounts.rs index fcdb74e0..f47f3086 100644 --- a/swap/src/bob/amounts.rs +++ b/swap/src/bob/amounts.rs @@ -15,7 +15,7 @@ use std::{ use tracing::error; use crate::{ - network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}, + network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT}, SwapAmounts, }; @@ -35,20 +35,6 @@ pub struct Amounts { } impl Amounts { - pub fn new(timeout: Duration) -> Self { - let mut config = RequestResponseConfig::default(); - config.set_request_timeout(timeout); - - Self { - rr: RequestResponse::new( - Codec::default(), - vec![(Protocol, ProtocolSupport::Full)], - config, - ), - events: Default::default(), - } - } - pub fn request_amounts(&mut self, alice: PeerId, btc: ::bitcoin::Amount) -> Result { let msg = BobToAlice::AmountsFromBtc(btc); let id = self.rr.send_request(&alice, msg); @@ -69,6 +55,24 @@ impl Amounts { } } +impl Default for Amounts { + fn default() -> Self { + let timeout = Duration::from_secs(TIMEOUT); + + let mut config = RequestResponseConfig::default(); + config.set_request_timeout(timeout); + + Self { + rr: RequestResponse::new( + Codec::default(), + vec![(Protocol, ProtocolSupport::Full)], + config, + ), + events: Default::default(), + } + } +} + impl NetworkBehaviourEventProcess> for Amounts { fn inject_event(&mut self, event: RequestResponseEvent) { match event { diff --git a/swap/src/bob/message0.rs b/swap/src/bob/message0.rs index fd20c841..3f283d69 100644 --- a/swap/src/bob/message0.rs +++ b/swap/src/bob/message0.rs @@ -13,7 +13,7 @@ use std::{ }; use tracing::error; -use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}; +use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT}; use xmr_btc::{alice, bob}; #[derive(Debug)] @@ -32,20 +32,6 @@ pub struct Message0 { } impl Message0 { - pub fn new(timeout: Duration) -> Self { - let mut config = RequestResponseConfig::default(); - config.set_request_timeout(timeout); - - Self { - rr: RequestResponse::new( - Codec::default(), - vec![(Protocol, ProtocolSupport::Full)], - config, - ), - events: Default::default(), - } - } - pub fn send(&mut self, alice: PeerId, msg: bob::Message0) { let msg = BobToAlice::Message0(msg); let _id = self.rr.send_request(&alice, msg); @@ -64,6 +50,23 @@ impl Message0 { } } +impl Default for Message0 { + fn default() -> Self { + let timeout = Duration::from_secs(TIMEOUT); + let mut config = RequestResponseConfig::default(); + config.set_request_timeout(timeout); + + Self { + rr: RequestResponse::new( + Codec::default(), + vec![(Protocol, ProtocolSupport::Full)], + config, + ), + events: Default::default(), + } + } +} + impl NetworkBehaviourEventProcess> for Message0 { fn inject_event(&mut self, event: RequestResponseEvent) { match event { diff --git a/swap/src/bob/message1.rs b/swap/src/bob/message1.rs index 43d24850..979957be 100644 --- a/swap/src/bob/message1.rs +++ b/swap/src/bob/message1.rs @@ -13,7 +13,7 @@ use std::{ }; use tracing::error; -use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol}; +use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT}; use xmr_btc::{alice, bob}; #[derive(Debug)] @@ -32,20 +32,6 @@ pub struct Message1 { } impl Message1 { - pub fn new(timeout: Duration) -> Self { - let mut config = RequestResponseConfig::default(); - config.set_request_timeout(timeout); - - Self { - rr: RequestResponse::new( - Codec::default(), - vec![(Protocol, ProtocolSupport::Full)], - config, - ), - events: Default::default(), - } - } - pub fn send(&mut self, alice: PeerId, msg: bob::Message1) { let msg = BobToAlice::Message1(msg); let _id = self.rr.send_request(&alice, msg); @@ -64,6 +50,23 @@ impl Message1 { } } +impl Default for Message1 { + fn default() -> Self { + let timeout = Duration::from_secs(TIMEOUT); + let mut config = RequestResponseConfig::default(); + config.set_request_timeout(timeout); + + Self { + rr: RequestResponse::new( + Codec::default(), + vec![(Protocol, ProtocolSupport::Full)], + config, + ), + events: Default::default(), + } + } +} + impl NetworkBehaviourEventProcess> for Message1 { fn inject_event(&mut self, event: RequestResponseEvent) { match event { From 19f065575d9e311b66230ac480611b0755f9b4a3 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 22 Oct 2020 14:30:10 +1100 Subject: [PATCH 50/52] Do a bunch of cleanups --- swap/src/alice/amounts.rs | 30 +++++------------ swap/src/alice/message0.rs | 33 ++++++------------- swap/src/alice/message1.rs | 66 +++++++++++++++----------------------- swap/src/bitcoin.rs | 9 +++--- swap/src/bob/amounts.rs | 25 +++++---------- swap/src/bob/message0.rs | 25 +++++---------- swap/src/bob/message1.rs | 25 +++++---------- 7 files changed, 72 insertions(+), 141 deletions(-) diff --git a/swap/src/alice/amounts.rs b/swap/src/alice/amounts.rs index e48fccfb..39396861 100644 --- a/swap/src/alice/amounts.rs +++ b/swap/src/alice/amounts.rs @@ -87,39 +87,25 @@ impl NetworkBehaviourEventProcess> fn inject_event(&mut self, event: RequestResponseEvent) { match event { RequestResponseEvent::Message { - peer: _, message: RequestResponseMessage::Request { - request, - request_id: _, - channel, + request, channel, .. }, + .. } => match request { BobToAlice::AmountsFromBtc(btc) => { self.events.push_back(OutEvent::Btc { btc, channel }) } - _ => panic!("unexpected request"), + other => debug!("got request: {:?}", other), }, RequestResponseEvent::Message { - peer: _, - message: - RequestResponseMessage::Response { - response: _, - request_id: _, - }, - } => panic!("unexpected response"), - RequestResponseEvent::InboundFailure { - peer: _, - request_id: _, - error, - } => { + message: RequestResponseMessage::Response { .. }, + .. + } => panic!("Alice should not get a Response"), + RequestResponseEvent::InboundFailure { error, .. } => { error!("Inbound failure: {:?}", error); } - RequestResponseEvent::OutboundFailure { - peer: _, - request_id: _, - error, - } => { + RequestResponseEvent::OutboundFailure { error, .. } => { error!("Outbound failure: {:?}", error); } } diff --git a/swap/src/alice/message0.rs b/swap/src/alice/message0.rs index 77b8d5ab..460c63e4 100644 --- a/swap/src/alice/message0.rs +++ b/swap/src/alice/message0.rs @@ -13,7 +13,7 @@ use std::{ task::{Context, Poll}, time::Duration, }; -use tracing::error; +use tracing::{debug, error}; use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT}; use xmr_btc::{alice::State0, bob}; @@ -80,13 +80,11 @@ impl NetworkBehaviourEventProcess> fn inject_event(&mut self, event: RequestResponseEvent) { match event { RequestResponseEvent::Message { - peer: _, message: RequestResponseMessage::Request { - request, - request_id: _, - channel, + request, channel, .. }, + .. } => match request { BobToAlice::Message0(msg) => { let response = match &self.state { @@ -97,31 +95,18 @@ impl NetworkBehaviourEventProcess> } }; self.rr.send_response(channel, response); - self.events.push_back(OutEvent::Msg(msg)); } - _ => panic!("unexpected request"), + other => debug!("got request: {:?}", other), }, RequestResponseEvent::Message { - peer: _, - message: - RequestResponseMessage::Response { - response: _, - request_id: _, - }, - } => panic!("unexpected response"), - RequestResponseEvent::InboundFailure { - peer: _, - request_id: _, - error, - } => { + message: RequestResponseMessage::Response { .. }, + .. + } => panic!("Alice should not get a Response"), + RequestResponseEvent::InboundFailure { error, .. } => { error!("Inbound failure: {:?}", error); } - RequestResponseEvent::OutboundFailure { - peer: _, - request_id: _, - error, - } => { + RequestResponseEvent::OutboundFailure { error, .. } => { error!("Outbound failure: {:?}", error); } } diff --git a/swap/src/alice/message1.rs b/swap/src/alice/message1.rs index bb4b368a..27df9e20 100644 --- a/swap/src/alice/message1.rs +++ b/swap/src/alice/message1.rs @@ -11,7 +11,7 @@ use std::{ task::{Context, Poll}, time::Duration, }; -use tracing::error; +use tracing::{debug, error}; use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT}; use xmr_btc::bob; @@ -55,62 +55,48 @@ impl Message1 { } } +impl Default for Message1 { + fn default() -> Self { + let timeout = Duration::from_secs(TIMEOUT); + let mut config = RequestResponseConfig::default(); + config.set_request_timeout(timeout); + + Self { + rr: RequestResponse::new( + Codec::default(), + vec![(Protocol, ProtocolSupport::Full)], + config, + ), + events: Default::default(), + } + } +} + impl NetworkBehaviourEventProcess> for Message1 { fn inject_event(&mut self, event: RequestResponseEvent) { match event { RequestResponseEvent::Message { - peer: _, message: RequestResponseMessage::Request { - request, - request_id: _, - channel, + request, channel, .. }, + .. } => match request { BobToAlice::Message1(msg) => { self.events.push_back(OutEvent::Msg { msg, channel }); } - other => panic!("unexpected request: {:?}", other), + other => debug!("got request: {:?}", other), }, RequestResponseEvent::Message { - peer: _, - message: - RequestResponseMessage::Response { - response: _, - request_id: _, - }, - } => panic!("unexpected response"), - RequestResponseEvent::InboundFailure { - peer: _, - request_id: _, - error, - } => { + message: RequestResponseMessage::Response { .. }, + .. + } => panic!("Alice should not get a Response"), + RequestResponseEvent::InboundFailure { error, .. } => { error!("Inbound failure: {:?}", error); } - RequestResponseEvent::OutboundFailure { - peer: _, - request_id: _, - error, - } => { + RequestResponseEvent::OutboundFailure { error, .. } => { error!("Outbound failure: {:?}", error); } } } } - -impl Default for Message1 { - fn default() -> Self { - let timeout = Duration::from_secs(TIMEOUT); - let mut config = RequestResponseConfig::default(); - config.set_request_timeout(timeout); - - Self { - rr: RequestResponse::new( - Codec::default(), - vec![(Protocol, ProtocolSupport::Full)], - config, - ), - events: Default::default(), - } - } -} diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 1b3495ea..fdea493e 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -1,16 +1,19 @@ use anyhow::Result; use async_trait::async_trait; use backoff::{future::FutureOperation as _, ExponentialBackoff}; -use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid}; +use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Transaction}; use bitcoin_harness::bitcoind_rpc::PsbtBase64; use reqwest::Url; use xmr_btc::{ bitcoin::{ - BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxLock, WatchForRawTransaction, + Amount, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxLock, Txid, + WatchForRawTransaction, }, MedianTime, }; +// This is cut'n'paste from xmr_btc/tests/harness/wallet/bitcoin.rs + #[derive(Debug)] pub struct Wallet(pub bitcoin_harness::Wallet); @@ -37,8 +40,6 @@ impl Wallet { .await .map(|res| bitcoin::Amount::from_btc(-res.fee))??; - // FIXME: Handle re-export of bitcoin::Amount correctly. - let fee = Amount::from_sat(fee.as_sat()); Ok(fee) } } diff --git a/swap/src/bob/amounts.rs b/swap/src/bob/amounts.rs index f47f3086..46c71920 100644 --- a/swap/src/bob/amounts.rs +++ b/swap/src/bob/amounts.rs @@ -12,7 +12,7 @@ use std::{ task::{Context, Poll}, time::Duration, }; -use tracing::error; +use tracing::{debug, error}; use crate::{ network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT}, @@ -77,29 +77,20 @@ impl NetworkBehaviourEventProcess> fn inject_event(&mut self, event: RequestResponseEvent) { match event { RequestResponseEvent::Message { - peer: _, message: RequestResponseMessage::Request { .. }, + .. } => panic!("Bob should never get a request from Alice"), RequestResponseEvent::Message { - peer: _, - message: - RequestResponseMessage::Response { - response, - request_id: _, - }, + message: RequestResponseMessage::Response { response, .. }, + .. } => match response { AliceToBob::Amounts(p) => self.events.push_back(OutEvent::Amounts(p)), - other => panic!("unexpected response: {:?}", other), + other => debug!("got response: {:?}", other), }, - - RequestResponseEvent::InboundFailure { .. } => { - panic!("Bob should never get a request from Alice, so should never get an InboundFailure"); + RequestResponseEvent::InboundFailure { error, .. } => { + error!("Inbound failure: {:?}", error); } - RequestResponseEvent::OutboundFailure { - peer: _, - request_id: _, - error, - } => { + RequestResponseEvent::OutboundFailure { error, .. } => { error!("Outbound failure: {:?}", error); } } diff --git a/swap/src/bob/message0.rs b/swap/src/bob/message0.rs index 3f283d69..268244a1 100644 --- a/swap/src/bob/message0.rs +++ b/swap/src/bob/message0.rs @@ -11,7 +11,7 @@ use std::{ task::{Context, Poll}, time::Duration, }; -use tracing::error; +use tracing::{debug, error}; use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT}; use xmr_btc::{alice, bob}; @@ -71,29 +71,20 @@ impl NetworkBehaviourEventProcess> fn inject_event(&mut self, event: RequestResponseEvent) { match event { RequestResponseEvent::Message { - peer: _, message: RequestResponseMessage::Request { .. }, + .. } => panic!("Bob should never get a request from Alice"), RequestResponseEvent::Message { - peer: _, - message: - RequestResponseMessage::Response { - response, - request_id: _, - }, + message: RequestResponseMessage::Response { response, .. }, + .. } => match response { AliceToBob::Message0(msg) => self.events.push_back(OutEvent::Msg(msg)), - other => panic!("unexpected response: {:?}", other), + other => debug!("got response: {:?}", other), }, - - RequestResponseEvent::InboundFailure { .. } => { - panic!("Bob should never get a request from Alice, so should never get an InboundFailure"); + RequestResponseEvent::InboundFailure { error, .. } => { + error!("Inbound failure: {:?}", error); } - RequestResponseEvent::OutboundFailure { - peer: _, - request_id: _, - error, - } => { + RequestResponseEvent::OutboundFailure { error, .. } => { error!("Outbound failure: {:?}", error); } } diff --git a/swap/src/bob/message1.rs b/swap/src/bob/message1.rs index 979957be..9e85ce3d 100644 --- a/swap/src/bob/message1.rs +++ b/swap/src/bob/message1.rs @@ -11,7 +11,7 @@ use std::{ task::{Context, Poll}, time::Duration, }; -use tracing::error; +use tracing::{debug, error}; use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Protocol, TIMEOUT}; use xmr_btc::{alice, bob}; @@ -71,29 +71,20 @@ impl NetworkBehaviourEventProcess> fn inject_event(&mut self, event: RequestResponseEvent) { match event { RequestResponseEvent::Message { - peer: _, message: RequestResponseMessage::Request { .. }, + .. } => panic!("Bob should never get a request from Alice"), RequestResponseEvent::Message { - peer: _, - message: - RequestResponseMessage::Response { - response, - request_id: _, - }, + message: RequestResponseMessage::Response { response, .. }, + .. } => match response { AliceToBob::Message1(msg) => self.events.push_back(OutEvent::Msg(msg)), - other => panic!("unexpected response: {:?}", other), + other => debug!("got response: {:?}", other), }, - - RequestResponseEvent::InboundFailure { .. } => { - panic!("Bob should never get a request from Alice, so should never get an InboundFailure"); + RequestResponseEvent::InboundFailure { error, .. } => { + error!("Inbound failure: {:?}", error); } - RequestResponseEvent::OutboundFailure { - peer: _, - request_id: _, - error, - } => { + RequestResponseEvent::OutboundFailure { error, .. } => { error!("Outbound failure: {:?}", error); } } From 97363cb05ce5a9483efefadfca65e482675ababc Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 22 Oct 2020 14:42:36 +1100 Subject: [PATCH 51/52] Fix build after merge --- swap/src/bitcoin.rs | 22 +++------------------- xmr-btc/tests/e2e.rs | 17 ++++++++++------- xmr-btc/tests/harness/mod.rs | 2 ++ 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index fdea493e..2a4a8e8d 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -4,12 +4,9 @@ use backoff::{future::FutureOperation as _, ExponentialBackoff}; use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Transaction}; use bitcoin_harness::bitcoind_rpc::PsbtBase64; use reqwest::Url; -use xmr_btc::{ - bitcoin::{ - Amount, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxLock, Txid, - WatchForRawTransaction, - }, - MedianTime, +use xmr_btc::bitcoin::{ + Amount, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxLock, Txid, + WatchForRawTransaction, }; // This is cut'n'paste from xmr_btc/tests/harness/wallet/bitcoin.rs @@ -100,16 +97,3 @@ impl WatchForRawTransaction for Wallet { .expect("transient errors to be retried") } } - -#[async_trait] -impl MedianTime for Wallet { - async fn median_time(&self) -> u32 { - (|| async { Ok(self.0.median_time().await?) }) - .retry(ExponentialBackoff { - max_elapsed_time: None, - ..Default::default() - }) - .await - .expect("transient errors to be retried") - } -} diff --git a/xmr-btc/tests/e2e.rs b/xmr-btc/tests/e2e.rs index 1688fecd..148dcbe4 100644 --- a/xmr-btc/tests/e2e.rs +++ b/xmr-btc/tests/e2e.rs @@ -8,8 +8,11 @@ mod tests { use crate::{ harness, - harness::node::{run_alice_until, run_bob_until}, - init_bitcoind, init_test, ALICE_TEST_DB_FOLDER, BOB_TEST_DB_FOLDER, + harness::{ + init_bitcoind, init_test, + node::{run_alice_until, run_bob_until}, + ALICE_TEST_DB_FOLDER, BOB_TEST_DB_FOLDER, + }, }; use futures::future; use monero_harness::Monero; @@ -258,7 +261,7 @@ mod tests { mut bob_node, initial_balances, swap_amounts, - ) = init_test(&monero, &bitcoind).await; + ) = init_test(&monero, &bitcoind, None, None).await; { let (alice_state, bob_state) = future::try_join( @@ -341,13 +344,13 @@ mod tests { assert_eq!( alice_final_xmr_balance, - initial_balances.alice_xmr - - u64::from(swap_amounts.xmr) - - u64::from(alice_state6.lock_xmr_fee()) + initial_balances.alice_xmr.as_piconero() + - swap_amounts.xmr.as_piconero() + - alice_state6.lock_xmr_fee().as_piconero() ); assert_eq!( bob_final_xmr_balance, - initial_balances.bob_xmr + u64::from(swap_amounts.xmr) + initial_balances.bob_xmr.as_piconero() + swap_amounts.xmr.as_piconero() ); } } diff --git a/xmr-btc/tests/harness/mod.rs b/xmr-btc/tests/harness/mod.rs index a078edb3..022e8715 100644 --- a/xmr-btc/tests/harness/mod.rs +++ b/xmr-btc/tests/harness/mod.rs @@ -59,6 +59,8 @@ use xmr_btc::{bitcoin, monero}; const TEN_XMR: u64 = 10_000_000_000_000; const RELATIVE_REFUND_TIMELOCK: u32 = 1; const RELATIVE_PUNISH_TIMELOCK: u32 = 1; +pub const ALICE_TEST_DB_FOLDER: &str = "../target/e2e-test-alice-recover"; +pub const BOB_TEST_DB_FOLDER: &str = "../target/e2e-test-bob-recover"; pub async fn init_bitcoind(tc_client: &Cli) -> Bitcoind<'_> { let bitcoind = Bitcoind::new(tc_client, "0.19.1").expect("failed to create bitcoind"); From 79c6c8bbef70cb88c61881d2a8c52c24f94f2c54 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 22 Oct 2020 15:02:32 +1100 Subject: [PATCH 52/52] Sort Cargo.toml lines --- xmr-btc/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index 254496f0..127dcb64 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -35,10 +35,10 @@ bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = futures = "0.3" hyper = "0.13" monero-harness = { path = "../monero-harness" } +port_check = "0.1" reqwest = { version = "0.10", default-features = false } serde_cbor = "0.11" sled = "0.34" -port_check = "0.1" spectral = "0.6" tempfile = "3" testcontainers = "0.10"