What happened?
I expected to see this happen:
After the chain tip advances, get_block_template returns work built on the new best tip.
Instead, this happened:
During an equal-height non-finalized reorg, get_block_template stops returning new work and keeps answering with the previous height's parent, until the next block breaks the tie — even though the state's best tip has already advanced.
What were you doing when the issue happened?
Found by source review of the get_block_template path (v1.4.0). No repro steps provided.
Zakura Version
v1.4.0 (same path on current master)
Additional information
The get_block_template long-poll loop gates emission on two conditions that both keep failing during an equal-height reorg.
1. Tip-agreement guard. The template parent comes from fetch_chain_info (methods.rs:3031, def get_block_template.rs:970), which reads ReadRequest::ChainInfo. Before returning, the loop checks the chain-tip watch against it: latest_chain_tip.best_tip_hash().is_some_and(|tip| tip != chain_info.tip_hash) → continue (methods.rs:3032-3033; the other path at methods.rs:2872-2875). On an equal-work fork, best_chain is decided by the raw tip-hash tie-break (ChainScore::cmp, frontier.rs:93-96; Chain::cmp, chain.rs:2659). Each same-height candidate committed through update_latest_chain_channels (write.rs:1371; non_finalized_state_sender.send at 1392, set_best_non_finalized_tip at 1394) can flip the best tip between the two observation points, so the guard keeps hitting continue.
2. Self-validation withhold. prepare_template_in_background (methods.rs:1286) submits the built template to ReadRequest::CheckBlockProposalValidity, whose handler requires the proposal's previous_block_hash == best_tip (service.rs:3404; check at ~3421: "proposal is not based on the current best chain tip"). If the best tip flipped between build and validate, this fails and finish_mining_template (methods.rs:1206) withholds / returns an error, so the miner keeps the previous job (TemplateRejections, get_block_template.rs:71; reject at 89, needs_fallback at 109).
Recovery. Only the next block (H+1) breaks the equal-work tie with greater SuffixWork (frontier.rs:93-96), stabilizing best_chain. The loop then sees a parent change and calls set_parent (get_block_template.rs:80, invoked at methods.rs:2879/3038), which clears TemplateRejections; both gates pass and work resumes.
Effect. Within the reorg window the node's best tip has advanced, but get_block_template keeps serving work for the already-superseded previous height.
Related. Same class as #14 (equal-height non-finalized reorg → stale state read) and #360 (multi-read RPC atomicity), but a distinct path (getblocktemplate withholding work).
Suggested direction. On tip change, emit work for the current best tip immediately instead of withholding on a parent flip — serving a template that may lose the tie is better than serving one for an already-superseded height. This matches upstream Zebra, which has neither the tip-agreement guard nor the TemplateRejections withhold.
What happened?
I expected to see this happen:
Instead, this happened:
What were you doing when the issue happened?
Found by source review of the
get_block_templatepath (v1.4.0). No repro steps provided.Zakura Version
v1.4.0 (same path on current master)
Additional information
The
get_block_templatelong-poll loop gates emission on two conditions that both keep failing during an equal-height reorg.1. Tip-agreement guard. The template parent comes from
fetch_chain_info(methods.rs:3031, defget_block_template.rs:970), which readsReadRequest::ChainInfo. Before returning, the loop checks the chain-tip watch against it:latest_chain_tip.best_tip_hash().is_some_and(|tip| tip != chain_info.tip_hash)→continue(methods.rs:3032-3033; the other path atmethods.rs:2872-2875). On an equal-work fork,best_chainis decided by the raw tip-hash tie-break (ChainScore::cmp,frontier.rs:93-96;Chain::cmp,chain.rs:2659). Each same-height candidate committed throughupdate_latest_chain_channels(write.rs:1371;non_finalized_state_sender.sendat1392,set_best_non_finalized_tipat1394) can flip the best tip between the two observation points, so the guard keeps hittingcontinue.2. Self-validation withhold.
prepare_template_in_background(methods.rs:1286) submits the built template toReadRequest::CheckBlockProposalValidity, whose handler requires the proposal'sprevious_block_hash == best_tip(service.rs:3404; check at ~3421: "proposal is not based on the current best chain tip"). If the best tip flipped between build and validate, this fails andfinish_mining_template(methods.rs:1206) withholds / returns an error, so the miner keeps the previous job (TemplateRejections,get_block_template.rs:71;rejectat89,needs_fallbackat109).Recovery. Only the next block (H+1) breaks the equal-work tie with greater
SuffixWork(frontier.rs:93-96), stabilizingbest_chain. The loop then sees a parent change and callsset_parent(get_block_template.rs:80, invoked atmethods.rs:2879/3038), which clearsTemplateRejections; both gates pass and work resumes.Effect. Within the reorg window the node's best tip has advanced, but
get_block_templatekeeps serving work for the already-superseded previous height.Related. Same class as #14 (equal-height non-finalized reorg → stale state read) and #360 (multi-read RPC atomicity), but a distinct path (getblocktemplate withholding work).
Suggested direction. On tip change, emit work for the current best tip immediately instead of withholding on a parent flip — serving a template that may lose the tie is better than serving one for an already-superseded height. This matches upstream Zebra, which has neither the tip-agreement guard nor the
TemplateRejectionswithhold.