Skip to main content

rialo_validator_registry_interface/
instruction.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Instructions for the Validator registry program.
5
6use std::collections::BTreeMap;
7
8use rialo_s_instruction::{AccountMeta, Instruction};
9use rialo_s_program::system_program;
10use rialo_s_pubkey::Pubkey;
11use serde::{Deserialize, Serialize};
12
13/// Instructions supported by the Validator Registry program
14#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
15pub enum ValidatorRegistryInstruction {
16    /// Register a new validator in the registry. The signing_key account pays for and
17    /// becomes the initial owner/withdrawer of the validator info PDA account.
18    ///
19    /// Returns `InstructionError::AccountAlreadyInitialized` if the validator is already
20    /// registered.
21    ///
22    /// Accounts expected:
23    /// 0. `[writable]` Validator info account (PDA derived from authority_key)
24    /// 1. `[signer, writable]` Signing key account (must match signing_key in instruction data, pays for PDA creation)
25    /// 2. `[]` System program
26    /// 3. `[writable]` Self-bond stake account (PDA derived from validator_info)
27    /// 4. `[]` StakeManager program
28    /// 5. `[]` ValidatorRegistry program (needed for ActivateStake → AddStake CPI callback)
29    Register {
30        /// The validator's signing key (the initial withdrawal key)
31        signing_key: Pubkey,
32        /// Network address for communicating with the authority.
33        address: Vec<u8>,
34        /// State sync address for state synchronization (Multiaddr bytes).
35        state_sync_address: Vec<u8>,
36        /// The authority's hostname, for metrics and logging.
37        hostname: String,
38        /// The authority's public key as Rialo identity.
39        authority_key: Vec<u8>,
40        /// The authority's public key for verifying blocks.
41        protocol_key: Pubkey,
42        /// The authority's public key for TLS and as network identity.
43        network_key: Pubkey,
44        /// The validator's commission from the inflation rewards
45        /// in basis points, e.g. 835 corresponds to 8.35%
46        commission_rate: u16,
47        /// The minimum duration in milliseconds that StakeInfo accounts must commit
48        /// to delegate for; can be set by the validator to earn
49        /// increased rewards (bonus)
50        lockup_period: u64,
51    },
52
53    /// Update the withdrawal key for a validator
54    ///
55    /// Accounts expected:
56    /// 0. `[writable]` Validator info account (PDA - cannot sign)
57    /// 1. `[signer]` Current withdrawal key
58    /// 2. `[]` New withdrawal key
59    UpdateWithdrawer {
60        /// New authority who can withdraw rewards
61        new_withdrawal_key: Pubkey,
62    },
63
64    /// Add stake to a validator's aggregate stake.
65    ///
66    /// This instruction is called via CPI from the StakeManager program when
67    /// a stake account is activated (delegated to a validator). It increases
68    /// the validator's `stake` field by the specified amount.
69    ///
70    /// This is a privileged operation that can only be called via CPI from
71    /// the StakeManager program.
72    ///
73    /// Accounts expected:
74    /// 0. `[writable]` Validator info account (PDA derived from authority_key)
75    AddStake {
76        /// Amount of stake to add
77        amount: u64,
78    },
79
80    /// Subtract stake from a validator's aggregate stake.
81    ///
82    /// This instruction is called via CPI from the StakeManager program when
83    /// a stake account is deactivated. It decreases the validator's `stake`
84    /// field by the specified amount.
85    ///
86    /// This is a privileged operation that can only be called via CPI from
87    /// the StakeManager program.
88    ///
89    /// Accounts expected:
90    /// 0. `[writable]` Validator info account (PDA derived from authority_key)
91    SubStake {
92        /// Amount of stake to subtract
93        amount: u64,
94    },
95
96    /// Set the unbonding period for a validator.
97    ///
98    /// This instruction sets the `unbonding_period` field on a validator's info account.
99    /// The unbonding period determines how long deactivated stake must wait before
100    /// it can be withdrawn. This is intended to be set by the runtime to punish
101    /// misbehaving validators.
102    ///
103    /// **IMPORTANT:** This instruction can ONLY be called via CPI from the
104    /// TokenomicsGovernance program. The processor verifies the CPI caller.
105    /// Direct invocation will fail with `IncorrectProgramId`.
106    ///
107    /// Accounts expected:
108    /// 0. `[writable]` Validator info account (PDA derived from authority_key)
109    SetUnbondingPeriod {
110        /// The new unbonding period in milliseconds
111        unbonding_period: u64,
112    },
113
114    /// Change the commission rate for a validator.
115    ///
116    /// This instruction allows a validator to request a commission rate change.
117    /// The new rate is stored in `new_commission_rate` and will be applied at
118    /// the next epoch boundary (during FreezeStakes) to give delegators time
119    /// to react to the change.
120    ///
121    /// Validation:
122    /// - 0 ≤ new_commission_rate ≤ MAX_COMMISSION_RATE (10000 basis points = 100%)
123    /// - |new_commission_rate - current_commission_rate| ≤ MAX_COMMISSION_CHANGE (200 bp)
124    ///   (protects delegators from sudden large changes)
125    /// - If lockup_period > 0, only decreases are allowed (increases are rejected).
126    ///   Validators with a lockup period commit to their commission rate in exchange
127    ///   for higher delegation and commission revenue from the higher lockup rewards.
128    ///
129    /// Accounts expected:
130    /// 0. `[writable]` Validator info account (PDA)
131    /// 1. `[signer]` Signing key (must match validator_account's signing_key)
132    ChangeCommissionRate {
133        /// New commission rate in basis points (e.g., 835 = 8.35%)
134        new_commission_rate: u16,
135    },
136
137    /// Withdraw kelvins from a validator account.
138    ///
139    /// This instruction allows the withdrawal key to withdraw kelvins
140    /// from the validator info account. This is typically used to withdraw
141    /// accumulated rewards or excess funds.
142    ///
143    /// The withdrawal transfers kelvins from the validator info account to
144    /// the withdrawal_key account. The validator account must have
145    /// sufficient kelvins after the withdrawal to remain rent-exempt.
146    ///
147    /// Accounts expected:
148    /// 0. `[writable]` Validator info account (PDA)
149    /// 1. `[signer, writable]` Withdrawal key (must match validator_account's withdrawal_key)
150    Withdraw {
151        /// Amount of kelvins to withdraw
152        amount: u64,
153    },
154
155    /// Change the earliest shutdown timestamp for a validator.
156    ///
157    /// This instruction allows a validator to announce a future timestamp after which
158    /// they don't intend to participate in committees and earn rewards.
159    /// This is particularly important for validators with a lockup_period, to prevent
160    /// new delegators from locking up their capital longer than it would be rewarded.
161    ///
162    /// When `earliest_shutdown` is `Some(timestamp)`:
163    /// - The timestamp must be >= current_timestamp + lockup_period.
164    ///   This ensures every currently-locked staker will have their full lockup
165    ///   honored before the validator stops.
166    ///
167    /// When `earliest_shutdown` is `None`:
168    /// - Resets the field, signaling the validator intends to continue operating.
169    ///
170    /// Accounts expected:
171    /// 0. `[writable]` Validator info account (PDA)
172    /// 1. `[signer]` Signing key (must match validator_account's signing_key)
173    ChangeEarliestShutdown {
174        /// The earliest timestamp (ms) after which the validator intends to shut down,
175        /// or None to reset/clear the shutdown signal.
176        earliest_shutdown: Option<u64>,
177    },
178
179    /// Update the signing key for a validator.
180    ///
181    /// This instruction allows a validator to rotate their signing key.
182    /// The current signing key must sign the transaction to authorize the change.
183    ///
184    /// **Note:** This only updates the ValidatorInfo account. If the validator's
185    /// self-bond stake account uses the signing key as admin_authority (the default
186    /// during registration), that authority must be updated separately via
187    /// `StakeManager::change_admin_authority` to fully migrate control.
188    ///
189    /// Accounts expected:
190    /// 0. `[writable]` Validator info account (PDA)
191    /// 1. `[signer]` Current signing key (must match validator_account's signing_key)
192    /// 2. `[]` New signing key
193    UpdateSigner {
194        /// The new signing key
195        new_signing_key: Pubkey,
196    },
197}
198
199/// Error type for converting a `StoredAccount` to `ValidatorInfo`.
200#[cfg(feature = "typed-account")]
201#[derive(Debug, thiserror::Error)]
202pub enum ValidatorInfoParseError {
203    /// The account is not owned by the ValidatorRegistry program.
204    #[error("Account owner mismatch: expected {expected}, got {actual}")]
205    InvalidOwner {
206        expected: rialo_s_pubkey::Pubkey,
207        actual: rialo_s_pubkey::Pubkey,
208    },
209    /// The account data could not be deserialized as `ValidatorInfo`.
210    #[error("Failed to deserialize ValidatorInfo: {0}")]
211    DeserializationFailed(#[from] bincode::Error),
212}
213
214/// Data structure for the validator information account. This data structure is meant to hold
215/// all information about a validator, such that we can leverage it in Epoch negotiation
216/// and create a proper `Authority` object on Epoch change.
217#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
218pub struct ValidatorInfo {
219    /// The validator's public key for transaction signing.
220    pub signing_key: Pubkey,
221    /// The validator's public key for withdrawing commission.
222    pub withdrawal_key: Pubkey,
223    /// Amount of token staked.
224    pub stake: u64,
225    /// Network address for consensus protocol communication (Multiaddr bytes).
226    pub address: Vec<u8>,
227    /// Network address for state sync communication (Multiaddr bytes).
228    pub state_sync_address: Vec<u8>,
229    /// Human-readable hostname for metrics and logging.
230    pub hostname: String,
231    /// The validator's identity key (currently BLS12-381).
232    pub authority_key: Vec<u8>,
233    /// The validator's public key for verifying blocks (Ed25519).
234    pub protocol_key: Pubkey,
235    /// The validator's public key for TLS and as network identity (Ed25519).
236    pub network_key: Pubkey,
237    /// When the validator registered.
238    pub registration_time: i64,
239    /// Last time info was updated.
240    pub last_update: i64,
241    /// Historical unbonding periods: effective_from_timestamp → unbonding_period_ms.
242    ///
243    /// The duration in milliseconds that deactivating StakeInfo accounts wait before becoming
244    /// inactive. Set by the runtime (via SetUnbondingPeriod) to punish misbehaving validators.
245    ///
246    /// The first entry (key = registration_time) contains the default unbonding period.
247    /// Subsequent entries are added by SetUnbondingPeriod to extend or reduce the period.
248    /// Entries are in ascending order by timestamp (guaranteed by BTreeMap).
249    ///
250    /// Note: A deactivated stake cannot be withdrawn until the unbonding period that was
251    /// effective at deactivation time has elapsed — see `end_of_unbonding()`.
252    pub unbonding_periods: BTreeMap<u64, u64>,
253    /// The minimum duration in milliseconds that StakeInfo accounts must commit to delegate for.
254    /// Can be set by the validator to earn increased rewards (bonus).
255    /// Note: An activated stake cannot be deactivated until
256    /// `current_timestamp >= activation_requested + lockup_period`.
257    /// This is fixed at registration and cannot be changed.
258    pub lockup_period: u64,
259    /// The validator's commission from the inflation rewards
260    /// in basis points, e.g. 835 corresponds to 8.35%
261    pub commission_rate: u16,
262    /// New commission_rate to be applied from the next epoch
263    pub new_commission_rate: Option<u16>,
264    /// Optional timestamp (ms) after which the validator does not intend to participate
265    /// in committees. Must be >= current_timestamp + lockup_period when set.
266    /// Passing None resets the field (validator decides to continue).
267    pub earliest_shutdown: Option<u64>,
268}
269
270impl ValidatorInfo {
271    /// Maximum number of historical unbonding period entries allowed in the `unbonding_periods` map.
272    /// 100 entries × 16 bytes (8-byte key + 8-byte value) = 1600 bytes of map data.
273    /// Enforced by the SetUnbondingPeriod handler to prevent unbounded account growth.
274    // Old entries predating all currently-unbonding stakes could be pruned to reclaim space.
275    pub const MAX_UNBONDING_PERIOD_ENTRIES: usize = 100;
276
277    /// Calculate the account data size needed to store a `ValidatorInfo` with the given
278    /// variable-length field sizes.
279    ///
280    /// Uses the in-memory struct size as a safe upper bound, plus the actual lengths of
281    /// the variable-length fields (address, hostname, authority_key).
282    #[inline]
283    pub const fn account_size(
284        address_len: usize,
285        state_sync_address_len: usize,
286        hostname_len: usize,
287        authority_key_len: usize,
288    ) -> usize {
289        std::mem::size_of::<Self>()
290            + address_len
291            + state_sync_address_len
292            + hostname_len
293            + authority_key_len
294    }
295
296    /// Calculate when unbonding ends for a stake that requested deactivation at the given timestamp.
297    ///
298    /// This handles three cases correctly:
299    /// 1. **Active stake**: Deactivated after punishment → uses punished period
300    /// 2. **Unbonding stake**: Was unbonding when punishment hit → gets extended
301    /// 3. **Fully unbonded**: Completed unbonding before punishment → exempt
302    ///
303    /// # Algorithm (Dual-Tracking)
304    ///
305    /// Uses two variables to achieve **fairness** (reductions apply to still-unbonding
306    /// accounts) while preventing **flash attacks** (brief 0-period can't free accounts):
307    ///
308    /// - `max_end`: monotonically increasing, tracks the longest unbonding window ever
309    ///   applicable. Used ONLY for the exemption check ("was this account genuinely done
310    ///   before this entry?"). Prevents flash reductions from creating false exemption gaps.
311    ///
312    /// - `effective_end`: tracks the latest applicable period. This is the ACTUAL result.
313    ///   Can decrease when governance reduces a punishment.
314    ///
315    /// # Complexity
316    /// O(log n + k) where n = total entries, k = entries after deactivation (typically 0-2)
317    ///
318    /// # Arguments
319    /// * `deactivation_requested` - When DeactivateStake was called (in milliseconds)
320    ///
321    /// # Returns
322    /// The timestamp when unbonding completes
323    pub fn end_of_unbonding(&self, deactivation_requested: u64) -> u64 {
324        // Step 1: Binary search to find the unbonding_period at deactivation time.
325        let initial_period = self
326            .unbonding_periods
327            .range(..=deactivation_requested)
328            .next_back()
329            .map(|(_, &period)| period)
330            .unwrap_or(0);
331
332        let initial_end = deactivation_requested.saturating_add(initial_period);
333        let mut max_end = initial_end; // monotonic — for exemption check only
334        let mut effective_end = initial_end; // latest applicable — the actual result
335
336        // Step 2: Iterate entries AFTER deactivation.
337        for (&entry_ts, &entry_period) in self.unbonding_periods.range((
338            std::ops::Bound::Excluded(deactivation_requested),
339            std::ops::Bound::Unbounded,
340        )) {
341            // Step 3a: Exemption check uses max_end (never decreases).
342            // If the account was genuinely done before this entry, stop.
343            if max_end < entry_ts {
344                break;
345            }
346
347            // Step 3b: Apply this entry.
348            let new_end = deactivation_requested.saturating_add(entry_period);
349            max_end = max_end.max(new_end); // only grows (preserves exemption window)
350            effective_end = new_end; // tracks latest (CAN decrease for reductions)
351        }
352
353        effective_end
354    }
355}
356
357#[cfg(feature = "typed-account")]
358impl TryFrom<rialo_s_account::StoredAccount> for ValidatorInfo {
359    type Error = ValidatorInfoParseError;
360
361    /// Attempts to parse a `StoredAccount` as a `ValidatorInfo`.
362    ///
363    /// # Errors
364    /// - `ValidatorInfoParseError::InvalidOwner` if the account is not owned by the ValidatorRegistry program.
365    /// - `ValidatorInfoParseError::DeserializationFailed` if the account data cannot be deserialized.
366    fn try_from(account: rialo_s_account::StoredAccount) -> Result<Self, Self::Error> {
367        use rialo_s_account::ReadableAccount;
368
369        // Verify ownership
370        let owner = *account.owner();
371        if owner != crate::id() {
372            return Err(ValidatorInfoParseError::InvalidOwner {
373                expected: crate::id(),
374                actual: owner,
375            });
376        }
377
378        // Deserialize the account data
379        let validator_info: ValidatorInfo = bincode::deserialize(account.data())?;
380        Ok(validator_info)
381    }
382}
383
384#[cfg(feature = "typed-account")]
385impl TryFrom<&rialo_s_account::StoredAccount> for ValidatorInfo {
386    type Error = ValidatorInfoParseError;
387
388    /// Attempts to parse a reference to a `StoredAccount` as a `ValidatorInfo`.
389    ///
390    /// # Errors
391    /// - `ValidatorInfoParseError::InvalidOwner` if the account is not owned by the ValidatorRegistry program.
392    /// - `ValidatorInfoParseError::DeserializationFailed` if the account data cannot be deserialized.
393    fn try_from(account: &rialo_s_account::StoredAccount) -> Result<Self, Self::Error> {
394        use rialo_s_account::ReadableAccount;
395
396        // Verify ownership
397        let owner = *account.owner();
398        if owner != crate::id() {
399            return Err(ValidatorInfoParseError::InvalidOwner {
400                expected: crate::id(),
401                actual: owner,
402            });
403        }
404
405        // Deserialize the account data
406        let validator_info: ValidatorInfo = bincode::deserialize(account.data())?;
407        Ok(validator_info)
408    }
409}
410
411impl ValidatorRegistryInstruction {
412    /// Create a `ValidatorRegistryInstruction::Register` `Instruction`
413    ///
414    /// # Account references
415    ///   0. `[WRITABLE]` Validator info account (PDA derived from authority_key)
416    ///   1. `[SIGNER, WRITABLE]` Signing key account (must match signing_key in instruction data, pays for PDA creation)
417    ///   2. `[]` System program
418    ///   3. `[WRITABLE]` Self-bond stake account (PDA derived from validator_info)
419    ///   4. `[]` StakeManager program
420    ///   5. `[]` ValidatorRegistry program (needed for ActivateStake → AddStake CPI callback)
421    ///
422    /// # Arguments
423    /// * `validator_info_pubkey` - The public key of the validator info account (PDA)
424    /// * `signing_key` - The validator's signing key (must sign, pays for PDA creation)
425    /// * `address` - The network address for communicating with the authority
426    /// * `state_sync_address` - The state sync address for state synchronization (Multiaddr bytes)
427    /// * `hostname` - The hostname of the authority
428    /// * `authority_key` - The public key of the authority (used to derive the PDA)
429    /// * `protocol_key` - The public key for verifying blocks
430    /// * `network_key` - The public key for TLS and as network identity
431    /// * `commission_rate` - The validator's commission in basis points (e.g. 835 = 8.35%)
432    /// * `lockup_period` - Minimum duration in milliseconds stake must be delegated for
433    ///
434    /// # Returns
435    /// * `Instruction` - A Solana instruction to be included in a transaction
436    pub fn register(
437        validator_info_pubkey: Pubkey,
438        signing_key: Pubkey,
439        address: Vec<u8>,
440        state_sync_address: Vec<u8>,
441        hostname: String,
442        authority_key: Vec<u8>,
443        protocol_key: Pubkey,
444        network_key: Pubkey,
445        commission_rate: u16,
446        lockup_period: u64,
447    ) -> Instruction {
448        // Derive self-bond PDA internally to avoid breaking caller interface
449        let self_bond_pubkey = crate::pda::derive_self_bond_address(&validator_info_pubkey);
450
451        Instruction::new_with_bincode(
452            crate::id(),
453            &ValidatorRegistryInstruction::Register {
454                signing_key,
455                address,
456                state_sync_address,
457                hostname,
458                authority_key,
459                protocol_key,
460                network_key,
461                commission_rate,
462                lockup_period,
463            },
464            vec![
465                AccountMeta::new(validator_info_pubkey, false), // PDA, not a signer
466                AccountMeta::new(signing_key, true), // signing key must sign and pays for PDA
467                AccountMeta::new_readonly(system_program::id(), false), // system program
468                AccountMeta::new(self_bond_pubkey, false), // self-bond PDA (writable)
469                AccountMeta::new_readonly(crate::STAKE_MANAGER_PROGRAM_ID, false), // StakeManager program
470                AccountMeta::new_readonly(crate::id(), false), // ValidatorRegistry program (for ActivateStake → AddStake CPI)
471            ],
472        )
473    }
474
475    /// Create a `ValidatorRegistryInstruction::UpdateWithdrawer` `Instruction`
476    ///
477    /// # Account references
478    ///   0. `[WRITABLE]` Validator info account (PDA - cannot sign)
479    ///   1. `[SIGNER]` Current withdrawal key account
480    ///   2. `[READONLY]` New withdrawal key account
481    ///
482    /// # Arguments
483    /// * `validator_info_pubkey` - The public key of the validator info account (PDA)
484    /// * `current_withdrawer` - The public key of the current withdrawal key
485    /// * `new_withdrawal_key` - The public key of the new withdrawal key
486    ///
487    /// # Returns
488    /// * `Instruction` - A Solana instruction to be included in a transaction
489    pub fn update_withdrawer(
490        validator_info_pubkey: Pubkey,
491        current_withdrawer: Pubkey,
492        new_withdrawal_key: Pubkey,
493    ) -> Instruction {
494        Instruction::new_with_bincode(
495            crate::id(),
496            &ValidatorRegistryInstruction::UpdateWithdrawer { new_withdrawal_key },
497            vec![
498                AccountMeta::new(validator_info_pubkey, false), // PDA cannot sign
499                AccountMeta::new_readonly(current_withdrawer, true),
500                AccountMeta::new_readonly(new_withdrawal_key, false),
501            ],
502        )
503    }
504
505    /// Create a `ValidatorRegistryInstruction::AddStake` `Instruction`
506    ///
507    /// This instruction adds stake to a validator's aggregate stake.
508    /// It is called via CPI from the StakeManager program when a stake
509    /// account is activated (delegated to a validator).
510    ///
511    /// # Account references
512    ///   0. `[WRITABLE]` Validator info account (PDA derived from authority_key)
513    ///
514    /// # Arguments
515    /// * `validator_info_pubkey` - The public key of the validator info account (PDA)
516    /// * `amount` - The amount of stake to add
517    ///
518    /// # Returns
519    /// * `Instruction` - A Solana instruction to be included in a transaction
520    pub fn add_stake(validator_info_pubkey: Pubkey, amount: u64) -> Instruction {
521        Instruction::new_with_bincode(
522            crate::id(),
523            &ValidatorRegistryInstruction::AddStake { amount },
524            vec![AccountMeta::new(validator_info_pubkey, false)],
525        )
526    }
527
528    /// Create a `ValidatorRegistryInstruction::SubStake` `Instruction`
529    ///
530    /// This instruction subtracts stake from a validator's aggregate stake.
531    /// It is called via CPI from the StakeManager program when a stake
532    /// account is deactivated.
533    ///
534    /// # Account references
535    ///   0. `[WRITABLE]` Validator info account (PDA derived from authority_key)
536    ///
537    /// # Arguments
538    /// * `validator_info_pubkey` - The public key of the validator info account (PDA)
539    /// * `amount` - The amount of stake to subtract
540    ///
541    /// # Returns
542    /// * `Instruction` - A Solana instruction to be included in a transaction
543    pub fn sub_stake(validator_info_pubkey: Pubkey, amount: u64) -> Instruction {
544        Instruction::new_with_bincode(
545            crate::id(),
546            &ValidatorRegistryInstruction::SubStake { amount },
547            vec![AccountMeta::new(validator_info_pubkey, false)],
548        )
549    }
550
551    /// Create a `ValidatorRegistryInstruction::SetUnbondingPeriod` `Instruction`
552    ///
553    /// This instruction sets the unbonding period for a validator. The unbonding
554    /// period determines how long deactivated stake must wait before it can be
555    /// withdrawn. This is intended to be set by the runtime to punish misbehaving
556    /// validators.
557    ///
558    /// **IMPORTANT:** This instruction can ONLY be called via CPI from the
559    /// TokenomicsGovernance program. The processor verifies the CPI caller.
560    /// Direct invocation will fail with `IncorrectProgramId`.
561    ///
562    /// # Account references
563    ///   0. `[WRITABLE]` Validator info account (PDA derived from authority_key)
564    ///
565    /// # Arguments
566    /// * `validator_info_pubkey` - The public key of the validator info account (PDA)
567    /// * `unbonding_period` - The new unbonding period in milliseconds
568    ///
569    /// # Returns
570    /// * `Instruction` - A Solana instruction to be included in a transaction
571    pub fn set_unbonding_period(
572        validator_info_pubkey: Pubkey,
573        unbonding_period: u64,
574    ) -> Instruction {
575        Instruction::new_with_bincode(
576            crate::id(),
577            &ValidatorRegistryInstruction::SetUnbondingPeriod { unbonding_period },
578            vec![
579                AccountMeta::new(validator_info_pubkey, false), // Validator info account (writable)
580            ],
581        )
582    }
583
584    /// Create a `ValidatorRegistryInstruction::ChangeCommissionRate` `Instruction`
585    ///
586    /// This instruction allows a validator to request a commission rate change.
587    /// The new rate is stored in `new_commission_rate` and will be applied at
588    /// the next epoch boundary (during FreezeStakes).
589    ///
590    /// # Account references
591    ///   0. `[WRITABLE]` Validator info account (PDA)
592    ///   1. `[SIGNER]` Signing key (must match validator_account's signing_key)
593    ///
594    /// # Arguments
595    /// * `validator_info_pubkey` - The public key of the validator info account (PDA)
596    /// * `signing_key` - The validator's signing key (must sign)
597    /// * `new_commission_rate` - The new commission rate in basis points (0-10000)
598    ///
599    /// # Returns
600    /// * `Instruction` - A Solana instruction to be included in a transaction
601    pub fn change_commission_rate(
602        validator_info_pubkey: Pubkey,
603        signing_key: Pubkey,
604        new_commission_rate: u16,
605    ) -> Instruction {
606        Instruction::new_with_bincode(
607            crate::id(),
608            &ValidatorRegistryInstruction::ChangeCommissionRate {
609                new_commission_rate,
610            },
611            vec![
612                AccountMeta::new(validator_info_pubkey, false), // Validator info account (writable)
613                AccountMeta::new_readonly(signing_key, true),   // Signing key must sign
614            ],
615        )
616    }
617
618    /// Create a `ValidatorRegistryInstruction::Withdraw` `Instruction`
619    ///
620    /// This instruction allows the withdrawal key to withdraw kelvins
621    /// from a validator info account. The withdrawal transfers kelvins to the
622    /// withdrawal_key account.
623    ///
624    /// # Account references
625    ///   0. `[WRITABLE]` Validator info account (PDA)
626    ///   1. `[SIGNER, WRITABLE]` Withdrawal key (must match validator_account's withdrawal_key)
627    ///
628    /// # Arguments
629    /// * `validator_info_pubkey` - The public key of the validator info account (PDA)
630    /// * `withdrawal_key` - The public key of the withdrawal key (must sign)
631    /// * `amount` - The amount of kelvins to withdraw
632    ///
633    /// # Returns
634    /// * `Instruction` - A Solana instruction to be included in a transaction
635    pub fn withdraw(
636        validator_info_pubkey: Pubkey,
637        withdrawal_key: Pubkey,
638        amount: u64,
639    ) -> Instruction {
640        Instruction::new_with_bincode(
641            crate::id(),
642            &ValidatorRegistryInstruction::Withdraw { amount },
643            vec![
644                AccountMeta::new(validator_info_pubkey, false), // Validator info account (writable)
645                AccountMeta::new(withdrawal_key, true), // Withdrawal key must sign, receives kelvins
646            ],
647        )
648    }
649
650    /// Create a `ValidatorRegistryInstruction::UpdateSigner` `Instruction`
651    ///
652    /// This instruction allows a validator to rotate their signing key.
653    /// The current signing key must sign the transaction.
654    ///
655    /// # Account references
656    ///   0. `[WRITABLE]` Validator info account (PDA)
657    ///   1. `[SIGNER]` Current signing key (must match validator_account's signing_key)
658    ///   2. `[READONLY]` New signing key
659    ///
660    /// # Arguments
661    /// * `validator_info_pubkey` - The public key of the validator info account (PDA)
662    /// * `current_signing_key` - The current signing key (must sign)
663    /// * `new_signing_key` - The new signing key
664    ///
665    /// # Returns
666    /// * `Instruction` - A Solana instruction to be included in a transaction
667    pub fn update_signer(
668        validator_info_pubkey: Pubkey,
669        current_signing_key: Pubkey,
670        new_signing_key: Pubkey,
671    ) -> Instruction {
672        Instruction::new_with_bincode(
673            crate::id(),
674            &ValidatorRegistryInstruction::UpdateSigner { new_signing_key },
675            vec![
676                AccountMeta::new(validator_info_pubkey, false), // PDA cannot sign
677                AccountMeta::new_readonly(current_signing_key, true), // Current signing key must sign
678                AccountMeta::new_readonly(new_signing_key, false),    // New signing key
679            ],
680        )
681    }
682
683    /// Create a `ValidatorRegistryInstruction::ChangeEarliestShutdown` `Instruction`
684    ///
685    /// This instruction allows a validator to announce (or clear) a future timestamp
686    /// after which they don't intend to participate in committees.
687    ///
688    /// # Account references
689    ///   0. `[WRITABLE]` Validator info account (PDA)
690    ///   1. `[SIGNER]` Signing key (must match validator_account's signing_key)
691    ///
692    /// # Arguments
693    /// * `validator_info_pubkey` - The public key of the validator info account (PDA)
694    /// * `signing_key` - The validator's signing key (must sign)
695    /// * `earliest_shutdown` - The shutdown timestamp in ms, or None to clear
696    ///
697    /// # Returns
698    /// * `Instruction` - A Solana instruction to be included in a transaction
699    pub fn change_earliest_shutdown(
700        validator_info_pubkey: Pubkey,
701        signing_key: Pubkey,
702        earliest_shutdown: Option<u64>,
703    ) -> Instruction {
704        Instruction::new_with_bincode(
705            crate::id(),
706            &ValidatorRegistryInstruction::ChangeEarliestShutdown { earliest_shutdown },
707            vec![
708                AccountMeta::new(validator_info_pubkey, false), // Validator info account (writable)
709                AccountMeta::new_readonly(signing_key, true),   // Signing key must sign
710            ],
711        )
712    }
713}
714
715#[cfg(test)]
716mod tests {
717    use std::net::Ipv4Addr;
718
719    use fastcrypto::{
720        bls12381::min_sig::BLS12381KeyPair,
721        traits::{KeyPair, ToFromBytes},
722    };
723    use multiaddr::Multiaddr;
724
725    use super::*;
726
727    fn create_test_pubkeys() -> (Pubkey, Pubkey, Pubkey) {
728        let validator_info_pubkey = Pubkey::new_unique();
729        let signing_key = Pubkey::new_unique();
730        let new_withdrawer = Pubkey::new_unique();
731        (validator_info_pubkey, signing_key, new_withdrawer)
732    }
733
734    #[test]
735    fn test_register_instruction() {
736        let mut rng = rand::thread_rng();
737
738        let (validator_info_pubkey, signing_key, protocol_key) = create_test_pubkeys();
739
740        let hostname = String::from("localhost");
741        let authority_key = BLS12381KeyPair::generate(&mut rng)
742            .public()
743            .as_bytes()
744            .to_vec();
745        let network_key = Pubkey::new_unique();
746        let address = Multiaddr::from(Ipv4Addr::LOCALHOST);
747        let commission_rate = 500u16; // 5%
748        let lockup_period = 10u64;
749
750        // Create the register instruction
751        let instruction = ValidatorRegistryInstruction::register(
752            validator_info_pubkey,
753            signing_key,
754            address.to_vec(),
755            vec![], // state_sync_address
756            hostname.clone(),
757            authority_key.clone(),
758            protocol_key,
759            network_key,
760            commission_rate,
761            lockup_period,
762        );
763
764        // Verify the program ID
765        assert_eq!(instruction.program_id, crate::id());
766
767        // Verify first 3 accounts (the remaining 3 are derived internally)
768        assert_eq!(instruction.accounts.len(), 6);
769        assert_eq!(
770            instruction.accounts[0],
771            AccountMeta::new(validator_info_pubkey, false)
772        ); // PDA
773        assert_eq!(instruction.accounts[1], AccountMeta::new(signing_key, true)); // signing key (signer)
774        assert_eq!(
775            instruction.accounts[2],
776            AccountMeta::new_readonly(system_program::id(), false)
777        ); // system program
778           // [3] = self-bond PDA (derived from validator_info_pubkey)
779        assert!(instruction.accounts[3].is_writable);
780        assert!(!instruction.accounts[3].is_signer);
781        // [4] = StakeManager program
782        assert_eq!(
783            instruction.accounts[4].pubkey,
784            crate::STAKE_MANAGER_PROGRAM_ID
785        );
786        assert!(!instruction.accounts[4].is_writable);
787        // [5] = ValidatorRegistry program (for ActivateStake → AddStake CPI)
788        assert_eq!(instruction.accounts[5].pubkey, crate::id());
789        assert!(!instruction.accounts[5].is_writable);
790
791        // Deserialize and verify instruction data
792        let decoded_instruction: ValidatorRegistryInstruction =
793            bincode::deserialize(&instruction.data).unwrap();
794
795        match decoded_instruction {
796            ValidatorRegistryInstruction::Register {
797                signing_key: decoded_signing_key,
798                address: decoded_address,
799                state_sync_address: decoded_state_sync_address,
800                hostname: decoded_hostname,
801                authority_key: decoded_authority_key,
802                protocol_key: decoded_protocol_key,
803                network_key: decoded_network_key,
804                commission_rate: decoded_commission_rate,
805                lockup_period: decoded_lockup_period,
806            } => {
807                assert_eq!(decoded_signing_key, signing_key);
808                assert_eq!(decoded_address, address.to_vec());
809                assert_eq!(decoded_state_sync_address, Vec::<u8>::new());
810                assert_eq!(decoded_hostname, hostname);
811                assert_eq!(decoded_authority_key, authority_key);
812                assert_eq!(decoded_protocol_key, protocol_key);
813                assert_eq!(decoded_network_key, network_key);
814                assert_eq!(decoded_commission_rate, commission_rate);
815                assert_eq!(decoded_lockup_period, lockup_period);
816            }
817            _ => panic!("Expected Register instruction"),
818        }
819    }
820
821    /// Helper to create a ValidatorInfo with the given unbonding_periods map.
822    fn vi_with_periods(periods: BTreeMap<u64, u64>) -> ValidatorInfo {
823        ValidatorInfo {
824            signing_key: Pubkey::default(),
825            withdrawal_key: Pubkey::default(),
826            stake: 0,
827            address: vec![],
828            state_sync_address: vec![],
829            hostname: String::new(),
830            authority_key: vec![],
831            protocol_key: Pubkey::default(),
832            network_key: Pubkey::default(),
833            registration_time: 0,
834            last_update: 0,
835            unbonding_periods: periods,
836            lockup_period: 0,
837            commission_rate: 0,
838            new_commission_rate: None,
839            earliest_shutdown: None,
840        }
841    }
842
843    /// Single entry — uses the one period (backward compatible with old behavior).
844    #[test]
845    fn test_end_of_unbonding_single_entry() {
846        let vi = vi_with_periods(BTreeMap::from([(0, 7)]));
847        assert_eq!(vi.end_of_unbonding(100), 107); // 100 + 7
848    }
849
850    /// Empty map — should return deactivation_requested + 0 = deactivation_requested.
851    #[test]
852    fn test_end_of_unbonding_empty_map() {
853        let vi = vi_with_periods(BTreeMap::new());
854        assert_eq!(vi.end_of_unbonding(100), 100); // 100 + 0
855    }
856
857    /// No punishment during unbonding (exempt).
858    ///
859    /// unbonding_periods = { 0: 7, 1000: 30 }
860    /// deactivation_requested = 100
861    /// initial_end = 100 + 7 = 107
862    /// Check entry at 1000: max_end(107) < 1000 → exempt, break
863    /// Result: 107 (original period, not affected by later punishment)
864    #[test]
865    fn test_end_of_unbonding_example1_exempt() {
866        let vi = vi_with_periods(BTreeMap::from([(0, 7), (1000, 30)]));
867        assert_eq!(vi.end_of_unbonding(100), 107);
868    }
869
870    /// Punishment during unbonding (extended).
871    ///
872    /// unbonding_periods = { 0: 7, 50: 30 }
873    /// deactivation_requested = 100
874    /// initial_period = 30 (entry at 50, which is <= 100)
875    /// Result: 100 + 30 = 130
876    #[test]
877    fn test_end_of_unbonding_example2_deactivated_after_punishment() {
878        let vi = vi_with_periods(BTreeMap::from([(0, 7), (50, 30)]));
879        assert_eq!(vi.end_of_unbonding(100), 130);
880    }
881
882    /// Deactivated before punishment, still unbonding when hit.
883    ///
884    /// unbonding_periods = { 0: 7, 105: 30 }
885    /// deactivation_requested = 100
886    /// initial_end = 107, max_end = 107
887    /// Entry at 105: max_end(107) >= 105 → apply. new_end = 130, max_end = 130
888    /// Result: 130 = 100 + 30
889    #[test]
890    fn test_end_of_unbonding_example3_extended_during_unbonding() {
891        let vi = vi_with_periods(BTreeMap::from([(0, 7), (105, 30)]));
892        assert_eq!(vi.end_of_unbonding(100), 130);
893    }
894
895    /// Multiple punishments, partial application.
896    ///
897    /// unbonding_periods = { 0: 5, 104: 10, 200: 30 }
898    /// deactivation_requested = 100
899    /// initial_end = 105, max_end = 105
900    /// Entry at 104: max_end(105) >= 104 → apply. new_end = 110, max_end = 110
901    /// Entry at 200: max_end(110) < 200 → exempt, break
902    /// Result: 110 = 100 + 10
903    #[test]
904    fn test_end_of_unbonding_example4_partial_application() {
905        let vi = vi_with_periods(BTreeMap::from([(0, 5), (104, 10), (200, 30)]));
906        assert_eq!(vi.end_of_unbonding(100), 110);
907    }
908
909    /// Genuine reduction (fairness).
910    ///
911    /// unbonding_periods = { 0: 2, 100: 30, 500: 7 }
912    /// deactivation_requested = 200 (during 30-period phase)
913    /// initial_period = 30 (entry at 100), initial_end = 230
914    /// Entry at 500: max_end(230) >= 500? No (230 < 500) → exempt, break
915    ///
916    /// In that case, 200 + 30 = 230 < 500. So the account IS exempt and gets 230.
917    ///
918    /// This is because in real milliseconds, 30 days = 2,592,000,000 ms >> 500.
919    ///
920    #[test]
921    fn test_end_of_unbonding_example5_reduction_fairness() {
922        let day: u64 = 86_400_000; // 1 day in ms
923        let vi = vi_with_periods(BTreeMap::from([
924            (0, 2 * day),
925            (100, 30 * day),
926            (500, 7 * day),
927        ]));
928        // deactivation at 200 (during 30-day phase)
929        // initial_period = 30 days, initial_end = 200 + 30*day
930        // Entry at 500: max_end(200 + 30*day) >= 500? Yes (huge) → apply
931        //   new_end = 200 + 7*day, max_end = max(200+30d, 200+7d) = 200+30d
932        //   effective_end = 200 + 7*day
933        // Result: 200 + 7 days ← reduction applied! ✅
934        assert_eq!(vi.end_of_unbonding(200), 200 + 7 * day);
935    }
936
937    ///  Mistaken lowering then revert (flash resistance).
938    ///
939    /// unbonding_periods = { 0: 30_days, 100: 0, 101: 30_days }
940    /// deactivation_requested = 50
941    /// initial_end = 50 + 30d, max_end = 50+30d
942    /// Entry at 100: max_end(50+30d) >= 100 → apply. new_end = 50. max_end stays 50+30d.
943    /// Entry at 101: max_end(50+30d) >= 101 → apply. new_end = 50+30d. max_end stays.
944    /// effective_end = 50 + 30d ← punishment preserved! ✅
945    #[test]
946    fn test_end_of_unbonding_example6_flash_resistance() {
947        let day: u64 = 86_400_000;
948        let vi = vi_with_periods(BTreeMap::from([(0, 30 * day), (100, 0), (101, 30 * day)]));
949        assert_eq!(vi.end_of_unbonding(50), 50 + 30 * day);
950    }
951
952    /// Deactivation at exact timestamp of a period change — uses that entry.
953    #[test]
954    fn test_end_of_unbonding_deactivation_at_exact_entry() {
955        let vi = vi_with_periods(BTreeMap::from([(0, 5), (100, 20)]));
956        // Deactivation at exactly 100 → initial_period = 20 (entry at 100 is <= 100)
957        assert_eq!(vi.end_of_unbonding(100), 120);
958    }
959
960    /// Deactivation before any entry — uses 0 as default.
961    #[test]
962    fn test_end_of_unbonding_deactivation_before_first_entry() {
963        let vi = vi_with_periods(BTreeMap::from([(100, 10)]));
964        // Deactivation at 50 — no entry <= 50, so period = 0
965        // initial_end = 50, max_end = 50
966        // Entry at 100: max_end(50) < 100 → exempt
967        assert_eq!(vi.end_of_unbonding(50), 50);
968    }
969
970    /// Multiple reductions — effective_end decreases with each.
971    #[test]
972    fn test_end_of_unbonding_multiple_reductions() {
973        let day: u64 = 86_400_000;
974        let vi = vi_with_periods(BTreeMap::from([
975            (0, 30 * day),
976            (500, 14 * day),
977            (600, 7 * day),
978        ]));
979        // deactivation at 200 (during 30-day phase)
980        // initial_end = 200 + 30d, max_end = 200 + 30d
981        // Entry at 500: apply → new_end = 200 + 14d, max_end stays 200+30d, effective = 200+14d
982        // Entry at 600: apply → new_end = 200 + 7d, max_end stays, effective = 200+7d
983        assert_eq!(vi.end_of_unbonding(200), 200 + 7 * day);
984    }
985
986    /// Punishment after full unbonding — must NOT extend (exempt).
987    #[test]
988    fn test_end_of_unbonding_punishment_after_full_unbonding() {
989        let vi = vi_with_periods(BTreeMap::from([(0, 5), (200, 30)]));
990        // deactivation at 100, initial_end = 105
991        // Entry at 200: max_end(105) < 200 → exempt
992        assert_eq!(vi.end_of_unbonding(100), 105);
993    }
994
995    /// Increasing punishment chain — each extends further.
996    #[test]
997    fn test_end_of_unbonding_increasing_punishments() {
998        let vi = vi_with_periods(BTreeMap::from([(0, 5), (104, 10), (109, 20)]));
999        // deactivation at 100, initial_end = 105
1000        // Entry at 104: max_end(105) >= 104 → apply. new_end = 110, max_end = 110
1001        // Entry at 109: max_end(110) >= 109 → apply. new_end = 120, max_end = 120
1002        assert_eq!(vi.end_of_unbonding(100), 120);
1003    }
1004
1005    #[test]
1006    fn test_update_withdrawer_instruction() {
1007        let (validator_info_pubkey, current_withdrawer, new_withdrawer) = create_test_pubkeys();
1008
1009        // Create the update withdrawer instruction
1010        let instruction = ValidatorRegistryInstruction::update_withdrawer(
1011            validator_info_pubkey,
1012            current_withdrawer,
1013            new_withdrawer,
1014        );
1015
1016        // Verify the program ID
1017        assert_eq!(instruction.program_id, crate::id());
1018
1019        // Expected accounts:
1020        // Note: validator_info_pubkey is a PDA and cannot sign
1021        let expected_accounts = vec![
1022            AccountMeta::new(validator_info_pubkey, false), // validator info account (PDA, writable, NOT a signer)
1023            AccountMeta::new_readonly(current_withdrawer, true), // current withdrawer (signer, readonly)
1024            AccountMeta::new_readonly(new_withdrawer, false),    // new withdrawer (readonly)
1025        ];
1026
1027        // Verify accounts
1028        assert_eq!(instruction.accounts, expected_accounts);
1029
1030        // Deserialize and verify instruction data
1031        let decoded_instruction: ValidatorRegistryInstruction =
1032            bincode::deserialize(&instruction.data).unwrap();
1033
1034        match decoded_instruction {
1035            ValidatorRegistryInstruction::UpdateWithdrawer { new_withdrawal_key } => {
1036                assert_eq!(new_withdrawal_key, new_withdrawer);
1037            }
1038            _ => panic!("Expected UpdateWithdrawer instruction"),
1039        }
1040    }
1041
1042    #[test]
1043    fn test_change_earliest_shutdown_instruction() {
1044        let validator_info_pubkey = Pubkey::new_unique();
1045        let signing_key = Pubkey::new_unique();
1046        let shutdown_ts = 1_000_000u64;
1047
1048        // Test with Some(timestamp)
1049        let instruction = ValidatorRegistryInstruction::change_earliest_shutdown(
1050            validator_info_pubkey,
1051            signing_key,
1052            Some(shutdown_ts),
1053        );
1054
1055        assert_eq!(instruction.program_id, crate::id());
1056        assert_eq!(instruction.accounts.len(), 2);
1057        assert_eq!(instruction.accounts[0].pubkey, validator_info_pubkey);
1058        assert!(!instruction.accounts[0].is_signer);
1059        assert!(instruction.accounts[0].is_writable);
1060        assert_eq!(instruction.accounts[1].pubkey, signing_key);
1061        assert!(instruction.accounts[1].is_signer);
1062        assert!(!instruction.accounts[1].is_writable);
1063
1064        let decoded: ValidatorRegistryInstruction =
1065            bincode::deserialize(&instruction.data).unwrap();
1066        assert_eq!(
1067            decoded,
1068            ValidatorRegistryInstruction::ChangeEarliestShutdown {
1069                earliest_shutdown: Some(shutdown_ts),
1070            }
1071        );
1072
1073        // Test with None
1074        let instruction_none = ValidatorRegistryInstruction::change_earliest_shutdown(
1075            validator_info_pubkey,
1076            signing_key,
1077            None,
1078        );
1079
1080        let decoded_none: ValidatorRegistryInstruction =
1081            bincode::deserialize(&instruction_none.data).unwrap();
1082        assert_eq!(
1083            decoded_none,
1084            ValidatorRegistryInstruction::ChangeEarliestShutdown {
1085                earliest_shutdown: None,
1086            }
1087        );
1088    }
1089
1090    #[test]
1091    fn test_update_signer_instruction() {
1092        let validator_info_pubkey = Pubkey::new_unique();
1093        let current_signing_key = Pubkey::new_unique();
1094        let new_signing_key = Pubkey::new_unique();
1095
1096        let instruction = ValidatorRegistryInstruction::update_signer(
1097            validator_info_pubkey,
1098            current_signing_key,
1099            new_signing_key,
1100        );
1101
1102        // Verify program ID
1103        assert_eq!(instruction.program_id, crate::id());
1104
1105        // Verify accounts: 3 accounts expected
1106        assert_eq!(instruction.accounts.len(), 3);
1107
1108        // Account 0: validator info PDA (writable, not signer)
1109        assert_eq!(instruction.accounts[0].pubkey, validator_info_pubkey);
1110        assert!(!instruction.accounts[0].is_signer);
1111        assert!(instruction.accounts[0].is_writable);
1112
1113        // Account 1: current signing key (signer, not writable)
1114        assert_eq!(instruction.accounts[1].pubkey, current_signing_key);
1115        assert!(instruction.accounts[1].is_signer);
1116        assert!(!instruction.accounts[1].is_writable);
1117
1118        // Account 2: new signing key (not signer, not writable)
1119        assert_eq!(instruction.accounts[2].pubkey, new_signing_key);
1120        assert!(!instruction.accounts[2].is_signer);
1121        assert!(!instruction.accounts[2].is_writable);
1122
1123        // Verify instruction data deserializes correctly
1124        let decoded: ValidatorRegistryInstruction =
1125            bincode::deserialize(&instruction.data).unwrap();
1126        assert_eq!(
1127            decoded,
1128            ValidatorRegistryInstruction::UpdateSigner { new_signing_key },
1129        );
1130    }
1131}