merkle 0.2.0 → 0.3.1

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: ce4faaabf24a85b734bd6cfe35081dfcc897bf418c5b391e68a84f8ec0aa9977
4
+ data.tar.gz: 944144e97463b2d0ad1ff7b2a882210548786035f52fa0090d505bbad7b9f88e
5
5
  SHA512:
6
- metadata.gz: 3aae0a25fdfca015e89a4123bf7956ec3b163ae3dce0cebed93652d0eb90babd9651043fedcd12a6f346b08d15e558f2480fd9325d39294cdbcb5739594d1537
7
- data.tar.gz: 1db01cd9b1de1bac9c1193d12e47dc324011dc3578f3b49620dc47bac7ed920da606227af0a954f5de0f0704775b7ebb90224da63a34757bd1ac2ed9b3cc86e9
6
+ metadata.gz: 49988d95199cf3e6a5e5944f69205d6b354ed4e5d811325c4e6ca8d4a868044b6c3dfc380491d1aa9c577ca7848770ea3c7e56d9212a1bb2a60cbc5c330a0060
7
+ data.tar.gz: 107fc3472cca81407b6fbc97a0981711f5538cdb2e657a032911d7866021fa37a4a30ac00ce5bcbebe2585849f700fc01c892c41670f619150d809c578fbb007
data/README.md CHANGED
@@ -4,7 +4,7 @@ A Ruby library for Merkle tree construction and proof generation with support fo
4
4
 
5
5
  ## Features
6
6
 
7
- - **Multiple tree structures**: Binary Tree (Bitcoin-compatible) and Adaptive Tree implementations
7
+ - **Multiple tree structures**: Binary Tree (Bitcoin-compatible), Adaptive Tree, and Custom Tree implementations
8
8
  - **Flexible configuration**: Support for different hash algorithms (SHA256, Double SHA256) and tagged hashing
9
9
  - **Proof generation and verification**: Generate and verify Merkle proofs for any leaf
10
10
  - **Sorted hashing support**: Optional lexicographical sorting for deterministic tree construction
@@ -98,6 +98,32 @@ proof = adaptive_tree.generate_proof(0)
98
98
  puts "Adaptive tree proof valid: #{proof.valid?}"
99
99
  ```
100
100
 
101
+ ### Custom Tree Example
102
+
103
+ ```ruby
104
+ # CustomTree allows you to define your own tree structure using nested arrays
105
+ # This gives you precise control over how leaves are grouped
106
+
107
+ # 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')
112
+
113
+ # Define structure: [[A, [B, C]], D]
114
+ nested_leaves = [[leaf_a, [leaf_b, leaf_c]], leaf_d]
115
+ custom_tree = Merkle::CustomTree.new(config: config, leaves: nested_leaves)
116
+
117
+ root = custom_tree.compute_root
118
+ puts "Custom tree root: #{root}"
119
+
120
+ # Valid structures:
121
+ # - [A, B] → Simple binary node
122
+ # - [[A, B], C] → Left subtree with right leaf
123
+ # - [A] → Single child node
124
+ # Invalid: [A, B, C] → Error (max 2 children per node)
125
+ ```
126
+
101
127
  ### Configuration Options
102
128
 
103
129
  ```ruby
@@ -107,10 +133,10 @@ bitcoin_config = Merkle::Config.new(hash_type: :double_sha256)
107
133
  # Configuration with tagged hashing (Taproot-style)
108
134
  taproot_config = Merkle::Config.taptree
109
135
 
110
- # Configuration with sorted hashing (no directions needed in proofs)
111
- sorted_config = Merkle::Config.new(
136
+ # Configuration with non-sorted hashing (directions needed in proofs)
137
+ non_sorted_config = Merkle::Config.new(
112
138
  hash_type: :sha256,
113
- sort_hashes: true
139
+ sort_hashes: false
114
140
  )
115
141
  ```
116
142
 
@@ -120,6 +146,7 @@ sorted_config = Merkle::Config.new(
120
146
 
121
147
  - **BinaryTree**: Bitcoin-compatible merkle tree that duplicates odd nodes
122
148
  - **AdaptiveTree**: Unbalanced tree that promotes odd nodes to higher levels for optimized access patterns
149
+ - **CustomTree**: User-defined tree structure using nested arrays for precise control over leaf grouping
123
150
 
124
151
  ### Proof System
125
152
 
@@ -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.
@@ -41,19 +41,21 @@ module Merkle
41
41
  # @return [String] Tagged hash value.
42
42
  def tagged_hash(data, tag = branch_tag)
43
43
  raise ArgumentError, "data must be string." unless data.is_a?(String)
44
- data = [data].pack('H*') if hex_string?(data)
44
+ raise ArgumentError, "tag must be a String." unless tag.is_a?(String)
45
+
46
+ data_bin = hex_to_bin(data).b
45
47
 
46
48
  unless tag.empty?
47
- tag_hash = Digest::SHA256.digest(tag)
48
- data = tag_hash + tag_hash + data
49
+ tag_bin = Digest::SHA256.digest(tag).b
50
+ data_bin = tag_bin + tag_bin + data_bin
49
51
  end
50
52
 
51
53
  case hash_type
52
54
  when :sha256
53
- Digest::SHA256.digest(data)
55
+ Digest::SHA256.digest(data_bin)
54
56
  when :double_sha256
55
- Digest::SHA256.digest(Digest::SHA256.digest(data))
57
+ Digest::SHA256.digest(Digest::SHA256.digest(data_bin))
56
58
  end
57
59
  end
58
60
  end
59
- end
61
+ end
@@ -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.1"
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.1
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