class Bitcoin::Key

bitcoin key class

Constants

COMPACT_SIGNATURE_SIZE
COMPACT_SIG_HEADER_BYTE
COMPRESSED_PUBLIC_KEY_SIZE
MAX_PRIV_KEY_MOD_ORDER

Order of secp256k1's generator minus 1.

MIN_PRIV_KEY_MOD_ORDER
PUBLIC_KEY_SIZE
SIGNATURE_SIZE
TYPES

Attributes

key_type[RW]
priv_key[RW]
pubkey[RW]
secp256k1_module[R]

Public Class Methods

compress_or_uncompress_pubkey?(pubkey) click to toggle source

check pubkey (hex) is compress or uncompress pubkey.

# File lib/bitcoin/key.rb, line 221
def self.compress_or_uncompress_pubkey?(pubkey)
  p = pubkey.htb
  return false if p.bytesize < COMPRESSED_PUBLIC_KEY_SIZE
  case p[0]
    when "\x04"
      return false unless p.bytesize == PUBLIC_KEY_SIZE
    when "\x02", "\x03"
      return false unless p.bytesize == COMPRESSED_PUBLIC_KEY_SIZE
    else
      return false
  end
  true
end
compress_pubkey?(pubkey) click to toggle source

check pubkey (hex) is compress pubkey.

# File lib/bitcoin/key.rb, line 236
def self.compress_pubkey?(pubkey)
  p = pubkey.htb
  p.bytesize == COMPRESSED_PUBLIC_KEY_SIZE && ["\x02", "\x03"].include?(p[0])
end
from_point(point, compressed: true) click to toggle source

Generate from public key point. @param [ECDSA::Point] point Public key point. @param [Boolean] compressed whether compressed or not. @return [Bitcoin::Key]

# File lib/bitcoin/key.rb, line 93
def self.from_point(point, compressed: true)
  pubkey = ECDSA::Format::PointOctetString.encode(point, compression: compressed).bth
  Bitcoin::Key.new(pubkey: pubkey, key_type: TYPES[:compressed])
end
from_wif(wif) click to toggle source

import private key from wif format en.bitcoin.it/wiki/Wallet_import_format

# File lib/bitcoin/key.rb, line 61
def self.from_wif(wif)
  hex = Base58.decode(wif)
  raise ArgumentError, 'data is too short' if hex.htb.bytesize < 4
  version = hex[0..1]
  data = hex[2...-8].htb
  checksum = hex[-8..-1]
  raise ArgumentError, 'invalid version' unless version == Bitcoin.chain_params.privkey_version
  raise ArgumentError, Errors::Messages::INVALID_CHECKSUM unless Bitcoin.calc_checksum(version + data.bth) == checksum
  key_len = data.bytesize
  if key_len == COMPRESSED_PUBLIC_KEY_SIZE && data[-1].unpack1('C') == 1
    key_type = TYPES[:compressed]
    data = data[0..-2]
  elsif key_len == 32
    key_type = TYPES[:uncompressed]
  else
    raise ArgumentError, 'Wrong number of bytes for a private key, not 32 or 33'
  end
  new(priv_key: data.bth, key_type: key_type)
end
from_xonly_pubkey(xonly_pubkey) click to toggle source

Generate from xonly public key. @param [String] xonly_pubkey xonly public key with hex format. @return [Bitcoin::Key] key object has public key.

# File lib/bitcoin/key.rb, line 84
def self.from_xonly_pubkey(xonly_pubkey)
  raise ArgumentError, 'xonly_pubkey must be 32 bytes' unless xonly_pubkey.htb.bytesize == 32
  Bitcoin::Key.new(pubkey: "02#{xonly_pubkey}", key_type: TYPES[:compressed])
end
generate(key_type = TYPES[:compressed]) click to toggle source

generate key pair

# File lib/bitcoin/key.rb, line 54
def self.generate(key_type = TYPES[:compressed])
  priv_key, pubkey = Bitcoin.secp_impl.generate_key_pair
  new(priv_key: priv_key, pubkey: pubkey, key_type: key_type)
