Skip to content

Commit 799fd7a

Browse files
author
MarcoFalke
committed
Merge bitcoin/bitcoin#23075: test: Fee estimation functional test cleanups
60ae116 qa: replace assert with test framework assertion helpers in fee estimation test (Antoine Poinsot) e502139 qa: fee estimation with RBF test cleanups (Antoine Poinsot) 15f5fd6 qa: don't mine non standard txs in fee estimation test (Antoine Poinsot) eae52dd qa: pass scriptsig directly to txins constructor in fee estimation test (Antoine Poinsot) 1fc0315 qa: split coins in a single tx in fee estimation test (Antoine Poinsot) cc204b8 qa: use a single p2sh script in fee estimation test (Antoine Poinsot) 19dd91a qa: remove a redundant condition in fee estimation test (Antoine Poinsot) Pull request description: Some cleanups that i noticed would be desirable while working on #23074 and #22539, which are intentionally not based on it. Mainly simplifications and a slight speedup. - Use a single tx to create the `2**9` coins instead of creating `2**8` 2-outputs transactions - Use a single P2SH script - Avoid the use of non-standard transactions - Misc style nits (happy to take more) ACKs for top commit: pg156: I ACK all commits up to bitcoin/bitcoin@60ae116 (except bitcoin/bitcoin@1fc0315, where I have a question more for my own learning than actually questioning the PR). I built and ran the test successfully. I agree after the changes, the behavior is kept the same and the code is shorter and easier to reason. glozow: utACK 60ae116 Tree-SHA512: 57ae2294eb68961ced30f32448c4a530ba1cdee17881594eecb97e1b9ba8927d58c25022b847eb07fb67d676bf436108c416c2f2174864d258fcca5b528b8bbd
2 parents cec6781 + 60ae116 commit 799fd7a

File tree

1 file changed

+88
-112
lines changed

1 file changed

+88
-112
lines changed

test/functional/feature_fee_estimation.py

Lines changed: 88 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from test_framework.script import (
1818
CScript,
1919
OP_1,
20-
OP_2,
2120
OP_DROP,
2221
OP_TRUE,
2322
)
@@ -36,16 +35,14 @@
3635
# Construct 2 trivial P2SH's and the ScriptSigs that spend them
3736
# So we can create many transactions without needing to spend
3837
# time signing.
39-
REDEEM_SCRIPT_1 = CScript([OP_1, OP_DROP])
40-
REDEEM_SCRIPT_2 = CScript([OP_2, OP_DROP])
41-
P2SH_1 = script_to_p2sh_script(REDEEM_SCRIPT_1)
42-
P2SH_2 = script_to_p2sh_script(REDEEM_SCRIPT_2)
38+
SCRIPT = CScript([OP_1, OP_DROP])
39+
P2SH = script_to_p2sh_script(SCRIPT)
40+
REDEEM_SCRIPT = CScript([OP_TRUE, SCRIPT])
4341

44-
# Associated ScriptSig's to spend satisfy P2SH_1 and P2SH_2
45-
SCRIPT_SIG = [CScript([OP_TRUE, REDEEM_SCRIPT_1]), CScript([OP_TRUE, REDEEM_SCRIPT_2])]
4642

