Skip to main content

tenzro_consensus/
validator.rs

1//! Validator set management for consensus
2
3use crate::error::{ConsensusError, Result};
4use crate::leader_reputation::LeaderReputation;
5use crate::voter::Vote;
6use dashmap::DashMap;
7use serde::{Deserialize, Deserializer, Serialize};
8use std::sync::Arc;
9use tenzro_crypto::pq::ML_DSA_65_VK_LEN;
10use tenzro_crypto::PublicKey;
11
12/// BLS12-381 G1-compressed public key length (`min_pk` scheme used by
13/// `tenzro_crypto::bls`). Every validator MUST advertise a BLS verifying key
14/// for HotStuff-2 vote-signature aggregation.
15pub const BLS_G1_COMPRESSED_LEN: usize = 48;
16use tenzro_types::primitives::{Address, Hash, Timestamp};
17use tenzro_types::tee::{AttestationReport, AttestationResult};
18
19/// Deserialize an ML-DSA-65 verifying key, rejecting any byte string that does
20/// not match the FIPS 204 length (1952 bytes). This prevents downgrade or
21/// truncation attacks at the wire level — every validator MUST advertise a
22/// well-formed PQ key per the Wave 3d migration.
23fn deserialize_pq_verifying_key<'de, D>(deserializer: D) -> std::result::Result<Vec<u8>, D::Error>
24where
25    D: Deserializer<'de>,
26{
27    let bytes = Vec::<u8>::deserialize(deserializer)?;
28    if bytes.len() != ML_DSA_65_VK_LEN {
29        return Err(serde::de::Error::custom(format!(
30            "validator PQ verifying key has wrong length: expected {} bytes (ML-DSA-65), got {}",
31            ML_DSA_65_VK_LEN,
32            bytes.len()
33        )));
34    }
35    Ok(bytes)
36}
37
38/// Deserialize a BLS12-381 G1-compressed verifying key, rejecting any byte
39/// string that does not match the 48-byte length. Every validator in the
40/// active set MUST advertise a well-formed BLS key for HotStuff-2 vote
41/// aggregation per ROADMAP B.1.
42fn deserialize_bls_verifying_key<'de, D>(deserializer: D) -> std::result::Result<Vec<u8>, D::Error>
43where
44    D: Deserializer<'de>,
45{
46    let bytes = Vec::<u8>::deserialize(deserializer)?;
47    if bytes.len() != BLS_G1_COMPRESSED_LEN {
48        return Err(serde::de::Error::custom(format!(
49            "validator BLS verifying key has wrong length: expected {} bytes (BLS12-381 G1 compressed), got {}",
50            BLS_G1_COMPRESSED_LEN,
51            bytes.len()
52        )));
53    }
54    Ok(bytes)
55}
56
57/// Information about a validator
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct ValidatorInfo {
60    /// Validator's address
61    pub address: Address,
62
63    /// Validator's classical public key (Ed25519) for signing
64    pub public_key: PublicKey,
65
66    /// Validator's ML-DSA-65 verifying key (1952 bytes, FIPS 204) for the
67    /// post-quantum signing leg. Mandatory: every validator in the active set
68    /// must advertise a hybrid key per the Wave 3d migration.
69    #[serde(deserialize_with = "deserialize_pq_verifying_key")]
70    pub pq_public_key: Vec<u8>,
71
72    /// Validator's BLS12-381 G1-compressed verifying key (48 bytes, `min_pk`
73    /// scheme). Mandatory: every validator MUST advertise a BLS key for
74    /// HotStuff-2 vote-signature aggregation per ROADMAP B.1. The aggregation
75    /// is the third signature leg alongside the per-vote Ed25519 + ML-DSA-65
76    /// `CompositeSignature` — BLS provides O(1) bandwidth and verification
77    /// CPU savings on the QC path; Composite preserves the PQ-hybrid property
78    /// per individual vote (Composite is pre-quantum on its classical leg
79    /// only, BLS12-381 itself is pre-quantum, hence we keep both).
80    #[serde(deserialize_with = "deserialize_bls_verifying_key")]
81    pub bls_public_key: Vec<u8>,
82
83    /// Stake amount (voting power)
84    pub stake: u128,
85
86    /// TEE attestation report (optional)
87    pub tee_attestation: Option<AttestationReport>,
88
89    /// TEE attestation verification result
90    pub tee_attestation_result: Option<AttestationResult>,
91
92    /// Validator status
93    pub status: ValidatorStatus,
94
95    /// Registration timestamp
96    pub registered_at: Timestamp,
97
98    /// Last attestation update
99    pub last_attestation_update: Option<Timestamp>,
100}
101
102impl ValidatorInfo {
103    /// Creates a new validator info.
104    ///
105    /// # Panics
106    ///
107    /// Panics if:
108    /// - `pq_public_key.len() != ML_DSA_65_VK_LEN` (1952 bytes), or
109    /// - `bls_public_key.len() != BLS_G1_COMPRESSED_LEN` (48 bytes).
110    ///
111    /// Both keys are mandatory; there is no fallback path. Construct the PQ
112    /// key via `MlDsaSigningKey::generate()` and pass
113    /// `key.verifying_key_bytes().to_vec()`. Construct the BLS key via
114    /// `tenzro_crypto::bls::BlsKeyPair::generate()` and pass
115    /// `keypair.public_key().to_bytes().to_vec()`.
116    pub fn new(
117        address: Address,
118        public_key: PublicKey,
119        pq_public_key: Vec<u8>,
120        bls_public_key: Vec<u8>,
121        stake: u128,
122    ) -> Self {
123        assert_eq!(
124            pq_public_key.len(),
125            ML_DSA_65_VK_LEN,
126            "validator PQ verifying key has wrong length: expected {} bytes (ML-DSA-65), got {}",
127            ML_DSA_65_VK_LEN,
128            pq_public_key.len()
129        );
130        assert_eq!(
131            bls_public_key.len(),
132            BLS_G1_COMPRESSED_LEN,
133            "validator BLS verifying key has wrong length: expected {} bytes (BLS12-381 G1 compressed), got {}",
134            BLS_G1_COMPRESSED_LEN,
135            bls_public_key.len()
136        );
137        Self {
138            address,
139            public_key,
140            pq_public_key,
141            bls_public_key,
142            stake,
143            tee_attestation: None,
144            tee_attestation_result: None,
145            status: ValidatorStatus::Active,
146            registered_at: Timestamp::now(),
147            last_attestation_update: None,
148        }
149    }
150
151    /// Returns the composite (classical + PQ) public key for this validator,
152    /// suitable for hybrid signature verification via `StandardHybridVerifier`.
153    pub fn composite_public_key(&self) -> tenzro_crypto::composite::CompositePublicKey {
154        tenzro_crypto::composite::CompositePublicKey::new(
155            self.public_key.clone(),
156            self.pq_public_key.clone(),
157        )
158    }
159
160    /// Adds TEE attestation to the validator
161    pub fn with_tee_attestation(
162        mut self,
163        attestation: AttestationReport,
164        result: AttestationResult,
165    ) -> Self {
166        self.tee_attestation = Some(attestation);
167        self.tee_attestation_result = Some(result);
168        self.last_attestation_update = Some(Timestamp::now());
169        self
170    }
171
172    /// Returns whether the validator has valid TEE attestation
173    pub fn has_valid_tee_attestation(&self) -> bool {
174        self.tee_attestation_result
175            .as_ref()
176            .map(|r| r.valid)
177            .unwrap_or(false)
178    }
179
180    /// Returns whether the validator is active
181    pub fn is_active(&self) -> bool {
182        self.status == ValidatorStatus::Active
183    }
184
185    /// Returns the voting power of the validator
186    pub fn voting_power(&self) -> u128 {
187        if self.is_active() {
188            self.stake
189        } else {
190            0
191        }
192    }
193
194    /// Returns the base priority for leader selection.
195    ///
196    /// This is just the validator's voting power (stake when active, zero
197    /// otherwise). Any TEE multiplier is applied by the proposer-election
198    /// implementation (see [`ReputationProposer`]), not baked into the
199    /// validator's intrinsic priority — so a TEE-attested validator with
200    /// degraded behaviour can still be deprioritized by reputation.
201    pub fn leader_priority(&self) -> u128 {
202        self.voting_power()
203    }
204}
205
206/// Validator operational status.
207///
208/// This mirrors the lifecycle in [`tenzro_token::ValidatorRegistryStatus`]
209/// but is the local view consensus uses to decide who participates in the
210/// current round. The token-side registry is the source of truth across
211/// epoch boundaries; consensus only ever sees `Active` / `Inactive` /
212/// `Jailed` / `Unbonding` states reflected from the registry's transition
213/// plan.
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
215pub enum ValidatorStatus {
216    /// Active and participating in consensus
217    Active,
218    /// Temporarily inactive
219    Inactive,
220    /// Jailed due to misbehavior
221    Jailed,
222    /// Unbonding (pending removal)
223    Unbonding,
224}
225
226/// A set of validators for an epoch
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct ValidatorSet {
229    /// Current epoch number
230    pub epoch: u64,
231
232    /// List of validators
233    validators: Vec<ValidatorInfo>,
234
235    /// Total stake across all validators
236    total_stake: u128,
237
238    /// Epoch start timestamp
239    pub epoch_start: Timestamp,
240}
241
242impl ValidatorSet {
243    /// Creates a new validator set for the given epoch
244    pub fn new(epoch: u64, validators: Vec<ValidatorInfo>) -> Result<Self> {
245        if validators.is_empty() {
246            return Err(ConsensusError::InvalidValidatorSet(
247                "Validator set cannot be empty".to_string(),
248            ));
249        }
250
251        let total_stake = validators.iter().map(|v| v.voting_power()).sum();
252
253        Ok(Self {
254            epoch,
255            validators,
256            total_stake,
257            epoch_start: Timestamp::now(),
258        })
259    }
260
261    /// Returns the validator at the given index
262    pub fn get(&self, index: usize) -> Option<&ValidatorInfo> {
263        self.validators.get(index)
264    }
265
266    /// Returns the validator with the given address
267    pub fn get_by_address(&self, address: &Address) -> Option<&ValidatorInfo> {
268        self.validators.iter().find(|v| &v.address == address)
269    }
270
271    /// Returns the position of a validator in `self.validators` matching the
272    /// given address, or `None` if not present.
273    ///
274    /// This is the canonical "validator index" used for bitmap-based BLS
275    /// signer encoding in [`crate::voter::QuorumCertificate::signer_bitmap`].
276    /// The index is stable for the lifetime of the validator set (i.e. the
277    /// duration of one epoch); a new epoch may renumber.
278    pub fn index_of(&self, address: &Address) -> Option<usize> {
279        self.validators.iter().position(|v| &v.address == address)
280    }
281
282    /// Returns the validators slice in canonical order (insertion / epoch
283    /// order). The position of each entry in this slice is the bit index used
284    /// by [`crate::voter::QuorumCertificate::signer_bitmap`] when verifying
285    /// the BLS aggregate.
286    pub fn active_validators(&self) -> &[ValidatorInfo] {
287        &self.validators
288    }
289
290    /// Returns the number of validators
291    pub fn len(&self) -> usize {
292        self.validators.len()
293    }
294
295    /// Returns whether the validator set is empty
296    pub fn is_empty(&self) -> bool {
297        self.validators.is_empty()
298    }
299
300    /// Returns an iterator over the validators
301    pub fn iter(&self) -> std::slice::Iter<'_, ValidatorInfo> {
302        self.validators.iter()
303    }
304
305    /// Returns the total stake
306    pub fn total_stake(&self) -> u128 {
307        self.total_stake
308    }
309
310    /// Returns the total voting power (sum of active validators' stake)
311    pub fn total_voting_power(&self) -> u128 {
312        self.validators.iter().map(|v| v.voting_power()).sum()
313    }
314
315    /// Returns whether the given address is a validator
316    pub fn is_validator(&self, address: &Address) -> bool {
317        self.get_by_address(address).is_some()
318    }
319
320    /// Selects the leader for the given view via deterministic round-robin.
321    ///
322    /// This is the simplest possible proposer election: `view % N`. It is
323    /// retained as a fallback / test path; production deployments should use
324    /// [`ReputationProposer`] via [`ProposerElection`] which incorporates
325    /// observed-behaviour reputation and a stake-weighted draw.
326    pub fn select_leader_round_robin(&self, view: u64) -> Result<&ValidatorInfo> {
327        if self.validators.is_empty() {
328            return Err(ConsensusError::InvalidValidatorSet(
329                "No validators available".to_string(),
330            ));
331        }
332        let index = (view as usize) % self.validators.len();
333        Ok(&self.validators[index])
334    }
335
336    /// Calculates the quorum threshold (2f+1)
337    pub fn quorum_threshold(&self) -> usize {
338        let n = self.validators.len();
339        let f = (n.saturating_sub(1)) / 3;
340        2 * f + 1
341    }
342
343    /// Returns the validators with valid TEE attestation
344    pub fn tee_attested_validators(&self) -> Vec<&ValidatorInfo> {
345        self.validators
346            .iter()
347            .filter(|v| v.has_valid_tee_attestation())
348            .collect()
349    }
350
351    /// Returns the percentage of validators with TEE attestation
352    pub fn tee_attestation_rate(&self) -> f64 {
353        if self.validators.is_empty() {
354            return 0.0;
355        }
356        let attested = self.tee_attested_validators().len();
357        (attested as f64 / self.validators.len() as f64) * 100.0
358    }
359}
360
361/// Evidence of validator equivocation (voting for multiple blocks in the same view)
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct EquivocationEvidence {
364    /// The validator who equivocated
365    pub validator: Address,
366
367    /// View number where equivocation occurred
368    pub view: u64,
369
370    /// First vote
371    pub vote1: Vote,
372
373    /// Second conflicting vote
374    pub vote2: Vote,
375
376    /// Timestamp when evidence was detected
377    pub detected_at: Timestamp,
378}
379
380impl EquivocationEvidence {
381    /// Creates new equivocation evidence
382    pub fn new(validator: Address, view: u64, vote1: Vote, vote2: Vote) -> Self {
383        Self {
384            validator,
385            view,
386            vote1,
387            vote2,
388            detected_at: Timestamp::now(),
389        }
390    }
391
392    /// Verifies that the evidence is valid (same view, different blocks)
393    pub fn is_valid(&self) -> bool {
394        self.vote1.view == self.vote2.view
395            && self.vote1.view == self.view
396            && self.vote1.voter == self.vote2.voter
397            && self.vote1.voter == self.validator
398            && self.vote1.block_hash != self.vote2.block_hash
399            && self.vote1.vote_type == self.vote2.vote_type
400    }
401}
402
403/// Key for tracking votes per (validator, view) pair
404#[derive(Debug, Clone, PartialEq, Eq, Hash)]
405struct ValidatorViewKey {
406    validator: Address,
407    view: u64,
408}
409
410/// Tracks validator votes to detect equivocation
411pub struct EquivocationDetector {
412    /// Stores (block_hash, vote_type) for each (validator, view) pair
413    /// Key: (validator, view), Value: (block_hash, vote_type)
414    votes: Arc<DashMap<ValidatorViewKey, (Hash, crate::voter::VoteType)>>,
415
416    /// Detected equivocation evidence
417    evidence: Arc<DashMap<(Address, u64), EquivocationEvidence>>,
418
419    /// Optional persistence backend. When set, every recorded vote
420    /// and every detected EquivocationEvidence writes through to a
421    /// dedicated `equivocation:*` keyspace in `CF_AUDIT` and is
422    /// hydrated on construction. Without persistence, a validator
423    /// equivocator can simply restart their node to wipe the
424    /// detector's state and avoid slashing.
425    storage: Option<Arc<dyn tenzro_storage::KvStore>>,
426}
427
428impl EquivocationDetector {
429    /// Creates a new equivocation detector (in-memory only — test
430    /// path).
431    pub fn new() -> Self {
432        Self {
433            votes: Arc::new(DashMap::new()),
434            evidence: Arc::new(DashMap::new()),
435            storage: None,
436        }
437    }
438
439    /// Production constructor: write-through to `CF_AUDIT` under
440    /// `equivocation/votes/*` and `equivocation/evidence/*`. Hydrates
441    /// both maps on construction.
442    pub fn with_storage(storage: Arc<dyn tenzro_storage::KvStore>) -> Self {
443        let d = Self {
444            votes: Arc::new(DashMap::new()),
445            evidence: Arc::new(DashMap::new()),
446            storage: Some(storage),
447        };
448        d.hydrate();
449        d
450    }
451
452    fn vote_key(validator: &Address, view: u64) -> Vec<u8> {
453        let mut k = b"equivocation/votes/".to_vec();
454        k.extend_from_slice(validator.as_bytes());
455        k.push(b'/');
456        k.extend_from_slice(&view.to_le_bytes());
457        k
458    }
459    fn evidence_key(validator: &Address, view: u64) -> Vec<u8> {
460        let mut k = b"equivocation/evidence/".to_vec();
461        k.extend_from_slice(validator.as_bytes());
462        k.push(b'/');
463        k.extend_from_slice(&view.to_le_bytes());
464        k
465    }
466
467    fn hydrate(&self) {
468        let Some(ref storage) = self.storage else {
469            return;
470        };
471        if let Ok(entries) =
472            storage.scan_prefix(tenzro_storage::CF_AUDIT, b"equivocation/votes/")
473        {
474            for (key, value) in entries {
475                if let Some(vk) = Self::parse_vote_key(&key) {
476                    if let Ok(payload) =
477                        serde_json::from_slice::<(Hash, crate::voter::VoteType)>(&value)
478                    {
479                        self.votes.insert(vk, payload);
480                    }
481                }
482            }
483        }
484        if let Ok(entries) =
485            storage.scan_prefix(tenzro_storage::CF_AUDIT, b"equivocation/evidence/")
486        {
487            for (key, value) in entries {
488                if let Some((addr, view)) = Self::parse_evidence_key(&key) {
489                    if let Ok(ev) =
490                        serde_json::from_slice::<EquivocationEvidence>(&value)
491                    {
492                        self.evidence.insert((addr, view), ev);
493                    }
494                }
495            }
496        }
497    }
498
499    fn parse_vote_key(key: &[u8]) -> Option<ValidatorViewKey> {
500        let prefix = b"equivocation/votes/";
501        if !key.starts_with(prefix) {
502            return None;
503        }
504        let rest = &key[prefix.len()..];
505        if rest.len() < 9 {
506            return None;
507        }
508        let addr_end = rest.len() - 9;
509        if rest[addr_end] != b'/' {
510            return None;
511        }
512        let validator = Address::from_bytes(&rest[..addr_end])?;
513        let mut view_buf = [0u8; 8];
514        view_buf.copy_from_slice(&rest[addr_end + 1..]);
515        let view = u64::from_le_bytes(view_buf);
516        Some(ValidatorViewKey { validator, view })
517    }
518
519    fn parse_evidence_key(key: &[u8]) -> Option<(Address, u64)> {
520        let prefix = b"equivocation/evidence/";
521        if !key.starts_with(prefix) {
522            return None;
523        }
524        let rest = &key[prefix.len()..];
525        if rest.len() < 9 {
526            return None;
527        }
528        let addr_end = rest.len() - 9;
529        if rest[addr_end] != b'/' {
530            return None;
531        }
532        let validator = Address::from_bytes(&rest[..addr_end])?;
533        let mut view_buf = [0u8; 8];
534        view_buf.copy_from_slice(&rest[addr_end + 1..]);
535        let view = u64::from_le_bytes(view_buf);
536        Some((validator, view))
537    }
538
539    fn persist_vote(
540        &self,
541        vk: &ValidatorViewKey,
542        payload: &(Hash, crate::voter::VoteType),
543    ) {
544        if let Some(ref storage) = self.storage {
545            if let Ok(bytes) = serde_json::to_vec(payload) {
546                let _ = storage.put(
547                    tenzro_storage::CF_AUDIT,
548                    &Self::vote_key(&vk.validator, vk.view),
549                    &bytes,
550                );
551            }
552        }
553    }
554
555    fn persist_evidence(&self, ev: &EquivocationEvidence) {
556        if let Some(ref storage) = self.storage {
557            if let Ok(bytes) = serde_json::to_vec(ev) {
558                let _ = storage.put(
559                    tenzro_storage::CF_AUDIT,
560                    &Self::evidence_key(&ev.validator, ev.view),
561                    &bytes,
562                );
563            }
564        }
565    }
566
567    /// Records a vote and checks for equivocation
568    /// Returns Ok(None) if no equivocation detected
569    /// Returns Ok(Some(evidence)) if equivocation detected
570    /// Returns Err if the vote itself is invalid
571    pub fn check_vote(&self, vote: &Vote) -> Result<Option<EquivocationEvidence>> {
572        let key = ValidatorViewKey {
573            validator: vote.voter,
574            view: vote.view,
575        };
576
577        // Check if validator already voted in this view
578        if let Some(existing) = self.votes.get(&key) {
579            let (existing_hash, existing_type) = *existing;
580
581            // If voting for the same block, it's a duplicate (not equivocation)
582            if existing_hash == vote.block_hash && existing_type == vote.vote_type {
583                return Err(ConsensusError::AlreadyVoted(vote.view));
584            }
585
586            // If voting for a different block in the same view with same type, it's equivocation
587            if existing_type == vote.vote_type {
588                // Create the evidence
589                // We need to reconstruct the first vote from stored data.
590                // The original signature/public_key were not stored (we only
591                // need the height, hash, voter, and type to prove
592                // equivocation), so we synthesise an empty composite signature
593                // and reuse the second vote's public key as a stand-in. The
594                // EquivocationEvidence::is_valid() check inspects only
595                // view/voter/block_hash/vote_type, so this is sufficient for
596                // slashing evidence.
597                let placeholder_sig = tenzro_crypto::composite::CompositeSignature::new(
598                    Vec::new(),
599                    Vec::new(),
600                );
601                // BLS signature follows the same stand-in rationale as the
602                // composite sig + public key above — `EquivocationEvidence::is_valid`
603                // only inspects view/voter/block_hash/vote_type.
604                let placeholder_bls = vote.bls_signature.clone();
605                let vote1 = Vote::new(
606                    vote.view,
607                    vote.height,
608                    existing_hash,
609                    vote.voter,
610                    placeholder_sig,
611                    vote.public_key.clone(),
612                    placeholder_bls,
613                    existing_type,
614                    vote.high_qc_view,
615                );
616
617                let evidence = EquivocationEvidence::new(
618                    vote.voter,
619                    vote.view,
620                    vote1,
621                    vote.clone(),
622                );
623
624                // Store the evidence
625                self.evidence.insert((vote.voter, vote.view), evidence.clone());
626                self.persist_evidence(&evidence);
627
628                tracing::warn!(
629                    validator = %vote.voter,
630                    view = vote.view,
631                    block1 = %existing_hash,
632                    block2 = %vote.block_hash,
633                    "Equivocation detected"
634                );
635
636                return Ok(Some(evidence));
637            }
638        }
639
640        // Record this vote
641        let payload = (vote.block_hash, vote.vote_type);
642        self.votes.insert(key.clone(), payload);
643        self.persist_vote(&key, &payload);
644
645        Ok(None)
646    }
647
648    /// Gets all detected equivocation evidence
649    pub fn get_all_evidence(&self) -> Vec<EquivocationEvidence> {
650        self.evidence.iter().map(|entry| entry.value().clone()).collect()
651    }
652
653    /// Gets equivocation evidence for a specific validator and view
654    pub fn get_evidence(&self, validator: &Address, view: u64) -> Option<EquivocationEvidence> {
655        self.evidence.get(&(*validator, view)).map(|e| e.clone())
656    }
657
658    /// Clears votes for views below the given minimum (cleanup).
659    ///
660    /// Prunes both the in-memory vote map AND the persisted
661    /// `equivocation/votes/*` rows — without the persisted delete, every
662    /// restart re-hydrated an unbounded backlog of stale votes that the
663    /// in-memory prune had already discarded (unbounded CF_AUDIT growth
664    /// plus stale-vote mis-fires after view-state resets).
665    ///
666    /// Evidence is deliberately KEPT — in memory and on disk. Evidence is
667    /// the slashing record; pruning it by view would let an equivocator
668    /// outlast the cleanup window and escape the penalty.
669    pub fn cleanup_old_votes(&self, min_view: u64) {
670        let mut pruned: Vec<ValidatorViewKey> = Vec::new();
671        self.votes.retain(|key, _| {
672            let keep = key.view >= min_view;
673            if !keep {
674                pruned.push(key.clone());
675            }
676            keep
677        });
678        if let Some(ref storage) = self.storage {
679            for key in &pruned {
680                let _ = storage.delete(
681                    tenzro_storage::CF_AUDIT,
682                    &Self::vote_key(&key.validator, key.view),
683                );
684            }
685        }
686    }
687
688    /// Returns the number of tracked votes
689    pub fn vote_count(&self) -> usize {
690        self.votes.len()
691    }
692
693    /// Returns the number of detected equivocations
694    pub fn evidence_count(&self) -> usize {
695        self.evidence.len()
696    }
697}
698
699impl Default for EquivocationDetector {
700    fn default() -> Self {
701        Self::new()
702    }
703}
704
705// ---------------------------------------------------------------------------
706// Proposer election
707// ---------------------------------------------------------------------------
708
709/// Strategy interface for selecting the proposer of a given round/view.
710///
711/// Two concrete implementations are provided:
712///
713/// - [`RoundRobinProposer`] — naïve `view % N` rotation. Useful for tests and
714///   for validator sets so small that reputation history is not yet
715///   meaningful. **Not recommended for production.**
716/// - [`ReputationProposer`] — Aptos LeaderReputation. Stake-weighted draw
717///   whose per-validator weight is multiplied by an observed-behaviour term
718///   (active / inactive / failed). A flaky validator's effective weight
719///   collapses to ~0.1% of a healthy peer's within ~20 rounds, which is what
720///   prevents naïve round-robin from wedging the chain when one of N
721///   validators is unresponsive.
722///
723/// The trait returns an [`Address`] (not a `&ValidatorInfo`) so it composes
724/// cleanly with the engine's hot path: the engine resolves the address back
725/// to the validator info via `validator_set.get_by_address` only when needed.
726pub trait ProposerElection: Send + Sync {
727    /// Selects the proposer for `round` in `epoch`.
728    ///
729    /// `prev_block_id` is the hash of the most recently finalized block —
730    /// this is the anti-grinding seed component (the parent the new proposal
731    /// will extend). For round-robin this argument is unused; for
732    /// reputation-based selection it is mixed into the seed.
733    fn select_leader(
734        &self,
735        round: u64,
736        epoch: u64,
737        prev_block_id: [u8; 32],
738        validator_set: &ValidatorSet,
739    ) -> Result<Address>;
740}
741
742/// Naïve `view % N` round-robin proposer.
743///
744/// Retained because some configurations (smoke tests, very small validator
745/// sets, deterministic-replay benchmarks) genuinely want it. Production
746/// deployments must use [`ReputationProposer`].
747#[derive(Debug, Default, Clone, Copy)]
748pub struct RoundRobinProposer;
749
750impl RoundRobinProposer {
751    pub const fn new() -> Self {
752        Self
753    }
754}
755
756impl ProposerElection for RoundRobinProposer {
757    fn select_leader(
758        &self,
759        round: u64,
760        _epoch: u64,
761        _prev_block_id: [u8; 32],
762        validator_set: &ValidatorSet,
763    ) -> Result<Address> {
764        validator_set
765            .select_leader_round_robin(round)
766            .map(|v| v.address)
767    }
768}
769
770/// Aptos LeaderReputation proposer.
771///
772/// Wraps a shared [`LeaderReputation`] state and dispatches to its seeded
773/// weighted draw. The wrapped state is the same one the engine feeds with
774/// `record_round_outcome` / `record_round_voters` after each round closes —
775/// so the reputation evolves alongside consensus rather than being a
776/// per-call computation.
777#[derive(Clone)]
778pub struct ReputationProposer {
779    reputation: Arc<LeaderReputation>,
780}
781
782impl ReputationProposer {
783    /// Wraps an existing [`LeaderReputation`] instance.
784    pub fn new(reputation: Arc<LeaderReputation>) -> Self {
785        Self { reputation }
786    }
787
788    /// Borrowed handle to the wrapped reputation state — useful for the
789    /// engine to record outcomes / voters after round close without going
790    /// through the trait.
791    pub fn reputation(&self) -> &Arc<LeaderReputation> {
792        &self.reputation
793    }
794}
795
796impl ProposerElection for ReputationProposer {
797    fn select_leader(
798        &self,
799        round: u64,
800        epoch: u64,
801        prev_block_id: [u8; 32],
802        validator_set: &ValidatorSet,
803    ) -> Result<Address> {
804        let prev_hash = Hash::new(prev_block_id);
805        self.reputation
806            .select_leader(round, epoch, &prev_hash, validator_set)
807            .map(|v| v.address)
808    }
809}
810
811#[cfg(test)]
812mod tests {
813    use super::*;
814    use tenzro_crypto::bls::BlsKeyPair;
815    use tenzro_crypto::pq::MlDsaSigningKey;
816    use tenzro_crypto::{KeyPair, KeyType};
817
818    fn create_test_validator(stake: u128) -> ValidatorInfo {
819        let keypair = KeyPair::generate(KeyType::Ed25519).unwrap();
820        // Convert tenzro_crypto::Address (20 bytes) to tenzro_types::Address (32 bytes)
821        let crypto_addr = keypair.address();
822        let mut addr_bytes = [0u8; 32];
823        addr_bytes[..20].copy_from_slice(crypto_addr.as_bytes());
824        let address = Address::new(addr_bytes);
825        let pq = MlDsaSigningKey::generate();
826        let bls = BlsKeyPair::generate().unwrap();
827        ValidatorInfo::new(
828            address,
829            keypair.public_key().clone(),
830            pq.verifying_key_bytes().to_vec(),
831            bls.public_key().to_bytes().to_vec(),
832            stake,
833        )
834    }
835
836    #[test]
837    fn test_validator_info() {
838        let validator = create_test_validator(1000);
839        assert_eq!(validator.voting_power(), 1000);
840        assert!(validator.is_active());
841        assert!(!validator.has_valid_tee_attestation());
842    }
843
844    #[test]
845    fn test_validator_set_creation() {
846        let validators = vec![
847            create_test_validator(1000),
848            create_test_validator(2000),
849            create_test_validator(3000),
850        ];
851
852        let set = ValidatorSet::new(1, validators).unwrap();
853        assert_eq!(set.len(), 3);
854        assert_eq!(set.total_voting_power(), 6000);
855        assert_eq!(set.quorum_threshold(), 1); // 2f+1 where f=(n-1)/3=(3-1)/3=0, so 2*0+1=1
856    }
857
858    #[test]
859    fn test_leader_selection() {
860        let validators = vec![
861            create_test_validator(1000),
862            create_test_validator(2000),
863            create_test_validator(3000),
864        ];
865
866        let set = ValidatorSet::new(1, validators).unwrap();
867
868        // Round-robin
869        let leader0 = set.select_leader_round_robin(0).unwrap();
870        let leader1 = set.select_leader_round_robin(1).unwrap();
871        let leader2 = set.select_leader_round_robin(2).unwrap();
872        let leader3 = set.select_leader_round_robin(3).unwrap();
873
874        assert_eq!(leader0.address, set.validators[0].address);
875        assert_eq!(leader1.address, set.validators[1].address);
876        assert_eq!(leader2.address, set.validators[2].address);
877        assert_eq!(leader3.address, set.validators[0].address); // wraps around
878    }
879
880    #[test]
881    fn test_empty_validator_set() {
882        let result = ValidatorSet::new(1, vec![]);
883        assert!(result.is_err());
884    }
885
886    #[test]
887    fn test_equivocation_detection() {
888        use crate::voter::{Vote, VoteType};
889        use tenzro_crypto::composite::{CompositePublicKey, CompositeSignature};
890        use tenzro_types::primitives::BlockHeight;
891
892        let detector = EquivocationDetector::new();
893        let validator = create_test_validator(1000);
894
895        let placeholder_pk = CompositePublicKey::new(
896            validator.public_key.clone(),
897            validator.pq_public_key.clone(),
898        );
899        let placeholder_sig = CompositeSignature::new(vec![0u8; 64], vec![0u8; 3309]);
900        // EquivocationDetector::check_vote inspects view/voter/block_hash/vote_type
901        // only, so a real BLS signature over arbitrary bytes is enough — the
902        // detector never re-verifies it.
903        let placeholder_bls_kp = BlsKeyPair::generate().unwrap();
904        let placeholder_bls = placeholder_bls_kp.sign(b"__placeholder__");
905
906        let vote1 = Vote::new(
907            1,
908            BlockHeight::from(10),
909            Hash::default(),
910            validator.address,
911            placeholder_sig.clone(),
912            placeholder_pk.clone(),
913            placeholder_bls.clone(),
914            VoteType::Prepare,
915            0,
916        );
917
918        // First vote should be recorded without issue
919        let result = detector.check_vote(&vote1);
920        assert!(result.is_ok());
921        assert!(result.unwrap().is_none());
922
923        // Same vote again should error (already voted)
924        let result = detector.check_vote(&vote1);
925        assert!(result.is_err());
926
927        // Different block hash in same view should detect equivocation
928        let mut different_hash = [0u8; 32];
929        different_hash[0] = 1;
930        let vote2 = Vote::new(
931            1,
932            BlockHeight::from(10),
933            Hash::new(different_hash),
934            validator.address,
935            placeholder_sig.clone(),
936            placeholder_pk.clone(),
937            placeholder_bls.clone(),
938            VoteType::Prepare,
939            0,
940        );
941
942        let result = detector.check_vote(&vote2);
943        assert!(result.is_ok());
944        let evidence = result.unwrap();
945        assert!(evidence.is_some());
946
947        let evidence = evidence.unwrap();
948        assert_eq!(evidence.validator, validator.address);
949        assert_eq!(evidence.view, 1);
950        assert!(evidence.is_valid());
951    }
952
953    #[test]
954    fn test_equivocation_detector_cleanup() {
955        use crate::voter::{Vote, VoteType};
956        use tenzro_crypto::composite::{CompositePublicKey, CompositeSignature};
957        use tenzro_types::primitives::BlockHeight;
958
959        let detector = EquivocationDetector::new();
960        let validator = create_test_validator(1000);
961
962        let placeholder_pk = CompositePublicKey::new(
963            validator.public_key.clone(),
964            validator.pq_public_key.clone(),
965        );
966        let placeholder_sig = CompositeSignature::new(vec![0u8; 64], vec![0u8; 3309]);
967        let placeholder_bls_kp = BlsKeyPair::generate().unwrap();
968        let placeholder_bls = placeholder_bls_kp.sign(b"__placeholder__");
969
970        // Add votes for views 1, 2, 3
971        for view in 1..=3 {
972            let vote = Vote::new(
973                view,
974                BlockHeight::from(10),
975                Hash::default(),
976                validator.address,
977                placeholder_sig.clone(),
978                placeholder_pk.clone(),
979                placeholder_bls.clone(),
980                VoteType::Prepare,
981                0,
982            );
983            let _ = detector.check_vote(&vote);
984        }
985
986        assert_eq!(detector.vote_count(), 3);
987
988        // Cleanup views below 2
989        detector.cleanup_old_votes(2);
990
991        // Should only have views 2 and 3 remaining
992        assert_eq!(detector.vote_count(), 2);
993    }
994}