use std::convert::{TryFrom, TryInto};
use num_traits::Zero;
use once_cell::sync::Lazy;
use thiserror::Error;
use crate::{
context::Context,
core::consensus::beacon,
migration,
module::{self, Module as _, Parameters as _},
modules::{self, accounts::API as _, core::API as _},
runtime::Runtime,
sdk_derive,
state::CurrentState,
storage::{self, Store},
types::address::{Address, SignatureAddressSpec},
};
#[cfg(test)]
mod test;
pub mod types;
const MODULE_NAME: &str = "rewards";
#[derive(Error, Debug, oasis_runtime_sdk_macros::Error)]
pub enum Error {
#[error("invalid argument")]
#[sdk_error(code = 1)]
InvalidArgument,
}
#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
pub struct Parameters {
pub schedule: types::RewardSchedule,
pub participation_threshold_numerator: u64,
pub participation_threshold_denominator: u64,
}
#[derive(Error, Debug)]
pub enum ParameterValidationError {
#[error("invalid participation threshold (numerator > denominator)")]
InvalidParticipationThreshold,
#[error("invalid schedule")]
InvalidSchedule(#[from] types::RewardScheduleError),
}
impl module::Parameters for Parameters {
type Error = ParameterValidationError;
fn validate_basic(&self) -> Result<(), Self::Error> {
self.schedule.validate_basic()?;
if self.participation_threshold_numerator > self.participation_threshold_denominator {
return Err(ParameterValidationError::InvalidParticipationThreshold);
}
if self.participation_threshold_denominator.is_zero() {
return Err(ParameterValidationError::InvalidParticipationThreshold);
}
Ok(())
}
}
#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
pub struct Genesis {
pub parameters: Parameters,
}
pub mod state {
pub const REWARDS: &[u8] = &[0x02];
}
pub struct Module;
pub static ADDRESS_REWARD_POOL: Lazy<Address> =
Lazy::new(|| Address::from_module(MODULE_NAME, "reward-pool"));
#[sdk_derive(Module)]
impl Module {
const NAME: &'static str = MODULE_NAME;
const VERSION: u32 = 2;
type Error = Error;
type Event = ();
type Parameters = Parameters;
type Genesis = Genesis;
#[migration(init)]
fn init(genesis: Genesis) {
genesis
.parameters
.validate_basic()
.expect("invalid genesis parameters");
Self::set_params(genesis.parameters);
}
#[migration(from = 1)]
fn migrate_v1_to_v2() {
CurrentState::with_store(|store| {
let mut store = storage::PrefixStore::new(store, &MODULE_NAME);
store.remove(&[0x01]);
});
}
}
impl module::TransactionHandler for Module {}
impl module::BlockHandler for Module {
fn end_block<C: Context>(ctx: &C) {
let epoch = ctx.epoch();
let mut rewards: types::EpochRewards = CurrentState::with_store(|store| {
let store = storage::PrefixStore::new(store, &MODULE_NAME);
let epochs =
storage::TypedStore::new(storage::PrefixStore::new(store, &state::REWARDS));
epochs.get(epoch.to_storage_key()).unwrap_or_default()
});
for entity_id in &ctx.runtime_round_results().good_compute_entities {
let address = Address::from_sigspec(&SignatureAddressSpec::Ed25519(entity_id.into()));
rewards.pending.entry(address).or_default().increment();
}
for entity_id in &ctx.runtime_round_results().bad_compute_entities {
let address = Address::from_sigspec(&SignatureAddressSpec::Ed25519(entity_id.into()));
rewards.pending.entry(address).or_default().forbid();
}
if <C::Runtime as Runtime>::Core::has_epoch_changed() {
let epoch_rewards = CurrentState::with_store(|store| {
let store = storage::PrefixStore::new(store, &MODULE_NAME);
let mut epochs =
storage::TypedStore::new(storage::PrefixStore::new(store, &state::REWARDS));
let epoch_rewards: Vec<(DecodableEpochTime, types::EpochRewards)> =
epochs.iter().collect();
for (epoch, _) in &epoch_rewards {
epochs.remove(epoch.0.to_storage_key());
}
epoch_rewards
});
let params = Self::params();
'epochs: for (epoch, rewards) in epoch_rewards {
let epoch = epoch.0;
let reward = params.schedule.for_epoch(epoch);
if reward.amount().is_zero() {
continue;
}
for address in rewards.for_disbursement(
params.participation_threshold_numerator,
params.participation_threshold_denominator,
) {
match <C::Runtime as Runtime>::Accounts::transfer(
*ADDRESS_REWARD_POOL,
address,
&reward,
) {
Ok(_) => {}
Err(modules::accounts::Error::InsufficientBalance) => {
continue 'epochs;
}
Err(err) => panic!("failed to disburse rewards: {err:?}"),
}
}
}
}
CurrentState::with_store(|store| {
let store = storage::PrefixStore::new(store, &MODULE_NAME);
let mut epochs =
storage::TypedStore::new(storage::PrefixStore::new(store, &state::REWARDS));
epochs.insert(epoch.to_storage_key(), rewards);
});
}
}
impl module::InvariantHandler for Module {}
trait ToStorageKey {
fn to_storage_key(&self) -> [u8; 8];
}
impl ToStorageKey for beacon::EpochTime {
fn to_storage_key(&self) -> [u8; 8] {
self.to_be_bytes()
}
}
struct DecodableEpochTime(beacon::EpochTime);
impl TryFrom<&[u8]> for DecodableEpochTime {
type Error = std::array::TryFromSliceError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Ok(DecodableEpochTime(beacon::EpochTime::from_be_bytes(
value.try_into()?,
)))
}
}