bitcoinrb 1.15.0 → 1.16.0

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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/bitcoinrb.gemspec +5 -4
  3. data/lib/bitcoin/bip321_uri.rb +95 -32
  4. data/lib/bitcoin/bip324/ell_swift_pubkey.rb +3 -1
  5. data/lib/bitcoin/bip324/fs_chacha20.rb +3 -0
  6. data/lib/bitcoin/bip324.rb +1 -1
  7. data/lib/bitcoin/bip85_entropy.rb +79 -29
  8. data/lib/bitcoin/bit_stream.rb +2 -2
  9. data/lib/bitcoin/block.rb +46 -7
  10. data/lib/bitcoin/block_filter.rb +3 -2
  11. data/lib/bitcoin/block_header.rb +31 -8
  12. data/lib/bitcoin/bloom_filter.rb +5 -2
  13. data/lib/bitcoin/chainparams/regtest.yml +0 -1
  14. data/lib/bitcoin/chainparams/signet.yml +1 -1
  15. data/lib/bitcoin/constants.rb +13 -3
  16. data/lib/bitcoin/descriptor/combo.rb +13 -1
  17. data/lib/bitcoin/descriptor/expression.rb +35 -15
  18. data/lib/bitcoin/descriptor/multi.rb +9 -1
  19. data/lib/bitcoin/descriptor/multi_a.rb +4 -0
  20. data/lib/bitcoin/descriptor/musig.rb +1 -0
  21. data/lib/bitcoin/descriptor/pk.rb +7 -0
  22. data/lib/bitcoin/descriptor/pkh.rb +5 -0
  23. data/lib/bitcoin/descriptor/raw.rb +1 -1
  24. data/lib/bitcoin/descriptor/sh.rb +1 -0
  25. data/lib/bitcoin/descriptor/sp.rb +2 -2
  26. data/lib/bitcoin/descriptor/tr.rb +29 -3
  27. data/lib/bitcoin/descriptor/wpkh.rb +1 -0
  28. data/lib/bitcoin/descriptor/wsh.rb +1 -0
  29. data/lib/bitcoin/descriptor.rb +59 -35
  30. data/lib/bitcoin/ext/array_ext.rb +3 -0
  31. data/lib/bitcoin/ext/ecdsa.rb +7 -1
  32. data/lib/bitcoin/ext_key.rb +10 -0
  33. data/lib/bitcoin/gcs_filter.rb +21 -6
  34. data/lib/bitcoin/key.rb +66 -14
  35. data/lib/bitcoin/key_path.rb +8 -5
  36. data/lib/bitcoin/message/addr.rb +1 -0
  37. data/lib/bitcoin/message/addr_v2.rb +1 -0
  38. data/lib/bitcoin/message/base.rb +8 -1
  39. data/lib/bitcoin/message/block.rb +4 -8
  40. data/lib/bitcoin/message/block_transaction_request.rb +11 -8
  41. data/lib/bitcoin/message/block_transactions.rb +2 -1
  42. data/lib/bitcoin/message/cf_parser.rb +1 -0
  43. data/lib/bitcoin/message/cfcheckpt.rb +3 -3
  44. data/lib/bitcoin/message/cfheaders.rb +5 -4
  45. data/lib/bitcoin/message/cfilter.rb +3 -3
  46. data/lib/bitcoin/message/error.rb +3 -1
  47. data/lib/bitcoin/message/fee_filter.rb +1 -0
  48. data/lib/bitcoin/message/filter_add.rb +3 -1
  49. data/lib/bitcoin/message/filter_load.rb +4 -4
  50. data/lib/bitcoin/message/get_cfcheckpt.rb +1 -0
  51. data/lib/bitcoin/message/header_and_short_ids.rb +26 -11
  52. data/lib/bitcoin/message/headers.rb +10 -2
  53. data/lib/bitcoin/message/headers_parser.rb +4 -2
  54. data/lib/bitcoin/message/inventories_parser.rb +2 -1
  55. data/lib/bitcoin/message/inventory.rb +15 -6
  56. data/lib/bitcoin/message/merkle_block.rb +4 -4
  57. data/lib/bitcoin/message/network_addr.rb +13 -13
  58. data/lib/bitcoin/message/ping.rb +1 -0
  59. data/lib/bitcoin/message/pong.rb +1 -0
  60. data/lib/bitcoin/message/reject.rb +1 -0
  61. data/lib/bitcoin/message/send_cmpct.rb +2 -2
  62. data/lib/bitcoin/message/send_tx_rcncl.rb +1 -1
  63. data/lib/bitcoin/message/version.rb +23 -8
  64. data/lib/bitcoin/message.rb +15 -0
  65. data/lib/bitcoin/message_sign.rb +44 -6
  66. data/lib/bitcoin/mnemonic.rb +8 -0
  67. data/lib/bitcoin/out_point.rb +6 -2
  68. data/lib/bitcoin/partial_tree.rb +41 -28
  69. data/lib/bitcoin/payment_code.rb +26 -8
  70. data/lib/bitcoin/psbt/hd_key_path.rb +1 -2
  71. data/lib/bitcoin/psbt/input.rb +199 -48
  72. data/lib/bitcoin/psbt/key_origin_info.rb +2 -0
  73. data/lib/bitcoin/psbt/output.rb +9 -6
  74. data/lib/bitcoin/psbt/proprietary.rb +6 -3
  75. data/lib/bitcoin/psbt/tx.rb +9 -4
  76. data/lib/bitcoin/psbt.rb +46 -0
  77. data/lib/bitcoin/rpc/bitcoin_core_client.rb +54 -13
  78. data/lib/bitcoin/rpc/error.rb +43 -0
  79. data/lib/bitcoin/rpc.rb +1 -0
  80. data/lib/bitcoin/script/script.rb +114 -33
  81. data/lib/bitcoin/script/script_interpreter.rb +11 -6
  82. data/lib/bitcoin/script/tx_checker.rb +7 -2
  83. data/lib/bitcoin/script_witness.rb +5 -1
  84. data/lib/bitcoin/secp256k1/ruby.rb +10 -4
  85. data/lib/bitcoin/silent_payment.rb +6 -3
  86. data/lib/bitcoin/slip39/share.rb +35 -9
  87. data/lib/bitcoin/slip39/sss.rb +26 -11
  88. data/lib/bitcoin/slip39.rb +13 -5
  89. data/lib/bitcoin/taproot/control_block.rb +6 -1
  90. data/lib/bitcoin/taproot/custom_depth_builder.rb +12 -0
  91. data/lib/bitcoin/tx.rb +35 -14
  92. data/lib/bitcoin/tx_in.rb +2 -2
  93. data/lib/bitcoin/tx_out.rb +2 -2
  94. data/lib/bitcoin/util.rb +49 -28
  95. data/lib/bitcoin/validation.rb +64 -12
  96. data/lib/bitcoin/version.rb +1 -1
  97. data/lib/bitcoin/wallet/account.rb +35 -11
  98. data/lib/bitcoin/wallet/base.rb +40 -15
  99. data/lib/bitcoin/wallet/utxo.rb +22 -4
  100. data/lib/bitcoin.rb +16 -4
  101. metadata +24 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5da84e4a456d4b5a47fba7e5cfcfceabbfbdb9fe757f6080519130d2ebabc41c
