Class: Bitcoin::Key

Inherits:
Object
  • Object
show all
Defined in:
lib/bitcoin/key.rb

Overview

bitcoin key class

Constant Summary collapse

PUBLIC_KEY_SIZE =
65
COMPRESSED_PUBLIC_KEY_SIZE =
33
SIGNATURE_SIZE =
72
COMPACT_SIGNATURE_SIZE =
65
COMPACT_SIG_HEADER_BYTE =
0x1b
TYPES =
{uncompressed: 0x00, compressed: 0x01, p2pkh: 0x10, p2wpkh: 0x11, p2wpkh_p2sh: 0x12, p2tr: 0x13}
MIN_PRIV_KEY_MOD_ORDER =
0x01
MAX_PRIV_KEY_MOD_ORDER =

Order of secp256k1’s generator minus 1.

ECDSA::Group::Secp256k1.order - 1

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(priv_key: nil, pubkey: nil, key_type: nil, compressed: true, allow_hybrid: false) ⇒ Bitcoin::Key

initialize private key

Parameters:

  • priv_key (String) (defaults to: nil)

    a private key with hex format.

  • pubkey (String) (defaults to: nil)

    a public key with hex format.

  • key_type (Integer) (defaults to: nil)

    a key type which determine address type.

  • compressed (Boolean) (defaults to: true)
    Deprecated

    whether public key is compressed.

Raises:

  • (ArgumentError)


32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/bitcoin/key.rb', line 32

def initialize(priv_key: nil, pubkey: nil, key_type: nil, compressed: true, allow_hybrid: false)
  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, 'Private key must be 32 bytes.' unless priv_key.htb.bytesize == 32
    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

Instance Attribute Details

#key_typeObject

Returns the value of attribute key_type.



17
18
19
# File 'lib/bitcoin/key.rb', line 17

def key_type
  @key_type
end

#priv_keyObject

Returns the value of attribute priv_key.



15
16
17
# File 'lib/bitcoin/key.rb', line 15

def priv_key
  @priv_key
end

#pubkeyObject

Returns the value of attribute pubkey.



16
17
18
# File 'lib/bitcoin/key.rb', line 16

def pubkey
  @pubkey
end

#secp256k1_moduleObject (readonly)

Returns the value of attribute secp256k1_module.



18
19
20
# File 'lib/bitcoin/key.rb', line 18

def secp256k1_module
  @secp256k1_module
end

Class Method Details

.compress_or_uncompress_pubkey?(pubkey) ⇒ Boolean

check pubkey (hex) is compress or uncompress pubkey.

Returns:

  • (Boolean)


271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/bitcoin/key.rb', line 271

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) ⇒ Boolean

check pubkey (hex) is compress pubkey.

Returns:

  • (Boolean)


286
287
288
289
# File 'lib/bitcoin/key.rb', line 286

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) ⇒ Bitcoin::Key

Generate from public key point.

Parameters:

  • point (ECDSA::Point)

    Public key point.

  • compressed (Boolean) (defaults to: true)

    whether compressed or not.

Returns:



93
94
95
96
# 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) ⇒ Object

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

Raises:

  • (ArgumentError)


61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# 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) ⇒ Bitcoin::Key

Generate from xonly public key.

Parameters:

  • xonly_pubkey (String)

    xonly public key with hex format.

Returns:

Raises:

  • (ArgumentError)


84
85
86
87
# File 'lib/bitcoin/key.rb', line 84

def self.from_xonly_pubkey(xonly_pubkey)
  raise ArgumentError, "xonly_pubkey must be #{X_ONLY_PUBKEY_SIZE} bytes" unless xonly_pubkey.htb.bytesize == X_ONLY_PUBKEY_SIZE
  Bitcoin::Key.new(pubkey: "02#{xonly_pubkey}", key_type: TYPES[:compressed])
end

.generate(key_type = TYPES[:compressed]) ⇒ Object

generate key pair



54
55
56
57
# File 'lib/bitcoin/key.rb', line 54

def self.generate(key_type = TYPES[:compressed])
  priv_key, pubkey = Bitcoin.secp_impl.generate_key_pair(compressed: key_type != TYPES[:uncompressed])
  new(priv_key: priv_key, pubkey: pubkey, key_type: key_type)
end

.low_signature?(sig) ⇒ Boolean

check sig is low.

Returns:

  • (Boolean)


292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/bitcoin/key.rb', line 292

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

.recover_compact(data, signature) ⇒ Bitcoin::Key

Recover public key from compact signature.

Parameters:

  • data (String)

    message digest using signature.

  • signature (String)

    signature with binary format.

Returns:

Raises:

  • (ArgumentError)


147
148
149
150
151
152
153
154
# 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) & 3
  raise ArgumentError, 'Invalid signature parameter' if rec < 0 || 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) ⇒ Boolean

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

Returns:

  • (Boolean)


309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/bitcoin/key.rb', line 309

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

Instance Method Details

#compressed?Boolean

Returns:

  • (Boolean)


246
247
248
# File 'lib/bitcoin/key.rb', line 246

def compressed?
  key_type != TYPES[:uncompressed]
end

#create_ell_pubkeyBitcoin::BIP324::EllSwiftPubkey

Create an ellswift-encoded public key for this key, with specified entropy.

Returns:

Raises:

  • ArgumentError If ent32 does not 32 bytes.



350
351
352
353
354
355
356
357
# File 'lib/bitcoin/key.rb', line 350

