Module: Secp256k1::SilentPayments

Included in:
Secp256k1
Defined in:
lib/secp256k1/silentpayments.rb

Overview

Silent Payments module (BIP-352). This module provides elliptic-curve operations required for sending and receiving Silent Payments. Note that this is not a full implementation of BIP-352; higher-level concepts like addresses, output script types or transactions must be handled by the caller.

Examples:

include Secp256k1

# Sender side:
outputs = silentpayments_sender_create_outputs(
  [[scan_pubkey, spend_pubkey]], outpoint_smallest, plain_seckeys: [seckey])

# Recipient side:
prevouts_summary = silentpayments_create_prevouts_summary(
  outpoint_smallest, plain_pubkeys: [pubkey])
found = silentpayments_scan_outputs(
  outputs, scan_key, prevouts_summary, spend_pubkey)

Defined Under Namespace

Classes: FoundOutput, Recipient

Constant Summary collapse

SP_LABEL_SIZE =

Size of a serialized Silent Payments label.

33
SP_LABEL_INTERNAL_SIZE =

Size of an internal Silent Payments label object.

68
SP_PREVOUTS_SUMMARY_SIZE =

Size of a Silent Payments prevouts summary object.

101
SP_RECIPIENT_GROUP_LIMIT =

Maximum number of recipients sharing the same scan public key (BIP-352).

2323

Instance Method Summary collapse

Instance Method Details

#silentpayments_available?Boolean

Whether the loaded libsecp256k1 supports the silentpayments module. The module is available in libsecp256k1 v0.8.0 or later.

Returns:

  • (Boolean)


49
50
51
# File 'lib/secp256k1/silentpayments.rb', line 49

def silentpayments_available?
  C.silentpayments_available?
end

#silentpayments_create_label(scan_key, m) ⇒ Array(String)

Create Silent Payments label tweak and label(recipient side).

Parameters:

  • scan_key (String)

    The recipient's scan key with hex format(32 bytes).

  • m (Integer)

    The integer for the m-th label (0 is used for change outputs).

Returns:

  • (Array(String))

    An array of serialized label(33 bytes) and label tweak(32 bytes) with hex format.

Raises:

  • (Secp256k1::Error)

    If creation failed.

  • (ArgumentError)

    If invalid arguments specified.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/secp256k1/silentpayments.rb', line 133

def silentpayments_create_label(scan_key, m)
  check_silentpayments_support!
  validate_string!("scan_key", scan_key, 32)
  raise ArgumentError, "m must be Integer." unless m.is_a?(Integer)
  raise ArgumentError, "m must be between 0 and 2**32-1." if m < 0 || m > 0xffffffff

  with_context do |context|
    label = FFI::MemoryPointer.new(:uchar, SP_LABEL_INTERNAL_SIZE)
    label_tweak = FFI::MemoryPointer.new(:uchar, 32)
    scan_key_ptr = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(scan_key))
    result = secp256k1_silentpayments_recipient_label_create(context, label, label_tweak, scan_key_ptr, m)
    raise Error, "secp256k1_silentpayments_recipient_label_create failed." unless result == 1
    out33 = FFI::MemoryPointer.new(:uchar, SP_LABEL_SIZE)
    secp256k1_silentpayments_recipient_label_serialize(context, out33, label)
    [out33.read_string(SP_LABEL_SIZE).unpack1('H*'), label_tweak.read_string(32).unpack1('H*')]
  end
end

#silentpayments_create_labeled_spend_pubkey(spend_pubkey, label) ⇒ String

Create Silent Payments labeled spend public key(recipient side).

Parameters:

  • spend_pubkey (String)

    The recipient's unlabeled spend public key with hex format(33 bytes).

  • label (String)

    The serialized label with hex format(33 bytes).

Returns:

  • (String)

    The labeled spend public key with hex format(33 bytes).

Raises:

  • (Secp256k1::Error)

    If creation failed.

  • (ArgumentError)

    If invalid arguments specified.



157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/secp256k1/silentpayments.rb', line 157

