bitcoinrb 1.12.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +1 -1
  3. data/bitcoinrb.gemspec +1 -6
  4. data/lib/bitcoin/bip324/fs_chacha20.rb +3 -5
  5. data/lib/bitcoin/bip324/fs_chacha_poly1305.rb +19 -17
  6. data/lib/bitcoin/bip324.rb +1 -1
  7. data/lib/bitcoin/descriptor/expression.rb +2 -2
  8. data/lib/bitcoin/descriptor/sp.rb +219 -0
  9. data/lib/bitcoin/descriptor.rb +20 -0
  10. data/lib/bitcoin/ext_key.rb +3 -3
  11. data/lib/bitcoin/message/ping.rb +4 -2
  12. data/lib/bitcoin/message/version.rb +1 -1
  13. data/lib/bitcoin/message_sign.rb +98 -38
  14. data/lib/bitcoin/rpc.rb +0 -2
  15. data/lib/bitcoin/silent_payment/output.rb +46 -0
  16. data/lib/bitcoin/silent_payment.rb +113 -12
  17. data/lib/bitcoin/util.rb +2 -2
  18. data/lib/bitcoin/version.rb +1 -1
  19. data/lib/bitcoin.rb +0 -6
  20. metadata +18 -108
  21. data/exe/bitcoinrb-cli +0 -5
  22. data/exe/bitcoinrbd +0 -46
  23. data/lib/bitcoin/network/connection.rb +0 -73
  24. data/lib/bitcoin/network/message_handler.rb +0 -243
  25. data/lib/bitcoin/network/peer.rb +0 -228
  26. data/lib/bitcoin/network/peer_discovery.rb +0 -42
  27. data/lib/bitcoin/network/pool.rb +0 -135
  28. data/lib/bitcoin/network.rb +0 -13
  29. data/lib/bitcoin/node/cli.rb +0 -122
  30. data/lib/bitcoin/node/configuration.rb +0 -40
  31. data/lib/bitcoin/node/spv.rb +0 -87
  32. data/lib/bitcoin/node.rb +0 -7
  33. data/lib/bitcoin/rpc/http_server.rb +0 -84
  34. data/lib/bitcoin/rpc/request_handler.rb +0 -150
  35. data/lib/bitcoin/store/chain_entry.rb +0 -68
  36. data/lib/bitcoin/store/db/level_db.rb +0 -98
  37. data/lib/bitcoin/store/db.rb +0 -9
  38. data/lib/bitcoin/store/spv_chain.rb +0 -101
  39. data/lib/bitcoin/store/utxo_db.rb +0 -226
  40. data/lib/bitcoin/store.rb +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d7be6093f10fa04e688b7106ecc080066e0ec693af6b23bb1e0ddb0dbc2e586
4
- data.tar.gz: e3fce6344304e22350cde36a0471d1dd1a0dc5d8c1d8b22309efc20704cd74f5
3
+ metadata.gz: e60d83b48a9f7189cd07c75ee61749ab50a83895f86dd97c7d7fbbd568234067
4
+ data.tar.gz: 04e39f6f592379617273944009f78d224cf181f1877de15a285665e9b25602b3
5
5
  SHA512:
6
- metadata.gz: f65b05a052712bf2186543fbae88222b5e8e753aa201ca00c6997ffad364cab62bfa0b47a091eb998343ffacbcd050c05c9def591d90e28c8a04a3c71286a909
7
- data.tar.gz: 47e7fd6c0cb573723523671b2f0002c2d347e4e8187e08157a72326fb91094631c581290356c29fe25d65d9adb5e420b39e3cf22efcbcff24e9d281cef734f3a
6
+ metadata.gz: 13046ad4ba44662c2689d13dec7d51bc88e2d80cccaedc0048ee7484136e5cf04600161b5b7ae99faf4e1d7081fb845b403ebc3defb9cf92e46847e500befd65
7
+ data.tar.gz: 12c68844e79ca382dc774d6d4d6cdad8bbd33064a315098e0a31eecb88ac1369c2bd4ff6b39da49d3d5b2eae9750beff11e5761dbbdbe6221c3ef1100cd8cde2
@@ -22,7 +22,7 @@ jobs:
22
22
  ruby-version: ['3.2', '3.3', '3.4', '4.0']
