Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions actuator/src/main/java/org/tron/core/utils/AbiValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package org.tron.core.utils;

import com.google.common.collect.ImmutableSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.tron.core.exception.ContractValidateException;
import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI;
import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry;
import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry.EntryType;
import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry.Param;
import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry.StateMutabilityType;

public final class AbiValidator {

private static final Pattern INT_TYPE = Pattern.compile("^(u?int)(\\d*)$");
private static final Pattern BYTES_N_TYPE = Pattern.compile("^bytes(\\d+)$");
private static final Pattern ARRAY_SUFFIX = Pattern.compile("\\[(\\d*)]$");

private static final Set<String> BASE_TYPES = ImmutableSet.of(
"address", "bool", "string", "bytes", "function", "tuple", "trcToken");

private AbiValidator() {
}

public static void validate(ABI abi) throws ContractValidateException {
if (abi == null || abi.getEntrysCount() == 0) {
return;
}

int constructorCount = 0;
int fallbackCount = 0;
int receiveCount = 0;

for (int i = 0; i < abi.getEntrysCount(); i++) {
Entry entry = abi.getEntrys(i);
EntryType type = entry.getType();

if (type == EntryType.UnknownEntryType || type == EntryType.UNRECOGNIZED) {
throw new ContractValidateException(
String.format("abi entry #%d: unknown entry type", i));
}

switch (type) {
case Constructor:
constructorCount++;
break;
case Fallback:
fallbackCount++;
if (entry.getInputsCount() > 0 || entry.getOutputsCount() > 0) {
throw new ContractValidateException(String.format(
"abi entry #%d: fallback function must not have inputs or outputs", i));
}
break;
case Receive:
receiveCount++;
if (entry.getInputsCount() > 0 || entry.getOutputsCount() > 0) {
throw new ContractValidateException(String.format(
"abi entry #%d: receive function must not have inputs or outputs", i));
}
if (entry.getStateMutability() != StateMutabilityType.Payable && !entry.getPayable()) {
throw new ContractValidateException(String.format(
"abi entry #%d: receive function must be payable", i));
}
break;
case Function:
if (entry.getName().isEmpty()) {
throw new ContractValidateException(String.format(
"abi entry #%d: function must have a name", i));
}
break;
case Event:
if (entry.getName().isEmpty() && !entry.getAnonymous()) {
throw new ContractValidateException(String.format(
"abi entry #%d: non-anonymous event must have a name", i));
}
break;
case Error:
if (entry.getName().isEmpty()) {
throw new ContractValidateException(String.format(
"abi entry #%d: error must have a name", i));
}
break;
default:
break;
}

validateParams(i, "inputs", entry.getInputsList());
validateParams(i, "outputs", entry.getOutputsList());
}

if (constructorCount > 1) {
throw new ContractValidateException("abi: only one constructor is allowed");
}
if (fallbackCount > 1) {
throw new ContractValidateException("abi: only one fallback function is allowed");
}
if (receiveCount > 1) {
throw new ContractValidateException("abi: only one receive function is allowed");
}
}

private static void validateParams(int entryIdx, String side, List<Param> params)
throws ContractValidateException {
for (int j = 0; j < params.size(); j++) {
String type = params.get(j).getType();
String reason = checkType(type);
if (reason != null) {
throw new ContractValidateException(String.format(
"abi entry #%d %s[%d] type '%s': %s", entryIdx, side, j, type, reason));
}
}
}

// Returns null when the type is acceptable, otherwise a short failure reason.
private static String checkType(String raw) {
if (raw == null || raw.isEmpty()) {
return "type must not be empty";
}
String t = raw.trim();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice helper layout — checkType returning either null or a short reason makes the call site read very cleanly, and isolating the regex/base-type tables at the top is great for future tweaking. ✨

One subtle gap: t = raw.trim() lets a type like " uint256 " pass validation, but the validator never normalizes the proto — the on-chain Param.type keeps the original whitespace. Per the linked issue's "fewer inconsistencies in stored ABI metadata" goal, would it make sense to reject whitespace-padded types upfront, e.g.:

if (!raw.equals(raw.trim())) {
  return "type must not contain leading/trailing whitespace";
}

That way the validator's "this passed" implies "what gets persisted is exactly what got checked", which matches the stated goal of strict-matching tooling compatibility.


while (true) {
Matcher m = ARRAY_SUFFIX.matcher(t);
if (!m.find()) {
break;
}
String n = m.group(1);
if (!n.isEmpty()) {
long size;
try {
size = Long.parseLong(n);
} catch (NumberFormatException nfe) {
return "malformed array size";
}
if (size <= 0) {
return "array size must be positive";
}
}
t = t.substring(0, t.length() - m.group().length());
}

if (t.indexOf('[') >= 0 || t.indexOf(']') >= 0) {
return "malformed array brackets";
}

if (BASE_TYPES.contains(t)) {
return null;
}

Matcher mi = INT_TYPE.matcher(t);
if (mi.matches()) {
String width = mi.group(2);
if (width.isEmpty()) {
return "shorthand uint/int is not allowed, use uintN/intN";
}
int w;
try {
w = Integer.parseInt(width);
} catch (NumberFormatException nfe) {
return "invalid integer width";
}
if (w < 8 || w > 256 || (w % 8) != 0) {
return "integer width must be a multiple of 8 in [8, 256]";
}
return null;
}

Matcher mb = BYTES_N_TYPE.matcher(t);
if (mb.matches()) {
int n;
try {
n = Integer.parseInt(mb.group(1));
} catch (NumberFormatException nfe) {
return "invalid bytesN size";
}
if (n < 1 || n > 32) {
return "bytesN size must be in [1, 32]";
}
return null;
}

if (t.startsWith("fixed") || t.startsWith("ufixed")) {
return "fixed/ufixed types are not supported";
}

return "unknown base type";
}
}
2 changes: 2 additions & 0 deletions framework/src/main/java/org/tron/core/Wallet.java
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@
import org.tron.core.store.StoreFactory;
import org.tron.core.store.VotesStore;
import org.tron.core.store.WitnessStore;
import org.tron.core.utils.AbiValidator;
import org.tron.core.utils.TransactionUtil;
import org.tron.core.vm.program.Program;
import org.tron.core.zen.ShieldedTRC20ParametersBuilder;
Expand Down Expand Up @@ -498,6 +499,7 @@ public TransactionCapsule createTransactionCapsule(com.google.protobuf.Message m
if (percent < 0 || percent > 100) {
throw new ContractValidateException("percent must be >= 0 and <= 100");
}
AbiValidator.validate(contract.getNewContract().getAbi());
}
setTransaction(trx);
return trx;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.tron.core.exception.ReceiptCheckErrException;
import org.tron.core.exception.VMIllegalException;
import org.tron.core.store.StoreFactory;
import org.tron.core.utils.AbiValidator;
import org.tron.core.vm.repository.Repository;
import org.tron.core.vm.repository.RepositoryImpl;

