Skip to content

fix(jsonrpc): handle null parameters consistently - #6984

Open
waynercheung wants to merge 4 commits into
tronprotocol:release_v4.8.3from
waynercheung:feat/jsonrpc-null-param
Open

waynercheung wants to merge 4 commits into
tronprotocol:release_v4.8.3from
waynercheung:feat/jsonrpc-null-param

Conversation

@waynercheung

@waynercheung waynercheung commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Rejects an explicit null with -32602 at 10 identified top-level parameter positions that could previously trigger unchecked dereferences, normalizes seven optional DTO fields whose explicit JSON null currently behaves differently from omission, and aligns eth_uninstallFilter lookup misses with go-ethereum. The per-parameter decision basis is recorded in #6951.

  • Handle the 10 affected positions without unchecked dereferences. For source-gated methods the request-source check (disableInPBFT / FullNode-only) stays first. Object and filter-ID arguments are validated before business processing. For the two block-query methods, the block hash or selector is validated first, followed by fullTransactionObjects, before any block lookup:
    • eth_getLogs / eth_newFilter with a null filter object -> -32602 "invalid filter request"
    • eth_call / eth_estimateGas / buildTransaction with a null argument object -> -32602 "invalid params"
    • eth_uninstallFilter / eth_getFilterChanges / eth_getFilterLogs with a null filter ID -> -32602 "invalid params"
    • eth_getBlockByHash / eth_getBlockByNumber with a null fullTransactionObjects -> -32602 "invalid params"; an invalid block hash or selector keeps its existing error
  • eth_call validates its required transaction argument before the block selector, so [null, null] returns -32602 instead of -32600.
  • @JsonSetter(nulls = Nulls.SKIP) on the six optional BuildArguments fields (tokenId, tokenValue, consumeUserResourcePercent, originEnergyLimit, permissionId, extraData) and on CallArguments.from, so an explicit JSON null keeps the field default exactly as omitting the field does.
  • One requireParam helper performs the null checks above. The two -32602 messages, invalid params and invalid filter request, are private constants in TronJsonRpcImpl.
  • eth_uninstallFilter returns false for unknown or already removed IDs, including empty and non-hexadecimal strings that miss the lookup. It returns true only when remove actually removes an installed event or block filter, without a separate containsKey check. ID normalization and request-source selection are unchanged; no format validation or locking is added.
  • Remove this method's obsolete ItemNotFoundException declaration from TronJsonRpc and TronJsonRpcImpl, and its corresponding -32000 annotation. Other methods' mappings are unchanged.

Remaining method-level error responses keep their existing @JsonRpcErrors mappings: data stays "{}" and the request id is echoed. Uninstall lookup misses now produce a Boolean result instead of the removed error mapping.

Why are these changes required?

Before this PR, the null paths at these positions dereferenced the argument without a null check. Where this raised a NullPointerException, the exception was not covered by @JsonRpcErrors, so jsonrpc4j's fallback returned:

{"jsonrpc":"2.0","id":1,"error":{"code":-32001,"message":null,"data":"java.lang.NullPointerException"}}

message: null violates JSON-RPC 2.0 (on JVMs where helpful NullPointerException messages are enabled, the default from JDK 15 onwards, it becomes a diagnostic string echoing internal class and method names), data exposes the Java exception type, and a required-parameter error such as this example is reported under the code java-tron documents for internal errors. The seven DTO fields were inconsistent among themselves: an explicit null failed with -32001 or -32000 depending on the path, while CallArguments.from returned -32602 where omitting the field continues with the zero address.

The per-parameter decisions follow the basis recorded in #6951: the Execution API required / schema first, then whether a zero value can be inferred from null, then go-ethereum (v1.17.6) / Besu (26.9.0) behavior as a reference. Details and reproduction steps are in #6951.

