secp256k1rb 0.2.0 → 0.8.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.
@@ -132,6 +132,44 @@ module Secp256k1
132
132
  end
133
133
  end
134
134
 
135
+ # Generate a nonce pair deterministically using a non-repeating counter instead of session randomness.
136
+ # The caller must ensure that +counter+ never repeats for the same key pair.
137
+ # @param [Integer] counter A non-repeating counter(uint64).
138
+ # @param [String] private_key The private key for which the partial signature is generated, with hex format.
139
+ # @param [Secp256k1::MuSig::KeyAggContext] key_agg_ctx (Optional) The aggregated public key context.
140
+ # @param [String] msg (Optional) The message to be signed.
141
+ # @param [String] extra_in (Optional) The auxiliary input.
142
+ # @return [Array(String)] The array of secret nonce and public nonce with hex format.
143
+ # @raise [ArgumentError] If invalid arguments specified.
144
+ # @raise [Secp256k1::Error]
145
+ def generate_musig_nonce_counter(counter, private_key, key_agg_ctx: nil, msg: nil, extra_in: nil)
146
+ raise ArgumentError, "counter must be Integer." unless counter.is_a?(Integer)
147
+ raise ArgumentError, "counter must be between 0 and 2**64-1." if counter < 0 || counter > (2**64 - 1)
148
+ validate_string!("private_key", private_key, 32)
149
+ validate_string!("msg", msg, 32) if msg
150
+ validate_string!("extra_in", extra_in, 32) if extra_in
151
+ if key_agg_ctx
152
+ raise ArgumentError, "key_agg_ctx must be Secp256k1::MuSig::KeyAggContext." unless key_agg_ctx.is_a?(KeyAggContext)
153
+ end
154
+
155
+ with_context do |context|
156
+ keypair = [create_keypair(private_key)].pack('H*')
157
+ keypair_ptr = FFI::MemoryPointer.new(:uchar, 96).put_bytes(0, keypair)
158
+ pubnonce = FFI::MemoryPointer.new(:uchar, 132)
159
+ secnonce = FFI::MemoryPointer.new(:uchar, 132)
160
+ msg32 = msg ? FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(msg)) : nil
161
+ extra_input32 = extra_in ? FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(extra_in)) : nil
162
+ cache_ptr = key_agg_ctx ? key_agg_ctx.pointer : nil
163
+
164
+ raise Error, "arguments is invalid." unless secp256k1_musig_nonce_gen_counter(
165
+ context, secnonce, pubnonce, counter, keypair_ptr, msg32, cache_ptr, extra_input32) == 1
166
+
167
+ pub66 = FFI::MemoryPointer.new(:uchar, 66)
168
+ secp256k1_musig_pubnonce_serialize(context, pub66, pubnonce)
169
+ [secnonce.read_string(132).unpack1('H*'), pub66.read_string(66).unpack1('H*')]
170
+ end
171
+ end
172
+
135
173
  # Aggregates the nonces of all signers into a single nonce.
136
174
  # @param [Array] pub_nonces An array of public nonces sent by the signers.
137
175
  # @return [String] An aggregated public nonce.
@@ -68,5 +68,28 @@ module Secp256k1
68
68
  serialize_pubkey_internal(context, pubkey.read_string(64), compressed)
69
69
  end
70
70
  end
71
+
72
+ # Convert a recoverable signature into a normal DER-encoded ECDSA signature.
73
+ # @param [String] signature The recoverable signature with binary format(65 bytes), as accepted by {#recover}.
74
+ # @return [String] DER-encoded signature with binary format.
75
+ # @raise [Secp256k1::Error] If conversion failed.
76
+ # @raise [ArgumentError] If invalid arguments specified.
77
+ def recoverable_signature_to_ecdsa(signature)
78
+ validate_string!("signature", signature, 65)
79
+ signature = hex2bin(signature)
80
+ rec = (signature[0].ord - 0x1b) & 3
81
+ raise ArgumentError, "rec must be between 0 and 3." if rec < 0 || rec > 3
82
+ with_context do |context|
83
+ recoverable = FFI::MemoryPointer.new(:uchar, 65)
84
+ input = FFI::MemoryPointer.new(:uchar, 64).put_bytes(0, signature[1..-1])
85
+ raise Error, 'secp256k1_ecdsa_recoverable_signature_parse_compact failed.' unless secp256k1_ecdsa_recoverable_signature_parse_compact(context, recoverable, input, rec) == 1
86
+ internal_signature = FFI::MemoryPointer.new(:uchar, 64)
87
+ raise Error, 'secp256k1_ecdsa_recoverable_signature_convert failed.' unless secp256k1_ecdsa_recoverable_signature_convert(context, internal_signature, recoverable) == 1
88
+ output = FFI::MemoryPointer.new(:uchar, 72)
89
+ output_len = FFI::MemoryPointer.new(:uint64).put_uint64(0, 72)
90
+ raise Error, 'secp256k1_ecdsa_signature_serialize_der failed.' unless secp256k1_ecdsa_signature_serialize_der(context, output, output_len, internal_signature) == 1
91
+ output.read_string(output_len.read_uint64)
92
+ end
93
+ end
71
94
  end
