secp256k1rb 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aff0d24f08ccdd2fb1d622ca2cb8655a9e36d3960890710861878e32e55850b3
4
- data.tar.gz: 6915105a7e248f5a1ca810e5f0b8af3dea75731a7b65bd111df74a8691f2d907
3
+ metadata.gz: 11620dd56323837c51b4adaf78b80e683c039b6086af0d501df6890f31c32e07
4
+ data.tar.gz: 27b5544236685afd32b11dd636d1f78ca626cfb63a120c6da88504238aa0c6b2
5
5
  SHA512:
6
- metadata.gz: 307ef35d2afa388b1640a2df6438ca19b14cc5d1bf1920f509e2f1768d610d0534f926ad0122122b5ad30da9ab6a9da3f49b17b6d1215fc0bd1e1fc55c42ab71
7
- data.tar.gz: a533565583b3af85dbfbb9f259e3c644a4de3ec5a8c52e08b37eb46ff0bdba922e5a14cd3f2588792caa1ae4934881b9563b045c6907dda50571349a97dcef9c
6
+ metadata.gz: 14177d6aa88872ae15db021b45d492ec59b083ad751d224d852c9f5440fd2a9b51855608a0c0456175aadd09b06d68591122b799fc2a7d08724c4f726c84c980
7
+ data.tar.gz: '0805a6d29bad9054f581115919365a3e8623adc8d93f64e92d18435f33012056a227ccbb4bd08c5421ec4a3bbc1e85f94d8b116cd47d4c205f51e0efe5306f00'
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # secp256k1rb
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/secp256k1`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ This is a Ruby binding for Bitcoin Core's [secp256k1 library](https://github.com/bitcoin-core/secp256k1/).
6
4
 
7
5
  ## Installation
8
6
 
@@ -22,22 +20,33 @@ Or install it yourself as:
22
20
 
23
21
  ## Usage
24
22
 
25
- TODO: Write usage instructions here
23
+ To use this library, you need to specify the path of the secp256k1 shared library in environment variable
24
+ `SECP256K1_LIB_PATH`, e.g: `$ export SECP256K1_LIB_PATH=/var/local/lib/libsecp256k1.so`.
25
+
26
+ Note: This library also implements the recovery module, so you must have built the secp256k1 library with the
27
+ `--enable-module-recovery` option.
26
28
 
27
- ## Development
29
+ By including the Secp256k1 module, you can use the features provided by the `libsepc256k1` library. For example:
28
30
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
31
+ ```ruby
32
+ require 'secp256k1'
30
33
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
34
+ include Secp256k1
32
35
 
