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