Class: Ethereum::Chain

Inherits:
Object show all
Defined in:
lib/ethereum/chain.rb

Overview

Manages the chain and requests to it.

Constant Summary collapse

HEAD_KEY =
'HEAD'.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(env, genesis: nil, new_head_cb: nil, coinbase: Address::ZERO) ⇒ Chain

Returns a new instance of Chain.

Parameters:

Raises:

  • (ArgumentError)


17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/ethereum/chain.rb', line 17

def initialize(env, genesis: nil, new_head_cb: nil, coinbase: Address::ZERO)
  raise ArgumentError, "env must be instance of Env" unless env.instance_of?(Env)

  @env = env
  @db = env.db

  @new_head_cb = new_head_cb
  @index = Index.new env
  @coinbase = coinbase

  initialize_blockchain(genesis) unless @db.has_key?(HEAD_KEY)
  logger.debug "chain @ head_hash=#{head}"

  @genesis = get @index.get_block_by_number(0)
  logger.debug "got genesis", nonce: Utils.encode_hex(@genesis.nonce), difficulty: @genesis.difficulty

  @head_candidate = nil
  update_head_candidate
end

Instance Attribute Details

#envObject (readonly)

Returns the value of attribute env.



12
13
14
# File 'lib/ethereum/chain.rb', line 12

def env
  @env
end

#head_candidateObject (readonly)

Returns the value of attribute head_candidate.



12
13
14
# File 'lib/ethereum/chain.rb', line 12

def head_candidate
  @head_candidate
end

#indexObject (readonly)

Returns the value of attribute index.



12
13
14
# File 'lib/ethereum/chain.rb', line 12

def index
  @index
end

Instance Method Details

#add_block(block, forward_pending_transaction = true) ⇒ Object

Returns ‘true` if block was added successfully.



109
110
111
112
113
114
115
116
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
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/ethereum/chain.rb', line 109

def add_block(block, forward_pending_transaction=true)
  unless block.has_parent? || block.genesis?
    logger.debug "missing parent", block_hash: block
    return false
  end

  unless block.validate_uncles
    logger.debug "invalid uncles", block_hash: block
    return false
  end

  unless block.header.check_pow || block.genesis?
    logger.debug "invalid nonce", block_hash: block
    return false
  end

  if block.has_parent?
    begin
      Block.verify(block, block.get_parent)
    rescue InvalidBlock => e
      log.fatal "VERIFICATION FAILED", block_hash: block, error: e

      f = File.join Utils.data_dir, 'badblock.log'
      File.write(f, Utils.encode_hex(RLP.encode(block)))
      return false
    end
  end

  if block.number < head.number
    logger.debug "older than head", block_hash: block, head_hash: head
  end

  @index.add_block block
  store_block block

  # set to head if this makes the longest chain w/ most work for that number
  if block.chain_difficulty > head.chain_difficulty
    logger.debug "new head", block_hash: block, num_tx: block.transaction_count
    update_head block, forward_pending_transaction
  elsif block.number > head.number
    logger.warn "has higher blk number than head but lower chain_difficulty", block_has: block, head_hash: head, block_difficulty: block.chain_difficulty, head_difficulty: head.chain_difficulty
  end

  # Refactor the long calling chain
  block.transactions.clear_all
  block.receipts.clear_all
  block.state.db.commit_refcount_changes block.number
  block.state.db.cleanup block.number

  commit # batch commits all changes that came with the new block
  true
end

#add_transaction(transaction) ⇒ Bool, NilClass

Add a transaction to the ‘head_candidate` block.

If the transaction is invalid, the block will not be changed.

Returns:

  • (Bool, NilClass)

    'true` is the transaction was successfully added or `false` if the transaction was invalid, `nil` if it's already included

Raises:



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
# File 'lib/ethereum/chain.rb', line 174

def add_transaction(transaction)
  raise AssertError, "head candiate cannot be nil" unless @head_candidate

  hc = @head_candidate
  logger.debug "add tx", num_txs: transaction_count, tx: transaction, on: hc

  if @head_candidate.include_transaction?(transaction.full_hash)
    logger.debug "known tx"
    return
  end

  old_state_root = hc.state_root
  # revert finalization
  hc.state_root = @pre_finalize_state_root
  begin
    success, output = hc.apply_transaction(transaction)
  rescue InvalidTransaction => e
    # if unsuccessful the prerequisites were not fullfilled and the tx is
    # invalid, state must not have changed
    logger.debug "invalid tx", error: e
    hc.state_root = old_state_root
    return false
  end
  logger.debug "valid tx"

  # we might have a new head_candidate (due to ctx switches in up layer)
  if @head_candidate != hc
    logger.debug "head_candidate changed during validation, trying again"
    return add_transaction(transaction)
  end

  @pre_finalize_state_root = hc.state_root
  hc.finalize
  logger.debug "tx applied", result: output

  raise AssertError, "state root unchanged!" unless old_state_root != hc.state_root
  true
end

#coinbaseObject

Raises:



43
44
45
46
# File 'lib/ethereum/chain.rb', line 43

def coinbase
  raise AssertError, "coinbase changed!" unless @head_candidate.coinbase == @coinbase
  @coinbase
end

#coinbase=(v) ⇒ Object



48
49
50
51
52
# File 'lib/ethereum/chain.rb', line 48

def coinbase=(v)
  @coinbase = v
  # block reward goes to different address => redo finalization of head candidate
  update_head head
end

#commitObject



102
103
104
# File 'lib/ethereum/chain.rb', line 102

def commit
  @db.commit
end

#get(blockhash) ⇒ Object