4
- data.tar.gz: 903301aa0eb0f349bc08c7133ebe80b9c731646fece6a407b75ace1bf6161aa8
3
+ metadata.gz: 3e3ac48f9f618bd15f40353c7ad8bf122f0600d2fc6ab53d07aaa0827cb8a766
4
+ data.tar.gz: 47c187e47c83ec2e13343200cd48ebee2690b4b2b67485115a199473390fdfa4
5
5
  SHA512:
6
- metadata.gz: 39a539b3380c6dba3ac091a2b3706508e790d16d4367ff91f715c9804ac5803dc56893f5aaf4c3bc4dec71d4a1c03a2d4da481e0b060c291a913a96cc3a7f1db
7
- data.tar.gz: dec61d2d90e67665aa4277de65b68a86c00779b820d5980e8d2facc85d0a0a4ba5c10989a43982fafc88463f40c02dfbfd3421b6f89f3e08620af4c008fa22fd
6
+ metadata.gz: af9833c6b9e3592ef67d41a0df565c0993654892629ff94842d111e1e04458f3c5386474735fa6056def1b8b3c6649dfc383688adb22fc041d52999e99eb26b5
7
+ data.tar.gz: '08a894cf0539aad6a5e01fc37c261f674f47e74c78c8491e8bbc600b451435c163aacc84816065dc35ccc7369074ce86fa3cb8aa6b3349297b2fb4e0db69e535'
data/bitcoinrb.gemspec CHANGED
@@ -20,14 +20,15 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ["lib"]
22
22
 
23
- spec.add_runtime_dependency 'ecdsa_ext', '~> 0.5.1'
23
+ spec.add_runtime_dependency 'ecdsa_ext', '~> 0.6.0'
24
24
  spec.add_runtime_dependency 'murmurhash3', '~> 0.1.7'
25
- spec.add_runtime_dependency 'bech32', '>= 1.5.0'
25
+ spec.add_runtime_dependency 'bech32', '>= 1.6.0'
26
26
  spec.add_runtime_dependency 'siphash'
27
- spec.add_runtime_dependency 'bip-schnorr', '>= 0.7.0'
27
+ spec.add_runtime_dependency 'bip-schnorr', '>= 0.8.0'
28
28
  spec.add_runtime_dependency 'base32', '>= 0.3.4'
29
29
  spec.add_runtime_dependency 'base64', '~> 0.2.0'
30
- spec.add_runtime_dependency 'secp256k1rb', '0.8.0'
30
+ spec.add_runtime_dependency 'bigdecimal'
31
+ spec.add_runtime_dependency 'secp256k1rb', '0.8.1'
31
32
  spec.add_runtime_dependency 'logger'
32
33
  spec.add_runtime_dependency 'merkle', '1.0.0'
33
34
  spec.add_runtime_dependency 'dnsruby', '1.73.0'
@@ -1,5 +1,6 @@
1
1
  require 'uri'
2
2
  require 'cgi'
3
+ require 'bigdecimal'
3
4
 
4
5
  module Bitcoin
5
6
  # BIP-321 URI
@@ -42,6 +43,7 @@ module Bitcoin
42
43
  if amount
