Class: Reth::AccountService

Inherits:
DEVp2p::Service
  • Object
show all
Includes:
Enumerable
Defined in:
lib/reth/account_service.rb

Constant Summary collapse

DEFAULT_COINBASE =
Utils.decode_hex('de0b295669a9fd93d5f28d9ec85e40f4cb697bae')

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app) ⇒ AccountService

Returns a new instance of AccountService.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/reth/account_service.rb', line 22

def initialize(app)
  super(app)

  if app.config[:accounts][:keystore_dir][0] == '/'
    @keystore_dir = app.config[:accounts][:keystore_dir]
  else # relative path
    @keystore_dir = File.join app.config[:data_dir], app.config[:accounts][:keystore_dir]
  end

  @accounts = []

  if !File.exist?(@keystore_dir)
    logger.warn "keystore directory does not exist", directory: @keystore_dir
  elsif !File.directory?(@keystore_dir)
    logger.error "configured keystore directory is a file, not a directory", directory: @keystore_dir
  else
    logger.info "searching for key files", directory: @keystore_dir

    ignore = %w(. ..)
    Dir.foreach(@keystore_dir) do |filename|
      next if ignore.include?(filename)

      begin
        @accounts.push Account.load(File.join(@keystore_dir, filename))
      rescue ValueError
        logger.warn "invalid file skipped in keystore directory", path: filename
      end
    end
  end
  @accounts.sort_by! {|acct| acct.path.to_s }

  if @accounts.empty?
    logger.warn "no accounts found"
  else
    logger.info "found account(s)", accounts: @accounts
  end
end

Instance Attribute Details

#accountsObject (readonly)

Returns the value of attribute accounts.



18
19
20
# File 'lib/reth/account_service.rb', line 18

def accounts
  @accounts
end

Instance Method Details

#[](address_or_idx) ⇒ Object



333
334
335
336
337
338
339
340
341
342
# File 'lib/reth/account_service.rb', line 333

def [](address_or_idx)
  if address_or_idx.instance_of?(String)
    raise ArgumentError, 'address must be 20 bytes' unless address_or_idx.size == 20
    acct = @accounts.find {|acct| acct.address == address_or_idx }
    acct or raise KeyError
  else
    raise ArgumentError, 'address_or_idx must be String or Integer' unless address_or_idx.is_a?(Integer)
    @accounts[address_or_idx]
  end
end

#accounts_with_addressObject



230
231
232
# File 'lib/reth/account_service.rb', line 230

def accounts_with_address
  @accounts.select {|acct| acct.address }
end

#add_account(account, store = true, include_address = true, include_id = true) ⇒ Object

Add an account.

If ‘store` is true the account will be stored as a key file at the location given by `account.path`. If this is `nil` a `ValueError` is raised. `include_address` and `include_id` determine if address and id should be removed for storage or not.

This method will raise a ‘ValueError` if the new account has the same UUID as an account already known to the service. Note that address collisions do not result in an exception as those may slip through anyway for locked accounts with hidden addresses.



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/reth/account_service.rb', line 117

def (, store=true, include_address=true, include_id=true)
  logger.info "adding account", account: 

  if .uuid && @accounts.any? {|acct| acct.uuid == .uuid }
    logger.error 'could not add account (UUID collision)', uuid: .uuid
    raise ValueError, 'Could not add account (UUID collision)'
  end

  if store
    raise ValueError, 'Cannot store account without path' if .path.nil?
    if File.exist?(.path)
      logger.error 'File does already exist', path: .path
      raise IOError, 'File does already exist'
    end

    raise AssertError if @accounts.any? {|acct| acct.path == .path }

    begin
      directory = File.dirname .path
      FileUtils.mkdir_p(directory) unless File.exist?(directory)

      File.open(.path, 'w') do |f|
        f.write .dump(include_address, include_id)
      end
    rescue IOError => e
      logger.error "Could not write to file", path: .path, message: e.to_s
      raise e
    end
  end

  @accounts.push 
  @accounts.sort_by! {|acct| acct.path.to_s }