This PR has been tested by:

  • Unit Tests

  • Manual Testing

  • JsonRpcArgumentNullBindingTest - Jackson binding through ObjectMapper: an explicit null and an omitted field produce the same BuildArguments / CallArguments.

  • JsonRpcCallAndEstimateGasTest - service-level results for null eth_call / eth_estimateGas argument objects. JsonRpcTest - a null entry inside a topic OR-list still returns invalid topic(s): null.

  • JsonrpcServiceTest - wire-level contract through a real JsonRpcServer: code / message / data for every case above, the eth_call argument-before-block ordering, filter-object null wildcard semantics unchanged (address: null, topics: null, positional [A, null]), optional DTO null fields, and an assertion that no response contains java. or an exception class name. Includes wire regressions for the four hash-parameter methods fixed by fix(jsonrpc): harden RPC/HTTP parameter validation #6828.

  • JsonrpcServiceTest also covers event/block filter creation and removal through the real server, repeated removal, unknown/empty/non-hexadecimal IDs, strict Boolean results without an error field, preserved ID normalization, unchanged get-filter errors for unknown IDs and concurrent removal of one installed filter.

  • JsonrpcServiceTest also covers null fullTransactionObjects for existing and non-existent blocks through both block-query methods, keeps the block hash or selector error when both parameters are invalid, and verifies with a mocked Wallet that a null flag is rejected before any block is read.

  • WalletCursorTest - FullNode/Solidity filter stores stay isolated; both filter types can be removed only from the selected source. PBFT rejects null, installed, unknown and malformed string IDs before removing anything. A null fullTransactionObjects returns -32602 on PBFT, where the block-query methods remain available.

On 2026-09-08, :framework:cleanTest :framework:test --no-build-cache for org.tron.core.jsonrpc.* and org.tron.core.services.jsonrpc.* passed 24 classes / 242 tests (0 failures, 0 errors, 0 skipped) on JDK 17 (arm64). On 2026-09-15 the same suite was re-run on the branch rebased onto release_v4.8.3 (0d19485318, which already carries the slf4j 2.0.17 / logback 1.3.16 / jackson 2.18.10 upgrade from #6950 and the HTTP error sanitization from #6954) with the same result. On 2026-09-27, after the null-handling update in dccf4b3, the same suite passed 24 classes / 243 tests (0 failures, 0 errors, 0 skipped) on JDK 17 (arm64); the added test is the mocked-Wallet check. On 2026-10-09 the branch was rebased onto release_v4.8.3 (6f7b83a6d6), and the tests added by this PR were adapted to the three-argument TronJsonRpcImpl constructor from #6990. With that adaptation and c74d9ea (shared null-check helper, and removal of the unreachable LogFilter constructor check together with its unit test), the same suite passed 27 classes / 253 tests (0 failures, 0 errors, 0 skipped) on JDK 17 (arm64). Compared with 2026-09-27, one test was removed with the constructor check, and the 3 classes / 11 tests added come from release_v4.8.3. :framework:checkstyleMain, :framework:checkstyleTest and git diff --check also pass. This validates compilation of the repository's callers after the checked-exception changes; external Java source callers may still need adjustment. Java 8/x86 execution was not performed.

Compatibility

Breaking, limited to the affected null-input and filter lookup-miss responses. Successful valid calls remain unchanged. Observable changes are:

  • required object parameters and null filter IDs change from the unmapped -32001 fallback to -32602;
  • a null fullTransactionObjects changes from -32001 to -32602; in addition, eth_getBlockByNumber with a non-existent block and a null fullTransactionObjects changes from result: null to -32602;
  • eth_uninstallFilter with an unknown, already removed, empty or non-hexadecimal string ID changes from -32000 "filter not found" to result: false. Existing filters still return true on removal; simultaneous removals no longer both report success for the same installed entry;
  • explicit null DTO fields recover the omitted-field semantics;
  • eth_call([null, null]) goes from -32600 to -32602;
  • eth_call([null, "earliest"]) and similar shapes where parameter 0 and the block selector are both invalid keep -32602, but the message changes from the block error to "invalid params" because the required argument is now validated first.

Clients matching on the old codes or messages for these paths, or branching on an uninstall error versus a result, need to adjust.

Removing the checked ItemNotFoundException declaration is Java binary-compatible, but source callers that specifically catch it around this call, or implementations that still declare it, may need adjustment when recompiled. On TronJsonRpcImpl, uninstallFilter and getFilterChanges now also declare JsonRpcInvalidParamsException, which TronJsonRpc already declares for them: this adds no checked-exception requirement for callers that use the interface, but code that calls these methods through the implementation type may need to handle it. This PR removes one obsolete mapping from uninstallFilter; unknown-ID errors from eth_getFilterChanges and eth_getFilterLogs remain unchanged.

Unchanged: successful valid-call results, HTTP status, request-source precedence, data on remaining mapped errors, result: null for a non-existent block with a non-null fullTransactionObjects, filter-object null wildcards, validation of non-null arguments (for example the field checks in eth_estimateGas and the finalized tag check in eth_newFilter), unknown-ID errors from eth_getFilterChanges / eth_getFilterLogs, gRPC and non-JSON-RPC HTTP APIs.

Follow up

Extra details

This PR and #7004, the first PR for #6676, both change TronJsonRpc, TronJsonRpcImpl and the JSON-RPC tests, and currently conflict in TronJsonRpcImpl and JsonrpcServiceTest. This PR also shares TronJsonRpc and TronJsonRpcImpl with the error-mapping change tracked by #6941. This PR does not require #6676 or #6941 to land first; whichever lands second rebases and re-runs the related regression tests.

Pre-submit checklist:

  • Google Java Style; Checkstyle passes on main and test sources
  • No debug code, temporary comments or TODOs
  • No numeric computation or narrowing casts introduced
  • No logging added or changed
  • No DB, consensus, config or dependency changes
  • Comments explain the non-obvious points: @JsonSetter protects Jackson input only, and positional vs. OR-list null topics in LogFilter

Closes #6951
Refs #6676

})
boolean uninstallFilter(String filterId) throws JsonRpcInvalidParamsException,
JsonRpcMethodNotFoundException, ItemNotFoundException;
JsonRpcMethodNotFoundException;

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] Preserve source compatibility for uninstallFilter

