diff --git a/actors/miner/src/lib.rs b/actors/miner/src/lib.rs index 9b88ed582..ab8f000c1 100644 --- a/actors/miner/src/lib.rs +++ b/actors/miner/src/lib.rs @@ -23,6 +23,7 @@ use fvm_shared::reward::ThisEpochRewardReturn; use fvm_shared::sector::*; use fvm_shared::smooth::FilterEstimate; use fvm_shared::{MethodNum, METHOD_CONSTRUCTOR, METHOD_SEND}; +use itertools::Itertools; use log::{error, info, warn}; use multihash::Code::Blake2b256; use num_derive::FromPrimitive; @@ -126,6 +127,11 @@ pub enum Method { // Method numbers derived from FRC-XXXX standards ChangeBenificiaryExported = frc42_dispatch::method_hash!("ChangeBeneficiary"), GetBeneficiaryExported = frc42_dispatch::method_hash!("GetBeneficiary"), + GetOwnerExported = frc42_dispatch::method_hash!("GetOwner"), + IsControllingAddressExported = frc42_dispatch::method_hash!("IsControllingAddress"), + GetSectorSizeExported = frc42_dispatch::method_hash!("GetSectorSize"), + GetAvailableBalanceExported = frc42_dispatch::method_hash!("GetAvailableBalance"), + GetVestingFundsExported = frc42_dispatch::method_hash!("GetVestingFunds"), } pub const ERR_BALANCE_INVARIANTS_BROKEN: ExitCode = ExitCode::new(1000); @@ -205,6 +211,7 @@ impl Actor { Ok(()) } + /// Returns the "controlling" addresses: the owner, the worker, and all control addresses fn control_addresses(rt: &mut impl Runtime) -> Result { rt.validate_immediate_caller_accept_any()?; let state: State = rt.state()?; @@ -216,6 +223,71 @@ impl Actor { }) } + /// Returns the owner address + fn get_owner(rt: &mut impl Runtime) -> Result { + rt.validate_immediate_caller_accept_any()?; + let state: State = rt.state()?; + let owner = get_miner_info(rt.store(), &state)?.owner; + Ok(GetOwnerReturn { owner }) + } + + /// Returns whether the provided address is "controlling". + /// The "controlling" addresses are the Owner, the Worker, and all Control Addresses. + fn is_controlling_address( + rt: &mut impl Runtime, + params: IsControllingAddressParam, + ) -> Result { + rt.validate_immediate_caller_accept_any()?; + let input = match rt.resolve_address(¶ms.address) { + Some(a) => Address::new_id(a), + None => return Ok(IsControllingAddressReturn { is_controlling: false }), + }; + let state: State = rt.state()?; + let info = get_miner_info(rt.store(), &state)?; + let is_controlling = info + .control_addresses + .iter() + .chain(&[info.worker, info.owner]) + .into_iter() + .any(|a| *a == input); + + Ok(IsControllingAddressReturn { is_controlling }) + } + + /// Returns the miner's sector size + fn get_sector_size(rt: &mut impl Runtime) -> Result { + rt.validate_immediate_caller_accept_any()?; + let state: State = rt.state()?; + let sector_size = get_miner_info(rt.store(), &state)?.sector_size; + Ok(GetSectorSizeReturn { sector_size }) + } + + /// Returns the available balance of this miner. + /// This is calculated as actor balance - (vesting funds + pre-commit deposit + ip requirement + fee debt) + /// Can go negative if the miner is in IP debt. + fn get_available_balance( + rt: &mut impl Runtime, + ) -> Result { + rt.validate_immediate_caller_accept_any()?; + let state: State = rt.state()?; + let available_balance = + state.get_available_balance(&rt.current_balance()).map_err(|e| { + actor_error!(illegal_state, "failed to calculate available balance: {}", e) + })?; + Ok(GetAvailableBalanceReturn { available_balance }) + } + + /// Returns the funds vesting in this miner as a list of (vesting_epoch, vesting_amount) tuples. + fn get_vesting_funds(rt: &mut impl Runtime) -> Result { + rt.validate_immediate_caller_accept_any()?; + let state: State = rt.state()?; + let vesting_funds = state + .load_vesting_funds(rt.store()) + .map_err(|e| actor_error!(illegal_state, "failed to load vesting funds: {}", e))?; + let ret = vesting_funds.funds.into_iter().map(|v| (v.epoch, v.amount)).collect_vec(); + Ok(GetVestingFundsReturn { vesting_funds: ret }) + } + /// Will ALWAYS overwrite the existing control addresses with the control addresses passed in the params. /// If an empty addresses vector is passed, the control addresses will be cleared. /// A worker change will be scheduled if the worker passed in the params is different from the existing worker. @@ -5001,6 +5073,26 @@ impl ActorCode for Actor { Ok(RawBytes::default()) } None => Err(actor_error!(unhandled_message, "Invalid method")), + Some(Method::GetOwnerExported) => { + let res = Self::get_owner(rt)?; + Ok(RawBytes::serialize(res)?) + } + Some(Method::IsControllingAddressExported) => { + let res = Self::is_controlling_address(rt, cbor::deserialize_params(params)?)?; + Ok(RawBytes::serialize(res)?) + } + Some(Method::GetSectorSizeExported) => { + let res = Self::get_sector_size(rt)?; + Ok(RawBytes::serialize(res)?) + } + Some(Method::GetAvailableBalanceExported) => { + let res = Self::get_available_balance(rt)?; + Ok(RawBytes::serialize(res)?) + } + Some(Method::GetVestingFundsExported) => { + let res = Self::get_vesting_funds(rt)?; + Ok(RawBytes::serialize(res)?) + } } } } diff --git a/actors/miner/src/state.rs b/actors/miner/src/state.rs index 401792706..270cd3e4e 100644 --- a/actors/miner/src/state.rs +++ b/actors/miner/src/state.rs @@ -983,7 +983,7 @@ impl State { &self, actor_balance: &TokenAmount, ) -> anyhow::Result { - // (actor_balance - &self.locked_funds) - &self.pre_commit_deposit + // (actor_balance - &self.locked_funds) - &self.pre_commit_deposit - &self.initial_pledge Ok(self.get_unlocked_balance(actor_balance)? - &self.fee_debt) } diff --git a/actors/miner/src/types.rs b/actors/miner/src/types.rs index 0d8dd31f1..49c8290d7 100644 --- a/actors/miner/src/types.rs +++ b/actors/miner/src/types.rs @@ -13,7 +13,7 @@ use fvm_shared::econ::TokenAmount; use fvm_shared::randomness::Randomness; use fvm_shared::sector::{ PoStProof, RegisteredPoStProof, RegisteredSealProof, RegisteredUpdateProof, SectorNumber, - StoragePower, + SectorSize, StoragePower, }; use fvm_shared::smooth::FilterEstimate; @@ -482,3 +482,50 @@ pub struct GetBeneficiaryReturn { } impl Cbor for GetBeneficiaryReturn {} + +#[derive(Serialize_tuple, Deserialize_tuple)] +#[serde(transparent)] +pub struct GetOwnerReturn { + pub owner: Address, +} + +impl Cbor for GetOwnerReturn {} + +#[derive(Serialize_tuple, Deserialize_tuple)] +#[serde(transparent)] +pub struct IsControllingAddressParam { + pub address: Address, +} + +impl Cbor for IsControllingAddressParam {} + +#[derive(Serialize_tuple, Deserialize_tuple)] +#[serde(transparent)] +pub struct IsControllingAddressReturn { + pub is_controlling: bool, +} + +impl Cbor for IsControllingAddressReturn {} + +#[derive(Serialize_tuple, Deserialize_tuple)] +#[serde(transparent)] +pub struct GetSectorSizeReturn { + pub sector_size: SectorSize, +} + +impl Cbor for GetSectorSizeReturn {} + +#[derive(Serialize_tuple, Deserialize_tuple)] +#[serde(transparent)] +pub struct GetAvailableBalanceReturn { + pub available_balance: TokenAmount, +} + +impl Cbor for GetAvailableBalanceReturn {} + +#[derive(Serialize_tuple, Deserialize_tuple)] +pub struct GetVestingFundsReturn { + pub vesting_funds: Vec<(ChainEpoch, TokenAmount)>, +} + +impl Cbor for GetVestingFundsReturn {} diff --git a/actors/miner/tests/apply_rewards.rs b/actors/miner/tests/apply_rewards.rs index 1bec82b51..44f36c332 100644 --- a/actors/miner/tests/apply_rewards.rs +++ b/actors/miner/tests/apply_rewards.rs @@ -18,6 +18,7 @@ use fvm_shared::error::ExitCode; use fvm_shared::METHOD_SEND; mod util; + use fil_actor_miner::testing::check_state_invariants; use util::*; @@ -165,11 +166,12 @@ fn rewards_pay_back_fee_debt() { assert!(st.locked_funds.is_zero()); let amt = rt.get_balance(); - let available_before = st.get_available_balance(&amt).unwrap(); + let available_before = h.get_available_balance(&mut rt).unwrap(); assert!(available_before.is_positive()); let init_fee_debt: TokenAmount = 2 * &amt; // FeeDebt twice total balance st.fee_debt = init_fee_debt.clone(); - let available_after = st.get_available_balance(&amt).unwrap(); + rt.replace_state(&st); + let available_after = h.get_available_balance(&mut rt).unwrap(); assert!(available_after.is_negative()); rt.replace_state(&st); @@ -178,7 +180,7 @@ fn rewards_pay_back_fee_debt() { let penalty = TokenAmount::zero(); // manually update actor balance to include the added funds from outside let new_balance = &amt + &reward; - rt.set_balance(new_balance.clone()); + rt.set_balance(new_balance); // pledge change is new reward - reward taken for fee debt // 3*LockedRewardFactor*amt - 2*amt = remainingLocked @@ -203,7 +205,7 @@ fn rewards_pay_back_fee_debt() { BURNT_FUNDS_ACTOR_ADDR, METHOD_SEND, RawBytes::default(), - expect_burnt.clone(), + expect_burnt, RawBytes::default(), ExitCode::OK, ); @@ -212,13 +214,10 @@ fn rewards_pay_back_fee_debt() { rt.call::(Method::ApplyRewards as u64, &RawBytes::serialize(params).unwrap()).unwrap(); rt.verify(); - // Set balance to deduct fee - let final_balance = &new_balance - &expect_burnt; - let st = h.get_state(&rt); // balance funds used to pay off fee debt // available balance should be 2 - let available_balance = st.get_available_balance(&final_balance).unwrap(); + let available_balance = h.get_available_balance(&mut rt).unwrap(); assert_eq!(available_before + reward - init_fee_debt - &remaining_locked, available_balance); assert!(!st.fee_debt.is_positive()); // remaining funds locked in vesting table diff --git a/actors/miner/tests/exported_getters.rs b/actors/miner/tests/exported_getters.rs new file mode 100644 index 000000000..459ec768d --- /dev/null +++ b/actors/miner/tests/exported_getters.rs @@ -0,0 +1,150 @@ +use fil_actor_miner::{ + Actor, GetAvailableBalanceReturn, GetOwnerReturn, GetSectorSizeReturn, + IsControllingAddressParam, IsControllingAddressReturn, Method, +}; +use fil_actors_runtime::cbor::serialize; +use fil_actors_runtime::test_utils::make_identity_cid; +use fil_actors_runtime::INIT_ACTOR_ADDR; +use fvm_ipld_encoding::RawBytes; +use fvm_shared::address::Address; +use fvm_shared::{clock::ChainEpoch, econ::TokenAmount, sector::MAX_SECTOR_NUMBER}; +use std::ops::Sub; + +mod util; + +use util::*; + +const PERIOD_OFFSET: ChainEpoch = 100; + +// an expiration ~10 days greater than effective min expiration taking into account 30 days max +// between pre and prove commit +const DEFAULT_SECTOR_EXPIRATION: ChainEpoch = 220; + +#[test] +fn info_getters() { + let h = ActorHarness::new(PERIOD_OFFSET); + let mut rt = h.new_runtime(); + rt.set_balance(BIG_BALANCE.clone()); + h.construct_and_verify(&mut rt); + + // set caller to not-builtin + rt.set_caller(make_identity_cid(b"1234"), Address::new_id(1234)); + + // owner is good + rt.expect_validate_caller_any(); + let owner_ret: GetOwnerReturn = rt + .call::(Method::GetOwnerExported as u64, &RawBytes::default()) + .unwrap() + .deserialize() + .unwrap(); + + rt.verify(); + + assert_eq!(h.owner, owner_ret.owner); + + // check that the controlling addresses all return true + for control in h.control_addrs.iter().chain(&[h.worker, h.owner]) { + rt.expect_validate_caller_any(); + let is_control_ret: IsControllingAddressReturn = rt + .call::( + Method::IsControllingAddressExported as u64, + &serialize(&IsControllingAddressParam { address: *control }, "serializing control") + .unwrap(), + ) + .unwrap() + .deserialize() + .unwrap(); + assert!(is_control_ret.is_controlling); + + rt.verify(); + } + + // check that a non-controlling address doesn't return true + + rt.expect_validate_caller_any(); + let is_control_ret: IsControllingAddressReturn = rt + .call::( + Method::IsControllingAddressExported as u64, + &serialize( + &IsControllingAddressParam { address: INIT_ACTOR_ADDR }, + "serializing control", + ) + .unwrap(), + ) + .unwrap() + .deserialize() + .unwrap(); + assert!(!is_control_ret.is_controlling); + + rt.verify(); + + // sector size is good + rt.expect_validate_caller_any(); + let sector_size_ret: GetSectorSizeReturn = rt + .call::(Method::GetSectorSizeExported as u64, &RawBytes::default()) + .unwrap() + .deserialize() + .unwrap(); + + rt.verify(); + + assert_eq!(h.sector_size, sector_size_ret.sector_size); + + h.check_state(&rt); +} + +#[test] +fn collateral_getters() { + let h = ActorHarness::new(PERIOD_OFFSET); + let mut rt = h.new_runtime(); + rt.balance.replace(BIG_BALANCE.clone()); + + let precommit_epoch = PERIOD_OFFSET + 1; + rt.set_epoch(precommit_epoch); + + h.construct_and_verify(&mut rt); + let dl_info = h.deadline(&rt); + + // Precommit a sector + // Use the max sector number to make sure everything works. + let sector_no = MAX_SECTOR_NUMBER; + let prove_commit_epoch = precommit_epoch + rt.policy.pre_commit_challenge_delay + 1; + let expiration = + dl_info.period_end() + DEFAULT_SECTOR_EXPIRATION * rt.policy.wpost_proving_period; // something on deadline boundary but > 180 days + + let precommit_params = + h.make_pre_commit_params(sector_no, precommit_epoch - 1, expiration, vec![]); + let precommit = + h.pre_commit_sector_and_get(&mut rt, precommit_params, PreCommitConfig::empty(), true); + + // run prove commit logic + rt.set_epoch(prove_commit_epoch); + let actor_balance = TokenAmount::from_whole(1000); + rt.balance.replace(actor_balance.clone()); + let pcc = ProveCommitConfig::empty(); + + let sector = h + .prove_commit_sector_and_confirm( + &mut rt, + &precommit, + h.make_prove_commit_params(sector_no), + pcc, + ) + .unwrap(); + + // query available balance + + rt.expect_validate_caller_any(); + let available_balance_ret: GetAvailableBalanceReturn = rt + .call::(Method::GetAvailableBalanceExported as u64, &RawBytes::default()) + .unwrap() + .deserialize() + .unwrap(); + + rt.verify(); + + // let's be sure we're not vacuously testing this method + assert_eq!(actor_balance.sub(sector.initial_pledge), available_balance_ret.available_balance); + + h.check_state(&rt); +} diff --git a/actors/miner/tests/util.rs b/actors/miner/tests/util.rs index e5929b958..5064f596b 100644 --- a/actors/miner/tests/util.rs +++ b/actors/miner/tests/util.rs @@ -22,14 +22,14 @@ use fil_actor_miner::{ CronEventPayload, Deadline, DeadlineInfo, Deadlines, DeclareFaultsParams, DeclareFaultsRecoveredParams, DeferredCronEventParams, DisputeWindowedPoStParams, ExpirationQueue, ExpirationSet, ExtendSectorExpiration2Params, ExtendSectorExpirationParams, - FaultDeclaration, GetBeneficiaryReturn, GetControlAddressesReturn, Method, - MinerConstructorParams as ConstructorParams, MinerInfo, Partition, PendingBeneficiaryChange, - PoStPartition, PowerPair, PreCommitSectorBatchParams, PreCommitSectorBatchParams2, - PreCommitSectorParams, ProveCommitSectorParams, RecoveryDeclaration, - ReportConsensusFaultParams, SectorOnChainInfo, SectorPreCommitInfo, SectorPreCommitOnChainInfo, - Sectors, State, SubmitWindowedPoStParams, TerminateSectorsParams, TerminationDeclaration, - VestingFunds, WindowedPoSt, WithdrawBalanceParams, WithdrawBalanceReturn, - CRON_EVENT_PROVING_DEADLINE, SECTORS_AMT_BITWIDTH, + FaultDeclaration, GetAvailableBalanceReturn, GetBeneficiaryReturn, GetControlAddressesReturn, + Method, MinerConstructorParams as ConstructorParams, MinerInfo, Partition, + PendingBeneficiaryChange, PoStPartition, PowerPair, PreCommitSectorBatchParams, + PreCommitSectorBatchParams2, PreCommitSectorParams, ProveCommitSectorParams, + RecoveryDeclaration, ReportConsensusFaultParams, SectorOnChainInfo, SectorPreCommitInfo, + SectorPreCommitOnChainInfo, Sectors, State, SubmitWindowedPoStParams, TerminateSectorsParams, + TerminationDeclaration, VestingFunds, WindowedPoSt, WithdrawBalanceParams, + WithdrawBalanceReturn, CRON_EVENT_PROVING_DEADLINE, SECTORS_AMT_BITWIDTH, }; use fil_actor_miner::{Method as MinerMethod, ProveCommitAggregateParams}; use fil_actor_power::{ @@ -860,6 +860,7 @@ impl ActorHarness { pc: &SectorPreCommitOnChainInfo, params: ProveCommitSectorParams, ) -> Result<(), ActorError> { + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, self.worker); let seal_rand = TEST_RANDOMNESS_ARRAY_FROM_ONE; let seal_int_rand = TEST_RANDOMNESS_ARRAY_FROM_TWO; let interactive_epoch = pc.pre_commit_epoch + rt.policy.pre_commit_challenge_delay; @@ -2531,6 +2532,17 @@ impl ActorHarness { } ret } + + pub fn get_available_balance(&self, rt: &mut MockRuntime) -> Result { + // set caller to non-builtin + rt.set_caller(make_identity_cid(b"1234"), Address::new_id(1234)); + rt.expect_validate_caller_any(); + let available_balance_ret: GetAvailableBalanceReturn = rt + .call::(Method::GetAvailableBalanceExported as u64, &RawBytes::default())? + .deserialize()?; + rt.verify(); + Ok(available_balance_ret.available_balance) + } } #[allow(dead_code)]