mirror of https://github.com/hyperledger/besu
[NC-1579] EIP-1283: Net gas metering for SSTORE (#10)
As per EIP-1283: https://eips.ethereum.org/EIPS/eip-1283
parent
2549f5ee6a
commit
be98c0dcd4
@ -0,0 +1,90 @@ |
||||
package net.consensys.pantheon.ethereum.core; |
||||
|
||||
import net.consensys.pantheon.crypto.SECP256K1.Signature; |
||||
import net.consensys.pantheon.ethereum.db.WorldStateArchive; |
||||
import net.consensys.pantheon.ethereum.mainnet.MainnetMessageCallProcessor; |
||||
import net.consensys.pantheon.ethereum.mainnet.PrecompileContractRegistry; |
||||
import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; |
||||
import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; |
||||
import net.consensys.pantheon.ethereum.vm.Code; |
||||
import net.consensys.pantheon.ethereum.vm.MessageFrame; |
||||
import net.consensys.pantheon.ethereum.vm.MessageFrame.Type; |
||||
import net.consensys.pantheon.ethereum.vm.OperationTracer; |
||||
import net.consensys.pantheon.util.bytes.BytesValue; |
||||
|
||||
import java.math.BigInteger; |
||||
import java.util.ArrayDeque; |
||||
import java.util.Deque; |
||||
import java.util.function.Consumer; |
||||
|
||||
public class TestCodeExecutor { |
||||
|
||||
private final ExecutionContextTestFixture fixture; |
||||
private final BlockHeader blockHeader = new BlockHeaderTestFixture().number(13).buildHeader(); |
||||
private static final Address SENDER_ADDRESS = AddressHelpers.ofValue(244259721); |
||||
|
||||
public TestCodeExecutor(final ProtocolSchedule<Void> protocolSchedule) { |
||||
fixture = new ExecutionContextTestFixture(protocolSchedule); |
||||
} |
||||
|
||||
public MessageFrame executeCode( |
||||
final String code, final long gasLimit, final Consumer<MutableAccount> accountSetup) { |
||||
final ProtocolSpec<Void> protocolSpec = fixture.getProtocolSchedule().getByBlockNumber(0); |
||||
final WorldUpdater worldState = |
||||
createInitialWorldState(accountSetup, fixture.getStateArchive()); |
||||
final Deque<MessageFrame> messageFrameStack = new ArrayDeque<>(); |
||||
|
||||
final MainnetMessageCallProcessor messageCallProcessor = |
||||
new MainnetMessageCallProcessor(protocolSpec.getEvm(), new PrecompileContractRegistry()); |
||||
|
||||
final Transaction transaction = |
||||
Transaction.builder() |
||||
.value(Wei.ZERO) |
||||
.sender(SENDER_ADDRESS) |
||||
.signature(Signature.create(BigInteger.ONE, BigInteger.TEN, (byte) 1)) |
||||
.gasLimit(gasLimit) |
||||
.to(SENDER_ADDRESS) |
||||
.payload(BytesValue.EMPTY) |
||||
.gasPrice(Wei.ZERO) |
||||
.nonce(0) |
||||
.build(); |
||||
final MessageFrame initialFrame = |
||||
MessageFrame.builder() |
||||
.type(Type.MESSAGE_CALL) |
||||
.messageFrameStack(messageFrameStack) |
||||
.blockchain(fixture.getBlockchain()) |
||||
.worldState(worldState) |
||||
.initialGas(Gas.of(gasLimit)) |
||||
.address(SENDER_ADDRESS) |
||||
.originator(SENDER_ADDRESS) |
||||
.contract(SENDER_ADDRESS) |
||||
.gasPrice(transaction.getGasPrice()) |
||||
.inputData(transaction.getPayload()) |
||||
.sender(SENDER_ADDRESS) |
||||
.value(transaction.getValue()) |
||||
.apparentValue(transaction.getValue()) |
||||
.code(new Code(BytesValue.fromHexString(code))) |
||||
.blockHeader(blockHeader) |
||||
.depth(0) |
||||
.completer(c -> {}) |
||||
.build(); |
||||
messageFrameStack.addFirst(initialFrame); |
||||
|
||||
while (!messageFrameStack.isEmpty()) { |
||||
messageCallProcessor.process(messageFrameStack.peekFirst(), OperationTracer.NO_TRACING); |
||||
} |
||||
return initialFrame; |
||||
} |
||||
|
||||
private WorldUpdater createInitialWorldState( |
||||
final Consumer<MutableAccount> accountSetup, final WorldStateArchive stateArchive) { |
||||
final MutableWorldState initialWorldState = stateArchive.getMutable(); |
||||
|
||||
final WorldUpdater worldState = initialWorldState.updater(); |
||||
final MutableAccount senderAccount = worldState.getOrCreate(TestCodeExecutor.SENDER_ADDRESS); |
||||
accountSetup.accept(senderAccount); |
||||
worldState.commit(); |
||||
initialWorldState.persist(); |
||||
return stateArchive.getMutable(initialWorldState.rootHash()).updater(); |
||||
} |
||||
} |
@ -0,0 +1,87 @@ |
||||
package net.consensys.pantheon.ethereum.mainnet; |
||||
|
||||
import static net.consensys.pantheon.util.uint.UInt256.ONE; |
||||
import static net.consensys.pantheon.util.uint.UInt256.ZERO; |
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
import net.consensys.pantheon.ethereum.core.Gas; |
||||
import net.consensys.pantheon.util.uint.UInt256; |
||||
|
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
import org.junit.runners.Parameterized; |
||||
import org.junit.runners.Parameterized.Parameter; |
||||
import org.junit.runners.Parameterized.Parameters; |
||||
|
||||
@RunWith(Parameterized.class) |
||||
public class ConstantinopleSstoreGasTest { |
||||
|
||||
private static final UInt256 TWO = UInt256.of(2); |
||||
|
||||
private final ConstantinopleGasCalculator gasCalculator = new ConstantinopleGasCalculator(); |
||||
|
||||
@Parameters(name = "original: {0}, current: {1}, new: {2}") |
||||
public static Object[][] scenarios() { |
||||
return new Object[][] { |
||||
// Zero no-op
|
||||
{ZERO, ZERO, ZERO, Gas.of(200), Gas.ZERO}, |
||||
|
||||
// Zero fresh change
|
||||
{ZERO, ZERO, ONE, Gas.of(20_000), Gas.ZERO}, |
||||
|
||||
// Dirty, reset to zero
|
||||
{ZERO, ONE, ZERO, Gas.of(200), Gas.of(19800)}, |
||||
|
||||
// Dirty, changed but not reset
|
||||
{ZERO, ONE, TWO, Gas.of(200), Gas.ZERO}, |
||||
|
||||
// Dirty no-op
|
||||
{ZERO, ONE, ONE, Gas.of(200), Gas.ZERO}, |
||||
|
||||
// Dirty, zero no-op
|
||||
{ONE, ZERO, ZERO, Gas.of(200), Gas.ZERO}, |
||||
|
||||
// Dirty, reset to non-zero
|
||||
{ONE, ZERO, ONE, Gas.of(200), Gas.of(-15000).plus(Gas.of(4800))}, |
||||
|
||||
// Fresh change to zero
|
||||
{ONE, ONE, ZERO, Gas.of(5000), Gas.of(15000)}, |
||||
|
||||
// Fresh change with all non-zero
|
||||
{ONE, ONE, TWO, Gas.of(5000), Gas.ZERO}, |
||||
|
||||
// Dirty, clear originally set value
|
||||
{ONE, TWO, ZERO, Gas.of(200), Gas.of(15000)}, |
||||
|
||||
// Non-zero no-op
|
||||
{ONE, ONE, ONE, Gas.of(200), Gas.ZERO}, |
||||
}; |
||||
} |
||||
|
||||
@Parameter public UInt256 originalValue; |
||||
|
||||
@Parameter(value = 1) |
||||
public UInt256 currentValue; |
||||
|
||||
@Parameter(value = 2) |
||||
public UInt256 newValue; |
||||
|
||||
@Parameter(value = 3) |
||||
public Gas expectedGasCost; |
||||
|
||||
@Parameter(value = 4) |
||||
public Gas expectedGasRefund; |
||||
|
||||
@Test |
||||
public void shouldChargeCorrectGas() { |
||||
assertThat(gasCalculator.calculateStorageCost(() -> originalValue, currentValue, newValue)) |
||||
.isEqualTo(expectedGasCost); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldRefundCorrectGas() { |
||||
assertThat( |
||||
gasCalculator.calculateStorageRefundAmount(() -> originalValue, currentValue, newValue)) |
||||
.isEqualTo(expectedGasRefund); |
||||
} |
||||
} |
@ -0,0 +1,79 @@ |
||||
package net.consensys.pantheon.ethereum.vm.operations; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
import net.consensys.pantheon.ethereum.core.Gas; |
||||
import net.consensys.pantheon.ethereum.core.TestCodeExecutor; |
||||
import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; |
||||
import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; |
||||
import net.consensys.pantheon.ethereum.vm.MessageFrame; |
||||
import net.consensys.pantheon.ethereum.vm.MessageFrame.State; |
||||
import net.consensys.pantheon.util.uint.UInt256; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
import org.junit.runners.Parameterized; |
||||
import org.junit.runners.Parameterized.Parameter; |
||||
import org.junit.runners.Parameterized.Parameters; |
||||
|
||||
@RunWith(Parameterized.class) |
||||
public class ConstantinopleSStoreOperationGasCostTest { |
||||
private static final ProtocolSchedule<Void> protocolSchedule = |
||||
MainnetProtocolSchedule.create(0, 0, 0, 0, 0, 0, 1); |
||||
|
||||
@Parameters(name = "Code: {0}, Original: {1}") |
||||
public static Object[][] scenarios() { |
||||
// Tests specified in EIP-1283.
|
||||
return new Object[][] { |
||||
{"0x60006000556000600055", 0, 412, 0}, |
||||
{"0x60006000556001600055", 0, 20212, 0}, |
||||
{"0x60016000556000600055", 0, 20212, 19800}, |
||||
{"0x60016000556002600055", 0, 20212, 0}, |
||||
{"0x60016000556001600055", 0, 20212, 0}, |
||||
{"0x60006000556000600055", 1, 5212, 15000}, |
||||
{"0x60006000556001600055", 1, 5212, 4800}, |
||||
{"0x60006000556002600055", 1, 5212, 0}, |
||||
{"0x60026000556003600055", 1, 5212, 0}, |
||||
{"0x60026000556001600055", 1, 5212, 4800}, |
||||
{"0x60026000556002600055", 1, 5212, 0}, |
||||
{"0x60016000556000600055", 1, 5212, 15000}, |
||||
{"0x60016000556002600055", 1, 5212, 0}, |
||||
{"0x60016000556001600055", 1, 412, 0}, |
||||
{"0x600160005560006000556001600055", 0, 40218, 19800}, |
||||
{"0x600060005560016000556000600055", 1, 10218, 19800}, |
||||
{"0x60026000556000600055", 1, 5212, 15000}, |
||||
}; |
||||
} |
||||
|
||||
private TestCodeExecutor codeExecutor; |
||||
|
||||
@Parameter public String code; |
||||
|
||||
@Parameter(value = 1) |
||||
public int originalValue; |
||||
|
||||
@Parameter(value = 2) |
||||
public int expectedGasUsed; |
||||
|
||||
@Parameter(value = 3) |
||||
public int expectedGasRefund; |
||||
|
||||
@Before |
||||
public void setUp() { |
||||
codeExecutor = new TestCodeExecutor(protocolSchedule); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldCalculateGasAccordingToEip1283() { |
||||
final long gasLimit = 1_000_000; |
||||
final MessageFrame frame = |
||||
codeExecutor.executeCode( |
||||
code, |
||||
gasLimit, |
||||
account -> account.setStorageValue(UInt256.ZERO, UInt256.of(originalValue))); |
||||
assertThat(frame.getState()).isEqualTo(State.COMPLETED_SUCCESS); |
||||
assertThat(frame.getRemainingGas()).isEqualTo(Gas.of(gasLimit - expectedGasUsed)); |
||||
assertThat(frame.getGasRefund()).isEqualTo(Gas.of(expectedGasRefund)); |
||||
} |
||||
} |
Loading…
Reference in new issue