staking.rs
1 // Copyright (c) 2025 ADnet Contributors 2 // SPDX-License-Identifier: Apache-2.0 3 4 //! DX Staking System (F-G16) 5 //! 6 //! Implements DX staking for dividend eligibility and governance participation. 7 //! Staking is **default ON** (opt-out model) - all wallets are staked by default. 8 //! 9 //! # Key Properties 10 //! 11 //! - Default state: `staking_enabled = true` (opt-out model) 12 //! - Staked DX cannot be transferred (must unstake first) 13 //! - Received DX inherits recipient wallet's staking state 14 //! - Unstaking takes effect at epoch transition (daily) 15 //! - Only staked DX earns dividends 16 //! 17 //! # Epoch Transition Priority 18 //! 19 //! 1. SLASHING (always first - no escape from governance penalties) 20 //! 2. DIVIDENDS (calculated on current staked balances before changes) 21 //! 3. STAKING (pending stake requests take effect) 22 //! 4. UNSTAKING (pending unstake requests take effect last) 23 24 use crate::TokenCommitment; 25 use anyhow::{ensure, Result}; 26 use serde::{Deserialize, Serialize}; 27 use sha2::{Digest, Sha256}; 28 use std::collections::HashMap; 29 30 // ============================================================================ 31 // Constants 32 // ============================================================================ 33 34 /// Default staking state for new wallets (true = opt-out model) 35 pub const DEFAULT_STAKING_STATE: bool = true; 36 37 /// Minimum DX required for governance eligibility (can only be decreased via governance) 38 pub const GOVERNANCE_THRESHOLD_DX: u64 = 10_000; 39 40 // ============================================================================ 41 // Staking State 42 // ============================================================================ 43 44 /// Staking state for a single wallet 45 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 46 pub struct StakingState { 47 /// Whether staking is enabled (default: true) 48 pub is_staking: bool, 49 /// Epoch when unstake was requested (None if not unstaking) 50 pub pending_unstake_epoch: Option<u64>, 51 /// Epoch when stake was requested (None if not re-staking) 52 pub pending_stake_epoch: Option<u64>, 53 /// Commitment to staked balance (for privacy) 54 pub staked_balance_commitment: Option<TokenCommitment>, 55 /// Last known staked balance (for dividend calculation) 56 /// Note: Actual balance is ZK-shielded, this is for internal tracking 57 staked_balance: u64, 58 } 59 60 impl Default for StakingState { 61 fn default() -> Self { 62 Self { 63 is_staking: DEFAULT_STAKING_STATE, 64 pending_unstake_epoch: None, 65 pending_stake_epoch: None, 66 staked_balance_commitment: None, 67 staked_balance: 0, 68 } 69 } 70 } 71 72 impl StakingState { 73 /// Create a new staking state with default settings (staking ON) 74 pub fn new() -> Self { 75 Self::default() 76 } 77 78 /// Create a staking state with specific initial balance 79 pub fn with_balance(balance: u64) -> Self { 80 Self { 81 is_staking: DEFAULT_STAKING_STATE, 82 pending_unstake_epoch: None, 83 pending_stake_epoch: None, 84 staked_balance_commitment: None, 85 staked_balance: balance, 86 } 87 } 88 89 /// Check if wallet is currently staking 90 pub fn is_staking(&self) -> bool { 91 self.is_staking 92 } 93 94 /// Check if there's a pending unstake request 95 pub fn has_pending_unstake(&self) -> bool { 96 self.pending_unstake_epoch.is_some() 97 } 98 99 /// Check if there's a pending stake request 100 pub fn has_pending_stake(&self) -> bool { 101 self.pending_stake_epoch.is_some() 102 } 103 104 /// Get staked balance (returns 0 if not staking) 105 pub fn staked_balance(&self) -> u64 { 106 if self.is_staking { 107 self.staked_balance 108 } else { 109 0 110 } 111 } 112 113 /// Get raw balance regardless of staking state 114 /// 115 /// Use this for checking actual token holdings after unstaking. 116 pub fn balance(&self) -> u64 { 117 self.staked_balance 118 } 119 120 /// Update staked balance 121 pub fn set_balance(&mut self, balance: u64) { 122 self.staked_balance = balance; 123 } 124 125 /// Add to staked balance (when receiving DX while staked) 126 pub fn add_balance(&mut self, amount: u64) { 127 self.staked_balance = self.staked_balance.saturating_add(amount); 128 } 129 130 /// Subtract from staked balance (for slashing) 131 pub fn subtract_balance(&mut self, amount: u64) { 132 self.staked_balance = self.staked_balance.saturating_sub(amount); 133 } 134 } 135 136 // ============================================================================ 137 // Pending Operations 138 // ============================================================================ 139 140 /// A pending staking operation 141 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 142 pub struct PendingOperation { 143 /// DX public key (wallet address hash) 144 pub dx_pubkey: [u8; 32], 145 /// Epoch when the operation was requested 146 pub requested_epoch: u64, 147 } 148 149 // ============================================================================ 150 // Staking Registry 151 // ============================================================================ 152 153 /// Registry tracking all wallet staking states 154 #[derive(Clone, Debug, Default)] 155 pub struct StakingRegistry { 156 /// Wallet states by DX pubkey hash 157 wallet_states: HashMap<[u8; 32], StakingState>, 158 /// Pending unstake requests (to be processed at epoch transition) 159 pending_unstakes: Vec<PendingOperation>, 160 /// Pending stake requests (to be processed at epoch transition) 161 pending_stakes: Vec<PendingOperation>, 162 /// Total staked DX across all wallets (aggregate, for explorer) 163 total_staked: u64, 164 /// Total number of staked wallets (for explorer) 165 staked_wallet_count: u64, 166 /// Current epoch 167 current_epoch: u64, 168 } 169 170 impl StakingRegistry { 171 /// Create a new empty registry 172 pub fn new() -> Self { 173 Self::default() 174 } 175 176 /// Create registry at a specific epoch 177 pub fn at_epoch(epoch: u64) -> Self { 178 Self { 179 current_epoch: epoch, 180 ..Default::default() 181 } 182 } 183 184 /// Get or create wallet state (new wallets default to staking ON) 185 pub fn get_or_create_wallet(&mut self, dx_pubkey: &[u8; 32]) -> &mut StakingState { 186 if !self.wallet_states.contains_key(dx_pubkey) { 187 self.wallet_states.insert(*dx_pubkey, StakingState::new()); 188 self.staked_wallet_count += 1; 189 } 190 self.wallet_states.get_mut(dx_pubkey).unwrap() 191 } 192 193 /// Get wallet state (read-only) 194 pub fn get_wallet(&self, dx_pubkey: &[u8; 32]) -> Option<&StakingState> { 195 self.wallet_states.get(dx_pubkey) 196 } 197 198 /// Check if a wallet is currently staking 199 /// 200 /// Returns true if: 201 /// - Wallet exists and has is_staking = true 202 /// - Wallet doesn't exist (new wallets default to staking) 203 pub fn is_staked(&self, dx_pubkey: &[u8; 32]) -> bool { 204 self.wallet_states 205 .get(dx_pubkey) 206 .map(|s| s.is_staking) 207 .unwrap_or(DEFAULT_STAKING_STATE) 208 } 209 210 /// Check if wallet can transfer (must be unstaked) 211 pub fn can_transfer(&self, dx_pubkey: &[u8; 32]) -> bool { 212 !self.is_staked(dx_pubkey) 213 } 214 215 /// Request to unstake (takes effect at next epoch transition) 216 /// 217 /// # Arguments 218 /// * `dx_pubkey` - The wallet's DX public key 219 /// 220 /// # Returns 221 /// Ok if unstake request was queued, Err if already unstaked 222 pub fn request_unstake(&mut self, dx_pubkey: &[u8; 32]) -> Result<()> { 223 let current_epoch = self.current_epoch; 224 let state = self.get_or_create_wallet(dx_pubkey); 225 226 ensure!(state.is_staking, "Wallet is already unstaked"); 227 ensure!( 228 state.pending_unstake_epoch.is_none(), 229 "Unstake already pending" 230 ); 231 232 state.pending_unstake_epoch = Some(current_epoch); 233 234 self.pending_unstakes.push(PendingOperation { 235 dx_pubkey: *dx_pubkey, 236 requested_epoch: current_epoch, 237 }); 238 239 Ok(()) 240 } 241 242 /// Request to stake (re-enable staking after unstaking) 243 /// 244 /// Takes effect at next epoch transition. 245 pub fn request_stake(&mut self, dx_pubkey: &[u8; 32]) -> Result<()> { 246 // Copy epoch before mutable borrow 247 let current_epoch = self.current_epoch; 248 let state = self.get_or_create_wallet(dx_pubkey); 249 250 ensure!(!state.is_staking, "Wallet is already staking"); 251 ensure!(state.pending_stake_epoch.is_none(), "Stake already pending"); 252 253 state.pending_stake_epoch = Some(current_epoch); 254 255 self.pending_stakes.push(PendingOperation { 256 dx_pubkey: *dx_pubkey, 257 requested_epoch: current_epoch, 258 }); 259 260 Ok(()) 261 } 262 263 /// Process pending stake requests (Priority 3 in epoch transition) 264 /// 265 /// Called during epoch transition AFTER dividends are calculated. 266 pub fn process_pending_stakes(&mut self, new_epoch: u64) -> Result<Vec<[u8; 32]>> { 267 let mut processed = Vec::new(); 268 269 for op in self.pending_stakes.drain(..) { 270 if let Some(state) = self.wallet_states.get_mut(&op.dx_pubkey) { 271 if state.pending_stake_epoch.is_some() { 272 state.is_staking = true; 273 state.pending_stake_epoch = None; 274 self.staked_wallet_count += 1; 275 self.total_staked = self.total_staked.saturating_add(state.staked_balance); 276 processed.push(op.dx_pubkey); 277 } 278 } 279 } 280 281 self.current_epoch = new_epoch; 282 Ok(processed) 283 } 284 285 /// Process pending unstake requests (Priority 4 in epoch transition - LAST) 286 /// 287 /// Called during epoch transition AFTER stake requests are processed. 288 pub fn process_pending_unstakes(&mut self, new_epoch: u64) -> Result<Vec<[u8; 32]>> { 289 let mut processed = Vec::new(); 290 291 for op in self.pending_unstakes.drain(..) { 292 if let Some(state) = self.wallet_states.get_mut(&op.dx_pubkey) { 293 if state.pending_unstake_epoch.is_some() { 294 state.is_staking = false; 295 state.pending_unstake_epoch = None; 296 self.staked_wallet_count = self.staked_wallet_count.saturating_sub(1); 297 self.total_staked = self.total_staked.saturating_sub(state.staked_balance); 298 processed.push(op.dx_pubkey); 299 } 300 } 301 } 302 303 self.current_epoch = new_epoch; 304 Ok(processed) 305 } 306 307 /// Register a new wallet with initial balance 308 /// 309 /// New wallets are staked by default. 310 pub fn register_wallet(&mut self, dx_pubkey: &[u8; 32], initial_balance: u64) { 311 let state = self.get_or_create_wallet(dx_pubkey); 312 state.set_balance(initial_balance); 313 self.total_staked = self.total_staked.saturating_add(initial_balance); 314 } 315 316 /// Update wallet balance (for tracking) 317 pub fn update_balance(&mut self, dx_pubkey: &[u8; 32], new_balance: u64) { 318 if let Some(state) = self.wallet_states.get_mut(dx_pubkey) { 319 let old_balance = state.staked_balance; 320 state.set_balance(new_balance); 321 322 if state.is_staking { 323 self.total_staked = self.total_staked.saturating_sub(old_balance); 324 self.total_staked = self.total_staked.saturating_add(new_balance); 325 } 326 } 327 } 328 329 /// Apply slashing to a wallet (reduces staked balance) 330 /// 331 /// Returns the actual amount slashed. 332 pub fn apply_slashing(&mut self, dx_pubkey: &[u8; 32], slash_amount: u64) -> u64 { 333 if let Some(state) = self.wallet_states.get_mut(dx_pubkey) { 334 let actual_slash = slash_amount.min(state.staked_balance); 335 state.subtract_balance(actual_slash); 336 self.total_staked = self.total_staked.saturating_sub(actual_slash); 337 actual_slash 338 } else { 339 0 340 } 341 } 342 343 /// Get staked balance for a wallet 344 pub fn get_staked_balance(&self, dx_pubkey: &[u8; 32]) -> u64 { 345 self.wallet_states 346 .get(dx_pubkey) 347 .map(|s| s.staked_balance()) 348 .unwrap_or(0) 349 } 350 351 /// Get total staked DX across all wallets (public aggregate) 352 pub fn total_staked(&self) -> u64 { 353 self.total_staked 354 } 355 356 /// Get count of staked wallets (public aggregate) 357 pub fn staked_wallet_count(&self) -> u64 { 358 self.staked_wallet_count 359 } 360 361 /// Get current epoch 362 pub fn current_epoch(&self) -> u64 { 363 self.current_epoch 364 } 365 366 /// Advance to new epoch 367 pub fn set_epoch(&mut self, epoch: u64) { 368 self.current_epoch = epoch; 369 } 370 371 /// Get all staked wallets with their balances (for dividend calculation) 372 pub fn get_staked_wallets(&self) -> Vec<([u8; 32], u64)> { 373 self.wallet_states 374 .iter() 375 .filter(|(_, state)| state.is_staking) 376 .map(|(pubkey, state)| (*pubkey, state.staked_balance)) 377 .collect() 378 } 379 380 /// Get iterator over all wallet states 381 pub fn iter_wallets(&self) -> impl Iterator<Item = (&[u8; 32], &StakingState)> { 382 self.wallet_states.iter() 383 } 384 } 385 386 // ============================================================================ 387 // Staking Proof (for privacy-preserving staking operations) 388 // ============================================================================ 389 390 /// Zero-knowledge proof of valid staking operation 391 #[derive(Clone, Debug, Serialize, Deserialize)] 392 pub struct StakingProof { 393 /// DX public key 394 pub dx_pubkey: [u8; 32], 395 /// Operation type 396 pub operation: StakingOperation, 397 /// Commitment to balance (hides actual amount) 398 pub balance_commitment: [u8; 32], 399 /// Proof data 400 pub proof_data: Vec<u8>, 401 /// Epoch of operation 402 pub epoch: u64, 403 } 404 405 /// Type of staking operation 406 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 407 pub enum StakingOperation { 408 /// Request to unstake 409 Unstake, 410 /// Request to re-stake 411 Stake, 412 } 413 414 impl StakingProof { 415 /// Create a new staking proof 416 pub fn new( 417 dx_pubkey: [u8; 32], 418 operation: StakingOperation, 419 balance: u64, 420 randomness: u64, 421 epoch: u64, 422 ) -> Self { 423 // Create balance commitment 424 let mut hasher = Sha256::new(); 425 hasher.update(b"STAKING_BALANCE_COMMIT"); 426 hasher.update(balance.to_le_bytes()); 427 hasher.update(randomness.to_le_bytes()); 428 let mut balance_commitment = [0u8; 32]; 429 balance_commitment.copy_from_slice(&hasher.finalize()); 430 431 // Create proof data 432 let mut hasher = Sha256::new(); 433 hasher.update(b"STAKING_PROOF"); 434 hasher.update(dx_pubkey); 435 hasher.update([operation.clone() as u8]); 436 hasher.update(balance_commitment); 437 hasher.update(epoch.to_le_bytes()); 438 let proof_data = hasher.finalize().to_vec(); 439 440 Self { 441 dx_pubkey, 442 operation, 443 balance_commitment, 444 proof_data, 445 epoch, 446 } 447 } 448 449 /// Verify the proof 450 pub fn verify(&self) -> bool { 451 // Verify proof data is not empty 452 if self.proof_data.is_empty() { 453 return false; 454 } 455 456 // Verify balance commitment is not zero 457 if self.balance_commitment == [0u8; 32] { 458 return false; 459 } 460 461 // In production: verify ZK proof 462 true 463 } 464 } 465 466 // ============================================================================ 467 // Tests 468 // ============================================================================ 469 470 #[cfg(test)] 471 mod tests { 472 use super::*; 473 474 #[test] 475 fn test_default_staking_state() { 476 let state = StakingState::new(); 477 assert!(state.is_staking()); 478 assert!(!state.has_pending_unstake()); 479 assert!(!state.has_pending_stake()); 480 } 481 482 #[test] 483 fn test_staking_registry_new_wallet() { 484 let mut registry = StakingRegistry::new(); 485 let pubkey = [1u8; 32]; 486 487 // New wallet should be staking by default 488 assert!(registry.is_staked(&pubkey)); 489 490 // Getting or creating should maintain default state 491 let state = registry.get_or_create_wallet(&pubkey); 492 assert!(state.is_staking()); 493 } 494 495 #[test] 496 fn test_request_unstake() { 497 let mut registry = StakingRegistry::at_epoch(100); 498 let pubkey = [2u8; 32]; 499 500 registry.register_wallet(&pubkey, 10_000); 501 502 // Should be staking initially 503 assert!(registry.is_staked(&pubkey)); 504 assert!(!registry.can_transfer(&pubkey)); 505 506 // Request unstake 507 registry.request_unstake(&pubkey).unwrap(); 508 509 // Still staking until epoch transition 510 assert!(registry.is_staked(&pubkey)); 511 512 // Process epoch transition (unstaking is LAST) 513 registry.process_pending_unstakes(101).unwrap(); 514 515 // Now should be unstaked 516 assert!(!registry.is_staked(&pubkey)); 517 assert!(registry.can_transfer(&pubkey)); 518 } 519 520 #[test] 521 fn test_request_stake_after_unstake() { 522 let mut registry = StakingRegistry::at_epoch(100); 523 let pubkey = [3u8; 32]; 524 525 registry.register_wallet(&pubkey, 10_000); 526 527 // Unstake 528 registry.request_unstake(&pubkey).unwrap(); 529 registry.process_pending_unstakes(101).unwrap(); 530 assert!(!registry.is_staked(&pubkey)); 531 532 // Re-stake 533 registry.request_stake(&pubkey).unwrap(); 534 registry.process_pending_stakes(102).unwrap(); 535 assert!(registry.is_staked(&pubkey)); 536 } 537 538 #[test] 539 fn test_already_unstaked_error() { 540 let mut registry = StakingRegistry::at_epoch(100); 541 let pubkey = [4u8; 32]; 542 543 registry.register_wallet(&pubkey, 10_000); 544 registry.request_unstake(&pubkey).unwrap(); 545 registry.process_pending_unstakes(101).unwrap(); 546 547 // Should error when trying to unstake again 548 assert!(registry.request_unstake(&pubkey).is_err()); 549 } 550 551 #[test] 552 fn test_already_staking_error() { 553 let mut registry = StakingRegistry::new(); 554 let pubkey = [5u8; 32]; 555 556 registry.register_wallet(&pubkey, 10_000); 557 558 // Should error when trying to stake while already staking 559 assert!(registry.request_stake(&pubkey).is_err()); 560 } 561 562 #[test] 563 fn test_total_staked_tracking() { 564 let mut registry = StakingRegistry::at_epoch(100); 565 566 let pubkey1 = [6u8; 32]; 567 let pubkey2 = [7u8; 32]; 568 569 registry.register_wallet(&pubkey1, 5_000); 570 registry.register_wallet(&pubkey2, 3_000); 571 572 assert_eq!(registry.total_staked(), 8_000); 573 assert_eq!(registry.staked_wallet_count(), 2); 574 575 // Unstake one 576 registry.request_unstake(&pubkey1).unwrap(); 577 registry.process_pending_unstakes(101).unwrap(); 578 579 assert_eq!(registry.total_staked(), 3_000); 580 assert_eq!(registry.staked_wallet_count(), 1); 581 } 582 583 #[test] 584 fn test_apply_slashing() { 585 let mut registry = StakingRegistry::new(); 586 let pubkey = [8u8; 32]; 587 588 registry.register_wallet(&pubkey, 10_000); 589 assert_eq!(registry.total_staked(), 10_000); 590 591 // Slash 1% (100 DX) 592 let slashed = registry.apply_slashing(&pubkey, 100); 593 assert_eq!(slashed, 100); 594 assert_eq!(registry.get_staked_balance(&pubkey), 9_900); 595 assert_eq!(registry.total_staked(), 9_900); 596 } 597 598 #[test] 599 fn test_slashing_capped_at_balance() { 600 let mut registry = StakingRegistry::new(); 601 let pubkey = [9u8; 32]; 602 603 registry.register_wallet(&pubkey, 100); 604 605 // Try to slash more than balance 606 let slashed = registry.apply_slashing(&pubkey, 1_000); 607 assert_eq!(slashed, 100); 608 assert_eq!(registry.get_staked_balance(&pubkey), 0); 609 } 610 611 #[test] 612 fn test_get_staked_wallets() { 613 let mut registry = StakingRegistry::at_epoch(100); 614 615 let pubkey1 = [10u8; 32]; 616 let pubkey2 = [11u8; 32]; 617 618 registry.register_wallet(&pubkey1, 5_000); 619 registry.register_wallet(&pubkey2, 3_000); 620 621 // Unstake one 622 registry.request_unstake(&pubkey2).unwrap(); 623 registry.process_pending_unstakes(101).unwrap(); 624 625 let staked = registry.get_staked_wallets(); 626 assert_eq!(staked.len(), 1); 627 assert_eq!(staked[0].0, pubkey1); 628 assert_eq!(staked[0].1, 5_000); 629 } 630 631 #[test] 632 fn test_staking_proof() { 633 let pubkey = [12u8; 32]; 634 let proof = StakingProof::new(pubkey, StakingOperation::Unstake, 10_000, 12345, 100); 635 636 assert!(proof.verify()); 637 assert_eq!(proof.dx_pubkey, pubkey); 638 assert_eq!(proof.operation, StakingOperation::Unstake); 639 } 640 641 #[test] 642 fn test_epoch_transition_priority() { 643 // This test verifies the correct order of operations 644 let mut registry = StakingRegistry::at_epoch(100); 645 646 let pubkey1 = [13u8; 32]; // Will unstake 647 let pubkey2 = [14u8; 32]; // Will re-stake 648 649 registry.register_wallet(&pubkey1, 5_000); 650 registry.register_wallet(&pubkey2, 3_000); 651 652 // First, unstake pubkey2 so we can re-stake it 653 registry.request_unstake(&pubkey2).unwrap(); 654 registry.process_pending_unstakes(101).unwrap(); 655 656 // Now queue operations for next epoch 657 registry.set_epoch(101); 658 registry.request_unstake(&pubkey1).unwrap(); 659 registry.request_stake(&pubkey2).unwrap(); 660 661 // Before processing: pubkey1 staked, pubkey2 unstaked 662 assert!(registry.is_staked(&pubkey1)); 663 assert!(!registry.is_staked(&pubkey2)); 664 665 // Process in priority order: 666 // 1. Slashing would go here 667 // 2. Dividends would be calculated here (on current state) 668 // 3. Process stakes 669 registry.process_pending_stakes(102).unwrap(); 670 // pubkey2 is now staked again 671 assert!(registry.is_staked(&pubkey2)); 672 673 // 4. Process unstakes (LAST) 674 registry.process_pending_unstakes(102).unwrap(); 675 // pubkey1 is now unstaked 676 assert!(!registry.is_staked(&pubkey1)); 677 } 678 }