diff options
Diffstat (limited to 'qa/rpc-tests/test_framework')
| -rw-r--r-- | qa/rpc-tests/test_framework/address.py | 74 | ||||
| -rw-r--r-- | qa/rpc-tests/test_framework/authproxy.py | 101 | ||||
| -rw-r--r-- | qa/rpc-tests/test_framework/auxpow.py | 8 | ||||
| -rw-r--r-- | qa/rpc-tests/test_framework/bignum.py | 5 | ||||
| -rw-r--r-- | qa/rpc-tests/test_framework/blockstore.py | 101 | ||||
| -rw-r--r-- | qa/rpc-tests/test_framework/blocktools.py | 80 | ||||
| -rwxr-xr-x | qa/rpc-tests/test_framework/comptool.py | 217 | ||||
| -rw-r--r-- | qa/rpc-tests/test_framework/coverage.py | 106 | ||||
| -rw-r--r-- | qa/rpc-tests/test_framework/key.py | 236 | ||||
| -rwxr-xr-x | qa/rpc-tests/test_framework/mininode.py | 1141 | ||||
| -rw-r--r-- | qa/rpc-tests/test_framework/netutil.py | 34 | ||||
| -rw-r--r-- | qa/rpc-tests/test_framework/script.py | 91 | ||||
| -rw-r--r-- | qa/rpc-tests/test_framework/scrypt_auxpow.py | 27 | ||||
| -rw-r--r-- | qa/rpc-tests/test_framework/siphash.py | 64 | ||||
| -rw-r--r-- | qa/rpc-tests/test_framework/socks5.py | 13 | ||||
| -rwxr-xr-x | qa/rpc-tests/test_framework/test_framework.py | 120 | ||||
| -rw-r--r-- | qa/rpc-tests/test_framework/util.py | 536 |
17 files changed, 2324 insertions, 630 deletions
diff --git a/qa/rpc-tests/test_framework/address.py b/qa/rpc-tests/test_framework/address.py new file mode 100644 index 000000000..50b999be6 --- /dev/null +++ b/qa/rpc-tests/test_framework/address.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# Copyright (c) 2016 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# +# address.py +# +# This file encodes and decodes BASE58 P2PKH and P2SH addresses +# + +from .script import hash256, hash160, sha256, CScript, OP_0 +from .util import bytes_to_hex_str, hex_str_to_bytes + +chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + +def byte_to_base58(b, version): + result = '' + str = bytes_to_hex_str(b) + str = bytes_to_hex_str(chr(version).encode('latin-1')) + str + checksum = bytes_to_hex_str(hash256(hex_str_to_bytes(str))) + str += checksum[:8] + value = int('0x'+str,0) + while value > 0: + result = chars[value % 58] + result + value //= 58 + while (str[:2] == '00'): + result = chars[0] + result + str = str[2:] + return result + +# TODO: def base58_decode + +def keyhash_to_p2pkh(hash, main = False): + assert (len(hash) == 20) + version = 0 if main else 111 + return byte_to_base58(hash, version) + +def scripthash_to_p2sh(hash, main = False): + assert (len(hash) == 20) + version = 5 if main else 196 + return byte_to_base58(hash, version) + +def key_to_p2pkh(key, main = False): + key = check_key(key) + return keyhash_to_p2pkh(hash160(key), main) + +def script_to_p2sh(script, main = False): + script = check_script(script) + return scripthash_to_p2sh(hash160(script), main) + +def key_to_p2sh_p2wpkh(key, main = False): + key = check_key(key) + p2shscript = CScript([OP_0, hash160(key)]) + return script_to_p2sh(p2shscript, main) + +def script_to_p2sh_p2wsh(script, main = False): + script = check_script(script) + p2shscript = CScript([OP_0, sha256(script)]) + return script_to_p2sh(p2shscript, main) + +def check_key(key): + if (type(key) is str): + key = hex_str_to_bytes(key) # Assuming this is hex string + if (type(key) is bytes and (len(key) == 33 or len(key) == 65)): + return key + assert(False) + +def check_script(script): + if (type(script) is str): + script = hex_str_to_bytes(script) # Assuming this is hex string + if (type(script) is bytes or type(script) is CScript): + return script + assert(False) diff --git a/qa/rpc-tests/test_framework/authproxy.py b/qa/rpc-tests/test_framework/authproxy.py index bc7d655fd..09ed61129 100644 --- a/qa/rpc-tests/test_framework/authproxy.py +++ b/qa/rpc-tests/test_framework/authproxy.py @@ -1,6 +1,6 @@ """ - Copyright 2011 Jeff Garzik + Copyright (c) 2011 Jeff Garzik AuthServiceProxy has the following improvements over python-jsonrpc's ServiceProxy class: @@ -42,6 +42,7 @@ import base64 import decimal import json import logging +import socket try: import urllib.parse as urlparse except ImportError: @@ -55,21 +56,27 @@ log = logging.getLogger("BitcoinRPC") class JSONRPCException(Exception): def __init__(self, rpc_error): - Exception.__init__(self) + try: + errmsg = '%(message)s (%(code)i)' % rpc_error + except (KeyError, TypeError): + errmsg = '' + Exception.__init__(self, errmsg) self.error = rpc_error def EncodeDecimal(o): if isinstance(o, decimal.Decimal): - return round(o, 8) + return str(o) raise TypeError(repr(o) + " is not JSON serializable") class AuthServiceProxy(object): __id_count = 0 - def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None): + # ensure_ascii: escape unicode as \uXXXX, passed to json.dumps + def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None, ensure_ascii=True): self.__service_url = service_url - self.__service_name = service_name + self._service_name = service_name + self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests self.__url = urlparse.urlparse(service_url) if self.__url.port is None: port = 80 @@ -92,36 +99,57 @@ class AuthServiceProxy(object): self.__conn = connection elif self.__url.scheme == 'https': self.__conn = httplib.HTTPSConnection(self.__url.hostname, port, - None, None, False, - timeout) + timeout=timeout) else: self.__conn = httplib.HTTPConnection(self.__url.hostname, port, - False, timeout) + timeout=timeout) def __getattr__(self, name): if name.startswith('__') and name.endswith('__'): # Python internal stuff raise AttributeError - if self.__service_name is not None: - name = "%s.%s" % (self.__service_name, name) + if self._service_name is not None: + name = "%s.%s" % (self._service_name, name) return AuthServiceProxy(self.__service_url, name, connection=self.__conn) - def __call__(self, *args): + def _request(self, method, path, postdata): + ''' + Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout). + This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5. + ''' + headers = {'Host': self.__url.hostname, + 'User-Agent': USER_AGENT, + 'Authorization': self.__auth_header, + 'Content-type': 'application/json'} + try: + self.__conn.request(method, path, postdata, headers) + return self._get_response() + except httplib.BadStatusLine as e: + if e.line == "''": # if connection was closed, try again + self.__conn.close() + self.__conn.request(method, path, postdata, headers) + return self._get_response() + else: + raise + except (BrokenPipeError,ConnectionResetError): + # Python 3.5+ raises BrokenPipeError instead of BadStatusLine when the connection was reset + # ConnectionResetError happens on FreeBSD with Python 3.4 + self.__conn.close() + self.__conn.request(method, path, postdata, headers) + return self._get_response() + + def __call__(self, *args, **argsn): AuthServiceProxy.__id_count += 1 - log.debug("-%s-> %s %s"%(AuthServiceProxy.__id_count, self.__service_name, - json.dumps(args, default=EncodeDecimal))) + log.debug("-%s-> %s %s"%(AuthServiceProxy.__id_count, self._service_name, + json.dumps(args, default=EncodeDecimal, ensure_ascii=self.ensure_ascii))) + if args and argsn: + raise ValueError('Cannot handle both named and positional arguments') postdata = json.dumps({'version': '1.1', - 'method': self.__service_name, - 'params': args, - 'id': AuthServiceProxy.__id_count}, default=EncodeDecimal) - self.__conn.request('POST', self.__url.path, postdata, - {'Host': self.__url.hostname, - 'User-Agent': USER_AGENT, - 'Authorization': self.__auth_header, - 'Content-type': 'application/json'}) - - response = self._get_response() + 'method': self._service_name, + 'params': args or argsn, + 'id': AuthServiceProxy.__id_count}, default=EncodeDecimal, ensure_ascii=self.ensure_ascii) + response = self._request('POST', self.__url.path, postdata.encode('utf-8')) if response['error'] is not None: raise JSONRPCException(response['error']) elif 'result' not in response: @@ -131,26 +159,33 @@ class AuthServiceProxy(object): return response['result'] def _batch(self, rpc_call_list): - postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal) + postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii) log.debug("--> "+postdata) - self.__conn.request('POST', self.__url.path, postdata, - {'Host': self.__url.hostname, - 'User-Agent': USER_AGENT, - 'Authorization': self.__auth_header, - 'Content-type': 'application/json'}) - - return self._get_response() + return self._request('POST', self.__url.path, postdata.encode('utf-8')) def _get_response(self): - http_response = self.__conn.getresponse() + try: + http_response = self.__conn.getresponse() + except socket.timeout as e: + raise JSONRPCException({ + 'code': -344, + 'message': '%r RPC took longer than %f seconds. Consider ' + 'using larger timeout for calls that take ' + 'longer to return.' % (self._service_name, + self.__conn.timeout)}) if http_response is None: raise JSONRPCException({ 'code': -342, 'message': 'missing HTTP response from server'}) + content_type = http_response.getheader('Content-Type') + if content_type != 'application/json': + raise JSONRPCException({ + 'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)}) + responsedata = http_response.read().decode('utf8') response = json.loads(responsedata, parse_float=decimal.Decimal) if "error" in response and response["error"] is None: - log.debug("<-%s- %s"%(response["id"], json.dumps(response["result"], default=EncodeDecimal))) + log.debug("<-%s- %s"%(response["id"], json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii))) else: log.debug("<-- "+responsedata) return response diff --git a/qa/rpc-tests/test_framework/auxpow.py b/qa/rpc-tests/test_framework/auxpow.py index 017c0b02e..4f5ce20c0 100644 --- a/qa/rpc-tests/test_framework/auxpow.py +++ b/qa/rpc-tests/test_framework/auxpow.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Copyright (c) 2014 Daniel Kraft # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -63,7 +63,7 @@ def mineAuxpowBlock (node): """ auxblock = node.getauxblock () - target = reverseHex (auxblock['target']) + target = reverseHex (auxblock['_target']) apow = computeAuxpow (auxblock['hash'], target, True) res = node.getauxblock (auxblock['hash'], apow) assert res @@ -105,7 +105,7 @@ def reverseHex (data): Flip byte order in the given data (hex string). """ - b = bytearray (binascii.unhexlify (data)) + b = bytearray (bytes.fromhex(data)) b.reverse () - return binascii.hexlify (b) + return b.hex() diff --git a/qa/rpc-tests/test_framework/bignum.py b/qa/rpc-tests/test_framework/bignum.py index b0c58ccd4..ef800e4d5 100644 --- a/qa/rpc-tests/test_framework/bignum.py +++ b/qa/rpc-tests/test_framework/bignum.py @@ -1,16 +1,15 @@ -# +#!/usr/bin/env python3 # # bignum.py # # This file is copied from python-bitcoinlib. # -# Distributed under the MIT/X11 software license, see the accompanying +# Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # """Bignum routines""" -from __future__ import absolute_import, division, print_function, unicode_literals import struct diff --git a/qa/rpc-tests/test_framework/blockstore.py b/qa/rpc-tests/test_framework/blockstore.py index c57b6df81..28a6b92b8 100644 --- a/qa/rpc-tests/test_framework/blockstore.py +++ b/qa/rpc-tests/test_framework/blockstore.py @@ -1,49 +1,72 @@ +#!/usr/bin/env python3 +# Copyright (c) 2015-2016 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. # BlockStore: a helper class that keeps a map of blocks and implements # helper functions for responding to getheaders and getdata, # and for constructing a getheaders message # -from mininode import * -import dbm +from .mininode import * +from io import BytesIO +import dbm.dumb as dbmd class BlockStore(object): def __init__(self, datadir): - self.blockDB = dbm.open(datadir + "/blocks", 'c') - self.currentBlock = 0L - + self.blockDB = dbmd.open(datadir + "/blocks", 'c') + self.currentBlock = 0 + self.headers_map = dict() + def close(self): self.blockDB.close() + def erase(self, blockhash): + del self.blockDB[repr(blockhash)] + + # lookup an entry and return the item as raw bytes def get(self, blockhash): - serialized_block = None + value = None try: - serialized_block = self.blockDB[repr(blockhash)] + value = self.blockDB[repr(blockhash)] except KeyError: return None - f = cStringIO.StringIO(serialized_block) - ret = CBlock() - ret.deserialize(f) - ret.calc_sha256() + return value + + # lookup an entry and return it as a CBlock + def get_block(self, blockhash): + ret = None + serialized_block = self.get(blockhash) + if serialized_block is not None: + f = BytesIO(serialized_block) + ret = CBlock() + ret.deserialize(f) + ret.calc_sha256() return ret + def get_header(self, blockhash): + try: + return self.headers_map[blockhash] + except KeyError: + return None + # Note: this pulls full blocks out of the database just to retrieve # the headers -- perhaps we could keep a separate data structure # to avoid this overhead. def headers_for(self, locator, hash_stop, current_tip=None): if current_tip is None: current_tip = self.currentBlock - current_block = self.get(current_tip) - if current_block is None: + current_block_header = self.get_header(current_tip) + if current_block_header is None: return None response = msg_headers() - headersList = [ CBlockHeader(current_block) ] + headersList = [ current_block_header ] maxheaders = 2000 while (headersList[0].sha256 not in locator.vHave): prevBlockHash = headersList[0].hashPrevBlock - prevBlock = self.get(prevBlockHash) - if prevBlock is not None: - headersList.insert(0, CBlockHeader(prevBlock)) + prevBlockHeader = self.get_header(prevBlockHash) + if prevBlockHeader is not None: + headersList.insert(0, prevBlockHeader) else: break headersList = headersList[:maxheaders] # truncate if we have too many @@ -59,16 +82,23 @@ class BlockStore(object): try: self.blockDB[repr(block.sha256)] = bytes(block.serialize()) except TypeError as e: - print "Unexpected error: ", sys.exc_info()[0], e.args + print("Unexpected error: ", sys.exc_info()[0], e.args) self.currentBlock = block.sha256 + self.headers_map[block.sha256] = CBlockHeader(block) + def add_header(self, header): + self.headers_map[header.sha256] = header + + # lookup the hashes in "inv", and return p2p messages for delivering + # blocks found. def get_blocks(self, inv): responses = [] for i in inv: if (i.type == 2): # MSG_BLOCK - block = self.get(i.hash) - if block is not None: - responses.append(msg_block(block)) + data = self.get(i.hash) + if data is not None: + # Use msg_generic to avoid re-serialization + responses.append(msg_generic(b"block", data)) return responses def get_locator(self, current_tip=None): @@ -77,11 +107,11 @@ class BlockStore(object): r = [] counter = 0 step = 1 - lastBlock = self.get(current_tip) + lastBlock = self.get_block(current_tip) while lastBlock is not None: r.append(lastBlock.hashPrevBlock) for i in range(step): - lastBlock = self.get(lastBlock.hashPrevBlock) + lastBlock = self.get_block(lastBlock.hashPrevBlock) if lastBlock is None: break counter += 1 @@ -93,21 +123,28 @@ class BlockStore(object): class TxStore(object): def __init__(self, datadir): - self.txDB = dbm.open(datadir + "/transactions", 'c') + self.txDB = dbmd.open(datadir + "/transactions", 'c') def close(self): self.txDB.close() + # lookup an entry and return the item as raw bytes def get(self, txhash): - serialized_tx = None + value = None try: - serialized_tx = self.txDB[repr(txhash)] + value = self.txDB[repr(txhash)] except KeyError: return None - f = cStringIO.StringIO(serialized_tx) - ret = CTransaction() - ret.deserialize(f) - ret.calc_sha256() + return value + + def get_transaction(self, txhash): + ret = None + serialized_tx = self.get(txhash) + if serialized_tx is not None: + f = BytesIO(serialized_tx) + ret = CTransaction() + ret.deserialize(f) + ret.calc_sha256() return ret def add_transaction(self, tx): @@ -115,7 +152,7 @@ class TxStore(object): try: self.txDB[repr(tx.sha256)] = bytes(tx.serialize()) except TypeError as e: - print "Unexpected error: ", sys.exc_info()[0], e.args + print("Unexpected error: ", sys.exc_info()[0], e.args) def get_transactions(self, inv): responses = [] @@ -123,5 +160,5 @@ class TxStore(object): if (i.type == 1): # MSG_TX tx = self.get(i.hash) if tx is not None: - responses.append(msg_tx(tx)) + responses.append(msg_generic(b"tx", tx)) return responses diff --git a/qa/rpc-tests/test_framework/blocktools.py b/qa/rpc-tests/test_framework/blocktools.py index f397fe7cd..47a68f435 100644 --- a/qa/rpc-tests/test_framework/blocktools.py +++ b/qa/rpc-tests/test_framework/blocktools.py @@ -1,15 +1,17 @@ +#!/usr/bin/env python3 # blocktools.py - utilities for manipulating blocks and transactions -# -# Distributed under the MIT/X11 software license, see the accompanying +# Copyright (c) 2015-2016 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -# -from mininode import * -from script import CScript, CScriptOp +from .mininode import * +from .script import CScript, OP_TRUE, OP_CHECKSIG, OP_RETURN # Create a block (with regtest difficulty) def create_block(hashprev, coinbase, nTime=None): block = CBlock() + # Dogecoin: Create a non-AuxPoW block but include chain ID + block.nVersion = 0x620003 if nTime is None: import time block.nTime = int(time.time()+600) @@ -22,6 +24,29 @@ def create_block(hashprev, coinbase, nTime=None): block.calc_sha256() return block +# From BIP141 +WITNESS_COMMITMENT_HEADER = b"\xaa\x21\xa9\xed" + +# According to BIP141, blocks with witness rules active must commit to the +# hash of all in-block transactions including witness. +def add_witness_commitment(block, nonce=0): + # First calculate the merkle root of the block's + # transactions, with witnesses. + witness_nonce = nonce + witness_root = block.calc_witness_merkle_root() + witness_commitment = uint256_from_str(hash256(ser_uint256(witness_root)+ser_uint256(witness_nonce))) + # witness_nonce should go to coinbase witness. + block.vtx[0].wit.vtxinwit = [CTxInWitness()] + block.vtx[0].wit.vtxinwit[0].scriptWitness.stack = [ser_uint256(witness_nonce)] + + # witness commitment is the last OP_RETURN output in coinbase + output_data = WITNESS_COMMITMENT_HEADER + ser_uint256(witness_commitment) + block.vtx[0].vout.append(CTxOut(0, CScript([OP_RETURN, output_data]))) + block.vtx[0].rehash() + block.hashMerkleRoot = block.calc_merkle_root() + block.rehash() + + def serialize_script_num(value): r = bytearray(0) if value == 0: @@ -29,7 +54,7 @@ def serialize_script_num(value): neg = value < 0 absvalue = -value if neg else value while (absvalue): - r.append(chr(absvalue & 0xff)) + r.append(int(absvalue & 0xff)) absvalue >>= 8 if r[-1] & 0x80: r.append(0x80 if neg else 0) @@ -37,29 +62,46 @@ def serialize_script_num(value): r[-1] |= 0x80 return r -counter=1 -# Create an anyone-can-spend coinbase transaction, assuming no miner fees -def create_coinbase(heightAdjust = 0): - global counter +# Create a coinbase transaction, assuming no miner fees. +# If pubkey is passed in, the coinbase output will be a P2PK output; +# otherwise an anyone-can-spend output. +def create_coinbase(height, pubkey = None): coinbase = CTransaction() coinbase.vin.append(CTxIn(COutPoint(0, 0xffffffff), - ser_string(serialize_script_num(counter+heightAdjust)), 0xffffffff)) - counter += 1 + ser_string(serialize_script_num(height)), 0xffffffff)) coinbaseoutput = CTxOut() - coinbaseoutput.nValue = 50*100000000 - halvings = int((counter+heightAdjust)/150) # regtest + coinbaseoutput.nValue = 500000 * COIN + halvings = int(height/150) # regtest coinbaseoutput.nValue >>= halvings - coinbaseoutput.scriptPubKey = "" + if (pubkey != None): + coinbaseoutput.scriptPubKey = CScript([pubkey, OP_CHECKSIG]) + else: + coinbaseoutput.scriptPubKey = CScript([OP_TRUE]) coinbase.vout = [ coinbaseoutput ] coinbase.calc_sha256() return coinbase -# Create a transaction with an anyone-can-spend output, that spends the -# nth output of prevtx. -def create_transaction(prevtx, n, sig, value): +# Create a transaction. +# If the scriptPubKey is not specified, make it anyone-can-spend. +def create_transaction(prevtx, n, sig, value, scriptPubKey=CScript()): tx = CTransaction() assert(n < len(prevtx.vout)) tx.vin.append(CTxIn(COutPoint(prevtx.sha256, n), sig, 0xffffffff)) - tx.vout.append(CTxOut(value, "")) + tx.vout.append(CTxOut(value, scriptPubKey)) tx.calc_sha256() return tx + +def get_legacy_sigopcount_block(block, fAccurate=True): + count = 0 + for tx in block.vtx: + count += get_legacy_sigopcount_tx(tx, fAccurate) + return count + +def get_legacy_sigopcount_tx(tx, fAccurate=True): + count = 0 + for i in tx.vout: + count += i.scriptPubKey.GetSigOpCount(fAccurate) + for j in tx.vin: + # scriptSig might be of type bytes, so convert to CScript for the moment + count += CScript(j.scriptSig).GetSigOpCount(fAccurate) + return count diff --git a/qa/rpc-tests/test_framework/comptool.py b/qa/rpc-tests/test_framework/comptool.py index 23a979250..17679fc7e 100755 --- a/qa/rpc-tests/test_framework/comptool.py +++ b/qa/rpc-tests/test_framework/comptool.py @@ -1,12 +1,11 @@ -#!/usr/bin/env python2 -# -# Distributed under the MIT/X11 software license, see the accompanying +#!/usr/bin/env python3 +# Copyright (c) 2015-2016 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -# -from mininode import * -from blockstore import BlockStore, TxStore -from util import p2p_port +from .mininode import * +from .blockstore import BlockStore, TxStore +from .util import p2p_port ''' This is a tool for comparing two or more bitcoinds to each other @@ -27,22 +26,41 @@ generator that returns TestInstance objects. See below for definition. global mininode_lock +class RejectResult(object): + ''' + Outcome that expects rejection of a transaction or block. + ''' + def __init__(self, code, reason=b''): + self.code = code + self.reason = reason + def match(self, other): + if self.code != other.code: + return False + return other.reason.startswith(self.reason) + def __repr__(self): + return '%i:%s' % (self.code,self.reason or '*') + class TestNode(NodeConnCB): def __init__(self, block_store, tx_store): NodeConnCB.__init__(self) - self.create_callback_map() self.conn = None self.bestblockhash = None self.block_store = block_store self.block_request_map = {} self.tx_store = tx_store self.tx_request_map = {} + self.block_reject_map = {} + self.tx_reject_map = {} # When the pingmap is non-empty we're waiting for # a response self.pingMap = {} self.lastInv = [] + self.closed = False + + def on_close(self, conn): + self.closed = True def add_connection(self, conn): self.conn = conn @@ -77,6 +95,12 @@ class TestNode(NodeConnCB): except KeyError: raise AssertionError("Got pong for unknown ping [%s]" % repr(message)) + def on_reject(self, conn, message): + if message.message == b'tx': + self.tx_reject_map[message.data] = RejectResult(message.code, message.reason) + if message.message == b'block': + self.block_reject_map[message.data] = RejectResult(message.code, message.reason) + def send_inv(self, obj): mtype = 2 if isinstance(obj, CBlock) else 1 self.conn.send_message(msg_inv([CInv(mtype, obj.sha256)])) @@ -87,6 +111,11 @@ class TestNode(NodeConnCB): m.locator = self.block_store.get_locator(self.bestblockhash) self.conn.send_message(m) + def send_header(self, header): + m = msg_headers() + m.headers.append(header) + self.conn.send_message(m) + # This assumes BIP31 def send_ping(self, nonce): self.pingMap[nonce] = True @@ -104,26 +133,33 @@ class TestNode(NodeConnCB): # Instances of these are generated by the test generator, and fed into the # comptool. # -# "blocks_and_transactions" should be an array of [obj, True/False/None]: -# - obj is either a CBlock or a CTransaction, and +# "blocks_and_transactions" should be an array of +# [obj, True/False/None, hash/None]: +# - obj is either a CBlock, CBlockHeader, or a CTransaction, and # - the second value indicates whether the object should be accepted # into the blockchain or mempool (for tests where we expect a certain # answer), or "None" if we don't expect a certain answer and are just # comparing the behavior of the nodes being tested. +# - the third value is the hash to test the tip against (if None or omitted, +# use the hash of the block) +# - NOTE: if a block header, no test is performed; instead the header is +# just added to the block_store. This is to facilitate block delivery +# when communicating with headers-first clients (when withholding an +# intermediate block). # sync_every_block: if True, then each block will be inv'ed, synced, and # nodes will be tested based on the outcome for the block. If False, # then inv's accumulate until all blocks are processed (or max inv size # is reached) and then sent out in one inv message. Then the final block # will be synced across all connections, and the outcome of the final # block will be tested. -# sync_every_tx: analagous to behavior for sync_every_block, except if outcome +# sync_every_tx: analogous to behavior for sync_every_block, except if outcome # on the final tx is None, then contents of entire mempool are compared # across all connections. (If outcome of final tx is specified as true # or false, then only the last tx is tested against outcome.) class TestInstance(object): - def __init__(self, objects=[], sync_every_block=True, sync_every_tx=False): - self.blocks_and_transactions = objects + def __init__(self, objects=None, sync_every_block=True, sync_every_tx=False): + self.blocks_and_transactions = objects if objects else [] self.sync_every_block = sync_every_block self.sync_every_tx = sync_every_tx @@ -132,6 +168,7 @@ class TestManager(object): def __init__(self, testgen, datadir): self.test_generator = testgen self.connections = [] + self.test_nodes = [] self.block_store = BlockStore(datadir) self.tx_store = TxStore(datadir) self.ping_counter = 1 @@ -139,57 +176,46 @@ class TestManager(object): def add_all_connections(self, nodes): for i in range(len(nodes)): # Create a p2p connection to each node - self.connections.append(NodeConn('127.0.0.1', p2p_port(i), - nodes[i], TestNode(self.block_store, self.tx_store))) + test_node = TestNode(self.block_store, self.tx_store) + self.test_nodes.append(test_node) + self.connections.append(NodeConn('127.0.0.1', p2p_port(i), nodes[i], test_node)) # Make sure the TestNode (callback class) has a reference to its # associated NodeConn - self.connections[-1].cb.add_connection(self.connections[-1]) + test_node.add_connection(self.connections[-1]) + + def clear_all_connections(self): + self.connections = [] + self.test_nodes = [] + + def wait_for_disconnections(self): + def disconnected(): + return all(node.closed for node in self.test_nodes) + return wait_until(disconnected, timeout=10) def wait_for_verack(self): - sleep_time = 0.05 - max_tries = 10 / sleep_time # Wait at most 10 seconds - while max_tries > 0: - done = True - with mininode_lock: - for c in self.connections: - if c.cb.verack_received is False: - done = False - break - if done: - break - time.sleep(sleep_time) + def veracked(): + return all(node.verack_received for node in self.test_nodes) + return wait_until(veracked, timeout=10) def wait_for_pings(self, counter): - received_pongs = False - while received_pongs is not True: - time.sleep(0.05) - received_pongs = True - with mininode_lock: - for c in self.connections: - if c.cb.received_ping_response(counter) is not True: - received_pongs = False - break + def received_pongs(): + return all(node.received_ping_response(counter) for node in self.test_nodes) + return wait_until(received_pongs) # sync_blocks: Wait for all connections to request the blockhash given # then send get_headers to find out the tip of each node, and synchronize # the response by using a ping (and waiting for pong with same nonce). def sync_blocks(self, blockhash, num_blocks): - # Wait for nodes to request block (50ms sleep * 20 tries * num_blocks) - max_tries = 20*num_blocks - while max_tries > 0: - with mininode_lock: - results = [ blockhash in c.cb.block_request_map and - c.cb.block_request_map[blockhash] for c in self.connections ] - if False not in results: - break - time.sleep(0.05) - max_tries -= 1 + def blocks_requested(): + return all( + blockhash in node.block_request_map and node.block_request_map[blockhash] + for node in self.test_nodes + ) # --> error if not requested - if max_tries == 0: + if not wait_until(blocks_requested, attempts=20*num_blocks): # print [ c.cb.block_request_map for c in self.connections ] raise AssertionError("Not all nodes requested block") - # --> Answer request (we did this inline!) # Send getheaders message [ c.cb.send_getheaders() for c in self.connections ] @@ -202,21 +228,16 @@ class TestManager(object): # Analogous to sync_block (see above) def sync_transaction(self, txhash, num_events): # Wait for nodes to request transaction (50ms sleep * 20 tries * num_events) - max_tries = 20*num_events - while max_tries > 0: - with mininode_lock: - results = [ txhash in c.cb.tx_request_map and - c.cb.tx_request_map[txhash] for c in self.connections ] - if False not in results: - break - time.sleep(0.05) - max_tries -= 1 + def transaction_requested(): + return all( + txhash in node.tx_request_map and node.tx_request_map[txhash] + for node in self.test_nodes + ) # --> error if not requested - if max_tries == 0: + if not wait_until(transaction_requested, attempts=20*num_events): # print [ c.cb.tx_request_map for c in self.connections ] raise AssertionError("Not all nodes requested transaction") - # --> Answer request (we did this inline!) # Get the mempool [ c.cb.send_mempool() for c in self.connections ] @@ -238,6 +259,15 @@ class TestManager(object): if outcome is None: if c.cb.bestblockhash != self.connections[0].cb.bestblockhash: return False + elif isinstance(outcome, RejectResult): # Check that block was rejected w/ code + if c.cb.bestblockhash == blockhash: + return False + if blockhash not in c.cb.block_reject_map: + print('Block not in reject map: %064x' % (blockhash)) + return False + if not outcome.match(c.cb.block_reject_map[blockhash]): + print('Block rejected with %s instead of expected %s: %064x' % (c.cb.block_reject_map[blockhash], outcome, blockhash)) + return False elif ((c.cb.bestblockhash == blockhash) != outcome): # print c.cb.bestblockhash, blockhash, outcome return False @@ -257,6 +287,15 @@ class TestManager(object): if c.cb.lastInv != self.connections[0].cb.lastInv: # print c.rpc.getrawmempool() return False + elif isinstance(outcome, RejectResult): # Check that tx was rejected w/ code + if txhash in c.cb.lastInv: + return False + if txhash not in c.cb.tx_reject_map: + print('Tx not in reject map: %064x' % (txhash)) + return False + if not outcome.match(c.cb.tx_reject_map[txhash]): + print('Tx rejected with %s instead of expected %s: %064x' % (c.cb.tx_reject_map[txhash], outcome, txhash)) + return False elif ((txhash in c.cb.lastInv) != outcome): # print c.rpc.getrawmempool(), c.cb.lastInv return False @@ -271,29 +310,65 @@ class TestManager(object): # We use these variables to keep track of the last block # and last transaction in the tests, which are used # if we're not syncing on every block or every tx. - [ block, block_outcome ] = [ None, None ] + [ block, block_outcome, tip ] = [ None, None, None ] [ tx, tx_outcome ] = [ None, None ] invqueue = [] - for b_or_t, outcome in test_instance.blocks_and_transactions: + for test_obj in test_instance.blocks_and_transactions: + b_or_t = test_obj[0] + outcome = test_obj[1] # Determine if we're dealing with a block or tx if isinstance(b_or_t, CBlock): # Block test runner block = b_or_t block_outcome = outcome + tip = block.sha256 + # each test_obj can have an optional third argument + # to specify the tip we should compare with + # (default is to use the block being tested) + if len(test_obj) >= 3: + tip = test_obj[2] + # Add to shared block_store, set as current block + # If there was an open getdata request for the block + # previously, and we didn't have an entry in the + # block_store, then immediately deliver, because the + # node wouldn't send another getdata request while + # the earlier one is outstanding. + first_block_with_hash = True + if self.block_store.get(block.sha256) is not None: + first_block_with_hash = False with mininode_lock: self.block_store.add_block(block) for c in self.connections: - c.cb.block_request_map[block.sha256] = False + if first_block_with_hash and block.sha256 in c.cb.block_request_map and c.cb.block_request_map[block.sha256] == True: + # There was a previous request for this block hash + # Most likely, we delivered a header for this block + # but never had the block to respond to the getdata + c.send_message(msg_block(block)) + else: + c.cb.block_request_map[block.sha256] = False # Either send inv's to each node and sync, or add # to invqueue for later inv'ing. if (test_instance.sync_every_block): - [ c.cb.send_inv(block) for c in self.connections ] - self.sync_blocks(block.sha256, 1) - if (not self.check_results(block.sha256, outcome)): + # if we expect success, send inv and sync every block + # if we expect failure, just push the block and see what happens. + if outcome == True: + [ c.cb.send_inv(block) for c in self.connections ] + self.sync_blocks(block.sha256, 1) + else: + [ c.send_message(msg_block(block)) for c in self.connections ] + [ c.cb.send_ping(self.ping_counter) for c in self.connections ] + self.wait_for_pings(self.ping_counter) + self.ping_counter += 1 + if (not self.check_results(tip, outcome)): raise AssertionError("Test failed at test %d" % test_number) else: invqueue.append(CInv(2, block.sha256)) + elif isinstance(b_or_t, CBlockHeader): + block_header = b_or_t + self.block_store.add_header(block_header) + [ c.cb.send_header(block_header) for c in self.connections ] + else: # Tx test runner assert(isinstance(b_or_t, CTransaction)) tx = b_or_t @@ -321,9 +396,8 @@ class TestManager(object): if len(invqueue) > 0: [ c.send_message(msg_inv(invqueue)) for c in self.connections ] invqueue = [] - self.sync_blocks(block.sha256, - len(test_instance.blocks_and_transactions)) - if (not self.check_results(block.sha256, block_outcome)): + self.sync_blocks(block.sha256, len(test_instance.blocks_and_transactions)) + if (not self.check_results(tip, block_outcome)): raise AssertionError("Block test failed at test %d" % test_number) if (not test_instance.sync_every_tx and tx is not None): if len(invqueue) > 0: @@ -333,9 +407,10 @@ class TestManager(object): if (not self.check_mempool(tx.sha256, tx_outcome)): raise AssertionError("Mempool test failed at test %d" % test_number) - print "Test %d: PASS" % test_number, [ c.rpc.getblockcount() for c in self.connections ] + print("Test %d: PASS" % test_number, [ c.rpc.getblockcount() for c in self.connections ]) test_number += 1 + [ c.disconnect_node() for c in self.connections ] + self.wait_for_disconnections() self.block_store.close() self.tx_store.close() - [ c.disconnect_node() for c in self.connections ] diff --git a/qa/rpc-tests/test_framework/coverage.py b/qa/rpc-tests/test_framework/coverage.py new file mode 100644 index 000000000..13b33869f --- /dev/null +++ b/qa/rpc-tests/test_framework/coverage.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# Copyright (c) 2015-2016 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +""" +This module contains utilities for doing coverage analysis on the RPC +interface. + +It provides a way to track which RPC commands are exercised during +testing. + +""" +import os + + +REFERENCE_FILENAME = 'rpc_interface.txt' + + +class AuthServiceProxyWrapper(object): + """ + An object that wraps AuthServiceProxy to record specific RPC calls. + + """ + def __init__(self, auth_service_proxy_instance, coverage_logfile=None): + """ + Kwargs: + auth_service_proxy_instance (AuthServiceProxy): the instance + being wrapped. + coverage_logfile (str): if specified, write each service_name + out to a file when called. + + """ + self.auth_service_proxy_instance = auth_service_proxy_instance + self.coverage_logfile = coverage_logfile + + def __getattr__(self, *args, **kwargs): + return_val = self.auth_service_proxy_instance.__getattr__( + *args, **kwargs) + + return AuthServiceProxyWrapper(return_val, self.coverage_logfile) + + def __call__(self, *args, **kwargs): + """ + Delegates to AuthServiceProxy, then writes the particular RPC method + called to a file. + + """ + return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs) + rpc_method = self.auth_service_proxy_instance._service_name + + if self.coverage_logfile: + with open(self.coverage_logfile, 'a+', encoding='utf8') as f: + f.write("%s\n" % rpc_method) + + return return_val + + @property + def url(self): + return self.auth_service_proxy_instance.url + + +def get_filename(dirname, n_node): + """ + Get a filename unique to the test process ID and node. + + This file will contain a list of RPC commands covered. + """ + pid = str(os.getpid()) + return os.path.join( + dirname, "coverage.pid%s.node%s.txt" % (pid, str(n_node))) + + +def write_all_rpc_commands(dirname, node): + """ + Write out a list of all RPC functions available in `bitcoin-cli` for + coverage comparison. This will only happen once per coverage + directory. + + Args: + dirname (str): temporary test dir + node (AuthServiceProxy): client + + Returns: + bool. if the RPC interface file was written. + + """ + filename = os.path.join(dirname, REFERENCE_FILENAME) + + if os.path.isfile(filename): + return False + + help_output = node.help().split('\n') + commands = set() + + for line in help_output: + line = line.strip() + + # Ignore blanks and headers + if line and not line.startswith('='): + commands.add("%s\n" % line.split()[0]) + + with open(filename, 'w', encoding='utf8') as f: + f.writelines(list(commands)) + + return True diff --git a/qa/rpc-tests/test_framework/key.py b/qa/rpc-tests/test_framework/key.py new file mode 100644 index 000000000..c63a15c1e --- /dev/null +++ b/qa/rpc-tests/test_framework/key.py @@ -0,0 +1,236 @@ +# Copyright (c) 2011 Sam Rushing +# +# key.py - OpenSSL wrapper +# +# This file is modified from python-bitcoinlib. +# + +"""ECC secp256k1 crypto routines + +WARNING: This module does not mlock() secrets; your private keys may end up on +disk in swap! Use with caution! +""" + +import ctypes +import ctypes.util +import hashlib +import sys + +ssl = ctypes.cdll.LoadLibrary(ctypes.util.find_library ('ssl') or 'libeay32') + +ssl.BN_new.restype = ctypes.c_void_p +ssl.BN_new.argtypes = [] + +ssl.BN_bin2bn.restype = ctypes.c_void_p +ssl.BN_bin2bn.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_void_p] + +ssl.BN_CTX_free.restype = None +ssl.BN_CTX_free.argtypes = [ctypes.c_void_p] + +ssl.BN_CTX_new.restype = ctypes.c_void_p +ssl.BN_CTX_new.argtypes = [] + +ssl.ECDH_compute_key.restype = ctypes.c_int +ssl.ECDH_compute_key.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] + +ssl.ECDSA_sign.restype = ctypes.c_int +ssl.ECDSA_sign.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + +ssl.ECDSA_verify.restype = ctypes.c_int +ssl.ECDSA_verify.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p] + +ssl.EC_KEY_free.restype = None +ssl.EC_KEY_free.argtypes = [ctypes.c_void_p] + +ssl.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p +ssl.EC_KEY_new_by_curve_name.argtypes = [ctypes.c_int] + +ssl.EC_KEY_get0_group.restype = ctypes.c_void_p +ssl.EC_KEY_get0_group.argtypes = [ctypes.c_void_p] + +ssl.EC_KEY_get0_public_key.restype = ctypes.c_void_p +ssl.EC_KEY_get0_public_key.argtypes = [ctypes.c_void_p] + +ssl.EC_KEY_set_private_key.restype = ctypes.c_int +ssl.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + +ssl.EC_KEY_set_conv_form.restype = None +ssl.EC_KEY_set_conv_form.argtypes = [ctypes.c_void_p, ctypes.c_int] + +ssl.EC_KEY_set_public_key.restype = ctypes.c_int +ssl.EC_KEY_set_public_key.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + +ssl.i2o_ECPublicKey.restype = ctypes.c_void_p +ssl.i2o_ECPublicKey.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + +ssl.EC_POINT_new.restype = ctypes.c_void_p +ssl.EC_POINT_new.argtypes = [ctypes.c_void_p] + +ssl.EC_POINT_free.restype = None +ssl.EC_POINT_free.argtypes = [ctypes.c_void_p] + +ssl.EC_POINT_mul.restype = ctypes.c_int +ssl.EC_POINT_mul.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + +# this specifies the curve used with ECDSA. +NID_secp256k1 = 714 # from openssl/obj_mac.h + +SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +SECP256K1_ORDER_HALF = SECP256K1_ORDER // 2 + +# Thx to Sam Devlin for the ctypes magic 64-bit fix. +def _check_result(val, func, args): + if val == 0: + raise ValueError + else: + return ctypes.c_void_p (val) + +ssl.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p +ssl.EC_KEY_new_by_curve_name.errcheck = _check_result + +class CECKey(object): + """Wrapper around OpenSSL's EC_KEY""" + + POINT_CONVERSION_COMPRESSED = 2 + POINT_CONVERSION_UNCOMPRESSED = 4 + + def __init__(self): + self.k = ssl.EC_KEY_new_by_curve_name(NID_secp256k1) + + def __del__(self): + if ssl: + ssl.EC_KEY_free(self.k) + self.k = None + + def set_secretbytes(self, secret): + priv_key = ssl.BN_bin2bn(secret, 32, ssl.BN_new()) + group = ssl.EC_KEY_get0_group(self.k) + pub_key = ssl.EC_POINT_new(group) + ctx = ssl.BN_CTX_new() + if not ssl.EC_POINT_mul(group, pub_key, priv_key, None, None, ctx): + raise ValueError("Could not derive public key from the supplied secret.") + ssl.EC_POINT_mul(group, pub_key, priv_key, None, None, ctx) + ssl.EC_KEY_set_private_key(self.k, priv_key) + ssl.EC_KEY_set_public_key(self.k, pub_key) + ssl.EC_POINT_free(pub_key) + ssl.BN_CTX_free(ctx) + return self.k + + def set_privkey(self, key): + self.mb = ctypes.create_string_buffer(key) + return ssl.d2i_ECPrivateKey(ctypes.byref(self.k), ctypes.byref(ctypes.pointer(self.mb)), len(key)) + + def set_pubkey(self, key): + self.mb = ctypes.create_string_buffer(key) + return ssl.o2i_ECPublicKey(ctypes.byref(self.k), ctypes.byref(ctypes.pointer(self.mb)), len(key)) + + def get_privkey(self): + size = ssl.i2d_ECPrivateKey(self.k, 0) + mb_pri = ctypes.create_string_buffer(size) + ssl.i2d_ECPrivateKey(self.k, ctypes.byref(ctypes.pointer(mb_pri))) + return mb_pri.raw + + def get_pubkey(self): + size = ssl.i2o_ECPublicKey(self.k, 0) + mb = ctypes.create_string_buffer(size) + ssl.i2o_ECPublicKey(self.k, ctypes.byref(ctypes.pointer(mb))) + return mb.raw + + def get_raw_ecdh_key(self, other_pubkey): + ecdh_keybuffer = ctypes.create_string_buffer(32) + r = ssl.ECDH_compute_key(ctypes.pointer(ecdh_keybuffer), 32, + ssl.EC_KEY_get0_public_key(other_pubkey.k), + self.k, 0) + if r != 32: + raise Exception('CKey.get_ecdh_key(): ECDH_compute_key() failed') + return ecdh_keybuffer.raw + + def get_ecdh_key(self, other_pubkey, kdf=lambda k: hashlib.sha256(k).digest()): + # FIXME: be warned it's not clear what the kdf should be as a default + r = self.get_raw_ecdh_key(other_pubkey) + return kdf(r) + + def sign(self, hash, low_s = True): + # FIXME: need unit tests for below cases + if not isinstance(hash, bytes): + raise TypeError('Hash must be bytes instance; got %r' % hash.__class__) + if len(hash) != 32: + raise ValueError('Hash must be exactly 32 bytes long') + + sig_size0 = ctypes.c_uint32() + sig_size0.value = ssl.ECDSA_size(self.k) + mb_sig = ctypes.create_string_buffer(sig_size0.value) + result = ssl.ECDSA_sign(0, hash, len(hash), mb_sig, ctypes.byref(sig_size0), self.k) + assert 1 == result + assert mb_sig.raw[0] == 0x30 + assert mb_sig.raw[1] == sig_size0.value - 2 + total_size = mb_sig.raw[1] + assert mb_sig.raw[2] == 2 + r_size = mb_sig.raw[3] + assert mb_sig.raw[4 + r_size] == 2 + s_size = mb_sig.raw[5 + r_size] + s_value = int.from_bytes(mb_sig.raw[6+r_size:6+r_size+s_size], byteorder='big') + if (not low_s) or s_value <= SECP256K1_ORDER_HALF: + return mb_sig.raw[:sig_size0.value] + else: + low_s_value = SECP256K1_ORDER - s_value + low_s_bytes = (low_s_value).to_bytes(33, byteorder='big') + while len(low_s_bytes) > 1 and low_s_bytes[0] == 0 and low_s_bytes[1] < 0x80: + low_s_bytes = low_s_bytes[1:] + new_s_size = len(low_s_bytes) + new_total_size_byte = (total_size + new_s_size - s_size).to_bytes(1,byteorder='big') + new_s_size_byte = (new_s_size).to_bytes(1,byteorder='big') + return b'\x30' + new_total_size_byte + mb_sig.raw[2:5+r_size] + new_s_size_byte + low_s_bytes + + def verify(self, hash, sig): + """Verify a DER signature""" + return ssl.ECDSA_verify(0, hash, len(hash), sig, len(sig), self.k) == 1 + + def set_compressed(self, compressed): + if compressed: + form = self.POINT_CONVERSION_COMPRESSED + else: + form = self.POINT_CONVERSION_UNCOMPRESSED + ssl.EC_KEY_set_conv_form(self.k, form) + + +class CPubKey(bytes): + """An encapsulated public key + + Attributes: + + is_valid - Corresponds to CPubKey.IsValid() + is_fullyvalid - Corresponds to CPubKey.IsFullyValid() + is_compressed - Corresponds to CPubKey.IsCompressed() + """ + + def __new__(cls, buf, _cec_key=None): + self = super(CPubKey, cls).__new__(cls, buf) + if _cec_key is None: + _cec_key = CECKey() + self._cec_key = _cec_key + self.is_fullyvalid = _cec_key.set_pubkey(self) != 0 + return self + + @property + def is_valid(self): + return len(self) > 0 + + @property + def is_compressed(self): + return len(self) == 33 + + def verify(self, hash, sig): + return self._cec_key.verify(hash, sig) + + def __str__(self): + return repr(self) + + def __repr__(self): + # Always have represent as b'<secret>' so test cases don't have to + # change for py2/3 + if sys.version > '3': + return '%s(%s)' % (self.__class__.__name__, super(CPubKey, self).__repr__()) + else: + return '%s(b%s)' % (self.__class__.__name__, super(CPubKey, self).__repr__()) + diff --git a/qa/rpc-tests/test_framework/mininode.py b/qa/rpc-tests/test_framework/mininode.py index 55d32678b..89d0ae4f1 100755 --- a/qa/rpc-tests/test_framework/mininode.py +++ b/qa/rpc-tests/test_framework/mininode.py @@ -1,7 +1,12 @@ -# mininode.py - Bitcoin P2P network half-a-node -# -# Distributed under the MIT/X11 software license, see the accompanying +#!/usr/bin/env python3 +# Copyright (c) 2010 ArtForz -- public domain half-a-node +# Copyright (c) 2012 Jeff Garzik +# Copyright (c) 2010-2016 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# +# mininode.py - Bitcoin P2P network half-a-node # # This python code was modified from ArtForz' public domain half-a-node, as # found in the mini-node branch of http://github.com/jgarzik/pynode. @@ -20,25 +25,37 @@ import struct import socket import asyncore -import binascii import time import sys import random -import cStringIO +from .util import hex_str_to_bytes, bytes_to_hex_str +from io import BytesIO +from codecs import encode import hashlib from threading import RLock from threading import Thread import logging import copy +import litecoin_scrypt +from test_framework.siphash import siphash256 BIP0031_VERSION = 60000 -MY_VERSION = 70003 # past bip-31 for ping/pong -MY_SUBVERSION = "/python-mininode-tester:0.0.1/" +MY_VERSION = 70014 # past bip-31 for ping/pong +MY_SUBVERSION = b"/python-mininode-tester:0.0.3/" +MY_RELAY = 1 # from version 70001 onwards, fRelay should be appended to version messages (BIP37) MAX_INV_SZ = 50000 +MAX_BLOCK_BASE_SIZE = 1000000 + +COIN = 100000000 # 1 btc in satoshis + +NODE_NETWORK = (1 << 0) +NODE_GETUTXO = (1 << 1) +NODE_BLOOM = (1 << 2) +NODE_WITNESS = (1 << 3) # Keep our own socket map for asyncore, so that we can track disconnects -# ourselves (to workaround an issue with closing an asyncore socket when +# ourselves (to workaround an issue with closing an asyncore socket when # using select) mininode_socket_map = dict() @@ -54,12 +71,25 @@ mininode_lock = RLock() def sha256(s): return hashlib.new('sha256', s).digest() +def ripemd160(s): + return hashlib.new('ripemd160', s).digest() def hash256(s): return sha256(sha256(s)) +def ser_compact_size(l): + r = b"" + if l < 253: + r = struct.pack("B", l) + elif l < 0x10000: + r = struct.pack("<BH", 253, l) + elif l < 0x100000000: + r = struct.pack("<BI", 254, l) + else: + r = struct.pack("<BQ", 255, l) + return r -def deser_string(f): +def deser_compact_size(f): nit = struct.unpack("<B", f.read(1))[0] if nit == 253: nit = struct.unpack("<H", f.read(2))[0] @@ -67,176 +97,130 @@ def deser_string(f): nit = struct.unpack("<I", f.read(4))[0] elif nit == 255: nit = struct.unpack("<Q", f.read(8))[0] - return f.read(nit) + return nit +def deser_string(f): + nit = deser_compact_size(f) + return f.read(nit) def ser_string(s): - if len(s) < 253: - return chr(len(s)) + s - elif len(s) < 0x10000: - return chr(253) + struct.pack("<H", len(s)) + s - elif len(s) < 0x100000000L: - return chr(254) + struct.pack("<I", len(s)) + s - return chr(255) + struct.pack("<Q", len(s)) + s - + return ser_compact_size(len(s)) + s def deser_uint256(f): - r = 0L - for i in xrange(8): + r = 0 + for i in range(8): t = struct.unpack("<I", f.read(4))[0] r += t << (i * 32) return r def ser_uint256(u): - rs = "" - for i in xrange(8): - rs += struct.pack("<I", u & 0xFFFFFFFFL) + rs = b"" + for i in range(8): + rs += struct.pack("<I", u & 0xFFFFFFFF) u >>= 32 return rs def uint256_from_str(s): - r = 0L + r = 0 t = struct.unpack("<IIIIIIII", s[:32]) - for i in xrange(8): + for i in range(8): r += t[i] << (i * 32) return r def uint256_from_compact(c): nbytes = (c >> 24) & 0xFF - v = (c & 0xFFFFFFL) << (8 * (nbytes - 3)) + v = (c & 0xFFFFFF) << (8 * (nbytes - 3)) return v def deser_vector(f, c): - nit = struct.unpack("<B", f.read(1))[0] - if nit == 253: - nit = struct.unpack("<H", f.read(2))[0] - elif nit == 254: - nit = struct.unpack("<I", f.read(4))[0] - elif nit == 255: - nit = struct.unpack("<Q", f.read(8))[0] + nit = deser_compact_size(f) r = [] - for i in xrange(nit): + for i in range(nit): t = c() t.deserialize(f) r.append(t) return r -def ser_vector(l): - r = "" - if len(l) < 253: - r = chr(len(l)) - elif len(l) < 0x10000: - r = chr(253) + struct.pack("<H", len(l)) - elif len(l) < 0x100000000L: - r = chr(254) + struct.pack("<I", len(l)) - else: - r = chr(255) + struct.pack("<Q", len(l)) +# ser_function_name: Allow for an alternate serialization function on the +# entries in the vector (we use this for serializing the vector of transactions +# for a witness block). +def ser_vector(l, ser_function_name=None): + r = ser_compact_size(len(l)) for i in l: - r += i.serialize() + if ser_function_name: + r += getattr(i, ser_function_name)() + else: + r += i.serialize() return r def deser_uint256_vector(f): - nit = struct.unpack("<B", f.read(1))[0] - if nit == 253: - nit = struct.unpack("<H", f.read(2))[0] - elif nit == 254: - nit = struct.unpack("<I", f.read(4))[0] - elif nit == 255: - nit = struct.unpack("<Q", f.read(8))[0] + nit = deser_compact_size(f) r = [] - for i in xrange(nit): + for i in range(nit): t = deser_uint256(f) r.append(t) return r def ser_uint256_vector(l): - r = "" - if len(l) < 253: - r = chr(len(l)) - elif len(l) < 0x10000: - r = chr(253) + struct.pack("<H", len(l)) - elif len(l) < 0x100000000L: - r = chr(254) + struct.pack("<I", len(l)) - else: - r = chr(255) + struct.pack("<Q", len(l)) + r = ser_compact_size(len(l)) for i in l: r += ser_uint256(i) return r def deser_string_vector(f): - nit = struct.unpack("<B", f.read(1))[0] - if nit == 253: - nit = struct.unpack("<H", f.read(2))[0] - elif nit == 254: - nit = struct.unpack("<I", f.read(4))[0] - elif nit == 255: - nit = struct.unpack("<Q", f.read(8))[0] + nit = deser_compact_size(f) r = [] - for i in xrange(nit): + for i in range(nit): t = deser_string(f) r.append(t) return r def ser_string_vector(l): - r = "" - if len(l) < 253: - r = chr(len(l)) - elif len(l) < 0x10000: - r = chr(253) + struct.pack("<H", len(l)) - elif len(l) < 0x100000000L: - r = chr(254) + struct.pack("<I", len(l)) - else: - r = chr(255) + struct.pack("<Q", len(l)) + r = ser_compact_size(len(l)) for sv in l: r += ser_string(sv) return r def deser_int_vector(f): - nit = struct.unpack("<B", f.read(1))[0] - if nit == 253: - nit = struct.unpack("<H", f.read(2))[0] - elif nit == 254: - nit = struct.unpack("<I", f.read(4))[0] - elif nit == 255: - nit = struct.unpack("<Q", f.read(8))[0] + nit = deser_compact_size(f) r = [] - for i in xrange(nit): + for i in range(nit): t = struct.unpack("<i", f.read(4))[0] r.append(t) return r def ser_int_vector(l): - r = "" - if len(l) < 253: - r = chr(len(l)) - elif len(l) < 0x10000: - r = chr(253) + struct.pack("<H", len(l)) - elif len(l) < 0x100000000L: - r = chr(254) + struct.pack("<I", len(l)) - else: - r = chr(255) + struct.pack("<Q", len(l)) + r = ser_compact_size(len(l)) for i in l: r += struct.pack("<i", i) return r +# Deserialize from a hex string representation (eg from RPC) +def FromHex(obj, hex_string): + obj.deserialize(BytesIO(hex_str_to_bytes(hex_string))) + return obj + +# Convert a binary-serializable object to hex (eg for submission via RPC) +def ToHex(obj): + return bytes_to_hex_str(obj.serialize()) # Objects that map to bitcoind objects, which can be serialized/deserialized class CAddress(object): def __init__(self): self.nServices = 1 - self.pchReserved = "\x00" * 10 + "\xff" * 2 + self.pchReserved = b"\x00" * 10 + b"\xff" * 2 self.ip = "0.0.0.0" self.port = 0 @@ -247,7 +231,7 @@ class CAddress(object): self.port = struct.unpack(">H", f.read(2))[0] def serialize(self): - r = "" + r = b"" r += struct.pack("<Q", self.nServices) r += self.pchReserved r += socket.inet_aton(self.ip) @@ -258,14 +242,19 @@ class CAddress(object): return "CAddress(nServices=%i ip=%s port=%i)" % (self.nServices, self.ip, self.port) +MSG_WITNESS_FLAG = 1<<30 class CInv(object): typemap = { 0: "Error", 1: "TX", - 2: "Block"} + 2: "Block", + 1|MSG_WITNESS_FLAG: "WitnessTx", + 2|MSG_WITNESS_FLAG : "WitnessBlock", + 4: "CompactBlock" + } - def __init__(self, t=0, h=0L): + def __init__(self, t=0, h=0): self.type = t self.hash = h @@ -274,7 +263,7 @@ class CInv(object): self.hash = deser_uint256(f) def serialize(self): - r = "" + r = b"" r += struct.pack("<i", self.type) r += ser_uint256(self.hash) return r @@ -294,7 +283,7 @@ class CBlockLocator(object): self.vHave = deser_uint256_vector(f) def serialize(self): - r = "" + r = b"" r += struct.pack("<i", self.nVersion) r += ser_uint256_vector(self.vHave) return r @@ -314,7 +303,7 @@ class COutPoint(object): self.n = struct.unpack("<I", f.read(4))[0] def serialize(self): - r = "" + r = b"" r += ser_uint256(self.hash) r += struct.pack("<I", self.n) return r @@ -324,7 +313,7 @@ class COutPoint(object): class CTxIn(object): - def __init__(self, outpoint=None, scriptSig="", nSequence=0): + def __init__(self, outpoint=None, scriptSig=b"", nSequence=0): if outpoint is None: self.prevout = COutPoint() else: @@ -339,7 +328,7 @@ class CTxIn(object): self.nSequence = struct.unpack("<I", f.read(4))[0] def serialize(self): - r = "" + r = b"" r += self.prevout.serialize() r += ser_string(self.scriptSig) r += struct.pack("<I", self.nSequence) @@ -347,12 +336,12 @@ class CTxIn(object): def __repr__(self): return "CTxIn(prevout=%s scriptSig=%s nSequence=%i)" \ - % (repr(self.prevout), binascii.hexlify(self.scriptSig), + % (repr(self.prevout), bytes_to_hex_str(self.scriptSig), self.nSequence) class CTxOut(object): - def __init__(self, nValue=0, scriptPubKey=""): + def __init__(self, nValue=0, scriptPubKey=b""): self.nValue = nValue self.scriptPubKey = scriptPubKey @@ -361,15 +350,75 @@ class CTxOut(object): self.scriptPubKey = deser_string(f) def serialize(self): - r = "" + r = b"" r += struct.pack("<q", self.nValue) r += ser_string(self.scriptPubKey) return r def __repr__(self): return "CTxOut(nValue=%i.%08i scriptPubKey=%s)" \ - % (self.nValue // 100000000, self.nValue % 100000000, - binascii.hexlify(self.scriptPubKey)) + % (self.nValue // COIN, self.nValue % COIN, + bytes_to_hex_str(self.scriptPubKey)) + + +class CScriptWitness(object): + def __init__(self): + # stack is a vector of strings + self.stack = [] + + def __repr__(self): + return "CScriptWitness(%s)" % \ + (",".join([bytes_to_hex_str(x) for x in self.stack])) + + def is_null(self): + if self.stack: + return False + return True + + +class CTxInWitness(object): + def __init__(self): + self.scriptWitness = CScriptWitness() + + def deserialize(self, f): + self.scriptWitness.stack = deser_string_vector(f) + + def serialize(self): + return ser_string_vector(self.scriptWitness.stack) + + def __repr__(self): + return repr(self.scriptWitness) + + def is_null(self): + return self.scriptWitness.is_null() + + +class CTxWitness(object): + def __init__(self): + self.vtxinwit = [] + + def deserialize(self, f): + for i in range(len(self.vtxinwit)): + self.vtxinwit[i].deserialize(f) + + def serialize(self): + r = b"" + # This is different than the usual vector serialization -- + # we omit the length of the vector, which is required to be + # the same length as the transaction's vin vector. + for x in self.vtxinwit: + r += x.serialize() + return r + + def __repr__(self): + return "CTxWitness(%s)" % \ + (';'.join([repr(x) for x in self.vtxinwit])) + + def is_null(self): + for x in self.vtxinwit: + if not x.is_null(): + return False + return True class CTransaction(object): @@ -378,6 +427,7 @@ class CTransaction(object): self.nVersion = 1 self.vin = [] self.vout = [] + self.wit = CTxWitness() self.nLockTime = 0 self.sha256 = None self.hash = None @@ -386,44 +436,92 @@ class CTransaction(object): self.vin = copy.deepcopy(tx.vin) self.vout = copy.deepcopy(tx.vout) self.nLockTime = tx.nLockTime - self.sha256 = None - self.hash = None + self.sha256 = tx.sha256 + self.hash = tx.hash + self.wit = copy.deepcopy(tx.wit) def deserialize(self, f): self.nVersion = struct.unpack("<i", f.read(4))[0] self.vin = deser_vector(f, CTxIn) - self.vout = deser_vector(f, CTxOut) + flags = 0 + if len(self.vin) == 0: + flags = struct.unpack("<B", f.read(1))[0] + # Not sure why flags can't be zero, but this + # matches the implementation in bitcoind + if (flags != 0): + self.vin = deser_vector(f, CTxIn) + self.vout = deser_vector(f, CTxOut) + else: + self.vout = deser_vector(f, CTxOut) + if flags != 0: + self.wit.vtxinwit = [CTxInWitness() for i in range(len(self.vin))] + self.wit.deserialize(f) self.nLockTime = struct.unpack("<I", f.read(4))[0] self.sha256 = None self.hash = None - def serialize(self): - r = "" + def serialize_without_witness(self): + r = b"" + r += struct.pack("<i", self.nVersion) + r += ser_vector(self.vin) + r += ser_vector(self.vout) + r += struct.pack("<I", self.nLockTime) + return r + + # Only serialize with witness when explicitly called for + def serialize_with_witness(self): + flags = 0 + if not self.wit.is_null(): + flags |= 1 + r = b"" r += struct.pack("<i", self.nVersion) + if flags: + dummy = [] + r += ser_vector(dummy) + r += struct.pack("<B", flags) r += ser_vector(self.vin) r += ser_vector(self.vout) + if flags & 1: + if (len(self.wit.vtxinwit) != len(self.vin)): + # vtxinwit must have the same length as vin + self.wit.vtxinwit = self.wit.vtxinwit[:len(self.vin)] + for i in range(len(self.wit.vtxinwit), len(self.vin)): + self.wit.vtxinwit.append(CTxInWitness()) + r += self.wit.serialize() r += struct.pack("<I", self.nLockTime) return r + # Regular serialization is without witness -- must explicitly + # call serialize_with_witness to include witness data. + def serialize(self): + return self.serialize_without_witness() + + # Recalculate the txid (transaction hash without witness) def rehash(self): self.sha256 = None self.calc_sha256() - def calc_sha256(self): + # We will only cache the serialization without witness in + # self.sha256 and self.hash -- those are expected to be the txid. + def calc_sha256(self, with_witness=False): + if with_witness: + # Don't cache the result, just return it + return uint256_from_str(hash256(self.serialize_with_witness())) + if self.sha256 is None: - self.sha256 = uint256_from_str(hash256(self.serialize())) - self.hash = hash256(self.serialize())[::-1].encode('hex_codec') + self.sha256 = uint256_from_str(hash256(self.serialize_without_witness())) + self.hash = encode(hash256(self.serialize())[::-1], 'hex_codec').decode('ascii') def is_valid(self): self.calc_sha256() for tout in self.vout: - if tout.nValue < 0 or tout.nValue > 21000000L * 100000000L: + if tout.nValue < 0 or tout.nValue > 21000000 * COIN: return False return True def __repr__(self): - return "CTransaction(nVersion=%i vin=%s vout=%s nLockTime=%i)" \ - % (self.nVersion, repr(self.vin), repr(self.vout), self.nLockTime) + return "CTransaction(nVersion=%i vin=%s vout=%s wit=%s nLockTime=%i)" \ + % (self.nVersion, repr(self.vin), repr(self.vout), repr(self.wit), self.nLockTime) class CBlockHeader(object): @@ -439,6 +537,7 @@ class CBlockHeader(object): self.nNonce = header.nNonce self.sha256 = header.sha256 self.hash = header.hash + self.scrypt256 = header.scrypt256 self.calc_sha256() def set_null(self): @@ -450,6 +549,7 @@ class CBlockHeader(object): self.nNonce = 0 self.sha256 = None self.hash = None + self.scrypt256 = None def deserialize(self, f): self.nVersion = struct.unpack("<i", f.read(4))[0] @@ -460,9 +560,10 @@ class CBlockHeader(object): self.nNonce = struct.unpack("<I", f.read(4))[0] self.sha256 = None self.hash = None + self.scrypt256 = None def serialize(self): - r = "" + r = b"" r += struct.pack("<i", self.nVersion) r += ser_uint256(self.hashPrevBlock) r += ser_uint256(self.hashMerkleRoot) @@ -473,7 +574,7 @@ class CBlockHeader(object): def calc_sha256(self): if self.sha256 is None: - r = "" + r = b"" r += struct.pack("<i", self.nVersion) r += ser_uint256(self.hashPrevBlock) r += ser_uint256(self.hashMerkleRoot) @@ -481,10 +582,12 @@ class CBlockHeader(object): r += struct.pack("<I", self.nBits) r += struct.pack("<I", self.nNonce) self.sha256 = uint256_from_str(hash256(r)) - self.hash = hash256(r)[::-1].encode('hex_codec') + self.hash = encode(hash256(r)[::-1], 'hex_codec').decode('ascii') + self.scrypt256 = uint256_from_str(litecoin_scrypt.getPoWHash(r)) def rehash(self): self.sha256 = None + self.scrypt256 = None self.calc_sha256() return self.sha256 @@ -503,29 +606,47 @@ class CBlock(CBlockHeader): super(CBlock, self).deserialize(f) self.vtx = deser_vector(f, CTransaction) - def serialize(self): - r = "" + def serialize(self, with_witness=False): + r = b"" r += super(CBlock, self).serialize() - r += ser_vector(self.vtx) + if with_witness: + r += ser_vector(self.vtx, "serialize_with_witness") + else: + r += ser_vector(self.vtx) return r - def calc_merkle_root(self): - hashes = [] - for tx in self.vtx: - tx.calc_sha256() - hashes.append(ser_uint256(tx.sha256)) + # Calculate the merkle root given a vector of transaction hashes + def get_merkle_root(self, hashes): while len(hashes) > 1: newhashes = [] - for i in xrange(0, len(hashes), 2): + for i in range(0, len(hashes), 2): i2 = min(i+1, len(hashes)-1) newhashes.append(hash256(hashes[i] + hashes[i2])) hashes = newhashes return uint256_from_str(hashes[0]) + def calc_merkle_root(self): + hashes = [] + for tx in self.vtx: + tx.calc_sha256() + hashes.append(ser_uint256(tx.sha256)) + return self.get_merkle_root(hashes) + + def calc_witness_merkle_root(self): + # For witness root purposes, the hash of the + # coinbase, with witness, is defined to be 0...0 + hashes = [ser_uint256(0)] + + for tx in self.vtx[1:]: + # Calculate the hashes with witness data + hashes.append(ser_uint256(tx.calc_sha256(True))) + + return self.get_merkle_root(hashes) + def is_valid(self): self.calc_sha256() target = uint256_from_compact(self.nBits) - if self.sha256 > target: + if self.scrypt256 > target: return False for tx in self.vtx: if not tx.is_valid(): @@ -535,9 +656,9 @@ class CBlock(CBlockHeader): return True def solve(self): - self.calc_sha256() + self.rehash() target = uint256_from_compact(self.nBits) - while self.sha256 > target: + while self.scrypt256 > target: self.nNonce += 1 self.rehash() @@ -559,9 +680,9 @@ class CUnsignedAlert(object): self.nMaxVer = 0 self.setSubVer = [] self.nPriority = 0 - self.strComment = "" - self.strStatusBar = "" - self.strReserved = "" + self.strComment = b"" + self.strStatusBar = b"" + self.strReserved = b"" def deserialize(self, f): self.nVersion = struct.unpack("<i", f.read(4))[0] @@ -579,7 +700,7 @@ class CUnsignedAlert(object): self.strReserved = deser_string(f) def serialize(self): - r = "" + r = b"" r += struct.pack("<i", self.nVersion) r += struct.pack("<q", self.nRelayUntil) r += struct.pack("<q", self.nExpiration) @@ -604,15 +725,15 @@ class CUnsignedAlert(object): class CAlert(object): def __init__(self): - self.vchMsg = "" - self.vchSig = "" + self.vchMsg = b"" + self.vchSig = b"" def deserialize(self, f): self.vchMsg = deser_string(f) self.vchSig = deser_string(f) def serialize(self): - r = "" + r = b"" r += ser_string(self.vchMsg) r += ser_string(self.vchSig) return r @@ -622,19 +743,222 @@ class CAlert(object): % (len(self.vchMsg), len(self.vchSig)) +class PrefilledTransaction(object): + def __init__(self, index=0, tx = None): + self.index = index + self.tx = tx + + def deserialize(self, f): + self.index = deser_compact_size(f) + self.tx = CTransaction() + self.tx.deserialize(f) + + def serialize(self, with_witness=False): + r = b"" + r += ser_compact_size(self.index) + if with_witness: + r += self.tx.serialize_with_witness() + else: + r += self.tx.serialize_without_witness() + return r + + def serialize_with_witness(self): + return self.serialize(with_witness=True) + + def __repr__(self): + return "PrefilledTransaction(index=%d, tx=%s)" % (self.index, repr(self.tx)) + +# This is what we send on the wire, in a cmpctblock message. +class P2PHeaderAndShortIDs(object): + def __init__(self): + self.header = CBlockHeader() + self.nonce = 0 + self.shortids_length = 0 + self.shortids = [] + self.prefilled_txn_length = 0 + self.prefilled_txn = [] + + def deserialize(self, f): + self.header.deserialize(f) + self.nonce = struct.unpack("<Q", f.read(8))[0] + self.shortids_length = deser_compact_size(f) + for i in range(self.shortids_length): + # shortids are defined to be 6 bytes in the spec, so append + # two zero bytes and read it in as an 8-byte number + self.shortids.append(struct.unpack("<Q", f.read(6) + b'\x00\x00')[0]) + self.prefilled_txn = deser_vector(f, PrefilledTransaction) + self.prefilled_txn_length = len(self.prefilled_txn) + + # When using version 2 compact blocks, we must serialize with_witness. + def serialize(self, with_witness=False): + r = b"" + r += self.header.serialize() + r += struct.pack("<Q", self.nonce) + r += ser_compact_size(self.shortids_length) + for x in self.shortids: + # We only want the first 6 bytes + r += struct.pack("<Q", x)[0:6] + if with_witness: + r += ser_vector(self.prefilled_txn, "serialize_with_witness") + else: + r += ser_vector(self.prefilled_txn) + return r + + def __repr__(self): + return "P2PHeaderAndShortIDs(header=%s, nonce=%d, shortids_length=%d, shortids=%s, prefilled_txn_length=%d, prefilledtxn=%s" % (repr(self.header), self.nonce, self.shortids_length, repr(self.shortids), self.prefilled_txn_length, repr(self.prefilled_txn)) + +# P2P version of the above that will use witness serialization (for compact +# block version 2) +class P2PHeaderAndShortWitnessIDs(P2PHeaderAndShortIDs): + def serialize(self): + return super(P2PHeaderAndShortWitnessIDs, self).serialize(with_witness=True) + +# Calculate the BIP 152-compact blocks shortid for a given transaction hash +def calculate_shortid(k0, k1, tx_hash): + expected_shortid = siphash256(k0, k1, tx_hash) + expected_shortid &= 0x0000ffffffffffff + return expected_shortid + +# This version gets rid of the array lengths, and reinterprets the differential +# encoding into indices that can be used for lookup. +class HeaderAndShortIDs(object): + def __init__(self, p2pheaders_and_shortids = None): + self.header = CBlockHeader() + self.nonce = 0 + self.shortids = [] + self.prefilled_txn = [] + self.use_witness = False + + if p2pheaders_and_shortids != None: + self.header = p2pheaders_and_shortids.header + self.nonce = p2pheaders_and_shortids.nonce + self.shortids = p2pheaders_and_shortids.shortids + last_index = -1 + for x in p2pheaders_and_shortids.prefilled_txn: + self.prefilled_txn.append(PrefilledTransaction(x.index + last_index + 1, x.tx)) + last_index = self.prefilled_txn[-1].index + + def to_p2p(self): + if self.use_witness: + ret = P2PHeaderAndShortWitnessIDs() + else: + ret = P2PHeaderAndShortIDs() + ret.header = self.header + ret.nonce = self.nonce + ret.shortids_length = len(self.shortids) + ret.shortids = self.shortids + ret.prefilled_txn_length = len(self.prefilled_txn) + ret.prefilled_txn = [] + last_index = -1 + for x in self.prefilled_txn: + ret.prefilled_txn.append(PrefilledTransaction(x.index - last_index - 1, x.tx)) + last_index = x.index + return ret + + def get_siphash_keys(self): + header_nonce = self.header.serialize() + header_nonce += struct.pack("<Q", self.nonce) + hash_header_nonce_as_str = sha256(header_nonce) + key0 = struct.unpack("<Q", hash_header_nonce_as_str[0:8])[0] + key1 = struct.unpack("<Q", hash_header_nonce_as_str[8:16])[0] + return [ key0, key1 ] + + # Version 2 compact blocks use wtxid in shortids (rather than txid) + def initialize_from_block(self, block, nonce=0, prefill_list = [0], use_witness = False): + self.header = CBlockHeader(block) + self.nonce = nonce + self.prefilled_txn = [ PrefilledTransaction(i, block.vtx[i]) for i in prefill_list ] + self.shortids = [] + self.use_witness = use_witness + [k0, k1] = self.get_siphash_keys() + for i in range(len(block.vtx)): + if i not in prefill_list: + tx_hash = block.vtx[i].sha256 + if use_witness: + tx_hash = block.vtx[i].calc_sha256(with_witness=True) + self.shortids.append(calculate_shortid(k0, k1, tx_hash)) + + def __repr__(self): + return "HeaderAndShortIDs(header=%s, nonce=%d, shortids=%s, prefilledtxn=%s" % (repr(self.header), self.nonce, repr(self.shortids), repr(self.prefilled_txn)) + + +class BlockTransactionsRequest(object): + + def __init__(self, blockhash=0, indexes = None): + self.blockhash = blockhash + self.indexes = indexes if indexes != None else [] + + def deserialize(self, f): + self.blockhash = deser_uint256(f) + indexes_length = deser_compact_size(f) + for i in range(indexes_length): + self.indexes.append(deser_compact_size(f)) + + def serialize(self): + r = b"" + r += ser_uint256(self.blockhash) + r += ser_compact_size(len(self.indexes)) + for x in self.indexes: + r += ser_compact_size(x) + return r + + # helper to set the differentially encoded indexes from absolute ones + def from_absolute(self, absolute_indexes): + self.indexes = [] + last_index = -1 + for x in absolute_indexes: + self.indexes.append(x-last_index-1) + last_index = x + + def to_absolute(self): + absolute_indexes = [] + last_index = -1 + for x in self.indexes: + absolute_indexes.append(x+last_index+1) + last_index = absolute_indexes[-1] + return absolute_indexes + + def __repr__(self): + return "BlockTransactionsRequest(hash=%064x indexes=%s)" % (self.blockhash, repr(self.indexes)) + + +class BlockTransactions(object): + + def __init__(self, blockhash=0, transactions = None): + self.blockhash = blockhash + self.transactions = transactions if transactions != None else [] + + def deserialize(self, f): + self.blockhash = deser_uint256(f) + self.transactions = deser_vector(f, CTransaction) + + def serialize(self, with_witness=False): + r = b"" + r += ser_uint256(self.blockhash) + if with_witness: + r += ser_vector(self.transactions, "serialize_with_witness") + else: + r += ser_vector(self.transactions) + return r + + def __repr__(self): + return "BlockTransactions(hash=%064x transactions=%s)" % (self.blockhash, repr(self.transactions)) + + # Objects that correspond to messages on the wire class msg_version(object): - command = "version" + command = b"version" def __init__(self): self.nVersion = MY_VERSION self.nServices = 1 - self.nTime = time.time() + self.nTime = int(time.time()) self.addrTo = CAddress() self.addrFrom = CAddress() self.nNonce = random.getrandbits(64) self.strSubVer = MY_SUBVERSION self.nStartingHeight = -1 + self.nRelay = MY_RELAY def deserialize(self, f): self.nVersion = struct.unpack("<i", f.read(4))[0] @@ -644,23 +968,34 @@ class msg_version(object): self.nTime = struct.unpack("<q", f.read(8))[0] self.addrTo = CAddress() self.addrTo.deserialize(f) + if self.nVersion >= 106: self.addrFrom = CAddress() self.addrFrom.deserialize(f) self.nNonce = struct.unpack("<Q", f.read(8))[0] self.strSubVer = deser_string(f) - if self.nVersion >= 209: - self.nStartingHeight = struct.unpack("<i", f.read(4))[0] - else: - self.nStartingHeight = None else: self.addrFrom = None self.nNonce = None self.strSubVer = None self.nStartingHeight = None + if self.nVersion >= 209: + self.nStartingHeight = struct.unpack("<i", f.read(4))[0] + else: + self.nStartingHeight = None + + if self.nVersion >= 70001: + # Relay field is optional for version 70001 onwards + try: + self.nRelay = struct.unpack("<b", f.read(1))[0] + except: + self.nRelay = 0 + else: + self.nRelay = 0 + def serialize(self): - r = "" + r = b"" r += struct.pack("<i", self.nVersion) r += struct.pack("<Q", self.nServices) r += struct.pack("<q", self.nTime) @@ -669,17 +1004,18 @@ class msg_version(object): r += struct.pack("<Q", self.nNonce) r += ser_string(self.strSubVer) r += struct.pack("<i", self.nStartingHeight) + r += struct.pack("<b", self.nRelay) return r def __repr__(self): - return 'msg_version(nVersion=%i nServices=%i nTime=%s addrTo=%s addrFrom=%s nNonce=0x%016X strSubVer=%s nStartingHeight=%i)' \ + return 'msg_version(nVersion=%i nServices=%i nTime=%s addrTo=%s addrFrom=%s nNonce=0x%016X strSubVer=%s nStartingHeight=%i nRelay=%i)' \ % (self.nVersion, self.nServices, time.ctime(self.nTime), repr(self.addrTo), repr(self.addrFrom), self.nNonce, - self.strSubVer, self.nStartingHeight) + self.strSubVer, self.nStartingHeight, self.nRelay) class msg_verack(object): - command = "verack" + command = b"verack" def __init__(self): pass @@ -688,14 +1024,14 @@ class msg_verack(object): pass def serialize(self): - return "" + return b"" def __repr__(self): return "msg_verack()" class msg_addr(object): - command = "addr" + command = b"addr" def __init__(self): self.addrs = [] @@ -711,7 +1047,7 @@ class msg_addr(object): class msg_alert(object): - command = "alert" + command = b"alert" def __init__(self): self.alert = CAlert() @@ -721,7 +1057,7 @@ class msg_alert(object): self.alert.deserialize(f) def serialize(self): - r = "" + r = b"" r += self.alert.serialize() return r @@ -730,7 +1066,7 @@ class msg_alert(object): class msg_inv(object): - command = "inv" + command = b"inv" def __init__(self, inv=None): if inv is None: @@ -749,10 +1085,10 @@ class msg_inv(object): class msg_getdata(object): - command = "getdata" + command = b"getdata" - def __init__(self): - self.inv = [] + def __init__(self, inv=None): + self.inv = inv if inv != None else [] def deserialize(self, f): self.inv = deser_vector(f, CInv) @@ -765,11 +1101,11 @@ class msg_getdata(object): class msg_getblocks(object): - command = "getblocks" + command = b"getblocks" def __init__(self): self.locator = CBlockLocator() - self.hashstop = 0L + self.hashstop = 0 def deserialize(self, f): self.locator = CBlockLocator() @@ -777,7 +1113,7 @@ class msg_getblocks(object): self.hashstop = deser_uint256(f) def serialize(self): - r = "" + r = b"" r += self.locator.serialize() r += ser_uint256(self.hashstop) return r @@ -788,7 +1124,7 @@ class msg_getblocks(object): class msg_tx(object): - command = "tx" + command = b"tx" def __init__(self, tx=CTransaction()): self.tx = tx @@ -797,14 +1133,19 @@ class msg_tx(object): self.tx.deserialize(f) def serialize(self): - return self.tx.serialize() + return self.tx.serialize_without_witness() def __repr__(self): return "msg_tx(tx=%s)" % (repr(self.tx)) +class msg_witness_tx(msg_tx): + + def serialize(self): + return self.tx.serialize_with_witness() + class msg_block(object): - command = "block" + command = b"block" def __init__(self, block=None): if block is None: @@ -821,9 +1162,27 @@ class msg_block(object): def __repr__(self): return "msg_block(block=%s)" % (repr(self.block)) +# for cases where a user needs tighter control over what is sent over the wire +# note that the user must supply the name of the command, and the data +class msg_generic(object): + def __init__(self, command, data=None): + self.command = command + self.data = data + + def serialize(self): + return self.data + + def __repr__(self): + return "msg_generic()" + +class msg_witness_block(msg_block): + + def serialize(self): + r = self.block.serialize(with_witness=True) + return r class msg_getaddr(object): - command = "getaddr" + command = b"getaddr" def __init__(self): pass @@ -832,14 +1191,14 @@ class msg_getaddr(object): pass def serialize(self): - return "" + return b"" def __repr__(self): return "msg_getaddr()" class msg_ping_prebip31(object): - command = "ping" + command = b"ping" def __init__(self): pass @@ -848,23 +1207,23 @@ class msg_ping_prebip31(object): pass def serialize(self): - return "" + return b"" def __repr__(self): return "msg_ping() (pre-bip31)" class msg_ping(object): - command = "ping" + command = b"ping" - def __init__(self, nonce=0L): + def __init__(self, nonce=0): self.nonce = nonce def deserialize(self, f): self.nonce = struct.unpack("<Q", f.read(8))[0] def serialize(self): - r = "" + r = b"" r += struct.pack("<Q", self.nonce) return r @@ -873,16 +1232,16 @@ class msg_ping(object): class msg_pong(object): - command = "pong" + command = b"pong" - def __init__(self, nonce=0L): + def __init__(self, nonce=0): self.nonce = nonce def deserialize(self, f): self.nonce = struct.unpack("<Q", f.read(8))[0] def serialize(self): - r = "" + r = b"" r += struct.pack("<Q", self.nonce) return r @@ -891,7 +1250,7 @@ class msg_pong(object): class msg_mempool(object): - command = "mempool" + command = b"mempool" def __init__(self): pass @@ -900,22 +1259,37 @@ class msg_mempool(object): pass def serialize(self): - return "" + return b"" def __repr__(self): return "msg_mempool()" +class msg_sendheaders(object): + command = b"sendheaders" + + def __init__(self): + pass + + def deserialize(self, f): + pass + + def serialize(self): + return b"" + + def __repr__(self): + return "msg_sendheaders()" + # getheaders message has # number of entries # vector of hashes # hash_stop (hash of last desired block header, 0 to get as many as possible) class msg_getheaders(object): - command = "getheaders" + command = b"getheaders" def __init__(self): self.locator = CBlockLocator() - self.hashstop = 0L + self.hashstop = 0 def deserialize(self, f): self.locator = CBlockLocator() @@ -923,7 +1297,7 @@ class msg_getheaders(object): self.hashstop = deser_uint256(f) def serialize(self): - r = "" + r = b"" r += self.locator.serialize() r += ser_uint256(self.hashstop) return r @@ -936,7 +1310,7 @@ class msg_getheaders(object): # headers message has # <count> <vector of block headers> class msg_headers(object): - command = "headers" + command = b"headers" def __init__(self): self.headers = [] @@ -956,26 +1330,29 @@ class msg_headers(object): class msg_reject(object): - command = "reject" + command = b"reject" + REJECT_MALFORMED = 1 def __init__(self): - self.message = "" - self.code = "" - self.reason = "" - self.data = 0L + self.message = b"" + self.code = 0 + self.reason = b"" + self.data = 0 def deserialize(self, f): self.message = deser_string(f) self.code = struct.unpack("<B", f.read(1))[0] self.reason = deser_string(f) - if (self.message == "block" or self.message == "tx"): + if (self.code != self.REJECT_MALFORMED and + (self.message == b"block" or self.message == b"tx")): self.data = deser_uint256(f) def serialize(self): r = ser_string(self.message) r += struct.pack("<B", self.code) r += ser_string(self.reason) - if (self.message == "block" or self.message == "tx"): + if (self.code != self.REJECT_MALFORMED and + (self.message == b"block" or self.message == b"tx")): r += ser_uint256(self.data) return r @@ -983,42 +1360,158 @@ class msg_reject(object): return "msg_reject: %s %d %s [%064x]" \ % (self.message, self.code, self.reason, self.data) +# Helper function +def wait_until(predicate, *, attempts=float('inf'), timeout=float('inf')): + attempt = 0 + elapsed = 0 + + while attempt < attempts and elapsed < timeout: + with mininode_lock: + if predicate(): + return True + attempt += 1 + elapsed += 0.05 + time.sleep(0.05) + + return False + +class msg_feefilter(object): + command = b"feefilter" + + def __init__(self, feerate=0): + self.feerate = feerate + + def deserialize(self, f): + self.feerate = struct.unpack("<Q", f.read(8))[0] + + def serialize(self): + r = b"" + r += struct.pack("<Q", self.feerate) + return r + + def __repr__(self): + return "msg_feefilter(feerate=%08x)" % self.feerate + +class msg_sendcmpct(object): + command = b"sendcmpct" + + def __init__(self): + self.announce = False + self.version = 1 + + def deserialize(self, f): + self.announce = struct.unpack("<?", f.read(1))[0] + self.version = struct.unpack("<Q", f.read(8))[0] + + def serialize(self): + r = b"" + r += struct.pack("<?", self.announce) + r += struct.pack("<Q", self.version) + return r + + def __repr__(self): + return "msg_sendcmpct(announce=%s, version=%lu)" % (self.announce, self.version) + +class msg_cmpctblock(object): + command = b"cmpctblock" + + def __init__(self, header_and_shortids = None): + self.header_and_shortids = header_and_shortids + + def deserialize(self, f): + self.header_and_shortids = P2PHeaderAndShortIDs() + self.header_and_shortids.deserialize(f) + + def serialize(self): + r = b"" + r += self.header_and_shortids.serialize() + return r + + def __repr__(self): + return "msg_cmpctblock(HeaderAndShortIDs=%s)" % repr(self.header_and_shortids) + +class msg_getblocktxn(object): + command = b"getblocktxn" + + def __init__(self): + self.block_txn_request = None + + def deserialize(self, f): + self.block_txn_request = BlockTransactionsRequest() + self.block_txn_request.deserialize(f) + + def serialize(self): + r = b"" + r += self.block_txn_request.serialize() + return r + + def __repr__(self): + return "msg_getblocktxn(block_txn_request=%s)" % (repr(self.block_txn_request)) + +class msg_blocktxn(object): + command = b"blocktxn" + + def __init__(self): + self.block_transactions = BlockTransactions() + + def deserialize(self, f): + self.block_transactions.deserialize(f) + + def serialize(self): + r = b"" + r += self.block_transactions.serialize() + return r + + def __repr__(self): + return "msg_blocktxn(block_transactions=%s)" % (repr(self.block_transactions)) + +class msg_witness_blocktxn(msg_blocktxn): + def serialize(self): + r = b"" + r += self.block_transactions.serialize(with_witness=True) + return r # This is what a callback should look like for NodeConn # Reimplement the on_* functions to provide handling for events class NodeConnCB(object): def __init__(self): self.verack_received = False + # deliver_sleep_time is helpful for debugging race conditions in p2p + # tests; it causes message delivery to sleep for the specified time + # before acquiring the global lock and delivering the next message. + self.deliver_sleep_time = None + # Remember the services our peer has advertised + self.peer_services = None + + def set_deliver_sleep_time(self, value): + with mininode_lock: + self.deliver_sleep_time = value + + def get_deliver_sleep_time(self): + with mininode_lock: + return self.deliver_sleep_time - # Derived classes should call this function once to set the message map - # which associates the derived classes' functions to incoming messages - def create_callback_map(self): - self.cbmap = { - "version": self.on_version, - "verack": self.on_verack, - "addr": self.on_addr, - "alert": self.on_alert, - "inv": self.on_inv, - "getdata": self.on_getdata, - "getblocks": self.on_getblocks, - "tx": self.on_tx, - "block": self.on_block, - "getaddr": self.on_getaddr, - "ping": self.on_ping, - "pong": self.on_pong, - "headers": self.on_headers, - "getheaders": self.on_getheaders, - "reject": self.on_reject, - "mempool": self.on_mempool - } + # Spin until verack message is received from the node. + # Tests may want to use this as a signal that the test can begin. + # This can be called from the testing thread, so it needs to acquire the + # global lock. + def wait_for_verack(self): + while True: + with mininode_lock: + if self.verack_received: + return + time.sleep(0.05) def deliver(self, conn, message): + deliver_sleep = self.get_deliver_sleep_time() + if deliver_sleep is not None: + time.sleep(deliver_sleep) with mininode_lock: try: - self.cbmap[message.command](conn, message) + getattr(self, 'on_' + message.command.decode('ascii'))(conn, message) except: - print "ERROR delivering %s (%s)" % (repr(message), - sys.exc_info()[0]) + print("ERROR delivering %s (%s)" % (repr(message), + sys.exc_info()[0])) def on_version(self, conn, message): if message.nVersion >= 209: @@ -1026,6 +1519,7 @@ class NodeConnCB(object): conn.ver_send = min(MY_VERSION, message.nVersion) if message.nVersion < 209: conn.ver_recv = conn.ver_send + conn.nServices = message.nServices def on_verack(self, conn, message): conn.ver_recv = conn.ver_send @@ -1052,46 +1546,89 @@ class NodeConnCB(object): if conn.ver_send > BIP0031_VERSION: conn.send_message(msg_pong(message.nonce)) def on_reject(self, conn, message): pass + def on_open(self, conn): pass def on_close(self, conn): pass def on_mempool(self, conn): pass def on_pong(self, conn, message): pass - + def on_feefilter(self, conn, message): pass + def on_sendheaders(self, conn, message): pass + def on_sendcmpct(self, conn, message): pass + def on_cmpctblock(self, conn, message): pass + def on_getblocktxn(self, conn, message): pass + def on_blocktxn(self, conn, message): pass + +# More useful callbacks and functions for NodeConnCB's which have a single NodeConn +class SingleNodeConnCB(NodeConnCB): + def __init__(self): + NodeConnCB.__init__(self) + self.connection = None + self.ping_counter = 1 + self.last_pong = msg_pong() + + def add_connection(self, conn): + self.connection = conn + + # Wrapper for the NodeConn's send_message function + def send_message(self, message): + self.connection.send_message(message) + + def send_and_ping(self, message): + self.send_message(message) + self.sync_with_ping() + + def on_pong(self, conn, message): + self.last_pong = message + + # Sync up with the node + def sync_with_ping(self, timeout=30): + def received_pong(): + return (self.last_pong.nonce == self.ping_counter) + self.send_message(msg_ping(nonce=self.ping_counter)) + success = wait_until(received_pong, timeout=timeout) + self.ping_counter += 1 + return success # The actual NodeConn class # This class provides an interface for a p2p connection to a specified node class NodeConn(asyncore.dispatcher): messagemap = { - "version": msg_version, - "verack": msg_verack, - "addr": msg_addr, - "alert": msg_alert, - "inv": msg_inv, - "getdata": msg_getdata, - "getblocks": msg_getblocks, - "tx": msg_tx, - "block": msg_block, - "getaddr": msg_getaddr, - "ping": msg_ping, - "pong": msg_pong, - "headers": msg_headers, - "getheaders": msg_getheaders, - "reject": msg_reject, - "mempool": msg_mempool + b"version": msg_version, + b"verack": msg_verack, + b"addr": msg_addr, + b"alert": msg_alert, + b"inv": msg_inv, + b"getdata": msg_getdata, + b"getblocks": msg_getblocks, + b"tx": msg_tx, + b"block": msg_block, + b"getaddr": msg_getaddr, + b"ping": msg_ping, + b"pong": msg_pong, + b"headers": msg_headers, + b"getheaders": msg_getheaders, + b"reject": msg_reject, + b"mempool": msg_mempool, + b"feefilter": msg_feefilter, + b"sendheaders": msg_sendheaders, + b"sendcmpct": msg_sendcmpct, + b"cmpctblock": msg_cmpctblock, + b"getblocktxn": msg_getblocktxn, + b"blocktxn": msg_blocktxn } MAGIC_BYTES = { - "mainnet": "\xf9\xbe\xb4\xd9", # mainnet - "testnet3": "\x0b\x11\x09\x07", # testnet3 - "regtest": "\xfa\xbf\xb5\xda" # regtest + "mainnet": b"\xc0\xc0\xc0\xc0", # mainnet + "testnet3": b"\xfc\xc1\xb7\xdc", # testnet3 + "regtest": b"\xfa\xbf\xb5\xda", # regtest } - def __init__(self, dstaddr, dstport, rpc, callback, net="regtest"): + def __init__(self, dstaddr, dstport, rpc, callback, net="regtest", services=NODE_NETWORK, send_version=True): asyncore.dispatcher.__init__(self, map=mininode_socket_map) self.log = logging.getLogger("NodeConn(%s:%d)" % (dstaddr, dstport)) self.dstaddr = dstaddr self.dstport = dstport self.create_socket(socket.AF_INET, socket.SOCK_STREAM) - self.sendbuf = "" - self.recvbuf = "" + self.sendbuf = b"" + self.recvbuf = b"" self.ver_send = 209 self.ver_recv = 209 self.last_sent = 0 @@ -1099,16 +1636,20 @@ class NodeConn(asyncore.dispatcher): self.network = net self.cb = callback self.disconnect = False + self.nServices = 0 + + if send_version: + # stuff version msg into sendbuf + vt = msg_version() + vt.nServices = services + vt.addrTo.ip = self.dstaddr + vt.addrTo.port = self.dstport + vt.addrFrom.ip = "0.0.0.0" + vt.addrFrom.port = 0 + self.send_message(vt, True) - # stuff version msg into sendbuf - vt = msg_version() - vt.addrTo.ip = self.dstaddr - vt.addrTo.port = self.dstport - vt.addrFrom.ip = "0.0.0.0" - vt.addrFrom.port = 0 - self.send_message(vt, True) - print 'MiniNode: Connecting to Bitcoin Node IP # ' + dstaddr + ':' \ - + str(dstport) + print('MiniNode: Connecting to Bitcoin Node IP # ' + dstaddr + ':' \ + + str(dstport)) try: self.connect((dstaddr, dstport)) @@ -1120,15 +1661,17 @@ class NodeConn(asyncore.dispatcher): self.log.debug(msg) def handle_connect(self): - self.show_debug_msg("MiniNode: Connected & Listening: \n") - self.state = "connected" + if self.state != "connected": + self.show_debug_msg("MiniNode: Connected & Listening: \n") + self.state = "connected" + self.cb.on_open(self) def handle_close(self): self.show_debug_msg("MiniNode: Closing Connection to %s:%d... " % (self.dstaddr, self.dstport)) self.state = "closed" - self.recvbuf = "" - self.sendbuf = "" + self.recvbuf = b"" + self.sendbuf = b"" try: self.close() except: @@ -1149,11 +1692,20 @@ class NodeConn(asyncore.dispatcher): def writable(self): with mininode_lock: + pre_connection = self.state == "connecting" length = len(self.sendbuf) - return (length > 0) + return (length > 0 or pre_connection) def handle_write(self): with mininode_lock: + # asyncore does not expose socket connection, only the first read/write + # event, thus we must check connection manually here to know when we + # actually connect + if self.state == "connecting": + self.handle_connect() + if not self.writable(): + return + try: sent = self.send(self.sendbuf) except: @@ -1162,53 +1714,58 @@ class NodeConn(asyncore.dispatcher): self.sendbuf = self.sendbuf[sent:] def got_data(self): - while True: - if len(self.recvbuf) < 4: - return - if self.recvbuf[:4] != self.MAGIC_BYTES[self.network]: - raise ValueError("got garbage %s" % repr(self.recvbuf)) - if self.ver_recv < 209: - if len(self.recvbuf) < 4 + 12 + 4: - return - command = self.recvbuf[4:4+12].split("\x00", 1)[0] - msglen = struct.unpack("<i", self.recvbuf[4+12:4+12+4])[0] - checksum = None - if len(self.recvbuf) < 4 + 12 + 4 + msglen: - return - msg = self.recvbuf[4+12+4:4+12+4+msglen] - self.recvbuf = self.recvbuf[4+12+4+msglen:] - else: - if len(self.recvbuf) < 4 + 12 + 4 + 4: - return - command = self.recvbuf[4:4+12].split("\x00", 1)[0] - msglen = struct.unpack("<i", self.recvbuf[4+12:4+12+4])[0] - checksum = self.recvbuf[4+12+4:4+12+4+4] - if len(self.recvbuf) < 4 + 12 + 4 + 4 + msglen: + try: + while True: + if len(self.recvbuf) < 4: return - msg = self.recvbuf[4+12+4+4:4+12+4+4+msglen] - th = sha256(msg) - h = sha256(th) - if checksum != h[:4]: - raise ValueError("got bad checksum " + repr(self.recvbuf)) - self.recvbuf = self.recvbuf[4+12+4+4+msglen:] - if command in self.messagemap: - f = cStringIO.StringIO(msg) - t = self.messagemap[command]() - t.deserialize(f) - self.got_message(t) - else: - self.show_debug_msg("Unknown command: '" + command + "' " + - repr(msg)) + if self.recvbuf[:4] != self.MAGIC_BYTES[self.network]: + raise ValueError("got garbage %s" % repr(self.recvbuf)) + if self.ver_recv < 209: + if len(self.recvbuf) < 4 + 12 + 4: + return + command = self.recvbuf[4:4+12].split(b"\x00", 1)[0] + msglen = struct.unpack("<i", self.recvbuf[4+12:4+12+4])[0] + checksum = None + if len(self.recvbuf) < 4 + 12 + 4 + msglen: + return + msg = self.recvbuf[4+12+4:4+12+4+msglen] + self.recvbuf = self.recvbuf[4+12+4+msglen:] + else: + if len(self.recvbuf) < 4 + 12 + 4 + 4: + return + command = self.recvbuf[4:4+12].split(b"\x00", 1)[0] + msglen = struct.unpack("<i", self.recvbuf[4+12:4+12+4])[0] + checksum = self.recvbuf[4+12+4:4+12+4+4] + if len(self.recvbuf) < 4 + 12 + 4 + 4 + msglen: + return + msg = self.recvbuf[4+12+4+4:4+12+4+4+msglen] + th = sha256(msg) + h = sha256(th) + if checksum != h[:4]: + raise ValueError("got bad checksum " + repr(self.recvbuf)) + self.recvbuf = self.recvbuf[4+12+4+4+msglen:] + if command in self.messagemap: + f = BytesIO(msg) + t = self.messagemap[command]() + t.deserialize(f) + self.got_message(t) + else: + self.show_debug_msg("Unknown command: '" + command + "' " + + repr(msg)) + except Exception as e: + print('got_data:', repr(e)) + # import traceback + # traceback.print_tb(sys.exc_info()[2]) def send_message(self, message, pushbuf=False): if self.state != "connected" and not pushbuf: - return + raise IOError('Not connected, no pushbuf') self.show_debug_msg("Send %s" % repr(message)) command = message.command data = message.serialize() tmsg = self.MAGIC_BYTES[self.network] tmsg += command - tmsg += "\x00" * (12 - len(command)) + tmsg += b"\x00" * (12 - len(command)) tmsg += struct.pack("<I", len(data)) if self.ver_send >= 209: th = sha256(data) @@ -1220,11 +1777,11 @@ class NodeConn(asyncore.dispatcher): self.last_sent = time.time() def got_message(self, message): - if message.command == "version": + if message.command == b"version": if message.nVersion <= BIP0031_VERSION: - self.messagemap['ping'] = msg_ping_prebip31 + self.messagemap[b'ping'] = msg_ping_prebip31 if self.last_sent + 30 * 60 < time.time(): - self.send_message(self.messagemap['ping']()) + self.send_message(self.messagemap[b'ping']()) self.show_debug_msg("Recv %s" % repr(message)) self.cb.deliver(self, message) diff --git a/qa/rpc-tests/test_framework/netutil.py b/qa/rpc-tests/test_framework/netutil.py index b30a88a4f..b92a9f6e1 100644 --- a/qa/rpc-tests/test_framework/netutil.py +++ b/qa/rpc-tests/test_framework/netutil.py @@ -1,16 +1,17 @@ -#!/usr/bin/env python2 -# Copyright (c) 2014 The Bitcoin Core developers +#!/usr/bin/env python3 +# Copyright (c) 2014-2016 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # Linux network utilities + import sys import socket import fcntl import struct import array import os -import binascii +from binascii import unhexlify, hexlify # Roughly based on http://voorloopnul.com/blog/a-python-netstat-in-less-than-100-lines-of-code/ by Ricardo Pascal STATE_ESTABLISHED = '01' @@ -43,9 +44,9 @@ def _remove_empty(array): def _convert_ip_port(array): host,port = array.split(':') # convert host from mangled-per-four-bytes form as used by kernel - host = binascii.unhexlify(host) + host = unhexlify(host) host_out = '' - for x in range(0, len(host)/4): + for x in range(0, len(host) // 4): (val,) = struct.unpack('=I', host[x*4:(x+1)*4]) host_out += '%08x' % val @@ -57,7 +58,7 @@ def netstat(typ='tcp'): To get pid of all network process running on system, you must run this script as superuser ''' - with open('/proc/net/'+typ,'r') as f: + with open('/proc/net/'+typ,'r',encoding='utf8') as f: content = f.readlines() content.pop(0) result = [] @@ -94,7 +95,7 @@ def all_interfaces(): max_possible = 8 # initial value while True: bytes = max_possible * struct_size - names = array.array('B', '\0' * bytes) + names = array.array('B', b'\0' * bytes) outbytes = struct.unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, # SIOCGIFCONF @@ -105,7 +106,7 @@ def all_interfaces(): else: break namestr = names.tostring() - return [(namestr[i:i+16].split('\0', 1)[0], + return [(namestr[i:i+16].split(b'\0', 1)[0], socket.inet_ntoa(namestr[i+20:i+24])) for i in range(0, outbytes, struct_size)] @@ -136,4 +137,19 @@ def addr_to_hex(addr): addr = sub[0] + ([0] * nullbytes) + sub[1] else: raise ValueError('Could not parse address %s' % addr) - return binascii.hexlify(bytearray(addr)) + return hexlify(bytearray(addr)).decode('ascii') + +def test_ipv6_local(): + ''' + Check for (local) IPv6 support. + ''' + import socket + # By using SOCK_DGRAM this will not actually make a connection, but it will + # fail if there is no route to IPv6 localhost. + have_ipv6 = True + try: + s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + s.connect(('::1', 0)) + except socket.error: + have_ipv6 = False + return have_ipv6 diff --git a/qa/rpc-tests/test_framework/script.py b/qa/rpc-tests/test_framework/script.py index e37ab5d45..83bbf2047 100644 --- a/qa/rpc-tests/test_framework/script.py +++ b/qa/rpc-tests/test_framework/script.py @@ -1,20 +1,23 @@ +#!/usr/bin/env python3 +# Copyright (c) 2015-2016 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + # # script.py # # This file is modified from python-bitcoinlib. # -# Distributed under the MIT/X11 software license, see the accompanying -# file COPYING or http://www.opensource.org/licenses/mit-license.php. -# """Scripts Functionality to build scripts, as well as SignatureHash(). """ -from __future__ import absolute_import, division, print_function, unicode_literals -from test_framework.mininode import CTransaction, CTxOut, hash256 +from .mininode import CTransaction, CTxOut, sha256, hash256, uint256_from_str, ser_uint256, ser_string +from binascii import hexlify +import hashlib import sys bchr = chr @@ -24,10 +27,9 @@ if sys.version > '3': bchr = lambda x: bytes([x]) bord = lambda x: x -import copy import struct -import test_framework.bignum +from .bignum import bn2vch MAX_SCRIPT_SIZE = 10000 MAX_SCRIPT_ELEMENT_SIZE = 520 @@ -35,6 +37,10 @@ MAX_SCRIPT_OPCODES = 201 OPCODE_NAMES = {} +def hash160(s): + return hashlib.new('ripemd160', sha256(s)).digest() + + _opcode_instances = [] class CScriptOp(int): """A single script opcode""" @@ -226,8 +232,8 @@ OP_CHECKMULTISIGVERIFY = CScriptOp(0xaf) # expansion OP_NOP1 = CScriptOp(0xb0) -OP_NOP2 = CScriptOp(0xb1) -OP_NOP3 = CScriptOp(0xb2) +OP_CHECKLOCKTIMEVERIFY = CScriptOp(0xb1) +OP_CHECKSEQUENCEVERIFY = CScriptOp(0xb2) OP_NOP4 = CScriptOp(0xb3) OP_NOP5 = CScriptOp(0xb4) OP_NOP6 = CScriptOp(0xb5) @@ -353,8 +359,8 @@ VALID_OPCODES = { OP_CHECKMULTISIGVERIFY, OP_NOP1, - OP_NOP2, - OP_NOP3, + OP_CHECKLOCKTIMEVERIFY, + OP_CHECKSEQUENCEVERIFY, OP_NOP4, OP_NOP5, OP_NOP6, @@ -472,8 +478,8 @@ OPCODE_NAMES.update({ OP_CHECKMULTISIG : 'OP_CHECKMULTISIG', OP_CHECKMULTISIGVERIFY : 'OP_CHECKMULTISIGVERIFY', OP_NOP1 : 'OP_NOP1', - OP_NOP2 : 'OP_NOP2', - OP_NOP3 : 'OP_NOP3', + OP_CHECKLOCKTIMEVERIFY : 'OP_CHECKLOCKTIMEVERIFY', + OP_CHECKSEQUENCEVERIFY : 'OP_CHECKSEQUENCEVERIFY', OP_NOP4 : 'OP_NOP4', OP_NOP5 : 'OP_NOP5', OP_NOP6 : 'OP_NOP6', @@ -591,8 +597,8 @@ OPCODES_BY_NAME = { 'OP_CHECKMULTISIG' : OP_CHECKMULTISIG, 'OP_CHECKMULTISIGVERIFY' : OP_CHECKMULTISIGVERIFY, 'OP_NOP1' : OP_NOP1, - 'OP_NOP2' : OP_NOP2, - 'OP_NOP3' : OP_NOP3, + 'OP_CHECKLOCKTIMEVERIFY' : OP_CHECKLOCKTIMEVERIFY, + 'OP_CHECKSEQUENCEVERIFY' : OP_CHECKSEQUENCEVERIFY, 'OP_NOP4' : OP_NOP4, 'OP_NOP5' : OP_NOP5, 'OP_NOP6' : OP_NOP6, @@ -629,7 +635,7 @@ class CScriptNum(object): neg = obj.value < 0 absvalue = -obj.value if neg else obj.value while (absvalue): - r.append(chr(absvalue & 0xff)) + r.append(absvalue & 0xff) absvalue >>= 8 if r[-1] & 0x80: r.append(0x80 if neg else 0) @@ -658,13 +664,13 @@ class CScript(bytes): other = bchr(CScriptOp(OP_0)) else: other = CScriptNum.encode(other) - elif isinstance(other, (int, long)): + elif isinstance(other, int): if 0 <= other <= 16: other = bytes(bchr(CScriptOp.encode_op_n(other))) elif other == -1: other = bytes(bchr(OP_1NEGATE)) else: - other = CScriptOp.encode_op_pushdata(bignum.bn2vch(other)) + other = CScriptOp.encode_op_pushdata(bn2vch(other)) elif isinstance(other, (bytes, bytearray)): other = CScriptOp.encode_op_pushdata(other) return other @@ -777,7 +783,7 @@ class CScript(bytes): # need to change def _repr(o): if isinstance(o, bytes): - return "x('%s')" % binascii.hexlify(o).decode('utf8') + return b"x('%s')" % hexlify(o).decode('ascii') else: return repr(o) @@ -876,7 +882,7 @@ def SignatureHash(script, txTo, inIdx, hashtype): tmp = txtmp.vout[outIdx] txtmp.vout = [] for i in range(outIdx): - txtmp.vout.append(CTxOut()) + txtmp.vout.append(CTxOut(-1)) txtmp.vout.append(tmp) for i in range(len(txtmp.vin)): @@ -894,3 +900,48 @@ def SignatureHash(script, txTo, inIdx, hashtype): hash = hash256(s) return (hash, None) + +# TODO: Allow cached hashPrevouts/hashSequence/hashOutputs to be provided. +# Performance optimization probably not necessary for python tests, however. +# Note that this corresponds to sigversion == 1 in EvalScript, which is used +# for version 0 witnesses. +def SegwitVersion1SignatureHash(script, txTo, inIdx, hashtype, amount): + + hashPrevouts = 0 + hashSequence = 0 + hashOutputs = 0 + + if not (hashtype & SIGHASH_ANYONECANPAY): + serialize_prevouts = bytes() + for i in txTo.vin: + serialize_prevouts += i.prevout.serialize() + hashPrevouts = uint256_from_str(hash256(serialize_prevouts)) + + if (not (hashtype & SIGHASH_ANYONECANPAY) and (hashtype & 0x1f) != SIGHASH_SINGLE and (hashtype & 0x1f) != SIGHASH_NONE): + serialize_sequence = bytes() + for i in txTo.vin: + serialize_sequence += struct.pack("<I", i.nSequence) + hashSequence = uint256_from_str(hash256(serialize_sequence)) + + if ((hashtype & 0x1f) != SIGHASH_SINGLE and (hashtype & 0x1f) != SIGHASH_NONE): + serialize_outputs = bytes() + for o in txTo.vout: + serialize_outputs += o.serialize() + hashOutputs = uint256_from_str(hash256(serialize_outputs)) + elif ((hashtype & 0x1f) == SIGHASH_SINGLE and inIdx < len(txTo.vout)): + serialize_outputs = txTo.vout[inIdx].serialize() + hashOutputs = uint256_from_str(hash256(serialize_outputs)) + + ss = bytes() + ss += struct.pack("<i", txTo.nVersion) + ss += ser_uint256(hashPrevouts) + ss += ser_uint256(hashSequence) + ss += txTo.vin[inIdx].prevout.serialize() + ss += ser_string(script) + ss += struct.pack("<q", amount) + ss += struct.pack("<I", txTo.vin[inIdx].nSequence) + ss += ser_uint256(hashOutputs) + ss += struct.pack("<i", txTo.nLockTime) + ss += struct.pack("<I", hashtype) + + return hash256(ss) diff --git a/qa/rpc-tests/test_framework/scrypt_auxpow.py b/qa/rpc-tests/test_framework/scrypt_auxpow.py index 987750e8f..1e95a5820 100644 --- a/qa/rpc-tests/test_framework/scrypt_auxpow.py +++ b/qa/rpc-tests/test_framework/scrypt_auxpow.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # Copyright (c) 2014 Daniel Kraft -# Copyright (c) 2015 The Dogecoin Core developers +# Copyright (c) 2015-2018 The Dogecoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -12,9 +12,7 @@ # package, which can be downloaded from: # https://pypi.python.org/packages/source/l/ltc_scrypt/ltc_scrypt-1.0.tar.gz -import binascii -import hashlib -import auxpow +from .auxpow import * import ltc_scrypt def computeAuxpowWithChainId (block, target, chainid, ok): @@ -25,26 +23,26 @@ def computeAuxpowWithChainId (block, target, chainid, ok): # Start by building the merge-mining coinbase. The merkle tree # consists only of the block hash as root. - coinbase = "fabe" + binascii.hexlify ("m" * 2) + coinbase = "fabe" + (b"m" * 2).hex() coinbase += block coinbase += "01000000" + ("00" * 4) # Construct "vector" of transaction inputs. vin = "01" vin += ("00" * 32) + ("ff" * 4) - vin += ("%02x" % (len (coinbase) / 2)) + coinbase + vin += ("%02x" % int(len (coinbase) / 2)) + coinbase vin += ("ff" * 4) # Build up the full coinbase transaction. It consists only # of the input and has no outputs. tx = "01000000" + vin + "00" + ("00" * 4) - txHash = auxpow.doubleHashHex (tx) + txHash = doubleHashHex (tx) # Construct the parent block header. It need not be valid, just good # enough for auxpow purposes. header = "0100" + chainid + "00" header += "00" * 32 - header += auxpow.reverseHex (txHash) + header += reverseHex (txHash) header += "00" * 4 header += "00" * 4 header += "00" * 4 @@ -72,10 +70,11 @@ def mineScryptAux (node, chainid, ok): """ auxblock = node.getauxblock () - target = auxpow.reverseHex (auxblock['target']) + target = reverseHex (auxblock['_target']) apow = computeAuxpowWithChainId (auxblock['hash'], target, chainid, ok) res = node.getauxblock (auxblock['hash'], apow) + return res def mineScryptBlock (header, target, ok): """ @@ -83,17 +82,17 @@ def mineScryptBlock (header, target, ok): for the given target. """ - data = bytearray (binascii.unhexlify (header)) + data = bytearray (bytes.fromhex(header)) while True: assert data[79] < 255 data[79] += 1 - hexData = binascii.hexlify (data) + hexData = data.hex() scrypt = getScryptPoW(hexData) if (ok and scrypt < target) or ((not ok) and scrypt > target): break - blockhash = auxpow.doubleHashHex (hexData) + blockhash = doubleHashHex (hexData) return (hexData, blockhash) def getScryptPoW(hexData): @@ -101,5 +100,5 @@ def getScryptPoW(hexData): Actual scrypt pow calculation """ - data = binascii.unhexlify(hexData) - return auxpow.reverseHex(binascii.hexlify(ltc_scrypt.getPoWHash(data))) + data = bytes.fromhex(hexData) + return reverseHex(ltc_scrypt.getPoWHash(data).hex()) diff --git a/qa/rpc-tests/test_framework/siphash.py b/qa/rpc-tests/test_framework/siphash.py new file mode 100644 index 000000000..9c0574bd9 --- /dev/null +++ b/qa/rpc-tests/test_framework/siphash.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# Copyright (c) 2016 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# +# siphash.py - Specialized SipHash-2-4 implementations +# +# This implements SipHash-2-4 for 256-bit integers. + +def rotl64(n, b): + return n >> (64 - b) | (n & ((1 << (64 - b)) - 1)) << b + +def siphash_round(v0, v1, v2, v3): + v0 = (v0 + v1) & ((1 << 64) - 1) + v1 = rotl64(v1, 13) + v1 ^= v0 + v0 = rotl64(v0, 32) + v2 = (v2 + v3) & ((1 << 64) - 1) + v3 = rotl64(v3, 16) + v3 ^= v2 + v0 = (v0 + v3) & ((1 << 64) - 1) + v3 = rotl64(v3, 21) + v3 ^= v0 + v2 = (v2 + v1) & ((1 << 64) - 1) + v1 = rotl64(v1, 17) + v1 ^= v2 + v2 = rotl64(v2, 32) + return (v0, v1, v2, v3) + +def siphash256(k0, k1, h): + n0 = h & ((1 << 64) - 1) + n1 = (h >> 64) & ((1 << 64) - 1) + n2 = (h >> 128) & ((1 << 64) - 1) + n3 = (h >> 192) & ((1 << 64) - 1) + v0 = 0x736f6d6570736575 ^ k0 + v1 = 0x646f72616e646f6d ^ k1 + v2 = 0x6c7967656e657261 ^ k0 + v3 = 0x7465646279746573 ^ k1 ^ n0 + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0 ^= n0 + v3 ^= n1 + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0 ^= n1 + v3 ^= n2 + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0 ^= n2 + v3 ^= n3 + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0 ^= n3 + v3 ^= 0x2000000000000000 + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0 ^= 0x2000000000000000 + v2 ^= 0xFF + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + return v0 ^ v1 ^ v2 ^ v3 diff --git a/qa/rpc-tests/test_framework/socks5.py b/qa/rpc-tests/test_framework/socks5.py index 1dbfb98d5..372f5ed60 100644 --- a/qa/rpc-tests/test_framework/socks5.py +++ b/qa/rpc-tests/test_framework/socks5.py @@ -1,11 +1,12 @@ -# Copyright (c) 2015 The Bitcoin Core developers +#!/usr/bin/env python3 +# Copyright (c) 2015-2016 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. ''' Dummy Socks5 server for testing. ''' -from __future__ import print_function, division, unicode_literals -import socket, threading, Queue + +import socket, threading, queue import traceback, sys ### Protocol constants @@ -102,7 +103,7 @@ class Socks5Connection(object): addr = recvall(self.conn, 4) elif atyp == AddressType.DOMAINNAME: n = recvall(self.conn, 1)[0] - addr = str(recvall(self.conn, n)) + addr = recvall(self.conn, n) elif atyp == AddressType.IPV6: addr = recvall(self.conn, 16) else: @@ -117,7 +118,7 @@ class Socks5Connection(object): self.serv.queue.put(cmdin) print('Proxy: ', cmdin) # Fall through to disconnect - except Exception,e: + except Exception as e: traceback.print_exc(file=sys.stderr) self.serv.queue.put(e) finally: @@ -132,7 +133,7 @@ class Socks5Server(object): self.s.listen(5) self.running = False self.thread = None - self.queue = Queue.Queue() # report connections and exceptions to client + self.queue = queue.Queue() # report connections and exceptions to client def run(self): while self.running: diff --git a/qa/rpc-tests/test_framework/test_framework.py b/qa/rpc-tests/test_framework/test_framework.py index 398cef877..7dd368130 100755 --- a/qa/rpc-tests/test_framework/test_framework.py +++ b/qa/rpc-tests/test_framework/test_framework.py @@ -1,39 +1,59 @@ -#!/usr/bin/env python2 -# Copyright (c) 2014 The Bitcoin Core developers +#!/usr/bin/env python3 +# Copyright (c) 2014-2016 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # Base class for RPC testing -# Add python-bitcoinrpc to module search path: +import logging +import optparse import os import sys - import shutil import tempfile import traceback -from authproxy import AuthServiceProxy, JSONRPCException -from util import * +from .util import ( + initialize_chain, + start_nodes, + connect_nodes_bi, + sync_blocks, + sync_mempools, + stop_nodes, + stop_node, + enable_coverage, + check_json_precision, + initialize_chain_clean, + PortSeed, +) +from .authproxy import JSONRPCException class BitcoinTestFramework(object): - # These may be over-ridden by subclasses: + def __init__(self): + self.num_nodes = 4 + self.setup_clean_chain = False + self.nodes = None + def run_test(self): - for node in self.nodes: - assert_equal(node.getblockcount(), 120) - assert_equal(node.getbalance(), 15*500000) + raise NotImplementedError def add_options(self, parser): pass def setup_chain(self): print("Initializing test directory "+self.options.tmpdir) - initialize_chain(self.options.tmpdir) + if self.setup_clean_chain: + initialize_chain_clean(self.options.tmpdir, self.num_nodes) + else: + initialize_chain(self.options.tmpdir, self.num_nodes, self.options.cachedir) + + def stop_node(self, num_node): + stop_node(self.nodes[num_node], num_node) def setup_nodes(self): - return start_nodes(4, self.options.tmpdir) + return start_nodes(self.num_nodes, self.options.tmpdir) def setup_network(self, split = False): self.nodes = self.setup_nodes() @@ -60,7 +80,6 @@ class BitcoinTestFramework(object): """ assert not self.is_network_split stop_nodes(self.nodes) - wait_bitcoinds() self.setup_network(True) def sync_all(self): @@ -79,67 +98,90 @@ class BitcoinTestFramework(object): """ assert self.is_network_split stop_nodes(self.nodes) - wait_bitcoinds() self.setup_network(False) def main(self): - import optparse parser = optparse.OptionParser(usage="%prog [options]") parser.add_option("--nocleanup", dest="nocleanup", default=False, action="store_true", help="Leave dogecoinds and test.* datadir on exit or error") parser.add_option("--noshutdown", dest="noshutdown", default=False, action="store_true", help="Don't stop dogecoinds after the test execution") - parser.add_option("--srcdir", dest="srcdir", default="../../src", - help="Source directory containing dogecoind/dogecoin-cli (default: %default)") + parser.add_option("--srcdir", dest="srcdir", default=os.path.normpath(os.path.dirname(os.path.realpath(__file__))+"/../../../src"), + help="Source directory containing dogecoind/bitcoin-cli (default: %default)") + parser.add_option("--cachedir", dest="cachedir", default=os.path.normpath(os.path.dirname(os.path.realpath(__file__))+"/../../cache"), + help="Directory for caching pregenerated datadirs") parser.add_option("--tmpdir", dest="tmpdir", default=tempfile.mkdtemp(prefix="test"), help="Root directory for datadirs") parser.add_option("--tracerpc", dest="trace_rpc", default=False, action="store_true", help="Print out all RPC calls as they are made") + parser.add_option("--portseed", dest="port_seed", default=os.getpid(), type='int', + help="The seed to use for assigning port numbers (default: current process id)") + parser.add_option("--coveragedir", dest="coveragedir", + help="Write tested RPC commands into this directory") self.add_options(parser) (self.options, self.args) = parser.parse_args() + # backup dir variable for removal at cleanup + self.options.root, self.options.tmpdir = self.options.tmpdir, self.options.tmpdir + '/' + str(self.options.port_seed) + if self.options.trace_rpc: - import logging - logging.basicConfig(level=logging.DEBUG) + logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) + + if self.options.coveragedir: + enable_coverage(self.options.coveragedir) - os.environ['PATH'] = self.options.srcdir+":"+os.environ['PATH'] + PortSeed.n = self.options.port_seed + + os.environ['PATH'] = self.options.srcdir+":"+self.options.srcdir+"/qt:"+os.environ['PATH'] check_json_precision() success = False try: - if not os.path.isdir(self.options.tmpdir): - os.makedirs(self.options.tmpdir) + os.makedirs(self.options.tmpdir, exist_ok=False) self.setup_chain() - self.setup_network() - self.run_test() - success = True - except JSONRPCException as e: print("JSONRPC error: "+e.error['message']) traceback.print_tb(sys.exc_info()[2]) except AssertionError as e: - print("Assertion failed: "+e.message) + print("Assertion failed: " + str(e)) + traceback.print_tb(sys.exc_info()[2]) + except KeyError as e: + print("key not found: "+ str(e)) traceback.print_tb(sys.exc_info()[2]) except Exception as e: - print("Unexpected exception caught during testing: "+str(e)) + print("Unexpected exception caught during testing: " + repr(e)) traceback.print_tb(sys.exc_info()[2]) + except KeyboardInterrupt as e: + print("Exiting after " + repr(e)) if not self.options.noshutdown: print("Stopping nodes") stop_nodes(self.nodes) - wait_bitcoinds() else: print("Note: dogecoinds were not stopped and may still be running") - if not self.options.nocleanup and not self.options.noshutdown: + if not self.options.nocleanup and not self.options.noshutdown and success: print("Cleaning up") shutil.rmtree(self.options.tmpdir) - + if not os.listdir(self.options.root): + os.rmdir(self.options.root) + else: + print("Not cleaning up dir %s" % self.options.tmpdir) + if os.getenv("PYTHON_DEBUG", ""): + # Dump the end of the debug logs, to aid in debugging rare + # travis failures. + import glob + filenames = glob.glob(self.options.tmpdir + "/node*/regtest/debug.log") + MAX_LINES_TO_PRINT = 1000 + for f in filenames: + print("From" , f, ":") + from collections import deque + print("".join(deque(open(f), MAX_LINES_TO_PRINT))) if success: print("Tests successful") sys.exit(0) @@ -156,9 +198,10 @@ class BitcoinTestFramework(object): class ComparisonTestFramework(BitcoinTestFramework): - # Can override the num_nodes variable to indicate how many nodes to run. def __init__(self): + super().__init__() self.num_nodes = 2 + self.setup_clean_chain = True def add_options(self, parser): parser.add_option("--testbinary", dest="testbinary", @@ -168,12 +211,9 @@ class ComparisonTestFramework(BitcoinTestFramework): default=os.getenv("DOGECOIND", "dogecoind"), help="dogecoind binary to use for reference nodes (if any)") - def setup_chain(self): - print "Initializing test directory "+self.options.tmpdir - initialize_chain_clean(self.options.tmpdir, self.num_nodes) - def setup_network(self): - self.nodes = start_nodes(self.num_nodes, self.options.tmpdir, - extra_args=[['-debug', '-whitelist=127.0.0.1']] * self.num_nodes, - binary=[self.options.testbinary] + - [self.options.refbinary]*(self.num_nodes-1)) + self.nodes = start_nodes( + self.num_nodes, self.options.tmpdir, + extra_args=[['-debug', '-whitelist=127.0.0.1']] * self.num_nodes, + binary=[self.options.testbinary] + + [self.options.refbinary]*(self.num_nodes-1)) diff --git a/qa/rpc-tests/test_framework/util.py b/qa/rpc-tests/test_framework/util.py index cdaabc185..4e4011b3a 100644 --- a/qa/rpc-tests/test_framework/util.py +++ b/qa/rpc-tests/test_framework/util.py @@ -1,29 +1,107 @@ -# Copyright (c) 2014 The Bitcoin Core developers +#!/usr/bin/env python3 +# Copyright (c) 2014-2016 The Bitcoin Core developers # Distributed under the MIT 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 math import os import sys +from binascii import hexlify, unhexlify +from base64 import b64encode from decimal import Decimal, ROUND_DOWN import json +import http.client import random import shutil import subprocess import time import re +import errno + +from . import coverage +from .authproxy import AuthServiceProxy, JSONRPCException + +COVERAGE_DIR = None + +# The maximum number of nodes a single test can spawn +MAX_NODES = 8 +# Don't assign rpc or p2p ports lower than this +PORT_MIN = 11000 +# The number of ports to "reserve" for p2p and rpc, each +PORT_RANGE = 5000 + +BITCOIND_PROC_WAIT_TIMEOUT = 60 + + +class PortSeed: + # Must be initialized with a unique integer for each process + n = None + +#Set Mocktime default to OFF. +#MOCKTIME is only needed for scripts that use the +#cached version of the blockchain. If the cached +#version of the blockchain is used without MOCKTIME +#then the mempools will not sync due to IBD. +MOCKTIME = 0 + +def enable_mocktime(): + #For backwared compatibility of the python scripts + #with previous versions of the cache, set MOCKTIME + #to Jan 1, 2014 + (201 * 10 * 60) + global MOCKTIME + MOCKTIME = 1388534400 + (201 * 10 * 60) + +def disable_mocktime(): + global MOCKTIME + MOCKTIME = 0 + +def get_mocktime(): + return MOCKTIME + +def enable_coverage(dirname): + """Maintain a log of which RPC calls are made during testing.""" + global COVERAGE_DIR + COVERAGE_DIR = dirname + + +def get_rpc_proxy(url, node_number, timeout=None): + """ + Args: + url (str): URL of the RPC server to call + node_number (int): the node number (or id) that this calls to + + Kwargs: + timeout (int): HTTP timeout in seconds + + Returns: + AuthServiceProxy. convenience object for making RPC calls. + + """ + proxy_kwargs = {} + if timeout is not None: + proxy_kwargs['timeout'] = timeout + + proxy = AuthServiceProxy(url, **proxy_kwargs) + proxy.url = url # store URL on proxy for info + + coverage_logfile = coverage.get_filename( + COVERAGE_DIR, node_number) if COVERAGE_DIR else None + + return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile) -from authproxy import AuthServiceProxy, JSONRPCException -from util import * def p2p_port(n): - return 11000 + n + os.getpid()%999 + assert(n <= MAX_NODES) + return PORT_MIN + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) + def rpc_port(n): - return 12000 + n + os.getpid()%999 + return PORT_MIN + PORT_RANGE + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) def check_json_precision(): """Make sure json library being used does not lose precision converting BTC values""" @@ -32,30 +110,71 @@ def check_json_precision(): if satoshis != 2000000000000003: raise RuntimeError("JSON encode/decode loses precision") -def sync_blocks(rpc_connections, wait=1): +def count_bytes(hex_string): + return len(bytearray.fromhex(hex_string)) + +def bytes_to_hex_str(byte_str): + return hexlify(byte_str).decode('ascii') + +def hex_str_to_bytes(hex_str): + return unhexlify(hex_str.encode('ascii')) + +def str_to_b64str(string): + return b64encode(string.encode('utf-8')).decode('ascii') + +def sync_blocks(rpc_connections, *, wait=1, timeout=60): """ - Wait until everybody has the same block count + Wait until everybody has the same tip. + + sync_blocks needs to be called with an rpc_connections set that has least + one node already synced to the latest, stable tip, otherwise there's a + chance it might return before all nodes are stably synced. """ - while True: - counts = [ x.getblockcount() for x in rpc_connections ] - if counts == [ counts[0] ]*len(counts): - break + # Use getblockcount() instead of waitforblockheight() to determine the + # initial max height because the two RPCs look at different internal global + # variables (chainActive vs latestBlock) and the former gets updated + # earlier. + maxheight = max(x.getblockcount() for x in rpc_connections) + start_time = cur_time = time.time() + while cur_time <= start_time + timeout: + tips = [r.waitforblockheight(maxheight, int(wait * 1000)) for r in rpc_connections] + if all(t["height"] == maxheight for t in tips): + if all(t["hash"] == tips[0]["hash"] for t in tips): + return + raise AssertionError("Block sync failed, mismatched block hashes:{}".format( + "".join("\n {!r}".format(tip) for tip in tips))) + cur_time = time.time() + raise AssertionError("Block sync to height {} timed out:{}".format( + maxheight, "".join("\n {!r}".format(tip) for tip in tips))) + +def sync_chain(rpc_connections, *, wait=1, timeout=60): + """ + Wait until everybody has the same best block + """ + while timeout > 0: + best_hash = [x.getbestblockhash() for x in rpc_connections] + if best_hash == [best_hash[0]]*len(best_hash): + return time.sleep(wait) + timeout -= wait + raise AssertionError("Chain sync failed: Best block hashes don't match") -def sync_mempools(rpc_connections, wait=1): +def sync_mempools(rpc_connections, *, wait=1, timeout=60): """ Wait until everybody has the same transactions in their memory pools """ - while True: + while timeout > 0: 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 + return time.sleep(wait) + timeout -= wait + raise AssertionError("Mempool sync failed") bitcoind_processes = {} @@ -63,71 +182,121 @@ def initialize_datadir(dirname, n): datadir = os.path.join(dirname, "node"+str(n)) if not os.path.isdir(datadir): os.makedirs(datadir) - with open(os.path.join(datadir, "dogecoin.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"); + rpc_u, rpc_p = rpc_auth_pair(n) + with open(os.path.join(datadir, "dogecoin.conf"), 'w', encoding='utf8') as f: + f.write("regtest=1\n") + f.write("rpcuser=" + rpc_u + "\n") + f.write("rpcpassword=" + rpc_p + "\n") + f.write("port="+str(p2p_port(n))+"\n") + f.write("rpcport="+str(rpc_port(n))+"\n") + f.write("listenonion=0\n") return datadir -def initialize_chain(test_dir): +def rpc_auth_pair(n): + return 'rpcuser💻' + str(n), 'rpcpass🔑' + str(n) + +def rpc_url(i, rpchost=None): + rpc_u, rpc_p = rpc_auth_pair(i) + host = '127.0.0.1' + port = rpc_port(i) + if rpchost: + parts = rpchost.split(':') + if len(parts) == 2: + host, port = parts + else: + host = rpchost + return "http://%s:%s@%s:%d" % (rpc_u, rpc_p, host, int(port)) + +def wait_for_bitcoind_start(process, url, i): + ''' + Wait for bitcoind to start. This means that RPC is accessible and fully initialized. + Raise an exception if bitcoind exits during initialization. + ''' + while True: + if process.poll() is not None: + raise Exception('bitcoind exited with status %i during initialization' % process.returncode) + try: + rpc = get_rpc_proxy(url, i) + blocks = rpc.getblockcount() + break # break out of loop on success + except IOError as e: + if e.errno != errno.ECONNREFUSED: # Port not yet open? + raise # unknown IO error + except JSONRPCException as e: # Initialization phase + if e.error['code'] != -28: # RPC in warmup? + raise # unknown JSON RPC exception + time.sleep(0.25) + +def initialize_chain(test_dir, num_nodes, cachedir): """ - Create (or copy from cache) a 200-block-long chain and - 4 wallets. - bitcoind and bitcoin-cli must be in search path. + Create a cache of a 120-block-long chain (with wallet) for MAX_NODES + Afterward, create num_nodes copies from the cache """ - if not os.path.isdir(os.path.join("cache", "node0")): - devnull = open("/dev/null", "w+") + assert num_nodes <= MAX_NODES + create_cache = False + for i in range(MAX_NODES): + if not os.path.isdir(os.path.join(cachedir, 'node'+str(i))): + create_cache = True + break + + if create_cache: + + #find and delete old cache directories if any exist + for i in range(MAX_NODES): + if os.path.isdir(os.path.join(cachedir,"node"+str(i))): + shutil.rmtree(os.path.join(cachedir,"node"+str(i))) + # Create cache directories, run bitcoinds: - for i in range(4): - datadir=initialize_datadir("cache", i) - args = [ os.getenv("DOGECOIND", "dogecoind"), "-keypool=1", "-datadir="+datadir, "-discover=0" ] + for i in range(MAX_NODES): + datadir=initialize_datadir(cachedir, i) + args = [ os.getenv("DOGECOIND", "dogecoind"), "-server", "-keypool=1", "-datadir="+datadir, "-discover=0" ] if i > 0: args.append("-connect=127.0.0.1:"+str(p2p_port(0))) bitcoind_processes[i] = subprocess.Popen(args) if os.getenv("PYTHON_DEBUG", ""): - print "initialize_chain: dogecoind started, calling dogecoin-cli -rpcwait getblockcount" - subprocess.check_call([ os.getenv("DOGECOINCLI", "dogecoin-cli"), "-datadir="+datadir, - "-rpcwait", "getblockcount"], stdout=devnull) + print("initialize_chain: dogecoind started, waiting for RPC to come up") + wait_for_bitcoind_start(bitcoind_processes[i], rpc_url(i), i) if os.getenv("PYTHON_DEBUG", ""): - print "initialize_chain: dogecoin-cli -rpcwait getblockcount completed" - devnull.close() + print("initialize_chain: RPC successfully started") + rpcs = [] - for i in range(4): + for i in range(MAX_NODES): try: - url = "http://rt:[email protected]:%d"%(rpc_port(i),) - rpcs.append(AuthServiceProxy(url)) + rpcs.append(get_rpc_proxy(rpc_url(i), i)) 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. - # blocks are created with timestamps 10 minutes apart, starting - # at 1 Jan 2014 - block_time = 1388534400 + # Create a 120-block-long chain; each of the 4 first nodes + # gets 15 mature blocks and 15 immature. + # Note: To preserve compatibility with older versions of + # initialize_chain, only 4 nodes will generate coins. + # + # blocks are created with timestamps 1 minute apart + # starting from 2010 minutes in the past + enable_mocktime() + block_time = get_mocktime() - (121 * 60) for i in range(2): for peer in range(4): for j in range(15): set_node_times(rpcs, block_time) rpcs[peer].generate(1) - block_time += 10*60 + block_time += 60 # Must sync before next peer starts generating blocks sync_blocks(rpcs) # Shut them down, and clean up cache directories: stop_nodes(rpcs) - wait_bitcoinds() - for i in range(4): - os.remove(log_filename("cache", i, "debug.log")) - os.remove(log_filename("cache", i, "db.log")) - os.remove(log_filename("cache", i, "peers.dat")) - os.remove(log_filename("cache", i, "fee_estimates.dat")) - - for i in range(4): - from_dir = os.path.join("cache", "node"+str(i)) + disable_mocktime() + for i in range(MAX_NODES): + os.remove(log_filename(cachedir, i, "debug.log")) + os.remove(log_filename(cachedir, i, "db.log")) + os.remove(log_filename(cachedir, i, "peers.dat")) + os.remove(log_filename(cachedir, i, "fee_estimates.dat")) + + for i in range(num_nodes): + from_dir = os.path.join(cachedir, "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 @@ -168,57 +337,58 @@ def start_node(i, dirname, extra_args=None, rpchost=None, timewait=None, binary= datadir = os.path.join(dirname, "node"+str(i)) if binary is None: binary = os.getenv("DOGECOIND", "dogecoind") - args = [ binary, "-datadir="+datadir, "-keypool=1", "-discover=0", "-rest" ] + args = [ binary, "-datadir="+datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-mocktime="+str(get_mocktime()) ] if extra_args is not None: args.extend(extra_args) bitcoind_processes[i] = subprocess.Popen(args) - devnull = open("/dev/null", "w+") if os.getenv("PYTHON_DEBUG", ""): - print "start_node: dogecoind started, calling dogecoin-cli -rpcwait getblockcount" - subprocess.check_call([ os.getenv("DOGECOINCLI", "dogecoin-cli"), "-datadir="+datadir] + - _rpchost_to_args(rpchost) + - ["-rpcwait", "getblockcount"], stdout=devnull) + print("start_node: dogecoind started, waiting for RPC to come up") + url = rpc_url(i, rpchost) + wait_for_bitcoind_start(bitcoind_processes[i], url, i) if os.getenv("PYTHON_DEBUG", ""): - print "start_node: calling dogecoin-cli -rpcwait getblockcount returned" - devnull.close() - url = "http://rt:rt@%s:%d" % (rpchost or '127.0.0.1', rpc_port(i)) - if timewait is not None: - proxy = AuthServiceProxy(url, timeout=timewait) - else: - proxy = AuthServiceProxy(url) - proxy.url = url # store URL on proxy for info + print("start_node: RPC successfully started") + proxy = get_rpc_proxy(url, i, timeout=timewait) + + if COVERAGE_DIR: + coverage.write_all_rpc_commands(COVERAGE_DIR, proxy) + return proxy -def start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, binary=None): +def start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, timewait=None, binary=None): """ Start multiple bitcoinds, return RPC connections to them """ - if extra_args is None: extra_args = [ None for i in range(num_nodes) ] - if binary is None: binary = [ None for i in range(num_nodes) ] - return [ start_node(i, dirname, extra_args[i], rpchost, binary=binary[i]) for i in range(num_nodes) ] + if extra_args is None: extra_args = [ None for _ in range(num_nodes) ] + if binary is None: binary = [ None for _ in range(num_nodes) ] + rpcs = [] + try: + for i in range(num_nodes): + rpcs.append(start_node(i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i])) + except: # If one node failed to start, stop the others + stop_nodes(rpcs) + raise + return rpcs def log_filename(dirname, n_node, logname): return os.path.join(dirname, "node"+str(n_node), "regtest", logname) def stop_node(node, i): - node.stop() - bitcoind_processes[i].wait() + try: + node.stop() + except http.client.CannotSendRequest as e: + print("WARN: Unable to stop node: " + repr(e)) + return_code = bitcoind_processes[i].wait(timeout=BITCOIND_PROC_WAIT_TIMEOUT) + assert_equal(return_code, 0) del bitcoind_processes[i] def stop_nodes(nodes): - for node in nodes: - node.stop() - del nodes[:] # Emptying array closes connections as a side effect + for i, node in enumerate(nodes): + stop_node(node, i) + assert not bitcoind_processes.values() # All connections must be gone now def set_node_times(nodes, t): for node in nodes: node.setmocktime(t) -def wait_bitcoinds(): - # Wait for all bitcoinds to cleanly exit - for bitcoind in bitcoind_processes.values(): - bitcoind.wait() - bitcoind_processes.clear() - def connect_nodes(from_connection, node_num): ip_port = "127.0.0.1:"+str(p2p_port(node_num)) from_connection.addnode(ip_port, "onetry") @@ -337,20 +507,212 @@ def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants): return (txid, signresult["hex"], fee) -def assert_equal(thing1, thing2): - if thing1 != thing2: - raise AssertionError("%s != %s"%(str(thing1),str(thing2))) +def assert_fee_amount(fee, tx_size, fee_per_kB): + """Assert the fee was in range""" + target_fee = round_tx_size(tx_size) * fee_per_kB / 1000 + if fee < target_fee: + raise AssertionError("Fee of %s BTC too low! (Should be %s BTC)"%(str(fee), str(target_fee))) + # allow the wallet's estimation to be at most 2 bytes off + if fee > round_tx_size(tx_size + 2) * fee_per_kB / 1000: + raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)"%(str(fee), str(target_fee))) + +def round_tx_size(tx_size): + return int(math.ceil(tx_size / 1000.0)) * 1000 + +def assert_equal(thing1, thing2, *args): + if thing1 != thing2 or any(thing1 != arg for arg in args): + raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args)) def assert_greater_than(thing1, thing2): if thing1 <= thing2: raise AssertionError("%s <= %s"%(str(thing1),str(thing2))) +def assert_greater_than_or_equal(thing1, thing2): + if thing1 < thing2: + raise AssertionError("%s < %s"%(str(thing1),str(thing2))) + def assert_raises(exc, fun, *args, **kwds): + assert_raises_message(exc, None, fun, *args, **kwds) + +def assert_raises_message(exc, message, fun, *args, **kwds): try: fun(*args, **kwds) - except exc: - pass + except exc as e: + if message is not None and message not in e.error['message']: + raise AssertionError("Expected substring not found:"+e.error['message']) except Exception as e: raise AssertionError("Unexpected exception raised: "+type(e).__name__) else: raise AssertionError("No exception raised") + +def assert_raises_jsonrpc(code, message, fun, *args, **kwds): + """Run an RPC and verify that a specific JSONRPC exception code and message is raised. + + Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException + and verifies that the error code and message are as expected. Throws AssertionError if + no JSONRPCException was returned or if the error code/message are not as expected. + + Args: + code (int), optional: the error code returned by the RPC call (defined + in src/rpc/protocol.h). Set to None if checking the error code is not required. + message (string), optional: [a substring of] the error string returned by the + RPC call. Set to None if checking the error string is not required + fun (function): the function to call. This should be the name of an RPC. + args*: positional arguments for the function. + kwds**: named arguments for the function. + """ + try: + fun(*args, **kwds) + except JSONRPCException as e: + # JSONRPCException was thrown as expected. Check the code and message values are correct. + if (code is not None) and (code != e.error["code"]): + raise AssertionError("Unexpected JSONRPC error code %i" % e.error["code"]) + if (message is not None) and (message not in e.error['message']): + raise AssertionError("Expected substring not found:"+e.error['message']) + except Exception as e: + raise AssertionError("Unexpected exception raised: "+type(e).__name__) + else: + raise AssertionError("No exception raised") + +def assert_is_hex_string(string): + try: + int(string, 16) + except Exception as e: + raise AssertionError( + "Couldn't interpret %r as hexadecimal; raised: %s" % (string, e)) + +def assert_is_hash_string(string, length=64): + if not isinstance(string, str): + raise AssertionError("Expected a string, got type %r" % type(string)) + elif length and len(string) != length: + raise AssertionError( + "String of length %d expected; got %d" % (length, len(string))) + elif not re.match('[abcdef0-9]+$', string): + raise AssertionError( + "String %r contains invalid characters for a hash." % string) + +def assert_array_result(object_array, to_match, expected, should_not_find = False): + """ + Pass in array of JSON objects, a dictionary with key/value pairs + to match against, and another dictionary with expected key/value + pairs. + If the should_not_find flag is true, to_match should not be found + in object_array + """ + if should_not_find == True: + assert_equal(expected, { }) + num_matched = 0 + for item in object_array: + all_match = True + for key,value in to_match.items(): + if item[key] != value: + all_match = False + if not all_match: + continue + elif should_not_find == True: + num_matched = num_matched+1 + for key,value in expected.items(): + if item[key] != value: + raise AssertionError("%s : expected %s=%s"%(str(item), str(key), str(value))) + num_matched = num_matched+1 + if num_matched == 0 and should_not_find != True: + raise AssertionError("No objects matched %s"%(str(to_match))) + if num_matched > 0 and should_not_find == True: + raise AssertionError("Objects were found %s"%(str(to_match))) + +def satoshi_round(amount): + return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) + +# Helper to create at least "count" utxos +# Pass in a fee that is sufficient for relay and mining new transactions. +def create_confirmed_utxos(fee, node, count): + node.generate(int(0.5*count)+61) + utxos = node.listunspent() + iterations = count - len(utxos) + addr1 = node.getnewaddress() + addr2 = node.getnewaddress() + if iterations <= 0: + return utxos + for i in range(iterations): + t = utxos.pop() + inputs = [] + inputs.append({ "txid" : t["txid"], "vout" : t["vout"]}) + outputs = {} + send_value = t['amount'] - fee + outputs[addr1] = satoshi_round(send_value/2) + outputs[addr2] = satoshi_round(send_value/2) + raw_tx = node.createrawtransaction(inputs, outputs) + signed_tx = node.signrawtransaction(raw_tx)["hex"] + txid = node.sendrawtransaction(signed_tx) + + while (node.getmempoolinfo()['size'] > 0): + node.generate(1) + + utxos = node.listunspent() + assert(len(utxos) >= count) + return utxos + +# Create large OP_RETURN txouts that can be appended to a transaction +# to make it large (helper for constructing large transactions). +def gen_return_txouts(): + # Some pre-processing to create a bunch of OP_RETURN txouts to insert into transactions we create + # So we have big transactions (and therefore can't fit very many into each block) + # create one script_pubkey + script_pubkey = "6a4d0200" #OP_RETURN OP_PUSH2 512 bytes + for i in range (512): + script_pubkey = script_pubkey + "01" + # concatenate 128 txouts of above script_pubkey which we'll insert before the txout for change + txouts = "81" + for k in range(128): + # add txout value + txouts = txouts + "0000000000000000" + # add length of script_pubkey + txouts = txouts + "fd0402" + # add script_pubkey + txouts = txouts + script_pubkey + return txouts + +def create_tx(node, coinbase, to_address, amount): + inputs = [{ "txid" : coinbase, "vout" : 0}] + outputs = { to_address : amount } + rawtx = node.createrawtransaction(inputs, outputs) + signresult = node.signrawtransaction(rawtx) + assert_equal(signresult["complete"], True) + return signresult["hex"] + +# Create a spend of each passed-in utxo, splicing in "txouts" to each raw +# transaction to make it large. See gen_return_txouts() above. +def create_lots_of_big_transactions(node, txouts, utxos, num, fee): + addr = node.getnewaddress() + txids = [] + for _ in range(num): + t = utxos.pop() + inputs=[{ "txid" : t["txid"], "vout" : t["vout"]}] + outputs = {} + change = t['amount'] - fee + outputs[addr] = satoshi_round(change) + rawtx = node.createrawtransaction(inputs, outputs) + newtx = rawtx[0:92] + newtx = newtx + txouts + newtx = newtx + rawtx[94:] + signresult = node.signrawtransaction(newtx, None, None, "NONE") + txid = node.sendrawtransaction(signresult["hex"], True) + txids.append(txid) + return txids + +def mine_large_block(node, utxos=None): + # generate a 66k transaction, + # and 14 of them is close to the 1MB block limit + num = 14 + txouts = gen_return_txouts() + utxos = utxos if utxos is not None else [] + if len(utxos) < num: + utxos.clear() + utxos.extend(node.listunspent()) + fee = 100 * node.getnetworkinfo()["relayfee"] + create_lots_of_big_transactions(node, txouts, utxos, num, fee=fee) + node.generate(1) + +def get_bip9_status(node, key): + info = node.getblockchaininfo() + return info['bip9_softforks'][key] |