mirror of
https://github.com/dashpay/dash.git
synced 2024-12-27 13:03:17 +01:00
65226da849
68faa87881f5334b2528db4adc72ec19d94316a3 test: use f-strings in mining_*.py tests (fanquake) c2a5d560df2824df5731100c2584e8ad7a3d7bc2 test: use f-strings in interface_*.py tests (fanquake) 86d958262dff43002820d58ccb8958e2dbfb9d5b test: use f-strings in feature_proxy.py (fanquake) 31bdb33dcb8345df1bb94b28e811252a918d7dcb test: use f-strings in feature_segwit.py (fanquake) b166d54c3cbb0c028210cee977b3dcde5ac5474f test: use f-strings in feature_versionbits_warning.py (fanquake) cf6d66bf941d946600047d712c7cd15d7605322e test: use f-strings in feature_settings.py (fanquake) 6651d77f22862716f5bd7d0b31cfbd3937ab7b1d test: use f-strings in feature_pruning.py (fanquake) 961f5813ba65b6a601081912c4ece96c2679794d test: use f-strings in feature_notifications.py (fanquake) 1a546e6f6ca95772f0d7dbc2792477becbb8ea63 test: use f-strings in feature_minchainwork.py (fanquake) 6679eceacc915a8ea7cd7063f103ffc5eb9da884 test: use f-strings in feature_logging.py (fanquake) fb633933ab570e945d2a366f37eeff39f516c613 test: use f-strings in feature_loadblock.py (fanquake) e9ca8b254d4b9567831c0e113ce1c0a2b4795a95 test: use f-strings in feature_help.py (fanquake) ff7e3309995a8960ac371741b2b00c6da40f7490 test: use f-strings in feature_filelock.py (fanquake) d5a6adc5e478fa5c6e562377eea873dc38e66578 test: use f-strings in feature_fee_estimation.py (fanquake) a2de33cbdc79202bccddb4beadfde88266ac979f test: use f-strings in feature_dersig.py (fanquake) a2502cc63fd308be8af840962da9c53339433fa6 test: use f-strings in feature_dbcrash.py (fanquake) 3e2f84e7a96cb4b97b609ac853f78edd0ed43f82 test: use f-strings in feature_csv_activation.py (fanquake) e2f1fd8ee92fa421b6d293169044d6ddd5a9b8df test: use f-strings in feature_config_args.py (fanquake) 36d33d32b1b498b61f56d552f6e2c1d064f978c3 test: use f-strings in feature_cltv.py (fanquake) dca173cc044270b30782b1e3355e9dcb8c534295 test: use f-strings in feature_blocksdir.py (fanquake) 5453e8706278918ac51a725e81599cfa18c8cdbc test: use f-strings in feature_backwards_compatibility.py (fanquake) 6f3d5ad67ac8e7b50abae1a2949898d858e38106 test: use f-strings in feature_asmap.py (fanquake) Pull request description: Rather than using 3 different ways to build/format strings (sometimes all in the same test, i.e [`feature_config_args.py`](https://github.com/bitcoin/bitcoin/blob/master/test/functional/feature_config_args.py)), consolidate to using [f-strings (3.6+)](https://docs.python.org/3/reference/lexical_analysis.html#f-strings), which are generally more concise / readable, as well as more performant than existing methods. This deals with the `feature_*.py`, `interface_*.py` and `mining_*.py` tests. See also: [PEP 498](https://www.python.org/dev/peps/pep-0498/) ACKs for top commit: mjdietzx: reACK 68faa87881f5334b2528db4adc72ec19d94316a3 Zero-1729: crACK 68faa87881f5334b2528db4adc72ec19d94316a3 Tree-SHA512: d4e1a42e07d96d2c552387a46da1534223c4ce408703d7568ad2ef580797dd68d9695b8d19666b567af37f44de6e430e8be5db5d5404ba8fcecf9f5b026a6efb
300 lines
13 KiB
Python
Executable File
300 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright (c) 2014-2020 The Bitcoin Core developers
|
|
# Distributed under the MIT software license, see the accompanying
|
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
"""Test fee estimation code."""
|
|
from decimal import Decimal
|
|
import random
|
|
|
|
from test_framework.messages import (
|
|
COIN,
|
|
COutPoint,
|
|
CTransaction,
|
|
CTxIn,
|
|
CTxOut,
|
|
)
|
|
from test_framework.script import (
|
|
CScript,
|
|
OP_1,
|
|
OP_2,
|
|
OP_DROP,
|
|
OP_EQUAL,
|
|
OP_HASH160,
|
|
OP_TRUE,
|
|
hash160,
|
|
)
|
|
from test_framework.test_framework import BitcoinTestFramework
|
|
from test_framework.util import (
|
|
assert_equal,
|
|
assert_greater_than,
|
|
assert_greater_than_or_equal,
|
|
assert_raises_rpc_error,
|
|
satoshi_round,
|
|
)
|
|
|
|
# Construct 2 trivial P2SH's and the ScriptSigs that spend them
|
|
# So we can create many transactions without needing to spend
|
|
# time signing.
|
|
REDEEM_SCRIPT_1 = CScript([OP_1, OP_DROP])
|
|
REDEEM_SCRIPT_2 = CScript([OP_2, OP_DROP])
|
|
P2SH_1 = CScript([OP_HASH160, hash160(REDEEM_SCRIPT_1), OP_EQUAL])
|
|
P2SH_2 = CScript([OP_HASH160, hash160(REDEEM_SCRIPT_2), OP_EQUAL])
|
|
|
|
# Associated ScriptSig's to spend satisfy P2SH_1 and P2SH_2
|
|
SCRIPT_SIG = [CScript([OP_TRUE, REDEEM_SCRIPT_1]), CScript([OP_TRUE, REDEEM_SCRIPT_2])]
|
|
|
|
|
|
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
|
|
are of the same form.
|
|
The function takes a list of confirmed outputs and unconfirmed outputs
|
|
and attempts to use the confirmed list first for its inputs.
|
|
It adds the newly created outputs to the unconfirmed list.
|
|
Returns (raw transaction, fee)."""
|
|
|
|
# It's best to exponentially distribute our random fees
|
|
# because the buckets are exponentially spaced.
|
|
# Exponentially distributed from 1-128 * fee_increment
|
|
rand_fee = float(fee_increment) * (1.1892 ** random.randint(0, 28))
|
|
# Total fee ranges from min_fee to min_fee + 127*fee_increment
|
|
fee = min_fee - fee_increment + satoshi_round(rand_fee)
|
|
tx = CTransaction()
|
|
total_in = Decimal("0.00000000")
|
|
while total_in <= (amount + fee) and len(conflist) > 0:
|
|
t = conflist.pop(0)
|
|
total_in += t["amount"]
|
|
tx.vin.append(CTxIn(COutPoint(int(t["txid"], 16), t["vout"]), b""))
|
|
if total_in <= amount + fee:
|
|
while total_in <= (amount + fee) and len(unconflist) > 0:
|
|
t = unconflist.pop(0)
|
|
total_in += t["amount"]
|
|
tx.vin.append(CTxIn(COutPoint(int(t["txid"], 16), t["vout"]), b""))
|
|
if total_in <= amount + fee:
|
|
raise RuntimeError(f"Insufficient funds: need {amount + fee}, have {total_in}")
|
|
tx.vout.append(CTxOut(int((total_in - amount - fee) * COIN), P2SH_1))
|
|
tx.vout.append(CTxOut(int(amount * COIN), P2SH_2))
|
|
# These transactions don't need to be signed, but we still have to insert
|
|
# the ScriptSig that will satisfy the ScriptPubKey.
|
|
for inp in tx.vin:
|
|
inp.scriptSig = SCRIPT_SIG[inp.prevout.n]
|
|
txid = from_node.sendrawtransaction(hexstring=tx.serialize().hex(), maxfeerate=0)
|
|
unconflist.append({"txid": txid, "vout": 0, "amount": total_in - amount - fee})
|
|
unconflist.append({"txid": txid, "vout": 1, "amount": amount})
|
|
|
|
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_1))
|
|
tx.vout.append(CTxOut(int(rem_change * COIN), P2SH_2))
|
|
|
|
# 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 = SCRIPT_SIG[prevtxout["vout"]]
|
|
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."""
|
|
|
|
delta = 1.0e-6 # account for rounding error
|
|
for i in range(1, 26):
|
|
for _, e in node.estimaterawfee(i).items():
|
|
feerate = float(e["feerate"])
|
|
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)})")
|
|
|
|
|
|
def check_smart_estimates(node, fees_seen):
|
|
"""Call estimatesmartfee and verify that the estimates meet certain invariants."""
|
|
|
|
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']
|
|
for i, e in enumerate(all_smart_estimates): # estimate is for i+1
|
|
feerate = float(e["feerate"])
|
|
assert_greater_than(feerate, 0)
|
|
assert_greater_than_or_equal(feerate, float(mempoolMinFee))
|
|
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)})")
|
|
if feerate - delta > last_feerate:
|
|
raise AssertionError(f"Estimated fee ({feerate}) larger than last fee ({last_feerate}) for lower number of confirms")
|
|
last_feerate = feerate
|
|
|
|
if i == 0:
|
|
assert_equal(e["blocks"], 2)
|
|
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)
|
|
|
|
class EstimateFeeTest(BitcoinTestFramework):
|
|
def set_test_params(self):
|
|
self.num_nodes = 3
|
|
# mine non-standard txs (e.g. txs with "dust" outputs)
|
|
# Force fSendTrickle to true (via whitelist)
|
|
self.extra_args = [
|
|
["-acceptnonstdtxn=1", "-whitelist=noban@127.0.0.1"],
|
|
["-acceptnonstdtxn=1", "-whitelist=noban@127.0.0.1", "-blockmaxsize=17000"],
|
|
["-acceptnonstdtxn=1", "-whitelist=noban@127.0.0.1", "-blockmaxsize=8000"]
|
|
]
|
|
|
|
def skip_test_if_missing_module(self):
|
|
self.skip_if_no_wallet()
|
|
|
|
def setup_network(self):
|
|
"""
|
|
We'll setup the network to have 3 nodes that all mine with different parameters.
|
|
But first we need to use one node to create a lot of outputs
|
|
which we will use to generate our transactions.
|
|
"""
|
|
self.add_nodes(3, extra_args=self.extra_args)
|
|
# Use node0 to mine blocks for input splitting
|
|
# Node1 mines small blocks but that are bigger than the expected transaction rate.
|
|
# NOTE: the CreateNewBlock code starts counting block size at 1,000 bytes,
|
|
# (17k is room enough for 110 or so transactions)
|
|
# Node2 is a stingy miner, that
|
|
# produces too small blocks (room for only 55 or so transactions)
|
|
self.start_nodes()
|
|
self.import_deterministic_coinbase_privkeys()
|
|
self.stop_nodes()
|
|
|
|
def transact_and_mine(self, numblocks, mining_node):
|
|
min_fee = Decimal("0.0001")
|
|
# We will now mine numblocks blocks generating on average 100 transactions between each block
|
|
# We shuffle our confirmed txout set before each set of transactions
|
|
# small_txpuzzle_randfee will use the transactions that have inputs already in the chain when possible
|
|
# resorting to tx's that depend on the mempool when those run out
|
|
for _ in range(numblocks):
|
|
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)
|
|
tx_kbytes = (len(txhex) // 2) / 1000.0
|
|
self.fees_per_kb.append(float(fee) / tx_kbytes)
|
|
self.sync_mempools(wait=.1)
|
|
mined = mining_node.getblock(self.generate(mining_node, 1)[0], True)["tx"]
|
|
self.sync_blocks(wait=.1)
|
|
# update which txouts are confirmed
|
|
newmem = []
|
|
for utx in self.memutxo:
|
|
if utx["txid"] in mined:
|
|
self.confutxo.append(utx)
|
|
else:
|
|
newmem.append(utx)
|
|
self.memutxo = newmem
|
|
|
|
def run_test(self):
|
|
self.log.info("This test is time consuming, please be patient")
|
|
self.log.info("Splitting inputs so we can generate tx's")
|
|
|
|
# Start node0
|
|
self.start_node(0)
|
|
self.txouts = []
|
|
self.txouts2 = []
|
|
# Split a coinbase into two transaction puzzle outputs
|
|
split_inputs(self.nodes[0], self.nodes[0].listunspent(0), self.txouts, True)
|
|
|
|
# Mine
|
|
while len(self.nodes[0].getrawmempool()) > 0:
|
|
self.generate(self.nodes[0], 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(self.nodes[0], self.txouts, self.txouts2)
|
|
while len(self.nodes[0].getrawmempool()) > 0:
|
|
self.generate(self.nodes[0], 1, sync_fun=self.no_op)
|
|
# Double txouts2 to txouts
|
|
while len(self.txouts2) > 0:
|
|
split_inputs(self.nodes[0], self.txouts2, self.txouts)
|
|
while len(self.nodes[0].getrawmempool()) > 0:
|
|
self.generate(self.nodes[0], 1, sync_fun=self.no_op)
|
|
reps += 1
|
|
self.log.info("Finished splitting")
|
|
|
|
# Now we can connect the other nodes, didn't want to connect them earlier
|
|
# so the estimates would not be affected by the splitting transactions
|
|
self.start_node(1)
|
|
self.start_node(2)
|
|
self.connect_nodes(1, 0)
|
|
self.connect_nodes(0, 2)
|
|
self.connect_nodes(2, 1)
|
|
|
|
self.sync_all()
|
|
|
|
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")
|
|
# 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")
|
|
# 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])
|
|
check_estimates(self.nodes[1], self.fees_per_kb)
|
|
|
|
# Finish by mining a normal-sized block:
|
|
while len(self.nodes[1].getrawmempool()) > 0:
|
|
self.generate(self.nodes[1], 1)
|
|
|
|
self.sync_blocks(self.nodes[0:3], wait=.1)
|
|
self.log.info("Final estimates after emptying mempools")
|
|
check_estimates(self.nodes[1], self.fees_per_kb)
|
|
|
|
# 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")
|
|
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.stop_node(1, expected_stderr="Warning: -minrelaytxfee is set very high! The wallet will avoid paying less than the minimum relay fee.")
|
|
|
|
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)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
EstimateFeeTest().main()
|