diff --git a/lib/ciri/evm.rb b/lib/ciri/evm.rb index 0cf968b..9e63d59 100644 --- a/lib/ciri/evm.rb +++ b/lib/ciri/evm.rb @@ -78,7 +78,7 @@ def transition(block, check_gas_limit: true, check_gas_used: true) end fork_config = Ciri::Forks.detect_fork(header: block.header, number: block.header.number) - rewards = fork_config.mining_rewards[block] + rewards = fork_config.mining_rewards_of_block(block) # apply rewards rewards.each do |address, value| @@ -104,7 +104,7 @@ def execute_transaction(t, header: nil, block_info: nil, ignore_exception: false state.add_balance(t.sender, -1 * t.gas_limit * t.gas_price) fork_config = Ciri::Forks.detect_fork(header: header, number: block_info&.number) - gas_limit = t.gas_limit - fork_config.intrinsic_gas_of_transaction[t] + gas_limit = t.gas_limit - fork_config.intrinsic_gas_of_transaction(t) instruction = Instruction.new( origin: t.sender, @@ -149,7 +149,7 @@ def execute_transaction(t, header: nil, block_info: nil, ignore_exception: false raise exception if !ignore_exception && exception # refund gas - refund_gas = fork_config.refund_gas[t, @vm] + refund_gas = fork_config.calculate_refund_gas(@vm) gas_used = t.gas_limit - @vm.remain_gas refund_gas = [refund_gas, gas_used / 2].min state.add_balance(t.sender, (refund_gas + @vm.remain_gas) * t.gas_price) diff --git a/lib/ciri/evm/forks/frontier.rb b/lib/ciri/evm/forks/frontier.rb deleted file mode 100644 index cfcc6f4..0000000 --- a/lib/ciri/evm/forks/frontier.rb +++ /dev/null @@ -1,183 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) 2018, by Jiang Jinyang. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - - -require 'ciri/evm/op' - -module Ciri - class EVM - module Forks - module Frontier - - module Cost - # fee schedule, start with G - G_ZERO = 0 - G_BASE = 2 - G_VERYLOW = 3 - G_LOW = 5 - G_MID = 8 - G_HIGH = 10 - G_EXTCODE = 700 - G_BALANCE = 400 - G_SLOAD = 50 - G_JUMPDEST = 1 - G_SSET = 20000 - G_RESET = 5000 - R_SCLEAR = 15000 - R_SELFDESTRUCT = 24000 - G_SELFDESTRUCT = 0 - G_CREATE = 32000 - G_CODEDEPOSIT = 200 - G_CALL = 700 - G_CALLVALUE = 9000 - G_CALLSTIPEND = 2300 - G_NEWACCOUNT = 25000 - G_EXP = 10 - G_EXPBYTE = 10 - G_MEMORY = 3 - G_TXCREATE = 32000 - G_TXDATAZERO = 4 - G_TXDATANONZERO = 68 - G_TRANSACTION = 21000 - G_LOG = 375 - G_LOGDATA = 8 - G_TOPIC = 375 - G_SHA3 = 30 - G_SHA3WORD = 6 - G_COPY = 3 - G_BLOCKHASH = 20 - G_QUADDIVISOR = 100 - - # operation code by group, for later calculation - W_ZERO = [OP::STOP, OP::RETURN, OP::REVERT] - W_BASE = [OP::ADDRESS, OP::ORIGIN, OP::CALLER, OP::CALLVALUE, OP::CALLDATASIZE, OP::CODESIZE, OP::GASPRICE, - OP::COINBASE, OP::TIMESTAMP, OP::NUMBER, OP::DIFFICULTY, OP::GASLIMIT, OP::RETURNDATASIZE, - OP::POP, OP::PC, OP::MSIZE, OP::GAS] - W_VERYLOW = [OP::ADD, OP::SUB, OP::NOT, OP::LT, OP::GT, OP::SLT, OP::SGT, OP::EQ, OP::ISZERO, OP::AND, OP::OR, - OP::XOR, OP::BYTE, OP::CALLDATALOAD, OP::MLOAD, OP::MSTORE, OP::MSTORE8, - *(1..32).map {|i| OP.get(OP::PUSH1 + i - 1).code}, # push1 - push32 - *(1..16).map {|i| OP.get(OP::DUP1 + i - 1).code}, # dup1 - dup16 - *(1..16).map {|i| OP.get(OP::SWAP1 + i - 1).code}] # swap1 - swap16 - W_LOW = [OP::MUL, OP::DIV, OP::SDIV, OP::MOD, OP::SMOD, OP::SIGNEXTEND] - W_MID = [OP::ADDMOD, OP::MULMOD, OP::JUMP] - W_HIGH = [OP::JUMPI] - W_EXTCODE = [OP::EXTCODESIZE] - - - class << self - # C(σ,μ,I) - # calculate cost of current operation - def cost_of_operation(vm) - ms = vm.machine_state - instruction = vm.instruction - w = instruction.get_op(ms.pc) - if w == OP::SSTORE - cost_of_sstore(vm) - elsif w == OP::EXP && ms.get_stack(1, Integer) == 0 - G_EXP - elsif w == OP::EXP && (x = ms.get_stack(1, Integer)) > 0 - G_EXP + G_EXPBYTE * Utils.ceil_div(x.bit_length, 8) - elsif w == OP::CALLDATACOPY || w == OP::CODECOPY || w == OP::RETURNDATACOPY - G_VERYLOW + G_COPY * Utils.ceil_div(ms.get_stack(2, Integer), 32) - elsif w == OP::EXTCODECOPY - G_EXTCODE + G_COPY * Utils.ceil_div(ms.get_stack(3, Integer), 32) - elsif (OP::LOG0..OP::LOG4).include? w - G_LOG + G_LOGDATA * ms.get_stack(1, Integer) + (w - OP::LOG0) * G_TOPIC - elsif w == OP::CALL || w == OP::CALLCODE || w == OP::DELEGATECALL - 1 # cost_of_call(state, ms) - elsif w == OP::SELFDESTRUCT - cost_of_self_destruct(vm) - elsif w == OP::CREATE - G_CREATE - elsif w == OP::SHA3 - G_SHA3 + G_SHA3WORD * Utils.ceil_div(ms.get_stack(1, Integer), 32) - elsif w == OP::JUMPDEST - G_JUMPDEST - elsif w == OP::SLOAD - G_SLOAD - elsif W_ZERO.include? w - G_ZERO - elsif W_BASE.include? w - G_BASE - elsif W_VERYLOW.include? w - G_VERYLOW - elsif W_LOW.include? w - G_LOW - elsif W_MID.include? w - G_MID - elsif W_HIGH.include? w - G_HIGH - elsif W_EXTCODE.include? w - G_EXTCODE - elsif w == OP::BALANCE - G_BALANCE - elsif w == OP::BLOCKHASH - G_BLOCKHASH - else - raise "can't compute cost for unknown op #{w}" - end - end - - def cost_of_memory(i) - G_MEMORY * i + (i ** 2) / 512 - end - - def intrinsic_gas_of_transaction(t) - gas = (t.data.each_byte || '').reduce(0) {|sum, i| sum + (i.zero? ? G_TXDATAZERO : G_TXDATANONZERO)} - # gas + (t.to.empty? ? G_TXCREATE : 0) + G_TRANSACTION - gas + G_TRANSACTION - end - - private - - def cost_of_self_destruct(vm) - G_SELFDESTRUCT - end - - def cost_of_call - - end - - def cost_of_sstore(vm) - ms = vm.machine_state - instruction = vm.instruction - - key = ms.get_stack(0, Integer) - value = ms.get_stack(1, Integer) - - stored_is_empty = vm.fetch(instruction.address, key).zero? - value_is_non_zero = value && !value.zero? - - if value_is_non_zero && stored_is_empty - G_SSET - else - G_RESET - end - end - - end - end - - end - end - end -end \ No newline at end of file diff --git a/lib/ciri/evm/vm.rb b/lib/ciri/evm/vm.rb index 58e15f4..833698c 100644 --- a/lib/ciri/evm/vm.rb +++ b/lib/ciri/evm/vm.rb @@ -131,7 +131,7 @@ def create_contract(value:, init:) call_instruction(create_contract_instruction) do execute - deposit_code_gas = fork_config.deposit_code_fee[output] + deposit_code_gas = fork_config.calculate_deposit_code_gas(output) if deposit_code_gas > remain_gas # deposit_code_gas not enough @@ -270,8 +270,8 @@ def operate raise "can't find operation #{w}, pc #{ms.pc}" unless operation - op_cost = fork_config.cost_of_operation[self] - old_memory_cost = fork_config.cost_of_memory[ms.memory_item] + op_cost = fork_config.gas_of_operation(self) + old_memory_cost = fork_config.gas_of_memory(ms.memory_item) ms.consume_gas op_cost prev_sub_state = sub_state.dup @@ -279,7 +279,7 @@ def operate # call operation operation.call(self) # calculate gas_cost - new_memory_cost = fork_config.cost_of_memory[ms.memory_item] + new_memory_cost = fork_config.gas_of_memory(ms.memory_item) memory_gas_cost = new_memory_cost - old_memory_cost if ms.remain_gas >= memory_gas_cost @@ -332,7 +332,7 @@ def check_exception(state, ms, instruction) InvalidOpCodeError.new "can't find op code #{w}" when ms.stack.size < (consume = OP.input_count(w)) StackError.new "stack not enough: stack:#{ms.stack.size} next consume: #{consume}" - when ms.remain_gas < (gas_cost = fork_config.cost_of_operation[self]) + when ms.remain_gas < (gas_cost = fork_config.gas_of_operation(self)) GasNotEnoughError.new "gas not enough: gas remain:#{ms.remain_gas} gas cost: #{gas_cost}" when w == OP::JUMP && instruction.destinations.include?(ms.get_stack(0, Integer)) InvalidJumpError.new "invalid jump dest #{ms.get_stack(0, Integer)}" diff --git a/lib/ciri/forks.rb b/lib/ciri/forks.rb index 5ca7264..5017d43 100644 --- a/lib/ciri/forks.rb +++ b/lib/ciri/forks.rb @@ -26,20 +26,9 @@ module Ciri module Forks - # Fork configure - ForkConfig = Struct.new( - :cost_of_operation, - :cost_of_memory, - :intrinsic_gas_of_transaction, - :deposit_code_fee, - :mining_rewards, - :refund_gas, - keyword_init: true - ) - def self.detect_fork(header: nil, number: nil) number ||= header.number - Frontier.fork_config + Frontier::Config.new end end diff --git a/lib/ciri/forks/base.rb b/lib/ciri/forks/base.rb new file mode 100644 index 0000000..70cd398 --- /dev/null +++ b/lib/ciri/forks/base.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# Copyright (c) 2018, by Jiang Jinyang. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +module Ciri + module Forks + + class Base + # gas methods + def gas_of_operation(vm) + raise NotImplementedError + end + + def gas_of_memory(word_count) + raise NotImplementedError + end + + def intrinsic_gas_of_transaction(transaction) + raise NotImplementedError + end + + def calculate_deposit_code_gas(code_bytes) + raise NotImplementedError + end + + def mining_rewards_of_block(block) + raise NotImplementedError + end + + def calculate_refund_gas(vm) + raise NotImplementedError + end + end + + end +end diff --git a/lib/ciri/forks/frontier.rb b/lib/ciri/forks/frontier.rb index b6b569a..00439af 100644 --- a/lib/ciri/forks/frontier.rb +++ b/lib/ciri/forks/frontier.rb @@ -21,43 +21,50 @@ # THE SOFTWARE. -require 'ciri/evm/forks/frontier' +require_relative 'base' +require_relative 'frontier/cost' module Ciri module Forks module Frontier + class Config < Base - extend self - - def fork_config - ForkConfig.new( - cost_of_operation: proc {|vm| EVM::Forks::Frontier::Cost.cost_of_operation vm}, - cost_of_memory: proc {|i| EVM::Forks::Frontier::Cost.cost_of_memory i}, - intrinsic_gas_of_transaction: proc {|t| EVM::Forks::Frontier::Cost.intrinsic_gas_of_transaction t}, - deposit_code_fee: proc {|code| EVM::Forks::Frontier::Cost::G_CODEDEPOSIT * (code || ''.b).size}, - mining_rewards: method(:mining_rewards).to_proc, - refund_gas: method(:refund_gas).to_proc - ) - end + BLOCK_REWARD = 5 * 10.pow(18) # 5 ether - def refund_gas(t, vm) - vm.sub_state.suicide_accounts.size * EVM::Forks::Frontier::Cost::R_SELFDESTRUCT - end + # gas methods + def gas_of_operation(vm) + Cost.cost_of_operation vm + end - BLOCK_REWARD = 5 * 10.pow(18) # 5 ether + def gas_of_memory(word_count) + Cost.cost_of_memory word_count + end - def mining_rewards(block) - rewards = Hash.new(0) - # reward miner - rewards[block.header.beneficiary] += ((1 + block.ommers.count.to_f / 32) * BLOCK_REWARD).to_i + def intrinsic_gas_of_transaction(transaction) + Cost.intrinsic_gas_of_transaction transaction + end - # reward ommer(uncle) block miners - block.ommers.each do |ommer| - rewards[ommer.beneficiary] += ((1 + (ommer.number - block.header.number).to_f / 8) * BLOCK_REWARD).to_i + def calculate_deposit_code_gas(code_bytes) + Cost::G_CODEDEPOSIT * (code_bytes || ''.b).size end - rewards - end + def calculate_refund_gas(vm) + vm.sub_state.suicide_accounts.size * Cost::R_SELFDESTRUCT + end + + def mining_rewards_of_block(block) + rewards = Hash.new(0) + # reward miner + rewards[block.header.beneficiary] += ((1 + block.ommers.count.to_f / 32) * BLOCK_REWARD).to_i + + # reward ommer(uncle) block miners + block.ommers.each do |ommer| + rewards[ommer.beneficiary] += ((1 + (ommer.number - block.header.number).to_f / 8) * BLOCK_REWARD).to_i + end + rewards + end + + end end end end diff --git a/lib/ciri/forks/frontier/cost.rb b/lib/ciri/forks/frontier/cost.rb new file mode 100644 index 0000000..3e6d0bd --- /dev/null +++ b/lib/ciri/forks/frontier/cost.rb @@ -0,0 +1,186 @@ +# frozen_string_literal: true + +# Copyright (c) 2018, by Jiang Jinyang. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +require 'ciri/evm/op' + +module Ciri + module Forks + module Frontier + + module Cost + + include Ciri::EVM::OP + + # fee schedule, start with G + G_ZERO = 0 + G_BASE = 2 + G_VERYLOW = 3 + G_LOW = 5 + G_MID = 8 + G_HIGH = 10 + G_EXTCODE = 700 + G_BALANCE = 400 + G_SLOAD = 50 + G_JUMPDEST = 1 + G_SSET = 20000 + G_RESET = 5000 + R_SCLEAR = 15000 + R_SELFDESTRUCT = 24000 + G_SELFDESTRUCT = 0 + G_CREATE = 32000 + G_CODEDEPOSIT = 200 + G_CALL = 700 + G_CALLVALUE = 9000 + G_CALLSTIPEND = 2300 + G_NEWACCOUNT = 25000 + G_EXP = 10 + G_EXPBYTE = 10 + G_MEMORY = 3 + G_TXCREATE = 32000 + G_TXDATAZERO = 4 + G_TXDATANONZERO = 68 + G_TRANSACTION = 21000 + G_LOG = 375 + G_LOGDATA = 8 + G_TOPIC = 375 + G_SHA3 = 30 + G_SHA3WORD = 6 + G_COPY = 3 + G_BLOCKHASH = 20 + G_QUADDIVISOR = 100 + + # operation code by group, for later calculation + W_ZERO = [STOP, RETURN, REVERT] + W_BASE = [ADDRESS, ORIGIN, CALLER, CALLVALUE, CALLDATASIZE, CODESIZE, GASPRICE, + COINBASE, TIMESTAMP, NUMBER, DIFFICULTY, GASLIMIT, RETURNDATASIZE, + POP, PC, MSIZE, GAS] + W_VERYLOW = [ADD, SUB, NOT, LT, GT, SLT, SGT, EQ, ISZERO, AND, OR, + XOR, BYTE, CALLDATALOAD, MLOAD, MSTORE, MSTORE8, + *(1..32).map {|i| EVM::OP.get(PUSH1 + i - 1).code}, # push1 - push32 + *(1..16).map {|i| EVM::OP.get(DUP1 + i - 1).code}, # dup1 - dup16 + *(1..16).map {|i| EVM::OP.get(SWAP1 + i - 1).code}] # swap1 - swap16 + W_LOW = [MUL, DIV, SDIV, MOD, SMOD, SIGNEXTEND] + W_MID = [ADDMOD, MULMOD, JUMP] + W_HIGH = [JUMPI] + W_EXTCODE = [EXTCODESIZE] + + + class << self + include Ciri::EVM::OP + + # C(σ,μ,I) + # calculate cost of current operation + def cost_of_operation(vm) + ms = vm.machine_state + instruction = vm.instruction + w = instruction.get_op(ms.pc) + if w == SSTORE + cost_of_sstore(vm) + elsif w == EXP && ms.get_stack(1, Integer) == 0 + G_EXP + elsif w == EXP && (x = ms.get_stack(1, Integer)) > 0 + G_EXP + G_EXPBYTE * Utils.ceil_div(x.bit_length, 8) + elsif w == CALLDATACOPY || w == CODECOPY || w == RETURNDATACOPY + G_VERYLOW + G_COPY * Utils.ceil_div(ms.get_stack(2, Integer), 32) + elsif w == EXTCODECOPY + G_EXTCODE + G_COPY * Utils.ceil_div(ms.get_stack(3, Integer), 32) + elsif (LOG0..LOG4).include? w + G_LOG + G_LOGDATA * ms.get_stack(1, Integer) + (w - LOG0) * G_TOPIC + elsif w == CALL || w == CALLCODE || w == DELEGATECALL + 1 # cost_of_call(state, ms) + elsif w == SELFDESTRUCT + cost_of_self_destruct(vm) + elsif w == CREATE + G_CREATE + elsif w == SHA3 + G_SHA3 + G_SHA3WORD * Utils.ceil_div(ms.get_stack(1, Integer), 32) + elsif w == JUMPDEST + G_JUMPDEST + elsif w == SLOAD + G_SLOAD + elsif W_ZERO.include? w + G_ZERO + elsif W_BASE.include? w + G_BASE + elsif W_VERYLOW.include? w + G_VERYLOW + elsif W_LOW.include? w + G_LOW + elsif W_MID.include? w + G_MID + elsif W_HIGH.include? w + G_HIGH + elsif W_EXTCODE.include? w + G_EXTCODE + elsif w == BALANCE + G_BALANCE + elsif w == BLOCKHASH + G_BLOCKHASH + else + raise "can't compute cost for unknown op #{w}" + end + end + + def cost_of_memory(i) + G_MEMORY * i + (i ** 2) / 512 + end + + def intrinsic_gas_of_transaction(t) + gas = (t.data.each_byte || '').reduce(0) {|sum, i| sum + (i.zero? ? G_TXDATAZERO : G_TXDATANONZERO)} + # gas + (t.to.empty? ? G_TXCREATE : 0) + G_TRANSACTION + gas + G_TRANSACTION + end + + private + + def cost_of_self_destruct(vm) + G_SELFDESTRUCT + end + + def cost_of_call + + end + + def cost_of_sstore(vm) + ms = vm.machine_state + instruction = vm.instruction + + key = ms.get_stack(0, Integer) + value = ms.get_stack(1, Integer) + + stored_is_empty = vm.fetch(instruction.address, key).zero? + value_is_non_zero = value && !value.zero? + + if value_is_non_zero && stored_is_empty + G_SSET + else + G_RESET + end + end + + end + end + + end + end +end diff --git a/spec/ciri/fixtures_tests/evm_spec.rb b/spec/ciri/fixtures_tests/evm_spec.rb index e12ad80..cbb41da 100644 --- a/spec/ciri/fixtures_tests/evm_spec.rb +++ b/spec/ciri/fixtures_tests/evm_spec.rb @@ -83,7 +83,7 @@ timestamp: env['currentTimestamp'], ) - fork_config = Ciri::Forks::Frontier.fork_config + fork_config = Ciri::Forks::Frontier::Config.new vm = Ciri::EVM::VM.new(state: state, machine_state: ms, instruction: instruction, block_info: block_info, fork_config: fork_config, burn_gas_on_exception: false)