merkle 0.4.0 → 1.0.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 +93 -12
- data/lib/merkle/abstract_tree.rb +13 -7
- data/lib/merkle/adaptive_tree.rb +1 -1
- data/lib/merkle/binary_tree.rb +1 -1
- data/lib/merkle/config.rb +52 -9
- data/lib/merkle/custom_tree.rb +50 -28
- data/lib/merkle/proof.rb +18 -10
- data/lib/merkle/util.rb +45 -9
- data/lib/merkle/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b9d4e8996bfda0b7e8baa3c2324f51222503f658fc43c066819058ef028bdc7
|
|
4
|
+
data.tar.gz: 107526e231fb5925e181c7747f8130489278684ecfd37355ccc869adbcdc6d97
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1467fc1c5715adfc418cc3ae5a44dab2c6dcad7b32f10d6fe2cd9eb264bace3d22d7f0f544cb87827048c67b6644a142c2b0754b5faaef2556966cf4493e515f
|
|
7
|
+
data.tar.gz: eb7621a6e1d771a96a219dcb283f379ace0c5528bb6ea5d8b1c08263b8e6f3f52166715848c35736aee449a1d6cbea890441a3c4f33861f7857a5ac664917c7b
|
data/README.md
CHANGED
|
@@ -33,7 +33,12 @@ Or install it yourself as:
|
|
|
33
33
|
require 'merkle'
|
|
34
34
|
|
|
35
35
|
# Create configuration
|
|
36
|
-
|
|
36
|
+
# element_encoding says how the elements passed to .from_elements are read:
|
|
37
|
+
# :hex - elements are hex strings and are decoded before hashing
|
|
38
|
+
# :binary - elements are byte strings and are hashed as-is
|
|
39
|
+
# :auto - legacy mode, see "Upgrading from 0.4.0 and earlier"
|
|
40
|
+
# It has no default: guessing it silently changes the merkle root.
|
|
41
|
+
config = Merkle::Config.new(element_encoding: :binary, hash_type: :sha256)
|
|
37
42
|
|
|
38
43
|
# Method 1: Using pre-hashed leaves
|
|
39
44
|
leaves = [
|
|
@@ -74,12 +79,12 @@ tree = Merkle::BinaryTree.from_elements(
|
|
|
74
79
|
root = tree.compute_root
|
|
75
80
|
puts "Root from elements: #{root}"
|
|
76
81
|
|
|
77
|
-
#
|
|
78
|
-
|
|
82
|
+
# Tags live on the config, so leaves and branches cannot drift apart.
|
|
83
|
+
# Config.taptree carries leaf_tag: 'TapLeaf' and branch_tag: 'TapBranch'.
|
|
84
|
+
taproot_config = Merkle::Config.taptree(element_encoding: :binary)
|
|
79
85
|
tagged_tree = Merkle::AdaptiveTree.from_elements(
|
|
80
86
|
config: taproot_config,
|
|
81
|
-
elements: elements
|
|
82
|
-
leaf_tag: 'TapLeaf' # Optional tag for leaf hashing
|
|
87
|
+
elements: elements
|
|
83
88
|
)
|
|
84
89
|
|
|
85
90
|
# Generate and verify proof
|
|
@@ -105,10 +110,11 @@ puts "Adaptive tree proof valid: #{proof.valid?}"
|
|
|
105
110
|
# This gives you precise control over how leaves are grouped
|
|
106
111
|
|
|
107
112
|
# Example 1: Basic usage with pre-hashed leaves
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
# Leaves are always 64-character hex strings, the same form #compute_root returns.
|
|
114
|
+
leaf_a = config.tagged_hash(config.encode_element('A')).unpack1('H*')
|
|
115
|
+
leaf_b = config.tagged_hash(config.encode_element('B')).unpack1('H*')
|
|
116
|
+
leaf_c = config.tagged_hash(config.encode_element('C')).unpack1('H*')
|
|
117
|
+
leaf_d = config.tagged_hash(config.encode_element('D')).unpack1('H*')
|
|
112
118
|
|
|
113
119
|
# Define structure: [[A, [B, C]], D]
|
|
114
120
|
nested_leaves = [[leaf_a, [leaf_b, leaf_c]], leaf_d]
|
|
@@ -120,21 +126,24 @@ puts "Custom tree root: #{root}"
|
|
|
120
126
|
# Valid structures:
|
|
121
127
|
# - [A, B] → Simple binary node
|
|
122
128
|
# - [[A, B], C] → Left subtree with right leaf
|
|
123
|
-
# - [A] →
|
|
129
|
+
# - [A] → A tree holding a single leaf
|
|
124
130
|
# Invalid: [A, B, C] → Error (max 2 children per node)
|
|
131
|
+
# Invalid: [[A, B]] → Error (a single-child node just passes its child's hash up,
|
|
132
|
+
# so it would commit to the same root as [A, B])
|
|
125
133
|
```
|
|
126
134
|
|
|
127
135
|
### Configuration Options
|
|
128
136
|
|
|
129
137
|
```ruby
|
|
130
138
|
# Bitcoin-compatible configuration with double SHA256
|
|
131
|
-
bitcoin_config = Merkle::Config.new(hash_type: :double_sha256)
|
|
139
|
+
bitcoin_config = Merkle::Config.new(element_encoding: :hex, hash_type: :double_sha256)
|
|
132
140
|
|
|
133
141
|
# Configuration with tagged hashing (Taproot-style)
|
|
134
|
-
taproot_config = Merkle::Config.taptree
|
|
142
|
+
taproot_config = Merkle::Config.taptree(element_encoding: :hex)
|
|
135
143
|
|
|
136
144
|
# Configuration with non-sorted hashing (directions needed in proofs)
|
|
137
145
|
non_sorted_config = Merkle::Config.new(
|
|
146
|
+
element_encoding: :binary,
|
|
138
147
|
hash_type: :sha256,
|
|
139
148
|
sort_hashes: false
|
|
140
149
|
)
|
|
@@ -162,3 +171,75 @@ The library generates compact Merkle proofs that include:
|
|
|
162
171
|
proof = tree.generate_proof(leaf_index)
|
|
163
172
|
is_valid = proof.valid? # Returns true/false
|
|
164
173
|
```
|
|
174
|
+
|
|
175
|
+
`#valid?` folds `leaf` upwards through `siblings` and compares the result to `root`. It answers
|
|
176
|
+
"do these hashes chain to this root", and nothing more. In particular it does not check that
|
|
177
|
+
`leaf` sits at the bottom of the tree.
|
|
178
|
+
|
|
179
|
+
**The verifier must derive `leaf` itself.** Hash the data you care about and build the proof
|
|
180
|
+
around that value:
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
leaf = config.tagged_hash(config.encode_element(my_data), config.leaf_tag).unpack1('H*')
|
|
184
|
+
proof = Merkle::Proof.new(config: config, root: trusted_root, leaf: leaf,
|
|
185
|
+
siblings: received_siblings, directions: received_directions)
|
|
186
|
+
proof.valid?
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Taking `leaf` from whoever supplied the proof defeats it: any internal node of the tree is a
|
|
190
|
+
value that chains to the root, so it would be accepted as if it were a leaf.
|
|
191
|
+
|
|
192
|
+
## Security considerations
|
|
193
|
+
|
|
194
|
+
This library lets you build trees that are not second-preimage resistant, because Bitcoin's
|
|
195
|
+
transaction merkle tree is one of them and cannot be changed. Two properties are left to the
|
|
196
|
+
protocol built on top of it:
|
|
197
|
+
|
|
198
|
+
- **Domain separation.** Give `leaf_tag` and `branch_tag` different values so that a leaf hash
|
|
199
|
+
can never equal an internal node hash. With both left empty, an attacker can craft an element
|
|
200
|
+
whose leaf hash equals an internal node and prove membership of something that was never in
|
|
201
|
+
the tree. `Config.taptree` sets both for you.
|
|
202
|
+
- **Duplicate leaves (CVE-2012-2459).** `BinaryTree` duplicates the last node when a level holds
|
|
203
|
+
an odd number of them, exactly as Bitcoin does, so `[a, b, c]` and `[a, b, c, c]` share a root.
|
|
204
|
+
Use `AdaptiveTree` or `CustomTree` if you do not need Bitcoin compatibility.
|
|
205
|
+
|
|
206
|
+
Element encoding, by contrast, is not left to guesswork: `element_encoding` is required on
|
|
207
|
+
`Config`, so `'hello'` and `'68656c6c6f'` cannot silently resolve to the same leaf.
|
|
208
|
+
|
|
209
|
+
### Upgrading from 0.4.0 and earlier
|
|
210
|
+
|
|
211
|
+
`element_encoding` has no default, so every `Config` construction has to be updated. Pick the
|
|
212
|
+
value that matches what you were already passing to `.from_elements`:
|
|
213
|
+
|
|
214
|
+
| What you pass as elements | Use |
|
|
215
|
+
| --- | --- |
|
|
216
|
+
| Hex strings | `:hex` |
|
|
217
|
+
| Raw byte strings | `:binary` |
|
|
218
|
+
| A mix of both | `:auto` |
|
|
219
|
+
|
|
220
|
+
`:auto` reproduces the old behaviour exactly, including its collisions: `'hello'` and
|
|
221
|
+
`'68656c6c6f'` share a leaf under it, and so do `'AB'` and `'ab'`. Use it to keep verifying roots
|
|
222
|
+
you already committed to, not for a new protocol. It reproduces 0.4.0; 0.3.1 and earlier also
|
|
223
|
+
padded odd-length hex, so `'abc'` and `'abc0'` shared a leaf there and no mode reproduces that.
|
|
224
|
+
|
|
225
|
+
`leaf_tag` moved from `.from_elements` to `Config`, next to `branch_tag`, so a protocol's tag
|
|
226
|
+
spec lives in one place. Pass it to `Config.new` instead. `Config.taptree` now sets
|
|
227
|
+
`leaf_tag: 'TapLeaf'` itself: if you were calling it without passing a leaf tag, your leaves were
|
|
228
|
+
untagged and your roots were not BIP341 script trees. They are now, which changes those roots.
|
|
229
|
+
|
|
230
|
+
Leaves are now always 64-character hex strings. If you were passing binary digests
|
|
231
|
+
(`config.tagged_hash(...)`) as leaves, append `.unpack1('H*')`.
|
|
232
|
+
|
|
233
|
+
**`Config#tagged_hash` is the one change that does not announce itself.** It now hashes its
|
|
234
|
+
argument as bytes instead of decoding it when it looks like hex, so a direct call keeps working
|
|
235
|
+
and returns a different digest:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
config.tagged_hash('deadbeef') # 0.4.0: hashed 4 bytes
|
|
239
|
+
config.tagged_hash(config.encode_element('deadbeef')) # 1.0.0: same 4 bytes, with :hex
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Everything reached through `.from_elements` already goes through `encode_element`, so this only
|
|
243
|
+
affects code that calls `tagged_hash` itself — typically to precompute leaves for `.new`. Audit
|
|
244
|
+
those call sites: wrap the argument in `encode_element` to keep the old digest, or leave it bare
|
|
245
|
+
if you were passing raw bytes all along.
|
data/lib/merkle/abstract_tree.rb
CHANGED
|
@@ -15,7 +15,9 @@ module Merkle
|
|
|
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
|
+
# Copy, so that the array the caller keeps cannot change this tree's root behind its back.
|
|
19
|
+
@leaves = leaves.dup
|
|
20
|
+
validate_leaves!
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
# Create tree from +elements+. For each element in elements,
|
|
@@ -23,13 +25,12 @@ module Merkle
|
|
|
23
25
|
# The resulting leaves are hex strings, the same representation as leaves passed to #initialize.
|
|
24
26
|
# @param [Merkle::Config] config Configuration for merkle tree.
|
|
25
27
|
# @param [Array] elements An array of element that will be hashed to become leaves.
|
|
26
|
-
#
|
|
27
|
-
def self.from_elements(config:, elements
|
|
28
|
+
# The tag used for the leaf hash comes from +config.leaf_tag+.
|
|
29
|
+
def self.from_elements(config:, elements:)
|
|
28
30
|
raise ArgumentError, 'config must be Merkle::Config' unless config.is_a?(Merkle::Config)
|
|
29
31
|
raise ArgumentError, 'elements must be Array' unless elements.is_a?(Array)
|
|
30
|
-
raise ArgumentError, 'leaf_tag must be string' unless leaf_tag.is_a?(String)
|
|
31
32
|
leaves = elements.map do |element|
|
|
32
|
-
config.tagged_hash(element, leaf_tag).unpack1('H*')
|
|
33
|
+
config.tagged_hash(config.encode_element(element), config.leaf_tag).unpack1('H*')
|
|
33
34
|
end
|
|
34
35
|
self.new(config: config, leaves: leaves)
|
|
35
36
|
end
|
|
@@ -39,8 +40,7 @@ module Merkle
|
|
|
39
40
|
# @raise [Merkle::Error] If leaves is empty.
|
|
40
41
|
def compute_root
|
|
41
42
|
raise Error, 'leaves is empty' if leaves.empty?
|
|
42
|
-
|
|
43
|
-
nodes = leaves.map {|leaf| hex_to_bin(leaf) }
|
|
43
|
+
nodes = leaves.map {|leaf| decode_hash(leaf) }
|
|
44
44
|
while nodes.length > 1
|
|
45
45
|
nodes = build_next_level(nodes)
|
|
46
46
|
end
|
|
@@ -64,6 +64,12 @@ module Merkle
|
|
|
64
64
|
|
|
65
65
|
private
|
|
66
66
|
|
|
67
|
+
# Validate the leaves this tree was built with.
|
|
68
|
+
# @raise [ArgumentError] If any leaf is not a node hash.
|
|
69
|
+
def validate_leaves!
|
|
70
|
+
leaves.each { |leaf| decode_hash(leaf) }
|
|
71
|
+
end
|
|
72
|
+
|
|
67
73
|
# Gets the siblings that corresponds to +leaf_index+ and its directions (if necessary).
|
|
68
74
|
# @param [Integer] leaf_index The leaf index.
|
|
69
75
|
# @return [Array] An array of siblings and directions.
|
data/lib/merkle/adaptive_tree.rb
CHANGED
data/lib/merkle/binary_tree.rb
CHANGED
data/lib/merkle/config.rb
CHANGED
|
@@ -6,36 +6,79 @@ module Merkle
|
|
|
6
6
|
# Supported Hash type.
|
|
7
7
|
HASH_TYPES = [:sha256, :double_sha256]
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
# How the elements passed to .from_elements are turned into bytes.
|
|
10
|
+
# :hex - each element is a hex string and is decoded before hashing.
|
|
11
|
+
# :binary - each element is already a byte string and is hashed as-is.
|
|
12
|
+
# :auto - each element is decoded if it looks like hex, otherwise hashed as-is.
|
|
13
|
+
#
|
|
14
|
+
# :auto exists to reproduce roots computed by 0.4.0 and earlier, where this was the only
|
|
15
|
+
# behaviour. Do not choose it for a new protocol: 'hello' and '68656c6c6f' resolve to the
|
|
16
|
+
# same leaf under it, and so do 'AB' and 'ab'. Note it reproduces 0.4.0, not 0.3.1 and
|
|
17
|
+
# earlier, which also padded odd-length hex ('abc' and 'abc0' shared a leaf there).
|
|
18
|
+
ELEMENT_ENCODINGS = [:hex, :binary, :auto]
|
|
19
|
+
|
|
20
|
+
attr_reader :hash_type, :leaf_tag, :branch_tag, :sort_hashes, :element_encoding
|
|
10
21
|
|
|
11
22
|
# Constructor
|
|
23
|
+
# @param [Symbol] element_encoding How elements are interpreted, :hex, :binary or :auto.
|
|
24
|
+
# This has no default on purpose. Guessing it silently changes the merkle root.
|
|
25
|
+
# See ELEMENT_ENCODINGS before reaching for :auto.
|
|
12
26
|
# @param [Symbol] hash_type The hashing algorithm used to hash the internal nodes.
|
|
27
|
+
# @param [String] leaf_tag Tag to use when hashing leaves.
|
|
28
|
+
# Give this and +branch_tag+ different values so that a leaf hash can never equal an internal
|
|
29
|
+
# node hash. With both left empty the tree is not second-preimage resistant.
|
|
13
30
|
# @param [String] branch_tag Tags to use when hashing internal nodes.
|
|
14
31
|
# @param [Boolean] sort_hashes Whether to sort internal nodes in lexicographical order and hash them.
|
|
15
32
|
# If you enable this, Merkle::Proof's directions are not required.
|
|
16
33
|
# @raise [ArgumentError]
|
|
17
|
-
def initialize(hash_type: :sha256, branch_tag: '', sort_hashes: true)
|
|
34
|
+
def initialize(element_encoding:, hash_type: :sha256, leaf_tag: '', branch_tag: '', sort_hashes: true)
|
|
35
|
+
raise ArgumentError, "element_encoding #{element_encoding} does not supported." unless ELEMENT_ENCODINGS.include?(element_encoding)
|
|
18
36
|
raise ArgumentError, "hash_type #{hash_type} does not supported." unless HASH_TYPES.include?(hash_type)
|
|
37
|
+
raise ArgumentError, "leaf_tag must be string." unless leaf_tag.is_a?(String)
|
|
19
38
|
raise ArgumentError, "internal_tag must be string." unless branch_tag.is_a?(String)
|
|
20
39
|
raise ArgumentError, "sort_hashes must be boolean." unless sort_hashes.is_a?(TrueClass) || sort_hashes.is_a?(FalseClass)
|
|
40
|
+
@element_encoding = element_encoding
|
|
21
41
|
@hash_type = hash_type
|
|
22
|
-
|
|
42
|
+
# Freeze the tags. A config is shared by every tree built with it, so mutating one in place
|
|
43
|
+
# would change the root of all of them and invalidate proofs already handed out.
|
|
44
|
+
@leaf_tag = leaf_tag.dup.freeze
|
|
45
|
+
@branch_tag = branch_tag.dup.freeze
|
|
23
46
|
@sort_hashes = sort_hashes
|
|
24
47
|
end
|
|
25
48
|
|
|
26
49
|
# Bitcoin configuration.
|
|
50
|
+
# @param [Symbol] element_encoding How elements are interpreted, :hex, :binary or :auto.
|
|
27
51
|
# @return [Merkle::Config]
|
|
28
|
-
def self.bitcoin
|
|
29
|
-
Config.new(hash_type: :double_sha256, sort_hashes: false)
|
|
52
|
+
def self.bitcoin(element_encoding:)
|
|
53
|
+
Config.new(element_encoding: element_encoding, hash_type: :double_sha256, sort_hashes: false)
|
|
30
54
|
end
|
|
31
55
|
|
|
32
56
|
# Taptree configuration.
|
|
57
|
+
# @param [Symbol] element_encoding How elements are interpreted, :hex, :binary or :auto.
|
|
33
58
|
# @return [Merkle::Config]
|
|
34
|
-
def self.taptree
|
|
35
|
-
Config.new(branch_tag: 'TapBranch')
|
|
59
|
+
def self.taptree(element_encoding:)
|
|
60
|
+
Config.new(element_encoding: element_encoding, leaf_tag: 'TapLeaf', branch_tag: 'TapBranch')
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Convert +element+ into the byte string to be hashed, following element_encoding.
|
|
64
|
+
# @param [String] element An element as given to .from_elements.
|
|
65
|
+
# @return [String] Byte string.
|
|
66
|
+
# @raise [ArgumentError] If +element+ does not match element_encoding.
|
|
67
|
+
def encode_element(element)
|
|
68
|
+
raise ArgumentError, "element must be string." unless element.is_a?(String)
|
|
69
|
+
case element_encoding
|
|
70
|
+
when :hex
|
|
71
|
+
raise ArgumentError, "element must be a hex string." unless hex_string?(element)
|
|
72
|
+
[element].pack('H*')
|
|
73
|
+
when :binary
|
|
74
|
+
element.b
|
|
75
|
+
when :auto
|
|
76
|
+
hex_string?(element) ? [element].pack('H*') : element.b
|
|
77
|
+
end
|
|
36
78
|
end
|
|
37
79
|
|
|
38
|
-
# Generate tagged hash.
|
|
80
|
+
# Generate tagged hash. +data+ is always hashed as a byte string.
|
|
81
|
+
# To hash an element written as hex, pass it through #encode_element first.
|
|
39
82
|
# @param [String] data The data to be hashed.
|
|
40
83
|
# @param [String] tag Tag string used tagging.
|
|
41
84
|
# @return [String] Tagged hash value.
|
|
@@ -43,7 +86,7 @@ module Merkle
|
|
|
43
86
|
raise ArgumentError, "data must be string." unless data.is_a?(String)
|
|
44
87
|
raise ArgumentError, "tag must be a String." unless tag.is_a?(String)
|
|
45
88
|
|
|
46
|
-
data_bin =
|
|
89
|
+
data_bin = data.b
|
|
47
90
|
|
|
48
91
|
unless tag.empty?
|
|
49
92
|
tag_bin = Digest::SHA256.digest(tag).b
|
data/lib/merkle/custom_tree.rb
CHANGED
|
@@ -9,28 +9,29 @@ module Merkle
|
|
|
9
9
|
# Each element can be a leaf hash (hex string) or an array of child nodes.
|
|
10
10
|
def initialize(config:, leaves:)
|
|
11
11
|
super(config: config, leaves: leaves)
|
|
12
|
-
# Validate nested structure before calling super
|
|
13
|
-
validate_leaves!(extract_leaves(leaves))
|
|
14
12
|
end
|
|
15
13
|
|
|
16
14
|
# Create tree from elements with custom structure
|
|
17
15
|
# @param [Merkle::Config] config Configuration for merkle tree.
|
|
18
16
|
# @param [Array] elements A nested array of elements that will be hashed to become leaves.
|
|
19
|
-
#
|
|
20
|
-
def self.from_elements(config:, elements
|
|
17
|
+
# The tag used for the leaf hash comes from +config.leaf_tag+.
|
|
18
|
+
def self.from_elements(config:, elements:)
|
|
21
19
|
raise ArgumentError, 'config must be Merkle::Config' unless config.is_a?(Merkle::Config)
|
|
22
20
|
raise ArgumentError, 'elements must be Array' unless elements.is_a?(Array)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
#
|
|
26
|
-
hashed_structure = convert_elements_to_hashes(elements, config
|
|
27
|
-
|
|
21
|
+
|
|
22
|
+
# Convert elements to hashes while preserving structure. This walks the input before the
|
|
23
|
+
# constructor gets to check it, so it has to enforce the depth limit itself.
|
|
24
|
+
hashed_structure = convert_elements_to_hashes(elements, config)
|
|
25
|
+
|
|
28
26
|
self.new(config: config, leaves: hashed_structure)
|
|
29
27
|
end
|
|
30
28
|
|
|
31
29
|
# Compute merkle root using custom structure
|
|
32
30
|
# @return [String] merkle root
|
|
33
31
|
def compute_root
|
|
32
|
+
# Re-check here rather than trusting the constructor. +leaves+ is readable and its arrays
|
|
33
|
+
# are mutable, so a structure that was rejected at construction can be assembled afterwards.
|
|
34
|
+
validate_leaves!
|
|
34
35
|
all_leaves = extract_leaves(@leaves)
|
|
35
36
|
raise Error, 'leaves is empty' if all_leaves.empty?
|
|
36
37
|
result = compute_node_hash(@leaves)
|
|
@@ -38,12 +39,13 @@ module Merkle
|
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
# Convert nested elements to nested hashes
|
|
41
|
-
def self.convert_elements_to_hashes(node, config,
|
|
42
|
+
def self.convert_elements_to_hashes(node, config, depth = 0)
|
|
42
43
|
if node.is_a?(Array)
|
|
43
|
-
|
|
44
|
+
raise ArgumentError, "Binary tree must not be deeper than #{MAX_DEPTH}" if depth >= MAX_DEPTH
|
|
45
|
+
node.map { |child| convert_elements_to_hashes(child, config, depth + 1) }
|
|
44
46
|
else
|
|
45
47
|
# This is a leaf element, hash it and convert to hex
|
|
46
|
-
config.tagged_hash(node, leaf_tag).unpack1('H*')
|
|
48
|
+
config.tagged_hash(config.encode_element(node), config.leaf_tag).unpack1('H*')
|
|
47
49
|
end
|
|
48
50
|
end
|
|
49
51
|
|
|
@@ -72,12 +74,18 @@ module Merkle
|
|
|
72
74
|
end
|
|
73
75
|
else
|
|
74
76
|
# Leaf node: already a hash, convert to binary
|
|
75
|
-
|
|
77
|
+
decode_hash(node)
|
|
76
78
|
end
|
|
77
79
|
end
|
|
78
80
|
|
|
81
|
+
private_class_method :convert_elements_to_hashes
|
|
82
|
+
private :compute_node_hash
|
|
83
|
+
|
|
79
84
|
# Override generate_proof to work with nested structure
|
|
80
85
|
def generate_proof(leaf_index)
|
|
86
|
+
# Walking the structure before checking it would hit the recursion limit on a structure
|
|
87
|
+
# assembled after construction, and SystemStackError escapes the caller's rescue.
|
|
88
|
+
validate_leaves!
|
|
81
89
|
all_leaves = extract_leaves(@leaves)
|
|
82
90
|
raise ArgumentError, 'leaf_index must be Integer' unless leaf_index.is_a?(Integer)
|
|
83
91
|
raise ArgumentError, 'leaf_index out of range' if leaf_index < 0 || all_leaves.length <= leaf_index
|
|
@@ -107,23 +115,37 @@ module Merkle
|
|
|
107
115
|
end
|
|
108
116
|
end
|
|
109
117
|
|
|
110
|
-
# Validate that
|
|
111
|
-
def validate_leaves!
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
end
|
|
115
|
-
validate_binary_structure(@leaves)
|
|
118
|
+
# Validate that the structure is a binary tree and that every leaf is a node hash.
|
|
119
|
+
def validate_leaves!
|
|
120
|
+
validate_binary_structure(@leaves, root: true)
|
|
121
|
+
extract_leaves(@leaves).each { |leaf| decode_hash(leaf) }
|
|
116
122
|
end
|
|
117
|
-
|
|
118
|
-
# Validate that the structure is a binary tree (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
|
|
124
|
+
# Validate that the structure is a binary tree (exactly 2 children per node)
|
|
125
|
+
# @param [Object] node A subtree (nested Array) or a leaf hash.
|
|
126
|
+
# @param [Boolean] root Whether +node+ is the whole tree.
|
|
127
|
+
# @param [Integer] depth How far below the root +node+ sits.
|
|
128
|
+
def validate_binary_structure(node, root: false, depth: 0)
|
|
129
|
+
return unless node.is_a?(Array)
|
|
130
|
+
# +depth+ counts the branches above this node, so a node here puts its children at
|
|
131
|
+
# depth + 1. Stopping at MAX_DEPTH keeps the deepest leaf within MAX_DEPTH branches,
|
|
132
|
+
# which is what Proof::MAX_SIBLINGS allows a proof to carry.
|
|
133
|
+
raise ArgumentError, "Binary tree must not be deeper than #{MAX_DEPTH}" if depth >= MAX_DEPTH
|
|
134
|
+
|
|
135
|
+
case node.length
|
|
136
|
+
when 0
|
|
137
|
+
raise ArgumentError, "Binary tree nodes cannot be empty"
|
|
138
|
+
when 1
|
|
139
|
+
# A node with one child contributes no branch hash, it just passes the child up.
|
|
140
|
+
# That would let [[a]] and [a, [b]] commit to the same root as [a] and [a, b].
|
|
141
|
+
# A tree holding a single leaf is the one case where there is nothing to confuse it with.
|
|
142
|
+
unless root && !node[0].is_a?(Array)
|
|
143
|
+
raise ArgumentError, "Binary tree nodes must have 2 children unless the tree is a single leaf"
|
|
125
144
|
end
|
|
126
|
-
|
|
145
|
+
when 2
|
|
146
|
+
node.each { |child| validate_binary_structure(child, depth: depth + 1) }
|
|
147
|
+
else
|
|
148
|
+
raise ArgumentError, "Binary tree nodes can have at most 2 children, got #{node.length}"
|
|
127
149
|
end
|
|
128
150
|
end
|
|
129
151
|
|
data/lib/merkle/proof.rb
CHANGED
|
@@ -4,8 +4,9 @@ module Merkle
|
|
|
4
4
|
|
|
5
5
|
# Upper bound on the number of siblings, i.e. the depth of the tree the proof came from.
|
|
6
6
|
# A proof longer than this cannot correspond to any realistic tree, so it is rejected
|
|
7
|
-
# rather than hashed.
|
|
8
|
-
|
|
7
|
+
# rather than hashed. It matches the deepest tree the library will build, which is also the
|
|
8
|
+
# deepest a BIP341 script tree can be: a control block carries at most 128 path elements.
|
|
9
|
+
MAX_SIBLINGS = MAX_DEPTH
|
|
9
10
|
|
|
10
11
|
attr_reader :config, :root, :leaf, :siblings, :directions
|
|
11
12
|
|
|
@@ -19,12 +20,14 @@ module Merkle
|
|
|
19
20
|
def initialize(config:, root:, leaf:, siblings:, directions: [])
|
|
20
21
|
raise ArgumentError, 'config must be a Merkle::Config' unless config.is_a?(Merkle::Config)
|
|
21
22
|
raise ArgumentError, 'root must be string' unless root.is_a?(String)
|
|
23
|
+
raise ArgumentError, "root must be a #{HASH_SIZE * 2}-character hex string" unless node_hash?(root)
|
|
22
24
|
raise ArgumentError, 'leaf must be string' unless leaf.is_a?(String)
|
|
25
|
+
raise ArgumentError, "leaf must be a #{HASH_SIZE * 2}-character hex string" unless node_hash?(leaf)
|
|
23
26
|
raise ArgumentError, 'siblings must be an Array' unless siblings.is_a?(Array)
|
|
24
27
|
raise ArgumentError, "siblings must not exceed #{MAX_SIBLINGS} elements" if siblings.length > MAX_SIBLINGS
|
|
25
28
|
siblings.each do |sibling|
|
|
26
29
|
raise ArgumentError, 'sibling must be string' unless sibling.is_a?(String)
|
|
27
|
-
raise ArgumentError,
|
|
30
|
+
raise ArgumentError, "sibling must be a #{HASH_SIZE * 2}-character hex string" unless node_hash?(sibling)
|
|
28
31
|
end
|
|
29
32
|
raise ArgumentError, 'directions must be an Array' unless directions.is_a?(Array)
|
|
30
33
|
raise ArgumentError, 'No directions are required because sorted_hash is enabled' if config.sort_hashes && !directions.empty?
|
|
@@ -33,19 +36,22 @@ module Merkle
|
|
|
33
36
|
raise ArgumentError, 'direction must be 0 or 1' unless directions.all? { |direction| direction == 0 || direction == 1 }
|
|
34
37
|
end
|
|
35
38
|
@config = config
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@
|
|
39
|
+
# Normalize and freeze. The checks above only bind if the arrays cannot grow afterwards:
|
|
40
|
+
# a caller holding the array it passed in could otherwise append past MAX_SIBLINGS, or
|
|
41
|
+
# shorten directions until #valid? reads nil and folds as if the sibling were on the right.
|
|
42
|
+
@root = normalize_hash(root)
|
|
43
|
+
@leaf = normalize_hash(leaf)
|
|
44
|
+
@siblings = siblings.map { |sibling| normalize_hash(sibling) }.freeze
|
|
45
|
+
@directions = directions.dup.freeze
|
|
40
46
|
end
|
|
41
47
|
|
|
42
48
|
# Verify the proof.
|
|
43
49
|
# @return [Boolean] true if the proof is valid, false otherwise.
|
|
44
50
|
def valid?
|
|
45
|
-
current =
|
|
51
|
+
current = decode_hash(leaf)
|
|
46
52
|
|
|
47
53
|
siblings.each_with_index do |sibling, index|
|
|
48
|
-
sibling_bin =
|
|
54
|
+
sibling_bin = decode_hash(sibling)
|
|
49
55
|
|
|
50
56
|
if config.sort_hashes
|
|
51
57
|
# Sort lexicographically when combining
|
|
@@ -59,7 +65,9 @@ module Merkle
|
|
|
59
65
|
current = config.tagged_hash(combined)
|
|
60
66
|
end
|
|
61
67
|
|
|
62
|
-
|
|
68
|
+
# Compare the decoded bytes. Comparing the hex would make the result depend on the case
|
|
69
|
+
# the caller happened to write +root+ in, even though both spell the same hash.
|
|
70
|
+
current == decode_hash(root)
|
|
63
71
|
end
|
|
64
72
|
|
|
65
73
|
end
|
data/lib/merkle/util.rb
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
module Merkle
|
|
2
2
|
module Util
|
|
3
3
|
|
|
4
|
+
# Size of a node hash in bytes. Leaves, siblings and internal nodes are all this size.
|
|
5
|
+
HASH_SIZE = 32
|
|
6
|
+
|
|
7
|
+
# Deepest tree accepted. A tree deeper than this cannot be walked without risking a
|
|
8
|
+
# SystemStackError, which is not a StandardError and so escapes a caller's rescue.
|
|
9
|
+
# 128 is also the deepest a BIP341 script tree can be.
|
|
10
|
+
MAX_DEPTH = 128
|
|
11
|
+
|
|
4
12
|
# Check whether +data+ is hex string or not.
|
|
5
13
|
# An odd-length string is not a hex string. Treating it as one would let
|
|
6
14
|
# +pack('H*')+ pad the missing nibble with zero, so 'abc' and 'abc0' would
|
|
@@ -10,16 +18,44 @@ module Merkle
|
|
|
10
18
|
# @raise [ArgumentError]
|
|
11
19
|
def hex_string?(data)
|
|
12
20
|
raise ArgumentError, 'data must be string' unless data.is_a?(String)
|
|
13
|
-
|
|
21
|
+
# Match on the bytes. Matching the string itself raises Encoding::CompatibilityError for a
|
|
22
|
+
# UTF-16 string and ArgumentError for invalid UTF-8, neither of which the caller expects.
|
|
23
|
+
bytes = data.b
|
|
24
|
+
bytes.bytesize.even? && bytes.match?(/\A[0-9a-fA-F]+\z/)
|
|
14
25
|
end
|
|
15
26
|
|
|
16
|
-
#
|
|
17
|
-
# @param [String]
|
|
18
|
-
# @return [
|
|
27
|
+
# Check whether +hex+ is the hex representation of a node hash.
|
|
28
|
+
# @param [String] hex
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
def node_hash?(hex)
|
|
31
|
+
return false unless hex.is_a?(String)
|
|
32
|
+
# Match on the bytes. Matching the string itself raises on a value that claims to be UTF-8
|
|
33
|
+
# but holds invalid bytes, which would surface as an unrelated ArgumentError.
|
|
34
|
+
bytes = hex.b
|
|
35
|
+
bytes.bytesize == HASH_SIZE * 2 && bytes.match?(/\A[0-9a-fA-F]+\z/)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Convert a node hash from its hex representation to binary.
|
|
39
|
+
# Node hashes are always +HASH_SIZE+ bytes written as hex, so anything else is rejected
|
|
40
|
+
# rather than guessed at. Accepting arbitrary lengths here would make the concatenation
|
|
41
|
+
# in an internal node ambiguous: ['aa', 'bbcc'] and ['aabb', 'cc'] would hash alike.
|
|
42
|
+
# @param [String] hex
|
|
43
|
+
# @return [String] Binary format hash.
|
|
19
44
|
# @raise [ArgumentError]
|
|
20
|
-
def
|
|
21
|
-
raise ArgumentError, '
|
|
22
|
-
|
|
45
|
+
def decode_hash(hex)
|
|
46
|
+
raise ArgumentError, 'hash must be string' unless hex.is_a?(String)
|
|
47
|
+
raise ArgumentError, "hash must be a #{HASH_SIZE * 2}-character hex string" unless node_hash?(hex)
|
|
48
|
+
[hex].pack('H*')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Rewrite a node hash in the one spelling the library uses: lower case hex.
|
|
52
|
+
# Upper case names the same hash, so returning it verbatim would let a caller that matches
|
|
53
|
+
# leaves as strings miss a leaf whose proof verifies.
|
|
54
|
+
# @param [String] hex
|
|
55
|
+
# @return [String] Frozen lower case hex.
|
|
56
|
+
# @raise [ArgumentError]
|
|
57
|
+
def normalize_hash(hex)
|
|
58
|
+
bin_to_hex(decode_hash(hex)).freeze
|
|
23
59
|
end
|
|
24
60
|
|
|
25
61
|
# Convert binary string +data+ to hex string.
|
|
@@ -28,7 +64,7 @@ module Merkle
|
|
|
28
64
|
# @raise [ArgumentError]
|
|
29
65
|
def bin_to_hex(data)
|
|
30
66
|
raise ArgumentError, 'data must be string' unless data.is_a?(String)
|
|
31
|
-
|
|
67
|
+
data.unpack1('H*')
|
|
32
68
|
end
|
|
33
69
|
|
|
34
70
|
# Combine two elements(+left+ and +right+) with sort configuration.
|
|
@@ -51,4 +87,4 @@ module Merkle
|
|
|
51
87
|
end
|
|
52
88
|
|
|
53
89
|
end
|
|
54
|
-
end
|
|
90
|
+
end
|
data/lib/merkle/version.rb
CHANGED