From 7e2e80e29867a2250fdab56d68c3da525ef8949c Mon Sep 17 00:00:00 2001 From: Cody Born Date: Fri, 3 Feb 2023 13:36:04 -0800 Subject: [PATCH] EIP-1153: Transient storage opcodes (#4118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement EIP-1153: Transient storage opcodes * Added a new ByteCodeBuilder class to make it easier to construct the E2E test cases. * currently targeting cancun Signed-off-by: Cody Born Signed-off-by: Fabio Di Fabio Signed-off-by: Jiri Peinlich Signed-off-by: Daniel Lehrner Signed-off-by: Karim TAAM Signed-off-by: Diego López León Signed-off-by: Antony Denyer Signed-off-by: Simon Dudley Signed-off-by: Danno Ferrin Co-authored-by: Fabio Di Fabio Co-authored-by: Jiri Peinlich Co-authored-by: Daniel Lehrner Co-authored-by: Justin Florentine Co-authored-by: garyschulte Co-authored-by: Gabriel Trintinalia Co-authored-by: Sally MacFarlane Co-authored-by: matkt Co-authored-by: Diego López León Co-authored-by: Antony Denyer Co-authored-by: Miguel Rojo Co-authored-by: Miguel Angel Rojo Co-authored-by: mark-terry <36909937+mark-terry@users.noreply.github.com> Co-authored-by: Simon Dudley Co-authored-by: Danno Ferrin --- .../TransientStorageOperationBenchmark.java | 91 +++ .../org/hyperledger/besu/evm/MainnetEVMs.java | 24 +- .../besu/evm/frame/MessageFrame.java | 49 ++ .../gascalculator/BerlinGasCalculator.java | 4 +- .../gascalculator/CancunGasCalculator.java | 39 ++ .../besu/evm/gascalculator/GasCalculator.java | 18 + .../besu/evm/operation/TLoadOperation.java | 57 ++ .../besu/evm/operation/TStoreOperation.java | 53 ++ .../processor/AbstractMessageProcessor.java | 1 + .../evm/operations/TStoreOperationTest.java | 593 ++++++++++++++++++ .../besu/evm/testutils/ByteCodeBuilder.java | 296 +++++++++ .../besu/evm/testutils/TestCodeExecutor.java | 18 +- 12 files changed, 1239 insertions(+), 4 deletions(-) create mode 100644 ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/TransientStorageOperationBenchmark.java create mode 100644 evm/src/main/java/org/hyperledger/besu/evm/gascalculator/CancunGasCalculator.java create mode 100644 evm/src/main/java/org/hyperledger/besu/evm/operation/TLoadOperation.java create mode 100644 evm/src/main/java/org/hyperledger/besu/evm/operation/TStoreOperation.java create mode 100644 evm/src/test/java/org/hyperledger/besu/evm/operations/TStoreOperationTest.java create mode 100644 evm/src/test/java/org/hyperledger/besu/evm/testutils/ByteCodeBuilder.java diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/TransientStorageOperationBenchmark.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/TransientStorageOperationBenchmark.java new file mode 100644 index 00000000000..612bedc2c74 --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/TransientStorageOperationBenchmark.java @@ -0,0 +1,91 @@ +/* + * Copyright Hyperledger Besu contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.vm.operations; + +import static org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider.createInMemoryWorldStateArchive; +import static org.mockito.Mockito.mock; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.chain.Blockchain; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; +import org.hyperledger.besu.ethereum.core.MessageFrameTestFixture; +import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.CancunGasCalculator; +import org.hyperledger.besu.evm.operation.TLoadOperation; +import org.hyperledger.besu.evm.operation.TStoreOperation; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt256; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + +@State(Scope.Thread) +public class TransientStorageOperationBenchmark { + + private OperationBenchmarkHelper operationBenchmarkHelper; + private TStoreOperation tstore; + private TLoadOperation tload; + private MessageFrame frame; + + private MessageFrame createMessageFrame(final Address address) { + final Blockchain blockchain = mock(Blockchain.class); + + final WorldStateArchive worldStateArchive = createInMemoryWorldStateArchive(); + final WorldUpdater worldStateUpdater = worldStateArchive.getMutable().updater(); + final BlockHeader blockHeader = new BlockHeaderTestFixture().buildHeader(); + final MessageFrame benchmarkFrame = + new MessageFrameTestFixture() + .address(address) + .worldUpdater(worldStateUpdater) + .blockHeader(blockHeader) + .blockchain(blockchain) + .build(); + worldStateUpdater.getOrCreate(address).getMutable().setBalance(Wei.of(1)); + worldStateUpdater.commit(); + + return benchmarkFrame; + } + + @Setup + public void prepare() throws Exception { + operationBenchmarkHelper = OperationBenchmarkHelper.create(); + CancunGasCalculator gasCalculator = new CancunGasCalculator(); + tstore = new TStoreOperation(gasCalculator); + tload = new TLoadOperation(gasCalculator); + frame = createMessageFrame(Address.fromHexString("0x18675309")); + } + + @TearDown + public void cleanUp() throws Exception { + operationBenchmarkHelper.cleanUp(); + } + + @Benchmark + public Bytes executeOperation() { + frame.pushStackItem(UInt256.ONE); + frame.pushStackItem(UInt256.fromHexString("0x01")); + tstore.execute(frame, null); + frame.pushStackItem(UInt256.fromHexString("0x01")); + tload.execute(frame, null); + return frame.popStackItem(); + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java b/evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java index f382d702d48..b8af21967bf 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java @@ -16,6 +16,7 @@ import org.hyperledger.besu.evm.gascalculator.BerlinGasCalculator; import org.hyperledger.besu.evm.gascalculator.ByzantiumGasCalculator; +import org.hyperledger.besu.evm.gascalculator.CancunGasCalculator; import org.hyperledger.besu.evm.gascalculator.ConstantinopleGasCalculator; import org.hyperledger.besu.evm.gascalculator.FrontierGasCalculator; import org.hyperledger.besu.evm.gascalculator.GasCalculator; @@ -111,6 +112,8 @@ import org.hyperledger.besu.evm.operation.StopOperation; import org.hyperledger.besu.evm.operation.SubOperation; import org.hyperledger.besu.evm.operation.SwapOperation; +import org.hyperledger.besu.evm.operation.TLoadOperation; +import org.hyperledger.besu.evm.operation.TStoreOperation; import org.hyperledger.besu.evm.operation.TimestampOperation; import org.hyperledger.besu.evm.operation.XorOperation; @@ -724,6 +727,15 @@ public static void registerShanghaiOperations( registry.put(new Create2Operation(gasCalculator, SHANGHAI_INIT_CODE_SIZE_LIMIT)); } + /** + * Cancun evm. + * + * @param evmConfiguration the evm configuration + * @return the evm + */ + public static EVM cancun(final EvmConfiguration evmConfiguration) { + return cancun(DEV_NET_CHAIN_ID, evmConfiguration); + } /** * Cancun evm. * @@ -732,7 +744,7 @@ public static void registerShanghaiOperations( * @return the evm */ public static EVM cancun(final BigInteger chainId, final EvmConfiguration evmConfiguration) { - return cancun(new ShanghaiGasCalculator(), chainId, evmConfiguration); + return cancun(new CancunGasCalculator(), chainId, evmConfiguration); } /** @@ -780,12 +792,20 @@ public static void registerCancunOperations( final GasCalculator gasCalculator, final BigInteger chainID) { registerShanghaiOperations(registry, gasCalculator, chainID); + + // EIP-4844 + registry.put(new DataHashOperation(gasCalculator)); + + // TSTORE/TLOAD + registry.put(new TStoreOperation(gasCalculator)); + registry.put(new TLoadOperation(gasCalculator)); + + // EOFV1 registry.put(new RelativeJumpOperation(gasCalculator)); registry.put(new RelativeJumpIfOperation(gasCalculator)); registry.put(new RelativeJumpVectorOperation(gasCalculator)); registry.put(new CallFOperation(gasCalculator)); registry.put(new RetFOperation(gasCalculator)); - registry.put(new DataHashOperation(gasCalculator)); } /** diff --git a/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java b/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java index f45eb568a12..94902bc7ae9 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java @@ -42,8 +42,10 @@ import java.util.function.Consumer; import java.util.function.Function; +import com.google.common.collect.HashBasedTable; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; +import com.google.common.collect.Table; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.bytes.MutableBytes; @@ -240,6 +242,8 @@ public enum Type { private final Map contextVariables; private final Optional> versionedHashes; + private final Table transientStorage = HashBasedTable.create(); + // Miscellaneous fields. private Optional exceptionalHaltReason = Optional.empty(); private Operation currentOperation; @@ -1287,6 +1291,7 @@ public void setCurrentOperation(final Operation currentOperation) { public Multimap getWarmedUpStorage() { return warmedUpStorage; } + /** * Gets maybe updated memory. * @@ -1305,6 +1310,50 @@ public Optional getMaybeUpdatedStorage() { return maybeUpdatedStorage; } + /** + * Gets the transient storage value, including values from parent frames if not set + * + * @param accountAddress The address of the executing context + * @param slot the slot to retrieve + * @return the data value read + */ + public Bytes32 getTransientStorageValue(final Address accountAddress, final Bytes32 slot) { + Bytes32 data = transientStorage.get(accountAddress, slot); + + if (data != null) { + return data; + } + + if (parentMessageFrame != null) { + data = parentMessageFrame.getTransientStorageValue(accountAddress, slot); + } + if (data == null) { + data = Bytes32.ZERO; + } + transientStorage.put(accountAddress, slot, data); + + return data; + } + + /** + * Gets the transient storage value, including values from parent frames if not set + * + * @param accountAddress The address of the executing context + * @param slot the slot to set + * @param value the value to set in the transient store + */ + public void setTransientStorageValue( + final Address accountAddress, final Bytes32 slot, final Bytes32 value) { + transientStorage.put(accountAddress, slot, value); + } + + /** Writes the transient storage to the parent frame, if one exists */ + public void commitTransientStorage() { + if (parentMessageFrame != null) { + parentMessageFrame.transientStorage.putAll(transientStorage); + } + } + /** * Accessor for versionedHashes, if present. * diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/BerlinGasCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/BerlinGasCalculator.java index 9630a8f9b86..c15de3270f8 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/BerlinGasCalculator.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/BerlinGasCalculator.java @@ -38,7 +38,9 @@ public class BerlinGasCalculator extends IstanbulGasCalculator { // new constants for EIP-2929 private static final long COLD_SLOAD_COST = 2100L; private static final long COLD_ACCOUNT_ACCESS_COST = 2600L; - private static final long WARM_STORAGE_READ_COST = 100L; + /** Warm storage read, defined in EIP-2929 */ + protected static final long WARM_STORAGE_READ_COST = 100L; + private static final long ACCESS_LIST_ADDRESS_COST = 2400L; /** The constant ACCESS_LIST_STORAGE_COST. */ protected static final long ACCESS_LIST_STORAGE_COST = 1900L; diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/CancunGasCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/CancunGasCalculator.java new file mode 100644 index 00000000000..c56cd6c60f9 --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/CancunGasCalculator.java @@ -0,0 +1,39 @@ +/* + * Copyright Hyperledger Besu contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.gascalculator; + +/** + * Gas Calculator for Cancun + * + *
    + *
  • Gas costs for TSTORE/TLOAD + *
+ */ +public class CancunGasCalculator extends LondonGasCalculator { + + private static final long TLOAD_GAS = WARM_STORAGE_READ_COST; + private static final long TSTORE_GAS = WARM_STORAGE_READ_COST; + + // EIP-1153 + @Override + public long getTransientLoadOperationGasCost() { + return TLOAD_GAS; + } + + @Override + public long getTransientStoreOperationGasCost() { + return TSTORE_GAS; + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java index ec8e7396237..e2b237abfa3 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java @@ -487,4 +487,22 @@ default long getMaxRefundQuotient() { */ // what would be the gas for a PMT with hash of all non-zeros long getMaximumTransactionCost(int size); + + /** + * Returns the cost of a loading from Transient Storage + * + * @return the cost of a TLOAD from a storage slot + */ + default long getTransientLoadOperationGasCost() { + return 0L; + } + + /** + * Returns the cost of a storing to Transient Storage + * + * @return the cost of a TSTORE to a storage slot + */ + default long getTransientStoreOperationGasCost() { + return 0L; + } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/TLoadOperation.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/TLoadOperation.java new file mode 100644 index 00000000000..3f05931db09 --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/TLoadOperation.java @@ -0,0 +1,57 @@ +/* + * Copyright Hyperledger Besu contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.operation; + +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.internal.FixedStack.OverflowException; +import org.hyperledger.besu.evm.internal.FixedStack.UnderflowException; + +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt256; + +/** Implements the TLOAD operation defined in EIP-1153 */ +public class TLoadOperation extends AbstractOperation { + + /** + * TLoad operation + * + * @param gasCalculator gas calculator for costing + */ + public TLoadOperation(final GasCalculator gasCalculator) { + super(0xb3, "TLOAD", 1, 1, gasCalculator); + } + + @Override + public OperationResult execute(final MessageFrame frame, final EVM evm) { + final long cost = gasCalculator().getTransientLoadOperationGasCost(); + try { + final Bytes32 slot = UInt256.fromBytes(frame.popStackItem()); + if (frame.getRemainingGas() < cost) { + return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); + } else { + frame.pushStackItem(frame.getTransientStorageValue(frame.getRecipientAddress(), slot)); + + return new OperationResult(cost, null); + } + } catch (final UnderflowException ufe) { + return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_STACK_ITEMS); + } catch (final OverflowException ofe) { + return new OperationResult(cost, ExceptionalHaltReason.TOO_MANY_STACK_ITEMS); + } + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/TStoreOperation.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/TStoreOperation.java new file mode 100644 index 00000000000..00d3de4dc12 --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/TStoreOperation.java @@ -0,0 +1,53 @@ +/* + * Copyright Hyperledger Besu contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.operation; + +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; + +import org.apache.tuweni.units.bigints.UInt256; + +/** Implements the TLOAD operation defined in EIP-1153 */ +public class TStoreOperation extends AbstractOperation { + + /** + * TLoad operation + * + * @param gasCalculator gas calculator for costing + */ + public TStoreOperation(final GasCalculator gasCalculator) { + super(0xb4, "TSTORE", 2, 0, gasCalculator); + } + + @Override + public OperationResult execute(final MessageFrame frame, final EVM evm) { + final UInt256 key = UInt256.fromBytes(frame.popStackItem()); + final UInt256 value = UInt256.fromBytes(frame.popStackItem()); + + final long cost = gasCalculator().getTransientStoreOperationGasCost(); + + final long remainingGas = frame.getRemainingGas(); + if (frame.isStatic()) { + return new OperationResult(remainingGas, ExceptionalHaltReason.ILLEGAL_STATE_CHANGE); + } else if (remainingGas < cost) { + return new OperationResult(cost, ExceptionalHaltReason.INSUFFICIENT_GAS); + } else { + frame.setTransientStorageValue(frame.getRecipientAddress(), key, value); + return new OperationResult(cost, null); + } + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/processor/AbstractMessageProcessor.java b/evm/src/main/java/org/hyperledger/besu/evm/processor/AbstractMessageProcessor.java index 9b82ebe913b..d2b1c99688d 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/processor/AbstractMessageProcessor.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/processor/AbstractMessageProcessor.java @@ -148,6 +148,7 @@ protected void revert(final MessageFrame frame) { */ private void completedSuccess(final MessageFrame frame) { frame.getWorldUpdater().commit(); + frame.commitTransientStorage(); frame.getMessageFrameStack().removeFirst(); frame.notifyCompletion(); } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/operations/TStoreOperationTest.java b/evm/src/test/java/org/hyperledger/besu/evm/operations/TStoreOperationTest.java new file mode 100644 index 00000000000..403bd68a7a6 --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/operations/TStoreOperationTest.java @@ -0,0 +1,593 @@ +/* + * Copyright Hyperledger Besu contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.operations; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.evm.frame.ExceptionalHaltReason.INSUFFICIENT_GAS; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.evm.MainnetEVMs; +import org.hyperledger.besu.evm.frame.BlockValues; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.CancunGasCalculator; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.internal.EvmConfiguration; +import org.hyperledger.besu.evm.operation.Operation.OperationResult; +import org.hyperledger.besu.evm.operation.TLoadOperation; +import org.hyperledger.besu.evm.operation.TStoreOperation; +import org.hyperledger.besu.evm.testutils.ByteCodeBuilder; +import org.hyperledger.besu.evm.testutils.FakeBlockValues; +import org.hyperledger.besu.evm.testutils.TestCodeExecutor; +import org.hyperledger.besu.evm.testutils.TestMessageFrameBuilder; +import org.hyperledger.besu.evm.toy.ToyWorld; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; + +import java.util.stream.Stream; + +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt256; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class TStoreOperationTest { + + private static final GasCalculator gasCalculator = new CancunGasCalculator(); + + private MessageFrame createMessageFrame( + final Address address, final long initialGas, final long remainingGas) { + final ToyWorld toyWorld = new ToyWorld(); + final WorldUpdater worldStateUpdater = toyWorld.updater(); + final BlockValues blockHeader = new FakeBlockValues(1337); + final MessageFrame frame = + new TestMessageFrameBuilder() + .address(address) + .worldUpdater(worldStateUpdater) + .blockValues(blockHeader) + .initialGas(initialGas) + .build(); + worldStateUpdater.getOrCreate(address).getMutable().setBalance(Wei.of(1)); + worldStateUpdater.commit(); + frame.setGasRemaining(remainingGas); + + return frame; + } + + @Test + void tstoreInsufficientGas() { + long initialGas = 10_000L; + long remainingGas = 99L; // TSTORE cost should be 100 + final TStoreOperation operation = new TStoreOperation(gasCalculator); + final MessageFrame frame = + createMessageFrame(Address.fromHexString("0x18675309"), initialGas, remainingGas); + frame.pushStackItem(UInt256.ZERO); + frame.pushStackItem(UInt256.fromHexString("0x01")); + + final OperationResult result = operation.execute(frame, null); + assertThat(result.getHaltReason()).isEqualTo(INSUFFICIENT_GAS); + } + + @Test + void tStoreSimpleTest() { + long initialGas = 10_000L; + long remainingGas = 10_000L; + final TStoreOperation operation = new TStoreOperation(gasCalculator); + final MessageFrame frame = + createMessageFrame(Address.fromHexString("0x18675309"), initialGas, remainingGas); + frame.pushStackItem(UInt256.ZERO); + frame.pushStackItem(UInt256.fromHexString("0x01")); + + final OperationResult result = operation.execute(frame, null); + assertThat(result.getHaltReason()).isNull(); + } + + @Test + void tLoadEmpty() { + long initialGas = 10_000L; + long remainingGas = 10_000L; + final MessageFrame frame = + createMessageFrame(Address.fromHexString("0x18675309"), initialGas, remainingGas); + + final TLoadOperation tload = new TLoadOperation(gasCalculator); + frame.pushStackItem(UInt256.fromHexString("0x01")); + final OperationResult tloadResult = tload.execute(frame, null); + assertThat(tloadResult.getHaltReason()).isNull(); + var tloadValue = frame.popStackItem(); + assertThat(tloadValue).isEqualTo(Bytes32.ZERO); + } + + @Test + void tStoreTLoad() { + long initialGas = 10_000L; + long remainingGas = 10_000L; + final TStoreOperation tstore = new TStoreOperation(gasCalculator); + final MessageFrame frame = + createMessageFrame(Address.fromHexString("0x18675309"), initialGas, remainingGas); + frame.pushStackItem(UInt256.ONE); + frame.pushStackItem(UInt256.fromHexString("0x01")); + + final OperationResult result = tstore.execute(frame, null); + assertThat(result.getHaltReason()).isNull(); + + TLoadOperation tload = new TLoadOperation(gasCalculator); + frame.pushStackItem(UInt256.fromHexString("0x01")); + OperationResult tloadResult = tload.execute(frame, null); + assertThat(tloadResult.getHaltReason()).isNull(); + UInt256 tloadValue = UInt256.fromBytes(frame.popStackItem()); + assertThat(tloadValue).isEqualTo(UInt256.ONE); + + // Loading from a different location returns default value + frame.pushStackItem(UInt256.fromHexString("0x02")); + tloadResult = tload.execute(frame, null); + assertThat(tloadResult.getHaltReason()).isNull(); + tloadValue = UInt256.fromBytes(frame.popStackItem()); + assertThat(tloadValue).isEqualTo(UInt256.ZERO); + } + + @Test + void tStoreUpdate() { + long initialGas = 10_000L; + long remainingGas = 10_000L; + final TStoreOperation tstore = new TStoreOperation(gasCalculator); + final MessageFrame frame = + createMessageFrame(Address.fromHexString("0x18675309"), initialGas, remainingGas); + frame.pushStackItem(UInt256.ONE); + frame.pushStackItem(UInt256.fromHexString("0x01")); + + OperationResult result = tstore.execute(frame, null); + assertThat(result.getHaltReason()).isNull(); + + // Store 2 at position 1 + frame.pushStackItem(UInt256.fromHexString("0x02")); + frame.pushStackItem(UInt256.fromHexString("0x01")); + + result = tstore.execute(frame, null); + assertThat(result.getHaltReason()).isNull(); + + final TLoadOperation tload = new TLoadOperation(gasCalculator); + frame.pushStackItem(UInt256.fromHexString("0x01")); + final OperationResult tloadResult = tload.execute(frame, null); + assertThat(tloadResult.getHaltReason()).isNull(); + UInt256 tloadValue = UInt256.fromBytes(frame.popStackItem()); + assertThat(tloadValue).isEqualTo(UInt256.fromHexString("0x02")); + } + + // Zeroing out a transient storage slot does not result in gas refund + @Test + void noGasRefundFromTransientState() { + long initialGas = 10_000L; + long remainingGas = 10_000L; + final TStoreOperation tstore = new TStoreOperation(gasCalculator); + final MessageFrame frame = + createMessageFrame(Address.fromHexString("0x18675309"), initialGas, remainingGas); + frame.pushStackItem(UInt256.ONE); + frame.pushStackItem(UInt256.fromHexString("0x01")); + + OperationResult result = tstore.execute(frame, null); + assertThat(result.getHaltReason()).isNull(); + + // Reset value to 0 + frame.pushStackItem(UInt256.fromHexString("0x00")); + frame.pushStackItem(UInt256.fromHexString("0x01")); + + result = tstore.execute(frame, null); + assertThat(result.getHaltReason()).isNull(); + + assertThat(result.getGasCost()).isEqualTo(100L); + } + + private static final UInt256 gasLimit = UInt256.valueOf(50_000); + private static final Address contractAddress = Address.fromHexString("0x675910144"); + + static Stream testParameters() { + return Stream.of( + // Tests specified in EIP-1153. + Arguments.of( + "Can TSTORE", + null, + new ByteCodeBuilder().tstore(1, 1), + MessageFrame.State.COMPLETED_SUCCESS, + 0, + 1), + Arguments.of( + "Can tload uninitialized", + null, + new ByteCodeBuilder().tload(1).dataOnStackToMemory(0).returnValueAtMemory(32, 0), + MessageFrame.State.COMPLETED_SUCCESS, + 0, + 1), + Arguments.of( + "Can tload after tstore", + null, + new ByteCodeBuilder() + .tstore(1, 2) + .tload(1) + .dataOnStackToMemory(0) + .returnValueAtMemory(32, 0), + MessageFrame.State.COMPLETED_SUCCESS, + 2, + 1), + Arguments.of( + "Can tload after tstore from different location", + null, + new ByteCodeBuilder() + .tstore(1, 2) + .tload(2) + .dataOnStackToMemory(0) + .returnValueAtMemory(32, 0), + MessageFrame.State.COMPLETED_SUCCESS, + 0, + 1), + Arguments.of( + "Contracts have separate transient storage", + new ByteCodeBuilder().tload(1).dataOnStackToMemory(0).returnValueAtMemory(32, 0), + new ByteCodeBuilder() + .tstore(1, 2) + .call(ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit) + .returnInnerCallResults(), + MessageFrame.State.COMPLETED_SUCCESS, + 0, + 1), + Arguments.of( + "Reentrant calls access the same transient storage", + new ByteCodeBuilder() + // check if caller is self + .callerIs(contractAddress) + .conditionalJump(78) + // non-reentrant, call self after tstore + .tstore(1, 8) + .call(ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit) + .returnInnerCallResults() + // reentrant, TLOAD and return value + .op(ByteCodeBuilder.Operation.JUMPDEST) + .tload(1) + .dataOnStackToMemory(0) + .returnValueAtMemory(32, 0), + new ByteCodeBuilder() + .call(ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit) + .returnInnerCallResults(), + MessageFrame.State.COMPLETED_SUCCESS, + 8, + 1), + Arguments.of( + "Successfully returned calls do not revert transient storage writes", + new ByteCodeBuilder() + // check if caller is self + .callerIs(contractAddress) + .conditionalJump(77) + // non-reentrant, call self after tstore + .tstore(1, 8) + .call(ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit) + .tload(1) + .dataOnStackToMemory(0) + .returnValueAtMemory(32, 0) + // reentrant, TLOAD and return value + .op(ByteCodeBuilder.Operation.JUMPDEST) + .tstore(1, 9), + new ByteCodeBuilder() + .call(ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit) + .returnInnerCallResults(), + MessageFrame.State.COMPLETED_SUCCESS, + 9, + 1), + Arguments.of( + "Revert undoes the transient storage write from the failed call", + new ByteCodeBuilder() + // check if caller is self + .callerIs(contractAddress) + .conditionalJump(77) + // non-reentrant, call self after tstore + .tstore(1, 8) + .call(ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit) + .tload(1) + .dataOnStackToMemory(0) + .returnValueAtMemory(32, 0) + // reentrant, TLOAD and return value + .op(ByteCodeBuilder.Operation.JUMPDEST) + .tstore(1, 9) + .op(ByteCodeBuilder.Operation.REVERT), + new ByteCodeBuilder() + .call(ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit) + .returnInnerCallResults(), + MessageFrame.State.COMPLETED_SUCCESS, + 8, + 1), + Arguments.of( + "Revert undoes all the transient storage writes to the same key from the failed call", + new ByteCodeBuilder() + // check if caller is self + .callerIs(contractAddress) + .conditionalJump(77) + // non-reentrant, call self after tstore + .tstore(1, 8) + .call(ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit) + .tload(1) + .dataOnStackToMemory(0) + .returnValueAtMemory(32, 0) + // reentrant, TLOAD and return value + .op(ByteCodeBuilder.Operation.JUMPDEST) + .tstore(1, 9) + .tstore(1, 10) + .op(ByteCodeBuilder.Operation.REVERT), + new ByteCodeBuilder() + .call(ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit) + .returnInnerCallResults(), + MessageFrame.State.COMPLETED_SUCCESS, + 8, + 1), + Arguments.of( + "Revert undoes transient storage writes from inner calls that successfully returned", + new ByteCodeBuilder() + // Check call depth + .push(0) + .op(ByteCodeBuilder.Operation.CALLDATALOAD) + // Store input in mem and reload it to stack + .dataOnStackToMemory(5) + .push(5) + .op(ByteCodeBuilder.Operation.MLOAD) + + // See if we're at call depth 1 + .push(1) + .op(ByteCodeBuilder.Operation.EQ) + .conditionalJump(84) + + // See if we're at call depth 2 + .push(5) + .op(ByteCodeBuilder.Operation.MLOAD) + .push(2) + .op(ByteCodeBuilder.Operation.EQ) + .conditionalJump(135) + + // Call depth = 0, call self after TSTORE 8 + .tstore(1, 8) + + // Recursive call with input + // Depth++ + .push(5) + .op(ByteCodeBuilder.Operation.MLOAD) + .push(1) + .op(ByteCodeBuilder.Operation.ADD) + .callWithInput(ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit) + + // TLOAD and return value + .tload(1) + .dataOnStackToMemory(0) + .returnValueAtMemory(32, 0) + + // Call depth 1, TSTORE 9 but REVERT after recursion + .op(ByteCodeBuilder.Operation.JUMPDEST) + .tstore(1, 9) + + // Recursive call with input + // Depth++ + .push(5) + .op(ByteCodeBuilder.Operation.MLOAD) + .push(1) + .op(ByteCodeBuilder.Operation.ADD) + .callWithInput(ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit) + .op(ByteCodeBuilder.Operation.REVERT) + + // Call depth 2, TSTORE 10 and complete + .op(ByteCodeBuilder.Operation.JUMPDEST) + .tstore(1, 10), + new ByteCodeBuilder() + // Call with input 0 + .callWithInput( + ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit, UInt256.valueOf(0)) + .returnInnerCallResults(), + MessageFrame.State.COMPLETED_SUCCESS, + 8, + 1), + Arguments.of( + "Transient storage cannot be manipulated in a static context", + new ByteCodeBuilder() + .tstore(1, 8) + .tload(1) + .dataOnStackToMemory(0) + .returnValueAtMemory(32, 0), + new ByteCodeBuilder() + .call(ByteCodeBuilder.Operation.STATICCALL, contractAddress, gasLimit) + .returnInnerCallResults(), + MessageFrame.State.COMPLETED_FAILED, + 0, + 1), + Arguments.of( + "Transient storage cannot be manipulated in a static context when calling self", + new ByteCodeBuilder() + // Check if caller is self + .callerIs(contractAddress) + .conditionalJump(82) + + // Non-reentrant, call self after TSTORE 8 + .tstore(1, 8) + .callWithInput( + ByteCodeBuilder.Operation.STATICCALL, + contractAddress, + gasLimit, + UInt256.valueOf(0)) + // Return the TLOAD value + // Should be 8 if call fails, 9 if success + .tload(1) + .dataOnStackToMemory(0) + .returnValueAtMemory(32, 0) + + // Reentrant, TSTORE 9 + .op(ByteCodeBuilder.Operation.JUMPDEST) + .tstore(1, 9), + new ByteCodeBuilder() + .call(ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit) + .returnInnerCallResults(), + MessageFrame.State.COMPLETED_SUCCESS, + 8, + 1), + Arguments.of( + "Transient storage cannot be manipulated in a nested static context", + new ByteCodeBuilder() + // Check call depth + .push(0) + .op(ByteCodeBuilder.Operation.CALLDATALOAD) + // Store input in mem and reload it to stack + .dataOnStackToMemory(5) + .push(5) + .op(ByteCodeBuilder.Operation.MLOAD) + + // See if we're at call depth 1 + .push(1) + .op(ByteCodeBuilder.Operation.EQ) + .conditionalJump(84) + + // See if we're at call depth 2 + .push(5) + .op(ByteCodeBuilder.Operation.MLOAD) + .push(2) + .op(ByteCodeBuilder.Operation.EQ) + .conditionalJump(140) + + // Call depth = 0, call self after TSTORE 8 + .tstore(1, 8) + + // Recursive call with input + // Depth++ + .push(5) + .op(ByteCodeBuilder.Operation.MLOAD) + .push(1) + .op(ByteCodeBuilder.Operation.ADD) + .callWithInput(ByteCodeBuilder.Operation.STATICCALL, contractAddress, gasLimit) + + // TLOAD and return value + .tload(1) + .dataOnStackToMemory(0) + .returnValueAtMemory(32, 0) + + // Call depth 1, TSTORE 9 but REVERT after recursion + .op(ByteCodeBuilder.Operation.JUMPDEST) // 84 + + // Recursive call with input + // Depth++ + .push(5) + .op(ByteCodeBuilder.Operation.MLOAD) + .push(1) + .op(ByteCodeBuilder.Operation.ADD) + .callWithInput(ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit) + + // TLOAD and return value + .tload(1) + .dataOnStackToMemory(0) + .returnValueAtMemory(32, 0) + + // Call depth 2, TSTORE 10 and complete + .op(ByteCodeBuilder.Operation.JUMPDEST) // 140 + .tstore(1, 10) // this call will fail + , + new ByteCodeBuilder() + // Call with input 0 + .callWithInput( + ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit, UInt256.valueOf(0)) + .returnInnerCallResults(), + MessageFrame.State.COMPLETED_SUCCESS, + 8, + 1), + Arguments.of( + "Delegatecall manipulates transient storage in the context of the current address", + new ByteCodeBuilder().tstore(1, 8), + new ByteCodeBuilder() + .tstore(1, 7) + .call(ByteCodeBuilder.Operation.DELEGATECALL, contractAddress, gasLimit) + .tload(1) + .dataOnStackToMemory(0) + .returnValueAtMemory(32, 0), + MessageFrame.State.COMPLETED_SUCCESS, + 8, + 1), + Arguments.of( + "Zeroing out a transient storage slot does not result in gas refund", + null, + new ByteCodeBuilder().tstore(1, 7).tstore(1, 0), + MessageFrame.State.COMPLETED_SUCCESS, + 0, + 1), + Arguments.of( + "Transient storage can be accessed in a static context when calling self", + new ByteCodeBuilder() + // Check if caller is self + .callerIs(contractAddress) + .conditionalJump(78) + + // Non-reentrant, call self after TSTORE 8 + .tstore(1, 8) + .call(ByteCodeBuilder.Operation.STATICCALL, contractAddress, gasLimit) + .returnInnerCallResults() + + // Reentrant, TLOAD and return + .op(ByteCodeBuilder.Operation.JUMPDEST) + .tload(1) + .dataOnStackToMemory(0) + .returnValueAtMemory(32, 0), + new ByteCodeBuilder() + .call(ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit) + .returnInnerCallResults(), + MessageFrame.State.COMPLETED_SUCCESS, + 8, + 1), + Arguments.of( + "Transient storage does not persist beyond a single transaction. The tstore should not have any impact on the second call", + new ByteCodeBuilder() + .tload(1) + .tstore(1, 1) + .dataOnStackToMemory(0) + .returnValueAtMemory(32, 0), + new ByteCodeBuilder() + .call(ByteCodeBuilder.Operation.CALL, contractAddress, gasLimit) + .returnInnerCallResults(), + MessageFrame.State.COMPLETED_SUCCESS, + 0, + 2)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("testParameters") + void transientStorageExecutionTest( + final String ignoredName, + final ByteCodeBuilder contractByteCodeBuilder, + final ByteCodeBuilder byteCodeBuilder, + final MessageFrame.State expectedResultState, + final int expectedReturnValue, + final int numberOfIterations) { + + TestCodeExecutor codeExecutor = + new TestCodeExecutor(MainnetEVMs.cancun(EvmConfiguration.DEFAULT)); + + // Pre-deploy the contract if it's specified + WorldUpdater worldUpdater = + TestCodeExecutor.createInitialWorldState( + account -> account.setStorageValue(UInt256.ZERO, UInt256.valueOf(0))); + if (contractByteCodeBuilder != null) { + WorldUpdater updater = worldUpdater.updater(); + TestCodeExecutor.deployContract(updater, contractAddress, contractByteCodeBuilder.toString()); + updater.commit(); + } + for (int i = 0; i < numberOfIterations; i++) { + final MessageFrame frame = + codeExecutor.executeCode( + byteCodeBuilder.toString(), gasLimit.toLong(), worldUpdater.updater()); + + assertThat(frame.getState()).isEqualTo(expectedResultState); + assertThat(frame.getGasRefund()).isZero(); + assertThat(UInt256.fromBytes(frame.getOutputData()).toInt()).isEqualTo(expectedReturnValue); + } + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/testutils/ByteCodeBuilder.java b/evm/src/test/java/org/hyperledger/besu/evm/testutils/ByteCodeBuilder.java new file mode 100644 index 00000000000..e95ba08dd3c --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/testutils/ByteCodeBuilder.java @@ -0,0 +1,296 @@ +/* + * Copyright Besu Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.testutils; + +import org.hyperledger.besu.datatypes.Address; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt256; + +/** ByteCodeBuilder is a helper class to aid in construction of EVM bytecode for tests */ +public class ByteCodeBuilder { + + public enum Operation { + ADD("01"), + CALL("f1"), + CALLDATALOAD("35"), + CALLER("33"), + DELEGATECALL("f4"), + EQ("14"), + JUMPDEST("5b"), + JUMPI("57"), + MLOAD("51"), + MSTORE("52"), + PUSH("60"), + RETURN("f3"), + RETURNDATACOPY("3e"), + REVERT("fd"), + STATICCALL("fa"), + TLOAD("b3"), + TSTORE("b4"); + + private final String value; + + Operation(final String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + } + + StringBuilder byteCode = new StringBuilder("0x"); + + @Override + public String toString() { + return byteCode.toString(); + } + + /** + * @return Bytecode as Bytes + */ + public Bytes toBytes() { + return Bytes.fromHexString(byteCode.toString()); + } + + /** + * Operation to push the value to the EVM stack + * + * @param value Integer value to push + * @return this + */ + public ByteCodeBuilder push(final int value) { + return this.push(UInt256.valueOf(value)); + } + + /** + * Operation to push the value to the EVM stack + * + * @param value Bytes value to push + * @return this + */ + public ByteCodeBuilder push(final Bytes value) { + UInt256 pushSize = UInt256.valueOf(Bytes.fromHexString("0x60").toInt() + value.size() - 1); + String byteString = standardizeByteString(pushSize); + byteCode.append(byteString); + byteCode.append(value.toUnprefixedHexString()); + return this; + } + + /** + * Operation to push the value to the EVM stack + * + * @param value UInt256 value to push + * @return this + */ + public ByteCodeBuilder push(final UInt256 value) { + String byteString = standardizeByteString(value); + byteCode.append(Operation.PUSH); + byteCode.append(byteString); + return this; + } + + private String standardizeByteString(final UInt256 value) { + String byteString = value.toMinimalBytes().toUnprefixedHexString(); + while (byteString.length() < 2) { + byteString = "0" + byteString; + } + return byteString; + } + + /** + * Operation to store the value in transient storage + * + * @param key location in the contract's storage + * @param value value to store + * @return this + */ + public ByteCodeBuilder tstore(final int key, final int value) { + this.push(value); + this.push(key); + byteCode.append(Operation.TSTORE); + return this; + } + + /** + * Operation to load a value from transient storage + * + * @param key location in the contract's storage + * @return this + */ + public ByteCodeBuilder tload(final int key) { + this.push(key); + byteCode.append(Operation.TLOAD); + return this; + } + + /** + * Operation to store the current stack value to memory + * + * @param key location in the contract's memory + * @return this + */ + public ByteCodeBuilder dataOnStackToMemory(final int key) { + this.push(key); + this.op(Operation.MSTORE); + return this; + } + + /** + * Operations to return the value at a given memory position + * + * @param size size of the returned value + * @param key location in memory of the returned value + * @return this + */ + public ByteCodeBuilder returnValueAtMemory(final int size, final int key) { + this.push(size); + this.push(key); + this.op(Operation.RETURN); + return this; + } + + /** + * Operation to call another smart contract + * + * @param callType CALL, STATICCALL, DELEGATECALL + * @param address contract's address + * @param gasLimit gas limit for the nested call + * @return this + */ + public ByteCodeBuilder call( + final Operation callType, final Address address, final UInt256 gasLimit) { + callTypeCheck(callType); + this.push(0); + this.push(0); + this.push(0); + this.push(0); + this.push(0); + this.push(Bytes.fromHexString(address.toHexString())); + this.push(gasLimit.toMinimalBytes()); + this.op(callType); + return this; + } + + private static void callTypeCheck(final Operation callType) { + + if (callType != Operation.CALL + && callType != Operation.STATICCALL + && callType != Operation.DELEGATECALL) { + throw new UnsupportedOperationException("callType not supported: " + callType); + } + } + + /** + * Operation to call another smart contract with input Assumes input data is 32 bytes, already in + * memory at position 0 + * + * @param callType CALL, STATICCALL, DELEGATECALL + * @param address contract's address + * @param gasLimit gas limit for the nested call + * @return this + */ + public ByteCodeBuilder callWithInput( + final Operation callType, final Address address, final UInt256 gasLimit) { + return callWithInput(callType, address, gasLimit, null); + } + + /** + * Operation to call another smart contract with input Assumes input data is 32 bytes + * + * @param callType CALL, STATICCALL, DELEGATECALL + * @param address contract's address + * @param gasLimit gas limit for the nested call + * @param input input to the nested call, if null pulls from memory at pos 0 + * @return this + */ + public ByteCodeBuilder callWithInput( + final Operation callType, + final Address address, + final UInt256 gasLimit, + final UInt256 input) { + callTypeCheck(callType); + if (input != null) { + this.push(input); + this.push(0); + this.op(Operation.MSTORE); + } else { + // Use top of stack as input + dataOnStackToMemory(0); + } + + this.push(0); + this.push(0); + this.push(32); + this.push(0); + this.push(0); + this.push(Bytes.fromHexString(address.toHexString())); + this.push(gasLimit.toMinimalBytes()); + this.op(callType); + return this; + } + + /** + * Operations to return the results of the nested call + * + * @return this + */ + public ByteCodeBuilder returnInnerCallResults() { + this.push(32); + this.push(0); + this.push(0); + byteCode.append(Operation.RETURNDATACOPY); + this.returnValueAtMemory(32, 0); + return this; + } + + /** + * Operations to check if the current caller is the address provided + * + * @param caller address of the expected caller + * @return this + */ + public ByteCodeBuilder callerIs(final Address caller) { + byteCode.append(Operation.CALLER); + this.push(Bytes.fromHexString(caller.toHexString())); + byteCode.append(Operation.EQ); + return this; + } + + /** + * Operations for a conditional jump + * + * @param dest location to jump to if true + * @return this + */ + public ByteCodeBuilder conditionalJump(final int dest) { + this.push(dest); + byteCode.append(Operation.JUMPI); + return this; + } + + /** + * Adds the specified operation to the set + * + * @param operation operation to add + * @return this + */ + public ByteCodeBuilder op(final Operation operation) { + byteCode.append(operation.toString()); + return this; + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/testutils/TestCodeExecutor.java b/evm/src/test/java/org/hyperledger/besu/evm/testutils/TestCodeExecutor.java index 9400fbc65b5..69fbe4e13bb 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/testutils/TestCodeExecutor.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/testutils/TestCodeExecutor.java @@ -49,6 +49,11 @@ public MessageFrame executeCode( final long gasLimit, final Consumer accountSetup) { final WorldUpdater worldUpdater = createInitialWorldState(accountSetup); + return executeCode(codeHexString, gasLimit, worldUpdater); + } + + public MessageFrame executeCode( + final String codeHexString, final long gasLimit, final WorldUpdater worldUpdater) { final Deque messageFrameStack = new ArrayDeque<>(); final MessageCallProcessor messageCallProcessor = @@ -80,7 +85,18 @@ public MessageFrame executeCode( return initialFrame; } - private WorldUpdater createInitialWorldState(final Consumer accountSetup) { + public static void deployContract( + final WorldUpdater worldUpdater, final Address contractAddress, final String codeHexString) { + var updater = worldUpdater.updater(); + final MutableAccount contract = updater.getOrCreate(contractAddress).getMutable(); + + contract.setNonce(0); + contract.clearStorage(); + contract.setCode(Bytes.fromHexStringLenient(codeHexString)); + updater.commit(); + } + + public static WorldUpdater createInitialWorldState(final Consumer accountSetup) { ToyWorld toyWorld = new ToyWorld(); final WorldUpdater worldState = toyWorld.updater();