interface_usdt_mempool.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 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()
351