Skip to content

Conversation

@nymius
Copy link
Contributor

@nymius nymius commented Jan 13, 2025

Description

There were multiple calls for de-duplication of selected UTxOs in Wallet::create_tx: (1) and (2).

As the test test_filter_duplicates shows, there are four possible cases for duplication of UTxOs while feeding the coin selection algorithms.

  1. no duplication: out of concern
  2. duplication in the required utxos only: covered by the source of required_utxos, Wallet::list_unspent, which roots back the provided UTxOs to a HashMap which should avoid any duplication by definition
  3. duplication in the optional utxos only: is the only one possible as optional UTxOs are stored in a Vec and no checks are performed about the duplicity of their members.
  4. duplication across the required and optional utxos: is already covered by Wallet::preselect_utxos, which avoid the processing of required UTxOs while listing the unspent available UTxOs in the wallet.

This refactor does the following:

  • Refactors TxParams::utxos type to be HashSet<LocalOutput> avoiding the duplication case 3
  • Keeps avoiding case 4 without overhead and allowing a query closer to O(1) on avg. to cover duplication case 4 (before was O(n) where n is the size of required utxos) thanks to the above change.
  • Still covers case 2 because the source of required_utxos, Wallet::list_unspent comes from a HashMap which should avoid duplication by definition.
  • Moves the computation of the WeightedUtxos to the last part of UTxO filtering, allowing the unification of the computation for local outputs.
  • Removes some extra allocations done for helpers structures or intermediate results while filtering UTxOs.
  • Allows for future integration of UTxO filtering methods for other utilities, e.g.: filter locked utxos
    .filter(|utxo| !self.is_utxo_locked(utxo.outpoint, self.latest_checkpoint().height()))
  • Adds more comments for each filtering step.
  • Differentiates foreign_utxos, which should include a provided satisfation weight to use them effectively, and utxos, manually selected UTxOs for which the wallet can compute their satisfaction weight without external resources.

With these changes all four cases would be covered, and coin_selection::filter_duplicates is no longer needed.

Fixes #1794.

Notes to the reviewers

I added three test to cover the interesting cases for duplication:
- there are no duplicates in required utxos
- there are no duplicates in optional utxos
- there are no duplicates across optional and required utxos
the three of them have been prefixed with not_duplicated_utxos* to allow its joint execution under the command:

cargo test -- not_duplicated_utxos

because the guarantees for the three conditions above are spread in different parts of the code.

Changelog notice

No changes to public APIs.

Checklists

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing
  • This pull request does not break the existing API
  • I've added tests to reproduce the issue which are now passing
  • I'm linking the issue being fixed by this PR

@evanlinjin
Copy link
Member

Hey @nymius, does this actually fix something or is this purely a refactor?

Note that we want to redo the tx building / creating logic to use bdk_coin_select and miniscript planning module so I think it's good policy to only do changes that are fixes or implement features that users are requesting.

@nymius
Copy link
Contributor Author

nymius commented Jan 14, 2025

Hey @nymius, does this actually fix something or is this purely a refactor?

Hi @evanlinjin. Yes, maybe I went to far with the changes, still, it fixes the duplicate issue I open in #1794 by using a HashSet to handle manually selected UTxOs.

Then, about the refactor:

The research of the code push me to isolate the pre selection steps as items on a checklist and that's why It ended up as a refactor.

My idea is to further isolate each filter in their own iterator adaptor (a separated function for each one) that consumes impl Iterator<Item = LocalOutputs> and returns the same type, to later also allow the introduction of custom filters in TxParams which I think can be helpful to implement checks on the ability to spend a Taproot output, for example, in Silent Payments.

The heavy use of iterators came for trying to avoid the allocation of helpers, like satisfies_confirmed.

I envisioned something like this:

let optional_utxos = self
    .list_unspent()
    .check_are_not_already_manually_selected()
    .check_are_not_unspendable()
    .check_confirmed_only_if_RBF()
    .check_is_local_utxo()
    .check_is_mature_if_coinbase();