def silentpayments_create_labeled_spend_pubkey(spend_pubkey, label)
  check_silentpayments_support!
  validate_string!("spend_pubkey", spend_pubkey, 33)
  validate_string!("label", label, SP_LABEL_SIZE)

  with_context do |context|
    spend_pubkey_ptr = parse_pubkey_internal(context, hex2bin(spend_pubkey))
    label_ptr = parse_label_internal(context, hex2bin(label))
    labeled = FFI::MemoryPointer.new(:uchar, 64)
    result = secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(context, labeled, spend_pubkey_ptr, label_ptr)
    raise Error, "secp256k1_silentpayments_recipient_create_labeled_spend_pubkey failed." unless result == 1
    serialize_pubkey_internal(context, labeled, true)
  end
end

#silentpayments_create_prevouts_summary(outpoint_smallest, plain_pubkeys: [], xonly_pubkeys: []) ⇒ String

Compute Silent Payments prevouts summary from prevout public keys and transaction inputs(recipient side).

Parameters:

  • outpoint_smallest (String)

    The smallest outpoint lexicographically from the transaction inputs(36 bytes).

  • plain_pubkeys (Array) (defaults to: [])

    (Optional) An array of public keys of non-taproot inputs with hex format(33 bytes each).

  • xonly_pubkeys (Array) (defaults to: [])

    (Optional) An array of x-only public keys of taproot inputs with hex format(32 bytes each).

Returns:

  • (String)

    The prevouts summary data with hex format(101 bytes). Note that this is an opaque representation which is not guaranteed to be portable between different platforms or library versions.

Raises:

  • (Secp256k1::Error)

    If the transaction is not a Silent Payments transaction.

  • (ArgumentError)

    If invalid arguments specified.



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/secp256k1/silentpayments.rb', line 181

def silentpayments_create_prevouts_summary(outpoint_smallest, plain_pubkeys: [], xonly_pubkeys: [])
  check_silentpayments_support!
  plain_pubkeys ||= []
  xonly_pubkeys ||= []
  raise ArgumentError, "plain_pubkeys must be an Array." unless plain_pubkeys.is_a?(Array)
  raise ArgumentError, "xonly_pubkeys must be an Array." unless xonly_pubkeys.is_a?(Array)
  if plain_pubkeys.empty? && xonly_pubkeys.empty?
    raise ArgumentError, "At least one public key(plain_pubkeys or xonly_pubkeys) is required."
  end
  validate_string!("outpoint_smallest", outpoint_smallest, 36)

  with_context do |context|
    xonly_ptrs = xonly_pubkeys.map do |pubkey|
      validate_string!("xonly_pubkey", pubkey, 32)
      parse_xonly_pubkey_internal(context, hex2bin(pubkey))
    end
    plain_ptrs = plain_pubkeys.map do |pubkey|
      validate_string!("plain_pubkey", pubkey, 33)
      parse_pubkey_internal(context, hex2bin(pubkey))
    end
    xonly_arr = if xonly_ptrs.empty?
                  nil
                else
                  FFI::MemoryPointer.new(:pointer, xonly_ptrs.length).write_array_of_pointer(xonly_ptrs)
                end
    plain_arr = if plain_ptrs.empty?
                  nil
                else
                  FFI::MemoryPointer.new(:pointer, plain_ptrs.length).write_array_of_pointer(plain_ptrs)
                end
    outpoint_ptr = FFI::MemoryPointer.new(:uchar, 36).put_bytes(0, hex2bin(outpoint_smallest))
    prevouts_summary = FFI::MemoryPointer.new(:uchar, SP_PREVOUTS_SUMMARY_SIZE)
    result = secp256k1_silentpayments_recipient_prevouts_summary_create(
      context, prevouts_summary, outpoint_ptr, xonly_arr, xonly_ptrs.length, plain_arr, plain_ptrs.length)
    raise Error, "secp256k1_silentpayments_recipient_prevouts_summary_create failed." unless result == 1
    prevouts_summary.read_string(SP_PREVOUTS_SUMMARY_SIZE).unpack1('H*')
  end
end

#silentpayments_scan_outputs(tx_outputs, scan_key, prevouts_summary, spend_pubkey, labels: nil) ⇒ Array

Scan for Silent Payments transaction outputs(recipient side).

Parameters:

  • tx_outputs (Array)

    An array of the transaction's x-only public key outputs with hex format(32 bytes each), in their original transaction (vout) order.

  • scan_key (String)

    The recipient's scan key with hex format(32 bytes).

  • prevouts_summary (String)

    The prevouts summary data created by #silentpayments_create_prevouts_summary with hex format(101 bytes).

  • spend_pubkey (String)

    The recipient's unlabeled spend public key with hex format(33 bytes).

  • labels (Hash) (defaults to: nil)

    (Optional) The recipient's label cache. A hash whose key is a serialized label(33 bytes) and whose value is the corresponding label tweak(32 bytes), both with hex format. Can be created with #silentpayments_create_label.

