From 752e5be8f34ef5eb5a64798baadc3d7ce0a64bd8 Mon Sep 17 00:00:00 2001 From: rishflab Date: Fri, 12 Mar 2021 16:26:23 +1100 Subject: [PATCH 01/11] Cleanup test logging --- swap/tests/testutils/mod.rs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/swap/tests/testutils/mod.rs b/swap/tests/testutils/mod.rs index 58efebe3..9d14bf6a 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -678,26 +678,9 @@ pub fn init_tracing() -> DefaultGuard { // trouble when running multiple tests. let _ = LogTracer::init(); - let global_filter = tracing::Level::WARN; - let swap_filter = tracing::Level::DEBUG; - let xmr_btc_filter = tracing::Level::DEBUG; - let monero_rpc_filter = tracing::Level::DEBUG; - let monero_harness_filter = tracing::Level::DEBUG; - let bitcoin_harness_filter = tracing::Level::INFO; - let testcontainers_filter = tracing::Level::DEBUG; - use tracing_subscriber::util::SubscriberInitExt as _; tracing_subscriber::fmt() - .with_env_filter(format!( - "{},swap={},xmr_btc={},monero_harness={},monero_rpc={},bitcoin_harness={},testcontainers={}", - global_filter, - swap_filter, - xmr_btc_filter, - monero_harness_filter, - monero_rpc_filter, - bitcoin_harness_filter, - testcontainers_filter - )) + .with_env_filter("warn,swap=debug,monero_harness=debug,monero_rpc=info,bitcoin_harness=info,testcontainers=info") .set_default() } From 458a8d594a47f0d2d4109ea1c746768606b4bad2 Mon Sep 17 00:00:00 2001 From: rishflab Date: Thu, 11 Mar 2021 16:58:46 +1100 Subject: [PATCH 02/11] Rename fn param to correctly reflect underlying type --- swap/src/bitcoin.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 03966199..b527ed9b 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -210,10 +210,10 @@ pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Resu } pub async fn poll_until_block_height_is_gte( - client: &crate::bitcoin::Wallet, + wallet: &crate::bitcoin::Wallet, target: BlockHeight, ) -> Result<()> { - while client.get_block_height().await? < target { + while wallet.get_block_height().await? < target { tokio::time::sleep(std::time::Duration::from_secs(1)).await; } Ok(()) From 6a3e4802f1a2b0a72646499b485c3841532dbbb6 Mon Sep 17 00:00:00 2001 From: rishflab Date: Thu, 11 Mar 2021 17:53:40 +1100 Subject: [PATCH 03/11] Remove redundant reference --- swap/src/protocol/alice/steps.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swap/src/protocol/alice/steps.rs b/swap/src/protocol/alice/steps.rs index a0e619c8..f50b6e22 100644 --- a/swap/src/protocol/alice/steps.rs +++ b/swap/src/protocol/alice/steps.rs @@ -123,7 +123,7 @@ pub async fn publish_cancel_transaction( let tx_lock_height = bitcoin_wallet .transaction_block_height(tx_lock.txid()) .await?; - poll_until_block_height_is_gte(&bitcoin_wallet, tx_lock_height + cancel_timelock).await?; + poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + cancel_timelock).await?; let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B); From 21429f24b24af313a64e7a2b290727a63958a8c6 Mon Sep 17 00:00:00 2001 From: rishflab Date: Thu, 11 Mar 2021 19:09:35 +1100 Subject: [PATCH 04/11] Inline wait_for_locked_bitcoin() that is only called once Reduce indirection. --- swap/src/protocol/alice/steps.rs | 25 ------------------------- swap/src/protocol/alice/swap.rs | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 32 deletions(-) diff --git a/swap/src/protocol/alice/steps.rs b/swap/src/protocol/alice/steps.rs index f50b6e22..ec959ac7 100644 --- a/swap/src/protocol/alice/steps.rs +++ b/swap/src/protocol/alice/steps.rs @@ -2,7 +2,6 @@ use crate::bitcoin::{ poll_until_block_height_is_gte, BlockHeight, CancelTimelock, EncryptedSignature, PunishTimelock, TxCancel, TxLock, TxRefund, }; -use crate::execution_params::ExecutionParams; use crate::protocol::alice; use crate::protocol::alice::event_loop::EventLoopHandle; use crate::protocol::alice::TransferProof; @@ -14,30 +13,6 @@ use futures::future::{select, Either}; use futures::pin_mut; use libp2p::PeerId; use sha2::Sha256; -use tokio::time::timeout; - -// TODO(Franck): Use helper functions from xmr-btc instead of re-writing them -// here -pub async fn wait_for_locked_bitcoin( - lock_bitcoin_txid: bitcoin::Txid, - bitcoin_wallet: &bitcoin::Wallet, - execution_params: ExecutionParams, -) -> Result<()> { - // We assume we will see Bob's transaction in the mempool first. - timeout( - execution_params.bob_time_to_act, - bitcoin_wallet.watch_for_raw_transaction(lock_bitcoin_txid), - ) - .await - .context("Failed to find lock Bitcoin tx")??; - - // // We saw the transaction in the mempool, waiting for it to be confirmed. - bitcoin_wallet - .wait_for_transaction_finality(lock_bitcoin_txid, execution_params) - .await?; - - Ok(()) -} pub async fn lock_xmr( bob_peer_id: PeerId, diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 28f3d751..93064260 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -9,16 +9,17 @@ use crate::protocol::alice::event_loop::EventLoopHandle; use crate::protocol::alice::steps::{ build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction, extract_monero_private_key, lock_xmr, publish_cancel_transaction, wait_for_bitcoin_encrypted_signature, - wait_for_bitcoin_refund, wait_for_locked_bitcoin, + wait_for_bitcoin_refund, }; use crate::protocol::alice::AliceState; use crate::{bitcoin, database, monero}; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use async_recursion::async_recursion; use futures::future::{select, Either}; use futures::pin_mut; use rand::{CryptoRng, RngCore}; use std::sync::Arc; +use tokio::time::timeout; use tracing::{error, info}; use uuid::Uuid; @@ -80,12 +81,16 @@ async fn run_until_internal( state3, bob_peer_id, } => { - let _ = wait_for_locked_bitcoin( - state3.tx_lock.txid(), - &bitcoin_wallet, - execution_params, + timeout( + execution_params.bob_time_to_act, + bitcoin_wallet.watch_for_raw_transaction(state3.tx_lock.txid()), ) - .await?; + .await + .context("Failed to find lock Bitcoin tx")??; + + bitcoin_wallet + .wait_for_transaction_finality(state3.tx_lock.txid(), execution_params) + .await?; let state = AliceState::BtcLocked { bob_peer_id, From dd6c66a594acb36ef07d6bb2efa2cdfdd717be11 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Fri, 12 Mar 2021 11:01:14 +1100 Subject: [PATCH 05/11] Move completing of Bitcoin redeem tx onto RedeemTx This allows us to have access to RedeemTx from within the scope of the state transition which we are going to need for more efficient watching of what happens to this TX on the blockchain. --- swap/src/bitcoin/redeem.rs | 38 +++++++++++++++++++++++--------- swap/src/protocol/alice/steps.rs | 33 --------------------------- swap/src/protocol/alice/swap.rs | 13 +++++------ 3 files changed, 34 insertions(+), 50 deletions(-) diff --git a/swap/src/bitcoin/redeem.rs b/swap/src/bitcoin/redeem.rs index bc670da4..0a7888c5 100644 --- a/swap/src/bitcoin/redeem.rs +++ b/swap/src/bitcoin/redeem.rs @@ -1,12 +1,16 @@ use crate::bitcoin::{ - verify_sig, Address, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs, - Transaction, TxLock, + verify_encsig, verify_sig, Address, EmptyWitnessStack, EncryptedSignature, NoInputs, + NotThreeWitnesses, PublicKey, SecretKey, TooManyInputs, Transaction, TxLock, }; use ::bitcoin::util::bip143::SigHashCache; use ::bitcoin::{SigHash, SigHashType, Txid}; use anyhow::{bail, Context, Result}; +use ecdsa_fun::adaptor::{Adaptor, HashTranscript}; +use ecdsa_fun::fun::Scalar; +use ecdsa_fun::nonce::Deterministic; use ecdsa_fun::Signature; use miniscript::{Descriptor, DescriptorTrait}; +use sha2::Sha256; use std::collections::HashMap; #[derive(Debug, Clone)] @@ -44,17 +48,31 @@ impl TxRedeem { self.digest } - pub fn add_signatures( - self, - (A, sig_a): (PublicKey, Signature), - (B, sig_b): (PublicKey, Signature), + pub fn complete( + mut self, + encrypted_signature: EncryptedSignature, + a: SecretKey, + s_a: Scalar, + B: PublicKey, ) -> Result { + verify_encsig( + B, + PublicKey::from(s_a.clone()), + &self.digest(), + &encrypted_signature, + ) + .context("Invalid encrypted signature received")?; + + let sig_a = a.sign(self.digest()); + let adaptor = Adaptor::, Deterministic>::default(); + let sig_b = adaptor.decrypt_signature(&s_a, encrypted_signature); + let satisfier = { let mut satisfier = HashMap::with_capacity(2); let A = ::bitcoin::PublicKey { compressed: true, - key: A.0.into(), + key: a.public.into(), }; let B = ::bitcoin::PublicKey { compressed: true, @@ -68,11 +86,11 @@ impl TxRedeem { satisfier }; - let mut tx_redeem = self.inner; self.lock_output_descriptor - .satisfy(&mut tx_redeem.input[0], satisfier)?; + .satisfy(&mut self.inner.input[0], satisfier) + .context("Failed to sign Bitcoin redeem transaction")?; - Ok(tx_redeem) + Ok(self.inner) } pub fn extract_signature_by_key( diff --git a/swap/src/protocol/alice/steps.rs b/swap/src/protocol/alice/steps.rs index ec959ac7..b44f1e61 100644 --- a/swap/src/protocol/alice/steps.rs +++ b/swap/src/protocol/alice/steps.rs @@ -7,12 +7,9 @@ use crate::protocol::alice::event_loop::EventLoopHandle; use crate::protocol::alice::TransferProof; use crate::{bitcoin, monero}; use anyhow::{Context, Result}; -use ecdsa_fun::adaptor::{Adaptor, HashTranscript}; -use ecdsa_fun::nonce::Deterministic; use futures::future::{select, Either}; use futures::pin_mut; use libp2p::PeerId; -use sha2::Sha256; pub async fn lock_xmr( bob_peer_id: PeerId, @@ -56,36 +53,6 @@ pub async fn wait_for_bitcoin_encrypted_signature( Ok(msg3.tx_redeem_encsig) } -pub fn build_bitcoin_redeem_transaction( - encrypted_signature: EncryptedSignature, - tx_lock: &TxLock, - a: bitcoin::SecretKey, - s_a: ecdsa_fun::fun::Scalar, - B: bitcoin::PublicKey, - redeem_address: &bitcoin::Address, -) -> Result { - let adaptor = Adaptor::, Deterministic>::default(); - - let tx_redeem = bitcoin::TxRedeem::new(tx_lock, redeem_address); - - bitcoin::verify_encsig( - B, - bitcoin::PublicKey::from(s_a.clone()), - &tx_redeem.digest(), - &encrypted_signature, - ) - .context("Invalid encrypted signature received")?; - - let sig_a = a.sign(tx_redeem.digest()); - let sig_b = adaptor.decrypt_signature(&s_a, encrypted_signature); - - let tx = tx_redeem - .add_signatures((a.public(), sig_a), (B, sig_b)) - .context("Failed to sign Bitcoin redeem transaction")?; - - Ok(tx) -} - pub async fn publish_cancel_transaction( tx_lock: TxLock, a: bitcoin::SecretKey, diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 93064260..12a72fbc 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -1,15 +1,14 @@ //! Run an XMR/BTC swap in the role of Alice. //! Alice holds XMR and wishes receive BTC. -use crate::bitcoin::ExpiredTimelocks; +use crate::bitcoin::{ExpiredTimelocks, TxRedeem}; use crate::database::Database; use crate::execution_params::ExecutionParams; use crate::monero_ext::ScalarExt; use crate::protocol::alice; use crate::protocol::alice::event_loop::EventLoopHandle; use crate::protocol::alice::steps::{ - build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction, extract_monero_private_key, - lock_xmr, publish_cancel_transaction, wait_for_bitcoin_encrypted_signature, - wait_for_bitcoin_refund, + build_bitcoin_punish_transaction, extract_monero_private_key, lock_xmr, + publish_cancel_transaction, wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, }; use crate::protocol::alice::AliceState; use crate::{bitcoin, database, monero}; @@ -203,13 +202,13 @@ async fn run_until_internal( } => { let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { ExpiredTimelocks::None => { - match build_bitcoin_redeem_transaction( + let tx_redeem = TxRedeem::new(&state3.tx_lock, &state3.redeem_address); + + match tx_redeem.complete( *encrypted_signature, - &state3.tx_lock, state3.a.clone(), state3.s_a.to_secpfun_scalar(), state3.B, - &state3.redeem_address, ) { Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await { Ok(txid) => { From 6beb732e35f7ca7ca4a43157c974b4b9412ffd27 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 16 Mar 2021 18:02:31 +1100 Subject: [PATCH 06/11] Eliminate `build_bitcoin_punish_transaction` We reduce indirection by constructing TxPunish directly based off `State3` and make the type itself more powerful by moving the logic of completing it with a signature onto it. --- swap/src/bitcoin.rs | 9 +++++++++ swap/src/bitcoin/punish.rs | 30 ++++++++++++++---------------- swap/src/protocol/alice/state.rs | 9 ++++++++- swap/src/protocol/alice/steps.rs | 22 ---------------------- swap/src/protocol/alice/swap.rs | 10 +++------- 5 files changed, 34 insertions(+), 46 deletions(-) diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index b527ed9b..56b4ea44 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -110,6 +110,15 @@ impl From for Point { } } +impl From for ::bitcoin::PublicKey { + fn from(from: PublicKey) -> Self { + ::bitcoin::PublicKey { + compressed: true, + key: from.0.into(), + } + } +} + impl From for PublicKey { fn from(p: Point) -> Self { Self(p) diff --git a/swap/src/bitcoin/punish.rs b/swap/src/bitcoin/punish.rs index a8d1c7d6..37033316 100644 --- a/swap/src/bitcoin/punish.rs +++ b/swap/src/bitcoin/punish.rs @@ -1,12 +1,11 @@ -use crate::bitcoin::{Address, PublicKey, PunishTimelock, Transaction, TxCancel}; +use crate::bitcoin::{self, Address, PunishTimelock, Transaction, TxCancel}; use ::bitcoin::util::bip143::SigHashCache; use ::bitcoin::{SigHash, SigHashType}; -use anyhow::Result; -use ecdsa_fun::Signature; +use anyhow::{Context, Result}; use miniscript::{Descriptor, DescriptorTrait}; use std::collections::HashMap; -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct TxPunish { inner: Transaction, digest: SigHash, @@ -39,22 +38,20 @@ impl TxPunish { self.digest } - pub fn add_signatures( + pub fn complete( self, - (A, sig_a): (PublicKey, Signature), - (B, sig_b): (PublicKey, Signature), + tx_punish_sig_bob: bitcoin::Signature, + a: bitcoin::SecretKey, + B: bitcoin::PublicKey, ) -> Result { + let sig_a = a.sign(self.digest()); + let sig_b = tx_punish_sig_bob; + let satisfier = { let mut satisfier = HashMap::with_capacity(2); - let A = ::bitcoin::PublicKey { - compressed: true, - key: A.0.into(), - }; - let B = ::bitcoin::PublicKey { - compressed: true, - key: B.0.into(), - }; + let A = a.public().into(); + let B = B.into(); // The order in which these are inserted doesn't matter satisfier.insert(A, (sig_a.into(), ::bitcoin::SigHashType::All)); @@ -65,7 +62,8 @@ impl TxPunish { let mut tx_punish = self.inner; self.cancel_output_descriptor - .satisfy(&mut tx_punish.input[0], satisfier)?; + .satisfy(&mut tx_punish.input[0], satisfier) + .context("Failed to satisfy inputs with given signatures")?; Ok(tx_punish) } diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index e4c23bfd..cdb696b2 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -1,6 +1,6 @@ use crate::bitcoin::{ current_epoch, wait_for_cancel_timelock_to_expire, CancelTimelock, ExpiredTimelocks, - PunishTimelock, TxCancel, TxRefund, + PunishTimelock, TxCancel, TxPunish, TxRefund, }; use crate::execution_params::ExecutionParams; use crate::protocol::alice::{Message1, Message3}; @@ -343,4 +343,11 @@ impl State3 { ) .await } + + pub fn tx_punish(&self) -> TxPunish { + let tx_cancel = + bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B); + + bitcoin::TxPunish::new(&tx_cancel, &self.punish_address, self.punish_timelock) + } } diff --git a/swap/src/protocol/alice/steps.rs b/swap/src/protocol/alice/steps.rs index b44f1e61..50819175 100644 --- a/swap/src/protocol/alice/steps.rs +++ b/swap/src/protocol/alice/steps.rs @@ -143,25 +143,3 @@ pub fn extract_monero_private_key( Ok(spend_key) } - -pub fn build_bitcoin_punish_transaction( - tx_lock: &TxLock, - cancel_timelock: CancelTimelock, - punish_address: &bitcoin::Address, - punish_timelock: PunishTimelock, - tx_punish_sig_bob: bitcoin::Signature, - a: bitcoin::SecretKey, - B: bitcoin::PublicKey, -) -> Result { - let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B); - let tx_punish = bitcoin::TxPunish::new(&tx_cancel, &punish_address, punish_timelock); - - let sig_a = a.sign(tx_punish.digest()); - let sig_b = tx_punish_sig_bob; - - let signed_tx_punish = tx_punish - .add_signatures((a.public(), sig_a), (B, sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_cancel"); - - Ok(signed_tx_punish) -} diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 12a72fbc..8c1f8bb5 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -7,8 +7,8 @@ use crate::monero_ext::ScalarExt; use crate::protocol::alice; use crate::protocol::alice::event_loop::EventLoopHandle; use crate::protocol::alice::steps::{ - build_bitcoin_punish_transaction, extract_monero_private_key, lock_xmr, - publish_cancel_transaction, wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, + extract_monero_private_key, lock_xmr, publish_cancel_transaction, + wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, }; use crate::protocol::alice::AliceState; use crate::{bitcoin, database, monero}; @@ -398,11 +398,7 @@ async fn run_until_internal( state3, monero_wallet_restore_blockheight, } => { - let signed_tx_punish = build_bitcoin_punish_transaction( - &state3.tx_lock, - state3.cancel_timelock, - &state3.punish_address, - state3.punish_timelock, + let signed_tx_punish = state3.tx_punish().complete( state3.tx_punish_sig_bob.clone(), state3.a.clone(), state3.B, From e17cbadccb2655fa1e127461ad4d52e9c90270ce Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 16 Mar 2021 19:09:06 +1100 Subject: [PATCH 07/11] Don't add signatures to transaction unless necessary In order to compute the cancel TxID, we don't need to add the signatures. --- swap/src/protocol/bob/state.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index 1588df50..46092393 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -437,17 +437,6 @@ impl State4 { let tx_cancel = bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); - let sig_a = self.tx_cancel_sig_a.clone(); - let sig_b = self.b.sign(tx_cancel.digest()); - - let tx_cancel = tx_cancel - .clone() - .add_signatures((self.A, sig_a), (self.b.public(), sig_b)) - .expect( - "sig_{a,b} to be valid signatures for - tx_cancel", - ); - let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?; Ok(tx) From e5c0158597651e3cc0e18e07cd697a3aa8fb517c Mon Sep 17 00:00:00 2001 From: rishflab Date: Thu, 11 Mar 2021 18:16:00 +1100 Subject: [PATCH 08/11] Greatly reduce load onto the Electrum backend We achieve our optimizations in three ways: 1. Batching calls instead of making them individually. To get access to the batch calls, we replace all our calls to the HTTP interface with RPC calls. 2. Never directly make network calls based on function calls on the wallet. Instead, inquiring about the status of a script always just returns information based on local data. With every call, we check when we last refreshed the local data and do so if the data is considered to be too old. This interval is configurable. 3. Use electrum's notification feature to get updated with the latest blockheight. Co-authored-by: Thomas Eizinger Co-authored-by: Rishab Sharma --- swap/src/asb/config.rs | 14 +- swap/src/bin/asb.rs | 1 - swap/src/bin/swap.rs | 72 ++--- swap/src/bitcoin.rs | 96 ++++--- swap/src/bitcoin/cancel.rs | 36 +++ swap/src/bitcoin/lock.rs | 5 + swap/src/bitcoin/timelocks.rs | 18 +- swap/src/bitcoin/wallet.rs | 435 +++++++++++++++++++++---------- swap/src/cli/command.rs | 43 ++- swap/src/cli/config.rs | 1 + swap/src/protocol/alice/state.rs | 36 ++- swap/src/protocol/alice/steps.rs | 39 +-- swap/src/protocol/alice/swap.rs | 43 ++- swap/src/protocol/bob/state.rs | 140 ++++++---- swap/src/protocol/bob/swap.rs | 15 +- swap/tests/testutils/mod.rs | 12 - 16 files changed, 628 insertions(+), 378 deletions(-) create mode 100644 swap/src/cli/config.rs diff --git a/swap/src/asb/config.rs b/swap/src/asb/config.rs index 9f764041..03a94f7e 100644 --- a/swap/src/asb/config.rs +++ b/swap/src/asb/config.rs @@ -12,7 +12,6 @@ use tracing::info; use url::Url; const DEFAULT_LISTEN_ADDRESS: &str = "/ip4/0.0.0.0/tcp/9939"; -const DEFAULT_ELECTRUM_HTTP_URL: &str = "https://blockstream.info/testnet/api/"; const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:60002"; const DEFAULT_MONERO_WALLET_RPC_TESTNET_URL: &str = "http://127.0.0.1:38083/json_rpc"; @@ -52,7 +51,6 @@ pub struct Network { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct Bitcoin { - pub electrum_http_url: Url, pub electrum_rpc_url: Url, } @@ -120,12 +118,6 @@ pub fn query_user_for_initial_testnet_config() -> Result { .interact_text()?; let listen_address = listen_address.as_str().parse()?; - let electrum_http_url: String = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter Electrum HTTP URL or hit return to use default") - .default(DEFAULT_ELECTRUM_HTTP_URL.to_owned()) - .interact_text()?; - let electrum_http_url = Url::parse(electrum_http_url.as_str())?; - let electrum_rpc_url: String = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter Electrum RPC URL or hit return to use default") .default(DEFAULT_ELECTRUM_RPC_URL.to_owned()) @@ -144,10 +136,7 @@ pub fn query_user_for_initial_testnet_config() -> Result { network: Network { listen: listen_address, }, - bitcoin: Bitcoin { - electrum_http_url, - electrum_rpc_url, - }, + bitcoin: Bitcoin { electrum_rpc_url }, monero: Monero { wallet_rpc_url: monero_wallet_rpc_url, }, @@ -170,7 +159,6 @@ mod tests { dir: Default::default(), }, bitcoin: Bitcoin { - electrum_http_url: Url::from_str(DEFAULT_ELECTRUM_HTTP_URL).unwrap(), electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), }, network: Network { diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index 5574d1a5..e618404b 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -136,7 +136,6 @@ async fn init_wallets( ) -> Result<(bitcoin::Wallet, monero::Wallet)> { let bitcoin_wallet = bitcoin::Wallet::new( config.bitcoin.electrum_rpc_url, - config.bitcoin.electrum_http_url, BITCOIN_NETWORK, bitcoin_wallet_data_dir, key, diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 771621e4..9c6be51f 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -21,9 +21,7 @@ use std::sync::Arc; use std::time::Duration; use structopt::StructOpt; use swap::bitcoin::{Amount, TxLock}; -use swap::cli::command::{ - AliceConnectParams, Arguments, BitcoinParams, Command, Data, MoneroParams, -}; +use swap::cli::command::{AliceConnectParams, Arguments, Command, Data, MoneroParams}; use swap::database::Database; use swap::execution_params::{ExecutionParams, GetExecutionParams}; use swap::network::quote::BidQuote; @@ -95,11 +93,7 @@ async fn main() -> Result<()> { receive_monero_address, monero_daemon_host, }, - bitcoin_params: - BitcoinParams { - electrum_http_url, - electrum_rpc_url, - }, + electrum_rpc_url, } => { if receive_monero_address.network != monero_network { bail!( @@ -109,14 +103,9 @@ async fn main() -> Result<()> { ) } - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_network, - electrum_rpc_url, - electrum_http_url, - seed, - data_dir.clone(), - ) - .await?; + let bitcoin_wallet = + init_bitcoin_wallet(bitcoin_network, electrum_rpc_url, seed, data_dir.clone()) + .await?; let (monero_wallet, _process) = init_monero_wallet( monero_network, data_dir, @@ -196,24 +185,15 @@ async fn main() -> Result<()> { receive_monero_address, monero_daemon_host, }, - bitcoin_params: - BitcoinParams { - electrum_http_url, - electrum_rpc_url, - }, + electrum_rpc_url, } => { if receive_monero_address.network != monero_network { bail!("The given monero address is on network {:?}, expected address of network {:?}.", receive_monero_address.network, monero_network) } - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_network, - electrum_rpc_url, - electrum_http_url, - seed, - data_dir.clone(), - ) - .await?; + let bitcoin_wallet = + init_bitcoin_wallet(bitcoin_network, electrum_rpc_url, seed, data_dir.clone()) + .await?; let (monero_wallet, _process) = init_monero_wallet( monero_network, data_dir, @@ -255,20 +235,10 @@ async fn main() -> Result<()> { Command::Cancel { swap_id, force, - bitcoin_params: - BitcoinParams { - electrum_http_url, - electrum_rpc_url, - }, + electrum_rpc_url, } => { - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_network, - electrum_rpc_url, - electrum_http_url, - seed, - data_dir, - ) - .await?; + let bitcoin_wallet = + init_bitcoin_wallet(bitcoin_network, electrum_rpc_url, seed, data_dir).await?; let resume_state = db.get_state(swap_id)?.try_into_bob()?.into(); let cancel = @@ -290,20 +260,10 @@ async fn main() -> Result<()> { Command::Refund { swap_id, force, - bitcoin_params: - BitcoinParams { - electrum_http_url, - electrum_rpc_url, - }, + electrum_rpc_url, } => { - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_network, - electrum_rpc_url, - electrum_http_url, - seed, - data_dir, - ) - .await?; + let bitcoin_wallet = + init_bitcoin_wallet(bitcoin_network, electrum_rpc_url, seed, data_dir).await?; let resume_state = db.get_state(swap_id)?.try_into_bob()?.into(); @@ -324,7 +284,6 @@ async fn main() -> Result<()> { async fn init_bitcoin_wallet( network: bitcoin::Network, electrum_rpc_url: Url, - electrum_http_url: Url, seed: Seed, data_dir: PathBuf, ) -> Result { @@ -332,7 +291,6 @@ async fn init_bitcoin_wallet( let wallet = bitcoin::Wallet::new( electrum_rpc_url.clone(), - electrum_http_url.clone(), network, &wallet_dir, seed.derive_extended_private_key(network)?, diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 56b4ea44..7577633d 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -20,6 +20,7 @@ pub use ecdsa_fun::fun::Scalar; pub use ecdsa_fun::Signature; pub use wallet::Wallet; +use crate::bitcoin::wallet::ScriptStatus; use ::bitcoin::hashes::hex::ToHex; use ::bitcoin::hashes::Hash; use ::bitcoin::{secp256k1, SigHash}; @@ -218,46 +219,21 @@ pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Resu Ok(s) } -pub async fn poll_until_block_height_is_gte( - wallet: &crate::bitcoin::Wallet, - target: BlockHeight, -) -> Result<()> { - while wallet.get_block_height().await? < target { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - } - Ok(()) -} - -pub async fn current_epoch( - bitcoin_wallet: &crate::bitcoin::Wallet, +pub fn current_epoch( cancel_timelock: CancelTimelock, punish_timelock: PunishTimelock, - lock_tx_id: ::bitcoin::Txid, -) -> Result { - let current_block_height = bitcoin_wallet.get_block_height().await?; - let lock_tx_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await?; - let cancel_timelock_height = lock_tx_height + cancel_timelock; - let punish_timelock_height = cancel_timelock_height + punish_timelock; - - match ( - current_block_height < cancel_timelock_height, - current_block_height < punish_timelock_height, - ) { - (true, _) => Ok(ExpiredTimelocks::None), - (false, true) => Ok(ExpiredTimelocks::Cancel), - (false, false) => Ok(ExpiredTimelocks::Punish), + tx_lock_status: ScriptStatus, + tx_cancel_status: ScriptStatus, +) -> ExpiredTimelocks { + if tx_cancel_status.is_confirmed_with(punish_timelock) { + return ExpiredTimelocks::Punish; } -} -pub async fn wait_for_cancel_timelock_to_expire( - bitcoin_wallet: &crate::bitcoin::Wallet, - cancel_timelock: CancelTimelock, - lock_tx_id: ::bitcoin::Txid, -) -> Result<()> { - let tx_lock_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await?; + if tx_lock_status.is_confirmed_with(cancel_timelock) { + return ExpiredTimelocks::Cancel; + } - poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + cancel_timelock).await?; - Ok(()) + ExpiredTimelocks::None } #[derive(Clone, Copy, thiserror::Error, Debug)] @@ -275,3 +251,53 @@ pub struct EmptyWitnessStack; #[derive(Clone, Copy, thiserror::Error, Debug)] #[error("input has {0} witnesses, expected 3")] pub struct NotThreeWitnesses(usize); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lock_confirmations_le_to_cancel_timelock_no_timelock_expired() { + let tx_lock_status = ScriptStatus::from_confirmations(4); + let tx_cancel_status = ScriptStatus::Unseen; + + let expired_timelock = current_epoch( + CancelTimelock::new(5), + PunishTimelock::new(5), + tx_lock_status, + tx_cancel_status, + ); + + assert_eq!(expired_timelock, ExpiredTimelocks::None) + } + + #[test] + fn lock_confirmations_ge_to_cancel_timelock_cancel_timelock_expired() { + let tx_lock_status = ScriptStatus::from_confirmations(5); + let tx_cancel_status = ScriptStatus::Unseen; + + let expired_timelock = current_epoch( + CancelTimelock::new(5), + PunishTimelock::new(5), + tx_lock_status, + tx_cancel_status, + ); + + assert_eq!(expired_timelock, ExpiredTimelocks::Cancel) + } + + #[test] + fn cancel_confirmations_ge_to_punish_timelock_punish_timelock_expired() { + let tx_lock_status = ScriptStatus::from_confirmations(10); + let tx_cancel_status = ScriptStatus::from_confirmations(5); + + let expired_timelock = current_epoch( + CancelTimelock::new(5), + PunishTimelock::new(5), + tx_lock_status, + tx_cancel_status, + ); + + assert_eq!(expired_timelock, ExpiredTimelocks::Punish) + } +} diff --git a/swap/src/bitcoin/cancel.rs b/swap/src/bitcoin/cancel.rs index d94c91dd..606ac579 100644 --- a/swap/src/bitcoin/cancel.rs +++ b/swap/src/bitcoin/cancel.rs @@ -5,9 +5,11 @@ use crate::bitcoin::{ use ::bitcoin::util::bip143::SigHashCache; use ::bitcoin::{OutPoint, SigHash, SigHashType, TxIn, TxOut, Txid}; use anyhow::Result; +use bitcoin::Script; use ecdsa_fun::Signature; use miniscript::{Descriptor, DescriptorTrait}; use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; use std::collections::HashMap; use std::ops::Add; @@ -33,6 +35,18 @@ impl Add for BlockHeight { } } +impl PartialOrd for u32 { + fn partial_cmp(&self, other: &CancelTimelock) -> Option { + self.partial_cmp(&other.0) + } +} + +impl PartialEq for u32 { + fn eq(&self, other: &CancelTimelock) -> bool { + self.eq(&other.0) + } +} + /// Represent a timelock, expressed in relative block height as defined in /// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki). /// E.g. The timelock expires 10 blocks after the reference transaction is @@ -55,6 +69,18 @@ impl Add for BlockHeight { } } +impl PartialOrd for u32 { + fn partial_cmp(&self, other: &PunishTimelock) -> Option { + self.partial_cmp(&other.0) + } +} + +impl PartialEq for u32 { + fn eq(&self, other: &PunishTimelock) -> bool { + self.eq(&other.0) + } +} + #[derive(Debug, Clone)] pub struct TxCancel { inner: Transaction, @@ -122,6 +148,16 @@ impl TxCancel { OutPoint::new(self.inner.txid(), 0) } + /// Return the relevant script_pubkey of this transaction. + /// + /// Even though a transaction can have multiple outputs, the nature of our + /// protocol is that there is only one relevant output within this one. + /// As such, subscribing or inquiring the status of this script allows us to + /// check the status of the whole transaction. + pub fn script_pubkey(&self) -> Script { + self.output_descriptor.script_pubkey() + } + pub fn add_signatures( self, (A, sig_a): (PublicKey, Signature), diff --git a/swap/src/bitcoin/lock.rs b/swap/src/bitcoin/lock.rs index f9ef3814..220f80da 100644 --- a/swap/src/bitcoin/lock.rs +++ b/swap/src/bitcoin/lock.rs @@ -4,6 +4,7 @@ use crate::bitcoin::{ use ::bitcoin::util::psbt::PartiallySignedTransaction; use ::bitcoin::{OutPoint, TxIn, TxOut, Txid}; use anyhow::Result; +use bitcoin::Script; use ecdsa_fun::fun::Point; use miniscript::{Descriptor, DescriptorTrait}; use rand::thread_rng; @@ -55,6 +56,10 @@ impl TxLock { .len() } + pub fn script_pubkey(&self) -> Script { + self.output_descriptor.script_pubkey() + } + /// Retreive the index of the locked output in the transaction outputs /// vector fn lock_output_vout(&self) -> usize { diff --git a/swap/src/bitcoin/timelocks.rs b/swap/src/bitcoin/timelocks.rs index 06dfc90c..ab365df8 100644 --- a/swap/src/bitcoin/timelocks.rs +++ b/swap/src/bitcoin/timelocks.rs @@ -1,4 +1,7 @@ +use anyhow::Context; +use bdk::electrum_client::HeaderNotification; use serde::{Deserialize, Serialize}; +use std::convert::{TryFrom, TryInto}; use std::ops::Add; /// Represent a block height, or block number, expressed in absolute block @@ -26,6 +29,19 @@ impl BlockHeight { } } +impl TryFrom for BlockHeight { + type Error = anyhow::Error; + + fn try_from(value: HeaderNotification) -> Result { + Ok(Self( + value + .height + .try_into() + .context("Failed to fit usize into u32")?, + )) + } +} + impl Add for BlockHeight { type Output = BlockHeight; fn add(self, rhs: u32) -> Self::Output { @@ -33,7 +49,7 @@ impl Add for BlockHeight { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum ExpiredTimelocks { None, Cancel, diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index 1227b2b0..7093743b 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -4,48 +4,31 @@ use crate::execution_params::ExecutionParams; use ::bitcoin::util::psbt::PartiallySignedTransaction; use ::bitcoin::Txid; use anyhow::{anyhow, bail, Context, Result}; -use backoff::backoff::Constant as ConstantBackoff; -use backoff::future::retry; use bdk::blockchain::{noop_progress, Blockchain, ElectrumBlockchain}; use bdk::descriptor::Segwitv0; -use bdk::electrum_client::{self, Client, ElectrumApi}; +use bdk::electrum_client::{self, ElectrumApi, GetHistoryRes}; use bdk::keys::DerivableKey; use bdk::{FeeRate, KeychainKind}; use bitcoin::Script; use reqwest::Url; -use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::convert::TryFrom; +use std::fmt; use std::path::Path; use std::sync::Arc; -use std::time::Duration; +use std::time::{Duration, Instant}; use tokio::sync::Mutex; -use tokio::time::interval; const SLED_TREE_NAME: &str = "default_tree"; -#[derive(Debug, thiserror::Error)] -enum Error { - #[error("Sending the request failed")] - Io(reqwest::Error), - #[error("Conversion to Integer failed")] - Parse(std::num::ParseIntError), - #[error("The transaction is not minded yet")] - NotYetMined, - #[error("Deserialization failed")] - JsonDeserialization(reqwest::Error), - #[error("Electrum client error")] - ElectrumClient(electrum_client::Error), -} - pub struct Wallet { - inner: Arc>>, - http_url: Url, - rpc_url: Url, + client: Arc>, + wallet: Arc>>, } impl Wallet { pub async fn new( electrum_rpc_url: Url, - electrum_http_url: Url, network: bitcoin::Network, wallet_dir: &Path, key: impl DerivableKey + Clone, @@ -53,8 +36,9 @@ impl Wallet { // Workaround for https://github.com/bitcoindevkit/rust-electrum-client/issues/47. let config = electrum_client::ConfigBuilder::default().retry(2).build(); - let client = Client::from_config(electrum_rpc_url.as_str(), config) - .map_err(|e| anyhow!("Failed to init electrum rpc client: {:?}", e))?; + let client = + bdk::electrum_client::Client::from_config(electrum_rpc_url.as_str(), config.clone()) + .map_err(|e| anyhow!("Failed to init electrum rpc client: {:?}", e))?; let db = bdk::sled::open(wallet_dir)?.open_tree(SLED_TREE_NAME)?; @@ -66,16 +50,20 @@ impl Wallet { ElectrumBlockchain::from(client), )?; + let electrum = bdk::electrum_client::Client::from_config(electrum_rpc_url.as_str(), config) + .map_err(|e| anyhow!("Failed to init electrum rpc client {:?}", e))?; + + let interval = Duration::from_secs(5); + Ok(Self { - inner: Arc::new(Mutex::new(bdk_wallet)), - http_url: electrum_http_url, - rpc_url: electrum_rpc_url, + wallet: Arc::new(Mutex::new(bdk_wallet)), + client: Arc::new(Mutex::new(Client::new(electrum, interval)?)), }) } pub async fn balance(&self) -> Result { let balance = self - .inner + .wallet .lock() .await .get_balance() @@ -86,7 +74,7 @@ impl Wallet { pub async fn new_address(&self) -> Result
{ let address = self - .inner + .wallet .lock() .await .get_new_address() @@ -96,13 +84,14 @@ impl Wallet { } pub async fn get_tx(&self, txid: Txid) -> Result> { - let tx = self.inner.lock().await.client().get_tx(&txid)?; + let tx = self.wallet.lock().await.client().get_tx(&txid)?; + Ok(tx) } pub async fn transaction_fee(&self, txid: Txid) -> Result { let fees = self - .inner + .wallet .lock() .await .list_transactions(true)? @@ -117,7 +106,7 @@ impl Wallet { } pub async fn sync(&self) -> Result<()> { - self.inner + self.wallet .lock() .await .sync(noop_progress(), None) @@ -131,7 +120,7 @@ impl Wallet { address: Address, amount: Amount, ) -> Result { - let wallet = self.inner.lock().await; + let wallet = self.wallet.lock().await; let mut tx_builder = wallet.build_tx(); tx_builder.add_recipient(address.script_pubkey(), amount.as_sat()); @@ -147,7 +136,7 @@ impl Wallet { /// already accounting for the fees we need to spend to get the /// transaction confirmed. pub async fn max_giveable(&self, locking_script_size: usize) -> Result { - let wallet = self.inner.lock().await; + let wallet = self.wallet.lock().await; let mut tx_builder = wallet.build_tx(); @@ -163,7 +152,7 @@ impl Wallet { } pub async fn get_network(&self) -> bitcoin::Network { - self.inner.lock().await.network() + self.wallet.lock().await.network() } /// Broadcast the given transaction to the network and emit a log statement @@ -171,7 +160,7 @@ impl Wallet { pub async fn broadcast(&self, transaction: Transaction, kind: &str) -> Result { let txid = transaction.txid(); - self.inner + self.wallet .lock() .await .broadcast(transaction) @@ -185,7 +174,7 @@ impl Wallet { } pub async fn sign_and_finalize(&self, psbt: PartiallySignedTransaction) -> Result { - let (signed_psbt, finalized) = self.inner.lock().await.sign(psbt, None)?; + let (signed_psbt, finalized) = self.wallet.lock().await.sign(psbt, None)?; if !finalized { bail!("PSBT is not finalized") @@ -202,106 +191,62 @@ impl Wallet { .ok_or_else(|| anyhow!("Could not get raw tx with id: {}", txid)) } - pub async fn watch_for_raw_transaction(&self, txid: Txid) -> Result { - tracing::debug!("watching for tx: {}", txid); - let tx = retry(ConstantBackoff::new(Duration::from_secs(1)), || async { - let client = Client::new(self.rpc_url.as_ref()) - .map_err(|err| backoff::Error::Permanent(Error::ElectrumClient(err)))?; - - let tx = client.transaction_get(&txid).map_err(|err| match err { - electrum_client::Error::Protocol(err) => { - tracing::debug!("Received protocol error {} from Electrum, retrying...", err); - backoff::Error::Transient(Error::NotYetMined) - } - err => backoff::Error::Permanent(Error::ElectrumClient(err)), - })?; - - Result::<_, backoff::Error>::Ok(tx) - }) - .await - .context("Transient errors should be retried")?; - - Ok(tx) + pub async fn status_of_script(&self, script: &Script, txid: &Txid) -> Result { + self.client.lock().await.status_of_script(script, txid) } - pub async fn get_block_height(&self) -> Result { - let url = make_blocks_tip_height_url(&self.http_url)?; + pub async fn watch_until_status( + &self, + txid: Txid, + script: Script, + mut status_fn: impl FnMut(ScriptStatus) -> bool, + ) -> Result<()> { + let mut last_status = None; - let height = retry(ConstantBackoff::new(Duration::from_secs(1)), || async { - let height = reqwest::get(url.clone()) - .await - .map_err(Error::Io)? - .text() - .await - .map_err(Error::Io)? - .parse::() - .map_err(|err| backoff::Error::Permanent(Error::Parse(err)))?; - Result::<_, backoff::Error>::Ok(height) - }) - .await - .context("Transient errors should be retried")?; + loop { + let new_status = self.client.lock().await.status_of_script(&script, &txid)?; - Ok(BlockHeight::new(height)) - } + if Some(new_status) != last_status { + tracing::debug!(%txid, "Transaction is {}", new_status); + } + last_status = Some(new_status); - pub async fn transaction_block_height(&self, txid: Txid) -> Result { - let status_url = make_tx_status_url(&self.http_url, txid)?; + if status_fn(new_status) { + break; + } - #[derive(Serialize, Deserialize, Debug, Clone)] - struct TransactionStatus { - block_height: Option, - confirmed: bool, + tokio::time::sleep(Duration::from_secs(5)).await; } - let height = retry(ConstantBackoff::new(Duration::from_secs(1)), || async { - let block_height = reqwest::get(status_url.clone()) - .await - .map_err(|err| backoff::Error::Transient(Error::Io(err)))? - .json::() - .await - .map_err(|err| backoff::Error::Permanent(Error::JsonDeserialization(err)))? - .block_height - .ok_or(backoff::Error::Transient(Error::NotYetMined))?; - Result::<_, backoff::Error>::Ok(block_height) - }) - .await - .context("Transient errors should be retried")?; - - Ok(BlockHeight::new(height)) + Ok(()) } pub async fn wait_for_transaction_finality( &self, txid: Txid, + script_to_watch: Script, execution_params: ExecutionParams, ) -> Result<()> { let conf_target = execution_params.bitcoin_finality_confirmations; tracing::info!(%txid, "Waiting for {} confirmation{} of Bitcoin transaction", conf_target, if conf_target > 1 { "s" } else { "" }); - // Divide by 4 to not check too often yet still be aware of the new block early - // on. - let mut interval = interval(execution_params.bitcoin_avg_block_time / 4); + let mut seen_confirmations = 0; - loop { - let tx_block_height = self.transaction_block_height(txid).await?; - tracing::debug!("tx_block_height: {:?}", tx_block_height); + self.watch_until_status(txid, script_to_watch, |status| match status { + ScriptStatus::Confirmed(inner) => { + let confirmations = inner.confirmations(); - let block_height = self.get_block_height().await?; - tracing::debug!("latest_block_height: {:?}", block_height); - - if let Some(confirmations) = block_height.checked_sub( - tx_block_height - .checked_sub(BlockHeight::new(1)) - .expect("transaction must be included in block with height >= 1"), - ) { - tracing::debug!(%txid, "confirmations: {:?}", confirmations); - if u32::from(confirmations) >= conf_target { - break; + if confirmations > seen_confirmations { + tracing::info!(%txid, "Bitcoin tx has {} out of {} confirmation{}", confirmations, conf_target, if conf_target > 1 { "s" } else { "" }); + seen_confirmations = confirmations; } - } - interval.tick().await; - } + + inner.meets_target(conf_target) + }, + _ => false + }) + .await?; Ok(()) } @@ -314,42 +259,258 @@ impl Wallet { } } -fn make_tx_status_url(base_url: &Url, txid: Txid) -> Result { - let url = base_url.join(&format!("tx/{}/status", txid))?; - - Ok(url) +struct Client { + electrum: bdk::electrum_client::Client, + latest_block: BlockHeight, + last_ping: Instant, + interval: Duration, + script_history: BTreeMap>, } -fn make_blocks_tip_height_url(base_url: &Url) -> Result { - let url = base_url.join("blocks/tip/height")?; +impl Client { + fn new(electrum: bdk::electrum_client::Client, interval: Duration) -> Result { + let latest_block = electrum.block_headers_subscribe().map_err(|e| { + anyhow!( + "Electrum client failed to subscribe to header notifications: {:?}", + e + ) + })?; - Ok(url) + Ok(Self { + electrum, + latest_block: BlockHeight::try_from(latest_block)?, + last_ping: Instant::now(), + interval, + script_history: Default::default(), + }) + } + + /// Ping the electrum server unless we already did within the set interval. + /// + /// Returns a boolean indicating whether we actually pinged the server. + fn ping(&mut self) -> bool { + if self.last_ping.elapsed() <= self.interval { + return false; + } + + match self.electrum.ping() { + Ok(()) => { + self.last_ping = Instant::now(); + + true + } + Err(error) => { + tracing::debug!(?error, "Failed to ping electrum server"); + + false + } + } + } + + fn drain_notifications(&mut self) -> Result<()> { + let pinged = self.ping(); + + if !pinged { + return Ok(()); + } + + self.drain_blockheight_notifications()?; + self.update_script_histories()?; + + Ok(()) + } + + fn status_of_script(&mut self, script: &Script, txid: &Txid) -> Result { + if !self.script_history.contains_key(script) { + self.script_history.insert(script.clone(), vec![]); + } + + self.drain_notifications()?; + + let history = self.script_history.entry(script.clone()).or_default(); + + let history_of_tx = history + .iter() + .filter(|entry| &entry.tx_hash == txid) + .collect::>(); + + match history_of_tx.as_slice() { + [] => Ok(ScriptStatus::Unseen), + [remaining @ .., last] => { + if !remaining.is_empty() { + tracing::warn!("Found more than a single history entry for script. This is highly unexpected and those history entries will be ignored.") + } + + if last.height <= 0 { + Ok(ScriptStatus::InMempool) + } else { + Ok(ScriptStatus::Confirmed( + Confirmed::from_inclusion_and_latest_block( + u32::try_from(last.height)?, + u32::from(self.latest_block), + ), + )) + } + } + } + } + + fn drain_blockheight_notifications(&mut self) -> Result<()> { + let latest_block = std::iter::from_fn(|| self.electrum.block_headers_pop().transpose()) + .last() + .transpose() + .map_err(|e| anyhow!("Failed to pop header notification: {:?}", e))?; + + if let Some(new_block) = latest_block { + tracing::debug!( + "Got notification for new block at height {}", + new_block.height + ); + self.latest_block = BlockHeight::try_from(new_block)?; + } + + Ok(()) + } + + fn update_script_histories(&mut self) -> Result<()> { + let histories = self + .electrum + .batch_script_get_history(self.script_history.keys()) + .map_err(|e| anyhow!("Failed to get script histories {:?}", e))?; + + if histories.len() != self.script_history.len() { + bail!( + "Expected {} history entries, received {}", + self.script_history.len(), + histories.len() + ); + } + + let scripts = self.script_history.keys().cloned(); + let histories = histories.into_iter(); + + self.script_history = scripts.zip(histories).collect::>(); + + Ok(()) + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum ScriptStatus { + Unseen, + InMempool, + Confirmed(Confirmed), +} + +impl ScriptStatus { + pub fn from_confirmations(confirmations: u32) -> Self { + match confirmations { + 0 => Self::InMempool, + confirmations => Self::Confirmed(Confirmed::new(confirmations - 1)), + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Confirmed { + /// The depth of this transaction within the blockchain. + /// + /// Will be zero if the transaction is included in the latest block. + depth: u32, +} + +impl Confirmed { + pub fn new(depth: u32) -> Self { + Self { depth } + } + + /// Compute the depth of a transaction based on its inclusion height and the + /// latest known block. + /// + /// Our information about the latest block might be outdated. To avoid an + /// overflow, we make sure the depth is 0 in case the inclusion height + /// exceeds our latest known block, + pub fn from_inclusion_and_latest_block(inclusion_height: u32, latest_block: u32) -> Self { + let depth = latest_block.saturating_sub(inclusion_height); + + Self { depth } + } + + pub fn confirmations(&self) -> u32 { + self.depth + 1 + } + + pub fn meets_target(&self, target: T) -> bool + where + u32: PartialOrd, + { + self.confirmations() >= target + } +} + +impl ScriptStatus { + /// Check if the script has any confirmations. + pub fn is_confirmed(&self) -> bool { + matches!(self, ScriptStatus::Confirmed(_)) + } + + /// Check if the script has met the given confirmation target. + pub fn is_confirmed_with(&self, target: T) -> bool + where + u32: PartialOrd, + { + match self { + ScriptStatus::Confirmed(inner) => inner.meets_target(target), + _ => false, + } + } + + pub fn has_been_seen(&self) -> bool { + matches!(self, ScriptStatus::InMempool | ScriptStatus::Confirmed(_)) + } +} + +impl fmt::Display for ScriptStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ScriptStatus::Unseen => write!(f, "unseen"), + ScriptStatus::InMempool => write!(f, "in mempool"), + ScriptStatus::Confirmed(inner) => { + write!(f, "confirmed with {} blocks", inner.confirmations()) + } + } + } } #[cfg(test)] mod tests { use super::*; - use crate::cli::command::DEFAULT_ELECTRUM_HTTP_URL; #[test] - fn create_tx_status_url_from_default_base_url_success() { - let base_url = DEFAULT_ELECTRUM_HTTP_URL.parse().unwrap(); - let txid = Txid::default; + fn given_depth_0_should_meet_confirmation_target_one() { + let script = ScriptStatus::Confirmed(Confirmed { depth: 0 }); - let url = make_tx_status_url(&base_url, txid()).unwrap(); + let confirmed = script.is_confirmed_with(1); - assert_eq!(url.as_str(), "https://blockstream.info/testnet/api/tx/0000000000000000000000000000000000000000000000000000000000000000/status"); + assert!(confirmed) } #[test] - fn create_block_tip_height_url_from_default_base_url_success() { - let base_url = DEFAULT_ELECTRUM_HTTP_URL.parse().unwrap(); + fn given_confirmations_1_should_meet_confirmation_target_one() { + let script = ScriptStatus::from_confirmations(1); - let url = make_blocks_tip_height_url(&base_url).unwrap(); + let confirmed = script.is_confirmed_with(1); - assert_eq!( - url.as_str(), - "https://blockstream.info/testnet/api/blocks/tip/height" - ); + assert!(confirmed) + } + + #[test] + fn given_inclusion_after_lastest_known_block_at_least_depth_0() { + let included_in = 10; + let latest_block = 9; + + let confirmed = Confirmed::from_inclusion_and_latest_block(included_in, latest_block); + + assert_eq!(confirmed.depth, 0) } } diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index d58fca57..d3870933 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -40,8 +40,11 @@ pub enum Command { #[structopt(flatten)] connect_params: AliceConnectParams, - #[structopt(flatten)] - bitcoin_params: BitcoinParams, + #[structopt(long = "electrum-rpc", + help = "Provide the Bitcoin Electrum RPC URL", + default_value = DEFAULT_ELECTRUM_RPC_URL + )] + electrum_rpc_url: Url, #[structopt(flatten)] monero_params: MoneroParams, @@ -59,8 +62,11 @@ pub enum Command { #[structopt(flatten)] connect_params: AliceConnectParams, - #[structopt(flatten)] - bitcoin_params: BitcoinParams, + #[structopt(long = "electrum-rpc", + help = "Provide the Bitcoin Electrum RPC URL", + default_value = DEFAULT_ELECTRUM_RPC_URL + )] + electrum_rpc_url: Url, #[structopt(flatten)] monero_params: MoneroParams, @@ -76,8 +82,11 @@ pub enum Command { #[structopt(short, long)] force: bool, - #[structopt(flatten)] - bitcoin_params: BitcoinParams, + #[structopt(long = "electrum-rpc", + help = "Provide the Bitcoin Electrum RPC URL", + default_value = DEFAULT_ELECTRUM_RPC_URL + )] + electrum_rpc_url: Url, }, /// Try to cancel a swap and refund my BTC (expert users only) Refund { @@ -90,8 +99,11 @@ pub enum Command { #[structopt(short, long)] force: bool, - #[structopt(flatten)] - bitcoin_params: BitcoinParams, + #[structopt(long = "electrum-rpc", + help = "Provide the Bitcoin Electrum RPC URL", + default_value = DEFAULT_ELECTRUM_RPC_URL + )] + electrum_rpc_url: Url, }, } @@ -128,21 +140,6 @@ pub struct MoneroParams { pub monero_daemon_host: String, } -#[derive(structopt::StructOpt, Debug)] -pub struct BitcoinParams { - #[structopt(long = "electrum-http", - help = "Provide the Bitcoin Electrum HTTP URL", - default_value = DEFAULT_ELECTRUM_HTTP_URL - )] - pub electrum_http_url: Url, - - #[structopt(long = "electrum-rpc", - help = "Provide the Bitcoin Electrum RPC URL", - default_value = DEFAULT_ELECTRUM_RPC_URL - )] - pub electrum_rpc_url: Url, -} - #[derive(Clone, Debug)] pub struct Data(pub PathBuf); diff --git a/swap/src/cli/config.rs b/swap/src/cli/config.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/swap/src/cli/config.rs @@ -0,0 +1 @@ + diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index cdb696b2..bbfa0e2d 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -1,6 +1,5 @@ use crate::bitcoin::{ - current_epoch, wait_for_cancel_timelock_to_expire, CancelTimelock, ExpiredTimelocks, - PunishTimelock, TxCancel, TxPunish, TxRefund, + current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxCancel, TxPunish, TxRefund, }; use crate::execution_params::ExecutionParams; use crate::protocol::alice::{Message1, Message3}; @@ -323,25 +322,36 @@ impl State3 { &self, bitcoin_wallet: &bitcoin::Wallet, ) -> Result<()> { - wait_for_cancel_timelock_to_expire( - bitcoin_wallet, - self.cancel_timelock, - self.tx_lock.txid(), - ) - .await + bitcoin_wallet + .watch_until_status( + self.tx_lock.txid(), + self.tx_lock.script_pubkey(), + |status| status.is_confirmed_with(self.cancel_timelock), + ) + .await?; + + Ok(()) } pub async fn expired_timelocks( &self, bitcoin_wallet: &bitcoin::Wallet, ) -> Result { - current_epoch( - bitcoin_wallet, + let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B); + + let tx_lock_status = bitcoin_wallet + .status_of_script(&self.tx_lock.script_pubkey(), &self.tx_lock.txid()) + .await?; + let tx_cancel_status = bitcoin_wallet + .status_of_script(&tx_cancel.script_pubkey(), &tx_cancel.txid()) + .await?; + + Ok(current_epoch( self.cancel_timelock, self.punish_timelock, - self.tx_lock.txid(), - ) - .await + tx_lock_status, + tx_cancel_status, + )) } pub fn tx_punish(&self) -> TxPunish { diff --git a/swap/src/protocol/alice/steps.rs b/swap/src/protocol/alice/steps.rs index 50819175..43732012 100644 --- a/swap/src/protocol/alice/steps.rs +++ b/swap/src/protocol/alice/steps.rs @@ -1,12 +1,11 @@ use crate::bitcoin::{ - poll_until_block_height_is_gte, BlockHeight, CancelTimelock, EncryptedSignature, - PunishTimelock, TxCancel, TxLock, TxRefund, + CancelTimelock, EncryptedSignature, PunishTimelock, TxCancel, TxLock, TxRefund, }; use crate::protocol::alice; use crate::protocol::alice::event_loop::EventLoopHandle; use crate::protocol::alice::TransferProof; use crate::{bitcoin, monero}; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use futures::future::{select, Either}; use futures::pin_mut; use libp2p::PeerId; @@ -61,11 +60,11 @@ pub async fn publish_cancel_transaction( tx_cancel_sig_bob: bitcoin::Signature, bitcoin_wallet: &bitcoin::Wallet, ) -> Result { - // First wait for cancel timelock to expire - let tx_lock_height = bitcoin_wallet - .transaction_block_height(tx_lock.txid()) + bitcoin_wallet + .watch_until_status(tx_lock.txid(), tx_lock.script_pubkey(), |status| { + status.is_confirmed_with(cancel_timelock) + }) .await?; - poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + cancel_timelock).await?; let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B); @@ -98,26 +97,36 @@ pub async fn publish_cancel_transaction( pub async fn wait_for_bitcoin_refund( tx_cancel: &TxCancel, - cancel_tx_height: BlockHeight, punish_timelock: PunishTimelock, refund_address: &bitcoin::Address, bitcoin_wallet: &bitcoin::Wallet, ) -> Result<(bitcoin::TxRefund, Option)> { - let punish_timelock_expired = - poll_until_block_height_is_gte(bitcoin_wallet, cancel_tx_height + punish_timelock); - let tx_refund = bitcoin::TxRefund::new(tx_cancel, refund_address); - // TODO(Franck): This only checks the mempool, need to cater for the case where - // the transaction goes directly in a block - let seen_refund_tx = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); + let seen_refund_tx = bitcoin_wallet.watch_until_status( + tx_refund.txid(), + refund_address.script_pubkey(), + |status| status.has_been_seen(), + ); + + let punish_timelock_expired = + bitcoin_wallet.watch_until_status(tx_cancel.txid(), tx_cancel.script_pubkey(), |status| { + status.is_confirmed_with(punish_timelock) + }); pin_mut!(punish_timelock_expired); pin_mut!(seen_refund_tx); match select(punish_timelock_expired, seen_refund_tx).await { Either::Left(_) => Ok((tx_refund, None)), - Either::Right((published_refund_tx, _)) => Ok((tx_refund, Some(published_refund_tx?))), + Either::Right((Ok(()), _)) => { + let published_refund_tx = bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?; + + Ok((tx_refund, Some(published_refund_tx))) + } + Either::Right((Err(e), _)) => { + bail!(e.context("Failed to monitor refund transaction")) + } } } diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 8c1f8bb5..e15ade0d 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -82,13 +82,21 @@ async fn run_until_internal( } => { timeout( execution_params.bob_time_to_act, - bitcoin_wallet.watch_for_raw_transaction(state3.tx_lock.txid()), + bitcoin_wallet.watch_until_status( + state3.tx_lock.txid(), + state3.tx_lock.script_pubkey(), + |status| status.has_been_seen(), + ), ) .await .context("Failed to find lock Bitcoin tx")??; bitcoin_wallet - .wait_for_transaction_finality(state3.tx_lock.txid(), execution_params) + .wait_for_transaction_finality( + state3.tx_lock.txid(), + state3.tx_lock.script_pubkey(), + execution_params, + ) .await?; let state = AliceState::BtcLocked { @@ -213,7 +221,11 @@ async fn run_until_internal( Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await { Ok(txid) => { let publishded_redeem_tx = bitcoin_wallet - .wait_for_transaction_finality(txid, execution_params) + .wait_for_transaction_finality( + txid, + state3.redeem_address.script_pubkey(), + execution_params, + ) .await; match publishded_redeem_tx { @@ -308,13 +320,8 @@ async fn run_until_internal( tx_cancel, monero_wallet_restore_blockheight, } => { - let tx_cancel_height = bitcoin_wallet - .transaction_block_height(tx_cancel.txid()) - .await?; - let (tx_refund, published_refund_tx) = wait_for_bitcoin_refund( &tx_cancel, - tx_cancel_height, state3.punish_timelock, &state3.refund_address, &bitcoin_wallet, @@ -404,25 +411,34 @@ async fn run_until_internal( state3.B, )?; + let punish_script_pubkey = state3.punish_address.script_pubkey(); + let punish_tx_finalised = async { let txid = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?; bitcoin_wallet - .wait_for_transaction_finality(txid, execution_params) + .wait_for_transaction_finality(txid, punish_script_pubkey, execution_params) .await?; Result::<_, anyhow::Error>::Ok(txid) }; - let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); + let refund_tx_seen = bitcoin_wallet.watch_until_status( + tx_refund.txid(), + state3.refund_address.script_pubkey(), + |status| status.has_been_seen(), + ); pin_mut!(punish_tx_finalised); pin_mut!(refund_tx_seen); match select(refund_tx_seen, punish_tx_finalised).await { - Either::Left((published_refund_tx, _)) => { + Either::Left((Ok(()), _)) => { + let published_refund_tx = + bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?; + let spend_key = extract_monero_private_key( - published_refund_tx?, + published_refund_tx, &tx_refund, state3.s_a, state3.a.clone(), @@ -448,6 +464,9 @@ async fn run_until_internal( ) .await } + Either::Left((Err(e), _)) => { + bail!(e.context("Failed to monitor refund transaction")) + } Either::Right(_) => { let state = AliceState::BtcPunished; let db_state = (&state).into(); diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index 46092393..d743c0f5 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -1,6 +1,6 @@ use crate::bitcoin::{ - self, current_epoch, wait_for_cancel_timelock_to_expire, CancelTimelock, ExpiredTimelocks, - PunishTimelock, Transaction, TxCancel, Txid, + self, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel, + TxLock, Txid, }; use crate::execution_params::ExecutionParams; use crate::monero; @@ -9,7 +9,7 @@ use crate::monero_ext::ScalarExt; use crate::protocol::alice::{Message1, Message3}; use crate::protocol::bob::{EncryptedSignature, Message0, Message2, Message4}; use crate::protocol::CROSS_CURVE_PROOF_SYSTEM; -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{anyhow, bail, Result}; use ecdsa_fun::adaptor::{Adaptor, HashTranscript}; use ecdsa_fun::nonce::Deterministic; use ecdsa_fun::Signature; @@ -262,31 +262,27 @@ impl State2 { } } - pub async fn lock_btc(self, bitcoin_wallet: &bitcoin::Wallet) -> Result { - let signed_tx = bitcoin_wallet - .sign_and_finalize(self.tx_lock.clone().into()) - .await - .context("Failed to sign Bitcoin lock transaction")?; - - let _ = bitcoin_wallet.broadcast(signed_tx, "lock").await?; - - Ok(State3 { - A: self.A, - b: self.b, - s_b: self.s_b, - S_a_monero: self.S_a_monero, - S_a_bitcoin: self.S_a_bitcoin, - v: self.v, - xmr: self.xmr, - cancel_timelock: self.cancel_timelock, - punish_timelock: self.punish_timelock, - refund_address: self.refund_address, - redeem_address: self.redeem_address, - tx_lock: self.tx_lock, - tx_cancel_sig_a: self.tx_cancel_sig_a, - tx_refund_encsig: self.tx_refund_encsig, - min_monero_confirmations: self.min_monero_confirmations, - }) + pub async fn lock_btc(self) -> Result<(State3, TxLock)> { + Ok(( + State3 { + A: self.A, + b: self.b, + s_b: self.s_b, + S_a_monero: self.S_a_monero, + S_a_bitcoin: self.S_a_bitcoin, + v: self.v, + xmr: self.xmr, + cancel_timelock: self.cancel_timelock, + punish_timelock: self.punish_timelock, + refund_address: self.refund_address, + redeem_address: self.redeem_address, + tx_lock: self.tx_lock.clone(), + tx_cancel_sig_a: self.tx_cancel_sig_a, + tx_refund_encsig: self.tx_refund_encsig, + min_monero_confirmations: self.min_monero_confirmations, + }, + self.tx_lock, + )) } } @@ -354,12 +350,14 @@ impl State3 { &self, bitcoin_wallet: &bitcoin::Wallet, ) -> Result<()> { - wait_for_cancel_timelock_to_expire( - bitcoin_wallet, - self.cancel_timelock, - self.tx_lock.txid(), - ) - .await + bitcoin_wallet + .watch_until_status( + self.tx_lock.txid(), + self.tx_lock.script_pubkey(), + |status| status.is_confirmed_with(self.cancel_timelock), + ) + .await?; + Ok(()) } pub fn cancel(&self) -> State4 { @@ -390,13 +388,21 @@ impl State3 { &self, bitcoin_wallet: &bitcoin::Wallet, ) -> Result { - current_epoch( - bitcoin_wallet, + let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); + + let tx_lock_status = bitcoin_wallet + .status_of_script(&self.tx_lock.script_pubkey(), &self.tx_lock.txid()) + .await?; + let tx_cancel_status = bitcoin_wallet + .status_of_script(&tx_cancel.script_pubkey(), &tx_cancel.txid()) + .await?; + + Ok(current_epoch( self.cancel_timelock, self.punish_timelock, - self.tx_lock.txid(), - ) - .await + tx_lock_status, + tx_cancel_status, + )) } } @@ -419,10 +425,9 @@ pub struct State4 { impl State4 { pub fn next_message(&self) -> EncryptedSignature { - let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address); - let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest()); - - EncryptedSignature { tx_redeem_encsig } + EncryptedSignature { + tx_redeem_encsig: self.tx_redeem_encsig(), + } } pub fn tx_redeem_encsig(&self) -> bitcoin::EncryptedSignature { @@ -466,10 +471,16 @@ impl State4 { let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address); let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest()); - let tx_redeem_candidate = bitcoin_wallet - .watch_for_raw_transaction(tx_redeem.txid()) + bitcoin_wallet + .watch_until_status( + tx_redeem.txid(), + self.redeem_address.script_pubkey(), + |status| status.has_been_seen(), + ) .await?; + let tx_redeem_candidate = bitcoin_wallet.get_raw_transaction(tx_redeem.txid()).await?; + let tx_redeem_sig = tx_redeem.extract_signature_by_key(tx_redeem_candidate, self.b.public())?; let s_a = bitcoin::recover(self.S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)?; @@ -488,25 +499,36 @@ impl State4 { &self, bitcoin_wallet: &bitcoin::Wallet, ) -> Result<()> { - wait_for_cancel_timelock_to_expire( - bitcoin_wallet, - self.cancel_timelock, - self.tx_lock.txid(), - ) - .await + bitcoin_wallet + .watch_until_status( + self.tx_lock.txid(), + self.tx_lock.script_pubkey(), + |status| status.is_confirmed_with(self.cancel_timelock), + ) + .await?; + + Ok(()) } pub async fn expired_timelock( &self, bitcoin_wallet: &bitcoin::Wallet, ) -> Result { - current_epoch( - bitcoin_wallet, + let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); + + let tx_lock_status = bitcoin_wallet + .status_of_script(&self.tx_lock.script_pubkey(), &self.tx_lock.txid()) + .await?; + let tx_cancel_status = bitcoin_wallet + .status_of_script(&tx_cancel.script_pubkey(), &tx_cancel.txid()) + .await?; + + Ok(current_epoch( self.cancel_timelock, self.punish_timelock, - self.tx_lock.txid(), - ) - .await + tx_lock_status, + tx_cancel_status, + )) } pub async fn refund_btc( @@ -530,7 +552,11 @@ impl State4 { let txid = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?; bitcoin_wallet - .wait_for_transaction_finality(txid, execution_params) + .wait_for_transaction_finality( + txid, + self.refund_address.script_pubkey(), + execution_params, + ) .await?; Ok(()) diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 30f84ec6..b051923a 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -6,7 +6,7 @@ use crate::protocol::bob; use crate::protocol::bob::event_loop::EventLoopHandle; use crate::protocol::bob::state::*; use crate::{bitcoin, monero}; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use async_recursion::async_recursion; use rand::rngs::OsRng; use std::sync::Arc; @@ -99,7 +99,18 @@ async fn run_until_internal( // Do not lock Bitcoin if not connected to Alice. event_loop_handle.dial().await?; // Alice and Bob have exchanged info - let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).await?; + let (state3, tx_lock) = state2.lock_btc().await?; + let signed_tx = bitcoin_wallet + .sign_and_finalize(tx_lock.clone().into()) + .await + .context("Failed to sign Bitcoin lock transaction")?; + let tx_lock_id = bitcoin_wallet.broadcast(signed_tx, "lock").await?; + + bitcoin_wallet + .watch_until_status(tx_lock_id, tx_lock.script_pubkey(), |status| { + status.is_confirmed() + }) + .await?; let state = BobState::BtcLocked(state3); let db_state = state.clone().into(); diff --git a/swap/tests/testutils/mod.rs b/swap/tests/testutils/mod.rs index 9d14bf6a..ce480ed8 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -342,10 +342,6 @@ where .electrs .get_host_port(testutils::electrs::RPC_PORT) .expect("Could not map electrs rpc port"); - let electrs_http_port = containers - .electrs - .get_host_port(testutils::electrs::HTTP_PORT) - .expect("Could not map electrs http port"); let alice_seed = Seed::random().unwrap(); let bob_seed = Seed::random().unwrap(); @@ -357,7 +353,6 @@ where alice_starting_balances.clone(), tempdir().unwrap().path(), electrs_rpc_port, - electrs_http_port, alice_seed, execution_params, ) @@ -380,7 +375,6 @@ where bob_starting_balances.clone(), tempdir().unwrap().path(), electrs_rpc_port, - electrs_http_port, bob_seed, execution_params, ) @@ -588,7 +582,6 @@ async fn init_test_wallets( starting_balances: StartingBalances, datadir: &Path, electrum_rpc_port: u16, - electrum_http_port: u16, seed: Seed, execution_params: ExecutionParams, ) -> (Arc, Arc) { @@ -608,14 +601,9 @@ async fn init_test_wallets( let input = format!("tcp://@localhost:{}", electrum_rpc_port); Url::parse(&input).unwrap() }; - let electrum_http_url = { - let input = format!("http://@localhost:{}", electrum_http_port); - Url::parse(&input).unwrap() - }; let btc_wallet = swap::bitcoin::Wallet::new( electrum_rpc_url, - electrum_http_url, bitcoin::Network::Regtest, datadir, seed.derive_extended_private_key(bitcoin::Network::Regtest) From 84ea092a1b354dac186b72f15795f0bc96720999 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 16 Mar 2021 18:31:13 +1100 Subject: [PATCH 09/11] Remove unnecessary state variables by constructing TXs on demand --- swap/src/database/alice.rs | 37 ++++++++------------------------ swap/src/protocol/alice/state.rs | 21 ++++++++++++------ swap/src/protocol/alice/steps.rs | 5 ++--- swap/src/protocol/alice/swap.rs | 17 ++++++--------- 4 files changed, 32 insertions(+), 48 deletions(-) diff --git a/swap/src/database/alice.rs b/swap/src/database/alice.rs index 6c1a19ab..cb6c30b6 100644 --- a/swap/src/database/alice.rs +++ b/swap/src/database/alice.rs @@ -1,4 +1,4 @@ -use crate::bitcoin::{EncryptedSignature, TxCancel, TxRefund}; +use crate::bitcoin::EncryptedSignature; use crate::monero; use crate::monero::monero_private_key; use crate::protocol::alice; @@ -177,37 +177,18 @@ impl From for AliceState { Alice::BtcCancelled { monero_wallet_restore_blockheight, state3, - } => { - let tx_cancel = TxCancel::new( - &state3.tx_lock, - state3.cancel_timelock, - state3.a.public(), - state3.B, - ); + } => AliceState::BtcCancelled { + monero_wallet_restore_blockheight, + state3: Box::new(state3), + }, - AliceState::BtcCancelled { - monero_wallet_restore_blockheight, - state3: Box::new(state3), - tx_cancel: Box::new(tx_cancel), - } - } Alice::BtcPunishable { monero_wallet_restore_blockheight, state3, - } => { - let tx_cancel = TxCancel::new( - &state3.tx_lock, - state3.cancel_timelock, - state3.a.public(), - state3.B, - ); - let tx_refund = TxRefund::new(&tx_cancel, &state3.refund_address); - AliceState::BtcPunishable { - monero_wallet_restore_blockheight, - tx_refund: Box::new(tx_refund), - state3: Box::new(state3), - } - } + } => AliceState::BtcPunishable { + monero_wallet_restore_blockheight, + state3: Box::new(state3), + }, Alice::BtcRefunded { monero_wallet_restore_blockheight, state3, diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index bbfa0e2d..f3b83881 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -36,7 +36,6 @@ pub enum AliceState { BtcRedeemed, BtcCancelled { monero_wallet_restore_blockheight: BlockHeight, - tx_cancel: Box, state3: Box, }, BtcRefunded { @@ -46,7 +45,6 @@ pub enum AliceState { }, BtcPunishable { monero_wallet_restore_blockheight: BlockHeight, - tx_refund: Box, state3: Box, }, XmrRefunded, @@ -337,7 +335,7 @@ impl State3 { &self, bitcoin_wallet: &bitcoin::Wallet, ) -> Result { - let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B); + let tx_cancel = self.tx_cancel(); let tx_lock_status = bitcoin_wallet .status_of_script(&self.tx_lock.script_pubkey(), &self.tx_lock.txid()) @@ -354,10 +352,19 @@ impl State3 { )) } - pub fn tx_punish(&self) -> TxPunish { - let tx_cancel = - bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B); + pub fn tx_cancel(&self) -> TxCancel { + TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B) + } - bitcoin::TxPunish::new(&tx_cancel, &self.punish_address, self.punish_timelock) + pub fn tx_punish(&self) -> TxPunish { + bitcoin::TxPunish::new( + &self.tx_cancel(), + &self.punish_address, + self.punish_timelock, + ) + } + + pub fn tx_refund(&self) -> TxRefund { + bitcoin::TxRefund::new(&self.tx_cancel(), &self.refund_address) } } diff --git a/swap/src/protocol/alice/steps.rs b/swap/src/protocol/alice/steps.rs index 43732012..d4516a07 100644 --- a/swap/src/protocol/alice/steps.rs +++ b/swap/src/protocol/alice/steps.rs @@ -59,7 +59,7 @@ pub async fn publish_cancel_transaction( cancel_timelock: CancelTimelock, tx_cancel_sig_bob: bitcoin::Signature, bitcoin_wallet: &bitcoin::Wallet, -) -> Result { +) -> Result<()> { bitcoin_wallet .watch_until_status(tx_lock.txid(), tx_lock.script_pubkey(), |status| { status.is_confirmed_with(cancel_timelock) @@ -81,7 +81,6 @@ pub async fn publish_cancel_transaction( let sig_b = tx_cancel_sig_bob.clone(); let tx_cancel = tx_cancel - .clone() .add_signatures((a.public(), sig_a), (B, sig_b)) .expect("sig_{a,b} to be valid signatures for tx_cancel"); @@ -92,7 +91,7 @@ pub async fn publish_cancel_transaction( // block height } - Ok(tx_cancel) + Ok(()) } pub async fn wait_for_bitcoin_refund( diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index e15ade0d..e041f37a 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -285,7 +285,7 @@ async fn run_until_internal( state3, monero_wallet_restore_blockheight, } => { - let tx_cancel = publish_cancel_transaction( + publish_cancel_transaction( state3.tx_lock.clone(), state3.a.clone(), state3.B, @@ -297,7 +297,6 @@ async fn run_until_internal( let state = AliceState::BtcCancelled { state3, - tx_cancel: Box::new(tx_cancel), monero_wallet_restore_blockheight, }; let db_state = (&state).into(); @@ -317,11 +316,10 @@ async fn run_until_internal( } AliceState::BtcCancelled { state3, - tx_cancel, monero_wallet_restore_blockheight, } => { let (tx_refund, published_refund_tx) = wait_for_bitcoin_refund( - &tx_cancel, + &state3.tx_cancel(), state3.punish_timelock, &state3.refund_address, &bitcoin_wallet, @@ -332,7 +330,6 @@ async fn run_until_internal( match published_refund_tx { None => { let state = AliceState::BtcPunishable { - tx_refund: Box::new(tx_refund), state3, monero_wallet_restore_blockheight, }; @@ -401,7 +398,6 @@ async fn run_until_internal( Ok(state) } AliceState::BtcPunishable { - tx_refund, state3, monero_wallet_restore_blockheight, } => { @@ -424,7 +420,7 @@ async fn run_until_internal( }; let refund_tx_seen = bitcoin_wallet.watch_until_status( - tx_refund.txid(), + state3.tx_refund().txid(), state3.refund_address.script_pubkey(), |status| status.has_been_seen(), ); @@ -434,12 +430,13 @@ async fn run_until_internal( match select(refund_tx_seen, punish_tx_finalised).await { Either::Left((Ok(()), _)) => { - let published_refund_tx = - bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?; + let published_refund_tx = bitcoin_wallet + .get_raw_transaction(state3.tx_refund().txid()) + .await?; let spend_key = extract_monero_private_key( published_refund_tx, - &tx_refund, + &state3.tx_refund(), state3.s_a, state3.a.clone(), state3.S_b_bitcoin, From a0830f099f46b487f738e1ab2476b05dcb1dc062 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 16 Mar 2021 18:51:29 +1100 Subject: [PATCH 10/11] Pass relevant execution params into wallet instead of via functions The execution params don't change throughout the lifetime of the program. They can be set in the wallet at the very beginning. This simplifies the interface of the wallet functions. --- swap/src/bin/asb.rs | 1 + swap/src/bin/swap.rs | 54 ++++++++++++------- swap/src/bitcoin/wallet.rs | 7 +-- swap/src/protocol/alice/swap.rs | 4 +- swap/src/protocol/bob/refund.rs | 6 +-- swap/src/protocol/bob/state.rs | 13 +---- swap/src/protocol/bob/swap.rs | 4 +- ...refunds_using_cancel_and_refund_command.rs | 1 - ...and_refund_command_timelock_not_expired.rs | 1 - ...fund_command_timelock_not_expired_force.rs | 1 - swap/tests/testutils/mod.rs | 1 + 11 files changed, 46 insertions(+), 47 deletions(-) diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index e618404b..6a494bba 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -137,6 +137,7 @@ async fn init_wallets( let bitcoin_wallet = bitcoin::Wallet::new( config.bitcoin.electrum_rpc_url, BITCOIN_NETWORK, + execution_params.bitcoin_finality_confirmations, bitcoin_wallet_data_dir, key, ) diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 9c6be51f..dabed242 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -103,9 +103,14 @@ async fn main() -> Result<()> { ) } - let bitcoin_wallet = - init_bitcoin_wallet(bitcoin_network, electrum_rpc_url, seed, data_dir.clone()) - .await?; + let bitcoin_wallet = init_bitcoin_wallet( + bitcoin_network, + electrum_rpc_url, + seed, + data_dir.clone(), + execution_params, + ) + .await?; let (monero_wallet, _process) = init_monero_wallet( monero_network, data_dir, @@ -191,9 +196,14 @@ async fn main() -> Result<()> { bail!("The given monero address is on network {:?}, expected address of network {:?}.", receive_monero_address.network, monero_network) } - let bitcoin_wallet = - init_bitcoin_wallet(bitcoin_network, electrum_rpc_url, seed, data_dir.clone()) - .await?; + let bitcoin_wallet = init_bitcoin_wallet( + bitcoin_network, + electrum_rpc_url, + seed, + data_dir.clone(), + execution_params, + ) + .await?; let (monero_wallet, _process) = init_monero_wallet( monero_network, data_dir, @@ -237,8 +247,14 @@ async fn main() -> Result<()> { force, electrum_rpc_url, } => { - let bitcoin_wallet = - init_bitcoin_wallet(bitcoin_network, electrum_rpc_url, seed, data_dir).await?; + let bitcoin_wallet = init_bitcoin_wallet( + bitcoin_network, + electrum_rpc_url, + seed, + data_dir, + execution_params, + ) + .await?; let resume_state = db.get_state(swap_id)?.try_into_bob()?.into(); let cancel = @@ -262,20 +278,18 @@ async fn main() -> Result<()> { force, electrum_rpc_url, } => { - let bitcoin_wallet = - init_bitcoin_wallet(bitcoin_network, electrum_rpc_url, seed, data_dir).await?; + let bitcoin_wallet = init_bitcoin_wallet( + bitcoin_network, + electrum_rpc_url, + seed, + data_dir, + execution_params, + ) + .await?; let resume_state = db.get_state(swap_id)?.try_into_bob()?.into(); - bob::refund( - swap_id, - resume_state, - execution_params, - Arc::new(bitcoin_wallet), - db, - force, - ) - .await??; + bob::refund(swap_id, resume_state, Arc::new(bitcoin_wallet), db, force).await??; } }; Ok(()) @@ -286,12 +300,14 @@ async fn init_bitcoin_wallet( electrum_rpc_url: Url, seed: Seed, data_dir: PathBuf, + execution_params: ExecutionParams, ) -> Result { let wallet_dir = data_dir.join("wallet"); let wallet = bitcoin::Wallet::new( electrum_rpc_url.clone(), network, + execution_params.bitcoin_finality_confirmations, &wallet_dir, seed.derive_extended_private_key(network)?, ) diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index 7093743b..f3306d25 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -1,6 +1,5 @@ use crate::bitcoin::timelocks::BlockHeight; use crate::bitcoin::{Address, Amount, Transaction}; -use crate::execution_params::ExecutionParams; use ::bitcoin::util::psbt::PartiallySignedTransaction; use ::bitcoin::Txid; use anyhow::{anyhow, bail, Context, Result}; @@ -24,12 +23,14 @@ const SLED_TREE_NAME: &str = "default_tree"; pub struct Wallet { client: Arc>, wallet: Arc>>, + bitcoin_finality_confirmations: u32, } impl Wallet { pub async fn new( electrum_rpc_url: Url, network: bitcoin::Network, + bitcoin_finality_confirmations: u32, wallet_dir: &Path, key: impl DerivableKey + Clone, ) -> Result { @@ -58,6 +59,7 @@ impl Wallet { Ok(Self { wallet: Arc::new(Mutex::new(bdk_wallet)), client: Arc::new(Mutex::new(Client::new(electrum, interval)?)), + bitcoin_finality_confirmations, }) } @@ -225,9 +227,8 @@ impl Wallet { &self, txid: Txid, script_to_watch: Script, - execution_params: ExecutionParams, ) -> Result<()> { - let conf_target = execution_params.bitcoin_finality_confirmations; + let conf_target = self.bitcoin_finality_confirmations; tracing::info!(%txid, "Waiting for {} confirmation{} of Bitcoin transaction", conf_target, if conf_target > 1 { "s" } else { "" }); diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index e041f37a..b59a2fd8 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -95,7 +95,6 @@ async fn run_until_internal( .wait_for_transaction_finality( state3.tx_lock.txid(), state3.tx_lock.script_pubkey(), - execution_params, ) .await?; @@ -224,7 +223,6 @@ async fn run_until_internal( .wait_for_transaction_finality( txid, state3.redeem_address.script_pubkey(), - execution_params, ) .await; @@ -413,7 +411,7 @@ async fn run_until_internal( let txid = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?; bitcoin_wallet - .wait_for_transaction_finality(txid, punish_script_pubkey, execution_params) + .wait_for_transaction_finality(txid, punish_script_pubkey) .await?; Result::<_, anyhow::Error>::Ok(txid) diff --git a/swap/src/protocol/bob/refund.rs b/swap/src/protocol/bob/refund.rs index ffbfe458..b1390045 100644 --- a/swap/src/protocol/bob/refund.rs +++ b/swap/src/protocol/bob/refund.rs @@ -1,6 +1,5 @@ use crate::bitcoin::Wallet; use crate::database::{Database, Swap}; -use crate::execution_params::ExecutionParams; use crate::protocol::bob::BobState; use anyhow::{bail, Result}; use std::sync::Arc; @@ -13,7 +12,6 @@ pub struct SwapNotCancelledYet(Uuid); pub async fn refund( swap_id: Uuid, state: BobState, - execution_params: ExecutionParams, bitcoin_wallet: Arc, db: Database, force: bool, @@ -41,9 +39,7 @@ pub async fn refund( } }; - state4 - .refund_btc(bitcoin_wallet.as_ref(), execution_params) - .await?; + state4.refund_btc(bitcoin_wallet.as_ref()).await?; let state = BobState::BtcRefunded(state4); let db_state = state.clone().into(); diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index d743c0f5..d723b9c2 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -2,7 +2,6 @@ use crate::bitcoin::{ self, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel, TxLock, Txid, }; -use crate::execution_params::ExecutionParams; use crate::monero; use crate::monero::{monero_private_key, InsufficientFunds, TransferProof}; use crate::monero_ext::ScalarExt; @@ -531,11 +530,7 @@ impl State4 { )) } - pub async fn refund_btc( - &self, - bitcoin_wallet: &bitcoin::Wallet, - execution_params: ExecutionParams, - ) -> Result<()> { + pub async fn refund_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<()> { let tx_cancel = bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address); @@ -552,11 +547,7 @@ impl State4 { let txid = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?; bitcoin_wallet - .wait_for_transaction_finality( - txid, - self.refund_address.script_pubkey(), - execution_params, - ) + .wait_for_transaction_finality(txid, self.refund_address.script_pubkey()) .await?; Ok(()) diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index b051923a..75db537d 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -382,9 +382,7 @@ async fn run_until_internal( bail!("Internal error: canceled state reached before cancel timelock was expired"); } ExpiredTimelocks::Cancel => { - state - .refund_btc(bitcoin_wallet.as_ref(), execution_params) - .await?; + state.refund_btc(bitcoin_wallet.as_ref()).await?; BobState::BtcRefunded(state) } ExpiredTimelocks::Punish => BobState::BtcPunished { diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command.rs index 1f464adb..44e05385 100644 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command.rs +++ b/swap/tests/bob_refunds_using_cancel_and_refund_command.rs @@ -48,7 +48,6 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() { let bob_state = bob::refund( bob_swap.swap_id, bob_swap.state, - bob_swap.execution_params, bob_swap.bitcoin_wallet, bob_swap.db, false, diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs index d5f23aad..42c747f7 100644 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs +++ b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs @@ -39,7 +39,6 @@ async fn given_bob_manually_cancels_when_timelock_not_expired_errors() { bob::refund( bob_swap.swap_id, bob_swap.state, - bob_swap.execution_params, bob_swap.bitcoin_wallet, bob_swap.db, false, diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs index 9ae6bf38..255fa007 100644 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs +++ b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs @@ -36,7 +36,6 @@ async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() { let is_error = bob::refund( bob_swap.swap_id, bob_swap.state, - bob_swap.execution_params, bob_swap.bitcoin_wallet, bob_swap.db, true, diff --git a/swap/tests/testutils/mod.rs b/swap/tests/testutils/mod.rs index ce480ed8..bd051ca4 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -605,6 +605,7 @@ async fn init_test_wallets( let btc_wallet = swap::bitcoin::Wallet::new( electrum_rpc_url, bitcoin::Network::Regtest, + execution_params.bitcoin_finality_confirmations, datadir, seed.derive_extended_private_key(bitcoin::Network::Regtest) .expect("Could not create extended private key from seed"), From 273cf15631735c985bf06b3ff44eec66cad31bdb Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 16 Mar 2021 19:11:14 +1100 Subject: [PATCH 11/11] Introduce `Watchable` abstraction for Bitcoin wallet We have a repeated pattern where we construct one of our Tx{Cancel,Redeem,Punish,Refund,Lock} transactions and wait until the status of this transaction changes. We can make this more ergonomic by creating and implementing a `Watchable` trait that gives access to the TxId and relevant script for this transaction. This allows us to remove a parameter from the `watch_until_status` function. Additionally, there is a 2nd pattern: "Completing" one of these transaction and waiting until they are confirmed with the configured number of blocks for finality. We can make this more ergonomic by returning a future from `broadcast` that callers can await in case they want to wait for the broadcasted transaction to reach finality. --- swap/src/bitcoin/cancel.rs | 23 ++++---- swap/src/bitcoin/lock.rs | 11 ++++ swap/src/bitcoin/punish.rs | 18 ++++++- swap/src/bitcoin/redeem.rs | 16 +++++- swap/src/bitcoin/refund.rs | 14 +++++ swap/src/bitcoin/wallet.rs | 90 ++++++++++++++++++++++++-------- swap/src/protocol/alice/state.rs | 16 ++---- swap/src/protocol/alice/steps.rs | 48 ++++++++--------- swap/src/protocol/alice/swap.rs | 67 ++++++++---------------- swap/src/protocol/bob/state.rs | 47 +++++------------ swap/src/protocol/bob/swap.rs | 6 +-- 11 files changed, 204 insertions(+), 152 deletions(-) diff --git a/swap/src/bitcoin/cancel.rs b/swap/src/bitcoin/cancel.rs index 606ac579..e54bc988 100644 --- a/swap/src/bitcoin/cancel.rs +++ b/swap/src/bitcoin/cancel.rs @@ -1,3 +1,4 @@ +use crate::bitcoin::wallet::Watchable; use crate::bitcoin::{ build_shared_output_descriptor, Address, Amount, BlockHeight, PublicKey, Transaction, TxLock, TX_FEE, @@ -81,7 +82,7 @@ impl PartialEq for u32 { } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct TxCancel { inner: Transaction, digest: SigHash, @@ -148,16 +149,6 @@ impl TxCancel { OutPoint::new(self.inner.txid(), 0) } - /// Return the relevant script_pubkey of this transaction. - /// - /// Even though a transaction can have multiple outputs, the nature of our - /// protocol is that there is only one relevant output within this one. - /// As such, subscribing or inquiring the status of this script allows us to - /// check the status of the whole transaction. - pub fn script_pubkey(&self) -> Script { - self.output_descriptor.script_pubkey() - } - pub fn add_signatures( self, (A, sig_a): (PublicKey, Signature), @@ -216,3 +207,13 @@ impl TxCancel { } } } + +impl Watchable for TxCancel { + fn id(&self) -> Txid { + self.txid() + } + + fn script(&self) -> Script { + self.output_descriptor.script_pubkey() + } +} diff --git a/swap/src/bitcoin/lock.rs b/swap/src/bitcoin/lock.rs index 220f80da..d2d5e7ca 100644 --- a/swap/src/bitcoin/lock.rs +++ b/swap/src/bitcoin/lock.rs @@ -1,3 +1,4 @@ +use crate::bitcoin::wallet::Watchable; use crate::bitcoin::{ build_shared_output_descriptor, Address, Amount, PublicKey, Transaction, Wallet, TX_FEE, }; @@ -105,3 +106,13 @@ impl From for PartiallySignedTransaction { from.inner } } + +impl Watchable for TxLock { + fn id(&self) -> Txid { + self.txid() + } + + fn script(&self) -> Script { + self.output_descriptor.script_pubkey() + } +} diff --git a/swap/src/bitcoin/punish.rs b/swap/src/bitcoin/punish.rs index 37033316..4845429d 100644 --- a/swap/src/bitcoin/punish.rs +++ b/swap/src/bitcoin/punish.rs @@ -1,15 +1,18 @@ -use crate::bitcoin::{self, Address, PunishTimelock, Transaction, TxCancel}; +use crate::bitcoin::wallet::Watchable; +use crate::bitcoin::{self, Address, PunishTimelock, Transaction, TxCancel, Txid}; use ::bitcoin::util::bip143::SigHashCache; use ::bitcoin::{SigHash, SigHashType}; use anyhow::{Context, Result}; +use bdk::bitcoin::Script; use miniscript::{Descriptor, DescriptorTrait}; use std::collections::HashMap; -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct TxPunish { inner: Transaction, digest: SigHash, cancel_output_descriptor: Descriptor<::bitcoin::PublicKey>, + watch_script: Script, } impl TxPunish { @@ -31,6 +34,7 @@ impl TxPunish { inner: tx_punish, digest, cancel_output_descriptor: tx_cancel.output_descriptor.clone(), + watch_script: punish_address.script_pubkey(), } } @@ -68,3 +72,13 @@ impl TxPunish { Ok(tx_punish) } } + +impl Watchable for TxPunish { + fn id(&self) -> Txid { + self.inner.txid() + } + + fn script(&self) -> Script { + self.watch_script.clone() + } +} diff --git a/swap/src/bitcoin/redeem.rs b/swap/src/bitcoin/redeem.rs index 0a7888c5..6d14c201 100644 --- a/swap/src/bitcoin/redeem.rs +++ b/swap/src/bitcoin/redeem.rs @@ -1,3 +1,4 @@ +use crate::bitcoin::wallet::Watchable; use crate::bitcoin::{ verify_encsig, verify_sig, Address, EmptyWitnessStack, EncryptedSignature, NoInputs, NotThreeWitnesses, PublicKey, SecretKey, TooManyInputs, Transaction, TxLock, @@ -5,6 +6,7 @@ use crate::bitcoin::{ use ::bitcoin::util::bip143::SigHashCache; use ::bitcoin::{SigHash, SigHashType, Txid}; use anyhow::{bail, Context, Result}; +use bitcoin::Script; use ecdsa_fun::adaptor::{Adaptor, HashTranscript}; use ecdsa_fun::fun::Scalar; use ecdsa_fun::nonce::Deterministic; @@ -13,11 +15,12 @@ use miniscript::{Descriptor, DescriptorTrait}; use sha2::Sha256; use std::collections::HashMap; -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct TxRedeem { inner: Transaction, digest: SigHash, lock_output_descriptor: Descriptor<::bitcoin::PublicKey>, + watch_script: Script, } impl TxRedeem { @@ -37,6 +40,7 @@ impl TxRedeem { inner: tx_redeem, digest, lock_output_descriptor: tx_lock.output_descriptor.clone(), + watch_script: redeem_address.script_pubkey(), } } @@ -130,3 +134,13 @@ impl TxRedeem { Ok(sig) } } + +impl Watchable for TxRedeem { + fn id(&self) -> Txid { + self.txid() + } + + fn script(&self) -> Script { + self.watch_script.clone() + } +} diff --git a/swap/src/bitcoin/refund.rs b/swap/src/bitcoin/refund.rs index 5257d859..3d282c99 100644 --- a/swap/src/bitcoin/refund.rs +++ b/swap/src/bitcoin/refund.rs @@ -1,3 +1,4 @@ +use crate::bitcoin::wallet::Watchable; use crate::bitcoin::{ verify_sig, Address, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs, Transaction, TxCancel, @@ -5,6 +6,7 @@ use crate::bitcoin::{ use ::bitcoin::util::bip143::SigHashCache; use ::bitcoin::{SigHash, SigHashType, Txid}; use anyhow::{bail, Context, Result}; +use bitcoin::Script; use ecdsa_fun::Signature; use miniscript::{Descriptor, DescriptorTrait}; use std::collections::HashMap; @@ -14,6 +16,7 @@ pub struct TxRefund { inner: Transaction, digest: SigHash, cancel_output_descriptor: Descriptor<::bitcoin::PublicKey>, + watch_script: Script, } impl TxRefund { @@ -31,6 +34,7 @@ impl TxRefund { inner: tx_punish, digest, cancel_output_descriptor: tx_cancel.output_descriptor.clone(), + watch_script: refund_address.script_pubkey(), } } @@ -110,3 +114,13 @@ impl TxRefund { Ok(sig) } } + +impl Watchable for TxRefund { + fn id(&self) -> Txid { + self.txid() + } + + fn script(&self) -> Script { + self.watch_script.clone() + } +} diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index f3306d25..480bf68e 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -13,6 +13,7 @@ use reqwest::Url; use std::collections::BTreeMap; use std::convert::TryFrom; use std::fmt; +use std::future::Future; use std::path::Path; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -159,9 +160,22 @@ impl Wallet { /// Broadcast the given transaction to the network and emit a log statement /// if done so successfully. - pub async fn broadcast(&self, transaction: Transaction, kind: &str) -> Result { + /// + /// Returns the transaction ID and a future for when the transaction meets + /// the configured finality confirmations. + pub async fn broadcast( + &self, + transaction: Transaction, + kind: &str, + ) -> Result<(Txid, impl Future> + '_)> { let txid = transaction.txid(); + // to watch for confirmations, watching a single output is enough + let watcher = self.wait_for_transaction_finality( + (txid, transaction.output[0].script_pubkey.clone()), + kind.to_owned(), + ); + self.wallet .lock() .await @@ -172,7 +186,7 @@ impl Wallet { tracing::info!(%txid, "Published Bitcoin {} transaction", kind); - Ok(txid) + Ok((txid, watcher)) } pub async fn sign_and_finalize(&self, psbt: PartiallySignedTransaction) -> Result { @@ -193,20 +207,27 @@ impl Wallet { .ok_or_else(|| anyhow!("Could not get raw tx with id: {}", txid)) } - pub async fn status_of_script(&self, script: &Script, txid: &Txid) -> Result { - self.client.lock().await.status_of_script(script, txid) + pub async fn status_of_script(&self, tx: &T) -> Result + where + T: Watchable, + { + self.client.lock().await.status_of_script(tx) } - pub async fn watch_until_status( + pub async fn watch_until_status( &self, - txid: Txid, - script: Script, + tx: &T, mut status_fn: impl FnMut(ScriptStatus) -> bool, - ) -> Result<()> { + ) -> Result<()> + where + T: Watchable, + { + let txid = tx.id(); + let mut last_status = None; loop { - let new_status = self.client.lock().await.status_of_script(&script, &txid)?; + let new_status = self.client.lock().await.status_of_script(tx)?; if Some(new_status) != last_status { tracing::debug!(%txid, "Transaction is {}", new_status); @@ -223,23 +244,23 @@ impl Wallet { Ok(()) } - pub async fn wait_for_transaction_finality( - &self, - txid: Txid, - script_to_watch: Script, - ) -> Result<()> { + async fn wait_for_transaction_finality(&self, tx: T, kind: String) -> Result<()> + where + T: Watchable, + { let conf_target = self.bitcoin_finality_confirmations; + let txid = tx.id(); - tracing::info!(%txid, "Waiting for {} confirmation{} of Bitcoin transaction", conf_target, if conf_target > 1 { "s" } else { "" }); + tracing::info!(%txid, "Waiting for {} confirmation{} of Bitcoin {} transaction", conf_target, if conf_target > 1 { "s" } else { "" }, kind); let mut seen_confirmations = 0; - self.watch_until_status(txid, script_to_watch, |status| match status { + self.watch_until_status(&tx, |status| match status { ScriptStatus::Confirmed(inner) => { let confirmations = inner.confirmations(); if confirmations > seen_confirmations { - tracing::info!(%txid, "Bitcoin tx has {} out of {} confirmation{}", confirmations, conf_target, if conf_target > 1 { "s" } else { "" }); + tracing::info!(%txid, "Bitcoin {} tx has {} out of {} confirmation{}", kind, confirmations, conf_target, if conf_target > 1 { "s" } else { "" }); seen_confirmations = confirmations; } @@ -260,6 +281,27 @@ impl Wallet { } } +/// Defines a watchable transaction. +/// +/// For a transaction to be watchable, we need to know two things: Its +/// transaction ID and the specific output script that is going to change. +/// A transaction can obviously have multiple outputs but our protocol purposes, +/// we are usually interested in a specific one. +pub trait Watchable { + fn id(&self) -> Txid; + fn script(&self) -> Script; +} + +impl Watchable for (Txid, Script) { + fn id(&self) -> Txid { + self.0 + } + + fn script(&self) -> Script { + self.1.clone() + } +} + struct Client { electrum: bdk::electrum_client::Client, latest_block: BlockHeight, @@ -321,18 +363,24 @@ impl Client { Ok(()) } - fn status_of_script(&mut self, script: &Script, txid: &Txid) -> Result { - if !self.script_history.contains_key(script) { + fn status_of_script(&mut self, tx: &T) -> Result + where + T: Watchable, + { + let txid = tx.id(); + let script = tx.script(); + + if !self.script_history.contains_key(&script) { self.script_history.insert(script.clone(), vec![]); } self.drain_notifications()?; - let history = self.script_history.entry(script.clone()).or_default(); + let history = self.script_history.entry(script).or_default(); let history_of_tx = history .iter() - .filter(|entry| &entry.tx_hash == txid) + .filter(|entry| entry.tx_hash == txid) .collect::>(); match history_of_tx.as_slice() { diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index f3b83881..263aee10 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -321,11 +321,9 @@ impl State3 { bitcoin_wallet: &bitcoin::Wallet, ) -> Result<()> { bitcoin_wallet - .watch_until_status( - self.tx_lock.txid(), - self.tx_lock.script_pubkey(), - |status| status.is_confirmed_with(self.cancel_timelock), - ) + .watch_until_status(&self.tx_lock, |status| { + status.is_confirmed_with(self.cancel_timelock) + }) .await?; Ok(()) @@ -337,12 +335,8 @@ impl State3 { ) -> Result { let tx_cancel = self.tx_cancel(); - let tx_lock_status = bitcoin_wallet - .status_of_script(&self.tx_lock.script_pubkey(), &self.tx_lock.txid()) - .await?; - let tx_cancel_status = bitcoin_wallet - .status_of_script(&tx_cancel.script_pubkey(), &tx_cancel.txid()) - .await?; + let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?; + let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?; Ok(current_epoch( self.cancel_timelock, diff --git a/swap/src/protocol/alice/steps.rs b/swap/src/protocol/alice/steps.rs index d4516a07..3f7b18e8 100644 --- a/swap/src/protocol/alice/steps.rs +++ b/swap/src/protocol/alice/steps.rs @@ -6,7 +6,6 @@ use crate::protocol::alice::event_loop::EventLoopHandle; use crate::protocol::alice::TransferProof; use crate::{bitcoin, monero}; use anyhow::{bail, Context, Result}; -use futures::future::{select, Either}; use futures::pin_mut; use libp2p::PeerId; @@ -61,9 +60,7 @@ pub async fn publish_cancel_transaction( bitcoin_wallet: &bitcoin::Wallet, ) -> Result<()> { bitcoin_wallet - .watch_until_status(tx_lock.txid(), tx_lock.script_pubkey(), |status| { - status.is_confirmed_with(cancel_timelock) - }) + .watch_until_status(&tx_lock, |status| status.is_confirmed_with(cancel_timelock)) .await?; let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B); @@ -85,7 +82,7 @@ pub async fn publish_cancel_transaction( .expect("sig_{a,b} to be valid signatures for tx_cancel"); // TODO(Franck): Error handling is delicate, why can't we broadcast? - bitcoin_wallet.broadcast(tx_cancel, "cancel").await?; + let (..) = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?; // TODO(Franck): Wait until transaction is mined and returned mined // block height @@ -96,35 +93,36 @@ pub async fn publish_cancel_transaction( pub async fn wait_for_bitcoin_refund( tx_cancel: &TxCancel, + tx_refund: &TxRefund, punish_timelock: PunishTimelock, - refund_address: &bitcoin::Address, bitcoin_wallet: &bitcoin::Wallet, -) -> Result<(bitcoin::TxRefund, Option)> { - let tx_refund = bitcoin::TxRefund::new(tx_cancel, refund_address); +) -> Result> { + let refund_tx_id = tx_refund.txid(); + let seen_refund_tx = + bitcoin_wallet.watch_until_status(tx_refund, |status| status.has_been_seen()); - let seen_refund_tx = bitcoin_wallet.watch_until_status( - tx_refund.txid(), - refund_address.script_pubkey(), - |status| status.has_been_seen(), - ); - - let punish_timelock_expired = - bitcoin_wallet.watch_until_status(tx_cancel.txid(), tx_cancel.script_pubkey(), |status| { - status.is_confirmed_with(punish_timelock) - }); + let punish_timelock_expired = bitcoin_wallet.watch_until_status(tx_cancel, |status| { + status.is_confirmed_with(punish_timelock) + }); pin_mut!(punish_timelock_expired); pin_mut!(seen_refund_tx); - match select(punish_timelock_expired, seen_refund_tx).await { - Either::Left(_) => Ok((tx_refund, None)), - Either::Right((Ok(()), _)) => { - let published_refund_tx = bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?; + tokio::select! { + seen_refund = seen_refund_tx => { + match seen_refund { + Ok(()) => { + let published_refund_tx = bitcoin_wallet.get_raw_transaction(refund_tx_id).await?; - Ok((tx_refund, Some(published_refund_tx))) + Ok(Some(published_refund_tx)) + } + Err(e) => { + bail!(e.context("Failed to monitor refund transaction")) + } + } } - Either::Right((Err(e), _)) => { - bail!(e.context("Failed to monitor refund transaction")) + _ = punish_timelock_expired => { + Ok(None) } } } diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index b59a2fd8..2d652416 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -82,20 +82,16 @@ async fn run_until_internal( } => { timeout( execution_params.bob_time_to_act, - bitcoin_wallet.watch_until_status( - state3.tx_lock.txid(), - state3.tx_lock.script_pubkey(), - |status| status.has_been_seen(), - ), + bitcoin_wallet + .watch_until_status(&state3.tx_lock, |status| status.has_been_seen()), ) .await .context("Failed to find lock Bitcoin tx")??; bitcoin_wallet - .wait_for_transaction_finality( - state3.tx_lock.txid(), - state3.tx_lock.script_pubkey(), - ) + .watch_until_status(&state3.tx_lock, |status| { + status.is_confirmed_with(execution_params.bitcoin_finality_confirmations) + }) .await?; let state = AliceState::BtcLocked { @@ -209,30 +205,19 @@ async fn run_until_internal( } => { let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { ExpiredTimelocks::None => { - let tx_redeem = TxRedeem::new(&state3.tx_lock, &state3.redeem_address); - - match tx_redeem.complete( + match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).complete( *encrypted_signature, state3.a.clone(), state3.s_a.to_secpfun_scalar(), state3.B, ) { Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await { - Ok(txid) => { - let publishded_redeem_tx = bitcoin_wallet - .wait_for_transaction_finality( - txid, - state3.redeem_address.script_pubkey(), - ) - .await; - - match publishded_redeem_tx { - Ok(_) => AliceState::BtcRedeemed, - Err(e) => { - bail!("Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e) - } + Ok((_, finality)) => match finality.await { + Ok(_) => AliceState::BtcRedeemed, + Err(e) => { + bail!("Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e) } - } + }, Err(e) => { error!("Publishing the redeem transaction failed with {}, attempting to wait for cancellation now. If you restart the application before the timelock is expired publishing the redeem transaction will be retried.", e); state3 @@ -316,10 +301,10 @@ async fn run_until_internal( state3, monero_wallet_restore_blockheight, } => { - let (tx_refund, published_refund_tx) = wait_for_bitcoin_refund( + let published_refund_tx = wait_for_bitcoin_refund( &state3.tx_cancel(), + &state3.tx_refund(), state3.punish_timelock, - &state3.refund_address, &bitcoin_wallet, ) .await?; @@ -350,7 +335,7 @@ async fn run_until_internal( Some(published_refund_tx) => { let spend_key = extract_monero_private_key( published_refund_tx, - &tx_refund, + &state3.tx_refund(), state3.s_a, state3.a.clone(), state3.S_b_bitcoin, @@ -405,36 +390,30 @@ async fn run_until_internal( state3.B, )?; - let punish_script_pubkey = state3.punish_address.script_pubkey(); - let punish_tx_finalised = async { - let txid = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?; + let (txid, finality) = + bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?; - bitcoin_wallet - .wait_for_transaction_finality(txid, punish_script_pubkey) - .await?; + finality.await?; Result::<_, anyhow::Error>::Ok(txid) }; - let refund_tx_seen = bitcoin_wallet.watch_until_status( - state3.tx_refund().txid(), - state3.refund_address.script_pubkey(), - |status| status.has_been_seen(), - ); + let tx_refund = state3.tx_refund(); + let refund_tx_seen = + bitcoin_wallet.watch_until_status(&tx_refund, |status| status.has_been_seen()); pin_mut!(punish_tx_finalised); pin_mut!(refund_tx_seen); match select(refund_tx_seen, punish_tx_finalised).await { Either::Left((Ok(()), _)) => { - let published_refund_tx = bitcoin_wallet - .get_raw_transaction(state3.tx_refund().txid()) - .await?; + let published_refund_tx = + bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?; let spend_key = extract_monero_private_key( published_refund_tx, - &state3.tx_refund(), + &tx_refund, state3.s_a, state3.a.clone(), state3.S_b_bitcoin, diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index d723b9c2..7d288a2e 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -350,11 +350,9 @@ impl State3 { bitcoin_wallet: &bitcoin::Wallet, ) -> Result<()> { bitcoin_wallet - .watch_until_status( - self.tx_lock.txid(), - self.tx_lock.script_pubkey(), - |status| status.is_confirmed_with(self.cancel_timelock), - ) + .watch_until_status(&self.tx_lock, |status| { + status.is_confirmed_with(self.cancel_timelock) + }) .await?; Ok(()) } @@ -389,12 +387,8 @@ impl State3 { ) -> Result { let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); - let tx_lock_status = bitcoin_wallet - .status_of_script(&self.tx_lock.script_pubkey(), &self.tx_lock.txid()) - .await?; - let tx_cancel_status = bitcoin_wallet - .status_of_script(&tx_cancel.script_pubkey(), &tx_cancel.txid()) - .await?; + let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?; + let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?; Ok(current_epoch( self.cancel_timelock, @@ -454,14 +448,13 @@ impl State4 { let sig_b = self.b.sign(tx_cancel.digest()); let tx_cancel = tx_cancel - .clone() .add_signatures((self.A, sig_a), (self.b.public(), sig_b)) .expect( "sig_{a,b} to be valid signatures for tx_cancel", ); - let tx_id = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?; + let (tx_id, _) = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?; Ok(tx_id) } @@ -471,11 +464,7 @@ impl State4 { let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest()); bitcoin_wallet - .watch_until_status( - tx_redeem.txid(), - self.redeem_address.script_pubkey(), - |status| status.has_been_seen(), - ) + .watch_until_status(&tx_redeem, |status| status.has_been_seen()) .await?; let tx_redeem_candidate = bitcoin_wallet.get_raw_transaction(tx_redeem.txid()).await?; @@ -499,11 +488,9 @@ impl State4 { bitcoin_wallet: &bitcoin::Wallet, ) -> Result<()> { bitcoin_wallet - .watch_until_status( - self.tx_lock.txid(), - self.tx_lock.script_pubkey(), - |status| status.is_confirmed_with(self.cancel_timelock), - ) + .watch_until_status(&self.tx_lock, |status| { + status.is_confirmed_with(self.cancel_timelock) + }) .await?; Ok(()) @@ -515,12 +502,8 @@ impl State4 { ) -> Result { let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); - let tx_lock_status = bitcoin_wallet - .status_of_script(&self.tx_lock.script_pubkey(), &self.tx_lock.txid()) - .await?; - let tx_cancel_status = bitcoin_wallet - .status_of_script(&tx_cancel.script_pubkey(), &tx_cancel.txid()) - .await?; + let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?; + let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?; Ok(current_epoch( self.cancel_timelock, @@ -544,11 +527,9 @@ impl State4 { let signed_tx_refund = tx_refund.add_signatures((self.A, sig_a), (self.b.public(), sig_b))?; - let txid = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?; + let (_, finality) = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?; - bitcoin_wallet - .wait_for_transaction_finality(txid, self.refund_address.script_pubkey()) - .await?; + finality.await?; Ok(()) } diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 75db537d..197e5fd9 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -104,12 +104,10 @@ async fn run_until_internal( .sign_and_finalize(tx_lock.clone().into()) .await .context("Failed to sign Bitcoin lock transaction")?; - let tx_lock_id = bitcoin_wallet.broadcast(signed_tx, "lock").await?; + let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?; bitcoin_wallet - .watch_until_status(tx_lock_id, tx_lock.script_pubkey(), |status| { - status.is_confirmed() - }) + .watch_until_status(&tx_lock, |status| status.is_confirmed()) .await?; let state = BobState::BtcLocked(state3);