interface_usdt_coinselection.py
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()