diff --git a/nimbus/constants.nim b/nimbus/constants.nim index 57aff9c43..23ec08ffc 100644 --- a/nimbus/constants.nim +++ b/nimbus/constants.nim @@ -52,3 +52,11 @@ const MAX_PREV_HEADER_DEPTH* = 256.toBlockNumber MaxCallDepth* = 1024 + + ## Fork specific constants + + # See: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-170.md + # and: https://github.com/ethereum/EIPs/issues/170 + EIP170_CODE_SIZE_LIMIT* = 24577 + + diff --git a/nimbus/db/db_chain.nim b/nimbus/db/db_chain.nim index 904e8697a..405e5c6f8 100644 --- a/nimbus/db/db_chain.nim +++ b/nimbus/db/db_chain.nim @@ -238,8 +238,10 @@ proc persistBlockToDb*(self: BaseChainDB; blk: Block) = # receiptDb[indexKey] = rlp.encode(receipt) # return receiptDb.rootHash -# proc snapshot*(self: BaseChainDB): UUID = -# return self.db.snapshot() +#proc snapshot*(self: BaseChainDB): UUID = + # Snapshots are a combination of the state_root at the time of the + # snapshot and the id of the changeset from the journaled DB. + #return self.db.snapshot() # proc commit*(self: BaseChainDB; checkpoint: UUID): void = # self.db.commit(checkpoint) @@ -248,6 +250,7 @@ proc persistBlockToDb*(self: BaseChainDB; blk: Block) = # self.db.clear() proc getStateDb*(self: BaseChainDB; stateRoot: Hash256; readOnly: bool = false): AccountStateDB = + # TODO: readOnly is not used. result = newAccountStateDB(self.db, stateRoot) diff --git a/nimbus/vm/computation.nim b/nimbus/vm/computation.nim index 0b405e665..e5b3c2936 100644 --- a/nimbus/vm/computation.nim +++ b/nimbus/vm/computation.nim @@ -10,7 +10,8 @@ import eth_common, ../constants, ../errors, ../validation, ../vm_state, ../vm_types, ./interpreter/[opcode_values, gas_meter, gas_costs, vm_forks], - ./code_stream, ./memory, ./message, ./stack + ./code_stream, ./memory, ./message, ./stack, ../db/[state_db, db_chain], + ../utils/header, byteutils, ranges logScope: topics = "vm computation" @@ -39,16 +40,30 @@ template isSuccess*(c: BaseComputation): bool = template isError*(c: BaseComputation): bool = not c.isSuccess -proc shouldBurnGas*(c: BaseComputation): bool = +func shouldBurnGas*(c: BaseComputation): bool = c.isError and c.error.burnsGas -proc shouldEraseReturnData*(c: BaseComputation): bool = +func shouldEraseReturnData*(c: BaseComputation): bool = c.isError and c.error.erasesReturnData func bytesToHex(x: openarray[byte]): string {.inline.} = ## TODO: use seq[byte] for raw data and delete this proc foldl(x, a & b.int.toHex(2).toLowerAscii, "0x") +func output*(c: BaseComputation): seq[byte] = + if c.shouldEraseReturnData: + @[] + else: + c.rawOutput + +func `output=`*(c: var BaseComputation, value: openarray[byte]) = + c.rawOutput = @value + +proc outputHex*(c: BaseComputation): string = + if c.shouldEraseReturnData: + return "0x" + c.rawOutput.bytesToHex + proc prepareChildMessage*( c: var BaseComputation, gas: GasInt, @@ -70,19 +85,119 @@ proc prepareChildMessage*( code, childOptions) -func output*(c: BaseComputation): seq[byte] = - if c.shouldEraseReturnData: - @[] +proc applyMessage(computation: var BaseComputation) = + var transaction = computation.vmState.beginTransaction() + defer: transaction.dispose() + + if computation.msg.depth > STACK_DEPTH_LIMIT: + raise newException(StackDepthError, "Stack depth limit reached") + + if computation.msg.value != 0: + let senderBalance = + computation.vmState.chainDb.getStateDb( + computation.vmState.blockHeader.hash, false). + getBalance(computation.msg.sender) + + if sender_balance < computation.msg.value: + raise newException(InsufficientFunds, + &"Insufficient funds: {senderBalance} < {computation.msg.value}" + ) + + computation.vmState.mutateStateDb: + db.deltaBalance(computation.msg.sender, -1 * computation.msg.value) + db.deltaBalance(computation.msg.storage_address, computation.msg.value) + + debug "Apply message", + value = computation.msg.value, + sender = computation.msg.sender.toHex, + address = computation.msg.storage_address.toHex + + computation.opcodeExec(computation) + + if not computation.isError: + transaction.commit() + +proc applyCreateMessage(fork: Fork, computation: var BaseComputation) = + computation.applyMessage() + + var transaction: DbTransaction + defer: transaction.safeDispose() + + if fork >= FkFrontier: + transaction = computation.vmState.beginTransaction() + + if computation.isError: + return else: - c.rawOutput + let contractCode = computation.output + if contractCode.len > 0: + if fork >= FkSpurious and contractCode.len >= EIP170_CODE_SIZE_LIMIT: + raise newException(OutOfGas, &"Contract code size exceeds EIP170 limit of {EIP170_CODE_SIZE_LIMIT}. Got code of size: {contractCode.len}") -func `output=`*(c: var BaseComputation, value: openarray[byte]) = - c.rawOutput = @value + try: + computation.gasMeter.consumeGas( + computation.gasCosts[Create].m_handler(0, 0, contractCode.len), + reason = "Write contract code for CREATE") -proc outputHex*(c: BaseComputation): string = - if c.shouldEraseReturnData: - return "0x" - c.rawOutput.bytesToHex + let storageAddr = computation.msg.storage_address + debug "SETTING CODE", + address = storageAddr.toHex, + length = len(contract_code), + hash = contractCode.rlpHash + + computation.vmState.mutateStateDb: + db.setCode(storageAddr, contractCode.toRange) + + if transaction != nil: + transaction.commit() + + except OutOfGas: + if fork == FkFrontier: + computation.output = @[] + else: + # Different from Frontier: + # Reverts state on gas failure while writing contract code. + # TODO: Revert snapshot + discard + else: + if transaction != nil: + transaction.commit() + +proc generateChildComputation*(fork: Fork, computation: BaseComputation, childMsg: Message): BaseComputation = + var childComp = newBaseComputation( + computation.vmState, + computation.vmState.blockHeader.blockNumber, + childMsg) + + # Copy the fork op code executor proc (assumes child computation is in the same fork) + childComp.opCodeExec = computation.opCodeExec + + if childMsg.isCreate: + fork.applyCreateMessage(childComp) + else: + applyMessage(childComp) + return childComp + +proc addChildComputation(fork: Fork, computation: BaseComputation, child: BaseComputation) = + if child.isError: + if child.msg.isCreate: + computation.returnData = child.output + elif child.shouldBurnGas: + computation.returnData = @[] + else: + computation.returnData = child.output + else: + if child.msg.isCreate: + computation.returnData = @[] + else: + computation.returnData = child.output + computation.children.add(child) + +proc applyChildComputation*(computation: BaseComputation, childMsg: Message): BaseComputation = + ## Apply the vm message childMsg as a child computation. + let fork = computation.vmState.blockHeader.blockNumber.toFork + result = fork.generateChildComputation(computation, childMsg) + fork.addChildComputation(computation, result) proc registerAccountForDeletion*(c: var BaseComputation, beneficiary: EthAddress) = if c.msg.storageAddress in c.accountsToDelete: diff --git a/nimbus/vm/interpreter/gas_costs.nim b/nimbus/vm/interpreter/gas_costs.nim index 65e50580c..36164beda 100644 --- a/nimbus/vm/interpreter/gas_costs.nim +++ b/nimbus/vm/interpreter/gas_costs.nim @@ -168,6 +168,9 @@ template gasCosts(FeeSchedule: GasFeeSchedule, prefix, ResultGasCostsName: untyp if not value.isZero: result += static(FeeSchedule[GasExpByte]) * (1 + log256(value)) + func `prefix gasCreate`(currentMemSize, memOffset, memLength: Natural): GasInt {.nimcall.} = + result = static(FeeSchedule[GasCodeDeposit]) * memLength + func `prefix gasSha3`(currentMemSize, memOffset, memLength: Natural): GasInt {.nimcall.} = result = `prefix gasMemoryExpansion`(currentMemSize, memOffset, memLength) @@ -494,7 +497,7 @@ template gasCosts(FeeSchedule: GasFeeSchedule, prefix, ResultGasCostsName: untyp Log4: memExpansion `prefix gasLog4`, # f0s: System operations - Create: fixed GasCreate, # TODO, dynamic cost + Create: memExpansion `prefix gasCreate`, # TODO: Change to dynamic? Call: complex `prefix gasCall`, CallCode: complex `prefix gasCall`, Return: memExpansion `prefix gasHalt`, diff --git a/nimbus/vm/interpreter/opcodes_impl.nim b/nimbus/vm/interpreter/opcodes_impl.nim index 2fb641f61..9cf08b0ed 100644 --- a/nimbus/vm/interpreter/opcodes_impl.nim +++ b/nimbus/vm/interpreter/opcodes_impl.nim @@ -695,20 +695,20 @@ template genCall(callName: untyped): untyped = computation.memory.extend(memInPos, memInLen) computation.memory.extend(memOutPos, memOutLen) - let callData = computation.memory.read(memInPos, memInLen) - - ##### getBalance type error: expression 'db' is of type: proc (vmState: untyped, readOnly: untyped, handler: untyped): untyped{.noSideEffect, gcsafe, locks: .} - # computation.vmState.db(readOnly = true): - # let senderBalance = db.getBalance(computation.msg.storageAddress) # TODO check gas balance rollover - - let insufficientFunds = false # shouldTransferValue and senderBalance < value - let stackTooDeep = computation.msg.depth >= MaxCallDepth + let + callData = computation.memory.read(memInPos, memInLen) + senderBalance = computation.vmState.readOnlyStateDb.getBalance(computation.msg.storageAddress) + # TODO check gas balance rollover + # TODO: shouldTransferValue in py-evm is: + # True for call and callCode + # False for callDelegate and callStatic + insufficientFunds = senderBalance < value # TODO: and shouldTransferValue + stackTooDeep = computation.msg.depth >= MaxCallDepth if insufficientFunds or stackTooDeep: computation.returnData = @[] var errMessage: string if insufficientFunds: - let senderBalance = -1 # TODO workaround # Note: for some reason we can't use strformat here, we get undeclared identifiers errMessage = &"Insufficient Funds: have: " & $senderBalance & "need: " & $value elif stackTooDeep: @@ -721,11 +721,11 @@ template genCall(callName: untyped): untyped = push: 0 return - ##### getCode type error: expression 'db' is of type: proc (vmState: untyped, readOnly: untyped, handler: untyped): untyped{.noSideEffect, gcsafe, locks: .} - # computation.vmState.db(readOnly = true): - # let code = if codeAddress != ZERO_ADDRESS: db.getCode(codeAddress) - # else: db.getCode(to) - let code: seq[byte] = @[] + let code = + if codeAddress != ZERO_ADDRESS: + computation.vmState.readOnlyStateDb.getCode(codeAddress) + else: + computation.vmState.readOnlyStateDb.getCode(to) var childMsg = prepareChildMessage( computation, @@ -733,17 +733,14 @@ template genCall(callName: untyped): untyped = to, value, callData, - code, + code.toSeq, MessageOptions(flags: flags) ) if sender != ZERO_ADDRESS: childMsg.sender = sender - # let childComputation = applyChildBaseComputation(computation, childMsg) - var childComputation: BaseComputation # TODO - stub - new childComputation - childComputation.gasMeter.init(0) + var childComputation = applyChildComputation(computation, childMsg) if childComputation.isError: push: 0 diff --git a/nimbus/vm/interpreter/vm_forks.nim b/nimbus/vm/interpreter/vm_forks.nim index 72e9cc7d4..442585c93 100644 --- a/nimbus/vm/interpreter/vm_forks.nim +++ b/nimbus/vm/interpreter/vm_forks.nim @@ -48,3 +48,15 @@ proc toFork*(blockNumber: UInt256): Fork = elif blockNumber < forkBlocks[FkByzantium]: FkSpurious else: FkByzantium # Update for constantinople when announced + +proc `$`*(fork: Fork): string = + case fork + of FkFrontier: result = "Frontier" + of FkThawing: result = "Thawing" + of FkHomestead: result = "Homestead" + of FkDao: result = "Dao" + of FkTangerine: result = "Tangerine Whistle" + of FkSpurious: result = "Spurious Dragon" + of FkByzantium: result = "Byzantium" + else: result = "UNKNOWN FORK" + diff --git a/nimbus/vm/interpreter_dispatch.nim b/nimbus/vm/interpreter_dispatch.nim index 46d529024..a64b9bd62 100644 --- a/nimbus/vm/interpreter_dispatch.nim +++ b/nimbus/vm/interpreter_dispatch.nim @@ -222,12 +222,22 @@ macro genFrontierDispatch(computation: BaseComputation): untyped = proc frontierVM(computation: var BaseComputation) = genFrontierDispatch(computation) +proc updateOpcodeExec*(computation: var BaseComputation, fork: Fork) = + case fork + of FkFrontier: + computation.opCodeExec = frontierVM + computation.frontierVM() + else: + raise newException(VMError, "Unknown or not implemented fork: " & $fork) + +proc updateOpcodeExec*(computation: var BaseComputation) = + let fork = computation.vmState.blockHeader.blockNumber.toFork + computation.updateOpcodeExec(fork) + proc executeOpcodes*(computation: var BaseComputation) = + # TODO: Optimise getting fork and updating opCodeExec only when necessary let fork = computation.vmState.blockHeader.blockNumber.toFork try: - case fork - of FkFrontier: computation.frontierVM() - else: - raise newException(ValueError, "not implemented fork: " & $fork) + computation.updateOpcodeExec(fork) except VMError: computation.error = Error(info: getCurrentExceptionMsg()) diff --git a/nimbus/vm/message.nim b/nimbus/vm/message.nim index 869749a56..5ef0be4aa 100644 --- a/nimbus/vm/message.nim +++ b/nimbus/vm/message.nim @@ -83,5 +83,5 @@ proc `storageAddress`*(message: Message): EthAddress = else: message.destination -proc isCreate(message: Message): bool = +proc isCreate*(message: Message): bool = message.destination == CREATE_CONTRACT_ADDRESS diff --git a/nimbus/vm_state.nim b/nimbus/vm_state.nim index e2b1999c6..5ebf332a9 100644 --- a/nimbus/vm_state.nim +++ b/nimbus/vm_state.nim @@ -7,7 +7,7 @@ import macros, strformat, tables, - eth_common, + eth_common, eth_trie/db, ./constants, ./errors, ./transaction, ./db/[db_chain, state_db], ./utils/header @@ -118,3 +118,9 @@ template mutateStateDB*(vmState: BaseVMState, body: untyped) = proc readOnlyStateDB*(vmState: BaseVMState): AccountStateDB {.inline.}= vmState.chaindb.getStateDb(vmState.blockHeader.stateRoot, readOnly = true) + +export DbTransaction, commit, rollback, dispose, safeDispose + +proc beginTransaction*(vmState: BaseVMState): DbTransaction = + vmState.chaindb.db.beginTransaction() + diff --git a/nimbus/vm_types.nim b/nimbus/vm_types.nim index a0bdf2a8c..1cdafb0b4 100644 --- a/nimbus/vm_types.nim +++ b/nimbus/vm_types.nim @@ -14,6 +14,8 @@ import type + OpcodeExecutor* = proc(computation: var BaseComputation) + BaseComputation* = ref object of RootObj # The execution computation vmState*: BaseVMState @@ -32,6 +34,7 @@ type opcodes*: Table[Op, proc(computation: var BaseComputation){.nimcall.}] precompiles*: Table[string, Opcode] gasCosts*: GasCosts # TODO - will be hidden at a lower layer + opCodeExec*: OpcodeExecutor Error* = ref object info*: string diff --git a/tests/test_generalstate_json.nim b/tests/test_generalstate_json.nim index af8ab7652..64bd5a1e0 100644 --- a/tests/test_generalstate_json.nim +++ b/tests/test_generalstate_json.nim @@ -52,7 +52,7 @@ proc testFixture(fixtures: JsonNode, testStatusIMPL: var TestStatus) = let gas_cost = transaction.gasLimit.u256 * transaction.gasPrice.u256 var memDb = newMemDB() - var vmState = newBaseVMState(header, newBaseChainDB(trieDB memDb)) + var vmState = newBaseVMState(header, newBaseChainDB(newMemoryDb())) vmState.mutateStateDB: setupStateDB(fixture{"pre"}, db) diff --git a/tests/test_opcode.nim b/tests/test_opcode.nim index d279a8c3d..b6ef21776 100644 --- a/tests/test_opcode.nim +++ b/tests/test_opcode.nim @@ -19,7 +19,7 @@ from eth_common import GasInt proc testCode(code: string, initialGas: GasInt, blockNum: UInt256): BaseComputation = let header = BlockHeader(blockNumber: blockNum) var memDb = newMemDB() - var vmState = newBaseVMState(header, newBaseChainDB(trieDB memDb)) + var vmState = newBaseVMState(header, newBaseChainDB(newMemoryDb())) # coinbase: "", # difficulty: fixture{"env"}{"currentDifficulty"}.getHexadecimalInt.u256, diff --git a/tests/test_rpc.nim b/tests/test_rpc.nim index 373ec4a4f..89ec268b4 100644 --- a/tests/test_rpc.nim +++ b/tests/test_rpc.nim @@ -45,7 +45,7 @@ proc doTests = emptyRlpHash = keccak256.digest(rlp.encode("").toOpenArray) header = BlockHeader(stateRoot: emptyRlpHash) var - chain = newBaseChainDB(trieDB newMemDB()) + chain = newBaseChainDB(newMemoryDb()) state = newBaseVMState(header, chain) ethNode.chain = chain diff --git a/tests/test_vm_json.nim b/tests/test_vm_json.nim index bbd079981..aec200b4d 100644 --- a/tests/test_vm_json.nim +++ b/tests/test_vm_json.nim @@ -40,7 +40,7 @@ proc testFixture(fixtures: JsonNode, testStatusIMPL: var TestStatus) = ) var memDb = newMemDB() - var vmState = newBaseVMState(header, newBaseChainDB(trieDB memDb)) + var vmState = newBaseVMState(header, newBaseChainDB(newMemoryDB())) let fexec = fixture["exec"] var code: seq[byte] vmState.mutateStateDB: