zold 0.0.8 → 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +12 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +11 -0
  4. data/.rubocop.yml +9 -1
  5. data/.simplecov +2 -2
  6. data/.travis.yml +1 -1
  7. data/Gemfile +1 -1
  8. data/LICENSE.txt +1 -1
  9. data/Procfile +1 -1
  10. data/README.md +208 -101
  11. data/Rakefile +2 -1
  12. data/bin/zold +135 -54
  13. data/features/cli.feature +1 -1
  14. data/features/step_definitions/steps.rb +1 -1
  15. data/features/support/env.rb +1 -1
  16. data/fixtures/scripts/push-and-pull.sh +35 -0
  17. data/lib/zold.rb +2 -2
  18. data/lib/zold/amount.rb +10 -2
  19. data/lib/zold/commands/{send.rb → clean.rb} +16 -16
  20. data/lib/zold/commands/create.rb +7 -5
  21. data/lib/zold/commands/diff.rb +59 -0
  22. data/lib/zold/commands/fetch.rb +74 -0
  23. data/lib/zold/commands/{pull.rb → list.rb} +11 -17
  24. data/lib/zold/commands/merge.rb +50 -0
  25. data/lib/zold/commands/node.rb +94 -0
  26. data/lib/zold/commands/pay.rb +58 -0
  27. data/lib/zold/commands/{check.rb → propagate.rb} +19 -20
  28. data/lib/zold/commands/push.rb +12 -12
  29. data/lib/zold/commands/remote.rb +115 -0
  30. data/lib/zold/commands/{balance.rb → show.rb} +11 -7
  31. data/lib/zold/copies.rb +126 -0
  32. data/lib/zold/http.rb +70 -0
  33. data/lib/zold/id.rb +8 -3
  34. data/lib/zold/key.rb +2 -2
  35. data/lib/zold/log.rb +51 -2
  36. data/lib/zold/node/farm.rb +81 -0
  37. data/lib/zold/node/front.rb +94 -46
  38. data/lib/zold/patch.rb +58 -0
  39. data/lib/zold/remotes.rb +106 -0
  40. data/lib/zold/score.rb +101 -0
  41. data/lib/zold/signature.rb +48 -0
  42. data/lib/zold/version.rb +3 -3
  43. data/lib/zold/wallet.rb +87 -55
  44. data/lib/zold/wallets.rb +13 -6
  45. data/resources/remotes +1 -0
  46. data/test/commands/test_clean.rb +41 -0
  47. data/test/commands/test_create.rb +2 -2
  48. data/test/commands/test_diff.rb +61 -0
  49. data/test/commands/test_fetch.rb +65 -0
  50. data/test/commands/test_list.rb +42 -0
  51. data/test/commands/test_merge.rb +62 -0
  52. data/test/commands/test_node.rb +56 -0
  53. data/test/commands/{test_send.rb → test_pay.rb} +10 -11
  54. data/test/commands/test_remote.rb +60 -0
  55. data/test/commands/{test_balance.rb → test_show.rb} +6 -8
  56. data/test/node/fake_node.rb +73 -0
  57. data/test/node/test_farm.rb +34 -0
  58. data/test/node/test_front.rb +26 -57
  59. data/test/test__helper.rb +1 -1
  60. data/test/test_amount.rb +10 -2
  61. data/test/test_copies.rb +73 -0
  62. data/test/test_http.rb +42 -0
  63. data/test/test_id.rb +2 -2
  64. data/test/test_key.rb +10 -10
  65. data/test/test_patch.rb +59 -0
  66. data/test/test_remotes.rb +72 -0
  67. data/test/test_score.rb +79 -0
  68. data/test/test_signature.rb +45 -0
  69. data/test/test_wallet.rb +18 -35
  70. data/test/test_wallets.rb +14 -3
  71. data/test/test_zold.rb +52 -5
  72. data/zold.gemspec +5 -3
  73. metadata +92 -21
  74. data/CONTRIBUTING.md +0 -19
  75. data/views/index.haml +0 -6
  76. data/views/layout.haml +0 -26
  77. data/views/not_found.haml +0 -3
