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 da9a3f0e..d11af5cb 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -152,8 +152,8 @@ 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, + 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 771621e4..dabed242 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!( @@ -112,9 +106,9 @@ async fn main() -> Result<()> { let bitcoin_wallet = init_bitcoin_wallet( bitcoin_network, electrum_rpc_url, - electrum_http_url, seed, data_dir.clone(), + execution_params, ) .await?; let (monero_wallet, _process) = init_monero_wallet( @@ -196,11 +190,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!("The given monero address is on network {:?}, expected address of network {:?}.", receive_monero_address.network, monero_network) @@ -209,9 +199,9 @@ async fn main() -> Result<()> { let bitcoin_wallet = init_bitcoin_wallet( bitcoin_network, electrum_rpc_url, - electrum_http_url, seed, data_dir.clone(), + execution_params, ) .await?; let (monero_wallet, _process) = init_monero_wallet( @@ -255,18 +245,14 @@ 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, + execution_params, ) .await?; @@ -290,32 +276,20 @@ 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, + 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(()) @@ -324,16 +298,16 @@ 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, + execution_params: ExecutionParams, ) -> Result { let wallet_dir = data_dir.join("wallet"); let wallet = bitcoin::Wallet::new( electrum_rpc_url.clone(), - electrum_http_url.clone(), network, + execution_params.bitcoin_finality_confirmations, &wallet_dir, seed.derive_extended_private_key(network)?, ) diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 03966199..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}; @@ -110,6 +111,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) @@ -209,46 +219,21 @@ pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Resu Ok(s) } -pub async fn poll_until_block_height_is_gte( - client: &crate::bitcoin::Wallet, - target: BlockHeight, -) -> Result<()> { - while client.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)] @@ -266,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..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, @@ -5,9 +6,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 +36,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,7 +70,19 @@ impl Add for BlockHeight { } } -#[derive(Debug, Clone)] +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)] pub struct TxCancel { inner: Transaction, digest: SigHash, @@ -180,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 f9ef3814..d2d5e7ca 100644 --- a/swap/src/bitcoin/lock.rs +++ b/swap/src/bitcoin/lock.rs @@ -1,9 +1,11 @@ +use crate::bitcoin::wallet::Watchable; use crate::bitcoin::{ build_shared_output_descriptor, Address, Amount, PublicKey, Transaction, Wallet, TX_FEE, }; 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 +57,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 { @@ -100,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 a8d1c7d6..4845429d 100644 --- a/swap/src/bitcoin/punish.rs +++ b/swap/src/bitcoin/punish.rs @@ -1,8 +1,9 @@ -use crate::bitcoin::{Address, PublicKey, 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::Result; -use ecdsa_fun::Signature; +use anyhow::{Context, Result}; +use bdk::bitcoin::Script; use miniscript::{Descriptor, DescriptorTrait}; use std::collections::HashMap; @@ -11,6 +12,7 @@ pub struct TxPunish { inner: Transaction, digest: SigHash, cancel_output_descriptor: Descriptor<::bitcoin::PublicKey>, + watch_script: Script, } impl TxPunish { @@ -32,6 +34,7 @@ impl TxPunish { inner: tx_punish, digest, cancel_output_descriptor: tx_cancel.output_descriptor.clone(), + watch_script: punish_address.script_pubkey(), } } @@ -39,22 +42,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,8 +66,19 @@ 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) } } + +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 bc670da4..6d14c201 100644 --- a/swap/src/bitcoin/redeem.rs +++ b/swap/src/bitcoin/redeem.rs @@ -1,19 +1,26 @@ +use crate::bitcoin::wallet::Watchable; 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 bitcoin::Script; +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)] +#[derive(Debug)] pub struct TxRedeem { inner: Transaction, digest: SigHash, lock_output_descriptor: Descriptor<::bitcoin::PublicKey>, + watch_script: Script, } impl TxRedeem { @@ -33,6 +40,7 @@ impl TxRedeem { inner: tx_redeem, digest, lock_output_descriptor: tx_lock.output_descriptor.clone(), + watch_script: redeem_address.script_pubkey(), } } @@ -44,17 +52,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 +90,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( @@ -112,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/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..480bf68e 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -1,60 +1,46 @@ 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}; -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::future::Future; 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>>, + bitcoin_finality_confirmations: u32, } impl Wallet { pub async fn new( electrum_rpc_url: Url, - electrum_http_url: Url, network: bitcoin::Network, + bitcoin_finality_confirmations: u32, wallet_dir: &Path, key: impl DerivableKey + Clone, ) -> Result { // 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 +52,21 @@ 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)?)), + bitcoin_finality_confirmations, }) } pub async fn balance(&self) -> Result { let balance = self - .inner + .wallet .lock() .await .get_balance() @@ -86,7 +77,7 @@ impl Wallet { pub async fn new_address(&self) -> Result
{ let address = self - .inner + .wallet .lock() .await .get_new_address() @@ -96,13 +87,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 +109,7 @@ impl Wallet { } pub async fn sync(&self) -> Result<()> { - self.inner + self.wallet .lock() .await .sync(noop_progress(), None) @@ -131,7 +123,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 +139,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,15 +155,28 @@ 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 /// 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(); - self.inner + // 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 .broadcast(transaction) @@ -181,11 +186,11 @@ impl Wallet { tracing::info!(%txid, "Published Bitcoin {} transaction", kind); - Ok(txid) + Ok((txid, watcher)) } 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,110 +207,72 @@ 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, tx: &T) -> Result + where + T: Watchable, + { + self.client.lock().await.status_of_script(tx) } - pub async fn get_block_height(&self) -> Result { - let url = make_blocks_tip_height_url(&self.http_url)?; - - 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")?; - - Ok(BlockHeight::new(height)) - } - - pub async fn transaction_block_height(&self, txid: Txid) -> Result { - let status_url = make_tx_status_url(&self.http_url, txid)?; - - #[derive(Serialize, Deserialize, Debug, Clone)] - struct TransactionStatus { - block_height: Option, - confirmed: bool, - } - 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)) - } - - pub async fn wait_for_transaction_finality( + pub async fn watch_until_status( &self, - txid: Txid, - execution_params: ExecutionParams, - ) -> Result<()> { - let conf_target = execution_params.bitcoin_finality_confirmations; + tx: &T, + mut status_fn: impl FnMut(ScriptStatus) -> bool, + ) -> Result<()> + where + T: Watchable, + { + let txid = tx.id(); - 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 last_status = None; loop { - let tx_block_height = self.transaction_block_height(txid).await?; - tracing::debug!("tx_block_height: {:?}", tx_block_height); + let new_status = self.client.lock().await.status_of_script(tx)?; - 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 Some(new_status) != last_status { + tracing::debug!(%txid, "Transaction is {}", new_status); } - interval.tick().await; + last_status = Some(new_status); + + if status_fn(new_status) { + break; + } + + tokio::time::sleep(Duration::from_secs(5)).await; } Ok(()) } + 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 { "" }, kind); + + let mut seen_confirmations = 0; + + 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{}", kind, confirmations, conf_target, if conf_target > 1 { "s" } else { "" }); + seen_confirmations = confirmations; + } + + inner.meets_target(conf_target) + }, + _ => false + }) + .await?; + + Ok(()) + } + /// Selects an appropriate [`FeeRate`] to be used for getting transactions /// confirmed within a reasonable amount of time. fn select_feerate(&self) -> FeeRate { @@ -314,42 +281,285 @@ impl Wallet { } } -fn make_tx_status_url(base_url: &Url, txid: Txid) -> Result { - let url = base_url.join(&format!("tx/{}/status", txid))?; - - Ok(url) +/// 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; } -fn make_blocks_tip_height_url(base_url: &Url) -> Result { - let url = base_url.join("blocks/tip/height")?; +impl Watchable for (Txid, Script) { + fn id(&self) -> Txid { + self.0 + } - Ok(url) + fn script(&self) -> Script { + self.1.clone() + } +} + +struct Client { + electrum: bdk::electrum_client::Client, + latest_block: BlockHeight, + last_ping: Instant, + interval: Duration, + script_history: BTreeMap>, +} + +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(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, 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).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/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 e4c23bfd..263aee10 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, TxRefund, + current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxCancel, TxPunish, TxRefund, }; use crate::execution_params::ExecutionParams; use crate::protocol::alice::{Message1, Message3}; @@ -37,7 +36,6 @@ pub enum AliceState { BtcRedeemed, BtcCancelled { monero_wallet_restore_blockheight: BlockHeight, - tx_cancel: Box, state3: Box, }, BtcRefunded { @@ -47,7 +45,6 @@ pub enum AliceState { }, BtcPunishable { monero_wallet_restore_blockheight: BlockHeight, - tx_refund: Box, state3: Box, }, XmrRefunded, @@ -323,24 +320,45 @@ 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, |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 = self.tx_cancel(); + + 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, self.punish_timelock, - self.tx_lock.txid(), + tx_lock_status, + tx_cancel_status, + )) + } + + pub fn tx_cancel(&self) -> TxCancel { + TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B) + } + + pub fn tx_punish(&self) -> TxPunish { + bitcoin::TxPunish::new( + &self.tx_cancel(), + &self.punish_address, + self.punish_timelock, ) - .await + } + + 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 a0e619c8..3f7b18e8 100644 --- a/swap/src/protocol/alice/steps.rs +++ b/swap/src/protocol/alice/steps.rs @@ -1,43 +1,13 @@ use crate::bitcoin::{ - poll_until_block_height_is_gte, BlockHeight, CancelTimelock, EncryptedSignature, - PunishTimelock, TxCancel, TxLock, TxRefund, + 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; 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 anyhow::{bail, Context, Result}; 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, @@ -81,36 +51,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, @@ -118,12 +58,10 @@ pub async fn publish_cancel_transaction( cancel_timelock: CancelTimelock, 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()) +) -> Result<()> { + bitcoin_wallet + .watch_until_status(&tx_lock, |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); @@ -140,42 +78,52 @@ 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"); // 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 } - Ok(tx_cancel) + Ok(()) } pub async fn wait_for_bitcoin_refund( tx_cancel: &TxCancel, - cancel_tx_height: BlockHeight, + tx_refund: &TxRefund, 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); +) -> 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 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 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((published_refund_tx, _)) => Ok((tx_refund, Some(published_refund_tx?))), + 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(Some(published_refund_tx)) + } + Err(e) => { + bail!(e.context("Failed to monitor refund transaction")) + } + } + } + _ = punish_timelock_expired => { + Ok(None) + } } } @@ -201,25 +149,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 28f3d751..2d652416 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -1,24 +1,24 @@ //! 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, wait_for_locked_bitcoin, + 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}; -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 +80,19 @@ 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_until_status(&state3.tx_lock, |status| status.has_been_seen()), ) - .await?; + .await + .context("Failed to find lock Bitcoin tx")??; + + bitcoin_wallet + .watch_until_status(&state3.tx_lock, |status| { + status.is_confirmed_with(execution_params.bitcoin_finality_confirmations) + }) + .await?; let state = AliceState::BtcLocked { bob_peer_id, @@ -198,27 +205,19 @@ async fn run_until_internal( } => { let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { ExpiredTimelocks::None => { - match build_bitcoin_redeem_transaction( + match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).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) => { - let publishded_redeem_tx = bitcoin_wallet - .wait_for_transaction_finality(txid, execution_params) - .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 @@ -269,7 +268,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, @@ -281,7 +280,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(); @@ -301,18 +299,12 @@ async fn run_until_internal( } AliceState::BtcCancelled { state3, - 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, + let published_refund_tx = wait_for_bitcoin_refund( + &state3.tx_cancel(), + &state3.tx_refund(), state3.punish_timelock, - &state3.refund_address, &bitcoin_wallet, ) .await?; @@ -321,7 +313,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, }; @@ -344,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, @@ -390,39 +381,38 @@ async fn run_until_internal( Ok(state) } AliceState::BtcPunishable { - tx_refund, 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, )?; 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, execution_params) - .await?; + finality.await?; Result::<_, anyhow::Error>::Ok(txid) }; - let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); + 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((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 +438,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/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 1588df50..7d288a2e 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -1,15 +1,14 @@ 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; use crate::monero::{monero_private_key, InsufficientFunds, TransferProof}; 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 +261,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 +349,12 @@ 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, |status| { + status.is_confirmed_with(self.cancel_timelock) + }) + .await?; + Ok(()) } pub fn cancel(&self) -> State4 { @@ -390,13 +385,17 @@ 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).await?; + let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?; + + Ok(current_epoch( self.cancel_timelock, self.punish_timelock, - self.tx_lock.txid(), - ) - .await + tx_lock_status, + tx_cancel_status, + )) } } @@ -419,10 +418,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 { @@ -437,17 +435,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) @@ -461,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) } @@ -477,10 +463,12 @@ 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, |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)?; @@ -499,32 +487,33 @@ 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, |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).await?; + let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).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( - &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); @@ -538,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, execution_params) - .await?; + finality.await?; Ok(()) } diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 30f84ec6..197e5fd9 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,16 @@ 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 (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?; + + bitcoin_wallet + .watch_until_status(&tx_lock, |status| status.is_confirmed()) + .await?; let state = BobState::BtcLocked(state3); let db_state = state.clone().into(); @@ -371,9 +380,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 240482bc..df0783c0 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 80256941..8edf705d 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 @@ -42,7 +42,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 6f659b9e..ff46d683 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 @@ -40,7 +40,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 0f80fb69..174c46d7 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -339,10 +339,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(); @@ -354,7 +350,6 @@ where alice_starting_balances.clone(), tempdir().unwrap().path(), electrs_rpc_port, - electrs_http_port, alice_seed, execution_params, ) @@ -377,7 +372,6 @@ where bob_starting_balances.clone(), tempdir().unwrap().path(), electrs_rpc_port, - electrs_http_port, bob_seed, execution_params, ) @@ -585,7 +579,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) { @@ -605,15 +598,11 @@ 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, + execution_params.bitcoin_finality_confirmations, datadir, seed.derive_extended_private_key(bitcoin::Network::Regtest) .expect("Could not create extended private key from seed"), @@ -675,26 +664,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() }