From 90c3ca4a9679286dc99e310aac6dbf7e0f6ad077 Mon Sep 17 00:00:00 2001 From: Mamy Ratsimbazafy Date: Tue, 12 Jun 2018 17:33:47 +0200 Subject: [PATCH] Gas refactoring - decouple opcode logic and gas (#49) * Decoupling op logic and gas - introduce gasometer, rework opcode declaration * Remove gas constants for gas opcode computation * Remove gas constants for precompiled contracts * make vm_types compile * Make opcode, call and computation compile * Distinguish between dynamic and complex gas costs, fix arithmetic * Fix context and sha3 * update memory and storage ops * Log opcode uses memory expansion code * update/stub system_ops with gas costs * Make test compile. Deactivate stub test_vm * all tests compiles, opcode fails due to https://github.com/nim-lang/Nim/issues/8007 (const object variant in tables reset at runtime) * Create an enum without holes - workaround: https://github.com/nim-lang/Nim/issues/8007 * Use arrays instead of tables for GasCosts, remove some unused imports - passing all basic tests! * Make test_vm_json compile * Fix test_vm_json - workaround https://github.com/nim-lang/Nim/issues/8015 * fix memory expansion cost bug * Remove leftover special handling from before GckMemExpansion * cleanup outdated comment, better align = * Fix sha3 gas cost not taking memory expansion into account * Improve gas error reporting of test_vm_json * Fix gas computation regression due to mem expansion * mass replace for memExpansion->RequestedMemSize was too eager * fix log gas cost (no tests :/) * missed a static FeeSchedule * static as expression is fickle --- VMTests.md | 24 +- nimbus/computation.nim | 40 +- nimbus/constants.nim | 58 --- nimbus/db/db_chain.nim | 2 +- nimbus/db/state_db.nim | 2 +- nimbus/logic/arithmetic.nim | 8 +- nimbus/logic/call.nim | 77 +--- nimbus/logic/context.nim | 51 ++- nimbus/logic/logging_ops.nim | 14 +- nimbus/logic/memory_ops.nim | 23 +- nimbus/logic/sha3.nim | 20 +- nimbus/logic/storage.nim | 14 +- nimbus/logic/system_ops.nim | 37 +- nimbus/opcode.nim | 61 +-- nimbus/opcode_table.nim | 248 +++++++----- nimbus/opcode_values.nim | 308 +++++++-------- nimbus/utils/macros_gen_opcodes.nim | 126 +++++++ nimbus/utils_numeric.nim | 37 +- nimbus/vm/forks/gas_costs.nim | 563 ++++++++++++++++++++++++++-- nimbus/vm_types.nim | 44 +-- tests/all_tests.nim | 4 +- tests/test_opcode.nim | 2 +- tests/test_vm.nim | 2 +- tests/test_vm_json.nim | 8 +- 24 files changed, 1137 insertions(+), 636 deletions(-) create mode 100644 nimbus/utils/macros_gen_opcodes.nim diff --git a/VMTests.md b/VMTests.md index 4866d9193..e75c5ee84 100644 --- a/VMTests.md +++ b/VMTests.md @@ -9,7 +9,7 @@ VMTests + add4.json OK + addmod0.json OK + addmod1.json OK -+ addmod1_overflow2.json OK +- addmod1_overflow2.json Fail + addmod1_overflow3.json OK + addmod1_overflow4.json OK + addmod1_overflowDiff.json OK @@ -135,7 +135,7 @@ VMTests + mulUnderFlow.json OK + mulmod0.json OK + mulmod1.json OK -+ mulmod1_overflow.json OK +- mulmod1_overflow.json Fail + mulmod1_overflow2.json OK + mulmod1_overflow3.json OK + mulmod1_overflow4.json OK @@ -177,7 +177,7 @@ VMTests + signextend_BitIsNotSet.json OK + signextend_BitIsNotSetInHigherByte.json OK + signextend_BitIsSetInHigherByte.json OK -- signextend_Overflow_dj42.json Fail ++ signextend_Overflow_dj42.json OK + signextend_bigBytePlus1.json OK + signextend_bitIsSet.json OK + smod0.json OK @@ -198,7 +198,7 @@ VMTests + sub3.json OK + sub4.json OK ``` -OK: 190/195 Fail: 4/195 Skip: 1/195 +OK: 189/195 Fail: 5/195 Skip: 1/195 ## vmBitwiseLogicOperation ```diff + and0.json OK @@ -265,20 +265,20 @@ OK: 190/195 Fail: 4/195 Skip: 1/195 OK: 60/60 Fail: 0/60 Skip: 0/60 ## vmBlockInfoTest ```diff -- blockhash257Block.json Fail -- blockhash258Block.json Fail -- blockhashInRange.json Fail -- blockhashMyBlock.json Fail -- blockhashNotExistingBlock.json Fail -- blockhashOutOfRange.json Fail ++ blockhash257Block.json OK ++ blockhash258Block.json OK ++ blockhashInRange.json OK ++ blockhashMyBlock.json OK ++ blockhashNotExistingBlock.json OK ++ blockhashOutOfRange.json OK + blockhashUnderFlow.json OK -- coinbase.json Fail ++ coinbase.json OK + difficulty.json OK + gaslimit.json OK + number.json OK + timestamp.json OK ``` -OK: 5/12 Fail: 7/12 Skip: 0/12 +OK: 12/12 Fail: 0/12 Skip: 0/12 ## vmEnvironmentalInfo ```diff ExtCodeSizeAddressInputTooBigLeftMyAddress.json Skip diff --git a/nimbus/computation.nim b/nimbus/computation.nim index 1186ee51f..956b5361e 100644 --- a/nimbus/computation.nim +++ b/nimbus/computation.nim @@ -6,25 +6,15 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import - strformat, strutils, sequtils, tables, macros, stint, terminal, math, eth_common, byteutils, - constants, errors, utils/hexadecimal, utils_numeric, validation, vm_state, logging, opcode_values, vm_types, - vm / [code_stream, gas_meter, memory, message, stack], + strformat, strutils, sequtils, macros, stint, terminal, math, eth_common, byteutils, tables, + ./constants, ./errors, ./utils/hexadecimal, ./utils_numeric, ./validation, ./vm_state, ./logging, ./opcode_values, ./vm_types, + ./vm/[code_stream, gas_meter, memory, message, stack], # TODO further refactoring of gas cost vm/forks/gas_costs, vm/forks/f20150730_frontier/frontier_vm_state, vm/forks/f20161018_tangerine_whistle/tangerine_vm_state -proc memoryGasCost*(sizeInBytes: Natural): GasInt = - let - sizeInWords = ceil32(sizeInBytes) div 32 - linearCost = sizeInWords * GAS_MEMORY - quadraticCost = sizeInWords ^ 2 div GAS_MEMORY_QUADRATIC_DENOMINATOR - totalCost = linearCost + quadraticCost - result = totalCost - -#const VARIABLE_GAS_COST_OPS* = {Op.Exp} - method newBaseComputation*(vmState: BaseVMState, message: Message): BaseComputation {.base.}= raise newException(ValueError, "Must be implemented by subclasses") @@ -105,26 +95,6 @@ method prepareChildMessage*( code, childOptions) -method extendMemory*(c: var BaseComputation, startPosition: Natural, size: Natural) = - # Memory Management - - let beforeSize = ceil32(len(c.memory)) - let afterSize = ceil32(startPosition + size) - - let beforeCost = memoryGasCost(beforeSize) - let afterCost = memoryGasCost(afterSize) - - c.logger.debug(&"MEMORY: size ({beforeSize} -> {afterSize}) | cost ({beforeCost} -> {afterCost})") - - if size > 0: - if beforeCost < afterCost: - var gasFee = afterCost - beforeCost - c.gasMeter.consumeGas( - gasFee, - reason = &"Expanding memory {beforeSize} -> {afterSize}") - - c.memory.extend(startPosition, size) - method output*(c: BaseComputation): string = if c.shouldEraseReturnData: "" @@ -261,10 +231,10 @@ template inComputation*(c: untyped, handler: untyped): untyped = c.gasMeter.gasRemaining, reason="Zeroing gas due to VM Exception: $1" % getCurrentExceptionMsg()) - method getOpcodeFn*(computation: var BaseComputation, op: Op): Opcode = + # TODO use isValidOpcode and remove the Op --> Opcode indirection if computation.opcodes.len > 0 and computation.opcodes.hasKey(op): - computation.opcodes[op] + OpCode(kind: op, runLogic: computation.opcodes[op]) else: raise newException(InvalidInstruction, &"Invalid opcode {op}") diff --git a/nimbus/constants.nim b/nimbus/constants.nim index db06182de..2c19b8699 100644 --- a/nimbus/constants.nim +++ b/nimbus/constants.nim @@ -114,66 +114,8 @@ let ZERO_HASH32* = Hash256() STACK_DEPTH_LIMIT* = 1024 - # GAS_NULL* = 0.u256 - # GAS_ZERO* = 0.u256 - # GAS_BASE* = 2.u256 - # GAS_VERY_LOW* = 3.u256 - # GAS_LOW* = 5.u256 - # GAS_MID* = 8.u256 - # GAS_HIGH* = 10.u256 - # GAS_EXT_CODE* = 20.u256 # TODO: this is pre-eip150, see also GAS_EXT_CODE_COST - # GAS_BALANCE* = 20.u256 # TODO: this is pre-eip150, see also GAS_COST_BALANCE - # GAS_SLOAD* = 200.u256 # TODO: pre eip150 - # GAS_JUMP_DEST* = 1.u256 - # GAS_SSET* = 20_000.u256 - # GAS_SRESET* = 5_000.u256 - # GAS_EXT_CODE_COST* = 700.u256 - # GAS_COINBASE* = 20.u256 - # GAS_SLOAD_COST* = 20.u256 - # GAS_SELF_DESTRUCT_COST* = 0.u256 - # GAS_IN_HANDLER* = 0.u256 # to be calculated in handler - REFUND_SCLEAR* = 15_000 - - # GAS_SELF_DESTRUCT* = 0.u256 - # GAS_SELF_DESTRUCT_NEW_ACCOUNT* = 25_000.u256 - GAS_CREATE* = 32_000.u256 - # GAS_CALL* = 40.u256 - GAS_CALL_VALUE* = 9_000 - GAS_CALL_STIPEND* = 2_300 - GAS_NEW_ACCOUNT* = 25_000 - - # GAS_COST_BALANCE* = 400.u256 # TODO: this is post-eip150, see also GAS_BALANCE - - # GAS_EXP* = 10.u256 - # GAS_EXP_BYTE* = 10.u256 - GAS_MEMORY* = 3 - GAS_TX_CREATE* = 32_000.u256 - GAS_TX_DATA_ZERO* = 4.u256 - GAS_TX_DATA_NON_ZERO* = 68.u256 - GAS_TX* = 21_000 - GAS_LOG* = 375.u256 - GAS_LOG_DATA* = 8 - GAS_LOG_TOPIC* = 375 - # GAS_SHA3* = 30.u256 - GAS_SHA3_WORD* = 6 - GAS_COPY* = 3 - GAS_BLOCK_HASH* = 20.u256 - GAS_CODE_DEPOSIT* = 200.u256 - GAS_MEMORY_QUADRATIC_DENOMINATOR* = 512 - GAS_SHA256* = 60.u256 - GAS_SHA256WORD* = 12.u256 - GAS_RIP_EMD160* = 600.u256 - GAS_RIP_EMD160WORD* = 120.u256 - GAS_IDENTITY* = 15.u256 - GAS_IDENTITY_WORD* = 3 - GAS_ECRECOVER* = 3_000.u256 - GAS_ECADD* = 500.u256 - GAS_ECMUL* = 40_000.u256 - GAS_ECPAIRING_BASE* = 100_000.u256 - GAS_ECPAIRING_PER_POINT* = 80_000.u256 GAS_LIMIT_EMA_DENOMINATOR* = 1_024 GAS_LIMIT_ADJUSTMENT_FACTOR* = 1_024 - GAS_LIMIT_MAXIMUM* = high(GasInt) GAS_LIMIT_USAGE_ADJUSTMENT_NUMERATOR* = 3 GAS_LIMIT_USAGE_ADJUSTMENT_DENOMINATOR* = 2 diff --git a/nimbus/db/db_chain.nim b/nimbus/db/db_chain.nim index e8f360450..96701c199 100644 --- a/nimbus/db/db_chain.nim +++ b/nimbus/db/db_chain.nim @@ -5,7 +5,7 @@ # * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) # at your option. This file may not be copied, modified, or distributed except according to those terms. -import strformat, tables, stint, rlp, ranges, state_db, backends / memory_backend, +import stint, tables, rlp, ranges, state_db, backends / memory_backend, ../errors, ../utils/header, ../constants, eth_common, byteutils type diff --git a/nimbus/db/state_db.nim b/nimbus/db/state_db.nim index 294d3a3b6..49112cfbe 100644 --- a/nimbus/db/state_db.nim +++ b/nimbus/db/state_db.nim @@ -6,7 +6,7 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import - strformat, tables, eth_common, + eth_common, tables, ../constants, ../errors, ../validation, ../account, ../logging, ../utils_numeric, .. / utils / [padding, bytes, keccak], stint, rlp diff --git a/nimbus/logic/arithmetic.nim b/nimbus/logic/arithmetic.nim index 919d33120..5212425d6 100644 --- a/nimbus/logic/arithmetic.nim +++ b/nimbus/logic/arithmetic.nim @@ -90,10 +90,10 @@ proc exp*(computation: var BaseComputation) = # Exponentiation let (base, exponent) = computation.stack.popInt(2) - var gasCost = computation.gasCosts[GasExp] - if not exponent.isZero: - gasCost += gasCost * (1 + log256(exponent)) - computation.gasMeter.consumeGas(gasCost, reason="EXP: exponent bytes") + computation.gasMeter.consumeGas( + computation.gasCosts[Exp].d_handler(exponent), + reason="EXP: exponent bytes" + ) let res = if base.isZero: 0.u256 # 0^0 is 0 in py-evm else: base.pow(exponent) diff --git a/nimbus/logic/call.nim b/nimbus/logic/call.nim index 3bd8b574f..9ddb7bbdd 100644 --- a/nimbus/logic/call.nim +++ b/nimbus/logic/call.nim @@ -39,20 +39,10 @@ type using computation: var BaseComputation -method msgExtraGas*(call: BaseCall, computation; gas: GasInt, to: EthAddress, value: UInt256): GasInt {.base.} = - raise newException(NotImplementedError, "Must be implemented by subclasses") - -method msgGas*(call: BaseCall, computation; gas: GasInt, to: EthAddress, value: UInt256): (GasInt, GasInt) {.base.} = - let extraGas = call.msgExtraGas(computation, gas, to, value) - let totalFee = gas + extraGas - let childMsgGas = gas + (if value != 0: GAS_CALL_STIPEND else: 0) - (childMsgGas, totalFee) - method callParams*(call: BaseCall, computation): (UInt256, UInt256, EthAddress, EthAddress, EthAddress, UInt256, UInt256, UInt256, UInt256, bool, bool) {.base.} = raise newException(NotImplementedError, "Must be implemented subclasses") method runLogic*(call: BaseCall, computation) = - computation.gasMeter.consumeGas(computation.gasCosts[call.gasCostKind], reason = $call.kind) # TODO: Refactoring call gas costs let (gas, value, to, sender, codeAddress, memoryInputStartPosition, memoryInputSize, @@ -62,12 +52,15 @@ method runLogic*(call: BaseCall, computation) = let (memInPos, memInLen, memOutPos, memOutLen) = (memoryInputStartPosition.toInt, memoryInputSize.toInt, memoryOutputStartPosition.toInt, memoryOutputSize.toInt) - computation.extendMemory(memInPos, memInLen) - computation.extendMemory(memOutPos, memOutLen) + let (gasCost, childMsgGas) = computation.gasCosts[Op.Call].c_handler( + value, + GasParams() # TODO - stub + ) + + computation.memory.extend(memInPos, memInLen) + computation.memory.extend(memOutPos, memOutLen) let callData = computation.memory.read(memInPos, memInLen) - let (childMsgGas, childMsgGasFee) = call.msgGas(computation, gas.toInt, to, value) - computation.gasMeter.consumeGas(childMsgGasFee, reason = $call.kind) # TODO: Pre-call checks # with computation.vm_state.state_db(read_only=True) as state_db: @@ -125,16 +118,6 @@ method runLogic*(call: BaseCall, computation) = if not childComputation.shouldBurnGas: computation.gasMeter.returnGas(childComputation.gasMeter.gasRemaining) -method msgExtraGas(call: Call, computation; gas: GasInt, to: EthAddress, value: UInt256): GasInt = - # TODO: db - # with computation.vm_state.state_db(read_only=True) as state_db: - # let accountExists = db.accountExists(to) - let accountExists = false - - let transferGasFee = if value != 0: GAS_CALL_VALUE else: 0 - let createGasFee = if not accountExists: GAS_NEW_ACCOUNT else: 0 - transferGasFee + createGasFee - method callParams(call: CallCode, computation): (UInt256, UInt256, EthAddress, EthAddress, EthAddress, UInt256, UInt256, UInt256, UInt256, bool, bool) = let gas = computation.stack.popInt() let to = computation.stack.popAddress() @@ -155,9 +138,6 @@ method callParams(call: CallCode, computation): (UInt256, UInt256, EthAddress, E true, # should_transfer_value, computation.msg.isStatic) -method msgExtraGas(call: CallCode, computation; gas: GasInt, to: EthAddress, value: UInt256): GasInt = - if value != 0: GAS_CALL_VALUE else: 0 - method callParams(call: Call, computation): (UInt256, UInt256, EthAddress, EthAddress, EthAddress, UInt256, UInt256, UInt256, UInt256, bool, bool) = let gas = computation.stack.popInt() let codeAddress = computation.stack.popAddress() @@ -181,12 +161,6 @@ method callParams(call: Call, computation): (UInt256, UInt256, EthAddress, EthAd true, # should_transfer_value, computation.msg.isStatic) -method msgGas(call: DelegateCall, computation; gas: GasInt, to: EthAddress, value: UInt256): (GasInt, GasInt) = - (gas, gas) - -method msgExtraGas(call: DelegateCall, computation; gas: GasInt, to: EthAddress, value: UInt256): GasInt = - 0 - method callParams(call: DelegateCall, computation): (UInt256, UInt256, EthAddress, EthAddress, EthAddress, UInt256, UInt256, UInt256, UInt256, bool, bool) = let gas = computation.stack.popInt() let codeAddress = computation.stack.popAddress() @@ -210,42 +184,6 @@ method callParams(call: DelegateCall, computation): (UInt256, UInt256, EthAddres false, # should_transfer_value, computation.msg.isStatic) -proc maxChildGasEIP150*(gas: GasInt): GasInt = - gas - gas div 64 - -proc computeEIP150MsgGas(computation; gas, extraGas: GasInt, value: UInt256, name: string, callStipend: GasInt): (GasInt, GasInt) = - if computation.gasMeter.gasRemaining < extraGas: - raise newException(OutOfGas, &"Out of gas: Needed {extraGas} - Remaining {computation.gasMeter.gasRemaining} - Reason: {name}") - let gas = min(gas, maxChildGasEIP150(computation.gasMeter.gasRemaining - extraGas)) - let totalFee = gas + extraGas - let childMsgGas = gas + (if value != 0: callStipend else: 0) - (childMsgGas, totalFee) - -method msgGas(call: CallEIP150, computation; gas: GasInt, to: EthAddress, value: UInt256): (GasInt, GasInt) = - let extraGas = call.msgExtraGas(computation, gas, to, value) - computeEIP150MsgGas(computation, gas, extraGas, value, $call.kind, GAS_CALL_STIPEND) - -method msgGas(call: CallCodeEIP150, computation; gas: GasInt, to: EthAddress, value: UInt256): (GasInt, GasInt) = - let extraGas = call.msgExtraGas(computation, gas, to, value) - computeEIP150MsgGas(computation, gas, extraGas, value, $call.kind, GAS_CALL_STIPEND) - -method msgGas(call: DelegateCallEIP150, computation; gas: GasInt, to: EthAddress, value: UInt256): (GasInt, GasInt) = - let extraGas = call.msgExtraGas(computation, gas, to, value) - computeEIP150MsgGas(computation, gas, extraGas, value, $call.kind, 0) - -proc msgExtraGas*(call: CallEIP161, computation; gas: GasInt, to: EthAddress, value: UInt256): GasInt = - # TODO: with - # with computation.vm_state.state_db(read_only=True) as state_db: - # account_is_dead = ( - # not state_db.account_exists(to) or - # state_db.account_is_empty(to)) - let accountIsDead = true - - let transferGasFee = if value != 0: GAS_CALL_VALUE else: 0 - let createGasFee = if accountIsDead and value != 0: GAS_NEW_ACCOUNT else: 0 - transferGasFee + createGasFee - - method callParams(call: StaticCall, computation): (UInt256, UInt256, EthAddress, EthAddress, EthAddress, UInt256, UInt256, UInt256, UInt256, bool, bool) = let gas = computation.stack.popInt() let to = computation.stack.popAddress() @@ -265,7 +203,6 @@ method callParams(call: StaticCall, computation): (UInt256, UInt256, EthAddress, false, # should_transfer_value, true) # is_static - method callParams(call: CallByzantium, computation): (UInt256, UInt256, EthAddress, EthAddress, EthAddress, UInt256, UInt256, UInt256, UInt256, bool, bool) = result = procCall callParams(call, computation) if computation.msg.isStatic and result[1] != 0: diff --git a/nimbus/logic/context.nim b/nimbus/logic/context.nim index 40ca01173..a013ade85 100644 --- a/nimbus/logic/context.nim +++ b/nimbus/logic/context.nim @@ -8,7 +8,8 @@ import strformat, ../constants, ../vm_types, ../errors, ../utils_numeric, ../computation, ../vm_state, ../account, ../db/state_db, ../validation, - .. / vm / [stack, message, gas_meter, memory, code_stream], .. / utils / [address, padding, bytes], stint + .. /vm/[stack, message, gas_meter, memory, code_stream], ../utils/[address, padding, bytes], stint, + ../opcode_values proc balance*(computation: var BaseComputation) = let address = computation.stack.popAddress() @@ -47,12 +48,14 @@ proc callDataCopy*(computation: var BaseComputation) = let (memStartPosition, calldataStartPosition, size) = computation.stack.popInt(3) - let (memPos, callPos, len) = (memStartPosition.toInt, calldataStartPosition.toInt, size.toInt) - computation.extendMemory(memPos, len) - let wordCount = ceil32(len) div 32 - let copyGasCost = wordCount * constants.GAS_COPY - computation.gasMeter.consumeGas(copyGasCost, reason="CALLDATACOPY fee") + computation.gasMeter.consumeGas( + computation.gasCosts[CallDataCopy].d_handler(size), + reason="CALLDATACOPY fee") + + let (memPos, callPos, len) = (memStartPosition.toInt, calldataStartPosition.toInt, size.toInt) + computation.memory.extend(memPos, len) + let value = computation.msg.data[callPos ..< callPos + len] let paddedValue = padRight(value, len, 0.byte) computation.memory.write(memPos, paddedValue) @@ -67,13 +70,14 @@ proc codecopy*(computation: var BaseComputation) = let (memStartPosition, codeStartPosition, size) = computation.stack.popInt(3) + + computation.gasMeter.consumeGas( + computation.gasCosts[CodeCopy].d_handler(size), + reason="CODECOPY: word gas cost") + let (memPos, codePos, len) = (memStartPosition.toInt, codeStartPosition.toInt, size.toInt) - computation.extendMemory(memPos, len) + computation.memory.extend(memPos, len) - let wordCount = ceil32(len) div 32 - let copyGasCost = constants.GAS_COPY * wordCount - - computation.gasMeter.consumeGas(copyGasCost, reason="CODECOPY: word gas cost") # TODO # with computation.code.seek(code_start_position): # code_bytes = computation.code.read(size) @@ -96,12 +100,15 @@ proc extCodeSize*(computation: var BaseComputation) = proc extCodeCopy*(computation: var BaseComputation) = let account = computation.stack.popAddress() let (memStartPosition, codeStartPosition, size) = computation.stack.popInt(3) - let (memPos, codePos, len) = (memStartPosition.toInt, codeStartPosition.toInt, size.toInt) - computation.extendMemory(memPos, len) - let wordCount = ceil32(len) div 32 - let copyGasCost = constants.GAS_COPY * wordCount - computation.gasMeter.consumeGas(copyGasCost, reason="EXTCODECOPY: word gas cost") + computation.gasMeter.consumeGas( + computation.gasCosts[ExtCodeCopy].d_handler(size), + reason="EXTCODECOPY: word gas cost" + ) + + let (memPos, codePos, len) = (memStartPosition.toInt, codeStartPosition.toInt, size.toInt) + computation.memory.extend(memPos, len) + # TODO: # with computation.vm_state.state_db(read_only=True) as state_db: @@ -116,6 +123,12 @@ proc returnDataSize*(computation: var BaseComputation) = proc returnDataCopy*(computation: var BaseComputation) = let (memStartPosition, returnDataStartPosition, size) = computation.stack.popInt(3) + + computation.gasMeter.consumeGas( + computation.gasCosts[ReturnDataCopy].d_handler(size), + reason="RETURNDATACOPY fee" + ) + let (memPos, returnPos, len) = (memStartPosition.toInt, returnDataStartPosition.toInt, size.toInt) if returnPos + len > computation.returnData.len: raise newException(OutOfBoundsRead, @@ -123,9 +136,7 @@ proc returnDataCopy*(computation: var BaseComputation) = &"for data from index {returnDataStartPosition} to {returnDataStartPosition + size}. Return data is {computation.returnData.len} in \n" & "length") - computation.extendMemory(memPos, len) - let wordCount = ceil32(len) div 32 - let copyGasCost = wordCount * constants.GAS_COPY - computation.gasMeter.consumeGas(copyGasCost, reason="RETURNDATACOPY fee") + computation.memory.extend(memPos, len) + let value = ($computation.returnData)[returnPos ..< returnPos + len] computation.memory.write(memPos, len, value) diff --git a/nimbus/logic/logging_ops.nim b/nimbus/logic/logging_ops.nim index 1aaa2562d..1df8d1925 100644 --- a/nimbus/logic/logging_ops.nim +++ b/nimbus/logic/logging_ops.nim @@ -7,7 +7,9 @@ import strformat, macros, - ../constants, ../errors, ../vm_types, ../computation, .. / vm / [stack, memory, gas_meter, message], .. / utils / bytes, stint + ../constants, ../errors, ../vm_types, ../computation, ../vm/[stack, memory, gas_meter, message], ../utils/bytes, + ../opcode_values, + stint {.this: computation.} {.experimental.} @@ -50,12 +52,12 @@ macro logXX(topicCount: static[int]): untyped = result.body.add(topicCode) + let OpName = ident(&"Log{topicCount}") let logicCode = quote: - let dataGasCost = constants.GAS_LOG_DATA * `len` - let topicGasCost = constants.GAS_LOG_TOPIC * `topicCount` - let totalGasCost = dataGasCost + topicGasCost - `computation`.gasMeter.consumeGas(totalGasCost, reason="Log topic and data gas cost") - `computation`.extendMemory(`memPos`, `len`) + `computation`.gasMeter.consumeGas( + `computation`.gasCosts[`OpName`].m_handler(`computation`.memory.len, `memPos` + `len`), + reason="Memory expansion, Log topic and data gas cost") + `computation`.memory.extend(`memPos`, `len`) let logData = `computation`.memory.read(`memPos`, `len`).toString `computation`.addLogEntry( account=`computation`.msg.storageAddress, diff --git a/nimbus/logic/memory_ops.nim b/nimbus/logic/memory_ops.nim index b6b24de8f..ea454be14 100644 --- a/nimbus/logic/memory_ops.nim +++ b/nimbus/logic/memory_ops.nim @@ -6,7 +6,8 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import - ../constants, ../computation, ../vm_types, .. / vm / [stack, memory], .. / utils / [padding, bytes], + ../constants, ../computation, ../vm_types, ../vm/[stack, memory, gas_meter], .. /utils/[padding, bytes], + ../opcode_values, stint @@ -22,7 +23,12 @@ proc mstore*(computation) = let start = stack.popInt().toInt let normalizedValue = stack.popInt().toByteArrayBE - extendMemory(start, 32) + computation.gasMeter.consumeGas( + computation.gasCosts[MStore].m_handler(computation.memory.len, start + 32), + reason="MSTORE: GasVeryLow + memory expansion" + ) + + memory.extend(start, 32) memory.write(start, normalizedValue) proc mstore8*(computation) = @@ -30,14 +36,23 @@ proc mstore8*(computation) = let value = stack.popInt() let normalizedValue = (value and 0xff).toByteArrayBE - extendMemory(start, 1) + computation.gasMeter.consumeGas( + computation.gasCosts[MStore8].m_handler(computation.memory.len, start + 1), + reason="MSTORE8: GasVeryLow + memory expansion" + ) + + memory.extend(start, 1) memory.write(start, [normalizedValue[0]]) proc mload*(computation) = let start = stack.popInt().toInt - extendMemory(start, 32) + computation.gasMeter.consumeGas( + computation.gasCosts[MLoad].m_handler(computation.memory.len, start + 32), + reason="MLOAD: GasVeryLow + memory expansion" + ) + memory.extend(start, 32) let value = memory.read(start, 32) stack.push(value) diff --git a/nimbus/logic/sha3.nim b/nimbus/logic/sha3.nim index 7a7076e5c..bc30ce611 100644 --- a/nimbus/logic/sha3.nim +++ b/nimbus/logic/sha3.nim @@ -6,15 +6,21 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import - ../constants, ../utils_numeric, .. / utils / [keccak, bytes], .. / vm / [stack, memory, gas_meter], ../computation, ../vm_types, helpers, stint + ../constants, ../utils_numeric, ../utils/[keccak, bytes], ../vm/[stack, memory, gas_meter], + ../computation, ../vm_types, ../opcode_values, + ./helpers, + stint proc sha3op*(computation: var BaseComputation) = let (startPosition, size) = computation.stack.popInt(2) let (pos, len) = (startPosition.toInt, size.toInt) - computation.extendMemory(pos, len) - let sha3Bytes = computation.memory.read(pos, len) - let wordCount = sha3Bytes.len.ceil32 div 32 # TODO, can't we just shr instead of rounding + div - let gasCost = constants.GAS_SHA3_WORD * wordCount - computation.gasMeter.consumeGas(gasCost, reason="SHA3: word gas cost") - var res = keccak("") + + computation.gasMeter.consumeGas( + computation.gasCosts[Sha3].m_handler(computation.memory.len, pos + len), + reason="SHA3: word gas cost" + ) + + computation.memory.extend(pos, len) + + var res = keccak("") # TODO: stub pushRes() diff --git a/nimbus/logic/storage.nim b/nimbus/logic/storage.nim index 0ca318bc4..85c406e87 100644 --- a/nimbus/logic/storage.nim +++ b/nimbus/logic/storage.nim @@ -9,6 +9,7 @@ import ../constants, ../vm_types, ../errors, ../computation, ../vm_state, ../utils/header, ../db/[db_chain, state_db], ../vm/[stack, gas_meter, message], + ../opcode_values, strformat, stint {.this: computation.} @@ -26,15 +27,14 @@ proc sstore*(computation) = computation.vmState.db(readOnly=false): (currentValue, existing) = db.getStorage(computation.msg.storageAddress, slot) - let isCurrentlyEmpty = not existing # currentValue == 0 - let isGoingToBeEmpty = value == 0 + let + gasParam = GasParams(kind: Op.Sstore, s_isStorageEmpty: not existing) + (gasCost, gasRefund) = computation.gasCosts[Sstore].c_handler(currentValue, gasParam) - let gasRefund = if isCurrentlyEmpty or not isGoingToBeEmpty: 0 else: REFUND_SCLEAR - let gasCost = if isCurrentlyEmpty and not isGoingToBeEmpty: GAS_SSET else: GAS_SRESET + computation.gasMeter.consumeGas(gasCost, &"SSTORE: {computation.msg.storageAddress}[slot] -> {value} ({currentValue})") - computation.gasMeter.consumeGas(computation.gasCosts[gasCost], &"SSTORE: {computation.msg.storageAddress}[slot] -> {value} ({currentValue})") - - if gasRefund > 0: computation.gasMeter.refundGas(gasRefund) + if gasRefund > 0: + computation.gasMeter.refundGas(gasRefund) computation.vmState.db(readOnly=false): db.setStorage(computation.msg.storageAddress, slot, value) diff --git a/nimbus/logic/system_ops.nim b/nimbus/logic/system_ops.nim index c43832496..bd81af836 100644 --- a/nimbus/logic/system_ops.nim +++ b/nimbus/logic/system_ops.nim @@ -9,6 +9,7 @@ import strformat, ../constants, ../vm_types, ../errors, ../computation, ../opcode, ../opcode_values, ../logging, ../vm_state, call, .. / vm / [stack, gas_meter, memory, message], .. / utils / [address, hexadecimal, bytes], + ../opcode_values, stint, byteutils, eth_common {.this: computation.} @@ -24,14 +25,14 @@ type CreateByzantium* = ref object of CreateEIP150 # TODO: Refactoring - put that in VM forks -method maxChildGasModifier(create: Create, gas: GasInt): GasInt {.base.} = - gas +# method maxChildGasModifier(create: Create, gas: GasInt): GasInt {.base.} = +# gas method runLogic*(create: Create, computation) = - computation.gasMeter.consumeGas(computation.gasCosts[create.gasCost(computation)], reason = $create.kind) # TODO: Refactoring create gas costs + # computation.gasMeter.consumeGas(computation.gasCosts[create.gasCost(computation)], reason = $create.kind) # TODO: Refactoring create gas costs let (value, startPosition, size) = computation.stack.popInt(3) let (pos, len) = (startPosition.toInt, size.toInt) - computation.extendMemory(pos, len) + computation.memory.extend(pos, len) # TODO: with # with computation.vm_state.state_db(read_only=True) as state_db: @@ -44,8 +45,9 @@ method runLogic*(create: Create, computation) = # return let callData = computation.memory.read(pos, len) - let createMsgGas = create.maxChildGasModifier(computation.gasMeter.gasRemaining) - computation.gasMeter.consumeGas(createMsgGas, reason="CREATE") + # TODO refactor gas + # let createMsgGas = create.maxChildGasModifier(computation.gasMeter.gasRemaining) + # computation.gasMeter.consumeGas(createMsgGas, reason="CREATE") # TODO: with # with computation.vm_state.state_db() as state_db: @@ -67,7 +69,7 @@ method runLogic*(create: Create, computation) = return let childMsg = computation.prepareChildMessage( - gas=createMsgGas, + gas=0, # TODO refactor gas to=constants.CREATE_CONTRACT_ADDRESS, value=value, data=cast[seq[byte]](@[]), @@ -83,8 +85,9 @@ method runLogic*(create: Create, computation) = computation.stack.push(contractAddress) computation.gasMeter.returnGas(childComputation.gasMeter.gasRemaining) -method maxChildGasModifier(create: CreateEIP150, gas: GasInt): GasInt = - maxChildGasEIP150(gas) +# TODO refactor gas +# method maxChildGasModifier(create: CreateEIP150, gas: GasInt): GasInt = +# maxChildGasEIP150(gas) method runLogic*(create: CreateByzantium, computation) = if computation.msg.isStatic: @@ -144,7 +147,13 @@ proc selfdestruct(computation; beneficiary: EthAddress) = proc returnOp*(computation) = let (startPosition, size) = stack.popInt(2) let (pos, len) = (startPosition.toInt, size.toInt) - computation.extendMemory(pos, len) + + computation.gasMeter.consumeGas( + computation.gasCosts[Return].m_handler(computation.memory.len, pos + len), + reason = "RETURN" + ) + + computation.memory.extend(pos, len) let output = memory.read(pos, len) computation.output = output.toString raise newException(Halt, "RETURN") @@ -152,7 +161,13 @@ proc returnOp*(computation) = proc revert*(computation) = let (startPosition, size) = stack.popInt(2) let (pos, len) = (startPosition.toInt, size.toInt) - computation.extendMemory(pos, len) + + computation.gasMeter.consumeGas( + computation.gasCosts[Op.Revert].m_handler(computation.memory.len, pos + len), + reason = "REVERT" + ) + + computation.memory.extend(pos, len) let output = memory.read(pos, len).toString computation.output = output raise newException(Revert, $output) diff --git a/nimbus/opcode.nim b/nimbus/opcode.nim index 1fafdadfc..4c2b15dd3 100644 --- a/nimbus/opcode.nim +++ b/nimbus/opcode.nim @@ -20,68 +20,15 @@ from logic.call import runLogic, BaseCall template run*(opcode: Opcode, computation: var BaseComputation) = # Hook for performing the actual VM execution # opcode.consumeGas(computation) - computation.gasMeter.consumeGas(computation.gasCosts[opcode.gasCost(computation)], reason = $opcode.kind) # TODO: further refactoring of gas costs if opcode.kind == Op.Call: # Super dirty fix for https://github.com/status-im/nimbus/issues/46 + # TODO remove this branch runLogic(BaseCall(opcode), computation) + elif computation.gasCosts[opcode.kind].kind != GckFixed: + opcode.runLogic(computation) else: + computation.gasMeter.consumeGas(computation.gasCosts[opcode.kind].cost, reason = $opcode.kind) opcode.runLogic(computation) method logger*(opcode: Opcode): Logger = logging.getLogger(&"vm.opcode.{opcode.kind}") - -method gasCost*(opcode: Opcode, computation: var BaseComputation): GasCostKind = - #if opcode.kind in VARIABLE_GAS_COST_OPS: - # opcode.gasCostHandler(computation) - #else: - opcode.gasCostKind - -template newOpcode*(kind: Op, gasCost: UInt256, logic: proc(computation: var BaseComputation)): Opcode = - Opcode(kind: kind, gasCostKind: gasCost, runLogic: logic) - -template newOpcode*(kind: Op, gasHandler: proc(computation: var BaseComputation): UInt256, logic: proc(computation: var BaseComputation)): Opcode = - Opcode(kind: kind, gasCostHandler: gasHandler, runLogic: logic) - -method `$`*(opcode: Opcode): string = - let gasCost = $opcode.gasCostKind - # if opcode.kind in VARIABLE_GAS_COST_OPS: - # "variable" - # else: - # $opcode.gasCostKind - &"{opcode.kind}(0x{opcode.kind.int.toHex(2)}: {gasCost})" - -macro initOpcodes*(spec: untyped): untyped = - var value = ident("value") - result = quote: - block: - var `value` = initTable[Op, Opcode]() - - for child in spec: - var ops, gasCosts, handlers: seq[NimNode] - if child.kind == nnkInfix and child[0].repr == "..": - ops = @[] - gasCosts = @[] - handlers = @[] - let first = child[1].repr.parseInt - let last = child[2][0].repr.parseInt - let op = child[2][1][1].repr - for z in first .. last: - ops.add(nnkDotExpr.newTree(ident("Op"), ident(op.replace("XX", $z)))) - gasCosts.add(child[3][0][0]) - handlers.add(ident(child[3][0][1].repr.replace("XX", $z))) - else: - ops = @[child[0]] - gasCosts = @[child[1][0][0]] - handlers = @[child[1][0][1]] - for z in 0 ..< ops.len: - let (op, gasCost, handler) = (ops[z], gasCosts[z], handlers[z]) - let opcode = if gasCost.repr[0].isLowerAscii(): - quote: - `value`[`op`] = Opcode(kind: `op`, gasCostHandler: `gasCost`, runLogic: `handler`) - else: - quote: - `value`[`op`] = Opcode(kind: `op`, gasCostKind: `gasCost`, runLogic: `handler`) - result[1].add(opcode) - - result[1].add(value) - diff --git a/nimbus/opcode_table.nim b/nimbus/opcode_table.nim index 1048a21c8..923aec925 100644 --- a/nimbus/opcode_table.nim +++ b/nimbus/opcode_table.nim @@ -6,114 +6,168 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import - strformat, strutils, tables, macros, - constants, stint, errors, logging, vm_state, + tables, + stint, logging, vm / [gas_meter, stack, code_stream, memory, message], db / db_chain, computation, opcode, opcode_values, utils / [header, address], logic / [arithmetic, comparison, sha3, context, block_ops, stack_ops, duplication, swap, memory_ops, storage, flow, logging_ops, invalid, call, system_ops], ./vm_types -var OPCODE_TABLE* = initOpcodes: - # arithmetic - Op.Add: GasVeryLow add - Op.Mul: GasLow mul - Op.Sub: GasVeryLow sub - Op.Div: GasLow divide - Op.SDiv: GasLow sdiv - Op.Mod: GasLow modulo - Op.SMod: GasLow smod - Op.AddMod: GasMid addmod - Op.MulMod: GasMid mulmod - Op.Exp: GasInHandler arithmetic.exp - Op.SignExtend: GasLow signextend +const + OpLogic*: Table[Op, proc(computation: var BaseComputation){.nimcall.}] = { + # 0s: Stop and Arithmetic Operations + Stop: stop, + Add: arithmetic.add, + Mul: mul, + Sub: sub, + Div: divide, + Sdiv: sdiv, + Mod: modulo, + Smod: smod, + Addmod: arithmetic.addmod, + Mulmod: arithmetic.mulmod, + Exp: arithmetic.exp, + SignExtend: signextend, + # 10s: Comparison & Bitwise Logic Operations + Lt: lt, + Gt: gt, + Slt: slt, + Sgt: sgt, + Eq: eq, + IsZero: comparison.isZero, + And: andOp, + Or: orOp, + Xor: xorOp, + Not: notOp, + Byte: byteOp, - # comparison - Op.Lt: GasVeryLow lt - Op.Gt: GasVeryLow gt - Op.SLt: GasVeryLow slt - Op.SGt: GasVeryLow sgt - Op.Eq: GasVeryLow eq - Op.IsZero: GasVeryLow iszero - Op.And: GasVeryLow andOp - Op.Or: GasVeryLow orOp - Op.Xor: GasVeryLow xorOp - Op.Not: GasVeryLow notOp - Op.Byte: GasVeryLow byteOp + # 20s: SHA3 + Sha3: sha3op, + # 30s: Environmental Information + Address: context.address, + Balance: balance, + Origin: context.origin, + Caller: caller, + CallValue: callValue, + CallDataLoad: callDataLoad, + CallDataSize: callDataSize, + CallDataCopy: callDataCopy, + CodeSize: codesize, + CodeCopy: codecopy, + GasPrice: gasPrice, # TODO this wasn't used previously + ExtCodeSize: extCodeSize, + ExtCodeCopy: extCodeCopy, + ReturnDataSize: returnDataSize, # TODO this wasn't used previously + ReturnDataCopy: returnDataCopy, - # sha3 - Op.SHA3: GasSHA3 sha3op + # 40s: Block Information + Blockhash: block_ops.blockhash, + Coinbase: block_ops.coinbase, + Timestamp: block_ops.timestamp, + Number: block_ops.number, + Difficulty: block_ops.difficulty, + GasLimit: block_ops.gaslimit, + # 50s: Stack, Memory, Storage and Flow Operations + Pop: stack_ops.pop, + Mload: mload, + Mstore: mstore, + Mstore8: mstore8, + Sload: sload, + Sstore: sstore, + Jump: jump, + JumpI: jumpi, + Pc: pc, + Msize: msize, + Gas: flow.gas, + JumpDest: jumpDest, - # context - Op.Address: GasBase context.address - Op.Balance: GasBalance balance - Op.Origin: GasBase origin - Op.Caller: GasBase caller - Op.CallValue: GasBase callValue - Op.CallDataLoad: GasVeryLow callDataLoad - Op.CallDataSize: GasBase callDataSize - Op.CallDataCopy: GasBase callDataCopy - Op.CodeSize: GasBase codesize - Op.CodeCopy: GasBase codecopy - Op.ExtCodeSize: GasExtCode extCodeSize - Op.ExtCodeCopy: GasExtCode extCodeCopy + # 60s & 70s: Push Operations + Push1: push1, + Push2: push2, + Push3: push3, + Push4: push4, + Push5: push5, + Push6: push6, + Push7: push7, + Push8: push8, + Push9: push9, + Push10: push10, + Push11: push11, + Push12: push12, + Push13: push13, + Push14: push14, + Push15: push15, + Push16: push16, + Push17: push17, + Push18: push18, + Push19: push19, + Push20: push20, + Push21: push21, + Push22: push22, + Push23: push23, + Push24: push24, + Push25: push25, + Push26: push26, + Push27: push27, + Push28: push28, + Push29: push29, + Push30: push30, + Push31: push31, + Push32: push32, + # 80s: Duplication Operations + Dup1: dup1, + Dup2: dup2, + Dup3: dup3, + Dup4: dup4, + Dup5: dup5, + Dup6: dup6, + Dup7: dup7, + Dup8: dup8, + Dup9: dup9, + Dup10: dup10, + Dup11: dup11, + Dup12: dup12, + Dup13: dup13, + Dup14: dup14, + Dup15: dup15, + Dup16: dup16, - # block - Op.Blockhash: GasBase block_ops.blockhash - Op.Coinbase: GasCoinbase coinbase - Op.Timestamp: GasBase timestamp - Op.Number: GasBase number - Op.Difficulty: GasBase difficulty - Op.GasLimit: GasBase gaslimit + # 90s: Exchange Operations + Swap1: swap1, + Swap2: swap2, + Swap3: swap3, + Swap4: swap4, + Swap5: swap5, + Swap6: swap6, + Swap7: swap7, + Swap8: swap8, + Swap9: swap9, + Swap10: swap10, + Swap11: swap11, + Swap12: swap12, + Swap13: swap13, + Swap14: swap14, + Swap15: swap15, + Swap16: swap16, + # a0s: Logging Operations + Log0: log0, + Log1: log1, + Log2: log2, + Log3: log3, + Log4: log4, - # stack - Op.Pop: GasBase stack_ops.pop - 1..32 Op.PushXX: GasVeryLow pushXX # XX replaced by macro - 1..16 Op.DupXX: GasVeryLow dupXX - 1..16 Op.SwapXX: GasVeryLow swapXX - - - # memory - Op.MLoad: GasVeryLow mload - Op.MStore: GasVeryLow mstore - Op.MStore8: GasVeryLow mstore8 - Op.MSize: GasBase msize - - # storage - Op.SLoad: GasSload sload - Op.SStore: GasInHandler sstore - - - # flow - Op.Jump: GasMid jump - Op.JumpI: GasMid jumpi - Op.PC: GasHigh pc - Op.Gas: GasBase flow.gas - Op.JumpDest: GasJumpDest jumpdest - Op.Stop: GasZero stop - - - # logging - 0..4 Op.LogXX: GasInHandler logXX - - - # invalid - Op.Invalid: GasZero invalidOp - - - # system - Op.Return: GasZero returnOp - Op.SelfDestruct: GasSelfDestruct selfdestruct - - -# call -OPCODE_TABLE[Op.Call] = Call(kind: Op.Call) -OPCODE_TABLE[Op.CallCode] = CallCode(kind: Op.CallCode) -OPCODE_TABLE[Op.DelegateCall] = DelegateCall(kind: Op.DelegateCall) - - -# system -OPCODE_TABLE[Op.Create] = Create(kind: Op.Create) + # f0s: System operations + # Create: create, + # Call: call, + # CallCode: callCode, + Return: returnOp, + # DelegateCall: delegateCall, + # StaticCall: staticCall, + Op.Revert: revert, + Invalid: invalidOp, + SelfDestruct: selfDestruct + }.toTable diff --git a/nimbus/opcode_values.nim b/nimbus/opcode_values.nim index b706151b1..dc5678676 100644 --- a/nimbus/opcode_values.nim +++ b/nimbus/opcode_values.nim @@ -5,164 +5,174 @@ # * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) # at your option. This file may not be copied, modified, or distributed except according to those terms. -type - Op* {.pure.} = enum - STOP = 0x0, # 0 - ADD, # 1 - MUL, # 2 - SUB, # 3 - DIV, # 4 - SDIV, # 5 - MOD, # 6 - SMOD, # 7 - ADDMOD, # 8 - MULMOD, # 9 - EXP, # 10 - SIGNEXTEND, # 11 +import ./utils/macros_gen_opcodes - LT = 0x10, # 16 - GT, # 17 - SLT, # 18 - SGT, # 19 - EQ, # 20 - ISZERO, # 21 - AND, # 22 - OR, # 23 - XOR, # 24 - NOT, # 25 - BYTE, # 26 +fill_enum_holes: + type + # Yellow Paper Appendix H - https://ethereum.github.io/yellowpaper/paper.pdf + # Special notes from Yellow Paper: + # - Signed values are treated as two’s complement signed 256-bit integers. + # - When −2^255 is negated, there is an overflow + # - For addmod and mulmod, intermediate computations are not subject to the 2^256 modulo. + # Nimbus authors note: + # - This means that we can't naively do (Uint256 + Uint256) mod uint256, + # because the intermediate sum (or multiplication) might roll over if + # intermediate result is greater or equal 2^256 - SHA3 = 0x20, # 32 + Op* {.pure.} = enum + # 0s: Stop and Arithmetic Operations + Stop = 0x00, # Halts execution. + Add = 0x01, # Addition operation. + Mul = 0x02, # Multiplication operation. + Sub = 0x03, # Subtraction operation. + Div = 0x04, # Integer division operation. + Sdiv = 0x05, # Signed integer division operation (truncated). + Mod = 0x06, # Modulo remainder operation. + Smod = 0x07, # Signed modulo remainder operation. + Addmod = 0x08, # Modulo addition operation. + Mulmod = 0x09, # Modulo multiplication operation. + Exp = 0x0A, # Exponential operation + SignExtend = 0x0B, # Extend length of two’s complement signed integer. - ADDRESS = 0x30,# 48 - BALANCE, # 49 - ORIGIN, # 50 + # 10s: Comparison & Bitwise Logic Operations + Lt = 0x10, # Less-than comparison. + Gt = 0x11, # Greater-than comparison. + Slt = 0x12, # Signed less-than comparison. + Sgt = 0x13, # Signed greater-than comparison. + Eq = 0x14, # Equality comparison. + IsZero = 0x15, # Simple not operator. (Note: real Yellow Paper description) + And = 0x16, # Bitwise AND operation. + Or = 0x17, # Bitwise OR operation. + Xor = 0x18, # Bitwise XOR operation. + Not = 0x19, # Bitwise NOT operation. + Byte = 0x1A, # Retrieve single byte from word. - CALLER, # 51 - CALLVALUE, # 52 - CALLDATALOAD, # 53 - CALLDATASIZE, # 54 - CALLDATACOPY, # 55 + # 20s: SHA3 + Sha3 = 0x20, # Compute Keccak-256 hash. - CODESIZE, # 56 - CODECOPY, # 57 + # 30s: Environmental Information + Address = 0x30, # Get address of currently executing account. + Balance = 0x31, # Get balance of the given account. + Origin = 0x32, # Get execution origination address. + Caller = 0x33, # Get caller address. + CallValue = 0x34, # Get deposited value by the instruction/transaction responsible for this execution. + CallDataLoad = 0x35, # Get input data of current environment. + CallDataSize = 0x36, # Get size of input data in current environment. + CallDataCopy = 0x37, # Copy input data in current environment to memory. + CodeSize = 0x38, # Get size of code running in current environment. + CodeCopy = 0x39, # Copy code running in current environment to memory. + GasPrice = 0x3a, # Get price of gas in current environment. + ExtCodeSize = 0x3b, # Get size of an account's code + ExtCodeCopy = 0x3c, # Copy an account's code to memory. + ReturnDataSize = 0x3d, # Get size of output data from the previous call from the current environment. + ReturnDataCopy = 0x3e, # Copy output data from the previous call to memory. - GASPRICE, # 58 + # 40s: Block Information + Blockhash = 0x40, # Get the hash of one of the 256 most recent complete blocks. + Coinbase = 0x41, # Get the block's beneficiary address. + Timestamp = 0x42, # Get the block's timestamp. + Number = 0x43, # Get the block's number. + Difficulty = 0x44, # Get the block's difficulty. + GasLimit = 0x45, # Get the block's gas limit. - EXTCODESIZE, # 59 - EXTCODECOPY, # 60 + # 50s: Stack, Memory, Storage and Flow Operations + Pop = 0x50, # Remove item from stack. + Mload = 0x51, # Load word from memory. + Mstore = 0x52, # Save word to memory. + Mstore8 = 0x53, # Save byte to memory. + Sload = 0x54, # Load word from storage. + Sstore = 0x55, # Save word to storage. + Jump = 0x56, # Alter the program counter. + JumpI = 0x57, # Conditionally alter the program counter. + Pc = 0x58, # Get the value of the program counter prior to the increment corresponding to this instruction. + Msize = 0x59, # Get the size of active memory in bytes. + Gas = 0x5a, # Get the amount of available gas, including the corresponding reduction for the cost of this instruction. + JumpDest = 0x5b, # Mark a valid destination for jumps. This operation has no effect on machine state during execution. - RETURNDATASIZE, # 61 - RETURNDATACOPY, # 62 + # 60s & 70s: Push Operations. + Push1 = 0x60, # Place 1-byte item on stack. + Push2 = 0x61, # Place 2-byte item on stack. + Push3, + Push4, + Push5, + Push6, + Push7, + Push8, + Push9, + Push10, + Push11, + Push12, + Push13, + Push14, + Push15, + Push16, + Push17, + Push18, + Push19, + Push20, + Push21, + Push22, + Push23, + Push24, + Push25, + Push26, + Push27, + Push28, + Push29, + Push30, + Push31, + Push32 = 0x7f, # Place 32-byte (full word) item on stack. - BLOCKHASH = 0x40,# 64 + # 80s: Duplication Operations + Dup1 = 0x80, # Duplicate 1st stack item. + Dup2 = 0x81, # Duplicate 2nd stack item. + Dup3, + Dup4, + Dup5, + Dup6, + Dup7, + Dup8, + Dup9, + Dup10, + Dup11, + Dup12, + Dup13, + Dup14, + Dup15, + Dup16 = 0x8f, # Duplicate 16th stack item. - COINBASE, # 65 + # 90s: Exchange Operations + Swap1 = 0x90, # Exchange 1st and 2nd stack items. + Swap2 = 0x91, # Exchange 1st and 3rd stack items. + Swap3, + Swap4, + Swap5, + Swap6, + Swap7, + Swap8, + Swap9, + Swap10, + Swap11, + Swap12, + Swap13, + Swap14, + Swap15, + Swap16 = 0x9f, # Exchange 1st and 17th stack items. - TIMESTAMP, # 66 - - NUMBER, # 67 - - DIFFICULTY, # 68 - - GASLIMIT, # 69 - - POP = 0x50, # 80 - - MLOAD, # 81 - MSTORE, # 82 - MSTORE8 # 83 - - SLOAD, # 84 - SSTORE, # 85 - - JUMP, # 86 - JUMPI, # 87 - - PC, # 88 - - MSIZE, # 89 - - GAS, # 90 - - JUMPDEST, # 91 - - PUSH1 = 0x60, # 96 - PUSH2, # 97 - PUSH3, # 98 - PUSH4, # 99 - PUSH5, # 100 - PUSH6, # 101 - PUSH7, # 102 - PUSH8, # 103 - PUSH9, # 104 - PUSH10, # 105 - PUSH11, # 106 - PUSH12, # 107 - PUSH13, # 108 - PUSH14, # 109 - PUSH15, # 110 - PUSH16, # 111 - PUSH17, # 112 - PUSH18, # 113 - PUSH19, # 114 - PUSH20, # 115 - PUSH21, # 116 - PUSH22, # 117 - PUSH23, # 118 - PUSH24, # 119 - PUSH25, # 120 - PUSH26, # 121 - PUSH27, # 122 - PUSH28, # 123 - PUSH29, # 124 - PUSH30, # 125 - PUSH31, # 126 - PUSH32, # 127 - DUP1, # 128 - DUP2, # 129 - DUP3, # 130 - DUP4, # 131 - DUP5, # 132 - DUP6, # 133 - DUP7, # 134 - DUP8, # 135 - DUP9, # 136 - DUP10, # 137 - DUP11, # 138 - DUP12, # 139 - DUP13, # 140 - DUP14, # 141 - DUP15, # 142 - DUP16, # 143 - SWAP1, # 144 - SWAP2, # 145 - SWAP3, # 146 - SWAP4, # 147 - SWAP5, # 148 - SWAP6, # 149 - SWAP7, # 150 - SWAP8, # 151 - SWAP9, # 152 - SWAP10, # 153 - SWAP11, # 154 - SWAP12, # 155 - SWAP13, # 156 - SWAP14, # 157 - SWAP15, # 158 - SWAP16, # 159 - LOG0, # 160 - LOG1, # 161 - LOG2, # 162 - LOG3, # 163 - LOG4, # 164 - CREATE = 0xf0, # 240 - CALL, # 241 - CALLCODE, # 242 - RETURN, # 243 - DELEGATECALL, # 244 - STATICCALL = 0xfa,# 250 - REVERT = 0xfd, # 253 - SELFDESTRUCT = 0xff,# 255 - INVALID # invalid + # a0s: Logging Operations + Log0 = 0xa0, # Append log record with no topics. + Log1 = 0xa1, # Append log record with one topics. + Log2, + Log3, + Log4 = 0xa4, # Append log record with four topics. + # f0s: System operations + Create = 0xf0, # Create a new account with associated code. + Call = 0xf1, # Message-call into an account. + CallCode = 0xf2, # Message-call into this account with an alternative account's code. + Return = 0xf3, # Halt execution returning output data. + DelegateCall = 0xf4, # Message-call into this account with an alternative account's code, but persisting the current values for sender and value. + StaticCall = 0xfa, # Static message-call into an account. + Revert = 0xfd, # Halt execution reverting state changes but returning data and remaining gas. + Invalid = 0xfe, # Designated invalid instruction. + SelfDestruct = 0xff # Halt execution and register account for later deletion. diff --git a/nimbus/utils/macros_gen_opcodes.nim b/nimbus/utils/macros_gen_opcodes.nim new file mode 100644 index 000000000..f99a98a2f --- /dev/null +++ b/nimbus/utils/macros_gen_opcodes.nim @@ -0,0 +1,126 @@ +# Nimbus +# Copyright (c) 2018 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +# * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +import macros, strformat, strutils + +# Due to https://github.com/nim-lang/Nim/issues/8007, we can't +# use compile-time Tables of object variants, so instead, we use +# const arrays to map Op --> gas prices. +# Arrays require an enum without hole. This macro will fill the holes +# in the enum. + +# This has an added benefits that we can use computed gotos (direct threaded interpreter) +# instead of call or subroutine threading to dispatch opcode. +# see: https://github.com/nim-lang/Nim/issues/7699 (computed gotos, bad codegen with enum with holes) +# see: https://github.com/status-im/nimbus/wiki/Interpreter-optimization-resources +# for interpreter dispatch strategies + +macro fill_enum_holes*(body: untyped): untyped = + ## Fill the holes of an enum + ## For example + ## type Foo = enum + ## A = 0x00, + ## B = 0x10 + + # Sanity checks + # + # StmtList + # TypeSection + # TypeDef + # PragmaExpr + # Postfix + # Ident "*" + # Ident "Op" + # Pragma + # Ident "pure" + # Empty + # EnumTy + # Empty + # EnumFieldDef + # Ident "Stop" + # IntLit 0 + # EnumFieldDef + # Ident "Add" + # IntLit 1 + body[0].expectKind(nnkTypeSection) + body[0][0][2].expectKind(nnkEnumTy) + + let opcodes = body[0][0][2] + + # We will iterate over all the opcodes + # check if the i-th value is declared, if not add a no-op + # and accumulate that in a "dense opcodes" declaration + + var + opcode = 0 + holes_idx = 1 + dense_opcs = nnkEnumTy.newTree() + dense_opcs.add newEmptyNode() + + # Iterate on the enum with holes + while holes_idx < opcodes.len: + let curr_ident = opcodes[holes_idx] + + if curr_ident.kind in {nnkIdent, nnkEmpty} or + (curr_ident.kind == nnkEnumFieldDef and + curr_ident[1].intVal == opcode): + + dense_opcs.add curr_ident + inc holes_idx + else: + dense_opcs.add newIdentNode(&"Nop0x{opcode.toHex(2)}") + + inc opcode + + result = body + result[0][0][2] = dense_opcs + +macro fill_enum_table_holes*(enumTy: typedesc[enum], nop_filler, body: untyped): untyped = + ## Fill the holes of table mapping for enum with a default value + ## + ## For example for enum + ## type Foo = enum + ## A = 0x00, + ## B = 0x01, + ## C = 0x02 + ## let foo = fill_enum_table_holes(Foo, 999): + ## [A: 10, C: 20] + ## + ## will result into `[A: 10, B: 999, C: 20]` + + # Sanity checks - body + # StmtList + # Bracket + # ExprColonExpr + # Ident "Stop" + # Command + # Ident "fixed" + # Ident "GasZero" + # ExprColonExpr + # Ident "Add" + # Command + # Ident "fixed" + # Ident "GasVeryLow" + body[0].expectKind(nnkBracket) + + let + enumImpl = enumTy.getType[1] + opctable = body[0] + + result = nnkBracket.newTree() + var + opcode = 1 # enumImpl[0] is an empty node + body_idx = 0 + + while opcode < enumImpl.len: + opctable[body_idx].expectKind(nnkExprColonExpr) + if eqIdent(enumImpl[opcode], opctable[body_idx][0]): + result.add opctable[body_idx] + inc body_idx + else: + result.add nop_filler + inc opcode diff --git a/nimbus/utils_numeric.nim b/nimbus/utils_numeric.nim index 67b47b22d..0a20ccd6f 100644 --- a/nimbus/utils_numeric.nim +++ b/nimbus/utils_numeric.nim @@ -9,8 +9,6 @@ import stint, constants, strformat, strutils, sequtils, endians, macros, utils / # some methods based on py-evm utils/numeric -# TODO improve - proc intToBigEndian*(value: UInt256): Bytes {.deprecated.} = result = newSeq[byte](32) result[0 .. ^1] = value.toByteArrayBE() @@ -21,21 +19,9 @@ proc bigEndianToInt*(value: openarray[byte]): UInt256 = else: readUintBE[256](padLeft(@value, 32, 0.byte)) -#echo intToBigEndian("32482610168005790164680892356840817100452003984372336767666156211029086934369".u256) - -# proc bitLength*(value: UInt256): int = -# 255 - value.countLeadingZeroBits - proc log256*(value: UInt256): Natural = (255 - value.countLeadingZeroBits) div 8 # Compilers optimize to `shr 3` -# proc ceil8*(value: int): int = -# let remainder = value mod 8 -# if remainder == 0: -# value -# else: -# value + 8 - remainder - proc unsignedToSigned*(value: UInt256): Int256 = 0.i256 # TODO Remove stub (used in quasiBoolean for signed comparison) @@ -54,17 +40,16 @@ proc pseudoSignedToUnsigned*(value: UInt256): UInt256 = if value > INT_256_MAX_AS_UINT256: result += INT_256_MAX_AS_UINT256 -# it's deasible to map nameXX methods like that (originally decorator) -macro ceilXX(ceiling: static[int]): untyped = - var name = ident(&"ceil{ceiling}") - result = quote: - proc `name`*(value: Natural): Natural = - var remainder = value mod `ceiling` - if remainder == 0: - return value - else: - return value + `ceiling` - remainder +func ceil32*(value: Natural): Natural {.inline.}= + # Round input to the nearest bigger multiple of 32 + result = value -ceilXX(32) -ceilXX(8) + let remainder = result and 31 # equivalent to modulo 32 + if remainder != 0: + return value + 32 - remainder + +func wordCount*(length: Natural): Natural {.inline.}= + # Returns the number of EVM words corresponding to a specific size. + # EVM words is rounded up + length.ceil32 shr 5 # equivalent to `div 32` (32 = 2^5) diff --git a/nimbus/vm/forks/gas_costs.nim b/nimbus/vm/forks/gas_costs.nim index 3b9f29a5b..2eabe1150 100644 --- a/nimbus/vm/forks/gas_costs.nim +++ b/nimbus/vm/forks/gas_costs.nim @@ -6,40 +6,549 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import - stint, ../../vm_types + stint, eth_common, math, + ../../utils/macros_gen_opcodes, + ../../opcode_values, + ../../utils_numeric -# TODO: Make that computation at compile-time. -# Go-Ethereum uses pure uint64 for gas computation -const BaseGasCosts*: GasCosts = [ - GasZero: 0'i64, - GasBase: 2, - GasVeryLow: 3, - GasLow: 5, - GasMid: 8, - GasHigh: 10, - GasSload: 50, # Changed to 200 in Tangerine (EIP150) - GasJumpDest: 1, - GasSset: 20_000, - GasSreset: 5_000, - GasExtCode: 20, - GasCoinbase: 20, - GasSelfDestruct: 0, # Changed to 5000 in Tangerine (EIP150) - GasInHandler: 0, # to be calculated in handler - GasRefundSclear: 15000, +# Gas Fee Schedule +# Yellow Paper Appendix G - https://ethereum.github.io/yellowpaper/paper.pdf +type + GasFeeKind = enum + GasZero, # Nothing paid for operations of the set Wzero. + GasBase, # Amount of gas to pay for operations of the set Wbase. + GasVeryLow, # Amount of gas to pay for operations of the set Wverylow. + GasLow, # Amount of gas to pay for operations of the set Wlow. + GasMid, # Amount of gas to pay for operations of the set Wmid. + GasHigh, # Amount of gas to pay for operations of the set Whigh. + GasExtCode, # Amount of gas to pay for operations of the set Wextcode. + GasBalance, # Amount of gas to pay for a BALANCE operation. + GasSload, # Paid for a SLOAD operation. + GasJumpDest, # Paid for a JUMPDEST operation. + GasSset, # Paid for an SSTORE operation when the storage value is set to non-zero from zero. + GasSreset, # Paid for an SSTORE operation when the storage value’s zeroness remains unchanged or is set to zero. + RefundSclear, # Refund given (added into refund counter) when the storage value is set to zero from non-zero. + RefundSelfDestruct, # Refund given (added into refund counter) for self-destructing an account. + GasSelfDestruct, # Amount of gas to pay for a SELFDESTRUCT operation. + GasCreate, # Paid for a CREATE operation. + GasCodeDeposit, # Paid per byte for a CREATE operation to succeed in placing code into state. + GasCall, # Paid for a CALL operation. + GasCallValue, # Paid for a non-zero value transfer as part of the CALL operation. + GasCallStipend, # A stipend for the called contract subtracted from Gcallvalue for a non-zero value transfer. + GasNewAccount, # Paid for a CALL or SELFDESTRUCT operation which creates an account. + GasExp, # Partial payment for an EXP operation. + GasExpByte, # Partial payment when multiplied by ⌈log256(exponent)⌉ for the EXP operation. + GasMemory, # Paid for every additional word when expanding memory. + GasTXCreate, # Paid by all contract-creating transactions after the Homestead transition. + GasTXDataZero, # Paid for every zero byte of data or code for a transaction. + GasTXDataNonZero, # Paid for every non-zero byte of data or code for a transaction. + GasTransaction, # Paid for every transaction. + GasLog, # Partial payment for a LOG operation. + GasLogData, # Paid for each byte in a LOG operation’s data. + GasLogTopic, # Paid for each topic of a LOG operation. + GasSha3, # Paid for each SHA3 operation. + GasSha3Word, # Paid for each word (rounded up) for input data to a SHA3 operation. + GasCopy, # Partial payment for COPY operations, multiplied by words copied, rounded up. + GasBlockhash # Payment for BLOCKHASH operation. + # GasQuadDivisor # The quadratic coefficient of the input sizes of the exponentiation-over-modulo precompiled contract. - GasBalance: 20, # Changed to 400 in Tangerine (EIP150) - GasCall: 40, # Changed to 700 in Tangerine (EIP150) - GasExp: 10, - GasSHA3: 30 -] + GasFeeSchedule = array[GasFeeKind, GasInt] -proc tangerineGasCosts(baseCosts: GasCosts): GasCosts = + GasParams* = object + # Yellow Paper, Appendix H - https://ethereum.github.io/yellowpaper/paper.pdf + # GasCost is a function of (σ, μ): + # - σ is the full system state + # - μ is the machine state + # In practice, we often require the following from + # - σ: an account address + # - μ: a value popped from the stack or its size. + case kind*: Op + of Sstore: + s_isStorageEmpty*: bool + of Call: + c_isNewAccount*: bool + c_gasBalance*: GasInt + c_contractGas*: Gasint + c_activeMemSize*: Natural + c_memRequested*: Natural + else: + discard + + GasCostKind* = enum + GckInvalidOp, + GckFixed, + GckDynamic, + GckMemExpansion, + GckComplex + + GasResult = tuple[gasCost, gasRefund: GasInt] + + GasCost = object + case kind*: GasCostKind + of GckInvalidOp: + discard + of GckFixed: + cost*: GasInt + of GckDynamic: + d_handler*: proc(value: Uint256): GasInt {.nimcall.} + of GckMemExpansion: + m_handler*: proc(activeMemSize, requestedMemSize: Natural): GasInt {.nimcall.} + of GckComplex: + c_handler*: proc(value: Uint256, gasParams: GasParams): GasResult {.nimcall.} + # We use gasCost/gasRefund for: + # - Properly log and order cost and refund (for Sstore especially) + # - Allow to use unsigned integer in the future + # - CALL instruction requires passing the child message gas (Ccallgas in yellow paper) + + GasCosts* = array[Op, GasCost] + +template gasCosts(FeeSchedule: GasFeeSchedule, prefix, ResultGasCostsName: untyped) = + + ## Generate the gas cost for each forks and store them in a const + ## named `ResultGasCostsName` + + # ############### Helper functions ############################## + + func `prefix gasMemoryExpansion`(activeMemSize, requestedMemSize: Natural): GasInt {.inline.} = + # Input: size (in bytes) + + # Yellow Paper: + # Memory expansion cost + # = Cmem(μ′i) − Cmem(μi) + # μi is memory size before opcode execution + # μ'i is the memory size after opcode execution + + # Cmem(a) ≡ Gmemory · a + a² / 512 + let + prev_words = activeMemSize.wordCount + prev_cost = prev_words * static(FeeSchedule[GasMemory]) + + (prev_words ^ 2) shr 9 # div 512 + + new_words = requestedMemSize.wordCount + new_cost = new_words * static(FeeSchedule[GasMemory]) + + (new_words ^ 2) shr 9 # div 512 + + # TODO: add logging + result = new_cost - prev_cost + + func `prefix all_but_one_64th`(gas: GasInt): GasInt {.inline.} = + ## Computes all but 1/64th + ## L(n) ≡ n − ⌊n/64⌋ - (floored(n/64)) + # Introduced in EIP-150 - https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md + # TODO: deactivate it pre-EIP150 + + # Note: The all-but-one-64th calculation should occur after the memory expansion fee is taken + # https://github.com/ethereum/yellowpaper/pull/442 + + result = gas - (gas shr 6) + + # ############### Opcode gas functions ############################## + + func `prefix gasExp`(value: Uint256): GasInt {.nimcall.} = + ## Value is the exponent + + result = static FeeSchedule[GasExp] + if not value.isZero: + result += static(FeeSchedule[GasExpByte]) * (1 + log256(value)) + + func `prefix gasSha3`(activeMemSize, requestedMemSize: Natural): GasInt {.nimcall.} = + + result = `prefix gasMemoryExpansion`(activeMemSize, requestedMemSize) + result += static(FeeSchedule[GasSha3]) + + static(FeeSchedule[GasSha3Word]) * requestedMemSize.wordCount + + func `prefix gasCopy`(value: Uint256): GasInt {.nimcall.} = + ## Value is the size of the input to the CallDataCopy/CodeCopy/ReturnDataCopy function + + result = static(FeeSchedule[GasVeryLow]) + + static(FeeSchedule[GasCopy]) * value.toInt.wordCount + + func `prefix gasExtCodeCopy`(value: Uint256): GasInt {.nimcall.} = + ## Value is the size of the input to the CallDataCopy/CodeCopy/ReturnDataCopy function + + result = static(FeeSchedule[GasVeryLow]) + + static(FeeSchedule[GasCopy]) * value.toInt.wordCount + + func `prefix gasLoadStore`(activeMemSize, requestedMemSize: Natural): GasInt {.nimcall.} = + result = static(FeeSchedule[GasVeryLow]) + result += `prefix gasMemoryExpansion`(activeMemSize, requestedMemSize) + + func `prefix gasSstore`(value: Uint256, gasParams: Gasparams): GasResult {.nimcall.} = + ## Value is word to save + + # workaround for static evaluation not working for if expression + const + gSet = FeeSchedule[GasSset] + gSreset = FeeSchedule[GasSreset] + + # Gas cost - literal translation of Yellow Paper + result.gasCost = if value.isZero.not xor gasParams.s_isStorageEmpty: + gSet + else: + gSreset + + # Refund + if value.isZero xor gasParams.s_isStorageEmpty: + result.gasRefund = static(FeeSchedule[RefundSclear]) + + func `prefix gasLog0`(activeMemSize, requestedMemSize: Natural): GasInt {.nimcall.} = + result = `prefix gasMemoryExpansion`(activeMemSize, requestedMemSize) + + result += static(FeeSchedule[GasLog]) + + static(FeeSchedule[GasLogData]) * (requestedMemSize - activeMemSize) + + func `prefix gasLog1`(activeMemSize, requestedMemSize: Natural): GasInt {.nimcall.} = + result = `prefix gasMemoryExpansion`(activeMemSize, requestedMemSize) + + result += static(FeeSchedule[GasLog]) + + static(FeeSchedule[GasLogData]) * (requestedMemSize - activeMemSize) + + static(FeeSchedule[GasLogTopic]) + + func `prefix gasLog2`(activeMemSize, requestedMemSize: Natural): GasInt {.nimcall.} = + result = `prefix gasMemoryExpansion`(activeMemSize, requestedMemSize) + + result += static(FeeSchedule[GasLog]) + + static(FeeSchedule[GasLogData]) * (requestedMemSize - activeMemSize) + + static(2 * FeeSchedule[GasLogTopic]) + + func `prefix gasLog3`(activeMemSize, requestedMemSize: Natural): GasInt {.nimcall.} = + result = `prefix gasMemoryExpansion`(activeMemSize, requestedMemSize) + + result = static(FeeSchedule[GasLog]) + + static(FeeSchedule[GasLogData]) * (requestedMemSize - activeMemSize) + + static(3 * FeeSchedule[GasLogTopic]) + + func `prefix gasLog4`(activeMemSize, requestedMemSize: Natural): GasInt {.nimcall.} = + result = `prefix gasMemoryExpansion`(activeMemSize, requestedMemSize) + + result = static(FeeSchedule[GasLog]) + + static(FeeSchedule[GasLogData]) * (requestedMemSize - activeMemSize) + + static(4 * FeeSchedule[GasLogTopic]) + + func `prefix gasCall`(value: Uint256, gasParams: Gasparams): GasResult {.nimcall.} = + + # From the Yellow Paper, going through the equation from bottom to top + # https://ethereum.github.io/yellowpaper/paper.pdf#appendix.H + # + # More readable info on the subtleties wiki page: https://github.com/ethereum/wiki/wiki/Subtleties#other-operations + # CALL has a multi-part gas cost: + # + # - 700 base + # - 9000 additional if the value is nonzero + # - 25000 additional if the destination account does not yet exist + # + # The child message of a nonzero-value CALL operation (NOT the top-level message arising from a transaction!) + # gains an additional 2300 gas on top of the gas supplied by the calling account; + # this stipend can be considered to be paid out of the 9000 mandatory additional fee for nonzero-value calls. + # This ensures that a call recipient will always have enough gas to log that it received funds. + # + # EIP150 goes over computation: https://github.com/ethereum/eips/issues/150 + # + # The discussion for the draft EIP-5, which proposes to change the CALL opcode also goes over + # the current implementation - https://github.com/ethereum/EIPs/issues/8 + + + # First we have to take into account the costs of memory expansion: + # Note there is a "bug" in the Ethereum Yellow Paper + # - https://github.com/ethereum/yellowpaper/issues/325 + # μg already includes memory expansion costs but it is not + # plainly explained n the CALL opcode details + + # i.e. Cmem(μ′i) − Cmem(μi) + # Yellow Paper: μ′i ≡ M(M(μi,μs[3],μs[4]),μs[5],μs[6]) + # M is the memory expansion function + # μ′i is passed through gasParams.memRequested + # TODO: + # - Py-EVM has costs for both input and output memory expansion + # https://github.com/ethereum/py-evm/blob/eed0bfe4499b394ee58113408e487e7d35ab88d6/evm/vm/logic/call.py#L56-L57 + # - Parity only for the largest expansion + # https://github.com/paritytech/parity/blob/af1088ef61323f171915555023d8e993aaaed755/ethcore/evm/src/interpreter/gasometer.rs#L192-L195 + # - Go-Ethereum only has one cost + # https://github.com/ethereum/go-ethereum/blob/13af27641829f61d1e6b383e37aab6caae22f2c1/core/vm/gas_table.go#L334 + # ⚠⚠ Py-EVM seems wrong if memory is needed for both in and out. + result.gasCost = `prefix gasMemoryExpansion`( + gasParams.c_activeMemSize, + gasParams.c_memRequested + ) + + # Cnew_account - TODO - pre-EIP158 zero-value call consumed 25000 gas + # https://github.com/ethereum/eips/issues/158 + if gasParams.c_isNewAccount and not value.isZero: + result.gasCost += static(FeeSchedule[GasNewAccount]) + + # Cxfer + if not value.isZero: + result.gasCost += static(FeeSchedule[GasCallValue]) + + # Cextra + result.gasCost += static(FeeSchedule[GasCall]) + let cextra = result.gasCost + + # Cgascap + result.gasCost = if gasParams.c_gasBalance >= result.gasCost: + min( + `prefix all_but_one_64th`(gasParams.c_gasBalance - result.gasCost), + gasParams.c_contract_gas + ) + else: + gasParams.c_contract_gas + + # Ccallgas - Gas sent to the child message + result.gasRefund = result.gasCost + if not value.isZero: + result.gasRefund += static(FeeSchedule[GasCallStipend]) + + # Ccall + result.gasCost += cextra + + func `prefix gasHalt`(activeMemSize, requestedMemSize: Natural): GasInt {.nimcall.} = + `prefix gasMemoryExpansion`(activeMemSize, requestedMemSize) + + func `prefix gasSelfDestruct`(value: Uint256, gasParams: Gasparams): GasResult {.nimcall.} = + # TODO + discard + + # ################################################################################################### + + # TODO - change this `let` into `const` - pending: https://github.com/nim-lang/Nim/issues/8015 + let `ResultGasCostsName`*{.inject.}: GasCosts = block: + # We use a block expression to avoid name redefinition conflicts + # with "fixed" and "dynamic" + + # Syntactic sugar + func fixed(gasFeeKind: static[GasFeeKind]): GasCost = + GasCost(kind: GckFixed, cost: static(FeeSchedule[gasFeeKind])) + + func dynamic(handler: proc(value: Uint256): GasInt {.nimcall.}): GasCost = + GasCost(kind: GckDynamic, d_handler: handler) + + func memExpansion(handler: proc(activeMemSize, memExpansion: Natural): GasInt {.nimcall.}): GasCost = + GasCost(kind: GckMemExpansion, m_handler: handler) + + func complex(handler: proc(value: Uint256, gasParams: GasParams): GasResult {.nimcall.}): GasCost = + GasCost(kind: GckComplex, c_handler: handler) + + # Returned value + fill_enum_table_holes(Op, GasCost(kind: GckInvalidOp)): + [ + # 0s: Stop and Arithmetic Operations + Stop: fixed GasZero, + Add: fixed GasVeryLow, + Mul: fixed GasLow, + Sub: fixed GasVeryLow, + Div: fixed GasLow, + Sdiv: fixed GasLow, + Mod: fixed GasLow, + Smod: fixed GasLow, + Addmod: fixed GasMid, + Mulmod: fixed GasMid, + Exp: dynamic `prefix gasExp`, + SignExtend: fixed GasLow, + + # 10s: Comparison & Bitwise Logic Operations + Lt: fixed GasVeryLow, + Gt: fixed GasVeryLow, + Slt: fixed GasVeryLow, + Sgt: fixed GasVeryLow, + Eq: fixed GasVeryLow, + IsZero: fixed GasVeryLow, + And: fixed GasVeryLow, + Or: fixed GasVeryLow, + Xor: fixed GasVeryLow, + Not: fixed GasVeryLow, + Byte: fixed GasVeryLow, + + # 20s: SHA3 + Sha3: memExpansion `prefix gasSha3`, + + # 30s: Environmental Information + Address: fixed GasBase, + Balance: fixed GasBalance, + Origin: fixed GasBase, + Caller: fixed GasBase, + CallValue: fixed GasBase, + CallDataLoad: fixed GasVeryLow, + CallDataSize: fixed GasBase, + CallDataCopy: dynamic `prefix gasCopy`, + CodeSize: fixed GasBase, + CodeCopy: dynamic `prefix gasCopy`, + GasPrice: fixed GasBase, + ExtCodeSize: fixed GasExtcode, + ExtCodeCopy: dynamic `prefix gasExtCodeCopy`, + ReturnDataSize: fixed GasBase, + ReturnDataCopy: dynamic `prefix gasCopy`, + + # 40s: Block Information + Blockhash: fixed GasBlockhash, + Coinbase: fixed GasBase, + Timestamp: fixed GasBase, + Number: fixed GasBase, + Difficulty: fixed GasBase, + GasLimit: fixed GasBase, + + # 50s: Stack, Memory, Storage and Flow Operations + Pop: fixed GasBase, + Mload: memExpansion `prefix gasLoadStore`, + Mstore: memExpansion `prefix gasLoadStore`, + Mstore8: memExpansion `prefix gasLoadStore`, + Sload: fixed GasSload, + Sstore: complex `prefix gasSstore`, + Jump: fixed GasMid, + JumpI: fixed GasHigh, + Pc: fixed GasBase, + Msize: fixed GasBase, + Gas: fixed GasBase, + JumpDest: fixed GasJumpDest, + + # 60s & 70s: Push Operations + Push1: fixed GasVeryLow, + Push2: fixed GasVeryLow, + Push3: fixed GasVeryLow, + Push4: fixed GasVeryLow, + Push5: fixed GasVeryLow, + Push6: fixed GasVeryLow, + Push7: fixed GasVeryLow, + Push8: fixed GasVeryLow, + Push9: fixed GasVeryLow, + Push10: fixed GasVeryLow, + Push11: fixed GasVeryLow, + Push12: fixed GasVeryLow, + Push13: fixed GasVeryLow, + Push14: fixed GasVeryLow, + Push15: fixed GasVeryLow, + Push16: fixed GasVeryLow, + Push17: fixed GasVeryLow, + Push18: fixed GasVeryLow, + Push19: fixed GasVeryLow, + Push20: fixed GasVeryLow, + Push21: fixed GasVeryLow, + Push22: fixed GasVeryLow, + Push23: fixed GasVeryLow, + Push24: fixed GasVeryLow, + Push25: fixed GasVeryLow, + Push26: fixed GasVeryLow, + Push27: fixed GasVeryLow, + Push28: fixed GasVeryLow, + Push29: fixed GasVeryLow, + Push30: fixed GasVeryLow, + Push31: fixed GasVeryLow, + Push32: fixed GasVeryLow, + + # 80s: Duplication Operations + Dup1: fixed GasVeryLow, + Dup2: fixed GasVeryLow, + Dup3: fixed GasVeryLow, + Dup4: fixed GasVeryLow, + Dup5: fixed GasVeryLow, + Dup6: fixed GasVeryLow, + Dup7: fixed GasVeryLow, + Dup8: fixed GasVeryLow, + Dup9: fixed GasVeryLow, + Dup10: fixed GasVeryLow, + Dup11: fixed GasVeryLow, + Dup12: fixed GasVeryLow, + Dup13: fixed GasVeryLow, + Dup14: fixed GasVeryLow, + Dup15: fixed GasVeryLow, + Dup16: fixed GasVeryLow, + + # 90s: Exchange Operations + Swap1: fixed GasVeryLow, + Swap2: fixed GasVeryLow, + Swap3: fixed GasVeryLow, + Swap4: fixed GasVeryLow, + Swap5: fixed GasVeryLow, + Swap6: fixed GasVeryLow, + Swap7: fixed GasVeryLow, + Swap8: fixed GasVeryLow, + Swap9: fixed GasVeryLow, + Swap10: fixed GasVeryLow, + Swap11: fixed GasVeryLow, + Swap12: fixed GasVeryLow, + Swap13: fixed GasVeryLow, + Swap14: fixed GasVeryLow, + Swap15: fixed GasVeryLow, + Swap16: fixed GasVeryLow, + + # a0s: Logging Operations + Log0: memExpansion `prefix gasLog0`, + Log1: memExpansion `prefix gasLog1`, + Log2: memExpansion `prefix gasLog2`, + Log3: memExpansion `prefix gasLog3`, + Log4: memExpansion `prefix gasLog4`, + + # f0s: System operations + Create: fixed GasCreate, + Call: complex `prefix gasCall`, + CallCode: complex `prefix gasCall`, + Return: memExpansion `prefix gasHalt`, + DelegateCall: complex `prefix gasCall`, + StaticCall: complex `prefix gasCall`, + Op.Revert: memExpansion `prefix gasHalt`, + Invalid: fixed GasZero, + SelfDestruct: complex `prefix gasSelfDestruct` + ] + +# Generate the fork-specific gas costs tables +const + BaseGasFees: GasFeeSchedule = [ + # Fee Schedule at for the initial Ethereum forks + GasZero: 0'i64, + GasBase: 2, + GasVeryLow: 3, + GasLow: 5, + GasMid: 8, + GasHigh: 10, + GasExtCode: 20, # Changed to 700 in Tangerine (EIP150) + GasBalance: 20, # Changed to 400 in Tangerine (EIP150) + GasSload: 50, # Changed to 200 in Tangerine (EIP150) + GasJumpDest: 1, + GasSset: 20_000, + GasSreset: 5_000, + RefundSclear: 15_000, + RefundSelfDestruct: 24_000, + GasSelfDestruct: 0, # Changed to 5000 in Tangerine (EIP150) + GasCreate: 32000, + GasCodeDeposit: 200, + GasCall: 40, # Changed to 700 in Tangerine (EIP150) + GasCallValue: 9000, + GasCallStipend: 2300, + GasNewAccount: 25_000, + GasExp: 10, + GasExpByte: 10, # Changed to 50 in Spurious Dragon (EIP160) + GasMemory: 3, + GasTXCreate: 32000, + GasTXDataZero: 4, + GasTXDataNonZero: 68, + GasTransaction: 21000, + GasLog: 375, + GasLogData: 8, + GasLogTopic: 375, + GasSha3: 30, + GasSha3Word: 6, + GasCopy: 3, + GasBlockhash: 20 + # GasQuadDivisor: 100 # Unused, do not confuse with the quadratic coefficient 512 for memory expansion + ] + +# Create the schedule for each forks +func tangerineGasFees(previous_fees: GasFeeSchedule): GasFeeSchedule = # https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md - result = baseCosts + result = previous_fees result[GasSload] = 200 result[GasSelfDestruct] = 5000 result[GasBalance] = 400 result[GasCall] = 40 -const TangerineGasCosts* = BaseGasCosts.tangerineGasCosts +func spuriousGasFees(previous_fees: GasFeeSchedule): GasFeeSchedule = + # https://github.com/ethereum/EIPs/blob/master/EIPS/eip-160.md + result = previous_fees + result[GasExpByte] = 50 + +const + TangerineGasFees = BaseGasFees.tangerineGasFees + SpuriousGasFees = TangerineGasFees.spuriousGasFees + +gasCosts(BaseGasFees, base, BaseGasCosts) +gasCosts(TangerineGasFees, tangerine, TangerineGasCosts) diff --git a/nimbus/vm_types.nim b/nimbus/vm_types.nim index f0ed30239..5f8e2e2fc 100644 --- a/nimbus/vm_types.nim +++ b/nimbus/vm_types.nim @@ -9,10 +9,10 @@ import tables, constants, vm_state, opcode_values, stint, eth_common, - vm / [code_stream, memory, stack], + vm / [code_stream, memory, stack, forks/gas_costs], ./logging -export GasInt +export GasInt, gas_costs type BaseComputation* = ref object of RootObj @@ -30,9 +30,9 @@ type logEntries*: seq[(EthAddress, seq[UInt256], string)] shouldEraseReturnData*: bool accountsToDelete*: Table[EthAddress, EthAddress] - opcodes*: Table[Op, Opcode] # TODO array[Op, Opcode] + opcodes*: Table[Op, proc(computation: var BaseComputation){.nimcall.}] precompiles*: Table[string, Opcode] - gasCosts*: GasCosts # TODO separate opcode processing and gas computation + gasCosts*: GasCosts # TODO - avoid allocating memory for this const Error* = ref object info*: string @@ -40,16 +40,10 @@ type erasesReturnData*: bool Opcode* = ref object of RootObj + # TODO can't use a stack-allocated object because + # "BaseComputation is not a concrete type" + # TODO: We can probably remove this. kind*: Op - #of VARIABLE_GAS_COST_OPS: - # gasCostHandler*: proc(computation: var BaseComputation): UInt256 - ## so, we could have special logic that separates all gas cost calculations - ## from actual opcode execution - ## that's what parity does: - ## it uses the peek methods of the stack and calculates the cost - ## then it actually pops/pushes stuff in exec - ## I followed the py-evm approach which does that in opcode logic - gasCostKind*: GasCostKind runLogic*: proc(computation: var BaseComputation) GasMeter* = ref object @@ -58,30 +52,6 @@ type startGas*: GasInt gasRemaining*: GasInt - GasCostKind* = enum - GasZero - GasBase - GasVeryLow - GasLow - GasMid - GasHigh - GasSload - GasJumpDest - GasSset - GasSreset - GasExtCode - GasCoinbase - GasSelfDestruct - GasInHandler - GasRefundSclear - - GasBalance - GasCall - GasExp - GasSHA3 - - GasCosts* = array[GasCostKind, GasInt] - Message* = ref object # A message for VM computation diff --git a/tests/all_tests.nim b/tests/all_tests.nim index 443d7bd5d..711408085 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -9,6 +9,6 @@ import ./test_code_stream, ./test_gas_meter, ./test_memory, ./test_stack, - ./test_opcode, - ./test_vm + ./test_opcode + # ./test_vm # ./test_vm_json diff --git a/tests/test_opcode.nim b/tests/test_opcode.nim index f06c1f65c..ca738c8ec 100644 --- a/tests/test_opcode.nim +++ b/tests/test_opcode.nim @@ -42,7 +42,7 @@ proc testCode(code: string, initialGas: GasInt, blockNum: UInt256): BaseComputat c.displayDecompiled() var computation = newBaseComputation(vm.state, message) - computation.opcodes = OPCODE_TABLE + computation.opcodes = OpLogic # TODO remove this need computation.precompiles = initTable[string, Opcode]() computation = computation.applyComputation(vm.state, message) diff --git a/tests/test_vm.nim b/tests/test_vm.nim index c179acc41..a56a5b0d0 100644 --- a/tests/test_vm.nim +++ b/tests/test_vm.nim @@ -30,7 +30,7 @@ suite "VM": # check(not computation.isError) let - txGas = tx.gasPrice * constants.GAS_TX + # txGas = tx.gasPrice * constants.GAS_TX state_db = vm.state.readOnlyStateDB b = vm.`block` diff --git a/tests/test_vm_json.nim b/tests/test_vm_json.nim index c3484fda2..9c533e813 100644 --- a/tests/test_vm_json.nim +++ b/tests/test_vm_json.nim @@ -9,12 +9,14 @@ import unittest, strformat, strutils, sequtils, tables, stint, json, ospaths, times, ./test_helpers, ../nimbus/[constants, errors, logging], - ../nimbus/[chain, vm_state, computation, opcode, vm_types, opcode_table], + ../nimbus/[chain, vm_state, computation, opcode, vm_types], ../nimbus/utils/[header, padding], ../nimbus/vm/[gas_meter, message, code_stream, stack], ../nimbus/vm/forks/vm_forks, ../nimbus/db/[db_chain, state_db, backends/memory_backend], eth_common +from ../nimbus/opcode_table import OpLogic + proc testFixture(fixtures: JsonNode, testStatusIMPL: var TestStatus) suite "vm json tests": @@ -58,7 +60,7 @@ proc testFixture(fixtures: JsonNode, testStatusIMPL: var TestStatus) = c.displayDecompiled() var computation = newBaseComputation(vm.state, message) - computation.opcodes = OPCODE_TABLE + computation.opcodes = OpLogic computation.precompiles = initTable[string, Opcode]() computation = computation.applyComputation(vm.state, message) @@ -85,7 +87,7 @@ proc testFixture(fixtures: JsonNode, testStatusIMPL: var TestStatus) = let expectedGasRemaining = fixture{"gas"}.getHexadecimalInt let actualGasRemaining = gasMeter.gasRemaining - checkpoint(&"{actualGasRemaining} {expectedGasRemaining}") + checkpoint(&"Remaining: {actualGasRemaining} - Expected: {expectedGasRemaining}") check(actualGasRemaining == expectedGasRemaining or computation.code.hasSStore() and (actualGasRemaining > expectedGasRemaining and (actualGasRemaining - expectedGasRemaining) mod 15_000 == 0 or