Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.evm.Code;
import org.hyperledger.besu.evm.ModificationNotAllowedException;
import org.hyperledger.besu.evm.frame.ExceptionalHaltReason;
import org.hyperledger.besu.evm.frame.MessageFrame;
import org.hyperledger.besu.evm.operation.AbstractCreateOperation;
Expand All @@ -42,6 +41,9 @@ public class DebugOperationTracer extends AbstractDebugOperationTracer {
private List<TraceFrame> traceFrames = new ArrayList<>();
private TraceFrame lastFrame;

private Optional<Bytes[]> preExecutionStack;
private Optional<UInt256> preExecutionStorageKey = Optional.empty();
private long gasRemaining;
private Bytes inputData;

/**
Expand All @@ -56,7 +58,21 @@ public DebugOperationTracer(final OpCodeTracerConfig options, final boolean reco
}

@Override
protected void capturePreExecutionState(final MessageFrame frame) {
public void tracePreExecution(final MessageFrame frame) {
final Operation currentOperation = frame.getCurrentOperation();
if (!(traceOpcode = traceOpcode(currentOperation))) {
preExecutionStorageKey = Optional.empty();
return;
}
preExecutionStack = captureStack(frame);
if (options.traceStorage()
&& "SLOAD".equals(currentOperation.getName())
&& frame.stackSize() > 0) {
preExecutionStorageKey = Optional.of(UInt256.fromBytes(frame.getStackItem(0)));
} else {
preExecutionStorageKey = Optional.empty();
}
gasRemaining = frame.getRemainingGas();
if (lastFrame != null && frame.getDepth() > lastFrame.getDepth())
inputData = frame.getInputData().copy();
else inputData = frame.getInputData();
Expand Down Expand Up @@ -90,7 +106,8 @@ public void tracePostExecution(final MessageFrame frame, final OperationResult o
traceFrames.add(updatedLast);
}

final Optional<Map<UInt256, UInt256>> storage = captureStorage(frame);
final Optional<Map<UInt256, UInt256>> storage =
captureStorage(frame, currentOperation, operationResult);
final Optional<Map<Address, Wei>> maybeRefunds =
frame.getRefunds().isEmpty() ? Optional.empty() : Optional.of(frame.getRefunds());
final long thisGasCost = computeGasCost(currentOperation, operationResult, frame);
Expand Down Expand Up @@ -226,20 +243,43 @@ private void addNewTraceFrame(
traceFrames.add(traceFrame);
}

private Optional<Map<UInt256, UInt256>> captureStorage(final MessageFrame frame) {
private Optional<Map<UInt256, UInt256>> captureStorage(
final MessageFrame frame,
final Operation currentOperation,
final OperationResult operationResult) {
if (!options.traceStorage()) {
return Optional.empty();
}
try {
Map<UInt256, UInt256> updatedStorage =
frame.getWorldUpdater().getAccount(frame.getRecipientAddress()).getUpdatedStorage();
if (updatedStorage.isEmpty()) return Optional.empty();
final Map<UInt256, UInt256> storageContents = new TreeMap<>(updatedStorage);

return Optional.of(storageContents);
} catch (final ModificationNotAllowedException e) {
return Optional.of(new TreeMap<>());
// Per execution-apis spec, the storage field is only emitted for SLOAD and SSTORE opcodes,
// showing only the single slot touched by that specific operation.
final String opName = currentOperation.getName();
if ("SSTORE".equals(opName)) {
// SStoreOperation calls frame.storageWasUpdated(key, newValue) on success;
// empty if SSTORE halted (e.g. insufficient gas), which is correct.
return frame
.getMaybeUpdatedStorage()
.map(
entry -> {
final Map<UInt256, UInt256> map = new TreeMap<>();
map.put(entry.getOffset(), UInt256.fromBytes(entry.getValue()));
return map;
});
}
if ("SLOAD".equals(opName) && operationResult.getHaltReason() == null) {
// preExecutionStorageKey holds the slot key captured before the opcode ran;
// after SLOAD executes the loaded value sits at the top of the stack.
return preExecutionStorageKey.map(
key -> {
if (frame.stackSize() == 0) {
return null;
}
final UInt256 loadedValue = UInt256.fromBytes(frame.getStackItem(0));
final Map<UInt256, UInt256> map = new TreeMap<>();
map.put(key, loadedValue);
return map;
});
}
return Optional.empty();
}

private Optional<Bytes[]> captureMemory(final MessageFrame frame) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
package org.hyperledger.besu.ethereum.vm;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.BlockHeader;
Expand All @@ -25,7 +23,6 @@
import org.hyperledger.besu.ethereum.core.MessageFrameTestFixture;
import org.hyperledger.besu.ethereum.referencetests.ReferenceTestBlockchain;
import org.hyperledger.besu.evm.EVM;
import org.hyperledger.besu.evm.account.MutableAccount;
import org.hyperledger.besu.evm.frame.ExceptionalHaltReason;
import org.hyperledger.besu.evm.frame.MessageFrame;
import org.hyperledger.besu.evm.gascalculator.CancunGasCalculator;
Expand All @@ -39,9 +36,7 @@
import org.hyperledger.besu.evm.worldstate.WorldUpdater;

import java.util.List;
import java.util.Map;
import java.util.OptionalLong;
import java.util.TreeMap;

import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
Expand Down Expand Up @@ -83,6 +78,30 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) {

private final CallOperation callOperation = new CallOperation(new CancunGasCalculator());

private static final UInt256 SLOAD_RETURNED_VALUE =
UInt256.fromHexString("0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc");

private final Operation SSTORE_OPERATION =
new AbstractOperation(0x55, "SSTORE", 2, 0, null) {
@Override
public OperationResult execute(final MessageFrame frame, final EVM evm) {
final UInt256 key = UInt256.fromBytes(frame.popStackItem());
final UInt256 value = UInt256.fromBytes(frame.popStackItem());
frame.storageWasUpdated(key, value);
return new OperationResult(5000L, null);
}
};

private final Operation SLOAD_OPERATION =
new AbstractOperation(0x54, "SLOAD", 1, 1, null) {
@Override
public OperationResult execute(final MessageFrame frame, final EVM evm) {
frame.popStackItem(); // consume the key
frame.pushStackItem(SLOAD_RETURNED_VALUE); // push the loaded value
return new OperationResult(2100L, null);
}
};

@Test
void shouldRecordProgramCounter() {
final MessageFrame frame = validMessageFrame();
Expand Down Expand Up @@ -192,20 +211,60 @@ void shouldNotRecordMemoryWhenDisabled() {
}

@Test
void shouldRecordStorageWhenEnabled() {
void shouldRecordStorageForSstoreWhenEnabled() {
final MessageFrame frame = validMessageFrame();
final UInt256 storageKey = UInt256.fromHexString("0x01");
final UInt256 storageValue = UInt256.fromHexString("0xdeadbeef");
frame.pushStackItem(storageValue); // value (popped second by SSTORE)
frame.pushStackItem(storageKey); // key (popped first by SSTORE)
final DebugOperationTracer tracer =
new DebugOperationTracer(
OpCodeTracerConfigBuilder.createFrom(OpCodeTracerConfig.DEFAULT)
.traceStorage(true)
.traceMemory(false)
.traceStack(false)
.build(),
false);
traceFrame(frame, tracer, SSTORE_OPERATION);
final TraceFrame traceFrame = getOnlyTraceFrame(tracer);
assertThat(traceFrame.getStorage()).isPresent();
assertThat(traceFrame.getStorage().get()).hasSize(1);
assertThat(traceFrame.getStorage().get()).containsEntry(storageKey, storageValue);
}

@Test
void shouldRecordStorageForSloadWhenEnabled() {
final MessageFrame frame = validMessageFrame();
final Map<UInt256, UInt256> updatedStorage = setupStorageForCapture(frame);
final UInt256 storageKey = UInt256.fromHexString("0x02");
frame.pushStackItem(storageKey); // key (popped by SLOAD)
final DebugOperationTracer tracer =
new DebugOperationTracer(
OpCodeTracerConfigBuilder.createFrom(OpCodeTracerConfig.DEFAULT)
.traceStorage(true)
.traceMemory(false)
.traceStack(false)
.build(),
false);
traceFrame(frame, tracer, SLOAD_OPERATION);
final TraceFrame traceFrame = getOnlyTraceFrame(tracer);
assertThat(traceFrame.getStorage()).isPresent();
assertThat(traceFrame.getStorage().get()).hasSize(1);
assertThat(traceFrame.getStorage().get()).containsEntry(storageKey, SLOAD_RETURNED_VALUE);
}

@Test
void shouldNotRecordStorageForNonStorageOpcodeWhenEnabled() {
// storage is only emitted for SLOAD/SSTORE; non-storage opcodes produce empty storage
final TraceFrame traceFrame =
traceFrame(
frame,
validMessageFrame(),
OpCodeTracerConfigBuilder.createFrom(OpCodeTracerConfig.DEFAULT)
.traceStorage(true)
.traceMemory(false)
.traceStack(false)
.build(),
false);
assertThat(traceFrame.getStorage()).isPresent();
assertThat(traceFrame.getStorage()).contains(updatedStorage);
assertThat(traceFrame.getStorage()).isEmpty();
}

@Test
Expand Down Expand Up @@ -277,7 +336,6 @@ void childGasFlagDoesNotMatterForNonCallOperations() {
@Test
void shouldCaptureFrameWhenExceptionalHaltOccurs() {
final MessageFrame frame = validMessageFrame();
final Map<UInt256, UInt256> updatedStorage = setupStorageForCapture(frame);

final DebugOperationTracer tracer =
new DebugOperationTracer(
Expand All @@ -294,7 +352,7 @@ void shouldCaptureFrameWhenExceptionalHaltOccurs() {
final TraceFrame traceFrame = getOnlyTraceFrame(tracer);
assertThat(traceFrame.getExceptionalHaltReason())
.contains(ExceptionalHaltReason.INSUFFICIENT_GAS);
assertThat(traceFrame.getStorage()).contains(updatedStorage);
assertThat(traceFrame.getStorage()).isEmpty();
}

@Test
Expand Down Expand Up @@ -485,23 +543,6 @@ private MessageFrame validMessageFrameWithInitiatedMemory(final Bytes32 initiate
return frame;
}

private Map<UInt256, UInt256> setupStorageForCapture(final MessageFrame frame) {
final MutableAccount account = mock(MutableAccount.class);
when(worldUpdater.getAccount(frame.getRecipientAddress())).thenReturn(account);

final Map<UInt256, UInt256> updatedStorage = new TreeMap<>();
updatedStorage.put(UInt256.ZERO, UInt256.valueOf(233));
updatedStorage.put(UInt256.ONE, UInt256.valueOf(2424));
when(account.getUpdatedStorage()).thenReturn(updatedStorage);
final Bytes32 word1 = Bytes32.fromHexString("0x01");
final Bytes32 word2 = Bytes32.fromHexString("0x02");
final Bytes32 word3 = Bytes32.fromHexString("0x03");
frame.writeMemory(0, 32, word1);
frame.writeMemory(32, 32, word2);
frame.writeMemory(64, 32, word3);
return updatedStorage;
}

private static DebugOperationTracer createDebugOperationTracerWithMemory() {
final OpCodeTracerConfig config =
OpCodeTracerConfigBuilder.createFrom(OpCodeTracerConfig.DEFAULT)
Expand Down
16 changes: 12 additions & 4 deletions evm/src/main/java/org/hyperledger/besu/evm/tracing/TraceFrame.java
Original file line number Diff line number Diff line change
Expand Up @@ -369,9 +369,13 @@ public Builder setMemory(final Optional<Bytes[]> memory) {
}

/**
* Sets the storage for this operation.
* Sets the storage entry touched by this operation.
*
* @param storage the storage as an optional map of UInt256 keys and values
* <p>Per the execution-apis spec, this field is only populated for SLOAD and SSTORE opcodes and
* contains solely the single slot accessed or written by that operation.
*
* @param storage the storage slot touched, as an optional single-entry map of slot key to
* value; empty for all opcodes other than SLOAD and SSTORE
* @return this builder instance for method chaining
*/
public Builder setStorage(final Optional<Map<UInt256, UInt256>> storage) {
Expand Down Expand Up @@ -712,9 +716,13 @@ public Optional<Bytes[]> getMemory() {
}

/**
* data storage slots and values
* Returns the storage slot touched by this operation.
*
* <p>Per the execution-apis spec, this field is only populated for SLOAD and SSTORE opcodes and
* contains solely the single slot accessed or written by that operation. Empty for all other
* opcodes.
*
* @return data storage slots and values
* @return an optional single-entry map of slot key to value, present only for SLOAD/SSTORE
*/
public Optional<Map<UInt256, UInt256>> getStorage() {
return storage;
Expand Down