mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-25 10:43:19 -03:00
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:
parent
cc204b8be7
commit
1fc03155e5
1 changed files with 62 additions and 71 deletions
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue