Browse Source

Add bip32.py and tests.

master
Neil Booth 8 years ago
parent
commit
9dfaedc727
  1. 61
      lib/coins.py
  2. 367
      tests/wallet/test_bip32.py
  3. 307
      wallet/bip32.py

61
lib/coins.py

@ -42,8 +42,9 @@ from lib.script import ScriptPubKey
from lib.tx import Deserializer, DeserializerSegWit, DeserializerAuxPow, \
DeserializerZcash, DeserializerTxTime, DeserializerReddcoin
from server.block_processor import BlockProcessor
from server.daemon import Daemon, LegacyRPCDaemon
from server.session import ElectrumX
from server.daemon import Daemon, DashDaemon, LegacyRPCDaemon
from server.session import ElectrumX, DashElectrumX
Block = namedtuple("Block", "header transactions")
@ -67,6 +68,8 @@ class Coin(object):
DESERIALIZER = Deserializer
DAEMON = Daemon
BLOCK_PROCESSOR = BlockProcessor
XPUB_VERBYTES = bytes('????', 'utf-8')
XPRV_VERBYTES = bytes('????', 'utf-8')
IRC_PREFIX = None
IRC_SERVER = "irc.freenode.net"
IRC_PORT = 6667
@ -153,7 +156,7 @@ class Coin(object):
def lookup_xverbytes(verbytes):
'''Return a (is_xpub, coin_class) pair given xpub/xprv verbytes.'''
# Order means BTC testnet will override NMC testnet
for coin in Coin.coin_classes():
for coin in util.subclasses(Coin):
if verbytes == coin.XPUB_VERBYTES:
return True, coin
if verbytes == coin.XPRV_VERBYTES:
@ -229,7 +232,7 @@ class Coin(object):
raise CoinError('invalid address: {}'.format(address))
@classmethod
def prvkey_WIF(cls, privkey_bytes, compressed):
def privkey_WIF(cls, privkey_bytes, compressed):
'''Return the private key encoded in Wallet Import Format.'''
payload = bytearray(cls.WIF_BYTE) + privkey_bytes
if compressed:
@ -299,10 +302,7 @@ class Coin(object):
}
class CoinAuxPow(Coin):
# Set NAME and NET to avoid exception in Coin::lookup_coin_class
NAME = ''
NET = ''
class AuxPowMixin(object):
STATIC_BLOCK_HEADERS = False
DESERIALIZER = DeserializerAuxPow
@ -411,8 +411,8 @@ class Litecoin(Coin):
NAME = "Litecoin"
SHORTNAME = "LTC"
NET = "mainnet"
XPUB_VERBYTES = bytes.fromhex("0488b21e")
XPRV_VERBYTES = bytes.fromhex("0488ade4")
XPUB_VERBYTES = bytes.fromhex("019d9cfe")
XPRV_VERBYTES = bytes.fromhex("019da462")
P2PKH_VERBYTE = bytes.fromhex("30")
P2SH_VERBYTES = [bytes.fromhex("32"), bytes.fromhex("05")]
WIF_BYTE = bytes.fromhex("b0")
@ -437,8 +437,8 @@ class Litecoin(Coin):
class LitecoinTestnet(Litecoin):
SHORTNAME = "XLT"
NET = "testnet"
XPUB_VERBYTES = bytes.fromhex("043587cf")
XPRV_VERBYTES = bytes.fromhex("04358394")
XPUB_VERBYTES = bytes.fromhex("0436ef7d")
XPRV_VERBYTES = bytes.fromhex("0436f6e1")
P2PKH_VERBYTE = bytes.fromhex("6f")
P2SH_VERBYTES = [bytes.fromhex("3a"), bytes.fromhex("c4")]
WIF_BYTE = bytes.fromhex("ef")
@ -456,12 +456,10 @@ class LitecoinTestnet(Litecoin):
]
class Viacoin(CoinAuxPow):
class Viacoin(AuxPowMixin, Coin):
NAME="Viacoin"
SHORTNAME = "VIA"
NET = "mainnet"
XPUB_VERBYTES = bytes.fromhex("0488B21E")
XPRV_VERBYTES = bytes.fromhex("0488ADE4")
P2PKH_VERBYTE = bytes.fromhex("47")
P2SH_VERBYTES = [bytes.fromhex("21")]
WIF_BYTE = bytes.fromhex("c7")
@ -485,8 +483,6 @@ class Viacoin(CoinAuxPow):
class ViacoinTestnet(Viacoin):
SHORTNAME = "TVI"
NET = "testnet"
XPUB_VERBYTES = bytes.fromhex("043587CF")
XPRV_VERBYTES = bytes.fromhex("04358394")
P2PKH_VERBYTE = bytes.fromhex("7f")
P2SH_VERBYTES = [bytes.fromhex("c4")]
WIF_BYTE = bytes.fromhex("ff")
@ -505,7 +501,7 @@ class ViacoinTestnetSegWit(ViacoinTestnet):
# Source: namecoin.org
class Namecoin(CoinAuxPow):
class Namecoin(AuxPowMixin, Coin):
NAME = "Namecoin"
SHORTNAME = "NMC"
NET = "mainnet"
@ -527,8 +523,6 @@ class NamecoinTestnet(Namecoin):
NAME = "Namecoin"
SHORTNAME = "XNM"
NET = "testnet"
XPUB_VERBYTES = bytes.fromhex("043587cf")
XPRV_VERBYTES = bytes.fromhex("04358394")
P2PKH_VERBYTE = bytes.fromhex("6f")
P2SH_VERBYTES = [bytes.fromhex("c4")]
WIF_BYTE = bytes.fromhex("ef")
@ -536,7 +530,7 @@ class NamecoinTestnet(Namecoin):
'a4cccff2a4767a8eee39c11db367b008')
class Dogecoin(CoinAuxPow):
class Dogecoin(AuxPowMixin, Coin):
NAME = "Dogecoin"
SHORTNAME = "DOGE"
NET = "mainnet"
@ -559,8 +553,6 @@ class DogecoinTestnet(Dogecoin):
NAME = "Dogecoin"
SHORTNAME = "XDT"
NET = "testnet"
XPUB_VERBYTES = bytes.fromhex("043587cf")
XPRV_VERBYTES = bytes.fromhex("04358394")
P2PKH_VERBYTE = bytes.fromhex("71")
P2SH_VERBYTES = [bytes.fromhex("c4")]
WIF_BYTE = bytes.fromhex("f1")
@ -570,8 +562,6 @@ class DogecoinTestnet(Dogecoin):
# Source: https://github.com/dashpay/dash
class Dash(Coin):
from server.session import DashElectrumX
from server.daemon import DashDaemon
NAME = "Dash"
SHORTNAME = "DASH"
NET = "mainnet"
@ -627,12 +617,10 @@ class DashTestnet(Dash):
]
class Argentum(CoinAuxPow):
class Argentum(AuxPowMixin, Coin):
NAME = "Argentum"
SHORTNAME = "ARG"
NET = "mainnet"
XPUB_VERBYTES = bytes.fromhex("0488b21e")
XPRV_VERBYTES = bytes.fromhex("0488ade4")
P2PKH_VERBYTE = bytes.fromhex("17")
P2SH_VERBYTES = [bytes.fromhex("05")]
WIF_BYTE = bytes.fromhex("97")
@ -649,8 +637,6 @@ class Argentum(CoinAuxPow):
class ArgentumTestnet(Argentum):
SHORTNAME = "XRG"
NET = "testnet"
XPUB_VERBYTES = bytes.fromhex("043587cf")
XPRV_VERBYTES = bytes.fromhex("04358394")
P2PKH_VERBYTE = bytes.fromhex("6f")
P2SH_VERBYTES = [bytes.fromhex("c4")]
WIF_BYTE = bytes.fromhex("ef")
@ -661,8 +647,6 @@ class DigiByte(Coin):
NAME = "DigiByte"
SHORTNAME = "DGB"
NET = "mainnet"
XPUB_VERBYTES = bytes.fromhex("0488b21e")
XPRV_VERBYTES = bytes.fromhex("0488ade4")
P2PKH_VERBYTE = bytes.fromhex("1E")
P2SH_VERBYTES = [bytes.fromhex("05")]
WIF_BYTE = bytes.fromhex("80")
@ -679,8 +663,6 @@ class DigiByte(Coin):
class DigiByteTestnet(DigiByte):
NET = "testnet"
XPUB_VERBYTES = bytes.fromhex("043587cf")
XPRV_VERBYTES = bytes.fromhex("04358394")
P2PKH_VERBYTE = bytes.fromhex("6f")
P2SH_VERBYTES = [bytes.fromhex("c4")]
WIF_BYTE = bytes.fromhex("ef")
@ -696,8 +678,6 @@ class FairCoin(Coin):
NAME = "FairCoin"
SHORTNAME = "FAIR"
NET = "mainnet"
XPUB_VERBYTES = bytes.fromhex("0488b21e")
XPRV_VERBYTES = bytes.fromhex("0488ade4")
P2PKH_VERBYTE = bytes.fromhex("5f")
P2SH_VERBYTES = [bytes.fromhex("24")]
WIF_BYTE = bytes.fromhex("df")
@ -745,8 +725,6 @@ class Zcash(Coin):
NAME = "Zcash"
SHORTNAME = "ZEC"
NET = "mainnet"
XPUB_VERBYTES = bytes.fromhex("0488b21e")
XPRV_VERBYTES = bytes.fromhex("0488ade4")
P2PKH_VERBYTE = bytes.fromhex("1CB8")
P2SH_VERBYTES = [bytes.fromhex("1CBD")]
WIF_BYTE = bytes.fromhex("80")
@ -789,9 +767,6 @@ class Einsteinium(Coin):
NAME = "Einsteinium"
SHORTNAME = "EMC2"
NET = "mainnet"
# TODO add correct values for XPUB, XPRIV
XPUB_VERBYTES = bytes.fromhex("0488b21e")
XPRV_VERBYTES = bytes.fromhex("0488ade4")
P2PKH_VERBYTE = bytes.fromhex("21")
P2SH_VERBYTES = [bytes.fromhex("05")]
WIF_BYTE = bytes.fromhex("a1")
@ -810,8 +785,6 @@ class Blackcoin(Coin):
NAME = "Blackcoin"
SHORTNAME = "BLK"
NET = "mainnet"
XPUB_VERBYTES = bytes.fromhex("0488B21E")
XPRV_VERBYTES = bytes.fromhex("0488ADE4")
P2PKH_VERBYTE = bytes.fromhex("19")
P2SH_VERBYTES = [bytes.fromhex("55")]
WIF_BYTE = bytes.fromhex("99")
@ -866,8 +839,6 @@ class Reddcoin(Coin):
NAME = "Reddcoin"
SHORTNAME = "RDD"
NET = "mainnet"
XPUB_VERBYTES = bytes.fromhex("0488B21E")
XPRV_VERBYTES = bytes.fromhex("0488ADE4")
P2PKH_VERBYTE = bytes.fromhex("3d")
P2SH_VERBYTES = [bytes.fromhex("05")]
WIF_BYTE = bytes.fromhex("bd")