end
low_signature?(sig) click to toggle source

check sig is low.

# File lib/bitcoin/key.rb, line 242
def self.low_signature?(sig)
  s = sig.unpack('C*')
  len_r = s[3]
  len_s = s[5 + len_r]
  val_s = s.slice(6 + len_r, len_s)
  max_mod_half_order = [
      0x7f,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
      0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
      0x5d,0x57,0x6e,0x73,0x57,0xa4,0x50,0x1d,
      0xdf,0xe9,0x2f,0x46,0x68,0x1b,0x20,0xa0]
  compare_big_endian(val_s, [0]) > 0 &&
      compare_big_endian(val_s, max_mod_half_order) <= 0
end
new(priv_key: nil, pubkey: nil, key_type: nil, compressed: true, allow_hybrid: false) click to toggle source

initialize private key @param [String] priv_key a private key with hex format. @param [String] pubkey a public key with hex format. @param [Integer] key_type a key type which determine address type. @param [Boolean] compressed [Deprecated] whether public key is compressed. @return [Bitcoin::Key] a key object.

# File lib/bitcoin/key.rb, line 32
def initialize(priv_key: nil, pubkey: nil, key_type: nil, compressed: true, allow_hybrid: false)
  puts "[Warning] Use key_type parameter instead of compressed. compressed parameter removed in the future." if key_type.nil? && !compressed.nil? && pubkey.nil?
  if key_type
    @key_type = key_type
    compressed = @key_type != TYPES[:uncompressed]
  else
    @key_type = compressed ? TYPES[:compressed] : TYPES[:uncompressed]
  end
  @secp256k1_module =  Bitcoin.secp_impl
  @priv_key = priv_key
  if @priv_key
    raise ArgumentError, Errors::Messages::INVALID_PRIV_KEY unless validate_private_key_range(@priv_key)
  end
  if pubkey
    @pubkey = pubkey
  else
    @pubkey = generate_pubkey(priv_key, compressed: compressed) if priv_key
  end
  raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless fully_valid_pubkey?(allow_hybrid)
end
recover_compact(data, signature) click to toggle source

Recover public key from compact signature. @param [String] data message digest using signature. @param [String] signature signature with binary format. @return [Bitcoin::Key] Recovered public key.

# File lib/bitcoin/key.rb, line 147
def self.recover_compact(data, signature)
  rec_id = signature.unpack1('C')
  rec = rec_id - Bitcoin::Key::COMPACT_SIG_HEADER_BYTE
  raise ArgumentError, 'Invalid signature parameter' if rec < 0 || rec > 15
  rec = rec & 3
  compressed = (rec_id - Bitcoin::Key::COMPACT_SIG_HEADER_BYTE) & 4 != 0
  Bitcoin.secp_impl.recover_compact(data, signature, rec, compressed)
end
valid_signature_encoding?(sig) click to toggle source

check sig is correct der encoding. This function is consensus-critical since BIP66.

# File lib/bitcoin/key.rb, line 259
def self.valid_signature_encoding?(sig)
  return false if sig.bytesize < 9 || sig.bytesize > 73 # Minimum and maximum size check

  s = sig.unpack('C*')

  return false if s[0] != 0x30 || s[1] != s.size - 3 # A signature is of type 0x30 (compound). Make sure the length covers the entire signature.

  len_r = s[3]
  return false if 5 + len_r >= s.size # Make sure the length of the S element is still inside the signature.

  len_s = s[5 + len_r]
  return false unless len_r + len_s + 7 == s.size #Verify that the length of the signature matches the sum of the length of the elements.

  return false unless s[2] == 0x02 # Check whether the R element is an integer.

  return false if len_r == 0 # Zero-length integers are not allowed for R.

  return false unless s[4] & 0x80 == 0 # Negative numbers are not allowed for R.

  # Null bytes at the start of R are not allowed, unless R would otherwise be interpreted as a negative number.
  return false if len_r > 1 && (s[4] == 0x00) && (s[5] & 0x80 == 0)

  return false unless s[len_r + 4] == 0x02 # Check whether the S element is an integer.

  return false if len_s == 0 # Zero-length integers are not allowed for S.
  return false unless (s[len_r + 6] & 0x80) == 0 # Negative numbers are not allowed for S.

  # Null bytes at the start of S are not allowed, unless S would otherwise be interpreted as a negative number.
  return false if len_s > 1 && (s[len_r + 6] == 0x00) && (s[len_r + 7] & 0x80 == 0)

  true
