merkle 0.2.0 → 0.3.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: acfb1daee054131392665bdb186173e4d9facf8d8a25f0d082d3d18bdb3a9b51
4
- data.tar.gz: cb94b561dc7dfbffd9a951a94c8a015710d388f7f49466808db0bdf583d1f5cb
3
+ metadata.gz: eaa0d4d13ee5bd6ff5e8b81aeb50396b8932f6efea765cc64dd1ff19614ebd9e
4
+ data.tar.gz: aa59ca4b87da23aa28707d4434209b36c03a7942b7635af036253c2dd7161607
5
5
  SHA512:
6
- metadata.gz: 3aae0a25fdfca015e89a4123bf7956ec3b163ae3dce0cebed93652d0eb90babd9651043fedcd12a6f346b08d15e558f2480fd9325d39294cdbcb5739594d1537
7
- data.tar.gz: 1db01cd9b1de1bac9c1193d12e47dc324011dc3578f3b49620dc47bac7ed920da606227af0a954f5de0f0704775b7ebb90224da63a34757bd1ac2ed9b3cc86e9
6
+ metadata.gz: fcf1016cc2bdbc721185ca647219c036cf05f13ad4534b4775d0ebb9ef3d585a9b97f1b40aac46b8f32b4aa3071821ba32d9ba7147ba86bf6c3a49eb0550290e
7
+ data.tar.gz: 36aec619bb2280e91b015409933c57d050787535a6890a4ee41efd86b6380af3300f81e488e2d5871fbb617b2af955ca4c9ba2437f012107fcff6c8f94eb3ecb
data/README.md CHANGED
@@ -107,10 +107,10 @@ bitcoin_config = Merkle::Config.new(hash_type: :double_sha256)
107
107
  # Configuration with tagged hashing (Taproot-style)
108
108
  taproot_config = Merkle::Config.taptree
109
109
 
