merkle 0.3.1 → 0.4.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: ce4faaabf24a85b734bd6cfe35081dfcc897bf418c5b391e68a84f8ec0aa9977
4
- data.tar.gz: 944144e97463b2d0ad1ff7b2a882210548786035f52fa0090d505bbad7b9f88e
3
+ metadata.gz: 900af6789d1eda0ed4bf1c4d8b3575a3b7c5789d4e5a84abe43304c49122a92b
4
+ data.tar.gz: d250d0b1ed630a82d8aa4fe66347319172d200b1416e95260a51a54135e539fc
5
5
  SHA512:
6
- metadata.gz: 49988d95199cf3e6a5e5944f69205d6b354ed4e5d811325c4e6ca8d4a868044b6c3dfc380491d1aa9c577ca7848770ea3c7e56d9212a1bb2a60cbc5c330a0060
7
- data.tar.gz: 107fc3472cca81407b6fbc97a0981711f5538cdb2e657a032911d7866021fa37a4a30ac00ce5bcbebe2585849f700fc01c892c41670f619150d809c578fbb007
6
+ metadata.gz: 2bd898709716daecc0b73f006e8d3367ad8a01d9e2a40fae32a1819cc62d85229ff671f38d217dd1ffef2f548be56ca485c42b097348ed95d382fde51fdc7743
7
+ data.tar.gz: 3b95465023c7edb109836caa261ef4a0bdd376ab042742392f7381204d715111ce0f8fab01d6fb94d7917862341d9cec7d4b14d8f55dcb34f7af755dec6ef2ad
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-3.4.5
1
+ ruby-4.0.0
@@ -20,6 +20,7 @@ module Merkle
20
20
 
21
21
  # Create tree from +elements+. For each element in elements,
22
22
  # we compute a tagged hash, which becomes the leaf value.
23
+ # The resulting leaves are hex strings, the same representation as leaves passed to #initialize.
23
24
  # @param [Merkle::Config] config Configuration for merkle tree.
24
25
  # @param [Array] elements An array of element that will be hashed to become leaves.
25
26
  # @param [String] leaf_tag An optional tag to use when computing the leaf hash.
@@ -28,7 +29,7 @@ module Merkle
28
29
  raise ArgumentError, 'elements must be Array' unless elements.is_a?(Array)
29
30
  raise ArgumentError, 'leaf_tag must be string' unless leaf_tag.is_a?(String)
30
31
  leaves = elements.map do |element|
31
- config.tagged_hash(element, leaf_tag)
32
+ config.tagged_hash(element, leaf_tag).unpack1('H*')
32
33
  end
33
34
  self.new(config: config, leaves: leaves)
34
35
  end
@@ -129,77 +129,49 @@ module Merkle
129
129
 
130
130
  # Override siblings_with_directions for proof generation
131
131
  def siblings_with_directions(leaf_index)
132
- all_leaves = extract_leaves(@leaves)
133
- target_leaf = all_leaves[leaf_index]
134
132
  siblings = []
135
133
  directions = []
136
-
137
- # Build proof by finding the path to the target leaf
138
- proof_path = build_proof_path(@leaves, target_leaf)
139
-
140
- proof_path.each do |level_info|
141
- next if level_info[:siblings].empty?
142
-
143
- level_info[:siblings].each do |sibling|
144
- siblings << sibling[:hash]
145
- directions << sibling[:direction]
146
- end
147
- end
148
-
134
+ collect_path(@leaves, leaf_index, siblings, directions)
149
135
  [siblings, directions]
150
136
  end
151
-
152
- # Build the proof path with siblings at each level
153
- def build_proof_path(node, target_leaf, path = [])
154
- if node.is_a?(Array)
155
- # Find which child contains the target
156
- node.each_with_index do |child, idx|
157
- child_path = build_proof_path(child, target_leaf, path)
158
-
159
- if child_path
160
- # Found the path, now collect siblings at this level
161
- level_siblings = []
162
- node.each_with_index do |sibling, sibling_idx|
163
- next if sibling_idx == idx # Skip the path we're on
164
-
165
- sibling_hash = compute_node_hash(sibling)
166
- direction = sibling_idx < idx ? 0 : 1
167
- level_siblings << { hash: sibling_hash, direction: direction }
168
- end
169
-
170
- return child_path + [{ siblings: level_siblings }]
171
- end
172
- end
173
- nil
174
- else
175
- # Leaf node
176
- if node == target_leaf
177
- []
178
- else
179
- nil
180
- end
137
+
138
+ # Walk down the structure toward the leaf at +index+ (counted within +node+), collecting the
139
+ # sibling hash and its direction at each branch. Siblings are collected deepest first, the
140
+ # order Proof#valid? folds them in.
141
+ # The descent is driven by the index rather than by the leaf value, so duplicate leaf hashes
142
+ # still yield the proof for the requested position.
143
+ # @param [Object] node A subtree (nested Array) or a leaf hash.
144
+ # @param [Integer] index The leaf index within +node+.
145
+ # @param [Array] siblings Collected sibling hashes(binary format).
146
+ # @param [Array] directions Collected directions(0: left, 1: right).
147
+ def collect_path(node, index, siblings, directions)
148
+ return unless node.is_a?(Array)
149
+
150
+ if node.length == 1
151
+ # Single child contributes no sibling, its hash is passed through unchanged.
152
+ return collect_path(node[0], index, siblings, directions)
181
153
  end
