Skip to content

Commit

Permalink
Merge pull request #2281 from rsksmart/juraj/rpc_w_revert_data
Browse files Browse the repository at this point in the history
feat(rpc): adds revert data to eth_call
  • Loading branch information
Vovchyk authored May 17, 2024
2 parents 9b9d831 + fad6deb commit a03fefd
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 58 deletions.
64 changes: 45 additions & 19 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 @@ -33,7 +33,14 @@
import co.rsk.trie.TrieStoreImpl;
import co.rsk.util.HexUtils;
import com.google.common.annotations.VisibleForTesting;
import org.ethereum.core.*;
import org.apache.commons.lang3.tuple.Pair;
import org.ethereum.core.Block;
import org.ethereum.core.Blockchain;
import org.ethereum.core.CallTransaction;
import org.ethereum.core.Repository;
import org.ethereum.core.Transaction;
import org.ethereum.core.TransactionExecutor;
import org.ethereum.core.TransactionPool;
import org.ethereum.datasource.HashMapDB;
import org.ethereum.db.MutableRepository;
import org.ethereum.rpc.CallArguments;
Expand All @@ -51,7 +58,9 @@

import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import static java.util.Arrays.copyOfRange;
import static org.ethereum.rpc.exception.RskJsonRpcRequestException.invalidParamError;
Expand Down Expand Up @@ -137,20 +146,25 @@ 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 {
Pair<String, byte[]> programRevert = decodeProgramRevert(res);
String revertReason = programRevert.getLeft();
byte[] revertData = programRevert.getRight();
if (revertData == null) {
throw RskJsonRpcRequestException.transactionRevertedExecutionError();
}
}

if (revertReason == null) {
throw RskJsonRpcRequestException.transactionRevertedExecutionError(revertData);
}

throw RskJsonRpcRequestException.transactionRevertedExecutionError(revertReason, revertData);
}
hReturn = HexUtils.toUnformattedJsonHex(res.getHReturn());

return hReturn;
} finally {
}
finally {
LOGGER.debug("eth_call(): {}", hReturn);
}
}
Expand Down Expand Up @@ -299,22 +313,34 @@ 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 Pair<String, byte[]> decodeProgramRevert(ProgramResult programResult) {
byte[] bytes = programResult.getHReturn();
if (bytes == null) {

return Pair.of(null, null);
}

final byte[] signature = copyOfRange(res.getHReturn(), 0, 4);
if (bytes.length < 4) {

return Pair.of(null, bytes);
}

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

return Pair.of(null, bytes);
}

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

return Pair.of(null, bytes);
}

final Object[] decode = ERROR_ABI_FUNCTION.decode(res.getHReturn());
return decode != null && decode.length > 0 ? Optional.of((String) decode[0]) : Optional.empty();
return Pair.of((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,11 +18,14 @@

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.apache.commons.lang3.tuple.Pair;
import org.ethereum.config.Constants;
import org.ethereum.core.Blockchain;
import org.ethereum.core.TransactionPool;
Expand All @@ -32,13 +35,8 @@
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 +113,18 @@ 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 {
Pair<String, byte[]> programRevert = EthModule.decodeProgramRevert(programResult);
String revertReason = programRevert.getLeft();
byte[] revertData = programRevert.getRight();
if (revertData == null) {
throw RskJsonRpcRequestException.transactionRevertedExecutionError();
}

if (revertReason == null) {
throw RskJsonRpcRequestException.transactionRevertedExecutionError(revertData);
}

throw RskJsonRpcRequestException.transactionRevertedExecutionError(revertReason, revertData);
}

if (!transactionInfo.getReceipt().isSuccessful()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import co.rsk.core.exception.InvalidRskAddressException;
import co.rsk.jsonrpc.JsonRpcError;
import co.rsk.util.HexUtils;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
Expand Down Expand Up @@ -31,7 +32,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 = revertData == null ? null : HexUtils.toUnformattedJsonHex(revertData);
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,66 @@
package org.ethereum.rpc.exception;

public class RskJsonRpcRequestException extends RuntimeException {
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public class RskJsonRpcRequestException extends RuntimeException {
private final Integer code;

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

protected RskJsonRpcRequestException(Integer code, @Nullable 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, null, message, e);
}

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

public RskJsonRpcRequestException(Integer code, String message) {
this(code, null, message);
}

public Integer getCode() {
return code;
}

public byte[] getRevertData() {
return revertData;
}

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

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

public static RskJsonRpcRequestException transactionRevertedExecutionError(String revertReason) {
return executionError("revert " + revertReason);
public static RskJsonRpcRequestException transactionRevertedExecutionError(
@Nonnull String revertReason,
@Nonnull byte[] revertData
) {
return executionError(
"revert " + revertReason,
revertData
);
}

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
71 changes: 65 additions & 6 deletions rskj-core/src/test/java/co/rsk/rpc/modules/eth/EthModuleTest.java
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,10 +189,69 @@ 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"));
BlockIdentifierParam blockIdentifierParam = new BlockIdentifierParam("latest");

CallArgumentsParam callArgumentsParam = TransactionFactoryHelper.toCallArgumentsParam(args);
RskJsonRpcRequestException exception = assertThrows(
RskJsonRpcRequestException.class,
() -> eth.call(
callArgumentsParam,
blockIdentifierParam
)
);
assertThat(exception.getMessage(), Matchers.containsString("deposit too big"));
assertNotNull(exception.getRevertData());
assertArrayEquals(hReturn, exception.getRevertData());
}

@Test
void test_revertedTransactionWithNoRevertDataOrSizeLowerThan4() {
CallArguments args = new CallArguments();
ExecutionBlockRetriever.Result blockResult = mock(ExecutionBlockRetriever.Result.class);
Block block = mock(Block.class);
ExecutionBlockRetriever retriever = mock(ExecutionBlockRetriever.class);
when(retriever.retrieveExecutionBlock("latest"))
.thenReturn(blockResult);
when(blockResult.getBlock()).thenReturn(block);

ProgramResult executorResult = mock(ProgramResult.class);
when(executorResult.isRevert()).thenReturn(true);

ReversibleTransactionExecutor executor = mock(ReversibleTransactionExecutor.class);
when(executor.executeTransaction(eq(blockResult.getBlock()), any(), any(), any(), any(), any(), any(), any()))
.thenReturn(executorResult);

EthModule eth = new EthModule(
null,
(byte) 0,
null,
null,
executor,
retriever,
null,
null,
null,
new BridgeSupportFactory(
null, null, null, signatureCache),
config.getGasEstimationCap(),
config.getCallGasCap());

BlockIdentifierParam blockIdentifierParam = new BlockIdentifierParam("latest");

CallArgumentsParam callArgumentsParam = TransactionFactoryHelper.toCallArgumentsParam(args);

List<byte[]> hReturns = Arrays.asList(null, new byte[0], Hex.decode("08"), Hex.decode("08c3"), Hex.decode("08c379"));
for (byte[] hReturn : hReturns) {
when(executorResult.getHReturn()).thenReturn(hReturn);

RskJsonRpcRequestException exception = assertThrows(
RskJsonRpcRequestException.class,
() -> eth.call(
callArgumentsParam,
blockIdentifierParam
)
);
assertArrayEquals(hReturn, exception.getRevertData());
}
}

Expand Down
Loading

0 comments on commit a03fefd

Please sign in to comment.