zold 0.5 → 0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -2
  3. data/bin/zold +34 -48
  4. data/features/cli.feature +1 -1
  5. data/features/step_definitions/steps.rb +1 -3
  6. data/features/support/env.rb +2 -0
  7. data/fixtures/scripts/push-and-pull.sh +6 -4
  8. data/lib/zold/amount.rb +17 -3
  9. data/lib/zold/commands/create.rb +9 -6
  10. data/lib/zold/commands/fetch.rb +11 -21
  11. data/lib/zold/commands/node.rb +7 -9
  12. data/lib/zold/commands/pay.rb +9 -6
  13. data/lib/zold/commands/propagate.rb +4 -5
  14. data/lib/zold/commands/push.rb +10 -5
  15. data/lib/zold/commands/remote.rb +22 -24
  16. data/lib/zold/commands/show.rb +1 -2
  17. data/lib/zold/commands/taxes.rb +154 -0
  18. data/lib/zold/http.rb +1 -3
  19. data/lib/zold/key.rb +1 -3
  20. data/lib/zold/node/entrance.rb +8 -3
  21. data/lib/zold/node/farm.rb +8 -6
  22. data/lib/zold/node/front.rb +0 -1
  23. data/lib/zold/patch.rb +1 -1
  24. data/lib/zold/remotes.rb +4 -0
  25. data/lib/zold/score.rb +85 -10
  26. data/lib/zold/signature.rb +7 -7
  27. data/lib/zold/tax.rb +79 -0
  28. data/lib/zold/txn.rb +12 -7
  29. data/lib/zold/version.rb +1 -1
  30. data/lib/zold/wallet.rb +2 -2
  31. data/test/commands/test_create.rb +3 -4
  32. data/test/commands/test_diff.rb +2 -3
  33. data/test/commands/test_merge.rb +4 -6
  34. data/test/commands/test_pay.rb +7 -5
  35. data/test/commands/test_remote.rb +5 -3
  36. data/test/commands/test_taxes.rb +66 -0
  37. data/test/node/fake_node.rb +1 -0
  38. data/test/node/test_farm.rb +2 -1
  39. data/test/node/test_front.rb +1 -0
  40. data/test/test__helper.rb +2 -0
  41. data/test/test_remotes.rb +0 -1
  42. data/test/test_score.rb +40 -21
  43. data/test/test_signature.rb +6 -3
  44. data/test/test_tax.rb +53 -0
  45. data/test/test_txn.rb +46 -0
  46. data/test/test_wallet.rb +2 -2
  47. data/test/test_zold.rb +1 -1
  48. data/wp/.gitignore +6 -0
  49. data/wp/wp.tex +38 -0
  50. data/zold.gemspec +1 -3
  51. metadata +12 -2
@@ -61,21 +61,20 @@ Available options:"
61
61
  wallet.txns.select { |t| t.amount.negative? }.each do |t|
62
62
  target = @wallets.find(t.bnf)
63
63
  unless target.exists?
64
- @log.debug("#{t.amount.mul(-1)} to #{t.bnf}: wallet is absent")
64
+ @log.debug("#{t.amount * -1} to #{t.bnf}: wallet is absent")
65
65
  next
66
66
  end
67
67
  next if target.has?(t.id, me)
68
68
  unless Prefixes.new(target).valid?(t.prefix)
69
- @log.info("#{t.amount.mul(-1)} to #{t.bnf}: wrong prefix")
69
+ @log.info("#{t.amount * -1} to #{t.bnf}: wrong prefix")
70
70
  next
71
71
  end
72
72
  target.add(t.inverse(me))
73
- @log.info("#{t.amount.mul(-1)} to #{t.bnf}")
73
+ @log.info("#{t.amount * -1} arrived to #{t.bnf}: #{t.details}")
74
74
  modified << t.id
75
75
  end
76
76
  modified.uniq!
