Skip to main content

hotmint_staking/
manager.rs

1use ruc::*;
2
3use std::cmp;
4
5use hotmint_types::crypto::PublicKey;
6use hotmint_types::validator::{ValidatorId, ValidatorSet};
7use hotmint_types::validator_update::ValidatorUpdate;
8
9use crate::rewards;
10use crate::store::StakingStore;
11use crate::types::{
12    SlashReason, SlashResult, StakeEntry, StakingConfig, UnbondingEntry, ValidatorState,
13};
14
15/// Central staking manager that operates on any [`StakingStore`] backend.
16///
17/// Use this inside your `Application::execute_block` to process staking
18/// transactions, distribute rewards, apply slashing, and compute validator
19/// set updates for epoch transitions.
20pub struct StakingManager<S: StakingStore> {
21    store: S,
22    config: StakingConfig,
23}
24
25impl<S: StakingStore> StakingManager<S> {
26    pub fn new(store: S, config: StakingConfig) -> Self {
27        Self { store, config }
28    }
29
30    pub fn config(&self) -> &StakingConfig {
31        &self.config
32    }
33
34    pub fn store(&self) -> &S {
35        &self.store
36    }
37
38    pub fn store_mut(&mut self) -> &mut S {
39        &mut self.store
40    }
41
42    // ── Validator registration ─────────────────────────────────────
43
44    /// Register a new validator with an initial self-stake.
45    pub fn register_validator(
46        &mut self,
47        id: ValidatorId,
48        pubkey: PublicKey,
49        self_stake: u64,
50    ) -> Result<()> {
51        if self.store.get_validator(id).is_some() {
52            return Err(eg!("validator {} already registered", id));
53        }
54        if self_stake < self.config.min_self_stake {
55            return Err(eg!(
56                "self-stake {} below minimum {}",
57                self_stake,
58                self.config.min_self_stake
59            ));
60        }
61
62        let state = ValidatorState {
63            id,
64            public_key: pubkey,
65            self_stake,
66            delegated_stake: 0,
67            score: self.config.initial_score,
68            jailed: false,
69            jail_until_height: 0,
70        };
71        self.store.set_validator(id, state);
72        Ok(())
73    }
74
75    /// Remove a validator from the staking system entirely.
76    /// All delegated stakes are returned (caller handles balance credits).
77    pub fn unregister_validator(&mut self, id: ValidatorId) -> Result<u64> {
78        let state = self
79            .store
80            .get_validator(id)
81            .ok_or_else(|| eg!("validator {} not found", id))?;
82        if state.jailed {
83            return Err(eg!("cannot unregister validator {} while jailed", id));
84        }
85        let total = state.total_stake();
86        // Remove all delegation entries
87        let stakers = self.store.stakers_of(id);
88        for (addr, _) in stakers {
89            self.store.remove_stake(&addr, id);
90        }
91        self.store.remove_validator(id);
92        Ok(total)
93    }
94
95    // ── Delegation ─────────────────────────────────────────────────
96
97    /// Delegate `amount` from `staker` to `validator`.
98    pub fn delegate(&mut self, staker: &[u8], validator: ValidatorId, amount: u64) -> Result<()> {
99        if amount == 0 {
100            return Err(eg!("cannot delegate zero amount"));
101        }
102        let mut vs = self
103            .store
104            .get_validator(validator)
105            .ok_or_else(|| eg!("validator {} not found", validator))?;
106
107        vs.delegated_stake = vs
108            .delegated_stake
109            .checked_add(amount)
110            .ok_or_else(|| eg!("delegated stake overflow for validator {}", validator))?;
111        self.store.set_validator(validator, vs);
112
113        let mut entry = self
114            .store
115            .get_stake(staker, validator)
116            .unwrap_or(StakeEntry { amount: 0 });
117        entry.amount = entry
118            .amount
119            .checked_add(amount)
120            .ok_or_else(|| eg!("stake entry overflow"))?;
121        self.store.set_stake(staker, validator, entry);
122        Ok(())
123    }
124
125    /// Undelegate `amount` from `staker`'s delegation to `validator`.
126    ///
127    /// Voting power is reduced immediately. If `unbonding_period > 0`, the
128    /// tokens are locked in an unbonding queue and released only after
129    /// [`Self::process_unbonding`] is called at or after `current_height + unbonding_period`.
130    pub fn undelegate(
131        &mut self,
132        staker: &[u8],
133        validator: ValidatorId,
134        amount: u64,
135        current_height: u64,
136    ) -> Result<()> {
137        if amount == 0 {
138            return Err(eg!("cannot undelegate zero amount"));
139        }
140        let mut vs = self
141            .store
142            .get_validator(validator)
143            .ok_or_else(|| eg!("validator {} not found", validator))?;
144        let mut entry = self
145            .store
146            .get_stake(staker, validator)
147            .ok_or_else(|| eg!("no stake from staker to validator {}", validator))?;
148
149        if entry.amount < amount {
150            return Err(eg!(
151                "insufficient delegation: have {}, requested {}",
152                entry.amount,
153                amount
154            ));
155        }
156
157        entry.amount -= amount;
158        vs.delegated_stake = vs.delegated_stake.saturating_sub(amount);
159
160        if entry.amount == 0 {
161            self.store.remove_stake(staker, validator);
162        } else {
163            self.store.set_stake(staker, validator, entry);
164        }
165        self.store.set_validator(validator, vs);
166
167        // Queue unbonding entry
168        let completion_height = current_height.saturating_add(self.config.unbonding_period);
169        self.store.push_unbonding(UnbondingEntry {
170            staker: staker.to_vec(),
171            validator,
172            amount,
173            completion_height,
174        });
175
176        Ok(())
177    }
178
179    /// Process mature unbondings whose lock period has elapsed.
180    ///
181    /// Returns the completed entries so the application can credit the
182    /// released tokens to the stakers' balances.
183    pub fn process_unbonding(&mut self, current_height: u64) -> Vec<UnbondingEntry> {
184        self.store.drain_mature_unbondings(current_height)
185    }
186
187    // ── Slashing ───────────────────────────────────────────────────
188
189    /// Slash a validator for misbehavior.
190    ///
191    /// Reduces self-stake and delegated stakes proportionally, jails the
192    /// validator, and returns the total slashed amount.
193    pub fn slash(
194        &mut self,
195        id: ValidatorId,
196        reason: SlashReason,
197        current_height: u64,
198    ) -> Result<SlashResult> {
199        let mut vs = self
200            .store
201            .get_validator(id)
202            .ok_or_else(|| eg!("validator {} not found", id))?;
203
204        if vs.jailed {
205            return Err(eg!(
206                "validator {} already jailed, refusing double slash",
207                id
208            ));
209        }
210
211        let rate = match reason {
212            SlashReason::DoubleSign => self.config.slash_rate_double_sign,
213            SlashReason::Downtime => self.config.slash_rate_downtime,
214        };
215
216        let self_slash = (vs.self_stake as u128 * rate as u128 / 10_000) as u64;
217        let del_slash = (vs.delegated_stake as u128 * rate as u128 / 10_000) as u64;
218
219        vs.self_stake = vs.self_stake.saturating_sub(self_slash);
220        vs.delegated_stake = vs.delegated_stake.saturating_sub(del_slash);
221        vs.jailed = true;
222        vs.jail_until_height = current_height.saturating_add(self.config.jail_duration);
223        vs.score = vs.score.saturating_sub(self.config.max_score / 10);
224
225        // Proportionally reduce each staker's delegation.
226        // The last staker absorbs any rounding remainder so that
227        // sum(staker.amount) == vs.delegated_stake after slashing.
228        if del_slash > 0 {
229            let stakers = self.store.stakers_of(id);
230            let total_del: u64 = stakers.iter().map(|(_, e)| e.amount).sum();
231            if total_del > 0 {
232                let count = stakers.len();
233                let mut slashed_so_far = 0u64;
234                for (i, (addr, mut entry)) in stakers.into_iter().enumerate() {
235                    let staker_slash = if i == count - 1 {
236                        // Last staker absorbs remainder
237                        del_slash.saturating_sub(slashed_so_far)
238                    } else {
239                        (entry.amount as u128 * del_slash as u128 / total_del as u128) as u64
240                    };
241                    slashed_so_far = slashed_so_far.saturating_add(staker_slash);
242                    entry.amount = entry.amount.saturating_sub(staker_slash);
243                    if entry.amount == 0 {
244                        self.store.remove_stake(&addr, id);
245                    } else {
246                        self.store.set_stake(&addr, id, entry);
247                    }
248                }
249            }
250        }
251
252        self.store.set_validator(id, vs);
253
254        // Slash pending unbondings for this validator at the same rate
255        let unbondings = self.store.all_unbondings();
256        let mut unbonding_slashed = 0u64;
257        let updated: Vec<UnbondingEntry> = unbondings
258            .into_iter()
259            .map(|mut e| {
260                if e.validator == id && e.amount > 0 {
261                    let ub_slash = (e.amount as u128 * rate as u128 / 10_000) as u64;
262                    e.amount = e.amount.saturating_sub(ub_slash);
263                    unbonding_slashed = unbonding_slashed.saturating_add(ub_slash);
264                }
265                e
266            })
267            .filter(|e| e.amount > 0)
268            .collect();
269        self.store.replace_unbondings(updated);
270
271        Ok(SlashResult {
272            self_slashed: self_slash,
273            delegated_slashed: del_slash.saturating_add(unbonding_slashed),
274            jailed: true,
275        })
276    }
277
278    /// Unjail a validator if the jail period has passed.
279    pub fn unjail(&mut self, id: ValidatorId, current_height: u64) -> Result<()> {
280        let mut vs = self
281            .store
282            .get_validator(id)
283            .ok_or_else(|| eg!("validator {} not found", id))?;
284        if !vs.jailed {
285            return Err(eg!("validator {} is not jailed", id));
286        }
287        if current_height < vs.jail_until_height {
288            return Err(eg!(
289                "validator {} jailed until height {}, current {}",
290                id,
291                vs.jail_until_height,
292                current_height
293            ));
294        }
295        vs.jailed = false;
296        vs.jail_until_height = 0;
297        self.store.set_validator(id, vs);
298        Ok(())
299    }
300
301    // ── Reputation score ───────────────────────────────────────────
302
303    /// Increase a validator's reputation score.
304    pub fn increment_score(&mut self, id: ValidatorId, delta: u32) {
305        if let Some(mut vs) = self.store.get_validator(id) {
306            vs.score = vs.score.saturating_add(delta).min(self.config.max_score);
307            self.store.set_validator(id, vs);
308        }
309    }
310
311    /// Decrease a validator's reputation score.
312    pub fn decrement_score(&mut self, id: ValidatorId, delta: u32) {
313        if let Some(mut vs) = self.store.get_validator(id) {
314            vs.score = vs.score.saturating_sub(delta);
315            self.store.set_validator(id, vs);
316        }
317    }
318
319    // ── Rewards ────────────────────────────────────────────────────
320
321    /// Add the configured block reward to the proposer's self-stake.
322    /// Returns the reward amount.
323    pub fn reward_proposer(&mut self, proposer: ValidatorId) -> Result<u64> {
324        let reward = rewards::proposer_reward(&self.config);
325        if reward == 0 {
326            return Ok(0);
327        }
328        let mut vs = self
329            .store
330            .get_validator(proposer)
331            .ok_or_else(|| eg!("proposer {} not found", proposer))?;
332        vs.self_stake = vs
333            .self_stake
334            .checked_add(reward)
335            .ok_or_else(|| eg!("stake overflow on reward"))?;
336        self.store.set_validator(proposer, vs);
337        Ok(reward)
338    }
339
340    // ── Queries ────────────────────────────────────────────────────
341
342    pub fn get_validator(&self, id: ValidatorId) -> Option<ValidatorState> {
343        self.store.get_validator(id)
344    }
345
346    pub fn voting_power(&self, id: ValidatorId) -> u64 {
347        self.store
348            .get_validator(id)
349            .map(|vs| vs.voting_power())
350            .unwrap_or(0)
351    }
352
353    pub fn total_staked(&self) -> u64 {
354        self.store
355            .all_validator_ids()
356            .into_iter()
357            .filter_map(|id| self.store.get_validator(id))
358            .map(|vs| vs.total_stake())
359            .fold(0u64, u64::saturating_add)
360    }
361
362    /// Return the top `max_validators` validators sorted by voting power (descending).
363    /// Jailed validators are excluded.
364    pub fn formal_validator_list(&self) -> Vec<ValidatorState> {
365        let mut active: Vec<ValidatorState> = self
366            .store
367            .all_validator_ids()
368            .into_iter()
369            .filter_map(|id| self.store.get_validator(id))
370            .filter(|vs| !vs.jailed && vs.self_stake >= self.config.min_self_stake)
371            .collect();
372        active.sort_by_key(|v| cmp::Reverse(v.voting_power()));
373        active.truncate(self.config.max_validators);
374        active
375    }
376
377    // ── Epoch integration ──────────────────────────────────────────
378
379    /// Compare current staking state against the active `ValidatorSet` and
380    /// produce the list of [`ValidatorUpdate`]s needed to synchronize them.
381    ///
382    /// This is intended to be called at the end of `execute_block` and
383    /// returned in [`EndBlockResponse::validator_updates`](hotmint_types::EndBlockResponse::validator_updates).
384    pub fn compute_validator_updates(&self, current_set: &ValidatorSet) -> Vec<ValidatorUpdate> {
385        let formal = self.formal_validator_list();
386        let mut updates = Vec::new();
387
388        // Add or update validators in the formal list
389        for vs in &formal {
390            let new_power = vs.voting_power();
391            let current_power = current_set.power_of(vs.id);
392            if new_power != current_power {
393                updates.push(ValidatorUpdate {
394                    id: vs.id,
395                    public_key: vs.public_key.clone(),
396                    power: new_power,
397                });
398            }
399        }
400
401        // Remove validators no longer in the formal list
402        for vi in current_set.validators() {
403            if !formal.iter().any(|f| f.id == vi.id) {
404                updates.push(ValidatorUpdate {
405                    id: vi.id,
406                    public_key: vi.public_key.clone(),
407                    power: 0,
408                });
409            }
410        }
411
412        updates
413    }
414}