oasis_runtime_sdk/modules/rewards/
mod.rs1use 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
25const MODULE_NAME: &str = "rewards";
27
28#[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#[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#[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#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
74pub struct Genesis {
75 pub parameters: Parameters,
76}
77
78pub mod state {
80 pub const REWARDS: &[u8] = &[0x02];
84}
85
86pub struct Module;
88
89pub 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 Self::set_params(genesis.parameters);
113 }
114
115 #[migration(from = 1)]
116 fn migrate_v1_to_v2() {
117 CurrentState::with_store(|store| {
118 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 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 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 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 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 for (epoch, _) in &epoch_rewards {
162 epochs.remove(epoch.0.to_storage_key());
163 }
164
165 epoch_rewards
166 });
167
168 let params = Self::params();
170 'epochs: for (epoch, rewards) in epoch_rewards {
171 let epoch = epoch.0;
172
173 let reward = params.schedule.for_epoch(epoch);
175 if reward.amount().is_zero() {
176 continue;
177 }
178
179 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 continue 'epochs;
195 }
196 Err(err) => panic!("failed to disburse rewards: {err:?}"),
197 }
198 }
199 }
200 }
201
202 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
214trait 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
225struct 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}