class Ethereum::FastVM

Constants

END_BREAKPOINTS
OP_INVALID
STACK_MAX
START_BREAKPOINTS

Public Class Methods

code_cache() click to toggle source
# File lib/ethereum/fast_vm.rb, line 22
def code_cache
  @code_cache ||= {}
end
execute(*args) click to toggle source
# File lib/ethereum/fast_vm.rb, line 26
def execute(*args)
  new.execute(*args)
end

Public Instance Methods

execute(ext, msg, code) click to toggle source
# File lib/ethereum/fast_vm.rb, line 31
def execute(ext, msg, code)
  s = State.new gas: msg.gas

  if VM.code_cache.has_key?(code)
    processed_code = VM.code_cache[code]
  else
    processed_code = preprocess_code code
    VM.code_cache[code] = processed_code
  end

  # for trace only
  steps = 0
  _prevop = nil

  timestamp = Time.now
  loop do
    return vm_exception('INVALID START POINT') if processed_code.has_key?(s.pc)

    cc = processed_code[s.pc]
    gas, min_stack, max_stack, s.pc = cc[0,4]
    ops = cc[4..-1]

    return vm_exception('OUT OF GAS') if gas > s.gas
    return vm_exception('INCOMPATIBLE STACK LENGTH', min_stack: min_stack, max_stack: max_stack, have: s.stack.size) unless s.stack.size >= min_stack && s.stack.size <= max_stack

    s.gas -= gas

    ops.each do |op|
      if log_vm_exit.trace?
        trace_data = {
          stack: s.stack.map(&:to_s),
          inst: op,
          pc: s.pc-1,
          op: op,
          steps: steps
        }

        if [OP_MLOAD, OP_MSTORE, OP_MSTORE8, OP_SHA3, OP_CALL, OP_CALLCODE, OP_CREATE, OP_CALLDATACOPY, OP_CODECOPY, OP_EXTCODECOPY].include?(_prevop)
          if s.memory.size < 1024
            trace_data[:memory] = Utils.encode_hex(Utils.int_array_to_bytes(s.memory))
          else
            trace_data[:sha3memory] = Utils.encode_hex(Utils.keccak256(Utils.int_array_to_bytes(s.memory)))
          end
        end

        if [OP_SSTORE, OP_SLOAD].include?(_prevop) || steps == 0
          trace_data[:storage] = ext.log_storage(msg.to)
        end

        if steps == 0
          trace_data[:depth] = msg.depth
          trace_data[:address] = msg.to
        end

        log_vm_op.trace('vm', **trace_data)

        steps += 1
        _prevop = op
      end

      # Invalid operation
      return vm_exception('INVALID OP', op: op) if op == OP_INVALID

      # Valid operations
      stk = s.stack
      mem = s.memory
      if op < 0x10 # Stop & Arithmetic Operations
        case op
        when OP_STOP
          return peaceful_exit('STOP', s.gas, [])
        when OP_ADD
          r = (stk.pop + stk.pop) & UINT_MAX
          stk.push r
        when OP_SUB
          r = (stk.pop - stk.pop) & UINT_MAX
          stk.push r
        when OP_MUL
          r = (stk.pop * stk.pop) & UINT_MAX
          stk.push r
        when OP_DIV
          s0, s1 = stk.pop, stk.pop
          stk.push(s1 == 0 ? 0 : s0 / s1)
        when OP_MOD
          s0, s1 = stk.pop, stk.pop
          stk.push(s1 == 0 ? 0 : s0 % s1)
        when OP_SDIV
          s0, s1 = Utils.to_signed(stk.pop), Utils.to_signed(stk.pop)
          r = s1 == 0 ? 0 : ((s0.abs / s1.abs * (s0*s1 < 0 ? -1 : 1)) & UINT_MAX)
          stk.push r
        when OP_SMOD
          s0, s1 = Utils.to_signed(stk.pop), Utils.to_signed(stk.pop)
          r = s1 == 0 ? 0 : ((s0.abs % s1.abs * (s0 < 0 ? -1 : 1)) & UINT_MAX)
          stk.push r
        when OP_ADDMOD
          s0, s1, s2 = stk.pop, stk.pop, stk.pop
          r = s2 == 0 ? 0 : (s0+s1) % s2
          stk.push r
        when OP_MULMOD
          s0, s1, s2 = stk.pop, stk.pop, stk.pop
          r = s2 == 0 ? 0 : Utils.mod_mul(s0, s1, s2)
          stk.push r
        when OP_EXP
          base, exponent = stk.pop, stk.pop

          # fee for exponent is dependent on its bytes
          # calc n bytes to represent exponent
          nbytes = Utils.encode_int(exponent).size
          expfee = nbytes * Opcodes::GEXPONENTBYTE
          if s.gas < expfee
            s.gas = 0
            return vm_exception('OOG EXPONENT')
          end

          s.gas -= expfee
          stk.push Utils.mod_exp(base, exponent, TT256)
        when OP_SIGNEXTEND # extend sign from bytes at s0 to left
          s0, s1 = stk.pop, stk.pop
          if s0 < 32
            testbit = s0*8 + 7
            mask = 1 << testbit
            if s1 & mask == 0 # extend 0s
              stk.push(s1 & (mask - 1))
            else # extend 1s
              stk.push(s1 | (TT256 - mask))
            end
          else
            stk.push s1
          end
        end
      elsif op < 0x20 # Comparison & Bitwise Logic Operations
        case op
        when OP_LT
          s0, s1 = stk.pop, stk.pop
          stk.push(s0 < s1 ? 1 : 0)
        when OP_GT
          s0, s1 = stk.pop, stk.pop
          stk.push(s0 > s1 ? 1 : 0)
        when OP_SLT
          s0, s1 = Utils.to_signed(stk.pop), Utils.to_signed(stk.pop)
          stk.push(s0 < s1 ? 1 : 0)
        when OP_SGT
          s0, s1 = Utils.to_signed(stk.pop), Utils.to_signed(stk.pop)
          stk.push(s0 > s1 ? 1 : 0)
        when OP_EQ
          s0, s1 = stk.pop, stk.pop
          stk.push(s0 == s1 ? 1 : 0)
        when OP_ISZERO
          s0 = stk.pop
          stk.push(s0 == 0 ? 1 : 0)
        when OP_AND
          s0, s1 = stk.pop, stk.pop
          stk.push(s0 & s1)
        when OP_OR
          s0, s1 = stk.pop, stk.pop
          stk.push(s0 | s1)
        when OP_XOR
          s0, s1 = stk.pop, stk.pop
          stk.push(s0 ^ s1)
        when OP_NOT
          s0 = stk.pop
          stk.push(UINT_MAX - s0)
        when OP_BYTE
          s0, s1 = stk.pop, stk.pop
          if s0 < 32
            stk.push((s1 / 256**(31-s0)) % 256)
          else
            stk.push(0)
          end
        end
      elsif op < 0x40 # SHA3 & Environmental Information
        case op
        when OP_SHA3
          s0, s1 = stk.pop, stk.pop

          s.gas -= Opcodes::GSHA3WORD * (Utils.ceil32(s1) / 32)
          return vm_exception('OOG PAYING FOR SHA3') if s.gas < 0

          return vm_exception('OOG EXTENDING MEMORY') unless mem_extend(mem, s, s0, s1)

          data = Utils.int_array_to_bytes mem.safe_slice(s0,s1)
          stk.push Utils.big_endian_to_int(Utils.keccak256(data))
        when OP_ADDRESS
          stk.push Utils.coerce_to_int(msg.to)
        when OP_BALANCE
          s0 = stk.pop
          addr = Utils.coerce_addr_to_hex(s0 % 2**160)
          stk.push ext.get_balance(addr)
        when OP_ORIGIN
          stk.push Utils.coerce_to_int(ext.tx_origin)
        when OP_CALLER
          stk.push Utils.coerce_to_int(msg.sender)
        when OP_CALLVALUE
          stk.push msg.value
        when OP_CALLDATALOAD
          stk.push msg.data.extract32(stk.pop)
        when OP_CALLDATASIZE
          stk.push msg.data.size
        when OP_CALLDATACOPY
          mstart, dstart, size = stk.pop, stk.pop, stk.pop

          return vm_exception('OOG EXTENDING MEMORY') unless mem_extend(mem, s, mstart, size)
          return vm_exception('OOG COPY DATA') unless data_copy(s, size)

          msg.data.extract_copy(mem, mstart, dstart, size)
        when OP_CODESIZE
          stk.push code.size
        when OP_CODECOPY
          mstart, cstart, size = stk.pop, stk.pop, stk.pop

          return vm_exception('OOG EXTENDING MEMORY') unless mem_extend(mem, s, mstart, size)
          return vm_exception('OOG COPY CODE') unless data_copy(s, size)

          size.times do |i|
            if cstart + i < code.size
              mem[mstart+i] = code[cstart+i].ord
            else
              mem[mstart+i] = 0
            end
          end
        when OP_GASPRICE
          stk.push ext.tx_gasprice
        when OP_EXTCODESIZE
          addr = stk.pop
          addr = Utils.coerce_addr_to_hex(addr % 2**160)
          stk.push (ext.get_code(addr) || Constant::BYTE_EMPTY).size
        when OP_EXTCODECOPY
          addr, mstart, cstart, size = stk.pop, stk.pop, stk.pop, stk.pop
          addr = Utils.coerce_addr_to_hex(addr % 2**160)
          extcode = ext.get_code(addr) || Constant::BYTE_EMPTY
          raise ValueError, "extcode must be string" unless extcode.is_a?(String)

          return vm_exception('OOG EXTENDING MEMORY') unless mem_extend(mem, s, mstart, size)
          return vm_exception('OOG COPY CODE') unless data_copy(s, size)

          size.times do |i|
            if cstart + i < extcode.size
              mem[mstart+i] = extcode[cstart+i].ord
            else
              mem[mstart+i] = 0
            end
          end
        end
      elsif op < 0x50 # Block Information
        case op
        when OP_BLOCKHASH
          s0 = stk.pop
          stk.push Utils.big_endian_to_int(ext.block_hash(s0))
        when OP_COINBASE
          stk.push Utils.big_endian_to_int(ext.block_coinbase)
        when OP_TIMESTAMP
          stk.push ext.block_timestamp
        when OP_NUMBER
          stk.push ext.block_number
        when OP_DIFFICULTY
          stk.push ext.block_difficulty
        when OP_GASLIMIT
          stk.push ext.block_gas_limit
        end
      elsif op < 0x60 # Stack, Memory, Storage and Flow Operations
        case op
        when OP_POP
          stk.pop
        when OP_MLOAD
          s0 = stk.pop
          return vm_exception('OOG EXTENDING MEMORY') unless mem_extend(mem, s, s0, 32)

          data = 0
          mem[s0, 32].each do |c|
            data = (data << 8) + c
          end

          stk.push data
        when OP_MSTORE
          s0, s1 = stk.pop, stk.pop
          return vm_exception('OOG EXTENDING MEMORY') unless mem_extend(mem, s, s0, 32)

          32.times.to_a.reverse.each do |i|
            mem[s0+i] = s1 % 256
            s1 /= 256
          end
        when OP_MSTORE8
          s0, s1 = stk.pop, stk.pop
          return vm_exception('OOG EXTENDING MEMORY') unless mem_extend(mem, s, s0, 1)
          mem[s0] = s1 % 256
        when OP_SLOAD
          s0 = stk.pop
          stk.push ext.get_storage_data(msg.to, s0)
        when OP_SSTORE
          s0, s1 = stk.pop, stk.pop

          if ext.get_storage_data(msg.to, s0) != 0
            gascost = s1 == 0 ? Opcodes::GSTORAGEKILL : Opcodes::GSTORAGEMOD
            refund = s1 == 0 ? Opcodes::GSTORAGEREFUND : 0
          else
            gascost = s1 == 0 ? Opcodes::GSTORAGEMOD : Opcodes::GSTORAGEADD
            refund = 0
          end

          return vm_exception('OUT OF GAS') if s.gas < gascost

          s.gas -= gascost
          ext.add_refund refund
          ext.set_storage_data msg.to, s0, s1
        when OP_JUMP
          s0 = stk.pop
          s.pc = s0

          op_new = processed_code.has_key?(s.pc) ? processed_code[s.pc][4] : OP_STOP
          return vm_exception('BAD JUMPDEST') if op_new != OP_JUMPDEST
        when OP_JUMPI
          s0, s1 = stk.pop, stk.pop
          if s1.true?
            s.pc = s0
            op_new = processed_code.has_key?(s.pc) ? processed_code[s.pc][4] : OP_STOP
            return vm_exception('BAD JUMPDEST') if op_new != OP_JUMPDEST
          end
        when OP_PC
          stk.push(s.pc - 1)
        when OP_MSIZE
          stk.push mem.size
        when OP_GAS
          stk.push s.gas # AFTER subtracting cost 1
        end
      elsif (op & 0xff) >= OP_PUSH1 && (op & 0xff) <= OP_PUSH32
        stk.push(op >> 8)
      elsif op >= OP_DUP1 && op <= OP_DUP16
        depth = op - OP_DUP1 + 1
        stk.push stk[-depth]
      elsif op >= OP_SWAP1 && op <= OP_SWAP16
        depth = op - OP_SWAP1 + 1
        temp = stk[-depth - 1]
        stk[-depth - 1] = stk[-1]
        stk[-1] = temp
      elsif op >= OP_LOG0 && op <= OP_LOG4
        # 0xa0 ... 0xa4, 32/64/96/128/160 + data.size gas
        #
        # a. Opcodes LOG0...LOG4 are added, takes 2-6 stake arguments:
        #      MEMSTART MEMSZ (TOPIC1) (TOPIC2) (TOPIC3) (TOPIC4)
        #
        # b. Logs are kept track of during tx execution exactly the same way
        #    as suicides (except as an ordered list, not a set).
        #
        #    Each log is in the form [address, [topic1, ... ], data] where:
        #    * address is what the ADDRESS opcode would output
        #    * data is mem[MEMSTART, MEMSZ]
        #    * topics are as provided by the opcode
        #
        # c. The ordered list of logs in the transation are expreseed as
        #    [log0, log1, ..., logN].
        #
        depth = op - OP_LOG0
        mstart, msz = stk.pop, stk.pop
        topics = depth.times.map {|i| stk.pop }

        s.gas -= msz * Opcodes::GLOGBYTE

        return vm_exception("OOG EXTENDING MEMORY") unless mem_extend(mem, s, mstart, msz)

        data = mem.safe_slice(mstart, msz)
        ext.log(msg.to, topics, Utils.int_array_to_bytes(data))
        log_log.trace('LOG', to: msg.to, topics: topics, data: data)
      elsif op == OP_CREATE
        value, mstart, msz = stk.pop, stk.pop, stk.pop

        return vm_exception('OOG EXTENDING MEMORY') unless mem_extend(mem, s, mstart, msz)

        if ext.get_balance(msg.to) >= value && msg.depth < 1024
          cd = CallData.new mem, mstart, msz
          create_msg = Message.new(msg.to, Constant::BYTE_EMPTY, value, s.gas, cd, depth: msg.depth+1)

          o, gas, addr = ext.create create_msg
          if o.true?
            stk.push Utils.coerce_to_int(addr)
            s.gas = gas
          else
            stk.push 0
            s.gas = 0
          end
        else
          stk.push(0)
        end
      elsif op == OP_CALL
        gas, to, value, memin_start, memin_sz, memout_start, memout_sz = \
          stk.pop, stk.pop, stk.pop, stk.pop, stk.pop, stk.pop, stk.pop

        return vm_exception('OOG EXTENDING MEMORY') unless mem_extend(mem, s, memin_start, memin_sz)
        return vm_exception('OOG EXTENDING MEMORY') unless mem_extend(mem, s, memout_start, memout_sz)

        to = Utils.zpad_int(to)[12..-1] # last 20 bytes
        extra_gas = (ext.account_exists(to) ? 0 : 1) * Opcodes::GCALLNEWACCOUNT +
          (value > 0 ? 1 : 0) * Opcodes::GCALLVALUETRANSFER
        submsg_gas = gas + Opcodes::GSTIPEND * (value > 0 ? 1 : 0)
        total_gas = gas + extra_gas

        return vm_exception('OUT OF GAS', needed: total_gas) if s.gas < total_gas

        if ext.get_balance(msg.to) >= value && msg.depth < 1024
          s.gas -= total_gas

          cd = CallData.new mem, memin_start, memin_sz
          call_msg = Message.new(msg.to, to, value, submsg_gas, cd, depth: msg.depth+1, code_address: to)

          result, gas, data = ext.apply_msg call_msg
          if result == 0
            stk.push 0
          else
            stk.push 1
            s.gas += gas
            [data.size, memout_sz].min.times do |i|
              mem[memout_start+i] = data[i]
            end
          end
        else
          s.gas -= (total_gas - submsg_gas)
          stk.push(0)
        end
      elsif op == OP_CALLCODE
        gas, to, value, memin_start, memin_sz, memout_start, memout_sz = \
          stk.pop, stk.pop, stk.pop, stk.pop, stk.pop, stk.pop, stk.pop

        return vm_exception('OOG EXTENDING MEMORY') unless mem_extend(mem, s, memin_start, memin_sz)
        return vm_exception('OOG EXTENDING MEMORY') unless mem_extend(mem, s, memout_start, memout_sz)

        extra_gas = (value > 0 ? 1 : 0) * Opcodes::GCALLVALUETRANSFER
        submsg_gas = gas + Opcodes::GSTIPEND * (value > 0 ? 1 : 0)
        total_gas = gas + extra_gas

        return vm_exception('OUT OF GAS', needed: total_gas) if s.gas < total_gas

        if ext.get_balance(msg.to) >= value && msg.depth < 1024
          s.gas -= total_gas

          to = Utils.zpad_int(to)[12..-1] # last 20 bytes
          cd = CallData.new mem, memin_start, memin_sz

          call_msg = Message.new(msg.to, msg.to, value, submsg_gas, cd, depth: msg.depth+1, code_address: to)

          result, gas, data = ext.apply_msg call_msg
          if result == 0
            stk.push 0
          else
            stk.push 1
            s.gas += gas
            [data.size, memout_sz].min.times do |i|
              mem[memout_start+i] = data[i]
            end
          end
        else
          s.gas -= (total_gas - submsg_gas)
          stk.push(0)
        end
      elsif op == OP_RETURN
        s0, s1 = stk.pop, stk.pop
        return vm_exception('OOG EXTENDING MEMORY') unless mem_extend(mem, s, s0, s1)
        return peaceful_exit('RETURN', s.gas, mem.safe_slice(s0, s1))
      elsif op == OP_SUICIDE
        s0 = stk.pop
        to = Utils.zpad_int(s0)[12..-1] # last 20 bytes

        xfer = ext.get_balance(msg.to)
        ext.set_balance(to, ext.get_balance(to)+xfer)
        ext.set_balance(msg.to, 0)
        ext.add_suicide(msg.to)

        return 1, s.gas, []
      end


    end
  end
