Implement RBF and add a few tests

This commit is contained in:
Alekos Filini
2020-08-13 16:51:27 +02:00
parent 8f8c393f6f
commit c12aa3d327
7 changed files with 1167 additions and 70 deletions

View File

@@ -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<Cs: coin_selection::CoinSelectionAlgorithm>(
&self,
txid: &Txid,
builder: TxBuilder<Cs>,
) -> 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::<Result<Vec<_>, 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<u32>) -> Result<(PSBT, bool), Error> {
// this helps us doing our job later
@@ -734,6 +891,80 @@ where
}
}
fn complete_transaction<Cs: coin_selection::CoinSelectionAlgorithm>(
&self,
tx: Transaction,
prev_script_pubkeys: HashMap<OutPoint, Script>,
builder: TxBuilder<Cs>,
) -> Result<PSBT, Error> {
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);
}
}