zold 0.13.46 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/.rultor.yml +2 -2
- data/.simplecov +1 -1
- data/.travis.yml +3 -1
- data/INSTALL.md +4 -1
- data/README.md +98 -3
- data/Rakefile +1 -0
- data/bin/zold +10 -1
- data/fixtures/merge/into-no-wallet/assert.rb +23 -0
- data/fixtures/merge/{random-expenses/0123456789abcdef → into-no-wallet/copies/0123456789abcdef/1.z} +0 -0
- data/fixtures/merge/into-no-wallet/copies/0123456789abcdef/scores.z +1 -0
- data/fixtures/merge/random-expenses/{0000000000000000 → 0000000000000000.z} +0 -0
- data/fixtures/merge/{simple-case/copies/0123456789abcdef/1 → random-expenses/0123456789abcdef.z} +0 -0
- data/fixtures/merge/random-expenses/copies/0123456789abcdef/{1 → 1.z} +0 -0
- data/fixtures/merge/random-expenses/copies/0123456789abcdef/{2 → 2.z} +0 -0
- data/fixtures/merge/random-expenses/copies/0123456789abcdef/{3 → 3.z} +0 -0
- data/fixtures/merge/random-expenses/copies/0123456789abcdef/{4 → 4.z} +0 -0
- data/fixtures/merge/random-expenses/copies/0123456789abcdef/{5 → 5.z} +0 -0
- data/fixtures/merge/random-expenses/copies/0123456789abcdef/scores.z +5 -0
- data/fixtures/merge/simple-case/{0000000000000000 → 0000000000000000.z} +0 -0
- data/fixtures/merge/simple-case/{0123456789abcdef → 0123456789abcdef.z} +0 -1
- data/fixtures/merge/simple-case/copies/0123456789abcdef/1.z +6 -0
- data/fixtures/merge/simple-case/copies/0123456789abcdef/scores.z +1 -0
- data/fixtures/scripts/push-and-pull.sh +1 -0
- data/lib/zold/atomic_file.rb +7 -6
- data/lib/zold/commands/alias.rb +37 -0
- data/lib/zold/commands/calculate.rb +5 -2
- data/lib/zold/commands/diff.rb +2 -2
- data/lib/zold/commands/fetch.rb +23 -3
- data/lib/zold/commands/merge.rb +12 -7
- data/lib/zold/commands/node.rb +5 -2
- data/lib/zold/commands/pay.rb +11 -0
- data/lib/zold/commands/push.rb +1 -1
- data/lib/zold/commands/remote.rb +28 -2
- data/lib/zold/commands/taxes.rb +14 -2
- data/lib/zold/copies.rb +9 -8
- data/lib/zold/hungry_wallets.rb +12 -0
- data/lib/zold/log.rb +17 -2
- data/lib/zold/node/async_entrance.rb +48 -9
- data/lib/zold/node/entrance.rb +1 -1
- data/lib/zold/node/front.rb +28 -3
- data/lib/zold/node/safe_entrance.rb +3 -3
- data/lib/zold/node/spread_entrance.rb +1 -1
- data/lib/zold/patch.rb +8 -4
- data/lib/zold/remotes.rb +7 -0
- data/lib/zold/score.rb +2 -2
- data/lib/zold/tax.rb +0 -1
- data/lib/zold/upgrades.rb +57 -0
- data/lib/zold/version.rb +2 -1
- data/lib/zold/version_file.rb +39 -0
- data/lib/zold/wallet.rb +17 -4
- data/lib/zold/wallets.rb +9 -6
- data/test/commands/routines/test_spread.rb +1 -0
- data/test/commands/test_alias.rb +7 -0
- data/test/commands/test_create.rb +2 -2
- data/test/commands/test_fetch.rb +2 -1
- data/test/commands/test_merge.rb +2 -0
- data/test/commands/test_node.rb +1 -0
- data/test/commands/test_pay.rb +24 -0
- data/test/commands/test_remote.rb +28 -0
- data/test/node/test_async_entrance.rb +6 -13
- data/test/node/test_front.rb +2 -1
- data/test/test__helper.rb +3 -2
- data/test/test_atomic_file.rb +0 -5
- data/test/test_copies.rb +14 -2
- data/test/test_remotes.rb +12 -0
- data/test/test_score.rb +14 -0
- data/test/test_tax.rb +5 -0
- data/test/test_upgrades.rb +97 -0
- data/test/test_version.rb +13 -0
- data/test/test_wallet.rb +21 -1
- data/test/test_wallets.rb +8 -0
- data/upgrades/2.rb +20 -0
- data/zold.gemspec +3 -3
- metadata +38 -23
- data/fixtures/merge/random-expenses/copies/0123456789abcdef/scores +0 -5
- data/fixtures/merge/simple-case/copies/0123456789abcdef/scores +0 -1
data/lib/zold/commands/remote.rb
CHANGED
@@ -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
|
-
|
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] >
|
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'
|
data/lib/zold/commands/taxes.rb
CHANGED
@@ -30,12 +30,24 @@ require_relative '../id'
|
|
30
30
|
require_relative '../tax'
|
31
31
|
require_relative '../http'
|
32
32
|
|
33
|
-
#
|
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,
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
@
|
73
|
-
|
74
|
-
|
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
|
-
|
78
|
-
|
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
|
data/lib/zold/node/entrance.rb
CHANGED
@@ -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
|
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)
|
data/lib/zold/node/front.rb
CHANGED
@@ -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.
|
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
|
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
|
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
|
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 '
|
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
|
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
|
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 '
|
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.
|
104
|
+
OpenSSL::Digest::SHA256.new("#{pfx} #{suffix}").hexdigest
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
data/lib/zold/tax.rb
CHANGED
@@ -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
@@ -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
|
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}\
|
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
|