end
preprocess_code(code) click to toggle source
# File lib/ethereum/fast_vm.rb, line 503
def preprocess_code(code)
  code = Utils.bytes_to_int_array code

  ops = {}
  cur_chunk = []
  cc_init_pos = 0 # cc = code chunk
  cc_gas_consumption = 0
  cc_stack_change = 0
  cc_min_req_stack = 0 # minimum stack depth before code chunk start
  cc_max_req_stack = STACK_MAX # maximum stack depth before code chunk start

  i = 0
  while i < code.size
    op, in_args, out_args, fee = Opcodes::TABLE.fetch(code[i], [:INVALID, 0, 0, 0])
    opcode, pushval = code[i], 0

    if op[0,Opcodes::PREFIX_PUSH.size] == Opcodes::PREFIX_PUSH
      n = op[Opcodes::PREFIX_PUSH.size..-1].to_i
      n.times do |j|
        i += 1
        byte = i < code.size ? code[i] : 0
        pushval = (pushval << 8) + byte
      end
    end

    i += 1

    opcode = OP_INVALID if op == :INVALID

    cc_gas_consumption += fee
    cc_min_req_stack = [cc_min_req_stack, 0 - cc_stack_change + in_args].max # should leave at least in_args values in stack as arguments of this op
    cc_max_req_stack = [cc_max_req_stack, STACK_MAX - cc_stack_change + in_args - out_args].min # should leave enough stack space for code chunk use
    cc_stack_change = cc_stack_change - in_args + out_args
    cur_chunk.push(opcode + (pushval << 8))

    if END_BREAKPOINTS.include?(op) || i >= code.size ||
        START_BREAKPOINTS.include?(Opcodes::TABLE.fetch(code[i], [:INVALID])[0])
      ops[cc_init_pos] = [
        cc_gas_consumption,
        cc_min_req_stack,
        cc_max_req_stack,
        i # end position
      ] + cur_chunk

      cur_chunk = []
      cc_init_pos = i
      cc_gas_consumption = 0
      cc_stack_change = 0
      cc_min_req_stack = 0
      cc_max_req_stack = STACK_MAX
    end
  end

  ops[i] = [0, 0, STACK_MAX, [0], 0]
  ops