72
95
  end
@@ -36,7 +36,44 @@ module Secp256k1
36
36
  end
37
37
  end
38
38
 
39
- # Verify ecdsa signature.
39
+ # Magic bytes for secp256k1_schnorrsig_extraparams.
40
+ SCHNORRSIG_EXTRAPARAMS_MAGIC = [0xda, 0x6f, 0xb3, 0x8c].pack('C*').freeze
41
+
42
+ # Sign to a variable-length message using schnorr (BIP340 sign with custom parameters).
43
+ # @param [String] data The message being signed with binary format. Unlike {#sign_schnorr}, the length is arbitrary.
44
+ # @param [String] private_key a private key with hex format using sign.
45
+ # @param [String] aux_rand (Optional)The 32-byte extra entropy.
46
+ # @return [String] signature data with binary format(64 bytes).
47
+ # @raise [Secp256k1::Error] If signing failed.
48
+ # @raise [ArgumentError] If invalid arguments specified.
49
+ def sign_schnorr_custom(data, private_key, aux_rand = nil)
50
+ raise ArgumentError, "data must be String." unless data.is_a?(String)
51
+ validate_string!("private_key", private_key, 32)
52
+ validate_string!("aux_rand", aux_rand, 32) if aux_rand
53
+ private_key = hex2bin(private_key)
54
+ data = hex2bin(data)
55
+ aux_rand = hex2bin(aux_rand) if aux_rand
56
+
57
+ with_context do |context|
58
+ keypair = [create_keypair(private_key)].pack('H*')
59
+ keypair = FFI::MemoryPointer.new(:uchar, 96).put_bytes(0, keypair)
60
+ signature = FFI::MemoryPointer.new(:uchar, 64)
61
+ msg = FFI::MemoryPointer.new(:uchar, [data.bytesize, 1].max).put_bytes(0, data)
62
+
63
+ # Build secp256k1_schnorrsig_extraparams: magic[4], noncefp(NULL=default BIP340), ndata(aux_rand or NULL).
64
+ ptr_size = FFI::Pointer.size
65
+ extraparams = FFI::MemoryPointer.new(:uchar, ptr_size * 3)
66
+ extraparams.put_bytes(0, SCHNORRSIG_EXTRAPARAMS_MAGIC)
67
+ extraparams.put_pointer(ptr_size, FFI::Pointer::NULL)
68
+ ndata = aux_rand ? FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, aux_rand) : FFI::Pointer::NULL
69
+ extraparams.put_pointer(ptr_size * 2, ndata)
70
+
71
+ raise Error, 'Failed to generate schnorr signature.' unless secp256k1_schnorrsig_sign_custom(context, signature, msg, data.bytesize, keypair, extraparams) == 1
72
+ signature.read_string(64)
73
+ end
74
+ end
75
+
76
+ # Verify schnorr signature.
40
77
  # @param [String] data The 32-byte message hash assumed to be signed.
41
78
  # @param [String] signature signature data with binary format
42
79
  # @param [String] pubkey a public key with hex format using verify.
@@ -44,6 +81,23 @@ module Secp256k1
44
81
  # @raise [ArgumentError] If invalid arguments specified.
45
82
  def verify_schnorr(data, signature, pubkey)
46
83
  validate_string!("data", data, 32)
84
+ verify_schnorr_internal(data, signature, pubkey)
85
+ end
86
+
87
+ # Verify a schnorr signature over a variable-length message (counterpart of {#sign_schnorr_custom}).
88
+ # @param [String] data The message assumed to be signed. The length is arbitrary.
89
+ # @param [String] signature signature data with binary format(64 bytes).
90
+ # @param [String] pubkey an x-only public key with hex format(32 bytes) using verify.
91
+ # @return [Boolean] verification result.
92
+ # @raise [ArgumentError] If invalid arguments specified.
93
+ def verify_schnorr_custom(data, signature, pubkey)
94
+ raise ArgumentError, "data must be String." unless data.is_a?(String)
95
+ verify_schnorr_internal(data, signature, pubkey)
96
+ end
97
+
98
+ private
99
+
100
+ def verify_schnorr_internal(data, signature, pubkey)
47
101
  validate_string!("signature", signature, 64)
48
102
  validate_string!("pubkey", pubkey, 32)
49
103
  data = hex2bin(data)
@@ -54,8 +108,8 @@ module Secp256k1
54
108
  pubkey = [full_pubkey_from_xonly_pubkey(pubkey)].pack('H*')
55
109
  xonly_pubkey = FFI::MemoryPointer.new(:uchar, pubkey.bytesize).put_bytes(0, pubkey)
