Skip to content

Commit 6a48063

Browse files
author
MarcoFalke
committed
Merge #19858: Periodically make block-relay connections and sync headers
b3a515c Clarify comments around outbound peer eviction (Suhas Daftuar) daffaf0 Periodically make block-relay connections and sync headers (Suhas Daftuar) 3cc8a7a Use conn_type to identify block-relay peers, rather than m_tx_relay == nullptr (Suhas Daftuar) 91d6195 Simplify and clarify extra outbound peer counting (Suhas Daftuar) Pull request description: To make eclipse attacks more difficult, regularly initiate outbound connections and stay connected long enough to sync headers and potentially learn of new blocks. If we learn a new block, rotate out an existing block-relay peer in favor of the new peer. This augments the existing outbound peer rotation that exists -- currently we make new full-relay connections when our tip is stale, which we disconnect after waiting a small time to see if we learn a new block. As block-relay connections use minimal bandwidth, we can make these connections regularly and not just when our tip is stale. Like feeler connections, these connections are not aggressive; whenever our timer fires (once every 5 minutes on average), we'll try to initiate a new block-relay connection as described, but if we fail to connect we just wait for our timer to fire again before repeating with a new peer. ACKs for top commit: ariard: Code Review ACK b3a515c, only change since last time is dropping a useless `cs_main` taking. I manually tested a previous version of the PR, and not substantial change has been introduced since then which would alter behavior IMO. jonatack: Tested ACK b3a515c over several weeks, though this change and behavior could benefit from test coverage and other follow-ups (refactoring, etc.) described in the review feedback. I did not verify the behavior of `m_start_extra_block_relay_peers` only being enabled after initial chain sync. Since my last review, one unneeded `cs_main` lock was removed. Tree-SHA512: 75fc6f8e8003e88e93f86b845caf2d30b8b9c0dbb0a6b8aabe4e24ea4f6327351f736a068a3b2720a8a581b789942a3a47f921e2afdb47e88bc50d078aa37b6f
2 parents 736eb4d + b3a515c commit 6a48063

File tree

6 files changed

+126
-20
lines changed

6 files changed

+126
-20
lines changed

src/init.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ void Shutdown(NodeContext& node)
200200
// using the other before destroying them.
201201
if (node.peerman) UnregisterValidationInterface(node.peerman.get());
202202
// Follow the lock order requirements:
203-
// * CheckForStaleTipAndEvictPeers locks cs_main before indirectly calling GetExtraOutboundCount
203+
// * CheckForStaleTipAndEvictPeers locks cs_main before indirectly calling GetExtraFullOutboundCount
204204
// which locks cs_vNodes.
205205
// * ProcessMessage locks cs_main and g_cs_orphans before indirectly calling ForEachNode which
206206
// locks cs_vNodes.

src/net.cpp

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1827,18 +1827,32 @@ void CConnman::SetTryNewOutboundPeer(bool flag)
18271827
// Also exclude peers that haven't finished initial connection handshake yet
18281828
// (so that we don't decide we're over our desired connection limit, and then
18291829
// evict some peer that has finished the handshake)
1830-
int CConnman::GetExtraOutboundCount()
1830+
int CConnman::GetExtraFullOutboundCount()
18311831
{
1832-
int nOutbound = 0;
1832+
int full_outbound_peers = 0;
18331833
{
18341834
LOCK(cs_vNodes);
18351835
for (const CNode* pnode : vNodes) {
1836-
if (pnode->fSuccessfullyConnected && !pnode->fDisconnect && pnode->IsOutboundOrBlockRelayConn()) {
1837-
++nOutbound;
1836+
if (pnode->fSuccessfullyConnected && !pnode->fDisconnect && pnode->IsFullOutboundConn()) {
1837+
++full_outbound_peers;
18381838
}
18391839
}
18401840
}
1841-
return std::max(nOutbound - m_max_outbound_full_relay - m_max_outbound_block_relay, 0);
1841+
return std::max(full_outbound_peers - m_max_outbound_full_relay, 0);
1842+
}
1843+
1844+
int CConnman::GetExtraBlockRelayCount()
1845+
{
1846+
int block_relay_peers = 0;
1847+
{
1848+
LOCK(cs_vNodes);
1849+
for (const CNode* pnode : vNodes) {
1850+
if (pnode->fSuccessfullyConnected && !pnode->fDisconnect && pnode->IsBlockOnlyConn()) {
1851+
++block_relay_peers;
1852+
}
1853+
}
1854+
}
1855+
return std::max(block_relay_peers - m_max_outbound_block_relay, 0);
18421856
}
18431857

18441858
void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
@@ -1869,6 +1883,7 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
18691883

18701884
// Minimum time before next feeler connection (in microseconds).
18711885
int64_t nNextFeeler = PoissonNextSend(nStart*1000*1000, FEELER_INTERVAL);
1886+
int64_t nNextExtraBlockRelay = PoissonNextSend(nStart*1000*1000, EXTRA_BLOCK_RELAY_ONLY_PEER_INTERVAL);
18721887
while (!interruptNet)
18731888
{
18741889
ProcessAddrFetch();
@@ -1941,8 +1956,9 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
19411956
// until we hit our block-relay-only peer limit.
19421957
// GetTryNewOutboundPeer() gets set when a stale tip is detected, so we
19431958
// try opening an additional OUTBOUND_FULL_RELAY connection. If none of
1944-
// these conditions are met, check the nNextFeeler timer to decide if
1945-
// we should open a FEELER.
1959+
// these conditions are met, check to see if it's time to try an extra
1960+
// block-relay-only peer (to confirm our tip is current, see below) or the nNextFeeler
1961+
// timer to decide if we should open a FEELER.
19461962

19471963
if (!m_anchors.empty() && (nOutboundBlockRelay < m_max_outbound_block_relay)) {
19481964
conn_type = ConnectionType::BLOCK_RELAY;
@@ -1953,6 +1969,30 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
19531969
conn_type = ConnectionType::BLOCK_RELAY;
19541970
} else if (GetTryNewOutboundPeer()) {
19551971
// OUTBOUND_FULL_RELAY
1972+
} else if (nTime > nNextExtraBlockRelay && m_start_extra_block_relay_peers) {
1973+
// Periodically connect to a peer (using regular outbound selection
1974+
// methodology from addrman) and stay connected long enough to sync
1975+
// headers, but not much else.
1976+
//
1977+
// Then disconnect the peer, if we haven't learned anything new.
1978+
//
1979+
// The idea is to make eclipse attacks very difficult to pull off,
1980+
// because every few minutes we're finding a new peer to learn headers
1981+
// from.
1982+
//
1983+
// This is similar to the logic for trying extra outbound (full-relay)
1984+
// peers, except:
1985+
// - we do this all the time on a poisson timer, rather than just when
1986+
// our tip is stale
1987+
// - we potentially disconnect our next-youngest block-relay-only peer, if our
1988+
// newest block-relay-only peer delivers a block more recently.
1989+
// See the eviction logic in net_processing.cpp.
1990+
//
1991+
// Because we can promote these connections to block-relay-only
1992+
// connections, they do not get their own ConnectionType enum
1993+
// (similar to how we deal with extra outbound peers).
1994+
nNextExtraBlockRelay = PoissonNextSend(nTime, EXTRA_BLOCK_RELAY_ONLY_PEER_INTERVAL);
1995+
conn_type = ConnectionType::BLOCK_RELAY;
19561996
} else if (nTime > nNextFeeler) {
19571997
nNextFeeler = PoissonNextSend(nTime, FEELER_INTERVAL);
19581998
conn_type = ConnectionType::FEELER;

src/net.h

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ static const bool DEFAULT_WHITELISTFORCERELAY = false;
4848
static const int TIMEOUT_INTERVAL = 20 * 60;
4949
/** Run the feeler connection loop once every 2 minutes or 120 seconds. **/
5050
static const int FEELER_INTERVAL = 120;
51+
/** Run the extra block-relay-only connection loop once every 5 minutes. **/
52+
static const int EXTRA_BLOCK_RELAY_ONLY_PEER_INTERVAL = 300;
5153
/** The maximum number of addresses from our addrman to return in response to a getaddr message. */
5254
static constexpr size_t MAX_ADDR_TO_SEND = 1000;
5355
/** Maximum length of incoming protocol messages (no message over 4 MB is currently acceptable). */
@@ -330,13 +332,20 @@ class CConnman
330332
void SetTryNewOutboundPeer(bool flag);
331333
bool GetTryNewOutboundPeer();
332334

335+
void StartExtraBlockRelayPeers() {
336+
LogPrint(BCLog::NET, "net: enabling extra block-relay-only peers\n");
337+
m_start_extra_block_relay_peers = true;
338+
}
339+
333340
// Return the number of outbound peers we have in excess of our target (eg,
334341
// if we previously called SetTryNewOutboundPeer(true), and have since set
335342
// to false, we may have extra peers that we wish to disconnect). This may
336343
// return a value less than (num_outbound_connections - num_outbound_slots)
337344
// in cases where some outbound connections are not yet fully connected, or
338345
// not yet fully disconnected.
339-
int GetExtraOutboundCount();
346+
int GetExtraFullOutboundCount();
347+
// Count the number of block-relay-only peers we have over our limit.
348+
int GetExtraBlockRelayCount();
340349

341350
bool AddNode(const std::string& node);
342351
bool RemoveAddedNode(const std::string& node);
@@ -594,6 +603,12 @@ class CConnman
594603
* This takes the place of a feeler connection */
595604
std::atomic_bool m_try_another_outbound_peer;
596605

606+
/** flag for initiating extra block-relay-only peer connections.
607+
* this should only be enabled after initial chain sync has occurred,
608+
* as these connections are intended to be short-lived and low-bandwidth.
609+
*/
610+
std::atomic_bool m_start_extra_block_relay_peers{false};
611+
597612
std::atomic<int64_t> m_next_send_inv_to_incoming{0};
598613

599614
/**

src/net_processing.cpp

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2466,7 +2466,7 @@ void PeerManager::ProcessMessage(CNode& pfrom, const std::string& msg_type, CDat
24662466
LogPrintf("New outbound peer connected: version: %d, blocks=%d, peer=%d%s (%s)\n",
24672467
pfrom.nVersion.load(), pfrom.nStartingHeight,
24682468
pfrom.GetId(), (fLogIPs ? strprintf(", peeraddr=%s", pfrom.addr.ToString()) : ""),
2469-
pfrom.m_tx_relay == nullptr ? "block-relay" : "full-relay");
2469+
pfrom.IsBlockOnlyConn() ? "block-relay" : "full-relay");
24702470
}
24712471

24722472
if (pfrom.GetCommonVersion() >= SENDHEADERS_VERSION) {
@@ -3909,11 +3909,54 @@ void PeerManager::ConsiderEviction(CNode& pto, int64_t time_in_seconds)
39093909

39103910
void PeerManager::EvictExtraOutboundPeers(int64_t time_in_seconds)
39113911
{
3912-
// Check whether we have too many outbound peers
3913-
int extra_peers = m_connman.GetExtraOutboundCount();
3914-
if (extra_peers > 0) {
3915-
// If we have more outbound peers than we target, disconnect one.
3916-
// Pick the outbound peer that least recently announced
3912+
// If we have any extra block-relay-only peers, disconnect the youngest unless
3913+
// it's given us a block -- in which case, compare with the second-youngest, and
3914+
// out of those two, disconnect the peer who least recently gave us a block.
3915+
// The youngest block-relay-only peer would be the extra peer we connected
3916+
// to temporarily in order to sync our tip; see net.cpp.
3917+
// Note that we use higher nodeid as a measure for most recent connection.
3918+
if (m_connman.GetExtraBlockRelayCount() > 0) {
3919+
std::pair<NodeId, int64_t> youngest_peer{-1, 0}, next_youngest_peer{-1, 0};
3920+
3921+
m_connman.ForEachNode([&](CNode* pnode) {
3922+
if (!pnode->IsBlockOnlyConn() || pnode->fDisconnect) return;
3923+
if (pnode->GetId() > youngest_peer.first) {
3924+
next_youngest_peer = youngest_peer;
3925+
youngest_peer.first = pnode->GetId();
3926+
youngest_peer.second = pnode->nLastBlockTime;
3927+
}
3928+
});
3929+
NodeId to_disconnect = youngest_peer.first;
3930+
if (youngest_peer.second > next_youngest_peer.second) {
3931+
// Our newest block-relay-only peer gave us a block more recently;
3932+
// disconnect our second youngest.
3933+
to_disconnect = next_youngest_peer.first;
3934+
}
3935+
m_connman.ForNode(to_disconnect, [&](CNode* pnode) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) {
3936+
AssertLockHeld(::cs_main);
3937+
// Make sure we're not getting a block right now, and that
3938+
// we've been connected long enough for this eviction to happen
3939+
// at all.
3940+
// Note that we only request blocks from a peer if we learn of a
3941+
// valid headers chain with at least as much work as our tip.
3942+
CNodeState *node_state = State(pnode->GetId());
3943+
if (node_state == nullptr ||
3944+
(time_in_seconds - pnode->nTimeConnected >= MINIMUM_CONNECT_TIME && node_state->nBlocksInFlight == 0)) {
3945+
pnode->fDisconnect = true;
3946+
LogPrint(BCLog::NET, "disconnecting extra block-relay-only peer=%d (last block received at time %d)\n", pnode->GetId(), pnode->nLastBlockTime);
3947+
return true;
3948+
} else {
3949+
LogPrint(BCLog::NET, "keeping block-relay-only peer=%d chosen for eviction (connect time: %d, blocks_in_flight: %d)\n",
3950+
pnode->GetId(), pnode->nTimeConnected, node_state->nBlocksInFlight);
3951+
}
3952+
return false;
3953+
});
3954+
}
3955+
3956+
// Check whether we have too many OUTBOUND_FULL_RELAY peers
3957+
if (m_connman.GetExtraFullOutboundCount() > 0) {
3958+
// If we have more OUTBOUND_FULL_RELAY peers than we target, disconnect one.
3959+
// Pick the OUTBOUND_FULL_RELAY peer that least recently announced
39173960
// us a new block, with ties broken by choosing the more recent
39183961
// connection (higher node id)
39193962
NodeId worst_peer = -1;
@@ -3922,14 +3965,13 @@ void PeerManager::EvictExtraOutboundPeers(int64_t time_in_seconds)
39223965
m_connman.ForEachNode([&](CNode* pnode) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) {
39233966
AssertLockHeld(::cs_main);
39243967

3925-
// Ignore non-outbound peers, or nodes marked for disconnect already
3926-
if (!pnode->IsOutboundOrBlockRelayConn() || pnode->fDisconnect) return;
3968+
// Only consider OUTBOUND_FULL_RELAY peers that are not already
3969+
// marked for disconnection
3970+
if (!pnode->IsFullOutboundConn() || pnode->fDisconnect) return;
39273971
CNodeState *state = State(pnode->GetId());
39283972
if (state == nullptr) return; // shouldn't be possible, but just in case
39293973
// Don't evict our protected peers
39303974
if (state->m_chain_sync.m_protect) return;
3931-
// Don't evict our block-relay-only peers.
3932-
if (pnode->m_tx_relay == nullptr) return;
39333975
if (state->m_last_block_announcement < oldest_block_announcement || (state->m_last_block_announcement == oldest_block_announcement && pnode->GetId() > worst_peer)) {
39343976
worst_peer = pnode->GetId();
39353977
oldest_block_announcement = state->m_last_block_announcement;
@@ -3985,6 +4027,11 @@ void PeerManager::CheckForStaleTipAndEvictPeers()
39854027
}
39864028
m_stale_tip_check_time = time_in_seconds + STALE_CHECK_INTERVAL;
39874029
}
4030+
4031+
if (!m_initial_sync_finished && CanDirectFetch(m_chainparams.GetConsensus())) {
4032+
m_connman.StartExtraBlockRelayPeers();
4033+
m_initial_sync_finished = true;
4034+
}
39884035
}
39894036

39904037
namespace {

src/net_processing.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@ class PeerManager final : public CValidationInterface, public NetEventsInterface
206206
//* Whether this node is running in blocks only mode */
207207
const bool m_ignore_incoming_txs;
208208

209+
/** Whether we've completed initial sync yet, for determining when to turn
210+
* on extra block-relay-only peers. */
211+
bool m_initial_sync_finished{false};
212+
209213
/** Protects m_peer_map */
210214
mutable Mutex m_peer_mutex;
211215
/**

src/test/fuzz/connman.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ void test_one_input(const std::vector<uint8_t>& buffer)
145145
}
146146
(void)connman.GetAddedNodeInfo();
147147
(void)connman.GetBestHeight();
148-
(void)connman.GetExtraOutboundCount();
148+
(void)connman.GetExtraFullOutboundCount();
149149
(void)connman.GetLocalServices();
150150
(void)connman.GetMaxOutboundTarget();
151151
(void)connman.GetMaxOutboundTimeframe();

0 commit comments

Comments
 (0)