Skip to content

Commit 6af0137

Browse files
author
MarcoFalke
committed
Merge bitcoin#19315: [tests] Allow outbound & block-relay-only connections in functional tests.
b4dd2ef [test] Test the add_outbound_p2p_connection functionality (Amiti Uttarwar) 602e69e [test] P2PBlocksOnly - Test block-relay-only connections. (Amiti Uttarwar) 8bb6bea [test/refactor] P2PBlocksOnly - Extract transaction violation test into helper. (Amiti Uttarwar) 99791e7 [test/refactor] P2PBlocksOnly - simplify transaction creation using blocktool helper. (Amiti Uttarwar) 3997ab9 [test] Add test framework support to create outbound connections. (Amiti Uttarwar) 5bc04e8 [rpc/net] Introduce addconnection to test outbounds & blockrelay (Amiti Uttarwar) Pull request description: The existing functional test framework uses the `addnode` RPC to spin up manual connections between bitcoind nodes. This limits our ability to add integration tests for our networking code, which often executes different code paths for different connection types. **This PR enables creating `outbound` & `block-relay-only` P2P connections in the functional tests.** This allows us to increase our p2p test coverage, since we can now verify expectations around these connection types. This builds out the [prototype](bitcoin#14210 (comment)) proposed by ajtowns in bitcoin#14210. 🙌🏽 An overview of this branch: - introduces a new test-only RPC function `addconnection` which initiates opening an `outbound` or `block-relay-only` connection. (conceptually similar to `addnode` but for different connection types & restricted to regtest) - adds `test_framework` support so a mininode can open an `outbound`/`block-relay-only` connection to a `P2PInterface`/`P2PConnection`. - updates `p2p_blocksonly` tests to create a `block-relay-only` connection & verify expectations around transaction relay. - introduces `p2p_add_connections` test that checks the behaviors of the newly introduced `add_outbound_p2p_connection` test framework function. With these changes, there are many more behaviors that we can add integration tests for. The blocksonly updates is just one example. Huge props to ajtowns for conceiving the approach & providing me feedback as I've built out this branch. Also thank you to jnewbery for lots of thoughtful input along the way. ACKs for top commit: troygiorshev: reACK b4dd2ef jnewbery: utACK b4dd2ef MarcoFalke: Approach ACK b4dd2ef 🍢 Tree-SHA512: d1cba768c19c9c80e6a38b1c340cc86a90701b14772c4a0791c458f9097f6a4574b4a4acc7d13d6790c7b1f1f197e2c3d87996270f177402145f084ef8519a6b
2 parents 6d81d7a + b4dd2ef commit 6af0137

File tree

9 files changed

+368
-54
lines changed

9 files changed

+368
-54
lines changed

src/net.cpp

+21
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,27 @@ void CConnman::AcceptConnection(const ListenSocket& hListenSocket) {
11221122
RandAddEvent((uint32_t)id);
11231123
}
11241124

1125+
bool CConnman::AddConnection(const std::string& address, ConnectionType conn_type)
1126+
{
1127+
if (conn_type != ConnectionType::OUTBOUND_FULL_RELAY && conn_type != ConnectionType::BLOCK_RELAY) return false;
1128+
1129+
const int max_connections = conn_type == ConnectionType::OUTBOUND_FULL_RELAY ? m_max_outbound_full_relay : m_max_outbound_block_relay;
1130+
1131+
// Count existing connections
1132+
int existing_connections = WITH_LOCK(cs_vNodes,
1133+
return std::count_if(vNodes.begin(), vNodes.end(), [conn_type](CNode* node) { return node->m_conn_type == conn_type; }););
1134+
1135+
// Max connections of specified type already exist
1136+
if (existing_connections >= max_connections) return false;
1137+
1138+
// Max total outbound connections already exist
1139+
CSemaphoreGrant grant(*semOutbound, true);
1140+
if (!grant) return false;
1141+
1142+
OpenNetworkConnection(CAddress(), false, &grant, address.c_str(), conn_type);
1143+
return true;
1144+
}
1145+
11251146
void CConnman::DisconnectNodes()
11261147
{
11271148
{

src/net.h

+13
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,19 @@ class CConnman
938938
bool RemoveAddedNode(const std::string& node);
939939
std::vector<AddedNodeInfo> GetAddedNodeInfo();
940940

941+
/**
942+
* Attempts to open a connection. Currently only used from tests.
943+
*
944+
* @param[in] address Address of node to try connecting to
945+
* @param[in] conn_type ConnectionType::OUTBOUND or ConnectionType::BLOCK_RELAY
946+
* @return bool Returns false if there are no available
947+
* slots for this connection:
948+
* - conn_type not a supported ConnectionType
949+
* - Max total outbound connection capacity filled
950+
* - Max connection capacity for type is filled
951+
*/
952+
bool AddConnection(const std::string& address, ConnectionType conn_type);
953+
941954
size_t GetNodeCount(NumConnections num);
942955
void GetNodeStats(std::vector<CNodeStats>& vstats);
943956
bool DisconnectNode(const std::string& node);

src/rpc/net.cpp

+58
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <rpc/server.h>
66

77
#include <banman.h>
8+
#include <chainparams.h>
89
#include <clientversion.h>
910
#include <core_io.h>
1011
#include <net.h>
@@ -314,6 +315,61 @@ static RPCHelpMan addnode()
314315
};
315316
}
316317

318+
static RPCHelpMan addconnection()
319+
{
320+
return RPCHelpMan{"addconnection",
321+
"\nOpen an outbound connection to a specified node. This RPC is for testing only.\n",
322+
{
323+
{"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The IP address and port to attempt connecting to."},
324+
{"connection_type", RPCArg::Type::STR, RPCArg::Optional::NO, "Type of connection to open, either \"outbound-full-relay\" or \"block-relay-only\"."},
325+
},
326+
RPCResult{
327+
RPCResult::Type::OBJ, "", "",
328+
{
329+
{ RPCResult::Type::STR, "address", "Address of newly added connection." },
330+
{ RPCResult::Type::STR, "connection_type", "Type of connection opened." },
331+
}},
332+
RPCExamples{
333+
HelpExampleCli("addconnection", "\"192.168.0.6:8333\" \"outbound-full-relay\"")
334+
+ HelpExampleRpc("addconnection", "\"192.168.0.6:8333\" \"outbound-full-relay\"")
335+
},
336+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
337+
{
338+
if (Params().NetworkIDString() != CBaseChainParams::REGTEST) {
339+
throw std::runtime_error("addconnection is for regression testing (-regtest mode) only.");
340+
}
341+
342+
RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VSTR});
343+
const std::string address = request.params[0].get_str();
344+
const std::string conn_type_in{TrimString(request.params[1].get_str())};
345+
ConnectionType conn_type{};
346+
if (conn_type_in == "outbound-full-relay") {
347+
conn_type = ConnectionType::OUTBOUND_FULL_RELAY;
348+
} else if (conn_type_in == "block-relay-only") {
349+
conn_type = ConnectionType::BLOCK_RELAY;
350+
} else {
351+
throw JSONRPCError(RPC_INVALID_PARAMETER, self.ToString());
352+
}
353+
354+
NodeContext& node = EnsureNodeContext(request.context);
355+
if (!node.connman) {
356+
throw JSONRPCError(RPC_CLIENT_P2P_DISABLED, "Error: Peer-to-peer functionality missing or disabled.");
357+
}
358+
359+
const bool success = node.connman->AddConnection(address, conn_type);
360+
if (!success) {
361+
throw JSONRPCError(RPC_CLIENT_NODE_CAPACITY_REACHED, "Error: Already at capacity for specified connection type.");
362+
}
363+
364+
UniValue info(UniValue::VOBJ);
365+
info.pushKV("address", address);
366+
info.pushKV("connection_type", conn_type_in);
367+
368+
return info;
369+
},
370+
};
371+
}
372+
317373
static RPCHelpMan disconnectnode()
318374
{
319375
return RPCHelpMan{"disconnectnode",
@@ -900,6 +956,8 @@ static const CRPCCommand commands[] =
900956
{ "network", "clearbanned", &clearbanned, {} },
901957
{ "network", "setnetworkactive", &setnetworkactive, {"state"} },
902958
{ "network", "getnodeaddresses", &getnodeaddresses, {"count"} },
959+
960+
{ "hidden", "addconnection", &addconnection, {"address", "connection_type"} },
903961
{ "hidden", "addpeeraddress", &addpeeraddress, {"address", "port"} },
904962
};
905963
// clang-format on

src/rpc/protocol.h

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ enum RPCErrorCode
6262
RPC_CLIENT_NODE_NOT_CONNECTED = -29, //!< Node to disconnect not found in connected nodes
6363
RPC_CLIENT_INVALID_IP_OR_SUBNET = -30, //!< Invalid IP/Subnet
6464
RPC_CLIENT_P2P_DISABLED = -31, //!< No valid connection manager instance found
65+
RPC_CLIENT_NODE_CAPACITY_REACHED= -34, //!< Max number of outbound or block-relay connections already open
6566

6667
//! Chain errors
6768
RPC_CLIENT_MEMPOOL_DISABLED = -33, //!< No mempool instance found
+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2020 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+
"""Test add_outbound_p2p_connection test framework functionality"""
6+
7+
from test_framework.p2p import P2PInterface
8+
from test_framework.test_framework import BitcoinTestFramework
9+
from test_framework.util import assert_equal
10+
11+
12+
def check_node_connections(*, node, num_in, num_out):
13+
info = node.getnetworkinfo()
14+
assert_equal(info["connections_in"], num_in)
15+
assert_equal(info["connections_out"], num_out)
16+
17+
18+
class P2PAddConnections(BitcoinTestFramework):
19+
def set_test_params(self):
20+
self.setup_clean_chain = False
21+
self.num_nodes = 2
22+
23+
def setup_network(self):
24+
self.setup_nodes()
25+
# Don't connect the nodes
26+
27+
def run_test(self):
28+
self.log.info("Add 8 outbounds to node 0")
29+
for i in range(8):
30+
self.log.info(f"outbound: {i}")
31+
self.nodes[0].add_outbound_p2p_connection(P2PInterface(), p2p_idx=i, connection_type="outbound-full-relay")
32+
33+
self.log.info("Add 2 block-relay-only connections to node 0")
34+
for i in range(2):
35+
self.log.info(f"block-relay-only: {i}")
36+
# set p2p_idx based on the outbound connections already open to the
37+
# node, so add 8 to account for the previous full-relay connections
38+
self.nodes[0].add_outbound_p2p_connection(P2PInterface(), p2p_idx=i + 8, connection_type="block-relay-only")
39+
40+
self.log.info("Add 2 block-relay-only connections to node 1")
41+
for i in range(2):
42+
self.log.info(f"block-relay-only: {i}")
43+
self.nodes[1].add_outbound_p2p_connection(P2PInterface(), p2p_idx=i, connection_type="block-relay-only")
44+
45+
self.log.info("Add 5 inbound connections to node 1")
46+
for i in range(5):
47+
self.log.info(f"inbound: {i}")
48+
self.nodes[1].add_p2p_connection(P2PInterface())
49+
50+
self.log.info("Add 8 outbounds to node 1")
51+
for i in range(8):
52+
self.log.info(f"outbound: {i}")
53+
# bump p2p_idx to account for the 2 existing outbounds on node 1
54+
self.nodes[1].add_outbound_p2p_connection(P2PInterface(), p2p_idx=i + 2)
55+
56+
self.log.info("Check the connections opened as expected")
57+
check_node_connections(node=self.nodes[0], num_in=0, num_out=10)
58+
check_node_connections(node=self.nodes[1], num_in=5, num_out=10)
59+
60+
self.log.info("Disconnect p2p connections & try to re-open")
61+
self.nodes[0].disconnect_p2ps()
62+
check_node_connections(node=self.nodes[0], num_in=0, num_out=0)
63+
64+
self.log.info("Add 8 outbounds to node 0")
65+
for i in range(8):
66+
self.log.info(f"outbound: {i}")
67+
self.nodes[0].add_outbound_p2p_connection(P2PInterface(), p2p_idx=i)
68+
check_node_connections(node=self.nodes[0], num_in=0, num_out=8)
69+
70+
self.log.info("Add 2 block-relay-only connections to node 0")
71+
for i in range(2):
72+
self.log.info(f"block-relay-only: {i}")
73+
# bump p2p_idx to account for the 8 existing outbounds on node 0
74+
self.nodes[0].add_outbound_p2p_connection(P2PInterface(), p2p_idx=i + 8, connection_type="block-relay-only")
75+
check_node_connections(node=self.nodes[0], num_in=0, num_out=10)
76+
77+
self.log.info("Restart node 0 and try to reconnect to p2ps")
78+
self.restart_node(0)
79+
80+
self.log.info("Add 4 outbounds to node 0")
81+
for i in range(4):
82+
self.log.info(f"outbound: {i}")
83+
self.nodes[0].add_outbound_p2p_connection(P2PInterface(), p2p_idx=i)
84+
check_node_connections(node=self.nodes[0], num_in=0, num_out=4)
85+
86+
self.log.info("Add 2 block-relay-only connections to node 0")
87+
for i in range(2):
88+
self.log.info(f"block-relay-only: {i}")
89+
# bump p2p_idx to account for the 4 existing outbounds on node 0
90+
self.nodes[0].add_outbound_p2p_connection(P2PInterface(), p2p_idx=i + 4, connection_type="block-relay-only")
91+
check_node_connections(node=self.nodes[0], num_in=0, num_out=6)
92+
93+
check_node_connections(node=self.nodes[1], num_in=5, num_out=10)
94+
95+
96+
if __name__ == '__main__':
97+
P2PAddConnections().main()

test/functional/p2p_blocksonly.py

+67-38
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
# Copyright (c) 2019-2020 The Bitcoin Core developers
33
# Distributed under the MIT software license, see the accompanying
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5-
"""Test p2p blocksonly"""
5+
"""Test p2p blocksonly mode & block-relay-only connections."""
66

7-
from test_framework.messages import msg_tx, CTransaction, FromHex
8-
from test_framework.p2p import P2PInterface
7+
import time
8+
9+
from test_framework.blocktools import create_transaction
10+
from test_framework.messages import msg_tx
11+
from test_framework.p2p import P2PInterface, P2PTxInvStore
912
from test_framework.test_framework import BitcoinTestFramework
1013
from test_framework.util import assert_equal
1114

@@ -16,48 +19,30 @@ def set_test_params(self):
1619
self.num_nodes = 1
1720
self.extra_args = [["-blocksonly"]]
1821

22+
def skip_test_if_missing_module(self):
23+
self.skip_if_no_wallet()
24+
1925
def run_test(self):
20-
block_relay_peer = self.nodes[0].add_p2p_connection(P2PInterface())
21-
22-
self.log.info('Check that txs from p2p are rejected and result in disconnect')
23-
prevtx = self.nodes[0].getblock(self.nodes[0].getblockhash(1), 2)['tx'][0]
24-
rawtx = self.nodes[0].createrawtransaction(
25-
inputs=[{
26-
'txid': prevtx['txid'],
27-
'vout': 0
28-
}],
29-
outputs=[{
30-
self.nodes[0].get_deterministic_priv_key().address: 50 - 0.00125
31-
}],
32-
)
33-
sigtx = self.nodes[0].signrawtransactionwithkey(
34-
hexstring=rawtx,
35-
privkeys=[self.nodes[0].get_deterministic_priv_key().key],
36-
prevtxs=[{
37-
'txid': prevtx['txid'],
38-
'vout': 0,
39-
'scriptPubKey': prevtx['vout'][0]['scriptPubKey']['hex'],
40-
}],
41-
)['hex']
26+
self.blocksonly_mode_tests()
27+
self.blocks_relay_conn_tests()
28+
29+
def blocksonly_mode_tests(self):
30+
self.log.info("Tests with node running in -blocksonly mode")
4231
assert_equal(self.nodes[0].getnetworkinfo()['localrelay'], False)
43-
with self.nodes[0].assert_debug_log(['transaction sent in violation of protocol peer=0']):
44-
block_relay_peer.send_message(msg_tx(FromHex(CTransaction(), sigtx)))
45-
block_relay_peer.wait_for_disconnect()
46-
assert_equal(self.nodes[0].getmempoolinfo()['size'], 0)
4732

48-
# Remove the disconnected peer and add a new one.
49-
del self.nodes[0].p2ps[0]
50-
tx_relay_peer = self.nodes[0].add_p2p_connection(P2PInterface())
33+
self.nodes[0].add_p2p_connection(P2PInterface())
34+
tx, txid, tx_hex = self.check_p2p_tx_violation()
5135

5236
self.log.info('Check that txs from rpc are not rejected and relayed to other peers')
37+
tx_relay_peer = self.nodes[0].add_p2p_connection(P2PInterface())
5338
assert_equal(self.nodes[0].getpeerinfo()[0]['relaytxes'], True)
54-
txid = self.nodes[0].testmempoolaccept([sigtx])[0]['txid']
39+
40+
assert_equal(self.nodes[0].testmempoolaccept([tx_hex])[0]['allowed'], True)
5541
with self.nodes[0].assert_debug_log(['received getdata for: wtx {} peer=1'.format(txid)]):
56-
self.nodes[0].sendrawtransaction(sigtx)
42+
self.nodes[0].sendrawtransaction(tx_hex)
5743
tx_relay_peer.wait_for_tx(txid)
5844
assert_equal(self.nodes[0].getmempoolinfo()['size'], 1)
5945

60-
self.log.info('Check that txs from peers with relay-permission are not rejected and relayed to others')
6146
self.log.info("Restarting node 0 with relay permission and blocksonly")
6247
self.restart_node(0, ["-persistmempool=0", "[email protected]", "-blocksonly"])
6348
assert_equal(self.nodes[0].getrawmempool(), [])
@@ -67,8 +52,7 @@ def run_test(self):
6752
assert_equal(peer_1_info['permissions'], ['relay'])
6853
peer_2_info = self.nodes[0].getpeerinfo()[1]
6954
assert_equal(peer_2_info['permissions'], ['relay'])
70-
assert_equal(self.nodes[0].testmempoolaccept([sigtx])[0]['allowed'], True)
71-
txid = self.nodes[0].testmempoolaccept([sigtx])[0]['txid']
55+
assert_equal(self.nodes[0].testmempoolaccept([tx_hex])[0]['allowed'], True)
7256

7357
self.log.info('Check that the tx from first_peer with relay-permission is relayed to others (ie.second_peer)')
7458
with self.nodes[0].assert_debug_log(["received getdata"]):
@@ -78,13 +62,58 @@ def run_test(self):
7862
# But if, for some reason, first_peer decides to relay transactions to us anyway, we should relay them to
7963
# second_peer since we gave relay permission to first_peer.
8064
# See https://github.com/bitcoin/bitcoin/issues/19943 for details.
81-
first_peer.send_message(msg_tx(FromHex(CTransaction(), sigtx)))
65+
first_peer.send_message(msg_tx(tx))
8266
self.log.info('Check that the peer with relay-permission is still connected after sending the transaction')
8367
assert_equal(first_peer.is_connected, True)
8468
second_peer.wait_for_tx(txid)
8569
assert_equal(self.nodes[0].getmempoolinfo()['size'], 1)
8670
self.log.info("Relay-permission peer's transaction is accepted and relayed")
8771

72+
self.nodes[0].disconnect_p2ps()
73+
self.nodes[0].generate(1)
74+
75+
def blocks_relay_conn_tests(self):
76+
self.log.info('Tests with node in normal mode with block-relay-only connections')
77+
self.restart_node(0, ["-noblocksonly"]) # disables blocks only mode
78+
assert_equal(self.nodes[0].getnetworkinfo()['localrelay'], True)
79+
80+
# Ensure we disconnect if a block-relay-only connection sends us a transaction
81+
self.nodes[0].add_outbound_p2p_connection(P2PInterface(), p2p_idx=0, connection_type="block-relay-only")
82+
assert_equal(self.nodes[0].getpeerinfo()[0]['relaytxes'], False)
83+
_, txid, tx_hex = self.check_p2p_tx_violation(index=2)
84+
85+
self.log.info("Check that txs from RPC are not sent to blockrelay connection")
86+
conn = self.nodes[0].add_outbound_p2p_connection(P2PTxInvStore(), p2p_idx=1, connection_type="block-relay-only")
87+
88+
self.nodes[0].sendrawtransaction(tx_hex)
89+
90+
# Bump time forward to ensure nNextInvSend timer pops
91+
self.nodes[0].setmocktime(int(time.time()) + 60)
92+
93+
# Calling sync_with_ping twice requires that the node calls
94+
# `ProcessMessage` twice, and thus ensures `SendMessages` must have
95+
# been called at least once
96+
conn.sync_with_ping()
97+
conn.sync_with_ping()
98+
assert(int(txid, 16) not in conn.get_invs())
99+
100+
def check_p2p_tx_violation(self, index=1):
101+
self.log.info('Check that txs from P2P are rejected and result in disconnect')
102+
input_txid = self.nodes[0].getblock(self.nodes[0].getblockhash(index), 2)['tx'][0]['txid']
103+
tx = create_transaction(self.nodes[0], input_txid, self.nodes[0].getnewaddress(), amount=(50 - 0.001))
104+
txid = tx.rehash()
105+
tx_hex = tx.serialize().hex()
106+
107+
with self.nodes[0].assert_debug_log(['transaction sent in violation of protocol peer=0']):
108+
self.nodes[0].p2ps[0].send_message(msg_tx(tx))
109+
self.nodes[0].p2ps[0].wait_for_disconnect()
110+
assert_equal(self.nodes[0].getmempoolinfo()['size'], 0)
111+
112+
# Remove the disconnected peer
113+
del self.nodes[0].p2ps[0]
114+
115+
return tx, txid, tx_hex
116+
88117

89118
if __name__ == '__main__':
90119
P2PBlocksOnly().main()

0 commit comments

Comments
 (0)