merkle 0.3.0 → 0.4.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/.ruby-version +1 -1
- data/README.md +28 -1
- data/lib/merkle/abstract_tree.rb +2 -1
- data/lib/merkle/config.rb +8 -6
- data/lib/merkle/custom_tree.rb +33 -61
- data/lib/merkle/proof.rb +16 -1
- data/lib/merkle/util.rb +4 -1
- data/lib/merkle/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 900af6789d1eda0ed4bf1c4d8b3575a3b7c5789d4e5a84abe43304c49122a92b
|
|
4
|
+
data.tar.gz: d250d0b1ed630a82d8aa4fe66347319172d200b1416e95260a51a54135e539fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2bd898709716daecc0b73f006e8d3367ad8a01d9e2a40fae32a1819cc62d85229ff671f38d217dd1ffef2f548be56ca485c42b097348ed95d382fde51fdc7743
|
|
7
|
+
data.tar.gz: 3b95465023c7edb109836caa261ef4a0bdd376ab042742392f7381204d715111ce0f8fab01d6fb94d7917862341d9cec7d4b14d8f55dcb34f7af755dec6ef2ad
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby-
|
|
1
|
+
ruby-4.0.0
|
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
|
|
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
|
|
@@ -120,6 +146,7 @@ non_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
|
|
data/lib/merkle/abstract_tree.rb
CHANGED
|
@@ -20,6 +20,7 @@ module Merkle
|
|
|
20
20
|
|
|
21
21
|
# Create tree from +elements+. For each element in elements,
|
|
22
22
|
# we compute a tagged hash, which becomes the leaf value.
|
|
23
|
+
# The resulting leaves are hex strings, the same representation as leaves passed to #initialize.
|
|
23
24
|
# @param [Merkle::Config] config Configuration for merkle tree.
|
|
24
25
|
# @param [Array] elements An array of element that will be hashed to become leaves.
|
|
25
26
|
# @param [String] leaf_tag An optional tag to use when computing the leaf hash.
|
|
@@ -28,7 +29,7 @@ module Merkle
|
|
|
28
29
|
raise ArgumentError, 'elements must be Array' unless elements.is_a?(Array)
|
|
29
30
|
raise ArgumentError, 'leaf_tag must be string' unless leaf_tag.is_a?(String)
|
|
30
31
|
leaves = elements.map do |element|
|
|
31
|
-
config.tagged_hash(element, leaf_tag)
|
|
32
|
+
config.tagged_hash(element, leaf_tag).unpack1('H*')
|
|
32
33
|
end
|
|
33
34
|
self.new(config: config, leaves: leaves)
|
|
34
35
|
end
|
data/lib/merkle/config.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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(
|
|
55
|
+
Digest::SHA256.digest(data_bin)
|
|
54
56
|
when :double_sha256
|
|
55
|
-
Digest::SHA256.digest(Digest::SHA256.digest(
|
|
57
|
+
Digest::SHA256.digest(Digest::SHA256.digest(data_bin))
|
|
56
58
|
end
|
|
57
59
|
end
|
|
58
60
|
end
|
|
59
|
-
end
|
|
61
|
+
end
|
data/lib/merkle/custom_tree.rb
CHANGED
|
@@ -129,77 +129,49 @@ module Merkle
|
|
|
129
129
|
|
|
130
130
|
# Override siblings_with_directions for proof generation
|
|
131
131
|
def siblings_with_directions(leaf_index)
|
|
132
|
-
all_leaves = extract_leaves(@leaves)
|
|
133
|
-
target_leaf = all_leaves[leaf_index]
|
|
134
132
|
siblings = []
|
|
135
133
|
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
|
-
|
|
134
|
+
collect_path(@leaves, leaf_index, siblings, directions)
|
|
149
135
|
[siblings, directions]
|
|
150
136
|
end
|
|
151
|
-
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
137
|
+
|
|
138
|
+
# Walk down the structure toward the leaf at +index+ (counted within +node+), collecting the
|
|
139
|
+
# sibling hash and its direction at each branch. Siblings are collected deepest first, the
|
|
140
|
+
# order Proof#valid? folds them in.
|
|
141
|
+
# The descent is driven by the index rather than by the leaf value, so duplicate leaf hashes
|
|
142
|
+
# still yield the proof for the requested position.
|
|
143
|
+
# @param [Object] node A subtree (nested Array) or a leaf hash.
|
|
144
|
+
# @param [Integer] index The leaf index within +node+.
|
|
145
|
+
# @param [Array] siblings Collected sibling hashes(binary format).
|
|
146
|
+
# @param [Array] directions Collected directions(0: left, 1: right).
|
|
147
|
+
def collect_path(node, index, siblings, directions)
|
|
148
|
+
return unless node.is_a?(Array)
|
|
149
|
+
|
|
150
|
+
if node.length == 1
|
|
151
|
+
# Single child contributes no sibling, its hash is passed through unchanged.
|
|
152
|
+
return collect_path(node[0], index, siblings, directions)
|
|
181
153
|
end
|
|
182
|
-
end
|
|
183
154
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
end
|
|
191
|
-
nil
|
|
155
|
+
left, right = node
|
|
156
|
+
left_leaves = leaf_count(left)
|
|
157
|
+
if index < left_leaves
|
|
158
|
+
collect_path(left, index, siblings, directions)
|
|
159
|
+
siblings << compute_node_hash(right)
|
|
160
|
+
directions << 1 # sibling is on the right
|
|
192
161
|
else
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
else
|
|
197
|
-
current_index[0] += 1
|
|
198
|
-
nil
|
|
199
|
-
end
|
|
162
|
+
collect_path(right, index - left_leaves, siblings, directions)
|
|
163
|
+
siblings << compute_node_hash(left)
|
|
164
|
+
directions << 0 # sibling is on the left
|
|
200
165
|
end
|
|
201
166
|
end
|
|
202
167
|
|
|
168
|
+
# Count the leaves under +node+.
|
|
169
|
+
# @param [Object] node A subtree (nested Array) or a leaf hash.
|
|
170
|
+
# @return [Integer] Number of leaves.
|
|
171
|
+
def leaf_count(node)
|
|
172
|
+
node.is_a?(Array) ? node.sum { |child| leaf_count(child) } : 1
|
|
173
|
+
end
|
|
174
|
+
|
|
203
175
|
# Not used in custom tree - structure is determined by nested array
|
|
204
176
|
def build_next_level(nodes)
|
|
205
177
|
raise NotImplementedError, "CustomTree uses structure-based computation"
|
data/lib/merkle/proof.rb
CHANGED
|
@@ -2,21 +2,36 @@ module Merkle
|
|
|
2
2
|
class Proof
|
|
3
3
|
include Util
|
|
4
4
|
|
|
5
|
+
# Upper bound on the number of siblings, i.e. the depth of the tree the proof came from.
|
|
6
|
+
# A proof longer than this cannot correspond to any realistic tree, so it is rejected
|
|
7
|
+
# rather than hashed.
|
|
8
|
+
MAX_SIBLINGS = 64
|
|
9
|
+
|
|
5
10
|
attr_reader :config, :root, :leaf, :siblings, :directions
|
|
6
11
|
|
|
7
12
|
# Constructor.
|
|
8
13
|
# @param [Merkle::Config] config
|
|
9
14
|
# @param [String] root
|
|
10
15
|
# @param [String] leaf
|
|
11
|
-
# @param [Array] siblings
|
|
16
|
+
# @param [Array] siblings An array of sibling hashes(64-character hex strings).
|
|
12
17
|
# @param [Array] directions Array of positions at each level(0: left, 1: right),
|
|
13
18
|
# only required if sort_hashes is false in config.
|
|
14
19
|
def initialize(config:, root:, leaf:, siblings:, directions: [])
|
|
15
20
|
raise ArgumentError, 'config must be a Merkle::Config' unless config.is_a?(Merkle::Config)
|
|
16
21
|
raise ArgumentError, 'root must be string' unless root.is_a?(String)
|
|
17
22
|
raise ArgumentError, 'leaf must be string' unless leaf.is_a?(String)
|
|
23
|
+
raise ArgumentError, 'siblings must be an Array' unless siblings.is_a?(Array)
|
|
24
|
+
raise ArgumentError, "siblings must not exceed #{MAX_SIBLINGS} elements" if siblings.length > MAX_SIBLINGS
|
|
25
|
+
siblings.each do |sibling|
|
|
26
|
+
raise ArgumentError, 'sibling must be string' unless sibling.is_a?(String)
|
|
27
|
+
raise ArgumentError, 'sibling must be a 64-character hex string' unless sibling.match?(/\A[0-9a-fA-F]{64}\z/)
|
|
28
|
+
end
|
|
18
29
|
raise ArgumentError, 'directions must be an Array' unless directions.is_a?(Array)
|
|
19
30
|
raise ArgumentError, 'No directions are required because sorted_hash is enabled' if config.sort_hashes && !directions.empty?
|
|
31
|
+
unless config.sort_hashes
|
|
32
|
+
raise ArgumentError, 'directions must have the same length as siblings' unless directions.length == siblings.length
|
|
33
|
+
raise ArgumentError, 'direction must be 0 or 1' unless directions.all? { |direction| direction == 0 || direction == 1 }
|
|
34
|
+
end
|
|
20
35
|
@config = config
|
|
21
36
|
@root = root
|
|
22
37
|
@leaf = leaf
|
data/lib/merkle/util.rb
CHANGED
|
@@ -2,12 +2,15 @@ module Merkle
|
|
|
2
2
|
module Util
|
|
3
3
|
|
|
4
4
|
# Check whether +data+ is hex string or not.
|
|
5
|
+
# An odd-length string is not a hex string. Treating it as one would let
|
|
6
|
+
# +pack('H*')+ pad the missing nibble with zero, so 'abc' and 'abc0' would
|
|
7
|
+
# collide.
|
|
5
8
|
# @param [String] data
|
|
6
9
|
# @return [Boolean]
|
|
7
10
|
# @raise [ArgumentError]
|
|
8
11
|
def hex_string?(data)
|
|
9
12
|
raise ArgumentError, 'data must be string' unless data.is_a?(String)
|
|
10
|
-
data.match?(/\A[0-9a-fA-F]+\z/)
|
|
13
|
+
data.length.even? && data.match?(/\A[0-9a-fA-F]+\z/)
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
# Convert hex string +data+ to binary.
|
data/lib/merkle/version.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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- azuchi
|
|
@@ -54,7 +54,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
54
54
|
- !ruby/object:Gem::Version
|
|
55
55
|
version: '0'
|
|
56
56
|
requirements: []
|
|
57
|
-
rubygems_version:
|
|
57
|
+
rubygems_version: 4.0.3
|
|
58
58
|
specification_version: 4
|
|
59
59
|
summary: A Ruby library for Merkle trees
|
|
60
60
|
test_files: []
|