Skip to content

Commit b420e64

Browse files
committed
Merge bitcoindevkit#337: fix(wallet): Don't fail in build_fee_bump for missing parent txid
f15582d fix(wallet): Don't fail in `build_fee_bump` for missing parent txid (valued mammal) 992a08f test: Add `test_bump_fee_pay_to_anchor_foreign_utxo` (valued mammal) Pull request description: ### Description Previously `build_fee_bump` could error with `UnknownUtxo` if a parent of the tx being fee-bumped wasn't found in the wallet. This made it impossible to use `build_fee_bump` on a transaction created using `add_foreign_utxo`. This PR is a refactor of `build_fee_bump` that manages to avoid the error by instead querying the tx graph for the txout specifically. This is a reasonable assumption because the previous txouts are necessary to compute the fee of the original tx. I've included a test in 992a08f to demonstrate the old behavior which now passes. may resolve bitcoindevkit#325. ### Notes to the reviewers ### Changelog notice - Fix `Wallet::build_fee_bump` to enable fee-bumping a tx which contains a foreign UTXO. ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) #### New Features: #### Bugfixes: * ~~[ ] This pull request breaks the existing API~~ * [x] I've added tests to reproduce the issue which are now passing * [x] I'm linking the issue being fixed by this PR ACKs for top commit: nymius: ACK f15582d notmandatory: ACK f15582d Tree-SHA512: 51248984a4e0eec30093aab7283057d50f356827ec267d6ed957f33525157a3ba0e3fdf69d57ae497b635e39f8dbf8bc88b89b01a95267092bdb43f7a9ef3f54
2 parents 6e94275 + f15582d commit b420e64

3 files changed

Lines changed: 103 additions & 67 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ rustdoc-args = ["--cfg", "docsrs"]
1818

1919
[dependencies]
2020
bdk_chain = { version = "0.23.1", features = ["miniscript", "serde"], default-features = false }
21-
bitcoin = { version = "0.32.6", features = ["serde", "base64"], default-features = false }
21+
bitcoin = { version = "0.32.7", features = ["serde", "base64"], default-features = false }
2222
miniscript = { version = "12.3.1", features = ["serde"], default-features = false }
2323
rand_core = { version = "0.6.0" }
2424
serde_json = { version = "1" }

src/wallet/mod.rs

Lines changed: 52 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1711,15 +1711,15 @@ impl Wallet {
17111711
&mut self,
17121712
txid: Txid,
17131713
) -> Result<TxBuilder<'_, DefaultCoinSelectionAlgorithm>, BuildFeeBumpError> {
1714-
let graph = self.indexed_graph.graph();
1714+
let tx_graph = self.indexed_graph.graph();
17151715
let txout_index = &self.indexed_graph.index;
17161716
let chain_tip = self.chain.tip().block_id();
1717-
let chain_positions = graph
1717+
let chain_positions: HashMap<Txid, ChainPosition<_>> = tx_graph
17181718
.list_canonical_txs(&self.chain, chain_tip, CanonicalizationParams::default())
17191719
.map(|canon_tx| (canon_tx.tx_node.txid, canon_tx.chain_position))
1720-
.collect::<HashMap<Txid, _>>();
1720+
.collect();
17211721

1722-
let mut tx = graph
1722+
let mut tx = tx_graph
17231723
.get_tx(txid)
17241724
.ok_or(BuildFeeBumpError::TransactionNotFound(txid))?
17251725
.as_ref()
@@ -1746,73 +1746,62 @@ impl Wallet {
17461746
let fee = self
17471747
.calculate_fee(&tx)
17481748
.map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?;
1749-
let fee_rate = self
1750-
.calculate_fee_rate(&tx)
1751-
.map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?;
1749+
let fee_rate = fee / tx.weight();
17521750

17531751
// Remove the inputs from the tx and process them.
1754-
let utxos = tx
1752+
let utxos: Vec<WeightedUtxo> = tx
17551753
.input
17561754
.drain(..)
17571755
.map(|txin| -> Result<_, BuildFeeBumpError> {
1758-
graph
1759-
// Get previous transaction.
1760-
.get_tx(txin.previous_output.txid)
1761-
.ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))
1762-
// Get chain position.
1763-
.and_then(|prev_tx| {
1756+
let outpoint = txin.previous_output;
1757+
let prev_txout = tx_graph
1758+
.get_txout(outpoint)
1759+
.cloned()
1760+
.ok_or(BuildFeeBumpError::UnknownUtxo(outpoint))?;
1761+
match txout_index.index_of_spk(prev_txout.script_pubkey.clone()) {
1762+
Some(&(keychain, derivation_index)) => {
1763+
let txout = prev_txout;
17641764
let chain_position = chain_positions
1765-
.get(&txin.previous_output.txid)
1765+
.get(&outpoint.txid)
17661766
.cloned()
1767-
.ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?;
1768-
let prev_txout = prev_tx
1769-
.output
1770-
.get(txin.previous_output.vout as usize)
1771-
.ok_or(BuildFeeBumpError::InvalidOutputIndex(txin.previous_output))
1772-
.cloned()?;
1773-
Ok((prev_tx, prev_txout, chain_position))
1774-
})
1775-
.map(|(prev_tx, prev_txout, chain_position)| {
1776-
match txout_index.index_of_spk(prev_txout.script_pubkey.clone()) {
1777-
Some(&(keychain, derivation_index)) => WeightedUtxo {
1778-
satisfaction_weight: self
1779-
.public_descriptor(keychain)
1780-
.max_weight_to_satisfy()
1781-
.unwrap(),
1782-
utxo: Utxo::Local(LocalOutput {
1783-
outpoint: txin.previous_output,
1784-
txout: prev_txout.clone(),
1785-
keychain,
1786-
is_spent: true,
1787-
derivation_index,
1788-
chain_position,
1789-
}),
1790-
},
1791-
None => {
1792-
let satisfaction_weight = Weight::from_wu_usize(
1793-
serialize(&txin.script_sig).len() * 4
1794-
+ serialize(&txin.witness).len(),
1795-
);
1796-
WeightedUtxo {
1797-
utxo: Utxo::Foreign {
1798-
outpoint: txin.previous_output,
1799-
sequence: txin.sequence,
1800-
psbt_input: Box::new(psbt::Input {
1801-
witness_utxo: prev_txout
1802-
.script_pubkey
1803-
.witness_version()
1804-
.map(|_| prev_txout.clone()),
1805-
non_witness_utxo: Some(prev_tx.as_ref().clone()),
1806-
..Default::default()
1807-
}),
1808-
},
1809-
satisfaction_weight,
1810-
}
1811-
}
1812-
}
1813-
})
1767+
.ok_or(BuildFeeBumpError::TransactionNotFound(outpoint.txid))?;
1768+
Ok(WeightedUtxo {
1769+
satisfaction_weight: self
1770+
.public_descriptor(keychain)
1771+
.max_weight_to_satisfy()
1772+
.expect("descriptor should be satisfiable"),
1773+
utxo: Utxo::Local(LocalOutput {
1774+
outpoint,
1775+
txout,
1776+
keychain,
1777+
is_spent: true,
1778+
derivation_index,
1779+
chain_position,
1780+
}),
1781+
})
1782+
}
1783+
None => Ok(WeightedUtxo {
1784+
satisfaction_weight: Weight::from_wu_usize(
1785+
serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len(),
1786+
),
1787+
utxo: Utxo::Foreign {
1788+
outpoint,
1789+
sequence: txin.sequence,
1790+
psbt_input: Box::new(psbt::Input {
1791+
witness_utxo: prev_txout
1792+
.script_pubkey
1793+
.witness_version()
1794+
.map(|_| prev_txout),
1795+
non_witness_utxo: tx_graph
1796+
.get_tx(outpoint.txid)
1797+
.map(|tx| tx.as_ref().clone()),
1798+
..Default::default()
1799+
}),
1800+
},
1801+
}),
1802+
}
18141803
})
1815-
.collect::<Result<Vec<WeightedUtxo>, BuildFeeBumpError>>()?;
1804+
.collect::<Result<_, _>>()?;
18161805

