blockchain: improve incoming block scan table handling
What changed, and why it matters
This commit refactors how Monero's blockchain code builds a lookup table used when checking incoming blocks. It replaces a slower, manual scan through offsets with a sorted binary search, and removes duplicate amount handling. The change appears to be a performance and correctness cleanup rather than a clear security fix, but the old code's linear search and duplicate-amount logic could theoretically hide or mishandle edge cases in block validation.
Treat as a hardening/maintenance change in consensus code. Run full regression and sync tests against mainnet/stagenet to ensure no consensus divergence. Review whether any previously accepted block relied on the old duplicate-amount or linear-search behavior. No immediate emergency response is indicated absent evidence of an exploitable vulnerability.
Security signals we found
Refactor of consensus-critical incoming-block validation path
Removal of duplicate-amount deduplication logic in scan table construction
Replacement of linear offset search with binary search (std::lower_bound)
Addition of explicit missing-amount guard with SCAN_TABLE_QUIT
Potential for subtle consensus divergence if old and new code disagree on duplicate amount handling or offset ordering
Evidence from the diff
The patch modifies Blockchain::prepare_handle_incoming_blocks in src/cryptonote_core/blockchain.cpp. Previously, the code collected all input amounts into a vector, sorted/unique’d them, then populated offset_map and tx_map buckets per unique amount. It then scanned offset_map[amount] linearly to locate each needed absolute offset. The new code directly emplaces amount buckets while iterating inputs, builds the amounts vector from offset_map keys after the fact, and uses std::lower_bound on the now-sorted offset list to find positions. It also adds explicit checks that the amount exists in both maps before lookup. This is primarily a refactor toward O(log n) offset lookup and simpler amount bookkeeping.
Changed components
src/cryptonote_core/blockchain.cppBlockchain::prepare_handle_incoming_blocksm_scan_table / offset_map / tx_mapInspect captured patch +21 / −32
diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp
index f2e8e23..9c19529 100644
--- a/src/cryptonote_core/blockchain.cpp
+++ b/src/cryptonote_core/blockchain.cpp
@@ -5093,7 +5093,7 @@ bool Blockchain::prepare_handle_incoming_blocks(const std::vector<block_complete
its = m_scan_table.find(tx_prefix_hash);
assert(its != m_scan_table.end());
- // get all amounts from tx.vin(s)
+ // initialize amount buckets for tx.vin(s)
for (const auto &txin : tx.vin)
{
const txin_to_key &in_to_key = boost::get < txin_to_key > (txin);
@@ -5103,22 +5103,8 @@ bool Blockchain::prepare_handle_incoming_blocks(const std::vector<block_complete
if (it != its->second.end())
SCAN_TABLE_QUIT("Duplicate key_image found from incoming blocks.");
- amounts.push_back(in_to_key.amount);
- }
-
- // sort and remove duplicate amounts from amounts list
- std::sort(amounts.begin(), amounts.end());
- auto last = std::unique(amounts.begin(), amounts.end());
- amounts.erase(last, amounts.end());
-
- // add amount to the offset_map and tx_map
- for (const uint64_t &amount : amounts)
- {
- if (offset_map.find(amount) == offset_map.end())
- offset_map.emplace(amount, std::vector<uint64_t>());
-
- if (tx_map.find(amount) == tx_map.end())
- tx_map.emplace(amount, std::vector<output_data_t>());
+ offset_map.emplace(in_to_key.amount, std::vector<uint64_t>());
+ tx_map.emplace(in_to_key.amount, std::vector<output_data_t>());
}
// add new absolute_offsets to offset_map
@@ -5134,6 +5120,10 @@ bool Blockchain::prepare_handle_incoming_blocks(const std::vector<block_complete
}
}
+ amounts.reserve(offset_map.size());
+ for (const auto &offsets : offset_map)
+ amounts.push_back(offsets.first);
+
// sort and remove duplicate absolute_offsets in offset_map
for (auto &offsets : offset_map)
{
@@ -5192,25 +5182,24 @@ bool Blockchain::prepare_handle_incoming_blocks(const std::vector<block_complete
const txin_to_key &in_to_key = boost::get < txin_to_key > (txin);
auto needed_offsets = relative_output_offsets_to_absolute(in_to_key.key_offsets);
+ const auto offset_it = offset_map.find(in_to_key.amount);
+ const auto tx_it = tx_map.find(in_to_key.amount);
+ if (offset_it == offset_map.end() || tx_it == tx_map.end())
+ SCAN_TABLE_QUIT("Amount not found on scan table from incoming blocks.");
+
+ const std::vector<uint64_t> &offsets_found = offset_it->second;
+ const std::vector<output_data_t> &outputs_found = tx_it->second;
+
std::vector<output_data_t> outputs;
for (const uint64_t & offset_needed : needed_offsets)
{
- size_t pos = 0;
- bool found = false;
-
- for (const uint64_t &offset_found : offset_map[in_to_key.amount])
- {
- if (offset_needed == offset_found)
- {
- found = true;
- break;
- }
-
- ++pos;
- }
+ // offsets_found is sorted above before output_scan_worker populates outputs_found.
+ const auto found_it = std::lower_bound(offsets_found.begin(), offsets_found.end(), offset_needed);
+ const size_t pos = found_it - offsets_found.begin();
+ const bool found = found_it != offsets_found.end() && *found_it == offset_needed;
- if (found && pos < tx_map[in_to_key.amount].size())
- outputs.push_back(tx_map[in_to_key.amount].at(pos));
+ if (found && pos < outputs_found.size())
+ outputs.push_back(outputs_found[pos]);
else
break;
}
Why this scored 34/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.