Expand Down Expand Up @@ -489,6 +490,8 @@ private static SmartContract.ABI.Entry.EntryType getEntryType(String type) {
return SmartContract.ABI.Entry.EntryType.Event;
case "fallback":
return SmartContract.ABI.Entry.EntryType.Fallback;
case "error":
return SmartContract.ABI.Entry.EntryType.Error;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great call adding case "error" here — without it any test ABI carrying an error entry would fall into UNRECOGNIZED and now hit the new "unknown entry type" rejection. Nice symmetry with PublicMethod.getEntryType which already had it. 🎯

Minor follow-up: both this helper and PublicMethod.getEntryType still omit case "receive", even though the new validator has dedicated handling for Receive (payability + IO checks). Any test that wants to exercise a receive entry through these helpers will still get UNRECOGNIZED and bounce off the new rejection. Worth adding the one-line case in both files while you're here, so the validator's full surface is reachable from test utilities?

default:
return SmartContract.ABI.Entry.EntryType.UNRECOGNIZED;
}
Expand Down Expand Up @@ -603,7 +606,13 @@ public static SmartContract.ABI jsonStr2Abi(String jsonStr) {
abiBuilder.addEntrys(entryBuilder.build());
}

return abiBuilder.build();
SmartContract.ABI abi = abiBuilder.build();
try {
AbiValidator.validate(abi);
} catch (ContractValidateException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
return abi;
}