18171806
if tx.output.len() > 1 {
18181807
let mut change_index = None;
@@ -1832,7 +1821,6 @@ impl Wallet {
18321821
}
18331822

18341823
let params = TxParams {
1835-
// TODO: figure out what rbf option should be?
18361824
version: Some(tx.version),
18371825
recipients: tx
18381826
.output

tests/build_fee_bump.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ use bdk_wallet::psbt::PsbtUtils;
88
use bdk_wallet::test_utils::*;
99
use bdk_wallet::KeychainKind;
1010
use bitcoin::{
11-
absolute, transaction, Address, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction,
12-
TxOut,
11+
absolute, hashes::Hash, psbt, transaction, Address, Amount, FeeRate, OutPoint, ScriptBuf,
12+
Sequence, Transaction, TxOut, Weight,
1313
};
1414

1515
mod common;
@@ -944,3 +944,51 @@ fn test_legacy_bump_fee_absolute_add_input() {
944944

945945
assert_eq!(fee, Amount::from_sat(6_000));
946946
}
947+
948+
// Test that we can fee-bump a tx containing a foreign (p2a) utxo.
949+
#[test]
950+
fn test_bump_fee_pay_to_anchor_foreign_utxo() {
951+
let (mut wallet, _) = get_funded_wallet_wpkh();
952+
let drain_spk = wallet
953+
.next_unused_address(KeychainKind::External)
954+
.script_pubkey();
955+
956+
let witness_utxo = TxOut {
957+
value: Amount::ONE_SAT,
958+
script_pubkey: bitcoin::ScriptBuf::new_p2a(),
959+
};
960+
// Remember to include this as a "floating" txout in the wallet.
961+
let outpoint = OutPoint::new(Hash::hash(b"prev"), 1);
962+
wallet.insert_txout(outpoint, witness_utxo.clone());
963+
let satisfaction_weight = Weight::from_wu(71);
964+
let psbt_input = psbt::Input {
965+
witness_utxo: Some(witness_utxo),
966+
..Default::default()
967+
};
968+
969+
let mut tx_builder = wallet.build_tx();
970+
tx_builder
971+
.add_foreign_utxo(outpoint, psbt_input, satisfaction_weight)
972+
.unwrap()
973+
.only_witness_utxo()
974+
.fee_rate(FeeRate::from_sat_per_vb_unchecked(2))
975+
.drain_to(drain_spk.clone());
976+
let psbt = tx_builder.finish().unwrap();
977+
let tx = psbt.unsigned_tx.clone();
978+
assert!(tx.input.iter().any(|txin| txin.previous_output == outpoint));
979+
let txid1 = tx.compute_txid();
980+
wallet.apply_unconfirmed_txs([(tx, 123456)]);
981+
982+
// Now build fee bump.
983+
let mut tx_builder = wallet
984+
.build_fee_bump(txid1)
985+
.expect("`build_fee_bump` should succeed");
986+
tx_builder
987+
.set_recipients(vec![])
988+
.drain_to(drain_spk)
989+
.only_witness_utxo()
990+
.fee_rate(FeeRate::from_sat_per_vb_unchecked(5));
991+
let psbt = tx_builder.finish().unwrap();
992+
let tx = &psbt.unsigned_tx;
993+
assert!(tx.input.iter().any(|txin| txin.previous_output == outpoint));
994+
}

0 commit comments

Comments
 (0)