diff --git a/vm/actor/src/builtin/power/policy.rs b/vm/actor/src/builtin/power/policy.rs index 085dfa4cf1ba..2cd0bfaf0338 100644 --- a/vm/actor/src/builtin/power/policy.rs +++ b/vm/actor/src/builtin/power/policy.rs @@ -4,7 +4,7 @@ use super::{SectorStorageWeightDesc, SectorTermination}; use crate::{reward, StoragePower}; use clock::ChainEpoch; -use num_bigint::BigInt; +use num_bigint::{BigInt, BigUint}; use num_traits::{FromPrimitive, Pow}; use runtime::ConsensusFaultType; use vm::TokenAmount; @@ -23,10 +23,10 @@ pub const CONSENSUS_FAULT_REPORTING_WINDOW: ChainEpoch = 2880; // 1 day @ 30 sec lazy_static! { /// Multiplier on sector pledge requirement. - pub static ref PLEDGE_FACTOR: BigInt = BigInt::from(3); // PARAM_FINISH + pub static ref PLEDGE_FACTOR: BigUint = BigUint::from(3u8); // PARAM_FINISH /// Total expected block reward per epoch (per-winner reward * expected winners), as input to pledge requirement. - pub static ref EPOCH_TOTAL_EXPECTED_REWARD: BigInt = reward::BLOCK_REWARD_TARGET.clone() * 5; // PARAM_FINISH + pub static ref EPOCH_TOTAL_EXPECTED_REWARD: BigUint = reward::BLOCK_REWARD_TARGET.clone() * 5u8; // PARAM_FINISH /// Minimum power of an individual miner to meet the threshold for leader election. pub static ref CONSENSUS_MINER_MIN_POWER: StoragePower = StoragePower::from(2 << 30); // placeholder @@ -101,9 +101,9 @@ pub fn pledge_for_weight( * weight.duration * &*EPOCH_TOTAL_EXPECTED_REWARD * &*PLEDGE_FACTOR; - let denominator = network_power; - - (numerator / denominator) + let denominator = network_power .to_biguint() - .expect("all values should be positive") + .expect("Storage power should be positive"); + + numerator / denominator } diff --git a/vm/actor/src/builtin/power/state.rs b/vm/actor/src/builtin/power/state.rs index 980c23501df6..49a8f4acca0d 100644 --- a/vm/actor/src/builtin/power/state.rs +++ b/vm/actor/src/builtin/power/state.rs @@ -351,7 +351,7 @@ impl State { epoch: ChainEpoch, ) -> Result<(), String> { let mut mmap = Multimap::from_root(s, &self.cron_event_queue)?; - mmap.remove_all(epoch_key(epoch))?; + mmap.remove_all(&epoch_key(epoch))?; self.cron_event_queue = mmap.root()?; Ok(()) } diff --git a/vm/actor/src/builtin/reward/mod.rs b/vm/actor/src/builtin/reward/mod.rs index c3e36e1ac75f..2856a8684fd5 100644 --- a/vm/actor/src/builtin/reward/mod.rs +++ b/vm/actor/src/builtin/reward/mod.rs @@ -4,15 +4,20 @@ mod state; mod types; -pub use self::state::{Reward, State}; +pub use self::state::{Reward, State, VestingFunction}; pub use self::types::*; -use crate::check_empty_params; +use crate::{ + check_empty_params, request_miner_control_addrs, Multimap, BURNT_FUNDS_ACTOR_ADDR, + SYSTEM_ACTOR_ADDR, +}; use address::Address; use ipld_blockstore::BlockStore; use num_derive::FromPrimitive; -use num_traits::FromPrimitive; +use num_traits::{FromPrimitive, Zero}; use runtime::{ActorCode, Runtime}; -use vm::{ActorError, ExitCode, MethodNum, Serialized, METHOD_CONSTRUCTOR}; +use vm::{ + ActorError, ExitCode, MethodNum, Serialized, TokenAmount, METHOD_CONSTRUCTOR, METHOD_SEND, +}; /// Reward actor methods available #[derive(FromPrimitive)] @@ -34,31 +39,146 @@ impl Method { pub struct Actor; impl Actor { /// Constructor for Reward actor - fn constructor(_rt: &RT) -> Result<(), ActorError> + fn constructor(rt: &mut RT) -> Result<(), ActorError> where BS: BlockStore, RT: Runtime, { - // TODO - todo!(); + rt.validate_immediate_caller_is(std::iter::once(&*SYSTEM_ACTOR_ADDR))?; + + let empty_root = Multimap::new(rt.store()).root().map_err(|e| { + ActorError::new( + ExitCode::ErrIllegalState, + format!("failed to construct state: {}", e), + ) + })?; + + rt.create(&State::new(empty_root))?; + Ok(()) } + /// Mints a reward and puts into state reward map - fn award_block_reward(_rt: &RT) -> Result<(), ActorError> + fn award_block_reward( + rt: &mut RT, + params: AwardBlockRewardParams, + ) -> Result<(), ActorError> where BS: BlockStore, RT: Runtime, { - // TODO add params type and implement - todo!(); + rt.validate_immediate_caller_is(std::iter::once(&*SYSTEM_ACTOR_ADDR))?; + let balance = rt.current_balance()?; + if balance < params.gas_reward { + return Err(ActorError::new( + ExitCode::ErrInsufficientFunds, + format!( + "actor current balance {} insufficient to pay gas reward {}", + balance, params.gas_reward + ), + )); + } + + if params.ticket_count == 0 { + return Err(ActorError::new( + ExitCode::ErrIllegalArgument, + "cannot give block reward for zero tickets".to_owned(), + )); + } + + let miner = rt.resolve_address(¶ms.miner)?; + + let prior_bal = rt.current_balance()?; + + let cur_epoch = rt.curr_epoch(); + let penalty: TokenAmount = rt + .transaction::<_, Result<_, String>, _>(|st: &mut State, bs| { + let block_rew = Self::compute_block_reward( + st, + &prior_bal - ¶ms.gas_reward, + params.ticket_count, + ); + let total_reward = block_rew + ¶ms.gas_reward; + + // Cap the penalty at the total reward value. + let penalty = std::cmp::min(params.penalty, total_reward.clone()); + // Reduce the payable reward by the penalty. + let rew_payable = total_reward - &penalty; + if (&rew_payable + &penalty) > prior_bal { + return Err(format!( + "reward payable {} + penalty {} exceeds balance {}", + rew_payable, penalty, prior_bal + )); + } + + // Record new reward into reward map. + if rew_payable > TokenAmount::zero() { + st.add_reward( + bs, + &miner, + Reward { + start_epoch: cur_epoch, + end_epoch: cur_epoch + REWARD_VESTING_PERIOD, + value: rew_payable, + amount_withdrawn: TokenAmount::zero(), + vesting_function: REWARD_VESTING_FUNCTION, + }, + )?; + } + // + Ok(penalty) + })? + .map_err(|e| ActorError::new(ExitCode::ErrIllegalState, e))?; + + // Burn the penalty + rt.send( + &*BURNT_FUNDS_ACTOR_ADDR, + METHOD_SEND, + &Serialized::default(), + &penalty, + )?; + + Ok(()) } + /// Withdraw available funds from reward map - fn withdraw_reward(_rt: &RT, _miner_in: &Address) -> Result<(), ActorError> + fn withdraw_reward(rt: &mut RT, miner_in: Address) -> Result<(), ActorError> where BS: BlockStore, RT: Runtime, { - // TODO - todo!(); + let maddr = rt.resolve_address(&miner_in)?; + + let (owner, worker) = request_miner_control_addrs(rt, &maddr)?; + + rt.validate_immediate_caller_is([owner.clone(), worker].iter())?; + + let cur_epoch = rt.curr_epoch(); + let withdrawable_reward = + rt.transaction::<_, Result<_, ActorError>, _>(|st: &mut State, bs| { + let withdrawn = st.withdraw_reward(bs, &maddr, cur_epoch).map_err(|e| { + ActorError::new( + ExitCode::ErrIllegalState, + format!("failed to withdraw record: {}", e), + ) + })?; + Ok(withdrawn) + })??; + + rt.send( + &owner, + METHOD_SEND, + &Serialized::default(), + &withdrawable_reward, + )?; + Ok(()) + } + + /// Withdraw available funds from reward map + fn compute_block_reward(st: &State, balance: TokenAmount, ticket_count: u64) -> TokenAmount { + let treasury = balance - &st.reward_total; + let target_rew = BLOCK_REWARD_TARGET.clone() * ticket_count; + + std::cmp::min(target_rew, treasury) } } @@ -80,12 +200,11 @@ impl ActorCode for Actor { Ok(Serialized::default()) } Some(Method::AwardBlockReward) => { - check_empty_params(params)?; - Self::award_block_reward(rt)?; + Self::award_block_reward(rt, params.deserialize()?)?; Ok(Serialized::default()) } Some(Method::WithdrawReward) => { - Self::withdraw_reward(rt, ¶ms.deserialize()?)?; + Self::withdraw_reward(rt, params.deserialize()?)?; Ok(Serialized::default()) } _ => Err(rt.abort(ExitCode::SysErrInvalidMethod, "Invalid method")), diff --git a/vm/actor/src/builtin/reward/state.rs b/vm/actor/src/builtin/reward/state.rs index d2aa44f99454..3cb94255877e 100644 --- a/vm/actor/src/builtin/reward/state.rs +++ b/vm/actor/src/builtin/reward/state.rs @@ -1,39 +1,106 @@ // Copyright 2020 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +use crate::Multimap; use address::Address; use cid::Cid; use clock::ChainEpoch; +use encoding::Cbor; use ipld_blockstore::BlockStore; use num_bigint::biguint_ser::{BigUintDe, BigUintSer}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use num_derive::FromPrimitive; +use num_traits::{CheckedSub, FromPrimitive}; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use vm::TokenAmount; -pub struct Reward { - // TODO update to new spec - pub start_epoch: ChainEpoch, - pub value: TokenAmount, - pub release_rate: TokenAmount, - pub amount_withdrawn: TokenAmount, -} - /// Reward actor state pub struct State { + /// Reward multimap indexing addresses. pub reward_map: Cid, + /// Sum of un-withdrawn rewards. pub reward_total: TokenAmount, } impl State { - pub fn withdraw_reward( - _store: &BS, - _owner: Address, - _curr_epoch: ChainEpoch, - ) -> TokenAmount { - // TODO - todo!() + pub fn new(empty_multimap: Cid) -> Self { + Self { + reward_map: empty_multimap, + reward_total: TokenAmount::default(), + } + } + + #[allow(dead_code)] + pub(super) fn add_reward( + &mut self, + store: &BS, + owner: &Address, + reward: Reward, + ) -> Result<(), String> { + let mut rewards = Multimap::from_root(store, &self.reward_map)?; + let value = reward.value.clone(); + + rewards.add(owner.hash_key(), reward)?; + + self.reward_map = rewards.root()?; + self.reward_total += value; + Ok(()) + } + + /// Calculates and subtracts the total withdrawable reward for an owner. + #[allow(dead_code)] + pub(super) fn withdraw_reward( + &mut self, + store: &BS, + owner: &Address, + curr_epoch: ChainEpoch, + ) -> Result { + let mut rewards = Multimap::from_root(store, &self.reward_map)?; + let key = owner.hash_key(); + + // Iterate rewards, accumulate total and remaining reward state + let mut remaining_rewards = Vec::new(); + let mut withdrawable_sum = TokenAmount::from(0u8); + rewards.for_each(&key, |_, reward: &Reward| { + let unlocked = reward.amount_vested(curr_epoch); + let withdrawable = unlocked + .checked_sub(&reward.amount_withdrawn) + .ok_or(format!( + "Unlocked amount {} less than amount withdrawn {} at epoch {}", + unlocked, reward.amount_withdrawn, curr_epoch + ))?; + + withdrawable_sum += withdrawable; + if unlocked < reward.value { + remaining_rewards.push(Reward { + vesting_function: reward.vesting_function, + start_epoch: reward.start_epoch, + end_epoch: reward.end_epoch, + value: reward.value.clone(), + amount_withdrawn: unlocked, + }); + } + Ok(()) + })?; + + assert!( + withdrawable_sum < self.reward_total, + "withdrawable amount cannot exceed previous total" + ); + + // Regenerate amt for multimap with updated rewards + rewards.remove_all(&key)?; + for rew in remaining_rewards { + rewards.add(key.clone(), rew)?; + } + + // Update rewards multimap root and total + self.reward_map = rewards.root()?; + self.reward_total -= &withdrawable_sum; + Ok(withdrawable_sum) } } +impl Cbor for State {} impl Serialize for State { fn serialize(&self, serializer: S) -> Result where @@ -55,3 +122,95 @@ impl<'de> Deserialize<'de> for State { }) } } + +/// Defines vestion function type for reward actor +#[derive(Clone, Debug, PartialEq, Copy, FromPrimitive)] +#[repr(u8)] +pub enum VestingFunction { + None = 0, + Linear = 1, +} + +impl Serialize for VestingFunction { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + (*self as u8).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for VestingFunction { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let b: u8 = Deserialize::deserialize(deserializer)?; + Ok(FromPrimitive::from_u8(b) + .ok_or_else(|| de::Error::custom("Invalid registered proof byte"))?) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Reward { + pub vesting_function: VestingFunction, + pub start_epoch: ChainEpoch, + pub end_epoch: ChainEpoch, + pub value: TokenAmount, + pub amount_withdrawn: TokenAmount, +} + +impl Reward { + pub fn amount_vested(&self, curr_epoch: ChainEpoch) -> TokenAmount { + match self.vesting_function { + VestingFunction::None => self.value.clone(), + VestingFunction::Linear => { + let elapsed = curr_epoch - self.start_epoch; + let vest_duration = self.end_epoch - self.start_epoch; + if elapsed >= vest_duration { + self.value.clone() + } else { + (self.value.clone() * elapsed) / vest_duration + } + } + } + } +} + +impl Serialize for Reward { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + ( + &self.vesting_function, + &self.start_epoch, + &self.end_epoch, + BigUintSer(&self.value), + BigUintSer(&self.amount_withdrawn), + ) + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Reward { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let ( + vesting_function, + start_epoch, + end_epoch, + BigUintDe(value), + BigUintDe(amount_withdrawn), + ) = Deserialize::deserialize(deserializer)?; + Ok(Self { + vesting_function, + start_epoch, + end_epoch, + value, + amount_withdrawn, + }) + } +} diff --git a/vm/actor/src/builtin/reward/types.rs b/vm/actor/src/builtin/reward/types.rs index 8884d794bbde..6c4cd52ce4cd 100644 --- a/vm/actor/src/builtin/reward/types.rs +++ b/vm/actor/src/builtin/reward/types.rs @@ -1,14 +1,62 @@ // Copyright 2020 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -use num_bigint::BigInt; +use super::VestingFunction; +use address::Address; +use clock::ChainEpoch; +use num_bigint::biguint_ser::{BigUintDe, BigUintSer}; +use num_bigint::BigUint; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use vm::TokenAmount; /// Number of token units in an abstract "FIL" token. /// The network works purely in the indivisible token amounts. This constant converts to a fixed decimal with more /// human-friendly scale. -pub const TOKEN_PRECISION: i64 = 1_000_000_000_000_000_000; +pub const TOKEN_PRECISION: u64 = 1_000_000_000_000_000_000; lazy_static! { /// Target reward released to each block winner. - pub static ref BLOCK_REWARD_TARGET: BigInt = BigInt::from(100) * TOKEN_PRECISION; + pub static ref BLOCK_REWARD_TARGET: BigUint = BigUint::from(100u8) * TOKEN_PRECISION; +} + +pub(super) const REWARD_VESTING_FUNCTION: VestingFunction = VestingFunction::None; +pub(super) const REWARD_VESTING_PERIOD: ChainEpoch = 0; + +#[derive(Clone, Debug, PartialEq)] +pub struct AwardBlockRewardParams { + pub miner: Address, + pub penalty: TokenAmount, + pub gas_reward: TokenAmount, + pub ticket_count: u64, +} + +impl Serialize for AwardBlockRewardParams { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + ( + &self.miner, + BigUintSer(&self.penalty), + BigUintSer(&self.gas_reward), + &self.ticket_count, + ) + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for AwardBlockRewardParams { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let (miner, BigUintDe(penalty), BigUintDe(gas_reward), ticket_count) = + Deserialize::deserialize(deserializer)?; + Ok(Self { + miner, + penalty, + gas_reward, + ticket_count, + }) + } } diff --git a/vm/actor/src/util/multimap.rs b/vm/actor/src/util/multimap.rs index 6c5be06d154b..30b147d710cc 100644 --- a/vm/actor/src/util/multimap.rs +++ b/vm/actor/src/util/multimap.rs @@ -65,9 +65,9 @@ where /// Removes all values for a key. #[inline] - pub fn remove_all(&mut self, key: String) -> Result<(), String> { + pub fn remove_all(&mut self, key: &str) -> Result<(), String> { // Remove entry from table - self.0.delete(&key)?; + self.0.delete(key)?; Ok(()) } diff --git a/vm/actor/tests/multimap_test.rs b/vm/actor/tests/multimap_test.rs index 426d8f13c4c6..117511670308 100644 --- a/vm/actor/tests/multimap_test.rs +++ b/vm/actor/tests/multimap_test.rs @@ -59,10 +59,10 @@ fn remove_all() { let arr: Amt = mm.get(&addr1.hash_key()).unwrap().unwrap(); assert_eq!(arr.get(1), Ok(Some(88))); - mm.remove_all(addr1.hash_key()).unwrap(); + mm.remove_all(&addr1.hash_key()).unwrap(); assert_eq!(mm.get::(&addr1.hash_key()), Ok(None)); assert!(mm.get::(&addr2.hash_key()).unwrap().is_some()); - mm.remove_all(addr2.hash_key()).unwrap(); + mm.remove_all(&addr2.hash_key()).unwrap(); assert_eq!(mm.get::(&addr2.hash_key()), Ok(None)); } diff --git a/vm/src/error.rs b/vm/src/error.rs index e5172bf7ab4d..4b45d39c2fe1 100644 --- a/vm/src/error.rs +++ b/vm/src/error.rs @@ -26,6 +26,7 @@ impl ActorError { msg, } } + pub fn is_fatal(&self) -> bool { self.fatal }