Module: Bitcoin::SilentPayment

Included in:
Tx
Defined in:
lib/bitcoin/silent_payment.rb,
lib/bitcoin/silent_payment/output.rb

Overview

BIP-352 silent payment module.

Defined Under Namespace

Classes: Output

Constant Summary collapse

K_MAX =

Maximum number of silent payment addresses that can share the same scan public key within a single transaction. This is the maximum number of P2TR outputs that can fit within a 100KB transaction under current standardness rules.

2323

Instance Method Summary collapse

Instance Method Details

#derive_payment_points(prevouts, private_keys, recipients) ⇒ Array<ECDSA::Point>

Derive payment point.

Parameters:

  • prevouts (Array<Bitcoin::Script>)

    An array of previous output script.

  • private_keys (Array<Bitcoin::Key>)

    An array of Bitcoin::Key objects corresponding to each public key in prevouts.

  • recipients (Array<Bech32::SilentPaymentAddr>)

Returns:

  • (Array<ECDSA::Point>)

    An array of derived points, one per recipient in the same order.

Raises:

  • (ArgumentError)


19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/bitcoin/silent_payment.rb', line 19

def derive_payment_points(prevouts, private_keys, recipients)
  raise ArgumentError, "prevouts must be Array." unless prevouts.is_a? Array
  raise ArgumentError, "private_keys must be Array." unless private_keys.is_a? Array
  raise ArgumentError, "prevouts and private_keys must be the same length." unless prevouts.length == private_keys.length
  raise ArgumentError, "recipients must be Array." unless recipients.is_a? Array

  field = ECDSA::PrimeField.new(Bitcoin::Secp256k1::GROUP.order)
  plain_seckeys = []
  taproot_seckeys = []
  sum_priv_keys = 0
  prevouts.each_with_index do |prevout, index|
    key = private_keys[index]
    raise ArgumentError, "private_keys element must be Bitcoin::Key." unless key.is_a? Bitcoin::Key
    public_key = extract_public_key(prevout, inputs[index])
    next if public_key.nil?
    priv_key_int = key.priv_key.to_i(16)
    if public_key.p2tr?
      taproot_seckeys << key.priv_key
      priv_key_int = field.mod(-priv_key_int) unless key.to_point.has_even_y?
    else
      plain_seckeys << key.priv_key
    end
    sum_priv_keys = field.mod(sum_priv_keys + priv_key_int)
  end
  return [] if plain_seckeys.empty? && taproot_seckeys.empty?
  # The input private keys sum to zero, so the aggregate public key is the point at infinity
  # and no shared secret exists.
  return [] if sum_priv_keys.zero?

  destinations = recipients.map do |sp_addr|
    raise ArgumentError, "recipients element must be Bech32::SilentPaymentAddr." unless sp_addr.is_a? Bech32::SilentPaymentAddr
    [sp_addr.scan_key, sp_addr.spend_key]
  end
  destinations.group_by(&:first).each_value do |group|
    raise ArgumentError, "Recipient group exceeds K_max limit (#{K_MAX})." if group.length > K_MAX
  end

  Bitcoin.secp_impl.sp_create_outputs(
    destinations, sp_outpoint_smallest,
    plain_seckeys: plain_seckeys, taproot_seckeys: taproot_seckeys
  ).map { |xonly| Bitcoin::Key.from_xonly_pubkey(xonly).to_point }
end

#extract_public_key(prevout, input) ⇒ Object

Extract public keys from prevout and input.



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/bitcoin/silent_payment.rb', line 126

def extract_public_key(prevout, input)
  if prevout.p2pkh?
    spk_hash = prevout.chunks[2].pushed_data.bth
    input.script_sig.chunks.reverse.each do |chunk|
      next unless chunk.pushdata?
      pubkey = chunk.pushed_data.bth
      if Bitcoin.hash160(pubkey) == spk_hash
        return Bitcoin::Key.new(pubkey: pubkey) if pubkey.htb.bytesize == Bitcoin::Key::COMPRESSED_PUBLIC_KEY_SIZE
      end
    end
  elsif prevout.p2sh?
    redeem_script = Bitcoin::Script.parse_from_payload(input.script_sig.chunks.last.pushed_data)
    if redeem_script.p2wpkh?
      pk = input.script_witness.stack.last
      return Bitcoin::Key.new(pubkey: pk.bth) if pk.bytesize == Bitcoin::Key::COMPRESSED_PUBLIC_KEY_SIZE
    end
  elsif prevout.p2wpkh?
    pk = input.script_witness.stack.last
    return Bitcoin::Key.new(pubkey: pk.bth) if pk.bytesize == Bitcoin::Key::COMPRESSED_PUBLIC_KEY_SIZE
  elsif prevout.p2tr?
    witness_stack = input.script_witness.stack.dup
    witness_stack.pop if witness_stack.last.bth.start_with?("50")
    if witness_stack.length > 1
      # script-path
      cb = Bitcoin::Taproot::ControlBlock.parse_from_payload(witness_stack.last)
      return nil if cb.internal_key == Bitcoin::Taproot::NUMS_H
    end
    pubkey = Bitcoin::Key.from_xonly_pubkey(prevout.chunks[1].pushed_data.bth)
    return pubkey if pubkey.compressed?
  end
  nil
