zold 0.5 → 0.6

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