def create_ell_pubkey
  raise ArgumentError, "private_key required." unless priv_key
  if secp256k1_module.is_a?(Bitcoin::Secp256k1::Native)
    Bitcoin::BIP324::EllSwiftPubkey.new(secp256k1_module.ellswift_create(priv_key))
  else
    Bitcoin::BIP324::EllSwiftPubkey.new(Bitcoin::BIP324.xelligatorswift(xonly_pubkey))
  end
end

#decompress_pubkeyString

Convert this key to decompress key.

Returns:

  • (String)

    decompress public key with hex format.



266
267
268
# File 'lib/bitcoin/key.rb', line 266

def decompress_pubkey
  pubkey.htb.bytesize == PUBLIC_KEY_SIZE ? pubkey : to_point.to_hex(false)
end

#fully_valid_pubkey?(allow_hybrid = false) ⇒ Boolean

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

Returns:

  • (Boolean)


343
344
345
# File 'lib/bitcoin/key.rb', line 343

def fully_valid_pubkey?(allow_hybrid = false)
  valid_pubkey? && secp256k1_module.parse_ec_pubkey?(pubkey, allow_hybrid)
end

#hash160Object

get hash160 public key.



179
180
181
# File 'lib/bitcoin/key.rb', line 179

def hash160
  Bitcoin.hash160(pubkey)
end

#nested_p2wpkh?Boolean

Determine if it is a nested P2WPKH from key_type.

Returns:

  • (Boolean)


221
222
223
# File 'lib/bitcoin/key.rb', line 221

def nested_p2wpkh?
  key_type == TYPES[:p2wpkh_p2sh]
end

#p2pkh?Boolean

Determine if it is a P2PKH from key_type.

Returns:

  • (Boolean)


209
210
211
# File 'lib/bitcoin/key.rb', line 209

def p2pkh?
  [TYPES[:uncompressed], TYPES[:compressed], TYPES[:p2pkh]].include?(key_type)
end

#p2tr?Boolean

Determine if it is a nested P2TR from key_type.

Returns:

  • (Boolean)


227
228
229
# File 'lib/bitcoin/key.rb', line 227

def p2tr?
  key_type == TYPES[:p2tr]
end

#p2wpkh?Boolean

Determine if it is a P2WPKH from key_type.

Returns:

  • (Boolean)


215
216
217
# File 'lib/bitcoin/key.rb', line 215

def p2wpkh?
  key_type == TYPES[:p2wpkh]
end

#sign(data, low_r = true, extra_entropy = nil, algo: :ecdsa) ⇒ String

sign data with private key

Parameters:

  • data (String)

    a data to be signed with binary format

  • low_r (Boolean) (defaults to: true)

    flag to apply low-R.

  • extra_entropy (String) (defaults to: nil)

    the extra entropy with binary format for rfc6979.

  • algo (Symbol) (defaults to: :ecdsa)

    signature algorithm. ecdsa(default) or schnorr.

Returns:

  • (String)

    signature data with binary format



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# 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) ⇒ String

Sign compact signature.

Parameters:

  • data (String)

    message digest to be signed.

Returns:

  • (String)

    compact signature with binary format.



136
137
138
139
140
141
# 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_addrString

Returns the address corresponding to key_type.

Returns:



233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/bitcoin/key.rb', line 233

def to_addr
  case key_type
  when TYPES[:uncompressed], TYPES[:compressed], TYPES[:p2pkh]
    to_p2pkh
  when TYPES[:p2wpkh]
    to_p2wpkh
  when TYPES[:p2wpkh_p2sh]
    to_nested_p2wpkh
  when TYPES[:p2tr]
    to_p2tr
  end
end

#to_nested_p2wpkhObject

Deprecated.

get p2wpkh address nested in p2sh.



203
204
205
# File 'lib/bitcoin/key.rb', line 203

def to_nested_p2wpkh
  Bitcoin::Script.to_p2wpkh(hash160).to_p2sh.to_addr
end

#to_p2pkhString

get pay to pubkey hash address

Returns:



185
186
187
# File 'lib/bitcoin/key.rb', line 185

def to_p2pkh
  Bitcoin::Script.to_p2pkh(hash160).to_addr
end

#to_p2trString

Get pay to taproot address

Returns:



197
198
199
# File 'lib/bitcoin/key.rb', line 197

def to_p2tr
  Bitcoin::Script.to_p2tr(self).to_addr
end

#to_p2wpkhString

get pay to witness pubkey hash address

Returns:



191
192
193
# File 'lib/bitcoin/key.rb', line 191

def to_p2wpkh
  Bitcoin::Script.to_p2wpkh(hash160).to_addr
end

#to_pointECDSA::Point

generate pubkey ec point

Returns:

  • (ECDSA::Point)


252
253
254
255
256
# File 'lib/bitcoin/key.rb', line 252

def to_point
  p = pubkey
  p ||= generate_pubkey(priv_key, compressed: compressed?)
  ECDSA::Format::PointOctetString.decode(p.htb, Bitcoin::Secp256k1::GROUP)
end

#to_wifObject

export private key with wif format



99
100
101
102
103
104
105
# 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) ⇒ Boolean

verify signature using public key

Parameters:

  • sig (String)

    signature data with binary format

  • data (String)

    original message

  • algo (Symbol) (defaults to: :ecdsa)

    signature algorithm. ecdsa(default) or schnorr.

Returns:

  • (Boolean)

    verify result



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# 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_pubkeyString

get xonly public key (32 bytes).

Returns:

  • (String)

    xonly public key with hex format



260
261
262
# File 'lib/bitcoin/key.rb', line 260

def xonly_pubkey
  pubkey[2..65]
end