|
| 1 | +//! Zero-copy stake state types. |
| 2 | +
|
| 3 | +use { |
| 4 | + crate::{ |
| 5 | + error::StakeStateError, |
| 6 | + pod::{PodAddress, PodI64, PodU32, PodU64}, |
| 7 | + }, |
| 8 | + core::mem::size_of, |
| 9 | + wincode::{SchemaRead, SchemaWrite, ZeroCopy}, |
| 10 | +}; |
| 11 | + |
| 12 | +#[repr(C)] |
| 13 | +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, SchemaWrite, SchemaRead)] |
| 14 | +#[wincode(assert_zero_copy)] |
| 15 | +pub struct Authorized { |
| 16 | + pub staker: PodAddress, |
| 17 | + pub withdrawer: PodAddress, |
| 18 | +} |
| 19 | + |
| 20 | +#[repr(C)] |
| 21 | +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, SchemaWrite, SchemaRead)] |
| 22 | +#[wincode(assert_zero_copy)] |
| 23 | +pub struct Lockup { |
| 24 | + /// `UnixTimestamp` at which this stake will allow withdrawal, unless the |
| 25 | + /// transaction is signed by the custodian. |
| 26 | + pub unix_timestamp: PodI64, |
| 27 | + /// Epoch height at which this stake will allow withdrawal, unless the |
| 28 | + /// transaction is signed by the custodian. |
| 29 | + pub epoch: PodU64, |
| 30 | + /// Custodian signature on a transaction exempts the operation from |
| 31 | + /// lockup constraints. |
| 32 | + pub custodian: PodAddress, |
| 33 | +} |
| 34 | + |
| 35 | +#[repr(C)] |
| 36 | +#[derive(Clone, Copy, Debug, PartialEq, Eq, SchemaWrite, SchemaRead, Default)] |
| 37 | +#[wincode(assert_zero_copy)] |
| 38 | +pub struct Meta { |
| 39 | + pub rent_exempt_reserve: PodU64, |
| 40 | + pub authorized: Authorized, |
| 41 | + pub lockup: Lockup, |
| 42 | +} |
| 43 | + |
| 44 | +#[repr(C)] |
| 45 | +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, SchemaWrite, SchemaRead)] |
| 46 | +#[wincode(assert_zero_copy)] |
| 47 | +pub struct Delegation { |
| 48 | + /// To whom the stake is delegated. |
| 49 | + pub voter_pubkey: PodAddress, |
| 50 | + /// Activated stake amount, set at delegate() time. |
| 51 | + pub stake: PodU64, |
| 52 | + /// Epoch at which this stake was activated, `u64::MAX` if is a bootstrap stake. |
| 53 | + pub activation_epoch: PodU64, |
| 54 | + /// Epoch the stake was deactivated, `u64::MAX` if not deactivated. |
| 55 | + pub deactivation_epoch: PodU64, |
| 56 | + /// Reserved bytes (formerly warmup/cooldown rate). |
| 57 | + /// Deprecated in the runtime but preserved for ABI compatibility. |
| 58 | + pub _reserved: [u8; 8], |
| 59 | +} |
| 60 | + |
| 61 | +#[repr(C)] |
| 62 | +#[derive(Clone, Copy, Debug, PartialEq, Eq, SchemaWrite, SchemaRead, Default)] |
| 63 | +#[wincode(assert_zero_copy)] |
| 64 | +pub struct Stake { |
| 65 | + pub delegation: Delegation, |
| 66 | + /// Credits observed is credits from vote account state when delegated or redeemed. |
| 67 | + pub credits_observed: PodU64, |
| 68 | +} |
| 69 | + |
| 70 | +/// Discriminant tag for stake account state (first 4 bytes). |
| 71 | +#[repr(u32)] |
| 72 | +#[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 73 | +pub enum StakeStateV2Tag { |
| 74 | + Uninitialized = 0, |
| 75 | + Initialized = 1, |
| 76 | + Stake = 2, |
| 77 | + RewardsPool = 3, |
| 78 | +} |
| 79 | + |
| 80 | +impl StakeStateV2Tag { |
| 81 | + pub const TAG_LEN: usize = size_of::<PodU32>(); |
| 82 | + |
| 83 | + #[inline] |
| 84 | + pub(crate) fn from_u32(v: u32) -> Result<Self, StakeStateError> { |
| 85 | + match v { |
| 86 | + 0 => Ok(Self::Uninitialized), |
| 87 | + 1 => Ok(Self::Initialized), |
| 88 | + 2 => Ok(Self::Stake), |
| 89 | + 3 => Ok(Self::RewardsPool), |
| 90 | + other => Err(StakeStateError::InvalidTag(other)), |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + #[inline] |
| 95 | + pub fn from_bytes(bytes: &[u8]) -> Result<Self, StakeStateError> { |
| 96 | + if bytes.len() < 4 { |
| 97 | + return Err(StakeStateError::UnexpectedEof); |
| 98 | + } |
| 99 | + let raw = u32::from_le_bytes(bytes[..4].try_into().unwrap()); |
| 100 | + Self::from_u32(raw) |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +/// 200-byte stake account layout: |
| 105 | +/// |
| 106 | +/// ```text |
| 107 | +/// ┌────────┬──────┬────────────┐ |
| 108 | +/// │ Offset │ Size │ Field │ |
| 109 | +/// ├────────┼──────┼────────────┤ |
| 110 | +/// │ 0 │ 4 │ Tag │ |
| 111 | +/// │ 4 │ 120 │ Meta │ |
| 112 | +/// │ 124 │ 72 │ Stake │ |
| 113 | +/// │ 196 │ 1 │ StakeFlags │ |
| 114 | +/// │ 197 │ 3 │ Padding │ |
| 115 | +/// └────────┴──────┴────────────┘ |
| 116 | +/// ``` |
| 117 | +/// |
| 118 | +/// All fields are alignment-1 for safe zero-copy from unaligned byte slices. |
| 119 | +#[repr(C)] |
| 120 | +#[derive(Clone, Copy, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] |
| 121 | +#[wincode(assert_zero_copy)] |
| 122 | +pub struct StakeStateV2 { |
| 123 | + tag: PodU32, |
| 124 | + meta: Meta, |
| 125 | + stake: Stake, |
| 126 | + stake_flags: u8, |
| 127 | + padding: [u8; 3], |
| 128 | +} |
| 129 | + |
| 130 | +// compile-time size check |
| 131 | +const _: () = assert!(size_of::<StakeStateV2>() == 200); |
| 132 | + |
| 133 | +impl StakeStateV2 { |
| 134 | + /// The fixed size of a stake account in bytes. |
| 135 | + pub const LEN: usize = size_of::<StakeStateV2>(); |
| 136 | + |
| 137 | + /// Parse stake account data into a read-only reference. |
| 138 | + pub fn from_bytes(data: &[u8]) -> Result<&Self, StakeStateError> { |
| 139 | + if data.len() < Self::LEN { |
| 140 | + return Err(StakeStateError::UnexpectedEof); |
| 141 | + } |
| 142 | + let state = <Self as ZeroCopy>::from_bytes(data)?; |
| 143 | + StakeStateV2Tag::from_u32(state.tag.get())?; |
| 144 | + Ok(state) |
| 145 | + } |
| 146 | + |
| 147 | + /// Parse stake account data into a mutable reference. |
| 148 | + pub fn from_bytes_mut(data: &mut [u8]) -> Result<&mut Self, StakeStateError> { |
| 149 | + if data.len() < Self::LEN { |
| 150 | + return Err(StakeStateError::UnexpectedEof); |
| 151 | + } |
| 152 | + let state = <Self as ZeroCopy>::from_bytes_mut(data)?; |
| 153 | + StakeStateV2Tag::from_u32(state.tag.get())?; |
| 154 | + Ok(state) |
| 155 | + } |
| 156 | + |
| 157 | + /// Returns the state tag (infallible since validated at construction). |
| 158 | + #[inline] |
| 159 | + pub fn tag(&self) -> StakeStateV2Tag { |
| 160 | + StakeStateV2Tag::from_u32(self.tag.get()).unwrap() |
| 161 | + } |
| 162 | + |
| 163 | + /// Returns a reference to `Meta` if in the `Initialized` or `Stake` state. |
| 164 | + #[inline] |
| 165 | + pub fn meta(&self) -> Option<&Meta> { |
| 166 | + match self.tag() { |
| 167 | + StakeStateV2Tag::Initialized | StakeStateV2Tag::Stake => Some(&self.meta), |
| 168 | + _ => None, |
| 169 | + } |
| 170 | + } |
| 171 | + |
| 172 | + /// Returns a reference to `Stake` if in the `Stake` state. |
| 173 | + #[inline] |
| 174 | + pub fn stake(&self) -> Option<&Stake> { |
| 175 | + match self.tag() { |
| 176 | + StakeStateV2Tag::Stake => Some(&self.stake), |
| 177 | + _ => None, |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | + /// Returns a mutable reference to `Meta` if in the `Initialized` or `Stake` state. |
| 182 | + #[inline] |
| 183 | + pub fn meta_mut(&mut self) -> Result<&mut Meta, StakeStateError> { |
| 184 | + match self.tag() { |
| 185 | + StakeStateV2Tag::Initialized | StakeStateV2Tag::Stake => Ok(&mut self.meta), |
| 186 | + tag => Err(StakeStateError::InvalidStateAccess(tag)), |
| 187 | + } |
| 188 | + } |
| 189 | + |
| 190 | + /// Returns a mutable reference to `Stake` if in the `Stake` state. |
| 191 | + #[inline] |
| 192 | + pub fn stake_mut(&mut self) -> Result<&mut Stake, StakeStateError> { |
| 193 | + match self.tag() { |
| 194 | + StakeStateV2Tag::Stake => Ok(&mut self.stake), |
| 195 | + tag => Err(StakeStateError::InvalidStateAccess(tag)), |
| 196 | + } |
| 197 | + } |
| 198 | + |
| 199 | + /// Transition from `Uninitialized` to `Initialized`. |
| 200 | + /// Clears the stake and tail regions. |
| 201 | + pub fn initialize(&mut self, meta: Meta) -> Result<(), StakeStateError> { |
| 202 | + let from = self.tag(); |
| 203 | + if from != StakeStateV2Tag::Uninitialized { |
| 204 | + return Err(StakeStateError::InvalidTransition { |
| 205 | + from, |
| 206 | + to: StakeStateV2Tag::Initialized, |
| 207 | + }); |
| 208 | + } |
| 209 | + |
| 210 | + self.stake = Stake::default(); |
| 211 | + self.stake_flags = 0; |
| 212 | + self.padding.fill(0); |
| 213 | + self.meta = meta; |
| 214 | + self.tag.set(StakeStateV2Tag::Initialized as u32); |
| 215 | + |
| 216 | + Ok(()) |
| 217 | + } |
| 218 | + |
| 219 | + /// Transition to `Stake` state from `Initialized` or `Stake`. |
| 220 | + /// - From `Initialized`: clears tail region to zero. |
| 221 | + /// - From `Stake`: preserves existing tail region. |
| 222 | + pub fn delegate(&mut self, meta: Meta, stake: Stake) -> Result<(), StakeStateError> { |
| 223 | + let from = self.tag(); |
| 224 | + if !matches!(from, StakeStateV2Tag::Initialized | StakeStateV2Tag::Stake) { |
| 225 | + return Err(StakeStateError::InvalidTransition { |
| 226 | + from, |
| 227 | + to: StakeStateV2Tag::Stake, |
| 228 | + }); |
| 229 | + } |
| 230 | + |
| 231 | + if from == StakeStateV2Tag::Initialized { |
| 232 | + self.stake_flags = 0; |
| 233 | + self.padding.fill(0); |
| 234 | + } |
| 235 | + |
| 236 | + self.meta = meta; |
| 237 | + self.stake = stake; |
| 238 | + self.tag.set(StakeStateV2Tag::Stake as u32); |
| 239 | + |
| 240 | + Ok(()) |
| 241 | + } |
| 242 | +} |
0 commit comments