/ test / functional / interface_usdt_mempool.py
interface_usdt_mempool.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 mempool:* tracepoint API interface.
  7       See https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#context-mempool
  8  """
  9  
 10  import ctypes
 11  from decimal import Decimal
 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.blocktools import COINBASE_MATURITY
 20  from test_framework.messages import COIN, DEFAULT_MEMPOOL_EXPIRY_HOURS
 21  from test_framework.p2p import P2PDataStore
 22  from test_framework.test_framework import BitcoinTestFramework
 23  from test_framework.util import (
 24      assert_equal,
 25      bpf_cflags,
 26  )
 27  from test_framework.wallet import MiniWallet
 28  
 29  MEMPOOL_TRACEPOINTS_PROGRAM = """
 30  # include <uapi/linux/ptrace.h>
 31  
 32  // The longest rejection reason is 118 chars and is generated in case of SCRIPT_ERR_EVAL_FALSE by
 33  // strprintf("block-script-verify-flag-failed (%s)", ScriptErrorString(check.GetScriptError()))
 34  #define MAX_REJECT_REASON_LENGTH        118
 35  // The longest string returned by RemovalReasonToString() is 'sizelimit'
 36  #define MAX_REMOVAL_REASON_LENGTH       9
 37  #define HASH_LENGTH                     32
 38  
 39  struct added_event
 40  {
 41    u8    hash[HASH_LENGTH];
 42    s32   vsize;
 43    s64   fee;
 44  };
 45  
 46  struct removed_event
 47  {
 48    u8    hash[HASH_LENGTH];
 49    char  reason[MAX_REMOVAL_REASON_LENGTH];
 50    s32   vsize;
 51    s64   fee;
 52    u64   entry_time;
 53  };
 54  
 55  struct rejected_event
 56  {
 57    u8    hash[HASH_LENGTH];
 58    char  reason[MAX_REJECT_REASON_LENGTH];
 59  };
 60  
 61  struct replaced_event
 62  {
 63    u8    replaced_hash[HASH_LENGTH];
 64    s32   replaced_vsize;
 65    s64   replaced_fee;
 66    u64   replaced_entry_time;
 67    u8    replacement_hash[HASH_LENGTH];
 68    s32   replacement_vsize;
 69    s64   replacement_fee;
 70    bool  replaced_by_transaction;
 71  };
 72  
 73  // BPF perf buffer to push the data to user space.
 74  BPF_PERF_OUTPUT(added_events);
 75  BPF_PERF_OUTPUT(removed_events);
 76  BPF_PERF_OUTPUT(rejected_events);
 77  BPF_PERF_OUTPUT(replaced_events);
 78  
 79  int trace_added(struct pt_regs *ctx) {
 80    struct added_event added = {};
 81    void *phash = NULL;
 82    bpf_usdt_readarg(1, ctx, &phash);
 83    bpf_probe_read_user(&added.hash, sizeof(added.hash), phash);
 84    bpf_usdt_readarg(2, ctx, &added.vsize);
 85    bpf_usdt_readarg(3, ctx, &added.fee);
 86  
 87    added_events.perf_submit(ctx, &added, sizeof(added));
 88    return 0;
 89  }
 90  
 91  int trace_removed(struct pt_regs *ctx) {
 92    struct removed_event removed = {};
 93    void *phash = NULL, *preason = NULL;
 94    bpf_usdt_readarg(1, ctx, &phash);
 95    bpf_probe_read_user(&removed.hash, sizeof(removed.hash), phash);
 96    bpf_usdt_readarg(2, ctx, &preason);
 97    bpf_probe_read_user_str(&removed.reason, sizeof(removed.reason), preason);
 98    bpf_usdt_readarg(3, ctx, &removed.vsize);
 99    bpf_usdt_readarg(4, ctx, &removed.fee);
100    bpf_usdt_readarg(5, ctx, &removed.entry_time);
101  
102    removed_events.perf_submit(ctx, &removed, sizeof(removed));
103    return 0;
104  }
105  
106  int trace_rejected(struct pt_regs *ctx) {
107    struct rejected_event rejected = {};
108    void *phash = NULL, *preason = NULL;
109    bpf_usdt_readarg(1, ctx, &phash);
110    bpf_probe_read_user(&rejected.hash, sizeof(rejected.hash), phash);
111    bpf_usdt_readarg(2, ctx, &preason);
112    bpf_probe_read_user_str(&rejected.reason, sizeof(rejected.reason), preason);
113    rejected_events.perf_submit(ctx, &rejected, sizeof(rejected));
114    return 0;
115  }
116  
117  int trace_replaced(struct pt_regs *ctx) {
118    struct replaced_event replaced = {};
119    void *preplaced_hash = NULL, *preplacement_hash = NULL;
120    bpf_usdt_readarg(1, ctx, &preplaced_hash);
121    bpf_probe_read_user(&replaced.replaced_hash, sizeof(replaced.replaced_hash), preplaced_hash);
122    bpf_usdt_readarg(2, ctx, &replaced.replaced_vsize);
123    bpf_usdt_readarg(3, ctx, &replaced.replaced_fee);
124    bpf_usdt_readarg(4, ctx, &replaced.replaced_entry_time);
125    bpf_usdt_readarg(5, ctx, &preplacement_hash);
126    bpf_probe_read_user(&replaced.replacement_hash, sizeof(replaced.replacement_hash), preplacement_hash);
127    bpf_usdt_readarg(6, ctx, &replaced.replacement_vsize);
128    bpf_usdt_readarg(7, ctx, &replaced.replacement_fee);
129    bpf_usdt_readarg(8, ctx, &replaced.replaced_by_transaction);
130  
131    replaced_events.perf_submit(ctx, &replaced, sizeof(replaced));
132    return 0;
133  }
134  
135  """
136  
137  
138  class MempoolReplaced(ctypes.Structure):
139      _fields_ = [
140          ("replaced_hash", ctypes.c_ubyte * 32),
141          ("replaced_vsize", ctypes.c_int32),
142          ("replaced_fee", ctypes.c_int64),
143          ("replaced_entry_time", ctypes.c_uint64),
144          ("replacement_hash", ctypes.c_ubyte * 32),
145          ("replacement_vsize", ctypes.c_int32),
146          ("replacement_fee", ctypes.c_int64),
147          ("replaced_by_transaction", ctypes.c_bool),
148      ]
149  
150  
151  class MempoolTracepointTest(BitcoinTestFramework):
152      def set_test_params(self):
153          self.num_nodes = 1
154          self.setup_clean_chain = True
155  
156      def skip_test_if_missing_module(self):
157          self.skip_if_platform_not_linux()
158          self.skip_if_no_bitcoind_tracepoints()
159          self.skip_if_no_python_bcc()
160          self.skip_if_no_bpf_permissions()
161          self.skip_if_running_under_valgrind()
162  
163      def added_test(self):
164          """Add a transaction to the mempool and make sure the tracepoint returns
165          the expected txid, vsize, and fee."""
166  
167          events = []
168  
169          self.log.info("Hooking into mempool:added tracepoint...")
170          node = self.nodes[0]
171          ctx = USDT(pid=node.process.pid)
172          ctx.enable_probe(probe="mempool:added", fn_name="trace_added")
173          bpf = BPF(text=MEMPOOL_TRACEPOINTS_PROGRAM, usdt_contexts=[ctx], debug=0, cflags=bpf_cflags())
174  
175          def handle_added_event(_, data, __):
176              events.append(bpf["added_events"].event(data))
177  
178          bpf["added_events"].open_perf_buffer(handle_added_event)
179  
180          self.log.info("Sending transaction...")
181          fee = Decimal(31200)
182          tx = self.wallet.send_self_transfer(from_node=node, fee=fee / COIN)
183  
184          self.log.info("Polling buffer...")
185          bpf.perf_buffer_poll(timeout=200)
186  
187          self.log.info("Cleaning up mempool...")
188          self.generate(node, 1)
189  
190          self.log.info("Ensuring mempool:added event was handled successfully...")
191          assert_equal(1, len(events))
192          event = events[0]
193          assert_equal(bytes(event.hash)[::-1].hex(), tx["txid"])
194          assert_equal(event.vsize, tx["tx"].get_vsize())
195          assert_equal(event.fee, fee)
196  
197          bpf.cleanup()
198          self.generate(self.wallet, 1)
199  
200      def removed_test(self):
201          """Expire a transaction from the mempool and make sure the tracepoint returns
202          the expected txid, expiry reason, vsize, and fee."""
203  
204          events = []
205  
206          self.log.info("Hooking into mempool:removed tracepoint...")
207          node = self.nodes[0]
208          ctx = USDT(pid=node.process.pid)
209          ctx.enable_probe(probe="mempool:removed", fn_name="trace_removed")
210          bpf = BPF(text=MEMPOOL_TRACEPOINTS_PROGRAM, usdt_contexts=[ctx], debug=0, cflags=bpf_cflags())
211  
212          def handle_removed_event(_, data, __):
213              events.append(bpf["removed_events"].event(data))
214  
215          bpf["removed_events"].open_perf_buffer(handle_removed_event)
216  
217          self.log.info("Sending transaction...")
218          fee = Decimal(31200)
219          tx = self.wallet.send_self_transfer(from_node=node, fee=fee / COIN)
220          txid = tx["txid"]
221  
222          self.log.info("Fast-forwarding time to mempool expiry...")
223          entry_time = node.getmempoolentry(txid)["time"]
224          expiry_time = entry_time + 60 * 60 * DEFAULT_MEMPOOL_EXPIRY_HOURS + 5
225          node.setmocktime(expiry_time)
226  
227          self.log.info("Triggering expiry...")
228          self.wallet.get_utxo(txid=txid)
229          self.wallet.send_self_transfer(from_node=node)
230  
231          self.log.info("Polling buffer...")
232          bpf.perf_buffer_poll(timeout=200)
233  
234          self.log.info("Ensuring mempool:removed event was handled successfully...")
235          assert_equal(1, len(events))
236          event = events[0]
237          assert_equal(bytes(event.hash)[::-1].hex(), txid)
238          assert_equal(event.reason.decode("UTF-8"), "expiry")
239          assert_equal(event.vsize, tx["tx"].get_vsize())
240          assert_equal(event.fee, fee)
241          assert_equal(event.entry_time, entry_time)
242  
243          bpf.cleanup()
244          self.generate(self.wallet, 1)
245  
246      def replaced_test(self):
247          """Replace one and two transactions in the mempool and make sure the tracepoint
248          returns the expected txids, vsizes, and fees."""
249  
250          events = []
251  
252          self.log.info("Hooking into mempool:replaced tracepoint...")
253          node = self.nodes[0]
254          ctx = USDT(pid=node.process.pid)
255          ctx.enable_probe(probe="mempool:replaced", fn_name="trace_replaced")
256          bpf = BPF(text=MEMPOOL_TRACEPOINTS_PROGRAM, usdt_contexts=[ctx], debug=0, cflags=bpf_cflags())
257  
258          def handle_replaced_event(_, data, __):
259              event = ctypes.cast(data, ctypes.POINTER(MempoolReplaced)).contents
260              events.append(event)
261  
262          bpf["replaced_events"].open_perf_buffer(handle_replaced_event)
263  
264          self.log.info("Sending RBF transaction...")
265          utxo = self.wallet.get_utxo(mark_as_spent=True)
266          original_fee = Decimal(40000)
267          original_tx = self.wallet.send_self_transfer(
268              from_node=node, utxo_to_spend=utxo, fee=original_fee / COIN
269          )
270          entry_time = node.getmempoolentry(original_tx["txid"])["time"]
271  
272          self.log.info("Sending replacement transaction...")
273          replacement_fee = Decimal(45000)
274          replacement_tx = self.wallet.send_self_transfer(
275              from_node=node, utxo_to_spend=utxo, fee=replacement_fee / COIN
276          )
277  
278          self.log.info("Polling buffer...")
279          bpf.perf_buffer_poll(timeout=200)
280  
281          self.log.info("Ensuring mempool:replaced event was handled successfully...")
282          assert_equal(1, len(events))
283          event = events[0]
284          assert_equal(bytes(event.replaced_hash)[::-1].hex(), original_tx["txid"])
285          assert_equal(event.replaced_vsize, original_tx["tx"].get_vsize())
286          assert_equal(event.replaced_fee, original_fee)
287          assert_equal(event.replaced_entry_time, entry_time)
288          assert_equal(bytes(event.replacement_hash)[::-1].hex(), replacement_tx["txid"])
289          assert_equal(event.replacement_vsize, replacement_tx["tx"].get_vsize())
290          assert_equal(event.replacement_fee, replacement_fee)
291          assert_equal(event.replaced_by_transaction, True)
292  
293          bpf.cleanup()
294          self.generate(self.wallet, 1)
295  
296      def rejected_test(self):
297          """Create an invalid transaction and make sure the tracepoint returns
298          the expected txid, rejection reason, peer id, and peer address."""
299  
300          events = []
301  
302          self.log.info("Adding P2P connection...")
303          node = self.nodes[0]
304          node.add_p2p_connection(P2PDataStore())
305  
306          self.log.info("Hooking into mempool:rejected tracepoint...")
307          ctx = USDT(pid=node.process.pid)
308          ctx.enable_probe(probe="mempool:rejected", fn_name="trace_rejected")
309          bpf = BPF(text=MEMPOOL_TRACEPOINTS_PROGRAM, usdt_contexts=[ctx], debug=0, cflags=bpf_cflags())
310  
311          def handle_rejected_event(_, data, __):
312              events.append(bpf["rejected_events"].event(data))
313  
314          bpf["rejected_events"].open_perf_buffer(handle_rejected_event)
315  
316          self.log.info("Sending invalid transaction...")
317          tx = self.wallet.create_self_transfer(fee_rate=Decimal(0))
318          node.p2ps[0].send_txs_and_test([tx["tx"]], node, success=False)
319  
320          self.log.info("Polling buffer...")
321          bpf.perf_buffer_poll(timeout=200)
322  
323          self.log.info("Ensuring mempool:rejected event was handled successfully...")
324          assert_equal(1, len(events))
325          event = events[0]
326          assert_equal(bytes(event.hash)[::-1].hex(), tx["tx"].txid_hex)
327          assert_equal(event.reason.decode("UTF-8"), "min relay fee not met")
328  
329          bpf.cleanup()
330          self.generate(self.wallet, 1)
331  
332      def run_test(self):
333          """Tests the mempool:added, mempool:removed, mempool:replaced,
334          and mempool:rejected tracepoints."""
335  
336          # Create some coinbase transactions and mature them so they can be spent
337          node = self.nodes[0]
338          self.wallet = MiniWallet(node)
339          self.generate(self.wallet, 4)
340          self.generate(node, COINBASE_MATURITY)
341  
342          # Test individual tracepoints
343          self.added_test()
344          self.removed_test()
345          self.replaced_test()
346          self.rejected_test()
347  
348  
349  if __name__ == "__main__":
350      MempoolTracepointTest(__file__).main()