Skip to content

Commit ab5af9c

Browse files
committed
test: Add test for coinselection tracepoints
1 parent ca02b68 commit ab5af9c

File tree

2 files changed

+209
-0
lines changed

2 files changed

+209
-0
lines changed
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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 coin_selection:* tracepoint API interface.
7+
See https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#context-coin_selection
8+
"""
9+
10+
# Test will be skipped if we don't have bcc installed
11+
try:
12+
from bcc import BPF, USDT # type: ignore[import]
13+
except ImportError:
14+
pass
15+
from test_framework.test_framework import BitcoinTestFramework
16+
from test_framework.util import (
17+
assert_equal,
18+
assert_greater_than,
19+
assert_raises_rpc_error,
20+
)
21+
22+
coinselection_tracepoints_program = """
23+
#include <uapi/linux/ptrace.h>
24+
25+
#define WALLET_NAME_LENGTH 16
26+
#define ALGO_NAME_LENGTH 16
27+
28+
struct event_data
29+
{
30+
u8 type;
31+
char wallet_name[WALLET_NAME_LENGTH];
32+
33+
// selected coins event
34+
char algo[ALGO_NAME_LENGTH];
35+
s64 target;
36+
s64 waste;
37+
s64 selected_value;
38+
39+
// create tx event
40+
bool success;
41+
s64 fee;
42+
s32 change_pos;
43+
44+
// aps create tx event
45+
bool use_aps;
46+
};
47+
48+
BPF_QUEUE(coin_selection_events, struct event_data, 1024);
49+
50+
int trace_selected_coins(struct pt_regs *ctx) {
51+
struct event_data data;
52+
__builtin_memset(&data, 0, sizeof(data));
53+
data.type = 1;
54+
bpf_usdt_readarg_p(1, ctx, &data.wallet_name, WALLET_NAME_LENGTH);
55+
bpf_usdt_readarg_p(2, ctx, &data.algo, ALGO_NAME_LENGTH);
56+
bpf_usdt_readarg(3, ctx, &data.target);
57+
bpf_usdt_readarg(4, ctx, &data.waste);
58+
bpf_usdt_readarg(5, ctx, &data.selected_value);
59+
coin_selection_events.push(&data, 0);
60+
return 0;
61+
}
62+
63+
int trace_normal_create_tx(struct pt_regs *ctx) {
64+
struct event_data data;
65+
__builtin_memset(&data, 0, sizeof(data));
66+
data.type = 2;
67+
bpf_usdt_readarg_p(1, ctx, &data.wallet_name, WALLET_NAME_LENGTH);
68+
bpf_usdt_readarg(2, ctx, &data.success);
69+
bpf_usdt_readarg(3, ctx, &data.fee);
70+
bpf_usdt_readarg(4, ctx, &data.change_pos);
71+
coin_selection_events.push(&data, 0);
72+
return 0;
73+
}
74+
75+
int trace_attempt_aps(struct pt_regs *ctx) {
76+
struct event_data data;
77+
__builtin_memset(&data, 0, sizeof(data));
78+
data.type = 3;
79+
bpf_usdt_readarg_p(1, ctx, &data.wallet_name, WALLET_NAME_LENGTH);
80+
coin_selection_events.push(&data, 0);
81+
return 0;
82+
}
83+
84+
int trace_aps_create_tx(struct pt_regs *ctx) {
85+
struct event_data data;
86+
__builtin_memset(&data, 0, sizeof(data));
87+
data.type = 4;
88+
bpf_usdt_readarg_p(1, ctx, &data.wallet_name, WALLET_NAME_LENGTH);
89+
bpf_usdt_readarg(2, ctx, &data.use_aps);
90+
bpf_usdt_readarg(3, ctx, &data.success);
91+
bpf_usdt_readarg(4, ctx, &data.fee);
92+
bpf_usdt_readarg(5, ctx, &data.change_pos);
93+
coin_selection_events.push(&data, 0);
94+
return 0;
95+
}
96+
"""
97+
98+
99+
class CoinSelectionTracepointTest(BitcoinTestFramework):
100+
def set_test_params(self):
101+
self.num_nodes = 1
102+
self.setup_clean_chain = True
103+
104+
def skip_test_if_missing_module(self):
105+
self.skip_if_platform_not_linux()
106+
self.skip_if_no_bitcoind_tracepoints()
107+
self.skip_if_no_python_bcc()
108+
self.skip_if_no_bpf_permissions()
109+
self.skip_if_no_wallet()
110+
111+
def get_tracepoints(self, expected_types):
112+
events = []
113+
try:
114+
for i in range(0, len(expected_types) + 1):
115+
event = self.bpf["coin_selection_events"].pop()
116+
assert_equal(event.wallet_name.decode(), self.default_wallet_name)
117+
assert_equal(event.type, expected_types[i])
118+
events.append(event)
119+
else:
120+
# If the loop exits successfully instead of throwing a KeyError, then we have had
121+
# more events than expected. There should be no more than len(expected_types) events.
122+
assert False
123+
except KeyError:
124+
assert_equal(len(events), len(expected_types))
125+
return events
126+
127+
128+
def determine_selection_from_usdt(self, events):
129+
success = None
130+
use_aps = None
131+
algo = None
132+
waste = None
133+
change_pos = None
134+
135+
is_aps = False
136+
sc_events = []
137+
for event in events:
138+
if event.type == 1:
139+
if not is_aps:
140+
algo = event.algo.decode()
141+
waste = event.waste
142+
sc_events.append(event)
143+
elif event.type == 2:
144+
success = event.success
145+
if not is_aps:
146+
change_pos = event.change_pos
147+
elif event.type == 3:
148+
is_aps = True
149+
elif event.type == 4:
150+
assert is_aps
151+
if event.use_aps:
152+
use_aps = True
153+
assert_equal(len(sc_events), 2)
154+
algo = sc_events[1].algo.decode()
155+
waste = sc_events[1].waste
156+
change_pos = event.change_pos
157+
return success, use_aps, algo, waste, change_pos
158+
159+
def run_test(self):
160+
self.log.info("hook into the coin_selection tracepoints")
161+
ctx = USDT(pid=self.nodes[0].process.pid)
162+
ctx.enable_probe(probe="coin_selection:selected_coins", fn_name="trace_selected_coins")
163+
ctx.enable_probe(probe="coin_selection:normal_create_tx_internal", fn_name="trace_normal_create_tx")
164+
ctx.enable_probe(probe="coin_selection:attempting_aps_create_tx", fn_name="trace_attempt_aps")
165+
ctx.enable_probe(probe="coin_selection:aps_create_tx_internal", fn_name="trace_aps_create_tx")
166+
self.bpf = BPF(text=coinselection_tracepoints_program, usdt_contexts=[ctx], debug=0)
167+
168+
self.log.info("Prepare wallets")
169+
self.generate(self.nodes[0], 101)
170+
wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
171+
172+
self.log.info("Sending a transaction should result in all tracepoints")
173+
# We should have 5 tracepoints in the order:
174+
# 1. selected_coins (type 1)
175+
# 2. normal_create_tx_internal (type 2)
176+
# 3. attempting_aps_create_tx (type 3)
177+
# 4. selected_coins (type 1)
178+
# 5. aps_create_tx_internal (type 4)
179+
wallet.sendtoaddress(wallet.getnewaddress(), 10)
180+
events = self.get_tracepoints([1, 2, 3, 1, 4])
181+
success, use_aps, algo, waste, change_pos = self.determine_selection_from_usdt(events)
182+
assert_equal(success, True)
183+
assert_greater_than(change_pos, -1)
184+
185+
self.log.info("Failing to fund results in 1 tracepoint")
186+
# We should have 1 tracepoints in the order
187+
# 1. normal_create_tx_internal (type 2)
188+
assert_raises_rpc_error(-6, "Insufficient funds", wallet.sendtoaddress, wallet.getnewaddress(), 102 * 50)
189+
events = self.get_tracepoints([2])
190+
success, use_aps, algo, waste, change_pos = self.determine_selection_from_usdt(events)
191+
assert_equal(success, False)
192+
193+
self.log.info("Explicitly enabling APS results in 2 tracepoints")
194+
# We should have 2 tracepoints in the order
195+
# 1. selected_coins (type 1)
196+
# 2. normal_create_tx_internal (type 2)
197+
wallet.setwalletflag("avoid_reuse")
198+
wallet.sendtoaddress(address=wallet.getnewaddress(), amount=10, avoid_reuse=True)
199+
events = self.get_tracepoints([1, 2])
200+
success, use_aps, algo, waste, change_pos = self.determine_selection_from_usdt(events)
201+
assert_equal(success, True)
202+
assert_equal(use_aps, None)
203+
204+
self.bpf.cleanup()
205+
206+
207+
if __name__ == '__main__':
208+
CoinSelectionTracepointTest().main()

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
'wallet_reorgsrestore.py',
169169
'interface_http.py',
170170
'interface_rpc.py',
171+
'interface_usdt_coinselection.py',
171172
'interface_usdt_net.py',
172173
'interface_usdt_utxocache.py',
173174
'interface_usdt_validation.py',

0 commit comments

Comments
 (0)