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()