v31.1-roots.1 · Commit 3: [PATCH 03/17] feat(policy): preserve cluster accounting and repair dynamic dust
test/functional/mempool_subdust_fee_penalty.py
Before: /dev/null · After: test/functional/mempool_subdust_fee_penalty.py
File metadata
new file mode 100644 index 0000000000000000000000000000000000000000..5af44872f9dab968de0ab3874808c16275bff870
Download original patch · Release notes
Diff rows 1–365 of 365. 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/mempool_subdust_fee_penalty.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
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
|
+
"""Test sub-dust output fee penalty (-subdustfeepenalty)."""
|
|
6
|
+
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from math import ceil
|
|
9
|
+
|
|
10
|
+
from test_framework.messages import (
|
|
11
|
+
COIN,
|
|
12
|
+
COutPoint,
|
|
13
|
+
CTransaction,
|
|
14
|
+
CTxIn,
|
|
15
|
+
CTxOut,
|
|
16
|
+
)
|
|
17
|
+
from test_framework.script import (
|
|
18
|
+
CScript,
|
|
19
|
+
OP_RETURN,
|
|
20
|
+
)
|
|
21
|
+
from test_framework.script_util import PAY_TO_ANCHOR
|
|
22
|
+
from test_framework.test_framework import BitcoinTestFramework
|
|
23
|
+
from test_framework.util import assert_equal
|
|
24
|
+
from test_framework.wallet import MiniWallet
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DUST_THRESHOLD = 330 # P2TR: (43 + 67) * 3000 / 1000 = 330 sats
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SubDustFeePenaltyTest(BitcoinTestFramework):
|
|
31
|
+
def set_test_params(self):
|
|
32
|
+
self.num_nodes = 1
|
|
33
|
+
self.extra_args = [["-acceptnonstdtxn=1", "-subdustfeepenalty=1"]]
|
|
34
|
+
|
|
35
|
+
def run_test(self):
|
|
36
|
+
self.wallet = MiniWallet(self.nodes[0])
|
|
37
|
+
|
|
38
|
+
info = self.nodes[0].getmempoolinfo()
|
|
39
|
+
self.minrelay_per_kvb = info['minrelaytxfee'] * COIN # sats per kvB
|
|
40
|
+
|
|
41
|
+
self.test_dust_output_increases_required_fee()
|
|
42
|
+
self.test_partial_dust_proportional_penalty()
|
|
43
|
+
self.test_multiple_dust_outputs_stack()
|
|
44
|
+
self.test_penalty_disabled()
|
|
45
|
+
self.test_op_return_not_penalized()
|
|
46
|
+
self.test_above_dust_not_penalized()
|
|
47
|
+
self.test_threshold_and_low_rate_boundaries()
|
|
48
|
+
self.test_anchor_and_standardness()
|
|
49
|
+
self.test_carrier_cost_interaction()
|
|
50
|
+
self.test_fee_delta_and_replacement()
|
|
51
|
+
self.test_package_and_truc()
|
|
52
|
+
self.test_mining_and_block_acceptance()
|
|
53
|
+
|
|
54
|
+
def build_tx_with_dust(self, dust_values, fee, *, utxo=None, dust_script=None, version=2, carrier=None):
|
|
55
|
+
utxo = utxo or self.wallet.get_utxo()
|
|
56
|
+
tx = CTransaction()
|
|
57
|
+
tx.version = version
|
|
58
|
+
tx.vin = [CTxIn(COutPoint(int(utxo["txid"], 16), utxo["vout"]))]
|
|
59
|
+
input_value = int(utxo["value"] * COIN)
|
|
60
|
+
change = input_value - sum(dust_values) - fee
|
|
61
|
+
assert change > 0, f"Not enough funds: input={input_value}, dust={dust_values}, fee={fee}"
|
|
62
|
+
tx.vout = [CTxOut(v, dust_script or self.wallet.get_output_script()) for v in dust_values]
|
|
63
|
+
if carrier is not None:
|
|
64
|
+
tx.vout.append(CTxOut(0, CScript([OP_RETURN, carrier])))
|
|
65
|
+
tx.vout.append(CTxOut(change, self.wallet.get_output_script()))
|
|
66
|
+
self.wallet.sign_tx(tx)
|
|
67
|
+
return tx
|
|
68
|
+
|
|
69
|
+
def get_min_relay_fee(self, tx):
|
|
70
|
+
"""Calculate the minimum relay fee for a transaction."""
|
|
71
|
+
decoded = self.nodes[0].decoderawtransaction(tx.serialize().hex())
|
|
72
|
+
return int(ceil(self.minrelay_per_kvb * decoded['vsize'] / 1000))
|
|
73
|
+
|
|
74
|
+
def test_dust_output_increases_required_fee(self):
|
|
75
|
+
self.log.info("Test: sub-dust output penalizes effective fee")
|
|
76
|
+
|
|
77
|
+
# Build a probe tx to determine the exact min relay fee
|
|
78
|
+
probe = self.build_tx_with_dust([0], fee=1000)
|
|
79
|
+
min_fee = self.get_min_relay_fee(probe)
|
|
80
|
+
|
|
81
|
+
# fee just covers min relay + penalty - 1: rejected
|
|
82
|
+
reject_fee = DUST_THRESHOLD + min_fee - 1
|
|
83
|
+
tx_low = self.build_tx_with_dust([0], fee=reject_fee)
|
|
84
|
+
result = self.nodes[0].testmempoolaccept([tx_low.serialize().hex()])
|
|
85
|
+
assert_equal(result[0]["allowed"], False)
|
|
86
|
+
assert_equal(result[0]["reject-reason"], "min relay fee not met")
|
|
87
|
+
|
|
88
|
+
# fee covers min relay + penalty: accepted
|
|
89
|
+
accept_fee = DUST_THRESHOLD + min_fee
|
|
90
|
+
tx_high = self.build_tx_with_dust([0], fee=accept_fee)
|
|
91
|
+
result = self.nodes[0].testmempoolaccept([tx_high.serialize().hex()])
|
|
92
|
+
assert_equal(result[0]["allowed"], True)
|
|
93
|
+
|
|
94
|
+
def test_partial_dust_proportional_penalty(self):
|
|
95
|
+
self.log.info("Test: partial dust value gets proportional penalty")
|
|
96
|
+
|
|
97
|
+
partial_value = 100
|
|
98
|
+
penalty = DUST_THRESHOLD - partial_value # 230 sats
|
|
99
|
+
|
|
100
|
+
probe = self.build_tx_with_dust([partial_value], fee=1000)
|
|
101
|
+
min_fee = self.get_min_relay_fee(probe)
|
|
102
|
+
|
|
103
|
+
reject_fee = penalty + min_fee - 1
|
|
104
|
+
tx_low = self.build_tx_with_dust([partial_value], fee=reject_fee)
|
|
105
|
+
result = self.nodes[0].testmempoolaccept([tx_low.serialize().hex()])
|
|
106
|
+
assert_equal(result[0]["allowed"], False)
|
|
107
|
+
assert_equal(result[0]["reject-reason"], "min relay fee not met")
|
|
108
|
+
|
|
109
|
+
accept_fee = penalty + min_fee
|
|
110
|
+
tx_high = self.build_tx_with_dust([partial_value], fee=accept_fee)
|
|
111
|
+
result = self.nodes[0].testmempoolaccept([tx_high.serialize().hex()])
|
|
112
|
+
assert_equal(result[0]["allowed"], True)
|
|
113
|
+
|
|
114
|
+
def test_multiple_dust_outputs_stack(self):
|
|
115
|
+
self.log.info("Test: multiple dust outputs stack penalties")
|
|
116
|
+
|
|
117
|
+
total_penalty = DUST_THRESHOLD * 2 # 660 sats
|
|
118
|
+
|
|
119
|
+
probe = self.build_tx_with_dust([0, 0], fee=1000)
|
|
120
|
+
min_fee = self.get_min_relay_fee(probe)
|
|
121
|
+
|
|
122
|
+
reject_fee = total_penalty + min_fee - 1
|
|
123
|
+
tx_low = self.build_tx_with_dust([0, 0], fee=reject_fee)
|
|
124
|
+
result = self.nodes[0].testmempoolaccept([tx_low.serialize().hex()])
|
|
125
|
+
assert_equal(result[0]["allowed"], False)
|
|
126
|
+
assert_equal(result[0]["reject-reason"], "min relay fee not met")
|
|
127
|
+
|
|
128
|
+
accept_fee = total_penalty + min_fee
|
|
129
|
+
tx_high = self.build_tx_with_dust([0, 0], fee=accept_fee)
|
|
130
|
+
result = self.nodes[0].testmempoolaccept([tx_high.serialize().hex()])
|
|
131
|
+
assert_equal(result[0]["allowed"], True)
|
|
132
|
+
|
|
133
|
+
def test_penalty_disabled(self):
|
|
134
|
+
self.log.info("Test: penalty disabled with -subdustfeepenalty=0")
|
|
135
|
+
self.restart_node(0, extra_args=["-acceptnonstdtxn=1", "-subdustfeepenalty=0"])
|
|
136
|
+
self.wallet.rescan_utxos()
|
|
137
|
+
|
|
138
|
+
probe = self.build_tx_with_dust([0], fee=1000)
|
|
139
|
+
min_fee = self.get_min_relay_fee(probe)
|
|
140
|
+
|
|
141
|
+
# Without penalty, just the min relay fee suffices
|
|
142
|
+
tx = self.build_tx_with_dust([0], fee=min_fee)
|
|
143
|
+
result = self.nodes[0].testmempoolaccept([tx.serialize().hex()])
|
|
144
|
+
assert_equal(result[0]["allowed"], True)
|
|
145
|
+
|
|
146
|
+
def test_op_return_not_penalized(self):
|
|
147
|
+
self.log.info("Test: OP_RETURN outputs are not penalized (threshold=0)")
|
|
148
|
+
self.restart_node(0, extra_args=["-acceptnonstdtxn=1", "-subdustfeepenalty=1"])
|
|
149
|
+
self.wallet.rescan_utxos()
|
|
150
|
+
|
|
151
|
+
utxo = self.wallet.get_utxo()
|
|
152
|
+
tx = CTransaction()
|
|
153
|
+
tx.version = 2
|
|
154
|
+
tx.vin = [CTxIn(COutPoint(int(utxo["txid"], 16), utxo["vout"]))]
|
|
155
|
+
input_value = int(utxo["value"] * COIN)
|
|
156
|
+
fee = 400
|
|
157
|
+
tx.vout = [
|
|
158
|
+
CTxOut(0, CScript([OP_RETURN, b"test data"])),
|
|
159
|
+
CTxOut(input_value - fee, self.wallet.get_output_script()),
|
|
160
|
+
]
|
|
161
|
+
self.wallet.sign_tx(tx)
|
|
162
|
+
|
|
163
|
+
result = self.nodes[0].testmempoolaccept([tx.serialize().hex()])
|
|
164
|
+
assert_equal(result[0]["allowed"], True)
|
|
165
|
+
|
|
166
|
+
def test_above_dust_not_penalized(self):
|
|
167
|
+
self.log.info("Test: outputs above dust threshold are not penalized")
|
|
168
|
+
|
|
169
|
+
probe = self.build_tx_with_dust([DUST_THRESHOLD + 100], fee=1000)
|
|
170
|
+
min_fee = self.get_min_relay_fee(probe)
|
|
171
|
+
|
|
172
|
+
tx = self.build_tx_with_dust([DUST_THRESHOLD + 100], fee=min_fee)
|
|
173
|
+
result = self.nodes[0].testmempoolaccept([tx.serialize().hex()])
|
|
174
|
+
assert_equal(result[0]["allowed"], True)
|
|
175
|
+
|
|
176
|
+
def reset_policy(self, *args):
|
|
177
|
+
# Confirm previous fixtures before reloading policy; avoid repricing saved entries.
|
|
178
|
+
self.generate(self.wallet, 1)
|
|
179
|
+
self.restart_node(0, extra_args=["-acceptnonstdtxn=1", "-subdustfeepenalty=1", *args])
|
|
180
|
+
self.wallet.rescan_utxos()
|
|
181
|
+
|
|
182
|
+
def check_boundary(self, values, penalty, **kwargs):
|
|
183
|
+
utxo = self.wallet.get_utxo()
|
|
184
|
+
probe = self.build_tx_with_dust(values, 1000, utxo=utxo, **kwargs)
|
|
185
|
+
min_fee = self.get_min_relay_fee(probe)
|
|
186
|
+
low = self.build_tx_with_dust(values, min_fee + penalty - 1, utxo=utxo, **kwargs)
|
|
187
|
+
high = self.build_tx_with_dust(values, min_fee + penalty, utxo=utxo, **kwargs)
|
|
188
|
+
rejection = self.nodes[0].testmempoolaccept([low.serialize().hex()])[0]
|
|
189
|
+
assert_equal(rejection["allowed"], False)
|
|
190
|
+
assert_equal(rejection["reject-reason"], "min relay fee not met")
|
|
191
|
+
assert_equal(self.nodes[0].testmempoolaccept([high.serialize().hex()])[0]["allowed"], True)
|
|
192
|
+
|
|
193
|
+
def test_threshold_and_low_rate_boundaries(self):
|
|
194
|
+
self.log.info("Exact threshold, per-output ceiling at one sat/kvB, and zero rate")
|
|
195
|
+
self.check_boundary([DUST_THRESHOLD - 1], 1)
|
|
196
|
+
self.check_boundary([DUST_THRESHOLD], 0)
|
|
197
|
+
self.reset_policy("-dustrelayfee=0.00000001")
|
|
198
|
+
# Each P2TR threshold rounds up separately, rather than rounding the sum.
|
|
199
|
+
self.check_boundary([0, 0], 2)
|
|
200
|
+
self.check_boundary([1], 0)
|
|
201
|
+
self.reset_policy("-dustrelayfee=0")
|
|
202
|
+
self.check_boundary([0, 0], 0)
|
|
203
|
+
self.reset_policy()
|
|
204
|
+
|
|
205
|
+
def test_anchor_and_standardness(self):
|
|
206
|
+
self.log.info("P2A permission does not exempt its zero-value output from the penalty")
|
|
207
|
+
# P2A is a witness program: 13-byte output + 67-byte spend estimate.
|
|
208
|
+
self.check_boundary([0], 240, dust_script=PAY_TO_ANCHOR)
|
|
209
|
+
self.reset_policy("-acceptnonstdtxn=0")
|
|
210
|
+
tx = self.build_tx_with_dust([0], 10000, dust_script=PAY_TO_ANCHOR)
|
|
211
|
+
rejection = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0]
|
|
212
|
+
assert_equal(rejection["allowed"], False)
|
|
213
|
+
assert_equal(rejection["reject-reason"], "dust")
|
|
214
|
+
# Paying the deficit cannot bypass standard dust rejection either.
|
|
215
|
+
tx = self.build_tx_with_dust([0], 10000)
|
|
216
|
+
assert_equal(self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0]["reject-reason"], "dust-nonanchor")
|
|
217
|
+
self.reset_policy("-acceptnonstdtxn=0", "-subdustfeepenalty=0")
|
|
218
|
+
assert_equal(self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0]["reject-reason"], "dust-nonanchor")
|
|
219
|
+
self.reset_policy()
|
|
220
|
+
|
|
221
|
+
def test_carrier_cost_interaction(self):
|
|
222
|
+
self.log.info("Carrier weight and dust deficit independently raise the required effective fee")
|
|
223
|
+
payload = b"x" * 16
|
|
224
|
+
self.reset_policy("-datacarriercost=1.01") # rounds to five weight units per byte
|
|
225
|
+
utxo = self.wallet.get_utxo()
|
|
226
|
+
probe = self.build_tx_with_dust([0], 1000, utxo=utxo, carrier=payload)
|
|
227
|
+
# Recognized carrier bytes include OP_RETURN and the push opcode.
|
|
228
|
+
policy_vsize = (probe.get_weight() + len(CScript([OP_RETURN, payload])) + 3) // 4
|
|
229
|
+
fee = policy_vsize + DUST_THRESHOLD
|
|
230
|
+
low = self.build_tx_with_dust([0], fee - 1, utxo=utxo, carrier=payload)
|
|
231
|
+
high = self.build_tx_with_dust([0], fee, utxo=utxo, carrier=payload)
|
|
232
|
+
assert_equal(self.nodes[0].testmempoolaccept([low.serialize().hex()])[0]["reject-reason"], "min relay fee not met")
|
|
233
|
+
txid = self.nodes[0].sendrawtransaction(high.serialize().hex())
|
|
234
|
+
entry = self.nodes[0].getmempoolentry(txid)
|
|
235
|
+
assert_equal(entry["vsize"], policy_vsize)
|
|
236
|
+
assert_equal(entry["fees"]["base"], Decimal(fee) / COIN)
|
|
237
|
+
assert_equal(entry["fees"]["modified"], Decimal(policy_vsize) / COIN)
|
|
238
|
+
self.reset_policy("-datacarriercost=0")
|
|
239
|
+
self.check_boundary([0], DUST_THRESHOLD, carrier=payload)
|
|
240
|
+
self.reset_policy("-datacarriersize=0")
|
|
241
|
+
tx = self.build_tx_with_dust([0], 10000, carrier=payload)
|
|
242
|
+
assert_equal(self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0]["reject-reason"], "txn-datacarrier-exceeded")
|
|
243
|
+
self.reset_policy("-datacarriersize=0", "-datacarrierfullcount=0")
|
|
244
|
+
self.check_boundary([0], DUST_THRESHOLD, carrier=payload)
|
|
245
|
+
self.reset_policy()
|
|
246
|
+
|
|
247
|
+
def test_fee_delta_and_replacement(self):
|
|
248
|
+
self.log.info("Fee deltas compose with the deficit; RBF compares modified fees")
|
|
249
|
+
node = self.nodes[0]
|
|
250
|
+
utxo = self.wallet.get_utxo()
|
|
251
|
+
tx = self.build_tx_with_dust([0], 1000, utxo=utxo)
|
|
252
|
+
txid = node.sendrawtransaction(tx.serialize().hex())
|
|
253
|
+
assert_equal(node.getmempoolentry(txid)["fees"]["modified"], Decimal(670) / COIN)
|
|
254
|
+
node.prioritisetransaction(txid, 0, 100)
|
|
255
|
+
assert_equal(node.getmempoolentry(txid)["fees"]["base"], Decimal(1000) / COIN)
|
|
256
|
+
assert_equal(node.getmempoolentry(txid)["fees"]["modified"], Decimal(770) / COIN)
|
|
257
|
+
# A higher actual fee can still have a lower effective fee than the conflict.
|
|
258
|
+
low = self.build_tx_with_dust([0], 1050, utxo=utxo)
|
|
259
|
+
assert_equal(node.testmempoolaccept([low.serialize().hex()])[0]["reject-reason"], "insufficient fee")
|
|
260
|
+
high = self.build_tx_with_dust([0], 2000, utxo=utxo)
|
|
261
|
+
replacement = node.sendrawtransaction(high.serialize().hex())
|
|
262
|
+
assert_equal(node.getrawmempool(), [replacement])
|
|
263
|
+
assert_equal(node.getmempoolentry(replacement)["fees"]["modified"], Decimal(1670) / COIN)
|
|
264
|
+
self.reset_policy()
|
|
265
|
+
# A pre-admission delta raises policy fees without changing actual output values.
|
|
266
|
+
tx = self.build_tx_with_dust([0], 0)
|
|
267
|
+
delta = DUST_THRESHOLD + self.get_min_relay_fee(tx)
|
|
268
|
+
node.prioritisetransaction(tx.txid_hex, 0, delta - 1)
|
|
269
|
+
assert_equal(node.testmempoolaccept([tx.serialize().hex()])[0]["allowed"], False)
|
|
270
|
+
node.prioritisetransaction(tx.txid_hex, 0, 1)
|
|
271
|
+
txid = node.sendrawtransaction(tx.serialize().hex())
|
|
272
|
+
assert_equal(node.getmempoolentry(txid)["fees"]["base"], 0)
|
|
273
|
+
assert_equal(node.getmempoolentry(txid)["fees"]["modified"], Decimal(delta - DUST_THRESHOLD) / COIN)
|
|
274
|
+
self.reset_policy()
|
|
275
|
+
|
|
276
|
+
def test_package_and_truc(self):
|
|
277
|
+
self.log.info("TRUC sponsorship pays the dust deficit; ordinary packages retain the relay floor")
|
|
278
|
+
node = self.nodes[0]
|
|
279
|
+
for version in (2, 3):
|
|
280
|
+
parent = self.build_tx_with_dust([0], 0, version=version)
|
|
281
|
+
child = CTransaction()
|
|
282
|
+
child.version = version
|
|
283
|
+
child.vin = [CTxIn(COutPoint(parent.txid_int, 1))]
|
|
284
|
+
child.vout = [CTxOut(parent.vout[1].nValue - 1000, self.wallet.get_output_script())]
|
|
285
|
+
self.wallet.sign_tx(child)
|
|
286
|
+
assert_equal(node.testmempoolaccept([parent.serialize().hex()])[0]["allowed"], False)
|
|
287
|
+
result = node.submitpackage([parent.serialize().hex(), child.serialize().hex()])
|
|
288
|
+
if version == 2:
|
|
289
|
+
assert_equal(result["tx-results"][parent.wtxid_hex]["error"], f"min relay fee not met, -{DUST_THRESHOLD} < {parent.get_vsize()}")
|
|
290
|
+
assert_equal(node.getrawmempool(), [])
|
|
291
|
+
self.generateblock(node, output=self.wallet.get_address(), transactions=[parent.serialize().hex(), child.serialize().hex()])
|
|
292
|
+
else:
|
|
293
|
+
assert_equal(result["package_msg"], "success")
|
|
294
|
+
parent_entry = node.getmempoolentry(parent.txid_hex)
|
|
295
|
+
child_entry = node.getmempoolentry(child.txid_hex)
|
|
296
|
+
assert_equal(parent_entry["fees"]["base"], 0)
|
|
297
|
+
assert_equal(parent_entry["fees"]["modified"], -Decimal(DUST_THRESHOLD) / COIN)
|
|
298
|
+
assert_equal(parent_entry["fees"]["descendant"], Decimal(1000 - DUST_THRESHOLD) / COIN)
|
|
299
|
+
assert_equal(child_entry["fees"]["ancestor"], Decimal(1000 - DUST_THRESHOLD) / COIN)
|
|
300
|
+
self.reset_policy()
|
|
301
|
+
|
|
302
|
+
# Enforced v3 can be sponsored below its individual relay floor, but the
|
|
303
|
+
# combined modified fee must still reach the package's mempool minimum.
|
|
304
|
+
parent = self.build_tx_with_dust([0], 0, version=3)
|
|
305
|
+
child = CTransaction()
|
|
306
|
+
child.version = 3
|
|
307
|
+
child.vin = [CTxIn(COutPoint(parent.txid_int, 1))]
|
|
308
|
+
child.vout = [CTxOut(parent.vout[1].nValue, self.wallet.get_output_script())]
|
|
309
|
+
self.wallet.sign_tx(child)
|
|
310
|
+
package_size = parent.get_vsize() + child.get_vsize()
|
|
311
|
+
fee = DUST_THRESHOLD + package_size
|
|
312
|
+
child.vout[0].nValue -= fee - 1
|
|
313
|
+
result = node.submitpackage([parent.serialize().hex(), child.serialize().hex()])
|
|
314
|
+
assert_equal(result["package_msg"], "transaction failed")
|
|
315
|
+
assert_equal(result["tx-results"][child.wtxid_hex]["error"], f"min relay fee not met, {package_size - 1} < {package_size}")
|
|
316
|
+
assert_equal(node.getrawmempool(), [])
|
|
317
|
+
child.vout[0].nValue -= 1
|
|
318
|
+
result = node.submitpackage([parent.serialize().hex(), child.serialize().hex()])
|
|
319
|
+
assert_equal(result["package_msg"], "success")
|
|
320
|
+
assert_equal(node.getmempoolentry(child.txid_hex)["fees"]["ancestor"], Decimal(package_size) / COIN)
|
|
321
|
+
self.reset_policy("-mempooltruc=accept")
|
|
322
|
+
parent = self.build_tx_with_dust([0], 0, version=3)
|
|
323
|
+
assert_equal(node.testmempoolaccept([parent.serialize().hex()])[0]["reject-reason"], "min relay fee not met")
|
|
324
|
+
self.reset_policy()
|
|
325
|
+
|
|
326
|
+
def test_mining_and_block_acceptance(self):
|
|
327
|
+
self.log.info("Templates select modified fees, while blocks reward actual fees and accept local rejects")
|
|
328
|
+
node = self.nodes[0]
|
|
329
|
+
self.reset_policy("-blockmintxfee=0.00002")
|
|
330
|
+
probe = self.build_tx_with_dust([0], 1000)
|
|
331
|
+
min_fee = self.get_min_relay_fee(probe)
|
|
332
|
+
low = self.build_tx_with_dust([0], min_fee)
|
|
333
|
+
assert_equal(node.testmempoolaccept([low.serialize().hex()])[0]["reject-reason"], "min relay fee not met")
|
|
334
|
+
previous_height = node.getblockcount()
|
|
335
|
+
block = self.generateblock(node, output=self.wallet.get_address(), transactions=[low.serialize().hex()])
|
|
336
|
+
assert_equal(node.getblockcount(), previous_height + 1)
|
|
337
|
+
assert low.txid_hex in node.getblock(block["hash"])["tx"]
|
|
338
|
+
self.wallet.rescan_utxos()
|
|
339
|
+
# Actual fee exceeds template minimum, but effective fee is one sat below it.
|
|
340
|
+
fee = DUST_THRESHOLD + 2 * min_fee - 1
|
|
341
|
+
tx = self.build_tx_with_dust([0], fee)
|
|
342
|
+
txid = node.sendrawtransaction(tx.serialize().hex())
|
|
343
|
+
template = node.getblocktemplate({"rules": ["segwit"]})
|
|
344
|
+
assert txid not in [entry["txid"] for entry in template["transactions"]]
|
|
345
|
+
node.prioritisetransaction(txid, 0, 1)
|
|
346
|
+
# Changing mocktime expires the RPC's cached template without wall-clock sleeps.
|
|
347
|
+
node.setmocktime(node.getblockheader(node.getbestblockhash())["time"] + 10)
|
|
348
|
+
template = node.getblocktemplate({"rules": ["segwit"]})
|
|
349
|
+
entry = next(entry for entry in template["transactions"] if entry["txid"] == txid)
|
|
350
|
+
assert_equal(entry["fee"], fee)
|
|
351
|
+
self.generate(self.wallet, 1)
|
|
352
|
+
mined = node.getblock(node.getbestblockhash(), 2)
|
|
353
|
+
assert txid in [entry["txid"] for entry in mined["tx"]]
|
|
354
|
+
# Regtest at this height has a 25 BTC subsidy. Penalty/delta do not alter reward.
|
|
355
|
+
assert_equal(sum(output["value"] for output in mined["tx"][0]["vout"]), Decimal(25) + Decimal(fee) / COIN)
|
|
356
|
+
self.reset_policy("-blockmintxfee=0.00002", "-subdustfeepenalty=0")
|
|
357
|
+
tx = self.build_tx_with_dust([0], fee)
|
|
358
|
+
txid = node.sendrawtransaction(tx.serialize().hex())
|
|
359
|
+
assert txid in [entry["txid"] for entry in node.getblocktemplate({"rules": ["segwit"]})["transactions"]]
|
|
360
|
+
# Priority-area selection is verified with its native L9 implementation.
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
if __name__ == '__main__':
|
|
365
|
+
SubDustFeePenaltyTest(__file__).main()
|