Skip to content

Commit ac94de2

Browse files
committed
merge bitcoin#28287: add sendmsgtopeer rpc and a test for net-level deadlock situation
`random_bytes()` is introduced in bitcoin#25625 but the function def alone doesn't warrant a full backport, so we'll only implement the section relevant to this PR.
1 parent d1fce0b commit ac94de2

File tree

7 files changed

+127
-0
lines changed

7 files changed

+127
-0
lines changed

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
230230
{ "getnodeaddresses", 0, "count"},
231231
{ "addpeeraddress", 1, "port"},
232232
{ "addpeeraddress", 2, "tried"},
233+
{ "sendmsgtopeer", 0, "peer_id" },
233234
{ "stop", 0, "wait" },
234235
{ "verifychainlock", 2, "blockHeight" },
235236
{ "verifyislock", 3, "maxHeight" },

src/rpc/net.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,53 @@ static RPCHelpMan addpeeraddress()
10211021
};
10221022
}
10231023

1024+
static RPCHelpMan sendmsgtopeer()
1025+
{
1026+
return RPCHelpMan{
1027+
"sendmsgtopeer",
1028+
"Send a p2p message to a peer specified by id.\n"
1029+
"The message type and body must be provided, the message header will be generated.\n"
1030+
"This RPC is for testing only.",
1031+
{
1032+
{"peer_id", RPCArg::Type::NUM, RPCArg::Optional::NO, "The peer to send the message to."},
1033+
{"msg_type", RPCArg::Type::STR, RPCArg::Optional::NO, strprintf("The message type (maximum length %i)", CMessageHeader::COMMAND_SIZE)},
1034+
{"msg", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The serialized message body to send, in hex, without a message header"},
1035+
},
1036+
RPCResult{RPCResult::Type::NONE, "", ""},
1037+
RPCExamples{
1038+
HelpExampleCli("sendmsgtopeer", "0 \"addr\" \"ffffff\"") + HelpExampleRpc("sendmsgtopeer", "0 \"addr\" \"ffffff\"")},
1039+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue {
1040+
const NodeId peer_id{request.params[0].get_int()};
1041+
const std::string& msg_type{request.params[1].get_str()};
1042+
if (msg_type.size() > CMessageHeader::COMMAND_SIZE) {
1043+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Error: msg_type too long, max length is %i", CMessageHeader::COMMAND_SIZE));
1044+
}
1045+
const std::string& msg{request.params[2].get_str()};
1046+
if (!msg.empty() && !IsHex(msg)) {
1047+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Error parsing input for msg");
1048+
}
1049+
1050+
NodeContext& node = EnsureAnyNodeContext(request.context);
1051+
CConnman& connman = EnsureConnman(node);
1052+
1053+
CSerializedNetMsg msg_ser;
1054+
msg_ser.data = ParseHex(msg);
1055+
msg_ser.m_type = msg_type;
1056+
1057+
bool success = connman.ForNode(peer_id, [&](CNode* node) {
1058+
connman.PushMessage(node, std::move(msg_ser));
1059+
return true;
1060+
});
1061+
1062+
if (!success) {
1063+
throw JSONRPCError(RPC_MISC_ERROR, "Error: Could not send message to peer");
1064+
}
1065+
1066+
return NullUniValue;
1067+
},
1068+
};
1069+
}
1070+
10241071
static RPCHelpMan setmnthreadactive()
10251072
{
10261073
return RPCHelpMan{"setmnthreadactive",
@@ -1070,6 +1117,7 @@ static const CRPCCommand commands[] =
10701117

10711118
{ "hidden", &addconnection, },
10721119
{ "hidden", &addpeeraddress, },
1120+
{ "hidden", &sendmsgtopeer },
10731121
{ "hidden", &setmnthreadactive },
10741122
};
10751123
// clang-format on

src/test/fuzz/rpc.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
149149
"pruneblockchain",
150150
"reconsiderblock",
151151
"scantxoutset",
152+
"sendmsgtopeer", // when no peers are connected, no p2p message is sent
152153
"sendrawtransaction",
153154
"setmnthreadactive",
154155
"setmocktime",
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2023-present The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
import threading
7+
from test_framework.messages import MAX_PROTOCOL_MESSAGE_LENGTH
8+
from test_framework.test_framework import BitcoinTestFramework
9+
from test_framework.util import random_bytes
10+
11+
class NetDeadlockTest(BitcoinTestFramework):
12+
def set_test_params(self):
13+
self.setup_clean_chain = True
14+
self.num_nodes = 2
15+
16+
def run_test(self):
17+
node0 = self.nodes[0]
18+
node1 = self.nodes[1]
19+
20+
self.log.info("Simultaneously send a large message on both sides")
21+
rand_msg = random_bytes(MAX_PROTOCOL_MESSAGE_LENGTH).hex()
22+
23+
thread0 = threading.Thread(target=node0.sendmsgtopeer, args=(0, "unknown", rand_msg))
24+
thread1 = threading.Thread(target=node1.sendmsgtopeer, args=(0, "unknown", rand_msg))
25+
26+
thread0.start()
27+
thread1.start()
28+
thread0.join()
29+
thread1.join()
30+
31+
self.log.info("Check whether a deadlock happened")
32+
self.nodes[0].generate(1)
33+
self.sync_blocks()
34+
35+
36+
if __name__ == '__main__':
37+
NetDeadlockTest().main()

test/functional/rpc_net.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from test_framework.p2p import P2PInterface
1111
import test_framework.messages
1212
from test_framework.messages import (
13+
MAX_PROTOCOL_MESSAGE_LENGTH,
1314
NODE_NETWORK,
1415
)
1516

@@ -66,6 +67,7 @@ def run_test(self):
6667
self.test_service_flags()
6768
self.test_getnodeaddresses()
6869
self.test_addpeeraddress()
70+
self.test_sendmsgtopeer()
6971

7072
def test_connection_count(self):
7173
self.log.info("Test getconnectioncount")
@@ -341,6 +343,37 @@ def test_addpeeraddress(self):
341343
addrs = node.getnodeaddresses(count=0) # getnodeaddresses re-runs the addrman checks
342344
assert_equal(len(addrs), 2)
343345

346+
def test_sendmsgtopeer(self):
347+
node = self.nodes[0]
348+
349+
self.restart_node(0)
350+
self.connect_nodes(0, 1)
351+
352+
self.log.info("Test sendmsgtopeer")
353+
self.log.debug("Send a valid message")
354+
with self.nodes[1].assert_debug_log(expected_msgs=["received: addr"]):
355+
node.sendmsgtopeer(peer_id=0, msg_type="addr", msg="FFFFFF")
356+
357+
self.log.debug("Test error for sending to non-existing peer")
358+
assert_raises_rpc_error(-1, "Error: Could not send message to peer", node.sendmsgtopeer, peer_id=100, msg_type="addr", msg="FF")
359+
360+
self.log.debug("Test that zero-length msg_type is allowed")
361+
node.sendmsgtopeer(peer_id=0, msg_type="addr", msg="")
362+
363+
self.log.debug("Test error for msg_type that is too long")
364+
assert_raises_rpc_error(-8, "Error: msg_type too long, max length is 12", node.sendmsgtopeer, peer_id=0, msg_type="long_msg_type", msg="FF")
365+
366+
self.log.debug("Test that unknown msg_type is allowed")
367+
node.sendmsgtopeer(peer_id=0, msg_type="unknown", msg="FF")
368+
369+
self.log.debug("Test that empty msg is allowed")
370+
node.sendmsgtopeer(peer_id=0, msg_type="addr", msg="FF")
371+
372+
self.log.debug("Test that oversized messages are allowed, but get us disconnected")
373+
zero_byte_string = b'\x00' * int(MAX_PROTOCOL_MESSAGE_LENGTH + 1)
374+
node.sendmsgtopeer(peer_id=0, msg_type="addr", msg=zero_byte_string.hex())
375+
self.wait_until(lambda: len(self.nodes[0].getpeerinfo()) == 0, timeout=10)
376+
344377

345378
if __name__ == '__main__':
346379
NetTest().main()

test/functional/test_framework/util.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import json
1414
import logging
1515
import os
16+
import random
1617
import shutil
1718
import re
1819
import time
@@ -274,6 +275,11 @@ def sha256sum_file(filename):
274275
d = f.read(4096)
275276
return h.digest()
276277

278+
# TODO: Remove and use random.randbytes(n) instead, available in Python 3.9
279+
def random_bytes(n):
280+
"""Return a random bytes object of length n."""
281+
return bytes(random.getrandbits(8) for i in range(n))
282+
277283
# RPC/P2P connection constants and functions
278284
############################################
279285

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@
259259
'p2p_leak_tx.py',
260260
'p2p_eviction.py',
261261
'p2p_ibd_stalling.py',
262+
'p2p_net_deadlock.py',
262263
'rpc_signmessage.py',
263264
'rpc_generateblock.py',
264265
'rpc_generate.py',

0 commit comments

Comments
 (0)