@@ -0,0 +1,81 @@
1
+ # Copyright (c) 2018 Yegor Bugayenko
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the 'Software'), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ require 'time'
22
+ require_relative '../log'
23
+ require_relative '../score'
24
+
25
+ # The farm of scores.
26
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
27
+ # Copyright:: Copyright (c) 2018 Yegor Bugayenko
28
+ # License:: MIT
29
+ module Zold
30
+ # Farm
31
+ class Farm
32
+ attr_reader :best
33
+ def initialize(log: Log::Quiet.new)
34
+ @log = log
35
+ @scores = []
36
+ @threads = []
37
+ @best = []
38
+ @best << Score::ZERO
39
+ @semaphore = Mutex.new
40
+ end
41
+
42
+ def start(host, port, strength: 8, threads: 8)
43
+ @log.debug('Zero-threads farm won\'t score anything!') if threads.zero?
44
+ @scores = Queue.new
45
+ first = Score.new(Time.now, host, port, strength: strength)
46
+ @best = [first]
47
+ @scores << first
48
+ @threads = (1..threads).map do |t|
49
+ Thread.new do
50
+ Thread.current.name = "farm-#{t}"
51
+ loop do
52
+ s = @scores.pop
53
+ next unless s.valid?
54
+ @semaphore.synchronize do
55
+ before = @best.map(&:value).max
56
+ @best << s
57
+ after = @best.map(&:value).max
58
+ @best.reject! { |b| b.value < after }
59
+ if before != after
60
+ @log.debug("#{Thread.current.name}: best is #{@best[0]}")
61
+ end
62
+ end
63
+ if @scores.length < 4
64
+ @scores << Score.new(Time.now, host, port, strength: strength)
65
+ end
66
+ @scores << s.next
67
+ end
68
+ end
69
+ end
70
+ @log.debug("Farm started with #{threads} threads at #{host}:#{port}")
71
+ end
72
+
73
+ def stop
74
+ @threads.each do |t|
75
+ t.exit
76
+ @log.debug("Thread #{t.name} terminated")
77
+ end
78
+ @log.debug('Farm stopped')
79
+ end
80
+ end
81
+ end
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2018 Zerocracy, Inc.
1
+ # Copyright (c) 2018 Yegor Bugayenko
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  # of this software and associated documentation files (the 'Software'), to deal
@@ -20,93 +20,141 @@
20
20
 
21
21
  STDOUT.sync = true
22
22
 
23
- require 'haml'
23
+ require 'slop'
24
+ require 'facter'
25
+ require 'facter/util/memory'
24
26
  require 'json'
25
27
  require 'sinatra/base'
28
+ require 'webrick'
26
29
 
30
+ require_relative 'farm'
27
31
  require_relative '../version'
28
32
  require_relative '../wallet'
29
33
  require_relative '../wallets'
34
+ require_relative '../log'
35
+ require_relative '../remotes'
30
36
  require_relative '../id'
31
- require_relative '../commands/check'
37
+ require_relative '../http'
38
+ require_relative '../commands/merge'
32
39
 
33
40
  # The web front of the node.
34
41
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
35
- # Copyright:: Copyright (c) 2018 Zerocracy, Inc.
42
+ # Copyright:: Copyright (c) 2018 Yegor Bugayenko
36
43
  # License:: MIT
37
44
  module Zold
38
45
  # Web front
39
46
  class Front < Sinatra::Base
40
47
  configure do
41
- Haml::Options.defaults[:format] = :xhtml
48
+ set :logging, true
49
+ set :start, Time.now
42
50
  set :lock, Mutex.new
43
- set :views, (proc { File.join(root, '../../../views') })
51
+ set :log, Log.new
44
52
  set :show_exceptions, false
