dusk_core/
stake.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) DUSK NETWORK. All rights reserved.
6
7//! Types used by Dusk's stake contract.
8
9#[cfg(feature = "serde")]
10use serde_with::{hex::Hex, serde_as, DisplayFromStr};
11
12use alloc::string::String;
13use alloc::vec::Vec;
14
15use bytecheck::CheckBytes;
16use dusk_bytes::Serializable;
17use piecrust_uplink::CONTRACT_ID_BYTES;
18use rkyv::{Archive, Deserialize, Serialize};
19
20use crate::abi::ContractId;
21use crate::signatures::bls::{
22    PublicKey as BlsPublicKey, SecretKey as BlsSecretKey,
23    Signature as BlsSignature,
24};
25use crate::transfer::withdraw::Withdraw as TransferWithdraw;
26use crate::{dusk, Dusk};
27
28/// ID of the genesis stake contract
29pub const STAKE_CONTRACT: ContractId = crate::reserved(0x2);
30
31/// Epoch used for stake operations
32pub const EPOCH: u64 = 2160;
33
34/// Default number of warnings before being penalized
35pub const DEFAULT_STAKE_WARNINGS: u8 = 1;
36
37/// The minimum amount of Dusk one can stake.
38#[deprecated(
39    since = "0.1.0",
40    note = "please use `DEFAULT_MINIMUM_STAKE` instead"
41)]
42pub const MINIMUM_STAKE: Dusk = DEFAULT_MINIMUM_STAKE;
43
44/// The default minimum amount of Dusk one can stake.
45pub const DEFAULT_MINIMUM_STAKE: Dusk = dusk(1_000.0);
46
47/// Configuration for the stake contract
48#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
49#[archive_attr(derive(CheckBytes))]
50#[cfg_attr(feature = "serde", cfg_eval, serde_as)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub struct StakeConfig {
53    /// Number of warnings before being penalized
54    pub warnings: u8,
55    /// Minimum amount of Dusk that can be staked
56    #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))]
57    pub minimum_stake: Dusk,
58}
59
60impl StakeConfig {
61    /// Create a new default stake configuration.
62    #[must_use]
63    pub const fn new() -> Self {
64        Self {
65            warnings: DEFAULT_STAKE_WARNINGS,
66            minimum_stake: DEFAULT_MINIMUM_STAKE,
67        }
68    }
69}
70
71impl Default for StakeConfig {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77/// Calculate the block height at which the next epoch takes effect.
78#[must_use]
79pub const fn next_epoch(block_height: u64) -> u64 {
80    let to_next_epoch = EPOCH - (block_height % EPOCH);
81    block_height + to_next_epoch
82}
83
84/// Stake a value on the stake contract.
85#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)]
86#[archive_attr(derive(CheckBytes))]
87#[cfg_attr(feature = "serde", cfg_eval, serde_as)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89pub struct Stake {
90    chain_id: u8,
91    keys: StakeKeys,
92    #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))]
93    value: u64,
94    signature: DoubleSignature,
95}
96
97impl Stake {
98    const MESSAGE_SIZE: usize =
99        1 + BlsPublicKey::SIZE + BlsPublicKey::SIZE + u64::SIZE;
100
101    /// Create a new stake specifying the owner.
102    #[must_use]
103    pub fn new(
104        account_sk: &BlsSecretKey,
105        owner_sk: &BlsSecretKey,
106        value: u64,
107        chain_id: u8,
108    ) -> Self {
109        let account = BlsPublicKey::from(account_sk);
110        let owner = BlsPublicKey::from(owner_sk);
111
112        let keys = StakeKeys::new(account, owner);
113
114        let mut stake = Stake {
115            chain_id,
116            keys,
117            value,
118            signature: DoubleSignature::default(),
119        };
120
121        let msg = stake.signature_message();
122
123        stake.signature = DoubleSignature {
124            account: account_sk.sign(&msg),
125            owner: owner_sk.sign(&msg),
126        };
127
128        stake
129    }
130
131    /// Create a new stake from a contract.
132    #[must_use]
133    pub fn new_from_contract(
134        sk: &BlsSecretKey,
135        contract: ContractId,
136        value: u64,
137        chain_id: u8,
138    ) -> Self {
139        let key = BlsPublicKey::from(sk);
140
141        let keys = StakeKeys::new(key, contract);
142
143        let mut stake = Stake {
144            chain_id,
145            keys,
146            value,
147            signature: DoubleSignature::default(),
148        };
149
150        let msg = stake.signature_message();
151
152        stake.signature = DoubleSignature {
153            account: sk.sign(&msg),
154            owner: sk.sign(&msg),
155        };
156
157        stake
158    }
159
160    /// Account to which the stake will belong.
161    #[must_use]
162    pub fn keys(&self) -> &StakeKeys {
163        &self.keys
164    }
165
166    /// Account to which the stake will belong.
167    #[must_use]
168    pub fn account(&self) -> &BlsPublicKey {
169        &self.keys.account
170    }
171
172    /// Value to stake.
173    #[must_use]
174    pub fn value(&self) -> u64 {
175        self.value
176    }
177
178    /// Returns the chain ID of the stake.
179    #[must_use]
180    pub fn chain_id(&self) -> u8 {
181        self.chain_id
182    }
183
184    /// Signature of the stake.
185    #[must_use]
186    pub fn signature(&self) -> &DoubleSignature {
187        &self.signature
188    }
189
190    /// Return the message that is used as the input to the signature.
191    #[must_use]
192    pub fn signature_message(&self) -> [u8; Self::MESSAGE_SIZE] {
193        let mut bytes = [0u8; Self::MESSAGE_SIZE];
194
195        bytes[0] = self.chain_id;
196        let mut offset = 1;
197
198        bytes[offset..offset + BlsPublicKey::SIZE]
199            .copy_from_slice(&self.keys.account.to_bytes());
200        offset += BlsPublicKey::SIZE;
201
202        match &self.keys.owner {
203            StakeFundOwner::Account(key) => bytes
204                [offset..offset + BlsPublicKey::SIZE]
205                .copy_from_slice(&key.to_bytes()),
206            StakeFundOwner::Contract(contract_id) => bytes
207                [offset..offset + CONTRACT_ID_BYTES]
208                .copy_from_slice(&contract_id.to_bytes()),
209        }
210
211        offset += BlsPublicKey::SIZE;
212
213        bytes[offset..offset + u64::SIZE]
214            .copy_from_slice(&self.value.to_bytes());
215
216        bytes
217    }
218}
219
220/// Withdraw some value from the stake contract to a smart contract
221///
222/// This struct contains the information necessary to perform the withdrawal,
223/// including the account initiating the withdrawal, the amount being withdrawn,
224/// and the name of the function to invoke on the target contract.
225#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize)]
226#[archive_attr(derive(CheckBytes))]
227#[cfg_attr(feature = "serde", cfg_eval, serde_as)]
228#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
229pub struct WithdrawToContract {
230    account: BlsPublicKey,
231    #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))]
232    value: u64,
233    fn_name: String,
234    #[cfg_attr(feature = "serde", serde_as(as = "Hex"))]
235    data: Vec<u8>,
236}
237
238impl WithdrawToContract {
239    /// Creates a new `WithdrawToContract` instance.
240    ///
241    /// # Parameters
242    /// - `account`: The BLS public key of the account initiating the
243    ///   withdrawal.
244    /// - `value`: The amount of the withdrawal in LUX
245    /// - `fn_name`: The name of the function to invoke on the target contract
246    ///   as callback for the `contract_to_contract` method.
247    ///
248    /// # Returns
249    /// A new `WithdrawToContract` instance with the specified account, value,
250    /// and function name.
251    pub fn new(
252        account: BlsPublicKey,
253        value: u64,
254        fn_name: impl Into<String>,
255    ) -> Self {
256        Self {
257            account,
258            value,
259            fn_name: fn_name.into(),
260            data: Vec::new(),
261        }
262    }
263
264    /// Returns the account initiating the withdrawal.
265    #[must_use]
266    pub fn account(&self) -> &BlsPublicKey {
267        &self.account
268    }
269
270    /// Returns the value (amount) of the withdrawal.
271    #[must_use]
272    pub fn value(&self) -> u64 {
273        self.value
274    }
275
276    /// Returns the name of the callback function to invoke on the target
277    /// contract.
278    #[must_use]
279    pub fn fn_name(&self) -> &str {
280        &self.fn_name
281    }
282
283    /// Returns the data to be passed to the target contract.
284    #[must_use]
285    pub fn data(&self) -> &[u8] {
286        &self.data
287    }
288
289    /// Set the data to be passed to the target contract.
290    #[must_use]
291    pub fn with_data(mut self, data: Vec<u8>) -> Self {
292        self.data = data;
293        self
294    }
295}
296
297/// Withdraw some value from the stake contract.
298///
299/// This is used in both `unstake` and `withdraw`.
300#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize)]
301#[archive_attr(derive(CheckBytes))]
302#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
303pub struct Withdraw {
304    account: BlsPublicKey,
305    withdraw: TransferWithdraw,
306    signature: DoubleSignature,
307}
308
309impl Withdraw {
310    /// Create a new withdraw call specifying the owner.
311    #[must_use]
312    pub fn new(
313        account_sk: &BlsSecretKey,
314        owner_sk: &BlsSecretKey,
315        withdraw: TransferWithdraw,
316    ) -> Self {
317        let account = BlsPublicKey::from(account_sk);
318
319        let mut stake_withdraw = Withdraw {
320            account,
321            withdraw,
322            signature: DoubleSignature::default(),
323        };
324
325        let msg = stake_withdraw.signature_message();
326
327        stake_withdraw.signature = DoubleSignature {
328            account: account_sk.sign(&msg),
329            owner: owner_sk.sign(&msg),
330        };
331
332        stake_withdraw
333    }
334
335    /// Create a new withdraw call using the same account as the owner.
336    #[must_use]
337    pub fn with_single_key(
338        sk: &BlsSecretKey,
339        withdraw: TransferWithdraw,
340    ) -> Self {
341        Self::new(sk, sk, withdraw)
342    }
343
344    /// The public key to withdraw from.
345    #[must_use]
346    pub fn account(&self) -> &BlsPublicKey {
347        &self.account
348    }
349
350    /// The inner withdrawal to pass to the transfer contract.
351    #[must_use]
352    pub fn transfer_withdraw(&self) -> &TransferWithdraw {
353        &self.withdraw
354    }
355
356    /// Signature of the withdraw.
357    #[must_use]
358    pub fn signature(&self) -> &DoubleSignature {
359        &self.signature
360    }
361
362    /// Signature message used for [`Withdraw`].
363    #[must_use]
364    pub fn signature_message(&self) -> Vec<u8> {
365        let mut bytes = Vec::new();
366
367        bytes.extend(self.account.to_bytes());
368        bytes.extend(self.withdraw.wrapped_signature_message());
369
370        bytes
371    }
372}
373
374/// Event emitted after a stake contract operation is performed.
375#[derive(Debug, Clone, Archive, Deserialize, Serialize, PartialEq)]
376#[archive_attr(derive(CheckBytes))]
377#[cfg_attr(feature = "serde", cfg_eval, serde_as)]
378#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
379pub struct StakeEvent {
380    /// Keys associated to the event.
381    pub keys: StakeKeys,
382    /// Effective value of the relevant operation, be it `stake`,
383    /// `unstake`,`withdraw`
384    #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))]
385    pub value: u64,
386    /// The locked amount involved in the operation (e.g., for `stake` or
387    /// `unstake`). Defaults to zero for operations that do not involve
388    /// locking.
389    #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))]
390    pub locked: u64,
391}
392
393impl StakeEvent {
394    /// Creates a new `StakeEvent` with the specified keys and value.
395    ///
396    /// ### Parameters
397    /// - `keys`: The keys associated with the stake event.
398    /// - `value`: The effective value of the operation (e.g., `stake`,
399    ///   `unstake`, `withdraw`).
400    ///
401    /// The `locked` amount is initialized to zero by default.
402    #[must_use]
403    pub fn new(keys: StakeKeys, value: u64) -> Self {
404        Self {
405            keys,
406            value,
407            locked: 0,
408        }
409    }
410    /// Sets the locked amount for the `StakeEvent`.
411    ///
412    /// ### Parameters
413    /// - `locked`: The locked amount associated with the operation.
414    #[must_use]
415    pub fn locked(mut self, locked: u64) -> Self {
416        self.locked = locked;
417        self
418    }
419}
420
421/// Event emitted after a slash operation is performed.
422#[derive(Debug, Clone, Archive, Deserialize, Serialize, PartialEq)]
423#[archive_attr(derive(CheckBytes))]
424#[cfg_attr(feature = "serde", cfg_eval, serde_as)]
425#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
426pub struct SlashEvent {
427    /// Account slashed.
428    pub account: BlsPublicKey,
429    /// Slashed amount
430    #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))]
431    pub value: u64,
432    /// New eligibility for the slashed account
433    #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))]
434    pub next_eligibility: u64,
435}
436
437/// The representation of a public key's stake.
438///
439/// A user can stake for a particular `amount` larger in value than the
440/// `MINIMUM_STAKE` value and is `reward`ed for participating in the consensus.
441/// A stake is valid only after a particular block height - called the
442/// eligibility.
443///
444/// To keep track of the number of interactions a public key has had with the
445/// contract a `counter` is used to prevent repeat attacks - where the same
446/// signature could be used to prove ownership of the secret key in two
447/// different transactions.
448#[derive(
449    Debug, Default, Clone, Copy, PartialEq, Eq, Archive, Deserialize, Serialize,
450)]
451#[archive_attr(derive(CheckBytes))]
452#[cfg_attr(feature = "serde", cfg_eval, serde_as)]
453#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
454pub struct StakeData {
455    /// Amount staked and eligibility.
456    pub amount: Option<StakeAmount>,
457    /// The reward for participating in consensus.
458    #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))]
459    pub reward: u64,
460    /// Faults
461    pub faults: u8,
462    /// Hard Faults
463    pub hard_faults: u8,
464}
465
466/// Keys that identify a stake
467#[derive(
468    Debug, Default, Clone, Copy, PartialEq, Eq, Archive, Deserialize, Serialize,
469)]
470#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
471#[archive_attr(derive(CheckBytes))]
472pub struct StakeKeys {
473    /// Key used for consensus operations, such as voting or producing blocks.
474    pub account: BlsPublicKey,
475
476    /// Key used to manage funds for operations like staking, unstaking, or
477    /// withdrawing.
478    ///
479    /// This field can represent ownership either through an individual account
480    /// (`StakeFundOwner::Account`) or through a smart contract
481    /// (`StakeFundOwner::Contract`).
482    pub owner: StakeFundOwner,
483}
484
485impl StakeKeys {
486    /// Creates a new `StakeKeys` instance where both the consensus key and the
487    /// owner key are set to the same account key.
488    #[must_use]
489    pub fn single_key(account: BlsPublicKey) -> Self {
490        Self::new(account, account)
491    }
492
493    /// Creates a new `StakeKeys` instance with the specified account and funds
494    /// owner.
495    ///
496    /// # Parameters
497    /// - `account`: The BLS public key used for consensus operations, such as
498    ///   voting or producing blocks.
499    /// - `owner`: The owner of the funds, which can be either an account or a
500    ///   contract. This parameter is any type that implements
501    ///   `Into<StakeFundOwner>`, allowing flexibility in how the funds owner is
502    ///   specified.
503    #[must_use]
504    pub fn new<F: Into<StakeFundOwner>>(
505        account: BlsPublicKey,
506        owner: F,
507    ) -> Self {
508        let owner = owner.into();
509        Self { account, owner }
510    }
511}
512
513/// Data used to check ownership of funds
514#[derive(
515    Debug, Clone, Copy, PartialEq, Eq, Archive, Deserialize, Serialize,
516)]
517#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
518#[archive_attr(derive(CheckBytes))]
519pub enum StakeFundOwner {
520    /// Represents an account-based owner identified by a BLS public key.
521    ///
522    /// This variant is used when the stake funds are associated directly with
523    /// an individual account.
524    Account(BlsPublicKey),
525
526    /// Represents a contract-based owner identified by a contract ID.
527    ///
528    /// This variant is used when the stake funds are managed or associated
529    /// with a specific smart contract.
530    Contract(ContractId),
531}
532
533impl Default for StakeFundOwner {
534    fn default() -> Self {
535        BlsPublicKey::default().into()
536    }
537}
538
539impl From<BlsPublicKey> for StakeFundOwner {
540    fn from(value: BlsPublicKey) -> Self {
541        Self::Account(value)
542    }
543}
544
545impl From<ContractId> for StakeFundOwner {
546    fn from(value: ContractId) -> Self {
547        Self::Contract(value)
548    }
549}
550
551/// Signature used to handle funds (stake/unstake/withdraw)
552#[derive(
553    Debug, Default, Clone, Copy, PartialEq, Eq, Archive, Deserialize, Serialize,
554)]
555#[archive_attr(derive(CheckBytes))]
556#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
557pub struct DoubleSignature {
558    /// Signature created with the account key
559    pub account: BlsSignature,
560    /// Signature created with the owner key
561    pub owner: BlsSignature,
562}
563
564impl StakeData {
565    /// An empty stake.
566    pub const EMPTY: Self = Self {
567        amount: None,
568        reward: 0,
569        faults: 0,
570        hard_faults: 0,
571    };
572
573    /// Create a new stake given its initial `value` and `reward`, together with
574    /// the `block_height` of its creation.
575    #[must_use]
576    pub const fn new(value: u64, reward: u64, block_height: u64) -> Self {
577        let eligibility = Self::eligibility_from_height(block_height);
578        Self::with_eligibility(value, reward, eligibility)
579    }
580
581    /// Create a new stake given its initial `value` and `reward`, together with
582    /// the `eligibility`.
583    #[must_use]
584    pub const fn with_eligibility(
585        value: u64,
586        reward: u64,
587        eligibility: u64,
588    ) -> Self {
589        let amount = match value {
590            0 => None,
591            _ => Some(StakeAmount {
592                value,
593                eligibility,
594                locked: 0,
595            }),
596        };
597
598        Self {
599            amount,
600            reward,
601            faults: 0,
602            hard_faults: 0,
603        }
604    }
605
606    /// Returns true if the stake is valid - meaning there is an `amount` staked
607    /// and the given `block_height` is larger or equal to the stake's
608    /// eligibility. If there is no `amount` staked this is false.
609    #[must_use]
610    pub fn is_valid(&self, block_height: u64) -> bool {
611        match &self.amount {
612            Some(amount) => block_height >= amount.eligibility,
613            None => false,
614        }
615    }
616
617    /// Compute the eligibility of a stake from the starting block height.
618    #[must_use]
619    pub const fn eligibility_from_height(block_height: u64) -> u64 {
620        StakeAmount::eligibility_from_height(block_height)
621    }
622
623    /// Check if there is no amount left to withdraw
624    ///
625    /// Return true if both stake and rewards are 0
626    pub fn is_empty(&self) -> bool {
627        let stake = self
628            .amount
629            .as_ref()
630            .map(StakeAmount::total_funds)
631            .unwrap_or_default();
632        self.reward + stake == 0
633    }
634}
635
636/// Value staked and eligibility.
637#[derive(
638    Debug, Default, Clone, Copy, PartialEq, Eq, Archive, Deserialize, Serialize,
639)]
640#[archive_attr(derive(CheckBytes))]
641#[cfg_attr(feature = "serde", cfg_eval, serde_as)]
642#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
643pub struct StakeAmount {
644    /// The value staked.
645    #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))]
646    pub value: u64,
647    /// The amount that has been locked (due to a soft slash).
648    #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))]
649    pub locked: u64,
650    /// The eligibility of the stake.
651    #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))]
652    pub eligibility: u64,
653}
654
655impl StakeAmount {
656    /// Create a new stake amount.
657    #[must_use]
658    pub const fn new(value: u64, block_height: u64) -> Self {
659        let eligibility = Self::eligibility_from_height(block_height);
660        Self::with_eligibility(value, eligibility)
661    }
662
663    /// Create a new stake given its initial `value` and `reward`, together with
664    /// the `eligibility`.
665    #[must_use]
666    pub const fn with_eligibility(value: u64, eligibility: u64) -> Self {
667        Self {
668            value,
669            eligibility,
670            locked: 0,
671        }
672    }
673
674    /// Compute the eligibility of a stake from the starting block height.
675    #[must_use]
676    pub const fn eligibility_from_height(block_height: u64) -> u64 {
677        let maturity_blocks = EPOCH;
678        next_epoch(block_height) + maturity_blocks
679    }
680
681    /// Move `amount` to locked value
682    pub fn lock_amount(&mut self, amount: u64) {
683        self.value -= amount;
684        self.locked += amount;
685    }
686
687    /// Get the total funds belonging to the stake (value + locked)
688    #[must_use]
689    pub fn total_funds(&self) -> u64 {
690        self.value + self.locked
691    }
692}
693
694/// Used in a `reward` call to reward a given account with an amount of Dusk,
695/// and emitted as an event, once a reward succeeds.
696#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)]
697#[archive_attr(derive(CheckBytes))]
698#[cfg_attr(feature = "serde", cfg_eval, serde_as)]
699#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
700pub struct Reward {
701    /// The account to be rewarded.
702    pub account: BlsPublicKey,
703    /// The amount to reward.
704    #[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))]
705    pub value: u64,
706    /// The reason for the reward.
707    pub reason: RewardReason,
708}
709
710/// The reason that a reward is issued.
711#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)]
712#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
713#[archive_attr(derive(CheckBytes))]
714pub enum RewardReason {
715    /// The fixed amount awarded to a generator.
716    GeneratorFixed,
717    /// Extra amount awarded to a generator.
718    GeneratorExtra,
719    /// Amount awarded to a voter.
720    Voter,
721    /// Amount awarded for another reason, such as rewarding Dusk.
722    Other,
723}