Skip to content

Commit b925402

Browse files
committed
Merge #262: Preserve insertion order of manually selected utxos if TxOrdering::Untouched
0522114 test(tx_builder): update precedence check of local UTXOs over add_foreign_utxos (valued mammal) 73bef28 doc(tx_builder): add info about manually selected UTxOs priority (nymius) 3316236 fix(tx_builder): preserve insertion order with TxOrdering::Untouched (nymius) Pull request description: ### Description On my attempt to fix bitcoindevkit/bdk#1794 in bitcoindevkit/bdk#1798, I broke the assumption that insertion order is preserved when `TxBuilder::ordering` is `TxOrdering::Untouched`. Some users are relying in this assumption, so here I'm trying to restore it back, without adding a new dependency for this single use case like #252, or creating a new struct just to track this. In this fourth alternative solution I'm going back to use `Vec` to store the manually selected UTxOs. I was reluctant to do it in this way because `HashMap` seems a better solution giving its property of avoiding duplicates, but as we also want to keep the secuential nature of the insertion of UTxOs in `TxBuilder`, here is an alternative aligned with that principle. May replace #252 May replace #261 . Fixes #244 ### Notes to the reviewers Also, as I was working on this, I came back to the following tests: - `test_prexisting_foreign_utxo_have_no_precedence_over_local_utxo_with_same_outpoint` - `test_prexisting_local_utxo_have_precedence_over_foreign_utxo_with_same_outpoint` Motivated during the implementation and review of bitcoindevkit/bdk#1798. Which required the underlying structure to also hold the properties of no duplication for manually selected UTxOs, as the structures were accessed directly for these cases. The test tries to cover the case when there are two wallets using the same descriptor, one tracks transactions and the other does not. The first passes UTxOs belonging to the second one and this one creates transactions using the `add_foreign_utxo` interface. In this case it was expected for any `LocalUtxo` of the offline wallet to supersede any conflicting foreign UTxO. But, the simulation of this case went against the borrowing constraints of rust. By how costly was to reproduce this behavior for me in the tests, I would like to have second opinions in the feasibility of the test case. ### Changelog notice No public APIs are changed by these commits. ### Checklists > [!IMPORTANT] > This pull request **DOES NOT** break the existing API * [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 `cargo +nightly fmt` and `cargo clippy` before committing * [x] I've added tests for the new code * [x] I've expanded docs addressing new code * [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: ValuedMammal: reACK 0522114 oleonardolima: ACK 0522114 Tree-SHA512: f2726d75eab83e28cc748ac5cd6bd0c7f3dddb409ac61baf7d0a7030ddf81c11b10dbd5b18e8ac3d29a6afb4b8f29ee9a88f83094aebec771fdb4da2cd718326
2 parents d0ba53c + 0522114 commit b925402

File tree

3 files changed

+244
-237
lines changed

3 files changed

+244
-237
lines changed

wallet/src/wallet/mod.rs

