Skip to main content

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