Returns:

  • (Array)

    An array of found outputs. Each element is a hash with the following keys:

    • :output x-only public key of the found output with hex format(32 bytes).
    • :tweak The 32-byte tweak needed to spend the output with hex format.
    • :label The serialized label(33 bytes with hex format) if the output was sent to a labeled address, otherwise nil.

Raises:

  • (Secp256k1::Error)

    If the transaction is not a Silent Payments transaction or arguments are invalid.

  • (ArgumentError)

    If invalid arguments specified.



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/secp256k1/silentpayments.rb', line 236

def silentpayments_scan_outputs(tx_outputs, scan_key, prevouts_summary, spend_pubkey, labels: nil)
  check_silentpayments_support!
  raise ArgumentError, "tx_outputs must be an Array." unless tx_outputs.is_a?(Array)
  raise ArgumentError, "tx_outputs must not be empty." if tx_outputs.empty?
  validate_string!("scan_key", scan_key, 32)
  validate_string!("prevouts_summary", prevouts_summary, SP_PREVOUTS_SUMMARY_SIZE)
  validate_string!("spend_pubkey", spend_pubkey, 33)
  raise ArgumentError, "labels must be a Hash." if labels && !labels.is_a?(Hash)

  with_context do |context|
    output_ptrs = tx_outputs.map do |output|
      validate_string!("tx_output", output, 32)
      parse_xonly_pubkey_internal(context, hex2bin(output))
    end
    tx_outputs_ptr = FFI::MemoryPointer.new(:pointer, output_ptrs.length).write_array_of_pointer(output_ptrs)

    found_outputs = Array.new(tx_outputs.length) { FoundOutput.new }
    found_outputs_ptr = FFI::MemoryPointer.new(:pointer, found_outputs.length)
    found_outputs_ptr.write_array_of_pointer(found_outputs.map(&:pointer))
    n_found = FFI::MemoryPointer.new(:uint32)

    scan_key_ptr = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(scan_key))
    prevouts_summary_ptr = FFI::MemoryPointer.new(:uchar, SP_PREVOUTS_SUMMARY_SIZE).
      put_bytes(0, hex2bin(prevouts_summary))
    spend_pubkey_ptr = parse_pubkey_internal(context, hex2bin(spend_pubkey))

    label_lookup = nil
    if labels
      # Keep tweak pointers referenced in this hash so they remain valid while scanning.
      tweak_cache = {}
      labels.each do |label, tweak|
        validate_string!("label", label, SP_LABEL_SIZE)
        validate_string!("label_tweak", tweak, 32)
        tweak_cache[hex2bin(label)] = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(tweak))
      end
      label_lookup = FFI::Function.new(:pointer, [:pointer, :pointer]) do |label33, _|
        tweak_cache[label33.read_string(SP_LABEL_SIZE)] || FFI::Pointer::NULL
      end
    end

    result = secp256k1_silentpayments_recipient_scan_outputs(
      context, found_outputs_ptr, n_found, tx_outputs_ptr, tx_outputs.length,
      scan_key_ptr, prevouts_summary_ptr, spend_pubkey_ptr, label_lookup, nil)
    raise Error, "secp256k1_silentpayments_recipient_scan_outputs failed." unless result == 1

    Array.new(n_found.read_uint32) do |i|
      found = found_outputs[i]
      label = if found[:found_with_label] != 0
                out33 = FFI::MemoryPointer.new(:uchar, SP_LABEL_SIZE)
                secp256k1_silentpayments_recipient_label_serialize(
                  context, out33, found.pointer + FoundOutput.offset_of(:label))
                out33.read_string(SP_LABEL_SIZE).unpack1('H*')
              end
      {
        output: serialize_xonly_pubkey_internal(context, found.pointer),
        tweak: found.pointer.get_bytes(FoundOutput.offset_of(:tweak), 32).unpack1('H*'),
        label: label
      }
    end
  end
end

#silentpayments_sender_create_outputs(recipients, outpoint_smallest, plain_seckeys: [], taproot_seckeys: []) ⇒ Array

