Skip to content

Commit 1c4c06d

Browse files
committed
feat(vm): add ABI semantic validation for /wallet/deploycontract (#6674)
Add a dedicated AbiValidator and invoke it inside the existing CreateSmartContract branch of Wallet.createTransactionCapsule, so both the HTTP /wallet/deploycontract path and the gRPC deployContract path fail fast on malformed ABI input with a field-path-anchored error. Rules align with go-ethereum's accounts/abi parser: - reject `uint`/`int` shorthand and out-of-range `uintN`/`intN` widths - reject `bytesN` with N outside [1, 32] - reject the entire `fixed`/`ufixed` family - reject malformed array suffixes - reject duplicate `constructor`/`fallback`/`receive` - require `receive` to be payable (legacy `payable` flag honored) - reject `fallback`/`receive` carrying inputs or outputs - reject UnknownEntryType - require a name for `function`; require a name for `event` only when not `anonymous` - keep `tuple` / `tuple[]` / `tuple[N]` permissive, since the proto schema cannot represent `components` Also reuse the validator in PublicMethod.jsonStr2Abi and TvmTestUtils.jsonStr2Abi so test/util ABI parsing and request-handling share the same rule set.
1 parent f0a8f0f commit 1c4c06d

6 files changed

Lines changed: 590 additions & 2 deletions

File tree

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package org.tron.core.utils;
2+
3+
import com.google.common.collect.ImmutableSet;
4+
import java.util.List;
5+
import java.util.Set;
6+
import java.util.regex.Matcher;
7+
import java.util.regex.Pattern;
8+
import org.tron.core.exception.ContractValidateException;
9+
import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI;
10+
import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry;
11+
import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry.EntryType;
12+
import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry.Param;
13+
import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry.StateMutabilityType;
14+
15+
public final class AbiValidator {
16+
17+
private static final Pattern INT_TYPE = Pattern.compile("^(u?int)(\\d*)$");
18+
private static final Pattern BYTES_N_TYPE = Pattern.compile("^bytes(\\d+)$");
19+
private static final Pattern ARRAY_SUFFIX = Pattern.compile("\\[(\\d*)]$");
20+
21+
private static final Set<String> BASE_TYPES = ImmutableSet.of(
22+
"address", "bool", "string", "bytes", "function", "tuple", "trcToken");
23+
24+
private AbiValidator() {
25+
}
26+
27+
public static void validate(ABI abi) throws ContractValidateException {
28+
if (abi == null || abi.getEntrysCount() == 0) {
29+
return;
30+
}
31+
32+
int constructorCount = 0;
33+
int fallbackCount = 0;
34+
int receiveCount = 0;
35+
36+
for (int i = 0; i < abi.getEntrysCount(); i++) {
37+
Entry entry = abi.getEntrys(i);
38+
EntryType type = entry.getType();
39+
40+
if (type == EntryType.UnknownEntryType || type == EntryType.UNRECOGNIZED) {
41+
throw new ContractValidateException(
42+
String.format("abi entry #%d: unknown entry type", i));
43+
}
44+
45+
switch (type) {
46+
case Constructor:
47+
constructorCount++;
48+
break;
49+
case Fallback:
50+
fallbackCount++;
51+
if (entry.getInputsCount() > 0 || entry.getOutputsCount() > 0) {
52+
throw new ContractValidateException(String.format(
53+
"abi entry #%d: fallback function must not have inputs or outputs", i));
54+
}
55+
break;
56+
case Receive:
57+
receiveCount++;
58+
if (entry.getInputsCount() > 0 || entry.getOutputsCount() > 0) {
59+
throw new ContractValidateException(String.format(
60+
"abi entry #%d: receive function must not have inputs or outputs", i));
61+
}
62+
if (entry.getStateMutability() != StateMutabilityType.Payable && !entry.getPayable()) {
63+
throw new ContractValidateException(String.format(
64+
"abi entry #%d: receive function must be payable", i));
65+
}
66+
break;
67+
case Function:
68+
if (entry.getName().isEmpty()) {
69+
throw new ContractValidateException(String.format(
70+
"abi entry #%d: function must have a name", i));
71+
}
72+
break;
73+
case Event:
74+
if (entry.getName().isEmpty() && !entry.getAnonymous()) {
75+
throw new ContractValidateException(String.format(
76+
"abi entry #%d: non-anonymous event must have a name", i));
77+
}
78+
break;
79+
case Error:
80+
if (entry.getName().isEmpty()) {
81+
throw new ContractValidateException(String.format(
82+
"abi entry #%d: error must have a name", i));
83+
}
84+
break;
85+
default:
86+
break;
87+
}
88+
89+
validateParams(i, "inputs", entry.getInputsList());
90+
validateParams(i, "outputs", entry.getOutputsList());
91+
}
92+
93+
if (constructorCount > 1) {
94+
throw new ContractValidateException("abi: only one constructor is allowed");
95+
}
96+
if (fallbackCount > 1) {
97+
throw new ContractValidateException("abi: only one fallback function is allowed");
98+
}
99+
if (receiveCount > 1) {
100+
throw new ContractValidateException("abi: only one receive function is allowed");
101+
}
102+
}
103+
104+
private static void validateParams(int entryIdx, String side, List<Param> params)
105+
throws ContractValidateException {
106+
for (int j = 0; j < params.size(); j++) {
107+
String type = params.get(j).getType();
108+
String reason = checkType(type);
109+
if (reason != null) {
110+
throw new ContractValidateException(String.format(
111+
"abi entry #%d %s[%d] type '%s': %s", entryIdx, side, j, type, reason));
112+
}
113+
}
114+
}
115+
116+
// Returns null when the type is acceptable, otherwise a short failure reason.
117+
private static String checkType(String raw) {
118+
if (raw == null || raw.isEmpty()) {
119+
return "type must not be empty";
120+
}
121+
String t = raw.trim();
122+
123+
while (true) {
124+
Matcher m = ARRAY_SUFFIX.matcher(t);
125+
if (!m.find()) {
126+
break;
127+
}
128+
String n = m.group(1);
129+
if (!n.isEmpty()) {
130+
long size;
131+
try {
132+
size = Long.parseLong(n);
133+
} catch (NumberFormatException nfe) {
134+
return "malformed array size";
135+
}
136+
if (size <= 0) {
137+
return "array size must be positive";
138+
}
139+
}
140+
t = t.substring(0, t.length() - m.group().length());
141+
}
142+
143+
if (t.indexOf('[') >= 0 || t.indexOf(']') >= 0) {
144+
return "malformed array brackets";
145+
}
146+
147+
if (BASE_TYPES.contains(t)) {
148+
return null;
149+
}
150+
151+
Matcher mi = INT_TYPE.matcher(t);
152+
if (mi.matches()) {
153+
String width = mi.group(2);
154+
if (width.isEmpty()) {
155+
return "shorthand uint/int is not allowed, use uintN/intN";
156+
}
157+
int w;
158+
try {
159+
w = Integer.parseInt(width);
160+
} catch (NumberFormatException nfe) {
161+
return "invalid integer width";
162+
}
163+
if (w < 8 || w > 256 || (w % 8) != 0) {
164+
return "integer width must be a multiple of 8 in [8, 256]";
165+
}
166+
return null;
167+
}
168+
169+
Matcher mb = BYTES_N_TYPE.matcher(t);
170+
if (mb.matches()) {
171+
int n;
172+
try {
173+
n = Integer.parseInt(mb.group(1));
174+
} catch (NumberFormatException nfe) {
175+
return "invalid bytesN size";
176+
}
177+
if (n < 1 || n > 32) {
178+
return "bytesN size must be in [1, 32]";
179+
}
180+
return null;
181+
}
182+
183+
if (t.startsWith("fixed") || t.startsWith("ufixed")) {
184+
return "fixed/ufixed types are not supported";
185+
}
186+
187+
return "unknown base type";
188+
}
189+
}

framework/src/main/java/org/tron/core/Wallet.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@
209209
import org.tron.core.store.StoreFactory;
210210
import org.tron.core.store.VotesStore;
211211
import org.tron.core.store.WitnessStore;
212+
import org.tron.core.utils.AbiValidator;
212213
import org.tron.core.utils.TransactionUtil;
213214
import org.tron.core.vm.program.Program;
214215
import org.tron.core.zen.ShieldedTRC20ParametersBuilder;
@@ -498,6 +499,7 @@ public TransactionCapsule createTransactionCapsule(com.google.protobuf.Message m
498499
if (percent < 0 || percent > 100) {
499500
throw new ContractValidateException("percent must be >= 0 and <= 100");
500501
}
502+
AbiValidator.validate(contract.getNewContract().getAbi());
501503
}
502504
setTransaction(trx);
503505
return trx;

framework/src/test/java/org/tron/common/runtime/TvmTestUtils.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.tron.core.exception.ReceiptCheckErrException;
2525
import org.tron.core.exception.VMIllegalException;
2626
import org.tron.core.store.StoreFactory;
27+
import org.tron.core.utils.AbiValidator;
2728
import org.tron.core.vm.repository.Repository;
2829
import org.tron.core.vm.repository.RepositoryImpl;
2930

@@ -489,6 +490,8 @@ private static SmartContract.ABI.Entry.EntryType getEntryType(String type) {
489490
return SmartContract.ABI.Entry.EntryType.Event;
490491
case "fallback":
491492
return SmartContract.ABI.Entry.EntryType.Fallback;
493+
case "error":
494+
return SmartContract.ABI.Entry.EntryType.Error;
492495
default:
493496
return SmartContract.ABI.Entry.EntryType.UNRECOGNIZED;
494497
}
@@ -603,7 +606,13 @@ public static SmartContract.ABI jsonStr2Abi(String jsonStr) {
603606
abiBuilder.addEntrys(entryBuilder.build());
604607
}
605608

606-
return abiBuilder.build();
609+
SmartContract.ABI abi = abiBuilder.build();
610+
try {
611+
AbiValidator.validate(abi);
612+
} catch (ContractValidateException e) {
613+
throw new IllegalArgumentException(e.getMessage(), e);
614+
}
615+
return abi;
607616
}
608617

