Skip to content

Commit a52837b

Browse files
committed
Merge bitcoin/bitcoin#29575: net_processing: make any misbehavior trigger immediate discouragement
6eecba4 net_processing: make MaybePunishNodeFor{Block,Tx} return void (Pieter Wuille) ae60d48 net_processing: remove Misbehavior score and increments (Pieter Wuille) 6457c31 net_processing: make all Misbehaving increments = 100 (Pieter Wuille) 5120ab1 net_processing: drop 8 headers threshold for incoming BIP130 (Pieter Wuille) 944c542 net_processing: drop Misbehavior for unconnecting headers (Pieter Wuille) 9f66ac7 net_processing: do not treat non-connecting headers as response (Pieter Wuille) Pull request description: So far, discouragement of peers triggers when their misbehavior score exceeds 100 points. Most types of misbehavior increment the score by 100, triggering immediate discouragement, but some types do not. This PR makes all increments equal to either 100 (meaning any misbehavior will immediately cause disconnection and discouragement) or 0 (making the behavior effectively unconditionally allowed), and then removes the logic for score accumulation. This simplifies the code a bit, but also makes protocol expectations clearer: if a peer misbehaves, they get disconnected. There is no good reason why certain types of protocol violations should be permitted 4 times (howmuch=20) or 9 times (howmuch=10), while many others are never allowed. Furthermore, the distinction between these looks arbitrary. The specific types of misbehavior that are changed to 100 are: * Sending us a `block` which does not connect to our header tree (which necessarily must have been unsollicited). [used to be score 10] * Sending us a `headers` with a non-continuous headers sequence. [used to be score 20] * Sending us more than 1000 addresses in a single `addr` or `addrv2` message [used to be score 20] * Sending us more than 50000 invs in a single `inv` message [used to be score 20] * Sending us more than 2000 headers in a single `headers` message [used to be score 20] The specific types of misbehavior that are changed to 0 are: * Sending us 10 (*) separate BIP130 headers announcements that do not connect to our block tree [used to be score 20] * Sending us more than 8 headers in a single `headers` message (which thus does not get treated as a BIP130 announcement) that does not connect to our block tree. [used to be score 10] I believe that none of these behaviors are unavoidable, except for the one marked (*) which can in theory happen still due to interaction between BIP130 and variations in system clocks (the max 2 hour in the future rule). This one has been removed entirely. In order to remove the impact of the bug it was designed to deal with, without relying on misbehavior, a separate improvement is included that makes `getheaders`-tracking more accurate. In another unrelated improvement, this also gets rid of the 8 header limit heuristic to determine whether an incoming non-connecting `headers` is a potential BIP130 announcement, as this rule is no longer needed to prevent spurious Misbehavior. Instead, any non-connecting `headers` is now treated as a potential announcement. ACKs for top commit: sr-gi: ACK [6eecba4](bitcoin/bitcoin@6eecba4) achow101: ACK 6eecba4 mzumsande: Code Review ACK 6eecba4 glozow: light code review / concept ACK 6eecba4 Tree-SHA512: e11e8a652c4ec048d8961086110a3594feefbb821e13f45c14ef81016377be0db44b5311751ef635d6e026def1960aff33f644e78ece11cfb54f2b7daa96f946
2 parents aa2ce2d + 6eecba4 commit a52837b

10 files changed

+100
-170
lines changed

src/banman.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class CSubNet;
3434
// disk on shutdown and reloaded on startup. Banning can be used to
3535
// prevent connections with spy nodes or other griefers.
3636
//
37-
// 2. Discouragement. If a peer misbehaves enough (see Misbehaving() in
37+
// 2. Discouragement. If a peer misbehaves (see Misbehaving() in
3838
// net_processing.cpp), we'll mark that address as discouraged. We still allow
3939
// incoming connections from them, but they're preferred for eviction when
4040
// we receive new incoming connections. We never make outgoing connections to

src/net_processing.cpp

Lines changed: 60 additions & 112 deletions
Large diffs are not rendered by default.

src/net_processing.h

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ static const uint32_t DEFAULT_MAX_ORPHAN_TRANSACTIONS{100};
2929
static const uint32_t DEFAULT_BLOCK_RECONSTRUCTION_EXTRA_TXN{100};
3030
static const bool DEFAULT_PEERBLOOMFILTERS = false;
3131
static const bool DEFAULT_PEERBLOCKFILTERS = false;
32-
/** Threshold for marking a node to be discouraged, e.g. disconnected and added to the discouragement filter. */
33-
static const int DISCOURAGEMENT_THRESHOLD{100};
3432
/** Maximum number of outstanding CMPCTBLOCK requests for the same block. */
3533
static const unsigned int MAX_CMPCTBLOCKS_INFLIGHT_PER_BLOCK = 3;
3634