43
44
  amount = BigDecimal(amount) if amount.is_a?(Integer)
44
45
  raise ArgumentError, "amount must be BigDecimal or integer." unless amount.is_a?(BigDecimal)
46
+ validate_amount!(amount)
45
47
  @amount = amount
46
48
  end
47
49
  raise ArgumentError, "label must be string." if label && !label.is_a?(String)
@@ -81,54 +83,105 @@ module Bitcoin
81
83
  end
82
84
 
83
85
  raise ArgumentError, "other_params must be Hash." unless other_params.is_a?(Hash)
84
- other_params.keys.each do |key|
85
- raise ArgumentError, 'An unsupported reqparam is included.' if key.start_with?('req-')
86
+ other_params.each do |key, value|
87
+ raise ArgumentError, 'other_params keys must be String.' unless key.is_a?(String)
88
+ raise ArgumentError, 'An unsupported reqparam is included.' if key.downcase.start_with?('req-')
89
+ # A key without a value is nil, and a key given more than once holds its values in an Array.
90
+ values = value.is_a?(Array) ? value : [value]
91
+ unless !values.empty? && values.all? { |v| v.nil? || v.is_a?(String) }
92
+ raise ArgumentError, "other_params['#{key}'] must be a String, nil or an Array of them."
93
+ end
86
94
  end
87
95
  @other_params = other_params
96
+
97
+ # BIP-321: the address may be left empty only if a query parameter carries a payment instruction.
98
+ # label, message, pop and amount describe a payment but are not one. A key this class does not know
99
+ # may be a payment instruction added after it, so it counts as one.
100
+ unless @addr || @query_addrs.any? || @lightning || @lno || @pay || @sp || @other_params.any?
101
+ raise ArgumentError, 'A URI without an address must have at least one payment instruction.'
102
+ end
88
103
  end
89
104
 
105
+ # The keys which hold one value. BIP-321 forbids a second label, message, pop or amount, and this
106
+ # class holds one value for the other payment instructions it knows.
107
+ SINGLE_VALUE_KEYS = %w[amount label message pop lightning lno pay sp].freeze
108
+ # The largest amount a URI may ask for, in BTC.
109
+ MAX_AMOUNT = BigDecimal(21_000_000)
110
+
90
111
  # Parse BIP-321 URI string.
112
+ # The scheme and the query keys are case-insensitive, values are percent-decoded as RFC 3986 says
113
+ # (a '+' stays a '+'), the amount has to match the grammar *digit ["." *digit], and a key which is not
114
+ # known here may appear more than once, in which case its values are collected in an Array.
91
115
  # @param [String] BIP-321 URI.
92
116
  # @return [Bitcoin::BIP321URI]
93
117
  # @raise [ArgumentError]
94
118
  def self.parse(uri)
95
119
  raise ArgumentError, "uri must be string." unless uri.is_a?(String)
96
120
  raise ArgumentError, "Invalid uri scheme." unless uri.downcase.start_with?('bitcoin:')
97
- uri = uri[8..-1]
98
- addr, params = uri.split('?', 2)
121
+ body, query = uri[8..-1].split('?', 2)
122
+ addr = body.nil? || body.empty? ? nil : body
123
+ params = {}
124
+ others = {}
99
125
  req_pop = false
100
126
  query_addrs = []
101
- params = if params
102
- decoded = URI.decode_www_form(params)
103
- decoded = decoded.map do |k, v|
104
- if k == 'req-pop'
105
- req_pop = true
106
- k = 'pop'
107
- end
108
- if %w[bc tb bcrt].include?(k.downcase)
109
- raise ArgumentError, "#{k} not allowed in current network." unless Bitcoin.chain_params.bech32_hrp == k.downcase
110
- query_addrs << v
111
- nil
112
- else
113
- [k, v]
114
- end
115
- end.compact
116
- keys = decoded.map(&:first)
117
- duplicate_key = keys.detect { |key| keys.count(key) > 1 }
118
- raise ArgumentError, "#{duplicate_key} must not appear twice." if duplicate_key
119
- decoded.to_h.except('')
120
- else
121
- {}
122
- end
123
- addr = nil if addr.empty?
124
- amount = params['amount'] ? BigDecimal(params['amount']) : nil
125
- excluded_keys = %w[amount label message pop lightning lno pay sp]
126
- others = params.except(*excluded_keys)
127
+ (query ? query.split('&') : []).each do |pair|
128
+ next if pair.empty?
129
+ raw_key, raw_value = pair.split('=', 2)
130
+ key = percent_decode(raw_key).downcase
131
+ next if key.empty?
132
+ value = raw_value.nil? ? nil : percent_decode(raw_value)
133
+ if key == 'req-pop'
134
+ req_pop = true
135
+ key = 'pop'
136
+ end
137
+ if %w[bc tb bcrt].include?(key)
138
+ raise ArgumentError, "#{key} not allowed in current network." unless Bitcoin.chain_params.bech32_hrp == key
139
+ # The key names the human readable part of the address it carries.
140
+ raise ArgumentError, "#{key} must hold a bech32 address with the prefix #{key}." unless value.to_s.downcase.start_with?("#{key}1")
141
+ query_addrs << value
142
+ elsif SINGLE_VALUE_KEYS.include?(key)
143
+ raise ArgumentError, "#{key} must not appear twice." if params.key?(key)
144
+ params[key] = value
145
+ else
146
+ raise ArgumentError, 'An unsupported reqparam is included.' if key.start_with?('req-')
147
+ # BIP-321: a key this implementation does not know may appear more than once. The values are kept in
148
+ # order, and a key given without a value is kept as nil.
149
+ others[key] = if others.key?(key)
150
+ values = others[key].is_a?(Array) ? others[key] : [others[key]]
151
+ values + [value]
152
+ else
153
+ value
154
+ end
155
+ end
156
+ end
157
+ amount = params['amount'] ? parse_amount(params['amount']) : nil
127
158
  BIP321URI.new(address: addr, amount: amount, label: params['label'], message: params['message'],
128
159
  pop: params['pop'], lightning: params['lightning'], lno: params['lno'],
129
160
  pay: params['pay'], sp: params['sp'], other_params: others, req_pop: req_pop, query_addrs: query_addrs)
