linera-io/linera-protocol · error · ChainError

Signatures in a certificate must form a quorum

Error message

Signatures in a certificate must form a quorum

What it means

Thrown by check_signatures when the summed voting weight of a certificate's signers is below the committee's quorum threshold (ChainError::CertificateRequiresQuorum). Even if every individual signature is valid, a certificate is only meaningful if it proves agreement of a quorum (by voting weight) of the committee. The check runs after per-validator membership checks and before batch signature verification.

Source

Thrown at linera-chain/src/data_types/mod.rs:1160

    signatures: &[(ValidatorPublicKey, ValidatorSignature)],
    committee: &Committee,
) -> Result<(), ChainError> {
    // Check the quorum.
    let mut weight = 0;
    let mut used_validators = HashSet::new();
    for (validator, _) in signatures {
        // Check that each validator only appears once.
        ensure!(
            !used_validators.contains(validator),
            ChainError::CertificateValidatorReuse
        );
        used_validators.insert(*validator);
        // Update weight.
        let voting_rights = committee.weight(validator);
        ensure!(voting_rights > 0, ChainError::InvalidSigner);
        weight += voting_rights;
    }
    ensure!(
        weight >= committee.quorum_threshold(),
        ChainError::CertificateRequiresQuorum
    );
    // All that is left is checking signatures!
    ValidatorSignature::verify_batch(value, signatures.iter())?;
    Ok(())
}

impl BcsSignable<'_> for ProposalContent {}

impl BcsSignable<'_> for VoteValue {}

doc_scalar!(
    MessageAction,
    "Whether an incoming message is accepted or rejected."
);

#[cfg(test)]

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Ensure the certificate is only used after the aggregator reached quorum — SignatureAggregator::append returns Some(cert) exactly when the threshold is met.
  2. Recollect the missing signatures from validators until quorum before verifying.
  3. Verify against the committee (epoch) that produced the certificate, so the threshold matches the signing set.
  4. If weight was lost to deduplication, confirm the removed entries really were duplicates and not distinct validators.

Example fix

// before
let cert = aggregator.partial_clone_hack(); // partial, below quorum
check_signatures(&value, &cert.signatures, &committee)?;

// after
let mut aggregator = SignatureAggregator::new(value, round, unlocking, first_round, commitment, &committee);
let cert = loop {
    let (key, sig) = receive_vote().await?;
    if let Some(cert) = aggregator.append(key, sig)? {
        break cert; // quorum reached
    }
};
Defensive patterns

Strategy: retry

Validate before calling

let weight: u64 = signatures.iter().map(|(k, _)| committee.weight(k)).sum();
if weight < committee.quorum_threshold() {
    return Err(anyhow::anyhow!(
        "insufficient voting weight: {}/{}",
        weight,
        committee.quorum_threshold()
    ));
}

Try / catch

match check_signatures(&value, &signatures, committee) {
    Err(ChainError::CertificateRequiresQuorum) => {
        // collect more signatures from remaining validators, then retry
        collect_missing_signatures(&mut signatures, committee).await?;
        check_signatures(&value, &signatures, committee)
    }
    other => other,
}

Prevention

When it happens

Trigger: Verifying a certificate whose signature set aggregates less than committee.quorum_threshold() voting weight — e.g. a partial certificate captured before quorum was reached, a certificate checked against a larger committee than the one that signed it, or signatures pruned/lost in transit.

Common situations: Persisting or forwarding a partial (non-final) certificate from SignatureAggregator (append returned None and the partial state leaked); committee enlargement between epochs raising the threshold; network paths dropping some signatures; tests using too few validator keys.

Understand the failure class

Related errors


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