zold 0.13.46 → 0.14.0

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