linera-io/linera-protocol · error · ChainError

Justification chain rounds must be strictly increasing

Error message

Justification chain rounds must be strictly increasing

What it means

Thrown by JustificationChain::verify when consecutive links of a justification chain do not have strictly increasing rounds (ChainError::JustificationRoundsNotIncreasing). A justification chain is an ordered sequence of quorums where each link must sit in a higher round than the link it justifies; equal or decreasing rounds break the chain's causal structure. verify scans every adjacent pair (windows(2)) and rejects the first non-increasing step.

Source

Thrown at linera-chain/src/justification/mod.rs:219

    ///
    /// Link signatures are deliberately *not* verified here. The quorum built on top of this
    /// chain signs the chain's commitment, so a single signature check over that quorum attests
    /// every link below it: each link's voters verified the quorum beneath them before signing
    /// over its hash. Signing over an invalid quorum is itself an attributable fault, so link
    /// signatures are only re-checked when auditing a chain to blame the validators that attested
    /// an invalid link.
    ///
    /// The chain length needs no explicit cap: strictly increasing rounds mean a chain of `n`
    /// links spans `n` distinct rounds, and each link must carry a genuine quorum, so a longer
    /// chain necessarily reaches a higher round and cannot be inflated cheaply. The observed
    /// length is recorded as a metric so real contention shows up in monitoring.
    pub fn verify(&self, value_hash: CryptoHash) -> Result<Option<CryptoHash>, ChainError> {
        #[cfg(with_metrics)]
        metrics::JUSTIFICATION_CHAIN_LENGTH
            .with_label_values(&[])
            .observe(self.links.len() as f64);
        for window in self.links.windows(2) {
            ensure!(
                window[0].round < window[1].round,
                ChainError::JustificationRoundsNotIncreasing
            );
        }
        Ok(self.commitment(value_hash))
    }
}

/// A confirmed block's *header* together with the justification that makes it self-contained
/// evidence: the round and quorum of `ConfirmedBlock` votes that finalized it, and the chain of
/// `ValidatedBlock` quorums for the same block, with its top link in the round the block was
/// confirmed. Only the header travels, never the block body. The header hashes to the value the
/// votes sign (`CryptoHash::new(&header)`) and carries the chain ID and height that scope the
/// fault. This is the shape a `ConfirmedBlockCertificate` reduces to for fault attribution.
#[derive(Clone, Debug)]
pub struct JustifiedConfirmation {
    /// The header of the confirmed block. Its hash is what the chain's `ValidatedBlock` and
    /// `ConfirmedBlock` votes sign (`ValidatedBlock` and `ConfirmedBlock` wrap the same block).

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Append links in strictly increasing round order — sort collected quorums by round before building the chain.
  2. Validate round ordering at deserialization/network ingress with verify() before storing or forwarding the chain.
  3. If building from a map of round -> quorum, iterate rounds sorted ascending.
  4. Treat chains failing this check as malicious input; do not attempt repair.

Example fix

// before
let chain = JustificationChain::new(links_as_received); // unordered
chain.verify(value_hash)?; // Err: rounds not increasing

// after
let mut links = links_as_received;
links.sort_by_key(|l| l.round);
links.dedup_by_key(|l| l.round);
let chain = JustificationChain::new(links);
chain.verify(value_hash)?;
Defensive patterns

Strategy: validation

Validate before calling

// verify round ordering before accepting a chain
for pair in chain.links().windows(2) {
    if pair[0].round >= pair[1].round {
        return Err(anyhow::anyhow!("justification chain rounds not strictly increasing"));
    }
}

Type guard

fn has_increasing_rounds(chain: &JustificationChain) -> bool {
    chain.links().windows(2).all(|w| w[0].round < w[1].round)
}

Try / catch

match chain.verify(value_hash) {
    Err(ChainError::JustificationRoundsNotIncreasing) => {
        // malformed or malicious chain: reject and penalize the sender
        Err(anyhow::anyhow!("rejecting malformed justification chain"))
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling JustificationChain::verify(value_hash) on a chain built by appending links out of round order, appending a link whose round is <= the current top link's round (the append API documents that new links must be in higher rounds), or deserializing a network-received chain with malformed ordering.

Common situations: Assembling chains from votes collected out of order without sorting by round; a Byzantine validator shipping a crafted chain with repeated rounds to probe clients; serialization bugs that reorder links; tests constructing chains with default Round::new(0) for every link.

Related errors


AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22). Data as JSON: /api/errors/15c33f1ca623afe6. Report an issue: GitHub.