v31.1-roots.1 · Commit 7: [PATCH 07/17] feat(rpc): preserve Roots operator interfaces on v31
test/functional/rpc_effective_policy.py
Before: /dev/null · After: test/functional/rpc_effective_policy.py
File metadata
new file mode 100644 index 0000000000000000000000000000000000000000..bfb7d42e8bf1fcd4824dae5a8cad1f254a1fa8f7
Download original patch · Release notes
Diff rows 1–244 of 244. Line numbers refer to the original before/after files.
+ Added− RemovedUnmarked lines are context.
Scroll the code area horizontally to read long lines. The two line-number columns refer to before and after.
test/functional/rpc_effective_policy.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Copyright (c) 2026 The Bitcoin Roots developers
|
|
3
|
+
# Distributed under the MIT software license, see the accompanying
|
|
4
|
+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
5
|
+
"""Check initialized policy reporting and representative admission parity."""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from decimal import Decimal
|
|
10
|
+
|
|
11
|
+
from test_framework.messages import COIN, CTxOut
|
|
12
|
+
from test_framework.script import CScript, OP_RETURN
|
|
13
|
+
from test_framework.test_framework import BitcoinTestFramework
|
|
14
|
+
from test_framework.util import assert_equal, assert_raises_rpc_error
|
|
15
|
+
from test_framework.wallet import MiniWallet
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_POLICY = {
|
|
19
|
+
"require_standard": True,
|
|
20
|
+
"max_script_size": 1650,
|
|
21
|
+
"bytes_per_sigop": 20,
|
|
22
|
+
"bytes_per_sigop_strict": 20,
|
|
23
|
+
"max_tx_legacy_sigops": 2500,
|
|
24
|
+
"datacarrier": True,
|
|
25
|
+
"max_datacarrier_bytes": 83,
|
|
26
|
+
"datacarrier_fullcount": True,
|
|
27
|
+
"datacarrier_weight_per_byte": 4,
|
|
28
|
+
"accept_nonstd_datacarrier": False,
|
|
29
|
+
"permit_bare_datacarrier": False,
|
|
30
|
+
"dust_relay_fee": Decimal("0.00003000"),
|
|
31
|
+
"subdust_fee_penalty": True,
|
|
32
|
+
"permit_bare_pubkey": False,
|
|
33
|
+
"permit_bare_multisig": False,
|
|
34
|
+
"reject_parasites": True,
|
|
35
|
+
"reject_tokens": False,
|
|
36
|
+
"accept_unknown_witness": True,
|
|
37
|
+
"permit_bare_anchor": True,
|
|
38
|
+
"ephemeral_anchor": True,
|
|
39
|
+
"ephemeral_dust": False,
|
|
40
|
+
"ephemeral_send": False,
|
|
41
|
+
"replacement": "always",
|
|
42
|
+
"truc": "enforce",
|
|
43
|
+
"min_relay_coinblocks": 0,
|
|
44
|
+
"min_relay_maturity": 0,
|
|
45
|
+
"spk_reuse": "allow",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class EffectivePolicyTest(BitcoinTestFramework):
|
|
50
|
+
def set_test_params(self):
|
|
51
|
+
self.num_nodes = 1
|
|
52
|
+
self.uses_wallet = None
|
|
53
|
+
self.wallet_names = []
|
|
54
|
+
|
|
55
|
+
def check_policy(self, overrides=None, *, min_relay_fee=Decimal("0.00001000"), incremental_fee=Decimal("0.00000100")):
|
|
56
|
+
expected = DEFAULT_POLICY | (overrides or {})
|
|
57
|
+
info = self.nodes[0].getmempoolinfo()
|
|
58
|
+
assert_equal(info["minrelaytxfee"], min_relay_fee)
|
|
59
|
+
assert_equal(info["incrementalrelayfee"], incremental_fee)
|
|
60
|
+
assert_equal(info["mempoolminfee"], min_relay_fee)
|
|
61
|
+
assert_equal(info["fullrbf"], expected["replacement"] == "always")
|
|
62
|
+
assert_equal(info["permitbaremultisig"], expected["permit_bare_multisig"])
|
|
63
|
+
assert_equal(info["maxdatacarriersize"], expected["max_datacarrier_bytes"])
|
|
64
|
+
policy = info["policy"]
|
|
65
|
+
# Permit future additive fields, but require every defined field and its type.
|
|
66
|
+
for key, value in expected.items():
|
|
67
|
+
assert_equal(policy[key], value)
|
|
68
|
+
assert_equal(type(policy[key]), type(value))
|
|
69
|
+
|
|
70
|
+
def run_test(self):
|
|
71
|
+
node = self.nodes[0]
|
|
72
|
+
assert_raises_rpc_error(-32601, "Method not found", node.getmempoolstats)
|
|
73
|
+
self.log.info("Checking ordinary initialized defaults")
|
|
74
|
+
self.check_policy()
|
|
75
|
+
help_text = node.help("getmempoolinfo")
|
|
76
|
+
assert "not consensus rules or mining limits" in help_text
|
|
77
|
+
for field in DEFAULT_POLICY:
|
|
78
|
+
assert f'"{field}"' in help_text
|
|
79
|
+
assert "satoshi-blocks" in help_text
|
|
80
|
+
# Exercise the CLI help entry point too, rather than only the RPC client.
|
|
81
|
+
assert_equal(node.cli.help("getmempoolinfo"), help_text.rstrip("\n"))
|
|
82
|
+
|
|
83
|
+
self.log.info("Checking initialized overrides, rounding and resolved token modes")
|
|
84
|
+
self.restart_node(0, extra_args=[
|
|
85
|
+
"-acceptnonstdtxn=1", "-maxscriptsize=2000", "-bytespersigop=25",
|
|
86
|
+
"-bytespersigopstrict=30", "-maxtxlegacysigops=3000", "-datacarriersize=100",
|
|
87
|
+
"-datacarrierfullcount=0", "-datacarriercost=0.26", "-acceptnonstddatacarrier=1",
|
|
88
|
+
"-permitbaredatacarrier=1", "-dustrelayfee=0.00004", "-subdustfeepenalty=0",
|
|
89
|
+
"-permitbarepubkey=1", "-permitbaremultisig=1", "-rejectparasites=0",
|
|
90
|
+
"-rejecttokens=1", "-acceptunknownwitness=0", "-permitbareanchor=0",
|
|
91
|
+
"-permitephemeral=dust", "-mempoolreplacement=optin", "-mempooltruc=accept",
|
|
92
|
+
"-minrelaycoinblocks=123456", "-minrelaymaturity=2", "-spkreuse=conflict",
|
|
93
|
+
"-incrementalrelayfee=0.00002",
|
|
94
|
+
])
|
|
95
|
+
self.check_policy({
|
|
96
|
+
"require_standard": False, "max_script_size": 2000, "bytes_per_sigop": 25,
|
|
97
|
+
"bytes_per_sigop_strict": 30, "max_tx_legacy_sigops": 3000,
|
|
98
|
+
"max_datacarrier_bytes": 100, "datacarrier_fullcount": False,
|
|
99
|
+
"datacarrier_weight_per_byte": 2, "accept_nonstd_datacarrier": True,
|
|
100
|
+
"permit_bare_datacarrier": True, "dust_relay_fee": Decimal("0.00004000"),
|
|
101
|
+
"subdust_fee_penalty": False, "permit_bare_pubkey": True,
|
|
102
|
+
"permit_bare_multisig": True, "reject_parasites": False, "reject_tokens": True,
|
|
103
|
+
"accept_unknown_witness": False, "permit_bare_anchor": False,
|
|
104
|
+
"ephemeral_dust": True, "ephemeral_send": True, "replacement": "optin",
|
|
105
|
+
"truc": "accept", "min_relay_coinblocks": 123456, "min_relay_maturity": 2,
|
|
106
|
+
"spk_reuse": "conflict",
|
|
107
|
+
}, min_relay_fee=Decimal("0.00002000"), incremental_fee=Decimal("0.00002000"))
|
|
108
|
+
|
|
109
|
+
self.log.info("Checking disabled controls and enabled zero-byte carrier budget")
|
|
110
|
+
self.restart_node(0, extra_args=[
|
|
111
|
+
"-datacarrier=0", "-datacarriersize=999", "-datacarriercost=0",
|
|
112
|
+
"-bytespersigop=0", "-bytespersigopstrict=0", "-maxscriptsize=0",
|
|
113
|
+
"-maxtxlegacysigops=0", "-dustrelayfee=0", "-subdustfeepenalty=0",
|
|
114
|
+
"-permitephemeral=0", "-mempoolreplacement=0", "-mempooltruc=reject",
|
|
115
|
+
])
|
|
116
|
+
self.check_policy({
|
|
117
|
+
"datacarrier": False, "max_datacarrier_bytes": 0,
|
|
118
|
+
"datacarrier_weight_per_byte": 0, "bytes_per_sigop": 0,
|
|
119
|
+
"bytes_per_sigop_strict": 0, "max_script_size": 0, "max_tx_legacy_sigops": 0,
|
|
120
|
+
"dust_relay_fee": Decimal("0.00000000"), "subdust_fee_penalty": False,
|
|
121
|
+
"ephemeral_anchor": False, "replacement": "never", "truc": "reject",
|
|
122
|
+
})
|
|
123
|
+
self.restart_node(0, extra_args=["-datacarrier=1", "-datacarriersize=0"])
|
|
124
|
+
self.check_policy({"max_datacarrier_bytes": 0})
|
|
125
|
+
|
|
126
|
+
self.log.info("Checking first config / last CLI selection and explicit fee override")
|
|
127
|
+
config_path = node.datadir_path / "bitcoin.conf"
|
|
128
|
+
base_config = config_path.read_text(encoding="utf-8")
|
|
129
|
+
self.stop_node(0)
|
|
130
|
+
config_path.write_text(base_config + "datacarriersize=90\ndatacarriersize=91\nsubdustfeepenalty=0\n", encoding="utf-8")
|
|
131
|
+
self.start_node(0)
|
|
132
|
+
self.check_policy({"max_datacarrier_bytes": 90, "subdust_fee_penalty": False})
|
|
133
|
+
self.restart_node(0, extra_args=["-datacarriersize=92", "-datacarriersize=93", "-subdustfeepenalty=1", "-incrementalrelayfee=0.00002", "-minrelaytxfee=0.00001"])
|
|
134
|
+
self.check_policy({"max_datacarrier_bytes": 93}, incremental_fee=Decimal("0.00002000"))
|
|
135
|
+
self.stop_node(0)
|
|
136
|
+
config_path.write_text(base_config, encoding="utf-8")
|
|
137
|
+
|
|
138
|
+
self.log.info("Read/write config overrides JSON and network config; CLI remains highest")
|
|
139
|
+
rw_path = node.chain_path / "bitcoin_rw.conf"
|
|
140
|
+
settings_path = node.chain_path / "settings.json"
|
|
141
|
+
settings_path.write_text(json.dumps({"datacarriersize": "94", "subdustfeepenalty": False}), encoding="utf-8")
|
|
142
|
+
config_path.write_text(base_config + "datacarriersize=95\n", encoding="utf-8")
|
|
143
|
+
rw_path.write_text("datacarriersize=96\ndatacarriersize=97\nsubdustfeepenalty=1\n", encoding="utf-8")
|
|
144
|
+
self.start_node(0)
|
|
145
|
+
self.check_policy({"max_datacarrier_bytes": 96})
|
|
146
|
+
self.restart_node(0, extra_args=["-datacarriersize=98", "-datacarriersize=99", "-subdustfeepenalty=0"])
|
|
147
|
+
self.check_policy({"max_datacarrier_bytes": 99, "subdust_fee_penalty": False})
|
|
148
|
+
alternate_path = node.chain_path / "alternate.conf"
|
|
149
|
+
alternate_path.write_text("subdustfeepenalty=0\n", encoding="utf-8")
|
|
150
|
+
self.restart_node(0, extra_args=["-confrw=alternate.conf"])
|
|
151
|
+
self.check_policy({"max_datacarrier_bytes": 94, "subdust_fee_penalty": False})
|
|
152
|
+
self.restart_node(0, extra_args=[f"-confrw={alternate_path}"])
|
|
153
|
+
self.check_policy({"max_datacarrier_bytes": 94, "subdust_fee_penalty": False})
|
|
154
|
+
self.stop_node(0)
|
|
155
|
+
alternate_path.unlink()
|
|
156
|
+
rw_path.unlink()
|
|
157
|
+
settings_path.write_text("{}", encoding="utf-8")
|
|
158
|
+
config_path.write_text(base_config, encoding="utf-8")
|
|
159
|
+
|
|
160
|
+
self.log.info("Invalid effective options prevent startup and cannot be reported")
|
|
161
|
+
node.assert_start_raises_init_error(extra_args=["-subdustfeepenalty=1junk"], expected_msg="Error: Invalid -subdustfeepenalty value: '1junk' (expected empty, 0, or 1)")
|
|
162
|
+
node.assert_start_raises_init_error(extra_args=["-datacarriercost=0.001"], expected_msg="Error: Invalid -datacarriercost value: '0.001' (expected a value from 0 to 1073741823.75 exactly representable in hundredths)")
|
|
163
|
+
self.start_node(0)
|
|
164
|
+
self.check_policy()
|
|
165
|
+
self.log.info("Dynamic dust without an estimate reports the configured floor")
|
|
166
|
+
self.restart_node(0, extra_args=["-dustrelayfee=0.00004", "-dustdynamic=mempool:1"])
|
|
167
|
+
self.check_policy({"dust_relay_fee": Decimal("0.00004000")})
|
|
168
|
+
self.restart_node(0)
|
|
169
|
+
self.test_carrier_admission()
|
|
170
|
+
self.test_subdust_admission()
|
|
171
|
+
|
|
172
|
+
def test_carrier_admission(self):
|
|
173
|
+
self.log.info("Reported carrier budget controls admission, independently of standardness")
|
|
174
|
+
node = self.nodes[0]
|
|
175
|
+
self.restart_node(0, extra_args=["-datacarriersize=5"])
|
|
176
|
+
self.check_policy({"max_datacarrier_bytes": 5})
|
|
177
|
+
wallet = MiniWallet(node)
|
|
178
|
+
budget = node.getmempoolinfo()["policy"]["max_datacarrier_bytes"]
|
|
179
|
+
|
|
180
|
+
def carrier_tx(payload_size):
|
|
181
|
+
tx = wallet.create_self_transfer(fee_rate=Decimal("0.0001"))["tx"]
|
|
182
|
+
tx.vout.append(CTxOut(0, CScript([OP_RETURN, b"x" * payload_size])))
|
|
183
|
+
wallet.sign_tx(tx)
|
|
184
|
+
return tx.serialize().hex()
|
|
185
|
+
|
|
186
|
+
# Small direct pushes have one OP_RETURN and one push opcode overhead.
|
|
187
|
+
exact_hex = carrier_tx(budget - 2)
|
|
188
|
+
over_hex = carrier_tx(budget - 1)
|
|
189
|
+
assert_equal(node.testmempoolaccept([exact_hex])[0]["allowed"], True)
|
|
190
|
+
rejection = node.testmempoolaccept([over_hex])[0]
|
|
191
|
+
assert_equal(rejection["allowed"], False)
|
|
192
|
+
assert_equal(rejection["reject-reason"], "datacarrier")
|
|
193
|
+
|
|
194
|
+
self.restart_node(0, extra_args=["-acceptnonstdtxn=1", "-datacarriersize=5"])
|
|
195
|
+
self.check_policy({"require_standard": False, "max_datacarrier_bytes": 5})
|
|
196
|
+
rejection = node.testmempoolaccept([over_hex])[0]
|
|
197
|
+
assert_equal(rejection["allowed"], False)
|
|
198
|
+
assert_equal(rejection["reject-reason"], "txn-datacarrier-exceeded")
|
|
199
|
+
|
|
200
|
+
self.restart_node(0, extra_args=["-datacarrier=0"])
|
|
201
|
+
self.check_policy({"datacarrier": False, "max_datacarrier_bytes": 0})
|
|
202
|
+
assert_equal(node.testmempoolaccept([exact_hex])[0]["allowed"], False)
|
|
203
|
+
# Local rejection does not prevent consensus-valid block acceptance.
|
|
204
|
+
self.generateblock(node, output=wallet.get_address(), transactions=[over_hex])
|
|
205
|
+
|
|
206
|
+
def test_subdust_admission(self):
|
|
207
|
+
self.log.info("Reported live dust rate and penalty flag determine the effective fee")
|
|
208
|
+
node = self.nodes[0]
|
|
209
|
+
self.restart_node(0, extra_args=["-acceptnonstdtxn=1", "-dustrelayfee=0.00006"])
|
|
210
|
+
self.check_policy({"require_standard": False, "dust_relay_fee": Decimal("0.00006000")})
|
|
211
|
+
wallet = MiniWallet(node)
|
|
212
|
+
utxo = wallet.get_utxo()
|
|
213
|
+
info = node.getmempoolinfo()
|
|
214
|
+
assert_equal(info["policy"]["subdust_fee_penalty"], True)
|
|
215
|
+
# MiniWallet's P2TR output is 43 bytes, with a 67-vbyte dust spend estimate.
|
|
216
|
+
p2tr_dust_size = 43 + 67
|
|
217
|
+
dust_threshold = int(info["policy"]["dust_relay_fee"] * COIN) * p2tr_dust_size // 1000
|
|
218
|
+
|
|
219
|
+
def dust_tx(fee):
|
|
220
|
+
tx = wallet.create_self_transfer(utxo_to_spend=utxo, fee_rate=0)["tx"]
|
|
221
|
+
tx.vout[0].nValue -= fee
|
|
222
|
+
tx.vout.append(CTxOut(0, wallet.get_output_script()))
|
|
223
|
+
wallet.sign_tx(tx)
|
|
224
|
+
return tx
|
|
225
|
+
|
|
226
|
+
min_relay_per_kvb = int(info["minrelaytxfee"] * COIN)
|
|
227
|
+
probe = dust_tx(0)
|
|
228
|
+
min_fee = (min_relay_per_kvb * probe.get_vsize() + 999) // 1000
|
|
229
|
+
low_hex = dust_tx(min_fee + dust_threshold - 1).serialize().hex()
|
|
230
|
+
high_hex = dust_tx(min_fee + dust_threshold).serialize().hex()
|
|
231
|
+
rejection = node.testmempoolaccept([low_hex])[0]
|
|
232
|
+
assert_equal(rejection["allowed"], False)
|
|
233
|
+
assert_equal(rejection["reject-reason"], "min relay fee not met")
|
|
234
|
+
assert_equal(node.testmempoolaccept([high_hex])[0]["allowed"], True)
|
|
235
|
+
|
|
236
|
+
self.restart_node(0, extra_args=["-acceptnonstdtxn=1", "-dustrelayfee=0.00006", "-subdustfeepenalty=0"])
|
|
237
|
+
self.check_policy({"require_standard": False, "dust_relay_fee": Decimal("0.00006000"), "subdust_fee_penalty": False})
|
|
238
|
+
assert_equal(node.testmempoolaccept([low_hex])[0]["allowed"], True)
|
|
239
|
+
assert_equal(node.testmempoolaccept([dust_tx(min_fee).serialize().hex()])[0]["allowed"], True)
|
|
240
|
+
assert_equal(node.testmempoolaccept([dust_tx(min_fee - 1).serialize().hex()])[0]["allowed"], False)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
if __name__ == '__main__':
|
|
244
|
+
EffectivePolicyTest(__file__).main()
|