Returning false on a lookup miss only requires the implementation to stop throwing ItemNotFoundException; removing the checked exception from this public interface also makes existing source callers that catch it and third-party implementations that still declare it fail to recompile. That is a separate compatibility cost from the intended JSON-RPC wire change.

Suggestion: Keep the existing ItemNotFoundException declaration and error mapping in this release while leaving the implementation non-throwing, and remove them only in a separately announced compatibility cleanup.

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.

Thanks for raising the source-compatibility concern. I agree that removing a checked exception can require Java callers that catch it, or implementations that declare it, to change when recompiled.

I'd prefer to keep the removal here. My understanding is that TronJsonRpc is an internal jsonrpc4j binding for the node's JSON-RPC endpoint, whose compatibility contract is the JSON-RPC wire protocol. TronJsonRpcImpl is its only implementation in the repository, and there are no direct calls to uninstallFilter in the main source set.

There is also precedent for changing this interface's exception declarations: #6732, included in GreatVoyage-v4.8.2, added JsonRpcExceedLimitException to newFilter without a deprecation step.

The built-in implementation no longer throws ItemNotFoundException: an unknown or already removed filter ID returns false. Removing the declaration and its -32000 mapping keeps the binding consistent with that behavior. The PR description already records the potential source-compatibility impact.

If maintaining source compatibility for external users of this Java interface is an intended project commitment, I can restore the declaration and the mapping. Otherwise, I would keep their removal in this PR.

@waynercheung

Copy link
Copy Markdown
Collaborator Author

Updated in dccf4b3001: an explicit null filter ID or fullTransactionObjects now returns -32602 "invalid params", instead of false / "filter not found" / treating the flag as false, following go-ethereum v1.17.6 and Besu 26.9.0. For the two block-query methods, the block hash or selector is still validated first, and the flag is checked before any block lookup. The rationale is in #6951 (comment), and the description above has been updated to match.

}

private static final String FILTER_NOT_FOUND = "filter not found";
private static final String INVALID_PARAMS = "invalid params";

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] Minor follow-ups — 2 items rolled up