end

Private Instance Methods

data_copy(s, sz) click to toggle source
# File lib/ethereum/fast_vm.rb, line 613
def data_copy(s, sz)
  if sz > 0
    copyfee = Opcodes::GCOPY * Utils.ceil32(sz) / 32

    if s.gas < copyfee
      s.gas = 0
      return false
    end
    s.gas -= copyfee
  end

  true
end
log_log() click to toggle source
# File lib/ethereum/fast_vm.rb, line 570
def log_log
  @log_log ||= Logger.new 'eth.vm.log'
end
log_vm_exit() click to toggle source
# File lib/ethereum/fast_vm.rb, line 562
def log_vm_exit
  @log_vm_exit ||= Logger.new 'eth.vm.exit'
end
log_vm_op() click to toggle source
# File lib/ethereum/fast_vm.rb, line 566
def log_vm_op
  @log_vm_op ||= Logger.new 'eth.vm.op'
end
mem_extend(mem, s, start, sz) click to toggle source
# File lib/ethereum/fast_vm.rb, line 588
def mem_extend(mem, s, start, sz)
  if sz > 0 && Utils.ceil32(start + sz) > mem.size
    oldsize = mem.size / 32
    old_totalfee = mem_fee oldsize

    newsize = Utils.ceil32(start + sz) / 32
    new_totalfee = mem_fee newsize

    if old_totalfee < new_totalfee
      memfee = new_totalfee - old_totalfee

      if s.gas < memfee
        s.gas = 0
        return false
      end
      s.gas -= memfee

      m_extend = (newsize - oldsize) * 32
      mem.concat([0]*m_extend)
    end
  end

  true
end
peaceful_exit(cause, gas, data, **kwargs) click to toggle source
# File lib/ethereum/fast_vm.rb, line 581
def peaceful_exit(cause, gas, data, **kwargs)
  if log_vm_exit.trace?
    log_vm_exit.trace('EXIT', cause: cause, **kwargs)
  end
  return 1, gas, data
end
vm_exception(error, **kwargs) click to toggle source
# File lib/ethereum/fast_vm.rb, line 574
def vm_exception(error, **kwargs)
  if log_vm_exit.trace?
    log_vm_exit.trace('EXCEPTION', cause: error, **kwargs)
  end
  return 0, 0, []
end