33
- ## Contributing
36
+ generate_key_pair
37
+ => ["e00c2ae99e59b5262be3d507d026081f0e6cf9972ffdd4f2d45a390f7a41b053", "027e0f70b540d627422cf7bb77d86ae1bb6829c80104dd48dc2539e6277ea25624"]
38
+ ```
34
39
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/secp256k1. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/secp256k1/blob/master/CODE_OF_CONDUCT.md).
40
+ See [here](https://www.rubydoc.info/gems/secp256k1rb/Secp256k1) for available methods.
41
+ In addition, the following modules are also included, so you can use them as they are.
36
42
 
37
- ## License
43
+ * [Recover](https://www.rubydoc.info/gems/secp256k1rb/Secp256k1/Recover)
44
+ * [SchnorrSig](https://www.rubydoc.info/gems/secp256k1rb/Secp256k1/SchnorrSig)
45
+ * [EllSwift](https://www.rubydoc.info/gems/secp256k1rb/Secp256k1/EllSwift)
38
46
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
47
+ ### Compatibility
40
48
 
41
- ## Code of Conduct
49
+ secp256k1 version | secp256k1rb version
50
+ :---:|:---:
51
+ v0.4.0 | v0.1.x
42
52
 
43
- Everyone interacting in the Secp256k1 project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/secp256k1/blob/master/CODE_OF_CONDUCT.md).
data/lib/secp256k1/c.rb CHANGED
@@ -28,10 +28,32 @@ module Secp256k1
28
28
  attach_function(:secp256k1_ecdsa_recoverable_signature_serialize_compact, [:pointer, :pointer, :pointer, :pointer], :int)
29
29
  attach_function(:secp256k1_ecdsa_recover, [:pointer, :pointer, :pointer, :pointer], :int)
30
30
  attach_function(:secp256k1_ecdsa_recoverable_signature_parse_compact, [:pointer, :pointer, :pointer, :int], :int)
31
+ # for EllSwift module
31
32
  attach_function(:secp256k1_ellswift_decode, [:pointer, :pointer, :pointer], :int)
32
33
  attach_function(:secp256k1_ellswift_create, [:pointer, :pointer, :pointer, :pointer], :int)
33
34
  attach_variable(:secp256k1_ellswift_xdh_hash_function_bip324, :pointer)
34
35
  attach_function(:secp256k1_ellswift_xdh, [:pointer, :pointer, :pointer, :pointer, :pointer, :int, :pointer, :pointer], :int)
36
+ # for ExtraKeys module
37
+ attach_function(:secp256k1_xonly_pubkey_parse, [:pointer, :pointer, :pointer], :int)
38
+ attach_function(:secp256k1_xonly_pubkey_serialize, [:pointer, :pointer, :pointer], :int)
39
+ attach_function(:secp256k1_keypair_create, [:pointer, :pointer, :pointer], :int)
40
+ # for MuSig module
41
+ attach_function(:secp256k1_musig_pubkey_agg, [:pointer, :pointer, :pointer, :pointer, :size_t], :int)
42
+ attach_function(:secp256k1_musig_pubkey_get, [:pointer, :pointer, :pointer], :int)
43
+ attach_function(:secp256k1_musig_pubkey_ec_tweak_add, [:pointer, :pointer, :pointer, :pointer], :int)
44
+ attach_function(:secp256k1_musig_pubkey_xonly_tweak_add, [:pointer, :pointer, :pointer, :pointer], :int)
45
+ attach_function(:secp256k1_musig_nonce_gen, [:pointer, :pointer, :pointer, :pointer, :pointer, :pointer, :pointer, :pointer, :pointer], :int)
46
+ attach_function(:secp256k1_musig_pubnonce_parse, [:pointer, :pointer, :pointer], :int)
47
+ attach_function(:secp256k1_musig_pubnonce_serialize, [:pointer, :pointer, :pointer], :int)
48
+ attach_function(:secp256k1_musig_aggnonce_serialize, [:pointer, :pointer, :pointer], :int)
49
+ attach_function(:secp256k1_musig_aggnonce_parse, [:pointer, :pointer, :pointer], :int)
50
+ attach_function(:secp256k1_musig_nonce_agg, [:pointer, :pointer, :pointer, :size_t], :int)
51
+ attach_function(:secp256k1_musig_nonce_process, [:pointer, :pointer, :pointer, :pointer, :pointer], :int)
52
+ attach_function(:secp256k1_musig_partial_sign, [:pointer, :pointer, :pointer, :pointer, :pointer, :pointer], :int)
53
+ attach_function(:secp256k1_musig_partial_sig_serialize, [:pointer, :pointer, :pointer], :int)
54
+ attach_function(:secp256k1_musig_partial_sig_parse, [:pointer, :pointer, :pointer], :int)
55
+ attach_function(:secp256k1_musig_partial_sig_verify, [:pointer, :pointer, :pointer, :pointer, :pointer, :pointer], :int)
56
+ attach_function(:secp256k1_musig_partial_sig_agg, [:pointer, :pointer, :pointer, :pointer, :size_t], :int)
35
57
 
36
58
  # Pointer to secp256k1_ellswift_xdh_hash_function_bip324 constant.
37
59
  # @return [FFI::Pointer]
@@ -8,9 +8,8 @@ module Secp256k1
8
8
  # @raise [Secp256k1::Error] If decode failed.
9
9
  # @raise [ArgumentError] If invalid arguments specified.
10
10
  def ellswift_decode(ell_key, compressed: true)
11
- raise ArgumentError, "ell_key must be String." unless ell_key.is_a?(String)
11
+ validate_string!("ell_key", ell_key, ELL_SWIFT_KEY_SIZE)
12
12
  ell_key = hex2bin(ell_key)
13
- raise ArgumentError, "ell_key must be 64 bytes." unless ell_key.bytesize == 64
14
13
  with_context do |context|
15
14
  ell64 = FFI::MemoryPointer.new(:uchar, ell_key.bytesize).put_bytes(0, ell_key)
16
15
  internal = FFI::MemoryPointer.new(:uchar, 64)
@@ -26,9 +25,8 @@ module Secp256k1
26
25
  # @raise [Secp256k1::Error] If failed to create elligattor swhift public key.
27
26
  # @raise [ArgumentError] If invalid arguments specified.
28
27
  def ellswift_create(private_key)
29
- raise ArgumentError, "private_key must be String." unless private_key.is_a?(String)
28
+ validate_string!("private_key", private_key, 32)
30
29
  private_key = hex2bin(private_key)
31
- raise ArgumentError, "private_key must be 32 bytes." unless private_key.bytesize == 32
32
30
  with_context(flags: CONTEXT_SIGN) do |context|
33
31
  ell64 = FFI::MemoryPointer.new(:uchar, 64)
34
32
  seckey32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, private_key)
@@ -46,15 +44,12 @@ module Secp256k1
46
44
  # @return [String] x coordinate with hex format.
47
45
  # @raise [Secp256k1::Error] If secret is invalid or hashfp return 0.
48
46
  def ellswift_ecdh_xonly(their_ell_pubkey, our_ell_pubkey, private_key, initiating)
49
- raise ArgumentError, "their_ell_pubkey must be String." unless their_ell_pubkey.is_a?(String)
50
- raise ArgumentError, "our_ell_pubkey must be String." unless our_ell_pubkey.is_a?(String)
51
- raise ArgumentError, "private_key must be String." unless private_key.is_a?(String)
47
+ validate_string!("their_ell_pubkey", their_ell_pubkey, ELL_SWIFT_KEY_SIZE)
48
+ validate_string!("our_ell_pubkey", our_ell_pubkey, ELL_SWIFT_KEY_SIZE)
49
+ validate_string!("private_key", private_key, 32)
52
50
  their_ell_pubkey = hex2bin(their_ell_pubkey)
53
51
  our_ell_pubkey = hex2bin(our_ell_pubkey)
54
52
  private_key = hex2bin(private_key)
55
- raise ArgumentError, "their_ell_pubkey must be #{ELL_SWIFT_KEY_SIZE} bytes." unless their_ell_pubkey.bytesize == ELL_SWIFT_KEY_SIZE
56
- raise ArgumentError, "our_ell_pubkey must be #{ELL_SWIFT_KEY_SIZE} bytes." unless our_ell_pubkey.bytesize == ELL_SWIFT_KEY_SIZE
57
- raise ArgumentError, "private_key must be 32 bytes." unless private_key.bytesize == 32
58
53
 
59
54
  with_context(flags: CONTEXT_SIGN) do |context|
60
55
  output = FFI::MemoryPointer.new(:uchar, 32)
@@ -0,0 +1,74 @@
1
+ module Secp256k1
2
+ module MuSig
3
+
4
+ # Opaque data structure that caches information about public key aggregation.
5
+ class KeyAggCache < FFI::Struct
6
+ layout :data, [:uchar, 197]
7
+ end
8
+
9
+ # Key aggregation context class.
10
+ class KeyAggContext
11
+ include Secp256k1
12
+
13
+ attr_reader :cache
14
+
15
+ # Constructor.
16
+ # @param [Secp256k1::MuSig::KeyAggCache] key_agg_cache Key aggregation cache.
17
+ # @raise [ArgumentError] If invalid arguments specified.
18
+ def initialize(key_agg_cache)
19
+ raise ArgumentError, "key_agg_cache must be Secp256k1::KeyAggCache." unless key_agg_cache.is_a?(Secp256k1::KeyAggCache)
20
+ @cache = key_agg_cache
21
+ end
22
+
23
+ # Get aggregate public key.
24
+ # @return [String] An aggregated public key.
25
+ def aggregate_public_key
26
+ with_context do |context|
27
+ agg_pubkey = FFI::MemoryPointer.new(:uchar, 64)
28
+ if secp256k1_musig_pubkey_get(context, agg_pubkey, cache.pointer) == 0
29
+ raise Error, "secp256k1_musig_pubkey_get arguments invalid."
30
+ end
31
+ serialize_pubkey(context, agg_pubkey)
32
+ end
33
+ end
34
+
35
+ # Apply ordinary "EC" tweaking to a public key.
36
+ # @param [String] tweak Tweak value to tweak the aggregated key.
37
+ # @param [Boolean] xonly Apply x-only tweaking or not.
38
+ # @return [String] Tweaked x-only public key with hex format.
39
+ # @raise [ArgumentError] If invalid arguments specified.
40
+ # @raise [Secp256k1::Error]
41
+ def tweak_add(tweak, xonly: false)
42
+ validate_string!("tweak", tweak, 32)
43
+ with_context do |context|
44
+ tweak_ptr = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(tweak))
45
+ pubkey_ptr = FFI::MemoryPointer.new(:uchar, 64)
46
+ if xonly
47
+ if secp256k1_musig_pubkey_xonly_tweak_add(context, pubkey_ptr, cache.pointer, tweak_ptr) == 0
48
+ raise Error, "secp256k1_musig_pubkey_tweak_add arguments invalid."
49
+ end
50
+ else
51
+ if secp256k1_musig_pubkey_ec_tweak_add(context, pubkey_ptr, cache.pointer, tweak_ptr) == 0
52
+ raise Error, "secp256k1_musig_pubkey_tweak_add arguments invalid."
53
+ end
54
+ end
55
+ serialize_pubkey(context, pubkey_ptr)
56
+ end
57
+ end
58
+
59
+ # Get KeyAggCache pointer.
60
+ # @return [FFI::MemoryPointer]
61
+ def pointer
62
+ cache.pointer
63
+ end
64
+
65
+ private
66
+
67
+ def serialize_pubkey(context, pubkey_ptr)
68
+ xonly = FFI::MemoryPointer.new(:uchar, 32)
69
+ secp256k1_xonly_pubkey_serialize(context, xonly, pubkey_ptr)
70
+ xonly.read_string(32).unpack1('H*')
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,123 @@
1
+ module Secp256k1
2
+ module MuSig
3
+ class Session
4
+ include Secp256k1
5
+ attr_reader :session
6
+ attr_reader :key_agg_ctx
7
+ attr_reader :agg_nonce
8
+ attr_reader :msg
9
+
10
+ # Create signing session.
11
+ # @param [Secp256k1::MuSig::KeyAggContext] key_agg_ctx The key aggregation context.
12
+ # @param [String] agg_nonce An aggregated public nonce.
13
+ # @param [String] msg The message to be signed.
14
+ # @raise [ArgumentError] If invalid arguments specified.
15
+ # @raise [Secp256k1::Error]
16
+ def initialize(key_agg_ctx, agg_nonce, msg)
17
+ raise ArgumentError, 'key_agg_ctx must be KeyAggContext.' unless key_agg_ctx.is_a?(KeyAggContext)
18
+ validate_string!('msg', msg, 32)
19
+ validate_string!('agg_nonce', agg_nonce, 66)
20
+ agg_nonce = hex2bin(agg_nonce)
21
+ msg = hex2bin(msg)
22
+ with_context do |context|
23
+ @session = FFI::MemoryPointer.new(:uchar, 133)
24
+ agg66 = FFI::MemoryPointer.new(:uchar, 66).put_bytes(0, agg_nonce)
25
+ agg_ptr = FFI::MemoryPointer.new(:uchar, 132)
26
+ if secp256k1_musig_aggnonce_parse(context, agg_ptr, agg66) == 0
27
+ raise Error, "secp256k1_musig_aggnonce_parse failed."
28
+ end
29
+ msg_ptr = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, msg)
30
+ if secp256k1_musig_nonce_process(context, @session, agg_ptr, msg_ptr, key_agg_ctx.pointer) == 0
31
+ raise Error, "secp256k1_musig_nonce_process arguments invalid."
32
+ end
33
+ end
34
+ @agg_nonce = agg_nonce.unpack1('H*')
35
+ @msg = msg.unpack1('H*')
36
+ @key_agg_ctx = key_agg_ctx
37
+ end
38
+
39
+ # Produces a partial signature for a given key pair and secret nonce.
40
+ # @param [String] sec_nonce The secret nonce to sign the message.
41
+ # @param [String] private_key The private key to sign the message.
42
+ # @return [String] A partial signature.
43
+ # @raise [ArgumentError] If invalid arguments specified.
44
+ # @raise [Secp256k1::Error]
45
+ def partial_sign(sec_nonce, private_key)
46
+ raise ArgumentError, 'key_agg_ctx must be KeyAggContext.' unless key_agg_ctx.is_a?(KeyAggContext)
47
+ validate_string!('sec_nonce', sec_nonce, 132)
48
+ validate_string!('private_key', private_key, 32)
49
+ with_context do |context|
50
+ partial_sig = FFI::MemoryPointer.new(:uchar, 36)
51
+ sec_nonce_ptr = FFI::MemoryPointer.new(:uchar, 132).put_bytes(0, hex2bin(sec_nonce))
52
+ private_key_ptr = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(private_key))
53
+ key_pair = FFI::MemoryPointer.new(:uchar, 86)
54
+ if secp256k1_keypair_create(context, key_pair, private_key_ptr) == 0
55
+ raise Error, "secp256k1_keypair_create invalid private_key."
56
+ end
57
+ if secp256k1_musig_partial_sign(
58
+ context, partial_sig, sec_nonce_ptr, key_pair, key_agg_ctx.cache.pointer, session) == 0
59
+ raise Error, "secp256k1_musig_partial_sign arguments invalid or sec_nonce has already been used for signing."
60
+ end
61
+ out32 = FFI::MemoryPointer.new(:uchar, 32)
62
+ secp256k1_musig_partial_sig_serialize(context, out32, partial_sig)
63
+ out32.read_string(32).unpack1('H*')
64
+ end
65
+ end
66
+
67
+ # Checks that an individual partial signature verifies.
68
+ # @param [String] partial_sig The partial signature to verify, sent by the signer associated with +pub_nonce+ and +public_key+.
69
+ # @param [String] pub_nonce The public nonce of the signer in the signing session.
70
+ # @param [String] public_key The public key of the signer in the signing session.
71
+ # @return [Boolean] The verification result.
72
+ # @raise [ArgumentError] If invalid arguments specified.
73
+ # @raise [Secp256k1::Error]
74
+ def verify_partial_sig(partial_sig, pub_nonce, public_key)
75
+ validate_string!('partial_sig', partial_sig, 32)
76
+ validate_string!('pub_nonce', pub_nonce, 66)
77
+ validate_string!('public_key', public_key, 33)
78
+ with_context do |context|
79
+ sig_ptr = parse_partial_sig(context, partial_sig)
80
+ public_key = FFI::MemoryPointer.new(:uchar, 33).put_bytes(0, hex2bin(public_key))
81
+ pubkey_ptr = FFI::MemoryPointer.new(:uchar, 64)
82
+ raise Error, "pubkey is invalid." unless secp256k1_ec_pubkey_parse(context, pubkey_ptr, public_key, 33) == 1
83
+ pub_nonce = FFI::MemoryPointer.new(:uchar, 66).put_bytes(0, hex2bin(pub_nonce))
84
+ nonce_ptr = FFI::MemoryPointer.new(:uchar, 132)
85
+ if secp256k1_musig_pubnonce_parse(context, nonce_ptr, pub_nonce) == 0
86
+ raise Error, "secp256k1_musig_pubnonce_parse failed."
87
+ end
88
+ secp256k1_musig_partial_sig_verify(context, sig_ptr, nonce_ptr, pubkey_ptr, key_agg_ctx.pointer, session) == 1
89
+ end
90
+ end
91
+
92
+ # Aggregates partial signatures
93
+ # @param [Array] partial_sigs Array of partial signatures.
94
+ # @return [String] An aggregated signature.
95
+ # @raise [ArgumentError] If invalid arguments specified.
96
+ # @raise [Secp256k1::Error]
97
+ def aggregate_partial_sigs(partial_sigs)
98
+ raise ArgumentError, "partial_sigs must be Array." unless partial_sigs.is_a?(Array)
99
+ raise ArgumentError, "partial_sigs must not be empty." if partial_sigs.empty?
100
+ with_context do |context|
101
+ sigs_ptr = FFI::MemoryPointer.new(:pointer, partial_sigs.length)
102
+ sigs_ptr.write_array_of_pointer(partial_sigs.map{|partial_sig| parse_partial_sig(context, partial_sig)})
103
+ sig64 = FFI::MemoryPointer.new(:uchar, 64)
104
+ if secp256k1_musig_partial_sig_agg(context, sig64, session, sigs_ptr, partial_sigs.length) == 0
105
+ raise Error, "secp256k1_musig_partial_sig_agg arguments invalid."
106
+ end
107
+ sig64.read_string(64).unpack1('H*')
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ def parse_partial_sig(context, partial_sig)
114
+ partial_sig = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(partial_sig))
115
+ sig_ptr = FFI::MemoryPointer.new(:uchar, 36)
116
+ if secp256k1_musig_partial_sig_parse(context, sig_ptr, partial_sig) == 0
117
+ raise Error, "secp256k1_musig_partial_sig_parse failed."
118
+ end
119
+ sig_ptr
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,165 @@
1
+ require_relative 'musig/key_agg'
2
+ require_relative 'musig/session'
3
+
4
+ module Secp256k1
5
+
6
+ # MuSig module
7
+ # @example
8
+ # include Secp256k1
9
+ #
10
+ # # Two signer.
11
+ # sk1, pk1 = generate_key_pair
12
+ # sk2, pk2 = generate_key_pair
13
+ #
14
+ # # Aggregate public key
15
+ # key_agg_ctx = aggregate_pubkey([pk1, pk2])
16
+ # agg_pubkey = key_agg_ctx.aggregate_public_key
17
+ #
18
+ # # If you need tweak
19
+ # key_agg_ctx.tweak_add('7468697320636f756c64206265206120424950333220747765616b2e2e2e2e00')
20
+ # # If you need tweak with xonly
21
+ # key_agg_ctx.tweak_add('7468697320636f756c64206265206120546170726f6f7420747765616b2e2e00', xonly: true)
22
+ #
23
+ # agg_pubkey = key_agg_ctx.aggregate_public_key
24
+ #
25
+ # msg = Digest::SHA256.digest('message')
26
+ #
27
+ # # Nonce generation
28
+ # session_id1 = generate_musig_session_id
29
+ # secnonce1, pubnonce1 = target.generate_musig_nonce(
30
+ # session_id1,
31
+ # pk1,
32
+ # sk: sk1,
33
+ # key_agg_ctx: key_agg_ctx,
34
+ # msg: msg
35
+ # )
36
+ #
37
+ # session_id2 = generate_musig_session_id
38
+ # secnonce2, pubnonce2 = target.generate_musig_nonce(
39
+ # session_id2,
40
+ # pk2,
41
+ # sk: sk2,
42
+ # key_agg_ctx: key_agg_ctx,
43
+ # msg: msg
44
+ # )
45
+ #
46
+ # # Aggregate public nonces
47
+ # agg_nonce = aggregate_musig_nonce([pubnonce1, pubnonce2])
48
+ #
49
+ # # Generate partial sig
50
+ # musig_session = Secp256k1::MuSig::Session.new(key_agg_ctx, agg_nonce, msg)
51
+ # partial_sig1 = musig_session.partial_sign(secnonce1, sk1)
52
+ # partial_sig2 = musig_session.partial_sign(secnonce2, sk2)
53
+ #
54
+ # # Aggregate signature
55
+ # agg_sig = musig_session.aggregate_partial_sigs([partial_sig1, partial_sig2])
56
+ #
57
+ # # Verify schnorr signature
58
+ # verify_schnorr(msg, agg_sig, agg_pubkey)
59
+ module MuSig
60
+
61
+ # Aggregate public keys.
62
+ # @param [Array] pubkeys An array of public keys.
63
+ # @return [Secp2561k::MuSig::KeyAggContext]
64
+ # @raise [Secp256k1::Error]
65
+ def aggregate_pubkey(pubkeys)
66
+ raise ArgumentError, "pubkeys must be an array." unless pubkeys.is_a?(Array)
67
+ with_context do |context|
68
+ pubkeys_ptrs = pubkeys.map do |pubkey|
69
+ pubkey = hex2bin(pubkey)
70
+ validate_string!('pubkey', pubkey, 33)
71
+ input = FFI::MemoryPointer.new(:uchar, 33).put_bytes(0, pubkey)
72
+ pubkey_ptr = FFI::MemoryPointer.new(:uchar, 64)
73
+ raise Error, "pubkey is invalid." unless secp256k1_ec_pubkey_parse(context, pubkey_ptr, input, 33) == 1
74
+ pubkey_ptr
75
+ end
76
+ pubkeys_ptr = FFI::MemoryPointer.new(:pointer, pubkeys.length)
77
+ pubkeys_ptr.write_array_of_pointer(pubkeys_ptrs)
78
+ agg_pubkey = FFI::MemoryPointer.new(:uchar, 64)
79
+ cache = Secp256k1::MuSig::KeyAggCache.new
80
+ if secp256k1_musig_pubkey_agg(context, agg_pubkey, cache.pointer, pubkeys_ptr, pubkeys.length) == 0
81
+ raise Error, "secp256k1_musig_pubkey_agg argument error."
82
+ end
83
+ Secp256k1::MuSig::KeyAggContext.new(cache)
84
+ end
85
+ end
86
+
87
+ # Generate fresh session id for musig signing session.
88
+ # @return [String] The session id.
89
+ def generate_musig_session_id
90
+ SecureRandom.random_bytes(32).unpack1('H*')
91
+ end
92
+
93
+ # Generate nonce pair.
94
+ # @param [String] session_id The uniform random identifier for this session.
95
+ # @param [String] pk The public key for which the partial signature is generated.
96
+ # @param [String] sk (Optional) The private key for which the partial signature is generated.
97
+ # @param [Secp256k1::MuSig::KeyAggContext] key_agg_ctx (Optional) The aggregated public key context.
98
+ # @param [String] msg (Optional) The message to be signed.
99
+ # @param [String] extra_in (Optional) The auxiliary input.
100
+ # @return [Array(String)] The array of secret nonce and public nonce with hex format.
101
+ # @raise [ArgumentError] If invalid arguments specified.
102
+ # @raise [Secp256k1::Error]
103
+ def generate_musig_nonce(session_id, pk, sk: nil, key_agg_ctx: nil, msg: nil, extra_in: nil)
104
+ validate_string!("session_id", session_id, 32)
105
+ validate_string!("pk", pk, 33)
106
+ validate_string!("sk", sk, 32) if sk
107
+ validate_string!("msg", msg, 32) if msg
108
+ validate_string!("extra_in", extra_in, 32) if extra_in
109
+
110
+ if key_agg_ctx
111
+ raise ArgumentError, "key_agg must be Secp256k1::MuSig::KeyAggContext." unless key_agg_ctx.is_a?(KeyAggContext)
112
+ end
113
+
114
+ with_context do |context|
115
+ pk_ptr = FFI::MemoryPointer.new(:uchar, 33).put_bytes(0, hex2bin(pk))
116
+ pubkey = FFI::MemoryPointer.new(:uchar, 64)
117
+ raise Error, "pk is invalid public key." unless secp256k1_ec_pubkey_parse(context, pubkey, pk_ptr, 33) == 1
118
+
119
+ pubnonce = FFI::MemoryPointer.new(:uchar, 132)
120
+ secnonce = FFI::MemoryPointer.new(:uchar, 132)
121
+ seckey = sk ? FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(sk)) : nil
122
+ msg32 = msg ? FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(msg)) : nil
123
+ extra_input32 = extra_in ? FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(extra_in)) : nil
124
+ session_secrand32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(session_id))
125
+
126
+ raise Error, "arguments is invalid." unless secp256k1_musig_nonce_gen(
127
+ context, secnonce, pubnonce, session_secrand32, seckey, pubkey, msg32, key_agg_ctx.pointer, extra_input32) == 1
128
+
129
+ pub66 = FFI::MemoryPointer.new(:uchar, 66)
130
+ secp256k1_musig_pubnonce_serialize(context, pub66, pubnonce)
131
+ [secnonce.read_string(132).unpack1('H*'), pub66.read_string(66).unpack1('H*')]
132
+ end
133
+ end
134
+
135
+ # Aggregates the nonces of all signers into a single nonce.
136
+ # @param [Array] pub_nonces An array of public nonces sent by the signers.
137
+ # @return [String] An aggregated public nonce.
138
+ # @raise [Secp256k1::Error]
139
+ # @raise [ArgumentError] If invalid arguments specified.
140
+ def aggregate_musig_nonce(pub_nonces)
141
+ raise ArgumentError, "pub_nonces must be Array." unless pub_nonces.is_a?(Array)
142
+
143
+ with_context do |context|
144
+ nonce_ptrs = pub_nonces.map do |pub_nonce|
145
+ pub_nonce = hex2bin(pub_nonce)
146
+ validate_string!("pub_nonce", pub_nonce, 66)
147
+ in66 = FFI::MemoryPointer.new(:uchar, 66).put_bytes(0, pub_nonce)
148
+ pub_nonce_ptr = FFI::MemoryPointer.new(:uchar, 132)
149
+ if secp256k1_musig_pubnonce_parse(context, pub_nonce_ptr, in66) == 0
150
+ raise Error, "secp256k1_musig_pubnonce_parse error."
151
+ end
152
+ pub_nonce_ptr
153
+ end
154
+ agg_nonce = FFI::MemoryPointer.new(:uchar, 132)
155
+ pubnonces = FFI::MemoryPointer.new(:pointer, pub_nonces.length)
156
+ pubnonces.write_array_of_pointer(nonce_ptrs)
157
+ result = secp256k1_musig_nonce_agg(context, agg_nonce, pubnonces, pub_nonces.length)
158
+ raise Error, "nonce aggregation failed." if result == 0
159
+ out66 = FFI::MemoryPointer.new(:uchar, 66)
160
+ secp256k1_musig_aggnonce_serialize(context, out66, agg_nonce)
161
+ out66.read_string(66).unpack1("H*")
162
+ end
163
+ end
164
+ end
165
+ end
@@ -1,4 +1,15 @@
1
1
  module Secp256k1
2
+ # Recover module
3
+ # @example
4
+ # include Secp256k1
5
+ #
6
+ # sk, _ = generate_key_pair
7
+ # msg = Digest::SHA256.digest('message')
8
+ # sig, rec = sign_recoverable(msg, sk)
9
+ # full_sig = [rec + 0x1b + 4].pack('C') + [sig].pack('H*')
10
+ # compressed = true
11
+ # recover_pubkey = target.recover(msg, full_sig, compressed)
12
+ #
2
13
  module Recover
3
14
  # Sign data with compact format.
4
15
  # @param [String] data The 32-byte message hash being signed.
@@ -7,13 +18,10 @@ module Secp256k1
7
18
  # @raise [Secp256k1::Error] If recovery failed.
8
19
  # @raise [ArgumentError] If invalid arguments specified.
9
20
  def sign_recoverable(data, private_key)
10
- raise ArgumentError, "private_key must be String." unless private_key.is_a?(String)
11
- raise ArgumentError, "data must by String." unless data.is_a?(String)
21
+ validate_string!("private_key", private_key, 32)
22
+ validate_string!("data", data, 32)
12
23
  private_key = hex2bin(private_key)
13
- raise ArgumentError, "private_key must be 32 bytes." unless private_key.bytesize == 32
14
24
  data = hex2bin(data)
15
- raise ArgumentError, "data must be 32 bytes." unless data.bytesize == 32
16
-
17
25
  with_context do |context|
18
26
  sig = FFI::MemoryPointer.new(:uchar, 65)
19
27
  hash =FFI::MemoryPointer.new(:uchar, data.bytesize).put_bytes(0, data)
@@ -39,12 +47,10 @@ module Secp256k1
39
47
  # @raise [Secp256k1::Error] If recover failed.
40
48
  # @raise [ArgumentError] If invalid arguments specified.
41
49
  def recover(data, signature, compressed)
42
- raise ArgumentError, "data must be String." unless data.is_a?(String)
43
- raise ArgumentError, "signature must be String." unless signature.is_a?(String)
50
+ validate_string!("data", data, 32)
51
+ validate_string!("signature", signature, 65)
44
52
  signature = hex2bin(signature)
45
- raise ArgumentError, "signature must be 65 bytes." unless signature.bytesize == 65
46
53
  data = hex2bin(data)
47
- raise ArgumentError, "data must be 32 bytes." unless data.bytesize == 32
48
54
  rec = (signature[0].ord - 0x1b) & 3
49
55
  raise ArgumentError, "rec must be between 0 and 3." if rec < 0 || rec > 3
50
56
 
@@ -1,20 +1,29 @@
1
1
  module Secp256k1
2
+ # SchnorrSig module
3
+ # @example
4
+ # include Secp256k1
5
+ #
6
+ # sk, pk = generate_key_pair
7
+ #
8
+ # # sign and verify (Schnorr)
9
+ # signature = sign_schnorr(msg, sk)
10
+ # verify_schnorr(msg, signature, pk[2..-1]) # public key must be 32 bytes
11
+ #
2
12
  module SchnorrSig
3
13
 
4
14
  # Sign to data using schnorr.
5
15
  # @param [String] data The 32-byte message hash being signed with binary format.
6
16
  # @param [String] private_key a private key with hex format using sign.
7
- # @param [String] aux_rand a extra entropy.
17
+ # @param [String] aux_rand The 32-byte extra entropy.
8
18
  # @return [String] signature data with binary format. If unsupported algorithm specified, return nil.
9
19
  # @raise [ArgumentError] If invalid arguments specified.
10
20
  def sign_schnorr(data, private_key, aux_rand = nil)
11
- raise ArgumentError, "private_key must be String." unless private_key.is_a?(String)
12
- raise ArgumentError, "data must by String." unless data.is_a?(String)
21
+ validate_string!("data", data, 32)
22
+ validate_string!("private_key", private_key, 32)
23
+ validate_string!("aux_rand", aux_rand, 32) if aux_rand
13
24
  raise ArgumentError, "aux_rand must be String." if !aux_rand.nil? && !aux_rand.is_a?(String)
14
25
  private_key = hex2bin(private_key)
15
- raise ArgumentError, "private_key must be 32 bytes." unless private_key.bytesize == 32
16
26
  data = hex2bin(data)
17
- raise ArgumentError, "data must be 32 bytes." unless data.bytesize == 32
18
27
 
19
28
  with_context do |context|
20
29
  keypair = [create_keypair(private_key)].pack('H*')
@@ -34,11 +43,10 @@ module Secp256k1
34
43
  # @return [Boolean] verification result.
35
44
  # @raise [ArgumentError] If invalid arguments specified.
36
45
  def verify_schnorr(data, signature, pubkey)
37
- raise ArgumentError, "sig must be String." unless signature.is_a?(String)
38
- raise ArgumentError, "pubkey must be String." unless pubkey.is_a?(String)
39
- raise ArgumentError, "data must be String." unless data.is_a?(String)
46
+ validate_string!("data", data, 32)
47
+ validate_string!("signature", signature, 64)
48
+ validate_string!("pubkey", pubkey, 32)
40
49
  data = hex2bin(data)
41
- raise ArgumentError, "data must be 32 bytes." unless data.bytesize == 32
42
50
  pubkey = hex2bin(pubkey)
43
51
  signature = hex2bin(signature)
44
52
  with_context do |context|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Secp256k1
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/secp256k1.rb CHANGED
@@ -6,8 +6,23 @@ require_relative 'secp256k1/c'
6
6
  require_relative 'secp256k1/recovery'
7
7
  require_relative 'secp256k1/ellswift'
8
8
  require_relative 'secp256k1/schnorrsig'
9
+ require_relative 'secp256k1/musig'
9
10
 
10
11
  # Binding for secp256k1 (https://github.com/bitcoin-core/secp256k1/)
12
+ # @example
13
+ # include Secp256k1
14
+ #
15
+ # # Generate key pair
16
+ # sk, pk = generate_key_pair
17
+ #
18
+ # # Generate public key
19
+ # pk = generate_pubkey(sk)
20
+ #
21
+ # # sign and verify (ECDSA)
22
+ # msg = Digest::SHA256.digest('message')
23
+ # signature = sign_ecdsa(msg, sk)
24
+ # verify_ecdsa(msg, signature, pk)
25
+ #
11
26
  module Secp256k1
12
27
 
13
28
  class Error < StandardError; end
@@ -16,6 +31,7 @@ module Secp256k1
16
31
  include Recover
17
32
  include SchnorrSig
18
33
  include EllSwift
34
+ include MuSig
19
35
 
20
36
  FLAGS_TYPE_MASK = ((1 << 8) - 1)
21
37
  FLAGS_TYPE_CONTEXT = (1 << 0)
@@ -79,34 +95,13 @@ module Secp256k1
79
95
  # @return [String] Public key with hex format.
80
96
  # @raise [ArgumentError] If invalid arguments specified.
81
97
  def generate_pubkey(private_key, compressed: true)
82
- raise ArgumentError, "private_key must be String." unless private_key.is_a?(String)
98
+ validate_string!("private_key", private_key, 32)
83
99
  private_key = hex2bin(private_key)
84
- raise ArgumentError, "private_key must by 32 bytes." unless private_key.bytesize == 32
85
100
  with_context do |context|
86
101
  generate_pubkey_in_context(context, private_key, compressed: compressed)
87
102
  end
88
103
  end
89
104
 
90
- # Sign to data.
91
- # @param [String] data The 32-byte message hash being signed with binary format.
92
- # @param [String] private_key a private key with hex format using sign.
93
- # @param [String] extra_entropy a extra entropy with binary format for rfc6979.
94
- # @param [Symbol] algo signature algorithm. ecdsa(default) or schnorr.
95
- # @return [String] signature data with binary format. If unsupported algorithm specified, return nil.
96
- # @raise [ArgumentError] If invalid arguments specified.
97
- def sign_data(data, private_key, extra_entropy = nil, algo: :ecdsa)
98
-
99
- case algo
100
- when :ecdsa
101
- sign_ecdsa(data, private_key, extra_entropy)
102
- when :schnorr
103
- sign_schnorr(data, private_key, extra_entropy)
104
- else
105
- raise ArgumentError, "unknown algo: #{algo}"
106
- end
107
- end
108
-
109
-
110
105
  # Validate whether this is a valid public key.
111
106
  # @param [String] pubkey public key with hex format.
112
107
  # @param [Boolean] allow_hybrid whether support hybrid public key.
@@ -131,9 +126,8 @@ module Secp256k1
131
126
  # @raise [Secp256k1::Error] If private_key is invalid.
132
127
  # @raise [ArgumentError] If invalid arguments specified.
133
128
  def create_keypair(private_key)
134
- raise ArgumentError, "private_key must be String." unless private_key.is_a?(String)
129
+ validate_string!("private_key", private_key, 32)
135
130
  private_key = hex2bin(private_key)
136
- raise ArgumentError, "private_key must be 32 bytes." unless private_key.bytesize == 32
137
131
  with_context do |context|
138
132
  secret = FFI::MemoryPointer.new(:uchar, private_key.bytesize).put_bytes(0, private_key)
139
133
  raise Error, 'private_key is invalid.' unless secp256k1_ec_seckey_verify(context, secret)
@@ -159,17 +153,15 @@ module Secp256k1
159
153
  # Sign to data using ecdsa.
160
154
  # @param [String] data The 32-byte message hash being signed with binary format.
161
155
  # @param [String] private_key a private key with hex format using sign.
162
- # @param [String] extra_entropy a extra entropy with binary format for rfc6979.
156
+ # @param [String] extra_entropy (Optional)An extra entropy with binary format for rfc6979.
163
157
  # @return [String] signature data with binary format. If unsupported algorithm specified, return nil.
164
158
  # @raise [ArgumentError] If invalid arguments specified.
165
- def sign_ecdsa(data, private_key, extra_entropy)
166
- raise ArgumentError, "private_key must be String." unless private_key.is_a?(String)
167
- raise ArgumentError, "data must by String." unless data.is_a?(String)
168
- raise ArgumentError, "extra_entropy must be String." if !extra_entropy.nil? && !extra_entropy.is_a?(String)
159
+ def sign_ecdsa(data, private_key, extra_entropy = nil)
160
+ validate_string!("private_key", private_key, 32)
161
+ validate_string!("data", data, 32)
162
+ validate_string!("extra_entropy", extra_entropy, 32) if extra_entropy
169
163
  private_key = hex2bin(private_key)
170
- raise ArgumentError, "private_key must be 32 bytes." unless private_key.bytesize == 32
171
164
  data = hex2bin(data)
172
- raise ArgumentError, "data must be 32 bytes." unless data.bytesize == 32
173
165
 
174
166
  with_context do |context|
175
167
  secret = FFI::MemoryPointer.new(:uchar, private_key.bytesize).put_bytes(0, private_key)
@@ -205,9 +197,8 @@ module Secp256k1
205
197
  def verify_ecdsa(data, signature, pubkey)
206
198
  raise ArgumentError, "sig must be String." unless signature.is_a?(String)
207
199
  raise ArgumentError, "pubkey must be String." unless pubkey.is_a?(String)
208
- raise ArgumentError, "data must be String." unless data.is_a?(String)
200
+ validate_string!("data", data, 32)
209
201
  data = hex2bin(data)
210
- raise ArgumentError, "data must be 32 bytes." unless data.bytesize == 32
211
202
  pubkey = hex2bin(pubkey)
212
203
  signature = hex2bin(signature)
213
204
  with_context do |context|
@@ -281,5 +272,10 @@ module Secp256k1
281
272
  def hex2bin(str)
282
273
  hex_string?(str) ? [str].pack('H*') : str
283
274
  end
275
+
276
+ def validate_string!(name, target, byte_length)
277
+ raise ArgumentError, "#{name} must be String." unless target.is_a?(String)
278
+ raise ArgumentError, "#{name} must be #{byte_length} bytes." unless hex2bin(target).bytesize == byte_length
279
+ end
284
280
  end
285
281
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: secp256k1rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - azuchi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-11-14 00:00:00.000000000 Z
11
+ date: 2024-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -43,6 +43,9 @@ files:
43
43
  - lib/secp256k1.rb
44
44
  - lib/secp256k1/c.rb
45
45
  - lib/secp256k1/ellswift.rb
46
+ - lib/secp256k1/musig.rb
47
+ - lib/secp256k1/musig/key_agg.rb
48
+ - lib/secp256k1/musig/session.rb
46
49
  - lib/secp256k1/recovery.rb
47
50
  - lib/secp256k1/schnorrsig.rb
48
51
  - lib/secp256k1/version.rb