367
tests/wallet/test_bip32.py

@ -0,0 +1,367 @@
#
# Tests of wallet/bip32.py
#
import pytest
import wallet.bip32 as bip32
from lib.coins import Bitcoin, CoinError
from lib.hash import Base58
MXPRV = 'xprv9s21ZrQH143K2gMVrSwwojnXigqHgm1khKZGTCm7K8w4PmuDEUrudk11ZBxhGPUiUeVcrfGLoZmt8rFNRDLp18jmKMcVma89z7PJd2Vn7R9'
MPRIVKEY = b';\xf4\xbfH\xd20\xea\x94\x01_\x10\x1b\xc3\xb0\xff\xc9\x17$?K\x02\xe5\x82R\xe5\xb3A\xdb\x87&E\x00'
MXPUB = 'xpub661MyMwAqRbcFARxxUUxAsjGGifn6Djc4YUsFbAisUU3GaEMn2BABYKVQTHrDtwvSfgY2bK8aFGyCNmB52SKjkFGP18sSRTNn1sCeez7Utd'
mpubkey, mpubcoin = bip32.from_extended_key_string(MXPUB)
mprivkey, mprivcoin = bip32.from_extended_key_string(MXPRV)
def test_from_extended_key():
# Tests the failure modes of from_extended_key.
with pytest.raises(TypeError):
bip32._from_extended_key('')
with pytest.raises(ValueError):
bip32._from_extended_key(b'')
with pytest.raises(CoinError):
bip32._from_extended_key(bytes(78))
# Invalid prefix byte
raw = Base58.decode_check(MXPRV)
with pytest.raises(ValueError):
bip32._from_extended_key(raw[:45] + b'\1' + raw[46:])
class TestPubKey(object):
def test_constructor(self):
cls = bip32.PubKey
raw_pubkey = b'\2' * 33
chain_code = bytes(32)
# Invalid constructions
with pytest.raises(TypeError):
cls(' ' * 33, chain_code, 0, 0)
with pytest.raises(ValueError):
cls(bytes(32), chain_code, -1, 0)
with pytest.raises(ValueError):
cls(bytes(33), chain_code, -1, 0)
with pytest.raises(ValueError):
cls(chain_code, chain_code, 0, 0)
with pytest.raises(TypeError):
cls(raw_pubkey, '0' * 32, 0, 0)
with pytest.raises(ValueError):
cls(raw_pubkey, bytes(31), 0, 0)
with pytest.raises(ValueError):
cls(raw_pubkey, chain_code, -1, 0)
with pytest.raises(ValueError):
cls(raw_pubkey, chain_code, 1 << 32, 0)
with pytest.raises(ValueError):
cls(raw_pubkey, chain_code, 0, -1)
with pytest.raises(ValueError):
cls(raw_pubkey, chain_code, 0, 256)
# These are OK
cls(b'\2' + b'\2' * 32, chain_code, 0, 0)
cls(b'\3' + b'\2' * 32, chain_code, 0, 0)
cls(raw_pubkey, chain_code, (1 << 32) - 1, 0)
cls(raw_pubkey, chain_code, 0, 255)
cls(raw_pubkey, chain_code, 0, 255, mpubkey)
# Construction from verifying key
dup = cls(mpubkey.verifying_key, chain_code, 0, 0)
assert mpubkey.ec_point() == dup.ec_point()
# Construction from raw pubkey bytes
pubkey = mpubkey.pubkey_bytes
dup = cls(pubkey, chain_code, 0, 0)
assert mpubkey.ec_point() == dup.ec_point()
# Construction from PubKey
with pytest.raises(TypeError):
cls(mpubkey, chain_code, 0, 0)
def test_from_extended_key_string(self):
assert mpubcoin == Bitcoin
assert mpubkey.n == 0
assert mpubkey.depth == 0
assert mpubkey.parent is None
assert mpubkey.chain_code == b'>V\x83\x92`\r\x17\xb3"\xa6\x7f\xaf\xc0\x930\xf7\x1e\xdc\x12i\x9c\xe4\xc0,a\x1a\x04\xec\x16\x19\xaeK'
assert mpubkey.ec_point().x() == 44977109961578369385937116592536468905742111247230478021459394832226142714624
def test_extended_key_string(self):
# Implictly tests extended_key()
assert mpubkey.extended_key_string(Bitcoin) == MXPUB
chg_master = mpubkey.child(1)
chg5 = chg_master.child(5)
assert chg5.address(Bitcoin) == '1BsEFqGtcZnVBbPeimcfAFTitQdTLvUXeX'
assert chg5.extended_key_string(Bitcoin) == 'xpub6AzPNZ1SAS7zmSnj6gakQ6tAKPzRVdQzieL3eCnoeT3A89nJaJKuUYWoZuYp8xWhCs1gF9yXAwGg7zKYhvCfhk9jrb1bULhLkQCwtB1Nnn1'
ext_key_base58 = chg5.extended_key_string(Bitcoin)
assert ext_key_base58 == 'xpub6AzPNZ1SAS7zmSnj6gakQ6tAKPzRVdQzieL3eCnoeT3A89nJaJKuUYWoZuYp8xWhCs1gF9yXAwGg7zKYhvCfhk9jrb1bULhLkQCwtB1Nnn1'
# Check can recreate
dup, coin = bip32.from_extended_key_string(ext_key_base58)
assert coin is Bitcoin
assert dup.chain_code == chg5.chain_code
assert dup.n == chg5.n == 5
assert dup.depth == chg5.depth == 2
assert dup.ec_point() == chg5.ec_point()
def test_child(self):
'''Test child derivations agree with Electrum.'''
rec_master = mpubkey.child(0)
assert rec_master.address(Bitcoin) == '18zW4D1Vxx9jVPGzsFzgXj8KrSLHt7w2cg'
chg_master = mpubkey.child(1)
assert chg_master.parent is mpubkey
assert chg_master.address(Bitcoin) == '1G8YpbkZd7bySHjpdQK3kMcHhc6BvHr5xy'
rec0 = rec_master.child(0)
assert rec0.address(Bitcoin) == '13nASW7rdE2dnSycrAP9VePhRmaLg9ziaw'
rec19 = rec_master.child(19)
assert rec19.address(Bitcoin) == '15QrXnPQ8aS8yCpA5tJkyvXfXpw8F8k3fB'
chg0 = chg_master.child(0)
assert chg0.parent is chg_master
assert chg0.address(Bitcoin) == '1L6fNSVhWjuMKNDigA99CweGEWtcqqhzDj'
with pytest.raises(ValueError):
mpubkey.child(-1)
with pytest.raises(ValueError):
mpubkey.child(1 << 31)
# OK
mpubkey.child((1 << 31) - 1)
def test_address(self):
assert mpubkey.address(Bitcoin) == '1ENCpq6mbb1KYcaodGG7eTpSpYvPnDjFmU'
def test_identifier(self):
assert mpubkey.identifier() == b'\x92\x9c=\xb8\xd6\xe7\xebR\x90Td\x85\x1c\xa7\x0c\x8aE`\x87\xdd'
def test_fingerprint(self):
assert mpubkey.fingerprint() == b'\x92\x9c=\xb8'
def test_parent_fingerprint(self):
assert mpubkey.parent_fingerprint() == bytes(4)
child = mpubkey.child(0)
assert child.parent_fingerprint() == mpubkey.fingerprint()
def test_pubkey_bytes(self):
# Also tests _exponent_to_bytes
pubkey = mpubkey.pubkey_bytes
assert pubkey == b'\x02cp$a\x18\xa7\xc2\x18\xfdUt\x96\xeb\xb2\xb0\x86-Y\xc6Hn\x88\xf8>\x07\xfd\x12\xce\x8a\x88\xfb\x00'
class TestPrivKey(object):
def test_constructor(self):
# Includes full tests of _signing_key_from_privkey and
# _privkey_secret_exponent
cls = bip32.PrivKey
chain_code = bytes(32)
# These are invalid
with pytest.raises(TypeError):
cls('0' * 32, chain_code, 0, 0)
with pytest.raises(ValueError):
cls(b'0' * 31, chain_code, 0, 0)
with pytest.raises(ValueError):
cls(MPRIVKEY, chain_code, -1, 0)
with pytest.raises(ValueError):
cls(MPRIVKEY, chain_code, 1 << 32, 0)
with pytest.raises(ValueError):
cls(MPRIVKEY, chain_code, 0, -1)
with pytest.raises(ValueError):
cls(MPRIVKEY, chain_code, 0, 256)
# Invalid exponents
with pytest.raises(ValueError):
cls(bip32._exponent_to_bytes(0), chain_code, 0, 0)
with pytest.raises(ValueError):
cls(bip32._exponent_to_bytes(cls.CURVE.order), chain_code, 0, 0)
# These are good
cls(MPRIVKEY, chain_code, 0, 0)
cls(MPRIVKEY, chain_code, (1 << 32) - 1, 0)
cls(MPRIVKEY, chain_code, 0, 0)
cls(bip32._exponent_to_bytes(cls.CURVE.order - 1), chain_code, 0, 0)
privkey = cls(MPRIVKEY, chain_code, 0, 255)
# Construction from signing key
dup = cls(privkey.signing_key, chain_code, 0, 0)
assert dup.ec_point() == privkey.ec_point()
# Construction from PrivKey
with pytest.raises(TypeError):
cls(privkey, chain_code, 0, 0)
def test_secret_exponent(self):
assert mprivkey.secret_exponent() == 27118888947022743980605817563635166434451957861641813930891160184742578898176
def test_identifier(self):
assert mprivkey.identifier() == mpubkey.identifier()
def test_address(self):
assert mprivkey.address(Bitcoin) == mpubkey.address(Bitcoin)
def test_fingerprint(self):
assert mprivkey.fingerprint() == mpubkey.fingerprint()
def test_parent_fingerprint(self):
assert mprivkey.parent_fingerprint() == bytes(4)
child = mprivkey.child(0)
assert child.parent_fingerprint() == mprivkey.fingerprint()
def test_from_extended_key_string(self):
# Also tests privkey_bytes and public_key
assert mprivcoin is Bitcoin
assert mprivkey.privkey_bytes == MPRIVKEY
assert mprivkey.ec_point() == mpubkey.ec_point()
assert mprivkey.public_key.chain_code == mpubkey.chain_code
assert mprivkey.public_key.n == mpubkey.n
assert mprivkey.public_key.depth == mpubkey.depth
def test_extended_key_string(self):
# Also tests extended_key, WIF and privkey_bytes
assert mprivkey.extended_key_string(Bitcoin) == MXPRV
chg_master = mprivkey.child(1)
chg5 = chg_master.child(5)
assert chg5.WIF(Bitcoin) == 'L5kTYMuajTGWdYiMoD4V8k6LS4Bg3HFMA5UGTfxG9Wh7UKu9CHFC'
ext_key_base58 = chg5.extended_key_string(Bitcoin)
assert ext_key_base58 == 'xprv9x12y3UYL4ZhYxiFzf3k2xwRmN9w6Ah9MRQSqpPC67WBFMTA2m1evkCKidz7UYBa5i8QwxmU9Ju7giqEmcPRXKXwzgAJwssNeZNQLPT3LAY'
# Check can recreate
dup, coin = bip32.from_extended_key_string(ext_key_base58)
assert coin is Bitcoin
assert dup.chain_code == chg5.chain_code
assert dup.n == chg5.n == 5
assert dup.depth == chg5.depth == 2
assert dup.ec_point() == chg5.ec_point()
def test_child(self):
'''Test child derivations agree with Electrum.'''
# Also tests WIF, address
rec_master = mprivkey.child(0)
assert rec_master.address(Bitcoin) == '18zW4D1Vxx9jVPGzsFzgXj8KrSLHt7w2cg'
chg_master = mprivkey.child(1)
assert chg_master.parent is mprivkey
assert chg_master.address(Bitcoin) == '1G8YpbkZd7bySHjpdQK3kMcHhc6BvHr5xy'
rec0 = rec_master.child(0)
assert rec0.WIF(Bitcoin) == 'L2M6WWMdu3YfWxvLGF76HZgHCA6idwVQx5QL91vfdqeZi8XAgWkz'
rec19 = rec_master.child(19)
assert rec19.WIF(Bitcoin) == 'KwMHa1fynU2J2iBGCuBZxumM2qDXHe5tVPU9VecNGQv3UCqnET7X'
chg0 = chg_master.child(0)
assert chg0.parent is chg_master
assert chg0.WIF(Bitcoin) == 'L4J1esD4rYuBHXwjg72yi7Rw4G3iF2yUHt7LN9trpC3snCppUbq8'
with pytest.raises(ValueError):
mprivkey.child(-1)
with pytest.raises(ValueError):
mprivkey.child(1 << 32)
# OK
mprivkey.child((1 << 32) - 1)
class TestVectors():
def test_vector1(self):
seed = bytes.fromhex("000102030405060708090a0b0c0d0e0f")
# Chain m
m = bip32.PrivKey.from_seed(seed)
xprv = m.extended_key_string(Bitcoin)
assert xprv == "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"
xpub = m.public_key.extended_key_string(Bitcoin)
assert xpub == "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"
# Chain m/0H
m1 = m.child(0 + m.HARDENED)
xprv = m1.extended_key_string(Bitcoin)
assert xprv == "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7"
xpub = m1.public_key.extended_key_string(Bitcoin)
assert xpub == "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw"
# Chain m/0H/1
m2 = m1.child(1)
xprv = m2.extended_key_string(Bitcoin)
assert xprv == "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs"
xpub = m2.public_key.extended_key_string(Bitcoin)
assert xpub == "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ"
# Chain m/0H/1/2H
m3 = m2.child(2 + m.HARDENED)
xprv = m3.extended_key_string(Bitcoin)
assert xprv == "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM"
xpub = m3.public_key.extended_key_string(Bitcoin)
assert xpub == "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"
# Chain m/0H/1/2H/2
m4 = m3.child(2)
xprv = m4.extended_key_string(Bitcoin)
assert xprv == "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334"
xpub = m4.public_key.extended_key_string(Bitcoin)
assert xpub == "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV"
# Chain m/0H/1/2H/2/1000000000
m5 = m4.child(1000000000)
xprv = m5.extended_key_string(Bitcoin)
assert xprv == "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76"
xpub = m5.public_key.extended_key_string(Bitcoin)
assert xpub == "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy"
def test_vector2(self):
seed = bytes.fromhex("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542")
# Chain m
m = bip32.PrivKey.from_seed(seed)
xprv = m.extended_key_string(Bitcoin)
assert xprv == "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U"
xpub = m.public_key.extended_key_string(Bitcoin)
assert xpub == "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"
# Chain m/0
m1 = m.child(0)
xprv = m1.extended_key_string(Bitcoin)
assert xprv == "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt"
xpub = m1.public_key.extended_key_string(Bitcoin)
assert xpub == "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH"
# Chain m/0H/2147483647H
m2 = m1.child(2147483647 + m.HARDENED)
xprv = m2.extended_key_string(Bitcoin)
assert xprv == "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9"
xpub = m2.public_key.extended_key_string(Bitcoin)
assert xpub == "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a"
# Chain m/0H/2147483647H/1
m3 = m2.child(1)
xprv = m3.extended_key_string(Bitcoin)
xpub = m3.public_key.extended_key_string(Bitcoin)
assert xprv == "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef"
assert xpub == "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon"
# Chain m/0/2147483647H/1/2147483646H
m4 = m3.child(2147483646 + m.HARDENED)
xprv = m4.extended_key_string(Bitcoin)
xpub = m4.public_key.extended_key_string(Bitcoin)
assert xprv == "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc"
assert xpub == "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"
# Chain m/0/2147483647H/1/2147483646H/2
m5 = m4.child(2)
xprv = m5.extended_key_string(Bitcoin)
xpub = m5.public_key.extended_key_string(Bitcoin)
assert xprv == "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j"
assert xpub == "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt"
def test_vector3(self):
seed = bytes.fromhex("4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be")
# Chain m
m = bip32.PrivKey.from_seed(seed)
xprv = m.extended_key_string(Bitcoin)
xpub = m.public_key.extended_key_string(Bitcoin)
assert xprv == "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6"
assert xpub == "xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13"
# Chain m/0H
m1 = m.child(0 + m.HARDENED)
xprv = m1.extended_key_string(Bitcoin)
xpub = m1.public_key.extended_key_string(Bitcoin)
assert xprv == "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L"
assert xpub == "xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y"