56
110
  signature = FFI::MemoryPointer.new(:uchar, signature.bytesize).put_bytes(0, signature)
57
- msg32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, data)
58
- result = secp256k1_schnorrsig_verify(context, signature, msg32, 32, xonly_pubkey)
111
+ msg = FFI::MemoryPointer.new(:uchar, [data.bytesize, 1].max).put_bytes(0, data)
112
+ result = secp256k1_schnorrsig_verify(context, signature, msg, data.bytesize, xonly_pubkey)
59
113
  result == 1
60
114
  end
61
115
  end
@@ -0,0 +1,319 @@
1
+ module Secp256k1
2
+
3
+ # Silent Payments module (BIP-352).
4
+ # This module provides elliptic-curve operations required for sending and
5
+ # receiving Silent Payments. Note that this is not a full implementation of
6
+ # BIP-352; higher-level concepts like addresses, output script types or
7
+ # transactions must be handled by the caller.
8
+ # @example
9
+ # include Secp256k1
10
+ #
11
+ # # Sender side:
12
+ # outputs = silentpayments_sender_create_outputs(
13
+ # [[scan_pubkey, spend_pubkey]], outpoint_smallest, plain_seckeys: [seckey])
14
+ #
15
+ # # Recipient side:
16
+ # prevouts_summary = silentpayments_create_prevouts_summary(
17
+ # outpoint_smallest, plain_pubkeys: [pubkey])
18
+ # found = silentpayments_scan_outputs(
19
+ # outputs, scan_key, prevouts_summary, spend_pubkey)
20
+ module SilentPayments
21
+
22
+ # Size of a serialized Silent Payments label.
23
+ SP_LABEL_SIZE = 33
24
+ # Size of an internal Silent Payments label object.
25
+ SP_LABEL_INTERNAL_SIZE = 68
26
+ # Size of a Silent Payments prevouts summary object.
27
+ SP_PREVOUTS_SUMMARY_SIZE = 101
28
+ # Maximum number of recipients sharing the same scan public key (BIP-352).
29
+ SP_RECIPIENT_GROUP_LIMIT = 2323
30
+
31
+ # Struct mirroring secp256k1_silentpayments_recipient.
32
+ class Recipient < FFI::Struct
33
+ layout :scan_pubkey, [:uchar, 64],
34
+ :spend_pubkey, [:uchar, 64],
35
+ :index, :size_t
36
+ end
37
+
38
+ # Struct mirroring secp256k1_silentpayments_found_output.
39
+ class FoundOutput < FFI::Struct
40
+ layout :output, [:uchar, 64],
41
+ :tweak, [:uchar, 32],
42
+ :found_with_label, :int,
43
+ :label, [:uchar, SP_LABEL_INTERNAL_SIZE]
44
+ end
45
+
46
+ # Whether the loaded libsecp256k1 supports the silentpayments module.
47
+ # The module is available in libsecp256k1 v0.8.0 or later.
48
+ # @return [Boolean]
49
+ def silentpayments_available?
50
+ C.silentpayments_available?
51
+ end
52
+
53
+ # Create Silent Payments outputs for recipient(s) (sender side).
54
+ # @param [Array] recipients An array of recipient's key pair. Each element is an array of
55
+ # scan public key and (labeled) spend public key with hex format(33 bytes each).
56
+ # @param [String] outpoint_smallest The smallest outpoint lexicographically from the transaction inputs(36 bytes).
57
+ # @param [Array] plain_seckeys (Optional) An array of private keys of non-taproot inputs with hex format(32 bytes each).
58
+ # @param [Array] taproot_seckeys (Optional) An array of private keys of taproot inputs with hex format(32 bytes each).
59
+ # @return [Array] An array of x-only public keys(hex format) for the taproot outputs, ordered same as +recipients+.
60
+ # @raise [Secp256k1::Error] If creation failed.
61
+ # @raise [ArgumentError] If invalid arguments specified.
62
+ def silentpayments_sender_create_outputs(recipients, outpoint_smallest, plain_seckeys: [], taproot_seckeys: [])
63
+ check_silentpayments_support!
64
+ raise ArgumentError, "recipients must be an Array." unless recipients.is_a?(Array)
65
+ raise ArgumentError, "recipients must not be empty." if recipients.empty?
66
+ plain_seckeys ||= []
67
+ taproot_seckeys ||= []
68
+ raise ArgumentError, "plain_seckeys must be an Array." unless plain_seckeys.is_a?(Array)
69
+ raise ArgumentError, "taproot_seckeys must be an Array." unless taproot_seckeys.is_a?(Array)
70
+ if plain_seckeys.empty? && taproot_seckeys.empty?
71
+ raise ArgumentError, "At least one private key(plain_seckeys or taproot_seckeys) is required."
72
+ end
73
+ validate_string!("outpoint_smallest", outpoint_smallest, 36)
74
+
75
+ with_context do |context|
76
+ recipient_structs = recipients.each_with_index.map do |recipient, i|
77
+ scan_pubkey, spend_pubkey = recipient
78
+ validate_string!("scan_pubkey", scan_pubkey, 33)
79
+ validate_string!("spend_pubkey", spend_pubkey, 33)
80
+ st = Recipient.new
81
+ st.pointer.put_bytes(0, parse_pubkey_internal(context, hex2bin(scan_pubkey)).read_string(64))
82
+ st.pointer.put_bytes(64, parse_pubkey_internal(context, hex2bin(spend_pubkey)).read_string(64))
83
+ st[:index] = i
84
+ st
85
+ end
86
+ recipients_ptr = FFI::MemoryPointer.new(:pointer, recipient_structs.length)
87
+ recipients_ptr.write_array_of_pointer(recipient_structs.map(&:pointer))
88
+
89
+ generated_outputs = Array.new(recipients.length) { FFI::MemoryPointer.new(:uchar, 64) }
90
+ generated_outputs_ptr = FFI::MemoryPointer.new(:pointer, generated_outputs.length)
91
+ generated_outputs_ptr.write_array_of_pointer(generated_outputs)
92
+
93
+ outpoint_ptr = FFI::MemoryPointer.new(:uchar, 36).put_bytes(0, hex2bin(outpoint_smallest))
94
+
95
+ keypairs = taproot_seckeys.map do |sk|
96
+ validate_string!("taproot_seckey", sk, 32)
97
+ seckey = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(sk))
98
+ keypair = FFI::MemoryPointer.new(:uchar, 96)
99
+ raise Error, "taproot_seckey is invalid." unless secp256k1_keypair_create(context, keypair, seckey) == 1
100
+ keypair
101
+ end
102
+ keypairs_ptr = if keypairs.empty?
103
+ nil
104
+ else
105
+ FFI::MemoryPointer.new(:pointer, keypairs.length).write_array_of_pointer(keypairs)
106
+ end
107
+
108
+ seckeys = plain_seckeys.map do |sk|
109
+ validate_string!("plain_seckey", sk, 32)
110
+ FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(sk))
111
+ end
112
+ seckeys_ptr = if seckeys.empty?
113
+ nil
114
+ else
115
+ FFI::MemoryPointer.new(:pointer, seckeys.length).write_array_of_pointer(seckeys)
116
+ end
117
+
118
+ result = secp256k1_silentpayments_sender_create_outputs(
119
+ context, generated_outputs_ptr, recipients_ptr, recipients.length, outpoint_ptr,
120
+ keypairs_ptr, keypairs.length, seckeys_ptr, seckeys.length)
121
+ raise Error, "secp256k1_silentpayments_sender_create_outputs failed." unless result == 1
122
+
123
+ generated_outputs.map { |output| serialize_xonly_pubkey_internal(context, output) }
124
+ end
125
+ end
126
+
127
+ # Create Silent Payments label tweak and label(recipient side).
128
+ # @param [String] scan_key The recipient's scan key with hex format(32 bytes).
129
+ # @param [Integer] m The integer for the m-th label (0 is used for change outputs).
130
+ # @return [Array(String)] An array of serialized label(33 bytes) and label tweak(32 bytes) with hex format.
131
+ # @raise [Secp256k1::Error] If creation failed.
132
+ # @raise [ArgumentError] If invalid arguments specified.
133
+ def silentpayments_create_label(scan_key, m)
134
+ check_silentpayments_support!
135
+ validate_string!("scan_key", scan_key, 32)
136
+ raise ArgumentError, "m must be Integer." unless m.is_a?(Integer)
137
+ raise ArgumentError, "m must be between 0 and 2**32-1." if m < 0 || m > 0xffffffff
138
+
139
+ with_context do |context|
140
+ label = FFI::MemoryPointer.new(:uchar, SP_LABEL_INTERNAL_SIZE)
141
+ label_tweak = FFI::MemoryPointer.new(:uchar, 32)
142
+ scan_key_ptr = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(scan_key))
143
+ result = secp256k1_silentpayments_recipient_label_create(context, label, label_tweak, scan_key_ptr, m)
144
+ raise Error, "secp256k1_silentpayments_recipient_label_create failed." unless result == 1
145
+ out33 = FFI::MemoryPointer.new(:uchar, SP_LABEL_SIZE)
146
+ secp256k1_silentpayments_recipient_label_serialize(context, out33, label)
147
+ [out33.read_string(SP_LABEL_SIZE).unpack1('H*'), label_tweak.read_string(32).unpack1('H*')]
148
+ end
149
+ end
150
+
151
+ # Create Silent Payments labeled spend public key(recipient side).
152
+ # @param [String] spend_pubkey The recipient's unlabeled spend public key with hex format(33 bytes).
153
+ # @param [String] label The serialized label with hex format(33 bytes).
154
+ # @return [String] The labeled spend public key with hex format(33 bytes).
155
+ # @raise [Secp256k1::Error] If creation failed.
156
+ # @raise [ArgumentError] If invalid arguments specified.
157
+ def silentpayments_create_labeled_spend_pubkey(spend_pubkey, label)
158
+ check_silentpayments_support!
159
+ validate_string!("spend_pubkey", spend_pubkey, 33)
160
+ validate_string!("label", label, SP_LABEL_SIZE)
161
+
162
+ with_context do |context|
163
+ spend_pubkey_ptr = parse_pubkey_internal(context, hex2bin(spend_pubkey))
164
+ label_ptr = parse_label_internal(context, hex2bin(label))
165
+ labeled = FFI::MemoryPointer.new(:uchar, 64)
166
+ result = secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(context, labeled, spend_pubkey_ptr, label_ptr)
167
+ raise Error, "secp256k1_silentpayments_recipient_create_labeled_spend_pubkey failed." unless result == 1
168
+ serialize_pubkey_internal(context, labeled, true)
169
+ end
170
+ end
171
+
172
+ # Compute Silent Payments prevouts summary from prevout public keys and transaction inputs(recipient side).
173
+ # @param [String] outpoint_smallest The smallest outpoint lexicographically from the transaction inputs(36 bytes).
174
+ # @param [Array] plain_pubkeys (Optional) An array of public keys of non-taproot inputs with hex format(33 bytes each).
175
+ # @param [Array] xonly_pubkeys (Optional) An array of x-only public keys of taproot inputs with hex format(32 bytes each).
176
+ # @return [String] The prevouts summary data with hex format(101 bytes).
177
+ # Note that this is an opaque representation which is not guaranteed to be
178
+ # portable between different platforms or library versions.
179
+ # @raise [Secp256k1::Error] If the transaction is not a Silent Payments transaction.
180
+ # @raise [ArgumentError] If invalid arguments specified.
181
+ def silentpayments_create_prevouts_summary(outpoint_smallest, plain_pubkeys: [], xonly_pubkeys: [])
182
+ check_silentpayments_support!
183
+ plain_pubkeys ||= []
184
+ xonly_pubkeys ||= []
185
+ raise ArgumentError, "plain_pubkeys must be an Array." unless plain_pubkeys.is_a?(Array)
186
+ raise ArgumentError, "xonly_pubkeys must be an Array." unless xonly_pubkeys.is_a?(Array)
187
+ if plain_pubkeys.empty? && xonly_pubkeys.empty?
188
+ raise ArgumentError, "At least one public key(plain_pubkeys or xonly_pubkeys) is required."
189
+ end
190
+ validate_string!("outpoint_smallest", outpoint_smallest, 36)
191
+
192
+ with_context do |context|
193
+ xonly_ptrs = xonly_pubkeys.map do |pubkey|
194
+ validate_string!("xonly_pubkey", pubkey, 32)
195
+ parse_xonly_pubkey_internal(context, hex2bin(pubkey))
196
+ end
197
+ plain_ptrs = plain_pubkeys.map do |pubkey|
198
+ validate_string!("plain_pubkey", pubkey, 33)
199
+ parse_pubkey_internal(context, hex2bin(pubkey))
200
+ end
201
+ xonly_arr = if xonly_ptrs.empty?
202
+ nil
203
+ else
204
+ FFI::MemoryPointer.new(:pointer, xonly_ptrs.length).write_array_of_pointer(xonly_ptrs)
205
+ end
206
+ plain_arr = if plain_ptrs.empty?
207
+ nil
208
+ else
209
+ FFI::MemoryPointer.new(:pointer, plain_ptrs.length).write_array_of_pointer(plain_ptrs)
210
+ end
211
+ outpoint_ptr = FFI::MemoryPointer.new(:uchar, 36).put_bytes(0, hex2bin(outpoint_smallest))
212
+ prevouts_summary = FFI::MemoryPointer.new(:uchar, SP_PREVOUTS_SUMMARY_SIZE)
213
+ result = secp256k1_silentpayments_recipient_prevouts_summary_create(
214
+ context, prevouts_summary, outpoint_ptr, xonly_arr, xonly_ptrs.length, plain_arr, plain_ptrs.length)
215
+ raise Error, "secp256k1_silentpayments_recipient_prevouts_summary_create failed." unless result == 1
216
+ prevouts_summary.read_string(SP_PREVOUTS_SUMMARY_SIZE).unpack1('H*')
217
+ end
218
+ end
219
+
220
+ # Scan for Silent Payments transaction outputs(recipient side).
221
+ # @param [Array] tx_outputs An array of the transaction's x-only public key outputs with hex format(32 bytes each),
222
+ # in their original transaction (vout) order.
223
+ # @param [String] scan_key The recipient's scan key with hex format(32 bytes).
224
+ # @param [String] prevouts_summary The prevouts summary data created by #silentpayments_create_prevouts_summary
225
+ # with hex format(101 bytes).
226
+ # @param [String] spend_pubkey The recipient's unlabeled spend public key with hex format(33 bytes).
227
+ # @param [Hash] labels (Optional) The recipient's label cache. A hash whose key is a serialized label(33 bytes)
228
+ # and whose value is the corresponding label tweak(32 bytes), both with hex format.
229
+ # Can be created with #silentpayments_create_label.
230
+ # @return [Array] An array of found outputs. Each element is a hash with the following keys:
231
+ # - :output x-only public key of the found output with hex format(32 bytes).
232
+ # - :tweak The 32-byte tweak needed to spend the output with hex format.
233
+ # - :label The serialized label(33 bytes with hex format) if the output was sent to a labeled address, otherwise nil.
234
+ # @raise [Secp256k1::Error] If the transaction is not a Silent Payments transaction or arguments are invalid.
235
+ # @raise [ArgumentError] If invalid arguments specified.
236
+ def silentpayments_scan_outputs(tx_outputs, scan_key, prevouts_summary, spend_pubkey, labels: nil)
237
+ check_silentpayments_support!
238
+ raise ArgumentError, "tx_outputs must be an Array." unless tx_outputs.is_a?(Array)
239
+ raise ArgumentError, "tx_outputs must not be empty." if tx_outputs.empty?
240
+ validate_string!("scan_key", scan_key, 32)
241
+ validate_string!("prevouts_summary", prevouts_summary, SP_PREVOUTS_SUMMARY_SIZE)
242
+ validate_string!("spend_pubkey", spend_pubkey, 33)
243
+ raise ArgumentError, "labels must be a Hash." if labels && !labels.is_a?(Hash)
244
+
245
+ with_context do |context|
246
+ output_ptrs = tx_outputs.map do |output|
247
+ validate_string!("tx_output", output, 32)
248
+ parse_xonly_pubkey_internal(context, hex2bin(output))
249
+ end
250
+ tx_outputs_ptr = FFI::MemoryPointer.new(:pointer, output_ptrs.length).write_array_of_pointer(output_ptrs)
251
+
252
+ found_outputs = Array.new(tx_outputs.length) { FoundOutput.new }
253
+ found_outputs_ptr = FFI::MemoryPointer.new(:pointer, found_outputs.length)
254
+ found_outputs_ptr.write_array_of_pointer(found_outputs.map(&:pointer))
255
+ n_found = FFI::MemoryPointer.new(:uint32)
256
+
257
+ scan_key_ptr = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(scan_key))
258
+ prevouts_summary_ptr = FFI::MemoryPointer.new(:uchar, SP_PREVOUTS_SUMMARY_SIZE).
259
+ put_bytes(0, hex2bin(prevouts_summary))
260
+ spend_pubkey_ptr = parse_pubkey_internal(context, hex2bin(spend_pubkey))
261
+
262
+ label_lookup = nil
263
+ if labels
264
+ # Keep tweak pointers referenced in this hash so they remain valid while scanning.
265
+ tweak_cache = {}
266
+ labels.each do |label, tweak|
267
+ validate_string!("label", label, SP_LABEL_SIZE)
268
+ validate_string!("label_tweak", tweak, 32)
269
+ tweak_cache[hex2bin(label)] = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(tweak))
270
+ end
271
+ label_lookup = FFI::Function.new(:pointer, [:pointer, :pointer]) do |label33, _|
272
+ tweak_cache[label33.read_string(SP_LABEL_SIZE)] || FFI::Pointer::NULL
273
+ end
274
+ end
275
+
276
+ result = secp256k1_silentpayments_recipient_scan_outputs(
277
+ context, found_outputs_ptr, n_found, tx_outputs_ptr, tx_outputs.length,
278
+ scan_key_ptr, prevouts_summary_ptr, spend_pubkey_ptr, label_lookup, nil)
279
+ raise Error, "secp256k1_silentpayments_recipient_scan_outputs failed." unless result == 1
280
+
281
+ Array.new(n_found.read_uint32) do |i|
282
+ found = found_outputs[i]
283
+ label = if found[:found_with_label] != 0
284
+ out33 = FFI::MemoryPointer.new(:uchar, SP_LABEL_SIZE)
285
+ secp256k1_silentpayments_recipient_label_serialize(
286
+ context, out33, found.pointer + FoundOutput.offset_of(:label))
287
+ out33.read_string(SP_LABEL_SIZE).unpack1('H*')
288
+ end
289
+ {
290
+ output: serialize_xonly_pubkey_internal(context, found.pointer),
291
+ tweak: found.pointer.get_bytes(FoundOutput.offset_of(:tweak), 32).unpack1('H*'),
292
+ label: label
293
+ }
294
+ end
295
+ end
296
+ end
297
+
298
+ private
299
+
300
+ # @raise [Secp256k1::Error] If the loaded libsecp256k1 does not support the silentpayments module.
301
+ def check_silentpayments_support!
302
+ unless C.silentpayments_available?
303
+ raise Error, "The loaded libsecp256k1 does not support the silentpayments module. libsecp256k1 v0.8.0 or later is required."
304
+ end
305
+ end
306
+
307
+ # Parse a serialized label into an internal label pointer.
308
+ # @param [FFI::Pointer] context secp256k1 context.
309
+ # @param [String] label serialized label with binary format(33 bytes).
310
+ # @return [FFI::Pointer] internal label pointer(68 bytes).
311
+ # @raise [Secp256k1::Error] If the label is invalid.
312
+ def parse_label_internal(context, label)
313
+ label_ptr = FFI::MemoryPointer.new(:uchar, SP_LABEL_SIZE).put_bytes(0, label)
314
+ internal = FFI::MemoryPointer.new(:uchar, SP_LABEL_INTERNAL_SIZE)
315
+ raise Error, 'An invalid label was specified.' unless secp256k1_silentpayments_recipient_label_parse(context, internal, label_ptr) == 1
316
+ internal
317
+ end
318
+ end
319
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Secp256k1
4
- VERSION = "0.2.0"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/secp256k1.rb CHANGED
@@ -3,10 +3,13 @@
3
3
  require 'securerandom'
