Module: Secp256k1

Includes:
C, ECDH, EllSwift, Key, MuSig, Recover, SchnorrSig
Included in:
MuSig::KeyAggContext, MuSig::Session
Defined in:
lib/secp256k1.rb,
lib/secp256k1/c.rb,
lib/secp256k1/key.rb,
lib/secp256k1/ecdh.rb,
lib/secp256k1/musig.rb,
lib/secp256k1/version.rb,
lib/secp256k1/ellswift.rb,
lib/secp256k1/recovery.rb,
lib/secp256k1/schnorrsig.rb,
lib/secp256k1/musig/key_agg.rb,
lib/secp256k1/musig/session.rb

Overview

Binding for secp256k1 (github.com/bitcoin-core/secp256k1/)

Examples:

include Secp256k1

# Generate key pair
sk, pk = generate_key_pair

# Generate public key
pk = generate_pubkey(sk)

# sign and verify (ECDSA)
msg = Digest::SHA256.digest('message')
signature = sign_ecdsa(msg, sk)
verify_ecdsa(msg, signature, pk)

Defined Under Namespace

Modules: C, ECDH, EllSwift, Key, MuSig, Recover, SchnorrSig Classes: Error

Constant Summary collapse

FLAGS_TYPE_MASK =
((1 << 8) - 1)
FLAGS_TYPE_CONTEXT =
(1 << 0)
FLAGS_TYPE_COMPRESSION =
(1 << 1)
FLAGS_BIT_CONTEXT_VERIFY =
(1 << 8)
FLAGS_BIT_CONTEXT_SIGN =
(1 << 9)
FLAGS_BIT_COMPRESSION =
(1 << 8)
CONTEXT_VERIFY =

Flags to pass to context_create.

(FLAGS_TYPE_CONTEXT | FLAGS_BIT_CONTEXT_VERIFY)
CONTEXT_SIGN =
(FLAGS_TYPE_CONTEXT | FLAGS_BIT_CONTEXT_SIGN)
EC_COMPRESSED =

Flag to pass to ec_pubkey_serialize and ec_privkey_export.

(FLAGS_TYPE_COMPRESSION | FLAGS_BIT_COMPRESSION)
EC_UNCOMPRESSED =
(FLAGS_TYPE_COMPRESSION)
X_ONLY_PUBKEY_SIZE =
32
ELL_SWIFT_KEY_SIZE =
64
VERSION =
"0.3.0"

Constants included from SchnorrSig

SchnorrSig::SCHNORRSIG_EXTRAPARAMS_MAGIC

Instance Method Summary collapse

Methods included from MuSig

#aggregate_musig_nonce, #aggregate_pubkey, #generate_musig_nonce, #generate_musig_nonce_counter, #generate_musig_session_id

Methods included from EllSwift

#ellswift_create, #ellswift_decode, #ellswift_ecdh_xonly, #ellswift_encode

Methods included from SchnorrSig

#sign_schnorr, #sign_schnorr_custom, #verify_schnorr, #verify_schnorr_custom

Methods included from Recover

#recover, #recoverable_signature_to_ecdsa, #sign_recoverable

Methods included from ECDH

#ecdh

Methods included from Key

#combine_pubkeys, #compare_pubkey, #compare_xonly_pubkey, #keypair_to_pubkey, #keypair_to_seckey, #keypair_to_xonly_pubkey, #keypair_xonly_tweak_add, #negate_pubkey, #negate_seckey, #sort_pubkeys, #tweak_add_pubkey, #tweak_add_seckey, #tweak_mul_pubkey, #tweak_mul_seckey, #xonly_pubkey_from_pubkey, #xonly_tweak_add_check?, #xonly_tweak_add_pubkey

Methods included from C

ecdh_hash_function_default, ecdh_hash_function_sha256, ellswift_xdh_hash_function_bip324

Instance Method Details

#create_keypair(private_key) ⇒ String

Create key pair data from private key.

Parameters:

  • private_key (String)

    with hex format

Returns:

  • (String)

    key pair data with hex format. data = private key(32 bytes) | public key(64 bytes).

Raises:

  • (Secp256k1::Error)

    If private_key is invalid.

  • (ArgumentError)

    If invalid arguments specified.



132
133
134
135
136
137
138
139
140
141
142
# File 'lib/secp256k1.rb', line 132

def create_keypair(private_key)
  validate_string!("private_key", private_key, 32)
  private_key = hex2bin(private_key)
  with_context do |context|
    secret = FFI::MemoryPointer.new(:uchar, private_key.bytesize).put_bytes(0, private_key)
    raise Error, 'private_key is invalid.' unless secp256k1_ec_seckey_verify(context, secret)
    keypair = FFI::MemoryPointer.new(:uchar, 96)
    raise Error 'private_key is invalid.' unless secp256k1_keypair_create(context, keypair, secret) == 1
    keypair.read_string(96).unpack1('H*')
  end
end

#ecdsa_signature_from_compact(signature) ⇒ String

Convert a compact(64 bytes) ECDSA signature into DER-encoded format.

