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 +4 -4
- data/README.md +3 -3
- data/lib/merkle/abstract_tree.rb +1 -1
- 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
@@ -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 (
|
111
|
-
|
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:
|
113
|
+
sort_hashes: false
|
114
114
|
)
|
115
115
|
```
|
116
116
|
|
data/lib/merkle/abstract_tree.rb
CHANGED
@@ -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|
|
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:
|
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
|