merkle 0.4.0 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 900af6789d1eda0ed4bf1c4d8b3575a3b7c5789d4e5a84abe43304c49122a92b
4
- data.tar.gz: d250d0b1ed630a82d8aa4fe66347319172d200b1416e95260a51a54135e539fc
3
+ metadata.gz: 9b9d4e8996bfda0b7e8baa3c2324f51222503f658fc43c066819058ef028bdc7
4
+ data.tar.gz: 107526e231fb5925e181c7747f8130489278684ecfd37355ccc869adbcdc6d97
5
5
  SHA512:
6
- metadata.gz: 2bd898709716daecc0b73f006e8d3367ad8a01d9e2a40fae32a1819cc62d85229ff671f38d217dd1ffef2f548be56ca485c42b097348ed95d382fde51fdc7743
7
- data.tar.gz: 3b95465023c7edb109836caa261ef4a0bdd376ab042742392f7381204d715111ce0f8fab01d6fb94d7917862341d9cec7d4b14d8f55dcb34f7af755dec6ef2ad
6
+ metadata.gz: 1467fc1c5715adfc418cc3ae5a44dab2c6dcad7b32f10d6fe2cd9eb264bace3d22d7f0f544cb87827048c67b6644a142c2b0754b5faaef2556966cf4493e515f
7
+ data.tar.gz: eb7621a6e1d771a96a219dcb283f379ace0c5528bb6ea5d8b1c08263b8e6f3f52166715848c35736aee449a1d6cbea890441a3c4f33861f7857a5ac664917c7b
data/README.md CHANGED
@@ -33,7 +33,12 @@ Or install it yourself as:
33
33
  require 'merkle'
34
34
 
35
35
  # Create configuration
36
- config = Merkle::Config.new(hash_type: :sha256)
36
+ # element_encoding says how the elements passed to .from_elements are read:
37
+ # :hex - elements are hex strings and are decoded before hashing
38
+ # :binary - elements are byte strings and are hashed as-is
39
+ # :auto - legacy mode, see "Upgrading from 0.4.0 and earlier"
40
+ # It has no default: guessing it silently changes the merkle root.
41
+ config = Merkle::Config.new(element_encoding: :binary, hash_type: :sha256)
37
42
 
38
43
  # Method 1: Using pre-hashed leaves