23
23
 
24
24
  steps:
25
- - uses: actions/checkout@v4
25
+ - uses: actions/checkout@v5
26
26
  - name: Install leveldb
27
27
  run: sudo apt-get install libleveldb-dev
28
28
  - name: Set up Ruby
data/bitcoinrb.gemspec CHANGED
@@ -21,19 +21,14 @@ Gem::Specification.new do |spec|
21
21
  spec.require_paths = ["lib"]
22
22
 
23
23
  spec.add_runtime_dependency 'ecdsa_ext', '~> 0.5.1'
24
- spec.add_runtime_dependency 'eventmachine'
25
24
  spec.add_runtime_dependency 'murmurhash3', '~> 0.1.7'
26
25
  spec.add_runtime_dependency 'bech32', '>= 1.5.0'
27
- spec.add_runtime_dependency 'daemon-spawn'
28
- spec.add_runtime_dependency 'thor'
29
- spec.add_runtime_dependency 'eventmachine_httpserver'
30
- spec.add_runtime_dependency 'iniparse'
31
26
  spec.add_runtime_dependency 'siphash'
32
27
  spec.add_runtime_dependency 'bip-schnorr', '>= 0.7.0'
33
28
  spec.add_runtime_dependency 'base32', '>= 0.3.4'
34
29
  spec.add_runtime_dependency 'base64', '~> 0.2.0'
35
- spec.add_runtime_dependency 'observer', '~> 0.1.2'
36
30
  spec.add_runtime_dependency 'secp256k1rb', '0.1.1'
37
31
  spec.add_runtime_dependency 'logger'
38
32
  spec.add_runtime_dependency 'merkle', '0.3.0'
33
+ spec.add_runtime_dependency 'dnsruby', '1.73.0'
39
34
  end
@@ -16,8 +16,6 @@ module Bitcoin
16
16
  # @param [Integer] bits
17
17
  # @return [Integer]
18
18
  def rotl32(v, bits)
19
- raise Bitcoin::BIP324::Error, "v must be integer" unless v.is_a?(Integer)
20
- raise Bitcoin::BIP324::Error, "bits must be integer" unless bits.is_a?(Integer)
21
19
  ((v << bits) & 0xffffffff) | (v >> (32 - bits))
22
20
  end
23
21
 
@@ -117,9 +115,9 @@ module Bitcoin
117
115
  # @return [String]
118
116
  def crypt(chunk)
119
117
  ks = key_stream_bytes(chunk.bytesize)
120
- ret = chunk.unpack("C*").zip(ks.unpack("C*")).map do |c, k|
121
- c ^ k
122
- end.pack("C*")
118
+ chunk_bytes = chunk.unpack("C*")
119
+ ks_bytes = ks.unpack("C*")
120
+ ret = chunk_bytes.each_with_index.map { |c, i| c ^ ks_bytes[i] }.pack("C*")
123
121
  if (self.chunk_counter + 1) % rekey_interval == 0
124
122
  self.key = key_stream_bytes(32)
125
123
  self.block_counter = 0
@@ -92,14 +92,11 @@ module Bitcoin
92
92
  # Encrypt a plaintext using ChaCha20Poly1305.
93
93
  def chacha20_poly1305_encrypt(key, nonce, aad, plaintext)
94
94
  msg_len = plaintext.bytesize
