Skip to content

fix(key-wallet): never offer an outpoint the builder already holds - #931

Merged
ZocoLini merged 1 commit into
devfrom
fix/add-funding-dedup
Aug 7, 2026
Merged

fix(key-wallet): never offer an outpoint the builder already holds#931
ZocoLini merged 1 commit into
devfrom
fix/add-funding-dedup

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Aug 7, 2026

Copy link
Copy Markdown
Member

What was fixed

add_funding (the additive replacement for set_funding, #925) appends every unreserved UTXO of a funding account to the candidate set without checking what the builder already holds. If the builder was seeded with one of those same UTXOs — add_inputs with a caller-chosen outpoint, or an earlier add_funding of an overlapping account — the outpoint became two candidates.

Coin selection performs no outpoint deduplication. SelectionStrategy::All clones every candidate, so the duplicate was guaranteed to reach the transaction; the ordinary strategies can select both copies and double-count them toward the target. Either way the built transaction spends one prevout twice, and Core rejects it.

This hazard is specific to additive funding: the set_funding it replaced did self.inputs = …, so a pre-seeded outpoint was silently dropped. Losing a caller's explicit input is a bug too, but building an invalid transaction is strictly worse, so the fix filters candidates rather than restoring the overwrite.

Reservation correctness

The funding entry still records every unreserved outpoint the account owns that is in the candidate pool — including one pre-seeded by add_inputs — so if selection picks it, the owning account reserves it. Recording only the UTXOs this call appended would leave a pre-seeded input unreserved and free for a concurrent build to select, which is the double-spend window the reservation system exists to close.

Testing

add_funding_does_not_duplicate_a_pre_seeded_input seeds a UTXO via add_inputs, funds the account that owns it, and drains with SelectionStrategy::All. It asserts the outpoint is offered exactly once, that the built transaction has no duplicate prevouts, and that the pre-seeded input is reserved. Verified it fails without the fix (the outpoint appears twice in the candidate list) and passes with it. Full key-wallet suite green (628 passed), clippy --all-targets -D warnings clean, cargo fmt clean.

Why now

Found while reviewing dashpay/platform#4329, which adopts multi-account funding: platform's FFI exposes core_wallet_tx_builder_add_inputs_from_outpoints (Swift addInputs), which seeds the builder with UTXOs drawn from the wallet's own account, and then finalizes through add_funding on that same account — exactly the sequence that duplicates. That platform PR needs a re-pin onto this fix before it can merge.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved transaction funding to prevent duplicate inputs when multiple funding sources overlap.
    • Ensured existing transaction inputs are correctly accounted for and reserved during funding.
    • Added safeguards to produce transactions with unique previous outputs and more reliable input selection.

`add_funding` appends every unreserved UTXO of a funding account to the
candidate set. When the builder was already seeded with one of those UTXOs —
`add_inputs` with a caller-chosen outpoint, or an earlier `add_funding` of an
overlapping account — the same outpoint became TWO candidates.

Coin selection does not deduplicate by outpoint: `SelectionStrategy::All`
takes every candidate, so the duplicate was guaranteed to reach the
transaction, and the ordinary strategies could pick both copies and
double-count them toward the target. Either way the result is a transaction
spending one prevout twice, which Core rejects.

This is specific to additive funding. The `set_funding` it replaced assigned
`self.inputs`, so a pre-seeded outpoint was silently dropped rather than
duplicated — an invalid transaction is the strictly worse failure, so fix it
where the candidates are built.

The account still records every unreserved outpoint it owns that is in the
candidate pool, including a pre-seeded one, so that if selection picks it the
owning account reserves it. Recording only the UTXOs this call appended would
leave a pre-seeded input unreserved and free for a concurrent build to select.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c81b66c3-9268-445e-8fcc-967ee23964f3

📥 Commits

Reviewing files that changed from the base of the PR and between 944e53a and 14eaa66.

📒 Files selected for processing (1)
  • key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs

📝 Walkthrough

Walkthrough

add_funding now prevents duplicate candidate outpoints and records pre-seeded eligible inputs for reservation. A regression test validates unique transaction prevouts and ownership-based reservation.

Changes

Funding outpoint deduplication

Layer / File(s) Summary
Funding selection and reservation validation
key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
add_funding tracks existing inputs, skips duplicate candidates, and records eligible account-owned outpoints. The regression test verifies unique prevouts and reservation by the owning account.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: ready-for-review

Suggested reviewers: zocolini, xdustinface, romchornyi

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: preventing the builder from receiving duplicate outpoints.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/add-funding-dedup

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.51%. Comparing base (944e53a) to head (14eaa66).

Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #931      +/-   ##
==========================================
+ Coverage   75.28%   75.51%   +0.23%     
==========================================
  Files         328      328              
  Lines       77982    78017      +35     
==========================================
+ Hits        58710    58918     +208     
+ Misses      19272    19099     -173     
Flag Coverage Δ
core 77.29% <ø> (ø)
ffi 51.49% <ø> (+1.64%) ⬆️
rpc 20.00% <ø> (ø)
spv 91.28% <ø> (+0.01%) ⬆️
wallet 76.88% <100.00%> (+0.03%) ⬆️
Files with missing lines Coverage Δ
.../wallet/managed_wallet_info/transaction_builder.rs 89.30% <100.00%> (+0.34%) ⬆️

... and 19 files with indirect coverage changes

QuantumExplorer added a commit to dashpay/platform that referenced this pull request Aug 7, 2026
…and guard duplicate prevouts

Review follow-ups on the pooled send path.

Contributors vs offered accounts. `funding_accounts` was pushed when an
account's UTXOs were OFFERED to selection, so a transaction funded entirely
from BIP44 still reported BIP32 and every DashPay contact as a contributor —
violating the field's documented contract and scaling release and registry
bookkeeping with the address book. Membership now rides a HashSet (the linear
`contains` made pooled funding quadratic in contact count), the ordered offered
list drives build-time cleanup only, and the list stored on the transaction is
derived by mapping each selected prevout back to the account that owns it.

Pooled shortfalls. A pooled build's `available`/`required` describe the union of
every offered source, so attributing them to the first preference reported
aggregate figures as "insufficient funds on BIP44 account 0" — and could name
BIP44 even when no such account existed and it was skipped. Single-source
builds keep the account-specific error; pooled builds get
`CorePooledInsufficientFunds`, carrying the source list instead of one account.
Both map to the same FFI code, so hosts classify a shortfall exactly as before.

Duplicate prevouts. Additive funding can offer an outpoint the builder was
already seeded with through `add_inputs` (the `add_inputs_from_outpoints` FFI
draws from the wallet's own account), and coin selection does not deduplicate,
so the transaction could spend one prevout twice — invalid, and Core rejects
it. Fixed upstream in dashpay/rust-dashcore#931; asserted here as well rather
than handing a signer and then the network a transaction that cannot confirm.

Swift `buildSignedPayment` defaulted to `.bip44` while its Kotlin counterpart
defaults to pooled, so Swift callers omitting `accountType` could hit an
insufficient-funds error with a sufficient pooled balance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the ready-for-review CodeRabbit has approved this PR label Aug 7, 2026
@ZocoLini
ZocoLini merged commit 6be2407 into dev Aug 7, 2026
38 checks passed
@ZocoLini
ZocoLini deleted the fix/add-funding-dedup branch August 7, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-review CodeRabbit has approved this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants