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] 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") + } +}