diff --git a/actors/builtin/miner/miner_actor.go b/actors/builtin/miner/miner_actor.go index 1400fc126e..6c2931518d 100644 --- a/actors/builtin/miner/miner_actor.go +++ b/actors/builtin/miner/miner_actor.go @@ -299,6 +299,7 @@ type SubmitWindowedPoStParams struct { func (a Actor) SubmitWindowedPoSt(rt Runtime, params *SubmitWindowedPoStParams) *abi.EmptyValue { currEpoch := rt.CurrEpoch() store := adt.AsStore(rt) + networkVersion := rt.NetworkVersion() var st State if params.Deadline >= WPoStPeriodDeadlines { @@ -394,23 +395,37 @@ func (a Actor) SubmitWindowedPoSt(rt Runtime, params *SubmitWindowedPoStParams) // Penalize new skipped faults and retracted recoveries as undeclared faults. // These pay a higher fee than faults declared before the deadline challenge window opened. undeclaredPenaltyPower := postResult.PenaltyPower() - undeclaredPenaltyTarget := PledgePenaltyForUndeclaredFault( - rewardStats.ThisEpochRewardSmoothed, pwrTotal.QualityAdjPowerSmoothed, undeclaredPenaltyPower.QA, - rt.NetworkVersion(), - ) - // Subtract the "ongoing" fault fee from the amount charged now, since it will be charged at - // the end-of-deadline cron. - undeclaredPenaltyTarget = big.Sub(undeclaredPenaltyTarget, PledgePenaltyForDeclaredFault( - rewardStats.ThisEpochRewardSmoothed, pwrTotal.QualityAdjPowerSmoothed, undeclaredPenaltyPower.QA, - )) + undeclaredPenaltyTarget := big.Zero() + if networkVersion >= network.Version3 { + // From version 3, skipped faults and retracted recoveries pay nothing at Window PoSt, + // but will incur the "ongoing" fault fee at deadline end. + } else { + undeclaredPenaltyTarget = PledgePenaltyForUndeclaredFault( + rewardStats.ThisEpochRewardSmoothed, pwrTotal.QualityAdjPowerSmoothed, undeclaredPenaltyPower.QA, + networkVersion, + ) + // Subtract the "ongoing" fault fee from the amount charged now, since it will be charged at + // the end-of-deadline cron. + undeclaredPenaltyTarget = big.Sub(undeclaredPenaltyTarget, PledgePenaltyForDeclaredFault( + rewardStats.ThisEpochRewardSmoothed, pwrTotal.QualityAdjPowerSmoothed, undeclaredPenaltyPower.QA, + networkVersion, + )) + } // Penalize recoveries as declared faults (a lower fee than the undeclared, above). // It sounds odd, but because faults are penalized in arrears, at the _end_ of the faulty period, we must // penalize recovered sectors here because they won't be penalized by the end-of-deadline cron for the // immediately-prior faulty period. - declaredPenaltyTarget := PledgePenaltyForDeclaredFault( - rewardStats.ThisEpochRewardSmoothed, pwrTotal.QualityAdjPowerSmoothed, postResult.RecoveredPower.QA, - ) + declaredPenaltyTarget := big.Zero() + if networkVersion >= network.Version3 { + // From version 3, recovered sectors pay no penalty. + // They won't pay anything at deadline end either, since they'll no longer be faulty. + } else { + declaredPenaltyTarget = PledgePenaltyForDeclaredFault( + rewardStats.ThisEpochRewardSmoothed, pwrTotal.QualityAdjPowerSmoothed, postResult.RecoveredPower.QA, + networkVersion, + ) + } // Note: We could delay this charge until end of deadline, but that would require more accounting state. totalPenaltyTarget := big.Add(undeclaredPenaltyTarget, declaredPenaltyTarget) @@ -1570,6 +1585,7 @@ func processEarlyTerminations(rt Runtime) (more bool) { func handleProvingDeadline(rt Runtime) { currEpoch := rt.CurrEpoch() store := adt.AsStore(rt) + networkVersion := rt.NetworkVersion() epochReward := requestCurrentEpochBlockReward(rt) pwrTotal := requestCurrentTotalPower(rt) @@ -1630,33 +1646,47 @@ func handleProvingDeadline(rt Runtime) { quant := QuantSpecForDeadline(dlInfo) unlockedBalance := st.GetUnlockedBalance(rt.CurrentBalance()) + // Remember power that was faulty before processing any missed PoSts. + previouslyFaultyPower := deadline.FaultyPower.QA + { // Detect and penalize missing proofs. faultExpiration := dlInfo.Last() + FaultMaxAge - penalizePowerTotal := big.Zero() newFaultyPower, failedRecoveryPower, err := deadline.ProcessDeadlineEnd(store, quant, faultExpiration) builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to process end of deadline %d", dlInfo.Index) powerDelta = powerDelta.Sub(newFaultyPower) - penalizePowerTotal = big.Sum(penalizePowerTotal, newFaultyPower.QA, failedRecoveryPower.QA) - - // Unlock sector penalty for all undeclared faults. - penaltyTarget := PledgePenaltyForUndeclaredFault(epochReward.ThisEpochRewardSmoothed, pwrTotal.QualityAdjPowerSmoothed, - penalizePowerTotal, rt.NetworkVersion()) - // 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, penalizePowerTotal)) - 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) + if networkVersion >= network.Version3 { + // From network version 3, faults detected from a missed PoSt pay nothing. + // Failed recoveries pay nothing here, but will pay the ongoing fault fee in the subsequent block. + } else { + penalizePowerTotal := big.Add(newFaultyPower.QA, failedRecoveryPower.QA) + + // Unlock sector penalty for all undeclared faults. + penaltyTarget := PledgePenaltyForUndeclaredFault(epochReward.ThisEpochRewardSmoothed, pwrTotal.QualityAdjPowerSmoothed, + penalizePowerTotal, rt.NetworkVersion()) + // 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, penalizePowerTotal, networkVersion)) + 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) + ongoingFaultyPower := deadline.FaultyPower.QA + if networkVersion >= network.Version3 { + // From network version 3, this *excludes* any power that was just faulted from missing a PoSt. + ongoingFaultyPower = previouslyFaultyPower + } + penaltyTarget := PledgePenaltyForDeclaredFault(epochReward.ThisEpochRewardSmoothed, + pwrTotal.QualityAdjPowerSmoothed, ongoingFaultyPower, networkVersion) 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 diff --git a/actors/builtin/miner/miner_internal_test.go b/actors/builtin/miner/miner_internal_test.go index 72998f6771..c38bb8a176 100644 --- a/actors/builtin/miner/miner_internal_test.go +++ b/actors/builtin/miner/miner_internal_test.go @@ -108,7 +108,7 @@ func TestFaultFeeInvariants(t *testing.T) { t.Run("Undeclared faults are more expensive than declared faults", func(t *testing.T) { faultySectorPower := abi.NewStoragePower(1 << 50) - ff := PledgePenaltyForDeclaredFault(rewardEstimate, powerEstimate, faultySectorPower) + ff := PledgePenaltyForDeclaredFault(rewardEstimate, powerEstimate, faultySectorPower, nv) sp := PledgePenaltyForUndeclaredFault(rewardEstimate, powerEstimate, faultySectorPower, nv) assert.True(t, sp.GreaterThan(ff)) }) @@ -163,11 +163,11 @@ func TestFaultFeeInvariants(t *testing.T) { totalFaultPower := big.Add(big.Add(faultySectorAPower, faultySectorBPower), faultySectorCPower) // Declared faults - ffA := PledgePenaltyForDeclaredFault(rewardEstimate, powerEstimate, faultySectorAPower) - ffB := PledgePenaltyForDeclaredFault(rewardEstimate, powerEstimate, faultySectorBPower) - ffC := PledgePenaltyForDeclaredFault(rewardEstimate, powerEstimate, faultySectorCPower) + ffA := PledgePenaltyForDeclaredFault(rewardEstimate, powerEstimate, faultySectorAPower, nv) + ffB := PledgePenaltyForDeclaredFault(rewardEstimate, powerEstimate, faultySectorBPower, nv) + ffC := PledgePenaltyForDeclaredFault(rewardEstimate, powerEstimate, faultySectorCPower, nv) - ffAll := PledgePenaltyForDeclaredFault(rewardEstimate, powerEstimate, totalFaultPower) + ffAll := PledgePenaltyForDeclaredFault(rewardEstimate, powerEstimate, totalFaultPower, nv) // Because we can introduce rounding error between 1 and zero for every penalty calculation // we can at best expect n calculations of 1 power to be within n of 1 calculation of n powers. diff --git a/actors/builtin/miner/miner_test.go b/actors/builtin/miner/miner_test.go index 6baca81548..b7ab693edd 100644 --- a/actors/builtin/miner/miner_test.go +++ b/actors/builtin/miner/miner_test.go @@ -929,7 +929,7 @@ func TestWindowPost(t *testing.T) { // Now submit PoSt // Power should return for recovered sector. // Recovery should be charged ongoing fee. - recoveryFee := actor.declaredFaultPenalty(infos) + recoveryFee := actor.declaredFaultPenalty(infos, rt.NetworkVersion()) cfg := &poStConfig{ expectedRawPowerDelta: pwr.Raw, expectedQAPowerDelta: pwr.QA, @@ -981,7 +981,7 @@ func TestWindowPost(t *testing.T) { // Fee for skipped fault is undeclared fault fee, but it is split into the ongoing fault fee // which is charged at next cron and the rest which is charged during submit PoSt. undeclaredFee := actor.undeclaredFaultPenalty(infos[:1], rt.NetworkVersion()) - declaredFee := actor.declaredFaultPenalty(infos[:1]) + declaredFee := actor.declaredFaultPenalty(infos[:1], rt.NetworkVersion()) faultFee := big.Sub(undeclaredFee, declaredFee) pwr := miner.PowerForSectors(actor.sectorSize, infos[:1]) @@ -1005,7 +1005,7 @@ func TestWindowPost(t *testing.T) { // skip second fault undeclaredFee = actor.undeclaredFaultPenalty(infos[1:], rt.NetworkVersion()) - declaredFee = actor.declaredFaultPenalty(infos[1:]) + declaredFee = actor.declaredFaultPenalty(infos[1:], rt.NetworkVersion()) faultFee = big.Sub(undeclaredFee, declaredFee) pwr = miner.PowerForSectors(actor.sectorSize, infos[1:]) @@ -1020,7 +1020,7 @@ func TestWindowPost(t *testing.T) { actor.submitWindowPoSt(rt, dlinfo, partitions, infos, cfg) // expect ongoing fault from both sectors - advanceDeadline(rt, actor, &cronConfig{ongoingFaultsPenalty: actor.declaredFaultPenalty(infos)}) + advanceDeadline(rt, actor, &cronConfig{ongoingFaultsPenalty: actor.declaredFaultPenalty(infos, rt.NetworkVersion())}) }) t.Run("skipped all sectors in a deadline may be skipped", func(t *testing.T) { @@ -1051,7 +1051,7 @@ func TestWindowPost(t *testing.T) { // Fee for skipped fault is undeclared fault fee, but it is split into the ongoing fault fee // which is charged at next cron and the rest which is charged during submit PoSt. undeclaredFee := actor.undeclaredFaultPenalty(infos, rt.NetworkVersion()) - declaredFee := actor.declaredFaultPenalty(infos) + declaredFee := actor.declaredFaultPenalty(infos, rt.NetworkVersion()) faultFee := big.Sub(undeclaredFee, declaredFee) pwr := miner.PowerForSectors(actor.sectorSize, infos) @@ -1105,7 +1105,7 @@ func TestWindowPost(t *testing.T) { // Now submit PoSt and skip recovered sector // No power should be returned // Retracted recovery will be charged difference between undeclared and ongoing fault fees - ongoingFee := actor.declaredFaultPenalty(infos) + ongoingFee := actor.declaredFaultPenalty(infos, rt.NetworkVersion()) recoveryFee := big.Sub(actor.undeclaredFaultPenalty(infos, rt.NetworkVersion()), ongoingFee) cfg := &poStConfig{ expectedRawPowerDelta: big.Zero(), @@ -1313,11 +1313,12 @@ func TestDeadlineCron(t *testing.T) { retractedPwr := miner.PowerForSectors(actor.sectorSize, allSectors[1:]) retractedPenalty := miner.PledgePenaltyForUndeclaredFault(actor.epochRewardSmooth, actor.epochQAPowerSmooth, retractedPwr.QA, rt.NetworkVersion()) // 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)) + retractedPenalty = big.Sub(retractedPenalty, + miner.PledgePenaltyForDeclaredFault(actor.epochRewardSmooth, actor.epochQAPowerSmooth, retractedPwr.QA, rt.NetworkVersion())) // Un-recovered faults are charged as ongoing faults ongoingPwr := miner.PowerForSectors(actor.sectorSize, allSectors) - ongoingPenalty := miner.PledgePenaltyForDeclaredFault(actor.epochRewardSmooth, actor.epochQAPowerSmooth, ongoingPwr.QA) + ongoingPenalty := miner.PledgePenaltyForDeclaredFault(actor.epochRewardSmooth, actor.epochQAPowerSmooth, ongoingPwr.QA, rt.NetworkVersion()) advanceDeadline(rt, actor, &cronConfig{ detectedFaultsPenalty: retractedPenalty, @@ -1413,7 +1414,7 @@ func TestDeclareFaults(t *testing.T) { // faults are charged at ongoing rate and no additional power is removed ongoingPwr := miner.PowerForSectors(actor.sectorSize, allSectors) - ongoingPenalty := miner.PledgePenaltyForDeclaredFault(actor.epochRewardSmooth, actor.epochQAPowerSmooth, ongoingPwr.QA) + ongoingPenalty := miner.PledgePenaltyForDeclaredFault(actor.epochRewardSmooth, actor.epochQAPowerSmooth, ongoingPwr.QA, rt.NetworkVersion()) advanceDeadline(rt, actor, &cronConfig{ ongoingFaultsPenalty: ongoingPenalty, @@ -3193,9 +3194,9 @@ func (h *actorHarness) withdrawFunds(rt *mock.Runtime, amount abi.TokenAmount) { rt.Verify() } -func (h *actorHarness) declaredFaultPenalty(sectors []*miner.SectorOnChainInfo) abi.TokenAmount { +func (h *actorHarness) declaredFaultPenalty(sectors []*miner.SectorOnChainInfo, nv network.Version) abi.TokenAmount { _, qa := powerForSectors(h.sectorSize, sectors) - return miner.PledgePenaltyForDeclaredFault(h.epochRewardSmooth, h.epochQAPowerSmooth, qa) + return miner.PledgePenaltyForDeclaredFault(h.epochRewardSmooth, h.epochQAPowerSmooth, qa, nv) } func (h *actorHarness) undeclaredFaultPenalty(sectors []*miner.SectorOnChainInfo, nv network.Version) abi.TokenAmount { diff --git a/actors/builtin/miner/monies.go b/actors/builtin/miner/monies.go index cb3a2f2174..bf0d346f15 100644 --- a/actors/builtin/miner/monies.go +++ b/actors/builtin/miner/monies.go @@ -29,9 +29,11 @@ var SpaceRaceInitialPledgeMaxPerByte = big.Div(big.NewInt(1e18), big.NewInt(32 < // FF = BR(t, DeclaredFaultProjectionPeriod) // projection period of 2.14 days: 2880 * 2.14 = 6163.2. Rounded to nearest epoch 6163 -var DeclaredFaultFactorNum = 214 +var DeclaredFaultFactorNumV0 = 214 +var DeclaredFaultFactorNumV3 = 351 var DeclaredFaultFactorDenom = 100 -var DeclaredFaultProjectionPeriod = abi.ChainEpoch((builtin.EpochsInDay * DeclaredFaultFactorNum) / DeclaredFaultFactorDenom) +var DeclaredFaultProjectionPeriodV0 = abi.ChainEpoch((builtin.EpochsInDay * DeclaredFaultFactorNumV0) / DeclaredFaultFactorDenom) +var DeclaredFaultProjectionPeriodV3 = abi.ChainEpoch((builtin.EpochsInDay * DeclaredFaultFactorNumV3) / DeclaredFaultFactorDenom) // SP = BR(t, UndeclaredFaultProjectionPeriod) var UndeclaredFaultFactorNumV0 = 50 @@ -60,8 +62,13 @@ func ExpectedRewardForPower(rewardEstimate, networkQAPowerEstimate *smoothing.Fi // This is the FF(t) penalty for a sector expected to be in the fault state either because the fault was declared or because // it has been previously detected by the network. // FF(t) = DeclaredFaultFactor * BR(t) -func PledgePenaltyForDeclaredFault(rewardEstimate, networkQAPowerEstimate *smoothing.FilterEstimate, qaSectorPower abi.StoragePower) abi.TokenAmount { - return ExpectedRewardForPower(rewardEstimate, networkQAPowerEstimate, qaSectorPower, DeclaredFaultProjectionPeriod) +func PledgePenaltyForDeclaredFault(rewardEstimate, networkQAPowerEstimate *smoothing.FilterEstimate, qaSectorPower abi.StoragePower, + networkVersion network.Version) abi.TokenAmount { + projectionPeriod := DeclaredFaultProjectionPeriodV0 + if networkVersion >= network.Version3 { + projectionPeriod = DeclaredFaultProjectionPeriodV3 + } + return ExpectedRewardForPower(rewardEstimate, networkQAPowerEstimate, qaSectorPower, projectionPeriod) } // This is the SP(t) penalty for a newly faulty sector that has not been declared.