{"record":{"id":"6df80362e67f8bdf","repo":"linera-io/linera-protocol","slug":"signatures-in-a-certificate-must-be-from-different","errorCode":null,"errorMessage":"Signatures in a certificate must be from different validators","messagePattern":"Signatures in a certificate must be from different validators","errorType":"validation","errorClass":"ChainError","httpStatus":null,"severity":"error","filePath":"linera-chain/src/data_types/mod.rs","lineNumber":1111,"sourceCode":"    pub fn append(\n        &mut self,\n        public_key: ValidatorPublicKey,\n        signature: ValidatorSignature,\n    ) -> Result<Option<GenericCertificate<T>>, ChainError>\n    where\n        T: CertificateValue,\n    {\n        let hash_and_round = VoteValue(\n            self.partial.hash(),\n            self.partial.round,\n            T::KIND,\n            self.partial.unlocking_round(),\n            self.partial.first_round(),\n            self.partial.justification_commitment(),\n        );\n        signature.check(&hash_and_round, public_key)?;\n        // Check that each validator only appears once.\n        ensure!(\n            !self.used_validators.contains(&public_key),\n            ChainError::CertificateValidatorReuse\n        );\n        self.used_validators.insert(public_key);\n        // Update weight.\n        let voting_rights = self.committee.weight(&public_key);\n        ensure!(voting_rights > 0, ChainError::InvalidSigner);\n        self.weight += voting_rights;\n        // Update certificate.\n        self.partial.add_signature((public_key, signature));\n\n        if self.weight >= self.committee.quorum_threshold() {\n            self.weight = 0; // Prevent from creating the certificate twice.\n            Ok(Some(self.partial.clone()))\n        } else {\n            Ok(None)\n        }\n    }","sourceCodeStart":1093,"sourceCodeEnd":1129,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-chain/src/data_types/mod.rs#L1093-L1129","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Deduplicate votes by ValidatorPublicKey before appending — keep the first (or freshest) vote per key in a BTreeMap/HashSet keyed merge","Ignore duplicate vote deliveries at the network/handler layer instead of forwarding them to the aggregator","When this appears in certificate verification (check_signatures), treat it as Byzantine evidence: reject the certificate and report the signer"],"exampleFix":"// before: appending every vote as it arrives\nfor vote in incoming_votes {\n    aggregator.append(vote.public_key, vote.signature).await?; // may hit reuse\n}\n\n// after: dedupe by validator key first\nlet mut seen = HashSet::new();\nfor vote in incoming_votes {\n    if seen.insert(vote.public_key) {\n        aggregator.append(vote.public_key, vote.signature).await?;\n    }\n}","handlingStrategy":"validation","validationCode":"// Deduplicate votes by validator key BEFORE aggregating (data_types/mod.rs:1093):\nlet mut by_validator: BTreeMap<ValidatorPublicKey, ValidatorSignature> = BTreeMap::new();\nfor (key, sig) in incoming_votes {\n    by_validator.entry(key).or_insert(sig); // first vote per validator wins\n}\nfor (key, sig) in by_validator {\n    if let Some(cert) = aggregator.append(key, sig)? {\n        return Ok(cert);\n    }\n}","typeGuard":"fn has_unique_signers(signatures: &[(ValidatorPublicKey, ValidatorSignature)]) -> bool {\n    let mut seen = HashSet::new();\n    signatures.iter().all(|(k, _)| seen.insert(*k))\n}","tryCatchPattern":"match aggregator.append(key, sig) {\n    Err(ChainError::CertificateValidatorReuse) => {\n        // duplicate delivery of an already-counted vote: skip silently — it carries\n        // no extra weight; do NOT count it toward the quorum\n    }\n    other => other?,\n}","preventionTips":["Merge vote collections keyed by ValidatorPublicKey, never by concatenation","Drop duplicate vote messages at the handler layer","Treat finished certificates with duplicate signers as Byzantine evidence"],"tags":["linera","consensus","signatures","voting","quorum","rust"],"backgroundTag":"duplicate-validator-signature","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}