Raises:

  • (ArgumentError)


83
84
85
86
87
# File 'lib/ethereum/chain.rb', line 83

def get(blockhash)
  raise ArgumentError, "blockhash must be a String" unless blockhash.instance_of?(String)
  raise ArgumentError, "blockhash size must be 32" unless blockhash.size == 32
  Block.find(@env, blockhash)
end

#get_bloom(blockhash) ⇒ Object



89
90
91
92
# File 'lib/ethereum/chain.rb', line 89

def get_bloom(blockhash)
  b = RLP.decode RLP.descend(@db.get(blockhash), 0, 6)
  Utils.big_endian_to_int b
end

#get_brothers(block) ⇒ Object

Return the uncles of the hypothetical child of ‘block`.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/ethereum/chain.rb', line 68

def get_brothers(block)
  o = []
  i = 0

  while block.has_parent? && i < @env.config[:max_uncle_depth]
    parent = block.get_parent
    children = get_children(parent).select {|c| c != block }
    o.concat children
    block = parent
    i += 1
  end

  o
end

#get_chain(start: '', count: 10) ⇒ Object

Return ‘count` of blocks starting from head or `start`.



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/ethereum/chain.rb', line 233

def get_chain(start: '', count: 10)
  logger.debug "get_chain", start: Utils.encode_hex(start), count: count

  if start.true?
    return [] unless @index.db.include?(start)

    block = get start
    return [] unless in_main_branch?(block)
  else
    block = head
  end

  blocks = []
  count.times do |i|
    blocks.push block
    break if block.genesis?
    block = block.get_parent
  end

  blocks
end

#get_children(block) ⇒ Object



162
163
164
# File 'lib/ethereum/chain.rb', line 162

def get_children(block)
  @index.get_children(block.full_hash).map {|c| get(c) }
end

#get_descendants(block, count: 1) ⇒ Object

Raises:



261
262
263
264
265
266
267
# File 'lib/ethereum/chain.rb', line 261

def get_descendants(block, count: 1)
  logger.debug "get_descendants", block_hash: block
  raise AssertError, "cannot find block hash in current chain" unless include?(block.full_hash)

  block_numbers = (block.number+1)...([head.number+1, block.number+count+1].min)
  block_numbers.map {|n| get @index.get_block_by_number(n) }
end

#get_transactionsObject

Get a list of new transactions not yet included in a mined block but known to the chain.



217
218
219
220
221
222
223
224
# File 'lib/ethereum/chain.rb', line 217

def get_transactions
  if @head_candidate
    logger.debug "get_transactions called", on: @head_candidate
    @head_candidate.get_transactions
  else
    []
  end
end

#get_uncles(block) ⇒ Object

Return the uncles of ‘block`.



57
58
59
60
61
62
63
# File 'lib/ethereum/chain.rb', line 57

def get_uncles(block)
  if block.has_parent?
    get_brothers(block.get_parent)
  else
    []
  end
end

#has_block(blockhash) ⇒ Object Also known as: include?, has_key?

Raises:

  • (ArgumentError)


94
95
96
97
98
# File 'lib/ethereum/chain.rb', line 94

def has_block(blockhash)
  raise ArgumentError, "blockhash must be a String" unless blockhash.instance_of?(String)
  raise ArgumentError, "blockhash size must be 32" unless blockhash.size == 32
  @db.include?(blockhash)
end

#headObject



37
38
39
40
41
# File 'lib/ethereum/chain.rb', line 37

def head
  initialize_blockchain unless @db && @db.has_key?(HEAD_KEY)
  ptr = @db.get HEAD_KEY
  Block.find @env, ptr
end

#in_main_branch?(block) ⇒ Boolean

Returns:

  • (Boolean)


255
256
257
258
259
# File 'lib/ethereum/chain.rb', line 255

def in_main_branch?(block)
  block.full_hash == @index.get_block_by_number(block.number)
rescue KeyError
  false
end

#transaction_countObject



226
227
228
# File 'lib/ethereum/chain.rb', line 226

def transaction_count
  @head_candidate ? @head_candidate.transaction_count : 0
end

#update_head(block, forward_pending_transaction = true) ⇒ Object



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/ethereum/chain.rb', line 269

def update_head(block, forward_pending_transaction=true)
  logger.debug "updating head"
  logger.debug "New Head is on a different branch", head_hash: block, old_head_hash: head if !block.genesis? && block.get_parent != head

  # Some temporary auditing to make sure pruning is working well
  if block.number > 0 && block.number % 500 == 0 && @db.instance_of?(DB::RefcountDB)
    # TODO
  end

  # Fork detected, revert death row and change logs
  if block.number > 0
    b = block.get_parent
    h = head
    b_children = []

    if b.full_hash != h.full_hash
      logger.warn "reverting"

      while h.number > b.number
        h.state.db.revert_refcount_changes h.number
        h = h.get_parent
      end
      while b.number > h.number
        b_children.push b
        b = b.get_parent
      end

      while b.full_hash != h.full_hash
        h.state.db.revert_refcount_changes h.number
        h = h.get_parent

        b_children.push b
        b = b.get_parent
      end

      b_children.each do |bc|
        Block.verify(bc, bc.get_parent)
      end
    end
  end

  @db.put HEAD_KEY, block.full_hash
  raise "Chain write error!" unless @db.get(HEAD_KEY) == block.full_hash

  @index.update_blocknumbers(head)
  raise "Fail to update head!" unless head == block

  logger.debug "set new head", head: head
  update_head_candidate forward_pending_transaction

  @new_head_cb.call(block) if @new_head_cb && !block.genesis?
end