-
Notifications
You must be signed in to change notification settings - Fork 33
/
Copy pathmerkletree.rb
174 lines (133 loc) · 4.45 KB
/
merkletree.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# encoding: utf-8
require 'digest' # for hash checksum digest function SHA256
require 'pp' # for pp => pretty printer
## require 'date'
## require 'time'
## require 'json'
## require 'uri'
## our own code
require 'merkletree/version' # note: let version always go first
class MerkleTree
class Node
attr_reader :value
attr_reader :left
attr_reader :right
def initialize( value, left, right )
@value = value
@left = left
@right = right
end
####
## for debugging / testing add pretty printing (dump tree)
def dump() do_dump( 0 ); end
def do_dump( depth ) ## dump (recursive_worker)
depth.times { print ' ' }
print "#{depth}:[#{value}] "
if @left
print '{'
puts
@left.do_dump( depth+1 )
@right.do_dump( depth+1) if @right # note: make right node optional (might be nil/empty)
depth.times { print ' ' }
print '}'
end
puts
end # do_dump
end # class Node
## convenience helpers
def self.for( *args )
if args.size == 1 && args[0].is_a?( Array )
transactions = args[0] ## "unwrap" array in array
else
transactions = args ## use "auto-wrapped" splat array
end
## for now use to_s for calculation hash
hashes = transactions.map { |tx| calc_hash( tx.to_s ) }
self.new( hashes )
end
def self.compute_root_for( *args )
if args.size == 1 && args[0].is_a?( Array )
transactions = args[0] ## "unwrap" array in array
else
transactions = args ## use "auto-wrapped" splat array
end
## for now use to_s for calculation hash
hashes = transactions.map { |tx| calc_hash( tx.to_s ) }
self.compute_root( hashes )
end
attr_reader :root
attr_reader :leaves
def initialize( *args )
if args.size == 1 && args[0].is_a?( Array )
hashes = args[0] ## "unwrap" array in array
else
hashes = args ## use "auto-wrapped" splat array
end
@hashes = hashes
@root = build_tree
end
def build_tree
level = @leaves = @hashes.map { |hash| Node.new( hash, nil, nil ) }
## todo/fix: handle hashes.size == 0 case
## - throw exception - why? why not?
## - return empty node with hash '0' - why? why not?
if @hashes.size == 1
level[0]
else
## while there's more than one hash in the layer, keep looping...
while level.size > 1
## loop through hashes two at a time
level = level.each_slice(2).map do |left, right|
## note: handle special case
# if number of nodes is odd e.g. 3,5,7,etc.
# last right node is nil -- duplicate node value for hash
## todo/check - duplicate just hash? or add right node ref too - why? why not?
right = left if right.nil?
Node.new( MerkleTree.calc_hash( left.value + right.value ), left, right)
end
## debug output
## puts "current merkle hash level (#{level.size} nodes):"
## pp level
end
### finally we end up with a single hash
level[0]
end
end # method build tree
### shortcut/convenience - compute root hash w/o building tree nodes
def self.compute_root( *args )
if args.size == 1 && args[0].is_a?( Array )
hashes = args[0] ## "unwrap" array in array
else
hashes = args ## use "auto-wrapped" splat array
end
## todo/fix: handle hashes.size == 0 case
## - throw exception - why? why not?
## - return empty node with hash '0' - why? why not?
if hashes.size == 1
hashes[0]
else
## while there's more than one hash in the list, keep looping...
while hashes.size > 1
# if number of hashes is odd e.g. 3,5,7,etc., duplicate last hash in list
hashes << hashes[-1] if hashes.size % 2 != 0
## loop through hashes two at a time
hashes = hashes.each_slice(2).map do |left,right|
## join both hashes slice[0]+slice[1] together
hash = calc_hash( left + right )
end
end
## debug output
## puts "current merkle hashes (#{hashes.size}):"
## pp hashes
### finally we end up with a single hash
hashes[0]
end
end # method compute_root
def self.calc_hash( data )
sha = Digest::SHA256.new
sha.update( data )
sha.hexdigest
end
end # class MerkleTree
# say hello
puts MerkleTree.banner ## if defined?($RUBYLIBS_DEBUG) && $RUBYLIBS_DEBUG