mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-12-24 15:09:35 -05:00
Merge #466
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>
This commit is contained in:
commit
08d7d587cf
76
Cargo.lock
generated
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 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);
|
||||
|
@ -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…
Reference in New Issue
Block a user