diff --git a/framework/src/main/java/org/tron/common/logsfilter/ContractEventParser.java b/framework/src/main/java/org/tron/common/logsfilter/ContractEventParser.java index 48181cb1255..ceefa9a8cae 100644 --- a/framework/src/main/java/org/tron/common/logsfilter/ContractEventParser.java +++ b/framework/src/main/java/org/tron/common/logsfilter/ContractEventParser.java @@ -1,8 +1,7 @@ package org.tron.common.logsfilter; -import static org.tron.common.math.Maths.min; - import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.util.regex.Pattern; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; @@ -38,9 +37,14 @@ public static String parseDataBytes(byte[] data, String typeStr, int index) { byte[] lengthBytes = subBytes(data, start, DATAWORD_UNIT_SIZE); // this length is byte count. no need X 32 int length = intValueExact(lengthBytes); + if (length < 0) { + throw new OutputLengthException("data length:" + length); + } byte[] realBytes = length > 0 ? subBytes(data, start + DATAWORD_UNIT_SIZE, length) : new byte[0]; - return type == Type.STRING ? new String(realBytes) : Hex.toHexString(realBytes); + return type == Type.STRING + ? new String(realBytes, StandardCharsets.UTF_8) + : Hex.toHexString(realBytes); } } catch (OutputLengthException | ArithmeticException e) { logger.debug("parseDataBytes ", e); @@ -74,11 +78,15 @@ protected static Integer intValueExact(byte[] data) { } protected static byte[] subBytes(byte[] src, int start, int length) { - if (ArrayUtils.isEmpty(src) || start >= src.length || length < 0) { - throw new OutputLengthException("data start:" + start + ", length:" + length); + if (ArrayUtils.isEmpty(src)) { + throw new OutputLengthException("source data is empty"); + } + if (start < 0 || start >= src.length || length < 0 || length > src.length - start) { + throw new OutputLengthException( + "data start:" + start + ", length:" + length + ", src.length:" + src.length); } byte[] dst = new byte[length]; - System.arraycopy(src, start, dst, 0, min(length, src.length - start, true)); + System.arraycopy(src, start, dst, 0, length); return dst; } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java index de939bdfff4..b6e7fed0ce0 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java @@ -169,6 +169,8 @@ public enum RequestSource { private static final String NO_BLOCK_HEADER_BY_HASH = "header for hash not found"; private static final String ERROR_SELECTOR = "08c379a0"; // Function selector for Error(string) + private static final int REVERT_REASON_SELECTOR_LENGTH = 4; + private static final int MAX_REVERT_REASON_PAYLOAD_BYTES = 4096; /** * thread pool of query section bloom store */ @@ -469,6 +471,36 @@ private void estimateEnergy(byte[] ownerAddressByte, byte[] contractAddressByte, estimateBuilder.setResult(retBuilder); } + /** + * Decodes an Error(string) revert reason when possible. + * Returns ": reason" on success, otherwise "". + */ + static String tryDecodeRevertReason(byte[] resData) { + if (resData == null || resData.length <= REVERT_REASON_SELECTOR_LENGTH) { + return ""; + } + if (!Hex.toHexString(resData, 0, REVERT_REASON_SELECTOR_LENGTH).equals(ERROR_SELECTOR)) { + return ""; + } + + int revertPayloadLength = resData.length - REVERT_REASON_SELECTOR_LENGTH; + if (revertPayloadLength > MAX_REVERT_REASON_PAYLOAD_BYTES) { + logger.debug("skip parsing oversized revert reason payload: {} bytes", revertPayloadLength); + return ""; + } + + try { + String reason = ContractEventParser.parseDataBytes( + org.bouncycastle.util.Arrays.copyOfRange(resData, REVERT_REASON_SELECTOR_LENGTH, + resData.length), + "string", 0); + return ": " + reason; + } catch (Exception e) { + logger.debug("parse revert reason failed", e); + return ""; + } + } + /** * @param data Hash of the method signature and encoded parameters. for example: * getMethodSign(methodName(uint256,uint256)) || data1 || data2 @@ -512,14 +544,8 @@ private String call(byte[] ownerAddressByte, byte[] contractAddressByte, long va } result = ByteArray.toJsonHex(listBytes); } else { - String errMsg = retBuilder.getMessage().toStringUtf8(); byte[] resData = trxExtBuilder.getConstantResult(0).toByteArray(); - if (resData.length > 4 && Hex.toHexString(resData).startsWith(ERROR_SELECTOR)) { - String msg = ContractEventParser - .parseDataBytes(org.bouncycastle.util.Arrays.copyOfRange(resData, 4, resData.length), - "string", 0); - errMsg += ": " + msg; - } + String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(resData); if (resData.length > 0) { throw new JsonRpcInternalException(errMsg, ByteArray.toJsonHex(resData)); @@ -677,15 +703,8 @@ public String estimateGas(CallArguments args) throws JsonRpcInvalidRequestExcept } if (trxExtBuilder.getTransaction().getRet(0).getRet().equals(code.FAILED)) { - String errMsg = retBuilder.getMessage().toStringUtf8(); - byte[] data = trxExtBuilder.getConstantResult(0).toByteArray(); - if (data.length > 4 && Hex.toHexString(data).startsWith(ERROR_SELECTOR)) { - String msg = ContractEventParser - .parseDataBytes(org.bouncycastle.util.Arrays.copyOfRange(data, 4, data.length), - "string", 0); - errMsg += ": " + msg; - } + String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(data); if (data.length > 0) { throw new JsonRpcInternalException(errMsg, ByteArray.toJsonHex(data)); diff --git a/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java b/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java index eff644b9cd9..9f7387adada 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java @@ -5,6 +5,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import org.bouncycastle.crypto.OutputLengthException; import org.bouncycastle.util.Arrays; import org.junit.Assert; import org.junit.Test; @@ -101,6 +102,81 @@ public synchronized void testEventParser() { } + @Test + public void testParseDataBytesIntegerTypes() { + // uint256 = 255 + byte[] uintData = ByteArray.fromHexString( + "00000000000000000000000000000000000000000000000000000000000000ff"); + Assert.assertEquals("255", ContractEventParser.parseDataBytes(uintData, "uint256", 0)); + + // int256 = -1 (two's complement 0xFF..FF is signed negative one) + byte[] negIntData = ByteArray.fromHexString( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + Assert.assertEquals("-1", ContractEventParser.parseDataBytes(negIntData, "int256", 0)); + + // trcToken is classified as INT_NUMBER + byte[] tokenData = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000064"); + Assert.assertEquals("100", ContractEventParser.parseDataBytes(tokenData, "trcToken", 0)); + } + + @Test + public void testParseDataBytesBool() { + byte[] trueData = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000001"); + Assert.assertEquals("true", ContractEventParser.parseDataBytes(trueData, "bool", 0)); + + byte[] falseData = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("false", ContractEventParser.parseDataBytes(falseData, "bool", 0)); + } + + @Test + public void testParseDataBytesFixedBytes() { + String hex = "1234567890abcdef0000000000000000000000000000000000000000000000ff"; + byte[] data = ByteArray.fromHexString(hex); + Assert.assertEquals(hex, ContractEventParser.parseDataBytes(data, "bytes32", 0)); + } + + @Test + public void testParseDataBytesAddress() { + Wallet.setAddressPreFixByte(ADD_PRE_FIX_BYTE_MAINNET); + // last 20 bytes = ca35...733c => Base58Check = TUQPrDEJkV4ttkrL7cVv1p3mikWYfM7LWt + byte[] data = ByteArray.fromHexString( + "000000000000000000000000ca35b7d915458ef540ade6068dfe2f44e8fa733c"); + Assert.assertEquals("TUQPrDEJkV4ttkrL7cVv1p3mikWYfM7LWt", + ContractEventParser.parseDataBytes(data, "address", 0)); + } + + @Test + public void testParseDataBytesDynamicBytes() { + // offset 0x20 | length 3 | 0x010203 padded to 32 bytes + byte[] data = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000003" + + "0102030000000000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("010203", ContractEventParser.parseDataBytes(data, "bytes", 0)); + } + + @Test + public void testParseDataBytesEmptyString() { + // offset 0x20 | length 0 + byte[] data = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("", ContractEventParser.parseDataBytes(data, "string", 0)); + } + + @Test + public void testParseDataBytesNonEmptyString() { + // "hello world" is 11 ASCII bytes (68656c6c6f20776f726c64), padded to 32 bytes. + byte[] data = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000000000000b" + + "68656c6c6f20776f726c64000000000000000000000000000000000000000000"); + Assert.assertEquals("hello world", ContractEventParser.parseDataBytes(data, "string", 0)); + } + @Test public void testParseRevert() { String dataHex = "08c379a0" @@ -114,4 +190,87 @@ public void testParseRevert() { Assert.assertEquals(msg, "not enough input value"); } + + @Test + public void testSubBytesRejectsOversizedLength() { + // Length must fit in the available source bytes. Reject instead of + // truncating so oversized ABI lengths are not silently coerced. + byte[] src = new byte[]{1, 2, 3}; + try { + ContractEventParser.subBytes(src, 0, Integer.MAX_VALUE); + Assert.fail("Expected OutputLengthException"); + } catch (OutputLengthException e) { + Assert.assertTrue(e.getMessage().contains("data start:0")); + Assert.assertTrue(e.getMessage().contains("length:2147483647")); + Assert.assertTrue(e.getMessage().contains("src.length:3")); + } + } + + @Test + public void testSubBytesAcceptsExactLength() { + byte[] src = new byte[]{1, 2, 3, 4}; + byte[] result = ContractEventParser.subBytes(src, 1, 3); + Assert.assertArrayEquals(new byte[]{2, 3, 4}, result); + } + + @Test + public void testSubBytesRejectsNegativeOffset() { + // ABI offsets are unsigned, but BigInteger(byte[]) interprets 0xFF..FF as + // -1. The guard should reject that value before System.arraycopy runs. + byte[] src = new byte[]{1, 2, 3, 4}; + try { + ContractEventParser.subBytes(src, -1, 3); + Assert.fail("Expected OutputLengthException"); + } catch (OutputLengthException e) { + Assert.assertTrue(e.getMessage().contains("data start:-1")); + Assert.assertTrue(e.getMessage().contains("length:3")); + Assert.assertTrue(e.getMessage().contains("src.length:4")); + } + } + + @Test + public void testSubBytesRejectsEmptySource() { + try { + ContractEventParser.subBytes(new byte[0], 0, 0); + Assert.fail("Expected OutputLengthException"); + } catch (OutputLengthException e) { + Assert.assertTrue(e.getMessage().contains("source data is empty")); + } + } + + @Test(expected = UnsupportedOperationException.class) + public void testParseDataBytesRejectsNegativeOffset() { + // End-to-end check: an offset field of 0xFF..FF decodes to -1 and should + // be rejected through the existing UnsupportedOperationException path. + String dataHex = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "0000000000000000000000000000000000000000000000000000000000000003" + + "414243"; + byte[] data = ByteArray.fromHexString(dataHex); + + ContractEventParser.parseDataBytes(data, "string", 0); + } + + @Test(expected = UnsupportedOperationException.class) + public void testParseDataBytesRejectsMalformedLength() { + // ABI-encoded "string" whose declared length exceeds the available payload + // should be rejected via the existing UnsupportedOperationException path. + String dataHex = "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000007fffffff" + + "414243"; + byte[] data = ByteArray.fromHexString(dataHex); + + ContractEventParser.parseDataBytes(data, "string", 0); + } + + @Test(expected = UnsupportedOperationException.class) + public void testParseDataBytesRejectsNegativeLength() { + // ABI length is an unsigned word. If 0xFF..FF is decoded as -1, reject it + // instead of treating it as an empty string/bytes payload. + String dataHex = "0000000000000000000000000000000000000000000000000000000000000020" + + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "414243"; + byte[] data = ByteArray.fromHexString(dataHex); + + ContractEventParser.parseDataBytes(data, "string", 0); + } } diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java new file mode 100644 index 00000000000..52915e2beef --- /dev/null +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java @@ -0,0 +1,299 @@ +package org.tron.core.jsonrpc; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.tron.api.GrpcAPI.EstimateEnergyMessage; +import org.tron.api.GrpcAPI.Return; +import org.tron.api.GrpcAPI.TransactionExtention; +import org.tron.common.parameter.CommonParameter; +import org.tron.common.utils.ByteArray; +import org.tron.core.Wallet; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.db.Manager; +import org.tron.core.exception.jsonrpc.JsonRpcInternalException; +import org.tron.core.services.NodeInfoService; +import org.tron.core.services.jsonrpc.TronJsonRpcImpl; +import org.tron.core.services.jsonrpc.types.CallArguments; +import org.tron.protos.Protocol; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract; + +public class JsonRpcCallAndEstimateGasTest { + + private static final String ERROR_REVERT_HEX = "08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000016" + + "6e6f7420656e6f75676820696e7075742076616c756500000000000000000000"; + private static final String REVERT_MSG = "REVERT opcode executed"; + private static final String MOCK_FROM_ADDRESS = "0x0000000000000000000000000000000000000000"; + private static final String MOCK_TO_ADDRESS = "0x0000000000000000000000000000000000000001"; + + private enum EstimatePath { + CONSTANT_CALL, + ESTIMATE_ENERGY + } + + private final boolean originalEstimateEnergy = CommonParameter.getInstance().isEstimateEnergy(); + private TronJsonRpcImpl mockRpc; + + @After + public void tearDown() throws Exception { + if (mockRpc != null) { + mockRpc.close(); + mockRpc = null; + } + CommonParameter.getInstance().setEstimateEnergy(originalEstimateEnergy); + Mockito.clearAllCaches(); + } + + @Test + public void testGetCallAppendsRevertReason() throws Exception { + byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX); + + mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.CONSTANT_CALL); + + try { + mockRpc.getCall(newCallArgs(), "latest"); + Assert.fail("expected JsonRpcInternalException"); + } catch (JsonRpcInternalException e) { + Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage()); + } + } + + @Test + public void testGetCallSkipsRevertReasonForPanicSelector() throws Exception { + byte[] panicData = ByteArray.fromHexString("4e487b71" + + "0000000000000000000000000000000000000000000000000000000000000001"); + + mockRpc = newRpcWithMockedFailedCall(panicData, EstimatePath.CONSTANT_CALL); + + try { + mockRpc.getCall(newCallArgs(), "latest"); + Assert.fail("expected JsonRpcInternalException"); + } catch (JsonRpcInternalException e) { + Assert.assertEquals(REVERT_MSG, e.getMessage()); + } + } + + @Test + public void testGetCallSkipsRevertReasonForShortData() throws Exception { + mockRpc = newRpcWithMockedFailedCall(new byte[] {1, 2, 3}, EstimatePath.CONSTANT_CALL); + + try { + mockRpc.getCall(newCallArgs(), "latest"); + Assert.fail("expected JsonRpcInternalException"); + } catch (JsonRpcInternalException e) { + Assert.assertEquals(REVERT_MSG, e.getMessage()); + } + } + + @Test + public void testEstimateGasAppendsRevertReason() throws Exception { + byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX); + + mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.CONSTANT_CALL); + CommonParameter.getInstance().setEstimateEnergy(false); + + try { + mockRpc.estimateGas(newCallArgs()); + Assert.fail("expected JsonRpcInternalException"); + } catch (JsonRpcInternalException e) { + Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage()); + } + } + + @Test + public void testEstimateGasSkipsRevertReasonForEmptyData() throws Exception { + mockRpc = newRpcWithMockedFailedCall(new byte[0], EstimatePath.CONSTANT_CALL); + CommonParameter.getInstance().setEstimateEnergy(false); + + try { + mockRpc.estimateGas(newCallArgs()); + Assert.fail("expected JsonRpcInternalException"); + } catch (JsonRpcInternalException e) { + Assert.assertEquals(REVERT_MSG, e.getMessage()); + } + } + + @Test + public void testEstimateGasWithEstimateEnergyAppendsRevertReason() throws Exception { + byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX); + + mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.ESTIMATE_ENERGY); + CommonParameter.getInstance().setEstimateEnergy(true); + + try { + mockRpc.estimateGas(newCallArgs()); + Assert.fail("expected JsonRpcInternalException"); + } catch (JsonRpcInternalException e) { + Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage()); + } + } + + @Test + public void testEstimateGasWithEstimateEnergySkipsRevertReasonForShortData() throws Exception { + mockRpc = newRpcWithMockedFailedCall(new byte[] {1, 2, 3}, EstimatePath.ESTIMATE_ENERGY); + CommonParameter.getInstance().setEstimateEnergy(true); + + try { + mockRpc.estimateGas(newCallArgs()); + Assert.fail("expected JsonRpcInternalException"); + } catch (JsonRpcInternalException e) { + Assert.assertEquals(REVERT_MSG, e.getMessage()); + } + } + + @Test + public void testEstimateGasWithEstimateEnergyReturnsEstimatedEnergy() throws Exception { + long energyRequired = 0x4321L; + + mockRpc = newRpcWithMockedEstimateGasSuccessfulCall(energyRequired, + EstimatePath.ESTIMATE_ENERGY); + CommonParameter.getInstance().setEstimateEnergy(true); + + String result = mockRpc.estimateGas(newCallArgs()); + + Assert.assertEquals(ByteArray.toJsonHex(energyRequired), result); + } + + @Test + public void testGetCallReturnsConstantResult() throws Exception { + byte[] part1 = ByteArray.fromHexString("deadbeef"); + byte[] part2 = ByteArray.fromHexString("cafebabe"); + + mockRpc = newRpcWithMockedSuccessfulCall(part1, part2); + + String result = mockRpc.getCall(newCallArgs(), "latest"); + + Assert.assertEquals("0xdeadbeefcafebabe", result); + } + + @Test + public void testEstimateGasReturnsEnergyUsed() throws Exception { + long energyUsed = 0x1234L; + + mockRpc = newRpcWithMockedEstimateGasSuccessfulCall(energyUsed, EstimatePath.CONSTANT_CALL); + CommonParameter.getInstance().setEstimateEnergy(false); + + String result = mockRpc.estimateGas(newCallArgs()); + + Assert.assertEquals(ByteArray.toJsonHex(energyUsed), result); + } + + private static CallArguments newCallArgs() { + CallArguments args = new CallArguments(); + args.setFrom(MOCK_FROM_ADDRESS); + args.setTo(MOCK_TO_ADDRESS); + args.setValue("0x0"); + args.setData("0x"); + return args; + } + + private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData, EstimatePath path) + throws Exception { + Wallet mockWallet = mock(Wallet.class); + Manager mockManager = mock(Manager.class); + NodeInfoService mockNodeInfo = mock(NodeInfoService.class); + + when(mockWallet.createTransactionCapsule(any(), any())) + .thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build())); + when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance()); + + if (path == EstimatePath.ESTIMATE_ENERGY) { + when(mockWallet.estimateEnergy(any(), any(), any(), any(), any())) + .thenAnswer(invocation -> { + TransactionExtention.Builder extBuilder = invocation.getArgument(2); + Return.Builder retBuilder = invocation.getArgument(3); + EstimateEnergyMessage.Builder estimateBuilder = invocation.getArgument(4); + extBuilder.addConstantResult(ByteString.copyFrom(resData)); + retBuilder.setMessage(ByteString.copyFromUtf8(REVERT_MSG)); + estimateBuilder.setResult(retBuilder); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.FAILED)) + .build(); + }); + } else { + when(mockWallet.triggerConstantContract(any(), any(), any(), any())) + .thenAnswer(invocation -> { + TransactionExtention.Builder extBuilder = invocation.getArgument(2); + Return.Builder retBuilder = invocation.getArgument(3); + extBuilder.addConstantResult(ByteString.copyFrom(resData)); + retBuilder.setMessage(ByteString.copyFromUtf8(REVERT_MSG)); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.FAILED)) + .build(); + }); + } + + return new TronJsonRpcImpl(mockNodeInfo, mockWallet, mockManager); + } + + private static TronJsonRpcImpl newRpcWithMockedSuccessfulCall(byte[]... constantResults) + throws Exception { + Wallet mockWallet = mock(Wallet.class); + Manager mockManager = mock(Manager.class); + NodeInfoService mockNodeInfo = mock(NodeInfoService.class); + + when(mockWallet.createTransactionCapsule(any(), any())) + .thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build())); + when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance()); + + when(mockWallet.triggerConstantContract(any(), any(), any(), any())) + .thenAnswer(invocation -> { + TransactionExtention.Builder extBuilder = invocation.getArgument(2); + for (byte[] bytes : constantResults) { + extBuilder.addConstantResult(ByteString.copyFrom(bytes)); + } + extBuilder.setEnergyUsed(0L); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.SUCESS)) + .build(); + }); + + return new TronJsonRpcImpl(mockNodeInfo, mockWallet, mockManager); + } + + private static TronJsonRpcImpl newRpcWithMockedEstimateGasSuccessfulCall(long energyValue, + EstimatePath path) throws Exception { + Wallet mockWallet = mock(Wallet.class); + Manager mockManager = mock(Manager.class); + NodeInfoService mockNodeInfo = mock(NodeInfoService.class); + + when(mockWallet.createTransactionCapsule(any(), any())) + .thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build())); + when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance()); + + if (path == EstimatePath.ESTIMATE_ENERGY) { + when(mockWallet.estimateEnergy(any(), any(), any(), any(), any())) + .thenAnswer(invocation -> { + EstimateEnergyMessage.Builder estimateBuilder = invocation.getArgument(4); + estimateBuilder.setEnergyRequired(energyValue); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.SUCESS)) + .build(); + }); + } else { + when(mockWallet.triggerConstantContract(any(), any(), any(), any())) + .thenAnswer(invocation -> { + TransactionExtention.Builder extBuilder = invocation.getArgument(2); + extBuilder.setEnergyUsed(energyValue); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.SUCESS)) + .build(); + }); + } + + return new TronJsonRpcImpl(mockNodeInfo, mockWallet, mockManager); + } +} diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcRevertReasonTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcRevertReasonTest.java new file mode 100644 index 00000000000..c788beaa66d --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcRevertReasonTest.java @@ -0,0 +1,88 @@ +package org.tron.core.services.jsonrpc; + +import org.junit.Assert; +import org.junit.Test; +import org.tron.common.utils.ByteArray; + +public class TronJsonRpcRevertReasonTest { + + @Test + public void testTryDecodeRevertReasonWithMalformedLength() { + // Error(string) selector + offset=0x20 + length=0x7FFFFFFF + 3 bytes of payload. + // parseDataBytes throws because the declared length exceeds the buffer. + // The helper should return "" and leave the raw revert hex untouched. + byte[] resData = ByteArray.fromHexString("08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000007fffffff" + + "414243"); + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithNegativeLength() { + byte[] resData = ByteArray.fromHexString("08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "414243"); + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithValidData() { + byte[] resData = ByteArray.fromHexString("08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000016" + + "6e6f7420656e6f75676820696e7075742076616c756500000000000000000000"); + Assert.assertEquals(": not enough input value", + TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithOversizedPayload() { + // selector(4) + payload(4097) one byte over the 4096 limit: must be rejected before parse. + byte[] resData = new byte[4101]; + resData[0] = 0x08; + resData[1] = (byte) 0xc3; + resData[2] = 0x79; + resData[3] = (byte) 0xa0; + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithNullData() { + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(null)); + } + + @Test + public void testTryDecodeRevertReasonWithShortSelector() { + // length == selector length (4): not enough bytes for any payload, reject. + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(new byte[]{ + 0x08, (byte) 0xc3, 0x79, (byte) 0xa0})); + } + + @Test + public void testTryDecodeRevertReasonWithNonErrorSelector() { + // Non-Error(string) selector (e.g. Panic(uint256) = 0x4e487b71) must be rejected. + byte[] resData = ByteArray.fromHexString("4e487b71" + + "0000000000000000000000000000000000000000000000000000000000000001"); + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonAtPayloadLimit() { + // selector(4) + payload(4096) exactly at the limit: must go through parse, not size-reject. + byte[] resData = new byte[4100]; + resData[0] = 0x08; + resData[1] = (byte) 0xc3; + resData[2] = 0x79; + resData[3] = (byte) 0xa0; + // ABI offset = 0x20 + resData[4 + 31] = 0x20; + // ABI string length = 2 + resData[4 + 32 + 31] = 0x02; + // data "ok", remaining bytes stay zero-padded + resData[4 + 64] = 'o'; + resData[4 + 65] = 'k'; + Assert.assertEquals(": ok", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } +}