Skip to main content

hotmint_staking/
manager.rs

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