From ae9b05dc8c6eb9fa1bb148f55652806a15a021a1 Mon Sep 17 00:00:00 2001 From: nicolas Date: Sat, 9 Sep 2023 07:56:18 -0400 Subject: [PATCH 1/3] The code uses the vbytes to determine if the input key is private or public. However, it's only checking against MAINNET_PRIVATE by default, which can lead to issues if you're using testnet prefixes. `private = (vbytes == prefixes[0])` has been changed to `private = vbytes in PRIVATE` With this change, both mainnet (xprv) and testnet (tprv) private keys will be recognized correctly. --- cryptos/deterministic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cryptos/deterministic.py b/cryptos/deterministic.py index 5200da3d..136727b2 100644 --- a/cryptos/deterministic.py +++ b/cryptos/deterministic.py @@ -74,7 +74,7 @@ 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 From 588ae0b4567871aba085a6d4a4302fbb7d582844 Mon Sep 17 00:00:00 2001 From: nicolas Date: Sat, 9 Sep 2023 19:09:58 -0400 Subject: [PATCH 2/3] Updated bip32_deserialize and bip32_deserialized functions in deterministic.py to work with testnet. Added testcase to test the deserialization and serialization for both mainnet and testnet. To run the test go the root directory and run: `python -m unittest cryptos/testing/testcases_determinisitic.py` --- cryptos/deterministic.py | 18 +++-- cryptos/testing/testcases_determinisitic.py | 80 +++++++++++++++++++++ 2 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 cryptos/testing/testcases_determinisitic.py diff --git a/cryptos/deterministic.py b/cryptos/deterministic.py index 136727b2..a1369796 100644 --- a/cryptos/deterministic.py +++ b/cryptos/deterministic.py @@ -98,14 +98,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 +124,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 00000000..6ee8eea8 --- /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) + + From 4053ad3466524f5e1f2ac53e4e61f565e68aae78 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 12 Sep 2023 09:46:13 -0400 Subject: [PATCH 3/3] added support for more version bytes --- cryptos/deterministic.py | 45 +++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/cryptos/deterministic.py b/cryptos/deterministic.py index a1369796..a657fb31 100644 --- a/cryptos/deterministic.py +++ b/cryptos/deterministic.py @@ -58,18 +58,53 @@ 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)