diff --git a/test/functional/feature_fee_estimation.py b/test/functional/feature_fee_estimation.py index 68453e50f47..936ca945805 100755 --- a/test/functional/feature_fee_estimation.py +++ b/test/functional/feature_fee_estimation.py @@ -3,39 +3,41 @@ # 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.test_framework import BitcoinTestFramework -from test_framework.util import * -from test_framework.script import CScript, OP_1, OP_DROP, OP_2, OP_HASH160, OP_EQUAL, hash160, OP_TRUE from test_framework.mininode import CTransaction, CTxIn, CTxOut, COutPoint, ToHex, COIN +from test_framework.script import CScript, OP_1, OP_DROP, OP_2, OP_HASH160, OP_EQUAL, hash160, OP_TRUE +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import satoshi_round, sync_mempools, sync_blocks, connect_nodes, assert_greater_than # 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]) +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])] +SCRIPT_SIG = [CScript([OP_TRUE, REDEEM_SCRIPT_1]), CScript([OP_TRUE, REDEEM_SCRIPT_2])] global log def small_txpuzzle_randfee(from_node, conflist, unconflist, amount, min_fee, fee_increment): - """ - Create and send a transaction with a random fee. + """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) - """ + 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)) + 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() @@ -50,83 +52,81 @@ def small_txpuzzle_randfee(from_node, conflist, unconflist, amount, min_fee, fee total_in += t["amount"] tx.vin.append(CTxIn(COutPoint(int(t["txid"], 16), t["vout"]), b"")) if total_in <= amount + fee: - raise RuntimeError("Insufficient funds: need %d, have %d"%(amount+fee, total_in)) - tx.vout.append(CTxOut(int((total_in - amount - fee)*COIN), P2SH_1)) - tx.vout.append(CTxOut(int(amount*COIN), P2SH_2)) + raise RuntimeError("Insufficient funds: need %d, have %d" % (amount + fee, 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(ToHex(tx), True) - unconflist.append({ "txid" : txid, "vout" : 0 , "amount" : total_in - amount - fee}) - unconflist.append({ "txid" : txid, "vout" : 1 , "amount" : amount}) + unconflist.append({"txid": txid, "vout": 0, "amount": total_in - amount - fee}) + unconflist.append({"txid": txid, "vout": 1, "amount": amount}) return (ToHex(tx), fee) -def split_inputs(from_node, txins, txouts, initial_split = False): - """ - We need to generate a lot of inputs so we can generate a ton of transactions. +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. - """ + 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)) + 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) : + if (initial_split): completetx = from_node.signrawtransaction(ToHex(tx))["hex"] - else : + else: tx.vin[0].scriptSig = SCRIPT_SIG[prevtxout["vout"]] completetx = ToHex(tx) txid = from_node.sendrawtransaction(completetx, True) - txouts.append({ "txid" : txid, "vout" : 0 , "amount" : half_change}) - txouts.append({ "txid" : txid, "vout" : 1 , "amount" : rem_change}) + txouts.append({"txid": txid, "vout": 0, "amount": half_change}) + txouts.append({"txid": txid, "vout": 1, "amount": rem_change}) -def check_estimates(node, fees_seen, max_invalid, print_estimates = True): - """ - This function calls estimatefee and verifies that the estimates - meet certain invariants. - """ - all_estimates = [ node.estimatefee(i) for i in range(1,26) ] +def check_estimates(node, fees_seen, max_invalid, print_estimates=True): + """Call estimatefee and verify that the estimates meet certain invariants.""" + + all_estimates = [node.estimatefee(i) for i in range(1, 26)] if print_estimates: - log.info([str(all_estimates[e-1]) for e in [1,2,3,6,15,25]]) - delta = 1.0e-6 # account for rounding error + log.info([str(all_estimates[e - 1]) for e in [1, 2, 3, 6, 15, 25]]) + delta = 1.0e-6 # account for rounding error last_e = max(fees_seen) for e in [x for x in all_estimates if x >= 0]: # Estimates should be within the bounds of what transactions fees actually were: - if float(e)+delta < min(fees_seen) or float(e)-delta > max(fees_seen): + if float(e) + delta < min(fees_seen) or float(e) - delta > max(fees_seen): raise AssertionError("Estimated fee (%f) out of range (%f,%f)" - %(float(e), min(fees_seen), max(fees_seen))) + % (float(e), min(fees_seen), max(fees_seen))) # Estimates should be monotonically decreasing - if float(e)-delta > last_e: + if float(e) - delta > last_e: raise AssertionError("Estimated fee (%f) larger than last fee (%f) for lower number of confirms" - %(float(e),float(last_e))) + % (float(e), float(last_e))) last_e = e valid_estimate = False invalid_estimates = 0 - for i,e in enumerate(all_estimates): # estimate is for i+1 + for i, e in enumerate(all_estimates): # estimate is for i+1 if e >= 0: valid_estimate = True if i >= 13: # for n>=14 estimatesmartfee(n/2) should be at least as high as estimatefee(n) - assert(node.estimatesmartfee((i+1)//2)["feerate"] > float(e) - delta) + assert_greater_than(node.estimatesmartfee((i + 1) // 2)["feerate"], float(e) - delta) else: invalid_estimates += 1 # estimatesmartfee should still be valid - approx_estimate = node.estimatesmartfee(i+1)["feerate"] - answer_found = node.estimatesmartfee(i+1)["blocks"] - assert(approx_estimate > 0) - assert(answer_found > i+1) + approx_estimate = node.estimatesmartfee(i + 1)["feerate"] + answer_found = node.estimatesmartfee(i + 1)["blocks"] + assert_greater_than(approx_estimate, 0) + assert_greater_than(answer_found, i + 1) # Once we're at a high enough confirmation count that we can give an estimate # We should have estimates for all higher confirmation counts @@ -136,7 +136,7 @@ def check_estimates(node, fees_seen, max_invalid, print_estimates = True): # Check on the expected number of different confirmation counts # that we might not have valid estimates for if invalid_estimates > max_invalid: - raise AssertionError("More than (%d) invalid estimates"%(max_invalid)) + raise AssertionError("More than (%d) invalid estimates" % (max_invalid)) return all_estimates @@ -160,7 +160,6 @@ class EstimateFeeTest(BitcoinTestFramework): # Node2 is a stingy miner, that # produces too small blocks (room for only 55 or so transactions) - def transact_and_mine(self, numblocks, mining_node): min_fee = Decimal("0.00001") # We will now mine numblocks blocks generating on average 100 transactions between each block @@ -169,14 +168,14 @@ class EstimateFeeTest(BitcoinTestFramework): # resorting to tx's that depend on the mempool when those run out for i in range(numblocks): random.shuffle(self.confutxo) - for j in range(random.randrange(100-50,100+50)): - from_index = random.randint(1,2) + for j 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.fees_per_kb.append(float(fee) / tx_kbytes) sync_mempools(self.nodes[0:3], wait=.1) - mined = mining_node.getblock(mining_node.generate(1)[0],True)["tx"] + mined = mining_node.getblock(mining_node.generate(1)[0], True)["tx"] sync_blocks(self.nodes[0:3], wait=.1) # update which txouts are confirmed newmem = [] @@ -210,13 +209,13 @@ class EstimateFeeTest(BitcoinTestFramework): # 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): + # 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.nodes[0].generate(1) - #Double txouts2 to txouts - while (len(self.txouts2)>0): + # 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.nodes[0].generate(1) @@ -235,7 +234,7 @@ class EstimateFeeTest(BitcoinTestFramework): self.fees_per_kb = [] self.memutxo = [] - self.confutxo = self.txouts # Start with the set of confirmed txouts after splitting + 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 i in range(2):