609618

framework/src/test/java/org/tron/common/utils/PublicMethod.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import org.tron.common.crypto.sm2.SM2Signer;
2222
import org.tron.common.utils.client.utils.TransactionUtils;
2323
import org.tron.core.Wallet;
24+
import org.tron.core.exception.ContractValidateException;
25+
import org.tron.core.utils.AbiValidator;
2426
import org.tron.protos.Protocol;
2527
import org.tron.protos.contract.BalanceContract;
2628
import org.tron.protos.contract.SmartContractOuterClass;
@@ -195,7 +197,13 @@ public static SmartContractOuterClass.SmartContract.ABI jsonStr2Abi(String jsonS
195197
abiBuilder.addEntrys(entryBuilder.build());
196198
}
197199

198-
return abiBuilder.build();
200+
SmartContractOuterClass.SmartContract.ABI abi = abiBuilder.build();
201+
try {
202+
AbiValidator.validate(abi);
203+
} catch (ContractValidateException e) {
204+
throw new IllegalArgumentException(e.getMessage(), e);
205+
}
206+
return abi;
199207
}
200208

201209
/** constructor. */
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package org.tron.core.services.http;
2+
3+
import static java.nio.charset.StandardCharsets.UTF_8;
4+
import static org.tron.common.utils.client.utils.HttpMethed.createRequest;
5+
6+
import javax.annotation.Resource;
7+
import org.apache.http.client.methods.HttpPost;
8+
import org.junit.Assert;
9+
import org.junit.Test;
10+
import org.springframework.mock.web.MockHttpServletRequest;
11+
import org.springframework.mock.web.MockHttpServletResponse;
12+
import org.tron.common.BaseTest;
13+
import org.tron.common.TestConstants;
14+
import org.tron.core.config.args.Args;
15+
16+
public class DeployContractServletTest extends BaseTest {
17+
18+
static {
19+
Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF);
20+
}
21+
22+
@Resource
23+
private DeployContractServlet deployContractServlet;
24+
25+
private static final String OWNER_ADDRESS = "A099357684BC659F5166046B56C95A0E99F1265CBD";
26+
27+
private MockHttpServletResponse postWithAbi(String abi) {
28+
String body = "{"
29+
+ "\"owner_address\":\"" + OWNER_ADDRESS + "\","
30+
+ "\"name\":\"abi_validation_test\","
31+
+ "\"bytecode\":\"00\","
32+
+ "\"abi\":" + abi
33+
+ "}";
34+
MockHttpServletRequest request = createRequest(HttpPost.METHOD_NAME);
35+
request.setContentType("application/json");
36+
request.setContent(body.getBytes(UTF_8));
37+
MockHttpServletResponse response = new MockHttpServletResponse();
38+
deployContractServlet.doPost(request, response);
39+
return response;
40+
}
41+
42+
private static void assertRejected(MockHttpServletResponse response, String snippet)
43+
throws Exception {
44+
Assert.assertEquals(200, response.getStatus());
45+
String body = response.getContentAsString();
46+
Assert.assertTrue("expected error containing '" + snippet + "', got: " + body,
47+
body.contains("Error") && body.contains(snippet));
48+
}
49+
50+
@Test
51+
public void rejectsShorthandUint() throws Exception {
52+
String abi = "[{\"type\":\"function\",\"name\":\"foo\","
53+
+ "\"inputs\":[{\"name\":\"x\",\"type\":\"uint\"}],\"outputs\":[]}]";
54+
assertRejected(postWithAbi(abi), "shorthand uint/int");
55+
}
56+
57+
@Test
58+
public void rejectsBadBytesN() throws Exception {
59+
String abi = "[{\"type\":\"function\",\"name\":\"foo\","
60+
+ "\"inputs\":[{\"name\":\"x\",\"type\":\"bytes33\"}],\"outputs\":[]}]";
61+
assertRejected(postWithAbi(abi), "bytesN size");
62+
}
63+
64+
@Test
65+
public void rejectsDuplicateFallback() throws Exception {
66+
String abi = "["
67+
+ "{\"type\":\"fallback\",\"stateMutability\":\"payable\"},"
68+
+ "{\"type\":\"fallback\",\"stateMutability\":\"payable\"}"
69+
+ "]";
70+
assertRejected(postWithAbi(abi), "only one fallback");
71+
}
72+
73+
@Test
74+
public void rejectsNonPayableReceive() throws Exception {
75+
String abi = "[{\"type\":\"receive\",\"stateMutability\":\"nonpayable\"}]";
76+
assertRejected(postWithAbi(abi), "must be payable");
77+
}
78+
79+
@Test
80+
public void rejectsFixedFamily() throws Exception {
81+
String abi = "[{\"type\":\"function\",\"name\":\"foo\","
82+
+ "\"inputs\":[{\"name\":\"x\",\"type\":\"fixed128x18\"}],\"outputs\":[]}]";
83+
assertRejected(postWithAbi(abi), "fixed/ufixed");
84+
}
85+
86+
@Test
87+
public void acceptsTuplePermissively() throws Exception {
88+
String abi = "[{\"type\":\"function\",\"name\":\"foo\","
89+
+ "\"inputs\":[{\"name\":\"x\",\"type\":\"tuple\"},"
90+
+ "{\"name\":\"y\",\"type\":\"tuple[]\"}],\"outputs\":[]}]";
91+
MockHttpServletResponse response = postWithAbi(abi);
92+
Assert.assertEquals(200, response.getStatus());
93+
String body = response.getContentAsString();
94+
Assert.assertFalse("tuple should not be rejected: " + body, body.contains("\"Error\""));
95+
}
96+
}

0 commit comments

Comments
 (0)