end

Private Class Methods

compare_big_endian(c1, c2) click to toggle source
# File lib/bitcoin/key.rb, line 299
def self.compare_big_endian(c1, c2)
  c1, c2 = c1.dup, c2.dup # Clone the arrays

  while c1.size > c2.size
    return 1 if c1.shift > 0
  end

  while c2.size > c1.size
    return -1 if c2.shift > 0
  end

  c1.size.times{|idx| return c1[idx] - c2[idx] if c1[idx] != c2[idx] }
  0
end

Public Instance Methods

compressed?() click to toggle source
# File lib/bitcoin/key.rb, line 201
def compressed?
  key_type != TYPES[:uncompressed]
end
fully_valid_pubkey?(allow_hybrid = false) click to toggle source

fully validate whether this is a valid public key (more expensive than IsValid())

# File lib/bitcoin/key.rb, line 293
def fully_valid_pubkey?(allow_hybrid = false)
  valid_pubkey? && secp256k1_module.parse_ec_pubkey?(pubkey, allow_hybrid)
end
hash160() click to toggle source

get hash160 public key.

# File lib/bitcoin/key.rb, line 179
def hash160
  Bitcoin.hash160(pubkey)
end
sign(data, low_r = true, extra_entropy = nil, algo: :ecdsa) click to toggle source

sign data with private key @param [String] data a data to be signed with binary format @param [Boolean] low_r flag to apply low-R. @param [String] extra_entropy the extra entropy with binary format for rfc6979. @param [Symbol] algo signature algorithm. ecdsa(default) or schnorr. @return [String] signature data with binary format

# File lib/bitcoin/key.rb, line 113
def sign(data, low_r = true, extra_entropy = nil, algo: :ecdsa)
  case algo
  when :ecdsa
    sig = secp256k1_module.sign_data(data, priv_key, extra_entropy)
    if low_r && !sig_has_low_r?(sig)
      counter = 1
      until sig_has_low_r?(sig)
        extra_entropy = [counter].pack('I*').bth.ljust(64, '0').htb
        sig = secp256k1_module.sign_data(data, priv_key, extra_entropy)
        counter += 1
      end
    end
    sig
  when :schnorr
    secp256k1_module.sign_data(data, priv_key, extra_entropy, algo: :schnorr)
  else
    raise ArgumentError "Unsupported algo specified: #{algo}"
  end
end
sign_compact(data) click to toggle source

Sign compact signature. @param [String] data message digest to be signed. @return [String] compact signature with binary format.

# File lib/bitcoin/key.rb, line 136
def sign_compact(data)
  signature, rec = secp256k1_module.sign_compact(data, priv_key)
  rec = Bitcoin::Key::COMPACT_SIG_HEADER_BYTE + rec + (compressed? ? 4 : 0)
  [rec].pack('C') + ECDSA::Format::IntegerOctetString.encode(signature.r, 32) +
    ECDSA::Format::IntegerOctetString.encode(signature.s, 32)
end
to_nested_p2wpkh() click to toggle source

get p2wpkh address nested in p2sh. @deprecated

# File lib/bitcoin/key.rb, line 197
def to_nested_p2wpkh
  Bitcoin::Script.to_p2wpkh(hash160).to_p2sh.to_addr
end
to_p2pkh() click to toggle source

get pay to pubkey hash address @deprecated

# File lib/bitcoin/key.rb, line 185
def to_p2pkh
  Bitcoin::Script.to_p2pkh(hash160).to_addr
