Skip to content

Commit 30b1637

Browse files
committed
merge bitcoin#23075: Fee estimation functional test cleanups
excludes: - e502139 (we don't support RBF) - 60ae116 (see above)
1 parent 8d5883c commit 30b1637

File tree

1 file changed

+76
-94
lines changed

1 file changed

+76
-94
lines changed

test/functional/feature_fee_estimation.py

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

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

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

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

8880

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

@@ -125,7 +88,10 @@ def check_raw_estimates(node, fees_seen):
12588
assert_greater_than(feerate, 0)
12689

12790
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)})")
91+
raise AssertionError(
92+
f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})"
93+
)
94+
12995

13096

13197
def check_smart_estimates(node, fees_seen):
@@ -134,18 +100,22 @@ def check_smart_estimates(node, fees_seen):
134100
delta = 1.0e-6 # account for rounding error
135101
last_feerate = float(max(fees_seen))
136102
all_smart_estimates = [node.estimatesmartfee(i) for i in range(1, 26)]
137-
mempoolMinFee = node.getmempoolinfo()['mempoolminfee']
138-
minRelaytxFee = node.getmempoolinfo()['minrelaytxfee']
103+
mempoolMinFee = node.getmempoolinfo()["mempoolminfee"]
104+
minRelaytxFee = node.getmempoolinfo()["minrelaytxfee"]
139105
for i, e in enumerate(all_smart_estimates): # estimate is for i+1
140106
feerate = float(e["feerate"])
141107
assert_greater_than(feerate, 0)
142108
assert_greater_than_or_equal(feerate, float(mempoolMinFee))
143109
assert_greater_than_or_equal(feerate, float(minRelaytxFee))
144110

145111
if feerate + delta < min(fees_seen) or feerate - delta > max(fees_seen):
146-
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+
)
147115
if feerate - delta > last_feerate:
148-
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+
)
149119
last_feerate = feerate
150120

151121
if i == 0:
@@ -161,12 +131,11 @@ def check_estimates(node, fees_seen):
161131
class EstimateFeeTest(BitcoinTestFramework):
162132
def set_test_params(self):
163133
self.num_nodes = 3
164-
# mine non-standard txs (e.g. txs with "dust" outputs)
165134
# Force fSendTrickle to true (via whitelist)
166135
self.extra_args = [
167-
["-acceptnonstdtxn=1", "-[email protected]"],
168-
["-acceptnonstdtxn=1", "-[email protected]", "-blockmaxsize=17000"],
169-
["-acceptnonstdtxn=1", "-[email protected]", "-blockmaxsize=8000"]
136+
137+
["[email protected]", "-blockmaxsize=17000"],
138+
["[email protected]", "-blockmaxsize=8000"]
170139
]
171140

