From 48b3d6ee6ccb21bb1d829b20fe39b8195f78506f Mon Sep 17 00:00:00 2001 From: rishflab Date: Wed, 17 Feb 2021 10:00:53 +1100 Subject: [PATCH] Make bitcoin tx fee configurable Alice specifies the fee rate in the quote response to Bob. Bob's CLI tool should calculate and show the fees (along with the rate) that he will pay for a swap. Bob can decide whether he wishes to proceed with the swap. Currently the fee rate is hardcoded for Alice and the tests are failing because the balance assertions assume fixed tx fees. Remove fee rate from BuildTxLockPsbt as we manually specify the fees in build_spend_transaction(). --- swap/src/bitcoin.rs | 47 +++++++++++++++++++---- swap/src/bitcoin/cancel.rs | 9 +++-- swap/src/bitcoin/lock.rs | 7 +++- swap/src/bitcoin/wallet.rs | 17 ++++---- swap/src/protocol/alice/event_loop.rs | 6 ++- swap/src/protocol/alice/quote_response.rs | 1 + swap/src/protocol/bob/state.rs | 10 +++++ swap/src/protocol/bob/swap.rs | 1 + swap/tests/testutils/mod.rs | 8 ++-- 9 files changed, 79 insertions(+), 27 deletions(-) diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index ad5f4fc0..932eb593 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -35,14 +35,7 @@ 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; +pub const FEE_RATE: u64 = 80; #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct SecretKey { @@ -317,3 +310,41 @@ pub struct EmptyWitnessStack; #[derive(Clone, Copy, thiserror::Error, Debug)] #[error("input has {0} witnesses, expected 3")] pub struct NotThreeWitnesses(usize); + +pub mod bitcoin_amount { + use crate::bitcoin::Amount; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(x: &Amount, s: S) -> Result + where + S: Serializer, + { + s.serialize_u64(x.as_sat()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result>::Error> + where + D: Deserializer<'de>, + { + let sats = u64::deserialize(deserializer)?; + let amount = Amount::from_sat(sats); + + Ok(amount) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + pub struct BitcoinAmount(#[serde(with = "bitcoin_amount")] crate::bitcoin::Amount); + + #[test] + fn serde_bitcoin_amount() { + let amount = BitcoinAmount(crate::bitcoin::Amount::from_sat(1000)); + let encoded = serde_cbor::to_vec(&amount).unwrap(); + let decoded: BitcoinAmount = serde_cbor::from_slice(&encoded).unwrap(); + assert_eq!(amount, decoded); + } +} diff --git a/swap/src/bitcoin/cancel.rs b/swap/src/bitcoin/cancel.rs index 72b719ad..48fa801d 100644 --- a/swap/src/bitcoin/cancel.rs +++ b/swap/src/bitcoin/cancel.rs @@ -1,6 +1,6 @@ use crate::bitcoin::{ build_shared_output_descriptor, Address, Amount, BlockHeight, PublicKey, Transaction, TxLock, - TX_FEE, + FEE_RATE, }; use ::bitcoin::{util::bip143::SigHashCache, OutPoint, SigHash, SigHashType, TxIn, TxOut, Txid}; use anyhow::Result; @@ -77,7 +77,7 @@ impl TxCancel { }; let tx_out = TxOut { - value: tx_lock.lock_amount().as_sat() - TX_FEE, + value: tx_lock.lock_amount().as_sat(), script_pubkey: cancel_output_descriptor.script_pubkey(NullCtx), }; @@ -165,8 +165,11 @@ impl TxCancel { witness: Vec::new(), }; + let vbytes = self.inner.get_weight() / 4; + let tx_fee = vbytes as u64 * FEE_RATE; + let tx_out = TxOut { - value: self.amount().as_sat() - TX_FEE, + value: self.amount().as_sat() - tx_fee, script_pubkey: spend_address.script_pubkey(), }; diff --git a/swap/src/bitcoin/lock.rs b/swap/src/bitcoin/lock.rs index 2bc6eb14..8f5471bb 100644 --- a/swap/src/bitcoin/lock.rs +++ b/swap/src/bitcoin/lock.rs @@ -1,6 +1,6 @@ use crate::bitcoin::{ build_shared_output_descriptor, Address, Amount, BuildTxLockPsbt, GetNetwork, PublicKey, - Transaction, TX_FEE, + Transaction, FEE_RATE, }; use ::bitcoin::{util::psbt::PartiallySignedTransaction, OutPoint, TxIn, TxOut, Txid}; use anyhow::Result; @@ -74,8 +74,11 @@ impl TxLock { witness: Vec::new(), }; + let vbytes = self.inner.clone().extract_tx().get_weight() / 4; + let tx_fee = vbytes as u64 * FEE_RATE; + 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 - tx_fee, script_pubkey: spend_address.script_pubkey(), }; diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index 7b590007..4be502fb 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -14,7 +14,6 @@ use bdk::{ blockchain::{noop_progress, Blockchain, ElectrumBlockchain}, electrum_client::{self, Client, ElectrumApi}, miniscript::bitcoin::PrivateKey, - FeeRate, }; use reqwest::{Method, Url}; use serde::{Deserialize, Serialize}; @@ -120,14 +119,14 @@ impl BuildTxLockPsbt for Wallet { ) -> Result { tracing::debug!("building tx lock"); self.sync_wallet().await?; - let (psbt, _details) = self.inner.lock().await.create_tx( - bdk::TxBuilder::with_recipients(vec![( - output_address.script_pubkey(), - output_amount.as_sat(), - )]) - // todo: get actual fee - .fee_rate(FeeRate::from_sat_per_vb(5.0)), - )?; + let (psbt, _details) = + self.inner + .lock() + .await + .create_tx(bdk::TxBuilder::with_recipients(vec![( + output_address.script_pubkey(), + output_amount.as_sat(), + )]))?; tracing::debug!("tx lock built"); Ok(psbt) } diff --git a/swap/src/protocol/alice/event_loop.rs b/swap/src/protocol/alice/event_loop.rs index df0e5ba9..2622457f 100644 --- a/swap/src/protocol/alice/event_loop.rs +++ b/swap/src/protocol/alice/event_loop.rs @@ -1,5 +1,6 @@ use crate::{ bitcoin, + bitcoin::FEE_RATE, database::Database, execution_params::ExecutionParams, monero, network, @@ -202,7 +203,10 @@ impl EventLoop { let btc_amount = quote_request.btc_amount; let xmr_amount = btc_amount.as_btc() * RATE as f64; let xmr_amount = monero::Amount::from_monero(xmr_amount)?; - let quote_response = QuoteResponse { xmr_amount }; + let quote_response = QuoteResponse { + xmr_amount, + tx_fee_rate: FEE_RATE, + }; self.swarm .send_quote_response(channel, quote_response) diff --git a/swap/src/protocol/alice/quote_response.rs b/swap/src/protocol/alice/quote_response.rs index 878dd530..c0f41e33 100644 --- a/swap/src/protocol/alice/quote_response.rs +++ b/swap/src/protocol/alice/quote_response.rs @@ -29,6 +29,7 @@ pub enum OutEvent { #[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub struct QuoteResponse { pub xmr_amount: monero::Amount, + pub tx_fee_rate: u64, } impl From> for OutEvent { diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index aa32b5e4..5b67ec46 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -79,6 +79,7 @@ pub struct State0 { #[serde(with = "::bitcoin::util::amount::serde::as_sat")] btc: bitcoin::Amount, xmr: monero::Amount, + btc_fee_rate: u64, cancel_timelock: CancelTimelock, punish_timelock: PunishTimelock, refund_address: bitcoin::Address, @@ -86,10 +87,12 @@ pub struct State0 { } impl State0 { + #[allow(clippy::too_many_arguments)] pub fn new( rng: &mut R, btc: bitcoin::Amount, xmr: monero::Amount, + btc_fee_rate: u64, cancel_timelock: CancelTimelock, punish_timelock: PunishTimelock, refund_address: bitcoin::Address, @@ -107,6 +110,7 @@ impl State0 { v_b, btc, xmr, + btc_fee_rate, dleq_proof_s_b, cancel_timelock, punish_timelock, @@ -559,6 +563,12 @@ impl State4 { pub fn tx_lock_id(&self) -> bitcoin::Txid { self.tx_lock.txid() } + + pub fn tx_cancel_id(&self) -> bitcoin::Txid { + let tx_cancel = + bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); + tx_cancel.txid() + } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index d645e83e..9fb3dec3 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -390,6 +390,7 @@ pub async fn request_quote_and_setup( &mut OsRng, btc_amount, quote_response.xmr_amount, + quote_response.tx_fee_rate, execution_params.bitcoin_cancel_timelock, execution_params.bitcoin_punish_timelock, bitcoin_refund_address, diff --git a/swap/tests/testutils/mod.rs b/swap/tests/testutils/mod.rs index 3f8fc49e..214a0ff6 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -139,7 +139,7 @@ impl TestContext { assert_eq!( btc_balance_after_swap, self.alice_starting_balances.btc + self.btc_amount - - bitcoin::Amount::from_sat(bitcoin::TX_FEE) + - bitcoin::Amount::from_sat(bitcoin::FEE_RATE) ); let xmr_balance_after_swap = self @@ -193,7 +193,7 @@ impl TestContext { assert_eq!( btc_balance_after_swap, self.alice_starting_balances.btc + self.btc_amount - - bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE) + - bitcoin::Amount::from_sat(2 * bitcoin::FEE_RATE) ); let xmr_balance_after_swap = self @@ -265,12 +265,12 @@ impl TestContext { let alice_submitted_cancel = btc_balance_after_swap == self.bob_starting_balances.btc - lock_tx_bitcoin_fee - - bitcoin::Amount::from_sat(bitcoin::TX_FEE); + - bitcoin::Amount::from_sat(bitcoin::FEE_RATE); 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); + - bitcoin::Amount::from_sat(2 * bitcoin::FEE_RATE); // The cancel tx can be submitted by both Alice and Bob. // Since we cannot be sure who submitted it we have to assert accordingly