diff --git a/src/main/scala/io/iohk/ethereum/vm/OpCode.scala b/src/main/scala/io/iohk/ethereum/vm/OpCode.scala index 45ddb546ec..645a162eb4 100644 --- a/src/main/scala/io/iohk/ethereum/vm/OpCode.scala +++ b/src/main/scala/io/iohk/ethereum/vm/OpCode.scala @@ -4,7 +4,9 @@ import akka.util.ByteString import io.iohk.ethereum.crypto.kec256 import io.iohk.ethereum.domain.{Account, Address, TxLogEntry, UInt256} import io.iohk.ethereum.domain.UInt256._ -import io.iohk.ethereum.vm.BlockchainConfigForEvm.EthForks +import io.iohk.ethereum.vm.BlockchainConfigForEvm.EtcForks.EtcFork +import io.iohk.ethereum.vm.BlockchainConfigForEvm.{EtcForks, EthForks} +import io.iohk.ethereum.vm.BlockchainConfigForEvm.EthForks.EthFork // scalastyle:off magic.number // scalastyle:off number.of.types @@ -593,9 +595,17 @@ case object MSTORE8 extends OpCode(0x53, 2, 0, _.G_verylow) { case object SSTORE extends OpCode(0x55, 2, 0, _.G_zero) { protected def exec[W <: WorldStateProxy[W, S], S <: Storage[S]](state: ProgramState[W, S]): ProgramState[W, S] = { + val currentBlockNumber = state.env.blockHeader.number + val etcFork = state.config.blockchainConfig.etcForkForBlockNumber(currentBlockNumber) + val ethFork = state.config.blockchainConfig.ethForkForBlockNumber(currentBlockNumber) + + val eip2200Enabled = isEip2200Enabled(etcFork, ethFork) + val eip1283Enabled = isEip1283Enabled(ethFork) + val (Seq(offset, newValue), stack1) = state.stack.pop(2) val currentValue = state.storage.load(offset) - val refund: BigInt = if (isEip1283Enabled(state)) { + + val refund: BigInt = if (eip2200Enabled || eip1283Enabled) { val originalValue = state.originalWorld.getStorage(state.ownAddress).load(offset) if (currentValue != newValue.toBigInt) { if (originalValue == currentValue) { // fresh slot @@ -629,7 +639,6 @@ case object SSTORE extends OpCode(0x55, 2, 0, _.G_zero) { else 0 } - val updatedStorage = state.storage.store(offset, newValue) state.withStack(stack1).withStorage(updatedStorage).refundGas(refund).step() } @@ -637,8 +646,17 @@ case object SSTORE extends OpCode(0x55, 2, 0, _.G_zero) { protected def varGas[W <: WorldStateProxy[W, S], S <: Storage[S]](state: ProgramState[W, S]): BigInt = { val (Seq(offset, newValue), _) = state.stack.pop(2) val currentValue = state.storage.load(offset) - if (isEip1283Enabled(state)) { - // https://eips.ethereum.org/EIPS/eip-1283 + + val currentBlockNumber = state.env.blockHeader.number + val etcFork = state.config.blockchainConfig.etcForkForBlockNumber(currentBlockNumber) + val ethFork = state.config.blockchainConfig.ethForkForBlockNumber(currentBlockNumber) + + val eip2200Enabled = isEip2200Enabled(etcFork, ethFork) + val eip1283Enabled = isEip1283Enabled(ethFork) + + if(eip2200Enabled && state.gas <= state.config.feeSchedule.G_callstipend){ + state.config.feeSchedule.G_callstipend + 1 // Out of gas error + } else if (eip2200Enabled || eip1283Enabled) { if (currentValue == newValue.toBigInt) { // no-op state.config.feeSchedule.G_sload } else { @@ -663,10 +681,11 @@ case object SSTORE extends OpCode(0x55, 2, 0, _.G_zero) { override protected def availableInContext[W <: WorldStateProxy[W, S], S <: Storage[S]]: ProgramState[W, S] => Boolean = !_.staticCtx - private def isEip1283Enabled[W <: WorldStateProxy[W, S], S <: Storage[S]](state: ProgramState[W, S]): Boolean = { - val blockNumber = state.env.blockHeader.number - state.config.blockchainConfig.ethForkForBlockNumber(blockNumber) == EthForks.Constantinople - } + // https://eips.ethereum.org/EIPS/eip-1283 + private def isEip1283Enabled(ethFork: EthFork): Boolean = ethFork == EthForks.Constantinople + + // https://eips.ethereum.org/EIPS/eip-2200 + private def isEip2200Enabled(etcFork: EtcFork, ethFork: EthFork): Boolean = (ethFork >= EthForks.Istanbul || etcFork >= EtcForks.Phoenix) } case object JUMP extends OpCode(0x56, 1, 0, _.G_mid) with ConstGas { diff --git a/src/test/scala/io/iohk/ethereum/vm/Fixtures.scala b/src/test/scala/io/iohk/ethereum/vm/Fixtures.scala index fbeb4b4765..e20a57d8e3 100644 --- a/src/test/scala/io/iohk/ethereum/vm/Fixtures.scala +++ b/src/test/scala/io/iohk/ethereum/vm/Fixtures.scala @@ -4,6 +4,7 @@ object Fixtures { val ConstantinopleBlockNumber = 200 val PetersburgBlockNumber = 400 + val PhoenixBlockNumber = 600 val blockchainConfig = BlockchainConfigForEvm( // block numbers are irrelevant @@ -19,7 +20,7 @@ object Fixtures { atlantisBlockNumber = 0, aghartaBlockNumber = 0, petersburgBlockNumber = PetersburgBlockNumber, - phoenixBlockNumber = 0, + phoenixBlockNumber = PhoenixBlockNumber, chainId = 0x3d.toByte ) diff --git a/src/test/scala/io/iohk/ethereum/vm/Generators.scala b/src/test/scala/io/iohk/ethereum/vm/Generators.scala index 9e06b3d3d8..1c63e7838d 100644 --- a/src/test/scala/io/iohk/ethereum/vm/Generators.scala +++ b/src/test/scala/io/iohk/ethereum/vm/Generators.scala @@ -88,7 +88,8 @@ object Generators extends ObjectGenerators { valueGen: Gen[UInt256] = getUInt256Gen(), blockNumberGen: Gen[UInt256] = getUInt256Gen(0, 300), evmConfig: EvmConfig = EvmConfig.PhoenixConfigBuilder(blockchainConfig), - returnDataGen: Gen[ByteString] = getByteStringGen(0, 0) + returnDataGen: Gen[ByteString] = getByteStringGen(0, 0), + isTopHeader: Boolean = false ): Gen[PS] = for { stack <- stackGen @@ -102,7 +103,7 @@ object Generators extends ObjectGenerators { blockPlacement <- getUInt256Gen(0, blockNumber) returnData <- returnDataGen - blockHeader = exampleBlockHeader.copy(number = blockNumber - blockPlacement) + blockHeader = exampleBlockHeader.copy(number = if(isTopHeader) blockNumber else blockNumber - blockPlacement) world = MockWorldState(numberOfHashes = blockNumber - 1) .saveCode(ownerAddr, code) diff --git a/src/test/scala/io/iohk/ethereum/vm/OpCodeGasSpec.scala b/src/test/scala/io/iohk/ethereum/vm/OpCodeGasSpec.scala index 97fc11c360..051749c950 100644 --- a/src/test/scala/io/iohk/ethereum/vm/OpCodeGasSpec.scala +++ b/src/test/scala/io/iohk/ethereum/vm/OpCodeGasSpec.scala @@ -412,7 +412,12 @@ class OpCodeGasSpec extends FunSuite with OpCodeTesting with Matchers with Prope } test(SSTORE) { op => - // Before Constantinople + // Before Constantinople + Petersburg + // Constantinople + Phoenix tested in SSTOREOpCodeGasPostConstantinopleSpec + + val petersburgConfig = EvmConfig.PetersburgConfigBuilder(blockchainConfig) + import petersburgConfig.feeSchedule._ + val storage = MockStorage.Empty.store(Zero, One) val table = Table[UInt256, UInt256, BigInt, BigInt](("offset", "value", "expectedGas", "expectedRefund"), (0, 1, G_sreset, 0), @@ -423,10 +428,14 @@ class OpCodeGasSpec extends FunSuite with OpCodeTesting with Matchers with Prope forAll(table) { (offset, value, expectedGas, _) => val stackIn = Stack.empty().push(value).push(offset) - val stateIn = getProgramStateGen(blockNumberGen = Gen.frequency( - (1, getUInt256Gen(0, Fixtures.ConstantinopleBlockNumber - 1)), - (1, getUInt256Gen(Fixtures.PetersburgBlockNumber + 1, UInt256.MaxValue)) - )).sample.get.withStack(stackIn).withStorage(storage).copy(gas = expectedGas) + val stateIn = getProgramStateGen( + blockNumberGen = Gen.frequency( + (1, getUInt256Gen(0, Fixtures.ConstantinopleBlockNumber - 1)), + (1, getUInt256Gen(Fixtures.PetersburgBlockNumber + 1, Fixtures.PhoenixBlockNumber - 1)) + ), + evmConfig = petersburgConfig, + isTopHeader = true + ).sample.get.withStack(stackIn).withStorage(storage).copy(gas = expectedGas) val stateOut = op.execute(stateIn) verifyGas(expectedGas, stateIn, stateOut, allowOOG = false) } @@ -435,11 +444,13 @@ class OpCodeGasSpec extends FunSuite with OpCodeTesting with Matchers with Prope val stateGen = getProgramStateGen( blockNumberGen = Gen.frequency( (1, getUInt256Gen(0, Fixtures.ConstantinopleBlockNumber - 1)), - (1, getUInt256Gen(Fixtures.PetersburgBlockNumber + 1, UInt256.MaxValue)) + (1, getUInt256Gen(Fixtures.PetersburgBlockNumber + 1, Fixtures.PhoenixBlockNumber - 1)) ), stackGen = getStackGen(elems = 2, maxUInt = Two), gasGen = getBigIntGen(max = maxGasUsage), - storageGen = getStorageGen(3, getUInt256Gen(max = One)) + storageGen = getStorageGen(3, getUInt256Gen(max = One)), + evmConfig = petersburgConfig, + isTopHeader = true ) forAll(stateGen) { stateIn => diff --git a/src/test/scala/io/iohk/ethereum/vm/PrecompiledContractsSpec.scala b/src/test/scala/io/iohk/ethereum/vm/PrecompiledContractsSpec.scala index 8992e557e0..0608822443 100644 --- a/src/test/scala/io/iohk/ethereum/vm/PrecompiledContractsSpec.scala +++ b/src/test/scala/io/iohk/ethereum/vm/PrecompiledContractsSpec.scala @@ -15,11 +15,11 @@ class PrecompiledContractsSpec extends FunSuite with Matchers with PropertyCheck val vm = new TestVM - def buildContext(recipient: Address, inputData: ByteString, gas: UInt256 = 1000000): PC = { + def buildContext(recipient: Address, inputData: ByteString, gas: UInt256 = 1000000, blockNumber: BigInt = 0): PC = { val origin = Address(0xcafebabe) val fakeHeader = BlockHeader(ByteString.empty, ByteString.empty, ByteString.empty, ByteString.empty, - ByteString.empty, ByteString.empty, ByteString.empty, 0, 0, 0, 0, 0, ByteString.empty, ByteString.empty, ByteString.empty) + ByteString.empty, ByteString.empty, ByteString.empty, 0, blockNumber, 0, 0, 0, ByteString.empty, ByteString.empty, ByteString.empty) val world = MockWorldState().saveAccount(origin, Account.empty()) @@ -257,7 +257,7 @@ class PrecompiledContractsSpec extends FunSuite with Matchers with PropertyCheck forAll(testData) { (input, expectedResult) => val inputArray = Hex.decode(input) val expectedNumOfRounds = BigInt(1, inputArray.take(4)) - val context = buildContext(PrecompiledContracts.Blake2bCompressionAddr, ByteString(inputArray)) + val context = buildContext(PrecompiledContracts.Blake2bCompressionAddr, ByteString(inputArray), blockNumber = Fixtures.PhoenixBlockNumber + 1) val result = vm.run(context) val gasUsed = context.startGas - result.gasRemaining gasUsed shouldEqual expectedNumOfRounds diff --git a/src/test/scala/io/iohk/ethereum/vm/SSTOREOpCodeGasPostConstantinopleSpec.scala b/src/test/scala/io/iohk/ethereum/vm/SSTOREOpCodeGasPostConstantinopleSpec.scala index 9cb293181e..33ea3845a7 100644 --- a/src/test/scala/io/iohk/ethereum/vm/SSTOREOpCodeGasPostConstantinopleSpec.scala +++ b/src/test/scala/io/iohk/ethereum/vm/SSTOREOpCodeGasPostConstantinopleSpec.scala @@ -10,36 +10,36 @@ import akka.util.ByteString.{empty => bEmpty} import io.iohk.ethereum.crypto.kec256 import org.bouncycastle.util.encoders.Hex -// EIP-1283 -// Spec https://eips.ethereum.org/EIPS/eip-1283 class StoreOpCodeGasPostConstantinopleSpec extends WordSpec with PropertyChecks with Matchers with TestSetup { - val table = Table[String, BigInt, BigInt, BigInt]( - ("code", "original", "gasUsed", "refund"), - ("60006000556000600055", 0, 412, 0), - ("60006000556001600055", 0, 20212, 0), - ("60016000556000600055", 0, 20212, 19800), - ("60016000556002600055", 0, 20212, 0), - ("60016000556001600055", 0, 20212, 0), - ("60006000556000600055", 1, 5212, 15000), - ("60006000556001600055", 1, 5212, 4800), - ("60006000556002600055", 1, 5212, 0), - ("60026000556000600055", 1, 5212, 15000), - ("60026000556003600055", 1, 5212, 0), - ("60026000556001600055", 1, 5212, 4800), - ("60026000556002600055", 1, 5212, 0), - ("60016000556000600055", 1, 5212, 15000), - ("60016000556002600055", 1, 5212, 0), - ("60016000556001600055", 1, 412, 0), - ("600160005560006000556001600055", 0, 40218, 19800), - ("600060005560016000556000600055", 1, 10218, 19800) - ) - + val defaultGaspool = 1000000 + // Spec https://eips.ethereum.org/EIPS/eip-1283 "Net gas metering for SSTORE after Constantinople hard fork (EIP-1283)" in { - forAll(table) { + val eip1283table = Table[String, BigInt, BigInt, BigInt]( + ("code", "original", "gasUsed", "refund"), + ("60006000556000600055", 0, 412, 0), + ("60006000556001600055", 0, 20212, 0), + ("60016000556000600055", 0, 20212, 19800), + ("60016000556002600055", 0, 20212, 0), + ("60016000556001600055", 0, 20212, 0), + ("60006000556000600055", 1, 5212, 15000), + ("60006000556001600055", 1, 5212, 4800), + ("60006000556002600055", 1, 5212, 0), + ("60026000556000600055", 1, 5212, 15000), + ("60026000556003600055", 1, 5212, 0), + ("60026000556001600055", 1, 5212, 4800), + ("60026000556002600055", 1, 5212, 0), + ("60016000556000600055", 1, 5212, 15000), + ("60016000556002600055", 1, 5212, 0), + ("60016000556001600055", 1, 412, 0), + ("600160005560006000556001600055", 0, 40218, 19800), + ("600060005560016000556000600055", 1, 10218, 19800) + ) + + forAll(eip1283table) { (code, original, gasUsed, refund) => { - val result = vm.exec(prepareProgramState(ByteString(Hex.decode(code)), original)) + val result = vm.exec(prepareProgramState(ByteString(Hex.decode(code)), original, defaultGaspool, EipToCheck.EIP1283)) result.gasUsed shouldEqual gasUsed result.gasRefund shouldEqual refund @@ -47,10 +47,44 @@ class StoreOpCodeGasPostConstantinopleSpec extends WordSpec with PropertyChecks } } + // Spec https://eips.ethereum.org/EIPS/eip-2200 + "Net gas metering for SSTORE after Phoenix hard fork (EIP-2200)" in { + val eip2200table = Table[String, BigInt, BigInt, BigInt, BigInt, Option[ProgramError]]( + ("code", "original", "gasUsed", "refund", "gaspool", "error"), + ("60006000556000600055", 0, 1612, 0, defaultGaspool, None), + ("60006000556001600055", 0, 20812, 0, defaultGaspool, None), + ("60016000556000600055", 0, 20812, 19200, defaultGaspool, None), + ("60016000556002600055", 0, 20812, 0, defaultGaspool, None), + ("60016000556001600055", 0, 20812, 0, defaultGaspool, None), + ("60006000556000600055", 1, 5812, 15000, defaultGaspool, None), + ("60006000556001600055", 1, 5812, 4200, defaultGaspool, None), + ("60006000556002600055", 1, 5812, 0, defaultGaspool, None), + ("60026000556000600055", 1, 5812, 15000, defaultGaspool, None), + ("60026000556003600055", 1, 5812, 0, defaultGaspool, None), + ("60026000556001600055", 1, 5812, 4200, defaultGaspool, None), + ("60026000556002600055", 1, 5812, 0, defaultGaspool, None), + ("60016000556000600055", 1, 5812, 15000, defaultGaspool, None), + ("60016000556002600055", 1, 5812, 0, defaultGaspool, None), + ("60016000556001600055", 1, 1612, 0, defaultGaspool, None), + ("600160005560006000556001600055", 0, 40818, 19200, defaultGaspool, None), + ("600060005560016000556000600055", 1, 10818, 19200, defaultGaspool, None), + ("6001600055", 1, 2306, 0, 2306, Some(OutOfGas)), + ("6001600055", 1, 806, 0, 2307, None) + ) + + forAll(eip2200table) { + (code, original, gasUsed, refund, gaspool, maybeError) => { + val result = vm.exec(prepareProgramState(ByteString(Hex.decode(code)), original, gaspool, EipToCheck.EIP2200)) + + result.gasUsed shouldEqual gasUsed + result.gasRefund shouldEqual refund + result.error shouldEqual maybeError + } + } + } } trait TestSetup { - val config = EvmConfig.ConstantinopleConfigBuilder(blockchainConfig) val vm = new TestVM val senderAddr = Address(0xcafebabeL) @@ -60,7 +94,7 @@ trait TestSetup { def defaultWorld: MockWorldState = MockWorldState().saveAccount(senderAddr, senderAcc) - val blockHeader = BlockHeader( + def prepareBlockHeader(blockNumber: BigInt): BlockHeader = BlockHeader( parentHash = bEmpty, ommersHash = bEmpty, beneficiary = bEmpty, @@ -69,7 +103,7 @@ trait TestSetup { receiptsRoot = bEmpty, logsBloom = bEmpty, difficulty = 1000000, - number = blockchainConfig.constantinopleBlockNumber + 1, + number = blockNumber, gasLimit = 10000000, gasUsed = 0, unixTimestamp = 0, @@ -78,34 +112,49 @@ trait TestSetup { nonce = bEmpty ) - def getContext(world: MockWorldState = defaultWorld, inputData: ByteString = bEmpty): PC = + def getContext(world: MockWorldState = defaultWorld, inputData: ByteString = bEmpty, eipToCheck: EipToCheck, gaspool: BigInt): PC = ProgramContext( callerAddr = senderAddr, originAddr = senderAddr, recipientAddr = None, gasPrice = 1, - startGas = 1000000, + startGas = gaspool, inputData = inputData, value = 100, endowment = 100, doTransfer = true, - blockHeader = blockHeader, + blockHeader = eipToCheck.blockHeader, callDepth = 0, world = world, initialAddressesToDelete = Set(), - evmConfig = config, + evmConfig = eipToCheck.config, originalWorld = world ) - def prepareProgramState(assemblyCode: ByteString, originalValue: BigInt): ProgramState[MockWorldState, MockStorage] = { + def prepareProgramState(assemblyCode: ByteString, originalValue: BigInt, gaspool: BigInt, eipToCheck: EipToCheck): ProgramState[MockWorldState, MockStorage] = { val newWorld = defaultWorld .saveAccount(senderAddr, accountWithCode(assemblyCode)) .saveCode(senderAddr, assemblyCode) .saveStorage(senderAddr, MockStorage(Map(BigInt(0) -> originalValue))) - val context: PC = getContext(newWorld) + val context: PC = getContext(newWorld, eipToCheck = eipToCheck, gaspool = gaspool) val env = ExecEnv(context, assemblyCode, context.originAddr) ProgramState(vm, context, env) } + + sealed trait EipToCheck { + val blockHeader: BlockHeader + val config: EvmConfig + } + object EipToCheck { + case object EIP1283 extends EipToCheck { + override val blockHeader: BlockHeader = prepareBlockHeader(blockchainConfig.constantinopleBlockNumber + 1) + override val config: EvmConfig = EvmConfig.ConstantinopleConfigBuilder(blockchainConfig) + } + case object EIP2200 extends EipToCheck { + override val blockHeader: BlockHeader = prepareBlockHeader(blockchainConfig.phoenixBlockNumber + 1) + override val config: EvmConfig = EvmConfig.PhoenixConfigBuilder(blockchainConfig) + } + } }