Skip to content

fix(jsonrpc): post log/block filters for reorg-applied blocks#13

Closed
0xbigapple wants to merge 3 commits into
release_v4.8.2from
fix/reorg-jsonrpc-logsfilter
Closed

fix(jsonrpc): post log/block filters for reorg-applied blocks#13
0xbigapple wants to merge 3 commits into
release_v4.8.2from
fix/reorg-jsonrpc-logsfilter

Conversation

@0xbigapple

@0xbigapple 0xbigapple commented Jun 4, 2026

Copy link
Copy Markdown
Owner

What does this PR do?

Adds reApplyLogsFilter(List<KhaosBlock>), invoked after switchFork has fully applied the
new branch (oldest-first), mirroring blockTrigger's jsonrpc FULL handling so reorg'd blocks
reach the FULL stream like the normal extend path.

Why are these changes required?

On a reorg, pushBlock calls switchFork() and returns early, never reaching
blockTrigger() — the only place that posts the "added" FULL filters (postBlockFilter /
postLogsFilter(block, false, false)). The rollback side withdraws the orphaned branch's
logs (reOrgLogsFilter, removed=true), but nothing re-posts the new branch's logs:
withdraw old, never add new, violating reorg semantics.

So dApps using eth_newFilter + eth_getFilterChanges miss every log in blocks applied via
switchFork. eth_getLogs (canonical-chain snapshot) is unaffected, which is why the gap is
easy to overlook.

This PR has been tested by:

  • Unit Tests
  • Manual Testing

Follow up

  • Event-plugin block/transaction triggers and the failed-rollback
    (switch-back) path are dropped on the same early-return; out of scope, known limitations.

Extra details

  • removed=true applies to the log filter (eth_newFilter) only. The block filter
    (eth_newBlockFilter) is a block-hash stream with no removed concept — per Ethereum
    semantics it is not withdrawn on a reorg, so the rollback side posts no block filter and
    only the new branch gets an added block filter.
  • Scope: jsonrpc FULL filters only (postBlockFilter + postLogsFilter).

Summary by cubic

Fixes missed FULL-node JSON-RPC filters after reorgs and hardens eth_newFilter topic/address parsing. After fork switches, we now emit block and log filters for the new canonical branch.

  • Bug Fixes
    • Added reApplyLogsFilter(List<KhaosBlock>) after switchFork to post FULL block/log filters for the new branch (oldest-first); includes a reorg test that withdraws orphaned logs and delivers new ones with correct removed flags. Documented the method and call site for clarity.
    • Restored LogFilter compatibility and validation: accept odd-length hex topics (padded) and reject non-string or malformed addresses/topics with JSON-RPC -32602.

Written for commit b2e3f7e. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability during blockchain fork reorganization by ensuring proper filter reapplication in exception recovery scenarios.
  • Tests

    • Added test coverage for filter handling during fork switch operations.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Manager's fork-switch exception recovery now re-enqueues block and logs filters for the re-applied branch via a new reApplyLogsFilter helper. A test validates this by driving a fork reorg and asserting filter capsule queue updates appear with the correct removed-state flags.

Changes

Fork-switch filter re-enqueuing

Layer / File(s) Summary
Filter re-enqueuing helper and integration
framework/src/main/java/org/tron/core/db/Manager.java
New private reApplyLogsFilter(List<KhaosBlock> newBranch) helper enqueues postBlockFilter and postLogsFilter calls for each block when JSON-RPC filtering is enabled. The switchFork exception handler invokes this to restore filter queues after re-applying a competing branch.
Fork reorg filter verification test
framework/src/test/java/org/tron/core/db/ManagerTest.java
Test constructs a fork scenario, triggers switchFork via conflicting branches, and asserts the FULL logs filter queue receives LogsFilterCapsule entries for orphaned blocks (marked removed) and newly-canonical blocks (marked not removed) matched by block hash. Includes helpers to build transaction/block capsules and scan filter queue.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A fork in the road, but we won't lose our way,
Re-enqueue the filters, let logic replay!
Old branches removed, new ones take the throne,
Through queue verification, we test what we've sown.
✨ One cohesive path where recovery's shown!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately summarizes the main change: adding filter posting for blocks that are re-applied during fork reorganization, which is the core objective of the PR.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/reorg-jsonrpc-logsfilter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@0xbigapple

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
framework/src/main/java/org/tron/core/db/Manager.java (1)

2288-2298: ⚡ Quick win

Extract the FULL-node filter posting into a shared helper.