172141
def skip_test_if_missing_module(self):
@@ -199,11 +168,17 @@ def transact_and_mine(self, numblocks, mining_node):
199168
random.shuffle(self.confutxo)
200169
for _ in range(random.randrange(100 - 50, 100 + 50)):
201170
from_index = random.randint(1, 2)
202-
(txhex, fee) = small_txpuzzle_randfee(self.nodes[from_index], self.confutxo,
203-
self.memutxo, Decimal("0.005"), min_fee, min_fee)
171+
(txhex, fee) = small_txpuzzle_randfee(
172+
self.nodes[from_index],
173+
self.confutxo,
174+
self.memutxo,
175+
Decimal("0.005"),
176+
min_fee,
177+
min_fee,
178+
)
204179
tx_kbytes = (len(txhex) // 2) / 1000.0
205180
self.fees_per_kb.append(float(fee) / tx_kbytes)
206-
self.sync_mempools(wait=.1)
181+
self.sync_mempools(wait=0.1)
207182
mined = mining_node.getblock(self.generate(mining_node, 1)[0], True)["tx"]
208183
# update which txouts are confirmed
209184
newmem = []
@@ -216,46 +191,49 @@ def transact_and_mine(self, numblocks, mining_node):
216191

217192
def initial_split(self, node):
218193
"""Split two coinbase UTxOs into many small coins"""
219-
self.txouts = []
220-
self.txouts2 = []
221-
# Split a coinbase into two transaction puzzle outputs
222-
split_inputs(node, node.listunspent(0), self.txouts, True)
223-
224-
# Mine
194+
utxo_count = 2048
195+
self.confutxo = []
196+
splitted_amount = Decimal("0.04")
197+
fee = Decimal("0.0007")
198+
# Calculate change from UTXOs instead of relying on hardcoded amounts
199+
coinbase_utxos = node.listunspent()[:2]
200+
total_input = sum(Decimal(str(cb["amount"])) for cb in coinbase_utxos)
201+
change = total_input - splitted_amount * utxo_count - fee
202+
203+
tx = CTransaction()
204+
tx.vin = [
205+
CTxIn(COutPoint(int(cb["txid"], 16), cb["vout"]))
206+
for cb in coinbase_utxos
207+
]
208+
tx.vout = [CTxOut(int(splitted_amount * COIN), P2SH) for _ in range(utxo_count)]
209+
tx.vout.append(CTxOut(int(change * COIN), P2SH))
210+
txhex = node.signrawtransactionwithwallet(tx.serialize().hex())["hex"]
211+
txid = node.sendrawtransaction(txhex)
212+
self.confutxo = [
213+
{"txid": txid, "vout": i, "amount": splitted_amount}
214+
for i in range(utxo_count)
215+
]
225216
while len(node.getrawmempool()) > 0:
226217
self.generate(node, 1, sync_fun=self.no_op)
227218

228-
# Repeatedly split those 2 outputs, doubling twice for each rep
229-
# Use txouts to monitor the available utxo, since these won't be tracked in wallet
230-
reps = 0
231-
while reps < 5:
232-
# Double txouts to txouts2
233-
while len(self.txouts) > 0:
234-
split_inputs(node, self.txouts, self.txouts2)
235-
while len(node.getrawmempool()) > 0:
236-
self.generate(node, 1, sync_fun=self.no_op)
237-
# Double txouts2 to txouts
238-
while len(self.txouts2) > 0:
239-
split_inputs(node, self.txouts2, self.txouts)
240-
while len(node.getrawmempool()) > 0:
241-
self.generate(node, 1, sync_fun=self.no_op)
242-
reps += 1
243-
244219
def sanity_check_estimates_range(self):
245220
"""Populate estimation buckets, assert estimates are in a sane range and
246221
are strictly increasing as the target decreases."""
247222
self.fees_per_kb = []
248223
self.memutxo = []
249-
self.confutxo = self.txouts # Start with the set of confirmed txouts after splitting
250224
self.log.info("Will output estimates for 1/2/3/6/15/25 blocks")
251225

252226
for _ in range(2):
253-
self.log.info("Creating transactions and mining them with a block size that can't keep up")
227+
self.log.info(
228+
"Creating transactions and mining them with a block size that can't keep up"
229+
)
254230
# Create transactions and mine 10 small blocks with node 2, but create txs faster than we can mine
255231
self.transact_and_mine(10, self.nodes[2])
256232
check_estimates(self.nodes[1], self.fees_per_kb)
257233

258-
self.log.info("Creating transactions and mining them at a block size that is just big enough")
234+
self.log.info(
235+
"Creating transactions and mining them at a block size that is just big enough"
236+
)
259237
# Generate transactions while mining 10 more blocks, this time with node1
260238
# which mines blocks with capacity just above the rate that transactions are being created
261239
self.transact_and_mine(10, self.nodes[1])
@@ -264,12 +242,13 @@ def sanity_check_estimates_range(self):
264242
# Finish by mining a normal-sized block:
265243
while len(self.nodes[1].getrawmempool()) > 0:
266244
self.generate(self.nodes[1], 1)
245+
267246
self.log.info("Final estimates after emptying mempools")
268247
check_estimates(self.nodes[1], self.fees_per_kb)
269248

270249
def test_feerate_mempoolminfee(self):
271-
high_val = 3*self.nodes[1].estimatesmartfee(1)['feerate']
272-
self.restart_node(1, extra_args=[f'-minrelaytxfee={high_val}'])
250+
high_val = 3 * self.nodes[1].estimatesmartfee(1)["feerate"]
251+
self.restart_node(1, extra_args=[f"-minrelaytxfee={high_val}"])
273252
check_estimates(self.nodes[1], self.fees_per_kb)
274253
self.stop_node(1, expected_stderr="Warning: -minrelaytxfee is set very high! The wallet will avoid paying less than the minimum relay fee.")
275254

@@ -295,14 +274,17 @@ def run_test(self):
295274
self.sanity_check_estimates_range()
296275

297276
# check that the effective feerate is greater than or equal to the mempoolminfee even for high mempoolminfee
298-
self.log.info("Test fee rate estimation after restarting node with high MempoolMinFee")
277+
self.log.info(
278+
"Test fee rate estimation after restarting node with high MempoolMinFee"
279+
)
299280
self.test_feerate_mempoolminfee()
300281

301282
self.log.info("Testing that fee estimation is disabled in blocksonly.")
302283
self.restart_node(0, ["-blocksonly"])
303-
assert_raises_rpc_error(-32603, "Fee estimation disabled",
304-
self.nodes[0].estimatesmartfee, 2)
284+
assert_raises_rpc_error(
285+
-32603, "Fee estimation disabled", self.nodes[0].estimatesmartfee, 2
286+
)
305287

306288

307-
if __name__ == '__main__':
289+
if __name__ == "__main__":
308290
EstimateFeeTest().main()

0 commit comments

Comments
 (0)