Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enabled key derivation for testnet account extended private keys (tprv) by fixing a bug in the raw_bip32_ckd function. #60

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 53 additions & 12 deletions cryptos/deterministic.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,58 @@ def crack_electrum_wallet(mpk, pk, n, for_change=0):
offset = dbl_sha256(str(n)+':'+str(for_change)+':'+bin_mpk)
return subtract_privkeys(pk, offset)

# Below code ASSUMES binary inputs and compressed pubkeys
# Mainnet
MAINNET_PRIVATE = b'\x04\x88\xAD\xE4'
MAINNET_PUBLIC = b'\x04\x88\xB2\x1E'

# Testnet or Regression Test Mode
TESTNET_PRIVATE = b'\x04\x35\x83\x94'
TESTNET_PUBLIC = b'\x04\x35\x87\xCF'
PRIVATE = [MAINNET_PRIVATE, TESTNET_PRIVATE]
PUBLIC = [MAINNET_PUBLIC, TESTNET_PUBLIC]
DEFAULT = (MAINNET_PRIVATE, MAINNET_PUBLIC)

# BIP32 child key derivation
# Mainnet Multi-signature (p2sh)
MAINNET_P2SH_PUBLIC = b'\x04\x9D\x7C\xB2'
MAINNET_P2SH_PRIVATE = b'\x04\x9D\x78\x78'

# Testnet Multi-signature (p2sh)
TESTNET_P2SH_PUBLIC = b'\x04\x4A\x52\x62'
TESTNET_P2SH_PRIVATE = b'\x04\x4A\x4E\x28'

# Mainnet Segwit (p2wpkh-nested-in-p2sh)
MAINNET_P2WPKH_NESTED_PUBLIC = b'\x04\xB2\x47\x46'
MAINNET_P2WPKH_NESTED_PRIVATE = b'\x04\xB2\x43\x0C'

# Testnet Segwit (p2wpkh-nested-in-p2sh)
TESTNET_P2WPKH_NESTED_PUBLIC = b'\x04\x5F\x1C\xF6'
TESTNET_P2WPKH_NESTED_PRIVATE = b'\x04\x5F\x18\xBC'

# Private version bytes array
PRIVATE = [
MAINNET_PRIVATE,
TESTNET_PRIVATE,
MAINNET_P2SH_PRIVATE,
TESTNET_P2SH_PRIVATE,
MAINNET_P2WPKH_NESTED_PRIVATE,
TESTNET_P2WPKH_NESTED_PRIVATE
]

# Public version bytes array
PUBLIC = [
MAINNET_PUBLIC,
TESTNET_PUBLIC,
MAINNET_P2SH_PUBLIC,
TESTNET_P2SH_PUBLIC,
MAINNET_P2WPKH_NESTED_PUBLIC,
TESTNET_P2WPKH_NESTED_PUBLIC
]

DEFAULT = (MAINNET_PRIVATE, MAINNET_PUBLIC)

# BIP32 child key derivation
def raw_bip32_ckd(rawtuple, i, prefixes=DEFAULT):
vbytes, depth, fingerprint, oldi, chaincode, key = rawtuple
i = int(i)

private = vbytes == prefixes[0]
private = (vbytes in PRIVATE)

if private:
priv = key
Expand All @@ -98,14 +133,21 @@ def raw_bip32_ckd(rawtuple, i, prefixes=DEFAULT):

return (vbytes, depth + 1, fingerprint, i, I[32:], newkey)


def bip32_serialize(rawtuple, prefixes=DEFAULT):
vbytes, depth, fingerprint, i, chaincode, key = rawtuple
# Ensure i is encoded correctly
i = encode(i, 256, 4)
chaincode = encode(hash_to_int(chaincode), 256, 32)
keydata = b'\x00'+key[:-1] if vbytes == prefixes[0] else key
# Check the encoding of the chaincode
chaincode = chaincode if len(chaincode) == 32 else encode(hash_to_int(chaincode), 256, 32)
# Depending on whether it's a private or public key, format keydata
if vbytes in PRIVATE: # if it's a private key, for both mainnet and testnet
keydata = b'\x00' + key[:-1]
else: # it's a public key
keydata = key
# Assemble all the pieces
bindata = vbytes + from_int_to_byte(depth % 256) + fingerprint + i + chaincode + keydata
return changebase(bindata+bin_dbl_sha256(bindata)[:4], 256, 58)
# Return the Base58Check encoded data
return changebase(bindata + bin_dbl_sha256(bindata)[:4], 256, 58)


