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
204 changes: 204 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,204 @@
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",
// Legacy Solidity alias for bytes1; pre-0.8 source could emit this in
// hand-written or older-tool ABIs. Modern solc canonicalizes to bytes1.
"byte");

private AbiValidator() {
}

public static void validate(ABI abi) throws ContractValidateException {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is validate(ABI abi) too strict? It may reject legitimate ABIs generated by older or non-standard compilers.

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()) {
// Pre-0.4.16 solc emitted fallback as `type: function, name: "" `.
// Tolerate the legacy shape (no inputs/outputs) and count it
// toward fallbackCount so duplicate fallbacks are still caught.
if (entry.getInputsCount() > 0 || entry.getOutputsCount() > 0) {
throw new ContractValidateException(String.format(
"abi entry #%d: function must have a name", i));
}
fallbackCount++;
}
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";
}
if (!raw.equals(raw.trim())) {
return "type must not contain leading/trailing whitespace";
}
String t = raw;

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()) {
// Solidity aliases: uint = uint256, int = int256. Hand-written and
// pre-canonicalization ABIs may carry the shorthand; modern solc
// already emits the explicit width.
return null;
}
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
16 changes: 14 additions & 2 deletions framework/src/test/java/org/tron/common/runtime/TvmTestUtils.java
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,10 @@ private static SmartContract.ABI.Entry.EntryType getEntryType(String type) {
return SmartContract.ABI.Entry.EntryType.Event;
case "fallback":
return SmartContract.ABI.Entry.EntryType.Fallback;
case "receive":
return SmartContract.ABI.Entry.EntryType.Receive;
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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch. Adding case "receive" to both TvmTestUtils.getEntryType and PublicMethod.getEntryType so the validator's Receive handling (payability + IO checks) is actually reachable from test helpers — otherwise any test that exercises a receive entry bounces off "unknown entry type" before it hits the rules the validator is there to enforce. Folds into the same commit as the whitespace fix above.

default:
return SmartContract.ABI.Entry.EntryType.UNRECOGNIZED;
}
Expand Down Expand Up @@ -541,7 +546,8 @@ public static SmartContract.ABI jsonStr2Abi(String jsonStr) {
logger.error("No type!");
return null;
}
if (!type.equalsIgnoreCase("fallback") && null == inputs) {
if (!type.equalsIgnoreCase("fallback") && !type.equalsIgnoreCase("receive")
&& null == inputs) {
logger.error("No inputs!");
return null;
}
Expand Down Expand Up @@ -603,7 +609,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.tron.common.runtime;

import static org.junit.Assert.assertEquals;

import org.junit.Test;
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;

public class TvmTestUtilsTest {

@Test
public void jsonStr2AbiAcceptsReceiveEntry() {
String json = "[{\"stateMutability\":\"payable\",\"type\":\"receive\"}]";
ABI abi = TvmTestUtils.jsonStr2Abi(json);
assertEquals(1, abi.getEntrysCount());
Entry entry = abi.getEntrys(0);
assertEquals(EntryType.Receive, entry.getType());
assertEquals(0, entry.getInputsCount());
assertEquals(0, entry.getOutputsCount());
}
}
15 changes: 13 additions & 2 deletions 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 @@ -129,7 +131,8 @@ public static SmartContractOuterClass.SmartContract.ABI jsonStr2Abi(String jsonS
logger.error("No type!");
return null;
}
if (!type.equalsIgnoreCase("fallback") && null == inputs) {
if (!type.equalsIgnoreCase("fallback") && !type.equalsIgnoreCase("receive")
&& null == inputs) {
logger.error("No inputs!");
return null;
}
Expand Down Expand Up @@ -195,7 +198,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 All @@ -210,6 +219,8 @@ public static SmartContractOuterClass.SmartContract.ABI jsonStr2Abi(String jsonS
return SmartContractOuterClass.SmartContract.ABI.Entry.EntryType.Event;
case "fallback":
return SmartContractOuterClass.SmartContract.ABI.Entry.EntryType.Fallback;
case "receive":
return SmartContractOuterClass.SmartContract.ABI.Entry.EntryType.Receive;
case "error":
return SmartContractOuterClass.SmartContract.ABI.Entry.EntryType.Error;
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.tron.common.utils;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import org.junit.Test;
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;

public class PublicMethodTest {

@Test
public void jsonStr2AbiAcceptsReceiveEntry() {
String json = "[{\"stateMutability\":\"payable\",\"type\":\"receive\"}]";
ABI abi = PublicMethod.jsonStr2Abi(json);
assertEquals(1, abi.getEntrysCount());
Entry entry = abi.getEntrys(0);
assertEquals(EntryType.Receive, entry.getType());
assertEquals(0, entry.getInputsCount());
assertEquals(0, entry.getOutputsCount());
}

@Test
public void getEntryTypeMapsReceive() {
assertEquals(EntryType.Receive, PublicMethod.getEntryType("receive"));
}

@Test
public void getEntryTypeUnknownStaysUnrecognized() {
assertTrue(PublicMethod.getEntryType("weirdo") == EntryType.UNRECOGNIZED);
}
}
Loading
Loading