Skip to content

Commit

Permalink
feat(rpc): adds revert data to eth_call
Browse files Browse the repository at this point in the history
  • Loading branch information
jurajpiar committed Apr 10, 2024
1 parent 7f6a8e9 commit ab724d8
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 65 deletions.
37 changes: 19 additions & 18 deletions rskj-core/src/main/java/co/rsk/rpc/modules/eth/EthModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -137,16 +137,10 @@ public String call(CallArgumentsParam argsParam, BlockIdentifierParam bnOrId) {
} else {
res = callConstant(args, block);
}

if (res.isRevert()) {
Optional<String> revertReason = decodeRevertReason(res);
if (revertReason.isPresent()) {
throw RskJsonRpcRequestException.transactionRevertedExecutionError(revertReason.get());
} else {
throw RskJsonRpcRequestException.transactionRevertedExecutionError();
}
}

throw RskJsonRpcRequestException.transactionRevertedExecutionError(decodeProgramRevert(res));
}
hReturn = HexUtils.toUnformattedJsonHex(res.getHReturn());

return hReturn;
Expand Down Expand Up @@ -299,22 +293,29 @@ public ProgramResult callConstant(CallArguments args, Block executionBlock) {
* Look for { Error("msg") } function, if it matches decode the "msg" param.
* The 4 first bytes are the function signature.
*
* @param res
* @return revert reason, empty if didnt match.
* @param programResult contains the result of execution of a smart contract
* @return revert reason and revert data. reason may be nullable or empty
*/
public static Optional<String> decodeRevertReason(ProgramResult res) {
byte[] bytes = res.getHReturn();
if (bytes == null || bytes.length < 4) {
return Optional.empty();
public static ProgramRevert decodeProgramRevert(ProgramResult programResult) {
byte[] bytes = programResult.getHReturn();
if (bytes.length < 4) {

return new ProgramRevert(bytes);
}

final byte[] signature = copyOfRange(res.getHReturn(), 0, 4);
final byte[] signature = copyOfRange(bytes, 0, 4);
if (!Arrays.equals(signature, ERROR_ABI_FUNCTION_SIGNATURE)) {
return Optional.empty();

return new ProgramRevert(bytes);
}

final Object[] decode = ERROR_ABI_FUNCTION.decode(bytes);
if (decode == null || decode.length == 0) {

return new ProgramRevert(bytes);
}

final Object[] decode = ERROR_ABI_FUNCTION.decode(res.getHReturn());
return decode != null && decode.length > 0 ? Optional.of((String) decode[0]) : Optional.empty();
return new ProgramRevert((String) decode[0], bytes);
}

private ProgramResult callConstantWithState(CallArguments args, Block executionBlock, Trie state) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,23 @@

package co.rsk.rpc.modules.eth;

import static org.ethereum.rpc.exception.RskJsonRpcRequestException.transactionRevertedExecutionError;
import static org.ethereum.rpc.exception.RskJsonRpcRequestException.unknownError;

import java.util.Optional;

import co.rsk.core.Wallet;
import co.rsk.core.bc.BlockExecutor;
import co.rsk.crypto.Keccak256;
import co.rsk.mine.MinerClient;
import co.rsk.mine.MinerServer;
import co.rsk.net.TransactionGateway;
import co.rsk.util.HexUtils;
import org.ethereum.config.Constants;
import org.ethereum.core.Blockchain;
import org.ethereum.core.TransactionPool;
import org.ethereum.db.TransactionInfo;
import org.ethereum.rpc.exception.RskJsonRpcRequestException;
import org.ethereum.rpc.parameters.CallArgumentsParam;
import org.ethereum.rpc.parameters.HexDataParam;
import org.ethereum.vm.program.ProgramResult;

import co.rsk.core.Wallet;
import co.rsk.core.bc.BlockExecutor;
import co.rsk.crypto.Keccak256;
import co.rsk.mine.MinerClient;
import co.rsk.mine.MinerServer;
import co.rsk.net.TransactionGateway;
import co.rsk.util.HexUtils;
import static org.ethereum.rpc.exception.RskJsonRpcRequestException.transactionRevertedExecutionError;
import static org.ethereum.rpc.exception.RskJsonRpcRequestException.unknownError;

public class EthModuleTransactionInstant extends EthModuleTransactionBase {

Expand Down Expand Up @@ -115,13 +111,7 @@ private String getReturnMessage(String txHash) {
ProgramResult programResult = this.blockExecutor.getProgramResult(hash);

if (programResult != null && programResult.isRevert()) {
Optional<String> revertReason = EthModule.decodeRevertReason(programResult);

if (revertReason.isPresent()) {
throw RskJsonRpcRequestException.transactionRevertedExecutionError(revertReason.get());
} else {
throw RskJsonRpcRequestException.transactionRevertedExecutionError();
}
throw transactionRevertedExecutionError(EthModule.decodeProgramRevert(programResult));
}

if (!transactionInfo.getReceipt().isSuccessful()) {
Expand Down
47 changes: 47 additions & 0 deletions rskj-core/src/main/java/co/rsk/rpc/modules/eth/ProgramRevert.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* This file is part of RskJ
* Copyright (C) 2018 RSK Labs Ltd.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package co.rsk.rpc.modules.eth;

import javax.annotation.Nonnull;

public class ProgramRevert {
@Nonnull
private final String reason;

@Nonnull
private final byte[] data;

public ProgramRevert(@Nonnull byte[] data) {
this("", data);
}

public ProgramRevert(@Nonnull String reason, @Nonnull byte[] data) {
this.reason = reason;
this.data = data;
}

@Nonnull
public String getReason() {
return reason;
}

@Nonnull
public byte[] getData() {
return data;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import java.util.Iterator;
import java.util.List;

import static javax.xml.bind.DatatypeConverter.printHexBinary;

/**
* Created by mario on 17/10/2016.
*/
Expand All @@ -31,7 +33,10 @@ public JsonError resolveError(Throwable t, Method method, List<JsonNode> argumen
"invalid argument 0: hex string has length " + arguments.get(0).asText().replace("0x", "").length() + ", want 40 for RSK address",
null);
} else if (t instanceof RskJsonRpcRequestException) {
error = new JsonError(((RskJsonRpcRequestException) t).getCode(), t.getMessage(), null);
RskJsonRpcRequestException rskJsonRpcRequestException = (RskJsonRpcRequestException) t;
byte[] revertData = rskJsonRpcRequestException.getRevertData();
String errorDataHexString = "0x" + printHexBinary(revertData == null ? new byte[]{} : revertData).toLowerCase();
error = new JsonError(rskJsonRpcRequestException.getCode(), t.getMessage(), errorDataHexString);
} else if (t instanceof InvalidFormatException) {
error = new JsonError(JsonRpcError.INTERNAL_ERROR, "Internal server error, probably due to invalid parameter type", null);
} else if (t instanceof UnrecognizedPropertyException) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,65 @@
package org.ethereum.rpc.exception;

import co.rsk.rpc.modules.eth.ProgramRevert;

import javax.annotation.Nullable;

public class RskJsonRpcRequestException extends RuntimeException {

private final Integer code;

protected RskJsonRpcRequestException(Integer code, String message, Exception e) {
private final byte[] revertData;

protected RskJsonRpcRequestException(Integer code, byte[] revertData, String message, Exception e) {
super(message, e);
this.code = code;
this.revertData = revertData;
}

public RskJsonRpcRequestException(Integer code, String message) {
protected RskJsonRpcRequestException(Integer code, String message, Exception e) {
this(code, new byte[]{}, message, e);
}

public RskJsonRpcRequestException(Integer code, byte[] revertData, String message) {
super(message);
this.code = code;
this.revertData = revertData;
}

public RskJsonRpcRequestException(Integer code, String message) {
this(code, new byte[]{}, message);
}

public Integer getCode() {
return code;
}

public static RskJsonRpcRequestException transactionRevertedExecutionError() {
return executionError("transaction reverted");
@Nullable
public byte[] getRevertData() {
return revertData;
}

public static RskJsonRpcRequestException transactionRevertedExecutionError(ProgramRevert programRevert) {
byte[] revertData = programRevert.getData();
String revertReason = programRevert.getReason();
if (revertReason.isEmpty()) {

return executionError("transaction reverted, no reason specified", revertData);
}

return executionError("revert " + revertReason, revertData);
}

public static RskJsonRpcRequestException transactionRevertedExecutionError(String revertReason) {
return executionError("revert " + revertReason);
public static RskJsonRpcRequestException transactionRevertedExecutionError() {
return executionError("transaction reverted", null);
}

public static RskJsonRpcRequestException unknownError(String message) {
return new RskJsonRpcRequestException(-32009, message);
}

private static RskJsonRpcRequestException executionError(String message) {
return new RskJsonRpcRequestException(-32015, String.format("VM Exception while processing transaction: %s", message));
private static RskJsonRpcRequestException executionError(String message, byte[] revertData) {
return new RskJsonRpcRequestException(-32015, revertData, String.format("VM Exception while processing transaction: %s", message));
}

public static RskJsonRpcRequestException transactionError(String message) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,15 @@ void test_revertedTransaction() {
.thenReturn(blockResult);
when(blockResult.getBlock()).thenReturn(block);

byte[] hreturn = Hex.decode(
byte[] hReturn = Hex.decode(
"08c379a000000000000000000000000000000000000000000000000000000000" +
"0000002000000000000000000000000000000000000000000000000000000000" +
"0000000f6465706f73697420746f6f2062696700000000000000000000000000" +
"00000000");
ProgramResult executorResult = mock(ProgramResult.class);
when(executorResult.isRevert()).thenReturn(true);
when(executorResult.getHReturn())
.thenReturn(hreturn);
.thenReturn(hReturn);

ReversibleTransactionExecutor executor = mock(ReversibleTransactionExecutor.class);
when(executor.executeTransaction(eq(blockResult.getBlock()), any(), any(), any(), any(), any(), any(), any()))
Expand All @@ -189,11 +189,10 @@ void test_revertedTransaction() {
config.getGasEstimationCap(),
config.getCallGasCap());

try {
eth.call(TransactionFactoryHelper.toCallArgumentsParam(args), new BlockIdentifierParam("latest"));
} catch (RskJsonRpcRequestException e) {
assertThat(e.getMessage(), Matchers.containsString("deposit too big"));
}
RskJsonRpcRequestException exception = assertThrows(RskJsonRpcRequestException.class, () -> eth.call(TransactionFactoryHelper.toCallArgumentsParam(args), new BlockIdentifierParam("latest")));
assertThat(exception.getMessage(), Matchers.containsString("deposit too big"));
assertNotNull(exception.getRevertData());
assertEquals(hReturn, exception.getRevertData());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,19 @@
import co.rsk.rpc.ModuleDescription;
import co.rsk.rpc.exception.JsonRpcResponseLimitError;
import co.rsk.rpc.exception.JsonRpcTimeoutError;
import co.rsk.rpc.modules.eth.ProgramRevert;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.googlecode.jsonrpc4j.JsonResponse;
import org.bouncycastle.util.encoders.Hex;
import org.ethereum.rpc.exception.RskErrorResolver;
import org.ethereum.rpc.exception.RskJsonRpcRequestException;
import org.ethereum.rpc.parameters.BlockIdentifierParam;
import org.ethereum.rpc.parameters.CallArgumentsParam;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.ArrayList;
Expand All @@ -38,10 +46,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class JsonRpcCustomServerTest {
Expand Down Expand Up @@ -228,6 +233,33 @@ void sendingRequestWithNonDeclaredMethodShouldFail() throws Exception {
assertEquals(expectedResponse, actualResponse.getResponse().toString());
}

@Test
void failedEthCall_givenRskErrorResolver_shouldThrowWithData() throws JsonProcessingException {
String jsonRequest = "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[{\"to\": \"0x77045E71a7A2c50903d88e564cD72fab11e82051\", \"data\": \"0x00004\"}, \"latest\"],\"id\":1}";
byte[] revertBytes = Hex.decode(
"08c379a000000000000000000000000000000000000000000000000000000000" +
"0000002000000000000000000000000000000000000000000000000000000000" +
"0000000f6465706f73697420746f6f2062696700000000000000000000000000" +
"00000000");
String expectedData = "0x" + Hex.toHexString(revertBytes);
JsonNode request = objectMapper.readTree(jsonRequest);

FakeWeb3ForEthCall web3ForEthCall = mock(FakeWeb3ForEthCall.class);
ProgramRevert fakeProgramRevert = new ProgramRevert("deposit too big", revertBytes);
RskJsonRpcRequestException exception = RskJsonRpcRequestException.transactionRevertedExecutionError(fakeProgramRevert);
when(web3ForEthCall.eth_call(any(), any())).thenThrow(exception);
jsonRpcCustomServer = new JsonRpcCustomServer(web3ForEthCall, FakeWeb3ForEthCall.class, modules, objectMapper);
jsonRpcCustomServer.setErrorResolver(new RskErrorResolver());

JsonResponse actualResponse = jsonRpcCustomServer.handleJsonNodeRequest(request);
String actualData = actualResponse.getResponse().get("error").get("data").asText();

assertEquals(expectedData, actualData);
}

public interface FakeWeb3ForEthCall {
String eth_call(CallArgumentsParam args, BlockIdentifierParam bnOrId);
}

public interface Web3Test {
String test_first(String param1);
Expand Down
Loading

0 comments on commit ab724d8

Please sign in to comment.