linera-io/linera-protocol · error · ChainError

Signatures in a certificate must be from different validator

Error message

Signatures in a certificate must be from different validators

What it means

SignatureAggregator::append (linera-chain/src/data_types/mod.rs:1093-1129) rejects a second signature from a validator key already recorded in used_validators (chain.rs 1110-1114): each validator may contribute at most one signature per certificate, so quorum weight comes only from distinct validators. The same guard exists in check_signatures (data_types/mod.rs:1150) when verifying finished certificates, so any certificate containing duplicate signers is invalid on its face — double-counted weight could fake a quorum.

Source

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

    pub fn append(
        &mut self,
        public_key: ValidatorPublicKey,
        signature: ValidatorSignature,
    ) -> Result<Option<GenericCertificate<T>>, ChainError>
    where
        T: CertificateValue,
    {
        let hash_and_round = VoteValue(
            self.partial.hash(),
            self.partial.round,
            T::KIND,
            self.partial.unlocking_round(),
            self.partial.first_round(),
            self.partial.justification_commitment(),
        );
        signature.check(&hash_and_round, public_key)?;
        // Check that each validator only appears once.
        ensure!(
            !self.used_validators.contains(&public_key),
            ChainError::CertificateValidatorReuse
        );
        self.used_validators.insert(public_key);
        // Update weight.
        let voting_rights = self.committee.weight(&public_key);
        ensure!(voting_rights > 0, ChainError::InvalidSigner);
        self.weight += voting_rights;
        // Update certificate.
        self.partial.add_signature((public_key, signature));

        if self.weight >= self.committee.quorum_threshold() {
            self.weight = 0; // Prevent from creating the certificate twice.
            Ok(Some(self.partial.clone()))
        } else {
            Ok(None)
        }
    }

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Deduplicate votes by ValidatorPublicKey before appending — keep the first (or freshest) vote per key in a BTreeMap/HashSet keyed merge
  2. Ignore duplicate vote deliveries at the network/handler layer instead of forwarding them to the aggregator
  3. When this appears in certificate verification (check_signatures), treat it as Byzantine evidence: reject the certificate and report the signer

Example fix

// before: appending every vote as it arrives
for vote in incoming_votes {
    aggregator.append(vote.public_key, vote.signature).await?; // may hit reuse
}

// after: dedupe by validator key first
let mut seen = HashSet::new();
for vote in incoming_votes {
    if seen.insert(vote.public_key) {
        aggregator.append(vote.public_key, vote.signature).await?;
    }
}
Defensive patterns

Strategy: validation

Validate before calling

// Deduplicate votes by validator key BEFORE aggregating (data_types/mod.rs:1093):
let mut by_validator: BTreeMap<ValidatorPublicKey, ValidatorSignature> = BTreeMap::new();
for (key, sig) in incoming_votes {
    by_validator.entry(key).or_insert(sig); // first vote per validator wins
}
for (key, sig) in by_validator {
    if let Some(cert) = aggregator.append(key, sig)? {
        return Ok(cert);
    }
}

Type guard

fn has_unique_signers(signatures: &[(ValidatorPublicKey, ValidatorSignature)]) -> bool {
    let mut seen = HashSet::new();
    signatures.iter().all(|(k, _)| seen.insert(*k))
}

Try / catch

match aggregator.append(key, sig) {
    Err(ChainError::CertificateValidatorReuse) => {
        // duplicate delivery of an already-counted vote: skip silently — it carries
        // no extra weight; do NOT count it toward the quorum
    }
    other => other?,
}

Prevention

When it happens

Trigger: Feeding the same validator's vote twice into the aggregator — duplicated network messages, rebroadcasts from different proxies, retry logic re-appending votes; aggregating from multiple sources without deduplicating by public key; verifying a Byzantine-forged certificate with repeated keys.

Common situations: Vote gossip forwarding the same vote via several paths; proxy nodes echoing messages; aggregator code that merges vote collections with concatenation instead of keying by ValidatorPublicKey.

Understand the failure class

Related errors


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