Skip to content

bug: Zebra reorgs away from an already-adopted tip on equal-work ties, using tip hash instead of first-seen #11240

Description

@wincss

What happened?

When two sibling blocks at the same height have equal cumulative work — which is always the case for siblings on Zcash, since nBits is fully determined by the ancestors — Zebra breaks the tie by comparing the tip block hash and preferring the larger one. Because best_chain() is recomputed as the max of chain_set after every commit, a node that has already validated, adopted, and announced block A will reorg away from it when an equal-work sibling B arrives later, if B's hash happens to sort higher.

This contradicts the protocol specification, diverges from zcashd, and in production it converts propagation wins into coin flips.

Expected behavior

The Zcash protocol spec (§3.3 The Block Chain, quoted in Zebra's own doc comment):

To break ties between leaf blocks, a node will prefer the block that it received first.

zcashd implements this with CBlockIndexWorkComparator: on equal nChainWork it prefers the lower nSequenceId, assigned in ReceivedBlockTransactions in order of full-block receipt. A later-arriving equal-work sibling is not even inserted into setBlockIndexCandidates, so zcashd never performs this switch.

Actual behavior

chain.rs

fn cmp(&self, other: &Self) -> Ordering {
    if self.partial_cumulative_work != other.partial_cumulative_work {
        self.partial_cumulative_work.cmp(&other.partial_cumulative_work)
    } else {
        // ...
        self_hash.0.cmp(&other_hash.0)   // line 2692
    }
}

Note Hash.0 is the raw [u8; 32] in internal byte order, so the comparison is on the reverse of the displayed hex — the tie is effectively decided by the least-significant display bytes.

Combined with best_chain() = chain_iter().next() over chain_set:BTreeSet<Arc<Chain>> (non_finalized_state.rs:257, :839), there is no stickiness: every commit re-evaluates, so an equal-work sibling arriving arbitrarily later within the non-finalized window can displace the current tip.

The existing doc comment acknowledges the divergence ("Despite the consensus rules, Zebra uses the tip block hash as a tie-breaker… This departure from the consensus rules may delay network convergence"), citing parallel downloads and the absence of receipt timestamps as the rationale.

Evidence

From production mining nodes (patched zebrad emitting zcashd-format UpdateTip lines), 2026-08-09, ZEC mainnet. Two of four orphans that day were caused by this rule alone.

Height 3442153 — our block was first on every node by ~0.4 s, and still lost:

node our block adopted competitor inv arrives reorg to competitor
A +0.19 s +0.59 s +1.92 s
B +0.34 s +0.51 s +0.60 s
C +0.44 s +0.60 s +0.95 s

Impact

  1. Propagation investment stops paying off. Under first-seen, winning a tie is a function of how fast you relay — something operators can and do invest in. Under a hash comparison, a node that won the propagation race by a clear margin still loses ~50% of ties. Multiple pools independently report a higher orphan rate since moving to Zebra; this is the mechanism we can demonstrate from logs.
  2. A node fights its own block. In the case above, our own nodes discarded a block we had just mined, announced, and seen echoed back by dozens of peers. The tip change then propagates into the mining stack and resets templates — so the pool's hashrate is wasted to extending a orphan block.
  3. No time bound. Since the comparison is re-run on every commit, a sibling arriving late in the fork window still triggers a reorg of an established tip, with no first-seen protection at all.

What were you doing when the issue happened?

No response

Zebra logs

No response

Zebra Version

No response

Which operating systems does the issue happen on?

  • Linux
  • macOS
  • Windows
  • Other OS

OS details

No response

Additional information

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions