Skip to content

Commit 6c9460e

Browse files
committed
Merge bitcoin/bitcoin#24358: test: USDT tracepoint interface tests
76c60d7 test: validation:block_connected tracepoint test (0xb10c) 260e28e test: utxocache:* tracepoint tests (0xb10c) 34b27ba test: net:in/out_message tracepoint tests (0xb10c) c934087 test: checks for tracepoint tests (0xb10c) Pull request description: This adds functional tests for the USDT tracepoints added in bitcoin/bitcoin#22006 and bitcoin/bitcoin#22902. This partially fixes #23296. The tests **are probably skipped** on most systems as these tests require: - a Linux system with a kernel that supports BPF (and available kernel headers) - that Bitcoin Core is compiled with tracepoints for USDT support (default when compiled with depends) - [bcc](https://github.com/iovisor/bcc) installed - the tests are run with a privileged user that is able to e.g. do BPF syscalls and load BPF maps The tests are not yet run in our CI as the CirrusCI containers lack the required permissions (see bitcoin/bitcoin#23296 (comment)). Running the tests in a VM in the CI could work, but I haven't experimented with this yet. The priority was to get the actual tests done first to ensure the tracepoints work as intended for the v23.0 release. Running the tracepoint tests in the CI is planned as the next step to finish #23296. The tests can, however, be run against e.g. release candidates by hand. Additionally, they provide a starting point for tests for future tracepoints. PRs adding new tracepoint should include tests. This makes reviewing these PRs easier. The tests require privileges to execute BPF sycalls (`CAP_SYS_ADMIN` before Linux kernel 5.8 and `CAP_BPF` and `CAP_PERFMON` on 5.8+) and permissions to `/sys/kernel/debug/tracing/`. It's currently recommended to run the tests in a virtual machine (or on a VPS) where it's sensible to use the `root` user to gain these privileges. Never run python scripts you haven't carefully reviewed with `root` permissions! It's unclear if a non-root user can even gain the required privileges. This needs more experimenting. The goal here is to test the tracepoint interface to make sure the [documented interface](https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#tracepoint-documentation) does not break by accident. The tracepoints expose implementation details. This means we also need to rely on implementation details of Bitcoin Core in these functional tests to trigger the tracepoints. An example is the test of the `utxocache:flush` tracepoint: On Bitcoin Core shutdown, the UTXO cache is flushed twice. The corresponding tracepoint test expects two flushes, too - if not, the test fails. Changing implementation details could cause these tests to fail and the tracepoint API to break. However, we purposefully treat the tracepoints only as [**semi-stable**](https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#semi-stable-api). The tracepoints should not block refactors or changes to other internals. ACKs for top commit: jb55: tACK 76c60d7 laanwj: Tested ACK 76c60d7 Tree-SHA512: 9a63d945c68102e59d751bd8d2805ddd7b37185408fa831d28a9cb6641b701961389b55f216c475df7d4771154e735625067ee957fc74f454ad7a7921255364c
2 parents b307279 + 76c60d7 commit 6c9460e

File tree

7 files changed

+747
-0
lines changed

7 files changed

+747
-0
lines changed

configure.ac

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,7 @@ if test "$use_usdt" != "no"; then
13811381
[AC_MSG_RESULT([no]); use_usdt=no;]
13821382
)
13831383
fi
1384+
AM_CONDITIONAL([ENABLE_USDT_TRACEPOINTS], [test "$use_usdt" = "yes"])
13841385

13851386
if test "$build_bitcoin_cli$build_bitcoin_tx$build_bitcoin_util$build_bitcoind$bitcoin_enable_qt$use_bench$use_tests" = "nonononononono"; then
13861387
use_upnp=no

test/config.ini.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ RPCAUTH=@abs_top_srcdir@/share/rpcauth/rpcauth.py
2525
@ENABLE_ZMQ_TRUE@ENABLE_ZMQ=true
2626
@ENABLE_EXTERNAL_SIGNER_TRUE@ENABLE_EXTERNAL_SIGNER=true
2727
@ENABLE_SYSCALL_SANDBOX_TRUE@ENABLE_SYSCALL_SANDBOX=true
28+
@ENABLE_USDT_TRACEPOINTS_TRUE@ENABLE_USDT_TRACEPOINTS=true

test/functional/interface_usdt_net.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2022 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+
""" Tests the net:* tracepoint API interface.
7+
See https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#context-net
8+
"""
9+
10+
import ctypes
11+
from io import BytesIO
12+
# Test will be skipped if we don't have bcc installed
13+
try:
14+
from bcc import BPF, USDT # type: ignore[import]
15+
except ImportError:
16+
pass
17+
from test_framework.messages import msg_version
18+
from test_framework.p2p import P2PInterface
19+
from test_framework.test_framework import BitcoinTestFramework
20+
from test_framework.util import assert_equal
21+
22+
# Tor v3 addresses are 62 chars + 6 chars for the port (':12345').
23+
MAX_PEER_ADDR_LENGTH = 68
24+
MAX_PEER_CONN_TYPE_LENGTH = 20
25+
MAX_MSG_TYPE_LENGTH = 20
26+
# We won't process messages larger than 150 byte in this test. For reading
27+
# larger messanges see contrib/tracing/log_raw_p2p_msgs.py
28+
MAX_MSG_DATA_LENGTH = 150
29+
30+
net_tracepoints_program = """
31+
#include <uapi/linux/ptrace.h>
32+
33+
#define MAX_PEER_ADDR_LENGTH {}
34+
#define MAX_PEER_CONN_TYPE_LENGTH {}
35+
#define MAX_MSG_TYPE_LENGTH {}
36+
#define MAX_MSG_DATA_LENGTH {}
37+
""".format(
38+
MAX_PEER_ADDR_LENGTH,
39+
MAX_PEER_CONN_TYPE_LENGTH,
40+
MAX_MSG_TYPE_LENGTH,
41+
MAX_MSG_DATA_LENGTH
42+
) + """
43+
#define MIN(a,b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a < _b ? _a : _b; })
44+
45+
struct p2p_message
46+
{
47+
u64 peer_id;
48+
char peer_addr[MAX_PEER_ADDR_LENGTH];
49+
char peer_conn_type[MAX_PEER_CONN_TYPE_LENGTH];
50+
char msg_type[MAX_MSG_TYPE_LENGTH];
51+
u64 msg_size;
52+
u8 msg[MAX_MSG_DATA_LENGTH];
53+
};
54+
55+
BPF_PERF_OUTPUT(inbound_messages);
56+
int trace_inbound_message(struct pt_regs *ctx) {
57+
struct p2p_message msg = {};
58+
bpf_usdt_readarg(1, ctx, &msg.peer_id);
59+
bpf_usdt_readarg_p(2, ctx, &msg.peer_addr, MAX_PEER_ADDR_LENGTH);
60+
bpf_usdt_readarg_p(3, ctx, &msg.peer_conn_type, MAX_PEER_CONN_TYPE_LENGTH);
61+
bpf_usdt_readarg_p(4, ctx, &msg.msg_type, MAX_MSG_TYPE_LENGTH);
62+
bpf_usdt_readarg(5, ctx, &msg.msg_size);
63+
bpf_usdt_readarg_p(6, ctx, &msg.msg, MIN(msg.msg_size, MAX_MSG_DATA_LENGTH));
64+
inbound_messages.perf_submit(ctx, &msg, sizeof(msg));
65+
return 0;
66+
}
67+
68+
BPF_PERF_OUTPUT(outbound_messages);
69+
int trace_outbound_message(struct pt_regs *ctx) {
70+
struct p2p_message msg = {};
71+
bpf_usdt_readarg(1, ctx, &msg.peer_id);
72+
bpf_usdt_readarg_p(2, ctx, &msg.peer_addr, MAX_PEER_ADDR_LENGTH);
73+
bpf_usdt_readarg_p(3, ctx, &msg.peer_conn_type, MAX_PEER_CONN_TYPE_LENGTH);
74+
bpf_usdt_readarg_p(4, ctx, &msg.msg_type, MAX_MSG_TYPE_LENGTH);
75+
bpf_usdt_readarg(5, ctx, &msg.msg_size);
76+
bpf_usdt_readarg_p(6, ctx, &msg.msg, MIN(msg.msg_size, MAX_MSG_DATA_LENGTH));
77+
outbound_messages.perf_submit(ctx, &msg, sizeof(msg));
78+
return 0;
79+
};
80+
"""
81+
82+
83+
class NetTracepointTest(BitcoinTestFramework):
84+
def set_test_params(self):
85+
self.num_nodes = 1
86+
87+
def skip_test_if_missing_module(self):
88+
self.skip_if_platform_not_linux()
89+
self.skip_if_no_bitcoind_tracepoints()
90+
self.skip_if_no_python_bcc()
91+
self.skip_if_no_bpf_permissions()
92+
93+
def run_test(self):
94+
# Tests the net:inbound_message and net:outbound_message tracepoints
95+
# See https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#context-net
96+
97+
class P2PMessage(ctypes.Structure):
98+
_fields_ = [
99+
("peer_id", ctypes.c_uint64),
100+
("peer_addr", ctypes.c_char * MAX_PEER_ADDR_LENGTH),
101+
("peer_conn_type", ctypes.c_char * MAX_PEER_CONN_TYPE_LENGTH),
102+
("msg_type", ctypes.c_char * MAX_MSG_TYPE_LENGTH),
103+
("msg_size", ctypes.c_uint64),
104+
("msg", ctypes.c_ubyte * MAX_MSG_DATA_LENGTH),
105+
]
106+
107+
def __repr__(self):
108+
return f"P2PMessage(peer={self.peer_id}, addr={self.peer_addr.decode('utf-8')}, conn_type={self.peer_conn_type.decode('utf-8')}, msg_type={self.msg_type.decode('utf-8')}, msg_size={self.msg_size})"
109+
110+
self.log.info(
111+
"hook into the net:inbound_message and net:outbound_message tracepoints")
112+
ctx = USDT(path=str(self.options.bitcoind))
113+
ctx.enable_probe(probe="net:inbound_message",
114+
fn_name="trace_inbound_message")
115+
ctx.enable_probe(probe="net:outbound_message",
116+
fn_name="trace_outbound_message")
117+
bpf = BPF(text=net_tracepoints_program, usdt_contexts=[ctx], debug=0)
118+
119+
# The handle_* function is a ctypes callback function called from C. When
120+
# we assert in the handle_* function, the AssertError doesn't propagate
121+
# back to Python. The exception is ignored. We manually count and assert
122+
# that the handle_* functions succeeded.
123+
EXPECTED_INOUTBOUND_VERSION_MSG = 1
124+
checked_inbound_version_msg = 0
125+
checked_outbound_version_msg = 0
126+
127+
def check_p2p_message(event, inbound):
128+
nonlocal checked_inbound_version_msg, checked_outbound_version_msg
129+
if event.msg_type.decode("utf-8") == "version":
130+
self.log.info(
131+
f"check_p2p_message(): {'inbound' if inbound else 'outbound'} {event}")
132+
peer = self.nodes[0].getpeerinfo()[0]
133+
msg = msg_version()
134+
msg.deserialize(BytesIO(bytes(event.msg[:event.msg_size])))
135+
assert_equal(peer["id"], event.peer_id, peer["id"])
136+
assert_equal(peer["addr"], event.peer_addr.decode("utf-8"))
137+
assert_equal(peer["connection_type"],
138+
event.peer_conn_type.decode("utf-8"))
139+
if inbound:
140+
checked_inbound_version_msg += 1
141+
else:
142+
checked_outbound_version_msg += 1
143+
144+
def handle_inbound(_, data, __):
145+
event = ctypes.cast(data, ctypes.POINTER(P2PMessage)).contents
146+
check_p2p_message(event, True)
147+
148+
def handle_outbound(_, data, __):
149+
event = ctypes.cast(data, ctypes.POINTER(P2PMessage)).contents
150+
check_p2p_message(event, False)
151+
152+
bpf["inbound_messages"].open_perf_buffer(handle_inbound)
153+
bpf["outbound_messages"].open_perf_buffer(handle_outbound)
154+
155+
self.log.info("connect a P2P test node to our bitcoind node")
156+
test_node = P2PInterface()
157+
self.nodes[0].add_p2p_connection(test_node)
158+
bpf.perf_buffer_poll(timeout=200)
159+
160+
self.log.info(
161+
"check that we got both an inbound and outbound version message")
162+
assert_equal(EXPECTED_INOUTBOUND_VERSION_MSG,
163+
checked_inbound_version_msg)
164+
assert_equal(EXPECTED_INOUTBOUND_VERSION_MSG,
165+
checked_outbound_version_msg)
166+
167+
bpf.cleanup()
168+
169+
170+
if __name__ == '__main__':
171+
NetTracepointTest().main()

0 commit comments

Comments
 (0)