130
161
  end
131
162
 
163
+ # Parse the value of an amount parameter.
164
+ # @param [String] str The value, which has to be *digit ["." *digit] with at most 8 decimal places.
165
+ # @return [BigDecimal]
166
+ # @raise [ArgumentError]
167
+ def self.parse_amount(str)
168
+ m = str.match(/\A([0-9]*)(?:\.([0-9]*))?\z/)
169
+ raise ArgumentError, "Invalid amount: #{str}" if m.nil? || (m[1].empty? && m[2].to_s.empty?)
170
+ raise ArgumentError, "Invalid amount: #{str}. An amount has at most 8 decimal places." if m[2].to_s.length > 8
171
+ amount = BigDecimal(str.end_with?('.') ? str + '0' : (str.start_with?('.') ? '0' + str : str))
172
+ raise ArgumentError, "Invalid amount: #{str}. It exceeds #{MAX_AMOUNT.to_i} BTC." if amount > MAX_AMOUNT
173
+ amount
174
+ end
175
+
176
+ # Percent-decode a URI component as RFC 3986 does. Unlike form decoding, a '+' is a literal '+'.
177
+ # @param [String] str
178
+ # @return [String]
179
+ def self.percent_decode(str)
180
+ URI.decode_www_form_component(str.gsub('+', '%2B'))
181
+ end
182
+
183
+ private_class_method :parse_amount, :percent_decode
184
+
132
185
  # Get all addresses contained in the URI body and query parameters
133
186
  # @return [Array] An array of address.
134
187
  def addresses
@@ -158,12 +211,22 @@ module Bitcoin
158
211
  base_params['pay'] = pay if pay
159
212
 
160
213
  all_params = base_params.merge other_params
161
- params = all_params.map do |k, v|
162
- "#{k}=#{CGI.escape(v).gsub('+', '%20')}"
214
+ params = all_params.flat_map do |k, values|
215
+ values = values.nil? ? [nil] : Array(values) # a key given without a value is written as the key alone
216
+ values.map { |v| v.nil? ? k : "#{k}=#{CGI.escape(v).gsub('+', '%20')}" }
163
217
  end
164
218
  params += query_addrs.map {|addr| "#{Bitcoin.chain_params.bech32_hrp}=#{addr}"}
165
219
  uri << "?#{params.join('&')}" unless params.empty?
166
220
  uri
167
221
  end
222
+
223
+ private
224
+
225
+ # @raise [ArgumentError] If +amount+ is negative, above MAX_AMOUNT or finer than a satoshi.
226
+ def validate_amount!(amount)
227
+ raise ArgumentError, 'amount must not be negative.' if amount.negative?
228
+ raise ArgumentError, "amount must not exceed #{MAX_AMOUNT.to_i} BTC." if amount > MAX_AMOUNT
229
+ raise ArgumentError, 'amount must not have more than 8 decimal places.' unless (amount * 100_000_000).frac.zero?
230
+ end
168
231
  end
169
232
  end
@@ -26,7 +26,9 @@ module Bitcoin
26
26
  u = key[0...32].bth
27
27
  t = key[32..-1].bth
28
28
  x = BIP324.xswiftec(u, t)
29
- Bitcoin::Key.new(pubkey: "03#{x}")
29
+ # The Y coordinate has the parity of t, as BIP-324 and libsecp256k1 decode it.
30
+ prefix = BIP324::FIELD.mod(t.hex).odd? ? '03' : '02'
31
+ Bitcoin::Key.new(pubkey: "#{prefix}#{x}", key_type: Bitcoin::Key::TYPES[:compressed])
30
32
  end
31
33
  end
32
34
 
@@ -121,6 +121,9 @@ module Bitcoin
121
121
  if (self.chunk_counter + 1) % rekey_interval == 0
122
122
  self.key = key_stream_bytes(32)
123
123
  self.block_counter = 0
124
+ # The rest of the current block belongs to the old key. The reference implementation starts the
125
+ # new key with a fresh keystream, and so does Bitcoin Core by seeking the cipher after SetKey.
126
+ self.key_stream = ''.b
124
127
  end
125
128
  self.chunk_counter += 1
126
129
  ret
@@ -99,7 +99,7 @@ module Bitcoin
99
99
  # @return [String] ElligatorSwift public key with hex format.
100
100
  def xelligatorswift(x)
