From c12aa3d32737f5e74f8a621c3a6cb95a5e4dfb34 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Thu, 13 Aug 2020 16:51:27 +0200 Subject: [PATCH] Implement RBF and add a few tests --- src/cli.rs | 92 +++- src/database/mod.rs | 6 +- src/error.rs | 4 + src/wallet/mod.rs | 901 +++++++++++++++++++++++++++++++++--- src/wallet/rbf.rs | 103 +++++ src/wallet/utils.rs | 2 +- testutils-macros/src/lib.rs | 129 ++++++ 7 files changed, 1167 insertions(+), 70 deletions(-) create mode 100644 src/wallet/rbf.rs diff --git a/src/cli.rs b/src/cli.rs index 164ae4a2..3da4d49e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -9,7 +9,7 @@ use log::{debug, error, info, trace, LevelFilter}; use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex}; use bitcoin::hashes::hex::{FromHex, ToHex}; use bitcoin::util::psbt::PartiallySignedTransaction; -use bitcoin::{Address, OutPoint}; +use bitcoin::{Address, OutPoint, Txid}; use crate::error::Error; use crate::types::ScriptType; @@ -83,6 +83,12 @@ pub fn make_cli_subcommands<'a, 'b>() -> App<'a, 'b> { .long("send_all") .help("Sends all the funds (or all the selected utxos). Requires only one addressees of value 0"), ) + .arg( + Arg::with_name("enable_rbf") + .short("rbf") + .long("enable_rbf") + .help("Enables Replace-By-Fee (BIP125)"), + ) .arg( Arg::with_name("utxos") .long("utxos") @@ -120,6 +126,53 @@ pub fn make_cli_subcommands<'a, 'b>() -> App<'a, 'b> { .number_of_values(1), ), ) + .subcommand( + SubCommand::with_name("bump_fee") + .about("Bumps the fees of an RBF transaction") + .arg( + Arg::with_name("txid") + .required(true) + .takes_value(true) + .short("txid") + .long("txid") + .help("TXID of the transaction to update"), + ) + .arg( + Arg::with_name("send_all") + .short("all") + .long("send_all") + .help("Allows the wallet to reduce the amount of the only output in order to increase fees. This is generally the expected behavior for transactions originally created with `send_all`"), + ) + .arg( + Arg::with_name("utxos") + .long("utxos") + .value_name("TXID:VOUT") + .help("Selects which utxos *must* be added to the tx. Unconfirmed utxos cannot be used") + .takes_value(true) + .number_of_values(1) + .multiple(true) + .validator(outpoint_validator), + ) + .arg( + Arg::with_name("unspendable") + .long("unspendable") + .value_name("TXID:VOUT") + .help("Marks an utxo as unspendable, in case more inputs are needed to cover the extra fees") + .takes_value(true) + .number_of_values(1) + .multiple(true) + .validator(outpoint_validator), + ) + .arg( + Arg::with_name("fee_rate") + .required(true) + .short("fee") + .long("fee_rate") + .value_name("SATS_VBYTE") + .help("The new targeted fee rate in sat/vbyte") + .takes_value(true), + ), + ) .subcommand( SubCommand::with_name("policies") .about("Returns the available spending policies for the descriptor") @@ -331,6 +384,9 @@ where if sub_matches.is_present("send_all") { tx_builder = tx_builder.send_all(); } + if sub_matches.is_present("enable_rbf") { + tx_builder = tx_builder.enable_rbf(); + } if let Some(fee_rate) = sub_matches.value_of("fee_rate") { let fee_rate = f32::from_str(fee_rate).map_err(|s| Error::Generic(s.to_string()))?; @@ -363,6 +419,40 @@ where result.1, base64::encode(&serialize(&result.0)) ))) + } else if let Some(sub_matches) = matches.subcommand_matches("bump_fee") { + let txid = Txid::from_str(sub_matches.value_of("txid").unwrap()) + .map_err(|s| Error::Generic(s.to_string()))?; + + let fee_rate = f32::from_str(sub_matches.value_of("fee_rate").unwrap()) + .map_err(|s| Error::Generic(s.to_string()))?; + let mut tx_builder = TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(fee_rate)); + + if sub_matches.is_present("send_all") { + tx_builder = tx_builder.send_all(); + } + + if let Some(utxos) = sub_matches.values_of("utxos") { + let utxos = utxos + .map(|i| parse_outpoint(i)) + .collect::, _>>() + .map_err(|s| Error::Generic(s.to_string()))?; + tx_builder = tx_builder.utxos(utxos); + } + + if let Some(unspendable) = sub_matches.values_of("unspendable") { + let unspendable = unspendable + .map(|i| parse_outpoint(i)) + .collect::, _>>() + .map_err(|s| Error::Generic(s.to_string()))?; + tx_builder = tx_builder.unspendable(unspendable); + } + + let result = wallet.bump_fee(&txid, tx_builder)?; + Ok(Some(format!( + "{:#?}\nPSBT: {}", + result.1, + base64::encode(&serialize(&result.0)) + ))) } else if let Some(_sub_matches) = matches.subcommand_matches("policies") { Ok(Some(format!( "External: {}\nInternal:{}", diff --git a/src/database/mod.rs b/src/database/mod.rs index 4ffb1347..5bb108e2 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -96,11 +96,11 @@ pub trait DatabaseUtils: Database { fn get_previous_output(&self, outpoint: &OutPoint) -> Result, Error> { self.get_raw_tx(&outpoint.txid)? - .and_then(|previous_tx| { + .map(|previous_tx| { if outpoint.vout as usize >= previous_tx.output.len() { - Some(Err(Error::InvalidOutpoint(outpoint.clone()))) + Err(Error::InvalidOutpoint(outpoint.clone())) } else { - Some(Ok(previous_tx.output[outpoint.vout as usize].clone())) + Ok(previous_tx.output[outpoint.vout as usize].clone()) } }) .transpose() diff --git a/src/error.rs b/src/error.rs index 22cbb09c..f7641bdd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,6 +14,10 @@ pub enum Error { InvalidAddressNetwork(Address), UnknownUTXO, DifferentTransactions, + TransactionNotFound, + TransactionConfirmed, + IrreplaceableTransaction, + FeeRateTooLow(crate::wallet::utils::FeeRate), ChecksumMismatch, DifferentDescriptorStructure, diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index fd868890..edc80360 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use std::collections::HashMap; use std::collections::{BTreeMap, HashSet}; -use std::ops::DerefMut; +use std::ops::{Deref, DerefMut}; use std::str::FromStr; use bitcoin::blockdata::opcodes; @@ -19,12 +19,13 @@ use log::{debug, error, info, trace}; pub mod coin_selection; pub mod export; +mod rbf; pub mod time; pub mod tx_builder; pub mod utils; use tx_builder::TxBuilder; -use utils::IsDust; +use utils::{FeeRate, IsDust}; use crate::blockchain::{noop_progress, Blockchain, OfflineBlockchain, OnlineBlockchain}; use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils}; @@ -308,67 +309,7 @@ where builder.ordering.modify_tx(&mut tx); let txid = tx.txid(); - let mut psbt = PSBT::from_unsigned_tx(tx)?; - - // add metadata for the inputs - for (psbt_input, input) in psbt - .inputs - .iter_mut() - .zip(psbt.global.unsigned_tx.input.iter()) - { - let prev_script = prev_script_pubkeys.get(&input.previous_output).unwrap(); - - // Add sighash, default is obviously "ALL" - psbt_input.sighash_type = builder.sighash.or(Some(SigHashType::All)); - - // Try to find the prev_script in our db to figure out if this is internal or external, - // and the derivation index - let (script_type, child) = match self - .database - .borrow() - .get_path_from_script_pubkey(&prev_script)? - { - Some(x) => x, - None => continue, - }; - - let (desc, _) = self.get_descriptor_for(script_type); - psbt_input.hd_keypaths = desc.get_hd_keypaths(child)?; - let derived_descriptor = desc.derive(child)?; - - psbt_input.redeem_script = derived_descriptor.psbt_redeem_script(); - psbt_input.witness_script = derived_descriptor.psbt_witness_script(); - - let prev_output = input.previous_output; - if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? { - if derived_descriptor.is_witness() { - psbt_input.witness_utxo = - Some(prev_tx.output[prev_output.vout as usize].clone()); - } - if !derived_descriptor.is_witness() || builder.force_non_witness_utxo { - psbt_input.non_witness_utxo = Some(prev_tx); - } - } - } - - // probably redundant but it doesn't hurt... - self.add_input_hd_keypaths(&mut psbt)?; - - // add metadata for the outputs - for (psbt_output, tx_output) in psbt - .outputs - .iter_mut() - .zip(psbt.global.unsigned_tx.output.iter()) - { - if let Some((script_type, child)) = self - .database - .borrow() - .get_path_from_script_pubkey(&tx_output.script_pubkey)? - { - let (desc, _) = self.get_descriptor_for(script_type); - psbt_output.hd_keypaths = desc.get_hd_keypaths(child)?; - } - } + let psbt = self.complete_transaction(tx, prev_script_pubkeys, builder)?; let transaction_details = TransactionDetails { transaction: None, @@ -383,6 +324,222 @@ where Ok((psbt, transaction_details)) } + // TODO: support for merging multiple transactions while bumping the fees + // TODO: option to force addition of an extra output? seems bad for privacy to update the + // change + pub fn bump_fee( + &self, + txid: &Txid, + builder: TxBuilder, + ) -> Result<(PSBT, TransactionDetails), Error> { + let mut details = match self.database.borrow().get_tx(&txid, true)? { + None => return Err(Error::TransactionNotFound), + Some(tx) if tx.transaction.is_none() => return Err(Error::TransactionNotFound), + Some(tx) if tx.height.is_some() => return Err(Error::TransactionConfirmed), + Some(tx) => tx, + }; + let mut tx = details.transaction.take().unwrap(); + if !tx.input.iter().any(|txin| txin.sequence <= 0xFFFFFFFD) { + return Err(Error::IrreplaceableTransaction); + } + + // the new tx must "pay for its bandwidth" + let vbytes = tx.get_weight() as f32 / 4.0; + let required_feerate = FeeRate::from_sat_per_vb(details.fees as f32 / vbytes + 1.0); + let new_feerate = builder.fee_rate.unwrap_or_default(); + + if new_feerate < required_feerate { + return Err(Error::FeeRateTooLow(required_feerate)); + } + let mut fee_difference = + (new_feerate.as_sat_vb() * tx.get_weight() as f32 / 4.0).ceil() as u64 - details.fees; + + if builder.send_all && tx.output.len() > 1 { + return Err(Error::SendAllMultipleOutputs); + } + + // find the index of the output that we can update. either the change or the only one if + // it's `send_all` + let updatable_output = if builder.send_all { + 0 + } else { + let mut change_output = None; + for (index, txout) in tx.output.iter().enumerate() { + // look for an output that we know and that has the right ScriptType. We use + // `get_deget_descriptor_for` to find what's the ScriptType for `Internal` + // addresses really is, because if there's no change_descriptor it's actually equal + // to "External" + let (_, change_type) = self.get_descriptor_for(ScriptType::Internal); + match self + .database + .borrow() + .get_path_from_script_pubkey(&txout.script_pubkey)? + { + Some((script_type, _)) if script_type == change_type => { + change_output = Some(index); + break; + } + _ => {} + } + } + + // we need a change output, add one here and take into account the extra fees for it + if change_output.is_none() { + let change_script = self.get_change_address()?; + let change_txout = TxOut { + script_pubkey: change_script, + value: 0, + }; + fee_difference += + (serialize(&change_txout).len() as f32 * new_feerate.as_sat_vb()).ceil() as u64; + tx.output.push(change_txout); + + change_output = Some(tx.output.len() - 1); + } + + change_output.unwrap() + }; + + // if `builder.utxos` is Some(_) we have to add inputs and we skip down to the last branch + match tx.output[updatable_output] + .value + .checked_sub(fee_difference) + { + Some(new_value) if !new_value.is_dust() && builder.utxos.is_none() => { + // try to reduce the "updatable output" amount + tx.output[updatable_output].value = new_value; + if self.is_mine(&tx.output[updatable_output].script_pubkey)? { + details.received -= fee_difference; + } + + details.fees += fee_difference; + } + _ if builder.send_all && builder.utxos.is_none() => { + // if the tx is "send_all" it doesn't make sense to either remove the only output + // or add more inputs + return Err(Error::InsufficientFunds); + } + _ => { + // initially always remove the change output + let mut removed_change_output = tx.output.remove(updatable_output); + if self.is_mine(&removed_change_output.script_pubkey)? { + details.received -= removed_change_output.value; + } + + // we want to add more inputs if: + // - builder.utxos tells us to do so + // - the removed change value is lower than the fee_difference we want to add + let needs_more_inputs = + builder.utxos.is_some() || removed_change_output.value <= fee_difference; + let added_amount = if needs_more_inputs { + // TODO: use the right weight instead of the maximum, and only fall-back to it if the + // script is unknown in the database + let input_witness_weight = std::cmp::max( + self.get_descriptor_for(ScriptType::Internal) + .0 + .max_satisfaction_weight(), + self.get_descriptor_for(ScriptType::External) + .0 + .max_satisfaction_weight(), + ); + + let (available_utxos, use_all_utxos) = self.get_available_utxos( + builder.change_policy, + &builder.utxos, + &builder.unspendable, + false, + )?; + let available_utxos = rbf::filter_available( + self.database.borrow().deref(), + available_utxos.into_iter(), + )?; + let coin_selection::CoinSelectionResult { + txin, + total_amount, + fee_amount, + } = builder.coin_selection.coin_select( + available_utxos, + use_all_utxos, + new_feerate.as_sat_vb(), + fee_difference + .checked_sub(removed_change_output.value) + .unwrap_or(0), + input_witness_weight, + 0.0, + )?; + fee_difference += fee_amount.ceil() as u64; + + // add the new inputs + let (mut txin, _): (Vec<_>, Vec<_>) = txin.into_iter().unzip(); + + // TODO: use tx_builder.sequence ?? + // copy the n_sequence from the inputs that were already in the transaction + txin.iter_mut() + .for_each(|i| i.sequence = tx.input[0].sequence); + tx.input.extend_from_slice(&mut txin); + + details.sent += total_amount; + total_amount + } else { + // otherwise just remove the output and add 0 new coins + 0 + }; + + match (removed_change_output.value + added_amount).checked_sub(fee_difference) { + None => return Err(Error::InsufficientFunds), + Some(new_value) if new_value.is_dust() => { + // the change would be dust, add that to fees + details.fees += fee_difference + new_value; + } + Some(new_value) => { + // add the change back + removed_change_output.value = new_value; + tx.output.push(removed_change_output); + + details.received += new_value; + details.fees += fee_difference; + } + } + } + }; + + // clear witnesses + for input in &mut tx.input { + input.script_sig = Script::default(); + input.witness = vec![]; + } + + // sort input/outputs according to the chosen algorithm + builder.ordering.modify_tx(&mut tx); + + // TODO: check that we are not replacing more than 100 txs from mempool + + details.txid = tx.txid(); + details.timestamp = time::get_timestamp(); + + let prev_script_pubkeys = tx + .input + .iter() + .map(|txin| { + Ok(( + txin.previous_output.clone(), + self.database + .borrow() + .get_previous_output(&txin.previous_output)?, + )) + }) + .collect::, Error>>()? + .into_iter() + .filter_map(|(outpoint, txout)| match txout { + Some(txout) => Some((outpoint, txout.script_pubkey)), + None => None, + }) + .collect(); + let psbt = self.complete_transaction(tx, prev_script_pubkeys, builder)?; + + Ok((psbt, details)) + } + // TODO: define an enum for signing errors pub fn sign(&self, mut psbt: PSBT, assume_height: Option) -> Result<(PSBT, bool), Error> { // this helps us doing our job later @@ -734,6 +891,80 @@ where } } + fn complete_transaction( + &self, + tx: Transaction, + prev_script_pubkeys: HashMap, + builder: TxBuilder, + ) -> Result { + let mut psbt = PSBT::from_unsigned_tx(tx)?; + + // add metadata for the inputs + for (psbt_input, input) in psbt + .inputs + .iter_mut() + .zip(psbt.global.unsigned_tx.input.iter()) + { + let prev_script = match prev_script_pubkeys.get(&input.previous_output) { + Some(prev_script) => prev_script, + None => continue, + }; + + // Add sighash, default is obviously "ALL" + psbt_input.sighash_type = builder.sighash.or(Some(SigHashType::All)); + + // Try to find the prev_script in our db to figure out if this is internal or external, + // and the derivation index + let (script_type, child) = match self + .database + .borrow() + .get_path_from_script_pubkey(&prev_script)? + { + Some(x) => x, + None => continue, + }; + + let (desc, _) = self.get_descriptor_for(script_type); + psbt_input.hd_keypaths = desc.get_hd_keypaths(child)?; + let derived_descriptor = desc.derive(child)?; + + psbt_input.redeem_script = derived_descriptor.psbt_redeem_script(); + psbt_input.witness_script = derived_descriptor.psbt_witness_script(); + + let prev_output = input.previous_output; + if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? { + if derived_descriptor.is_witness() { + psbt_input.witness_utxo = + Some(prev_tx.output[prev_output.vout as usize].clone()); + } + if !derived_descriptor.is_witness() || builder.force_non_witness_utxo { + psbt_input.non_witness_utxo = Some(prev_tx); + } + } + } + + // probably redundant but it doesn't hurt... + self.add_input_hd_keypaths(&mut psbt)?; + + // add metadata for the outputs + for (psbt_output, tx_output) in psbt + .outputs + .iter_mut() + .zip(psbt.global.unsigned_tx.output.iter()) + { + if let Some((script_type, child)) = self + .database + .borrow() + .get_path_from_script_pubkey(&tx_output.script_pubkey)? + { + let (desc, _) = self.get_descriptor_for(script_type); + psbt_output.hd_keypaths = desc.get_hd_keypaths(child)?; + } + } + + Ok(psbt) + } + fn add_input_hd_keypaths(&self, psbt: &mut PSBT) -> Result<(), Error> { let mut input_utxos = Vec::with_capacity(psbt.inputs.len()); for n in 0..psbt.inputs.len() { @@ -984,14 +1215,43 @@ mod test { let txid = wallet.database.borrow_mut().received_tx( testutils! { - @tx ( (@external descriptors, 0) => 50_000 ) + @tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1) }, - None, + Some(100), ); (wallet, descriptors, txid) } + macro_rules! assert_fee_rate { + ($tx:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({ + let mut tx = $tx.clone(); + $( + $( $add_signature )* + for txin in &mut tx.input { + txin.witness.push([0x00; 108].to_vec()); // fake signature + } + )* + + #[allow(unused_mut)] + #[allow(unused_assignments)] + let mut dust_change = false; + $( + $( $dust_change )* + dust_change = true; + )* + + let tx_fee_rate = $fees as f32 / (tx.get_weight() as f32 / 4.0); + let fee_rate = $fee_rate.as_sat_vb(); + + if !dust_change { + assert!((tx_fee_rate - fee_rate).abs() < 0.5, format!("Expected fee rate of {}, the tx has {}", fee_rate, tx_fee_rate)); + } else { + assert!(tx_fee_rate >= fee_rate, format!("Expected fee rate of at least {}, the tx has {}", fee_rate, tx_fee_rate)); + } + }); + } + #[test] #[should_panic(expected = "NoAddressees")] fn test_create_tx_empty_addressees() { @@ -1214,6 +1474,32 @@ mod test { ); } + #[test] + fn test_create_tx_default_fee_rate() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, details) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr.clone(), 0)]).send_all()) + .unwrap(); + + assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::default(), @add_signature); + } + + #[test] + fn test_create_tx_custom_fee_rate() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, details) = wallet + .create_tx( + TxBuilder::from_addressees(vec![(addr.clone(), 0)]) + .fee_rate(FeeRate::from_sat_per_vb(5.0)) + .send_all(), + ) + .unwrap(); + + assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(5.0), @add_signature); + } + #[test] fn test_create_tx_add_change() { use super::tx_builder::TxOrdering; @@ -1465,4 +1751,489 @@ mod test { assert!(psbt.inputs[0].non_witness_utxo.is_some()); assert!(psbt.inputs[0].witness_utxo.is_some()); } + + #[test] + #[should_panic(expected = "IrreplaceableTransaction")] + fn test_bump_fee_irreplaceable_tx() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, mut details) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)])) + .unwrap(); + let tx = psbt.extract_tx(); + let txid = tx.txid(); + // skip saving the utxos, we know they can't be used anyways + details.transaction = Some(tx); + wallet.database.borrow_mut().set_tx(&details).unwrap(); + + wallet.bump_fee(&txid, TxBuilder::new()).unwrap(); + } + + #[test] + #[should_panic(expected = "TransactionConfirmed")] + fn test_bump_fee_confirmed_tx() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, mut details) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)])) + .unwrap(); + let tx = psbt.extract_tx(); + let txid = tx.txid(); + // skip saving the utxos, we know they can't be used anyways + details.transaction = Some(tx); + details.height = Some(42); + wallet.database.borrow_mut().set_tx(&details).unwrap(); + + wallet.bump_fee(&txid, TxBuilder::new()).unwrap(); + } + + #[test] + #[should_panic(expected = "FeeRateTooLow")] + fn test_bump_fee_low_fee_rate() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, mut details) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)]).enable_rbf()) + .unwrap(); + let tx = psbt.extract_tx(); + let txid = tx.txid(); + // skip saving the utxos, we know they can't be used anyways + details.transaction = Some(tx); + wallet.database.borrow_mut().set_tx(&details).unwrap(); + + wallet + .bump_fee( + &txid, + TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(1.0)), + ) + .unwrap(); + } + + #[test] + fn test_bump_fee_reduce_change() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let (psbt, mut original_details) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr.clone(), 25_000)]).enable_rbf()) + .unwrap(); + let mut tx = psbt.extract_tx(); + let txid = tx.txid(); + // skip saving the new utxos, we know they can't be used anyways + for txin in &mut tx.input { + txin.witness.push([0x00; 108].to_vec()); // fake signature + wallet + .database + .borrow_mut() + .del_utxo(&txin.previous_output) + .unwrap(); + } + original_details.transaction = Some(tx); + wallet + .database + .borrow_mut() + .set_tx(&original_details) + .unwrap(); + + let (psbt, details) = wallet + .bump_fee( + &txid, + TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(2.5)), + ) + .unwrap(); + + assert_eq!(details.sent, original_details.sent); + assert_eq!( + details.received + details.fees, + original_details.received + original_details.fees + ); + assert!(details.fees > original_details.fees); + + let tx = &psbt.global.unsigned_tx; + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + 25_000 + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + details.received + ); + + assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(2.5), @add_signature); + } + + #[test] + fn test_bump_fee_reduce_send_all() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let (psbt, mut original_details) = wallet + .create_tx( + TxBuilder::from_addressees(vec![(addr.clone(), 0)]) + .send_all() + .enable_rbf(), + ) + .unwrap(); + let mut tx = psbt.extract_tx(); + let txid = tx.txid(); + for txin in &mut tx.input { + txin.witness.push([0x00; 108].to_vec()); // fake signature + wallet + .database + .borrow_mut() + .del_utxo(&txin.previous_output) + .unwrap(); + } + original_details.transaction = Some(tx); + wallet + .database + .borrow_mut() + .set_tx(&original_details) + .unwrap(); + + let (psbt, details) = wallet + .bump_fee( + &txid, + TxBuilder::new() + .send_all() + .fee_rate(FeeRate::from_sat_per_vb(2.5)), + ) + .unwrap(); + + assert_eq!(details.sent, original_details.sent); + assert!(details.fees > original_details.fees); + + let tx = &psbt.global.unsigned_tx; + assert_eq!(tx.output.len(), 1); + assert_eq!(tx.output[0].value + details.fees, details.sent); + + assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(2.5), @add_signature); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_bump_fee_remove_send_all_output() { + let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); + // receive an extra tx, to make sure that in case of "send_all" we get an error and it + // doesn't try to pick more inputs + let incoming_txid = wallet.database.borrow_mut().received_tx( + testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), + Some(100), + ); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let (psbt, mut original_details) = wallet + .create_tx( + TxBuilder::from_addressees(vec![(addr.clone(), 0)]) + .utxos(vec![OutPoint { + txid: incoming_txid, + vout: 0, + }]) + .send_all() + .enable_rbf(), + ) + .unwrap(); + let mut tx = psbt.extract_tx(); + let txid = tx.txid(); + for txin in &mut tx.input { + txin.witness.push([0x00; 108].to_vec()); // fake signature + wallet + .database + .borrow_mut() + .del_utxo(&txin.previous_output) + .unwrap(); + } + original_details.transaction = Some(tx); + wallet + .database + .borrow_mut() + .set_tx(&original_details) + .unwrap(); + assert_eq!(original_details.sent, 25_000); + + wallet + .bump_fee( + &txid, + TxBuilder::new() + .send_all() + .fee_rate(FeeRate::from_sat_per_vb(225.0)), + ) + .unwrap(); + } + + #[test] + fn test_bump_fee_add_input() { + let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); + wallet.database.borrow_mut().received_tx( + testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), + Some(100), + ); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let (psbt, mut original_details) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr.clone(), 45_000)]).enable_rbf()) + .unwrap(); + let mut tx = psbt.extract_tx(); + let txid = tx.txid(); + // skip saving the new utxos, we know they can't be used anyways + for txin in &mut tx.input { + txin.witness.push([0x00; 108].to_vec()); // fake signature + wallet + .database + .borrow_mut() + .del_utxo(&txin.previous_output) + .unwrap(); + } + original_details.transaction = Some(tx); + wallet + .database + .borrow_mut() + .set_tx(&original_details) + .unwrap(); + + let (psbt, details) = wallet + .bump_fee( + &txid, + TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(50.0)), + ) + .unwrap(); + + assert_eq!(details.sent, original_details.sent + 25_000); + assert_eq!(details.fees + details.received, 30_000); + + let tx = &psbt.global.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + 45_000 + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + details.received + ); + + assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(50.0), @add_signature); + } + + #[test] + fn test_bump_fee_no_change_add_input_and_change() { + let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); + let incoming_txid = wallet.database.borrow_mut().received_tx( + testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), + Some(100), + ); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let (psbt, mut original_details) = wallet + .create_tx( + TxBuilder::from_addressees(vec![(addr.clone(), 0)]) + .send_all() + .add_utxo(OutPoint { + txid: incoming_txid, + vout: 0, + }) + .enable_rbf(), + ) + .unwrap(); + let mut tx = psbt.extract_tx(); + let txid = tx.txid(); + // skip saving the new utxos, we know they can't be used anyways + for txin in &mut tx.input { + txin.witness.push([0x00; 108].to_vec()); // fake signature + wallet + .database + .borrow_mut() + .del_utxo(&txin.previous_output) + .unwrap(); + } + original_details.transaction = Some(tx); + wallet + .database + .borrow_mut() + .set_tx(&original_details) + .unwrap(); + + // NOTE: we don't set "send_all" here. so we have a transaction with only one input, but + // here we are allowed to add more, and we will also have to add a change + let (psbt, details) = wallet + .bump_fee( + &txid, + TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(50.0)), + ) + .unwrap(); + + let original_send_all_amount = original_details.sent - original_details.fees; + assert_eq!(details.sent, original_details.sent + 50_000); + assert_eq!( + details.received, + 75_000 - original_send_all_amount - details.fees + ); + + let tx = &psbt.global.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + original_send_all_amount + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + 75_000 - original_send_all_amount - details.fees + ); + + assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(50.0), @add_signature); + } + + #[test] + fn test_bump_fee_add_input_change_dust() { + let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); + wallet.database.borrow_mut().received_tx( + testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), + Some(100), + ); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let (psbt, mut original_details) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr.clone(), 45_000)]).enable_rbf()) + .unwrap(); + let mut tx = psbt.extract_tx(); + assert_eq!(tx.input.len(), 1); + assert_eq!(tx.output.len(), 2); + let txid = tx.txid(); + // skip saving the new utxos, we know they can't be used anyways + for txin in &mut tx.input { + txin.witness.push([0x00; 108].to_vec()); // fake signature + wallet + .database + .borrow_mut() + .del_utxo(&txin.previous_output) + .unwrap(); + } + original_details.transaction = Some(tx); + wallet + .database + .borrow_mut() + .set_tx(&original_details) + .unwrap(); + + let (psbt, details) = wallet + .bump_fee( + &txid, + TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(140.0)), + ) + .unwrap(); + + assert_eq!(original_details.received, 5_000 - original_details.fees); + + assert_eq!(details.sent, original_details.sent + 25_000); + assert_eq!(details.fees, 30_000); + assert_eq!(details.received, 0); + + let tx = &psbt.global.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 1); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + 45_000 + ); + + assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(140.0), @dust_change, @add_signature); + } + + #[test] + fn test_bump_fee_force_add_input() { + let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); + let incoming_txid = wallet.database.borrow_mut().received_tx( + testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), + Some(100), + ); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let (psbt, mut original_details) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr.clone(), 45_000)]).enable_rbf()) + .unwrap(); + let mut tx = psbt.extract_tx(); + let txid = tx.txid(); + // skip saving the new utxos, we know they can't be used anyways + for txin in &mut tx.input { + txin.witness.push([0x00; 108].to_vec()); // fake signature + wallet + .database + .borrow_mut() + .del_utxo(&txin.previous_output) + .unwrap(); + } + original_details.transaction = Some(tx); + wallet + .database + .borrow_mut() + .set_tx(&original_details) + .unwrap(); + + // the new fee_rate is low enough that just reducing the change would be fine, but we force + // the addition of an extra input with `add_utxo()` + let (psbt, details) = wallet + .bump_fee( + &txid, + TxBuilder::new() + .add_utxo(OutPoint { + txid: incoming_txid, + vout: 0, + }) + .fee_rate(FeeRate::from_sat_per_vb(5.0)), + ) + .unwrap(); + + assert_eq!(details.sent, original_details.sent + 25_000); + assert_eq!(details.fees + details.received, 30_000); + + let tx = &psbt.global.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + 45_000 + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + details.received + ); + + assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(5.0), @add_signature); + } } diff --git a/src/wallet/rbf.rs b/src/wallet/rbf.rs new file mode 100644 index 00000000..cd7327ee --- /dev/null +++ b/src/wallet/rbf.rs @@ -0,0 +1,103 @@ +use crate::database::Database; +use crate::error::Error; +use crate::types::*; + +/// Filters unspent utxos +pub(super) fn filter_available, D: Database>( + database: &D, + iter: I, +) -> Result, Error> { + Ok(iter + .map(|utxo| { + Ok(match database.get_tx(&utxo.outpoint.txid, true)? { + None => None, + Some(tx) if tx.height.is_none() => None, + Some(_) => Some(utxo), + }) + }) + .collect::, Error>>()? + .into_iter() + .filter_map(|x| x) + .collect()) +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use bitcoin::{OutPoint, Transaction, TxIn, TxOut, Txid}; + + use super::*; + use crate::database::{BatchOperations, MemoryDatabase}; + + fn add_transaction( + database: &mut MemoryDatabase, + spend: Vec, + outputs: Vec, + ) -> Txid { + let tx = Transaction { + version: 1, + lock_time: 0, + input: spend + .iter() + .cloned() + .map(|previous_output| TxIn { + previous_output, + ..Default::default() + }) + .collect(), + output: outputs + .iter() + .cloned() + .map(|value| TxOut { + value, + ..Default::default() + }) + .collect(), + }; + let txid = tx.txid(); + + for input in &spend { + database.del_utxo(input).unwrap(); + } + for vout in 0..outputs.len() { + database + .set_utxo(&UTXO { + txout: tx.output[vout].clone(), + outpoint: OutPoint { + txid, + vout: vout as u32, + }, + is_internal: true, + }) + .unwrap(); + } + database + .set_tx(&TransactionDetails { + txid, + transaction: Some(tx), + height: None, + ..Default::default() + }) + .unwrap(); + + txid + } + + #[test] + fn test_filter_available() { + let mut database = MemoryDatabase::new(); + add_transaction( + &mut database, + vec![OutPoint::from_str( + "aad194c72fd5cfd16d23da9462930ca91e35df1cfee05242b62f4034f50c3d41:5", + ) + .unwrap()], + vec![50_000], + ); + + let filtered = + filter_available(&database, database.iter_utxos().unwrap().into_iter()).unwrap(); + assert_eq!(filtered, &[]); + } +} diff --git a/src/wallet/utils.rs b/src/wallet/utils.rs index 465036f0..f6c287cf 100644 --- a/src/wallet/utils.rs +++ b/src/wallet/utils.rs @@ -14,7 +14,7 @@ impl IsDust for u64 { } } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] // Internally stored as satoshi/vbyte pub struct FeeRate(f32); diff --git a/testutils-macros/src/lib.rs b/testutils-macros/src/lib.rs index 1a8d42a8..b70a37ec 100644 --- a/testutils-macros/src/lib.rs +++ b/testutils-macros/src/lib.rs @@ -363,6 +363,135 @@ pub fn magical_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenSt wallet.sync(None).unwrap(); assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent); } + + #[test] + #[serial] + fn test_sync_bump_fee() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + let node_addr = test_client.get_node_address(None); + + test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1) + }); + + wallet.sync(None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000); + + let (psbt, details) = wallet.create_tx(TxBuilder::from_addressees(vec![(node_addr.clone(), 5_000)]).enable_rbf()).unwrap(); + let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(psbt.extract_tx()).unwrap(); + wallet.sync(None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fees - 5_000); + assert_eq!(wallet.get_balance().unwrap(), details.received); + + let (new_psbt, new_details) = wallet.bump_fee(&details.txid, TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(2.1))).unwrap(); + let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(new_psbt.extract_tx()).unwrap(); + wallet.sync(None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fees - 5_000); + assert_eq!(wallet.get_balance().unwrap(), new_details.received); + + assert!(new_details.fees > details.fees); + } + + #[test] + #[serial] + fn test_sync_bump_fee_remove_change() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + let node_addr = test_client.get_node_address(None); + + test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1) + }); + + wallet.sync(None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000); + + let (psbt, details) = wallet.create_tx(TxBuilder::from_addressees(vec![(node_addr.clone(), 49_000)]).enable_rbf()).unwrap(); + let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(psbt.extract_tx()).unwrap(); + wallet.sync(None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fees); + assert_eq!(wallet.get_balance().unwrap(), details.received); + + let (new_psbt, new_details) = wallet.bump_fee(&details.txid, TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(5.0))).unwrap(); + + let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(new_psbt.extract_tx()).unwrap(); + wallet.sync(None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 0); + assert_eq!(new_details.received, 0); + + assert!(new_details.fees > details.fees); + } + + #[test] + #[serial] + fn test_sync_bump_fee_add_input() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + let node_addr = test_client.get_node_address(None); + + test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1) + }); + + wallet.sync(None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 75_000); + + let (psbt, details) = wallet.create_tx(TxBuilder::from_addressees(vec![(node_addr.clone(), 49_000)]).enable_rbf()).unwrap(); + let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(psbt.extract_tx()).unwrap(); + wallet.sync(None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees); + assert_eq!(details.received, 1_000 - details.fees); + + let (new_psbt, new_details) = wallet.bump_fee(&details.txid, TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(10.0))).unwrap(); + + let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(new_psbt.extract_tx()).unwrap(); + wallet.sync(None).unwrap(); + assert_eq!(new_details.sent, 75_000); + assert_eq!(wallet.get_balance().unwrap(), new_details.received); + } + + #[test] + #[serial] + fn test_sync_bump_fee_add_input_no_change() { + let (wallet, descriptors, mut test_client) = init_single_sig(); + let node_addr = test_client.get_node_address(None); + + test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1) + }); + + wallet.sync(None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 75_000); + + let (psbt, details) = wallet.create_tx(TxBuilder::from_addressees(vec![(node_addr.clone(), 49_000)]).enable_rbf()).unwrap(); + let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(psbt.extract_tx()).unwrap(); + wallet.sync(None).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees); + assert_eq!(details.received, 1_000 - details.fees); + + let (new_psbt, new_details) = wallet.bump_fee(&details.txid, TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(123.0))).unwrap(); + println!("{:#?}", new_details); + + let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap(); + assert!(finalized, "Cannot finalize transaction"); + wallet.broadcast(new_psbt.extract_tx()).unwrap(); + wallet.sync(None).unwrap(); + assert_eq!(new_details.sent, 75_000); + assert_eq!(wallet.get_balance().unwrap(), 0); + assert_eq!(new_details.received, 0); + } } };