77
- @log.debug("Wallet #{me} propagated successfully, \
78
- #{modified.count} wallets affected")
77
+ @log.debug("Wallet #{me} propagated successfully, #{modified.count} wallets affected")
79
78
  modified
80
79
  end
81
80
  end
@@ -69,12 +69,17 @@ Available options:"
69
69
  next
70
70
  end
71
71
  json = JSON.parse(response.body)['score']
72
- score = Score.new(
73
- Time.parse(json['time']), json['host'],
74
- json['port'], json['suffixes']
75
- )
72
+ score = Score.parse_json(json)
76
73
  unless score.valid?
77
- @log.error("#{uri} invalid score")
74
+ @log.error("#{uri} invalid score: #{score}")
75
+ next
76
+ end
77
+ if score.expired?
78
+ @log.error("#{uri} expired score: #{score}")
79
+ next
80
+ end
81
+ if score.strength < Score::STRENGTH
82
+ @log.error("#{uri} score is too weak")
78
83
  next
79
84
  end
80
85
  @log.info("#{uri} accepted: #{Rainbow(score.value).green}")
@@ -50,9 +50,9 @@ Available commands:
50
50
  Remove all registered remote nodes
51
51
  #{Rainbow('remote reset').green}
52
52
  Restore it back to the default list of nodes
53
- #{Rainbow('remote add').green} host port
53
+ #{Rainbow('remote add').green} host [port]
54
54
  Add a new remote node
55
- #{Rainbow('remote remove').green} host port
55
+ #{Rainbow('remote remove').green} host [port]
56
56
  Remove the remote node
57
57
  #{Rainbow('remote update').green}
58
58
  Check each registered remote node for availability
@@ -62,7 +62,7 @@ Available options:"
62
62
  default: false
63
63
  o.bool '--help', 'Print instructions'
64
64
  end
65
- command = args[0]
65
+ command = opts.arguments[0]
66
66
  case command
67
67
  when 'show'
68
68
  show
@@ -71,9 +71,9 @@ Available options:"
71
71
  when 'reset'
72
72
  reset
73
73
  when 'add'
74
- add(opts.arguments[1], opts.arguments[2].to_i)
74
+ add(opts.arguments[1], opts.arguments[2] ? opts.arguments[2].to_i : Remotes::PORT)
75
75
  when 'remove'
76
- remove(opts.arguments[1], opts.arguments[2].to_i)
76
+ remove(opts.arguments[1], opts.arguments[2] ? opts.arguments[2].to_i : Remotes::PORT)
77
77
  when 'update'
78
78
  update(opts)
79
79
  update(opts, false)
@@ -117,9 +117,7 @@ Available options:"
117
117
  res = Http.new(uri).get
118
118
  unless res.code == '200'
119
119
  @remotes.remove(r[:host], r[:port])
120
- @log.info(
121
- "#{Rainbow(r[:host]).red} #{res.code} \"#{res.message}\" #{uri}"
122
- )
120
+ @log.info("#{Rainbow(r[:host]).red} #{res.code} \"#{res.message}\" #{uri}")
123
121
  next
124
122
  end
125
123
  begin
@@ -129,39 +127,39 @@ Available options:"
129
127
  @log.info("#{Rainbow(r[:host]).red} \"#{e.message}\": #{res.body}")
130
128
  next
131
129
  end
132
- score = Score.new(
133
- Time.parse(json['score']['time']), r[:host],
134
- r[:port], json['score']['suffixes']
135
- )
130
+ score = Score.parse_json(json['score'])
136
131
  unless score.valid?
137
132
  remove(r[:host], r[:port])
138
133
  @log.info("#{Rainbow(r[:host]).red} invalid score")
139
134
  next
140
135
  end
136
+ if score.expired?
137
+ remove(r[:host], r[:port])
138
+ @log.info("#{Rainbow(r[:host]).red} expired score")
139
+ next
140
+ end
141
141
  if score.strength < Score::STRENGTH && !opts['ignore-score-weakness']
142
142
  remove(r[:host], r[:port])
143
- @log.info(
144
- "#{Rainbow(r[:host]).red} score too weak: #{score.strength}"
145
- )
143
+ @log.info("#{Rainbow(r[:host]).red} score too weak: #{score.strength}")
146
144
  next
147
145
  end
148
- @remotes.rescore(r[:host], r[:port], score.value)
146
+ if r[:host] != score.host || r[:port] != score.port
147
+ @remotes.remove(r[:host], r[:port])
148
+ @remotes.add(score.host, score.port)
149
+ @log.info("#{r[:host]}:#{r[:port]} renamed to #{score.host}:#{score.port}")
150
+ end
151
+ @remotes.rescore(score.host, score.port, score.value)
149
152
  if deep
150
153
  json['all'].each do |s|
151
- unless @remotes.exists?(s['host'], s['port'])
152
- add(s['host'], s['port'])
153
- end
154
+ add(s['host'], s['port']) unless @remotes.exists?(s['host'], s['port'])
154
155
  end
155
156
  end
156
- @log.info("#{r[:host]}:#{r[:port]}: #{Rainbow(score.value).green} \
157
- (v.#{json['version']})")
157
+ @log.info("#{r[:host]}:#{r[:port]}: #{Rainbow(score.value).green} (v.#{json['version']})")
158
158
  end
159
159
  total = @remotes.all.size
160
160
  if total.zero?
161
161
  @log.debug("The list of remotes is #{Rainbow('empty').red}!")
162
- @log.debug(
163
- "Run 'zold remote add b1.zold.io 80` and then `zold update`"
164
- )
162
+ @log.debug("Run 'zold remote add b1.zold.io` and then `zold update`")
165
163
  else
166
164
  @log.debug("There are #{total} known remotes")
167
165
  end
@@ -60,8 +60,7 @@ Available options:"
60
60
  def show(wallet, _)
61
61
  balance = wallet.balance
62
62
  wallet.txns.each do |t|
63
- @log.info("##{t.id} #{t.date.utc.iso8601} \
64
- #{t.amount} #{t.bnf} #{t.details}")
63
+ @log.info(t.to_text)
65
64
  end
66
65
  @log.info("The balance of #{wallet}: #{balance}")
67
66
  balance
@@ -0,0 +1,154 @@
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 'slop'
22
+ require 'json'
23
+ require_relative 'pay'
24
+ require_relative '../log'
25
+ require_relative '../score'
26
+ require_relative '../id'
27
+ require_relative '../tax'
28
+ require_relative '../http'
29
+
30
+ # TAXES command.
31
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
32
+ # Copyright:: Copyright (c) 2018 Yegor Bugayenko
33
+ # License:: MIT
34
+ module Zold
35
+ # Taxes command
36
+ class Taxes
37
+ def initialize(wallets:, remotes:, log: Log::Quiet.new)
38
+ @wallets = wallets
39
+ @remotes = remotes
40
+ @log = log
41
+ end
42
+
43
+ def run(args = [])
44
+ opts = Slop.parse(args, help: true) do |o|
45
+ o.banner = "Usage: zold taxes command [options]
46
+ Available commands:
47
+ #{Rainbow('taxes pay').green} wallet
48
+ Pay taxes for the given wallet
49
+ #{Rainbow('taxes show').green}
50
+ Show taxes status for the given wallet
51
+ #{Rainbow('taxes debt').green}
52
+ Show current debt
53
+ Available options:"
54
+ o.string '--private-key',
55
+ 'The location of RSA private key (default: ~/.ssh/id_rsa)',
56
+ require: true,
57
+ default: '~/.ssh/id_rsa'
58
+ o.bool '--help', 'Print instructions'
59
+ end
60
+ command = opts.arguments[0]
61
+ case command
62
+ when 'show'
63
+ raise 'At least one wallet ID is required' unless opts.arguments[1]
64
+ opts.arguments[1..-1].each do |id|
65
+ show(@wallets.find(Id.new(id), opts))
66
+ end
67
+ when 'debt'
68
+ raise 'At least one wallet ID is required' unless opts.arguments[1]
69
+ opts.arguments[1..-1].each do |id|
70
+ debt(@wallets.find(Id.new(id), opts))
71
+ end
72
+ when 'pay'
73
+ raise 'At least one wallet ID is required' unless opts.arguments[1]
74
+ opts.arguments[1..-1].each do |id|
75
+ pay(@wallets.find(Id.new(id)), opts)
76
+ end
77
+ else
78
+ @log.info(opts.to_s)
79
+ end
80
+ end
81
+
82
+ def pay(wallet, _)
83
+ raise 'The wallet is absent' unless wallet.exists?
84
+ tax = Tax.new(wallet)
85
+ debt = tax.debt
86
+ @log.debug("The current debt is #{debt} (#{debt.to_i} zents)")
87
+ if debt <= Tax::TRIAL
88
+ @log.debug("No need to pay taxes yet, until the debt is less than #{Tax::TRIAL} (#{Tax::TRIAL.to_i} zents)")
89
+ return
90
+ end
91
+ top = top_scores
92
+ while debt > Amount::ZERO
93
+ raise 'No acceptable remote nodes, try later' if top.empty?
94
+ best = top.shift
95
+ txn = tax.pay(Zold::Key.new(file: opts['private-key']), best)
96
+ wallet.add(txn)
97
+ debt -= txn.amount
98
+ @log.info("#{txn.amount} of taxes paid to #{txn.bnf}, #{debt} left to pay")
99
+ end
100
+ @log.info('The wallet is in good standing, all taxes paid')
101
+ end
102
+
103
+ def debt(wallet, _)
104
+ raise 'The wallet is absent' unless wallet.exists?
105
+ tax = Tax.new(wallet)
106
+ @log.info(tax.debt)
107
+ end
108
+
109
+ def show(_, _)
110
+ raise 'Not implemented yet'
111
+ end
112
+
113
+ private
114
+
115
+ def top_scores
116
+ best = []
117
+ @remotes.all.each do |r|
118
+ uri = URI(r[:home])
119
+ name = "#{r[:host]}:#{r[:port]}"
120
+ res = Http.new(uri).get
121
+ unless res.code == '200'
122
+ @log.info("#{name}: #{Rainbow(res.code).red} \"#{res.message}\" at #{uri}")
123
+ next
124
+ end
125
+ begin
126
+ json = JSON.parse(res.body)
127
+ rescue JSON::ParserError => e
128
+ @log.info("#{name}: #{Rainbow('broken').red} JSON \"#{e.message}\": #{res.body}")
129
+ next
130
+ end
131
+ score = Score.parse_json(json['score'])
132
+ unless score.valid?
133
+ @log.info("#{name}: #{Rainbow('invalid').red} score")
134
+ next
135
+ end
136
+ if score.expired?
137
+ @log.info("#{name}: #{Rainbow('expired').red} score")
138
+ next
139
+ end
140
+ if score.strength < Score::STRENGTH
141
+ @log.info("#{name} score #{Rainbow(score.value).red} is too weak (<#{Score::STRENGTH})")
142
+ next
143
+ end
144
+ if score.value < Tax::MIN_SCORE
145
+ @log.info("#{name} score #{Rainbow(score.value).red} is too small (<#{Tax::MIN_SCORE})")
146
+ next
147
+ end
148
+ @log.info("#{score.host}:#{score.port}: #{Rainbow(score.value).green}")
149
+ best << score
150
+ end
151
+ best.sort_by(&:value).reverse
152
+ end
153
+ end
154
+ end
data/lib/zold/http.rb CHANGED
@@ -66,9 +66,7 @@ module Zold
66
66
  'User-Agent': "Zold #{VERSION}",
67
67
  'Connection': 'close'
68
68
  }
69
- if @score.valid? && @score.value >= 3
70
- headers[SCORE_HEADER] = score.reduced(4).to_s
71
- end
69
+ headers[SCORE_HEADER] = score.reduced(4).to_s if @score.valid? && @score.value >= 3 && !score.expired?
72
70
  headers
73
71
  end
74
72
  end
data/lib/zold/key.rb CHANGED
@@ -34,9 +34,7 @@ module Zold
34
34
  @body = lambda do
35
35
  unless file.nil?
36
36
  path = File.expand_path(file)
37
- unless File.exist?(path)
38
- raise "Can't find RSA key at #{file} (#{path})"
39
- end
37
+ raise "Can't find RSA key at #{file} (#{path})" unless File.exist?(path)
40
38
  return File.read(path)
41
39
  end
42
40
  unless text.nil?
@@ -22,6 +22,7 @@ require 'concurrent'
22
22
  require_relative '../log'
23
23
  require_relative '../remotes'
24
24
  require_relative '../copies'
25
+ require_relative '../tax'
25
26
  require_relative '../commands/merge'
26
27
  require_relative '../commands/fetch'
27
28
  require_relative '../commands/push'
@@ -52,15 +53,19 @@ module Zold
52
53
  def push_unsafe(id, body)
53
54
  copies = Copies.new(File.join(@copies, id.to_s))
54
55
  copies.add(body, 'remote', Remotes::PORT, 0)
55
- Zold::Fetch.new(
56
+ Fetch.new(
56
57
  remotes: @remotes, copies: copies.root, log: @log
57
58
  ).run([id.to_s, "--ignore-node=#{@address}"])
58
- modified = Zold::Merge.new(
59
+ modified = Merge.new(
59
60
  wallets: @wallets, copies: copies.root, log: @log
60
61
  ).run([id.to_s])
62
+ debt = Tax.new(@wallets.find(id)).debt
63
+ if debt > Tax::TRIAL
64
+ raise "Taxes are not paid, the debt is #{debt} (#{debt.to_i} zents), won't promote the wallet"
65
+ end
61
66
  copies.remove('remote', Remotes::PORT)
62
67
  modified.each do |m|
63
- Zold::Push.new(
68
+ Push.new(
64
69
  wallets: @wallets, remotes: @remotes, log: @log
65
70
  ).run([m.to_s])
66
71
  end
@@ -30,8 +30,9 @@ module Zold
30
30
  # Farm
31
31
  class Farm
32
32
  attr_reader :best
33
- def initialize(log: Log::Quiet.new)
33
+ def initialize(invoice, log: Log::Quiet.new)
34
34
  @log = log
35
+ @invoice = invoice
35
36
  @scores = []
36
37
  @threads = []
37
38
  @best = []
@@ -50,7 +51,7 @@ module Zold
50
51
  def start(host, port, strength: 8, threads: 8)
51
52
  @log.debug('Zero-threads farm won\'t score anything!') if threads.zero?
52
53
  @scores = Queue.new
53
- first = Score.new(Time.now, host, port, strength: strength)
54
+ first = Score.new(Time.now, host, port, @invoice, strength: strength)
54
55
  @best = [first]
55
56
  @scores << first
56
57
  @threads = (1..threads).map do |t|
@@ -64,12 +65,13 @@ module Zold
64
65
  @best << s
65
66
  after = @best.map(&:value).max
66
67
  @best.reject! { |b| b.value < after }
67
- if before != after
68
- @log.debug("#{Thread.current.name}: best is #{@best[0]}")
69
- end
68
+ @log.debug("#{Thread.current.name}: best is #{@best[0]}") if before != after
70
69
  end
71
70
  if @scores.length < 4
72
- @scores << Score.new(Time.now, host, port, strength: strength)
71
+ @scores << Score.new(
72
+ Time.now, host, port, @invoice,
73
+ strength: strength
74
+ )
73
75
  end
74
76
  @scores << s.next
75
77
  end
@@ -60,7 +60,6 @@ module Zold
60
60
  if request.env[Http::SCORE_HEADER] && !settings.remotes.empty?
61
61
  s = Score.parse(request.env[Http::SCORE_HEADER])
62
62
  error(400, 'The score is invalid') unless s.valid?
63
- error(400, 'The score is weak') if s.strength < Score::STRENGTH
64
63
  settings.remotes.add(s.host, s.port) if s.value > 3
65
64
  end
66
65
  end
data/lib/zold/patch.rb CHANGED
@@ -44,7 +44,7 @@ module Zold
44
44
  (txn.id <= max ||
45
45
  @txns.find { |t| t.id == txn.id } ||
46
46
  @txns.map(&:amount).inject(&:+) < txn.amount)
47
- next unless Signature.new.valid?(@key, txn)
47
+ next unless Signature.new.valid?(@key, wallet.id, txn)
48
48
  @txns << txn
49
49
  end
50
50
  end
data/lib/zold/remotes.rb CHANGED
@@ -67,6 +67,7 @@ module Zold
67
67
  raise 'Port has to be of type Integer' unless port.is_a?(Integer)
68
68
  raise 'Port can\'t be negative' if port < 0
69
69
  raise 'Port can\'t be over 65536' if port > 0xffff
70
+ raise "#{host}:#{port} alread exists" if exists?(host, port)
70
71
  list = load
71
72
  list << { host: host.downcase, port: port, score: 0 }
72
73
  list.uniq! { |r| "#{r[:host]}:#{r[:port]}" }
@@ -75,6 +76,7 @@ module Zold
75
76
 
76
77
  def remove(host, port = Remotes::PORT)
77
78
  raise 'Port has to be of type Integer' unless port.is_a?(Integer)
79
+ raise "#{host}:#{port} is absent" unless exists?(host, port)
78
80
  list = load
79
81
  list.reject! { |r| r[:host] == host.downcase && r[:port] == port }
80
82
  save(list)
@@ -82,11 +84,13 @@ module Zold
82
84
 
83
85
  def score(host, port = Remotes::PORT)
84
86
  raise 'Port has to be of type Integer' unless port.is_a?(Integer)
87
+ raise "#{host}:#{port} is absent" unless exists?(host, port)
85
88
  load.find { |r| r[:host] == host.downcase && r[:port] == port }[:score]
86
89
  end
87
90
 
88
91
  def rescore(host, port, score)
89
92
  raise 'Port has to be of type Integer' unless port.is_a?(Integer)
93
+ raise "#{host}:#{port} is absent" unless exists?(host, port)
90
94
  list = load
91
95
  list.find do |r|
92
96
  r[:host] == host.downcase && r[:port] == port