lapidar 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +21 -4
- data/bin/run +43 -9
- data/docs/visualization.png +0 -0
- data/lapidar.gemspec +3 -1
- data/lib/lapidar.rb +9 -70
- data/lib/lapidar/block.rb +1 -1
- data/lib/lapidar/chain.rb +31 -11
- data/lib/lapidar/miner.rb +8 -1
- data/lib/lapidar/persistence.rb +18 -0
- data/lib/lapidar/runner.rb +86 -0
- data/lib/lapidar/version.rb +1 -1
- metadata +35 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7550a6712aa1a4ec73b55dd89292c037e9d7c518
|
4
|
+
data.tar.gz: 157f2757955b82097def09ccd07e8d34e6bd49e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2d7af0295583b6e07a2a329c9a93f478c1ec61f33d140262768dd2f384fd7cf341e5bac0a0efd10255399f60eab826baf527c78dceef95a7d5b42757013fa910
|
7
|
+
data.tar.gz: a1b80a2db11d67d29c80a03113040ab8309e679961e71476282bdc4c64315865fb7dcb2bdd45da0a096fb127c89e5df1bc087e062b72ce4fe3f643a162ea7d3b
|
data/CHANGELOG.md
CHANGED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## 0.2.0
|
4
|
+
|
5
|
+
* Better threading
|
6
|
+
* Blocks now use Unix timestamp
|
7
|
+
* Blocks with equal hashes are only added to the chain once
|
8
|
+
* Chains can be persisted and loaded.
|
9
|
+
* Data can be fed to new blocks.
|
10
|
+
|
11
|
+
## 0.1.0
|
12
|
+
|
13
|
+
* Initial release
|
data/README.md
CHANGED
@@ -21,17 +21,34 @@ Or install it yourself as:
|
|
21
21
|
|
22
22
|
## Usage
|
23
23
|
|
24
|
-
|
24
|
+
Have a look at `bin/run`. You'll see that 5 nodes spin up and connect to each other
|
25
|
+
via [buschtelefon](https://github.com/renuo/lapidar) and contest each other
|
26
|
+
in a race which looks like this:
|
27
|
+
|
28
|
+
![](docs/visualization.png)
|
29
|
+
|
30
|
+
To get a colorful output like this you need to install the gem [*paint*](https://github.com/janlelis/paint).
|
31
|
+
|
32
|
+
### Persistence
|
33
|
+
|
34
|
+
The chain will we loaded from `~/.lapidar/<port>.json` depending on the port
|
35
|
+
on which you start the runner. And it will be saved there after you stop the runner.
|
25
36
|
|
26
37
|
## Development
|
27
38
|
|
28
|
-
After checking out the repo, run `bin/setup` to install dependencies.
|
39
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
40
|
+
Then, run `rake spec` to run the tests. You can also run `bin/console`
|
41
|
+
for an interactive prompt that will allow you to experiment.
|
29
42
|
|
30
|
-
To install this gem onto your local machine, run `bundle exec rake install`.
|
43
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
44
|
+
To release a new version, update the version number in `version.rb`, and then
|
45
|
+
run `bundle exec rake release`, which will create a git tag for the version,
|
46
|
+
push git commits and tags, and push the `.gem` file
|
47
|
+
to [rubygems.org](https://rubygems.org).
|
31
48
|
|
32
49
|
## Contributing
|
33
50
|
|
34
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
51
|
+
Bug reports and pull requests are welcome on GitHub at <https://github.com/renuo/lapidar>.
|
35
52
|
|
36
53
|
## License
|
37
54
|
|
data/bin/run
CHANGED
@@ -1,17 +1,51 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
Thread.abort_on_exception = true
|
2
3
|
|
4
|
+
require "paint"
|
3
5
|
require "bundler"
|
4
6
|
Bundler.setup(:default)
|
5
7
|
|
6
|
-
require_relative
|
8
|
+
require_relative "../lib/lapidar"
|
7
9
|
|
8
|
-
puts
|
10
|
+
puts "Starting experiment…"
|
9
11
|
|
10
|
-
[
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
]
|
12
|
+
runners = [
|
13
|
+
Lapidar.runner(port: 9999, neighbors: [
|
14
|
+
{ host: "localhost", port: 9995 },
|
15
|
+
{ host: "localhost", port: 9996 },
|
16
|
+
{ host: "localhost", port: 9997 },
|
17
|
+
{ host: "localhost", port: 9998 },
|
18
|
+
]),
|
19
|
+
Lapidar.runner(port: 9998, neighbors: [{ host: "localhost", port: 9996 }]),
|
20
|
+
Lapidar.runner(port: 9997, neighbors: [{ host: "localhost", port: 9997 }]),
|
21
|
+
Lapidar.runner(port: 9996, neighbors: [{ host: "localhost", port: 9998 }]),
|
22
|
+
Lapidar.runner(port: 9995, neighbors: [{ host: "localhost", port: 9999 }])
|
23
|
+
]
|
17
24
|
|
25
|
+
threads = runners.map do |runner|
|
26
|
+
[
|
27
|
+
Thread.new { runner.start },
|
28
|
+
Thread.new { loop { runner.punch_queue << rand.to_s } }
|
29
|
+
]
|
30
|
+
end.flatten
|
31
|
+
|
32
|
+
logger_thread = Thread.new do
|
33
|
+
sleep(1)
|
34
|
+
|
35
|
+
loop do
|
36
|
+
system("clear")
|
37
|
+
puts(runners.map do |runner|
|
38
|
+
"Runner on port #{runner.network_endpoint.port}:\n#{runner.chain.to_colorful_string(5)}"
|
39
|
+
end.join("\n"))
|
40
|
+
sleep(1)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
trap "SIGINT" do
|
45
|
+
puts "Shutting down…"
|
46
|
+
runners.each(&:stop)
|
47
|
+
logger_thread.exit
|
48
|
+
end
|
49
|
+
|
50
|
+
threads.each(&:join)
|
51
|
+
logger_thread.join
|
Binary file
|
data/lapidar.gemspec
CHANGED
@@ -30,9 +30,11 @@ and evaluates block order and correctness. Build any distributed business logic
|
|
30
30
|
|
31
31
|
spec.required_ruby_version = ">= 2.3.0"
|
32
32
|
|
33
|
-
spec.add_dependency "buschtelefon", "~> 0.
|
33
|
+
spec.add_dependency "buschtelefon", "~> 0.3"
|
34
|
+
spec.add_dependency "oj", ">= 1.0"
|
34
35
|
spec.add_development_dependency "bundler", ">= 1.17"
|
35
36
|
spec.add_development_dependency "factory_bot", "~> 5.0"
|
37
|
+
spec.add_development_dependency "paint", "~> 2.0"
|
36
38
|
spec.add_development_dependency "rake", "~> 10.0"
|
37
39
|
spec.add_development_dependency "rspec", "~> 3.8"
|
38
40
|
spec.add_development_dependency "simplecov", "~> 0.17"
|
data/lib/lapidar.rb
CHANGED
@@ -1,81 +1,20 @@
|
|
1
|
+
require "json"
|
2
|
+
require "buschtelefon"
|
3
|
+
|
1
4
|
require_relative "lapidar/assessment"
|
2
5
|
require_relative "lapidar/block"
|
3
6
|
require_relative "lapidar/chain"
|
4
7
|
require_relative "lapidar/miner"
|
8
|
+
require_relative "lapidar/persistence"
|
9
|
+
require_relative "lapidar/runner"
|
5
10
|
require_relative "lapidar/version"
|
6
11
|
|
7
|
-
require "json"
|
8
|
-
require "buschtelefon"
|
9
|
-
|
10
12
|
module Lapidar
|
11
|
-
def self.
|
12
|
-
|
13
|
-
miner = Miner.new
|
14
|
-
incoming_blocks = Queue.new
|
15
|
-
|
16
|
-
me = Buschtelefon::NetTattler.new(port: port)
|
13
|
+
def self.runner(port:, neighbors:)
|
14
|
+
network_endpoint = Buschtelefon::NetTattler.new(port: port)
|
17
15
|
neighbors.map! { |neighbor_location| Buschtelefon::RemoteTattler.new(neighbor_location) }
|
18
|
-
neighbors.each { |neighbor|
|
19
|
-
|
20
|
-
Thread.abort_on_exception = true
|
21
|
-
|
22
|
-
consumer = Thread.new {
|
23
|
-
until_shutdown do
|
24
|
-
begin
|
25
|
-
chain.add(incoming_blocks.pop)
|
26
|
-
print "+"
|
27
|
-
rescue
|
28
|
-
puts "Consumer error"
|
29
|
-
end
|
30
|
-
end
|
31
|
-
}
|
32
|
-
|
33
|
-
network_producer = Thread.new {
|
34
|
-
until_shutdown do
|
35
|
-
me.listen do |message|
|
36
|
-
begin
|
37
|
-
incoming_json = JSON.parse(message, symbolize_names: true)
|
38
|
-
|
39
|
-
incoming_blocks << Block.new(
|
40
|
-
number: incoming_json[:number].to_i,
|
41
|
-
hash: incoming_json[:hash].to_s,
|
42
|
-
nonce: incoming_json[:none].to_i,
|
43
|
-
data: incoming_json[:data].to_s,
|
44
|
-
created_at: Time.parse(incoming_json[:created_at])
|
45
|
-
)
|
46
|
-
rescue JSON::ParserError, ArgumentError => e
|
47
|
-
puts "Incoming block isn't valid: #{e.message}"
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
}
|
52
|
-
|
53
|
-
local_producer = Thread.new {
|
54
|
-
until_shutdown do
|
55
|
-
new_block = miner.mine(chain.blocks.last)
|
56
|
-
|
57
|
-
me.feed(Buschtelefon::Gossip.new(new_block.to_h.to_json))
|
58
|
-
incoming_blocks << new_block
|
59
|
-
|
60
|
-
print "⚒ "
|
61
|
-
end
|
62
|
-
}
|
63
|
-
|
64
|
-
local_producer.join
|
65
|
-
network_producer.join
|
66
|
-
consumer.join
|
67
|
-
|
68
|
-
puts "\nShutting down…"
|
69
|
-
end
|
70
|
-
|
71
|
-
def self.until_shutdown
|
72
|
-
trap "SIGINT" do
|
73
|
-
puts "\nshutting down"
|
74
|
-
exit
|
75
|
-
end
|
16
|
+
neighbors.each { |neighbor| network_endpoint.connect(neighbor) }
|
76
17
|
|
77
|
-
|
78
|
-
yield
|
79
|
-
end
|
18
|
+
Runner.new(network_endpoint)
|
80
19
|
end
|
81
20
|
end
|
data/lib/lapidar/block.rb
CHANGED
@@ -2,7 +2,7 @@ module Lapidar
|
|
2
2
|
class Block
|
3
3
|
attr_reader :number, :hash, :data, :nonce, :created_at
|
4
4
|
|
5
|
-
def initialize(number:, hash:, nonce:, data: nil, created_at: Time.now)
|
5
|
+
def initialize(number:, hash:, nonce:, data: nil, created_at: Time.now.to_f)
|
6
6
|
@number = number
|
7
7
|
@hash = hash
|
8
8
|
@nonce = nonce
|
data/lib/lapidar/chain.rb
CHANGED
@@ -1,17 +1,19 @@
|
|
1
1
|
module Lapidar
|
2
2
|
class Chain
|
3
|
+
attr_reader :block_stacks
|
4
|
+
|
3
5
|
def initialize
|
4
|
-
@
|
6
|
+
@block_stacks = []
|
5
7
|
end
|
6
8
|
|
7
9
|
# TODO: Queue up future blocks for later use
|
8
10
|
# TODO: Check for duplicates and dont add them to the chains
|
9
11
|
def add(block)
|
10
|
-
raise "future block?" if block.number > @
|
12
|
+
raise "future block?" if block.number > @block_stacks.count
|
11
13
|
raise "invalid block" unless valid?(block)
|
12
14
|
|
13
|
-
@
|
14
|
-
@
|
15
|
+
@block_stacks[block.number] ||= []
|
16
|
+
@block_stacks[block.number].push(block) unless @block_stacks[block.number].map(&:hash).include?(block.hash)
|
15
17
|
|
16
18
|
# Rebalance if second to last block has more than one candidate
|
17
19
|
rebalance if !contested?(block.number) && contested?(block.number - 1)
|
@@ -19,7 +21,25 @@ module Lapidar
|
|
19
21
|
|
20
22
|
# For each positition in the chain the candidate positioned first is considered the valid one
|
21
23
|
def blocks
|
22
|
-
@
|
24
|
+
@block_stacks.map { |candidates| candidates&.first }
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_colorful_string(depth = 0)
|
28
|
+
[*0..depth].map { |level|
|
29
|
+
@block_stacks.map { |block_stack|
|
30
|
+
if block_stack[level]
|
31
|
+
number_display = block_stack[level].number.to_s
|
32
|
+
if defined? Paint
|
33
|
+
number_display = Paint[number_display, block_stack[level].hash[-6..-1]] # use last hash digits as color
|
34
|
+
number_display = Paint[number_display, :bright, :underline] if level == 0 # emphasize preferred chain
|
35
|
+
number_display
|
36
|
+
end
|
37
|
+
number_display
|
38
|
+
else
|
39
|
+
" " * block_stack[0].number.to_s.length # padding by digit count
|
40
|
+
end
|
41
|
+
}.join(" ")
|
42
|
+
}.join("\n")
|
23
43
|
end
|
24
44
|
|
25
45
|
private
|
@@ -30,30 +50,30 @@ module Lapidar
|
|
30
50
|
return false unless Assessment.meets_difficulty?(block) # early invalid if difficulty not met
|
31
51
|
|
32
52
|
# Check if there's an existing parent
|
33
|
-
@
|
53
|
+
@block_stacks[block.number - 1].any? do |previous_block|
|
34
54
|
Assessment.valid_link?(previous_block, block)
|
35
55
|
end
|
36
56
|
end
|
37
57
|
|
38
58
|
# If a new last block comes in, we realign the first blocks to build the longest chain
|
39
59
|
def rebalance
|
40
|
-
winning_block = @
|
41
|
-
parent_position = @
|
60
|
+
winning_block = @block_stacks.last.first
|
61
|
+
parent_position = @block_stacks.count - 2
|
42
62
|
|
43
63
|
while contested?(parent_position)
|
44
64
|
# TODO: Is there's a smarter way to persistently select a winner than sorting the competition
|
45
|
-
@
|
65
|
+
@block_stacks[parent_position].sort_by! do |previous_block|
|
46
66
|
Assessment.valid_link?(previous_block, winning_block) ? 0 : 1
|
47
67
|
end
|
48
68
|
|
49
|
-
winning_block = @
|
69
|
+
winning_block = @block_stacks[parent_position].first
|
50
70
|
parent_position -= 1
|
51
71
|
end
|
52
72
|
end
|
53
73
|
|
54
74
|
# Contested evaluates to true if there blocks are competing for the same position in the blockchain
|
55
75
|
def contested?(block_number)
|
56
|
-
@
|
76
|
+
@block_stacks[block_number].count > 1
|
57
77
|
end
|
58
78
|
end
|
59
79
|
end
|
data/lib/lapidar/miner.rb
CHANGED
@@ -9,7 +9,14 @@ module Lapidar
|
|
9
9
|
def mine(base_block, data = "")
|
10
10
|
base_block ||= god
|
11
11
|
nonce = 0
|
12
|
-
|
12
|
+
|
13
|
+
until meets_difficulty?(digest(base_block, nonce, data))
|
14
|
+
nonce += 1
|
15
|
+
|
16
|
+
# Let others do work as well (TODO: nicer solution without thread context in the miner?)
|
17
|
+
Thread.pass if (nonce % 1000).zero?
|
18
|
+
end
|
19
|
+
|
13
20
|
Block.new(number: base_block.number + 1, hash: digest(base_block, nonce, data), nonce: nonce, data: data)
|
14
21
|
end
|
15
22
|
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "oj"
|
2
|
+
|
3
|
+
module Lapidar
|
4
|
+
class Persistence
|
5
|
+
CONFIG_DIR = File.join(ENV["HOME"], ".lapidar")
|
6
|
+
|
7
|
+
def self.save_chain(filename, chain)
|
8
|
+
Dir.mkdir(CONFIG_DIR) unless File.exist?(CONFIG_DIR)
|
9
|
+
File.write(File.join(CONFIG_DIR, filename), Oj.dump(chain))
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.load_chain(filename)
|
13
|
+
Oj.load(File.read(File.join(CONFIG_DIR, filename)))
|
14
|
+
rescue
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require "logger"
|
2
|
+
|
3
|
+
module Lapidar
|
4
|
+
class Runner
|
5
|
+
attr_reader :chain, :punch_queue, :network_endpoint, :logger
|
6
|
+
|
7
|
+
def initialize(network_endpoint)
|
8
|
+
@logger = Logger.new(StringIO.new)
|
9
|
+
@network_endpoint = network_endpoint
|
10
|
+
@chain = Persistence.load_chain("#{@network_endpoint.port}.json") || Chain.new
|
11
|
+
@incoming_blocks = Queue.new
|
12
|
+
@punch_queue = SizedQueue.new(1)
|
13
|
+
@should_stop = nil
|
14
|
+
@threads = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def start
|
18
|
+
@should_stop = false
|
19
|
+
@threads = [consumer, local_producer, network_producer]
|
20
|
+
@threads.each { |t| t.abort_on_exception = true }
|
21
|
+
@threads.each(&:join)
|
22
|
+
end
|
23
|
+
|
24
|
+
def stop
|
25
|
+
@should_stop = true
|
26
|
+
Thread.pass
|
27
|
+
Persistence.save_chain("#{@network_endpoint.port}.json", @chain)
|
28
|
+
@threads.each(&:exit)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def consumer
|
34
|
+
Thread.new do
|
35
|
+
until @should_stop
|
36
|
+
begin
|
37
|
+
@chain.add(@incoming_blocks.pop)
|
38
|
+
@logger.info("consumer") { "+" }
|
39
|
+
rescue => e
|
40
|
+
@logger.debug("consumer") { "Block cannot be added to chain: #{e.message}" }
|
41
|
+
@logger.info("consumer") { "_" }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def local_producer
|
48
|
+
Thread.new do
|
49
|
+
miner = Miner.new
|
50
|
+
until @should_stop
|
51
|
+
begin
|
52
|
+
new_block = miner.mine(@chain.blocks.last, @punch_queue.pop)
|
53
|
+
@network_endpoint.feed(Buschtelefon::Gossip.new(new_block.to_h.to_json))
|
54
|
+
@incoming_blocks << new_block
|
55
|
+
@logger.info("local_producer") { "!" }
|
56
|
+
rescue => e
|
57
|
+
@logger.debug("local_producer") { "Mint block isn't valid: #{e.message}" }
|
58
|
+
@logger.info("local_producer") { "F" }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def network_producer
|
65
|
+
Thread.new do
|
66
|
+
@network_endpoint.listen do |gossip|
|
67
|
+
break if @should_stop
|
68
|
+
|
69
|
+
begin
|
70
|
+
incoming_json = JSON.parse(gossip.message, symbolize_names: true)
|
71
|
+
|
72
|
+
@incoming_blocks << Block.new(
|
73
|
+
number: incoming_json[:number].to_i,
|
74
|
+
hash: incoming_json[:hash].to_s,
|
75
|
+
nonce: incoming_json[:nonce].to_i,
|
76
|
+
data: incoming_json[:data].to_s,
|
77
|
+
created_at: incoming_json[:created_at].to_f
|
78
|
+
)
|
79
|
+
rescue JSON::ParserError, ArgumentError => e
|
80
|
+
@logger.debug("network_producer") { "Incoming block isn't valid: #{e.message}" }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/lapidar/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lapidar
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josua Schmid
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-09-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: buschtelefon
|
@@ -16,14 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0.
|
19
|
+
version: '0.3'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '0.
|
26
|
+
version: '0.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: oj
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.0'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: bundler
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +66,20 @@ dependencies:
|
|
52
66
|
- - "~>"
|
53
67
|
- !ruby/object:Gem::Version
|
54
68
|
version: '5.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: paint
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.0'
|
55
83
|
- !ruby/object:Gem::Dependency
|
56
84
|
name: rake
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -130,12 +158,15 @@ files:
|
|
130
158
|
- bin/console
|
131
159
|
- bin/run
|
132
160
|
- bin/setup
|
161
|
+
- docs/visualization.png
|
133
162
|
- lapidar.gemspec
|
134
163
|
- lib/lapidar.rb
|
135
164
|
- lib/lapidar/assessment.rb
|
136
165
|
- lib/lapidar/block.rb
|
137
166
|
- lib/lapidar/chain.rb
|
138
167
|
- lib/lapidar/miner.rb
|
168
|
+
- lib/lapidar/persistence.rb
|
169
|
+
- lib/lapidar/runner.rb
|
139
170
|
- lib/lapidar/version.rb
|
140
171
|
homepage: https://github.com/renuo/lapidar
|
141
172
|
licenses:
|