Class: BSV::Primitives::PrivateKey
- Inherits:
-
Object
- Object
- BSV::Primitives::PrivateKey
- Defined in:
- lib/bsv/primitives/private_key.rb
Overview
A secp256k1 private key for signing transactions and deriving public keys.
Can be created from random entropy, raw bytes, hex, or WIF (Wallet Import Format). Produces deterministic ECDSA signatures via ECDSA.
Constant Summary collapse
- MAINNET_PREFIX =
WIF version prefix for mainnet private keys.
"\x80".b
- TESTNET_PREFIX =
WIF version prefix for testnet private keys.
"\xef".b
Instance Attribute Summary collapse
-
#bn ⇒ OpenSSL::BN
readonly
The private key as a big number.
Class Method Summary collapse
-
.from_backup_shares(shares) ⇒ PrivateKey
Reconstruct a private key from backup-format share strings.
-
.from_bytes(bytes) ⇒ PrivateKey
Create a private key from raw 32-byte big-endian encoding.
-
.from_hex(hex) ⇒ PrivateKey
Create a private key from a hex string.
-
.from_key_shares(key_shares) ⇒ PrivateKey
Reconstruct a private key from a KeyShares object.
-
.from_wif(wif_string) ⇒ PrivateKey
Create a private key from Wallet Import Format (WIF).
-
.generate ⇒ PrivateKey
Generate a new random private key using secure random bytes.
Instance Method Summary collapse
-
#derive_child(public_key, invoice_number) ⇒ PrivateKey
Derive a child private key using BRC-42 key derivation.
-
#derive_shared_secret(public_key) ⇒ PublicKey
Derive an ECDH shared secret with another party’s public key.
-
#initialize(bn) ⇒ PrivateKey
constructor
A new instance of PrivateKey.
-
#public_key ⇒ PublicKey
Derive the corresponding public key.
-
#sign(hash) ⇒ Signature
Sign a 32-byte hash using deterministic ECDSA (RFC 6979).
-
#to_backup_shares(threshold, total_shares) ⇒ Array<String>
Serialise this key as Shamir backup share strings.
-
#to_bytes ⇒ String
Serialise the private key as 32-byte big-endian binary.
-
#to_hex ⇒ String
Serialise the private key as a 64-character hex string.
-
#to_key_shares(threshold, total_shares) ⇒ KeyShares
Split this private key into Shamir’s Secret Sharing shares.
-
#to_wif(network: :mainnet) ⇒ String
Serialise the private key in Wallet Import Format (WIF).
Constructor Details
#initialize(bn) ⇒ PrivateKey
Returns a new instance of PrivateKey.
32 33 34 35 36 37 |
# File 'lib/bsv/primitives/private_key.rb', line 32 def initialize(bn) raise ArgumentError, 'private key must be an OpenSSL::BN' unless bn.is_a?(OpenSSL::BN) raise ArgumentError, 'private key out of range' if bn <= OpenSSL::BN.new('0') || bn >= Curve::N @bn = bn end |
Instance Attribute Details
#bn ⇒ OpenSSL::BN (readonly)
Returns the private key as a big number.
28 29 30 |
# File 'lib/bsv/primitives/private_key.rb', line 28 def bn @bn end |
Class Method Details
.from_backup_shares(shares) ⇒ PrivateKey
Reconstruct a private key from backup-format share strings.
275 276 277 |
# File 'lib/bsv/primitives/private_key.rb', line 275 def self.from_backup_shares(shares) from_key_shares(KeyShares.from_backup_format(shares)) end |
.from_bytes(bytes) ⇒ PrivateKey
Create a private key from raw 32-byte big-endian encoding.
54 55 56 |
# File 'lib/bsv/primitives/private_key.rb', line 54 def self.from_bytes(bytes) new(OpenSSL::BN.new(bytes, 2)) end |
.from_hex(hex) ⇒ PrivateKey
Create a private key from a hex string.
62 63 64 |
# File 'lib/bsv/primitives/private_key.rb', line 62 def self.from_hex(hex) new(OpenSSL::BN.new(hex, 16)) end |
.from_key_shares(key_shares) ⇒ PrivateKey
Reconstruct a private key from a KeyShares object.
Evaluates the Lagrange polynomial at x=0 to recover the secret, then checks the integrity hash against the reconstructed public key.
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/bsv/primitives/private_key.rb', line 249 def self.from_key_shares(key_shares) points = key_shares.points threshold = key_shares.threshold integrity = key_shares.integrity raise ArgumentError, 'threshold must be at least 2' if threshold < 2 raise ArgumentError, "at least #{threshold} shares are required" if points.length < threshold # Guard against duplicate x-coordinates xs = points.first(threshold).map { |p| p.x.to_s } raise ArgumentError, 'duplicate share detected; each share must be unique' if xs.length != xs.uniq.length poly = Polynomial.new(points.first(threshold), threshold) secret = poly.value_at(OpenSSL::BN.new('0')) key = new(secret) actual_integrity = key.public_key.hash160[0, 4].unpack1('H*') raise ArgumentError, 'integrity hash mismatch — shares may be corrupt or belong to a different key' unless actual_integrity == integrity key end |
.from_wif(wif_string) ⇒ PrivateKey
Create a private key from Wallet Import Format (WIF).
Supports both compressed and uncompressed WIF encodings, and both mainnet and testnet prefixes.
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/bsv/primitives/private_key.rb', line 74 def self.from_wif(wif_string) data = Base58.check_decode(wif_string) prefix = data[0] raise ArgumentError, "unknown WIF network prefix: 0x#{prefix.unpack1('H*')}" unless [MAINNET_PREFIX, TESTNET_PREFIX].include?(prefix) case data.length when 33 # Uncompressed: prefix (1) + key (32) from_bytes(data[1, 32]) when 34 # Compressed: prefix (1) + key (32) + 0x01 (1) raise ArgumentError, 'invalid compression flag' unless data[33] == "\x01".b from_bytes(data[1, 32]) else raise ArgumentError, "invalid WIF length: #{data.length}" end end |
.generate ⇒ PrivateKey
Generate a new random private key using secure random bytes.
42 43 44 45 46 47 48 |
# File 'lib/bsv/primitives/private_key.rb', line 42 def self.generate loop do bytes = SecureRandom.random_bytes(32) bn = OpenSSL::BN.new(bytes, 2) return new(bn) if bn > OpenSSL::BN.new('0') && bn < Curve::N end end |
Instance Method Details
#derive_child(public_key, invoice_number) ⇒ PrivateKey
Derive a child private key using BRC-42 key derivation.
Computes HMAC-SHA256(key: ECDH_shared_secret, msg: invoice_number) and adds it to this private key’s scalar mod n. The corresponding public key can be derived without the private key using BSV::Primitives::PublicKey#derive_child.
165 166 167 168 169 170 |
# File 'lib/bsv/primitives/private_key.rb', line 165 def derive_child(public_key, invoice_number) shared = derive_shared_secret(public_key) hmac = Digest.hmac_sha256(shared.compressed, invoice_number.encode('UTF-8')) hmac_bn = OpenSSL::BN.new(hmac.unpack1('H*'), 16) PrivateKey.new(@bn.mod_add(hmac_bn, Curve::N)) end |
#derive_shared_secret(public_key) ⇒ PublicKey
Derive an ECDH shared secret with another party’s public key.
Computes the shared point by multiplying the given public key by this private key’s scalar. The result is commutative:
alice_priv.derive_shared_secret(bob_pub) ==
bob_priv.derive_shared_secret(alice_pub)
Uses constant-time scalar multiplication to protect the private key scalar from timing side-channels.
This is the foundational primitive for BRC-42 key derivation, BRC-77/78 messaging, and ECIES encryption.
150 151 152 153 |
# File 'lib/bsv/primitives/private_key.rb', line 150 def derive_shared_secret(public_key) shared_point = Curve.multiply_point_ct(public_key.point, @bn) PublicKey.new(shared_point) end |
#public_key ⇒ PublicKey
Derive the corresponding public key.
Uses constant-time scalar multiplication to protect the private key scalar from timing side-channels during derivation.
131 132 133 |
# File 'lib/bsv/primitives/private_key.rb', line 131 def public_key @public_key ||= PublicKey.new(Curve.multiply_generator_ct(@bn)) end |
#sign(hash) ⇒ Signature
Sign a 32-byte hash using deterministic ECDSA (RFC 6979).
176 177 178 |
# File 'lib/bsv/primitives/private_key.rb', line 176 def sign(hash) ECDSA.sign(hash, @bn) end |
#to_backup_shares(threshold, total_shares) ⇒ Array<String>
Serialise this key as Shamir backup share strings.
Convenience wrapper around #to_key_shares and KeyShares#to_backup_format.
237 238 239 |
# File 'lib/bsv/primitives/private_key.rb', line 237 def to_backup_shares(threshold, total_shares) to_key_shares(threshold, total_shares).to_backup_format end |
#to_bytes ⇒ String
Serialise the private key as 32-byte big-endian binary.
96 97 98 99 100 |
# File 'lib/bsv/primitives/private_key.rb', line 96 def to_bytes raw = @bn.to_s(2) # Pad to 32 bytes raw.length < 32 ? ("\x00".b * (32 - raw.length)) + raw : raw end |
#to_hex ⇒ String
Serialise the private key as a 64-character hex string.
105 106 107 |
# File 'lib/bsv/primitives/private_key.rb', line 105 def to_hex to_bytes.unpack1('H*') end |
#to_key_shares(threshold, total_shares) ⇒ KeyShares
Split this private key into Shamir’s Secret Sharing shares.
Generates total_shares evaluation points on a random polynomial whose y-intercept encodes this key. Any threshold of them suffice to reconstruct the key. X-coordinates are derived via HMAC-SHA-512 over a 64-byte random seed, ensuring uniqueness even under partial RNG failure.
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/bsv/primitives/private_key.rb', line 191 def to_key_shares(threshold, total_shares) raise ArgumentError, 'threshold must be an integer' unless threshold.is_a?(Integer) raise ArgumentError, 'total_shares must be an integer' unless total_shares.is_a?(Integer) raise ArgumentError, 'threshold must be at least 2' if threshold < 2 raise ArgumentError, 'total_shares must be at least 2' if total_shares < 2 raise ArgumentError, 'threshold must be <= total_shares' if threshold > total_shares poly = Polynomial.from_private_key(self, threshold: threshold) seed = SecureRandom.random_bytes(64) used_x = {} points = [] total_shares.times do |i| x = nil attempts = 0 loop do counter_bytes = [i, attempts].pack('N*') + SecureRandom.random_bytes(32) h = Digest.hmac_sha512(seed, counter_bytes) candidate = OpenSSL::BN.new(h.unpack1('H*'), 16) % PointInFiniteField::P attempts += 1 raise ArgumentError, 'failed to generate unique x-coordinate after 5 attempts' if attempts > 5 next if candidate.zero? || used_x.key?(candidate.to_s) x = candidate break end used_x[x.to_s] = true y = poly.value_at(x) points << PointInFiniteField.new(x, y) end integrity = public_key.hash160[0, 4].unpack1('H*') KeyShares.new(points, threshold, integrity) end |
#to_wif(network: :mainnet) ⇒ String
Serialise the private key in Wallet Import Format (WIF).
Always produces a compressed WIF (the 0x01 compression flag is appended). BSV exclusively uses compressed public keys; uncompressed WIF export is not supported (“construct only what’s valid”).
The from_wif parser continues to accept both compressed and uncompressed WIF for import compatibility with legacy wallets.
120 121 122 123 |
# File 'lib/bsv/primitives/private_key.rb', line 120 def to_wif(network: :mainnet) prefix = network == :mainnet ? MAINNET_PREFIX : TESTNET_PREFIX Base58.check_encode(prefix + to_bytes + "\x01".b) end |