Parameters:

  • signature (String)

    compact signature(64 bytes) with binary format.

Returns:

  • (String)

    DER-encoded signature with binary format.

Raises:

  • (Secp256k1::Error)

    If the signature could not be parsed.

  • (ArgumentError)

    If invalid arguments specified.



253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/secp256k1.rb', line 253

def ecdsa_signature_from_compact(signature)
  validate_string!("signature", signature, 64)
  signature = hex2bin(signature)
  with_context do |context|
    sig_ptr = FFI::MemoryPointer.new(:uchar, 64).put_bytes(0, signature)
    internal_signature = FFI::MemoryPointer.new(:uchar, 64)
    raise Error, 'secp256k1_ecdsa_signature_parse_compact failed.' unless secp256k1_ecdsa_signature_parse_compact(context, internal_signature, sig_ptr) == 1
    output = FFI::MemoryPointer.new(:uchar, 72)
    output_len = FFI::MemoryPointer.new(:uint64).put_uint64(0, 72)
    raise Error, 'secp256k1_ecdsa_signature_serialize_der failed.' unless secp256k1_ecdsa_signature_serialize_der(context, output, output_len, internal_signature) == 1
    output.read_string(output_len.read_uint64)
  end
end

#ecdsa_signature_to_compact(signature) ⇒ String

Convert a DER-encoded ECDSA signature into compact(64 bytes) format.

Parameters:

  • signature (String)

    DER-encoded signature with binary format.

Returns:

  • (String)

    compact signature(64 bytes) with binary format.

Raises:

  • (Secp256k1::Error)

    If the signature could not be parsed.

  • (ArgumentError)

    If invalid arguments specified.



235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/secp256k1.rb', line 235

def ecdsa_signature_to_compact(signature)
  raise ArgumentError, "signature must be String." unless signature.is_a?(String)
  signature = hex2bin(signature)
  with_context do |context|
    sig_ptr = FFI::MemoryPointer.new(:uchar, signature.bytesize).put_bytes(0, signature)
    internal_signature = FFI::MemoryPointer.new(:uchar, 64)
    raise Error, 'secp256k1_ecdsa_signature_parse_der failed.' unless secp256k1_ecdsa_signature_parse_der(context, internal_signature, sig_ptr, signature.bytesize) == 1
    output = FFI::MemoryPointer.new(:uchar, 64)
    secp256k1_ecdsa_signature_serialize_compact(context, output, internal_signature)
    output.read_string(64)
  end
end

#generate_key_pair(compressed: true) ⇒ Array

Randomly generate ec private key and public key.

Parameters:

  • compressed (Boolean) (defaults to: true)

    Whether to generate a compressed public key.

Returns:

  • (Array)

    Array of public key and public key (Both are hex values).

Raises:

  • (Secp256k1::Error)

    If secp256k1_ec_seckey_verify in generate_key_pair failed.



82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/secp256k1.rb', line 82

def generate_key_pair(compressed: true)
  with_context do |context|
    ret, tries, max = 0, 0, 20
    while ret != 1
      raise Error, 'secp256k1_ec_seckey_verify in generate_key_pair failed.' if tries >= max
      tries += 1
      private_key = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, SecureRandom.random_bytes(32))
      ret = secp256k1_ec_seckey_verify(context, private_key)
    end
    private_key =  private_key.read_string(32).unpack1('H*')
    [private_key , generate_pubkey_in_context(context,  private_key, compressed: compressed) ]
  end
end

#generate_pubkey(private_key, compressed: true) ⇒ String

Generate public key from private_key.

Parameters:

  • private_key (String)

    Private key with hex format.

  • compressed (Boolean) (defaults to: true)

    Whether to generate a compressed public key.

Returns:

  • (String)

    Public key with hex format.

Raises:

  • (ArgumentError)

    If invalid arguments specified.



101
102
103
104
105
106
107
# File 'lib/secp256k1.rb', line 101

def generate_pubkey(private_key, compressed: true)
  validate_string!("private_key", private_key, 32)
  private_key = hex2bin(private_key)
  with_context do |context|
    generate_pubkey_in_context(context, private_key, compressed: compressed)
  end
end

#parse_ec_pubkey?(pubkey, allow_hybrid = false) ⇒ Boolean

Validate whether this is a valid public key.

Parameters:

  • pubkey (String)

    public key with hex format.

  • allow_hybrid (Boolean) (defaults to: false)

    whether support hybrid public key.

Returns:

  • (Boolean)

    If valid public key return true, otherwise false.

Raises:

  • (ArgumentError)

    If invalid arguments specified.



114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/secp256k1.rb', line 114

def parse_ec_pubkey?(pubkey, allow_hybrid = false)
  raise ArgumentError, "pubkey must be String." unless pubkey.is_a?(String)
  pubkey = hex2bin(pubkey)
  return false if !allow_hybrid && ![0x02, 0x03, 0x04].include?(pubkey[0].ord)
  with_context do |context|
    pubkey_size = pubkey.bytesize
    pubkey = FFI::MemoryPointer.new(:uchar, pubkey_size).put_bytes(0, pubkey)
    internal_pubkey = FFI::MemoryPointer.new(:uchar, 64)
    result = secp256k1_ec_pubkey_parse(context, internal_pubkey, pubkey, pubkey_size)
    result == 1
  end
