From 41e3f66969e8d5248f2c86b42a85effa33ff9ca1 Mon Sep 17 00:00:00 2001 From: JohnnyLawDGB Date: Sat, 7 Mar 2026 16:04:26 -0600 Subject: [PATCH 1/2] fix: auto-consolidate fragmented UTXOs when minting DigiDollar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a wallet has too many small UTXOs (>400) to cover collateral in a single transaction, mintdigidollar now automatically creates a consolidation transaction using the wallet's standard coin selection, then retries the mint using the consolidated output. This fixes the "Too many small UTXOs" error that miners and pool operators encounter when their wallet contains hundreds of small mining reward UTXOs that individually don't cover the collateral requirement within the 400-input transaction limit. The consolidation is transparent — the response includes consolidation_txid and utxos_consolidated fields when auto- consolidation was performed. Both the consolidation and mint transactions are broadcast together and chain in the mempool. Tested with 500 x 600 DGB UTXOs (300,000 DGB total) where tier 0 minting requires ~246,000 DGB collateral. Without this fix, the mint fails because 400 x 600 = 240,000 < 246,000. With this fix, the wallet auto-consolidates and the mint succeeds. Co-Authored-By: Claude Opus 4.6 --- src/rpc/digidollar.cpp | 85 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/src/rpc/digidollar.cpp b/src/rpc/digidollar.cpp index fcef0743ca..5d0a8084fe 100644 --- a/src/rpc/digidollar.cpp +++ b/src/rpc/digidollar.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -745,7 +746,9 @@ RPCHelpMan mintdigidollar() {RPCResult::Type::NUM, "unlock_height", "Block height when collateral becomes unlockable"}, {RPCResult::Type::NUM, "collateral_ratio", "Effective collateral ratio percentage"}, {RPCResult::Type::STR_AMOUNT, "fee_paid", "Transaction fee paid"}, - {RPCResult::Type::STR, "position_id", "Unique position identifier"} + {RPCResult::Type::STR, "position_id", "Unique position identifier"}, + {RPCResult::Type::STR_HEX, "consolidation_txid", /*optional=*/true, "TXID of auto-consolidation transaction (only present if UTXOs were consolidated)"}, + {RPCResult::Type::BOOL, "utxos_consolidated", /*optional=*/true, "True if wallet UTXOs were auto-consolidated before minting"} } }, RPCExamples{ @@ -941,6 +944,82 @@ RPCHelpMan mintdigidollar() DigiDollar::TxBuilderResult result = builder.BuildMintTransaction(params); + // Auto-consolidate if mint failed due to UTXO fragmentation + std::string consolidation_txid; + if (!result.success && result.error.find("Too many small UTXOs") != std::string::npos) { + LogPrintf("DigiDollar RPC Mint: UTXO fragmentation detected (%zu UTXOs). Auto-consolidating...\n", + availableUtxos.size()); + + // Calculate consolidation target: collateral + fees + 10% margin + CAmount consolidationTarget = result.collateralRequired + + (result.collateralRequired / 10) + 10000000; // +10% + 0.1 DGB fee buffer + + // Cap at available balance + CAmount totalAvailable = 0; + for (const auto& [outpoint, value] : utxoValues) { + totalAvailable += value; + } + if (consolidationTarget > totalAvailable) { + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, + strprintf("Insufficient funds for collateral. Need %.2f DGB, have %.2f DGB across %zu small UTXOs.", + result.collateralRequired / 100000000.0, + totalAvailable / 100000000.0, + availableUtxos.size())); + } + + // Create consolidation transaction using wallet's standard coin selection + CTxDestination consolidationDest; + { + LOCK(pwallet->cs_wallet); + auto op_dest = pwallet->GetNewChangeDestination(OutputType::BECH32); + if (!op_dest) { + throw JSONRPCError(RPC_WALLET_ERROR, "Failed to get consolidation address"); + } + consolidationDest = *op_dest; + } + + wallet::CCoinControl coin_control; + wallet::CRecipient recipient{consolidationDest, consolidationTarget, false}; + std::vector recipients = {recipient}; + + auto consolidation_result = wallet::CreateTransaction(*pwallet, recipients, /*change_pos=*/-1, coin_control, /*sign=*/true); + if (!consolidation_result) { + throw JSONRPCError(RPC_WALLET_ERROR, + strprintf("Auto-consolidation failed: %s. Try manually consolidating UTXOs with: " + "sendtoaddress ", + util::ErrorString(consolidation_result).original)); + } + + const CTransactionRef& consolidation_tx = consolidation_result->tx; + consolidation_txid = consolidation_tx->GetHash().GetHex(); + { + LOCK(pwallet->cs_wallet); + pwallet->CommitTransaction(consolidation_tx, {}, {}); + } + + LogPrintf("DigiDollar RPC Mint: Consolidation tx broadcast: %s (%.2f DGB)\n", + consolidation_txid, consolidationTarget / 100000000.0); + + // Re-gather UTXOs (now includes unconfirmed consolidation output) + availableUtxos.clear(); + utxoValues.clear(); + { + LOCK(pwallet->cs_wallet); + wallet::CoinsResult coins = wallet::AvailableCoins(*pwallet); + for (const wallet::COutput& coin : coins.All()) { + availableUtxos.push_back(coin.outpoint); + utxoValues[coin.outpoint] = coin.txout.nValue; + } + } + + LogPrintf("DigiDollar RPC Mint: After consolidation: %zu UTXOs available\n", availableUtxos.size()); + + // Rebuild builder with new UTXO map and retry + RpcMintTxBuilder retryBuilder(Params(), currentHeight, oraclePriceMicroUSD, utxoValues); + params.utxos = availableUtxos; + result = retryBuilder.BuildMintTransaction(params); + } + if (!result.success) { throw JSONRPCError(RPC_WALLET_ERROR, "Failed to build mint transaction: " + result.error); } @@ -1024,6 +1103,10 @@ RPCHelpMan mintdigidollar() resultObj.pushKV("collateral_ratio", collateralRatio); resultObj.pushKV("fee_paid", ValueFromAmount(result.totalFees)); resultObj.pushKV("position_id", tx->GetHash().GetHex()); + if (!consolidation_txid.empty()) { + resultObj.pushKV("consolidation_txid", consolidation_txid); + resultObj.pushKV("utxos_consolidated", true); + } return resultObj; }, From 13f98b4a6300e98c3ebd22b0cbd23afe8a929728 Mon Sep 17 00:00:00 2001 From: JohnnyLawDGB Date: Sun, 8 Mar 2026 09:49:20 -0500 Subject: [PATCH 2/2] fix: purge confirmed txs from Dandelion stempool on block connect When a block is connected, removeForBlock() was called on the mempool but not the stempool. This caused confirmed transactions to remain in the stempool indefinitely as phantom ancestors, inflating ancestor counts for any new stempool transactions until they hit the 25-ancestor limit and were rejected with "too-long-mempool-chain". Also adds stempool cleanup during DisconnectTip reorg path for consistency with all other mempool/stempool cleanup code paths. Fixes: Dandelion stempool ancestor leak (Bug #7) Co-Authored-By: Claude Opus 4.6 --- src/validation.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/validation.cpp b/src/validation.cpp index f56e7b173c..476630b63c 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3350,9 +3350,10 @@ bool Chainstate::DisconnectTip(BlockValidationState& state, DisconnectedBlockTra if (disconnectpool && m_mempool) { // Save transactions to re-add to mempool at end of reorg. If any entries are evicted for - // exceeding memory limits, remove them and their descendants from the mempool. + // exceeding memory limits, remove them and their descendants from the mempool and stempool. for (auto&& evicted_tx : disconnectpool->AddTransactionsFromBlock(block.vtx)) { m_mempool->removeRecursive(*evicted_tx, MemPoolRemovalReason::REORG); + if (m_stempool) m_stempool->removeRecursive(*evicted_tx, MemPoolRemovalReason::REORG); } } @@ -3483,6 +3484,13 @@ bool Chainstate::ConnectTip(BlockValidationState& state, CBlockIndex* pindexNew, m_mempool->removeForBlock(blockConnecting.vtx, pindexNew->nHeight); disconnectpool.removeForBlock(blockConnecting.vtx); } + // CRITICAL FIX: Also remove confirmed transactions from the Dandelion stempool. + // Without this, confirmed txs remain as phantom ancestors in the stempool, + // inflating ancestor counts until new stempool txs hit the 25-ancestor limit + // and get rejected with "too-long-mempool-chain". + if (m_stempool) { + m_stempool->removeForBlock(blockConnecting.vtx, pindexNew->nHeight); + } // Update m_chain & related variables. m_chain.SetTip(*pindexNew); UpdateTip(pindexNew);