diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -226,7 +226,7 @@ } // Follow the lock order requirements: // * CheckForStaleTipAndEvictPeers locks cs_main before indirectly calling - // GetExtraOutboundCount which locks cs_vNodes. + // GetExtraFullOutboundCount which locks cs_vNodes. // * ProcessMessage locks cs_main and g_cs_orphans before indirectly calling // ForEachNode which locks cs_vNodes. // * CConnman::Stop calls DeleteNode, which calls FinalizeNode, which locks diff --git a/src/net.h b/src/net.h --- a/src/net.h +++ b/src/net.h @@ -59,6 +59,8 @@ static const int TIMEOUT_INTERVAL = 20 * 60; /** Run the feeler connection loop once every 2 minutes or 120 seconds. **/ static const int FEELER_INTERVAL = 120; +/** Run the extra block-relay-only connection loop once every 5 minutes. **/ +static const int EXTRA_BLOCK_RELAY_ONLY_PEER_INTERVAL = 300; /** * The maximum number of addresses from our addrman to return in response to * a getaddr message. @@ -1113,13 +1115,20 @@ void SetTryNewOutboundPeer(bool flag); bool GetTryNewOutboundPeer(); + void StartExtraBlockRelayPeers() { + LogPrint(BCLog::NET, "net: enabling extra block-relay-only peers\n"); + m_start_extra_block_relay_peers = true; + } + // Return the number of outbound peers we have in excess of our target (eg, // if we previously called SetTryNewOutboundPeer(true), and have since set // to false, we may have extra peers that we wish to disconnect). This may // return a value less than (num_outbound_connections - num_outbound_slots) // in cases where some outbound connections are not yet fully connected, or // not yet fully disconnected. - int GetExtraOutboundCount(); + int GetExtraFullOutboundCount(); + // Count the number of block-relay-only peers we have over our limit. + int GetExtraBlockRelayCount(); bool AddNode(const std::string &node); bool RemoveAddedNode(const std::string &node); @@ -1409,6 +1418,13 @@ */ std::atomic_bool m_try_another_outbound_peer; + /** + * flag for initiating extra block-relay-only peer connections. + * this should only be enabled after initial chain sync has occurred, + * as these connections are intended to be short-lived and low-bandwidth. + */ + std::atomic_bool m_start_extra_block_relay_peers{false}; + std::atomic m_next_send_inv_to_incoming{0}; /** diff --git a/src/net.cpp b/src/net.cpp --- a/src/net.cpp +++ b/src/net.cpp @@ -2068,19 +2068,32 @@ // Also exclude peers that haven't finished initial connection handshake yet (so // that we don't decide we're over our desired connection limit, and then evict // some peer that has finished the handshake). -int CConnman::GetExtraOutboundCount() { - int nOutbound = 0; +int CConnman::GetExtraFullOutboundCount() { + int full_outbound_peers = 0; { LOCK(cs_vNodes); for (const CNode *pnode : vNodes) { if (pnode->fSuccessfullyConnected && !pnode->fDisconnect && - pnode->IsOutboundOrBlockRelayConn()) { - ++nOutbound; + pnode->IsFullOutboundConn()) { + ++full_outbound_peers; } } } - return std::max( - nOutbound - m_max_outbound_full_relay - m_max_outbound_block_relay, 0); + return std::max(full_outbound_peers - m_max_outbound_full_relay, 0); +} + +int CConnman::GetExtraBlockRelayCount() { + int block_relay_peers = 0; + { + LOCK(cs_vNodes); + for (const CNode *pnode : vNodes) { + if (pnode->fSuccessfullyConnected && !pnode->fDisconnect && + pnode->IsBlockOnlyConn()) { + ++block_relay_peers; + } + } + } + return std::max(block_relay_peers - m_max_outbound_block_relay, 0); } void CConnman::ThreadOpenConnections(const std::vector connect) { @@ -2111,6 +2124,8 @@ // Minimum time before next feeler connection (in microseconds). int64_t nNextFeeler = PoissonNextSend(nStart * 1000 * 1000, FEELER_INTERVAL); + int64_t nNextExtraBlockRelay = PoissonNextSend( + nStart * 1000 * 1000, EXTRA_BLOCK_RELAY_ONLY_PEER_INTERVAL); while (!interruptNet) { ProcessAddrFetch(); @@ -2194,8 +2209,9 @@ // until we hit our block-relay-only peer limit. // GetTryNewOutboundPeer() gets set when a stale tip is detected, so we // try opening an additional OUTBOUND_FULL_RELAY connection. If none of - // these conditions are met, check the nNextFeeler timer to decide if - // we should open a FEELER. + // these conditions are met, check to see if it's time to try an extra + // block-relay-only peer (to confirm our tip is current, see below) or + // the nNextFeeler timer to decide if we should open a FEELER. if (!m_anchors.empty() && (nOutboundBlockRelay < m_max_outbound_block_relay)) { @@ -2207,6 +2223,33 @@ conn_type = ConnectionType::BLOCK_RELAY; } else if (GetTryNewOutboundPeer()) { // OUTBOUND_FULL_RELAY + } else if (nTime > nNextExtraBlockRelay && + m_start_extra_block_relay_peers) { + // Periodically connect to a peer (using regular outbound selection + // methodology from addrman) and stay connected long enough to sync + // headers, but not much else. + // + // Then disconnect the peer, if we haven't learned anything new. + // + // The idea is to make eclipse attacks very difficult to pull off, + // because every few minutes we're finding a new peer to learn + // headers from. + // + // 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 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. + // See the eviction logic in net_processing.cpp. + // + // 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). + nNextExtraBlockRelay = + PoissonNextSend(nTime, EXTRA_BLOCK_RELAY_ONLY_PEER_INTERVAL); + conn_type = ConnectionType::BLOCK_RELAY; } else if (nTime > nNextFeeler) { nNextFeeler = PoissonNextSend(nTime, FEELER_INTERVAL); conn_type = ConnectionType::FEELER; diff --git a/src/net_processing.cpp b/src/net_processing.cpp --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -558,6 +558,12 @@ /** Whether this node is running in blocks only mode */ const bool m_ignore_incoming_txs; + /** + * Whether we've completed initial sync yet, for determining when to turn + * on extra block-relay-only peers. + */ + bool m_initial_sync_finished{false}; + /** * Protects m_peer_map. This mutex must not be locked while holding a lock * on any of the mutexes inside a Peer object. @@ -3467,7 +3473,7 @@ pfrom.nVersion.load(), peer->m_starting_height, pfrom.GetId(), (fLogIPs ? strprintf(", peeraddr=%s", pfrom.addr.ToString()) : ""), - pfrom.m_tx_relay == nullptr ? "block-relay" : "full-relay"); + pfrom.IsBlockOnlyConn() ? "block-relay" : "full-relay"); } if (pfrom.GetCommonVersion() >= SENDHEADERS_VERSION) { @@ -5381,15 +5387,76 @@ } void PeerManagerImpl::EvictExtraOutboundPeers(int64_t time_in_seconds) { - // Check whether we have too many outbound peers - int extra_peers = m_connman.GetExtraOutboundCount(); - if (extra_peers <= 0) { + // If we have any extra block-relay-only peers, disconnect the youngest + // unless it's given us a block -- in which case, compare with the + // second-youngest, and out of those two, disconnect the peer who least + // recently gave us a block. + // The youngest block-relay-only peer would be the extra peer we connected + // to temporarily in order to sync our tip; see net.cpp. + // Note that we use higher nodeid as a measure for most recent connection. + if (m_connman.GetExtraBlockRelayCount() > 0) { + std::pair youngest_peer{-1, 0}, + next_youngest_peer{-1, 0}; + + m_connman.ForEachNode([&](CNode *pnode) { + if (!pnode->IsBlockOnlyConn() || pnode->fDisconnect) { + return; + } + if (pnode->GetId() > youngest_peer.first) { + next_youngest_peer = youngest_peer; + youngest_peer.first = pnode->GetId(); + youngest_peer.second = pnode->nLastBlockTime; + } + }); + + NodeId to_disconnect = youngest_peer.first; + if (youngest_peer.second > next_youngest_peer.second) { + // Our newest block-relay-only peer gave us a block more recently; + // disconnect our second youngest. + to_disconnect = next_youngest_peer.first; + } + + m_connman.ForNode( + to_disconnect, + [&](CNode *pnode) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + AssertLockHeld(::cs_main); + // Make sure we're not getting a block right now, and that we've + // been connected long enough for this eviction to happen at + // all. Note that we only request blocks from a peer if we learn + // of a valid headers chain with at least as much work as our + // tip. + CNodeState *node_state = State(pnode->GetId()); + if (node_state == nullptr || + (time_in_seconds - pnode->nTimeConnected >= + MINIMUM_CONNECT_TIME && + node_state->nBlocksInFlight == 0)) { + pnode->fDisconnect = true; + LogPrint(BCLog::NET, + "disconnecting extra block-relay-only peer=%d " + "(last block received at time %d)\n", + pnode->GetId(), pnode->nLastBlockTime); + return true; + } else { + LogPrint( + BCLog::NET, + "keeping block-relay-only peer=%d chosen for eviction " + "(connect time: %d, blocks_in_flight: %d)\n", + pnode->GetId(), pnode->nTimeConnected, + node_state->nBlocksInFlight); + } + return false; + }); + } + + // Check whether we have too many OUTBOUND_FULL_RELAY peers + if (m_connman.GetExtraFullOutboundCount() <= 0) { return; } - // If we have more outbound peers than we target, disconnect one. - // Pick the outbound peer that least recently announced us a new block, with - // ties broken by choosing the more recent connection (higher node id) + // If we have more OUTBOUND_FULL_RELAY peers than we target, disconnect one. + // Pick the OUTBOUND_FULL_RELAY peer that least recently announced us a new + // block, with ties broken by choosing the more recent connection (higher + // node id) NodeId worst_peer = -1; int64_t oldest_block_announcement = std::numeric_limits::max(); @@ -5397,8 +5464,9 @@ ::cs_main) { AssertLockHeld(::cs_main); - // Ignore non-outbound peers, or nodes marked for disconnect already - if (!pnode->IsOutboundOrBlockRelayConn() || pnode->fDisconnect) { + // Only consider OUTBOUND_FULL_RELAY peers that are not already marked + // for disconnection + if (!pnode->IsFullOutboundConn() || pnode->fDisconnect) { return; } CNodeState *state = State(pnode->GetId()); @@ -5410,11 +5478,6 @@ if (state->m_chain_sync.m_protect) { return; } - // Don't evict our block-relay-only peers. - if (pnode->m_tx_relay == nullptr) { - return; - } - if (state->m_last_block_announcement < oldest_block_announcement || (state->m_last_block_announcement == oldest_block_announcement && pnode->GetId() > worst_peer)) { @@ -5472,23 +5535,27 @@ EvictExtraOutboundPeers(time_in_seconds); - if (time_in_seconds <= m_stale_tip_check_time) { - return; + if (time_in_seconds > m_stale_tip_check_time) { + // Check whether our tip is stale, and if so, allow using an extra + // outbound peer. + if (!fImporting && !fReindex && m_connman.GetNetworkActive() && + m_connman.GetUseAddrmanOutgoing() && + TipMayBeStale(m_chainparams.GetConsensus())) { + LogPrintf("Potential stale tip detected, will try using extra " + "outbound peer (last tip update: %d seconds ago)\n", + time_in_seconds - g_last_tip_update); + m_connman.SetTryNewOutboundPeer(true); + } else if (m_connman.GetTryNewOutboundPeer()) { + m_connman.SetTryNewOutboundPeer(false); + } + m_stale_tip_check_time = time_in_seconds + STALE_CHECK_INTERVAL; } - // Check whether our tip is stale, and if so, allow using an extra outbound - // peer. - if (!fImporting && !fReindex && m_connman.GetNetworkActive() && - m_connman.GetUseAddrmanOutgoing() && - TipMayBeStale(m_chainparams.GetConsensus())) { - LogPrintf("Potential stale tip detected, will try using extra outbound " - "peer (last tip update: %d seconds ago)\n", - time_in_seconds - g_last_tip_update); - m_connman.SetTryNewOutboundPeer(true); - } else if (m_connman.GetTryNewOutboundPeer()) { - m_connman.SetTryNewOutboundPeer(false); + if (!m_initial_sync_finished && + CanDirectFetch(m_chainparams.GetConsensus())) { + m_connman.StartExtraBlockRelayPeers(); + m_initial_sync_finished = true; } - m_stale_tip_check_time = time_in_seconds + STALE_CHECK_INTERVAL; } void PeerManagerImpl::MaybeSendPing(CNode &node_to, Peer &peer) {