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
@@ -60,7 +60,16 @@ module Bitcoin
60
60
  SCRIPT_VERIFY_DISCOURAGE_OP_SUCCESS = (1 << 19) # Making unknown OP_SUCCESS non-standard
61
61
  SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE = (1 << 20) # Making unknown public key versions (in BIP 342 scripts) non-standard
62
62
 
63
- MANDATORY_SCRIPT_VERIFY_FLAGS = SCRIPT_VERIFY_P2SH
63
+ # Script verification flags of the rules which every block has to follow, the soft forks which are now consensus.
64
+ MANDATORY_SCRIPT_VERIFY_FLAGS = [
65
+ SCRIPT_VERIFY_P2SH,
66
+ SCRIPT_VERIFY_DERSIG,
67
+ SCRIPT_VERIFY_NULLDUMMY,
68
+ SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY,
69
+ SCRIPT_VERIFY_CHECKSEQUENCEVERIFY,
70
+ SCRIPT_VERIFY_WITNESS,
71
+ SCRIPT_VERIFY_TAPROOT
72
+ ].inject(SCRIPT_VERIFY_NONE){|flags, f| flags |= f}
64
73
 
65
74
  # Standard script verification flags that standard transactions will comply with.
66
75
  STANDARD_SCRIPT_VERIFY_FLAGS = [
@@ -127,8 +136,9 @@ module Bitcoin
127
136
  # Maximum number length in bytes
128
137
  DEFAULT_MAX_NUM_SIZE = 4
129
138
 
130
- # 80 bytes of data, +1 for OP_RETURN, +2 for the pushdata opcodes.
131
- MAX_OP_RETURN_RELAY = 83
139
+ # The default -datacarriersize of Bitcoin Core, the total size of the OP_RETURN scriptPubKeys a standard tx may have.
140
+ # It is MAX_STANDARD_TX_WEIGHT / WITNESS_SCALE_FACTOR, so that the size of a standard tx is the only limit.
141
+ MAX_OP_RETURN_RELAY = 100_000
132
142
 
133
143
  # for script error
134
144
  SCRIPT_ERR_OK = 0
@@ -7,8 +7,20 @@ module Bitcoin
7
7
  :combo
8
8
  end
9
9
 
10
- def to_hex
10
+ def initialize(key)
11
+ super(key)
12
+ validate_not_xonly!(key)
13
+ end
14
+
15
+ # combo() stands for several scripts, see #to_scripts.
16
+ # @raise [ArgumentError] always.
17
+ def to_script
11
18
  raise ArgumentError, 'musig() is not allowed in top-level combo().' if musig?
19
+ raise ArgumentError, 'combo() expands to several scripts. Use #to_scripts.'
20
+ end
21
+
22
+ def to_hex
23
+ to_script
12
24
  end
13
25
 
14
26
  def to_scripts
@@ -54,16 +54,17 @@ module Bitcoin
54
54
  if key.start_with?('[') # BIP32 fingerprint
55
55
  raise ArgumentError, "Multiple ']' characters found for a single pubkey." if key.count('[') > 1 || key.count(']') > 1
56
56
  info = key[1...key.index(']')]
57
- fingerprint, *paths = info.split('/')
58
- raise ArgumentError, "Fingerprint '#{fingerprint}' is not hex." unless fingerprint.valid_hex?
57
+ fingerprint, *paths = info.split('/', -1) # keep a trailing empty element, so that a trailing '/' is rejected
59
58
  raise ArgumentError, "Fingerprint '#{fingerprint}' is not 4 bytes." unless fingerprint.size == 8
59
+ raise ArgumentError, "Fingerprint '#{fingerprint}' is not hex." unless fingerprint.valid_hex?
60
+ paths.each { |path| parse_path_element(path) } # The origin path is not derived, but it has to be a path.
60
61
  key = key[(key.index(']') + 1)..-1]
61
62
  else
62
63
  raise ArgumentError, 'Invalid key origin.' if key.include?(']')
63
64
  end
64
65
 
65
66
  # check BIP32 derivation path
66
- key, *paths = key.split('/')
67
+ key, *paths = key.split('/', -1)
67
68
 
68
69
  raise ArgumentError, "No key provided." unless key
69
70
 
@@ -97,24 +98,43 @@ module Bitcoin
97
98
  def derive_path(key, paths)
98
99
  is_private = key.is_a?(Bitcoin::ExtKey)
99
100
  paths.each do |path|
100
- raise ArgumentError, 'xpub can not derive hardened key.' if !is_private && path.end_with?("'")
101
101
  raise ArgumentError, 'Key ranges are not supported.' if path.include?("*")
102
102
  raise ArgumentError, 'Key multipath are not supported.' if path.include?("<")
103
- if is_private
104
- hardened = path.end_with?("'")
105
- path = hardened ? path[0..-2] : path
106
- raise ArgumentError, 'Key path value is not a valid value.' unless path =~ /^[0-9]+$/
107
- raise ArgumentError, 'Key path value is out of range.' if !hardened && path.to_i >= Bitcoin::HARDENED_THRESHOLD
108
- key = key.derive(path.to_i, hardened)
109
- else
110
- raise ArgumentError, 'Key path value is not a valid value.' unless path =~ /^[0-9]+$/
111
- raise ArgumentError, 'Key path value is out of range.' if path.to_i >= Bitcoin::HARDENED_THRESHOLD
112
- key = key.derive(path.to_i)
113
- end
103
+ index, hardened = parse_path_element(path)
104
+ raise ArgumentError, 'xpub can not derive hardened key.' if hardened && !is_private
105
+ key = is_private ? key.derive(index, hardened) : key.derive(index)
114
106
  end
115
107
  key
116
108
  end
117
109
 
110
+ # Parse one element of a derivation path. A hardened index is marked with ' or h, as BIP-380 allows.
111
+ # @param [String] path The element, e.g. "0", "0'" or "0h".
112
+ # @return [Array(Integer, Boolean)] the index and whether it is hardened.
113
+ # @raise [ArgumentError] If it is not a number below 2^31.
114
+ def parse_path_element(path)
115
+ hardened = path.end_with?("'", 'h')
116
+ index = hardened ? path[0..-2] : path
117
+ raise ArgumentError, 'Key path value is not a valid value.' unless index =~ /\A[0-9]+\z/
118
+ raise ArgumentError, 'Key path value is out of range.' if index.to_i >= Bitcoin::HARDENED_THRESHOLD
119
+ [index.to_i, hardened]
120
+ end
121
+
122
+ # Whether +key+ is given as a 32 byte x-only public key. Such a key is only valid inside tr().
123
+ # @param [String, MuSig] key KEY string.
124
+ # @return [Boolean]
125
+ def xonly_key?(key)
126
+ return false unless key.is_a?(String)
127
+ key = key[(key.index(']') + 1)..-1] if key.include?(']')
128
+ key = key.split('/').first.to_s
129
+ key.length == 64 && key.valid_hex?
130
+ end
131
+
132
+ # Raise unless +key+ is usable outside tr().
133
+ # @raise [ArgumentError]
134
+ def validate_not_xonly!(key)
135
+ raise ArgumentError, 'x-only public key is only allowed inside tr().' if xonly_key?(key)
136
+ end
137
+
118
138
  def ==(other)
119
139
  return false unless other.is_a?(Expression)
120
140
  type == other.type && to_script == other.to_script
@@ -37,12 +37,20 @@ module Bitcoin
37
37
 
38
38
  private
39
39
 
40
+ # Whether the keys may be x-only. Only the tapscript variants allow it.
41
+ def xonly_allowed?
42
+ false
43
+ end
44
+
40
45
  def validate!(threshold, keys)
41
46
  raise ArgumentError, "Multisig threshold '#{threshold}' is not valid." unless threshold.is_a?(Integer)
42
47
  raise ArgumentError, 'Multisig threshold cannot be 0, must be at least 1.' unless threshold > 0
43
48
  raise ArgumentError, 'Multisig threshold cannot be larger than the number of keys.' if threshold > keys.size
44
49
  raise ArgumentError, "Multisig must have between 1 and #{Bitcoin::MAX_PUBKEYS_PER_MULTISIG} keys, inclusive." if keys.size > Bitcoin::MAX_PUBKEYS_PER_MULTISIG
45
- keys.each{|key| extract_pubkey(key) }
50
+ keys.each do |key|
51
+ validate_not_xonly!(key) unless xonly_allowed?
52
+ extract_pubkey(key)
53
+ end
46
54
  end
47
55
  end
48
56
  end
@@ -19,6 +19,10 @@ module Bitcoin
19
19
 
20
20
  private
21
21
 
22
+ def xonly_allowed?
23
+ true
24
+ end
25
+
22
26
  def multisig_script(keys)
23
27
  script = Bitcoin::Script.new
24
28
  keys.each.with_index do |k, i|
@@ -18,6 +18,7 @@ module Bitcoin
18
18
  # @return [Bitcoin::Descriptor::MuSig]
19
19
  def initialize(keys, path = nil)
20
20
  raise ArgumentError, "keys must be an array." unless keys.is_a?(Array)
21
+ raise ArgumentError, 'musig() requires at least one key.' if keys.empty?
21
22
  unless path.nil?
22
23
  raise ArgumentError, "path must be String." unless path.is_a?(String)
23
24
  raise ArgumentError, "path must be start with /." unless path.start_with?("/")
@@ -17,9 +17,16 @@ module Bitcoin
17
17
 
18
18
  def to_hex
19
19
  raise ArgumentError, 'musig() is not allowed in top-level pk().' if musig?
20
+ validate_not_xonly!(key)
20
21
  super
21
22
  end
22
23
 
24
+ # Whether the key is given as an x-only key, which only a tr() tree can hold.
25
+ # @return [Boolean]
26
+ def xonly_key?(k = key)
27
+ super(k)
28
+ end
29
+
23
30
  # Convert to bitcoin script.
24
31
  # @return [Bitcoin::Script]
25
32
  def to_script
@@ -7,6 +7,11 @@ module Bitcoin
7
7
  :pkh
8
8
  end
9
9
 
10
+ def initialize(key)
11
+ super(key)
12
+ validate_not_xonly!(key)
13
+ end
14
+
10
15
  def to_hex
11
16
  raise ArgumentError, 'musig() is not allowed in top-level pkh().' if musig?
12
17
  super
@@ -8,7 +8,7 @@ module Bitcoin
8
8
  # @param [String] hex
9
9
  def initialize(hex)
10
10
  raise ArgumentError, "Raw script must be string." unless hex.is_a?(String)
11
- raise ArgumentError, "Raw script is not hex." unless hex.valid_hex?
11
+ raise ArgumentError, "Raw script is not hex." if hex.empty? || !hex.valid_hex?
12
12
  @hex = hex
13
13
  end
14
14
 
@@ -20,6 +20,7 @@ module Bitcoin
20
20
  def validate!(script)
21
21
  super(script)
22
22
  raise ArgumentError, 'A function is needed within P2SH.' unless script.is_a?(Expression)
23
+ raise ArgumentError, 'x-only public key is only allowed inside tr().' if script.is_a?(Pk) && script.xonly_key?
23
24
  script_size = script.to_script.size
24
25
  if script_size > Bitcoin::MAX_SCRIPT_ELEMENT_SIZE
25
26
  raise ArgumentError,
@@ -92,10 +92,10 @@ module Bitcoin
92
92
  end
93
93
 
94
94
  # Silent payment descriptors do not produce a single script.
95
- # Use to_address to get the silent payment address.
95
+ # Use to_addr to get the silent payment address.
96
96
  # @raise [RuntimeError]
97
97
  def to_script
98
- raise RuntimeError, "sp() descriptor does not produce a fixed script. Use to_address instead."
98
+ raise RuntimeError, "sp() descriptor does not produce a fixed script. Use to_addr instead."
99
99
  end
100
100
 
101
101
  private
@@ -55,7 +55,7 @@ module Bitcoin
55
55
  if item.is_a?(Array)
56
56
  items << parse_tree_items(item)
57
57
  elsif item.is_a?(Expression)
58
- item.xonly = true
58
+ item.xonly = true if item.respond_to?(:xonly=)
59
59
  items << Bitcoin::Taproot::LeafNode.new(item.to_script)
60
60
  else
61
61
  raise RuntimeError, "Unsupported item #{item}"
@@ -65,8 +65,10 @@ module Bitcoin
65
65
  end
66
66
 
67
67
  def validate_tree!(tree)
68
- return if tree.nil? || tree.is_a?(Expression)
69
- if tree.is_a?(Array)
68
+ return if tree.nil?
69
+ if tree.is_a?(Expression)
70
+ validate_leaf!(tree)
71
+ elsif tree.is_a?(Array)
70
72
  tree.each do |item|
71
73
  validate_tree!(item)
72
74
  end
@@ -75,6 +77,30 @@ module Bitcoin
75
77
  end
76
78
  end
77
79
 
80
+ # Check that +leaf+ can be a tapscript leaf. Bitcoin Core allows pk(), multi_a(), sortedmulti_a() and
81
+ # miniscript inside tr(); miniscript is not supported here. Any other expression, such as a wrapper
82
+ # which produces a scriptPubKey, would end up as a tapleaf nobody can spend.
83
+ # @param [Bitcoin::Descriptor::Expression] leaf
84
+ # @raise [ArgumentError]
85
+ def validate_leaf!(leaf)
86
+ message = case leaf.type
87
+ when :pk, :multi_a, :sortedmulti_a then nil
88
+ when :multi, :sortedmulti then 'Can only have multi/sortedmulti at top level, in sh(), or in wsh().'
89
+ when :wpkh then 'Can only have wpkh() at top level or inside sh().'
90
+ when :sh then 'Can only have sh() at top level.'
91
+ when :wsh then 'Can only have wsh() at top level or inside sh().'
92
+ when :addr then 'Can only have addr() at top level.'
93
+ when :combo then 'Can only have combo() at top level.'
94
+ when :raw then 'Can only have raw() at top level.'
95
+ when :tr then 'Can only have tr() at top level.'
96
+ when :rawtr then 'Can only have rawtr() at top level.'
97
+ else "#{leaf.type}() is not allowed in a tr() tree."
98
+ end
99
+ raise ArgumentError, message if message
100
+ keys = leaf.is_a?(KeyExpression) ? [leaf.extracted_key] : leaf.keys.map { |k| leaf.extract_pubkey(k) }
101
+ raise ArgumentError, "Uncompressed key are not allowed." unless keys.all?(&:compressed?)
102
+ end
103
+
78
104
  def tree_string(tree)
79
105
  buffer = '{'
80
106
  left, right = tree
@@ -5,6 +5,7 @@ module Bitcoin
5
5
  def initialize(key)
6
6
  raise ArgumentError, 'musig() is not allowed in wpkh().' if key.is_a?(MuSig)
7
7
  super(key)
8
+ validate_not_xonly!(key)
8
9
  raise ArgumentError, "Uncompressed key are not allowed." unless extract_pubkey(key).compressed?
9
10
  end
10
11
 
@@ -18,6 +18,7 @@ module Bitcoin
18
18
  def validate!(script)
19
19
  super(script)
20
20
  raise ArgumentError, 'A function is needed within P2WSH.' unless script.is_a?(Expression)
21
+ raise ArgumentError, 'x-only public key is only allowed inside tr().' if script.is_a?(Pk) && script.xonly_key?
21
22
  if script.is_a?(Wpkh) || script.is_a?(Wsh)
22
23
  raise ArgumentError, "Can only have #{script.type}() at top level or inside sh()."
23
24
  end
@@ -153,8 +153,13 @@ module Bitcoin
153
153
  def parse(string, top_level = true)
154
154
  validate_checksum!(string)
155
155
  content, _ = string.split('#')
156
- exp, args_str, path = content.match(/(\w+)\((.+)\)(.*)/).captures # split "tag(10, 20)/0/1"
156
+ matched = content.to_s.match(/\A(\w+)\((.*)\)(.*)\z/) # split "tag(10, 20)/0/1"
157
+ raise ArgumentError, "Invalid descriptor format: '#{string}'." unless matched
158
+ exp, args_str, path = matched.captures
157
159
  raise ArgumentError, 'Invalid format.' if exp != 'musig' && !path.empty?
160
+ if %w[pk pkh wpkh sh wsh combo raw addr rawtr].include?(exp) && split_two(args_str)[1]
161
+ raise ArgumentError, "#{exp}() takes one argument."
162
+ end
158
163
 
159
164
  case exp
160
165
  when 'pk'
@@ -175,6 +180,8 @@ module Bitcoin
175
180
  combo(args_str)
176
181
  when 'multi', 'sortedmulti', 'multi_a', 'sortedmulti_a'
177
182
  args = args_str.split(',')
183
+ # Only a plain decimal number, as ToIntegral in Bitcoin Core: no sign, no space, no other characters.
184
+ raise ArgumentError, "Multisig threshold '#{args[0]}' is not valid." unless args[0] =~ /\A[0-9]+\z/
178
185
  threshold = args[0].to_i
179
186
  keys = args[1..-1]
180
187
  case exp
@@ -196,13 +203,7 @@ module Bitcoin
196
203
  when 'tr'
197
204
  key, tree = split_two(args_str)
198
205
  key = parse(key) if key.include?('(')
199
- if tree.nil?
200
- tr(key)
201
- elsif tree.start_with?('{')
202
- tr(key, parse_nested_string(tree))
203
- else
204
- tr(key, parse(tree, false))
205
- end
206
+ tree.nil? ? tr(key) : tr(key, parse_tree_string(tree))
206
207
  when 'rawtr'
207
208
  if args_str.include?('(')
208
209
  rawtr(parse(args_str, false))
@@ -242,35 +243,58 @@ module Bitcoin
242
243
  end
243
244
  end
244
245
 
245
- def parse_nested_string(string)
246
- return nil if string.nil?
247
- stack = []
248
- current = []
249
- buffer = ""
250
- string.each_char do |c|
251
- case c
252
- when '{'
253
- stack << current
254
- current = []
255
- when '}'
256
- unless buffer.empty?
257
- current << parse(buffer, false)
258
- buffer = ""
259
- end
260
- nested = current
261
- current = stack.pop
262
- current << nested
263
- when ','
264
- unless buffer.empty?
265
- current << parse(buffer, false)
266
- buffer = ""
267
- end
268
- else
269
- buffer << c
246
+ # Parse the script tree of a tr() expression the way Bitcoin Core does. A tree is a script expression or
247
+ # '{' tree ',' tree '}', and nothing may follow it.
248
+ # @param [String] string The arguments of tr() after the key.
249
+ # @return [Bitcoin::Descriptor::Expression, Array] A leaf, or a two element Array of subtrees.
250
+ # @raise [ArgumentError] If +string+ is not a tree.
251
+ def parse_tree_string(string)
252
+ rest = string
253
+ branches = [] # one entry per open brace: false while in its left subtree, true while in its right one
254
+ lefts = [] # the finished left subtree of every brace which is in its right subtree
255
+ tree = loop do
256
+ while rest.start_with?('{')
257
+ rest = rest[1..]
258
+ branches << false
259
+ raise ArgumentError, "tr() supports at most #{TAPROOT_CONTROL_MAX_NODE_COUNT} nesting levels." if branches.size > TAPROOT_CONTROL_MAX_NODE_COUNT
260
+ end
261
+ leaf, rest = take_expression(rest)
262
+ raise ArgumentError, 'tr(): expected script expression.' if leaf.empty?
263
+ node = parse(leaf, false)
264
+ # Close every brace whose right subtree this finishes.
265
+ while branches.any? && branches.last
266
+ raise ArgumentError, "tr(): expected '}' after script expression." unless rest.start_with?('}')
267
+ rest = rest[1..]
268
+ branches.pop
269
+ node = [lefts.pop, node]
270
+ end
271
+ break node if branches.empty?
272
+ # This finishes the left subtree of a brace, so its right one follows a comma.
273
+ raise ArgumentError, "tr(): expected ',' after script expression." unless rest.start_with?(',')
274
+ rest = rest[1..]
275
+ branches[-1] = true
276
+ lefts << node
277
+ end
278
+ raise ArgumentError, "tr(): expected ')' after script expression." unless rest.empty?
279
+ tree
280
+ end
281
+
282
+ # Split off the leading script or key expression of +string+. It ends at the first ',', ')' or '}'
283
+ # outside any parentheses or braces, as Expr does in Bitcoin Core.
284
+ # @param [String] string
285
+ # @return [Array] The expression and the rest of +string+.
286
+ def take_expression(string)
287
+ level = 0
288
+ string.each_char.with_index do |c, i|
289
+ if c == '(' || c == '{'
290
+ level += 1
291
+ elsif level > 0 && (c == ')' || c == '}')
292
+ level -= 1
293
+ elsif level.zero? && (c == ')' || c == '}' || c == ',')
294
+ return [string[0...i], string[i..]]
270
295
  end
271
296
  end
272
- current << parse(buffer, false) unless buffer.empty?
273
- current.first
297
+ [string, '']
274
298
  end
275
299
 
276
300
  def split_two(content)
@@ -6,7 +6,10 @@ module Bitcoin
6
6
 
7
7
  # resize array content with +initial_value+.
8
8
  # expect to behave like vec#resize in c++.
9
+ # @raise [ArgumentError] If +new_size+ is not a non-negative integer. A negative size would
10
+ # otherwise empty the array.
9
11
  def resize!(new_size, initial_value = 0)
12
+ raise ArgumentError, 'new_size must be a non-negative integer.' unless new_size.is_a?(Integer) && new_size >= 0
10
13
  if size < new_size
11
14
  (new_size - size).times{self.<< initial_value}
12
15
  elsif size > new_size
@@ -21,7 +21,13 @@ module ::ECDSA::Format::PointOctetString
21
21
  raise ECDSA::Format::DecodeError, 'Point octet string is empty.' if string.empty?
22
22
  if [6, 7].include?(string[0].ord)
23
23
  raise ECDSA::Format::DecodeError, 'Unrecognized start byte for point octet string: 0x%x' % string[0].ord unless allow_hybrid
24
- decode_uncompressed string, group if allow_hybrid
24
+ point = decode_uncompressed(string, group)
25
+ # A hybrid encoding carries the parity of y in its first byte, 0x06 for even and 0x07 for odd, and
26
+ # libsecp256k1 rejects the point when the byte does not match the y it carries.
27
+ unless point.infinity? || (string[0].ord == 6) == point.y.even?
28
+ raise ECDSA::Format::DecodeError, 'Hybrid point octet string has a parity byte which does not match y.'
29
+ end
30
+ point
25
31
  else
26
32
  base_decode(string, group)
27
33
  end
@@ -21,6 +21,9 @@ module Bitcoin
21
21
  # generate master key from seed.
22
22
  # @params [String] seed a seed data with hex format.
23
23
  def self.generate_master(seed)
24
+ # BIP-32: the seed is between 128 and 512 bits.
25
+ raise ArgumentError, 'The seed must be a hex string.' unless seed.is_a?(String)
26
+ raise ArgumentError, 'The seed must be 16 to 64 bytes.' unless (16..64).cover?(seed.htb.bytesize)
24
27
  ext_key = ExtKey.new
25
28
  ext_key.depth = ext_key.number = 0
26
29
  ext_key.parent_fingerprint = MASTER_FINGERPRINT
@@ -95,6 +98,10 @@ module Bitcoin
95
98
  # @param [Boolean] harden whether hardened key or not. If true, 2^31 is added to +number+.
96
99
  # @return [Bitcoin::ExtKey] derived new key.
97
100
  def derive(number, harden = false)
101
+ # A child index is 31 bits plus the hardened bit. Anything else was packed with 'N' and silently
102
+ # wrapped, giving a key whose serialized index differs from the one it was derived with.
103
+ limit = harden ? Bitcoin::HARDENED_THRESHOLD : 2**32
104
+ raise ArgumentError, 'Child index is out of range.' unless number.is_a?(Integer) && number >= 0 && number < limit
98
105
  number += Bitcoin::HARDENED_THRESHOLD if harden
99
106
  new_key = ExtKey.new
100
107
  new_key.depth = depth + 1
@@ -144,6 +151,7 @@ module Bitcoin
144
151
  end
145
152
 
146
153
  def self.parse_from_payload(payload)
154
+ raise ArgumentError, 'Invalid extended key length.' unless payload.bytesize == BIP32_EXTKEY_WITH_VERSION_SIZE
147
155
  buf = StringIO.new(payload)
148
156
  ext_key = ExtKey.new
149
157
  ext_key.ver = buf.read(4).bth # version
@@ -273,6 +281,7 @@ module Bitcoin
273
281
 
274
282
  # derive child key
275
283
  def derive(number)
284
+ raise ArgumentError, 'Child index is out of range.' unless number.is_a?(Integer) && number >= 0 && number < 2**32
276
285
  new_key = ExtPubkey.new
277
286
  new_key.depth = depth + 1
278
287
  raise IndexError, 'Depth over 255.' if new_key.depth > Bitcoin::ExtKey::MAX_DEPTH
@@ -318,6 +327,7 @@ module Bitcoin
318
327
  end
319
328
 
320
329
  def self.parse_from_payload(payload)
330
+ raise ArgumentError, 'Invalid extended key length.' unless payload.bytesize == BIP32_EXTKEY_WITH_VERSION_SIZE
321
331
  buf = StringIO.new(payload)
322
332
  ext_pubkey = ExtPubkey.new
323
333
  ext_pubkey.ver = buf.read(4).bth # version
@@ -21,14 +21,18 @@ module Bitcoin
21
21
  # @param [Array] elements the filter elements.
22
22
  # @param [String] encoded_filter encoded filter with hex format.
23
23
  # @return [Bitcoin::GCSFilter]
24
+ # @raise [ArgumentError] If a parameter is out of range, or +encoded_filter+ is not the encoding of
25
+ # exactly N elements, as Bitcoin Core's GCSFilter checks it.
24
26
  def initialize(key, p, m, elements: nil, encoded_filter: nil)
25
- raise 'specify either elements or encoded_filter.' if elements.nil? && encoded_filter.nil?
26
- raise 'p must be <= 32' if p > 32
27
- @key = key
27
+ raise ArgumentError, 'specify either elements or encoded_filter.' if elements.nil? && encoded_filter.nil?
28
+ raise ArgumentError, 'key must be 16 bytes.' unless key.is_a?(String) && key.bytesize == 16
29
+ raise ArgumentError, 'p must be between 0 and 32.' unless p.is_a?(Integer) && p.between?(0, 32)
30
+ raise ArgumentError, 'm must be a positive integer.' unless m.is_a?(Integer) && m > 0
31
+ @key = key.b
28
32
  @p = p
29
33
  @m = m
30
34
  if elements
31
- raise 'elements size must be < 2**32.' if elements.size >= MAX_ELEMENTS_SIZE
35
+ raise ArgumentError, 'elements size must be < 2**32.' if elements.size >= MAX_ELEMENTS_SIZE
32
36
  @n = elements.size
33
37
  encoded = Bitcoin.pack_var_int(@n)
34
38
  bit_writer = Bitcoin::BitStreamWriter.new
@@ -45,8 +49,19 @@ module Bitcoin
45
49
  encoded << bit_writer.stream
46
50
  @encoded = encoded.bth
47
51
  else
52
+ raise ArgumentError, 'encoded_filter must be a hex string.' unless encoded_filter.is_a?(String) && encoded_filter.valid_hex?
48
53
  @encoded = encoded_filter
49
- @n, payload = Bitcoin.unpack_var_int(encoded_filter.htb)
54
+ buf = StringIO.new(encoded_filter.htb)
55
+ @n = Bitcoin.unpack_var_int_from_io(buf)
56
+ raise ArgumentError, 'N must be < 2**32.' if @n >= MAX_ELEMENTS_SIZE
57
+ # The filter has to hold exactly N elements: neither fewer, nor bytes after the last one.
58
+ bit_reader = Bitcoin::BitStreamReader.new(buf.read)
59
+ begin
60
+ @n.times { golomb_rice_decode(bit_reader, p) }
61
+ rescue IOError
62
+ raise ArgumentError, 'encoded_filter is truncated.'
63
+ end
64
+ raise ArgumentError, 'encoded_filter contains excess data.' unless bit_reader.stream.eof?
50
65
  end
51
66
  end
52
67
 
@@ -59,7 +74,7 @@ module Bitcoin
59
74
  # @param [String] element with binary format.
60
75
  # @return [Integer]
61
76
  def hash_to_range(element)
62
- hash = SipHash.digest(key, element)
77
+ hash = SipHash.digest(key, element.b) # bytes, whatever the encoding of the string says
63
78
  map_into_range(hash, f)
64
79
  end
65
80