47-
48-
def small_txpuzzle_randfee(from_node, conflist, unconflist, amount, min_fee, fee_increment):
43+
def small_txpuzzle_randfee(
44+
from_node, conflist, unconflist, amount, min_fee, fee_increment
45+
):
4946
"""Create and send a transaction with a random fee.
5047
5148
The transaction pays to a trivial P2SH script, and assumes that its inputs
@@ -66,55 +63,22 @@ def small_txpuzzle_randfee(from_node, conflist, unconflist, amount, min_fee, fee
6663
while total_in <= (amount + fee) and len(conflist) > 0:
6764
t = conflist.pop(0)
6865
total_in += t["amount"]
69-
tx.vin.append(CTxIn(COutPoint(int(t["txid"], 16), t["vout"]), b""))
66+
tx.vin.append(CTxIn(COutPoint(int(t["txid"], 16), t["vout"]), REDEEM_SCRIPT))
67+
while total_in <= (amount + fee) and len(unconflist) > 0:
68+
t = unconflist.pop(0)
69+
total_in += t["amount"]
70+
tx.vin.append(CTxIn(COutPoint(int(t["txid"], 16), t["vout"]), REDEEM_SCRIPT))
7071
if total_in <= amount + fee:
71-
while total_in <= (amount + fee) and len(unconflist) > 0:
72-
t = unconflist.pop(0)
73-
total_in += t["amount"]
74-
tx.vin.append(CTxIn(COutPoint(int(t["txid"], 16), t["vout"]), b""))
75-
if total_in <= amount + fee:
76-
raise RuntimeError(f"Insufficient funds: need {amount + fee}, have {total_in}")
77-
tx.vout.append(CTxOut(int((total_in - amount - fee) * COIN), P2SH_1))
78-
tx.vout.append(CTxOut(int(amount * COIN), P2SH_2))
79-
# These transactions don't need to be signed, but we still have to insert
80-
# the ScriptSig that will satisfy the ScriptPubKey.
81-
for inp in tx.vin:
82-
inp.scriptSig = SCRIPT_SIG[inp.prevout.n]
72+
raise RuntimeError(f"Insufficient funds: need {amount + fee}, have {total_in}")
73+
tx.vout.append(CTxOut(int((total_in - amount - fee) * COIN), P2SH))
74+
tx.vout.append(CTxOut(int(amount * COIN), P2SH))
8375
txid = from_node.sendrawtransaction(hexstring=tx.serialize().hex(), maxfeerate=0)
8476
unconflist.append({"txid": txid, "vout": 0, "amount": total_in - amount - fee})
8577
unconflist.append({"txid": txid, "vout": 1, "amount": amount})
8678

8779
return (tx.serialize().hex(), fee)
8880

8981

90-
def split_inputs(from_node, txins, txouts, initial_split=False):
91-
"""Generate a lot of inputs so we can generate a ton of transactions.
92-
93-
This function takes an input from txins, and creates and sends a transaction
94-
which splits the value into 2 outputs which are appended to txouts.
95-
Previously this was designed to be small inputs so they wouldn't have
96-
a high coin age when the notion of priority still existed."""
97-
98-
prevtxout = txins.pop()
99-
tx = CTransaction()
100-
tx.vin.append(CTxIn(COutPoint(int(prevtxout["txid"], 16), prevtxout["vout"]), b""))
101-
102-
half_change = satoshi_round(prevtxout["amount"] / 2)
103-
rem_change = prevtxout["amount"] - half_change - Decimal("0.00001000")
104-
tx.vout.append(CTxOut(int(half_change * COIN), P2SH_1))
105-
tx.vout.append(CTxOut(int(rem_change * COIN), P2SH_2))
106-
107-
# If this is the initial split we actually need to sign the transaction
108-
# Otherwise we just need to insert the proper ScriptSig
109-
if (initial_split):
110-
completetx = from_node.signrawtransactionwithwallet(tx.serialize().hex())["hex"]
111-
else:
112-
tx.vin[0].scriptSig = SCRIPT_SIG[prevtxout["vout"]]
113-
completetx = tx.serialize().hex()
114-
txid = from_node.sendrawtransaction(hexstring=completetx, maxfeerate=0)
115-
txouts.append({"txid": txid, "vout": 0, "amount": half_change})
116-
txouts.append({"txid": txid, "vout": 1, "amount": rem_change})
117-
11882
def check_raw_estimates(node, fees_seen):
11983
"""Call estimaterawfee and verify that the estimates meet certain invariants."""
12084

@@ -125,61 +89,67 @@ def check_raw_estimates(node, fees_seen):
12589
assert_greater_than(feerate, 0)
12690

12791
if feerate + delta < min(fees_seen) or feerate - delta > max(fees_seen):
128-
raise AssertionError(f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})")
92+
raise AssertionError(
93+
f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})"
94+
)
95+
12996

13097
def check_smart_estimates(node, fees_seen):
13198
"""Call estimatesmartfee and verify that the estimates meet certain invariants."""
13299

133100
delta = 1.0e-6 # account for rounding error
134101
last_feerate = float(max(fees_seen))
135102
all_smart_estimates = [node.estimatesmartfee(i) for i in range(1, 26)]
136-
mempoolMinFee = node.getmempoolinfo()['mempoolminfee']
137-
minRelaytxFee = node.getmempoolinfo()['minrelaytxfee']
103+
mempoolMinFee = node.getmempoolinfo()["mempoolminfee"]
104+
minRelaytxFee = node.getmempoolinfo()["minrelaytxfee"]
138105
for i, e in enumerate(all_smart_estimates): # estimate is for i+1
139106
feerate = float(e["feerate"])
140107
assert_greater_than(feerate, 0)
141108
assert_greater_than_or_equal(feerate, float(mempoolMinFee))
142109
assert_greater_than_or_equal(feerate, float(minRelaytxFee))
143110

144111
if feerate + delta < min(fees_seen) or feerate - delta > max(fees_seen):
145-
raise AssertionError(f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})")
112+
raise AssertionError(
113+
f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})"
114+
)
146115
if feerate - delta > last_feerate:
147-
raise AssertionError(f"Estimated fee ({feerate}) larger than last fee ({last_feerate}) for lower number of confirms")
116+
raise AssertionError(
117+
f"Estimated fee ({feerate}) larger than last fee ({last_feerate}) for lower number of confirms"
118+
)
148119
last_feerate = feerate
149120

150121
if i == 0:
151122
assert_equal(e["blocks"], 2)
152123
else:
153124
assert_greater_than_or_equal(i + 1, e["blocks"])
154125

126+
155127
def check_estimates(node, fees_seen):
156128
check_raw_estimates(node, fees_seen)
157129
check_smart_estimates(node, fees_seen)
158130

159131

160132
def send_tx(node, utxo, feerate):
161133
"""Broadcast a 1in-1out transaction with a specific input and feerate (sat/vb)."""
162-
overhead, op, scriptsig, nseq, value, spk = 10, 36, 5, 4, 8, 24
163-
tx_size = overhead + op + scriptsig + nseq + value + spk
164-
fee = tx_size * feerate
165-
166134
tx = CTransaction()
167-
tx.vin = [CTxIn(COutPoint(int(utxo["txid"], 16), utxo["vout"]), SCRIPT_SIG[utxo["vout"]])]
168-
tx.vout = [CTxOut(int(utxo["amount"] * COIN) - fee, P2SH_1)]
169-
txid = node.sendrawtransaction(tx.serialize().hex())
135+
tx.vin = [CTxIn(COutPoint(int(utxo["txid"], 16), utxo["vout"]), REDEEM_SCRIPT)]
136+
tx.vout = [CTxOut(int(utxo["amount"] * COIN), P2SH)]
137+
138+
# vbytes == bytes as we are using legacy transactions
139+
fee = tx.get_vsize() * feerate
140+
tx.vout[0].nValue -= fee
170141

171-
return txid
142+
return node.sendrawtransaction(tx.serialize().hex())
172143

173144

174145
class EstimateFeeTest(BitcoinTestFramework):
175146
def set_test_params(self):
176147
self.num_nodes = 3
177-
# mine non-standard txs (e.g. txs with "dust" outputs)
178148
# Force fSendTrickle to true (via whitelist.noban)
179149
self.extra_args = [
180-
["-acceptnonstdtxn", "-[email protected]"],
181-
["-acceptnonstdtxn", "-[email protected]", "-blockmaxweight=68000"],
182-
["-acceptnonstdtxn", "-[email protected]", "-blockmaxweight=32000"],
150+
151+
["[email protected]", "-blockmaxweight=68000"],
152+
["[email protected]", "-blockmaxweight=32000"],
183153
]
184154

185155
def skip_test_if_missing_module(self):
@@ -212,11 +182,17 @@ def transact_and_mine(self, numblocks, mining_node):
212182
random.shuffle(self.confutxo)
213183
for _ in range(random.randrange(100 - 50, 100 + 50)):
214184
from_index = random.randint(1, 2)
215-
(txhex, fee) = small_txpuzzle_randfee(self.nodes[from_index], self.confutxo,
216-
self.memutxo, Decimal("0.005"), min_fee, min_fee)
185+
(txhex, fee) = small_txpuzzle_randfee(
186+
self.nodes[from_index],
187+
self.confutxo,
188+
self.memutxo,
189+
Decimal("0.005"),
190+
min_fee,
191+
min_fee,
192+
)
217193
tx_kbytes = (len(txhex) // 2) / 1000.0
218194
self.fees_per_kb.append(float(fee) / tx_kbytes)
219-
self.sync_mempools(wait=.1)
195+
self.sync_mempools(wait=0.1)
220196
mined = mining_node.getblock(self.generate(mining_node, 1)[0], True)["tx"]
221197
# update which txouts are confirmed
222198
newmem = []
@@ -229,46 +205,45 @@ def transact_and_mine(self, numblocks, mining_node):
229205

230206
def initial_split(self, node):
231207
"""Split two coinbase UTxOs into many small coins"""
232-
self.txouts = []
233-
self.txouts2 = []
234-
# Split a coinbase into two transaction puzzle outputs
235-
split_inputs(node, node.listunspent(0), self.txouts, True)
236-
237-
# Mine
208+
utxo_count = 2048
209+
self.confutxo = []
210+
splitted_amount = Decimal("0.04")
211+
fee = Decimal("0.1")
212+
change = Decimal("100") - splitted_amount * utxo_count - fee
213+
tx = CTransaction()
214+
tx.vin = [
215+
CTxIn(COutPoint(int(cb["txid"], 16), cb["vout"]))
216+
for cb in node.listunspent()[:2]
217+
]
218+
tx.vout = [CTxOut(int(splitted_amount * COIN), P2SH) for _ in range(utxo_count)]
219+
tx.vout.append(CTxOut(int(change * COIN), P2SH))
220+
txhex = node.signrawtransactionwithwallet(tx.serialize().hex())["hex"]
221+
txid = node.sendrawtransaction(txhex)
222+
self.confutxo = [
223+
{"txid": txid, "vout": i, "amount": splitted_amount}
224+
for i in range(utxo_count)
225+
]
238226
while len(node.getrawmempool()) > 0:
239227
self.generate(node, 1, sync_fun=self.no_op)
240228

241-
# Repeatedly split those 2 outputs, doubling twice for each rep
242-
# Use txouts to monitor the available utxo, since these won't be tracked in wallet
243-
reps = 0
244-
while reps < 5:
245-
# Double txouts to txouts2
246-
while len(self.txouts) > 0:
247-
split_inputs(node, self.txouts, self.txouts2)
248-
while len(node.getrawmempool()) > 0:
249-
self.generate(node, 1, sync_fun=self.no_op)
250-
# Double txouts2 to txouts
251-
while len(self.txouts2) > 0:
252-
split_inputs(node, self.txouts2, self.txouts)
253-
while len(node.getrawmempool()) > 0:
254-
self.generate(node, 1, sync_fun=self.no_op)
255-
reps += 1
256-
257229
def sanity_check_estimates_range(self):
258230
"""Populate estimation buckets, assert estimates are in a sane range and
259231
are strictly increasing as the target decreases."""
260232
self.fees_per_kb = []
261233
self.memutxo = []
262-
self.confutxo = self.txouts # Start with the set of confirmed txouts after splitting
263234
self.log.info("Will output estimates for 1/2/3/6/15/25 blocks")
264235

265236
for _ in range(2):
266-
self.log.info("Creating transactions and mining them with a block size that can't keep up")
237+
self.log.info(
238+
"Creating transactions and mining them with a block size that can't keep up"
239+
)
267240
# Create transactions and mine 10 small blocks with node 2, but create txs faster than we can mine
268241
self.transact_and_mine(10, self.nodes[2])
269242
check_estimates(self.nodes[1], self.fees_per_kb)
270243

271-
self.log.info("Creating transactions and mining them at a block size that is just big enough")
244+
self.log.info(
245+
"Creating transactions and mining them at a block size that is just big enough"
246+
)
272247
# Generate transactions while mining 10 more blocks, this time with node1
273248
# which mines blocks with capacity just above the rate that transactions are being created
274249
self.transact_and_mine(10, self.nodes[1])
@@ -277,12 +252,13 @@ def sanity_check_estimates_range(self):
277252
# Finish by mining a normal-sized block:
278253
while len(self.nodes[1].getrawmempool()) > 0:
279254
self.generate(self.nodes[1], 1)
255+
280256
self.log.info("Final estimates after emptying mempools")
281257
check_estimates(self.nodes[1], self.fees_per_kb)
282258

283259
def test_feerate_mempoolminfee(self):
284-
high_val = 3*self.nodes[1].estimatesmartfee(1)['feerate']
285-
self.restart_node(1, extra_args=[f'-minrelaytxfee={high_val}'])
260+
high_val = 3 * self.nodes[1].estimatesmartfee(1)["feerate"]
261+
self.restart_node(1, extra_args=[f"-minrelaytxfee={high_val}"])
286262
check_estimates(self.nodes[1], self.fees_per_kb)
287263
self.restart_node(1)
288264

@@ -303,7 +279,7 @@ def sanity_check_rbf_estimates(self, utxos):
303279
utxos_to_respend = []
304280
txids_to_replace = []
305281

306-
assert len(utxos) >= 250
282+
assert_greater_than_or_equal(len(utxos), 250)
307283
for _ in range(5):
308284
# Broadcast 45 low fee transactions that will need to be RBF'd
309285
for _ in range(45):
@@ -315,27 +291,24 @@ def sanity_check_rbf_estimates(self, utxos):
315291
for _ in range(5):
316292
send_tx(node, utxos.pop(0), low_feerate)
317293
# Mine the transactions on another node
318-
self.sync_mempools(wait=.1, nodes=[node, miner])
294+
self.sync_mempools(wait=0.1, nodes=[node, miner])
319295
for txid in txids_to_replace:
320296
miner.prioritisetransaction(txid=txid, fee_delta=-COIN)
321297
self.generate(miner, 1)
322298
# RBF the low-fee transactions
323-
while True:
324-
try:
325-
u = utxos_to_respend.pop(0)
326-
send_tx(node, u, high_feerate)
327-
except IndexError:
328-
break
299+
while len(utxos_to_respend) > 0:
300+
u = utxos_to_respend.pop(0)
301+
send_tx(node, u, high_feerate)
329302

330303
# Mine the last replacement txs
331-
self.sync_mempools(wait=.1, nodes=[node, miner])
304+
self.sync_mempools(wait=0.1, nodes=[node, miner])
332305
self.generate(miner, 1)
333306

334307
# Only 10% of the transactions were really confirmed with a low feerate,
335308
# the rest needed to be RBF'd. We must return the 90% conf rate feerate.
336-
high_feerate_kvb = Decimal(high_feerate) / COIN * 10**3
309+
high_feerate_kvb = Decimal(high_feerate) / COIN * 10 ** 3
337310
est_feerate = node.estimatesmartfee(2)["feerate"]
338-
assert est_feerate == high_feerate_kvb
311+
assert_equal(est_feerate, high_feerate_kvb)
339312

340313
def run_test(self):
341314
self.log.info("This test is time consuming, please be patient")
@@ -359,7 +332,9 @@ def run_test(self):
359332
self.sanity_check_estimates_range()
360333

361334
# check that the effective feerate is greater than or equal to the mempoolminfee even for high mempoolminfee
362-
self.log.info("Test fee rate estimation after restarting node with high MempoolMinFee")
335+
self.log.info(
336+
"Test fee rate estimation after restarting node with high MempoolMinFee"
337+
)
363338
self.test_feerate_mempoolminfee()
364339

365340
self.log.info("Restarting node with fresh estimation")
@@ -375,9 +350,10 @@ def run_test(self):
375350

376351
self.log.info("Testing that fee estimation is disabled in blocksonly.")
377352
self.restart_node(0, ["-blocksonly"])
378-
assert_raises_rpc_error(-32603, "Fee estimation disabled",
379-
self.nodes[0].estimatesmartfee, 2)
353+
assert_raises_rpc_error(
354+
-32603, "Fee estimation disabled", self.nodes[0].estimatesmartfee, 2
355+
)
380356

381357

382-
if __name__ == '__main__':
358+
if __name__ == "__main__":
383359
EstimateFeeTest().main()

0 commit comments

Comments
 (0)