This replays the same postBlockFilter(...)/postLogsFilter(..., false, false) pair already embedded in blockTrigger(...) at framework/src/main/java/org/tron/core/db/Manager.java:1423-1426. Keeping two copies of that contract is how the reorg path drifted in the first place; a small shared helper would keep the normal-extend and switch-fork paths aligned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/src/main/java/org/tron/core/db/Manager.java` around lines 2288 -
2298, Extract the duplicate FULL-node filter posting into a new private helper
(e.g., postFullNodeFilters or postFullStreamFilters) that accepts a BlockCapsule
and encapsulates the two calls postBlockFilter(blockCapsule, false) and
postLogsFilter(blockCapsule, false, false); then replace the inline pairs in
both reApplyLogsFilter(List<KhaosBlock> newBranch) and the blockTrigger(...)
path with a call to this new helper (preserving the surrounding
CommonParameter.getInstance().isJsonRpcHttpFullNodeEnable() check and any
boolean context).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@framework/src/test/java/org/tron/core/db/ManagerTest.java`:
- Around line 1551-1624: The test switchForkShouldPostFullNodeFilterForNewBranch
sets CommonParameter.getInstance().jsonRpcHttpFullNodeEnable = true but never
restores it; wrap the test body in try/finally (or capture the original boolean
before changing) and in the finally block reset
CommonParameter.getInstance().jsonRpcHttpFullNodeEnable back to its original
value so subsequent tests are unaffected; ensure the finally executes even on
exceptions so the flag is always restored.

---

Nitpick comments:
In `@framework/src/main/java/org/tron/core/db/Manager.java`:
- Around line 2288-2298: Extract the duplicate FULL-node filter posting into a
new private helper (e.g., postFullNodeFilters or postFullStreamFilters) that
accepts a BlockCapsule and encapsulates the two calls
postBlockFilter(blockCapsule, false) and postLogsFilter(blockCapsule, false,
false); then replace the inline pairs in both reApplyLogsFilter(List<KhaosBlock>
newBranch) and the blockTrigger(...) path with a call to this new helper
(preserving the surrounding
CommonParameter.getInstance().isJsonRpcHttpFullNodeEnable() check and any
boolean context).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a2ca6477-a955-4965-b10f-90f3ea5d02cf

📥 Commits

Reviewing files that changed from the base of the PR and between a41321c and c509397.

📒 Files selected for processing (2)
  • framework/src/main/java/org/tron/core/db/Manager.java
  • framework/src/test/java/org/tron/core/db/ManagerTest.java

