Skip to content

Investigate nil-precommit timeout certificates for Tenderlink fork recovery #283

Description

@zmanian

Summary

Tenderlink currently treats 2f+1 nil precommits as evidence that the current round did not decide, but not as evidence that validators should stop carrying the round's prior PoW finalization candidate.

This is normal Tendermint-style behavior, but Crosslink has an extra liveness pressure: the value being finalized is a PoW checkpoint sampled from the node's current best chain. During fork recovery, the PoW best chain may move forward or switch forks while BFT is stuck around an older candidate.

The design question is whether a 2f+1 PRECOMMIT nil certificate for (height, round) should explicitly cancel the liveness momentum created by that same round's prevotes, allowing a later round to sample a fresher tip - sigma checkpoint from the PoW chain.

Current behavior

At a Tenderlink BFT height, each round runs:

Propose -> Prevote -> Precommit

Current code behavior at the pinned Tenderlink rev:

  • 2f+1 yes prevotes set locked_value_round and valid_value_round, then precommit that value.
  • 2f+1 nil prevotes cause a nil precommit.
  • 2f+1 precommits of any kind only start the precommit timeout.
  • Only 2f+1 yes precommits decide a value and advance the BFT height.
  • locked_value_round and valid_value_round reset only after a value is decided.

Relevant local areas:

  • zebra-crosslink/Cargo.toml pins the external tenderlink dependency.
  • Tenderlink TMState::bft_update handles the Tendermint conditions for prevote, precommit, decision, catch-up, and timeouts.
  • Zebra's proposer closure samples the current PoW tip and proposes tip - sigma only when Tenderlink does not already have a carried valid_value_round.

Why this matters

During fork recovery, the PoW best chain may move forward or switch forks while the BFT round is between prevote and precommit.

The conservative behavior is:

  1. If a value got 2f+1 prevotes, keep it sticky.
  2. If it never got 2f+1 yes precommits, keep trying to complete or repropose it in later rounds.
  3. After it decides, the next BFT height can jump forward to a fresher PoW checkpoint.

That preserves the familiar Tendermint safety argument, but it can make Crosslink finalization chase a stale checkpoint even after validators have effectively nil-precommitted the round.

A stronger nil-precommit rule may have better liveness: a 2f+1 PRECOMMIT nil certificate is evidence that this round is dead as a finalization attempt. If the protocol keeps carrying that round's valid_value_round or same-round lock momentum forward anyway, it can keep trying to finalize a PoW snapshot that the live network has already moved past.

Validator-set scaling intuition

The larger motivation is validator-set size.

For a globally distributed validator set, the time to reach GST-like effective synchrony grows with communication delay across the validator set, and the worst-case message pattern can be quadratic. As the validator set grows, the prevote-to-precommit window is more likely to overlap with a PoW stream change: a new longest chain appears, or the current tip - sigma candidate advances.

In the current protocol shape, that stream change can turn into a halt. Validators may have enough prevotes to carry a stale candidate, but not enough matching precommits to decide it. The later rounds then keep trying to recover around stale BFT state rather than simply sampling the current PoW stream.

With nil-precommit resampling, a stream change does not have to halt the BFT height. It can just cause the round number to increment until the network and the PoW stream are stable enough for one candidate to survive through prevote and precommit.

This matches the current Crosslink implementation model: proposals are sampled as head - sigma. The PoW stream continues progressing uninterrupted between BFT rounds, and the BFT layer should be able to re-enter that stream after an abandoned round instead of staying pinned to an obsolete sample.

Ebb-and-Flow composition

This rule is best understood as a liveness bridge between the two Crosslink pieces:

  • The PoW side is the flow layer. It keeps producing availability and may reveal a fresher tip - sigma candidate after a partition heals or a longer fork appears.
  • The BFT side is the ebb/finality layer. It decides which PoW snapshot becomes finalized and must preserve monotonic finality.
  • A nil-precommit certificate says the BFT layer failed to finalize this round, so the next round should be allowed to resample from the current PoW flow.

