diff --git a/cryptos/deterministic.py b/cryptos/deterministic.py index 5200da3..a657fb3 100644 --- a/cryptos/deterministic.py +++ b/cryptos/deterministic.py @@ -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 @@ -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): @@ -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) diff --git a/cryptos/testing/testcases_determinisitic.py b/cryptos/testing/testcases_determinisitic.py new file mode 100644 index 0000000..6ee8eea --- /dev/null +++ b/cryptos/testing/testcases_determinisitic.py @@ -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) + +