module Bitcoin::Secp256k1::Native

binding for secp256k1 (github.com/bitcoin-core/secp256k1/) commit: efad3506a8937162e8010f5839fdf3771dfcf516 this is not included by default, to enable set shared object path to ENV for linux, ENV = '/usr/local/lib/libsecp256k1.so' for mac,

Constants

SECP256K1_CONTEXT_SIGN
SECP256K1_CONTEXT_VERIFY

Flags to pass to secp256k1_context_create.

SECP256K1_EC_COMPRESSED

Flag to pass to secp256k1_ec_pubkey_serialize and secp256k1_ec_privkey_export.

SECP256K1_EC_UNCOMPRESSED
SECP256K1_FLAGS_BIT_COMPRESSION
SECP256K1_FLAGS_BIT_CONTEXT_SIGN
SECP256K1_FLAGS_BIT_CONTEXT_VERIFY
SECP256K1_FLAGS_TYPE_COMPRESSION
SECP256K1_FLAGS_TYPE_CONTEXT
SECP256K1_FLAGS_TYPE_MASK

Public Instance Methods

create_keypair(priv_key) click to toggle source

Create key pair data from private key. @param [String] priv_key with hex format @return [String] key pair data with hex format. data = private key(32 bytes) | public key(64 bytes).

# File lib/bitcoin/secp256k1/native.rb, line 204
def create_keypair(priv_key)
  with_context do |context|
    priv_key = priv_key.htb
    secret = FFI::MemoryPointer.new(:uchar, priv_key.bytesize).put_bytes(0, priv_key)
    raise 'priv_key is invalid.' unless secp256k1_ec_seckey_verify(context, secret)
    keypair = FFI::MemoryPointer.new(:uchar, 96)
    raise 'priv_key is invalid.' unless secp256k1_keypair_create(context, keypair, secret) == 1
    keypair.read_string(96).bth
  end
end
generate_key(compressed: true) click to toggle source

generate bitcoin key object

# File lib/bitcoin/secp256k1/native.rb, line 95
def generate_key(compressed: true)
  privkey, pubkey = generate_key_pair(compressed: compressed)
  Bitcoin::Key.new(priv_key: privkey, pubkey: pubkey, compressed: compressed)
end
generate_key_pair(compressed: true) click to toggle source

generate ec private key and public key

# File lib/bitcoin/secp256k1/native.rb, line 80
def generate_key_pair(compressed: true)
  with_context do |context|
    ret, tries, max = 0, 0, 20
    while ret != 1
      raise 'secp256k1_ec_seckey_verify in generate_key_pair failed.' if tries >= max
      tries += 1
      priv_key = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, SecureRandom.random_bytes(32))
      ret = secp256k1_ec_seckey_verify(context, priv_key)
    end
    private_key =  priv_key.read_string(32).bth
    [private_key , generate_pubkey_in_context(context,  private_key, compressed: compressed) ]
  end
end
generate_pubkey(priv_key, compressed: true) click to toggle source
# File lib/bitcoin/secp256k1/native.rb, line 100
def generate_pubkey(priv_key, compressed: true)
  with_context do |context|
    generate_pubkey_in_context(context, priv_key, compressed: compressed)
  end
end
init() click to toggle source
# File lib/bitcoin/secp256k1/native.rb, line 34
def init
  raise 'secp256k1 library dose not found.' unless File.exist?(ENV['SECP256K1_LIB_PATH'])
  ffi_lib(ENV['SECP256K1_LIB_PATH'])
  load_functions
