wallet2: guard optimized coinbase refresh against empty vout
What changed, and why it matters
This patch fixes a corner case in Monero's wallet refresh logic. When scanning a coinbase (miner) transaction with no outputs, the wallet could previously call an internal output-generation helper with zero outputs, which may cause an error or undefined behavior during refresh. The fix skips that work when there are no outputs. It is a defensive hardening change rather than a clear exploit path for stealing funds.
Apply the patch. Monitor for any related crash reports or refresh failures on older hard-fork versions. Consider adding a regression test with an empty-vout coinbase transaction.
Security signals we found
empty-container edge case in transaction parsing
defensive guard around output-generation helper
potential crash or undefined behavior during wallet refresh
no explicit authentication/authorization bypass
Evidence from the diff
In wallet2::process_parsed_blocks(), the optimized coinbase refresh path assumed a miner transaction always had at least one vout. For tx.version < 2 with RefreshOptimizeCoinbase, it forced n_vouts to 1 even if tx.vout was empty, then passed that to geniod/geniods. The patch adds !tx.vout.empty() to the condition and wraps the geniod call in if (n_vouts > 0), preventing a zero-output invocation. The exact failure mode is not shown in the diff, but the change is clearly defensive against an empty-container edge case.
Changed components
src/wallet/wallet2.cppwallet2::process_parsed_blockscoinbase/miner transaction refresh pathInspect captured patch +8 / −5
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
index 5479aae..6332a2f 100644
--- a/src/wallet/wallet2.cpp
+++ b/src/wallet/wallet2.cpp
@@ -3362,11 +3362,14 @@ void wallet2::process_parsed_blocks(const uint64_t start_height, const std::vect
{
THROW_WALLET_EXCEPTION_IF(txidx >= tx_cache_data.size(), error::wallet_internal_error, "txidx out of range");
const cryptonote::transaction& tx = parsed_blocks[i].block.miner_tx;
- const size_t n_vouts = (m_refresh_type == RefreshType::RefreshOptimizeCoinbase && tx.version < 2) ? 1 : tx.vout.size();
- if (parsed_blocks[i].block.major_version >= hf_version_view_tags)
- geniods.push_back(geniod_params{ tx, n_vouts, txidx });
- else
- tpool.submit(&waiter, [&, n_vouts, txidx](){ geniod(tx, n_vouts, txidx); }, true);
+ const size_t n_vouts = (m_refresh_type == RefreshType::RefreshOptimizeCoinbase && tx.version < 2 && !tx.vout.empty()) ? 1 : tx.vout.size();
+ if (n_vouts > 0)
+ {
+ if (parsed_blocks[i].block.major_version >= hf_version_view_tags)
+ geniods.push_back(geniod_params{ tx, n_vouts, txidx });
+ else
+ tpool.submit(&waiter, [&, n_vouts, txidx](){ geniod(tx, n_vouts, txidx); }, true);
+ }
}
++txidx;
for (size_t j = 0; j < parsed_blocks[i].txes.size(); ++j)
Why this scored 29/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.