4
4
  require_relative "secp256k1/version"
5
5
  require_relative 'secp256k1/c'
6
+ require_relative 'secp256k1/key'
7
+ require_relative 'secp256k1/ecdh'
6
8
  require_relative 'secp256k1/recovery'
7
9
  require_relative 'secp256k1/ellswift'
8
10
  require_relative 'secp256k1/schnorrsig'
9
11
  require_relative 'secp256k1/musig'
12
+ require_relative 'secp256k1/silentpayments'
10
13
 
11
14
  # Binding for secp256k1 (https://github.com/bitcoin-core/secp256k1/)
12
15
  # @example
@@ -28,10 +31,13 @@ module Secp256k1
28
31
  class Error < StandardError; end
29
32
 
30
33
  include C
34
+ include Key
35
+ include ECDH
31
36
  include Recover
32
37
  include SchnorrSig
33
38
  include EllSwift
34
39
  include MuSig
40
+ include SilentPayments
35
41
 
36
42
  FLAGS_TYPE_MASK = ((1 << 8) - 1)
37
43
  FLAGS_TYPE_CONTEXT = (1 << 0)
@@ -84,8 +90,8 @@ module Secp256k1
84
90
  private_key = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, SecureRandom.random_bytes(32))
85
91
  ret = secp256k1_ec_seckey_verify(context, private_key)