// then
let (required, optional) = optional_utxos.chain(required_utxos.iter().clone())
    .get_weighted_utxos()
    .chain(foreign_utxos.iter().clone())
    .apply_custom_validation_for_all_tx_inputs()
    .split_utxos_in_required_and_optional()

Discussing this today with @ValuedMammal, I decided to do the following changes:

  1. Just return optional utxos as they are the only needed values.
  2. Move the calculus of WeightedUtxos to another method. Placed inside create_tx for now
  3. Rename preselect_utxos to filter_utxos to correct the semantics the above point will change.
  4. Implement unit test for the feature to make the PR sound.
  5. Make current_height parameter not optional.

If we don't agree on the above, I propose the following alternatives:

  • keep points 1 and 2 above.
  • keep the old filtering implementation in place.
  • keep the changes to the build_fee_bump (new foreign_utxos field in TxParams).
  • drop all the other changes.

@nymius nymius force-pushed the refactor/use-iterators-to-preselect-utxos branch from 05ac09c to e42b5aa Compare January 15, 2025 21:30
@nymius nymius force-pushed the refactor/use-iterators-to-preselect-utxos branch 3 times, most recently from 05d94f3 to 388b7cc Compare January 28, 2025 16:04
@nymius
Copy link
Contributor Author

nymius commented Jan 28, 2025

Rebased

@ValuedMammal
Copy link
Collaborator

ValuedMammal commented Feb 4, 2025

📌 I don't think we've considered what would happen if a third party provides us with a "foreign" utxo that actually duplicates a utxo owned by the wallet, potentially causing us to sign something we didn't agree to. It might need more research so I'll open an issue. bitcoindevkit/bdk_wallet#29

@nymius nymius force-pushed the refactor/use-iterators-to-preselect-utxos branch from 388b7cc to e8e21e1 Compare February 5, 2025 20:16
@nymius
Copy link
Contributor Author

nymius commented Feb 5, 2025

📌 I don't think we've considered what would happen if a third party provides us with a "foreign" utxo that actually duplicates a utxo owned by the wallet, potentially causing us to sign something we didn't agree to. It might need more research so I'll open an issue. bitcoindevkit/bdk_wallet#29

I created a separated draft PR #1823 to address the issue.

@nymius
Copy link
Contributor Author

nymius commented Feb 7, 2025

review club notes 02/06/2025

  1. Include psbt::Input::non_witness_utxo field together with psbt::Input::non_witness_utxo when txout.script_pubkey.witness_version().is_some() while re constructing foreign UTxOs in Wallet::build_fee_bump.
  2. The relevance of the integration of miniscript plan (feat(wallet)!: use miniscript plan in place of old policy mod #1786 ) for this PR was mentioned. Wallet::get_available_utxos obtained the satisfaction weight of UTxOs before passing them to preselect_utxos. As satisfaction weight is dependent of planning, there should be future work to integrate this, as it is a previous step to Wallet::filter_utxos.
  3. The split of the PR was suggested, in two different ones to allow a faster review and merge of the fix for Off-by-one error is making optional UTxOs selection too restrictive for coinbase outputs #1810.
  4. As commits addressing suggested changes had been acknowledge by reviewers, the next step should be to squash them in four or less commits:
    1. Refactor
    2. coin_selection::filter_duplicates removal.
    3. Tests
    4. Miscellaneous not related to refactor
  5. As part of the review we talked about [wallet] Document about extra validation when signing transactions including foreign utxos bdk_wallet#29 and its fix candidate Check foreign utxos are not local before inclusion #1823 :
    1. As Check foreign utxos are not local before inclusion #1823 introduces breaking changes, a compromise solution would be to add a silent check which works as a no-op in case transaction is found to be owned by the wallet. Also, one possible approach is to delay the introduction of the new NotForeignUtxo variant error (breaking change) until the release of the new major version, 2.0.0 with a TODO comment.
    2. It wasn't clear the ownership of the transactions stored in TxGraph. The terms canonical and relevant were mentioned as a good source to get an idea of the topic from the documentation.
      Wallet::transactions has a good explanation of this.
      TxGraph's transactions don't have to belong to the wallet necessarily, and get_txout shouldn't be considered a good source of truth to establish if an Outpoint can be unlocked by Wallet or not. Wallet::is_mine is the correct method for this.

Thanks to @ValuedMammal, @oleonardolima, @LagginTimes and @evanlinjin for participating!

@nymius
Copy link
Contributor Author

nymius commented Feb 7, 2025

Next steps based on review club:

@nymius nymius force-pushed the refactor/use-iterators-to-preselect-utxos branch from e8e21e1 to 905c8dd Compare February 10, 2025 14:19
@nymius
Copy link
Contributor Author

nymius commented Feb 10, 2025

905c8dd will be squashed to b1bff02 once #1830 is merged

@nymius
Copy link
Contributor Author

nymius commented Feb 10, 2025

Now that foreign UTxOs are separated from manually selected UTxOs there are new cases for duplicity:

  1. Duplicated UTxOs inside foreign_utxos field.
  2. Duplicated UTxOs across foreign_utxos and required/optional UTxOs. Shouldn't be a problem once Check foreign utxos are not local before inclusion #1823 is merged. Because of the new check which avoids the addition of local UTxOs for Wallet as foreign UTxOs.

TODO:

  • Ensure foreign_utxos does not contain duplicated UTxOs: TxParams::foreign_utxos transformed into HashSet in 7cb0243 and added test to confirm property. Will be squashed to b1bff02 once change is ACKnowledge. The avoidance of duplicity across foreign and optional/required UTxOs will be guaranteed by changes in Check foreign utxos are not local before inclusion #1823

@nymius nymius force-pushed the refactor/use-iterators-to-preselect-utxos branch from 7cb0243 to 49867ad Compare February 11, 2025 19:08
pub(crate) external_policy_path: Option<BTreeMap<String, Vec<usize>>>,
pub(crate) utxos: Vec<WeightedUtxo>,
pub(crate) utxos: HashSet<LocalOutput>,
pub(crate) foreign_utxos: HashSet<WeightedUtxo>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried that this won't be sufficient to check that the outpoint is unique. For instance, one could change the satisfaction weight and get a different WeightedUtxo while using the same outpoint.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, will think more about this. Will reproduce your case in a test and use a HashMap with outpoint as key in the meantime.

Copy link
Contributor Author

@nymius nymius Feb 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I modified the test slightly to cover that case. I didn't add a different one because it is not modifying the logic being tested, but just being more precise with the assertions.

I used HashMap instead of BTreeMap because we don't need the ordering features, however it can be argued that BTreeMap is already use it in crates/wallet/src/wallet/tx_builder.rs and we are adding a new import.
I prefered the former over the later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid the duplication concern we had for #1582, we can implement Hash manually for WeightedUTxO and make it use the Hash implementation of Outpoint.

Copy link
Contributor Author

@nymius nymius Feb 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized this won't be enough, as HashSet requires the following for keys k1, k2:
$k1 == k2 \Rightarrow \text{hash}(k1) == \text{hash}(k2)$.
But this should enforced by us. If we implement Hash as I said above and don't also modify PartialEq implementation to be compatible with this, the case where outpoint is the same but weight is different will introduce two different utxos in foreign utxos set.

I wouldn't modify PartialEq because is probably going to break other logic depending of the usual way of comparing two elements (i.e. considering all its fields).

HashMap is still the best solution so far.

@nymius nymius force-pushed the refactor/use-iterators-to-preselect-utxos branch from 49867ad to 1b90b92 Compare February 12, 2025 20:15
evanlinjin added a commit that referenced this pull request Feb 14, 2025
…nal UTxOs

03b7eca fix(wallet): off-by-one error checking coinbase maturity in optional UTxOs (nymius)

Pull request description:

  ### Description

  As I was developing the changes in #1798 I discover issue #1810. So I introduced the fixes in that PR but later I split them in two to ease the review by suggestion of @oleonardolima .

  The `preselect_utxos` method has an off-by-one error that is making the selection of optional UTxOs too restrictive, by
  requiring the coinbase outputs to surpass or equal coinbase maturity time at the current height of the selection, and
  not in the block in which the transaction may be included in the blockchain.

  The changes in this commit fix it by considering the maturity of the coinbase output at the spending height and not
  the transaction creation height, this means, a +1 at the considered height at the moment of building the
  transaction.

  Fixes #1810.

  ### Notes to the reviewers

  Tests for issue #1810 have not been explicitly added, as there already was a `text_spend_coinbase` test which was corrected to ensure coinbase maturation is considered in alignment with the new logic.

  Changes are not breaking but I'm modifying slightly the documentation for the public method `TxBuilder::current_height` to adjust to the fixed code. Does this merit an entry in the CHANGELOG?

  ### Changelog notice

  `Wallet` now considers a utxo originated from a coinbase transaction (`coinbase utxo`) as available for selection if it will mature in the next block after the height provided to the selection, the current height by default. The previous behavior allowed selecting a `coinbase utxo` only when the height at the moment of selection was equal to maturity height or greater.

  ### Checklists

  * [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 fmt` and `cargo clippy` before committing
  * [x] I've updated existing tests to match the fix
  * [x] I've updated docs to match the fix logic
  * [x] This pull request DOES NOT break the existing API
  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  LagginTimes:
    ACK 03b7eca
  evanlinjin:
    ACK 03b7eca

Tree-SHA512: f270b73963bd6f141c8a3e759bc9b9bf75de7c52f37fff93f0a6b8b996b449d98c58e5eeb2b56f0ee236222f0807da5c8201ade7462813743e0c4d255313e2b5
@nymius nymius force-pushed the refactor/use-iterators-to-preselect-utxos branch from 1b90b92 to ebecafc Compare February 14, 2025 21:13
@nymius
Copy link
Contributor Author

nymius commented Feb 14, 2025

905c8dd will be squashed to b1bff02 once #1830 is merged

Squash done and rebased on master.

@ValuedMammal ValuedMammal added this to the 1.2.0 milestone Feb 25, 2025
@nymius nymius force-pushed the refactor/use-iterators-to-preselect-utxos branch 2 times, most recently from 8de57db to d08c270 Compare February 27, 2025 01:12
@nymius
Copy link
Contributor Author

nymius commented Feb 27, 2025

Thanks @ValuedMammal! Updated and rebased

@nymius nymius force-pushed the refactor/use-iterators-to-preselect-utxos branch from d08c270 to 375dff4 Compare February 28, 2025 03:38
@nymius
Copy link
Contributor Author

nymius commented Feb 28, 2025

Addressed comments and rebased onto master.

@nymius nymius force-pushed the refactor/use-iterators-to-preselect-utxos branch from 375dff4 to 6ae12d4 Compare February 28, 2025 04:35
@nymius nymius force-pushed the refactor/use-iterators-to-preselect-utxos branch from 6ae12d4 to a1611dc Compare February 28, 2025 04:56
Copy link
Member

@evanlinjin evanlinjin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation suggestions. Otherwise, LGTM

///
/// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
/// the "utxos" and the "unspendable" list, it will be spent.
pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, AddUtxoError> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we would error here if an OutPoint conflicts with a foreign UTXO. However, we will need to add new error variants (which is a MAJOR change since the error type does not have non-exhaustive).

For now, how about we make a note in the docs that if you add the same UTXO as both foreign and local, the latest change has precedence (please word that better).

Same goes for the add-foreign-utxo methods.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the add-foreign-utxo methods, ideally we will error out if it conflicts with pre-existing in TxParams::utxos or utxos in TxBuilder::wallet. However, the error variant is not non-exhaustive so that will be breaking.

For now, how about we just note that it would be unexpected behavior in the docs?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nymius had a better idea of making local UTXOs have precedence over foreign UTXOs. Let's do this and also update the documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added the documentation requested and two tests to address the discussed cases. #1823 will fix the issue against tracked local UTxOs (the one the wallet already has knowledge about, aka optional).

@nymius nymius force-pushed the refactor/use-iterators-to-preselect-utxos branch 2 times, most recently from f5828c0 to 0d20209 Compare March 3, 2025 06:30
@nymius
Copy link
Contributor Author

nymius commented Mar 3, 2025

Rebased onto b26ff89

@nymius nymius requested a review from evanlinjin March 3, 2025 06:30
nymius added 3 commits March 6, 2025 10:47
There were multiple calls for de-duplication of selected UTxOs.

As the test `test_filter_duplicates` shows, there are four possible
cases for duplication of UTxOs while feeding the coin selection
algorithms.

1. no duplication: out of concern
2. duplication in the required utxos only: covered by the source of
   `required_utxos`, `Wallet::list_unspent`, which roots back the
   provided `UTxOs` to a `HashMap` which should avoid any duplication by
   definition
3. duplication in the optional utxos only: is the only one possible as
   optional `UTxOs` are stored in a `Vec` and no checks are performed
   about the duplicity of their members.
4. duplication across the required and optional utxos: is already
   covered by `Wallet::preselect_utxos`, which avoid the processing of
   required UTxOs while listing the unspent available UTxOs in the
   wallet.

This refactor changes:
- `TxParams::utxos` type to be `HashSet<LocalOutput>` avoiding the
  duplication case 3, and allowing a query closer to O(1) on avg. to
  cover duplication case 4 (before was O(n) where n is the size of
  required utxos).
- Moves the computation of the `WeightedUtxos` to the last part of UTxO
  filtering, allowing the unification of the computation for local
  outputs.
- Removes some extra allocations done for helpers structures or
  intermediate results while filtering UTxOs.
- Allows for future integration of UTxO filtering methods for other
  utilities.
- Adds more comments for each filtering step.

With these changes all four cases would be covered, and
`coin_selection::filter_duplicates` would be no longer needed.
…nal utxos

This test replaces the one used to test
`coin_selection::filter_duplicates` introduced in
5299db3.

As the code changed and there is not a single point to verificate the
following properties:
- there are no duplicates in required utxos
- there are no duplicates in optional utxos
- there are no duplicates across optional and required utxos
anymore, test have been prefixed with `not_duplicated_utxos*` to allow
its joint execution by using the following command:
cargo test -- not_duplicated_utxos
@nymius nymius force-pushed the refactor/use-iterators-to-preselect-utxos branch from 0d20209 to 2f83b45 Compare March 5, 2025 23:48
@nymius
Copy link
Contributor Author

nymius commented Mar 5, 2025

Rebased onto 362c3dc

Copy link
Member

@evanlinjin evanlinjin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 2f83b45

Just a nit for future reference.

Comment on lines +1633 to 1690
graph
// Get previous transaction
.get_tx(txin.previous_output.txid)
.ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?;
let txout = &prev_tx.output[txin.previous_output.vout as usize];

let chain_position = chain_positions
.get(&txin.previous_output.txid)
.cloned()
.ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?;

let weighted_utxo = match txout_index.index_of_spk(txout.script_pubkey.clone()) {
Some(&(keychain, derivation_index)) => {
let satisfaction_weight = self
.public_descriptor(keychain)
.max_weight_to_satisfy()
.unwrap();
WeightedUtxo {
utxo: Utxo::Local(LocalOutput {
outpoint: txin.previous_output,
txout: txout.clone(),
keychain,
is_spent: true,
derivation_index,
chain_position,
}),
satisfaction_weight,
}
}
None => {
let satisfaction_weight = Weight::from_wu_usize(
serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len(),
);
WeightedUtxo {
utxo: Utxo::Foreign {
outpoint: txin.previous_output,
sequence: txin.sequence,
psbt_input: Box::new(psbt::Input {
witness_utxo: Some(txout.clone()),
non_witness_utxo: Some(prev_tx.as_ref().clone()),
..Default::default()
}),
},
satisfaction_weight,
.ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))
// Get chain position
.and_then(|prev_tx| {
chain_positions
.get(&txin.previous_output.txid)
.cloned()
.ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))
.map(|chain_position| (prev_tx, chain_position))
})
.map(|(prev_tx, chain_position)| {
let txout = prev_tx.output[txin.previous_output.vout as usize].clone();
match txout_index.index_of_spk(txout.script_pubkey.clone()) {
Some(&(keychain, derivation_index)) => (
txin.previous_output,
WeightedUtxo {
satisfaction_weight: self
.public_descriptor(keychain)
.max_weight_to_satisfy()
.unwrap(),
utxo: Utxo::Local(LocalOutput {
outpoint: txin.previous_output,
txout: txout.clone(),
keychain,
is_spent: true,
derivation_index,
chain_position,
}),
},
),
None => {
let satisfaction_weight = Weight::from_wu_usize(
serialize(&txin.script_sig).len() * 4
+ serialize(&txin.witness).len(),
);

(
txin.previous_output,
WeightedUtxo {
utxo: Utxo::Foreign {
outpoint: txin.previous_output,
sequence: txin.sequence,
psbt_input: Box::new(psbt::Input {
witness_utxo: txout
.script_pubkey
.witness_version()
.map(|_| txout.clone()),
non_witness_utxo: Some(prev_tx.as_ref().clone()),
..Default::default()
}),
},
satisfaction_weight,
},
)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: trying to make this section fully functional has made it harder to read. There is weird map statements, and it becomes hard to see whether we are map-ing on an iterator or option.

I would prefer the following:

                let prev_tx = graph
                    .get_tx(txin.previous_output.txid)
                    .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?;
                let chain_position = chain_positions
                    .get(&txin.previous_output.txid)
                    .cloned()
                    .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?;
                let txout = prev_tx.output[txin.previous_output.vout as usize].clone();
                let (outpoint, weighted_utxo) = match txout_index
                    .index_of_spk(txout.script_pubkey.clone())
                {
                    Some(&(keychain, derivation_index)) => (
                        txin.previous_output,
                        WeightedUtxo {
                            satisfaction_weight: self
                                .public_descriptor(keychain)
                                .max_weight_to_satisfy()
                                .unwrap(),
                            utxo: Utxo::Local(LocalOutput {
                                outpoint: txin.previous_output,
                                txout: txout.clone(),
                                keychain,
                                is_spent: true,
                                derivation_index,
                                chain_position,
                            }),
                        },
                    ),
                    None => {
                        let satisfaction_weight = Weight::from_wu_usize(
                            serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len(),
                        );
                        (
                            txin.previous_output,
                            WeightedUtxo {
                                utxo: Utxo::Foreign {
                                    outpoint: txin.previous_output,
                                    sequence: txin.sequence,
                                    psbt_input: Box::new(psbt::Input {
                                        witness_utxo: txout
                                            .script_pubkey
                                            .witness_version()
                                            .map(|_| txout.clone()),
                                        non_witness_utxo: Some(prev_tx.as_ref().clone()),
                                        ..Default::default()
                                    }),
                                },
                                satisfaction_weight,
                            },
                        )
                    }
                };
                Ok((outpoint, weighted_utxo))

Copy link
Contributor Author

@nymius nymius Mar 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, will try to control the bias towards functional style.

@evanlinjin evanlinjin merged commit 1975835 into bitcoindevkit:master Mar 6, 2025
23 checks passed
@github-project-automation github-project-automation bot moved this from Needs Review to Done in BDK Wallet Mar 6, 2025
@ValuedMammal ValuedMammal mentioned this pull request Apr 3, 2025
41 tasks
oleonardolima added a commit to bitcoindevkit/bdk_wallet that referenced this pull request Jul 1, 2025
…xOrdering::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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

Utxo filtering done twice (presumed redundantly) while creating transaction

4 participants