307
wallet/bip32.py

@ -0,0 +1,307 @@
# Copyright (c) 2017, Neil Booth
#
# All rights reserved.
#
# See the file "LICENCE" for information about the copyright
# and warranty status of this software.
'''Logic for BIP32 Hierarchical Key Derviation.'''
import struct
import ecdsa
import ecdsa.ellipticcurve as EC
import ecdsa.numbertheory as NT
from lib.coins import Coin
from lib.hash import Base58, hmac_sha512, hash160
from lib.util import cachedproperty, bytes_to_int, int_to_bytes
class DerivationError(Exception):
'''Raised when an invalid derivation occurs.'''
class _KeyBase(object):
'''A BIP32 Key, public or private.'''
CURVE = ecdsa.SECP256k1
def __init__(self, chain_code, n, depth, parent):
if not isinstance(chain_code, (bytes, bytearray)):
raise TypeError('chain code must be raw bytes')
if len(chain_code) != 32:
raise ValueError('invalid chain code')
if not 0 <= n < 1 << 32:
raise ValueError('invalid child number')
if not 0 <= depth < 256:
raise ValueError('invalid depth')
if parent is not None:
if not isinstance(parent, type(self)):
raise TypeError('parent key has bad type')
self.chain_code = chain_code
self.n = n
self.depth = depth
self.parent = parent
def _hmac_sha512(self, msg):
'''Use SHA-512 to provide an HMAC, returned as a pair of 32-byte
objects.
'''
hmac = hmac_sha512(self.chain_code, msg)
return hmac[:32], hmac[32:]
def _extended_key(self, ver_bytes, raw_serkey):
'''Return the 78-byte extended key given prefix version bytes and
serialized key bytes.
'''
if not isinstance(ver_bytes, (bytes, bytearray)):
raise TypeError('ver_bytes must be raw bytes')
if len(ver_bytes) != 4:
raise ValueError('ver_bytes must have length 4')
if not isinstance(raw_serkey, (bytes, bytearray)):
raise TypeError('raw_serkey must be raw bytes')
if len(raw_serkey) != 33:
raise ValueError('raw_serkey must have length 33')
return (ver_bytes + bytes([self.depth])
+ self.parent_fingerprint() + struct.pack('>I', self.n)
+ self.chain_code + raw_serkey)
def fingerprint(self):
'''Return the key's fingerprint as 4 bytes.'''
return self.identifier()[:4]
def parent_fingerprint(self):
'''Return the parent key's fingerprint as 4 bytes.'''
return self.parent.fingerprint() if self.parent else bytes(4)
def extended_key_string(self, coin):
'''Return an extended key as a base58 string.'''
return Base58.encode_check(self.extended_key(coin))
class PubKey(_KeyBase):
'''A BIP32 public key.'''
def __init__(self, pubkey, chain_code, n, depth, parent=None):
super().__init__(chain_code, n, depth, parent)
if isinstance(pubkey, ecdsa.VerifyingKey):
self.verifying_key = pubkey
else:
self.verifying_key = self._verifying_key_from_pubkey(pubkey)
self.addresses = {}
@classmethod
def _verifying_key_from_pubkey(cls, pubkey):
'''Converts a 33-byte compressed pubkey into an ecdsa.VerifyingKey
object'''
if not isinstance(pubkey, (bytes, bytearray)):
raise TypeError('pubkey must be raw bytes')
if len(pubkey) != 33:
raise ValueError('pubkey must be 33 bytes')
if pubkey[0] not in (2, 3):
raise ValueError('invalid pubkey prefix byte')
curve = cls.CURVE.curve
is_odd = pubkey[0] == 3
x = bytes_to_int(pubkey[1:])
# p is the finite field order
a, b, p = curve.a(), curve.b(), curve.p()
y2 = pow(x, 3, p) + b
if a:
y2 += a * pow(x, 2, p)
y = NT.square_root_mod_prime(y2 % p, p)
if bool(y & 1) != is_odd:
y = p - y
point = EC.Point(curve, x, y)
return ecdsa.VerifyingKey.from_public_point(point, curve=cls.CURVE)
@cachedproperty
def pubkey_bytes(self):
'''Return the compressed public key as 33 bytes.'''
point = self.verifying_key.pubkey.point
prefix = bytes([2 + (point.y() & 1)])
padded_bytes = _exponent_to_bytes(point.x())
return prefix + padded_bytes
def address(self, coin):
"The public key as a P2PKH address"
address = self.addresses.get(coin)
if not address:
address = coin.P2PKH_address_from_pubkey(self.pubkey_bytes)
self.addresses[coin] = address
return address
def ec_point(self):
return self.verifying_key.pubkey.point
def child(self, n):
'''Return the derived child extended pubkey at index N.'''
if not 0 <= n < (1 << 31):
raise ValueError('invalid BIP32 public key child number')
msg = self.pubkey_bytes + struct.pack('>I', n)
L, R = self._hmac_sha512(msg)
curve = self.CURVE
L = bytes_to_int(L)
if L >= curve.order:
raise DerivationError
point = curve.generator * L + self.ec_point()
if point == EC.INFINITY:
raise DerivationError
verkey = ecdsa.VerifyingKey.from_public_point(point, curve=curve)
return PubKey(verkey, R, n, self.depth + 1, self)
def identifier(self):
'''Return the key's identifier as 20 bytes.'''
return hash160(self.pubkey_bytes)
def extended_key(self, coin):
'''Return a raw extended public key.'''
return self._extended_key(coin.XPUB_VERBYTES, self.pubkey_bytes)
class PrivKey(_KeyBase):
'''A BIP32 private key.'''
HARDENED = 1 << 31
def __init__(self, privkey, chain_code, n, depth, parent=None):
super().__init__(chain_code, n, depth, parent)
if isinstance(privkey, ecdsa.SigningKey):
self.signing_key = privkey
else:
self.signing_key = self._signing_key_from_privkey(privkey)
@classmethod
def _signing_key_from_privkey(cls, privkey):
'''Converts a 32-byte privkey into an ecdsa.SigningKey object.'''
exponent = cls._privkey_secret_exponent(privkey)
return ecdsa.SigningKey.from_secret_exponent(exponent, curve=cls.CURVE)
@classmethod
def _privkey_secret_exponent(cls, privkey):
'''Return the private key as a secret exponent if it is a valid private
key.'''
if not isinstance(privkey, (bytes, bytearray)):
raise TypeError('privkey must be raw bytes')
if len(privkey) != 32:
raise ValueError('privkey must be 32 bytes')
exponent = bytes_to_int(privkey)
if not 1 <= exponent < cls.CURVE.order:
raise ValueError('privkey represents an invalid exponent')
return exponent
@classmethod
def from_seed(cls, seed):
# This hard-coded message string seems to be coin-independent...
hmac = hmac_sha512(b'Bitcoin seed', seed)
privkey, chain_code = hmac[:32], hmac[32:]
return cls(privkey, chain_code, 0, 0)
@cachedproperty
def privkey_bytes(self):
'''Return the serialized private key (no leading zero byte).'''
return _exponent_to_bytes(self.secret_exponent())
@cachedproperty
def public_key(self):
'''Return the corresponding extended public key.'''
verifying_key = self.signing_key.get_verifying_key()
parent_pubkey = self.parent.public_key if self.parent else None
return PubKey(verifying_key, self.chain_code, self.n, self.depth,
parent_pubkey)
def ec_point(self):
return self.public_key.ec_point()
def secret_exponent(self):
'''Return the private key as a secret exponent.'''
return self.signing_key.privkey.secret_multiplier
def WIF(self, coin):
'''Return the private key encoded in Wallet Import Format.'''
return coin.privkey_WIF(self.privkey_bytes, compressed=True)
def address(self, coin):
"The public key as a P2PKH address"
return self.public_key.address(coin)
def child(self, n):
'''Return the derived child extended privkey at index N.'''
if not 0 <= n < (1 << 32):
raise ValueError('invalid BIP32 private key child number')
if n >= self.HARDENED:
serkey = b'\0' + self.privkey_bytes
else:
serkey = self.public_key.pubkey_bytes
msg = serkey + struct.pack('>I', n)
L, R = self._hmac_sha512(msg)
curve = self.CURVE
L = bytes_to_int(L)
exponent = (L + bytes_to_int(self.privkey_bytes)) % curve.order
if exponent == 0 or L >= curve.order:
raise DerivationError
privkey = _exponent_to_bytes(exponent)
return PrivKey(privkey, R, n, self.depth + 1, self)
def identifier(self):
'''Return the key's identifier as 20 bytes.'''
return self.public_key.identifier()
def extended_key(self, coin):
'''Return a raw extended private key.'''
return self._extended_key(coin.XPRV_VERBYTES,
b'\0' + self.privkey_bytes)
def _exponent_to_bytes(exponent):
'''Convert an exponent to 32 big-endian bytes'''
return (bytes(32) + int_to_bytes(exponent))[-32:]
def _from_extended_key(ekey):
'''Return a PubKey or PrivKey from an extended key raw bytes.'''
if not isinstance(ekey, (bytes, bytearray)):
raise TypeError('extended key must be raw bytes')
if len(ekey) != 78:
raise ValueError('extended key must have length 78')
is_public, coin = Coin.lookup_xverbytes(ekey[:4])
depth = ekey[4]
fingerprint = ekey[5:9] # Not used
n, = struct.unpack('>I', ekey[9:13])
chain_code = ekey[13:45]
if is_public:
pubkey = ekey[45:]
key = PubKey(pubkey, chain_code, n, depth)
else:
if ekey[45] is not 0:
raise ValueError('invalid extended private key prefix byte')
privkey = ekey[46:]
key = PrivKey(privkey, chain_code, n, depth)
return key, coin
def from_extended_key_string(ekey_str):
'''Given an extended key string, such as
xpub6BsnM1W2Y7qLMiuhi7f7dbAwQZ5Cz5gYJCRzTNainXzQXYjFwtuQXHd
3qfi3t3KJtHxshXezfjft93w4UE7BGMtKwhqEHae3ZA7d823DVrL
return a (key, coin) pair. key is either a PubKey or PrivKey.
'''
return _from_extended_key(Base58.decode_check(ekey_str))
Loading…
Cancel
Save