86
92
  end
87
- private_key = private_key.read_string(32).unpack1('H*')
88
- [private_key , generate_pubkey_in_context(context, private_key, compressed: compressed) ]
93
+ private_key = private_key.read_string(32)
94
+ [private_key.unpack1('H*'), generate_pubkey_in_context(context, private_key, compressed: compressed)]
89
95
  end
90
96
  end
91
97
 
@@ -223,6 +229,64 @@ module Secp256k1
223
229
  end
224
230
  end
225
231
 
232
+ # Convert a DER-encoded ECDSA signature into compact(64 bytes) format.
233
+ # @param [String] signature DER-encoded signature with binary format.
234
+ # @return [String] compact signature(64 bytes) with binary format.
235
+ # @raise [Secp256k1::Error] If the signature could not be parsed.
236
+ # @raise [ArgumentError] If invalid arguments specified.
237
+ def ecdsa_signature_to_compact(signature)
238
+ raise ArgumentError, "signature must be String." unless signature.is_a?(String)
239
+ signature = hex2bin(signature)
240
+ with_context do |context|
241
+ sig_ptr = FFI::MemoryPointer.new(:uchar, signature.bytesize).put_bytes(0, signature)
242
+ internal_signature = FFI::MemoryPointer.new(:uchar, 64)
243
+ raise Error, 'secp256k1_ecdsa_signature_parse_der failed.' unless secp256k1_ecdsa_signature_parse_der(context, internal_signature, sig_ptr, signature.bytesize) == 1
244
+ output = FFI::MemoryPointer.new(:uchar, 64)
245
+ secp256k1_ecdsa_signature_serialize_compact(context, output, internal_signature)
246
+ output.read_string(64)
247
+ end
248
+ end
249
+
250
+ # Convert a compact(64 bytes) ECDSA signature into DER-encoded format.
251
+ # @param [String] signature compact signature(64 bytes) with binary format.
252
+ # @return [String] DER-encoded signature with binary format.
253
+ # @raise [Secp256k1::Error] If the signature could not be parsed.
254
+ # @raise [ArgumentError] If invalid arguments specified.
255
+ def ecdsa_signature_from_compact(signature)
256
+ validate_string!("signature", signature, 64)
257
+ signature = hex2bin(signature)
258
+ with_context do |context|
259
+ sig_ptr = FFI::MemoryPointer.new(:uchar, 64).put_bytes(0, signature)
260
+ internal_signature = FFI::MemoryPointer.new(:uchar, 64)
261
+ raise Error, 'secp256k1_ecdsa_signature_parse_compact failed.' unless secp256k1_ecdsa_signature_parse_compact(context, internal_signature, sig_ptr) == 1
262
+ output = FFI::MemoryPointer.new(:uchar, 72)
263
+ output_len = FFI::MemoryPointer.new(:uint64).put_uint64(0, 72)
264
+ raise Error, 'secp256k1_ecdsa_signature_serialize_der failed.' unless secp256k1_ecdsa_signature_serialize_der(context, output, output_len, internal_signature) == 1
265
+ output.read_string(output_len.read_uint64)
266
+ end
267
+ end
268
+
269
+ # Compute a tagged hash as defined in BIP-340: SHA256(SHA256(tag) || SHA256(tag) || msg).
270
+ # @param [String] tag the tag with binary format.
271
+ # @param [String] msg the message with binary format.
272
+ # @return [String] 32-byte tagged hash with hex format.
273
+ # @raise [Secp256k1::Error] If the hash could not be computed.
274
+ # @raise [ArgumentError] If invalid arguments specified.
275
+ def tagged_sha256(tag, msg)
276
+ raise ArgumentError, "tag must be String." unless tag.is_a?(String)
277
+ raise ArgumentError, "msg must be String." unless msg.is_a?(String)
278
+ tag = hex2bin(tag)
279
+ msg = hex2bin(msg)
280
+ with_context do |context|
281
+ hash32 = FFI::MemoryPointer.new(:uchar, 32)
282
+ # The library requires non-NULL pointers even for zero-length input, so allocate at least 1 byte.
283
+ tag_ptr = FFI::MemoryPointer.new(:uchar, [tag.bytesize, 1].max).put_bytes(0, tag)
284
+ msg_ptr = FFI::MemoryPointer.new(:uchar, [msg.bytesize, 1].max).put_bytes(0, msg)
285
+ raise Error, 'secp256k1_tagged_sha256 failed.' unless secp256k1_tagged_sha256(context, hash32, tag_ptr, tag.bytesize, msg_ptr, msg.bytesize) == 1
286
+ hash32.read_string(32).unpack1('H*')
287
+ end
288
+ end
289
+
226
290
  private
