lapidar 0.1.0 → 0.2.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 +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
|
+

|
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:
|