def derive_child_sec_from_xpub(xpub, path): child_xpub = bip32.derive(xpub, path) child_xpub_bytes = base58.decode_check(child_xpub) # assertion about length? child_sec_bytes = child_xpub_bytes[-33:] child_sec_hex = child_sec_bytes.hex() return child_sec_hex
def xpub_from_xprv(xprv: octets) -> bytes: """Neutered Derivation (ND) Computation of the extended public key corresponding to an extended private key (“neutered” as it removes the ability to sign transactions) """ xprv = base58.decode_check(xprv, 78) if xprv[45] != 0: raise ValueError("extended key is not a private one") i = PRV.index(xprv[:4]) # serialization data xpub = PUB[i] # version # unchanged serialization data xpub += xprv[4:5] # depth xpub += xprv[5:9] # parent pubkey fingerprint xpub += xprv[9:13] # child index xpub += xprv[13:45] # chain code p = int_from_octets(xprv[46:]) P = mult(ec, p, ec.G) xpub += octets_from_point(ec, P, True) # public key return base58.encode_check(xpub)
def derive(xkey: octets, path: Union[str, Sequence[int]]) -> bytes: """derive an extended key according to path like "m/44'/0'/1'/0/10" (absolute) or "./0/10" (relative) """ if isinstance(path, str): steps = path.split('/') if steps[0] not in {'m', '.'}: raise ValueError(f'Invalid derivation path: {path}') if steps[0] == 'm': decoded = base58.decode_check(xkey, 78) t = b'\x00' * 9 if decoded[4:13] != t: raise ValueError("Absolute derivation path for non-master key") indexes: List[int] = list() for step in steps[1:]: hardened = False if step[-1] == "'" or step[-1] == "H": hardened = True step = step[:-1] index = int(step) index += 0x80000000 if hardened else 0 indexes.append(index) else: indexes = path for index in indexes: xkey = ckd(xkey, index) return xkey
def test_wif(self): # https://en.bitcoin.it/wiki/Wallet_import_format prvkey = 0xC28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D uncompressedKey = b'\x80' + prvkey.to_bytes(32, byteorder='big') uncompressedWIF = b'5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ' wif = base58.encode_check(uncompressedKey) self.assertEqual(wif, uncompressedWIF) key = base58.decode_check(uncompressedWIF) self.assertEqual(key, uncompressedKey) compressedKey = b'\x80' + prvkey.to_bytes(32, byteorder='big') + b'\x01' compressedWIF = b'KwdMAjGmerYanjeui5SHS7JkmpZvVipYvB2LJGU1ZxJwYvP98617' wif = base58.encode_check(compressedKey) self.assertEqual(wif, compressedWIF) key = base58.decode_check(compressedWIF) self.assertEqual(key, compressedKey) # string compressedWIF = 'KwdMAjGmerYanjeui5SHS7JkmpZvVipYvB2LJGU1ZxJwYvP98617' key = base58.decode_check(compressedWIF) self.assertEqual(key, compressedKey) # string with leading space compressedWIF = ' KwdMAjGmerYanjeui5SHS7JkmpZvVipYvB2LJGU1ZxJwYvP98617' base58.decode_check(compressedWIF) self.assertEqual(key, compressedKey) # string with trailing space compressedWIF = 'KwdMAjGmerYanjeui5SHS7JkmpZvVipYvB2LJGU1ZxJwYvP98617 ' base58.decode_check(compressedWIF) self.assertEqual(key, compressedKey)
def crack(parent_xpub: octets, child_xprv: octets) -> bytes: parent_xpub = base58.decode_check(parent_xpub, 78) if parent_xpub[45] not in (2, 3): raise ValueError("extended parent key is not a public one") child_xprv = base58.decode_check(child_xprv, 78) if child_xprv[45] != 0: raise ValueError("extended child key is not a private one") # check depth if child_xprv[4] != parent_xpub[4] + 1: raise ValueError("wrong child/parent depth relation") # check fingerprint Parent_bytes = parent_xpub[45:] if child_xprv[5:9] != _h160(Parent_bytes)[:4]: raise ValueError("not a child for the provided parent") # check normal derivation child_index = child_xprv[9:13] if child_index[0] >= 0x80: raise ValueError("hardened derivation") parent_xprv = child_xprv[:4] # version parent_xprv += parent_xpub[4:5] # depth parent_xprv += parent_xpub[5:9] # parent pubkey fingerprint parent_xprv += parent_xpub[9:13] # child index parent_chain_code = parent_xpub[13:45] parent_xprv += parent_chain_code # chain code h = HMAC(parent_chain_code, Parent_bytes + child_index, sha512).digest() offset = int.from_bytes(h[:32], 'big') child = int.from_bytes(child_xprv[46:], 'big') parent = (child - offset) % ec.n parent_bytes = b'\x00' + parent.to_bytes(32, 'big') parent_xprv += parent_bytes # private key return base58.encode_check(parent_xprv)
def address_from_xpub(xpub: octets, version: Optional[octets] = None) -> bytes: xpub = base58.decode_check(xpub, 78) if xpub[45] not in (2, 3): raise ValueError("extended key is not a public one") # bitcoin: address version can be derived from xkey version # altcoin: address version cannot be derived from xkey version # if xkey version bytes have not been specialized # FIXME use BIP44 here if version is None: xversion = xpub[:4] i = PUB.index(xversion) version = ADDRESS[i] P = point_from_octets(ec, xpub[45:]) return address_from_pubkey(P, True, version)
def prvkey_from_wif(wif: octets) -> Tuple[int, bool]: """Wallet Import Format to (bytes) private key""" payload = base58.decode_check(wif) if payload[0] != 0x80: raise ValueError("Not a private key WIF: missing leading 0x80") if len(payload) == ec.psize + 2: # compressed WIF compressed = True if payload[ec.psize + 1] != 0x01: # must have a trailing 0x01 raise ValueError("Not a compressed WIF: missing trailing 0x01") prvkey = int_from_octets(payload[1:-1]) elif len(payload) == ec.psize + 1: # uncompressed WIF compressed = False prvkey = int_from_octets(payload[1:]) else: raise ValueError(f"Not a WIF: wrong size ({len(payload)})") if not 0 < prvkey < ec.n: raise ValueError(f"Not a WIF: private key {hex(prvkey)} not in (0, n)") return prvkey, compressed
def ckd(xparentkey: octets, index: Union[octets, int]) -> bytes: """Child Key Derivation (CDK) Key derivation is normal if the extended parent key is public or child_index is less than 0x80000000. Key derivation is hardened if the extended parent key is private and child_index is not less than 0x80000000. """ if isinstance(index, int): index = index.to_bytes(4, 'big') elif isinstance(index, str): # hex string index = bytes.fromhex(index) if len(index) != 4: raise ValueError(f"a 4 bytes int is required, not {len(index)}") xparent = base58.decode_check(xparentkey, 78) version = xparent[:4] # serialization data xkey = version # version xkey += (xparent[4] + 1).to_bytes(1, 'big') # (increased) depth if (version in PUB): if xparent[45] not in (2, 3): # not a compressed public key raise ValueError("version/key mismatch in extended parent key") Parent_bytes = xparent[45:] Parent = point_from_octets(ec, Parent_bytes) xkey += _h160(Parent_bytes)[:4] # parent pubkey fingerprint if index[0] >= 0x80: raise ValueError("no private/hardened derivation from pubkey") xkey += index # child index parent_chain_code = xparent[13:45] # normal derivation # actual extended key (key + chain code) derivation h = HMAC(parent_chain_code, Parent_bytes + index, sha512).digest() offset = int.from_bytes(h[:32], 'big') Offset = mult(ec, offset, ec.G) Child = ec.add(Parent, Offset) Child_bytes = octets_from_point(ec, Child, True) xkey += h[32:] # chain code xkey += Child_bytes # public key elif (version in PRV): if xparent[45] != 0: # not a private key raise ValueError("version/key mismatch in extended parent key") parent = int.from_bytes(xparent[46:], 'big') Parent = mult(ec, parent, ec.G) Parent_bytes = octets_from_point(ec, Parent, True) xkey += _h160(Parent_bytes)[:4] # parent pubkey fingerprint xkey += index # child index # actual extended key (key + chain code) derivation parent_chain_code = xparent[13:45] if (index[0] < 0x80): # normal derivation h = HMAC(parent_chain_code, Parent_bytes + index, sha512).digest() else: # hardened derivation h = HMAC(parent_chain_code, xparent[45:] + index, sha512).digest() offset = int.from_bytes(h[:32], 'big') child = (parent + offset) % ec.n child_bytes = b'\x00' + child.to_bytes(32, 'big') xkey += h[32:] # chain code xkey += child_bytes # private key else: raise ValueError("invalid extended key version") return base58.encode_check(xkey)
def child_index(xkey: octets) -> bytes: xkey = base58.decode_check(xkey, 78) if xkey[4] == 0: raise ValueError("master key provided") return xkey[9:13]
print("\n*** [7] Base58 encoding") wif = base58.encode(checksummed_payload) print(wif) assert wif == b'KwdMAjGmerYanjeui5SHS7JkmpZvVipYvB2LJGU1ZxJwYvP98617', "failure" assert base58.encode_check( payload ) == b'KwdMAjGmerYanjeui5SHS7JkmpZvVipYvB2LJGU1ZxJwYvP98617', "failure" print("\n****** WIF to private key ******") print("\n*** [1] Base58 WIF") print(wif) compressed = len(wif) - 51 print("compressed" if (compressed == 1) else "uncompressed") print("\n*** [2] Base58 decoding") checksummed_payload = base58.decode(wif) print(checksummed_payload.hex()) print("\n*** [3] Extended key (checksum verified)") payload, checksum = checksummed_payload[:-4], checksummed_payload[-4:] verified = (sha256(sha256(payload).digest()).digest()[:4] == checksum) print(payload.hex() + " (" + ("true" if verified else "false") + ")") print(base58.decode_check(wif).hex()) print("\n*** [4] Private key") p2 = payload[1:-1].hex() if compressed else payload[1:].hex() assert int(p2, 16) == q, "failure" print(p2)
def test_mainnet(self): # bitcoin core derivation style rootxprv = b'xprv9s21ZrQH143K2ZP8tyNiUtgoezZosUkw9hhir2JFzDhcUWKz8qFYk3cxdgSFoCMzt8E2Ubi1nXw71TLhwgCfzqFHfM5Snv4zboSebePRmLS' # m/0'/0'/463' addr1 = b'1DyfBWxhVLmrJ7keyiHeMbt7N3UdeGU4G5' indexes = [0x80000000, 0x80000000, 0x80000000 + 463] addr = bip32.p2pkh_address_from_xpub(bip32.xpub_from_xprv(bip32.derive(rootxprv, indexes))) self.assertEqual(addr, addr1) path = "m/0'/0'/463'" addr = bip32.p2pkh_address_from_xpub(bip32.xpub_from_xprv(bip32.derive(rootxprv, path))) self.assertEqual(addr, addr1) # m/0'/0'/267' addr2 = b'11x2mn59Qy43DjisZWQGRResjyQmgthki' indexes = [0x80000000, 0x80000000, 0x80000000 + 267] addr = bip32.p2pkh_address_from_xpub(bip32.xpub_from_xprv(bip32.derive(rootxprv, indexes))) self.assertEqual(addr, addr2) path = "m/0'/0'/267'" addr = bip32.p2pkh_address_from_xpub(bip32.xpub_from_xprv(bip32.derive(rootxprv, path))) self.assertEqual(addr, addr2) xkey_version = bip32.PRV_VERSION[0] seed = "bfc4cbaad0ff131aa97fa30a48d09ae7df914bcc083af1e07793cd0a7c61a03f65d622848209ad3366a419f4718a80ec9037df107d8d12c19b83202de00a40ad" seed = bytes.fromhex(seed) xprv = bip32.rootxprv_from_seed(seed, xkey_version) xpub = b'xpub661MyMwAqRbcFMYjmw8C6dJV97a4oLss6hb3v9wTQn2X48msQB61RCaLGtNhzgPCWPaJu7SvuB9EBSFCL43kTaFJC3owdaMka85uS154cEh' self.assertEqual(bip32.xpub_from_xprv(xprv), xpub) ind = [0, 0] addr = bip32.p2pkh_address_from_xpub(bip32.xpub_from_xprv(bip32.derive(xprv, ind))) self.assertEqual(addr, b'1FcfDbWwGs1PmyhMVpCAhoTfMnmSuptH6g') ind = [0, 1] addr = bip32.p2pkh_address_from_xpub(bip32.xpub_from_xprv(bip32.derive(xprv, ind))) self.assertEqual(addr, b'1K5GjYkZnPFvMDTGaQHTrVnd8wjmrtfR5x') ind = [0, 2] addr = bip32.p2pkh_address_from_xpub(bip32.xpub_from_xprv(bip32.derive(xprv, ind))) self.assertEqual(addr, b'1PQYX2uN7NYFd7Hq22ECMzfDcKhtrHmkfi') ind = [1, 0] addr = bip32.p2pkh_address_from_xpub(bip32.xpub_from_xprv(bip32.derive(xprv, ind))) self.assertEqual(addr, b'1BvSYpojWoWUeaMLnzbkK55v42DbizCoyq') ind = [1, 1] addr = bip32.p2pkh_address_from_xpub(bip32.xpub_from_xprv(bip32.derive(xprv, ind))) self.assertEqual(addr, b'1NXB59hF4QzYpFrB7o6usLBjbk2D3ZqxAL') ind = [1, 2] addr = bip32.p2pkh_address_from_xpub(bip32.xpub_from_xprv(bip32.derive(xprv, ind))) self.assertEqual(addr, b'16NLYkKtvYhW1Jp86tbocku3gxWcvitY1w') # version/key mismatch in extended parent key temp = base58.decode_check(rootxprv) bad_xprv = base58.encode_check(temp[0:45] + b'\x01' + temp[46:]) self.assertRaises(ValueError, bip32.ckd, bad_xprv, 1) #bip32.ckd(bad_xprv, 1) # version/key mismatch in extended parent key xpub = bip32.xpub_from_xprv(rootxprv) temp = base58.decode_check(xpub) bad_xpub = base58.encode_check(temp[0:45] + b'\x00' + temp[46:]) self.assertRaises(ValueError, bip32.ckd, bad_xpub, 1) #bip32.ckd(bad_xpub, 1) # no private/hardened derivation from pubkey self.assertRaises(ValueError, bip32.ckd, xpub, 0x80000000)
def _h160_from_address(addr: octets) -> bytes: payload = base58.decode_check(addr, 21) # FIXME: this is mainnet only if payload[0] != 0x00: raise ValueError("not a mainnet address") return payload[1:]
def hash_160_from_address(addr): return base58.decode_check(addr)[1:21]