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 b2b2eeec8d..e6ccb4e4d1 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 @@ -620,6 +620,10 @@ protected static void mergeField(Tokenizer tokenizer, if (field != null) { tokenizer.consume(":"); + // Match protobuf JsonFormat: a field whose value is null is treated as absent. + if (tokenizer.tryConsume("null")) { + return; + } boolean array = tokenizer.tryConsume("["); if (array) { 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 6ef74ce2a1..75bb59d2e5 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -1488,6 +1488,52 @@ public void testBuildTransactionTransfer() { } } + @Test + public void testBuildCreateSmartContractAcceptsNullAbiOutputsOverHttp() { + fullNodeJsonRpcHttpService.start(); + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + JsonObject buildArgs = new JsonObject(); + buildArgs.addProperty("from", "0xabd4b9367799eaa3197fecb144eb71de1e049abc"); + buildArgs.addProperty("data", "608060405234801561001057600080fd5b50"); + buildArgs.addProperty("gas", "0x3b9aca00"); + buildArgs.addProperty("abi", "[{\"inputs\":[],\"name\":\"test\",\"outputs\":null," + + "\"type\":\"function\"}]"); + JsonArray params = new JsonArray(); + params.add(buildArgs); + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("jsonrpc", "2.0"); + requestBody.addProperty("method", "buildTransaction"); + requestBody.add("params", params); + requestBody.addProperty("id", 1); + + HttpPost httpPost = new HttpPost("http://127.0.0.1:" + + CommonParameter.getInstance().getJsonRpcHttpFullNodePort() + "/jsonrpc"); + httpPost.addHeader("Content-Type", "application/json"); + httpPost.setEntity(new StringEntity(requestBody.toString())); + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + String resp = EntityUtils.toString(response.getEntity()); + JSONObject json = JSON.parseObject(resp); + Assert.assertNull(resp, json.getJSONObject("error")); + JSONObject tx = json.getJSONObject("result").getJSONObject("transaction"); + Assert.assertNotNull("transaction must be a JSON object", tx); + + JSONArray contracts = tx.getJSONObject("raw_data").getJSONArray("contract"); + Assert.assertEquals(1, contracts.size()); + JSONObject contract = contracts.getJSONObject(0); + Assert.assertEquals("CreateSmartContract", contract.getString("type")); + JSONObject value = contract.getJSONObject("parameter").getJSONObject("value"); + JSONObject abi = value.getJSONObject("new_contract").getJSONObject("abi"); + JSONArray entrys = abi.getJSONArray("entrys"); + Assert.assertEquals(1, entrys.size()); + Assert.assertFalse(entrys.getJSONObject(0).containsKey("outputs")); + } + } catch (Exception e) { + Assert.fail(e.getMessage()); + } finally { + fullNodeJsonRpcHttpService.stop(); + } + } + @Test public void testBuildTransactionRejectsDeeplyNestedAbi() { // A deeply nested ABI must surface as invalid-params (-32602), not as a generic 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 83fb64880c..703f278c89 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 @@ -55,4 +55,31 @@ && addressEquals(((CreateSmartContract) c).getOwnerAddress(), eq(Protocol.Transaction.Contract.ContractType.CreateSmartContract)); assertTransactionResponse(response); } + + @Test + public void testDeployContractOmitsNullAbiOutputs() throws Exception { + String jsonParam = "{" + + "\"owner_address\": \"4199357684BC659F5166046B56C95A0E99F1265CD1\"," + + "\"name\": \"TestContract\"," + + "\"abi\": [{\"inputs\":[],\"name\":\"test\",\"outputs\":null," + + "\"type\":\"function\"}]," + + "\"bytecode\": \"608060405234801561001057600080fd5b50\"," + + "\"fee_limit\": 1000000000," + + "\"call_value\": 0," + + "\"consume_user_resource_percent\": 100," + + "\"origin_energy_limit\": 10000000" + + "}"; + MockHttpServletRequest request = postRequest(jsonParam); + + MockHttpServletResponse response = newResponse(); + servlet.doPost(request, response); + assertEquals(200, response.getStatus()); + verify(wallet).createTransactionCapsule( + argThat(c -> c instanceof CreateSmartContract + && ((CreateSmartContract) c).getNewContract().getAbi().getEntrysCount() == 1 + && ((CreateSmartContract) c).getNewContract().getAbi().getEntrys(0) + .getOutputsCount() == 0), + eq(Protocol.Transaction.Contract.ContractType.CreateSmartContract)); + assertTransactionResponse(response); + } } diff --git a/framework/src/test/java/org/tron/core/services/http/GetAccountServletTest.java b/framework/src/test/java/org/tron/core/services/http/GetAccountServletTest.java index 31cfc174a7..1c1d42c9a5 100644 --- a/framework/src/test/java/org/tron/core/services/http/GetAccountServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/GetAccountServletTest.java @@ -46,6 +46,20 @@ public void testGetAccountPost() throws Exception { assertTrue("Should contain address", content.contains("address")); } + @Test + public void testGetAccountPostNullAddressKeepsDefault() throws Exception { + MockHttpServletRequest request = postRequest("{\"address\": null}"); + + MockHttpServletResponse response = newResponse(); + servlet.doPost(request, response); + assertEquals(200, response.getStatus()); + verify(wallet).getAccount(argThat(req -> req != null + && req.getAddress().equals(ByteString.EMPTY))); + String content = response.getContentAsString(); + assertFalse("Should not contain error", content.contains("\"Error\"")); + assertTrue("Should contain address", content.contains("address")); + } + @Test public void testGetAccountGet() throws Exception { MockHttpServletRequest request = getRequest("address", addrStr); diff --git a/framework/src/test/java/org/tron/core/services/http/JsonFormatTest.java b/framework/src/test/java/org/tron/core/services/http/JsonFormatTest.java index e38af6ead2..46d1743c5b 100644 --- a/framework/src/test/java/org/tron/core/services/http/JsonFormatTest.java +++ b/framework/src/test/java/org/tron/core/services/http/JsonFormatTest.java @@ -1,6 +1,7 @@ package org.tron.core.services.http; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -19,6 +20,9 @@ import org.mockito.Mockito; import org.tron.core.Constant; import org.tron.protos.Protocol; +import org.tron.protos.contract.ProposalContract.ProposalCreateContract; +import org.tron.protos.contract.SmartContractOuterClass.CreateSmartContract; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry; public class JsonFormatTest { @After @@ -280,6 +284,25 @@ public void testTrailingCommaInKnownRepeatedField() throws Exception { assertEquals(ByteString.copyFrom(new byte[] {1}), proposal.getApprovals(1)); } + @Test + public void testKnownFieldNullIsSkipped() throws Exception { + Protocol.HelloMessage.Builder hello = Protocol.HelloMessage.newBuilder(); + JsonFormat.merge("{\"address\":null}", hello, false); + assertEquals(ByteString.EMPTY, hello.getAddress()); + + Entry.Builder entry = Entry.newBuilder(); + JsonFormat.merge("{\"outputs\":null}", entry, false); + assertEquals(0, entry.getOutputsCount()); + + CreateSmartContract.Builder contract = CreateSmartContract.newBuilder(); + JsonFormat.merge("{\"new_contract\":null}", contract, false); + assertFalse(contract.hasNewContract()); + + ProposalCreateContract.Builder proposal = ProposalCreateContract.newBuilder(); + JsonFormat.merge("{\"parameters\":null}", proposal, false); + assertTrue(proposal.getParametersMap().isEmpty()); + } + @Test public void testKnownRepeatedPrimitiveFieldRejectsNestedArray() { Protocol.Proposal.Builder builder = Protocol.Proposal.newBuilder(); @@ -300,6 +323,16 @@ public void testKnownRepeatedMessageFieldRejectsNestedArray() { assertTrue(e.getMessage().contains("Expected \"{\".")); } + @Test + public void testKnownRepeatedMessageFieldRejectsNullElement() { + Entry.Builder builder = Entry.newBuilder(); + + JsonFormat.ParseException e = assertThrows(JsonFormat.ParseException.class, + () -> JsonFormat.merge("{\"outputs\":[null]}", builder, false)); + + assertTrue(e.getMessage().contains("Expected \"{\".")); + } + @Test public void testMissingFieldRejectsTrailingCommaInNestedObject() { Protocol.HelloMessage.Builder builder = Protocol.HelloMessage.newBuilder(); 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 49b8f848e3..c619fd0de5 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 @@ -17,6 +17,7 @@ import org.tron.json.JSONObject; import org.tron.protos.Protocol; import org.tron.protos.Protocol.Transaction; +import org.tron.protos.contract.SmartContractOuterClass.CreateSmartContract; public class UtilTest extends BaseTest { @@ -191,6 +192,50 @@ public void testPackTransaction() { Assert.assertNotNull(txSignWeight); } + @Test + public void testPackCreateSmartContractOmitsNullAbiOutputs() throws Exception { + String strTransaction = "{\n" + + " \"visible\": false,\n" + + " \"raw_data\": {\n" + + " \"contract\": [\n" + + " {\n" + + " \"parameter\": {\n" + + " \"value\": {\n" + + " \"owner_address\":\"41c076305e35aea1fe45a772fcaaab8a36e87bdb55\"," + + " \"new_contract\": {\n" + + " \"origin_address\":" + + " \"41c076305e35aea1fe45a772fcaaab8a36e87bdb55\"," + + " \"name\":\"TestContract\"," + + " \"abi\": {\n" + + " \"entrys\": [\n" + + " {\"inputs\":[],\"name\":\"test\"," + + " \"outputs\":null,\"type\":\"function\"}\n" + + " ]\n" + + " }\n" + + " }\n" + + " },\n" + + " \"type_url\":" + + " \"type.googleapis.com/protocol.CreateSmartContract\"\n" + + " },\n" + + " \"type\": \"CreateSmartContract\"\n" + + " }\n" + + " ],\n" + + " \"ref_block_bytes\": \"d8ed\",\n" + + " \"ref_block_hash\": \"2e066c3259e756f5\",\n" + + " \"expiration\": 1651906644000,\n" + + " \"timestamp\": 1651906586162\n" + + " }\n" + + "}"; + + Transaction transaction = Util.packTransaction(strTransaction, false); + Assert.assertNotNull(transaction); + Assert.assertEquals(1, transaction.getRawData().getContractCount()); + CreateSmartContract contract = transaction.getRawData().getContract(0) + .getParameter().unpack(CreateSmartContract.class); + Assert.assertEquals(1, contract.getNewContract().getAbi().getEntrysCount()); + Assert.assertEquals(0, contract.getNewContract().getAbi().getEntrys(0).getOutputsCount()); + } + private Transaction buildTooManySigsTransaction() { String strTransaction = "{\n" + " \"visible\": false,\n" diff --git a/framework/src/test/java/org/tron/json/JsonTest.java b/framework/src/test/java/org/tron/json/JsonTest.java index eb9fed8ff2..f430188611 100644 --- a/framework/src/test/java/org/tron/json/JsonTest.java +++ b/framework/src/test/java/org/tron/json/JsonTest.java @@ -9,6 +9,7 @@ import static org.junit.Assert.assertTrue; import com.fasterxml.jackson.core.StreamReadConstraints; +import com.fasterxml.jackson.databind.node.NullNode; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Arrays; @@ -158,6 +159,22 @@ public void testToJSONString() { assertEquals("\"hi\"", JSON.toJSONString("hi", true)); } + @Test + public void testExplicitNullNodeSerializationIsPreserved() { + JSONObject parsedNull = JSON.parseObject("{\"a\":null,\"b\":1}"); + assertTrue(parsedNull.containsKey("a")); + assertNull(parsedNull.get("a")); + assertEquals("{\"a\":null,\"b\":1}", parsedNull.toJSONString()); + + JSONObject explicitNull = new JSONObject().put("a", NullNode.getInstance()).put("b", 1); + assertTrue(explicitNull.containsKey("a")); + assertEquals("{\"a\":null,\"b\":1}", explicitNull.toJSONString()); + + explicitNull.put("a", (Object) null); + assertFalse(explicitNull.containsKey("a")); + assertEquals("{\"b\":1}", explicitNull.toJSONString()); + } + @Test public void testJsonObjectGetters() { JSONObject o = JSON.parseObject(