diff --git a/Cargo.lock b/Cargo.lock index d578daff..c100705f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -323,6 +323,21 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitcoin" version = "0.26.0" @@ -2692,6 +2707,26 @@ dependencies = [ "unicode-xid 0.2.1", ] +[[package]] +name = "proptest" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5" +dependencies = [ + "bit-set", + "bitflags", + "byteorder", + "lazy_static", + "num-traits", + "quick-error 2.0.0", + "rand 0.8.3", + "rand_chacha 0.3.0", + "rand_xorshift 0.3.0", + "regex-syntax", + "rusty-fork", + "tempfile", +] + [[package]] name = "prost" version = "0.7.0" @@ -2749,6 +2784,12 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-error" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ac73b1112776fc109b2e61909bc46c7e1bf0d7f690ffb1676553acce16d5cda" + [[package]] name = "quicksink" version = "0.1.2" @@ -2806,7 +2847,7 @@ dependencies = [ "rand_jitter", "rand_os", "rand_pcg", - "rand_xorshift", + "rand_xorshift 0.1.1", "winapi 0.3.9", ] @@ -2978,6 +3019,15 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.2", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -3103,7 +3153,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" dependencies = [ "hostname", - "quick-error", + "quick-error 1.2.3", ] [[package]] @@ -3197,6 +3247,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb5d2a036dc6d2d8fd16fde3498b04306e29bd193bf306a57427019b823d5acd" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + [[package]] name = "rw-stream-sink" version = "0.2.1" @@ -3762,6 +3824,7 @@ dependencies = [ "pem", "port_check", "prettytable-rs", + "proptest", "rand 0.7.3", "rand_chacha 0.2.2", "reqwest", @@ -4492,6 +4555,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "want" version = "0.3.0" diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 88c782dd..382fc526 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -36,6 +36,7 @@ monero = { version = "0.12", features = [ "serde_support" ] } monero-rpc = { path = "../monero-rpc" } pem = "0.8" prettytable-rs = "0.8" +proptest = "1" rand = "0.7" rand_chacha = "0.2" reqwest = { version = "0.11", features = [ "rustls-tls", "stream", "socks" ], default-features = false } diff --git a/swap/src/asb/config.rs b/swap/src/asb/config.rs index 2c1e86e9..7657916d 100644 --- a/swap/src/asb/config.rs +++ b/swap/src/asb/config.rs @@ -16,6 +16,7 @@ const DEFAULT_LISTEN_ADDRESS_TCP: &str = "/ip4/0.0.0.0/tcp/9939"; const DEFAULT_LISTEN_ADDRESS_WS: &str = "/ip4/0.0.0.0/tcp/9940/ws"; 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"; +const DEFAULT_BITCOIN_CONFIRMATION_TARGET: usize = 3; #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] pub struct Config { @@ -55,6 +56,7 @@ pub struct Network { #[serde(deny_unknown_fields)] pub struct Bitcoin { pub electrum_rpc_url: Url, + pub target_block: usize, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] @@ -148,6 +150,10 @@ pub fn query_user_for_initial_testnet_config() -> Result { .interact_text()?; let data_dir = data_dir.as_str().parse()?; + let target_block = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("How fast should your Bitcoin transactions be confirmed? Your transaction fee will be calculated based on this target. Hit return to use default") + .default(DEFAULT_BITCOIN_CONFIRMATION_TARGET) + .interact_text()?; let listen_addresses = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter multiaddresses (comma separated) on which asb should list for peer-to-peer communications or hit return to use default") .default( format!("{},{}", DEFAULT_LISTEN_ADDRESS_TCP, DEFAULT_LISTEN_ADDRESS_WS)) @@ -186,7 +192,10 @@ pub fn query_user_for_initial_testnet_config() -> Result { network: Network { listen: listen_addresses, }, - bitcoin: Bitcoin { electrum_rpc_url }, + bitcoin: Bitcoin { + electrum_rpc_url, + target_block, + }, monero: Monero { wallet_rpc_url: monero_wallet_rpc_url, }, @@ -214,6 +223,7 @@ mod tests { }, bitcoin: Bitcoin { electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), + target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET, }, network: Network { listen: vec![ diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index 663cf266..4d99aadb 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -211,6 +211,7 @@ async fn init_bitcoin_wallet( &wallet_dir, seed.derive_extended_private_key(env_config.bitcoin_network)?, env_config, + config.bitcoin.target_block, ) .await .context("Failed to initialize Bitcoin wallet")?; diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index eb5e9322..bb43c1a9 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -52,6 +52,7 @@ async fn main() -> Result<()> { }, electrum_rpc_url, tor_socks5_port, + bitcoin_target_block, } => { let swap_id = Uuid::new_v4(); @@ -71,8 +72,14 @@ async fn main() -> Result<()> { ) } - let bitcoin_wallet = - init_bitcoin_wallet(electrum_rpc_url, &seed, data_dir.clone(), env_config).await?; + let bitcoin_wallet = init_bitcoin_wallet( + electrum_rpc_url, + &seed, + data_dir.clone(), + env_config, + bitcoin_target_block, + ) + .await?; let (monero_wallet, _process) = init_monero_wallet(data_dir, monero_daemon_host, env_config).await?; let bitcoin_wallet = Arc::new(bitcoin_wallet); @@ -154,6 +161,7 @@ async fn main() -> Result<()> { }, electrum_rpc_url, tor_socks5_port, + bitcoin_target_block, } => { let data_dir = data.0; cli::tracing::init(debug, data_dir.join("logs"), swap_id)?; @@ -167,8 +175,14 @@ async fn main() -> Result<()> { bail!("The given monero address is on network {:?}, expected address of network {:?}.", receive_monero_address.network, env_config.monero_network) } - let bitcoin_wallet = - init_bitcoin_wallet(electrum_rpc_url, &seed, data_dir.clone(), env_config).await?; + let bitcoin_wallet = init_bitcoin_wallet( + electrum_rpc_url, + &seed, + data_dir.clone(), + env_config, + bitcoin_target_block, + ) + .await?; let (monero_wallet, _process) = init_monero_wallet(data_dir, monero_daemon_host, env_config).await?; let bitcoin_wallet = Arc::new(bitcoin_wallet); @@ -209,6 +223,7 @@ async fn main() -> Result<()> { swap_id, force, electrum_rpc_url, + bitcoin_target_block, } => { let data_dir = data.0; cli::tracing::init(debug, data_dir.join("logs"), swap_id)?; @@ -218,8 +233,14 @@ async fn main() -> Result<()> { .context("Failed to read in seed file")?; let env_config = env::Testnet::get_config(); - let bitcoin_wallet = - init_bitcoin_wallet(electrum_rpc_url, &seed, data_dir, env_config).await?; + let bitcoin_wallet = init_bitcoin_wallet( + electrum_rpc_url, + &seed, + data_dir, + env_config, + bitcoin_target_block, + ) + .await?; let resume_state = db.get_state(swap_id)?.try_into_bob()?.into(); let cancel = @@ -239,6 +260,7 @@ async fn main() -> Result<()> { swap_id, force, electrum_rpc_url, + bitcoin_target_block, } => { let data_dir = data.0; cli::tracing::init(debug, data_dir.join("logs"), swap_id)?; @@ -248,8 +270,14 @@ async fn main() -> Result<()> { .context("Failed to read in seed file")?; let env_config = env::Testnet::get_config(); - let bitcoin_wallet = - init_bitcoin_wallet(electrum_rpc_url, &seed, data_dir, env_config).await?; + let bitcoin_wallet = init_bitcoin_wallet( + electrum_rpc_url, + &seed, + data_dir, + env_config, + bitcoin_target_block, + ) + .await?; let resume_state = db.get_state(swap_id)?.try_into_bob()?.into(); @@ -264,6 +292,7 @@ async fn init_bitcoin_wallet( seed: &Seed, data_dir: PathBuf, env_config: Config, + bitcoin_target_block: usize, ) -> Result { let wallet_dir = data_dir.join("wallet"); @@ -272,6 +301,7 @@ async fn init_bitcoin_wallet( &wallet_dir, seed.derive_extended_private_key(env_config.bitcoin_network)?, env_config, + bitcoin_target_block, ) .await .context("Failed to initialize Bitcoin wallet")?; diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 84434ffe..654b9860 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -37,15 +37,6 @@ use serde::{Deserialize, Serialize}; use sha2::Sha256; use std::str::FromStr; -// TODO: Configurable tx-fee (note: parties have to agree prior to swapping) -// Current reasoning: -// tx with largest weight (as determined by get_weight() upon broadcast in e2e -// test) = 609 assuming segwit and 60 sat/vB: -// (609 / 4) * 60 (sat/vB) = 9135 sats -// Recommended: Overpay a bit to ensure we don't have to wait too long for test -// runs. -pub const TX_FEE: u64 = 15_000; - #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct SecretKey { inner: Scalar, @@ -263,6 +254,12 @@ pub struct NotThreeWitnesses(usize); #[cfg(test)] mod tests { use super::*; + use crate::bitcoin::wallet::EstimateFeeRate; + use crate::env::{GetConfig, Regtest}; + use crate::protocol::{alice, bob}; + use bdk::FeeRate; + use rand::rngs::OsRng; + use uuid::Uuid; #[test] fn lock_confirmations_le_to_cancel_timelock_no_timelock_expired() { @@ -308,4 +305,128 @@ mod tests { assert_eq!(expired_timelock, ExpiredTimelocks::Punish) } + + struct StaticFeeRate {} + impl EstimateFeeRate for StaticFeeRate { + fn estimate_feerate(&self, _target_block: usize) -> Result { + Ok(FeeRate::default_min_relay_fee()) + } + + fn min_relay_fee(&self) -> Result { + Ok(bitcoin::Amount::from_sat(1_000)) + } + } + + #[tokio::test] + async fn calculate_transaction_weights() { + let alice_wallet = Wallet::new_funded(Amount::ONE_BTC.as_sat(), StaticFeeRate {}); + let bob_wallet = Wallet::new_funded(Amount::ONE_BTC.as_sat(), StaticFeeRate {}); + let spending_fee = Amount::from_sat(1_000); + let btc_amount = Amount::from_sat(500_000); + let xmr_amount = crate::monero::Amount::from_piconero(10000); + + let tx_redeem_fee = alice_wallet + .estimate_fee(TxRedeem::weight(), btc_amount) + .await + .unwrap(); + let tx_punish_fee = alice_wallet + .estimate_fee(TxPunish::weight(), btc_amount) + .await + .unwrap(); + let redeem_address = alice_wallet.new_address().await.unwrap(); + let punish_address = alice_wallet.new_address().await.unwrap(); + + let config = Regtest::get_config(); + let alice_state0 = alice::State0::new( + btc_amount, + xmr_amount, + config, + redeem_address, + punish_address, + tx_redeem_fee, + tx_punish_fee, + &mut OsRng, + ) + .unwrap(); + + let bob_state0 = bob::State0::new( + Uuid::new_v4(), + &mut OsRng, + btc_amount, + xmr_amount, + config.bitcoin_cancel_timelock, + config.bitcoin_punish_timelock, + bob_wallet.new_address().await.unwrap(), + config.monero_finality_confirmations, + spending_fee, + spending_fee, + ); + + let message0 = bob_state0.next_message(); + + let (_, alice_state1) = alice_state0.receive(message0).unwrap(); + let alice_message1 = alice_state1.next_message(); + + let bob_state1 = bob_state0 + .receive(&bob_wallet, alice_message1) + .await + .unwrap(); + let bob_message2 = bob_state1.next_message(); + + let alice_state2 = alice_state1.receive(bob_message2).unwrap(); + let alice_message3 = alice_state2.next_message(); + + let bob_state2 = bob_state1.receive(alice_message3).unwrap(); + let bob_message4 = bob_state2.next_message(); + + let alice_state3 = alice_state2.receive(bob_message4).unwrap(); + + let (bob_state3, _tx_lock) = bob_state2.lock_btc().await.unwrap(); + let bob_state4 = bob_state3.xmr_locked(monero_rpc::wallet::BlockHeight { height: 0 }); + let encrypted_signature = bob_state4.tx_redeem_encsig(); + let bob_state6 = bob_state4.cancel(); + + let cancel_transaction = alice_state3.signed_cancel_transaction().unwrap(); + let punish_transaction = alice_state3.signed_punish_transaction().unwrap(); + let redeem_transaction = alice_state3 + .signed_redeem_transaction(encrypted_signature) + .unwrap(); + let refund_transaction = bob_state6.signed_refund_transaction().unwrap(); + + assert_weight( + redeem_transaction.get_weight(), + TxRedeem::weight(), + "TxRedeem", + ); + assert_weight( + cancel_transaction.get_weight(), + TxCancel::weight(), + "TxCancel", + ); + assert_weight( + punish_transaction.get_weight(), + TxPunish::weight(), + "TxPunish", + ); + assert_weight( + refund_transaction.get_weight(), + TxRefund::weight(), + "TxRefund", + ); + } + + // Weights fluctuate -+1 wu because of the length of the signatures. + // Some of our transactions have 2 signatures and hence the weight can fluctuate + // +-2 + fn assert_weight(is_weight: usize, expected_weight: usize, tx_name: &str) { + assert!( + is_weight + 1 == expected_weight + || is_weight + 2 == expected_weight + || is_weight == expected_weight, + "{} to have weight {}, but was {}", + tx_name, + expected_weight, + is_weight + ) + } } diff --git a/swap/src/bitcoin/cancel.rs b/swap/src/bitcoin/cancel.rs index d1d0bc43..85cc233a 100644 --- a/swap/src/bitcoin/cancel.rs +++ b/swap/src/bitcoin/cancel.rs @@ -2,7 +2,6 @@ use crate::bitcoin; use crate::bitcoin::wallet::Watchable; use crate::bitcoin::{ build_shared_output_descriptor, Address, Amount, BlockHeight, PublicKey, Transaction, TxLock, - TX_FEE, }; use ::bitcoin::util::bip143::SigHashCache; use ::bitcoin::{OutPoint, Script, SigHash, SigHashType, TxIn, TxOut, Txid}; @@ -96,6 +95,7 @@ impl TxCancel { cancel_timelock: CancelTimelock, A: PublicKey, B: PublicKey, + spending_fee: Amount, ) -> Self { let cancel_output_descriptor = build_shared_output_descriptor(A.0, B.0); @@ -107,7 +107,7 @@ impl TxCancel { }; let tx_out = TxOut { - value: tx_lock.lock_amount().as_sat() - TX_FEE, + value: tx_lock.lock_amount().as_sat() - spending_fee.as_sat(), script_pubkey: cancel_output_descriptor.script_pubkey(), }; @@ -216,6 +216,7 @@ impl TxCancel { &self, spend_address: &Address, sequence: Option, + spending_fee: Amount, ) -> Transaction { let previous_output = self.as_outpoint(); @@ -227,7 +228,7 @@ impl TxCancel { }; let tx_out = TxOut { - value: self.amount().as_sat() - TX_FEE, + value: self.amount().as_sat() - spending_fee.as_sat(), script_pubkey: spend_address.script_pubkey(), }; @@ -238,6 +239,10 @@ impl TxCancel { output: vec![tx_out], } } + + pub fn weight() -> usize { + 596 + } } impl Watchable for TxCancel { diff --git a/swap/src/bitcoin/lock.rs b/swap/src/bitcoin/lock.rs index 63f2fb53..5d7933d6 100644 --- a/swap/src/bitcoin/lock.rs +++ b/swap/src/bitcoin/lock.rs @@ -1,6 +1,6 @@ -use crate::bitcoin::wallet::Watchable; +use crate::bitcoin::wallet::{EstimateFeeRate, Watchable}; use crate::bitcoin::{ - build_shared_output_descriptor, Address, Amount, PublicKey, Transaction, Wallet, TX_FEE, + build_shared_output_descriptor, Address, Amount, PublicKey, Transaction, Wallet, }; use ::bitcoin::util::psbt::PartiallySignedTransaction; use ::bitcoin::{OutPoint, TxIn, TxOut, Txid}; @@ -26,6 +26,7 @@ impl TxLock { B: PublicKey, ) -> Result where + C: EstimateFeeRate, D: BatchDatabase, { let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0); @@ -136,6 +137,7 @@ impl TxLock { &self, spend_address: &Address, sequence: Option, + spending_fee: Amount, ) -> Transaction { let previous_output = self.as_outpoint(); @@ -146,8 +148,11 @@ impl TxLock { witness: Vec::new(), }; + let spending_fee = spending_fee.as_sat(); + tracing::debug!(%spending_fee, "Redeem tx fee"); let tx_out = TxOut { - value: self.inner.clone().extract_tx().output[self.lock_output_vout()].value - TX_FEE, + value: self.inner.clone().extract_tx().output[self.lock_output_vout()].value + - spending_fee, script_pubkey: spend_address.script_pubkey(), }; @@ -179,11 +184,23 @@ impl Watchable for TxLock { #[cfg(test)] mod tests { use super::*; + use bdk::FeeRate; + + struct StaticFeeRate {} + impl EstimateFeeRate for StaticFeeRate { + fn estimate_feerate(&self, _target_block: usize) -> Result { + Ok(FeeRate::default_min_relay_fee()) + } + + fn min_relay_fee(&self) -> Result { + Ok(bitcoin::Amount::from_sat(1_000)) + } + } #[tokio::test] async fn given_bob_sends_good_psbt_when_reconstructing_then_succeeeds() { let (A, B) = alice_and_bob(); - let wallet = Wallet::new_funded(50000); + let wallet = Wallet::new_funded(50000, StaticFeeRate {}); let agreed_amount = Amount::from_sat(10000); let psbt = bob_make_psbt(A, B, &wallet, agreed_amount).await; @@ -197,7 +214,7 @@ mod tests { let (A, B) = alice_and_bob(); let fees = 610; let agreed_amount = Amount::from_sat(10000); - let wallet = Wallet::new_funded(agreed_amount.as_sat() + fees); + let wallet = Wallet::new_funded(agreed_amount.as_sat() + fees, StaticFeeRate {}); let psbt = bob_make_psbt(A, B, &wallet, agreed_amount).await; assert_eq!( @@ -213,7 +230,7 @@ mod tests { #[tokio::test] async fn given_bob_is_sending_less_than_agreed_when_reconstructing_txlock_then_fails() { let (A, B) = alice_and_bob(); - let wallet = Wallet::new_funded(50000); + let wallet = Wallet::new_funded(50000, StaticFeeRate {}); let agreed_amount = Amount::from_sat(10000); let bad_amount = Amount::from_sat(5000); @@ -226,7 +243,7 @@ mod tests { #[tokio::test] async fn given_bob_is_sending_to_a_bad_output_reconstructing_txlock_then_fails() { let (A, B) = alice_and_bob(); - let wallet = Wallet::new_funded(50000); + let wallet = Wallet::new_funded(50000, StaticFeeRate {}); let agreed_amount = Amount::from_sat(10000); let E = eve(); @@ -242,7 +259,7 @@ mod tests { async fn bob_make_psbt( A: PublicKey, B: PublicKey, - wallet: &Wallet<(), bdk::database::MemoryDatabase, ()>, + wallet: &Wallet<(), bdk::database::MemoryDatabase, StaticFeeRate>, amount: Amount, ) -> PartiallySignedTransaction { TxLock::new(&wallet, amount, A, B).await.unwrap().into() diff --git a/swap/src/bitcoin/punish.rs b/swap/src/bitcoin/punish.rs index 4845429d..674d93a2 100644 --- a/swap/src/bitcoin/punish.rs +++ b/swap/src/bitcoin/punish.rs @@ -1,5 +1,5 @@ use crate::bitcoin::wallet::Watchable; -use crate::bitcoin::{self, Address, PunishTimelock, Transaction, TxCancel, Txid}; +use crate::bitcoin::{self, Address, Amount, PunishTimelock, Transaction, TxCancel, Txid}; use ::bitcoin::util::bip143::SigHashCache; use ::bitcoin::{SigHash, SigHashType}; use anyhow::{Context, Result}; @@ -20,8 +20,10 @@ impl TxPunish { tx_cancel: &TxCancel, punish_address: &Address, punish_timelock: PunishTimelock, + spending_fee: Amount, ) -> Self { - let tx_punish = tx_cancel.build_spend_transaction(punish_address, Some(punish_timelock)); + let tx_punish = + tx_cancel.build_spend_transaction(punish_address, Some(punish_timelock), spending_fee); let digest = SigHashCache::new(&tx_punish).signature_hash( 0, // Only one input: cancel transaction @@ -71,6 +73,10 @@ impl TxPunish { Ok(tx_punish) } + + pub fn weight() -> usize { + 548 + } } impl Watchable for TxPunish { diff --git a/swap/src/bitcoin/redeem.rs b/swap/src/bitcoin/redeem.rs index da073693..39c237fb 100644 --- a/swap/src/bitcoin/redeem.rs +++ b/swap/src/bitcoin/redeem.rs @@ -1,6 +1,6 @@ use crate::bitcoin::wallet::Watchable; use crate::bitcoin::{ - verify_encsig, verify_sig, Address, EmptyWitnessStack, EncryptedSignature, NoInputs, + verify_encsig, verify_sig, Address, Amount, EmptyWitnessStack, EncryptedSignature, NoInputs, NotThreeWitnesses, PublicKey, SecretKey, TooManyInputs, Transaction, TxLock, }; use ::bitcoin::util::bip143::SigHashCache; @@ -24,10 +24,10 @@ pub struct TxRedeem { } impl TxRedeem { - pub fn new(tx_lock: &TxLock, redeem_address: &Address) -> Self { + pub fn new(tx_lock: &TxLock, redeem_address: &Address, spending_fee: Amount) -> Self { // lock_input is the shared output that is now being used as an input for the // redeem transaction - let tx_redeem = tx_lock.build_spend_transaction(redeem_address, None); + let tx_redeem = tx_lock.build_spend_transaction(redeem_address, None, spending_fee); let digest = SigHashCache::new(&tx_redeem).signature_hash( 0, // Only one input: lock_input (lock transaction) @@ -133,6 +133,15 @@ impl TxRedeem { Ok(sig) } + + pub fn weight() -> usize { + 548 + } + + #[cfg(test)] + pub fn inner(&self) -> Transaction { + self.inner.clone() + } } impl Watchable for TxRedeem { diff --git a/swap/src/bitcoin/refund.rs b/swap/src/bitcoin/refund.rs index 34057c79..a0f3dc64 100644 --- a/swap/src/bitcoin/refund.rs +++ b/swap/src/bitcoin/refund.rs @@ -1,7 +1,7 @@ use crate::bitcoin::wallet::Watchable; use crate::bitcoin::{ - verify_sig, Address, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs, - Transaction, TxCancel, + verify_sig, Address, Amount, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, + TooManyInputs, Transaction, TxCancel, }; use crate::{bitcoin, monero}; use ::bitcoin::util::bip143::SigHashCache; @@ -20,10 +20,10 @@ pub struct TxRefund { } impl TxRefund { - pub fn new(tx_cancel: &TxCancel, refund_address: &Address) -> Self { - let tx_punish = tx_cancel.build_spend_transaction(refund_address, None); + pub fn new(tx_cancel: &TxCancel, refund_address: &Address, spending_fee: Amount) -> Self { + let tx_refund = tx_cancel.build_spend_transaction(refund_address, None, spending_fee); - let digest = SigHashCache::new(&tx_punish).signature_hash( + let digest = SigHashCache::new(&tx_refund).signature_hash( 0, // Only one input: cancel transaction &tx_cancel.output_descriptor.script_code(), tx_cancel.amount().as_sat(), @@ -31,7 +31,7 @@ impl TxRefund { ); Self { - inner: tx_punish, + inner: tx_refund, digest, cancel_output_descriptor: tx_cancel.output_descriptor.clone(), watch_script: refund_address.script_pubkey(), @@ -137,6 +137,10 @@ impl TxRefund { Ok(sig) } + + pub fn weight() -> usize { + 548 + } } impl Watchable for TxRefund { diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index a5efa20f..801091d8 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -23,11 +23,17 @@ use tokio::sync::{watch, Mutex}; const SLED_TREE_NAME: &str = "default_tree"; +/// Assuming we add a spread of 3% we don't want to pay more than 3% of the +/// amount for tx fees. +const MAX_RELATIVE_TX_FEE: f64 = 0.03; +const MAX_ABSOLUTE_TX_FEE: u64 = 100_000; + pub struct Wallet { client: Arc>, wallet: Arc>>, finality_confirmations: u32, network: Network, + target_block: usize, } impl Wallet { @@ -36,6 +42,7 @@ impl Wallet { wallet_dir: &Path, key: impl DerivableKey + Clone, env_config: env::Config, + target_block: usize, ) -> Result { let client = bdk::electrum_client::Client::new(electrum_rpc_url.as_str()) .context("Failed to initialize Electrum RPC client")?; @@ -63,6 +70,7 @@ impl Wallet { wallet: Arc::new(Mutex::new(wallet)), finality_confirmations: env_config.bitcoin_finality_confirmations, network, + target_block, }) } @@ -238,6 +246,7 @@ impl Subscription { impl Wallet where + C: EstimateFeeRate, D: BatchDatabase, { pub async fn balance(&self) -> Result { @@ -282,10 +291,12 @@ where amount: Amount, ) -> Result { let wallet = self.wallet.lock().await; + let client = self.client.lock().await; + let fee_rate = client.estimate_feerate(self.target_block)?; let mut tx_builder = wallet.build_tx(); tx_builder.add_recipient(address.script_pubkey(), amount.as_sat()); - tx_builder.fee_rate(self.select_feerate()); + tx_builder.fee_rate(fee_rate); let (psbt, _details) = tx_builder.finish()?; Ok(psbt) @@ -298,19 +309,99 @@ where /// transaction confirmed. pub async fn max_giveable(&self, locking_script_size: usize) -> Result { let wallet = self.wallet.lock().await; + let client = self.client.lock().await; + let fee_rate = client.estimate_feerate(self.target_block)?; let mut tx_builder = wallet.build_tx(); let dummy_script = Script::from(vec![0u8; locking_script_size]); tx_builder.set_single_recipient(dummy_script); tx_builder.drain_wallet(); - tx_builder.fee_rate(self.select_feerate()); + tx_builder.fee_rate(fee_rate); let (_, details) = tx_builder.finish().context("Failed to build transaction")?; let max_giveable = details.sent - details.fees; Ok(Amount::from_sat(max_giveable)) } + + /// Estimate total tx fee for a pre-defined target block based on the + /// transaction weight. The max fee cannot be more than MAX_PERCENTAGE_FEE + /// of amount + pub async fn estimate_fee( + &self, + weight: usize, + transfer_amount: bitcoin::Amount, + ) -> Result { + let client = self.client.lock().await; + let fee_rate = client.estimate_feerate(self.target_block)?; + + let min_relay_fee = client.min_relay_fee()?; + tracing::debug!("Min relay fee: {}", min_relay_fee); + + Ok(estimate_fee( + weight, + transfer_amount, + fee_rate, + min_relay_fee, + )) + } +} + +fn estimate_fee( + weight: usize, + transfer_amount: Amount, + fee_rate: FeeRate, + min_relay_fee: Amount, +) -> Amount { + // Doing some heavy math here :) + // `usize` is 32 or 64 bits wide, but `f32`'s mantissa is only 23 bits wide. + // This is fine because such a big transaction cannot exist and there are also + // no negative fees. + #[allow( + clippy::cast_precision_loss, + clippy::cast_possible_truncation, + clippy::cast_sign_loss + )] + let sats_per_vbyte = ((weight as f32) / 4.0 * fee_rate.as_sat_vb()) as u64; + tracing::debug!( + "Estimated fee for weight: {} for fee_rate: {:?} is in total: {}", + weight, + fee_rate, + sats_per_vbyte + ); + + // Similar as above: we do not care about fractional fees and have to cast a + // couple of times. + #[allow( + clippy::cast_precision_loss, + clippy::cast_possible_truncation, + clippy::cast_sign_loss + )] + let max_allowed_fee = (transfer_amount.as_sat() as f64 * MAX_RELATIVE_TX_FEE).ceil() as u64; + + if sats_per_vbyte < min_relay_fee.as_sat() { + tracing::warn!( + "Estimated fee of {} is smaller than the min relay fee, defaulting to min relay fee {}", + sats_per_vbyte, + min_relay_fee.as_sat() + ); + min_relay_fee + } else if sats_per_vbyte > max_allowed_fee && sats_per_vbyte > MAX_ABSOLUTE_TX_FEE { + tracing::warn!( + "Hard bound of transaction fees reached. Falling back to: {} sats", + MAX_ABSOLUTE_TX_FEE + ); + bitcoin::Amount::from_sat(MAX_ABSOLUTE_TX_FEE) + } else if sats_per_vbyte > max_allowed_fee { + tracing::warn!( + "Relative bound of transaction fees reached. Falling back to: {} sats", + max_allowed_fee + ); + bitcoin::Amount::from_sat(max_allowed_fee) + } else { + bitcoin::Amount::from_sat(sats_per_vbyte) + } } impl Wallet @@ -340,19 +431,20 @@ impl Wallet { pub fn get_network(&self) -> bitcoin::Network { self.network } +} - /// Selects an appropriate [`FeeRate`] to be used for getting transactions - /// confirmed within a reasonable amount of time. - fn select_feerate(&self) -> FeeRate { - // TODO: This should obviously not be a const :) - FeeRate::from_sat_per_vb(5.0) - } +pub trait EstimateFeeRate { + fn estimate_feerate(&self, target_block: usize) -> Result; + fn min_relay_fee(&self) -> Result; } #[cfg(test)] -impl Wallet<(), bdk::database::MemoryDatabase, ()> { +impl Wallet<(), bdk::database::MemoryDatabase, EFR> +where + EFR: EstimateFeeRate, +{ /// Creates a new, funded wallet to be used within tests. - pub fn new_funded(amount: u64) -> Self { + pub fn new_funded(amount: u64, estimate_fee_rate: EFR) -> Self { use bdk::database::MemoryDatabase; use bdk::{LocalUtxo, TransactionDetails}; use bitcoin::OutPoint; @@ -374,10 +466,11 @@ impl Wallet<(), bdk::database::MemoryDatabase, ()> { bdk::Wallet::new_offline(&descriptors.0, None, Network::Regtest, database).unwrap(); Self { - client: Arc::new(Mutex::new(())), + client: Arc::new(Mutex::new(estimate_fee_rate)), wallet: Arc::new(Mutex::new(wallet)), finality_confirmations: 1, network: Network::Regtest, + target_block: 1, } } } @@ -544,6 +637,24 @@ impl Client { } } +impl EstimateFeeRate for Client { + fn estimate_feerate(&self, target_block: usize) -> Result { + // https://github.com/romanz/electrs/blob/f9cf5386d1b5de6769ee271df5eef324aa9491bc/src/rpc.rs#L213 + // Returned estimated fees are per BTC/kb. + let fee_per_byte = self.electrum.estimate_fee(target_block)?; + // we do not expect fees being that high. + #[allow(clippy::cast_possible_truncation)] + Ok(FeeRate::from_btc_per_kvb(fee_per_byte as f32)) + } + + fn min_relay_fee(&self) -> Result { + // https://github.com/romanz/electrs/blob/f9cf5386d1b5de6769ee271df5eef324aa9491bc/src/rpc.rs#L219 + // Returned fee is in BTC/kb + let relay_fee = bitcoin::Amount::from_btc(self.electrum.relay_fee()?)?; + Ok(relay_fee) + } +} + #[derive(Debug, Copy, Clone, PartialEq)] pub enum ScriptStatus { Unseen, @@ -634,6 +745,7 @@ impl fmt::Display for ScriptStatus { #[cfg(test)] mod tests { use super::*; + use proptest::prelude::*; #[test] fn given_depth_0_should_meet_confirmation_target_one() { @@ -662,4 +774,132 @@ mod tests { assert_eq!(confirmed.depth, 0) } + + #[test] + fn given_one_BTC_and_100k_sats_per_vb_fees_should_not_hit_max() { + // 400 weight = 100 vbyte + let weight = 400; + let amount = bitcoin::Amount::from_sat(100_000_000); + + let sat_per_vb = 100.0; + let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb); + + let relay_fee = bitcoin::Amount::ONE_SAT; + let is_fee = estimate_fee(weight, amount, fee_rate, relay_fee); + + // weight / 4.0 * sat_per_vb + let should_fee = bitcoin::Amount::from_sat(10_000); + assert_eq!(is_fee, should_fee); + } + + #[test] + fn given_1BTC_and_1_sat_per_vb_fees_and_100ksat_min_relay_fee_should_hit_min() { + // 400 weight = 100 vbyte + let weight = 400; + let amount = bitcoin::Amount::from_sat(100_000_000); + + let sat_per_vb = 1.0; + let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb); + + let relay_fee = bitcoin::Amount::from_sat(100_000); + let is_fee = estimate_fee(weight, amount, fee_rate, relay_fee); + + // weight / 4.0 * sat_per_vb would be smaller than relay fee hence we take min + // relay fee + let should_fee = bitcoin::Amount::from_sat(100_000); + assert_eq!(is_fee, should_fee); + } + + #[test] + fn given_1mio_sat_and_1k_sats_per_vb_fees_should_hit_relative_max() { + // 400 weight = 100 vbyte + let weight = 400; + let amount = bitcoin::Amount::from_sat(1_000_000); + + let sat_per_vb = 1_000.0; + let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb); + + let relay_fee = bitcoin::Amount::ONE_SAT; + let is_fee = estimate_fee(weight, amount, fee_rate, relay_fee); + + // weight / 4.0 * sat_per_vb would be greater than 3% hence we take max + // relative fee. + let should_fee = bitcoin::Amount::from_sat(30_000); + assert_eq!(is_fee, should_fee); + } + + #[test] + fn given_1BTC_and_4mio_sats_per_vb_fees_should_hit_total_max() { + // even if we send 1BTC we don't want to pay 0.3BTC in fees. This would be + // $1,650 at the moment. + let weight = 400; + let amount = bitcoin::Amount::from_sat(100_000_000); + + let sat_per_vb = 4_000_000.0; + let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb); + + let relay_fee = bitcoin::Amount::ONE_SAT; + let is_fee = estimate_fee(weight, amount, fee_rate, relay_fee); + + // weight / 4.0 * sat_per_vb would be greater than 3% hence we take total + // max allowed fee. + let should_fee = bitcoin::Amount::from_sat(MAX_ABSOLUTE_TX_FEE); + assert_eq!(is_fee, should_fee); + } + + proptest! { + #[test] + fn given_randon_amount_random_fee_and_random_relay_rate_but_fix_weight_does_not_panic( + amount in prop::num::u64::ANY, + sat_per_vb in prop::num::f32::POSITIVE, + relay_fee in prop::num::u64::ANY + ) { + let weight = 400; + let amount = bitcoin::Amount::from_sat(amount); + + let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb); + + let relay_fee = bitcoin::Amount::from_sat(relay_fee); + let _is_fee = estimate_fee(weight, amount, fee_rate, relay_fee); + + } + } + + proptest! { + #[test] + fn given_amount_in_range_fix_fee_fix_relay_rate_fix_weight_fee_always_smaller_max( + amount in 0u64..100_000_000, + ) { + let weight = 400; + let amount = bitcoin::Amount::from_sat(amount); + + let sat_per_vb = 100.0; + let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb); + + let relay_fee = bitcoin::Amount::ONE_SAT; + let is_fee = estimate_fee(weight, amount, fee_rate, relay_fee); + + // weight / 4 * 1_000 is always lower than MAX_ABSOLUTE_TX_FEE + assert!(is_fee.as_sat() < MAX_ABSOLUTE_TX_FEE); + } + } + + proptest! { + #[test] + fn given_amount_high_fix_fee_fix_relay_rate_fix_weight_fee_always_max( + amount in 100_000_000u64.., + ) { + let weight = 400; + let amount = bitcoin::Amount::from_sat(amount); + + let sat_per_vb = 1_000.0; + let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb); + + let relay_fee = bitcoin::Amount::ONE_SAT; + let is_fee = estimate_fee(weight, amount, fee_rate, relay_fee); + + // weight / 4 * 1_000 is always higher than MAX_ABSOLUTE_TX_FEE + assert!(is_fee.as_sat() >= MAX_ABSOLUTE_TX_FEE); + } + } } diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index 6cac6c0f..deb19ce5 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -15,6 +15,9 @@ const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:60002"; const DEFAULT_TOR_SOCKS5_PORT: &str = "9050"; +// Bitcoin transactions should be confirmed within X blocks +const DEFAULT_BITCOIN_CONFIRMATION_TARGET: &str = "3"; + #[derive(structopt::StructOpt, Debug)] #[structopt(name = "swap", about = "CLI for swapping BTC for XMR", author)] pub struct Arguments { @@ -53,6 +56,9 @@ pub enum Command { #[structopt(long = "tor-socks5-port", help = "Your local Tor socks5 proxy port", default_value = DEFAULT_TOR_SOCKS5_PORT)] tor_socks5_port: u16, + + #[structopt(long = "bitcoin-target-block", help = "Within how many blocks should the Bitcoin transactions be confirmed.", default_value = DEFAULT_BITCOIN_CONFIRMATION_TARGET)] + bitcoin_target_block: usize, }, /// Show a list of past ongoing and completed swaps History, @@ -78,6 +84,9 @@ pub enum Command { #[structopt(long = "tor-socks5-port", help = "Your local Tor socks5 proxy port", default_value = DEFAULT_TOR_SOCKS5_PORT)] tor_socks5_port: u16, + + #[structopt(long = "bitcoin-target-block", help = "Within how many blocks should the Bitcoin transactions be confirmed.", default_value = DEFAULT_BITCOIN_CONFIRMATION_TARGET)] + bitcoin_target_block: usize, }, /// Try to cancel an ongoing swap (expert users only) Cancel { @@ -95,6 +104,9 @@ pub enum Command { default_value = DEFAULT_ELECTRUM_RPC_URL )] electrum_rpc_url: Url, + + #[structopt(long = "bitcoin-target-block", help = "Within how many blocks should the Bitcoin transactions be confirmed.", default_value = DEFAULT_BITCOIN_CONFIRMATION_TARGET)] + bitcoin_target_block: usize, }, /// Try to cancel a swap and refund my BTC (expert users only) Refund { @@ -112,6 +124,9 @@ pub enum Command { default_value = DEFAULT_ELECTRUM_RPC_URL )] electrum_rpc_url: Url, + + #[structopt(long = "bitcoin-target-block", help = "Within how many blocks should the Bitcoin transactions be confirmed.", default_value = DEFAULT_BITCOIN_CONFIRMATION_TARGET)] + bitcoin_target_block: usize, }, } diff --git a/swap/src/protocol.rs b/swap/src/protocol.rs index 58b616b3..59b100c0 100644 --- a/swap/src/protocol.rs +++ b/swap/src/protocol.rs @@ -28,6 +28,10 @@ pub struct Message0 { dleq_proof_s_b: CrossCurveDLEQProof, v_b: monero::PrivateViewKey, refund_address: bitcoin::Address, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_refund_fee: bitcoin::Amount, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_cancel_fee: bitcoin::Amount, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -39,6 +43,10 @@ pub struct Message1 { v_a: monero::PrivateViewKey, redeem_address: bitcoin::Address, punish_address: bitcoin::Address, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_redeem_fee: bitcoin::Amount, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_punish_fee: bitcoin::Amount, } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/swap/src/protocol/alice/event_loop.rs b/swap/src/protocol/alice/event_loop.rs index af21f28b..45258ef8 100644 --- a/swap/src/protocol/alice/event_loop.rs +++ b/swap/src/protocol/alice/event_loop.rs @@ -161,8 +161,51 @@ where continue; } } + let tx_redeem_fee = self.bitcoin_wallet + .estimate_fee(bitcoin::TxRedeem::weight(), btc) + .await; + let tx_punish_fee = self.bitcoin_wallet + .estimate_fee(bitcoin::TxPunish::weight(), btc) + .await; + let redeem_address = self.bitcoin_wallet.new_address().await; + let punish_address = self.bitcoin_wallet.new_address().await; - let state0 = match State0::new(btc, xmr, self.env_config, self.bitcoin_wallet.as_ref(), &mut OsRng).await { + let (redeem_address, punish_address) = match ( + redeem_address, + punish_address, + ) { + (Ok(redeem_address), Ok(punish_address)) => { + (redeem_address, punish_address) + } + _ => { + tracing::error!("Could not get new address."); + continue; + } + }; + + let (tx_redeem_fee, tx_punish_fee) = match ( + tx_redeem_fee, + tx_punish_fee, + ) { + (Ok(tx_redeem_fee), Ok(tx_punish_fee)) => { + (tx_redeem_fee, tx_punish_fee) + } + _ => { + tracing::error!("Could not calculate transaction fees."); + continue; + } + }; + + let state0 = match State0::new( + btc, + xmr, + self.env_config, + redeem_address, + punish_address, + tx_redeem_fee, + tx_punish_fee, + &mut OsRng + ) { Ok(state) => state, Err(e) => { tracing::warn!(%peer, "Failed to make State0 for execution setup: {:#}", e); diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index 86810e5b..b091ebb4 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -108,14 +108,20 @@ pub struct State0 { punish_timelock: PunishTimelock, redeem_address: bitcoin::Address, punish_address: bitcoin::Address, + tx_redeem_fee: bitcoin::Amount, + tx_punish_fee: bitcoin::Amount, } impl State0 { - pub async fn new( + #[allow(clippy::too_many_arguments)] + pub fn new( btc: bitcoin::Amount, xmr: monero::Amount, env_config: Config, - bitcoin_wallet: &bitcoin::Wallet, + redeem_address: bitcoin::Address, + punish_address: bitcoin::Address, + tx_redeem_fee: bitcoin::Amount, + tx_punish_fee: bitcoin::Amount, rng: &mut R, ) -> Result where @@ -123,8 +129,6 @@ impl State0 { { let a = bitcoin::SecretKey::new_random(rng); let v_a = monero::PrivateViewKey::new_random(rng); - let redeem_address = bitcoin_wallet.new_address().await?; - let punish_address = bitcoin_wallet.new_address().await?; let s_a = monero::Scalar::random(rng); let (dleq_proof_s_a, (S_a_bitcoin, S_a_monero)) = CROSS_CURVE_PROOF_SYSTEM.prove(&s_a, rng); @@ -144,6 +148,8 @@ impl State0 { xmr, cancel_timelock: env_config.bitcoin_cancel_timelock, punish_timelock: env_config.bitcoin_punish_timelock, + tx_redeem_fee, + tx_punish_fee, }) } @@ -183,6 +189,10 @@ impl State0 { refund_address: msg.refund_address, redeem_address: self.redeem_address, punish_address: self.punish_address, + tx_redeem_fee: self.tx_redeem_fee, + tx_punish_fee: self.tx_punish_fee, + tx_refund_fee: msg.tx_refund_fee, + tx_cancel_fee: msg.tx_cancel_fee, })) } } @@ -206,6 +216,10 @@ pub struct State1 { refund_address: bitcoin::Address, redeem_address: bitcoin::Address, punish_address: bitcoin::Address, + tx_redeem_fee: bitcoin::Amount, + tx_punish_fee: bitcoin::Amount, + tx_refund_fee: bitcoin::Amount, + tx_cancel_fee: bitcoin::Amount, } impl State1 { @@ -218,6 +232,8 @@ impl State1 { v_a: self.v_a, redeem_address: self.redeem_address.clone(), punish_address: self.punish_address.clone(), + tx_redeem_fee: self.tx_redeem_fee, + tx_punish_fee: self.tx_punish_fee, } } @@ -240,6 +256,10 @@ impl State1 { redeem_address: self.redeem_address, punish_address: self.punish_address, tx_lock, + tx_redeem_fee: self.tx_redeem_fee, + tx_punish_fee: self.tx_punish_fee, + tx_refund_fee: self.tx_refund_fee, + tx_cancel_fee: self.tx_cancel_fee, }) } } @@ -260,14 +280,24 @@ pub struct State2 { redeem_address: bitcoin::Address, punish_address: bitcoin::Address, tx_lock: bitcoin::TxLock, + tx_redeem_fee: bitcoin::Amount, + tx_punish_fee: bitcoin::Amount, + tx_refund_fee: bitcoin::Amount, + tx_cancel_fee: bitcoin::Amount, } impl State2 { pub fn next_message(&self) -> Message3 { - let tx_cancel = - bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B); + let tx_cancel = bitcoin::TxCancel::new( + &self.tx_lock, + self.cancel_timelock, + self.a.public(), + self.B, + self.tx_cancel_fee, + ); - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address); + let tx_refund = + bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee); // Alice encsigns the refund transaction(bitcoin) digest with Bob's monero // pubkey(S_b). The refund transaction spends the output of // tx_lock_bitcoin to Bob's refund address. @@ -283,12 +313,21 @@ impl State2 { } pub fn receive(self, msg: Message4) -> Result { - let tx_cancel = - bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B); + let tx_cancel = bitcoin::TxCancel::new( + &self.tx_lock, + self.cancel_timelock, + self.a.public(), + self.B, + self.tx_cancel_fee, + ); bitcoin::verify_sig(&self.B, &tx_cancel.digest(), &msg.tx_cancel_sig) .context("Failed to verify cancel transaction")?; - let tx_punish = - bitcoin::TxPunish::new(&tx_cancel, &self.punish_address, self.punish_timelock); + let tx_punish = bitcoin::TxPunish::new( + &tx_cancel, + &self.punish_address, + self.punish_timelock, + self.tx_punish_fee, + ); bitcoin::verify_sig(&self.B, &tx_punish.digest(), &msg.tx_punish_sig) .context("Failed to verify punish transaction")?; @@ -309,6 +348,10 @@ impl State2 { tx_lock: self.tx_lock, tx_punish_sig_bob: msg.tx_punish_sig, tx_cancel_sig_bob: msg.tx_cancel_sig, + tx_redeem_fee: self.tx_redeem_fee, + tx_punish_fee: self.tx_punish_fee, + tx_refund_fee: self.tx_refund_fee, + tx_cancel_fee: self.tx_cancel_fee, }) } } @@ -332,6 +375,14 @@ pub struct State3 { pub tx_lock: bitcoin::TxLock, tx_punish_sig_bob: bitcoin::Signature, tx_cancel_sig_bob: bitcoin::Signature, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_redeem_fee: bitcoin::Amount, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_punish_fee: bitcoin::Amount, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_refund_fee: bitcoin::Amount, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_cancel_fee: bitcoin::Amount, } impl State3 { @@ -384,11 +435,17 @@ impl State3 { } pub fn tx_cancel(&self) -> TxCancel { - TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B) + TxCancel::new( + &self.tx_lock, + self.cancel_timelock, + self.a.public(), + self.B, + self.tx_cancel_fee, + ) } pub fn tx_refund(&self) -> TxRefund { - bitcoin::TxRefund::new(&self.tx_cancel(), &self.refund_address) + bitcoin::TxRefund::new(&self.tx_cancel(), &self.refund_address, self.tx_refund_fee) } pub fn extract_monero_private_key( @@ -407,7 +464,7 @@ impl State3 { &self, sig: bitcoin::EncryptedSignature, ) -> Result { - bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address) + bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee) .complete(sig, self.a.clone(), self.s_a.to_secpfun_scalar(), self.B) .context("Failed to complete Bitcoin redeem transaction") } @@ -429,6 +486,7 @@ impl State3 { &self.tx_cancel(), &self.punish_address, self.punish_timelock, + self.tx_punish_fee, ) } } diff --git a/swap/src/protocol/bob/refund.rs b/swap/src/protocol/bob/refund.rs index cb00ce57..82168eb0 100644 --- a/swap/src/protocol/bob/refund.rs +++ b/swap/src/protocol/bob/refund.rs @@ -45,7 +45,7 @@ pub async fn refund( } }; - state6.refund_btc(bitcoin_wallet.as_ref()).await?; + state6.publish_refund_btc(bitcoin_wallet.as_ref()).await?; let state = BobState::BtcRefunded(state6); let db_state = state.clone().into(); diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index df184f6e..758a0f93 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -1,3 +1,4 @@ +use crate::bitcoin::wallet::EstimateFeeRate; use crate::bitcoin::{ self, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel, TxLock, Txid, @@ -8,6 +9,7 @@ use crate::monero::{monero_private_key, TransferProof}; use crate::monero_ext::ScalarExt; use crate::protocol::{Message0, Message1, Message2, Message3, Message4, CROSS_CURVE_PROOF_SYSTEM}; use anyhow::{anyhow, bail, Context, Result}; +use bdk::database::BatchDatabase; use ecdsa_fun::adaptor::{Adaptor, HashTranscript}; use ecdsa_fun::nonce::Deterministic; use ecdsa_fun::Signature; @@ -83,6 +85,8 @@ pub struct State0 { punish_timelock: PunishTimelock, refund_address: bitcoin::Address, min_monero_confirmations: u64, + tx_refund_fee: bitcoin::Amount, + tx_cancel_fee: bitcoin::Amount, } impl State0 { @@ -96,6 +100,8 @@ impl State0 { punish_timelock: PunishTimelock, refund_address: bitcoin::Address, min_monero_confirmations: u64, + tx_refund_fee: bitcoin::Amount, + tx_cancel_fee: bitcoin::Amount, ) -> Self { let b = bitcoin::SecretKey::new_random(rng); @@ -120,6 +126,8 @@ impl State0 { punish_timelock, refund_address, min_monero_confirmations, + tx_refund_fee, + tx_cancel_fee, } } @@ -132,10 +140,20 @@ impl State0 { dleq_proof_s_b: self.dleq_proof_s_b.clone(), v_b: self.v_b, refund_address: self.refund_address.clone(), + tx_refund_fee: self.tx_refund_fee, + tx_cancel_fee: self.tx_cancel_fee, } } - pub async fn receive(self, wallet: &bitcoin::Wallet, msg: Message1) -> Result { + pub async fn receive( + self, + wallet: &bitcoin::Wallet, + msg: Message1, + ) -> Result + where + C: EstimateFeeRate, + D: BatchDatabase, + { let valid = CROSS_CURVE_PROOF_SYSTEM.verify( &msg.dleq_proof_s_a, ( @@ -169,6 +187,10 @@ impl State0 { punish_address: msg.punish_address, tx_lock, min_monero_confirmations: self.min_monero_confirmations, + tx_redeem_fee: msg.tx_redeem_fee, + tx_refund_fee: self.tx_refund_fee, + tx_punish_fee: msg.tx_punish_fee, + tx_cancel_fee: self.tx_cancel_fee, }) } } @@ -189,6 +211,10 @@ pub struct State1 { punish_address: bitcoin::Address, tx_lock: bitcoin::TxLock, min_monero_confirmations: u64, + tx_redeem_fee: bitcoin::Amount, + tx_refund_fee: bitcoin::Amount, + tx_punish_fee: bitcoin::Amount, + tx_cancel_fee: bitcoin::Amount, } impl State1 { @@ -199,8 +225,15 @@ impl State1 { } pub fn receive(self, msg: Message3) -> Result { - let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address); + let tx_cancel = TxCancel::new( + &self.tx_lock, + self.cancel_timelock, + self.A, + self.b.public(), + self.tx_cancel_fee, + ); + let tx_refund = + bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee); bitcoin::verify_sig(&self.A, &tx_cancel.digest(), &msg.tx_cancel_sig)?; bitcoin::verify_encsig( @@ -227,6 +260,10 @@ impl State1 { tx_cancel_sig_a: msg.tx_cancel_sig, tx_refund_encsig: msg.tx_refund_encsig, min_monero_confirmations: self.min_monero_confirmations, + tx_redeem_fee: self.tx_redeem_fee, + tx_refund_fee: self.tx_refund_fee, + tx_punish_fee: self.tx_punish_fee, + tx_cancel_fee: self.tx_cancel_fee, }) } } @@ -249,14 +286,32 @@ pub struct State2 { tx_cancel_sig_a: Signature, tx_refund_encsig: bitcoin::EncryptedSignature, min_monero_confirmations: u64, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_redeem_fee: bitcoin::Amount, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_punish_fee: bitcoin::Amount, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_refund_fee: bitcoin::Amount, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_cancel_fee: bitcoin::Amount, } impl State2 { pub fn next_message(&self) -> Message4 { - let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); + let tx_cancel = TxCancel::new( + &self.tx_lock, + self.cancel_timelock, + self.A, + self.b.public(), + self.tx_cancel_fee, + ); let tx_cancel_sig = self.b.sign(tx_cancel.digest()); - let tx_punish = - bitcoin::TxPunish::new(&tx_cancel, &self.punish_address, self.punish_timelock); + let tx_punish = bitcoin::TxPunish::new( + &tx_cancel, + &self.punish_address, + self.punish_timelock, + self.tx_punish_fee, + ); let tx_punish_sig = self.b.sign(tx_punish.digest()); Message4 { @@ -283,6 +338,9 @@ impl State2 { tx_cancel_sig_a: self.tx_cancel_sig_a, tx_refund_encsig: self.tx_refund_encsig, min_monero_confirmations: self.min_monero_confirmations, + tx_redeem_fee: self.tx_redeem_fee, + tx_refund_fee: self.tx_refund_fee, + tx_cancel_fee: self.tx_cancel_fee, }, self.tx_lock, )) @@ -306,6 +364,12 @@ pub struct State3 { tx_cancel_sig_a: Signature, tx_refund_encsig: bitcoin::EncryptedSignature, min_monero_confirmations: u64, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_redeem_fee: bitcoin::Amount, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_refund_fee: bitcoin::Amount, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_cancel_fee: bitcoin::Amount, } impl State3 { @@ -338,6 +402,9 @@ impl State3 { tx_cancel_sig_a: self.tx_cancel_sig_a, tx_refund_encsig: self.tx_refund_encsig, monero_wallet_restore_blockheight, + tx_redeem_fee: self.tx_redeem_fee, + tx_refund_fee: self.tx_refund_fee, + tx_cancel_fee: self.tx_cancel_fee, } } @@ -352,6 +419,8 @@ impl State3 { tx_lock: self.tx_lock.clone(), tx_cancel_sig_a: self.tx_cancel_sig_a.clone(), tx_refund_encsig: self.tx_refund_encsig.clone(), + tx_refund_fee: self.tx_refund_fee, + tx_cancel_fee: self.tx_cancel_fee, } } @@ -363,7 +432,13 @@ impl State3 { &self, bitcoin_wallet: &bitcoin::Wallet, ) -> Result { - let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); + let tx_cancel = TxCancel::new( + &self.tx_lock, + self.cancel_timelock, + self.A, + self.b.public(), + self.tx_cancel_fee, + ); 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?; @@ -392,16 +467,24 @@ pub struct State4 { tx_cancel_sig_a: Signature, tx_refund_encsig: bitcoin::EncryptedSignature, monero_wallet_restore_blockheight: BlockHeight, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_redeem_fee: bitcoin::Amount, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_refund_fee: bitcoin::Amount, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + tx_cancel_fee: bitcoin::Amount, } impl State4 { pub fn tx_redeem_encsig(&self) -> bitcoin::EncryptedSignature { - let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address); + let tx_redeem = + bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee); self.b.encsign(self.S_a_bitcoin, tx_redeem.digest()) } pub async fn watch_for_redeem_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result { - let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address); + let tx_redeem = + bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee); let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest()); bitcoin_wallet @@ -430,7 +513,13 @@ impl State4 { &self, bitcoin_wallet: &bitcoin::Wallet, ) -> Result { - let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); + let tx_cancel = TxCancel::new( + &self.tx_lock, + self.cancel_timelock, + self.A, + self.b.public(), + self.tx_cancel_fee, + ); 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?; @@ -454,6 +543,8 @@ impl State4 { tx_lock: self.tx_lock, tx_cancel_sig_a: self.tx_cancel_sig_a, tx_refund_encsig: self.tx_refund_encsig, + tx_refund_fee: self.tx_refund_fee, + tx_cancel_fee: self.tx_cancel_fee, } } } @@ -492,6 +583,10 @@ pub struct State6 { tx_lock: bitcoin::TxLock, tx_cancel_sig_a: Signature, tx_refund_encsig: bitcoin::EncryptedSignature, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub tx_refund_fee: bitcoin::Amount, + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub tx_cancel_fee: bitcoin::Amount, } impl State6 { @@ -499,7 +594,13 @@ impl State6 { &self, bitcoin_wallet: &bitcoin::Wallet, ) -> Result { - let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); + let tx_cancel = TxCancel::new( + &self.tx_lock, + self.cancel_timelock, + self.A, + self.b.public(), + self.tx_cancel_fee, + ); 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?; @@ -516,8 +617,13 @@ impl State6 { &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_cancel = bitcoin::TxCancel::new( + &self.tx_lock, + self.cancel_timelock, + self.A, + self.b.public(), + self.tx_cancel_fee, + ); let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?; @@ -525,20 +631,39 @@ impl State6 { } pub async fn submit_tx_cancel(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result { - let transaction = - bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()) - .complete_as_bob(self.A, self.b.clone(), self.tx_cancel_sig_a.clone()) - .context("Failed to complete Bitcoin cancel transaction")?; + let transaction = bitcoin::TxCancel::new( + &self.tx_lock, + self.cancel_timelock, + self.A, + self.b.public(), + self.tx_cancel_fee, + ) + .complete_as_bob(self.A, self.b.clone(), self.tx_cancel_sig_a.clone()) + .context("Failed to complete Bitcoin cancel transaction")?; let (tx_id, _) = bitcoin_wallet.broadcast(transaction, "cancel").await?; Ok(tx_id) } - 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); + pub async fn publish_refund_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<()> { + let signed_tx_refund = self.signed_refund_transaction()?; + let (_, subscription) = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?; + subscription.wait_until_final().await?; + + Ok(()) + } + + pub fn signed_refund_transaction(&self) -> Result { + let tx_cancel = bitcoin::TxCancel::new( + &self.tx_lock, + self.cancel_timelock, + self.A, + self.b.public(), + self.tx_cancel_fee, + ); + let tx_refund = + bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee); let adaptor = Adaptor::, Deterministic>::default(); @@ -548,12 +673,7 @@ impl State6 { let signed_tx_refund = tx_refund.add_signatures((self.A, sig_a), (self.b.public(), sig_b))?; - - let (_, subscription) = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?; - - subscription.wait_until_final().await?; - - Ok(()) + Ok(signed_tx_refund) } pub fn tx_lock_id(&self) -> bitcoin::Txid { diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 88ad040d..07425f53 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -1,4 +1,4 @@ -use crate::bitcoin::ExpiredTimelocks; +use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund}; use crate::database::Swap; use crate::env::Config; use crate::protocol::bob; @@ -66,6 +66,12 @@ async fn next_state( Ok(match state { BobState::Started { btc_amount } => { let bitcoin_refund_address = bitcoin_wallet.new_address().await?; + let tx_refund_fee = bitcoin_wallet + .estimate_fee(TxRefund::weight(), btc_amount) + .await?; + let tx_cancel_fee = bitcoin_wallet + .estimate_fee(TxCancel::weight(), btc_amount) + .await?; let state2 = request_price_and_setup( swap_id, @@ -73,6 +79,8 @@ async fn next_state( event_loop_handle, env_config, bitcoin_refund_address, + tx_refund_fee, + tx_cancel_fee, ) .await?; @@ -247,7 +255,7 @@ async fn next_state( ); } ExpiredTimelocks::Cancel => { - state.refund_btc(bitcoin_wallet).await?; + state.publish_refund_btc(bitcoin_wallet).await?; BobState::BtcRefunded(state) } ExpiredTimelocks::Punish => BobState::BtcPunished { @@ -268,6 +276,8 @@ pub async fn request_price_and_setup( event_loop_handle: &mut EventLoopHandle, env_config: &Config, bitcoin_refund_address: bitcoin::Address, + tx_refund_fee: bitcoin::Amount, + tx_cancel_fee: bitcoin::Amount, ) -> Result { let xmr = event_loop_handle.request_spot_price(btc).await?; @@ -282,6 +292,8 @@ pub async fn request_price_and_setup( env_config.bitcoin_punish_timelock, bitcoin_refund_address, env_config.monero_finality_confirmations, + tx_refund_fee, + tx_cancel_fee, ); let state2 = event_loop_handle.execution_setup(state0).await?; diff --git a/swap/tests/harness/mod.rs b/swap/tests/harness/mod.rs index fcdc77bb..cadce4c4 100644 --- a/swap/tests/harness/mod.rs +++ b/swap/tests/harness/mod.rs @@ -14,7 +14,7 @@ use std::fmt; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; -use swap::bitcoin::{CancelTimelock, PunishTimelock}; +use swap::bitcoin::{CancelTimelock, PunishTimelock, TxCancel, TxPunish, TxRedeem, TxRefund}; use swap::database::Database; use swap::env::{Config, GetConfig}; use swap::network::swarm; @@ -285,6 +285,7 @@ async fn init_test_wallets( seed.derive_extended_private_key(env_config.bitcoin_network) .expect("Could not create extended private key from seed"), env_config, + 1, ) .await .expect("could not init btc wallet"); @@ -539,7 +540,7 @@ impl TestContext { assert_eventual_balance( self.alice_bitcoin_wallet.as_ref(), Ordering::Equal, - self.alice_redeemed_btc_balance(), + self.alice_redeemed_btc_balance().await, ) .await .unwrap(); @@ -580,7 +581,7 @@ impl TestContext { assert_eventual_balance( self.alice_bitcoin_wallet.as_ref(), Ordering::Equal, - self.alice_punished_btc_balance(), + self.alice_punished_btc_balance().await, ) .await .unwrap(); @@ -631,19 +632,21 @@ impl TestContext { let btc_balance_after_swap = self.bob_bitcoin_wallet.balance().await.unwrap(); - let alice_submitted_cancel = btc_balance_after_swap - == self.bob_starting_balances.btc - - lock_tx_bitcoin_fee - - bitcoin::Amount::from_sat(bitcoin::TX_FEE); + let cancel_fee = self + .alice_bitcoin_wallet + .estimate_fee(TxCancel::weight(), self.btc_amount) + .await + .expect("To estimate fee correctly"); + let refund_fee = self + .alice_bitcoin_wallet + .estimate_fee(TxRefund::weight(), self.btc_amount) + .await + .expect("To estimate fee correctly"); - let bob_submitted_cancel = btc_balance_after_swap - == self.bob_starting_balances.btc - - lock_tx_bitcoin_fee - - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE); + let bob_cancelled_and_refunded = btc_balance_after_swap + == self.bob_starting_balances.btc - lock_tx_bitcoin_fee - cancel_fee - refund_fee; - // The cancel tx can be submitted by both Alice and Bob. - // Since we cannot be sure who submitted it we have to assert accordingly - assert!(alice_submitted_cancel || bob_submitted_cancel); + assert!(bob_cancelled_and_refunded); assert_eventual_balance( self.bob_monero_wallet.as_ref(), @@ -676,9 +679,13 @@ impl TestContext { self.alice_starting_balances.xmr - self.xmr_amount } - fn alice_redeemed_btc_balance(&self) -> bitcoin::Amount { - self.alice_starting_balances.btc + self.btc_amount - - bitcoin::Amount::from_sat(bitcoin::TX_FEE) + async fn alice_redeemed_btc_balance(&self) -> bitcoin::Amount { + let fee = self + .alice_bitcoin_wallet + .estimate_fee(TxRedeem::weight(), self.btc_amount) + .await + .expect("To estimate fee correctly"); + self.alice_starting_balances.btc + self.btc_amount - fee } fn bob_redeemed_xmr_balance(&self) -> monero::Amount { @@ -715,9 +722,18 @@ impl TestContext { self.alice_starting_balances.xmr - self.xmr_amount } - fn alice_punished_btc_balance(&self) -> bitcoin::Amount { - self.alice_starting_balances.btc + self.btc_amount - - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE) + async fn alice_punished_btc_balance(&self) -> bitcoin::Amount { + let cancel_fee = self + .alice_bitcoin_wallet + .estimate_fee(TxCancel::weight(), self.btc_amount) + .await + .expect("To estimate fee correctly"); + let punish_fee = self + .alice_bitcoin_wallet + .estimate_fee(TxPunish::weight(), self.btc_amount) + .await + .expect("To estimate fee correctly"); + self.alice_starting_balances.btc + self.btc_amount - cancel_fee - punish_fee } fn bob_punished_xmr_balance(&self) -> monero::Amount {