Grouped as one comment since both are small, non-blocking nits from this review.

  1. Constant placement inconsistency. INVALID_PARAMS is private in TronJsonRpcImpl (framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java:125) while INVALID_FILTER_REQUEST is shared via JsonRpcApiUtil (framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java:66) — both are user-visible -32602 messages. The split has a reason (the LogFilter constructor needs cross-class access), but it is implicit, and future null-check sites may drift in wording or add yet another copy. Consider unifying these message constants in JsonRpcApiUtil in a follow-up cleanup.

  2. Unrestored dynamic property in test. In JsonrpcServiceTest.executeBuiltTransaction (framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java:2218), saveMaxCpuTimeOfOneTx(5_000L) is written but never restored, unlike the symmetric save/restore of ConfigLoader.disable and VMConfig.initVmHardFork in the same try block. Since JsonrpcServiceTest extends BaseTest shares the Spring context and DB at class level, the value persists for later tests in the class. It is currently benign (more permissive direction, no downstream consumer), but it violates the "who pollutes, who cleans up" convention and can become a hidden ordering dependency for future CPU-limit-sensitive tests. Restore the original value in the finally block, as done for ConfigLoader.disable.

Suggestion: unify the -32602 message constants in a follow-up cleanup, and restore maxCpuTimeOfOneTx in the test's finally block.

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.

Thanks for the review.

  1. Addressed in c74d9ea, although in the other direction from moving them into JsonRpcApiUtil. After removing the unreachable LogFilter constructor check (discussion), INVALID_FILTER_REQUEST is only used by TronJsonRpcImpl. Both messages are now private constants there, together with the class's other method-level messages such as FILTER_NOT_FOUND and JSON_ERROR, while JsonRpcApiUtil keeps the messages thrown by its own helpers, such as BLOCK_NUM_ERROR. JsonRpcApiUtil is no longer changed by this PR. The ten required-parameter null checks go through one requireParam helper with these two constants, and the message strings are unchanged.

  2. The CPU-limit write is inside an uncommitted revoking-store session (try (ISession ignored = dbManager.getRevokingStore().buildSession())). The store is enabled during Manager initialization, so closing the session without a commit rolls the write back on both normal and exceptional exit. A probe on this branch read 5000 inside the session and the original 50 after it closed. ConfigLoader.disable and the hard-fork flag set by VMConfig.initVmHardFork (CommonParameter.ENERGY_LIMIT_HARD_FORK) are static values outside that mechanism, which is why only those two are restored in finally.

    This close-without-commit behavior is already covered by SnapshotManagerTest.testClose. Given the existing session scope here, I suggest keeping the helper unchanged rather than adding a second restoration mechanism.

return (b == null ? null : getBlockResult(b, fullTransactionObjects));
}

private static void requireFullTransactionObjects(Boolean fullTransactionObjects)

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] Optional: the "null means invalid params" rule is now written in three ways:

  • six inline if (x == null) throw new JsonRpcInvalidParamsException(INVALID_PARAMS) blocks (L702, L1034, L1369, L1536, L1562, L1605)
  • requireFullTransactionObjects here
  • two inline guards with INVALID_FILTER_REQUEST (L1481, L1587)

One private static <T> T requireParam(T value, String message) would cover all of them. Keep both messages, since the tests assert them.

Related: ethGetBlockByHash (L366-368) re-inlines getBlockByJsonHash so it can check the flag before the lookup. A small getBlockByHash(byte[]) used by both would mirror the parseBlockSelector / getBlockBySelector split on the number path.

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.

Thanks, applied in c74d9ea. All ten call sites now use private static <T> T requireParam(T value, String message), and both messages are kept as they are. ethGetBlockByHash and getBlockByJsonHash share a new getBlockByHash(byte[]), so ethGetBlockByHash no longer inlines the lookup, mirroring the parseBlockSelector / getBlockBySelector split on the number path. The validation order is unchanged.

