merkle_tree 0.1.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 +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +361 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/merkle_tree.rb +258 -0
- data/lib/merkle_tree/leaf.rb +57 -0
- data/lib/merkle_tree/node.rb +137 -0
- data/lib/merkle_tree/version.rb +5 -0
- data/merkle_tree.gemspec +33 -0
- data/spec/perf/speed_spec.rb +26 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/unit/add_spec.rb +36 -0
- data/spec/unit/auth_path_spec.rb +18 -0
- data/spec/unit/empty_spec.rb +15 -0
- data/spec/unit/height_spec.rb +15 -0
- data/spec/unit/include_spec.rb +27 -0
- data/spec/unit/leaf/build_spec.rb +9 -0
- data/spec/unit/leaf/eql_spec.rb +16 -0
- data/spec/unit/leaves_spec.rb +16 -0
- data/spec/unit/new_spec.rb +80 -0
- data/spec/unit/node/build_spec.rb +56 -0
- data/spec/unit/node/eql_spec.rb +31 -0
- data/spec/unit/root_spec.rb +15 -0
- data/spec/unit/size_spec.rb +15 -0
- data/spec/unit/subtree_spec.rb +35 -0
- data/spec/unit/to_s_spec.rb +17 -0
- data/spec/unit/update_spec.rb +25 -0
- data/tasks/console.rake +9 -0
- data/tasks/coverage.rake +9 -0
- data/tasks/spec.rake +32 -0
- metadata +137 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MerkleTree
|
4
|
+
# An object to hold one-time signature
|
5
|
+
# @api private
|
6
|
+
class Leaf
|
7
|
+
include Comparable
|
8
|
+
|
9
|
+
attr_reader :height
|
10
|
+
|
11
|
+
attr_accessor :value
|
12
|
+
|
13
|
+
attr_reader :left_index
|
14
|
+
|
15
|
+
attr_reader :right_index
|
16
|
+
|
17
|
+
def self.build(value, position, digest: MerkleTree.default_digest)
|
18
|
+
new(digest.(value), position, position)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Create a leaf node
|
22
|
+
#
|
23
|
+
# @api private
|
24
|
+
def initialize(value, left_index, right_index)
|
25
|
+
@value = value
|
26
|
+
@left_index = left_index
|
27
|
+
@right_index = right_index
|
28
|
+
@height = 0
|
29
|
+
end
|
30
|
+
|
31
|
+
def leaf?
|
32
|
+
true
|
33
|
+
end
|
34
|
+
|
35
|
+
def include?(index)
|
36
|
+
(left_index..right_index).cover?(index)
|
37
|
+
end
|
38
|
+
|
39
|
+
def size
|
40
|
+
1
|
41
|
+
end
|
42
|
+
|
43
|
+
def <=>(other)
|
44
|
+
value <=> other.value &&
|
45
|
+
left_index <=> other.left_index &&
|
46
|
+
right_index <=> other.right_index
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_h
|
50
|
+
{ value: value }
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_s(indent = '')
|
54
|
+
indent + value
|
55
|
+
end
|
56
|
+
end # Leaf
|
57
|
+
end # MerkleTree
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MerkleTree
|
4
|
+
# Holds information about intermediate hashes
|
5
|
+
# @api private
|
6
|
+
class Node
|
7
|
+
include Comparable
|
8
|
+
|
9
|
+
UNDEFINED = Module.new
|
10
|
+
|
11
|
+
attr_reader :value
|
12
|
+
|
13
|
+
attr_accessor :left
|
14
|
+
|
15
|
+
attr_accessor :right
|
16
|
+
|
17
|
+
# The node height in the tree
|
18
|
+
attr_reader :height
|
19
|
+
|
20
|
+
# The sequential position in the tree
|
21
|
+
attr_reader :left_index
|
22
|
+
|
23
|
+
attr_reader :right_index
|
24
|
+
|
25
|
+
def self.build(left, right, digest: MerkleTree.default_digest)
|
26
|
+
value = digest.(left.value + right.value)
|
27
|
+
height = left.height + 1
|
28
|
+
left_index = left.left_index
|
29
|
+
right_index = right.right_index
|
30
|
+
|
31
|
+
new(value, left, right, height, left_index, right_index)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Create a node
|
35
|
+
#
|
36
|
+
# @api private
|
37
|
+
def initialize(value, left, right, height, left_index, right_index)
|
38
|
+
@value = value
|
39
|
+
@left = left
|
40
|
+
@right = right
|
41
|
+
@height = height
|
42
|
+
@left_index = left_index
|
43
|
+
@right_index = right_index
|
44
|
+
end
|
45
|
+
|
46
|
+
def leaf?
|
47
|
+
false
|
48
|
+
end
|
49
|
+
|
50
|
+
def size
|
51
|
+
left.size + 1 + right.size
|
52
|
+
end
|
53
|
+
|
54
|
+
def include?(index)
|
55
|
+
(left_index..right_index).cover?(index)
|
56
|
+
end
|
57
|
+
|
58
|
+
def update(digest)
|
59
|
+
@value = digest.(left.value + right.value)
|
60
|
+
end
|
61
|
+
|
62
|
+
def child(index)
|
63
|
+
if left.include?(index)
|
64
|
+
left
|
65
|
+
else
|
66
|
+
right.include?(index) ? right : EMPTY
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Find sibling child node for the index
|
71
|
+
def sibling(index)
|
72
|
+
if left.include?(index)
|
73
|
+
[:right, right.value]
|
74
|
+
else
|
75
|
+
right.include?(index) ? [:left, left.value] : EMPTY
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Find subtree that matches the index
|
80
|
+
def subtree(index)
|
81
|
+
if left.include?(index)
|
82
|
+
left
|
83
|
+
else
|
84
|
+
right.include?(index) ? right : EMPTY
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def <=>(other)
|
89
|
+
value <=> other.value &&
|
90
|
+
left_index <=> other.left_index &&
|
91
|
+
right_index <=> other.right_index
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_h
|
95
|
+
{ value: value, left: left.to_h, right: right.to_h }
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_s(indent = '')
|
99
|
+
indent + value.to_s + $RS +
|
100
|
+
left.to_s(indent + ' ') + $RS +
|
101
|
+
right.to_s(indent + ' ')
|
102
|
+
end
|
103
|
+
|
104
|
+
# An empty node used as placeholder
|
105
|
+
# @api private
|
106
|
+
class EmptyNode < Node
|
107
|
+
def initialize
|
108
|
+
@value = ''
|
109
|
+
@height = 0
|
110
|
+
@left = UNDEFINED
|
111
|
+
@right = UNDEFINED
|
112
|
+
@left_index = UNDEFINED
|
113
|
+
@right_index = UNDEFINED
|
114
|
+
end
|
115
|
+
|
116
|
+
def size
|
117
|
+
0
|
118
|
+
end
|
119
|
+
|
120
|
+
def sibling(*)
|
121
|
+
[]
|
122
|
+
end
|
123
|
+
|
124
|
+
def subtree(*)
|
125
|
+
{}
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_s; end
|
129
|
+
|
130
|
+
def to_h
|
131
|
+
{}
|
132
|
+
end
|
133
|
+
end # EmptyNode
|
134
|
+
|
135
|
+
EMPTY = EmptyNode.new.freeze
|
136
|
+
end # Node
|
137
|
+
end # MerkleTree
|
data/merkle_tree.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "merkle_tree/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "merkle_tree"
|
7
|
+
spec.version = MerkleTree::VERSION
|
8
|
+
spec.authors = ["Piotr Murach"]
|
9
|
+
spec.email = ["me@piotrmurach.com"]
|
10
|
+
|
11
|
+
spec.summary = %q{A binary tree of one-time signatures known as a merkle tree.}
|
12
|
+
spec.description = %q{A binary tree of one-time singatures known as a merkle tree. Often used in distributed systems such as Git, Cassandra or Bitcoin for efficiently summarizing sets of data.}
|
13
|
+
spec.homepage = "https://github.com/piotrmurach/merkle_tree"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
if spec.respond_to?(:metadata)
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/piotrmurach/merkle_tree"
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/piotrmurach/merkle_tree/blob/master/CHANGELOG.md"
|
20
|
+
end
|
21
|
+
|
22
|
+
spec.files = Dir['{lib,spec,examples}/**/*.rb']
|
23
|
+
spec.files += Dir['{bin,tasks}/*', 'merkle_tree.gemspec']
|
24
|
+
spec.files += Dir['README.md', 'CHANGELOG.md', 'LICENSE.txt', 'Rakefile']
|
25
|
+
spec.bindir = "exe"
|
26
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
|
+
spec.require_paths = ["lib"]
|
28
|
+
|
29
|
+
spec.add_development_dependency "bundler", ">= 1.17"
|
30
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
31
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
32
|
+
spec.add_development_dependency "rspec-benchmark"
|
33
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe 'speed performance' do
|
4
|
+
it "creates merkle trees in linear time" do
|
5
|
+
messages = bench_range(8, 8 << 12).map do |n|
|
6
|
+
Array.new(n) { "L#{n}" }
|
7
|
+
end
|
8
|
+
|
9
|
+
expect { |n, i|
|
10
|
+
MerkleTree.new(*messages[i])
|
11
|
+
}.to perform_linear.in_range(8, 8 << 12)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "checks if a message belongs in logarithmic time" do
|
15
|
+
trees = []
|
16
|
+
bench_range(8, 8 << 12).each do |n|
|
17
|
+
messages = []
|
18
|
+
n.times { |i| messages << "L#{i}" }
|
19
|
+
trees << MerkleTree.new(*messages)
|
20
|
+
end
|
21
|
+
|
22
|
+
expect { |n, i|
|
23
|
+
trees[i].include?("L#{n/2}", n/2)
|
24
|
+
}.to perform_logarithmic.in_range(8, 8 << 12).sample(100).times
|
25
|
+
end
|
26
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
if ENV['COVERAGE'] || ENV['TRAVIS']
|
4
|
+
require 'simplecov'
|
5
|
+
require 'coveralls'
|
6
|
+
|
7
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
8
|
+
SimpleCov::Formatter::HTMLFormatter,
|
9
|
+
Coveralls::SimpleCov::Formatter
|
10
|
+
]
|
11
|
+
|
12
|
+
SimpleCov.start do
|
13
|
+
command_name 'spec'
|
14
|
+
add_filter 'spec'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
require "bundler/setup"
|
19
|
+
require "rspec-benchmark"
|
20
|
+
require "merkle_tree"
|
21
|
+
|
22
|
+
RSpec.configure do |config|
|
23
|
+
config.include RSpec::Benchmark::Matchers
|
24
|
+
|
25
|
+
# Enable flags like --only-failures and --next-failure
|
26
|
+
# config.example_status_persistence_file_path = ".rspec_status"
|
27
|
+
|
28
|
+
# Disable RSpec exposing methods globally on `Module` and `main`
|
29
|
+
config.disable_monkey_patching!
|
30
|
+
|
31
|
+
config.expect_with :rspec do |c|
|
32
|
+
c.syntax = :expect
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe MerkleTree, '#add' do
|
4
|
+
it "adds one message" do
|
5
|
+
merkle_tree = MerkleTree.new("L1", "L2", "L3", "L4")
|
6
|
+
expanded_tree = MerkleTree.new("L1", "L2", "L3", "L4", "L5")
|
7
|
+
|
8
|
+
merkle_tree << "L5"
|
9
|
+
|
10
|
+
expect(merkle_tree.leaves.size).to eq(expanded_tree.leaves.size)
|
11
|
+
expect(merkle_tree.size).to eq(expanded_tree.size)
|
12
|
+
expect(merkle_tree.root.value).to eq(expanded_tree.root.value)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "adds even messages" do
|
16
|
+
merkle_tree = MerkleTree.new("L1", "L2", "L3", "L4")
|
17
|
+
expanded_tree = MerkleTree.new("L1", "L2", "L3", "L4", "L5", "L6")
|
18
|
+
|
19
|
+
merkle_tree.add("L5", "L6")
|
20
|
+
|
21
|
+
expect(merkle_tree.leaves.size).to eq(expanded_tree.leaves.size)
|
22
|
+
expect(merkle_tree.size).to eq(expanded_tree.size)
|
23
|
+
expect(merkle_tree.root.value).to eq(expanded_tree.root.value)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "adds messages double the size" do
|
27
|
+
merkle_tree = MerkleTree.new("L1", "L2", "L3", "L4")
|
28
|
+
expanded_tree = MerkleTree.new("L1", "L2", "L3", "L4", "L5", "L6", "L7", "L8")
|
29
|
+
|
30
|
+
merkle_tree.add("L5", "L6", "L7", "L8")
|
31
|
+
|
32
|
+
expect(merkle_tree.leaves.size).to eq(expanded_tree.leaves.size)
|
33
|
+
expect(merkle_tree.root.value).to eq(expanded_tree.root.value)
|
34
|
+
expect(merkle_tree.size).to eq(expanded_tree.size)
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe MerkleTree, '#auth_path' do
|
4
|
+
it "fails to find authentication path for an index" do
|
5
|
+
merkle_tree = MerkleTree.new("L1", "L2", "L3", "L4")
|
6
|
+
|
7
|
+
expect(merkle_tree.auth_path(100)).to eq([MerkleTree::Node::EMPTY])
|
8
|
+
end
|
9
|
+
|
10
|
+
it "finds authentication path for an index" do
|
11
|
+
merkle_tree = MerkleTree.new("L1", "L2", "L3", "L4")
|
12
|
+
|
13
|
+
expect(merkle_tree.auth_path(2)).to eq([
|
14
|
+
[:left,"f2b92f33b56466fce14bc2ccf6a92f6edfcd8111446644c20221d6ae831dd67c"],
|
15
|
+
[:right,"4a5a97c6433c4c062457e9335709d57493e75527809d8a9586c141e591ac9f2c"]
|
16
|
+
])
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe MerkleTree, '#empty?' do
|
4
|
+
it "returns true when tree has no messages" do
|
5
|
+
merkle_tree = MerkleTree.new
|
6
|
+
|
7
|
+
expect(merkle_tree.empty?).to eq(true)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "returns false when tree has messages" do
|
11
|
+
merkle_tree = MerkleTree.new("L1", "L2", "L3", "L4", "L5", "L6", "L7", "L8")
|
12
|
+
|
13
|
+
expect(merkle_tree.empty?).to eq(false)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe MerkleTree, '#height' do
|
4
|
+
it "has no messages" do
|
5
|
+
merkle_tree = MerkleTree.new
|
6
|
+
|
7
|
+
expect(merkle_tree.height).to eq(0)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "calculates tree height" do
|
11
|
+
merkle_tree = MerkleTree.new("L1", "L2", "L3", "L4", "L5", "L6")
|
12
|
+
|
13
|
+
expect(merkle_tree.height).to eq(3)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe MerkleTree, '#include?' do
|
4
|
+
it "checks message inclusion in an empty tree" do
|
5
|
+
merkle_tree = MerkleTree.new
|
6
|
+
|
7
|
+
expect(merkle_tree.include?("L3", 2)).to eq(false)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "checks valid message inclusion in 4 signatures tree" do
|
11
|
+
merkle_tree = MerkleTree.new("L1", "L2", "L3", "L4")
|
12
|
+
|
13
|
+
expect(merkle_tree.include?("L3", 2)).to eq(true)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "checks valid message inclusion in 8 signatures tree" do
|
17
|
+
merkle_tree = MerkleTree.new("L1", "L2", "L3", "L4", "L5", "L6", "L7", "L8")
|
18
|
+
|
19
|
+
expect(merkle_tree.include?("L5", 4)).to eq(true)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "checks invalid message inclusion in 8 signatures tree" do
|
23
|
+
merkle_tree = MerkleTree.new("L1", "L2", "L3", "L4", "L5", "L6", "L7", "L8")
|
24
|
+
|
25
|
+
expect(merkle_tree.include?("invalid", 4)).to eq(false)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe MerkleTree::Leaf, '::build' do
|
4
|
+
it "creates a leaf node" do
|
5
|
+
leaf_node = MerkleTree::Leaf.build("L1", 0)
|
6
|
+
|
7
|
+
expect(leaf_node.value).to eq("dffe8596427fc50e8f64654a609af134d45552f18bbecef90b31135a9e7acaa0")
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe MerkleTree::Leaf, '#==' do
|
4
|
+
it "compares two different leaves with the same message" do
|
5
|
+
leaf_a = MerkleTree::Leaf.build("L1", 0)
|
6
|
+
leaf_b = MerkleTree::Leaf.build("L1", 1)
|
7
|
+
|
8
|
+
expect(leaf_a).to_not eq(leaf_b)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "compares successfully only with the same leaf" do
|
12
|
+
leaf = MerkleTree::Leaf.build("L1", 0)
|
13
|
+
|
14
|
+
expect(leaf).to eq(leaf)
|
15
|
+
end
|
16
|
+
end
|