diff --git a/Cargo.lock b/Cargo.lock index ab4000a17..7f0f61d4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1750,9 +1750,9 @@ checksum = "0d762194228a2f1c11063e46e32e5acb96e66e906382b9eb5441f2e0504bbd5a" [[package]] name = "ipld-core" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ede82a79e134f179f4b29b5fdb1eb92bd1b38c4dfea394c539051150a21b9b" +checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db" dependencies = [ "cid", "serde", @@ -2407,9 +2407,9 @@ dependencies = [ [[package]] name = "serde_ipld_dagcbor" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded35fbe4ab8fdec1f1d14b4daff2206b1eada4d6e708cb451d464d2d965f493" +checksum = "6851dcd54a7271dd9013195fdccbdaba70c8e71014364e396d4b938d0e67f324" dependencies = [ "cbor4ii", "ipld-core", diff --git a/actors/miner/src/deadline_state.rs b/actors/miner/src/deadline_state.rs index bf9160dca..30dcf6b3e 100644 --- a/actors/miner/src/deadline_state.rs +++ b/actors/miner/src/deadline_state.rs @@ -107,7 +107,7 @@ impl Deadlines { } /// Deadline holds the state for all sectors due at a specific deadline. -#[derive(Debug, Default, Serialize_tuple, Deserialize_tuple)] +#[derive(Debug, Default, Serialize_tuple, Deserialize_tuple, PartialEq)] pub struct Deadline { /// Partitions in this deadline, in order. /// The keys of this AMT are always sequential integers beginning with zero. @@ -163,6 +163,14 @@ pub struct Deadline { // These proofs may be disputed via DisputeWindowedPoSt. Successfully // disputed window PoSts are removed from the snapshot. pub optimistic_post_submissions_snapshot: Cid, + + /// Memoized sum of all non-terminated power in partitions, including active, faulty, and + /// unproven. Used to cap the daily fee as a proportion of expected block reward. + pub live_power: PowerPair, + + /// Memoized sum of daily fee payable to the network for the active sectors + /// in this deadline. + pub daily_fee: TokenAmount, } #[derive(Serialize_tuple, Deserialize_tuple, Clone)] @@ -233,6 +241,8 @@ impl Deadline { partitions_snapshot: empty_partitions_array, sectors_snapshot: empty_sectors_array, optimistic_post_submissions_snapshot: empty_post_submissions_array, + live_power: PowerPair::zero(), + daily_fee: TokenAmount::zero(), }) } @@ -347,6 +357,7 @@ impl Deadline { let mut all_on_time_pledge = TokenAmount::zero(); let mut all_active_power = PowerPair::zero(); let mut all_faulty_power = PowerPair::zero(); + let mut all_fee_deductions = TokenAmount::zero(); let mut partitions_with_early_terminations = Vec::::new(); // For each partition with an expiry, remove and collect expirations from the partition queue. @@ -374,6 +385,7 @@ impl Deadline { all_active_power += &partition_expiration.active_power; all_faulty_power += &partition_expiration.faulty_power; all_on_time_pledge += &partition_expiration.on_time_pledge; + all_fee_deductions += &partition_expiration.fee_deduction; partitions.set(partition_idx, partition)?; } @@ -394,6 +406,10 @@ impl Deadline { self.live_sectors -= on_time_count + early_count; self.faulty_power -= &all_faulty_power; + self.live_power -= &all_faulty_power; + self.live_power -= &all_active_power; + + self.daily_fee -= &all_fee_deductions; Ok(ExpirationSet { on_time_sectors: all_on_time_sectors, @@ -401,25 +417,35 @@ impl Deadline { on_time_pledge: all_on_time_pledge, active_power: all_active_power, faulty_power: all_faulty_power, + fee_deduction: all_fee_deductions, }) } + #[allow(clippy::too_many_arguments)] /// Adds sectors to a deadline. It's the caller's responsibility to make sure /// that this deadline isn't currently "open" (i.e., being proved at this point - /// in time). + /// in time). Sectors can be either proven or unproven, affecting how their + /// power is tracked. Daily fees may or may not be added using the new_fees + /// argument, depending on whether sectors are new to this deadline or just + /// being moved around. /// The sectors are assumed to be non-faulty. pub fn add_sectors( &mut self, store: &BS, partition_size: u64, proven: bool, + new_fees: bool, mut sectors: &[SectorOnChainInfo], sector_size: SectorSize, quant: QuantSpec, - ) -> anyhow::Result { + ) -> anyhow::Result<( + PowerPair, // power added + TokenAmount, // daily fee added + )> { let mut total_power = PowerPair::zero(); + let mut total_daily_fee: TokenAmount = TokenAmount::zero(); if sectors.is_empty() { - return Ok(total_power); + return Ok((total_power, total_daily_fee)); } // First update partitions, consuming the sectors @@ -455,9 +481,12 @@ impl Deadline { sectors = §ors[size..]; // Add sectors to partition. - let partition_power = + let (partition_power, partition_daily_fee) = partition.add_sectors(store, proven, partition_new_sectors, sector_size, quant)?; total_power += &partition_power; + if new_fees { + total_daily_fee += &partition_daily_fee; + } // Save partition back. partitions.set(partition_idx, partition)?; @@ -482,8 +511,10 @@ impl Deadline { .add_many_to_queue_values(partition_deadline_updates.iter().copied()) .map_err(|e| e.downcast_wrap("failed to add expirations for new deadlines"))?; self.expirations_epochs = deadline_expirations.amt.flush()?; + self.daily_fee += &total_daily_fee; + self.live_power += &total_power; - Ok(total_power) + Ok((total_power, total_daily_fee)) } pub fn pop_early_terminations( @@ -589,7 +620,7 @@ impl Deadline { )? .clone(); - let removed = partition + let (removed, removed_unproven) = partition .terminate_sectors( policy, store, @@ -619,6 +650,10 @@ impl Deadline { } // note: we should _always_ have early terminations, unless the early termination bitfield is empty. self.faulty_power -= &removed.faulty_power; + self.live_power -= &removed.active_power; + self.live_power -= &removed.faulty_power; + self.live_power -= &removed_unproven; + self.daily_fee -= &removed.fee_deduction; // Aggregate power lost from active sectors power_lost += &removed.active_power; @@ -632,23 +667,19 @@ impl Deadline { } /// RemovePartitions removes the specified partitions, shifting the remaining - /// ones to the left, and returning the live and dead sectors they contained. + /// ones to the left, and returning the dead sectors they contained. /// /// Returns an error if any of the partitions contained faulty sectors or early /// terminations. - pub fn remove_partitions( + pub fn compact_partitions( &mut self, store: &BS, + sectors: &mut Sectors, + sector_size: SectorSize, + window_post_partition_sectors: u64, to_remove: &BitField, quant: QuantSpec, - ) -> Result< - ( - BitField, // live - BitField, // dead - PowerPair, // removed power - ), - anyhow::Error, - > { + ) -> Result { let old_partitions = self.partitions_amt(store).map_err(|e| e.downcast_wrap("failed to load partitions"))?; @@ -666,7 +697,7 @@ impl Deadline { } } else { // Nothing to do. - return Ok((BitField::new(), BitField::new(), PowerPair::zero())); + return Ok(BitField::new()); } // Should already be checked earlier, but we might as well check again. @@ -739,6 +770,10 @@ impl Deadline { self.live_sectors -= removed_live_sectors; self.total_sectors -= removed_live_sectors + removed_dead_sectors; + // we can leave faulty power alone because there can be no faults here. + self.live_power -= &removed_power; + // NOTE: We don't update the fees here, we fix them up in the compact_partition logic (the + // only caller). This will be fixed in a future commit. // Update expiration bitfields. let mut expiration_epochs = BitFieldQueue::new(store, &self.expirations_epochs, quant) @@ -753,7 +788,35 @@ impl Deadline { .flush() .map_err(|e| e.downcast_wrap("failed persist deadline expiration queue"))?; - Ok((live, dead, removed_power)) + let live_sectors = sectors.load_sectors(&live)?; + let proven = true; // these are already proven sectors + let new_fees = false; // these are not new sectors, so don't adjust the deadline's daily_fee + let (added_power, added_fee) = self + .add_sectors( + store, + window_post_partition_sectors, + proven, + new_fees, + &live_sectors, + sector_size, + quant, + ) + .map_err(|e| { + e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to add back moved sectors") + })?; + + if !added_fee.is_zero() { + return Err(anyhow!("unexpected fee when adding back moved sectors: {}", added_fee)); + } + if removed_power != added_power { + return Err(anyhow!( + "power changed when compacting partitions: was {:?}, is now {:?}", + removed_power, + added_power + )); + } + + Ok(dead) } pub fn record_faults( diff --git a/actors/miner/src/expiration_queue.rs b/actors/miner/src/expiration_queue.rs index 54fbefadf..d177cfdca 100644 --- a/actors/miner/src/expiration_queue.rs +++ b/actors/miner/src/expiration_queue.rs @@ -31,7 +31,7 @@ const ENTRY_SECTORS_MAX: u64 = 10_000; /// Note that there is not a direct correspondence between on-time sectors and active power; /// a sector may be faulty but expiring on-time if it faults just prior to expected termination. /// Early sectors are always faulty, and active power always represents on-time sectors. -#[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug, Default)] +#[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug, Default, PartialEq)] pub struct ExpirationSet { /// Sectors expiring "on time" at the end of their committed life pub on_time_sectors: BitField, @@ -43,6 +43,15 @@ pub struct ExpirationSet { pub active_power: PowerPair, /// Power that is currently faulty pub faulty_power: PowerPair, + /// Adjustment to the daily fee recorded for the deadline associated with this expiration set + /// to account for expiring sectors. + /// + /// This field is not included in the serialised form of the struct prior to the activation of + /// FIP-0100, and is added as the 6th element of the array after that point only for new objects + /// or objects that are updated after that point. For old objects, the value of this field will + /// always be zero. + #[serde(default)] + pub fee_deduction: TokenAmount, } impl ExpirationSet { @@ -58,12 +67,14 @@ impl ExpirationSet { on_time_pledge: &TokenAmount, active_power: &PowerPair, faulty_power: &PowerPair, + fee_deduction: &TokenAmount, ) -> anyhow::Result<()> { self.on_time_sectors |= on_time_sectors; self.early_sectors |= early_sectors; self.on_time_pledge += on_time_pledge; self.active_power += active_power; self.faulty_power += faulty_power; + self.fee_deduction += fee_deduction; self.validate_state()?; Ok(()) @@ -77,6 +88,7 @@ impl ExpirationSet { on_time_pledge: &TokenAmount, active_power: &PowerPair, faulty_power: &PowerPair, + fee_deduction: &TokenAmount, ) -> anyhow::Result<()> { // Check for sector intersection. This could be cheaper with a combined intersection/difference method used below. if !self.on_time_sectors.contains_all(on_time_sectors) { @@ -99,6 +111,7 @@ impl ExpirationSet { self.on_time_pledge -= on_time_pledge; self.active_power -= active_power; self.faulty_power -= faulty_power; + self.fee_deduction -= fee_deduction; // Check underflow. if self.on_time_pledge.is_negative() { @@ -107,6 +120,9 @@ impl ExpirationSet { if self.active_power.qa.is_negative() || self.faulty_power.qa.is_negative() { return Err(anyhow!("expiration set power underflow: {:?}", self)); } + if self.fee_deduction.is_negative() { + return Err(anyhow!("expiration set fee deduction underflow: {:?}", self)); + } self.validate_state()?; Ok(()) } @@ -144,6 +160,10 @@ impl ExpirationSet { return Err(anyhow!("ExpirationSet left with negative qa faulty power")); } + if self.fee_deduction.is_negative() { + return Err(anyhow!("ExpirationSet left with negative fee deduction")); + } + Ok(()) } } @@ -167,14 +187,20 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { /// Adds a collection of sectors to their on-time target expiration entries (quantized). /// The sectors are assumed to be active (non-faulty). - /// Returns the sector numbers, power, and pledge added. + /// Returns the sector numbers, power, pledge added and daily fee for the sectors. pub fn add_active_sectors<'a>( &mut self, sectors: impl IntoIterator, sector_size: SectorSize, - ) -> anyhow::Result<(BitField, PowerPair, TokenAmount)> { + ) -> anyhow::Result<( + BitField, // sector numbers + PowerPair, // power + TokenAmount, // pledge + TokenAmount, // daily fee + )> { let mut total_power = PowerPair::zero(); let mut total_pledge = TokenAmount::zero(); + let mut total_daily_fee = TokenAmount::zero(); let mut total_sectors = Vec::::new(); for group in group_new_sectors_by_declared_expiration(sector_size, sectors, self.quant) { @@ -187,16 +213,18 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { &group.power, &PowerPair::zero(), &group.pledge, + &group.daily_fee, ) .map_err(|e| e.downcast_wrap("failed to record new sector expirations"))?; total_sectors.push(sector_numbers); total_power += &group.power; total_pledge += &group.pledge; + total_daily_fee += &group.daily_fee; } let sector_numbers = BitField::union(total_sectors.iter()); - Ok((sector_numbers, total_power, total_pledge)) + Ok((sector_numbers, total_power, total_pledge, total_daily_fee)) } /// Reschedules some sectors to a new (quantized) expiration epoch. @@ -213,7 +241,7 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { return Ok(()); } - let (sector_numbers, power, pledge) = self + let (sector_numbers, power, pledge, daily_fee) = self .remove_active_sectors(sectors, sector_size) .map_err(|e| e.downcast_wrap("failed to remove sector expirations"))?; @@ -224,6 +252,7 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { &power, &PowerPair::zero(), &pledge, + &daily_fee, ) .map_err(|e| e.downcast_wrap("failed to record new sector expirations"))?; @@ -243,6 +272,7 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { let mut sectors_total = Vec::new(); let mut expiring_power = PowerPair::zero(); let mut rescheduled_power = PowerPair::zero(); + let mut rescheduled_daily_fee = TokenAmount::zero(); let groups = self.find_sectors_by_expiration(sector_size, sectors)?; @@ -263,10 +293,12 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { group.expiration_set.on_time_sectors -= §ors_bitfield; group.expiration_set.on_time_pledge -= &group.sector_epoch_set.pledge; group.expiration_set.active_power -= &group.sector_epoch_set.power; + group.expiration_set.fee_deduction -= &group.sector_epoch_set.daily_fee; // Accumulate the sectors and power removed. sectors_total.extend_from_slice(&group.sector_epoch_set.sectors); rescheduled_power += &group.sector_epoch_set.power; + rescheduled_daily_fee += &group.sector_epoch_set.daily_fee; } self.must_update_or_delete(group.sector_epoch_set.epoch, group.expiration_set.clone())?; @@ -284,6 +316,7 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { &PowerPair::zero(), &rescheduled_power, &TokenAmount::zero(), + &rescheduled_daily_fee, )?; } @@ -295,6 +328,7 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { let mut rescheduled_epochs = Vec::::new(); let mut rescheduled_sectors = BitField::new(); let mut rescheduled_power = PowerPair::zero(); + let mut rescheduled_daily_fee = TokenAmount::zero(); let mut mutated_expiration_sets = Vec::<(ChainEpoch, ExpirationSet)>::new(); @@ -307,6 +341,7 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { // Regardless of whether the sectors were expiring on-time or early, all the power is now faulty. // Pledge is still on-time. + // Fees are not adjusted because the expiration set is not being removed. expiration_set.faulty_power += &expiration_set.active_power; expiration_set.active_power = PowerPair::zero(); mutated_expiration_sets.push((epoch, expiration_set)); @@ -321,6 +356,7 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { rescheduled_sectors |= &expiration_set.on_time_sectors; rescheduled_power += &expiration_set.active_power; rescheduled_power += &expiration_set.faulty_power; + rescheduled_daily_fee += &expiration_set.fee_deduction; } Ok(()) @@ -345,6 +381,7 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { &PowerPair::zero(), &rescheduled_power, &TokenAmount::zero(), + &rescheduled_daily_fee, )?; // Trim the rescheduled epochs from the queue. @@ -375,6 +412,7 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { self.iter_while_mut(|_epoch, expiration_set| { let mut faulty_power_delta = PowerPair::zero(); let mut active_power_delta = PowerPair::zero(); + let mut daily_fee_delta = TokenAmount::zero(); for sector_number in expiration_set.on_time_sectors.iter() { let sector = match remaining.remove(§or_number) { @@ -401,6 +439,7 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { early_unset.push(sector_number); let power = power_for_sector(sector_size, sector); faulty_power_delta -= &power; + daily_fee_delta -= §or.daily_fee; sectors_rescheduled.push(sector); recovered_power += &power; @@ -414,6 +453,7 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { { expiration_set.active_power += &active_power_delta; expiration_set.faulty_power += &faulty_power_delta; + expiration_set.fee_deduction += &daily_fee_delta; expiration_set.early_sectors -= BitField::try_from_bits(early_unset)?; } @@ -443,12 +483,18 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { old_sectors: &[SectorOnChainInfo], new_sectors: &[SectorOnChainInfo], sector_size: SectorSize, - ) -> anyhow::Result<(BitField, BitField, PowerPair, TokenAmount)> { - let (old_sector_numbers, old_power, old_pledge) = self + ) -> anyhow::Result<( + BitField, // old sector numbers + BitField, // new sector numbers + PowerPair, // power delta + TokenAmount, // pledge delta + TokenAmount, // daily fee delta + )> { + let (old_sector_numbers, old_power, old_pledge, old_daily_fee) = self .remove_active_sectors(old_sectors, sector_size) .map_err(|e| e.downcast_wrap("failed to remove replaced sectors"))?; - let (new_sector_numbers, new_power, new_pledge) = self + let (new_sector_numbers, new_power, new_pledge, new_daily_fee) = self .add_active_sectors(new_sectors, sector_size) .map_err(|e| e.downcast_wrap("failed to add replacement sectors"))?; @@ -457,6 +503,7 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { new_sector_numbers, &new_power - &old_power, new_pledge - old_pledge, + new_daily_fee - old_daily_fee, )) } @@ -511,12 +558,13 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { } // Remove non-faulty sectors. - let (removed_sector_numbers, removed_power, removed_pledge) = self + let (removed_sector_numbers, removed_power, removed_pledge, removed_daily_fee) = self .remove_active_sectors(&non_faulty_sectors, sector_size) .map_err(|e| e.downcast_wrap("failed to remove on-time recoveries"))?; removed.on_time_sectors = removed_sector_numbers; removed.active_power = removed_power; removed.on_time_pledge = removed_pledge; + removed.fee_deduction = removed_daily_fee; // Finally, remove faulty sectors (on time and not). These sectors can // only appear within the first 14 days (fault max age). Given that this @@ -572,6 +620,9 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { } remaining.remove(§or_number); + + removed.fee_deduction += §or.daily_fee; + expiration_set.fee_deduction -= §or.daily_fee; } } @@ -595,6 +646,7 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { let mut active_power = PowerPair::zero(); let mut faulty_power = PowerPair::zero(); let mut on_time_pledge = TokenAmount::zero(); + let mut fee_deduction = TokenAmount::zero(); let mut popped_keys = Vec::::new(); self.amt.for_each_while(|i, this_value| { @@ -608,6 +660,7 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { active_power += &this_value.active_power; faulty_power += &this_value.faulty_power; on_time_pledge += &this_value.on_time_pledge; + fee_deduction += &this_value.fee_deduction; Ok(true) })?; @@ -620,9 +673,11 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { on_time_pledge, active_power, faulty_power, + fee_deduction, }) } + #[allow(clippy::too_many_arguments)] fn add( &mut self, raw_epoch: ChainEpoch, @@ -631,18 +686,20 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { active_power: &PowerPair, faulty_power: &PowerPair, pledge: &TokenAmount, + daily_fee: &TokenAmount, ) -> anyhow::Result<()> { let epoch = self.quant.quantize_up(raw_epoch); let mut expiration_set = self.may_get(epoch)?; expiration_set - .add(on_time_sectors, early_sectors, pledge, active_power, faulty_power) + .add(on_time_sectors, early_sectors, pledge, active_power, faulty_power, daily_fee) .map_err(|e| anyhow!("failed to add expiration values for epoch {}: {}", epoch, e))?; self.must_update(epoch, expiration_set)?; Ok(()) } + #[allow(clippy::too_many_arguments)] fn remove( &mut self, raw_epoch: ChainEpoch, @@ -651,6 +708,7 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { active_power: &PowerPair, faulty_power: &PowerPair, pledge: &TokenAmount, + fee_deduction: &TokenAmount, ) -> anyhow::Result<()> { let epoch = self.quant.quantize_up(raw_epoch); let mut expiration_set = self @@ -660,10 +718,17 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { .ok_or_else(|| anyhow!("missing expected expiration set at epoch {}", epoch))? .clone(); expiration_set - .remove(on_time_sectors, early_sectors, pledge, active_power, faulty_power) + .remove( + on_time_sectors, + early_sectors, + pledge, + active_power, + faulty_power, + fee_deduction, + ) .map_err(|e| { - anyhow!("failed to remove expiration values for queue epoch {}: {}", epoch, e) - })?; + anyhow!("failed to remove expiration values for queue epoch {}: {}", epoch, e) + })?; self.must_update_or_delete(epoch, expiration_set)?; Ok(()) @@ -673,10 +738,16 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { &mut self, sectors: &[SectorOnChainInfo], sector_size: SectorSize, - ) -> anyhow::Result<(BitField, PowerPair, TokenAmount)> { + ) -> anyhow::Result<( + BitField, // sector numbers + PowerPair, // power + TokenAmount, // pledge + TokenAmount, // daily fee + )> { let mut removed_sector_numbers = Vec::::new(); let mut removed_power = PowerPair::zero(); let mut removed_pledge = TokenAmount::zero(); + let mut removed_daily_fee = TokenAmount::zero(); // Group sectors by their expiration, then remove from existing queue entries according to those groups. let groups = self.find_sectors_by_expiration(sector_size, sectors)?; @@ -690,15 +761,22 @@ impl<'db, BS: Blockstore> ExpirationQueue<'db, BS> { &group.sector_epoch_set.power, &PowerPair::zero(), &group.sector_epoch_set.pledge, + &group.sector_epoch_set.daily_fee, )?; removed_sector_numbers.extend(&group.sector_epoch_set.sectors); removed_power += &group.sector_epoch_set.power; removed_pledge += &group.sector_epoch_set.pledge; + removed_daily_fee += &group.sector_epoch_set.daily_fee; } - Ok((BitField::try_from_bits(removed_sector_numbers)?, removed_power, removed_pledge)) + Ok(( + BitField::try_from_bits(removed_sector_numbers)?, + removed_power, + removed_pledge, + removed_daily_fee, + )) } /// Traverses the entire queue with a callback function that may mutate entries. @@ -865,6 +943,7 @@ struct SectorEpochSet { sectors: Vec, power: PowerPair, pledge: TokenAmount, + daily_fee: TokenAmount, } /// Takes a slice of sector infos and returns sector info sets grouped and @@ -890,11 +969,13 @@ fn group_new_sectors_by_declared_expiration<'a>( let mut sector_numbers = Vec::::with_capacity(epoch_sectors.len()); let mut total_power = PowerPair::zero(); let mut total_pledge = TokenAmount::zero(); + let mut total_daily_fee = TokenAmount::zero(); for sector in epoch_sectors { sector_numbers.push(sector.sector_number); total_power += &power_for_sector(sector_size, sector); total_pledge += §or.initial_pledge; + total_daily_fee += §or.daily_fee; } SectorEpochSet { @@ -902,6 +983,7 @@ fn group_new_sectors_by_declared_expiration<'a>( sectors: sector_numbers, power: total_power, pledge: total_pledge, + daily_fee: total_daily_fee, } }) .collect() @@ -917,6 +999,7 @@ fn group_expiration_set( let mut sector_numbers = Vec::new(); let mut total_power = PowerPair::zero(); let mut total_pledge = TokenAmount::default(); + let mut total_daily_fee = TokenAmount::default(); for u in es.on_time_sectors.iter() { if include_set.remove(&u) { @@ -924,6 +1007,7 @@ fn group_expiration_set( sector_numbers.push(u); total_power += &power_for_sector(sector_size, sector); total_pledge += §or.initial_pledge; + total_daily_fee += §or.daily_fee; } } @@ -933,6 +1017,7 @@ fn group_expiration_set( sectors: sector_numbers, power: total_power, pledge: total_pledge, + daily_fee: total_daily_fee, }, expiration_set: es, } diff --git a/actors/miner/src/expiration_queue/tests.rs b/actors/miner/src/expiration_queue/tests.rs index 3b4986759..13fe24a85 100644 --- a/actors/miner/src/expiration_queue/tests.rs +++ b/actors/miner/src/expiration_queue/tests.rs @@ -26,12 +26,14 @@ fn test_expirations() { qa: StoragePower::from(2048 * 3), }, pledge: Zero::zero(), + daily_fee: Zero::zero(), }, SectorEpochSet { epoch: 23, sectors: vec![3], power: PowerPair { raw: StoragePower::from(2048), qa: StoragePower::from(2048) }, pledge: Zero::zero(), + daily_fee: Zero::zero(), }, ]; assert_eq!(expected.len(), result.len()); diff --git a/actors/miner/src/lib.rs b/actors/miner/src/lib.rs index b3da4eb16..820b1a7dc 100644 --- a/actors/miner/src/lib.rs +++ b/actors/miner/src/lib.rs @@ -28,6 +28,7 @@ use fvm_shared::sector::{ RegisteredUpdateProof, ReplicaUpdateInfo, SealRandomness, SealVerifyInfo, SectorID, SectorInfo, SectorNumber, SectorSize, StoragePower, WindowPoStVerifyInfo, }; +use fvm_shared::version::NetworkVersion; use fvm_shared::{ActorID, MethodNum, METHOD_CONSTRUCTOR, METHOD_SEND}; use itertools::Itertools; use log::{error, info, warn}; @@ -312,10 +313,12 @@ impl Actor { 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 }) + .vesting_funds + .load(rt.store())? + .into_iter() + .map(|v| (v.epoch, v.amount)) + .collect_vec(); + Ok(GetVestingFundsReturn { vesting_funds }) } /// Will ALWAYS overwrite the existing control addresses with the control addresses passed in the params. @@ -848,8 +851,6 @@ impl Actor { emit::sector_activated(rt, pc.info.sector_number, unsealed_cid, &data.pieces)?; } - // The aggregate fee is paid on the sectors successfully proven. - pay_aggregate_seal_proof_fee(rt, valid_precommits.len())?; Ok(()) } @@ -873,7 +874,6 @@ impl Actor { deadline: ru.deadline, partition: ru.partition, new_sealed_cid: ru.new_sealed_cid, - new_unsealed_cid: None, // Unknown deals: ru.deals, update_proof_type: ru.update_proof_type, replica_proof: ru.replica_proof, @@ -1062,29 +1062,21 @@ impl Actor { .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load sectors array")?; let mut sector_infos = Vec::with_capacity(params.sector_updates.len()); let mut updates = Vec::with_capacity(params.sector_updates.len()); - let mut sector_commds: HashMap = - HashMap::with_capacity(params.sector_updates.len()); for (i, update) in params.sector_updates.iter().enumerate() { let sector = sectors.must_get(update.sector)?; - let sector_type = sector.seal_proof; sector_infos.push(sector); - let computed_commd = unsealed_cid_from_pieces(rt, &update.pieces, sector_type)?; - updates.push(ReplicaUpdateInner { sector_number: update.sector, deadline: update.deadline, partition: update.partition, new_sealed_cid: update.new_sealed_cid, - new_unsealed_cid: Some(computed_commd.get_cid(sector_type)?), deals: vec![], update_proof_type: params.update_proofs_type, // Replica proof may be empty if an aggregate is being proven. // Validation needs to accept this empty proof. replica_proof: params.sector_proofs.get(i).unwrap_or(&RawBytes::default()).clone(), }); - - sector_commds.insert(update.sector, computed_commd); } // Validate inputs. @@ -1103,6 +1095,9 @@ impl Actor { let valid_unproven_usis = validation_batch.successes(&update_sector_infos); let valid_manifests = validation_batch.successes(¶ms.sector_updates); + let mut sector_commds: HashMap = + HashMap::with_capacity(params.sector_updates.len()); + // Verify proofs before activating anything. let mut proven_manifests: Vec<(&SectorUpdateManifest, &SectorOnChainInfo)> = vec![]; let mut proven_batch_gen = BatchReturnGen::new(validation_batch.success_count as usize); @@ -1121,13 +1116,16 @@ impl Actor { // return a BatchReturn, and then extract successes from // valid_unproven_usis and valid_manifests, following the pattern used elsewhere. for (usi, manifest) in valid_unproven_usis.iter().zip(valid_manifests) { + let sector_type = usi.sector_info.seal_proof; + let computed_commd = unsealed_cid_from_pieces(rt, &manifest.pieces, sector_type)?; let proof_inputs = ReplicaUpdateInfo { update_proof_type: usi.update.update_proof_type, new_sealed_cid: usi.update.new_sealed_cid, old_sealed_cid: usi.sector_info.sealed_cid, - new_unsealed_cid: usi.update.new_unsealed_cid.unwrap(), // set above + new_unsealed_cid: computed_commd.get_cid(sector_type)?, proof: usi.update.replica_proof.clone().into(), }; + sector_commds.insert(manifest.sector, computed_commd); match rt.verify_replica_update(&proof_inputs) { Ok(_) => { proven_manifests.push((manifest, usi.sector_info)); @@ -1410,7 +1408,7 @@ impl Actor { let penalty_target = &penalty_base + &reward_target; st.apply_penalty(&penalty_target) .map_err(|e| actor_error!(illegal_state, "failed to apply penalty {}", e))?; - let (penalty_from_vesting, penalty_from_balance) = st + let (to_burn, total_unlocked) = st .repay_partial_debt_in_priority_order( rt.store(), current_epoch, @@ -1420,13 +1418,11 @@ impl Actor { e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to pay debt") })?; - let to_burn = &penalty_from_vesting + &penalty_from_balance; - // Now, move as much of the target reward as // we can from the burn to the reward. let to_reward = std::cmp::min(&to_burn, &reward_target); let to_burn = &to_burn - to_reward; - let pledge_delta = penalty_from_vesting.neg(); + let pledge_delta = total_unlocked.neg(); Ok((pledge_delta, to_burn, power_delta, to_reward.clone())) })?; @@ -1485,18 +1481,8 @@ impl Actor { sectors: Vec, ) -> Result<(), ActorError> { let curr_epoch = rt.curr_epoch(); - { - let policy = rt.policy(); - if sectors.is_empty() { - return Err(actor_error!(illegal_argument, "batch empty")); - } else if sectors.len() > policy.pre_commit_sector_batch_max_size { - return Err(actor_error!( - illegal_argument, - "batch of {} too large, max {}", - sectors.len(), - policy.pre_commit_sector_batch_max_size - )); - } + if sectors.is_empty() { + return Err(actor_error!(illegal_argument, "batch empty")); } // Check per-sector preconditions before opening state transaction or sending other messages. let challenge_earliest = curr_epoch - rt.policy().max_pre_commit_randomness_lookback; @@ -1589,22 +1575,8 @@ impl Actor { let mut fee_to_burn = TokenAmount::zero(); let mut needs_cron = false; rt.transaction(|state: &mut State, rt| { - // Aggregate fee applies only when batching. - if sectors.len() > 1 { - let aggregate_fee = aggregate_pre_commit_network_fee(sectors.len(), &rt.base_fee()); - // AggregateFee applied to fee debt to consolidate burn with outstanding debts - state.apply_penalty(&aggregate_fee) - .map_err(|e| { - actor_error!( - illegal_state, - "failed to apply penalty: {}", - e - ) - })?; - } - // available balance already accounts for fee debt so it is correct to call - // this before RepayDebts. We would have to - // subtract fee debt explicitly if we called this after. + // Available balance already accounts for fee debt so it is correct to call this before + // repay_debts. We would have to subtract fee debt explicitly if we called this after. let available_balance = state .get_available_balance(&rt.current_balance()) .map_err(|e| { @@ -1924,13 +1896,6 @@ impl Actor { &info, )?; - if !params.aggregate_proof.is_empty() { - // Aggregate fee is paid on the sectors successfully proven, - // but without regard to data activation which may have subsequently failed - // and prevented sector activation. - pay_aggregate_seal_proof_fee(rt, proven_activation_inputs.len())?; - } - // Notify data consumers. let mut notifications: Vec = vec![]; for (activations, sector) in &successful_sector_activations { @@ -2116,6 +2081,10 @@ impl Actor { pledge_inputs.ramp_duration_epochs, ); + let circulating_supply = rt.total_fil_circ_supply(); + // Same fee for all sectors: same size, all raw + let daily_fee = daily_proof_fee(policy, &circulating_supply, &qa_sector_power); + let sectors_to_add = valid_sectors .iter() .map(|sector| SectorOnChainInfo { @@ -2134,6 +2103,7 @@ impl Actor { replaced_day_reward: None, sector_key_cid: None, flags: SectorOnChainInfoFlags::SIMPLE_QA_POWER, + daily_fee: daily_fee.clone(), }) .collect::>(); @@ -2192,13 +2162,6 @@ impl Actor { burn_funds(rt, fee_to_burn)?; - let len_for_aggregate_fee = if sectors_len <= NI_AGGREGATE_FEE_BASE_SECTOR_COUNT { - 0 - } else { - sectors_len - NI_AGGREGATE_FEE_BASE_SECTOR_COUNT - }; - pay_aggregate_seal_proof_fee(rt, len_for_aggregate_fee)?; - notify_pledge_changed(rt, &total_pledge)?; let state: State = rt.state()?; @@ -2313,6 +2276,7 @@ impl Actor { kind: ExtensionKind, ) -> Result<(), ActorError> { let curr_epoch = rt.curr_epoch(); + let circulating_supply = rt.total_fil_circ_supply(); /* Loop over sectors and do extension */ let (power_delta, pledge_delta) = rt.transaction(|state: &mut State, rt| { @@ -2359,6 +2323,10 @@ impl Actor { let quant = state.quant_spec_for_deadline(policy, deadline_idx); + let mut deadline_power_delta = PowerPair::zero(); + let mut deadline_pledge_delta = TokenAmount::zero(); + let mut deadline_daily_fee_delta = TokenAmount::zero(); + // Group modified partitions by epoch to which they are extended. Duplicates are ok. let mut partitions_by_new_epoch = BTreeMap::>::new(); let mut epochs_to_reschedule = Vec::::new(); @@ -2378,7 +2346,7 @@ impl Actor { .ok_or_else(|| actor_error!(not_found, "no such partition {:?}", key))?; let old_sectors = sectors - .load_sector(&decl.sectors) + .load_sectors(&decl.sectors) .map_err(|e| e.wrap("failed to load sectors"))?; let new_sectors: Vec = old_sectors .iter() @@ -2386,9 +2354,12 @@ impl Actor { ExtensionKind::ExtendCommittmentLegacy => { extend_sector_committment_legacy( rt.policy(), + rt.network_version(), curr_epoch, + &circulating_supply, decl.new_expiration, sector, + info.sector_size, ) } ExtensionKind::ExtendCommittment => match &inner.claims { @@ -2398,9 +2369,12 @@ impl Actor { )), Some(claim_space_by_sector) => extend_sector_committment( rt.policy(), + rt.network_version(), curr_epoch, + &circulating_supply, decl.new_expiration, sector, + info.sector_size, claim_space_by_sector, ), }, @@ -2416,23 +2390,28 @@ impl Actor { })?; // Remove old sectors from partition and assign new sectors. - let (partition_power_delta, partition_pledge_delta) = partition - .replace_sectors( - rt.store(), - &old_sectors, - &new_sectors, - info.sector_size, - quant, - ) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - format!("failed to replace sector expirations at {:?}", key), + let (partition_power_delta, partition_pledge_delta, partition_daily_fee_delta) = + partition + .replace_sectors( + rt.store(), + &old_sectors, + &new_sectors, + info.sector_size, + quant, ) - })?; + .map_err(|e| { + e.downcast_default( + ExitCode::USR_ILLEGAL_STATE, + format!("failed to replace sector expirations at {:?}", key), + ) + })?; - power_delta += &partition_power_delta; - pledge_delta += partition_pledge_delta; // expected to be zero, see note below. + deadline_power_delta += &partition_power_delta; + // expected to be zero, see note below. + deadline_pledge_delta += &partition_pledge_delta; + // non-zero when extending sectors that previously paid no fees (e.g., because + // they were sealed before we started charging fees). + deadline_daily_fee_delta += &partition_daily_fee_delta; partitions.set(decl.partition, partition).map_err(|e| { e.downcast_default( @@ -2454,6 +2433,12 @@ impl Actor { } } + deadline.live_power += &deadline_power_delta; + deadline.daily_fee += &deadline_daily_fee_delta; + + power_delta += &deadline_power_delta; + pledge_delta += &deadline_pledge_delta; + deadline.partitions = partitions.flush().map_err(|e| { e.downcast_default( ExitCode::USR_ILLEGAL_STATE, @@ -2498,6 +2483,8 @@ impl Actor { Ok((power_delta, pledge_delta)) })?; + // power_delta should be zero in most cases, but can be negative if claims are dropped in + // the process of extending sector expirations. request_update_power(rt, power_delta)?; // Note: the pledge delta is expected to be zero, since pledge is not re-calculated for the extension. @@ -2530,18 +2517,6 @@ impl Actor { // Note: this cannot terminate pre-committed but un-proven sectors. // They must be allowed to expire (and deposit burnt). - { - let policy = rt.policy(); - if params.terminations.len() as u64 > policy.declarations_max { - return Err(actor_error!( - illegal_argument, - "too many declarations when terminating sectors: {} > {}", - params.terminations.len(), - policy.declarations_max - )); - } - } - let mut to_process = DeadlineSectorMap::new(); for term in params.terminations { @@ -2671,18 +2646,6 @@ impl Actor { } fn declare_faults(rt: &impl Runtime, params: DeclareFaultsParams) -> Result<(), ActorError> { - { - let policy = rt.policy(); - if params.faults.len() as u64 > policy.declarations_max { - return Err(actor_error!( - illegal_argument, - "too many fault declarations for a single message: {} > {}", - params.faults.len(), - policy.declarations_max - )); - } - } - let mut to_process = DeadlineSectorMap::new(); for term in params.faults { @@ -2804,18 +2767,6 @@ impl Actor { rt: &impl Runtime, params: DeclareFaultsRecoveredParams, ) -> Result<(), ActorError> { - { - let policy = rt.policy(); - if params.recoveries.len() as u64 > policy.declarations_max { - return Err(actor_error!( - illegal_argument, - "too many recovery declarations for a single message: {} > {}", - params.recoveries.len(), - policy.declarations_max - )); - } - } - let mut to_process = DeadlineSectorMap::new(); for term in params.recoveries { @@ -2996,46 +2947,35 @@ impl Actor { let mut deadline = deadlines.load_deadline(store, params_deadline)?; - let (live, dead, removed_power) = - deadline.remove_partitions(store, partitions, quant).map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - format!("failed to remove partitions from deadline {}", params_deadline), - ) - })?; + let daily_fee_before = deadline.daily_fee.clone(); - state.delete_sectors(store, &dead).map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to delete dead sectors") + let mut sectors = Sectors::load(store, &state.sectors).map_err(|e| { + e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to load sectors") })?; - let sectors = state.load_sector_infos(store, &live).map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to load moved sectors") - })?; - let proven = true; - let added_power = deadline - .add_sectors( + let dead = deadline + .compact_partitions( store, - info.window_post_partition_sectors, - proven, - §ors, + &mut sectors, info.sector_size, + info.window_post_partition_sectors, + partitions, quant, ) .map_err(|e| { e.downcast_default( ExitCode::USR_ILLEGAL_STATE, - "failed to add back moved sectors", + format!("failed to remove partitions from deadline {}", params_deadline), ) })?; - if removed_power != added_power { - return Err(actor_error!( - illegal_state, - "power changed when compacting partitions: was {:?}, is now {:?}", - removed_power, - added_power - )); - } + sectors.delete_sectors(&dead).map_err(|e| { + e.wrap("failed to delete sectors removed during partition compaction") + })?; + state.sectors = + sectors.amt.flush().with_context_code(ExitCode::USR_ILLEGAL_STATE, || { + "failed to save sectors after compaction" + })?; deadlines.update_deadline(policy, store, params_deadline, &deadline).map_err(|e| { e.downcast_default( @@ -3051,6 +2991,14 @@ impl Actor { ) })?; + let daily_fee_after = deadline.daily_fee; + if daily_fee_before != daily_fee_after { + return Err(actor_error!( + illegal_state, + "unexpected daily fee change during partition compaction" + )); + } + Ok(()) })?; @@ -3162,7 +3110,7 @@ impl Actor { // Attempt to repay all fee debt in this call. In most cases the miner will have enough // funds in the *reward alone* to cover the penalty. In the rare case a miner incurs more // penalty than it can pay for with reward and existing funds, it will go into fee debt. - let (penalty_from_vesting, penalty_from_balance) = st + let (to_burn, total_unlocked) = st .repay_partial_debt_in_priority_order( rt.store(), rt.curr_epoch(), @@ -3171,8 +3119,7 @@ impl Actor { .map_err(|e| { e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to repay penalty") })?; - pledge_delta_total -= &penalty_from_vesting; - let to_burn = penalty_from_vesting + penalty_from_balance; + pledge_delta_total -= &total_unlocked; Ok((pledge_delta_total, to_burn)) })?; @@ -3247,7 +3194,7 @@ impl Actor { })?; // Pay penalty - let (penalty_from_vesting, penalty_from_balance) = st + let (mut burn_amount, total_unlocked) = st .repay_partial_debt_in_priority_order( rt.store(), rt.curr_epoch(), @@ -3257,8 +3204,7 @@ impl Actor { e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to pay fees") })?; - let mut burn_amount = &penalty_from_vesting + &penalty_from_balance; - pledge_delta -= penalty_from_vesting; + pledge_delta -= total_unlocked; // clamp reward at funds burnt let reward_amount = std::cmp::min(&burn_amount, &slasher_reward).clone(); @@ -3539,14 +3485,14 @@ impl Actor { } fn repay_debt(rt: &impl Runtime) -> Result<(), ActorError> { - let (from_vesting, from_balance, state) = rt.transaction(|state: &mut State, rt| { + let (burn_amount, total_unlocked, state) = rt.transaction(|state: &mut State, rt| { let info = get_miner_info(rt.store(), state)?; rt.validate_immediate_caller_is( info.control_addresses.iter().chain(&[info.worker, info.owner]), )?; // Repay as much fee debt as possible. - let (from_vesting, from_balance) = state + let (burn_amount, total_unlocked) = state .repay_partial_debt_in_priority_order( rt.store(), rt.curr_epoch(), @@ -3556,11 +3502,10 @@ impl Actor { e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to unlock fee debt") })?; - Ok((from_vesting, from_balance, state.clone())) + Ok((burn_amount, total_unlocked, state.clone())) })?; - let burn_amount = from_balance + &from_vesting; - notify_pledge_changed(rt, &from_vesting.neg())?; + notify_pledge_changed(rt, &total_unlocked.neg())?; burn_funds(rt, burn_amount)?; state.check_balance_invariants(&rt.current_balance()).map_err(balance_invariants_broken)?; @@ -3626,8 +3571,6 @@ pub struct ReplicaUpdateInner { pub deadline: u64, pub partition: u64, pub new_sealed_cid: Cid, - /// None means unknown - pub new_unsealed_cid: Option, pub deals: Vec, pub update_proof_type: RegisteredUpdateProof, pub replica_proof: RawBytes, @@ -3765,26 +3708,59 @@ fn validate_extension_declarations( #[allow(clippy::too_many_arguments)] fn extend_sector_committment( policy: &Policy, + curr_nv: NetworkVersion, curr_epoch: ChainEpoch, + circulating_supply: &TokenAmount, new_expiration: ChainEpoch, - sector: &SectorOnChainInfo, + sector_info: &SectorOnChainInfo, + sector_size: SectorSize, claim_space_by_sector: &BTreeMap, ) -> Result { - validate_extended_expiration(policy, curr_epoch, new_expiration, sector)?; + validate_extended_expiration(policy, curr_epoch, new_expiration, sector_info)?; // all simple_qa_power sectors with VerifiedDealWeight > 0 MUST check all claims - if sector.flags.contains(SectorOnChainInfoFlags::SIMPLE_QA_POWER) { - extend_simple_qap_sector(policy, new_expiration, curr_epoch, sector, claim_space_by_sector) + let mut new_sector_info = if sector_info.flags.contains(SectorOnChainInfoFlags::SIMPLE_QA_POWER) + { + extend_simple_qap_sector( + policy, + new_expiration, + curr_epoch, + sector_info, + claim_space_by_sector, + ) } else { - extend_non_simple_qap_sector(new_expiration, curr_epoch, sector) + extend_non_simple_qap_sector(new_expiration, curr_epoch, sector_info) + }?; + + let new_qa_power = qa_power_for_weight( + sector_size, + new_sector_info.expiration - new_sector_info.power_base_epoch, + &new_sector_info.verified_deal_weight, + ); + if new_sector_info.daily_fee.is_zero() { + // pre-FIP-0100 sector + if curr_nv >= FIP_0100_GRACE_PERIOD_END_VERSION { + new_sector_info.daily_fee = daily_proof_fee(policy, circulating_supply, &new_qa_power); + } // else grace period + } else { + let old_qa_power = qa_power_for_sector(sector_size, sector_info); + if old_qa_power != new_qa_power { + // adjust the daily_fee by the same proportion as the power changed + new_sector_info.daily_fee = + daily_proof_fee_adjust(§or_info.daily_fee, &old_qa_power, &new_qa_power) + } } + Ok(new_sector_info) } fn extend_sector_committment_legacy( policy: &Policy, + curr_nv: NetworkVersion, curr_epoch: ChainEpoch, + circulating_supply: &TokenAmount, new_expiration: ChainEpoch, sector: &SectorOnChainInfo, + sector_size: SectorSize, ) -> Result { validate_extended_expiration(policy, curr_epoch, new_expiration, sector)?; @@ -3798,7 +3774,19 @@ fn extend_sector_committment_legacy( sector.sector_number )); } - extend_non_simple_qap_sector(new_expiration, curr_epoch, sector) + let mut sector = extend_non_simple_qap_sector(new_expiration, curr_epoch, sector)?; + + // adjust daily fee only if this is a legacy sector and we're not in the grace period; no need + // to handle the QAP change case here as QAP can only change with ExtendSectorExpiration2 + if curr_nv >= FIP_0100_GRACE_PERIOD_END_VERSION && sector.daily_fee.is_zero() { + let power = qa_power_for_weight( + sector_size, + sector.expiration - sector.power_base_epoch, + §or.verified_deal_weight, + ); + sector.daily_fee = daily_proof_fee(policy, circulating_supply, &power); + } + Ok(sector) } fn validate_extended_expiration( @@ -3954,15 +3942,6 @@ fn validate_replica_updates<'a, BS>( where BS: Blockstore, { - if updates.len() > policy.prove_replica_updates_max_size { - return Err(actor_error!( - illegal_argument, - "too many updates ({} > {})", - updates.len(), - policy.prove_replica_updates_max_size - )); - } - let mut sector_numbers = BTreeSet::::new(); let mut validate_one = |update: &ReplicaUpdateInner, sector_info: &SectorOnChainInfo| @@ -4134,9 +4113,14 @@ where let quant = state.quant_spec_for_deadline(rt.policy(), dl_idx); + let mut deadline_power_delta = PowerPair::zero(); + let mut deadline_pledge_delta = TokenAmount::zero(); + let mut deadline_daily_fee_delta = TokenAmount::zero(); + for update in updates { // Compute updated sector info. let new_sector_info = update_existing_sector_info( + rt.policy(), update.sector_info, &update.activated_data, &pledge_inputs, @@ -4163,23 +4147,25 @@ where })?; // Note: replacing sectors one at a time in each partition is inefficient. - let (partition_power_delta, partition_pledge_delta) = partition - .replace_sectors( - rt.store(), - std::slice::from_ref(update.sector_info), - std::slice::from_ref(&new_sector_info), - sector_size, - quant, - ) - .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { - format!( - "failed to replace sector at deadline {} partition {}", - update.deadline, update.partition + let (partition_power_delta, partition_pledge_delta, partition_daily_fee_delta) = + partition + .replace_sectors( + rt.store(), + std::slice::from_ref(update.sector_info), + std::slice::from_ref(&new_sector_info), + sector_size, + quant, ) - })?; + .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { + format!( + "failed to replace sector at deadline {} partition {}", + update.deadline, update.partition + ) + })?; - power_delta += &partition_power_delta; - pledge_delta += &partition_pledge_delta; + deadline_power_delta += &partition_power_delta; + deadline_pledge_delta += &partition_pledge_delta; + deadline_daily_fee_delta += &partition_daily_fee_delta; partitions.set(update.partition, partition).with_context_code( ExitCode::USR_ILLEGAL_STATE, @@ -4194,6 +4180,12 @@ where new_sectors.push(new_sector_info); } // End loop over declarations in one deadline. + deadline.live_power += &deadline_power_delta; + deadline.daily_fee += &deadline_daily_fee_delta; + + power_delta += &deadline_power_delta; + pledge_delta += &deadline_pledge_delta; + deadline.partitions = partitions.flush().with_context_code(ExitCode::USR_ILLEGAL_STATE, || { format!("failed to save partitions for deadline {}", dl_idx) @@ -4255,6 +4247,7 @@ where // Builds a new sector info representing newly activated data in an existing sector. fn update_existing_sector_info( + policy: &Policy, sector_info: &SectorOnChainInfo, activated_data: &ReplicaUpdateActivatedData, pledge_inputs: &NetworkPledgeInputs, @@ -4278,7 +4271,8 @@ fn update_existing_sector_info( new_sector_info.verified_deal_weight = activated_data.verified_space.clone() * duration; // compute initial pledge - let qa_pow = qa_power_for_weight(sector_size, duration, &new_sector_info.verified_deal_weight); + let new_qa_power = + qa_power_for_weight(sector_size, duration, &new_sector_info.verified_deal_weight); new_sector_info.expected_day_reward = None; new_sector_info.replaced_day_reward = None; @@ -4287,7 +4281,7 @@ fn update_existing_sector_info( new_sector_info.initial_pledge = max( new_sector_info.initial_pledge, initial_pledge_for_power( - &qa_pow, + &new_qa_power, &pledge_inputs.network_baseline, &pledge_inputs.epoch_reward, &pledge_inputs.network_qap, @@ -4296,6 +4290,19 @@ fn update_existing_sector_info( pledge_inputs.ramp_duration_epochs, ), ); + if new_sector_info.daily_fee.is_zero() { + // pre-FIP-0100 sector + new_sector_info.daily_fee = + daily_proof_fee(policy, &pledge_inputs.circulating_supply, &new_qa_power); + } else { + let old_qa_power = + qa_power_for_weight(sector_size, duration, §or_info.verified_deal_weight); + if old_qa_power != new_qa_power { + // adjust the daily_fee by the same proportion as the power changed + new_sector_info.daily_fee = + daily_proof_fee_adjust(&new_sector_info.daily_fee, &old_qa_power, &new_qa_power) + } + } new_sector_info } @@ -4304,7 +4311,10 @@ fn process_early_terminations( rt: &impl Runtime, reward_smoothed: &FilterEstimate, quality_adj_power_smoothed: &FilterEstimate, -) -> Result { +) -> Result< + bool, // has more + ActorError, +> { let mut terminated_sector_nums = vec![]; let mut sectors_with_data = vec![]; let (result, more, penalty, pledge_delta) = rt.transaction(|state: &mut State, rt| { @@ -4338,7 +4348,7 @@ fn process_early_terminations( for (epoch, sector_numbers) in result.iter() { let sectors = sectors - .load_sector(sector_numbers) + .load_sectors(sector_numbers) .map_err(|e| e.wrap("failed to load sector infos"))?; for sector in §ors { @@ -4371,7 +4381,7 @@ fn process_early_terminations( })?; // Use unlocked pledge to pay down outstanding fee debt - let (penalty_from_vesting, penalty_from_balance) = state + let (penalty, total_unlocked) = state .repay_partial_debt_in_priority_order( rt.store(), rt.curr_epoch(), @@ -4381,8 +4391,7 @@ fn process_early_terminations( e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to repay penalty") })?; - let penalty = &penalty_from_vesting + penalty_from_balance; - pledge_delta -= penalty_from_vesting; + pledge_delta -= total_unlocked; Ok((result, more, penalty, pledge_delta)) })?; @@ -4435,25 +4444,6 @@ fn handle_proving_deadline( let state: State = rt.transaction(|state: &mut State, rt| { let policy = rt.policy(); - // Vesting rewards for a miner are quantized to every 12 hours and we can determine what those "vesting epochs" are. - // So, only do the vesting here if the current epoch is a "vesting epoch" - let q = QuantSpec { - unit: REWARD_VESTING_SPEC.quantization, - offset: state.proving_period_start, - }; - - if q.quantize_up(curr_epoch) == curr_epoch { - // Vest locked funds. - // This happens first so that any subsequent penalties are taken - // from locked vesting funds before funds free this epoch. - let newly_vested = - state.unlock_vested_funds(rt.store(), rt.curr_epoch()).map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to vest funds") - })?; - - pledge_delta_total -= newly_vested; - } - // Process pending worker change if any let mut info = get_miner_info(rt.store(), state)?; process_pending_worker(&mut info, rt, state)?; @@ -4505,7 +4495,25 @@ fn handle_proving_deadline( penalty_target ); - let (penalty_from_vesting, penalty_from_balance) = state + if result.daily_fee.is_positive() { + // Apply daily fee for sectors in this deadline, applied through the penalty/fee_debt + // mechanism. + // The daily fee payable is capped at a fraction of estimated daily block reward for the + // sectors being charged. + let day_reward = expected_reward_for_power( + reward_smoothed, + quality_adj_power_smoothed, + &result.live_power.qa, + fil_actors_runtime::EPOCHS_IN_DAY, + ); + let daily_fee = daily_proof_fee_payable(policy, &result.daily_fee, &day_reward); + + state + .apply_penalty(&daily_fee) + .map_err(|e| actor_error!(illegal_state, "failed to apply penalty: {}", e))?; + } + + let (penalty, total_unlocked) = state .repay_partial_debt_in_priority_order( rt.store(), rt.curr_epoch(), @@ -4515,8 +4523,19 @@ fn handle_proving_deadline( e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to unlock penalty") })?; - penalty_total = &penalty_from_vesting + penalty_from_balance; - pledge_delta_total -= penalty_from_vesting; + penalty_total = penalty; + pledge_delta_total -= total_unlocked; + + // Vest locked funds. Locked funds will already have been vested automatically if we paid + // any fees/penalties, but we try to vest one more time just in case. + // + // If there's nothing to vest, this is an inexpensive operation as it'll just look at the + // "head" of the vesting queue, which is inlined into the root state object. + let newly_vested = state + .unlock_vested_funds(rt.store(), rt.curr_epoch()) + .map_err(|e| e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to vest funds"))?; + + pledge_delta_total -= newly_vested; continue_cron = state.continue_deadline_cron(); if !continue_cron { @@ -4529,6 +4548,7 @@ fn handle_proving_deadline( // Remove power for new faults, and burn penalties. request_update_power(rt, power_delta_total)?; burn_funds(rt, penalty_total)?; + // Update the total locked funds in the network. notify_pledge_changed(rt, &pledge_delta_total)?; // Schedule cron callback for next deadline's last epoch. @@ -5096,29 +5116,6 @@ fn verify_aggregate_seal( .context_code(ExitCode::USR_ILLEGAL_ARGUMENT, "aggregate seal verify failed") } -// Compute and burn the aggregate network fee. -fn pay_aggregate_seal_proof_fee( - rt: &impl Runtime, - aggregate_size: usize, -) -> Result<(), ActorError> { - // State is loaded afresh as earlier operations for sector/data activation can change it. - let state: State = rt.state()?; - let aggregate_fee = aggregate_prove_commit_network_fee(aggregate_size, &rt.base_fee()); - let unlocked_balance = state - .get_unlocked_balance(&rt.current_balance()) - .map_err(|_e| actor_error!(illegal_state, "failed to determine unlocked balance"))?; - if unlocked_balance < aggregate_fee { - return Err(actor_error!( - insufficient_funds, - "remaining unlocked funds after prove-commit {} are insufficient to pay aggregation fee of {}", - unlocked_balance, - aggregate_fee - )); - } - burn_funds(rt, aggregate_fee)?; - state.check_balance_invariants(&rt.current_balance()).map_err(balance_invariants_broken) -} - fn verify_deals( rt: &impl Runtime, sectors: &[ext::market::SectorDeals], @@ -5154,7 +5151,7 @@ fn request_current_epoch_block_reward( Default::default(), TokenAmount::zero(), )) - .map_err(|e| e.wrap("failed to check epoch baseline power"))?, + .map_err(|e| e.wrap("failed to check epoch reward"))?, ) } @@ -5362,6 +5359,10 @@ pub fn power_for_sectors(sector_size: SectorSize, sectors: &[SectorOnChainInfo]) PowerPair { raw: BigInt::from(sector_size as u64) * BigInt::from(sectors.len()), qa } } +pub fn daily_fee_for_sectors(sectors: &[SectorOnChainInfo]) -> TokenAmount { + sectors.iter().map(|s| &s.daily_fee).sum() +} + fn get_miner_info(store: &BS, state: &State) -> Result where BS: Blockstore, @@ -5507,6 +5508,7 @@ fn activate_new_sector_infos( let verified_deal_weight = &deal_spaces.verified_space * duration; let power = qa_power_for_weight(info.sector_size, duration, &verified_deal_weight); + let daily_fee = daily_proof_fee(rt.policy(), &pledge_inputs.circulating_supply, &power); let initial_pledge = initial_pledge_for_power( &power, @@ -5537,6 +5539,7 @@ fn activate_new_sector_infos( replaced_day_reward: None, sector_key_cid: None, flags: SectorOnChainInfoFlags::SIMPLE_QA_POWER, + daily_fee, }; new_sector_numbers.push(new_sector_info.sector_number); diff --git a/actors/miner/src/monies.rs b/actors/miner/src/monies.rs index 30d94d6e8..0b9d76013 100644 --- a/actors/miner/src/monies.rs +++ b/actors/miner/src/monies.rs @@ -1,13 +1,13 @@ // Copyright 2019-2022 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -use std::cmp::{self, max}; +use std::cmp; use fil_actors_runtime::network::EPOCHS_IN_DAY; use fil_actors_runtime::reward::math::PRECISION; use fil_actors_runtime::reward::{smooth, FilterEstimate}; use fil_actors_runtime::EXPECTED_LEADERS_PER_EPOCH; -use fvm_shared::bigint::{BigInt, Integer}; +use fvm_shared::bigint::Integer; use fvm_shared::clock::ChainEpoch; use fvm_shared::econ::TokenAmount; use fvm_shared::sector::StoragePower; @@ -119,10 +119,6 @@ pub fn expected_reward_for_power( pub mod detail { use super::*; - lazy_static! { - pub static ref BATCH_BALANCER: TokenAmount = TokenAmount::from_nano(5); - } - // BR but zero values are clamped at 1 attofil // Some uses of BR (PCD, IP) require a strictly positive value for BR derived values so // accounting variables can be used as succinct indicators of miner activity. @@ -324,35 +320,3 @@ pub fn locked_reward_from_reward(reward: TokenAmount) -> (TokenAmount, &'static let lock_amount = (reward * LOCKED_REWARD_FACTOR_NUM).div_floor(LOCKED_REWARD_FACTOR_DENOM); (lock_amount, &REWARD_VESTING_SPEC) } - -const BATCH_DISCOUNT_NUM: u32 = 1; -const BATCH_DISCOUNT_DENOM: u32 = 20; - -lazy_static! { - static ref ESTIMATED_SINGLE_PROVE_COMMIT_GAS_USAGE: BigInt = BigInt::from(49299973); - static ref ESTIMATED_SINGLE_PRE_COMMIT_GAS_USAGE: BigInt = BigInt::from(16433324); -} - -pub fn aggregate_prove_commit_network_fee( - aggregate_size: usize, - base_fee: &TokenAmount, -) -> TokenAmount { - aggregate_network_fee(aggregate_size, &ESTIMATED_SINGLE_PROVE_COMMIT_GAS_USAGE, base_fee) -} - -pub fn aggregate_pre_commit_network_fee( - aggregate_size: usize, - base_fee: &TokenAmount, -) -> TokenAmount { - aggregate_network_fee(aggregate_size, &ESTIMATED_SINGLE_PRE_COMMIT_GAS_USAGE, base_fee) -} - -pub fn aggregate_network_fee( - aggregate_size: usize, - gas_usage: &BigInt, - base_fee: &TokenAmount, -) -> TokenAmount { - let effective_gas_fee = max(base_fee, &*BATCH_BALANCER); - let network_fee_num = effective_gas_fee * gas_usage * aggregate_size * BATCH_DISCOUNT_NUM; - network_fee_num.div_floor(BATCH_DISCOUNT_DENOM) -} diff --git a/actors/miner/src/partition_state.rs b/actors/miner/src/partition_state.rs index 1eb2b6824..e3a9fb971 100644 --- a/actors/miner/src/partition_state.rs +++ b/actors/miner/src/partition_state.rs @@ -110,6 +110,7 @@ impl Partition { /// AddSectors adds new sectors to the partition. /// The sectors are "live", neither faulty, recovering, nor terminated. /// Each new sector's expiration is scheduled shortly after its target expiration epoch. + /// Returns the power of the new sectors as well as the daily fee total for these new sectors. pub fn add_sectors( &mut self, store: &BS, @@ -117,11 +118,11 @@ impl Partition { sectors: &[SectorOnChainInfo], sector_size: SectorSize, quant: QuantSpec, - ) -> anyhow::Result { + ) -> anyhow::Result<(PowerPair, TokenAmount)> { let mut expirations = ExpirationQueue::new(store, &self.expirations_epochs, quant) .map_err(|e| e.downcast_wrap("failed to load sector expirations"))?; - let (sector_numbers, power, _) = expirations + let (sector_numbers, power, _, daily_fee) = expirations .add_active_sectors(sectors, sector_size) .map_err(|e| e.downcast_wrap("failed to record new sector expirations"))?; @@ -148,7 +149,7 @@ impl Partition { // No change to faults, recoveries, or terminations. // No change to faulty or recovering power. - Ok(power) + Ok((power, daily_fee)) } /// marks a set of sectors faulty @@ -231,8 +232,9 @@ impl Partition { new_faults -= &self.faults; // Add new faults to state. - let new_fault_sectors = - sectors.load_sector(&new_faults).map_err(|e| e.wrap("failed to load fault sectors"))?; + let new_fault_sectors = sectors + .load_sectors(&new_faults) + .map_err(|e| e.wrap("failed to load fault sectors"))?; let (power_delta, new_faulty_power) = if !new_fault_sectors.is_empty() { self.add_faults( @@ -250,7 +252,7 @@ impl Partition { // remove faulty recoveries from state let retracted_recovery_sectors = sectors - .load_sector(&retracted_recoveries) + .load_sectors(&retracted_recoveries) .map_err(|e| e.wrap("failed to load recovery sectors"))?; if !retracted_recovery_sectors.is_empty() { let retracted_recovery_power = @@ -278,7 +280,7 @@ impl Partition { // Process recoveries, assuming the proof will be successful. // This similarly updates state. let recovered_sectors = sectors - .load_sector(&self.recoveries) + .load_sectors(&self.recoveries) .map_err(|e| e.wrap("failed to load recovered sectors"))?; // Load expiration queue @@ -331,7 +333,7 @@ impl Partition { // Record the new recoveries for processing at Window PoSt or deadline cron. let recovery_sectors = sectors - .load_sector(&recoveries) + .load_sectors(&recoveries) .map_err(|e| e.wrap("failed to load recovery sectors"))?; self.recoveries |= &recoveries; @@ -388,7 +390,7 @@ impl Partition { // Filter out faulty sectors. let active = &live - &self.faults; - let sector_infos = sectors.load_sector(&active)?; + let sector_infos = sectors.load_sectors(&active)?; let mut expirations = ExpirationQueue::new(store, &self.expirations_epochs, quant) .map_err(|e| e.downcast_wrap("failed to load sector expirations"))?; expirations.reschedule_expirations(new_expiration, §or_infos, sector_size)?; @@ -412,13 +414,14 @@ impl Partition { new_sectors: &[SectorOnChainInfo], sector_size: SectorSize, quant: QuantSpec, - ) -> anyhow::Result<(PowerPair, TokenAmount)> { + ) -> anyhow::Result<(PowerPair, TokenAmount, TokenAmount)> { let mut expirations = ExpirationQueue::new(store, &self.expirations_epochs, quant) .map_err(|e| e.downcast_wrap("failed to load sector expirations"))?; - let (old_sector_numbers, new_sector_numbers, power_delta, pledge_delta) = expirations - .replace_sectors(old_sectors, new_sectors, sector_size) - .map_err(|e| e.downcast_wrap("failed to replace sector expirations"))?; + let (old_sector_numbers, new_sector_numbers, power_delta, pledge_delta, fee_delta) = + expirations + .replace_sectors(old_sectors, new_sectors, sector_size) + .map_err(|e| e.downcast_wrap("failed to replace sector expirations"))?; self.expirations_epochs = expirations .amt @@ -447,7 +450,7 @@ impl Partition { // No change to faults, recoveries, or terminations. // No change to faulty or recovering power. - Ok((power_delta, pledge_delta)) + Ok((power_delta, pledge_delta, fee_delta)) } /// Record the epoch of any sectors expiring early, for termination fee calculation later. @@ -486,14 +489,14 @@ impl Partition { sector_numbers: &BitField, sector_size: SectorSize, quant: QuantSpec, - ) -> anyhow::Result { + ) -> anyhow::Result<(ExpirationSet, PowerPair)> { let live_sectors = self.live_sectors(); if !live_sectors.contains_all(sector_numbers) { return Err(actor_error!(illegal_argument, "can only terminate live sectors").into()); } - let sector_infos = sectors.load_sector(sector_numbers)?; + let sector_infos = sectors.load_sectors(sector_numbers)?; let mut expirations = ExpirationQueue::new(store, &self.expirations_epochs, quant) .map_err(|e| e.downcast_wrap("failed to load sector expirations"))?; let (mut removed, removed_recovering) = expirations @@ -531,7 +534,7 @@ impl Partition { // check invariants self.validate_state()?; - Ok(removed) + Ok((removed, removed_unproven_power)) } /// PopExpiredSectors traverses the expiration queue up to and including some epoch, and marks all expiring @@ -733,14 +736,14 @@ impl Partition { // Find all skipped faults that have been labeled recovered let retracted_recoveries = &self.recoveries & skipped; let retracted_recovery_sectors = sectors - .load_sector(&retracted_recoveries) + .load_sectors(&retracted_recoveries) .map_err(|e| e.wrap("failed to load sectors"))?; let retracted_recovery_power = power_for_sectors(sector_size, &retracted_recovery_sectors); // Ignore skipped faults that are already faults or terminated. let new_faults = &(skipped - &self.terminated) - &self.faults; let new_fault_sectors = - sectors.load_sector(&new_faults).map_err(|e| e.wrap("failed to load sectors"))?; + sectors.load_sectors(&new_faults).map_err(|e| e.wrap("failed to load sectors"))?; // Record new faults let (power_delta, new_fault_power) = self diff --git a/actors/miner/src/policy.rs b/actors/miner/src/policy.rs index 97cb52796..012c1ec9f 100644 --- a/actors/miner/src/policy.rs +++ b/actors/miner/src/policy.rs @@ -12,6 +12,7 @@ use fvm_shared::clock::ChainEpoch; use fvm_shared::commcid::{FIL_COMMITMENT_SEALED, POSEIDON_BLS12_381_A1_FC1}; use fvm_shared::econ::TokenAmount; use fvm_shared::sector::{RegisteredPoStProof, RegisteredSealProof, SectorSize, StoragePower}; +use fvm_shared::version::NetworkVersion; use lazy_static::lazy_static; use super::types::SectorOnChainInfo; @@ -20,16 +21,12 @@ use super::{PowerPair, BASE_REWARD_FOR_DISPUTED_WINDOW_POST}; /// Precision used for making QA power calculations pub const SECTOR_QUALITY_PRECISION: i64 = 20; -/// Base number of sectors before imposing the additional aggregate fee in ProveCommitSectorsNI -pub const NI_AGGREGATE_FEE_BASE_SECTOR_COUNT: usize = 5; - lazy_static! { /// Quality multiplier for committed capacity (no deals) in a sector pub static ref QUALITY_BASE_MULTIPLIER: BigInt = BigInt::from(10); /// Quality multiplier for verified deals in a sector pub static ref VERIFIED_DEAL_WEIGHT_MULTIPLIER: BigInt = BigInt::from(100); - } /// The maximum number of partitions that may be required to be loaded in a single invocation, @@ -206,3 +203,49 @@ pub fn reward_for_disputed_window_post( // This is currently just the base. In the future, the fee may scale based on the disputed power. BASE_REWARD_FOR_DISPUTED_WINDOW_POST.clone() } + +// Network version at which the FIP-0100 grace period for application of the fee to legacy sector +// extensions ends. After we reach this network version, the fee will be applied to all sector +// extensions which currently have a zero fee value. +pub const FIP_0100_GRACE_PERIOD_END_VERSION: NetworkVersion = NetworkVersion::new(26); + +// Calculate the daily fee for a sector's quality-adjusted power based on the current circulating +// supply. +pub fn daily_proof_fee( + policy: &Policy, + circulating_supply: &TokenAmount, + qa_power: &StoragePower, +) -> TokenAmount { + // daily_fee_circulating_supply_qap_multiplier{num/denom} gives us the fraction of the + // circulating supply that should be paid as a fee per byte of quality-adjusted power. + TokenAmount::from_atto( + (&policy.daily_fee_circulating_supply_qap_multiplier_num + * circulating_supply.atto() + * qa_power) + .div_floor(&policy.daily_fee_circulating_supply_qap_multiplier_denom), + ) +} + +// Adjust the daily fee based on the change in quality-adjusted power. +pub fn daily_proof_fee_adjust( + daily_fee: &TokenAmount, + old_qa_power: &StoragePower, + new_qa_power: &StoragePower, +) -> TokenAmount { + if old_qa_power == new_qa_power { + return daily_fee.clone(); + } + TokenAmount::from_atto((daily_fee.atto() * new_qa_power).div_floor(old_qa_power)) +} + +// Given a daily fee payable and an estimated BR for the sector(s) the fee is being paid for, +// calculate the fee payable for the sector(s) by applying the appropriate BR cap. +pub fn daily_proof_fee_payable( + policy: &Policy, + daily_fee: &TokenAmount, + estimated_day_reward: &TokenAmount, +) -> TokenAmount { + let cap_denom = BigInt::from(policy.daily_fee_block_reward_cap_denom); + let cap = estimated_day_reward.div_floor(cap_denom); + std::cmp::min(&cap, daily_fee).clone() +} diff --git a/actors/miner/src/sectors.rs b/actors/miner/src/sectors.rs index 4de55a949..870443661 100644 --- a/actors/miner/src/sectors.rs +++ b/actors/miner/src/sectors.rs @@ -24,7 +24,7 @@ impl<'db, BS: Blockstore> Sectors<'db, BS> { Ok(Self { amt: Array::load(root, store)? }) } - pub fn load_sector( + pub fn load_sectors( &self, sector_numbers: &BitField, ) -> Result, ActorError> { @@ -46,6 +46,21 @@ impl<'db, BS: Blockstore> Sectors<'db, BS> { Ok(sector_infos) } + pub fn delete_sectors(&mut self, sector_numbers: &BitField) -> Result<(), ActorError> { + for sector_num in sector_numbers.iter() { + let deleted_sector = self.amt.delete(sector_num).map_err(|e| { + e.downcast_default( + ExitCode::USR_ILLEGAL_STATE, + format!("failed to delete sector {}", sector_num), + ) + })?; + if deleted_sector.is_none() { + return Err(actor_error!(not_found; "sector not found: {}", sector_num)); + } + } + Ok(()) + } + pub fn get( &self, sector_number: SectorNumber, diff --git a/actors/miner/src/state.rs b/actors/miner/src/state.rs index 0ba88729a..3db8fc2a6 100644 --- a/actors/miner/src/state.rs +++ b/actors/miner/src/state.rs @@ -5,9 +5,8 @@ use std::borrow::Borrow; use std::cmp; use std::ops::Neg; -use anyhow::{anyhow, Error}; +use anyhow::anyhow; use cid::Cid; -use fvm_ipld_amt::Error as AmtError; use fvm_ipld_bitfield::BitField; use fvm_ipld_blockstore::Blockstore; use fvm_ipld_encoding::tuple::*; @@ -64,7 +63,7 @@ pub struct State { pub locked_funds: TokenAmount, /// VestingFunds (Vesting Funds schedule for the miner). - pub vesting_funds: Cid, + pub vesting_funds: VestingFunds, /// Absolute value of debt this miner owes from unpaid fees. pub fee_debt: TokenAmount, @@ -164,18 +163,13 @@ impl State { e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to construct illegal state") })?; - let empty_vesting_funds_cid = - store.put_cbor(&VestingFunds::new(), Code::Blake2b256).map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to construct illegal state") - })?; - Ok(Self { info: info_cid, pre_commit_deposits: TokenAmount::default(), locked_funds: TokenAmount::default(), - vesting_funds: empty_vesting_funds_cid, + vesting_funds: VestingFunds::new(), initial_pledge: TokenAmount::default(), fee_debt: TokenAmount::default(), @@ -393,30 +387,6 @@ impl State { sectors.get(sector_num) } - pub fn delete_sectors( - &mut self, - store: &BS, - sector_nos: &BitField, - ) -> Result<(), AmtError> { - let mut sectors = Sectors::load(store, &self.sectors)?; - - for sector_num in sector_nos.iter() { - let deleted_sector = sectors - .amt - .delete(sector_num) - .map_err(|e| e.downcast_wrap("could not delete sector number"))?; - if deleted_sector.is_none() { - return Err(AmtError::Dynamic(Error::msg(format!( - "sector {} doesn't exist, failed to delete", - sector_num - )))); - } - } - - self.sectors = sectors.amt.flush()?; - Ok(()) - } - pub fn for_each_sector(&self, store: &BS, mut f: F) -> anyhow::Result<()> where F: FnMut(&SectorOnChainInfo) -> anyhow::Result<()>, @@ -536,10 +506,13 @@ impl State { // The power returned from AddSectors is ignored because it's not activated (proven) yet. let proven = false; + // New sectors, so the deadline has new fees. + let new_fees = true; deadline.add_sectors( store, partition_size, proven, + new_fees, &deadline_sectors, sector_size, quant, @@ -585,8 +558,9 @@ impl State { let quant = self.quant_spec_for_deadline(policy, deadline_idx); let proven = false; + let new_fees = true; // New sectors, so the deadline has new fees. deadline - .add_sectors(store, partition_size, proven, §ors, sector_size, quant) + .add_sectors(store, partition_size, proven, new_fees, §ors, sector_size, quant) .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { format!("failed to add sectors to deadline {}", deadline_idx) })?; @@ -760,7 +734,7 @@ impl State { store: &BS, sectors: &BitField, ) -> anyhow::Result> { - Ok(Sectors::load(store, &self.sectors)?.load_sector(sectors)?) + Ok(Sectors::load(store, &self.sectors)?.load_sectors(sectors)?) } pub fn load_deadlines(&self, store: &BS) -> Result { @@ -783,28 +757,6 @@ impl State { Ok(()) } - /// Loads the vesting funds table from the store. - pub fn load_vesting_funds(&self, store: &BS) -> anyhow::Result { - Ok(store - .get_cbor(&self.vesting_funds) - .map_err(|e| { - e.downcast_wrap(format!("failed to load vesting funds {}", self.vesting_funds)) - })? - .ok_or_else( - || actor_error!(not_found; "failed to load vesting funds {:?}", self.vesting_funds), - )?) - } - - /// Saves the vesting table to the store. - pub fn save_vesting_funds( - &mut self, - store: &BS, - funds: &VestingFunds, - ) -> anyhow::Result<()> { - self.vesting_funds = store.put_cbor(funds, Code::Blake2b256)?; - Ok(()) - } - // Return true when the miner actor needs to continue scheduling deadline crons pub fn continue_deadline_cron(&self) -> bool { !self.pre_commit_deposits.is_zero() @@ -864,11 +816,17 @@ impl State { if vesting_sum.is_negative() { return Err(anyhow!("negative vesting sum {}", vesting_sum)); } + // add new funds and unlock already vested funds. + let amount_unlocked = self.vesting_funds.add_locked_funds( + store, + current_epoch, + vesting_sum, + self.proving_period_start, + spec, + )?; - let mut vesting_funds = self.load_vesting_funds(store)?; - - // unlock vested funds first - let amount_unlocked = vesting_funds.unlock_vested_funds(current_epoch); + // We shouldn't unlock any of the new funds, so the locked funds should remain non-negative + // when we deduct the amount unlocked. self.locked_funds -= &amount_unlocked; if self.locked_funds.is_negative() { return Err(anyhow!( @@ -877,20 +835,17 @@ impl State { amount_unlocked )); } - // add locked funds now - vesting_funds.add_locked_funds(current_epoch, vesting_sum, self.proving_period_start, spec); - self.locked_funds += vesting_sum; - // save the updated vesting table state - self.save_vesting_funds(store, &vesting_funds)?; + // Finally, record the new locked-funds total. + self.locked_funds += vesting_sum; Ok(amount_unlocked) } /// Draws from vesting table and unlocked funds to repay up to the fee debt. - /// Returns the amount unlocked from the vesting table and the amount taken from - /// current balance. If the fee debt exceeds the total amount available for repayment - /// the fee debt field is updated to track the remaining debt. Otherwise it is set to zero. + /// Returns the amount to burn and the total amount unlocked from the vesting table (both vested + /// and unvested) If the fee debt exceeds the total amount available for repayment the fee debt + /// field is updated to track the remaining debt. Otherwise it is set to zero. pub fn repay_partial_debt_in_priority_order( &mut self, store: &BS, @@ -898,25 +853,26 @@ impl State { curr_balance: &TokenAmount, ) -> Result< ( - TokenAmount, // from vesting - TokenAmount, // from balance + TokenAmount, // fee to burn + TokenAmount, // total unlocked ), anyhow::Error, > { - let unlocked_balance = self.get_unlocked_balance(curr_balance)?; - let fee_debt = self.fee_debt.clone(); - let from_vesting = self.unlock_unvested_funds(store, current_epoch, &fee_debt)?; + let (from_vesting, total_unlocked) = + self.unlock_vested_and_unvested_funds(store, current_epoch, &fee_debt)?; if from_vesting > self.fee_debt { return Err(anyhow!("should never unlock more than the debt we need to repay")); } - self.fee_debt -= &from_vesting; - let from_balance = cmp::min(&unlocked_balance, &self.fee_debt).clone(); - self.fee_debt -= &from_balance; + // locked unvested funds should now have been moved to unlocked balance if + // there was enough to cover the fee debt + let unlocked_balance = self.get_unlocked_balance(curr_balance)?; + let to_burn = cmp::min(&unlocked_balance, &self.fee_debt).clone(); + self.fee_debt -= &to_burn; - Ok((from_vesting, from_balance)) + Ok((to_burn, total_unlocked)) } /// Repays the full miner actor fee debt. Returns the amount that must be @@ -937,32 +893,37 @@ impl State { Ok(std::mem::take(&mut self.fee_debt)) } - /// Unlocks an amount of funds that have *not yet vested*, if possible. - /// The soonest-vesting entries are unlocked first. - /// Returns the amount actually unlocked. - pub fn unlock_unvested_funds( + + /// Unlocks all vested funds and then unlocks an amount of funds that have *not yet vested*, if + /// possible. The soonest-vesting entries are unlocked first. + /// + /// Returns the amount of unvested funds unlocked, along with the total amount of funds unlocked. + pub fn unlock_vested_and_unvested_funds( &mut self, store: &BS, current_epoch: ChainEpoch, target: &TokenAmount, - ) -> anyhow::Result { + ) -> anyhow::Result<( + TokenAmount, // unlocked_unvested + TokenAmount, // total_unlocked + )> { if target.is_zero() || self.locked_funds.is_zero() { - return Ok(TokenAmount::zero()); + return Ok((TokenAmount::zero(), TokenAmount::zero())); } - let mut vesting_funds = self.load_vesting_funds(store)?; - let amount_unlocked = vesting_funds.unlock_unvested_funds(current_epoch, target); - self.locked_funds -= &amount_unlocked; + let (unlocked_vested, unlocked_unvested) = + self.vesting_funds.unlock_vested_and_unvested_funds(store, current_epoch, target)?; + let total_unlocked = &unlocked_vested + &unlocked_unvested; + self.locked_funds -= &total_unlocked; if self.locked_funds.is_negative() { return Err(anyhow!( "negative locked funds {} after unlocking {}", self.locked_funds, - amount_unlocked + total_unlocked, )); } - self.save_vesting_funds(store, &vesting_funds)?; - Ok(amount_unlocked) + Ok((unlocked_unvested, total_unlocked)) } /// Unlocks all vesting funds that have vested before the provided epoch. @@ -976,8 +937,7 @@ impl State { return Ok(TokenAmount::zero()); } - let mut vesting_funds = self.load_vesting_funds(store)?; - let amount_unlocked = vesting_funds.unlock_vested_funds(current_epoch); + let amount_unlocked = self.vesting_funds.unlock_vested_funds(store, current_epoch)?; self.locked_funds -= &amount_unlocked; if self.locked_funds.is_negative() { return Err(anyhow!( @@ -986,24 +946,9 @@ impl State { )); } - self.save_vesting_funds(store, &vesting_funds)?; Ok(amount_unlocked) } - /// CheckVestedFunds returns the amount of vested funds that have vested before the provided epoch. - pub fn check_vested_funds( - &self, - store: &BS, - current_epoch: ChainEpoch, - ) -> anyhow::Result { - let vesting_funds = self.load_vesting_funds(store)?; - Ok(vesting_funds - .funds - .iter() - .take_while(|fund| fund.epoch < current_epoch) - .fold(TokenAmount::zero(), |acc, fund| acc + &fund.amount)) - } - /// Unclaimed funds that are not locked -- includes funds used to cover initial pledge requirement. pub fn get_unlocked_balance(&self, actor_balance: &TokenAmount) -> anyhow::Result { let unlocked_balance = @@ -1040,7 +985,7 @@ impl State { let min_balance = &self.pre_commit_deposits + &self.locked_funds + &self.initial_pledge; if balance < &min_balance { - return Err(anyhow!("fee debt is negative: {}", self.fee_debt)); + return Err(anyhow!("balance {} below minimum {}", balance, min_balance)); } Ok(()) @@ -1142,6 +1087,8 @@ impl State { previously_faulty_power: PowerPair::zero(), detected_faulty_power: PowerPair::zero(), total_faulty_power: PowerPair::zero(), + daily_fee: TokenAmount::zero(), + live_power: PowerPair::zero(), }); } @@ -1163,6 +1110,8 @@ impl State { previously_faulty_power, detected_faulty_power: PowerPair::zero(), total_faulty_power: deadline.faulty_power, + daily_fee: TokenAmount::zero(), + live_power: PowerPair::zero(), }); } @@ -1206,6 +1155,8 @@ impl State { previously_faulty_power, detected_faulty_power, total_faulty_power, + daily_fee: deadline.daily_fee, + live_power: deadline.live_power, }) } @@ -1244,6 +1195,10 @@ pub struct AdvanceDeadlineResult { /// Note that failed recovery power is included in both PreviouslyFaultyPower and /// DetectedFaultyPower, so TotalFaultyPower is not simply their sum. pub total_faulty_power: PowerPair, + /// Fee payable for the sectors in the deadline being advanced + pub daily_fee: TokenAmount, + /// Total power for the deadline, including active, faulty, and unproven + pub live_power: PowerPair, } /// Static information about miner diff --git a/actors/miner/src/testing.rs b/actors/miner/src/testing.rs index 64beb603a..2855295a2 100644 --- a/actors/miner/src/testing.rs +++ b/actors/miner/src/testing.rs @@ -1,7 +1,7 @@ use crate::{ - power_for_sectors, BitFieldQueue, Deadline, ExpirationQueue, MinerInfo, Partition, PowerPair, - PreCommitMap, QuantSpec, SectorOnChainInfo, SectorOnChainInfoFlags, Sectors, State, - NO_QUANTIZATION, PRECOMMIT_CONFIG, + daily_fee_for_sectors, power_for_sectors, BitFieldQueue, Deadline, ExpirationQueue, MinerInfo, + Partition, PowerPair, PreCommitMap, QuantSpec, SectorOnChainInfo, SectorOnChainInfoFlags, + Sectors, State, NO_QUANTIZATION, PRECOMMIT_CONFIG, }; use fil_actors_runtime::runtime::Policy; use fil_actors_runtime::{DealWeight, MessageAccumulator}; @@ -131,6 +131,7 @@ pub fn check_state_invariants( miner_summary.live_power += &deadline_summary.live_power; miner_summary.active_power += &deadline_summary.active_power; miner_summary.faulty_power += &deadline_summary.faulty_power; + miner_summary.daily_fee += &deadline_summary.daily_fee; Ok(()) }); @@ -160,6 +161,7 @@ pub struct StateSummary { pub deadline_cron_active: bool, // sectors with non zero (verified) deal weight that may carry deals pub live_data_sectors: BTreeMap, + pub daily_fee: TokenAmount, } impl Default for StateSummary { @@ -171,6 +173,7 @@ impl Default for StateSummary { window_post_proof_type: RegisteredPoStProof::Invalid(0), deadline_cron_active: false, live_data_sectors: BTreeMap::new(), + daily_fee: TokenAmount::zero(), } } } @@ -273,10 +276,10 @@ fn check_miner_balances( // locked funds must be sum of vesting table and vesting table payments must be quantized let mut vesting_sum = TokenAmount::zero(); - match state.load_vesting_funds(store) { + match state.vesting_funds.load(store) { Ok(funds) => { let quant = state.quant_spec_every_deadline(policy); - funds.funds.iter().for_each(|entry| { + funds.into_iter().for_each(|entry| { acc.require( entry.amount.is_positive(), format!("non-positive amount in miner vesting table entry {entry:?}"), @@ -385,6 +388,7 @@ pub struct DeadlineStateSummary { pub live_power: PowerPair, pub active_power: PowerPair, pub faulty_power: PowerPair, + pub daily_fee: TokenAmount, } pub type SectorsMap = BTreeMap; @@ -404,6 +408,7 @@ pub struct PartitionStateSummary { // Epochs at which some sector is scheduled to expire. pub expiration_epochs: Vec, pub early_termination_count: usize, + pub daily_fee: TokenAmount, } impl PartitionStateSummary { @@ -464,6 +469,7 @@ impl PartitionStateSummary { let mut live_power = PowerPair::zero(); let mut faulty_power = PowerPair::zero(); let mut unproven_power = PowerPair::zero(); + let mut daily_fee = TokenAmount::zero(); let (live_sectors, missing) = select_sectors_map(sectors_map, &live); if missing.is_empty() { @@ -473,6 +479,7 @@ impl PartitionStateSummary { partition.live_power == live_power, format!("live power was {:?}, expected {:?}", partition.live_power, live_power), ); + daily_fee = daily_fee_for_sectors(&live_sectors.values().cloned().collect::>()); } else { acc.add(format!("live sectors missing from all sectors: {missing:?}")); } @@ -553,6 +560,13 @@ impl PartitionStateSummary { let queue_sectors = BitField::union([&queue_summary.on_time_sectors, &queue_summary.early_sectors]); require_equal(&live, &queue_sectors, acc, "live does not equal all expirations"); + acc.require( + queue_summary.fee_deduction == daily_fee, + format!( + "total fee deductions {:?} does not equal daily fee {:?}", + queue_summary.fee_deduction, daily_fee + ), + ); } Err(err) => { acc.add(format!("error loading expiration_queue: {err}")); @@ -583,6 +597,7 @@ impl PartitionStateSummary { recovering_power: partition.recovering_power, expiration_epochs, early_termination_count, + daily_fee, } } } @@ -598,6 +613,7 @@ struct ExpirationQueueStateSummary { #[allow(dead_code)] pub on_time_pledge: TokenAmount, pub expiration_epochs: Vec, + pub fee_deduction: TokenAmount, } impl ExpirationQueueStateSummary { @@ -617,6 +633,7 @@ impl ExpirationQueueStateSummary { let mut all_active_power = PowerPair::zero(); let mut all_faulty_power = PowerPair::zero(); let mut all_on_time_pledge = TokenAmount::zero(); + let mut all_fee_deductions = TokenAmount::zero(); let ret = expiration_queue.amt.for_each(|epoch, expiration_set| { let epoch = epoch as i64; @@ -627,6 +644,7 @@ impl ExpirationQueueStateSummary { expiration_epochs.push(epoch); let mut on_time_sectors_pledge = TokenAmount::zero(); + let mut total_daily_fee = TokenAmount::zero(); for sector_number in expiration_set.on_time_sectors.iter() { // check sectors are present only once if !seen_sectors.insert(sector_number) { @@ -638,6 +656,7 @@ impl ExpirationQueueStateSummary { let target = quant.quantize_up(sector.expiration); acc.require(epoch == target, format!("invalid expiration {epoch} for sector {sector_number}, expected {target}")); on_time_sectors_pledge += sector.initial_pledge.clone(); + total_daily_fee += sector.daily_fee.clone(); } else { acc.add(format!("on time expiration sector {sector_number} isn't live")); } @@ -656,6 +675,7 @@ impl ExpirationQueueStateSummary { if let Some(sector) = live_sectors.get(§or_number) { let target = quant.quantize_up(sector.expiration); acc.require(epoch < target, format!("invalid early expiration {epoch} for sector {sector_number}, expected < {target}")); + total_daily_fee += sector.daily_fee.clone(); } else { acc.add(format!("on time expiration sector {sector_number} isn't live")); } @@ -680,11 +700,14 @@ impl ExpirationQueueStateSummary { acc.require(expiration_set.on_time_pledge == on_time_sectors_pledge, format!("on time pledge recorded {} doesn't match computed: {on_time_sectors_pledge}", expiration_set.on_time_pledge)); + acc.require(expiration_set.fee_deduction == total_daily_fee, format!("fee deduction recorded {} doesn't match computed: {total_daily_fee}", expiration_set.fee_deduction)); + all_on_time.push(expiration_set.on_time_sectors.clone()); all_early.push(expiration_set.early_sectors.clone()); all_active_power += &expiration_set.active_power; all_faulty_power += &expiration_set.faulty_power; all_on_time_pledge += &expiration_set.on_time_pledge; + all_fee_deductions += &expiration_set.fee_deduction; Ok(()) }); @@ -700,6 +723,7 @@ impl ExpirationQueueStateSummary { faulty_power: all_faulty_power, on_time_pledge: all_on_time_pledge, expiration_epochs, + fee_deduction: all_fee_deductions, } } } @@ -806,6 +830,7 @@ pub fn check_deadline_state_invariants( let mut all_live_power = PowerPair::zero(); let mut all_active_power = PowerPair::zero(); let mut all_faulty_power = PowerPair::zero(); + let mut all_daily_fees = TokenAmount::zero(); let mut partition_count = 0; @@ -855,6 +880,7 @@ pub fn check_deadline_state_invariants( all_live_power += &summary.live_power; all_active_power += &summary.active_power; all_faulty_power += &summary.faulty_power; + all_daily_fees += &summary.daily_fee; Ok(()) }) @@ -957,6 +983,14 @@ pub fn check_deadline_state_invariants( ), ); + acc.require( + deadline.live_power == all_live_power, + format!( + "deadline live power {:?} != partitions total {all_live_power:?}", + deadline.live_power + ), + ); + // Validate partition expiration queue contains an entry for each partition and epoch with an expiration. // The queue may be a superset of the partitions that have expirations because we never remove from it. match BitFieldQueue::new(store, &deadline.expirations_epochs, quant) { @@ -986,6 +1020,17 @@ pub fn check_deadline_state_invariants( "deadline early terminations doesn't match expected partitions", ); + let live_sectors_daily_fee = daily_fee_for_sectors( + &live_sectors.iter().map(|n| sectors.get(&n).unwrap().clone()).collect::>(), + ); + acc.require( + deadline.daily_fee == live_sectors_daily_fee, + format!( + "deadline daily fee {:?} != sum of live sectors daily fees {:?}", + deadline.daily_fee, live_sectors_daily_fee, + ), + ); + DeadlineStateSummary { all_sectors, live_sectors, @@ -996,5 +1041,6 @@ pub fn check_deadline_state_invariants( live_power: all_live_power, active_power: all_active_power, faulty_power: all_faulty_power, + daily_fee: all_daily_fees, } } diff --git a/actors/miner/src/types.rs b/actors/miner/src/types.rs index f8437dbb8..082b90793 100644 --- a/actors/miner/src/types.rs +++ b/actors/miner/src/types.rs @@ -461,6 +461,16 @@ pub struct SectorOnChainInfo { pub sector_key_cid: Option, /// Additional flags, see [`SectorOnChainInfoFlags`] pub flags: SectorOnChainInfoFlags, + /// The total fee payable per day for this sector. The value of this field is set at the time of + /// sector activation, extension and whenever a sector's QAP is changed. This fee is payable for + /// the lifetime of the sector and is aggregated in the deadline's `daily_fee` field. + /// + /// This field is not included in the serialised form of the struct prior to the activation of + /// FIP-0100, and is added as the 16th element of the array after that point only for new sectors + /// or sectors that are updated after that point. For old sectors, the value of this field will + /// always be zero. + #[serde(default)] + pub daily_fee: TokenAmount, } bitflags::bitflags! { diff --git a/actors/miner/src/vesting_state.rs b/actors/miner/src/vesting_state.rs index de247d383..8ca9ca1fe 100644 --- a/actors/miner/src/vesting_state.rs +++ b/actors/miner/src/vesting_state.rs @@ -1,58 +1,135 @@ // Copyright 2019-2022 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -use std::{iter, mem}; +use std::iter; +use cid::Cid; +use fil_actors_runtime::{ActorError, AsActorError}; +use fvm_ipld_blockstore::Blockstore; +use fvm_ipld_encoding::serde::{Deserialize, Serialize}; use fvm_ipld_encoding::tuple::*; +use fvm_ipld_encoding::CborStore; use fvm_shared::clock::ChainEpoch; use fvm_shared::econ::TokenAmount; -use itertools::{EitherOrBoth, Itertools}; +use fvm_shared::error::ExitCode; +use itertools::{EitherOrBoth, Itertools, PeekingNext}; +use multihash_codetable::Code; use num_traits::Zero; use super::{QuantSpec, VestSpec}; // Represents miner funds that will vest at the given epoch. -#[derive(Debug, Serialize_tuple, Deserialize_tuple)] +#[derive(Default, Debug, Serialize_tuple, Deserialize_tuple, Clone)] pub struct VestingFund { pub epoch: ChainEpoch, pub amount: TokenAmount, } -/// Represents the vesting table state for the miner. -/// It is a slice of (VestingEpoch, VestingAmount). -/// The slice will always be sorted by the VestingEpoch. -#[derive(Serialize_tuple, Deserialize_tuple, Default)] -pub struct VestingFunds { - pub funds: Vec, +/// Represents the vesting table state for the miner. It's composed of a `head` (stored inline) and +/// a tail (referenced by CID). +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[serde(transparent)] +pub struct VestingFunds(Option); + +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] +struct VestingFundsInner { + // The "next" batch of vesting funds. + head: VestingFund, + // The rest of the vesting funds, if any. + tail: Cid, // Vec +} + +/// Take vested funds from the passed iterator. This assumes the iterator returns `VestingFund`s in +/// epoch order. +fn take_vested( + iter: &mut impl PeekingNext, + current_epoch: ChainEpoch, +) -> TokenAmount { + iter.peeking_take_while(|fund| fund.epoch < current_epoch).map(|f| f.amount).sum() } impl VestingFunds { pub fn new() -> Self { - Default::default() + Self(None) + } + + pub fn load(&self, store: &impl Blockstore) -> Result, ActorError> { + let Some(this) = &self.0 else { return Ok(Vec::new()) }; + let mut funds: Vec<_> = store + .get_cbor(&this.tail) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load the vesting funds")? + .context_code(ExitCode::USR_ILLEGAL_STATE, "missing vesting funds state")?; + + // NOTE: we allow head to be drawn down to zero through fees/penalties. However, when + // inspecting the vesting table, we never want to see a "zero" entry so we skip it in + // that case. + if this.head.amount.is_positive() { + funds.insert(0, this.head.clone()); + } + Ok(funds) + } + + fn save( + &mut self, + store: &impl Blockstore, + funds: impl IntoIterator, + ) -> Result<(), ActorError> { + let mut funds = funds.into_iter(); + let Some(head) = funds.next() else { + self.0 = None; + return Ok(()); + }; + + let tail = store + .put_cbor(&funds.collect_vec(), Code::Blake2b256) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to store the vesting funds")?; + + self.0 = Some(VestingFundsInner { head, tail }); + Ok(()) } - pub fn unlock_vested_funds(&mut self, current_epoch: ChainEpoch) -> TokenAmount { - // TODO: the funds are sorted by epoch, so we could do a binary search here - let i = self - .funds - .iter() - .position(|fund| fund.epoch >= current_epoch) - .unwrap_or(self.funds.len()); + fn can_vest(&self, current_epoch: ChainEpoch) -> bool { + self.0.as_ref().map(|v| v.head.epoch < current_epoch).unwrap_or(false) + } - self.funds.drain(..i).map(|f| f.amount).sum() + pub fn unlock_vested_funds( + &mut self, + store: &impl Blockstore, + current_epoch: ChainEpoch, + ) -> Result { + if !self.can_vest(current_epoch) { + return Ok(TokenAmount::zero()); + } + let mut funds = self.load(store)?.into_iter().peekable(); + let unlocked = take_vested(&mut funds, current_epoch); + self.save(store, funds)?; + Ok(unlocked) } + // Adds locked funds and unlocks everything that has already vested. pub fn add_locked_funds( &mut self, + store: &impl Blockstore, current_epoch: ChainEpoch, vesting_sum: &TokenAmount, proving_period_start: ChainEpoch, spec: &VestSpec, - ) { - // Quantization is aligned with when regular cron will be invoked, in the last epoch of deadlines. + ) -> Result { + // Quantization is aligned with the beginning of the next 0th or 23rd deadline, whichever + // comes first, and funds vest the epoch _after_ the quantized epoch (vesting_epoch < + // current_epoch). + // + // This means that: + // + // 1. Vesting funds will become available to withdraw the first epoch after the start of the + // 0th or 23rd deadline. + // 2. Vesting funds won't automatically vest in cron until the next deadline (the 1st or the + // 24th). + let vest_begin = current_epoch + spec.initial_delay; // Nothing unlocks here, this is just the start of the clock. - let mut vested_so_far = TokenAmount::zero(); + let quant = QuantSpec { unit: spec.quantization, offset: proving_period_start }; + let mut vested_so_far = TokenAmount::zero(); let mut epoch = vest_begin; // Create an iterator for the vesting schedule we're going to "join" with the current @@ -64,8 +141,7 @@ impl VestingFunds { epoch += spec.step_duration; - let vest_epoch = QuantSpec { unit: spec.quantization, offset: proving_period_start } - .quantize_up(epoch); + let vest_epoch = quant.quantize_up(epoch); let elapsed = vest_epoch - vest_begin; let target_vest = if elapsed < spec.vest_period { @@ -81,56 +157,85 @@ impl VestingFunds { Some(VestingFund { epoch: vest_epoch, amount: vest_this_time }) }); - // Take the old funds array and replace it with a new one. - let funds_len = self.funds.len(); - let old_funds = mem::replace(&mut self.funds, Vec::with_capacity(funds_len)); + let old_funds = self.load(store)?; // Fill back in the funds array, merging existing and new schedule. - self.funds.extend( - old_funds.into_iter().merge_join_by(new_funds, |a, b| a.epoch.cmp(&b.epoch)).map( - |item| match item { - EitherOrBoth::Left(a) => a, - EitherOrBoth::Right(b) => b, - EitherOrBoth::Both(a, b) => { - VestingFund { epoch: a.epoch, amount: a.amount + b.amount } - } - }, - ), - ); + let mut combined_funds = old_funds + .into_iter() + .merge_join_by(new_funds, |a, b| a.epoch.cmp(&b.epoch)) + .map(|item| match item { + EitherOrBoth::Left(a) => a, + EitherOrBoth::Right(b) => b, + EitherOrBoth::Both(a, b) => { + VestingFund { epoch: a.epoch, amount: a.amount + b.amount } + } + }) + .peekable(); + + // Take any unlocked funds. + let unlocked = take_vested(&mut combined_funds, current_epoch); + + // Write back the new value. + self.save(store, combined_funds)?; + + Ok(unlocked) } - pub fn unlock_unvested_funds( + /// Unlock all vested (first return value) then unlock unvested funds up to at most the + /// specified target. + pub fn unlock_vested_and_unvested_funds( &mut self, + store: &impl Blockstore, current_epoch: ChainEpoch, target: &TokenAmount, - ) -> TokenAmount { - let mut amount_unlocked = TokenAmount::zero(); - let mut last = None; - let mut start = 0; - for (i, vf) in self.funds.iter_mut().enumerate() { - if &amount_unlocked >= target { - break; - } + ) -> Result< + ( + TokenAmount, // automatic vested + TokenAmount, // unlocked unvested + ), + ActorError, + > { + // If our inner value is None, there's nothing to vest. + let Some(this) = &mut self.0 else { + return Ok(Default::default()); + }; + + let mut target = target.clone(); + + // Fast path: take it out of the head and don't touch the tail. + if this.head.epoch >= current_epoch && this.head.amount >= target { + this.head.amount -= ⌖ + return Ok((TokenAmount::zero(), target)); + } - if vf.epoch >= current_epoch { - let unlock_amount = std::cmp::min(target - &amount_unlocked, vf.amount.clone()); - amount_unlocked += &unlock_amount; - let new_amount = &vf.amount - &unlock_amount; + // Slow path, take it out of the tail. - if new_amount.is_zero() { - last = Some(i); - } else { - vf.amount = new_amount; - } - } else { - start = i + 1; + let mut unvested = TokenAmount::zero(); + let mut vested = TokenAmount::zero(); + + let mut funds = itertools::put_back(self.load(store)?); + while let Some(mut vf) = funds.next() { + // already vested + if vf.epoch < current_epoch { + vested += vf.amount; + continue; + } + + // take all + if vf.amount < target { + target -= &vf.amount; + unvested += &vf.amount; + continue; } - } - if let Some(end) = last { - self.funds.drain(start..=end); + // take some and stop. + unvested += ⌖ + vf.amount -= ⌖ + funds.put_back(vf); + break; } + self.save(store, funds)?; - amount_unlocked + Ok((vested, unvested)) } } diff --git a/actors/miner/tests/aggregate_network_fee_test.rs b/actors/miner/tests/aggregate_network_fee_test.rs deleted file mode 100644 index fd2d433ad..000000000 --- a/actors/miner/tests/aggregate_network_fee_test.rs +++ /dev/null @@ -1,89 +0,0 @@ -use fil_actor_miner::detail::BATCH_BALANCER; -use fil_actor_miner::{aggregate_pre_commit_network_fee, aggregate_prove_commit_network_fee}; -use fvm_shared::econ::TokenAmount; -use num_traits::zero; - -#[test] -fn constant_fee_per_sector_when_base_fee_is_below_5_nfil() { - for fee_func in [aggregate_prove_commit_network_fee, aggregate_pre_commit_network_fee] { - let one_sector_fee = fee_func(1, &zero()); - let ten_sector_fee = fee_func(10, &zero()); - assert_eq!(&one_sector_fee * 10, ten_sector_fee); - - let forty_sector_fee = fee_func(40, &TokenAmount::from_nano(1)); - assert_eq!(&one_sector_fee * 40, forty_sector_fee); - - let two_hundred_sector_fee = fee_func(200, &TokenAmount::from_nano(3)); - assert_eq!(one_sector_fee * 200, two_hundred_sector_fee); - } -} - -#[test] -fn fee_increases_if_basefee_crosses_threshold() { - for fee_func in [aggregate_prove_commit_network_fee, aggregate_pre_commit_network_fee] { - let at_no_base_fee = fee_func(10, &zero()); - let at_balance_minus_one_base_fee = - fee_func(10, &(&*BATCH_BALANCER - TokenAmount::from_atto(1))); - let at_balance_base_fee = fee_func(10, &BATCH_BALANCER); - let at_balance_plus_one_base_fee = - fee_func(10, &(&*BATCH_BALANCER + TokenAmount::from_nano(1))); - let at_balance_plus_two_base_fee = - fee_func(10, &(&*BATCH_BALANCER + TokenAmount::from_nano(2))); - let at_balance_times_two_base = fee_func(10, &(2 * &*BATCH_BALANCER)); - - assert_eq!(at_no_base_fee, at_balance_minus_one_base_fee); - assert_eq!(at_no_base_fee, at_balance_base_fee); - assert!(at_balance_base_fee < at_balance_plus_one_base_fee); - assert!(at_balance_plus_one_base_fee < at_balance_plus_two_base_fee); - assert_eq!(at_balance_times_two_base, 2 * at_balance_base_fee); - } -} - -#[test] -fn regression_tests() { - let magic_number = 65733297; - let fee = |aggregate_size, base_fee_multiplier| { - aggregate_prove_commit_network_fee( - aggregate_size, - &TokenAmount::from_nano(base_fee_multiplier), - ) + aggregate_pre_commit_network_fee( - aggregate_size, - &TokenAmount::from_nano(base_fee_multiplier), - ) - }; - - // (5/20) * x * 10 = (5/2) * x - let expected = (TokenAmount::from_nano(5) * magic_number).div_floor(2); - assert_eq!(expected, fee(10, 0)); - assert_eq!(expected, fee(10, 1)); - - let expected = TokenAmount::from_nano(25) * magic_number; - assert_eq!(expected, fee(100, 3)); - - let expected = TokenAmount::from_nano(30) * magic_number; - assert_eq!(expected, fee(100, 6)); -} - -#[test] -fn split_25_75() { - // check 25/75% split up to uFIL precision - let one_micro_fil = TokenAmount::from_nano(1000); - - for base_fee_multiplier in [0, 5, 20] { - for aggregate_size in [13, 303] { - let fee_pre = aggregate_pre_commit_network_fee( - aggregate_size, - &TokenAmount::from_nano(base_fee_multiplier), - ) - .atto() - / one_micro_fil.atto(); - let fee_prove = aggregate_prove_commit_network_fee( - aggregate_size, - &TokenAmount::from_nano(base_fee_multiplier), - ) - .atto() - / one_micro_fil.atto(); - assert_eq!(fee_prove, 3 * fee_pre); - } - } -} diff --git a/actors/miner/tests/aggregate_prove_commit.rs b/actors/miner/tests/aggregate_prove_commit.rs index 5979690f9..95e20600a 100644 --- a/actors/miner/tests/aggregate_prove_commit.rs +++ b/actors/miner/tests/aggregate_prove_commit.rs @@ -10,7 +10,6 @@ use fvm_shared::{bigint::BigInt, clock::ChainEpoch, econ::TokenAmount}; mod util; use fil_actors_runtime::test_utils::make_piece_cid; -use num_traits::Zero; use util::*; // an expiration ~10 days greater than effective min expiration taking into account 30 days max @@ -69,7 +68,6 @@ fn valid_precommits_then_aggregate_provecommit() { pcc, precommits, make_prove_commit_aggregate(§or_nos_bf), - &TokenAmount::zero(), ) .unwrap(); diff --git a/actors/miner/tests/apply_rewards.rs b/actors/miner/tests/apply_rewards.rs index 55bfe2515..389d5fdcc 100644 --- a/actors/miner/tests/apply_rewards.rs +++ b/actors/miner/tests/apply_rewards.rs @@ -45,25 +45,25 @@ fn funds_vest() { h.construct_and_verify(&rt); let st = h.get_state(&rt); - let vesting_funds = st.load_vesting_funds(&rt.store).unwrap(); + let vesting_funds = st.vesting_funds.load(&rt.store).unwrap(); // Nothing vesting to start - assert!(vesting_funds.funds.is_empty()); + assert_eq!(0, vesting_funds.len()); assert!(st.locked_funds.is_zero()); // Lock some funds with AddLockedFund let amt = TokenAmount::from_atto(600_000); h.apply_rewards(&rt, amt.clone(), TokenAmount::zero()); let st = h.get_state(&rt); - let vesting_funds = st.load_vesting_funds(&rt.store).unwrap(); + let vesting_funds = st.vesting_funds.load(&rt.store).unwrap(); - assert_eq!(180, vesting_funds.funds.len()); + assert_eq!(180, vesting_funds.len()); // Vested FIL pays out on epochs with expected offset let quant_spec = QuantSpec { unit: REWARD_VESTING_SPEC.quantization, offset: PERIOD_OFFSET }; let curr_epoch = *rt.epoch.borrow(); - for (i, vf) in vesting_funds.funds.iter().enumerate() { + for (i, vf) in vesting_funds.iter().enumerate() { let step = REWARD_VESTING_SPEC.initial_delay + (i as i64 + 1) * REWARD_VESTING_SPEC.step_duration; let expected_epoch = quant_spec.quantize_up(curr_epoch + step); @@ -71,7 +71,7 @@ fn funds_vest() { } let expected_offset = PERIOD_OFFSET % REWARD_VESTING_SPEC.quantization; - for vf in vesting_funds.funds.iter() { + for vf in vesting_funds.iter() { assert_eq!(expected_offset, vf.epoch % REWARD_VESTING_SPEC.quantization); } diff --git a/actors/miner/tests/batch_method_network_fees_test.rs b/actors/miner/tests/batch_method_network_fees_test.rs deleted file mode 100644 index 7ee093171..000000000 --- a/actors/miner/tests/batch_method_network_fees_test.rs +++ /dev/null @@ -1,288 +0,0 @@ -use std::collections::HashMap; - -use fil_actor_miner::{ - aggregate_pre_commit_network_fee, aggregate_prove_commit_network_fee, - pre_commit_deposit_for_power, qa_power_max, PreCommitSectorBatchParams, State, -}; -use fil_actors_runtime::test_utils::{expect_abort, expect_abort_contains_message}; -use fvm_ipld_bitfield::BitField; -use fvm_shared::{clock::ChainEpoch, econ::TokenAmount, error::ExitCode}; -use lazy_static::lazy_static; - -mod util; -use util::*; - -lazy_static! { - static ref PERIOD_OFFSET: ChainEpoch = ChainEpoch::from(100); -} - -#[test] -fn insufficient_funds_for_aggregated_prove_commit_network_fee() { - let actor = ActorHarness::new(*PERIOD_OFFSET); - let rt = actor.new_runtime(); - rt.set_balance(BIG_BALANCE.clone()); - let precommit_epoch = *PERIOD_OFFSET + 1; - rt.set_epoch(precommit_epoch); - actor.construct_and_verify(&rt); - let dl_info = actor.deadline(&rt); - - // make a good commitment for the proof to target - let prove_commit_epoch = precommit_epoch + rt.policy.pre_commit_challenge_delay + 1; - // something on deadline boundary but > 180 days - let expiration = - dl_info.period_end() + rt.policy.wpost_proving_period * DEFAULT_SECTOR_EXPIRATION as i64; - - let mut precommits = Vec::new(); - let mut sector_nos_bf = BitField::new(); - for i in 0..4 { - sector_nos_bf.set(i); - let precommit_params = - actor.make_pre_commit_params(i, precommit_epoch - 1, expiration, vec![1]); - let precommit = actor.pre_commit_sector_and_get( - &rt, - precommit_params, - PreCommitConfig::empty(), - i == 0, - ); - precommits.push(precommit); - } - - // set base fee extremely high so AggregateProveCommitNetworkFee is > 1000 FIL. Set balance to 1000 FIL to easily cover IP but not cover network fee - rt.set_epoch(prove_commit_epoch); - let balance = TokenAmount::from_whole(1000); - rt.set_balance(balance.clone()); - let base_fee = TokenAmount::from_atto(10u64.pow(16)); - rt.base_fee.replace(base_fee.clone()); - assert!(aggregate_prove_commit_network_fee(precommits.len(), &base_fee) > balance); - - let res = actor.prove_commit_aggregate_sector( - &rt, - ProveCommitConfig::empty(), - precommits, - make_prove_commit_aggregate(§or_nos_bf), - &base_fee, - ); - - expect_abort(ExitCode::USR_INSUFFICIENT_FUNDS, res); - rt.reset(); -} - -#[test] -fn insufficient_funds_for_batch_precommit_network_fee() { - let actor = ActorHarness::new(*PERIOD_OFFSET); - let rt = actor.new_runtime(); - rt.set_balance(BIG_BALANCE.clone()); - let precommit_epoch = *PERIOD_OFFSET + 1; - rt.set_epoch(precommit_epoch); - actor.construct_and_verify(&rt); - let dl_info = actor.deadline(&rt); - // something on deadline boundary but > 180 days - let expiration = - dl_info.period_end() + rt.policy.wpost_proving_period * DEFAULT_SECTOR_EXPIRATION as i64; - - let mut precommits = Vec::new(); - let mut sector_nos_bf = BitField::new(); - for i in 0..4 { - sector_nos_bf.set(i); - let precommit = actor.make_pre_commit_params(i, precommit_epoch - 1, expiration, vec![]); - precommits.push(precommit); - } - - // set base fee extremely high so AggregateProveCommitNetworkFee is > 1000 FIL. Set balance to 1000 FIL to easily cover PCD but not network fee - let balance = TokenAmount::from_whole(1000); - rt.set_balance(balance.clone()); - let base_fee = TokenAmount::from_atto(10u64.pow(16)); - rt.base_fee.replace(base_fee.clone()); - assert!(aggregate_pre_commit_network_fee(precommits.len(), &base_fee) > balance); - - let res = actor.pre_commit_sector_batch( - &rt, - PreCommitSectorBatchParams { sectors: precommits }, - &PreCommitBatchConfig { first_for_miner: true, ..Default::default() }, - &base_fee, - ); - - // state untouched - let state: State = rt.get_state(); - assert!(state.pre_commit_deposits.is_zero()); - let expirations = actor.collect_precommit_expirations(&rt, &state); - assert_eq!(HashMap::new(), expirations); - - expect_abort_contains_message( - ExitCode::USR_INSUFFICIENT_FUNDS, - "unlocked balance can not repay fee debt", - res, - ); - rt.reset(); -} - -#[test] -fn insufficient_funds_for_batch_precommit_in_combination_of_fee_debt_and_network_fee() { - let actor = ActorHarness::new(*PERIOD_OFFSET); - let rt = actor.new_runtime(); - rt.set_balance(BIG_BALANCE.clone()); - let precommit_epoch = *PERIOD_OFFSET + 1; - rt.set_epoch(precommit_epoch); - actor.construct_and_verify(&rt); - let dl_info = actor.deadline(&rt); - // something on deadline boundary but > 180 days - let expiration = - dl_info.period_end() + rt.policy.wpost_proving_period * DEFAULT_SECTOR_EXPIRATION as i64; - - let mut precommits = Vec::new(); - let mut sector_nos_bf = BitField::new(); - for i in 0..4 { - sector_nos_bf.set(i); - let precommit = actor.make_pre_commit_params(i, precommit_epoch - 1, expiration, vec![]); - precommits.push(precommit); - } - - // set base fee extremely high so AggregateProveCommitNetworkFee is > 1000 FIL. Set balance to 1000 FIL to easily cover PCD but not network fee - let base_fee = TokenAmount::from_atto(10u64.pow(16)); - rt.base_fee.replace(base_fee.clone()); - let net_fee = aggregate_pre_commit_network_fee(precommits.len(), &base_fee); - - // setup miner to have fee debt equal to net fee - let mut state: State = rt.get_state(); - state.fee_debt = net_fee.clone(); - rt.replace_state(&state); - - // give miner almost enough balance to pay both - let balance = (2 * net_fee) - TokenAmount::from_atto(1); - rt.set_balance(balance); - - let res = actor.pre_commit_sector_batch( - &rt, - PreCommitSectorBatchParams { sectors: precommits }, - &PreCommitBatchConfig { first_for_miner: true, ..Default::default() }, - &base_fee, - ); - - // state untouched - let state: State = rt.get_state(); - assert!(state.pre_commit_deposits.is_zero()); - let expirations = actor.collect_precommit_expirations(&rt, &state); - assert_eq!(HashMap::new(), expirations); - - expect_abort_contains_message( - ExitCode::USR_INSUFFICIENT_FUNDS, - "unlocked balance can not repay fee debt", - res, - ); - rt.reset(); -} - -#[test] -fn enough_funds_for_fee_debt_and_network_fee_but_not_for_pcd() { - let actor = ActorHarness::new(*PERIOD_OFFSET); - let rt = actor.new_runtime(); - rt.set_balance(BIG_BALANCE.clone()); - let precommit_epoch = *PERIOD_OFFSET + 1; - rt.set_epoch(precommit_epoch); - actor.construct_and_verify(&rt); - let dl_info = actor.deadline(&rt); - // something on deadline boundary but > 180 days - let expiration = - dl_info.period_end() + rt.policy.wpost_proving_period * DEFAULT_SECTOR_EXPIRATION as i64; - - let mut precommits = Vec::new(); - let mut sector_nos_bf = BitField::new(); - for i in 0..4 { - sector_nos_bf.set(i); - let precommit = actor.make_pre_commit_params(i, precommit_epoch - 1, expiration, vec![]); - precommits.push(precommit); - } - - // set base fee and fee debt high - let base_fee = TokenAmount::from_atto(10u64.pow(16)); - rt.base_fee.replace(base_fee.clone()); - let net_fee = aggregate_pre_commit_network_fee(precommits.len(), &base_fee); - // setup miner to have feed debt equal to net fee - let mut state: State = rt.get_state(); - state.fee_debt = net_fee.clone(); - rt.replace_state(&state); - - // give miner enough balance to pay both but not any extra for pcd - let balance = 2 * net_fee; - rt.set_balance(balance); - - let res = actor.pre_commit_sector_batch( - &rt, - PreCommitSectorBatchParams { sectors: precommits }, - &PreCommitBatchConfig { first_for_miner: true, ..Default::default() }, - &base_fee, - ); - - expect_abort_contains_message( - ExitCode::USR_INSUFFICIENT_FUNDS, - "insufficient funds 0.0 for pre-commit deposit", - res, - ); - rt.reset(); - - // state untouched - let state: State = rt.get_state(); - assert!(state.pre_commit_deposits.is_zero()); - let expirations = actor.collect_precommit_expirations(&rt, &state); - assert_eq!(HashMap::new(), expirations); -} - -#[test] -fn enough_funds_for_everything() { - let actor = ActorHarness::new(*PERIOD_OFFSET); - let rt = actor.new_runtime(); - rt.set_balance(BIG_BALANCE.clone()); - let precommit_epoch = *PERIOD_OFFSET + 1; - rt.set_epoch(precommit_epoch); - actor.construct_and_verify(&rt); - let dl_info = actor.deadline(&rt); - // something on deadline boundary but > 180 days - let expiration = - dl_info.period_end() + rt.policy.wpost_proving_period * DEFAULT_SECTOR_EXPIRATION as i64; - - let mut precommits = Vec::new(); - let mut sector_nos_bf = BitField::new(); - for i in 0..4 { - sector_nos_bf.set(i); - let precommit = actor.make_pre_commit_params(i, precommit_epoch - 1, expiration, vec![]); - precommits.push(precommit); - } - - // set base fee extremely high so AggregateProveCommitNetworkFee is > 1000 FIL. Set balance to 1000 FIL to easily cover PCD but not network fee - let base_fee = TokenAmount::from_atto(10u64.pow(16)); - rt.base_fee.replace(base_fee.clone()); - let net_fee = aggregate_pre_commit_network_fee(precommits.len(), &base_fee); - - // setup miner to have fee debt equal to net fee - let mut state: State = rt.get_state(); - state.fee_debt = net_fee.clone(); - rt.replace_state(&state); - - // give miner enough balance to pay both and pcd - let mut balance = 2 * net_fee; - let expected_deposit = pre_commit_deposit_for_power( - &actor.epoch_reward_smooth, - &actor.epoch_qa_power_smooth, - &qa_power_max(actor.sector_size), - ) * precommits.len(); - balance += expected_deposit.clone(); - rt.set_balance(balance); - - actor - .pre_commit_sector_batch( - &rt, - PreCommitSectorBatchParams { sectors: precommits }, - &PreCommitBatchConfig { first_for_miner: true, ..Default::default() }, - &base_fee, - ) - .unwrap(); - - // state updated - let state: State = rt.get_state(); - assert_eq!(expected_deposit, state.pre_commit_deposits); - let expirations = actor.collect_precommit_expirations(&rt, &state); - assert_eq!(1, expirations.len()); - for (_, map) in expirations { - assert_eq!(4, map.len()); - } -} diff --git a/actors/miner/tests/daily_fees_test.rs b/actors/miner/tests/daily_fees_test.rs new file mode 100644 index 000000000..2515bc907 --- /dev/null +++ b/actors/miner/tests/daily_fees_test.rs @@ -0,0 +1,400 @@ +use std::collections::HashMap; +use std::ops::Neg; + +use num_traits::Signed; + +use fil_actor_market::{ActivatedDeal, NO_ALLOCATION_ID}; +use fil_actor_miner::{ + daily_fee_for_sectors, daily_proof_fee, expected_reward_for_power, + pledge_penalty_for_continued_fault, pledge_penalty_for_termination, power_for_sectors, + qa_power_for_sector, Actor, ApplyRewardParams, DeadlineInfo, Method, PoStPartition, + SectorOnChainInfo, +}; +use fil_actors_runtime::reward::FilterEstimate; +use fil_actors_runtime::test_utils::{MockRuntime, REWARD_ACTOR_CODE_ID}; +use fil_actors_runtime::{BURNT_FUNDS_ACTOR_ADDR, EPOCHS_IN_DAY, REWARD_ACTOR_ADDR}; + +use fvm_ipld_encoding::ipld_block::IpldBlock; +use fvm_shared::bigint::{BigInt, Zero}; +use fvm_shared::error::ExitCode; +use fvm_shared::piece::PaddedPieceSize; +use fvm_shared::METHOD_SEND; +use fvm_shared::{clock::ChainEpoch, econ::TokenAmount}; + +use test_case::test_case; + +mod util; +use crate::util::*; + +const PERIOD_OFFSET: ChainEpoch = 100; + +#[test] +fn fee_paid_at_deadline() { + let (mut h, rt) = setup(); + // set circulating supply to a value that will yield a predictable fee using the original + // 5.56e-15 * CS for 32GiB QAP metric: 3873652000000 for a CS of 696.7M FIL + let reference_cs = TokenAmount::from_whole(696_700_000); + let reference_fee = TokenAmount::from_atto(3_873_652_000_000_u64); + rt.set_circulating_supply(reference_cs); + + let one_sector = h.commit_and_prove_sectors(&rt, 1, DEFAULT_SECTOR_EXPIRATION, vec![], true); + let daily_fee = daily_fee_for_sectors(&one_sector); + + { + // sanity check that we get within a reasonable distance from the reference amount using our + // per-byte multiplier (we expect to round under the reference amount) + let fee_ref_diff = reference_fee.atto() - daily_fee.atto(); + // should be within 1/100th of a nanoFIL of the reference amount + let fee_ref_diff_tolerance = TokenAmount::from_nano(1).div_floor(100); + assert!( + fee_ref_diff.is_positive() && fee_ref_diff.abs() <= *fee_ref_diff_tolerance.atto(), + "daily_fee: {} !≈ reference amount ({})", + daily_fee.atto(), + reference_fee.atto() + ); + } + + // plenty of funds available to pay fees + let miner_balance_before = rt.get_balance(); + h.advance_and_submit_posts(&rt, &one_sector); + let miner_balance_after = rt.get_balance(); + assert_eq!(miner_balance_before - &daily_fee, miner_balance_after); + + let mut st = h.get_state(&rt); + + // set balance to locked balance plus just enough to pay fees + rt.set_balance(&st.initial_pledge + &daily_fee); + h.advance_and_submit_posts(&rt, &one_sector); + let miner_balance_after = rt.get_balance(); + assert_eq!(st.initial_pledge, miner_balance_after); // back to locked balance + st = h.get_state(&rt); + assert!(st.fee_debt.is_zero()); // no debt + + h.advance_and_submit_posts(&rt, &one_sector); + assert_eq!(st.initial_pledge, miner_balance_after); // still at locked balance + st = h.get_state(&rt); + assert_eq!(st.fee_debt, daily_fee); // now in debt + + // set balance to pay back debt and half of the next fee + let extra = &daily_fee.div_floor(2); + let available_balance = &daily_fee + extra; + rt.set_balance(&st.initial_pledge + &available_balance); + { + // ApplyRewards to pay back fee debt, not a normal situation; see note in ActorHarness::apply_rewards + rt.set_caller(*REWARD_ACTOR_CODE_ID, REWARD_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![REWARD_ACTOR_ADDR]); + rt.expect_send_simple( + BURNT_FUNDS_ACTOR_ADDR, + METHOD_SEND, + None, + daily_fee.clone(), + None, + ExitCode::OK, + ); + let params = ApplyRewardParams { reward: daily_fee.clone(), penalty: TokenAmount::zero() }; + rt.call::(Method::ApplyRewards as u64, IpldBlock::serialize_cbor(¶ms).unwrap()) + .unwrap(); + rt.verify(); + } + + let miner_balance_before = rt.get_balance(); + st = h.get_state(&rt); + assert_eq!(&st.initial_pledge + extra, miner_balance_before); // back to locked balance + extra + assert!(st.fee_debt.is_zero()); // no debt + + h.advance_and_submit_posts(&rt, &one_sector); + let miner_balance_after = rt.get_balance(); + assert_eq!(st.initial_pledge, miner_balance_after); // back to locked balance + st = h.get_state(&rt); + assert_eq!(st.fee_debt, daily_fee - extra); // paid back debt, but added half back + + h.check_state(&rt); +} + +#[test_case(true, 1; "capped upfront, single sector")] +#[test_case(true, 55; "capped upfront, many sectors")] +#[test_case(false, 1; "capped later, single sector")] +#[test_case(false, 55; "capped later, many sectors")] +fn test_fee_capped_by_reward(capped_upfront: bool, num_sectors: usize) { + // This tests that the miner's daily fee is capped by the reward for the day. We work through + // various sector lifecycle scenarios to ensure that the fee is correctly calculated and paid. + // - capped_upfront: whether the capped reward is set before sector commitment + // - num_sectors: number of sectors to commit + + let (mut h, rt) = setup(); + + let original_epoch_reward_smooth = h.epoch_reward_smooth.clone(); + rt.set_circulating_supply(TokenAmount::from_whole(500_000_000)); + + if capped_upfront { + // set low reward before sector commitment in the capped-upfront case, this value should + // leave us with a daily reward that's less than double the daily fee + h.epoch_reward_smooth = FilterEstimate::new(BigInt::from(5e13 as u64), BigInt::zero()); + } + + // make sure we can pay whatever fees we need from rewards + h.apply_rewards(&rt, BIG_REWARDS.clone(), TokenAmount::zero()); + + let mut sectors = + h.commit_and_prove_sectors(&rt, num_sectors, DEFAULT_SECTOR_EXPIRATION, vec![], true); + let (dlidx, pidx) = h.get_state(&rt).find_sector(&rt.store, sectors[0].sector_number).unwrap(); + let sectors_power = power_for_sectors(h.sector_size, §ors); + let daily_fee = daily_fee_for_sectors(§ors); + + if !capped_upfront { + // Step 0. In the case where the fee is not capped upfront, proceed with a standard PoST and + // expect the normal daily_fee to be applied. Change the reward after so we get the cap for + // step 1. + + h.advance_and_submit_posts(&rt, §ors); + + h.epoch_reward_smooth = FilterEstimate::new(BigInt::from(5e13 as u64), BigInt::zero()); + } + + let day_reward = expected_reward_for_power( + &h.epoch_reward_smooth, + &h.epoch_qa_power_smooth, + &power_for_sectors(h.sector_size, §ors).qa, + EPOCHS_IN_DAY, + ); + + assert!(daily_fee < day_reward, "daily_fee: {} < day_reward: {}", daily_fee, day_reward); // fee should be less than daily reward + assert!( + daily_fee > day_reward.div_floor(2), + "daily_fee: {} <, day_reward/2: {}", + daily_fee, + day_reward.div_floor(2) + ); // but greater than 50% of daily reward + + // define various helper functions to keep this terse + + let advance_to_post_deadline = || -> DeadlineInfo { + let mut dlinfo = h.deadline(&rt); + while dlinfo.index != dlidx { + dlinfo = h.advance_deadline(&rt, CronConfig::empty()); + } + dlinfo + }; + + let verify_balance_change = |expected_deduction: &TokenAmount, operation: &dyn Fn()| { + let miner_balance_before = rt.get_balance(); + operation(); + let miner_balance_after = rt.get_balance(); + assert_eq!(miner_balance_before - expected_deduction, miner_balance_after); + }; + + let submit_window_post = + |dlinfo: &DeadlineInfo, sectors: &Vec, post_config: PoStConfig| { + let partition = PoStPartition { index: pidx, skipped: make_empty_bitfield() }; + h.submit_window_post(&rt, dlinfo, vec![partition], sectors.clone(), post_config) + }; + + // Step 1. Normal PoST but capped by reward, not daily_fee + + let dlinfo = advance_to_post_deadline(); + // configure post for power delta in the capped-upfront case + let cfg = if capped_upfront { + PoStConfig::with_expected_power_delta(§ors_power.clone()) + } else { + PoStConfig::empty() // no power delta, we've had first-post already + }; + submit_window_post(&dlinfo, §ors, cfg); + + let state = h.get_state(&rt); + let unvested = unvested_vesting_funds(&rt, &state); + let available = rt.get_balance() + unvested.clone() - &state.initial_pledge; + let burnt_funds = day_reward.div_floor(2); // i.e. not daily_fee + assert!(available >= burnt_funds); + + verify_balance_change(&burnt_funds, &|| { + h.advance_deadline( + &rt, + CronConfig { + burnt_funds: burnt_funds.clone(), + pledge_delta: burnt_funds.clone().neg(), + ..Default::default() + }, + ); + }); + + // Step 2. Advance to next deadline, fail to submit post, make sure we have faulty power, and + // then assert that the cap is unchanged. i.e. it includes faulty power in its calculation. + + advance_to_post_deadline(); + verify_balance_change(&burnt_funds, &|| { + h.advance_deadline( + &rt, + CronConfig { + burnt_funds: burnt_funds.clone(), + pledge_delta: burnt_funds.clone().neg(), + power_delta: Some(sectors_power.clone().neg()), + ..Default::default() + }, + ); + }); + + // Step 3. Advance to next deadline and submit post, recovering power, and pay the same capped + // fee. + + let bf = bitfield_from_slice(§ors.iter().map(|s| s.sector_number).collect::>()); + h.declare_recoveries(&rt, dlidx, pidx, bf, TokenAmount::zero()).unwrap(); + let dlinfo = advance_to_post_deadline(); + submit_window_post(&dlinfo, §ors, PoStConfig::with_expected_power_delta(§ors_power)); + verify_balance_change(&burnt_funds, &|| { + h.advance_deadline( + &rt, + CronConfig { + burnt_funds: burnt_funds.clone(), + pledge_delta: -burnt_funds.clone(), + ..Default::default() + }, + ); + }); + + if num_sectors > 1 { + // Step 4 (multiple sectors). Terminate a sector, make sure we pay a capped fee that is + // proportional to the power of the remaining sectors. + + let terminated_sector = §ors[0]; + let sector_power = qa_power_for_sector(h.sector_size, terminated_sector); + let sector_age = *rt.epoch.borrow() - terminated_sector.power_base_epoch; + let fault_fee = pledge_penalty_for_continued_fault( + &h.epoch_reward_smooth, + &h.epoch_qa_power_smooth, + §or_power, + ); + + let expected_fee = pledge_penalty_for_termination( + &terminated_sector.initial_pledge, + sector_age, + &fault_fee, + ); + h.terminate_sectors( + &rt, + &bitfield_from_slice(&[terminated_sector.sector_number]), + expected_fee, + ); + + sectors.remove(0); + + let mut dlinfo = h.deadline(&rt); + while dlinfo.index != dlidx { + dlinfo = h.advance_deadline(&rt, CronConfig::empty()); + } + + submit_window_post(&dlinfo, §ors, PoStConfig::empty()); + + let day_reward = expected_reward_for_power( + &h.epoch_reward_smooth, + &h.epoch_qa_power_smooth, + &power_for_sectors(h.sector_size, §ors).qa, + EPOCHS_IN_DAY, + ); + let burnt_funds = day_reward.div_floor(2); + verify_balance_change(&burnt_funds, &|| { + h.advance_deadline( + &rt, + CronConfig { + burnt_funds: burnt_funds.clone(), + pledge_delta: burnt_funds.clone().neg(), + ..Default::default() + }, + ); + }); + } + + // Step 5. Reset the reward to the original value, advance to the next deadline, and submit a + // post. We should pay the standard daily_fee with no cap. + + h.epoch_reward_smooth = original_epoch_reward_smooth; + let daily_fee = daily_fee_for_sectors(§ors); + verify_balance_change(&daily_fee, &|| { + h.advance_and_submit_posts(&rt, §ors); + }); +} + +#[test] +fn fees_proportional_to_qap() { + let (mut h, rt) = setup(); + + let sectors = h.commit_and_prove_sectors_with_cfgs( + &rt, + 5, + DEFAULT_SECTOR_EXPIRATION, + vec![vec![], vec![1], vec![2], vec![3], vec![4]], + true, + ProveCommitConfig { + verify_deals_exit: Default::default(), + claim_allocs_exit: Default::default(), + activated_deals: HashMap::from_iter(vec![ + ( + 1, + vec![ActivatedDeal { + client: 0, + allocation_id: NO_ALLOCATION_ID, + data: Default::default(), + size: PaddedPieceSize(h.sector_size as u64), + }], + ), + ( + 2, + vec![ActivatedDeal { + client: 0, + allocation_id: NO_ALLOCATION_ID, + data: Default::default(), + size: PaddedPieceSize(h.sector_size as u64 / 2), + }], + ), + ( + 3, + vec![ActivatedDeal { + client: 0, + allocation_id: 1, + data: Default::default(), + size: PaddedPieceSize(h.sector_size as u64), + }], + ), + ( + 4, + vec![ActivatedDeal { + client: 0, + allocation_id: 2, + data: Default::default(), + size: PaddedPieceSize(h.sector_size as u64 / 2), + }], + ), + ]), + }, + ); + + // for a reference we'll calculate the fee for a fully verified sector and + // divide as required + let full_verified_fee = daily_proof_fee( + &rt.policy, + &rt.circulating_supply.borrow(), + &BigInt::from(h.sector_size as u64 * 10), + ); + + // no deals + assert_eq!(full_verified_fee.div_floor(10), sectors[0].daily_fee); + // deal, unverified + assert_eq!(full_verified_fee.div_floor(10), sectors[1].daily_fee); + // deal, half, unverified + assert_eq!(full_verified_fee.div_floor(10), sectors[2].daily_fee); + // deal, verified + assert_eq!(full_verified_fee.clone(), sectors[3].daily_fee); + // deal, half, verified + assert!( + ((full_verified_fee.clone() * 11).div_floor(20) - §ors[4].daily_fee).atto().abs() + <= BigInt::from(1) + ); +} + +fn setup() -> (ActorHarness, MockRuntime) { + let h = ActorHarness::new(PERIOD_OFFSET); + let rt = h.new_runtime(); + h.construct_and_verify(&rt); + rt.set_balance(BIG_BALANCE.clone()); + + (h, rt) +} diff --git a/actors/miner/tests/deadline_cron.rs b/actors/miner/tests/deadline_cron.rs index 1adc3305f..ed6499e26 100644 --- a/actors/miner/tests/deadline_cron.rs +++ b/actors/miner/tests/deadline_cron.rs @@ -1,11 +1,11 @@ use fil_actor_miner::testing::{check_deadline_state_invariants, DeadlineStateSummary}; use fil_actor_miner::{ - pledge_penalty_for_continued_fault, power_for_sectors, Deadline, PowerPair, QuantSpec, - SectorOnChainInfo, REWARD_VESTING_SPEC, + daily_fee_for_sectors, pledge_penalty_for_continued_fault, power_for_sectors, Deadline, + PowerPair, QuantSpec, SectorOnChainInfo, REWARD_VESTING_SPEC, }; use fil_actors_runtime::runtime::RuntimePolicy; use fil_actors_runtime::test_utils::MockRuntime; -use fil_actors_runtime::{MessageAccumulator, EPOCHS_IN_DAY}; +use fil_actors_runtime::{MessageAccumulator, EPOCHS_IN_DAY, EPOCHS_IN_HOUR}; use fvm_ipld_bitfield::BitField; use fvm_shared::bigint::Zero; use fvm_shared::clock::ChainEpoch; @@ -16,7 +16,7 @@ use std::ops::Neg; mod util; use crate::util::*; -// an expriration ~10 days greater than effective min expiration taking into account 30 days max +// 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; @@ -69,8 +69,8 @@ fn test_vesting_on_cron() { // --- ASSERT FUNDS TO BE VESTED let st = h.get_state(&rt); - let vesting_funds = st.load_vesting_funds(&rt.store).unwrap(); - assert_eq!(360, vesting_funds.funds.len()); + let vesting_funds = st.vesting_funds.load(&rt.store).unwrap(); + assert_eq!(360, vesting_funds.len()); let q = QuantSpec { unit: REWARD_VESTING_SPEC.quantization, offset: st.proving_period_start }; @@ -78,29 +78,37 @@ fn test_vesting_on_cron() { let st = h.get_state(&rt); rt.set_epoch(new_epoch); let new_deadline_info = st.deadline_info(rt.policy(), new_epoch + 1); + let vesting_now = immediately_vesting_funds(&rt, &st); let old_locked = st.locked_funds; h.on_deadline_cron( &rt, - CronConfig { expected_enrollment: new_deadline_info.last(), ..CronConfig::default() }, + CronConfig { + pledge_delta: -vesting_now.clone(), + expected_enrollment: new_deadline_info.last(), + ..CronConfig::default() + }, ); let new_locked = h.get_state(&rt).locked_funds; if should_vest { + assert!(vesting_now.is_positive()); assert_ne!(old_locked, new_locked); } else { + assert!(vesting_now.is_zero()); assert_eq!(old_locked, new_locked); } }; - // move current epoch by a day so funds get vested - let new_epoch = q.quantize_up(current_epoch + EPOCHS_IN_DAY + 10); + // move current epoch by a day so funds get vested. +1 because rewards are vested at the end of + // an epoch. + let new_epoch = q.quantize_up(current_epoch + 12 * EPOCHS_IN_HOUR) + 1; assert_locked_fn(new_epoch, true); // no funds get vested if epoch moves by <12 hours - let new_epoch = new_epoch + (EPOCHS_IN_DAY / 2) - 100; + let new_epoch = new_epoch + (12 * EPOCHS_IN_HOUR) - 100; assert_locked_fn(new_epoch, false); // funds get vested again if epoch is quantised - let new_epoch = q.quantize_up(new_epoch); + let new_epoch = q.quantize_up(new_epoch) + 1; assert_locked_fn(new_epoch, true); h.check_state(&rt); @@ -143,8 +151,8 @@ fn sector_expires() { &rt, CronConfig { no_enrollment: true, - expired_sectors_power_delta: Some(power_delta), - expired_sectors_pledge_delta: initial_pledge.neg(), + power_delta: Some(power_delta), + pledge_delta: initial_pledge.neg(), ..CronConfig::default() }, ); @@ -199,9 +207,9 @@ fn sector_expires_and_repays_fee_debt() { &rt, CronConfig { no_enrollment: true, - expired_sectors_power_delta: Some(power_delta), - expired_sectors_pledge_delta: initial_pledge.neg(), - repaid_fee_debt: initial_pledge.clone(), + power_delta: Some(power_delta), + burnt_funds: initial_pledge.clone(), + pledge_delta: initial_pledge.neg(), ..CronConfig::default() }, ); @@ -243,10 +251,16 @@ fn detects_and_penalizes_faults() { } // Skip to end of the deadline, cron detects sectors as faulty + let mut fee_payable = daily_fee_for_sectors(&all_sectors); let active_power_delta = active_power.neg(); h.advance_deadline( &rt, - CronConfig { detected_faults_power_delta: Some(active_power_delta), ..Default::default() }, + CronConfig { + burnt_funds: fee_payable.clone(), + pledge_delta: -fee_payable.clone(), + power_delta: Some(active_power_delta), + ..Default::default() + }, ); // expect faulty power to be added to state @@ -279,10 +293,15 @@ fn detects_and_penalizes_faults() { &h.epoch_qa_power_smooth, &ongoing_pwr.qa, ); + fee_payable += ongoing_penalty; h.advance_deadline( &rt, - CronConfig { continued_faults_penalty: ongoing_penalty, ..Default::default() }, + CronConfig { + burnt_funds: fee_payable.clone(), + pledge_delta: -fee_payable, + ..Default::default() + }, ); // recorded faulty power is unchanged @@ -329,7 +348,7 @@ fn test_cron_run_trigger_faults() { // run cron and expect all sectors to be detected as faults (no penalty) let pwr = power_for_sectors(h.sector_size, &all_sectors); - + let daily_fee = daily_fee_for_sectors(&all_sectors); // power for sectors is removed let power_delta_claim = PowerPair { raw: pwr.raw.neg(), qa: pwr.qa.neg() }; @@ -339,8 +358,10 @@ fn test_cron_run_trigger_faults() { h.on_deadline_cron( &rt, CronConfig { + burnt_funds: daily_fee.clone(), + pledge_delta: daily_fee.neg(), + power_delta: Some(power_delta_claim), expected_enrollment: next_cron, - detected_faults_power_delta: Some(power_delta_claim), ..CronConfig::default() }, ); diff --git a/actors/miner/tests/deadline_state_test.rs b/actors/miner/tests/deadline_state_test.rs index 6a4e1dd75..ec251884c 100644 --- a/actors/miner/tests/deadline_state_test.rs +++ b/actors/miner/tests/deadline_state_test.rs @@ -2,8 +2,8 @@ use std::collections::HashMap; use fil_actor_miner::testing::{check_deadline_state_invariants, DeadlineStateSummary}; use fil_actor_miner::{ - power_for_sectors, Deadline, PartitionSectorMap, PoStPartition, PowerPair, QuantSpec, - SectorOnChainInfo, TerminationResult, + daily_fee_for_sectors, power_for_sectors, Deadline, PartitionSectorMap, PoStPartition, + PowerPair, QuantSpec, SectorOnChainInfo, TerminationResult, }; use fil_actors_runtime::runtime::{Policy, Runtime}; use fil_actors_runtime::test_utils::MockRuntime; @@ -22,20 +22,20 @@ const QUANT_SPEC: QuantSpec = QuantSpec { unit: 4, offset: 1 }; fn sectors() -> Vec { vec![ - test_sector(2, 1, 50, 60, 1000), - test_sector(3, 2, 51, 61, 1001), - test_sector(7, 3, 52, 62, 1002), - test_sector(8, 4, 53, 63, 1003), - test_sector(8, 5, 54, 64, 1004), - test_sector(11, 6, 55, 65, 1005), - test_sector(13, 7, 56, 66, 1006), - test_sector(8, 8, 57, 67, 1007), - test_sector(8, 9, 58, 68, 1008), + test_sector(2, 1, 50, 60, 1000, 1000), + test_sector(3, 2, 51, 61, 1001, 2000), + test_sector(7, 3, 52, 62, 1002, 3000), + test_sector(8, 4, 53, 63, 1003, 4000), + test_sector(8, 5, 54, 64, 1004, 5000), + test_sector(11, 6, 55, 65, 1005, 6000), + test_sector(13, 7, 56, 66, 1006, 7000), + test_sector(8, 8, 57, 67, 1007, 8000), + test_sector(8, 9, 58, 68, 1008, 9000), ] } fn extra_sectors() -> Vec { - vec![test_sector(8, 10, 58, 68, 1008)] + vec![test_sector(8, 10, 58, 68, 1008, 9000)] } fn all_sectors() -> Vec { @@ -60,12 +60,14 @@ fn add_sectors( let sectors = sectors(); let store = rt.store(); - let power = power_for_sectors(SECTOR_SIZE, §ors); - let activated_power = deadline - .add_sectors(store, PARTITION_SIZE, false, §ors, SECTOR_SIZE, QUANT_SPEC) + let (activated_power, daily_fee) = deadline + .add_sectors(store, PARTITION_SIZE, false, true, §ors, SECTOR_SIZE, QUANT_SPEC) .expect("Couldn't add sectors"); + let expected_power = power_for_sectors(SECTOR_SIZE, §ors); + let expected_daily_fee = daily_fee_for_sectors(§ors); - assert_eq!(activated_power, power); + assert_eq!(activated_power, expected_power); + assert_eq!(daily_fee, expected_daily_fee); let deadline_state = deadline_state() .with_unproven(&[1, 2, 3, 4, 5, 6, 7, 8, 9]) @@ -80,13 +82,13 @@ fn add_sectors( return (deadline_state, sectors); } - let mut sector_array = sectors_arr(store, sectors.to_owned()); + let mut sectors_array = sectors_arr(store, sectors.to_owned()); //prove everything let result = deadline .record_proven_sectors( store, - §or_array, + §ors_array, SECTOR_SIZE, QUANT_SPEC, 0, @@ -98,9 +100,9 @@ fn add_sectors( ) .unwrap(); - assert_eq!(result.power_delta, power); + assert_eq!(result.power_delta, expected_power); - let sectors_root = sector_array.amt.flush().unwrap(); + let sectors_root = sectors_array.amt.flush().unwrap(); let (faulty_power, recovery_power) = deadline.process_deadline_end(store, QUANT_SPEC, 0, sectors_root).unwrap(); @@ -199,20 +201,24 @@ fn add_then_terminate_then_remove_partition( ) -> (ExpectedDeadlineState, Vec) { let (deadline_state, sectors) = add_then_terminate_then_pop_early(rt, deadline); let store = rt.store(); + let mut sectors_array = sectors_arr(store, sectors.to_owned()); - let (live, dead, removed_power) = deadline - .remove_partitions(store, &bitfield_from_slice(&[0]), QUANT_SPEC) + let dead = deadline + .compact_partitions( + store, + &mut sectors_array, + SECTOR_SIZE, + PARTITION_SIZE, + &bitfield_from_slice(&[0]), + QUANT_SPEC, + ) .expect("should have removed partitions"); - assert_bitfield_equals(&live, &[2, 4]); assert_bitfield_equals(&dead, &[1, 3]); - let live_power = power_for_sectors(SECTOR_SIZE, &select_sectors(§ors, &live)); - assert_eq!(live_power, removed_power); - let deadline_state = deadline_state .with_terminations(&[6]) - .with_partitions(vec![bitfield_from_slice(&[5, 6, 7, 8]), bitfield_from_slice(&[9])]) + .with_partitions(vec![bitfield_from_slice(&[5, 6, 7, 8]), bitfield_from_slice(&[2, 4, 9])]) .assert(store, §ors, deadline); (deadline_state, sectors) @@ -329,10 +335,21 @@ fn cannot_remove_partitions_with_early_terminations() { let (_, rt) = setup(); let mut deadline = Deadline::new(rt.store()).unwrap(); - add_then_terminate(&rt, &mut deadline, false); + let (_, sectors) = add_then_terminate(&rt, &mut deadline, false); let store = rt.store(); - assert!(deadline.remove_partitions(store, &bitfield_from_slice(&[0]), QUANT_SPEC).is_err()); + let mut sectors_array = sectors_arr(store, sectors.to_owned()); + + assert!(deadline + .compact_partitions( + store, + &mut sectors_array, + SECTOR_SIZE, + PARTITION_SIZE, + &bitfield_from_slice(&[0]), + QUANT_SPEC, + ) + .is_err()); } #[test] @@ -382,9 +399,19 @@ fn cannot_remove_missing_partition() { let (_, rt) = setup(); let mut deadline = Deadline::new(rt.store()).unwrap(); - add_then_terminate_then_remove_partition(&rt, &mut deadline); + let (_, sectors) = add_then_terminate_then_remove_partition(&rt, &mut deadline); + let store = rt.store(); + let mut sectors_array = sectors_arr(store, sectors.to_owned()); + assert!(deadline - .remove_partitions(rt.store(), &bitfield_from_slice(&[2]), QUANT_SPEC) + .compact_partitions( + store, + &mut sectors_array, + SECTOR_SIZE, + PARTITION_SIZE, + &bitfield_from_slice(&[2]), + QUANT_SPEC, + ) .is_err()); } @@ -394,12 +421,20 @@ fn removing_no_partitions_does_nothing() { let mut deadline = Deadline::new(rt.store()).unwrap(); let (deadline_state, sectors) = add_then_terminate_then_pop_early(&rt, &mut deadline); - let (live, dead, removed_power) = deadline - .remove_partitions(rt.store(), &bitfield_from_slice(&[]), QUANT_SPEC) + let store = rt.store(); + let mut sectors_array = sectors_arr(store, sectors.to_owned()); + + let dead = deadline + .compact_partitions( + store, + &mut sectors_array, + SECTOR_SIZE, + PARTITION_SIZE, + &bitfield_from_slice(&[]), + QUANT_SPEC, + ) .expect("should not have failed to remove partitions"); - assert!(removed_power.is_zero()); - assert!(live.is_empty()); assert!(dead.is_empty()); // Popping early terminations doesn't affect the terminations bitfield. @@ -418,11 +453,19 @@ fn fails_to_remove_partitions_with_faulty_sectors() { let (_, rt) = setup(); let mut deadline = Deadline::new(rt.store()).unwrap(); - add_then_mark_faulty(&rt, &mut deadline, false); + let (_, sectors) = add_then_mark_faulty(&rt, &mut deadline, false); + let store = rt.store(); + let mut sectors_array = sectors_arr(store, sectors.to_owned()); - // Try to remove a partition with faulty sectors. assert!(deadline - .remove_partitions(rt.store(), &bitfield_from_slice(&[1]), QUANT_SPEC) + .compact_partitions( + store, + &mut sectors_array, + SECTOR_SIZE, + PARTITION_SIZE, + &bitfield_from_slice(&[1]), + QUANT_SPEC, + ) .is_err()); } @@ -660,11 +703,21 @@ fn post_all_the_things() { add_sectors(&rt, &mut deadline, true); // add an inactive sector - let power = deadline - .add_sectors(rt.store(), PARTITION_SIZE, false, &extra_sectors(), SECTOR_SIZE, QUANT_SPEC) + let (power, daily_fee) = deadline + .add_sectors( + rt.store(), + PARTITION_SIZE, + false, + true, + &extra_sectors(), + SECTOR_SIZE, + QUANT_SPEC, + ) .unwrap(); let expected_power = power_for_sectors(SECTOR_SIZE, &extra_sectors()); + let expected_daily_fee = daily_fee_for_sectors(&extra_sectors()); assert_eq!(expected_power, power); + assert_eq!(expected_daily_fee, daily_fee); let mut sectors_array = sectors_arr(rt.store(), all_sectors()); @@ -759,11 +812,21 @@ fn post_with_unproven_faults_recoveries_untracted_recoveries() { add_then_mark_faulty(&rt, &mut deadline, true); // add an inactive sector - let power = deadline - .add_sectors(rt.store(), PARTITION_SIZE, false, &extra_sectors(), SECTOR_SIZE, QUANT_SPEC) + let (power, daily_fee) = deadline + .add_sectors( + rt.store(), + PARTITION_SIZE, + false, + true, + &extra_sectors(), + SECTOR_SIZE, + QUANT_SPEC, + ) .unwrap(); let expected_power = power_for_sectors(SECTOR_SIZE, &extra_sectors()); + let expected_daily_fee = daily_fee_for_sectors(&extra_sectors()); assert_eq!(power, expected_power); + assert_eq!(daily_fee, expected_daily_fee); let mut sectors_array = sectors_arr(rt.store(), all_sectors()); @@ -871,11 +934,21 @@ fn post_with_skipped_unproven() { add_sectors(&rt, &mut deadline, true); // add an inactive sector - let power = deadline - .add_sectors(rt.store(), PARTITION_SIZE, false, &extra_sectors(), SECTOR_SIZE, QUANT_SPEC) + let (power, daily_fee) = deadline + .add_sectors( + rt.store(), + PARTITION_SIZE, + false, + true, + &extra_sectors(), + SECTOR_SIZE, + QUANT_SPEC, + ) .unwrap(); let expected_power = power_for_sectors(SECTOR_SIZE, &extra_sectors()); + let expected_daily_fee = daily_fee_for_sectors(&extra_sectors()); assert_eq!(power, expected_power); + assert_eq!(daily_fee, expected_daily_fee); let mut sectors_array = sectors_arr(rt.store(), all_sectors()); let mut post_partitions = [ @@ -941,11 +1014,21 @@ fn post_missing_partition() { add_sectors(&rt, &mut deadline, true); // add an inactive sector - let power = deadline - .add_sectors(rt.store(), PARTITION_SIZE, false, &extra_sectors(), SECTOR_SIZE, QUANT_SPEC) + let (power, daily_fee) = deadline + .add_sectors( + rt.store(), + PARTITION_SIZE, + false, + true, + &extra_sectors(), + SECTOR_SIZE, + QUANT_SPEC, + ) .unwrap(); let expected_power = power_for_sectors(SECTOR_SIZE, &extra_sectors()); + let expected_daily_fee = daily_fee_for_sectors(&extra_sectors()); assert_eq!(power, expected_power); + assert_eq!(daily_fee, expected_daily_fee); let sectors_array = sectors_arr(rt.store(), all_sectors()); let mut post_partitions = [ @@ -978,11 +1061,21 @@ fn post_partition_twice() { add_sectors(&rt, &mut deadline, true); // add an inactive sector - let power = deadline - .add_sectors(rt.store(), PARTITION_SIZE, false, &extra_sectors(), SECTOR_SIZE, QUANT_SPEC) + let (power, daily_fee) = deadline + .add_sectors( + rt.store(), + PARTITION_SIZE, + false, + true, + &extra_sectors(), + SECTOR_SIZE, + QUANT_SPEC, + ) .unwrap(); let expected_power = power_for_sectors(SECTOR_SIZE, &extra_sectors()); + let expected_daily_fee = daily_fee_for_sectors(&extra_sectors()); assert_eq!(power, expected_power); + assert_eq!(daily_fee, expected_daily_fee); let sectors_array = sectors_arr(rt.store(), all_sectors()); let mut post_partitions = [ @@ -1288,7 +1381,11 @@ impl ExpectedDeadlineState { for (i, partition_sectors) in self.partition_sectors.iter().enumerate() { let partitions = partitions.get(i as u64).unwrap().unwrap(); - assert_eq!(partition_sectors, &partitions.sectors); + assert_eq!( + partition_sectors, &partitions.sectors, + "Partition {} sectors do not match. Expected: {:?}, Found: {:?}", + i, partition_sectors, partitions.sectors + ); } self diff --git a/actors/miner/tests/declare_faults.rs b/actors/miner/tests/declare_faults.rs index a613b44f8..7d45ee597 100644 --- a/actors/miner/tests/declare_faults.rs +++ b/actors/miner/tests/declare_faults.rs @@ -1,3 +1,4 @@ +use fil_actor_miner::daily_fee_for_sectors; use fil_actor_miner::pledge_penalty_for_continued_fault; use fil_actor_miner::power_for_sectors; @@ -58,9 +59,10 @@ fn declare_fault_pays_fee_at_window_post() { &h.epoch_qa_power_smooth, &ongoing_pwr.qa, ); + let burnt_funds = daily_fee_for_sectors(&all_sectors) + ongoing_penalty; h.advance_deadline( &rt, - CronConfig { continued_faults_penalty: ongoing_penalty, ..Default::default() }, + CronConfig { pledge_delta: -burnt_funds.clone(), burnt_funds, ..Default::default() }, ); h.check_state(&rt); } diff --git a/actors/miner/tests/declare_recoveries.rs b/actors/miner/tests/declare_recoveries.rs index e04c1ea25..9d9ce9587 100644 --- a/actors/miner/tests/declare_recoveries.rs +++ b/actors/miner/tests/declare_recoveries.rs @@ -1,3 +1,4 @@ +use fil_actor_miner::daily_fee_for_sectors; use fil_actor_miner::pledge_penalty_for_continued_fault; use fil_actor_miner::power_for_sectors; use fil_actors_runtime::test_utils::expect_abort_contains_message; @@ -50,13 +51,14 @@ fn recovery_happy_path() { fn recovery_must_pay_back_fee_debt() { let (mut h, rt) = setup(); let one_sector = h.commit_and_prove_sectors(&rt, 1, DEFAULT_SECTOR_EXPIRATION, vec![], true); + let daily_fee = daily_fee_for_sectors(&one_sector); // advance to first proving period and submit so we'll have time to declare the fault next cycle h.advance_and_submit_posts(&rt, &one_sector); // Fault will take miner into fee debt let mut st = h.get_state(&rt); - rt.set_balance(&st.pre_commit_deposits + &st.initial_pledge + &st.locked_funds); + rt.set_balance(&st.pre_commit_deposits + &st.initial_pledge + &st.locked_funds + &daily_fee); h.declare_faults(&rt, &one_sector); @@ -73,13 +75,7 @@ fn recovery_must_pay_back_fee_debt() { &h.epoch_qa_power_smooth, &ongoing_pwr.qa, ); - h.advance_deadline( - &rt, - CronConfig { - continued_faults_penalty: TokenAmount::zero(), // fee is instead added to debt - ..Default::default() - }, - ); + h.advance_deadline(&rt, CronConfig { burnt_funds: daily_fee, ..Default::default() }); st = h.get_state(&rt); assert_eq!(ff, st.fee_debt); diff --git a/actors/miner/tests/expiration_queue.rs b/actors/miner/tests/expiration_queue.rs index 90aae1cb0..384ccd7a8 100644 --- a/actors/miner/tests/expiration_queue.rs +++ b/actors/miner/tests/expiration_queue.rs @@ -3,16 +3,12 @@ use fil_actor_miner::{ NO_QUANTIZATION, }; use fil_actors_runtime::test_blockstores::MemoryBlockstore; -use fil_actors_runtime::{ - test_utils::{make_sealed_cid, MockRuntime}, - DealWeight, -}; +use fil_actors_runtime::test_utils::MockRuntime; use fvm_ipld_amt::Amt; use fvm_ipld_bitfield::{BitField, MaybeBitField}; use fvm_shared::bigint::Zero; -use fvm_shared::clock::ChainEpoch; use fvm_shared::econ::TokenAmount; -use fvm_shared::sector::{SectorNumber, SectorSize, StoragePower}; +use fvm_shared::sector::{SectorSize, StoragePower}; use std::convert::TryInto; use std::iter::FromIterator; @@ -40,6 +36,9 @@ fn active_power() -> PowerPair { fn faulty_power() -> PowerPair { PowerPair { raw: StoragePower::from(1 << 11), qa: StoragePower::from(1 << 12) } } +fn fee_deduction() -> TokenAmount { + TokenAmount::from_atto(2_000) +} fn default_set() -> ExpirationSet { let mut set = ExpirationSet::empty(); set.add( @@ -48,6 +47,7 @@ fn default_set() -> ExpirationSet { &on_time_pledge(), &active_power(), &faulty_power(), + &fee_deduction(), ) .unwrap(); set @@ -68,6 +68,7 @@ fn adds_sectors_and_power_to_empty_set() { assert_eq!(set.on_time_pledge, on_time_pledge()); assert_eq!(set.active_power, active_power()); assert_eq!(set.faulty_power, faulty_power()); + assert_eq!(set.fee_deduction, fee_deduction()); assert_eq!(set.len(), 5); } @@ -82,6 +83,7 @@ fn adds_sectors_and_power_to_non_empty_set() { &TokenAmount::from_atto(300), &power_pair(3, 13), &power_pair(3, 11), + &TokenAmount::from_atto(500), ) .unwrap(); @@ -104,6 +106,7 @@ fn removes_sectors_and_power_set() { &TokenAmount::from_atto(800), &power_pair(3, 11), &power_pair(3, 9), + &TokenAmount::from_atto(1200), ) .unwrap(); @@ -114,6 +117,7 @@ fn removes_sectors_and_power_set() { assert_eq!(set.active_power, active); let faulty = power_pair(1, 9); assert_eq!(set.faulty_power, faulty); + assert_eq!(set.fee_deduction, TokenAmount::from_atto(800)); } #[test] @@ -127,6 +131,7 @@ fn remove_fails_when_pledge_underflows() { &TokenAmount::from_atto(1200), &power_pair(3, 11), &power_pair(3, 9), + &TokenAmount::from_atto(800), ) .err() .unwrap(); @@ -135,6 +140,26 @@ fn remove_fails_when_pledge_underflows() { assert!(err.to_string().contains("pledge underflow")); } +#[test] +fn remove_fails_when_fee_underflows() { + let mut set = default_set(); + + let err = set + .remove( + &mk_bitfield([9]), + &mk_bitfield([2]), + &TokenAmount::from_atto(800), + &power_pair(3, 11), + &power_pair(3, 9), + &TokenAmount::from_atto(2200), + ) + .err() + .unwrap(); + // XXX: This is not a good way to check for specific errors. + // See: https://github.com/filecoin-project/builtin-actors/issues/338 + assert!(err.to_string().contains("fee deduction underflow")); +} + #[test] fn remove_fails_to_remove_sectors_it_does_not_contain() { let mut set = default_set(); @@ -147,6 +172,7 @@ fn remove_fails_to_remove_sectors_it_does_not_contain() { &TokenAmount::zero(), &power_pair(3, 11), &power_pair(3, 9), + &TokenAmount::zero(), ) .err() .unwrap(); @@ -162,6 +188,7 @@ fn remove_fails_to_remove_sectors_it_does_not_contain() { &TokenAmount::zero(), &power_pair(3, 11), &power_pair(3, 9), + &TokenAmount::zero(), ) .err() .unwrap(); @@ -182,6 +209,7 @@ fn remove_fails_when_active_or_fault_qa_power_underflows() { &TokenAmount::from_atto(200), &power_pair(3, 12), &power_pair(3, 9), + &TokenAmount::from_atto(600), ) .err() .unwrap(); @@ -199,6 +227,7 @@ fn remove_fails_when_active_or_fault_qa_power_underflows() { &TokenAmount::from_atto(200), &power_pair(3, 11), &power_pair(3, 10), + &TokenAmount::from_atto(700), ) .err() .unwrap(); @@ -220,6 +249,7 @@ fn set_is_empty_when_all_sectors_removed() { &on_time_pledge(), &active_power(), &faulty_power(), + &fee_deduction(), ) .unwrap(); @@ -231,6 +261,7 @@ fn set_is_empty_when_all_sectors_removed() { &on_time_pledge(), &active_power(), &faulty_power(), + &fee_deduction(), ) .unwrap(); @@ -240,12 +271,12 @@ fn set_is_empty_when_all_sectors_removed() { fn sectors() -> [SectorOnChainInfo; 6] { [ - test_sector(2, 1, 50, 60, 1000), - test_sector(3, 2, 51, 61, 1001), - test_sector(7, 3, 52, 62, 1002), - test_sector(8, 4, 53, 63, 1003), - test_sector(11, 5, 54, 64, 1004), - test_sector(13, 6, 55, 65, 1005), + test_sector(2, 1, 50, 60, 1000, 3000), + test_sector(3, 2, 51, 61, 1001, 4000), + test_sector(7, 3, 52, 62, 1002, 5000), + test_sector(8, 4, 53, 63, 1003, 6000), + test_sector(11, 5, 54, 64, 1004, 7000), + test_sector(13, 6, 55, 65, 1005, 8000), ] } @@ -257,10 +288,12 @@ fn added_sectors_can_be_popped_off_queue() { let rt = h.new_runtime(); let mut queue = empty_expiration_queue(&rt); - let (sec_nums, power, pledge) = queue.add_active_sectors(§ors(), SECTOR_SIZE).unwrap(); + let (sec_nums, power, pledge, daily_fee) = + queue.add_active_sectors(§ors(), SECTOR_SIZE).unwrap(); assert_eq!(sec_nums, mk_bitfield([1, 2, 3, 4, 5, 6])); assert_eq!(power, power_for_sectors(SECTOR_SIZE, §ors())); assert_eq!(pledge, TokenAmount::from_atto(6015)); + assert_eq!(daily_fee, TokenAmount::from_atto(3000 + 4000 + 5000 + 6000 + 7000 + 8000)); // default test quantizing of 1 means every sector is in its own expriation set assert_eq!(sectors().len(), queue.amt.count() as usize); @@ -304,10 +337,12 @@ fn quantizes_added_sectors_by_expiration() { let mut queue = empty_expiration_queue_with_quantizing(&rt, QuantSpec { unit: 5, offset: 3 }); - let (sec_nums, power, pledge) = queue.add_active_sectors(§ors(), SECTOR_SIZE).unwrap(); + let (sec_nums, power, pledge, daily_fee) = + queue.add_active_sectors(§ors(), SECTOR_SIZE).unwrap(); assert_eq!(sec_nums, mk_bitfield([1, 2, 3, 4, 5, 6])); assert_eq!(power, power_for_sectors(SECTOR_SIZE, §ors())); assert_eq!(pledge, TokenAmount::from_atto(6015)); + assert_eq!(daily_fee, TokenAmount::from_atto(3000 + 4000 + 5000 + 6000 + 7000 + 8000)); // quantizing spec means sectors should be grouped into 3 sets expiring at 3, 8 and 13 assert_eq!(queue.amt.count(), 3); @@ -354,7 +389,8 @@ fn reschedules_sectors_as_faults() { // Create 3 expiration sets with 2 sectors apiece let mut queue = empty_expiration_queue_with_quantizing(&rt, QuantSpec { unit: 4, offset: 1 }); - let (_sec_nums, _power, _pledge) = queue.add_active_sectors(§ors(), SECTOR_SIZE).unwrap(); + let (_sec_nums, _power, _pledge, _daily_fee) = + queue.add_active_sectors(§ors(), SECTOR_SIZE).unwrap(); let _ = queue.amt.flush().unwrap(); @@ -412,7 +448,8 @@ fn reschedules_all_sectors_as_faults() { // Create expiration 3 sets with 2 sectors apiece let mut queue = empty_expiration_queue_with_quantizing(&rt, QuantSpec { unit: 4, offset: 1 }); - let (_sec_nums, _power, _pledge) = queue.add_active_sectors(§ors(), SECTOR_SIZE).unwrap(); + let (_sec_nums, _power, _pledge, _daily_fee) = + queue.add_active_sectors(§ors(), SECTOR_SIZE).unwrap(); let _ = queue.amt.flush().unwrap(); @@ -470,7 +507,8 @@ fn reschedule_recover_restores_all_sector_stats() { // Create expiration 3 sets with 2 sectors apiece let mut queue = empty_expiration_queue_with_quantizing(&rt, QuantSpec { unit: 4, offset: 1 }); - let (_sec_nums, _power, _pledge) = queue.add_active_sectors(§ors(), SECTOR_SIZE).unwrap(); + let (_sec_nums, _power, _pledge, _daily_fee) = + queue.add_active_sectors(§ors(), SECTOR_SIZE).unwrap(); let _ = queue.amt.flush().unwrap(); @@ -534,7 +572,7 @@ fn replaces_sectors_with_new_sectors() { // add sectors to each set let sectors = sectors(); - let (_sec_nums, _power, _pledge) = queue + let (_sec_nums, _power, _pledge, _daily_fee) = queue .add_active_sectors( &[sectors[0].clone(), sectors[1].clone(), sectors[3].clone(), sectors[5].clone()], SECTOR_SIZE, @@ -546,7 +584,7 @@ fn replaces_sectors_with_new_sectors() { // remove all from first set, replace second set, and append to third let to_remove = [sectors[0].clone(), sectors[1].clone(), sectors[3].clone()]; let to_add = [sectors[2].clone(), sectors[4].clone()]; - let (removed, added, power_delta, pledge_delta) = + let (removed, added, power_delta, pledge_delta, _daily_fee) = queue.replace_sectors(&to_remove, &to_add, SECTOR_SIZE).unwrap(); assert_eq!(removed, mk_bitfield([1, 2, 4])); assert_eq!(added, mk_bitfield([3, 5])); @@ -692,24 +730,6 @@ fn rescheduling_no_sectors_as_recovered_leaves_the_queue_empty() { assert!(queue.amt.count().is_zero()); } -fn test_sector( - expiration: ChainEpoch, - sector_number: SectorNumber, - weight: u64, - vweight: u64, - pledge: u64, -) -> SectorOnChainInfo { - SectorOnChainInfo { - expiration, - sector_number, - deal_weight: DealWeight::from(weight), - verified_deal_weight: DealWeight::from(vweight), - initial_pledge: TokenAmount::from_atto(pledge), - sealed_cid: make_sealed_cid(format!("commR-{}", sector_number).as_bytes()), - ..Default::default() - } -} - fn empty_expiration_queue_with_quantizing( rt: &MockRuntime, quant: QuantSpec, diff --git a/actors/miner/tests/extend_sector_expiration_test.rs b/actors/miner/tests/extend_sector_expiration_test.rs index 3ac3af134..9a513881e 100644 --- a/actors/miner/tests/extend_sector_expiration_test.rs +++ b/actors/miner/tests/extend_sector_expiration_test.rs @@ -1,7 +1,7 @@ use fil_actor_market::ActivatedDeal; use fil_actor_miner::ext::verifreg::Claim as FILPlusClaim; use fil_actor_miner::{ - power_for_sector, seal_proof_sector_maximum_lifetime, ExpirationExtension, + daily_proof_fee, power_for_sector, seal_proof_sector_maximum_lifetime, ExpirationExtension, ExpirationExtension2, ExtendSectorExpiration2Params, ExtendSectorExpirationParams, PoStPartition, SectorClaim, SectorOnChainInfo, State, }; @@ -13,7 +13,11 @@ use fil_actors_runtime::{ EPOCHS_IN_DAY, }; use fvm_ipld_bitfield::BitField; +use fvm_shared::bigint::BigInt; use fvm_shared::deal::DealID; +use fvm_shared::econ::TokenAmount; +use fvm_shared::piece::PaddedPieceSize; +use fvm_shared::version::NetworkVersion; use fvm_shared::{ address::Address, clock::ChainEpoch, @@ -21,6 +25,8 @@ use fvm_shared::{ sector::{RegisteredSealProof, SectorNumber}, ActorID, }; + +use num_traits::Zero; use std::collections::HashMap; mod util; @@ -30,7 +36,7 @@ use itertools::Itertools; use test_case::test_case; use util::*; -// an expriration ~10 days greater than effective min expiration taking into account 30 days max between pre and prove commit +// 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; fn setup() -> (ActorHarness, MockRuntime) { @@ -177,6 +183,7 @@ fn rejects_extension_past_max_for_seal_proof(v2: bool) { #[test_case(true; "v2")] fn updates_expiration_with_valid_params(v2: bool) { let (mut h, rt) = setup(); + let old_sector = commit_sector(&mut h, &rt); h.advance_and_submit_posts(&rt, &vec![old_sector.clone()]); @@ -197,12 +204,20 @@ fn updates_expiration_with_valid_params(v2: bool) { }], }; + // Change the circulating supply so we can detect fee changes (that shouldn't happen). + rt.set_circulating_supply(rt.total_fil_circ_supply() * 2); + h.extend_sectors_versioned(&rt, params, v2).unwrap(); // assert sector expiration is set to the new value let new_sector = h.get_sector(&rt, old_sector.sector_number); assert_eq!(new_expiration, new_sector.expiration); + // assert that the fee hasn't changed + assert_eq!(old_sector.daily_fee, new_sector.daily_fee); + let deadline = h.get_deadline(&rt, deadline_index); + assert_eq!(new_sector.daily_fee, deadline.daily_fee); + let quant = state.quant_spec_for_deadline(rt.policy(), deadline_index); // assert that new expiration exists @@ -220,6 +235,164 @@ fn updates_expiration_with_valid_params(v2: bool) { h.check_state(&rt); } +#[test_case(false, 25; "v1_grace")] +#[test_case(false, 26; "v1_active")] +#[test_case(true, 25; "v2_grace")] +#[test_case(true, 26; "v2_active")] +fn updates_expiration_and_daily_fee(v2: bool, nv: u32) { + // Start with sectors that have a zero fee (i.e. indicating they are pre-FIP-0100). Two sectors + // for both cases, but in v2 we will make the second sector fully verified to test the fee + // calculation. + + let (mut h, rt) = setup(); + + // Common setup + h.construct_and_verify(&rt); + rt.set_circulating_supply(TokenAmount::zero()); + rt.set_network_version(NetworkVersion::from(nv)); + + // Create deal for v2 cases + let deal = ActivatedDeal { + client: 0, + allocation_id: 1, + data: Default::default(), + size: PaddedPieceSize(h.sector_size as u64), + }; + + // Configure sectors based on version + let sector_pieces = vec![vec![], if v2 { vec![1] } else { vec![] }]; + let activated_deals = + if v2 { HashMap::from_iter(vec![(1, vec![deal.clone()])]) } else { Default::default() }; + // Commit sectors + let config = ProveCommitConfig { + verify_deals_exit: Default::default(), + claim_allocs_exit: Default::default(), + activated_deals, + }; + + let old_sectors = h.commit_and_prove_sectors_with_cfgs( + &rt, + 2, + DEFAULT_SECTOR_EXPIRATION as u64, + sector_pieces, + true, + config, + ); + h.advance_and_submit_posts(&rt, &old_sectors); + + // Verify initial state (fees should be zero) + for sector in &old_sectors { + assert_eq!( + sector.daily_fee, + TokenAmount::zero(), + "expected sector's daily fee to be zero because the circulating supply is zero" + ); + } + h.advance_and_submit_posts(&rt, &old_sectors); + + // Prepare extension parameters + let state: State = rt.get_state(); + let (deadline_index, partition_index) = + state.find_sector(rt.store(), old_sectors[0].sector_number).unwrap(); + let extension = 42 * rt.policy().wpost_proving_period; + let new_expiration = old_sectors[0].expiration + extension; + + assert!(h.get_deadline(&rt, deadline_index).daily_fee.is_zero()); + + // Set circulating supply to trigger fee calculation + let new_circulating_supply = TokenAmount::from_whole(500_000_000); + rt.set_circulating_supply(new_circulating_supply.clone()); + + // Extend sectors using version-appropriate method + if v2 { + // v2: separate regular sectors and sectors with claims + let params = ExtendSectorExpiration2Params { + extensions: vec![ExpirationExtension2 { + deadline: deadline_index, + partition: partition_index, + sectors: make_bitfield(&[old_sectors[0].sector_number]), + sectors_with_claims: vec![SectorClaim { + sector_number: old_sectors[1].sector_number, + maintain_claims: vec![1], + drop_claims: vec![], + }], + new_expiration, + }], + }; + + // Create claim for v2 + let client = Address::new_id(3000).id().unwrap(); + let claim = make_claim( + 1, + &old_sectors[1], + client, + h.receiver.id().unwrap(), + new_expiration, + &deal, + rt.policy.minimum_verified_allocation_term, + ); + + let mut claims = HashMap::new(); + claims.insert(1, Ok(claim)); + + h.extend_sectors2(&rt, params, claims).unwrap(); + } else { + // v1: extend all sectors together + let params = ExtendSectorExpirationParams { + extensions: vec![ExpirationExtension { + deadline: deadline_index, + partition: partition_index, + sectors: make_bitfield( + old_sectors.iter().map(|s| s.sector_number).collect_vec().as_slice(), + ), + new_expiration, + }], + }; + h.extend_sectors(&rt, params).unwrap(); + } + + let new_sectors = old_sectors.iter().map(|s| h.get_sector(&rt, s.sector_number)).collect_vec(); + + // Verify expirations + for sector in &new_sectors { + assert_eq!(new_expiration, sector.expiration); + } + + // Calculate expected fee for a full verified sector and the total fee of our two sectors + // combined, taking into account the grace period during which fees are zero. + let (full_verified_fee, total_fee) = if nv >= 26 { + ( + daily_proof_fee( + &rt.policy, + &rt.circulating_supply.borrow(), + &BigInt::from(h.sector_size as u64 * 10), + ), + new_sectors[0].daily_fee.clone() + new_sectors[1].daily_fee.clone(), + ) + } else { + (TokenAmount::zero(), TokenAmount::zero()) // grace period + }; + + // Verify fees - first sector has divided fee in both versions + assert_eq!(full_verified_fee.div_floor(10), new_sectors[0].daily_fee); + // Second sector fee depends on version + let expected_fee = if v2 { full_verified_fee } else { full_verified_fee.div_floor(10) }; + assert_eq!(expected_fee, new_sectors[1].daily_fee); + + let (deadline, partition) = h.get_deadline_and_partition(&rt, deadline_index, partition_index); + // Deadline has the two fees + assert_eq!(total_fee, deadline.daily_fee); + + // Partition expiration queue has the total fee as a deduction + let quant = h.get_state(&rt).quant_spec_for_deadline(&rt.policy, deadline_index); + let quantized_expiration = quant.quantize_up(new_sectors[0].expiration); + let p_queue = h.collect_partition_expirations(&rt, &partition); + let entry = p_queue.get(&quantized_expiration).cloned().unwrap(); + assert_eq!(total_fee, entry.fee_deduction); + + h.check_state(&rt); +} + #[test_case(false; "v1")] #[test_case(true; "v2")] fn updates_many_sectors(v2: bool) { @@ -363,8 +536,8 @@ fn supports_extensions_off_deadline_boundary(v2: bool) { let power = -power_for_sector(h.sector_size, &new_sector); let mut cron_config = CronConfig::empty(); cron_config.no_enrollment = true; - cron_config.expired_sectors_power_delta = Some(power); - cron_config.expired_sectors_pledge_delta = -new_sector.initial_pledge; + cron_config.power_delta = Some(power); + cron_config.pledge_delta = -new_sector.initial_pledge; h.advance_deadline(&rt, cron_config); @@ -444,6 +617,22 @@ fn update_expiration2_multiple_claims() { deadline_index, partition_index, ); + + // fee should not have changed + let new_sector = h.get_sector(&rt, old_sector.sector_number); + assert_eq!(old_sector.daily_fee, new_sector.daily_fee); + + let (deadline, partition) = h.get_deadline_and_partition(&rt, deadline_index, partition_index); + + // deadline has the fee + assert_eq!(new_sector.daily_fee, deadline.daily_fee); + + // partition expiration queue has the fee as a deduction + let quant = h.get_state(&rt).quant_spec_for_deadline(&rt.policy, deadline_index); + let quantized_expiration = quant.quantize_up(new_sector.expiration); + let p_queue = h.collect_partition_expirations(&rt, &partition); + let entry = p_queue.get(&quantized_expiration).cloned().unwrap(); + assert_eq!(new_sector.daily_fee, entry.fee_deduction); } #[test] @@ -618,13 +807,28 @@ fn extend_expiration2_drop_claims() { ]; let policy = Policy::default(); let old_sector = commit_sector_verified_deals(&verified_deals, &mut h, &rt); - h.advance_and_submit_posts(&rt, &vec![old_sector.clone()]); - let state: State = rt.get_state(); - let (deadline_index, partition_index) = state.find_sector(rt.store(), old_sector.sector_number).unwrap(); + { + // sanity check deadline and partition state is correct for original sector's fees + let (deadline, partition) = + h.get_deadline_and_partition(&rt, deadline_index, partition_index); + + // deadline has the fee + assert_eq!(old_sector.daily_fee, deadline.daily_fee); + + // partition expiration queue has the fee as a deduction + let quant = h.get_state(&rt).quant_spec_for_deadline(&rt.policy, deadline_index); + let quantized_expiration = quant.quantize_up(old_sector.expiration); + let p_queue = h.collect_partition_expirations(&rt, &partition); + let entry = p_queue.get(&quantized_expiration).cloned().unwrap(); + assert_eq!(old_sector.daily_fee, entry.fee_deduction); + } + + h.advance_and_submit_posts(&rt, &vec![old_sector.clone()]); + let extension = 42 * rt.policy().wpost_proving_period; let new_expiration = old_sector.expiration + extension; @@ -680,6 +884,33 @@ fn extend_expiration2_drop_claims() { assert_sector_verified_space(&mut h, &rt, old_sector.sector_number, verified_deals[0].size.0); + let new_sector = h.get_sector(&rt, old_sector.sector_number); + + { + // after dropping a claim that occupies half of the sector the daily fee should go down to the + // fee of a half verified sector + let expected_new_fee = (old_sector.daily_fee * 11).div_floor(20); + assert_eq!(expected_new_fee, new_sector.daily_fee); + let deadline = h.get_deadline(&rt, deadline_index); + assert_eq!(expected_new_fee, deadline.daily_fee); + } + + { + // check the deadline and partition state is correct for the replaced sector's fee + let (deadline, partition) = + h.get_deadline_and_partition(&rt, deadline_index, partition_index); + + // deadline has the fee + assert_eq!(new_sector.daily_fee, deadline.daily_fee); + + // partition expiration queue has the fee as a deduction + let quant = h.get_state(&rt).quant_spec_for_deadline(&rt.policy, deadline_index); + let quantized_expiration = quant.quantize_up(new_sector.expiration); + let p_queue = h.collect_partition_expirations(&rt, &partition); + let entry = p_queue.get(&quantized_expiration).cloned().unwrap(); + assert_eq!(new_sector.daily_fee, entry.fee_deduction); + } + // only claim0 stored in verifreg now let mut claims = HashMap::new(); claims.insert(claim_ids[0], Ok(claim0)); diff --git a/actors/miner/tests/miner_actor_test_partitions.rs b/actors/miner/tests/miner_actor_test_partitions.rs index 2d406a0e8..ce425317b 100644 --- a/actors/miner/tests/miner_actor_test_partitions.rs +++ b/actors/miner/tests/miner_actor_test_partitions.rs @@ -1,6 +1,6 @@ use fil_actor_miner::{ - power_for_sectors, testing::PartitionStateSummary, BitFieldQueue, ExpirationQueue, Partition, - PowerPair, QuantSpec, SectorOnChainInfo, + daily_fee_for_sectors, power_for_sectors, testing::PartitionStateSummary, BitFieldQueue, + ExpirationQueue, Partition, PowerPair, QuantSpec, SectorOnChainInfo, }; use fil_actors_runtime::runtime::Policy; use fil_actors_runtime::test_blockstores::MemoryBlockstore; @@ -20,12 +20,12 @@ const QUANT_SPEC: QuantSpec = QuantSpec { unit: 4, offset: 1 }; fn sectors() -> Vec { vec![ - test_sector(2, 1, 50, 60, 1000), - test_sector(3, 2, 51, 61, 1001), - test_sector(7, 3, 52, 62, 1002), - test_sector(8, 4, 53, 63, 1003), - test_sector(11, 5, 54, 64, 1004), - test_sector(13, 6, 55, 65, 1005), + test_sector(2, 1, 50, 60, 1000, 3000), + test_sector(3, 2, 51, 61, 1001, 4000), + test_sector(7, 3, 52, 62, 1002, 5000), + test_sector(8, 4, 53, 63, 1003, 6000), + test_sector(11, 5, 54, 64, 1004, 7000), + test_sector(13, 6, 55, 65, 1005, 8000), ] } @@ -63,11 +63,13 @@ fn setup_unproven() -> (MockRuntime, Partition) { let (_, rt) = setup(); let mut partition = Partition::new(&rt.store).unwrap(); - let power = + let (power, daily_fee) = partition.add_sectors(&rt.store, false, §ors(), SECTOR_SIZE, QUANT_SPEC).unwrap(); let expected_power = power_for_sectors(SECTOR_SIZE, §ors()); assert_eq!(expected_power, power); + let expected_daily_fee = daily_fee_for_sectors(§ors()); + assert_eq!(expected_daily_fee, daily_fee); (rt, partition) } @@ -466,23 +468,26 @@ mod miner_actor_test_partitions { let old_sectors = sectors()[1..4].to_vec(); let old_sector_power = power_for_sectors(SECTOR_SIZE, &old_sectors); let old_sector_pledge = TokenAmount::from_atto(1001 + 1002 + 1003); + let old_daily_fee = daily_fee_for_sectors(&old_sectors); // replace 1 and add 2 new sectors let new_sectors = vec![ - test_sector(10, 2, 150, 260, 3000), - test_sector(10, 7, 151, 261, 3001), - test_sector(18, 8, 152, 262, 3002), + test_sector(10, 2, 150, 260, 3000, 4000), + test_sector(10, 7, 151, 261, 3001, 5000), + test_sector(18, 8, 152, 262, 3002, 6000), ]; let new_sector_power = power_for_sectors(SECTOR_SIZE, &new_sectors); let new_sector_pledge = TokenAmount::from_atto(3000u64 + 3001 + 3002); + let new_daily_fee = daily_fee_for_sectors(&new_sectors); - let (power_delta, pledge_delta) = partition + let (power_delta, pledge_delta, daily_fee_delta) = partition .replace_sectors(&rt.store, &old_sectors, &new_sectors, SECTOR_SIZE, QUANT_SPEC) .unwrap(); let expected_power_delta = new_sector_power - old_sector_power; assert_eq!(expected_power_delta, power_delta); assert_eq!(new_sector_pledge - old_sector_pledge, pledge_delta); + assert_eq!(new_daily_fee - old_daily_fee, daily_fee_delta); // partition state should contain new sectors and not old sectors let mut all_sectors = new_sectors.clone(); @@ -530,7 +535,7 @@ mod miner_actor_test_partitions { let old_sectors = sectors()[1..4].to_vec(); // replace sector 2 - let new_sectors = vec![test_sector(10, 2, 150, 260, 3000)]; + let new_sectors = vec![test_sector(10, 2, 150, 260, 3000, 4000)]; let res = partition.replace_sectors( &rt.store, @@ -554,7 +559,7 @@ mod miner_actor_test_partitions { let old_sectors = sectors()[1..4].to_vec(); // replace sector 2 - let new_sectors = vec![test_sector(10, 2, 150, 260, 3000)]; + let new_sectors = vec![test_sector(10, 2, 150, 260, 3000, 4000)]; let res = partition.replace_sectors( &rt.store, @@ -574,17 +579,18 @@ mod miner_actor_test_partitions { fn terminate_sectors() { let (rt, mut partition) = setup_partition(); - let unproven_sector = vec![test_sector(13, 7, 55, 65, 1006)]; + let unproven_sector = vec![test_sector(13, 7, 55, 65, 1006, 9000)]; let mut all_sectors = sectors(); all_sectors.extend(unproven_sector.clone()); let sector_arr = sectors_arr(&rt.store, all_sectors); // Add an unproven sector. - let power = partition + let (power, daily_fee) = partition .add_sectors(&rt.store, false, &unproven_sector, SECTOR_SIZE, QUANT_SPEC) .unwrap(); let expected_power = power_for_sectors(SECTOR_SIZE, &unproven_sector); assert_eq!(expected_power, power); + assert_eq!(TokenAmount::from_atto(9000), daily_fee); // fault sector 3, 4, 5 and 6 let fault_set = make_bitfield(&[3, 4, 5, 6]); @@ -599,7 +605,7 @@ mod miner_actor_test_partitions { // now terminate 1, 3, 5, and 7 let terminations = make_bitfield(&[1, 3, 5, 7]); let termination_epoch = 3; - let removed = partition + let (removed, removed_unproven) = partition .terminate_sectors( &Policy::default(), &rt.store, @@ -617,6 +623,8 @@ mod miner_actor_test_partitions { let expected_faulty_power = power_for_sectors(SECTOR_SIZE, &select_sectors(§ors(), &make_bitfield(&[3, 5]))); assert_eq!(expected_faulty_power, removed.faulty_power); + let expected_unproven_power = power_for_sectors(SECTOR_SIZE, &unproven_sector); + assert_eq!(expected_unproven_power, removed_unproven); // expect partition state to no longer reflect power and pledge from terminated sectors and terminations to contain new sectors assert_partition_state( @@ -680,7 +688,7 @@ mod miner_actor_test_partitions { let termination_epoch = 3; // First termination works. - let removed = partition + let (removed, unproven_power) = partition .terminate_sectors( &Policy::default(), &rt.store, @@ -695,6 +703,7 @@ mod miner_actor_test_partitions { power_for_sectors(SECTOR_SIZE, &select_sectors(§ors(), &make_bitfield(&[1]))); assert_eq!(expected_active_power, removed.active_power); assert_eq!(removed.faulty_power, PowerPair::zero()); + assert_eq!(unproven_power, PowerPair::zero()); let count = removed.len(); assert_eq!(1, count); @@ -842,17 +851,18 @@ mod miner_actor_test_partitions { fn records_missing_post() { let (rt, mut partition) = setup_partition(); - let unproven_sector = vec![test_sector(13, 7, 55, 65, 1006)]; + let unproven_sector = vec![test_sector(13, 7, 55, 65, 1006, 5000)]; let mut all_sectors = sectors(); all_sectors.extend_from_slice(&unproven_sector); let sector_arr = sectors_arr(&rt.store, sectors()); // Add an unproven sector. - let power = partition + let (power, daily_fee) = partition .add_sectors(&rt.store, false, &unproven_sector, SECTOR_SIZE, QUANT_SPEC) .unwrap(); let expected_power = power_for_sectors(SECTOR_SIZE, &unproven_sector); assert_eq!(expected_power, power); + assert_eq!(TokenAmount::from_atto(5000), daily_fee); // make 4, 5 and 6 faulty let fault_set = make_bitfield(&[4, 5, 6]); @@ -977,15 +987,17 @@ mod miner_actor_test_partitions { for (i, info) in many_sectors.iter_mut().enumerate() { let id = (i as u64 + 1) << 50; ids[i] = id; - *info = test_sector(i as i64 + 1, id, 50, 60, 1000); + *info = test_sector(i as i64 + 1, id, 50, 60, 1000, i as u64 * 1000); } let sector_numbers = bitfield_from_slice(&ids); - let power = partition + let (power, daily_fee) = partition .add_sectors(&rt.store, false, &many_sectors, SECTOR_SIZE, QUANT_SPEC) .unwrap(); let expected_power = power_for_sectors(SECTOR_SIZE, &many_sectors); + let expected_daily_fee = daily_fee_for_sectors(&many_sectors); assert_eq!(expected_power, power); + assert_eq!(expected_daily_fee, daily_fee); assert_partition_state( &rt.store, diff --git a/actors/miner/tests/miner_actor_test_precommit_batch.rs b/actors/miner/tests/miner_actor_test_precommit_batch.rs index 0413fb60c..dd913e4d8 100644 --- a/actors/miner/tests/miner_actor_test_precommit_batch.rs +++ b/actors/miner/tests/miner_actor_test_precommit_batch.rs @@ -1,10 +1,9 @@ use fil_actor_market::Method as MarketMethod; use fil_actor_miner::{ - aggregate_pre_commit_network_fee, max_prove_commit_duration, pre_commit_deposit_for_power, - qa_power_max, PreCommitSectorBatchParams, PreCommitSectorParams, State, + max_prove_commit_duration, pre_commit_deposit_for_power, qa_power_max, + PreCommitSectorBatchParams, PreCommitSectorParams, State, }; use fil_actor_power::Method as PowerMethod; -use fil_actors_runtime::runtime::Policy; use fil_actors_runtime::test_utils::*; use fvm_shared::clock::ChainEpoch; use fvm_shared::deal::DealID; @@ -36,7 +35,6 @@ struct DealSpec { fn assert_simple_batch( batch_size: usize, balance_surplus: TokenAmount, - base_fee: TokenAmount, deal_specs: &[DealSpec], exit_code: ExitCode, error_str: &str, @@ -81,21 +79,14 @@ fn assert_simple_batch( &pwr_estimate, ); } - let net_fee = aggregate_pre_commit_network_fee(batch_size, &base_fee); let total_deposit: TokenAmount = deposits.iter().sum(); - let total_balance = net_fee + &total_deposit; - rt.set_balance(total_balance + balance_surplus); + rt.set_balance(&total_deposit + balance_surplus); if exit_code != ExitCode::OK { expect_abort_contains_message( exit_code, error_str, - h.pre_commit_sector_batch( - &rt, - PreCommitSectorBatchParams { sectors }, - &conf, - &base_fee, - ), + h.pre_commit_sector_batch(&rt, PreCommitSectorBatchParams { sectors }, &conf), ); rt.reset(); @@ -110,7 +101,6 @@ fn assert_simple_batch( &rt, PreCommitSectorBatchParams { sectors: sectors.clone() }, &conf, - &base_fee, ); // Check precommits @@ -156,17 +146,17 @@ mod miner_actor_precommit_batch { #[test] fn one_sector() { - assert_simple_batch(1, TokenAmount::zero(), TokenAmount::zero(), &[], ExitCode::OK, ""); + assert_simple_batch(1, TokenAmount::zero(), &[], ExitCode::OK, ""); } #[test] fn thirty_two_sectors() { - assert_simple_batch(32, TokenAmount::zero(), TokenAmount::zero(), &[], ExitCode::OK, ""); + assert_simple_batch(32, TokenAmount::zero(), &[], ExitCode::OK, ""); } #[test] fn max_sectors() { - assert_simple_batch(256, TokenAmount::zero(), TokenAmount::zero(), &[], ExitCode::OK, ""); + assert_simple_batch(256, TokenAmount::zero(), &[], ExitCode::OK, ""); } #[test] @@ -174,7 +164,6 @@ mod miner_actor_precommit_batch { assert_simple_batch( 3, TokenAmount::zero(), - TokenAmount::zero(), &[DealSpec { ids: vec![1], commd: Some(make_piece_cid("1".as_bytes())) }], ExitCode::OK, "", @@ -185,7 +174,6 @@ mod miner_actor_precommit_batch { assert_simple_batch( 3, TokenAmount::zero(), - TokenAmount::zero(), &[ DealSpec { ids: vec![1], commd: Some(make_piece_cid("1".as_bytes())) }, DealSpec { ids: vec![2], commd: Some(make_piece_cid("2".as_bytes())) }, @@ -201,30 +189,17 @@ mod miner_actor_precommit_batch { assert_simple_batch( 0, TokenAmount::zero(), - TokenAmount::zero(), &[], ExitCode::USR_ILLEGAL_ARGUMENT, "batch empty", ); } - #[test] - fn too_many_sectors() { - assert_simple_batch( - Policy::default().pre_commit_sector_batch_max_size + 1, - TokenAmount::zero(), - TokenAmount::zero(), - &[], - ExitCode::USR_ILLEGAL_ARGUMENT, - "batch of 257 too large", - ); - } #[test] fn insufficient_balance() { assert_simple_batch( 10, TokenAmount::from_atto(-1), - TokenAmount::zero(), &[], ExitCode::USR_INSUFFICIENT_FUNDS, "insufficient funds", @@ -267,7 +242,6 @@ mod miner_actor_precommit_batch { &rt, PreCommitSectorBatchParams { sectors }, &PreCommitBatchConfig { sector_unsealed_cid: vec![], first_for_miner: true }, - &TokenAmount::zero(), ), ); rt.reset(); @@ -304,7 +278,6 @@ mod miner_actor_precommit_batch { &rt, PreCommitSectorBatchParams { sectors }, &PreCommitBatchConfig { sector_unsealed_cid: vec![], first_for_miner: true }, - &TokenAmount::zero(), ), ); rt.reset(); diff --git a/actors/miner/tests/miner_actor_test_wpost.rs b/actors/miner/tests/miner_actor_test_wpost.rs index d1aee1318..b1761bdef 100644 --- a/actors/miner/tests/miner_actor_test_wpost.rs +++ b/actors/miner/tests/miner_actor_test_wpost.rs @@ -1,7 +1,6 @@ #![allow(clippy::all)] use fil_actor_miner as miner; -use fil_actor_miner::PowerPair; use fil_actors_runtime::runtime::DomainSeparationTag; use fil_actors_runtime::test_utils::*; use fvm_ipld_bitfield::BitField; @@ -67,7 +66,8 @@ fn basic_post_and_dispute() { assert_bitfield_equals(&posts[0].partitions, &deadline_bits); // Advance to end-of-deadline cron to verify no penalties. - h.advance_deadline(&rt, CronConfig::empty()); + let burnt_funds = miner::daily_fee_for_sectors(§ors); + h.advance_deadline(&rt, CronConfig { burnt_funds, ..Default::default() }); h.check_state(&rt); // Proofs should exist in snapshot. @@ -573,7 +573,8 @@ fn duplicate_proof_rejected() { rt.reset(); // Advance to end-of-deadline cron to verify no penalties. - h.advance_deadline(&rt, CronConfig::empty()); + let burnt_funds = miner::daily_fee_for_sectors(§ors); + h.advance_deadline(&rt, CronConfig { burnt_funds, ..Default::default() }); h.check_state(&rt); } @@ -627,6 +628,8 @@ fn duplicate_proof_rejected_with_many_partitions() { let deadline_bits = [0]; assert_bitfield_equals(&deadline.partitions_posted, &deadline_bits); } + + let mut daily_fee = TokenAmount::zero(); // daily_fee for all sectors in this deadline { // Attempt PoSt for both partitions, thus duplicating proof for partition 0, so rejected let post_partitions = vec![ @@ -637,6 +640,7 @@ fn duplicate_proof_rejected_with_many_partitions() { (0..h.partition_size).map(|i| sectors[i as usize].clone()).collect(); sectors_to_prove.push(last_sector.clone()); let pwr = miner::power_for_sectors(h.sector_size, §ors_to_prove); + daily_fee += miner::daily_fee_for_sectors(§ors_to_prove); let params = miner::SubmitWindowedPoStParams { deadline: dlinfo.index, @@ -679,7 +683,7 @@ fn duplicate_proof_rejected_with_many_partitions() { } // Advance to end-of-deadline cron to verify no penalties. - h.advance_deadline(&rt, CronConfig::empty()); + h.advance_deadline(&rt, CronConfig { burnt_funds: daily_fee, ..Default::default() }); h.check_state(&rt); } @@ -748,9 +752,18 @@ fn successful_recoveries_recover_power() { assert!(posts.is_empty()); // Next deadline cron does not charge for the fault - h.advance_deadline(&rt, CronConfig::empty()); + let daily_fee = miner::daily_fee_for_sectors(&infos); + h.advance_deadline( + &rt, + CronConfig { + pledge_delta: -daily_fee.clone(), + burnt_funds: daily_fee.clone(), + ..Default::default() + }, + ); - assert_eq!(initial_locked, h.get_locked_funds(&rt)); + // the daily_fee was charged out of our locked rewards, so we expect a reduction by one for each day / PoST + assert_eq!(initial_locked - daily_fee * 2, h.get_locked_funds(&rt)); h.check_state(&rt); } @@ -798,10 +811,18 @@ fn skipped_faults_adjust_power() { ); // expect continued fault fee to be charged during cron - let fault_fee = h.continued_fault_penalty(&infos1); - dlinfo = h.advance_deadline(&rt, CronConfig::with_continued_faults_penalty(fault_fee)); + let continued_faults_penalty = h.continued_fault_penalty(&infos1); + let burnt_funds = miner::daily_fee_for_sectors(&infos) + continued_faults_penalty; + dlinfo = h.advance_deadline( + &rt, + CronConfig { + pledge_delta: -burnt_funds.clone(), + burnt_funds: burnt_funds.clone(), + ..Default::default() + }, + ); - // advance to next proving period, expect no fees + // advance to next proving period, expect no fees other than the daily_fee while dlinfo.index != dlidx { dlinfo = h.advance_deadline(&rt, CronConfig::empty()); } @@ -831,13 +852,16 @@ fn skipped_faults_adjust_power() { // The second sector is detected faulty but pays nothing yet. // Expect ongoing fault penalty for only the first, continuing-faulty sector. + // burnt_funds remains the same. let pwr_delta = -miner::power_for_sectors(h.sector_size, &infos2); - let fault_fee = h.continued_fault_penalty(&infos1); h.advance_deadline( &rt, - CronConfig::with_detected_faults_power_delta_and_continued_faults_penalty( - &pwr_delta, fault_fee, - ), + CronConfig { + pledge_delta: -burnt_funds.clone(), + burnt_funds: burnt_funds.clone(), + power_delta: Some(pwr_delta), + ..Default::default() + }, ); h.check_state(&rt); @@ -887,7 +911,11 @@ fn skipping_all_sectors_in_a_partition_rejected() { rt.reset(); // These sectors are detected faulty and pay no penalty this time. - h.advance_deadline(&rt, CronConfig::with_continued_faults_penalty(TokenAmount::zero())); + let burnt_funds = miner::daily_fee_for_sectors(&infos); + h.advance_deadline( + &rt, + CronConfig { pledge_delta: -burnt_funds.clone(), burnt_funds, ..Default::default() }, + ); h.check_state(&rt); } @@ -938,8 +966,12 @@ fn skipped_recoveries_are_penalized_and_do_not_recover_power() { h.submit_window_post(&rt, &dlinfo, vec![partition], infos.clone(), PoStConfig::empty()); // sector will be charged ongoing fee at proving period cron - let ongoing_fee = h.continued_fault_penalty(&infos1); - h.advance_deadline(&rt, CronConfig::with_continued_faults_penalty(ongoing_fee)); + let continued_faults_penalty = h.continued_fault_penalty(&infos1); + let burnt_funds = miner::daily_fee_for_sectors(&infos) + continued_faults_penalty; + h.advance_deadline( + &rt, + CronConfig { pledge_delta: -burnt_funds.clone(), burnt_funds, ..Default::default() }, + ); h.check_state(&rt); } @@ -1250,16 +1282,23 @@ fn bad_post_fails_when_verified() { // Become faulty h.advance_to_deadline(&rt, dlidx); - h.advance_deadline(&rt, CronConfig::empty()); + let daily_fee = miner::daily_fee_for_sectors(&infos); + h.advance_deadline( + &rt, + CronConfig { + pledge_delta: -daily_fee.clone(), + burnt_funds: daily_fee.clone(), + ..Default::default() + }, + ); h.advance_to_deadline(&rt, dlidx); - let fault_fee = h.continued_fault_penalty(&vec![infos[0].clone(), infos[1].clone()]); + let continued_faults_penalty = + h.continued_fault_penalty(&vec![infos[0].clone(), infos[1].clone()]); + let burnt_funds = daily_fee + continued_faults_penalty; h.advance_deadline( &rt, - CronConfig::with_detected_faults_power_delta_and_continued_faults_penalty( - &PowerPair::zero(), - fault_fee, - ), + CronConfig { pledge_delta: -burnt_funds.clone(), burnt_funds, ..Default::default() }, ); // Promise to recover diff --git a/actors/miner/tests/policy_test.rs b/actors/miner/tests/policy_test.rs index ef56479e3..e77b3d310 100644 --- a/actors/miner/tests/policy_test.rs +++ b/actors/miner/tests/policy_test.rs @@ -1,13 +1,17 @@ -use fil_actor_miner::{qa_power_for_weight, quality_for_weight}; +use fil_actor_miner::{daily_proof_fee, qa_power_for_weight, quality_for_weight}; use fil_actor_miner::{ QUALITY_BASE_MULTIPLIER, SECTOR_QUALITY_PRECISION, VERIFIED_DEAL_WEIGHT_MULTIPLIER, }; +use fil_actors_runtime::runtime::Policy; use fil_actors_runtime::DealWeight; use fil_actors_runtime::{EPOCHS_IN_DAY, SECONDS_IN_DAY}; use fvm_shared::bigint::{BigInt, Integer, Zero}; use fvm_shared::clock::ChainEpoch; +use fvm_shared::econ::TokenAmount; use fvm_shared::sector::SectorSize; +use num_traits::Signed; + #[test] fn quality_is_independent_of_size_and_duration() { // Quality of space with no deals. This doesn't depend on either the sector size or duration. @@ -291,3 +295,38 @@ fn original_quality_for_weight( .div_floor(§or_space_time) .div_floor(&QUALITY_BASE_MULTIPLIER) } + +#[test] +fn daily_proof_fee_calc() { + let policy = Policy::default(); + // Given a CS of 680M FIL, 32GiB QAP, a fee multiplier of 5.56e-15 per 32GiB QAP, the daily proof + // fee should be 3780 nanoFIL. + // 680M * 5.56e-15 = 0.000003780800 FIL + // 0.0000037808 * 1e9 = 3780 nanoFIL + // 0.0000037808 * 1e18 = 3780800000000 attoFIL + // As a per-byte multiplier we use 1.61817e-25, a close approximation of 5.56e-15 / 32GiB. + // 680M * 32GiB * 1.61817e-25 = 0.000003780793052776 FIL + // 0.000003780793052776 * 1e18 = 3780793052776 attoFIL + let circulating_supply = TokenAmount::from_whole(680_000_000); + + let ref_32gib_fee = 3780793052776_u64; + [ + (32_u64, ref_32gib_fee), + (64, ref_32gib_fee * 2), + (32 * 10, ref_32gib_fee * 10), + (32 * 5, ref_32gib_fee * 5), + (64 * 10, ref_32gib_fee * 20), + ] + .iter() + .for_each(|(size, expected_fee)| { + let power = BigInt::from(*size) << 30; // 32GiB raw QAP + let fee = daily_proof_fee(&policy, &circulating_supply, &power); + assert!( + (fee.atto() - BigInt::from(*expected_fee)).abs() <= BigInt::from(10), + "size: {}, fee: {}, expected_fee: {} (±10)", + size, + fee.atto(), + expected_fee + ); + }); +} diff --git a/actors/miner/tests/prove_commit2_failures_test.rs b/actors/miner/tests/prove_commit2_failures_test.rs index 028ccb383..c1d5d7408 100644 --- a/actors/miner/tests/prove_commit2_failures_test.rs +++ b/actors/miner/tests/prove_commit2_failures_test.rs @@ -3,7 +3,7 @@ use fvm_shared::address::Address; use fvm_shared::deal::DealID; use fvm_shared::error::ExitCode; use fvm_shared::sector::SectorNumber; -use fvm_shared::{bigint::Zero, clock::ChainEpoch, econ::TokenAmount, ActorID}; +use fvm_shared::{clock::ChainEpoch, ActorID}; use fil_actor_miner::ext::verifreg::AllocationID; use fil_actor_miner::{ @@ -134,7 +134,7 @@ fn reject_precommit_deals() { &[&[piece_size], &[piece_size]], ); precommits[0].deal_ids.push(1); - h.pre_commit_sector_batch_v2(&rt, &precommits, true, &TokenAmount::zero()).unwrap(); + h.pre_commit_sector_batch_v2(&rt, &precommits, true).unwrap(); rt.set_epoch(precommit_epoch + rt.policy.pre_commit_challenge_delay + 1); let manifests: Vec = precommits @@ -274,7 +274,7 @@ fn setup_precommits( sector_expiry, &piece_sizes, ); - h.pre_commit_sector_batch_v2(&rt, &precommits, true, &TokenAmount::zero()).unwrap(); + h.pre_commit_sector_batch_v2(&rt, &precommits, true).unwrap(); rt.set_epoch(precommit_epoch + rt.policy.pre_commit_challenge_delay + 1); let manifests = precommits diff --git a/actors/miner/tests/prove_commit2_test.rs b/actors/miner/tests/prove_commit2_test.rs index a2aaea21c..61442a57a 100644 --- a/actors/miner/tests/prove_commit2_test.rs +++ b/actors/miner/tests/prove_commit2_test.rs @@ -1,7 +1,7 @@ use fvm_ipld_encoding::RawBytes; use fvm_shared::error::ExitCode; use fvm_shared::sector::SectorNumber; -use fvm_shared::{bigint::Zero, clock::ChainEpoch, econ::TokenAmount, ActorID}; +use fvm_shared::{clock::ChainEpoch, ActorID}; use fil_actor_miner::ext::verifreg::{AllocationClaim, SectorAllocationClaims}; use fil_actor_miner::{ @@ -521,7 +521,7 @@ fn precommit_sectors_from( sector_expiry, piece_sizes, ); - h.pre_commit_sector_batch_v2(rt, &precommits, first_for_miner, &TokenAmount::zero()).unwrap(); + h.pre_commit_sector_batch_v2(rt, &precommits, first_for_miner).unwrap(); rt.set_epoch(precommit_epoch + rt.policy.pre_commit_challenge_delay + 1); precommits } diff --git a/actors/miner/tests/prove_commit_niporep.rs b/actors/miner/tests/prove_commit_niporep.rs index 8cbe80f67..40fb0362b 100644 --- a/actors/miner/tests/prove_commit_niporep.rs +++ b/actors/miner/tests/prove_commit_niporep.rs @@ -4,7 +4,6 @@ use fvm_shared::{bigint::BigInt, clock::ChainEpoch, error::ExitCode}; use fil_actor_miner::{ Actor, Method, SectorNIActivationInfo, SectorOnChainInfo, SectorOnChainInfoFlags, - NI_AGGREGATE_FEE_BASE_SECTOR_COUNT, }; use num_traits::Zero; use util::*; @@ -185,10 +184,9 @@ fn ni_prove_partialy_valid_sectors_not_required_activation() { rt.set_epoch(activation_epoch); + let num_fails: usize = 5; let num_success: usize = 2; - let sector_nums = - (0..((NI_AGGREGATE_FEE_BASE_SECTOR_COUNT + num_success) as u64)).collect::>(); - let num_fails = NI_AGGREGATE_FEE_BASE_SECTOR_COUNT; + let sector_nums = (0..((num_fails + num_success) as u64)).collect::>(); let mut params = h.make_prove_commit_ni_params( miner, §or_nums, diff --git a/actors/miner/tests/prove_replica_failures_test.rs b/actors/miner/tests/prove_replica_failures_test.rs index 4493374d5..41e287c48 100644 --- a/actors/miner/tests/prove_replica_failures_test.rs +++ b/actors/miner/tests/prove_replica_failures_test.rs @@ -19,7 +19,7 @@ use util::*; mod util; -// Tests for ProveReplicaUpdates2 where the request fails completely +// Tests for ProveReplicaUpdates3 where the request fails completely const CLIENT_ID: ActorID = 1000; const DEFAULT_SECTOR_EXPIRATION_DAYS: ChainEpoch = 220; @@ -35,7 +35,7 @@ fn reject_unauthorized_caller() { expect_abort_contains_message( ExitCode::USR_FORBIDDEN, "caller", - h.prove_replica_updates2_batch(&rt, §or_updates, false, false, cfg), + h.prove_replica_updates3_batch(&rt, §or_updates, false, false, cfg), ); h.check_state(&rt); } @@ -53,7 +53,7 @@ fn reject_no_proof_types() { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, "exactly one of sector proofs or aggregate proof must be non-empty", - h.prove_replica_updates2_batch(&rt, §or_updates, false, false, cfg), + h.prove_replica_updates3_batch(&rt, §or_updates, false, false, cfg), ); h.check_state(&rt); } @@ -71,7 +71,7 @@ fn reject_both_proof_types() { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, "exactly one of sector proofs or aggregate proof must be non-empty", - h.prove_replica_updates2_batch(&rt, §or_updates, false, false, cfg), + h.prove_replica_updates3_batch(&rt, §or_updates, false, false, cfg), ); h.check_state(&rt); } @@ -88,7 +88,7 @@ fn reject_mismatched_proof_len() { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, "mismatched lengths", - h.prove_replica_updates2_batch(&rt, §or_updates, false, false, cfg), + h.prove_replica_updates3_batch(&rt, §or_updates, false, false, cfg), ); h.check_state(&rt); } @@ -107,7 +107,7 @@ fn reject_aggregate_proof() { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, "aggregate update proofs not yet supported", - h.prove_replica_updates2_batch(&rt, §or_updates, false, false, cfg), + h.prove_replica_updates3_batch(&rt, §or_updates, false, false, cfg), ); h.check_state(&rt); } @@ -119,7 +119,7 @@ fn reject_all_proofs_fail() { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, "no valid updates", - h.prove_replica_updates2_batch(&rt, §or_updates, false, false, cfg), + h.prove_replica_updates3_batch(&rt, §or_updates, false, false, cfg), ); h.check_state(&rt); } @@ -132,7 +132,7 @@ fn reject_invalid_update() { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, "invalid update 1 while requiring activation success", - h.prove_replica_updates2_batch(&rt, §or_updates, true, false, cfg), + h.prove_replica_updates3_batch(&rt, §or_updates, true, false, cfg), ); h.check_state(&rt); } @@ -144,7 +144,7 @@ fn reject_required_proof_failure() { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, "requiring activation success", - h.prove_replica_updates2_batch(&rt, §or_updates, true, false, cfg), + h.prove_replica_updates3_batch(&rt, §or_updates, true, false, cfg), ); h.check_state(&rt); } @@ -156,7 +156,7 @@ fn reject_required_claim_failure() { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, "error claiming allocations", - h.prove_replica_updates2_batch(&rt, §or_updates, true, false, cfg), + h.prove_replica_updates3_batch(&rt, §or_updates, true, false, cfg), ); h.check_state(&rt); } @@ -173,7 +173,7 @@ fn reject_required_notification_abort() { expect_abort_contains_message( ERR_NOTIFICATION_RECEIVER_ABORTED, "receiver aborted", - h.prove_replica_updates2_batch(&rt, §or_updates, false, true, cfg), + h.prove_replica_updates3_batch(&rt, §or_updates, false, true, cfg), ); h.check_state(&rt); } @@ -187,7 +187,7 @@ fn reject_required_notification_rejected() { expect_abort_contains_message( ERR_NOTIFICATION_REJECTED, "sector change rejected", - h.prove_replica_updates2_batch(&rt, §or_updates, false, true, cfg), + h.prove_replica_updates3_batch(&rt, §or_updates, false, true, cfg), ); h.check_state(&rt); } diff --git a/actors/miner/tests/prove_replica_test.rs b/actors/miner/tests/prove_replica_test.rs index 5669b550b..9edc459a9 100644 --- a/actors/miner/tests/prove_replica_test.rs +++ b/actors/miner/tests/prove_replica_test.rs @@ -1,14 +1,19 @@ use fvm_ipld_encoding::RawBytes; +use fvm_shared::bigint::BigInt; +use fvm_shared::econ::TokenAmount; use fvm_shared::error::ExitCode; use fvm_shared::sector::SectorNumber; use fvm_shared::{clock::ChainEpoch, ActorID}; use fil_actor_miner::ext::verifreg::{AllocationClaim, SectorAllocationClaims}; -use fil_actor_miner::{DataActivationNotification, PieceChange, SectorChanges, State}; +use fil_actor_miner::{ + daily_proof_fee, DataActivationNotification, PieceChange, SectorChanges, State, +}; use fil_actor_miner::{ProveReplicaUpdates3Return, SectorOnChainInfo}; use fil_actors_runtime::cbor::serialize; use fil_actors_runtime::test_utils::{expect_abort_contains_message, MockRuntime}; use fil_actors_runtime::{runtime::Runtime, BatchReturn, EPOCHS_IN_DAY, STORAGE_MARKET_ACTOR_ADDR}; +use num_traits::Zero; use util::*; mod util; @@ -20,6 +25,11 @@ const FIRST_SECTOR_NUMBER: SectorNumber = 100; #[test] fn update_batch() { let (h, rt, sectors) = setup_empty_sectors(4); + + // Reduce the circulating supply. We expect the fees to stay the same after replica update even + // if the circulating supply changes. + rt.set_circulating_supply(TokenAmount::from_whole(200_000)); + let snos = sectors.iter().map(|s| s.sector_number).collect::>(); let st: State = h.get_state(&rt); let store = rt.store(); @@ -34,7 +44,7 @@ fn update_batch() { let cfg = ProveReplicaUpdatesConfig::default(); let (result, claims, notifications) = - h.prove_replica_updates2_batch(&rt, §or_updates, true, true, cfg).unwrap(); + h.prove_replica_updates3_batch(&rt, §or_updates, true, true, cfg).unwrap(); assert_update_result(&vec![ExitCode::OK; sectors.len()], &result); // Explicitly verify claims match what we expect. @@ -99,14 +109,202 @@ fn update_batch() { notifications ); - // Sector 0: Even though there's no "deal", the data weight is set. - verify_weights(&rt, &h, snos[0], piece_size, 0); - // Sector 1: With an allocation, the verified weight is set instead. - verify_weights(&rt, &h, snos[1], 0, piece_size); - // Sector 2: Deal weight is set. - verify_weights(&rt, &h, snos[2], piece_size, 0); - // Sector 3: Deal doesn't make a difference to verified weight only set. - verify_weights(&rt, &h, snos[3], 0, piece_size); + let sectors_after = snos.iter().map(|sno| h.get_sector(&rt, *sno)).collect::>(); + let mut total_fees = TokenAmount::zero(); + for (i, (before, after)) in sectors.iter().zip(§ors_after).enumerate() { + // Sectors with odd indices (1 and 3) are full of verified data, even indices (0 and 2) are not + let has_verified = i % 2 == 1; + + verify_weights( + &rt, + &h, + before.sector_number, + if has_verified { 0 } else { piece_size }, + if has_verified { piece_size } else { 0 }, + ); + + // Check daily fees - if we added verified data, we expect the fees to be x10 + let expected_fee = &before.daily_fee * if has_verified { 10 } else { 1 }; + assert_eq!( + expected_fee, after.daily_fee, + "daily fees differ for sector {}", + before.sector_number + ); + + total_fees += &after.daily_fee; + } + + let (deadline_index, partition_index) = st.find_sector(rt.store(), snos[0]).unwrap(); + // check the deadline and partition state is correct for the replaced sector's fee + let (deadline, partition) = h.get_deadline_and_partition(&rt, deadline_index, partition_index); + + // deadline has the total fees for all sectors + assert_eq!(total_fees, deadline.daily_fee); + + // partition expiration queue has the total fees for all sectors as a deduction + let quant = h.get_state(&rt).quant_spec_for_deadline(&rt.policy, deadline_index); + let quantized_expiration = quant.quantize_up(sectors_after[0].expiration); + let p_queue = h.collect_partition_expirations(&rt, &partition); + let entry = p_queue.get(&quantized_expiration).unwrap().clone(); + assert_eq!(total_fees, entry.fee_deduction); + + h.check_state(&rt); +} + +#[test] +fn update_fee() { + let (h, rt) = setup_basic(); + + // Set the circulating supply to 0 to get no fees. + rt.set_circulating_supply(TokenAmount::zero()); + let sector_expiry = *rt.epoch.borrow() + DEFAULT_SECTOR_EXPIRATION_DAYS * EPOCHS_IN_DAY; + let sectors = onboard_empty_sectors(&rt, &h, sector_expiry, FIRST_SECTOR_NUMBER, 4); + let st: State = h.get_state(&rt); + let (deadline_index, partition_index) = + st.find_sector(rt.store(), sectors[0].sector_number).unwrap(); + // check the deadline and partition state is correct for the replaced sector's fee + let (deadline, partition) = h.get_deadline_and_partition(&rt, deadline_index, partition_index); + + // sanity check the fee state + // 1. sectors have no fees + assert!(sectors.iter().all(|s| s.daily_fee.is_zero())); + // 2. deadline has no fees + assert!(deadline.daily_fee.is_zero()); + // 3. expiration queue has no fees + let quant = h.get_state(&rt).quant_spec_for_deadline(&rt.policy, deadline_index); + let quantized_expiration = quant.quantize_up(sectors[0].expiration); + let p_queue = h.collect_partition_expirations(&rt, &partition); + let entry = p_queue.get(&quantized_expiration).unwrap().clone(); + assert!(entry.fee_deduction.is_zero()); + + // Now set the circulating supply to a non-zero value. Snapping should change the daily fee. + let new_circulating_supply = TokenAmount::from_whole(500_000); + rt.set_circulating_supply(new_circulating_supply.clone()); + + let snos = sectors.iter().map(|s| s.sector_number).collect::>(); + let st: State = h.get_state(&rt); + let store = rt.store(); + let piece_size = h.sector_size as u64; + // Update in batch, each with a single piece. + let sector_updates = vec![ + make_update_manifest(&st, store, snos[0], &[(piece_size, 0, 0, 0)]), // No alloc or deal + make_update_manifest(&st, store, snos[1], &[(piece_size, CLIENT_ID, 1000, 0)]), // Just an alloc + make_update_manifest(&st, store, snos[2], &[(piece_size, 0, 0, 2000)]), // Just a deal + make_update_manifest(&st, store, snos[3], &[(piece_size, CLIENT_ID, 1001, 2001)]), // Alloc and deal + ]; + + let cfg = ProveReplicaUpdatesConfig::default(); + let (result, claims, notifications) = + h.prove_replica_updates3_batch(&rt, §or_updates, true, true, cfg).unwrap(); + assert_update_result(&vec![ExitCode::OK; sectors.len()], &result); + + // Explicitly verify claims match what we expect. + assert_eq!( + vec![ + SectorAllocationClaims { + sector: snos[0], + expiry: sectors[0].expiration, + claims: vec![], + }, + SectorAllocationClaims { + sector: snos[1], + expiry: sectors[1].expiration, + claims: vec![AllocationClaim { + client: CLIENT_ID, + allocation_id: 1000, + data: sector_updates[1].pieces[0].cid, + size: sector_updates[1].pieces[0].size, + }], + }, + SectorAllocationClaims { + sector: snos[2], + expiry: sectors[2].expiration, + claims: vec![], + }, + SectorAllocationClaims { + sector: snos[3], + expiry: sectors[3].expiration, + claims: vec![AllocationClaim { + client: CLIENT_ID, + allocation_id: 1001, + data: sector_updates[3].pieces[0].cid, + size: sector_updates[3].pieces[0].size, + }], + }, + ], + claims + ); + + // Explicitly verify notifications match what we expect. + assert_eq!( + vec![ + SectorChanges { + sector: snos[2], + minimum_commitment_epoch: sectors[2].expiration, + added: vec![PieceChange { + data: sector_updates[2].pieces[0].cid, + size: sector_updates[2].pieces[0].size, + payload: serialize(&2000, "").unwrap(), + },], + }, + SectorChanges { + sector: snos[3], + minimum_commitment_epoch: sectors[3].expiration, + added: vec![PieceChange { + data: sector_updates[3].pieces[0].cid, + size: sector_updates[3].pieces[0].size, + payload: serialize(&2001, "").unwrap(), + },], + }, + ], + notifications + ); + + // When checking sector daily_fee, for a reference we'll calculate the fee for a fully verified + // sector and divide as required + let full_verified_fee = daily_proof_fee( + &rt.policy, + &new_circulating_supply, + &BigInt::from(h.sector_size as u64 * 10), + ); + + let sectors_after = snos.iter().map(|sno| h.get_sector(&rt, *sno)).collect::>(); + let mut total_fees = TokenAmount::zero(); + for (i, (before, after)) in sectors.iter().zip(§ors_after).enumerate() { + // Sectors with odd indices (1 and 3) are full of verified data, even indices (0 and 2) are not + let has_verified = i % 2 == 1; + + verify_weights( + &rt, + &h, + before.sector_number, + if has_verified { 0 } else { piece_size }, + if has_verified { piece_size } else { 0 }, + ); + + // Check daily fees - for unverified sectors, the full verified fee is divided by 10 + let expected_fee = full_verified_fee.div_floor(if has_verified { 1 } else { 10 }); + assert_eq!( + expected_fee, after.daily_fee, + "daily fees differ for sector {}", + before.sector_number + ); + + total_fees += &after.daily_fee; + } + + let (deadline_index, partition_index) = st.find_sector(rt.store(), snos[0]).unwrap(); + // check the deadline and partition state is correct for the replaced sector's fee + let (deadline, partition) = h.get_deadline_and_partition(&rt, deadline_index, partition_index); + + // deadline has the total fees for all sectors + assert_eq!(total_fees, deadline.daily_fee); + + // partition expiration queue has the total fees for all sectors as a deduction + let p_queue = h.collect_partition_expirations(&rt, &partition); + let entry = p_queue.get(&quantized_expiration).unwrap().clone(); + assert_eq!(total_fees, entry.fee_deduction); + h.check_state(&rt); } @@ -134,7 +332,7 @@ fn multiple_pieces_in_sector() { let cfg = ProveReplicaUpdatesConfig::default(); let (result, claims, notifications) = - h.prove_replica_updates2_batch(&rt, §or_updates, true, true, cfg).unwrap(); + h.prove_replica_updates3_batch(&rt, §or_updates, true, true, cfg).unwrap(); assert_update_result(&[ExitCode::OK, ExitCode::OK], &result); // Explicitly verify claims match what we expect. @@ -247,7 +445,7 @@ fn multiple_notifs_for_piece() { let cfg = ProveReplicaUpdatesConfig::default(); let (result, _, notifications) = - h.prove_replica_updates2_batch(&rt, §or_updates, true, true, cfg).unwrap(); + h.prove_replica_updates3_batch(&rt, §or_updates, true, true, cfg).unwrap(); assert_update_result(&[ExitCode::OK, ExitCode::OK], &result); // Explicitly verify notifications match what we expect. @@ -319,7 +517,7 @@ fn cant_update_nonempty_sector() { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, "cannot update sector with non-zero data", - h.prove_replica_updates2_batch(&rt, §or_updates, true, true, cfg), + h.prove_replica_updates3_batch(&rt, §or_updates, true, true, cfg), ); h.check_state(&rt); } @@ -341,7 +539,7 @@ fn invalid_update_dropped() { let cfg = ProveReplicaUpdatesConfig { validation_failure: vec![0], ..Default::default() }; let (result, claims, notifications) = - h.prove_replica_updates2_batch(&rt, §or_updates, false, false, cfg).unwrap(); + h.prove_replica_updates3_batch(&rt, §or_updates, false, false, cfg).unwrap(); assert_update_result(&[ExitCode::USR_ILLEGAL_ARGUMENT, ExitCode::OK], &result); // Sector 0: no change. @@ -369,7 +567,7 @@ fn invalid_proof_dropped() { let cfg = ProveReplicaUpdatesConfig { proof_failure: vec![0], ..Default::default() }; let (result, _, _) = - h.prove_replica_updates2_batch(&rt, §or_updates, false, false, cfg).unwrap(); + h.prove_replica_updates3_batch(&rt, §or_updates, false, false, cfg).unwrap(); assert_update_result(&[ExitCode::USR_ILLEGAL_ARGUMENT, ExitCode::OK], &result); verify_weights(&rt, &h, snos[0], 0, 0); @@ -391,7 +589,7 @@ fn invalid_claim_dropped() { let cfg = ProveReplicaUpdatesConfig { claim_failure: vec![0], ..Default::default() }; let (result, _, _) = - h.prove_replica_updates2_batch(&rt, §or_updates, false, false, cfg).unwrap(); + h.prove_replica_updates3_batch(&rt, §or_updates, false, false, cfg).unwrap(); assert_update_result(&[ExitCode::USR_ILLEGAL_ARGUMENT, ExitCode::OK], &result); verify_weights(&rt, &h, snos[0], 0, 0); @@ -417,7 +615,7 @@ fn aborted_notification_dropped() { ..Default::default() }; let (result, _, _) = - h.prove_replica_updates2_batch(&rt, §or_updates, false, false, cfg).unwrap(); + h.prove_replica_updates3_batch(&rt, §or_updates, false, false, cfg).unwrap(); // All sectors succeed anyway. assert_update_result(&vec![ExitCode::OK; sectors.len()], &result); @@ -443,7 +641,7 @@ fn rejected_notification_dropped() { let cfg = ProveReplicaUpdatesConfig { notification_rejected: true, ..Default::default() }; let (result, _, _) = - h.prove_replica_updates2_batch(&rt, §or_updates, false, false, cfg).unwrap(); + h.prove_replica_updates3_batch(&rt, §or_updates, false, false, cfg).unwrap(); // All sectors succeed anyway. assert_update_result(&vec![ExitCode::OK; sectors.len()], &result); @@ -467,7 +665,7 @@ fn update_to_empty() { let cfg = ProveReplicaUpdatesConfig::default(); let (result, _, _) = - h.prove_replica_updates2_batch(&rt, §or_updates, true, true, cfg).unwrap(); + h.prove_replica_updates3_batch(&rt, §or_updates, true, true, cfg).unwrap(); assert_update_result(&vec![ExitCode::OK; sectors.len()], &result); verify_weights(&rt, &h, snos[0], 0, 0); @@ -477,7 +675,7 @@ fn update_to_empty() { let cfg = ProveReplicaUpdatesConfig::default(); let (result, _, _) = - h.prove_replica_updates2_batch(&rt, §or_updates, true, true, cfg).unwrap(); + h.prove_replica_updates3_batch(&rt, §or_updates, true, true, cfg).unwrap(); assert_update_result(&vec![ExitCode::OK; sectors.len()], &result); // But not again now it's non-empty. @@ -485,7 +683,7 @@ fn update_to_empty() { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, "cannot update sector with non-zero data", - h.prove_replica_updates2_batch(&rt, §or_updates, true, true, cfg), + h.prove_replica_updates3_batch(&rt, §or_updates, true, true, cfg), ); h.check_state(&rt); diff --git a/actors/miner/tests/record_skipped_faults.rs b/actors/miner/tests/record_skipped_faults.rs index bc45481a3..0b0936892 100644 --- a/actors/miner/tests/record_skipped_faults.rs +++ b/actors/miner/tests/record_skipped_faults.rs @@ -1,9 +1,8 @@ -use fil_actor_miner::power_for_sectors; -use fil_actor_miner::select_sectors; use fil_actor_miner::testing::PartitionStateSummary; use fil_actor_miner::Partition; use fil_actor_miner::QuantSpec; use fil_actor_miner::SectorOnChainInfo; +use fil_actor_miner::{daily_fee_for_sectors, power_for_sectors, select_sectors}; use fil_actors_runtime::runtime::Policy; use fil_actors_runtime::ActorError; use fil_actors_runtime::MessageAccumulator; @@ -20,12 +19,12 @@ use crate::util::*; fn sectors() -> Vec { vec![ - test_sector(2, 1, 50, 60, 1000), - test_sector(3, 2, 51, 61, 1001), - test_sector(7, 3, 52, 62, 1002), - test_sector(8, 4, 53, 63, 1003), - test_sector(11, 5, 54, 64, 1004), - test_sector(13, 6, 55, 65, 1005), + test_sector(2, 1, 50, 60, 1000, 3000), + test_sector(3, 2, 51, 61, 1001, 4000), + test_sector(7, 3, 52, 62, 1002, 5000), + test_sector(8, 4, 53, 63, 1003, 6000), + test_sector(11, 5, 54, 64, 1004, 7000), + test_sector(13, 6, 55, 65, 1005, 8000), ] } @@ -37,9 +36,12 @@ fn setup() -> (MemoryBlockstore, Partition) { let store = MemoryBlockstore::default(); let mut partition = Partition::new(&store).unwrap(); - let power = partition.add_sectors(&store, true, §ors(), SECTOR_SIZE, QUANT_SPEC).unwrap(); + let (power, daily_fee) = + partition.add_sectors(&store, true, §ors(), SECTOR_SIZE, QUANT_SPEC).unwrap(); let expected_power = power_for_sectors(SECTOR_SIZE, §ors()); + let expected_daily_fee = daily_fee_for_sectors(§ors()); assert_eq!(expected_power, power); + assert_eq!(expected_daily_fee, daily_fee); (store, partition) } diff --git a/actors/miner/tests/repay_debt_test.rs b/actors/miner/tests/repay_debt_test.rs index a1b60c791..31a0b6238 100644 --- a/actors/miner/tests/repay_debt_test.rs +++ b/actors/miner/tests/repay_debt_test.rs @@ -16,8 +16,8 @@ fn repay_debt_in_priority_order() { let (penalty_from_vesting, penalty_from_balance) = h.st.repay_partial_debt_in_priority_order(&h.store, 0, ¤t_balance).unwrap(); - assert_eq!(penalty_from_vesting, TokenAmount::zero()); - assert_eq!(penalty_from_balance, current_balance); + assert_eq!(penalty_from_vesting, current_balance); + assert_eq!(penalty_from_balance, TokenAmount::zero()); let expected_debt = -(current_balance - fee); assert_eq!(expected_debt, h.st.fee_debt); diff --git a/actors/miner/tests/sectors.rs b/actors/miner/tests/sectors.rs index 272b6ad99..34c6b5c14 100644 --- a/actors/miner/tests/sectors.rs +++ b/actors/miner/tests/sectors.rs @@ -35,13 +35,13 @@ fn loads_sectors() { let sectors = setup_sectors(&store); let mut bf = bf_from_vec(vec![0, 5]); - let vec_sectors = sectors.load_sector(&bf).unwrap(); + let vec_sectors = sectors.load_sectors(&bf).unwrap(); assert_eq!(vec_sectors.len(), 2); assert_eq!(make_sector(0), vec_sectors[0]); assert_eq!(make_sector(5), vec_sectors[1]); bf = bf_from_vec(vec![0, 3]); - let res = sectors.load_sector(&bf); + let res = sectors.load_sectors(&bf); assert!(res.is_err()); } @@ -60,7 +60,7 @@ fn stores_sectors() { sectors.store(vec![s3.clone(), s1.clone()]).unwrap(); let bf = bf_from_vec(vec![0, 1, 3, 5]); - let vec_sectors = sectors.load_sector(&bf).unwrap(); + let vec_sectors = sectors.load_sectors(&bf).unwrap(); assert_eq!(vec_sectors.len(), 4); assert_eq!(&s0, &vec_sectors[0]); assert_eq!(&s1, &vec_sectors[1]); @@ -75,7 +75,7 @@ fn loads_and_stores_no_sectors() { let mut sectors = setup_sectors(&store); let bf = bf_from_vec(vec![]); - let vec_sectors = sectors.load_sector(&bf).unwrap(); + let vec_sectors = sectors.load_sectors(&bf).unwrap(); assert_eq!(vec_sectors.len(), 0); sectors.store(vec![]).unwrap(); } @@ -148,3 +148,16 @@ fn no_non_faulty_sectors() { let vec_sectors = sectors.load_for_proof(&bf_from_vec(vec![1]), &bf_from_vec(vec![1])).unwrap(); assert_eq!(vec_sectors.len(), 0); } + +#[test] +fn delete_nonexistent_value_returns_an_error() { + let store = MemoryBlockstore::default(); + let mut sectors = setup_sectors(&store); + let mut bf = BitField::new(); + + bf.set(100); // doesn't exist + assert!(sectors.delete_sectors(&bf).is_err()); + + bf.set(0); // does exist but 100 still doesn't + assert!(sectors.delete_sectors(&bf).is_err()); +} diff --git a/actors/miner/tests/sectors_stores_test.rs b/actors/miner/tests/sectors_stores_test.rs index a1784407e..28a68a6c9 100644 --- a/actors/miner/tests/sectors_stores_test.rs +++ b/actors/miner/tests/sectors_stores_test.rs @@ -1,7 +1,6 @@ use cid::Cid; use fil_actor_miner::SectorOnChainInfo; use fil_actors_runtime::test_utils::*; -use fvm_ipld_bitfield::BitField; use fvm_shared::{ bigint::BigInt, clock::ChainEpoch, @@ -13,7 +12,7 @@ mod util; use state_harness::*; #[test] -fn put_get_and_delete() { +fn put_and_get() { let mut h = StateHarness::new(0); let sector_no = 1u64; @@ -39,20 +38,6 @@ fn put_get_and_delete() { assert!(h.has_sector_number(sector_no)); let out = h.get_sector(sector_no); assert_eq!(sector_info_2, out); - - h.delete_sectors(vec![sector_no]); - assert!(!h.has_sector_number(sector_no)); -} - -#[test] -fn delete_nonexistent_value_returns_an_error() { - let mut h = StateHarness::new(ChainEpoch::from(0)); - - let sector_no = 1u64; - let mut bf = BitField::new(); - bf.set(sector_no); - - assert!(h.st.delete_sectors(&h.store, &bf).is_err()); } #[test] @@ -64,11 +49,11 @@ fn get_nonexistent_value_returns_false() { } #[test] -fn iterate_and_delete_multiple_sectors() { +fn iterate_multiple_sectors() { let mut h = StateHarness::new(ChainEpoch::from(0)); // set of sectors, the larger numbers here are not significant - let sector_nos = vec![100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]; + let sector_nos = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]; // put all the sectors in the store for (i, s) in sector_nos.iter().enumerate() { @@ -90,10 +75,6 @@ fn iterate_and_delete_multiple_sectors() { // ensure we iterated over the expected number of sectors assert_eq!(sector_nos.len(), sector_no_idx); - h.delete_sectors(sector_nos.clone()); - for s in sector_nos { - assert!(!h.has_sector_number(s)); - } } // returns a unique SectorOnChainInfo with each invocation with SectorNumber set to `sectorNo`. diff --git a/actors/miner/tests/state_harness.rs b/actors/miner/tests/state_harness.rs index dc9cb1f05..bdf14990d 100644 --- a/actors/miner/tests/state_harness.rs +++ b/actors/miner/tests/state_harness.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use fil_actor_miner::{ BitFieldQueue, CollisionPolicy, MinerInfo, QuantSpec, SectorOnChainInfo, - SectorPreCommitOnChainInfo, State, VestSpec, VestingFunds, + SectorPreCommitOnChainInfo, State, VestSpec, }; use fil_actors_runtime::test_blockstores::MemoryBlockstore; use fil_actors_runtime::{runtime::Policy, ActorError}; @@ -120,12 +120,12 @@ impl StateHarness { } #[allow(dead_code)] - pub fn unlock_unvested_funds( + pub fn unlock_vested_and_unvested_funds( &mut self, current_epoch: ChainEpoch, target: &TokenAmount, - ) -> anyhow::Result { - self.st.unlock_unvested_funds(&self.store, current_epoch, target) + ) -> anyhow::Result<(TokenAmount, TokenAmount)> { + self.st.unlock_vested_and_unvested_funds(&self.store, current_epoch, target) } pub fn has_sector_number(&self, sector_no: SectorNumber) -> bool { @@ -140,19 +140,9 @@ impl StateHarness { self.st.get_sector(&self.store, sector_number).unwrap().unwrap() } - // makes a bit field from the passed sector numbers - pub fn delete_sectors(&mut self, sector_numbers: Vec) { - let mut bf = BitField::new(); - for b in sector_numbers.iter() { - bf.set(*b); - } - self.st.delete_sectors(&self.store, &bf).unwrap(); - } - #[allow(dead_code)] pub fn vesting_funds_store_empty(&self) -> bool { - let vesting = self.store.get_cbor::(&self.st.vesting_funds).unwrap().unwrap(); - vesting.funds.is_empty() + self.st.vesting_funds.load(&self.store).unwrap().is_empty() } pub fn assign_sectors_to_deadlines( diff --git a/actors/miner/tests/types_test.rs b/actors/miner/tests/types_test.rs index 4d7af2b2e..ae4db2f9b 100644 --- a/actors/miner/tests/types_test.rs +++ b/actors/miner/tests/types_test.rs @@ -1,13 +1,23 @@ // Tests to match with Go github.com/filecoin-project/go-state-types/builtin/*/miner mod serialization { + use std::iter; + use std::ops::Range; use std::str::FromStr; use cid::Cid; use hex_literal::hex; - use fil_actor_miner::{ProveCommitSectorsNIParams, SectorNIActivationInfo}; + use fil_actor_miner::{ + Deadline, ExpirationSet, PowerPair, ProveCommitSectorsNIParams, SectorNIActivationInfo, + SectorOnChainInfo, SectorOnChainInfoFlags, + }; + use fvm_ipld_bitfield::iter::Ranges; + use fvm_ipld_bitfield::BitField; use fvm_ipld_encoding::ipld_block::IpldBlock; + use fvm_shared::bigint::BigInt; + use fvm_shared::econ::TokenAmount; use fvm_shared::sector::{RegisteredAggregateProof, RegisteredSealProof}; + use num_traits::Zero; #[test] fn prove_commit_sectors_ni_params() { @@ -74,11 +84,210 @@ mod serialization { ), ]; - for (params, expected_hex) in test_cases { + for (params, expected) in test_cases { let encoded = IpldBlock::serialize_cbor(¶ms).unwrap().unwrap(); - assert_eq!(encoded.data, expected_hex); + assert_eq!(encoded.data, expected); let decoded: ProveCommitSectorsNIParams = IpldBlock::deserialize(&encoded).unwrap(); assert_eq!(params, decoded); } } + + #[test] + fn sector_on_chain_info() { + let test_cases = vec![ + ( + SectorOnChainInfo { + ..Default::default() + }, + // [0,-1,{"/":"baeaaaaa"},[],0,0,[],[],[],null,null,0,null,null,0,[]] + &hex!("900020d82a450001000000800000404040f6f600f6f60040")[..], + // same on write as read + &hex!("900020d82a450001000000800000404040f6f600f6f60040")[..], + ), + ( + SectorOnChainInfo { + sector_number: 1, + seal_proof: RegisteredSealProof::StackedDRG32GiBV1P1, + sealed_cid: Cid::from_str("bagboea4seaaqa").unwrap(), + deprecated_deal_ids: vec![], + activation: 2, + expiration: 3, + deal_weight: 4.into(), + verified_deal_weight: 5.into(), + initial_pledge: TokenAmount::from_whole(6), + expected_day_reward: None, + expected_storage_pledge: None, + power_base_epoch: 9, + replaced_day_reward: None, + sector_key_cid: None, + flags: Default::default(), + daily_fee: TokenAmount::from_whole(11), + }, + // '[1,8,{"/":"bagboea4seaaqa"},[],2,3,[AAQ],[AAU],[AFNESDXsWAAA],null,null,9,null,null,0,[AJin2bgxTAAA]]' + &hex!("900108d82a49000182e20392200100800203420004420005490053444835ec580000f6f609f6f600490098a7d9b8314c0000"), + // same on write as read + &hex!("900108d82a49000182e20392200100800203420004420005490053444835ec580000f6f609f6f600490098a7d9b8314c0000"), + ), + ( + SectorOnChainInfo { + sector_number: 1, + seal_proof: RegisteredSealProof::StackedDRG32GiBV1P1, + sealed_cid: Cid::from_str("bagboea4seaaqa").unwrap(), + deprecated_deal_ids: vec![], + activation: 2, + expiration: 3, + deal_weight: 4.into(), + verified_deal_weight: 5.into(), + initial_pledge: TokenAmount::from_whole(6), + expected_day_reward: None, + expected_storage_pledge: None, + power_base_epoch: 9, + replaced_day_reward: None, + sector_key_cid: Some(Cid::from_str("baga6ea4seaaqc").unwrap()), + flags: SectorOnChainInfoFlags::SIMPLE_QA_POWER, + daily_fee: TokenAmount::from_whole(11), + }, + // [1,8,{"/":"bagboea4seaaqa"},[],2,3,[AAQ],[AAU],[AFNESDXsWAAA],null,null,9,null,{"/":"baga6ea4seaaqc"},1,[AJin2bgxTAAA]] + &hex!("900108d82a49000182e20392200100800203420004420005490053444835ec580000f6f609f6d82a49000181e2039220010101490098a7d9b8314c0000"), + // same on write as read + &hex!("900108d82a49000182e20392200100800203420004420005490053444835ec580000f6f609f6d82a49000181e2039220010101490098a7d9b8314c0000"), + ), + ( + // old format stored on chain but materialised as the new format with a default value at the end + SectorOnChainInfo { + sector_number: 1, + seal_proof: RegisteredSealProof::StackedDRG64GiBV1P1, + sealed_cid: Cid::from_str("bagboea4seaaqa").unwrap(), + deprecated_deal_ids: vec![], + activation: 2, + expiration: 3, + deal_weight: 4.into(), + verified_deal_weight: 5.into(), + initial_pledge: TokenAmount::from_whole(6), + expected_day_reward: None, + expected_storage_pledge: None, + power_base_epoch: 9, + replaced_day_reward: None, + sector_key_cid: None, + flags: SectorOnChainInfoFlags::SIMPLE_QA_POWER, + daily_fee: TokenAmount::zero(), // default, not present in the binary + }, + // [1,9,{"/":"bagboea4seaaqa"},[],2,3,[AAQ],[AAU],[AFNESDXsWAAA],null,null,9,null,null,1] + &hex!("8f0109d82a49000182e20392200100800203420004420005490053444835ec580000f6f609f6f601"), + // extra field at the end on write, zero BigInt (bytes) for daily_fee + // [1,9,{"/":"bagboea4seaaqa"},[],2,3,[AAQ],[AAU],[AFNESDXsWAAA],null,null,9,null,null,1,[]] + &hex!("900109d82a49000182e20392200100800203420004420005490053444835ec580000f6f609f6f60140"), + ), + ]; + + for (idx, (params, read_bytes, write_bytes)) in test_cases.into_iter().enumerate() { + let encoded = IpldBlock::serialize_cbor(¶ms).unwrap().unwrap(); + assert_eq!(encoded.data, write_bytes, "Test case {} encoding failed", idx); + + let decoded: SectorOnChainInfo = + IpldBlock::deserialize(&IpldBlock { codec: 0x71, data: read_bytes.to_vec() }) + .unwrap(); + assert_eq!(params, decoded, "Test case {} decoding failed", idx); + } + } + + #[test] + fn expiration_set() { + // ExpirationSet's fields are all bytes or byte tuples + let test_cases = vec![ + ( + ExpirationSet { ..Default::default() }, + // [[],[],[],[[],[]],[[],[]],[]] + &hex!("8640404082404082404040")[..], + // same on write as read + &hex!("8640404082404082404040")[..], + ), + ( + ExpirationSet { + on_time_sectors: BitField::from_ranges(Ranges::new( + iter::once(0..1).collect::>>(), + )), + early_sectors: BitField::from_ranges(Ranges::new( + iter::once(1..2).collect::>>(), + )), + on_time_pledge: TokenAmount::from_whole(2), + active_power: PowerPair::new(BigInt::from(3), BigInt::from(4)), + faulty_power: PowerPair::new(BigInt::from(5), BigInt::from(6)), + fee_deduction: TokenAmount::from_whole(7), + }, + // [[DA],[GA],[ABvBbWdOyAAA],[[AAM],[AAQ]],[[AAU],[AAY]],[AGEk/umTvAAA]] + &hex!("86410c411849001bc16d674ec80000824200034200048242000542000649006124fee993bc0000"), + // same on write as read + &hex!("86410c411849001bc16d674ec80000824200034200048242000542000649006124fee993bc0000"), + ), + ( + ExpirationSet { + on_time_sectors: BitField::from_ranges(Ranges::new( + iter::once(0..1).collect::>>(), + )), + early_sectors: BitField::from_ranges(Ranges::new( + iter::once(1..2).collect::>>(), + )), + on_time_pledge: TokenAmount::from_whole(2), + active_power: PowerPair::new(BigInt::from(3), BigInt::from(4)), + faulty_power: PowerPair::new(BigInt::from(5), BigInt::from(6)), + fee_deduction: TokenAmount::zero(), + }, + // [[DA],[GA],[ABvBbWdOyAAA],[[AAM],[AAQ]],[[AAU],[AAY]]] + &hex!("85410c411849001bc16d674ec800008242000342000482420005420006"), + // [[DA],[GA],[ABvBbWdOyAAA],[[AAM],[AAQ]],[[AAU],[AAY]],[]] + &hex!("86410c411849001bc16d674ec80000824200034200048242000542000640"), + ), + ]; + + for (idx, (params, read_bytes, write_bytes)) in test_cases.into_iter().enumerate() { + let encoded = IpldBlock::serialize_cbor(¶ms).unwrap().unwrap(); + assert_eq!(encoded.data, write_bytes, "Test case {} encoding failed", idx); + + let decoded: ExpirationSet = + IpldBlock::deserialize(&IpldBlock { codec: 0x71, data: read_bytes.to_vec() }) + .unwrap(); + assert_eq!(params, decoded, "Test case {} decoding failed", idx); + } + } + + #[test] + fn deadline() { + let test_cases = vec![( + Deadline { ..Default::default() }, + // [baeaaaaa,baeaaaaa,[],[],0,0,[[],[]],baeaaaaa,baeaaaaa,baeaaaaa,baeaaaaa,[[],[]],[]] + &hex!("8dd82a450001000000d82a45000100000040400000824040d82a450001000000d82a450001000000d82a450001000000d82a45000100000082404040")[..], + ), + ( + Deadline{ + partitions: Cid::from_str("bagboea4seaaqa").unwrap(), + expirations_epochs: Cid::from_str("bagboea4seaaqc").unwrap(), + partitions_posted: BitField::from_ranges(Ranges::new( + iter::once(0..1).collect::>>(), + )), + early_terminations: BitField::from_ranges(Ranges::new( + iter::once(1..2).collect::>>(), + )), + live_sectors: 2, + total_sectors: 3, + faulty_power: PowerPair::new(BigInt::from(4), BigInt::from(5)), + optimistic_post_submissions: Cid::from_str("bagboea4seaaqe").unwrap(), + sectors_snapshot: Cid::from_str("bagboea4seaaqg").unwrap(), + partitions_snapshot: Cid::from_str("bagboea4seaaqi").unwrap(), + optimistic_post_submissions_snapshot: Cid::from_str("bagboea4seaaqk").unwrap(), + live_power: PowerPair::new(BigInt::from(6), BigInt::from(7)), + daily_fee: TokenAmount::from_whole(8), + }, + // [bagboea4seaaqa,bagboea4seaaqc,[DA],[GA],2,3,[[AAQ],[AAU]],bagboea4seaaqe,bagboea4seaaqg,bagboea4seaaqi,bagboea4seaaqk,[[AAY],[AAc]],[AG8FtZ07IAAA]] + &hex!("8dd82a49000182e20392200100d82a49000182e20392200101410c4118020382420004420005d82a49000182e20392200102d82a49000182e20392200103d82a49000182e20392200104d82a49000182e203922001058242000642000749006f05b59d3b200000"), + ), + ]; + + for (params, expected) in test_cases { + let encoded = IpldBlock::serialize_cbor(¶ms).unwrap().unwrap(); + assert_eq!(encoded.data, expected); + let decoded: Deadline = IpldBlock::deserialize(&encoded).unwrap(); + assert_eq!(params, decoded); + } + } } diff --git a/actors/miner/tests/util.rs b/actors/miner/tests/util.rs index e9519cd7a..ea6179cb3 100644 --- a/actors/miner/tests/util.rs +++ b/actors/miner/tests/util.rs @@ -15,7 +15,7 @@ use fvm_ipld_blockstore::Blockstore; use fvm_ipld_encoding::de::Deserialize; use fvm_ipld_encoding::ipld_block::IpldBlock; use fvm_ipld_encoding::ser::Serialize; -use fvm_ipld_encoding::{BytesDe, CborStore, RawBytes}; +use fvm_ipld_encoding::{BytesDe, RawBytes}; use fvm_shared::address::Address; use fvm_shared::bigint::BigInt; use fvm_shared::bigint::Zero; @@ -47,8 +47,7 @@ use fil_actor_market::{ VerifyDealsForActivationParams, VerifyDealsForActivationReturn, NO_ALLOCATION_ID, }; use fil_actor_miner::{ - aggregate_pre_commit_network_fee, aggregate_prove_commit_network_fee, consensus_fault_penalty, - ext, + consensus_fault_penalty, ext, ext::market::ON_MINER_SECTORS_TERMINATE_METHOD, ext::power::UPDATE_CLAIMED_POWER_METHOD, ext::verifreg::{ @@ -76,10 +75,9 @@ use fil_actor_miner::{ SectorActivationManifest, SectorChanges, SectorContentChangedParams, SectorContentChangedReturn, SectorOnChainInfo, SectorPreCommitInfo, SectorPreCommitOnChainInfo, SectorReturn, SectorUpdateManifest, Sectors, State, SubmitWindowedPoStParams, - TerminateSectorsParams, TerminationDeclaration, VerifiedAllocationKey, VestingFunds, - WindowedPoSt, WithdrawBalanceParams, WithdrawBalanceReturn, CRON_EVENT_PROVING_DEADLINE, - NI_AGGREGATE_FEE_BASE_SECTOR_COUNT, NO_QUANTIZATION, REWARD_VESTING_SPEC, SECTORS_AMT_BITWIDTH, - SECTOR_CONTENT_CHANGED, + TerminateSectorsParams, TerminationDeclaration, VerifiedAllocationKey, WindowedPoSt, + WithdrawBalanceParams, WithdrawBalanceReturn, CRON_EVENT_PROVING_DEADLINE, NO_QUANTIZATION, + SECTORS_AMT_BITWIDTH, SECTOR_CONTENT_CHANGED, }; use fil_actor_miner::{ raw_power_for_sector, ProveCommitSectorsNIParams, ProveCommitSectorsNIReturn, @@ -158,8 +156,6 @@ pub struct ActorHarness { pub epoch_reward_smooth: FilterEstimate, pub epoch_qa_power_smooth: FilterEstimate, - pub base_fee: TokenAmount, - pub options: HarnessOptions, } @@ -209,8 +205,6 @@ impl ActorHarness { epoch_reward_smooth: FilterEstimate::new(rwd.atto().clone(), BigInt::from(0)), epoch_qa_power_smooth: FilterEstimate::new(pwr, BigInt::from(0)), - base_fee: TokenAmount::zero(), - options, } } @@ -278,6 +272,8 @@ impl ActorHarness { IpldBlock::serialize_cbor(&self.worker_key).unwrap(), ExitCode::OK, ); + // set circulating supply non-zero so we get non-zero fees + rt.set_circulating_supply(TokenAmount::from_whole(500_000)); let result = rt .call::(Method::Constructor as u64, IpldBlock::serialize_cbor(¶ms).unwrap()) @@ -578,7 +574,6 @@ impl ActorHarness { rt: &MockRuntime, params: PreCommitSectorBatchParams, conf: &PreCommitBatchConfig, - base_fee: &TokenAmount, ) -> Result, ActorError> { let v2: Vec<_> = params .sectors @@ -595,7 +590,7 @@ impl ActorHarness { }) .collect(); - return self.pre_commit_sector_batch_v2(rt, &v2, conf.first_for_miner, base_fee); + return self.pre_commit_sector_batch_v2(rt, &v2, conf.first_for_miner); } pub fn pre_commit_sector_batch_v2( @@ -603,7 +598,6 @@ impl ActorHarness { rt: &MockRuntime, sectors: &[SectorPreCommitInfo], first_for_miner: bool, - base_fee: &TokenAmount, ) -> Result, ActorError> { rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, self.worker); rt.expect_validate_caller_addr(self.caller_addrs()); @@ -640,21 +634,8 @@ impl ActorHarness { let state = self.get_state(rt); - let mut expected_network_fee = TokenAmount::zero(); - if sectors.len() > 1 { - expected_network_fee = aggregate_pre_commit_network_fee(sectors.len(), base_fee); - } - // burn networkFee - if state.fee_debt.is_positive() || expected_network_fee.is_positive() { - let expected_burn = expected_network_fee + state.fee_debt; - rt.expect_send_simple( - BURNT_FUNDS_ACTOR_ADDR, - METHOD_SEND, - None, - expected_burn, - None, - ExitCode::OK, - ); + if state.fee_debt.is_positive() { + expect_burn(rt, state.fee_debt); } if first_for_miner { @@ -691,9 +672,8 @@ impl ActorHarness { rt: &MockRuntime, params: PreCommitSectorBatchParams, conf: &PreCommitBatchConfig, - base_fee: &TokenAmount, ) -> Vec { - let result = self.pre_commit_sector_batch(rt, params.clone(), conf, base_fee).unwrap(); + let result = self.pre_commit_sector_batch(rt, params.clone(), conf).unwrap(); expect_empty(result); rt.verify(); @@ -712,7 +692,6 @@ impl ActorHarness { rt, PreCommitSectorBatchParams { sectors: vec![params] }, &PreCommitBatchConfig { sector_unsealed_cid: vec![conf.0], first_for_miner: first }, - &self.base_fee, ); result } @@ -723,7 +702,7 @@ impl ActorHarness { sectors: Vec, first: bool, ) -> Vec { - let result = self.pre_commit_sector_batch_v2(rt, §ors, first, &self.base_fee).unwrap(); + let result = self.pre_commit_sector_batch_v2(rt, §ors, first).unwrap(); expect_empty(result); rt.verify(); @@ -742,7 +721,6 @@ impl ActorHarness { rt, PreCommitSectorBatchParams { sectors: vec![params] }, &PreCommitBatchConfig { sector_unsealed_cid: vec![conf.0], first_for_miner: first }, - &self.base_fee, ); rt.verify(); result[0].clone() @@ -911,21 +889,6 @@ impl ActorHarness { self.expect_query_network_info(rt); - if params.sectors.len() - fail_count > NI_AGGREGATE_FEE_BASE_SECTOR_COUNT { - let aggregate_fee = aggregate_prove_commit_network_fee( - params.sectors.len() - fail_count - NI_AGGREGATE_FEE_BASE_SECTOR_COUNT, - &rt.base_fee.borrow(), - ); - rt.expect_send_simple( - BURNT_FUNDS_ACTOR_ADDR, - METHOD_SEND, - None, - aggregate_fee, - None, - ExitCode::OK, - ); - } - let qa_sector_power = raw_power_for_sector(self.sector_size); let sector_pledge = self.initial_pledge_for_power(rt, &qa_sector_power); let total_pledge = BigInt::from(expected_success_count) * sector_pledge; @@ -993,7 +956,6 @@ impl ActorHarness { config: ProveCommitConfig, precommits: Vec, params: ProveCommitAggregateParams, - base_fee: &TokenAmount, ) -> Result<(), ActorError> { let comm_ds: Vec<_> = precommits .iter() @@ -1031,18 +993,6 @@ impl ActorHarness { expect_sector_event(rt, "sector-activated", &num, unsealed_cids[i], &piece_info); } - // burn network fee - let expected_fee = aggregate_prove_commit_network_fee(precommits.len(), base_fee); - assert!(expected_fee.is_positive()); - rt.expect_send_simple( - BURNT_FUNDS_ACTOR_ADDR, - METHOD_SEND, - None, - expected_fee, - None, - ExitCode::OK, - ); - rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, self.worker); let addrs = self.caller_addrs().clone(); rt.expect_validate_caller_addr(addrs); @@ -1417,21 +1367,6 @@ impl ActorHarness { // Expect pledge & power updates. self.expect_query_network_info(rt); expect_update_pledge(rt, &expected_pledge); - // Pay aggregation fee for successful proofs. - if aggregate { - let proven_count = - sector_activations.len() - (cfg.validation_failure.len() + cfg.proof_failure.len()); - let expected_fee = aggregate_prove_commit_network_fee(proven_count, &self.base_fee); - assert!(expected_fee.is_positive()); - rt.expect_send_simple( - BURNT_FUNDS_ACTOR_ADDR, - METHOD_SEND, - None, - expected_fee, - None, - ExitCode::OK, - ); - } // Expect SectorContentChanged notification to market. let sector_notification_resps: Vec = expected_sector_notifications @@ -1492,11 +1427,11 @@ impl ActorHarness { Ok((result, sector_allocation_claims, expected_sector_notifications)) } - // Invokes prove_replica_updates2 with a batch of sector updates, and + // Invokes prove_replica_updates3 with a batch of sector updates, and // sets and checks mock expectations for the expected interactions. // Returns the result of the invocation along with the expected sector claims and notifications // (which match the actual, if mock verification succeeded). - pub fn prove_replica_updates2_batch( + pub fn prove_replica_updates3_batch( &self, rt: &MockRuntime, sector_updates: &[SectorUpdateManifest], @@ -1534,14 +1469,15 @@ impl ActorHarness { let mut unsealed_cids: HashMap> = HashMap::new(); for (i, sup) in sector_updates.iter().enumerate() { + if cfg.validation_failure.contains(&i) { + continue; + } + let sector = self.get_sector(rt, sup.sector); let unsealed_cid = expect_compute_unsealed_cid_from_pieces(rt, self.seal_proof_type, &sup.pieces); unsealed_cids.insert(sector.sector_number, unsealed_cid.0); - if cfg.validation_failure.contains(&i) { - continue; - } let proof_ok = !cfg.proof_failure.contains(&i); rt.expect_replica_verify( ReplicaUpdateInfo { @@ -1719,6 +1655,7 @@ impl ActorHarness { dlinfo } + // TODO: remove this, duplicate of get_deadline_info pub fn deadline(&self, rt: &MockRuntime) -> DeadlineInfo { let state = self.get_state(rt); state.recorded_deadline_info(&rt.policy, *rt.epoch.borrow()) @@ -1734,6 +1671,7 @@ impl ActorHarness { if state.deadline_cron_active { rt.epoch.replace(deadline.last()); + cfg.pledge_delta -= immediately_vesting_funds(rt, &state); cfg.expected_enrollment = deadline.last() + rt.policy.wpost_challenge_window; self.on_deadline_cron(rt, cfg); } @@ -1744,47 +1682,10 @@ impl ActorHarness { } pub fn on_deadline_cron(&self, rt: &MockRuntime, cfg: CronConfig) { - let state = self.get_state(rt); rt.expect_validate_caller_addr(vec![STORAGE_POWER_ACTOR_ADDR]); - - // preamble - let mut power_delta = PowerPair::zero(); - power_delta += &cfg.detected_faults_power_delta.unwrap_or_else(PowerPair::zero); - power_delta += &cfg.expired_sectors_power_delta.unwrap_or_else(PowerPair::zero); - expect_update_power(rt, power_delta); - - let mut penalty_total = TokenAmount::zero(); - let mut pledge_delta = TokenAmount::zero(); - - penalty_total += cfg.continued_faults_penalty.clone(); - penalty_total += cfg.repaid_fee_debt.clone(); - penalty_total += cfg.expired_precommit_penalty.clone(); - - if !penalty_total.is_zero() { - rt.expect_send_simple( - BURNT_FUNDS_ACTOR_ADDR, - METHOD_SEND, - None, - penalty_total.clone(), - None, - ExitCode::OK, - ); - - let mut penalty_from_vesting = penalty_total.clone(); - // Outstanding fee debt is only repaid from unlocked balance, not vesting funds. - penalty_from_vesting -= cfg.repaid_fee_debt.clone(); - // Precommit deposit burns are repaid from PCD account - penalty_from_vesting -= cfg.expired_precommit_penalty.clone(); - // New penalties are paid first from vesting funds but, if exhausted, overflow to unlocked balance. - penalty_from_vesting -= cfg.penalty_from_unlocked.clone(); - - pledge_delta -= penalty_from_vesting; - } - - pledge_delta += cfg.expired_sectors_pledge_delta; - pledge_delta -= immediately_vesting_funds(rt, &state); - - expect_update_pledge(rt, &pledge_delta); + expect_update_power(rt, cfg.power_delta.unwrap_or_else(PowerPair::zero)); + expect_burn(rt, cfg.burnt_funds); + expect_update_pledge(rt, &cfg.pledge_delta); // Re-enrollment for next period. if !cfg.no_enrollment { @@ -2019,15 +1920,7 @@ impl ActorHarness { } if dispute_result.expected_penalty.is_some() { - let expected_penalty = dispute_result.expected_penalty.unwrap(); - rt.expect_send_simple( - BURNT_FUNDS_ACTOR_ADDR, - METHOD_SEND, - None, - expected_penalty, - None, - ExitCode::OK, - ); + expect_burn(rt, dispute_result.expected_penalty.unwrap()); } if dispute_result.expected_pledge_delta.is_some() { @@ -2094,16 +1987,8 @@ impl ActorHarness { rt.set_caller(*REWARD_ACTOR_CODE_ID, REWARD_ACTOR_ADDR); rt.expect_validate_caller_addr(vec![REWARD_ACTOR_ADDR]); expect_update_pledge(rt, &pledge_delta); - if penalty.is_positive() { - rt.expect_send_simple( - BURNT_FUNDS_ACTOR_ADDR, - METHOD_SEND, - None, - penalty.clone(), - None, - ExitCode::OK, - ); + expect_burn(rt, penalty.clone()); } let params = ApplyRewardParams { reward: amt, penalty: penalty }; @@ -2142,12 +2027,14 @@ impl ActorHarness { let mut dlinfo = self.current_deadline(rt); while deadlines.len() > 0 { + let mut daily_fee = TokenAmount::zero(); match deadlines.get(&dlinfo.index) { None => {} Some(dl_sectors) => { let mut sector_nos = BitField::new(); for sector in dl_sectors { sector_nos.set(sector.sector_number); + daily_fee += §or.daily_fee; } let dl_arr = state.load_deadlines(&rt.store).unwrap(); @@ -2218,8 +2105,15 @@ impl ActorHarness { } } - self.advance_deadline(rt, CronConfig::empty()); - dlinfo = self.current_deadline(rt); + let state = self.get_state(&rt); + let unvested = unvested_vesting_funds(rt, &state); + let available_to_burn = rt.get_balance() - &state.initial_pledge; + let burnt_funds = daily_fee.clone().clamp(TokenAmount::zero(), available_to_burn); + // advance_deadline() will update our pledge_delta with immediately_vesting + // so we don't need to do it here + let pledge_delta = -std::cmp::min(unvested, daily_fee); + let cfg = CronConfig { burnt_funds, pledge_delta, ..Default::default() }; + dlinfo = self.advance_deadline(rt, cfg); } } @@ -2257,14 +2151,7 @@ impl ActorHarness { rt.expect_validate_caller_addr(self.caller_addrs()); if expected_debt_repaid.is_positive() { - rt.expect_send_simple( - BURNT_FUNDS_ACTOR_ADDR, - METHOD_SEND, - None, - expected_debt_repaid, - None, - ExitCode::OK, - ); + expect_burn(rt, expected_debt_repaid.clone()); } // Calculate params from faulted sector infos @@ -2394,15 +2281,7 @@ impl ActorHarness { rt.expect_send_simple(from, METHOD_SEND, None, reward_total.clone(), None, ExitCode::OK); // pay fault fee - let to_burn = &penalty_total - &reward_total; - rt.expect_send_simple( - BURNT_FUNDS_ACTOR_ADDR, - METHOD_SEND, - None, - to_burn, - None, - ExitCode::OK, - ); + expect_burn(rt, &penalty_total - &reward_total); let result = rt.call::( Method::ReportConsensusFault as u64, @@ -2471,18 +2350,12 @@ impl ActorHarness { } sector_infos.push(self.get_sector(rt, sector)); } + self.expect_query_network_info(rt); let mut pledge_delta = TokenAmount::zero(); if expected_fee.is_positive() { - rt.expect_send_simple( - BURNT_FUNDS_ACTOR_ADDR, - METHOD_SEND, - None, - expected_fee.clone(), - None, - ExitCode::OK, - ); + expect_burn(rt, expected_fee.clone()); pledge_delta = expected_fee.neg(); } @@ -2593,14 +2466,7 @@ impl ActorHarness { let total_repaid = expected_repaid_from_vest + expected_repaid_from_balance; if total_repaid.is_positive() { - rt.expect_send_simple( - BURNT_FUNDS_ACTOR_ADDR, - METHOD_SEND, - None, - total_repaid.clone(), - None, - ExitCode::OK, - ); + expect_burn(rt, total_repaid.clone()); } let result = rt.call::(Method::RepayDebt as u64, None)?; expect_empty(result); @@ -2631,15 +2497,9 @@ impl ActorHarness { } if expected_debt_repaid.is_positive() { - rt.expect_send_simple( - BURNT_FUNDS_ACTOR_ADDR, - METHOD_SEND, - None, - expected_debt_repaid.clone(), - None, - ExitCode::OK, - ); + expect_burn(rt, expected_debt_repaid.clone()); } + let ret = rt .call::( Method::WithdrawBalance as u64, @@ -3167,21 +3027,13 @@ pub struct ProveCommitSectors3Config { pub notification_rejected: bool, // Whether to reject the notification } -#[derive(Default)] +#[derive(Default, Debug)] pub struct CronConfig { - pub no_enrollment: bool, - // true if expect not to continue enrollment false otherwise + pub no_enrollment: bool, // true if expect not to continue enrollment false otherwise pub expected_enrollment: ChainEpoch, - pub detected_faults_power_delta: Option, - pub expired_sectors_power_delta: Option, - pub expired_sectors_pledge_delta: TokenAmount, - pub continued_faults_penalty: TokenAmount, - // Expected amount burnt to pay continued fault penalties. - pub expired_precommit_penalty: TokenAmount, - // Expected amount burnt to pay for expired precommits - pub repaid_fee_debt: TokenAmount, - // Expected amount burnt to repay fee debt. - pub penalty_from_unlocked: TokenAmount, // Expected reduction in unlocked balance from penalties exceeding vesting funds. + pub power_delta: Option, + pub pledge_delta: TokenAmount, // Expected change in miner's pledge + pub burnt_funds: TokenAmount, // Expected burnt funds, through penalties, fee debt repayments and daily fees } #[allow(dead_code)] @@ -3190,31 +3042,11 @@ impl CronConfig { CronConfig { no_enrollment: false, expected_enrollment: 0, - detected_faults_power_delta: None, - expired_sectors_power_delta: None, - expired_sectors_pledge_delta: TokenAmount::zero(), - continued_faults_penalty: TokenAmount::zero(), - expired_precommit_penalty: TokenAmount::zero(), - repaid_fee_debt: TokenAmount::zero(), - penalty_from_unlocked: TokenAmount::zero(), + power_delta: None, + pledge_delta: TokenAmount::zero(), + burnt_funds: TokenAmount::zero(), } } - - pub fn with_continued_faults_penalty(fault_fee: TokenAmount) -> CronConfig { - let mut cfg = CronConfig::empty(); - cfg.continued_faults_penalty = fault_fee; - cfg - } - - pub fn with_detected_faults_power_delta_and_continued_faults_penalty( - pwr_delta: &PowerPair, - fault_fee: TokenAmount, - ) -> CronConfig { - let mut cfg = CronConfig::empty(); - cfg.detected_faults_power_delta = Some(pwr_delta.clone()); - cfg.continued_faults_penalty = fault_fee; - cfg - } } #[allow(dead_code)] @@ -3303,25 +3135,24 @@ enum MhCode { Sha256TruncPaddedFake, } -fn immediately_vesting_funds(rt: &MockRuntime, state: &State) -> TokenAmount { +fn vesting_funds(rt: &MockRuntime, state: &State, vested: bool) -> TokenAmount { let curr_epoch = *rt.epoch.borrow(); + state + .vesting_funds + .load(&rt.store) + .unwrap() + .into_iter() + .filter(|vf| vested == (vf.epoch < curr_epoch)) + .map(|vf| vf.amount) + .sum() +} - let q = - QuantSpec { unit: REWARD_VESTING_SPEC.quantization, offset: state.proving_period_start }; - if q.quantize_up(curr_epoch) != curr_epoch { - return TokenAmount::zero(); - } +pub fn immediately_vesting_funds(rt: &MockRuntime, state: &State) -> TokenAmount { + vesting_funds(rt, state, true) +} - let vesting = rt.store.get_cbor::(&state.vesting_funds).unwrap().unwrap(); - let mut sum = TokenAmount::zero(); - for vf in vesting.funds { - if vf.epoch < curr_epoch { - sum += vf.amount; - } else { - break; - } - } - sum +pub fn unvested_vesting_funds(rt: &MockRuntime, state: &State) -> TokenAmount { + vesting_funds(rt, state, false) } pub fn make_post_proofs(proof_type: RegisteredPoStProof) -> Vec { @@ -3368,7 +3199,7 @@ pub fn onboard_sectors( precommits: &[SectorPreCommitInfo], ) -> Vec { // Precommit sectors in batch. - h.pre_commit_sector_batch_v2(rt, &precommits, true, &TokenAmount::zero()).unwrap(); + h.pre_commit_sector_batch_v2(rt, &precommits, true).unwrap(); let precommits: Vec = precommits.iter().map(|sector| h.get_precommit(rt, sector.sector_number)).collect(); @@ -3680,6 +3511,7 @@ pub fn test_sector( deal_weight: u64, verified_deal_weight: u64, pledge: u64, + daily_fee: u64, ) -> SectorOnChainInfo { SectorOnChainInfo { expiration, @@ -3688,6 +3520,7 @@ pub fn test_sector( verified_deal_weight: DealWeight::from(verified_deal_weight), initial_pledge: TokenAmount::from_atto(pledge), sealed_cid: make_sector_commr(sector_number), + daily_fee: TokenAmount::from_atto(daily_fee), ..Default::default() } } @@ -3789,6 +3622,19 @@ fn expect_update_pledge(rt: &MockRuntime, pledge_delta: &TokenAmount) { } } +fn expect_burn(rt: &MockRuntime, burn_amount: TokenAmount) { + if !burn_amount.is_zero() { + rt.expect_send_simple( + BURNT_FUNDS_ACTOR_ADDR, + METHOD_SEND, + None, + burn_amount, + None, + ExitCode::OK, + ); + } +} + fn expect_update_power(rt: &MockRuntime, delta: PowerPair) { if !(delta.is_zero()) { rt.expect_send_simple( @@ -4056,7 +3902,7 @@ impl CronControl { rt, CronConfig { no_enrollment: true, - expired_precommit_penalty: st.pre_commit_deposits, + burnt_funds: st.pre_commit_deposits, ..CronConfig::empty() }, ); diff --git a/actors/miner/tests/vesting_unvested_funds.rs b/actors/miner/tests/vesting_unvested_funds.rs index 6cc1cae64..81ccb0a0a 100644 --- a/actors/miner/tests/vesting_unvested_funds.rs +++ b/actors/miner/tests/vesting_unvested_funds.rs @@ -1,6 +1,4 @@ use fil_actor_miner::VestSpec; -use fil_actor_miner::VestingFunds; -use fvm_ipld_encoding::CborStore; use fvm_shared::bigint::Zero; use fvm_shared::econ::TokenAmount; @@ -17,8 +15,10 @@ fn unlock_unvested_funds_leaving_bucket_with_non_zero_tokens() { h.add_locked_funds(vest_start, &vest_sum, &vspec).unwrap(); - let amount_unlocked = h.unlock_unvested_funds(vest_start, &TokenAmount::from_atto(39)).unwrap(); - assert_eq!(TokenAmount::from_atto(39), amount_unlocked); + let (vested_unlocked, total_unlocked) = + h.unlock_vested_and_unvested_funds(vest_start, &TokenAmount::from_atto(39)).unwrap(); + assert_eq!(TokenAmount::from_atto(39), vested_unlocked); + assert_eq!(TokenAmount::from_atto(39), total_unlocked); // no vested funds available to unlock until strictly after first vesting epoch assert_eq!(TokenAmount::zero(), h.unlock_vested_funds(vest_start).unwrap()); @@ -49,7 +49,8 @@ fn unlock_unvested_funds_leaving_bucket_with_zero_tokens() { h.add_locked_funds(vest_start, &vest_sum, &vspec).unwrap(); - let amount_unlocked = h.unlock_unvested_funds(vest_start, &TokenAmount::from_atto(40)).unwrap(); + let amount_unlocked = + h.unlock_vested_and_unvested_funds(vest_start, &TokenAmount::from_atto(40)).unwrap().0; assert_eq!(TokenAmount::from_atto(40), amount_unlocked); assert_eq!(TokenAmount::zero(), h.unlock_vested_funds(vest_start).unwrap()); @@ -78,7 +79,7 @@ fn unlock_all_unvested_funds() { let vest_sum = TokenAmount::from_atto(100); h.add_locked_funds(vest_start, &vest_sum, &vspec).unwrap(); - let unvested_funds = h.unlock_unvested_funds(vest_start, &vest_sum).unwrap(); + let unvested_funds = h.unlock_vested_and_unvested_funds(vest_start, &vest_sum).unwrap().0; assert_eq!(vest_sum, unvested_funds); assert!(h.st.locked_funds.is_zero()); @@ -93,7 +94,8 @@ fn unlock_unvested_funds_value_greater_than_locked_funds() { let vest_start = 10; let vest_sum = TokenAmount::from_atto(100); h.add_locked_funds(vest_start, &vest_sum, &vspec).unwrap(); - let unvested_funds = h.unlock_unvested_funds(vest_start, &TokenAmount::from_atto(200)).unwrap(); + let unvested_funds = + h.unlock_vested_and_unvested_funds(vest_start, &TokenAmount::from_atto(200)).unwrap().0; assert_eq!(vest_sum, unvested_funds); assert!(h.st.locked_funds.is_zero()); @@ -109,25 +111,30 @@ fn unlock_unvested_funds_when_there_are_vested_funds_in_the_table() { let vest_sum = TokenAmount::from_atto(100); // will lock funds from epochs 11 to 60 - h.add_locked_funds(vest_start, &vest_sum, &vspec).unwrap(); + let vested = h.add_locked_funds(vest_start, &vest_sum, &vspec).unwrap(); + assert_eq!(TokenAmount::zero(), vested); - // unlock funds from epochs 30 to 60 + // unlock funds from epochs 30 through 59 let new_epoch = 30; let target = TokenAmount::from_atto(60); - let remaining = &vest_sum - ⌖ - let unvested_funds = h.unlock_unvested_funds(new_epoch, &target).unwrap(); + let remaining = TokenAmount::from_atto(2); + let (unvested_funds, total_unlocked) = + h.unlock_vested_and_unvested_funds(new_epoch, &target).unwrap(); assert_eq!(target, unvested_funds); + assert_eq!(vest_sum - &remaining, total_unlocked); + // we expect 2 left over, locked for epoch 60 assert_eq!(remaining, h.st.locked_funds); - // vesting funds should have all epochs from 11 to 29 - let vesting = h.store.get_cbor::(&h.st.vesting_funds).unwrap().unwrap(); - let mut epoch = 11; - for vf in vesting.funds { - assert_eq!(epoch, vf.epoch); - epoch += 1; - if epoch == 30 { - break; - } - } + // vesting funds should have epoch 60 and only epoch 60 + assert_eq!( + &[60][..], + &h.st + .vesting_funds + .load(&h.store) + .unwrap() + .into_iter() + .map(|vf| vf.epoch) + .collect::>(), + ); } diff --git a/integration_tests/src/tests/batch_onboarding.rs b/integration_tests/src/tests/batch_onboarding.rs index 1405a0a4b..b03fddcb7 100644 --- a/integration_tests/src/tests/batch_onboarding.rs +++ b/integration_tests/src/tests/batch_onboarding.rs @@ -2,9 +2,7 @@ use export_macro::vm_test; use fil_actor_miner::SectorPreCommitOnChainInfo; use fil_actor_miner::{power_for_sector, State as MinerState}; use fil_actors_runtime::runtime::policy::policy_constants::PRE_COMMIT_CHALLENGE_DELAY; -use fil_actors_runtime::runtime::policy_constants::{ - MAX_AGGREGATED_SECTORS, PRE_COMMIT_SECTOR_BATCH_MAX_SIZE, -}; +use fil_actors_runtime::runtime::policy_constants::MAX_AGGREGATED_SECTORS; use fil_actors_runtime::runtime::Policy; use fvm_shared::bigint::BigInt; use fvm_shared::econ::TokenAmount; @@ -21,7 +19,6 @@ use crate::util::{ struct Onboarding { epoch_delay: i64, // epochs to advance since the prior action pre_commit_sector_count: usize, // sectors to batch pre-commit - pre_commit_batch_size: usize, // batch size (multiple batches if committing more) prove_commit_sector_count: usize, // sectors to aggregate prove-commit prove_commit_aggregate_size: usize, // aggregate size (multiple aggregates if proving more) } @@ -30,14 +27,12 @@ impl Onboarding { fn new( epoch_delay: i64, pre_commit_sector_count: usize, - pre_commit_batch_size: usize, prove_commit_sector_count: usize, prove_commit_aggregate_size: usize, ) -> Self { Self { epoch_delay, pre_commit_sector_count, - pre_commit_batch_size, prove_commit_sector_count, prove_commit_aggregate_size, } @@ -76,12 +71,12 @@ pub fn batch_onboarding_test(v: &dyn VM) { let mut pre_committed_count = 0; let vec_onboarding = vec![ - Onboarding::new(0, 10, PRE_COMMIT_SECTOR_BATCH_MAX_SIZE, 0, 0), - Onboarding::new(1, 20, 12, 0, 0), - Onboarding::new(PRE_COMMIT_CHALLENGE_DELAY + 1, 0, 0, 8, MAX_AGGREGATED_SECTORS as usize), - Onboarding::new(1, 0, 0, 8, 4), - Onboarding::new(1, 10, 4, 0, 0), - Onboarding::new(PRE_COMMIT_CHALLENGE_DELAY + 1, 0, 0, 24, 10), + Onboarding::new(0, 10, 0, 0), + Onboarding::new(1, 20, 0, 0), + Onboarding::new(PRE_COMMIT_CHALLENGE_DELAY + 1, 0, 8, MAX_AGGREGATED_SECTORS as usize), + Onboarding::new(1, 0, 8, 4), + Onboarding::new(1, 10, 0, 0), + Onboarding::new(PRE_COMMIT_CHALLENGE_DELAY + 1, 0, 24, 10), ]; let mut precommmits: Vec = vec![]; @@ -94,7 +89,6 @@ pub fn batch_onboarding_test(v: &dyn VM) { let mut new_precommits = precommit_sectors_v2( v, item.pre_commit_sector_count, - item.pre_commit_batch_size, vec![], &worker, &id_addr, diff --git a/integration_tests/src/tests/batch_onboarding_deals_test.rs b/integration_tests/src/tests/batch_onboarding_deals_test.rs index 6aa256e7e..e2920fa82 100644 --- a/integration_tests/src/tests/batch_onboarding_deals_test.rs +++ b/integration_tests/src/tests/batch_onboarding_deals_test.rs @@ -74,7 +74,6 @@ pub fn pre_commit_requires_commd_test(v: &dyn VM) { precommit_sectors_v2_expect_code( v, 1, - 1, vec![PrecommitMetadata { deals: vec![0], commd: CompactCommD(None) }], &worker, &miner, @@ -89,7 +88,6 @@ pub fn pre_commit_requires_commd_test(v: &dyn VM) { precommit_sectors_v2_expect_code( v, 1, - 1, vec![PrecommitMetadata { deals: vec![0], commd: CompactCommD(Some(make_piece_cid("This is not commP".as_bytes()))), @@ -151,7 +149,6 @@ pub fn batch_onboarding_deals_test(v: &dyn VM) { let precommits = precommit_sectors_v2( v, BATCH_SIZE, - BATCH_SIZE, sector_precommit_data, &worker, &miner, diff --git a/integration_tests/src/tests/commit_post_test.rs b/integration_tests/src/tests/commit_post_test.rs index 95f200f29..cdc9d5429 100644 --- a/integration_tests/src/tests/commit_post_test.rs +++ b/integration_tests/src/tests/commit_post_test.rs @@ -65,7 +65,6 @@ fn setup(v: &dyn VM) -> (MinerInfo, SectorInfo) { let _ = precommit_sectors_v2( v, 1, - 1, vec![], &worker, &id_addr, @@ -151,12 +150,59 @@ pub fn submit_post_succeeds_test(v: &dyn VM) { sector_info.partition_index, Some(sector_power.clone()), ); + + // move to proving period end + v.set_epoch(sector_info.deadline_info.last()); + + cron_tick(v); + + ExpectInvocation { + to: CRON_ACTOR_ADDR, + method: CronMethod::EpochTick as u64, + params: None, + subinvocs: Some(vec![ + ExpectInvocation { + from: CRON_ACTOR_ID, + to: STORAGE_POWER_ACTOR_ADDR, + method: PowerMethod::OnEpochTickEnd as u64, + subinvocs: Some(vec![ + Expect::reward_this_epoch(STORAGE_POWER_ACTOR_ID), + ExpectInvocation { + from: STORAGE_POWER_ACTOR_ID, + to: miner_info.miner_id, + method: MinerMethod::OnDeferredCronEvent as u64, + subinvocs: Some(vec![ + Expect::burn(miner_info.miner_id.id().unwrap(), Some(sector.daily_fee)), + Expect::power_enrol_cron(miner_info.miner_id.id().unwrap()), + ]), + ..Default::default() + }, + Expect::reward_update_kpi(), + ]), + ..Default::default() + }, + ExpectInvocation { + from: CRON_ACTOR_ID, + to: STORAGE_MARKET_ACTOR_ADDR, + method: MarketMethod::CronTick as u64, + ..Default::default() + }, + ]), + ..Default::default() + } + .matches(v.take_invocations().last().unwrap()); + let balances = miner_balance(v, &miner_info.miner_id); assert!(balances.initial_pledge.is_positive()); let p_st: PowerState = get_state(v, &STORAGE_POWER_ACTOR_ADDR).unwrap(); assert_eq!(sector_power.raw, p_st.total_bytes_committed); - assert_invariants(v, &Policy::default(), None); + expect_invariants( + v, + &Policy::default(), + &[invariant_failure_patterns::REWARD_STATE_EPOCH_MISMATCH.to_owned()], + None, + ); } #[vm_test] @@ -208,6 +254,10 @@ pub fn missed_first_post_deadline_test(v: &dyn VM) { // Run cron to detect missing PoSt cron_tick(v); + let st: MinerState = get_state(v, &miner_info.miner_id).unwrap(); + let sector = + st.get_sector(&DynBlockstore::wrap(v.blockstore()), sector_info.number).unwrap().unwrap(); + ExpectInvocation { to: CRON_ACTOR_ADDR, method: CronMethod::EpochTick as u64, @@ -223,9 +273,10 @@ pub fn missed_first_post_deadline_test(v: &dyn VM) { from: STORAGE_POWER_ACTOR_ID, to: miner_info.miner_id, method: MinerMethod::OnDeferredCronEvent as u64, - subinvocs: Some(vec![Expect::power_enrol_cron( - miner_info.miner_id.id().unwrap(), - )]), + subinvocs: Some(vec![ + Expect::burn(miner_info.miner_id.id().unwrap(), Some(sector.daily_fee)), + Expect::power_enrol_cron(miner_info.miner_id.id().unwrap()), + ]), ..Default::default() }, Expect::reward_update_kpi(), @@ -277,7 +328,6 @@ pub fn overdue_precommit_test(v: &dyn VM) { let precommit = precommit_sectors_v2( v, 1, - 1, vec![], &worker, &id_addr, @@ -390,7 +440,6 @@ pub fn aggregate_bad_sector_number_test(v: &dyn VM) { precommit_sectors_v2( v, 4, - policy.pre_commit_sector_batch_max_size, vec![], &worker, &id_addr, @@ -464,7 +513,6 @@ pub fn aggregate_size_limits_test(v: &dyn VM) { precommit_sectors_v2( v, oversized_batch, - policy.pre_commit_sector_batch_max_size, vec![], &worker, &id_addr, @@ -568,7 +616,6 @@ pub fn aggregate_bad_sender_test(v: &dyn VM) { precommit_sectors_v2( v, 4, - policy.pre_commit_sector_batch_max_size, vec![], &worker, &id_addr, @@ -640,7 +687,6 @@ pub fn aggregate_one_precommit_expires_test(v: &dyn VM) { let early_precommits = precommit_sectors_v2( v, 1, - policy.pre_commit_sector_batch_max_size, vec![], &worker, &miner_addr, @@ -660,7 +706,6 @@ pub fn aggregate_one_precommit_expires_test(v: &dyn VM) { let later_precommits = precommit_sectors_v2( v, 3, - policy.pre_commit_sector_batch_max_size, vec![], &worker, &miner_addr, @@ -734,7 +779,6 @@ pub fn aggregate_one_precommit_expires_test(v: &dyn VM) { Expect::reward_this_epoch(miner_id), Expect::power_current_total(miner_id), Expect::power_update_pledge(miner_id, None), - Expect::burn(miner_id, None), ]), events: Some(events), ..Default::default() diff --git a/integration_tests/src/tests/prove_commit3_test.rs b/integration_tests/src/tests/prove_commit3_test.rs index 94746eed9..082078a7e 100644 --- a/integration_tests/src/tests/prove_commit3_test.rs +++ b/integration_tests/src/tests/prove_commit3_test.rs @@ -8,13 +8,13 @@ use fvm_shared::deal::DealID; use fvm_shared::econ::TokenAmount; use fvm_shared::piece::{PaddedPieceSize, PieceInfo}; use fvm_shared::sector::{RegisteredSealProof, SectorNumber, StoragePower}; -use num_traits::Zero; +use num_traits::{Signed, Zero}; use fil_actor_market::Method as MarketMethod; use fil_actor_miner::{ - max_prove_commit_duration, CompactCommD, DataActivationNotification, PieceActivationManifest, - PieceChange, ProveCommitSectors3Params, SectorActivationManifest, SectorChanges, - SectorContentChangedParams, SectorOnChainInfoFlags, + daily_proof_fee, max_prove_commit_duration, qa_power_for_weight, CompactCommD, + DataActivationNotification, PieceActivationManifest, PieceChange, ProveCommitSectors3Params, + SectorActivationManifest, SectorChanges, SectorContentChangedParams, SectorOnChainInfoFlags, }; use fil_actor_miner::{Method as MinerMethod, VerifiedAllocationKey}; use fil_actor_verifreg::{ @@ -190,7 +190,6 @@ pub fn prove_commit_sectors2_test(v: &dyn VM) { precommit_sectors_v2( v, meta.len(), - meta.len(), meta.clone(), &worker, &maddr, @@ -203,6 +202,8 @@ pub fn prove_commit_sectors2_test(v: &dyn VM) { let activation_epoch = v.epoch() + policy.pre_commit_challenge_delay + 1; advance_by_deadline_to_epoch(v, &maddr, activation_epoch); + let circulating_supply_at_commit = v.circulating_supply(); + // Prove-commit let proofs = vec![RawBytes::new(vec![1, 2, 3, 4]); manifests.len()]; let params = ProveCommitSectors3Params { @@ -391,16 +392,36 @@ pub fn prove_commit_sectors2_test(v: &dyn VM) { } let full_sector_weight = BigInt::from(full_piece_size.0 * (sector_expiry - activation_epoch) as u64); + let full_sector_power = + qa_power_for_weight(sector_size, sector_expiry - activation_epoch, &full_sector_weight); + let full_sector_daily_fee = + daily_proof_fee(&policy, &circulating_supply_at_commit, &full_sector_power); + assert_eq!(BigInt::zero(), sectors[0].deal_weight); assert_eq!(BigInt::zero(), sectors[0].verified_deal_weight); + assert_eq!(full_sector_daily_fee.div_floor(10), sectors[0].daily_fee); + assert_eq!(full_sector_weight, sectors[1].deal_weight); assert_eq!(BigInt::zero(), sectors[1].verified_deal_weight); + assert_eq!(full_sector_daily_fee.div_floor(10), sectors[1].daily_fee); + assert_eq!(BigInt::zero(), sectors[2].deal_weight); assert_eq!(full_sector_weight, sectors[2].verified_deal_weight); + assert_eq!(full_sector_daily_fee, sectors[2].daily_fee); + assert_eq!(full_sector_weight, sectors[3].deal_weight); assert_eq!(BigInt::zero(), sectors[3].verified_deal_weight); + assert_eq!(full_sector_daily_fee.div_floor(10), sectors[3].daily_fee); + assert_eq!(BigInt::zero(), sectors[4].deal_weight); assert_eq!(full_sector_weight / 2, sectors[4].verified_deal_weight); + assert!( + ((&full_sector_daily_fee * 11).div_floor(20) - §ors[4].daily_fee).atto().abs() + <= BigInt::from(1), + "expected: {}, got: {}", + (full_sector_daily_fee * 11).div_floor(20), + sectors[4].daily_fee + ); // Brief checks on state consistency between actors. let claims = verifreg_list_claims(v, miner_id); diff --git a/integration_tests/src/tests/prove_commit_niporep_test.rs b/integration_tests/src/tests/prove_commit_niporep_test.rs index df8c877f6..2e6806c25 100644 --- a/integration_tests/src/tests/prove_commit_niporep_test.rs +++ b/integration_tests/src/tests/prove_commit_niporep_test.rs @@ -4,11 +4,13 @@ use fvm_ipld_encoding::RawBytes; use fvm_shared::bigint::BigInt; use fvm_shared::econ::TokenAmount; use fvm_shared::error::ExitCode; -use fvm_shared::sector::{RegisteredAggregateProof, RegisteredSealProof, SectorNumber}; +use fvm_shared::sector::{ + RegisteredAggregateProof, RegisteredSealProof, SectorNumber, StoragePower, +}; use num_traits::Zero; use export_macro::vm_test; -use fil_actor_miner::{Method as MinerMethod, SectorOnChainInfoFlags}; +use fil_actor_miner::{daily_proof_fee, Method as MinerMethod, SectorOnChainInfoFlags}; use fil_actor_miner::{ ProveCommitSectorsNIParams, ProveCommitSectorsNIReturn, SectorNIActivationInfo, }; @@ -29,14 +31,14 @@ pub fn prove_commit_ni_whole_success_test(v: &dyn VM) { // Expectations depend on the correct unsealed CID for empty sector. override_compute_unsealed_sector_cid(v); let addrs = create_accounts(v, 3, &TokenAmount::from_whole(10_000)); - let seal_proof = RegisteredSealProof::StackedDRG32GiBV1P2_Feat_NiPoRep; + let seal_proof_type = RegisteredSealProof::StackedDRG32GiBV1P2_Feat_NiPoRep; let (owner, worker, _, _) = (addrs[0], addrs[0], addrs[1], addrs[2]); let worker_id = worker.id().unwrap(); let (maddr, _) = create_miner( v, &owner, &worker, - seal_proof.registered_window_post_proof().unwrap(), + seal_proof_type.registered_window_post_proof().unwrap(), &TokenAmount::from_whole(8_000), ); let miner_id = maddr.id().unwrap(); @@ -72,7 +74,7 @@ pub fn prove_commit_ni_whole_success_test(v: &dyn VM) { let aggregate_proof = RawBytes::new(vec![1, 2, 3, 4]); let params = ProveCommitSectorsNIParams { sectors: sectors_info.clone(), - seal_proof_type: RegisteredSealProof::StackedDRG32GiBV1P2_Feat_NiPoRep, + seal_proof_type, aggregate_proof, aggregate_proof_type: RegisteredAggregateProof::SnarkPackV2, proving_deadline, @@ -125,6 +127,11 @@ pub fn prove_commit_ni_whole_success_test(v: &dyn VM) { .iter() .map(|sector_number| sector_info(v, &maddr, *sector_number)) .collect::>(); + let expected_daily_fee = daily_proof_fee( + &policy, + &v.circulating_supply(), + &StoragePower::from(seal_proof_type.sector_size().unwrap() as u64), + ); for (on_chain_sector, input_sector) in sectors.iter().zip(sectors_info) { assert_eq!(input_sector.sector_number, on_chain_sector.sector_number); @@ -137,6 +144,7 @@ pub fn prove_commit_ni_whole_success_test(v: &dyn VM) { assert_eq!(BigInt::zero(), on_chain_sector.verified_deal_weight); assert_eq!(activation_epoch, on_chain_sector.power_base_epoch); assert!(on_chain_sector.flags.contains(SectorOnChainInfoFlags::SIMPLE_QA_POWER)); + assert_eq!(expected_daily_fee, on_chain_sector.daily_fee); } let deadline = deadline_state(v, &maddr, proving_deadline); diff --git a/integration_tests/src/tests/replica_update3_test.rs b/integration_tests/src/tests/replica_update3_test.rs index fe7936dec..4989b4ca2 100644 --- a/integration_tests/src/tests/replica_update3_test.rs +++ b/integration_tests/src/tests/replica_update3_test.rs @@ -80,7 +80,6 @@ pub fn prove_replica_update2_test(v: &dyn VM) { precommit_sectors_v2( v, meta.len(), - meta.len(), meta, &worker, &maddr, diff --git a/integration_tests/src/tests/replica_update_test.rs b/integration_tests/src/tests/replica_update_test.rs index 09ed7afdb..dfc62b0c1 100644 --- a/integration_tests/src/tests/replica_update_test.rs +++ b/integration_tests/src/tests/replica_update_test.rs @@ -178,7 +178,6 @@ pub fn prove_replica_update_multi_dline_test(v: &dyn VM) { let new_precommits = precommit_sectors_v2( v, more_than_one_partition, - batch_size, vec![], &worker, &maddr, @@ -466,58 +465,6 @@ pub fn terminated_sector_failure_test(v: &dyn VM) { assert_invariants(v, &Policy::default(), None) } -#[vm_test] -pub fn bad_batch_size_failure_test(v: &dyn VM) { - let policy = Policy::default(); - let addrs = create_accounts(v, 1, &TokenAmount::from_whole(100_000)); - let (worker, owner) = (addrs[0], addrs[0]); - let seal_proof = RegisteredSealProof::StackedDRG32GiBV1P1; - let (maddr, robust) = create_miner( - v, - &owner, - &worker, - seal_proof.registered_window_post_proof().unwrap(), - &TokenAmount::from_whole(10_000), - ); - - // advance to have seal randomness epoch in the past - v.set_epoch(200); - - let sector_number = 100; - let (d_idx, p_idx) = create_sector(v, worker, maddr, sector_number, seal_proof); - - // make some deals - let deal_ids = create_deals(1, v, worker, worker, maddr); - - // fail to replicaUpdate more sectors than batch size - let new_cid = make_sealed_cid(b"replica1"); - let mut updates = vec![]; - - for _ in 0..policy.prove_replica_updates_max_size + 1 { - updates.push(ReplicaUpdate { - sector_number, - deadline: d_idx, - partition: p_idx, - new_sealed_cid: new_cid, - deals: deal_ids.clone(), - update_proof_type: fvm_shared::sector::RegisteredUpdateProof::StackedDRG32GiBV1, - replica_proof: vec![].into(), - }); - } - - apply_code( - v, - &worker, - &robust, - &TokenAmount::zero(), - MinerMethod::ProveReplicaUpdates as u64, - Some(ProveReplicaUpdatesParams { updates }), - ExitCode::USR_ILLEGAL_ARGUMENT, - ); - - assert_invariants(v, &Policy::default(), None) -} - #[vm_test] pub fn nodispute_after_upgrade_test(v: &dyn VM) { let (_, worker, miner_id, deadline_index, _, _) = create_miner_and_upgrade_sector(v); @@ -731,7 +678,6 @@ pub fn extend_after_upgrade_test(v: &dyn VM) { #[vm_test] pub fn wrong_deadline_index_failure_test(v: &dyn VM) { - let policy = Policy::default(); let addrs = create_accounts(v, 1, &TokenAmount::from_whole(100_000)); let (worker, owner) = (addrs[0], addrs[0]); let seal_proof = RegisteredSealProof::StackedDRG32GiBV1P1; @@ -757,7 +703,7 @@ pub fn wrong_deadline_index_failure_test(v: &dyn VM) { let new_cid = make_sealed_cid(b"replica1"); let mut updates = vec![]; - for _ in 0..policy.prove_replica_updates_max_size + 1 { + for _ in 0..256 + 1 { updates.push(ReplicaUpdate { sector_number, deadline: d_idx + 1, @@ -787,7 +733,6 @@ pub fn wrong_deadline_index_failure_test(v: &dyn VM) { #[vm_test] pub fn wrong_partition_index_failure_test(v: &dyn VM) { - let policy = Policy::default(); let addrs = create_accounts(v, 1, &TokenAmount::from_whole(100_000)); let (worker, owner) = (addrs[0], addrs[0]); let seal_proof = RegisteredSealProof::StackedDRG32GiBV1P1; @@ -813,7 +758,7 @@ pub fn wrong_partition_index_failure_test(v: &dyn VM) { let new_cid = make_sealed_cid(b"replica1"); let mut updates = vec![]; - for _ in 0..policy.prove_replica_updates_max_size + 1 { + for _ in 0..256 + 1 { updates.push(ReplicaUpdate { sector_number, deadline: d_idx, @@ -863,7 +808,6 @@ pub fn deal_included_in_multiple_sectors_failure_test(v: &dyn VM) { let precommits = precommit_sectors_v2( v, policy.min_aggregated_sectors as usize, - policy.pre_commit_sector_batch_max_size, vec![], &worker, &maddr, @@ -1183,7 +1127,6 @@ pub fn create_sector( let precommits = precommit_sectors_v2( v, 1, - 1, vec![], &worker, &maddr, diff --git a/integration_tests/src/util/workflows.rs b/integration_tests/src/util/workflows.rs index 619cf17ae..15902746e 100644 --- a/integration_tests/src/util/workflows.rs +++ b/integration_tests/src/util/workflows.rs @@ -38,7 +38,6 @@ use fil_actor_market::{ PublishStorageDealsReturn, SectorDeals, State as MarketState, MARKET_NOTIFY_DEAL_METHOD, }; use fil_actor_miner::{ - aggregate_pre_commit_network_fee, aggregate_prove_commit_network_fee, max_prove_commit_duration, ChangeBeneficiaryParams, CompactCommD, DataActivationNotification, DeadlineInfo, DeclareFaultsRecoveredParams, ExpirationExtension2, ExtendSectorExpiration2Params, Method as MinerMethod, PieceActivationManifest, PoStPartition, @@ -150,7 +149,6 @@ pub fn miner_precommit_one_sector_v2( precommit_sectors_v2( v, 1, - 1, vec![meta_data], worker, maddr, @@ -207,7 +205,6 @@ pub struct PrecommitMetadata { pub fn precommit_sectors_v2_expect_code( v: &dyn VM, count: usize, - batch_size: usize, metadata: Vec, // Per-sector deal metadata, or empty vector for no deals. worker: &Address, maddr: &Address, @@ -237,8 +234,7 @@ pub fn precommit_sectors_v2_expect_code( let mut invocs = vec![Expect::reward_this_epoch(miner_id), Expect::power_current_total(miner_id)]; let mut param_sectors = Vec::::new(); - let mut j = 0; - while j < batch_size && sector_idx < count { + while sector_idx < count { let sector_number = sector_number_base + sector_idx as u64; let sector_meta = metadata.get(sector_idx).unwrap_or(&no_deals); param_sectors.push(SectorPreCommitInfo { @@ -259,7 +255,6 @@ pub fn precommit_sectors_v2_expect_code( }); } sector_idx += 1; - j += 1; } let events: Vec = param_sectors @@ -270,12 +265,6 @@ pub fn precommit_sectors_v2_expect_code( if !sectors_with_deals.is_empty() { invocs.push(Expect::market_verify_deals(miner_id, sectors_with_deals.clone())); } - if param_sectors.len() > 1 { - invocs.push(Expect::burn( - miner_id, - Some(aggregate_pre_commit_network_fee(param_sectors.len(), &TokenAmount::zero())), - )); - } if expect_cron_enroll && msg_sector_idx_base == 0 { invocs.push(Expect::power_enrol_cron(miner_id)); } @@ -313,7 +302,6 @@ pub fn precommit_sectors_v2_expect_code( pub fn precommit_sectors_v2( v: &dyn VM, count: usize, - batch_size: usize, metadata: Vec, // Per-sector deal metadata, or empty vector for no deals. worker: &Address, maddr: &Address, @@ -326,7 +314,6 @@ pub fn precommit_sectors_v2( precommit_sectors_v2_expect_code( v, count, - batch_size, metadata, worker, maddr, @@ -431,7 +418,6 @@ pub fn prove_commit_sectors( }) .collect(); - let expected_fee = aggregate_prove_commit_network_fee(to_prove.len(), &TokenAmount::zero()); ExpectInvocation { from: worker_id, to: *maddr, @@ -441,7 +427,6 @@ pub fn prove_commit_sectors( Expect::reward_this_epoch(miner_id), Expect::power_current_total(miner_id), Expect::power_update_pledge(miner_id, None), - Expect::burn(miner_id, Some(expected_fee)), ]), events: Some(events), ..Default::default() diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 9f2cd9bdf..fd2e3de29 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -24,7 +24,7 @@ itertools = { workspace = true } lazy_static = { workspace = true } log = { workspace = true } multihash-codetable = { workspace = true } -num = { workspace = true } +num = { workspace = true } num-derive = { workspace = true } num-traits = { workspace = true } regex = { workspace = true } diff --git a/runtime/src/runtime/policy.rs b/runtime/src/runtime/policy.rs index 333f30075..9ba4d8bd3 100644 --- a/runtime/src/runtime/policy.rs +++ b/runtime/src/runtime/policy.rs @@ -1,3 +1,4 @@ +use fvm_shared::bigint::BigInt; use fvm_shared::clock::ChainEpoch; use fvm_shared::sector::{RegisteredPoStProof, RegisteredSealProof, StoragePower}; use num_traits::FromPrimitive; @@ -11,6 +12,9 @@ pub trait RuntimePolicy { // The policy itself #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct Policy { + // + // --- miner policy --- + // /// Maximum amount of sectors that can be aggregated. pub max_aggregated_sectors: u64, /// Minimum amount of sectors that can be aggregated. @@ -20,11 +24,6 @@ pub struct Policy { /// Maximum total replica update proof size. pub max_replica_update_proof_size: usize, - /// The maximum number of sector pre-commitments in a single batch. - pub pre_commit_sector_batch_max_size: usize, - /// The maximum number of sector replica updates in a single batch. - pub prove_replica_updates_max_size: usize, - /// The delay between pre commit expiration and clean up from state. This enforces that expired pre-commits /// stay in state for a period of time creating a grace period during which a late-running aggregated prove-commit /// can still prove its non-expired precommits without resubmitting a message @@ -63,9 +62,6 @@ pub struct Policy { /// This limits the number of simultaneous fault, recovery, or sector-extension declarations. pub addressed_partitions_max: u64, - /// Maximum number of unique "declarations" in batch operations. - pub declarations_max: u64, - /// The maximum number of sector numbers addressable in a single invocation /// (which implies also the max infos that may be loaded at once). /// One upper bound on this is the max size of a storage block: 1MiB supports 130k at 8 bytes each, @@ -134,7 +130,21 @@ pub struct Policy { /// Allowed pre commit proof types for new miners pub valid_pre_commit_proof_type: ProofSet, - // --- verifreg policy + /// Numerator of the fraction of circulating supply that will be used to calculate + /// the daily fee for new sectors. + pub daily_fee_circulating_supply_qap_multiplier_num: BigInt, + /// Denominator of the fraction of circulating supply that will be used to calculate + /// the daily fee for new sectors. + pub daily_fee_circulating_supply_qap_multiplier_denom: BigInt, + /// Denominator for the fraction of estimated daily block reward for the sector(s) + /// attracting a fee, to be used as a cap for the fees when payable. + /// No numerator is provided as the fee is calculated as a fraction of the estimated + /// daily block reward. + pub daily_fee_block_reward_cap_denom: i64, + + // + // --- verifreg policy --- + // /// Minimum verified deal size pub minimum_verified_allocation_size: StoragePower, /// Minimum term for a verified data allocation (epochs) @@ -147,7 +157,9 @@ pub struct Policy { // Period of time at the end of a sector's life during which claims can be dropped pub end_of_life_claim_drop_period: ChainEpoch, + // // --- market policy --- + // /// The number of blocks between payouts for deals pub deal_updates_interval: i64, @@ -163,7 +175,9 @@ pub struct Policy { /// allocation's maximum term. pub market_default_allocation_term_buffer: i64, - // --- power --- + // + // --- power policy --- + // /// Minimum miner consensus power pub minimum_consensus_power: StoragePower, } @@ -175,8 +189,6 @@ impl Default for Policy { min_aggregated_sectors: policy_constants::MIN_AGGREGATED_SECTORS, max_aggregated_proof_size: policy_constants::MAX_AGGREGATED_PROOF_SIZE, max_replica_update_proof_size: policy_constants::MAX_REPLICA_UPDATE_PROOF_SIZE, - pre_commit_sector_batch_max_size: policy_constants::PRE_COMMIT_SECTOR_BATCH_MAX_SIZE, - prove_replica_updates_max_size: policy_constants::PROVE_REPLICA_UPDATES_MAX_SIZE, expired_pre_commit_clean_up_delay: policy_constants::EXPIRED_PRE_COMMIT_CLEAN_UP_DELAY, wpost_proving_period: policy_constants::WPOST_PROVING_PERIOD, wpost_challenge_window: policy_constants::WPOST_CHALLENGE_WINDOW, @@ -189,7 +201,6 @@ impl Default for Policy { max_peer_id_length: policy_constants::MAX_PEER_ID_LENGTH, max_multiaddr_data: policy_constants::MAX_MULTIADDR_DATA, addressed_partitions_max: policy_constants::ADDRESSED_PARTITIONS_MAX, - declarations_max: policy_constants::DECLARATIONS_MAX, addressed_sectors_max: policy_constants::ADDRESSED_SECTORS_MAX, posted_partitions_max: policy_constants::POSTED_PARTITIONS_MAX, max_pre_commit_randomness_lookback: @@ -210,6 +221,15 @@ impl Default for Policy { policy_constants::CONSENSUS_FAULT_INELIGIBILITY_DURATION, new_sectors_per_period_max: policy_constants::NEW_SECTORS_PER_PERIOD_MAX, chain_finality: policy_constants::CHAIN_FINALITY, + daily_fee_circulating_supply_qap_multiplier_num: BigInt::from_u64( + policy_constants::DAILY_FEE_CIRCULATING_SUPPLY_QAP_MULTIPLIER_NUM, + ) + .unwrap(), + daily_fee_circulating_supply_qap_multiplier_denom: BigInt::from_u128( + policy_constants::DAILY_FEE_CIRCULATING_SUPPLY_QAP_MULTIPLIER_DENOM, + ) + .unwrap(), + daily_fee_block_reward_cap_denom: policy_constants::DAILY_FEE_BLOCK_REWARD_CAP_DENOM, valid_post_proof_type: ProofSet::default_post_proofs(), valid_pre_commit_proof_type: ProofSet::default_precommit_seal_proofs(), @@ -241,6 +261,10 @@ pub mod policy_constants { use crate::builtin::*; + // + // --- miner policy --- + // + /// The maximum assignable sector number. /// Raising this would require modifying our AMT implementation. pub const MAX_SECTOR_NUMBER: SectorNumber = i64::MAX as u64; @@ -254,12 +278,6 @@ pub mod policy_constants { pub const MAX_REPLICA_UPDATE_PROOF_SIZE: usize = 4096; - // 32 sectors per epoch would support a single miner onboarding 1EiB of 32GiB sectors in 1 year. - pub const PRE_COMMIT_SECTOR_BATCH_MAX_SIZE: usize = 256; - - // Same as PRE_COMMIT_SECTOR_BATCH_MAX_SIZE for consistency. - pub const PROVE_REPLICA_UPDATES_MAX_SIZE: usize = PRE_COMMIT_SECTOR_BATCH_MAX_SIZE; - pub const EXPIRED_PRE_COMMIT_CLEAN_UP_DELAY: i64 = 8 * EPOCHS_IN_HOUR; pub const WPOST_PROVING_PERIOD: ChainEpoch = EPOCHS_IN_DAY; @@ -294,8 +312,6 @@ pub mod policy_constants { // A miner can of course submit more messages. pub const ADDRESSED_PARTITIONS_MAX: u64 = MAX_PARTITIONS_PER_DEADLINE; - pub const DECLARATIONS_MAX: u64 = ADDRESSED_PARTITIONS_MAX; - pub const ADDRESSED_SECTORS_MAX: u64 = 25_000; pub const POSTED_PARTITIONS_MAX: u64 = 3; @@ -347,6 +363,23 @@ pub mod policy_constants { /// This is a conservative value that is chosen via simulations of all known attacks. pub const CHAIN_FINALITY: ChainEpoch = 900; + // Fraction of circulating supply per byte of quality adjusted power that will be used to calculate + // the daily fee for new sectors. + // The target multiplier is: + // 5.56e-15 / 32GiB = 5.56e-15 / (32 * 2^30) = 5.56e-15 / 34,359,738,368 ≈ 1.61817e-25 + // (i.e. slightly rounded for simplicity and a more direct multiplication). + // We implement this as 161817e-30. + pub const DAILY_FEE_CIRCULATING_SUPPLY_QAP_MULTIPLIER_NUM: u64 = 161817; + pub const DAILY_FEE_CIRCULATING_SUPPLY_QAP_MULTIPLIER_DENOM: u128 = + 1_000_000_000_000_000_000_000_000_000_000; // 10^30 + + // 50% of estimated daily block rewards + pub const DAILY_FEE_BLOCK_REWARD_CAP_DENOM: i64 = 2; + + // + // --- verifreg policy --- + // + #[cfg(not(feature = "small-deals"))] pub const MINIMUM_VERIFIED_ALLOCATION_SIZE: i32 = 1 << 20; #[cfg(feature = "small-deals")] @@ -356,6 +389,10 @@ pub mod policy_constants { pub const MAXIMUM_VERIFIED_ALLOCATION_EXPIRATION: i64 = 60 * EPOCHS_IN_DAY; pub const END_OF_LIFE_CLAIM_DROP_PERIOD: ChainEpoch = 30 * EPOCHS_IN_DAY; + // + // --- market policy --- + // + pub const DEAL_UPDATES_INTERVAL: i64 = 30 * EPOCHS_IN_DAY; #[cfg(not(feature = "no-provider-deal-collateral"))] @@ -367,6 +404,10 @@ pub mod policy_constants { pub const MARKET_DEFAULT_ALLOCATION_TERM_BUFFER: i64 = 90 * EPOCHS_IN_DAY; + // + // --- power policy --- + // + #[cfg(feature = "min-power-2k")] pub const MINIMUM_CONSENSUS_POWER: i64 = 2 << 10; #[cfg(feature = "min-power-2g")] diff --git a/runtime/src/test_utils.rs b/runtime/src/test_utils.rs index 2ee344101..e76c96ed3 100644 --- a/runtime/src/test_utils.rs +++ b/runtime/src/test_utils.rs @@ -157,7 +157,7 @@ pub struct MockRuntime { &[u8; SECP_SIG_LEN], ) -> Result<[u8; SECP_PUB_LEN], ()>, >, - pub network_version: NetworkVersion, + pub network_version: RefCell, // Actor State pub state: RefCell>, @@ -346,7 +346,7 @@ impl MockRuntime { value_received: Default::default(), hash_func: Box::new(hash), recover_secp_pubkey_fn: Box::new(recover_secp_public_key), - network_version: NetworkVersion::V0, + network_version: RefCell::new(NetworkVersion::V0), state: Default::default(), balance: Default::default(), in_call: Default::default(), @@ -730,6 +730,10 @@ impl MockRuntime { self.circulating_supply.replace(circ_supply); } + pub fn set_network_version(&self, nv: NetworkVersion) { + self.network_version.replace(nv); + } + #[allow(dead_code)] pub fn set_epoch(&self, epoch: ChainEpoch) -> ChainEpoch { self.epoch.replace(epoch); @@ -853,7 +857,7 @@ impl Runtime for MockRuntime { type Blockstore = Rc; fn network_version(&self) -> NetworkVersion { - self.network_version + *self.network_version.borrow() } fn message(&self) -> &dyn MessageInfo { diff --git a/test_vm/src/lib.rs b/test_vm/src/lib.rs index 907b99c3b..f9db4e885 100644 --- a/test_vm/src/lib.rs +++ b/test_vm/src/lib.rs @@ -306,7 +306,7 @@ impl VM for TestVM { originator_stable_addr: *from, originator_call_seq: call_seq, new_actor_addr_count: RefCell::new(0), - circ_supply: TokenAmount::from_whole(1_000_000_000), + circ_supply: self.circulating_supply.borrow().clone(), }; let msg = InternalMessage { from: from_id.id().unwrap(), diff --git a/test_vm/tests/suite/replica_update_test.rs b/test_vm/tests/suite/replica_update_test.rs index e2ef8bf08..1865e5c7d 100644 --- a/test_vm/tests/suite/replica_update_test.rs +++ b/test_vm/tests/suite/replica_update_test.rs @@ -5,9 +5,8 @@ use std::rc::Rc; use test_vm::TestVM; use fil_actors_integration_tests::tests::{ - bad_batch_size_failure_test, bad_post_upgrade_dispute_test, - deal_included_in_multiple_sectors_failure_test, extend_after_upgrade_test, - immutable_deadline_failure_test, nodispute_after_upgrade_test, + bad_post_upgrade_dispute_test, deal_included_in_multiple_sectors_failure_test, + extend_after_upgrade_test, immutable_deadline_failure_test, nodispute_after_upgrade_test, prove_replica_update_multi_dline_test, replica_update_full_path_success_test, replica_update_verified_deal_max_term_violated_test, replica_update_verified_deal_test, terminate_after_upgrade_test, terminated_sector_failure_test, unhealthy_sector_failure_test, @@ -71,13 +70,6 @@ fn terminated_sector_failure() { terminated_sector_failure_test(&v); } -#[test] -fn bad_batch_size_failure() { - let store = MemoryBlockstore::new(); - let v = TestVM::new_with_singletons(store); - bad_batch_size_failure_test(&v); -} - #[test] fn no_dispute_after_upgrade() { let store = MemoryBlockstore::new();