From 8b7ca2017697f08bbf49edef2c80e10513bdd3a7 Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Wed, 20 Nov 2024 08:16:33 +1000 Subject: [PATCH] eth_call overrides (#7801) * add state and account overrides Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane Co-authored-by: Fabio Di Fabio --- CHANGELOG.md | 1 + .../api/jsonrpc/internal/methods/EthCall.java | 20 ++ .../internal/methods/TraceCallMany.java | 7 +- .../jsonrpc/internal/methods/EthCallTest.java | 81 ++++++-- .../jsonrpc/eth/eth_call_overrides_empty.json | 24 +++ .../jsonrpc/eth/eth_call_stateOverride.json | 32 +++ ...all_stateOverride_insufficientBalance.json | 34 ++++ .../transaction/TransactionSimulator.java | 66 +++++- .../besu/ethereum/util/AccountOverride.java | 147 ++++++++++++++ .../ethereum/util/AccountOverrideMap.java | 27 +++ .../transaction/TransactionSimulatorTest.java | 41 ++++ .../util/AccountOverrideParameterTest.java | 188 ++++++++++++++++++ 12 files changed, 647 insertions(+), 21 deletions(-) create mode 100644 ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_call_overrides_empty.json create mode 100644 ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_call_stateOverride.json create mode 100644 ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_call_stateOverride_insufficientBalance.json create mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/util/AccountOverride.java create mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/util/AccountOverrideMap.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/util/AccountOverrideParameterTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index a0a53280feb..2efa312d263 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Update Java dependencies [#7786](https://github.com/hyperledger/besu/pull/7786) - Add a method to get all the transaction in the pool, to the `TransactionPoolService`, to easily access the transaction pool content from plugins [#7813](https://github.com/hyperledger/besu/pull/7813) - Add a method to check if a metric category is enabled to the plugin API [#7832](https://github.com/hyperledger/besu/pull/7832) +- Add account and state overrides to `eth_call` and `eth_estimateGas` [#7801](https://github.com/hyperledger/besu/pull/7801) ### Bug fixes - Fix registering new metric categories from plugins [#7825](https://github.com/hyperledger/besu/pull/7825) diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthCall.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthCall.java index 0e0318f9c2b..5f736e8d5a9 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthCall.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthCall.java @@ -23,6 +23,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcRequestException; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameterOrBlockHash; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonCallParameter; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter.JsonRpcParameterException; @@ -40,8 +41,13 @@ import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; import org.hyperledger.besu.ethereum.transaction.TransactionSimulator; import org.hyperledger.besu.ethereum.transaction.TransactionSimulatorResult; +import org.hyperledger.besu.ethereum.util.AccountOverrideMap; import org.hyperledger.besu.evm.tracing.OperationTracer; +import java.util.Optional; + +import com.google.common.annotations.VisibleForTesting; + public class EthCall extends AbstractBlockParameterOrBlockHashMethod { private final TransactionSimulator transactionSimulator; @@ -81,10 +87,13 @@ protected Object resultByBlockHash(final JsonRpcRequestContext request, final Ha protected Object resultByBlockHeader( final JsonRpcRequestContext request, final BlockHeader header) { JsonCallParameter callParams = JsonCallParameterUtil.validateAndGetCallParams(request); + Optional maybeStateOverrides = getAddressAccountOverrideMap(request); + // TODO implement for block overrides return transactionSimulator .process( callParams, + maybeStateOverrides, buildTransactionValidationParams(header, callParams), OperationTracer.NO_TRACING, (mutableWorldState, transactionSimulatorResult) -> @@ -108,6 +117,17 @@ protected Object resultByBlockHeader( .orElse(errorResponse(request, INTERNAL_ERROR)); } + @VisibleForTesting + protected Optional getAddressAccountOverrideMap( + final JsonRpcRequestContext request) { + try { + return request.getOptionalParameter(2, AccountOverrideMap.class); + } catch (JsonRpcParameterException e) { + throw new InvalidJsonRpcRequestException( + "Invalid account overrides parameter (index 2)", RpcErrorType.INVALID_CALL_PARAMS, e); + } + } + @Override public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { return (JsonRpcResponse) handleParamTypes(requestContext); diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TraceCallMany.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TraceCallMany.java index 10d4018bce2..1601241db62 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TraceCallMany.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TraceCallMany.java @@ -160,7 +160,12 @@ private JsonNode getSingleCallResult( new DebugOperationTracer(buildTraceOptions(traceTypes), false); final Optional maybeSimulatorResult = transactionSimulator.processWithWorldUpdater( - callParameter, buildTransactionValidationParams(), tracer, header, worldUpdater); + callParameter, + Optional.empty(), + buildTransactionValidationParams(), + tracer, + header, + worldUpdater); LOG.trace("Executing {} call for transaction {}", traceTypeParameter, callParameter); if (maybeSimulatorResult.isEmpty()) { diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthCallTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthCallTest.java index 8b8ff67cd81..1ba9839b66d 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthCallTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthCallTest.java @@ -51,6 +51,8 @@ import org.hyperledger.besu.ethereum.transaction.PreCloseStateHandler; import org.hyperledger.besu.ethereum.transaction.TransactionSimulator; import org.hyperledger.besu.ethereum.transaction.TransactionSimulatorResult; +import org.hyperledger.besu.ethereum.util.AccountOverride; +import org.hyperledger.besu.ethereum.util.AccountOverrideMap; import java.util.Optional; @@ -92,6 +94,33 @@ public void shouldReturnCorrectMethodName() { assertThat(method.getName()).isEqualTo("eth_call"); } + @Test + public void noAccountOverrides() { + final JsonRpcRequestContext request = ethCallRequest(callParameter(), "latest"); + Optional overrideMap = method.getAddressAccountOverrideMap(request); + assertThat(overrideMap.isPresent()).isFalse(); + } + + @Test + public void someAccountOverrides() { + AccountOverrideMap expectedOverrides = new AccountOverrideMap(); + AccountOverride override = new AccountOverride.Builder().withNonce(88L).build(); + final Address address = Address.fromHexString("0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3"); + expectedOverrides.put(address, override); + + final JsonRpcRequestContext request = + ethCallRequestWithStateOverrides(callParameter(), "latest", expectedOverrides); + + Optional maybeOverrideMap = method.getAddressAccountOverrideMap(request); + assertThat(maybeOverrideMap.isPresent()).isTrue(); + AccountOverrideMap overrideMap = maybeOverrideMap.get(); + assertThat(overrideMap.keySet()).hasSize(1); + assertThat(overrideMap.values()).hasSize(1); + + assertThat(overrideMap).containsKey(address); + assertThat(overrideMap).containsValue(override); + } + @Test public void shouldReturnInternalErrorWhenProcessorReturnsEmpty() { final JsonRpcRequestContext request = ethCallRequest(callParameter(), "latest"); @@ -99,7 +128,7 @@ public void shouldReturnInternalErrorWhenProcessorReturnsEmpty() { when(blockchainQueries.getBlockchain()).thenReturn(blockchain); when(blockchain.getChainHead()).thenReturn(chainHead); - when(transactionSimulator.process(any(), any(), any(), any(), any())) + when(transactionSimulator.process(any(), any(), any(), any(), any(), any())) .thenReturn(Optional.empty()); final BlockHeader blockHeader = mock(BlockHeader.class); @@ -109,7 +138,7 @@ public void shouldReturnInternalErrorWhenProcessorReturnsEmpty() { final JsonRpcResponse response = method.response(request); assertThat(response).usingRecursiveComparison().isEqualTo(expectedResponse); - verify(transactionSimulator).process(any(), any(), any(), any(), any()); + verify(transactionSimulator).process(any(), any(), any(), any(), any(), any()); } @Test @@ -130,12 +159,13 @@ public void shouldAcceptRequestWhenMissingOptionalFields() { when(result.isSuccessful()).thenReturn(true); when(result.getValidationResult()).thenReturn(ValidationResult.valid()); when(result.getOutput()).thenReturn(Bytes.of()); - verify(transactionSimulator).process(any(), any(), any(), mapperCaptor.capture(), any()); + verify(transactionSimulator) + .process( + eq(callParameter), eq(Optional.empty()), any(), any(), mapperCaptor.capture(), any()); assertThat(mapperCaptor.getValue().apply(mock(MutableWorldState.class), Optional.of(result))) .isEqualTo(Optional.of(expectedResponse)); assertThat(response).usingRecursiveComparison().isEqualTo(expectedResponse); - verify(transactionSimulator).process(eq(callParameter), any(), any(), any(), any()); } @Test @@ -158,7 +188,8 @@ public void shouldReturnExecutionResultWhenExecutionIsSuccessful() { when(result.getValidationResult()).thenReturn(ValidationResult.valid()); when(result.getOutput()).thenReturn(Bytes.of(1)); verify(transactionSimulator) - .process(eq(callParameter()), any(), any(), mapperCaptor.capture(), any()); + .process( + eq(callParameter()), eq(Optional.empty()), any(), any(), mapperCaptor.capture(), any()); assertThat(mapperCaptor.getValue().apply(mock(MutableWorldState.class), Optional.of(result))) .isEqualTo(Optional.of(expectedResponse)); @@ -196,7 +227,8 @@ public void shouldReturnBasicExecutionRevertErrorWithoutReason() { when(result.isSuccessful()).thenReturn(false); when(result.getValidationResult()).thenReturn(ValidationResult.valid()); when(result.result()).thenReturn(processingResult); - verify(transactionSimulator).process(any(), any(), any(), mapperCaptor.capture(), any()); + verify(transactionSimulator) + .process(any(), eq(Optional.empty()), any(), any(), mapperCaptor.capture(), any()); assertThat(mapperCaptor.getValue().apply(mock(MutableWorldState.class), Optional.of(result))) .isEqualTo(Optional.of(expectedResponse)); @@ -235,7 +267,8 @@ public void shouldReturnExecutionRevertErrorWithABIParseError() { when(result.isSuccessful()).thenReturn(false); when(result.getValidationResult()).thenReturn(ValidationResult.valid()); when(result.result()).thenReturn(processingResult); - verify(transactionSimulator).process(any(), any(), any(), mapperCaptor.capture(), any()); + verify(transactionSimulator) + .process(any(), eq(Optional.empty()), any(), any(), mapperCaptor.capture(), any()); assertThat(mapperCaptor.getValue().apply(mock(MutableWorldState.class), Optional.of(result))) .isEqualTo(Optional.of(expectedResponse)); @@ -277,7 +310,8 @@ public void shouldReturnExecutionRevertErrorWithParsedABI() { when(result.getValidationResult()).thenReturn(ValidationResult.valid()); when(result.result()).thenReturn(processingResult); - verify(transactionSimulator).process(any(), any(), any(), mapperCaptor.capture(), any()); + verify(transactionSimulator) + .process(any(), eq(Optional.empty()), any(), any(), mapperCaptor.capture(), any()); assertThat(mapperCaptor.getValue().apply(mock(MutableWorldState.class), Optional.of(result))) .isEqualTo(Optional.of(expectedResponse)); @@ -291,7 +325,7 @@ public void shouldUseCorrectBlockNumberWhenLatest() { final JsonRpcRequestContext request = ethCallRequest(callParameter(), "latest"); when(blockchainQueries.getBlockchain()).thenReturn(blockchain); when(blockchain.getChainHead()).thenReturn(chainHead); - when(transactionSimulator.process(any(), any(), any(), any(), any())) + when(transactionSimulator.process(any(), eq(Optional.empty()), any(), any(), any(), any())) .thenReturn(Optional.empty()); final BlockHeader blockHeader = mock(BlockHeader.class); @@ -301,7 +335,7 @@ public void shouldUseCorrectBlockNumberWhenLatest() { method.response(request); verify(blockchainQueries, atLeastOnce()).getBlockchain(); - verify(transactionSimulator).process(any(), any(), any(), any(), any()); + verify(transactionSimulator).process(any(), eq(Optional.empty()), any(), any(), any(), any()); } @Test @@ -315,7 +349,7 @@ public void shouldUseCorrectBlockNumberWhenEarliest() { method.response(request); verify(blockchainQueries).getBlockHeaderByHash(eq(Hash.ZERO)); - verify(transactionSimulator).process(any(), any(), any(), any(), any()); + verify(transactionSimulator).process(any(), eq(Optional.empty()), any(), any(), any(), any()); } @Test @@ -323,13 +357,13 @@ public void shouldUseCorrectBlockNumberWhenSafe() { final JsonRpcRequestContext request = ethCallRequest(callParameter(), "safe"); when(blockchainQueries.getBlockHeaderByHash(Hash.ZERO)).thenReturn(Optional.of(blockHeader)); when(blockchainQueries.safeBlockHeader()).thenReturn(Optional.of(blockHeader)); - when(transactionSimulator.process(any(), any(), any(), any(), any())) + when(transactionSimulator.process(any(), eq(Optional.empty()), any(), any(), any(), any())) .thenReturn(Optional.empty()); method.response(request); verify(blockchainQueries).getBlockHeaderByHash(Hash.ZERO); verify(blockchainQueries).safeBlockHeader(); - verify(transactionSimulator).process(any(), any(), any(), any(), any()); + verify(transactionSimulator).process(any(), eq(Optional.empty()), any(), any(), any(), any()); } @Test @@ -337,13 +371,13 @@ public void shouldUseCorrectBlockNumberWhenFinalized() { final JsonRpcRequestContext request = ethCallRequest(callParameter(), "finalized"); when(blockchainQueries.getBlockHeaderByHash(Hash.ZERO)).thenReturn(Optional.of(blockHeader)); when(blockchainQueries.finalizedBlockHeader()).thenReturn(Optional.of(blockHeader)); - when(transactionSimulator.process(any(), any(), any(), any(), any())) + when(transactionSimulator.process(any(), eq(Optional.empty()), any(), any(), any(), any())) .thenReturn(Optional.empty()); method.response(request); verify(blockchainQueries).getBlockHeaderByHash(Hash.ZERO); verify(blockchainQueries).finalizedBlockHeader(); - verify(transactionSimulator).process(any(), any(), any(), any(), any()); + verify(transactionSimulator).process(any(), eq(Optional.empty()), any(), any(), any(), any()); } @Test @@ -353,13 +387,13 @@ public void shouldUseCorrectBlockNumberWhenSpecified() { when(blockchainQueries.getBlockHashByNumber(anyLong())).thenReturn(Optional.of(Hash.ZERO)); when(blockchainQueries.getBlockHeaderByHash(Hash.ZERO)) .thenReturn(Optional.of(mock(BlockHeader.class))); - when(transactionSimulator.process(any(), any(), any(), any(), any())) + when(transactionSimulator.process(any(), eq(Optional.empty()), any(), any(), any(), any())) .thenReturn(Optional.empty()); method.response(request); verify(blockchainQueries).getBlockHeaderByHash(eq(Hash.ZERO)); - verify(transactionSimulator).process(any(), any(), any(), any(), any()); + verify(transactionSimulator).process(any(), eq(Optional.empty()), any(), any(), any(), any()); } @Test @@ -431,7 +465,7 @@ private void internalAutoSelectIsAllowedExceedingBalance( .build(); verify(transactionSimulator) - .process(any(), eq(transactionValidationParams), any(), any(), any()); + .process(any(), eq(Optional.empty()), eq(transactionValidationParams), any(), any(), any()); } private JsonCallParameter callParameter() { @@ -458,8 +492,17 @@ private JsonRpcRequestContext ethCallRequest( new JsonRpcRequest("2.0", "eth_call", new Object[] {callParameter, blockNumberInHex})); } + private JsonRpcRequestContext ethCallRequestWithStateOverrides( + final CallParameter callParameter, + final String blockNumberInHex, + final AccountOverrideMap overrides) { + return new JsonRpcRequestContext( + new JsonRpcRequest( + "2.0", "eth_call", new Object[] {callParameter, blockNumberInHex, overrides})); + } + private void mockTransactionProcessorSuccessResult(final JsonRpcResponse jsonRpcResponse) { - when(transactionSimulator.process(any(), any(), any(), any(), any())) + when(transactionSimulator.process(any(), eq(Optional.empty()), any(), any(), any(), any())) .thenReturn(Optional.of(jsonRpcResponse)); } } diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_call_overrides_empty.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_call_overrides_empty.json new file mode 100644 index 00000000000..6280773f3d0 --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_call_overrides_empty.json @@ -0,0 +1,24 @@ +{ + "request": { + "id": 3, + "jsonrpc": "2.0", + "method": "eth_call", + "params": [ + { + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "from": "a94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "data": "0x12a7b914" + }, + "latest", + { + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b": {} + } + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 3, + "result": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_call_stateOverride.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_call_stateOverride.json new file mode 100644 index 00000000000..762202f96b2 --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_call_stateOverride.json @@ -0,0 +1,32 @@ +{ + "request": { + "id": 3, + "jsonrpc": "2.0", + "method": "eth_call", + "params": [ + { + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "from": "a94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "data": "0x12a7b914" + }, + "latest", + { + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xde0b6b3a7640000", + "nonce": 88 + }, + "0xb9741079a300Cb3B8f324CdDB847c0d1d273a05E": { + "stateDiff": { + "0x1cf7945003fc5b59d2f6736f0704557aa805c4f2844084ccd1173b8d56946962": "0x000000000000000000000000000000000000000000000000000000110ed03bf7" + } + } + } + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 3, + "result": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_call_stateOverride_insufficientBalance.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_call_stateOverride_insufficientBalance.json new file mode 100644 index 00000000000..c3832c5689a --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_call_stateOverride_insufficientBalance.json @@ -0,0 +1,34 @@ +{ + "request": { + "id": 3, + "jsonrpc": "2.0", + "method": "eth_call", + "params": [ + { + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "from": "a94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "value": "0x000002" + }, + "latest", + { + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0x000001" + }, + "0xb9741079a300Cb3B8f324CdDB847c0d1d273a05E": { + "stateDiff": { + "0x1cf7945003fc5b59d2f6736f0704557aa805c4f2844084ccd1173b8d56946962": "0x000000000000000000000000000000000000000000000000000000110ed03bf7" + } + } + } + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 3, + "error" : { + "code" : -32004, + "message" : "Upfront cost exceeds account balance" + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java index 321dc965ecd..b0a7fa43257 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java @@ -34,10 +34,13 @@ import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; +import org.hyperledger.besu.ethereum.util.AccountOverride; +import org.hyperledger.besu.ethereum.util.AccountOverrideMap; import org.hyperledger.besu.ethereum.vm.CachingBlockHashLookup; import org.hyperledger.besu.ethereum.vm.DebugOperationTracer; import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; import org.hyperledger.besu.evm.account.Account; +import org.hyperledger.besu.evm.account.MutableAccount; import org.hyperledger.besu.evm.tracing.OperationTracer; import org.hyperledger.besu.evm.worldstate.WorldUpdater; @@ -46,8 +49,10 @@ import java.util.function.Supplier; import javax.annotation.Nonnull; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt256; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -152,6 +157,35 @@ public Optional process( final OperationTracer operationTracer, final PreCloseStateHandler preWorldStateCloseGuard, final BlockHeader header) { + return process( + callParams, + Optional.empty(), + transactionValidationParams, + operationTracer, + preWorldStateCloseGuard, + header); + } + + /** + * Processes a transaction simulation with the provided parameters and executes pre-worldstate + * close actions. + * + * @param callParams The call parameters for the transaction. + * @param maybeStateOverrides The map of state overrides to apply to the state for this + * transaction. + * @param transactionValidationParams The validation parameters for the transaction. + * @param operationTracer The tracer for capturing operations during processing. + * @param preWorldStateCloseGuard The pre-worldstate close guard for executing pre-close actions. + * @param header The block header. + * @return An Optional containing the result of the processing. + */ + public Optional process( + final CallParameter callParams, + final Optional maybeStateOverrides, + final TransactionValidationParams transactionValidationParams, + final OperationTracer operationTracer, + final PreCloseStateHandler preWorldStateCloseGuard, + final BlockHeader header) { if (header == null) { return Optional.empty(); } @@ -169,7 +203,12 @@ public Optional process( return preWorldStateCloseGuard.apply( ws, processWithWorldUpdater( - callParams, transactionValidationParams, operationTracer, header, updater)); + callParams, + maybeStateOverrides, + transactionValidationParams, + operationTracer, + header, + updater)); } catch (final Exception e) { return Optional.empty(); @@ -208,6 +247,7 @@ private MutableWorldState getWorldState(final BlockHeader header) { @Nonnull public Optional processWithWorldUpdater( final CallParameter callParams, + final Optional maybeStateOverrides, final TransactionValidationParams transactionValidationParams, final OperationTracer operationTracer, final BlockHeader header, @@ -226,6 +266,12 @@ public Optional processWithWorldUpdater( .blockHeaderFunctions(protocolSpec.getBlockHeaderFunctions()) .buildBlockHeader(); } + if (maybeStateOverrides.isPresent()) { + for (Address accountToOverride : maybeStateOverrides.get().keySet()) { + final AccountOverride overrides = maybeStateOverrides.get().get(accountToOverride); + applyOverrides(updater.getOrCreate(accountToOverride), overrides); + } + } final Account sender = updater.get(senderAddress); final long nonce = sender != null ? sender.getNonce() : 0L; @@ -284,6 +330,24 @@ public Optional processWithWorldUpdater( return Optional.of(new TransactionSimulatorResult(transaction, result)); } + @VisibleForTesting + protected void applyOverrides(final MutableAccount account, final AccountOverride override) { + LOG.debug("applying overrides to state for account {}", account.getAddress()); + override.getNonce().ifPresent(account::setNonce); + if (override.getBalance().isPresent()) { + account.setBalance(override.getBalance().get()); + } + override.getCode().ifPresent(n -> account.setCode(Bytes.fromHexString(n))); + override + .getStateDiff() + .ifPresent( + d -> + d.forEach( + (key, value) -> + account.setStorageValue( + UInt256.fromHexString(key), UInt256.fromHexString(value)))); + } + private long calculateSimulationGasCap( final long userProvidedGasLimit, final long blockGasLimit) { final long simulationGasCap; diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/util/AccountOverride.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/util/AccountOverride.java new file mode 100644 index 00000000000..3bae4af1d84 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/util/AccountOverride.java @@ -0,0 +1,147 @@ +/* + * Copyright contributors to Besu. + * + * 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.util; + +import org.hyperledger.besu.datatypes.Wei; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// similar to AccountDiff +// BUT +// there are more fields that need to be added +// stateDiff +// movePrecompileToAddress +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonDeserialize(builder = AccountOverride.Builder.class) +public class AccountOverride { + private static final Logger LOG = LoggerFactory.getLogger(AccountOverride.class); + + private final Optional balance; + private final Optional nonce; + private final Optional code; + private final Optional> stateDiff; + + private AccountOverride( + final Optional balance, + final Optional nonce, + final Optional code, + final Optional> stateDiff) { + this.balance = balance; + this.nonce = nonce; + this.code = code; + this.stateDiff = stateDiff; + } + + public Optional getBalance() { + return balance; + } + + public Optional getNonce() { + return nonce; + } + + public Optional getCode() { + return code; + } + + public Optional> getStateDiff() { + return stateDiff; + } + + public static class Builder { + private Optional balance = Optional.empty(); + private Optional nonce = Optional.empty(); + private Optional code = Optional.empty(); + private Optional> stateDiff = Optional.empty(); + + /** Default constructor. */ + public Builder() {} + + public Builder withBalance(final Wei balance) { + this.balance = Optional.ofNullable(balance); + return this; + } + + public Builder withNonce(final Long nonce) { + this.nonce = Optional.ofNullable(nonce); + return this; + } + + public Builder withCode(final String code) { + this.code = Optional.ofNullable(code); + return this; + } + + public Builder withStateDiff(final Map stateDiff) { + this.stateDiff = Optional.ofNullable(stateDiff); + return this; + } + + public AccountOverride build() { + return new AccountOverride(balance, nonce, code, stateDiff); + } + } + + @JsonAnySetter + public void withUnknownProperties(final String key, final Object value) { + LOG.debug( + "unknown property - {} with value - {} and type - {} caught during serialization", + key, + value, + value != null ? value.getClass() : "NULL"); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final AccountOverride accountOverride = (AccountOverride) o; + return balance.equals(accountOverride.balance) + && nonce.equals(accountOverride.nonce) + && code.equals(accountOverride.code) + && stateDiff.equals(accountOverride.stateDiff); + } + + @Override + public int hashCode() { + return Objects.hash(balance, nonce, code, stateDiff); + } + + @Override + public String toString() { + return "AccountOverride{" + + "balance=" + + balance + + ", nonce=" + + nonce + + ", code=" + + code + + ", stateDiff=" + + stateDiff + + '}'; + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/util/AccountOverrideMap.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/util/AccountOverrideMap.java new file mode 100644 index 00000000000..30fc808c9bb --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/util/AccountOverrideMap.java @@ -0,0 +1,27 @@ +/* + * Copyright contributors to Besu. + * + * 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.util; + +import org.hyperledger.besu.datatypes.Address; + +import java.util.HashMap; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class AccountOverrideMap extends HashMap { + + public AccountOverrideMap() {} +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulatorTest.java index c6c676c4151..0dfa7e924fa 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulatorTest.java @@ -20,6 +20,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import org.hyperledger.besu.crypto.SECPSignature; @@ -47,17 +48,21 @@ import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult.Status; +import org.hyperledger.besu.ethereum.util.AccountOverride; import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; import org.hyperledger.besu.evm.account.Account; +import org.hyperledger.besu.evm.account.MutableAccount; import org.hyperledger.besu.evm.tracing.OperationTracer; import org.hyperledger.besu.evm.worldstate.WorldUpdater; import java.math.BigInteger; +import java.util.Map; import java.util.Optional; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt256; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -100,6 +105,42 @@ public void setUp() { new TransactionSimulator(blockchain, worldStateArchive, protocolSchedule, GAS_CAP); } + @Test + public void testOverrides_whenNoOverrides_noUpdates() { + MutableAccount mutableAccount = mock(MutableAccount.class); + when(mutableAccount.getAddress()).thenReturn(DEFAULT_FROM); // called from logging + AccountOverride.Builder builder = new AccountOverride.Builder(); + AccountOverride override = builder.build(); + transactionSimulator.applyOverrides(mutableAccount, override); + verify(mutableAccount).getAddress(); + verifyNoMoreInteractions(mutableAccount); + } + + @Test + public void testOverrides_whenBalanceOverrides_balanceIsUpdated() { + MutableAccount mutableAccount = mock(MutableAccount.class); + when(mutableAccount.getAddress()).thenReturn(DEFAULT_FROM); + AccountOverride.Builder builder = new AccountOverride.Builder().withBalance(Wei.of(99)); + AccountOverride override = builder.build(); + transactionSimulator.applyOverrides(mutableAccount, override); + verify(mutableAccount).setBalance(eq(Wei.of(99))); + } + + @Test + public void testOverrides_whenStateDiffOverrides_stateIsUpdated() { + MutableAccount mutableAccount = mock(MutableAccount.class); + when(mutableAccount.getAddress()).thenReturn(DEFAULT_FROM); + final String storageKey = "0x01a2"; + final String storageValue = "0x00ff"; + AccountOverride.Builder builder = + new AccountOverride.Builder().withStateDiff(Map.of(storageKey, storageValue)); + AccountOverride override = builder.build(); + transactionSimulator.applyOverrides(mutableAccount, override); + verify(mutableAccount) + .setStorageValue( + eq(UInt256.fromHexString(storageKey)), eq(UInt256.fromHexString(storageValue))); + } + @Test public void shouldReturnEmptyWhenBlockDoesNotExist() { when(blockchain.getBlockHeader(eq(1L))).thenReturn(Optional.empty()); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/util/AccountOverrideParameterTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/util/AccountOverrideParameterTest.java new file mode 100644 index 00000000000..8e7b5c3f0eb --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/util/AccountOverrideParameterTest.java @@ -0,0 +1,188 @@ +/* + * Copyright contributors to Besu. + * + * 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.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; + +import java.util.Optional; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +public class AccountOverrideParameterTest { + + private static final String ADDRESS_HEX1 = "0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3"; + private static final String ADDRESS_HEX2 = "0xd5E23607D5d73ff2293152f464C3caB005f87696"; + private static final String STORAGE_KEY = + "0x1cf7945003fc5b59d2f6736f0704557aa805c4f2844084ccd1173b8d56946962"; + private static final String STORAGE_VALUE = + "0x000000000000000000000000000000000000000000000000000000110ed03bf7"; + private static final String CODE_STRING = + "0xdbf4257000000000000000000000000000000000000000000000000000000000"; + + @Test + public void jsonDeserializesCorrectly() throws Exception { + final String json = + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[{" + + "\"from\":\"0x0\", \"to\": \"0x0\"}, " + + "\"latest\"," + + "{\"" + + ADDRESS_HEX1 + + "\":" + + "{" + + "\"balance\": \"0x01\"," + + "\"nonce\": 88" + + "}}],\"id\":1}"; + + final JsonRpcRequestContext request = new JsonRpcRequestContext(readJsonAsJsonRpcRequest(json)); + final AccountOverrideMap accountOverrideParam = + request.getRequiredParameter(2, AccountOverrideMap.class); + + final AccountOverride accountOverride = + accountOverrideParam.get(Address.fromHexString(ADDRESS_HEX1)); + + assertThat(accountOverride.getNonce()).isEqualTo(Optional.of(88L)); + assertThat(accountOverride.getBalance()).isEqualTo(Optional.of(Wei.of(1))); + assertFalse(accountOverride.getStateDiff().isPresent()); + } + + @Test + public void jsonWithCodeDeserializesCorrectly() throws Exception { + final String json = + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[{" + + "\"from\":\"0x0\", \"to\": \"0x0\"}, " + + "\"latest\"," + + "{\"" + + ADDRESS_HEX1 + + "\":" + + "{" + + "\"balance\": \"0x01\"," + + "\"code\": \"" + + CODE_STRING + + "\"" + + "}}],\"id\":1}"; + + final JsonRpcRequestContext request = new JsonRpcRequestContext(readJsonAsJsonRpcRequest(json)); + final AccountOverrideMap accountOverrideParam = + request.getRequiredParameter(2, AccountOverrideMap.class); + + final AccountOverride accountOverride = + accountOverrideParam.get(Address.fromHexString(ADDRESS_HEX1)); + + assertFalse(accountOverride.getNonce().isPresent()); + assertThat(accountOverride.getBalance()).isEqualTo(Optional.of(Wei.of(1))); + assertThat(accountOverride.getCode()).isEqualTo(Optional.of(CODE_STRING)); + assertFalse(accountOverride.getStateDiff().isPresent()); + } + + @Test + public void jsonWithStorageOverridesDeserializesCorrectly() throws Exception { + final String json = + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[{" + + "\"from\":\"0x0\", \"to\": \"0x0\"}, " + + "\"latest\"," + + "{\"" + + ADDRESS_HEX1 + + "\":" + + "{" + + "\"balance\": \"0x01\"," + + "\"nonce\": 88," + + "\"stateDiff\": {" + + "\"" + + STORAGE_KEY + + "\": \"" + + STORAGE_VALUE + + "\"" + + "}}}],\"id\":1}"; + + final JsonRpcRequestContext request = new JsonRpcRequestContext(readJsonAsJsonRpcRequest(json)); + + final AccountOverrideMap accountOverrideParam = + request.getRequiredParameter(2, AccountOverrideMap.class); + assertThat(accountOverrideParam.size()).isEqualTo(1); + + final AccountOverride accountOverride = + accountOverrideParam.get(Address.fromHexString(ADDRESS_HEX1)); + assertThat(accountOverride.getNonce()).isEqualTo(Optional.of(88L)); + + assertTrue(accountOverride.getStateDiff().isPresent()); + assertThat(accountOverride.getStateDiff().get().get(STORAGE_KEY)).isEqualTo(STORAGE_VALUE); + } + + @Test + public void jsonWithMultipleAccountOverridesDeserializesCorrectly() throws Exception { + final String json = + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[{" + + "\"from\":\"0x0\", \"to\": \"0x0\"}, " + + "\"latest\"," + + "{\"" + + ADDRESS_HEX1 + + "\":" + + "{" + + "\"balance\": \"0x01\"," + + "\"nonce\": 88," + + "\"stateDiff\": {" + + "\"" + + STORAGE_KEY + + "\": \"" + + STORAGE_VALUE + + "\"" + + "}}," + + "\"" + + ADDRESS_HEX2 + + "\":" + + "{" + + "\"balance\": \"0xFF\"," + + "\"nonce\": 99," + + "\"stateDiff\": {" + + "\"" + + STORAGE_KEY + + "\": \"" + + STORAGE_VALUE + + "\"" + + "}}}],\"id\":1}"; + + final JsonRpcRequestContext request = new JsonRpcRequestContext(readJsonAsJsonRpcRequest(json)); + + final AccountOverrideMap accountOverrideParam = + request.getRequiredParameter(2, AccountOverrideMap.class); + assertThat(accountOverrideParam.size()).isEqualTo(2); + + final AccountOverride accountOverride1 = + accountOverrideParam.get(Address.fromHexString(ADDRESS_HEX1)); + assertThat(accountOverride1.getNonce()).isEqualTo(Optional.of(88L)); + assertThat(accountOverride1.getBalance()).isEqualTo(Optional.of(Wei.fromHexString("0x01"))); + assertTrue(accountOverride1.getStateDiff().isPresent()); + assertThat(accountOverride1.getStateDiff().get().get(STORAGE_KEY)).isEqualTo(STORAGE_VALUE); + + final AccountOverride accountOverride2 = + accountOverrideParam.get(Address.fromHexString(ADDRESS_HEX2)); + assertThat(accountOverride2.getNonce()).isEqualTo(Optional.of(99L)); + assertThat(accountOverride2.getBalance()).isEqualTo(Optional.of(Wei.fromHexString("0xFF"))); + assertTrue(accountOverride2.getStateDiff().isPresent()); + assertThat(accountOverride2.getStateDiff().get().get(STORAGE_KEY)).isEqualTo(STORAGE_VALUE); + } + + private JsonRpcRequest readJsonAsJsonRpcRequest(final String json) throws java.io.IOException { + return new ObjectMapper().readValue(json, JsonRpcRequest.class); + } +}