fix(jsonrpc): accept "input" as alias for "data" in call args#6722
fix(jsonrpc): accept "input" as alias for "data" in call args#6722waynercheung wants to merge 1 commit intotronprotocol:developfrom
Conversation
0b55d1a to
8df76fe
Compare
Closes tronprotocol#6517. JSON-RPC requests using the execution-apis field name `input` were rejected with UnrecognizedPropertyException, blocking spec-compliant clients -- notably go-ethereum's ethclient since ethereum/go-ethereum#28078, which only emits `input`. CallArguments and BuildArguments now declare both fields. A new resolveData() does pure precedence resolution -- `input` wins over `data` -- mirroring go-ethereum's TransactionArgs.data(). Five call sites in TronJsonRpcImpl use the resolver instead of getData(). The verb-prefix name (not getXxx) keeps Jackson and FastJSON's JavaBean introspection from picking the method up as a `resolveData` wire property; two serialisation tests pin this as a regression guard. Hex validation is mode-driven via JsonRpcApiUtil.requireValidHex (HexMode.STRICT / HexMode.LENIENT): - `input` (new field) is STRICT: follows the execution-apis BYTES schema -- requires `0x` prefix and even length; "" is accepted as empty bytes per geth's hexutil.Bytes.UnmarshalText. - `data` retains LENIENT parsing via ByteArray.fromHexString for backward compatibility with existing callers (e.g. BuildTransactionTest.testCreateSmartContract uses bare-hex bytecode). Conflict between `input` and `data` (both set, not equal) is detected on the build path at BuildArguments.getContractType(), mirroring geth's data() / setDefaults split. The error message matches go-ethereum's setDefaults wording verbatim. Comparison is byte-level so case differences are not flagged. The query path (CallArguments.resolveData()) stays lenient -- input wins silently. Tested by 52 unit tests including Jackson/FastJSON serialisation safety guards; BuildTransactionTest.testCreateSmartContract still passes; checkstyle clean.
8df76fe to
5f8d04b
Compare
|
LGTM |
| if (value.isEmpty()) { | ||
| return; | ||
| } | ||
| if (!value.startsWith("0x") || value.length() % 2 != 0) { |
There was a problem hiding this comment.
[SHOULD] STRICT mode rejects 0X (uppercase) prefix, diverging from execution-apis spec
value.startsWith("0x") is case-sensitive, so an input like 0Xdeadbeef is rejected with invalid hex string for "input". go-ethereum's hexutil.has0xPrefix accepts both 0x and 0X (input[1] == 'x' || input[1] == 'X'), so a spec-compliant client emitting 0X... is bounced by java-tron but accepted by geth — the same regression class this PR set out to fix.
Note that ByteArray.fromHexString itself is case-sensitive on the prefix and Hex.decode rejects X as a digit, so even after relaxing this gate the STRICT path needs to normalize the prefix (e.g. value = "0x" + value.substring(2)) before delegating to fromHexString, otherwise the call still throws downstream.
Suggestion: Make the STRICT prefix check case-insensitive and normalize the prefix before passing to ByteArray.fromHexString; add a resolveData_inputUppercase0X_accepted test in both CallArgumentsTest and BuildArgumentsTest.
| * args must have passed {@code requireValidHex} first. | ||
| */ | ||
| private static boolean calldataEquals(String a, String b) { | ||
| return Arrays.equals(ByteArray.fromHexString(a), ByteArray.fromHexString(b)); |
There was a problem hiding this comment.
[SHOULD] calldataEquals depends on an undocumented invariant that fromHexString("") never throws
The inline comment at line 127 states "calldataEquals relies on resolveData() having validated hex first", but requireValidHex("input", "", STRICT) short-circuits on value.isEmpty() and never calls ByteArray.fromHexString. So when input="" and data="0xdeadbeef", conflict detection enters calldataEquals and calls fromHexString("") for the first time. Today this happens to return [] (because Hex.decode("") returns an empty array), so the test passes — but the PR description's own follow-up ("Tighten ByteArray.fromHexString to the execution-apis BYTES schema") would make this throw, and the unwrapped RuntimeException would escape validateCallDataConflict, get caught by the outer RPC handler, and downgrade the error code from -32602 to -32603.
Suggestion: Either wrap calldataEquals in a try-catch that rethrows as JsonRpcInvalidParamsException, or run a LENIENT validation on input before invoking validateCallDataConflict so fromHexString is exercised once before the conflict path needs it.
| * {@link BuildArguments#resolveData()} for the rationale on | ||
| * naming, validation split, and serialiser interaction. | ||
| */ | ||
| public String resolveData() throws JsonRpcInvalidParamsException { |
There was a problem hiding this comment.
[SHOULD] resolveData() relies solely on a fragile naming convention; add explicit serializer-ignore annotations
The verb-prefix idiom (resolveData instead of getResolvedData) is the only reason Jackson and FastJSON skip this method during bean introspection. The 4 regression tests pin this for the default ObjectMapper and default JSON.toJSONString paths, but they cannot cover: (a) a future contributor renaming it to getXxx, (b) a custom ObjectMapper opting into MapperFeature.ALLOW_FIELDS_WITHOUT_GETTERS, (c) a Jackson MixIn injected at the project level, or (d) a subclass adding @JsonProperty that retunes the bean model. If the invariant ever breaks, the serializer invokes resolveData(), which throws JsonRpcInvalidParamsException for any conflicting in-flight DTO and breaks the whole RPC response path.
The same applies to BuildArguments.resolveData() at line 118.
Suggestion: Annotate both resolveData() methods with @com.fasterxml.jackson.annotation.JsonIgnore and @com.alibaba.fastjson.annotation.JSONField(serialize = false, deserialize = false) as a belt-and-braces guard.
| private void validateCallDataConflict() throws JsonRpcInvalidParamsException { | ||
| if (input != null && data != null && !calldataEquals(input, data)) { | ||
| throw new JsonRpcInvalidParamsException( | ||
| "both \"data\" and \"input\" are set and not equal. " |
There was a problem hiding this comment.
[NIT] Extract the conflict error string to a constant with a do-not-change javadoc
The message "both \"data\" and \"input\" are set and not equal. Please use \"input\" to pass transaction call data" is a cross-project contract — external tooling may match this exact string to detect the geth/setDefaults error. Inlined as a literal it's too easy for a future refactor to "improve" the wording (e.g. wrap in String.format to include both byte values) and silently break that contract.
Suggestion: Lift to a private static final String field (e.g. CONFLICT_ERR_MSG) and add a one-line javadoc "wording mirrors go-ethereum's setDefaults; do not change — external tooling may match this string".
| JsonRpcInvalidParamsException { | ||
| // Fail fast on bad hex / conflict before the state lookup; | ||
| // calldataEquals relies on resolveData() having validated hex first. | ||
| String resolvedData = resolveData(); |
There was a problem hiding this comment.
[NIT] Hex validation runs twice on the build path
getContractType() calls resolveData() (which runs requireValidHex on both fields and decodes the winning value), and the handler later calls args.resolveData() again before ByteArray.fromHexString(...) to set the bytecode (TronJsonRpcImpl lines 1132 and 1177). For a 24KB CreateSmartContract bytecode this is two redundant hex decodes per request — small but measurable.
Suggestion: Either return a tuple of (ContractType, resolvedCalldata) from getContractType so the handler can reuse the already-validated result, or document on resolveData() that callers should invoke it once per request.
| ContractType contractType; | ||
|
|
||
| // to is null | ||
| if (paramStringIsNull(to)) { |
There was a problem hiding this comment.
[NIT] Align javadoc/PR description with paramStringIsNull collapsing "" to absent
The javadoc and PR description both state that "" is presence (matching geth's hexutil.Bytes.UnmarshalText), but paramStringIsNull("") returns true and treats "" as absent — for example, BuildArguments.getContractType() with input="" and data=null falls into the paramStringIsNull(resolvedData) branch and throws "invalid json request" rather than recognising presence. The observable behaviour is correct (geth also rejects empty bytecode for contract creation), but the internal semantics drift from the stated invariant, which makes future readers misjudge edge cases.
Suggestion: Update the javadoc to clarify that on the wire "" is presence, but inside the DTO paramStringIsNull collapses both "" and 0x to absent, matching the "no bytecode" semantics geth enforces in setDefaults.
| @@ -29,45 +30,299 @@ public class BuildArgumentsTest extends BaseTest { | |||
| public void initBuildArgs() { | |||
| buildArguments = new BuildArguments( | |||
There was a problem hiding this comment.
[NIT] Replace 17-arg positional constructor in test fixture with setter chain
initBuildArgs() and testBuildArgument() build instances via @AllArgsConstructor's positional form; this PR widened both signatures (16→17 and 7→8) and the next field addition will force the same edit again. Other test classes in the module (WalletCursorTest, BuildTransactionTest, JsonRpcTest) already use new BuildArguments() plus setters and are immune to this churn. The 17-arg literal is also hard to read — a mix of address strings, decimals, longs, and a trailing boolean.
Suggestion: Convert these two fixtures to no-arg constructor + setter calls so they don't need touching the next time the DTO grows.
| * {@code mode}. {@code null} is treated as absent and returns | ||
| * silently. {@code fieldName} is used only in error messages. | ||
| */ | ||
| public static void requireValidHex(String fieldName, String value, HexMode mode) |
There was a problem hiding this comment.
[NIT] Add direct unit tests for requireValidHex / HexMode
HexMode and requireValidHex are designed as a reusable utility (the comment hints at future fields adopting it), but coverage today comes only via CallArgumentsTest / BuildArgumentsTest. When the next caller (e.g. an accessList field, or a future RPC method) adopts it, regressing the utility itself will manifest as failures in those callers, which is hard to debug.
Suggestion: Add a JsonRpcApiUtilTest.requireValidHex_* series covering null, "" for both modes, 0x only, valid hex, no-prefix, odd-length, non-hex digits, and 0X (uppercase) — the last one fails before the case-sensitivity fix and pins it once landed.
| * Throws when both fields decode to non-equal bytes. Wording matches | ||
| * geth's setDefaults so existing tooling can detect the error string. | ||
| */ | ||
| private void validateCallDataConflict() throws JsonRpcInvalidParamsException { |
There was a problem hiding this comment.
[NIT] Document the precondition that calldataEquals requires pre-validated hex
The contract "both args must have passed requireValidHex first" lives on calldataEquals (line 192-195) but the actual ordering happens up in getContractType (line 127-129). A reader visiting validateCallDataConflict in isolation has no warning that calling it on un-validated input is unsafe — and the next P1 finding (calldataEquals + fromHexString("")) is exactly an instance of that precondition not holding for input="".
Suggestion: Restate the precondition in validateCallDataConflict's javadoc ("caller must ensure both fields have passed requireValidHex"), so the contract is documented at every entry point, not only on the leaf method.
What does this PR do?
Accept
inputalongsidedataoneth_call,eth_estimateGas, andbuildTransactioncall arguments, matching the Ethereum execution-apis spec and resolving #6517.input(raw field) toCallArgumentsandBuildArguments.resolveData()- pure precedence resolution withinputwinning overdata, mirroring go-ethereum'sTransactionArgs.data().inputanddata(both set, not equal) are detected atBuildArguments.getContractType()- mirroring geth'sdata()/setDefaultssplit. The error message uses go-ethereum's wording verbatim. The query path stays lenient (inputwins silently).TronJsonRpcImplfromgetData()toresolveData().JsonRpcApiUtil.requireValidHex(HexMode.STRICT/HexMode.LENIENT):inputis STRICT: follows the execution-apis BYTES schema (^0x[0-9a-f]*$, even length;""is accepted as empty bytes per geth'shexutil.Bytes.UnmarshalText).datakeeps LENIENT parsing viaByteArray.fromHexString(accepts bare hex and odd length) for backward compatibility with existing callers - notablyBuildTransactionTest.testCreateSmartContract, which submits bare-hex bytecode.resolveData()- verb prefix, notgetXxx- so Jackson's and FastJSON's JavaBean introspection skips it with no per-serializer annotations needed.CallArguments7->8 args,BuildArguments16->17 args). The named-JSON deserialisation path is unaffected.Why are these changes required?
The Ethereum execution-apis spec uses
inputas the canonical calldata field;datais the legacy alias. go-ethereum accepts both, withinputwinning when equal and erroring when not. Notably, go-ethereum'sethclienthas emitted onlyinputsince ethereum/go-ethereum#28078, so any tron RPC user upgrading their geth client side hits this.Before this PR, java-tron only read
data. A spec-compliant client carrying onlyinputfailed two ways:FAIL_ON_UNKNOWN_PROPERTIES=true), returning-32603instead of executing the call.to) trippedinvalid json requestbecause calldata appeared absent.Both symptoms are captured in #6517.
This PR has been tested by:
CallArgumentsTest(23 tests) +BuildArgumentsTest(29 tests) = 52 total. Coverage:inputonly,dataonly, neither.CallArgumentsreturnsinputsilently;BuildArguments.getContractType()throws with geth-equivalent wording.""is presence (matches geth).0x(zero-length payload) is presence and never collapses to absent.0xDEAD == 0xdead).inputvalidation rejects bare hex, odd length, non-hex chars.datavalidation still accepts bare hex and odd length;BuildTransactionTest.testCreateSmartContractstill passes.datais rejected even wheninputis the precedence winner, and vice versa.resolveDatanever appears as a wire property and serialisation never throws even with conflictinginput/data(regression guard for the verb-prefix naming).inputfield foreth_call? #6517 reproductions: Jackson deserialisation of{"input": "0xdeadbeef"}succeeds; contract-creation viainput(noto) resolves toCreateSmartContract.Compatibility notes
Requests sending only
datawith valid hex are byte-for-byte unchanged. Two error paths shift, both improvements:Issue Are there plans to support parameter passing via the
inputfield foreth_call? #6517 itself - any request carryinginput. Pre-PR Jackson rejects the unknown property; jsonrpc4j surfaces this as-32700 "JSON parse error"withid: "null". Post-PR the request parses cleanly and either succeeds or returns a specific-32602(on malformed hex or build-path conflict).Invalid hex in
data(e.g.data: "0xzz") - pre-PR BouncyCastle'sDecoderExceptionfalls through to jsonrpc4j's default resolver:Post-PR
requireValidHexcatches and rethrows asJsonRpcInvalidParamsException, mapped via@JsonRpcErrorto standard-32602:Standard JSON-RPC code, deterministic message, no internal class leak. Clients hardcoded against
-32700(#6517 symptom) or-32001(the BC leak) will see different behaviour.Follow up
inputanddatapayloads. Useful but not blocking - the unit-level path is fully covered.ByteArray.fromHexStringto the execution-apis BYTES schema, then drop the asymmetric branch inrequireValidHex. Behaviour-changing for non-RPC callers, so deferred to a separate change.