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
- 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.
- 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.
- 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?
OS details
No response
Additional information
No response
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):
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
Note
Hash.0is 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()overchain_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:
Impact
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?
OS details
No response
Additional information
No response