diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 00000000..dcfa3db4 --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,40 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python package + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..48cf063c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +git: + depth: 9999999 +language: python +python: + - "3.4" + - "3.5" + - "3.6" + - "3.7" +# command to run tests +script: python3 -m unittest discover -v diff --git a/README.md b/README.md index 550d53d1..e112c0df 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # python-bitcoinlib -This Python2/3 library provides an easy interface to the bitcoin data +This Python3 library provides an easy interface to the bitcoin data structures and protocol. The approach is low-level and "ground up", with a focus on providing tools to manipulate the internals of how Bitcoin works. @@ -11,7 +11,7 @@ focus on providing tools to manipulate the internals of how Bitcoin works. sudo apt-get install libssl-dev -The RPC interface, `bitcoin.rpc`, is designed to work with Bitcoin Core v0.16.0. +The RPC interface, `bitcoin.rpc`, should work with Bitcoin Core v24.0 or later. Older versions may work but there do exist some incompatibilities. @@ -59,7 +59,7 @@ Rather confusingly Bitcoin Core shows transaction and block hashes as little-endian hex rather than the big-endian the rest of the world uses for SHA256. python-bitcoinlib provides the convenience functions x() and lx() in bitcoin.core to convert from big-endian and little-endian hex to raw bytes to -accomodate this. In addition see b2x() and b2lx() for conversion from bytes to +accommodate this. In addition see b2x() and b2lx() for conversion from bytes to big/little-endian hex. @@ -75,7 +75,7 @@ appropriately. See `examples/` directory. For instance this example creates a transaction spending a pay-to-script-hash transaction output: - $ PYTHONPATH=. examples/spend-pay-to-script-hash-txout.py + $ PYTHONPATH=. examples/spend-p2sh-txout.py @@ -86,7 +86,7 @@ Do the following: import bitcoin bitcoin.SelectParams(NAME) -Where NAME is one of 'testnet', 'mainnet', or 'regtest'. The chain currently +Where NAME is one of 'testnet', 'mainnet', 'signet', or 'regtest'. The chain currently selected is a global variable that changes behavior everywhere, just like in the Satoshi codebase. @@ -95,27 +95,16 @@ the Satoshi codebase. Under bitcoin/tests using test data from Bitcoin Core. To run them: - python -m unittest discover && python3 -m unittest discover - -Please run the tests on both Python2 and Python3 for your pull-reqs! + python3 -m unittest discover Alternately, if Tox (see https://tox.readthedocs.org/) is available on your system, you can run unit tests for multiple Python versions: ./runtests.sh -Currently, the following implementations are tried (any not installed are -skipped): - - * CPython 2.7 - * CPython 3.3 - * CPython 3.4 - * CPython 3.5 - * PyPy - * PyPy3 - HTML coverage reports can then be found in the htmlcov/ subdirectory. + ## Documentation Sphinx documentation is in the "doc" subdirectory. Run "make help" from there diff --git a/bitcoin/__init__.py b/bitcoin/__init__.py index d52d9145..8af1b1bb 100644 --- a/bitcoin/__init__.py +++ b/bitcoin/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2018 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,14 +9,13 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals import bitcoin.core # Note that setup.py can break if __init__.py imports any external # dependencies, as these might not be installed when setup.py runs. In this # case __version__ could be moved to a separate version.py and imported here. -__version__ = '0.10.2dev' +__version__ = '0.12.2' class MainParams(bitcoin.core.CoreMainParams): MESSAGE_START = b'\xf9\xbe\xb4\xd9' @@ -26,23 +25,37 @@ class MainParams(bitcoin.core.CoreMainParams): ('bluematt.me', 'dnsseed.bluematt.me'), ('dashjr.org', 'dnsseed.bitcoin.dashjr.org'), ('bitcoinstats.com', 'seed.bitcoinstats.com'), + ('petertodd.org', 'seed.btc.petertodd.net'), ('xf2.org', 'bitseed.xf2.org'), ('bitcoin.jonasschnelli.ch', 'seed.bitcoin.jonasschnelli.ch')) BASE58_PREFIXES = {'PUBKEY_ADDR':0, 'SCRIPT_ADDR':5, 'SECRET_KEY' :128} + BECH32_HRP = 'bc' class TestNetParams(bitcoin.core.CoreTestNetParams): MESSAGE_START = b'\x0b\x11\x09\x07' DEFAULT_PORT = 18333 RPC_PORT = 18332 DNS_SEEDS = (('testnetbitcoin.jonasschnelli.ch', 'testnet-seed.bitcoin.jonasschnelli.ch'), - ('petertodd.org', 'seed.tbtc.petertodd.org'), + ('petertodd.org', 'seed.tbtc.petertodd.net'), ('bluematt.me', 'testnet-seed.bluematt.me'), ('bitcoin.schildbach.de', 'testnet-seed.bitcoin.schildbach.de')) BASE58_PREFIXES = {'PUBKEY_ADDR':111, 'SCRIPT_ADDR':196, 'SECRET_KEY' :239} + BECH32_HRP = 'tb' + +class SigNetParams(bitcoin.core.CoreSigNetParams): + MESSAGE_START = b'\x0a\x03\xcf\x40' + DEFAULT_PORT = 38333 + RPC_PORT = 38332 + DNS_SEEDS = (("signet.bitcoin.sprovoost.nl", "seed.signet.bitcoin.sprovoost.nl")) + BASE58_PREFIXES = {'PUBKEY_ADDR':111, + 'SCRIPT_ADDR':196, + 'SECRET_KEY' :239} + + BECH32_HRP = 'tb' class RegTestParams(bitcoin.core.CoreRegTestParams): MESSAGE_START = b'\xfa\xbf\xb5\xda' @@ -52,6 +65,7 @@ class RegTestParams(bitcoin.core.CoreRegTestParams): BASE58_PREFIXES = {'PUBKEY_ADDR':111, 'SCRIPT_ADDR':196, 'SECRET_KEY' :239} + BECH32_HRP = 'bcrt' """Master global setting for what chain params we're using. @@ -76,5 +90,7 @@ def SelectParams(name): params = bitcoin.core.coreparams = TestNetParams() elif name == 'regtest': params = bitcoin.core.coreparams = RegTestParams() + elif name == 'signet': + params = bitcoin.core.coreparams = SigNetParams() else: raise ValueError('Unknown chain %r' % name) diff --git a/bitcoin/base58.py b/bitcoin/base58.py index 4253240e..18956223 100644 --- a/bitcoin/base58.py +++ b/bitcoin/base58.py @@ -12,15 +12,6 @@ """Base58 encoding and decoding""" -from __future__ import absolute_import, division, print_function, unicode_literals - -import sys -_bchr = chr -_bord = ord -if sys.version > '3': - long = int - _bchr = lambda x: bytes([x]) - _bord = lambda x: x import binascii @@ -52,10 +43,7 @@ def encode(b): res = ''.join(res[::-1]) # Encode leading zeros as base58 zeros - czero = b'\x00' - if sys.version > '3': - # In Python3 indexing a bytes returns numbers, not characters. - czero = 0 + czero = 0 pad = 0 for c in b: if c == czero: @@ -108,7 +96,7 @@ def __new__(cls, s): if check0 != check1: raise Base58ChecksumError('Checksum mismatch: expected %r, calculated %r' % (check0, check1)) - return cls.from_bytes(data, _bord(verbyte[0])) + return cls.from_bytes(data, verbyte[0]) def __init__(self, s): """Initialize from base58-encoded string @@ -138,7 +126,7 @@ def to_bytes(self): def __str__(self): """Convert to string""" - vs = _bchr(self.nVersion) + self + vs = bytes([self.nVersion]) + self check = bitcoin.core.Hash(vs)[0:4] return encode(vs + check) diff --git a/bitcoin/bech32.py b/bitcoin/bech32.py new file mode 100644 index 00000000..1ef88b57 --- /dev/null +++ b/bitcoin/bech32.py @@ -0,0 +1,73 @@ +# Copyright (C) The python-bitcoinlib developers +# +# This file is part of python-bitcoinlib. +# +# It is subject to the license terms in the LICENSE file found in the top-level +# directory of this distribution. +# +# No part of python-bitcoinlib, including this file, may be copied, modified, +# propagated, or distributed except according to the terms contained in the +# LICENSE file. + +"""Bech32 encoding and decoding""" + +from bitcoin.segwit_addr import encode, decode +import bitcoin + +class Bech32Error(Exception): + pass + +class Bech32ChecksumError(Bech32Error): + pass + +class CBech32Data(bytes): + """Bech32-encoded data + + Includes a witver and checksum. + """ + def __new__(cls, s): + """from bech32 addr to """ + witver, data = decode(bitcoin.params.BECH32_HRP, s) + if witver is None and data is None: + raise Bech32Error('Bech32 decoding error') + + return cls.from_bytes(witver, data) + + def __init__(self, s): + """Initialize from bech32-encoded string + + Note: subclasses put your initialization routines here, but ignore the + argument - that's handled by __new__(), and .from_bytes() will call + __init__() with None in place of the string. + """ + + @classmethod + def from_bytes(cls, witver, witprog): + """Instantiate from witver and data""" + if not (0 <= witver <= 16): + raise ValueError('witver must be in range 0 to 16 inclusive; got %d' % witver) + self = bytes.__new__(cls, witprog) + self.witver = witver + + return self + + def to_bytes(self): + """Convert to bytes instance + + Note that it's the data represented that is converted; the checkum and + witver is not included. + """ + return b'' + self + + def __str__(self): + """Convert to string""" + return encode(bitcoin.params.BECH32_HRP, self.witver, self) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, str(self)) + +__all__ = ( + 'Bech32Error', + 'Bech32ChecksumError', + 'CBech32Data', +) diff --git a/bitcoin/bloom.py b/bitcoin/bloom.py index f74980cf..d0e3b765 100644 --- a/bitcoin/bloom.py +++ b/bitcoin/bloom.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2014 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -11,10 +11,8 @@ """Bloom filter support""" -from __future__ import absolute_import, division, print_function, unicode_literals import struct -import sys import math import bitcoin.core @@ -56,16 +54,12 @@ def MurmurHash3(nHashSeed, vDataToHash): # tail k1 = 0 j = (len(vDataToHash) // 4) * 4 - bord = ord - if sys.version > '3': - # In Py3 indexing bytes returns numbers, not characters - bord = lambda x: x if len(vDataToHash) & 3 >= 3: - k1 ^= bord(vDataToHash[j+2]) << 16 + k1 ^= vDataToHash[j+2] << 16 if len(vDataToHash) & 3 >= 2: - k1 ^= bord(vDataToHash[j+1]) << 8 + k1 ^= vDataToHash[j+1] << 8 if len(vDataToHash) & 3 >= 1: - k1 ^= bord(vDataToHash[j]) + k1 ^= vDataToHash[j] k1 &= 0xFFFFFFFF k1 = (k1 * c1) & 0xFFFFFFFF @@ -180,11 +174,7 @@ def stream_deserialize(cls, f): return deserialized def stream_serialize(self, f): - if sys.version > '3': - bitcoin.core.serialize.BytesSerializer.stream_serialize(self.vData, f) - else: - # 2.7 has problems with f.write(bytearray()) - bitcoin.core.serialize.BytesSerializer.stream_serialize(bytes(self.vData), f) + bitcoin.core.serialize.BytesSerializer.stream_serialize(self.vData, f) f.write(self.__struct.pack(self.nHashFuncs, self.nTweak, self.nFlags)) __all__ = ( diff --git a/bitcoin/core/__init__.py b/bitcoin/core/__init__.py index 272bec5c..ea578f9b 100644 --- a/bitcoin/core/__init__.py +++ b/bitcoin/core/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2017 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,28 +9,22 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function import binascii import struct -import sys import time -from .script import CScript, CScriptWitness, CScriptOp, OP_RETURN +from . import script +from .script import CScript, CScriptWitness, OP_RETURN from .serialize import * -if sys.version > '3': - _bytes = bytes -else: - _bytes = lambda x: bytes(bytearray(x)) - # Core definitions COIN = 100000000 MAX_BLOCK_SIZE = 1000000 MAX_BLOCK_WEIGHT = 4000000 MAX_BLOCK_SIGOPS = MAX_BLOCK_SIZE/50 -WITNESS_COINBASE_SCRIPTPUBKEY_MAGIC = _bytes([OP_RETURN, 0x24, 0xaa, 0x21, 0xa9, 0xed]) +WITNESS_COINBASE_SCRIPTPUBKEY_MAGIC = bytes([OP_RETURN, 0x24, 0xaa, 0x21, 0xa9, 0xed]) def MoneyRange(nValue, params=None): global coreparams @@ -39,30 +33,14 @@ def MoneyRange(nValue, params=None): return 0 <= nValue <= params.MAX_MONEY -def _py2_x(h): - """Convert a hex string to bytes""" - return binascii.unhexlify(h) - def x(h): """Convert a hex string to bytes""" return binascii.unhexlify(h.encode('utf8')) -def _py2_b2x(b): - """Convert bytes to a hex string""" - return binascii.hexlify(b) - def b2x(b): """Convert bytes to a hex string""" return binascii.hexlify(b).decode('utf8') -def _py2_lx(h): - """Convert a little-endian hex string to bytes - - Lets you write uint256's and uint160's the way the Satoshi codebase shows - them. - """ - return binascii.unhexlify(h)[::-1] - def lx(h): """Convert a little-endian hex string to bytes @@ -71,14 +49,6 @@ def lx(h): """ return binascii.unhexlify(h.encode('utf8'))[::-1] -def _py2_b2lx(b): - """Convert bytes to a little-endian hex string - - Lets you show uint256's and uint160's the way the Satoshi codebase shows - them. - """ - return binascii.hexlify(b[::-1]) - def b2lx(b): """Convert bytes to a little-endian hex string @@ -87,18 +57,6 @@ def b2lx(b): """ return binascii.hexlify(b[::-1]).decode('utf8') -if not (sys.version > '3'): - x = _py2_x - b2x = _py2_b2x - lx = _py2_lx - b2lx = _py2_b2lx - -del _py2_x -del _py2_b2x -del _py2_lx -del _py2_b2lx - - def str_money_value(value): """Convert an integer money value to a fixed point string""" r = '%i.%08i' % (value // COIN, value % COIN) @@ -476,9 +434,11 @@ def from_tx(cls, tx): return cls(tx.vin, tx.vout, tx.nLockTime, tx.nVersion, tx.wit) def GetTxid(self): - """Get the transaction ID. This differs from the transactions hash as - given by GetHash. GetTxid excludes witness data, while GetHash - includes it. """ + """Get the transaction ID. + + This differs from the transactions hash as given by GetHash. GetTxid + excludes witness data, while GetHash includes it. + """ if self.wit != CTxWitness(): txid = Hash(CTransaction(self.vin, self.vout, self.nLockTime, self.nVersion).serialize()) @@ -486,6 +446,24 @@ def GetTxid(self): txid = Hash(self.serialize()) return txid + def calc_weight(self): + """Calculate the transaction weight, as defined by BIP141. + + The transaction must contain at least one input and one output. + """ + # Not clear how calc_weight() should be defined for the zero vin/vout + # cases, so punting on that decision for now. + assert len(self.vin) > 0 + assert len(self.vout) > 0 + + # This special case isn't strictly necessary. But saves on serializing + # the transaction twice in the no-witness case. + if self.wit.is_null(): + return len(self.serialize()) * 4 + else: + stripped = CTransaction(self.vin, self.vout, self.nLockTime, self.nVersion) + return len(stripped.serialize()) * 3 + len(self.serialize()) + @__make_mutable class CMutableTransaction(CTransaction): """A mutable transaction""" @@ -752,6 +730,10 @@ class CoreTestNetParams(CoreMainParams): NAME = 'testnet' GENESIS_BLOCK = CBlock.deserialize(x('0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae180101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000')) +class CoreSigNetParams(CoreMainParams): + NAME = 'signet' + GENESIS_BLOCK = CBlock.deserialize(x('0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a008f4d5fae77031e8ad222030101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000')) + class CoreRegTestParams(CoreTestNetParams): NAME = 'regtest' GENESIS_BLOCK = CBlock.deserialize(x('0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff7f20020000000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000')) @@ -774,6 +756,8 @@ def _SelectCoreParams(name): coreparams = CoreTestNetParams() elif name == 'regtest': coreparams = CoreRegTestParams() + elif name == 'signet': + coreparams = CoreSigNetParams() else: raise ValueError('Unknown chain %r' % name) diff --git a/bitcoin/core/_bignum.py b/bitcoin/core/_bignum.py index 42985a7f..9b9f11ac 100644 --- a/bitcoin/core/_bignum.py +++ b/bitcoin/core/_bignum.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2014 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -13,7 +13,6 @@ # # Internally used for script evaluation; not to be used externally. -from __future__ import absolute_import, division, print_function, unicode_literals import struct diff --git a/bitcoin/core/contrib/__init__.py b/bitcoin/core/contrib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bitcoin/core/contrib/ripemd160.py b/bitcoin/core/contrib/ripemd160.py new file mode 100644 index 00000000..2f5eaeaf --- /dev/null +++ b/bitcoin/core/contrib/ripemd160.py @@ -0,0 +1,110 @@ +# Copyright (c) 2021 Pieter Wuille +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +""" +Pure Python RIPEMD160 implementation. Note that this impelentation is not constant time. +Original source: https://github.com/bitcoin/bitcoin/pull/23716 +""" + +# Message schedule indexes for the left path. +ML = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, + 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, + 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, + 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13 +] + +# Message schedule indexes for the right path. +MR = [ + 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, + 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, + 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, + 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, + 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11 +] + +# Rotation counts for the left path. +RL = [ + 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, + 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, + 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, + 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, + 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6 +] + +# Rotation counts for the right path. +RR = [ + 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, + 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, + 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, + 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, + 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11 +] + +# K constants for the left path. +KL = [0, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e] + +# K constants for the right path. +KR = [0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0] + + +def fi(x, y, z, i): + """The f1, f2, f3, f4, and f5 functions from the specification.""" + if i == 0: + return x ^ y ^ z + elif i == 1: + return (x & y) | (~x & z) + elif i == 2: + return (x | ~y) ^ z + elif i == 3: + return (x & z) | (y & ~z) + elif i == 4: + return x ^ (y | ~z) + else: + assert False + + +def rol(x, i): + """Rotate the bottom 32 bits of x left by i bits.""" + return ((x << i) | ((x & 0xffffffff) >> (32 - i))) & 0xffffffff + + +def compress(h0, h1, h2, h3, h4, block): + """Compress state (h0, h1, h2, h3, h4) with block.""" + # Left path variables. + al, bl, cl, dl, el = h0, h1, h2, h3, h4 + # Right path variables. + ar, br, cr, dr, er = h0, h1, h2, h3, h4 + # Message variables. + x = [int.from_bytes(block[4*i:4*(i+1)], 'little') for i in range(16)] + + # Iterate over the 80 rounds of the compression. + for j in range(80): + rnd = j >> 4 + # Perform left side of the transformation. + al = rol(al + fi(bl, cl, dl, rnd) + x[ML[j]] + KL[rnd], RL[j]) + el + al, bl, cl, dl, el = el, al, bl, rol(cl, 10), dl + # Perform right side of the transformation. + ar = rol(ar + fi(br, cr, dr, 4 - rnd) + x[MR[j]] + KR[rnd], RR[j]) + er + ar, br, cr, dr, er = er, ar, br, rol(cr, 10), dr + + # Compose old state, left transform, and right transform into new state. + return h1 + cl + dr, h2 + dl + er, h3 + el + ar, h4 + al + br, h0 + bl + cr + + +def ripemd160(data): + """Compute the RIPEMD-160 hash of data.""" + # Initialize state. + state = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0) + # Process full 64-byte blocks in the input. + for b in range(len(data) >> 6): + state = compress(*state, data[64*b:64*(b+1)]) + # Construct final blocks (with padding and size). + pad = b"\x80" + b"\x00" * ((119 - len(data)) & 63) + fin = data[len(data) & ~63:] + pad + (8 * len(data)).to_bytes(8, 'little') + # Process final blocks. + for b in range(len(fin) >> 6): + state = compress(*state, fin[64*b:64*(b+1)]) + # Produce output. + return b"".join((h & 0xffffffff).to_bytes(4, 'little') for h in state) \ No newline at end of file diff --git a/bitcoin/core/key.py b/bitcoin/core/key.py index 4fce32c2..0f902c8c 100644 --- a/bitcoin/core/key.py +++ b/bitcoin/core/key.py @@ -18,19 +18,22 @@ import ctypes import ctypes.util import hashlib -import sys +from os import urandom import bitcoin import bitcoin.signature -_bchr = chr -_bord = ord -if sys.version > '3': - _bchr = lambda x: bytes([x]) - _bord = lambda x: x - import bitcoin.core.script -_ssl = ctypes.cdll.LoadLibrary(ctypes.util.find_library('ssl') or 'libeay32') +_ssl = ctypes.cdll.LoadLibrary( + ctypes.util.find_library('ssl.35') or ctypes.util.find_library('ssl') or ctypes.util.find_library('libeay32') + or ctypes.util.find_library('libcrypto') +) + +_libsecp256k1_path = ctypes.util.find_library('secp256k1') +_libsecp256k1_enable_signing = False +_libsecp256k1_context = None +_libsecp256k1 = None + class OpenSSLException(EnvironmentError): pass @@ -185,12 +188,56 @@ def _check_res_void_p(val, func, args): # pylint: disable=unused-argument _ssl.o2i_ECPublicKey.restype = ctypes.c_void_p _ssl.o2i_ECPublicKey.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_long] +_ssl.BN_num_bits.restype = ctypes.c_int +_ssl.BN_num_bits.argtypes = [ctypes.c_void_p] +_ssl.EC_KEY_get0_private_key.restype = ctypes.c_void_p + # this specifies the curve used with ECDSA. _NID_secp256k1 = 714 # from openssl/obj_mac.h # test that OpenSSL supports secp256k1 _ssl.EC_KEY_new_by_curve_name(_NID_secp256k1) +SECP256K1_FLAGS_TYPE_CONTEXT = (1 << 0) +SECP256K1_FLAGS_BIT_CONTEXT_SIGN = (1 << 9) +SECP256K1_CONTEXT_SIGN = \ + (SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_SIGN) + + +def is_libsec256k1_available(): + return _libsecp256k1_path is not None + + +def use_libsecp256k1_for_signing(do_use): + global _libsecp256k1 + global _libsecp256k1_context + global _libsecp256k1_enable_signing + + if not do_use: + _libsecp256k1_enable_signing = False + return + + if not is_libsec256k1_available(): + raise ImportError("unable to locate libsecp256k1") + + if _libsecp256k1_context is None: + _libsecp256k1 = ctypes.cdll.LoadLibrary(_libsecp256k1_path) + _libsecp256k1.secp256k1_context_create.restype = ctypes.c_void_p + _libsecp256k1.secp256k1_context_create.errcheck = _check_res_void_p + _libsecp256k1.secp256k1_context_randomize.restype = ctypes.c_int + _libsecp256k1.secp256k1_context_randomize.argtypes = [ctypes.c_void_p, ctypes.c_char_p] + _libsecp256k1_context = _libsecp256k1.secp256k1_context_create(SECP256K1_CONTEXT_SIGN) + assert(_libsecp256k1_context is not None) + seed = urandom(32) + result = _libsecp256k1.secp256k1_context_randomize(_libsecp256k1_context, seed) + assert 1 == result + + + + _libsecp256k1_enable_signing = True + + + # From openssl/ecdsa.h class ECDSA_SIG_st(ctypes.Structure): _fields_ = [("r", ctypes.c_void_p), @@ -211,16 +258,18 @@ def __del__(self): self.k = None def set_secretbytes(self, secret): - priv_key = _ssl.BN_bin2bn(secret, 32, _ssl.BN_new()) + if(len(secret) != 32): + raise ValueError("Secret bytes must be exactly 32 bytes") + priv_key = _ssl.BN_bin2bn(secret, 32, None) 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_free(priv_key) _ssl.BN_CTX_free(ctx) return self.k @@ -258,12 +307,39 @@ def get_ecdh_key(self, other_pubkey, kdf=lambda k: hashlib.sha256(k).digest()): r = self.get_raw_ecdh_key(other_pubkey) return kdf(r) + def get_raw_privkey(self): + bn = _ssl.EC_KEY_get0_private_key(self.k) + bn = ctypes.c_void_p(bn) + size = (_ssl.BN_num_bits(bn) + 7) / 8 + mb = ctypes.create_string_buffer(int(size)) + _ssl.BN_bn2bin(bn, mb) + return mb.raw.rjust(32, b'\x00') + + def _sign_with_libsecp256k1(self, hash): + raw_sig = ctypes.create_string_buffer(64) + result = _libsecp256k1.secp256k1_ecdsa_sign( + _libsecp256k1_context, raw_sig, hash, self.get_raw_privkey(), None, None) + assert 1 == result + sig_size0 = ctypes.c_size_t() + sig_size0.value = 75 + mb_sig = ctypes.create_string_buffer(sig_size0.value) + result = _libsecp256k1.secp256k1_ecdsa_signature_serialize_der( + _libsecp256k1_context, mb_sig, ctypes.byref(sig_size0), raw_sig) + assert 1 == result + # libsecp256k1 creates signatures already in lower-S form, no further + # conversion needed. + return mb_sig.raw[:sig_size0.value] + + def sign(self, hash): # pylint: disable=redefined-builtin 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') + if _libsecp256k1_enable_signing: + return self._sign_with_libsecp256k1(hash) + sig_size0 = ctypes.c_uint32() sig_size0.value = _ssl.ECDSA_size(self.k) mb_sig = ctypes.create_string_buffer(sig_size0.value) @@ -359,6 +435,12 @@ def verify(self, hash, sig): # pylint: disable=redefined-builtin norm_sig = ctypes.c_void_p(0) _ssl.d2i_ECDSA_SIG(ctypes.byref(norm_sig), ctypes.byref(ctypes.c_char_p(sig)), len(sig)) + # Newer versions of OpenSSL (>3.0.0?) seem to fail here, leaving a null + # pointer in norm_sig + if not norm_sig: + return False + + # Older versions (<3.0.0?) seem to fail here, with a empty derlen derlen = _ssl.i2d_ECDSA_SIG(norm_sig, 0) if derlen == 0: _ssl.ECDSA_SIG_free(norm_sig) @@ -507,8 +589,8 @@ def recover_compact(cls, hash, sig): # pylint: disable=redefined-builtin if len(sig) != 65: raise ValueError("Signature should be 65 characters, not [%d]" % (len(sig), )) - recid = (_bord(sig[0]) - 27) & 3 - compressed = (_bord(sig[0]) - 27) & 4 != 0 + recid = (sig[0] - 27) & 3 + compressed = (sig[0] - 27) & 4 != 0 cec_key = CECKey() cec_key.set_compressed(compressed) @@ -540,12 +622,7 @@ def __str__(self): return repr(self) def __repr__(self): - # Always have represent as b'' 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__()) + return '%s(%s)' % (self.__class__.__name__, super(CPubKey, self).__repr__()) __all__ = ( 'CECKey', diff --git a/bitcoin/core/script.py b/bitcoin/core/script.py index ffd97b8b..c0058a41 100644 --- a/bitcoin/core/script.py +++ b/bitcoin/core/script.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2015 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -15,18 +15,8 @@ is in bitcoin.core.scripteval """ -from __future__ import absolute_import, division, print_function -import sys -_bchr = chr -_bord = ord -if sys.version > '3': - long = int - _bchr = lambda x: bytes([x]) - _bord = lambda x: x - from io import BytesIO as _BytesIO -else: - from cStringIO import StringIO as _BytesIO +from io import BytesIO import struct @@ -50,9 +40,9 @@ class CScriptOp(int): def encode_op_pushdata(d): """Encode a PUSHDATA op, returning bytes""" if len(d) < 0x4c: - return b'' + _bchr(len(d)) + d # OP_PUSHDATA + return bytes([len(d)]) + d # OP_PUSHDATA elif len(d) <= 0xff: - return b'\x4c' + _bchr(len(d)) + d # OP_PUSHDATA1 + return b'\x4c' + bytes([len(d)]) + d # OP_PUSHDATA1 elif len(d) <= 0xffff: return b'\x4d' + struct.pack(b' OP_PUSHDATA4: @@ -585,21 +578,21 @@ def raw_iter(self): pushdata_type = 'PUSHDATA1' if i >= len(self): raise CScriptInvalidError('PUSHDATA1: missing data length') - datasize = _bord(self[i]) + datasize = self[i] i += 1 elif opcode == OP_PUSHDATA2: pushdata_type = 'PUSHDATA2' if i + 1 >= len(self): raise CScriptInvalidError('PUSHDATA2: missing data length') - datasize = _bord(self[i]) + (_bord(self[i+1]) << 8) + datasize = self[i] + (self[i+1] << 8) i += 2 elif opcode == OP_PUSHDATA4: pushdata_type = 'PUSHDATA4' if i + 3 >= len(self): raise CScriptInvalidError('PUSHDATA4: missing data length') - datasize = _bord(self[i]) + (_bord(self[i+1]) << 8) + (_bord(self[i+2]) << 16) + (_bord(self[i+3]) << 24) + datasize = self[i] + (self[i+1] << 8) + (self[i+2] << 16) + (self[i+3] << 24) i += 4 else: @@ -673,14 +666,28 @@ def is_p2sh(self): Note that this test is consensus-critical. """ return (len(self) == 23 and - _bord(self[0]) == OP_HASH160 and - _bord(self[1]) == 0x14 and - _bord(self[22]) == OP_EQUAL) + self[0] == OP_HASH160 and + self[1] == 0x14 and + self[22] == OP_EQUAL) def is_witness_scriptpubkey(self): - """Returns true if this is a scriptpubkey signaling segregated witness - data. """ - return 3 <= len(self) <= 42 and CScriptOp(struct.unpack(' 42: + return False + + head = struct.unpack(' OP_16: continue - elif op < OP_PUSHDATA1 and op > OP_0 and len(data) == 1 and _bord(data[0]) <= 16: + elif op < OP_PUSHDATA1 and op > OP_0 and len(data) == 1 and data[0] <= 16: # Could have used an OP_n code, rather than a 1-byte push. return False @@ -753,7 +760,7 @@ def has_canonical_pushes(self): def is_unspendable(self): """Test if the script is provably unspendable""" return (len(self) > 0 and - _bord(self[0]) == OP_RETURN) + self[0] == OP_RETURN) def is_valid(self): """Return True if the script is valid, False otherwise @@ -1001,7 +1008,7 @@ def SignatureHash(script, txTo, inIdx, hashtype, amount=None, sigversion=SIGVERS serialize_outputs = txTo.vout[inIdx].serialize() hashOutputs = bitcoin.core.Hash(serialize_outputs) - f = _BytesIO() + f = BytesIO() f.write(struct.pack(" '3': - long = int - _bord = lambda x: x import hashlib @@ -30,6 +23,7 @@ import bitcoin.core._bignum import bitcoin.core.key import bitcoin.core.serialize +from bitcoin.core.contrib.ripemd160 import ripemd160 # Importing everything for simplicity; note that we use __all__ at the end so # we're not exporting the whole contents of the script module. @@ -122,7 +116,7 @@ def _CastToBigNum(s, err_raiser): def _CastToBool(s): for i in range(len(s)): - sv = _bord(s[i]) + sv = s[i] if sv != 0: if (i == (len(s) - 1)) and (sv == 0x80): return False @@ -137,7 +131,7 @@ def _CheckSig(sig, pubkey, script, txTo, inIdx, err_raiser): if len(sig) == 0: return False - hashtype = _bord(sig[-1]) + hashtype = sig[-1] sig = sig[:-1] # Raw signature hash due to the SIGHASH_SINGLE bug @@ -260,10 +254,10 @@ def _UnaryOp(opcode, stack, err_raiser): bn = -bn elif opcode == OP_NOT: - bn = long(bn == 0) + bn = int(bn == 0) elif opcode == OP_0NOTEQUAL: - bn = long(bn != 0) + bn = int(bn != 0) else: raise AssertionError("Unknown unary opcode encountered; this should not happen") @@ -305,16 +299,16 @@ def _BinOp(opcode, stack, err_raiser): bn = bn1 - bn2 elif opcode == OP_BOOLAND: - bn = long(bn1 != 0 and bn2 != 0) + bn = int(bn1 != 0 and bn2 != 0) elif opcode == OP_BOOLOR: - bn = long(bn1 != 0 or bn2 != 0) + bn = int(bn1 != 0 or bn2 != 0) elif opcode == OP_NUMEQUAL: - bn = long(bn1 == bn2) + bn = int(bn1 == bn2) elif opcode == OP_NUMEQUALVERIFY: - bn = long(bn1 == bn2) + bn = int(bn1 == bn2) if not bn: err_raiser(VerifyOpFailedError, opcode) else: @@ -324,19 +318,19 @@ def _BinOp(opcode, stack, err_raiser): return elif opcode == OP_NUMNOTEQUAL: - bn = long(bn1 != bn2) + bn = int(bn1 != bn2) elif opcode == OP_LESSTHAN: - bn = long(bn1 < bn2) + bn = int(bn1 < bn2) elif opcode == OP_GREATERTHAN: - bn = long(bn1 > bn2) + bn = int(bn1 > bn2) elif opcode == OP_LESSTHANOREQUAL: - bn = long(bn1 <= bn2) + bn = int(bn1 <= bn2) elif opcode == OP_GREATERTHANOREQUAL: - bn = long(bn1 >= bn2) + bn = int(bn1 >= bn2) elif opcode == OP_MIN: if bn1 < bn2: @@ -631,9 +625,7 @@ def check_args(n): elif sop == OP_RIPEMD160: check_args(1) - h = hashlib.new('ripemd160') - h.update(stack.pop()) - stack.append(h.digest()) + stack.append(ripemd160(stack.pop())) elif sop == OP_ROT: check_args(3) diff --git a/bitcoin/core/serialize.py b/bitcoin/core/serialize.py index c89b73e4..ae82d503 100644 --- a/bitcoin/core/serialize.py +++ b/bitcoin/core/serialize.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2018 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -11,39 +11,27 @@ """Serialization routines -You probabably don't need to use these directly. +You probably don't need to use these directly. """ -from __future__ import absolute_import, division, print_function, unicode_literals import hashlib import struct -# Py3 compatibility -import sys +from io import BytesIO -if sys.version > '3': - _bchr = lambda x: bytes([x]) - _bord = lambda x: x[0] - from io import BytesIO as _BytesIO -else: - _bchr = chr - _bord = ord - from cStringIO import StringIO as _BytesIO +from bitcoin.core.contrib.ripemd160 import ripemd160 MAX_SIZE = 0x02000000 def Hash(msg): - """SHA256^2)(msg) -> bytes""" + """SHA256^2(msg) -> bytes""" return hashlib.sha256(hashlib.sha256(msg).digest()).digest() def Hash160(msg): """RIPEME160(SHA256(msg)) -> bytes""" - h = hashlib.new('ripemd160') - h.update(hashlib.sha256(msg).digest()) - return h.digest() - + return ripemd160(hashlib.sha256(msg).digest()) class SerializationError(Exception): """Base class for serialization errors""" @@ -98,7 +86,7 @@ def stream_deserialize(cls, f, **kwargs): def serialize(self, params={}): """Serialize, returning bytes""" - f = _BytesIO() + f = BytesIO() self.stream_serialize(f, **params) return f.getvalue() @@ -111,7 +99,7 @@ def deserialize(cls, buf, allow_padding=False, params={}): If allow_padding is False and not all bytes are consumed during deserialization DeserializationExtraDataError will be raised. """ - fd = _BytesIO(buf) + fd = BytesIO(buf) r = cls.stream_deserialize(fd, **params) if not allow_padding: padding = fd.read() @@ -179,14 +167,14 @@ def stream_deserialize(cls, f): @classmethod def serialize(cls, obj): - f = _BytesIO() + f = BytesIO() cls.stream_serialize(obj, f) return f.getvalue() @classmethod def deserialize(cls, buf): if isinstance(buf, str) or isinstance(buf, bytes): - buf = _BytesIO(buf) + buf = BytesIO(buf) return cls.stream_deserialize(buf) @@ -197,20 +185,20 @@ def stream_serialize(cls, i, f): if i < 0: raise ValueError('varint must be non-negative integer') elif i < 0xfd: - f.write(_bchr(i)) + f.write(bytes([i])) elif i <= 0xffff: - f.write(_bchr(0xfd)) + f.write(b'\xfd') f.write(struct.pack(b' '3': - _bchr = lambda x: bytes([x]) - _bord = lambda x: x[0] - from io import BytesIO as _BytesIO -else: - _bchr = chr - _bord = ord - from cStringIO import StringIO as _BytesIO +from io import BytesIO # Bad practice, so we have a __all__ at the end; this should be cleaned up # later. @@ -59,7 +51,7 @@ def msg_deser(cls, f, protover=PROTO_VERSION): raise NotImplementedError def to_bytes(self): - f = _BytesIO() + f = BytesIO() self.msg_ser(f) body = f.getvalue() res = bitcoin.params.MESSAGE_START @@ -77,7 +69,7 @@ def to_bytes(self): @classmethod def from_bytes(cls, b, protover=PROTO_VERSION): - f = _BytesIO(b) + f = BytesIO(b) return MsgSerializable.stream_deserialize(f, protover=protover) @classmethod @@ -107,7 +99,7 @@ def stream_deserialize(cls, f, protover=PROTO_VERSION): if command in messagemap: cls = messagemap[command] # print("Going to deserialize '%s'" % msg) - return cls.msg_deser(_BytesIO(msg)) + return cls.msg_deser(BytesIO(msg)) else: print("Command '%s' not in messagemap" % repr(command)) return None @@ -160,7 +152,7 @@ def msg_deser(cls, f, protover=PROTO_VERSION): else: c.fRelay = True return c - + def msg_ser(self, f): f.write(struct.pack(b" '3': - unhexlify = lambda h: binascii.unhexlify(h.encode('utf8')) - hexlify = lambda b: binascii.hexlify(b).decode('utf8') +def unhexlify_str(h): + """ + Converts a string containing hexadecimal encoding into a bytes-object. + + It works by encoding the given string h as ASCII, then interpreting each of + its two-character ASCII tuples as bytes: + - "00" to byte 0 + - "ff" to byte 255 + - "FF" also to byte 255 + + The string must only contain characters in the ranges 0-9, a-f and A-F. + + If the string contains characters not in ASCII, + UnicodeEncodeError is raised. + If the string contains out-of-range ASCII characters, + binascii.Error is raised. + If number of encoded ASCII bytes is odd, + binascii.Error is raised. + """ + return binascii.unhexlify(h.encode('ascii')) + +def hexlify_str(b): + """ + Given an arbitrary bytes-like object, returns a string (that would encode + as ASCII) containing the hex-representation of the bytes-like object. + + Always succeeds. + """ + return binascii.hexlify(b).decode('ascii') class JSONRPCError(Exception): @@ -123,7 +136,8 @@ def __init__(self, service_url=None, service_port=None, btc_conf_file=None, - timeout=DEFAULT_HTTP_TIMEOUT): + timeout=DEFAULT_HTTP_TIMEOUT, + connection=None): # Create a dummy connection early on so if __init__() fails prior to # __conn being created __del__() can detect the condition and handle it @@ -169,7 +183,9 @@ def __init__(self, ('http', conf['rpchost'], conf['rpcport'])) cookie_dir = conf.get('datadir', os.path.dirname(btc_conf_file)) - if bitcoin.params.NAME != "mainnet": + if bitcoin.params.NAME == 'testnet': + cookie_dir = os.path.join(cookie_dir, 'testnet3') + elif bitcoin.params.NAME == 'regtest': cookie_dir = os.path.join(cookie_dir, bitcoin.params.NAME) cookie_file = os.path.join(cookie_dir, ".cookie") try: @@ -189,11 +205,14 @@ def __init__(self, self.__service_url = service_url self.__url = urlparse.urlparse(service_url) - if self.__url.scheme not in ('http',): + if self.__url.scheme not in ('http', 'https'): raise ValueError('Unsupported URL scheme %r' % self.__url.scheme) if self.__url.port is None: - port = httplib.HTTP_PORT + if self.__url.scheme == 'https': + port = httplib.HTTPS_PORT + else: + port = httplib.HTTP_PORT else: port = self.__url.port self.__id_count = 0 @@ -204,8 +223,15 @@ def __init__(self, authpair = authpair.encode('utf8') self.__auth_header = b"Basic " + base64.b64encode(authpair) - self.__conn = httplib.HTTPConnection(self.__url.hostname, port=port, - timeout=timeout) + if connection: + self.__conn = connection + else: + if self.__url.scheme == 'https': + self.__conn = httplib.HTTPSConnection(self.__url.hostname, port=port, + timeout=timeout) + else: + self.__conn = httplib.HTTPConnection(self.__url.hostname, port=port, + timeout=timeout) def _call(self, service_name, *args): self.__id_count += 1 @@ -227,8 +253,13 @@ def _call(self, service_name, *args): self.__conn.request('POST', self.__url.path, postdata, headers) response = self._get_response() - if response['error'] is not None: - raise JSONRPCError(response['error']) + err = response.get('error') + if err is not None: + if isinstance(err, dict): + raise JSONRPCError( + {'code': err.get('code', -345), + 'message': err.get('message', 'error message not specified')}) + raise JSONRPCError({'code': -344, 'message': str(err)}) elif 'result' not in response: raise JSONRPCError({ 'code': -343, 'message': 'missing JSON-RPC result'}) @@ -256,8 +287,15 @@ def _get_response(self): raise JSONRPCError({ 'code': -342, 'message': 'missing HTTP response from server'}) - return json.loads(http_response.read().decode('utf8'), - parse_float=decimal.Decimal) + rdata = http_response.read().decode('utf8') + try: + return json.loads(rdata, parse_float=decimal.Decimal) + except Exception: + raise JSONRPCError({ + 'code': -342, + 'message': ('non-JSON HTTP response with \'%i %s\' from server: \'%.20s%s\'' + % (http_response.status, http_response.reason, + rdata, '...' if len(rdata) > 20 else ''))}) def close(self): if self.__conn is not None: @@ -289,7 +327,10 @@ def __init__(self, def __getattr__(self, name): if name.startswith('__') and name.endswith('__'): - # Python internal stuff + # Prevent RPC calls for non-existing python internal attribute + # access. If someone tries to get an internal attribute + # of RawProxy instance, and the instance does not have this + # attribute, we do not want the bogus RPC call to happen. raise AttributeError # Create a callable to do the actual call @@ -323,7 +364,7 @@ def __init__(self, out of the file ``btc_conf_file``. If ``btc_conf_file`` is not specified, ``~/.bitcoin/bitcoin.conf`` or equivalent is used by default. The default port is set according to the chain parameters in - use: mainnet, testnet, or regtest. + use: mainnet, testnet, signet, or regtest. Usually no arguments to ``Proxy()`` are needed; the local bitcoind will be used. @@ -360,10 +401,10 @@ def fundrawtransaction(self, tx, include_watching=False): 'changepos': Position of added change output, or -1, } """ - hextx = hexlify(tx.serialize()) + hextx = hexlify_str(tx.serialize()) r = self._call('fundrawtransaction', hextx, include_watching) - r['tx'] = CTransaction.deserialize(unhexlify(r['hex'])) + r['tx'] = CTransaction.deserialize(unhexlify_str(r['hex'])) del r['hex'] r['fee'] = int(r['fee'] * COIN) @@ -371,7 +412,10 @@ def fundrawtransaction(self, tx, include_watching=False): return r def generate(self, numblocks): - """Mine blocks immediately (before the RPC call returns) + """ + DEPRECATED (will be removed in bitcoin-core v0.19) + + Mine blocks immediately (before the RPC call returns) numblocks - How many blocks are generated immediately. @@ -380,6 +424,19 @@ def generate(self, numblocks): r = self._call('generate', numblocks) return (lx(blk_hash) for blk_hash in r) + def generatetoaddress(self, numblocks, addr): + """Mine blocks immediately (before the RPC call returns) and + allocate block reward to passed address. Replaces deprecated + "generate(self,numblocks)" method. + + numblocks - How many blocks are generated immediately. + addr - Address to receive block reward (CBitcoinAddress instance) + + Returns iterable of block hashes generated. + """ + r = self._call('generatetoaddress', numblocks, str(addr)) + return (lx(blk_hash) for blk_hash in r) + def getaccountaddress(self, account=None): """Return the current Bitcoin address for receiving payments to this account.""" @@ -435,7 +492,7 @@ def getblockheader(self, block_hash, verbose=False): 'nextblockhash':nextblockhash, 'chainwork':x(r['chainwork'])} else: - return CBlockHeader.deserialize(unhexlify(r)) + return CBlockHeader.deserialize(unhexlify_str(r)) def getblock(self, block_hash): @@ -449,11 +506,14 @@ def getblock(self, block_hash): raise TypeError('%s.getblock(): block_hash must be bytes; got %r instance' % (self.__class__.__name__, block_hash.__class__)) try: + # With this change ( https://github.com/bitcoin/bitcoin/commit/96c850c20913b191cff9f66fedbb68812b1a41ea#diff-a0c8f511d90e83aa9b5857e819ced344 ), + # bitcoin core's rpc takes 0/1/2 instead of true/false as the 2nd argument which specifies verbosity, since v0.15.0. + # The change above is backward-compatible so far; the old "false" is taken as the new "0". r = self._call('getblock', block_hash, False) except InvalidAddressOrKeyError as ex: raise IndexError('%s.getblock(): %s (%d)' % (self.__class__.__name__, ex.error['message'], ex.error['code'])) - return CBlock.deserialize(unhexlify(r)) + return CBlock.deserialize(unhexlify_str(r)) def getblockcount(self): """Return the number of blocks in the longest block chain""" @@ -515,7 +575,7 @@ def getrawmempool(self, verbose=False): r = [lx(txid) for txid in r] return r - def getrawtransaction(self, txid, verbose=False): + def getrawtransaction(self, txid, verbose=False, block_hash=None): """Return transaction with hash txid Raises IndexError if transaction not found. @@ -523,16 +583,22 @@ def getrawtransaction(self, txid, verbose=False): verbose - If true a dict is returned instead with additional information on the transaction. + block_hash - Hash of the block containing the transaction + (required when transaction not currently indexed by Bitcoin Core) + Note that if all txouts are spent and the transaction index is not enabled the transaction may not be available. """ try: - r = self._call('getrawtransaction', b2lx(txid), 1 if verbose else 0) + if block_hash is None: + r = self._call('getrawtransaction', b2lx(txid), 1 if verbose else 0) + else: + r = self._call('getrawtransaction', b2lx(txid), 1 if verbose else 0, b2lx(block_hash)) except InvalidAddressOrKeyError as ex: raise IndexError('%s.getrawtransaction(): %s (%d)' % (self.__class__.__name__, ex.error['message'], ex.error['code'])) if verbose: - r['tx'] = CTransaction.deserialize(unhexlify(r['hex'])) + r['tx'] = CTransaction.deserialize(unhexlify_str(r['hex'])) del r['hex'] del r['txid'] del r['version'] @@ -541,8 +607,7 @@ def getrawtransaction(self, txid, verbose=False): del r['vout'] r['blockhash'] = lx(r['blockhash']) if 'blockhash' in r else None else: - r = CTransaction.deserialize(unhexlify(r)) - + r = CTransaction.deserialize(unhexlify_str(r)) return r def getreceivedbyaddress(self, addr, minconf=1): @@ -589,7 +654,7 @@ def gettxout(self, outpoint, includemempool=True): raise IndexError('%s.gettxout(): unspent txout %r not found' % (self.__class__.__name__, outpoint)) r['txout'] = CTxOut(int(r['value'] * COIN), - CScript(unhexlify(r['scriptPubKey']['hex']))) + CScript(unhexlify_str(r['scriptPubKey']['hex']))) del r['value'] del r['scriptPubKey'] r['bestblock'] = lx(r['bestblock']) @@ -629,7 +694,7 @@ def listunspent(self, minconf=0, maxconf=9999999, addrs=None): unspent['address'] = CBitcoinAddress(unspent['address']) except KeyError: pass - unspent['scriptPubKey'] = CScript(unhexlify(unspent['scriptPubKey'])) + unspent['scriptPubKey'] = CScript(unhexlify_str(unspent['scriptPubKey'])) unspent['amount'] = int(unspent['amount'] * COIN) r2.append(unspent) return r2 @@ -645,7 +710,7 @@ def sendrawtransaction(self, tx, allowhighfees=False): allowhighfees - Allow even if fees are unreasonably high. """ - hextx = hexlify(tx.serialize()) + hextx = hexlify_str(tx.serialize()) r = None if allowhighfees: r = self._call('sendrawtransaction', hextx, True) @@ -675,9 +740,21 @@ def signrawtransaction(self, tx, *args): FIXME: implement options """ - hextx = hexlify(tx.serialize()) + hextx = hexlify_str(tx.serialize()) r = self._call('signrawtransaction', hextx, *args) - r['tx'] = CTransaction.deserialize(unhexlify(r['hex'])) + r['tx'] = CTransaction.deserialize(unhexlify_str(r['hex'])) + del r['hex'] + return r + + def signrawtransactionwithwallet(self, tx, *args): + """Sign inputs for transaction + bicoincore >= 0.17.x + + FIXME: implement options + """ + hextx = hexlify_str(tx.serialize()) + r = self._call('signrawtransactionwithwallet', hextx, *args) + r['tx'] = CTransaction.deserialize(unhexlify_str(r['hex'])) del r['hex'] return r @@ -687,7 +764,7 @@ def submitblock(self, block, params=None): params is optional and is currently ignored by bitcoind. See https://en.bitcoin.it/wiki/BIP_0022 for full specification. """ - hexblock = hexlify(block.serialize()) + hexblock = hexlify_str(block.serialize()) if params is not None: return self._call('submitblock', hexblock, params) else: @@ -699,7 +776,7 @@ def validateaddress(self, address): if r['isvalid']: r['address'] = CBitcoinAddress(r['address']) if 'pubkey' in r: - r['pubkey'] = unhexlify(r['pubkey']) + r['pubkey'] = unhexlify_str(r['pubkey']) return r def unlockwallet(self, password, timeout=60): @@ -713,6 +790,25 @@ def unlockwallet(self, password, timeout=60): r = self._call('walletpassphrase', password, timeout) return r + def createwallet(self, name): + """create a new wallet. + + name - The wallet name. + + """ + r = self._call('createwallet', name) + return r + + def loadwallet(self, name, load_on_startup=False): + """load a wallet. + + name - The wallet name. + load_on_startup - whether to remember to load it automatically next time bitcoind starts. + + """ + r = self._call('loadwallet', name, load_on_startup) + return r + def _addnode(self, node, arg): r = self._call('addnode', node, arg) return r diff --git a/bitcoin/segwit_addr.py b/bitcoin/segwit_addr.py new file mode 100644 index 00000000..c762fdd8 --- /dev/null +++ b/bitcoin/segwit_addr.py @@ -0,0 +1,122 @@ +# Copyright (c) 2017 Pieter Wuille +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Reference implementation for Bech32 and segwit addresses.""" + +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + + +def bech32_polymod(values): + """Internal function that computes the Bech32 checksum.""" + generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + chk = 1 + for value in values: + top = chk >> 25 + chk = (chk & 0x1ffffff) << 5 ^ value + for i in range(5): + chk ^= generator[i] if ((top >> i) & 1) else 0 + return chk + + +def bech32_hrp_expand(hrp): + """Expand the HRP into values for checksum computation.""" + return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] + + +def bech32_verify_checksum(hrp, data): + """Verify a checksum given HRP and converted data characters.""" + return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1 + + +def bech32_create_checksum(hrp, data): + """Compute the checksum values given HRP and data.""" + values = bech32_hrp_expand(hrp) + data + polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 + return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] + + +def bech32_encode(hrp, data): + """Compute a Bech32 string given HRP and data values.""" + combined = data + bech32_create_checksum(hrp, data) + return hrp + '1' + ''.join([CHARSET[d] for d in combined]) + + +def bech32_decode(bech): + """Validate a Bech32 string, and determine HRP and data.""" + if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or + (bech.lower() != bech and bech.upper() != bech)): + return (None, None) + bech = bech.lower() + pos = bech.rfind('1') + if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: + return (None, None) + if not all(x in CHARSET for x in bech[pos+1:]): + return (None, None) + hrp = bech[:pos] + data = [CHARSET.find(x) for x in bech[pos+1:]] + if not bech32_verify_checksum(hrp, data): + return (None, None) + return (hrp, data[:-6]) + + +def convertbits(data, frombits, tobits, pad=True): + """General power-of-2 base conversion.""" + acc = 0 + bits = 0 + ret = [] + maxv = (1 << tobits) - 1 + max_acc = (1 << (frombits + tobits - 1)) - 1 + for value in data: + if value < 0 or (value >> frombits): + return None + acc = ((acc << frombits) | value) & max_acc + bits += frombits + while bits >= tobits: + bits -= tobits + ret.append((acc >> bits) & maxv) + if pad: + if bits: + ret.append((acc << (tobits - bits)) & maxv) + elif bits >= frombits or ((acc << (tobits - bits)) & maxv): + return None + return ret + + +def decode(hrp, addr): + """Decode a segwit address.""" + hrpgot, data = bech32_decode(addr) + if hrpgot != hrp: + return (None, None) + decoded = convertbits(data[1:], 5, 8, False) + if decoded is None or len(decoded) < 2 or len(decoded) > 40: + return (None, None) + if data[0] > 16: + return (None, None) + if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: + return (None, None) + return (data[0], decoded) + + +def encode(hrp, witver, witprog): + """Encode a segwit address.""" + ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) + if decode(hrp, ret) == (None, None): + return None + return ret diff --git a/bitcoin/signature.py b/bitcoin/signature.py index f339491e..89434b7a 100644 --- a/bitcoin/signature.py +++ b/bitcoin/signature.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2014 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,17 +9,13 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals from bitcoin.core.serialize import * # Py3 compatibility import sys -if sys.version > '3': - from io import BytesIO as _BytesIO -else: - from cStringIO import StringIO as _BytesIO +from io import BytesIO class DERSignature(ImmutableSerializable): @@ -34,7 +30,7 @@ def __init__(self, r, s, length): def stream_deserialize(cls, f): assert ser_read(f, 1) == b"\x30" rs = BytesSerializer.stream_deserialize(f) - f = _BytesIO(rs) + f = BytesIO(rs) assert ser_read(f, 1) == b"\x02" r = BytesSerializer.stream_deserialize(f) assert ser_read(f, 1) == b"\x02" diff --git a/bitcoin/signmessage.py b/bitcoin/signmessage.py index a62fe8c1..d893f07a 100644 --- a/bitcoin/signmessage.py +++ b/bitcoin/signmessage.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2015 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,21 +9,12 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals from bitcoin.core.key import CPubKey from bitcoin.core.serialize import ImmutableSerializable from bitcoin.wallet import P2PKHBitcoinAddress import bitcoin import base64 -import sys - -_bchr = chr -_bord = ord -if sys.version > '3': - long = int - _bchr = lambda x: bytes([x]) - _bord = lambda x: x def VerifyMessage(address, message, sig): @@ -42,7 +33,7 @@ def SignMessage(key, message): if key.is_compressed: meta += 4 - return base64.b64encode(_bchr(meta) + sig) + return base64.b64encode(bytes([meta]) + sig) class BitcoinMessage(ImmutableSerializable): @@ -66,4 +57,4 @@ def __str__(self): return self.message.decode('ascii') def __repr__(self): - return 'BitcoinMessage(%s, %s)' % (self.magic, self.message) \ No newline at end of file + return 'BitcoinMessage(%s, %s)' % (self.magic, self.message) diff --git a/bitcoin/tests/__init__.py b/bitcoin/tests/__init__.py index 1d9a3755..0ce1c371 100644 --- a/bitcoin/tests/__init__.py +++ b/bitcoin/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2014 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,4 +9,3 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals diff --git a/bitcoin/tests/data/bech32_encode_decode.json b/bitcoin/tests/data/bech32_encode_decode.json new file mode 100644 index 00000000..d8b2b0eb --- /dev/null +++ b/bitcoin/tests/data/bech32_encode_decode.json @@ -0,0 +1,8 @@ +[ +["0014751e76e8199196d454941c45d1b3a323f1433bd6", "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4"], +["00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7"], +["5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6", "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx"], +["6002751e", "BC1SW50QA3JX3S"], +["5210751e76e8199196d454941c45d1b3a323", "bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj"], +["0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy"] +] diff --git a/bitcoin/tests/data/bech32_invalid.json b/bitcoin/tests/data/bech32_invalid.json new file mode 100644 index 00000000..aeb898b0 --- /dev/null +++ b/bitcoin/tests/data/bech32_invalid.json @@ -0,0 +1,12 @@ +[ + ["tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", "Invalid human-readable part"], + ["bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", "Invalid checksum"], + ["BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", "Invalid witness version"], + ["bc1rw5uspcuh", "Invalid program length"], + ["bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", "Invalid program length"], + ["BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", "Invalid program length for witness version 0 (per BIP141)"], + ["tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", "Mixed case"], + ["bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", "zero padding of more than 4 bits"], + ["tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", "Non-zero padding in 8-to-5 conversion"], + ["bc1gmk9yu", "Empty data section"] +] \ No newline at end of file diff --git a/bitcoin/tests/data/script_invalid.json b/bitcoin/tests/data/script_invalid.json index 43513388..1a68f44c 100644 --- a/bitcoin/tests/data/script_invalid.json +++ b/bitcoin/tests/data/script_invalid.json @@ -455,18 +455,6 @@ "P2SH", "P2SH(2-of-3), 1 sig" ], -[ - "0x47 0x30440220005ece1335e7f657a1a1f476a7fb5bd90964e8a022489f890614a04acfb734c002206c12b8294a6513c7710e8c82d3c23d75cdbfe83200eb7efb495701958501a5d601", - "0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 CHECKSIG NOT", - "", - "P2PK NOT with too much R padding but no DERSIG" -], -[ - "0x47 0x304402208e43c0b91f7c1e5bc58e41c8185f8a6086e111b0090187968a86f2822462d3c902200a58f4076b1133b18ff1dc83ee51676e44c60cc608d9534e0df5ace0424fc0be01", - "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG NOT", - "", - "BIP66 example 2, without DERSIG" -], [ "0", "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", @@ -479,12 +467,6 @@ "", "BIP66 example 5, without DERSIG" ], -[ - "0 0x47 0x30440220b119d67d389315308d1745f734a51ff3ec72e06081e84e236fdf9dc2f5d2a64802204b04e3bc38674c4422ea317231d642b56dc09d214a1ecbbf16ecca01ed996e2201 0x47 0x3044022079ea80afd538d9ada421b5101febeb6bc874e01dde5bca108c1d0479aec339a4022004576db8f66130d1df686ccf00935703689d69cf539438da1edab208b0d63c4801", - "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 2 CHECKMULTISIG NOT", - "", - "BIP66 example 8, without DERSIG" -], [ "0 0 0x47 0x3044022081aa9d436f2154e8b6d600516db03d78de71df685b585a9807ead4210bd883490220534bb6bdf318a419ac0749660b60e78d17d515558ef369bf872eff405b676b2e01", "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 2 CHECKMULTISIG", diff --git a/bitcoin/tests/data/script_valid.json b/bitcoin/tests/data/script_valid.json index e5f0d17b..33ef138e 100644 --- a/bitcoin/tests/data/script_valid.json +++ b/bitcoin/tests/data/script_valid.json @@ -744,36 +744,12 @@ "P2SH", "P2SH(2-of-3)" ], -[ - "0x47 0x304402200060558477337b9022e70534f1fea71a318caf836812465a2509931c5e7c4987022078ec32bd50ac9e03a349ba953dfd9fe1c8d2dd8bdb1d38ddca844d3d5c78c11801", - "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", - "", - "P2PK with too much R padding but no DERSIG" -], -[ - "0x48 0x304502202de8c03fc525285c9c535631019a5f2af7c6454fa9eb392a3756a4917c420edd02210046130bf2baf7cfc065067c8b9e33a066d9c15edcea9feb0ca2d233e3597925b401", - "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", - "", - "P2PK with too much S padding but no DERSIG" -], -[ - "0x47 0x30440220d7a0417c3f6d1a15094d1cf2a3378ca0503eb8a57630953a9e2987e21ddd0a6502207a6266d686c99090920249991d3d42065b6d43eb70187b219c0db82e4f94d1a201", - "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", - "", - "P2PK with too little R padding but no DERSIG" -], [ "0x47 0x30440220005ece1335e7f757a1a1f476a7fb5bd90964e8a022489f890614a04acfb734c002206c12b8294a6513c7710e8c82d3c23d75cdbfe83200eb7efb495701958501a5d601", "0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 CHECKSIG NOT", "", "P2PK NOT with bad sig with too much R padding but no DERSIG" ], -[ - "0x47 0x30440220d7a0417c3f6d1a15094d1cf2a3378ca0503eb8a57630953a9e2987e21ddd0a6502207a6266d686c99090920249991d3d42065b6d43eb70187b219c0db82e4f94d1a201", - "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", - "", - "BIP66 example 1, without DERSIG" -], [ "0", "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG NOT", @@ -792,12 +768,6 @@ "", "BIP66 example 6, without DERSIG" ], -[ - "0 0x47 0x30440220cae00b1444babfbf6071b0ba8707f6bd373da3df494d6e74119b0430c5db810502205d5231b8c5939c8ff0c82242656d6e06edb073d42af336c99fe8837c36ea39d501 0x47 0x3044022027c2714269ca5aeecc4d70edc88ba5ee0e3da4986e9216028f489ab4f1b8efce022022bd545b4951215267e4c5ceabd4c5350331b2e4a0b6494c56f361fa5a57a1a201", - "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 2 CHECKMULTISIG", - "", - "BIP66 example 7, without DERSIG" -], [ "0 0 0x47 0x30440220da6f441dc3b4b2c84cfa8db0cd5b34ed92c9e01686de5a800d40498b70c0dcac02207c2cf91b0c32b860c4cd4994be36cfb84caf8bb7c3a8e4d96a31b2022c5299c501", "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 2 CHECKMULTISIG NOT", diff --git a/bitcoin/tests/data/tx_valid.json b/bitcoin/tests/data/tx_valid.json index d2a4c9df..ef70a542 100644 --- a/bitcoin/tests/data/tx_valid.json +++ b/bitcoin/tests/data/tx_valid.json @@ -5,18 +5,6 @@ ["serializedTransaction, enforceP2SH]"], ["Objects that are only a single string (like this one) are ignored"], -["The following is 23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63"], -["It is of particular interest because it contains an invalidly-encoded signature which OpenSSL accepts"], -["See http://r6.ca/blog/20111119T211504Z.html"], -["It is also the first OP_CHECKMULTISIG transaction in standard form"], -[[["60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1", 0, "1 0x41 0x04cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4 0x41 0x0461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af 2 OP_CHECKMULTISIG"]], -"0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000490047304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000", true], - -["The following is a tweaked form of 23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63"], -["It has an arbitrary extra byte stuffed into the signature at pos length - 2"], -[[["60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1", 0, "1 0x41 0x04cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4 0x41 0x0461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af 2 OP_CHECKMULTISIG"]], -"0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004A0048304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2bab01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000", true], - ["The following is c99c49da4c38af669dea436d3e73780dfdb6c1ecf9958baa52960e8baee30e73"], ["It is of interest because it contains a 0-sequence as well as a signature of SIGHASH type 0 (which is not a real type)"], [[["406b2b06bcd34d3c8733e6b79f7a394c8a431fbf4ff5ac705c93f4076bb77602", 0, "DUP HASH160 0x14 0xdc44b1164188067c3a32d4780f5996fa14a4f2d9 EQUALVERIFY CHECKSIG"]], @@ -30,12 +18,6 @@ [[["0000000000000000000000000000000000000000000000000000000000000100", 0, "DUP HASH160 0x14 0x5b6462475454710f3c22f5fdf0b40704c92f25c3 EQUALVERIFY CHECKSIGVERIFY 1 0x47 0x3044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a01"]], "01000000010001000000000000000000000000000000000000000000000000000000000000000000006a473044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a012103ba8c8b86dea131c22ab967e6dd99bdae8eff7a1f75a2c35f1f944109e3fe5e22ffffffff010000000000000000015100000000", true], -["The following is f7fdd091fa6d8f5e7a8c2458f5c38faffff2d3f1406b6e4fe2c99dcc0d2d1cbb"], -["It caught a bug in the workaround for 23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63 in an overly simple implementation"], -[[["b464e85df2a238416f8bdae11d120add610380ea07f4ef19c5f9dfd472f96c3d", 0, "DUP HASH160 0x14 0xbef80ecf3a44500fda1bc92176e442891662aed2 EQUALVERIFY CHECKSIG"], -["b7978cc96e59a8b13e0865d3f95657561a7f725be952438637475920bac9eb21", 1, "DUP HASH160 0x14 0xbef80ecf3a44500fda1bc92176e442891662aed2 EQUALVERIFY CHECKSIG"]], -"01000000023d6cf972d4dff9c519eff407ea800361dd0a121de1da8b6f4138a2f25de864b4000000008a4730440220ffda47bfc776bcd269da4832626ac332adfca6dd835e8ecd83cd1ebe7d709b0e022049cffa1cdc102a0b56e0e04913606c70af702a1149dc3b305ab9439288fee090014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff21ebc9ba20594737864352e95b727f1a565756f9d365083eb1a8596ec98c97b7010000008a4730440220503ff10e9f1e0de731407a4a245531c9ff17676eda461f8ceeb8c06049fa2c810220c008ac34694510298fa60b3f000df01caa244f165b727d4896eb84f81e46bcc4014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff01f0da5200000000001976a914857ccd42dded6df32949d4646dfa10a92458cfaa88ac00000000", true], - ["The following tests for the presence of a bug in the handling of SIGHASH_SINGLE"], ["It results in signing the constant 1, instead of something generated based on the transaction,"], ["when the input doing the signing has an index greater than the maximum output index"], diff --git a/bitcoin/tests/fakebitcoinproxy.py b/bitcoin/tests/fakebitcoinproxy.py new file mode 100644 index 00000000..effabf4a --- /dev/null +++ b/bitcoin/tests/fakebitcoinproxy.py @@ -0,0 +1,328 @@ +""" +`FakeBitcoinProxy` allows for unit testing of code that normally uses bitcoin +RPC without requiring a running bitcoin node. + +`FakeBitcoinProxy` has an interface similar to `bitcoin.rpc.Proxy`, but does not +connect to a local bitcoin RPC node. Hence, `FakeBitcoinProxy` is similar to a +mock for the RPC tests. + +`FakeBitcoinProxy` does _not_ implement a full bitcoin RPC node. Instead, it +currently implements only a subset of the available RPC commands. Test setup is +responsible for populating a `FakeBitcoinProxy` object with reasonable mock +data. + +:author: Bryan Bishop +""" + +import random +import hashlib + +from bitcoin.core import ( + # bytes to hex (see x) + b2x, + + # convert hex string to bytes (see b2x) + x, + + # convert little-endian hex string to bytes (see b2lx) + lx, + + # convert bytes to little-endian hex string (see lx) + b2lx, + + # number of satoshis per bitcoin + COIN, + + # a type for a transaction that isn't finished building + CMutableTransaction, + CMutableTxIn, + CMutableTxOut, + COutPoint, + CTxIn, +) + +from bitcoin.wallet import ( + # bitcoin address initialized from base58-encoded string + CBitcoinAddress, + + # base58-encoded secret key + CBitcoinSecret, + + # has a nifty function from_pubkey + P2PKHBitcoinAddress, +) + +def make_address_from_passphrase(passphrase, compressed=True, as_str=True): + """ + Create a Bitcoin address from a passphrase. The passphrase is hashed and + then used as the secret bytes to construct the CBitcoinSecret. + """ + if not isinstance(passphrase, bytes): + passphrase = bytes(passphrase, "utf-8") + passphrasehash = hashlib.sha256(passphrase).digest() + private_key = CBitcoinSecret.from_secret_bytes(passphrasehash, compressed=compressed) + address = P2PKHBitcoinAddress.from_pubkey(private_key.pub) + if as_str: + return str(address) + else: + return address + +def make_txout(amount=None, address=None, counter=None): + """ + Make a CTxOut object based on the parameters. Otherwise randomly generate a + CTxOut to represent a transaction output. + + :param amount: amount in satoshis + """ + passphrase_template = "correct horse battery staple txout {counter}" + + if not counter: + counter = random.randrange(0, 2**50) + + if not address: + passphrase = passphrase_template.format(counter=counter) + address = make_address_from_passphrase(bytes(passphrase, "utf-8")) + + if not amount: + maxsatoshis = (21 * 1000 * 1000) * (100 * 1000 * 1000) # 21 million BTC * 100 million satoshi per BTC + amount = random.randrange(0, maxsatoshis) # between 0 satoshi and 21 million BTC + + txout = CMutableTxOut(amount, CBitcoinAddress(address).to_scriptPubKey()) + + return txout + +def make_blocks_from_blockhashes(blockhashes): + """ + Create some block data suitable for FakeBitcoinProxy to consume during + instantiation. + """ + blocks = [] + previousblockhash = None + + for (height, blockhash) in enumerate(blockhashes): + block = {"hash": blockhash, "height": height, "tx": []} + if height != 0: + block["previousblockhash"] = previousblockhash + blocks.append(block) + previousblockhash = blockhash + + return blocks + +def make_rpc_batch_request_entry(rpc_name, params): + """ + Construct an entry for the list of commands that will be passed as a batch + (for `_batch`). + """ + return { + "id": "50", + "version": "1.1", + "method": rpc_name, + "params": params, + } + +class FakeBitcoinProxyException(Exception): + """ + Incorrect usage of fake proxy. + """ + pass + +class FakeBitcoinProxy(object): + """ + This is an alternative to using `bitcoin.rpc.Proxy` in tests. This class + can store a number of blocks and transactions, which can then be retrieved + by calling various "RPC" methods. + """ + + def __init__(self, blocks=None, transactions=None, getnewaddress_offset=None, getnewaddress_passphrase_template="getnewaddress passphrase template {}", num_fundrawtransaction_inputs=5): + """ + :param getnewaddress_offset: a number to start using and incrementing + in template used by getnewaddress. + :type getnewaddress_offset: int + :param int num_fundrawtransaction_inputs: number of inputs to create + during fundrawtransaction. + """ + self.blocks = blocks or {} + self.transactions = transactions or {} + + if getnewaddress_offset == None: + self._getnewaddress_offset = 0 + else: + self._getnewaddress_offset = getnewaddress_offset + + self._getnewaddress_passphrase_template = getnewaddress_passphrase_template + self._num_fundrawtransaction_inputs = num_fundrawtransaction_inputs + self.populate_blocks_with_blockheights() + + def _call(self, rpc_method_name, *args, **kwargs): + """ + This represents a "raw" RPC call, which has output that + python-bitcoinlib does not parse. + """ + method = getattr(self, rpc_method_name) + return method(*args, **kwargs) + + def populate_blocks_with_blockheights(self): + """ + Helper method to correctly apply "height" on all blocks. + """ + for (height, block) in enumerate(self.blocks): + block["height"] = height + + def getblock(self, blockhash, *args, **kwargs): + """ + :param blockhash: hash of the block to retrieve data for + :raise IndexError: invalid blockhash + """ + + # Note that the actual "getblock" bitcoind RPC call from + # python-bitcoinlib returns a CBlock object, not a dictionary. + + if isinstance(blockhash, bytes): + blockhash = b2lx(blockhash) + + for block in self.blocks: + if block["hash"] == blockhash: + return block + + raise IndexError("no block found for blockhash {}".format(blockhash)) + + def getblockhash(self, blockheight): + """ + Get block by blockheight. + + :type blockheight: int + :rtype: dict + """ + for block in self.blocks: + if block["height"] == int(blockheight): + return block["hash"] + + def getblockcount(self): + """ + Return the total number of blocks. When there is only one block in the + blockchain, this function will return zero. + + :rtype: int + """ + return len(self.blocks) - 1 + + def getrawtransaction(self, txid, *args, **kwargs): + """ + Get parsed transaction. + + :type txid: bytes or str + :rtype: dict + """ + if isinstance(txid, bytes): + txid = b2lx(txid) + return self.transactions[txid] + + def getnewaddress(self): + """ + Construct a new address based on a passphrase template. As more + addresses are generated, the template value goes up. + """ + passphrase = self._getnewaddress_passphrase_template.format(self._getnewaddress_offset) + address = make_address_from_passphrase(bytes(passphrase, "utf-8")) + self._getnewaddress_offset += 1 + return CBitcoinAddress(address) + + def importaddress(self, *args, **kwargs): + """ + Completely unrealistic fake version of importaddress. + """ + return True + + # This was implemented a long time ago and it's possible that this does not + # match the current behavior of fundrawtransaction. + def fundrawtransaction(self, given_transaction, *args, **kwargs): + """ + Make up some inputs for the given transaction. + """ + # just use any txid here + vintxid = lx("99264749804159db1e342a0c8aa3279f6ef4031872051a1e52fb302e51061bef") + + if isinstance(given_transaction, str): + given_bytes = x(given_transaction) + elif isinstance(given_transaction, CMutableTransaction): + given_bytes = given_transaction.serialize() + else: + raise FakeBitcoinProxyException("Wrong type passed to fundrawtransaction.") + + # this is also a clever way to not cause a side-effect in this function + transaction = CMutableTransaction.deserialize(given_bytes) + + for vout_counter in range(0, self._num_fundrawtransaction_inputs): + txin = CMutableTxIn(COutPoint(vintxid, vout_counter)) + transaction.vin.append(txin) + + # also allocate a single output (for change) + txout = make_txout() + transaction.vout.append(txout) + + transaction_hex = b2x(transaction.serialize()) + + return {"hex": transaction_hex, "fee": 5000000} + + def signrawtransaction(self, given_transaction): + """ + This method does not actually sign the transaction, but it does return + a transaction based on the given transaction. + """ + if isinstance(given_transaction, str): + given_bytes = x(given_transaction) + elif isinstance(given_transaction, CMutableTransaction): + given_bytes = given_transaction.serialize() + else: + raise FakeBitcoinProxyException("Wrong type passed to signrawtransaction.") + + transaction = CMutableTransaction.deserialize(given_bytes) + transaction_hex = b2x(transaction.serialize()) + return {"hex": transaction_hex} + + def sendrawtransaction(self, given_transaction): + """ + Pretend to broadcast and relay the transaction. Return the txid of the + given transaction. + """ + if isinstance(given_transaction, str): + given_bytes = x(given_transaction) + elif isinstance(given_transaction, CMutableTransaction): + given_bytes = given_transaction.serialize() + else: + raise FakeBitcoinProxyException("Wrong type passed to sendrawtransaction.") + transaction = CMutableTransaction.deserialize(given_bytes) + return b2lx(transaction.GetHash()) + + def _batch(self, batch_request_entries): + """ + Process a bunch of requests all at once. This mimics the _batch RPC + feature found in python-bitcoinlib and bitcoind RPC. + """ + necessary_keys = ["id", "version", "method", "params"] + + results = [] + + for (idx, request) in enumerate(batch_request_entries): + error = None + result = None + + # assert presence of important details + for necessary_key in necessary_keys: + if not necessary_key in request.keys(): + raise FakeBitcoinProxyException("Missing necessary key {} for _batch request number {}".format(necessary_key, idx)) + + if isinstance(request["params"], list): + method = getattr(self, request["method"]) + result = method(*request["params"]) + else: + # matches error message received through python-bitcoinrpc + error = {"message": "Params must be an array", "code": -32600} + + results.append({ + "error": error, + "id": request["id"], + "result": result, + }) + + return results diff --git a/bitcoin/tests/test_base58.py b/bitcoin/tests/test_base58.py index a57d1fe1..59091034 100644 --- a/bitcoin/tests/test_base58.py +++ b/bitcoin/tests/test_base58.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2014 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,7 +9,6 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals import json import os diff --git a/bitcoin/tests/test_bech32.py b/bitcoin/tests/test_bech32.py new file mode 100644 index 00000000..06da1244 --- /dev/null +++ b/bitcoin/tests/test_bech32.py @@ -0,0 +1,65 @@ +# Copyright (C) The python-bitcoinlib developers +# +# This file is part of python-bitcoinlib. +# +# It is subject to the license terms in the LICENSE file found in the top-level +# directory of this distribution. +# +# No part of python-bitcoinlib, including this file, may be copied, modified, +# propagated, or distributed except according to the terms contained in the +# LICENSE file. + + +import json +import os +import unittest +from binascii import unhexlify + +from bitcoin.core.script import CScript, OP_0, OP_1, OP_16 +from bitcoin.bech32 import * +from bitcoin.segwit_addr import encode, decode + + +def load_test_vectors(name): + with open(os.path.dirname(__file__) + '/data/' + name, 'r') as fd: + for testcase in json.load(fd): + yield testcase + +def to_scriptPubKey(witver, witprog): + """Decoded bech32 address to script""" + return CScript([witver]) + CScript(bytes(witprog)) + +class Test_bech32(unittest.TestCase): + + def op_decode(self, witver): + """OP encoding to int""" + if witver == OP_0: + return 0 + if OP_1 <= witver <= OP_16: + return witver - OP_1 + 1 + self.fail('Wrong witver: %d' % witver) + + def test_encode_decode(self): + for exp_bin, exp_bech32 in load_test_vectors('bech32_encode_decode.json'): + exp_bin = unhexlify(exp_bin.encode('utf8')) + witver = self.op_decode(exp_bin[0]) + hrp = exp_bech32[:exp_bech32.rindex('1')].lower() + self.assertEqual(exp_bin[1], len(exp_bin[2:])) + act_bech32 = encode(hrp, witver, exp_bin[2:]) + act_bin = decode(hrp, exp_bech32) + + self.assertEqual(act_bech32.lower(), exp_bech32.lower()) + self.assertEqual(to_scriptPubKey(*act_bin), bytes(exp_bin)) + +class Test_CBech32Data(unittest.TestCase): + def test_from_data(self): + b = CBech32Data.from_bytes(0, unhexlify('751e76e8199196d454941c45d1b3a323f1433bd6')) + self.assertEqual(b.witver, 0) + self.assertEqual(str(b).upper(), 'BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4') + + def test_invalid_bech32_exception(self): + + for invalid, _ in load_test_vectors("bech32_invalid.json"): + msg = '%r should have raised Bech32Error but did not' % invalid + with self.assertRaises(Bech32Error, msg=msg): + CBech32Data(invalid) diff --git a/bitcoin/tests/test_bloom.py b/bitcoin/tests/test_bloom.py index c0d7a019..436a97ea 100644 --- a/bitcoin/tests/test_bloom.py +++ b/bitcoin/tests/test_bloom.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2014 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,7 +9,6 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals import unittest diff --git a/bitcoin/tests/test_checkblock.py b/bitcoin/tests/test_checkblock.py index a011fe26..10c1c9a9 100644 --- a/bitcoin/tests/test_checkblock.py +++ b/bitcoin/tests/test_checkblock.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2014 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,7 +9,6 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals import json import unittest diff --git a/bitcoin/tests/test_core.py b/bitcoin/tests/test_core.py index 1d612afd..ff16ca56 100644 --- a/bitcoin/tests/test_core.py +++ b/bitcoin/tests/test_core.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2015 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,7 +9,6 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals import unittest diff --git a/bitcoin/tests/test_fakebitcoinproxy.py b/bitcoin/tests/test_fakebitcoinproxy.py new file mode 100644 index 00000000..0d8114f7 --- /dev/null +++ b/bitcoin/tests/test_fakebitcoinproxy.py @@ -0,0 +1,420 @@ +""" +:author: Bryan Bishop +""" + +import unittest +import random +from copy import copy + +from bitcoin.core import ( + # bytes to hex (see x) + b2x, + + # convert hex string to bytes (see b2x) + x, + + # convert little-endian hex string to bytes (see b2lx) + lx, + + # convert bytes to little-endian hex string (see lx) + b2lx, + + # number of satoshis per bitcoin + COIN, + + # a type for a transaction that isn't finished building + CMutableTransaction, + CMutableTxIn, + CMutableTxOut, + COutPoint, + CTxIn, +) + +from bitcoin.wallet import ( + # has a nifty function from_pubkey + P2PKHBitcoinAddress, +) + +from bitcoin.tests.fakebitcoinproxy import ( + FakeBitcoinProxy, + make_txout, + make_blocks_from_blockhashes, + make_rpc_batch_request_entry, + + # TODO: import and test with FakeBitcoinProxyException +) + +class FakeBitcoinProxyTestCase(unittest.TestCase): + def test_constructor(self): + FakeBitcoinProxy() + + def test_constructor_accepts_blocks(self): + blockhash0 = "blockhash0" + blockhash1 = "blockhash1" + + blocks = [ + {"hash": blockhash0}, + {"hash": blockhash1}, + ] + + proxy = FakeBitcoinProxy(blocks=blocks) + + self.assertNotEqual(proxy.blocks, {}) + self.assertTrue(proxy.blocks is blocks) + + def test_getblock_with_string(self): + blockhash0 = "blockhash0" + blockhash1 = "blockhash1" + + blocks = [ + {"hash": blockhash0}, + {"hash": blockhash1}, + ] + + proxy = FakeBitcoinProxy(blocks=blocks) + result = proxy.getblock(blockhash0) + + self.assertEqual(type(result), dict) + + # should have a height now + self.assertTrue("height" in result.keys()) + self.assertEqual(result["height"], 0) + + def test_blockheight_extensively(self): + blockhashes = ["blockhash{}".format(x) for x in range(0, 10)] + + blocks = [] + for blockhash in blockhashes: + blocks.append({"hash": blockhash}) + + proxy = FakeBitcoinProxy(blocks=blocks) + + for (expected_height, blockhash) in enumerate(blockhashes): + blockdata = proxy.getblock(blockhash) + self.assertEqual(blockdata["height"], expected_height) + + def test_getblock_with_bytes(self): + blockhash0 = "00008c0c84aee66413f1e8ff95fdca5e8ebf35c94b090290077cdcea64936301" + + blocks = [ + {"hash": blockhash0}, + ] + + proxy = FakeBitcoinProxy(blocks=blocks) + result = proxy.getblock(lx(blockhash0)) + + self.assertEqual(type(result), dict) + + def test_getblock_returns_transaction_data(self): + blockhash0 = "00008c0c84aee66413f1e8ff95fdca5e8ebf35c94b090290077cdcea64936302" + + transaction_txids = ["foo", "bar"] + + blocks = [ + {"hash": blockhash0, "tx": transaction_txids}, + ] + + proxy = FakeBitcoinProxy(blocks=blocks) + result = proxy.getblock(blockhash0) + + self.assertTrue("tx" in result.keys()) + self.assertEqual(type(result["tx"]), list) + self.assertEqual(result["tx"], transaction_txids) + + def test_getblockhash_zero(self): + blockhash0 = "00008c0c84aee66413f1e8ff95fdca5e8ebf35c94b090290077cdcea64936302" + + blocks = [ + {"hash": blockhash0}, + ] + + proxy = FakeBitcoinProxy(blocks=blocks) + + blockhash_result = proxy.getblockhash(0) + + self.assertEqual(blockhash_result, blockhash0) + + def test_getblockhash_many(self): + blockhashes = [ + "00008c0c84aee66413f1e8ff95fdca5e8ebf35c94b090290077cdcea64936302", + "00008c0c84aee66413f1e8ff95fdca5e8ebf35c94b090290077cdcea64936303", + "00008c0c84aee66413f1e8ff95fdca5e8ebf35c94b090290077cdcea64936304", + "00008c0c84aee66413f1e8ff95fdca5e8ebf35c94b090290077cdcea64936305", + ] + + blocks = make_blocks_from_blockhashes(blockhashes) + proxy = FakeBitcoinProxy(blocks=blocks) + + for (height, expected_blockhash) in enumerate(blockhashes): + blockhash_result = proxy.getblockhash(height) + self.assertEqual(blockhash_result, expected_blockhash) + + def test_getblockcount_zero(self): + blocks = [] + proxy = FakeBitcoinProxy(blocks=blocks) + count = proxy.getblockcount() + self.assertEqual(count, len(blocks) - 1) + + def test_getblockcount_many(self): + blockhash0 = "00008c0c84aee66413f1e8ff95fdca5e8ebf35c94b090290077cdcea64936301" + blockhash1 = "0000b9fc70e9a8bb863a8c37f0f3d10d4d3e99e0e94bd42b35f0fdbb497399eb" + + blocks = [ + {"hash": blockhash0}, + {"hash": blockhash1, "previousblockhash": blockhash0}, + ] + + proxy = FakeBitcoinProxy(blocks=blocks) + count = proxy.getblockcount() + self.assertEqual(count, len(blocks) - 1) + + def test_getrawtransaction(self): + txids = [ + "b9e3b3366b31136bd262c64ff6e4c29918a457840c5f53cbeeeefa41ca3b7005", + "21886e5c02049751acea9d462359d68910bef938d9a58288c961d18b6e50daef", + "b2d0cc6840be86516ce79ed7249bfd09f95923cc27784c2fa0dd910bfdb03173", + "eacb81d11a539047af73b5d810d3683af3b1171e683dcbfcfb2819286d762c0c", + "eee72e928403fea7bca29f366483c3e0ab629ac5dce384a0f541aacf6f810d30", + ] + + txdefault = {"tx": "", "confirmations": 0} + + transactions = dict([(txid, copy(txdefault)) for txid in txids]) + + # just making sure... + self.assertTrue(transactions[txids[0]] is not transactions[txids[1]]) + + proxy = FakeBitcoinProxy(transactions=transactions) + + for txid in txids: + txdata = proxy.getrawtransaction(txid) + + self.assertEqual(type(txdata), dict) + self.assertTrue("tx" in txdata.keys()) + self.assertEqual(type(txdata["tx"]), str) + self.assertEqual(txdata["tx"], "") + self.assertTrue("confirmations" in txdata.keys()) + self.assertEqual(txdata["confirmations"], 0) + + def test_getnewaddress_returns_different_addresses(self): + num_addresses = random.randint(10, 100) + addresses = set() + + proxy = FakeBitcoinProxy() + + for each in range(num_addresses): + address = proxy.getnewaddress() + addresses.add(str(address)) + + self.assertEqual(len(addresses), num_addresses) + + def test_getnewaddress_returns_cbitcoinaddress(self): + proxy = FakeBitcoinProxy() + address = proxy.getnewaddress() + self.assertEqual(type(address), P2PKHBitcoinAddress) + + def test_importaddress(self): + proxy = FakeBitcoinProxy() + proxy.importaddress("foo") + + def test_importaddress_with_parameters(self): + proxy = FakeBitcoinProxy() + address = "some_address" + label = "" + rescan = False + proxy.importaddress(address, label, rescan) + + def test_fundrawtransaction(self): + unfunded_transaction = CMutableTransaction([], [make_txout() for x in range(0, 5)]) + proxy = FakeBitcoinProxy() + + funded_transaction_hex = proxy.fundrawtransaction(unfunded_transaction)["hex"] + funded_transaction = CMutableTransaction.deserialize(x(funded_transaction_hex)) + + self.assertTrue(unfunded_transaction is not funded_transaction) + self.assertEqual(type(funded_transaction), type(unfunded_transaction)) + self.assertNotEqual(len(funded_transaction.vin), 0) + self.assertTrue(len(funded_transaction.vin) > len(unfunded_transaction.vin)) + self.assertEqual(type(funded_transaction.vin[0]), CTxIn) + + def test_fundrawtransaction_hex_hash(self): + unfunded_transaction = CMutableTransaction([], [make_txout() for x in range(0, 5)]) + proxy = FakeBitcoinProxy() + + funded_transaction_hex = proxy.fundrawtransaction(b2x(unfunded_transaction.serialize()))["hex"] + funded_transaction = CMutableTransaction.deserialize(x(funded_transaction_hex)) + + self.assertTrue(unfunded_transaction is not funded_transaction) + self.assertEqual(type(funded_transaction), type(unfunded_transaction)) + self.assertNotEqual(len(funded_transaction.vin), 0) + self.assertTrue(len(funded_transaction.vin) > len(unfunded_transaction.vin)) + self.assertEqual(type(funded_transaction.vin[0]), CTxIn) + + def test_fundrawtransaction_adds_output(self): + num_outputs = 5 + unfunded_transaction = CMutableTransaction([], [make_txout() for x in range(0, num_outputs)]) + proxy = FakeBitcoinProxy() + + funded_transaction_hex = proxy.fundrawtransaction(b2x(unfunded_transaction.serialize()))["hex"] + funded_transaction = CMutableTransaction.deserialize(x(funded_transaction_hex)) + + self.assertTrue(len(funded_transaction.vout) > num_outputs) + self.assertEqual(len(funded_transaction.vout), num_outputs + 1) + + def test_signrawtransaction(self): + num_outputs = 5 + given_transaction = CMutableTransaction([], [make_txout() for x in range(0, num_outputs)]) + proxy = FakeBitcoinProxy() + + result = proxy.signrawtransaction(given_transaction) + + self.assertEqual(type(result), dict) + self.assertTrue("hex" in result.keys()) + + result_transaction_hex = result["hex"] + result_transaction = CMutableTransaction.deserialize(x(result_transaction_hex)) + + self.assertTrue(result_transaction is not given_transaction) + self.assertTrue(result_transaction.vin is not given_transaction.vin) + self.assertTrue(result_transaction.vout is not given_transaction.vout) + self.assertEqual(len(result_transaction.vout), len(given_transaction.vout)) + self.assertEqual(result_transaction.vout[0].scriptPubKey, given_transaction.vout[0].scriptPubKey) + + def test_sendrawtransaction(self): + num_outputs = 5 + given_transaction = CMutableTransaction([], [make_txout() for x in range(0, num_outputs)]) + expected_txid = b2lx(given_transaction.GetHash()) + given_transaction_hex = b2x(given_transaction.serialize()) + proxy = FakeBitcoinProxy() + resulting_txid = proxy.sendrawtransaction(given_transaction_hex) + self.assertEqual(resulting_txid, expected_txid) + + def test__batch_empty_list_input(self): + requests = [] + self.assertEqual(len(requests), 0) # must be empty for test + proxy = FakeBitcoinProxy() + results = proxy._batch(requests) + self.assertEqual(type(results), list) + self.assertEqual(len(results), 0) + + def test__batch_raises_when_no_params(self): + proxy = FakeBitcoinProxy() + with self.assertRaises(TypeError): + proxy._batch() + + def test__batch_same_count_results_as_requests(self): + proxy = FakeBitcoinProxy() + request = make_rpc_batch_request_entry("getblockcount", []) + requests = [request] + results = proxy._batch(requests) + self.assertEqual(len(requests), len(results)) + + def test__batch_gives_reasonable_getblockcount_result(self): + proxy = FakeBitcoinProxy() + request = make_rpc_batch_request_entry("getblockcount", []) + requests = [request] + results = proxy._batch(requests) + self.assertEqual(results[0]["result"], -1) + + def test__batch_result_keys(self): + expected_keys = ["error", "id", "result"] + proxy = FakeBitcoinProxy() + request = make_rpc_batch_request_entry("getblockcount", []) + requests = [request] + results = proxy._batch(requests) + result = results[0] + self.assertTrue(all([expected_key in result.keys() for expected_key in expected_keys])) + + def test__batch_result_error_is_none(self): + proxy = FakeBitcoinProxy() + request = make_rpc_batch_request_entry("getblockcount", []) + requests = [request] + results = proxy._batch(requests) + result = results[0] + self.assertEqual(result["error"], None) + + def test__batch_returns_error_when_given_invalid_params(self): + params = 1 + proxy = FakeBitcoinProxy() + request = make_rpc_batch_request_entry("getblockcount", params) + requests = [request] + results = proxy._batch(requests) + result = results[0] + self.assertEqual(type(result), dict) + self.assertIn("error", result.keys()) + self.assertIn("id", result.keys()) + self.assertIn("result", result.keys()) + self.assertEqual(result["result"], None) + self.assertEqual(type(result["error"]), dict) + self.assertIn("message", result["error"].keys()) + self.assertIn("code", result["error"].keys()) + self.assertEqual(result["error"]["message"], "Params must be an array") + + def test__batch_getblockhash_many(self): + block_count = 100 + blocks = [] + + previous_blockhash = None + for counter in range(0, block_count): + blockhash = "00008c0c84aee66413f1e8ff95fdca5e8ebf35c94b090290077cdcea649{}".format(counter) + block_data = { + "hash": blockhash, + } + + if previous_blockhash: + block_data["previous_blockhash"] = previous_blockhash + + blocks.append(block_data) + previous_blockhash = blockhash + + proxy = FakeBitcoinProxy(blocks=blocks) + count = proxy.getblockcount() + self.assertEqual(count, len(blocks) - 1) + + requests = [] + for counter in range(0, count + 1): # from 0 to len(blocks) + request = make_rpc_batch_request_entry("getblockhash", [counter]) + requests.append(request) + + results = proxy._batch(requests) + + self.assertEqual(type(results), list) + self.assertEqual(len(results), len(requests)) + + for (counter, result) in enumerate(results): + self.assertEqual(result["error"], None) + + expected_blockhash = "00008c0c84aee66413f1e8ff95fdca5e8ebf35c94b090290077cdcea649{}".format(counter) + self.assertEqual(result["result"], expected_blockhash) + + def test_make_blocks_from_blockhashes(self): + blockhashes = ["blah{}".format(x) for x in range(0, 25)] + blocks = make_blocks_from_blockhashes(blockhashes) + + self.assertEqual(type(blocks), list) + self.assertEqual(len(blocks), len(blockhashes)) + self.assertEqual(type(blocks[0]), dict) + self.assertTrue(all(["hash" in block.keys() for block in blocks])) + self.assertTrue(all(["height" in block.keys() for block in blocks])) + self.assertTrue(all(["tx" in block.keys() for block in blocks])) + self.assertNotIn("previousblockhash", blocks[0].keys()) + self.assertTrue(all(["previousblockhash" in block.keys() for block in blocks[1:]])) + self.assertEqual(blocks[-1]["previousblockhash"], blocks[-2]["hash"]) + self.assertEqual(sorted([block["hash"] for block in blocks]), sorted(blockhashes)) + + def test_make_blocks_from_blockhashes_empty(self): + blockhashes = [] + blocks = make_blocks_from_blockhashes(blockhashes) + self.assertEqual(type(blocks), list) + self.assertEqual(len(blocks), len(blockhashes)) + + # Just in case for some reason the function ever appends to the given + # list (incorrectly).. + self.assertEqual(len(blockhashes), 0) + self.assertEqual(len(blocks), 0) + + # and because paranoia + self.assertTrue(blockhashes is not blocks) + +if __name__ == "__main__": + unittest.main() diff --git a/bitcoin/tests/test_key.py b/bitcoin/tests/test_key.py index a99aa873..6cb18b54 100644 --- a/bitcoin/tests/test_key.py +++ b/bitcoin/tests/test_key.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2014 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,7 +9,6 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals import unittest diff --git a/bitcoin/tests/test_messages.py b/bitcoin/tests/test_messages.py index 8b51a126..8b201dc2 100644 --- a/bitcoin/tests/test_messages.py +++ b/bitcoin/tests/test_messages.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2014 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -16,11 +16,7 @@ msg_block, msg_getaddr, msg_ping, msg_pong, msg_mempool, MsgSerializable, \ msg_notfound, msg_reject -import sys -if sys.version > '3': - from io import BytesIO -else: - from cStringIO import StringIO as BytesIO +from io import BytesIO class MessageTestCase(unittest.TestCase): diff --git a/bitcoin/tests/test_net.py b/bitcoin/tests/test_net.py index df83856f..74a81325 100644 --- a/bitcoin/tests/test_net.py +++ b/bitcoin/tests/test_net.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2014 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -13,13 +13,7 @@ from bitcoin.net import CAddress, CAlert, CUnsignedAlert, CInv -# Py3 compatibility -import sys - -if sys.version > '3': - from io import BytesIO as _BytesIO -else: - from cStringIO import StringIO as _BytesIO +from io import BytesIO class TestUnsignedAlert(unittest.TestCase): @@ -27,10 +21,10 @@ def test_serialization(self): alert = CUnsignedAlert() alert.setCancel = [1, 2, 3] alert.strComment = b"Comment" - stream = _BytesIO() + stream = BytesIO() alert.stream_serialize(stream) - serialized = _BytesIO(stream.getvalue()) + serialized = BytesIO(stream.getvalue()) deserialized = CUnsignedAlert.stream_deserialize(serialized) self.assertEqual(deserialized, alert) @@ -41,10 +35,10 @@ def test_serialization(self): alert = CAlert() alert.setCancel = [1, 2, 3] alert.strComment = b"Comment" - stream = _BytesIO() + stream = BytesIO() alert.stream_serialize(stream) - serialized = _BytesIO(stream.getvalue()) + serialized = BytesIO(stream.getvalue()) deserialized = CAlert.stream_deserialize(serialized) self.assertEqual(deserialized, alert) @@ -55,10 +49,10 @@ def test_serialization(self): inv = CInv() inv.type = 123 inv.hash = b"0" * 32 - stream = _BytesIO() + stream = BytesIO() inv.stream_serialize(stream) - serialized = _BytesIO(stream.getvalue()) + serialized = BytesIO(stream.getvalue()) deserialized = CInv.stream_deserialize(serialized) self.assertEqual(deserialized, inv) diff --git a/bitcoin/tests/test_ripemd160.py b/bitcoin/tests/test_ripemd160.py new file mode 100644 index 00000000..df1374d9 --- /dev/null +++ b/bitcoin/tests/test_ripemd160.py @@ -0,0 +1,24 @@ +import unittest + +from bitcoin.core.contrib.ripemd160 import ripemd160 + + +class Test_ripemd160(unittest.TestCase): + def test_ripemd160(self): + """RIPEMD-160 test vectors.""" + # See https://homes.esat.kuleuven.be/~bosselae/ripemd160.html + for msg, hexout in [ + (b"", "9c1185a5c5e9fc54612808977ee8f548b2258d31"), + (b"a", "0bdc9d2d256b3ee9daae347be6f4dc835a467ffe"), + (b"abc", "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc"), + (b"message digest", "5d0689ef49d2fae572b881b123a85ffa21595f36"), + (b"abcdefghijklmnopqrstuvwxyz", + "f71c27109c692c1b56bbdceb5b9d2865b3708dbc"), + (b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "12a053384a9c0c88e405a06c27dcf49ada62eb2b"), + (b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + "b0e20b6e3116640286ed3a87a5713079b21f5189"), + (b"1234567890" * 8, "9b752e45573d4b39f4dbd3323cab82bf63326bfb"), + (b"a" * 1000000, "52783243c1697bdbe16d37f97f68f08325dc1528") + ]: + self.assertEqual(ripemd160(msg).hex(), hexout) diff --git a/bitcoin/tests/test_rpc.py b/bitcoin/tests/test_rpc.py index 07d1f964..743db350 100644 --- a/bitcoin/tests/test_rpc.py +++ b/bitcoin/tests/test_rpc.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2014 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,7 +9,6 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals import unittest diff --git a/bitcoin/tests/test_script.py b/bitcoin/tests/test_script.py index b461e675..1ca62eb0 100644 --- a/bitcoin/tests/test_script.py +++ b/bitcoin/tests/test_script.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2015 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,7 +9,6 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals import unittest import os diff --git a/bitcoin/tests/test_scripteval.py b/bitcoin/tests/test_scripteval.py index ee2ceb96..a1762020 100644 --- a/bitcoin/tests/test_scripteval.py +++ b/bitcoin/tests/test_scripteval.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2017 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,16 +9,11 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals import json import os import unittest -import sys -if sys.version > '3': - long = int - from binascii import unhexlify from bitcoin.core import * @@ -40,7 +35,7 @@ def ishex(s): for word in s.split(): if word.isdigit() or (word[0] == '-' and word[1:].isdigit()): - r.append(CScript([long(word)])) + r.append(CScript([int(word)])) elif word.startswith('0x') and ishex(word[2:]): # Raw ex data, inserted NOT pushed onto stack: r.append(unhexlify(word[2:].encode('utf8'))) diff --git a/bitcoin/tests/test_segwit.py b/bitcoin/tests/test_segwit.py index 62e22ea2..76b89de0 100644 --- a/bitcoin/tests/test_segwit.py +++ b/bitcoin/tests/test_segwit.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,7 +9,6 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals import unittest import random @@ -20,11 +19,6 @@ from bitcoin.core.script import * from bitcoin.wallet import * -if sys.version > '3': - _bytes = bytes -else: - _bytes = lambda x: bytes(bytearray(x)) - # Test serialization @@ -36,8 +30,8 @@ def test_p2wpkh_signaturehash(self): scriptpubkey = CScript(x('00141d0f172a0ecb48aee1be1f2687d2963ae33f71a1')) value = int(6*COIN) - address = CBitcoinAddress.from_scriptPubKey(scriptpubkey) - self.assertEqual(SignatureHash(address.to_scriptPubKey(), CTransaction.deserialize(unsigned_tx), + address = CBech32BitcoinAddress.from_scriptPubKey(scriptpubkey) + self.assertEqual(SignatureHash(address.to_redeemScript(), CTransaction.deserialize(unsigned_tx), 1, SIGHASH_ALL, value, SIGVERSION_WITNESS_V0), x('c37af31116d1b27caf68aae9e3ac82f1477929014d5b917657d0eb49478cb670')) @@ -47,8 +41,8 @@ def test_p2sh_p2wpkh_signaturehash(self): redeemscript = CScript(x('001479091972186c449eb1ded22b78e40d009bdf0089')) value = int(10*COIN) - address = CBitcoinAddress.from_scriptPubKey(redeemscript) - self.assertEqual(SignatureHash(address.to_scriptPubKey(), CTransaction.deserialize(unsigned_tx), + address = CBase58BitcoinAddress.from_scriptPubKey(redeemscript) + self.assertEqual(SignatureHash(address.to_redeemScript(), CTransaction.deserialize(unsigned_tx), 0, SIGHASH_ALL, value, SIGVERSION_WITNESS_V0), x('64f3b0f4dd2bb3aa1ce8566d220cc74dda9df97d8490cc81d89d735c92e59fb6')) @@ -122,7 +116,7 @@ def test_checkblock(self): str_tx = '0100000001e3d0c1d051a3abe79d5951dcab86f71d8926aff5caed5ff9bd72cb593e4ebaf5010000006b483045022100a28e1c57e160296953e1af85c5034bb1b907c12c50367da75ba547874a8a8c52022040682e888ddce7fd5c72519a9f28f22f5164c8af864d33332bbc7f65596ad4ae012102db30394fd5cc8288bed607b9382338240c014a98c9c0febbfb380db74ceb30a3ffffffff020d920000000000001976a914ad781c8ffcc18b2155433cba2da4601180a66eef88aca3170000000000001976a914bacb1c4b0725e61e385c7093b4260533953c8e1688ac00000000' # SegWit transaction str_w_tx = '0100000000010115e180dc28a2327e687facc33f10f2a20da717e5548406f7ae8b4c811072f8560100000000ffffffff0100b4f505000000001976a9141d7cd6c75c2e86f4cbf98eaed221b30bd9a0b92888ac02483045022100df7b7e5cda14ddf91290e02ea10786e03eb11ee36ec02dd862fe9a326bbcb7fd02203f5b4496b667e6e281cc654a2da9e4f08660c620a1051337fa8965f727eb19190121038262a6c6cec93c2d3ecd6c6072efea86d02ff8e3328bbd0242b20af3425990ac00000000' - witness_nonce = _bytes(random.getrandbits(8) for _ in range(32)) + witness_nonce = bytes(random.getrandbits(8) for _ in range(32)) coinbase = CMutableTransaction.deserialize(x(str_coinbase)) coinbase.wit = CTxWitness([CTxInWitness(CScriptWitness([witness_nonce]))]) diff --git a/bitcoin/tests/test_serialize.py b/bitcoin/tests/test_serialize.py index a5e45330..2031b8ac 100644 --- a/bitcoin/tests/test_serialize.py +++ b/bitcoin/tests/test_serialize.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2014 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,7 +9,6 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals import unittest, random diff --git a/bitcoin/tests/test_signmessage.py b/bitcoin/tests/test_signmessage.py index 33dd7ad8..bfdc3efb 100644 --- a/bitcoin/tests/test_signmessage.py +++ b/bitcoin/tests/test_signmessage.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2015 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,23 +9,14 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals import unittest from bitcoin.wallet import CBitcoinSecret from bitcoin.signmessage import BitcoinMessage, VerifyMessage, SignMessage -import sys import os import json -_bchr = chr -_bord = ord -if sys.version > '3': - long = int - _bchr = lambda x: bytes([x]) - _bord = lambda x: x - def load_test_vectors(name): with open(os.path.dirname(__file__) + '/data/' + name, 'r') as fd: return json.load(fd) diff --git a/bitcoin/tests/test_transactions.py b/bitcoin/tests/test_transactions.py index f96c5b32..bdff08f2 100644 --- a/bitcoin/tests/test_transactions.py +++ b/bitcoin/tests/test_transactions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2014 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,7 +9,6 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals import json import unittest @@ -151,3 +150,24 @@ def test_tx_invalid(self): flags.add(SCRIPT_VERIFY_P2SH) VerifyScript(tx.vin[i].scriptSig, prevouts[tx.vin[i].prevout], tx, i, flags=flags) + + def test_calc_weight(self): + # test vectors taken from rust-bitcoin + txs = [ + # one segwit input (P2WPKH) + ('020000000001018a763b78d3e17acea0625bf9e52b0dc1beb2241b2502185348ba8ff4a253176e0100000000ffffffff0280d725000000000017a914c07ed639bd46bf7087f2ae1dfde63b815a5f8b488767fda20300000000160014869ec8520fa2801c8a01bfdd2e82b19833cd0daf02473044022016243edad96b18c78b545325aaff80131689f681079fb107a67018cb7fb7830e02205520dae761d89728f73f1a7182157f6b5aecf653525855adb7ccb998c8e6143b012103b9489bde92afbcfa85129a82ffa512897105d1a27ad9806bded27e0532fc84e700000000', 565), + # one segwit input (P2WSH) + ('01000000000101a3ccad197118a2d4975fadc47b90eacfdeaf8268adfdf10ed3b4c3b7e1ad14530300000000ffffffff0200cc5501000000001976a91428ec6f21f4727bff84bb844e9697366feeb69f4d88aca2a5100d00000000220020701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d04004730440220548f11130353b3a8f943d2f14260345fc7c20bde91704c9f1cbb5456355078cd0220383ed4ed39b079b618bcb279bbc1f2ca18cb028c4641cb522c9c5868c52a0dc20147304402203c332ecccb3181ca82c0600520ee51fee80d3b4a6ab110945e59475ec71e44ac0220679a11f3ca9993b04ccebda3c834876f353b065bb08f50076b25f5bb93c72ae1016952210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea368e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae00000000', 766), + # one segwit input (P2WPKH) and two legacy inputs (P2PKH) + ('010000000001036b6b6ac7e34e97c53c1cc74c99c7948af2e6aac75d8778004ae458d813456764000000006a473044022001deec7d9075109306320b3754188f81a8236d0d232b44bc69f8309115638b8f02204e17a5194a519cf994d0afeea1268740bdc10616b031a521113681cc415e815c012103488d3272a9fad78ee887f0684cb8ebcfc06d0945e1401d002e590c7338b163feffffffffc75bd7aa6424aee972789ec28ba181254ee6d8311b058d165bd045154d7660b0000000006b483045022100c8641bcbee3e4c47a00417875015d8c5d5ea918fb7e96f18c6ffe51bc555b401022074e2c46f5b1109cd79e39a9aa203eadd1d75356415e51d80928a5fb5feb0efee0121033504b4c6dfc3a5daaf7c425aead4c2dbbe4e7387ce8e6be2648805939ecf7054ffffffff494df3b205cd9430a26f8e8c0dc0bb80496fbc555a524d6ea307724bc7e60eee0100000000ffffffff026d861500000000001976a9145c54ed1360072ebaf56e87693b88482d2c6a101588ace407000000000000160014761e31e2629c6e11936f2f9888179d60a5d4c1f900000247304402201fa38a67a63e58b67b6cfffd02f59121ca1c8a1b22e1efe2573ae7e4b4f06c2b022002b9b431b58f6e36b3334fb14eaecee7d2f06967a77ef50d8d5f90dda1057f0c01210257dc6ce3b1100903306f518ee8fa113d778e403f118c080b50ce079fba40e09a00000000', 1755), + # three legacy inputs (P2PKH) + ('0100000003e4d7be4314204a239d8e00691128dca7927e19a7339c7948bde56f669d27d797010000006b483045022100b988a858e2982e2daaf0755b37ad46775d6132057934877a5badc91dee2f66ff022020b967c1a2f0916007662ec609987e951baafa6d4fda23faaad70715611d6a2501210254a2dccd8c8832d4677dc6f0e562eaaa5d11feb9f1de2c50a33832e7c6190796ffffffff9e22eb1b3f24c260187d716a8a6c2a7efb5af14a30a4792a6eeac3643172379c000000006a47304402207df07f0cd30dca2cf7bed7686fa78d8a37fe9c2254dfdca2befed54e06b779790220684417b8ff9f0f6b480546a9e90ecee86a625b3ea1e4ca29b080da6bd6c5f67e01210254a2dccd8c8832d4677dc6f0e562eaaa5d11feb9f1de2c50a33832e7c6190796ffffffff1123df3bfb503b59769731da103d4371bc029f57979ebce68067768b958091a1000000006a47304402207a016023c2b0c4db9a7d4f9232fcec2193c2f119a69125ad5bcedcba56dd525e02206a734b3a321286c896759ac98ebfd9d808df47f1ce1fbfbe949891cc3134294701210254a2dccd8c8832d4677dc6f0e562eaaa5d11feb9f1de2c50a33832e7c6190796ffffffff0200c2eb0b000000001976a914e5eb3e05efad136b1405f5c2f9adb14e15a35bb488ac88cfff1b000000001976a9144846db516db3130b7a3c92253599edec6bc9630b88ac00000000', 2080), + # one segwit input (P2TR) + ('01000000000101b5cee87f1a60915c38bb0bc26aaf2b67be2b890bbc54bb4be1e40272e0d2fe0b0000000000ffffffff025529000000000000225120106daad8a5cb2e6fc74783714273bad554a148ca2d054e7a19250e9935366f3033760000000000002200205e6d83c44f57484fd2ef2a62b6d36cdcd6b3e06b661e33fd65588a28ad0dbe060141df9d1bfce71f90d68bf9e9461910b3716466bfe035c7dbabaa7791383af6c7ef405a3a1f481488a91d33cd90b098d13cb904323a3e215523aceaa04e1bb35cdb0100000000', 617), + # one legacy input (P2PKH) + ('0100000001c336895d9fa674f8b1e294fd006b1ac8266939161600e04788c515089991b50a030000006a47304402204213769e823984b31dcb7104f2c99279e74249eacd4246dabcf2575f85b365aa02200c3ee89c84344ae326b637101a92448664a8d39a009c8ad5d147c752cbe112970121028b1b44b4903c9103c07d5a23e3c7cf7aeb0ba45ddbd2cfdce469ab197381f195fdffffff040000000000000000536a4c5058325bb7b7251cf9e36cac35d691bd37431eeea426d42cbdecca4db20794f9a4030e6cb5211fabf887642bcad98c9994430facb712da8ae5e12c9ae5ff314127d33665000bb26c0067000bb0bf00322a50c300000000000017a9145ca04fdc0a6d2f4e3f67cfeb97e438bb6287725f8750c30000000000001976a91423086a767de0143523e818d4273ddfe6d9e4bbcc88acc8465003000000001976a914c95cbacc416f757c65c942f9b6b8a20038b9b12988ac00000000', 1396), + ] + + for tx, expected_wu in txs: + tx = CTransaction.deserialize(x(tx)) + self.assertEqual(tx.calc_weight(), expected_wu) diff --git a/bitcoin/tests/test_wallet.py b/bitcoin/tests/test_wallet.py index 5ee839af..68ba69f0 100644 --- a/bitcoin/tests/test_wallet.py +++ b/bitcoin/tests/test_wallet.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2015 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -9,24 +9,27 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from __future__ import absolute_import, division, print_function, unicode_literals +import hashlib import unittest from bitcoin.core import b2x, x from bitcoin.core.script import CScript, IsLowDERSignature -from bitcoin.core.key import CPubKey +from bitcoin.core.key import CPubKey, is_libsec256k1_available, use_libsecp256k1_for_signing from bitcoin.wallet import * class Test_CBitcoinAddress(unittest.TestCase): def test_create_from_string(self): """Create CBitcoinAddress's from strings""" - def T(str_addr, expected_bytes, expected_nVersion, expected_class): + def T(str_addr, expected_bytes, expected_version, expected_class): addr = CBitcoinAddress(str_addr) self.assertEqual(addr.to_bytes(), expected_bytes) - self.assertEqual(addr.nVersion, expected_nVersion) self.assertEqual(addr.__class__, expected_class) + if isinstance(addr, CBase58BitcoinAddress): + self.assertEqual(addr.nVersion, expected_version) + elif isinstance(addr, CBech32BitcoinAddress): + self.assertEqual(addr.witver, expected_version) T('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', x('62e907b15cbf27d5425399ebf6f0fb50ebb88f18'), 0, @@ -36,6 +39,14 @@ def T(str_addr, expected_bytes, expected_nVersion, expected_class): x('4266fc6f2c2861d7fe229b279a79803afca7ba34'), 5, P2SHBitcoinAddress) + T('BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4', + x('751e76e8199196d454941c45d1b3a323f1433bd6'), 0, + P2WPKHBitcoinAddress) + + T('bc1qc7slrfxkknqcq2jevvvkdgvrt8080852dfjewde450xdlk4ugp7szw5tk9', + x('c7a1f1a4d6b4c1802a59631966a18359de779e8a6a65973735a3ccdfdabc407d'), 0, + P2WSHBitcoinAddress) + def test_wrong_nVersion(self): """Creating a CBitcoinAddress from a unknown nVersion fails""" @@ -57,6 +68,12 @@ def T(hex_scriptpubkey, expected_str_address, expected_class): P2SHBitcoinAddress) T('76a914000000000000000000000000000000000000000088ac', '1111111111111111111114oLvT2', P2PKHBitcoinAddress) + T('0014751e76e8199196d454941c45d1b3a323f1433bd6', + 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4', + P2WPKHBitcoinAddress) + T('0020c7a1f1a4d6b4c1802a59631966a18359de779e8a6a65973735a3ccdfdabc407d', + 'bc1qc7slrfxkknqcq2jevvvkdgvrt8080852dfjewde450xdlk4ugp7szw5tk9', + P2WSHBitcoinAddress) def test_from_nonstd_scriptPubKey(self): """CBitcoinAddress.from_scriptPubKey() with non-standard scriptPubKeys""" @@ -100,6 +117,23 @@ def test_from_invalid_scriptPubKey(self): with self.assertRaises(CBitcoinAddressError): CBitcoinAddress.from_scriptPubKey(scriptPubKey) + def test_to_redeemScript(self): + def T(str_addr, expected_scriptPubKey_hexbytes): + addr = CBitcoinAddress(str_addr) + + actual_scriptPubKey = addr.to_redeemScript() + self.assertEqual(b2x(actual_scriptPubKey), + expected_scriptPubKey_hexbytes) + + T('31h1vYVSYuKP6AhS86fbRdMw9XHieotbST', + 'a914000000000000000000000000000000000000000087') + + T('1111111111111111111114oLvT2', + '76a914000000000000000000000000000000000000000088ac') + + T('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4', + '76a914751e76e8199196d454941c45d1b3a323f1433bd688ac') + def test_to_scriptPubKey(self): """CBitcoinAddress.to_scriptPubKey() works""" def T(str_addr, expected_scriptPubKey_hexbytes): @@ -114,10 +148,17 @@ def T(str_addr, expected_scriptPubKey_hexbytes): T('1111111111111111111114oLvT2', '76a914000000000000000000000000000000000000000088ac') + class Test_P2SHBitcoinAddress(unittest.TestCase): def test_from_redeemScript(self): - addr = P2SHBitcoinAddress.from_redeemScript(CScript()) - self.assertEqual(str(addr), '3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy') + def T(script, expected_str_address): + addr = P2SHBitcoinAddress.from_redeemScript(script) + self.assertEqual(str(addr), expected_str_address) + + T(CScript(), '3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy') + T(CScript(x('76a914751e76e8199196d454941c45d1b3a323f1433bd688ac')), + '3LRW7jeCvQCRdPF8S3yUCfRAx4eqXFmdcr') + class Test_P2PKHBitcoinAddress(unittest.TestCase): def test_from_non_canonical_scriptPubKey(self): @@ -238,3 +279,57 @@ def test_sign_invalid_hash(self): hash = b'\x00' * 32 with self.assertRaises(ValueError): sig = key.sign(hash[0:-2]) + + +class Test_RFC6979(unittest.TestCase): + def test(self): + if not is_libsec256k1_available(): + return + + use_libsecp256k1_for_signing(True) + + # Test Vectors for RFC 6979 ECDSA, secp256k1, SHA-256 + # (private key, message, expected k, expected signature) + test_vectors = [ + (0x1, "Satoshi Nakamoto", 0x8F8A276C19F4149656B280621E358CCE24F5F52542772691EE69063B74F15D15, "934b1ea10a4b3c1757e2b0c017d0b6143ce3c9a7e6a4a49860d7a6ab210ee3d82442ce9d2b916064108014783e923ec36b49743e2ffa1c4496f01a512aafd9e5"), + (0x1, "All those moments will be lost in time, like tears in rain. Time to die...", 0x38AA22D72376B4DBC472E06C3BA403EE0A394DA63FC58D88686C611ABA98D6B3, "8600dbd41e348fe5c9465ab92d23e3db8b98b873beecd930736488696438cb6b547fe64427496db33bf66019dacbf0039c04199abb0122918601db38a72cfc21"), + (0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140, "Satoshi Nakamoto", 0x33A19B60E25FB6F4435AF53A3D42D493644827367E6453928554F43E49AA6F90, "fd567d121db66e382991534ada77a6bd3106f0a1098c231e47993447cd6af2d06b39cd0eb1bc8603e159ef5c20a5c8ad685a45b06ce9bebed3f153d10d93bed5"), + (0xf8b8af8ce3c7cca5e300d33939540c10d45ce001b8f252bfbc57ba0342904181, "Alan Turing", 0x525A82B70E67874398067543FD84C83D30C175FDC45FDEEE082FE13B1D7CFDF1, "7063ae83e7f62bbb171798131b4a0564b956930092b33b07b395615d9ec7e15c58dfcc1e00a35e1572f366ffe34ba0fc47db1e7189759b9fb233c5b05ab388ea"), + (0xe91671c46231f833a6406ccbea0e3e392c76c167bac1cb013f6f1013980455c2, "There is a computer disease that anybody who works with computers knows about. It's a very serious disease and it interferes completely with the work. The trouble with computers is that you 'play' with them!", 0x1F4B84C23A86A221D233F2521BE018D9318639D5B8BBD6374A8A59232D16AD3D, "b552edd27580141f3b2a5463048cb7cd3e047b97c9f98076c32dbdf85a68718b279fa72dd19bfae05577e06c7c0c1900c371fcd5893f7e1d56a37d30174671f6") + ] + for vector in test_vectors: + secret = CBitcoinSecret.from_secret_bytes(x('{:064x}'.format(vector[0]))) + encoded_sig = secret.sign(hashlib.sha256(vector[1].encode('utf8')).digest()) + + assert(encoded_sig[0] == 0x30) + assert(encoded_sig[1] == len(encoded_sig)-2) + assert(encoded_sig[2] == 0x02) + + rlen = encoded_sig[3] + rpos = 4 + assert(rlen in (32, 33)) + + if rlen == 33: + assert(encoded_sig[rpos] == 0) + rpos += 1 + rlen -= 1 + + rval = encoded_sig[rpos:rpos+rlen] + spos = rpos+rlen + assert(encoded_sig[spos] == 0x02) + + spos += 1 + slen = encoded_sig[spos] + assert(slen in (32, 33)) + + spos += 1 + if slen == 33: + assert(encoded_sig[spos] == 0) + spos += 1 + slen -= 1 + + sval = encoded_sig[spos:spos+slen] + sig = b2x(rval + sval) + assert(str(sig) == vector[3]) + + use_libsecp256k1_for_signing(False) diff --git a/bitcoin/wallet.py b/bitcoin/wallet.py index 3c463a3d..292d6592 100644 --- a/bitcoin/wallet.py +++ b/bitcoin/wallet.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2014 The python-bitcoinlib developers +# Copyright (C) The python-bitcoinlib developers # # This file is part of python-bitcoinlib. # @@ -15,29 +15,98 @@ scriptPubKeys; currently there is no actual wallet support implemented. """ -from __future__ import absolute_import, division, print_function, unicode_literals - -import sys - -_bord = ord -if sys.version > '3': - _bord = lambda x: x import bitcoin import bitcoin.base58 +import bitcoin.bech32 import bitcoin.core import bitcoin.core.key import bitcoin.core.script as script -class CBitcoinAddressError(bitcoin.base58.Base58Error): + +class CBitcoinAddress(object): + + def __new__(cls, s): + try: + return CBech32BitcoinAddress(s) + except bitcoin.bech32.Bech32Error: + pass + + try: + return CBase58BitcoinAddress(s) + except bitcoin.base58.Base58Error: + pass + + raise CBitcoinAddressError('Unrecognized encoding for bitcoin address') + + @classmethod + def from_scriptPubKey(cls, scriptPubKey): + """Convert a scriptPubKey to a subclass of CBitcoinAddress""" + try: + return CBech32BitcoinAddress.from_scriptPubKey(scriptPubKey) + except CBitcoinAddressError: + pass + + try: + return CBase58BitcoinAddress.from_scriptPubKey(scriptPubKey) + except CBitcoinAddressError: + pass + + raise CBitcoinAddressError('scriptPubKey is not in a recognized address format') + + +class CBitcoinAddressError(Exception): """Raised when an invalid Bitcoin address is encountered""" -class CBitcoinAddress(bitcoin.base58.CBase58Data): - """A Bitcoin address""" + +class CBech32BitcoinAddress(bitcoin.bech32.CBech32Data, CBitcoinAddress): + """A Bech32-encoded Bitcoin address""" + + @classmethod + def from_bytes(cls, witver, witprog): + + assert witver == 0 + self = super(CBech32BitcoinAddress, cls).from_bytes( + witver, + bytes(witprog) + ) + + if len(self) == 32: + self.__class__ = P2WSHBitcoinAddress + elif len(self) == 20: + self.__class__ = P2WPKHBitcoinAddress + else: + raise CBitcoinAddressError('witness program does not match any known segwit address format') + + return self + + @classmethod + def from_scriptPubKey(cls, scriptPubKey): + """Convert a scriptPubKey to a CBech32BitcoinAddress + + Returns a CBech32BitcoinAddress subclass, either P2WSHBitcoinAddress or + P2WPKHBitcoinAddress. If the scriptPubKey is not recognized + CBitcoinAddressError will be raised. + """ + try: + return P2WSHBitcoinAddress.from_scriptPubKey(scriptPubKey) + except CBitcoinAddressError: + pass + + try: + return P2WPKHBitcoinAddress.from_scriptPubKey(scriptPubKey) + except CBitcoinAddressError: + pass + + raise CBitcoinAddressError('scriptPubKey not a valid bech32-encoded address') + + +class CBase58BitcoinAddress(bitcoin.base58.CBase58Data, CBitcoinAddress): + """A Base58-encoded Bitcoin address""" @classmethod def from_bytes(cls, data, nVersion): - self = super(CBitcoinAddress, cls).from_bytes(data, nVersion) + self = super(CBase58BitcoinAddress, cls).from_bytes(data, nVersion) if nVersion == bitcoin.params.BASE58_PREFIXES['SCRIPT_ADDR']: self.__class__ = P2SHBitcoinAddress @@ -68,13 +137,10 @@ def from_scriptPubKey(cls, scriptPubKey): except CBitcoinAddressError: pass - raise CBitcoinAddressError('scriptPubKey not a valid address') + raise CBitcoinAddressError('scriptPubKey not a valid base58-encoded address') - def to_scriptPubKey(self): - """Convert an address to a scriptPubKey""" - raise NotImplementedError -class P2SHBitcoinAddress(CBitcoinAddress): +class P2SHBitcoinAddress(CBase58BitcoinAddress): @classmethod def from_bytes(cls, data, nVersion=None): if nVersion is None: @@ -112,7 +178,11 @@ def to_scriptPubKey(self): assert self.nVersion == bitcoin.params.BASE58_PREFIXES['SCRIPT_ADDR'] return script.CScript([script.OP_HASH160, self, script.OP_EQUAL]) -class P2PKHBitcoinAddress(CBitcoinAddress): + def to_redeemScript(self): + return self.to_scriptPubKey() + + +class P2PKHBitcoinAddress(CBase58BitcoinAddress): @classmethod def from_bytes(cls, data, nVersion=None): if nVersion is None: @@ -170,11 +240,11 @@ def from_scriptPubKey(cls, scriptPubKey, accept_non_canonical_pushdata=True, acc elif scriptPubKey.is_witness_v0_nested_keyhash(): return cls.from_bytes(scriptPubKey[3:23], bitcoin.params.BASE58_PREFIXES['PUBKEY_ADDR']) elif (len(scriptPubKey) == 25 - and _bord(scriptPubKey[0]) == script.OP_DUP - and _bord(scriptPubKey[1]) == script.OP_HASH160 - and _bord(scriptPubKey[2]) == 0x14 - and _bord(scriptPubKey[23]) == script.OP_EQUALVERIFY - and _bord(scriptPubKey[24]) == script.OP_CHECKSIG): + and scriptPubKey[0] == script.OP_DUP + and scriptPubKey[1] == script.OP_HASH160 + and scriptPubKey[2] == 0x14 + and scriptPubKey[23] == script.OP_EQUALVERIFY + and scriptPubKey[24] == script.OP_CHECKSIG): return cls.from_bytes(scriptPubKey[3:23], bitcoin.params.BASE58_PREFIXES['PUBKEY_ADDR']) elif accept_bare_checksig: @@ -183,14 +253,14 @@ def from_scriptPubKey(cls, scriptPubKey, accept_non_canonical_pushdata=True, acc # We can operate on the raw bytes directly because we've # canonicalized everything above. if (len(scriptPubKey) == 35 # compressed - and _bord(scriptPubKey[0]) == 0x21 - and _bord(scriptPubKey[34]) == script.OP_CHECKSIG): + and scriptPubKey[0] == 0x21 + and scriptPubKey[34] == script.OP_CHECKSIG): pubkey = scriptPubKey[1:34] elif (len(scriptPubKey) == 67 # uncompressed - and _bord(scriptPubKey[0]) == 0x41 - and _bord(scriptPubKey[66]) == script.OP_CHECKSIG): + and scriptPubKey[0] == 0x41 + and scriptPubKey[66] == script.OP_CHECKSIG): pubkey = scriptPubKey[1:65] @@ -204,6 +274,55 @@ def to_scriptPubKey(self, nested=False): assert self.nVersion == bitcoin.params.BASE58_PREFIXES['PUBKEY_ADDR'] return script.CScript([script.OP_DUP, script.OP_HASH160, self, script.OP_EQUALVERIFY, script.OP_CHECKSIG]) + def to_redeemScript(self): + return self.to_scriptPubKey() + + +class P2WSHBitcoinAddress(CBech32BitcoinAddress): + + @classmethod + def from_scriptPubKey(cls, scriptPubKey): + """Convert a scriptPubKey to a P2WSH address + + Raises CBitcoinAddressError if the scriptPubKey isn't of the correct + form. + """ + if scriptPubKey.is_witness_v0_scripthash(): + return cls.from_bytes(0, scriptPubKey[2:34]) + else: + raise CBitcoinAddressError('not a P2WSH scriptPubKey') + + def to_scriptPubKey(self): + """Convert an address to a scriptPubKey""" + assert self.witver == 0 + return script.CScript([0, self]) + + def to_redeemScript(self): + raise NotImplementedError("Not enough data in p2wsh address to reconstruct redeem script") + + +class P2WPKHBitcoinAddress(CBech32BitcoinAddress): + + @classmethod + def from_scriptPubKey(cls, scriptPubKey): + """Convert a scriptPubKey to a P2WPKH address + + Raises CBitcoinAddressError if the scriptPubKey isn't of the correct + form. + """ + if scriptPubKey.is_witness_v0_keyhash(): + return cls.from_bytes(0, scriptPubKey[2:22]) + else: + raise CBitcoinAddressError('not a P2WPKH scriptPubKey') + + def to_scriptPubKey(self): + """Convert an address to a scriptPubKey""" + assert self.witver == 0 + return script.CScript([0, self]) + + def to_redeemScript(self): + return script.CScript([script.OP_DUP, script.OP_HASH160, self, script.OP_EQUALVERIFY, script.OP_CHECKSIG]) + class CKey(object): """An encapsulated private key @@ -250,14 +369,18 @@ def __init__(self, s): raise CBitcoinSecretError('Not a base58-encoded secret key: got nVersion=%d; expected nVersion=%d' % \ (self.nVersion, bitcoin.params.BASE58_PREFIXES['SECRET_KEY'])) - CKey.__init__(self, self[0:32], len(self) > 32 and _bord(self[32]) == 1) + CKey.__init__(self, self[0:32], len(self) > 32 and self[32] == 1) __all__ = ( 'CBitcoinAddressError', 'CBitcoinAddress', + 'CBase58BitcoinAddress', + 'CBech32BitcoinAddress', 'P2SHBitcoinAddress', 'P2PKHBitcoinAddress', + 'P2WSHBitcoinAddress', + 'P2WPKHBitcoinAddress', 'CKey', 'CBitcoinSecretError', 'CBitcoinSecret', diff --git a/examples/make-bootstrap-rpc.py b/examples/make-bootstrap-rpc.py index d2e15ce7..a26d98e2 100755 --- a/examples/make-bootstrap-rpc.py +++ b/examples/make-bootstrap-rpc.py @@ -13,11 +13,6 @@ """Make a boostrap.dat file by getting the blocks from the RPC interface.""" -import sys -if sys.version_info.major < 3: - sys.stderr.write('Sorry, Python 3.x required by this example.\n') - sys.exit(1) - import bitcoin import bitcoin.rpc @@ -34,7 +29,7 @@ if len(sys.argv) == 3: bitcoin.SelectParams(sys.argv[2]) except Exception as ex: - print('Usage: %s [network=(mainnet|testnet|regtest)] > bootstrap.dat' % sys.argv[0], file=sys.stderr) + print('Usage: %s [network=(mainnet|testnet|regtest|signet)] > bootstrap.dat' % sys.argv[0], file=sys.stderr) sys.exit(1) diff --git a/examples/msg-serializable.py b/examples/msg-serializable.py index ae6004b9..b3c5aa69 100755 --- a/examples/msg-serializable.py +++ b/examples/msg-serializable.py @@ -13,11 +13,6 @@ """Serialize some bitcoin datastructures and show them in serialized and repr form.""" -import sys -if sys.version_info.major < 3: - sys.stderr.write('Sorry, Python 3.x required by this example.\n') - sys.exit(1) - from bitcoin import SelectParams from bitcoin.messages import msg_version, msg_tx, msg_block diff --git a/examples/publish-text.py b/examples/publish-text.py index 5c0cf504..afb15c16 100755 --- a/examples/publish-text.py +++ b/examples/publish-text.py @@ -28,13 +28,7 @@ # # https://github.com/petertodd/python-bitcoinlib/commit/6a0a2b9429edea318bea7b65a68a950cae536790 -import sys -if sys.version_info.major < 3: - sys.stderr.write('Sorry, Python 3.x required by this example.\n') - sys.exit(1) - import argparse -import hashlib import logging import sys import os diff --git a/examples/spend-p2pkh-txout.py b/examples/spend-p2pkh-txout.py index 7da294a6..da2fd818 100755 --- a/examples/spend-p2pkh-txout.py +++ b/examples/spend-p2pkh-txout.py @@ -13,11 +13,6 @@ """Low-level example of how to spend a standard pay-to-pubkey-hash (P2PKH) txout""" -import sys -if sys.version_info.major < 3: - sys.stderr.write('Sorry, Python 3.x required by this example.\n') - sys.exit(1) - import hashlib from bitcoin import SelectParams diff --git a/examples/spend-p2sh-txout.py b/examples/spend-p2sh-txout.py index 9f0215b0..b28396ec 100755 --- a/examples/spend-p2sh-txout.py +++ b/examples/spend-p2sh-txout.py @@ -13,11 +13,6 @@ """Low-level example of how to spend a P2SH/BIP16 txout""" -import sys -if sys.version_info.major < 3: - sys.stderr.write('Sorry, Python 3.x required by this example.\n') - sys.exit(1) - import hashlib from bitcoin import SelectParams diff --git a/examples/spend-p2wpkh.py b/examples/spend-p2wpkh.py new file mode 100755 index 00000000..f18a2fd9 --- /dev/null +++ b/examples/spend-p2wpkh.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2020 The python-bitcoinlib developers +# +# This file is part of python-bitcoinlib. +# +# It is subject to the license terms in the LICENSE file found in the top-level +# directory of this distribution. +# +# No part of python-bitcoinlib, including this file, may be copied, modified, +# propagated, or distributed except according to the terms contained in the +# LICENSE file. + +"""Low-level example of how to spend a P2WPKH output.""" + +import sys + +import hashlib + +from bitcoin import SelectParams +from bitcoin.core import b2x, b2lx, lx, COIN, COutPoint, CTxOut, CTxIn, CTxInWitness, CTxWitness, CScriptWitness, CMutableTransaction, Hash160 +from bitcoin.core.script import CScript, OP_0, SignatureHash, SIGHASH_ALL, SIGVERSION_WITNESS_V0 +from bitcoin.wallet import CBitcoinSecret, P2WPKHBitcoinAddress +from bitcoin.rpc import Proxy + +SelectParams("regtest") +connection = Proxy() + +if connection._call("getblockchaininfo")["chain"] != "regtest": + sys.stderr.write("This example is intended for regtest only.\n") + sys.exit(1) + + +# Create the (in)famous correct brainwallet secret key. +h = hashlib.sha256(b'correct horse battery staple').digest() +seckey = CBitcoinSecret.from_secret_bytes(h) + +# Create an address from that private key. +public_key = seckey.pub +scriptPubKey = CScript([OP_0, Hash160(public_key)]) +address = P2WPKHBitcoinAddress.from_scriptPubKey(scriptPubKey) + +# Give the private key to bitcoind (for ismine, listunspent, etc). +connection._call("importprivkey", str(seckey)) + +# Check if there's any funds available. +unspentness = lambda: connection._call("listunspent", 6, 9999, [str(address)], True, {"minimumAmount": 1.0}) +unspents = unspentness() +while len(unspents) == 0: + # mine some funds into the address + connection._call("generatetoaddress", 110, str(address)) + unspents = unspentness() + +# Choose the first UTXO, let's spend it! +unspent_utxo_details = unspents[0] +txid = unspent_utxo_details["txid"] +vout = unspent_utxo_details["vout"] +amount = int(float(unspent_utxo_details["amount"]) * COIN) + +# Calculate an amount for the upcoming new UTXO. Set a high fee to bypass +# bitcoind minfee setting. +amount_less_fee = int(amount - (0.01 * COIN)) + +# Create a destination to send the coins. +destination_address = connection._call("getnewaddress", "python-bitcoinlib-example", "bech32") +destination_address = P2WPKHBitcoinAddress(destination_address) +target_scriptPubKey = destination_address.to_scriptPubKey() + +# Create the unsigned transaction. +txin = CTxIn(COutPoint(lx(txid), vout)) +txout = CTxOut(amount_less_fee, target_scriptPubKey) +tx = CMutableTransaction([txin], [txout]) + +# Specify which transaction input is going to be signed for. +txin_index = 0 + +# When signing a P2WPKH transaction, use an "implicit" script that isn't +# specified in the scriptPubKey or the witness. +redeem_script = address.to_redeemScript() + +# Calculate the signature hash for the transaction. This is then signed by the +# private key that controls the UTXO being spent here at this txin_index. +sighash = SignatureHash(redeem_script, tx, txin_index, SIGHASH_ALL, amount=amount, sigversion=SIGVERSION_WITNESS_V0) +signature = seckey.sign(sighash) + bytes([SIGHASH_ALL]) + +# Construct a witness for this transaction input. The public key is given in +# the witness so that the appropriate redeem_script can be calculated by +# anyone. The original scriptPubKey had only the Hash160 hash of the public +# key, not the public key itself, and the redeem script can be entirely +# re-constructed if given just the public key. So the public key is added to +# the witness. This is P2WPKH in bip141. +witness = [signature, public_key] + +# Aggregate all of the witnesses together, and then assign them to the +# transaction object. +ctxinwitnesses = [CTxInWitness(CScriptWitness(witness))] +tx.wit = CTxWitness(ctxinwitnesses) + +# Broadcast the transaction to the regtest network. +spend_txid = connection.sendrawtransaction(tx) + +# Done! Print the transaction to standard output. Show the transaction +# serialization in hex (instead of bytes), and render the txid. +print("serialized transaction: {}".format(b2x(tx.serialize()))) +print("txid: {}".format(b2lx(spend_txid))) diff --git a/examples/spend-p2wsh-txout.py b/examples/spend-p2wsh-txout.py new file mode 100755 index 00000000..f7cb8c6d --- /dev/null +++ b/examples/spend-p2wsh-txout.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2014 The python-bitcoinlib developers +# +# This file is part of python-bitcoinlib. +# +# It is subject to the license terms in the LICENSE file found in the top-level +# directory of this distribution. +# +# No part of python-bitcoinlib, including this file, may be copied, modified, +# propagated, or distributed except according to the terms contained in the +# LICENSE file. + +"""Low-level example of how to spend a P2WSH/BIP141 txout""" + +import hashlib + +from bitcoin import SelectParams +from bitcoin.core import b2x, lx, COIN, COutPoint, CMutableTxOut, CMutableTxIn, CMutableTransaction, CTxInWitness, CTxWitness +from bitcoin.core.script import CScript, CScriptWitness, OP_0, OP_CHECKSIG, SignatureHash, SIGHASH_ALL, SIGVERSION_WITNESS_V0 +from bitcoin.wallet import CBitcoinSecret, CBitcoinAddress, P2WSHBitcoinAddress + +SelectParams('testnet') + +# Create the (in)famous correct brainwallet secret key. +h = hashlib.sha256(b'correct horse battery staple').digest() +seckey = CBitcoinSecret.from_secret_bytes(h) + +# Create a witnessScript and corresponding redeemScript. Similar to a scriptPubKey +# the redeemScript must be satisfied for the funds to be spent. +txin_redeemScript = CScript([seckey.pub, OP_CHECKSIG]) +txin_scriptHash = hashlib.sha256(txin_redeemScript).digest() +txin_scriptPubKey = CScript([OP_0, txin_scriptHash]) + + +# Convert the P2WSH scriptPubKey to a base58 Bitcoin address and print it. +# You'll need to send some funds to it to create a txout to spend. +txin_p2wsh_address = P2WSHBitcoinAddress.from_scriptPubKey(txin_scriptPubKey) +print('Pay to:', str(txin_p2wsh_address)) + +# Same as the txid:vout the createrawtransaction RPC call requires +# lx() takes *little-endian* hex and converts it to bytes; in Bitcoin +# transaction hashes are shown little-endian rather than the usual big-endian. +txid = lx('ace9dc7c987a52266e38fe8544c2d12182401341c98d151f4b394cf69aa5c3e5') +vout = 0 + +# Specify the amount send to your P2WSH address. +amount = int(0.0001 * COIN) + +# Calculate an amount for the upcoming new UTXO. Set a high fee (5%) to bypass +# bitcoind minfee setting. +amount_less_fee = amount * 0.95 + +# Create the txin structure, which includes the outpoint. The scriptSig +# defaults to being empty as is necessary for spending a P2WSH output. +txin = CMutableTxIn(COutPoint(txid, vout)) + + +# Specify a destination address and create the txout. +destination_address = CBitcoinAddress( + '2NGZrVvZG92qGYqzTLjCAewvPZ7JE8S8VxE').to_scriptPubKey() +txout = CMutableTxOut(amount_less_fee, destination_address) + +# Create the unsigned transaction. +tx = CMutableTransaction([txin], [txout]) + +# Calculate the signature hash for that transaction. +sighash = SignatureHash(script=txin_redeemScript, txTo=tx, inIdx=0, + hashtype=SIGHASH_ALL, amount=amount, sigversion=SIGVERSION_WITNESS_V0) + +# Now sign it. We have to append the type of signature we want to the end, in +# this case the usual SIGHASH_ALL. +sig = seckey.sign(sighash) + bytes([SIGHASH_ALL]) + + +# # Construct a witness for this P2WSH transaction and add to tx. +witness = CScriptWitness([sig, txin_redeemScript]) +tx.wit = CTxWitness([CTxInWitness(witness)]) + +# TODO: upgrade VerifyScript to support Segregated Witness and place verify the witness program here. + +# Done! Print the transaction to standard output with the bytes-to-hex +# function. +print(b2x(tx.serialize())) + diff --git a/examples/timestamp-op-ret.py b/examples/timestamp-op-ret.py index 63d4758f..c8ae0a97 100755 --- a/examples/timestamp-op-ret.py +++ b/examples/timestamp-op-ret.py @@ -13,12 +13,6 @@ """Example of timestamping a file via OP_RETURN""" -import sys -if sys.version_info.major < 3: - sys.stderr.write('Sorry, Python 3.x required by this example.\n') - sys.exit(1) - -import hashlib import bitcoin.rpc import sys diff --git a/release-notes.md b/release-notes.md index 57e26c3c..5d5b14ee 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,5 +1,51 @@ # python-bitcoinlib release notes +## v0.12.2 + +* Fixed RPC cookie location for testnet + +## v0.12.1 + +* Added `calc_weight()` method. +* (Hopefully) resolved failure to find `libeay32` on Windows. + +## v0.12.0 + +* `CECKey` now rejects secrets that aren't exactly 32 bytes +* Now compatible with OpenSSL v3; broke compatibility with certain script edge + cases around malformed signatures. This is acceptable because + python-bitcoinlib doesn't claim to have consensus compatibility and no-one + should use it for script/tx validation. + +## v0.11.2 + +* Fixed one remaining use of OpenSSL for RIPEMD-160 + +## v0.11.1 + +* Pure-python RIPEMD-160, for newer versions of OpenSSL without RIPEMD-160 + support. +* Signet support + +## v0.11.0 + +* Bech32 implementation +* Segwit implementation (for P2WSH and P2WPKH transactions) and examples +* Use libsecp256k1 for signing +* Implement OP_CHECKSEQUENCEVERIFY + +New maintainer: Bryan Bishop + +## v0.10.2 + +Note: this will be the last release of python-bitcoinlib with Python 2.7 +compatibility. + +* New RPC `generatetoaddress(self,numblocks,addr)`. +* Fixed Python 2.7 incompatibility. +* Various OpenSSL fixes, including a memory leak. + + ## v0.10.1 Identical in every way to v0.10.0, but re-uploaded under a new version to fix a diff --git a/setup.py b/setup.py index 2805b530..c8d74f5c 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from setuptools import setup, find_packages import os diff --git a/tox.ini b/tox.ini index 7e35da57..fc6b5cf3 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ [tox] #------------------------------------------------------------------- -envlist = reset,py27,py33,py34,py35,pypy,pypy3,stats +envlist = reset,py34,py35,py36,py37,py38,py39,pypy3,stats skip_missing_interpreters = True [testenv] #---------------------------------------------------------------