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 +4 -4
- data/README.md +33 -4
- data/lib/merkle/abstract_tree.rb +17 -2
- data/lib/merkle/config.rb +3 -3
- data/lib/merkle/custom_tree.rb +209 -0
- data/lib/merkle/util.rb +11 -0
- data/lib/merkle/version.rb +1 -1
- data/lib/merkle.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eaa0d4d13ee5bd6ff5e8b81aeb50396b8932f6efea765cc64dd1ff19614ebd9e
|
4
|
+
data.tar.gz: aa59ca4b87da23aa28707d4434209b36c03a7942b7635af036253c2dd7161607
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
#
|
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 (
|
82
|
-
|
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:
|
113
|
+
sort_hashes: false
|
85
114
|
)
|
86
115
|
```
|
87
116
|
|
data/lib/merkle/abstract_tree.rb
CHANGED
@@ -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|
|
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:
|
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'
|
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).
|
data/lib/merkle/version.rb
CHANGED
data/lib/merkle.rb
CHANGED
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
|
+
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
|