zold 0.13.46 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/.rultor.yml +2 -2
  4. data/.simplecov +1 -1
  5. data/.travis.yml +3 -1
  6. data/INSTALL.md +4 -1
  7. data/README.md +98 -3
  8. data/Rakefile +1 -0
  9. data/bin/zold +10 -1
  10. data/fixtures/merge/into-no-wallet/assert.rb +23 -0
  11. data/fixtures/merge/{random-expenses/0123456789abcdef → into-no-wallet/copies/0123456789abcdef/1.z} +0 -0
  12. data/fixtures/merge/into-no-wallet/copies/0123456789abcdef/scores.z +1 -0
  13. data/fixtures/merge/random-expenses/{0000000000000000 → 0000000000000000.z} +0 -0
  14. data/fixtures/merge/{simple-case/copies/0123456789abcdef/1 → random-expenses/0123456789abcdef.z} +0 -0
  15. data/fixtures/merge/random-expenses/copies/0123456789abcdef/{1 → 1.z} +0 -0
  16. data/fixtures/merge/random-expenses/copies/0123456789abcdef/{2 → 2.z} +0 -0
  17. data/fixtures/merge/random-expenses/copies/0123456789abcdef/{3 → 3.z} +0 -0
  18. data/fixtures/merge/random-expenses/copies/0123456789abcdef/{4 → 4.z} +0 -0
  19. data/fixtures/merge/random-expenses/copies/0123456789abcdef/{5 → 5.z} +0 -0
  20. data/fixtures/merge/random-expenses/copies/0123456789abcdef/scores.z +5 -0
  21. data/fixtures/merge/simple-case/{0000000000000000 → 0000000000000000.z} +0 -0
  22. data/fixtures/merge/simple-case/{0123456789abcdef → 0123456789abcdef.z} +0 -1
  23. data/fixtures/merge/simple-case/copies/0123456789abcdef/1.z +6 -0
  24. data/fixtures/merge/simple-case/copies/0123456789abcdef/scores.z +1 -0
  25. data/fixtures/scripts/push-and-pull.sh +1 -0
  26. data/lib/zold/atomic_file.rb +7 -6
  27. data/lib/zold/commands/alias.rb +37 -0
  28. data/lib/zold/commands/calculate.rb +5 -2
  29. data/lib/zold/commands/diff.rb +2 -2
  30. data/lib/zold/commands/fetch.rb +23 -3
  31. data/lib/zold/commands/merge.rb +12 -7
  32. data/lib/zold/commands/node.rb +5 -2
  33. data/lib/zold/commands/pay.rb +11 -0
  34. data/lib/zold/commands/push.rb +1 -1
  35. data/lib/zold/commands/remote.rb +28 -2
  36. data/lib/zold/commands/taxes.rb +14 -2
  37. data/lib/zold/copies.rb +9 -8
  38. data/lib/zold/hungry_wallets.rb +12 -0
  39. data/lib/zold/log.rb +17 -2
  40. data/lib/zold/node/async_entrance.rb +48 -9
  41. data/lib/zold/node/entrance.rb +1 -1
  42. data/lib/zold/node/front.rb +28 -3
  43. data/lib/zold/node/safe_entrance.rb +3 -3
  44. data/lib/zold/node/spread_entrance.rb +1 -1
  45. data/lib/zold/patch.rb +8 -4
  46. data/lib/zold/remotes.rb +7 -0
  47. data/lib/zold/score.rb +2 -2
  48. data/lib/zold/tax.rb +0 -1
  49. data/lib/zold/upgrades.rb +57 -0
  50. data/lib/zold/version.rb +2 -1
  51. data/lib/zold/version_file.rb +39 -0
  52. data/lib/zold/wallet.rb +17 -4
  53. data/lib/zold/wallets.rb +9 -6
  54. data/test/commands/routines/test_spread.rb +1 -0
  55. data/test/commands/test_alias.rb +7 -0
  56. data/test/commands/test_create.rb +2 -2
  57. data/test/commands/test_fetch.rb +2 -1
  58. data/test/commands/test_merge.rb +2 -0
  59. data/test/commands/test_node.rb +1 -0
  60. data/test/commands/test_pay.rb +24 -0
  61. data/test/commands/test_remote.rb +28 -0
  62. data/test/node/test_async_entrance.rb +6 -13
  63. data/test/node/test_front.rb +2 -1
  64. data/test/test__helper.rb +3 -2
  65. data/test/test_atomic_file.rb +0 -5
  66. data/test/test_copies.rb +14 -2
  67. data/test/test_remotes.rb +12 -0
  68. data/test/test_score.rb +14 -0
  69. data/test/test_tax.rb +5 -0
  70. data/test/test_upgrades.rb +97 -0
  71. data/test/test_version.rb +13 -0
  72. data/test/test_wallet.rb +21 -1
  73. data/test/test_wallets.rb +8 -0
  74. data/upgrades/2.rb +20 -0
  75. data/zold.gemspec +3 -3
  76. metadata +38 -23
  77. data/fixtures/merge/random-expenses/copies/0123456789abcdef/scores +0 -5
  78. data/fixtures/merge/simple-case/copies/0123456789abcdef/scores +0 -1
