reth 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +6 -0
- data/bin/reth +208 -0
- data/lib/reth.rb +44 -0
- data/lib/reth/account.rb +195 -0
- data/lib/reth/account_service.rb +361 -0
- data/lib/reth/app.rb +15 -0
- data/lib/reth/chain_service.rb +500 -0
- data/lib/reth/config.rb +88 -0
- data/lib/reth/db_service.rb +90 -0
- data/lib/reth/duplicates_filter.rb +29 -0
- data/lib/reth/eth_protocol.rb +209 -0
- data/lib/reth/genesisdata/genesis_frontier.json +26691 -0
- data/lib/reth/genesisdata/genesis_morden.json +27 -0
- data/lib/reth/genesisdata/genesis_olympic.json +48 -0
- data/lib/reth/jsonrpc.rb +13 -0
- data/lib/reth/jsonrpc/app.rb +79 -0
- data/lib/reth/jsonrpc/filter.rb +288 -0
- data/lib/reth/jsonrpc/handler.rb +424 -0
- data/lib/reth/jsonrpc/helper.rb +156 -0
- data/lib/reth/jsonrpc/server.rb +24 -0
- data/lib/reth/jsonrpc/service.rb +37 -0
- data/lib/reth/keystore.rb +150 -0
- data/lib/reth/leveldb_service.rb +79 -0
- data/lib/reth/profile.rb +66 -0
- data/lib/reth/sync_task.rb +273 -0
- data/lib/reth/synchronizer.rb +192 -0
- data/lib/reth/transient_block.rb +40 -0
- data/lib/reth/utils.rb +22 -0
- data/lib/reth/version.rb +3 -0
- metadata +201 -0
@@ -0,0 +1,361 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Reth
|
6
|
+
|
7
|
+
class AccountService < ::DEVp2p::Service
|
8
|
+
|
9
|
+
name 'accounts'
|
10
|
+
|
11
|
+
default_config(
|
12
|
+
accounts: {
|
13
|
+
keystore_dir: 'keystore',
|
14
|
+
must_include_coinbase: true
|
15
|
+
}
|
16
|
+
)
|
17
|
+
|
18
|
+
attr :accounts
|
19
|
+
|
20
|
+
DEFAULT_COINBASE = Utils.decode_hex('de0b295669a9fd93d5f28d9ec85e40f4cb697bae')
|
21
|
+
|
22
|
+
def initialize(app)
|
23
|
+
super(app)
|
24
|
+
|
25
|
+
if app.config[:accounts][:keystore_dir][0] == '/'
|
26
|
+
@keystore_dir = app.config[:accounts][:keystore_dir]
|
27
|
+
else # relative path
|
28
|
+
@keystore_dir = File.join app.config[:data_dir], app.config[:accounts][:keystore_dir]
|
29
|
+
end
|
30
|
+
|
31
|
+
@accounts = []
|
32
|
+
|
33
|
+
if !File.exist?(@keystore_dir)
|
34
|
+
logger.warn "keystore directory does not exist", directory: @keystore_dir
|
35
|
+
elsif !File.directory?(@keystore_dir)
|
36
|
+
logger.error "configured keystore directory is a file, not a directory", directory: @keystore_dir
|
37
|
+
else
|
38
|
+
logger.info "searching for key files", directory: @keystore_dir
|
39
|
+
|
40
|
+
ignore = %w(. ..)
|
41
|
+
Dir.foreach(@keystore_dir) do |filename|
|
42
|
+
next if ignore.include?(filename)
|
43
|
+
|
44
|
+
begin
|
45
|
+
@accounts.push Account.load(File.join(@keystore_dir, filename))
|
46
|
+
rescue ValueError
|
47
|
+
logger.warn "invalid file skipped in keystore directory", path: filename
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
@accounts.sort_by! {|acct| acct.path.to_s }
|
52
|
+
|
53
|
+
if @accounts.empty?
|
54
|
+
logger.warn "no accounts found"
|
55
|
+
else
|
56
|
+
logger.info "found account(s)", accounts: @accounts
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def start
|
61
|
+
# do nothing
|
62
|
+
end
|
63
|
+
|
64
|
+
def stop
|
65
|
+
# do nothing
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Return the address that should be used as coinbase for new blocks.
|
70
|
+
#
|
71
|
+
# The coinbase address is given by the config field pow.coinbase_hex. If
|
72
|
+
# this does not exist, the address of the first account is used instead.
|
73
|
+
# If there are no accounts, the coinbase is `DEFAULT_COINBASE`.
|
74
|
+
#
|
75
|
+
# @raise [ValueError] if the coinbase is invalid (no string, wrong
|
76
|
+
# length) or there is no account for it and the config flag
|
77
|
+
# `accounts.check_coinbase` is set (does not apply to the default
|
78
|
+
# coinbase).
|
79
|
+
#
|
80
|
+
def coinbase
|
81
|
+
cb_hex = (app.config[:pow] || {})[:coinbase_hex]
|
82
|
+
if cb_hex
|
83
|
+
raise ValueError, 'coinbase must be String' unless cb_hex.is_a?(String)
|
84
|
+
begin
|
85
|
+
cb = Utils.decode_hex Utils.remove_0x_head(cb_hex)
|
86
|
+
rescue TypeError
|
87
|
+
raise ValueError, 'invalid coinbase'
|
88
|
+
end
|
89
|
+
else
|
90
|
+
accts = accounts_with_address
|
91
|
+
return DEFAULT_COINBASE if accts.empty?
|
92
|
+
cb = accts[0].address
|
93
|
+
end
|
94
|
+
|
95
|
+
raise ValueError, 'wrong coinbase length' if cb.size != 20
|
96
|
+
|
97
|
+
if config[:accounts][:must_include_coinbase]
|
98
|
+
raise ValueError, 'no account for coinbase' if !@accounts.map(&:address).include?(cb)
|
99
|
+
end
|
100
|
+
|
101
|
+
cb
|
102
|
+
end
|
103
|
+
|
104
|
+
##
|
105
|
+
# Add an account.
|
106
|
+
#
|
107
|
+
# If `store` is true the account will be stored as a key file at the
|
108
|
+
# location given by `account.path`. If this is `nil` a `ValueError` is
|
109
|
+
# raised. `include_address` and `include_id` determine if address and id
|
110
|
+
# should be removed for storage or not.
|
111
|
+
#
|
112
|
+
# This method will raise a `ValueError` if the new account has the same
|
113
|
+
# UUID as an account already known to the service. Note that address
|
114
|
+
# collisions do not result in an exception as those may slip through
|
115
|
+
# anyway for locked accounts with hidden addresses.
|
116
|
+
#
|
117
|
+
def add_account(account, store=true, include_address=true, include_id=true)
|
118
|
+
logger.info "adding account", account: account
|
119
|
+
|
120
|
+
if account.uuid && @accounts.any? {|acct| acct.uuid == account.uuid }
|
121
|
+
logger.error 'could not add account (UUID collision)', uuid: account.uuid
|
122
|
+
raise ValueError, 'Could not add account (UUID collision)'
|
123
|
+
end
|
124
|
+
|
125
|
+
if store
|
126
|
+
raise ValueError, 'Cannot store account without path' if account.path.nil?
|
127
|
+
if File.exist?(account.path)
|
128
|
+
logger.error 'File does already exist', path: account.path
|
129
|
+
raise IOError, 'File does already exist'
|
130
|
+
end
|
131
|
+
|
132
|
+
raise AssertError if @accounts.any? {|acct| acct.path == account.path }
|
133
|
+
|
134
|
+
begin
|
135
|
+
directory = File.dirname account.path
|
136
|
+
FileUtils.mkdir_p(directory) unless File.exist?(directory)
|
137
|
+
|
138
|
+
File.open(account.path, 'w') do |f|
|
139
|
+
f.write account.dump(include_address, include_id)
|
140
|
+
end
|
141
|
+
rescue IOError => e
|
142
|
+
logger.error "Could not write to file", path: account.path, message: e.to_s
|
143
|
+
raise e
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
@accounts.push account
|
148
|
+
@accounts.sort_by! {|acct| acct.path.to_s }
|
149
|
+
end
|
150
|
+
|
151
|
+
##
|
152
|
+
# Replace the password of an account.
|
153
|
+
#
|
154
|
+
# The update is carried out in three steps:
|
155
|
+
#
|
156
|
+
# 1. the old keystore file is renamed
|
157
|
+
# 2. the new keystore file is created at the previous location of the old
|
158
|
+
# keystore file
|
159
|
+
# 3. the old keystore file is removed
|
160
|
+
#
|
161
|
+
# In this way, at least one of the keystore files exists on disk at any
|
162
|
+
# time and can be recovered if the process is interrupted.
|
163
|
+
#
|
164
|
+
# @param account [Account] which must be unlocked, stored on disk and
|
165
|
+
# included in `@accounts`
|
166
|
+
# @param include_address [Bool] forwarded to `add_account` during step 2
|
167
|
+
# @param include_id [Bool] forwarded to `add_account` during step 2
|
168
|
+
#
|
169
|
+
# @raise [ValueError] if the account is locked, if it is not added to the
|
170
|
+
# account manager, or if it is not stored
|
171
|
+
#
|
172
|
+
def update_account(account, new_password, include_address=true, include_id=true)
|
173
|
+
raise ValueError, "Account not managed by account service" unless @accounts.include?(account)
|
174
|
+
raise ValueError, "Cannot update locked account" if account.locked?
|
175
|
+
raise ValueError, 'Account not stored on disk' unless account.path
|
176
|
+
|
177
|
+
logger.debug "creating new account"
|
178
|
+
new_account = Account.create new_password, account.privkey, account.uuid, account.path
|
179
|
+
|
180
|
+
backup_path = account.path + '~'
|
181
|
+
i = 1
|
182
|
+
while File.exist?(backup_path)
|
183
|
+
backup_path = backup_path[0, backup_path.rindex('~')+1] + i.to_s
|
184
|
+
i += 1
|
185
|
+
end
|
186
|
+
raise AssertError if File.exist?(backup_path)
|
187
|
+
|
188
|
+
logger.info 'moving old keystore file to backup location', from: account.path, to: backup_path
|
189
|
+
begin
|
190
|
+
FileUtils.mv account.path, backup_path
|
191
|
+
rescue
|
192
|
+
logger.error "could not backup keystore, stopping account update", from: account.path, to: backup_path
|
193
|
+
raise $!
|
194
|
+
end
|
195
|
+
raise AssertError unless File.exist?(backup_path)
|
196
|
+
raise AssertError if File.exist?(new_account.path)
|
197
|
+
account.path = backup_path
|
198
|
+
|
199
|
+
@accounts.delete account
|
200
|
+
begin
|
201
|
+
add_account new_account, include_address, include_id
|
202
|
+
rescue
|
203
|
+
logger.error 'adding new account failed, recovering from backup'
|
204
|
+
FileUtils.mv backup_path, new_account.path
|
205
|
+
account.path = new_account.path
|
206
|
+
@accounts.push account
|
207
|
+
@accounts.sort_by! {|acct| acct.path.to_s }
|
208
|
+
raise $!
|
209
|
+
end
|
210
|
+
raise AssertError unless File.exist?(new_account.path)
|
211
|
+
|
212
|
+
logger.info "deleting backup of old keystore", path: backup_path
|
213
|
+
begin
|
214
|
+
FileUtils.rm backup_path
|
215
|
+
rescue
|
216
|
+
logger.error 'failed to delete no longer needed backup of old keystore', path: account.path
|
217
|
+
raise $!
|
218
|
+
end
|
219
|
+
|
220
|
+
account.keystore = new_account.keystore
|
221
|
+
account.path = new_account.path
|
222
|
+
|
223
|
+
@accounts.push account
|
224
|
+
@accounts.delete new_account
|
225
|
+
@accounts.sort_by! {|acct| acct.path.to_s }
|
226
|
+
|
227
|
+
logger.debug "account update successful"
|
228
|
+
end
|
229
|
+
|
230
|
+
def accounts_with_address
|
231
|
+
@accounts.select {|acct| acct.address }
|
232
|
+
end
|
233
|
+
|
234
|
+
def unlocked_accounts
|
235
|
+
@accounts.select {|acct| !acct.locked? }
|
236
|
+
end
|
237
|
+
|
238
|
+
##
|
239
|
+
# Find an account by either its address, its id, or its index as string.
|
240
|
+
#
|
241
|
+
# Example identifiers:
|
242
|
+
#
|
243
|
+
# - '9c0e0240776cfbe6fa1eb37e57721e1a88a563d1' (address)
|
244
|
+
# - '0x9c0e0240776cfbe6fa1eb37e57721e1a88a563d1' (address with 0x prefix)
|
245
|
+
# - '01dd527b-f4a5-4b3c-9abb-6a8e7cd6722f' (UUID)
|
246
|
+
# - '3' (index)
|
247
|
+
#
|
248
|
+
# @param identifier [String] the accounts hex encoded, case insensitive
|
249
|
+
# address (with optional 0x prefix), its UUID or its index (as string,
|
250
|
+
# >= 1)
|
251
|
+
#
|
252
|
+
# @raise [ValueError] if the identifier could not be interpreted
|
253
|
+
# @raise [KeyError] if the identified account is not known to the account
|
254
|
+
# service
|
255
|
+
#
|
256
|
+
def find(identifier)
|
257
|
+
identifier = identifier.downcase
|
258
|
+
|
259
|
+
if identifier =~ /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/ # uuid
|
260
|
+
return get_by_id(identifier)
|
261
|
+
end
|
262
|
+
|
263
|
+
begin
|
264
|
+
address = Address.new(identifier).to_bytes
|
265
|
+
raise AssertError unless address.size == 20
|
266
|
+
return self[address]
|
267
|
+
rescue
|
268
|
+
# do nothing
|
269
|
+
end
|
270
|
+
|
271
|
+
index = identifier.to_i
|
272
|
+
raise ValueError, 'Index must be 1 or greater' if index <= 0
|
273
|
+
raise KeyError if index > @accounts.size
|
274
|
+
@accounts[index-1]
|
275
|
+
end
|
276
|
+
|
277
|
+
##
|
278
|
+
# Return the account with a given id.
|
279
|
+
#
|
280
|
+
# Note that accounts are not required to have an id.
|
281
|
+
#
|
282
|
+
# @raise [KeyError] if no matching account can be found
|
283
|
+
#
|
284
|
+
def get_by_id(id)
|
285
|
+
accts = @accounts.select {|acct| acct.uuid == id }
|
286
|
+
|
287
|
+
if accts.size == 0
|
288
|
+
raise KeyError, "account with id #{id} unknown"
|
289
|
+
elsif accts.size > 1
|
290
|
+
logger.warn "multiple accounts with same UUID found", uuid: id
|
291
|
+
end
|
292
|
+
|
293
|
+
accts[0]
|
294
|
+
end
|
295
|
+
|
296
|
+
##
|
297
|
+
# Get an account by its address.
|
298
|
+
#
|
299
|
+
# Note that even if an account with the given address exists, it might
|
300
|
+
# not be found if it is locked. Also, multiple accounts with the same
|
301
|
+
# address may exist, in which case the first one is returned (and a
|
302
|
+
# warning is logged).
|
303
|
+
#
|
304
|
+
# @raise [KeyError] if no matching account can be found
|
305
|
+
#
|
306
|
+
def get_by_address(address)
|
307
|
+
raise ArgumentError, 'address must be 20 bytes' unless address.size == 20
|
308
|
+
|
309
|
+
accts = @accounts.select {|acct| acct.address == address }
|
310
|
+
|
311
|
+
if accts.size == 0
|
312
|
+
raise KeyError, "account not found by address #{Utils.encode_hex(address)}"
|
313
|
+
elsif accts.size > 1
|
314
|
+
logger.warn "multiple accounts with same address found", address: Utils.encode_hex(address)
|
315
|
+
end
|
316
|
+
|
317
|
+
accts[0]
|
318
|
+
end
|
319
|
+
|
320
|
+
def sign_tx(address, tx)
|
321
|
+
get_by_address(address).sign_tx(tx)
|
322
|
+
end
|
323
|
+
|
324
|
+
def propose_path(address)
|
325
|
+
File.join @keystore_dir, Utils.encode_hex(address)
|
326
|
+
end
|
327
|
+
|
328
|
+
def include?(address)
|
329
|
+
raise ArgumentError, 'address must be 20 bytes' unless address.size == 20
|
330
|
+
@accounts.any? {|acct| acct.address == address }
|
331
|
+
end
|
332
|
+
|
333
|
+
def [](address_or_idx)
|
334
|
+
if address_or_idx.instance_of?(String)
|
335
|
+
raise ArgumentError, 'address must be 20 bytes' unless address_or_idx.size == 20
|
336
|
+
acct = @accounts.find {|acct| acct.address == address_or_idx }
|
337
|
+
acct or raise KeyError
|
338
|
+
else
|
339
|
+
raise ArgumentError, 'address_or_idx must be String or Integer' unless address_or_idx.is_a?(Integer)
|
340
|
+
@accounts[address_or_idx]
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
include Enumerable
|
345
|
+
def each(&block)
|
346
|
+
@accounts.each(&block)
|
347
|
+
end
|
348
|
+
|
349
|
+
def size
|
350
|
+
@accounts.size
|
351
|
+
end
|
352
|
+
|
353
|
+
private
|
354
|
+
|
355
|
+
def logger
|
356
|
+
@logger ||= Logger.new('accounts')
|
357
|
+
end
|
358
|
+
|
359
|
+
end
|
360
|
+
|
361
|
+
end
|
data/lib/reth/app.rb
ADDED
@@ -0,0 +1,500 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
module Reth
|
6
|
+
|
7
|
+
class ChainService < ::DEVp2p::WiredService
|
8
|
+
name 'chain'
|
9
|
+
|
10
|
+
default_config(
|
11
|
+
eth: {
|
12
|
+
network_id: 0,
|
13
|
+
genesis: '',
|
14
|
+
pruning: -1
|
15
|
+
},
|
16
|
+
block: Env::DEFAULT_CONFIG
|
17
|
+
)
|
18
|
+
|
19
|
+
BLOCK_QUEUE_SIZE = 512
|
20
|
+
TRANSACTION_QUEUE_SIZE = 512
|
21
|
+
|
22
|
+
MAX_NEWBLOCK_PROCESSING_TIME_STATS = 1000
|
23
|
+
|
24
|
+
attr :chain, :block_queue, :transaction_queue, :synchronizer
|
25
|
+
|
26
|
+
def initialize(app)
|
27
|
+
setup_db(app)
|
28
|
+
super(app)
|
29
|
+
|
30
|
+
logger.info 'initializing chain'
|
31
|
+
coinbase = app.services.accounts.coinbase
|
32
|
+
env = Env.new @db, config: config[:eth][:block]
|
33
|
+
@chain = Chain.new env, new_head_cb: method(:on_new_head), coinbase: coinbase
|
34
|
+
|
35
|
+
logger.info 'chain at', number: @chain.head.number
|
36
|
+
if config[:eth][:genesis_hash]
|
37
|
+
raise AssertError, "Genesis hash mismatch. Expected: #{config[:eth][:genesis_hash]}, Got: #{@chain.genesis.full_hash_hex}" unless config[:eth][:genesis_hash] == @chain.genesis.full_hash_hex
|
38
|
+
end
|
39
|
+
|
40
|
+
@synchronizer = Synchronizer.new(self, nil)
|
41
|
+
|
42
|
+
@block_queue = SyncQueue.new BLOCK_QUEUE_SIZE
|
43
|
+
@transaction_queue = SyncQueue.new TRANSACTION_QUEUE_SIZE
|
44
|
+
@add_blocks_lock = false
|
45
|
+
@add_transaction_lock = Mutex.new # TODO: should be semaphore
|
46
|
+
|
47
|
+
@broadcast_filter = DuplicatesFilter.new
|
48
|
+
@on_new_head_cbs = []
|
49
|
+
@on_new_head_candidate_cbs = []
|
50
|
+
@newblock_processing_times = []
|
51
|
+
|
52
|
+
@processed_gas = 0
|
53
|
+
@processed_elapsed = 0
|
54
|
+
|
55
|
+
@wire_protocol = ETHProtocol
|
56
|
+
end
|
57
|
+
|
58
|
+
def start
|
59
|
+
# do nothing
|
60
|
+
end
|
61
|
+
|
62
|
+
def stop
|
63
|
+
# do nothing
|
64
|
+
end
|
65
|
+
|
66
|
+
def syncing?
|
67
|
+
@synchronizer.syncing?
|
68
|
+
end
|
69
|
+
|
70
|
+
def mining?
|
71
|
+
app.services.include?('pow') && app.services.pow.active?
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_transaction(tx, origin=nil, force_broadcast=false)
|
75
|
+
if syncing?
|
76
|
+
if force_broadcast
|
77
|
+
raise AssertError, 'only allowed for local txs' if origin
|
78
|
+
logger.debug 'force broadcasting unvalidated tx'
|
79
|
+
broadcast_transaction tx, origin
|
80
|
+
end
|
81
|
+
|
82
|
+
return
|
83
|
+
end
|
84
|
+
|
85
|
+
logger.debug 'add_transaction', locked: !@add_transaction_lock.locked?, tx: tx
|
86
|
+
raise ArgumentError, 'tx must be Transaction' unless tx.instance_of?(Transaction)
|
87
|
+
raise ArgumentError, 'origin must be nil or DEVp2p::Protocol' unless origin.nil? || origin.is_a?(DEVp2p::Protocol)
|
88
|
+
|
89
|
+
if @broadcast_filter.include?(tx.full_hash)
|
90
|
+
logger.debug 'discarding known tx'
|
91
|
+
return
|
92
|
+
end
|
93
|
+
|
94
|
+
begin
|
95
|
+
@chain.head_candidate.validate_transaction tx
|
96
|
+
logger.debug 'valid tx, broadcasting'
|
97
|
+
broadcast_transaction tx, origin
|
98
|
+
rescue InvalidTransaction => e
|
99
|
+
logger.debug 'invalid tx', error: e
|
100
|
+
return
|
101
|
+
end
|
102
|
+
|
103
|
+
if origin # not locally added via jsonrpc
|
104
|
+
if !mining? || syncing?
|
105
|
+
logger.debug 'discarding tx', syncing: syncing?, mining: mining?
|
106
|
+
return
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
@add_transaction_lock.lock
|
111
|
+
success = @chain.add_transaction tx
|
112
|
+
@add_transaction_lock.unlock
|
113
|
+
|
114
|
+
on_new_head_candidate if success
|
115
|
+
success
|
116
|
+
end
|
117
|
+
|
118
|
+
def add_block(t_block, proto)
|
119
|
+
@block_queue.enq [t_block, proto] # blocks if full
|
120
|
+
if !@add_blocks_lock
|
121
|
+
@add_blocks_lock = true
|
122
|
+
Thread.new { add_blocks }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def add_blocks
|
127
|
+
logger.debug 'add_blocks', qsize: @block_queue.size, add_tx_lock: @add_transaction_lock.locked?
|
128
|
+
raise AssertError unless @add_blocks_lock
|
129
|
+
@add_transaction_lock.lock
|
130
|
+
|
131
|
+
while !@block_queue.empty?
|
132
|
+
t_block, proto = @block_queue.peek
|
133
|
+
|
134
|
+
if @chain.include?(t_block.header.full_hash)
|
135
|
+
logger.warn 'known block', block: t_block
|
136
|
+
@block_queue.deq
|
137
|
+
next
|
138
|
+
end
|
139
|
+
|
140
|
+
if !@chain.include?(t_block.header.prevhash)
|
141
|
+
logger.warn 'missing parent', block: t_block, head: @chain.head
|
142
|
+
@block_queue.deq
|
143
|
+
next
|
144
|
+
end
|
145
|
+
|
146
|
+
block = nil
|
147
|
+
begin # deserialize
|
148
|
+
t = Time.now
|
149
|
+
block = t_block.to_block @chain.env
|
150
|
+
elapsed = Time.now - t
|
151
|
+
logger.debug 'deserialized', elapsed: elapsed, gas_used: block.gas_used, gpsec: gpsec(block.gas_used, elapsed)
|
152
|
+
rescue InvalidTransaction => e
|
153
|
+
logger.warn 'invalid transaction', block: t_block, error: e
|
154
|
+
errtype = case e
|
155
|
+
when InvalidNonce then 'InvalidNonce'
|
156
|
+
when InsufficientBalance then 'NotEnoughCash'
|
157
|
+
when InsufficientStartGas then 'OutOfGasBase'
|
158
|
+
else 'other_transaction_error'
|
159
|
+
end
|
160
|
+
warn_invalid t_block, errtype
|
161
|
+
@block_queue.deq
|
162
|
+
next
|
163
|
+
rescue ValidationError => e
|
164
|
+
logger.warn 'verification failed', error: e
|
165
|
+
warn_invalid t_block, 'other_block_error'
|
166
|
+
@block_queue.deq
|
167
|
+
next
|
168
|
+
end
|
169
|
+
|
170
|
+
# all check passed
|
171
|
+
logger.debug 'adding', block: block
|
172
|
+
t = Time.now
|
173
|
+
if @chain.add_block(block, mining?)
|
174
|
+
logger.info 'added', block: block, txs: block.transaction_count, gas_used: block.gas_used, time: (Time.now-t)
|
175
|
+
|
176
|
+
now = Time.now.to_i
|
177
|
+
if t_block.newblock_timestamp && t_block.newblock_timestamp > 0
|
178
|
+
total = now - t_block.newblock_timestamp
|
179
|
+
@newblock_processing_times.push total
|
180
|
+
@newblock_processing_times.shift if @newblock_processing_times.size > MAX_NEWBLOCK_AGE
|
181
|
+
|
182
|
+
avg = @newblock_processing_times.reduce(0.0, &:+) / @newblock_processing_times.size
|
183
|
+
max = @newblock_processing_times.max
|
184
|
+
min = @newblock_processing_times.min
|
185
|
+
logger.info 'processing time', last: total, avg: avg, max: max, min: min
|
186
|
+
end
|
187
|
+
else
|
188
|
+
logger.warn 'could not add', block: block
|
189
|
+
end
|
190
|
+
|
191
|
+
@block_queue.deq
|
192
|
+
sleep 0.001
|
193
|
+
end
|
194
|
+
rescue
|
195
|
+
logger.error $!
|
196
|
+
logger.error $!.backtrace[0,10].join("\n")
|
197
|
+
ensure
|
198
|
+
@add_blocks_lock = false
|
199
|
+
@add_transaction_lock.unlock
|
200
|
+
end
|
201
|
+
|
202
|
+
def add_mined_block(block)
|
203
|
+
logger.debug 'adding mined block', block: block
|
204
|
+
raise ArgumentError, 'block must be Block' unless block.is_a?(Block)
|
205
|
+
raise AssertError, 'invalid pow' unless block.header.check_pow
|
206
|
+
|
207
|
+
if @chain.add_block(block)
|
208
|
+
logger.debug 'added', block: block
|
209
|
+
raise AssertError, 'block is not head' unless block == @chain.head
|
210
|
+
broadcast_newblock block, block.chain_difficulty
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
##
|
215
|
+
# if block is in chain or in queue
|
216
|
+
#
|
217
|
+
def knows_block(blockhash)
|
218
|
+
return true if @chain.include?(blockhash)
|
219
|
+
@block_queue.queue.any? {|(block, proto)| block.header.full_hash == blockhash }
|
220
|
+
end
|
221
|
+
|
222
|
+
def broadcast_newblock(block, chain_difficulty=nil, origin=nil)
|
223
|
+
unless chain_difficulty
|
224
|
+
raise AssertError, 'block not in chain' unless @chain.include?(block.full_hash)
|
225
|
+
chain_difficulty = block.chain_difficulty
|
226
|
+
end
|
227
|
+
|
228
|
+
raise ArgumentError, 'block must be Block or TransientBlock' unless block.is_a?(Block) or block.instance_of?(TransientBlock)
|
229
|
+
|
230
|
+
if @broadcast_filter.update(block.header.full_hash)
|
231
|
+
logger.debug 'broadcasting newblock', origin: origin
|
232
|
+
exclude_peers = origin ? [origin.peer] : []
|
233
|
+
app.services.peermanager.broadcast(ETHProtocol, 'newblock', [block, chain_difficulty], {}, nil, exclude_peers)
|
234
|
+
else
|
235
|
+
logger.debug 'already broadcasted block'
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def broadcast_transaction(tx, origin=nil)
|
240
|
+
raise ArgumentError, 'tx must be Transaction' unless tx.instance_of?(Transaction)
|
241
|
+
|
242
|
+
if @broadcast_filter.update(tx.full_hash)
|
243
|
+
logger.debug 'broadcasting tx', origin: origin
|
244
|
+
exclude_peers = origin ? [origin.peer] : []
|
245
|
+
app.services.peermanager.broadcast ETHProtocol, 'transactions', [tx], {}, nil, exclude_peers
|
246
|
+
else
|
247
|
+
logger.debug 'already broadcasted tx'
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def on_wire_protocol_start(proto)
|
252
|
+
logger.debug 'on_wire_protocol_start', proto: proto
|
253
|
+
raise AssertError, 'incompatible protocol' unless proto.instance_of?(@wire_protocol)
|
254
|
+
|
255
|
+
# register callbacks
|
256
|
+
%i(status newblockhashes transactions getblockhashes blockhashes getblocks blocks newblock getblockhashesfromnumber).each do |cmd|
|
257
|
+
proto.send(:"receive_#{cmd}_callbacks").push method(:"on_receive_#{cmd}")
|
258
|
+
end
|
259
|
+
|
260
|
+
head = @chain.head
|
261
|
+
proto.send_status head.chain_difficulty, head.full_hash, @chain.genesis.full_hash
|
262
|
+
end
|
263
|
+
|
264
|
+
def on_wire_protocol_stop(proto)
|
265
|
+
raise AssertError, 'incompatible protocol' unless proto.instance_of?(@wire_protocol)
|
266
|
+
logger.debug 'on_wire_protocol_stop', proto: proto
|
267
|
+
end
|
268
|
+
|
269
|
+
def on_receive_status(proto, options)
|
270
|
+
eth_version = options[:eth_version]
|
271
|
+
network_id = options[:network_id]
|
272
|
+
chain_difficulty = options[:chain_difficulty]
|
273
|
+
chain_head_hash = options[:chain_head_hash]
|
274
|
+
genesis_hash = options[:genesis_hash]
|
275
|
+
|
276
|
+
logger.debug 'status received', proto: proto, eth_version: eth_version
|
277
|
+
raise ETHProtocolError, 'eth version mismatch' unless eth_version == proto.version
|
278
|
+
|
279
|
+
if network_id != config[:eth].fetch(:network_id, proto.network_id)
|
280
|
+
logger.warn 'invalid network id', remote_network_id: network_id, expected_network_id: config[:eth].fetch(:network_id, proto.network_id)
|
281
|
+
raise ETHProtocolError, 'wrong network id'
|
282
|
+
end
|
283
|
+
|
284
|
+
# check genesis
|
285
|
+
if genesis_hash != @chain.genesis.full_hash
|
286
|
+
logger.warn 'invalid genesis hash', remote_id: proto, genesis: Utils.encode_hex(genesis_hash)
|
287
|
+
raise ETHProtocolError, 'wrong genesis block'
|
288
|
+
end
|
289
|
+
|
290
|
+
# request chain
|
291
|
+
@synchronizer.receive_status proto, chain_head_hash, chain_difficulty
|
292
|
+
|
293
|
+
# send transactions
|
294
|
+
transactions = @chain.get_transactions
|
295
|
+
unless transactions.empty?
|
296
|
+
logger.debug 'sending transactions', remote_id: proto
|
297
|
+
proto.send_transactions *transactions
|
298
|
+
end
|
299
|
+
rescue ETHProtocolError
|
300
|
+
app.services.peermanager.exclude proto.peer
|
301
|
+
rescue
|
302
|
+
logger.error $!
|
303
|
+
logger.error $!.backtrace[0,10].join("\n")
|
304
|
+
end
|
305
|
+
|
306
|
+
def on_receive_transactions(proto, transactions)
|
307
|
+
logger.debug 'remote transactions received', count: transactions.size, remote_id: proto
|
308
|
+
transactions.each do |tx|
|
309
|
+
add_transaction tx, proto
|
310
|
+
end
|
311
|
+
rescue
|
312
|
+
logger.debug $!
|
313
|
+
logger.debug $!.backtrace[0,10].join("\n")
|
314
|
+
end
|
315
|
+
|
316
|
+
def on_receive_newblockhashes(proto, newblockhashes)
|
317
|
+
logger.debug 'recv newblockhashes', num: newblockhashes.size, remote_id: proto
|
318
|
+
raise AssertError, 'cannot handle more than 32 block hashes at one time' unless newblockhashes.size <= 32
|
319
|
+
|
320
|
+
@synchronizer.receive_newblockhashes(proto, newblockhashes)
|
321
|
+
end
|
322
|
+
|
323
|
+
def on_receive_getblockhashes(proto, options)
|
324
|
+
child_block_hash = options[:child_block_hash]
|
325
|
+
count = options[:count]
|
326
|
+
|
327
|
+
logger.debug 'handle getblockhashes', count: count, block_hash: Utils.encode_hex(child_block_hash)
|
328
|
+
|
329
|
+
max_hashes = [count, @wire_protocol::MAX_GETBLOCKHASHES_COUNT].min
|
330
|
+
found = []
|
331
|
+
|
332
|
+
unless @chain.include?(child_block_hash)
|
333
|
+
logger.debug 'unknown block'
|
334
|
+
proto.send_blockhashes
|
335
|
+
return
|
336
|
+
end
|
337
|
+
|
338
|
+
last = child_block_hash
|
339
|
+
while found.size < max_hashes
|
340
|
+
begin
|
341
|
+
last = RLP.decode_lazy(@chain.db.get(last))[0][0] # [head][prevhash]
|
342
|
+
rescue KeyError
|
343
|
+
# this can happen if we started a chain download, which did not complete
|
344
|
+
# should not happen if the hash is part of the canonical chain
|
345
|
+
logger.warn 'KeyError in getblockhashes', hash: last
|
346
|
+
break
|
347
|
+
end
|
348
|
+
|
349
|
+
if last
|
350
|
+
found.push(last)
|
351
|
+
else
|
352
|
+
break
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
logger.debug 'sending: found block_hashes', count: found.size
|
357
|
+
proto.send_blockhashes *found
|
358
|
+
end
|
359
|
+
|
360
|
+
def on_receive_blockhashes(proto, blockhashes)
|
361
|
+
if blockhashes.empty?
|
362
|
+
logger.debug 'recv 0 remote block hashes, signifying genesis block'
|
363
|
+
else
|
364
|
+
logger.debug 'on receive blockhashes', count: blockhashes.size, remote_id: proto, first: Utils.encode_hex(blockhashes.first), last: Utils.encode_hex(blockhashes.last)
|
365
|
+
end
|
366
|
+
|
367
|
+
@synchronizer.receive_blockhashes proto, blockhashes
|
368
|
+
end
|
369
|
+
|
370
|
+
def on_receive_getblocks(proto, blockhashes)
|
371
|
+
logger.debug 'on receive getblocks', count: blockhashes.size
|
372
|
+
|
373
|
+
found = []
|
374
|
+
blockhashes[0, @wire_protocol::MAX_GETBLOCKS_COUNT].each do |bh|
|
375
|
+
begin
|
376
|
+
found.push @chain.db.get(bh)
|
377
|
+
rescue KeyError
|
378
|
+
logger.debug 'unknown block requested', block_hash: Utils.encode_hex(bh)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
unless found.empty?
|
383
|
+
logger.debug 'found', count: found.dize
|
384
|
+
proto.send_blocks *found
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
def on_receive_blocks(proto, transient_blocks)
|
389
|
+
blk_number = transient_blocks.empty? ? 0 : transient_blocks.map {|blk| blk.header.number }.max
|
390
|
+
logger.debug 'recv blocks', count: transient_blocks.size, remote_id: proto, highest_number: blk_number
|
391
|
+
|
392
|
+
unless transient_blocks.empty?
|
393
|
+
@synchronizer.receive_blocks proto, transient_blocks
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
def on_receive_newblock(proto, options)
|
398
|
+
block = options[:block]
|
399
|
+
chain_difficulty = options[:chain_difficulty]
|
400
|
+
|
401
|
+
logger.debug 'recv newblock', block: block, remote_id: proto
|
402
|
+
@synchronizer.receive_newblock proto, block, chain_difficulty
|
403
|
+
rescue
|
404
|
+
logger.debug $!
|
405
|
+
logger.debug $!.backtrace[0,10].join("\n")
|
406
|
+
end
|
407
|
+
|
408
|
+
def on_receive_getblockhashesfromnumber(proto, options)
|
409
|
+
number = options[:number]
|
410
|
+
count = options[:count]
|
411
|
+
|
412
|
+
logger.debug 'recv getblockhashesfromnumber', number: number, count: count, remote_id: proto
|
413
|
+
|
414
|
+
found = []
|
415
|
+
count = [count, @wire_protocol::MAX_GETBLOCKHASHES_COUNT].min
|
416
|
+
|
417
|
+
for i in (number...(number+count))
|
418
|
+
begin
|
419
|
+
h = @chain.index.get_block_by_number(i)
|
420
|
+
found.push h
|
421
|
+
rescue KeyError
|
422
|
+
logger.debug 'unknown block requested', number: number
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
logger.debug 'sending: found block_hashes', count: found.size
|
427
|
+
proto.send_blockhashes *found
|
428
|
+
end
|
429
|
+
|
430
|
+
private
|
431
|
+
|
432
|
+
def logger
|
433
|
+
@logger ||= Logger.new('eth.chainservice')
|
434
|
+
end
|
435
|
+
|
436
|
+
def on_new_head(block)
|
437
|
+
logger.debug 'new head cbs', num: @on_new_head_cbs.size
|
438
|
+
@on_new_head_cbs.each {|cb| cb.call block }
|
439
|
+
on_new_head_candidate # we implicitly have a new head_candidate
|
440
|
+
end
|
441
|
+
|
442
|
+
def on_new_head_candidate
|
443
|
+
@on_new_head_candidate_cbs.each {|cb| cb.call @chain.head_candidate }
|
444
|
+
end
|
445
|
+
|
446
|
+
def gpsec(gas_spent=0, elapsed=0)
|
447
|
+
if gas_spent != 0
|
448
|
+
@processed_gas += gas_spent
|
449
|
+
@processed_elapsed += elapsed
|
450
|
+
end
|
451
|
+
|
452
|
+
(@processed_gas / (0.001 + @processed_elapsed)).to_i
|
453
|
+
end
|
454
|
+
|
455
|
+
def warn_invalid(block, errortype='other')
|
456
|
+
# TODO: send to badblocks.ethereum.org
|
457
|
+
end
|
458
|
+
|
459
|
+
def setup_db(app)
|
460
|
+
data_dir = app.config[:data_dir]
|
461
|
+
eth_config = app.config[:eth] || {}
|
462
|
+
|
463
|
+
if eth_config[:pruning].to_i >= 0
|
464
|
+
@db = DB::RefcountDB.new app.services.db
|
465
|
+
|
466
|
+
if @db.db.include?("I am not pruning")
|
467
|
+
raise "The database in '#{data_dir}' was initialized as non-pruning. Can not enable pruning now."
|
468
|
+
end
|
469
|
+
|
470
|
+
@db.ttl = eth_config[:pruning].to_i
|
471
|
+
@db.db.put "I am pruning", "1"
|
472
|
+
@db.commit
|
473
|
+
else
|
474
|
+
@db = app.services.db
|
475
|
+
|
476
|
+
if @db.include?("I am pruning")
|
477
|
+
raise "The database in '#{data_dir}' was initialized as pruning. Can not disable pruning now."
|
478
|
+
end
|
479
|
+
|
480
|
+
@db.put "I am not pruning", "1"
|
481
|
+
@db.commit
|
482
|
+
end
|
483
|
+
|
484
|
+
if @db.include?('network_id')
|
485
|
+
db_network_id = @db.get 'network_id'
|
486
|
+
|
487
|
+
if db_network_id != eth_config[:network_id].to_s
|
488
|
+
raise "The database in '#{data_dir}' was initialized with network id #{db_network_id} and can not be used when connecting to network id #{eth_config[:network_id]}. Please choose a different data directory."
|
489
|
+
end
|
490
|
+
else
|
491
|
+
@db.put 'network_id', eth_config[:network_id].to_s
|
492
|
+
@db.commit
|
493
|
+
end
|
494
|
+
|
495
|
+
raise AssertError, 'failed to setup db' if @db.nil?
|
496
|
+
end
|
497
|
+
|
498
|
+
end
|
499
|
+
|
500
|
+
end
|