-
Notifications
You must be signed in to change notification settings - Fork 156
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #88 from fubuloubu/bip32
Add support for BIP32 (Heirarchical Deterministic Wallets)
- Loading branch information
Showing
4 changed files
with
353 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import hashlib | ||
import hmac | ||
|
||
from eth_keys import ( | ||
keys, | ||
) | ||
from hexbytes import ( | ||
HexBytes, | ||
) | ||
|
||
SECP256K1_N = int("FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_BAAEDCE6_AF48A03B_BFD25E8C_D0364141", 16) | ||
|
||
|
||
def hmac_sha512(chain_code: bytes, data: bytes) -> bytes: | ||
""" | ||
As specified by RFC4231 - https://tools.ietf.org/html/rfc4231 | ||
""" | ||
return hmac.new(chain_code, data, hashlib.sha512).digest() | ||
|
||
|
||
def ec_point(pkey: bytes) -> bytes: | ||
""" | ||
Compute `point(p)`, where `point` is ecdsa point multiplication | ||
Note: Result is ecdsa public key serialized to compressed form | ||
""" | ||
return keys.PrivateKey(HexBytes(pkey)).public_key.to_compressed_bytes() |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,236 @@ | ||
""" | ||
Heirarchical Deterministic Wallet generator (HDWallet) | ||
Partially implements the BIP-0032, BIP-0043, and BIP-0044 specifications: | ||
BIP-0032: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki | ||
BIP-0043: https://github.com/bitcoin/bips/blob/master/bip-0043.mediawiki | ||
BIP-0044: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki | ||
Skips serialization and public key derivation as unnecssary for this library's purposes. | ||
Notes | ||
----- | ||
* Integers are modulo the order of the curve (referred to as n). | ||
* Addition (+) of two coordinate pair is defined as application of the EC group operation. | ||
* Concatenation (||) is the operation of appending one byte sequence onto another. | ||
Definitions | ||
----------- | ||
* point(p): returns the coordinate pair resulting from EC point multiplication | ||
(repeated application of the EC group operation) of the secp256k1 base point | ||
with the integer p. | ||
* ser_32(i): serialize a 32-bit unsigned integer i as a 4-byte sequence, | ||
most significant byte first. | ||
* ser_256(p): serializes the integer p as a 32-byte sequence, most significant byte first. | ||
* ser_P(P): serializes the coordinate pair P = (x,y) as a byte sequence using SEC1's compressed | ||
form: (0x02 or 0x03) || ser_256(x), where the header byte depends on the parity of the | ||
omitted y coordinate. | ||
* parse_256(p): interprets a 32-byte sequence as a 256-bit number, most significant byte first. | ||
""" | ||
# Additional notes: | ||
# - This module currently only implements private parent key => private child key CKD function, | ||
# as it is not necessary to the HD key derivation functions used in this library to implement | ||
# the other functions yet (as this module is only used for derivation of private keys). That | ||
# could change, but wasn't deemed necessary at the time this module was introduced. | ||
# - Unlike other libraries, this library does not use Bitcoin key serialization, because it is | ||
# not intended to be ultimately used for Bitcoin key derivations. This presents a simplified | ||
# API, and no expectation is given for `xpub/xpriv` key derivation. | ||
from typing import ( | ||
Tuple, | ||
Union, | ||
) | ||
|
||
from eth_utils import ( | ||
ValidationError, | ||
to_int, | ||
) | ||
|
||
from ._utils import ( | ||
SECP256K1_N, | ||
ec_point, | ||
hmac_sha512, | ||
) | ||
|
||
BASE_NODE_IDENTIFIERS = {"m", "M"} | ||
HARD_NODE_SUFFIXES = {"'", "H"} | ||
|
||
|
||
class Node(int): | ||
TAG = "" # No tag | ||
OFFSET = 0x0 # No offset | ||
""" | ||
Base node class | ||
""" | ||
def __new__(cls, index): | ||
if 0 > index or index > 2**31: | ||
raise ValidationError( | ||
f"{cls} cannot be initialized with value {index}" | ||
) | ||
obj = int.__new__(cls, index + cls.OFFSET) | ||
obj.index = index | ||
return obj | ||
|
||
def __repr__(self): | ||
return f"{self.__class__.__name__}({self.index})" | ||
|
||
def __add__(self, other: int): | ||
return self.__class__(self.index + other) | ||
|
||
def serialize(self) -> bytes: | ||
return self.to_bytes(4, byteorder="big") | ||
|
||
def encode(self) -> str: | ||
return str(self.index) + self.TAG | ||
|
||
@staticmethod | ||
def decode(node: str) -> Union["SoftNode", "HardNode"]: | ||
if len(node) < 1: | ||
raise ValidationError("Cannot use empty string") | ||
|
||
if node[-1] in HARD_NODE_SUFFIXES: | ||
node_class = HardNode | ||
node_index = node[:-1] | ||
else: | ||
node_class = SoftNode | ||
node_index = node | ||
|
||
try: | ||
node_value = int(node_index) | ||
except ValueError as err: | ||
raise ValidationError(f"'{node_index}' is not a valid node index.") from err | ||
|
||
return node_class(node_value) | ||
|
||
|
||
class SoftNode(Node): | ||
""" | ||
Soft node (unhardened), where value = index | ||
""" | ||
TAG = "" # No tag | ||
OFFSET = 0x0 # No offset | ||
|
||
|
||
class HardNode(Node): | ||
""" | ||
Hard node, where value = index + BIP32_HARDENED_CONSTANT | ||
""" | ||
TAG = "H" # "H" (or "'") means hard node (but use "H" for clarity) | ||
OFFSET = 0x80000000 # 2**31, BIP32 "Hardening constant" | ||
|
||
|
||
def derive_child_key( | ||
parent_key: bytes, | ||
parent_chain_code: bytes, | ||
node: Node, | ||
) -> Tuple[bytes, bytes]: | ||
""" | ||
From BIP32: | ||
The function CKDpriv((k_par, c_par), i) → (k_i, c_i) computes a child extended | ||
private key from the parent extended private key: | ||
- Check whether i ≥ 2**31 (whether the child is a hardened key). | ||
- If so (hardened child): | ||
let I = HMAC-SHA512(Key = c_par, Data = 0x00 || ser_256(k_par) || ser_32(i)). | ||
(Note: The 0x00 pads the private key to make it 33 bytes long.) | ||
- If not (normal child): | ||
let I = HMAC-SHA512(Key = c_par, Data = ser_P(point(k_par)) || ser_32(i)). | ||
- Split I into two 32-byte sequences, I_L and I_R. | ||
- The returned child key k_i is parse_256(I_L) + k_par (mod n). | ||
- The returned chain code c_i is I_R. | ||
- In case parse_256(I_L) ≥ n or k_i = 0, the resulting key is invalid, | ||
and one should proceed with the next value for i. | ||
(Note: this has probability lower than 1 in 2**127.) | ||
""" | ||
assert len(parent_chain_code) == 32 | ||
if isinstance(node, HardNode): | ||
# NOTE Empty byte is added to align to SoftNode case | ||
assert len(parent_key) == 32 # Should be guaranteed here in return statment | ||
child = hmac_sha512(parent_chain_code, b"\x00" + parent_key + node.serialize()) | ||
|
||
elif isinstance(node, SoftNode): | ||
assert len(ec_point(parent_key)) == 33 # Should be guaranteed by Account class | ||
child = hmac_sha512(parent_chain_code, ec_point(parent_key) + node.serialize()) | ||
|
||
else: | ||
raise ValidationError(f"Cannot process: {node}") | ||
|
||
assert len(child) == 64 | ||
|
||
if to_int(child[:32]) >= SECP256K1_N: | ||
# Invalid key, compute using next node (< 2**-127 probability) | ||
return derive_child_key(parent_key, parent_chain_code, node + 1) | ||
|
||
child_key = (to_int(child[:32]) + to_int(parent_key)) % SECP256K1_N | ||
if child_key == 0: | ||
# Invalid key, compute using next node (< 2**-127 probability) | ||
return derive_child_key(parent_key, parent_chain_code, node + 1) | ||
|
||
child_key = child_key.to_bytes(32, byteorder="big") | ||
child_chain_code = child[32:] | ||
return child_key, child_chain_code | ||
|
||
|
||
class HDPath: | ||
def __init__(self, path: str): | ||
""" | ||
Constructor for this class. Initializes an hd account generator using the | ||
given path string (from BIP-0032). The path is decoded into nodes of the | ||
derivation key tree, which define a pathway from a given master seed to | ||
the child key that is used for a given purpose. Please also reference BIP- | ||
0043 (which definites the first level as the "purpose" field of an HD path) | ||
and BIP-0044 (which defines a commonly-used, 5-level scheme for BIP32 paths) | ||
for examples of how this object may be used. Please note however that this | ||
object makes no such assumptions of the use of BIP43 or BIP44, or later BIPs. | ||
:param path : BIP32-compatible derivation path | ||
:type path : str as "m/idx_0/.../idx_n" or "m/idx_0/.../idx_n" | ||
where idx_* is either an integer value (soft node) | ||
or an integer value followed by either the "'" char | ||
or the "H" char (hardened node) | ||
""" | ||
if len(path) < 1: | ||
raise ValidationError("Cannot parse path from empty string.") | ||
|
||
nodes = path.split('/') # Should at least make 1 entry in resulting list | ||
if nodes[0] not in BASE_NODE_IDENTIFIERS: | ||
raise ValidationError(f'Path is not valid: "{path}". Must start with "m"') | ||
|
||
decoded_path = [] | ||
for idx, node in enumerate(nodes[1:]): # We don't need the root node 'm' | ||
try: | ||
decoded_path.append(Node.decode(node)) | ||
except ValidationError as err: | ||
raise ValidationError( | ||
f'Path "{path}" is not valid. Issue with node "{node}": {err}' | ||
) from err | ||
|
||
self._path = decoded_path | ||
|
||
def __repr__(self) -> str: | ||
return f'{self.__class__.__name__}(path="{self.encode()}")' | ||
|
||
def encode(self) -> str: | ||
""" | ||
Encodes this class to a string (reversing the decoding in the constructor) | ||
""" | ||
encoded_path = ('m',) + tuple(node.encode() for node in self._path) | ||
return '/'.join(encoded_path) | ||
|
||
def derive(self, seed: bytes) -> bytes: | ||
""" | ||
Perform the BIP32 Heirarchical Derivation recursive loop with the given Path | ||
Note that the key and chain_code are initialized with the master seed, and that | ||
the key that is returned is the child key at the end of derivation process (and | ||
the chain code is discarded) | ||
""" | ||
master_node = hmac_sha512(b"Bitcoin seed", seed) | ||
key = master_node[:32] | ||
chain_code = master_node[32:] | ||
for node in self._path: | ||
key, chain_code = derive_child_key(key, chain_code, node) | ||
return key |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import pytest | ||
|
||
from eth_account.hdaccount.deterministic import ( | ||
HDPath, | ||
) | ||
|
||
|
||
# Test vectors from: https://en.bitcoin.it/wiki/BIP_0032_TestVectors | ||
# Confirmed using https://github.com/richardkiss/pycoin | ||
@pytest.mark.parametrize("seed,path,key", [ | ||
# --- BIP32 Testvector 1 --- | ||
( | ||
"000102030405060708090a0b0c0d0e0f", "m", | ||
"e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35" | ||
), | ||
( | ||
"000102030405060708090a0b0c0d0e0f", "m/0H", | ||
"edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea" | ||
), | ||
( | ||
"000102030405060708090a0b0c0d0e0f", "m/0H/1", | ||
"3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368" | ||
), | ||
( | ||
"000102030405060708090a0b0c0d0e0f", "m/0H/1/2H", | ||
"cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca" | ||
), | ||
( | ||
"000102030405060708090a0b0c0d0e0f", "m/0H/1/2H/2", | ||
"0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4" | ||
), | ||
( | ||
"000102030405060708090a0b0c0d0e0f", "m/0H/1/2H/2/1000000000", | ||
"471b76e389e528d6de6d816857e012c5455051cad6660850e58372a6c3e6e7c8" | ||
), | ||
# --- BIP32 Testvector 2 --- | ||
( | ||
"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c" | ||
"999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542", | ||
"m", | ||
"4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e" | ||
), | ||
( | ||
"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c" | ||
"999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542", | ||
"m/0", | ||
"abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e" | ||
), | ||
( | ||
"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c" | ||
"999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542", | ||
"m/0/2147483647H", | ||
"877c779ad9687164e9c2f4f0f4ff0340814392330693ce95a58fe18fd52e6e93" | ||
), | ||
( | ||
"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c" | ||
"999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542", | ||
"m/0/2147483647H/1", | ||
"704addf544a06e5ee4bea37098463c23613da32020d604506da8c0518e1da4b7" | ||
), | ||
( | ||
"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c" | ||
"999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542", | ||
"m/0/2147483647H/1/2147483646H", | ||
"f1c7c871a54a804afe328b4c83a1c33b8e5ff48f5087273f04efa83b247d6a2d" | ||
), | ||
( | ||
"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c" | ||
"999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542", | ||
"m/0/2147483647H/1/2147483646H/2", | ||
"bb7d39bdb83ecf58f2fd82b6d918341cbef428661ef01ab97c28a4842125ac23" | ||
), | ||
# --- BIP32 Testvector 3 --- | ||
# NOTE: Leading zeros bug https://github.com/iancoleman/bip39/issues/58 | ||
( | ||
"4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45" | ||
"d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be", | ||
"m", | ||
# NOTE Contains leading zero byte (which was the bug) | ||
"00ddb80b067e0d4993197fe10f2657a844a384589847602d56f0c629c81aae32" | ||
), | ||
( | ||
"4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45" | ||
"d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be", | ||
"m/0H", | ||
"491f7a2eebc7b57028e0d3faa0acda02e75c33b03c48fb288c41e2ea44e1daef" | ||
) | ||
]) | ||
def test_bip32_testvectors(seed, path, key): | ||
assert HDPath(path).derive(bytes.fromhex(seed)).hex() == key |