fix(jsonrpc): enforce log filter cap and improve match efficiency
Problem
Three issues exist in the JSON-RPC log filter subsystem (eth_newFilter / eth_getLogs), reported in tronprotocol/java-tron#6510:
-
Unbounded memory growth —
eth_newFilterimposes no limit on the number of active filters held in memory. A client can register filters indefinitely and eventually exhaust heap on the node. -
Correctness bug —
LogMatch.matchBlockOneByOneevaluated theMAX_RESULTguard afteraddAll, so the result list could transiently contain more thanMAX_RESULTentries before the exception was thrown. -
Performance bottleneck under high filter count —
handleLogsFilteriterated the filter map with anIteratorand calledresult.add()once per element, which is slow and contention-prone when thousands of filters are active.
Changes
1. Enforce a configurable cap on active log filters
Adds node.jsonrpc.maxLogFilterNum (default: 20000). When the cap is reached, eth_newFilter immediately returns JsonRpcExceedLimitException (JSON-RPC code -32005) instead of growing without bound.
node {
jsonrpc {
# Maximum number of concurrent eth_newFilter registrations (0 = unlimited)
maxLogFilterNum = 20000
}
}
2. Fix the MAX_RESULT boundary check in LogMatch.matchBlockOneByOne (correctness)
Moves the size + matchedLog.size() > MAX_RESULT guard before addAll. The result list now never exceeds MAX_RESULT entries regardless of how many logs a single block contributes.
3. Optimize handleLogsFilter for large filter maps
| Condition | Before | After |
|---|---|---|
| filter count ≤ 10,000 (common case) | Iterator loop, result.add() per element | ConcurrentHashMap.forEach with local list + single addAll |
| filter count > 10,000 | same slow path | bounded ForkJoinPool(3) + parallelStream |
Key improvements:
- Bounded parallelism:
logsFilterPoolis aForkJoinPool(3)created once perTronJsonRpcImplinstance and shut down with it, avoiding unbounded thread creation under spike load. - Observability: a
logger.debugtiming line records dispatch cost and filter-map size.
4. Decouple filter state from static class scope
The four filter maps (eventFilter2ResultFull, blockFilter2ResultFull, eventFilter2ResultSolidity, blockFilter2ResultSolidity), logsFilterPool, and filterParallelThreshold are now instance fields. handleBLockFilter, handleLogsFilter, and processLogFilterEntry are now instance methods.
Motivation: with static shared state, any test that constructs a TronJsonRpcImpl instance would silently read and write the same filter maps used by the running node, making test isolation impossible. Switching to instance state means each instance owns its own filter maps; tests create a lightweight instance with no Spring dependencies (new TronJsonRpcImpl(null, null, null)) and operate on an isolated map.
As a consequence, BlockFilterCapsule and LogsFilterCapsule now receive the TronJsonRpcImpl instance at construction time and call handleBLockFilter / handleLogsFilter on it directly, eliminating the static imports that were the only way to invoke those methods before.
5. Behavor changes when node.jsonrpc.maxBlockFilterNum = 0
It forbid all new block filters when maxBlockFilterNum = 0 in release-4.8.1, now, it is not limited.
It has the same meaning as node.jsonrpc.maxLogFilterNum =0 for log filter number.
Testing
- Unit tests (new / updated):
HandleLogsFilterTest— 10 cases covering event dispatch, block-range filtering, expired-filter removal, solidified/non-solidified routing, and 3 parallel-path cases (all-filters receive events, expired eviction, solidified routing — each withfilterParallelThresholdlowered via reflection to keep the test fast)LogMatchOverLimitTest— 4 cases covering under-limit, exact-limit, exceed-limit (verifies exception is thrown beforeaddAll), and empty-block skipWalletCursorTest.testNewFilter_exceedsCapThrowsException— verifies the cap throws the correct exception with the limit value in the message; fixed to populate the map on the same instance that callsnewFilter(the original test was populating the static map then constructing a new instance, so it never actually tested the cap)BlockFilterCapsuleTest,LogsFilterCapsuleTest,ConcurrentHashMapTest— updated to pass aTronJsonRpcImplinstance to constructors after the static-to-instance refactoring
- Manual testing
Closes tronprotocol/java-tron#6510