@@ -63,10 +63,15 @@ Available commands:
63
63
  #{Rainbow('remote elect').green}
64
64
  Pick a random remote node as a target for a bonus awarding
65
65
  #{Rainbow('remote trim').green}
66
- Remote the least reliable nodes
66
+ Remove the least reliable nodes
67
+ #{Rainbow('remote select [options]').green}
68
+ Select the strongest n nodes.
67
69
  #{Rainbow('remote update').green}
68
70
  Check each registered remote node for availability
69
71
  Available options:"
72
+ o.integer '--tolerate',
73
+ 'Maximum level of errors we are able to tolerate',
74
+ default: Remotes::TOLERANCE
70
75
  o.bool '--ignore-score-weakness',
71
76
  'Don\'t complain when their score is too weak',
72
77
  default: false
@@ -86,6 +91,19 @@ Available options:"
86
91
  o.bool '--reboot',
87
92
  'Exit if any node reports version higher than we have',
88
93
  default: false
94
+
95
+ # @todo #292:30min Group options by subcommands
96
+ # Having all the options in one place _rather than grouping them by subcommands_
97
+ # makes the help totally misleading and hard to read.
98
+ # Not all the options are valid for every command - that's the key here.
99
+ # The option below (`--max-nodes`) is an example.
100
+ # **Next actions:**
101
+ # - Implement the suggestion above.
102
+ # - Remove note from the --max-nodes option saying that it applies to the select
103
+ # subcommand only.
104
+ o.integer '--max-nodes',
105
+ "This applies only to the select subcommand. Number of nodes to limit to. Defaults to #{Remotes::MAX_NODES}.",
106
+ default: Remotes::MAX_NODES
89
107
  o.bool '--help', 'Print instructions'
90
108
  end
91
109
  mine = Args.new(opts, @log).take || return
@@ -109,6 +127,8 @@ Available options:"
109
127
  when 'update'
110
128
  update(opts)
111
129
  update(opts, false)
130
+ when 'select'
131
+ select(opts)
112
132
  else
113
133
  raise "Unknown command '#{command}'"
114
134
  end
@@ -184,7 +204,7 @@ Available options:"
184
204
 
185
205
  def trim(opts)
186
206
  @remotes.all.each do |r|
187
- remove(r[:host], r[:port], opts) if r[:errors] > Remotes::TOLERANCE
207
+ remove(r[:host], r[:port], opts) if r[:errors] > opts['tolerate']
188
208
  end
189
209
  @log.info("The list of remotes trimmed, #{@remotes.all.count} nodes left there")
190
210
  end
@@ -231,6 +251,12 @@ in #{(Time.now - start).round(2)}s")
231
251
  end
232
252
  end
233
253
 
254
+ # @todo #292:30min Implement the logic of selecting the nodes as per #292.
255
+ # The strongest n nodes should be selected, where n = opts['max-nodes'].
256
+ def select(_opts)
257
+ raise NotImplementedError, 'This feature is not yet implemented.'
258
+ end
259
+
234
260
  def terminate
