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.
- checksums.yaml +4 -4
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +32 -0
- data/Gemfile +1 -0
- data/README.md +16 -1
- data/lib/secp256k1/c.rb +88 -2
- data/lib/secp256k1/ecdh.rb +37 -0
- data/lib/secp256k1/ellswift.rb +20 -0
- data/lib/secp256k1/key.rb +342 -0
- data/lib/secp256k1/musig.rb +38 -0
- data/lib/secp256k1/recovery.rb +23 -0
- data/lib/secp256k1/schnorrsig.rb +57 -3
- data/lib/secp256k1/silentpayments.rb +319 -0
- data/lib/secp256k1/version.rb +1 -1
- data/lib/secp256k1.rb +92 -3
- metadata +8 -6
data/lib/secp256k1/musig.rb
CHANGED
|
@@ -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.
|
data/lib/secp256k1/recovery.rb
CHANGED
|
@@ -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
|
data/lib/secp256k1/schnorrsig.rb
CHANGED
|
@@ -36,7 +36,44 @@ module Secp256k1
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
#
|
|
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
|
-
|
|
58
|
-
result = secp256k1_schnorrsig_verify(context, signature,
|
|
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
|
data/lib/secp256k1/version.rb
CHANGED
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 =
|
|
88
|
-
[private_key
|
|
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,
|
|
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)
|