diff --git a/Cargo.lock b/Cargo.lock index 1ce88b3e..e406c0ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,17 @@ dependencies = [ "syn 1.0.48", ] +[[package]] +name = "async-recursion" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5444eec77a9ec2bfe4524139e09195862e981400c4358d3b760cae634e4c4ee" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.48", +] + [[package]] name = "async-trait" version = "0.1.41" @@ -261,6 +272,26 @@ dependencies = [ "url", ] +[[package]] +name = "bitcoin-harness" +version = "0.1.0" +source = "git+https://github.com/d4nte/bitcoin-harness-rs?branch=access-wallet-client#2ceccaa89bd994c2bc49e23a11cd8af647afc31b" +dependencies = [ + "base64 0.12.3", + "bitcoin", + "bitcoincore-rpc-json", + "futures", + "hex 0.4.2", + "reqwest", + "serde", + "serde_json", + "testcontainers", + "thiserror", + "tokio", + "tracing", + "url", +] + [[package]] name = "bitcoin_hashes" version = "0.7.6" @@ -534,6 +565,21 @@ dependencies = [ "bitflags", ] +[[package]] +name = "conquer-once" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cb45322099323eefa1b48850ce6c148f5b510894c531e038539f6370c887214" +dependencies = [ + "conquer-util", +] + +[[package]] +name = "conquer-util" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e763eef8846b13b380f37dfecda401770b0ca4e56e95170237bd7c25c7db3582" + [[package]] name = "const_fn" version = "0.4.3" @@ -1975,10 +2021,8 @@ name = "monero-harness" version = "0.1.0" dependencies = [ "anyhow", - "curve25519-dalek 2.1.0", "digest_auth", "futures", - "monero", "port_check", "rand 0.7.3", "reqwest", @@ -3339,12 +3383,14 @@ name = "swap" version = "0.1.0" dependencies = [ "anyhow", + "async-recursion", "async-trait", "atty", "backoff", "base64 0.12.3", "bitcoin", - "bitcoin-harness", + "bitcoin-harness 0.1.0 (git+https://github.com/d4nte/bitcoin-harness-rs?branch=access-wallet-client)", + "conquer-once", "derivative", "ecdsa_fun", "futures", @@ -4121,7 +4167,8 @@ dependencies = [ "backoff", "base64 0.12.3", "bitcoin", - "bitcoin-harness", + "bitcoin-harness 0.1.0 (git+https://github.com/coblox/bitcoin-harness-rs?rev=3be644cd9512c157d3337a189298b8257ed54d04)", + "conquer-once", "cross-curve-dleq", "curve25519-dalek 2.1.0", "ecdsa_fun", diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 5d606654..d71488e5 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -13,7 +13,7 @@ atty = "0.2" 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 = "3be644cd9512c157d3337a189298b8257ed54d04" } +bitcoin-harness = { git = "https://github.com/d4nte/bitcoin-harness-rs", branch = "access-wallet-client" } conquer-once = "0.3" derivative = "2" ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat", "serde", "serialization"] } diff --git a/swap/src/alice/execution.rs b/swap/src/alice/execution.rs index 0350eeb3..cd7115f2 100644 --- a/swap/src/alice/execution.rs +++ b/swap/src/alice/execution.rs @@ -15,6 +15,7 @@ use libp2p::request_response::ResponseChannel; use sha2::Sha256; use std::{sync::Arc, time::Duration}; use tokio::time::timeout; +use tracing::trace; use xmr_btc::{ alice, alice::{State0, State3}, @@ -23,13 +24,11 @@ use xmr_btc::{ EncryptedSignature, GetRawTransaction, TransactionBlockHeight, TxCancel, TxLock, TxRefund, WaitForTransactionFinality, WatchForRawTransaction, }, + config::Config, cross_curve_dleq, monero::Transfer, }; -// For each step, we are giving Bob 10 minutes to act. -static BOB_TIME_TO_ACT: Lazy = Lazy::new(|| Duration::from_secs(10 * 60)); - // The maximum we assume we need to wait from the moment the monero transaction // is mined to the moment it reaches finality. We set 15 confirmations for now // (based on Kraken). 1.5 multiplier in case the blockchain is slower than @@ -44,8 +43,10 @@ pub async fn negotiate( v_a: monero::PrivateViewKey, swarm: &mut Swarm, bitcoin_wallet: Arc, + config: Config, ) -> Result<(ResponseChannel, State3)> { - let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) + trace!("Starting negotiate"); + let event = timeout(config.bob_time_to_act, swarm.next()) .await .context("Failed to receive dial connection from Bob")?; match event { @@ -53,7 +54,7 @@ pub async fn negotiate( other => bail!("Unexpected event received: {:?}", other), } - let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) + let event = timeout(config.bob_time_to_act, swarm.next()) .await .context("Failed to receive amounts from Bob")?; let (btc, channel) = match event { @@ -89,7 +90,7 @@ pub async fn negotiate( // TODO(Franck): Understand why this is needed. swarm.set_state0(state0.clone()); - let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) + let event = timeout(config.bob_time_to_act, swarm.next()) .await .context("Failed to receive message 0 from Bob")?; let message0 = match event { @@ -99,7 +100,7 @@ pub async fn negotiate( let state1 = state0.receive(message0)?; - let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) + let event = timeout(config.bob_time_to_act, swarm.next()) .await .context("Failed to receive message 1 from Bob")?; let (msg, channel) = match event { @@ -112,7 +113,7 @@ pub async fn negotiate( let message1 = state2.next_message(); swarm.send_message1(channel, message1); - let event = timeout(*BOB_TIME_TO_ACT, swarm.next()) + let event = timeout(config.bob_time_to_act, swarm.next()) .await .context("Failed to receive message 2 from Bob")?; let (msg, channel) = match event { @@ -128,13 +129,14 @@ pub async fn negotiate( pub async fn wait_for_locked_bitcoin( lock_bitcoin_txid: bitcoin::Txid, bitcoin_wallet: Arc, + config: Config, ) -> Result<()> where W: WatchForRawTransaction + WaitForTransactionFinality, { // We assume we will see Bob's transaction in the mempool first. timeout( - *BOB_TIME_TO_ACT, + config.bob_time_to_act, bitcoin_wallet.watch_for_raw_transaction(lock_bitcoin_txid), ) .await @@ -142,7 +144,7 @@ where // // We saw the transaction in the mempool, waiting for it to be confirmed. // bitcoin_wallet - // .wait_for_transaction_finality(lock_bitcoin_txid) + // .wait_for_transaction_finality(lock_bitcoin_txid, config) // .await; Ok(()) @@ -225,17 +227,18 @@ pub fn build_bitcoin_redeem_transaction( pub async fn publish_bitcoin_redeem_transaction( redeem_tx: bitcoin::Transaction, bitcoin_wallet: Arc, + config: Config, ) -> Result<()> where W: BroadcastSignedTransaction + WaitForTransactionFinality, { - let _tx_id = bitcoin_wallet + let tx_id = bitcoin_wallet .broadcast_signed_transaction(redeem_tx) .await?; - // // TODO(Franck): Not sure if we wait for finality here or just mined - // bitcoin_wallet.wait_for_transaction_finality(tx_id).await; - Ok(()) + bitcoin_wallet + .wait_for_transaction_finality(tx_id, config) + .await } pub async fn publish_cancel_transaction( @@ -364,6 +367,7 @@ pub fn build_bitcoin_punish_transaction( pub async fn publish_bitcoin_punish_transaction( punish_tx: bitcoin::Transaction, bitcoin_wallet: Arc, + config: Config, ) -> Result where W: BroadcastSignedTransaction + WaitForTransactionFinality, @@ -372,8 +376,9 @@ where .broadcast_signed_transaction(punish_tx) .await?; - // todo: enable this once trait is implemented - // bitcoin_wallet.wait_for_transaction_finality(txid).await; + bitcoin_wallet + .wait_for_transaction_finality(txid, config) + .await?; Ok(txid) } diff --git a/swap/src/alice/swap.rs b/swap/src/alice/swap.rs index a6330205..3bcb255f 100644 --- a/swap/src/alice/swap.rs +++ b/swap/src/alice/swap.rs @@ -28,6 +28,7 @@ use std::sync::Arc; use xmr_btc::{ alice::State3, bitcoin::{TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction}, + config::Config, cross_curve_dleq, monero::CreateWalletForOutput, }; @@ -92,6 +93,7 @@ pub async fn swap( mut swarm: Swarm, bitcoin_wallet: Arc, monero_wallet: Arc, + config: Config, ) -> Result { match state { AliceState::Started { @@ -100,8 +102,16 @@ pub async fn swap( s_a, v_a, } => { - let (channel, state3) = - negotiate(amounts, a, s_a, v_a, &mut swarm, bitcoin_wallet.clone()).await?; + let (channel, state3) = negotiate( + amounts, + a, + s_a, + v_a, + &mut swarm, + bitcoin_wallet.clone(), + config, + ) + .await?; swap( AliceState::Negotiated { @@ -112,6 +122,7 @@ pub async fn swap( swarm, bitcoin_wallet, monero_wallet, + config, ) .await } @@ -120,7 +131,8 @@ pub async fn swap( channel, amounts, } => { - let _ = wait_for_locked_bitcoin(state3.tx_lock.txid(), bitcoin_wallet.clone()).await?; + let _ = wait_for_locked_bitcoin(state3.tx_lock.txid(), bitcoin_wallet.clone(), config) + .await?; swap( AliceState::BtcLocked { @@ -131,6 +143,7 @@ pub async fn swap( swarm, bitcoin_wallet, monero_wallet, + config, ) .await } @@ -153,6 +166,7 @@ pub async fn swap( swarm, bitcoin_wallet, monero_wallet, + config, ) .await } @@ -169,6 +183,7 @@ pub async fn swap( swarm, bitcoin_wallet, monero_wallet, + config, ) .await } @@ -178,6 +193,7 @@ pub async fn swap( swarm, bitcoin_wallet, monero_wallet, + config, ) .await } @@ -202,6 +218,7 @@ pub async fn swap( swarm, bitcoin_wallet, monero_wallet, + config, ) .await; } @@ -210,13 +227,15 @@ pub async fn swap( // TODO(Franck): Error handling is delicate here. // If Bob sees this transaction he can redeem Monero // e.g. If the Bitcoin node is down then the user needs to take action. - publish_bitcoin_redeem_transaction(signed_tx_redeem, bitcoin_wallet.clone()).await?; + publish_bitcoin_redeem_transaction(signed_tx_redeem, bitcoin_wallet.clone(), config) + .await?; swap( AliceState::BtcRedeemed, swarm, bitcoin_wallet, monero_wallet, + config, ) .await } @@ -236,6 +255,7 @@ pub async fn swap( swarm, bitcoin_wallet, monero_wallet, + config, ) .await } @@ -261,6 +281,7 @@ pub async fn swap( swarm, bitcoin_wallet.clone(), monero_wallet, + config, ) .await } @@ -274,6 +295,7 @@ pub async fn swap( swarm, bitcoin_wallet.clone(), monero_wallet, + config, ) .await } @@ -310,8 +332,11 @@ pub async fn swap( state3.B.clone(), )?; - let punish_tx_finalised = - publish_bitcoin_punish_transaction(signed_tx_punish, bitcoin_wallet.clone()); + let punish_tx_finalised = publish_bitcoin_punish_transaction( + signed_tx_punish, + bitcoin_wallet.clone(), + config, + ); let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); @@ -325,6 +350,7 @@ pub async fn swap( swarm, bitcoin_wallet.clone(), monero_wallet, + config, ) .await } @@ -338,6 +364,7 @@ pub async fn swap( swarm, bitcoin_wallet.clone(), monero_wallet, + config, ) .await } diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 8425fb85..bcef2462 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -6,9 +6,13 @@ use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _ use bitcoin::util::psbt::PartiallySignedTransaction; use bitcoin_harness::bitcoind_rpc::PsbtBase64; use reqwest::Url; -use xmr_btc::bitcoin::{ - BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TransactionBlockHeight, - WatchForRawTransaction, +use tokio::time::interval; +use xmr_btc::{ + bitcoin::{ + BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, + TransactionBlockHeight, WatchForRawTransaction, + }, + config::Config, }; pub use ::bitcoin::{Address, Transaction}; @@ -148,7 +152,23 @@ impl TransactionBlockHeight for Wallet { #[async_trait] impl WaitForTransactionFinality for Wallet { - async fn wait_for_transaction_finality(&self, _txid: Txid) { - todo!() + async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()> { + // TODO(Franck): This assumes that bitcoind runs with txindex=1 + + // Divide by 4 to not check too often yet still be aware of the new block early + // on. + let mut interval = interval(config.bitcoin_avg_block_time / 4); + + loop { + let tx = self.0.client.get_raw_transaction_verbose(txid).await?; + if let Some(confirmations) = tx.confirmations { + if confirmations >= config.bitcoin_finality_confirmations { + break; + } + } + interval.tick().await; + } + + Ok(()) } } diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs index 442fba5f..5807a913 100644 --- a/swap/tests/e2e.rs +++ b/swap/tests/e2e.rs @@ -202,11 +202,13 @@ async fn happy_path_recursive_executor() { }; let alice_swarm = alice::new_swarm(alice_multiaddr.clone(), alice_transport, alice_behaviour).unwrap(); + let config = xmr_btc::config::Config::regtest(); let alice_swap = alice::swap::swap( alice_state, alice_swarm, alice_btc_wallet.clone(), alice_xmr_wallet.clone(), + config, ); let bob_db_dir = tempdir().unwrap(); diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index e95bc5a7..66430caa 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -10,6 +10,7 @@ edition = "2018" anyhow = "1" async-trait = "0.1" bitcoin = { version = "0.23", features = ["rand", "serde"] } +conquer-once = "0.3" cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "a19608734da1e8803cb4c806022483df4e7d5588", features = ["serde"] } curve25519-dalek = "2" ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat", "serde", "serialization"] } diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index 1cbb49f5..531420dc 100644 --- a/xmr-btc/src/bitcoin.rs +++ b/xmr-btc/src/bitcoin.rs @@ -1,5 +1,6 @@ pub mod transactions; +use crate::config::Config; use anyhow::{anyhow, bail, Result}; use async_trait::async_trait; use bitcoin::hashes::{hex::ToHex, Hash}; @@ -188,7 +189,7 @@ pub trait WatchForRawTransaction { #[async_trait] pub trait WaitForTransactionFinality { - async fn wait_for_transaction_finality(&self, txid: Txid); + async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()>; } #[async_trait] diff --git a/xmr-btc/src/config.rs b/xmr-btc/src/config.rs new file mode 100644 index 00000000..0a635e26 --- /dev/null +++ b/xmr-btc/src/config.rs @@ -0,0 +1,49 @@ +use conquer_once::Lazy; +use std::time::Duration; + +#[derive(Debug, Copy, Clone)] +pub struct Config { + pub bob_time_to_act: Duration, + pub bitcoin_finality_confirmations: u32, + pub bitcoin_avg_block_time: Duration, +} + +impl Config { + pub fn mainnet() -> Self { + Self { + bob_time_to_act: *mainnet::BOB_TIME_TO_ACT, + bitcoin_finality_confirmations: mainnet::BITCOIN_FINALITY_CONFIRMATIONS, + bitcoin_avg_block_time: *mainnet::BITCOIN_AVG_BLOCK_TIME, + } + } + + pub fn regtest() -> Self { + Self { + bob_time_to_act: *regtest::BOB_TIME_TO_ACT, + bitcoin_finality_confirmations: regtest::BITCOIN_FINALITY_CONFIRMATIONS, + bitcoin_avg_block_time: *regtest::BITCOIN_AVG_BLOCK_TIME, + } + } +} + +mod mainnet { + use super::*; + + // For each step, we are giving Bob 10 minutes to act. + pub static BOB_TIME_TO_ACT: Lazy = Lazy::new(|| Duration::from_secs(10 * 60)); + + pub static BITCOIN_FINALITY_CONFIRMATIONS: u32 = 3; + + pub static BITCOIN_AVG_BLOCK_TIME: Lazy = Lazy::new(|| Duration::from_secs(10 * 60)); +} + +mod regtest { + use super::*; + + // In test, set to 5 seconds to fail fast + pub static BOB_TIME_TO_ACT: Lazy = Lazy::new(|| Duration::from_secs(5)); + + pub static BITCOIN_FINALITY_CONFIRMATIONS: u32 = 1; + + pub static BITCOIN_AVG_BLOCK_TIME: Lazy = Lazy::new(|| Duration::from_secs(1)); +} diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index cea64d88..7d200891 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -48,6 +48,7 @@ mod utils { pub mod alice; pub mod bitcoin; pub mod bob; +pub mod config; pub mod monero; pub mod serde; pub mod transport;