@@ -108,7 +106,7 @@ class PeerManager : public CValidationInterface, public NetEventsInterface
108106
virtual void SetBestBlock(int height, std::chrono::seconds time) = 0;
109107

110108
/* Public for unit testing. */
111-
virtual void UnitTestMisbehaving(NodeId peer_id, int howmuch) = 0;
109+
virtual void UnitTestMisbehaving(NodeId peer_id) = 0;
112110

113111
/**
114112
* Evict extra outbound peers. If we think our tip may be stale, connect to an extra outbound.

src/test/denialofservice_tests.cpp

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ BOOST_AUTO_TEST_CASE(peer_discouragement)
330330
peerLogic->InitializeNode(*nodes[0], NODE_NETWORK);
331331
nodes[0]->fSuccessfullyConnected = true;
332332
connman->AddTestNode(*nodes[0]);
333-
peerLogic->UnitTestMisbehaving(nodes[0]->GetId(), DISCOURAGEMENT_THRESHOLD); // Should be discouraged
333+
peerLogic->UnitTestMisbehaving(nodes[0]->GetId()); // Should be discouraged
334334
BOOST_CHECK(peerLogic->SendMessages(nodes[0]));
335335

336336
BOOST_CHECK(banman->IsDiscouraged(addr[0]));
@@ -350,15 +350,14 @@ BOOST_AUTO_TEST_CASE(peer_discouragement)
350350
peerLogic->InitializeNode(*nodes[1], NODE_NETWORK);
351351
nodes[1]->fSuccessfullyConnected = true;
352352
connman->AddTestNode(*nodes[1]);
353-
peerLogic->UnitTestMisbehaving(nodes[1]->GetId(), DISCOURAGEMENT_THRESHOLD - 1);
354353
BOOST_CHECK(peerLogic->SendMessages(nodes[1]));
355354
// [0] is still discouraged/disconnected.
356355
BOOST_CHECK(banman->IsDiscouraged(addr[0]));
357356
BOOST_CHECK(nodes[0]->fDisconnect);
358357
// [1] is not discouraged/disconnected yet.
359358
BOOST_CHECK(!banman->IsDiscouraged(addr[1]));
360359
BOOST_CHECK(!nodes[1]->fDisconnect);
361-
peerLogic->UnitTestMisbehaving(nodes[1]->GetId(), 1); // [1] reaches discouragement threshold
360+
peerLogic->UnitTestMisbehaving(nodes[1]->GetId());
362361
BOOST_CHECK(peerLogic->SendMessages(nodes[1]));
363362
// Expect both [0] and [1] to be discouraged/disconnected now.
364363
BOOST_CHECK(banman->IsDiscouraged(addr[0]));
@@ -381,7 +380,7 @@ BOOST_AUTO_TEST_CASE(peer_discouragement)
381380
peerLogic->InitializeNode(*nodes[2], NODE_NETWORK);
382381
nodes[2]->fSuccessfullyConnected = true;
383382
connman->AddTestNode(*nodes[2]);
384-
peerLogic->UnitTestMisbehaving(nodes[2]->GetId(), DISCOURAGEMENT_THRESHOLD);
383+
peerLogic->UnitTestMisbehaving(nodes[2]->GetId());
385384
BOOST_CHECK(peerLogic->SendMessages(nodes[2]));
386385
BOOST_CHECK(banman->IsDiscouraged(addr[0]));
387386
BOOST_CHECK(banman->IsDiscouraged(addr[1]));
@@ -423,7 +422,7 @@ BOOST_AUTO_TEST_CASE(DoS_bantime)
423422
peerLogic->InitializeNode(dummyNode, NODE_NETWORK);
424423
dummyNode.fSuccessfullyConnected = true;
425424

426-
peerLogic->UnitTestMisbehaving(dummyNode.GetId(), DISCOURAGEMENT_THRESHOLD);
425+
peerLogic->UnitTestMisbehaving(dummyNode.GetId());
427426
BOOST_CHECK(peerLogic->SendMessages(&dummyNode));
428427
BOOST_CHECK(banman->IsDiscouraged(addr));
429428

test/functional/p2p_addr_relay.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ def oversized_addr_test(self):
142142

143143
msg = self.setup_addr_msg(1010)
144144
with self.nodes[0].assert_debug_log(['addr message size = 1010']):
145-
addr_source.send_and_ping(msg)
145+
addr_source.send_message(msg)
146+
addr_source.wait_for_disconnect()
146147

147148
self.nodes[0].disconnect_p2ps()
148149

test/functional/p2p_addrv2_relay.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,6 @@ def run_test(self):
8686
addr_source = self.nodes[0].add_p2p_connection(P2PInterface())
8787
msg = msg_addrv2()
8888

89-
self.log.info('Send too-large addrv2 message')
90-
msg.addrs = ADDRS * 101
91-
with self.nodes[0].assert_debug_log(['addrv2 message size = 1010']):
92-
addr_source.send_and_ping(msg)
93-
9489
self.log.info('Check that addrv2 message content is relayed and added to addrman')
9590
addr_receiver = self.nodes[0].add_p2p_connection(AddrReceiver())
9691
msg.addrs = ADDRS
@@ -106,6 +101,13 @@ def run_test(self):
106101
assert addr_receiver.addrv2_received_and_checked
107102
assert_equal(len(self.nodes[0].getnodeaddresses(count=0, network="i2p")), 0)
108103

104+
self.log.info('Send too-large addrv2 message')
105+
msg.addrs = ADDRS * 101
106+
with self.nodes[0].assert_debug_log(['addrv2 message size = 1010']):
107+
addr_source.send_message(msg)
108+
addr_source.wait_for_disconnect()
109+
110+
109111

110112
if __name__ == '__main__':
111113
AddrTest().main()

test/functional/p2p_invalid_messages.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,9 @@ def test_oversized_msg(self, msg, size):
260260
msg_type = msg.msgtype.decode('ascii')
261261
self.log.info("Test {} message of size {} is logged as misbehaving".format(msg_type, size))
262262
with self.nodes[0].assert_debug_log(['Misbehaving', '{} message size = {}'.format(msg_type, size)]):
263-
self.nodes[0].add_p2p_connection(P2PInterface()).send_and_ping(msg)
263+
conn = self.nodes[0].add_p2p_connection(P2PInterface())
264+
conn.send_message(msg)
265+
conn.wait_for_disconnect()
264266
self.nodes[0].disconnect_p2ps()
265267

266268
def test_oversized_inv_msg(self):
@@ -321,7 +323,8 @@ def test_noncontinuous_headers_msg(self):
321323
# delete arbitrary block header somewhere in the middle to break link
322324
del block_headers[random.randrange(1, len(block_headers)-1)]
323325
with self.nodes[0].assert_debug_log(expected_msgs=MISBEHAVING_NONCONTINUOUS_HEADERS_MSGS):
324-
peer.send_and_ping(msg_headers(block_headers))
326+
peer.send_message(msg_headers(block_headers))
327+
peer.wait_for_disconnect()
325328
self.nodes[0].disconnect_p2ps()
326329

327330
def test_resource_exhaustion(self):

test/functional/p2p_mutated_blocks.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,10 @@ def self_transfer_requested():
104104
block_missing_prev.hashPrevBlock = 123
105105
block_missing_prev.solve()
106106

107-
# Attacker gets a DoS score of 10, not immediately disconnected, so we do it 10 times to get to 100
108-
for _ in range(10):
109-
assert_equal(len(self.nodes[0].getpeerinfo()), 2)
110-
with self.nodes[0].assert_debug_log(expected_msgs=["AcceptBlock FAILED (prev-blk-not-found)"]):
111-
attacker.send_message(msg_block(block_missing_prev))
107+
# Check that non-connecting block causes disconnect
108+
assert_equal(len(self.nodes[0].getpeerinfo()), 2)
109+
with self.nodes[0].assert_debug_log(expected_msgs=["AcceptBlock FAILED (prev-blk-not-found)"]):
110+
attacker.send_message(msg_block(block_missing_prev))
112111
attacker.wait_for_disconnect(timeout=5)
113112

114113

test/functional/p2p_sendheaders.py

Lines changed: 13 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,13 @@
7171
Expect: no response.
7272
7373
Part 5: Test handling of headers that don't connect.
74-
a. Repeat 10 times:
74+
a. Repeat 100 times:
7575
1. Announce a header that doesn't connect.
7676
Expect: getheaders message
7777
2. Send headers chain.
7878
Expect: getdata for the missing blocks, tip update.
79-
b. Then send 9 more headers that don't connect.
79+
b. Then send 99 more headers that don't connect.
8080
Expect: getheaders message each time.
81-
c. Announce a header that does connect.
82-
Expect: no response.
83-
d. Announce 49 headers that don't connect.
84-
Expect: getheaders message each time.
85-
e. Announce one more that doesn't connect.
86-
Expect: disconnect.
8781
"""
8882
from test_framework.blocktools import create_block, create_coinbase
8983
from test_framework.messages import CInv
@@ -526,7 +520,8 @@ def test_nonnull_locators(self, test_node, inv_node):
526520
# First we test that receipt of an unconnecting header doesn't prevent
527521
# chain sync.
528522
expected_hash = tip
529-
for i in range(10):
523+
NUM_HEADERS = 100
524+
for i in range(NUM_HEADERS):
530525
self.log.debug("Part 5.{}: starting...".format(i))
531526
test_node.last_message.pop("getdata", None)
532527
blocks = []
@@ -550,41 +545,24 @@ def test_nonnull_locators(self, test_node, inv_node):
550545
blocks = []
551546
# Now we test that if we repeatedly don't send connecting headers, we
552547
# don't go into an infinite loop trying to get them to connect.
553-
MAX_NUM_UNCONNECTING_HEADERS_MSGS = 10
554-
for _ in range(MAX_NUM_UNCONNECTING_HEADERS_MSGS + 1):
548+
for _ in range(NUM_HEADERS + 1):
555549
blocks.append(create_block(tip, create_coinbase(height), block_time))
556550
blocks[-1].solve()
557551
tip = blocks[-1].sha256
558552
block_time += 1
559553
height += 1
560554

561-
for i in range(1, MAX_NUM_UNCONNECTING_HEADERS_MSGS):
562-
# Send a header that doesn't connect, check that we get a getheaders.
555+
for i in range(1, NUM_HEADERS):
556+
with p2p_lock:
557+
test_node.last_message.pop("getheaders", None)
558+
# Send an empty header as a failed response to the received getheaders
559+
# (from the previous iteration). Otherwise, the new headers will be
560+
# treated as a response instead of as an announcement.
561+
test_node.send_header_for_blocks([])
562+
# Send the actual unconnecting header, which should trigger a new getheaders.
563563
test_node.send_header_for_blocks([blocks[i]])
564564
test_node.wait_for_getheaders(block_hash=expected_hash)
565565

566-
# Next header will connect, should re-set our count:
567-
test_node.send_header_for_blocks([blocks[0]])
568-
expected_hash = blocks[0].sha256
569-
570-
# Remove the first two entries (blocks[1] would connect):
571-
blocks = blocks[2:]
572-
573-
# Now try to see how many unconnecting headers we can send
574-
# before we get disconnected. Should be 5*MAX_NUM_UNCONNECTING_HEADERS_MSGS
575-
for i in range(5 * MAX_NUM_UNCONNECTING_HEADERS_MSGS - 1):
576-
# Send a header that doesn't connect, check that we get a getheaders.
577-
test_node.send_header_for_blocks([blocks[i % len(blocks)]])
578-
test_node.wait_for_getheaders(block_hash=expected_hash)
579-
580-
# Eventually this stops working.
581-
test_node.send_header_for_blocks([blocks[-1]])
582-
583-
# Should get disconnected
584-
test_node.wait_for_disconnect()
585-
586-
self.log.info("Part 5: success!")
587-
588566
# Finally, check that the inv node never received a getdata request,
589567
# throughout the test
590568
assert "getdata" not in inv_node.last_message

test/functional/p2p_unrequested_blocks.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,11 @@ def run_test(self):
170170
tip = next_block
171171

172172
# Now send the block at height 5 and check that it wasn't accepted (missing header)
173-
test_node.send_and_ping(msg_block(all_blocks[1]))
173+
test_node.send_message(msg_block(all_blocks[1]))
174+
test_node.wait_for_disconnect()
174175
assert_raises_rpc_error(-5, "Block not found", self.nodes[0].getblock, all_blocks[1].hash)
175176
assert_raises_rpc_error(-5, "Block not found", self.nodes[0].getblockheader, all_blocks[1].hash)
177+
test_node = self.nodes[0].add_p2p_connection(P2PInterface())
176178

177179
# The block at height 5 should be accepted if we provide the missing header, though
178180
headers_message = msg_headers()

0 commit comments

Comments
 (0)