fil_actor_reward/
lib.rs

1// Copyright 2019-2022 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use fil_actors_runtime::runtime::{ActorCode, Runtime};
5use fil_actors_runtime::{
6    actor_error, cbor, ActorError, BURNT_FUNDS_ACTOR_ADDR, EXPECTED_LEADERS_PER_EPOCH,
7    STORAGE_POWER_ACTOR_ADDR, SYSTEM_ACTOR_ADDR,
8};
9use fvm_ipld_blockstore::Blockstore;
10use fvm_ipld_encoding::RawBytes;
11use fvm_shared::address::Address;
12use fvm_shared::bigint::bigint_ser::BigIntDe;
13use fvm_shared::econ::TokenAmount;
14use fvm_shared::sector::StoragePower;
15use fvm_shared::{MethodNum, METHOD_CONSTRUCTOR, METHOD_SEND};
16use log::{error, warn};
17use num_derive::FromPrimitive;
18use num_traits::FromPrimitive;
19
20pub use self::logic::*;
21pub use self::state::{Reward, State, VestingFunction};
22pub use self::types::*;
23
24#[cfg(feature = "fil-actor")]
25fil_actors_runtime::wasm_trampoline!(Actor);
26
27pub(crate) mod expneg;
28mod logic;
29mod state;
30pub mod testing;
31mod types;
32
33// only exported for tests
34#[doc(hidden)]
35pub mod ext;
36
37// * Updated to specs-actors commit: 999e57a151cc7ada020ca2844b651499ab8c0dec (v3.0.1)
38
39/// PenaltyMultiplier is the factor miner penalties are scaled up by
40pub const PENALTY_MULTIPLIER: u64 = 3;
41
42/// Reward actor methods available
43#[derive(FromPrimitive)]
44#[repr(u64)]
45pub enum Method {
46    Constructor = METHOD_CONSTRUCTOR,
47    AwardBlockReward = 2,
48    ThisEpochReward = 3,
49    UpdateNetworkKPI = 4,
50}
51
52/// Reward Actor
53pub struct Actor;
54impl Actor {
55    /// Constructor for Reward actor
56    fn constructor<BS, RT>(
57        rt: &mut RT,
58        curr_realized_power: Option<StoragePower>,
59    ) -> Result<(), ActorError>
60    where
61        BS: Blockstore,
62        RT: Runtime<BS>,
63    {
64        rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?;
65
66        if let Some(power) = curr_realized_power {
67            rt.create(&State::new(power))?;
68            Ok(())
69        } else {
70            Err(actor_error!(illegal_argument, "argument should not be nil"))
71        }
72    }
73
74    /// Awards a reward to a block producer.
75    /// This method is called only by the system actor, implicitly, as the last message in the evaluation of a block.
76    /// The system actor thus computes the parameters and attached value.
77    ///
78    /// The reward includes two components:
79    /// - the epoch block reward, computed and paid from the reward actor's balance,
80    /// - the block gas reward, expected to be transferred to the reward actor with this invocation.
81    ///
82    /// The reward is reduced before the residual is credited to the block producer, by:
83    /// - a penalty amount, provided as a parameter, which is burnt,
84    fn award_block_reward<BS, RT>(
85        rt: &mut RT,
86        params: AwardBlockRewardParams,
87    ) -> Result<(), ActorError>
88    where
89        BS: Blockstore,
90        RT: Runtime<BS>,
91    {
92        rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?;
93        let prior_balance = rt.current_balance();
94        if params.penalty.is_negative() {
95            return Err(actor_error!(illegal_argument, "negative penalty {}", params.penalty));
96        }
97        if params.gas_reward.is_negative() {
98            return Err(actor_error!(
99                illegal_argument,
100                "negative gas reward {}",
101                params.gas_reward
102            ));
103        }
104        if prior_balance < params.gas_reward {
105            return Err(actor_error!(
106                illegal_state,
107                "actor current balance {} insufficient to pay gas reward {}",
108                prior_balance,
109                params.gas_reward
110            ));
111        }
112        if params.win_count <= 0 {
113            return Err(actor_error!(illegal_argument, "invalid win count {}", params.win_count));
114        }
115
116        let miner_id = rt
117            .resolve_address(&params.miner)
118            .ok_or_else(|| actor_error!(not_found, "failed to resolve given owner address"))?;
119
120        let penalty: TokenAmount = &params.penalty * PENALTY_MULTIPLIER;
121
122        let total_reward = rt.transaction(|st: &mut State, rt| {
123            let mut block_reward: TokenAmount =
124                (&st.this_epoch_reward * params.win_count).div_floor(EXPECTED_LEADERS_PER_EPOCH);
125            let mut total_reward = &params.gas_reward + &block_reward;
126            let curr_balance = rt.current_balance();
127            if total_reward > curr_balance {
128                warn!(
129                    "reward actor balance {} below totalReward expected {},\
130                    paying out rest of balance",
131                    curr_balance, total_reward
132                );
133                total_reward = curr_balance;
134                block_reward = &total_reward - &params.gas_reward;
135                if block_reward.is_negative() {
136                    return Err(actor_error!(
137                        illegal_state,
138                        "programming error, block reward {} below zero",
139                        block_reward
140                    ));
141                }
142            }
143            st.total_storage_power_reward += block_reward;
144            Ok(total_reward)
145        })?;
146
147        // * Go implementation added this and removed capping it -- this could potentially panic
148        // * as they treat panics as an exit code. Revisit this.
149        if total_reward > prior_balance {
150            return Err(actor_error!(
151                illegal_state,
152                "reward {} exceeds balance {}",
153                total_reward,
154                prior_balance
155            ));
156        }
157
158        // if this fails, we can assume the miner is responsible and avoid failing here.
159        let reward_params = ext::miner::ApplyRewardParams { reward: total_reward.clone(), penalty };
160        let res = rt.send(
161            &Address::new_id(miner_id),
162            ext::miner::APPLY_REWARDS_METHOD,
163            RawBytes::serialize(&reward_params)?,
164            total_reward.clone(),
165        );
166        if let Err(e) = res {
167            error!(
168                "failed to send ApplyRewards call to the miner actor with funds {}, code: {:?}",
169                total_reward,
170                e.exit_code()
171            );
172            let res =
173                rt.send(&BURNT_FUNDS_ACTOR_ADDR, METHOD_SEND, RawBytes::default(), total_reward);
174            if let Err(e) = res {
175                error!(
176                    "failed to send unsent reward to the burnt funds actor, code: {:?}",
177                    e.exit_code()
178                );
179            }
180        }
181
182        Ok(())
183    }
184
185    /// The award value used for the current epoch, updated at the end of an epoch
186    /// through cron tick.  In the case previous epochs were null blocks this
187    /// is the reward value as calculated at the last non-null epoch.
188    fn this_epoch_reward<BS, RT>(rt: &mut RT) -> Result<ThisEpochRewardReturn, ActorError>
189    where
190        BS: Blockstore,
191        RT: Runtime<BS>,
192    {
193        rt.validate_immediate_caller_accept_any()?;
194        let st: State = rt.state()?;
195        Ok(ThisEpochRewardReturn {
196            this_epoch_baseline_power: st.this_epoch_baseline_power,
197            this_epoch_reward_smoothed: st.this_epoch_reward_smoothed,
198        })
199    }
200
201    /// Called at the end of each epoch by the power actor (in turn by its cron hook).
202    /// This is only invoked for non-empty tipsets, but catches up any number of null
203    /// epochs to compute the next epoch reward.
204    fn update_network_kpi<BS, RT>(
205        rt: &mut RT,
206        curr_realized_power: Option<StoragePower>,
207    ) -> Result<(), ActorError>
208    where
209        BS: Blockstore,
210        RT: Runtime<BS>,
211    {
212        rt.validate_immediate_caller_is(std::iter::once(&STORAGE_POWER_ACTOR_ADDR))?;
213        let curr_realized_power = curr_realized_power
214            .ok_or_else(|| actor_error!(illegal_argument, "argument cannot be None"))?;
215
216        rt.transaction(|st: &mut State, rt| {
217            let prev = st.epoch;
218            // if there were null runs catch up the computation until
219            // st.Epoch == rt.CurrEpoch()
220            while st.epoch < rt.curr_epoch() {
221                // Update to next epoch to process null rounds
222                st.update_to_next_epoch(&curr_realized_power);
223            }
224
225            st.update_to_next_epoch_with_reward(&curr_realized_power);
226            st.update_smoothed_estimates(st.epoch - prev);
227            Ok(())
228        })?;
229        Ok(())
230    }
231}
232
233impl ActorCode for Actor {
234    fn invoke_method<BS, RT>(
235        rt: &mut RT,
236        method: MethodNum,
237        params: &RawBytes,
238    ) -> Result<RawBytes, ActorError>
239    where
240        BS: Blockstore,
241        RT: Runtime<BS>,
242    {
243        match FromPrimitive::from_u64(method) {
244            Some(Method::Constructor) => {
245                let param: Option<BigIntDe> = cbor::deserialize_params(params)?;
246                Self::constructor(rt, param.map(|v| v.0))?;
247                Ok(RawBytes::default())
248            }
249            Some(Method::AwardBlockReward) => {
250                Self::award_block_reward(rt, cbor::deserialize_params(params)?)?;
251                Ok(RawBytes::default())
252            }
253            Some(Method::ThisEpochReward) => {
254                let res = Self::this_epoch_reward(rt)?;
255                Ok(RawBytes::serialize(&res)?)
256            }
257            Some(Method::UpdateNetworkKPI) => {
258                let param: Option<BigIntDe> = cbor::deserialize_params(params)?;
259                Self::update_network_kpi(rt, param.map(|v| v.0))?;
260                Ok(RawBytes::default())
261            }
262            None => Err(actor_error!(unhandled_message, "Invalid method")),
263        }
264    }
265}