45
- set :wallets, Wallets.new(Dir.mktmpdir('zold-', '/tmp'))
53
+ set :home, Dir.pwd
54
+ set :farm, Farm.new
55
+ set :server, 'webrick'
46
56
  end
47
57
 
48
- get '/' do
49
- haml :index, layout: :layout, locals: {
50
- title: 'zold',
51
- total: settings.wallets.total
52
- }
58
+ before do
59
+ if request.env[Http::SCORE_HEADER]
60
+ s = Score.parse(request.env[Http::SCORE_HEADER])
61
+ raise 'The score is invalid' if !s.valid? || s.value < 3
62
+ settings.remotes.add(s.host, s.port)
63
+ end
53
64
  end
54
65
 
55
66
  get '/robots.txt' do
56
67
  'User-agent: *'
57
68
  end
58
69
 
59
- get '/version' do
60
- VERSION
70
+ get '/favicon.ico' do
71
+ redirect 'https://www.zold.io/logo.png'
72
+ end
73
+
74
+ get '/' do
75
+ content_type 'application/json'
76
+ JSON.pretty_generate(
77
+ version: VERSION,
78
+ score: score.to_h,
79
+ platform: {
80
+ uptime: `uptime`.strip,
81
+ hostname: `hostname`.strip,
82
+ # see https://docs.puppet.com/facter/3.3/core_facts.html
83
+ kernel: Facter.value(:kernel),
84
+ processors: Facter.value(:processors)['count'],
85
+ memory: Facter::Memory.mem_size
86
+ },
87
+ date: `date --iso-8601=seconds -u`.strip,
88
+ age: Time.now - settings.start,
89
+ home: 'https://www.zold.io'
90
+ )
61
91
  end
62
92
 
63
- get %r{/wallets/(?<id>[a-f0-9]{16})/?} do
93
+ get %r{/wallet/(?<id>[A-Fa-f0-9]{16})} do
64
94
  id = Id.new(params[:id])
65
- wallet = settings.wallets.find(id)
95
+ wallet = wallets.find(id)
66
96
  error 404 unless wallet.exists?
67
- File.read(wallet.path)
97
+ content_type 'application/json'
98
+ {
99
+ score: score.to_h,
100
+ body: File.read(wallet.path)
101
+ }.to_json
68
102
  end
69
103
 
70
- put %r{/wallets/(?<id>[a-f0-9]{16})/?} do
71
- settings.lock.synchronize do
72
- id = Id.new(params[:id])
73
- wallet = settings.wallets.find(id)
74
- temp = before = nil
75
- if wallet.exists?
76
- before = wallet.version
77
- temp = Tempfile.new('z')
78
- FileUtils.cp(wallet.path, temp)
79
- end
80
- begin
81
- request.body.rewind
82
- File.write(wallet.path, request.body.read)
83
- unless before.nil?
84
- after = wallet.version
85
- error 403 if after < before
86
- end
87
- unless Check.new(wallet: wallet, wallets: settings.wallets).run
88
- error 403
89
- end
90
- ensure
91
- unless temp.nil?
92
- FileUtils.cp(temp, wallet.path)
93
- temp.unlink
94
- end
104
+ put %r{/wallet/(?<id>[A-Fa-f0-9]{16})/?} do
105
+ id = Id.new(params[:id])
106
+ wallet = wallets.find(id)
107
+ request.body.rewind
108
+ cps = copies(id)
109
+ cps.add(request.body.read, 'remote', 80, 0)
110
+ Zold::Merge.new(wallet: wallet, copies: cps).run
111
+ "Success, #{wallet.id} balance is #{wallet.balance}"
112
+ end
113
+
114
+ get '/remotes' do
115
+ content_type 'application/json'
116
+ JSON.pretty_generate(
117
+ score: score.to_h,
118
+ all: remotes.all.map do |r|
119
+ {
120
+ host: r[:host],
121
+ port: r[:port]
122
+ }
95
123
  end
96
- end
124
+ )
97
125
  end
