From f67d5efaf97b89a1b2d94b4caf9586e24ff35f32 Mon Sep 17 00:00:00 2001 From: waynercheung Date: Mon, 3 Aug 2026 17:53:10 +0800 Subject: [PATCH] fix(jsonrpc): handle null parameters consistently Validate nullable JSON-RPC arguments before they are dereferenced. Return stable protocol errors instead of jsonrpc4j's unhandled -32001 fallback, which exposes Java exception types. Validate eth_call's required transaction argument before its block selector. The double-null case now returns -32602 instead of -32600. Align eth_uninstallFilter lookup misses with geth: null, unknown and already removed IDs return false. Empty and non-hexadecimal strings also return false when no filter matches, without new format checks. Non-null lookup misses now return false instead of -32000. Use the value returned by remove to report whether a filter was removed and avoid the race between checking and removing the same entry. Remove the obsolete uninstall ItemNotFoundException declaration and error mapping. Java binaries remain compatible, but source callers catching that checked exception, or implementations still declaring it, may need adjustment when recompiled. Keep getFilterChanges and getFilterLogs on their existing lookup-miss errors, and preserve request-source precedence and filter isolation. Treat null block-detail flags as false and retain DTO defaults for explicitly null optional fields. Preserve filter wildcard semantics. Add binding, service, wire and VM execution regressions for null arguments, error contracts, filter removal, source isolation and concurrent uninstall results. --- .../core/services/jsonrpc/JsonRpcApiUtil.java | 1 + .../core/services/jsonrpc/TronJsonRpc.java | 3 +- .../services/jsonrpc/TronJsonRpcImpl.java | 50 +- .../services/jsonrpc/filters/LogFilter.java | 8 +- .../jsonrpc/types/BuildArguments.java | 17 +- .../services/jsonrpc/types/CallArguments.java | 12 +- .../JsonRpcArgumentNullBindingTest.java | 54 ++ .../JsonRpcCallAndEstimateGasTest.java | 29 + .../org/tron/core/jsonrpc/JsonRpcTest.java | 26 +- .../tron/core/jsonrpc/JsonrpcServiceTest.java | 600 +++++++++++++++++- .../tron/core/jsonrpc/WalletCursorTest.java | 114 +++- 11 files changed, 878 insertions(+), 36 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/jsonrpc/JsonRpcArgumentNullBindingTest.java diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java index f4bba9fbf37..6bc6ac26c4d 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java @@ -63,6 +63,7 @@ public class JsonRpcApiUtil { public static final String TAG_SAFE_SUPPORT_ERROR = "TAG safe not supported"; public static final String BLOCK_NUM_ERROR = "invalid block number"; public static final String TX_INDEX_ERROR = "invalid index value"; + public static final String INVALID_FILTER_REQUEST = "invalid filter request"; private static final SecureRandom random = new SecureRandom(); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java index 50da763b8b9..476bf76f724 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java @@ -307,10 +307,9 @@ String newFilter(FilterRequest fr) throws JsonRpcInvalidParamsException, @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcMethodNotFoundException.class, code = -32601, data = "{}"), @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), - @JsonRpcError(exception = ItemNotFoundException.class, code = -32000, data = "{}"), }) boolean uninstallFilter(String filterId) throws JsonRpcInvalidParamsException, - JsonRpcMethodNotFoundException, ItemNotFoundException; + JsonRpcMethodNotFoundException; @JsonRpcMethod("eth_getFilterChanges") @JsonRpcErrors({ 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 6be47886117..94d46170a6d 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 @@ -122,6 +122,7 @@ public enum RequestSource { } private static final String FILTER_NOT_FOUND = "filter not found"; + private static final String INVALID_PARAMS = "invalid params"; public static final int EXPIRE_SECONDS = 5 * 60; private final int maxBlockFilterNum = Args.getInstance().getJsonRpcMaxBlockFilterNum(); private final int maxLogFilterNum = Args.getInstance().getJsonRpcMaxLogFilterNum(); @@ -363,14 +364,14 @@ public String ethGetBlockTransactionCountByNumber(String blockNumOrTag) public BlockResult ethGetBlockByHash(String blockHash, Boolean fullTransactionObjects) throws JsonRpcInvalidParamsException { final Block b = getBlockByJsonHash(blockHash); - return getBlockResult(b, fullTransactionObjects); + return getBlockResult(b, Boolean.TRUE.equals(fullTransactionObjects)); } @Override public BlockResult ethGetBlockByNumber(String blockNumOrTag, Boolean fullTransactionObjects) throws JsonRpcInvalidParamsException { final Block b = getBlockByNumOrTag(blockNumOrTag); - return (b == null ? null : getBlockResult(b, fullTransactionObjects)); + return (b == null ? null : getBlockResult(b, Boolean.TRUE.equals(fullTransactionObjects))); } /** @@ -673,6 +674,10 @@ public String gasPrice() { @Override public String estimateGas(CallArguments args) throws JsonRpcInvalidRequestException, JsonRpcInvalidParamsException, JsonRpcInternalException { + if (args == null) { + throw new JsonRpcInvalidParamsException(INVALID_PARAMS); + } + byte[] ownerAddress = addressCompatibleToByteArray(args.getFrom()); ContractType contractType = args.getContractType(wallet); @@ -1001,6 +1006,9 @@ private List getTransactionReceiptsFromBlock(BlockCapsule bl public String getCall(CallArguments transactionCall, Object blockParamObj) throws JsonRpcInvalidParamsException, JsonRpcInvalidRequestException, JsonRpcInternalException { + if (transactionCall == null) { + throw new JsonRpcInvalidParamsException(INVALID_PARAMS); + } String blockNumOrTag; if (blockParamObj instanceof HashMap) { @@ -1333,6 +1341,10 @@ public TransactionJson buildTransaction(BuildArguments args) throw new JsonRpcMethodNotFoundException(msg); } + if (args == null) { + throw new JsonRpcInvalidParamsException(INVALID_PARAMS); + } + byte[] fromAddressData; try { fromAddressData = addressCompatibleToByteArray(args.getFrom()); @@ -1441,6 +1453,10 @@ public String newFilter(FilterRequest fr) throws JsonRpcInvalidParamsException, JsonRpcMethodNotFoundException, JsonRpcExceedLimitException { disableInPBFT("eth_newFilter"); + if (fr == null) { + throw new JsonRpcInvalidParamsException(JsonRpcApiUtil.INVALID_FILTER_REQUEST); + } + // not supports finalized as block parameter if (FINALIZED_STR.equalsIgnoreCase(fr.getFromBlock()) || FINALIZED_STR.equalsIgnoreCase(fr.getToBlock())) { @@ -1488,10 +1504,13 @@ public String newBlockFilter() throws JsonRpcMethodNotFoundException, } @Override - public boolean uninstallFilter(String filterId) throws ItemNotFoundException, - JsonRpcMethodNotFoundException { + public boolean uninstallFilter(String filterId) throws JsonRpcMethodNotFoundException { disableInPBFT("eth_uninstallFilter"); + if (filterId == null) { + return false; + } + Map blockFilter2Result; Map eventFilter2Result; if (getSource() == RequestSource.FULLNODE) { @@ -1503,15 +1522,10 @@ public boolean uninstallFilter(String filterId) throws ItemNotFoundException, } filterId = ByteArray.fromHex(filterId); - if (eventFilter2Result.containsKey(filterId)) { - eventFilter2Result.remove(filterId); - } else if (blockFilter2Result.containsKey(filterId)) { - blockFilter2Result.remove(filterId); - } else { - throw new ItemNotFoundException(FILTER_NOT_FOUND); + if (eventFilter2Result.remove(filterId) != null) { + return true; } - - return true; + return blockFilter2Result.remove(filterId) != null; } @Override @@ -1519,6 +1533,10 @@ public Object[] getFilterChanges(String filterId) throws ItemNotFoundException, JsonRpcMethodNotFoundException { disableInPBFT("eth_getFilterChanges"); + if (filterId == null) { + throw new ItemNotFoundException(FILTER_NOT_FOUND); + } + Map blockFilter2Result; Map eventFilter2Result; if (getSource() == RequestSource.FULLNODE) { @@ -1540,6 +1558,10 @@ public LogFilterElement[] getLogs(FilterRequest fr) throws JsonRpcInvalidParamsE JsonRpcMethodNotFoundException, JsonRpcTooManyResultException { disableInPBFT("eth_getLogs"); + if (fr == null) { + throw new JsonRpcInvalidParamsException(JsonRpcApiUtil.INVALID_FILTER_REQUEST); + } + long currentMaxBlockNum = wallet.getNowBlock().getBlockHeader().getRawData().getNumber(); //convert FilterRequest to LogFilterWrapper LogFilterWrapper logFilterWrapper = new LogFilterWrapper(fr, currentMaxBlockNum, wallet, true); @@ -1554,6 +1576,10 @@ public LogFilterElement[] getFilterLogs(String filterId) throws JsonRpcMethodNotFoundException, JsonRpcTooManyResultException { disableInPBFT("eth_getFilterLogs"); + if (filterId == null) { + throw new ItemNotFoundException(FILTER_NOT_FOUND); + } + Map eventFilter2Result; if (getSource() == RequestSource.FULLNODE) { eventFilter2Result = eventFilter2ResultFull; diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java index 03232f3549d..40230ab8090 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java @@ -1,5 +1,6 @@ package org.tron.core.services.jsonrpc.filters; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.INVALID_FILTER_REQUEST; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.addressToByteArray; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.topicToByteArray; @@ -28,7 +29,8 @@ public class LogFilter { private byte[][] contractAddresses = new byte[0][]; // example: [[func1, func2], null, [A, B], [C]] // first topic must be func1 or func2,second can be any,third must be A or B,forth must be C - // [A, null] is not allowed. + // A null positional topic is a wildcard, so [A, null] is valid. + // A null entry in an OR-list is invalid, so [[A, null]] is not allowed. @Getter @Setter private List topics = new ArrayList<>(); @@ -46,6 +48,10 @@ public LogFilter() { * construct one LogFilter from part parameters of FilterRequest */ public LogFilter(FilterRequest fr) throws JsonRpcInvalidParamsException { + if (fr == null) { + throw new JsonRpcInvalidParamsException(INVALID_FILTER_REQUEST); + } + if (fr.getAddress() instanceof String) { withContractAddress(addressToByteArray((String) fr.getAddress())); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/BuildArguments.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/BuildArguments.java index ef4e958ae44..84853a1fac0 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/types/BuildArguments.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/BuildArguments.java @@ -6,6 +6,8 @@ import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseQuantityValue; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.requireValidHex; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; import com.google.protobuf.ByteString; import java.util.Arrays; import lombok.AllArgsConstructor; @@ -23,6 +25,13 @@ import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.contract.SmartContractOuterClass.SmartContract; +/** + * Arguments for {@code buildTransaction}. + * + *

Fields annotated with {@link JsonSetter} retain their defaults when an explicit JSON + * {@code null} is deserialized. This protection applies to Jackson input only; direct setter calls + * may still assign {@code null} and callers must preserve the DTO invariants. + */ @NoArgsConstructor @AllArgsConstructor @ToString @@ -65,18 +74,22 @@ public class BuildArguments { @Getter @Setter + @JsonSetter(nulls = Nulls.SKIP) private Long tokenId = 0L; @Getter @Setter + @JsonSetter(nulls = Nulls.SKIP) private Long tokenValue = 0L; @Getter @Setter private String abi = ""; @Getter @Setter + @JsonSetter(nulls = Nulls.SKIP) private Long consumeUserResourcePercent = 0L; @Getter @Setter + @JsonSetter(nulls = Nulls.SKIP) private Long originEnergyLimit = 0L; @Getter @Setter @@ -84,9 +97,11 @@ public class BuildArguments { @Getter @Setter + @JsonSetter(nulls = Nulls.SKIP) private Integer permissionId = 0; @Getter @Setter + @JsonSetter(nulls = Nulls.SKIP) private String extraData = ""; @Getter @@ -205,4 +220,4 @@ private static boolean calldataEquals(String a, String b) { return Arrays.equals(ByteArray.fromHexString(a), ByteArray.fromHexString(b)); } -} \ No newline at end of file +} diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java index 1715636a2a4..8219b108c70 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java @@ -5,6 +5,8 @@ import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseQuantityValue; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.requireValidHex; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; import com.google.protobuf.ByteString; import lombok.AllArgsConstructor; import lombok.Getter; @@ -20,6 +22,13 @@ import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.contract.SmartContractOuterClass.SmartContract; +/** + * Arguments for EVM call-style JSON-RPC methods. + * + *

Fields annotated with {@link JsonSetter} retain their defaults when an explicit JSON + * {@code null} is deserialized. This protection applies to Jackson input only; direct setter calls + * may still assign {@code null} and callers must preserve the DTO invariants. + */ @NoArgsConstructor @AllArgsConstructor @ToString @@ -27,6 +36,7 @@ public class CallArguments { @Getter @Setter + @JsonSetter(nulls = Nulls.SKIP) private String from = "0x0000000000000000000000000000000000000000"; @Getter @Setter @@ -108,4 +118,4 @@ public ContractType getContractType(Wallet wallet) throws JsonRpcInvalidRequestE public long parseValue() throws JsonRpcInvalidParamsException { return parseQuantityValue(value); } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcArgumentNullBindingTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcArgumentNullBindingTest.java new file mode 100644 index 00000000000..f035f0a3949 --- /dev/null +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcArgumentNullBindingTest.java @@ -0,0 +1,54 @@ +package org.tron.core.jsonrpc; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Assert; +import org.junit.Test; +import org.tron.core.services.jsonrpc.types.BuildArguments; +import org.tron.core.services.jsonrpc.types.CallArguments; + +public class JsonRpcArgumentNullBindingTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + public void testBuildArgumentsExplicitNullMatchesOmittedDefaults() throws Exception { + BuildArguments omitted = MAPPER.readValue("{}", BuildArguments.class); + String[] optionalFields = { + "tokenId", + "tokenValue", + "consumeUserResourcePercent", + "originEnergyLimit", + "permissionId", + "extraData" + }; + + for (String field : optionalFields) { + ObjectNode input = MAPPER.createObjectNode(); + input.putNull(field); + BuildArguments explicitNull = MAPPER.treeToValue(input, BuildArguments.class); + + assertBuildDefaultsEqual(omitted, explicitNull); + } + } + + @Test + public void testCallArgumentsNullFromMatchesOmittedDefault() throws Exception { + CallArguments omitted = MAPPER.readValue("{}", CallArguments.class); + CallArguments explicitNull = MAPPER.readValue("{\"from\":null}", CallArguments.class); + + Assert.assertEquals(omitted.getFrom(), explicitNull.getFrom()); + Assert.assertEquals( + "0x0000000000000000000000000000000000000000", explicitNull.getFrom()); + } + + private static void assertBuildDefaultsEqual(BuildArguments expected, BuildArguments actual) { + Assert.assertEquals(expected.getTokenId(), actual.getTokenId()); + Assert.assertEquals(expected.getTokenValue(), actual.getTokenValue()); + Assert.assertEquals(expected.getConsumeUserResourcePercent(), + actual.getConsumeUserResourcePercent()); + Assert.assertEquals(expected.getOriginEnergyLimit(), actual.getOriginEnergyLimit()); + Assert.assertEquals(expected.getPermissionId(), actual.getPermissionId()); + Assert.assertEquals(expected.getExtraData(), actual.getExtraData()); + } +} diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java index 2ab455fa580..b64e3009a79 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java @@ -18,6 +18,7 @@ import org.tron.core.capsule.TransactionCapsule; import org.tron.core.db.Manager; import org.tron.core.exception.jsonrpc.JsonRpcInternalException; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.services.NodeInfoService; import org.tron.core.services.jsonrpc.TronJsonRpcImpl; import org.tron.core.services.jsonrpc.types.CallArguments; @@ -51,6 +52,34 @@ public void tearDown() throws Exception { CommonParameter.getInstance().setEstimateEnergy(originalEstimateEnergy); } + @Test + public void testNullCallArgumentsRejectedAsInvalidParams() throws Exception { + mockRpc = new TronJsonRpcImpl(mock(NodeInfoService.class), mock(Wallet.class)); + + JsonRpcInvalidParamsException callError = assertThrows(JsonRpcInvalidParamsException.class, + () -> mockRpc.getCall(null, "latest")); + Assert.assertEquals("invalid params", callError.getMessage()); + + JsonRpcInvalidParamsException estimateError = assertThrows(JsonRpcInvalidParamsException.class, + () -> mockRpc.estimateGas(null)); + Assert.assertEquals("invalid params", estimateError.getMessage()); + } + + @Test + public void testEstimateGasKeepsInvalidQuantityAndDataAsInvalidParams() throws Exception { + mockRpc = new TronJsonRpcImpl(mock(NodeInfoService.class), mock(Wallet.class)); + + CallArguments invalidQuantity = newCallArgs(); + invalidQuantity.setValue("0xzz"); + assertThrows(JsonRpcInvalidParamsException.class, + () -> mockRpc.estimateGas(invalidQuantity)); + + CallArguments invalidData = newCallArgs(); + invalidData.setData("0xzz"); + assertThrows(JsonRpcInvalidParamsException.class, + () -> mockRpc.estimateGas(invalidData)); + } + @Test public void testGetCallAppendsRevertReason() throws Exception { byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX); diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java index 49f875f3823..74a1ab18f29 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java @@ -229,14 +229,16 @@ public void testLogFilter() { Assert.assertTrue(e.getMessage().contains("invalid topic")); } - // not empty topic and null cannot be in same level - try { - new LogFilter(new FilterRequest(null, null, null, new String[][] { - {"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", null}, - }, null)); - } catch (JsonRpcInvalidParamsException e) { - Assert.assertTrue(e.getMessage().contains("invalid topic")); - } + // null is invalid inside an OR-list. Use ArrayList to match Jackson's nested-array binding. + ArrayList invalidOrTopics = new ArrayList<>(); + invalidOrTopics.add( + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"); + invalidOrTopics.add(null); + JsonRpcInvalidParamsException invalidOrTopic = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilter(new FilterRequest( + null, null, null, new Object[] {invalidOrTopics}, null))); + Assert.assertEquals("invalid topic(s): null", invalidOrTopic.getMessage()); // non-string element in address array -> -32602, not a leaked ClassCastException JsonRpcInvalidParamsException badAddrElement = Assert.assertThrows( @@ -287,6 +289,14 @@ public void testLogFilter() { } } + @Test + public void testNullLogFilterRequestRejectedAsInvalidParams() { + JsonRpcInvalidParamsException error = Assert.assertThrows( + JsonRpcInvalidParamsException.class, () -> new LogFilter(null)); + + Assert.assertEquals("invalid filter request", error.getMessage()); + } + @Test public void testLogFilterAddressSizeLimit() { // Two valid 20-byte addresses (40 hex chars with 0x prefix) diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java index e8d14ace060..cb81b84c3ca 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -7,15 +7,26 @@ import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockTag; import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.protobuf.ByteString; +import com.googlecode.jsonrpc4j.JsonRpcServer; +import com.googlecode.jsonrpc4j.ProxyUtil; import io.prometheus.client.CollectorRegistry; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import javax.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.http.client.methods.CloseableHttpResponse; @@ -35,6 +46,7 @@ import org.tron.common.application.HttpService; import org.tron.common.parameter.CommonParameter; import org.tron.common.prometheus.Metrics; +import org.tron.common.runtime.RuntimeImpl; import org.tron.common.utils.ByteArray; import org.tron.common.utils.PublicMethod; import org.tron.common.utils.Sha256Hash; @@ -46,25 +58,39 @@ import org.tron.core.capsule.TransactionRetCapsule; import org.tron.core.capsule.utils.BlockUtil; import org.tron.core.config.args.Args; +import org.tron.core.db.TransactionTrace; +import org.tron.core.db2.ISession; +import org.tron.core.exception.ContractValidateException; +import org.tron.core.exception.ItemNotFoundException; import org.tron.core.exception.jsonrpc.JsonRpcInternalException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.services.NodeInfoService; +import org.tron.core.services.http.Util; import org.tron.core.services.interfaceJsonRpcOnPBFT.JsonRpcServiceOnPBFT; import org.tron.core.services.interfaceJsonRpcOnSolidity.JsonRpcServiceOnSolidity; import org.tron.core.services.jsonrpc.FullNodeJsonRpcHttpService; +import org.tron.core.services.jsonrpc.JsonRpcErrorResolver; +import org.tron.core.services.jsonrpc.TronJsonRpc; import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; import org.tron.core.services.jsonrpc.TronJsonRpc.LogFilterElement; import org.tron.core.services.jsonrpc.TronJsonRpcImpl; +import org.tron.core.services.jsonrpc.filters.BlockFilterAndResult; import org.tron.core.services.jsonrpc.filters.LogFilterWrapper; import org.tron.core.services.jsonrpc.types.BlockResult; import org.tron.core.services.jsonrpc.types.BuildArguments; +import org.tron.core.services.jsonrpc.types.CallArguments; import org.tron.core.services.jsonrpc.types.TransactionReceipt; import org.tron.core.services.jsonrpc.types.TransactionResult; +import org.tron.core.store.StoreFactory; +import org.tron.core.vm.config.ConfigLoader; +import org.tron.core.vm.config.VMConfig; import org.tron.json.JSON; import org.tron.json.JSONArray; import org.tron.json.JSONObject; import org.tron.protos.Protocol; +import org.tron.protos.Protocol.Transaction; import org.tron.protos.Protocol.Transaction.Contract.ContractType; +import org.tron.protos.Protocol.Transaction.Result.contractResult; import org.tron.protos.contract.BalanceContract.TransferContract; @@ -241,6 +267,11 @@ public void testWeb3Sha3() { @Test public void testGetBlockTransactionCountByHash() { + JsonRpcInvalidParamsException nullError = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> tronJsonRpc.ethGetBlockTransactionCountByHash(null)); + Assert.assertEquals("invalid hash value", nullError.getMessage()); + try { tronJsonRpc.ethGetBlockTransactionCountByHash("0x111111"); } catch (Exception e) { @@ -268,6 +299,11 @@ public void testGetBlockTransactionCountByHash() { @Test public void testGetBlockTransactionCountByNumber() { + JsonRpcInvalidParamsException nullError = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> tronJsonRpc.ethGetBlockTransactionCountByNumber(null)); + Assert.assertEquals("invalid block number", nullError.getMessage()); + String result = ""; try { result = tronJsonRpc.ethGetBlockTransactionCountByNumber("0x0"); @@ -322,6 +358,19 @@ public void testGetBlockByHash() { Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getNum()), blockResult.getNumber()); Assert.assertEquals(blockCapsule1.getTransactions().size(), blockResult.getTransactions().length); + + try { + blockResult = tronJsonRpc.ethGetBlockByHash( + Hex.toHexString(blockCapsule1.getBlockId().getBytes()), null); + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + Assert.assertTrue(blockResult.getTransactions()[0] instanceof String); + + JsonRpcInvalidParamsException nullHashError = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> tronJsonRpc.ethGetBlockByHash(null, true)); + Assert.assertEquals("invalid hash value", nullHashError.getMessage()); } @Test @@ -336,6 +385,18 @@ public void testGetBlockByNumber() { Assert.fail(); } Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getNum()), blockResult.getNumber()); + + try { + blockResult = tronJsonRpc.ethGetBlockByNumber("latest", null); + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + Assert.assertTrue(blockResult.getTransactions()[0] instanceof String); + + JsonRpcInvalidParamsException nullNumberError = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> tronJsonRpc.ethGetBlockByNumber(null, true)); + Assert.assertEquals("invalid block number", nullNumberError.getMessage()); Assert.assertEquals(blockCapsule1.getTransactions().size(), blockResult.getTransactions().length); Assert.assertEquals("0x0000000000000000", blockResult.getNonce()); @@ -392,6 +453,10 @@ public void testGetBlockByNumber() { @Test public void testGetTransactionByHash() { + JsonRpcInvalidParamsException nullError = Assert.assertThrows( + JsonRpcInvalidParamsException.class, () -> tronJsonRpc.getTransactionByHash(null)); + Assert.assertEquals("invalid hash value", nullError.getMessage()); + TransactionResult transactionResult = null; try { transactionResult = tronJsonRpc.getTransactionByHash( @@ -649,21 +714,346 @@ public void testGetABIOfSmartContract() { @Test public void testGetCall() { + CallArguments validArgs = newValidCallArguments(); Exception e1 = Assert.assertThrows(Exception.class, - () -> tronJsonRpc.getCall(null, "earliest")); + () -> tronJsonRpc.getCall(validArgs, "earliest")); Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e1.getMessage()); Exception e2 = Assert.assertThrows(Exception.class, - () -> tronJsonRpc.getCall(null, "pending")); + () -> tronJsonRpc.getCall(validArgs, "pending")); Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e2.getMessage()); Exception e3 = Assert.assertThrows(Exception.class, - () -> tronJsonRpc.getCall(null, "finalized")); + () -> tronJsonRpc.getCall(validArgs, "finalized")); Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e3.getMessage()); Exception e4 = Assert.assertThrows(Exception.class, - () -> tronJsonRpc.getCall(null, "safe")); + () -> tronJsonRpc.getCall(validArgs, "safe")); Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e4.getMessage()); + + JsonRpcInvalidParamsException nullArgsError = Assert.assertThrows( + JsonRpcInvalidParamsException.class, () -> tronJsonRpc.getCall(null, "latest")); + Assert.assertEquals("invalid params", nullArgsError.getMessage()); + + JsonRpcInvalidParamsException nullArgsWithUnsupportedTagError = Assert.assertThrows( + JsonRpcInvalidParamsException.class, () -> tronJsonRpc.getCall(null, "earliest")); + Assert.assertEquals("invalid params", nullArgsWithUnsupportedTagError.getMessage()); + + JsonRpcInvalidParamsException doubleNullError = Assert.assertThrows( + JsonRpcInvalidParamsException.class, () -> tronJsonRpc.getCall(null, null)); + Assert.assertEquals("invalid params", doubleNullError.getMessage()); + } + + @Test + public void testTopLevelNullParametersAtServiceLayer() throws Exception { + JsonRpcInvalidParamsException estimateError = Assert.assertThrows( + JsonRpcInvalidParamsException.class, () -> tronJsonRpc.estimateGas(null)); + Assert.assertEquals("invalid params", estimateError.getMessage()); + + JsonRpcInvalidParamsException buildError = Assert.assertThrows( + JsonRpcInvalidParamsException.class, () -> tronJsonRpc.buildTransaction(null)); + Assert.assertEquals("invalid params", buildError.getMessage()); + + JsonRpcInvalidParamsException logsError = Assert.assertThrows( + JsonRpcInvalidParamsException.class, () -> tronJsonRpc.getLogs(null)); + Assert.assertEquals("invalid filter request", logsError.getMessage()); + + JsonRpcInvalidParamsException newFilterError = Assert.assertThrows( + JsonRpcInvalidParamsException.class, () -> tronJsonRpc.newFilter(null)); + Assert.assertEquals("invalid filter request", newFilterError.getMessage()); + + Assert.assertFalse(tronJsonRpc.uninstallFilter(null)); + + ItemNotFoundException changesError = Assert.assertThrows(ItemNotFoundException.class, + () -> tronJsonRpc.getFilterChanges(null)); + Assert.assertEquals("filter not found", changesError.getMessage()); + + ItemNotFoundException filterLogsError = Assert.assertThrows(ItemNotFoundException.class, + () -> tronJsonRpc.getFilterLogs(null)); + Assert.assertEquals("filter not found", filterLogsError.getMessage()); + + Assert.assertFalse(tronJsonRpc.uninstallFilter("0xdeadbeef")); + + JsonRpcInvalidParamsException receiptError = Assert.assertThrows( + JsonRpcInvalidParamsException.class, () -> tronJsonRpc.getTransactionReceipt(null)); + Assert.assertEquals("invalid hash value", receiptError.getMessage()); + } + + @Test + public void testNullParameterWireContract() throws Exception { + JsonRpcServer server = newJsonRpcServer(); + + assertMappedError(handleJsonRpc(server, "eth_getLogs", jsonParams((Object) null)), + -32602, "invalid filter request"); + assertMappedError(handleJsonRpc(server, "eth_newFilter", jsonParams((Object) null)), + -32602, "invalid filter request"); + assertMappedError(handleJsonRpc(server, "eth_estimateGas", jsonParams((Object) null)), + -32602, "invalid params"); + assertMappedError(handleJsonRpc(server, "buildTransaction", jsonParams((Object) null)), + -32602, "invalid params"); + + assertFrameworkError(handleJsonRpc(server, "eth_call", jsonParams((Object) null)), + -32602, "method parameters invalid"); + assertMappedError(handleJsonRpc(server, "eth_call", jsonParams(null, null)), + -32602, "invalid params"); + assertMappedError(handleJsonRpc(server, "eth_call", jsonParams(null, "latest")), + -32602, "invalid params"); + + JSONObject uninstallNull = handleJsonRpc( + server, "eth_uninstallFilter", jsonParams((Object) null)); + assertBooleanResult(uninstallNull, false); + + assertBooleanResult(handleJsonRpc(server, "eth_uninstallFilter", + jsonParams("0xdeadbeef")), false); + assertMappedError(handleJsonRpc(server, "eth_getFilterChanges", jsonParams((Object) null)), + -32000, "filter not found"); + assertMappedError(handleJsonRpc(server, "eth_getFilterLogs", jsonParams((Object) null)), + -32000, "filter not found"); + + String blockHash = ByteArray.toJsonHex(blockCapsule1.getBlockId().getBytes()); + JSONObject blockByHash = handleJsonRpc( + server, "eth_getBlockByHash", jsonParams(blockHash, null)); + assertHashOnlyTransactions(blockByHash); + JSONObject blockByNumber = handleJsonRpc( + server, "eth_getBlockByNumber", jsonParams("latest", null)); + assertHashOnlyTransactions(blockByNumber); + + assertMappedError(handleJsonRpc(server, "eth_getBlockByHash", jsonParams(null, true)), + -32602, "invalid hash value"); + assertMappedError(handleJsonRpc(server, "eth_getBlockByNumber", jsonParams(null, true)), + -32602, "invalid block number"); + + String address = "0xabd4b9367799eaa3197fecb144eb71de1e049abc"; + assertMappedError(handleJsonRpc(server, "eth_getStorageAt", + jsonParams(address, null, "latest")), -32602, "invalid storage key value"); + assertMappedError(handleJsonRpc(server, "eth_getBlockTransactionCountByNumber", + jsonParams((Object) null)), -32602, "invalid block number"); + + assertMappedError(handleJsonRpc(server, "eth_getBlockReceipts", jsonParams((Object) null)), + -32602, "invalid block number"); + assertMappedError(handleJsonRpc(server, "eth_getTransactionByHash", jsonParams((Object) null)), + -32602, "invalid hash value"); + assertMappedError(handleJsonRpc(server, "eth_getTransactionReceipt", + jsonParams((Object) null)), -32602, "invalid hash value"); + assertMappedError(handleJsonRpc(server, "eth_getBlockTransactionCountByHash", + jsonParams((Object) null)), -32602, "invalid hash value"); + } + + @Test + public void testUninstallFilterLookupMisses() throws Exception { + JsonRpcServer server = newJsonRpcServer(); + for (String filterId : new String[] {null, "0xdeadbeef", "", "0x", "not-hex", "0xz"}) { + Assert.assertFalse("filter ID: " + filterId, tronJsonRpc.uninstallFilter(filterId)); + assertBooleanResult(handleJsonRpc(server, "eth_uninstallFilter", jsonParams(filterId)), + false); + } + } + + @Test + public void testUninstallInstalledFilters() throws Exception { + String eventId = tronJsonRpc.newFilter(new FilterRequest()); + String blockId = tronJsonRpc.newBlockFilter(); + String eventKey = ByteArray.fromHex(eventId); + String blockKey = ByteArray.fromHex(blockId); + + Assert.assertTrue(tronJsonRpc.getEventFilter2ResultFull().containsKey(eventKey)); + Assert.assertTrue(tronJsonRpc.getBlockFilter2ResultFull().containsKey(blockKey)); + Assert.assertTrue(tronJsonRpc.uninstallFilter(eventId)); + Assert.assertFalse(tronJsonRpc.getEventFilter2ResultFull().containsKey(eventKey)); + Assert.assertTrue(tronJsonRpc.getBlockFilter2ResultFull().containsKey(blockKey)); + Assert.assertFalse(tronJsonRpc.uninstallFilter(eventId)); + + Assert.assertTrue(tronJsonRpc.uninstallFilter(blockId)); + Assert.assertFalse(tronJsonRpc.getBlockFilter2ResultFull().containsKey(blockKey)); + Assert.assertFalse(tronJsonRpc.uninstallFilter(blockId)); + } + + @Test + public void testUninstallInstalledFiltersOverWire() throws Exception { + JsonRpcServer server = newJsonRpcServer(); + for (String method : new String[] {"eth_newFilter", "eth_newBlockFilter"}) { + boolean eventFilter = "eth_newFilter".equals(method); + JSONObject created = handleJsonRpc(server, method, + eventFilter ? jsonParams(new JsonObject()) : jsonParams()); + Assert.assertFalse(created.toJSONString(), created.containsKey("error")); + Assert.assertTrue(created.toJSONString(), created.get("result") instanceof String); + String filterId = created.getString("result"); + String key = ByteArray.fromHex(filterId); + Map filters = eventFilter + ? tronJsonRpc.getEventFilter2ResultFull() : tronJsonRpc.getBlockFilter2ResultFull(); + Assert.assertTrue(filters.containsKey(key)); + + assertBooleanResult(handleJsonRpc(server, "eth_uninstallFilter", jsonParams(filterId)), true); + Assert.assertFalse(filters.containsKey(key)); + assertBooleanResult(handleJsonRpc(server, "eth_uninstallFilter", jsonParams(filterId)), + false); + } + } + + @Test + public void testFilterLookupsStillRejectUnknownIds() throws Exception { + String filterId = "0xdeadbeef"; + ItemNotFoundException changesError = Assert.assertThrows(ItemNotFoundException.class, + () -> tronJsonRpc.getFilterChanges(filterId)); + Assert.assertEquals("filter not found", changesError.getMessage()); + ItemNotFoundException logsError = Assert.assertThrows(ItemNotFoundException.class, + () -> tronJsonRpc.getFilterLogs(filterId)); + Assert.assertEquals("filter not found", logsError.getMessage()); + + JsonRpcServer server = newJsonRpcServer(); + assertMappedError(handleJsonRpc(server, "eth_getFilterChanges", jsonParams(filterId)), + -32000, "filter not found"); + assertMappedError(handleJsonRpc(server, "eth_getFilterLogs", jsonParams(filterId)), + -32000, "filter not found"); + } + + @Test + public void testUninstallFilterPreservesIdNormalization() throws Exception { + for (String filterId : new String[] {"0xa", "a", "0x0a", "0a"}) { + tronJsonRpc.getBlockFilter2ResultFull().put("0a", new BlockFilterAndResult()); + Assert.assertTrue("filter ID: " + filterId, tronJsonRpc.uninstallFilter(filterId)); + Assert.assertFalse(tronJsonRpc.getBlockFilter2ResultFull().containsKey("0a")); + } + } + + @Test(timeout = 15000) + public void testConcurrentUninstallRemovesFilterOnce() throws Exception { + String filterId = tronJsonRpc.newBlockFilter(); + CountDownLatch ready = new CountDownLatch(2); + CountDownLatch start = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(2); + // Keep cleanup failures suppressed when the test itself has already failed. + try (AutoCloseable cleanup = () -> { + start.countDown(); + executor.shutdownNow(); + try { + Assert.assertTrue("workers must terminate", executor.awaitTermination(5, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw e; + } + }) { + List> results = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + results.add(executor.submit(() -> { + ready.countDown(); + if (!start.await(5, TimeUnit.SECONDS)) { + throw new IllegalStateException("timed out waiting to uninstall filter"); + } + return tronJsonRpc.uninstallFilter(filterId); + })); + } + Assert.assertTrue("workers must be ready", ready.await(5, TimeUnit.SECONDS)); + start.countDown(); + Assert.assertNotEquals(results.get(0).get(5, TimeUnit.SECONDS), + results.get(1).get(5, TimeUnit.SECONDS)); + Assert.assertFalse(tronJsonRpc.getBlockFilter2ResultFull() + .containsKey(ByteArray.fromHex(filterId))); + } + } + + @Test + public void testFilterFieldNullSemanticsOverWire() throws Exception { + JsonRpcServer server = newJsonRpcServer(); + String topic = + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; + + JsonObject nullAddress = new JsonObject(); + nullAddress.add("address", JsonNull.INSTANCE); + assertSuccessfulLogsResult(handleJsonRpc(server, "eth_getLogs", jsonParams(nullAddress))); + + JsonObject nullTopics = new JsonObject(); + nullTopics.add("topics", JsonNull.INSTANCE); + assertSuccessfulLogsResult(handleJsonRpc(server, "eth_getLogs", jsonParams(nullTopics))); + + JsonArray positionalTopics = new JsonArray(); + positionalTopics.add(topic); + positionalTopics.add(JsonNull.INSTANCE); + JsonObject positionalWildcard = new JsonObject(); + positionalWildcard.add("topics", positionalTopics); + assertSuccessfulLogsResult(handleJsonRpc( + server, "eth_getLogs", jsonParams(positionalWildcard))); + + JsonArray invalidOrList = new JsonArray(); + invalidOrList.add(topic); + invalidOrList.add(JsonNull.INSTANCE); + JsonArray nestedTopics = new JsonArray(); + nestedTopics.add(invalidOrList); + JsonObject invalidOrWildcard = new JsonObject(); + invalidOrWildcard.add("topics", nestedTopics); + assertMappedError(handleJsonRpc(server, "eth_getLogs", jsonParams(invalidOrWildcard)), + -32602, "invalid topic(s): null"); + } + + @Test + public void testOptionalDtoNullFieldsOverWire() throws Exception { + JsonRpcServer server = newJsonRpcServer(); + + JSONObject omittedTransfer = handleJsonRpc( + server, "buildTransaction", jsonParams(newTransferBuildArguments())); + assertSuccessfulResponse(omittedTransfer); + String[] optionalBuildFields = { + "tokenId", + "tokenValue", + "consumeUserResourcePercent", + "originEnergyLimit", + "permissionId", + "extraData" + }; + for (String field : optionalBuildFields) { + JsonObject explicitNullArgs = newTransferBuildArguments(); + explicitNullArgs.add(field, JsonNull.INSTANCE); + JSONObject explicitNull = handleJsonRpc( + server, "buildTransaction", jsonParams(explicitNullArgs)); + assertEquivalentBuiltTransaction(omittedTransfer, explicitNull); + } + + JsonObject omittedConsume = newCreateBuildArguments(); + omittedConsume.addProperty("originEnergyLimit", 10_000_000L); + JsonObject nullConsume = newCreateBuildArguments(); + nullConsume.add("consumeUserResourcePercent", JsonNull.INSTANCE); + nullConsume.addProperty("originEnergyLimit", 10_000_000L); + JSONObject omittedConsumeResponse = handleJsonRpc( + server, "buildTransaction", jsonParams(omittedConsume)); + JSONObject nullConsumeResponse = handleJsonRpc( + server, "buildTransaction", jsonParams(nullConsume)); + assertEquivalentCreatedContract(omittedConsumeResponse, nullConsumeResponse); + Assert.assertEquals(0L, getNewContract(nullConsumeResponse) + .getLongValue("consume_user_resource_percent")); + Assert.assertEquals(10_000_000L, getNewContract(nullConsumeResponse) + .getLongValue("origin_energy_limit")); + assertSuccessfulVmExecution(omittedConsumeResponse); + assertSuccessfulVmExecution(nullConsumeResponse); + + JsonObject omittedOrigin = newCreateBuildArguments(); + omittedOrigin.addProperty("consumeUserResourcePercent", 10L); + JsonObject nullOrigin = newCreateBuildArguments(); + nullOrigin.addProperty("consumeUserResourcePercent", 10L); + nullOrigin.add("originEnergyLimit", JsonNull.INSTANCE); + JSONObject omittedOriginResponse = handleJsonRpc( + server, "buildTransaction", jsonParams(omittedOrigin)); + JSONObject nullOriginResponse = handleJsonRpc( + server, "buildTransaction", jsonParams(nullOrigin)); + assertEquivalentCreatedContract(omittedOriginResponse, nullOriginResponse); + Assert.assertEquals(0L, + getNewContract(nullOriginResponse).getLongValue("origin_energy_limit")); + ContractValidateException omittedOriginError = Assert.assertThrows( + ContractValidateException.class, + () -> executeBuiltTransaction(omittedOriginResponse)); + ContractValidateException nullOriginError = Assert.assertThrows( + ContractValidateException.class, + () -> executeBuiltTransaction(nullOriginResponse)); + Assert.assertEquals("The originEnergyLimit must be > 0", omittedOriginError.getMessage()); + Assert.assertEquals(omittedOriginError.getMessage(), nullOriginError.getMessage()); + + JsonObject omittedFrom = newEstimateGasArguments(); + JsonObject nullFrom = newEstimateGasArguments(); + nullFrom.add("from", JsonNull.INSTANCE); + JSONObject omittedFromResponse = handleJsonRpc( + server, "eth_estimateGas", jsonParams(omittedFrom)); + JSONObject nullFromResponse = handleJsonRpc( + server, "eth_estimateGas", jsonParams(nullFrom)); + Assert.assertEquals(omittedFromResponse.toJSONString(), nullFromResponse.toJSONString()); } @Test @@ -773,35 +1163,36 @@ public void testGetTransactionByBlockNumberAndIndex() { */ @Test public void testGetCallWithBlockObject() { + CallArguments validArgs = newValidCallArguments(); // neither HashMap nor String -> invalid json request Exception nonMapEx = Assert.assertThrows(Exception.class, - () -> tronJsonRpc.getCall(null, new Object())); + () -> tronJsonRpc.getCall(validArgs, new Object())); Assert.assertEquals("invalid json request", nonMapEx.getMessage()); // HashMap without blockNumber/blockHash keys -> invalid json request Exception emptyMapEx = Assert.assertThrows(Exception.class, - () -> tronJsonRpc.getCall(null, new HashMap())); + () -> tronJsonRpc.getCall(validArgs, new HashMap())); Assert.assertEquals("invalid json request", emptyMapEx.getMessage()); // blockNumber with malformed hex -> invalid block number HashMap badHexParams = new HashMap<>(); badHexParams.put("blockNumber", "xxx"); Exception badHexEx = Assert.assertThrows(Exception.class, - () -> tronJsonRpc.getCall(null, badHexParams)); + () -> tronJsonRpc.getCall(validArgs, badHexParams)); Assert.assertEquals("invalid block number", badHexEx.getMessage()); // blockNumber overflows long -> invalid block number (longValueExact) HashMap overflowParams = new HashMap<>(); overflowParams.put("blockNumber", "0x10000000000000000"); Exception overflowEx = Assert.assertThrows(Exception.class, - () -> tronJsonRpc.getCall(null, overflowParams)); + () -> tronJsonRpc.getCall(validArgs, overflowParams)); Assert.assertEquals("invalid block number", overflowEx.getMessage()); // blockNumber points to a non-existent block -> header not found HashMap missingNumParams = new HashMap<>(); missingNumParams.put("blockNumber", "0x1"); Exception missingNumEx = Assert.assertThrows(Exception.class, - () -> tronJsonRpc.getCall(null, missingNumParams)); + () -> tronJsonRpc.getCall(validArgs, missingNumParams)); Assert.assertEquals("header not found", missingNumEx.getMessage()); // blockHash of an unknown block -> header for hash not found @@ -809,7 +1200,7 @@ public void testGetCallWithBlockObject() { missingHashParams.put("blockHash", "0x1111111111111111111111111111111111111111111111111111111111111111"); Exception missingHashEx = Assert.assertThrows(Exception.class, - () -> tronJsonRpc.getCall(null, missingHashParams)); + () -> tronJsonRpc.getCall(validArgs, missingHashParams)); Assert.assertEquals("header for hash not found", missingHashEx.getMessage()); } @@ -1632,4 +2023,193 @@ public void testJsonRpcSizeLimitIntegration() { fullNodeJsonRpcHttpService.stop(); } } + + private static JsonRpcServer newJsonRpcServer() { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Object compositeService = ProxyUtil.createCompositeServiceProxy( + classLoader, + new Object[] {tronJsonRpc}, + new Class[] {TronJsonRpc.class}, + true); + JsonRpcServer server = new JsonRpcServer(compositeService); + server.setErrorResolver(JsonRpcErrorResolver.INSTANCE); + server.setShouldLogInvocationErrors(false); + return server; + } + + private static JSONObject handleJsonRpc(JsonRpcServer server, String method, JsonArray params) + throws Exception { + JsonObject request = new JsonObject(); + request.addProperty("jsonrpc", "2.0"); + request.addProperty("method", method); + request.add("params", params); + request.addProperty("id", 1); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + server.handleRequest( + new ByteArrayInputStream(request.toString().getBytes("UTF-8")), output); + String responseBody = output.toString("UTF-8"); + + Assert.assertFalse(responseBody, responseBody.contains("java.")); + Assert.assertFalse(responseBody, responseBody.contains("NullPointerException")); + JSONObject response = JSON.parseObject(responseBody); + Assert.assertEquals("2.0", response.getString("jsonrpc")); + Assert.assertEquals(1, response.getIntValue("id")); + return response; + } + + private static JsonArray jsonParams(Object... values) { + JsonArray params = new JsonArray(); + for (Object value : values) { + if (value == null) { + params.add(JsonNull.INSTANCE); + } else if (value instanceof JsonElement) { + params.add((JsonElement) value); + } else if (value instanceof String) { + params.add((String) value); + } else if (value instanceof Boolean) { + params.add((Boolean) value); + } else if (value instanceof Number) { + params.add((Number) value); + } else { + throw new IllegalArgumentException("unsupported JSON parameter type: " + value.getClass()); + } + } + return params; + } + + private static void assertMappedError(JSONObject response, int code, String message) { + JSONObject error = response.getJSONObject("error"); + Assert.assertNotNull(response.toJSONString(), error); + Assert.assertEquals(code, error.getIntValue("code")); + Assert.assertEquals(message, error.getString("message")); + Assert.assertTrue(error.toJSONString(), error.containsKey("data")); + Assert.assertEquals("{}", error.get("data")); + } + + private static void assertFrameworkError(JSONObject response, int code, String message) { + JSONObject error = response.getJSONObject("error"); + Assert.assertNotNull(response.toJSONString(), error); + Assert.assertEquals(code, error.getIntValue("code")); + Assert.assertEquals(message, error.getString("message")); + Assert.assertFalse(error.toJSONString(), error.containsKey("data")); + } + + private static void assertSuccessfulLogsResult(JSONObject response) { + Assert.assertFalse(response.toJSONString(), response.containsKey("error")); + Assert.assertNotNull(response.toJSONString(), response.getJSONArray("result")); + } + + private static void assertBooleanResult(JSONObject response, boolean expected) { + Assert.assertEquals(response.toJSONString(), Boolean.valueOf(expected), response.get("result")); + Assert.assertFalse(response.toJSONString(), response.containsKey("error")); + } + + private static void assertHashOnlyTransactions(JSONObject response) { + JSONObject result = response.getJSONObject("result"); + Assert.assertNotNull(response.toJSONString(), result); + JSONArray transactions = result.getJSONArray("transactions"); + Assert.assertTrue(transactions.size() > 0); + Assert.assertTrue(transactions.get(0) instanceof String); + } + + private static JsonObject newTransferBuildArguments() { + JsonObject args = new JsonObject(); + args.addProperty("from", "0xabd4b9367799eaa3197fecb144eb71de1e049abc"); + args.addProperty("to", "0x548794500882809695a8a687866e76d4271a1abc"); + args.addProperty("value", "0x1f4"); + return args; + } + + private static JsonObject newCreateBuildArguments() { + JsonObject args = new JsonObject(); + args.addProperty("from", "0xabd4b9367799eaa3197fecb144eb71de1e049abc"); + args.addProperty("data", "60006000f3"); + args.addProperty("gas", "0xf4240"); + args.addProperty("abi", "[]"); + return args; + } + + private static JsonObject newEstimateGasArguments() { + JsonObject args = new JsonObject(); + args.addProperty("to", "0x548794500882809695a8a687866e76d4271a1abc"); + args.addProperty("value", "0x1"); + return args; + } + + private static void assertSuccessfulResponse(JSONObject response) { + Assert.assertFalse(response.toJSONString(), response.containsKey("error")); + Assert.assertNotNull(response.toJSONString(), response.getJSONObject("result")); + } + + private void assertSuccessfulVmExecution(JSONObject response) throws Exception { + TransactionTrace trace = executeBuiltTransaction(response); + Assert.assertEquals(contractResult.SUCCESS, trace.getRuntimeResult().getResultCode()); + Assert.assertFalse(trace.getRuntimeResult().isRevert()); + Assert.assertNull(trace.getRuntimeError()); + } + + private TransactionTrace executeBuiltTransaction(JSONObject response) throws Exception { + assertSuccessfulResponse(response); + JSONObject transactionJson = response.getJSONObject("result").getJSONObject("transaction"); + Transaction transaction = Util.packTransaction(transactionJson.toJSONString(), false); + Assert.assertNotNull(transactionJson.toJSONString(), transaction); + + boolean loaderWasDisabled = ConfigLoader.disable; + boolean energyLimitForkWasEnabled = VMConfig.getEnergyLimitHardFork(); + try { + ConfigLoader.disable = true; + VMConfig.initVmHardFork(true); + try (ISession ignored = dbManager.getRevokingStore().buildSession()) { + dbManager.getDynamicPropertiesStore().saveMaxCpuTimeOfOneTx(5_000L); + TransactionTrace trace = new TransactionTrace( + new TransactionCapsule(transaction), StoreFactory.getInstance(), new RuntimeImpl()); + trace.init(null); + trace.exec(); + return trace; + } + } finally { + ConfigLoader.disable = loaderWasDisabled; + VMConfig.initVmHardFork(energyLimitForkWasEnabled); + } + } + + private static void assertEquivalentBuiltTransaction(JSONObject expected, JSONObject actual) { + assertSuccessfulResponse(expected); + assertSuccessfulResponse(actual); + JSONObject expectedRawData = expected.getJSONObject("result") + .getJSONObject("transaction").getJSONObject("raw_data"); + JSONObject actualRawData = actual.getJSONObject("result") + .getJSONObject("transaction").getJSONObject("raw_data"); + Assert.assertEquals(expectedRawData.getJSONArray("contract").toJSONString(), + actualRawData.getJSONArray("contract").toJSONString()); + Assert.assertEquals(expectedRawData.get("data"), actualRawData.get("data")); + } + + private static void assertEquivalentCreatedContract(JSONObject expected, JSONObject actual) { + assertSuccessfulResponse(expected); + assertSuccessfulResponse(actual); + Assert.assertEquals(getNewContract(expected).toJSONString(), + getNewContract(actual).toJSONString()); + } + + private static JSONObject getNewContract(JSONObject response) { + return response.getJSONObject("result") + .getJSONObject("transaction") + .getJSONObject("raw_data") + .getJSONArray("contract") + .getJSONObject(0) + .getJSONObject("parameter") + .getJSONObject("value") + .getJSONObject("new_contract"); + } + + private static CallArguments newValidCallArguments() { + CallArguments args = new CallArguments(); + args.setFrom("0x0000000000000000000000000000000000000000"); + args.setTo("0x0000000000000000000000000000000000000001"); + args.setValue("0x0"); + args.setData("0x"); + return args; + } } diff --git a/framework/src/test/java/org/tron/core/jsonrpc/WalletCursorTest.java b/framework/src/test/java/org/tron/core/jsonrpc/WalletCursorTest.java index 24ca71a74bc..c7bd75614c3 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/WalletCursorTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/WalletCursorTest.java @@ -16,11 +16,15 @@ import org.tron.core.capsule.AccountCapsule; import org.tron.core.config.args.Args; import org.tron.core.db2.core.Chainbase.Cursor; +import org.tron.core.exception.ItemNotFoundException; import org.tron.core.exception.jsonrpc.JsonRpcExceedLimitException; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.exception.jsonrpc.JsonRpcMethodNotFoundException; import org.tron.core.services.NodeInfoService; import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; import org.tron.core.services.jsonrpc.TronJsonRpcImpl; import org.tron.core.services.jsonrpc.TronJsonRpcImpl.RequestSource; +import org.tron.core.services.jsonrpc.filters.BlockFilterAndResult; import org.tron.core.services.jsonrpc.filters.LogFilterAndResult; import org.tron.core.services.jsonrpc.types.BuildArguments; import org.tron.protos.Protocol; @@ -58,6 +62,114 @@ public void init() { init = true; } + @Test + public void testNullParameterChecksRespectRequestSource() throws Exception { + TronJsonRpcImpl tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet); + tronJsonRpc.setManager(dbManager); + + try { + dbManager.setCursor(Cursor.SOLIDITY); + Assert.assertThrows(JsonRpcInvalidParamsException.class, + () -> tronJsonRpc.newFilter(null)); + Assert.assertThrows(JsonRpcInvalidParamsException.class, + () -> tronJsonRpc.getLogs(null)); + Assert.assertFalse(tronJsonRpc.uninstallFilter(null)); + Assert.assertThrows(ItemNotFoundException.class, + () -> tronJsonRpc.getFilterChanges(null)); + Assert.assertThrows(ItemNotFoundException.class, + () -> tronJsonRpc.getFilterLogs(null)); + Assert.assertThrows(JsonRpcMethodNotFoundException.class, + () -> tronJsonRpc.buildTransaction(null)); + + dbManager.resetCursor(); + dbManager.setCursor(Cursor.PBFT); + Assert.assertThrows(JsonRpcMethodNotFoundException.class, + () -> tronJsonRpc.newFilter(null)); + Assert.assertThrows(JsonRpcMethodNotFoundException.class, + () -> tronJsonRpc.getLogs(null)); + Assert.assertThrows(JsonRpcMethodNotFoundException.class, + () -> tronJsonRpc.uninstallFilter(null)); + Assert.assertThrows(JsonRpcMethodNotFoundException.class, + () -> tronJsonRpc.getFilterChanges(null)); + Assert.assertThrows(JsonRpcMethodNotFoundException.class, + () -> tronJsonRpc.getFilterLogs(null)); + Assert.assertThrows(JsonRpcMethodNotFoundException.class, + () -> tronJsonRpc.buildTransaction(null)); + } finally { + dbManager.resetCursor(); + tronJsonRpc.close(); + } + } + + @Test + public void testUninstallFilterKeepsSourcesIsolated() throws Exception { + try (TronJsonRpcImpl tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet)) { + for (boolean eventFilter : new boolean[] {true, false}) { + if (eventFilter) { + tronJsonRpc.getEventFilter2ResultFull() + .put("10", new LogFilterAndResult(new FilterRequest(), 0L, wallet)); + tronJsonRpc.getEventFilter2ResultSolidity() + .put("20", new LogFilterAndResult(new FilterRequest(), 0L, wallet)); + } else { + tronJsonRpc.getBlockFilter2ResultFull().put("10", new BlockFilterAndResult()); + tronJsonRpc.getBlockFilter2ResultSolidity().put("20", new BlockFilterAndResult()); + } + Map fullFilters = eventFilter + ? tronJsonRpc.getEventFilter2ResultFull() : tronJsonRpc.getBlockFilter2ResultFull(); + Map solidityFilters = eventFilter + ? tronJsonRpc.getEventFilter2ResultSolidity() + : tronJsonRpc.getBlockFilter2ResultSolidity(); + Object fullFilter = fullFilters.get("10"); + Object solidityFilter = solidityFilters.get("20"); + + dbManager.resetCursor(); + dbManager.setCursor(Cursor.HEAD); + Assert.assertFalse(tronJsonRpc.uninstallFilter("0x20")); + Assert.assertSame(solidityFilter, solidityFilters.get("20")); + + dbManager.resetCursor(); + dbManager.setCursor(Cursor.SOLIDITY); + Assert.assertFalse(tronJsonRpc.uninstallFilter("0x10")); + Assert.assertSame(fullFilter, fullFilters.get("10")); + Assert.assertTrue(tronJsonRpc.uninstallFilter("0x20")); + Assert.assertFalse(solidityFilters.containsKey("20")); + Assert.assertFalse(tronJsonRpc.uninstallFilter("0x20")); + + dbManager.resetCursor(); + dbManager.setCursor(Cursor.HEAD); + Assert.assertTrue(tronJsonRpc.uninstallFilter("0x10")); + Assert.assertFalse(fullFilters.containsKey("10")); + Assert.assertFalse(tronJsonRpc.uninstallFilter("0x10")); + } + } finally { + dbManager.resetCursor(); + } + } + + @Test + public void testUninstallFilterInPbftRejectsAllIdsBeforeRemoval() throws Exception { + try (TronJsonRpcImpl tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet)) { + BlockFilterAndResult fullFilter = new BlockFilterAndResult(); + BlockFilterAndResult solidityFilter = new BlockFilterAndResult(); + tronJsonRpc.getBlockFilter2ResultFull().put("10", fullFilter); + tronJsonRpc.getBlockFilter2ResultSolidity().put("20", solidityFilter); + dbManager.setCursor(Cursor.PBFT); + + String[] filterIds = {null, "0x10", "0x20", "0xdeadbeef", "", "not-hex"}; + for (String filterId : filterIds) { + JsonRpcMethodNotFoundException error = Assert.assertThrows( + JsonRpcMethodNotFoundException.class, () -> tronJsonRpc.uninstallFilter(filterId)); + Assert.assertEquals( + "the method eth_uninstallFilter does not exist/is not available in PBFT", + error.getMessage()); + } + Assert.assertSame(fullFilter, tronJsonRpc.getBlockFilter2ResultFull().get("10")); + Assert.assertSame(solidityFilter, tronJsonRpc.getBlockFilter2ResultSolidity().get("20")); + } finally { + dbManager.resetCursor(); + } + } + @Test public void testSource() { TronJsonRpcImpl tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet); @@ -189,4 +301,4 @@ public void testNewFilter_exceedsCapThrowsException() throws Exception { } } -} \ No newline at end of file +}