Comment on lines +1551 to +1624
public void switchForkShouldPostFullNodeFilterForNewBranch() throws Exception {
CommonParameter.getInstance().jsonRpcHttpFullNodeEnable = true;
// filterProcessLoop only starts when isJsonRpcFilterEnabled() held at Manager.init() time; it
// was false then, so filterCapsuleQueue is produce-only here and fully observable.

// bootstrap a head with a known witness
String key = PublicMethod.getRandomPrivateKey();
byte[] privateKey = ByteArray.fromHexString(key);
final ECKey ecKey = ECKey.fromPrivate(privateKey);
byte[] address = ecKey.getAddress();
ByteString addressByte = ByteString.copyFrom(address);
chainManager.getAccountStore().put(addressByte.toByteArray(),
new AccountCapsule(Protocol.Account.newBuilder().setAddress(addressByte).build()));
WitnessCapsule witnessCapsule = new WitnessCapsule(addressByte);
chainManager.getWitnessScheduleStore().saveActiveWitnesses(new ArrayList<>());
chainManager.addWitness(addressByte);
chainManager.getWitnessStore().put(address, witnessCapsule);
Block block = blockGenerate.getSignedBlock(
witnessCapsule.getAddress(), 1533529947843L, privateKey);
dbManager.pushBlock(new BlockCapsule(block));

Map<ByteString, String> keys = addTestWitnessAndAccount();
keys.put(addressByte, key);

// fund an owner; transfers go owner -> witness 'address' (an existing account)
ECKey ownerKey = new ECKey(Utils.getRandom());
byte[] owner = ownerKey.getAddress();
AccountCapsule ownerAccount = new AccountCapsule(
Protocol.Account.newBuilder().setAddress(ByteString.copyFrom(owner)).build());
ownerAccount.setBalance(1_000_000_000L);
chainManager.getAccountStore().put(owner, ownerAccount);

long t = 1533529947843L;
long base = chainManager.getDynamicPropertiesStore().getLatestBlockHeaderNumber();

// common ancestor P (empty) — fork point and tapos reference
BlockCapsule p = createTestBlockCapsule(t + 3000, base + 1,
chainManager.getDynamicPropertiesStore().getLatestBlockHeaderHash().getByteString(), keys);
dbManager.pushBlock(p);

long expiration = t + 1_000_000L;
BlockingQueue<FilterTriggerCapsule> queue =
ReflectUtils.getFieldValue(dbManager, "filterCapsuleQueue");
queue.clear();

// old branch: A carries a transfer; applied via the normal extend path
BlockCapsule a = blockWithTransfer(t + 6000, base + 2, p.getBlockId().getByteString(), keys,
transfer(owner, address, 1L, p, expiration));
dbManager.pushBlock(a);
Assert.assertEquals("control: head should be A after normal extend",
a.getBlockId(), chainManager.getDynamicPropertiesStore().getLatestBlockHeaderHash());
Assert.assertTrue("control: normal-path block A's logs must reach FULL stream (added)",
hasFullLogsFilter(queue, a, false));

// heavier competing branch P -> B1 -> B2, each carrying a transfer, to force switchFork
BlockCapsule b1 = blockWithTransfer(t + 6001, base + 2, p.getBlockId().getByteString(), keys,
transfer(owner, address, 2L, p, expiration));
dbManager.pushBlock(b1); // num <= head -> kept in khaosDb, no switch yet
BlockCapsule b2 = blockWithTransfer(t + 9000, base + 3, b1.getBlockId().getByteString(), keys,
transfer(owner, address, 3L, p, expiration));
dbManager.pushBlock(b2); // num > head & parent != head -> triggers switchFork

Assert.assertEquals("reorg must switch the canonical head to the competing branch (B2)",
b2.getBlockId(), chainManager.getDynamicPropertiesStore().getLatestBlockHeaderHash());

// reorg withdraws the orphaned old-branch logs (removed=true)
Assert.assertTrue("reorg: orphaned block A's logs must be withdrawn (removed=true)",
hasFullLogsFilter(queue, a, true));
// the fix: new canonical blocks' logs are delivered (added, i.e. removed=false)
Assert.assertTrue("reorg: new canonical block B1's logs must reach FULL stream (added)",
hasFullLogsFilter(queue, b1, false));
Assert.assertTrue("reorg: new canonical block B2's logs must reach FULL stream (added)",
hasFullLogsFilter(queue, b2, false));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restore the FULL-node flag in a finally block.

This test mutates the global CommonParameter singleton and never puts it back. If another test runs afterward in the same JVM, it can start seeing extra FULL-node filter postings and fail nondeterministically.

Suggested fix
   `@Test`
   public void switchForkShouldPostFullNodeFilterForNewBranch() throws Exception {
-    CommonParameter.getInstance().jsonRpcHttpFullNodeEnable = true;
+    boolean originalFullNodeEnable = CommonParameter.getInstance().jsonRpcHttpFullNodeEnable;
+    CommonParameter.getInstance().jsonRpcHttpFullNodeEnable = true;
     // filterProcessLoop only starts when isJsonRpcFilterEnabled() held at Manager.init() time; it
     // was false then, so filterCapsuleQueue is produce-only here and fully observable.
+    BlockingQueue<FilterTriggerCapsule> queue =
+        ReflectUtils.getFieldValue(dbManager, "filterCapsuleQueue");
+    try {
 
-    // bootstrap a head with a known witness
-    String key = PublicMethod.getRandomPrivateKey();
-    byte[] privateKey = ByteArray.fromHexString(key);
-    final ECKey ecKey = ECKey.fromPrivate(privateKey);
-    byte[] address = ecKey.getAddress();
-    ByteString addressByte = ByteString.copyFrom(address);
-    chainManager.getAccountStore().put(addressByte.toByteArray(),
-        new AccountCapsule(Protocol.Account.newBuilder().setAddress(addressByte).build()));
-    WitnessCapsule witnessCapsule = new WitnessCapsule(addressByte);
-    chainManager.getWitnessScheduleStore().saveActiveWitnesses(new ArrayList<>());
-    chainManager.addWitness(addressByte);
-    chainManager.getWitnessStore().put(address, witnessCapsule);
-    Block block = blockGenerate.getSignedBlock(
-        witnessCapsule.getAddress(), 1533529947843L, privateKey);
-    dbManager.pushBlock(new BlockCapsule(block));
+      // existing test body
+    } finally {
+      CommonParameter.getInstance().jsonRpcHttpFullNodeEnable = originalFullNodeEnable;
+      queue.clear();
+    }
-    ...
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/src/test/java/org/tron/core/db/ManagerTest.java` around lines 1551
- 1624, The test switchForkShouldPostFullNodeFilterForNewBranch sets
CommonParameter.getInstance().jsonRpcHttpFullNodeEnable = true but never
restores it; wrap the test body in try/finally (or capture the original boolean
before changing) and in the finally block reset
CommonParameter.getInstance().jsonRpcHttpFullNodeEnable back to its original
value so subsequent tests are unaffected; ensure the finally executes even on
exceptions so the flag is always restored.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Re-trigger cubic

@0xbigapple 0xbigapple changed the title fix(jsonrpc): post FULL-node filters for reorg-applied blocks fix(jsonrpc): post log/block filters for reorg-applied blocks Jun 4, 2026
…er elements

 - Restore topicToByteArray for LogFilter topics, guard with (0x)?[0-9a-fA-F]{63,64}$ so the stripped zero is padded back by ByteArray.fromHexString, while non-hex or wrong-length input still gets a clean -32602.
 - LogFilter: validate element types before the (String) cast in the address array and nested topic array loops.
@0xbigapple 0xbigapple closed this Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant