From feb635f3b89377146e58c6f9d1b5b261bd93421a Mon Sep 17 00:00:00 2001 From: vach Date: Sun, 13 Sep 2026 18:34:45 +0800 Subject: [PATCH 1/4] net: disconnect spam node peers when no longer useful --- src/net_processing.cpp | 31 ++++++ .../functional/p2p_blake2b_postfork_filter.py | 104 ++++++++++++++++++ test/functional/test_runner.py | 2 + 3 files changed, 137 insertions(+) create mode 100644 test/functional/p2p_blake2b_postfork_filter.py diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 53f1aa313acd..083b5c6703f1 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -564,6 +564,14 @@ class PeerManagerImpl final : public PeerManager */ bool MaybeDiscourageAndDisconnect(CNode& pnode, Peer& peer); + /** Check if we are good to disconnect SpamCoin peers. Pure check, callers log and disconnect. + * + * @param[in] node The node to check. + * @param[in] services The services the peer advertised in its version message. + * @return True if peer lacks NODE_BLAKE2B bit AND pre-fork history is complete AND peer is not manually added. + */ + bool CanDisconnectSpamCoinPeer(const CNode& node, ServiceFlags services) const; + /** Handle a transaction whose result was not MempoolAcceptResult::ResultType::VALID. * @param[in] first_time_failure Whether we should consider inserting into vExtraTxnForCompact, adding * a new orphan to resolve, or looking for a package to submit. @@ -3470,6 +3478,12 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, return; } + if (CanDisconnectSpamCoinPeer(pfrom, nServices)) { + LogDebug(BCLog::NET, "node lacks NODE_BLAKE2B, %s\n", pfrom.DisconnectMsg(fLogIPs)); + pfrom.fDisconnect = true; + return; + } + if (nVersion < MIN_PEER_PROTO_VERSION) { // disconnect from peers older than this proto version LogDebug(BCLog::NET, "peer using obsolete version %i, %s\n", nVersion, pfrom.DisconnectMsg(fLogIPs)); @@ -4954,6 +4968,16 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, return; } +bool PeerManagerImpl::CanDisconnectSpamCoinPeer(const CNode& node, ServiceFlags services) const +{ + // Past the shared pre-fork history, a peer lacking NODE_BLAKE2B is no longer useful + bool weArePastSharedHistory = m_chainparams.GetConsensus().IsBlake2bHeight(m_best_height + 1); + bool peerLacksBlakeBit = !(services & NODE_BLAKE2B); + bool peerIsNotManuallyAdded = !node.IsManualConn(); + + return weArePastSharedHistory && peerLacksBlakeBit && peerIsNotManuallyAdded; +} + bool PeerManagerImpl::MaybeDiscourageAndDisconnect(CNode& pnode, Peer& peer) { { @@ -5520,6 +5544,13 @@ bool PeerManagerImpl::SendMessages(CNode* pto) return true; } + // Peers that connected before shared history was completed are dropped once shared history is synced. + if (CanDisconnectSpamCoinPeer(*pto, peer->m_their_services)) { + LogDebug(BCLog::NET, "existing node lacking NODE_BLAKE2B is no longer useful, %s\n", pto->DisconnectMsg(fLogIPs)); + pto->fDisconnect = true; + return true; + } + MaybeSendPing(*pto, *peer, current_time); // MaybeSendPing may have marked peer for disconnection diff --git a/test/functional/p2p_blake2b_postfork_filter.py b/test/functional/p2p_blake2b_postfork_filter.py new file mode 100644 index 000000000000..648e194edff0 --- /dev/null +++ b/test/functional/p2p_blake2b_postfork_filter.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 The Bitcoin Knots developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test refusal of peers lacking NODE_BLAKE2B once shared pre-fork history is complete. + +While the next block to connect is below the fork height, peers without +NODE_BLAKE2B are accepted. From then on they are disconnected: new ones at +their version message, connected ones once the tip crosses into fork history. +""" + +from test_framework.messages import ( + NODE_BLAKE2B, + NODE_NETWORK, + NODE_P2P_V2, + NODE_WITNESS, +) +from test_framework.p2p import P2PInterface +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + +FORK_HEIGHT = 20 +HANDSHAKE_DROPPED = "node lacks NODE_BLAKE2B, disconnecting" +NO_LONGER_VALID = "existing node lacking NODE_BLAKE2B is no longer useful, disconnecting" + + +class Blake2bPostforkFilterTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [[ + f"-testactivationheight=blake2b@{FORK_HEIGHT}", + "-blake2b_headline=BLAKE2b functional test headline", + "-maxconnections=30", + ]] + + def services(self, *, blake2b): + services = NODE_NETWORK | NODE_WITNESS + if blake2b: + services |= NODE_BLAKE2B + if self.options.v2transport: + services |= NODE_P2P_V2 + return services + + def inbound(self, *, blake2b, **kwargs): + return self.nodes[0].add_p2p_connection( + P2PInterface(), services=self.services(blake2b=blake2b), **kwargs) + + def outbound(self, p2p_idx, *, blake2b, **kwargs): + return self.nodes[0].add_outbound_p2p_connection( + P2PInterface(), p2p_idx=p2p_idx, connection_type="outbound-full-relay", + services=self.services(blake2b=blake2b), + supports_v2_p2p=self.options.v2transport, + advertise_v2_p2p=self.options.v2transport, **kwargs) + + def run_test(self): + node = self.nodes[0] + addr = node.get_deterministic_priv_key().address + + self.log.info("Below the fork height, peers lacking NODE_BLAKE2B are accepted") + self.generatetoaddress(node, FORK_HEIGHT - 2, addr) + legacy_in = self.inbound(blake2b=False) + fork_in = self.inbound(blake2b=True) + legacy_out = self.outbound(0, blake2b=False) + fork_out = self.outbound(1, blake2b=True) + for peer in (legacy_in, fork_in, legacy_out, fork_out): + peer.sync_with_ping() + assert_equal(len(node.getpeerinfo()), 4) + + self.log.info("Completing shared history disconnects connected peers lacking NODE_BLAKE2B") + with node.assert_debug_log([NO_LONGER_VALID], unexpected_msgs=[HANDSHAKE_DROPPED]): + self.generatetoaddress(node, 1, addr) + legacy_in.wait_for_disconnect() + legacy_out.wait_for_disconnect() + fork_in.sync_with_ping() + fork_out.sync_with_ping() + self.wait_until(lambda: len(node.getpeerinfo()) == 2) + + self.log.info("New inbound peers lacking NODE_BLAKE2B are refused at their version message") + with node.assert_debug_log([HANDSHAKE_DROPPED], unexpected_msgs=[NO_LONGER_VALID]): + refused = self.inbound(blake2b=False, expect_success=False) + refused.wait_for_disconnect() + + self.log.info("New outbound peers lacking NODE_BLAKE2B are refused at their version message") + with node.assert_debug_log([HANDSHAKE_DROPPED], unexpected_msgs=[NO_LONGER_VALID]): + self.outbound(2, blake2b=False, wait_for_disconnect=True) + self.wait_until(lambda: len(node.getpeerinfo()) == 2) + + self.log.info("Peers advertising NODE_BLAKE2B still connect") + self.inbound(blake2b=True).sync_with_ping() + self.outbound(3, blake2b=True).sync_with_ping() + self.wait_until(lambda: len(node.getpeerinfo()) == 4) + + self.log.info("Fork peers stay connected across the first BLAKE2b block") + self.generatetoaddress(node, 1, addr) + assert_equal(node.getblockcount(), FORK_HEIGHT) + for peer in node.p2ps: + if peer.is_connected: + peer.sync_with_ping() + assert_equal(len(node.getpeerinfo()), 4) + + +if __name__ == '__main__': + Blake2bPostforkFilterTest(__file__).main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index e912af0f5cfc..d04ec265ba86 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -144,6 +144,8 @@ 'p2p_dns_seeds.py', 'p2p_blake2b_dns_immediate.py', 'p2p_blake2b_outbound_preference.py', + 'p2p_blake2b_postfork_filter.py', + 'p2p_blake2b_postfork_filter.py --v2transport', 'wallet_groups.py --legacy-wallet', 'wallet_groups.py --descriptors', 'p2p_blockfilters.py', From 3d8eb29cf4f53ccbc3cb7dd397d2fa469a98066f Mon Sep 17 00:00:00 2001 From: vach Date: Sun, 13 Sep 2026 23:10:55 +0800 Subject: [PATCH 2/4] net: don't disconnect addr-fetch and feeler connections lacking NODE_BLAKE2B --- src/net_processing.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 083b5c6703f1..e7c7a27a26bb 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -568,7 +568,7 @@ class PeerManagerImpl final : public PeerManager * * @param[in] node The node to check. * @param[in] services The services the peer advertised in its version message. - * @return True if peer lacks NODE_BLAKE2B bit AND pre-fork history is complete AND peer is not manually added. + * @return True if peer lacks NODE_BLAKE2B bit AND pre-fork history is complete AND peer is not manually added AND peer is not addr-fetch or feeler. */ bool CanDisconnectSpamCoinPeer(const CNode& node, ServiceFlags services) const; @@ -4974,8 +4974,10 @@ bool PeerManagerImpl::CanDisconnectSpamCoinPeer(const CNode& node, ServiceFlags bool weArePastSharedHistory = m_chainparams.GetConsensus().IsBlake2bHeight(m_best_height + 1); bool peerLacksBlakeBit = !(services & NODE_BLAKE2B); bool peerIsNotManuallyAdded = !node.IsManualConn(); + // Addr-fetch and feeler connections are short-lived and close on their own. + bool peerIsNotShortLived = !node.IsAddrFetchConn() && !node.IsFeelerConn(); - return weArePastSharedHistory && peerLacksBlakeBit && peerIsNotManuallyAdded; + return weArePastSharedHistory && peerLacksBlakeBit && peerIsNotManuallyAdded && peerIsNotShortLived; } bool PeerManagerImpl::MaybeDiscourageAndDisconnect(CNode& pnode, Peer& peer) From 533c363b51b0c06140d72616e12aeacbb7c760b4 Mon Sep 17 00:00:00 2001 From: vach Date: Sun, 13 Sep 2026 23:11:13 +0800 Subject: [PATCH 3/4] net: don't disconnect NoBan peers lacking NODE_BLAKE2B --- src/net_processing.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/net_processing.cpp b/src/net_processing.cpp index e7c7a27a26bb..1a1c5a384a48 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -568,7 +568,7 @@ class PeerManagerImpl final : public PeerManager * * @param[in] node The node to check. * @param[in] services The services the peer advertised in its version message. - * @return True if peer lacks NODE_BLAKE2B bit AND pre-fork history is complete AND peer is not manually added AND peer is not addr-fetch or feeler. + * @return True if peer lacks NODE_BLAKE2B bit AND pre-fork history is complete AND peer is not manually added or NoBan AND peer is not addr-fetch or feeler. */ bool CanDisconnectSpamCoinPeer(const CNode& node, ServiceFlags services) const; @@ -4974,10 +4974,11 @@ bool PeerManagerImpl::CanDisconnectSpamCoinPeer(const CNode& node, ServiceFlags bool weArePastSharedHistory = m_chainparams.GetConsensus().IsBlake2bHeight(m_best_height + 1); bool peerLacksBlakeBit = !(services & NODE_BLAKE2B); bool peerIsNotManuallyAdded = !node.IsManualConn(); + bool peerIsNotNoBan = !node.HasPermission(NetPermissionFlags::NoBan); // Addr-fetch and feeler connections are short-lived and close on their own. bool peerIsNotShortLived = !node.IsAddrFetchConn() && !node.IsFeelerConn(); - return weArePastSharedHistory && peerLacksBlakeBit && peerIsNotManuallyAdded && peerIsNotShortLived; + return weArePastSharedHistory && peerLacksBlakeBit && peerIsNotManuallyAdded && peerIsNotNoBan && peerIsNotShortLived; } bool PeerManagerImpl::MaybeDiscourageAndDisconnect(CNode& pnode, Peer& peer) From 363d44334efb3e56963763b2ce4313923041167f Mon Sep 17 00:00:00 2001 From: vach Date: Sun, 13 Sep 2026 23:11:58 +0800 Subject: [PATCH 4/4] net: disconnect peers lacking NODE_BLAKE2B once the tip is at the fork height --- src/net_processing.cpp | 2 +- test/functional/p2p_blake2b_postfork_filter.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 1a1c5a384a48..1e9c1b307e9b 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -4971,7 +4971,7 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, bool PeerManagerImpl::CanDisconnectSpamCoinPeer(const CNode& node, ServiceFlags services) const { // Past the shared pre-fork history, a peer lacking NODE_BLAKE2B is no longer useful - bool weArePastSharedHistory = m_chainparams.GetConsensus().IsBlake2bHeight(m_best_height + 1); + bool weArePastSharedHistory = m_chainparams.GetConsensus().IsBlake2bHeight(m_best_height); bool peerLacksBlakeBit = !(services & NODE_BLAKE2B); bool peerIsNotManuallyAdded = !node.IsManualConn(); bool peerIsNotNoBan = !node.HasPermission(NetPermissionFlags::NoBan); diff --git a/test/functional/p2p_blake2b_postfork_filter.py b/test/functional/p2p_blake2b_postfork_filter.py index 648e194edff0..8eeb1f2c9b8b 100644 --- a/test/functional/p2p_blake2b_postfork_filter.py +++ b/test/functional/p2p_blake2b_postfork_filter.py @@ -4,9 +4,9 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test refusal of peers lacking NODE_BLAKE2B once shared pre-fork history is complete. -While the next block to connect is below the fork height, peers without -NODE_BLAKE2B are accepted. From then on they are disconnected: new ones at -their version message, connected ones once the tip crosses into fork history. +While the tip is below the fork height, peers without NODE_BLAKE2B are +accepted. From then on they are disconnected: new ones at their version +message, connected ones once the tip crosses into fork history. """ from test_framework.messages import ( @@ -58,7 +58,7 @@ def run_test(self): addr = node.get_deterministic_priv_key().address self.log.info("Below the fork height, peers lacking NODE_BLAKE2B are accepted") - self.generatetoaddress(node, FORK_HEIGHT - 2, addr) + self.generatetoaddress(node, FORK_HEIGHT - 1, addr) legacy_in = self.inbound(blake2b=False) fork_in = self.inbound(blake2b=True) legacy_out = self.outbound(0, blake2b=False) @@ -91,9 +91,9 @@ def run_test(self): self.outbound(3, blake2b=True).sync_with_ping() self.wait_until(lambda: len(node.getpeerinfo()) == 4) - self.log.info("Fork peers stay connected across the first BLAKE2b block") + self.log.info("Fork peers stay connected across the next BLAKE2b block") self.generatetoaddress(node, 1, addr) - assert_equal(node.getblockcount(), FORK_HEIGHT) + assert_equal(node.getblockcount(), FORK_HEIGHT + 1) for peer in node.p2ps: if peer.is_connected: peer.sync_with_ping()