end
load_functions() click to toggle source
# File lib/bitcoin/secp256k1/native.rb, line 40
def load_functions
  attach_function(:secp256k1_context_create, [:uint], :pointer)
  attach_function(:secp256k1_context_destroy, [:pointer], :void)
  attach_function(:secp256k1_context_randomize, [:pointer, :pointer], :int)
  attach_function(:secp256k1_ec_pubkey_create, [:pointer, :pointer, :pointer], :int)
  attach_function(:secp256k1_ec_seckey_verify, [:pointer, :pointer], :int)
  attach_function(:secp256k1_ecdsa_sign, [:pointer, :pointer, :pointer, :pointer, :pointer, :pointer], :int)
  attach_function(:secp256k1_ec_pubkey_serialize, [:pointer, :pointer, :pointer, :pointer, :uint], :int)
  attach_function(:secp256k1_ecdsa_signature_serialize_der, [:pointer, :pointer, :pointer, :pointer], :int)
  attach_function(:secp256k1_ec_pubkey_parse, [:pointer, :pointer, :pointer, :size_t], :int)
  attach_function(:secp256k1_ecdsa_signature_parse_der, [:pointer, :pointer, :pointer, :size_t], :int)
  attach_function(:secp256k1_ecdsa_signature_normalize, [:pointer, :pointer, :pointer], :int)
  attach_function(:secp256k1_ecdsa_verify, [:pointer, :pointer, :pointer, :pointer], :int)
  attach_function(:secp256k1_schnorrsig_sign, [:pointer, :pointer, :pointer, :pointer, :pointer, :pointer], :int)
  attach_function(:secp256k1_schnorrsig_verify, [:pointer, :pointer, :pointer, :pointer], :int)
  attach_function(:secp256k1_keypair_create, [:pointer, :pointer, :pointer], :int)
  attach_function(:secp256k1_xonly_pubkey_parse, [:pointer, :pointer, :pointer], :int)
  attach_function(:secp256k1_ecdsa_sign_recoverable, [:pointer, :pointer, :pointer, :pointer, :pointer, :pointer], :int)
  attach_function(:secp256k1_ecdsa_recoverable_signature_serialize_compact, [:pointer, :pointer, :pointer, :pointer], :int)
  attach_function(:secp256k1_ecdsa_recover, [:pointer, :pointer, :pointer, :pointer], :int)
  attach_function(:secp256k1_ecdsa_recoverable_signature_parse_compact, [:pointer, :pointer, :pointer, :int], :int)
end
parse_ec_pubkey?(pub_key, allow_hybrid = false) click to toggle source

# validate whether this is a valid public key (more expensive than IsValid()) @param [String] pub_key public key with hex format. @param [Boolean] allow_hybrid whether support hybrid public key. @return [Boolean] If valid public key return true, otherwise false.

# File lib/bitcoin/secp256k1/native.rb, line 190
def parse_ec_pubkey?(pub_key, allow_hybrid = false)
  pub_key = pub_key.htb
  return false if !allow_hybrid && ![0x02, 0x03, 0x04].include?(pub_key[0].ord)
  with_context do |context|
    pubkey = FFI::MemoryPointer.new(:uchar, pub_key.bytesize).put_bytes(0, pub_key)
    internal_pubkey = FFI::MemoryPointer.new(:uchar, 64)
    result = secp256k1_ec_pubkey_parse(context, internal_pubkey, pubkey, pub_key.bytesize)
    result == 1
  end
end
recover_compact(data, signature, rec, compressed) click to toggle source

Recover public key from compact signature. @param [String] data message digest using signature. @param [String] signature signature with binary format. @param [Integer] rec recovery id. @param [Boolean] compressed whether compressed public key or not. @return [Bitcoin::Key] Recovered public key.

# File lib/bitcoin/secp256k1/native.rb, line 152
def recover_compact(data, signature, rec, compressed)
  with_context do |context|
    sig = FFI::MemoryPointer.new(:uchar, 65)
    input = FFI::MemoryPointer.new(:uchar, 64).put_bytes(0, signature[1..-1])
    result = secp256k1_ecdsa_recoverable_signature_parse_compact(context, sig, input, rec)
    raise 'secp256k1_ecdsa_recoverable_signature_parse_compact failed.' unless result == 1

    pubkey = FFI::MemoryPointer.new(:uchar, 64)
    msg = FFI::MemoryPointer.new(:uchar, data.bytesize).put_bytes(0, data)
    result = secp256k1_ecdsa_recover(context, pubkey, sig, msg)
    raise 'secp256k1_ecdsa_recover failed.' unless result == 1

    pubkey = serialize_pubkey_internal(context, pubkey.read_string(64), compressed)
    Bitcoin::Key.new(pubkey: pubkey, compressed: compressed)
  end
end
sign_compact(data, privkey) click to toggle source

Sign data with compact format. @param [String] data a data to be signed with binary format @param [String] privkey a private key using sign with hex format @return [Array[signature, recovery id]]

# File lib/bitcoin/secp256k1/native.rb, line 127
def sign_compact(data, privkey)
  with_context do |context|
    sig = FFI::MemoryPointer.new(:uchar, 65)
    hash =FFI::MemoryPointer.new(:uchar, data.bytesize).put_bytes(0, data)
    priv_key = privkey.htb
    sec_key = FFI::MemoryPointer.new(:uchar, priv_key.bytesize).put_bytes(0, priv_key)
    result = secp256k1_ecdsa_sign_recoverable(context, sig, hash, sec_key, nil, nil)
    raise 'secp256k1_ecdsa_sign_recoverable failed.' if result == 0

    output = FFI::MemoryPointer.new(:uchar, 64)
    rec = FFI::MemoryPointer.new(:uint64)
    result = secp256k1_ecdsa_recoverable_signature_serialize_compact(context, output, rec, sig)
    raise 'secp256k1_ecdsa_recoverable_signature_serialize_compact failed.' unless result == 1

    raw_sig = output.read_string(64)
    [ECDSA::Signature.new(raw_sig[0...32].bti, raw_sig[32..-1].bti), rec.read(:int)]
  end
end
sign_data(data, privkey, extra_entropy = nil, algo: :ecdsa) click to toggle source

sign data. @param [String] data a data to be signed with binary format @param [String] privkey a private key with hex format using sign @param [String] extra_entropy a 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/secp256k1/native.rb, line 112
def sign_data(data, privkey, extra_entropy = nil, algo: :ecdsa)
  case algo
  when :ecdsa
    sign_ecdsa(data, privkey, extra_entropy)
  when :schnorr
    sign_schnorr(data, privkey, extra_entropy)
  else
    nil
  end
end
valid_xonly_pubkey?(pub_key) click to toggle source

Check whether valid x-only public key or not. @param [String] pub_key x-only public key with hex format(32 bytes). @return [Boolean] result.

# File lib/bitcoin/secp256k1/native.rb, line 218
def valid_xonly_pubkey?(pub_key)
  begin
    full_pubkey_from_xonly_pubkey(pub_key)
  rescue Exception
    return false
  end
  true
end
verify_sig(data, sig, pubkey, algo: :ecdsa) click to toggle source

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

# File lib/bitcoin/secp256k1/native.rb, line 175
def verify_sig(data, sig, pubkey, algo: :ecdsa)
  case algo
  when :ecdsa
    verify_ecdsa(data, sig, pubkey)
  when :schnorr
    verify_schnorr(data, sig, pubkey)
  else
    false
  end
end
with_context(flags: (SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN)) { |context| ... } click to toggle source
# File lib/bitcoin/secp256k1/native.rb, line 63
def with_context(flags: (SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN))
  init
  begin
    context = secp256k1_context_create(flags)
    ret, tries, max = 0, 0, 20
    while ret != 1
      raise 'secp256k1_context_randomize failed.' if tries >= max
      tries += 1
      ret = secp256k1_context_randomize(context, FFI::MemoryPointer.from_string(SecureRandom.random_bytes(32)))
    end
    yield(context) if block_given?
  ensure
    secp256k1_context_destroy(context)
  end
end

Private Instance Methods

full_pubkey_from_xonly_pubkey(pub_key) click to toggle source

Calculate full public key(64 bytes) from public key(32 bytes). @param [String] pub_key x-only public key with hex format(32 bytes). @return [String] x-only public key with hex format(64 bytes).

# File lib/bitcoin/secp256k1/native.rb, line 232
def full_pubkey_from_xonly_pubkey(pub_key)
  with_context do |context|
    pubkey = pub_key.htb
    raise ArgumentError, 'Pubkey size must be 32 bytes.' unless pubkey.bytesize == 32
    xonly_pubkey = FFI::MemoryPointer.new(:uchar, pubkey.bytesize).put_bytes(0, pubkey)
    full_pubkey = FFI::MemoryPointer.new(:uchar, 64)
    raise ArgumentError, 'An invalid public key was specified.' unless secp256k1_xonly_pubkey_parse(context, full_pubkey, xonly_pubkey) == 1
    full_pubkey.read_string(64).bth
  end
end
generate_pubkey_in_context(context, privkey, compressed: true) click to toggle source
# File lib/bitcoin/secp256k1/native.rb, line 243
def generate_pubkey_in_context(context, privkey, compressed: true)
  internal_pubkey = FFI::MemoryPointer.new(:uchar, 64)
  result = secp256k1_ec_pubkey_create(context, internal_pubkey, privkey.htb)
  raise 'error creating pubkey' unless result
  serialize_pubkey_internal(context, internal_pubkey, compressed)
end
serialize_pubkey_internal(context, pubkey_input, compressed) click to toggle source

Serialize public key.

