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 <darosior@protonmail.com>
This commit is contained in:
Antoine Poinsot 2021-09-20 23:11:10 +02:00
parent cc204b8be7
commit 1fc03155e5
No known key found for this signature in database
GPG key ID: E13FC145CD3F4304

View file

@ -40,7 +40,9 @@ P2SH = script_to_p2sh_script(SCRIPT)
REDEEM_SCRIPT = CScript([OP_TRUE, SCRIPT])
def small_txpuzzle_randfee(from_node, conflist, unconflist, amount, min_fee, fee_increment):
def small_txpuzzle_randfee(
from_node, conflist, unconflist, amount, min_fee, fee_increment
):
"""Create and send a transaction with a random fee.
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
return (tx.serialize().hex(), fee)
def split_inputs(from_node, txins, txouts, initial_split=False):
"""Generate a lot of inputs so we can generate a ton of transactions.
This function takes an input from txins, and creates and sends a transaction
which splits the value into 2 outputs which are appended to txouts.
Previously this was designed to be small inputs so they wouldn't have
a high coin age when the notion of priority still existed."""
prevtxout = txins.pop()
tx = CTransaction()
tx.vin.append(CTxIn(COutPoint(int(prevtxout["txid"], 16), prevtxout["vout"]), b""))
half_change = satoshi_round(prevtxout["amount"] / 2)
rem_change = prevtxout["amount"] - half_change - Decimal("0.00001000")
tx.vout.append(CTxOut(int(half_change * COIN), P2SH))
tx.vout.append(CTxOut(int(rem_change * COIN), P2SH))
# If this is the initial split we actually need to sign the transaction
# Otherwise we just need to insert the proper ScriptSig
if (initial_split):
completetx = from_node.signrawtransactionwithwallet(tx.serialize().hex())["hex"]
else:
tx.vin[0].scriptSig = REDEEM_SCRIPT
completetx = tx.serialize().hex()
txid = from_node.sendrawtransaction(hexstring=completetx, maxfeerate=0)
txouts.append({"txid": txid, "vout": 0, "amount": half_change})
txouts.append({"txid": txid, "vout": 1, "amount": rem_change})
def check_raw_estimates(node, fees_seen):
"""Call estimaterawfee and verify that the estimates meet certain invariants."""
@ -119,7 +93,10 @@ def check_raw_estimates(node, fees_seen):
assert_greater_than(feerate, 0)
if feerate + delta < min(fees_seen) or feerate - delta > max(fees_seen):
raise AssertionError(f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})")
raise AssertionError(
f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})"
)
def check_smart_estimates(node, fees_seen):
"""Call estimatesmartfee and verify that the estimates meet certain invariants."""
@ -127,8 +104,8 @@ def check_smart_estimates(node, fees_seen):
delta = 1.0e-6 # account for rounding error
last_feerate = float(max(fees_seen))
all_smart_estimates = [node.estimatesmartfee(i) for i in range(1, 26)]
mempoolMinFee = node.getmempoolinfo()['mempoolminfee']
minRelaytxFee = node.getmempoolinfo()['minrelaytxfee']
mempoolMinFee = node.getmempoolinfo()["mempoolminfee"]
minRelaytxFee = node.getmempoolinfo()["minrelaytxfee"]
for i, e in enumerate(all_smart_estimates): # estimate is for i+1
feerate = float(e["feerate"])
assert_greater_than(feerate, 0)
@ -136,9 +113,13 @@ def check_smart_estimates(node, fees_seen):
assert_greater_than_or_equal(feerate, float(minRelaytxFee))
if feerate + delta < min(fees_seen) or feerate - delta > max(fees_seen):
raise AssertionError(f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})")
raise AssertionError(
f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})"
)
if feerate - delta > last_feerate:
raise AssertionError(f"Estimated fee ({feerate}) larger than last fee ({last_feerate}) for lower number of confirms")
raise AssertionError(
f"Estimated fee ({feerate}) larger than last fee ({last_feerate}) for lower number of confirms"
)
last_feerate = feerate
if i == 0:
@ -146,6 +127,7 @@ def check_smart_estimates(node, fees_seen):
else:
assert_greater_than_or_equal(i + 1, e["blocks"])
def check_estimates(node, fees_seen):
check_raw_estimates(node, fees_seen)
check_smart_estimates(node, fees_seen)
@ -206,11 +188,17 @@ class EstimateFeeTest(BitcoinTestFramework):
random.shuffle(self.confutxo)
for _ in range(random.randrange(100 - 50, 100 + 50)):
from_index = random.randint(1, 2)
(txhex, fee) = small_txpuzzle_randfee(self.nodes[from_index], self.confutxo,
self.memutxo, Decimal("0.005"), min_fee, min_fee)
(txhex, fee) = small_txpuzzle_randfee(
self.nodes[from_index],
self.confutxo,
self.memutxo,
Decimal("0.005"),
min_fee,
min_fee,
)
tx_kbytes = (len(txhex) // 2) / 1000.0
self.fees_per_kb.append(float(fee) / tx_kbytes)
self.sync_mempools(wait=.1)
self.sync_mempools(wait=0.1)
mined = mining_node.getblock(self.generate(mining_node, 1)[0], True)["tx"]
# update which txouts are confirmed
newmem = []
@ -223,46 +211,45 @@ class EstimateFeeTest(BitcoinTestFramework):
def initial_split(self, node):
"""Split two coinbase UTxOs into many small coins"""
self.txouts = []
self.txouts2 = []
# Split a coinbase into two transaction puzzle outputs
split_inputs(node, node.listunspent(0), self.txouts, True)
# Mine
utxo_count = 2048
self.confutxo = []
splitted_amount = Decimal("0.04")
fee = Decimal("0.1")
change = Decimal("100") - splitted_amount * utxo_count - fee
tx = CTransaction()
tx.vin = [
CTxIn(COutPoint(int(cb["txid"], 16), cb["vout"]), b"")
for cb in node.listunspent()[:2]
]
tx.vout = [CTxOut(int(splitted_amount * COIN), P2SH) for _ in range(utxo_count)]
tx.vout.append(CTxOut(int(change * COIN), P2SH))
txhex = node.signrawtransactionwithwallet(tx.serialize().hex())["hex"]
txid = node.sendrawtransaction(txhex)
self.confutxo = [
{"txid": txid, "vout": i, "amount": splitted_amount}
for i in range(utxo_count)
]
while len(node.getrawmempool()) > 0:
self.generate(node, 1, sync_fun=self.no_op)
# Repeatedly split those 2 outputs, doubling twice for each rep
# Use txouts to monitor the available utxo, since these won't be tracked in wallet
reps = 0
while reps < 5:
# Double txouts to txouts2
while len(self.txouts) > 0:
split_inputs(node, self.txouts, self.txouts2)
while len(node.getrawmempool()) > 0:
self.generate(node, 1, sync_fun=self.no_op)
# Double txouts2 to txouts
while len(self.txouts2) > 0:
split_inputs(node, self.txouts2, self.txouts)
while len(node.getrawmempool()) > 0:
self.generate(node, 1, sync_fun=self.no_op)
reps += 1
def sanity_check_estimates_range(self):
"""Populate estimation buckets, assert estimates are in a sane range and
are strictly increasing as the target decreases."""
self.fees_per_kb = []
self.memutxo = []
self.confutxo = self.txouts # Start with the set of confirmed txouts after splitting
self.log.info("Will output estimates for 1/2/3/6/15/25 blocks")
for _ in range(2):
self.log.info("Creating transactions and mining them with a block size that can't keep up")
self.log.info(
"Creating transactions and mining them with a block size that can't keep up"
)
# Create transactions and mine 10 small blocks with node 2, but create txs faster than we can mine
self.transact_and_mine(10, self.nodes[2])
check_estimates(self.nodes[1], self.fees_per_kb)
self.log.info("Creating transactions and mining them at a block size that is just big enough")
self.log.info(
"Creating transactions and mining them at a block size that is just big enough"
)
# Generate transactions while mining 10 more blocks, this time with node1
# which mines blocks with capacity just above the rate that transactions are being created
self.transact_and_mine(10, self.nodes[1])
@ -271,12 +258,13 @@ class EstimateFeeTest(BitcoinTestFramework):
# Finish by mining a normal-sized block:
while len(self.nodes[1].getrawmempool()) > 0:
self.generate(self.nodes[1], 1)
self.log.info("Final estimates after emptying mempools")
check_estimates(self.nodes[1], self.fees_per_kb)
def test_feerate_mempoolminfee(self):
high_val = 3*self.nodes[1].estimatesmartfee(1)['feerate']
self.restart_node(1, extra_args=[f'-minrelaytxfee={high_val}'])
high_val = 3 * self.nodes[1].estimatesmartfee(1)["feerate"]
self.restart_node(1, extra_args=[f"-minrelaytxfee={high_val}"])
check_estimates(self.nodes[1], self.fees_per_kb)
self.restart_node(1)
@ -309,7 +297,7 @@ class EstimateFeeTest(BitcoinTestFramework):
for _ in range(5):
send_tx(node, utxos.pop(0), low_feerate)
# Mine the transactions on another node
self.sync_mempools(wait=.1, nodes=[node, miner])
self.sync_mempools(wait=0.1, nodes=[node, miner])
for txid in txids_to_replace:
miner.prioritisetransaction(txid=txid, fee_delta=-COIN)
self.generate(miner, 1)
@ -322,12 +310,12 @@ class EstimateFeeTest(BitcoinTestFramework):
break
# Mine the last replacement txs
self.sync_mempools(wait=.1, nodes=[node, miner])
self.sync_mempools(wait=0.1, nodes=[node, miner])
self.generate(miner, 1)
# Only 10% of the transactions were really confirmed with a low feerate,
# the rest needed to be RBF'd. We must return the 90% conf rate feerate.
high_feerate_kvb = Decimal(high_feerate) / COIN * 10**3
high_feerate_kvb = Decimal(high_feerate) / COIN * 10 ** 3
est_feerate = node.estimatesmartfee(2)["feerate"]
assert est_feerate == high_feerate_kvb
@ -353,7 +341,9 @@ class EstimateFeeTest(BitcoinTestFramework):
self.sanity_check_estimates_range()
# check that the effective feerate is greater than or equal to the mempoolminfee even for high mempoolminfee
self.log.info("Test fee rate estimation after restarting node with high MempoolMinFee")
self.log.info(
"Test fee rate estimation after restarting node with high MempoolMinFee"
)
self.test_feerate_mempoolminfee()
self.log.info("Restarting node with fresh estimation")
@ -369,9 +359,10 @@ class EstimateFeeTest(BitcoinTestFramework):
self.log.info("Testing that fee estimation is disabled in blocksonly.")
self.restart_node(0, ["-blocksonly"])
assert_raises_rpc_error(-32603, "Fee estimation disabled",
self.nodes[0].estimatesmartfee, 2)
assert_raises_rpc_error(
-32603, "Fee estimation disabled", self.nodes[0].estimatesmartfee, 2
)
if __name__ == '__main__':
if __name__ == "__main__":
EstimateFeeTest().main()