/ test / functional / interface_usdt_validation.py
interface_usdt_validation.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 validation:* tracepoint API interface.
  7      See https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#context-validation
  8  """
  9  
 10  import ctypes
 11  import time
 12  
 13  # Test will be skipped if we don't have bcc installed
 14  try:
 15      from bcc import BPF, USDT # type: ignore[import]
 16  except ImportError:
 17      pass
 18  
 19  from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE
 20  from test_framework.test_framework import BitcoinTestFramework
 21  from test_framework.util import (
 22      assert_equal,
 23      bpf_cflags,
 24  )
 25  
 26  validation_blockconnected_program = """
 27  #include <uapi/linux/ptrace.h>
 28  
 29  typedef signed long long i64;
 30  
 31  struct connected_block
 32  {
 33      char        hash[32];
 34      int         height;
 35      i64         transactions;
 36      int         inputs;
 37      i64         sigops;
 38      u64         duration;
 39  };
 40  
 41  BPF_PERF_OUTPUT(block_connected);
 42  int trace_block_connected(struct pt_regs *ctx) {
 43      struct connected_block block = {};
 44      void *phash = NULL;
 45      bpf_usdt_readarg(1, ctx, &phash);
 46      bpf_probe_read_user(&block.hash, sizeof(block.hash), phash);
 47      bpf_usdt_readarg(2, ctx, &block.height);
 48      bpf_usdt_readarg(3, ctx, &block.transactions);
 49      bpf_usdt_readarg(4, ctx, &block.inputs);
 50      bpf_usdt_readarg(5, ctx, &block.sigops);
 51      bpf_usdt_readarg(6, ctx, &block.duration);
 52      block_connected.perf_submit(ctx, &block, sizeof(block));
 53      return 0;
 54  }
 55  """
 56  
 57  
 58  class ValidationTracepointTest(BitcoinTestFramework):
 59      def set_test_params(self):
 60          self.num_nodes = 1
 61  
 62      def skip_test_if_missing_module(self):
 63          self.skip_if_platform_not_linux()
 64          self.skip_if_no_bitcoind_tracepoints()
 65          self.skip_if_no_python_bcc()
 66          self.skip_if_no_bpf_permissions()
 67          self.skip_if_running_under_valgrind()
 68  
 69      def run_test(self):
 70          # Tests the validation:block_connected tracepoint by generating blocks
 71          # and comparing the values passed in the tracepoint arguments with the
 72          # blocks.
 73          # See https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#tracepoint-validationblock_connected
 74  
 75          class Block(ctypes.Structure):
 76              _fields_ = [
 77                  ("hash", ctypes.c_ubyte * 32),
 78                  ("height", ctypes.c_int),
 79                  ("transactions", ctypes.c_int64),
 80                  ("inputs", ctypes.c_int),
 81                  ("sigops", ctypes.c_int64),
 82                  ("duration", ctypes.c_uint64),
 83              ]
 84  
 85              def __repr__(self):
 86                  return "ConnectedBlock(hash=%s height=%d, transactions=%d, inputs=%d, sigops=%d, duration=%d)" % (
 87                      bytes(self.hash[::-1]).hex(),
 88                      self.height,
 89                      self.transactions,
 90                      self.inputs,
 91                      self.sigops,
 92                      self.duration)
 93  
 94          BLOCKS_EXPECTED = 2
 95          expected_blocks = dict()
 96          events = []
 97  
 98          self.log.info("hook into the validation:block_connected tracepoint")
 99          ctx = USDT(pid=self.nodes[0].process.pid)
100          ctx.enable_probe(probe="validation:block_connected",
101                           fn_name="trace_block_connected")
102          bpf = BPF(text=validation_blockconnected_program,
103                    usdt_contexts=[ctx], debug=0, cflags=bpf_cflags())
104  
105          def handle_blockconnected(_, data, __):
106              event = ctypes.cast(data, ctypes.POINTER(Block)).contents
107              self.log.info(f"handle_blockconnected(): {event}")
108              events.append(event)
109  
110          bpf["block_connected"].open_perf_buffer(
111              handle_blockconnected)
112  
113          self.log.info(f"mine {BLOCKS_EXPECTED} blocks")
114          generatetoaddress_duration = dict()
115          for _ in range(BLOCKS_EXPECTED):
116              start = time.time()
117              hash = self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE)[0]
118              generatetoaddress_duration[hash] = (time.time() - start) * 1e9  # in nanoseconds
119              expected_blocks[hash] = self.nodes[0].getblock(hash, 2)
120  
121          bpf.perf_buffer_poll(timeout=200)
122  
123          self.log.info(f"check that we correctly traced {BLOCKS_EXPECTED} blocks")
124          for event in events:
125              block_hash = bytes(event.hash[::-1]).hex()
126              block = expected_blocks[block_hash]
127              assert_equal(block["hash"], block_hash)
128              assert_equal(block["height"], event.height)
129              assert_equal(len(block["tx"]), event.transactions)
130              assert_equal(len([tx["vin"] for tx in block["tx"]]), event.inputs)
131              assert_equal(0, event.sigops)  # no sigops in coinbase tx
132              # only plausibility checks
133              assert event.duration > 0
134              # generatetoaddress (mining and connecting) takes longer than
135              # connecting the block. In case the duration unit is off, we'll
136              # detect it with this assert.
137              assert event.duration < generatetoaddress_duration[block_hash]
138              del expected_blocks[block_hash]
139          assert_equal(BLOCKS_EXPECTED, len(events))
140          assert_equal(0, len(expected_blocks))
141  
142          bpf.cleanup()
143  
144  
145  if __name__ == '__main__':
146      ValidationTracepointTest(__file__).main()