227
291
 
228
292
  # Calculate full public key(64 bytes) from public key(32 bytes).
@@ -239,9 +303,34 @@ module Secp256k1
239
303
  end
240
304
  end
241
305
 
306
+ # Parse a serialized public key into an internal 64-byte pubkey pointer.
307
+ # @param [FFI::Pointer] context secp256k1 context.
308
+ # @param [String] pubkey serialized public key with binary format.
309
+ # @return [FFI::Pointer] internal pubkey pointer(64 bytes).
310
+ # @raise [Secp256k1::Error] If the public key is invalid.
311
+ def parse_pubkey_internal(context, pubkey)
312
+ pubkey_ptr = FFI::MemoryPointer.new(:uchar, pubkey.bytesize).put_bytes(0, pubkey)
313
+ internal_pubkey = FFI::MemoryPointer.new(:uchar, 64)
314
+ raise Error, 'An invalid public key was specified.' unless secp256k1_ec_pubkey_parse(context, internal_pubkey, pubkey_ptr, pubkey.bytesize) == 1
315
+ internal_pubkey
316
+ end
317
+
318
+ # Serialize an internal x-only public key pointer(64 bytes) to 32-byte hex.
319
+ # @param [FFI::Pointer] context secp256k1 context.
320
+ # @param [FFI::Pointer] xonly_input internal x-only pubkey pointer(64 bytes).
321
+ # @return [String] x-only public key with hex format(32 bytes).
322
+ def serialize_xonly_pubkey_internal(context, xonly_input)
323
+ output = FFI::MemoryPointer.new(:uchar, X_ONLY_PUBKEY_SIZE)
324
+ secp256k1_xonly_pubkey_serialize(context, output, xonly_input)
325
+ output.read_string(X_ONLY_PUBKEY_SIZE).unpack1('H*')
326
+ end
327
+
328
+ # Generate public key from +private_key+ using given +context+.
329
+ # +private_key+ must be a 32-byte binary string, not hex, to avoid a second
330
+ # hex conversion corrupting binary keys that happen to look like hex strings.
242
331
  def generate_pubkey_in_context(context, private_key, compressed: true)
243
332
  internal_pubkey = FFI::MemoryPointer.new(:uchar, 64)
244
- priv_ptr = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(private_key))
333
+ priv_ptr = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, private_key)
245
334
  result = secp256k1_ec_pubkey_create(context, internal_pubkey, priv_ptr)
246
335
  raise 'error creating pubkey' unless result
247
336
  serialize_pubkey_internal(context, internal_pubkey, compressed)