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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MerkleTree
4
+ VERSION = '0.1.0'
5
+ end
@@ -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
@@ -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