1use crate::error::{Result, TokenError};
25use dashmap::DashMap;
26use serde::{Deserialize, Serialize};
27use std::sync::Arc;
28use tenzro_types::primitives::{Address, Timestamp};
29use tracing::{info, warn};
30
31pub const DEFAULT_MIN_VALIDATOR_SELF_STAKE: u128 = 10_000 * 1_000_000_000_000_000_000;
38
39pub const DEFAULT_MIN_RPC_PROVIDER_STAKE: u128 = 100_000 * 1_000_000_000_000_000_000;
53
54pub const DEFAULT_ACTIVATION_CHURN_BPS: u32 = 400;
57
58pub const DEFAULT_EXIT_CHURN_BPS: u32 = 400;
61
62pub const MIN_CHURN_PER_EPOCH: u32 = 1;
65
66pub const DEFAULT_REENTRY_COOLDOWN_EPOCHS: u64 = 4;
70
71pub const ACTIVATION_EFFECTIVE_DELAY_BLOCKS: u64 = 3;
76
77pub const VALIDATOR_PREFIX: &str = "validator:";
79pub const VALIDATOR_INDEX_KEY: &str = "validator:index";
81pub const VALIDATOR_CONFIG_KEY: &str = "validator:config";
83
84const REGISTRY_CF: &str = tenzro_storage::CF_TOKENS;
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(rename_all = "snake_case")]
109pub enum ValidatorTier {
110 ResourceOnly,
111 Staked,
112 RpcProvider,
113}
114
115impl ValidatorTier {
116 pub fn from_stake(self_stake: u128, cfg: &ValidatorRegistryConfig) -> Self {
119 if self_stake >= cfg.min_rpc_provider_stake {
120 Self::RpcProvider
121 } else if self_stake >= cfg.min_self_stake {
122 Self::Staked
123 } else {
124 Self::ResourceOnly
125 }
126 }
127
128 pub fn admits_high_trust(&self) -> bool {
132 !matches!(self, Self::ResourceOnly)
133 }
134
135 pub fn has_governance_weight(&self) -> bool {
139 !matches!(self, Self::ResourceOnly)
140 }
141
142 pub fn has_financial_slashing(&self) -> bool {
145 !matches!(self, Self::ResourceOnly)
146 }
147
148 pub fn admits_rpc_role(&self) -> bool {
152 matches!(self, Self::RpcProvider)
153 }
154
155 pub fn as_str(&self) -> &'static str {
156 match self {
157 Self::ResourceOnly => "resource_only",
158 Self::Staked => "staked",
159 Self::RpcProvider => "rpc_provider",
160 }
161 }
162}
163
164impl Default for ValidatorTier {
165 fn default() -> Self {
166 Self::Staked
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
210pub enum ValidatorRegistryStatus {
211 Candidate,
214 PendingActive,
217 Active,
219 PendingExit,
222 Exited,
225 Jailed,
228}
229
230impl ValidatorRegistryStatus {
231 pub fn counts_toward_active(&self) -> bool {
234 matches!(self, Self::Active | Self::PendingExit)
235 }
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct ValidatorRegistryEntry {
241 pub address: Address,
243 pub consensus_pubkey: Vec<u8>,
245 pub pq_pubkey: Vec<u8>,
247 pub bls_pubkey: Vec<u8>,
251 pub withdrawal_address: Address,
254 pub self_stake: u128,
256 #[serde(default)]
269 pub tier: ValidatorTier,
270 pub status: ValidatorRegistryStatus,
272 pub registered_at_epoch: u64,
274 pub activated_at_epoch: Option<u64>,
276 pub exited_at_epoch: Option<u64>,
279 pub jailed_until_epoch: Option<u64>,
282 pub tee_attestation_hash: Option<[u8; 32]>,
285 pub metadata_uri: String,
288 pub updated_at: Timestamp,
290}
291
292impl ValidatorRegistryEntry {
293 pub fn new_candidate(
303 address: Address,
304 consensus_pubkey: Vec<u8>,
305 pq_pubkey: Vec<u8>,
306 bls_pubkey: Vec<u8>,
307 withdrawal_address: Address,
308 self_stake: u128,
309 registered_at_epoch: u64,
310 metadata_uri: String,
311 cfg: &ValidatorRegistryConfig,
312 ) -> Result<Self> {
313 if consensus_pubkey.len() != 32 {
314 return Err(TokenError::InvalidParameter(format!(
315 "consensus public key must be 32 bytes, got {}",
316 consensus_pubkey.len()
317 )));
318 }
319 if pq_pubkey.len() != tenzro_crypto::pq::ML_DSA_65_VK_LEN {
320 return Err(TokenError::InvalidParameter(format!(
321 "PQ verifying key must be {} bytes (ML-DSA-65), got {}",
322 tenzro_crypto::pq::ML_DSA_65_VK_LEN,
323 pq_pubkey.len()
324 )));
325 }
326 if bls_pubkey.len() != 48 {
327 return Err(TokenError::InvalidParameter(format!(
328 "BLS verifying key must be 48 bytes (BLS12-381 G1-compressed, min_pk), got {}",
329 bls_pubkey.len()
330 )));
331 }
332 if metadata_uri.len() > 256 {
333 return Err(TokenError::InvalidParameter(format!(
334 "metadata_uri exceeds 256 bytes (got {})",
335 metadata_uri.len()
336 )));
337 }
338 let tier = ValidatorTier::from_stake(self_stake, cfg);
339 Ok(Self {
340 address,
341 consensus_pubkey,
342 pq_pubkey,
343 bls_pubkey,
344 withdrawal_address,
345 self_stake,
346 tier,
347 status: ValidatorRegistryStatus::Candidate,
348 registered_at_epoch,
349 activated_at_epoch: None,
350 exited_at_epoch: None,
351 jailed_until_epoch: None,
352 tee_attestation_hash: None,
353 metadata_uri,
354 updated_at: Timestamp::now(),
355 })
356 }
357}
358
359#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
361pub struct ValidatorRegistryConfig {
362 pub min_self_stake: u128,
367 #[serde(default = "default_min_rpc_provider_stake")]
373 pub min_rpc_provider_stake: u128,
374 pub activation_churn_bps: u32,
376 pub exit_churn_bps: u32,
378 pub reentry_cooldown_epochs: u64,
380}
381
382fn default_min_rpc_provider_stake() -> u128 {
386 DEFAULT_MIN_RPC_PROVIDER_STAKE
387}
388
389impl Default for ValidatorRegistryConfig {
390 fn default() -> Self {
391 Self {
392 min_self_stake: DEFAULT_MIN_VALIDATOR_SELF_STAKE,
393 min_rpc_provider_stake: DEFAULT_MIN_RPC_PROVIDER_STAKE,
394 activation_churn_bps: DEFAULT_ACTIVATION_CHURN_BPS,
395 exit_churn_bps: DEFAULT_EXIT_CHURN_BPS,
396 reentry_cooldown_epochs: DEFAULT_REENTRY_COOLDOWN_EPOCHS,
397 }
398 }
399}
400
401#[derive(Debug, Clone, Default)]
408pub struct EpochTransitionPlan {
409 pub activations: Vec<Address>,
412 pub exits: Vec<Address>,
415 pub effective_activations: Vec<Address>,
417 pub effective_exits: Vec<Address>,
419}
420
421pub struct ValidatorRegistry {
425 entries: DashMap<Address, ValidatorRegistryEntry>,
427 config: parking_lot::RwLock<ValidatorRegistryConfig>,
429 storage: Option<Arc<dyn tenzro_storage::KvStore>>,
431}
432
433impl ValidatorRegistry {
434 pub fn new() -> Self {
436 Self {
437 entries: DashMap::new(),
438 config: parking_lot::RwLock::new(ValidatorRegistryConfig::default()),
439 storage: None,
440 }
441 }
442
443 pub fn with_storage(storage: Arc<dyn tenzro_storage::KvStore>) -> Self {
446 let registry = Self {
447 entries: DashMap::new(),
448 config: parking_lot::RwLock::new(ValidatorRegistryConfig::default()),
449 storage: Some(storage),
450 };
451 if let Err(e) = registry.load_from_storage() {
452 warn!("Failed to hydrate validator registry: {}", e);
453 }
454 registry
455 }
456
457 fn persist_entry(&self, entry: &ValidatorRegistryEntry) {
458 if let Some(storage) = &self.storage {
459 let key = format!("{}{}", VALIDATOR_PREFIX, hex::encode(entry.address.as_bytes()));
460 match bincode::serialize(entry) {
461 Ok(data) => {
462 if let Err(e) = storage.put(REGISTRY_CF, key.as_bytes(), &data) {
463 warn!("Failed to persist validator {}: {}", entry.address, e);
464 }
465 }
466 Err(e) => warn!("Failed to serialize validator {}: {}", entry.address, e),
467 }
468 }
469 }
470
471 fn persist_index(&self) {
472 if let Some(storage) = &self.storage {
473 let addresses: Vec<String> = self
474 .entries
475 .iter()
476 .map(|e| hex::encode(e.key().as_bytes()))
477 .collect();
478 match bincode::serialize(&addresses) {
479 Ok(data) => {
480 if let Err(e) = storage.put(REGISTRY_CF, VALIDATOR_INDEX_KEY.as_bytes(), &data)
481 {
482 warn!("Failed to persist validator index: {}", e);
483 }
484 }
485 Err(e) => warn!("Failed to serialize validator index: {}", e),
486 }
487 }
488 }
489
490 fn persist_config(&self) {
491 if let Some(storage) = &self.storage {
492 let cfg = *self.config.read();
493 match bincode::serialize(&cfg) {
494 Ok(data) => {
495 if let Err(e) = storage.put(REGISTRY_CF, VALIDATOR_CONFIG_KEY.as_bytes(), &data)
496 {
497 warn!("Failed to persist validator config: {}", e);
498 }
499 }
500 Err(e) => warn!("Failed to serialize validator config: {}", e),
501 }
502 }
503 }
504
505 fn load_from_storage(&self) -> Result<()> {
506 let storage = match &self.storage {
507 Some(s) => s,
508 None => return Ok(()),
509 };
510
511 if let Ok(Some(cfg_data)) = storage.get(REGISTRY_CF, VALIDATOR_CONFIG_KEY.as_bytes())
512 && let Ok(cfg) = bincode::deserialize::<ValidatorRegistryConfig>(&cfg_data)
513 {
514 *self.config.write() = cfg;
515 }
516
517 if let Ok(Some(idx_data)) = storage.get(REGISTRY_CF, VALIDATOR_INDEX_KEY.as_bytes()) {
518 let addresses: Vec<String> = match bincode::deserialize(&idx_data) {
519 Ok(v) => v,
520 Err(e) => {
521 warn!("Failed to deserialize validator index: {}", e);
522 return Ok(());
523 }
524 };
525 for hex_addr in &addresses {
526 let key = format!("{}{}", VALIDATOR_PREFIX, hex_addr);
527 if let Ok(Some(data)) = storage.get(REGISTRY_CF, key.as_bytes()) {
528 match bincode::deserialize::<ValidatorRegistryEntry>(&data) {
529 Ok(entry) => {
530 self.entries.insert(entry.address, entry);
531 }
532 Err(e) => warn!("Failed to deserialize validator {}: {}", hex_addr, e),
533 }
534 }
535 }
536 info!("Loaded {} validators from registry", addresses.len());
537 }
538
539 Ok(())
540 }
541
542 pub fn config(&self) -> ValidatorRegistryConfig {
544 *self.config.read()
545 }
546
547 pub fn set_config(&self, cfg: ValidatorRegistryConfig) {
550 *self.config.write() = cfg;
551 self.persist_config();
552 }
553
554 pub fn get(&self, address: &Address) -> Option<ValidatorRegistryEntry> {
556 self.entries.get(address).map(|e| e.value().clone())
557 }
558
559 pub fn list(&self) -> Vec<ValidatorRegistryEntry> {
561 self.entries.iter().map(|e| e.value().clone()).collect()
562 }
563
564 pub fn list_active(&self) -> Vec<ValidatorRegistryEntry> {
566 self.entries
567 .iter()
568 .filter(|e| e.value().status == ValidatorRegistryStatus::Active)
569 .map(|e| e.value().clone())
570 .collect()
571 }
572
573 fn active_set_size(&self) -> usize {
577 self.entries
578 .iter()
579 .filter(|e| e.value().status.counts_toward_active())
580 .count()
581 }
582
583 fn churn_budgets(&self) -> (u32, u32) {
586 let cfg = *self.config.read();
587 let n = self.active_set_size() as u64;
588 let act = ((n.saturating_mul(cfg.activation_churn_bps as u64)) / 10_000) as u32;
589 let exit = ((n.saturating_mul(cfg.exit_churn_bps as u64)) / 10_000) as u32;
590 (act.max(MIN_CHURN_PER_EPOCH), exit.max(MIN_CHURN_PER_EPOCH))
591 }
592
593 #[allow(clippy::too_many_arguments)]
597 pub fn register_candidate(
598 &self,
599 address: Address,
600 consensus_pubkey: Vec<u8>,
601 pq_pubkey: Vec<u8>,
602 bls_pubkey: Vec<u8>,
603 withdrawal_address: Address,
604 self_stake: u128,
605 current_epoch: u64,
606 metadata_uri: String,
607 ) -> Result<()> {
608 let cfg = *self.config.read();
609
610 if let Some(existing) = self.entries.get(&address) {
621 match existing.status {
622 ValidatorRegistryStatus::Exited => {
623 if let Some(exit_epoch) = existing.exited_at_epoch
624 && current_epoch < exit_epoch.saturating_add(cfg.reentry_cooldown_epochs)
625 {
626 return Err(TokenError::Unauthorized {
627 reason: format!(
628 "re-entry cooldown: must wait until epoch {}",
629 exit_epoch.saturating_add(cfg.reentry_cooldown_epochs)
630 ),
631 });
632 }
633 }
634 _ => {
635 return Err(TokenError::InvalidParameter(format!(
636 "validator {} already registered with status {:?}",
637 address, existing.status
638 )));
639 }
640 }
641 }
642
643 let entry = ValidatorRegistryEntry::new_candidate(
644 address,
645 consensus_pubkey,
646 pq_pubkey,
647 bls_pubkey,
648 withdrawal_address,
649 self_stake,
650 current_epoch,
651 metadata_uri,
652 &cfg,
653 )?;
654 let tier = entry.tier;
655 self.entries.insert(address, entry.clone());
656 self.persist_entry(&entry);
657 self.persist_index();
658 info!(
659 "Registered validator candidate {} at epoch {} with stake {} (tier={})",
660 address, current_epoch, self_stake, tier.as_str()
661 );
662 Ok(())
663 }
664
665 pub fn seed_genesis_active(
677 &self,
678 address: Address,
679 consensus_pubkey: Vec<u8>,
680 pq_pubkey: Vec<u8>,
681 bls_pubkey: Vec<u8>,
682 withdrawal_address: Address,
683 self_stake: u128,
684 metadata_uri: String,
685 ) -> Result<bool> {
686 if self.entries.contains_key(&address) {
687 return Ok(false);
688 }
689 if consensus_pubkey.len() != 32 {
690 return Err(TokenError::InvalidParameter(format!(
691 "consensus public key must be 32 bytes, got {}",
692 consensus_pubkey.len()
693 )));
694 }
695 if pq_pubkey.len() != tenzro_crypto::pq::ML_DSA_65_VK_LEN {
696 return Err(TokenError::InvalidParameter(format!(
697 "PQ verifying key must be {} bytes (ML-DSA-65), got {}",
698 tenzro_crypto::pq::ML_DSA_65_VK_LEN,
699 pq_pubkey.len()
700 )));
701 }
702 if bls_pubkey.len() != 48 {
703 return Err(TokenError::InvalidParameter(format!(
704 "BLS verifying key must be 48 bytes (BLS12-381 G1-compressed, min_pk), got {}",
705 bls_pubkey.len()
706 )));
707 }
708 if metadata_uri.len() > 256 {
709 return Err(TokenError::InvalidParameter(format!(
710 "metadata_uri exceeds 256 bytes (got {})",
711 metadata_uri.len()
712 )));
713 }
714 let cfg = *self.config.read();
720 let tier = ValidatorTier::from_stake(self_stake, &cfg);
721 let entry = ValidatorRegistryEntry {
722 address,
723 consensus_pubkey,
724 pq_pubkey,
725 bls_pubkey,
726 withdrawal_address,
727 self_stake,
728 tier,
729 status: ValidatorRegistryStatus::Active,
730 registered_at_epoch: 0,
731 activated_at_epoch: Some(0),
732 exited_at_epoch: None,
733 jailed_until_epoch: None,
734 tee_attestation_hash: None,
735 metadata_uri,
736 updated_at: Timestamp::now(),
737 };
738 self.entries.insert(address, entry.clone());
739 self.persist_entry(&entry);
740 self.persist_index();
741 info!(
742 "Seeded genesis Active validator {} with stake {} (tier={})",
743 address, self_stake, tier.as_str()
744 );
745 Ok(true)
746 }
747
748 pub fn request_exit(&self, address: &Address) -> Result<()> {
752 let mut entry = self
753 .entries
754 .get_mut(address)
755 .ok_or_else(|| TokenError::NotFound(format!("validator {}", address)))?;
756
757 match entry.status {
758 ValidatorRegistryStatus::Active => {
759 entry.status = ValidatorRegistryStatus::PendingExit;
760 entry.updated_at = Timestamp::now();
761 }
762 ValidatorRegistryStatus::Candidate | ValidatorRegistryStatus::PendingActive => {
763 entry.status = ValidatorRegistryStatus::PendingExit;
766 entry.updated_at = Timestamp::now();
767 }
768 ValidatorRegistryStatus::PendingExit | ValidatorRegistryStatus::Exited => {
769 return Err(TokenError::InvalidParameter(format!(
770 "validator {} already exiting (status {:?})",
771 address, entry.status
772 )));
773 }
774 ValidatorRegistryStatus::Jailed => {
775 return Err(TokenError::InvalidParameter(format!(
776 "validator {} is jailed; exit forbidden until governance restores",
777 address
778 )));
779 }
780 }
781 let snapshot = entry.clone();
782 drop(entry);
783 self.persist_entry(&snapshot);
784 info!("Validator {} requested exit", address);
785 Ok(())
786 }
787
788 pub fn update_metadata(
791 &self,
792 address: &Address,
793 metadata_uri: Option<String>,
794 tee_attestation_hash: Option<[u8; 32]>,
795 ) -> Result<()> {
796 let mut entry = self
797 .entries
798 .get_mut(address)
799 .ok_or_else(|| TokenError::NotFound(format!("validator {}", address)))?;
800
801 if entry.status == ValidatorRegistryStatus::Exited {
802 return Err(TokenError::InvalidParameter(
803 "cannot update metadata for exited validator".to_string(),
804 ));
805 }
806 if let Some(uri) = metadata_uri {
807 if uri.len() > 256 {
808 return Err(TokenError::InvalidParameter(format!(
809 "metadata_uri exceeds 256 bytes (got {})",
810 uri.len()
811 )));
812 }
813 entry.metadata_uri = uri;
814 }
815 if let Some(h) = tee_attestation_hash {
816 entry.tee_attestation_hash = Some(h);
817 }
818 entry.updated_at = Timestamp::now();
819 let snapshot = entry.clone();
820 drop(entry);
821 self.persist_entry(&snapshot);
822 Ok(())
823 }
824
825 pub fn rotate_keys(
839 &self,
840 address: &Address,
841 new_consensus_pubkey: Vec<u8>,
842 new_pq_pubkey: Vec<u8>,
843 new_bls_pubkey: Vec<u8>,
844 ) -> Result<()> {
845 if new_consensus_pubkey.len() != 32 {
847 return Err(TokenError::InvalidParameter(format!(
848 "rotate_keys: new consensus_pubkey must be 32 bytes (Ed25519), got {}",
849 new_consensus_pubkey.len()
850 )));
851 }
852 if new_pq_pubkey.len() != 1952 {
853 return Err(TokenError::InvalidParameter(format!(
854 "rotate_keys: new pq_pubkey must be 1952 bytes (ML-DSA-65), got {}",
855 new_pq_pubkey.len()
856 )));
857 }
858 if new_bls_pubkey.len() != 48 {
859 return Err(TokenError::InvalidParameter(format!(
860 "rotate_keys: new bls_pubkey must be 48 bytes (BLS12-381 G1 compressed), got {}",
861 new_bls_pubkey.len()
862 )));
863 }
864
865 let mut entry = self
866 .entries
867 .get_mut(address)
868 .ok_or_else(|| TokenError::NotFound(format!("validator {}", address)))?;
869
870 if matches!(
873 entry.status,
874 ValidatorRegistryStatus::Exited | ValidatorRegistryStatus::Jailed
875 ) {
876 return Err(TokenError::InvalidParameter(format!(
877 "cannot rotate keys for validator {} in state {:?} — \
878 reactivate first",
879 address, entry.status
880 )));
881 }
882
883 if entry.consensus_pubkey == new_consensus_pubkey
885 && entry.pq_pubkey == new_pq_pubkey
886 && entry.bls_pubkey == new_bls_pubkey
887 {
888 return Ok(());
889 }
890
891 entry.consensus_pubkey = new_consensus_pubkey;
892 entry.pq_pubkey = new_pq_pubkey;
893 entry.bls_pubkey = new_bls_pubkey;
894 entry.updated_at = Timestamp::now();
895 let snapshot = entry.clone();
896 drop(entry);
897 self.persist_entry(&snapshot);
898 info!(
899 "Validator {} keys rotated — change takes effect at next epoch boundary",
900 address
901 );
902 Ok(())
903 }
904
905 pub fn jail(&self, address: &Address, current_epoch: u64) -> Result<()> {
908 let mut entry = self
909 .entries
910 .get_mut(address)
911 .ok_or_else(|| TokenError::NotFound(format!("validator {}", address)))?;
912
913 entry.status = ValidatorRegistryStatus::Jailed;
914 entry.jailed_until_epoch = None; entry.exited_at_epoch = Some(current_epoch);
916 entry.updated_at = Timestamp::now();
917 let snapshot = entry.clone();
918 drop(entry);
919 self.persist_entry(&snapshot);
920 warn!(
921 "Validator {} jailed at epoch {} — pending governance restoration",
922 address, current_epoch
923 );
924 Ok(())
925 }
926
927 pub fn compute_epoch_transition(&self, new_epoch: u64) -> EpochTransitionPlan {
935 let (act_budget, exit_budget) = self.churn_budgets();
936 let mut plan = EpochTransitionPlan::default();
937
938 let to_finalize: Vec<(Address, ValidatorRegistryStatus)> = self
941 .entries
942 .iter()
943 .filter_map(|e| {
944 let status = e.value().status;
945 if matches!(
946 status,
947 ValidatorRegistryStatus::PendingActive | ValidatorRegistryStatus::PendingExit
948 ) {
949 Some((e.value().address, status))
950 } else {
951 None
952 }
953 })
954 .collect();
955
956 for (addr, status) in to_finalize {
957 if let Some(mut entry) = self.entries.get_mut(&addr) {
958 match status {
959 ValidatorRegistryStatus::PendingActive => {
960 entry.status = ValidatorRegistryStatus::Active;
961 entry.activated_at_epoch = Some(new_epoch);
962 entry.updated_at = Timestamp::now();
963 plan.effective_activations.push(addr);
964 }
965 ValidatorRegistryStatus::PendingExit => {
966 entry.status = ValidatorRegistryStatus::Exited;
967 entry.exited_at_epoch = Some(new_epoch);
968 entry.updated_at = Timestamp::now();
969 plan.effective_exits.push(addr);
970 }
971 _ => unreachable!(),
972 }
973 let snapshot = entry.clone();
974 drop(entry);
975 self.persist_entry(&snapshot);
976 }
977 }
978
979 let mut candidates: Vec<(Address, u128, u64)> = self
984 .entries
985 .iter()
986 .filter(|e| e.value().status == ValidatorRegistryStatus::Candidate)
987 .map(|e| {
988 (
989 e.value().address,
990 e.value().self_stake,
991 e.value().registered_at_epoch,
992 )
993 })
994 .collect();
995 candidates.sort_by(|a, b| {
996 b.1.cmp(&a.1)
997 .then(a.2.cmp(&b.2))
998 .then(a.0.as_bytes().cmp(b.0.as_bytes()))
999 });
1000
1001 for (addr, _, _) in candidates.into_iter().take(act_budget as usize) {
1002 if let Some(mut entry) = self.entries.get_mut(&addr) {
1003 entry.status = ValidatorRegistryStatus::PendingActive;
1004 entry.updated_at = Timestamp::now();
1005 plan.activations.push(addr);
1006 let snapshot = entry.clone();
1007 drop(entry);
1008 self.persist_entry(&snapshot);
1009 }
1010 }
1011
1012 if plan.effective_exits.len() > exit_budget as usize {
1019 let overflow = plan.effective_exits.split_off(exit_budget as usize);
1020 for addr in &overflow {
1021 if let Some(mut entry) = self.entries.get_mut(addr) {
1022 entry.status = ValidatorRegistryStatus::PendingExit;
1023 entry.exited_at_epoch = None;
1024 entry.updated_at = Timestamp::now();
1025 let snapshot = entry.clone();
1026 drop(entry);
1027 self.persist_entry(&snapshot);
1028 }
1029 }
1030 }
1031
1032 info!(
1033 "Epoch {} transition: +{} activations ({} effective), -{} exits ({} effective)",
1034 new_epoch,
1035 plan.activations.len(),
1036 plan.effective_activations.len(),
1037 plan.exits.len(),
1038 plan.effective_exits.len()
1039 );
1040
1041 plan
1042 }
1043}
1044
1045impl Default for ValidatorRegistry {
1046 fn default() -> Self {
1047 Self::new()
1048 }
1049}
1050
1051#[cfg(test)]
1052mod tests {
1053 use super::*;
1054 use tenzro_crypto::pq::ML_DSA_65_VK_LEN;
1055
1056 fn make_address(seed: u8) -> Address {
1057 let mut bytes = [0u8; 32];
1058 bytes[0] = seed;
1059 Address::new(bytes)
1060 }
1061
1062 fn make_keys() -> (Vec<u8>, Vec<u8>, Vec<u8>) {
1063 (vec![0u8; 32], vec![0u8; ML_DSA_65_VK_LEN], vec![0u8; 48])
1064 }
1065
1066 #[test]
1067 fn seed_genesis_active_idempotent() {
1068 let reg = ValidatorRegistry::new();
1069 let (ck, pk, bk) = make_keys();
1070 let a = make_address(7);
1071
1072 let inserted = reg
1074 .seed_genesis_active(
1075 a,
1076 ck.clone(),
1077 pk.clone(),
1078 bk.clone(),
1079 a,
1080 DEFAULT_MIN_VALIDATOR_SELF_STAKE,
1081 String::new(),
1082 )
1083 .unwrap();
1084 assert!(inserted);
1085 let entry = reg.get(&a).unwrap();
1086 assert_eq!(entry.status, ValidatorRegistryStatus::Active);
1087 assert_eq!(entry.activated_at_epoch, Some(0));
1088 assert_eq!(entry.registered_at_epoch, 0);
1089 assert_eq!(entry.self_stake, DEFAULT_MIN_VALIDATOR_SELF_STAKE);
1090
1091 let inserted_again = reg
1093 .seed_genesis_active(
1094 a,
1095 ck.clone(),
1096 pk.clone(),
1097 bk.clone(),
1098 a,
1099 DEFAULT_MIN_VALIDATOR_SELF_STAKE * 2,
1100 String::from("changed"),
1101 )
1102 .unwrap();
1103 assert!(!inserted_again);
1104 let entry2 = reg.get(&a).unwrap();
1105 assert_eq!(entry2.self_stake, DEFAULT_MIN_VALIDATOR_SELF_STAKE);
1106 assert_eq!(entry2.metadata_uri, "");
1107
1108 assert_eq!(reg.list_active().len(), 1);
1110 }
1111
1112 #[test]
1113 fn seed_genesis_active_rejects_bad_keys() {
1114 let reg = ValidatorRegistry::new();
1115 let a = make_address(8);
1116
1117 let err = reg
1119 .seed_genesis_active(
1120 a,
1121 vec![0u8; 31],
1122 vec![0u8; ML_DSA_65_VK_LEN],
1123 vec![0u8; 48],
1124 a,
1125 DEFAULT_MIN_VALIDATOR_SELF_STAKE,
1126 String::new(),
1127 )
1128 .unwrap_err();
1129 assert!(matches!(err, TokenError::InvalidParameter(_)));
1130
1131 let err = reg
1133 .seed_genesis_active(
1134 a,
1135 vec![0u8; 32],
1136 vec![0u8; 100],
1137 vec![0u8; 48],
1138 a,
1139 DEFAULT_MIN_VALIDATOR_SELF_STAKE,
1140 String::new(),
1141 )
1142 .unwrap_err();
1143 assert!(matches!(err, TokenError::InvalidParameter(_)));
1144
1145 let err = reg
1147 .seed_genesis_active(
1148 a,
1149 vec![0u8; 32],
1150 vec![0u8; ML_DSA_65_VK_LEN],
1151 vec![0u8; 47],
1152 a,
1153 DEFAULT_MIN_VALIDATOR_SELF_STAKE,
1154 String::new(),
1155 )
1156 .unwrap_err();
1157 assert!(matches!(err, TokenError::InvalidParameter(_)));
1158 }
1159
1160 #[test]
1161 fn register_below_min_stake_admits_as_tier_1() {
1162 let reg = ValidatorRegistry::new();
1168 let (ck, pk, bk) = make_keys();
1169 let a = make_address(1);
1170
1171 reg.register_candidate(a, ck.clone(), pk.clone(), bk.clone(), a, 0, 0, String::new())
1173 .unwrap();
1174 let entry = reg.get(&a).unwrap();
1175 assert_eq!(entry.tier, ValidatorTier::ResourceOnly);
1176 assert_eq!(entry.self_stake, 0);
1177 assert_eq!(entry.status, ValidatorRegistryStatus::Candidate);
1178 }
1179
1180 #[test]
1181 fn register_at_tier_2_threshold_sets_staked() {
1182 let reg = ValidatorRegistry::new();
1183 let (ck, pk, bk) = make_keys();
1184 let a = make_address(2);
1185 reg.register_candidate(
1186 a,
1187 ck,
1188 pk,
1189 bk,
1190 a,
1191 DEFAULT_MIN_VALIDATOR_SELF_STAKE,
1192 0,
1193 String::new(),
1194 )
1195 .unwrap();
1196 assert_eq!(reg.get(&a).unwrap().tier, ValidatorTier::Staked);
1197 }
1198
1199 #[test]
1200 fn register_at_tier_3_threshold_sets_rpc_provider() {
1201 let reg = ValidatorRegistry::new();
1202 let (ck, pk, bk) = make_keys();
1203 let a = make_address(3);
1204 reg.register_candidate(
1205 a,
1206 ck,
1207 pk,
1208 bk,
1209 a,
1210 DEFAULT_MIN_RPC_PROVIDER_STAKE,
1211 0,
1212 String::new(),
1213 )
1214 .unwrap();
1215 assert_eq!(reg.get(&a).unwrap().tier, ValidatorTier::RpcProvider);
1216 }
1217
1218 #[test]
1219 fn register_then_activate_then_exit_full_lifecycle() {
1220 let reg = ValidatorRegistry::new();
1221 let (ck, pk, bk) = make_keys();
1222 let a = make_address(1);
1223 reg.register_candidate(
1224 a,
1225 ck.clone(),
1226 pk.clone(),
1227 bk.clone(),
1228 a,
1229 DEFAULT_MIN_VALIDATOR_SELF_STAKE,
1230 0,
1231 String::new(),
1232 )
1233 .unwrap();
1234 assert_eq!(
1235 reg.get(&a).unwrap().status,
1236 ValidatorRegistryStatus::Candidate
1237 );
1238
1239 let plan = reg.compute_epoch_transition(1);
1241 assert_eq!(plan.activations, vec![a]);
1242 assert_eq!(
1243 reg.get(&a).unwrap().status,
1244 ValidatorRegistryStatus::PendingActive
1245 );
1246
1247 let plan = reg.compute_epoch_transition(2);
1249 assert_eq!(plan.effective_activations, vec![a]);
1250 assert_eq!(reg.get(&a).unwrap().status, ValidatorRegistryStatus::Active);
1251
1252 reg.request_exit(&a).unwrap();
1254 assert_eq!(
1255 reg.get(&a).unwrap().status,
1256 ValidatorRegistryStatus::PendingExit
1257 );
1258
1259 let plan = reg.compute_epoch_transition(3);
1261 assert_eq!(plan.effective_exits, vec![a]);
1262 assert_eq!(reg.get(&a).unwrap().status, ValidatorRegistryStatus::Exited);
1263 assert_eq!(reg.get(&a).unwrap().exited_at_epoch, Some(3));
1264 }
1265
1266 #[test]
1267 fn reentry_blocked_during_cooldown() {
1268 let reg = ValidatorRegistry::new();
1269 let (ck, pk, bk) = make_keys();
1270 let a = make_address(1);
1271 reg.register_candidate(
1272 a,
1273 ck.clone(),
1274 pk.clone(),
1275 bk.clone(),
1276 a,
1277 DEFAULT_MIN_VALIDATOR_SELF_STAKE,
1278 0,
1279 String::new(),
1280 )
1281 .unwrap();
1282 reg.compute_epoch_transition(1);
1283 reg.compute_epoch_transition(2);
1284 reg.request_exit(&a).unwrap();
1285 reg.compute_epoch_transition(3);
1286 assert_eq!(reg.get(&a).unwrap().status, ValidatorRegistryStatus::Exited);
1287
1288 let err = reg
1290 .register_candidate(
1291 a,
1292 ck.clone(),
1293 pk.clone(),
1294 bk.clone(),
1295 a,
1296 DEFAULT_MIN_VALIDATOR_SELF_STAKE,
1297 3,
1298 String::new(),
1299 )
1300 .unwrap_err();
1301 assert!(matches!(err, TokenError::Unauthorized { .. }));
1302
1303 reg.register_candidate(
1305 a,
1306 ck,
1307 pk,
1308 bk,
1309 a,
1310 DEFAULT_MIN_VALIDATOR_SELF_STAKE,
1311 3 + DEFAULT_REENTRY_COOLDOWN_EPOCHS,
1312 String::new(),
1313 )
1314 .unwrap();
1315 assert_eq!(
1316 reg.get(&a).unwrap().status,
1317 ValidatorRegistryStatus::Candidate
1318 );
1319 }
1320
1321 #[test]
1322 fn jail_forces_exit() {
1323 let reg = ValidatorRegistry::new();
1324 let (ck, pk, bk) = make_keys();
1325 let a = make_address(1);
1326 reg.register_candidate(
1327 a,
1328 ck,
1329 pk,
1330 bk,
1331 a,
1332 DEFAULT_MIN_VALIDATOR_SELF_STAKE,
1333 0,
1334 String::new(),
1335 )
1336 .unwrap();
1337 reg.compute_epoch_transition(1);
1338 reg.compute_epoch_transition(2);
1339 assert_eq!(reg.get(&a).unwrap().status, ValidatorRegistryStatus::Active);
1340 reg.jail(&a, 2).unwrap();
1341 assert_eq!(reg.get(&a).unwrap().status, ValidatorRegistryStatus::Jailed);
1342 }
1344
1345 #[test]
1346 fn churn_cap_limits_activations() {
1347 let reg = ValidatorRegistry::new();
1348 for i in 1..=5u8 {
1351 let (ck, pk, bk) = make_keys();
1352 let a = make_address(i);
1353 reg.register_candidate(
1354 a,
1355 ck,
1356 pk,
1357 bk,
1358 a,
1359 DEFAULT_MIN_VALIDATOR_SELF_STAKE,
1360 0,
1361 String::new(),
1362 )
1363 .unwrap();
1364 }
1365 for i in 1..=5u8 {
1370 let a = make_address(i);
1371 if let Some(mut entry) = reg.entries.get_mut(&a) {
1372 entry.status = ValidatorRegistryStatus::Active;
1373 }
1374 }
1375 assert_eq!(reg.list_active().len(), 5);
1376
1377 for i in 6..=8u8 {
1379 let (ck, pk, bk) = make_keys();
1380 let a = make_address(i);
1381 reg.register_candidate(
1382 a,
1383 ck,
1384 pk,
1385 bk,
1386 a,
1387 DEFAULT_MIN_VALIDATOR_SELF_STAKE,
1388 0,
1389 String::new(),
1390 )
1391 .unwrap();
1392 }
1393 let plan = reg.compute_epoch_transition(1);
1394 assert_eq!(plan.activations.len(), 1);
1397 }
1398
1399 #[test]
1400 fn rotate_keys_updates_registry_in_place() {
1401 let reg = ValidatorRegistry::new();
1402 let (ck0, pk0, bk0) = make_keys();
1403 let a = make_address(11);
1404 reg.register_candidate(
1405 a,
1406 ck0.clone(),
1407 pk0.clone(),
1408 bk0.clone(),
1409 a,
1410 DEFAULT_MIN_VALIDATOR_SELF_STAKE,
1411 0,
1412 String::new(),
1413 )
1414 .unwrap();
1415
1416 let new_ck = vec![1u8; 32];
1418 let new_pk = vec![2u8; ML_DSA_65_VK_LEN];
1419 let new_bk = vec![3u8; 48];
1420 reg.rotate_keys(&a, new_ck.clone(), new_pk.clone(), new_bk.clone())
1421 .unwrap();
1422
1423 let entry = reg.get(&a).unwrap();
1424 assert_eq!(entry.consensus_pubkey, new_ck);
1425 assert_eq!(entry.pq_pubkey, new_pk);
1426 assert_eq!(entry.bls_pubkey, new_bk);
1427 assert_eq!(entry.self_stake, DEFAULT_MIN_VALIDATOR_SELF_STAKE);
1429 assert_eq!(entry.tier, ValidatorTier::Staked);
1430 assert_eq!(entry.status, ValidatorRegistryStatus::Candidate);
1431 assert_eq!(entry.withdrawal_address, a);
1432 }
1433
1434 #[test]
1435 fn rotate_keys_rejects_wrong_lengths() {
1436 let reg = ValidatorRegistry::new();
1437 let (ck, pk, bk) = make_keys();
1438 let a = make_address(12);
1439 reg.register_candidate(a, ck, pk, bk, a, 0, 0, String::new()).unwrap();
1440
1441 let bad = reg.rotate_keys(&a, vec![1u8; 31], vec![2u8; ML_DSA_65_VK_LEN], vec![3u8; 48]);
1443 assert!(bad.is_err(), "31-byte consensus_pubkey must be rejected");
1444
1445 let bad = reg.rotate_keys(&a, vec![1u8; 32], vec![2u8; 1951], vec![3u8; 48]);
1447 assert!(bad.is_err(), "wrong-length pq_pubkey must be rejected");
1448
1449 let bad = reg.rotate_keys(&a, vec![1u8; 32], vec![2u8; ML_DSA_65_VK_LEN], vec![3u8; 47]);
1451 assert!(bad.is_err(), "wrong-length bls_pubkey must be rejected");
1452 }
1453
1454 #[test]
1455 fn rotate_keys_refuses_exited_validator() {
1456 let reg = ValidatorRegistry::new();
1457 let (ck, pk, bk) = make_keys();
1458 let a = make_address(13);
1459 reg.register_candidate(
1460 a,
1461 ck.clone(),
1462 pk.clone(),
1463 bk.clone(),
1464 a,
1465 DEFAULT_MIN_VALIDATOR_SELF_STAKE,
1466 0,
1467 String::new(),
1468 )
1469 .unwrap();
1470
1471 reg.entries
1474 .alter(&a, |_, mut e| {
1475 e.status = ValidatorRegistryStatus::Exited;
1476 e
1477 });
1478
1479 let new_ck = vec![1u8; 32];
1480 let err = reg
1481 .rotate_keys(&a, new_ck, vec![2u8; ML_DSA_65_VK_LEN], vec![3u8; 48])
1482 .unwrap_err();
1483 let msg = format!("{}", err);
1484 assert!(
1485 msg.contains("Exited"),
1486 "expected Exited-state refusal, got: {}",
1487 msg
1488 );
1489 }
1490
1491 #[test]
1492 fn rotate_keys_idempotent_on_noop() {
1493 let reg = ValidatorRegistry::new();
1494 let (ck, pk, bk) = make_keys();
1495 let a = make_address(14);
1496 reg.register_candidate(
1497 a,
1498 ck.clone(),
1499 pk.clone(),
1500 bk.clone(),
1501 a,
1502 DEFAULT_MIN_VALIDATOR_SELF_STAKE,
1503 0,
1504 String::new(),
1505 )
1506 .unwrap();
1507
1508 reg.rotate_keys(&a, ck.clone(), pk.clone(), bk.clone())
1510 .unwrap();
1511 let entry = reg.get(&a).unwrap();
1512 assert_eq!(entry.consensus_pubkey, ck);
1513 }
1514}