101
101
  loop do
102
- u = SecureRandom.random_number(1..ECDSA::Group::Secp256k1.order).to_s(16)
102
+ u = SecureRandom.random_number(1...FIELD_SIZE).to_s(16) # a non-zero field element
103
103
  c = SecureRandom.random_number(CASE_COUNT)
104
104
  t = xswiftec_inv(x, u, c)
105
105
  unless t.nil?
@@ -18,23 +18,40 @@ module Bitcoin
18
18
  self.new(key)
19
19
  end
20
20
 
21
+ # The alphabet of the Base85 encoding BIP-85 uses, which is the one of RFC 1924 and Python's base64.b85encode.
22
+ BASE85_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~'.freeze
23
+
21
24
  # derive entropy
22
- # @param [String] path derive path.
25
+ # The applications of BIP-85 are understood: BIP39 (39'), HD-Seed WIF (2'), XPRV (32'), HEX (128169'),
26
+ # PWD BASE64 (707764') and PWD BASE85 (707785'). For any other application the 64 byte entropy is returned
27
+ # as it is.
28
+ # @param [String] path derive path. Every index has to be hardened.
23
29
  # @return [Tuple(String, Object)] a tuple of entropy with hex format and results depending the application.
30
+ # @raise [ArgumentError] If +path+ is not a BIP-85 path, or a parameter of the application is out of range.
24
31
  def derive(path)
25
32
  raise ArgumentError, "Invalid BIP85 path format." unless path.start_with?("m/83696968'")
33
+ numbers = parse_key_path(path)
34
+ raise ArgumentError, 'Every index of a BIP85 path must be hardened.' unless numbers.all? { |n| n >= HARDENED_THRESHOLD }
26
35
  derived_key = root_key
27
- parse_key_path(path).each{|num| derived_key = derived_key.derive(num)}
36
+ numbers.each{|num| derived_key = derived_key.derive(num)}
28
37
  derived_key = derived_key.priv
29
38
  entropy = Bitcoin.hmac_sha512("bip-entropy-from-k", derived_key.htb).bth
30
- app_no = path.split('/')[2]
39
+ app_no, *params = numbers[1..-1].map { |n| n - HARDENED_THRESHOLD }
31
40
  case app_no
32
- when "39'"
33
- bip39_entropy(path, entropy)
34
- when "2'"
41
+ when 39
42
+ bip39_entropy(params, entropy)
43
+ when 2
44
+ validate_params!(params, 1)
35
45
  hd_seed_entropy(entropy)
36
- when "32'"
46
+ when 32
47
+ validate_params!(params, 1)
37
48
  xprv_entropy(entropy)
49
+ when 128169
50
+ hex_entropy(params, entropy)
51
+ when 707764
52
+ pwd_base64_entropy(params, entropy)
53
+ when 707785
54
+ pwd_base85_entropy(params, entropy)
38
55
  else
39
56
  [entropy, entropy]
40
57
  end
@@ -46,21 +63,21 @@ module Bitcoin
46
63
  @root_key = root_key
47
64
  end
48
65
 
66
+ # Check that the application has +count+ parameters after its number, the last one being the index.
67
+ def validate_params!(params, count)
68
+ raise ArgumentError, "Invalid BIP85 path format." unless params.size == count
69
+ end
70
+
49
71
  # derive BIP39 entropy.
50
- def bip39_entropy(path, entropy)
51
- params = path.split('/')
52
- word_len = params[4]
53
- language = code_to_language(params[3])
54
- entropy = case word_len
55
- when "12'"
56
- entropy[0...32]
57
- when "18'"
58
- entropy[0...48]
59
- when "24'"
60
- entropy[0...64]
61
- else
62
- raise ArgumentError, "Word length #{word_len} does not supported."
63
- end
72
+ # @param [Array[Integer]] params language, word count and index.
73
+ def bip39_entropy(params, entropy)
74
+ validate_params!(params, 3)
75
+ language = code_to_language(params[0])
76
+ words = params[1]
77
+ unless Bitcoin::Mnemonic::WORD_COUNTS.include?(words)
78
+ raise ArgumentError, "Word length #{words}' does not supported."
79
+ end
80
+ entropy = entropy[0...(words / 3 * 8)] # 12 words use 128 bits, and every 3 words add 32 bits.
64
81
  mnemonic = Bitcoin::Mnemonic.new(language)
65
82
  [entropy, mnemonic.to_mnemonic(entropy)]
66
83
  end
@@ -84,25 +101,58 @@ module Bitcoin
84
101
  [entropy, ext_key.to_base58]
85
102
  end
86
103
 
