diff --git a/rskj-core/src/main/java/co/rsk/rpc/modules/debug/trace/call/CallTraceTransformer.java b/rskj-core/src/main/java/co/rsk/rpc/modules/debug/trace/call/CallTraceTransformer.java index 04825839bfc..67fa1a6232c 100644 --- a/rskj-core/src/main/java/co/rsk/rpc/modules/debug/trace/call/CallTraceTransformer.java +++ b/rskj-core/src/main/java/co/rsk/rpc/modules/debug/trace/call/CallTraceTransformer.java @@ -35,6 +35,7 @@ import java.math.BigInteger; import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class CallTraceTransformer { @@ -96,7 +97,6 @@ private static TxTraceResult toTrace(TraceType traceType, CallType callType, Inv String from; String to = null; String gas = null; - //TODO input data is missing in TransferInvoke String input = null; String value = null; String output = null; @@ -105,11 +105,7 @@ private static TxTraceResult toTrace(TraceType traceType, CallType callType, Inv String error = null; - if (callType == CallType.DELEGATECALL) { - from = new RskAddress(invoke.getOwnerAddress().getLast20Bytes()).toJsonString(); - } else { - from = new RskAddress(invoke.getCallerAddress().getLast20Bytes()).toJsonString(); - } + from = getFrom(callType, invoke); List logInfoResultList = null; @@ -118,7 +114,7 @@ private static TxTraceResult toTrace(TraceType traceType, CallType callType, Inv if (traceType == TraceType.CREATE) { if (creationData != null) { - input = HexUtils.toJsonHex(creationData.getCreationInput()); + input = HexUtils.toUnformattedJsonHex(creationData.getCreationInput()); output = creationData.getCreatedAddress().toJsonString(); } value = HexUtils.toQuantityJsonHex(callValue.getData()); @@ -126,7 +122,7 @@ private static TxTraceResult toTrace(TraceType traceType, CallType callType, Inv } if (traceType == TraceType.CALL) { - input = HexUtils.toJsonHex(invoke.getDataCopy(DataWord.ZERO, invoke.getDataSize())); + input = HexUtils.toUnformattedJsonHex(invoke.getDataCopy(DataWord.ZERO, invoke.getDataSize())); value = HexUtils.toQuantityJsonHex(callValue.getData()); if (callType == CallType.DELEGATECALL) { @@ -143,35 +139,28 @@ private static TxTraceResult toTrace(TraceType traceType, CallType callType, Inv } - if (programResult != null) { gasUsed = HexUtils.toQuantityJsonHex(programResult.getGasUsed()); if (programResult.isRevert()) { Pair programRevert = EthModule.decodeProgramRevert(programResult); revertReason = programRevert.getLeft(); - output = HexUtils.toJsonHex(programRevert.getRight()); + output = HexUtils.toQuantityJsonHex(programRevert.getRight()); error = "execution reverted"; } else if (traceType != TraceType.CREATE) { - output = HexUtils.toJsonHex(programResult.getHReturn()); + output = HexUtils.toQuantityJsonHex(programResult.getHReturn()); } + if (programResult.getException() != null) { error = programResult.getException().toString(); } } - if(withLog) { - logInfoResultList = new ArrayList<>(); - List logInfoList = programResult.getLogInfoList(); - if(logInfoList != null) { - for (int i = 0; i < programResult.getLogInfoList().size(); i++) { - LogInfo logInfo = programResult.getLogInfoList().get(i); - LogInfoResult logInfoResult = fromLogInfo(logInfo, i); - logInfoResultList.add(logInfoResult); - } - } + if (withLog) { + logInfoResultList = getLogs(programResult); } + return TxTraceResult.builder() .type(type) .from(from) @@ -188,6 +177,30 @@ private static TxTraceResult toTrace(TraceType traceType, CallType callType, Inv } + private static String getFrom(CallType callType, InvokeData invoke) { + if (callType == CallType.DELEGATECALL) { + return new RskAddress(invoke.getOwnerAddress().getLast20Bytes()).toJsonString(); + } else { + return new RskAddress(invoke.getCallerAddress().getLast20Bytes()).toJsonString(); + } + } + + private static List getLogs(ProgramResult programResult) { + if (programResult == null) { + return Collections.emptyList(); + } + List logInfoResultList = new ArrayList<>(); + List logInfoList = programResult.getLogInfoList(); + if (logInfoList != null) { + for (int i = 0; i < programResult.getLogInfoList().size(); i++) { + LogInfo logInfo = programResult.getLogInfoList().get(i); + LogInfoResult logInfoResult = fromLogInfo(logInfo, i); + logInfoResultList.add(logInfoResult); + } + } + return logInfoResultList; + } + private static LogInfoResult fromLogInfo(LogInfo logInfo, int index) { String address = HexUtils.toJsonHex(logInfo.getAddress()); List topics = logInfo.getTopics().stream().map(DataWord::getData).map(HexUtils::toJsonHex).toList(); diff --git a/rskj-core/src/main/java/co/rsk/rpc/modules/debug/trace/call/CallTracer.java b/rskj-core/src/main/java/co/rsk/rpc/modules/debug/trace/call/CallTracer.java index 53b33cf5a1f..b769e2c31df 100644 --- a/rskj-core/src/main/java/co/rsk/rpc/modules/debug/trace/call/CallTracer.java +++ b/rskj-core/src/main/java/co/rsk/rpc/modules/debug/trace/call/CallTracer.java @@ -68,11 +68,16 @@ public JsonNode traceTransaction(@Nonnull String transactionHash, @Nonnull Trace logger.trace("trace_transaction({})", transactionHash); byte[] hash = HexUtils.stringHexToByteArray(transactionHash); + if(hash == null) { + logger.error("Invalid transaction hash: {}", transactionHash); + throw new IllegalArgumentException("Invalid transaction hash: " + transactionHash); + } + TransactionInfo txInfo = this.receiptStore.getInMainChain(hash, this.blockStore).orElse(null); if (txInfo == null) { logger.trace("No transaction info for {}", transactionHash); - return null; + throw new IllegalArgumentException("No transaction info for " + transactionHash); } Block block = this.blockchain.getBlockByHash(txInfo.getBlockHash()); diff --git a/rskj-core/src/main/java/co/rsk/rpc/modules/debug/trace/call/TxTraceResult.java b/rskj-core/src/main/java/co/rsk/rpc/modules/debug/trace/call/TxTraceResult.java index 95eea860732..11a221484a2 100644 --- a/rskj-core/src/main/java/co/rsk/rpc/modules/debug/trace/call/TxTraceResult.java +++ b/rskj-core/src/main/java/co/rsk/rpc/modules/debug/trace/call/TxTraceResult.java @@ -40,6 +40,22 @@ public class TxTraceResult { private final List calls; private final List logs; + //Used by deserializer + public TxTraceResult(){ + this.type = null; + this.from = null; + this.to = null; + this.value = null; + this.gas = null; + this.gasUsed = null; + this.input = null; + this.output = null; + this.error = null; + this.revertReason = null; + this.calls = new ArrayList<>(); + this.logs = new ArrayList<>(); + } + public TxTraceResult(String type, String from, String to, String value, String gas, String gasUsed, String input, String output, String error, String revertReason, List calls, List logs) { this.type = type; this.from = from; diff --git a/rskj-core/src/main/java/org/ethereum/vm/program/invoke/TransferInvoke.java b/rskj-core/src/main/java/org/ethereum/vm/program/invoke/TransferInvoke.java index fa5b77f47ba..cb373bf3a01 100644 --- a/rskj-core/src/main/java/org/ethereum/vm/program/invoke/TransferInvoke.java +++ b/rskj-core/src/main/java/org/ethereum/vm/program/invoke/TransferInvoke.java @@ -84,7 +84,7 @@ public DataWord getDataValue(DataWord indexData) { int size = 32; // maximum datavalue size if (index >= msgData.length - || tempIndex.compareTo(maxMsgData) == 1) { + || tempIndex.compareTo(maxMsgData) > 0) { return DataWord.ZERO; } if (index + size > msgData.length) { diff --git a/rskj-core/src/test/java/co/rsk/rpc/modules/debug/TraceOptionsTest.java b/rskj-core/src/test/java/co/rsk/rpc/modules/debug/TraceOptionsTest.java index b369a5a0917..d5e03e955d6 100644 --- a/rskj-core/src/test/java/co/rsk/rpc/modules/debug/TraceOptionsTest.java +++ b/rskj-core/src/test/java/co/rsk/rpc/modules/debug/TraceOptionsTest.java @@ -19,9 +19,11 @@ package co.rsk.rpc.modules.debug; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -140,4 +142,123 @@ void testTraceOptions_mixOfSupportedAndUnsupportedOptionsGiven_disabledFieldsAnd Assertions.assertTrue(options.getUnsupportedOptions().contains("unsupportedOption.2")); } + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void testDeserialize_withValidJson_shouldSetAllFields() throws IOException { + // Given + String json = """ + { + "onlyTopCall": "true", + "withLog": "false", + "disableMemory": "true", + "disableStack": "false", + "disableStorage": "true" + } + """; + + // When + TraceOptions options = objectMapper.readValue(json, TraceOptions.class); + + // Then + Assertions.assertTrue(options.isOnlyTopCall()); + Assertions.assertFalse(options.isWithLog()); + Assertions.assertTrue(options.getDisabledFields().contains("memory")); + Assertions.assertFalse(options.getDisabledFields().contains("stack")); + Assertions.assertTrue(options.getDisabledFields().contains("storage")); + } + + @Test + void testDeserialize_withUnsupportedOptions_shouldAddToUnsupported() throws IOException { + // Given + String json = """ + { + "onlyTopCall": "true", + "withLog": "true", + "unsupportedOption1": "true", + "unsupportedOption2": "false" + } + """; + + // When + TraceOptions options = objectMapper.readValue(json, TraceOptions.class); + + // Then + Assertions.assertTrue(options.isOnlyTopCall()); + Assertions.assertTrue(options.isWithLog()); + Assertions.assertTrue(options.getUnsupportedOptions().contains("unsupportedOption1")); + Assertions.assertTrue(options.getUnsupportedOptions().contains("unsupportedOption2")); + } + + @Test + void testDeserialize_withTracerConfig_shouldHandleNestedOptions() throws IOException { + // Given + String json = """ + { + "tracerConfig": { + "disableMemory": "true", + "disableStack": "true" + }, + "withLog": "false" + } + """; + + // When + TraceOptions options = objectMapper.readValue(json, TraceOptions.class); + + // Then + Assertions.assertFalse(options.isWithLog()); + Assertions.assertTrue(options.getDisabledFields().contains("memory")); + Assertions.assertTrue(options.getDisabledFields().contains("stack")); + } + + @Test + void testDeserialize_withEmptyJson_shouldReturnDefaultValues() throws IOException { + // Given + String json = "{}"; + + // When + TraceOptions options = objectMapper.readValue(json, TraceOptions.class); + + // Then + Assertions.assertFalse(options.isOnlyTopCall()); + Assertions.assertFalse(options.isWithLog()); + Assertions.assertTrue(options.getDisabledFields().isEmpty()); + Assertions.assertTrue(options.getUnsupportedOptions().isEmpty()); + } + + @Test + void testDeserialize_withInvalidJson_shouldThrowException() { + // Given + String invalidJson = "{ invalid json }"; + + // Then + Assertions.assertThrows(IOException.class, () -> { + // When + objectMapper.readValue(invalidJson, TraceOptions.class); + }); + } + + @Test + void testDeserialize_withConflictingOptions_shouldResolveCorrectly() throws IOException { + // Given + String json = """ + { + "onlyTopCall": "true", + "withLog": "true", + "disableMemory": "true", + "disableMemory": "false" + } + """; + + // When + TraceOptions options = objectMapper.readValue(json, TraceOptions.class); + + // Then + Assertions.assertTrue(options.isOnlyTopCall()); + Assertions.assertTrue(options.isWithLog()); + // Last occurrence of conflicting key takes precedence + Assertions.assertFalse(options.getDisabledFields().contains("memory")); + } + } diff --git a/rskj-core/src/test/java/co/rsk/rpc/modules/debug/trace/call/CallTracerTest.java b/rskj-core/src/test/java/co/rsk/rpc/modules/debug/trace/call/CallTracerTest.java new file mode 100644 index 00000000000..68ba2842050 --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/rpc/modules/debug/trace/call/CallTracerTest.java @@ -0,0 +1,59 @@ +package co.rsk.rpc.modules.debug.trace.call; + +import co.rsk.rpc.ExecutionBlockRetriever; +import co.rsk.rpc.Web3InformationRetriever; +import co.rsk.rpc.modules.debug.TraceOptions; +import co.rsk.test.World; +import co.rsk.test.dsl.DslParser; +import co.rsk.test.dsl.WorldDslProcessor; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.ethereum.core.Account; +import org.ethereum.core.TransactionReceipt; +import org.ethereum.datasource.HashMapDB; +import org.ethereum.db.ReceiptStore; +import org.ethereum.db.ReceiptStoreImpl; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class CallTracerTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + + @Test + void retrieveSimpleStorageContractCreationTrace() throws Exception { + DslParser parser = DslParser.fromResource("dsl/simple_storage.txt"); + ReceiptStore receiptStore = new ReceiptStoreImpl(new HashMapDB()); + World world = new World(receiptStore); + ExecutionBlockRetriever executionBlockRetriever = Mockito.mock(ExecutionBlockRetriever.class); + Web3InformationRetriever web3InformationRetriever = new Web3InformationRetriever(world.getTransactionPool(), world.getBlockChain(), world.getRepositoryLocator(), executionBlockRetriever); + + + WorldDslProcessor processor = new WorldDslProcessor(world); + processor.processCommands(parser); + + //Transaction transaction = world.getTransactionByName("tx01"); + TransactionReceipt contractTransactionReceipt = world.getTransactionReceiptByName("tx01"); + + CallTracer callTracer = new CallTracer(world.getBlockStore(), world.getBlockExecutor(), web3InformationRetriever, receiptStore, world.getBlockChain()); + + JsonNode result = callTracer.traceTransaction(contractTransactionReceipt.getTransaction().getHash().toJsonString(), new TraceOptions()); + + assertNotNull(result); + + TxTraceResult traceResult = objectMapper.treeToValue(result, TxTraceResult.class); + + Account account = world.getAccountByName("acc1"); + + assertNotNull(traceResult); + assertEquals("CREATE", traceResult.getType()); + assertEquals("0x608060405234801561000f575f80fd5b506101438061001d5f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c80636057361d146100385780636d4ce63c14610054575b5f80fd5b610052600480360381019061004d91906100ba565b610072565b005b61005c61007b565b60405161006991906100f4565b60405180910390f35b805f8190555050565b5f8054905090565b5f80fd5b5f819050919050565b61009981610087565b81146100a3575f80fd5b50565b5f813590506100b481610090565b92915050565b5f602082840312156100cf576100ce610083565b5b5f6100dc848285016100a6565b91505092915050565b6100ee81610087565b82525050565b5f6020820190506101075f8301846100e5565b9291505056fea2646970667358221220271ba6597ab51821beed677d25c76e319892db25c1f66b3dc76e547fdc1fd0e164736f6c63430008140033", traceResult.getInput()); + assertEquals(account.getAddress().toJsonString(), traceResult.getFrom()); + + } + +} \ No newline at end of file diff --git a/rskj-core/src/test/java/org/ethereum/rpc/parameters/DebugTracerParamTest.java b/rskj-core/src/test/java/org/ethereum/rpc/parameters/DebugTracerParamTest.java new file mode 100644 index 00000000000..a7ddf97be99 --- /dev/null +++ b/rskj-core/src/test/java/org/ethereum/rpc/parameters/DebugTracerParamTest.java @@ -0,0 +1,191 @@ +package org.ethereum.rpc.parameters; + +import co.rsk.rpc.modules.debug.TraceOptions; +import co.rsk.rpc.modules.debug.trace.TracerType; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Map; + +class DebugTracerParamTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void testConstructor_withNullValues_shouldInitializeFieldsToNull() { + // Given + DebugTracerParam param = new DebugTracerParam(null, null); + + // Then + Assertions.assertNull(param.getTracerType()); + Assertions.assertNull(param.getTraceOptions()); + } + + @Test + void testConstructor_withoutValues_shouldInitializeFieldsToNull() { + // Given + DebugTracerParam param = new DebugTracerParam(); + + // Then + Assertions.assertNull(param.getTracerType()); + Assertions.assertNull(param.getTraceOptions()); + } + + @Test + void testConstructor_withNonNullValues_shouldInitializeFieldsCorrectly() { + // Given + TraceOptions traceOptions = new TraceOptions(Map.of("disableMemory", "true")); + TracerType tracerType = TracerType.CALL_TRACER; + + // When + DebugTracerParam param = new DebugTracerParam(tracerType, traceOptions); + + // Then + Assertions.assertEquals(tracerType, param.getTracerType()); + Assertions.assertEquals(traceOptions, param.getTraceOptions()); + } + + @Test + void oldStyleTracerCall_shouldReturnCorrectInstance() throws IOException { + // Given + String json = """ + { + "disableMemory": "true", + "disableStorage": "false" + } + """; + + // When + DebugTracerParam param = objectMapper.readValue(json, DebugTracerParam.class); + + // Then + Assertions.assertNotNull(param); + Assertions.assertNull(param.getTracerType()); + Assertions.assertNotNull(param.getTraceOptions()); + Assertions.assertTrue(param.getTraceOptions().getDisabledFields().contains("memory")); + Assertions.assertFalse(param.getTraceOptions().getDisabledFields().contains("storage")); + } + + @Test + void callTracerWithConfig_shouldReturnCorrectInstance() throws IOException { + // Given + String json = """ + { + "tracer": "callTracer", + "tracerConfig": { + "onlyTopCall": true, + "withLog": true + } + } + """; + + // When + DebugTracerParam param = objectMapper.readValue(json, DebugTracerParam.class); + + // Then + Assertions.assertNotNull(param); + Assertions.assertEquals(TracerType.CALL_TRACER, param.getTracerType()); + Assertions.assertNotNull(param.getTraceOptions()); + Assertions.assertTrue(param.getTraceOptions().isWithLog()); + Assertions.assertTrue(param.getTraceOptions().isOnlyTopCall()); + } + + + @Test + void testDeserialize_withUnknownTracerType_shouldThrowException() { + // Given + String json = """ + { + "tracer": "unknownTracer" + } + """; + + // Then + Assertions.assertThrows(IllegalArgumentException.class, () -> { + // When + objectMapper.readValue(json, DebugTracerParam.class); + }); + } + + @Test + void testDeserialize_withInvalidTraceOptions_shouldStillDeserialize() throws IOException { + // Given + String json = """ + { + "tracer": "callTracer", + "unsupportedOption": "true" + } + """; + + // When + DebugTracerParam param = objectMapper.readValue(json, DebugTracerParam.class); + + // Then + Assertions.assertNotNull(param); + Assertions.assertEquals(TracerType.CALL_TRACER, param.getTracerType()); + Assertions.assertNotNull(param.getTraceOptions()); + Assertions.assertTrue(param.getTraceOptions().getUnsupportedOptions().contains("unsupportedOption")); + } + + @Test + void testDeserialize_withEmptyJson_shouldReturnDefaultValues() throws IOException { + // Given + String json = "{}"; + + // When + DebugTracerParam param = objectMapper.readValue(json, DebugTracerParam.class); + + // Then + Assertions.assertNotNull(param); + Assertions.assertNull(param.getTracerType()); + Assertions.assertNotNull(param.getTraceOptions()); + Assertions.assertTrue(param.getTraceOptions().getDisabledFields().isEmpty()); + Assertions.assertTrue(param.getTraceOptions().getUnsupportedOptions().isEmpty()); + } + + @Test + void testDeserialize_withInvalidJson_shouldThrowException() { + // Given + String invalidJson = "{ invalid json }"; + + // Then + Assertions.assertThrows(IllegalArgumentException.class, () -> { + // When + objectMapper.readValue(invalidJson, DebugTracerParam.class); + }); + } + + @Test + void testDeserializer_getTracerType_shouldReturnCorrectTracerType() throws IOException { + // Given + String json = """ + { + "tracer": "callTracer" + } + """; + + // When + DebugTracerParam param = objectMapper.readValue(json, DebugTracerParam.class); + + // Then + Assertions.assertEquals(TracerType.CALL_TRACER, param.getTracerType()); + } + + @Test + void testDeserializer_getTracerType_withNull_shouldThrowException() { + // Given + String json = """ + { + "tracer": null + } + """; + + // Then + Assertions.assertThrows(IllegalArgumentException.class, () -> { + // When + objectMapper.readValue(json, DebugTracerParam.class); + }); + } +} \ No newline at end of file diff --git a/rskj-core/src/test/resources/dsl/simple_storage.txt b/rskj-core/src/test/resources/dsl/simple_storage.txt new file mode 100644 index 00000000000..e4d467d5598 --- /dev/null +++ b/rskj-core/src/test/resources/dsl/simple_storage.txt @@ -0,0 +1,21 @@ +account_new acc1 10000000 + +transaction_build tx01 + sender acc1 + receiverAddress 00 + value 0 + data 608060405234801561000f575f80fd5b506101438061001d5f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c80636057361d146100385780636d4ce63c14610054575b5f80fd5b610052600480360381019061004d91906100ba565b610072565b005b61005c61007b565b60405161006991906100f4565b60405180910390f35b805f8190555050565b5f8054905090565b5f80fd5b5f819050919050565b61009981610087565b81146100a3575f80fd5b50565b5f813590506100b481610090565b92915050565b5f602082840312156100cf576100ce610083565b5b5f6100dc848285016100a6565b91505092915050565b6100ee81610087565b82525050565b5f6020820190506101075f8301846100e5565b9291505056fea2646970667358221220271ba6597ab51821beed677d25c76e319892db25c1f66b3dc76e547fdc1fd0e164736f6c63430008140033 + gas 1200000 + build + +block_build b01 + parent g00 + gasLimit 7500000 + transactions tx01 + build + +block_connect b01 + +# Assert best block +assert_best b01 +