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