98
126
 
99
127
  not_found do
100
128
  status 404
101
- haml :not_found, layout: :layout, locals: {
102
- title: 'Page not found'
103
- }
129
+ content_type 'text/plain'
130
+ 'Page not found'
104
131
  end
105
132
 
106
133
  error do
107
134
  status 503
108
135
  e = env['sinatra.error']
136
+ content_type 'text/plain'
109
137
  "#{e.message}\n\t#{e.backtrace.join("\n\t")}"
110
138
  end
139
+
140
+ private
141
+
142
+ def copies(id)
143
+ Copies.new(File.join(settings.home, ".zold/copies/#{id}"))
144
+ end
145
+
146
+ def remotes
147
+ Remotes.new(File.join(settings.home, '.zold/remotes'))
148
+ end
149
+
150
+ def wallets
151
+ Wallets.new(settings.home)
152
+ end
153
+
154
+ def score
155
+ best = settings.farm.best
156
+ error 404 if best.empty?
157
+ best[0]
158
+ end
111
159
  end
112
160
  end
@@ -0,0 +1,58 @@
1
+ # Copyright (c) 2018 Yegor Bugayenko
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the 'Software'), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ require_relative 'wallet.rb'
22
+
23
+ # Patch.
24
+ #
25
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
26
+ # Copyright:: Copyright (c) 2018 Yegor Bugayenko
27
+ # License:: MIT
28
+ module Zold
29
+ # A patch
30
+ class Patch
31
+ def start(wallet)
32
+ @id = wallet.id
33
+ @key = wallet.key
34
+ @txns = wallet.txns
35
+ end
36
+
37
+ def join(wallet)
38
+ negative = @txns.select { |t| t[:amount].negative? }
39
+ max = negative.empty? ? 0 : negative.max_by { |t| t[:id] }[:id]
40
+ wallet.txns.each do |txn|
41
+ next if @txns.find { |t| t[:id] == txn[:id] && t[:bnf] == txn[:bnf] }
42
+ next if
43
+ txn[:amount].negative? && !@txns.empty? &&
44
+ (txn[:id] <= max ||
45
+ @txns.find { |t| t[:id] == txn[:id] } ||
46
+ @txns.map { |t| t[:amount] }.inject(&:+) < txn[:amount])
47
+ next unless Signature.new.valid?(@key, txn)
48
+ @txns << txn
49
+ end
50
+ end
51
+
52
+ def save(file, overwrite: false)
53
+ wallet = Zold::Wallet.new(file)
54
+ wallet.init(@id, @key, overwrite: overwrite)
55
+ @txns.each { |t| wallet.add(t) }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,106 @@
1
+ # Copyright (c) 2018 Yegor Bugayenko
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the 'Software'), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ require 'csv'
22
+ require 'uri'
23
+ require 'fileutils'
24
+
25
+ # The list of remotes.
26
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
27
+ # Copyright:: Copyright (c) 2018 Yegor Bugayenko
28
+ # License:: MIT
29
+ module Zold
30
+ # All remotes
31
+ class Remotes
32
+ def initialize(file)
33
+ @file = file
34
+ end
35
+
36
+ def total
37
+ load.length
38
+ end
39
+
40
+ def all
41
+ load.sort_by { |r| r[:score] }.reverse
42
+ end
43
+
44
+ def clean
45
+ save([])
46
+ end
47
+
48
+ def reset
49
+ FileUtils.mkdir_p(File.dirname(@file))
50
+ FileUtils.copy(
51
+ File.join(File.dirname(__FILE__), '../../resources/remotes'),
52
+ @file
53
+ )
54
+ end
55
+
56
+ def add(host, port = 80)
57
+ list = load
58
+ list << { host: host, port: port, score: 0 }
59
+ list.uniq! { |r| "#{r[:host]}:#{r[:port]}" }
60
+ save(list)
61
+ end
62
+
63
+ def remove(host, port = 80)
64
+ list = load
65
+ list.reject! { |r| r[:host] == host && r[:port] == port }
66
+ save(list)
67
+ end
68
+
69
+ def score(host, port = 80)
70
+ load.find { |r| r[:host] == host && r[:port] == port }[:score]
71
+ end
72
+
73
+ def rescore(host, port, score)
74
+ list = load
75
+ list.find do |r|
76
+ r[:host] == host && r[:port] == port
77
+ end[:score] = score
78
+ save(list)
79
+ end
80
+
81
+ private
82
+
83
+ def load
84
+ CSV.read(file).map do |r|
85
+ {
86
+ host: r[0],
87
+ port: r[1].to_i,
88
+ score: r[2].to_i,
89
+ home: URI("http://#{r[0]}:#{r[1]}/")
90
+ }
91
+ end
92
+ end
93
+
94
+ def save(list)
95
+ File.write(
96
+ file,
97
+ list.map { |r| "#{r[:host]},#{r[:port]},#{r[:score]}" }.join("\n")
98
+ )
99
+ end
100
+
101
+ def file
102
+ reset unless File.exist?(@file)
103
+ @file
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,101 @@
1
+ # Copyright (c) 2018 Yegor Bugayenko
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the 'Software'), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ require 'digest'
22
+ require 'time'
23
+
24
+ # The score.
25
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
26
+ # Copyright:: Copyright (c) 2018 Yegor Bugayenko
27
+ # License:: MIT
28
+ module Zold
29
+ # Score
30
+ class Score
31
+ DEFAULT_STRENGTH = 8
32
+ attr_reader :time, :host, :port
33
+ # time: UTC ISO 8601 string
34
+ def initialize(time, host, port, suffixes = [], strength: DEFAULT_STRENGTH)
35
+ raise 'Time must be of type Time' unless time.is_a?(Time)
36
+ raise 'Port must be of type Integer' unless port.is_a?(Integer)
37
+ @time = time
38
+ @host = host
39
+ @port = port
40
+ @suffixes = suffixes
41
+ @strength = strength
42
+ end
43
+
44
+ ZERO = Score.new(Time.now, 'localhost', 80)
45
+
46
+ def self.parse(text, strength: DEFAULT_STRENGTH)
47
+ _, time, host, port, suffixes = text.split(' ', 5)
48
+ Score.new(
49
+ Time.parse(time), host, port.to_i,
50
+ suffixes.split(' '), strength: strength
51
+ )
52
+ end
53
+
54
+ def to_s
55
+ "#{value}: #{@time.utc.iso8601} #{@host} #{@port} #{@suffixes.join(' ')}"
56
+ end
57
+
58
+ def to_h
59
+ {
60
+ value: value,
61
+ host: @host,
62
+ port: @port,
63
+ time: @time.utc.iso8601,
64
+ suffixes: @suffixes,
65
+ strength: @strength
66
+ }
67
+ end
68
+
69
+ def reduced(max = 4)
70
+ Score.new(@time, @host, @port, @suffixes[0..max - 1], strength: @strength)
71
+ end
72
+
73
+ def next
74
+ raise 'This score is not valid' unless valid?
75
+ idx = 0
76
+ loop do
77
+ suffix = idx.to_s(16)
78
+ score = Score.new(
79
+ @time, @host, @port, @suffixes + [suffix],
80
+ strength: @strength
81
+ )
82
+ return score if score.valid?
83
+ idx += 1
84
+ end
85
+ end
86
+
87
+ def valid?
88
+ start = "#{@time.utc.iso8601} #{@host} #{@port}"
89
+ @suffixes.reduce(start) do |prefix, suffix|
90
+ hex = Digest::SHA256.hexdigest(prefix + ' ' + suffix)
91
+ return false unless hex.end_with?('0' * @strength)
92
+ hex[0, 19]
93
+ end
94
+ true
95
+ end
96
+
97
+ def value
98
+ @suffixes.length
99
+ end
100
+ end
101
+ end