Skip to content
This repository has been archived by the owner on Jun 6, 2023. It is now read-only.

Commit

Permalink
Refactor deadline handling (#1009)
Browse files Browse the repository at this point in the history
* Moves most logic into helper functions.
* Avoids loading deadlines, vesting table, etc. unnecessarily.
* Loads the vesting table at most twice.
  • Loading branch information
Stebalien authored Aug 24, 2020
1 parent eb96719 commit d67d2a7
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 109 deletions.
135 changes: 30 additions & 105 deletions actors/builtin/miner/miner_actor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1655,36 +1655,23 @@ func handleProvingDeadline(rt Runtime) {

hadEarlyTerminations := false

powerDelta := PowerPair{big.Zero(), big.Zero()}
powerDeltaTotal := NewPowerPairZero()
penaltyTotal := abi.NewTokenAmount(0)
pledgeDelta := abi.NewTokenAmount(0)
pledgeDeltaTotal := abi.NewTokenAmount(0)

var st State
rt.State().Transaction(&st, func() {
var err error
{
// Vest locked funds.
// This happens first so that any subsequent penalties are taken
// from locked vesting funds before funds free this epoch.
newlyVested, err := st.UnlockVestedFunds(store, rt.CurrEpoch())
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to vest funds")
pledgeDelta = big.Add(pledgeDelta, newlyVested.Neg())
pledgeDeltaTotal = big.Add(pledgeDeltaTotal, newlyVested.Neg())
}

{
// expire pre-committed sectors
expiryQ, err := LoadBitfieldQueue(store, st.PreCommittedSectorsExpiry, st.QuantSpecEveryDeadline())
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load sector expiry queue")

bf, modified, err := expiryQ.PopUntil(currEpoch)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to pop expired sectors")

if modified {
st.PreCommittedSectorsExpiry, err = expiryQ.Root()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to save expiry queue")
}

depositToBurn, err := st.checkPrecommitExpiry(store, bf)
depositToBurn, err := st.ExpirePreCommits(store, currEpoch)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to expire pre-committed sectors")
penaltyTotal = big.Add(penaltyTotal, depositToBurn)
}
Expand All @@ -1693,103 +1680,41 @@ func handleProvingDeadline(rt Runtime) {
// That way, don't re-schedule a cron callback if one is already scheduled.
hadEarlyTerminations = havePendingEarlyTerminations(rt, &st)

// Note: because the cron actor is not invoked on epochs with empty tipsets, the current epoch is not necessarily
// exactly the final epoch of the deadline; it may be slightly later (i.e. in the subsequent deadline/period).
// Further, this method is invoked once *before* the first proving period starts, after the actor is first
// constructed; this is detected by !dlInfo.PeriodStarted().
// Use dlInfo.PeriodEnd() rather than rt.CurrEpoch unless certain of the desired semantics.
dlInfo := st.DeadlineInfo(currEpoch)
if !dlInfo.PeriodStarted() {
return // Skip checking faults on the first, incomplete period.
}
deadlines, err := st.LoadDeadlines(store)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load deadlines")
deadline, err := deadlines.LoadDeadline(store, dlInfo.Index)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load deadline %d", dlInfo.Index)
quant := dlInfo.QuantSpec()
unlockedBalance := st.GetUnlockedBalance(rt.CurrentBalance())

{
// Detect and penalize missing proofs.
faultExpiration := dlInfo.Last() + FaultMaxAge

partPowerDelta, penalizedPower, err := deadline.ProcessDeadlineEnd(store, quant, faultExpiration)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to process end of deadline %d", dlInfo.Index)

powerDelta = powerDelta.Add(partPowerDelta)

// Unlock sector penalty for all undeclared faults.
penaltyTarget := PledgePenaltyForUndeclaredFault(epochReward.ThisEpochRewardSmoothed, pwrTotal.QualityAdjPowerSmoothed, penalizedPower.QA)
// Subtract the "ongoing" fault fee from the amount charged now, since it will be added on just below.
penaltyTarget = big.Sub(penaltyTarget, PledgePenaltyForDeclaredFault(epochReward.ThisEpochRewardSmoothed, pwrTotal.QualityAdjPowerSmoothed, penalizedPower.QA))
penaltyFromVesting, penaltyFromBalance, err := st.PenalizeFundsInPriorityOrder(store, currEpoch, penaltyTarget, unlockedBalance)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to unlock penalty")
unlockedBalance = big.Sub(unlockedBalance, penaltyFromBalance)
penaltyTotal = big.Sum(penaltyTotal, penaltyFromVesting, penaltyFromBalance)
pledgeDelta = big.Sub(pledgeDelta, penaltyFromVesting)

}
{
// Record faulty power for penalisation of ongoing faults, before popping expirations.
// This includes any power that was just faulted from missing a PoSt.
penaltyTarget := PledgePenaltyForDeclaredFault(epochReward.ThisEpochRewardSmoothed, pwrTotal.QualityAdjPowerSmoothed, deadline.FaultyPower.QA)
penaltyFromVesting, penaltyFromBalance, err := st.PenalizeFundsInPriorityOrder(store, currEpoch, penaltyTarget, unlockedBalance)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to unlock penalty")
unlockedBalance = big.Sub(unlockedBalance, penaltyFromBalance) //nolint:ineffassign
penaltyTotal = big.Sum(penaltyTotal, penaltyFromVesting, penaltyFromBalance)
pledgeDelta = big.Sub(pledgeDelta, penaltyFromVesting)
}
{
// Expire sectors that are due, either for on-time expiration or "early" faulty-for-too-long.
expired, err := deadline.PopExpiredSectors(store, dlInfo.Last(), quant)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load expired sectors")

// Release pledge requirements for the sectors expiring on-time.
// Pledge for the sectors expiring early is retained to support the termination fee that will be assessed
// when the early termination is processed.
pledgeDelta = big.Sub(pledgeDelta, expired.OnTimePledge)
st.AddInitialPledgeRequirement(expired.OnTimePledge.Neg())

// Record reduction in power of the amount of expiring active power.
// Faulty power has already been lost, so the amount expiring can be excluded from the delta.
powerDelta = powerDelta.Sub(expired.ActivePower)

// Record deadlines with early terminations. While this
// bitfield is non-empty, the miner is locked until they
// pay the fee.
noEarlyTerminations, err := expired.EarlySectors.IsEmpty()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to count early terminations")
if !noEarlyTerminations {
st.EarlyTerminations.Set(dlInfo.Index)
}

// The termination fee is paid later, in early-termination queue processing.
// We could charge at least the undeclared fault fee here, which is a lower bound on the penalty.
// https://github.com/filecoin-project/specs-actors/issues/674

// The deals are not terminated yet, that is left for processing of the early termination queue.
}
result, err := st.AdvanceDeadline(store, currEpoch)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to advance deadline")

// Charge detected faults as undeclared.
undeclaredPenalty := PledgePenaltyForUndeclaredFault(
epochReward.ThisEpochRewardSmoothed,
pwrTotal.QualityAdjPowerSmoothed,
result.DetectedFaultyPower.QA,
)
// Charge the rest as declared.
declaredPenalty := PledgePenaltyForDeclaredFault(
epochReward.ThisEpochRewardSmoothed,
pwrTotal.QualityAdjPowerSmoothed,
big.Sub(result.TotalFaultyPower.QA, result.DetectedFaultyPower.QA),
)

// Save new deadline state.
err = deadlines.UpdateDeadline(store, dlInfo.Index, deadline)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to update deadline %d", dlInfo.Index)
powerDeltaTotal = powerDeltaTotal.Add(result.PowerDelta)
pledgeDeltaTotal = big.Add(pledgeDeltaTotal, result.PledgeDelta)

err = st.SaveDeadlines(store, deadlines)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to save deadlines")

// Increment current deadline, and proving period if necessary.
if dlInfo.PeriodStarted() {
st.CurrentDeadline = (st.CurrentDeadline + 1) % WPoStPeriodDeadlines
if st.CurrentDeadline == 0 {
st.ProvingPeriodStart = st.ProvingPeriodStart + WPoStProvingPeriod
penaltyTarget := big.Add(declaredPenalty, undeclaredPenalty)
if !penaltyTarget.IsZero() {
unlockedBalance := st.GetUnlockedBalance(rt.CurrentBalance())
penaltyFromVesting, penaltyFromBalance, err := st.PenalizeFundsInPriorityOrder(store, currEpoch, penaltyTarget, unlockedBalance)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to unlock penalty")
penaltyTotal = big.Sum(penaltyTotal, penaltyFromVesting, penaltyFromBalance)
pledgeDeltaTotal = big.Sub(pledgeDeltaTotal, penaltyFromVesting)
}
}
})

// Remove power for new faults, and burn penalties.
requestUpdatePower(rt, powerDelta)
requestUpdatePower(rt, powerDeltaTotal)
burnFunds(rt, penaltyTotal)
notifyPledgeChanged(rt, pledgeDelta)
notifyPledgeChanged(rt, pledgeDeltaTotal)

// Schedule cron callback for next deadline's last epoch.
newDlInfo := st.DeadlineInfo(currEpoch)
Expand Down
150 changes: 149 additions & 1 deletion actors/builtin/miner/miner_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,11 @@ func (st *State) RepayDebt(currBalance abi.TokenAmount) (abi.TokenAmount, error)
// The soonest-vesting entries are unlocked first.
// Returns the amount actually unlocked.
func (st *State) UnlockUnvestedFunds(store adt.Store, currEpoch abi.ChainEpoch, target abi.TokenAmount) (abi.TokenAmount, error) {
// Nothing to unlock, don't bother loading any state.
if target.IsZero() || st.LockedFunds.IsZero() {
return big.Zero(), nil
}

vestingFunds, err := st.LoadVestingFunds(store)
if err != nil {
return big.Zero(), xerrors.Errorf("failed tp load vesting funds: %w", err)
Expand All @@ -817,6 +822,11 @@ func (st *State) UnlockUnvestedFunds(store adt.Store, currEpoch abi.ChainEpoch,
// Unlocks all vesting funds that have vested before the provided epoch.
// Returns the amount unlocked.
func (st *State) UnlockVestedFunds(store adt.Store, currEpoch abi.ChainEpoch) (abi.TokenAmount, error) {
// Short-circuit to avoid loading vesting funds if we don't have any.
if st.LockedFunds.IsZero() {
return big.Zero(), nil
}

vestingFunds, err := st.LoadVestingFunds(store)
if err != nil {
return big.Zero(), xerrors.Errorf("failed to load vesting funds: %w", err)
Expand Down Expand Up @@ -918,9 +928,27 @@ func (st *State) AddPreCommitExpiry(store adt.Store, expireEpoch abi.ChainEpoch,
return nil
}

func (st *State) checkPrecommitExpiry(store adt.Store, sectors abi.BitField) (depositToBurn abi.TokenAmount, err error) {
func (st *State) ExpirePreCommits(store adt.Store, currEpoch abi.ChainEpoch) (depositToBurn abi.TokenAmount, err error) {
depositToBurn = abi.NewTokenAmount(0)

// expire pre-committed sectors
expiryQ, err := LoadBitfieldQueue(store, st.PreCommittedSectorsExpiry, st.QuantSpecEveryDeadline())
if err != nil {
return depositToBurn, xerrors.Errorf("failed to load sector expiry queue: %w", err)
}

sectors, modified, err := expiryQ.PopUntil(currEpoch)
if err != nil {
return depositToBurn, xerrors.Errorf("failed to pop expired sectors: %w", err)
}

if modified {
st.PreCommittedSectorsExpiry, err = expiryQ.Root()
if err != nil {
return depositToBurn, xerrors.Errorf("failed to save expiry queue: %w", err)
}
}

var precommitsToDelete []abi.SectorNumber
if err = sectors.ForEach(func(i uint64) error {
sectorNo := abi.SectorNumber(i)
Expand Down Expand Up @@ -957,6 +985,126 @@ func (st *State) checkPrecommitExpiry(store adt.Store, sectors abi.BitField) (de
return depositToBurn, nil
}

type AdvanceDeadlineResult struct {
PledgeDelta abi.TokenAmount
PowerDelta PowerPair
DetectedFaultyPower, TotalFaultyPower PowerPair
}

// AdvanceDeadline advances the deadline. It:
// - Processes expired sectors.
// - Handles missed proofs.
// - Returns the changes to power & pledge, and faulty power (both declared and undeclared).
func (st *State) AdvanceDeadline(store adt.Store, currEpoch abi.ChainEpoch) (*AdvanceDeadlineResult, error) {
pledgeDelta := abi.NewTokenAmount(0)
powerDelta := NewPowerPairZero()

detectedFaultyPower := NewPowerPairZero()

// Note: Use dlInfo.Last() rather than rt.CurrEpoch unless certain
// of the desired semantics. In the past, this method would sometimes be
// invoked late due to skipped blocks. This is no longer the case, but
// we still use dlInfo.Last().
dlInfo := st.DeadlineInfo(currEpoch)

// This method is invoked once *before* the first proving period starts,
// after the actor is first constructed; this is detected by
// !dlInfo.PeriodStarted().
if !dlInfo.PeriodStarted() {
return &AdvanceDeadlineResult{
pledgeDelta,
powerDelta,
detectedFaultyPower,
NewPowerPairZero(),
}, nil
}

// Advance to the next deadline (in case we short-circuit below).
st.CurrentDeadline = (st.CurrentDeadline + 1) % WPoStPeriodDeadlines
if st.CurrentDeadline == 0 {
st.ProvingPeriodStart = st.ProvingPeriodStart + WPoStProvingPeriod
}

deadlines, err := st.LoadDeadlines(store)
if err != nil {
return nil, xerrors.Errorf("failed to load deadlines: %w", err)
}
deadline, err := deadlines.LoadDeadline(store, dlInfo.Index)
if err != nil {
return nil, xerrors.Errorf("failed to load deadline %d: %w", dlInfo.Index, err)
}

// No live sectors in this deadline, nothing to do.
if deadline.LiveSectors == 0 {
return &AdvanceDeadlineResult{
pledgeDelta,
powerDelta,
detectedFaultyPower,
deadline.FaultyPower,
}, nil
}

quant := dlInfo.QuantSpec()
{
// Detect and penalize missing proofs.
faultExpiration := dlInfo.Last() + FaultMaxAge

powerDelta, detectedFaultyPower, err = deadline.ProcessDeadlineEnd(store, quant, faultExpiration)
if err != nil {
return nil, xerrors.Errorf("failed to process end of deadline %d: %w", dlInfo.Index, err)
}
}
{
// Expire sectors that are due, either for on-time expiration or "early" faulty-for-too-long.
expired, err := deadline.PopExpiredSectors(store, dlInfo.Last(), quant)
if err != nil {
return nil, xerrors.Errorf("failed to load expired sectors: %w", err)
}

// Release pledge requirements for the sectors expiring on-time.
// Pledge for the sectors expiring early is retained to support the termination fee that will be assessed
// when the early termination is processed.
pledgeDelta = big.Sub(pledgeDelta, expired.OnTimePledge)
st.AddInitialPledgeRequirement(expired.OnTimePledge.Neg())

// Record reduction in power of the amount of expiring active power.
// Faulty power has already been lost, so the amount expiring can be excluded from the delta.
powerDelta = powerDelta.Sub(expired.ActivePower)

// Record deadlines with early terminations. While this
// bitfield is non-empty, the miner is locked until they
// pay the fee.
noEarlyTerminations, err := expired.EarlySectors.IsEmpty()
if err != nil {
return nil, xerrors.Errorf("failed to count early terminations: %w", err)
}
if !noEarlyTerminations {
st.EarlyTerminations.Set(dlInfo.Index)
}
}

// Save new deadline state.
err = deadlines.UpdateDeadline(store, dlInfo.Index, deadline)
if err != nil {
return nil, xerrors.Errorf("failed to update deadline %d: %w", dlInfo.Index, err)
}

err = st.SaveDeadlines(store, deadlines)
if err != nil {
return nil, xerrors.Errorf("failed to save deadlines: %w", err)
}

// Compute penalties all together.
// Be very careful when changing these as any changes can affect
// rounding.
return &AdvanceDeadlineResult{
pledgeDelta,
powerDelta,
detectedFaultyPower,
deadline.FaultyPower,
}, nil
}

func MinerEligibleForElection(store adt.Store, mSt *State, thisEpochReward abi.TokenAmount, minerActorBalance abi.TokenAmount, currEpoch abi.ChainEpoch) (bool, error) {
// IP requirements are met. This includes zero fee debt
if !mSt.MeetsInitialPledgeCondition(minerActorBalance) {
Expand Down
4 changes: 1 addition & 3 deletions actors/builtin/miner/miner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1702,11 +1702,9 @@ func TestDeadlineCron(t *testing.T) {
// Retracted recovery is penalized as an undetected fault, but power is unchanged
retractedPwr := miner.PowerForSectors(actor.sectorSize, allSectors[1:])
retractedPenalty := miner.PledgePenaltyForUndeclaredFault(actor.epochRewardSmooth, actor.epochQAPowerSmooth, retractedPwr.QA)
// subtract ongoing penalty, because it's charged below (this prevents round-off mismatches)
retractedPenalty = big.Sub(retractedPenalty, miner.PledgePenaltyForDeclaredFault(actor.epochRewardSmooth, actor.epochQAPowerSmooth, retractedPwr.QA))

// Un-recovered faults are charged as ongoing faults
ongoingPwr := miner.PowerForSectors(actor.sectorSize, allSectors)
ongoingPwr := miner.PowerForSectors(actor.sectorSize, allSectors[:1])
ongoingPenalty := miner.PledgePenaltyForDeclaredFault(actor.epochRewardSmooth, actor.epochQAPowerSmooth, ongoingPwr.QA)

advanceDeadline(rt, actor, &cronConfig{
Expand Down

0 comments on commit d67d2a7

Please sign in to comment.