end

#coinbaseObject

Return the address that should be used as coinbase for new blocks.

The coinbase address is given by the config field pow.coinbase_hex. If this does not exist, the address of the first account is used instead. If there are no accounts, the coinbase is ‘DEFAULT_COINBASE`.

Raises:

  • (ValueError)

    if the coinbase is invalid (no string, wrong length) or there is no account for it and the config flag 'accounts.check_coinbase` is set (does not apply to the default coinbase).



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/reth/account_service.rb', line 80

def coinbase
  cb_hex = (app.config[:pow] || {})[:coinbase_hex]
  if cb_hex
    raise ValueError, 'coinbase must be String' unless cb_hex.is_a?(String)
    begin
      cb = Utils.decode_hex Utils.remove_0x_head(cb_hex)
    rescue TypeError
      raise ValueError, 'invalid coinbase'
    end
  else
    accts = accounts_with_address
    return DEFAULT_COINBASE if accts.empty?
    cb = accts[0].address
  end

  raise ValueError, 'wrong coinbase length' if cb.size != 20

  if config[:accounts][:must_include_coinbase]
    raise ValueError, 'no account for coinbase' if !@accounts.map(&:address).include?(cb)
  end

  cb
end

#each(&block) ⇒ Object



345
346
347
# File 'lib/reth/account_service.rb', line 345

def each(&block)
  @accounts.each(&block)
end

#find(identifier) ⇒ Object

Find an account by either its address, its id, or its index as string.

Example identifiers:

  • ‘9c0e0240776cfbe6fa1eb37e57721e1a88a563d1’ (address)

  • ‘0x9c0e0240776cfbe6fa1eb37e57721e1a88a563d1’ (address with 0x prefix)

  • ‘01dd527b-f4a5-4b3c-9abb-6a8e7cd6722f’ (UUID)

  • ‘3’ (index)

Parameters:

  • identifier (String)

    the accounts hex encoded, case insensitive address (with optional 0x prefix), its UUID or its index (as string, >= 1)

Raises:

  • (ValueError)

    if the identifier could not be interpreted

  • (KeyError)

    if the identified account is not known to the account service



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/reth/account_service.rb', line 256

def find(identifier)
  identifier = identifier.downcase

  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
    return get_by_id(identifier)
  end

  begin
    address = Address.new(identifier).to_bytes
    raise AssertError unless address.size == 20
    return self[address]
  rescue
    # do nothing
  end

  index = identifier.to_i
  raise ValueError, 'Index must be 1 or greater' if index <= 0
  raise KeyError if index > @accounts.size
  @accounts[index-1]
end

#get_by_address(address) ⇒ Object

Get an account by its address.

Note that even if an account with the given address exists, it might not be found if it is locked. Also, multiple accounts with the same address may exist, in which case the first one is returned (and a warning is logged).

Raises:

  • (KeyError)

    if no matching account can be found



306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/reth/account_service.rb', line 306

def get_by_address(address)
  raise ArgumentError, 'address must be 20 bytes' unless address.size == 20

  accts = @accounts.select {|acct| acct.address == address }

  if accts.size == 0
    raise KeyError, "account not found by address #{Utils.encode_hex(address)}"
  elsif accts.size > 1
    logger.warn "multiple accounts with same address found", address: Utils.encode_hex(address)
  end

  accts[0]
end

#get_by_id(id) ⇒ Object

Return the account with a given id.

Note that accounts are not required to have an id.

Raises:

  • (KeyError)

    if no matching account can be found



284
285
286
287
288
289
290
291
292
293
294
# File 'lib/reth/account_service.rb', line 284

def get_by_id(id)
  accts = @accounts.select {|acct| acct.uuid == id }

  if accts.size == 0
    raise KeyError, "account with id #{id} unknown"
  elsif accts.size > 1
    logger.warn "multiple accounts with same UUID found", uuid: id
  end

  accts[0]
