Skip to content

Commit c9273f6

Browse files
committed
Merge bitcoin#28287: rpc, test: add sendmsgtopeer rpc and a test for net-level deadlock situation
b3a93b4 test: add functional test for deadlock situation (Martin Zumsande) 3557aa4 test: add basic tests for sendmsgtopeer to rpc_net.py (Martin Zumsande) a9a1d69 rpc: add test-only sendmsgtopeer rpc (Martin Zumsande) Pull request description: This adds a `sendmsgtopeer` rpc (for testing only) that allows a node to send a message (provided in hex) to a peer. While we would usually use a `p2p` object instead of a node for this in the test framework, that isn't possible in situations where this message needs to trigger an actual interaction of multiple nodes. Use this rpc to add test coverage for the bug fixed in bitcoin#27981 (that just got merged): The test lets two nodes (almost) simultaneously send a single large (4MB) p2p message to each other, which would have caused a deadlock previously (making this test fail), but succeeds now. As can be seen from the discussion in bitcoin#27981, it was not easy to reproduce this bug without `sendmsgtopeer`. I would imagine that `sendmsgtopeer` could also be helpful in various other test constellations. ACKs for top commit: ajtowns: ACK b3a93b4 sipa: ACK b3a93b4 achow101: ACK b3a93b4 Tree-SHA512: 6e22e72402f3c4dd70cddb9e96ea988444720f7a164031df159fbdd48056c8ac77ac53def045d9208a3ca07437c7c8e34f8b4ebc7066c0a84d81cd53f2f4fa5f
2 parents 5ce200d + b3a93b4 commit c9273f6

File tree

6 files changed

+121
-0
lines changed

6 files changed

+121
-0
lines changed

src/rpc/client.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
299299
{ "getnodeaddresses", 0, "count"},
300300
{ "addpeeraddress", 1, "port"},
301301
{ "addpeeraddress", 2, "tried"},
302+
{ "sendmsgtopeer", 0, "peer_id" },
302303
{ "stop", 0, "wait" },
303304
};
304305
// clang-format on

src/rpc/net.cpp

+49
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,54 @@ static RPCHelpMan addpeeraddress()
968968
};
969969
}
970970

971+
static RPCHelpMan sendmsgtopeer()
972+
{
973+
return RPCHelpMan{
974+
"sendmsgtopeer",
975+
"Send a p2p message to a peer specified by id.\n"
976+
"The message type and body must be provided, the message header will be generated.\n"
977+
"This RPC is for testing only.",
978+
{
979+
{"peer_id", RPCArg::Type::NUM, RPCArg::Optional::NO, "The peer to send the message to."},
980+
{"msg_type", RPCArg::Type::STR, RPCArg::Optional::NO, strprintf("The message type (maximum length %i)", CMessageHeader::COMMAND_SIZE)},
981+
{"msg", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The serialized message body to send, in hex, without a message header"},
982+
},
983+
RPCResult{RPCResult::Type::OBJ, "", "", std::vector<RPCResult>{}},
984+
RPCExamples{
985+
HelpExampleCli("sendmsgtopeer", "0 \"addr\" \"ffffff\"") + HelpExampleRpc("sendmsgtopeer", "0 \"addr\" \"ffffff\"")},
986+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue {
987+
const NodeId peer_id{request.params[0].getInt<int64_t>()};
988+
const std::string& msg_type{request.params[1].get_str()};
989+
if (msg_type.size() > CMessageHeader::COMMAND_SIZE) {
990+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Error: msg_type too long, max length is %i", CMessageHeader::COMMAND_SIZE));
991+
}
992+
auto msg{TryParseHex<unsigned char>(request.params[2].get_str())};
993+
if (!msg.has_value()) {
994+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Error parsing input for msg");
995+
}
996+
997+
NodeContext& node = EnsureAnyNodeContext(request.context);
998+
CConnman& connman = EnsureConnman(node);
999+
1000+
CSerializedNetMsg msg_ser;
1001+
msg_ser.data = msg.value();
1002+
msg_ser.m_type = msg_type;
1003+
1004+
bool success = connman.ForNode(peer_id, [&](CNode* node) {
1005+
connman.PushMessage(node, std::move(msg_ser));
1006+
return true;
1007+
});
1008+
1009+
if (!success) {
1010+
throw JSONRPCError(RPC_MISC_ERROR, "Error: Could not send message to peer");
1011+
}
1012+
1013+
UniValue ret{UniValue::VOBJ};
1014+
return ret;
1015+
},
1016+
};
1017+
}
1018+
9711019
void RegisterNetRPCCommands(CRPCTable& t)
9721020
{
9731021
static const CRPCCommand commands[]{
@@ -986,6 +1034,7 @@ void RegisterNetRPCCommands(CRPCTable& t)
9861034
{"network", &getnodeaddresses},
9871035
{"hidden", &addconnection},
9881036
{"hidden", &addpeeraddress},
1037+
{"hidden", &sendmsgtopeer},
9891038
};
9901039
for (const auto& c : commands) {
9911040
t.appendCommand(c.name, &c);

src/test/fuzz/rpc.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
158158
"reconsiderblock",
159159
"scanblocks",
160160
"scantxoutset",
161+
"sendmsgtopeer", // when no peers are connected, no p2p message is sent
161162
"sendrawtransaction",
162163
"setmocktime",
163164
"setnetworkactive",

test/functional/p2p_net_deadlock.py

+37
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.test_framework import BitcoinTestFramework
8+
from test_framework.util import random_bytes
9+
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(4000000).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.generate(node0, 1)
33+
self.sync_blocks()
34+
35+
36+
if __name__ == '__main__':
37+
NetDeadlockTest().main()

test/functional/rpc_net.py

+32
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def run_test(self):
6565
self.test_service_flags()
6666
self.test_getnodeaddresses()
6767
self.test_addpeeraddress()
68+
self.test_sendmsgtopeer()
6869

6970
def test_connection_count(self):
7071
self.log.info("Test getconnectioncount")
@@ -328,6 +329,37 @@ def test_addpeeraddress(self):
328329
addrs = node.getnodeaddresses(count=0) # getnodeaddresses re-runs the addrman checks
329330
assert_equal(len(addrs), 2)
330331

332+
def test_sendmsgtopeer(self):
333+
node = self.nodes[0]
334+
335+
self.restart_node(0)
336+
self.connect_nodes(0, 1)
337+
338+
self.log.info("Test sendmsgtopeer")
339+
self.log.debug("Send a valid message")
340+
with self.nodes[1].assert_debug_log(expected_msgs=["received: addr"]):
341+
node.sendmsgtopeer(peer_id=0, msg_type="addr", msg="FFFFFF")
342+
343+
self.log.debug("Test error for sending to non-existing peer")
344+
assert_raises_rpc_error(-1, "Error: Could not send message to peer", node.sendmsgtopeer, peer_id=100, msg_type="addr", msg="FF")
345+
346+
self.log.debug("Test that zero-length msg_type is allowed")
347+
node.sendmsgtopeer(peer_id=0, msg_type="addr", msg="")
348+
349+
self.log.debug("Test error for msg_type that is too long")
350+
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")
351+
352+
self.log.debug("Test that unknown msg_type is allowed")
353+
node.sendmsgtopeer(peer_id=0, msg_type="unknown", msg="FF")
354+
355+
self.log.debug("Test that empty msg is allowed")
356+
node.sendmsgtopeer(peer_id=0, msg_type="addr", msg="FF")
357+
358+
self.log.debug("Test that oversized messages are allowed, but get us disconnected")
359+
zero_byte_string = b'\x00' * 4000001
360+
node.sendmsgtopeer(peer_id=0, msg_type="addr", msg=zero_byte_string.hex())
361+
self.wait_until(lambda: len(self.nodes[0].getpeerinfo()) == 0, timeout=10)
362+
331363

332364
if __name__ == '__main__':
333365
NetTest().main()

test/functional/test_runner.py

+1
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@
268268
'p2p_leak_tx.py',
269269
'p2p_eviction.py',
270270
'p2p_ibd_stalling.py',
271+
'p2p_net_deadlock.py',
271272
'wallet_signmessagewithaddress.py',
272273
'rpc_signmessagewithprivkey.py',
273274
'rpc_generate.py',

0 commit comments

Comments
 (0)