Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rpc): adds revert data to eth_call #2281

Merged
merged 5 commits into from
May 17, 2024
Merged
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
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) {
jurajpiar marked this conversation as resolved.
Show resolved Hide resolved

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;
jurajpiar marked this conversation as resolved.
Show resolved Hide resolved

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
Loading