Expand Down
10 changes: 9 additions & 1 deletion framework/src/test/java/org/tron/common/utils/PublicMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import org.tron.common.crypto.sm2.SM2Signer;
import org.tron.common.utils.client.utils.TransactionUtils;
import org.tron.core.Wallet;
import org.tron.core.exception.ContractValidateException;
import org.tron.core.utils.AbiValidator;
import org.tron.protos.Protocol;
import org.tron.protos.contract.BalanceContract;
import org.tron.protos.contract.SmartContractOuterClass;
Expand Down Expand Up @@ -195,7 +197,13 @@ public static SmartContractOuterClass.SmartContract.ABI jsonStr2Abi(String jsonS
abiBuilder.addEntrys(entryBuilder.build());
}

return abiBuilder.build();
SmartContractOuterClass.SmartContract.ABI abi = abiBuilder.build();
try {
AbiValidator.validate(abi);
} catch (ContractValidateException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
return abi;
}

/** constructor. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.tron.core.services.http;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.tron.common.utils.client.utils.HttpMethed.createRequest;

import javax.annotation.Resource;
import org.apache.http.client.methods.HttpPost;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.tron.common.BaseTest;
import org.tron.common.TestConstants;
import org.tron.core.config.args.Args;

public class DeployContractServletTest extends BaseTest {

static {
Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF);
}

@Resource
private DeployContractServlet deployContractServlet;

private static final String OWNER_ADDRESS = "A099357684BC659F5166046B56C95A0E99F1265CBD";

private MockHttpServletResponse postWithAbi(String abi) {
String body = "{"
+ "\"owner_address\":\"" + OWNER_ADDRESS + "\","
+ "\"name\":\"abi_validation_test\","
+ "\"bytecode\":\"00\","
+ "\"abi\":" + abi
+ "}";
MockHttpServletRequest request = createRequest(HttpPost.METHOD_NAME);
request.setContentType("application/json");
request.setContent(body.getBytes(UTF_8));
MockHttpServletResponse response = new MockHttpServletResponse();
deployContractServlet.doPost(request, response);
return response;
}

private static void assertRejected(MockHttpServletResponse response, String snippet)
throws Exception {
Assert.assertEquals(200, response.getStatus());
String body = response.getContentAsString();
Assert.assertTrue("expected error containing '" + snippet + "', got: " + body,
body.contains("Error") && body.contains(snippet));
}

@Test
public void rejectsShorthandUint() throws Exception {
String abi = "[{\"type\":\"function\",\"name\":\"foo\","
+ "\"inputs\":[{\"name\":\"x\",\"type\":\"uint\"}],\"outputs\":[]}]";
assertRejected(postWithAbi(abi), "shorthand uint/int");
}

@Test
public void rejectsBadBytesN() throws Exception {
String abi = "[{\"type\":\"function\",\"name\":\"foo\","
+ "\"inputs\":[{\"name\":\"x\",\"type\":\"bytes33\"}],\"outputs\":[]}]";
assertRejected(postWithAbi(abi), "bytesN size");
}

@Test
public void rejectsDuplicateFallback() throws Exception {
String abi = "["
+ "{\"type\":\"fallback\",\"stateMutability\":\"payable\"},"
+ "{\"type\":\"fallback\",\"stateMutability\":\"payable\"}"
+ "]";
assertRejected(postWithAbi(abi), "only one fallback");
}

@Test
public void rejectsNonPayableReceive() throws Exception {
String abi = "[{\"type\":\"receive\",\"stateMutability\":\"nonpayable\"}]";
assertRejected(postWithAbi(abi), "must be payable");
}

@Test
public void rejectsFixedFamily() throws Exception {
String abi = "[{\"type\":\"function\",\"name\":\"foo\","
+ "\"inputs\":[{\"name\":\"x\",\"type\":\"fixed128x18\"}],\"outputs\":[]}]";
assertRejected(postWithAbi(abi), "fixed/ufixed");
}

@Test
public void acceptsTuplePermissively() throws Exception {
String abi = "[{\"type\":\"function\",\"name\":\"foo\","
+ "\"inputs\":[{\"name\":\"x\",\"type\":\"tuple\"},"
+ "{\"name\":\"y\",\"type\":\"tuple[]\"}],\"outputs\":[]}]";
MockHttpServletResponse response = postWithAbi(abi);
Assert.assertEquals(200, response.getStatus());
String body = response.getContentAsString();
Assert.assertFalse("tuple should not be rejected: " + body, body.contains("\"Error\""));
}
}
Loading
Loading