merkle-hash-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.
- data/.gitignore +1 -0
- data/Gemfile +3 -0
- data/Guardfile +13 -0
- data/README.md +148 -0
- data/Rakefile +38 -0
- data/doc/DAI.md +111 -0
- data/lib/merkle-hash-tree.rb +283 -0
- data/lib/range_extensions.rb +13 -0
- data/merkle-hash-tree.gemspec +31 -0
- data/spec/audit_proof_spec.rb +116 -0
- data/spec/consistency_proof_spec.rb +41 -0
- data/spec/dai_caching_spec.rb +42 -0
- data/spec/head_n_spec.rb +36 -0
- data/spec/head_spec.rb +45 -0
- data/spec/power_of_2_smaller_than_spec.rb +37 -0
- data/spec/spec_helper.rb +34 -0
- metadata +243 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
/Gemfile.lock
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
guard 'spork' do
|
2
|
+
watch('Gemfile') { :rspec }
|
3
|
+
watch('Gemfile.lock') { :rspec }
|
4
|
+
watch('spec/spec_helper.rb') { :rspec }
|
5
|
+
end
|
6
|
+
|
7
|
+
guard 'rspec',
|
8
|
+
:cmd => "rspec --drb",
|
9
|
+
:all_on_start => true,
|
10
|
+
:all_after_pass => true do
|
11
|
+
watch(%r{^spec/.+_spec\.rb$})
|
12
|
+
watch(%r{^lib/}) { "spec" }
|
13
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
This gem contains an implementation of "Merkle Hash Trees" (MHT).
|
2
|
+
Specifically, it implements the variant described in
|
3
|
+
[RFC6962](http://tools.ietf.org/html/rfc6962), as the initial use-case for
|
4
|
+
this gem was for an implementation of a [Certificate
|
5
|
+
Transparency](http://www.certificate-transparency.org/) log server.
|
6
|
+
|
7
|
+
# Installation
|
8
|
+
|
9
|
+
Installation should be trivial, if you're using rubygems:
|
10
|
+
|
11
|
+
gem install merkle-hash-tree
|
12
|
+
|
13
|
+
If you want to install directly from the git repo, run `rake install`.
|
14
|
+
|
15
|
+
|
16
|
+
# Usage
|
17
|
+
|
18
|
+
Using `MerkleHashTree` is relatively straightforward, although it does have
|
19
|
+
one or two intricacies. Because MHTs typically deal with large volumes of
|
20
|
+
data, it isn't enough to just load a giant list of objects into memory and
|
21
|
+
go to town -- you'll run out of memory pretty quickly, and on a large tree
|
22
|
+
you'll likely burn a lot of CPU time computing hashes. Instead, in order to
|
23
|
+
instantiate an MHT you must first construct an object that implements a
|
24
|
+
specific interface, which the MHT implementation then uses to interact with
|
25
|
+
your dataset.
|
26
|
+
|
27
|
+
## Basic Usage
|
28
|
+
|
29
|
+
For now, though, let's assume that you have such an object, named
|
30
|
+
`mht_data`, and we'll look at how to use the MHT. (It might be useful to
|
31
|
+
understand [how MHT proofs
|
32
|
+
work](http://www.certificate-transparency.org/log-proofs-work) before you go
|
33
|
+
too deeply into this).
|
34
|
+
|
35
|
+
For starters, we'll create a new MHT:
|
36
|
+
|
37
|
+
mht = MerkleHashTree.new(mht_data, Digest::SHA256)
|
38
|
+
|
39
|
+
The `MerkleHashTree` constructor takes exactly two arguments: an object that
|
40
|
+
implements the data access interface we'll talk about later, and a class (or
|
41
|
+
object) which implements the same `digest` method signature as the core
|
42
|
+
`Digest::Base` class. Typically, this will simply be a `Digest` subclass,
|
43
|
+
such as `Digest::MD5`, `Digest::SHA1`, or (as in the example above)
|
44
|
+
`Digest::SHA256`. This second argument is the way that the MHT calculates
|
45
|
+
hashes in the tree -- it simply calls the `#digest` method on whatever you
|
46
|
+
pass in as the second argument, passing in a string and expecting raw octets
|
47
|
+
out the other end.
|
48
|
+
|
49
|
+
Once we have our MHT object, we can start to do things with it. For
|
50
|
+
example, we can get the hash of the "head" of the tree:
|
51
|
+
|
52
|
+
mht.head # => "<some long string of octets>"
|
53
|
+
|
54
|
+
You can also get the head of any subtree, by specifying the
|
55
|
+
first and last elements of the list to be covered by the subtree:
|
56
|
+
|
57
|
+
mht.head(16, 20) # => "<some more octets>"
|
58
|
+
|
59
|
+
Note that the beginning element must be a power of 2.
|
60
|
+
|
61
|
+
If you want to get the subtree from 0 to an arbitrary element in the list,
|
62
|
+
you can just specify the last element:
|
63
|
+
|
64
|
+
mht.head(42) # => "<some other long string of octets>"
|
65
|
+
# equivalent to
|
66
|
+
mht.head(0, 42)
|
67
|
+
|
68
|
+
We can also ask for a "consistency proof" between any two subtrees:
|
69
|
+
|
70
|
+
mht.consistency_proof(42, 69) # => ["<hash>", "<hash>", ... ]
|
71
|
+
|
72
|
+
If we want a consistency proof between a subtree and the current head, we
|
73
|
+
can drop the second parameter:
|
74
|
+
|
75
|
+
mht.consistency_proof(42) # => ["<hash>", "<hash>", ... ]
|
76
|
+
|
77
|
+
I'm not going to describe Merkle consistency proofs here; the Internet does
|
78
|
+
a far better job than I ever will. The return value of `#consistency_proof`
|
79
|
+
is simply an array of the hashes that are required by a client to prove that
|
80
|
+
the smaller subtree is, indeed, a subtree of the larger one (and nothing
|
81
|
+
dodgy has gone on behind the scenes). [RFC6962,
|
82
|
+
s2.1.2](http://tools.ietf.org/html/rfc6962#section-2.1.2) has all the gory
|
83
|
+
details of how to calculate it and how to use the result.
|
84
|
+
|
85
|
+
There are also such things as "audit proofs" (again, I'm not going to
|
86
|
+
explain them here), which you get by specifying a single leaf number and a
|
87
|
+
subtree ID:
|
88
|
+
|
89
|
+
mht.audit_proof(13, 42) # => ["<hash>", "<hash>", ... ]
|
90
|
+
|
91
|
+
In this example, the audit proof will return a list of hashes, starting from
|
92
|
+
the leaf node's sibling and working up towards the root node for a hash tree
|
93
|
+
containing 42 elements, that demonstrate that leaf 13 is in the tree and
|
94
|
+
hasn't been removed or altered.
|
95
|
+
|
96
|
+
You can also drop the second argument, in which case you get an audit proof
|
97
|
+
for the tree that represents the entire list as it currently exists:
|
98
|
+
|
99
|
+
mht.audit_proof(13) # => ["<hash>", "<hash>", ... ]
|
100
|
+
|
101
|
+
And that's it! There really isn't much you can do from the outside. All
|
102
|
+
the fun happens inside.
|
103
|
+
|
104
|
+
|
105
|
+
## The Data Access Interface
|
106
|
+
|
107
|
+
Rather than trying to work with an entire dataset in memory,
|
108
|
+
`MerkleHashTree` is capable of working with a dataset far larger than what
|
109
|
+
could fit in memory, by using a data access object to fetch items and cache
|
110
|
+
intermediate results (the hashes of nodes in the tree). To do this, though,
|
111
|
+
a fair number of methods need to be implemented.
|
112
|
+
|
113
|
+
How you implement them is up to you -- you could query a backend database,
|
114
|
+
or just make up data as you felt like it. In the minimal case, you *can*
|
115
|
+
pass in an instance of Array, although I doubt you'll enjoy the performance
|
116
|
+
on any but the smallest possible hash tree.
|
117
|
+
|
118
|
+
The complete interface definition is given in `doc/DAI.md`, for those who
|
119
|
+
wish to implement their own interface. Essentially, you *must* to implement
|
120
|
+
`[](n)`, which returns the `n`th entry in the (zero-indexed) list, as well
|
121
|
+
as `length`, which returns the current size of the list. You can also
|
122
|
+
implement `cache_set(n1, n2, s)` and `cache_get(n1, n2)`, which set and get
|
123
|
+
entries in the cache of node values. If you don't implement these, then
|
124
|
+
`MerkleHashTree` will need to recalculate every hash in the tree repeatedly
|
125
|
+
for most every operation -- which will be *very* slow for anything other
|
126
|
+
than the most trivial result.
|
127
|
+
|
128
|
+
As I said before, you *can* just use Array, if you want to, which could look
|
129
|
+
something like this:
|
130
|
+
|
131
|
+
a = Array.new
|
132
|
+
mht = MerkleHashTree.new(a, Digest::MD5)
|
133
|
+
|
134
|
+
a << 'a'
|
135
|
+
a << 'b'
|
136
|
+
a << 'c'
|
137
|
+
a << 'd'
|
138
|
+
a << 'e'
|
139
|
+
|
140
|
+
mht.head # => "O\xA2\x03\x12\xF6\x0F\xFBtU\x95GY\xE53\x17\x8D"
|
141
|
+
|
142
|
+
|
143
|
+
## Further Info
|
144
|
+
|
145
|
+
In a reversal of standard operating procedure, I heavily document all the
|
146
|
+
methods and interfaces I write. You can get complete API documentation by
|
147
|
+
using `ri` (or a descendent thereof), or via your web-based rdoc browser of
|
148
|
+
choice.
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
|
4
|
+
task :default => :test
|
5
|
+
|
6
|
+
begin
|
7
|
+
Bundler.setup(:default, :development)
|
8
|
+
rescue Bundler::BundlerError => e
|
9
|
+
$stderr.puts e.message
|
10
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
11
|
+
exit e.status_code
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'git-version-bump/rake-tasks'
|
15
|
+
|
16
|
+
Bundler::GemHelper.install_tasks
|
17
|
+
|
18
|
+
require 'rdoc/task'
|
19
|
+
|
20
|
+
Rake::RDocTask.new do |rd|
|
21
|
+
rd.main = "README.md"
|
22
|
+
rd.title = 'lvmsync'
|
23
|
+
rd.rdoc_files.include("README.md", "lib/**/*.rb")
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "Run guard"
|
27
|
+
task :guard do
|
28
|
+
require 'guard'
|
29
|
+
::Guard.start(:clear => true)
|
30
|
+
while ::Guard.running do
|
31
|
+
sleep 0.5
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
require 'rspec/core/rake_task'
|
36
|
+
RSpec::Core::RakeTask.new :test do |t|
|
37
|
+
t.pattern = "spec/**/*_spec.rb"
|
38
|
+
end
|
data/doc/DAI.md
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
The Data Access Interface for this library is a flexible way for the tree to
|
2
|
+
retrieve and cache information it needs. This is important, because the use
|
3
|
+
case for this library is to provide hash trees for datasets *far* larger
|
4
|
+
than what can be reasonably stored in memory by Ruby objects, and
|
5
|
+
potentially in diverse and application-specific stores. Therefore, it is
|
6
|
+
important that the interface between instances of `MerkleHashTree` and the
|
7
|
+
underlying list data is as flexible as possible.
|
8
|
+
|
9
|
+
The interface is designed so that an instance of `Array` *will* work, in the
|
10
|
+
minimal case, although it won't perform particularly well. In order to
|
11
|
+
maximise performance, it is recommended that the optional caching methods
|
12
|
+
also be implemented, with the cache data stored either in memory, or in a
|
13
|
+
fast network-accessable cache such as memcached or Redis.
|
14
|
+
|
15
|
+
|
16
|
+
# Mandatory Methods
|
17
|
+
|
18
|
+
There are two mandatory methods which *must* be implemented by any object
|
19
|
+
which is passed in as the data object to a call to `MerkleHashTree.new`.
|
20
|
+
They might look familiar.
|
21
|
+
|
22
|
+
Both of these methods are called *very* frequently and repeatedly; it is
|
23
|
+
highly recommended that they perform their own caching of results if
|
24
|
+
retrieval from backing store is an expensive operation. Caching is quite
|
25
|
+
easy in this system, because no value ever changes once it has been defined
|
26
|
+
(with the exception of `length`).
|
27
|
+
|
28
|
+
|
29
|
+
## `length`
|
30
|
+
|
31
|
+
This method returns the number of items in the list. This number must be
|
32
|
+
monotonically increasing with each call -- that is, there must never be a
|
33
|
+
case where a call to `#length` on a given data object returns a
|
34
|
+
value less than that returned by a previous call to `#length` on that same
|
35
|
+
object. Failure to observe this property will absolutely and with
|
36
|
+
guaranteed certainty lead to heartbreak.
|
37
|
+
|
38
|
+
|
39
|
+
## `[](n)`
|
40
|
+
|
41
|
+
This method returns the `n`th item in the list, indexed from zero. All
|
42
|
+
values of `[](n)` for `0 <= n < length` must return an object which responds
|
43
|
+
to `to_s` correctly (the value of `to_s` is used as the value passed to the
|
44
|
+
hashing function which calculates the leaf hash value).
|
45
|
+
|
46
|
+
If `n` is greater than or equal to a value previously returned from a call
|
47
|
+
to `length` on the same object, it is permissible to either return `nil`,
|
48
|
+
raise `ArgumentError`, or do whatever you like -- if `MerkleHashTree` ever
|
49
|
+
does that, it's a big fat stinky bug in this library.
|
50
|
+
|
51
|
+
Once a call to this method with a given value of `n` has been made, *every*
|
52
|
+
future call for the same value of `n` MUST return an object whose `to_s`
|
53
|
+
method returns an identical string. Failure to observe this requirement
|
54
|
+
will surely cause demons to fly out of your nose.
|
55
|
+
|
56
|
+
|
57
|
+
# Optional Methods
|
58
|
+
|
59
|
+
There are two optional methods which your DAI object may choose to
|
60
|
+
implement. If you implement one, though, you must implement both. They are
|
61
|
+
used to cache intermediate hashes within the tree nodes, and can
|
62
|
+
significantly improve performance on large trees, because it's a lot quicker
|
63
|
+
to retrieve a value from a database than it is to recalculate a few hundred
|
64
|
+
thousand SHA256 hashes.
|
65
|
+
|
66
|
+
|
67
|
+
## `mht_cache_set(key, value)`
|
68
|
+
|
69
|
+
This method takes a string `key` and a string `value`, and should store that
|
70
|
+
association somewhere convenient for later retrieval. The return value is
|
71
|
+
ignored (although raising an exception is Just Not On).
|
72
|
+
|
73
|
+
For a given `key`, only one `value` will *ever* be passed (for a given DAI
|
74
|
+
object). If this allows you to optimise some part of your cache
|
75
|
+
implementation, mazel tov.
|
76
|
+
|
77
|
+
|
78
|
+
## `mht_cache_get(key)`
|
79
|
+
|
80
|
+
This method takes a string `key` and returns either a string `value` or
|
81
|
+
`nil`. If a string is returned, that string MUST be the value passed to a
|
82
|
+
previous call to `mht_cache_set` for the same `key`.
|
83
|
+
|
84
|
+
Since this is a caching interface, It is entirely permissible to return
|
85
|
+
`nil` to a call to `mht_cache_get` for a given key when a previous call for
|
86
|
+
the same key returned a string `value`. The cache entry may well have
|
87
|
+
expired in the interim. `MerkleHashTree` will *always* handle a call to
|
88
|
+
`mht_cache_get` returning `nil` (by recalculating any and all hashes
|
89
|
+
required to regenerate the value that has not been cached).
|
90
|
+
|
91
|
+
This method MAY be called with a given `key` without a previous call to
|
92
|
+
`mht_cache_set` being made for the same `key`, and your implementation must
|
93
|
+
handle that gracefully (by returning `nil`).
|
94
|
+
|
95
|
+
|
96
|
+
# Item Methods
|
97
|
+
|
98
|
+
The objects returned from calls to `[](n)` must implement a `to_s` method
|
99
|
+
that returns a string. There is no requirement for the value returned by
|
100
|
+
`to_s` to be unique amongst all objects returned from `[](n)`, but I
|
101
|
+
certainly wouldn't recommend them all returning the same value (it would be
|
102
|
+
a very boring-looking hash tree).
|
103
|
+
|
104
|
+
To slightly improve performance, objects can also implement an accessor
|
105
|
+
method pair, `mht_leaf_hash` and `mht_leaf_hash=(s)`. If available,
|
106
|
+
`mht_leaf_hash` will be called to determine the hash value of the object; if
|
107
|
+
this method returns `nil`, then the hash value will be calculated from the
|
108
|
+
string returned by `to_s`, and then cached in the object by calling
|
109
|
+
`mht_leaf_hash=(h)`. It is not recommended that you try to be clever by
|
110
|
+
implementing a hashing scheme yourself in `mht_leaf_hash`; that way lies
|
111
|
+
madness.
|
@@ -0,0 +1,283 @@
|
|
1
|
+
require 'range_extensions'
|
2
|
+
|
3
|
+
# Implement an RFC6962-compliant Merkle Hash Tree.
|
4
|
+
#
|
5
|
+
class MerkleHashTree
|
6
|
+
# Instantiate a new MerkleHashTree.
|
7
|
+
#
|
8
|
+
# Arguments:
|
9
|
+
#
|
10
|
+
# * `data_access` -- An object which implements the Data Access Interface
|
11
|
+
# specified in `doc/DAI.md`. `Array` implements the basic interface,
|
12
|
+
# but for performance you'll want to implement the caching methods
|
13
|
+
# described in `doc/DAI.md`.
|
14
|
+
#
|
15
|
+
# The MerkleHashTree gets all of its data from this object.
|
16
|
+
#
|
17
|
+
# * `hash_class` -- An object which provides a `.digest` method which
|
18
|
+
# behaves identically to `Digest::Base.digest` -- that is, it takes
|
19
|
+
# an arbitrary string and returns another string, with the requirement
|
20
|
+
# that every call with the same input will return the same output.
|
21
|
+
#
|
22
|
+
# Raises:
|
23
|
+
#
|
24
|
+
# * `ArgumentError` -- If either argument does not meet the basic
|
25
|
+
# requirements specified above (that is, the objects don't implement
|
26
|
+
# the defined interface).
|
27
|
+
#
|
28
|
+
def initialize(data_access, hash_class)
|
29
|
+
@data = data_access
|
30
|
+
unless @data.respond_to?(:[])
|
31
|
+
raise ArgumentError,
|
32
|
+
"data_access (#{@data}) does not implement #[]"
|
33
|
+
end
|
34
|
+
unless @data.respond_to?(:length)
|
35
|
+
raise ArgumentError,
|
36
|
+
"data_access (#{@data}) does not implement #length"
|
37
|
+
end
|
38
|
+
|
39
|
+
@digest = hash_class
|
40
|
+
unless @digest.respond_to?(:digest)
|
41
|
+
raise ArgumentError,
|
42
|
+
"hash_class (#{@digest}) does not implement #digest"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Return the hash value of a subtree.
|
47
|
+
#
|
48
|
+
# Arguments:
|
49
|
+
#
|
50
|
+
# * `subtree` -- A range of the list items over which the tree hash will
|
51
|
+
# be calculated. If not specified, it defaults to the entire current
|
52
|
+
# list.
|
53
|
+
#
|
54
|
+
# Raises:
|
55
|
+
#
|
56
|
+
# * `ArgumentError` -- if the range doesn't consist of integers, or if the
|
57
|
+
# range is outside the bounds of the current list size.
|
58
|
+
#
|
59
|
+
def head(subtree = nil)
|
60
|
+
# Super-special case when we're asking for the hash of an entire list
|
61
|
+
# that... just happens to be empty
|
62
|
+
if subtree.nil? and @data.length == 0
|
63
|
+
return digest("")
|
64
|
+
end
|
65
|
+
|
66
|
+
subtree ||= 0..(@data.length-1)
|
67
|
+
|
68
|
+
unless subtree.min.is_a? Integer and subtree.max.is_a? Integer
|
69
|
+
raise ArgumentError,
|
70
|
+
"subtree is not all integers (got #{subtree.inspect})"
|
71
|
+
end
|
72
|
+
|
73
|
+
if subtree.min < 0
|
74
|
+
raise ArgumentError,
|
75
|
+
"subtree cannot go negative (#{subtree.inspect})"
|
76
|
+
end
|
77
|
+
|
78
|
+
if subtree.max >= @data.length
|
79
|
+
raise ArgumentError,
|
80
|
+
"subtree extends beyond list length (subtree is #{subtree.inspect}, list has #{@data.length} items)"
|
81
|
+
end
|
82
|
+
|
83
|
+
if subtree.max < subtree.min
|
84
|
+
raise ArgumentError,
|
85
|
+
"subtree goes backwards (#{subtree.inspect})"
|
86
|
+
end
|
87
|
+
|
88
|
+
if @data.respond_to?(:mht_cache_get) and h = @data.mht_cache_get(subtree.inspect)
|
89
|
+
return h
|
90
|
+
end
|
91
|
+
|
92
|
+
# No caching, or not in the cache... recalculate!
|
93
|
+
h = if subtree.size == 1
|
94
|
+
# We're at a leaf!
|
95
|
+
leaf_hash(subtree.min)
|
96
|
+
else
|
97
|
+
k = power_of_2_smaller_than(subtree.size)
|
98
|
+
|
99
|
+
node_hash(head((0..k-1)+subtree.min), head(subtree.min+k..subtree.max))
|
100
|
+
end
|
101
|
+
|
102
|
+
if @data.respond_to?(:mht_cache_set)
|
103
|
+
@data.mht_cache_set(subtree.inspect, h)
|
104
|
+
end
|
105
|
+
|
106
|
+
h
|
107
|
+
end
|
108
|
+
|
109
|
+
# Generate an "audit proof" for a list item.
|
110
|
+
#
|
111
|
+
# Arguments:
|
112
|
+
#
|
113
|
+
# * `item` -- Specifies the index in the list to retrieve the audit proof
|
114
|
+
# for. Must be a non-negative integer within the bounds of the current
|
115
|
+
# list.
|
116
|
+
#
|
117
|
+
# * `subtree` -- A range which defines the subset of list items within
|
118
|
+
# which to generate the audit proof. The bounds of the range must be
|
119
|
+
# within the bounds of the current list.
|
120
|
+
#
|
121
|
+
# The return value of this method is an array of node hashes which make
|
122
|
+
# up the audit proof. The first element of the array is the immediate
|
123
|
+
# sibling of the item requested; the last is a child of the root.
|
124
|
+
#
|
125
|
+
# Raises:
|
126
|
+
#
|
127
|
+
# * `ArgumentError` -- if any provided argument isn't an integer, or is
|
128
|
+
# negative, or is out of range.
|
129
|
+
#
|
130
|
+
# * `RuntimeError` -- if an attempt is made to request an audit proof on
|
131
|
+
# an empty list.
|
132
|
+
#
|
133
|
+
def audit_proof(item, subtree=nil)
|
134
|
+
if @data.length == 0
|
135
|
+
raise RuntimeError,
|
136
|
+
"Cannot calculate an audit proof on an empty list"
|
137
|
+
end
|
138
|
+
|
139
|
+
subtree ||= (0..@data.length - 1)
|
140
|
+
|
141
|
+
unless subtree.min.is_a? Integer and subtree.max.is_a? Integer
|
142
|
+
raise ArgumentError,
|
143
|
+
"subtree must be an integer range (got #{subtree.inspect})"
|
144
|
+
end
|
145
|
+
|
146
|
+
unless item.is_a? Integer
|
147
|
+
raise ArgumentError,
|
148
|
+
"item must be an integer (got #{item.inspect})"
|
149
|
+
end
|
150
|
+
|
151
|
+
if subtree.min < 0
|
152
|
+
raise ArgumentError,
|
153
|
+
"subtree range must be non-negative (subtree is #{subtree.inspect})"
|
154
|
+
end
|
155
|
+
|
156
|
+
if subtree.max >= @data.length
|
157
|
+
raise ArgumentError,
|
158
|
+
"subtree must not extend beyond the end of the list (subtree is #{subtree.inspect}, list has #{@data.length} items)"
|
159
|
+
end
|
160
|
+
|
161
|
+
if subtree.max < subtree.min
|
162
|
+
raise ArgumentError,
|
163
|
+
"subtree must be min..max (subtree is #{subtree.inspect})"
|
164
|
+
end
|
165
|
+
|
166
|
+
# And finally, after all that, we can start actually *doing* something
|
167
|
+
if subtree.size == 1
|
168
|
+
# Audit proof for a single item is defined as being empty
|
169
|
+
[]
|
170
|
+
else
|
171
|
+
k = power_of_2_smaller_than(subtree.size)
|
172
|
+
|
173
|
+
if item < k
|
174
|
+
audit_proof(item, (0..k-1)+subtree.min) + [head(subtree.min+k..subtree.max)]
|
175
|
+
else
|
176
|
+
audit_proof(subtree.min+item-k, subtree.min+k..subtree.max) +
|
177
|
+
[head((0..k-1)+subtree.min)]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Generate a consistency proof.
|
183
|
+
#
|
184
|
+
# Arguments:
|
185
|
+
#
|
186
|
+
# * `m` -- The smaller list size for which you wish to generate
|
187
|
+
# the consistency proof.
|
188
|
+
#
|
189
|
+
# * `n` -- The larger list size for which you wish to generate
|
190
|
+
# the consistency proof.
|
191
|
+
#
|
192
|
+
# Raises:
|
193
|
+
#
|
194
|
+
# * `ArgumentError` -- If the arguments aren't integers, or if they're
|
195
|
+
# negative, or if `n < m`.
|
196
|
+
#
|
197
|
+
def consistency_proof(m, n)
|
198
|
+
unless m.is_a? Integer
|
199
|
+
raise ArgumentError,
|
200
|
+
"m is not an integer (got #{m.inspect})"
|
201
|
+
end
|
202
|
+
|
203
|
+
unless n.is_a? Integer
|
204
|
+
raise ArgumentError,
|
205
|
+
"n is not an integer (got #{n.inspect})"
|
206
|
+
end
|
207
|
+
|
208
|
+
if m < 0
|
209
|
+
raise ArgumentError,
|
210
|
+
"m cannot be negative (m is #{m})"
|
211
|
+
end
|
212
|
+
|
213
|
+
if n > @data.length
|
214
|
+
raise ArgumentError,
|
215
|
+
"n cannot be larger than the list length (n is #{n}, list has #{@data.length} elements)"
|
216
|
+
end
|
217
|
+
|
218
|
+
if n < m
|
219
|
+
raise ArgumentError,
|
220
|
+
"n cannot be less than m (m is #{m}, n is #{n})"
|
221
|
+
end
|
222
|
+
|
223
|
+
# This is taken from in-practice behaviour of the Google pilot/aviator
|
224
|
+
# CT servers... when first=0, you always get an empty proof.
|
225
|
+
return [] if m == 0
|
226
|
+
|
227
|
+
# And now... on to the real show!
|
228
|
+
subproof(m, 0..n-1, true)
|
229
|
+
end
|
230
|
+
|
231
|
+
private
|
232
|
+
# :nodoc:
|
233
|
+
|
234
|
+
def subproof(m, n, b)
|
235
|
+
if n.max == m-1
|
236
|
+
if b
|
237
|
+
[]
|
238
|
+
else
|
239
|
+
[head(n)]
|
240
|
+
end
|
241
|
+
elsif n.min == n.max
|
242
|
+
[head(n)]
|
243
|
+
else
|
244
|
+
k = power_of_2_smaller_than(n.size)
|
245
|
+
|
246
|
+
if m <= k+n.min
|
247
|
+
subproof(m, (0..k-1)+n.min, b) + [head((n.min+k)..n.max)]
|
248
|
+
else
|
249
|
+
subproof(m, ((n.min+k)..n.max), false) + [head((0..k-1)+n.min)]
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def digest(s)
|
255
|
+
@digest.digest(s)
|
256
|
+
end
|
257
|
+
|
258
|
+
def leaf_hash(n)
|
259
|
+
if @data[n].respond_to?(:mht_leaf_hash) and h = @data[n].mht_leaf_hash
|
260
|
+
return h
|
261
|
+
end
|
262
|
+
|
263
|
+
h = digest("\0" + @data[n].to_s)
|
264
|
+
|
265
|
+
if @data[n].respond_to?(:mht_leaf_hash=)
|
266
|
+
@data[n].mht_leaf_hash = h
|
267
|
+
end
|
268
|
+
|
269
|
+
h
|
270
|
+
end
|
271
|
+
|
272
|
+
def node_hash(h1, h2)
|
273
|
+
digest("\x01" + h1 + h2)
|
274
|
+
end
|
275
|
+
|
276
|
+
# This is almost certainly horribly inefficient, but my math skills have
|
277
|
+
# atrophied embarrassingly
|
278
|
+
def power_of_2_smaller_than(n)
|
279
|
+
raise ArgumentError, "Too small, Jim" if n < 2
|
280
|
+
s = (n-1).to_s(2).length-1
|
281
|
+
2**s
|
282
|
+
end
|
283
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'git-version-bump'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "merkle-hash-tree"
|
5
|
+
|
6
|
+
s.version = GVB.version
|
7
|
+
s.date = GVB.date
|
8
|
+
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
|
11
|
+
s.homepage = "http://theshed.hezmatt.org/merkle-hash-tree"
|
12
|
+
s.summary = "An RFC6962-compliant implementation of Merkle Hash Trees"
|
13
|
+
s.authors = ["Matt Palmer"]
|
14
|
+
|
15
|
+
s.extra_rdoc_files = ["README.md"]
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
|
18
|
+
s.add_runtime_dependency "git-version-bump"
|
19
|
+
|
20
|
+
s.add_development_dependency 'bundler'
|
21
|
+
s.add_development_dependency 'guard-spork'
|
22
|
+
s.add_development_dependency 'guard-rspec'
|
23
|
+
s.add_development_dependency 'plymouth'
|
24
|
+
s.add_development_dependency 'pry-debugger'
|
25
|
+
s.add_development_dependency 'rake'
|
26
|
+
# Needed for guard
|
27
|
+
s.add_development_dependency 'rb-inotify', '~> 0.9'
|
28
|
+
s.add_development_dependency 'rdoc'
|
29
|
+
s.add_development_dependency 'rspec', '~> 2.11'
|
30
|
+
s.add_development_dependency 'rspec-mocks'
|
31
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require_relative './spec_helper'
|
2
|
+
require_relative '../lib/merkle-hash-tree'
|
3
|
+
|
4
|
+
describe "MerkleHashTree#audit_proof" do
|
5
|
+
let(:mht) { MerkleHashTree.new(data, IdentityDigest) }
|
6
|
+
|
7
|
+
context "with a single element" do
|
8
|
+
let(:data) { %w{a} }
|
9
|
+
|
10
|
+
it "returns empty" do
|
11
|
+
expect(mht.audit_proof(0)).to eq([])
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context "with a one-level tree" do
|
16
|
+
let(:data) { %w{a b} }
|
17
|
+
|
18
|
+
it "works for 'b'" do
|
19
|
+
expect(mht.audit_proof(1)).to eq(['a'])
|
20
|
+
end
|
21
|
+
|
22
|
+
it "works for 'a'" do
|
23
|
+
expect(mht.audit_proof(0)).to eq(['b'])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "with a two-level tree" do
|
28
|
+
let(:data) { %w{a b c d} }
|
29
|
+
|
30
|
+
it "works for 'a'" do
|
31
|
+
expect(mht.audit_proof(0)).to eq(['b', 'cd'])
|
32
|
+
end
|
33
|
+
|
34
|
+
it "works for 'b'" do
|
35
|
+
expect(mht.audit_proof(1)).to eq(['a', 'cd'])
|
36
|
+
end
|
37
|
+
|
38
|
+
it "works for 'c'" do
|
39
|
+
expect(mht.audit_proof(2)).to eq(['d', 'ab'])
|
40
|
+
end
|
41
|
+
|
42
|
+
it "works for 'd'" do
|
43
|
+
expect(mht.audit_proof(3)).to eq(['c', 'ab'])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "with an unbalanced three-level tree" do
|
48
|
+
let(:data) { %w{a b c d e} }
|
49
|
+
|
50
|
+
it "works for 'a'" do
|
51
|
+
expect(mht.audit_proof(0)).to eq(['b', 'cd', 'e'])
|
52
|
+
end
|
53
|
+
|
54
|
+
it "works for 'b'" do
|
55
|
+
expect(mht.audit_proof(1)).to eq(['a', 'cd', 'e'])
|
56
|
+
end
|
57
|
+
|
58
|
+
it "works for 'c'" do
|
59
|
+
expect(mht.audit_proof(2)).to eq(['d', 'ab', 'e'])
|
60
|
+
end
|
61
|
+
|
62
|
+
it "works for 'd'" do
|
63
|
+
expect(mht.audit_proof(3)).to eq(['c', 'ab', 'e'])
|
64
|
+
end
|
65
|
+
|
66
|
+
it "works for 'e'" do
|
67
|
+
# It makes sense if you drink *juuuuust* enough tequila
|
68
|
+
expect(mht.audit_proof(4)).to eq(['abcd'])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "with a seven node tree" do
|
73
|
+
# Taken from RFC6962, s2.1.3
|
74
|
+
let(:data) { %w{a b c d e f g} }
|
75
|
+
|
76
|
+
it "works for 'a'" do
|
77
|
+
expect(mht.audit_proof(0)).to eq(%w{b cd efg})
|
78
|
+
end
|
79
|
+
|
80
|
+
it "works for 'd'" do
|
81
|
+
expect(mht.audit_proof(3)).to eq(%w{c ab efg})
|
82
|
+
end
|
83
|
+
|
84
|
+
it "works for 'e'" do
|
85
|
+
expect(mht.audit_proof(4)).to eq(%w{f g abcd})
|
86
|
+
end
|
87
|
+
|
88
|
+
it "works for 'g'" do
|
89
|
+
expect(mht.audit_proof(6)).to eq(%w{ef abcd})
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context "with a large tree" do
|
94
|
+
let(:data) { %w{a b c d e f g h} }
|
95
|
+
|
96
|
+
it "works for 'd'" do
|
97
|
+
expect(mht.audit_proof(3)).to eq(%w{c ab efgh})
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context "with a hueg tree" do
|
102
|
+
let(:data) { %w{a b c d e f g h i j k l m n o p q r s t u v w x y z} }
|
103
|
+
|
104
|
+
it "works for 'f'" do
|
105
|
+
expect(mht.audit_proof(5)).to eq(%w{e gh abcd ijklmnop qrstuvwxyz})
|
106
|
+
end
|
107
|
+
|
108
|
+
it "works for 'q'" do
|
109
|
+
expect(mht.audit_proof(15)).to eq(%w{o mn ijkl abcdefgh qrstuvwxyz})
|
110
|
+
end
|
111
|
+
|
112
|
+
it "works for 'z'" do
|
113
|
+
expect(mht.audit_proof(25)).to eq(%w{y qrstuvwx abcdefghijklmnop})
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative './spec_helper'
|
2
|
+
require_relative '../lib/merkle-hash-tree'
|
3
|
+
|
4
|
+
describe "MerkleHashTree#consistency_proof" do
|
5
|
+
let(:mht) { MerkleHashTree.new(data, IdentityDigest) }
|
6
|
+
|
7
|
+
context "with a seven-node tree" do
|
8
|
+
# Taken from RFC6962, s2.1.3
|
9
|
+
let(:data) { %w{a b c d e f g} }
|
10
|
+
|
11
|
+
it "is empty for 0->7" do
|
12
|
+
expect(mht.consistency_proof(0, 7)).to eq([])
|
13
|
+
end
|
14
|
+
|
15
|
+
it "is empty for 7->7" do
|
16
|
+
expect(mht.consistency_proof(7, 7)).to eq([])
|
17
|
+
end
|
18
|
+
|
19
|
+
it "works for 3->7" do
|
20
|
+
expect(mht.consistency_proof(3, 7)).to eq(%w{c d ab efg})
|
21
|
+
end
|
22
|
+
|
23
|
+
it "works for 4->7" do
|
24
|
+
expect(mht.consistency_proof(4, 7)).to eq(%w{efg})
|
25
|
+
end
|
26
|
+
|
27
|
+
it "works for 6->7" do
|
28
|
+
expect(mht.consistency_proof(6, 7)).to eq(%w{ef g abcd})
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "with a full three-level tree" do
|
33
|
+
# Taken from www.certificate-transparency.org's "How Log Proofs Work"
|
34
|
+
# page
|
35
|
+
let(:data) { %w{a b c d e f g h} }
|
36
|
+
|
37
|
+
it "works for 6->8" do
|
38
|
+
expect(mht.consistency_proof(6, 8)).to eq(%w{ef gh abcd})
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative './spec_helper'
|
2
|
+
require_relative '../lib/merkle-hash-tree'
|
3
|
+
|
4
|
+
describe "DAI caching" do
|
5
|
+
let(:dai) do
|
6
|
+
%w{a b c d e f g}
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:mht) { MerkleHashTree.new(dai, IdentityDigest) }
|
10
|
+
|
11
|
+
it "tries to get cache values, but is OK with nil" do
|
12
|
+
dai.
|
13
|
+
should_receive(:mht_cache_get).
|
14
|
+
with(any_args).
|
15
|
+
at_least(:once).
|
16
|
+
and_return(nil)
|
17
|
+
|
18
|
+
mht.head
|
19
|
+
end
|
20
|
+
|
21
|
+
it "respects cache values" do
|
22
|
+
dai.
|
23
|
+
should_receive(:mht_cache_get).
|
24
|
+
with('1..5').
|
25
|
+
and_return('xyzzy')
|
26
|
+
|
27
|
+
expect(mht.head(1..5)).to eq('xyzzy')
|
28
|
+
end
|
29
|
+
|
30
|
+
it "tries to cache calculated values" do
|
31
|
+
dai.
|
32
|
+
should_receive(:mht_cache_get).
|
33
|
+
with(any_args).
|
34
|
+
at_least(:once).
|
35
|
+
and_return(nil)
|
36
|
+
dai.
|
37
|
+
should_receive(:mht_cache_set).
|
38
|
+
with('2..2', 'c')
|
39
|
+
|
40
|
+
mht.head(2..2)
|
41
|
+
end
|
42
|
+
end
|
data/spec/head_n_spec.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative './spec_helper'
|
2
|
+
require_relative '../lib/merkle-hash-tree'
|
3
|
+
require 'digest/sha2'
|
4
|
+
|
5
|
+
describe "MerkleHashTree#head" do
|
6
|
+
let(:data) do
|
7
|
+
["", "\0", "\x10", "\x20\x21", "\x30\x31", "\x40\x41\x42\x43",
|
8
|
+
"\x50\x51\x52\x53\x54\x55\x56\x57",
|
9
|
+
"\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f"
|
10
|
+
]
|
11
|
+
end
|
12
|
+
let(:mht) { MerkleHashTree.new(data, Digest::SHA256) }
|
13
|
+
|
14
|
+
hashes = ["6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d",
|
15
|
+
"fac54203e7cc696cf0dfcb42c92a1d9dbaf70ad9e621f4bd8d98662f00e3c125",
|
16
|
+
"aeb6bcfe274b70a14fb067a5e5578264db0fa9b51af5e0ba159158f329e06e77",
|
17
|
+
"d37ee418976dd95753c1c73862b9398fa2a2cf9b4ff0fdfe8b30cd95209614b7",
|
18
|
+
"4e3bbb1f7b478dcfe71fb631631519a3bca12c9aefca1612bfce4c13a86264d4",
|
19
|
+
"76e67dadbcdf1e10e1b74ddc608abd2f98dfb16fbce75277b5232a127f2087ef",
|
20
|
+
"ddb89be403809e325750d3d263cd78929c2942b7942a34b77e122c9594a74c8c",
|
21
|
+
"5dc9da79a70659a9ad559cb701ded9a2ab9d823aad2f4960cfe370eff4604328"
|
22
|
+
]
|
23
|
+
|
24
|
+
|
25
|
+
def hexstring(s)
|
26
|
+
s.scan(/./m).map { |c| sprintf("%02x", c.ord) }.join
|
27
|
+
end
|
28
|
+
|
29
|
+
8.times do |i|
|
30
|
+
context "head(#{i})" do
|
31
|
+
it "gives a specific hash" do
|
32
|
+
expect(hexstring(mht.head(0..i))).to eq(hashes[i])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/spec/head_spec.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require_relative './spec_helper'
|
2
|
+
require_relative '../lib/merkle-hash-tree'
|
3
|
+
require 'digest/sha2'
|
4
|
+
|
5
|
+
# These tests are taken directly from the CT reference implementation's
|
6
|
+
# merkle tree test suite; they're used to ensure we conform to that
|
7
|
+
# specification correctly
|
8
|
+
describe "MerkleHashTree#head" do
|
9
|
+
let(:mht) { MerkleHashTree.new(data, Digest::SHA256) }
|
10
|
+
|
11
|
+
hashes = [Digest::SHA256.hexdigest(""),
|
12
|
+
"6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d",
|
13
|
+
"fac54203e7cc696cf0dfcb42c92a1d9dbaf70ad9e621f4bd8d98662f00e3c125",
|
14
|
+
"aeb6bcfe274b70a14fb067a5e5578264db0fa9b51af5e0ba159158f329e06e77",
|
15
|
+
"d37ee418976dd95753c1c73862b9398fa2a2cf9b4ff0fdfe8b30cd95209614b7",
|
16
|
+
"4e3bbb1f7b478dcfe71fb631631519a3bca12c9aefca1612bfce4c13a86264d4",
|
17
|
+
"76e67dadbcdf1e10e1b74ddc608abd2f98dfb16fbce75277b5232a127f2087ef",
|
18
|
+
"ddb89be403809e325750d3d263cd78929c2942b7942a34b77e122c9594a74c8c",
|
19
|
+
"5dc9da79a70659a9ad559cb701ded9a2ab9d823aad2f4960cfe370eff4604328"
|
20
|
+
]
|
21
|
+
|
22
|
+
leaves = ["",
|
23
|
+
"\0",
|
24
|
+
"\x10",
|
25
|
+
"\x20\x21",
|
26
|
+
"\x30\x31",
|
27
|
+
"\x40\x41\x42\x43",
|
28
|
+
"\x50\x51\x52\x53\x54\x55\x56\x57",
|
29
|
+
"\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f"
|
30
|
+
]
|
31
|
+
|
32
|
+
def string_from_hex(s)
|
33
|
+
s.scan(/../).map { |c| c.to_i(16).chr }.join
|
34
|
+
end
|
35
|
+
|
36
|
+
9.times do |i|
|
37
|
+
context "with #{i} items" do
|
38
|
+
let(:data) { leaves.take(i) }
|
39
|
+
|
40
|
+
it "gives a specific hash" do
|
41
|
+
expect(mht.head).to eq(string_from_hex(hashes[i]))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative './spec_helper'
|
2
|
+
require_relative '../lib/merkle-hash-tree'
|
3
|
+
require 'digest/sha1'
|
4
|
+
|
5
|
+
describe "MerkleHashTree#power_of_2_smaller_than" do
|
6
|
+
tests = {
|
7
|
+
2 => 1,
|
8
|
+
3 => 2,
|
9
|
+
4 => 2,
|
10
|
+
5 => 4,
|
11
|
+
6 => 4,
|
12
|
+
7 => 4,
|
13
|
+
8 => 4,
|
14
|
+
9 => 8,
|
15
|
+
10 => 8,
|
16
|
+
15 => 8,
|
17
|
+
16 => 8,
|
18
|
+
17 => 16,
|
19
|
+
18 => 16,
|
20
|
+
31 => 16,
|
21
|
+
32 => 16,
|
22
|
+
33 => 32
|
23
|
+
}
|
24
|
+
|
25
|
+
let(:mht) { MerkleHashTree.new([], Digest::SHA1) }
|
26
|
+
|
27
|
+
it "bombs out for n=1" do
|
28
|
+
expect { mht.send(:power_of_2_smaller_than, 1) }.
|
29
|
+
to raise_error(ArgumentError, /Too small, Jim/)
|
30
|
+
end
|
31
|
+
|
32
|
+
tests.each_pair do |k, v|
|
33
|
+
it "(#{k}) => #{v}" do
|
34
|
+
expect(mht.send(:power_of_2_smaller_than, k)).to eq(v)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spork'
|
2
|
+
|
3
|
+
Spork.prefork do
|
4
|
+
require 'bundler'
|
5
|
+
Bundler.setup(:default, :test)
|
6
|
+
require 'rspec/core'
|
7
|
+
|
8
|
+
require 'rspec/mocks'
|
9
|
+
|
10
|
+
require 'pry'
|
11
|
+
# require 'plymouth'
|
12
|
+
|
13
|
+
RSpec.configure do |config|
|
14
|
+
config.fail_fast = true
|
15
|
+
# config.full_backtrace = true
|
16
|
+
|
17
|
+
config.expect_with :rspec do |c|
|
18
|
+
c.syntax = :expect
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Our super-special digest class to make it easier to understand WTF is
|
23
|
+
# going on
|
24
|
+
class IdentityDigest
|
25
|
+
def self.digest(s)
|
26
|
+
# Strip off the first character, it'll just be a \0 or \x1 anyway
|
27
|
+
s[1..-1]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
Spork.each_run do
|
33
|
+
# Nothing to do here, specs will load the files they need
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,243 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: merkle-hash-tree
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Matt Palmer
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-07-10 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: git-version-bump
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: bundler
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: guard-spork
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: guard-rspec
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: plymouth
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: pry-debugger
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: rake
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: rb-inotify
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ~>
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0.9'
|
134
|
+
type: :development
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ~>
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0.9'
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
name: rdoc
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ! '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
type: :development
|
151
|
+
prerelease: false
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ! '>='
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
- !ruby/object:Gem::Dependency
|
159
|
+
name: rspec
|
160
|
+
requirement: !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ~>
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '2.11'
|
166
|
+
type: :development
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
none: false
|
170
|
+
requirements:
|
171
|
+
- - ~>
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '2.11'
|
174
|
+
- !ruby/object:Gem::Dependency
|
175
|
+
name: rspec-mocks
|
176
|
+
requirement: !ruby/object:Gem::Requirement
|
177
|
+
none: false
|
178
|
+
requirements:
|
179
|
+
- - ! '>='
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
type: :development
|
183
|
+
prerelease: false
|
184
|
+
version_requirements: !ruby/object:Gem::Requirement
|
185
|
+
none: false
|
186
|
+
requirements:
|
187
|
+
- - ! '>='
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: '0'
|
190
|
+
description:
|
191
|
+
email:
|
192
|
+
executables: []
|
193
|
+
extensions: []
|
194
|
+
extra_rdoc_files:
|
195
|
+
- README.md
|
196
|
+
files:
|
197
|
+
- .gitignore
|
198
|
+
- Gemfile
|
199
|
+
- Guardfile
|
200
|
+
- README.md
|
201
|
+
- Rakefile
|
202
|
+
- doc/DAI.md
|
203
|
+
- lib/merkle-hash-tree.rb
|
204
|
+
- lib/range_extensions.rb
|
205
|
+
- merkle-hash-tree.gemspec
|
206
|
+
- spec/audit_proof_spec.rb
|
207
|
+
- spec/consistency_proof_spec.rb
|
208
|
+
- spec/dai_caching_spec.rb
|
209
|
+
- spec/head_n_spec.rb
|
210
|
+
- spec/head_spec.rb
|
211
|
+
- spec/power_of_2_smaller_than_spec.rb
|
212
|
+
- spec/spec_helper.rb
|
213
|
+
homepage: http://theshed.hezmatt.org/merkle-hash-tree
|
214
|
+
licenses: []
|
215
|
+
post_install_message:
|
216
|
+
rdoc_options: []
|
217
|
+
require_paths:
|
218
|
+
- lib
|
219
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
220
|
+
none: false
|
221
|
+
requirements:
|
222
|
+
- - ! '>='
|
223
|
+
- !ruby/object:Gem::Version
|
224
|
+
version: '0'
|
225
|
+
segments:
|
226
|
+
- 0
|
227
|
+
hash: 4544178081523726354
|
228
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
229
|
+
none: false
|
230
|
+
requirements:
|
231
|
+
- - ! '>='
|
232
|
+
- !ruby/object:Gem::Version
|
233
|
+
version: '0'
|
234
|
+
segments:
|
235
|
+
- 0
|
236
|
+
hash: 4544178081523726354
|
237
|
+
requirements: []
|
238
|
+
rubyforge_project:
|
239
|
+
rubygems_version: 1.8.23
|
240
|
+
signing_key:
|
241
|
+
specification_version: 3
|
242
|
+
summary: An RFC6962-compliant implementation of Merkle Hash Trees
|
243
|
+
test_files: []
|