extendedkey.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2026-present The Bitcoin Core developers
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  """Base classes for creating dynamic xprvs and xpubs. Only basic
   6  functionality of BIP32 is provided here. These classes work over
   7  the ECKey and ECPubKey classes in the key.py."""
   8  
   9  import hashlib
  10  import hmac
  11  import unittest
  12  
  13  from test_framework.address import byte_to_base58
  14  from test_framework.crypto import secp256k1
  15  from test_framework.key import (
  16      ECKey,
  17      ECPubKey,
  18      generate_privkey,
  19      ORDER,
  20  )
  21  from test_framework.script import hash160
  22  
  23  BIP32_HARDENED = 0x80000000
  24  
  25  def get_version(private=False, mainnet=False):
  26      if mainnet:
  27          if private:
  28              return bytes.fromhex("0488ADE4")
  29          return bytes.fromhex("0488B21E")
  30  
  31      if private:
  32          return bytes.fromhex("04358394")
  33      return bytes.fromhex("043587CF")
  34  
  35  def hardened(index):
  36      assert 0 <= index < BIP32_HARDENED
  37      return index | BIP32_HARDENED
  38  
  39  def derive_path(key, path, path_idx_parser):
  40      if path in ("", "m"):
  41          return key
  42      parts = path.split("/")
  43      if parts[0] == "m":
  44          parts = parts[1:]
  45      for part in parts:
  46          index = path_idx_parser(part)
  47          key = key._derive(index)
  48  
  49      return key
  50  
  51  class ExtendedPrivateKey:
  52      def __init__(self, key, chaincode, depth=0, parent_fingerprint_bytes=b"\x00\x00\x00\x00", child_num=0):
  53          self.key = key
  54          self.chaincode = chaincode
  55          self.depth = depth
  56          self.parent_fingerprint_bytes = parent_fingerprint_bytes
  57          self.child_num = child_num
  58  
  59      @classmethod
  60      def from_seed(cls, seed):
  61          I = hmac.new(b"Bitcoin seed", seed, hashlib.sha512).digest()
  62  
  63          secret = I[:32]
  64          chaincode = I[32:]
  65  
  66          secret_int = int.from_bytes(secret, "big")
  67          if secret_int == 0 or secret_int >= ORDER:
  68              raise ValueError("Invalid master key")
  69  
  70          key = ECKey()
  71          key.set(secret, compressed=True)
  72  
  73          return cls(key, chaincode)
  74  
  75      @classmethod
  76      def generate(cls):
  77          return cls.from_seed(generate_privkey())
  78  
  79      def _fingerprint(self):
  80          return hash160(self.key.get_pubkey().get_bytes())[:4]
  81  
  82      def _derive(self, index):
  83          if index >= BIP32_HARDENED:
  84              data = (b"\x00" + self.key.get_bytes() + index.to_bytes(4, "big"))
  85          else:
  86              data = (self.key.get_pubkey().get_bytes() + index.to_bytes(4, "big"))
  87  
  88          I = hmac.new(self.chaincode, data, hashlib.sha512).digest()
  89          IL = I[:32]
  90          IR = I[32:]
  91  
  92          IL_int = int.from_bytes(IL, "big")
  93          child_secret = (IL_int + int.from_bytes(self.key.get_bytes(), "big")) % ORDER
  94          # Per BIP32, if IL >= n or the child key is 0, the key is invalid. This is
  95          # astronomically unlikely (~1 in 2^127), so reject rather than retrying the next index.
  96          if IL_int >= ORDER or child_secret == 0:
  97              raise ValueError("Invalid BIP32 child key")
  98  
  99          child = ECKey()
 100          child.set(child_secret.to_bytes(32, 'big'), compressed=True)
 101  
 102          return ExtendedPrivateKey(child, IR, self.depth + 1, self._fingerprint(), index)
 103  
 104      def _serialize(self):
 105          return (bytes([self.depth]) + self.parent_fingerprint_bytes + self.child_num.to_bytes(4, "big") + self.chaincode + b"\x00" + self.key.get_bytes())
 106  
 107      def pubkey(self):
 108          return ExtendedPublicKey(self.key.get_pubkey(), self.chaincode, self.depth, self.parent_fingerprint_bytes, self.child_num)
 109  
 110      def derive_path(self, path):
 111          def path_idx_parser(part):
 112              if part.endswith(("h", "'")):
 113                  return hardened(int(part[:-1]))
 114              return int(part)
 115  
 116          return derive_path(self, path, path_idx_parser)
 117  
 118      def to_string(self, mainnet=False):
 119          return byte_to_base58(self._serialize(), get_version(private=True, mainnet=mainnet))
 120  
 121  class ExtendedPublicKey:
 122      def __init__(self, pubkey, chaincode, depth, parent_fingerprint_bytes, child_num):
 123          self.pubkey = pubkey
 124          self.chaincode = chaincode
 125          self.depth = depth
 126          self.parent_fingerprint_bytes = parent_fingerprint_bytes
 127          self.child_num = child_num
 128  
 129      def _fingerprint(self):
 130          return hash160(self.pubkey.get_bytes())[:4]
 131  
 132      def _derive(self, index):
 133          if index >= BIP32_HARDENED:
 134              raise ValueError("Cannot derive hardened child from xpub")
 135  
 136          data = (self.pubkey.get_bytes() + index.to_bytes(4, "big"))
 137          I = hmac.new(self.chaincode, data, hashlib.sha512).digest()
 138          IL = I[:32]
 139          IR = I[32:]
 140  
 141          IL_int = int.from_bytes(IL, "big")
 142          child_point = IL_int * secp256k1.G + self.pubkey.p
 143          # Per BIP32, if IL >= n or the resulting point is infinity, the key is invalid.
 144          if IL_int >= ORDER or child_point.infinity:
 145              raise ValueError("Invalid BIP32 child key")
 146  
 147          child_pubkey = ECPubKey()
 148          child_pubkey.set(child_point.to_bytes_compressed())
 149  
 150          return ExtendedPublicKey(child_pubkey, IR, self.depth + 1, self._fingerprint(), index)
 151  
 152      def _serialize(self):
 153          return (bytes([self.depth]) + self.parent_fingerprint_bytes + self.child_num.to_bytes(4, "big") + self.chaincode + self.pubkey.get_bytes())
 154  
 155      def derive_path(self, path):
 156          return derive_path(self, path, lambda x: int(x))
 157  
 158      def to_string(self, mainnet=False):
 159          return byte_to_base58(self._serialize(), get_version(private=False, mainnet=mainnet))
 160  
 161  class TestFrameworkExtendedKey(unittest.TestCase):
 162      def test_bip32_vectors(self):
 163          vectors = [
 164              [
 165                  "000102030405060708090a0b0c0d0e0f",
 166                  [
 167                      ["m", "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"],
 168                      ["m/0h", "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7", "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw"],
 169                      ["m/0h/1", "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs", "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ"],
 170                      ["m/0h/1/2h", "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM", "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"],
 171                      ["m/0h/1/2h/2", "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334", "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV"],
 172                      ["m/0h/1/2h/2/1000000000", "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76", "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy"],
 173                  ]
 174              ],
 175              [
 176                  "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
 177                  [
 178                      ["m", "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U", "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"],
 179                      ["m/0", "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt", "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH"],
 180                      ["m/0/2147483647h", "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9", "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a"],
 181                      ["m/0/2147483647h/1", "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef", "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon"],
 182                      ["m/0/2147483647h/1/2147483646h", "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc", "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"],
 183                      ["m/0/2147483647h/1/2147483646h/2", "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j", "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt"]
 184                  ]
 185              ]
 186          ]
 187  
 188          for vector in vectors:
 189              seed = bytes.fromhex(vector[0])
 190              xprv = ExtendedPrivateKey.from_seed(seed)
 191              for seed_vector in vector[1]:
 192                  path = seed_vector[0]
 193                  derivedxprv = xprv.derive_path(path)
 194                  self.assertEqual(derivedxprv.to_string(mainnet=True), seed_vector[1])
 195                  self.assertEqual(derivedxprv.pubkey().to_string(mainnet=True), seed_vector[2])
 196