fix(event): correct fork rollback handling in solidity event maps#6718
Conversation
6102162 to
2831e35
Compare
| private void clearSolidityContractTriggerCache(long blockNum) { | ||
| if (eventPluginLoaded | ||
| && (EventPluginLoader.getInstance().isSolidityEventTriggerEnable() | ||
| || EventPluginLoader.getInstance().isSolidityLogTriggerEnable())) { |
There was a problem hiding this comment.
[SHOULD] clearSolidityContractTriggerCache guard is narrower than the write side; redundancy path leaks the map.
ContractTriggerCapsule.java:162 ( the event-as-log redundancy path) also writes to solidityContractLogTriggerMap under this combination:
isContractLogTriggerEnable = true
isContractLogTriggerRedundancy = true
isSolidityLogTriggerRedundancy = true
isSolidityLogTriggerEnable = false
isSolidityEventTriggerEnable = false
outer condition (isContractLogEnable && isContractLogRedundancy) || ... is true → enters the redundancy block; inner isSolidityLogTriggerRedundancy=true → writes to the solidity log map. But both clear-side guard predicates are false → clear is skipped.
Subtler still: the consumer postSolidityTrigger is also gated by isSolidityLogTriggerEnable, so under this configuration the consumer does not run either. The result: solidityContractLogTriggerMap accumulates one entry per event-bearing block, never consumed, never cleared — a memory leak. It does not produce Issue #6678's "duplicate downstream events" symptom (no downstream is wired up), but since the PR formalizes the clearing responsibility, the clear's semantics should be aligned with the write side.
Suggestion (preferred): simply drop the guard. ConcurrentHashMap.remove(key) on a non-existent key is an O(1) bucket lookup returning null — the cost is negligible. The guard was only a defensive optimization; without it, write-side condition changes won't desync the clear:
private void clearSolidityContractTriggerCache(long blockNum) {
Args.getSolidityContractLogTriggerMap().remove(blockNum);
Args.getSolidityContractEventTriggerMap().remove(blockNum);
}There was a problem hiding this comment.
Enabling only isContractLogTriggerEnable = true and isContractLogTriggerRedundancy = true will not write data; only enabling the solid event switch will write data.
| } catch (Throwable throwable) { | ||
| logger.error(throwable.getMessage(), throwable); | ||
| khaosDb.removeBlk(block.getBlockId()); | ||
| clearSolidityContractTriggerCache(block.getNum()); |
There was a problem hiding this comment.
[SHOULD] switchFork has two applyBlock failure paths that don't clear the cache.
There was a problem hiding this comment.
Repeated chain switching is a historical oversight and will not be addressed in this PR. It can be discussed separately in a later issue.
| try { | ||
| contractTriggerCapsule.processTrigger(); | ||
| } catch (Throwable throwable) { | ||
| logger.warn("Post contract trigger failed. {}", throwable.getMessage()); |
There was a problem hiding this comment.
[SHOULD] logger.warn loses stack trace; trigger failures will be undebuggable.
Suggestion:
} catch (Throwable throwable) {
logger.warn("Post contract trigger failed.", throwable);
}| } | ||
|
|
||
| @Test | ||
| public void testReOrgContractTriggerClearsMap() throws Exception { |
There was a problem hiding this comment.
[NIT] testReOrgContractTriggerClearsMap doesn't actually call reOrgContractTrigger — the name is misleading
There was a problem hiding this comment.
@0xbigapple, you're right. The test name promises the reorg path but only exercises clearSolidityContractTriggerCache, which also overlaps with testClearSolidityContractTriggerCache below. I'll rewrite this case to actually go through reOrgContractTrigger (construct a forked block and assert the corresponding entry is removed from the cache) so the name and behavior line up.
…trigger processing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2831e35 to
7d33b6a
Compare
What does this PR do?
Fix two correctness bugs in event service rollback handling that cause solidity-side event/log triggers to either leak fork- removed entries or be lost during reorg.
When a block is rolled back during fork resolution, its contract triggers are marked
removed=true. The solidity event pipeline depends on this flag plus an in-memory map (Args.getSolidityContractEventTriggerMap/getSolidityContractLogTriggerMap) keyed by block number. Two paths through this pipeline were broken:Removed triggers reached the solidity sink.
ContractTriggerCapsule.processTrigger()only checked
isSolidityEventTriggerEnable / isSolidityLogTriggerEnable, never theremovedflag. Triggers from forked-out blocks were emitted as valid solidity events.Async enqueue raced with reorg cache clearing.
Manager.postContractTriggerposted to a global
triggerCapsuleQueuefor async draining, butreOrgContractTriggerclears the in-memory cache synchronously. Capsules alreadyin the queue could reference state that was about to be invalidated, leading to
either lost events or stale cache reads.
Solidity caches were not cleared on block-process failure or shutdown.
getSolidityContract{Log,Event}TriggerMapentries are populated per block.If
processBlockthrew after population, or on node shutdown, those entriesleaked.
Why are these changes required?
This PR has been tested by:
Follow up
Extra details
Fixes #6678