104
+ # derive HEX entropy: the first num_bytes bytes, 16 <= num_bytes <= 64.
105
+ # @param [Array[Integer]] params num_bytes and index.
106
+ def hex_entropy(params, entropy)
107
+ validate_params!(params, 2)
108
+ num_bytes = params[0]
109
+ raise ArgumentError, "num_bytes #{num_bytes}' must be between 16 and 64." unless num_bytes.between?(16, 64)
110
+ result = entropy[0...(num_bytes * 2)]
111
+ [result, result]
112
+ end
113
+
114
+ # derive PWD BASE64: the first pwd_len characters of the Base64 encoding of the entropy, 20 <= pwd_len <= 86.
115
+ # @param [Array[Integer]] params pwd_len and index.
116
+ def pwd_base64_entropy(params, entropy)
117
+ validate_params!(params, 2)
118
+ pwd_len = params[0]
119
+ raise ArgumentError, "pwd_len #{pwd_len}' must be between 20 and 86." unless pwd_len.between?(20, 86)
120
+ [entropy, Base64.strict_encode64(entropy.htb)[0, pwd_len]]
121
+ end
122
+
123
+ # derive PWD BASE85: the first pwd_len characters of the Base85 encoding of the entropy, 10 <= pwd_len <= 80.
124
+ # @param [Array[Integer]] params pwd_len and index.
125
+ def pwd_base85_entropy(params, entropy)
126
+ validate_params!(params, 2)
127
+ pwd_len = params[0]
128
+ raise ArgumentError, "pwd_len #{pwd_len}' must be between 10 and 80." unless pwd_len.between?(10, 80)
129
+ [entropy, base85(entropy.htb)[0, pwd_len]]
130
+ end
131
+
132
+ # Base85 encode +data+, whose size is a multiple of 4 bytes, as Python's base64.b85encode does.
133
+ def base85(data)
134
+ data.unpack('N*').flat_map { |word| 5.times.map { |i| BASE85_ALPHABET[(word / 85**(4 - i)) % 85] } }.join
135
+ end
136
+
87
137
  # convert language code to language string.
88
138
  def code_to_language(code)
89
139
  case code
90
- when "0'"
140
+ when 0
91
141
  "english"
92
- when "1'"
142
+ when 1
93
143
  "japanese"
94
- when "3'"
144
+ when 3
95
145
  "spanish"
96
- when "4'"
146
+ when 4
97
147
  "chinese_simplified"
98
- when "5'"
148
+ when 5
99
149
  "chinese_traditional"
100
- when "6'"
150
+ when 6
101
151
  "french"
102
- when "7'"
152
+ when 7
103
153
  "italian"
104
154
  else
105
- raise ArgumentError, "bitcoinrb does not support language: #{code}"
155
+ raise ArgumentError, "bitcoinrb does not support language: #{code}'"
106
156
  end
107
157
  end
108
158
 
@@ -15,7 +15,7 @@ module Bitcoin
15
15
  end
16
16
 
17
17
  def write(data, nbits)
18
- raise "nbits must be between 0 and 64" if nbits < 0 || nbits > 64
18
+ raise ArgumentError, 'nbits must be between 0 and 64' if nbits < 0 || nbits > 64
19
19
  while nbits > 0
20
20
  bits = [8 - offset, nbits].min
21
21
  tmp = (data << (64 - nbits)) & 0b1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111
@@ -49,7 +49,7 @@ module Bitcoin
49
49
 
50
50
  # offset
51
51
  def read(nbits)
52
- raise 'nbits must be between 0 and 64' if nbits < 0 || nbits > 64
52
+ raise ArgumentError, 'nbits must be between 0 and 64' if nbits < 0 || nbits > 64
53
53
  data = 0
54
54
  while nbits > 0
55
55
  if offset == 8
data/lib/bitcoin/block.rb CHANGED
@@ -66,26 +66,62 @@ module Bitcoin
66
66
  end
67
67
 
68
68
  # check the merkle root in the block header matches merkle root calculated from tx list.
69
+ # A tx list with the duplication of CVE-2012-2459 is reported as invalid even though its root
70
+ # matches, as Bitcoin Core does, since the same root also belongs to the list without the duplicate.
71
+ # A block without any tx has no merkle root, and is not valid (bad-blk-length in Bitcoin Core).
69
72
  def valid_merkle_root?
70
- calculate_merkle_root == header.merkle_root
73
+ return false if transactions.empty?
74
+ !merkle_root_mutated? && calculate_merkle_root == header.merkle_root
75
+ end
76
+
77
+ # Whether the tx list is a mutation of another list with the same merkle root (CVE-2012-2459).
78
+ # Duplicating the last tx of a level with an odd count leaves the root unchanged, and it shows up as
79
+ # two identical hashes next to each other at some level of the tree. The last hash of an odd level is
80
+ # paired with itself by construction and is not a mutation. See ComputeMerkleRoot in Bitcoin Core.
81
+ # @return [Boolean]
82
+ def merkle_root_mutated?
83
+ hashes = transactions.map(&:tx_hash)
84
+ while hashes.size > 1
85
+ hashes.each_slice(2) { |left, right| return true if right && left == right }
86
+ hashes << hashes.last if hashes.size.odd?
87
+ hashes = hashes.each_slice(2).map { |left, right| Bitcoin.double_sha256([left + right].pack('H*')).bth }
88
+ end
89
+ false
71
90
  end
72
91
 
73
92
  # calculate merkle root from tx list.
93
+ # @raise [ArgumentError] If the block has no tx.
74
94
  def calculate_merkle_root
95
+ raise ArgumentError, 'A block without any tx has no merkle root.' if transactions.empty?
75
96
  tree = Merkle::BinaryTree.new(config: Merkle::Config.bitcoin(element_encoding: :hex), leaves: transactions.map(&:tx_hash))
76
97
  tree.compute_root
77
98
  end
78
99
 
