Skip to content

bug: getblocktemplate withholds fresh work during an equal-height non-finalized reorg, serving a stale parent until the next block #1080

Description

@learnprogram2

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.

Activity

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions