fix(wallet): exclude unconfirmed v3 outputs from non-v3 coin selection#442
fix(wallet): exclude unconfirmed v3 outputs from non-v3 coin selection#442EliteCoder18 wants to merge 2 commits intobitcoindevkit:masterfrom
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #442 +/- ##
==========================================
+ Coverage 80.21% 80.24% +0.02%
==========================================
Files 24 24
Lines 5348 5355 +7
Branches 242 243 +1
==========================================
+ Hits 4290 4297 +7
Misses 980 980
Partials 78 78
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…_unchecked be710a8 chore(tests): replace deprecated FeeRate::from_sat_per_vb_unchecked (none34829) Pull request description: ### Description `bitcoin-units 0.1.3` deprecated `FeeRate::from_sat_per_vb_unchecked` in favor of `FeeRate::from_sat_per_vb_u32`. Cargo resolves any 0.1.x automatically, so fresh CI runs started failing on master (and all open PRs) once 0.1.3 was published. This patch swaps every call site over to the new name. The two functions compute the same value for any input that fits in `u32`; every existing call site passes a small literal (1, 3, 5, 10, 25, 50, 255, 454, 1000, 10_000), so no behavioural change. All usages are in test/fixture code (`src/wallet/coin_selection.rs` test module, `tests/wallet.rs`, `tests/build_fee_bump.rs`) — no production code path was using the deprecated function. ### Notes to the reviewers - Pure mechanical rename; no logic or type changes. - Unblocks CI on master and every open PR (e.g. #449, #442, #445, #448). ### Changelog notice N/A — internal test-only change. ### 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 ACKs for top commit: ValuedMammal: ACK be710a8 Tree-SHA512: b2354fcf6e39228bc5796f92af4d3692fbdb750267c2c5a7d814fc5c84f4af64b746562fda7e01dac282ba04acb17ff7bf5c7ef0155a3d6f302974003baf0629
oleonardolima
left a comment
There was a problem hiding this comment.
Thanks for working on this one!
I'm still wondering if it's the best way to address the issue. I'll give it a try on some other alternatives.
| &self, | ||
| params: &TxParams, | ||
| current_height: u32, | ||
| tx_version: transaction::Version, |
There was a problem hiding this comment.
You don't need to add a new argument, it's already available in TxParams.
There was a problem hiding this comment.
Done.... removed the extra argument and reading from TxParams.version directly now.
There was a problem hiding this comment.
Yes but we already validated the transaction version around line 1294, so not re-initializing it with unwrap_or(Version::TWO) would also make sense?
There was a problem hiding this comment.
Good point, thanks!
You're right, we already validate and resolve the transaction version earlier in create_tx. Re-doing unwrap_or(Version::TWO) inside filter_utxos is redundant.
I'll update it to pass the already-resolved version directly into filter_utxos from the call site instead of re-deriving it inside the function.
yan-pi
left a comment
There was a problem hiding this comment.
First I reproduced #419 against pre-fix bdk-cli and bitcoind rejects on broadcast as expected.
Validated this branch with a local path dep:
create_tx returns InsufficientFunds, available == confirmed matched exactly.
Covered direction works as advertised.
That said, Fixes #419 closes the issue on merge, but some gaps remain:
- v3 -> non-v3 unconfirmed (other half of BIP-431 Rule 2).
Reproducible on this branch. add_utxo/add_foreign_utxobypass: PR body mentions, rustdoc doesn't.- vB rules (BIP-431 Rules 3/4/5): out of scope in this PR.
But oleonardolima asked for this in the issue.
I think you should either expand scope, or change body to "addresses part of #419" and link follow-ups before merge, dunno about how this last one is handled in BDK org.
| // NOTE: manual selection overrides unspendable | ||
| let mut required: Vec<WeightedUtxo> = params.utxos.clone(); | ||
| let optional = self.filter_utxos(¶ms, current_height.to_consensus_u32()); | ||
| let optional = self.filter_utxos(¶ms, current_height.to_consensus_u32(), version); |
There was a problem hiding this comment.
The bypass for add_utxo / add_foreign_utxo is mentioned in the PR body, but not in the rustdoc of those methods.
IMO worth a note.
An integrator using manual selection might assume "fixes #419" covers them and still hit TRUC-violation.
There was a problem hiding this comment.
Thanks for calling this out.... That's a great point. I’ve now added explicit rustdoc notes on both TxBuilder::add_utxo and TxBuilder::add_foreign_utxo clarifying that manually selected UTXOs bypass optional-UTXO filtering (including TRUC version-compatibility checks), so callers must validate unconfirmed manual inputs against the target tx version themselves.
This should make the behavior clear for integrators and avoid a false assumption that the #419 fix also covers manual selection paths.
| || tx_graph | ||
| .get_tx(local_output.outpoint.txid) | ||
| .is_none_or(|tx| tx.version != transaction::Version(3)) | ||
| }) |
There was a problem hiding this comment.
BIP-431 Rule 2 has two clauses, this predicate only covers one:
Any TRUC transaction's unconfirmed ancestors must all be TRUC. Any descendant of an unconfirmed TRUC transaction must also be TRUC.
When target is v3, the filter is off: coin selection can pick non-v3 unconfirmed UTXOs and bitcoind rejects on broadcast.
Same shape as #419, other direction.
Reproduced on this branch:
1.5 BTC confirmed + 0.3 BTC v2 unconfirmed, target v3 - 1.6 BTC -> PSBT includes the v2 unconfirmed.
Attempting broadcast (expected: TRUC-violation)...
bitcoind rejected: JSON-RPC error: RPC error response: RpcError { code: -26, message: "TRUC-violation, version=
3 tx 6d60a9caac7631812c5ed083c8c583d3cd9382485640aa7a543d3c5d19a00419 (wtxid=726b4a450e32084d5237c8a098f9e26c399b
e7f27ae3af7d328bb000ddb4aa19) cannot spend from non-version=3 tx 8ff29f49e540dbd32c9d1207628861504e0f981a1f5a2025
b6550bd380118724 (wtxid=38b52d8ad666a8296a4c4383e4572dc8b45cc796203e85d39033b772cabc1242)", data: None }
Not sure if you'd want to fix here or create a follow-up issue/pr?
There was a problem hiding this comment.
The bidirectional filter is included in the latest revision both directions are covered by (tx.version.0 == 3) == target_is_truc. Could you re-test against the current HEAD? Happy to look into it further if the violation still reproduces.
When a v3 (TRUC) transaction produced change back into the wallet, that unconfirmed output entered coin selection like any other UTXO. Building a subsequent non-v3 transaction that spent it caused bitcoind to reject the broadcast with "TRUC-violation, non-version=3 tx cannot spend from version=3 tx" (BIP-431). Thread `tx_version` into `filter_utxos` so that unconfirmed UTXOs whose parent is version 3 are excluded when building a non-v3 transaction. Confirmed outputs are unaffected since TRUC rules only apply while the parent remains unconfirmed. Fixes bitcoindevkit#419.
Add two tests covering BIP-431 (TRUC) coin selection behavior. `test_create_tx_non_v3_excludes_unconfirmed_v3_utxos` verifies that a non-v3 transaction cannot coin-select an unconfirmed v3 parent output, while a v3 transaction targeting the same output succeeds. `test_create_tx_non_v3_allows_unconfirmed_non_v3_utxos` verifies that unconfirmed outputs from non-v3 parents remain eligible for standard transactions, preserving existing behavior.
cd4dd53 to
230ebcb
Compare
Description
BIP-431 (TRUC) requires that any transaction spending an unconfirmed v3 output must itself be v3. BDK's coin selection had no awareness of this and would freely include unconfirmed v3 parent outputs when building standard non-v3 transactions, causing bitcoind to reject the broadcast.
This PR threads
tx_versionintofilter_utxosto exclude such UTXOs when the target transaction is not v3. Confirmed outputs are unaffected.Fixes #419.
Notes to the reviewers
The fix is intentionally minimal only the cross-version spending restriction from BIP-431 is addressed here. The broader TRUC topological constraints (cluster size, child vbyte limit) are out of scope for this PR and can be tracked separately.
Manually selected UTXOs via
TxBuilder::add_utxobypassfilter_utxosentirely and are not covered by this fix. Callers are responsible for not adding unconfirmed v3 outputs to non-v3 transactions manually.Changelog notice
filter_utxosnow excludes unconfirmed UTXOs from v3 parent transactions when constructing non-v3 transactions, preventing TRUC policy violations at broadcast.Checklists
All Submissions:
just pbefore pushingBugfixes: