cryptonote_protocol: reject conflicting span reservations
What changed, and why it matters
This Monero code change fixes a bookkeeping bug in how the node keeps track of which blockchain chunks (called 'spans') it has already asked other nodes to download. Before the fix, the code could reserve the same span twice for different peers, even with different block hashes, which could confuse download tracking and potentially allow a malicious peer to interfere with another peer's download reservation. After the fix, the node rejects any new reservation that conflicts with an existing one.
Apply the patch. Monitor for related P2P synchronization issues and consider additional hardening around span lifecycle management.
Security signals we found
Conflicting resource reservation prevented
P2P protocol state inconsistency mitigated
Unit tests added for conflict and prefix-skip behavior
Evidence from the diff
The patch modifies block_queue::add_blocks to return a bool indicating whether the span was newly inserted (std::set::insert().second). reserve_span now checks this return value and aborts the reservation if a span already starts at the chosen height, preventing conflicting span reservations. New unit tests verify that a second reservation at the same height with different hashes is rejected, and that subsequent reservations correctly skip already-requested prefixes.
Changed components
src/cryptonote_protocol/block_queue.cppsrc/cryptonote_protocol/block_queue.htests/unit_tests/block_queue.cppInspect captured patch +65 / −4
### src/cryptonote_protocol/block_queue.cpp
@@ -62,11 +62,11 @@ void block_queue::add_blocks(uint64_t height, std::vector<cryptonote::block_comp
}
}
-void block_queue::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)
+bool block_queue::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)
{
CHECK_AND_ASSERT_THROW_MES(nblocks > 0, "Empty span");
boost::unique_lock<boost::recursive_mutex> lock(mutex);
- blocks.insert(span(height, nblocks, connection_id, addr, time));
+ return blocks.insert(span(height, nblocks, connection_id, addr, time)).second;
}
void block_queue::flush_spans(const boost::uuids::uuid &connection_id, bool all)
@@ -315,8 +315,12 @@ std::pair<uint64_t, uint64_t> block_queue::reserve_span(uint64_t first_block_hei
MDEBUG("span_length 0, cannot reserve");
return std::make_pair(0, 0);
}
+ if (!add_blocks(span_start_height, span_length, connection_id, addr, time))
+ {
+ MDEBUG("Span already starts at height " << span_start_height << ", cannot reserve");
+ return std::make_pair(0, 0);
+ }
MDEBUG("Reserving span " << span_start_height << " - " << (span_start_height + span_length - 1) << " for " << connection_id);
- add_blocks(span_start_height, span_length, connection_id, addr, time);
set_span_hashes(span_start_height, connection_id, hashes);
return std::make_pair(span_start_height, span_length);
}
### src/cryptonote_protocol/block_queue.h
@@ -72,7 +72,7 @@ namespace cryptonote
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, 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);
+ 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);
bool remove_span(uint64_t start_block_height, std::vector<crypto::hash> *hashes = NULL);
### tests/unit_tests/block_queue.cpp
@@ -107,3 +107,60 @@ TEST(block_queue, reserve_does_not_overlap_later_span)
// overlapping the span which is already reserved at height 57.
ASSERT_EQ(bq.reserve_span(1, 100, 36, uuid2(), na, false, 0, 0, 101, hashes), std::make_pair(uint64_t{29}, uint64_t{28}));
}
+
+TEST(block_queue, reserve_span_same_height_different_hashes)
+{
+ const std::vector<std::pair<crypto::hash, uint64_t>> first_hashes{
+ {crypto::rand<crypto::hash>(), 0}, {crypto::rand<crypto::hash>(), 0}};
+ const std::vector<std::pair<crypto::hash, uint64_t>> conflicting_hashes{
+ {crypto::rand<crypto::hash>(), 0}, {crypto::rand<crypto::hash>(), 0}};
+ epee::net_utils::network_address na;
+
+ for (const auto &connection_id: {uuid1(), uuid2()})
+ {
+ cryptonote::block_queue bq;
+ ASSERT_EQ(bq.reserve_span(100, 101, 2, uuid1(), na, false, 0, 0, 1000, first_hashes),
+ std::make_pair(uint64_t(100), uint64_t(2)));
+ ASSERT_EQ(bq.reserve_span(100, 101, 2, connection_id, na, false, 0, 0, 1000, conflicting_hashes),
+ std::make_pair(uint64_t(0), uint64_t(0)));
+
+ std::vector<crypto::hash> hashes;
+ boost::uuids::uuid owner;
+ boost::posix_time::ptime time;
+ ASSERT_EQ(bq.get_next_span_if_scheduled(hashes, owner, time),
+ std::make_pair(uint64_t(100), uint64_t(2)));
+ ASSERT_EQ(owner, uuid1());
+ ASSERT_EQ(hashes, (std::vector<crypto::hash>{first_hashes[0].first, first_hashes[1].first}));
+ for (const auto &entry: first_hashes)
+ ASSERT_TRUE(bq.requested(entry.first));
+ for (const auto &entry: conflicting_hashes)
+ ASSERT_FALSE(bq.requested(entry.first));
+
+ bq.flush_spans(uuid1());
+ ASSERT_EQ(bq.reserve_span(100, 101, 2, connection_id, na, false, 0, 0, 1000, conflicting_hashes),
+ std::make_pair(uint64_t(100), uint64_t(2)));
+ for (const auto &entry: first_hashes)
+ ASSERT_FALSE(bq.requested(entry.first));
+ for (const auto &entry: conflicting_hashes)
+ ASSERT_TRUE(bq.requested(entry.first));
+ }
+}
+
+TEST(block_queue, reserve_span_skips_requested_prefix)
+{
+ const std::vector<std::pair<crypto::hash, uint64_t>> hashes{
+ {crypto::rand<crypto::hash>(), 0}, {crypto::rand<crypto::hash>(), 0},
+ {crypto::rand<crypto::hash>(), 0}, {crypto::rand<crypto::hash>(), 0}};
+ cryptonote::block_queue bq;
+ epee::net_utils::network_address na;
+
+ ASSERT_EQ(bq.reserve_span(100, 103, 2, uuid1(), na, false, 0, 0, 1000, hashes),
+ std::make_pair(uint64_t(100), uint64_t(2)));
+ ASSERT_EQ(bq.reserve_span(100, 103, 2, uuid2(), na, false, 0, 0, 1000, hashes),
+ std::make_pair(uint64_t(102), uint64_t(2)));
+ bq.flush_spans(uuid1());
+ ASSERT_FALSE(bq.requested(hashes[0].first));
+ ASSERT_FALSE(bq.requested(hashes[1].first));
+ ASSERT_TRUE(bq.requested(hashes[2].first));
+ ASSERT_TRUE(bq.requested(hashes[3].first));
+}Why this scored 51/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.