235
261
  @log.info("All threads before exit: #{Thread.list.map { |t| "#{t.name}/#{t.status}" }.join(', ')}")
236
262
  require_relative '../node/front'
@@ -30,12 +30,24 @@ require_relative '../id'
30
30
  require_relative '../tax'
31
31
  require_relative '../http'
32
32
 
33
- # TAXES command.
33
+ # Zold module.
34
34
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
35
35
  # Copyright:: Copyright (c) 2018 Yegor Bugayenko
36
36
  # License:: MIT
37
37
  module Zold
38
- # Taxes command
38
+ # Taxes command.
39
+ #
40
+ # The user pays taxes for his/her wallet by running 'zold taxes pay'. As
41
+ # the White Paper explains (find it at http://papers.zold.io), each wallet
42
+ # has to pay certain amount of taxes in order to be accepted by any node
43
+ # in the network. Of course, a node may make a decision to accept and
44
+ # store any wallet, even if taxes are not paid, but the majority of
45
+ # nodes will obey the rules and will reject wallets that haven't paid
46
+ # enough taxes.
47
+ #
48
+ # Taxes are paid from wallet to wallet, not from clients to nodes. A wallet
49
+ # just selects the most suitable wallet to transfer taxes to and sends
50
+ # the payment. More details you can find in the White Paper.
39
51
  class Taxes
40
52
  def initialize(wallets:, remotes:, log: Log::Quiet.new)
41
53
  @wallets = wallets
data/lib/zold/copies.rb CHANGED
@@ -22,6 +22,7 @@ require 'time'
22
22
  require 'csv'
23
23
  require_relative 'atomic_file'
24
24
  require_relative 'log'
25
+ require_relative 'wallet'
25
26
 
26
27
  # The list of copies.
27
28
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -52,8 +53,8 @@ module Zold
52
53
  list.reject! { |s| s[:time] < Time.now - 24 * 60 * 60 }
53
54
  save(list)
54
55
  deleted = 0
55
- Dir.new(@dir).select { |f| f =~ /^[0-9]+$/ }.each do |f|
56
- next unless list.find { |s| s[:name] == f }.nil?
56
+ Dir.new(@dir).select { |f| File.basename(f, Wallet::EXTENSION) =~ /^[0-9]+$/ }.each do |f|
57
+ next unless list.find { |s| s[:name] == File.basename(f, Wallet::EXTENSION) }.nil?
57
58
  file = File.join(@dir, f)
58
59
  size = File.size(file)
59
60
  File.delete(file)
@@ -83,17 +84,17 @@ module Zold
83
84
  FileUtils.mkdir_p(@dir)
84
85
  list = load
85
86
  target = list.find do |s|
86
- f = File.join(@dir, s[:name])
87
+ f = File.join(@dir, "#{s[:name]}#{Wallet::EXTENSION}")
87
88
  File.exist?(f) && AtomicFile.new(f).read == content
88
89
  end
89
90
  if target.nil?
90
91
  max = Dir.new(@dir)
91
- .select { |f| f =~ /^[0-9]+$/ }
92
+ .select { |f| File.basename(f, Wallet::EXTENSION) =~ /^[0-9]+$/ }
92
93
  .map(&:to_i)
93
94
  .max
94
95
  max = 0 if max.nil?
95
96
  name = (max + 1).to_s
96
- AtomicFile.new(File.join(@dir, name)).write(content)
97
+ AtomicFile.new(File.join(@dir, "#{name}#{Wallet::EXTENSION}")).write(content)
97
98
  else
98
99
  name = target[:name]
99
100
  end
@@ -115,12 +116,12 @@ module Zold
115
116
  load.group_by { |s| s[:name] }.map do |name, scores|
116
117
  {
117
118
  name: name,
118
- path: File.join(@dir, name),
119
+ path: File.join(@dir, "#{name}#{Wallet::EXTENSION}"),
119
120
  score: scores.select { |s| s[:time] > Time.now - 24 * 60 * 60 }
120
121
  .map { |s| s[:score] }
121
122
  .inject(&:+) || 0
122
123
  }
123
- end.select { |c| File.exist?(c[:path]) }
124
+ end.select { |c| File.exist?(c[:path]) }.sort_by { |c| c[:score] }.reverse
124
125
  end
125
126
  end
126
127
 
@@ -153,7 +154,7 @@ module Zold
153
154
  end
154
155
 
155
156
  def file
156
- File.join(@dir, 'scores')
157
+ File.join(@dir, "scores#{Wallet::EXTENSION}")
157
158
  end
158
159
  end
159
160
  end
@@ -0,0 +1,12 @@
1
+ require 'delegate'
2
+
3
+ module Zold
4
+ # Wallets decorator that adds missing wallets to the queue to be pulled later.
5
+ class HungryWallets < SimpleDelegator
6
+ # @todo #280:30min Add to the queue. Once in there, try
7
+ # to pull it as soon as possible as is described in #280.
8
+ def find(id)
9
+ super(id)
10
+ end
11
+ end
12
+ end
data/lib/zold/log.rb CHANGED
@@ -22,12 +22,27 @@ require 'rainbow'
22
22
 
23
23
  STDOUT.sync = true
24
24
 
25
- # The log.
25
+ # Zold module.
26
26
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
27
27
  # Copyright:: Copyright (c) 2018 Yegor Bugayenko
28
28
  # License:: MIT
29
29
  module Zold
30
- # Logging
30
+ # Logging facilities.
31
+ #
32
+ # There are a few logging classes, which can be used depending on what
33
+ # you want a user to see. There are three logging levels: INFO, ERROR,
34
+ # and DEBUG. In "quiet" mode the user won't see anything. This logging
35
+ # mode is used only for testing, when we don't want to see absolutely
36
+ # anything in the console. In order to turn off logging entirely, see
37
+ # how we configure it in test__helper.rb
38
+ #
39
+ # The default "regular" logging mode is what a user gets when he/she runs
40
+ # the gem in commmand line without any specific flags. In that case,
41
+ # the user will see only INFO and ERROR messages.
42
+ #
43
+ # In a "verbose" mode the user will see everything, including DEBUG
44
+ # messages. The user turns this mode by using --verbose command line argument.
45
+ #
31
46
  module Log
32
47
  # Extra verbose log
33
48
  class Verbose
@@ -20,6 +20,7 @@
20
20
 
21
21
  require 'concurrent'
22
22
  require_relative '../log'
23
+ require_relative '../id'
23
24
  require_relative '../verbose_thread'
24
25
 
25
26
  # The async entrance of the web front.
@@ -29,20 +30,36 @@ require_relative '../verbose_thread'
29
30
  module Zold
30
31
  # The entrance
31
32
  class AsyncEntrance
32
- def initialize(entrance, log: Log::Quiet.new)
33
+ THREADS = Concurrent.processor_count * 4
34
+
35
+ def initialize(entrance, dir, log: Log::Quiet.new)
33
36
  raise 'Entrance can\'t be nil' if entrance.nil?
34
37
  @entrance = entrance
38
+ raise 'Directory can\'t be nil' if dir.nil?
39
+ raise 'Directory must be of type String' unless dir.is_a?(String)
40
+ @dir = dir
35
41
  raise 'Log can\'t be nil' if log.nil?
36
42
  @log = log
43
+ @mutex = Mutex.new
37
44
  end
38
45
 
39
46
  def start
40
47
  @entrance.start do
48
+ FileUtils.mkdir_p(@dir)
41
49
  @pool = Concurrent::FixedThreadPool.new(
42
- Concurrent.processor_count * 8,
43
- max_queue: Concurrent.processor_count * 32,
44
- fallback_policy: :abort
50
+ AsyncEntrance::THREADS, max_queue: AsyncEntrance::THREADS, fallback_policy: :abort
45
51
  )
52
+ AsyncEntrance::THREADS.times do
53
+ @pool.post do
54
+ VerboseThread.new(@log).run(true) do
55
+ loop do
56
+ take
57
+ break if @pool.shuttingdown?
58
+ sleep Random.rand(100) / 100
59
+ end
60
+ end
61
+ end
62
+ end
46
63
  begin
47
64
  yield(self)
48
65
  ensure
@@ -69,13 +86,35 @@ module Zold
69
86
  end
70
87
 
71
88
  def push(id, body)
72
- @pool.post do
73
- VerboseThread.new(@log).run(true) do
74
- @entrance.push(id, body)
89
+ @mutex.synchronize do
90
+ AtomicFile.new(File.join(@dir, id.to_s)).write(body)
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def take
97
+ id = ''
98
+ body = ''
99
+ @mutex.synchronize do
100
+ opts = queue
101
+ unless opts.empty?
102
+ file = File.join(@dir, opts[0])
103
+ id = opts[0]
104
+ body = File.read(file)
105
+ File.delete(file)
75
106
  end
76
107
  end
77
- @log.debug("Pushed #{id}/#{body.length}b to #{@entrance.class.name}, \
78
- pool: #{@pool.length}/#{@pool.queue_length}")
108
+ return if id.empty? || body.empty?
109
+ start = Time.now
110
+ @entrance.push(Id.new(id), body)
111
+ @log.debug("Pushed #{id}/#{body.length}b to #{@entrance.class.name} in #{(Time.now - start).round}s")
112
+ end
113
+
114
+ def queue
115
+ Dir.new(@dir)
116
+ .select { |f| f =~ /^[0-9a-f]{16}$/ }
117
+ .sort_by { |f| File.mtime(File.join(@dir, f)) }
79
118
  end
80
119
  end
81
120
  end
@@ -37,7 +37,7 @@ module Zold
37
37
  class Entrance
38
38
  def initialize(wallets, remotes, copies, address, log: Log::Quiet.new)
39
39
  raise 'Wallets can\'t be nil' if wallets.nil?
40
- raise 'Wallets must be of type Wallets' unless wallets.is_a?(Wallets)
40
+ raise 'Wallets must implement the contract of Wallets: method #find is required' unless wallets.respond_to?(:find)
41
41
  @wallets = wallets
42
42
  raise 'Remotes can\'t be nil' if remotes.nil?
43
43
  raise "Remotes must be of type Remotes: #{remotes.class.name}" unless remotes.is_a?(Remotes)
@@ -49,6 +49,7 @@ module Zold
49
49
  set :show_exceptions, false
50
50
  set :server, 'webrick'
51
51
  set :version, VERSION # to be injected at node.rb
52
+ set :protocol, PROTOCOL # to be injected at node.rb
52
53
  set :ignore_score_weakness, false # to be injected at node.rb
53
54
  set :reboot, false # to be injected at node.rb
54
55
  set :home, nil? # to be injected at node.rb
@@ -89,10 +90,13 @@ module Zold
89
90
  end
90
91
  end
91
92
 
93
+ # @todo #357:30min Test that the headers are being set correctly.
94
+ # Currently there are no tests at all that would verify the headers.
92
95
  after do
93
96
  headers['Cache-Control'] = 'no-cache'
94
97
  headers['Connection'] = 'close'
95
98
  headers['X-Zold-Version'] = settings.version
99
+ headers['X-Zold-Protocol'] = settings.protocol.to_s
96
100
  headers['Access-Control-Allow-Origin'] = '*'
97
101
  headers[Http::SCORE_HEADER] = score.reduced(16).to_s
98
102
  end
@@ -150,6 +154,8 @@ module Zold
150
154
  {
151
155
  version: settings.version,
152
156
  score: score.to_h,
157
+ wallets: settings.wallets.all.count,
158
+ mtime: wallet.mtime.utc.iso8601,
153
159
  body: AtomicFile.new(wallet.path).read
154
160
  }.to_json
155
161
  end
@@ -170,6 +176,22 @@ module Zold
170
176
  wallet.key.to_s
171
177
  end
172
178
 
179
+ get %r{/wallet/(?<id>[A-Fa-f0-9]{16})/mtime} do
180
+ id = Id.new(params[:id])
181
+ wallet = settings.wallets.find(id)
182
+ error 404 unless wallet.exists?
183
+ content_type 'text/plain'
184
+ wallet.mtime.utc.iso8601.to_s
185
+ end
186
+
187
+ get %r{/wallet/(?<id>[A-Fa-f0-9]{16})/digest} do
188
+ id = Id.new(params[:id])
189
+ wallet = settings.wallets.find(id)
190
+ error 404 unless wallet.exists?
191
+ content_type 'text/plain'
192
+ wallet.digest
193
+ end
194
+
173
195
  get %r{/wallet/(?<id>[A-Fa-f0-9]{16})\.txt} do
174
196
  id = Id.new(params[:id])
175
197
  wallet = settings.wallets.find(id)
@@ -177,7 +199,7 @@ module Zold
177
199
  content_type 'text/plain'
178
200
  [
179
201
  wallet.network,
180
- wallet.version,
202
+ wallet.protocol,
181
203
  wallet.id.to_s,
182
204
  wallet.key.to_s,
183
205
  '',
@@ -185,7 +207,9 @@ module Zold
185
207
  '',
186
208
  '--',
187
209
  "Balance: #{wallet.balance.to_zld}",
188
- "Transactions: #{wallet.txns.count}"
210
+ "Transactions: #{wallet.txns.count}",
211
+ "Modified: #{wallet.mtime.utc.iso8601}",
212
+ "Digest: #{wallet.digest}"
189
213
  ].join("\n")
190
214
  end
191
215
 
@@ -209,7 +233,8 @@ module Zold
209
233
  settings.entrance.push(id, after)
210
234
  JSON.pretty_generate(
211
235
  version: settings.version,
212
- score: score.to_h
236
+ score: score.to_h,
237
+ wallets: settings.wallets.all.count
213
238
  )
214
239
  end
215
240
 
@@ -56,9 +56,9 @@ module Zold
56
56
  raise 'Id can\'t be nil' if id.nil?
57
57
  raise 'Id must be of type Id' unless id.is_a?(Id)
58
58
  raise 'Body can\'t be nil' if body.nil?
59
- Tempfile.open do |f|
60
- File.write(f.path, body)
61
- wallet = Wallet.new(f)
59
+ Tempfile.open(['', Wallet::EXTENSION]) do |f|
60
+ File.write(f, body)
61
+ wallet = Wallet.new(f.path)
62
62
  unless wallet.network == @network
63
63
  raise "The network name mismatch, the wallet is in '#{wallet.network}', we are in '#{@network}'"
64
64
  end
@@ -41,7 +41,7 @@ module Zold
41
41
  raise 'Entrance can\'t be nil' if entrance.nil?
42
42
  @entrance = entrance
43
43
  raise 'Wallets can\'t be nil' if wallets.nil?
44
- raise 'Wallets must be of type Wallets' unless wallets.is_a?(Wallets)
44
+ raise 'Wallets must implement the contract of Wallets: method #find is required' unless wallets.respond_to?(:find)
45
45
  @wallets = wallets
46
46
  raise 'Remotes can\'t be nil' if remotes.nil?
47
47
  raise 'Remotes must be of type Remotes' unless remotes.is_a?(Remotes)
data/lib/zold/patch.rb CHANGED
@@ -33,14 +33,14 @@ module Zold
33
33
  class Patch
34
34
  def initialize(wallets, log: Log::Quiet.new)
35
35
  raise 'Wallets can\'t be nil' if wallets.nil?
36
- raise 'Wallets must be of type Wallets' unless wallets.is_a?(Wallets)
36
+ raise 'Wallets must implement the contract of Wallets: method #find is required' unless wallets.respond_to?(:find)
37
37
  @wallets = wallets
38
38
  @txns = []
39
39
  @log = log
40
40
  end
41
41
 
42
42
  def to_s
43
- return 'empty patch' if @txns.empty?
43
+ return 'nothing' if @txns.empty?
44
44
  "#{@txns.count} txns"
45
45
  end
46
46
 
@@ -50,7 +50,7 @@ module Zold
50
50
  @key = wallet.key
51
51
  if baseline
52
52
  @txns = wallet.txns
53
- @log.debug("The baseline: #{@txns.count} transactions, the balance is #{wallet.balance}")
53
+ @log.debug("The baseline of #{wallet.id} is #{wallet.balance}/#{@txns.count}t")
54
54
  else
55
55
  @log.debug("The baseline of #{@txns.count} transactions ignored")
56
56
  end
@@ -84,9 +84,13 @@ module Zold
84
84
  @log.error("RSA signature is redundant at ##{txn.id} of #{wallet.id}: #{txn.to_text}")
85
85
  next
86
86
  end
87
+ unless wallet.key.to_s.include?(txn.prefix)
88
+ @log.error("Payment prefix doesn't match with the key of #{wallet.id}: #{txn.to_text}")
89
+ next
90
+ end
87
91
  payer = @wallets.find(txn.bnf)
88
92
  unless payer.exists?
89
- @log.error("Paying wallet #{wallet.id} is absent at ##{txn.id}: #{txn.to_text}")
93
+ @log.error("Paying wallet file is absent: #{txn.to_text}")
90
94
  next
91
95
  end
92
96
  unless payer.has?(txn.id, wallet.id)
data/lib/zold/remotes.rb CHANGED
@@ -39,6 +39,12 @@ module Zold
39
39
  # At what amount of errors we delete the remote automatically
40
40
  TOLERANCE = 8
41
41
 
42
+ # After this limit, the remote runtime must be recorded
43
+ RUNTIME_LIMIT = 16
44
+
45
+ # Default number of nodes to fetch.
46
+ MAX_NODES = 16
47
+
42
48
  # Empty, for standalone mode
43
49
  class Empty < Remotes
44
50
  def initialize
@@ -180,6 +186,7 @@ module Zold
180
186
  begin
181
187
  yield Remotes::Remote.new(r[:host], r[:port], score, idx, log: log, network: @network)
182
188
  idx += 1
189
+ raise 'Took too long to execute' if (Time.now - start).round > Remotes::RUNTIME_LIMIT
183
190
  rescue StandardError => e
184
191
  error(r[:host], r[:port])
185
192
  errors = errors(r[:host], r[:port])
data/lib/zold/score.rb CHANGED
@@ -18,7 +18,7 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
19
  # SOFTWARE.
20
20
 
21
- require 'digest'
21
+ require 'openssl'
22
22
  require 'time'
23
23
  require_relative 'remotes'
24
24
 
@@ -101,7 +101,7 @@ module Zold
101
101
  def hash
102
102
  raise 'Score has zero value, there is no hash' if @suffixes.empty?
103
103
  @suffixes.reduce(prefix) do |pfx, suffix|
104
- Digest::SHA256.hexdigest(pfx + ' ' + suffix)[0, 64]
104
+ OpenSSL::Digest::SHA256.new("#{pfx} #{suffix}").hexdigest
105
105
  end
106
106
  end
107
107
 
data/lib/zold/tax.rb CHANGED
@@ -18,7 +18,6 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
19
  # SOFTWARE.
20
20
 
21
- require 'digest'
22
21
  require_relative 'key'
23
22
  require_relative 'id'
24
23
  require_relative 'amount'
@@ -0,0 +1,57 @@
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 'log'
22
+
23
+ module Zold
24
+ # Class to manage data upgrades (when zold itself upgrades).
25
+ class Upgrades
26
+ def initialize(version, directory, log: Log::Verbose.new)
27
+ @version = version
28
+ @directory = directory
29
+ @log = log
30
+ end
31
+
32
+ # @todo #285:30min Write the upgrade manager tests that ensure:
33
+ # - Nothing breaks without the version file.
34
+ # - The upgrade scripts run when there is a version file and there are pending upgrade scripts.
35
+ # - Make sure *only* the correct upgrade scripts run.
36
+ def run
37
+ # This is a workaround, remove it once this class works correctly
38
+ require_relative '../../upgrades/2.rb'
39
+ UpgradeTo2.new(Dir.pwd, @log).exec
40
+
41
+ scripts.each do |script|
42
+ @version.apply(script)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def scripts
49
+ Dir.glob("#{@directory}/*.rb").sort.map do |path|
50
+ basename = File.basename(path)
51
+ match = basename.match(/^(\d+\.\d+\.\d+)\.rb$/)
52
+ raise 'An upgrade script has to be named <version>.rb.' unless match
53
+ match[1]
54
+ end
55
+ end
56
+ end
57
+ end
data/lib/zold/version.rb CHANGED
@@ -23,5 +23,6 @@
23
23
  # Copyright:: Copyright (c) 2018 Yegor Bugayenko
24
24
  # License:: MIT
25
25
  module Zold
26
- VERSION = '0.13.46'.freeze
26
+ VERSION = '0.14.0'.freeze
27
+ PROTOCOL = 2
27
28
  end
@@ -0,0 +1,39 @@
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 'semantic'
22
+ require_relative 'log'
23
+
24
+ module Zold
25
+ # Read and write .zoldata/version.
26
+ class VersionFile
27
+ def initialize(path, log: Log::Verbose.new)
28
+ @path = path
29
+ @log = log
30
+ end
31
+
32
+ # @todo #285:30min Replace this stub with functionality.
33
+ # We need to run the script (`yield`) if the version of
34
+ # the script is between the saved version and the current one.
35
+ def apply(version)
36
+ @log.info("Version #{version} doesn't need to be applied.")
37
+ end
38
+ end
39
+ end
data/lib/zold/wallet.rb CHANGED
@@ -19,6 +19,7 @@
19
19
  # SOFTWARE.
20
20
 
21
21
  require 'time'
22
+ require 'openssl'
22
23
  require_relative 'version'
23
24
  require_relative 'key'
24
25
  require_relative 'id'
@@ -44,8 +45,12 @@ module Zold
44
45
  # must have different names.
45
46
  MAIN_NETWORK = 'zold'.freeze
46
47
 
48
+ # The extension of the wallet files
49
+ EXTENSION = '.z'.freeze
50
+
47
51
  def initialize(file)
48
52
  @file = file
53
+ @file = "#{file}#{EXTENSION}" if File.extname(file).empty?
49
54
  end
50
55
 
51
56
  def ==(other)
@@ -62,10 +67,10 @@ module Zold
62
67
  n
63
68
  end
64
69
 
65
- def version
70
+ def protocol
66
71
  v = lines[1].strip
67
- raise "Invalid version name '#{v}'" unless v =~ /^[0-9]+$/
68
- v
72
+ raise "Invalid protocol version name '#{v}'" unless v =~ /^[0-9]+$/
73
+ v.to_i
69
74
  end
70
75
 
71
76
  def exists?
@@ -79,7 +84,7 @@ module Zold
79
84
  def init(id, pubkey, overwrite: false, network: 'test')
80
85
  raise "File '#{@file}' already exists" if File.exist?(@file) && !overwrite
81
86
  raise "Invalid network name '#{network}'" unless network =~ /^[a-z]{4,16}$/
82
- AtomicFile.new(@file).write("#{network}\n1\n#{id}\n#{pubkey.to_pub}\n\n")
87
+ AtomicFile.new(@file).write("#{network}\n#{PROTOCOL}\n#{id}\n#{pubkey.to_pub}\n\n")
83
88
  end
84
89
 
85
90
  def root?
@@ -139,6 +144,14 @@ module Zold
139
144
  end
140
145
  end
141
146
 
147
+ def mtime
148
+ File.mtime(@file)
149
+ end
150
+
151
+ def digest
152
+ OpenSSL::Digest::SHA256.new(File.read(@file)).hexdigest
153
+ end
154
+
142
155
  # Age of wallet in hours
143
156
  def age
144
157
  list = txns