mirror of
https://github.com/bitcoin/bips.git
synced 2025-03-17 13:22:51 +01:00
245 lines
8.5 KiB
Python
245 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright (c) 2020 The HWI developers
|
|
# Distributed under the MIT software license, see the accompanying
|
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
|
|
"""
|
|
Key Classes and Utilities
|
|
*************************
|
|
|
|
Classes and utilities for working with extended public keys, key origins, and other key related things.
|
|
"""
|
|
|
|
import _base58 as base58
|
|
from _common import (
|
|
hash256,
|
|
hash160,
|
|
)
|
|
import binascii
|
|
import hmac
|
|
import hashlib
|
|
import struct
|
|
from typing import (
|
|
Dict,
|
|
Optional,
|
|
Sequence,
|
|
Tuple,
|
|
)
|
|
|
|
|
|
HARDENED_FLAG = 1 << 31
|
|
|
|
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
|
|
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)
|
|
|
|
Point = Optional[Tuple[int, int]]
|
|
|
|
def H_(x: int) -> int:
|
|
"""
|
|
Shortcut function that "hardens" a number in a BIP44 path.
|
|
"""
|
|
return x | HARDENED_FLAG
|
|
|
|
def is_hardened(i: int) -> bool:
|
|
"""
|
|
Returns whether an index is hardened
|
|
"""
|
|
return i & HARDENED_FLAG != 0
|
|
|
|
|
|
def point_add(p1: Point, p2: Point) -> Point:
|
|
if (p1 is None):
|
|
return p2
|
|
if (p2 is None):
|
|
return p1
|
|
if (p1[0] == p2[0] and p1[1] != p2[1]):
|
|
return None
|
|
if (p1 == p2):
|
|
lam = (3 * p1[0] * p1[0] * pow(2 * p1[1], p - 2, p)) % p
|
|
else:
|
|
lam = ((p2[1] - p1[1]) * pow(p2[0] - p1[0], p - 2, p)) % p
|
|
x3 = (lam * lam - p1[0] - p2[0]) % p
|
|
return (x3, (lam * (p1[0] - x3) - p1[1]) % p)
|
|
|
|
|
|
def point_mul(p: Point, n: int) -> Point:
|
|
r = None
|
|
for i in range(256):
|
|
if ((n >> i) & 1):
|
|
r = point_add(r, p)
|
|
p = point_add(p, p)
|
|
return r
|
|
|
|
|
|
def deserialize_point(b: bytes) -> Point:
|
|
x = int.from_bytes(b[1:], byteorder="big")
|
|
y = pow((x * x * x + 7) % p, (p + 1) // 4, p)
|
|
if (y & 1 != b[0] & 1):
|
|
y = p - y
|
|
return (x, y)
|
|
|
|
|
|
def bytes_to_point(point_bytes: bytes) -> Point:
|
|
header = point_bytes[0]
|
|
if header == 4:
|
|
x = point_bytes = point_bytes[1:33]
|
|
y = point_bytes = point_bytes[33:65]
|
|
return (int(binascii.hexlify(x), 16), int(binascii.hexlify(y), 16))
|
|
return deserialize_point(point_bytes)
|
|
|
|
def point_to_bytes(p: Point) -> bytes:
|
|
if p is None:
|
|
raise ValueError("Cannot convert None to bytes")
|
|
return (b'\x03' if p[1] & 1 else b'\x02') + p[0].to_bytes(32, byteorder="big")
|
|
|
|
|
|
# An extended public key (xpub) or private key (xprv). Just a data container for now.
|
|
# Only handles deserialization of extended keys into component data to be handled by something else
|
|
class ExtendedKey(object):
|
|
"""
|
|
A BIP 32 extended public key.
|
|
"""
|
|
|
|
MAINNET_PUBLIC = b'\x04\x88\xB2\x1E'
|
|
MAINNET_PRIVATE = b'\x04\x88\xAD\xE4'
|
|
TESTNET_PUBLIC = b'\x04\x35\x87\xCF'
|
|
TESTNET_PRIVATE = b'\x04\x35\x83\x94'
|
|
|
|
def __init__(self, version: bytes, depth: int, parent_fingerprint: bytes, child_num: int, chaincode: bytes, privkey: Optional[bytes], pubkey: bytes) -> None:
|
|
"""
|
|
:param version: The version bytes for this xpub
|
|
:param depth: The depth of this xpub as defined in BIP 32
|
|
:param parent_fingerprint: The 4 byte fingerprint of the parent xpub as defined in BIP 32
|
|
:param child_num: The number of this xpub as defined in BIP 32
|
|
:param chaincode: The chaincode of this xpub as defined in BIP 32
|
|
:param privkey: The private key for this xpub if available
|
|
:param pubkey: The public key for this xpub
|
|
"""
|
|
self.version: bytes = version
|
|
self.is_testnet: bool = version == ExtendedKey.TESTNET_PUBLIC or version == ExtendedKey.TESTNET_PRIVATE
|
|
self.is_private: bool = version == ExtendedKey.MAINNET_PRIVATE or version == ExtendedKey.TESTNET_PRIVATE
|
|
self.depth: int = depth
|
|
self.parent_fingerprint: bytes = parent_fingerprint
|
|
self.child_num: int = child_num
|
|
self.chaincode: bytes = chaincode
|
|
self.pubkey: bytes = pubkey
|
|
self.privkey: Optional[bytes] = privkey
|
|
|
|
@classmethod
|
|
def deserialize(cls, xpub: str) -> 'ExtendedKey':
|
|
"""
|
|
Create an :class:`~ExtendedKey` from a Base58 check encoded xpub
|
|
|
|
:param xpub: The Base58 check encoded xpub
|
|
"""
|
|
data = base58.decode(xpub)[:-4] # Decoded xpub without checksum
|
|
return cls.from_bytes(data)
|
|
|
|
@classmethod
|
|
def from_bytes(cls, data: bytes) -> 'ExtendedKey':
|
|
"""
|
|
Create an :class:`~ExtendedKey` from a serialized xpub
|
|
|
|
:param xpub: The serialized xpub
|
|
"""
|
|
|
|
version = data[0:4]
|
|
if version not in [ExtendedKey.MAINNET_PRIVATE, ExtendedKey.MAINNET_PUBLIC, ExtendedKey.TESTNET_PRIVATE, ExtendedKey.TESTNET_PUBLIC]:
|
|
raise Exception(f"Extended key magic of {version.hex()} is invalid")
|
|
is_private = version == ExtendedKey.MAINNET_PRIVATE or version == ExtendedKey.TESTNET_PRIVATE
|
|
depth = data[4]
|
|
parent_fingerprint = data[5:9]
|
|
child_num = struct.unpack('>I', data[9:13])[0]
|
|
chaincode = data[13:45]
|
|
|
|
if is_private:
|
|
privkey = data[46:]
|
|
pubkey = point_to_bytes(point_mul(G, int.from_bytes(privkey, byteorder="big")))
|
|
return cls(version, depth, parent_fingerprint, child_num, chaincode, privkey, pubkey)
|
|
else:
|
|
pubkey = data[45:78]
|
|
return cls(version, depth, parent_fingerprint, child_num, chaincode, None, pubkey)
|
|
|
|
def serialize(self) -> bytes:
|
|
"""
|
|
Serialize the ExtendedKey with the serialization format described in BIP 32.
|
|
Does not create an xpub string, but the bytes serialized here can be Base58 check encoded into one.
|
|
|
|
:return: BIP 32 serialized extended key
|
|
"""
|
|
r = self.version + struct.pack('B', self.depth) + self.parent_fingerprint + struct.pack('>I', self.child_num) + self.chaincode
|
|
if self.is_private:
|
|
if self.privkey is None:
|
|
raise ValueError("Somehow we are private but don't have a privkey")
|
|
r += b"\x00" + self.privkey
|
|
else:
|
|
r += self.pubkey
|
|
return r
|
|
|
|
def to_string(self) -> str:
|
|
"""
|
|
Serialize the ExtendedKey as a Base58 check encoded xpub string
|
|
|
|
:return: Base58 check encoded xpub
|
|
"""
|
|
data = self.serialize()
|
|
checksum = hash256(data)[0:4]
|
|
return base58.encode(data + checksum)
|
|
|
|
def get_printable_dict(self) -> Dict[str, object]:
|
|
"""
|
|
Get the attributes of this ExtendedKey as a dictionary that can be printed
|
|
|
|
:return: Dictionary containing ExtendedKey information that can be printed
|
|
"""
|
|
d: Dict[str, object] = {}
|
|
d['testnet'] = self.is_testnet
|
|
d['private'] = self.is_private
|
|
d['depth'] = self.depth
|
|
d['parent_fingerprint'] = binascii.hexlify(self.parent_fingerprint).decode()
|
|
d['child_num'] = self.child_num
|
|
d['chaincode'] = binascii.hexlify(self.chaincode).decode()
|
|
if self.is_private and isinstance(self.privkey, bytes):
|
|
d['privkey'] = binascii.hexlify(self.privkey).decode()
|
|
d['pubkey'] = binascii.hexlify(self.pubkey).decode()
|
|
return d
|
|
|
|
def derive_pub(self, i: int) -> 'ExtendedKey':
|
|
"""
|
|
Derive the public key at the given child index.
|
|
|
|
:param i: The child index of the pubkey to derive
|
|
"""
|
|
if is_hardened(i):
|
|
raise ValueError("Index cannot be larger than 2^31")
|
|
|
|
# Data to HMAC. Same as CKDpriv() for public child key.
|
|
data = self.pubkey + struct.pack(">L", i)
|
|
|
|
# Get HMAC of data
|
|
Ihmac = hmac.new(self.chaincode, data, hashlib.sha512).digest()
|
|
Il = Ihmac[:32]
|
|
Ir = Ihmac[32:]
|
|
|
|
# Construct curve point Il*G+K
|
|
Il_int = int(binascii.hexlify(Il), 16)
|
|
child_pubkey = point_add(point_mul(G, Il_int), bytes_to_point(self.pubkey))
|
|
|
|
# Construct and return a new BIP32Key
|
|
pubkey = point_to_bytes(child_pubkey)
|
|
chaincode = Ir
|
|
fingerprint = hash160(self.pubkey)[0:4]
|
|
return ExtendedKey(ExtendedKey.TESTNET_PUBLIC if self.is_testnet else ExtendedKey.MAINNET_PUBLIC, self.depth + 1, fingerprint, i, chaincode, None, pubkey)
|
|
|
|
def derive_pub_path(self, path: Sequence[int]) -> 'ExtendedKey':
|
|
"""
|
|
Derive the public key at the given path
|
|
|
|
:param path: Sequence of integers for the path of the pubkey to derive
|
|
"""
|
|
key = self
|
|
for i in path:
|
|
key = key.derive_pub(i)
|
|
return key
|