110
- # Configuration with sorted hashing (no directions needed in proofs)
111
- sorted_config = Merkle::Config.new(
110
+ # Configuration with non-sorted hashing (directions needed in proofs)
111
+ non_sorted_config = Merkle::Config.new(
112
112
  hash_type: :sha256,
113
- sort_hashes: true
113
+ sort_hashes: false
114
114
  )
115
115
  ```
116
116
 
@@ -56,7 +56,7 @@ module Merkle
56
56
  raise ArgumentError, 'leaf_index out of range' if leaf_index < 0 || leaves.length <= leaf_index
57
57
 
58
58
  siblings, directions = siblings_with_directions(leaf_index)
59
- siblings = siblings.map{|sibling| hex_string?(sibling) ? sibling : sibling.unpack1('H*')}
59
+ siblings = siblings.map{|sibling| bin_to_hex(sibling) }
60
60
  directions = [] if config.sort_hashes
61
61
  Proof.new(config: config, root: compute_root, leaf: leaves[leaf_index], siblings: siblings, directions: directions)
62
62
  end
data/lib/merkle/config.rb CHANGED
@@ -14,7 +14,7 @@ module Merkle
14
14
  # @param [Boolean] sort_hashes Whether to sort internal nodes in lexicographical order and hash them.
15
15
  # If you enable this, Merkle::Proof's directions are not required.
16
16
  # @raise [ArgumentError]
17
- def initialize(hash_type: :sha256, branch_tag: '', sort_hashes: false)
17
+ def initialize(hash_type: :sha256, branch_tag: '', sort_hashes: true)
18
18
  raise ArgumentError, "hash_type #{hash_type} does not supported." unless HASH_TYPES.include?(hash_type)
19
19
  raise ArgumentError, "internal_tag must be string." unless branch_tag.is_a?(String)
20
20
  raise ArgumentError, "sort_hashes must be boolean." unless sort_hashes.is_a?(TrueClass) || sort_hashes.is_a?(FalseClass)
@@ -26,13 +26,13 @@ module Merkle
26
26
  # Bitcoin configuration.
27
27
  # @return [Merkle::Config]
28
28
  def self.bitcoin
29
- Config.new(hash_type: :double_sha256)
29
+ Config.new(hash_type: :double_sha256, sort_hashes: false)
30
30
  end
31
31
 
32
32
  # Taptree configuration.
33
33
  # @return [Merkle::Config]
34
34
  def self.taptree
35
- Config.new(branch_tag: 'TapBranch', sort_hashes: true)
35
+ Config.new(branch_tag: 'TapBranch')
36
36
  end
37
37
 
38
38
  # Generate tagged hash.
@@ -0,0 +1,209 @@
1
+ module Merkle
2
+ # Custom Merkle tree implementation that allows specifying the tree structure
3
+ # Example: [leaf_A, [leaf_B, leaf_C], [leaf_D, leaf_E], leaf_F]
4
+ class CustomTree < AbstractTree
5
+
6
+ # Constructor
7
+ # @param [Merkle::Config] config Configuration for merkle tree.
8
+ # @param [Array] leaves A nested array representing the tree structure.
9
+ # Each element can be a leaf hash (hex string) or an array of child nodes.
10
+ def initialize(config:, leaves:)
11
+ super(config: config, leaves: leaves)
12
+ # Validate nested structure before calling super
13
+ validate_leaves!(extract_leaves(leaves))
14
+ end
15
+
16
+ # Create tree from elements with custom structure
17
+ # @param [Merkle::Config] config Configuration for merkle tree.
18
+ # @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: '')
21
+ raise ArgumentError, 'config must be Merkle::Config' unless config.is_a?(Merkle::Config)
22
+ 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
+
28
+ self.new(config: config, leaves: hashed_structure)
29
+ end
30
+
31
+ # Compute merkle root using custom structure
32
+ # @return [String] merkle root
33
+ def compute_root
34
+ all_leaves = extract_leaves(@leaves)
35
+ raise Error, 'leaves is empty' if all_leaves.empty?
36
+ result = compute_node_hash(@leaves)
37
+ result.unpack1('H*')
38
+ end
39
+
40
+ # Convert nested elements to nested hashes
41
+ def self.convert_elements_to_hashes(node, config, leaf_tag)
42
+ if node.is_a?(Array)
43
+ node.map { |child| convert_elements_to_hashes(child, config, leaf_tag) }
44
+ else
45
+ # This is a leaf element, hash it and convert to hex
46
+ config.tagged_hash(node, leaf_tag).unpack1('H*')
47
+ end
48
+ end
49
+
50
+ # Compute hash for a node in the structure (binary tree only)
51
+ def compute_node_hash(node)
52
+ if node.is_a?(Array)
53
+ case node.length
54
+ when 1
55
+ # Single child - just return its hash
56
+ compute_node_hash(node[0])
57
+ when 2
58
+ # Binary node: compute hash of left and right children
59
+ left_hash = compute_node_hash(node[0])
60
+ right_hash = compute_node_hash(node[1])
61
+
62
+ # Combine hashes according to sort_hashes config
63
+ combined = if config.sort_hashes
64
+ [left_hash, right_hash].sort.join
65
+ else
66
+ left_hash + right_hash
67
+ end
68
+
69
+ config.tagged_hash(combined)
70
+ else
71
+ raise ArgumentError, "Binary tree nodes must have 1 or 2 children, got #{node.length}"
72
+ end
73
+ else
74
+ # Leaf node: already a hash, convert to binary
75
+ hex_to_bin(node)
76
+ end
77
+ end
78
+
79
+ # Override generate_proof to work with nested structure
80
+ def generate_proof(leaf_index)
81
+ all_leaves = extract_leaves(@leaves)
82
+ raise ArgumentError, 'leaf_index must be Integer' unless leaf_index.is_a?(Integer)
83
+ raise ArgumentError, 'leaf_index out of range' if leaf_index < 0 || all_leaves.length <= leaf_index
84
+
85
+ siblings, directions = siblings_with_directions(leaf_index)
86
+ siblings = siblings.map { |sibling| bin_to_hex(sibling) }
87
+ directions = [] if config.sort_hashes
88
+
89
+ Proof.new(
90
+ config: config,
91
+ root: compute_root,
92
+ leaf: all_leaves[leaf_index],
93
+ siblings: siblings,
94
+ directions: directions
95
+ )
96
+ end
97
+
98
+ private
99
+
100
+ # Extract all leaf hashes from the nested structure
101
+ def extract_leaves(node)
102
+ if node.is_a?(Array)
103
+ node.flat_map { |child| extract_leaves(child) }
104
+ else
105
+ # This is a leaf hash
106
+ [node]
107
+ end
108
+ end
109
+
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)
116
+ 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}"
125
+ end
126
+ node.each { |child| validate_binary_structure(child) }
127
+ end
128
+ end
129
+
130
+ # Override siblings_with_directions for proof generation
131
+ def siblings_with_directions(leaf_index)
132
+ all_leaves = extract_leaves(@leaves)
133
+ target_leaf = all_leaves[leaf_index]
134
+ siblings = []
135
+ 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
+
149
+ [siblings, directions]
150
+ 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
181
+ end
182
+ end
183
+
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
192
+ 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
200
+ end
201
+ end
202
+
203
+ # Not used in custom tree - structure is determined by nested array
204
+ def build_next_level(nodes)
205
+ raise NotImplementedError, "CustomTree uses structure-based computation"
206
+ end
207
+
208
+ end
209
+ end
data/lib/merkle/util.rb CHANGED
@@ -2,6 +2,7 @@ module Merkle
2
2
  module Util
3
3
 
4
4
  # Check whether +data+ is hex string or not.
5
+ # @param [String] data
5
6
  # @return [Boolean]
6
7
  # @raise [ArgumentError]
7
8
  def hex_string?(data)
@@ -10,6 +11,7 @@ module Merkle
10
11
  end
11
12
 
12
13
  # Convert hex string +data+ to binary.
14
+ # @param [String] data
13
15
  # @return [String]
14
16
  # @raise [ArgumentError]
15
17
  def hex_to_bin(data)
@@ -17,6 +19,15 @@ module Merkle
17
19
  hex_string?(data) ? [data].pack('H*') : data
18
20
  end
19
21
 
22
+ # Convert binary string +data+ to hex string.
23
+ # @param [String] data
24
+ # @return [String]
25
+ # @raise [ArgumentError]
26
+ def bin_to_hex(data)
27
+ raise ArgumentError, 'data must be string' unless data.is_a?(String)
28
+ hex_string?(data) ? data : data.unpack1('H*')
29
+ end
30
+
20
31
  # Combine two elements(+left+ and +right+) with sort configuration.
21
32
  # @param [Merkle::Config] config
22
33
  # @param [String] left Left element(binary format).
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Merkle
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/merkle.rb CHANGED
@@ -6,6 +6,7 @@ require_relative 'merkle/config'
6
6
  require_relative 'merkle/abstract_tree'
7
7
  require_relative 'merkle/binary_tree'
8
8
  require_relative 'merkle/adaptive_tree'
9
+ require_relative 'merkle/custom_tree'
9
10
  require_relative 'merkle/proof'
10
11
 
11
12
  # Merkle tree module.
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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - azuchi
@@ -28,6 +28,7 @@ files:
28
28
  - lib/merkle/adaptive_tree.rb
29
29
  - lib/merkle/binary_tree.rb
30
30
  - lib/merkle/config.rb
31
+ - lib/merkle/custom_tree.rb
31
32
  - lib/merkle/proof.rb
32
33
  - lib/merkle/util.rb
33
34
  - lib/merkle/version.rb