end

#scan_sp_outputs(prevouts, scan_private_key, spend_pubkey, labels = []) ⇒ Array<Bitcoin::SilentPayment::Output>

Scan transaction outputs for silent payment outputs belonging to the receiver.

Parameters:

  • prevouts (Array<Bitcoin::Script>)

    An array of previous output scripts corresponding to each input.

  • scan_private_key (Bitcoin::Key)

    The receiver's scan private key (b_scan).

  • spend_pubkey (Bitcoin::Key)

    The receiver's spend key. Pass a Key initialized with spend_priv_key to derive the public key.

  • labels (Array<Integer>) (defaults to: [])

    An array of label integers for labeled addresses (default: []).

Returns:

Raises:

  • (ArgumentError)

    If any of the required parameters are invalid.



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
# File 'lib/bitcoin/silent_payment.rb', line 71

def scan_sp_outputs(prevouts, scan_private_key, spend_pubkey, labels = [])
  raise ArgumentError, "prevouts must be Array." unless prevouts.is_a? Array
  raise ArgumentError, "scan_private_key must be Bitcoin::Key." unless scan_private_key.is_a? Bitcoin::Key
  raise ArgumentError, "spend_pubkey must be Bitcoin::Key." unless spend_pubkey.is_a? Bitcoin::Key

  taproot_outputs = outputs.select{|o| o.script_pubkey.p2tr? }
  return [] if taproot_outputs.empty?

  plain_pubkeys = []
  xonly_pubkeys = []
  sum_pub_keys = Bitcoin::Secp256k1::GROUP.infinity.to_jacobian
  maximum_witness_version = Bitcoin::Opcodes.opcode_to_small_int(Bitcoin::Opcodes::OP_1)
  prevouts.each.with_index do |prevout, index|
    return [] if prevout.witness_program? && prevout.witness_data.first > maximum_witness_version

    public_key = extract_public_key(prevout, inputs[index])
    next if public_key.nil?
    if public_key.p2tr?
      xonly_pubkeys << public_key.xonly_pubkey
    else
      plain_pubkeys << public_key.pubkey
    end
    sum_pub_keys += public_key.to_point.to_jacobian
  end
  return [] if plain_pubkeys.empty? && xonly_pubkeys.empty?
  # Not a silent payment transaction, so there is nothing to find. Checked here rather than
  # left to the implementation, which reports it as an error.
  return [] if sum_pub_keys.infinity?

  impl = Bitcoin.secp_impl
  label_values = {}
  label_tweaks = {}
  labels.each do |m|
    label, tweak = impl.sp_create_label(scan_private_key.priv_key, m)
    label_values[label] = m
    label_tweaks[label] = tweak
  end

  tx_outputs = taproot_outputs.map{|o| o.script_pubkey.witness_data[1].bth }
  impl.sp_scan_outputs(tx_outputs, scan_private_key.priv_key, sp_outpoint_smallest, spend_pubkey.pubkey,
                       plain_pubkeys: plain_pubkeys, xonly_pubkeys: xonly_pubkeys,
                       labels: label_tweaks).map do |found|
    tx_out = taproot_outputs[tx_outputs.index(found[:output])]
    SilentPayment::Output.new(tx_out, found[:tweak].htb, found[:label] && label_values[found[:label]])
  end
end

#sp_outpoint_smallestString

The lexicographically smallest outpoint of this tx's inputs, which the input hash of BIP-352 commits to.

Returns:

  • (String)

    The outpoint with hex format(36 bytes).



121
122
123
# File 'lib/bitcoin/silent_payment.rb', line 121

def sp_outpoint_smallest
  inputs.map{|i| i.out_point.to_hex }.min
end