Skip to content

Commit 5ef5e4a

Browse files
pastapasta
authored andcommitted
fix: add missing changes from bitcoin#28629
- Add missing file: test/functional/interface_usdt_mempool.py - Add missing file: test/functional/interface_usdt_net.py - Add missing file: test/functional/interface_usdt_validation.py - Remove scope creep: duplicated pruning test functionality in interface_usdt_utxocache.py Resolves validation issues while preserving Bitcoin intent to fix USDT undeclared function errors.
1 parent 88a8d17 commit 5ef5e4a

File tree

4 files changed

+391
-96
lines changed

4 files changed

+391
-96
lines changed
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
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()

0 commit comments

Comments
 (0)