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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d024f24e254ae420266eb80b779cdca010785af1
4
- data.tar.gz: 8012ac6e40bfcd0be7644d9b4b002466a63cd841
3
+ metadata.gz: 7550a6712aa1a4ec73b55dd89292c037e9d7c518
4
+ data.tar.gz: 157f2757955b82097def09ccd07e8d34e6bd49e2
5
5
  SHA512:
6
- metadata.gz: c6ee55f3749e40f80d361be1d8ddbde43bdb9c1abdbbd572c4fa6845320006e3ac800a4a80f2ac35a3ab04b6378829f4ec5005633ef2e368b45bd666ffe8cfb0
7
- data.tar.gz: 7cc94309a33288b6d48d89ac6dd1a45ff5cd83fe1d21e39d69de32940b4e72dbc8c600e0a990fcd167f1855f9855877f823b59da71f37741eb96d418f4f46744
6
+ metadata.gz: 2d7af0295583b6e07a2a329c9a93f478c1ec61f33d140262768dd2f384fd7cf341e5bac0a0efd10255399f60eab826baf527c78dceef95a7d5b42757013fa910
7
+ data.tar.gz: a1b80a2db11d67d29c80a03113040ab8309e679961e71476282bdc4c64315865fb7dcb2bdd45da0a096fb127c89e5df1bc087e062b72ce4fe3f643a162ea7d3b
@@ -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
- TODO: Write usage instructions here
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. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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/[USERNAME]/lapidar.
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 '../lib/lapidar'
8
+ require_relative "../lib/lapidar"
7
9
 
8
- puts 'Starting experiment…'
10
+ puts "Starting experiment…"
9
11
 
10
- [
11
- Thread.new { Lapidar.start_mining(port: 9999, neighbors: [{ host: 'localhost', port: 9995 }]) },
12
- Thread.new { Lapidar.start_mining(port: 9998, neighbors: [{ host: 'localhost', port: 9996 }]) },
13
- Thread.new { Lapidar.start_mining(port: 9997, neighbors: [{ host: 'localhost', port: 9997 }]) },
14
- Thread.new { Lapidar.start_mining(port: 9996, neighbors: [{ host: 'localhost', port: 9998 }]) },
15
- Thread.new { Lapidar.start_mining(port: 9995, neighbors: [{ host: 'localhost', port: 9999 }]) }
16
- ].each(&:join)
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
@@ -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.2"
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"
@@ -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.start_mining(port:, neighbors:)
12
- chain = Chain.new
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| me.connect(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
- loop do
78
- yield
79
- end
18
+ Runner.new(network_endpoint)
80
19
  end
81
20
  end
@@ -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
@@ -1,17 +1,19 @@
1
1
  module Lapidar
2
2
  class Chain
3
+ attr_reader :block_stacks
4
+
3
5
  def initialize
4
- @blocks = []
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 > @blocks.count
12
+ raise "future block?" if block.number > @block_stacks.count
11
13
  raise "invalid block" unless valid?(block)
12
14
 
13
- @blocks[block.number] ||= []
14
- @blocks[block.number].push(block)
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
- @blocks.map { |candidates| candidates&.first }
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
- @blocks[block.number - 1].any? do |previous_block|
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 = @blocks.last.first
41
- parent_position = @blocks.count - 2
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
- @blocks[parent_position].sort_by! do |previous_block|
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 = @blocks[parent_position].first
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
- @blocks[block_number].count > 1
76
+ @block_stacks[block_number].count > 1
57
77
  end
58
78
  end
59
79
  end
@@ -9,7 +9,14 @@ module Lapidar
9
9
  def mine(base_block, data = "")
10
10
  base_block ||= god
11
11
  nonce = 0
12
- nonce += 1 until meets_difficulty?(digest(base_block, nonce, data))
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
@@ -1,3 +1,3 @@
1
1
  module Lapidar
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
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.1.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-08-26 00:00:00.000000000 Z
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.2'
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.2'
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: