oasis_runtime_sdk/modules/rewards/
mod.rs

1//! Rewards module.
2use std::convert::{TryFrom, TryInto};
3
4use num_traits::Zero;
5use once_cell::sync::Lazy;
6use thiserror::Error;
7
8use crate::{
9    context::Context,
10    core::consensus::beacon,
11    migration,
12    module::{self, Module as _, Parameters as _},
13    modules::{self, accounts::API as _, core::API as _},
14    runtime::Runtime,
15    sdk_derive,
16    state::CurrentState,
17    storage::{self, Store},
18    types::address::{Address, SignatureAddressSpec},
19};
20
21#[cfg(test)]
22mod test;
23pub mod types;
24
25/// Unique module name.
26const MODULE_NAME: &str = "rewards";
27
28/// Errors emitted by the rewards module.
29#[derive(Error, Debug, oasis_runtime_sdk_macros::Error)]
30pub enum Error {
31    #[error("invalid argument")]
32    #[sdk_error(code = 1)]
33    InvalidArgument,
34}
35
36/// Parameters for the rewards module.
37#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
38pub struct Parameters {
39    pub schedule: types::RewardSchedule,
40
41    pub participation_threshold_numerator: u64,
42    pub participation_threshold_denominator: u64,
43}
44
45/// Errors emitted during rewards parameter validation.
46#[derive(Error, Debug)]
47pub enum ParameterValidationError {
48    #[error("invalid participation threshold (numerator > denominator)")]
49    InvalidParticipationThreshold,
50
51    #[error("invalid schedule")]
52    InvalidSchedule(#[from] types::RewardScheduleError),
53}
54
55impl module::Parameters for Parameters {
56    type Error = ParameterValidationError;
57
58    fn validate_basic(&self) -> Result<(), Self::Error> {
59        self.schedule.validate_basic()?;
60
61        if self.participation_threshold_numerator > self.participation_threshold_denominator {
62            return Err(ParameterValidationError::InvalidParticipationThreshold);
63        }
64        if self.participation_threshold_denominator.is_zero() {
65            return Err(ParameterValidationError::InvalidParticipationThreshold);
66        }
67
68        Ok(())
69    }
70}
71
72/// Genesis state for the rewards module.
73#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
74pub struct Genesis {
75    pub parameters: Parameters,
76}
77
78/// State schema constants.
79pub mod state {
80    // 0x01 is reserved.
81
82    /// Map of epochs to rewards pending distribution.
83    pub const REWARDS: &[u8] = &[0x02];
84}
85
86/// Rewards module.
87pub struct Module;
88
89/// Module's address that has the reward pool.
90///
91/// oasis1qp7x0q9qahahhjas0xde8w0v04ctp4pqzu5mhjav
92pub static ADDRESS_REWARD_POOL: Lazy<Address> =
93    Lazy::new(|| Address::from_module(MODULE_NAME, "reward-pool"));
94
95#[sdk_derive(Module)]
96impl Module {
97    const NAME: &'static str = MODULE_NAME;
98    const VERSION: u32 = 2;
99    type Error = Error;
100    type Event = ();
101    type Parameters = Parameters;
102    type Genesis = Genesis;
103
104    #[migration(init)]
105    fn init(genesis: Genesis) {
106        genesis
107            .parameters
108            .validate_basic()
109            .expect("invalid genesis parameters");
110
111        // Set genesis parameters.
112        Self::set_params(genesis.parameters);
113    }
114
115    #[migration(from = 1)]
116    fn migrate_v1_to_v2() {
117        CurrentState::with_store(|store| {
118            // Version 2 removes the LAST_EPOCH storage state which was at 0x01.
119            let mut store = storage::PrefixStore::new(store, &MODULE_NAME);
120            store.remove(&[0x01]);
121        });
122    }
123}
124
125impl module::TransactionHandler for Module {}
126
127impl module::BlockHandler for Module {
128    fn end_block<C: Context>(ctx: &C) {
129        let epoch = ctx.epoch();
130
131        // Load rewards accumulator for the current epoch.
132        let mut rewards: types::EpochRewards = CurrentState::with_store(|store| {
133            let store = storage::PrefixStore::new(store, &MODULE_NAME);
134            let epochs =
135                storage::TypedStore::new(storage::PrefixStore::new(store, &state::REWARDS));
136            epochs.get(epoch.to_storage_key()).unwrap_or_default()
137        });
138
139        // Reward each good entity.
140        for entity_id in &ctx.runtime_round_results().good_compute_entities {
141            let address = Address::from_sigspec(&SignatureAddressSpec::Ed25519(entity_id.into()));
142            rewards.pending.entry(address).or_default().increment();
143        }
144
145        // Punish each bad entity by forbidding rewards for this epoch.
146        for entity_id in &ctx.runtime_round_results().bad_compute_entities {
147            let address = Address::from_sigspec(&SignatureAddressSpec::Ed25519(entity_id.into()));
148            rewards.pending.entry(address).or_default().forbid();
149        }
150
151        // Disburse any rewards for previous epochs when the epoch changes.
152        if <C::Runtime as Runtime>::Core::has_epoch_changed() {
153            let epoch_rewards = CurrentState::with_store(|store| {
154                let store = storage::PrefixStore::new(store, &MODULE_NAME);
155                let mut epochs =
156                    storage::TypedStore::new(storage::PrefixStore::new(store, &state::REWARDS));
157                let epoch_rewards: Vec<(DecodableEpochTime, types::EpochRewards)> =
158                    epochs.iter().collect();
159
160                // Remove all epochs that we will process.
161                for (epoch, _) in &epoch_rewards {
162                    epochs.remove(epoch.0.to_storage_key());
163                }
164
165                epoch_rewards
166            });
167
168            // Process accumulated rewards for previous epochs.
169            let params = Self::params();
170            'epochs: for (epoch, rewards) in epoch_rewards {
171                let epoch = epoch.0;
172
173                // Fetch reward schedule for the given epoch.
174                let reward = params.schedule.for_epoch(epoch);
175                if reward.amount().is_zero() {
176                    continue;
177                }
178
179                // Disburse rewards.
180                for address in rewards.for_disbursement(
181                    params.participation_threshold_numerator,
182                    params.participation_threshold_denominator,
183                ) {
184                    match <C::Runtime as Runtime>::Accounts::transfer(
185                        *ADDRESS_REWARD_POOL,
186                        address,
187                        &reward,
188                    ) {
189                        Ok(_) => {}
190                        Err(modules::accounts::Error::InsufficientBalance) => {
191                            // Since rewards are the same for the whole epoch, if there is not
192                            // enough in the pool, just continue with the next epoch which may
193                            // specify a lower amount or a different denomination.
194                            continue 'epochs;
195                        }
196                        Err(err) => panic!("failed to disburse rewards: {err:?}"),
197                    }
198                }
199            }
200        }
201
202        // Update rewards for current epoch.
203        CurrentState::with_store(|store| {
204            let store = storage::PrefixStore::new(store, &MODULE_NAME);
205            let mut epochs =
206                storage::TypedStore::new(storage::PrefixStore::new(store, &state::REWARDS));
207            epochs.insert(epoch.to_storage_key(), rewards);
208        });
209    }
210}
211
212impl module::InvariantHandler for Module {}
213
214/// A trait that exists solely to convert `beacon::EpochTime` to bytes for use as a storage key.
215trait ToStorageKey {
216    fn to_storage_key(&self) -> [u8; 8];
217}
218
219impl ToStorageKey for beacon::EpochTime {
220    fn to_storage_key(&self) -> [u8; 8] {
221        self.to_be_bytes()
222    }
223}
224
225/// A struct that exists solely to decode `beacon::EpochTime` previously encoded via `ToStorageKey`.
226struct DecodableEpochTime(beacon::EpochTime);
227
228impl TryFrom<&[u8]> for DecodableEpochTime {
229    type Error = std::array::TryFromSliceError;
230
231    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
232        Ok(DecodableEpochTime(beacon::EpochTime::from_be_bytes(
233            value.try_into()?,
234        )))
235    }
236}