bitcoinrb 1.11.0 → 1.12.1
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/.github/workflows/ruby.yml +2 -2
- data/.ruby-version +1 -1
- data/bitcoinrb.gemspec +1 -6
- data/lib/bitcoin/bip324/fs_chacha20.rb +3 -5
- data/lib/bitcoin/bip324/fs_chacha_poly1305.rb +19 -17
- data/lib/bitcoin/bip324.rb +1 -1
- data/lib/bitcoin/constants.rb +1 -0
- data/lib/bitcoin/descriptor/expression.rb +2 -2
- data/lib/bitcoin/descriptor/sp.rb +219 -0
- data/lib/bitcoin/descriptor.rb +20 -0
- data/lib/bitcoin/ext_key.rb +3 -3
- data/lib/bitcoin/message/ping.rb +4 -2
- data/lib/bitcoin/message/send_tx_rcncl.rb +35 -0
- data/lib/bitcoin/message/version.rb +1 -1
- data/lib/bitcoin/message/wtxid_relay.rb +14 -0
- data/lib/bitcoin/message.rb +65 -58
- data/lib/bitcoin/message_sign.rb +98 -38
- data/lib/bitcoin/rpc.rb +0 -2
- data/lib/bitcoin/script/script_error.rb +5 -0
- data/lib/bitcoin/script/script_interpreter.rb +5 -3
- data/lib/bitcoin/silent_payment/output.rb +46 -0
- data/lib/bitcoin/silent_payment.rb +113 -12
- data/lib/bitcoin/util.rb +2 -2
- data/lib/bitcoin/version.rb +1 -1
- data/lib/bitcoin.rb +0 -6
- metadata +21 -109
- data/exe/bitcoinrb-cli +0 -5
- data/exe/bitcoinrbd +0 -46
- data/lib/bitcoin/network/connection.rb +0 -73
- data/lib/bitcoin/network/message_handler.rb +0 -243
- data/lib/bitcoin/network/peer.rb +0 -228
- data/lib/bitcoin/network/peer_discovery.rb +0 -42
- data/lib/bitcoin/network/pool.rb +0 -135
- data/lib/bitcoin/network.rb +0 -13
- data/lib/bitcoin/node/cli.rb +0 -122
- data/lib/bitcoin/node/configuration.rb +0 -40
- data/lib/bitcoin/node/spv.rb +0 -87
- data/lib/bitcoin/node.rb +0 -7
- data/lib/bitcoin/rpc/http_server.rb +0 -65
- data/lib/bitcoin/rpc/request_handler.rb +0 -150
- data/lib/bitcoin/store/chain_entry.rb +0 -68
- data/lib/bitcoin/store/db/level_db.rb +0 -98
- data/lib/bitcoin/store/db.rb +0 -9
- data/lib/bitcoin/store/spv_chain.rb +0 -101
- data/lib/bitcoin/store/utxo_db.rb +0 -226
- data/lib/bitcoin/store.rb +0 -12
data/lib/bitcoin/message_sign.rb
CHANGED
|
@@ -10,6 +10,22 @@ module Bitcoin
|
|
|
10
10
|
FORMAT_SIMPLE = :simple
|
|
11
11
|
FORMAT_FULL = :full
|
|
12
12
|
|
|
13
|
+
# Prefix
|
|
14
|
+
SIGNATURE_PREFIX_SIMPLE = 'smp'
|
|
15
|
+
SIGNATURE_PREFIX_FULL = 'ful'
|
|
16
|
+
SIGNATURE_PREFIX_POF = 'pof'
|
|
17
|
+
|
|
18
|
+
# BIP-322 Required rules
|
|
19
|
+
BIP322_VERIFY_FLAGS = [
|
|
20
|
+
SCRIPT_VERIFY_P2SH, SCRIPT_VERIFY_DERSIG, SCRIPT_VERIFY_STRICTENC,
|
|
21
|
+
SCRIPT_VERIFY_LOW_S, SCRIPT_VERIFY_NULLFAIL, SCRIPT_VERIFY_MINIMALDATA,
|
|
22
|
+
SCRIPT_VERIFY_CLEANSTACK, SCRIPT_VERIFY_MINIMALIF,
|
|
23
|
+
SCRIPT_VERIFY_CONST_SCRIPTCODE,
|
|
24
|
+
SCRIPT_VERIFY_WITNESS, SCRIPT_VERIFY_TAPROOT,
|
|
25
|
+
SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS,
|
|
26
|
+
SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM
|
|
27
|
+
].inject(:|)
|
|
28
|
+
|
|
13
29
|
# Sign a message.
|
|
14
30
|
# @param [Bitcoin::Key] key Private key to sign.
|
|
15
31
|
# @param [String] message The message to be signed.
|
|
@@ -20,28 +36,38 @@ module Bitcoin
|
|
|
20
36
|
def sign_message(key, message, prefix: Bitcoin.chain_params.message_magic, format: FORMAT_LEGACY, address: nil)
|
|
21
37
|
validate_format!(format)
|
|
22
38
|
digest = message_hash(message, prefix: prefix, legacy: format == FORMAT_LEGACY)
|
|
39
|
+
prefix_marker = ''
|
|
23
40
|
sig = case format
|
|
24
41
|
when FORMAT_LEGACY
|
|
25
42
|
key.sign_compact(digest)
|
|
26
43
|
else
|
|
27
44
|
validate_address!(address)
|
|
28
45
|
addr = Bitcoin::Script.parse_from_addr(address)
|
|
29
|
-
sig_ver, algo = if addr.p2wpkh?
|
|
46
|
+
sig_ver, algo = if addr.p2wpkh? || addr.p2wsh?
|
|
30
47
|
[:witness_v0, :ecdsa]
|
|
31
48
|
elsif addr.p2tr?
|
|
32
49
|
[:taproot, :schnorr]
|
|
33
50
|
else
|
|
34
|
-
|
|
51
|
+
|
|
35
52
|
end
|
|
36
53
|
tx = to_sign_tx(digest, address)
|
|
37
|
-
prev_out = Bitcoin::TxOut.new(script_pubkey: addr)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
prev_out = Bitcoin::TxOut.new(script_pubkey: addr, value: 0)
|
|
55
|
+
if addr.p2tr?
|
|
56
|
+
sighash = tx.sighash_for_input(0, addr, sig_version: :taproot, prevouts: [prev_out], hash_type: Bitcoin::SIGHASH_TYPE[:default])
|
|
57
|
+
tweaked = Bitcoin::Taproot.tweak_private_key(key, '')
|
|
58
|
+
tx.in[0].script_witness.stack << tweaked.sign(sighash, algo: :schnorr)
|
|
59
|
+
elsif addr.p2wpkh? || addr.p2wsh?
|
|
60
|
+
sighash = tx.sighash_for_input(0, addr, sig_version: :witness_v0, amount: 0, prevouts: [prev_out])
|
|
61
|
+
ecdsa = key.sign(sighash, algo: :ecdsa) + [Bitcoin::SIGHASH_TYPE[:all]].pack('C')
|
|
62
|
+
tx.in[0].script_witness.stack << ecdsa
|
|
63
|
+
tx.in[0].script_witness.stack << key.pubkey.htb
|
|
64
|
+
else
|
|
65
|
+
raise ArgumentError, "#{address} dose not supported."
|
|
66
|
+
end
|
|
67
|
+
prefix_marker = format == FORMAT_SIMPLE ? SIGNATURE_PREFIX_SIMPLE : SIGNATURE_PREFIX_FULL
|
|
42
68
|
format == FORMAT_SIMPLE ? tx.in[0].script_witness.to_payload : tx.to_payload
|
|
43
69
|
end
|
|
44
|
-
Base64.strict_encode64(sig)
|
|
70
|
+
prefix_marker + Base64.strict_encode64(sig)
|
|
45
71
|
end
|
|
46
72
|
|
|
47
73
|
# Verify a signed message.
|
|
@@ -51,41 +77,41 @@ module Bitcoin
|
|
|
51
77
|
# @return [Boolean] Verification result.
|
|
52
78
|
def verify_message(address, signature, message, prefix: Bitcoin.chain_params.message_magic)
|
|
53
79
|
addr_script = Bitcoin::Script.parse_from_addr(address)
|
|
80
|
+
variant, body = case signature
|
|
81
|
+
when ''
|
|
82
|
+
raise ArgumentError, 'signature too short'
|
|
83
|
+
when /\A#{SIGNATURE_PREFIX_SIMPLE}/
|
|
84
|
+
[:simple, signature[3..]]
|
|
85
|
+
when /\A#{SIGNATURE_PREFIX_FULL}/
|
|
86
|
+
[:full, signature[3..]]
|
|
87
|
+
when /\A#{SIGNATURE_PREFIX_POF}/
|
|
88
|
+
[:pof, signature[3..]]
|
|
89
|
+
else
|
|
90
|
+
[:fallback, signature]
|
|
91
|
+
end
|
|
54
92
|
begin
|
|
55
|
-
|
|
93
|
+
payload = Base64.strict_decode64(body)
|
|
56
94
|
rescue ArgumentError
|
|
57
95
|
raise ArgumentError, 'Invalid signature'
|
|
58
96
|
end
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
end
|
|
68
|
-
elsif addr_script.witness_program?
|
|
69
|
-
# BIP322 verification
|
|
70
|
-
digest = message_hash(message, prefix: prefix, legacy: false)
|
|
97
|
+
|
|
98
|
+
case variant
|
|
99
|
+
when :simple, :fallback
|
|
100
|
+
return verify_legacy(address, payload, message, prefix: prefix) if addr_script.p2pkh?
|
|
101
|
+
raise ArgumentError, 'simple format not supported for this address' unless
|
|
102
|
+
addr_script.p2wpkh? || addr_script.p2wsh? || addr_script.p2tr?
|
|
103
|
+
verify_simple(addr_script, payload, message, prefix: prefix)
|
|
104
|
+
when :full
|
|
71
105
|
begin
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
to_spend = to_spend_tx(digest, address)
|
|
76
|
-
return false unless tx.in[0].out_point.tx_hash == to_spend.tx_hash
|
|
77
|
-
rescue Exception
|
|
78
|
-
# Simple
|
|
79
|
-
tx = to_sign_tx(digest, address)
|
|
80
|
-
tx.in[0].script_witness = Bitcoin::ScriptWitness.parse_from_payload(sig)
|
|
106
|
+
tx = Bitcoin::Tx.parse_from_payload(payload)
|
|
107
|
+
rescue StandardError
|
|
108
|
+
raise ArgumentError, 'error parsing signature as full variant'
|
|
81
109
|
end
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
interpreter = Bitcoin::ScriptInterpreter.new(flags: flags, checker: Bitcoin::TxChecker.new(tx: tx, input_index: 0, prevouts: [tx_out]))
|
|
86
|
-
interpreter.verify_script(Bitcoin::Script.new, script_pubkey, tx.in[0].script_witness)
|
|
110
|
+
verify_full(addr_script, tx, message, prefix: prefix)
|
|
111
|
+
when :pof
|
|
112
|
+
raise NotImplementedError, 'proof of funds variant is not supported'
|
|
87
113
|
else
|
|
88
|
-
raise ArgumentError, "
|
|
114
|
+
raise ArgumentError, "unknown signature variant: #{variant.inspect}"
|
|
89
115
|
end
|
|
90
116
|
end
|
|
91
117
|
|
|
@@ -99,8 +125,7 @@ module Bitcoin
|
|
|
99
125
|
end
|
|
100
126
|
|
|
101
127
|
def validate_address!(address)
|
|
102
|
-
|
|
103
|
-
raise ArgumentError, 'This address unsupported' if script.p2sh? || script.p2wsh?
|
|
128
|
+
Bitcoin::Script.parse_from_addr(address)
|
|
104
129
|
end
|
|
105
130
|
|
|
106
131
|
def validate_format!(format)
|
|
@@ -110,6 +135,7 @@ module Bitcoin
|
|
|
110
135
|
end
|
|
111
136
|
|
|
112
137
|
def validate_to_sign_tx!(tx)
|
|
138
|
+
raise ArgumentError, "Invalid version." unless [0, 2].include?(tx.version)
|
|
113
139
|
raise ArgumentError, "Multiple inputs (proof of funds) are not supported." unless tx.in.length == 1
|
|
114
140
|
raise ArgumentError, "vin[0].prevout.n must be 0." unless tx.in[0].out_point.index == 0
|
|
115
141
|
raise ArgumentError, "Multiple outputs are not supported." unless tx.out.length == 1
|
|
@@ -140,8 +166,42 @@ module Bitcoin
|
|
|
140
166
|
tx
|
|
141
167
|
end
|
|
142
168
|
|
|
169
|
+
def verify_simple(addr_script, witness_payload, message, prefix:)
|
|
170
|
+
digest = message_hash(message, prefix: prefix, legacy: false)
|
|
171
|
+
tx = to_sign_tx(digest, addr_script.to_addr)
|
|
172
|
+
tx.in[0].script_witness = Bitcoin::ScriptWitness.parse_from_payload(witness_payload)
|
|
173
|
+
run_interpreter(tx, addr_script)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def verify_full(addr_script, tx, message, prefix:)
|
|
177
|
+
digest = message_hash(message, prefix: prefix, legacy: false)
|
|
178
|
+
to_spend = to_spend_tx(digest, addr_script.to_addr)
|
|
179
|
+
validate_to_sign_tx!(tx)
|
|
180
|
+
return false unless tx.in[0].out_point.tx_hash == to_spend.tx_hash
|
|
181
|
+
run_interpreter(tx, addr_script)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def run_interpreter(tx, script_pubkey)
|
|
185
|
+
tx_out = Bitcoin::TxOut.new(script_pubkey: script_pubkey)
|
|
186
|
+
checker = Bitcoin::TxChecker.new(tx: tx, input_index: 0, prevouts: [tx_out])
|
|
187
|
+
interpreter = Bitcoin::ScriptInterpreter.new(flags: BIP322_VERIFY_FLAGS, checker: checker)
|
|
188
|
+
interpreter.verify_script(tx.in[0].script_sig, script_pubkey, tx.in[0].script_witness)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def verify_legacy(address, sig_bytes, message, prefix:)
|
|
192
|
+
pubkey = Bitcoin::Key.recover_compact(message_hash(message, prefix: prefix, legacy: true), sig_bytes)
|
|
193
|
+
return false unless pubkey
|
|
194
|
+
pubkey.to_p2pkh == address
|
|
195
|
+
rescue StandardError
|
|
196
|
+
false
|
|
197
|
+
end
|
|
198
|
+
|
|
143
199
|
private_class_method :validate_address!
|
|
144
200
|
private_class_method :validate_format!
|
|
145
201
|
private_class_method :validate_to_sign_tx!
|
|
202
|
+
private_class_method :run_interpreter
|
|
203
|
+
private_class_method :verify_simple
|
|
204
|
+
private_class_method :verify_full
|
|
205
|
+
private_class_method :verify_legacy
|
|
146
206
|
end
|
|
147
207
|
end
|
data/lib/bitcoin/rpc.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
module Bitcoin
|
|
2
2
|
|
|
3
|
+
class ScriptNumError < StandardError
|
|
4
|
+
end
|
|
5
|
+
|
|
3
6
|
# bitcoin script error
|
|
4
7
|
class ScriptError < Exception
|
|
5
8
|
|
|
@@ -122,6 +125,8 @@ module Bitcoin
|
|
|
122
125
|
'Signature is found in scriptCode'
|
|
123
126
|
when SCRIPT_ERR_UNKNOWN_ERROR, SCRIPT_ERR_ERROR_COUNT
|
|
124
127
|
'unknown error'
|
|
128
|
+
when SCRIPT_ERR_SCRIPTNUM
|
|
129
|
+
'Script number overflowed or is non-minimally encoded'
|
|
125
130
|
else
|
|
126
131
|
extra_msg ? extra_msg : 'unknown error'
|
|
127
132
|
end
|
|
@@ -258,7 +258,7 @@ module Bitcoin
|
|
|
258
258
|
when OP_IF, OP_NOTIF
|
|
259
259
|
result = false
|
|
260
260
|
if need_exec
|
|
261
|
-
return set_error(
|
|
261
|
+
return set_error(SCRIPT_ERR_INVALID_STACK_OPERATION) if stack.size < 1
|
|
262
262
|
value = pop_string.htb
|
|
263
263
|
if sig_version == :witness_v0 && flag?(SCRIPT_VERIFY_MINIMALIF) || sig_version == :tapscript
|
|
264
264
|
# Under witness v0 rules it is only a policy rule, enabled through SCRIPT_VERIFY_MINIMALIF.
|
|
@@ -566,6 +566,8 @@ module Bitcoin
|
|
|
566
566
|
# max stack size check
|
|
567
567
|
return set_error(SCRIPT_ERR_STACK_SIZE) if stack.size + alt_stack.size > MAX_STACK_SIZE
|
|
568
568
|
end
|
|
569
|
+
rescue ScriptNumError => e
|
|
570
|
+
return set_error(SCRIPT_ERR_SCRIPTNUM, e.message)
|
|
569
571
|
rescue Exception => e
|
|
570
572
|
return set_error(SCRIPT_ERR_UNKNOWN_ERROR, e.message)
|
|
571
573
|
end
|
|
@@ -591,10 +593,10 @@ module Bitcoin
|
|
|
591
593
|
# cast item to int value.
|
|
592
594
|
def cast_to_int(s, max_num_size = DEFAULT_MAX_NUM_SIZE)
|
|
593
595
|
data = s.htb
|
|
594
|
-
raise '"script number overflow"' if data.bytesize > max_num_size
|
|
596
|
+
raise ScriptNumError, '"script number overflow"' if data.bytesize > max_num_size
|
|
595
597
|
if require_minimal && data.bytesize > 0
|
|
596
598
|
if data.bytes[-1] & 0x7f == 0 && (data.bytesize <= 1 || data.bytes[data.bytesize - 2] & 0x80 == 0)
|
|
597
|
-
raise 'non-minimally encoded script number'
|
|
599
|
+
raise ScriptNumError, 'non-minimally encoded script number'
|
|
598
600
|
end
|
|
599
601
|
end
|
|
600
602
|
Script.decode_number(s)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Bitcoin
|
|
2
|
+
module SilentPayment
|
|
3
|
+
# Represents a detected silent payment output.
|
|
4
|
+
class Output
|
|
5
|
+
# @return [Bitcoin::TxOut] The detected transaction output.
|
|
6
|
+
attr_reader :tx_out
|
|
7
|
+
|
|
8
|
+
# @return [String] The tweak value (t_k) used to derive the output key (32 bytes binary).
|
|
9
|
+
attr_reader :tweak
|
|
10
|
+
|
|
11
|
+
# @return [Integer, nil] The label used for this output, or nil if no label was used.
|
|
12
|
+
attr_reader :label
|
|
13
|
+
|
|
14
|
+
# @param [Bitcoin::TxOut] tx_out The detected transaction output.
|
|
15
|
+
# @param [String] tweak The tweak value (t_k) as 32 bytes binary.
|
|
16
|
+
# @param [Integer, nil] label The label integer, or nil if no label was used.
|
|
17
|
+
# @raise [ArgumentError] If any parameter has an invalid type.
|
|
18
|
+
def initialize(tx_out, tweak, label = nil)
|
|
19
|
+
raise ArgumentError, "tx_out must be a Bitcoin::TxOut." unless tx_out.is_a?(Bitcoin::TxOut)
|
|
20
|
+
raise ArgumentError, "tweak must be a 32-byte String." unless tweak.is_a?(String) && tweak.bytesize == 32
|
|
21
|
+
raise ArgumentError, "label must be an Integer or nil." unless label.nil? || label.is_a?(Integer)
|
|
22
|
+
@tx_out = tx_out
|
|
23
|
+
@tweak = tweak
|
|
24
|
+
@label = label
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns the x-only public key of the output.
|
|
28
|
+
# @return [String] The x-only public key as hex string.
|
|
29
|
+
def pubkey
|
|
30
|
+
tx_out.script_pubkey.witness_data[1].bth
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns the tweak as hex string.
|
|
34
|
+
# @return [String] The tweak value as hex string.
|
|
35
|
+
def tweak_hex
|
|
36
|
+
tweak.bth
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Returns whether this output was detected using a label.
|
|
40
|
+
# @return [Boolean]
|
|
41
|
+
def labeled?
|
|
42
|
+
!label.nil?
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -1,12 +1,22 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
1
3
|
module Bitcoin
|
|
4
|
+
# BIP-352 silent payment module.
|
|
5
|
+
# @see https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki
|
|
2
6
|
module SilentPayment
|
|
7
|
+
autoload :Output, 'bitcoin/silent_payment/output'
|
|
3
8
|
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
#
|
|
9
|
+
# Maximum number of silent payment addresses that can share the same scan public key
|
|
10
|
+
# within a single transaction. This is the maximum number of P2TR outputs that can fit
|
|
11
|
+
# within a 100KB transaction under current standardness rules.
|
|
12
|
+
K_MAX = 2323
|
|
13
|
+
|
|
14
|
+
# Derive payment point.
|
|
15
|
+
#
|
|
16
|
+
# @param [Array<Bitcoin::Script>] prevouts An array of previous output script.
|
|
17
|
+
# @param [Array<Bitcoin::Key>] private_keys An array of Bitcoin::Key objects corresponding to each public key in prevouts.
|
|
18
|
+
# @param [Array<Bech32::SilentPaymentAddr>] recipients
|
|
19
|
+
# @return [Array<ECDSA::Point>] An array of derived points.
|
|
10
20
|
# @raise [ArgumentError]
|
|
11
21
|
def derive_payment_points(prevouts, private_keys, recipients)
|
|
12
22
|
raise ArgumentError, "prevouts must be Array." unless prevouts.is_a? Array
|
|
@@ -14,19 +24,19 @@ module Bitcoin
|
|
|
14
24
|
raise ArgumentError, "prevouts and private_keys must be the same length." unless prevouts.length == private_keys.length
|
|
15
25
|
raise ArgumentError, "recipients must be Array." unless recipients.is_a? Array
|
|
16
26
|
|
|
17
|
-
outpoint_l = inputs.map{|i|i.out_point.to_hex}.sort.first
|
|
18
|
-
|
|
19
27
|
input_pub_keys = []
|
|
20
28
|
field = ECDSA::PrimeField.new(Bitcoin::Secp256k1::GROUP.order)
|
|
21
29
|
sum_priv_keys = 0
|
|
22
30
|
prevouts.each_with_index do |prevout, index|
|
|
23
|
-
|
|
31
|
+
key = private_keys[index]
|
|
32
|
+
raise ArgumentError, "private_keys element must be Bitcoin::Key." unless key.is_a? Bitcoin::Key
|
|
33
|
+
priv_key_int = key.priv_key.to_i(16)
|
|
24
34
|
public_key = extract_public_key(prevout, inputs[index])
|
|
25
35
|
next if public_key.nil?
|
|
26
|
-
private_key = if public_key.p2tr? &&
|
|
27
|
-
field.mod(-
|
|
36
|
+
private_key = if public_key.p2tr? && key.to_point.y.odd?
|
|
37
|
+
field.mod(-priv_key_int)
|
|
28
38
|
else
|
|
29
|
-
|
|
39
|
+
priv_key_int
|
|
30
40
|
end
|
|
31
41
|
input_pub_keys << public_key
|
|
32
42
|
sum_priv_keys = field.mod(sum_priv_keys + private_key)
|
|
@@ -34,6 +44,8 @@ module Bitcoin
|
|
|
34
44
|
agg_pubkey = (Bitcoin::Secp256k1::GROUP.generator.to_jacobian * sum_priv_keys).to_affine
|
|
35
45
|
return [] if agg_pubkey.infinity?
|
|
36
46
|
|
|
47
|
+
outpoint_l = inputs.map{|i|i.out_point.to_hex}.sort.first
|
|
48
|
+
|
|
37
49
|
input_hash = Bitcoin.tagged_hash("BIP0352/Inputs", outpoint_l.htb + agg_pubkey.to_hex.htb).bth
|
|
38
50
|
|
|
39
51
|
destinations = {}
|
|
@@ -42,6 +54,12 @@ module Bitcoin
|
|
|
42
54
|
destinations[sp_addr.scan_key] = [] unless destinations.has_key?(sp_addr.scan_key)
|
|
43
55
|
destinations[sp_addr.scan_key] << sp_addr.spend_key
|
|
44
56
|
end
|
|
57
|
+
|
|
58
|
+
# Check K_max limit: fail if any group exceeds the limit
|
|
59
|
+
destinations.each_value do |spends|
|
|
60
|
+
raise ArgumentError, "Recipient group exceeds K_max limit (#{K_MAX})." if spends.length > K_MAX
|
|
61
|
+
end
|
|
62
|
+
|
|
45
63
|
outputs = []
|
|
46
64
|
destinations.each do |scan_key, spends|
|
|
47
65
|
scan_key = Bitcoin::Key.new(pubkey: scan_key).to_point.to_jacobian
|
|
@@ -55,6 +73,89 @@ module Bitcoin
|
|
|
55
73
|
outputs
|
|
56
74
|
end
|
|
57
75
|
|
|
76
|
+
|
|
77
|
+
# Scan transaction outputs for silent payment outputs belonging to the receiver.
|
|
78
|
+
#
|
|
79
|
+
# @param [Array<Bitcoin::Script>] prevouts An array of previous output scripts corresponding to each input.
|
|
80
|
+
# @param [Bitcoin::Key] scan_private_key The receiver's scan private key (b_scan).
|
|
81
|
+
# @param [Bitcoin::Key] spend_pubkey The receiver's spend key. Pass a Key initialized with spend_priv_key to derive the public key.
|
|
82
|
+
# @param [Array<Integer>] labels An array of label integers for labeled addresses (default: []).
|
|
83
|
+
# @return [Array<Bitcoin::SilentPayment::Output>] An array of detected silent payment outputs.
|
|
84
|
+
# @raise [ArgumentError] If any of the required parameters are invalid.
|
|
85
|
+
def scan_sp_outputs(prevouts, scan_private_key, spend_pubkey, labels = [])
|
|
86
|
+
raise ArgumentError, "prevouts must be Array." unless prevouts.is_a? Array
|
|
87
|
+
raise ArgumentError, "scan_private_key must be Bitcoin::Key." unless scan_private_key.is_a? Bitcoin::Key
|
|
88
|
+
raise ArgumentError, "spend_pubkey must be Bitcoin::Key." unless spend_pubkey.is_a? Bitcoin::Key
|
|
89
|
+
|
|
90
|
+
has_taproot = !outputs.find{|o| o.script_pubkey.p2tr? }.nil?
|
|
91
|
+
return [] unless has_taproot
|
|
92
|
+
sum_pub_keys = Bitcoin::Secp256k1::GROUP.infinity.to_jacobian
|
|
93
|
+
maximum_witness_version = Bitcoin::Opcodes.opcode_to_small_int(Bitcoin::Opcodes::OP_1)
|
|
94
|
+
prevouts.each.with_index do |prevout, index|
|
|
95
|
+
return [] if prevout.witness_program? && prevout.witness_data.first > maximum_witness_version
|
|
96
|
+
|
|
97
|
+
public_key = extract_public_key(prevout, inputs[index])
|
|
98
|
+
next if public_key.nil?
|
|
99
|
+
sum_pub_keys += public_key.to_point.to_jacobian
|
|
100
|
+
end
|
|
101
|
+
return [] if sum_pub_keys.infinity?
|
|
102
|
+
|
|
103
|
+
field = ECDSA::PrimeField.new(Bitcoin::Secp256k1::GROUP.order)
|
|
104
|
+
outpoint_l = inputs.map{|i|i.out_point.to_hex}.sort.first
|
|
105
|
+
input_hash = Bitcoin.tagged_hash("BIP0352/Inputs", outpoint_l.htb + sum_pub_keys.to_affine.to_hex.htb).bth
|
|
106
|
+
ecdh_shared_secret = (sum_pub_keys * field.mod(input_hash.to_i(16) * scan_private_key.priv_key.to_i(16))).to_affine.to_hex.htb
|
|
107
|
+
|
|
108
|
+
# Pre-compute label tweak points with their label values and scalar tweaks
|
|
109
|
+
label_tweaks = labels.map do |m|
|
|
110
|
+
label_tweak = Bitcoin.tagged_hash('BIP0352/Label', scan_private_key.priv_key.htb + [m].pack('N'))
|
|
111
|
+
label_point = Bitcoin::Secp256k1::GROUP.generator.to_jacobian * label_tweak.bti
|
|
112
|
+
[m, label_tweak, label_point]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
k = 0
|
|
116
|
+
results = []
|
|
117
|
+
found_outputs = Set.new
|
|
118
|
+
loop do
|
|
119
|
+
# Stop scanning if K_max limit is reached
|
|
120
|
+
break if k == K_MAX
|
|
121
|
+
|
|
122
|
+
t_k = Bitcoin.tagged_hash('BIP0352/SharedSecret', ecdh_shared_secret + [k].pack('N'))
|
|
123
|
+
p_k = Bitcoin::Secp256k1::GROUP.generator.to_jacobian * t_k.bti + spend_pubkey.to_point.to_jacobian
|
|
124
|
+
found = false
|
|
125
|
+
outputs.each do |output|
|
|
126
|
+
next unless output.script_pubkey.p2tr?
|
|
127
|
+
next if found_outputs.include?(output)
|
|
128
|
+
output_pubkey = Bitcoin::Key.from_xonly_pubkey(output.script_pubkey.witness_data[1].bth)
|
|
129
|
+
|
|
130
|
+
# Check basic match (no label)
|
|
131
|
+
if p_k.to_affine.x == output_pubkey.to_point.x
|
|
132
|
+
results << SilentPayment::Output.new(output, t_k)
|
|
133
|
+
found_outputs << output
|
|
134
|
+
k += 1
|
|
135
|
+
found = true
|
|
136
|
+
break
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Check labeled matches
|
|
140
|
+
label_tweaks.each do |label_value, label_tweak_scalar, label_point|
|
|
141
|
+
p_k_labeled = p_k + label_point
|
|
142
|
+
if p_k_labeled.to_affine.x == output_pubkey.to_point.x
|
|
143
|
+
# Full tweak is t_k + label_tweak (mod order)
|
|
144
|
+
full_tweak = field.mod(t_k.bti + label_tweak_scalar.bti).to_s(16).rjust(64, '0').htb
|
|
145
|
+
results << SilentPayment::Output.new(output, full_tweak, label_value)
|
|
146
|
+
found_outputs << output
|
|
147
|
+
k += 1
|
|
148
|
+
found = true
|
|
149
|
+
break
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
break if found
|
|
153
|
+
end
|
|
154
|
+
break unless found
|
|
155
|
+
end
|
|
156
|
+
results
|
|
157
|
+
end
|
|
158
|
+
|
|
58
159
|
# Extract public keys from +prevout+ and input.
|
|
59
160
|
def extract_public_key(prevout, input)
|
|
60
161
|
if prevout.p2pkh?
|
data/lib/bitcoin/util.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Bitcoin
|
|
|
14
14
|
|
|
15
15
|
def unpack_var_string(payload)
|
|
16
16
|
size, payload = unpack_var_int(payload)
|
|
17
|
-
size > 0 ? payload.unpack("a#{size}a*") : [
|
|
17
|
+
size && size > 0 ? payload.unpack("a#{size}a*") : ['', payload.to_s]
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def pack_var_int(i)
|
|
@@ -102,7 +102,7 @@ module Bitcoin
|
|
|
102
102
|
# @return [String] the hash value with binary format.
|
|
103
103
|
def tagged_hash(tag, msg)
|
|
104
104
|
tag_hash = Digest::SHA256.digest(tag)
|
|
105
|
-
Digest::SHA256.digest(tag_hash + tag_hash + msg)
|
|
105
|
+
Digest::SHA256.digest(tag_hash + tag_hash + msg.b)
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
# encode Base58 check address.
|
data/lib/bitcoin/version.rb
CHANGED
data/lib/bitcoin.rb
CHANGED
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
# https://github.com/lian/bitcoin-ruby/blob/master/COPYING
|
|
3
3
|
|
|
4
4
|
require 'bitcoin/version'
|
|
5
|
-
require 'eventmachine'
|
|
6
5
|
require 'schnorr'
|
|
7
6
|
require 'securerandom'
|
|
8
7
|
require 'json'
|
|
9
8
|
require 'bech32'
|
|
10
9
|
require 'base64'
|
|
11
|
-
require 'observer'
|
|
12
10
|
require 'tmpdir'
|
|
13
11
|
require 'merkle'
|
|
14
12
|
|
|
@@ -36,13 +34,10 @@ module Bitcoin
|
|
|
36
34
|
autoload :ExtKey, 'bitcoin/ext_key'
|
|
37
35
|
autoload :ExtPubkey, 'bitcoin/ext_key'
|
|
38
36
|
autoload :Opcodes, 'bitcoin/opcodes'
|
|
39
|
-
autoload :Node, 'bitcoin/node'
|
|
40
37
|
autoload :Base58, 'bitcoin/base58'
|
|
41
38
|
autoload :Secp256k1, 'bitcoin/secp256k1'
|
|
42
39
|
autoload :Mnemonic, 'bitcoin/mnemonic'
|
|
43
40
|
autoload :ValidationState, 'bitcoin/validation'
|
|
44
|
-
autoload :Network, 'bitcoin/network'
|
|
45
|
-
autoload :Store, 'bitcoin/store'
|
|
46
41
|
autoload :RPC, 'bitcoin/rpc'
|
|
47
42
|
autoload :Wallet, 'bitcoin/wallet'
|
|
48
43
|
autoload :BloomFilter, 'bitcoin/bloom_filter'
|
|
@@ -64,7 +59,6 @@ module Bitcoin
|
|
|
64
59
|
autoload :SilentPayment, 'bitcoin/silent_payment'
|
|
65
60
|
autoload :BIP324, 'bitcoin/bip324'
|
|
66
61
|
autoload :BIP321URI, 'bitcoin/bip321_uri'
|
|
67
|
-
autoload :BIP353, 'bitcoin/bip353'
|
|
68
62
|
|
|
69
63
|
require_relative 'bitcoin/constants'
|
|
70
64
|
require_relative 'bitcoin/ext/ecdsa'
|