merkle 0.1.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: bce04358fd44b062aa388fdc8bb834a33e6dc082ae522e097f7a90a777cceb7d
4
- data.tar.gz: e9cdee4066043760ecd72b89bc355060cb47273ae13a4b1c379e7ecca494fb62
3
+ metadata.gz: eaa0d4d13ee5bd6ff5e8b81aeb50396b8932f6efea765cc64dd1ff19614ebd9e
4
+ data.tar.gz: aa59ca4b87da23aa28707d4434209b36c03a7942b7635af036253c2dd7161607
5
5
  SHA512:
6
- metadata.gz: 7f5719c3393e13411152b2207ad1c3c109c468d0611d9c5d5c83d1430ee427dbfe914aa427145052a7bc8a55f8ebb71aaa809d45516b61243981f50a58388165
7
- data.tar.gz: 7dac36c4c6aaa55107f8ca2b7c7b06efba49c59f66017e50a225c463d17f4635f13f5401b640a0735c2893ac819574589655ec9f5e5eeed0ffaf2a1704a0a7f8
6
+ metadata.gz: fcf1016cc2bdbc721185ca647219c036cf05f13ad4534b4775d0ebb9ef3d585a9b97f1b40aac46b8f32b4aa3071821ba32d9ba7147ba86bf6c3a49eb0550290e
7
+ data.tar.gz: 36aec619bb2280e91b015409933c57d050787535a6890a4ee41efd86b6380af3300f81e488e2d5871fbb617b2af955ca4c9ba2437f012107fcff6c8f94eb3ecb
data/README.md CHANGED
@@ -35,7 +35,7 @@ require 'merkle'
35
35
  # Create configuration
36
36
  config = Merkle::Config.new(hash_type: :sha256)
37
37
 
38
- # Prepare leaves (hex strings)
38
+ # Method 1: Using pre-hashed leaves
39
39
  leaves = [
40
40
  'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3',
41
41
  'b3a8e0e1f9ab1bfe3a36f231f676f78bb30a519d2b21e6c530c0eee8ebb4a5d0',
@@ -58,6 +58,35 @@ puts "Proof directions: #{proof.directions}"
58
58
  puts "Proof valid: #{proof.valid?}"
59
59
  ```
60
60
 
61
+ ### Using from_elements
62
+
63
+ ```ruby
64
+ # Method 2: Using from_elements to automatically hash raw data
65
+ elements = ['hello', 'world', 'merkle', 'tree']
66
+
67
+ # Create tree from raw elements
68
+ tree = Merkle::BinaryTree.from_elements(
69
+ config: config,
70
+ elements: elements
71
+ )
72
+
73
+ # The elements are automatically hashed before building the tree
74
+ root = tree.compute_root
75
+ puts "Root from elements: #{root}"
76
+
77
+ # With optional leaf tag for tagged hashing (e.g., Taproot)
78
+ taproot_config = Merkle::Config.taptree
79
+ tagged_tree = Merkle::AdaptiveTree.from_elements(
80
+ config: taproot_config,
81
+ elements: elements,
82
+ leaf_tag: 'TapLeaf' # Optional tag for leaf hashing
83
+ )
84
+
85
+ # Generate and verify proof
86
+ proof = tree.generate_proof(0)
87
+ puts "Proof for first element valid: #{proof.valid?}"
88
+ ```
89
+
61
90
  ### Adaptive Tree Example
62
91
 
63
92
  ```ruby
@@ -78,10 +107,10 @@ bitcoin_config = Merkle::Config.new(hash_type: :double_sha256)
78
107
  # Configuration with tagged hashing (Taproot-style)
79
108
  taproot_config = Merkle::Config.taptree
80
109
 
81
- # Configuration with sorted hashing (no directions needed in proofs)
82
- sorted_config = Merkle::Config.new(
110
+ # Configuration with non-sorted hashing (directions needed in proofs)
111
+ non_sorted_config = Merkle::Config.new(
83
112
  hash_type: :sha256,
84
- sort_hashes: true
113
+ sort_hashes: false
85
114
  )
86
115
  ```
87
116
 
@@ -11,13 +11,28 @@ module Merkle
11
11
  # @param [Merkle::Config] config Configuration for merkle tree.
12
12
  # @param [Array] leaves An array of leaves.
13
13
  # @raise [ArgumentError]
14
- def initialize(config:, leaves: [])
14
+ def initialize(config:, leaves: )
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
18
  @leaves = leaves
19
19
  end
20
20
 
21
+ # Create tree from +elements+. For each element in elements,
22
+ # we compute a tagged hash, which becomes the leaf value.
23
+ # @param [Merkle::Config] config Configuration for merkle tree.
24
+ # @param [Array] elements An array of element that will be hashed to become leaves.
25
+ # @param [String] leaf_tag An optional tag to use when computing the leaf hash.
26
+ def self.from_elements(config:, elements:, leaf_tag: '')
27
+ raise ArgumentError, 'config must be Merkle::Config' unless config.is_a?(Merkle::Config)
28
+ raise ArgumentError, 'elements must be Array' unless elements.is_a?(Array)
29
+ raise ArgumentError, 'leaf_tag must be string' unless leaf_tag.is_a?(String)
30
+ leaves = elements.map do |element|
31
+ config.tagged_hash(element, leaf_tag)
32
+ end
33
+ self.new(config: config, leaves: leaves)
34
+ end
35
+
21
36
  # Compute merkle root.
22
37
  # @return [String] merkle root (hex value). For Bitcoin, the endianness of this value must be reversed.
23
38
  # @raise [Merkle::Error] If leaves is empty.
@@ -41,7 +56,7 @@ module Merkle
41
56
  raise ArgumentError, 'leaf_index out of range' if leaf_index < 0 || leaves.length <= leaf_index
42
57
 
43
58
  siblings, directions = siblings_with_directions(leaf_index)
44
- siblings = siblings.map{|sibling| hex_string?(sibling) ? sibling : sibling.unpack1('H*')}
59
+ siblings = siblings.map{|sibling| bin_to_hex(sibling) }
45
60
  directions = [] if config.sort_hashes
46
61
  Proof.new(config: config, root: compute_root, leaf: leaves[leaf_index], siblings: siblings, directions: directions)
47
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.1.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.1.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