interface_usdt_coinselection.py
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 add_options(self, parser): 101 self.add_wallet_options(parser) 102 103 def set_test_params(self): 104 self.num_nodes = 1 105 self.setup_clean_chain = True 106 107 def skip_test_if_missing_module(self): 108 self.skip_if_platform_not_linux() 109 self.skip_if_no_bitcoind_tracepoints() 110 self.skip_if_no_python_bcc() 111 self.skip_if_no_bpf_permissions() 112 self.skip_if_no_wallet() 113 114 def get_tracepoints(self, expected_types): 115 events = [] 116 try: 117 for i in range(0, len(expected_types) + 1): 118 event = self.bpf["coin_selection_events"].pop() 119 assert_equal(event.wallet_name.decode(), self.default_wallet_name) 120 assert_equal(event.type, expected_types[i]) 121 events.append(event) 122 else: 123 # If the loop exits successfully instead of throwing a KeyError, then we have had 124 # more events than expected. There should be no more than len(expected_types) events. 125 assert False 126 except KeyError: 127 assert_equal(len(events), len(expected_types)) 128 return events 129 130 131 def determine_selection_from_usdt(self, events): 132 success = None 133 use_aps = None 134 algo = None 135 waste = None 136 change_pos = None 137 138 is_aps = False 139 sc_events = [] 140 for event in events: 141 if event.type == 1: 142 if not is_aps: 143 algo = event.algo.decode() 144 waste = event.waste 145 sc_events.append(event) 146 elif event.type == 2: 147 success = event.success 148 if not is_aps: 149 change_pos = event.change_pos 150 elif event.type == 3: 151 is_aps = True 152 elif event.type == 4: 153 assert is_aps 154 if event.use_aps: 155 use_aps = True 156 assert_equal(len(sc_events), 2) 157 algo = sc_events[1].algo.decode() 158 waste = sc_events[1].waste 159 change_pos = event.change_pos 160 return success, use_aps, algo, waste, change_pos 161 162 def run_test(self): 163 self.log.info("hook into the coin_selection tracepoints") 164 ctx = USDT(pid=self.nodes[0].process.pid) 165 ctx.enable_probe(probe="coin_selection:selected_coins", fn_name="trace_selected_coins") 166 ctx.enable_probe(probe="coin_selection:normal_create_tx_internal", fn_name="trace_normal_create_tx") 167 ctx.enable_probe(probe="coin_selection:attempting_aps_create_tx", fn_name="trace_attempt_aps") 168 ctx.enable_probe(probe="coin_selection:aps_create_tx_internal", fn_name="trace_aps_create_tx") 169 self.bpf = BPF(text=coinselection_tracepoints_program, usdt_contexts=[ctx], debug=0, cflags=["-Wno-error=implicit-function-declaration"]) 170 171 self.log.info("Prepare wallets") 172 self.generate(self.nodes[0], 101) 173 wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name) 174 175 self.log.info("Sending a transaction should result in all tracepoints") 176 # We should have 5 tracepoints in the order: 177 # 1. selected_coins (type 1) 178 # 2. normal_create_tx_internal (type 2) 179 # 3. attempting_aps_create_tx (type 3) 180 # 4. selected_coins (type 1) 181 # 5. aps_create_tx_internal (type 4) 182 wallet.sendtoaddress(wallet.getnewaddress(), 10) 183 events = self.get_tracepoints([1, 2, 3, 1, 4]) 184 success, use_aps, algo, waste, change_pos = self.determine_selection_from_usdt(events) 185 assert_equal(success, True) 186 assert_greater_than(change_pos, -1) 187 188 self.log.info("Failing to fund results in 1 tracepoint") 189 # We should have 1 tracepoints in the order 190 # 1. normal_create_tx_internal (type 2) 191 assert_raises_rpc_error(-6, "Insufficient funds", wallet.sendtoaddress, wallet.getnewaddress(), 102 * 50) 192 events = self.get_tracepoints([2]) 193 success, use_aps, algo, waste, change_pos = self.determine_selection_from_usdt(events) 194 assert_equal(success, False) 195 196 self.log.info("Explicitly enabling APS results in 2 tracepoints") 197 # We should have 2 tracepoints in the order 198 # 1. selected_coins (type 1) 199 # 2. normal_create_tx_internal (type 2) 200 wallet.setwalletflag("avoid_reuse") 201 wallet.sendtoaddress(address=wallet.getnewaddress(), amount=10, avoid_reuse=True) 202 events = self.get_tracepoints([1, 2]) 203 success, use_aps, algo, waste, change_pos = self.determine_selection_from_usdt(events) 204 assert_equal(success, True) 205 assert_equal(use_aps, None) 206 207 self.log.info("Change position is -1 if no change is created with APS when APS was initially not used") 208 # We should have 2 tracepoints in the order: 209 # 1. selected_coins (type 1) 210 # 2. normal_create_tx_internal (type 2) 211 # 3. attempting_aps_create_tx (type 3) 212 # 4. selected_coins (type 1) 213 # 5. aps_create_tx_internal (type 4) 214 wallet.sendtoaddress(address=wallet.getnewaddress(), amount=wallet.getbalance(), subtractfeefromamount=True, avoid_reuse=False) 215 events = self.get_tracepoints([1, 2, 3, 1, 4]) 216 success, use_aps, algo, waste, change_pos = self.determine_selection_from_usdt(events) 217 assert_equal(success, True) 218 assert_equal(change_pos, -1) 219 220 self.log.info("Change position is -1 if no change is created normally and APS is not used") 221 # We should have 2 tracepoints in the order: 222 # 1. selected_coins (type 1) 223 # 2. normal_create_tx_internal (type 2) 224 wallet.sendtoaddress(address=wallet.getnewaddress(), amount=wallet.getbalance(), subtractfeefromamount=True) 225 events = self.get_tracepoints([1, 2]) 226 success, use_aps, algo, waste, change_pos = self.determine_selection_from_usdt(events) 227 assert_equal(success, True) 228 assert_equal(change_pos, -1) 229 230 self.bpf.cleanup() 231 232 233 if __name__ == '__main__': 234 CoinSelectionTracepointTest().main()