use serde::{Deserialize, Serialize};
use serde_big_array::BigArray;
use crate::{Balance, BlockHeight};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum ValidatorStatus {
Active = 0,
Inactive = 1,
Jailed = 2,
Unbonding = 3,
}
impl ValidatorStatus {
pub fn from_byte(b: u8) -> Option<Self> {
match b {
0 => Some(ValidatorStatus::Active),
1 => Some(ValidatorStatus::Inactive),
2 => Some(ValidatorStatus::Jailed),
3 => Some(ValidatorStatus::Unbonding),
_ => None,
}
}
pub fn can_validate(&self) -> bool {
matches!(self, ValidatorStatus::Active)
}
}
impl Default for ValidatorStatus {
fn default() -> Self {
ValidatorStatus::Inactive
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValidatorInfo {
pub pubkey: [u8; 32],
pub stake: Balance,
pub total_delegated: Balance,
pub commission_bps: u16,
pub status: ValidatorStatus,
pub joined_at: BlockHeight,
pub jailed_until: BlockHeight,
pub slash_count: u32,
pub pending_rewards: Balance,
pub metadata: Vec<u8>,
}
impl ValidatorInfo {
pub fn new(pubkey: [u8; 32], stake: Balance, commission_bps: u16, joined_at: BlockHeight) -> Self {
Self {
pubkey,
stake,
total_delegated: 0,
commission_bps: commission_bps.min(10000), status: ValidatorStatus::Active,
joined_at,
jailed_until: 0,
slash_count: 0,
pending_rewards: 0,
metadata: Vec::new(),
}
}
pub fn total_stake(&self) -> Balance {
self.stake.saturating_add(self.total_delegated)
}
pub fn add_delegation(&mut self, amount: Balance) {
self.total_delegated = self.total_delegated.saturating_add(amount);
}
pub fn remove_delegation(&mut self, amount: Balance) {
self.total_delegated = self.total_delegated.saturating_sub(amount);
}
pub fn is_jailed(&self) -> bool {
self.status == ValidatorStatus::Jailed
}
pub fn can_unjail(&self, current_height: BlockHeight) -> bool {
self.is_jailed() && current_height >= self.jailed_until
}
pub fn apply_slash(&mut self, penalty_bps: u16) {
let penalty = (self.stake * penalty_bps as u128) / 10000;
self.stake = self.stake.saturating_sub(penalty);
self.slash_count += 1;
}
pub fn jail(&mut self, until_height: BlockHeight) {
self.status = ValidatorStatus::Jailed;
self.jailed_until = until_height;
}
pub fn unjail(&mut self) {
self.status = ValidatorStatus::Active;
self.jailed_until = 0;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum StakingOperation {
CreateValidator = 0,
AddStake = 1,
Unstake = 2,
UpdateValidator = 3,
Unjail = 4,
ClaimRewards = 5,
Delegate = 6,
Undelegate = 7,
ClaimDelegationRewards = 8,
WithdrawUnbonded = 9,
SubmitEvidence = 10,
}
impl StakingOperation {
pub fn from_byte(b: u8) -> Option<Self> {
match b {
0 => Some(StakingOperation::CreateValidator),
1 => Some(StakingOperation::AddStake),
2 => Some(StakingOperation::Unstake),
3 => Some(StakingOperation::UpdateValidator),
4 => Some(StakingOperation::Unjail),
5 => Some(StakingOperation::ClaimRewards),
6 => Some(StakingOperation::Delegate),
7 => Some(StakingOperation::Undelegate),
8 => Some(StakingOperation::ClaimDelegationRewards),
9 => Some(StakingOperation::WithdrawUnbonded),
10 => Some(StakingOperation::SubmitEvidence),
_ => None,
}
}
pub fn requires_validator(&self) -> bool {
matches!(
self,
StakingOperation::AddStake
| StakingOperation::Unstake
| StakingOperation::UpdateValidator
| StakingOperation::Unjail
| StakingOperation::ClaimRewards
)
}
pub fn is_delegation(&self) -> bool {
matches!(
self,
StakingOperation::Delegate
| StakingOperation::Undelegate
| StakingOperation::ClaimDelegationRewards
| StakingOperation::WithdrawUnbonded
)
}
pub fn is_slashing(&self) -> bool {
matches!(self, StakingOperation::SubmitEvidence)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StakingTxData {
pub operation: StakingOperation,
pub data: Vec<u8>,
}
impl StakingTxData {
pub fn new(operation: StakingOperation, data: Vec<u8>) -> Self {
Self { operation, data }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreateValidatorData {
pub stake: Balance,
pub commission_bps: u16,
pub metadata: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AddStakeData {
pub amount: Balance,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnstakeData {
pub amount: Balance,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpdateValidatorData {
pub commission_bps: Option<u16>,
pub metadata: Option<Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DelegationInfo {
pub delegator: [u8; 32],
pub validator_pubkey: [u8; 32],
pub amount: Balance,
pub pending_rewards: Balance,
pub delegated_at: BlockHeight,
}
impl DelegationInfo {
pub fn new(delegator: [u8; 32], validator_pubkey: [u8; 32], amount: Balance, delegated_at: BlockHeight) -> Self {
Self {
delegator,
validator_pubkey,
amount,
pending_rewards: 0,
delegated_at,
}
}
pub fn add_stake(&mut self, amount: Balance) {
self.amount = self.amount.saturating_add(amount);
}
pub fn remove_stake(&mut self, amount: Balance) -> Balance {
let removed = amount.min(self.amount);
self.amount = self.amount.saturating_sub(removed);
removed
}
pub fn add_rewards(&mut self, rewards: Balance) {
self.pending_rewards = self.pending_rewards.saturating_add(rewards);
}
pub fn claim_rewards(&mut self) -> Balance {
let rewards = self.pending_rewards;
self.pending_rewards = 0;
rewards
}
pub fn apply_slash(&mut self, penalty_bps: u16) {
let penalty = (self.amount * penalty_bps as u128) / 10000;
self.amount = self.amount.saturating_sub(penalty);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnbondingDelegation {
pub delegator: [u8; 32],
pub validator_pubkey: [u8; 32],
pub amount: Balance,
pub completion_height: BlockHeight,
}
impl UnbondingDelegation {
pub fn new(
delegator: [u8; 32],
validator_pubkey: [u8; 32],
amount: Balance,
completion_height: BlockHeight,
) -> Self {
Self {
delegator,
validator_pubkey,
amount,
completion_height,
}
}
pub fn is_complete(&self, current_height: BlockHeight) -> bool {
current_height >= self.completion_height
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DelegateData {
pub validator_pubkey: [u8; 32],
pub amount: Balance,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UndelegateData {
pub validator_pubkey: [u8; 32],
pub amount: Balance,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClaimDelegationRewardsData {
pub validator_pubkey: [u8; 32],
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WithdrawUnbondedData {
pub validator_pubkey: Option<[u8; 32]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum EvidenceType {
DoubleSign = 0,
Downtime = 1,
}
impl EvidenceType {
pub fn from_byte(b: u8) -> Option<Self> {
match b {
0 => Some(EvidenceType::DoubleSign),
1 => Some(EvidenceType::Downtime),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
EvidenceType::DoubleSign => "double_sign",
EvidenceType::Downtime => "downtime",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DoubleSignEvidence {
pub validator_pubkey: [u8; 32],
pub height: BlockHeight,
pub block_hash_1: [u8; 32],
#[serde(with = "BigArray")]
pub signature_1: [u8; 64],
pub block_hash_2: [u8; 32],
#[serde(with = "BigArray")]
pub signature_2: [u8; 64],
pub submitted_at: BlockHeight,
}
impl DoubleSignEvidence {
pub fn new(
validator_pubkey: [u8; 32],
height: BlockHeight,
block_hash_1: [u8; 32],
signature_1: [u8; 64],
block_hash_2: [u8; 32],
signature_2: [u8; 64],
submitted_at: BlockHeight,
) -> Self {
Self {
validator_pubkey,
height,
block_hash_1,
signature_1,
block_hash_2,
signature_2,
submitted_at,
}
}
pub fn is_valid(&self) -> bool {
self.block_hash_1 != self.block_hash_2
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DowntimeEvidence {
pub validator_pubkey: [u8; 32],
pub start_height: BlockHeight,
pub end_height: BlockHeight,
pub missed_blocks: u64,
pub submitted_at: BlockHeight,
}
impl DowntimeEvidence {
pub fn new(
validator_pubkey: [u8; 32],
start_height: BlockHeight,
end_height: BlockHeight,
missed_blocks: u64,
submitted_at: BlockHeight,
) -> Self {
Self {
validator_pubkey,
start_height,
end_height,
missed_blocks,
submitted_at,
}
}
pub fn exceeds_threshold(&self, threshold: u64) -> bool {
self.missed_blocks >= threshold
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValidatorSigningInfo {
pub validator_pubkey: [u8; 32],
pub start_height: BlockHeight,
pub index_offset: u64,
pub missed_blocks_counter: u64,
pub tombstoned: bool,
pub jailed_until: BlockHeight,
}
impl ValidatorSigningInfo {
pub fn new(validator_pubkey: [u8; 32], start_height: BlockHeight) -> Self {
Self {
validator_pubkey,
start_height,
index_offset: 0,
missed_blocks_counter: 0,
tombstoned: false,
jailed_until: 0,
}
}
pub fn increment_missed(&mut self) {
self.missed_blocks_counter = self.missed_blocks_counter.saturating_add(1);
}
pub fn reset_missed(&mut self) {
self.missed_blocks_counter = 0;
}
pub fn exceeds_threshold(&self, threshold: u64) -> bool {
self.missed_blocks_counter >= threshold
}
pub fn tombstone(&mut self) {
self.tombstoned = true;
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SlashingRecord {
pub validator_pubkey: [u8; 32],
pub evidence_type: EvidenceType,
pub slashed_at: BlockHeight,
pub validator_slash_amount: Balance,
pub delegation_slash_amount: Balance,
pub jailed_until: BlockHeight,
pub tombstoned: bool,
pub slash_fraction_bps: u16,
}
impl SlashingRecord {
pub fn new(
validator_pubkey: [u8; 32],
evidence_type: EvidenceType,
slashed_at: BlockHeight,
validator_slash_amount: Balance,
delegation_slash_amount: Balance,
jailed_until: BlockHeight,
tombstoned: bool,
slash_fraction_bps: u16,
) -> Self {
Self {
validator_pubkey,
evidence_type,
slashed_at,
validator_slash_amount,
delegation_slash_amount,
jailed_until,
tombstoned,
slash_fraction_bps,
}
}
pub fn total_slashed(&self) -> Balance {
self.validator_slash_amount.saturating_add(self.delegation_slash_amount)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SubmitEvidenceData {
pub evidence_type: EvidenceType,
pub evidence: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StakingParams {
pub min_validator_stake: Balance,
pub max_validators: u32,
pub unbonding_period: BlockHeight,
pub max_commission_bps: u16,
pub double_sign_slash_bps: u16,
pub downtime_slash_bps: u16,
pub double_sign_jail_duration: BlockHeight,
pub downtime_jail_duration: BlockHeight,
pub downtime_threshold: u64,
pub epoch_length: BlockHeight,
pub stake_weighted_selection: bool,
}
impl Default for StakingParams {
fn default() -> Self {
Self {
min_validator_stake: 1_000_000_000_000_000_000, max_validators: 100,
unbonding_period: 100_800, max_commission_bps: 10000, double_sign_slash_bps: 500, downtime_slash_bps: 10, double_sign_jail_duration: 14400, downtime_jail_duration: 2400, downtime_threshold: 500, epoch_length: 14400, stake_weighted_selection: true, }
}
}
impl StakingParams {
pub fn epoch_for_height(&self, height: BlockHeight) -> u64 {
if self.epoch_length == 0 {
return 0;
}
height / self.epoch_length
}
pub fn is_epoch_boundary(&self, height: BlockHeight) -> bool {
if self.epoch_length == 0 {
return false;
}
height > 0 && height % self.epoch_length == 0
}
pub fn epoch_start_height(&self, epoch: u64) -> BlockHeight {
epoch * self.epoch_length
}
pub fn epoch_end_height(&self, epoch: u64) -> BlockHeight {
(epoch + 1) * self.epoch_length - 1
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValidatorSetEntry {
pub pubkey: [u8; 32],
pub voting_power: Balance,
pub commission_bps: u16,
}
impl ValidatorSetEntry {
pub fn new(pubkey: [u8; 32], voting_power: Balance, commission_bps: u16) -> Self {
Self {
pubkey,
voting_power,
commission_bps,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValidatorSet {
pub epoch: u64,
pub active_from: BlockHeight,
pub validators: Vec<ValidatorSetEntry>,
pub total_voting_power: Balance,
pub proposer_seed: [u8; 32],
}
impl ValidatorSet {
pub fn new(epoch: u64, active_from: BlockHeight, validators: Vec<ValidatorSetEntry>, proposer_seed: [u8; 32]) -> Self {
let total_voting_power = validators.iter().map(|v| v.voting_power).sum();
Self {
epoch,
active_from,
validators,
total_voting_power,
proposer_seed,
}
}
pub fn len(&self) -> usize {
self.validators.len()
}
pub fn is_empty(&self) -> bool {
self.validators.is_empty()
}
pub fn pubkeys(&self) -> Vec<[u8; 32]> {
self.validators.iter().map(|v| v.pubkey).collect()
}
pub fn contains(&self, pubkey: &[u8; 32]) -> bool {
self.validators.iter().any(|v| &v.pubkey == pubkey)
}
pub fn get(&self, pubkey: &[u8; 32]) -> Option<&ValidatorSetEntry> {
self.validators.iter().find(|v| &v.pubkey == pubkey)
}
pub fn get_stake_weighted_proposer(&self, height: BlockHeight) -> Option<[u8; 32]> {
if self.validators.is_empty() || self.total_voting_power == 0 {
return None;
}
let mut seed_input = [0u8; 40];
seed_input[..32].copy_from_slice(&self.proposer_seed);
seed_input[32..40].copy_from_slice(&height.to_le_bytes());
let hash = blake3::hash(&seed_input);
let hash_bytes = hash.as_bytes();
let selection_bytes: [u8; 16] = hash_bytes[..16].try_into().unwrap();
let selection_value = u128::from_le_bytes(selection_bytes);
let selection_point = selection_value % (self.total_voting_power as u128);
let mut cumulative = 0u128;
for validator in &self.validators {
cumulative += validator.voting_power as u128;
if selection_point < cumulative {
return Some(validator.pubkey);
}
}
Some(self.validators[0].pubkey)
}
pub fn get_round_robin_proposer(&self, height: BlockHeight) -> Option<[u8; 32]> {
if self.validators.is_empty() {
return None;
}
let idx = (height as usize) % self.validators.len();
Some(self.validators[idx].pubkey)
}
pub fn voting_power(&self, pubkey: &[u8; 32]) -> Balance {
self.get(pubkey).map(|v| v.voting_power).unwrap_or(0)
}
pub fn voting_power_percentage(&self, pubkey: &[u8; 32]) -> u16 {
if self.total_voting_power == 0 {
return 0;
}
let power = self.voting_power(pubkey);
((power * 10000) / self.total_voting_power) as u16
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validator_status_from_byte() {
assert_eq!(ValidatorStatus::from_byte(0), Some(ValidatorStatus::Active));
assert_eq!(ValidatorStatus::from_byte(1), Some(ValidatorStatus::Inactive));
assert_eq!(ValidatorStatus::from_byte(2), Some(ValidatorStatus::Jailed));
assert_eq!(ValidatorStatus::from_byte(3), Some(ValidatorStatus::Unbonding));
assert_eq!(ValidatorStatus::from_byte(99), None);
}
#[test]
fn test_validator_info_creation() {
let pubkey = [1u8; 32];
let validator = ValidatorInfo::new(pubkey, 1000, 500, 100);
assert_eq!(validator.stake, 1000);
assert_eq!(validator.commission_bps, 500);
assert_eq!(validator.status, ValidatorStatus::Active);
assert!(!validator.is_jailed());
}
#[test]
fn test_validator_slash() {
let pubkey = [1u8; 32];
let mut validator = ValidatorInfo::new(pubkey, 10000, 500, 100);
validator.apply_slash(500);
assert_eq!(validator.stake, 9500);
assert_eq!(validator.slash_count, 1);
}
#[test]
fn test_validator_jail_unjail() {
let pubkey = [1u8; 32];
let mut validator = ValidatorInfo::new(pubkey, 1000, 500, 100);
validator.jail(200);
assert!(validator.is_jailed());
assert!(!validator.can_unjail(150));
assert!(validator.can_unjail(200));
validator.unjail();
assert!(!validator.is_jailed());
assert_eq!(validator.status, ValidatorStatus::Active);
}
#[test]
fn test_staking_operation_from_byte() {
assert_eq!(StakingOperation::from_byte(0), Some(StakingOperation::CreateValidator));
assert_eq!(StakingOperation::from_byte(4), Some(StakingOperation::Unjail));
assert_eq!(StakingOperation::from_byte(99), None);
}
#[test]
fn test_commission_cap() {
let pubkey = [1u8; 32];
let validator = ValidatorInfo::new(pubkey, 1000, 15000, 100);
assert_eq!(validator.commission_bps, 10000); }
#[test]
fn test_validator_delegation() {
let pubkey = [1u8; 32];
let mut validator = ValidatorInfo::new(pubkey, 1000, 500, 100);
assert_eq!(validator.total_stake(), 1000);
assert_eq!(validator.total_delegated, 0);
validator.add_delegation(500);
assert_eq!(validator.total_delegated, 500);
assert_eq!(validator.total_stake(), 1500);
validator.remove_delegation(200);
assert_eq!(validator.total_delegated, 300);
assert_eq!(validator.total_stake(), 1300);
}
#[test]
fn test_delegation_info_creation() {
let delegator = [2u8; 32];
let validator_pubkey = [1u8; 32];
let delegation = DelegationInfo::new(delegator, validator_pubkey, 1000, 100);
assert_eq!(delegation.delegator, delegator);
assert_eq!(delegation.validator_pubkey, validator_pubkey);
assert_eq!(delegation.amount, 1000);
assert_eq!(delegation.pending_rewards, 0);
assert_eq!(delegation.delegated_at, 100);
}
#[test]
fn test_delegation_stake_operations() {
let delegator = [2u8; 32];
let validator_pubkey = [1u8; 32];
let mut delegation = DelegationInfo::new(delegator, validator_pubkey, 1000, 100);
delegation.add_stake(500);
assert_eq!(delegation.amount, 1500);
let removed = delegation.remove_stake(700);
assert_eq!(removed, 700);
assert_eq!(delegation.amount, 800);
let removed = delegation.remove_stake(1000);
assert_eq!(removed, 800);
assert_eq!(delegation.amount, 0);
}
#[test]
fn test_delegation_rewards() {
let delegator = [2u8; 32];
let validator_pubkey = [1u8; 32];
let mut delegation = DelegationInfo::new(delegator, validator_pubkey, 1000, 100);
delegation.add_rewards(100);
assert_eq!(delegation.pending_rewards, 100);
delegation.add_rewards(50);
assert_eq!(delegation.pending_rewards, 150);
let claimed = delegation.claim_rewards();
assert_eq!(claimed, 150);
assert_eq!(delegation.pending_rewards, 0);
}
#[test]
fn test_delegation_slash() {
let delegator = [2u8; 32];
let validator_pubkey = [1u8; 32];
let mut delegation = DelegationInfo::new(delegator, validator_pubkey, 10000, 100);
delegation.apply_slash(500); assert_eq!(delegation.amount, 9500);
}
#[test]
fn test_unbonding_delegation() {
let delegator = [2u8; 32];
let validator_pubkey = [1u8; 32];
let unbonding = UnbondingDelegation::new(delegator, validator_pubkey, 500, 200);
assert!(!unbonding.is_complete(100));
assert!(!unbonding.is_complete(199));
assert!(unbonding.is_complete(200));
assert!(unbonding.is_complete(300));
}
#[test]
fn test_delegation_operations() {
assert_eq!(StakingOperation::from_byte(6), Some(StakingOperation::Delegate));
assert_eq!(StakingOperation::from_byte(7), Some(StakingOperation::Undelegate));
assert_eq!(StakingOperation::from_byte(8), Some(StakingOperation::ClaimDelegationRewards));
assert_eq!(StakingOperation::from_byte(9), Some(StakingOperation::WithdrawUnbonded));
assert!(StakingOperation::Delegate.is_delegation());
assert!(StakingOperation::Undelegate.is_delegation());
assert!(!StakingOperation::CreateValidator.is_delegation());
}
}