cryptonote_protocol: include pruned weights in sync sizing
What changed, and why it matters
This Monero commit changes how a node calculates the size of upcoming block batches during sync. Previously, when a peer sent 'pruned' blocks (a compressed form), the node did not fully count those blocks toward its sync-size budget and only checked that their reported weight was non-zero. The patch makes the node use the pruned blocks' reported weights in its size calculations and verifies those weights against data already stored in its own chain. The main effect is to prevent a malicious or buggy peer from making the node request far more data than expected, which could slow or disrupt syncing. It is a hardening/DoS-mitigation change rather than a direct coin-theft bug.
Treat as a recommended stability and DoS-resistance patch for nodes that perform pruned sync. Operators and downstream wallets/nodes should update to include this commit, especially if they rely on adaptive block-sync sizing. No immediate emergency response is indicated by the diff alone, but the change addresses a real protocol-handling weakness.
Security signals we found
Denial-of-service hardening: prevents under-counting of pruned block data during sync, which could cause a node to request oversized batches
Input validation: pruned block weights are now cross-checked against prevalidated on-chain weights instead of only being checked for non-zero
Integer-overflow avoidance: sync-size threshold comparison is restructured to avoid projected_blocksize * BLOCKS_MAX_WINDOW overflowing
Locking consistency: new weight check is performed under CRITICAL_REGION_LOCAL(m_blockchain_lock)
Evidence from the diff
The patch adds a sync_size field to block_queue spans and propagates it from the protocol handler. In handle_response_get_blocks, blocks_sync_size is computed as the sum of each block’s size, but for pruned entries the size is taken as max(actual_blob_size, block_weight). This value is stored in the span and exposed via get_max_block_size_average(), replacing the old size/nblocks average. get_block_sync_size now receives this pruned-aware average only when adaptive sync sizing is enabled. A new Blockchain::check_block_weights() routine verifies every pruned block’s block_weight against m_blocks_hash_check under m_blockchain_lock, replacing the prior zero-weight-only check. The patch also fixes a potential overflow in the tiny-blocks branch by comparing projected_blocksize <= (batch_max_weight - 1) / BLOCKS_MAX_WINDOW instead of multiplying.
Changed components
src/cryptonote_core/blockchain.cpp/h - weight verification and sync-size logicsrc/cryptonote_core/cryptonote_core.cpp/h - core wrappers and adaptive sync flagsrc/cryptonote_protocol/block_queue.cpp/h - span metadata and average-size querysrc/cryptonote_protocol/cryptonote_protocol_handler.h/inl - block response handling and sync request sizingtests/unit_tests/block_queue.cpp and node_server.cpp - updated/added unit testsInspect captured patch +109 / −33
### src/cryptonote_core/blockchain.cpp
@@ -4877,6 +4877,22 @@ bool Blockchain::has_block_weights(uint64_t height, uint64_t nblocks) const
return true;
}
+bool Blockchain::check_block_weights(uint64_t height, const std::vector<block_complete_entry> &blocks) const
+{
+ CRITICAL_REGION_LOCAL(m_blockchain_lock);
+ const uint64_t available = height < m_blocks_hash_check.size() ? m_blocks_hash_check.size() - height : 0;
+ for (size_t i = 0; i < blocks.size(); ++i)
+ {
+ const block_complete_entry &entry = blocks[i];
+ if (!entry.pruned)
+ continue;
+ // Check the remaining range before adding to height.
+ if (i >= available || entry.block_weight == 0 || entry.block_weight != m_blocks_hash_check[height + i].second)
+ return false;
+ }
+ return true;
+}
+
//------------------------------------------------------------------
// ND: Speedups:
// 1. Thread long_hash computations if possible (m_max_prepare_blocks_threads = nthreads, default = 4)
### src/cryptonote_core/blockchain.h
@@ -1138,6 +1138,14 @@ namespace cryptonote
*/
bool has_block_weights(uint64_t height, uint64_t nblocks) const;
+ /**
+ * @brief checks pruned block weights against prevalidated chain data under one lock
+ * @param height the height of the first block
+ * @param blocks consecutive blocks; full blocks are ignored
+ * @return false if a pruned block has a zero, unavailable, or mismatched weight
+ */
+ bool check_block_weights(uint64_t height, const std::vector<block_complete_entry> &blocks) const;
+
/**
* @brief flush the invalid blocks set
*/
### src/cryptonote_core/cryptonote_core.cpp
@@ -941,7 +941,8 @@ namespace cryptonote
<< " bytes and the max average blocksize in the queue is " << max_average_of_blocksize_in_queue << " bytes");
uint64_t projected_blocksize = std::max(max_average_of_blocksize_in_queue, max_weight);
uint64_t blocks_huge_threshold = (batch_max_weight / 2);
- if ((projected_blocksize * BLOCKS_MAX_WINDOW) < batch_max_weight)
+ // batch_max_weight is positive; compare without overflowing the projected batch weight.
+ if (projected_blocksize <= (batch_max_weight - 1) / BLOCKS_MAX_WINDOW)
{
res = BLOCKS_MAX_WINDOW;
MINFO("blocks are tiny, " << projected_blocksize << " bytes, sync " << res << " blocks in next batch");
@@ -1905,6 +1906,11 @@ namespace cryptonote
return get_blockchain_storage().has_block_weights(height, nblocks);
}
//-----------------------------------------------------------------------------------------------
+ bool core::check_block_weights(uint64_t height, const std::vector<block_complete_entry> &blocks) const
+ {
+ return get_blockchain_storage().check_block_weights(height, blocks);
+ }
+ //-----------------------------------------------------------------------------------------------
std::time_t core::get_start_time() const
{
return start_time;
### src/cryptonote_core/cryptonote_core.h
@@ -765,6 +765,7 @@ namespace cryptonote
* @return the number of blocks to sync in one go
*/
size_t get_block_sync_size(uint64_t height, const uint64_t max_average_of_blocksize_in_queue = 0) const;
+ bool is_block_sync_size_adaptive() const { return block_sync_size == 0; }
/**
* @brief get the sum of coinbase tx amounts between blocks
@@ -853,6 +854,14 @@ namespace cryptonote
*/
bool has_block_weights(uint64_t height, uint64_t nblocks) const;
+ /**
+ * @brief checks pruned block weights against prevalidated chain data
+ * @param height the height of the first block
+ * @param blocks consecutive blocks; full blocks are ignored
+ * @return false if a pruned block has a zero, unavailable, or mismatched weight
+ */
+ bool check_block_weights(uint64_t height, const std::vector<block_complete_entry> &blocks) const;
+
/**
* @brief flushes the invalid block cache
*/
### src/cryptonote_protocol/block_queue.cpp
@@ -45,12 +45,12 @@
namespace cryptonote
{
-void block_queue::add_blocks(uint64_t height, std::vector<cryptonote::block_complete_entry> bcel, const boost::uuids::uuid &connection_id, const epee::net_utils::network_address &addr, float rate, size_t size)
+void block_queue::add_blocks(uint64_t height, std::vector<cryptonote::block_complete_entry> bcel, const boost::uuids::uuid &connection_id, const epee::net_utils::network_address &addr, float rate, size_t size, uint64_t sync_size)
{
boost::unique_lock<boost::recursive_mutex> lock(mutex);
std::vector<crypto::hash> hashes;
bool has_hashes = remove_span(height, &hashes);
- blocks.insert(span(height, std::move(bcel), connection_id, addr, rate, size));
+ blocks.insert(span(height, std::move(bcel), connection_id, addr, rate, size, sync_size));
if (has_hashes)
{
for (std::size_t i = 0; i < hashes.size(); ++i)
@@ -451,6 +451,20 @@ uint64_t block_queue::get_num_filled_blocks() const
return size;
}
+uint64_t block_queue::get_max_block_size_average() const
+{
+ boost::unique_lock<boost::recursive_mutex> lock(mutex);
+ uint64_t max_average = 0;
+ for (const auto &span: blocks)
+ {
+ if (span.blocks.empty())
+ continue;
+
+ max_average = std::max(max_average, span.sync_size / span.nblocks);
+ }
+ return max_average;
+}
+
float block_queue::get_speed(const boost::uuids::uuid &connection_id) const
{
boost::unique_lock<boost::recursive_mutex> lock(mutex);
### src/cryptonote_protocol/block_queue.h
@@ -58,20 +58,21 @@ namespace cryptonote
uint64_t nblocks;
float rate;
size_t size;
+ uint64_t sync_size; // includes prevalidated weights for pruned blocks
boost::posix_time::ptime time;
epee::net_utils::network_address origin{};
- span(uint64_t start_block_height, std::vector<cryptonote::block_complete_entry> blocks, const boost::uuids::uuid &connection_id, const epee::net_utils::network_address &addr, float rate, size_t size):
- start_block_height(start_block_height), blocks(std::move(blocks)), connection_id(connection_id), nblocks(this->blocks.size()), rate(rate), size(size), time(boost::date_time::min_date_time), origin(addr) {}
+ span(uint64_t start_block_height, std::vector<cryptonote::block_complete_entry> blocks, const boost::uuids::uuid &connection_id, const epee::net_utils::network_address &addr, float rate, size_t size, uint64_t sync_size):
+ start_block_height(start_block_height), blocks(std::move(blocks)), connection_id(connection_id), nblocks(this->blocks.size()), rate(rate), size(size), sync_size(sync_size), time(boost::date_time::min_date_time), origin(addr) {}
span(uint64_t start_block_height, uint64_t nblocks, const boost::uuids::uuid &connection_id, const epee::net_utils::network_address &addr, boost::posix_time::ptime time):
- start_block_height(start_block_height), connection_id(connection_id), nblocks(nblocks), rate(0.0f), size(0), time(time), origin(addr) {}
+ start_block_height(start_block_height), connection_id(connection_id), nblocks(nblocks), rate(0.0f), size(0), sync_size(0), time(time), origin(addr) {}
bool operator<(const span &s) const { return start_block_height < s.start_block_height; }
};
typedef std::set<span> block_map;
public:
- void add_blocks(uint64_t height, std::vector<cryptonote::block_complete_entry> bcel, const boost::uuids::uuid &connection_id, const epee::net_utils::network_address &addr, float rate, size_t size);
+ void add_blocks(uint64_t height, std::vector<cryptonote::block_complete_entry> bcel, const boost::uuids::uuid &connection_id, const epee::net_utils::network_address &addr, float rate, size_t size, uint64_t sync_size);
bool add_blocks(uint64_t height, uint64_t nblocks, const boost::uuids::uuid &connection_id, const epee::net_utils::network_address &addr, boost::posix_time::ptime time = boost::date_time::min_date_time);
void flush_spans(const boost::uuids::uuid &connection_id, bool all = false);
void flush_stale_spans(const std::set<boost::uuids::uuid> &live_connections);
@@ -90,6 +91,7 @@ namespace cryptonote
size_t get_data_size() const;
size_t get_num_filled_spans() const;
uint64_t get_num_filled_blocks() const;
+ uint64_t get_max_block_size_average() const;
float get_speed(const boost::uuids::uuid &connection_id) const;
bool foreach(std::function<bool(const span&)> f) const;
bool requested(const crypto::hash &hash) const;
### src/cryptonote_protocol/cryptonote_protocol_handler.h
@@ -114,14 +114,9 @@ namespace cryptonote
const block_queue &get_block_queue() const { return m_block_queue; }
std::uint64_t max_average_of_blocksize_in_queue()
{
- std::vector<std::uint64_t> average_blocksize{0};
- m_block_queue.foreach([&](const cryptonote::block_queue::span &span)
- {
- average_blocksize.push_back(span.size / span.nblocks);
- return true; // we don't care about the return value
- });
- MINFO("Maximum average of blocksize for current batches : " << *std::max_element(average_blocksize.begin(), average_blocksize.end()));
- return *std::max_element(average_blocksize.begin(), average_blocksize.end());
+ const uint64_t max_average = m_block_queue.get_max_block_size_average();
+ MINFO("Maximum average of blocksize for current batches : " << max_average);
+ return max_average;
}
void stop();
void on_connection_close(cryptonote_connection_context &context);
### src/cryptonote_protocol/cryptonote_protocol_handler.inl
@@ -1087,10 +1087,15 @@ namespace cryptonote
// calculate size of request
size_t size = 0;
size_t blocks_size = 0;
+ uint64_t blocks_sync_size = 0;
for (const auto &element : arg.blocks) {
- blocks_size += element.block.size();
+ uint64_t block_size = element.block.size();
for (const auto &tx : element.txs)
- blocks_size += tx.blob.size();
+ block_size += tx.blob.size();
+ blocks_size += block_size;
+ if (element.pruned)
+ block_size = std::max(block_size, element.block_weight);
+ blocks_sync_size += std::min(block_size, std::numeric_limits<uint64_t>::max() - blocks_sync_size);
}
size += blocks_size;
@@ -1273,19 +1278,13 @@ namespace cryptonote
}
}
}
- else
+ else if (!m_core.check_block_weights(start_height, arg.blocks))
{
- // we accept pruned data, check that if we got some, then no weights are zero
- for (block_complete_entry& block_entry: arg.blocks)
- {
- if (block_entry.block_weight == 0 && block_entry.pruned)
- {
- MERROR(context << "returned at least one pruned block with 0 weight, dropping connection");
- drop_connection(context, false, false);
- ++m_sync_bad_spans_downloaded;
- return LEVIN_ERROR_CONNECTION;
- }
- }
+ // Weights used for adaptive sync sizing must match the prevalidated chain data.
+ MERROR(context << "returned an incorrect weight for a pruned block in span starting at height " << start_height << ", dropping connection");
+ drop_connection(context, false, false);
+ ++m_sync_bad_spans_downloaded;
+ return LEVIN_ERROR_CONNECTION;
}
{
@@ -1297,7 +1296,7 @@ namespace cryptonote
const boost::posix_time::time_duration dt = now - request_time;
const float rate = size * 1e6 / (dt.total_microseconds() + 1);
MDEBUG(context << " adding span: " << arg.blocks.size() << " at height " << start_height << ", " << dt.total_microseconds()/1e6 << " seconds, " << (rate/1024) << " kB/s, size now " << (m_block_queue.get_data_size() + blocks_size) / 1048576.f << " MB");
- m_block_queue.add_blocks(start_height, std::move(arg.blocks), context.m_connection_id, context.m_remote_address, rate, blocks_size);
+ m_block_queue.add_blocks(start_height, std::move(arg.blocks), context.m_connection_id, context.m_remote_address, rate, blocks_size, blocks_sync_size);
const crypto::hash last_block_hash = cryptonote::get_block_hash(b);
context.m_last_known_hash = last_block_hash;
@@ -2206,7 +2205,8 @@ skip:
NOTIFY_REQUEST_GET_OBJECTS::request req;
bool is_next = false;
size_t count = 0;
- const size_t l_m_bss = m_core.get_block_sync_size(m_core.get_current_blockchain_height(), max_average_of_blocksize_in_queue());
+ const uint64_t max_average = m_core.is_block_sync_size_adaptive() ? max_average_of_blocksize_in_queue() : 0;
+ const size_t l_m_bss = m_core.get_block_sync_size(m_core.get_current_blockchain_height(), max_average);
std::pair<uint64_t, uint64_t> span = std::make_pair(0, 0);
if (force_next_span)
{
### tests/unit_tests/block_queue.cpp
@@ -170,10 +170,34 @@ TEST(block_queue, count_filled_blocks)
cryptonote::block_queue bq;
epee::net_utils::network_address na;
- bq.add_blocks(0, std::vector<cryptonote::block_complete_entry>(3), uuid1(), na, 0.0f, 0);
+ bq.add_blocks(0, std::vector<cryptonote::block_complete_entry>(3), uuid1(), na, 0.0f, 0, 0);
bq.add_blocks(3, 2, uuid2(), na);
- bq.add_blocks(5, std::vector<cryptonote::block_complete_entry>(4), uuid2(), na, 0.0f, 0);
+ bq.add_blocks(5, std::vector<cryptonote::block_complete_entry>(4), uuid2(), na, 0.0f, 0, 0);
ASSERT_EQ(bq.get_num_filled_spans(), 2);
ASSERT_EQ(bq.get_num_filled_blocks(), 7);
}
+
+TEST(block_queue, cached_sync_size_tracks_span_changes)
+{
+ cryptonote::block_queue bq;
+ epee::net_utils::network_address na;
+
+ bq.add_blocks(0, 2, uuid1(), na);
+ ASSERT_EQ(bq.get_max_block_size_average(), 0);
+ bq.add_blocks(0, std::vector<cryptonote::block_complete_entry>(2), uuid1(), na, 0.0f, 100, 1000);
+ bq.set_span_hashes(0, uuid1(), std::vector<crypto::hash>(2));
+ bq.add_blocks(2, std::vector<cryptonote::block_complete_entry>(1), uuid2(), na, 0.0f, 200, 900);
+ ASSERT_EQ(bq.get_max_block_size_average(), 900);
+ ASSERT_EQ(bq.get_data_size(), 300);
+
+ bq.add_blocks(2, std::vector<cryptonote::block_complete_entry>(1), uuid2(), na, 0.0f, 150, 300);
+ ASSERT_EQ(bq.get_max_block_size_average(), 500);
+ ASSERT_EQ(bq.get_data_size(), 250);
+
+ bq.flush_spans(uuid1(), true);
+ ASSERT_EQ(bq.get_max_block_size_average(), 300);
+ ASSERT_TRUE(bq.remove_span(2));
+ ASSERT_EQ(bq.get_max_block_size_average(), 0);
+ ASSERT_EQ(bq.get_data_size(), 0);
+}
### tests/unit_tests/node_server.cpp
@@ -93,6 +93,7 @@ class test_core : public cryptonote::i_core_events
bool update_checkpoints(const bool skip_dns = false) { return true; }
uint64_t get_target_blockchain_height() const { return 1; }
size_t get_block_sync_size(uint64_t height, const uint64_t max_average_of_blocksize_in_queue = 0) const { return BLOCKS_SYNCHRONIZING_DEFAULT_COUNT; }
+ bool is_block_sync_size_adaptive() const { return false; }
virtual void on_transactions_relayed(epee::span<const cryptonote::blobdata> tx_blobs, cryptonote::relay_method tx_relay) {}
cryptonote::network_type get_nettype() const { return cryptonote::MAINNET; }
bool get_pool_transaction(const crypto::hash& id, cryptonote::blobdata& tx_blob, cryptonote::relay_category tx_category) const { return false; }
@@ -112,6 +113,7 @@ class test_core : public cryptonote::i_core_events
bool prune_blockchain(uint32_t pruning_seed = 0) { return true; }
bool is_within_compiled_block_hash_area(uint64_t height) const { return false; }
bool has_block_weights(uint64_t height, uint64_t nblocks) const { return false; }
+ bool check_block_weights(uint64_t height, const std::vector<cryptonote::block_complete_entry> &blocks) const { return false; }
bool get_txpool_complement(const std::vector<crypto::hash> &hashes, std::vector<cryptonote::blobdata> &txes) { return false; }
bool get_pool_transaction_hashes(std::vector<crypto::hash>& txs, bool include_unrelayed_txes = true) const { return false; }
crypto::hash get_block_id_by_height(uint64_t height) const { return crypto::null_hash; }Why this scored 45/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.