466: Add support to dynamically chose bitcoin transaction fees.  r=bonomat a=bonomat

Resolves #443 (Bitcoin only). 

@da-kami  /@thomaseizinger: do you know how fees work in Monero? 
If not, I'll read up on it and to come up with a proper strategy. 

Monero Fees will be covered in #470

Note: 
The there is a hardcoded relative and absolute upper bound. I personally feel safer if this is hardcoded for now as it is too easy to get wrong. 
At the time of writing, this equals roughly USD $56. 
Eventually we will want to make this also configurable.


Co-authored-by: Philipp Hoenisch <philipp@hoenisch.at>
Co-authored-by: Philipp Hoenisch <philipp@coblox.tech>
pull/479/head
bors[bot] 3 years ago committed by GitHub
commit 08d7d587cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

76
Cargo.lock generated

@ -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"

@ -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 }

@ -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<Config> {
.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<Config> {
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![

@ -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")?;

@ -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<bitcoin::Wallet> {
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")?;

@ -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<FeeRate> {
Ok(FeeRate::default_min_relay_fee())
}
fn min_relay_fee(&self) -> Result<bitcoin::Amount> {
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
)
}
}

@ -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<PunishTimelock>,
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 {

@ -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<Self>
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<u32>,
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<FeeRate> {
Ok(FeeRate::default_min_relay_fee())
}
fn min_relay_fee(&self) -> Result<bitcoin::Amount> {
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()

@ -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 {

@ -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 {

@ -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 {

@ -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<B = ElectrumBlockchain, D = bdk::sled::Tree, C = Client> {
client: Arc<Mutex<C>>,
wallet: Arc<Mutex<bdk::Wallet<B, D>>>,
finality_confirmations: u32,
network: Network,
target_block: usize,
}
impl Wallet {
@ -36,6 +42,7 @@ impl Wallet {
wallet_dir: &Path,
key: impl DerivableKey<Segwitv0> + Clone,
env_config: env::Config,
target_block: usize,
) -> Result<Self> {
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<B, D, C> Wallet<B, D, C>
where
C: EstimateFeeRate,
D: BatchDatabase,
{
pub async fn balance(&self) -> Result<Amount> {
@ -282,10 +291,12 @@ where
amount: Amount,
) -> Result<PartiallySignedTransaction> {
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<Amount> {
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<bitcoin::Amount> {
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<B, D, C> Wallet<B, D, C>
@ -340,19 +431,20 @@ impl<B, D, C> Wallet<B, D, C> {
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<FeeRate>;
fn min_relay_fee(&self) -> Result<bitcoin::Amount>;
}
#[cfg(test)]
impl Wallet<(), bdk::database::MemoryDatabase, ()> {
impl<EFR> 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<FeeRate> {
// 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<bitcoin::Amount> {
// 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);
}
}
}

@ -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,
},
}

@ -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)]

@ -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 (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, self.bitcoin_wallet.as_ref(), &mut OsRng).await {
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);

@ -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<R>(
#[allow(clippy::too_many_arguments)]
pub fn new<R>(
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<Self>
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<State3> {
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::Transaction> {
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,
)
}
}

@ -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();

@ -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<State1> {
pub async fn receive<B, D, C>(
self,
wallet: &bitcoin::Wallet<B, D, C>,
msg: Message1,
) -> Result<State1>
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<State2> {
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<ExpiredTimelocks> {
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<State5> {
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<ExpiredTimelocks> {
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<ExpiredTimelocks> {
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<Transaction> {
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<Txid> {
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<Transaction> {
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::<HashTranscript<Sha256>, Deterministic<Sha256>>::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 {

@ -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<bob::state::State2> {
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?;

@ -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 {

Loading…
Cancel
Save