wallet: fix redeemdigidollar failure on wallets with fragmented DGB UTXOs - #439
Open
JohnnyLawDGB wants to merge 1 commit into
Open
Conversation
DigiDollar redemptions failed with "Insufficient fee inputs for DD redemption fee" on wallets whose DGB was fragmented into small UTXOs, even when the wallet held plenty of spendable DGB. DigiDollarWallet::SelectFeeCoins() treated a fee UTXO's raw value as its contribution to the fee. It is not. Every input added to a DigiDollar transaction enlarges the transaction the fee is computed from: at the DD fee rate (35,000,000 sat/kvB) and with the per-input accounting in DigiDollar::EstimateTransactionVSize() (41 base bytes plus a flat 110 witness bytes, +35% margin, i.e. ~92 vB), one extra input costs about 3,220,000 sat to spend. Any fee UTXO below ~0.0322 DGB therefore has negative effective value, and SelectFeeCoins() sorted smallest-first and stopped as soon as the raw sum reached the target, so it selected precisely those. The caller compounded it: redeemdigidollar asked for a fixed (400 vB * feeRate) + 50% estimate that was never revisited, while BuildRedemptionTransaction() recomputed the true fee from the transaction it had built and failed when the selection came up short, with no re-selection. SelectFeeCoins() now prices every candidate by effective value (value - EstimateInputSpendCost(fee_rate)), skips UTXOs that cannot pay for their own spending, and requires the target to be met in effective value rather than raw value; selected_total keeps its raw meaning. This applies to all callers, because a selection whose raw sum meets the target but whose effective sum does not is wrong on every path. The fee rate is a parameter so the cost tracks what the transaction will pay. Input ordering stays opt-in. A new minimize_inputs flag, off by default, keeps the smallest-first order DD transfers have always used; only the redemption path sets it, because it must converge on an exact fee with as few inputs as possible, the same reasoning as TxBuilder::SelectCoins. Note that transfers on a fragmented wallet change behaviour: selection that used to come up short now succeeds, using several small inputs and paying a correspondingly larger fee. A new DigiDollarWallet::SelectRedemptionFeeCoins() applies the pattern the DD transfer path already uses in PreflightDDTransferCapacity(): it builds a projected transaction from the actually-selected inputs, derives the fee from EstimateTransactionVSize(), and re-selects against the updated fee, bounded to six rounds. It excludes the collateral outpoint and the DD UTXOs being burned, preserves the MIN_DD_TX_FEE floor and the MAX_STANDARD_TX_WEIGHT guard, and reports its outcome as a category so the RPC can distinguish a funding shortfall from a transaction that cannot be built. Both redemption call sites use it in place of the fixed size guess, and failures now name the numbers and tell the user to consolidate small DGB UTXOs. MIN_DD_FEE_RATE and MIN_DD_TX_FEE move to txbuilder.h and replace the copies scattered across the wallet and RPC. Mint is unaffected: it funds itself through TxBuilder::SelectCoins. No consensus code is touched. Covered by unit tests for fee-input selection and redemption fee convergence, and by a new functional test, digidollar_redeem_fragmented_fees.py, which reproduces the original user-facing failure on regtest: a wallet fragmented into 0.0525 DGB UTXOs could not redeem a matured vault, and now can.
ycagel
requested review from
JaredTate,
SmartArray,
digicontributer,
gto90 and
ycagel
August 10, 2026 03:24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
redeemdigidollarfails on wallets whose DGB is fragmented into small UTXOs, even when the wallet holds ample spendable DGB:This is reachable in ordinary use — any wallet that has accumulated small payments (mining payouts, tips, faucet drips) can hit it, and the practical workaround has been to consolidate UTXOs by hand first. It is the redemption analogue of the fragmented-UTXO mint bug fixed in #396.
Root cause
A fee UTXO's raw value is not its contribution to the fee, because every input added to a DigiDollar transaction enlarges the transaction the fee is computed from.
At the DD fee rate (35,000,000 sat/kvB) and with the per-input accounting in
DigiDollar::EstimateTransactionVSize()(41 base bytes + a flat 110 witness bytes, +35% margin ≈ 92 vB), one extra input costs about 3,220,000 sat (~0.0322 DGB) to spend. Any fee UTXO below that threshold has negative effective value — adding it reduces the amount available to pay the fee.Three things then interact:
DigiDollarWallet::SelectFeeCoins()sorted smallest-first and stopped as soon as the raw sum reached the target — so it selected precisely the UTXOs that cannot pay for their own spending.redeemdigidollarasked for a fixed(400 vB × feeRate) + 50%estimate that was never revisited — a hard budget of ~600 vB, exhausted after roughly 3–4 fee inputs.BuildRedemptionTransaction()recomputed the true fee from the transaction it had actually built and failed when the selection came up short, with no re-selection.Worked example from the regression test: four 0.0525 DGB UTXOs total 21,000,000 sat against a 21,000,000 sat target, but spending them costs 12,880,000 sat, leaving 8,120,000 sat effective — while an untouched 5 DGB UTXO sat in the same wallet.
Note that simply enlarging the fixed estimate does not fix this: a bigger target just pulls in more uneconomic inputs, and the spend cost scales with it.
Fix
SelectFeeCoins()now prices every candidate by effective value (value - EstimateInputSpendCost(fee_rate)), skips UTXOs that cannot pay for their own spending, and requires the target to be met in effective rather than raw value.selected_totalkeeps its raw meaning. This applies to all callers, because a selection whose raw sum meets the target but whose effective sum does not is arithmetically wrong on every path. The fee rate is now a parameter so the cost tracks what the transaction will actually pay.Input ordering stays opt-in: a new
minimize_inputsflag, off by default, preserves the smallest-first order DD transfers have always used. Only the redemption path sets it.DigiDollarWallet::SelectRedemptionFeeCoins()applies the pattern this codebase already uses for DD transfers inPreflightDDTransferCapacity(): build a projected transaction from the actually-selected inputs, derive the fee fromEstimateTransactionVSize(), and re-select against the updated fee, bounded to six rounds. It preserves theMIN_DD_TX_FEEfloor and theMAX_STANDARD_TX_WEIGHTguard, excludes the collateral outpoint and the DD UTXOs being burned, and reports its outcome as a category so the RPC can distinguish a funding shortfall from a transaction that cannot be built.Failures now name the numbers and tell the user to consolidate, instead of a bare
"Insufficient fee inputs".MIN_DD_FEE_RATEandMIN_DD_TX_FEEmove totxbuilder.h, replacing copies scattered across the wallet and RPC.No consensus code is touched. Mint is unaffected — it funds itself through
TxBuilder::SelectCoins(which, incidentally, already sorts largest-first with the comment "This minimizes the number of inputs needed").Behaviour change worth calling out
senddigidollaron a fragmented wallet changes: selection that previously came up short now succeeds, using several small inputs and paying a correspondingly larger fee. On the wallet in the regression test that is ~0.249 DGB across five small inputs, where a single large input would have cost ~0.12 DGB.This is a fail → succeed change, so it is an improvement, but it is a real change and reviewers should see it stated rather than discover it. Flipping the transfer default to
minimize_inputswould reduce that fee; it is deliberately left out of this PR to keep the blast radius contained, and can follow separately if maintainers prefer.Testing
Unit (
src/test/digidollar_wallet_tests.cpp, all additions — no existing assertion was modified or removed):The exclusion-guard test is mutation-checked: injecting
exclude_utxos.clear()fails it 5/5.Functional: new
test/functional/digidollar_redeem_fragmented_fees.py— mints $1000 DD, fragments the wallet into 12 × 0.0525 DGB, matures, and redeems. Reverting the source changes reproduces the originalInsufficient fee inputs for DD redemption fee (-4)failure; with the fix it passes.Full unit suite: 3414/3414 passing.
digidollar_redemption_e2e.pyalso still passes, confirming the ordinary redemption path is unaffected.Prepared with AI assistance; all changes were human-reviewed, and every test result above was independently reproduced before submission.