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