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