Skip to content

Bug: Shared failedChan causes cross-chain block number contamination in rescanner #72

@anhthii

Description

@anhthii

Bug Description

The rescanner worker consumes failed block events from other chains, causing it to retry block numbers that are invalid for its own chain. This results in errors like Bitcoin testnet trying to fetch Solana-scale block heights (~449M).

Root Cause

In factory.go:743, a single failedChan is created and shared across all chains:

failedChan := make(chan FailedBlockEvent, 100)

All chain workers (Solana, Bitcoin, EVM, etc.) write to this same channel via base.go:146:

bw.failedChan <- FailedBlockEvent{
    Chain:   bw.chain.GetName(),  // Chain field is set...
    Block:   result.Number,
    Attempt: 1,
}

But the rescanner reads from it without filtering by chain (rescanner.go:77-81):

for evt := range rw.failedChan {
    // evt.Chain is ignored — any chain's failed block gets added
    rw.addFailedBlock(evt.Block, ...)
}

So when a Solana devnet block (~449M) fails, the Bitcoin testnet rescanner can consume it and attempt to fetch block 449,310,635 from Bitcoin testnet RPC, which fails with "Block height out of range".

Observed Symptoms

DBG Provider failed but not switching provider=bitcoin_testnet-1
    url=https://bitcoin-testnet-rpc.publicnode.com
    error="failed to get block hash for height 449310635: getblockhash failed:
           getblockhash RPC error: RPC error -8: Block height out of range"

Bitcoin testnet latest block is ~4.8M. The 449M height is a Solana devnet slot number.

Proposed Fix

Create a per-chain failedChan instead of a single shared one. Move the channel creation inside the per-chain loop in CreateManagerWithWorkers:

for _, chainName := range managerCfg.Chains {
    // ...
    failedChan := make(chan FailedBlockEvent, 100)  // per-chain channel
    deps := WorkerDeps{
        // ...
        FailedChan: failedChan,
    }
    // ...
}

Remove the unused failedChan field from Manager (it stores but never reads from it).

Note: A simple filter (if evt.Chain != rw.chain.GetName() { continue }) would NOT work because the channel has multiple consumers — skipped events would be lost instead of reaching the correct chain's rescanner.

Affected Files

  • internal/worker/factory.go — shared channel creation (line 743)
  • internal/worker/rescanner.go — unfiltered consumption (lines 77-81)
  • internal/worker/manager.go — unused failedChan field
  • internal/worker/base.go — event producer (line 146)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions