1use crate::error::{ConsensusError, Result};
38use crate::validator::ValidatorSet;
39use dashmap::DashMap;
40use parking_lot::Mutex;
41use serde::{Deserialize, Serialize};
42use std::collections::HashSet;
43use std::sync::Arc;
44use tenzro_crypto::composite::{
45 CompositePublicKey, CompositeSignature, HybridVerifier, StandardHybridVerifier,
46};
47use tenzro_types::primitives::Address;
48
49pub const TIMEOUT_MSG_FORMAT_VERSION: u8 = 3;
60
61const TIMEOUT_SIGNING_DOMAIN: &[u8] = b"TENZRO_TIMEOUT:";
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct TimeoutMsg {
79 pub format_version: u8,
81
82 pub view: u64,
84
85 pub high_qc_view: u64,
90
91 pub finalized_height: u64,
97
98 pub voter: Address,
100
101 pub signature: CompositeSignature,
103
104 pub public_key: CompositePublicKey,
107}
108
109impl TimeoutMsg {
110 pub fn new(
112 view: u64,
113 high_qc_view: u64,
114 finalized_height: u64,
115 voter: Address,
116 signature: CompositeSignature,
117 public_key: CompositePublicKey,
118 ) -> Self {
119 Self {
120 format_version: TIMEOUT_MSG_FORMAT_VERSION,
121 view,
122 high_qc_view,
123 finalized_height,
124 voter,
125 signature,
126 public_key,
127 }
128 }
129
130 pub fn signing_payload(&self) -> Vec<u8> {
142 let mut payload = Vec::with_capacity(15 + 1 + 8 + 8 + 8 + 32);
143 payload.extend_from_slice(TIMEOUT_SIGNING_DOMAIN);
144 payload.push(self.format_version);
145 payload.extend_from_slice(&self.view.to_le_bytes());
146 payload.extend_from_slice(&self.high_qc_view.to_le_bytes());
147 payload.extend_from_slice(&self.finalized_height.to_le_bytes());
148 payload.extend_from_slice(self.voter.as_bytes());
149 payload
150 }
151
152 pub fn verify(&self, validator_set: &ValidatorSet) -> Result<()> {
164 if self.format_version != TIMEOUT_MSG_FORMAT_VERSION {
165 return Err(ConsensusError::InvalidSignature(format!(
166 "timeout message rejected: unsupported format_version {} (expected {})",
167 self.format_version, TIMEOUT_MSG_FORMAT_VERSION
168 )));
169 }
170
171 if self.high_qc_view >= self.view {
172 return Err(ConsensusError::InvalidSignature(format!(
173 "timeout message rejected: high_qc_view {} >= view {} (impossible)",
174 self.high_qc_view, self.view
175 )));
176 }
177
178 let validator = validator_set.get_by_address(&self.voter).ok_or_else(|| {
179 ConsensusError::NonValidator(format!("Address: {}", self.voter))
180 })?;
181
182 if !validator.is_active() {
183 return Err(ConsensusError::NonValidator(format!(
184 "Validator {} is not active",
185 self.voter
186 )));
187 }
188
189 if self.public_key.classical != validator.public_key {
190 return Err(ConsensusError::InvalidSignature(format!(
191 "timeout classical public key does not match registered validator key for {}",
192 self.voter
193 )));
194 }
195 if self.public_key.pq != validator.pq_public_key {
196 return Err(ConsensusError::InvalidSignature(format!(
197 "timeout PQ public key does not match registered validator key for {}",
198 self.voter
199 )));
200 }
201
202 let payload = self.signing_payload();
203 let verifier = StandardHybridVerifier::new(validator.composite_public_key());
204 verifier.verify(&payload, &self.signature).map_err(|e| {
205 ConsensusError::InvalidSignature(format!(
206 "hybrid timeout signature verification failed for {}: {}",
207 self.voter, e
208 ))
209 })?;
210
211 Ok(())
212 }
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
230pub struct TcSigner {
231 pub voter: Address,
233
234 pub high_qc_view: u64,
238
239 pub finalized_height: u64,
243
244 pub signature: CompositeSignature,
247
248 pub public_key: CompositePublicKey,
251}
252
253pub const TIMEOUT_CERTIFICATE_FORMAT_VERSION: u8 = 1;
258
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
272pub struct TimeoutCertificate {
273 pub format_version: u8,
275
276 pub view: u64,
278
279 pub signers: Vec<TcSigner>,
281}
282
283impl TimeoutCertificate {
284 pub fn new(view: u64, signers: Vec<TcSigner>) -> Self {
287 Self {
288 format_version: TIMEOUT_CERTIFICATE_FORMAT_VERSION,
289 view,
290 signers,
291 }
292 }
293
294 pub fn max_high_qc_view(&self) -> u64 {
298 self.signers
299 .iter()
300 .map(|s| s.high_qc_view)
301 .max()
302 .unwrap_or(0)
303 }
304
305 pub fn verify(&self, validator_set: &ValidatorSet) -> Result<()> {
319 if self.format_version != TIMEOUT_CERTIFICATE_FORMAT_VERSION {
320 return Err(ConsensusError::InvalidSignature(format!(
321 "TC rejected: unsupported format_version {} (expected {})",
322 self.format_version, TIMEOUT_CERTIFICATE_FORMAT_VERSION
323 )));
324 }
325
326 let quorum = validator_set.quorum_threshold();
327 if self.signers.len() < quorum {
328 return Err(ConsensusError::InvalidSignature(format!(
329 "TC has {} signers, below 2f+1 quorum threshold {}",
330 self.signers.len(),
331 quorum
332 )));
333 }
334
335 let mut seen: HashSet<Address> = HashSet::with_capacity(self.signers.len());
336 for signer in &self.signers {
337 if !seen.insert(signer.voter) {
338 return Err(ConsensusError::InvalidSignature(format!(
339 "TC contains duplicate signer {}",
340 signer.voter
341 )));
342 }
343
344 if signer.high_qc_view >= self.view {
345 return Err(ConsensusError::InvalidSignature(format!(
346 "TC signer {} has high_qc_view {} >= tc.view {}",
347 signer.voter, signer.high_qc_view, self.view
348 )));
349 }
350
351 let validator = validator_set.get_by_address(&signer.voter).ok_or_else(|| {
352 ConsensusError::NonValidator(format!("TC signer {}", signer.voter))
353 })?;
354
355 if !validator.is_active() {
356 return Err(ConsensusError::NonValidator(format!(
357 "TC signer {} is not active",
358 signer.voter
359 )));
360 }
361
362 if signer.public_key.classical != validator.public_key {
363 return Err(ConsensusError::InvalidSignature(format!(
364 "TC signer {}: classical key mismatch with registered validator",
365 signer.voter
366 )));
367 }
368 if signer.public_key.pq != validator.pq_public_key {
369 return Err(ConsensusError::InvalidSignature(format!(
370 "TC signer {}: PQ key mismatch with registered validator",
371 signer.voter
372 )));
373 }
374
375 let placeholder = CompositeSignature::new(Vec::new(), Vec::new());
380 let unsigned = TimeoutMsg::new(
381 self.view,
382 signer.high_qc_view,
383 signer.finalized_height,
384 signer.voter,
385 placeholder,
386 signer.public_key.clone(),
387 );
388 let payload = unsigned.signing_payload();
389
390 let verifier = StandardHybridVerifier::new(validator.composite_public_key());
391 verifier.verify(&payload, &signer.signature).map_err(|e| {
392 ConsensusError::InvalidSignature(format!(
393 "TC signer {}: hybrid signature verification failed: {}",
394 signer.voter, e
395 ))
396 })?;
397 }
398
399 Ok(())
400 }
401}
402
403struct PerViewCollector {
406 by_voter: std::collections::HashMap<Address, TcSigner>,
409
410 bracha_fired: bool,
414
415 tc_formed: bool,
419}
420
421impl PerViewCollector {
422 fn new() -> Self {
423 Self {
424 by_voter: std::collections::HashMap::new(),
425 bracha_fired: false,
426 tc_formed: false,
427 }
428 }
429}
430
431#[derive(Debug, Clone, PartialEq, Eq)]
433pub enum CollectOutcome {
434 Added,
436
437 BrachaBoost,
442
443 CertificateFormed(TimeoutCertificate),
446
447 Duplicate,
451}
452
453pub struct TimeoutCollector {
462 views: DashMap<u64, Arc<Mutex<PerViewCollector>>>,
464
465 quorum_threshold: usize,
469
470 bracha_threshold: usize,
472}
473
474impl TimeoutCollector {
475 pub fn new(validator_set: Arc<ValidatorSet>) -> Self {
477 let n = validator_set.len();
478 let f = (n.saturating_sub(1)) / 3;
479 let quorum_threshold = 2 * f + 1;
480 let bracha_threshold = f + 1;
481 Self {
482 views: DashMap::new(),
483 quorum_threshold,
484 bracha_threshold,
485 }
486 }
487
488 pub fn add(&self, msg: &TimeoutMsg) -> CollectOutcome {
496 let view = msg.view;
497 let entry = self
498 .views
499 .entry(view)
500 .or_insert_with(|| Arc::new(Mutex::new(PerViewCollector::new())))
501 .clone();
502 let mut state = entry.lock();
503
504 if state.tc_formed {
505 return match state.by_voter.get(&msg.voter) {
509 Some(_) => CollectOutcome::Duplicate,
510 None => {
511 state.by_voter.insert(
512 msg.voter,
513 TcSigner {
514 voter: msg.voter,
515 high_qc_view: msg.high_qc_view,
516 finalized_height: msg.finalized_height,
517 signature: msg.signature.clone(),
518 public_key: msg.public_key.clone(),
519 },
520 );
521 CollectOutcome::Added
522 }
523 };
524 }
525
526 if state.by_voter.contains_key(&msg.voter) {
527 return CollectOutcome::Duplicate;
531 }
532
533 state.by_voter.insert(
534 msg.voter,
535 TcSigner {
536 voter: msg.voter,
537 high_qc_view: msg.high_qc_view,
538 finalized_height: msg.finalized_height,
539 signature: msg.signature.clone(),
540 public_key: msg.public_key.clone(),
541 },
542 );
543
544 let count = state.by_voter.len();
545
546 if count >= self.quorum_threshold {
549 state.tc_formed = true;
550 state.bracha_fired = true;
552 let signers: Vec<TcSigner> = state.by_voter.values().cloned().collect();
553 let tc = TimeoutCertificate::new(view, signers);
554 return CollectOutcome::CertificateFormed(tc);
555 }
556
557 if !state.bracha_fired && count >= self.bracha_threshold {
558 state.bracha_fired = true;
559 return CollectOutcome::BrachaBoost;
560 }
561
562 CollectOutcome::Added
563 }
564
565 pub fn cleanup_below(&self, min_view: u64) {
570 self.views.retain(|view, _| *view >= min_view);
571 }
572
573 pub fn quorum_threshold(&self) -> usize {
575 self.quorum_threshold
576 }
577
578 pub fn bracha_threshold(&self) -> usize {
580 self.bracha_threshold
581 }
582}
583
584pub const NO_ENDORSEMENT_MSG_FORMAT_VERSION: u8 = 1;
617
618pub const NO_ENDORSEMENT_CERTIFICATE_FORMAT_VERSION: u8 = 1;
620
621const NO_ENDORSEMENT_SIGNING_DOMAIN: &[u8] = b"TENZRO_NO_ENDORSEMENT:";
625
626#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
635pub struct NoEndorsementMsg {
636 pub format_version: u8,
638
639 pub view: u64,
643
644 pub voter: Address,
646
647 pub signature: CompositeSignature,
649
650 pub public_key: CompositePublicKey,
652}
653
654impl NoEndorsementMsg {
655 pub fn new(
657 view: u64,
658 voter: Address,
659 signature: CompositeSignature,
660 public_key: CompositePublicKey,
661 ) -> Self {
662 Self {
663 format_version: NO_ENDORSEMENT_MSG_FORMAT_VERSION,
664 view,
665 voter,
666 signature,
667 public_key,
668 }
669 }
670
671 pub fn signing_payload(&self) -> Vec<u8> {
679 let mut payload = Vec::with_capacity(22 + 1 + 8 + 32);
680 payload.extend_from_slice(NO_ENDORSEMENT_SIGNING_DOMAIN);
681 payload.push(self.format_version);
682 payload.extend_from_slice(&self.view.to_le_bytes());
683 payload.extend_from_slice(self.voter.as_bytes());
684 payload
685 }
686
687 pub fn verify(&self, validator_set: &ValidatorSet) -> Result<()> {
698 if self.format_version != NO_ENDORSEMENT_MSG_FORMAT_VERSION {
699 return Err(ConsensusError::InvalidSignature(format!(
700 "no-endorsement message rejected: unsupported format_version {} (expected {})",
701 self.format_version, NO_ENDORSEMENT_MSG_FORMAT_VERSION
702 )));
703 }
704
705 if self.view == 0 {
706 return Err(ConsensusError::InvalidSignature(
707 "no-endorsement message rejected: view 0 has no predecessor".to_string(),
708 ));
709 }
710
711 let validator = validator_set.get_by_address(&self.voter).ok_or_else(|| {
712 ConsensusError::NonValidator(format!("Address: {}", self.voter))
713 })?;
714
715 if !validator.is_active() {
716 return Err(ConsensusError::NonValidator(format!(
717 "Validator {} is not active",
718 self.voter
719 )));
720 }
721
722 if self.public_key.classical != validator.public_key {
723 return Err(ConsensusError::InvalidSignature(format!(
724 "no-endorsement classical public key does not match registered validator key for {}",
725 self.voter
726 )));
727 }
728 if self.public_key.pq != validator.pq_public_key {
729 return Err(ConsensusError::InvalidSignature(format!(
730 "no-endorsement PQ public key does not match registered validator key for {}",
731 self.voter
732 )));
733 }
734
735 let payload = self.signing_payload();
736 let verifier = StandardHybridVerifier::new(validator.composite_public_key());
737 verifier.verify(&payload, &self.signature).map_err(|e| {
738 ConsensusError::InvalidSignature(format!(
739 "hybrid no-endorsement signature verification failed for {}: {}",
740 self.voter, e
741 ))
742 })?;
743
744 Ok(())
745 }
746}
747
748#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
750pub struct NecSigner {
751 pub voter: Address,
753
754 pub signature: CompositeSignature,
757
758 pub public_key: CompositePublicKey,
760}
761
762#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
773pub struct NoEndorsementCertificate {
774 pub format_version: u8,
777
778 pub view: u64,
780
781 pub signers: Vec<NecSigner>,
783}
784
785impl NoEndorsementCertificate {
786 pub fn new(view: u64, signers: Vec<NecSigner>) -> Self {
789 Self {
790 format_version: NO_ENDORSEMENT_CERTIFICATE_FORMAT_VERSION,
791 view,
792 signers,
793 }
794 }
795
796 pub fn verify(&self, validator_set: &ValidatorSet) -> Result<()> {
810 if self.format_version != NO_ENDORSEMENT_CERTIFICATE_FORMAT_VERSION {
811 return Err(ConsensusError::InvalidSignature(format!(
812 "NEC rejected: unsupported format_version {} (expected {})",
813 self.format_version, NO_ENDORSEMENT_CERTIFICATE_FORMAT_VERSION
814 )));
815 }
816
817 if self.view == 0 {
818 return Err(ConsensusError::InvalidSignature(
819 "NEC rejected: view 0 has no predecessor".to_string(),
820 ));
821 }
822
823 let n = validator_set.len();
825 let f = n.saturating_sub(1) / 3;
826 let bracha = f + 1;
827 if self.signers.len() < bracha {
828 return Err(ConsensusError::InvalidSignature(format!(
829 "NEC has {} signers, below f+1 threshold {}",
830 self.signers.len(),
831 bracha
832 )));
833 }
834
835 let mut seen: HashSet<Address> = HashSet::with_capacity(self.signers.len());
836 for signer in &self.signers {
837 if !seen.insert(signer.voter) {
838 return Err(ConsensusError::InvalidSignature(format!(
839 "NEC contains duplicate signer {}",
840 signer.voter
841 )));
842 }
843
844 let validator = validator_set.get_by_address(&signer.voter).ok_or_else(|| {
845 ConsensusError::NonValidator(format!("NEC signer {}", signer.voter))
846 })?;
847
848 if !validator.is_active() {
849 return Err(ConsensusError::NonValidator(format!(
850 "NEC signer {} is not active",
851 signer.voter
852 )));
853 }
854
855 if signer.public_key.classical != validator.public_key {
856 return Err(ConsensusError::InvalidSignature(format!(
857 "NEC signer {}: classical key mismatch with registered validator",
858 signer.voter
859 )));
860 }
861 if signer.public_key.pq != validator.pq_public_key {
862 return Err(ConsensusError::InvalidSignature(format!(
863 "NEC signer {}: PQ key mismatch with registered validator",
864 signer.voter
865 )));
866 }
867
868 let placeholder = CompositeSignature::new(Vec::new(), Vec::new());
870 let unsigned = NoEndorsementMsg::new(
871 self.view,
872 signer.voter,
873 placeholder,
874 signer.public_key.clone(),
875 );
876 let payload = unsigned.signing_payload();
877
878 let verifier = StandardHybridVerifier::new(validator.composite_public_key());
879 verifier.verify(&payload, &signer.signature).map_err(|e| {
880 ConsensusError::InvalidSignature(format!(
881 "NEC signer {}: hybrid signature verification failed: {}",
882 signer.voter, e
883 ))
884 })?;
885 }
886
887 Ok(())
888 }
889}
890
891struct PerViewNecCollector {
893 by_voter: std::collections::HashMap<Address, NecSigner>,
894 nec_formed: bool,
895}
896
897impl PerViewNecCollector {
898 fn new() -> Self {
899 Self {
900 by_voter: std::collections::HashMap::new(),
901 nec_formed: false,
902 }
903 }
904}
905
906#[derive(Debug, Clone, PartialEq, Eq)]
908pub enum NecCollectOutcome {
909 Added,
911
912 CertificateFormed(NoEndorsementCertificate),
915
916 Duplicate,
918}
919
920pub struct NoEndorsementCollector {
925 views: DashMap<u64, Arc<Mutex<PerViewNecCollector>>>,
926
927 bracha_threshold: usize,
929}
930
931impl NoEndorsementCollector {
932 pub fn new(validator_set: Arc<ValidatorSet>) -> Self {
934 let n = validator_set.len();
935 let f = n.saturating_sub(1) / 3;
936 let bracha_threshold = f + 1;
937 Self {
938 views: DashMap::new(),
939 bracha_threshold,
940 }
941 }
942
943 pub fn add(&self, msg: &NoEndorsementMsg) -> NecCollectOutcome {
945 let view = msg.view;
946 let entry = self
947 .views
948 .entry(view)
949 .or_insert_with(|| Arc::new(Mutex::new(PerViewNecCollector::new())))
950 .clone();
951 let mut state = entry.lock();
952
953 if state.nec_formed {
954 return match state.by_voter.get(&msg.voter) {
957 Some(_) => NecCollectOutcome::Duplicate,
958 None => {
959 state.by_voter.insert(
960 msg.voter,
961 NecSigner {
962 voter: msg.voter,
963 signature: msg.signature.clone(),
964 public_key: msg.public_key.clone(),
965 },
966 );
967 NecCollectOutcome::Added
968 }
969 };
970 }
971
972 if state.by_voter.contains_key(&msg.voter) {
973 return NecCollectOutcome::Duplicate;
974 }
975
976 state.by_voter.insert(
977 msg.voter,
978 NecSigner {
979 voter: msg.voter,
980 signature: msg.signature.clone(),
981 public_key: msg.public_key.clone(),
982 },
983 );
984
985 let count = state.by_voter.len();
986
987 if count >= self.bracha_threshold {
988 state.nec_formed = true;
989 let signers: Vec<NecSigner> = state.by_voter.values().cloned().collect();
990 let nec = NoEndorsementCertificate::new(view, signers);
991 return NecCollectOutcome::CertificateFormed(nec);
992 }
993
994 NecCollectOutcome::Added
995 }
996
997 pub fn cleanup_below(&self, min_view: u64) {
999 self.views.retain(|view, _| *view >= min_view);
1000 }
1001
1002 pub fn bracha_threshold(&self) -> usize {
1004 self.bracha_threshold
1005 }
1006}
1007
1008#[cfg(test)]
1009mod tests {
1010 use super::*;
1011 use crate::validator::ValidatorInfo;
1012 use std::sync::Arc;
1013 use tenzro_crypto::bls::BlsKeyPair;
1014 use tenzro_crypto::composite::{HybridSigner, InMemoryHybridSigner};
1015 use tenzro_crypto::pq::MlDsaSigningKey;
1016 use tenzro_crypto::signatures::Ed25519SignerImpl;
1017 use tenzro_crypto::{KeyPair, KeyType};
1018
1019 fn build_validator(stake: u128) -> (KeyPair, MlDsaSigningKey, Address, ValidatorInfo) {
1020 let keypair = KeyPair::generate(KeyType::Ed25519).unwrap();
1021 let pq = MlDsaSigningKey::generate();
1022 let bls = BlsKeyPair::generate().unwrap();
1023 let crypto_addr = keypair.address();
1024 let mut addr_bytes = [0u8; 32];
1025 addr_bytes[..20].copy_from_slice(crypto_addr.as_bytes());
1026 let address = Address::new(addr_bytes);
1027 let info = ValidatorInfo::new(
1028 address,
1029 keypair.public_key().clone(),
1030 pq.verifying_key_bytes().to_vec(),
1031 bls.public_key().to_bytes().to_vec(),
1032 stake,
1033 );
1034 (keypair, pq, address, info)
1035 }
1036
1037 const TEST_FINALIZED_HEIGHT: u64 = 7;
1040
1041 fn sign_timeout(
1042 view: u64,
1043 high_qc_view: u64,
1044 address: Address,
1045 keypair: &KeyPair,
1046 pq: &MlDsaSigningKey,
1047 ) -> TimeoutMsg {
1048 let composite_pk = CompositePublicKey::new(
1049 keypair.public_key().clone(),
1050 pq.verifying_key_bytes().to_vec(),
1051 );
1052 let placeholder = CompositeSignature::new(Vec::new(), Vec::new());
1053 let unsigned = TimeoutMsg::new(
1054 view,
1055 high_qc_view,
1056 TEST_FINALIZED_HEIGHT,
1057 address,
1058 placeholder,
1059 composite_pk.clone(),
1060 );
1061 let payload = unsigned.signing_payload();
1062
1063 let kp_bytes = keypair.to_bytes();
1064 let kp_copy = KeyPair::from_bytes(keypair.key_type(), &kp_bytes).unwrap();
1065 let classical = Ed25519SignerImpl::new(kp_copy).unwrap();
1066 let pq_copy = MlDsaSigningKey::from_seed(pq.seed_bytes()).unwrap();
1067 let signer = InMemoryHybridSigner::new(Box::new(classical), pq_copy);
1068
1069 let signature = signer.sign(&payload).unwrap();
1070 TimeoutMsg::new(
1071 view,
1072 high_qc_view,
1073 TEST_FINALIZED_HEIGHT,
1074 address,
1075 signature,
1076 composite_pk,
1077 )
1078 }
1079
1080 fn vset(infos: Vec<ValidatorInfo>) -> Arc<ValidatorSet> {
1081 Arc::new(ValidatorSet::new(0, infos).unwrap())
1082 }
1083
1084 #[test]
1087 fn timeout_msg_round_trips_signature() {
1088 let (kp, pq, addr, info) = build_validator(1000);
1089 let validator_set = vset(vec![info]);
1090 let msg = sign_timeout(42, 41, addr, &kp, &pq);
1091 msg.verify(&validator_set).expect("signature verifies");
1092 }
1093
1094 #[test]
1095 fn timeout_msg_rejects_unknown_voter() {
1096 let (_kp, _pq, _addr, info) = build_validator(1000);
1097 let validator_set = vset(vec![info]);
1098 let (kp_other, pq_other, addr_other, _) = build_validator(1000);
1099 let msg = sign_timeout(42, 41, addr_other, &kp_other, &pq_other);
1100 let err = msg.verify(&validator_set).unwrap_err();
1101 assert!(matches!(err, ConsensusError::NonValidator(_)), "got {err:?}");
1102 }
1103
1104 #[test]
1105 fn timeout_msg_rejects_format_version_mismatch() {
1106 let (kp, pq, addr, info) = build_validator(1000);
1107 let validator_set = vset(vec![info]);
1108 let mut msg = sign_timeout(42, 41, addr, &kp, &pq);
1109 msg.format_version = 99;
1110 let err = msg.verify(&validator_set).unwrap_err();
1111 assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
1112 }
1113
1114 #[test]
1115 fn timeout_msg_rejects_older_formats() {
1116 let (kp, pq, addr, info) = build_validator(1000);
1119 let validator_set = vset(vec![info]);
1120 for old_version in [1u8, 2u8] {
1121 let mut msg = sign_timeout(42, 41, addr, &kp, &pq);
1122 msg.format_version = old_version;
1123 let err = msg.verify(&validator_set).unwrap_err();
1124 assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
1125 }
1126 }
1127
1128 #[test]
1129 fn timeout_msg_rejects_wrong_view_after_signing() {
1130 let (kp, pq, addr, info) = build_validator(1000);
1132 let validator_set = vset(vec![info]);
1133 let mut msg = sign_timeout(42, 41, addr, &kp, &pq);
1134 msg.view = 99;
1135 let err = msg.verify(&validator_set).unwrap_err();
1136 assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
1137 }
1138
1139 #[test]
1140 fn timeout_msg_rejects_tampered_high_qc_view() {
1141 let (kp, pq, addr, info) = build_validator(1000);
1144 let validator_set = vset(vec![info]);
1145 let mut msg = sign_timeout(42, 10, addr, &kp, &pq);
1146 msg.high_qc_view = 41;
1147 let err = msg.verify(&validator_set).unwrap_err();
1148 assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
1149 }
1150
1151 #[test]
1152 fn timeout_msg_rejects_tampered_finalized_height() {
1153 let (kp, pq, addr, info) = build_validator(1000);
1158 let validator_set = vset(vec![info]);
1159 let mut msg = sign_timeout(42, 41, addr, &kp, &pq);
1160 msg.finalized_height = TEST_FINALIZED_HEIGHT + 1_000_000;
1161 let err = msg.verify(&validator_set).unwrap_err();
1162 assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
1163 }
1164
1165 #[test]
1166 fn timeout_msg_rejects_high_qc_view_ge_view() {
1167 let (kp, pq, addr, info) = build_validator(1000);
1172 let validator_set = vset(vec![info]);
1173 let msg = sign_timeout(42, 42, addr, &kp, &pq);
1176 let err = msg.verify(&validator_set).unwrap_err();
1177 assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
1178 }
1179
1180 fn build_n_signed_timeouts(
1186 n: usize,
1187 view: u64,
1188 high_qc_views: &[u64],
1189 ) -> (
1190 Arc<ValidatorSet>,
1191 Vec<(KeyPair, MlDsaSigningKey, Address)>,
1192 Vec<TimeoutMsg>,
1193 ) {
1194 assert_eq!(high_qc_views.len(), n);
1195 let mut keys = Vec::with_capacity(n);
1196 let mut infos = Vec::with_capacity(n);
1197 for _ in 0..n {
1198 let (kp, pq, addr, info) = build_validator(1000);
1199 keys.push((kp, pq, addr));
1200 infos.push(info);
1201 }
1202 let validator_set = vset(infos);
1203 let timeouts: Vec<TimeoutMsg> = keys
1204 .iter()
1205 .zip(high_qc_views.iter())
1206 .map(|((kp, pq, addr), &hqv)| sign_timeout(view, hqv, *addr, kp, pq))
1207 .collect();
1208 (validator_set, keys, timeouts)
1209 }
1210
1211 fn tc_from_timeouts(view: u64, timeouts: &[TimeoutMsg]) -> TimeoutCertificate {
1212 let signers = timeouts
1213 .iter()
1214 .map(|t| TcSigner {
1215 voter: t.voter,
1216 high_qc_view: t.high_qc_view,
1217 finalized_height: t.finalized_height,
1218 signature: t.signature.clone(),
1219 public_key: t.public_key.clone(),
1220 })
1221 .collect();
1222 TimeoutCertificate::new(view, signers)
1223 }
1224
1225 #[test]
1226 fn tc_verifies_with_2f_plus_1_signers() {
1227 let (vset, _keys, timeouts) =
1229 build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
1230 let tc = tc_from_timeouts(100, &timeouts[..3]);
1232 tc.verify(&vset).expect("TC verifies");
1233 assert_eq!(tc.max_high_qc_view(), 95);
1234 }
1235
1236 #[test]
1237 fn tc_rejects_below_quorum() {
1238 let (vset, _keys, timeouts) =
1240 build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
1241 let tc = tc_from_timeouts(100, &timeouts[..2]);
1242 let err = tc.verify(&vset).unwrap_err();
1243 assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
1244 }
1245
1246 #[test]
1247 fn tc_rejects_duplicate_signers() {
1248 let (vset, _keys, timeouts) =
1249 build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
1250 let mut signers: Vec<TcSigner> = timeouts[..3]
1252 .iter()
1253 .map(|t| TcSigner {
1254 voter: t.voter,
1255 high_qc_view: t.high_qc_view,
1256 finalized_height: t.finalized_height,
1257 signature: t.signature.clone(),
1258 public_key: t.public_key.clone(),
1259 })
1260 .collect();
1261 signers.push(signers[0].clone());
1262 let tc = TimeoutCertificate::new(100, signers);
1263 let err = tc.verify(&vset).unwrap_err();
1264 assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
1265 }
1266
1267 #[test]
1268 fn tc_rejects_tampered_high_qc_view() {
1269 let (vset, _keys, timeouts) =
1271 build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
1272 let mut tc = tc_from_timeouts(100, &timeouts[..3]);
1273 tc.signers[0].high_qc_view = 60;
1274 let err = tc.verify(&vset).unwrap_err();
1275 assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
1276 }
1277
1278 #[test]
1279 fn tc_rejects_tampered_finalized_height() {
1280 let (vset, _keys, timeouts) =
1284 build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
1285 let mut tc = tc_from_timeouts(100, &timeouts[..3]);
1286 tc.signers[0].finalized_height = TEST_FINALIZED_HEIGHT + 1_000_000;
1287 let err = tc.verify(&vset).unwrap_err();
1288 assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
1289 }
1290
1291 #[test]
1292 fn tc_rejects_tampered_view() {
1293 let (vset, _keys, timeouts) =
1296 build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
1297 let mut tc = tc_from_timeouts(100, &timeouts[..3]);
1298 tc.view = 200;
1299 let err = tc.verify(&vset).unwrap_err();
1300 assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
1301 }
1302
1303 #[test]
1304 fn tc_rejects_unregistered_signer() {
1305 let (vset, _keys, mut timeouts) =
1307 build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
1308 let (kp_outsider, pq_outsider, addr_outsider, _) = build_validator(1000);
1309 let outsider_msg = sign_timeout(100, 50, addr_outsider, &kp_outsider, &pq_outsider);
1310 timeouts.push(outsider_msg);
1311 let mut t3 = timeouts[..2].to_vec();
1313 t3.push(timeouts[4].clone());
1314 let tc = tc_from_timeouts(100, &t3);
1315 let err = tc.verify(&vset).unwrap_err();
1316 assert!(matches!(err, ConsensusError::NonValidator(_)), "got {err:?}");
1317 }
1318
1319 #[test]
1320 fn tc_format_version_mismatch_rejected() {
1321 let (vset, _keys, timeouts) =
1322 build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
1323 let mut tc = tc_from_timeouts(100, &timeouts[..3]);
1324 tc.format_version = 99;
1325 let err = tc.verify(&vset).unwrap_err();
1326 assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
1327 }
1328
1329 #[test]
1330 fn tc_max_high_qc_view_is_max() {
1331 let (_vset, _keys, timeouts) =
1332 build_n_signed_timeouts(4, 100, &[10, 99, 50, 30]);
1333 let tc = tc_from_timeouts(100, &timeouts);
1334 assert_eq!(tc.max_high_qc_view(), 99);
1336 }
1337
1338 #[test]
1341 fn collector_emits_added_for_first_timeout() {
1342 let (vset, _keys, timeouts) =
1343 build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
1344 let collector = TimeoutCollector::new(vset);
1345 assert_eq!(collector.bracha_threshold(), 2);
1347 assert_eq!(collector.quorum_threshold(), 3);
1348
1349 let outcome = collector.add(&timeouts[0]);
1350 assert_eq!(outcome, CollectOutcome::Added);
1351 }
1352
1353 #[test]
1354 fn collector_emits_bracha_boost_at_f_plus_1() {
1355 let (vset, _keys, timeouts) =
1356 build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
1357 let collector = TimeoutCollector::new(vset);
1358
1359 assert_eq!(collector.add(&timeouts[0]), CollectOutcome::Added);
1360 let outcome = collector.add(&timeouts[1]);
1362 assert_eq!(outcome, CollectOutcome::BrachaBoost);
1363 }
1364
1365 #[test]
1366 fn collector_bracha_boost_is_idempotent() {
1367 let (vset, _keys, timeouts) =
1371 build_n_signed_timeouts(7, 100, &[10, 20, 30, 40, 50, 60, 70]);
1372 let collector = TimeoutCollector::new(vset);
1374 assert_eq!(collector.bracha_threshold(), 3);
1375 assert_eq!(collector.quorum_threshold(), 5);
1376
1377 assert_eq!(collector.add(&timeouts[0]), CollectOutcome::Added);
1378 assert_eq!(collector.add(&timeouts[1]), CollectOutcome::Added);
1379 assert_eq!(collector.add(&timeouts[2]), CollectOutcome::BrachaBoost);
1380 assert_eq!(collector.add(&timeouts[3]), CollectOutcome::Added);
1382 }
1383
1384 #[test]
1385 fn collector_emits_certificate_at_quorum() {
1386 let (vset, _keys, timeouts) =
1387 build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
1388 let collector = TimeoutCollector::new(vset.clone());
1389
1390 assert_eq!(collector.add(&timeouts[0]), CollectOutcome::Added);
1391 assert_eq!(collector.add(&timeouts[1]), CollectOutcome::BrachaBoost);
1392 let outcome = collector.add(&timeouts[2]);
1393 match outcome {
1394 CollectOutcome::CertificateFormed(tc) => {
1395 tc.verify(&vset).expect("emitted TC verifies");
1396 assert_eq!(tc.view, 100);
1397 assert_eq!(tc.signers.len(), 3);
1398 assert_eq!(tc.max_high_qc_view(), 95);
1399 }
1400 other => panic!("expected CertificateFormed, got {other:?}"),
1401 }
1402 }
1403
1404 #[test]
1405 fn collector_certificate_is_idempotent() {
1406 let (vset, _keys, timeouts) =
1409 build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
1410 let collector = TimeoutCollector::new(vset);
1411
1412 let _ = collector.add(&timeouts[0]);
1413 let _ = collector.add(&timeouts[1]);
1414 let _ = collector.add(&timeouts[2]); let outcome = collector.add(&timeouts[3]);
1416 assert_eq!(outcome, CollectOutcome::Added);
1418 }
1419
1420 #[test]
1421 fn collector_dedupes_same_voter() {
1422 let (vset, _keys, timeouts) =
1423 build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
1424 let collector = TimeoutCollector::new(vset);
1425
1426 assert_eq!(collector.add(&timeouts[0]), CollectOutcome::Added);
1427 assert_eq!(collector.add(&timeouts[0]), CollectOutcome::Duplicate);
1429 assert_eq!(collector.add(&timeouts[1]), CollectOutcome::BrachaBoost);
1431 }
1432
1433 #[test]
1434 fn collector_separates_views() {
1435 let (vset, _keys, timeouts_v100) =
1436 build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
1437 let collector = TimeoutCollector::new(vset);
1438
1439 assert_eq!(collector.add(&timeouts_v100[0]), CollectOutcome::Added);
1441 assert_eq!(collector.add(&timeouts_v100[1]), CollectOutcome::BrachaBoost);
1442
1443 }
1449
1450 #[test]
1451 fn collector_cleanup_below_drops_old_views() {
1452 let (vset, _keys, timeouts) =
1453 build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
1454 let collector = TimeoutCollector::new(vset);
1455
1456 let _ = collector.add(&timeouts[0]);
1457 assert_eq!(collector.views.len(), 1);
1458
1459 collector.cleanup_below(101);
1460 assert_eq!(collector.views.len(), 0);
1461 }
1462
1463 #[test]
1464 fn collector_quorum_in_one_shot_when_n_eq_1() {
1465 let (vset, _keys, timeouts) = build_n_signed_timeouts(1, 50, &[10]);
1470 let collector = TimeoutCollector::new(vset.clone());
1471 assert_eq!(collector.bracha_threshold(), 1);
1472 assert_eq!(collector.quorum_threshold(), 1);
1473
1474 let outcome = collector.add(&timeouts[0]);
1475 match outcome {
1476 CollectOutcome::CertificateFormed(tc) => {
1477 tc.verify(&vset).expect("single-validator TC verifies");
1478 assert_eq!(tc.signers.len(), 1);
1479 }
1480 other => panic!("expected CertificateFormed, got {other:?}"),
1481 }
1482 }
1483
1484 fn sign_no_endorsement(
1487 view: u64,
1488 address: Address,
1489 keypair: &KeyPair,
1490 pq: &MlDsaSigningKey,
1491 ) -> NoEndorsementMsg {
1492 let composite_pk = CompositePublicKey::new(
1493 keypair.public_key().clone(),
1494 pq.verifying_key_bytes().to_vec(),
1495 );
1496 let placeholder = CompositeSignature::new(Vec::new(), Vec::new());
1497 let unsigned = NoEndorsementMsg::new(view, address, placeholder, composite_pk.clone());
1498 let payload = unsigned.signing_payload();
1499
1500 let kp_bytes = keypair.to_bytes();
1501 let kp_copy = KeyPair::from_bytes(keypair.key_type(), &kp_bytes).unwrap();
1502 let classical = Ed25519SignerImpl::new(kp_copy).unwrap();
1503 let pq_copy = MlDsaSigningKey::from_seed(pq.seed_bytes()).unwrap();
1504 let signer = InMemoryHybridSigner::new(Box::new(classical), pq_copy);
1505
1506 let signature = signer.sign(&payload).unwrap();
1507 NoEndorsementMsg::new(view, address, signature, composite_pk)
1508 }
1509
1510 fn build_n_signed_no_endorsements(
1511 n: usize,
1512 view: u64,
1513 ) -> (
1514 Arc<ValidatorSet>,
1515 Vec<(KeyPair, MlDsaSigningKey, Address)>,
1516 Vec<NoEndorsementMsg>,
1517 ) {
1518 let mut keys = Vec::with_capacity(n);
1519 let mut infos = Vec::with_capacity(n);
1520 for _ in 0..n {
1521 let (kp, pq, addr, info) = build_validator(1000);
1522 keys.push((kp, pq, addr));
1523 infos.push(info);
1524 }
1525 let validator_set = vset(infos);
1526 let msgs: Vec<NoEndorsementMsg> = keys
1527 .iter()
1528 .map(|(kp, pq, addr)| sign_no_endorsement(view, *addr, kp, pq))
1529 .collect();
1530 (validator_set, keys, msgs)
1531 }
1532
1533 fn nec_from_msgs(view: u64, msgs: &[NoEndorsementMsg]) -> NoEndorsementCertificate {
1534 let signers = msgs
1535 .iter()
1536 .map(|m| NecSigner {
1537 voter: m.voter,
1538 signature: m.signature.clone(),
1539 public_key: m.public_key.clone(),
1540 })
1541 .collect();
1542 NoEndorsementCertificate::new(view, signers)
1543 }
1544
1545 #[test]
1546 fn nec_msg_round_trips_signature() {
1547 let (kp, pq, addr, info) = build_validator(1000);
1548 let validator_set = vset(vec![info]);
1549 let msg = sign_no_endorsement(42, addr, &kp, &pq);
1550 msg.verify(&validator_set).expect("signature verifies");
1551 }
1552
1553 #[test]
1554 fn nec_msg_rejects_view_zero() {
1555 let (kp, pq, addr, info) = build_validator(1000);
1556 let validator_set = vset(vec![info]);
1557 let msg = sign_no_endorsement(0, addr, &kp, &pq);
1558 let err = msg.verify(&validator_set).unwrap_err();
1559 assert!(
1560 matches!(err, ConsensusError::InvalidSignature(_)),
1561 "got {err:?}"
1562 );
1563 }
1564
1565 #[test]
1566 fn nec_msg_rejects_unknown_voter() {
1567 let (_kp, _pq, _addr, info) = build_validator(1000);
1568 let validator_set = vset(vec![info]);
1569 let (kp_other, pq_other, addr_other, _) = build_validator(1000);
1570 let msg = sign_no_endorsement(42, addr_other, &kp_other, &pq_other);
1571 let err = msg.verify(&validator_set).unwrap_err();
1572 assert!(matches!(err, ConsensusError::NonValidator(_)), "got {err:?}");
1573 }
1574
1575 #[test]
1576 fn nec_msg_rejects_format_version_mismatch() {
1577 let (kp, pq, addr, info) = build_validator(1000);
1578 let validator_set = vset(vec![info]);
1579 let mut msg = sign_no_endorsement(42, addr, &kp, &pq);
1580 msg.format_version = 99;
1581 let err = msg.verify(&validator_set).unwrap_err();
1582 assert!(
1583 matches!(err, ConsensusError::InvalidSignature(_)),
1584 "got {err:?}"
1585 );
1586 }
1587
1588 #[test]
1589 fn nec_msg_rejects_tampered_view() {
1590 let (kp, pq, addr, info) = build_validator(1000);
1591 let validator_set = vset(vec![info]);
1592 let mut msg = sign_no_endorsement(42, addr, &kp, &pq);
1593 msg.view = 99;
1594 let err = msg.verify(&validator_set).unwrap_err();
1595 assert!(
1596 matches!(err, ConsensusError::InvalidSignature(_)),
1597 "got {err:?}"
1598 );
1599 }
1600
1601 #[test]
1602 fn nec_payload_distinct_from_timeout_payload() {
1603 let (kp, pq, addr, info) = build_validator(1000);
1607 let validator_set = vset(vec![info]);
1608 let timeout = sign_timeout(42, 41, addr, &kp, &pq);
1609 let nec = NoEndorsementMsg::new(
1610 42,
1611 addr,
1612 timeout.signature.clone(),
1613 timeout.public_key.clone(),
1614 );
1615 let err = nec.verify(&validator_set).unwrap_err();
1616 assert!(
1617 matches!(err, ConsensusError::InvalidSignature(_)),
1618 "got {err:?}"
1619 );
1620 }
1621
1622 #[test]
1623 fn nec_verifies_with_f_plus_1_signers() {
1624 let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
1626 let nec = nec_from_msgs(100, &msgs[..2]);
1627 nec.verify(&vset).expect("NEC verifies at f+1");
1628 }
1629
1630 #[test]
1631 fn nec_rejects_below_f_plus_1() {
1632 let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
1633 let nec = nec_from_msgs(100, &msgs[..1]);
1634 let err = nec.verify(&vset).unwrap_err();
1635 assert!(
1636 matches!(err, ConsensusError::InvalidSignature(_)),
1637 "got {err:?}"
1638 );
1639 }
1640
1641 #[test]
1642 fn nec_rejects_duplicate_signers() {
1643 let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
1644 let mut signers: Vec<NecSigner> = msgs[..2]
1645 .iter()
1646 .map(|m| NecSigner {
1647 voter: m.voter,
1648 signature: m.signature.clone(),
1649 public_key: m.public_key.clone(),
1650 })
1651 .collect();
1652 signers.push(signers[0].clone());
1653 let nec = NoEndorsementCertificate::new(100, signers);
1654 let err = nec.verify(&vset).unwrap_err();
1655 assert!(
1656 matches!(err, ConsensusError::InvalidSignature(_)),
1657 "got {err:?}"
1658 );
1659 }
1660
1661 #[test]
1662 fn nec_rejects_tampered_view() {
1663 let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
1664 let mut nec = nec_from_msgs(100, &msgs[..2]);
1665 nec.view = 200;
1666 let err = nec.verify(&vset).unwrap_err();
1667 assert!(
1668 matches!(err, ConsensusError::InvalidSignature(_)),
1669 "got {err:?}"
1670 );
1671 }
1672
1673 #[test]
1674 fn nec_collector_emits_added_for_first_msg() {
1675 let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
1676 let collector = NoEndorsementCollector::new(vset);
1677 assert_eq!(collector.bracha_threshold(), 2);
1679 let outcome = collector.add(&msgs[0]);
1680 assert_eq!(outcome, NecCollectOutcome::Added);
1681 }
1682
1683 #[test]
1684 fn nec_collector_emits_certificate_at_f_plus_1() {
1685 let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
1686 let collector = NoEndorsementCollector::new(vset.clone());
1687 assert_eq!(collector.add(&msgs[0]), NecCollectOutcome::Added);
1688 let outcome = collector.add(&msgs[1]);
1689 match outcome {
1690 NecCollectOutcome::CertificateFormed(nec) => {
1691 nec.verify(&vset).expect("emitted NEC verifies");
1692 assert_eq!(nec.view, 100);
1693 assert_eq!(nec.signers.len(), 2);
1694 }
1695 other => panic!("expected CertificateFormed, got {other:?}"),
1696 }
1697 }
1698
1699 #[test]
1700 fn nec_collector_certificate_is_idempotent() {
1701 let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
1702 let collector = NoEndorsementCollector::new(vset);
1703
1704 let _ = collector.add(&msgs[0]);
1705 let _ = collector.add(&msgs[1]); let outcome = collector.add(&msgs[2]);
1707 assert_eq!(outcome, NecCollectOutcome::Added);
1709 }
1710
1711 #[test]
1712 fn nec_collector_dedupes_same_voter() {
1713 let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
1714 let collector = NoEndorsementCollector::new(vset);
1715 assert_eq!(collector.add(&msgs[0]), NecCollectOutcome::Added);
1716 assert_eq!(collector.add(&msgs[0]), NecCollectOutcome::Duplicate);
1717 }
1718
1719 #[test]
1720 fn nec_collector_cleanup_below_drops_old_views() {
1721 let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
1722 let collector = NoEndorsementCollector::new(vset);
1723 let _ = collector.add(&msgs[0]);
1724 assert_eq!(collector.views.len(), 1);
1725 collector.cleanup_below(101);
1726 assert_eq!(collector.views.len(), 0);
1727 }
1728
1729 #[test]
1730 fn nec_collector_certificate_at_n_eq_1() {
1731 let (vset, _keys, msgs) = build_n_signed_no_endorsements(1, 50);
1733 let collector = NoEndorsementCollector::new(vset.clone());
1734 assert_eq!(collector.bracha_threshold(), 1);
1735 let outcome = collector.add(&msgs[0]);
1736 match outcome {
1737 NecCollectOutcome::CertificateFormed(nec) => {
1738 nec.verify(&vset).expect("single-validator NEC verifies");
1739 assert_eq!(nec.signers.len(), 1);
1740 }
1741 other => panic!("expected CertificateFormed, got {other:?}"),
1742 }
1743 }
1744}