Lines changed: 43 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,7 +1511,7 @@ impl Wallet {
15111511

15121512
let (required_utxos, optional_utxos) = {
15131513
// NOTE: manual selection overrides unspendable
1514-
let mut required: Vec<WeightedUtxo> = params.utxos.values().cloned().collect();
1514+
let mut required: Vec<WeightedUtxo> = params.utxos.clone();
15151515
let optional = self.filter_utxos(&params, current_height.to_consensus_u32());
15161516

15171517
// if drain_wallet is true, all UTxOs are required
@@ -1740,52 +1740,45 @@ impl Wallet {
17401740
})
17411741
.map(|(prev_tx, prev_txout, chain_position)| {
17421742
match txout_index.index_of_spk(prev_txout.script_pubkey.clone()) {
1743-
Some(&(keychain, derivation_index)) => (
1744-
txin.previous_output,
1745-
WeightedUtxo {
1746-
satisfaction_weight: self
1747-
.public_descriptor(keychain)
1748-
.max_weight_to_satisfy()
1749-
.unwrap(),
1750-
utxo: Utxo::Local(LocalOutput {
1751-
outpoint: txin.previous_output,
1752-
txout: prev_txout.clone(),
1753-
keychain,
1754-
is_spent: true,
1755-
derivation_index,
1756-
chain_position,
1757-
}),
1758-
},
1759-
),
1743+
Some(&(keychain, derivation_index)) => WeightedUtxo {
1744+
satisfaction_weight: self
1745+
.public_descriptor(keychain)
1746+
.max_weight_to_satisfy()
1747+
.unwrap(),
1748+
utxo: Utxo::Local(LocalOutput {
1749+
outpoint: txin.previous_output,
1750+
txout: prev_txout.clone(),
1751+
keychain,
1752+
is_spent: true,
1753+
derivation_index,
1754+
chain_position,
1755+
}),
1756+
},
17601757
None => {
17611758
let satisfaction_weight = Weight::from_wu_usize(
17621759
serialize(&txin.script_sig).len() * 4
17631760
+ serialize(&txin.witness).len(),
17641761
);
1765-
1766-
(
1767-
txin.previous_output,
1768-
WeightedUtxo {
1769-
utxo: Utxo::Foreign {
1770-
outpoint: txin.previous_output,
1771-
sequence: txin.sequence,
1772-
psbt_input: Box::new(psbt::Input {
1773-
witness_utxo: prev_txout
1774-
.script_pubkey
1775-
.witness_version()
1776-
.map(|_| prev_txout.clone()),
1777-
non_witness_utxo: Some(prev_tx.as_ref().clone()),
1778-
..Default::default()
1779-
}),
1780-
},
1781-
satisfaction_weight,
1762+
WeightedUtxo {
1763+
utxo: Utxo::Foreign {
1764+
outpoint: txin.previous_output,
1765+
sequence: txin.sequence,
1766+
psbt_input: Box::new(psbt::Input {
1767+
witness_utxo: prev_txout
1768+
.script_pubkey
1769+
.witness_version()
1770+
.map(|_| prev_txout.clone()),
1771+
non_witness_utxo: Some(prev_tx.as_ref().clone()),
1772+
..Default::default()
1773+
}),
17821774
},
1783-
)
1775+
satisfaction_weight,
1776+
}
17841777
}
17851778
}
17861779
})
17871780
})
1788-
.collect::<Result<HashMap<OutPoint, WeightedUtxo>, BuildFeeBumpError>>()?;
1781+
.collect::<Result<Vec<WeightedUtxo>, BuildFeeBumpError>>()?;
17891782

17901783
if tx.output.len() > 1 {
17911784
let mut change_index = None;
@@ -2099,6 +2092,11 @@ impl Wallet {
20992092
vec![]
21002093
// only process optional UTxOs if manually_selected_only is false
21012094
} else {
2095+
let manually_selected_outpoints = params
2096+
.utxos
2097+
.iter()
2098+
.map(|wutxo| wutxo.utxo.outpoint())
2099+
.collect::<HashSet<OutPoint>>();
21022100
self.indexed_graph
21032101
.graph()
21042102
// get all unspent UTxOs from wallet
@@ -2116,9 +2114,10 @@ impl Wallet {
21162114
.is_mature(current_height)
21172115
.then(|| new_local_utxo(k, i, full_txo))
21182116
})
2119-
// only process UTxOs not selected manually, they will be considered later in the
2120-
// chain NOTE: this avoid UTxOs in both required and optional list
2121-
.filter(|may_spend| !params.utxos.contains_key(&may_spend.outpoint))
2117+
// only process UTXOs not selected manually, they will be considered later in the
2118+
// chain
2119+
// NOTE: this avoid UTXOs in both required and optional list
2120+
.filter(|may_spend| !manually_selected_outpoints.contains(&may_spend.outpoint))
21222121
// only add to optional UTxOs those which satisfy the change policy if we reuse
21232122
// change
21242123
.filter(|local_output| {
@@ -2745,18 +2744,10 @@ mod test {
27452744
let txid = two_output_tx.compute_txid();
27462745
insert_tx(&mut wallet, two_output_tx);
27472746

2748-
let mut params = TxParams::default();
2749-
let output = wallet.get_utxo(OutPoint { txid, vout: 0 }).unwrap();
2750-
params.utxos.insert(
2751-
output.outpoint,
2752-
WeightedUtxo {
2753-
satisfaction_weight: wallet
2754-
.public_descriptor(output.keychain)
2755-
.max_weight_to_satisfy()
2756-
.unwrap(),
2757-
utxo: Utxo::Local(output),
2758-
},
2759-
);
2747+
let outpoint = OutPoint { txid, vout: 0 };
2748+
let mut builder = wallet.build_tx();
2749+
builder.add_utxo(outpoint).expect("should add local utxo");
2750+
let params = builder.params.clone();
27602751
// enforce selection of first output in transaction
27612752
let received = wallet.filter_utxos(&params, wallet.latest_checkpoint().block_id().height);
27622753
// notice expected doesn't include the first output from two_output_tx as it should be

0 commit comments

Comments
 (0)