p2p: Use different inbound inv timer per network
What changed, and why it matters
This change tweaks how Bitcoin nodes schedule transaction announcements to inbound peers. Previously, all inbound peers were told about new transactions on the same shared timer, which could let a spy with multiple connections figure out when a node first learned of a transaction and possibly trace it back to its source. The patch splits that timer by network group so that different peer networks get staggered announcement times, making that timing-based fingerprinting harder. It is a privacy-hardening fix, not a fix for a code crash or theft bug.
Treat as a low-severity privacy improvement. No urgent deployment required for security, but include in normal release cycle. Operators concerned about transaction-origin privacy should upgrade when convenient. No immediate incident response needed.
Security signals we found
Privacy/fingerprinting mitigation
P2P network timing side-channel reduction
Transaction origin deanonymization mitigation
Comment explicitly describes spy-node attack scenario
Evidence from the diff
The commit replaces a single atomic timer (m_next_inv_to_inbounds) with a per-network-key map of timers (m_next_inv_to_inbounds_per_network_key) in net_processing.cpp. NextInvToInbounds now takes a network_key argument and returns the next scheduled inv time for that key. In SendMessages, inbound connections use pto->m_network_key to look up their network’s timer. The goal is to prevent a spy that opens many inbound connections from observing identical inv timing across all connections and using it to deanonymize transaction propagation.
Changed components
src/net_processing.cppPeerManagerImpl::NextInvToInboundsPeerManagerImpl::SendMessagesInbound inventory broadcast schedulingInspect captured patch +15 / −13
diff --git a/src/net_processing.cpp b/src/net_processing.cpp
index 336669a8..8a39cb58 100644
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -807,7 +807,7 @@ private:
uint32_t GetFetchFlags(const Peer& peer) const;
- std::atomic<std::chrono::microseconds> m_next_inv_to_inbounds{0us};
+ std::map<uint64_t, std::chrono::microseconds> m_next_inv_to_inbounds_per_network_key GUARDED_BY(g_msgproc_mutex);
/** Number of nodes with fSyncStarted. */
int nSyncStarted GUARDED_BY(cs_main) = 0;
@@ -837,12 +837,14 @@ private:
/**
* For sending `inv`s to inbound peers, we use a single (exponentially
- * distributed) timer for all peers. If we used a separate timer for each
+ * distributed) timer for all peers with the same network key. If we used a separate timer for each
* peer, a spy node could make multiple inbound connections to us to
- * accurately determine when we received the transaction (and potentially
- * determine the transaction's origin). */
+ * accurately determine when we received a transaction (and potentially
+ * determine the transaction's origin). Each network key has its own timer
+ * to make fingerprinting harder. */
std::chrono::microseconds NextInvToInbounds(std::chrono::microseconds now,
- std::chrono::seconds average_interval) EXCLUSIVE_LOCKS_REQUIRED(g_msgproc_mutex);
+ std::chrono::seconds average_interval,
+ uint64_t network_key) EXCLUSIVE_LOCKS_REQUIRED(g_msgproc_mutex);
// All of the following cache a recent block, and are protected by m_most_recent_block_mutex
@@ -1143,15 +1145,15 @@ static bool CanServeWitnesses(const Peer& peer)
}
std::chrono::microseconds PeerManagerImpl::NextInvToInbounds(std::chrono::microseconds now,
- std::chrono::seconds average_interval)
+ std::chrono::seconds average_interval,
+ uint64_t network_key)
{
- if (m_next_inv_to_inbounds.load() < now) {
- // If this function were called from multiple threads simultaneously
- // it would possible that both update the next send variable, and return a different result to their caller.
- // This is not possible in practice as only the net processing thread invokes this function.
- m_next_inv_to_inbounds = now + m_rng.rand_exp_duration(average_interval);
+ auto [it, inserted] = m_next_inv_to_inbounds_per_network_key.try_emplace(network_key, 0us);
+ auto& timer{it->second};
+ if (timer < now) {
+ timer = now + m_rng.rand_exp_duration(average_interval);
}
- return m_next_inv_to_inbounds;
+ return timer;
}
bool PeerManagerImpl::IsBlockRequested(const uint256& hash)
@@ -5711,7 +5713,7 @@ bool PeerManagerImpl::SendMessages(CNode* pto)
if (tx_relay->m_next_inv_send_time < current_time) {
fSendTrickle = true;
if (pto->IsInboundConn()) {
- tx_relay->m_next_inv_send_time = NextInvToInbounds(current_time, INBOUND_INVENTORY_BROADCAST_INTERVAL);
+ tx_relay->m_next_inv_send_time = NextInvToInbounds(current_time, INBOUND_INVENTORY_BROADCAST_INTERVAL, pto->m_network_key);
} else {
tx_relay->m_next_inv_send_time = current_time + m_rng.rand_exp_duration(OUTBOUND_INVENTORY_BROADCAST_INTERVAL);
}
Why this scored 43/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.