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
-
#derive_payment_points(prevouts, private_keys, recipients) ⇒ Array<ECDSA::Point>
Derive payment point.
-
#extract_public_key(prevout, input) ⇒ Object
Extract public keys from
prevoutand input. -
#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.
-
#sp_outpoint_smallest ⇒ String
The lexicographically smallest outpoint of this tx's inputs, which the input hash of BIP-352 commits to.
Instance Method Details
#derive_payment_points(prevouts, private_keys, recipients) ⇒ Array<ECDSA::Point>
Derive payment point.
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.
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_smallest ⇒ String
The lexicographically smallest outpoint of this tx's inputs, which the input hash of BIP-352 commits to.
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 |