39
44
  leaves = [
@@ -74,12 +79,12 @@ tree = Merkle::BinaryTree.from_elements(
74
79
  root = tree.compute_root
75
80
  puts "Root from elements: #{root}"
76
81
 
77
- # With optional leaf tag for tagged hashing (e.g., Taproot)
78
- taproot_config = Merkle::Config.taptree
82
+ # Tags live on the config, so leaves and branches cannot drift apart.
83
+ # Config.taptree carries leaf_tag: 'TapLeaf' and branch_tag: 'TapBranch'.
84
+ taproot_config = Merkle::Config.taptree(element_encoding: :binary)
79
85
  tagged_tree = Merkle::AdaptiveTree.from_elements(
80
86
  config: taproot_config,
81
- elements: elements,
82
- leaf_tag: 'TapLeaf' # Optional tag for leaf hashing
87
+ elements: elements
83
88
  )
84
89
 
85
90
  # Generate and verify proof
@@ -105,10 +110,11 @@ puts "Adaptive tree proof valid: #{proof.valid?}"
105
110
  # This gives you precise control over how leaves are grouped
106
111
 
107
112
  # Example 1: Basic usage with pre-hashed leaves
108
- leaf_a = config.tagged_hash('A')
109
- leaf_b = config.tagged_hash('B')
110
- leaf_c = config.tagged_hash('C')
111
- leaf_d = config.tagged_hash('D')
113
+ # Leaves are always 64-character hex strings, the same form #compute_root returns.
114
+ leaf_a = config.tagged_hash(config.encode_element('A')).unpack1('H*')
115
+ leaf_b = config.tagged_hash(config.encode_element('B')).unpack1('H*')
116
+ leaf_c = config.tagged_hash(config.encode_element('C')).unpack1('H*')
117
+ leaf_d = config.tagged_hash(config.encode_element('D')).unpack1('H*')
112
118
 
113
119
  # Define structure: [[A, [B, C]], D]
114
120
  nested_leaves = [[leaf_a, [leaf_b, leaf_c]], leaf_d]
@@ -120,21 +126,24 @@ puts "Custom tree root: #{root}"
120
126
  # Valid structures:
121
127
  # - [A, B] → Simple binary node
122
128
  # - [[A, B], C] → Left subtree with right leaf
123
- # - [A] → Single child node
129
+ # - [A] → A tree holding a single leaf
124
130
  # Invalid: [A, B, C] → Error (max 2 children per node)
131
+ # Invalid: [[A, B]] → Error (a single-child node just passes its child's hash up,
132
+ # so it would commit to the same root as [A, B])
125
133
  ```
126
134
 
127
135
  ### Configuration Options
128
136
 
129
137
  ```ruby
130
138
  # Bitcoin-compatible configuration with double SHA256
131
- bitcoin_config = Merkle::Config.new(hash_type: :double_sha256)
139
+ bitcoin_config = Merkle::Config.new(element_encoding: :hex, hash_type: :double_sha256)
132
140
 
133
141
  # Configuration with tagged hashing (Taproot-style)
134
- taproot_config = Merkle::Config.taptree
142
+ taproot_config = Merkle::Config.taptree(element_encoding: :hex)
135
143
 
136
144
  # Configuration with non-sorted hashing (directions needed in proofs)
137
145
  non_sorted_config = Merkle::Config.new(
146
+ element_encoding: :binary,
138
147
  hash_type: :sha256,
139
148
  sort_hashes: false
140
149
  )
@@ -162,3 +171,75 @@ The library generates compact Merkle proofs that include:
162
171
  proof = tree.generate_proof(leaf_index)
163
172
  is_valid = proof.valid? # Returns true/false
164
173
  ```
174
+
175
+ `#valid?` folds `leaf` upwards through `siblings` and compares the result to `root`. It answers
176
+ "do these hashes chain to this root", and nothing more. In particular it does not check that
177
+ `leaf` sits at the bottom of the tree.
178
+
179
+ **The verifier must derive `leaf` itself.** Hash the data you care about and build the proof
180
+ around that value:
181
+
182
+ ```ruby
183
+ leaf = config.tagged_hash(config.encode_element(my_data), config.leaf_tag).unpack1('H*')
184
+ proof = Merkle::Proof.new(config: config, root: trusted_root, leaf: leaf,
185
+ siblings: received_siblings, directions: received_directions)
186
+ proof.valid?
187
+ ```
188
+
189
+ Taking `leaf` from whoever supplied the proof defeats it: any internal node of the tree is a
190
+ value that chains to the root, so it would be accepted as if it were a leaf.
191
+
192
+ ## Security considerations
193
+
194
+ This library lets you build trees that are not second-preimage resistant, because Bitcoin's
195
+ transaction merkle tree is one of them and cannot be changed. Two properties are left to the
196
+ protocol built on top of it:
197
+
198
+ - **Domain separation.** Give `leaf_tag` and `branch_tag` different values so that a leaf hash
199
+ can never equal an internal node hash. With both left empty, an attacker can craft an element
200
+ whose leaf hash equals an internal node and prove membership of something that was never in
201
+ the tree. `Config.taptree` sets both for you.
202
+ - **Duplicate leaves (CVE-2012-2459).** `BinaryTree` duplicates the last node when a level holds
203
+ an odd number of them, exactly as Bitcoin does, so `[a, b, c]` and `[a, b, c, c]` share a root.
204
+ Use `AdaptiveTree` or `CustomTree` if you do not need Bitcoin compatibility.
205
+
206
+ Element encoding, by contrast, is not left to guesswork: `element_encoding` is required on
207
+ `Config`, so `'hello'` and `'68656c6c6f'` cannot silently resolve to the same leaf.
208
+
209
+ ### Upgrading from 0.4.0 and earlier
210
+
211
+ `element_encoding` has no default, so every `Config` construction has to be updated. Pick the
212
+ value that matches what you were already passing to `.from_elements`:
213
+
214
+ | What you pass as elements | Use |
215
+ | --- | --- |
216
+ | Hex strings | `:hex` |
217
+ | Raw byte strings | `:binary` |
218
+ | A mix of both | `:auto` |
219
+
220
+ `:auto` reproduces the old behaviour exactly, including its collisions: `'hello'` and
221
+ `'68656c6c6f'` share a leaf under it, and so do `'AB'` and `'ab'`. Use it to keep verifying roots
222
+ you already committed to, not for a new protocol. It reproduces 0.4.0; 0.3.1 and earlier also
223
+ padded odd-length hex, so `'abc'` and `'abc0'` shared a leaf there and no mode reproduces that.
224
+
225
+ `leaf_tag` moved from `.from_elements` to `Config`, next to `branch_tag`, so a protocol's tag
226
+ spec lives in one place. Pass it to `Config.new` instead. `Config.taptree` now sets
227
+ `leaf_tag: 'TapLeaf'` itself: if you were calling it without passing a leaf tag, your leaves were
228
+ untagged and your roots were not BIP341 script trees. They are now, which changes those roots.
229
+
230
+ Leaves are now always 64-character hex strings. If you were passing binary digests
231
+ (`config.tagged_hash(...)`) as leaves, append `.unpack1('H*')`.
232
+
233
+ **`Config#tagged_hash` is the one change that does not announce itself.** It now hashes its
234
+ argument as bytes instead of decoding it when it looks like hex, so a direct call keeps working
235
+ and returns a different digest:
236
+
237
+ ```ruby
238
+ config.tagged_hash('deadbeef') # 0.4.0: hashed 4 bytes
239
+ config.tagged_hash(config.encode_element('deadbeef')) # 1.0.0: same 4 bytes, with :hex
240
+ ```
241
+
242
+ Everything reached through `.from_elements` already goes through `encode_element`, so this only
243
+ affects code that calls `tagged_hash` itself — typically to precompute leaves for `.new`. Audit
244
+ those call sites: wrap the argument in `encode_element` to keep the old digest, or leave it bare
245
+ if you were passing raw bytes all along.
@@ -15,7 +15,9 @@ module Merkle
15
15
  raise ArgumentError, 'config must be Merkle::Config' unless config.is_a?(Merkle::Config)
16
16
  raise ArgumentError, 'leaves must be Array' unless leaves.is_a?(Array)
17
17
  @config = config
18
- @leaves = leaves
18
+ # Copy, so that the array the caller keeps cannot change this tree's root behind its back.
19
+ @leaves = leaves.dup
20
+ validate_leaves!
19
21
  end
20
22
 
21
23
  # Create tree from +elements+. For each element in elements,
@@ -23,13 +25,12 @@ module Merkle
23
25
  # The resulting leaves are hex strings, the same representation as leaves passed to #initialize.
24
26
  # @param [Merkle::Config] config Configuration for merkle tree.
25
27
  # @param [Array] elements An array of element that will be hashed to become leaves.
26
- # @param [String] leaf_tag An optional tag to use when computing the leaf hash.
27
- def self.from_elements(config:, elements:, leaf_tag: '')
28
+ # The tag used for the leaf hash comes from +config.leaf_tag+.
29
+ def self.from_elements(config:, elements:)
28
30
  raise ArgumentError, 'config must be Merkle::Config' unless config.is_a?(Merkle::Config)
29
31
  raise ArgumentError, 'elements must be Array' unless elements.is_a?(Array)
30
- raise ArgumentError, 'leaf_tag must be string' unless leaf_tag.is_a?(String)
31
32
  leaves = elements.map do |element|
32
- config.tagged_hash(element, leaf_tag).unpack1('H*')
33
+ config.tagged_hash(config.encode_element(element), config.leaf_tag).unpack1('H*')
33
34
  end
34
35
  self.new(config: config, leaves: leaves)
35
36
  end
@@ -39,8 +40,7 @@ module Merkle
39
40
  # @raise [Merkle::Error] If leaves is empty.
40
41
  def compute_root
41
42
  raise Error, 'leaves is empty' if leaves.empty?
42
- # nodes = leaves
43
- nodes = leaves.map {|leaf| hex_to_bin(leaf) }
43
+ nodes = leaves.map {|leaf| decode_hash(leaf) }
44
44
  while nodes.length > 1
45
45
  nodes = build_next_level(nodes)
46
46
  end
@@ -64,6 +64,12 @@ module Merkle
64
64
 
65
65
  private
66
66
 
67
+ # Validate the leaves this tree was built with.
68
+ # @raise [ArgumentError] If any leaf is not a node hash.
69
+ def validate_leaves!
70
+ leaves.each { |leaf| decode_hash(leaf) }
71
+ end
72
+
67
73
  # Gets the siblings that corresponds to +leaf_index+ and its directions (if necessary).
68
74
  # @param [Integer] leaf_index The leaf index.
69
75
  # @return [Array] An array of siblings and directions.
@@ -12,7 +12,7 @@ module Merkle
12
12
  directions = []
13
13
 
14
14
  current_index = leaf_index
15
- nodes = leaves.map {|leaf| hex_to_bin(leaf) }
15
+ nodes = leaves.map {|leaf| decode_hash(leaf) }
16
16
 
17
17
  while nodes.length > 1
18
18
  # For adaptive tree, odd nodes are promoted to next level
@@ -13,7 +13,7 @@ module Merkle
13
13
  directions = []
14
14
 
15
15
  current_index = leaf_index
16
- nodes = leaves.map {|leaf| hex_to_bin(leaf) }
16
+ nodes = leaves.map {|leaf| decode_hash(leaf) }
17
17
 
18
18
  while nodes.length > 1
19
19
  # If odd number of nodes, duplicate the last one
data/lib/merkle/config.rb CHANGED
@@ -6,36 +6,79 @@ module Merkle
6
6
  # Supported Hash type.
7
7
  HASH_TYPES = [:sha256, :double_sha256]
8
8
 
9
- attr_reader :hash_type, :branch_tag, :sort_hashes
9
+ # How the elements passed to .from_elements are turned into bytes.
10
+ # :hex - each element is a hex string and is decoded before hashing.
11
+ # :binary - each element is already a byte string and is hashed as-is.
12
+ # :auto - each element is decoded if it looks like hex, otherwise hashed as-is.
13
+ #
14
+ # :auto exists to reproduce roots computed by 0.4.0 and earlier, where this was the only
15
+ # behaviour. Do not choose it for a new protocol: 'hello' and '68656c6c6f' resolve to the
16
+ # same leaf under it, and so do 'AB' and 'ab'. Note it reproduces 0.4.0, not 0.3.1 and
17
+ # earlier, which also padded odd-length hex ('abc' and 'abc0' shared a leaf there).
18
+ ELEMENT_ENCODINGS = [:hex, :binary, :auto]
19
+
20
+ attr_reader :hash_type, :leaf_tag, :branch_tag, :sort_hashes, :element_encoding
10
21
 
11
22
  # Constructor
23
+ # @param [Symbol] element_encoding How elements are interpreted, :hex, :binary or :auto.
24
+ # This has no default on purpose. Guessing it silently changes the merkle root.
25
+ # See ELEMENT_ENCODINGS before reaching for :auto.
12
26
  # @param [Symbol] hash_type The hashing algorithm used to hash the internal nodes.
27
+ # @param [String] leaf_tag Tag to use when hashing leaves.
28
+ # Give this and +branch_tag+ different values so that a leaf hash can never equal an internal
29
+ # node hash. With both left empty the tree is not second-preimage resistant.
13
30
  # @param [String] branch_tag Tags to use when hashing internal nodes.
14
31
  # @param [Boolean] sort_hashes Whether to sort internal nodes in lexicographical order and hash them.
15
32
  # If you enable this, Merkle::Proof's directions are not required.
16
33
  # @raise [ArgumentError]
17
- def initialize(hash_type: :sha256, branch_tag: '', sort_hashes: true)
34
+ def initialize(element_encoding:, hash_type: :sha256, leaf_tag: '', branch_tag: '', sort_hashes: true)
35
+ raise ArgumentError, "element_encoding #{element_encoding} does not supported." unless ELEMENT_ENCODINGS.include?(element_encoding)
18
36
  raise ArgumentError, "hash_type #{hash_type} does not supported." unless HASH_TYPES.include?(hash_type)
37
+ raise ArgumentError, "leaf_tag must be string." unless leaf_tag.is_a?(String)
19
38
  raise ArgumentError, "internal_tag must be string." unless branch_tag.is_a?(String)
20
39
  raise ArgumentError, "sort_hashes must be boolean." unless sort_hashes.is_a?(TrueClass) || sort_hashes.is_a?(FalseClass)
40
+ @element_encoding = element_encoding
21
41
  @hash_type = hash_type
22
- @branch_tag = branch_tag
42
+ # Freeze the tags. A config is shared by every tree built with it, so mutating one in place
43
+ # would change the root of all of them and invalidate proofs already handed out.
44
+ @leaf_tag = leaf_tag.dup.freeze
45
+ @branch_tag = branch_tag.dup.freeze
23
46
  @sort_hashes = sort_hashes
24
47
  end
25
48
 
26
49
  # Bitcoin configuration.
50
+ # @param [Symbol] element_encoding How elements are interpreted, :hex, :binary or :auto.
27
51
  # @return [Merkle::Config]
28
- def self.bitcoin
29
- Config.new(hash_type: :double_sha256, sort_hashes: false)
52
+ def self.bitcoin(element_encoding:)
53
+ Config.new(element_encoding: element_encoding, hash_type: :double_sha256, sort_hashes: false)
30
54
  end
31
55
 
32
56
  # Taptree configuration.
57
+ # @param [Symbol] element_encoding How elements are interpreted, :hex, :binary or :auto.
33
58
  # @return [Merkle::Config]
34
- def self.taptree
35
- Config.new(branch_tag: 'TapBranch')
59
+ def self.taptree(element_encoding:)
60
+ Config.new(element_encoding: element_encoding, leaf_tag: 'TapLeaf', branch_tag: 'TapBranch')
61
+ end
62
+
63
+ # Convert +element+ into the byte string to be hashed, following element_encoding.
64
+ # @param [String] element An element as given to .from_elements.
65
+ # @return [String] Byte string.
66
+ # @raise [ArgumentError] If +element+ does not match element_encoding.
67
+ def encode_element(element)
68
+ raise ArgumentError, "element must be string." unless element.is_a?(String)
69
+ case element_encoding
70
+ when :hex
71
+ raise ArgumentError, "element must be a hex string." unless hex_string?(element)
72
+ [element].pack('H*')
73
+ when :binary
74
+ element.b
75
+ when :auto
76
+ hex_string?(element) ? [element].pack('H*') : element.b
77
+ end
36
78
  end
37
79
 
38
- # Generate tagged hash.
80
+ # Generate tagged hash. +data+ is always hashed as a byte string.
81
+ # To hash an element written as hex, pass it through #encode_element first.
39
82
  # @param [String] data The data to be hashed.
40
83
  # @param [String] tag Tag string used tagging.
41
84
  # @return [String] Tagged hash value.
@@ -43,7 +86,7 @@ module Merkle
43
86
  raise ArgumentError, "data must be string." unless data.is_a?(String)
44
87
  raise ArgumentError, "tag must be a String." unless tag.is_a?(String)
45
88
 
46
- data_bin = hex_to_bin(data).b
89
+ data_bin = data.b
47
90
 
48
91
  unless tag.empty?
49
92
  tag_bin = Digest::SHA256.digest(tag).b
@@ -9,28 +9,29 @@ module Merkle
9
9
  # Each element can be a leaf hash (hex string) or an array of child nodes.
10
10
  def initialize(config:, leaves:)
11
11
  super(config: config, leaves: leaves)
12
- # Validate nested structure before calling super
13
- validate_leaves!(extract_leaves(leaves))
14
12
  end
15
13
 
16
14
  # Create tree from elements with custom structure
17
15
  # @param [Merkle::Config] config Configuration for merkle tree.
18
16
  # @param [Array] elements A nested array of elements that will be hashed to become leaves.
19
- # @param [String] leaf_tag An optional tag to use when computing the leaf hash.
20
- def self.from_elements(config:, elements:, leaf_tag: '')
17
+ # The tag used for the leaf hash comes from +config.leaf_tag+.
18
+ def self.from_elements(config:, elements:)
21
19
  raise ArgumentError, 'config must be Merkle::Config' unless config.is_a?(Merkle::Config)
22
20
  raise ArgumentError, 'elements must be Array' unless elements.is_a?(Array)
23
- raise ArgumentError, 'leaf_tag must be string' unless leaf_tag.is_a?(String)
24
-
25
- # Convert elements to hashes while preserving structure
26
- hashed_structure = convert_elements_to_hashes(elements, config, leaf_tag)
27
-
21
+
22
+ # Convert elements to hashes while preserving structure. This walks the input before the
23
+ # constructor gets to check it, so it has to enforce the depth limit itself.
24
+ hashed_structure = convert_elements_to_hashes(elements, config)
25
+
28
26
  self.new(config: config, leaves: hashed_structure)
29
27
  end
30
28
 
31
29
  # Compute merkle root using custom structure
32
30
  # @return [String] merkle root
33
31
  def compute_root
32
+ # Re-check here rather than trusting the constructor. +leaves+ is readable and its arrays
33
+ # are mutable, so a structure that was rejected at construction can be assembled afterwards.
34
+ validate_leaves!
34
35
  all_leaves = extract_leaves(@leaves)
35
36
  raise Error, 'leaves is empty' if all_leaves.empty?
36
37
  result = compute_node_hash(@leaves)
@@ -38,12 +39,13 @@ module Merkle
38
39
  end
39
40
 
40
41
  # Convert nested elements to nested hashes
41
- def self.convert_elements_to_hashes(node, config, leaf_tag)
42
+ def self.convert_elements_to_hashes(node, config, depth = 0)
42
43
  if node.is_a?(Array)
43
- node.map { |child| convert_elements_to_hashes(child, config, leaf_tag) }
44
+ raise ArgumentError, "Binary tree must not be deeper than #{MAX_DEPTH}" if depth >= MAX_DEPTH
45
+ node.map { |child| convert_elements_to_hashes(child, config, depth + 1) }
44
46
  else
45
47
  # This is a leaf element, hash it and convert to hex
46
- config.tagged_hash(node, leaf_tag).unpack1('H*')
48
+ config.tagged_hash(config.encode_element(node), config.leaf_tag).unpack1('H*')
47
49
  end
48
50
  end
49
51
 
@@ -72,12 +74,18 @@ module Merkle
72
74
  end
73
75
  else
74
76
  # Leaf node: already a hash, convert to binary
75
- hex_to_bin(node)
77
+ decode_hash(node)
76
78
  end
77
79
  end
78
80
 
81
+ private_class_method :convert_elements_to_hashes
82
+ private :compute_node_hash
83
+
79
84
  # Override generate_proof to work with nested structure
80
85
  def generate_proof(leaf_index)
86
+ # Walking the structure before checking it would hit the recursion limit on a structure
87
+ # assembled after construction, and SystemStackError escapes the caller's rescue.
88
+ validate_leaves!
81
89
  all_leaves = extract_leaves(@leaves)
82
90
  raise ArgumentError, 'leaf_index must be Integer' unless leaf_index.is_a?(Integer)
83
91
  raise ArgumentError, 'leaf_index out of range' if leaf_index < 0 || all_leaves.length <= leaf_index
@@ -107,23 +115,37 @@ module Merkle
107
115
  end
108
116
  end
109
117
 
110
- # Validate that all leaves are valid hex strings and structure is binary
111
- def validate_leaves!(leaves_to_validate)
112
- leaves_to_validate.each do |leaf|
113
- raise ArgumentError, "leaf hash must be string." unless leaf.is_a?(String)
114
- end
115
- validate_binary_structure(@leaves)
118
+ # Validate that the structure is a binary tree and that every leaf is a node hash.
119
+ def validate_leaves!
120
+ validate_binary_structure(@leaves, root: true)
121
+ extract_leaves(@leaves).each { |leaf| decode_hash(leaf) }
116
122
  end
117
-
118
- # Validate that the structure is a binary tree (max 2 children per node)
119
- def validate_binary_structure(node)
120
- if node.is_a?(Array)
121
- if node.length == 0
122
- raise ArgumentError, "Binary tree nodes cannot be empty"
123
- elsif node.length > 2
124
- raise ArgumentError, "Binary tree nodes can have at most 2 children, got #{node.length}"
123
+
124
+ # Validate that the structure is a binary tree (exactly 2 children per node)
125
+ # @param [Object] node A subtree (nested Array) or a leaf hash.
126
+ # @param [Boolean] root Whether +node+ is the whole tree.
127
+ # @param [Integer] depth How far below the root +node+ sits.
128
+ def validate_binary_structure(node, root: false, depth: 0)
129
+ return unless node.is_a?(Array)
130
+ # +depth+ counts the branches above this node, so a node here puts its children at
131
+ # depth + 1. Stopping at MAX_DEPTH keeps the deepest leaf within MAX_DEPTH branches,
132
+ # which is what Proof::MAX_SIBLINGS allows a proof to carry.
133
+ raise ArgumentError, "Binary tree must not be deeper than #{MAX_DEPTH}" if depth >= MAX_DEPTH
134
+
135
+ case node.length
136
+ when 0
137
+ raise ArgumentError, "Binary tree nodes cannot be empty"
138
+ when 1
139
+ # A node with one child contributes no branch hash, it just passes the child up.
140
+ # That would let [[a]] and [a, [b]] commit to the same root as [a] and [a, b].
141
+ # A tree holding a single leaf is the one case where there is nothing to confuse it with.
142
+ unless root && !node[0].is_a?(Array)
143
+ raise ArgumentError, "Binary tree nodes must have 2 children unless the tree is a single leaf"
125
144
  end
126
- node.each { |child| validate_binary_structure(child) }
145
+ when 2
146
+ node.each { |child| validate_binary_structure(child, depth: depth + 1) }
147
+ else
148
+ raise ArgumentError, "Binary tree nodes can have at most 2 children, got #{node.length}"
127
149
  end
128
150
  end
129
151
 
data/lib/merkle/proof.rb CHANGED
@@ -4,8 +4,9 @@ module Merkle
4
4
 
5
5
  # Upper bound on the number of siblings, i.e. the depth of the tree the proof came from.
6
6
  # A proof longer than this cannot correspond to any realistic tree, so it is rejected
7
- # rather than hashed.
8
- MAX_SIBLINGS = 64
7
+ # rather than hashed. It matches the deepest tree the library will build, which is also the
8
+ # deepest a BIP341 script tree can be: a control block carries at most 128 path elements.
9
+ MAX_SIBLINGS = MAX_DEPTH
9
10
 
10
11
  attr_reader :config, :root, :leaf, :siblings, :directions
11
12
 
@@ -19,12 +20,14 @@ module Merkle
19
20
  def initialize(config:, root:, leaf:, siblings:, directions: [])
20
21
  raise ArgumentError, 'config must be a Merkle::Config' unless config.is_a?(Merkle::Config)
21
22
  raise ArgumentError, 'root must be string' unless root.is_a?(String)
23
+ raise ArgumentError, "root must be a #{HASH_SIZE * 2}-character hex string" unless node_hash?(root)
22
24
  raise ArgumentError, 'leaf must be string' unless leaf.is_a?(String)
25
+ raise ArgumentError, "leaf must be a #{HASH_SIZE * 2}-character hex string" unless node_hash?(leaf)
23
26
  raise ArgumentError, 'siblings must be an Array' unless siblings.is_a?(Array)
24
27
  raise ArgumentError, "siblings must not exceed #{MAX_SIBLINGS} elements" if siblings.length > MAX_SIBLINGS
25
28
  siblings.each do |sibling|
26
29
  raise ArgumentError, 'sibling must be string' unless sibling.is_a?(String)
27
- raise ArgumentError, 'sibling must be a 64-character hex string' unless sibling.match?(/\A[0-9a-fA-F]{64}\z/)
30
+ raise ArgumentError, "sibling must be a #{HASH_SIZE * 2}-character hex string" unless node_hash?(sibling)
28
31
  end
29
32
  raise ArgumentError, 'directions must be an Array' unless directions.is_a?(Array)
30
33
  raise ArgumentError, 'No directions are required because sorted_hash is enabled' if config.sort_hashes && !directions.empty?
@@ -33,19 +36,22 @@ module Merkle
33
36
  raise ArgumentError, 'direction must be 0 or 1' unless directions.all? { |direction| direction == 0 || direction == 1 }
34
37
  end
35
38
  @config = config
36
- @root = root
37
- @leaf = leaf
38
- @siblings = siblings
39
- @directions = directions
39
+ # Normalize and freeze. The checks above only bind if the arrays cannot grow afterwards:
40
+ # a caller holding the array it passed in could otherwise append past MAX_SIBLINGS, or
41
+ # shorten directions until #valid? reads nil and folds as if the sibling were on the right.
42
+ @root = normalize_hash(root)
43
+ @leaf = normalize_hash(leaf)
44
+ @siblings = siblings.map { |sibling| normalize_hash(sibling) }.freeze
45
+ @directions = directions.dup.freeze
40
46
  end
41
47
 
42
48
  # Verify the proof.
43
49
  # @return [Boolean] true if the proof is valid, false otherwise.
44
50
  def valid?
45
- current = hex_to_bin(leaf)
51
+ current = decode_hash(leaf)
46
52
 
47
53
  siblings.each_with_index do |sibling, index|
48
- sibling_bin = hex_to_bin(sibling)
54
+ sibling_bin = decode_hash(sibling)
49
55
 
50
56
  if config.sort_hashes
51
57
  # Sort lexicographically when combining
@@ -59,7 +65,9 @@ module Merkle
59
65
  current = config.tagged_hash(combined)
60
66
  end
61
67
 
62
- current.unpack1('H*') == root
68
+ # Compare the decoded bytes. Comparing the hex would make the result depend on the case
69
+ # the caller happened to write +root+ in, even though both spell the same hash.
70
+ current == decode_hash(root)
63
71
  end
64
72
 
65
73
  end
data/lib/merkle/util.rb CHANGED
@@ -1,6 +1,14 @@
1
1
  module Merkle
2
2
  module Util
3
3
 
4
+ # Size of a node hash in bytes. Leaves, siblings and internal nodes are all this size.
5
+ HASH_SIZE = 32
6
+
7
+ # Deepest tree accepted. A tree deeper than this cannot be walked without risking a
8
+ # SystemStackError, which is not a StandardError and so escapes a caller's rescue.
9
+ # 128 is also the deepest a BIP341 script tree can be.
10
+ MAX_DEPTH = 128
11
+
4
12
  # Check whether +data+ is hex string or not.
5
13
  # An odd-length string is not a hex string. Treating it as one would let
6
14
  # +pack('H*')+ pad the missing nibble with zero, so 'abc' and 'abc0' would
@@ -10,16 +18,44 @@ module Merkle
10
18
  # @raise [ArgumentError]
11
19
  def hex_string?(data)
12
20
  raise ArgumentError, 'data must be string' unless data.is_a?(String)
13
- data.length.even? && data.match?(/\A[0-9a-fA-F]+\z/)
21
+ # Match on the bytes. Matching the string itself raises Encoding::CompatibilityError for a
22
+ # UTF-16 string and ArgumentError for invalid UTF-8, neither of which the caller expects.
23
+ bytes = data.b
24
+ bytes.bytesize.even? && bytes.match?(/\A[0-9a-fA-F]+\z/)
14
25
  end
15
26
 
16
- # Convert hex string +data+ to binary.
17
- # @param [String] data
18
- # @return [String]
27
+ # Check whether +hex+ is the hex representation of a node hash.
28
+ # @param [String] hex
29
+ # @return [Boolean]
30
+ def node_hash?(hex)
31
+ return false unless hex.is_a?(String)
32
+ # Match on the bytes. Matching the string itself raises on a value that claims to be UTF-8
33
+ # but holds invalid bytes, which would surface as an unrelated ArgumentError.
34
+ bytes = hex.b
35
+ bytes.bytesize == HASH_SIZE * 2 && bytes.match?(/\A[0-9a-fA-F]+\z/)
36
+ end
37
+
38
+ # Convert a node hash from its hex representation to binary.
39
+ # Node hashes are always +HASH_SIZE+ bytes written as hex, so anything else is rejected
40
+ # rather than guessed at. Accepting arbitrary lengths here would make the concatenation
41
+ # in an internal node ambiguous: ['aa', 'bbcc'] and ['aabb', 'cc'] would hash alike.
42
+ # @param [String] hex
43
+ # @return [String] Binary format hash.
19
44
  # @raise [ArgumentError]
20
- def hex_to_bin(data)
21
- raise ArgumentError, 'data must be string' unless data.is_a?(String)
22
- hex_string?(data) ? [data].pack('H*') : data
45
+ def decode_hash(hex)
46
+ raise ArgumentError, 'hash must be string' unless hex.is_a?(String)
47
+ raise ArgumentError, "hash must be a #{HASH_SIZE * 2}-character hex string" unless node_hash?(hex)
48
+ [hex].pack('H*')
49
+ end
50
+
51
+ # Rewrite a node hash in the one spelling the library uses: lower case hex.
52
+ # Upper case names the same hash, so returning it verbatim would let a caller that matches
53
+ # leaves as strings miss a leaf whose proof verifies.
54
+ # @param [String] hex
55
+ # @return [String] Frozen lower case hex.
56
+ # @raise [ArgumentError]
57
+ def normalize_hash(hex)
58
+ bin_to_hex(decode_hash(hex)).freeze
23
59
  end
24
60
 
25
61
  # Convert binary string +data+ to hex string.
@@ -28,7 +64,7 @@ module Merkle
28
64
  # @raise [ArgumentError]
29
65
  def bin_to_hex(data)
30
66
  raise ArgumentError, 'data must be string' unless data.is_a?(String)
31
- hex_string?(data) ? data : data.unpack1('H*')
67
+ data.unpack1('H*')
32
68
  end
33
69
 
34
70
  # Combine two elements(+left+ and +right+) with sort configuration.
@@ -51,4 +87,4 @@ module Merkle
51
87
  end
52
88
 
53
89
  end
54
- end
90
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Merkle
4
- VERSION = "0.4.0"
4
+ VERSION = "1.0.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: merkle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - azuchi