# File lib/bitcoin/secp256k1/native.rb, line 325
def serialize_pubkey_internal(context, pubkey_input, compressed)
  pubkey = FFI::MemoryPointer.new(:uchar, 65)
  pubkey_len = FFI::MemoryPointer.new(:uint64)
  result = if compressed
             pubkey_len.put_uint64(0, 33)
             secp256k1_ec_pubkey_serialize(context, pubkey, pubkey_len, pubkey_input, SECP256K1_EC_COMPRESSED)
           else
             pubkey_len.put_uint64(0, 65)
             secp256k1_ec_pubkey_serialize(context, pubkey, pubkey_len, pubkey_input, SECP256K1_EC_UNCOMPRESSED)
           end
  raise 'error serialize pubkey' unless result || pubkey_len.read_uint64 > 0
  pubkey.read_string(pubkey_len.read_uint64).bth
end
sign_ecdsa(data, privkey, extra_entropy) click to toggle source
# File lib/bitcoin/secp256k1/native.rb, line 250
def sign_ecdsa(data, privkey, extra_entropy)
  with_context do |context|
    secret = FFI::MemoryPointer.new(:uchar, privkey.htb.bytesize).put_bytes(0, privkey.htb)
    raise 'priv_key is invalid' unless secp256k1_ec_seckey_verify(context, secret)

    internal_signature = FFI::MemoryPointer.new(:uchar, 64)
    msg32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, data)
    entropy = extra_entropy ? FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, extra_entropy) : nil

    ret, tries, max = 0, 0, 20

    while ret != 1
      raise 'secp256k1_ecdsa_sign failed.' if tries >= max
      tries += 1
      ret = secp256k1_ecdsa_sign(context, internal_signature, msg32, secret, nil, entropy)
    end

    signature = FFI::MemoryPointer.new(:uchar, 72)
    signature_len = FFI::MemoryPointer.new(:uint64).put_uint64(0, 72)
    result = secp256k1_ecdsa_signature_serialize_der(context, signature, signature_len, internal_signature)
    raise 'secp256k1_ecdsa_signature_serialize_der failed' unless result

    signature.read_string(signature_len.read_uint64)
  end
end
sign_schnorr(data, privkey, aux_rand = nil) click to toggle source
# File lib/bitcoin/secp256k1/native.rb, line 276
def sign_schnorr(data, privkey, aux_rand = nil)
  with_context do |context|
    keypair = create_keypair(privkey).htb
    keypair = FFI::MemoryPointer.new(:uchar, 96).put_bytes(0, keypair)
    signature = FFI::MemoryPointer.new(:uchar, 64)
    msg32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, data)
    aux_rand = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, aux_rand) if aux_rand
    raise 'Failed to generate schnorr signature.' unless secp256k1_schnorrsig_sign(context, signature, msg32, keypair, nil, aux_rand) == 1
    signature.read_string(64)
  end
end
verify_ecdsa(data, sig, pubkey) click to toggle source
# File lib/bitcoin/secp256k1/native.rb, line 288
def verify_ecdsa(data, sig, pubkey)
  with_context do |context|
    return false if data.bytesize == 0
    pubkey = pubkey.htb
    pubkey = FFI::MemoryPointer.new(:uchar, pubkey.bytesize).put_bytes(0, pubkey)
    internal_pubkey = FFI::MemoryPointer.new(:uchar, 64)
    result = secp256k1_ec_pubkey_parse(context, internal_pubkey, pubkey, pubkey.size)
    return false unless result

    signature = FFI::MemoryPointer.new(:uchar, sig.bytesize).put_bytes(0, sig)
    internal_signature = FFI::MemoryPointer.new(:uchar, 64)
    result = secp256k1_ecdsa_signature_parse_der(context, internal_signature, signature, signature.size)
    return false unless result

    # libsecp256k1's ECDSA verification requires lower-S signatures, which have not historically been enforced in Bitcoin, so normalize them first.
    secp256k1_ecdsa_signature_normalize(context, internal_signature, internal_signature)

    msg32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, data)
    result = secp256k1_ecdsa_verify(context, internal_signature, msg32, internal_pubkey)

    result == 1
  end
end
verify_schnorr(data, sig, pubkey) click to toggle source
# File lib/bitcoin/secp256k1/native.rb, line 312
def verify_schnorr(data, sig, pubkey)
  with_context do |context|
    return false if data.bytesize == 0
    pubkey = full_pubkey_from_xonly_pubkey(pubkey).htb
    xonly_pubkey = FFI::MemoryPointer.new(:uchar, pubkey.bytesize).put_bytes(0, pubkey)
    signature = FFI::MemoryPointer.new(:uchar, sig.bytesize).put_bytes(0, sig)
    msg32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, data)
    result = secp256k1_schnorrsig_verify(context, signature, msg32, xonly_pubkey)
    result == 1
  end
end