Skip to main content

sumchain_primitives/
staking.rs

1//! Staking types for SUM Chain validators.
2//!
3//! Defines validator staking structures, operations, and parameters.
4
5use serde::{Deserialize, Serialize};
6use serde_big_array::BigArray;
7
8use crate::{Balance, BlockHeight};
9
10/// Validator status in the staking system
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[repr(u8)]
13pub enum ValidatorStatus {
14    /// Actively participating in consensus
15    Active = 0,
16    /// Voluntarily stopped validating
17    Inactive = 1,
18    /// Penalized and temporarily removed from validator set
19    Jailed = 2,
20    /// Waiting to withdraw stake (unbonding period)
21    Unbonding = 3,
22}
23
24impl ValidatorStatus {
25    /// Convert from byte
26    pub fn from_byte(b: u8) -> Option<Self> {
27        match b {
28            0 => Some(ValidatorStatus::Active),
29            1 => Some(ValidatorStatus::Inactive),
30            2 => Some(ValidatorStatus::Jailed),
31            3 => Some(ValidatorStatus::Unbonding),
32            _ => None,
33        }
34    }
35
36    /// Check if validator can participate in consensus
37    pub fn can_validate(&self) -> bool {
38        matches!(self, ValidatorStatus::Active)
39    }
40}
41
42impl Default for ValidatorStatus {
43    fn default() -> Self {
44        ValidatorStatus::Inactive
45    }
46}
47
48/// Validator information stored on-chain
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct ValidatorInfo {
51    /// Validator's Ed25519 public key (32 bytes)
52    pub pubkey: [u8; 32],
53    /// Self-staked amount
54    pub stake: Balance,
55    /// Total amount delegated to this validator by others
56    pub total_delegated: Balance,
57    /// Commission rate in basis points (100 = 1%, max 10000 = 100%)
58    pub commission_bps: u16,
59    /// Current status
60    pub status: ValidatorStatus,
61    /// Block height when validator joined
62    pub joined_at: BlockHeight,
63    /// Block height when validator was jailed (0 if not jailed)
64    pub jailed_until: BlockHeight,
65    /// Number of times this validator has been slashed
66    pub slash_count: u32,
67    /// Accumulated rewards (not yet claimed)
68    pub pending_rewards: Balance,
69    /// Optional metadata (e.g., name, website) - max 256 bytes
70    pub metadata: Vec<u8>,
71}
72
73impl ValidatorInfo {
74    /// Create a new validator with initial stake
75    pub fn new(pubkey: [u8; 32], stake: Balance, commission_bps: u16, joined_at: BlockHeight) -> Self {
76        Self {
77            pubkey,
78            stake,
79            total_delegated: 0,
80            commission_bps: commission_bps.min(10000), // Cap at 100%
81            status: ValidatorStatus::Active,
82            joined_at,
83            jailed_until: 0,
84            slash_count: 0,
85            pending_rewards: 0,
86            metadata: Vec::new(),
87        }
88    }
89
90    /// Get total voting power (self-stake + delegations)
91    pub fn total_stake(&self) -> Balance {
92        self.stake.saturating_add(self.total_delegated)
93    }
94
95    /// Add delegation to this validator
96    pub fn add_delegation(&mut self, amount: Balance) {
97        self.total_delegated = self.total_delegated.saturating_add(amount);
98    }
99
100    /// Remove delegation from this validator
101    pub fn remove_delegation(&mut self, amount: Balance) {
102        self.total_delegated = self.total_delegated.saturating_sub(amount);
103    }
104
105    /// Check if validator is currently jailed
106    pub fn is_jailed(&self) -> bool {
107        self.status == ValidatorStatus::Jailed
108    }
109
110    /// Check if validator can be unjailed at given height
111    pub fn can_unjail(&self, current_height: BlockHeight) -> bool {
112        self.is_jailed() && current_height >= self.jailed_until
113    }
114
115    /// Apply a slash penalty
116    pub fn apply_slash(&mut self, penalty_bps: u16) {
117        let penalty = (self.stake * penalty_bps as u128) / 10000;
118        self.stake = self.stake.saturating_sub(penalty);
119        self.slash_count += 1;
120    }
121
122    /// Jail the validator until a specific height
123    pub fn jail(&mut self, until_height: BlockHeight) {
124        self.status = ValidatorStatus::Jailed;
125        self.jailed_until = until_height;
126    }
127
128    /// Unjail the validator
129    pub fn unjail(&mut self) {
130        self.status = ValidatorStatus::Active;
131        self.jailed_until = 0;
132    }
133}
134
135/// Staking operation types
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
137#[repr(u8)]
138pub enum StakingOperation {
139    /// Register as a new validator with initial stake
140    CreateValidator = 0,
141    /// Add more stake to an existing validator
142    AddStake = 1,
143    /// Begin unbonding stake (start withdrawal process)
144    Unstake = 2,
145    /// Update validator commission or metadata
146    UpdateValidator = 3,
147    /// Request to unjail after jail period
148    Unjail = 4,
149    /// Claim accumulated rewards
150    ClaimRewards = 5,
151    // Delegation operations (6-9)
152    /// Delegate tokens to a validator
153    Delegate = 6,
154    /// Begin unbonding delegation from a validator
155    Undelegate = 7,
156    /// Claim delegation rewards from a validator
157    ClaimDelegationRewards = 8,
158    /// Withdraw completed unbonding delegations
159    WithdrawUnbonded = 9,
160    // Slashing operations (10)
161    /// Submit evidence of misbehavior (double sign or downtime)
162    SubmitEvidence = 10,
163}
164
165impl StakingOperation {
166    /// Convert from byte
167    pub fn from_byte(b: u8) -> Option<Self> {
168        match b {
169            0 => Some(StakingOperation::CreateValidator),
170            1 => Some(StakingOperation::AddStake),
171            2 => Some(StakingOperation::Unstake),
172            3 => Some(StakingOperation::UpdateValidator),
173            4 => Some(StakingOperation::Unjail),
174            5 => Some(StakingOperation::ClaimRewards),
175            6 => Some(StakingOperation::Delegate),
176            7 => Some(StakingOperation::Undelegate),
177            8 => Some(StakingOperation::ClaimDelegationRewards),
178            9 => Some(StakingOperation::WithdrawUnbonded),
179            10 => Some(StakingOperation::SubmitEvidence),
180            _ => None,
181        }
182    }
183
184    /// Check if this operation requires the sender to be a validator
185    pub fn requires_validator(&self) -> bool {
186        matches!(
187            self,
188            StakingOperation::AddStake
189                | StakingOperation::Unstake
190                | StakingOperation::UpdateValidator
191                | StakingOperation::Unjail
192                | StakingOperation::ClaimRewards
193        )
194    }
195
196    /// Check if this operation is a delegation operation
197    pub fn is_delegation(&self) -> bool {
198        matches!(
199            self,
200            StakingOperation::Delegate
201                | StakingOperation::Undelegate
202                | StakingOperation::ClaimDelegationRewards
203                | StakingOperation::WithdrawUnbonded
204        )
205    }
206
207    /// Check if this operation is a slashing operation
208    pub fn is_slashing(&self) -> bool {
209        matches!(self, StakingOperation::SubmitEvidence)
210    }
211}
212
213/// Staking transaction data
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
215pub struct StakingTxData {
216    /// Staking operation type
217    pub operation: StakingOperation,
218    /// Operation-specific data (serialized)
219    pub data: Vec<u8>,
220}
221
222impl StakingTxData {
223    /// Create a new staking transaction data
224    pub fn new(operation: StakingOperation, data: Vec<u8>) -> Self {
225        Self { operation, data }
226    }
227}
228
229/// Data for CreateValidator operation
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231pub struct CreateValidatorData {
232    /// Initial stake amount
233    pub stake: Balance,
234    /// Commission rate in basis points
235    pub commission_bps: u16,
236    /// Optional metadata (name, website, etc.)
237    pub metadata: Vec<u8>,
238}
239
240/// Data for AddStake operation
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242pub struct AddStakeData {
243    /// Amount to add to stake
244    pub amount: Balance,
245}
246
247/// Data for Unstake operation
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
249pub struct UnstakeData {
250    /// Amount to unstake
251    pub amount: Balance,
252}
253
254/// Data for UpdateValidator operation
255#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
256pub struct UpdateValidatorData {
257    /// New commission rate (None = keep current)
258    pub commission_bps: Option<u16>,
259    /// New metadata (None = keep current)
260    pub metadata: Option<Vec<u8>>,
261}
262
263// ============================================================================
264// Delegation Types
265// ============================================================================
266
267/// Delegation information for a delegator to a validator
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
269pub struct DelegationInfo {
270    /// Delegator's address (32 bytes)
271    pub delegator: [u8; 32],
272    /// Validator's public key (32 bytes)
273    pub validator_pubkey: [u8; 32],
274    /// Delegated amount
275    pub amount: Balance,
276    /// Accumulated rewards (not yet claimed)
277    pub pending_rewards: Balance,
278    /// Block height when delegation started
279    pub delegated_at: BlockHeight,
280}
281
282impl DelegationInfo {
283    /// Create a new delegation
284    pub fn new(delegator: [u8; 32], validator_pubkey: [u8; 32], amount: Balance, delegated_at: BlockHeight) -> Self {
285        Self {
286            delegator,
287            validator_pubkey,
288            amount,
289            pending_rewards: 0,
290            delegated_at,
291        }
292    }
293
294    /// Add more stake to the delegation
295    pub fn add_stake(&mut self, amount: Balance) {
296        self.amount = self.amount.saturating_add(amount);
297    }
298
299    /// Remove stake from the delegation
300    pub fn remove_stake(&mut self, amount: Balance) -> Balance {
301        let removed = amount.min(self.amount);
302        self.amount = self.amount.saturating_sub(removed);
303        removed
304    }
305
306    /// Add rewards to pending rewards
307    pub fn add_rewards(&mut self, rewards: Balance) {
308        self.pending_rewards = self.pending_rewards.saturating_add(rewards);
309    }
310
311    /// Claim all pending rewards, returning the amount claimed
312    pub fn claim_rewards(&mut self) -> Balance {
313        let rewards = self.pending_rewards;
314        self.pending_rewards = 0;
315        rewards
316    }
317
318    /// Apply a slash penalty to the delegation
319    pub fn apply_slash(&mut self, penalty_bps: u16) {
320        let penalty = (self.amount * penalty_bps as u128) / 10000;
321        self.amount = self.amount.saturating_sub(penalty);
322    }
323}
324
325/// Unbonding delegation entry (pending withdrawal)
326#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
327pub struct UnbondingDelegation {
328    /// Delegator's address (32 bytes)
329    pub delegator: [u8; 32],
330    /// Validator's public key (32 bytes)
331    pub validator_pubkey: [u8; 32],
332    /// Amount being unbonded
333    pub amount: Balance,
334    /// Block height when unbonding completes
335    pub completion_height: BlockHeight,
336}
337
338impl UnbondingDelegation {
339    /// Create a new unbonding delegation
340    pub fn new(
341        delegator: [u8; 32],
342        validator_pubkey: [u8; 32],
343        amount: Balance,
344        completion_height: BlockHeight,
345    ) -> Self {
346        Self {
347            delegator,
348            validator_pubkey,
349            amount,
350            completion_height,
351        }
352    }
353
354    /// Check if unbonding is complete
355    pub fn is_complete(&self, current_height: BlockHeight) -> bool {
356        current_height >= self.completion_height
357    }
358}
359
360/// Data for Delegate operation
361#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
362pub struct DelegateData {
363    /// Validator public key to delegate to (hex)
364    pub validator_pubkey: [u8; 32],
365    /// Amount to delegate
366    pub amount: Balance,
367}
368
369/// Data for Undelegate operation
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
371pub struct UndelegateData {
372    /// Validator public key to undelegate from
373    pub validator_pubkey: [u8; 32],
374    /// Amount to undelegate
375    pub amount: Balance,
376}
377
378/// Data for ClaimDelegationRewards operation
379#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
380pub struct ClaimDelegationRewardsData {
381    /// Validator public key to claim rewards from
382    pub validator_pubkey: [u8; 32],
383}
384
385/// Data for WithdrawUnbonded operation
386#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
387pub struct WithdrawUnbondedData {
388    /// Validator public key (optional - if None, withdraw all)
389    pub validator_pubkey: Option<[u8; 32]>,
390}
391
392// ============================================================================
393// Slashing Types
394// ============================================================================
395
396/// Type of slashing evidence
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
398#[repr(u8)]
399pub enum EvidenceType {
400    /// Validator signed two different blocks at the same height
401    DoubleSign = 0,
402    /// Validator was offline/missed too many blocks
403    Downtime = 1,
404}
405
406impl EvidenceType {
407    /// Convert from byte
408    pub fn from_byte(b: u8) -> Option<Self> {
409        match b {
410            0 => Some(EvidenceType::DoubleSign),
411            1 => Some(EvidenceType::Downtime),
412            _ => None,
413        }
414    }
415
416    /// Get the name of this evidence type
417    pub fn name(&self) -> &'static str {
418        match self {
419            EvidenceType::DoubleSign => "double_sign",
420            EvidenceType::Downtime => "downtime",
421        }
422    }
423}
424
425/// Evidence of double signing (equivocation)
426#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
427pub struct DoubleSignEvidence {
428    /// Validator's public key
429    pub validator_pubkey: [u8; 32],
430    /// Block height where double sign occurred
431    pub height: BlockHeight,
432    /// First block hash signed
433    pub block_hash_1: [u8; 32],
434    /// Signature for first block
435    #[serde(with = "BigArray")]
436    pub signature_1: [u8; 64],
437    /// Second block hash signed (different from first)
438    pub block_hash_2: [u8; 32],
439    /// Signature for second block
440    #[serde(with = "BigArray")]
441    pub signature_2: [u8; 64],
442    /// Block height when evidence was submitted
443    pub submitted_at: BlockHeight,
444}
445
446impl DoubleSignEvidence {
447    /// Create new double sign evidence
448    pub fn new(
449        validator_pubkey: [u8; 32],
450        height: BlockHeight,
451        block_hash_1: [u8; 32],
452        signature_1: [u8; 64],
453        block_hash_2: [u8; 32],
454        signature_2: [u8; 64],
455        submitted_at: BlockHeight,
456    ) -> Self {
457        Self {
458            validator_pubkey,
459            height,
460            block_hash_1,
461            signature_1,
462            block_hash_2,
463            signature_2,
464            submitted_at,
465        }
466    }
467
468    /// Check if evidence is valid (basic checks, not cryptographic verification)
469    pub fn is_valid(&self) -> bool {
470        // Block hashes must be different
471        self.block_hash_1 != self.block_hash_2
472    }
473}
474
475/// Evidence of downtime (missed blocks)
476#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
477pub struct DowntimeEvidence {
478    /// Validator's public key
479    pub validator_pubkey: [u8; 32],
480    /// Starting height of the downtime window
481    pub start_height: BlockHeight,
482    /// Ending height of the downtime window
483    pub end_height: BlockHeight,
484    /// Number of blocks missed in this window
485    pub missed_blocks: u64,
486    /// Block height when evidence was submitted
487    pub submitted_at: BlockHeight,
488}
489
490impl DowntimeEvidence {
491    /// Create new downtime evidence
492    pub fn new(
493        validator_pubkey: [u8; 32],
494        start_height: BlockHeight,
495        end_height: BlockHeight,
496        missed_blocks: u64,
497        submitted_at: BlockHeight,
498    ) -> Self {
499        Self {
500            validator_pubkey,
501            start_height,
502            end_height,
503            missed_blocks,
504            submitted_at,
505        }
506    }
507
508    /// Check if evidence meets threshold
509    pub fn exceeds_threshold(&self, threshold: u64) -> bool {
510        self.missed_blocks >= threshold
511    }
512}
513
514/// Validator signing info for tracking liveness
515#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
516pub struct ValidatorSigningInfo {
517    /// Validator's public key
518    pub validator_pubkey: [u8; 32],
519    /// Block height when signing info was last updated
520    pub start_height: BlockHeight,
521    /// Index offset into signed blocks bit array
522    pub index_offset: u64,
523    /// Number of blocks missed in the current window
524    pub missed_blocks_counter: u64,
525    /// Whether the validator has been tombstoned (permanently jailed)
526    pub tombstoned: bool,
527    /// Block height of the last time validator was jailed
528    pub jailed_until: BlockHeight,
529}
530
531impl ValidatorSigningInfo {
532    /// Create new signing info for a validator
533    pub fn new(validator_pubkey: [u8; 32], start_height: BlockHeight) -> Self {
534        Self {
535            validator_pubkey,
536            start_height,
537            index_offset: 0,
538            missed_blocks_counter: 0,
539            tombstoned: false,
540            jailed_until: 0,
541        }
542    }
543
544    /// Increment missed blocks counter
545    pub fn increment_missed(&mut self) {
546        self.missed_blocks_counter = self.missed_blocks_counter.saturating_add(1);
547    }
548
549    /// Reset missed blocks counter (e.g., when validator is slashed)
550    pub fn reset_missed(&mut self) {
551        self.missed_blocks_counter = 0;
552    }
553
554    /// Check if validator has exceeded downtime threshold
555    pub fn exceeds_threshold(&self, threshold: u64) -> bool {
556        self.missed_blocks_counter >= threshold
557    }
558
559    /// Tombstone the validator (permanent jail)
560    pub fn tombstone(&mut self) {
561        self.tombstoned = true;
562    }
563}
564
565/// Record of a slashing event
566#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
567pub struct SlashingRecord {
568    /// Validator's public key
569    pub validator_pubkey: [u8; 32],
570    /// Type of evidence that caused the slash
571    pub evidence_type: EvidenceType,
572    /// Block height when slash was applied
573    pub slashed_at: BlockHeight,
574    /// Amount of validator's self-stake slashed
575    pub validator_slash_amount: Balance,
576    /// Amount of delegator stake slashed
577    pub delegation_slash_amount: Balance,
578    /// Jail until block height (0 if not jailed)
579    pub jailed_until: BlockHeight,
580    /// Whether this was a tombstone (permanent jail)
581    pub tombstoned: bool,
582    /// Fraction slashed (basis points)
583    pub slash_fraction_bps: u16,
584}
585
586impl SlashingRecord {
587    /// Create a new slashing record
588    pub fn new(
589        validator_pubkey: [u8; 32],
590        evidence_type: EvidenceType,
591        slashed_at: BlockHeight,
592        validator_slash_amount: Balance,
593        delegation_slash_amount: Balance,
594        jailed_until: BlockHeight,
595        tombstoned: bool,
596        slash_fraction_bps: u16,
597    ) -> Self {
598        Self {
599            validator_pubkey,
600            evidence_type,
601            slashed_at,
602            validator_slash_amount,
603            delegation_slash_amount,
604            jailed_until,
605            tombstoned,
606            slash_fraction_bps,
607        }
608    }
609
610    /// Get total slashed amount
611    pub fn total_slashed(&self) -> Balance {
612        self.validator_slash_amount.saturating_add(self.delegation_slash_amount)
613    }
614}
615
616/// Data for SubmitEvidence operation
617#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
618pub struct SubmitEvidenceData {
619    /// Type of evidence
620    pub evidence_type: EvidenceType,
621    /// Serialized evidence data
622    pub evidence: Vec<u8>,
623}
624
625/// Staking parameters from genesis
626#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
627pub struct StakingParams {
628    /// Minimum stake required to be a validator
629    pub min_validator_stake: Balance,
630    /// Maximum number of active validators
631    pub max_validators: u32,
632    /// Unbonding period in blocks
633    pub unbonding_period: BlockHeight,
634    /// Maximum commission rate in basis points
635    pub max_commission_bps: u16,
636    /// Slash penalty for double signing (basis points)
637    pub double_sign_slash_bps: u16,
638    /// Slash penalty for downtime (basis points)
639    pub downtime_slash_bps: u16,
640    /// Jail duration for double signing (blocks)
641    pub double_sign_jail_duration: BlockHeight,
642    /// Jail duration for downtime (blocks)
643    pub downtime_jail_duration: BlockHeight,
644    /// Number of missed blocks before downtime slash
645    pub downtime_threshold: u64,
646    /// Epoch length in blocks (validator set updates at epoch boundaries)
647    pub epoch_length: BlockHeight,
648    /// Enable stake-weighted proposer selection (vs round-robin)
649    pub stake_weighted_selection: bool,
650}
651
652impl Default for StakingParams {
653    fn default() -> Self {
654        Self {
655            min_validator_stake: 1_000_000_000_000_000_000, // 1e18 base units = 1,000,000,000 Koppa (1B Koppa with 9 decimals)
656            max_validators: 100,
657            unbonding_period: 100_800, // ~7 days at 6s blocks
658            max_commission_bps: 10000, // 100%
659            double_sign_slash_bps: 500, // 5%
660            downtime_slash_bps: 10, // 0.1%
661            double_sign_jail_duration: 14400, // ~24 hours
662            downtime_jail_duration: 2400, // ~4 hours
663            downtime_threshold: 500, // 500 missed blocks
664            epoch_length: 14400, // ~24 hours at 6s blocks
665            stake_weighted_selection: true, // Use stake-weighted selection by default
666        }
667    }
668}
669
670impl StakingParams {
671    /// Get the epoch number for a given block height
672    pub fn epoch_for_height(&self, height: BlockHeight) -> u64 {
673        if self.epoch_length == 0 {
674            return 0;
675        }
676        height / self.epoch_length
677    }
678
679    /// Check if a block height is at an epoch boundary
680    pub fn is_epoch_boundary(&self, height: BlockHeight) -> bool {
681        if self.epoch_length == 0 {
682            return false;
683        }
684        height > 0 && height % self.epoch_length == 0
685    }
686
687    /// Get the first block height of an epoch
688    pub fn epoch_start_height(&self, epoch: u64) -> BlockHeight {
689        epoch * self.epoch_length
690    }
691
692    /// Get the last block height of an epoch
693    pub fn epoch_end_height(&self, epoch: u64) -> BlockHeight {
694        (epoch + 1) * self.epoch_length - 1
695    }
696}
697
698// ============================================================================
699// Validator Set Types
700// ============================================================================
701
702/// A validator in the active set with voting power
703#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
704pub struct ValidatorSetEntry {
705    /// Validator's public key
706    pub pubkey: [u8; 32],
707    /// Total voting power (stake + delegations)
708    pub voting_power: Balance,
709    /// Commission rate in basis points
710    pub commission_bps: u16,
711}
712
713impl ValidatorSetEntry {
714    /// Create a new validator set entry
715    pub fn new(pubkey: [u8; 32], voting_power: Balance, commission_bps: u16) -> Self {
716        Self {
717            pubkey,
718            voting_power,
719            commission_bps,
720        }
721    }
722}
723
724/// The active validator set for an epoch
725#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
726pub struct ValidatorSet {
727    /// The epoch this validator set is for
728    pub epoch: u64,
729    /// Block height when this set became active
730    pub active_from: BlockHeight,
731    /// List of validators sorted by voting power (descending)
732    pub validators: Vec<ValidatorSetEntry>,
733    /// Total voting power in this set
734    pub total_voting_power: Balance,
735    /// Proposer selection seed (hash of previous epoch's last block)
736    pub proposer_seed: [u8; 32],
737}
738
739impl ValidatorSet {
740    /// Create a new validator set
741    pub fn new(epoch: u64, active_from: BlockHeight, validators: Vec<ValidatorSetEntry>, proposer_seed: [u8; 32]) -> Self {
742        let total_voting_power = validators.iter().map(|v| v.voting_power).sum();
743        Self {
744            epoch,
745            active_from,
746            validators,
747            total_voting_power,
748            proposer_seed,
749        }
750    }
751
752    /// Get the number of validators
753    pub fn len(&self) -> usize {
754        self.validators.len()
755    }
756
757    /// Check if the set is empty
758    pub fn is_empty(&self) -> bool {
759        self.validators.is_empty()
760    }
761
762    /// Get validator pubkeys
763    pub fn pubkeys(&self) -> Vec<[u8; 32]> {
764        self.validators.iter().map(|v| v.pubkey).collect()
765    }
766
767    /// Check if a pubkey is in the validator set
768    pub fn contains(&self, pubkey: &[u8; 32]) -> bool {
769        self.validators.iter().any(|v| &v.pubkey == pubkey)
770    }
771
772    /// Get a validator's entry by pubkey
773    pub fn get(&self, pubkey: &[u8; 32]) -> Option<&ValidatorSetEntry> {
774        self.validators.iter().find(|v| &v.pubkey == pubkey)
775    }
776
777    /// Get the proposer for a given height using stake-weighted selection
778    pub fn get_stake_weighted_proposer(&self, height: BlockHeight) -> Option<[u8; 32]> {
779        if self.validators.is_empty() || self.total_voting_power == 0 {
780            return None;
781        }
782
783        // Combine proposer seed with height for deterministic but varying selection
784        let mut seed_input = [0u8; 40];
785        seed_input[..32].copy_from_slice(&self.proposer_seed);
786        seed_input[32..40].copy_from_slice(&height.to_le_bytes());
787
788        // Simple hash to get a selection point
789        let hash = blake3::hash(&seed_input);
790        let hash_bytes = hash.as_bytes();
791
792        // Convert first 16 bytes to u128 for selection
793        let selection_bytes: [u8; 16] = hash_bytes[..16].try_into().unwrap();
794        let selection_value = u128::from_le_bytes(selection_bytes);
795
796        // Map to range [0, total_voting_power)
797        let selection_point = selection_value % (self.total_voting_power as u128);
798
799        // Select proposer based on cumulative voting power
800        let mut cumulative = 0u128;
801        for validator in &self.validators {
802            cumulative += validator.voting_power as u128;
803            if selection_point < cumulative {
804                return Some(validator.pubkey);
805            }
806        }
807
808        // Fallback to first validator (shouldn't happen)
809        Some(self.validators[0].pubkey)
810    }
811
812    /// Get the proposer for a given height using round-robin selection
813    pub fn get_round_robin_proposer(&self, height: BlockHeight) -> Option<[u8; 32]> {
814        if self.validators.is_empty() {
815            return None;
816        }
817        let idx = (height as usize) % self.validators.len();
818        Some(self.validators[idx].pubkey)
819    }
820
821    /// Get the voting power for a validator
822    pub fn voting_power(&self, pubkey: &[u8; 32]) -> Balance {
823        self.get(pubkey).map(|v| v.voting_power).unwrap_or(0)
824    }
825
826    /// Calculate the voting power percentage for a validator (in basis points)
827    pub fn voting_power_percentage(&self, pubkey: &[u8; 32]) -> u16 {
828        if self.total_voting_power == 0 {
829            return 0;
830        }
831        let power = self.voting_power(pubkey);
832        ((power * 10000) / self.total_voting_power) as u16
833    }
834}
835
836#[cfg(test)]
837mod tests {
838    use super::*;
839
840    #[test]
841    fn test_validator_status_from_byte() {
842        assert_eq!(ValidatorStatus::from_byte(0), Some(ValidatorStatus::Active));
843        assert_eq!(ValidatorStatus::from_byte(1), Some(ValidatorStatus::Inactive));
844        assert_eq!(ValidatorStatus::from_byte(2), Some(ValidatorStatus::Jailed));
845        assert_eq!(ValidatorStatus::from_byte(3), Some(ValidatorStatus::Unbonding));
846        assert_eq!(ValidatorStatus::from_byte(99), None);
847    }
848
849    #[test]
850    fn test_validator_info_creation() {
851        let pubkey = [1u8; 32];
852        let validator = ValidatorInfo::new(pubkey, 1000, 500, 100);
853
854        assert_eq!(validator.stake, 1000);
855        assert_eq!(validator.commission_bps, 500);
856        assert_eq!(validator.status, ValidatorStatus::Active);
857        assert!(!validator.is_jailed());
858    }
859
860    #[test]
861    fn test_validator_slash() {
862        let pubkey = [1u8; 32];
863        let mut validator = ValidatorInfo::new(pubkey, 10000, 500, 100);
864
865        validator.apply_slash(500); // 5% slash
866
867        assert_eq!(validator.stake, 9500);
868        assert_eq!(validator.slash_count, 1);
869    }
870
871    #[test]
872    fn test_validator_jail_unjail() {
873        let pubkey = [1u8; 32];
874        let mut validator = ValidatorInfo::new(pubkey, 1000, 500, 100);
875
876        validator.jail(200);
877        assert!(validator.is_jailed());
878        assert!(!validator.can_unjail(150));
879        assert!(validator.can_unjail(200));
880
881        validator.unjail();
882        assert!(!validator.is_jailed());
883        assert_eq!(validator.status, ValidatorStatus::Active);
884    }
885
886    #[test]
887    fn test_staking_operation_from_byte() {
888        assert_eq!(StakingOperation::from_byte(0), Some(StakingOperation::CreateValidator));
889        assert_eq!(StakingOperation::from_byte(4), Some(StakingOperation::Unjail));
890        assert_eq!(StakingOperation::from_byte(99), None);
891    }
892
893    #[test]
894    fn test_commission_cap() {
895        let pubkey = [1u8; 32];
896        let validator = ValidatorInfo::new(pubkey, 1000, 15000, 100); // Try 150%
897
898        assert_eq!(validator.commission_bps, 10000); // Capped at 100%
899    }
900
901    // ========================================================================
902    // Delegation Tests
903    // ========================================================================
904
905    #[test]
906    fn test_validator_delegation() {
907        let pubkey = [1u8; 32];
908        let mut validator = ValidatorInfo::new(pubkey, 1000, 500, 100);
909
910        assert_eq!(validator.total_stake(), 1000);
911        assert_eq!(validator.total_delegated, 0);
912
913        validator.add_delegation(500);
914        assert_eq!(validator.total_delegated, 500);
915        assert_eq!(validator.total_stake(), 1500);
916
917        validator.remove_delegation(200);
918        assert_eq!(validator.total_delegated, 300);
919        assert_eq!(validator.total_stake(), 1300);
920    }
921
922    #[test]
923    fn test_delegation_info_creation() {
924        let delegator = [2u8; 32];
925        let validator_pubkey = [1u8; 32];
926        let delegation = DelegationInfo::new(delegator, validator_pubkey, 1000, 100);
927
928        assert_eq!(delegation.delegator, delegator);
929        assert_eq!(delegation.validator_pubkey, validator_pubkey);
930        assert_eq!(delegation.amount, 1000);
931        assert_eq!(delegation.pending_rewards, 0);
932        assert_eq!(delegation.delegated_at, 100);
933    }
934
935    #[test]
936    fn test_delegation_stake_operations() {
937        let delegator = [2u8; 32];
938        let validator_pubkey = [1u8; 32];
939        let mut delegation = DelegationInfo::new(delegator, validator_pubkey, 1000, 100);
940
941        delegation.add_stake(500);
942        assert_eq!(delegation.amount, 1500);
943
944        let removed = delegation.remove_stake(700);
945        assert_eq!(removed, 700);
946        assert_eq!(delegation.amount, 800);
947
948        // Try to remove more than available
949        let removed = delegation.remove_stake(1000);
950        assert_eq!(removed, 800);
951        assert_eq!(delegation.amount, 0);
952    }
953
954    #[test]
955    fn test_delegation_rewards() {
956        let delegator = [2u8; 32];
957        let validator_pubkey = [1u8; 32];
958        let mut delegation = DelegationInfo::new(delegator, validator_pubkey, 1000, 100);
959
960        delegation.add_rewards(100);
961        assert_eq!(delegation.pending_rewards, 100);
962
963        delegation.add_rewards(50);
964        assert_eq!(delegation.pending_rewards, 150);
965
966        let claimed = delegation.claim_rewards();
967        assert_eq!(claimed, 150);
968        assert_eq!(delegation.pending_rewards, 0);
969    }
970
971    #[test]
972    fn test_delegation_slash() {
973        let delegator = [2u8; 32];
974        let validator_pubkey = [1u8; 32];
975        let mut delegation = DelegationInfo::new(delegator, validator_pubkey, 10000, 100);
976
977        delegation.apply_slash(500); // 5% slash
978        assert_eq!(delegation.amount, 9500);
979    }
980
981    #[test]
982    fn test_unbonding_delegation() {
983        let delegator = [2u8; 32];
984        let validator_pubkey = [1u8; 32];
985        let unbonding = UnbondingDelegation::new(delegator, validator_pubkey, 500, 200);
986
987        assert!(!unbonding.is_complete(100));
988        assert!(!unbonding.is_complete(199));
989        assert!(unbonding.is_complete(200));
990        assert!(unbonding.is_complete(300));
991    }
992
993    #[test]
994    fn test_delegation_operations() {
995        assert_eq!(StakingOperation::from_byte(6), Some(StakingOperation::Delegate));
996        assert_eq!(StakingOperation::from_byte(7), Some(StakingOperation::Undelegate));
997        assert_eq!(StakingOperation::from_byte(8), Some(StakingOperation::ClaimDelegationRewards));
998        assert_eq!(StakingOperation::from_byte(9), Some(StakingOperation::WithdrawUnbonded));
999
1000        assert!(StakingOperation::Delegate.is_delegation());
1001        assert!(StakingOperation::Undelegate.is_delegation());
1002        assert!(!StakingOperation::CreateValidator.is_delegation());
1003    }
1004}