95
- ret = ((msg_len + 63) / 64).times.map do |i|
96
- now = [64, msg_len - 64 * i].min
97
- keystream = ChaCha20.block(key, nonce, i + 1)
98
- now.times.map do |j|
99
- plaintext[j + 64 * i].unpack1('C') ^ keystream[j].unpack1('C')
100
- end
101
- end
102
- ret = ret.flatten.pack('C*')
95
+ num_blocks = (msg_len + 63) / 64
96
+ keystream = num_blocks.times.map { |i| ChaCha20.block(key, nonce, i + 1) }.join
97
+ pt_bytes = plaintext.unpack('C*')
98
+ ks_bytes = keystream.unpack('C*')
99
+ ret = Array.new(msg_len) { |i| pt_bytes[i] ^ ks_bytes[i] }.pack('C*')
103
100
  poly1305 = Poly1305.new(ChaCha20.block(key, nonce, 0)[0...32])
104
101
  poly1305.add(aad, padding: true).add(ret, padding: true)
105
102
  poly1305.add([aad.bytesize, msg_len].pack("Q<Q<"))
@@ -114,15 +111,20 @@ module Bitcoin
114
111
  poly1305.add(aad, padding: true)
115
112
  poly1305.add(ciphertext, length: msg_len, padding: true)
116
113
  poly1305.add([aad.bytesize, msg_len].pack("Q<Q<"))
117
- return nil unless ciphertext[-16..-1] == poly1305.tag
118
- ret = ((msg_len + 63) / 64).times.map do |i|
119
- now = [64, msg_len - 64 * i].min
120
- keystream = ChaCha20.block(key, nonce, i + 1)
121
- now.times.map do |j|
122
- ciphertext[j + 64 * i].unpack1('C') ^ keystream[j].unpack1('C')
123
- end
124
- end
125
- ret.flatten.pack('C*')
114
+ return nil unless constant_time_equal?(ciphertext[-16..-1], poly1305.tag)
115
+ num_blocks = (msg_len + 63) / 64
116
+ keystream = num_blocks.times.map { |i| ChaCha20.block(key, nonce, i + 1) }.join
117
+ ct_bytes = ciphertext.unpack('C*')
118
+ ks_bytes = keystream.unpack('C*')
119
+ Array.new(msg_len) { |i| ct_bytes[i] ^ ks_bytes[i] }.pack('C*')
120
+ end
121
+
122
+ # Constant-time comparison of two equal-length byte strings.
123
+ def constant_time_equal?(a, b)
124
+ return false unless a.bytesize == b.bytesize
125
+ diff = 0
126
+ a.bytesize.times { |i| diff |= a.getbyte(i) ^ b.getbyte(i) }
127
+ diff.zero?
126
128
  end
127
129
  end
128
130
  end
@@ -95,7 +95,7 @@ module Bitcoin
95
95
  def xelligatorswift(x)
96
96
  loop do
97
97
  u = SecureRandom.random_number(1..ECDSA::Group::Secp256k1.order).to_s(16)
98
- c = Random.rand(0..8)
98
+ c = SecureRandom.random_number(9)
99
99
  t = xswiftec_inv(x, u, c)
100
100
  unless t.nil?
