Class: BSV::Primitives::ExtendedKey
- Inherits:
-
Object
- Object
- BSV::Primitives::ExtendedKey
- Defined in:
- lib/bsv/primitives/extended_key.rb
Overview
BIP-32 hierarchical deterministic (HD) extended key.
Supports both private and public extended keys, serialised as Base58Check xprv/xpub strings. Provides child key derivation (normal and hardened), path-based derivation (+m/44’/0’/0’+), and neutering (private → public).
Constant Summary collapse
- HARDENED =
Offset added to child indices for hardened derivation.
0x80000000- VERSIONS =
Version bytes for extended key serialisation (BIP-32).
{ mainnet_private: "\x04\x88\xAD\xE4".b, mainnet_public: "\x04\x88\xB2\x1E".b, testnet_private: "\x04\x35\x83\x94".b, testnet_public: "\x04\x35\x87\xCF".b }.freeze
- PRIVATE_VERSIONS =
Private extended key version bytes.
[VERSIONS[:mainnet_private], VERSIONS[:testnet_private]].freeze
- PUBLIC_VERSIONS =
Public extended key version bytes.
[VERSIONS[:mainnet_public], VERSIONS[:testnet_public]].freeze
Instance Attribute Summary collapse
-
#chain_code ⇒ String
readonly
32-byte chain code for child derivation.
-
#child_number ⇒ Integer
readonly
Child number (index used to derive this key).
-
#depth ⇒ Integer
readonly
Depth in the derivation tree (0 = master).
-
#key ⇒ String
readonly
Raw key bytes (32-byte private or 33-byte compressed public).
-
#parent_fingerprint ⇒ String
readonly
4-byte fingerprint of the parent key.
-
#version ⇒ String
readonly
4-byte version prefix.
Class Method Summary collapse
-
.from_seed(seed, network: :mainnet) ⇒ ExtendedKey
Derive a master extended key from a binary seed.
-
.from_string(base58) ⇒ ExtendedKey
Parse an extended key from a Base58Check-encoded string (xprv/xpub).
Instance Method Summary collapse
-
#child(index) ⇒ ExtendedKey
Derive a child key at the given index.
-
#derive_path(path) ⇒ ExtendedKey
Derive a child key from a BIP-32 path string.
-
#fingerprint ⇒ String
The 4-byte fingerprint of this key (first 4 bytes of identifier).
-
#identifier ⇒ String
The 20-byte Hash160 identifier for this key.
-
#initialize(key:, chain_code:, version:, depth: 0, parent_fingerprint: "\x00\x00\x00\x00".b, child_number: 0) ⇒ ExtendedKey
constructor
A new instance of ExtendedKey.
-
#neuter ⇒ ExtendedKey
Convert a private extended key to its public counterpart.
-
#private? ⇒ Boolean
Whether this is a private extended key.
-
#private_key ⇒ PrivateKey
Extract the PrivateKey from a private extended key.
-
#public? ⇒ Boolean
Whether this is a public extended key.
-
#public_key ⇒ PublicKey
Extract the PublicKey from this extended key.
-
#to_s ⇒ String
Serialise as a Base58Check-encoded string (xprv or xpub).
Constructor Details
#initialize(key:, chain_code:, version:, depth: 0, parent_fingerprint: "\x00\x00\x00\x00".b, child_number: 0) ⇒ ExtendedKey
Returns a new instance of ExtendedKey.
65 66 67 68 69 70 71 72 |
# File 'lib/bsv/primitives/extended_key.rb', line 65 def initialize(key:, chain_code:, version:, depth: 0, parent_fingerprint: "\x00\x00\x00\x00".b, child_number: 0) @key = key @chain_code = chain_code @depth = depth @parent_fingerprint = parent_fingerprint @child_number = child_number @version = version end |
Instance Attribute Details
#chain_code ⇒ String (readonly)
Returns 32-byte chain code for child derivation.
45 46 47 |
# File 'lib/bsv/primitives/extended_key.rb', line 45 def chain_code @chain_code end |
#child_number ⇒ Integer (readonly)
Returns child number (index used to derive this key).
54 55 56 |
# File 'lib/bsv/primitives/extended_key.rb', line 54 def child_number @child_number end |
#depth ⇒ Integer (readonly)
Returns depth in the derivation tree (0 = master).
48 49 50 |
# File 'lib/bsv/primitives/extended_key.rb', line 48 def depth @depth end |
#key ⇒ String (readonly)
Returns raw key bytes (32-byte private or 33-byte compressed public).
42 43 44 |
# File 'lib/bsv/primitives/extended_key.rb', line 42 def key @key end |
#parent_fingerprint ⇒ String (readonly)
Returns 4-byte fingerprint of the parent key.
51 52 53 |
# File 'lib/bsv/primitives/extended_key.rb', line 51 def parent_fingerprint @parent_fingerprint end |
#version ⇒ String (readonly)
Returns 4-byte version prefix.
57 58 59 |
# File 'lib/bsv/primitives/extended_key.rb', line 57 def version @version end |
Class Method Details
.from_seed(seed, network: :mainnet) ⇒ ExtendedKey
Derive a master extended key from a binary seed.
Uses HMAC-SHA-512 with key “Bitcoin seed” per BIP-32.
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/bsv/primitives/extended_key.rb', line 82 def self.from_seed(seed, network: :mainnet) seed = seed.b raise ArgumentError, 'seed must be between 16 and 64 bytes' unless seed.length.between?(16, 64) hmac = Digest.hmac_sha512('Bitcoin seed', seed) il = hmac[0, 32] ir = hmac[32, 32] il_bn = OpenSSL::BN.new(il, 2) raise ArgumentError, 'invalid seed: derived key is zero or >= curve order' if il_bn.zero? || il_bn >= Curve::N new( key: il, chain_code: ir, version: VERSIONS[:"#{network}_private"] ) end |
.from_string(base58) ⇒ ExtendedKey
Parse an extended key from a Base58Check-encoded string (xprv/xpub).
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/bsv/primitives/extended_key.rb', line 105 def self.from_string(base58) data = Base58.check_decode(base58) raise ArgumentError, "invalid extended key length: #{data.length}" unless data.length == 78 version = data[0, 4] depth = data[4].unpack1('C') parent_fingerprint = data[5, 4] child_number = data[9, 4].unpack1('N') chain_code = data[13, 32] key_data = data[45, 33] if key_data[0] == "\x00".b raise ArgumentError, 'private key data with public version bytes' unless PRIVATE_VERSIONS.include?(version) key = key_data[1, 32] elsif ["\x02".b, "\x03".b].include?(key_data[0]) raise ArgumentError, 'public key data with private version bytes' unless PUBLIC_VERSIONS.include?(version) key = key_data else raise ArgumentError, "invalid key data prefix: 0x#{key_data[0].unpack1('H*')}" end new( key: key, chain_code: chain_code, version: version, depth: depth, parent_fingerprint: parent_fingerprint, child_number: child_number ) end |
Instance Method Details
#child(index) ⇒ ExtendedKey
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/bsv/primitives/extended_key.rb', line 160 def child(index) raise ArgumentError, 'cannot derive child at depth 255' if @depth >= 255 if index >= HARDENED raise ArgumentError, 'cannot derive hardened child from public key' if public? data = "\x00".b + padded_key + [index].pack('N') else data = compressed_pubkey_bytes + [index].pack('N') end hmac = Digest.hmac_sha512(@chain_code, data) il = hmac[0, 32] ir = hmac[32, 32] il_bn = OpenSSL::BN.new(il, 2) raise ArgumentError, 'invalid child: IL >= curve order' if il_bn >= Curve::N fp = fingerprint child_key_bytes = if private? child_key_bn = il_bn.mod_add(OpenSSL::BN.new(@key, 2), Curve::N) raise ArgumentError, 'invalid child: derived key is zero' if child_key_bn.zero? bn_to_32bytes(child_key_bn) else parent_point = Curve.point_from_bytes(@key) il_point = Curve.multiply_generator_ct(il_bn) child_point = Curve.add_points(parent_point, il_point) raise ArgumentError, 'invalid child: derived point is at infinity' if child_point.infinity? child_point.to_octet_string(:compressed) end self.class.new( key: child_key_bytes, chain_code: ir, version: @version, depth: @depth + 1, parent_fingerprint: fp, child_number: index ) end |
#derive_path(path) ⇒ ExtendedKey
Derive a child key from a BIP-32 path string.
209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/bsv/primitives/extended_key.rb', line 209 def derive_path(path) components = path.strip.split('/') raise ArgumentError, "path must start with 'm'" unless components.first == 'm' components[1..].reduce(self) do |key, component| hardened = component.end_with?("'", 'H', 'h') numeric_part = component.delete("'Hh") raise ArgumentError, "invalid path component: '#{component}'" unless numeric_part.match?(/\A\d+\z/) index = numeric_part.to_i index += HARDENED if hardened key.child(index) end end |
#fingerprint ⇒ String
The 4-byte fingerprint of this key (first 4 bytes of identifier).
283 284 285 |
# File 'lib/bsv/primitives/extended_key.rb', line 283 def fingerprint identifier[0, 4] end |
#identifier ⇒ String
The 20-byte Hash160 identifier for this key.
290 291 292 |
# File 'lib/bsv/primitives/extended_key.rb', line 290 def identifier Digest.hash160(compressed_pubkey_bytes) end |
#neuter ⇒ ExtendedKey
Convert a private extended key to its public counterpart.
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 |
# File 'lib/bsv/primitives/extended_key.rb', line 228 def neuter raise ArgumentError, 'already a public key' if public? pub_version = if @version == VERSIONS[:mainnet_private] VERSIONS[:mainnet_public] else VERSIONS[:testnet_public] end self.class.new( key: compressed_pubkey_bytes, chain_code: @chain_code, version: pub_version, depth: @depth, parent_fingerprint: @parent_fingerprint, child_number: @child_number ) end |
#private? ⇒ Boolean
Whether this is a private extended key.
141 142 143 |
# File 'lib/bsv/primitives/extended_key.rb', line 141 def private? PRIVATE_VERSIONS.include?(@version) end |
#private_key ⇒ PrivateKey
Extract the PrivateKey from a private extended key.
267 268 269 270 271 |
# File 'lib/bsv/primitives/extended_key.rb', line 267 def private_key raise ArgumentError, 'not a private extended key' unless private? PrivateKey.from_bytes(padded_key) end |
#public? ⇒ Boolean
Whether this is a public extended key.
148 149 150 |
# File 'lib/bsv/primitives/extended_key.rb', line 148 def public? PUBLIC_VERSIONS.include?(@version) end |
#public_key ⇒ PublicKey
Extract the PublicKey from this extended key.
276 277 278 |
# File 'lib/bsv/primitives/extended_key.rb', line 276 def public_key PublicKey.from_bytes(compressed_pubkey_bytes) end |
#to_s ⇒ String
Serialise as a Base58Check-encoded string (xprv or xpub).
250 251 252 253 254 255 256 257 258 259 260 261 |
# File 'lib/bsv/primitives/extended_key.rb', line 250 def to_s key_data = private? ? "\x00".b + padded_key : @key payload = @version + [@depth].pack('C') + @parent_fingerprint + [@child_number].pack('N') + @chain_code + key_data Base58.check_encode(payload) end |