end

#sign_ecdsa(data, private_key, extra_entropy = nil) ⇒ String

Sign to data using ecdsa.

Parameters:

  • data (String)

    The 32-byte message hash being signed with binary format.

  • private_key (String)

    a private key with hex format using sign.

  • extra_entropy (String) (defaults to: nil)

    (Optional)An extra entropy with binary format for rfc6979.

Returns:

  • (String)

    signature data with binary format. If unsupported algorithm specified, return nil.

Raises:

  • (ArgumentError)

    If invalid arguments specified.



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
# File 'lib/secp256k1.rb', line 163

def sign_ecdsa(data, private_key, extra_entropy = nil)
  validate_string!("private_key", private_key, 32)
  validate_string!("data", data, 32)
  validate_string!("extra_entropy", extra_entropy, 32) if extra_entropy
  private_key = hex2bin(private_key)
  data = hex2bin(data)

  with_context do |context|
    secret = FFI::MemoryPointer.new(:uchar, private_key.bytesize).put_bytes(0, private_key)
    raise Error, 'private_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 Error, '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 Error, 'secp256k1_ecdsa_signature_serialize_der failed' unless result

    signature.read_string(signature_len.read_uint64)
  end
end

#tagged_sha256(tag, msg) ⇒ String

Compute a tagged hash as defined in BIP-340: SHA256(SHA256(tag) || SHA256(tag) || msg).

Parameters:

  • tag (String)

    the tag with binary format.

  • msg (String)

    the message with binary format.

Returns:

  • (String)

    32-byte tagged hash with hex format.

Raises:

  • (Secp256k1::Error)

    If the hash could not be computed.

  • (ArgumentError)

    If invalid arguments specified.



273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/secp256k1.rb', line 273

def tagged_sha256(tag, msg)
  raise ArgumentError, "tag must be String." unless tag.is_a?(String)
  raise ArgumentError, "msg must be String." unless msg.is_a?(String)
  tag = hex2bin(tag)
  msg = hex2bin(msg)
  with_context do |context|
    hash32 = FFI::MemoryPointer.new(:uchar, 32)
    # The library requires non-NULL pointers even for zero-length input, so allocate at least 1 byte.
    tag_ptr = FFI::MemoryPointer.new(:uchar, [tag.bytesize, 1].max).put_bytes(0, tag)
    msg_ptr = FFI::MemoryPointer.new(:uchar, [msg.bytesize, 1].max).put_bytes(0, msg)
    raise Error, 'secp256k1_tagged_sha256 failed.' unless secp256k1_tagged_sha256(context, hash32, tag_ptr, tag.bytesize, msg_ptr, msg.bytesize) == 1
    hash32.read_string(32).unpack1('H*')
  end
end

#valid_xonly_pubkey?(pubkey) ⇒ Boolean

Check whether valid x-only public key or not.

Parameters:

  • pubkey (String)

    x-only public key with hex format(32 bytes).

Returns:

  • (Boolean)

    result.



147
148
149
150
151
152
153
154
155
# File 'lib/secp256k1.rb', line 147

def valid_xonly_pubkey?(pubkey)
  return false unless pubkey.is_a?(String)
  begin
    full_pubkey_from_xonly_pubkey(hex2bin(pubkey))
  rescue Exception
    return false
  end
  true
end

#verify_ecdsa(data, signature, pubkey) ⇒ Boolean

Verify ecdsa signature.

Parameters:

  • data (String)

    The 32-byte message hash assumed to be signed.

  • signature (String)

    signature data with binary format

  • pubkey (String)

    a public key with hex format using verify.

Returns:

  • (Boolean)

    verification result.

Raises:

  • (ArgumentError)

    If invalid arguments specified.



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/secp256k1.rb', line 201

def verify_ecdsa(data, signature, pubkey)
  raise ArgumentError, "sig must be String." unless signature.is_a?(String)
  raise ArgumentError, "pubkey must be String." unless pubkey.is_a?(String)
  validate_string!("data", data, 32)
  data = hex2bin(data)
  pubkey = hex2bin(pubkey)
  signature = hex2bin(signature)
  with_context do |context|
    return false if data.bytesize == 0
    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, signature.bytesize).put_bytes(0, signature)
    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

#with_context(flags: (CONTEXT_VERIFY | CONTEXT_SIGN)) ⇒ Object

Creates a secp256k1 context object, performs the operations passed in the block, and then ensures that the secp256k1 context object is destroyed at the end.

Parameters:

  • flags (Integer) (defaults to: (CONTEXT_VERIFY | CONTEXT_SIGN))

    The flag to use when performing the operation.

Raises:



63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/secp256k1.rb', line 63

def with_context(flags: (CONTEXT_VERIFY | CONTEXT_SIGN))
  begin
    context = secp256k1_context_create(flags)
    ret, tries, max = 0, 0, 20
    while ret != 1
      raise Error, '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