So the intended behavior is not "flow overrides finality". It is "failed ebb attempts should not force future ebb attempts to keep using stale flow input".

The safety boundary is important:

  • A nil-precommit certificate should cancel same-round prevote momentum.
  • It should allow later proposals to ignore the abandoned round's stale valid_value_round.
  • It may be safe to unlock validators whose lock was created by that same round, because a same-round 2f+1 PRECOMMIT nil certificate should rule out a same-round value commit certificate unless validators equivocate.
  • It should not automatically cancel locks from older rounds. Older locks still need the usual Tendermint-compatible unlock proof.

Proposed rule shape

Introduce a formal nil-precommit certificate:

  • A 2f+1 PRECOMMIT nil certificate for (height, round) is recorded as an abandon certificate for that round.
  • Later proposals may carry this certificate.
  • Validators that verify the certificate may clear round-local carried state derived from that same round.
  • At minimum, this should clear the corresponding stale valid_value_round so proposers can sample the latest PoW tip - sigma.
  • Optionally, the rule may also unlock validators whose locked_value_round was created in the same round as the nil-precommit certificate.
  • The rule must not unlock validators locked in an earlier round unless the proposal carries an independently valid Tendermint-compatible unlock proof.

One crisp formulation:

For a fixed BFT height and round r, 2f+1 PRECOMMIT nil cancels the finalization attempt for round r. It can cancel state created by round r, but it cannot cancel safety locks from rounds < r.

Safety obligations

This should not be implemented as local cleanup without a protocol rule.

The analysis should prove or reject at least the following:

  • For fixed (height, round), 2f+1 PRECOMMIT nil and 2f+1 PRECOMMIT value cannot both exist unless more than f voting power equivocates or an honest validator violates the protocol.
  • A same-round nil-precommit certificate is sufficient evidence that a value locked due to that round's prevotes cannot still be decided in that round.
  • Unlocking only same-round locks does not allow conflicting commits across later rounds.
  • Validators that do not immediately see the nil-precommit certificate remain safe, and later converge when a proposal carries the certificate.
  • Clearing valid_value_round without clearing locked_value_round is safe but may leave liveness on the table; clearing same-round locks may improve liveness further and needs the stronger proof.
  • Proposal validation must define exactly which certificates can be carried and when they are accepted.

Acceptance criteria

  • Document the intended behavior for 2f+1 nil precommits in Crosslink/Tenderlink.
  • Decide whether nil precommit should:
    • only advance the round, as in the current Tendermint-like behavior;
    • clear valid_value_round but not unlock;
    • unlock only same-round locks via a formal nil-precommit certificate;
    • unlock older locks only with a separate Tendermint-compatible proof; or
    • be left unchanged until Crosslink proposal-validity rules are fully implemented.
  • If a new rule is chosen, specify the certificate format, verification rules, and required updates to proposal validation.
  • Add targeted tests covering:
    • stale PoW checkpoint carried across rounds after yes prevotes;
    • nil-precommit quorum with no decision;
    • fork recovery where the best PoW chain changes between prevote and precommit;
    • same-round nil-precommit certificate clearing stale proposal state;
    • safety against conflicting commits across rounds.

Recommendation

Treat this as a protocol-design improvement, not a local bug fix.

My current bias is:

  1. Model 2f+1 PRECOMMIT nil as a first-class round-abandon certificate.
  2. Use it to clear stale valid_value_round so Crosslink can resample from the current PoW flow after a failed finality attempt.
  3. Investigate same-round unlock as the liveness-improving version of the rule.
  4. Preserve older locks unless the protocol has a clear Tendermint-compatible unlock proof.

This version appears to compose well with Ebb-and-Flow: PoW availability keeps moving, BFT finality remains monotonic, and a failed BFT round does not keep dragging stale PoW input into future rounds.

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