From 9e64d69bf74c8a381fb59841519cc3736bce14d4 Mon Sep 17 00:00:00 2001 From: John Newbery Date: Tue, 14 Dec 2021 10:15:10 +0000 Subject: [PATCH 1/5] [move] Move PoissonNextSend to src/random and update comment PoissonNextSend is used by net and net_processing and is stateless, so place it in the utility random.cpp translation unit. --- src/net.cpp | 6 ------ src/net.h | 3 --- src/random.cpp | 7 +++++++ src/random.h | 14 +++++++++++++- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/net.cpp b/src/net.cpp index 89a4aee5d98..1b1e7a4e8a0 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -3069,12 +3069,6 @@ std::chrono::microseconds CConnman::PoissonNextSendInbound(std::chrono::microsec return m_next_send_inv_to_incoming; } -std::chrono::microseconds PoissonNextSend(std::chrono::microseconds now, std::chrono::seconds average_interval) -{ - double unscaled = -log1p(GetRand(1ULL << 48) * -0.0000000000000035527136788 /* -1/2^48 */); - return now + std::chrono::duration_cast(unscaled * average_interval + 0.5us); -} - CSipHasher CConnman::GetDeterministicRandomizer(uint64_t id) const { return CSipHasher(nSeed0, nSeed1).Write(id); diff --git a/src/net.h b/src/net.h index 80fc93a5d03..b0162bb9492 100644 --- a/src/net.h +++ b/src/net.h @@ -1270,9 +1270,6 @@ private: friend struct ConnmanTestMsg; }; -/** Return a timestamp in the future (in microseconds) for exponentially distributed events. */ -std::chrono::microseconds PoissonNextSend(std::chrono::microseconds now, std::chrono::seconds average_interval); - /** Dump binary message to file, with timestamp */ void CaptureMessage(const CAddress& addr, const std::string& msg_type, const Span& data, bool is_incoming); diff --git a/src/random.cpp b/src/random.cpp index 6eb06c5d472..919061e61d9 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -19,6 +19,7 @@ #include // for Mutex #include // for GetTimeMicros() +#include #include #include @@ -714,3 +715,9 @@ void RandomInit() ReportHardwareRand(); } + +std::chrono::microseconds PoissonNextSend(std::chrono::microseconds now, std::chrono::seconds average_interval) +{ + double unscaled = -log1p(GetRand(1ULL << 48) * -0.0000000000000035527136788 /* -1/2^48 */); + return now + std::chrono::duration_cast(unscaled * average_interval + 0.5us); +} diff --git a/src/random.h b/src/random.h index 0c6dc249835..be73e44a877 100644 --- a/src/random.h +++ b/src/random.h @@ -10,7 +10,7 @@ #include #include -#include // For std::chrono::microseconds +#include #include #include @@ -82,6 +82,18 @@ D GetRandomDuration(typename std::common_type::type max) noexcept }; constexpr auto GetRandMicros = GetRandomDuration; constexpr auto GetRandMillis = GetRandomDuration; + +/** + * Return a timestamp in the future sampled from an exponential distribution + * (https://en.wikipedia.org/wiki/Exponential_distribution). This distribution + * is memoryless and should be used for repeated network events (e.g. sending a + * certain type of message) to minimize leaking information to observers. + * + * The probability of an event occuring before time x is 1 - e^-(x/a) where a + * is the average interval between events. + * */ +std::chrono::microseconds PoissonNextSend(std::chrono::microseconds now, std::chrono::seconds average_interval); + int GetRandInt(int nMax) noexcept; uint256 GetRandHash() noexcept; From 03cfa1b6035dbcf6a414f9bc432bd9e612801ebb Mon Sep 17 00:00:00 2001 From: John Newbery Date: Fri, 8 May 2020 13:30:55 -0400 Subject: [PATCH 2/5] [refactor] Use uint64_t and std namespace in PoissonNextSend Co-authored-by: Martin Zumsande --- src/random.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/random.cpp b/src/random.cpp index 919061e61d9..03b8d7530d5 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -718,6 +718,6 @@ void RandomInit() std::chrono::microseconds PoissonNextSend(std::chrono::microseconds now, std::chrono::seconds average_interval) { - double unscaled = -log1p(GetRand(1ULL << 48) * -0.0000000000000035527136788 /* -1/2^48 */); + double unscaled = -std::log1p(GetRand(uint64_t{1} << 48) * -0.0000000000000035527136788 /* -1/2^48 */); return now + std::chrono::duration_cast(unscaled * average_interval + 0.5us); } From bb060746df22c956b8f44e5b8cd1ae4ed73faddc Mon Sep 17 00:00:00 2001 From: John Newbery Date: Wed, 15 Apr 2020 19:06:59 -0400 Subject: [PATCH 3/5] scripted-diff: replace PoissonNextSend with GetExponentialRand This distribution is used for more than just the next inv send, so make the name more generic. Also rename to "exponential" to avoid the confusion that this is a poisson distribution. -BEGIN VERIFY SCRIPT- ren() { sed -i "s/\<$1\>/$2/g" $(git grep -l "$1" ./src) ; } ren PoissonNextSend GetExponentialRand ren "a poisson timer" "an exponential timer" -END VERIFY SCRIPT- --- src/net.cpp | 12 ++++++------ src/net_processing.cpp | 8 ++++---- src/random.cpp | 2 +- src/random.h | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/net.cpp b/src/net.cpp index 1b1e7a4e8a0..3ce0b8ee810 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1878,8 +1878,8 @@ void CConnman::ThreadOpenConnections(const std::vector connect) auto start = GetTime(); // Minimum time before next feeler connection (in microseconds). - auto next_feeler = PoissonNextSend(start, FEELER_INTERVAL); - auto next_extra_block_relay = PoissonNextSend(start, EXTRA_BLOCK_RELAY_ONLY_PEER_INTERVAL); + auto next_feeler = GetExponentialRand(start, FEELER_INTERVAL); + auto next_extra_block_relay = GetExponentialRand(start, EXTRA_BLOCK_RELAY_ONLY_PEER_INTERVAL); const bool dnsseed = gArgs.GetBoolArg("-dnsseed", DEFAULT_DNSSEED); bool add_fixed_seeds = gArgs.GetBoolArg("-fixedseeds", DEFAULT_FIXEDSEEDS); @@ -1999,7 +1999,7 @@ void CConnman::ThreadOpenConnections(const std::vector connect) // // This is similar to the logic for trying extra outbound (full-relay) // peers, except: - // - we do this all the time on a poisson timer, rather than just when + // - we do this all the time on an exponential timer, rather than just when // our tip is stale // - we potentially disconnect our next-youngest block-relay-only peer, if our // newest block-relay-only peer delivers a block more recently. @@ -2008,10 +2008,10 @@ void CConnman::ThreadOpenConnections(const std::vector connect) // Because we can promote these connections to block-relay-only // connections, they do not get their own ConnectionType enum // (similar to how we deal with extra outbound peers). - next_extra_block_relay = PoissonNextSend(now, EXTRA_BLOCK_RELAY_ONLY_PEER_INTERVAL); + next_extra_block_relay = GetExponentialRand(now, EXTRA_BLOCK_RELAY_ONLY_PEER_INTERVAL); conn_type = ConnectionType::BLOCK_RELAY; } else if (now > next_feeler) { - next_feeler = PoissonNextSend(now, FEELER_INTERVAL); + next_feeler = GetExponentialRand(now, FEELER_INTERVAL); conn_type = ConnectionType::FEELER; fFeeler = true; } else { @@ -3064,7 +3064,7 @@ std::chrono::microseconds CConnman::PoissonNextSendInbound(std::chrono::microsec // 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_send_inv_to_incoming = PoissonNextSend(now, average_interval); + m_next_send_inv_to_incoming = GetExponentialRand(now, average_interval); } return m_next_send_inv_to_incoming; } diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 51aa498fb0d..b24f94bd6de 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -4428,13 +4428,13 @@ void PeerManagerImpl::MaybeSendAddr(CNode& node, Peer& peer, std::chrono::micros FastRandomContext insecure_rand; PushAddress(peer, *local_addr, insecure_rand); } - peer.m_next_local_addr_send = PoissonNextSend(current_time, AVG_LOCAL_ADDRESS_BROADCAST_INTERVAL); + peer.m_next_local_addr_send = GetExponentialRand(current_time, AVG_LOCAL_ADDRESS_BROADCAST_INTERVAL); } // We sent an `addr` message to this peer recently. Nothing more to do. if (current_time <= peer.m_next_addr_send) return; - peer.m_next_addr_send = PoissonNextSend(current_time, AVG_ADDRESS_BROADCAST_INTERVAL); + peer.m_next_addr_send = GetExponentialRand(current_time, AVG_ADDRESS_BROADCAST_INTERVAL); if (!Assume(peer.m_addrs_to_send.size() <= MAX_ADDR_TO_SEND)) { // Should be impossible since we always check size before adding to @@ -4506,7 +4506,7 @@ void PeerManagerImpl::MaybeSendFeefilter(CNode& pto, std::chrono::microseconds c m_connman.PushMessage(&pto, CNetMsgMaker(pto.GetCommonVersion()).Make(NetMsgType::FEEFILTER, filterToSend)); pto.m_tx_relay->lastSentFeeFilter = filterToSend; } - pto.m_tx_relay->m_next_send_feefilter = PoissonNextSend(current_time, AVG_FEEFILTER_BROADCAST_INTERVAL); + pto.m_tx_relay->m_next_send_feefilter = GetExponentialRand(current_time, AVG_FEEFILTER_BROADCAST_INTERVAL); } // If the fee filter has changed substantially and it's still more than MAX_FEEFILTER_CHANGE_DELAY // until scheduled broadcast, then move the broadcast to within MAX_FEEFILTER_CHANGE_DELAY. @@ -4788,7 +4788,7 @@ bool PeerManagerImpl::SendMessages(CNode* pto) if (pto->IsInboundConn()) { pto->m_tx_relay->nNextInvSend = m_connman.PoissonNextSendInbound(current_time, INBOUND_INVENTORY_BROADCAST_INTERVAL); } else { - pto->m_tx_relay->nNextInvSend = PoissonNextSend(current_time, OUTBOUND_INVENTORY_BROADCAST_INTERVAL); + pto->m_tx_relay->nNextInvSend = GetExponentialRand(current_time, OUTBOUND_INVENTORY_BROADCAST_INTERVAL); } } diff --git a/src/random.cpp b/src/random.cpp index 03b8d7530d5..5dae80fe313 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -716,7 +716,7 @@ void RandomInit() ReportHardwareRand(); } -std::chrono::microseconds PoissonNextSend(std::chrono::microseconds now, std::chrono::seconds average_interval) +std::chrono::microseconds GetExponentialRand(std::chrono::microseconds now, std::chrono::seconds average_interval) { double unscaled = -std::log1p(GetRand(uint64_t{1} << 48) * -0.0000000000000035527136788 /* -1/2^48 */); return now + std::chrono::duration_cast(unscaled * average_interval + 0.5us); diff --git a/src/random.h b/src/random.h index be73e44a877..5174c553fb9 100644 --- a/src/random.h +++ b/src/random.h @@ -92,7 +92,7 @@ constexpr auto GetRandMillis = GetRandomDuration; * The probability of an event occuring before time x is 1 - e^-(x/a) where a * is the average interval between events. * */ -std::chrono::microseconds PoissonNextSend(std::chrono::microseconds now, std::chrono::seconds average_interval); +std::chrono::microseconds GetExponentialRand(std::chrono::microseconds now, std::chrono::seconds average_interval); int GetRandInt(int nMax) noexcept; uint256 GetRandHash() noexcept; From ea99f5d01e56ab0192d211da1034ffb299876937 Mon Sep 17 00:00:00 2001 From: John Newbery Date: Tue, 14 Dec 2021 10:35:37 +0000 Subject: [PATCH 4/5] [net processing] Move PoissonNextSendInbound to PeerManager --- src/net.cpp | 11 ----------- src/net.h | 8 -------- src/net_processing.cpp | 25 ++++++++++++++++++++++++- src/test/fuzz/connman.cpp | 6 ------ 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/net.cpp b/src/net.cpp index 3ce0b8ee810..a3ff6670b85 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -3058,17 +3058,6 @@ bool CConnman::ForNode(NodeId id, std::function func) return found != nullptr && NodeFullyConnected(found) && func(found); } -std::chrono::microseconds CConnman::PoissonNextSendInbound(std::chrono::microseconds now, std::chrono::seconds average_interval) -{ - if (m_next_send_inv_to_incoming.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_send_inv_to_incoming = GetExponentialRand(now, average_interval); - } - return m_next_send_inv_to_incoming; -} - CSipHasher CConnman::GetDeterministicRandomizer(uint64_t id) const { return CSipHasher(nSeed0, nSeed1).Write(id); diff --git a/src/net.h b/src/net.h index b0162bb9492..ea929b0d0eb 100644 --- a/src/net.h +++ b/src/net.h @@ -936,12 +936,6 @@ public: void WakeMessageHandler(); - /** Attempts to obfuscate tx time through exponentially distributed emitting. - Works assuming that a single interval is used. - Variable intervals will result in privacy decrease. - */ - std::chrono::microseconds PoissonNextSendInbound(std::chrono::microseconds now, std::chrono::seconds average_interval); - /** Return true if we should disconnect the peer for failing an inactivity check. */ bool ShouldRunInactivityChecks(const CNode& node, std::chrono::seconds now) const; @@ -1221,8 +1215,6 @@ private: */ std::atomic_bool m_start_extra_block_relay_peers{false}; - std::atomic m_next_send_inv_to_incoming{0us}; - /** * A vector of -bind=
:=onion arguments each of which is * an address and port that are designated for incoming Tor connections. diff --git a/src/net_processing.cpp b/src/net_processing.cpp index b24f94bd6de..8a7dc9e1f7d 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -444,6 +444,8 @@ private: */ std::map m_peer_map GUARDED_BY(m_peer_mutex); + std::atomic m_next_send_inv_to_incoming{0us}; + /** Number of nodes with fSyncStarted. */ int nSyncStarted GUARDED_BY(cs_main) = 0; @@ -518,6 +520,15 @@ private: Mutex m_recent_confirmed_transactions_mutex; CRollingBloomFilter m_recent_confirmed_transactions GUARDED_BY(m_recent_confirmed_transactions_mutex){48'000, 0.000'001}; + /** + * 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 + * 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). */ + std::chrono::microseconds PoissonNextSendInbound(std::chrono::microseconds now, + std::chrono::seconds average_interval); + /** Have we requested this block from a peer */ bool IsBlockRequested(const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(cs_main); @@ -819,6 +830,18 @@ static void UpdatePreferredDownload(const CNode& node, CNodeState* state) EXCLUS nPreferredDownload += state->fPreferredDownload; } +std::chrono::microseconds PeerManagerImpl::PoissonNextSendInbound(std::chrono::microseconds now, + std::chrono::seconds average_interval) +{ + if (m_next_send_inv_to_incoming.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_send_inv_to_incoming = GetExponentialRand(now, average_interval); + } + return m_next_send_inv_to_incoming; +} + bool PeerManagerImpl::IsBlockRequested(const uint256& hash) { return mapBlocksInFlight.find(hash) != mapBlocksInFlight.end(); @@ -4786,7 +4809,7 @@ bool PeerManagerImpl::SendMessages(CNode* pto) if (pto->m_tx_relay->nNextInvSend < current_time) { fSendTrickle = true; if (pto->IsInboundConn()) { - pto->m_tx_relay->nNextInvSend = m_connman.PoissonNextSendInbound(current_time, INBOUND_INVENTORY_BROADCAST_INTERVAL); + pto->m_tx_relay->nNextInvSend = PoissonNextSendInbound(current_time, INBOUND_INVENTORY_BROADCAST_INTERVAL); } else { pto->m_tx_relay->nNextInvSend = GetExponentialRand(current_time, OUTBOUND_INVENTORY_BROADCAST_INTERVAL); } diff --git a/src/test/fuzz/connman.cpp b/src/test/fuzz/connman.cpp index f87b6f15038..b6154620010 100644 --- a/src/test/fuzz/connman.cpp +++ b/src/test/fuzz/connman.cpp @@ -89,12 +89,6 @@ FUZZ_TARGET_INIT(connman, initialize_connman) [&] { (void)connman.OutboundTargetReached(fuzzed_data_provider.ConsumeBool()); }, - [&] { - // Limit now to int32_t to avoid signed integer overflow - (void)connman.PoissonNextSendInbound( - std::chrono::microseconds{fuzzed_data_provider.ConsumeIntegral()}, - std::chrono::seconds{fuzzed_data_provider.ConsumeIntegral()}); - }, [&] { CSerializedNetMsg serialized_net_msg; serialized_net_msg.m_type = fuzzed_data_provider.ConsumeRandomLengthString(CMessageHeader::COMMAND_SIZE); From 9b8dcb25b57ad31b77c9f37d9a1f5b07dc6378b4 Mon Sep 17 00:00:00 2001 From: John Newbery Date: Tue, 14 Dec 2021 10:46:09 +0000 Subject: [PATCH 5/5] [net processing] Rename PoissonNextSendInbound to NextInvToInbounds --- src/net_processing.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 8a7dc9e1f7d..5e8cd28d076 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -444,7 +444,7 @@ private: */ std::map m_peer_map GUARDED_BY(m_peer_mutex); - std::atomic m_next_send_inv_to_incoming{0us}; + std::atomic m_next_inv_to_inbounds{0us}; /** Number of nodes with fSyncStarted. */ int nSyncStarted GUARDED_BY(cs_main) = 0; @@ -526,8 +526,8 @@ private: * 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). */ - std::chrono::microseconds PoissonNextSendInbound(std::chrono::microseconds now, - std::chrono::seconds average_interval); + std::chrono::microseconds NextInvToInbounds(std::chrono::microseconds now, + std::chrono::seconds average_interval); /** Have we requested this block from a peer */ bool IsBlockRequested(const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(cs_main); @@ -830,16 +830,16 @@ static void UpdatePreferredDownload(const CNode& node, CNodeState* state) EXCLUS nPreferredDownload += state->fPreferredDownload; } -std::chrono::microseconds PeerManagerImpl::PoissonNextSendInbound(std::chrono::microseconds now, - std::chrono::seconds average_interval) +std::chrono::microseconds PeerManagerImpl::NextInvToInbounds(std::chrono::microseconds now, + std::chrono::seconds average_interval) { - if (m_next_send_inv_to_incoming.load() < now) { + 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_send_inv_to_incoming = GetExponentialRand(now, average_interval); + m_next_inv_to_inbounds = GetExponentialRand(now, average_interval); } - return m_next_send_inv_to_incoming; + return m_next_inv_to_inbounds; } bool PeerManagerImpl::IsBlockRequested(const uint256& hash) @@ -4809,7 +4809,7 @@ bool PeerManagerImpl::SendMessages(CNode* pto) if (pto->m_tx_relay->nNextInvSend < current_time) { fSendTrickle = true; if (pto->IsInboundConn()) { - pto->m_tx_relay->nNextInvSend = PoissonNextSendInbound(current_time, INBOUND_INVENTORY_BROADCAST_INTERVAL); + pto->m_tx_relay->nNextInvSend = NextInvToInbounds(current_time, INBOUND_INVENTORY_BROADCAST_INTERVAL); } else { pto->m_tx_relay->nNextInvSend = GetExponentialRand(current_time, OUTBOUND_INVENTORY_BROADCAST_INTERVAL); }