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