def bip32_deserialize(data, prefixes=DEFAULT):
Expand All @@ -117,10 +159,9 @@ def bip32_deserialize(data, prefixes=DEFAULT):
fingerprint = dbin[5:9]
i = decode(dbin[9:13], 256)
chaincode = dbin[13:45]
key = dbin[46:78]+b'\x01' if vbytes == prefixes[0] else dbin[45:78]
key = dbin[46:78]+b'\x01' if vbytes in PRIVATE else dbin[45:78]
return (vbytes, depth, fingerprint, i, chaincode, key)


def is_xprv(text, prefixes=DEFAULT):
try:
vbytes, depth, fingerprint, i, chaincode, key = bip32_deserialize(text, prefixes)
Expand Down
80 changes: 80 additions & 0 deletions cryptos/testing/testcases_determinisitic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import unittest
from cryptos import *

class MyTests(unittest.TestCase):
def test_bip32_deserialize(self):
for i in range(0, 10):
words = entropy_to_words(os.urandom(32))
# Sample xpub and xprv
coin = Bitcoin(testnet=False)
wallet = coin.wallet(words)
xprv_sample = wallet.keystore.xprv
xpub_sample = wallet.keystore.xpub

# For testnet: tpub and tprv
coin = Bitcoin(testnet=True)
wallet = coin.wallet(words)
tprv_sample = wallet.keystore.xprv
tpub_sample = wallet.keystore.xpub


# Deserialize
xpub_deserialized = bip32_deserialize(xpub_sample)
xprv_deserialized = bip32_deserialize(xprv_sample)
tpub_deserialized = bip32_deserialize(tpub_sample)
tprv_deserialized = bip32_deserialize(tprv_sample)

# Assert checks
# Check xpub
assert len(xpub_deserialized[-1]) == 33 # Compressed public key length
assert xpub_deserialized[-1][0] in [2, 3] # Must start with 02 or 03

# Check tpub
assert len(tpub_deserialized[-1]) == 33
assert tpub_deserialized[-1][0] in [2, 3]

# Check xprv - It should have an appended '01' to indicate that it's a compressed private key
assert xprv_deserialized[-1][-1] == 1

# Check tprv
assert tprv_deserialized[-1][-1] == 1

print("All tests passed!")

def test_child_derivation_unhardened(self):

words = entropy_to_words(os.urandom(32))
coin = Bitcoin(testnet=True)
wallet = coin.wallet(words)
tprv = wallet.keystore.xprv
tpub = wallet.keystore.xpub

for i in range(10):
path = "m/0/{}".format(i)

child_tprv = bip32_ckd(tprv, path, prefixes=PRIVATE, public=False)
privkey = bip32_deserialize(child_tprv)[-1]

child_tpub = bip32_ckd(tpub, path, prefixes=PUBLIC, public=False)
pubkey = bip32_deserialize(child_tpub)[-1]

assert(privtopub(privkey) == pubkey)


coin = Bitcoin(testnet=False)
wallet = coin.wallet(words)
xprv = wallet.keystore.xprv
xpub = wallet.keystore.xpub

for i in range(10):
path = "m/0/{}".format(i)

child_xprv = bip32_ckd(xprv, path, prefixes=PRIVATE, public=False)
privkey = bip32_deserialize(child_xprv)[-1]

child_xpub = bip32_ckd(xpub, path, prefixes=PUBLIC, public=False)
pubkey = bip32_deserialize(child_xpub)[-1]

assert(privtopub(privkey) == pubkey)