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