From b4e14bc356728a9027d38b1c0e577ed276b86420 Mon Sep 17 00:00:00 2001 From: David Mechler Date: Wed, 5 May 2021 23:06:39 -0400 Subject: [PATCH] Add debug_accountAt RPC end point (#1981) * Add debug_accountAt RPC end point Signed-off-by: David Mechler * PR comment Signed-off-by: David Mechler Co-authored-by: Antoine Toulme Co-authored-by: Sally MacFarlane --- .../besu/ethereum/api/jsonrpc/RpcMethod.java | 1 + .../internal/methods/DebugAccountAt.java | 123 +++++++++++++ .../internal/response/JsonRpcError.java | 1 + .../results/DebugAccountAtResult.java | 28 +++ .../jsonrpc/methods/DebugJsonRpcMethods.java | 7 +- .../internal/methods/DebugAccountAtTest.java | 168 ++++++++++++++++++ .../api/jsonrpc/debug/debug_accountAt.json | 23 +++ 7 files changed, 347 insertions(+), 4 deletions(-) create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugAccountAt.java create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/DebugAccountAtResult.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugAccountAtTest.java create mode 100644 ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/debug_accountAt.json diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java index d762659694..752aa676d5 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java @@ -32,6 +32,7 @@ public enum RpcMethod { CLIQUE_GET_PROPOSALS("clique_proposals"), CLIQUE_PROPOSE("clique_propose"), CLIQUE_GET_SIGNER_METRICS("clique_getSignerMetrics"), + DEBUG_ACCOUNT_AT("debug_accountAt"), DEBUG_METRICS("debug_metrics"), DEBUG_STORAGE_RANGE_AT("debug_storageRangeAt"), DEBUG_TRACE_BLOCK("debug_traceBlock"), diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugAccountAt.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugAccountAt.java new file mode 100644 index 0000000000..4821fcde1d --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugAccountAt.java @@ -0,0 +1,123 @@ +/* + * Copyright ConsenSys AG. + * + * 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.api.jsonrpc.internal.methods; + +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.parameters.BlockParameterOrBlockHash; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.BlockTrace; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.BlockTracer; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.TransactionTrace; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.ImmutableDebugAccountAtResult; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.Quantity; +import org.hyperledger.besu.ethereum.api.query.BlockWithMetadata; +import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; +import org.hyperledger.besu.ethereum.api.query.TransactionWithMetadata; +import org.hyperledger.besu.ethereum.core.Account; +import org.hyperledger.besu.ethereum.core.Address; +import org.hyperledger.besu.ethereum.core.Hash; +import org.hyperledger.besu.ethereum.debug.TraceOptions; +import org.hyperledger.besu.ethereum.vm.DebugOperationTracer; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +public class DebugAccountAt extends AbstractBlockParameterOrBlockHashMethod { + private final Supplier blockTracerSupplier; + + public DebugAccountAt( + final BlockchainQueries blockchainQueries, final Supplier blockTracerSupplier) { + super(blockchainQueries); + this.blockTracerSupplier = blockTracerSupplier; + } + + public DebugAccountAt( + final Supplier blockchainQueries, + final Supplier blockTracerSupplier) { + super(blockchainQueries); + this.blockTracerSupplier = blockTracerSupplier; + } + + @Override + public String getName() { + return RpcMethod.DEBUG_ACCOUNT_AT.getMethodName(); + } + + @Override + protected BlockParameterOrBlockHash blockParameterOrBlockHash( + final JsonRpcRequestContext requestContext) { + return requestContext.getRequiredParameter(0, BlockParameterOrBlockHash.class); + } + + @Override + protected Object resultByBlockHash( + final JsonRpcRequestContext requestContext, final Hash blockHash) { + final Integer txIndex = requestContext.getRequiredParameter(1, Integer.class); + final Address address = requestContext.getRequiredParameter(2, Address.class); + + Optional> block = + blockchainQueries.get().blockByHash(blockHash); + if (block.isEmpty()) { + return new JsonRpcErrorResponse( + requestContext.getRequest().getId(), JsonRpcError.BLOCK_NOT_FOUND); + } + + List transactions = block.get().getTransactions(); + if (transactions.isEmpty() || txIndex < 0 || txIndex > block.get().getTransactions().size()) { + return new JsonRpcErrorResponse( + requestContext.getRequest().getId(), JsonRpcError.INVALID_PARAMS); + } + + final Optional transactionTrace = + blockTracerSupplier.get() + .trace(blockHash, new DebugOperationTracer(new TraceOptions(false, true, true))) + .map(BlockTrace::getTransactionTraces).orElse(Collections.emptyList()).stream() + .filter( + trxTrace -> + trxTrace + .getTransaction() + .getHash() + .equals(transactions.get(txIndex).getTransaction().getHash())) + .findFirst(); + + if (transactionTrace.isEmpty()) { + return new JsonRpcErrorResponse( + requestContext.getRequest().getId(), JsonRpcError.TRANSACTION_NOT_FOUND); + } + + Optional account = + transactionTrace.get().getTraceFrames().stream() + .map(traceFrame -> traceFrame.getWorldUpdater().get(address)) + .filter(Objects::nonNull) + .filter(a -> a.getAddress().equals(address)) + .findFirst(); + if (account.isEmpty()) { + return new JsonRpcErrorResponse( + requestContext.getRequest().getId(), JsonRpcError.NO_ACCOUNT_FOUND); + } + + return ImmutableDebugAccountAtResult.builder() + .code(Quantity.create(account.get().getCode())) + .nonce(Quantity.create(account.get().getNonce())) + .balance(Quantity.create(account.get().getBalance())) + .codehash(Quantity.create(account.get().getCodeHash())) + .build(); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/JsonRpcError.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/JsonRpcError.java index 7dbf9a941d..8ec11b0381 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/JsonRpcError.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/JsonRpcError.java @@ -64,6 +64,7 @@ public enum JsonRpcError { TX_FEECAP_EXCEEDED(-32000, "Transaction fee cap exceeded"), REVERT_ERROR(-32000, "Execution reverted"), GAS_PRICE_MUST_BE_ZERO(-3200, "gasPrice must be set to zero on a GoQuorum compatible network"), + TRANSACTION_NOT_FOUND(-32000, "Transaction not found"), // Miner failures COINBASE_NOT_SET(-32010, "Coinbase not set. Unable to start mining without a coinbase"), diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/DebugAccountAtResult.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/DebugAccountAtResult.java new file mode 100644 index 0000000000..637b1fc7d3 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/DebugAccountAtResult.java @@ -0,0 +1,28 @@ +/* + * Copyright ConsenSys AG. + * + * 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.api.jsonrpc.internal.results; + +import org.immutables.value.Value; + +@Value.Immutable +public abstract class DebugAccountAtResult implements JsonRpcResult { + public abstract String getCode(); + + public abstract String getNonce(); + + public abstract String getBalance(); + + public abstract String getCodehash(); +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/DebugJsonRpcMethods.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/DebugJsonRpcMethods.java index 13c5fd6c99..7bac0f8763 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/DebugJsonRpcMethods.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/DebugJsonRpcMethods.java @@ -16,6 +16,7 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.methods; import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApi; import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.DebugAccountAt; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.DebugAccountRange; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.DebugBatchSendRawTransaction; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.DebugGetBadBlocks; @@ -93,9 +94,7 @@ public class DebugJsonRpcMethods extends ApiGroupJsonRpcMethods { new DebugStandardTraceBlockToFile( () -> new TransactionTracer(blockReplay), blockchainQueries, dataDir), new DebugStandardTraceBadBlockToFile( - () -> new TransactionTracer(blockReplay), - blockchainQueries, - protocolSchedule, - dataDir)); + () -> new TransactionTracer(blockReplay), blockchainQueries, protocolSchedule, dataDir), + new DebugAccountAt(blockchainQueries, () -> new BlockTracer(blockReplay))); } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugAccountAtTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugAccountAtTest.java new file mode 100644 index 0000000000..ee0ce541f9 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugAccountAtTest.java @@ -0,0 +1,168 @@ +/* + * Copyright ConsenSys AG. + * + * 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.api.jsonrpc.internal.methods; + +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.BlockTrace; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.BlockTracer; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.TransactionTrace; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; +import org.hyperledger.besu.ethereum.api.query.BlockWithMetadata; +import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; +import org.hyperledger.besu.ethereum.api.query.TransactionWithMetadata; +import org.hyperledger.besu.ethereum.core.Address; +import org.hyperledger.besu.ethereum.core.Hash; +import org.hyperledger.besu.ethereum.core.Transaction; + +import java.util.Collections; +import java.util.Optional; + +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class DebugAccountAtTest { + private final BlockTracer blockTracer = Mockito.mock(BlockTracer.class); + private final BlockchainQueries blockchainQueries = Mockito.mock(BlockchainQueries.class); + + @SuppressWarnings("unchecked") + private final BlockWithMetadata blockWithMetadata = + Mockito.mock(BlockWithMetadata.class); + + private final TransactionWithMetadata transactionWithMetadata = + Mockito.mock(TransactionWithMetadata.class); + private final BlockTrace blockTrace = Mockito.mock(BlockTrace.class); + private final TransactionTrace transactionTrace = Mockito.mock(TransactionTrace.class); + private final DebugAccountAt debugAccountAt = + new DebugAccountAt(blockchainQueries, () -> blockTracer); + private final Transaction transaction = Mockito.mock(Transaction.class); + + @Test + public void nameShouldBeDebugAccountAt() { + Assertions.assertThat(debugAccountAt.getName()).isEqualTo("debug_accountAt"); + } + + @Test + public void testBlockNotFoundResponse() { + Mockito.when(blockchainQueries.blockByHash(Mockito.any())).thenReturn(Optional.empty()); + + final Object[] params = new Object[] {Hash.ZERO.toHexString(), 0, Address.ZERO.toHexString()}; + final JsonRpcRequestContext request = + new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_accountAt", params)); + final JsonRpcResponse response = debugAccountAt.response(request); + + Assertions.assertThat(response instanceof JsonRpcErrorResponse).isTrue(); + Assertions.assertThat(((JsonRpcErrorResponse) response).getError()) + .isEqualByComparingTo(JsonRpcError.BLOCK_NOT_FOUND); + } + + @Test + public void testInvalidParamsResponseEmptyList() { + Mockito.when(blockchainQueries.blockByHash(Mockito.any())) + .thenReturn(Optional.of(blockWithMetadata)); + Mockito.when(blockWithMetadata.getTransactions()).thenReturn(Collections.emptyList()); + + final Object[] params = new Object[] {Hash.ZERO.toHexString(), 0, Address.ZERO.toHexString()}; + final JsonRpcRequestContext request = + new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_accountAt", params)); + final JsonRpcResponse response = debugAccountAt.response(request); + + Assertions.assertThat(response instanceof JsonRpcErrorResponse).isTrue(); + Assertions.assertThat(((JsonRpcErrorResponse) response).getError()) + .isEqualByComparingTo(JsonRpcError.INVALID_PARAMS); + } + + @Test + public void testInvalidParamsResponseNegative() { + Mockito.when(blockchainQueries.blockByHash(Mockito.any())) + .thenReturn(Optional.of(blockWithMetadata)); + Mockito.when(blockWithMetadata.getTransactions()) + .thenReturn(Collections.singletonList(transactionWithMetadata)); + + final Object[] params = new Object[] {Hash.ZERO.toHexString(), -1, Address.ZERO.toHexString()}; + final JsonRpcRequestContext request = + new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_accountAt", params)); + final JsonRpcResponse response = debugAccountAt.response(request); + + Assertions.assertThat(response instanceof JsonRpcErrorResponse).isTrue(); + Assertions.assertThat(((JsonRpcErrorResponse) response).getError()) + .isEqualByComparingTo(JsonRpcError.INVALID_PARAMS); + } + + @Test + public void testInvalidParamsResponseTooHigh() { + Mockito.when(blockchainQueries.blockByHash(Mockito.any())) + .thenReturn(Optional.of(blockWithMetadata)); + Mockito.when(blockWithMetadata.getTransactions()) + .thenReturn(Collections.singletonList(transactionWithMetadata)); + + final Object[] params = new Object[] {Hash.ZERO.toHexString(), 2, Address.ZERO.toHexString()}; + final JsonRpcRequestContext request = + new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_accountAt", params)); + final JsonRpcResponse response = debugAccountAt.response(request); + + Assertions.assertThat(response instanceof JsonRpcErrorResponse).isTrue(); + Assertions.assertThat(((JsonRpcErrorResponse) response).getError()) + .isEqualByComparingTo(JsonRpcError.INVALID_PARAMS); + } + + @Test + public void testTransactionNotFoundResponse() { + Mockito.when(blockchainQueries.blockByHash(Mockito.any())) + .thenReturn(Optional.of(blockWithMetadata)); + Mockito.when(blockWithMetadata.getTransactions()) + .thenReturn(Collections.singletonList(transactionWithMetadata)); + + final Object[] params = new Object[] {Hash.ZERO.toHexString(), 0, Address.ZERO.toHexString()}; + final JsonRpcRequestContext request = + new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_accountAt", params)); + final JsonRpcResponse response = debugAccountAt.response(request); + + Assertions.assertThat(response instanceof JsonRpcErrorResponse).isTrue(); + Assertions.assertThat(((JsonRpcErrorResponse) response).getError()) + .isEqualByComparingTo(JsonRpcError.TRANSACTION_NOT_FOUND); + } + + @Test + public void testNoAccountFoundResponse() { + Mockito.when(blockchainQueries.blockByHash(Mockito.any())) + .thenReturn(Optional.of(blockWithMetadata)); + Mockito.when(blockWithMetadata.getTransactions()) + .thenReturn(Collections.singletonList(transactionWithMetadata)); + Mockito.when(blockTracer.trace(Mockito.any(Hash.class), Mockito.any())) + .thenReturn(Optional.of(blockTrace)); + Mockito.when(blockTrace.getTransactionTraces()) + .thenReturn(Collections.singletonList(transactionTrace)); + Mockito.when(transactionTrace.getTransaction()).thenReturn(transaction); + Mockito.when(transactionWithMetadata.getTransaction()).thenReturn(transaction); + Mockito.when(transaction.getHash()).thenReturn(Hash.ZERO); + + final Object[] params = new Object[] {Hash.ZERO.toHexString(), 0, Address.ZERO.toHexString()}; + final JsonRpcRequestContext request = + new JsonRpcRequestContext(new JsonRpcRequest("2.0", "debug_accountAt", params)); + + final JsonRpcResponse response = debugAccountAt.response(request); + + Assertions.assertThat(response instanceof JsonRpcErrorResponse).isTrue(); + Assertions.assertThat(((JsonRpcErrorResponse) response).getError()) + .isEqualByComparingTo(JsonRpcError.NO_ACCOUNT_FOUND); + } +} diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/debug_accountAt.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/debug_accountAt.json new file mode 100644 index 0000000000..5df4dbc1c9 --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/debug/debug_accountAt.json @@ -0,0 +1,23 @@ +{ + "request": { + "id": 1, + "jsonrpc": "2.0", + "method": "debug_accountAt", + "params": [ + "0xc8df1f061abb4d0c107b2b1a794ade8780b3120e681f723fe55a7be586d95ba6", + 0, + "0xbcde5374fce5edbc8e2a8697c15331677e6ebf0b" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "code": "0x0", + "nonce": "0x0", + "balance": "0x340aad21b3b70000", + "codehash" : "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + } + }, + "statusCode": 200 +} \ No newline at end of file