interface_usdt_validation.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 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()
 147