Skip to content

reconsiderblock / invalidateblock can leave the node on a lighter chain than Core #1080

Closed

Description

@niteshbalusu11

invalidateblock + reconsiderblock can leave rbitcoin on a chain with less work than Bitcoin Core picks for the same calls. Only these two operator RPCs are affected. No invalid block is ever accepted.

Found by running the same regtest steps on Core 31.1 and rbitcoin.

Repro 1

  1. Mine a1..a10.
  2. invalidateblock a10, then invalidateblock a8.
  3. Mine b8, b9 on a7.
  4. reconsiderblock a8.

Core ends on a10. rbitcoin stays on b9.

Why: each restored path is re-applied on its own. [a8, a9] only ties b8..b9, and [a10] has no parent on the chain yet.

Repro 2

  1. Mine a1..a5.
  2. invalidateblock a5.
  3. Mine b5, b6.
  4. reconsiderblock a5 (both stay on b6, which is fine).
  5. invalidateblock b5.

Core ends on a5. rbitcoin falls back to a4.

Why: after the reconsider, a5 isn't kept as a candidate.

What I tried

I tried two small fixes in a draft PR (#1079, now closed). Review found that each one broke other cases:

  • Reusing the after-invalidateblock pick: an invalid held branch blocked recovery, header-only and connected blocks showed up as valid-fork, and a restore longer than 10,000 blocks stopped short.
  • Joining the restored paths into one branch: a sibling fork sharing a restored prefix got dropped (Core reaches b4, this stopped at a3). A block that failed validation but still has an archived body got listed as valid-fork. Ordering paths by height hit the 10,000-header lookup cap.

Question

A proper fix looks like it needs to know whether each block was fully validated, the way Core tracks block status, not just whether its body is stored.

How would you like this handled? I'm happy to do the work once there's a direction.

Activity

  1. reardencode commented on Oct 19, 2026

    @reardencode
    Owner

    Here's grok and my proposed fix story - let us know what you and your clanker think:

    Today reconsider_block_inner clears the invalid marks, then calls accept_branch_inner on each stored path by itself. try_apply_after_invalidate never sees those paths. A path whose parent is in another path dies as branch parent not on chain, and a path that loses is not inserted into fork_tips.

    Replace that per-path apply with this:

    1. Clear invalid marks from the stored path vectors, as now.
    2. Build the candidate leaves. Start from fork_tips, held bodies, and preciousblock. Add the last hash of each path whose mark this call just cleared. Drop any hash that is the parent of another candidate, and drop any hash with no reconstructable body.
    3. Build one branch per leaf by chaining the stored path vectors until the parent is on the active chain. Do not rediscover that chain with the 10,000-step prev_of walk.
    4. Rank those branches with the existing rule: more total chain work wins; equal work prefers precious, then the earlier held tip.
    5. Connect the winner once, through accept_branch. If that connect fails, leave the tip where it is and try the next branch.
    6. After that, insert into fork_tips each leaf that did not become the tip, and only if every block on its branch was on an active-chain invalidate path. Those blocks were fully connected before the mark. A block that only failed validation, a header-only hash, and an ancestor of a leaf stay out of fork_tips.

    invalidateblock keeps writing one contiguous path from the invalidated height through the tip. The later selection uses the same picker, so a leaf parked in step 6 is visible the next time the tip is invalidated.

  2. added a commit that references this issue on Oct 19, 2026
    db2c43b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions