Skip to content

Instantly share code, notes, and snippets.

@bjarnemagnussen
Created May 9, 2020 15:45
Show Gist options
  • Save bjarnemagnussen/2fd7bc5ce07d35e92fe7e6f40a16e11e to your computer and use it in GitHub Desktop.
Save bjarnemagnussen/2fd7bc5ce07d35e92fe7e6f40a16e11e to your computer and use it in GitHub Desktop.
#
# 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