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)