diff --git a/builtin/methods.go b/builtin/methods.go index 3cc0c620..050f5e86 100644 --- a/builtin/methods.go +++ b/builtin/methods.go @@ -175,6 +175,7 @@ var MethodsPower = struct { CurrentTotalPowerMinerRawPowerExported abi.MethodNum CurrentTotalPowerMinerCountExported abi.MethodNum CurrentTotalPowerMinerConsensusCountExported abi.MethodNum + MinerPowerExported abi.MethodNum }{ MethodConstructor, 2, @@ -190,6 +191,7 @@ var MethodsPower = struct { MustGenerateFRCMethodNum("MinerRawPower"), MustGenerateFRCMethodNum("MinerCount"), MustGenerateFRCMethodNum("MinerConsensusCount"), + MustGenerateFRCMethodNum("MinerPower"), } var MethodsMiner = struct { @@ -241,9 +243,11 @@ var MethodsMiner = struct { GetPeerIDExported abi.MethodNum GetMultiaddrsExported abi.MethodNum // MovePartitionsExported abi.MethodNum - ProveCommitSectors3 abi.MethodNum - ProveReplicaUpdates3 abi.MethodNum - ProveCommitSectorsNI abi.MethodNum + ProveCommitSectors3 abi.MethodNum + ProveReplicaUpdates3 abi.MethodNum + ProveCommitSectorsNI abi.MethodNum + MaxTerminationFeeExported abi.MethodNum + InitialPledgeExported abi.MethodNum }{ MethodConstructor, 2, @@ -296,6 +300,8 @@ var MethodsMiner = struct { 34, 35, 36, + MustGenerateFRCMethodNum("MaxTerminationFee"), + MustGenerateFRCMethodNum("InitialPledge"), } var MethodsVerifiedRegistry = struct { diff --git a/builtin/v16/gen/gen.go b/builtin/v16/gen/gen.go index 5879a6b2..945aca71 100644 --- a/builtin/v16/gen/gen.go +++ b/builtin/v16/gen/gen.go @@ -105,6 +105,7 @@ func main() { power.MinerRawPowerReturn{}, // other types power.CronEvent{}, + power.MinerPowerReturn{}, ); err != nil { panic(err) } @@ -217,6 +218,7 @@ func main() { miner.SectorClaim{}, miner.SectorNIActivationInfo{}, miner.ProveCommitSectorsNIParams{}, + miner.MaxTerminationFeeParams{}, ); err != nil { panic(err) } diff --git a/builtin/v16/miner/cbor_gen.go b/builtin/v16/miner/cbor_gen.go index b3a1bb4a..e880f94e 100644 --- a/builtin/v16/miner/cbor_gen.go +++ b/builtin/v16/miner/cbor_gen.go @@ -9930,3 +9930,73 @@ func (t *ProveCommitSectorsNIParams) UnmarshalCBOR(r io.Reader) (err error) { } return nil } + +var lengthBufMaxTerminationFeeParams = []byte{130} + +func (t *MaxTerminationFeeParams) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufMaxTerminationFeeParams); err != nil { + return err + } + + // t.Power (big.Int) (struct) + if err := t.Power.MarshalCBOR(cw); err != nil { + return err + } + + // t.InitialPledge (big.Int) (struct) + if err := t.InitialPledge.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *MaxTerminationFeeParams) UnmarshalCBOR(r io.Reader) (err error) { + *t = MaxTerminationFeeParams{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 2 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.Power (big.Int) (struct) + + { + + if err := t.Power.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Power: %w", err) + } + + } + // t.InitialPledge (big.Int) (struct) + + { + + if err := t.InitialPledge.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.InitialPledge: %w", err) + } + + } + return nil +} diff --git a/builtin/v16/miner/miner_types.go b/builtin/v16/miner/miner_types.go index 095a4503..5188a8d3 100644 --- a/builtin/v16/miner/miner_types.go +++ b/builtin/v16/miner/miner_types.go @@ -516,3 +516,12 @@ type ProveCommitSectorsNIParams struct { } type ProveCommitSectorsNIReturn = batch.BatchReturn + +type MaxTerminationFeeParams struct { + Power abi.StoragePower + InitialPledge abi.TokenAmount +} + +type MaxTerminationFeeReturn = abi.TokenAmount + +type InitialPledgeReturn = abi.TokenAmount diff --git a/builtin/v16/miner/monies.go b/builtin/v16/miner/monies.go index 82962159..7549a8ab 100644 --- a/builtin/v16/miner/monies.go +++ b/builtin/v16/miner/monies.go @@ -10,12 +10,12 @@ import ( // Projection period of expected sector block reward for deposit required to pre-commit a sector. // This deposit is lost if the pre-commitment is not timely followed up by a commitment proof. -var PreCommitDepositFactor = 20 // PARAM_SPEC +var PreCommitDepositFactor = 20 var PreCommitDepositProjectionPeriod = abi.ChainEpoch(PreCommitDepositFactor) * builtin.EpochsInDay // Projection period of expected sector block rewards for storage pledge required to commit a sector. // This pledge is lost if a sector is terminated before its full committed lifetime. -var InitialPledgeFactor = 20 // PARAM_SPEC +var InitialPledgeFactor = 20 var InitialPledgeProjectionPeriod = abi.ChainEpoch(InitialPledgeFactor) * builtin.EpochsInDay // Cap on initial pledge requirement for sectors. @@ -26,7 +26,7 @@ var InitialPledgeMaxPerByte = big.Div(big.NewInt(1e18), big.NewInt(32<<30)) // Multiplier of share of circulating money supply for consensus pledge required to commit a sector. // This pledge is lost if a sector is terminated before its full committed lifetime. var InitialPledgeLockTarget = builtin.BigFrac{ - Numerator: big.NewInt(3), // PARAM_SPEC + Numerator: big.NewInt(3), Denominator: big.NewInt(10), } @@ -133,25 +133,60 @@ func InitialPledgeForPower( return big.Min(nominalPledge, pledgeCap) } -var EstimatedSingleProveCommitGasUsage = big.NewInt(49299973) // PARAM_SPEC -var EstimatedSinglePreCommitGasUsage = big.NewInt(16433324) // PARAM_SPEC -var BatchDiscount = builtin.BigFrac{ // PARAM_SPEC - Numerator: big.NewInt(1), - Denominator: big.NewInt(20), +// Maximum number of lifetime days penalized when a sector is terminated. +const TerminationLifetimeCap abi.ChainEpoch = 140 + +// Used to compute termination fees in the base case by multiplying against initial pledge. +var TermFeePledgeMultiple = builtin.BigFrac{ + Numerator: big.NewInt(85), + Denominator: big.NewInt(1000), } -var BatchBalancer = big.Mul(big.NewInt(5), builtin.OneNanoFIL) // PARAM_SPEC -func AggregateProveCommitNetworkFee(aggregateSize int, baseFee abi.TokenAmount) abi.TokenAmount { - return aggregateNetworkFee(aggregateSize, EstimatedSingleProveCommitGasUsage, baseFee) +// Used to ensure the termination fee for young sectors is not arbitrarily low. +var TermFeeMinPledgeMultiple = builtin.BigFrac{ + Numerator: big.NewInt(2), + Denominator: big.NewInt(100), } -func AggregatePreCommitNetworkFee(aggregateSize int, baseFee abi.TokenAmount) abi.TokenAmount { - return aggregateNetworkFee(aggregateSize, EstimatedSinglePreCommitGasUsage, baseFee) +// Used to compute termination fees when the termination fee of a sector is less than the fault fee for the same sector. +var TermFeeMaxFaultFeeMultiple = builtin.BigFrac{ + Numerator: big.NewInt(105), + Denominator: big.NewInt(100), } -func aggregateNetworkFee(aggregateSize int, gasUsage big.Int, baseFee abi.TokenAmount) abi.TokenAmount { - effectiveGasFee := big.Max(baseFee, BatchBalancer) - networkFeeNum := big.Product(effectiveGasFee, gasUsage, big.NewInt(int64(aggregateSize)), BatchDiscount.Numerator) - networkFee := big.Div(networkFeeNum, BatchDiscount.Denominator) - return networkFee +const ContinuedFaultFactorNum = 351 +const ContinuedFaultFactorDenom = 100 +const ContinuedFaultProjectionPeriod abi.ChainEpoch = (builtin.EpochsInDay * ContinuedFaultFactorNum) / ContinuedFaultFactorDenom + +// PledgePenaltyForContinuedFault calculates the penalty for a sector continuing faulty for another +// proving period. +// It is a projection of the expected reward earned by the sector. Also known as "FF(t)" +func PledgePenaltyForContinuedFault(rewardEstimate smoothing.FilterEstimate, networkQaPowerEstimate smoothing.FilterEstimate, qaSectorPower abi.StoragePower) abi.TokenAmount { + return ExpectedRewardForPower(rewardEstimate, networkQaPowerEstimate, qaSectorPower, ContinuedFaultProjectionPeriod) +} + +// PledgePenaltyForTermination Calculates termination fee for a given sector. Normally, it's +// calculated as a fixed percentage of the initial pledge. However, there are some special cases +// outlined in [FIP-0098](https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0098.md). +func PledgePenaltyForTermination( + initialPledge abi.TokenAmount, + sectorAge abi.ChainEpoch, + faultFee abi.TokenAmount, +) abi.TokenAmount { + // Use the Percentage of the initial pledge strategy to determine the termination fee. + simpleTerminationFee := + big.Div(big.Mul(initialPledge, TermFeePledgeMultiple.Numerator), TermFeePledgeMultiple.Denominator) + + durationTerminationFee := + big.Div(big.Mul(big.NewInt(int64(sectorAge)), simpleTerminationFee), big.NewInt(int64(TerminationLifetimeCap*builtin.EpochsInDay))) + + // Apply the age adjustment for young sectors to arrive at the base termination fee. + baseTerminationFee := big.Min(simpleTerminationFee, durationTerminationFee) + + // Calculate the minimum allowed fee (a lower bound on the termination fee) by comparing the absolute minimum termination fee value against the fault fee. Whatever result is Larger sets the lower bound for the termination fee. + minimumFeeAbs := big.Div(big.Mul(initialPledge, TermFeeMinPledgeMultiple.Numerator), TermFeeMinPledgeMultiple.Denominator) + minimumFeeFf := big.Div(big.Mul(faultFee, TermFeeMaxFaultFeeMultiple.Numerator), TermFeeMaxFaultFeeMultiple.Denominator) + minimumFee := big.Max(minimumFeeAbs, minimumFeeFf) + + return big.Max(baseTerminationFee, minimumFee) } diff --git a/builtin/v16/miner/monies_test.go b/builtin/v16/miner/monies_test.go index b54dba9f..21eb40df 100644 --- a/builtin/v16/miner/monies_test.go +++ b/builtin/v16/miner/monies_test.go @@ -1,10 +1,12 @@ package miner_test import ( + "fmt" "testing" abi "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/big" + "github.com/filecoin-project/go-state-types/builtin" "github.com/filecoin-project/go-state-types/builtin/v16/miner" "github.com/filecoin-project/go-state-types/builtin/v16/util/smoothing" ) @@ -176,3 +178,154 @@ func TestInitialPledgeForPowerFip0081(t *testing.T) { }) } } + +func TestNegativeBRClamp(t *testing.T) { + epochTargetReward := big.NewInt(1 << 50) + qaSectorPower := abi.StoragePower(big.NewInt(1 << 36)) + networkQAPower := abi.StoragePower(big.NewInt(1 << 10)) + powerRateOfChange := big.NewInt(1 << 10).Neg() + rewardEstimate := smoothing.FilterEstimate{ + PositionEstimate: abi.TokenAmount(epochTargetReward), + VelocityEstimate: big.Zero(), + } + powerEstimate := smoothing.FilterEstimate{ + PositionEstimate: networkQAPower, + VelocityEstimate: powerRateOfChange, + } + + if big.Add(powerEstimate.PositionEstimate, big.Mul(powerEstimate.VelocityEstimate, big.NewInt(4))).GreaterThan(networkQAPower) { + t.Fatalf("power estimate extrapolated incorrectly") + } + + fourBR := miner.ExpectedRewardForPower(rewardEstimate, powerEstimate, qaSectorPower, 4) + if !fourBR.IsZero() { + t.Fatalf("expected zero BR, got %v", fourBR) + } +} + +func TestZeroPowerMeansZeroFaultPenalty(t *testing.T) { + epochTargetReward := big.NewInt(1 << 50) + zeroQAPower := abi.StoragePower(big.Zero()) + networkQAPower := abi.StoragePower(big.NewInt(1 << 10)) + powerRateOfChange := big.NewInt(1 << 10) + rewardEstimate := smoothing.FilterEstimate{ + PositionEstimate: abi.TokenAmount(epochTargetReward), + VelocityEstimate: big.Zero(), + } + powerEstimate := smoothing.FilterEstimate{ + PositionEstimate: networkQAPower, + VelocityEstimate: powerRateOfChange, + } + + penaltyForZeroPowerFaulted := miner.PledgePenaltyForContinuedFault(rewardEstimate, powerEstimate, zeroQAPower) + if !penaltyForZeroPowerFaulted.IsZero() { + t.Fatalf("expected zero penalty, got %v", penaltyForZeroPowerFaulted) + } +} + +func TestAggregatePowerPledgePenaltyForContinuedFault(t *testing.T) { + epochTargetReward := big.NewInt(1 << 50) + networkQAPower := abi.StoragePower(big.NewInt(1 << 10)) + powerRateOfChange := big.NewInt(1 << 10) + rewardEstimate := smoothing.NewEstimate(abi.TokenAmount(epochTargetReward), big.Zero()) + powerEstimate := smoothing.NewEstimate(networkQAPower, powerRateOfChange) + + testCases := []struct { + sectorMultiple int64 + qaPower abi.StoragePower + }{ + {10, abi.StoragePower(big.NewInt(1 << 6))}, + {10, abi.StoragePower(big.NewInt(1 << 36))}, + {10, abi.StoragePower(big.NewInt(1 << 50))}, + {1000, abi.StoragePower(big.NewInt(1 << 6))}, + {1000, abi.StoragePower(big.NewInt(1 << 36))}, + {1000, abi.StoragePower(big.NewInt(1 << 50))}, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("%d sectors, %s qap", tc.sectorMultiple, tc.qaPower), func(t *testing.T) { + sectorMultiple := tc.sectorMultiple + qaPower := tc.qaPower + + aggregatePenalty := miner.PledgePenaltyForContinuedFault( + rewardEstimate, + powerEstimate, + big.Mul(qaPower, big.NewInt(sectorMultiple)), + ) + + individualPenalties := big.Zero() + for i := int64(0); i < sectorMultiple; i++ { + individualPenalty := miner.PledgePenaltyForContinuedFault(rewardEstimate, powerEstimate, qaPower) + individualPenalties = big.Add(individualPenalties, individualPenalty) + } + if aggregatePenalty.LessThanEqual(big.Zero()) { + t.Fatalf("aggregate penalty is not positive: %s", aggregatePenalty) + } + + diff := big.Sub(aggregatePenalty, individualPenalties).Abs() + allowedAttoDifference := big.NewInt(sectorMultiple) + if diff.GreaterThan(allowedAttoDifference) { + t.Fatalf("aggregate_penalty: %v, individual_penalties: %v, diff: %v", aggregatePenalty, individualPenalties, diff) + } + }) + } +} + +func TestPledgePenaltyForTermination(t *testing.T) { + t.Run("when sector age exceeds cap returns percentage of initial pledge", func(t *testing.T) { + sectorAgeInDays := miner.TerminationLifetimeCap + 1 + sectorAge := sectorAgeInDays * builtin.EpochsInDay + + initialPledge := abi.NewTokenAmount(1 << 10) + faultFee := abi.NewTokenAmount(0) + fee := miner.PledgePenaltyForTermination(initialPledge, sectorAge, faultFee) + + expectedFee := big.Div(big.Mul(initialPledge, miner.TermFeePledgeMultiple.Numerator), miner.TermFeePledgeMultiple.Denominator) + if !fee.Equals(abi.TokenAmount(expectedFee)) { + t.Fatalf("expected fee %v, got %v", expectedFee, fee) + } + }) + + t.Run("when sector age below cap returns percentage of initial pledge percentage", func(t *testing.T) { + sectorAgeInDays := miner.TerminationLifetimeCap / 2 + sectorAge := sectorAgeInDays * builtin.EpochsInDay + + initialPledge := abi.NewTokenAmount(1 << 10) + faultFee := abi.NewTokenAmount(0) + fee := miner.PledgePenaltyForTermination(initialPledge, sectorAge, faultFee) + + simpleTerminationFee := big.Div(big.Mul(initialPledge, miner.TermFeePledgeMultiple.Numerator), miner.TermFeePledgeMultiple.Denominator) + expectedFee := big.Div(big.Mul(simpleTerminationFee, big.NewInt(int64(sectorAgeInDays))), big.NewInt(int64(miner.TerminationLifetimeCap))) + + if !fee.Equals(abi.TokenAmount(expectedFee)) { + t.Fatalf("expected fee %v, got %v", expectedFee, fee) + } + }) + + t.Run("when termination fee less than fault fee returns multiple of fault fee", func(t *testing.T) { + sectorAgeInDays := miner.TerminationLifetimeCap + 1 + sectorAge := sectorAgeInDays * builtin.EpochsInDay + + initialPledge := abi.NewTokenAmount(1 << 10) + faultFee := abi.NewTokenAmount(1 << 10) + fee := miner.PledgePenaltyForTermination(initialPledge, sectorAge, faultFee) + + expectedFee := big.Div(big.Mul(faultFee, miner.TermFeeMaxFaultFeeMultiple.Numerator), miner.TermFeeMaxFaultFeeMultiple.Denominator) + if !fee.Equals(abi.TokenAmount(expectedFee)) { + t.Fatalf("expected fee %v, got %v", expectedFee, fee) + } + }) + + t.Run("when termination fee less than minimum returns minimum", func(t *testing.T) { + sectorAge := abi.ChainEpoch(0) + + initialPledge := abi.NewTokenAmount(1 << 10) + faultFee := abi.NewTokenAmount(0) + fee := miner.PledgePenaltyForTermination(initialPledge, sectorAge, faultFee) + + expectedFee := big.Div(big.Mul(initialPledge, miner.TermFeeMinPledgeMultiple.Numerator), miner.TermFeeMinPledgeMultiple.Denominator) + if !fee.Equals(abi.TokenAmount(expectedFee)) { + t.Fatalf("expected fee %v, got %v", expectedFee, fee) + } + }) +} diff --git a/builtin/v16/power/cbor_gen.go b/builtin/v16/power/cbor_gen.go index 47af6013..b93d205c 100644 --- a/builtin/v16/power/cbor_gen.go +++ b/builtin/v16/power/cbor_gen.go @@ -1588,3 +1588,73 @@ func (t *CronEvent) UnmarshalCBOR(r io.Reader) (err error) { return nil } + +var lengthBufMinerPowerReturn = []byte{130} + +func (t *MinerPowerReturn) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufMinerPowerReturn); err != nil { + return err + } + + // t.RawBytePower (big.Int) (struct) + if err := t.RawBytePower.MarshalCBOR(cw); err != nil { + return err + } + + // t.QualityAdjPower (big.Int) (struct) + if err := t.QualityAdjPower.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *MinerPowerReturn) UnmarshalCBOR(r io.Reader) (err error) { + *t = MinerPowerReturn{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 2 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.RawBytePower (big.Int) (struct) + + { + + if err := t.RawBytePower.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.RawBytePower: %w", err) + } + + } + // t.QualityAdjPower (big.Int) (struct) + + { + + if err := t.QualityAdjPower.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.QualityAdjPower: %w", err) + } + + } + return nil +} diff --git a/builtin/v16/power/power_types.go b/builtin/v16/power/power_types.go index defca6f8..d95cadea 100644 --- a/builtin/v16/power/power_types.go +++ b/builtin/v16/power/power_types.go @@ -63,3 +63,10 @@ type MinerRawPowerReturn struct { type MinerCountReturn = cbg.CborInt type MinerConsensusCountReturn = cbg.CborInt + +type MinerPowerParams = cbg.CborInt // abi.ActorID + +type MinerPowerReturn struct { + RawBytePower abi.StoragePower + QualityAdjPower abi.StoragePower +}