class Ethereum::Chain
Manages the chain and requests to it.
Constants
- HEAD_KEY
Attributes
env[RW]
Public Class Methods
new(env, genesis: nil, new_head_cb: nil, coinbase: Address::ZERO)
click to toggle source
@param env [Ethereum::Env] configuration of the chain
# File lib/ethereum/chain.rb, line 16 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
Public Instance Methods
add_block(block, forward_pending_transaction=true)
click to toggle source
Returns `true` if block was added successfully.
# File lib/ethereum/chain.rb, line 108 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)
click to toggle source
Add a transaction to the `head_candidate` block.
If the transaction is invalid, the block will not be changed.
@return [Bool,NilClass] `true` is the transaction was successfully added or
`false` if the transaction was invalid, `nil` if it's already included
# File lib/ethereum/chain.rb, line 173 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
coinbase()
click to toggle source
# File lib/ethereum/chain.rb, line 42 def coinbase raise AssertError, "coinbase changed!" unless @head_candidate.coinbase == @coinbase @coinbase end
coinbase=(v)
click to toggle source
# File lib/ethereum/chain.rb, line 47 def coinbase=(v) @coinbase = v # block reward goes to different address => redo finalization of head candidate update_head head end
commit()
click to toggle source
# File lib/ethereum/chain.rb, line 101 def commit @db.commit end
get(blockhash)
click to toggle source
# File lib/ethereum/chain.rb, line 82 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)
click to toggle source
# File lib/ethereum/chain.rb, line 88 def get_bloom(blockhash) b = RLP.decode RLP.descend(@db.get(blockhash), 0, 6) Utils.big_endian_to_int b end
get_brothers(block)
click to toggle source
Return the uncles of the hypothetical child of `block`.
# File lib/ethereum/chain.rb, line 67 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)
click to toggle source
Return `count` of blocks starting from head or `start`.
# File lib/ethereum/chain.rb, line 232 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)
click to toggle source
# File lib/ethereum/chain.rb, line 161 def get_children(block) @index.get_children(block.full_hash).map {|c| get(c) } end
get_descendants(block, count: 1)
click to toggle source
# File lib/ethereum/chain.rb, line 260 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_transactions()
click to toggle source
Get a list of new transactions not yet included in a mined block but known to the chain.
# File lib/ethereum/chain.rb, line 216 def get_transactions if @head_candidate logger.debug "get_transactions called", on: @head_candidate @head_candidate.get_transactions else [] end end
get_uncles(block)
click to toggle source
Return the uncles of `block`.
# File lib/ethereum/chain.rb, line 56 def get_uncles(block) if block.has_parent? get_brothers(block.get_parent) else [] end end
has_block(blockhash)
click to toggle source
# File lib/ethereum/chain.rb, line 93 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
head()
click to toggle source
# File lib/ethereum/chain.rb, line 36 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)
click to toggle source
# File lib/ethereum/chain.rb, line 254 def in_main_branch?(block) block.full_hash == @index.get_block_by_number(block.number) rescue KeyError false end
transaction_count()
click to toggle source
# File lib/ethereum/chain.rb, line 225 def transaction_count @head_candidate ? @head_candidate.transaction_count : 0 end
update_head(block, forward_pending_transaction=true)
click to toggle source
# File lib/ethereum/chain.rb, line 268 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
Private Instance Methods
initialize_blockchain(genesis=nil)
click to toggle source
# File lib/ethereum/chain.rb, line 327 def initialize_blockchain(genesis=nil) logger.info "Initializing new chain" unless genesis genesis = Block.genesis(@env) logger.info "new genesis", genesis_hash: genesis, difficulty: genesis.difficulty @index.add_block genesis end store_block genesis raise "failed to store block" unless genesis == Block.find(@env, genesis.full_hash) update_head genesis raise "falied to update head" unless include?(genesis.full_hash) commit end
logger()
click to toggle source
# File lib/ethereum/chain.rb, line 323 def logger @logger ||= Logger.new 'eth.chain' end
store_block(block)
click to toggle source
# File lib/ethereum/chain.rb, line 345 def store_block(block) if block.number > 0 @db.put_temporarily block.full_hash, RLP.encode(block) else @db.put block.full_hash, RLP.encode(block) end end
update_head_candidate(forward_pending_transaction=true)
click to toggle source
after new head is set
# File lib/ethereum/chain.rb, line 354 def update_head_candidate(forward_pending_transaction=true) logger.debug "updating head candidate", head: head # collect uncles blk = head # parent of the block we are collecting uncles for uncles = get_brothers(blk).map(&:header).uniq (@env.config[:max_uncle_depth]+2).times do |i| blk.uncles.each {|u| uncles.delete u } blk = blk.get_parent if blk.has_parent? end raise "strange uncle found!" unless uncles.empty? || uncles.map(&:number).max <= head.number uncles = uncles[0, @env.config[:max_uncles]] # create block ts = [Time.now.to_i, head.timestamp+1].max _env = Env.new DB::OverlayDB.new(head.db), config: @env.config, global_config: @env.global_config hc = Block.build_from_parent head, @coinbase, timestamp: ts, uncles: uncles, env: _env raise ValidationError, "invalid uncles" unless hc.validate_uncles @pre_finalize_state_root = hc.state_root hc.finalize # add transactions from previous head candidate old_hc = @head_candidate @head_candidate = hc if old_hc tx_hashes = head.get_transaction_hashes pending = old_hc.get_transactions.select {|tx| !tx_hashes.include?(tx.full_hash) } if pending.true? if forward_pending_transaction logger.debug "forwarding pending transaction", num: pending.size pending.each {|tx| add_transaction tx } else logger.debug "discarding pending transaction", num: pending.size end end end end