Skip to content

fix(jsonrpc): accept "input" as alias for "data" in call args#6722

Open
waynercheung wants to merge 1 commit intotronprotocol:developfrom
waynercheung:fix/jsonrpc-eth-call-input-param-6517
Open

fix(jsonrpc): accept "input" as alias for "data" in call args#6722
waynercheung wants to merge 1 commit intotronprotocol:developfrom
waynercheung:fix/jsonrpc-eth-call-input-param-6517

Conversation

@waynercheung
Copy link
Copy Markdown
Collaborator

@waynercheung waynercheung commented Apr 28, 2026

What does this PR do?

Accept input alongside data on eth_call, eth_estimateGas, and buildTransaction call arguments, matching the Ethereum execution-apis spec and resolving #6517.

  • Add input (raw field) to CallArguments and BuildArguments.
  • Resolve calldata via resolveData() - pure precedence resolution with input winning over data, mirroring go-ethereum's TransactionArgs.data().
  • On the build path, conflicts between input and data (both set, not equal) are detected at BuildArguments.getContractType() - mirroring geth's data() / setDefaults split. The error message uses go-ethereum's wording verbatim. The query path stays lenient (input wins silently).
  • Switch five production read sites in TronJsonRpcImpl from getData() to resolveData().
  • Hex validation is mode-driven via JsonRpcApiUtil.requireValidHex (HexMode.STRICT / HexMode.LENIENT):
    • input is STRICT: follows the execution-apis BYTES schema (^0x[0-9a-f]*$, even length; "" is accepted as empty bytes per geth's hexutil.Bytes.UnmarshalText).
    • data keeps LENIENT parsing via ByteArray.fromHexString (accepts bare hex and odd length) for backward compatibility with existing callers - notably BuildTransactionTest.testCreateSmartContract, which submits bare-hex bytecode.
  • The resolver is named resolveData() - verb prefix, not getXxx - so Jackson's and FastJSON's JavaBean introspection skips it with no per-serializer annotations needed.
  • The Lombok-generated positional constructors widen by one slot (CallArguments 7->8 args, BuildArguments 16->17 args). The named-JSON deserialisation path is unaffected.

Why are these changes required?

The Ethereum execution-apis spec uses input as the canonical calldata field; data is the legacy alias. go-ethereum accepts both, with input winning when equal and erroring when not. Notably, go-ethereum's ethclient has emitted only input since 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 only input failed two ways:

  1. Jackson rejected the unknown property (FAIL_ON_UNKNOWN_PROPERTIES=true), returning -32603 instead of executing the call.
  2. Even when the field was tolerated, contract-creation paths (no to) tripped invalid json request because calldata appeared absent.

Both symptoms are captured in #6517.

This PR has been tested by:

  • Unit tests
  • Integration tests
  • Manual tests

CallArgumentsTest (23 tests) + BuildArgumentsTest (29 tests) = 52 total. Coverage:

  • Single-field cases: input only, data only, neither.
  • Both fields equal — precedence resolves cleanly.
  • Both fields not equal: CallArguments returns input silently; BuildArguments.getContractType() throws with geth-equivalent wording.
  • Empty-string "" is presence (matches geth).
  • 0x (zero-length payload) is presence and never collapses to absent.
  • Case-insensitive byte equality (0xDEAD == 0xdead).
  • Strict input validation rejects bare hex, odd length, non-hex chars.
  • Lenient data validation still accepts bare hex and odd length; BuildTransactionTest.testCreateSmartContract still passes.
  • Loser-field validation: malformed data is rejected even when input is the precedence winner, and vice versa.
  • Jackson + FastJSON serialisation: resolveData never appears as a wire property and serialisation never throws even with conflicting input / data (regression guard for the verb-prefix naming).
  • Are there plans to support parameter passing via the input field for eth_call? #6517 reproductions: Jackson deserialisation of {"input": "0xdeadbeef"} succeeds; contract-creation via input (no to) resolves to CreateSmartContract.

Compatibility notes

Requests sending only data with valid hex are byte-for-byte unchanged. Two error paths shift, both improvements:

  • Issue Are there plans to support parameter passing via the input field for eth_call? #6517 itself - any request carrying input. Pre-PR Jackson rejects the unknown property; jsonrpc4j surfaces this as -32700 "JSON parse error" with id: "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's DecoderException falls through to jsonrpc4j's default resolver:

code -32001
message "exception decoding Hex string: invalid characters encountered in Hex string"
data "org.bouncycastle.util.encoders.DecoderException"

Post-PR requireValidHex catches and rethrows as JsonRpcInvalidParamsException, mapped via @JsonRpcError to standard -32602:

code -32602
message "invalid hex string for "data""
data "{}"

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

  • Add a JsonRpcServer-level integration test exercising the full Jackson -> handler -> response path for both input and data payloads. Useful but not blocking - the unit-level path is fully covered.
  • Tighten ByteArray.fromHexString to the execution-apis BYTES schema, then drop the asymmetric branch in requireValidHex. Behaviour-changing for non-RPC callers, so deferred to a separate change.

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.
@waynercheung waynercheung force-pushed the fix/jsonrpc-eth-call-input-param-6517 branch from 8df76fe to 5f8d04b Compare April 30, 2026 05:51
@0xbigapple
Copy link
Copy Markdown
Collaborator

LGTM

if (value.isEmpty()) {
return;
}
if (!value.startsWith("0x") || value.length() % 2 != 0) {
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.

[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));
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.

[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 {
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.

[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. "
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.

[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();
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.

[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)) {
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.

[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(
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.

[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)
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.

[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 {
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.

[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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Are there plans to support parameter passing via the input field for eth_call?

4 participants