79
100
  # check the witness commitment in coinbase tx matches witness commitment calculated from tx list.
101
+ # As in Bitcoin Core, the commitment is the last output of the coinbase which looks like one, and the
102
+ # witness of the coinbase input has to be exactly one item of 32 bytes, the witness reserved value.
103
+ # A block without a commitment output is not valid here.
80
104
  def valid_witness_commitment?
81
- transactions[0].witness_commitment == calculate_witness_commitment
105
+ return false if transactions.empty?
106
+ commitment = transactions[0].witness_commitment
107
+ return false if commitment.nil? || !valid_witness_reserved_value?
108
+ commitment == calculate_witness_commitment
109
+ end
110
+
111
+ # Whether the witness of the coinbase input is one item of 32 bytes, see valid_witness_commitment?.
112
+ # @return [Boolean]
113
+ def valid_witness_reserved_value?
114
+ stack = transactions[0]&.inputs&.first&.script_witness&.stack
115
+ !stack.nil? && stack.size == 1 && stack[0].bytesize == 32
82
116
  end
83
117
 
84
118
  # calculate witness commitment from tx list.
119
+ # @raise [ArgumentError] If the coinbase input does not carry the witness reserved value.
85
120
  def calculate_witness_commitment
121
+ raise ArgumentError, 'The witness reserved value is missing.' unless valid_witness_reserved_value?
86
122
  witness_hashes = [COINBASE_WTXID]
87
123
  witness_hashes += (transactions[1..-1].map(&:witness_hash))
88
- reserved_value = transactions[0].inputs[0].script_witness.stack.map(&:bth).join
124
+ reserved_value = transactions[0].inputs[0].script_witness.stack[0].bth
89
125
  tree = Merkle::BinaryTree.new(config: Merkle::Config.bitcoin(element_encoding: :hex), leaves: witness_hashes)
90
126
  root_hash = tree.compute_root
91
127
  Bitcoin.double_sha256([root_hash + reserved_value].pack('H*')).bth
@@ -96,10 +132,13 @@ module Bitcoin
96
132
  def height
97
133
  return nil if header.version < 2
98
134
  coinbase_tx = transactions[0]
99
- return nil unless coinbase_tx.coinbase_tx?
100
- buf = StringIO.new(coinbase_tx.inputs[0].script_sig.to_payload)
101
- len = Bitcoin.unpack_var_int_from_io(buf)
102
- buf.read(len).reverse.bth.to_i(16)
135
+ return nil unless coinbase_tx&.coinbase_tx?
136
+ # BIP-34: the height is the first push of the coinbase scriptSig, as a script number. A height from
137
+ # 1 to 16 is pushed with OP_1 to OP_16, which is not a data push.
138
+ chunk = coinbase_tx.inputs[0].script_sig.chunks.first
139
+ return nil if chunk.nil?
140
+ return Bitcoin::Script.decode_number(chunk.pushed_data.bth) if chunk.pushdata?
141
+ Bitcoin::Opcodes.opcode_to_small_int(chunk.opcode)
103
142
  end
104
143
 
105
144
  end
@@ -45,15 +45,16 @@ module Bitcoin
45
45
 
46
46
  # Decode Block Filter from encoded filter
47
47
  # @param [Integer] filter_type filter type.
48
- # @param [String] block_hash block hash with hex format. not little endian.
48
+ # @param [String] block_hash block hash with hex format, in the byte order Bitcoin::Block#block_hash returns it.
49
49
  # @param [String] encoded encoded_filter with hex format.
50
50
  # @return [Bitcoin::BlockFilter] block filter object.
51
+ # @raise [ArgumentError] If +encoded+ is not a well formed filter, see Bitcoin::GCSFilter.
51
52
  def self.decode(filter_type, block_hash, encoded)
52
53
  filter = case filter_type
53
54
  when TYPE[:basic]
54
55
  GCSFilter.new(block_hash.htb[0...16], BASIC_FILTER_P, BASIC_FILTER_M, encoded_filter: encoded)
55
56
  else
56
- raise "unknown filter type: #{filter_type}."
57
+ raise ArgumentError, "unknown filter type: #{filter_type}."
57
58
  end
58
59
  BlockFilter.new(filter_type, filter, block_hash)
59
60
  end
@@ -26,7 +26,7 @@ module Bitcoin
26
26
  # @return [Bitcoin::BlockHeader]
27
27
  def self.parse_from_payload(payload)
28
28
  buf = payload.is_a?(String) ? StringIO.new(payload) : payload
29
- version, prev_hash, merkle_root, time, bits, nonce = buf.read(80).unpack('Va32a32VVV')
29
+ version, prev_hash, merkle_root, time, bits, nonce = Bitcoin.read_exact(buf, 80).unpack('Va32a32VVV')
30
30
  new(version, prev_hash.bth, merkle_root.bth, time, bits, nonce)
31
31
  end
32
32
 
@@ -34,12 +34,30 @@ module Bitcoin
34
34
  [version, prev_hash.htb, merkle_root.htb, time, bits, nonce].pack('Va32a32VVV')
35
35
  end
36
36
 