101
101
  return (ECDSA::Format::IntegerOctetString.encode(u.hex, 32) +
@@ -67,10 +67,10 @@ module Bitcoin
67
67
 
68
68
  raise ArgumentError, "No key provided." unless key
69
69
 
70
- if key.start_with?('xprv')
70
+ if key.start_with?('xprv') || key.start_with?('tprv')
71
71
  key = Bitcoin::ExtKey.from_base58(key)
72
72
  key = derive_path(key, paths) if paths
73
- elsif key.start_with?('xpub')
73
+ elsif key.start_with?('xpub') || key.start_with?('tpub')
74
74
  key = Bitcoin::ExtPubkey.from_base58(key)
75
75
  key = derive_path(key, paths) if paths
76
76
  else
@@ -0,0 +1,219 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ # sp() expression for Silent Payment descriptor.
4
+ # Supports both single-argument (spscan/spspend) and two-argument forms.
5
+ # @see https://github.com/bitcoin/bips/blob/master/bip-0392.mediawiki
6
+ class Sp < Expression
7
+
8
+ # HRP for spscan/spspend encoding
9
+ SPSCAN_HRP = 'spscan'
10
+ SPSPEND_HRP = 'spspend'
11
+ TSPSCAN_HRP = 'tspscan'
12
+ TSPSPEND_HRP = 'tspspend'
13
+
14
+ attr_reader :key_arg
15
+ attr_reader :scan_key
16
+ attr_reader :spend_key
17
+
18
+ # Constructor.
19
+ # @param [String] key_or_scan_key Either a spscan/spspend encoded key, or the scan key for two-argument form.
20
+ # @param [String, Expression, nil] spend_key The spend key (for two-argument form) or nil for single-argument form.
21
+ # @raise [ArgumentError] If keys are invalid.
22
+ def initialize(key_or_scan_key, spend_key = nil)
23
+ raise ArgumentError, "key_or_scan_key must be a String." unless key_or_scan_key.is_a?(String)
24
+ unless spend_key.nil? || spend_key.is_a?(String) || spend_key.is_a?(Expression)
25
+ raise ArgumentError, "spend_key must be a String, Expression, or nil."
26
+ end
27
+ if spend_key.nil?
28
+ # Single argument form: spscan or spspend encoded key
29
+ @key_arg = key_or_scan_key
30
+ parse_encoded_key(key_or_scan_key)
31
+ else
32
+ # Two argument form: separate scan and spend keys
33
+ @key_arg = nil
34
+ @scan_key = key_or_scan_key
35
+ @spend_key = spend_key
36
+ end
37
+ validate_keys!
38
+ end
39
+
40
+ def type
41
+ :sp
42
+ end
43
+
44
+ def top_level?
45
+ true
46
+ end
47
+
48
+ def args
49
+ if key_arg
50
+ # Single argument form
51
+ key_arg
52
+ else
53
+ # Two argument form
54
+ spend_arg = spend_key.is_a?(Expression) ? spend_key.to_s : spend_key
55
+ "#{scan_key},#{spend_arg}"
56
+ end
57
+ end
58
+
59
+ # Check if this is a single-argument form (spscan/spspend).
60
+ # @return [Boolean]
61
+ def single_key?
62
+ !key_arg.nil?
63
+ end
64
+
65
+ # Check if this descriptor has spend private key (spspend form).
66
+ # @return [Boolean]
67
+ def has_spend_private_key?
68
+ return @has_spend_private_key if defined?(@has_spend_private_key)
69
+ if single_key?
70
+ @has_spend_private_key
71
+ else
72
+ # Check if spend_key is a private key
73
+ return false if spend_key.is_a?(Expression)
74
+ begin
75
+ key = extract_pubkey(spend_key)
76
+ !key.priv_key.nil?
77
+ rescue
78
+ false
79
+ end
80
+ end
81
+ end
82
+
83
+ # Generate silent payment address.
84
+ # @return [Bech32::SilentPaymentAddr] Silent payment address.
85
+ def to_addr
86
+ Bech32::SilentPaymentAddr.new(
87
+ address_hrp,
88
+ 0,
89
+ extracted_scan_pubkey,
90
+ extracted_spend_pubkey
91
+ )
92
+ end
93
+
94
+ # Silent payment descriptors do not produce a single script.
95
+ # Use to_address to get the silent payment address.
96
+ # @raise [RuntimeError]
97
+ def to_script
98
+ raise RuntimeError, "sp() descriptor does not produce a fixed script. Use to_address instead."
99
+ end
100
+
101
+ private
102
+
103
+ # Parse spscan or spspend encoded key.
104
+ # @param [String] encoded_key The encoded key string.
105
+ # @raise [ArgumentError] If the key format is invalid.
106
+ def parse_encoded_key(encoded_key)
107
+ # Handle key origin (e.g., [fingerprint/path]key)
108
+ key_str = encoded_key
109
+ if key_str.start_with?('[')
110
+ key_str = key_str[(key_str.index(']') + 1)..-1]
111
+ end
112
+
113
+ # Decode Bech32m (max_length increased for longer spscan/spspend strings)
114
+ hrp, data, spec = Bech32.decode(key_str, 200)
115
+ raise ArgumentError, "Invalid spscan/spspend encoding." unless hrp
116
+ raise ArgumentError, "spscan/spspend must use Bech32m encoding." unless spec == Bech32::Encoding::BECH32M
117
+
118
+ # Validate HRP
119
+ valid_hrps = [SPSCAN_HRP, SPSPEND_HRP, TSPSCAN_HRP, TSPSPEND_HRP]
120
+ raise ArgumentError, "Invalid HRP: #{hrp}. Expected one of: #{valid_hrps.join(', ')}" unless valid_hrps.include?(hrp)
121
+
122
+ # Check version (first data element should be 0 for version 0)
123
+ raise ArgumentError, "Invalid version." unless data[0] == 0
124
+
125
+ # Convert from 5-bit to 8-bit
126
+ payload = Bech32.convert_bits(data[1..-1], 5, 8, false)
127
+ payload_bytes = payload.pack('C*')
128
+
129
+ is_spspend = [SPSPEND_HRP, TSPSPEND_HRP].include?(hrp)
130
+
131
+ if is_spspend
132
+ # spspend: 32 bytes scan + 32 bytes spend
133
+ raise ArgumentError, "Invalid spspend payload length." unless payload_bytes.bytesize == 64
134
+ scan_priv = payload_bytes[0, 32].bth
135
+ spend_priv = payload_bytes[32, 32].bth
136
+ @scan_key = scan_priv
137
+ @spend_key = spend_priv
138
+ @has_spend_private_key = true
139
+ else
140
+ # spscan: 32 bytes scan + 33 bytes spend pubkey
141
+ raise ArgumentError, "Invalid spscan payload length." unless payload_bytes.bytesize == 65
142
+ scan_priv = payload_bytes[0, 32].bth
143
+ spend_pub = payload_bytes[32, 33].bth
144
+ @scan_key = scan_priv
145
+ @spend_key = spend_pub
146
+ @has_spend_private_key = false
147
+ end
148
+ end
149
+
150
+ def validate_keys!
151
+ # Scan key must be a private key
152
+ scan = extracted_scan_key
153
+ raise ArgumentError, "Scan key must be a private key." unless scan.priv_key
154
+
155
+ # Validate spend key
156
+ spend = extracted_spend_key
157
+ raise ArgumentError, "Uncompressed keys are not allowed." unless spend.compressed?
158
+ end
159
+
160
+ # Get the extracted scan key.
161
+ # @return [Bitcoin::Key] Extracted scan key.
162
+ def extracted_scan_key
163
+ if single_key?
164
+ # scan_key is already a hex private key from spscan/spspend
165
+ Bitcoin::Key.new(priv_key: scan_key)
166
+ else
167
+ extract_pubkey(scan_key)
168
+ end
169
+ end
170
+
171
+ # Get the extracted scan public key (compressed, 33 bytes hex).
172
+ # @return [String] Compressed public key hex.
173
+ def extracted_scan_pubkey
174
+ extracted_scan_key.pubkey
175
+ end
176
+
177
+ # Get the extracted spend key.
178
+ # @return [Bitcoin::Key] Extracted spend key.
179
+ def extracted_spend_key
180
+ if spend_key.is_a?(MuSig)
181
+ # MuSig returns x-only pubkey, convert to full pubkey
182
+ Bitcoin::Key.from_xonly_pubkey(spend_key.to_hex)
183
+ elsif spend_key.is_a?(Expression)
184
+ spend_key.extracted_key
185
+ elsif single_key? && has_spend_private_key?
186
+ # spspend: spend_key is hex private key
187
+ Bitcoin::Key.new(priv_key: spend_key)
188
+ elsif single_key?
189
+ # spscan: spend_key is hex public key
190
+ Bitcoin::Key.new(pubkey: spend_key)
191
+ else
192
+ extract_pubkey(spend_key)
193
+ end
194
+ end
195
+
196
+ # Get the extracted spend public key (compressed, 33 bytes hex).
197
+ # @return [String] Compressed public key hex.
198
+ def extracted_spend_pubkey
199
+ if spend_key.is_a?(MuSig)
200
+ # MuSig aggregated key as compressed pubkey (even y-coordinate assumed)
201
+ '02' + spend_key.to_hex
202
+ else
203
+ extracted_spend_key.pubkey
204
+ end
205
+ end
206
+
207
+ # Get HRP for silent payment address based on network.
208
+ # @return [String] HRP (sp for mainnet, tsp for testnet/signet).
209
+ def address_hrp
210
+ case Bitcoin.chain_params.network
211
+ when 'mainnet'
212
+ Bech32::SilentPaymentAddr::HRP_MAINNET
213
+ else
214
+ Bech32::SilentPaymentAddr::HRP_TESTNET
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -22,6 +22,7 @@ module Bitcoin
22
22
  autoload :RawTr, 'bitcoin/descriptor/raw_tr'
23
23
  autoload :Checksum, 'bitcoin/descriptor/checksum'
24
24
  autoload :MuSig, 'bitcoin/descriptor/musig'
25
+ autoload :Sp, 'bitcoin/descriptor/sp'
25
26
 
26
27
  module_function
27
28
 
@@ -137,6 +138,15 @@ module Bitcoin
137
138
  MuSig.new(keys, path)
138
139
  end
139
140
 
141
+ # Generate sp() descriptor for Silent Payment.
142
+ # @param [String] key_or_scan_key Either a spscan/spspend encoded key (single-argument form),
143
+ # or the scan key for two-argument form.
144
+ # @param [String, Expression, nil] spend_key The spend key (for two-argument form) or nil for single-argument form.
145
+ # @return [Bitcoin::Descriptor::Sp]
146
+ def sp(key_or_scan_key, spend_key = nil)
147
+ Sp.new(key_or_scan_key, spend_key)
148
+ end
149
+
140
150
  # Parse descriptor string.
141
151
  # @param [String] string Descriptor string.
142
152
  # @return [Bitcoin::Descriptor::Expression]
@@ -202,6 +212,16 @@ module Bitcoin
202
212
  when 'musig'
203
213
  keys = args_str.split(',')
204
214
  path.empty? ? musig(*keys) : musig(*keys, path: path)
215
+ when 'sp'
216
+ scan_key, spend_key_str = split_two(args_str)
217
+ if spend_key_str.nil?
218
+ # Single argument form: spscan or spspend encoded key
219
+ sp(scan_key)
220
+ else
221
+ # Two argument form: separate scan and spend keys
222
+ spend_key = spend_key_str.include?('(') ? parse(spend_key_str, false) : spend_key_str
223
+ sp(scan_key, spend_key)
224
+ end
205
225
  else
206
226
  raise ArgumentError, "Parse failed: #{string}"
207
227
  end
@@ -108,9 +108,9 @@ module Bitcoin
108
108
  end
109
109
  l = Bitcoin.hmac_sha512(chain_code, data)
110
110
  left = l[0..31].bth.to_i(16)
111
- raise 'invalid key' if left >= CURVE_ORDER
111
+ raise 'invalid key' if left >= CURVE_ORDER || left == 0
112
112
  child_priv = (left + key.priv_key.to_i(16)) % CURVE_ORDER
113
- raise 'invalid key ' if child_priv >= CURVE_ORDER
113
+ raise 'invalid key' if child_priv == 0
114
114
  child_priv = ECDSA::Format::IntegerOctetString.encode(child_priv, 32)
115
115
  new_key.key = Bitcoin::Key.new(priv_key: child_priv.bth, key_type: key_type)
116
116
  new_key.chain_code = l[32..-1]
@@ -280,7 +280,7 @@ module Bitcoin
280
280
  data = pub.htb << [number].pack('N')
281
281
  l = Bitcoin.hmac_sha512(chain_code, data)
282
282
  left = l[0..31].bth.to_i(16)
283
- raise 'invalid key' if left >= CURVE_ORDER
283
+ raise 'invalid key' if left >= CURVE_ORDER || left == 0
284
284
  l_priv = ECDSA::Format::IntegerOctetString.encode(left, 32)
285
285
  p1 = Bitcoin::Key.new(priv_key: l_priv.bth, key_type: Bitcoin::Key::TYPES[:uncompressed]).to_point
286
286
  p2 = Bitcoin::Key.new(pubkey: pubkey, key_type: key_type).to_point
@@ -18,11 +18,13 @@ module Bitcoin
18
18
  end
19
19
 
20
20
  def to_payload
21
- [nonce].pack('Q')
21
+ nonce ? [nonce].pack('Q') : ''
22
22
  end
23
23
 
24
+ # Generate pong message as a response. Return nil if the ping has no nonce
25
+ # (sent by a node before BIP-31), since such a ping does not expect a pong.
24
26
  def to_response
25
- Pong.new(nonce)
27
+ Pong.new(nonce) if nonce
26
28
  end
27
29
 
28
30
  end
@@ -61,7 +61,7 @@ module Bitcoin
61
61
  end
62
62
 
63
63
  def unpack_relay_field(payload)
64
- ( version >= 70001 && payload ) ? unpack_boolean(payload) : [ true, nil ]
64
+ ( version >= 70001 && payload && !payload.empty? ) ? unpack_boolean(payload) : [ true, nil ]
65
65
  end
66
66
 
67
67
  # Check whether +service_flag+ support this version.
@@ -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
- raise ArgumentError "#{address} dose not supported."
51
+
35
52
  end
36
53
  tx = to_sign_tx(digest, address)
37
- prev_out = Bitcoin::TxOut.new(script_pubkey: addr)
38
- sighash = tx.sighash_for_input(0, addr, sig_version: sig_ver, amount: 0, prevouts: [prev_out])
39
- sig = key.sign(sighash, algo: algo) + [Bitcoin::SIGHASH_TYPE[:all]].pack('C')
40
- tx.in[0].script_witness.stack << sig
41
- tx.in[0].script_witness.stack << key.pubkey.htb
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
- sig = Base64.strict_decode64(signature)
93
+ payload = Base64.strict_decode64(body)
56
94
  rescue ArgumentError
57
95
  raise ArgumentError, 'Invalid signature'
58
96
  end
59
- if addr_script.p2pkh?
60
- begin
61
- # Legacy verification
62
- pubkey = Bitcoin::Key.recover_compact(message_hash(message, prefix: prefix, legacy: true), sig)
63
- return false unless pubkey
64
- pubkey.to_p2pkh == address
65
- rescue Exception
66
- false
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
- # Full
73
- tx = Bitcoin::Tx.parse_from_payload(sig)
74
- validate_to_sign_tx!(tx)
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
- script_pubkey = Bitcoin::Script.parse_from_addr(address)
83
- tx_out = Bitcoin::TxOut.new(script_pubkey: script_pubkey)
84
- flags = Bitcoin::STANDARD_SCRIPT_VERIFY_FLAGS
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, "This address unsupported."
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
- script = Bitcoin::Script.parse_from_addr(address)
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,7 +1,5 @@
1
1
  module Bitcoin
2
2
  module RPC
3
- autoload :HttpServer, 'bitcoin/rpc/http_server'
4
- autoload :RequestHandler, 'bitcoin/rpc/request_handler'
5
3
  autoload :BitcoinCoreClient, 'bitcoin/rpc/bitcoin_core_client'
6
4
  end
7
5
  end