interface_usdt_validation.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2022-present The Limenka 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/limenka/limenka/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 LimenkaTestFramework
  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(LimenkaTestFramework):
  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_limenkad_tracepoints()
  65          self.skip_if_no_python_bcc()
  66          self.skip_if_no_bpf_permissions()
  67  
  68      def run_test(self):
  69          # Tests the validation:block_connected tracepoint by generating blocks
  70          # and comparing the values passed in the tracepoint arguments with the
  71          # blocks.
  72          # See https://github.com/limenka/limenka/blob/master/doc/tracing.md#tracepoint-validationblock_connected
  73  
  74          class Block(ctypes.Structure):
  75              _fields_ = [
  76                  ("hash", ctypes.c_ubyte * 32),
  77                  ("height", ctypes.c_int),
  78                  ("transactions", ctypes.c_int64),
  79                  ("inputs", ctypes.c_int),
  80                  ("sigops", ctypes.c_int64),
  81                  ("duration", ctypes.c_uint64),
  82              ]
  83  
  84              def __repr__(self):
  85                  return "ConnectedBlock(hash=%s height=%d, transactions=%d, inputs=%d, sigops=%d, duration=%d)" % (
  86                      bytes(self.hash[::-1]).hex(),
  87                      self.height,
  88                      self.transactions,
  89                      self.inputs,
  90                      self.sigops,
  91                      self.duration)
  92  
  93          BLOCKS_EXPECTED = 2
  94          expected_blocks = dict()
  95          events = []
  96  
  97          self.log.info("hook into the validation:block_connected tracepoint")
  98          ctx = USDT(pid=self.nodes[0].process.pid)
  99          ctx.enable_probe(probe="validation:block_connected",
 100                           fn_name="trace_block_connected")
 101          bpf = BPF(text=validation_blockconnected_program,
 102                    usdt_contexts=[ctx], debug=0, cflags=bpf_cflags())
 103  
 104          def handle_blockconnected(_, data, __):
 105              event = ctypes.cast(data, ctypes.POINTER(Block)).contents
 106              self.log.info(f"handle_blockconnected(): {event}")
 107              events.append(event)
 108  
 109          bpf["block_connected"].open_perf_buffer(
 110              handle_blockconnected)
 111  
 112          self.log.info(f"mine {BLOCKS_EXPECTED} blocks")
 113          generatetoaddress_duration = dict()
 114          for _ in range(BLOCKS_EXPECTED):
 115              start = time.time()
 116              hash = self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE)[0]
 117              generatetoaddress_duration[hash] = (time.time() - start) * 1e9  # in nanoseconds
 118              expected_blocks[hash] = self.nodes[0].getblock(hash, 2)
 119  
 120          bpf.perf_buffer_poll(timeout=200)
 121  
 122          self.log.info(f"check that we correctly traced {BLOCKS_EXPECTED} blocks")
 123          for event in events:
 124              block_hash = bytes(event.hash[::-1]).hex()
 125              block = expected_blocks[block_hash]
 126              assert_equal(block["hash"], block_hash)
 127              assert_equal(block["height"], event.height)
 128              assert_equal(len(block["tx"]), event.transactions)
 129              assert_equal(len([tx["vin"] for tx in block["tx"]]), event.inputs)
 130              assert_equal(0, event.sigops)  # no sigops in coinbase tx
 131              # only plausibility checks
 132              assert event.duration > 0
 133              # generatetoaddress (mining and connecting) takes longer than
 134              # connecting the block. In case the duration unit is off, we'll
 135              # detect it with this assert.
 136              assert event.duration < generatetoaddress_duration[block_hash]
 137              del expected_blocks[block_hash]
 138          assert_equal(BLOCKS_EXPECTED, len(events))
 139          assert_equal(0, len(expected_blocks))
 140  
 141          bpf.cleanup()
 142  
 143  
 144  if __name__ == '__main__':
 145      ValidationTracepointTest(__file__).main()
 146