diff --git a/lib/coins.py b/lib/coins.py index 82c48d6..40caa28 100644 --- a/lib/coins.py +++ b/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") diff --git a/tests/wallet/test_bip32.py b/tests/wallet/test_bip32.py new file mode 100644 index 0000000..0fd1eac --- /dev/null +++ b/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" diff --git a/wallet/bip32.py b/wallet/bip32.py new file mode 100644 index 0000000..048b046 --- /dev/null +++ b/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))