171ca7745e
New RPC methods: return an estimate of the fee (or priority) a transaction needs to be likely to confirm in a given number of blocks. Mike Hearn created the first version of this method for estimating fees. It works as follows: For transactions that took 1 to N (I picked N=25) blocks to confirm, keep N buckets with at most 100 entries in each recording the fees-per-kilobyte paid by those transactions. (separate buckets are kept for transactions that confirmed because they are high-priority) The buckets are filled as blocks are found, and are saved/restored in a new fee_estiamtes.dat file in the data directory. A few variations on Mike's initial scheme: To estimate the fee needed for a transaction to confirm in X buckets, all of the samples in all of the buckets are used and a median of all of the data is used to make the estimate. For example, imagine 25 buckets each containing the full 100 entries. Those 2,500 samples are sorted, and the estimate of the fee needed to confirm in the very next block is the 50'th-highest-fee-entry in that sorted list; the estimate of the fee needed to confirm in the next two blocks is the 150'th-highest-fee-entry, etc. That algorithm has the nice property that estimates of how much fee you need to pay to get confirmed in block N will always be greater than or equal to the estimate for block N+1. It would clearly be wrong to say "pay 11 uBTC and you'll get confirmed in 3 blocks, but pay 12 uBTC and it will take LONGER". A single block will not contribute more than 10 entries to any one bucket, so a single miner and a large block cannot overwhelm the estimates.
290 lines
9.9 KiB
Python
290 lines
9.9 KiB
Python
# Copyright (c) 2014 The Bitcoin Core developers
|
|
# Distributed under the MIT/X11 software license, see the accompanying
|
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
#
|
|
# Helpful routines for regression testing
|
|
#
|
|
|
|
# Add python-bitcoinrpc to module search path:
|
|
import os
|
|
import sys
|
|
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "python-bitcoinrpc"))
|
|
|
|
from decimal import Decimal
|
|
import json
|
|
import random
|
|
import shutil
|
|
import subprocess
|
|
import time
|
|
import re
|
|
|
|
from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException
|
|
from util import *
|
|
|
|
def p2p_port(n):
|
|
return 11000 + n + os.getpid()%999
|
|
def rpc_port(n):
|
|
return 12000 + n + os.getpid()%999
|
|
|
|
def check_json_precision():
|
|
"""Make sure json library being used does not lose precision converting BTC values"""
|
|
n = Decimal("20000000.00000003")
|
|
satoshis = int(json.loads(json.dumps(float(n)))*1.0e8)
|
|
if satoshis != 2000000000000003:
|
|
raise RuntimeError("JSON encode/decode loses precision")
|
|
|
|
def sync_blocks(rpc_connections):
|
|
"""
|
|
Wait until everybody has the same block count
|
|
"""
|
|
while True:
|
|
counts = [ x.getblockcount() for x in rpc_connections ]
|
|
if counts == [ counts[0] ]*len(counts):
|
|
break
|
|
time.sleep(1)
|
|
|
|
def sync_mempools(rpc_connections):
|
|
"""
|
|
Wait until everybody has the same transactions in their memory
|
|
pools
|
|
"""
|
|
while True:
|
|
pool = set(rpc_connections[0].getrawmempool())
|
|
num_match = 1
|
|
for i in range(1, len(rpc_connections)):
|
|
if set(rpc_connections[i].getrawmempool()) == pool:
|
|
num_match = num_match+1
|
|
if num_match == len(rpc_connections):
|
|
break
|
|
time.sleep(1)
|
|
|
|
|
|
bitcoind_processes = []
|
|
|
|
def initialize_datadir(dir, n):
|
|
datadir = os.path.join(dir, "node"+str(n))
|
|
if not os.path.isdir(datadir):
|
|
os.makedirs(datadir)
|
|
with open(os.path.join(datadir, "bitcoin.conf"), 'w') as f:
|
|
f.write("regtest=1\n");
|
|
f.write("rpcuser=rt\n");
|
|
f.write("rpcpassword=rt\n");
|
|
f.write("port="+str(p2p_port(n))+"\n");
|
|
f.write("rpcport="+str(rpc_port(n))+"\n");
|
|
return datadir
|
|
|
|
def initialize_chain(test_dir):
|
|
"""
|
|
Create (or copy from cache) a 200-block-long chain and
|
|
4 wallets.
|
|
bitcoind and bitcoin-cli must be in search path.
|
|
"""
|
|
|
|
if not os.path.isdir(os.path.join("cache", "node0")):
|
|
devnull = open("/dev/null", "w+")
|
|
# Create cache directories, run bitcoinds:
|
|
for i in range(4):
|
|
datadir=initialize_datadir("cache", i)
|
|
args = [ "bitcoind", "-keypool=1", "-datadir="+datadir ]
|
|
if i > 0:
|
|
args.append("-connect=127.0.0.1:"+str(p2p_port(0)))
|
|
bitcoind_processes.append(subprocess.Popen(args))
|
|
subprocess.check_call([ "bitcoin-cli", "-datadir="+datadir,
|
|
"-rpcwait", "getblockcount"], stdout=devnull)
|
|
devnull.close()
|
|
rpcs = []
|
|
for i in range(4):
|
|
try:
|
|
url = "http://rt:rt@127.0.0.1:%d"%(rpc_port(i),)
|
|
rpcs.append(AuthServiceProxy(url))
|
|
except:
|
|
sys.stderr.write("Error connecting to "+url+"\n")
|
|
sys.exit(1)
|
|
|
|
# Create a 200-block-long chain; each of the 4 nodes
|
|
# gets 25 mature blocks and 25 immature.
|
|
for i in range(4):
|
|
rpcs[i].setgenerate(True, 25)
|
|
sync_blocks(rpcs)
|
|
for i in range(4):
|
|
rpcs[i].setgenerate(True, 25)
|
|
sync_blocks(rpcs)
|
|
|
|
# Shut them down, and remove debug.logs:
|
|
stop_nodes(rpcs)
|
|
wait_bitcoinds()
|
|
for i in range(4):
|
|
os.remove(debug_log("cache", i))
|
|
|
|
for i in range(4):
|
|
from_dir = os.path.join("cache", "node"+str(i))
|
|
to_dir = os.path.join(test_dir, "node"+str(i))
|
|
shutil.copytree(from_dir, to_dir)
|
|
initialize_datadir(test_dir, i) # Overwrite port/rpcport in bitcoin.conf
|
|
|
|
def _rpchost_to_args(rpchost):
|
|
'''Convert optional IP:port spec to rpcconnect/rpcport args'''
|
|
if rpchost is None:
|
|
return []
|
|
|
|
match = re.match('(\[[0-9a-fA-f:]+\]|[^:]+)(?::([0-9]+))?$', rpchost)
|
|
if not match:
|
|
raise ValueError('Invalid RPC host spec ' + rpchost)
|
|
|
|
rpcconnect = match.group(1)
|
|
rpcport = match.group(2)
|
|
|
|
if rpcconnect.startswith('['): # remove IPv6 [...] wrapping
|
|
rpcconnect = rpcconnect[1:-1]
|
|
|
|
rv = ['-rpcconnect=' + rpcconnect]
|
|
if rpcport:
|
|
rv += ['-rpcport=' + rpcport]
|
|
return rv
|
|
|
|
def start_node(i, dir, extra_args=None, rpchost=None):
|
|
"""
|
|
Start a bitcoind and return RPC connection to it
|
|
"""
|
|
datadir = os.path.join(dir, "node"+str(i))
|
|
args = [ "bitcoind", "-datadir="+datadir, "-keypool=1" ]
|
|
if extra_args is not None: args.extend(extra_args)
|
|
bitcoind_processes.append(subprocess.Popen(args))
|
|
devnull = open("/dev/null", "w+")
|
|
subprocess.check_call([ "bitcoin-cli", "-datadir="+datadir] +
|
|
_rpchost_to_args(rpchost) +
|
|
["-rpcwait", "getblockcount"], stdout=devnull)
|
|
devnull.close()
|
|
url = "http://rt:rt@%s:%d" % (rpchost or '127.0.0.1', rpc_port(i))
|
|
return AuthServiceProxy(url)
|
|
|
|
def start_nodes(num_nodes, dir, extra_args=None, rpchost=None):
|
|
"""
|
|
Start multiple bitcoinds, return RPC connections to them
|
|
"""
|
|
if extra_args is None: extra_args = [ None for i in range(num_nodes) ]
|
|
return [ start_node(i, dir, extra_args[i], rpchost) for i in range(num_nodes) ]
|
|
|
|
def debug_log(dir, n_node):
|
|
return os.path.join(dir, "node"+str(n_node), "regtest", "debug.log")
|
|
|
|
def stop_nodes(nodes):
|
|
for i in range(len(nodes)):
|
|
nodes[i].stop()
|
|
del nodes[:] # Emptying array closes connections as a side effect
|
|
|
|
def wait_bitcoinds():
|
|
# Wait for all bitcoinds to cleanly exit
|
|
for bitcoind in bitcoind_processes:
|
|
bitcoind.wait()
|
|
del bitcoind_processes[:]
|
|
|
|
def connect_nodes(from_connection, node_num):
|
|
ip_port = "127.0.0.1:"+str(p2p_port(node_num))
|
|
from_connection.addnode(ip_port, "onetry")
|
|
|
|
def find_output(node, txid, amount):
|
|
"""
|
|
Return index to output of txid with value amount
|
|
Raises exception if there is none.
|
|
"""
|
|
txdata = node.getrawtransaction(txid, 1)
|
|
for i in range(len(txdata["vout"])):
|
|
if txdata["vout"][i]["value"] == amount:
|
|
return i
|
|
raise RuntimeError("find_output txid %s : %s not found"%(txid,str(amount)))
|
|
|
|
def gather_inputs(from_node, amount_needed):
|
|
"""
|
|
Return a random set of unspent txouts that are enough to pay amount_needed
|
|
"""
|
|
utxo = from_node.listunspent(1)
|
|
random.shuffle(utxo)
|
|
inputs = []
|
|
total_in = Decimal("0.00000000")
|
|
while total_in < amount_needed and len(utxo) > 0:
|
|
t = utxo.pop()
|
|
total_in += t["amount"]
|
|
inputs.append({ "txid" : t["txid"], "vout" : t["vout"], "address" : t["address"] } )
|
|
if total_in < amount_needed:
|
|
raise RuntimeError("Insufficient funds: need %d, have %d"%(amount+fee*2, total_in))
|
|
return (total_in, inputs)
|
|
|
|
def make_change(from_node, amount_in, amount_out, fee):
|
|
"""
|
|
Create change output(s), return them
|
|
"""
|
|
outputs = {}
|
|
amount = amount_out+fee
|
|
change = amount_in - amount
|
|
if change > amount*2:
|
|
# Create an extra change output to break up big inputs
|
|
outputs[from_node.getnewaddress()] = float(change/2)
|
|
change = change/2
|
|
if change > 0:
|
|
outputs[from_node.getnewaddress()] = float(change)
|
|
return outputs
|
|
|
|
def send_zeropri_transaction(from_node, to_node, amount, fee):
|
|
"""
|
|
Create&broadcast a zero-priority transaction.
|
|
Returns (txid, hex-encoded-txdata)
|
|
Ensures transaction is zero-priority by first creating a send-to-self,
|
|
then using it's output
|
|
"""
|
|
|
|
# Create a send-to-self with confirmed inputs:
|
|
self_address = from_node.getnewaddress()
|
|
(total_in, inputs) = gather_inputs(from_node, amount+fee*2)
|
|
outputs = make_change(from_node, total_in, amount+fee, fee)
|
|
outputs[self_address] = float(amount+fee)
|
|
|
|
self_rawtx = from_node.createrawtransaction(inputs, outputs)
|
|
self_signresult = from_node.signrawtransaction(self_rawtx)
|
|
self_txid = from_node.sendrawtransaction(self_signresult["hex"], True)
|
|
|
|
vout = find_output(from_node, self_txid, amount+fee)
|
|
# Now immediately spend the output to create a 1-input, 1-output
|
|
# zero-priority transaction:
|
|
inputs = [ { "txid" : self_txid, "vout" : vout } ]
|
|
outputs = { to_node.getnewaddress() : float(amount) }
|
|
|
|
rawtx = from_node.createrawtransaction(inputs, outputs)
|
|
signresult = from_node.signrawtransaction(rawtx)
|
|
txid = from_node.sendrawtransaction(signresult["hex"], True)
|
|
|
|
return (txid, signresult["hex"])
|
|
|
|
def random_zeropri_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
|
|
"""
|
|
Create a random zero-priority transaction.
|
|
Returns (txid, hex-encoded-transaction-data, fee)
|
|
"""
|
|
from_node = random.choice(nodes)
|
|
to_node = random.choice(nodes)
|
|
fee = min_fee + fee_increment*random.randint(0,fee_variants)
|
|
(txid, txhex) = send_zeropri_transaction(from_node, to_node, amount, fee)
|
|
return (txid, txhex, fee)
|
|
|
|
def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
|
|
"""
|
|
Create a random transaction.
|
|
Returns (txid, hex-encoded-transaction-data, fee)
|
|
"""
|
|
from_node = random.choice(nodes)
|
|
to_node = random.choice(nodes)
|
|
fee = min_fee + fee_increment*random.randint(0,fee_variants)
|
|
|
|
(total_in, inputs) = gather_inputs(from_node, amount+fee)
|
|
outputs = make_change(from_node, total_in, amount, fee)
|
|
outputs[to_node.getnewaddress()] = float(amount)
|
|
|
|
rawtx = from_node.createrawtransaction(inputs, outputs)
|
|
signresult = from_node.signrawtransaction(rawtx)
|
|
txid = from_node.sendrawtransaction(signresult["hex"], True)
|
|
|
|
return (txid, signresult["hex"], fee)
|
|
|
|
def assert_equal(thing1, thing2):
|
|
if thing1 != thing2:
|
|
raise AssertionError("%s != %s"%(str(thing1),str(thing2)))
|