182
- end
183
154
 
184
- # Find the leaf index by searching through the tree
185
- def find_leaf_index(node, target_leaf, current_index = [0])
186
- if node.is_a?(Array)
187
- node.each do |child|
188
- result = find_leaf_index(child, target_leaf, current_index)
189
- return result if result
190
- end
191
- nil
155
+ left, right = node
156
+ left_leaves = leaf_count(left)
157
+ if index < left_leaves
158
+ collect_path(left, index, siblings, directions)
159
+ siblings << compute_node_hash(right)
160
+ directions << 1 # sibling is on the right
192
161
  else
193
- # This is a leaf
194
- if node == target_leaf
195
- current_index[0]
196
- else
197
- current_index[0] += 1
198
- nil
199
- end
162
+ collect_path(right, index - left_leaves, siblings, directions)
163
+ siblings << compute_node_hash(left)
164
+ directions << 0 # sibling is on the left
200
165
  end
201
166
  end
202
167
 
168
+ # Count the leaves under +node+.
169
+ # @param [Object] node A subtree (nested Array) or a leaf hash.
170
+ # @return [Integer] Number of leaves.
171
+ def leaf_count(node)
172
+ node.is_a?(Array) ? node.sum { |child| leaf_count(child) } : 1
173
+ end
174
+
203
175
  # Not used in custom tree - structure is determined by nested array
204
176
  def build_next_level(nodes)
205
177
  raise NotImplementedError, "CustomTree uses structure-based computation"
data/lib/merkle/proof.rb CHANGED
@@ -2,21 +2,36 @@ module Merkle
2
2
  class Proof
3
3
  include Util
4
4
 
5
+ # Upper bound on the number of siblings, i.e. the depth of the tree the proof came from.
6
+ # A proof longer than this cannot correspond to any realistic tree, so it is rejected
7
+ # rather than hashed.
8
+ MAX_SIBLINGS = 64
9
+
5
10
  attr_reader :config, :root, :leaf, :siblings, :directions
6
11
 
7
12
  # Constructor.
8
13
  # @param [Merkle::Config] config
9
14
  # @param [String] root
10
15
  # @param [String] leaf
11
- # @param [Array] siblings
16
+ # @param [Array] siblings An array of sibling hashes(64-character hex strings).
12
17
  # @param [Array] directions Array of positions at each level(0: left, 1: right),
13
18
  # only required if sort_hashes is false in config.
14
19
  def initialize(config:, root:, leaf:, siblings:, directions: [])
15
20
  raise ArgumentError, 'config must be a Merkle::Config' unless config.is_a?(Merkle::Config)
16
21
  raise ArgumentError, 'root must be string' unless root.is_a?(String)
17
22
  raise ArgumentError, 'leaf must be string' unless leaf.is_a?(String)
23
+ raise ArgumentError, 'siblings must be an Array' unless siblings.is_a?(Array)
24
+ raise ArgumentError, "siblings must not exceed #{MAX_SIBLINGS} elements" if siblings.length > MAX_SIBLINGS
25
+ siblings.each do |sibling|
26
+ 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/)
28
+ end
18
29
  raise ArgumentError, 'directions must be an Array' unless directions.is_a?(Array)
19
30
  raise ArgumentError, 'No directions are required because sorted_hash is enabled' if config.sort_hashes && !directions.empty?
31
+ unless config.sort_hashes
32
+ raise ArgumentError, 'directions must have the same length as siblings' unless directions.length == siblings.length
33
+ raise ArgumentError, 'direction must be 0 or 1' unless directions.all? { |direction| direction == 0 || direction == 1 }
34
+ end
20
35
  @config = config
21
36
  @root = root
22
37
  @leaf = leaf
data/lib/merkle/util.rb CHANGED
@@ -2,12 +2,15 @@ module Merkle
2
2
  module Util
3
3
 
4
4
  # Check whether +data+ is hex string or not.
5
+ # An odd-length string is not a hex string. Treating it as one would let
6
+ # +pack('H*')+ pad the missing nibble with zero, so 'abc' and 'abc0' would
7
+ # collide.
5
8
  # @param [String] data
6
9
  # @return [Boolean]
7
10
  # @raise [ArgumentError]
8
11
  def hex_string?(data)
9
12
  raise ArgumentError, 'data must be string' unless data.is_a?(String)
10
- data.match?(/\A[0-9a-fA-F]+\z/)
13
+ data.length.even? && data.match?(/\A[0-9a-fA-F]+\z/)
11
14
  end
12
15
 
13
16
  # Convert hex string +data+ to binary.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Merkle
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.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.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - azuchi
@@ -54,7 +54,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
54
54
  - !ruby/object:Gem::Version
55
55
  version: '0'
56
56
  requirements: []
57
- rubygems_version: 3.6.9
57
+ rubygems_version: 4.0.3
58
58
  specification_version: 4
59
59
  summary: A Ruby library for Merkle trees
60
60
  test_files: []