37
+ # Decode a compact target the way Bitcoin Core's arith_uint256::SetCompact does.
38
+ # @param [Integer] compact The 32 bit compact encoding, such as the bits of a block header.
39
+ # @return [Integer] The target. It is negative when the sign bit is set, and it may not fit in
40
+ # 256 bits, see .compact_overflow?.
41
+ def self.decode_compact(compact)
42
+ exponent = (compact >> 24) & 0xff
43
+ mantissa = compact & 0x7fffff
44
+ target = exponent <= 3 ? mantissa >> (8 * (3 - exponent)) : mantissa << (8 * (exponent - 3))
45
+ (compact & 0x800000).zero? ? target : -target
46
+ end
47
+
48
+ # Whether the target of +compact+ does not fit in 256 bits, as Bitcoin Core's SetCompact reports it.
49
+ # @param [Integer] compact The 32 bit compact encoding.
50
+ # @return [Boolean]
51
+ def self.compact_overflow?(compact)
52
+ exponent = (compact >> 24) & 0xff
53
+ mantissa = compact & 0x7fffff
54
+ mantissa != 0 && (exponent > 34 || (mantissa > 0xff && exponent > 33) || (mantissa > 0xffff && exponent > 32))
55
+ end
56
+
37
57
  # compute difficulty target from bits.
58
+ # @return [Integer] see .decode_compact.
38
59
  def difficulty_target
39
- exponent = ((bits >> 24) & 0xff)
40
- mantissa = bits & 0x7fffff
41
- mantissa *= -1 if (bits & 0x800000) > 0
42
- (mantissa * 2 ** (8 * (exponent - 3)))
60
+ BlockHeader.decode_compact(bits)
43
61
  end
44
62
 
45
63
  def hash
@@ -60,9 +78,14 @@ module Bitcoin
60
78
  valid_pow? && valid_timestamp?
61
79
  end
62
80
 
63
- # evaluate valid proof of work.
81
+ # evaluate valid proof of work, as Bitcoin Core's CheckProofOfWork does: the target has to be positive,
82
+ # fit in 256 bits and be no easier than the proof of work limit of the current chain, and the block
83
+ # hash has to be no greater than the target.
64
84
  def valid_pow?
65
- block_id.hex < difficulty_target
85
+ target = difficulty_target
86
+ return false if target <= 0 || BlockHeader.compact_overflow?(bits)
87
+ return false if target > BlockHeader.decode_compact(Bitcoin.chain_params.proof_of_work_limit)
88
+ block_id.hex <= target
66
89
  end
67
90
 
68
91
  # evaluate valid timestamp.
@@ -75,7 +98,7 @@ module Bitcoin
75
98
  # @return [Integer] a chain work.
76
99
  def work
77
100
  target = difficulty_target
78
- return 0 if target < 1
101
+ return 0 if target < 1 || BlockHeader.compact_overflow?(bits)
79
102
  115792089237316195423570985008687907853269984665640564039457584007913129639936.div(target + 1) # 115792089237316195423570985008687907853269984665640564039457584007913129639936 is 2**256
80
103
  end
81
104
 
@@ -19,12 +19,15 @@ module Bitcoin
19
19
  # @param [Integer] elements_length the number of elements
20
20
  # @param [Float] fp_rate the false positive rate chosen by the client
21
21
  # @param [Integer] tweak A random value to add to the seed value in the hash function used by the bloom filter
22
+ # @raise [ArgumentError] If +elements_length+ is not a positive integer.
22
23
  def self.create_filter(elements_length, fp_rate, tweak = 0)
24
+ raise ArgumentError, 'elements_length must be a positive integer.' unless elements_length.is_a?(Integer) && elements_length > 0
23
25
  # The size S of the filter in bytes is given by (-1 / pow(log(2), 2) * N * log(P)) / 8
24
26
  len = [[(-elements_length * Math.log(fp_rate) / (LN2_SQUARED * 8)).to_i, MAX_BLOOM_FILTER_SIZE].min, 1].max
25
27
  filter = Array.new(len, 0)
26
- # The number of hash functions required is given by S * 8 / N * log(2)
27
- hash_funcs = [[(filter.size * 8 * LN2 / elements_length).to_i, MAX_HASH_FUNCS].min, 1].max
28
+ # The number of hash functions required is given by S * 8 / N * log(2).
29
+ # S * 8 / N is an integer division, as in Bitcoin Core's CBloomFilter, so the same parameters make the same filter.
30
+ hash_funcs = [[(filter.size * 8 / elements_length * LN2).to_i, MAX_HASH_FUNCS].min, 1].max
28
31
  BloomFilter.new(filter, hash_funcs, tweak)
29
32
  end
30
33
 
@@ -34,4 +34,3 @@ genesis:
34
34
  version: 1
35
35
  prev_hash: "0000000000000000000000000000000000000000000000000000000000000000"
36
36
  bip44_coin_type: 1
37
- dust_relay_fee: 3600
@@ -23,7 +23,7 @@ retarget_time: 1209600 # 2 weeks
23
23
  target_spacing: 600 # block interval
24
24
  max_money: 21000000
25
25
  bip34_height: 1
26
- proof_of_work_limit: 0x1d00ffff
26
+ proof_of_work_limit: 0x1e0377ae
27
27
  dns_seeds:
28
28
  - "seed.signet.bitcoin.sprovoost.nl"
29
29
  - "seed.signet.achownodes.xyz"