├── keychain ├── __init__.py ├── configs.py ├── utils.py ├── private_keychain.py └── public_keychain.py ├── .gitignore ├── setup.py ├── LICENSE ├── README.md └── unit_tests.py /keychain/__init__.py: -------------------------------------------------------------------------------- 1 | from .private_keychain import PrivateKeychain 2 | from .public_keychain import PublicKeychain 3 | -------------------------------------------------------------------------------- /keychain/configs.py: -------------------------------------------------------------------------------- 1 | EXTENDED_PRIVATE_KEY_VERSION_BYTES = '\x04\x88\xad\xe4' # spells out 'xprv' 2 | EXTENDED_PUBLIC_KEY_VERSION_BYTES = '\x04\x88\xb2\x1e' # spells out 'xpub' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | keychain 4 | ============== 5 | 6 | """ 7 | 8 | from setuptools import setup, find_packages 9 | 10 | setup( 11 | name='keychain', 12 | version='0.14.2.0', 13 | url='https://github.com/blockstack/keychain-manager-py', 14 | license='MIT', 15 | author='Blockstack Developers', 16 | author_email='support@blockstack.com', 17 | description="""Library for BIP32 hierarchical deterministic keychains / wallets.""", 18 | keywords='bitcoin blockchain bip32 HD hierarchical deterministic keychain wallet', 19 | packages=find_packages(), 20 | zip_safe=False, 21 | install_requires=[ 22 | 'bitmerchant>=0.1.8', 23 | 'keylib>=0.1.0', 24 | ], 25 | classifiers=[ 26 | 'Intended Audience :: Developers', 27 | 'License :: OSI Approved :: MIT License', 28 | 'Operating System :: OS Independent', 29 | 'Programming Language :: Python', 30 | 'Topic :: Internet', 31 | 'Topic :: Security :: Cryptography', 32 | 'Topic :: Software Development :: Libraries :: Python Modules', 33 | ], 34 | ) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Blockstack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /keychain/utils.py: -------------------------------------------------------------------------------- 1 | from binascii import unhexlify 2 | from keylib.key_formatting import encode, decode, from_int_to_byte, from_byte_to_int, changebase, bin_dbl_sha256 3 | 4 | MAINNET_PRIVATE = b'\x04\x88\xAD\xE4' 5 | MAINNET_PUBLIC = b'\x04\x88\xB2\x1E' 6 | TESTNET_PRIVATE = b'\x04\x35\x83\x94' 7 | TESTNET_PUBLIC = b'\x04\x35\x87\xCF' 8 | PRIVATE = [MAINNET_PRIVATE, TESTNET_PRIVATE] 9 | PUBLIC = [MAINNET_PUBLIC, TESTNET_PUBLIC] 10 | 11 | def extract_bin_chain_path(chain_path): 12 | if len(chain_path) == 64: 13 | return unhexlify(chain_path) 14 | elif len(chain_path) == 32: 15 | return chain_path 16 | else: 17 | raise ValueError('Invalid chain path') 18 | 19 | def hash_to_int(x): 20 | if len(x) in [40, 64]: 21 | # decode as hex string 22 | return decode(x, 16) 23 | 24 | # decode as byte string 25 | return decode(x, 256) 26 | 27 | 28 | def bip32_serialize(rawtuple): 29 | """ 30 | Derived from code from pybitcointools (https://github.com/vbuterin/pybitcointools) 31 | by Vitalik Buterin 32 | """ 33 | vbytes, depth, fingerprint, i, chaincode, key = rawtuple 34 | i = encode(i, 256, 4) 35 | chaincode = encode(hash_to_int(chaincode), 256, 32) 36 | keydata = b'\x00' +key[:-1] if vbytes in PRIVATE else key 37 | bindata = vbytes + from_int_to_byte(depth % 256) + fingerprint + i + chaincode + keydata 38 | return changebase(bindata + bin_dbl_sha256(bindata)[:4], 256, 58) 39 | 40 | 41 | def bip32_deserialize(data): 42 | """ 43 | Derived from code from pybitcointools (https://github.com/vbuterin/pybitcointools) 44 | by Vitalik Buterin 45 | """ 46 | dbin = changebase(data, 58, 256) 47 | if bin_dbl_sha256(dbin[:-4])[:4] != dbin[-4:]: 48 | raise Exception("Invalid checksum") 49 | vbytes = dbin[0:4] 50 | depth = from_byte_to_int(dbin[4]) 51 | fingerprint = dbin[5:9] 52 | i = decode(dbin[9:13], 256) 53 | chaincode = dbin[13:45] 54 | key = dbin[46:78]+b'\x01' if vbytes in PRIVATE else dbin[45:78] 55 | return (vbytes, depth, fingerprint, i, chaincode, key) 56 | 57 | -------------------------------------------------------------------------------- /keychain/private_keychain.py: -------------------------------------------------------------------------------- 1 | from bitmerchant.wallet import Wallet as HDWallet 2 | from .public_keychain import PublicKeychain 3 | from binascii import unhexlify, hexlify 4 | from .utils import extract_bin_chain_path, bip32_serialize 5 | from .configs import EXTENDED_PRIVATE_KEY_VERSION_BYTES as version_bytes 6 | 7 | import keylib 8 | from keylib.key_formatting import encode_privkey as encode_private_key 9 | 10 | class PrivateKeychain(): 11 | def __init__(self, private_keychain=None): 12 | if private_keychain: 13 | if isinstance(private_keychain, HDWallet): 14 | self.hdkeychain = private_keychain 15 | elif isinstance(private_keychain, (str, unicode)): 16 | self.hdkeychain = HDWallet.deserialize(private_keychain) 17 | else: 18 | raise ValueError('private keychain must be a string') 19 | else: 20 | self.hdkeychain = HDWallet.new_random_wallet() 21 | 22 | def __str__(self): 23 | return self.hdkeychain.serialize_b58(private=True) 24 | 25 | def hardened_child(self, index): 26 | child_keychain = self.hdkeychain.get_child( 27 | index, is_prime=True, as_private=True) 28 | return PrivateKeychain(child_keychain) 29 | 30 | def child(self, index): 31 | child_keychain = self.hdkeychain.get_child( 32 | index, is_prime=False, as_private=True) 33 | return PrivateKeychain(child_keychain) 34 | 35 | def public_keychain(self): 36 | public_keychain = self.hdkeychain.public_copy() 37 | return PublicKeychain(public_keychain) 38 | 39 | def private_key(self, compressed=True): 40 | private_key = self.hdkeychain.get_private_key_hex() 41 | if compressed: 42 | private_key += '01' 43 | return private_key 44 | 45 | @classmethod 46 | def from_private_key(cls, private_key, chain_path='\x00'*32, depth=0, 47 | fingerprint='\x00'*4, child_index=0): 48 | private_key_bytes = encode_private_key(private_key, 'bin_compressed') 49 | chain_path = extract_bin_chain_path(chain_path) 50 | keychain_parts = (version_bytes, depth, fingerprint, 51 | child_index, chain_path, private_key_bytes) 52 | public_keychain_string = bip32_serialize(keychain_parts) 53 | return PrivateKeychain(public_keychain_string) 54 | -------------------------------------------------------------------------------- /keychain/public_keychain.py: -------------------------------------------------------------------------------- 1 | from bitmerchant.wallet import ( 2 | Wallet as HDWallet 3 | ) 4 | from binascii import hexlify, unhexlify 5 | from .utils import extract_bin_chain_path, bip32_serialize 6 | from .configs import EXTENDED_PUBLIC_KEY_VERSION_BYTES as version_bytes 7 | 8 | import keylib 9 | from keylib.key_formatting import encode_pubkey as encode_public_key 10 | 11 | class PublicKeychain(): 12 | def __init__(self, public_keychain): 13 | if isinstance(public_keychain, HDWallet): 14 | self.hdkeychain = public_keychain 15 | elif isinstance(public_keychain, (str, unicode)): 16 | self.hdkeychain = HDWallet.deserialize(public_keychain) 17 | else: 18 | raise ValueError('public keychain must be a string') 19 | 20 | def __str__(self): 21 | return self.hdkeychain.serialize_b58(private=False) 22 | 23 | def child(self, index): 24 | child_keychain = self.hdkeychain.get_child( 25 | index, is_prime=False, as_private=False) 26 | return PublicKeychain(child_keychain) 27 | 28 | def descendant(self, chain_path): 29 | """ A descendant is a child many steps down. 30 | """ 31 | public_child = self.hdkeychain 32 | chain_step_bytes = 4 33 | max_bits_per_step = 2**31 34 | chain_steps = [ 35 | int(chain_path[i:i+chain_step_bytes*2], 16) % max_bits_per_step 36 | for i in range(0, len(chain_path), chain_step_bytes*2) 37 | ] 38 | for step in chain_steps: 39 | public_child = public_child.get_child(step) 40 | 41 | return PublicKeychain(public_child) 42 | 43 | def public_key(self, compressed=True): 44 | return self.hdkeychain.get_public_key_hex(compressed=compressed) 45 | 46 | def address(self): 47 | return str(self.hdkeychain.to_address()) 48 | 49 | @classmethod 50 | def from_public_key(cls, public_key, chain_path='\x00'*32, depth=0, 51 | fingerprint='\x00'*4, child_index=0): 52 | public_key_bytes = encode_public_key(public_key, 'bin_compressed') 53 | chain_path = extract_bin_chain_path(chain_path) 54 | keychain_parts = (version_bytes, depth, fingerprint, 55 | child_index, chain_path, public_key_bytes) 56 | public_keychain_string = bip32_serialize(keychain_parts) 57 | return PublicKeychain(public_keychain_string) 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keychain Manager 2 | 3 | [![CircleCI](https://img.shields.io/circleci/project/blockstack/keychain-manager-py.svg)](https://pypi.python.org/pypi/keychain/) 4 | [![PyPI](https://img.shields.io/pypi/v/keychain.svg)](https://pypi.python.org/pypi/keychain/) 5 | [![PyPI](https://img.shields.io/pypi/dm/keychain.svg)](https://pypi.python.org/pypi/keychain/) 6 | [![PyPI](https://img.shields.io/pypi/l/keychain.svg)](https://pypi.python.org/pypi/keychain/) 7 | [![Slack](http://slack.blockstack.org/badge.svg)](http://slack.blockstack.org/) 8 | 9 | A key system (implemented in python) based around hierarchical deterministic (HD / BIP32) public and private keychains, each with ECDSA keypairs and the ability to generate child keys (the ones Bitcoin uses). 10 | 11 | ### Getting Started 12 | 13 | ``` 14 | pip install keychain 15 | ``` 16 | 17 | ```python 18 | >>> from keychain import PrivateKeychain, PublicKeychain 19 | ``` 20 | 21 | ### Private Keychains 22 | 23 | *Note: A private keychain is a BIP32 hierarchical determinstic extended private key.* 24 | 25 | ```python 26 | >>> private_keychain = PrivateKeychain("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi") 27 | >>> print private_keychain 28 | xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi 29 | >>> private_key = private_keychain.private_key() 30 | ``` 31 | 32 | ### Public Keychains 33 | 34 | *Note: A public keychain is a BIP32 hierarchical determinstic extended public key.* 35 | 36 | #### Public Keychains from Private Keychains 37 | 38 | ```python 39 | >>> public_keychain = private_keychain.public_keychain() 40 | >>> print public_keychain 41 | xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8 42 | >>> public_key = public_keychain.public_key() 43 | >>> address = public_keychain.address() 44 | ``` 45 | 46 | #### Public Keychains From Serialized Data 47 | 48 | ```python 49 | >>> public_keychain = PublicKeychain("xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8") 50 | ``` 51 | 52 | #### Public Keychains from Public Keys 53 | 54 | ```python 55 | >>> public_key = '032532502314356f83068bdbd283c86398d9ffd1308192474e6d3d6156eaf3d67f' 56 | >>> chain_path = '\x00'*32 57 | >>> public_keychain = PublicKeychain.from_public_key(public_key, chain_path) 58 | ``` 59 | 60 | You can also leave out the chain path like so: 61 | 62 | ```python 63 | >>> public_keychain = PublicKeychain.from_public_key(public_key) 64 | ``` 65 | 66 | ### Un-hardened Child Keychains 67 | 68 | Un-hardened keychains use the public key as a seed to derive the child. This means that you can derive the parent public keychain of a given public keychain that was derived in an un-hardened way. 69 | 70 | #### Private Un-hardened Child Keychains 71 | 72 | ```python 73 | >>> private_child = private_keychain.hardened_child(0).child(1) 74 | ``` 75 | 76 | #### Public Un-hardened Child Keychains 77 | 78 | ```python 79 | >>> public_child = private_child.public_keychain(0) 80 | >>> public_grandchild = public_child.child(1) 81 | ``` 82 | 83 | ### Hardened Child Keychains 84 | 85 | Hardened keychains use the private key as a seed to derive the child. This means that with a public keychain derived from a hardened private keychain, you can't derive the parent public keychain, as is the case with the un-hardened counterparts. 86 | 87 | #### Private Hardened Child Keychains 88 | 89 | ```python 90 | >>> private_hardened_child = private_keychain.hardened_child(0) 91 | ``` 92 | 93 | #### Public Hardened Child Keychains 94 | 95 | *Note: these do not exist, as a private key is required to harden a child.* 96 | -------------------------------------------------------------------------------- /unit_tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | import traceback 3 | import unittest 4 | from test import test_support 5 | from keychain import PrivateKeychain, PublicKeychain 6 | from keychain.utils import bip32_deserialize 7 | import keylib 8 | 9 | class BasicKeychainTest(unittest.TestCase): 10 | def setUp(self): 11 | self.private_keychains = { 12 | "root": "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", 13 | "0H": "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7", 14 | "0H/1": "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs", 15 | "0H/1/2H": "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM", 16 | "0H/1/2H/2": "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334", 17 | "0H/1/2H/2/1000000000": "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76" 18 | } 19 | self.public_keychains = { 20 | "root": "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", 21 | "0H": "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw", 22 | "0H/1": "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ", 23 | "0H/1/2H": "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5", 24 | "0H/1/2H/2": "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV", 25 | "0H/1/2H/2/1000000000": "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy" 26 | } 27 | self.root_private_keychain = PrivateKeychain(self.private_keychains["root"]) 28 | 29 | def tearDown(self): 30 | pass 31 | 32 | def test_root_private_to_public(self): 33 | public_keychain = self.root_private_keychain.public_keychain() 34 | self.assertEqual(str(public_keychain), str(self.public_keychains["root"])) 35 | 36 | def test_hardened_child_0H(self): 37 | private_keychain = self.root_private_keychain.hardened_child(0) 38 | self.assertEqual(str(private_keychain), str(self.private_keychains["0H"])) 39 | self.assertEqual(str(private_keychain.public_keychain()), str(self.public_keychains["0H"])) 40 | 41 | def test_unhardened_child_0H_1(self): 42 | private_keychain = self.root_private_keychain.hardened_child(0).child(1) 43 | self.assertEqual(str(private_keychain), str(self.private_keychains["0H/1"])) 44 | public_keychain = private_keychain.public_keychain() 45 | self.assertEqual(str(public_keychain), str(self.public_keychains["0H/1"])) 46 | public_keychain_2 = self.root_private_keychain.hardened_child(0).public_keychain().child(1) 47 | self.assertEqual(str(public_keychain), str(public_keychain_2)) 48 | 49 | def test_5_step_derivation(self): 50 | private_keychain = self.root_private_keychain.hardened_child(0).child(1).hardened_child(2).child(2).child(1000000000) 51 | self.assertEqual(str(private_keychain), str(self.private_keychains["0H/1/2H/2/1000000000"])) 52 | public_keychain = private_keychain.public_keychain() 53 | self.assertEqual(str(public_keychain), str(self.public_keychains["0H/1/2H/2/1000000000"])) 54 | 55 | def test_private_key(self): 56 | root_private_key = self.root_private_keychain.private_key() 57 | self.assertTrue(len(root_private_key) == 66) 58 | 59 | def test_address(self): 60 | address = self.root_private_keychain.public_keychain().address() 61 | self.assertTrue(address[0] == '1') 62 | 63 | 64 | class KeychainDescendantTest(unittest.TestCase): 65 | def setUp(self): 66 | self.public_keychain_string = 'xpub661MyMwAqRbcFQVrQr4Q4kPjaP4JjWaf39fBVKjPdK6oGBayE46GAmKzo5UDPQdLSM9DufZiP8eauy56XNuHicBySvZp7J5wsyQVpi2axzZ' 67 | self.public_keychain = PublicKeychain(self.public_keychain_string) 68 | self.chain_path = 'bd62885ec3f0e3838043115f4ce25eedd22cc86711803fb0c19601eeef185e39' 69 | self.reference_public_key = '03fdd57adec3d438ea237fe46b33ee1e016eda6b585c3e27ea66686c2ea5358479' 70 | 71 | def tearDown(self): 72 | pass 73 | 74 | def test_descendant(self): 75 | descendant_public_keychain = self.public_keychain.descendant(self.chain_path) 76 | descendant_public_key = descendant_public_keychain.public_key() 77 | self.assertEqual(descendant_public_key, self.reference_public_key) 78 | 79 | 80 | class KeychainDerivationTest(unittest.TestCase): 81 | def setUp(self): 82 | self.chain_path = 'bd62885ec3f0e3838043115f4ce25eedd22cc86711803fb0c19601eeef185e39' 83 | self.public_key_hex = '032532502314356f83068bdbd283c86398d9ffd1308192474e6d3d6156eaf3d67f' 84 | self.private_key_hex = 'e4557e22988ab073d4c605c4548577a3c87019198e514346c26c3cff5d546f7e01' 85 | self.reference_public_keychain = 'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6SJRqgVDtiwxFwbqpq3DhkYnpKaV7ShnnpTQTmQbf1gBWB5yEhw' 86 | self.reference_child_0_chaincode = 'y1N\x14\x1b\xbcZ\xfe;\x88\x96\xd4\xd8@(\xe8\xc3\xd6\x9fK\x1c\x04\xa6\t\xe6%\xadz\xefcB!' 87 | 88 | self.private_key_hex_2 = '0e1f04e0c9154cd880b4df17357516736d53d4d1a9875ae40643b3197dfb738c' 89 | self.public_key_hex_2 = '04ed34a7f541de185fdcbf8e1a9f169b6a9146b62b34172cbfce22c0667b58e795bc28b30b931713743260390da739584eca6729af0e8011be4e5e7fb42b13c4c9' 90 | self.reference_public_keychain_2 = 'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6TpWofxjzJQxPqLwhMi3YenYyEXtWUa55DzZZCyuZzjrrusaHDJ' 91 | 92 | self.public_key_hex_3 = '047c7f6d1f71780ccd373a7d2a020a1aeb7d47639e86fe951f5ba23a9ca8d6f7cfb03ed7ca411b22fa5244b9998d27d9c7bf7f0603f1997d1c7b3dc5a9b342c554' 93 | 94 | def tearDown(self): 95 | pass 96 | 97 | def test_derivation_from_raw_keys(self): 98 | public_keychain = PublicKeychain.from_public_key(self.public_key_hex) 99 | private_keychain = PrivateKeychain.from_private_key(self.private_key_hex) 100 | public_keychain_2 = private_keychain.public_keychain() 101 | self.assertEqual(str(public_keychain), str(public_keychain_2)) 102 | self.assertEqual(str(public_keychain), self.reference_public_keychain) 103 | 104 | def test_derivation_from_raw_uncompressed_keys(self): 105 | public_keychain = PublicKeychain.from_public_key(self.public_key_hex_2) 106 | private_keychain = PrivateKeychain.from_private_key(self.private_key_hex_2) 107 | public_keychain_2 = private_keychain.public_keychain() 108 | self.assertEqual(str(public_keychain), str(public_keychain_2)) 109 | self.assertEqual(str(public_keychain), self.reference_public_keychain_2) 110 | 111 | def test_child_generation(self): 112 | public_keychain = PublicKeychain.from_public_key(self.public_key_hex) 113 | public_keychain_child = public_keychain.child(0) 114 | keychain_parts = bip32_deserialize(str(public_keychain_child)) 115 | self.assertEqual(keychain_parts[4], self.reference_child_0_chaincode) 116 | 117 | 118 | class HighVolumeKeyDerivationTest(unittest.TestCase): 119 | def setUp(self): 120 | self.public_key_hex = '032532502314356f83068bdbd283c86398d9ffd1308192474e6d3d6156eaf3d67f' 121 | self.private_key_hex = 'e4557e22988ab073d4c605c4548577a3c87019198e514346c26c3cff5d546f7e01' 122 | 123 | def tearDown(self): 124 | pass 125 | 126 | def test_high_volume_derivation(self): 127 | number_of_keys = 10 128 | public_keychain = PublicKeychain.from_public_key(self.public_key_hex) 129 | private_keychain = PrivateKeychain.from_private_key(self.private_key_hex) 130 | keypairs = [] 131 | print "" 132 | for i in range(number_of_keys): 133 | print "making key %i of %i" % (i+1, number_of_keys) 134 | public_key = public_keychain.child(i).public_key() 135 | private_key = private_keychain.child(i).private_key() 136 | keypairs.append({ 'public': public_key, 'private': private_key }) 137 | 138 | for i in range(len(keypairs)): 139 | keypair = keypairs[i] 140 | print "checking key %i of %i" % (i+1, number_of_keys) 141 | # self.assertEqual(privkey_to_pubkey(keypair['private']), keypair['public']) 142 | self.assertEqual(keylib.ECPrivateKey(keypair['private']).public_key().to_hex(), keylib.ECPublicKey(keypair['public']).to_hex()) 143 | 144 | 145 | def test_main(): 146 | test_support.run_unittest( 147 | KeychainDerivationTest, 148 | BasicKeychainTest, 149 | KeychainDescendantTest, 150 | HighVolumeKeyDerivationTest 151 | ) 152 | 153 | 154 | if __name__ == '__main__': 155 | test_main() 156 | --------------------------------------------------------------------------------