end

#include?(address) ⇒ Boolean

Returns:

  • (Boolean)

Raises:

  • (ArgumentError)


328
329
330
331
# File 'lib/reth/account_service.rb', line 328

def include?(address)
  raise ArgumentError, 'address must be 20 bytes' unless address.size == 20
  @accounts.any? {|acct| acct.address == address }
end

#propose_path(address) ⇒ Object



324
325
326
# File 'lib/reth/account_service.rb', line 324

def propose_path(address)
  File.join @keystore_dir, Utils.encode_hex(address)
end

#sign_tx(address, tx) ⇒ Object



320
321
322
# File 'lib/reth/account_service.rb', line 320

def sign_tx(address, tx)
  get_by_address(address).sign_tx(tx)
end

#sizeObject



349
350
351
# File 'lib/reth/account_service.rb', line 349

def size
  @accounts.size
end

#startObject



60
61
62
# File 'lib/reth/account_service.rb', line 60

def start
  # do nothing
end

#stopObject



64
65
66
# File 'lib/reth/account_service.rb', line 64

def stop
  # do nothing
end

#unlocked_accountsObject



234
235
236
# File 'lib/reth/account_service.rb', line 234

def unlocked_accounts
  @accounts.select {|acct| !acct.locked? }
end

#update_account(account, new_password, include_address = true, include_id = true) ⇒ Object

Replace the password of an account.

The update is carried out in three steps:

  1. the old keystore file is renamed

  2. the new keystore file is created at the previous location of the old

keystore file
  1. the old keystore file is removed

In this way, at least one of the keystore files exists on disk at any time and can be recovered if the process is interrupted.

Parameters:

  • account (Account)

    which must be unlocked, stored on disk and included in '@accounts`

  • include_address (Bool) (defaults to: true)

    forwarded to 'add_account` during step 2

  • include_id (Bool) (defaults to: true)

    forwarded to 'add_account` during step 2

Raises:

  • (ValueError)

    if the account is locked, if it is not added to the account manager, or if it is not stored



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/reth/account_service.rb', line 172

def (, new_password, include_address=true, include_id=true)
  raise ValueError, "Account not managed by account service" unless @accounts.include?()
  raise ValueError, "Cannot update locked account" if .locked?
  raise ValueError, 'Account not stored on disk' unless .path

  logger.debug "creating new account"
   = Account.create new_password, .privkey, .uuid, .path

  backup_path = .path + '~'
  i = 1
  while File.exist?(backup_path)
    backup_path = backup_path[0, backup_path.rindex('~')+1] + i.to_s
    i += 1
  end
  raise AssertError if File.exist?(backup_path)

  logger.info 'moving old keystore file to backup location', from: .path, to: backup_path
  begin
    FileUtils.mv .path, backup_path
  rescue
    logger.error "could not backup keystore, stopping account update", from: .path, to: backup_path
    raise $!
  end
  raise AssertError unless File.exist?(backup_path)
  raise AssertError if File.exist?(.path)
  .path = backup_path

  @accounts.delete 
  begin
     , include_address, include_id
  rescue
    logger.error 'adding new account failed, recovering from backup'
    FileUtils.mv backup_path, .path
    .path = .path
    @accounts.push 
    @accounts.sort_by! {|acct| acct.path.to_s }
    raise $!
  end
  raise AssertError unless File.exist?(.path)

  logger.info "deleting backup of old keystore", path: backup_path
  begin
    FileUtils.rm backup_path
  rescue
    logger.error 'failed to delete no longer needed backup of old keystore', path: .path
    raise $!
  end

  .keystore = .keystore
  .path = .path

  @accounts.push 
  @accounts.delete 
  @accounts.sort_by! {|acct| acct.path.to_s }

  logger.debug "account update successful"
end