Created
May 9, 2020 15:45
-
-
Save bjarnemagnussen/2fd7bc5ce07d35e92fe7e6f40a16e11e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# Based on bip32utils by Corgan Labs | |
# Reimplemented using the library coincurve in bit | |
# | |
import os | |
from binascii import b2a_hex | |
from . import base58 | |
from bit.crypto import hmac_sha512, hash160, ECPublicKey, ECPrivateKey | |
from bit.curve import ( | |
GROUP_ORDER as CURVE_ORDER, | |
FIELD_SIZE as FIELD_ORDER, | |
BASE_POINT_PARAMETERS, | |
x_to_y | |
) | |
BASE_POINT = ECPublicKey.from_point(*BASE_POINT_PARAMETERS) | |
MIN_ENTROPY_LEN = 128 # bits | |
BIP32_HARDEN = 0x80000000 # choose from hardened set of child keys | |
EX_MAIN_PRIVATE = [ bytes.fromhex('0488ade4'), bytes.fromhex('049d7878') ] # Version strings for mainnet extended private keys | |
EX_MAIN_PUBLIC = [ bytes.fromhex('0488b21e'), bytes.fromhex('049d7cb2') ] # Version strings for mainnet extended public keys | |
EX_TEST_PRIVATE = [ bytes.fromhex('04358394') ] # Version strings for testnet extended private keys | |
EX_TEST_PUBLIC = [ bytes.fromhex('043587CF') ] # Version strings for testnet extended public keys | |
class BIP32Key(object): | |
# Static initializers to create from entropy or external formats | |
# | |
@staticmethod | |
def fromEntropy(entropy, public=False, testnet=False): | |
"Create a BIP32Key using supplied entropy >= MIN_ENTROPY_LEN" | |
if entropy == None: | |
entropy = os.urandom(MIN_ENTROPY_LEN//8) # Python doesn't have os.random() | |
if not len(entropy) >= MIN_ENTROPY_LEN//8: | |
raise ValueError("Initial entropy %i must be at least %i bits" % | |
(len(entropy), MIN_ENTROPY_LEN)) | |
I = hmac_sha512(b"Bitcoin seed", entropy) | |
Il, Ir = I[:32], I[32:] | |
# FIXME test Il for 0 or less than SECP256k1 prime field order | |
key = BIP32Key(secret=Il, chain=Ir, depth=0, index=0, fpr=b'\0\0\0\0', public=False, testnet=testnet) | |
if public: | |
key.SetPublic() | |
return key | |
@staticmethod | |
def fromExtendedKey(xkey, public=False): | |
""" | |
Create a BIP32Key by importing from extended private or public key string | |
If public is True, return a public-only key regardless of input type. | |
""" | |
# Sanity checks | |
raw = base58.b58decode_check(xkey) | |
if len(raw) != 78: | |
raise ValueError("extended key format wrong length") | |
# Verify address version/type | |
version = raw[:4] | |
if version in EX_MAIN_PRIVATE: | |
is_testnet = False | |
is_pubkey = False | |
elif version in EX_TEST_PRIVATE: | |
is_testnet = True | |
is_pubkey = False | |
elif version in EX_MAIN_PUBLIC: | |
is_testnet = False | |
is_pubkey = True | |
elif version in EX_TEST_PUBLIC: | |
is_testnet = True | |
is_pubkey = True | |
else: | |
raise ValueError("unknown extended key version") | |
# Extract remaining fields | |
depth = raw[4] | |
fpr = raw[5:9] | |
child = int.from_bytes(raw[9:13], byteorder='big') | |
chain = raw[13:45] | |
secret = raw[45:78] | |
# Extract private key or public key point | |
if not is_pubkey: | |
secret = secret[1:] | |
else: | |
# Recover public curve point from compressed key | |
# Python3 FIX | |
lsb = secret[0] & 1 if type(secret[0]) == int else ord(secret[0]) & 1 | |
x = int.from_bytes(secret[1:], byteorder='big') | |
y = x_to_y(x, lsb) | |
secret = ECPublicKey.from_point(x, y) | |
key = BIP32Key(secret=secret, chain=chain, depth=depth, index=child, fpr=fpr, public=is_pubkey, testnet=is_testnet) | |
if not is_pubkey and public: | |
key = key.SetPublic() | |
return key | |
# Normal class initializer | |
def __init__(self, secret, chain, depth, index, fpr, public=False, testnet=False): | |
""" | |
Create a public or private BIP32Key using key material and chain code. | |
secret This is the source material to generate the keypair, either a | |
32-byte string representation of a private key, or the | |
coincurve library object representing a public key. | |
chain This is a 32-byte string representation of the chain code | |
depth Child depth; parent increments its own by one when assigning this | |
index Child index | |
fpr Parent fingerprint | |
public If true, this keypair will only contain a public key and can only create | |
a public key chain. | |
key = BIP32Key(secret=Il, chain=Ir, depth=0, index=0, fpr=b'\0\0\0\0', public=False, testnet=testnet) | |
""" | |
self.public = public | |
if public is False: | |
self.k = ECPrivateKey(secret=secret) | |
self.K = self.k.public_key | |
else: | |
self.k = None | |
self.K = secret | |
self.C = chain | |
self.depth = depth | |
self.index = index | |
self.parent_fpr = fpr | |
self.testnet = testnet | |
# Internal methods not intended to be called externally | |
# | |
def hmac(self, data): | |
""" | |
Calculate the HMAC-SHA512 of input data using the chain code as key. | |
Returns a tuple of the left and right halves of the HMAC | |
""" | |
I = hmac_sha512(self.C, data) | |
return (I[:32], I[32:]) | |
def CKDpriv(self, i): | |
""" | |
Create a child key of index 'i'. | |
If the most significant bit of 'i' is set, then select from the | |
hardened key set, otherwise, select a regular child key. | |
Returns a BIP32Key constructed with the child key parameters, | |
or None if i index would result in an invalid key. | |
""" | |
# Index as bytes, BE | |
i_str = (i).to_bytes(4, byteorder='big') | |
# Data to HMAC | |
if i & BIP32_HARDEN: | |
data = b'\0' + self.k.secret + i_str | |
else: | |
data = self.PublicKey() + i_str | |
# Get HMAC of data | |
(Il, Ir) = self.hmac(data) | |
# Construct new key material from Il and current private key | |
Il_int = int.from_bytes(Il, byteorder='big') | |
if Il_int > CURVE_ORDER: | |
return None | |
pvt_int = self.k.to_int() | |
k_int = (Il_int + pvt_int) % CURVE_ORDER | |
if (k_int == 0): | |
return None | |
secret = (k_int).to_bytes(32, byteorder='big') | |
# Construct and return a new BIP32Key | |
return BIP32Key(secret=secret, chain=Ir, depth=self.depth+1, index=i, fpr=self.Fingerprint(), public=False, testnet=self.testnet) | |
def CKDpub(self, i): | |
""" | |
Create a publicly derived child key of index 'i'. | |
If the most significant bit of 'i' is set, this is | |
an error. | |
Returns a BIP32Key constructed with the child key parameters, | |
or None if index would result in invalid key. | |
""" | |
if i & BIP32_HARDEN: | |
raise Exception("Cannot create a hardened child key using public child derivation") | |
# Data to HMAC. Same as CKDpriv() for public child key. | |
data = self.PublicKey() + (i).to_bytes(4, byteorder='big') | |
# Get HMAC of data | |
(Il, Ir) = self.hmac(data) | |
# Construct curve point Il*G+K | |
Il_int = int.from_bytes(Il, byteorder='big') | |
if Il_int >= CURVE_ORDER: | |
return None | |
Il_G = BASE_POINT.multiply(Il) | |
point = ECPublicKey.combine_keys(public_keys=[Il_G, self.K]).point() | |
# Retrieve public key based on curve point | |
K_i = ECPublicKey.from_point(*point) | |
# Construct and return a new BIP32Key | |
return BIP32Key(secret=K_i, chain=Ir, depth=self.depth+1, index=i, fpr=self.Fingerprint(), public=True, testnet=self.testnet) | |
# Public methods | |
# | |
def ChildKey(self, i): | |
""" | |
Create and return a child key of this one at index 'i'. | |
The index 'i' should be summed with BIP32_HARDEN to indicate | |
to use the private derivation algorithm. | |
""" | |
if self.public is False: | |
return self.CKDpriv(i) | |
else: | |
return self.CKDpub(i) | |
def SetPublic(self): | |
"Convert a private BIP32Key into a public one" | |
self.k = None | |
self.public = True | |
def PrivateKey(self): | |
"Return private key as string" | |
if self.public: | |
raise Exception("Publicly derived deterministic keys have no private half") | |
else: | |
return self.k.secret | |
def PublicKey(self): | |
"Return compressed public key encoding" | |
return self.K.format(compressed=True) | |
def ChainCode(self): | |
"Return chain code as string" | |
return self.C | |
def Identifier(self): | |
"Return key identifier as string" | |
cK = self.PublicKey() | |
return hash160(cK) | |
def Fingerprint(self): | |
"Return key fingerprint as string" | |
return self.Identifier()[:4] | |
def Address(self): | |
"Return compressed public key address" | |
addressversion = b'\x00' if not self.testnet else b'\x6f' | |
vh160 = addressversion + self.Identifier() | |
return base58.b58encode_check(vh160) | |
def P2WPKHoP2SHAddress(self): | |
"Return P2WPKH over P2SH segwit address" | |
pk_bytes = self.PublicKey() | |
assert len(pk_bytes) == 33 and (pk_bytes.startswith(b"\x02") or pk_bytes.startswith(b"\x03")), \ | |
"Only compressed public keys are compatible with p2sh-p2wpkh addresses. " \ | |
"See https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki." | |
pk_hash = hash160(pk_bytes) | |
push_20 = bytes.fromhex('0014') | |
script_sig = push_20 + pk_hash | |
address_bytes = hash160(script_sig) | |
prefix = b"\xc4" if self.testnet else b"\x05" | |
return base58.b58encode_check(prefix + address_bytes) | |
def WalletImportFormat(self): | |
"Returns private key encoded for wallet import" | |
if self.public: | |
raise Exception("Publicly derived deterministic keys have no private half") | |
addressversion = b'\x80' if not self.testnet else b'\xef' | |
raw = addressversion + self.k.secret + b'\x01' # Always compressed | |
return base58.b58encode_check(raw) | |
def ExtendedKey(self, private=True, encoded=True): | |
"Return extended private or public key as string, optionally Base58 encoded" | |
if self.public is True and private is True: | |
raise Exception("Cannot export an extended private key from a public-only deterministic key") | |
if not self.testnet: | |
version = EX_MAIN_PRIVATE[0] if private else EX_MAIN_PUBLIC[0] | |
else: | |
version = EX_TEST_PRIVATE[0] if private else EX_TEST_PUBLIC[0] | |
depth = bytes(bytearray([self.depth])) | |
fpr = self.parent_fpr | |
child = (self.inde).to_bytes(4, byteorder='big') | |
chain = self.C | |
if self.public is True or private is False: | |
data = self.PublicKey() | |
else: | |
data = b'\x00' + self.PrivateKey() | |
raw = version+depth+fpr+child+chain+data | |
if not encoded: | |
return raw | |
else: | |
return base58.b58encode_check(raw) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment