From 971e3b53e40e289fcf07a2d8dc6f8fb597832e1a Mon Sep 17 00:00:00 2001 From: halibobo1205 Date: Mon, 21 Sep 2026 14:56:55 +0800 Subject: [PATCH 1/4] feat(api): enforce token budget in JsonFormat parser --- .../tron/core/services/http/JsonFormat.java | 15 ++- .../http/DeployContractServletTest.java | 29 +++++ .../http/JsonFormatTokenLimitTest.java | 109 ++++++++++++++++++ .../http/JsonFormatUnicodeErrorTest.java | 2 +- .../jsonrpc/TronJsonRpcAbiTokenLimitTest.java | 109 ++++++++++++++++++ 5 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/services/http/JsonFormatTokenLimitTest.java create mode 100644 framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcAbiTokenLimitTest.java diff --git a/framework/src/main/java/org/tron/core/services/http/JsonFormat.java b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java index e9aa801f078..efc70fd300f 100644 --- a/framework/src/main/java/org/tron/core/services/http/JsonFormat.java +++ b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java @@ -1344,6 +1344,7 @@ protected static class Tokenizer { private int previousLine = 0; private int previousColumn = 0; private int currentDepth = 0; + private int tokenCount = 0; public void enterRecursion() throws ParseException { if (currentDepth >= Constant.MAX_NESTING_DEPTH) { @@ -1359,7 +1360,7 @@ public void exitRecursion() { /** * Construct a tokenizer that parses tokens from the given text. */ - public Tokenizer(CharSequence text) { + public Tokenizer(CharSequence text) throws ParseException { this.text = text; matcher = WHITESPACE.matcher(text); skipWhitespace(); @@ -1410,7 +1411,7 @@ public boolean atEnd() { /** * Advance to the next token. */ - public void nextToken() { + public void nextToken() throws ParseException { previousLine = line; previousColumn = column; @@ -1440,6 +1441,14 @@ public void nextToken() { matcher.region(pos + 1, matcher.regionEnd()); } + // Match Jackson's token count for JSON: separators are not tokens, but field names, + // values and container boundaries are. Each parsed document has its own budget. + if (!",".equals(currentToken) && !":".equals(currentToken) + && ++tokenCount > Constant.MAX_TOKEN_COUNT) { + throw new ParseException("Token count exceeds the maximum allowed (" + + Constant.MAX_TOKEN_COUNT + ")."); + } + skipWhitespace(); } } @@ -1458,7 +1467,7 @@ private void skipWhitespace() { * If the next token exactly matches {@code token}, consume it and return {@code true}. * Otherwise, return {@code false} without doing anything. */ - public boolean tryConsume(String token) { + public boolean tryConsume(String token) throws ParseException { if (currentToken.equals(token)) { nextToken(); return true; diff --git a/framework/src/test/java/org/tron/core/services/http/DeployContractServletTest.java b/framework/src/test/java/org/tron/core/services/http/DeployContractServletTest.java index 703f278c890..aa109f5b82a 100644 --- a/framework/src/test/java/org/tron/core/services/http/DeployContractServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/DeployContractServletTest.java @@ -1,16 +1,22 @@ package org.tron.core.services.http; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Collections; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.tron.core.capsule.TransactionCapsule; +import org.tron.json.JSONObject; import org.tron.protos.Protocol; import org.tron.protos.contract.SmartContractOuterClass.CreateSmartContract; @@ -82,4 +88,27 @@ public void testDeployContractOmitsNullAbiOutputs() throws Exception { eq(Protocol.Transaction.Contract.ContractType.CreateSmartContract)); assertTransactionResponse(response); } + + @Test + public void testDeployContractRejectsStringAbiOverTokenLimit() throws Exception { + // The outer document sees one string; its ABI expands to over 100,000 JSON tokens. + String abi = "[" + String.join(",", Collections.nCopies(50_000, "{}")) + "]"; + String jsonParam = "{" + + "\"owner_address\":\"4199357684BC659F5166046B56C95A0E99F1265CD1\"," + + "\"abi\":\"" + abi + "\"," + + "\"bytecode\":\"6000\"" + + "}"; + assertTrue(jsonParam.getBytes(UTF_8).length < 4 * 1024 * 1024); + assertEquals(abi, JSONObject.parseObject(jsonParam).getString("abi")); + + MockHttpServletResponse response = newResponse(); + servlet.doPost(postRequest(jsonParam), response); + + assertEquals(200, response.getStatus()); + String content = response.getContentAsString(); + assertEquals("Token count exceeds the maximum allowed (100000).", + JSONObject.parseObject(content).getString("Error")); + assertFalse(content.contains("internal server error")); + verify(wallet, never()).createTransactionCapsule(any(), any()); + } } diff --git a/framework/src/test/java/org/tron/core/services/http/JsonFormatTokenLimitTest.java b/framework/src/test/java/org/tron/core/services/http/JsonFormatTokenLimitTest.java new file mode 100644 index 00000000000..b3efd67c21d --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/JsonFormatTokenLimitTest.java @@ -0,0 +1,109 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import java.io.IOException; +import java.io.StringReader; +import java.util.Collections; +import org.junit.Test; +import org.tron.core.Constant; +import org.tron.protos.Protocol; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI; + +public class JsonFormatTokenLimitTest { + + @Test + public void testExactLimitWithStringSeparatorsAndIndependentMerges() throws Exception { + // The object, field name and array consume five tokens. Each quoted value is one token, + // even when its contents are a comma or colon. + String input = "{\"unknown\":[" + + repeat("\",:\"", Constant.MAX_TOKEN_COUNT - 5) + "]}"; + assertEquals(Constant.MAX_TOKEN_COUNT, jacksonTokenCount(input)); + + JsonFormat.merge(input, Protocol.HelloMessage.newBuilder()); + // A fresh merge gets a fresh budget; whitespace and EOF must not consume it. + JsonFormat.merge(new StringReader(input + " \n\t"), Protocol.HelloMessage.newBuilder()); + } + + @Test + public void testOneTokenOverLimitAndRecovery() throws Exception { + String input = "{\"unknown\":[" + + repeat("0", Constant.MAX_TOKEN_COUNT - 4) + "]}"; + assertEquals(Constant.MAX_TOKEN_COUNT + 1, jacksonTokenCount(input)); + + assertTokenLimit(assertThrows(JsonFormat.ParseException.class, + () -> JsonFormat.merge(input, Protocol.HelloMessage.newBuilder()))); + assertTokenLimit(assertThrows(JsonFormat.ParseException.class, + () -> JsonFormat.merge(new StringReader(input), Protocol.HelloMessage.newBuilder()))); + + String atLimit = "{\"unknown\":[" + + repeat("0", Constant.MAX_TOKEN_COUNT - 5) + "]}"; + JsonFormat.merge(atLimit, Protocol.HelloMessage.newBuilder()); + } + + @Test + public void testFieldNamesAndContainerBoundariesCount() throws Exception { + String atLimit = "{" + repeat("\"unknown\":0", (Constant.MAX_TOKEN_COUNT - 2) / 2) + "}"; + assertEquals(Constant.MAX_TOKEN_COUNT, jacksonTokenCount(atLimit)); + JsonFormat.merge(atLimit, Protocol.HelloMessage.newBuilder()); + + // Wrapping an existing scalar adds exactly one START_ARRAY and one END_ARRAY. + String overLimit = atLimit.replaceFirst(":0", ":[0]"); + assertEquals(Constant.MAX_TOKEN_COUNT + 2, jacksonTokenCount(overLimit)); + assertTokenLimit(assertThrows(JsonFormat.ParseException.class, + () -> JsonFormat.merge(overLimit, Protocol.HelloMessage.newBuilder()))); + } + + @Test + public void testWideAbiStopsBeforeAllEntriesAreBuilt() throws Exception { + int entryCount = Constant.MAX_TOKEN_COUNT / 2; + String input = "{\"entrys\":[" + repeat("{}", entryCount) + "]}"; + assertEquals(Constant.MAX_TOKEN_COUNT + 5, jacksonTokenCount(input)); + ABI.Builder builder = ABI.newBuilder(); + + assertTokenLimit(assertThrows(JsonFormat.ParseException.class, + () -> JsonFormat.merge(input, builder))); + assertTrue(builder.getEntrysCount() > 0); + assertTrue(builder.getEntrysCount() < entryCount); + } + + @Test + public void testNestedKnownAndUnknownFieldsShareBudget() throws Exception { + String entry = "{\"inputs\":[" + + repeat("{\"name\":\"p\",\"type\":\"uint256\"}", Constant.MAX_TOKEN_COUNT / 10) + + "]}"; + String knownFields = "{\"entrys\":[" + entry + "]}"; + assertTrue(jacksonTokenCount(knownFields) < Constant.MAX_TOKEN_COUNT); + JsonFormat.merge(knownFields, ABI.newBuilder()); + + String input = "{\"entrys\":[" + entry + "],\"unknown\":[" + + repeat("0", Constant.MAX_TOKEN_COUNT / 2) + "]}"; + assertTrue(jacksonTokenCount(input) > Constant.MAX_TOKEN_COUNT); + assertTokenLimit(assertThrows(JsonFormat.ParseException.class, + () -> JsonFormat.merge(input, ABI.newBuilder()))); + } + + private static String repeat(String value, int count) { + return String.join(",", Collections.nCopies(count, value)); + } + + private static long jacksonTokenCount(String input) throws IOException { + long count = 0; + try (JsonParser parser = new JsonFactory().createParser(input)) { + while (parser.nextToken() != null) { + count++; + } + } + return count; + } + + private static void assertTokenLimit(JsonFormat.ParseException exception) { + assertEquals(JsonFormat.ParseException.class, exception.getClass()); + assertEquals("Token count exceeds the maximum allowed (" + Constant.MAX_TOKEN_COUNT + ").", + exception.getMessage()); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/JsonFormatUnicodeErrorTest.java b/framework/src/test/java/org/tron/core/services/http/JsonFormatUnicodeErrorTest.java index e5d9d5146c8..4fbcff90026 100644 --- a/framework/src/test/java/org/tron/core/services/http/JsonFormatUnicodeErrorTest.java +++ b/framework/src/test/java/org/tron/core/services/http/JsonFormatUnicodeErrorTest.java @@ -102,7 +102,7 @@ public void testParserErrorsReplaceOnlyUnpairedSurrogates() throws Exception { } @Test - public void testParserErrorsPreserveValidDescriptions() { + public void testParserErrorsPreserveValidDescriptions() throws Exception { String description = "invalid " + (char) 0x4E2D + EMOJI; JsonFormat.Tokenizer tokenizer = new JsonFormat.Tokenizer("field"); diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcAbiTokenLimitTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcAbiTokenLimitTest.java new file mode 100644 index 00000000000..08947db82f9 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcAbiTokenLimitTest.java @@ -0,0 +1,109 @@ +package org.tron.core.services.jsonrpc; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.tron.common.TestConstants; +import org.tron.core.Wallet; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.config.args.Args; +import org.tron.core.db.Manager; +import org.tron.core.db2.core.Chainbase.Cursor; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.services.NodeInfoService; +import org.tron.core.services.jsonrpc.TronJsonRpc.TransactionJson; +import org.tron.core.services.jsonrpc.types.BuildArguments; +import org.tron.json.JSONObject; +import org.tron.protos.Protocol.Transaction; +import org.tron.protos.Protocol.Transaction.Contract.ContractType; +import org.tron.protos.contract.SmartContractOuterClass.CreateSmartContract; + +public class TronJsonRpcAbiTokenLimitTest { + + private Wallet wallet; + private TronJsonRpcImpl jsonRpc; + + @BeforeClass + public static void initArgs() { + Args.setParam(new String[]{}, TestConstants.TEST_CONF); + } + + @AfterClass + public static void clearArgs() { + Args.clearParam(); + } + + @Before + public void setUp() { + wallet = mock(Wallet.class); + when(wallet.getCursor()).thenReturn(Cursor.HEAD); + jsonRpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet, mock(Manager.class)); + } + + @After + public void tearDown() throws Exception { + if (jsonRpc != null) { + jsonRpc.close(); + } + } + + @Test + public void testBuildTransactionRejectsAbiOverTokenLimit() throws Exception { + String abi = "[" + String.join(",", Collections.nCopies(50_000, "{}")) + "]"; + String json = "{\"abi\":\"" + abi + "\"}"; + assertTrue(json.getBytes(UTF_8).length < 4 * 1024 * 1024); + assertEquals(abi, JSONObject.parseObject(json).getString("abi")); + BuildArguments args = createArguments(abi); + + JsonRpcInvalidParamsException exception = assertThrows(JsonRpcInvalidParamsException.class, + () -> jsonRpc.buildTransaction(args)); + + assertEquals("invalid abi", exception.getMessage()); + verify(wallet, never()).createTransactionCapsule(any(), any()); + } + + @Test + public void testBuildTransactionAcceptsSmallAbi() throws Exception { + Transaction transaction = Transaction.newBuilder() + .setRawData(Transaction.raw.newBuilder().addContract(Transaction.Contract.newBuilder())) + .build(); + when(wallet.createTransactionCapsule(any(), eq(ContractType.CreateSmartContract))) + .thenReturn(new TransactionCapsule(transaction)); + BuildArguments args = createArguments("[{\"name\":\"test\",\"type\":\"function\"}]"); + + TransactionJson result = jsonRpc.buildTransaction(args); + + assertNotNull(result.getTransaction()); + assertTrue(result.getTransaction().containsKey("txID")); + verify(wallet).createTransactionCapsule( + argThat(contract -> contract instanceof CreateSmartContract + && ((CreateSmartContract) contract).getNewContract().getAbi().getEntrysCount() == 1 + && ((CreateSmartContract) contract).getNewContract().getAbi().getEntrys(0) + .getName().equals("test")), + eq(ContractType.CreateSmartContract)); + } + + private static BuildArguments createArguments(String abi) { + BuildArguments args = new BuildArguments(); + args.setFrom("0x99357684bc659f5166046b56c95a0e99f1265cd1"); + args.setData("0x6000"); + args.setAbi(abi); + return args; + } +} From 165af9dec51f8afc2c334b37f001342b33e4c303 Mon Sep 17 00:00:00 2001 From: halibobo1205 Date: Thu, 8 Oct 2026 18:36:13 +0800 Subject: [PATCH 2/4] feat(api): bound form parameter count in getJsonString --- .../org/tron/core/services/http/Util.java | 14 +++++- .../tron/core/services/http/UtilMockTest.java | 44 ++++++++++++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index b4902a0d9d0..bbf312d5415 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -28,6 +28,7 @@ import org.bouncycastle.util.encoders.Hex; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.MultiMap; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.UrlEncoded; @@ -687,13 +688,14 @@ public static void validateParameter(String contract) throws InvalidParameterExc } } - public static String getJsonString(String str) { + public static String getJsonString(String str) throws ParseException { if (StringUtils.isEmpty(str)) { return EMPTY; } if (isValidJson(str)) { return str; } + checkFormPairs(str); MultiMap params = new MultiMap<>(); UrlEncoded.decodeUtf8To(str, params); JSONObject json = new JSONObject(); @@ -709,6 +711,16 @@ public static String getJsonString(String str) { return json.toString(); } + private static void checkFormPairs(String form) throws ParseException { + int max = ContextHandler.DEFAULT_MAX_FORM_KEYS; + int pairs = 1; + for (int i = 0; i < form.length(); i++) { + if (form.charAt(i) == '&' && ++pairs > max) { + throw new ParseException("form has too many parameters, the limit is " + max); + } + } + } + public static boolean isValidJson(String json) { try { JSON.parse(json); diff --git a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java index ed43e960520..1d61cf46a47 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java @@ -7,7 +7,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.StringJoiner; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jetty.server.handler.ContextHandler; import org.junit.After; import org.junit.Assert; import org.junit.Test; @@ -357,7 +359,7 @@ public void testValidateParameter() { } @Test - public void testGetJsonString() { + public void testGetJsonString() throws Exception { String str = ""; String ret = Util.getJsonString(str); Assert.assertTrue(StringUtils.isEmpty(ret)); @@ -374,4 +376,44 @@ public void testGetJsonString() { Assert.assertEquals(expect, ret2); } + private static String buildForm(int n, boolean distinct) { + StringJoiner joiner = new StringJoiner("&"); + for (int i = 0; i < n; i++) { + joiner.add((distinct ? "k" + i : "k") + "=v"); + } + return joiner.toString(); + } + + private static void assertRejectsTooManyFormPairs(boolean distinct) { + JsonFormat.ParseException e = Assert.assertThrows(JsonFormat.ParseException.class, + () -> Util.getJsonString(buildForm(ContextHandler.DEFAULT_MAX_FORM_KEYS + 1, distinct))); + Assert.assertEquals("form has too many parameters, the limit is " + + ContextHandler.DEFAULT_MAX_FORM_KEYS, e.getMessage()); + } + + @Test + public void testGetJsonStringRejectsTooManyDistinctKeys() { + assertRejectsTooManyFormPairs(true); + } + + @Test + public void testGetJsonStringRejectsTooManyDuplicateKeys() { + assertRejectsTooManyFormPairs(false); + } + + @Test + public void testGetJsonStringAcceptsMaxKeys() throws Exception { + int max = ContextHandler.DEFAULT_MAX_FORM_KEYS; + String ret = Util.getJsonString(buildForm(max, true)); + Assert.assertTrue(ret.contains("\"k0\":\"v\"")); + Assert.assertTrue(ret.contains("\"k" + (max - 1) + "\":\"v\"")); + } + + @Test + public void testGetJsonStringKeepsJsonWithManyAmpersands() throws Exception { + String json = "{\"k\":\"" + StringUtils.repeat('&', ContextHandler.DEFAULT_MAX_FORM_KEYS + 1) + + "\"}"; + Assert.assertEquals(json, Util.getJsonString(json)); + } + } From b03d1ddd8282e6232ca15c3d06b2de6d58f25206 Mon Sep 17 00:00:00 2001 From: halibobo1205 Date: Fri, 9 Oct 2026 15:34:28 +0800 Subject: [PATCH 3/4] feat(api): log invalid contracts at debug in packTransaction --- .../src/main/java/org/tron/common/utils/DecodeUtil.java | 9 +++------ .../src/main/java/org/tron/core/services/http/Util.java | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/common/src/main/java/org/tron/common/utils/DecodeUtil.java b/common/src/main/java/org/tron/common/utils/DecodeUtil.java index 769268da2b9..f430411a25c 100644 --- a/common/src/main/java/org/tron/common/utils/DecodeUtil.java +++ b/common/src/main/java/org/tron/common/utils/DecodeUtil.java @@ -14,19 +14,16 @@ public class DecodeUtil { public static boolean addressValid(byte[] address) { if (ArrayUtils.isEmpty(address)) { - logger.warn("Warning: Address is empty !!"); + logger.debug("Address is empty !!"); return false; } if (address.length != ADDRESS_SIZE / 2) { - logger.warn( - "Warning: Address length need " + ADDRESS_SIZE + " but " + address.length - + " !!"); + logger.debug("Address length need {} but {} !!", ADDRESS_SIZE, address.length); return false; } if (address[0] != addressPreFixByte) { - logger.warn("Warning: Address need prefix with " + addressPreFixByte + " but " - + address[0] + " !!"); + logger.debug("Address need prefix with {} but {} !!", addressPreFixByte, address[0]); return false; } return true; diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index bbf312d5415..07a36ca29e4 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -357,7 +357,7 @@ public static Transaction packTransaction(String strTransaction, boolean selfTyp } catch (JSONException e) { logger.debug("JSONException: {}", e.getMessage()); } catch (Exception e) { - logger.warn("{}", contractType, e); + logger.debug("Skip invalid contract: {}", contractType, e); } } rawData.put("contract", contracts); From f17a200ec232fd49414ebe4d05cd6944e5727462 Mon Sep 17 00:00:00 2001 From: halibobo1205 Date: Fri, 9 Oct 2026 19:12:33 +0800 Subject: [PATCH 4/4] feat(api): require exactly one contract in transaction APIs --- .../tron/core/actuator/ActuatorConstant.java | 2 + .../org/tron/core/utils/TransactionUtil.java | 9 + .../src/main/java/org/tron/core/Wallet.java | 20 ++- .../main/java/org/tron/core/db/Manager.java | 4 +- .../org/tron/core/services/RpcApiService.java | 13 ++ .../core/services/http/BroadcastServlet.java | 22 ++- .../http/GetShieldTransactionHashServlet.java | 9 +- .../GetTransactionApprovedListServlet.java | 14 +- .../http/GetTransactionSignWeightServlet.java | 14 +- .../org/tron/core/services/http/Util.java | 14 +- .../core/services/RpcApiServicesTest.java | 53 ++++-- .../org/tron/core/services/http/UtilTest.java | 166 +++++++++++++++++- 12 files changed, 307 insertions(+), 33 deletions(-) diff --git a/actuator/src/main/java/org/tron/core/actuator/ActuatorConstant.java b/actuator/src/main/java/org/tron/core/actuator/ActuatorConstant.java index ed5ca2f5a77..98ee5168304 100644 --- a/actuator/src/main/java/org/tron/core/actuator/ActuatorConstant.java +++ b/actuator/src/main/java/org/tron/core/actuator/ActuatorConstant.java @@ -8,6 +8,8 @@ public class ActuatorConstant { public static final String NOT_EXIST_STR = "] not exists"; public static final String TX_RESULT_NULL = "TransactionResultCapsule is null"; public static final String CONTRACT_NOT_EXIST = "No contract!"; + public static final String CONTRACT_SIZE_NOT_EQUAL_TO_ONE = + "contract size should be exactly 1, this is extend feature ,actual :%d"; public static final String STORE_NOT_EXIST = "No account store or dynamic store!"; public static final int ONE_HUNDRED = 100; public static final int PRECISION_DECIMAL = 6; diff --git a/actuator/src/main/java/org/tron/core/utils/TransactionUtil.java b/actuator/src/main/java/org/tron/core/utils/TransactionUtil.java index efb492d14c9..ee61cb4b5fa 100644 --- a/actuator/src/main/java/org/tron/core/utils/TransactionUtil.java +++ b/actuator/src/main/java/org/tron/core/utils/TransactionUtil.java @@ -40,6 +40,7 @@ import org.tron.api.GrpcAPI.TransactionSignWeight.Result; import org.tron.common.utils.Sha256Hash; import org.tron.core.ChainBaseManager; +import org.tron.core.actuator.ActuatorConstant; import org.tron.core.capsule.AccountCapsule; import org.tron.core.capsule.TransactionCapsule; import org.tron.core.exception.PermissionException; @@ -197,6 +198,14 @@ public static Transaction truncateSignatures(Transaction trx) { public TransactionSignWeight getTransactionSignWeight(Transaction trx) { TransactionSignWeight.Builder tswBuilder = TransactionSignWeight.newBuilder(); Result.Builder resultBuilder = Result.newBuilder(); + int contractCount = trx.getRawData().getContractCount(); + if (contractCount > 1) { + resultBuilder.setCode(Result.response_code.OTHER_ERROR); + resultBuilder.setMessage("Invalid transaction: " + + String.format(ActuatorConstant.CONTRACT_SIZE_NOT_EQUAL_TO_ONE, contractCount)); + tswBuilder.setResult(resultBuilder); + return tswBuilder.build(); + } if (trx.getSignatureCount() > chainBaseManager.getDynamicPropertiesStore() .getTotalSignNum()) { resultBuilder.setCode(Result.response_code.OTHER_ERROR); diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 84fb41bf9f0..ac07b375b41 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -171,6 +171,7 @@ import org.tron.core.exception.AccountResourceInsufficientException; import org.tron.core.exception.BadItemException; import org.tron.core.exception.ContractExeException; +import org.tron.core.exception.ContractSizeNotEqualToOneException; import org.tron.core.exception.ContractValidateException; import org.tron.core.exception.DupTransactionException; import org.tron.core.exception.HeaderNotFound; @@ -567,9 +568,14 @@ public GrpcAPI.Return broadcastTransaction(Transaction signedTransaction) { if (chainBaseManager.getDynamicPropertiesStore().supportVM()) { trx.resetResult(); } - if (trx.getInstance().getRawData().getContractCount() == 0) { + int contractCount = trx.getInstance().getRawData().getContractCount(); + if (contractCount == 0) { throw new ContractValidateException(ActuatorConstant.CONTRACT_NOT_EXIST); } + if (contractCount > 1) { + throw new ContractSizeNotEqualToOneException(String.format( + "tx %s " + ActuatorConstant.CONTRACT_SIZE_NOT_EQUAL_TO_ONE, txID, contractCount)); + } trx.checkExpiration(chainBaseManager.getNextBlockSlotTime()); dbManager.pushTransaction(trx); TransactionMessage message = new TransactionMessage(trx.getInstance().toByteArray()); @@ -632,6 +638,14 @@ public TransactionApprovedList getTransactionApprovedList(Transaction trx) { TransactionApprovedList.Builder tswBuilder = TransactionApprovedList.newBuilder(); TransactionApprovedList.Result.Builder resultBuilder = TransactionApprovedList.Result .newBuilder(); + int contractCount = trx.getRawData().getContractCount(); + if (contractCount > 1) { + resultBuilder.setCode(TransactionApprovedList.Result.response_code.OTHER_ERROR); + resultBuilder.setMessage("Invalid transaction: " + + String.format(ActuatorConstant.CONTRACT_SIZE_NOT_EQUAL_TO_ONE, contractCount)); + tswBuilder.setResult(resultBuilder); + return tswBuilder.build(); + } if (trx.getSignatureCount() > chainBaseManager.getDynamicPropertiesStore() .getTotalSignNum()) { resultBuilder.setCode(TransactionApprovedList.Result.response_code.OTHER_ERROR); @@ -2759,6 +2773,10 @@ public BytesMessage getShieldTransactionHash(Transaction transaction) if (contract == null || contract.isEmpty()) { throw new ContractValidateException("Contract is null"); } + if (contract.size() > 1) { + throw new ContractValidateException( + String.format(ActuatorConstant.CONTRACT_SIZE_NOT_EQUAL_TO_ONE, contract.size())); + } ContractType contractType = contract.get(0).getType(); if (contractType != ContractType.ShieldedTransferContract) { throw new ContractValidateException("Not a shielded transaction"); diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index b7effeb72e2..6119504deec 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -88,6 +88,7 @@ import org.tron.core.ChainBaseManager; import org.tron.core.Constant; import org.tron.core.Wallet; +import org.tron.core.actuator.ActuatorConstant; import org.tron.core.actuator.ActuatorCreator; import org.tron.core.capsule.AccountCapsule; import org.tron.core.capsule.BlockBalanceTraceCapsule; @@ -1477,8 +1478,7 @@ public TransactionInfo processTransaction(final TransactionCapsule trxCap, Block Sha256Hash txId = trxCap.getTransactionId(); if (trxCap.getInstance().getRawData().getContractList().size() != 1) { throw new ContractSizeNotEqualToOneException( - String.format( - "tx %s contract size should be exactly 1, this is extend feature ,actual :%d", + String.format("tx %s " + ActuatorConstant.CONTRACT_SIZE_NOT_EQUAL_TO_ONE, txId, trxCap.getInstance().getRawData().getContractList().size())); } Contract contract = trxCap.getInstance().getRawData().getContract(0); diff --git a/framework/src/main/java/org/tron/core/services/RpcApiService.java b/framework/src/main/java/org/tron/core/services/RpcApiService.java index c9e961099bf..716046784d1 100755 --- a/framework/src/main/java/org/tron/core/services/RpcApiService.java +++ b/framework/src/main/java/org/tron/core/services/RpcApiService.java @@ -76,6 +76,7 @@ import org.tron.common.utils.StringUtil; import org.tron.core.ChainBaseManager; import org.tron.core.Wallet; +import org.tron.core.actuator.ActuatorConstant; import org.tron.core.capsule.AccountCapsule; import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.TransactionCapsule; @@ -2503,6 +2504,18 @@ public void updateBrokerage(UpdateBrokerageContract request, @Override public void createCommonTransaction(Transaction request, StreamObserver responseObserver) { + int contractCount = request.getRawData().getContractCount(); + if (contractCount != 1) { + String message = contractCount == 0 ? ActuatorConstant.CONTRACT_NOT_EXIST + : String.format(ActuatorConstant.CONTRACT_SIZE_NOT_EQUAL_TO_ONE, contractCount); + Return result = Return.newBuilder().setResult(false) + .setCode(response_code.CONTRACT_VALIDATE_ERROR) + .setMessage(ByteString.copyFromUtf8(Wallet.CONTRACT_VALIDATE_ERROR + message)) + .build(); + responseObserver.onNext(TransactionExtention.newBuilder().setResult(result).build()); + responseObserver.onCompleted(); + return; + } Transaction.Contract contract = request.getRawData().getContract(0); createTransactionExtention(contract.getParameter(), contract.getType(), responseObserver); diff --git a/framework/src/main/java/org/tron/core/services/http/BroadcastServlet.java b/framework/src/main/java/org/tron/core/services/http/BroadcastServlet.java index 844d606ebb2..e95e1c9c5ff 100644 --- a/framework/src/main/java/org/tron/core/services/http/BroadcastServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/BroadcastServlet.java @@ -1,5 +1,6 @@ package org.tron.core.services.http; +import com.google.protobuf.ByteString; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -9,6 +10,7 @@ import org.tron.common.utils.ByteArray; import org.tron.core.Wallet; import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.exception.ContractSizeNotEqualToOneException; import org.tron.json.JSONObject; import org.tron.protos.Protocol.Transaction; @@ -23,11 +25,21 @@ public class BroadcastServlet extends RateLimiterServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) { try { PostParams params = PostParams.getPostParams(request); - Transaction transaction = Util.packTransaction(params.getParams(), params.isVisible()); - TransactionCapsule transactionCapsule = new TransactionCapsule(transaction); - String transactionID = ByteArray - .toHexString(transactionCapsule.getTransactionId().getBytes()); - GrpcAPI.Return result = wallet.broadcastTransaction(transaction); + GrpcAPI.Return result; + String transactionID = ""; + try { + Transaction transaction = Util.packTransaction(params.getParams(), params.isVisible()); + TransactionCapsule transactionCapsule = new TransactionCapsule(transaction); + transactionID = ByteArray + .toHexString(transactionCapsule.getTransactionId().getBytes()); + result = wallet.broadcastTransaction(transaction); + } catch (ContractSizeNotEqualToOneException e) { + result = GrpcAPI.Return.newBuilder() + .setResult(false) + .setCode(GrpcAPI.Return.response_code.CONTRACT_VALIDATE_ERROR) + .setMessage(ByteString.copyFromUtf8(Wallet.CONTRACT_VALIDATE_ERROR + e.getMessage())) + .build(); + } JSONObject res = JSONObject.outboundParseObject( JsonFormat.printToString(result, params.isVisible())); res.put("txid", transactionID); diff --git a/framework/src/main/java/org/tron/core/services/http/GetShieldTransactionHashServlet.java b/framework/src/main/java/org/tron/core/services/http/GetShieldTransactionHashServlet.java index 64f0c7e7b28..43c42c1e110 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetShieldTransactionHashServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetShieldTransactionHashServlet.java @@ -8,6 +8,8 @@ import org.springframework.stereotype.Component; import org.tron.api.GrpcAPI.BytesMessage; import org.tron.core.Wallet; +import org.tron.core.exception.ContractSizeNotEqualToOneException; +import org.tron.core.exception.ContractValidateException; import org.tron.protos.Protocol.Transaction; @@ -28,7 +30,12 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) .collect(Collectors.joining(System.lineSeparator())); Util.checkBodySize(input); boolean visible = Util.getVisiblePost(input); - Transaction transaction = Util.packTransaction(input, visible); + Transaction transaction; + try { + transaction = Util.packTransaction(input, visible); + } catch (ContractSizeNotEqualToOneException e) { + throw new ContractValidateException(e.getMessage()); + } BytesMessage result = wallet.getShieldTransactionHash(transaction); response.getWriter().println(JsonFormat.printToString(result, visible)); diff --git a/framework/src/main/java/org/tron/core/services/http/GetTransactionApprovedListServlet.java b/framework/src/main/java/org/tron/core/services/http/GetTransactionApprovedListServlet.java index d4d123b7efe..64d7ad7a354 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetTransactionApprovedListServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetTransactionApprovedListServlet.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Component; import org.tron.api.GrpcAPI.TransactionApprovedList; import org.tron.core.Wallet; +import org.tron.core.exception.ContractSizeNotEqualToOneException; import org.tron.protos.Protocol.Transaction; @@ -24,8 +25,17 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { protected void doPost(HttpServletRequest request, HttpServletResponse response) { try { PostParams params = PostParams.getPostParams(request); - Transaction transaction = Util.packTransaction(params.getParams(), params.isVisible()); - TransactionApprovedList reply = wallet.getTransactionApprovedList(transaction); + TransactionApprovedList reply; + try { + Transaction transaction = Util.packTransaction(params.getParams(), params.isVisible()); + reply = wallet.getTransactionApprovedList(transaction); + } catch (ContractSizeNotEqualToOneException e) { + TransactionApprovedList.Result result = TransactionApprovedList.Result.newBuilder() + .setCode(TransactionApprovedList.Result.response_code.OTHER_ERROR) + .setMessage("Invalid transaction: " + e.getMessage()) + .build(); + reply = TransactionApprovedList.newBuilder().setResult(result).build(); + } if (reply != null) { response.getWriter().println(Util.printTransactionApprovedList(reply, params.isVisible())); } else { diff --git a/framework/src/main/java/org/tron/core/services/http/GetTransactionSignWeightServlet.java b/framework/src/main/java/org/tron/core/services/http/GetTransactionSignWeightServlet.java index bbd819d4e6d..80fe73e6ef0 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetTransactionSignWeightServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetTransactionSignWeightServlet.java @@ -6,6 +6,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.api.GrpcAPI.TransactionSignWeight; +import org.tron.core.exception.ContractSizeNotEqualToOneException; import org.tron.core.utils.TransactionUtil; import org.tron.protos.Protocol.Transaction; @@ -24,8 +25,17 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { protected void doPost(HttpServletRequest request, HttpServletResponse response) { try { PostParams params = PostParams.getPostParams(request); - Transaction transaction = Util.packTransaction(params.getParams(), params.isVisible()); - TransactionSignWeight reply = transactionUtil.getTransactionSignWeight(transaction); + TransactionSignWeight reply; + try { + Transaction transaction = Util.packTransaction(params.getParams(), params.isVisible()); + reply = transactionUtil.getTransactionSignWeight(transaction); + } catch (ContractSizeNotEqualToOneException e) { + TransactionSignWeight.Result result = TransactionSignWeight.Result.newBuilder() + .setCode(TransactionSignWeight.Result.response_code.OTHER_ERROR) + .setMessage("Invalid transaction: " + e.getMessage()) + .build(); + reply = TransactionSignWeight.newBuilder().setResult(result).build(); + } if (reply != null) { response.getWriter().println(Util.printTransactionSignWeight(reply, params.isVisible())); } else { diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index 07a36ca29e4..d878cbe5282 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -43,11 +43,13 @@ import org.tron.common.utils.ByteArray; import org.tron.common.utils.Sha256Hash; import org.tron.core.Constant; +import org.tron.core.actuator.ActuatorConstant; import org.tron.core.actuator.TransactionFactory; import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.TransactionCapsule; import org.tron.core.config.args.Args; import org.tron.core.db.TransactionTrace; +import org.tron.core.exception.ContractSizeNotEqualToOneException; import org.tron.core.exception.ContractValidateException; import org.tron.core.exception.MaintenanceUnavailableException; import org.tron.core.services.http.JsonFormat.ParseException; @@ -313,13 +315,21 @@ public static JSONObject printTransactionToJSON(Transaction transaction, boolean } /** - * Note: the contracts of the returned transaction may be empty + * Note: the contracts of the returned transaction may be empty, or more than one when a + * field-number key replaces raw_data; null is returned when the transaction cannot be parsed. + * + * @throws ContractSizeNotEqualToOneException if raw_data.contract has more than one element */ - public static Transaction packTransaction(String strTransaction, boolean selfType) { + public static Transaction packTransaction(String strTransaction, boolean selfType) + throws ContractSizeNotEqualToOneException { JSONObject jsonTransaction = JSON.parseObject(strTransaction); JSONObject rawData = jsonTransaction.getJSONObject("raw_data"); JSONArray contracts = new JSONArray(); JSONArray rawContractArray = rawData.getJSONArray("contract"); + if (rawContractArray.size() > 1) { + throw new ContractSizeNotEqualToOneException(String.format( + ActuatorConstant.CONTRACT_SIZE_NOT_EQUAL_TO_ONE, rawContractArray.size())); + } String contractType = null; for (int i = 0; i < rawContractArray.size(); i++) { diff --git a/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java b/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java index 6d20f72da19..fca5613b694 100644 --- a/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java +++ b/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java @@ -49,6 +49,8 @@ import org.tron.api.GrpcAPI.PaginatedMessage; import org.tron.api.GrpcAPI.PrivateParameters; import org.tron.api.GrpcAPI.PrivateParametersWithoutAsk; +import org.tron.api.GrpcAPI.Return; +import org.tron.api.GrpcAPI.Return.response_code; import org.tron.api.GrpcAPI.ViewingKeyMessage; import org.tron.api.WalletGrpc; import org.tron.api.WalletGrpc.WalletBlockingStub; @@ -651,21 +653,46 @@ public void testUpdateBrokerage() { assertNotNull(blockingStubFull.updateBrokerage(message)); } + private static Transaction.Contract brokerageContract() { + UpdateBrokerageContract updateBrokerageContract = UpdateBrokerageContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(Objects + .requireNonNull(decodeFromBase58Check("TN3zfjYUmMFK3ZsHSsrdJoNRtGkQmZLBLz")))) + .setBrokerage(10) + .build(); + return Transaction.Contract.newBuilder() + .setType(Transaction.Contract.ContractType.UpdateBrokerageContract) + .setParameter(Any.pack(updateBrokerageContract)) + .build(); + } + @Test public void testCreateCommonTransaction() { - UpdateBrokerageContract.Builder updateBrokerageContract = UpdateBrokerageContract.newBuilder(); - updateBrokerageContract.setOwnerAddress( - ByteString.copyFrom(Objects - .requireNonNull(decodeFromBase58Check("TN3zfjYUmMFK3ZsHSsrdJoNRtGkQmZLBLz")))) - .setBrokerage(10); - Transaction.Builder transaction = Transaction.newBuilder(); - Transaction.raw.Builder raw = Transaction.raw.newBuilder(); - Transaction.Contract.Builder contract = Transaction.Contract.newBuilder(); - contract.setType(Transaction.Contract.ContractType.UpdateBrokerageContract) - .setParameter(Any.pack(updateBrokerageContract.build())); - raw.addContract(contract.build()); - transaction.setRawData(raw.build()); - assertNotNull(blockingStubFull.createCommonTransaction(transaction.build())); + Transaction transaction = Transaction.newBuilder() + .setRawData(Transaction.raw.newBuilder().addContract(brokerageContract())) + .build(); + assertNotNull(blockingStubFull.createCommonTransaction(transaction)); + } + + @Test + public void testCreateCommonTransactionRejectsMultipleContracts() { + Transaction transaction = Transaction.newBuilder() + .setRawData(Transaction.raw.newBuilder() + .addContract(brokerageContract()) + .addContract(brokerageContract())) + .build(); + Return result = blockingStubFull.createCommonTransaction(transaction).getResult(); + Assert.assertEquals(response_code.CONTRACT_VALIDATE_ERROR, result.getCode()); + Assert.assertEquals("Contract validate error : contract size should be exactly 1," + + " this is extend feature ,actual :2", result.getMessage().toStringUtf8()); + } + + @Test + public void testCreateCommonTransactionRejectsEmptyContractList() { + Return result = blockingStubFull.createCommonTransaction(Transaction.getDefaultInstance()) + .getResult(); + Assert.assertEquals(response_code.CONTRACT_VALIDATE_ERROR, result.getCode()); + Assert.assertEquals("Contract validate error : No contract!", + result.getMessage().toStringUtf8()); } @Test diff --git a/framework/src/test/java/org/tron/core/services/http/UtilTest.java b/framework/src/test/java/org/tron/core/services/http/UtilTest.java index c619fd0de54..4f3003732ba 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilTest.java @@ -1,10 +1,13 @@ package org.tron.core.services.http; import com.google.protobuf.ByteString; +import java.nio.charset.StandardCharsets; import javax.annotation.Resource; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.springframework.mock.web.MockHttpServletResponse; +import org.tron.api.GrpcAPI.Return; import org.tron.api.GrpcAPI.TransactionApprovedList; import org.tron.api.GrpcAPI.TransactionSignWeight; import org.tron.common.BaseTest; @@ -12,7 +15,10 @@ import org.tron.common.utils.ByteArray; import org.tron.core.Wallet; import org.tron.core.capsule.AccountCapsule; +import org.tron.core.capsule.TransactionCapsule; import org.tron.core.config.args.Args; +import org.tron.core.exception.ContractSizeNotEqualToOneException; +import org.tron.core.exception.ContractValidateException; import org.tron.core.utils.TransactionUtil; import org.tron.json.JSONObject; import org.tron.protos.Protocol; @@ -22,15 +28,29 @@ public class UtilTest extends BaseTest { private static final String OWNER_ADDRESS; + private static final String TRANSFER_CONTRACT = "{\"parameter\":{\"value\":{\"amount\":10," + + "\"owner_address\":\"41c076305e35aea1fe45a772fcaaab8a36e87bdb55\"," + + "\"to_address\":\"415624c12e308b03a1a6b21d9b86e3942fac1ab92b\"}," + + "\"type_url\":\"type.googleapis.com/protocol.TransferContract\"}," + + "\"type\":\"TransferContract\"}"; + private static final String TWO_CONTRACTS_MESSAGE = + "contract size should be exactly 1, this is extend feature ,actual :2"; @Resource private Wallet wallet; @Resource private TransactionUtil transactionUtil; + @Resource + private BroadcastServlet broadcastServlet; + @Resource + private GetTransactionSignWeightServlet getTransactionSignWeightServlet; + @Resource + private GetTransactionApprovedListServlet getTransactionApprovedListServlet; static { OWNER_ADDRESS = Wallet.getAddressPreFixString() + "c076305e35aea1fe45a772fcaaab8a36e87bdb55"; Args.setParam(new String[] {"-d", dbPath()}, TestConstants.TEST_CONF); + Args.getInstance().setMinEffectiveConnection(0); } @Before @@ -47,7 +67,7 @@ public void setUp() { } @Test - public void testPackTransactionWithInvalidType() { + public void testPackTransactionWithInvalidType() throws Exception { String strTransaction = "{\n" + " \"visible\": false,\n" @@ -155,7 +175,7 @@ public void testCheckBodySizeUsesHttpLimit() throws Exception { } @Test - public void testPackTransaction() { + public void testPackTransaction() throws Exception { String strTransaction = "{\n" + " \"visible\": false,\n" + " \"signature\": [\n" @@ -236,7 +256,7 @@ public void testPackCreateSmartContractOmitsNullAbiOutputs() throws Exception { Assert.assertEquals(0, contract.getNewContract().getAbi().getEntrys(0).getOutputsCount()); } - private Transaction buildTooManySigsTransaction() { + private Transaction buildTooManySigsTransaction() throws Exception { String strTransaction = "{\n" + " \"visible\": false,\n" + " \"txID\": \"fc33817936b06e50d4b6f1797e62f52d69af6c0da580a607241a9c03a48e390e\",\n" @@ -275,7 +295,7 @@ private Transaction buildTooManySigsTransaction() { } @Test - public void testPrintApprovedListTooManySigsHttpPath() { + public void testPrintApprovedListTooManySigsHttpPath() throws Exception { Transaction transaction = buildTooManySigsTransaction(); TransactionApprovedList reply = wallet.getTransactionApprovedList(transaction); Assert.assertEquals(TransactionApprovedList.Result.response_code.OTHER_ERROR, @@ -289,7 +309,7 @@ public void testPrintApprovedListTooManySigsHttpPath() { } @Test - public void testPrintSignWeightTooManySigsHttpPath() { + public void testPrintSignWeightTooManySigsHttpPath() throws Exception { Transaction transaction = buildTooManySigsTransaction(); TransactionSignWeight reply = transactionUtil.getTransactionSignWeight(transaction); Assert.assertEquals(TransactionSignWeight.Result.response_code.OTHER_ERROR, @@ -301,4 +321,140 @@ public void testPrintSignWeightTooManySigsHttpPath() { Assert.assertTrue(jsonObject.getJSONObject("result").getString("message") .contains("too many signatures")); } + + private static String transactionJson(String contracts) { + return "{\"raw_data\":{\"contract\":" + contracts + ",\"ref_block_bytes\":\"d8ed\"," + + "\"ref_block_hash\":\"2e066c3259e756f5\",\"expiration\":1651906644000," + + "\"timestamp\":1651906586162}}"; + } + + private static Transaction twoContractTransaction() throws Exception { + Transaction transaction = + Util.packTransaction(transactionJson("[" + TRANSFER_CONTRACT + "]"), false); + Transaction.raw rawData = transaction.getRawData(); + return transaction.toBuilder() + .setRawData(rawData.toBuilder().addContract(rawData.getContract(0))) + .build(); + } + + private static void assertMultipleContractsReply(MockHttpServletResponse response) + throws Exception { + JSONObject reply = JSONObject.parseObject(response.getContentAsString()); + Assert.assertNull(reply.getJSONObject("transaction")); + JSONObject result = reply.getJSONObject("result"); + Assert.assertEquals("OTHER_ERROR", result.getString("code")); + Assert.assertEquals("Invalid transaction: " + TWO_CONTRACTS_MESSAGE, + result.getString("message")); + } + + @Test + public void testPackTransactionRejectsMultipleContracts() { + String strTransaction = + transactionJson("[" + TRANSFER_CONTRACT + "," + TRANSFER_CONTRACT + "]"); + ContractSizeNotEqualToOneException e = Assert.assertThrows( + ContractSizeNotEqualToOneException.class, + () -> Util.packTransaction(strTransaction, false)); + Assert.assertEquals(TWO_CONTRACTS_MESSAGE, e.getMessage()); + } + + @Test + public void testServletsRejectMultipleContracts() throws Exception { + String twoContracts = + transactionJson("[" + TRANSFER_CONTRACT + "," + TRANSFER_CONTRACT + "]"); + + MockHttpServletResponse response = BaseHttpTest.newResponse(); + broadcastServlet.doPost(BaseHttpTest.postRequest(twoContracts), response); + JSONObject broadcast = JSONObject.parseObject(response.getContentAsString()); + Assert.assertEquals("CONTRACT_VALIDATE_ERROR", broadcast.getString("code")); + Assert.assertEquals("Contract validate error : " + TWO_CONTRACTS_MESSAGE, + new String(ByteArray.fromHexString(broadcast.getString("message")), + StandardCharsets.UTF_8)); + Assert.assertEquals("", broadcast.getString("txid")); + + response = BaseHttpTest.newResponse(); + getTransactionSignWeightServlet.doPost(BaseHttpTest.postRequest(twoContracts), response); + assertMultipleContractsReply(response); + + response = BaseHttpTest.newResponse(); + getTransactionApprovedListServlet.doPost(BaseHttpTest.postRequest(twoContracts), response); + assertMultipleContractsReply(response); + } + + @Test + public void testPackTransactionKeepsEmptyContractList() throws Exception { + Transaction transaction = Util.packTransaction(transactionJson("[]"), false); + Assert.assertEquals(0, transaction.getRawData().getContractCount()); + + TransactionSignWeight signWeight = transactionUtil.getTransactionSignWeight(transaction); + Assert.assertEquals("Invalid transaction: no valid contract", + signWeight.getResult().getMessage()); + Assert.assertTrue(signWeight.hasTransaction()); + + TransactionApprovedList approvedList = wallet.getTransactionApprovedList(transaction); + Assert.assertEquals("Invalid transaction: no valid contract", + approvedList.getResult().getMessage()); + Assert.assertTrue(approvedList.hasTransaction()); + } + + @Test + public void testSignWeightAndApprovedListRejectMultipleContracts() throws Exception { + Transaction transaction = twoContractTransaction(); + + TransactionSignWeight signWeight = transactionUtil.getTransactionSignWeight(transaction); + Assert.assertEquals(TransactionSignWeight.Result.response_code.OTHER_ERROR, + signWeight.getResult().getCode()); + Assert.assertEquals("Invalid transaction: " + TWO_CONTRACTS_MESSAGE, + signWeight.getResult().getMessage()); + Assert.assertFalse(signWeight.hasTransaction()); + + TransactionApprovedList approvedList = wallet.getTransactionApprovedList(transaction); + Assert.assertEquals(TransactionApprovedList.Result.response_code.OTHER_ERROR, + approvedList.getResult().getCode()); + Assert.assertEquals("Invalid transaction: " + TWO_CONTRACTS_MESSAGE, + approvedList.getResult().getMessage()); + Assert.assertFalse(approvedList.hasTransaction()); + } + + @Test + public void testBroadcastRejectsMultipleContracts() throws Exception { + Transaction transaction = twoContractTransaction(); + String txId = new TransactionCapsule(transaction).getTransactionId().toString(); + + Return result = wallet.broadcastTransaction(transaction); + Assert.assertEquals(Return.response_code.CONTRACT_VALIDATE_ERROR, result.getCode()); + Assert.assertEquals("Contract validate error : tx " + txId + " " + TWO_CONTRACTS_MESSAGE, + result.getMessage().toStringUtf8()); + } + + @Test + public void testShieldTransactionHashRejectsMultipleContracts() throws Exception { + Transaction transaction = twoContractTransaction(); + boolean allowShieldedTransactionApi = Args.getInstance().isAllowShieldedTransactionApi(); + Args.getInstance().setAllowShieldedTransactionApi(true); + try { + ContractValidateException e = Assert.assertThrows(ContractValidateException.class, + () -> wallet.getShieldTransactionHash(transaction)); + Assert.assertEquals(TWO_CONTRACTS_MESSAGE, e.getMessage()); + } finally { + Args.getInstance().setAllowShieldedTransactionApi(allowShieldedTransactionApi); + } + } + + @Test + public void testServletsReportErrorWhenTransactionCannotBeParsed() throws Exception { + String unparsable = "{\"raw_data\":{\"contract\":[{}],\"timestamp\":\"bad\"}}"; + String error = "{\"Error\":\"internal server error\"}"; + + MockHttpServletResponse response = BaseHttpTest.newResponse(); + broadcastServlet.doPost(BaseHttpTest.postRequest(unparsable), response); + Assert.assertEquals(error, response.getContentAsString().trim()); + + response = BaseHttpTest.newResponse(); + getTransactionSignWeightServlet.doPost(BaseHttpTest.postRequest(unparsable), response); + Assert.assertEquals(error, response.getContentAsString().trim()); + + response = BaseHttpTest.newResponse(); + getTransactionApprovedListServlet.doPost(BaseHttpTest.postRequest(unparsable), response); + Assert.assertEquals(error, response.getContentAsString().trim()); + } }