* construct one LogFilter from part parameters of FilterRequest
*/
public LogFilter(FilterRequest fr) throws JsonRpcInvalidParamsException {
if (fr == null) {

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] Optional: production can't reach this check. new LogFilter(fr) is only built via LogFilterWrapper, from getLogs and from newFilter (through LogFilterAndResult), and both now reject a null fr first (TronJsonRpcImpl L1481, L1587). newFilter needs its own check anyway because of fr.getFromBlock() at L1486. So this is a third guard for the same condition, and only testNullLogFilterRequestRejectedAsInvalidParams reaches it. Either drop it along with that test, or keep it as the only check for getLogs and remove the one at L1587.

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.

Agreed, applied the first option in c74d9ea: removed the constructor check and testNullLogFilterRequestRejectedAsInvalidParams. The checks stay at the eth_getLogs and eth_newFilter entry points, so eth_getLogs still rejects a null filter before reading the head block, and all ten positions are validated the same way. Null filter requests for both methods remain covered in JsonrpcServiceTest at the service level (testTopLevelNullParametersAtServiceLayer) and the wire level (testNullParameterWireContract). As a result, INVALID_FILTER_REQUEST no longer needs to be shared. It is now a private constant in TronJsonRpcImpl, and JsonRpcApiUtil is no longer changed by this PR.

Validate nullable JSON-RPC arguments before they are dereferenced.
Return stable protocol errors instead of jsonrpc4j's unhandled -32001
fallback, which exposes Java exception types.

Validate eth_call's required transaction argument before its block
selector. The double-null case now returns -32602 instead of -32600.

Align eth_uninstallFilter lookup misses with geth: null, unknown and
already removed IDs return false. Empty and non-hexadecimal strings
also return false when no filter matches, without new format checks.
Non-null lookup misses now return false instead of -32000.
Use the value returned by remove to report whether a filter was removed
and avoid the race between checking and removing the same entry.

Remove the obsolete uninstall ItemNotFoundException declaration and
error mapping. Java binaries remain compatible, but source callers
catching that checked exception, or implementations still declaring it,
may need adjustment when recompiled.
Keep getFilterChanges and getFilterLogs on their existing lookup-miss
errors, and preserve request-source precedence and filter isolation.

Treat null block-detail flags as false and retain DTO defaults for
explicitly null optional fields. Preserve filter wildcard semantics.

Add binding, service, wire and VM execution regressions for null
arguments, error contracts, filter removal, source isolation and
concurrent uninstall results.
Return invalid params for an explicit null filter ID in
eth_uninstallFilter, eth_getFilterChanges and eth_getFilterLogs, and for
a null fullTransactionObjects flag in eth_getBlockByHash and
eth_getBlockByNumber. The block hash or selector is still validated
first, and the flag is checked before any block lookup, so the result
does not depend on whether the block exists. Unknown non-null filter
IDs keep their existing results.

This replaces the earlier null-as-false handling of the flag and the
filter ID. go-ethereum v1.17.6 rejects null for these required
arguments; earlier releases decoded it as a zero value.
Route the null checks for the ten required JSON-RPC parameter
positions through one requireParam helper instead of repeating the
same if/throw block at each site. The "invalid params" and "invalid
filter request" messages are unchanged and are now both private
constants in TronJsonRpcImpl.

Remove the null check from the LogFilter constructor. In production
code, LogFilter is only built through LogFilterWrapper from
eth_getLogs and eth_newFilter, and both reject a null filter request
before reaching it, so the constructor check and its unit test are
dropped. Null filter requests remain covered at the service and wire
level.

Extract getBlockByHash(byte[]) and use it from ethGetBlockByHash and
getBlockByJsonHash, so ethGetBlockByHash no longer inlines the block
lookup. The validation order is unchanged.

No behavior change.
Adapt the tests added for null parameter handling to the constructor
that now takes the Manager, and drop the removed setManager call.
Tests built only from mocks pass a mocked Manager, and tests backed by
the shared database pass dbManager, matching the existing tests.
@waynercheung
waynercheung force-pushed the feat/jsonrpc-null-param branch from dccf4b3 to 010242a Compare October 9, 2026 10:24
@waynercheung
waynercheung requested a review from bladehan1 October 9, 2026 10:32
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.

3 participants