Neil Booth
8 years ago
3 changed files with 690 additions and 45 deletions
@ -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" |
@ -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…
Reference in new issue