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