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