Skip to content

Commit dae7419

Browse files
committed
Merge #349: Backport #337 - fix(wallet): Don't fail in build_fee_bump for missing parent txid
238c82a ci: Pin `quote` to 1.0.41 (valued mammal) 2f643dd fix(wallet): Don't fail in `build_fee_bump` for missing parent txid (valued mammal) 43529b3 test: Add `test_bump_fee_pay_to_anchor_foreign_utxo` (valued mammal) Pull request description: ### Description Backport #337 to the `release/2.x` branch in preparation for `wallet-2.3.0`. ### 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) * [x] I ran `just p` before pushing #### Bugfixes: * ~~[ ] This pull request breaks the existing API~~ * [x] I've added tests to reproduce the issue which are now passing ACKs for top commit: notmandatory: ACK 238c82a Tree-SHA512: 30797c689f9260f4c1f94a57de0f9cc954e209e52516fced3999c399e89a227d31f60d3b7e798b2f62287103703b7c4c8bb99b8fc9826190d54a50d17794e0b4
2 parents abc9cd8 + 238c82a commit dae7419

File tree

4 files changed

+105
-68
lines changed

4 files changed

+105
-68
lines changed

ci/pin-msrv.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ set -euo pipefail
1212

1313
cargo update -p once_cell --precise "1.20.3"
1414
cargo update -p syn --precise "2.0.106"
15+
cargo update -p quote --precise "1.0.41"

wallet/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ workspace = true
1818
[dependencies]
1919
rand_core = { version = "0.6.0" }
2020
miniscript = { version = "12.3.1", features = [ "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
serde = { version = "^1.0", features = ["derive"] }
2323
serde_json = { version = "^1.0" }
2424
bdk_chain = { version = "0.23.1", features = [ "miniscript", "serde" ], default-features = false }

wallet/src/wallet/mod.rs

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

1725-
let mut tx = graph
1725+
let mut tx = tx_graph
17261726
.get_tx(txid)
17271727
.ok_or(BuildFeeBumpError::TransactionNotFound(txid))?
17281728
.as_ref()
@@ -1749,73 +1749,62 @@ impl Wallet {
17491749
let fee = self
17501750
.calculate_fee(&tx)
17511751
.map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?;
1752-
let fee_rate = self
1753-
.calculate_fee_rate(&tx)
1754-
.map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?;
1752+
let fee_rate = fee / tx.weight();
17551753

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

18201809
if tx.output.len() > 1 {
18211810
let mut change_index = None;
@@ -1835,7 +1824,6 @@ impl Wallet {
18351824
}
18361825

18371826
let params = TxParams {
1838-
// TODO: figure out what rbf option should be?
18391827
version: Some(tx.version),
18401828
recipients: tx
18411829
.output

wallet/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)