Create Silent Payments outputs for recipient(s) (sender side).

Parameters:

  • recipients (Array)

    An array of recipient's key pair. Each element is an array of scan public key and (labeled) spend public key with hex format(33 bytes each).

  • outpoint_smallest (String)

    The smallest outpoint lexicographically from the transaction inputs(36 bytes).

  • plain_seckeys (Array) (defaults to: [])

    (Optional) An array of private keys of non-taproot inputs with hex format(32 bytes each).

  • taproot_seckeys (Array) (defaults to: [])

    (Optional) An array of private keys of taproot inputs with hex format(32 bytes each).

Returns:

  • (Array)

    An array of x-only public keys(hex format) for the taproot outputs, ordered same as recipients.

Raises:

  • (Secp256k1::Error)

    If creation failed.

  • (ArgumentError)

    If invalid arguments specified.



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/secp256k1/silentpayments.rb', line 62

def silentpayments_sender_create_outputs(recipients, outpoint_smallest, plain_seckeys: [], taproot_seckeys: [])
  check_silentpayments_support!
  raise ArgumentError, "recipients must be an Array." unless recipients.is_a?(Array)
  raise ArgumentError, "recipients must not be empty." if recipients.empty?
  plain_seckeys ||= []
  taproot_seckeys ||= []
  raise ArgumentError, "plain_seckeys must be an Array." unless plain_seckeys.is_a?(Array)
  raise ArgumentError, "taproot_seckeys must be an Array." unless taproot_seckeys.is_a?(Array)
  if plain_seckeys.empty? && taproot_seckeys.empty?
    raise ArgumentError, "At least one private key(plain_seckeys or taproot_seckeys) is required."
  end
  validate_string!("outpoint_smallest", outpoint_smallest, 36)

  with_context do |context|
    recipient_structs = recipients.each_with_index.map do |recipient, i|
      scan_pubkey, spend_pubkey = recipient
      validate_string!("scan_pubkey", scan_pubkey, 33)
      validate_string!("spend_pubkey", spend_pubkey, 33)
      st = Recipient.new
      st.pointer.put_bytes(0, parse_pubkey_internal(context, hex2bin(scan_pubkey)).read_string(64))
      st.pointer.put_bytes(64, parse_pubkey_internal(context, hex2bin(spend_pubkey)).read_string(64))
      st[:index] = i
      st
    end
    recipients_ptr = FFI::MemoryPointer.new(:pointer, recipient_structs.length)
    recipients_ptr.write_array_of_pointer(recipient_structs.map(&:pointer))

    generated_outputs = Array.new(recipients.length) { FFI::MemoryPointer.new(:uchar, 64) }
    generated_outputs_ptr = FFI::MemoryPointer.new(:pointer, generated_outputs.length)
    generated_outputs_ptr.write_array_of_pointer(generated_outputs)

    outpoint_ptr = FFI::MemoryPointer.new(:uchar, 36).put_bytes(0, hex2bin(outpoint_smallest))

    keypairs = taproot_seckeys.map do |sk|
      validate_string!("taproot_seckey", sk, 32)
      seckey = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(sk))
      keypair = FFI::MemoryPointer.new(:uchar, 96)
      raise Error, "taproot_seckey is invalid." unless secp256k1_keypair_create(context, keypair, seckey) == 1
      keypair
    end
    keypairs_ptr = if keypairs.empty?
                     nil
                   else
                     FFI::MemoryPointer.new(:pointer, keypairs.length).write_array_of_pointer(keypairs)
                   end

    seckeys = plain_seckeys.map do |sk|
      validate_string!("plain_seckey", sk, 32)
      FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(sk))
    end
    seckeys_ptr = if seckeys.empty?
                    nil
                  else
                    FFI::MemoryPointer.new(:pointer, seckeys.length).write_array_of_pointer(seckeys)
                  end

    result = secp256k1_silentpayments_sender_create_outputs(
      context, generated_outputs_ptr, recipients_ptr, recipients.length, outpoint_ptr,
      keypairs_ptr, keypairs.length, seckeys_ptr, seckeys.length)
    raise Error, "secp256k1_silentpayments_sender_create_outputs failed." unless result == 1

    generated_outputs.map { |output| serialize_xonly_pubkey_internal(context, output) }
  end
end