From e84c56378c59f2d7312170a418a11e8df3196f48 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Fri, 23 Oct 2020 23:05:34 +1100 Subject: [PATCH 1/7] Test that both parties refund if Alice does not redeem Also: - Move generator functions to `alice` and `bob` modules. This makes using `tracing` a lot easier, since the context of the file name let's us differentiate between Alice's and Bob's generator logs more clearly. - Accept 0 confirmations when watching for the Monero lock transaction. This should eventually be configured by the application, but in the tests it's making things unexpectedly slower. --- xmr-btc/src/alice.rs | 330 +++++++++++++- xmr-btc/src/bitcoin.rs | 23 + xmr-btc/src/bob.rs | 236 +++++++++- xmr-btc/src/lib.rs | 557 ------------------------ xmr-btc/tests/harness/wallet/bitcoin.rs | 8 +- xmr-btc/tests/on_chain.rs | 172 +++++++- 6 files changed, 735 insertions(+), 591 deletions(-) diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index 73d83976..9667c18f 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -1,23 +1,349 @@ use crate::{ bitcoin, - bitcoin::{BroadcastSignedTransaction, WatchForRawTransaction}, + bitcoin::{poll_until_block_height_is_gte, BroadcastSignedTransaction, WatchForRawTransaction}, bob, monero, monero::{CreateWalletForOutput, Transfer}, transport::{ReceiveMessage, SendMessage}, }; use anyhow::{anyhow, Result}; +use async_trait::async_trait; use ecdsa_fun::{ adaptor::{Adaptor, EncryptedSignature}, nonce::Deterministic, }; +use futures::{ + future::{select, Either}, + FutureExt, +}; +use genawaiter::sync::{Gen, GenBoxed}; use rand::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; use sha2::Sha256; -use std::convert::{TryFrom, TryInto}; +use std::{ + convert::{TryFrom, TryInto}, + sync::Arc, + time::Duration, +}; +use tokio::time::timeout; +use tracing::error; pub mod message; pub use message::{Message, Message0, Message1, Message2}; +#[derive(Debug)] +pub enum Action { + // 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( + mut network: N, + bitcoin_client: Arc, + // TODO: Replace this with a new, slimmer struct? + 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, + .. + }: State3, +) -> GenBoxed +where + N: ReceiveBitcoinRedeemEncsig + Send + Sync + 'static, + B: bitcoin::BlockHeight + + bitcoin::TransactionBlockHeight + + bitcoin::WatchForRawTransaction + + Send + + Sync + + 'static, +{ + #[derive(Debug)] + enum SwapFailed { + BeforeBtcLock, + AfterXmrLock { tx_lock_height: u32, reason: Reason }, + } + + /// Reason why the swap has failed. + #[derive(Debug)] + enum Reason { + /// The refund timelock has been reached. + BtcExpired, + } + + #[derive(Debug)] + 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, + } + + Gen::new_boxed(|co| async move { + let swap_result: Result<(), SwapFailed> = async { + timeout( + Duration::from_secs(bob::SECS_TO_ACT), + bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), + ) + .await + .map_err(|_| SwapFailed::BeforeBtcLock)?; + + let tx_lock_height = bitcoin_client + .transaction_block_height(tx_lock.txid()) + .await; + let poll_until_btc_has_expired = poll_until_block_height_is_gte( + bitcoin_client.as_ref(), + tx_lock_height + refund_timelock, + ) + .shared(); + futures::pin_mut!(poll_until_btc_has_expired); + + let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { + scalar: s_a.into_ed25519(), + }); + + co.yield_(Action::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: Reason::BtcExpired, + tx_lock_height, + }) + } + }; + + 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_(Action::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: Reason::BtcExpired, + tx_lock_height, + }) + } + }; + + Ok(()) + } + .await; + + if let Err(ref err) = swap_result { + error!("swap failed: {:?}", err); + } + + if let Err(SwapFailed::AfterXmrLock { + reason: Reason::BtcExpired, + tx_lock_height, + }) = swap_result + { + let refund_result: Result<(), RefundFailed> = async { + let poll_until_bob_can_be_punished = poll_until_block_height_is_gte( + bitcoin_client.as_ref(), + tx_lock_height + refund_timelock + punish_timelock, + ) + .shared(); + futures::pin_mut!(poll_until_bob_can_be_punished); + + let tx_cancel = + bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone()); + let signed_tx_cancel = { + let sig_a = a.sign(tx_cancel.digest()); + let sig_b = tx_cancel_sig_bob.clone(); + + 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_(Action::CancelBtc(signed_tx_cancel)).await; + + 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, a.public()) + .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_(Action::CreateMoneroWalletForOutput { + spend_key: s_a + s_b, + view_key: v, + }) + .await; + + Ok(()) + } + .await; + + if let Err(ref err) = refund_result { + error!("refund failed: {:?}", err); + } + + // 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_(Action::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_(Action::PunishBtc(signed_tx_punish)).await; + + let _ = bitcoin_client + .watch_for_raw_transaction(tx_punish_txid) + .await; + } + } + }) +} + // There are no guarantees that send_message and receive_massage do not block // the flow of execution. Therefore they must be paired between Alice/Bob, one // send to one receive in the correct order. diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index f85c0271..9ac38365 100644 --- a/xmr-btc/src/bitcoin.rs +++ b/xmr-btc/src/bitcoin.rs @@ -190,6 +190,16 @@ pub trait WatchForRawTransaction { async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction; } +#[async_trait] +pub trait BlockHeight { + async fn block_height(&self) -> u32; +} + +#[async_trait] +pub trait TransactionBlockHeight { + async fn transaction_block_height(&self, txid: Txid) -> u32; +} + pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result { let adaptor = Adaptor::>::default(); @@ -200,3 +210,16 @@ pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Resu Ok(s) } + +pub async fn poll_until_block_height_is_gte(client: &B, target: u32) +where + B: BlockHeight, +{ + loop { + if client.block_height().await >= target { + return; + } + + tokio::time::delay_for(std::time::Duration::from_secs(1)).await; + } +} diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 5f4f0f44..5b1c1667 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -1,28 +1,258 @@ use crate::{ alice, bitcoin::{ - self, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxCancel, - WatchForRawTransaction, + self, poll_until_block_height_is_gte, BroadcastSignedTransaction, BuildTxLockPsbt, + SignTxLock, TxCancel, WatchForRawTransaction, }, monero, serde::monero_private_key, transport::{ReceiveMessage, SendMessage}, }; use anyhow::{anyhow, Result}; +use async_trait::async_trait; use ecdsa_fun::{ adaptor::{Adaptor, EncryptedSignature}, nonce::Deterministic, Signature, }; +use futures::{ + future::{select, Either}, + FutureExt, +}; +use genawaiter::sync::{Gen, GenBoxed}; use rand::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; use sha2::Sha256; -use std::convert::{TryFrom, TryInto}; +use std::{ + convert::{TryFrom, TryInto}, + sync::Arc, + time::Duration, +}; +use tokio::time::timeout; +use tracing::error; pub mod message; use crate::monero::{CreateWalletForOutput, WatchForTransfer}; pub use message::{Message, Message0, Message1, Message2, Message3}; +// 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. +pub const SECS_TO_ACT: u64 = 60; + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum Action { + LockBtc(bitcoin::TxLock), + SendBtcRedeemEncsig(bitcoin::EncryptedSignature), + CreateXmrWalletForOutput { + spend_key: monero::PrivateKey, + view_key: monero::PrivateViewKey, + }, + CancelBtc(bitcoin::Transaction), + RefundBtc(bitcoin::Transaction), +} + +// TODO: This could be moved to the monero module +#[async_trait] +pub trait ReceiveTransferProof { + async fn receive_transfer_proof(&mut 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( + mut network: N, + monero_client: Arc, + bitcoin_client: Arc, + // TODO: Replace this with a new, slimmer struct? + 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, + .. + }: State2, +) -> GenBoxed +where + N: ReceiveTransferProof + Send + Sync + 'static, + M: monero::WatchForTransfer + Send + Sync + 'static, + B: bitcoin::BlockHeight + + bitcoin::TransactionBlockHeight + + bitcoin::WatchForRawTransaction + + Send + + Sync + + 'static, +{ + #[derive(Debug)] + enum SwapFailed { + BeforeBtcLock, + AfterBtcLock(Reason), + AfterBtcRedeem(Reason), + } + + /// Reason why the swap has failed. + #[derive(Debug)] + enum Reason { + /// The refund timelock has been reached. + 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, + } + + Gen::new_boxed(|co| async move { + let swap_result: Result<(), SwapFailed> = async { + co.yield_(Action::LockBtc(tx_lock.clone())).await; + + timeout( + Duration::from_secs(SECS_TO_ACT), + bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), + ) + .await + .map(|tx| tx.txid()) + .map_err(|_| SwapFailed::BeforeBtcLock)?; + + let tx_lock_height = bitcoin_client + .transaction_block_height(tx_lock.txid()) + .await; + let poll_until_btc_has_expired = poll_until_block_height_is_gte( + bitcoin_client.as_ref(), + tx_lock_height + refund_timelock, + ) + .shared(); + futures::pin_mut!(poll_until_btc_has_expired); + + 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::AfterBtcLock(Reason::BtcExpired)), + }; + + 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; + + match select( + monero_client.watch_for_transfer(S, v.public(), transfer_proof, xmr, 0), + poll_until_btc_has_expired.clone(), + ) + .await + { + Either::Left((Err(e), _)) => { + return Err(SwapFailed::AfterBtcLock(Reason::InsufficientXmr(e))) + } + Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), + _ => {} + } + + 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::SendBtcRedeemEncsig(tx_redeem_encsig.clone())) + .await; + + let tx_redeem_published = match select( + bitcoin_client.watch_for_raw_transaction(tx_redeem.txid()), + poll_until_btc_has_expired, + ) + .await + { + Either::Left((tx, _)) => tx, + Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), + }; + + let tx_redeem_sig = tx_redeem + .extract_signature_by_key(tx_redeem_published, b.public()) + .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(), + )); + + let s_b = monero::PrivateKey { + scalar: s_b.into_ed25519(), + }; + + co.yield_(Action::CreateXmrWalletForOutput { + spend_key: s_a + s_b, + view_key: v, + }) + .await; + + Ok(()) + } + .await; + + if let Err(ref err) = swap_result { + error!("swap failed: {:?}", err); + } + + 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()); + + 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") + }; + + co.yield_(Action::CancelBtc(signed_tx_cancel)).await; + + let _ = bitcoin_client + .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 = + 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::RefundBtc(signed_tx_refund)).await; + + let _ = bitcoin_client + .watch_for_raw_transaction(tx_refund_txid) + .await; + } + }) +} + // There are no guarantees that send_message and receive_massage do not block // the flow of execution. Therefore they must be paired between Alice/Bob, one // send to one receive in the correct order. diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index f2effbb5..84ca9daa 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -54,560 +54,3 @@ pub mod transport; pub use cross_curve_dleq; pub use curve25519_dalek; - -use async_trait::async_trait; -use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; -use futures::{ - future::{select, Either}, - Future, FutureExt, -}; -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)] -pub enum BobAction { - LockBitcoin(bitcoin::TxLock), - SendBitcoinRedeemEncsig(bitcoin::EncryptedSignature), - CreateMoneroWalletForOutput { - spend_key: monero::PrivateKey, - view_key: monero::PrivateViewKey, - }, - CancelBitcoin(bitcoin::Transaction), - RefundBitcoin(bitcoin::Transaction), -} - -// TODO: This could be moved to the monero module -#[async_trait] -pub trait ReceiveTransferProof { - async fn receive_transfer_proof(&mut self) -> monero::TransferProof; -} - -#[async_trait] -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. -/// -/// This is called post handshake, after all the keys, addresses and most of the -/// signatures have been exchanged. -pub fn action_generator_bob( - mut network: N, - monero_client: Arc, - bitcoin_client: Arc, - // 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 + 'static, - M: monero::WatchForTransfer + Send + Sync + 'static, - B: BlockHeight - + TransactionBlockHeight - + bitcoin::WatchForRawTransaction - + Send - + Sync - + 'static, -{ - #[derive(Debug)] - enum SwapFailed { - BeforeBtcLock, - AfterBtcLock(Reason), - AfterBtcRedeem(Reason), - } - - /// Reason why the swap has failed. - #[derive(Debug)] - enum Reason { - /// The refund timelock has been reached. - 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) { - loop { - if condition_future.clone().await { - return; - } - - tokio::time::delay_for(std::time::Duration::from_secs(1)).await; - } - } - - async fn bitcoin_block_height_is_gte(bitcoin_client: &B, n_blocks: u32) -> bool - where - B: BlockHeight, - { - bitcoin_client.block_height().await >= n_blocks - } - - Gen::new_boxed(|co| async move { - let swap_result: Result<(), SwapFailed> = async { - co.yield_(BobAction::LockBitcoin(tx_lock.clone())).await; - - timeout( - Duration::from_secs(SECS_TO_ACT_BOB), - bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), - ) - .await - .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(), - poll_until_btc_has_expired.clone(), - ) - .await - { - Either::Left((proof, _)) => proof, - Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), - }; - - 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; - - match select( - monero_client.watch_for_transfer( - S, - v.public(), - transfer_proof, - xmr, - monero::MIN_CONFIRMATIONS, - ), - poll_until_btc_has_expired.clone(), - ) - .await - { - Either::Left((Err(e), _)) => { - return Err(SwapFailed::AfterBtcLock(Reason::InsufficientXmr(e))) - } - Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), - _ => {} - } - - 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_(BobAction::SendBitcoinRedeemEncsig(tx_redeem_encsig.clone())) - .await; - - let tx_redeem_published = match select( - bitcoin_client.watch_for_raw_transaction(tx_redeem.txid()), - poll_until_btc_has_expired, - ) - .await - { - Either::Left((tx, _)) => tx, - Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), - }; - - let tx_redeem_sig = tx_redeem - .extract_signature_by_key(tx_redeem_published, b.public()) - .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(), - )); - - let s_b = monero::PrivateKey { - scalar: s_b.into_ed25519(), - }; - - co.yield_(BobAction::CreateMoneroWalletForOutput { - spend_key: s_a + s_b, - view_key: v, - }) - .await; - - Ok(()) - } - .await; - - 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(); - 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") - }; - - co.yield_(BobAction::CancelBitcoin(signed_tx_cancel)).await; - - let _ = bitcoin_client - .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 = - 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_(BobAction::RefundBitcoin(signed_tx_refund)).await; - - let _ = bitcoin_client - .watch_for_raw_transaction(tx_refund_txid) - .await; - } - }) -} - -#[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( - mut network: N, - bitcoin_client: Arc, - // 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 + '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, - } - - 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; - } - - tokio::time::delay_for(std::time::Duration::from_secs(1)).await; - } - } - - async fn bitcoin_block_height_is_gte(bitcoin_client: &B, n_blocks: u32) -> bool - where - B: BlockHeight, - { - bitcoin_client.block_height().await >= n_blocks - } - - Gen::new_boxed(|co| async move { - let swap_result: Result<(), SwapFailed> = async { - timeout( - Duration::from_secs(SECS_TO_ACT_BOB), - bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), - ) - .await - .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(), - }); - - 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_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); - - 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/harness/wallet/bitcoin.rs b/xmr-btc/tests/harness/wallet/bitcoin.rs index c39ba3f7..f9d2c91d 100644 --- a/xmr-btc/tests/harness/wallet/bitcoin.rs +++ b/xmr-btc/tests/harness/wallet/bitcoin.rs @@ -6,11 +6,9 @@ use bitcoin_harness::{bitcoind_rpc::PsbtBase64, Bitcoind}; use reqwest::Url; use std::time::Duration; use tokio::time; -use xmr_btc::{ - bitcoin::{ - BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxLock, WatchForRawTransaction, - }, - BlockHeight, TransactionBlockHeight, +use xmr_btc::bitcoin::{ + BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TransactionBlockHeight, + TxLock, WatchForRawTransaction, }; #[derive(Debug)] diff --git a/xmr-btc/tests/on_chain.rs b/xmr-btc/tests/on_chain.rs index f9e1bfcf..11aa4ebc 100644 --- a/xmr-btc/tests/on_chain.rs +++ b/xmr-btc/tests/on_chain.rs @@ -19,11 +19,10 @@ use rand::rngs::OsRng; use testcontainers::clients::Cli; use tracing::info; use xmr_btc::{ - action_generator_alice, action_generator_bob, alice, + alice::{self, action_generator_alice, ReceiveBitcoinRedeemEncsig}, bitcoin::{BroadcastSignedTransaction, EncryptedSignature, SignTxLock}, - bob, + bob::{self, ReceiveTransferProof}, monero::{CreateWalletForOutput, Transfer, TransferProof}, - AliceAction, BobAction, ReceiveBitcoinRedeemEncsig, ReceiveTransferProof, }; type AliceNetwork = Network; @@ -58,6 +57,26 @@ impl ReceiveBitcoinRedeemEncsig for AliceNetwork { } } +struct AliceBehaviour { + lock_xmr: bool, + redeem_btc: bool, + cancel_btc: bool, + punish_btc: bool, + create_monero_wallet_for_output: bool, +} + +impl Default for AliceBehaviour { + fn default() -> Self { + Self { + lock_xmr: true, + redeem_btc: true, + cancel_btc: true, + punish_btc: true, + create_monero_wallet_for_output: true, + } + } +} + async fn swap_as_alice( network: AliceNetwork, // FIXME: It would be more intuitive to have a single network/transport struct instead of @@ -65,6 +84,7 @@ async fn swap_as_alice( mut sender: Sender, monero_wallet: &harness::wallet::monero::Wallet, bitcoin_wallet: Arc, + behaviour: AliceBehaviour, state: alice::State3, ) -> Result<()> { let mut action_generator = action_generator_alice(network, bitcoin_wallet.clone(), state); @@ -72,32 +92,46 @@ async fn swap_as_alice( loop { let state = action_generator.async_resume().await; - info!("resumed execution of generator, got: {:?}", state); + info!("resumed execution of alice generator, got: {:?}", state); match state { - GeneratorState::Yielded(AliceAction::LockXmr { + GeneratorState::Yielded(alice::Action::LockXmr { amount, public_spend_key, public_view_key, }) => { - let (transfer_proof, _) = monero_wallet - .transfer(public_spend_key, public_view_key, amount) - .await?; + if behaviour.lock_xmr { + let (transfer_proof, _) = monero_wallet + .transfer(public_spend_key, public_view_key, amount) + .await?; - sender.send(transfer_proof).await.unwrap(); + sender.send(transfer_proof).await?; + } } - 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(alice::Action::RedeemBtc(tx)) => { + if behaviour.redeem_btc { + let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?; + } } - GeneratorState::Yielded(AliceAction::CreateMoneroWalletForOutput { + GeneratorState::Yielded(alice::Action::CancelBtc(tx)) => { + if behaviour.cancel_btc { + let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?; + } + } + GeneratorState::Yielded(alice::Action::PunishBtc(tx)) => { + if behaviour.punish_btc { + let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?; + } + } + GeneratorState::Yielded(alice::Action::CreateMoneroWalletForOutput { spend_key, view_key, }) => { - monero_wallet - .create_and_load_wallet_for_output(spend_key, view_key) - .await?; + if behaviour.create_monero_wallet_for_output { + monero_wallet + .create_and_load_wallet_for_output(spend_key, view_key) + .await?; + } } GeneratorState::Complete(()) => return Ok(()), } @@ -111,7 +145,7 @@ async fn swap_as_bob( bitcoin_wallet: Arc, state: bob::State2, ) -> Result<()> { - let mut action_generator = action_generator_bob( + let mut action_generator = bob::action_generator( network, monero_wallet.clone(), bitcoin_wallet.clone(), @@ -121,19 +155,19 @@ async fn swap_as_bob( loop { let state = action_generator.async_resume().await; - info!("resumed execution of generator, got: {:?}", state); + info!("resumed execution of bob generator, got: {:?}", state); match state { - GeneratorState::Yielded(BobAction::LockBitcoin(tx_lock)) => { + GeneratorState::Yielded(bob::Action::LockBtc(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(BobAction::SendBitcoinRedeemEncsig(tx_redeem_encsig)) => { + GeneratorState::Yielded(bob::Action::SendBtcRedeemEncsig(tx_redeem_encsig)) => { sender.send(tx_redeem_encsig).await.unwrap(); } - GeneratorState::Yielded(BobAction::CreateMoneroWalletForOutput { + GeneratorState::Yielded(bob::Action::CreateXmrWalletForOutput { spend_key, view_key, }) => { @@ -141,12 +175,12 @@ async fn swap_as_bob( .create_and_load_wallet_for_output(spend_key, view_key) .await?; } - GeneratorState::Yielded(BobAction::CancelBitcoin(tx_cancel)) => { + GeneratorState::Yielded(bob::Action::CancelBtc(tx_cancel)) => { let _ = bitcoin_wallet .broadcast_signed_transaction(tx_cancel) .await?; } - GeneratorState::Yielded(BobAction::RefundBitcoin(tx_refund)) => { + GeneratorState::Yielded(bob::Action::RefundBtc(tx_refund)) => { let _ = bitcoin_wallet .broadcast_signed_transaction(tx_refund) .await?; @@ -205,6 +239,7 @@ async fn on_chain_happy_path() { alice_sender, &alice_monero_wallet.clone(), alice_bitcoin_wallet.clone(), + AliceBehaviour::default(), alice, ), swap_as_bob( @@ -249,3 +284,92 @@ async fn on_chain_happy_path() { initial_balances.bob_xmr + swap_amounts.xmr ); } + +#[tokio::test] +async fn on_chain_both_refund_if_alice_never_redeems() { + let cli = Cli::default(); + let (monero, _container) = Monero::new(&cli).unwrap(); + 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(10), Some(10)).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(), + AliceBehaviour { + redeem_btc: false, + ..Default::default() + }, + 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(); + + monero.wait_for_alice_wallet_block_height().await.unwrap(); + let alice_final_xmr_balance = alice_monero_wallet.get_balance().await.unwrap(); + + let bob_final_xmr_balance = bob_monero_wallet.get_balance().await.unwrap(); + + assert_eq!(alice_final_btc_balance, initial_balances.alice_btc); + assert_eq!( + bob_final_btc_balance, + // The 2 * TX_FEE corresponds to tx_refund and tx_cancel. + initial_balances.bob_btc + - bitcoin::Amount::from_sat(2 * xmr_btc::bitcoin::TX_FEE) + - lock_tx_bitcoin_fee + ); + + // Because we create a new wallet when claiming Monero, we can only assert on + // this new wallet owning all of `xmr_amount` after refund + assert_eq!(alice_final_xmr_balance, swap_amounts.xmr); + assert_eq!(bob_final_xmr_balance, initial_balances.bob_xmr); +} From c86a82b31581d343906b6059f9d0d4c9fdd45426 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Mon, 26 Oct 2020 10:59:28 +1100 Subject: [PATCH 2/7] Rename action_generator_alice to action_generator --- xmr-btc/src/alice.rs | 2 +- xmr-btc/tests/on_chain.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index 9667c18f..3bb7a94d 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -58,7 +58,7 @@ 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( +pub fn action_generator( mut network: N, bitcoin_client: Arc, // TODO: Replace this with a new, slimmer struct? diff --git a/xmr-btc/tests/on_chain.rs b/xmr-btc/tests/on_chain.rs index 11aa4ebc..6ef6e28c 100644 --- a/xmr-btc/tests/on_chain.rs +++ b/xmr-btc/tests/on_chain.rs @@ -19,7 +19,7 @@ use rand::rngs::OsRng; use testcontainers::clients::Cli; use tracing::info; use xmr_btc::{ - alice::{self, action_generator_alice, ReceiveBitcoinRedeemEncsig}, + alice::{self, ReceiveBitcoinRedeemEncsig}, bitcoin::{BroadcastSignedTransaction, EncryptedSignature, SignTxLock}, bob::{self, ReceiveTransferProof}, monero::{CreateWalletForOutput, Transfer, TransferProof}, @@ -87,7 +87,7 @@ async fn swap_as_alice( behaviour: AliceBehaviour, state: alice::State3, ) -> Result<()> { - let mut action_generator = action_generator_alice(network, bitcoin_wallet.clone(), state); + let mut action_generator = alice::action_generator(network, bitcoin_wallet.clone(), state); loop { let state = action_generator.async_resume().await; From def3399d1c6d845c1e8150390930b33fe84fa056 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Mon, 26 Oct 2020 11:00:33 +1100 Subject: [PATCH 3/7] Use while instead of loop --- xmr-btc/src/bitcoin.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index 9ac38365..da75fd6f 100644 --- a/xmr-btc/src/bitcoin.rs +++ b/xmr-btc/src/bitcoin.rs @@ -215,11 +215,7 @@ pub async fn poll_until_block_height_is_gte(client: &B, target: u32) where B: BlockHeight, { - loop { - if client.block_height().await >= target { - return; - } - + while client.block_height().await < target { tokio::time::delay_for(std::time::Duration::from_secs(1)).await; } } From 41e8c7283c5d7299c77e37a76068807ae954f158 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Mon, 26 Oct 2020 11:27:35 +1100 Subject: [PATCH 4/7] Verify Bob's redeem encsig as Alice Not doing so means that receiving an invalid encrypted signature from Bob would make the generator produce a `RedeemBtc` action that should not be accepted by the node (since Bob's signature would be invalid after decrypting his encrypted signature). It's better to fail early and let the user know what went wrong, rather than let them hit an incomprehensible error when using their wallet. --- xmr-btc/src/alice.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index 3bb7a94d..72b5996c 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -99,6 +99,9 @@ where /// Reason why the swap has failed. #[derive(Debug)] enum Reason { + /// Bob's encrypted signature on the Bitcoin redeem transaction is + /// invalid. + InvalidEncryptedSignature, /// The refund timelock has been reached. BtcExpired, } @@ -169,6 +172,17 @@ where let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address); + bitcoin::verify_encsig( + B.clone(), + s_a.into_secp256k1().into(), + &tx_redeem.digest(), + &tx_redeem_encsig, + ) + .map_err(|_| SwapFailed::AfterXmrLock { + reason: Reason::InvalidEncryptedSignature, + tx_lock_height, + })?; + let sig_a = a.sign(tx_redeem.digest()); let sig_b = adaptor.decrypt_signature(&s_a.into_secp256k1(), tx_redeem_encsig.clone()); From aa2a20916e2a026b2d78ef852b3bd863aedb945d Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Mon, 26 Oct 2020 11:33:51 +1100 Subject: [PATCH 5/7] Include Bob being inactive as a reason for failure --- xmr-btc/src/alice.rs | 6 ++++-- xmr-btc/src/bob.rs | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index 72b5996c..50734c69 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -92,13 +92,15 @@ where { #[derive(Debug)] enum SwapFailed { - BeforeBtcLock, + BeforeBtcLock(Reason), AfterXmrLock { tx_lock_height: u32, reason: Reason }, } /// Reason why the swap has failed. #[derive(Debug)] enum Reason { + /// Bob was too slow to lock the bitcoin. + InactiveBob, /// Bob's encrypted signature on the Bitcoin redeem transaction is /// invalid. InvalidEncryptedSignature, @@ -126,7 +128,7 @@ where bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), ) .await - .map_err(|_| SwapFailed::BeforeBtcLock)?; + .map_err(|_| SwapFailed::BeforeBtcLock(Reason::InactiveBob))?; let tx_lock_height = bitcoin_client .transaction_block_height(tx_lock.txid()) diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 5b1c1667..8751e9c0 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -97,7 +97,7 @@ where { #[derive(Debug)] enum SwapFailed { - BeforeBtcLock, + BeforeBtcLock(Reason), AfterBtcLock(Reason), AfterBtcRedeem(Reason), } @@ -105,6 +105,8 @@ where /// Reason why the swap has failed. #[derive(Debug)] enum Reason { + /// Bob was too slow to lock the bitcoin. + InactiveBob, /// The refund timelock has been reached. BtcExpired, /// Alice did not lock up enough monero in the shared output. @@ -127,7 +129,7 @@ where ) .await .map(|tx| tx.txid()) - .map_err(|_| SwapFailed::BeforeBtcLock)?; + .map_err(|_| SwapFailed::BeforeBtcLock(Reason::InactiveBob))?; let tx_lock_height = bitcoin_client .transaction_block_height(tx_lock.txid()) From 1d21ae7e7abda35f1f4004f6d2313f0e4bd7746e Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Mon, 26 Oct 2020 11:36:52 +1100 Subject: [PATCH 6/7] Use pin_mut! instead of futures::pin_mut! --- xmr-btc/src/alice.rs | 6 +++--- xmr-btc/src/bob.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index 50734c69..06d0bca9 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -13,7 +13,7 @@ use ecdsa_fun::{ }; use futures::{ future::{select, Either}, - FutureExt, + pin_mut, FutureExt, }; use genawaiter::sync::{Gen, GenBoxed}; use rand::{CryptoRng, RngCore}; @@ -138,7 +138,7 @@ where tx_lock_height + refund_timelock, ) .shared(); - futures::pin_mut!(poll_until_btc_has_expired); + pin_mut!(poll_until_btc_has_expired); let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: s_a.into_ed25519(), @@ -233,7 +233,7 @@ where tx_lock_height + refund_timelock + punish_timelock, ) .shared(); - futures::pin_mut!(poll_until_bob_can_be_punished); + pin_mut!(poll_until_bob_can_be_punished); let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone()); diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 8751e9c0..9b6efb36 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -17,7 +17,7 @@ use ecdsa_fun::{ }; use futures::{ future::{select, Either}, - FutureExt, + pin_mut, FutureExt, }; use genawaiter::sync::{Gen, GenBoxed}; use rand::{CryptoRng, RngCore}; @@ -139,7 +139,7 @@ where tx_lock_height + refund_timelock, ) .shared(); - futures::pin_mut!(poll_until_btc_has_expired); + pin_mut!(poll_until_btc_has_expired); let transfer_proof = match select( network.receive_transfer_proof(), From cea1af1e1aa01dfe1b5421eb24c1ed2a28425bda Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Mon, 26 Oct 2020 12:03:14 +1100 Subject: [PATCH 7/7] Take bitcoin_tx_lock_timeout as argument to action generators --- xmr-btc/src/alice.rs | 6 +++++- xmr-btc/src/bob.rs | 11 +++++------ xmr-btc/tests/on_chain.rs | 11 ++++++++++- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index 06d0bca9..95e12ed6 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -58,6 +58,9 @@ pub trait ReceiveBitcoinRedeemEncsig { /// /// This is called post handshake, after all the keys, addresses and most of the /// signatures have been exchanged. +/// +/// The argument `bitcoin_tx_lock_timeout` is used to determine how long we will +/// wait for Bob, the counterparty, to lock up the bitcoin. pub fn action_generator( mut network: N, bitcoin_client: Arc, @@ -80,6 +83,7 @@ pub fn action_generator( tx_cancel_sig_bob, .. }: State3, + bitcoin_tx_lock_timeout: u64, ) -> GenBoxed where N: ReceiveBitcoinRedeemEncsig + Send + Sync + 'static, @@ -124,7 +128,7 @@ where Gen::new_boxed(|co| async move { let swap_result: Result<(), SwapFailed> = async { timeout( - Duration::from_secs(bob::SECS_TO_ACT), + Duration::from_secs(bitcoin_tx_lock_timeout), bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), ) .await diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 9b6efb36..ac1180c9 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -35,11 +35,6 @@ pub mod message; use crate::monero::{CreateWalletForOutput, WatchForTransfer}; pub use message::{Message, Message0, Message1, Message2, Message3}; -// 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. -pub const SECS_TO_ACT: u64 = 60; - #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum Action { @@ -63,6 +58,9 @@ pub trait ReceiveTransferProof { /// /// This is called post handshake, after all the keys, addresses and most of the /// signatures have been exchanged. +/// +/// The argument `bitcoin_tx_lock_timeout` is used to determine how long we will +/// wait for Bob, the caller of this function, to lock up the bitcoin. pub fn action_generator( mut network: N, monero_client: Arc, @@ -84,6 +82,7 @@ pub fn action_generator( tx_refund_encsig, .. }: State2, + bitcoin_tx_lock_timeout: u64, ) -> GenBoxed where N: ReceiveTransferProof + Send + Sync + 'static, @@ -124,7 +123,7 @@ where co.yield_(Action::LockBtc(tx_lock.clone())).await; timeout( - Duration::from_secs(SECS_TO_ACT), + Duration::from_secs(bitcoin_tx_lock_timeout), bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), ) .await diff --git a/xmr-btc/tests/on_chain.rs b/xmr-btc/tests/on_chain.rs index 6ef6e28c..8cac4c57 100644 --- a/xmr-btc/tests/on_chain.rs +++ b/xmr-btc/tests/on_chain.rs @@ -25,6 +25,9 @@ use xmr_btc::{ monero::{CreateWalletForOutput, Transfer, TransferProof}, }; +/// Time given to Bob to get the Bitcoin lock transaction included in a block. +const BITCOIN_TX_LOCK_TIMEOUT: u64 = 5; + type AliceNetwork = Network; type BobNetwork = Network; @@ -87,7 +90,12 @@ async fn swap_as_alice( behaviour: AliceBehaviour, state: alice::State3, ) -> Result<()> { - let mut action_generator = alice::action_generator(network, bitcoin_wallet.clone(), state); + let mut action_generator = alice::action_generator( + network, + bitcoin_wallet.clone(), + state, + BITCOIN_TX_LOCK_TIMEOUT, + ); loop { let state = action_generator.async_resume().await; @@ -150,6 +158,7 @@ async fn swap_as_bob( monero_wallet.clone(), bitcoin_wallet.clone(), state, + BITCOIN_TX_LOCK_TIMEOUT, ); loop {