Skip to content

Commit 6e3b2d3

Browse files
committed
[ETCM-22] EIP-2200: Structured Definitions for Net Gas Metering
1 parent 9958457 commit 6e3b2d3

File tree

5 files changed

+160
-81
lines changed

5 files changed

+160
-81
lines changed

src/main/scala/io/iohk/ethereum/vm/OpCode.scala

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import akka.util.ByteString
44
import io.iohk.ethereum.crypto.kec256
55
import io.iohk.ethereum.domain.{Account, Address, TxLogEntry, UInt256}
66
import io.iohk.ethereum.domain.UInt256._
7-
import io.iohk.ethereum.vm.BlockchainConfigForEvm.EthForks
7+
import io.iohk.ethereum.vm.BlockchainConfigForEvm.EtcForks.EtcFork
8+
import io.iohk.ethereum.vm.BlockchainConfigForEvm.{EtcForks, EthForks}
9+
import io.iohk.ethereum.vm.BlockchainConfigForEvm.EthForks.EthFork
810

911
// scalastyle:off magic.number
1012
// scalastyle:off number.of.types
@@ -593,52 +595,67 @@ case object MSTORE8 extends OpCode(0x53, 2, 0, _.G_verylow) {
593595

594596
case object SSTORE extends OpCode(0x55, 2, 0, _.G_zero) {
595597
protected def exec[W <: WorldStateProxy[W, S], S <: Storage[S]](state: ProgramState[W, S]): ProgramState[W, S] = {
596-
val (Seq(offset, newValue), stack1) = state.stack.pop(2)
597-
val currentValue = state.storage.load(offset)
598-
val refund: BigInt = if (isEip1283Enabled(state)) {
599-
val originalValue = state.originalWorld.getStorage(state.ownAddress).load(offset)
600-
if (currentValue != newValue.toBigInt) {
601-
if (originalValue == currentValue) { // fresh slot
602-
if (originalValue != 0 && newValue.isZero)
603-
state.config.feeSchedule.R_sclear
604-
else 0
605-
} else { // dirty slot
606-
val clear = if (originalValue != 0) {
607-
if (currentValue == 0)
608-
-state.config.feeSchedule.R_sclear
609-
else if (newValue.isZero)
610-
state.config.feeSchedule.R_sclear
611-
else
612-
BigInt(0)
613-
} else {
614-
BigInt(0)
615-
}
616-
617-
val reset = if (originalValue == newValue.toBigInt) {
618-
if (UInt256(originalValue).isZero)
619-
state.config.feeSchedule.R_sclear + state.config.feeSchedule.G_sreset - state.config.feeSchedule.G_sload
620-
else
621-
state.config.feeSchedule.G_sreset - state.config.feeSchedule.G_sload
598+
val currentBlockNumber = state.env.blockHeader.number
599+
val etcFork = state.config.blockchainConfig.etcForkForBlockNumber(currentBlockNumber)
600+
val ethFork = state.config.blockchainConfig.ethForkForBlockNumber(currentBlockNumber)
601+
602+
val eip2200Enabled = isEip2200Enabled(etcFork, ethFork)
603+
val eip1283Enabled = isEip1283Enabled(ethFork)
604+
605+
if(eip2200Enabled && state.gas <= state.config.feeSchedule.G_callstipend){
606+
state.withError(OutOfGas)
607+
} else {
608+
val (Seq(offset, newValue), stack1) = state.stack.pop(2)
609+
val currentValue = state.storage.load(offset)
610+
611+
val refund: BigInt = if (eip2200Enabled || eip1283Enabled) {
612+
val originalValue = state.originalWorld.getStorage(state.ownAddress).load(offset)
613+
if (currentValue != newValue.toBigInt) {
614+
if (originalValue == currentValue) { // fresh slot
615+
if (originalValue != 0 && newValue.isZero)
616+
state.config.feeSchedule.R_sclear
617+
else 0
618+
} else { // dirty slot
619+
val clear = if (originalValue != 0) {
620+
if (currentValue == 0)
621+
-state.config.feeSchedule.R_sclear
622+
else if (newValue.isZero)
623+
state.config.feeSchedule.R_sclear
624+
else
625+
BigInt(0)
626+
} else {
627+
BigInt(0)
628+
}
629+
630+
val reset = if (originalValue == newValue.toBigInt) {
631+
if (UInt256(originalValue).isZero)
632+
state.config.feeSchedule.R_sclear + state.config.feeSchedule.G_sreset - state.config.feeSchedule.G_sload
633+
else
634+
state.config.feeSchedule.G_sreset - state.config.feeSchedule.G_sload
635+
} else BigInt(0)
636+
clear + reset
637+
}
622638
} else BigInt(0)
623-
clear + reset
639+
} else {
640+
if (newValue.isZero && !UInt256(currentValue).isZero)
641+
state.config.feeSchedule.R_sclear
642+
else
643+
0
624644
}
625-
} else BigInt(0)
626-
} else {
627-
if (newValue.isZero && !UInt256(currentValue).isZero)
628-
state.config.feeSchedule.R_sclear
629-
else
630-
0
645+
val updatedStorage = state.storage.store(offset, newValue)
646+
state.withStack(stack1).withStorage(updatedStorage).refundGas(refund).step()
631647
}
632-
633-
val updatedStorage = state.storage.store(offset, newValue)
634-
state.withStack(stack1).withStorage(updatedStorage).refundGas(refund).step()
635648
}
636649

637650
protected def varGas[W <: WorldStateProxy[W, S], S <: Storage[S]](state: ProgramState[W, S]): BigInt = {
638651
val (Seq(offset, newValue), _) = state.stack.pop(2)
639652
val currentValue = state.storage.load(offset)
640-
if (isEip1283Enabled(state)) {
641-
// https://eips.ethereum.org/EIPS/eip-1283
653+
654+
val currentBlockNumber = state.env.blockHeader.number
655+
val etcFork = state.config.blockchainConfig.etcForkForBlockNumber(currentBlockNumber)
656+
val ethFork = state.config.blockchainConfig.ethForkForBlockNumber(currentBlockNumber)
657+
658+
if (isEip2200Enabled(etcFork, ethFork) || isEip1283Enabled(ethFork)) {
642659
if (currentValue == newValue.toBigInt) { // no-op
643660
state.config.feeSchedule.G_sload
644661
} else {
@@ -663,10 +680,11 @@ case object SSTORE extends OpCode(0x55, 2, 0, _.G_zero) {
663680

664681
override protected def availableInContext[W <: WorldStateProxy[W, S], S <: Storage[S]]: ProgramState[W, S] => Boolean = !_.staticCtx
665682

666-
private def isEip1283Enabled[W <: WorldStateProxy[W, S], S <: Storage[S]](state: ProgramState[W, S]): Boolean = {
667-
val blockNumber = state.env.blockHeader.number
668-
state.config.blockchainConfig.ethForkForBlockNumber(blockNumber) == EthForks.Constantinople
669-
}
683+
// https://eips.ethereum.org/EIPS/eip-1283
684+
private def isEip1283Enabled(ethFork: EthFork): Boolean = ethFork == EthForks.Constantinople
685+
686+
// https://eips.ethereum.org/EIPS/eip-2200
687+
private def isEip2200Enabled(etcFork: EtcFork, ethFork: EthFork): Boolean = (ethFork >= EthForks.Istanbul || etcFork >= EtcForks.Phoenix)
670688
}
671689

672690
case object JUMP extends OpCode(0x56, 1, 0, _.G_mid) with ConstGas {

src/test/scala/io/iohk/ethereum/vm/CallOpFixture.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ class CallOpFixture(val config: EvmConfig, val startState: MockWorldState) {
5454
SELFDESTRUCT
5555
)
5656

57+
val sstoreCode = Assembly(
58+
//Save a value to the storage
59+
PUSH1, 10,
60+
PUSH1, 0,
61+
SSTORE,
62+
)
63+
5764
val sstoreWithClearCode = Assembly(
5865
//Save a value to the storage
5966
PUSH1, 10,
@@ -106,6 +113,7 @@ class CallOpFixture(val config: EvmConfig, val startState: MockWorldState) {
106113
val invalidProgram = Program(extProgram.code.init :+ INVALID.code)
107114
val selfDestructProgram = selfDestructCode.program
108115
val sstoreWithClearProgram = sstoreWithClearCode.program
116+
val sstoreProgram = sstoreCode.program
109117
val accountWithCode: ByteString => Account = code => Account.empty().withCode(kec256(code))
110118

111119
val worldWithoutExtAccount = startState.saveAccount(ownerAddr, initialOwnerAccount)
@@ -130,6 +138,9 @@ class CallOpFixture(val config: EvmConfig, val startState: MockWorldState) {
130138
val worldWithSstoreWithClearProgram = worldWithoutExtAccount.saveAccount(extAddr, accountWithCode(sstoreWithClearProgram.code))
131139
.saveCode(extAddr, sstoreWithClearCode.code)
132140

141+
val worldWithSstoreProgram = worldWithoutExtAccount.saveAccount(extAddr, accountWithCode(sstoreProgram.code))
142+
.saveCode(extAddr, sstoreCode.code)
143+
133144
val worldWithReturnSingleByteCode = worldWithoutExtAccount.saveAccount(extAddr, accountWithCode(returnSingleByteProgram.code))
134145
.saveCode(extAddr, returnSingleByteProgram.code)
135146

src/test/scala/io/iohk/ethereum/vm/Fixtures.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ object Fixtures {
44

55
val ConstantinopleBlockNumber = 200
66
val PetersburgBlockNumber = 400
7+
val PhoenixBlockNumber = 600
78

89
val blockchainConfig = BlockchainConfigForEvm(
910
// block numbers are irrelevant
@@ -19,7 +20,7 @@ object Fixtures {
1920
atlantisBlockNumber = 0,
2021
aghartaBlockNumber = 0,
2122
petersburgBlockNumber = PetersburgBlockNumber,
22-
phoenixBlockNumber = 0,
23+
phoenixBlockNumber = PhoenixBlockNumber,
2324
chainId = 0x3d.toByte
2425
)
2526

src/test/scala/io/iohk/ethereum/vm/OpCodeGasSpec.scala

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,12 @@ class OpCodeGasSpec extends FunSuite with OpCodeTesting with Matchers with Prope
412412
}
413413

414414
test(SSTORE) { op =>
415-
// Before Constantinople
415+
// Before Constantinople + Petersburg
416+
// Constantinople + Phoenix tested in SSTOREOpCodeGasPostConstantinopleSpec
417+
418+
val config = EvmConfig.PetersburgConfigBuilder(blockchainConfig)
419+
import config.feeSchedule._
420+
416421
val storage = MockStorage.Empty.store(Zero, One)
417422
val table = Table[UInt256, UInt256, BigInt, BigInt](("offset", "value", "expectedGas", "expectedRefund"),
418423
(0, 1, G_sreset, 0),
@@ -425,8 +430,8 @@ class OpCodeGasSpec extends FunSuite with OpCodeTesting with Matchers with Prope
425430
val stackIn = Stack.empty().push(value).push(offset)
426431
val stateIn = getProgramStateGen(blockNumberGen = Gen.frequency(
427432
(1, getUInt256Gen(0, Fixtures.ConstantinopleBlockNumber - 1)),
428-
(1, getUInt256Gen(Fixtures.PetersburgBlockNumber + 1, UInt256.MaxValue))
429-
)).sample.get.withStack(stackIn).withStorage(storage).copy(gas = expectedGas)
433+
(1, getUInt256Gen(Fixtures.PetersburgBlockNumber + 1, Fixtures.PhoenixBlockNumber - 1))
434+
), evmConfig = config).sample.get.withStack(stackIn).withStorage(storage).copy(gas = expectedGas)
430435
val stateOut = op.execute(stateIn)
431436
verifyGas(expectedGas, stateIn, stateOut, allowOOG = false)
432437
}
@@ -435,7 +440,7 @@ class OpCodeGasSpec extends FunSuite with OpCodeTesting with Matchers with Prope
435440
val stateGen = getProgramStateGen(
436441
blockNumberGen = Gen.frequency(
437442
(1, getUInt256Gen(0, Fixtures.ConstantinopleBlockNumber - 1)),
438-
(1, getUInt256Gen(Fixtures.PetersburgBlockNumber + 1, UInt256.MaxValue))
443+
(1, getUInt256Gen(Fixtures.PetersburgBlockNumber + 1, Fixtures.PhoenixBlockNumber - 1))
439444
),
440445
stackGen = getStackGen(elems = 2, maxUInt = Two),
441446
gasGen = getBigIntGen(max = maxGasUsage),

src/test/scala/io/iohk/ethereum/vm/SSTOREOpCodeGasPostConstantinopleSpec.scala

Lines changed: 77 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,47 +10,76 @@ import akka.util.ByteString.{empty => bEmpty}
1010
import io.iohk.ethereum.crypto.kec256
1111
import org.bouncycastle.util.encoders.Hex
1212

13-
// EIP-1283
14-
// Spec https://eips.ethereum.org/EIPS/eip-1283
1513
class StoreOpCodeGasPostConstantinopleSpec extends WordSpec with PropertyChecks with Matchers with TestSetup {
1614

17-
val table = Table[String, BigInt, BigInt, BigInt](
18-
("code", "original", "gasUsed", "refund"),
19-
("60006000556000600055", 0, 412, 0),
20-
("60006000556001600055", 0, 20212, 0),
21-
("60016000556000600055", 0, 20212, 19800),
22-
("60016000556002600055", 0, 20212, 0),
23-
("60016000556001600055", 0, 20212, 0),
24-
("60006000556000600055", 1, 5212, 15000),
25-
("60006000556001600055", 1, 5212, 4800),
26-
("60006000556002600055", 1, 5212, 0),
27-
("60026000556000600055", 1, 5212, 15000),
28-
("60026000556003600055", 1, 5212, 0),
29-
("60026000556001600055", 1, 5212, 4800),
30-
("60026000556002600055", 1, 5212, 0),
31-
("60016000556000600055", 1, 5212, 15000),
32-
("60016000556002600055", 1, 5212, 0),
33-
("60016000556001600055", 1, 412, 0),
34-
("600160005560006000556001600055", 0, 40218, 19800),
35-
("600060005560016000556000600055", 1, 10218, 19800)
36-
)
37-
38-
15+
// Spec https://eips.ethereum.org/EIPS/eip-1283
3916
"Net gas metering for SSTORE after Constantinople hard fork (EIP-1283)" in {
40-
forAll(table) {
17+
val eip1283table = Table[String, BigInt, BigInt, BigInt](
18+
("code", "original", "gasUsed", "refund"),
19+
("60006000556000600055", 0, 412, 0),
20+
("60006000556001600055", 0, 20212, 0),
21+
("60016000556000600055", 0, 20212, 19800),
22+
("60016000556002600055", 0, 20212, 0),
23+
("60016000556001600055", 0, 20212, 0),
24+
("60006000556000600055", 1, 5212, 15000),
25+
("60006000556001600055", 1, 5212, 4800),
26+
("60006000556002600055", 1, 5212, 0),
27+
("60026000556000600055", 1, 5212, 15000),
28+
("60026000556003600055", 1, 5212, 0),
29+
("60026000556001600055", 1, 5212, 4800),
30+
("60026000556002600055", 1, 5212, 0),
31+
("60016000556000600055", 1, 5212, 15000),
32+
("60016000556002600055", 1, 5212, 0),
33+
("60016000556001600055", 1, 412, 0),
34+
("600160005560006000556001600055", 0, 40218, 19800),
35+
("600060005560016000556000600055", 1, 10218, 19800)
36+
)
37+
38+
forAll(eip1283table) {
4139
(code, original, gasUsed, refund) => {
42-
val result = vm.exec(prepareProgramState(ByteString(Hex.decode(code)), original))
40+
val result = vm.exec(prepareProgramState(ByteString(Hex.decode(code)), original, EipToCheck.EIP1283))
4341

4442
result.gasUsed shouldEqual gasUsed
4543
result.gasRefund shouldEqual refund
4644
}
4745
}
4846
}
4947

48+
// Spec https://eips.ethereum.org/EIPS/eip-2200
49+
"Net gas metering for SSTORE after Phoenix hard fork (EIP-2200)" in {
50+
val eip2200table = Table[String, BigInt, BigInt, BigInt](
51+
("code", "original", "gasUsed", "refund"),
52+
("60006000556000600055", 0, 1612, 0),
53+
("60006000556001600055", 0, 20812, 0),
54+
("60016000556000600055", 0, 20812, 19200),
55+
("60016000556002600055", 0, 20812, 0),
56+
("60016000556001600055", 0, 20812, 0),
57+
("60006000556000600055", 1, 5812, 15000),
58+
("60006000556001600055", 1, 5812, 4200),
59+
("60006000556002600055", 1, 5812, 0),
60+
("60026000556000600055", 1, 5812, 15000),
61+
("60026000556003600055", 1, 5812, 0),
62+
("60026000556001600055", 1, 5812, 4200),
63+
("60026000556002600055", 1, 5812, 0),
64+
("60016000556000600055", 1, 5812, 15000),
65+
("60016000556002600055", 1, 5812, 0),
66+
("60016000556001600055", 1, 1612, 0),
67+
("600160005560006000556001600055", 0, 40818, 19200),
68+
("600060005560016000556000600055", 1, 10818, 19200)
69+
)
70+
71+
forAll(eip2200table) {
72+
(code, original, gasUsed, refund) => {
73+
val result = vm.exec(prepareProgramState(ByteString(Hex.decode(code)), original, EipToCheck.EIP2200))
74+
75+
result.gasUsed shouldEqual gasUsed
76+
result.gasRefund shouldEqual refund
77+
}
78+
}
79+
}
5080
}
5181

5282
trait TestSetup {
53-
val config = EvmConfig.ConstantinopleConfigBuilder(blockchainConfig)
5483
val vm = new TestVM
5584

5685
val senderAddr = Address(0xcafebabeL)
@@ -60,7 +89,7 @@ trait TestSetup {
6089

6190
def defaultWorld: MockWorldState = MockWorldState().saveAccount(senderAddr, senderAcc)
6291

63-
val blockHeader = BlockHeader(
92+
def prepareBlockHeader(blockNumber: BigInt): BlockHeader = BlockHeader(
6493
parentHash = bEmpty,
6594
ommersHash = bEmpty,
6695
beneficiary = bEmpty,
@@ -78,7 +107,7 @@ trait TestSetup {
78107
nonce = bEmpty
79108
)
80109

81-
def getContext(world: MockWorldState = defaultWorld, inputData: ByteString = bEmpty): PC =
110+
def getContext(world: MockWorldState = defaultWorld, inputData: ByteString = bEmpty, eipToCheck: EipToCheck): PC =
82111
ProgramContext(
83112
callerAddr = senderAddr,
84113
originAddr = senderAddr,
@@ -89,23 +118,38 @@ trait TestSetup {
89118
value = 100,
90119
endowment = 100,
91120
doTransfer = true,
92-
blockHeader = blockHeader,
121+
blockHeader = eipToCheck.blockHeader,
93122
callDepth = 0,
94123
world = world,
95124
initialAddressesToDelete = Set(),
96-
evmConfig = config,
125+
evmConfig = eipToCheck.config,
97126
originalWorld = world
98127
)
99128

100-
def prepareProgramState(assemblyCode: ByteString, originalValue: BigInt): ProgramState[MockWorldState, MockStorage] = {
129+
def prepareProgramState(assemblyCode: ByteString, originalValue: BigInt, eipToCheck: EipToCheck): ProgramState[MockWorldState, MockStorage] = {
101130
val newWorld = defaultWorld
102131
.saveAccount(senderAddr, accountWithCode(assemblyCode))
103132
.saveCode(senderAddr, assemblyCode)
104133
.saveStorage(senderAddr, MockStorage(Map(BigInt(0) -> originalValue)))
105134

106-
val context: PC = getContext(newWorld)
135+
val context: PC = getContext(newWorld, eipToCheck = eipToCheck)
107136
val env = ExecEnv(context, assemblyCode, context.originAddr)
108137

109138
ProgramState(vm, context, env)
110139
}
140+
141+
sealed trait EipToCheck {
142+
val blockHeader: BlockHeader
143+
val config: EvmConfig
144+
}
145+
object EipToCheck {
146+
case object EIP1283 extends EipToCheck {
147+
override val blockHeader: BlockHeader = prepareBlockHeader(blockchainConfig.constantinopleBlockNumber + 1)
148+
override val config: EvmConfig = EvmConfig.ConstantinopleConfigBuilder(blockchainConfig)
149+
}
150+
case object EIP2200 extends EipToCheck {
151+
override val blockHeader: BlockHeader = prepareBlockHeader(blockchainConfig.phoenixBlockNumber + 1)
152+
override val config: EvmConfig = EvmConfig.PhoenixConfigBuilder(blockchainConfig)
153+
}
154+
}
111155
}

0 commit comments

Comments
 (0)