Skip to content

Commit 1fc0315

Browse files
committed
qa: split coins in a single tx in fee estimation test
This simplifies the code, and slightly speeds up the test. Running `./test/functional/test_runner.py -j15 $(printf 'feature_fee_estimation %.0s' {1..15})` on master 3 times gives: - Before: ALL | ✓ Passed | 788 s (accumulated) ALL | ✓ Passed | 818 s (accumulated) ALL | ✓ Passed | 873 s (accumulated) - After: ALL | ✓ Passed | 763 s (accumulated) ALL | ✓ Passed | 798 s (accumulated) ALL | ✓ Passed | 731 s (accumulated) Signed-off-by: Antoine Poinsot <[email protected]>
1 parent cc204b8 commit 1fc0315

File tree

1 file changed

+62
-71
lines changed

1 file changed

+62
-71
lines changed

test/functional/feature_fee_estimation.py

Lines changed: 62 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@
4040
REDEEM_SCRIPT = CScript([OP_TRUE, SCRIPT])
4141

4242

43-
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+
):
4446
"""Create and send a transaction with a random fee.
4547
4648
The transaction pays to a trivial P2SH script, and assumes that its inputs
@@ -81,34 +83,6 @@ def small_txpuzzle_randfee(from_node, conflist, unconflist, amount, min_fee, fee
8183
return (tx.serialize().hex(), fee)
8284

8385

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

@@ -119,33 +93,41 @@ def check_raw_estimates(node, fees_seen):
11993
assert_greater_than(feerate, 0)
12094

12195
if feerate + delta < min(fees_seen) or feerate - delta > max(fees_seen):
122-
raise AssertionError(f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})")
96+
raise AssertionError(
97+
f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})"
98+
)
99+
123100

124101
def check_smart_estimates(node, fees_seen):
125102
"""Call estimatesmartfee and verify that the estimates meet certain invariants."""
126103

127104
delta = 1.0e-6 # account for rounding error
128105
last_feerate = float(max(fees_seen))
129106
all_smart_estimates = [node.estimatesmartfee(i) for i in range(1, 26)]
130-
mempoolMinFee = node.getmempoolinfo()['mempoolminfee']
131-
minRelaytxFee = node.getmempoolinfo()['minrelaytxfee']
107+
mempoolMinFee = node.getmempoolinfo()["mempoolminfee"]
108+
minRelaytxFee = node.getmempoolinfo()["minrelaytxfee"]
132109
for i, e in enumerate(all_smart_estimates): # estimate is for i+1
133110
feerate = float(e["feerate"])
134111
assert_greater_than(feerate, 0)
135112
assert_greater_than_or_equal(feerate, float(mempoolMinFee))
136113
assert_greater_than_or_equal(feerate, float(minRelaytxFee))
137114

138115
if feerate + delta < min(fees_seen) or feerate - delta > max(fees_seen):
139-
raise AssertionError(f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})")
116+
raise AssertionError(
117+
f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})"
118+
)
140119
if feerate - delta > last_feerate:
141-
raise AssertionError(f"Estimated fee ({feerate}) larger than last fee ({last_feerate}) for lower number of confirms")
120+
raise AssertionError(
121+
f"Estimated fee ({feerate}) larger than last fee ({last_feerate}) for lower number of confirms"
122+
)
142123
last_feerate = feerate
143124

144125
if i == 0:
145126
assert_equal(e["blocks"], 2)
146127
else:
147128
assert_greater_than_or_equal(i + 1, e["blocks"])
148129

130+
149131
def check_estimates(node, fees_seen):
150132
check_raw_estimates(node, fees_seen)
151133
check_smart_estimates(node, fees_seen)
@@ -206,11 +188,17 @@ def transact_and_mine(self, numblocks, mining_node):
206188
random.shuffle(self.confutxo)
207189
for _ in range(random.randrange(100 - 50, 100 + 50)):
208190
from_index = random.randint(1, 2)
209-
(txhex, fee) = small_txpuzzle_randfee(self.nodes[from_index], self.confutxo,
210-
self.memutxo, Decimal("0.005"), min_fee, min_fee)
191+
(txhex, fee) = small_txpuzzle_randfee(
192+
self.nodes[from_index],
193+
self.confutxo,
194+
self.memutxo,
195+
Decimal("0.005"),
196+
min_fee,
197+
min_fee,
198+
)
211199
tx_kbytes = (len(txhex) // 2) / 1000.0
212200
self.fees_per_kb.append(float(fee) / tx_kbytes)
213-
self.sync_mempools(wait=.1)
201+
self.sync_mempools(wait=0.1)
214202
mined = mining_node.getblock(self.generate(mining_node, 1)[0], True)["tx"]
215203
# update which txouts are confirmed
216204
newmem = []
@@ -223,46 +211,45 @@ def transact_and_mine(self, numblocks, mining_node):
223211

224212
def initial_split(self, node):
225213
"""Split two coinbase UTxOs into many small coins"""
226-
self.txouts = []
227-
self.txouts2 = []
228-
# Split a coinbase into two transaction puzzle outputs
229-
split_inputs(node, node.listunspent(0), self.txouts, True)
230-
231-
# Mine
214+
utxo_count = 2048
215+
self.confutxo = []
216+
splitted_amount = Decimal("0.04")
217+
fee = Decimal("0.1")
218+
change = Decimal("100") - splitted_amount * utxo_count - fee
219+
tx = CTransaction()
220+
tx.vin = [
221+
CTxIn(COutPoint(int(cb["txid"], 16), cb["vout"]), b"")
222+
for cb in node.listunspent()[:2]
223+
]
224+
tx.vout = [CTxOut(int(splitted_amount * COIN), P2SH) for _ in range(utxo_count)]
225+
tx.vout.append(CTxOut(int(change * COIN), P2SH))
226+
txhex = node.signrawtransactionwithwallet(tx.serialize().hex())["hex"]
227+
txid = node.sendrawtransaction(txhex)
228+
self.confutxo = [
229+
{"txid": txid, "vout": i, "amount": splitted_amount}
230+
for i in range(utxo_count)
231+
]
232232
while len(node.getrawmempool()) > 0:
233233
self.generate(node, 1, sync_fun=self.no_op)
234234

235-
# Repeatedly split those 2 outputs, doubling twice for each rep
236-
# Use txouts to monitor the available utxo, since these won't be tracked in wallet
237-
reps = 0
238-
while reps < 5:
239-
# Double txouts to txouts2
240-
while len(self.txouts) > 0:
241-
split_inputs(node, self.txouts, self.txouts2)
242-
while len(node.getrawmempool()) > 0:
243-
self.generate(node, 1, sync_fun=self.no_op)
244-
# Double txouts2 to txouts
245-
while len(self.txouts2) > 0:
246-
split_inputs(node, self.txouts2, self.txouts)
247-
while len(node.getrawmempool()) > 0:
248-
self.generate(node, 1, sync_fun=self.no_op)
249-
reps += 1
250-
251235
def sanity_check_estimates_range(self):
252236
"""Populate estimation buckets, assert estimates are in a sane range and
253237
are strictly increasing as the target decreases."""
254238
self.fees_per_kb = []
255239
self.memutxo = []
256-
self.confutxo = self.txouts # Start with the set of confirmed txouts after splitting
257240
self.log.info("Will output estimates for 1/2/3/6/15/25 blocks")
258241

259242
for _ in range(2):
260-
self.log.info("Creating transactions and mining them with a block size that can't keep up")
243+
self.log.info(
244+
"Creating transactions and mining them with a block size that can't keep up"
245+
)
261246
# Create transactions and mine 10 small blocks with node 2, but create txs faster than we can mine
262247
self.transact_and_mine(10, self.nodes[2])
263248
check_estimates(self.nodes[1], self.fees_per_kb)
264249

265-
self.log.info("Creating transactions and mining them at a block size that is just big enough")
250+
self.log.info(
251+
"Creating transactions and mining them at a block size that is just big enough"
252+
)
266253
# Generate transactions while mining 10 more blocks, this time with node1
267254
# which mines blocks with capacity just above the rate that transactions are being created
268255
self.transact_and_mine(10, self.nodes[1])
@@ -271,12 +258,13 @@ def sanity_check_estimates_range(self):
271258
# Finish by mining a normal-sized block:
272259
while len(self.nodes[1].getrawmempool()) > 0:
273260
self.generate(self.nodes[1], 1)
261+
274262
self.log.info("Final estimates after emptying mempools")
275263
check_estimates(self.nodes[1], self.fees_per_kb)
276264

277265
def test_feerate_mempoolminfee(self):
278-
high_val = 3*self.nodes[1].estimatesmartfee(1)['feerate']
279-
self.restart_node(1, extra_args=[f'-minrelaytxfee={high_val}'])
266+
high_val = 3 * self.nodes[1].estimatesmartfee(1)["feerate"]
267+
self.restart_node(1, extra_args=[f"-minrelaytxfee={high_val}"])
280268
check_estimates(self.nodes[1], self.fees_per_kb)
281269
self.restart_node(1)
282270

@@ -309,7 +297,7 @@ def sanity_check_rbf_estimates(self, utxos):
309297
for _ in range(5):
310298
send_tx(node, utxos.pop(0), low_feerate)
311299
# Mine the transactions on another node
312-
self.sync_mempools(wait=.1, nodes=[node, miner])
300+
self.sync_mempools(wait=0.1, nodes=[node, miner])
313301
for txid in txids_to_replace:
314302
miner.prioritisetransaction(txid=txid, fee_delta=-COIN)
315303
self.generate(miner, 1)
@@ -322,12 +310,12 @@ def sanity_check_rbf_estimates(self, utxos):
322310
break
323311

324312
# Mine the last replacement txs
325-
self.sync_mempools(wait=.1, nodes=[node, miner])
313+
self.sync_mempools(wait=0.1, nodes=[node, miner])
326314
self.generate(miner, 1)
327315

328316
# Only 10% of the transactions were really confirmed with a low feerate,
329317
# the rest needed to be RBF'd. We must return the 90% conf rate feerate.
330-
high_feerate_kvb = Decimal(high_feerate) / COIN * 10**3
318+
high_feerate_kvb = Decimal(high_feerate) / COIN * 10 ** 3
331319
est_feerate = node.estimatesmartfee(2)["feerate"]
332320
assert est_feerate == high_feerate_kvb
333321

@@ -353,7 +341,9 @@ def run_test(self):
353341
self.sanity_check_estimates_range()
354342

355343
# check that the effective feerate is greater than or equal to the mempoolminfee even for high mempoolminfee
356-
self.log.info("Test fee rate estimation after restarting node with high MempoolMinFee")
344+
self.log.info(
345+
"Test fee rate estimation after restarting node with high MempoolMinFee"
346+
)
357347
self.test_feerate_mempoolminfee()
358348

359349
self.log.info("Restarting node with fresh estimation")
@@ -369,9 +359,10 @@ def run_test(self):
369359

370360
self.log.info("Testing that fee estimation is disabled in blocksonly.")
371361
self.restart_node(0, ["-blocksonly"])
372-
assert_raises_rpc_error(-32603, "Fee estimation disabled",
373-
self.nodes[0].estimatesmartfee, 2)
362+
assert_raises_rpc_error(
363+
-32603, "Fee estimation disabled", self.nodes[0].estimatesmartfee, 2
364+
)
374365

375366

376-
if __name__ == '__main__':
367+
if __name__ == "__main__":
377368
EstimateFeeTest().main()

0 commit comments

Comments
 (0)