end
to_p2wpkh() click to toggle source

get pay to witness pubkey hash address @deprecated

# File lib/bitcoin/key.rb, line 191
def to_p2wpkh
  Bitcoin::Script.to_p2wpkh(hash160).to_addr
end
to_point() click to toggle source

generate pubkey ec point @return [ECDSA::Point]

# File lib/bitcoin/key.rb, line 207
def to_point
  p = pubkey
  p ||= generate_pubkey(priv_key, compressed: compressed?)
  ECDSA::Format::PointOctetString.decode(p.htb, Bitcoin::Secp256k1::GROUP)
end
to_wif() click to toggle source

export private key with wif format

# File lib/bitcoin/key.rb, line 99
def to_wif
  version = Bitcoin.chain_params.privkey_version
  hex = version + priv_key
  hex += '01' if compressed?
  hex += Bitcoin.calc_checksum(hex)
  Base58.encode(hex)
end
verify(sig, data, algo: :ecdsa) click to toggle source

verify signature using public key @param [String] sig signature data with binary format @param [String] data original message @param [Symbol] algo signature algorithm. ecdsa(default) or schnorr. @return [Boolean] verify result

# File lib/bitcoin/key.rb, line 161
def verify(sig, data, algo: :ecdsa)
  return false unless valid_pubkey?
  begin
    case algo
    when :ecdsa
      sig = ecdsa_signature_parse_der_lax(sig)
      secp256k1_module.verify_sig(data, sig, pubkey)
    when :schnorr
      secp256k1_module.verify_sig(data, sig, xonly_pubkey, algo: :schnorr)
    else
      false
    end
  rescue Exception
    false
  end
end
xonly_pubkey() click to toggle source

get xonly public key (32 bytes). @return [String] xonly public key with hex format

# File lib/bitcoin/key.rb, line 215
def xonly_pubkey
  puts "Derive a public key whose y-coordinate is different from this public key." if compressed? && pubkey[0...2] != '02'
  pubkey[2..65]
end

Private Instance Methods

ecdsa_signature_parse_der_lax(sig) click to toggle source

Supported violations include negative integers, excessive padding, garbage at the end, and overly long length descriptors. This is safe to use in Bitcoin because since the activation of BIP66, signatures are verified to be strict DER before being passed to this module, and we know it supports all violations present in the blockchain before that point.

# File lib/bitcoin/key.rb, line 333
def ecdsa_signature_parse_der_lax(sig)
  sig_array = sig.unpack('C*')
  len_r = sig_array[3]
  r = sig_array[4...(len_r+4)].pack('C*').bth
  len_s = sig_array[len_r + 5]
  s = sig_array[(len_r + 6)...(len_r + 6 + len_s)].pack('C*').bth
  ECDSA::Signature.new(r.to_i(16), s.to_i(16)).to_der
end
generate_pubkey(privkey, compressed: true) click to toggle source

generate publick key from private key @param [String] privkey a private key with string format @param [Boolean] compressed pubkey compressed? @return [String] a pubkey which generate from privkey

# File lib/bitcoin/key.rb, line 318
def generate_pubkey(privkey, compressed: true)
  @secp256k1_module.generate_pubkey(privkey, compressed: compressed)
end
sig_has_low_r?(sig) click to toggle source

check whether the signature is low-R @param [String] sig the signature data @return [Boolean] result

# File lib/bitcoin/key.rb, line 349
def sig_has_low_r?(sig)
  sig[3].bth.to_i(16) == 0x20 && sig[4].bth.to_i(16) < 0x80
end
valid_pubkey?() click to toggle source
# File lib/bitcoin/key.rb, line 342
def valid_pubkey?
  !pubkey.nil? && pubkey.size > 0
end
validate_private_key_range(private_key) click to toggle source

check private key range.

# File lib/bitcoin/key.rb, line 323
def validate_private_key_range(private_key)
  value = private_key.to_i(16)
  MIN_PRIV_KEY_MOD_ORDER <= value && value <= MAX_PRIV_KEY_MOD_ORDER
end