Skip to content
Bitcoin Roots

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. Before is on the left; after is on the right. Unified view fits narrow screens more easily.

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