diff --git a/actors/builtin/methods.go b/actors/builtin/methods.go index 4de1e2b99..fe2dd71f8 100644 --- a/actors/builtin/methods.go +++ b/actors/builtin/methods.go @@ -100,7 +100,8 @@ var MethodsMiner = struct { RepayDebt abi.MethodNum ChangeOwnerAddress abi.MethodNum DisputeWindowedPoSt abi.MethodNum -}{MethodConstructor, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24} + PreCommitSectorBatch abi.MethodNum +}{MethodConstructor, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25} var MethodsVerifiedRegistry = struct { Constructor abi.MethodNum diff --git a/actors/builtin/miner/cbor_gen.go b/actors/builtin/miner/cbor_gen.go index 1331bc224..ca526bbf7 100644 --- a/actors/builtin/miner/cbor_gen.go +++ b/actors/builtin/miner/cbor_gen.go @@ -8,6 +8,7 @@ import ( address "github.com/filecoin-project/go-address" abi "github.com/filecoin-project/go-state-types/abi" + miner "github.com/filecoin-project/specs-actors/actors/builtin/miner" proof "github.com/filecoin-project/specs-actors/actors/runtime/proof" cid "github.com/ipfs/go-cid" cbg "github.com/whyrusleeping/cbor-gen" @@ -2481,3 +2482,82 @@ func (t *WindowedPoSt) UnmarshalCBOR(r io.Reader) error { return nil } + +var lengthBufPreCommitSectorBatchParams = []byte{129} + +func (t *PreCommitSectorBatchParams) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + if _, err := w.Write(lengthBufPreCommitSectorBatchParams); err != nil { + return err + } + + scratch := make([]byte, 9) + + // t.Sectors ([]*miner.SectorPreCommitInfo) (slice) + if len(t.Sectors) > cbg.MaxLength { + return xerrors.Errorf("Slice value in field t.Sectors was too long") + } + + if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajArray, uint64(len(t.Sectors))); err != nil { + return err + } + for _, v := range t.Sectors { + if err := v.MarshalCBOR(w); err != nil { + return err + } + } + return nil +} + +func (t *PreCommitSectorBatchParams) UnmarshalCBOR(r io.Reader) error { + *t = PreCommitSectorBatchParams{} + + br := cbg.GetPeeker(r) + scratch := make([]byte, 8) + + maj, extra, err := cbg.CborReadHeaderBuf(br, scratch) + if err != nil { + return err + } + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 1 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.Sectors ([]*miner.SectorPreCommitInfo) (slice) + + maj, extra, err = cbg.CborReadHeaderBuf(br, scratch) + if err != nil { + return err + } + + if extra > cbg.MaxLength { + return fmt.Errorf("t.Sectors: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Sectors = make([]*miner.SectorPreCommitInfo, extra) + } + + for i := 0; i < int(extra); i++ { + + var v miner.SectorPreCommitInfo + if err := v.UnmarshalCBOR(br); err != nil { + return err + } + + t.Sectors[i] = &v + } + + return nil +} diff --git a/actors/builtin/miner/miner_actor.go b/actors/builtin/miner/miner_actor.go index dd866024a..3913c2f8c 100644 --- a/actors/builtin/miner/miner_actor.go +++ b/actors/builtin/miner/miner_actor.go @@ -74,6 +74,7 @@ func (a Actor) Exports() []interface{} { 22: a.RepayDebt, 23: a.ChangeOwnerAddress, 24: a.DisputeWindowedPoSt, + 25: a.PreCommitSectorBatch, } } @@ -643,65 +644,103 @@ func (a Actor) DisputeWindowedPoSt(rt Runtime, params *DisputeWindowedPoStParams //} type PreCommitSectorParams = miner0.SectorPreCommitInfo -// Proposals must be posted on chain via sma.PublishStorageDeals before PreCommitSector. -// Optimization: PreCommitSector could contain a list of deals that are not published yet. +// Pledges to seal and commit a single sector. +// See PreCommitSectorBatch for details. +// This method may be deprecated and removed in the future. func (a Actor) PreCommitSector(rt Runtime, params *PreCommitSectorParams) *abi.EmptyValue { + // This is a direct method call to self, not a message send. + batchParams := &PreCommitSectorBatchParams{Sectors: []*miner0.SectorPreCommitInfo{params}} + a.PreCommitSectorBatch(rt, batchParams) + return nil +} + +type PreCommitSectorBatchParams struct { + Sectors []*miner0.SectorPreCommitInfo +} + +// Pledges the miner to seal and commit some new sectors. +// The caller specifies sector numbers, sealed sector data CIDs, seal randomness epoch, expiration, and the IDs +// of any storage marge deals contained in the sector data. The storage deal proposals must be already submitted +// to the storage market actor. +// A pre-commitment may specify an existing committed-capacity sector that the committed sector will replace +// when proven. +// This method calculates the sector's power, locks a pre-commit deposit for the sector, stores information about the +// sector in state and waits for it to be proven or expire. +func (a Actor) PreCommitSectorBatch(rt Runtime, params *PreCommitSectorBatchParams) *abi.EmptyValue { nv := rt.NetworkVersion() - if !CanPreCommitSealProof(params.SealProof, nv) { - rt.Abortf(exitcode.ErrIllegalArgument, "unsupported seal proof type %v at network version %v", params.SealProof, nv) - } - if params.SectorNumber > abi.MaxSectorNumber { - rt.Abortf(exitcode.ErrIllegalArgument, "sector number %d out of range 0..(2^63-1)", params.SectorNumber) - } - if !params.SealedCID.Defined() { - rt.Abortf(exitcode.ErrIllegalArgument, "sealed CID undefined") - } - if params.SealedCID.Prefix() != SealedCIDPrefix { - rt.Abortf(exitcode.ErrIllegalArgument, "sealed CID had wrong prefix") - } - if params.SealRandEpoch >= rt.CurrEpoch() { - rt.Abortf(exitcode.ErrIllegalArgument, "seal challenge epoch %v must be before now %v", params.SealRandEpoch, rt.CurrEpoch()) - } + currEpoch := rt.CurrEpoch() + if len(params.Sectors) == 0 { + rt.Abortf(exitcode.ErrIllegalArgument, "batch empty") + } else if len(params.Sectors) > PreCommitSectorBatchMaxSize { + rt.Abortf(exitcode.ErrIllegalArgument, "batch of %d too large, max %d", len(params.Sectors), PreCommitSectorBatchMaxSize) + } + + // Check per-sector preconditions before opening state transaction or sending other messages. + challengeEarliest := currEpoch - MaxPreCommitRandomnessLookback + sectorsDeals := make([]market.SectorDeals, len(params.Sectors)) + sectorNumbers := bitfield.New() + for i, precommit := range params.Sectors { + // Bitfied.IsSet() is fast when there are only locally-set values. + set, err := sectorNumbers.IsSet(uint64(precommit.SectorNumber)) + builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "error checking sector number") + if set { + rt.Abortf(exitcode.ErrIllegalArgument, "duplicate sector number %d", precommit.SectorNumber) + } + sectorNumbers.Set(uint64(precommit.SectorNumber)) - challengeEarliest := rt.CurrEpoch() - MaxPreCommitRandomnessLookback - if params.SealRandEpoch < challengeEarliest { - rt.Abortf(exitcode.ErrIllegalArgument, "seal challenge epoch %v too old, must be after %v", params.SealRandEpoch, challengeEarliest) - } + if !CanPreCommitSealProof(precommit.SealProof, nv) { + rt.Abortf(exitcode.ErrIllegalArgument, "unsupported seal proof type %v at network version %v", precommit.SealProof, nv) + } + if precommit.SectorNumber > abi.MaxSectorNumber { + rt.Abortf(exitcode.ErrIllegalArgument, "sector number %d out of range 0..(2^63-1)", precommit.SectorNumber) + } + if !precommit.SealedCID.Defined() { + rt.Abortf(exitcode.ErrIllegalArgument, "sealed CID undefined") + } + if precommit.SealedCID.Prefix() != SealedCIDPrefix { + rt.Abortf(exitcode.ErrIllegalArgument, "sealed CID had wrong prefix") + } + if precommit.SealRandEpoch >= currEpoch { + rt.Abortf(exitcode.ErrIllegalArgument, "seal challenge epoch %v must be before now %v", precommit.SealRandEpoch, rt.CurrEpoch()) + } + if precommit.SealRandEpoch < challengeEarliest { + rt.Abortf(exitcode.ErrIllegalArgument, "seal challenge epoch %v too old, must be after %v", precommit.SealRandEpoch, challengeEarliest) + } - // Require sector lifetime meets minimum by assuming activation happens at last epoch permitted for seal proof. - // This could make sector maximum lifetime validation more lenient if the maximum sector limit isn't hit first. - maxActivation := rt.CurrEpoch() + MaxProveCommitDuration[params.SealProof] - validateExpiration(rt, maxActivation, params.Expiration, params.SealProof) + // Require sector lifetime meets minimum by assuming activation happens at last epoch permitted for seal proof. + // This could make sector maximum lifetime validation more lenient if the maximum sector limit isn't hit first. + maxActivation := currEpoch + MaxProveCommitDuration[precommit.SealProof] + validateExpiration(rt, maxActivation, precommit.Expiration, precommit.SealProof) - if params.ReplaceCapacity && len(params.DealIDs) == 0 { - rt.Abortf(exitcode.ErrIllegalArgument, "cannot replace sector without committing deals") - } - if params.ReplaceSectorDeadline >= WPoStPeriodDeadlines { - rt.Abortf(exitcode.ErrIllegalArgument, "invalid deadline %d", params.ReplaceSectorDeadline) - } - if params.ReplaceSectorNumber > abi.MaxSectorNumber { - rt.Abortf(exitcode.ErrIllegalArgument, "invalid sector number %d", params.ReplaceSectorNumber) + if precommit.ReplaceCapacity && len(precommit.DealIDs) == 0 { + rt.Abortf(exitcode.ErrIllegalArgument, "cannot replace sector without committing deals") + } + if precommit.ReplaceSectorDeadline >= WPoStPeriodDeadlines { + rt.Abortf(exitcode.ErrIllegalArgument, "invalid deadline %d", precommit.ReplaceSectorDeadline) + } + if precommit.ReplaceSectorNumber > abi.MaxSectorNumber { + rt.Abortf(exitcode.ErrIllegalArgument, "invalid sector number %d", precommit.ReplaceSectorNumber) + } + + sectorsDeals[i] = market.SectorDeals{ + SectorExpiry: precommit.Expiration, + DealIDs: precommit.DealIDs, + } } // gather information from other actors - rewardStats := requestCurrentEpochBlockReward(rt) pwrTotal := requestCurrentTotalPower(rt) - dealWeights := requestDealWeights(rt, []market.SectorDeals{ - { - SectorExpiry: params.Expiration, - DealIDs: params.DealIDs, - }, - }) - if len(dealWeights.Sectors) == 0 { - rt.Abortf(exitcode.ErrIllegalState, "deal weight request returned no records") + dealWeights := requestDealWeights(rt, sectorsDeals) + + if len(dealWeights.Sectors) != len(params.Sectors) { + rt.Abortf(exitcode.ErrIllegalState, "deal weight request returned %d records, expected %d", + len(dealWeights.Sectors), len(params.Sectors)) } - dealWeight := dealWeights.Sectors[0] store := adt.AsStore(rt) var st State var err error - newlyVested := big.Zero() feeToBurn := abi.NewTokenAmount(0) var needsCron bool rt.StateTransaction(&st, func() { @@ -715,93 +754,98 @@ func (a Actor) PreCommitSector(rt Runtime, params *PreCommitSectorParams) *abi.E info := getMinerInfo(rt, &st) rt.ValidateImmediateCallerIs(append(info.ControlAddresses, info.Owner, info.Worker)...) - if ConsensusFaultActive(info, rt.CurrEpoch()) { - rt.Abortf(exitcode.ErrForbidden, "precommit not allowed during active consensus fault") - } - - // From network version 7, the pre-commit seal type must have the same Window PoSt proof type as the miner, - // rather than be exactly the same seal type. - // This permits a transition window from V1 to V1_1 seal types (which share Window PoSt proof type). - sectorWPoStProof, err := params.SealProof.RegisteredWindowPoStProof() - builtin.RequireNoErr(rt, err, exitcode.ErrIllegalArgument, "failed to lookup Window PoSt proof type for sector seal proof %d", params.SealProof) - if sectorWPoStProof != info.WindowPoStProofType { - rt.Abortf(exitcode.ErrIllegalArgument, "sector Window PoSt proof type %d must match miner Window PoSt proof type %d (seal proof type %d)", - sectorWPoStProof, info.WindowPoStProofType, params.SealProof) + if ConsensusFaultActive(info, currEpoch) { + rt.Abortf(exitcode.ErrForbidden, "pre-commit not allowed during active consensus fault") } + chainInfos := make([]*SectorPreCommitOnChainInfo, len(params.Sectors)) + totalDepositRequired := big.Zero() + expirations := map[abi.ChainEpoch][]uint64{} dealCountMax := SectorDealsMax(info.SectorSize) - if uint64(len(params.DealIDs)) > dealCountMax { - rt.Abortf(exitcode.ErrIllegalArgument, "too many deals for sector %d > %d", len(params.DealIDs), dealCountMax) - } + for i, precommit := range params.Sectors { + // Sector must have the same Window PoSt proof type as the miner's recorded seal type. + sectorWPoStProof, err := precommit.SealProof.RegisteredWindowPoStProof() + builtin.RequireNoErr(rt, err, exitcode.ErrIllegalArgument, "failed to lookup Window PoSt proof type for sector seal proof %d", precommit.SealProof) + if sectorWPoStProof != info.WindowPoStProofType { + rt.Abortf(exitcode.ErrIllegalArgument, "sector Window PoSt proof type %d must match miner Window PoSt proof type %d (seal proof type %d)", + sectorWPoStProof, info.WindowPoStProofType, precommit.SealProof) + } - // Ensure total deal space does not exceed sector size. - if dealWeight.DealSpace > uint64(info.SectorSize) { - rt.Abortf(exitcode.ErrIllegalArgument, "deals too large to fit in sector %d > %d", dealWeight.DealSpace, info.SectorSize) - } + if uint64(len(precommit.DealIDs)) > dealCountMax { + rt.Abortf(exitcode.ErrIllegalArgument, "too many deals for sector %d > %d", len(precommit.DealIDs), dealCountMax) + } - err = st.AllocateSectorNumbers(store, bitfield.NewFromSet([]uint64{uint64(params.SectorNumber)}), DenyCollisions) - builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to allocate sector id %d", params.SectorNumber) + // Ensure total deal space does not exceed sector size. + dealWeight := dealWeights.Sectors[i] + if dealWeight.DealSpace > uint64(info.SectorSize) { + rt.Abortf(exitcode.ErrIllegalArgument, "deals too large to fit in sector %d > %d", dealWeight.DealSpace, info.SectorSize) + } - // This sector check is redundant given the allocated sectors bitfield, but remains for safety. - sectorFound, err := st.HasSectorNo(store, params.SectorNumber) - builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to check sector %v", params.SectorNumber) - if sectorFound { - rt.Abortf(exitcode.ErrIllegalState, "sector %v already committed", params.SectorNumber) - } + if precommit.ReplaceCapacity { + validateReplaceSector(rt, &st, store, precommit) + } - if params.ReplaceCapacity { - validateReplaceSector(rt, &st, store, params) - } + // Estimate the sector weight using the current epoch as an estimate for activation, + // and compute the pre-commit deposit using that weight. + // The sector's power will be recalculated when it's proven. + duration := precommit.Expiration - currEpoch + sectorWeight := QAPowerForWeight(info.SectorSize, duration, dealWeight.DealWeight, dealWeight.VerifiedDealWeight) + depositReq := PreCommitDepositForPower(rewardStats.ThisEpochRewardSmoothed, pwrTotal.QualityAdjPowerSmoothed, sectorWeight) + + // Build on-chain record. + chainInfos[i] = &SectorPreCommitOnChainInfo{ + Info: SectorPreCommitInfo(*precommit), + PreCommitDeposit: depositReq, + PreCommitEpoch: currEpoch, + DealWeight: dealWeight.DealWeight, + VerifiedDealWeight: dealWeight.VerifiedDealWeight, + } + totalDepositRequired = big.Add(totalDepositRequired, depositReq) - duration := params.Expiration - rt.CurrEpoch() - sectorWeight := QAPowerForWeight(info.SectorSize, duration, dealWeight.DealWeight, dealWeight.VerifiedDealWeight) - depositReq := PreCommitDepositForPower(rewardStats.ThisEpochRewardSmoothed, pwrTotal.QualityAdjPowerSmoothed, sectorWeight) - if availableBalance.LessThan(depositReq) { - rt.Abortf(exitcode.ErrInsufficientFunds, "insufficient funds for pre-commit deposit: %v", depositReq) + // Calculate pre-commit expiry + msd, ok := MaxProveCommitDuration[precommit.SealProof] + if !ok { + rt.Abortf(exitcode.ErrIllegalArgument, "no max seal duration set for proof type: %d", precommit.SealProof) + } + // The +1 here is critical for the batch verification of proofs. Without it, if a proof arrived exactly on the + // due epoch, ProveCommitSector would accept it, then the expiry event would remove it, and then + // ConfirmSectorProofsValid would fail to find it. + expiryBound := currEpoch + msd + 1 + expirations[expiryBound] = append(expirations[expiryBound], uint64(precommit.SectorNumber)) } - err = st.AddPreCommitDeposit(depositReq) - builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to add pre-commit deposit %v", depositReq) - - if err := st.PutPrecommittedSectors(store, &SectorPreCommitOnChainInfo{ - Info: SectorPreCommitInfo(*params), - PreCommitDeposit: depositReq, - PreCommitEpoch: rt.CurrEpoch(), - DealWeight: dealWeight.DealWeight, - VerifiedDealWeight: dealWeight.VerifiedDealWeight, - }); err != nil { - rt.Abortf(exitcode.ErrIllegalState, "failed to write pre-committed sector %v: %v", params.SectorNumber, err) - } - // add precommit expiry to the queue - msd, ok := MaxProveCommitDuration[params.SealProof] - if !ok { - rt.Abortf(exitcode.ErrIllegalArgument, "no max seal duration set for proof type: %d", params.SealProof) + // Batch update actor state. + if availableBalance.LessThan(totalDepositRequired) { + rt.Abortf(exitcode.ErrInsufficientFunds, "insufficient funds %v for pre-commit deposit: %v", availableBalance, totalDepositRequired) } - // The +1 here is critical for the batch verification of proofs. Without it, if a proof arrived exactly on the - // due epoch, ProveCommitSector would accept it, then the expiry event would remove it, and then - // ConfirmSectorProofsValid would fail to find it. - expiryBound := rt.CurrEpoch() + msd + 1 + err = st.AddPreCommitDeposit(totalDepositRequired) + builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to add pre-commit deposit %v", totalDepositRequired) + + err = st.AllocateSectorNumbers(store, sectorNumbers, DenyCollisions) + builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to allocate sector ids %v", sectorNumbers) - err = st.AddPreCommitExpirations(store, map[abi.ChainEpoch][]uint64{expiryBound: {uint64(params.SectorNumber)}}) + err = st.PutPrecommittedSectors(store, chainInfos...) + builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to write pre-committed sectors") + + err = st.AddPreCommitExpirations(store, expirations) builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to add pre-commit expiry to queue") - // activate miner cron + // Activate miner cron needsCron = !st.DeadlineCronActive st.DeadlineCronActive = true }) + burnFunds(rt, feeToBurn) rt.StateReadonly(&st) err = st.CheckBalanceInvariants(rt.CurrentBalance()) builtin.RequireNoErr(rt, err, ErrBalanceInvariantBroken, "balance invariants broken") if needsCron { - newDlInfo := st.DeadlineInfo(rt.CurrEpoch()) + newDlInfo := st.DeadlineInfo(currEpoch) enrollCronEvent(rt, newDlInfo.Last(), &CronEventPayload{ EventType: CronEventProvingDeadline, }) } - notifyPledgeChanged(rt, newlyVested.Neg()) - return nil } @@ -2149,7 +2193,7 @@ func validateExpiration(rt Runtime, activation, expiration abi.ChainEpoch, sealP } } -func validateReplaceSector(rt Runtime, st *State, store adt.Store, params *PreCommitSectorParams) { +func validateReplaceSector(rt Runtime, st *State, store adt.Store, params *miner0.SectorPreCommitInfo) { replaceSector, found, err := st.GetSector(store, params.ReplaceSectorNumber) builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load sector %v", params.SectorNumber) if !found { diff --git a/actors/builtin/miner/miner_commitment_test.go b/actors/builtin/miner/miner_commitment_test.go index 6839265f1..bb7599ef2 100644 --- a/actors/builtin/miner/miner_commitment_test.go +++ b/actors/builtin/miner/miner_commitment_test.go @@ -9,7 +9,9 @@ import ( "github.com/filecoin-project/go-state-types/dline" "github.com/filecoin-project/go-state-types/exitcode" "github.com/filecoin-project/go-state-types/network" + miner0 "github.com/filecoin-project/specs-actors/actors/builtin/miner" "github.com/filecoin-project/specs-actors/v5/actors/builtin" + "github.com/filecoin-project/specs-actors/v5/actors/builtin/market" "github.com/filecoin-project/specs-actors/v5/actors/builtin/miner" "github.com/filecoin-project/specs-actors/v5/actors/runtime" "github.com/filecoin-project/specs-actors/v5/actors/util/smoothing" @@ -23,6 +25,95 @@ import ( func TestCommitments(t *testing.T) { periodOffset := abi.ChainEpoch(100) + // Simple pre-commits + for _, test := range []struct { + name string + sectorNo abi.SectorNumber + dealSize uint64 + verifiedDealSize uint64 + dealIds []abi.DealID + }{{ + name: "no deals", + sectorNo: 0, + dealSize: 0, + verifiedDealSize: 0, + dealIds: nil, + }, { + name: "max sector number", + sectorNo: abi.MaxSectorNumber, + dealSize: 0, + verifiedDealSize: 0, + dealIds: nil, + }, { + name: "unverified deal", + sectorNo: 100, + dealSize: 32 << 30, + verifiedDealSize: 0, + dealIds: []abi.DealID{1}, + }, { + name: "verified deal", + sectorNo: 100, + dealSize: 0, + verifiedDealSize: 32 << 30, + dealIds: []abi.DealID{1}, + }, { + name: "two deals", + sectorNo: 100, + dealSize: 16 << 30, + verifiedDealSize: 16 << 30, + dealIds: []abi.DealID{1, 2}, + }, + } { + t.Run(test.name, func(t *testing.T) { + actor := newHarness(t, periodOffset) + rt := builderForHarness(actor). + WithBalance(bigBalance, big.Zero()). + Build(t) + precommitEpoch := periodOffset + 1 + rt.SetEpoch(precommitEpoch) + actor.constructAndVerify(rt) + dlInfo := actor.deadline(rt) + + expiration := dlInfo.PeriodEnd() + defaultSectorExpiration*miner.WPoStProvingPeriod // on deadline boundary but > 180 days + proveCommitEpoch := precommitEpoch + miner.PreCommitChallengeDelay + 1 + dealLifespan := expiration - proveCommitEpoch + dealSpace := test.dealSize + test.verifiedDealSize + dealWeight := big.Mul(big.NewIntUnsigned(test.dealSize), big.NewInt(int64(dealLifespan))) + verifiedDealWeight := big.Mul(big.NewIntUnsigned(test.verifiedDealSize), big.NewInt(int64(dealLifespan))) + + precommitParams := actor.makePreCommit(test.sectorNo, precommitEpoch-1, expiration, test.dealIds) + precommit := actor.preCommitSector(rt, precommitParams, preCommitConf{ + dealWeight: dealWeight, + verifiedDealWeight: verifiedDealWeight, + dealSpace: abi.SectorSize(dealSpace), + }, true) + + // Check precommit expectations. + assert.Equal(t, precommitEpoch, precommit.PreCommitEpoch) + assert.Equal(t, dealWeight, precommit.DealWeight) + assert.Equal(t, verifiedDealWeight, precommit.VerifiedDealWeight) + + assert.Equal(t, test.sectorNo, precommit.Info.SectorNumber) + assert.Equal(t, precommitParams.SealProof, precommit.Info.SealProof) + assert.Equal(t, precommitParams.SealedCID, precommit.Info.SealedCID) + assert.Equal(t, precommitParams.SealRandEpoch, precommit.Info.SealRandEpoch) + assert.Equal(t, precommitParams.DealIDs, precommit.Info.DealIDs) + assert.Equal(t, precommitParams.Expiration, precommit.Info.Expiration) + + pwrEstimate := miner.QAPowerForWeight(actor.sectorSize, precommit.Info.Expiration-precommitEpoch, dealWeight, verifiedDealWeight) + expectedDeposit := miner.PreCommitDepositForPower(actor.epochRewardSmooth, actor.epochQAPowerSmooth, pwrEstimate) + assert.Equal(t, expectedDeposit, precommit.PreCommitDeposit) + + st := getState(rt) + assert.True(t, expectedDeposit.GreaterThan(big.Zero())) + assert.Equal(t, expectedDeposit, st.PreCommitDeposits) + + expirations := actor.collectPrecommitExpirations(rt, st) + expectedPrecommitExpiration := st.QuantSpecEveryDeadline().QuantizeUp(precommitEpoch + miner.MaxProveCommitDuration[actor.sealProofType] + 1) + assert.Equal(t, map[abi.ChainEpoch][]uint64{expectedPrecommitExpiration: {uint64(test.sectorNo)}}, expirations) + }) + } + t.Run("insufficient funds for pre-commit", func(t *testing.T) { actor := newHarness(t, periodOffset) insufficientBalance := abi.NewTokenAmount(10) // 10 AttoFIL @@ -177,6 +268,17 @@ func TestCommitments(t *testing.T) { }) rt.Reset() + // Deals too large for sector + dealWeight := big.Mul(big.NewIntUnsigned(32<<30), big.NewInt(int64(expiration-rt.Epoch()))) + rt.ExpectAbortContainsMessage(exitcode.ErrIllegalArgument, "deals too large", func() { + actor.preCommitSector(rt, actor.makePreCommit(0, challengeEpoch, expiration, []abi.DealID{1}), preCommitConf{ + dealWeight: dealWeight, + verifiedDealWeight: big.Zero(), + dealSpace: 32<<30 + 1, + }, false) + }) + rt.Reset() + // Try to precommit while in fee debt with insufficient balance st = getState(rt) st.FeeDebt = big.Add(rt.Balance(), abi.NewTokenAmount(1e18)) @@ -191,13 +293,12 @@ func TestCommitments(t *testing.T) { // Try to precommit with an active consensus fault st = getState(rt) - actor.reportConsensusFault(rt, addr.TestAddress, &runtime.ConsensusFault{ Target: actor.receiver, Epoch: rt.Epoch() - 1, Type: runtime.ConsensusFaultDoubleForkMining, }) - rt.ExpectAbortContainsMessage(exitcode.ErrForbidden, "precommit not allowed during active consensus fault", func() { + rt.ExpectAbortContainsMessage(exitcode.ErrForbidden, "active consensus fault", func() { actor.preCommitSector(rt, actor.makePreCommit(102, challengeEpoch, expiration, nil), preCommitConf{}, false) }) // reset state back to normal @@ -227,9 +328,7 @@ func TestCommitments(t *testing.T) { return ids } - // Make a good commitment for the proof to target. sectorNo := abi.SectorNumber(100) - dealLimits := map[abi.RegisteredSealProof]int{ abi.RegisteredSealProof_StackedDrg2KiBV1_1: 256, abi.RegisteredSealProof_StackedDrg32GiBV1_1: 256, @@ -237,7 +336,7 @@ func TestCommitments(t *testing.T) { } for proof, limit := range dealLimits { - // attempt to pre-commmit a sector with too many sectors + // attempt to pre-commmit a sector with too many deals rt, actor, deadline := setup(proof) expiration := deadline.PeriodEnd() + defaultSectorExpiration*miner.WPoStProvingPeriod precommit := actor.makePreCommit(sectorNo, rt.Epoch()-1, expiration, makeDealIDs(limit+1)) @@ -285,15 +384,13 @@ func TestCommitments(t *testing.T) { }) for _, test := range []struct { - name string - version network.Version - expectedPledgeDelta abi.TokenAmount - sealProofType abi.RegisteredSealProof + name string + version network.Version + sealProofType abi.RegisteredSealProof }{{ - name: "precommit does not vest funds in version 8", - version: network.Version8, - expectedPledgeDelta: abi.NewTokenAmount(0), - sealProofType: abi.RegisteredSealProof_StackedDrg32GiBV1_1, + name: "precommit does not vest funds in version 8", + version: network.Version8, + sealProofType: abi.RegisteredSealProof_StackedDrg32GiBV1_1, }} { t.Run(test.name, func(t *testing.T) { actor := newHarness(t, periodOffset) @@ -326,20 +423,220 @@ func TestCommitments(t *testing.T) { // Pre-commit with a deal in order to exercise non-zero deal weights. precommitParams := actor.makePreCommit(sectorNo, precommitEpoch-1, expiration, []abi.DealID{1}) - actor.preCommitSector(rt, precommitParams, preCommitConf{ - pledgeDelta: &test.expectedPledgeDelta, - }, true) + actor.preCommitSector(rt, precommitParams, preCommitConf{}, true) }) } } +func TestPreCommitBatch(t *testing.T) { + periodOffset := abi.ChainEpoch(100) + type dealSpec struct { + size uint64 + verifiedSize uint64 + IDs []abi.DealID + } + + // Simple batches + for _, test := range []struct { + name string + batchSize int + balanceSurplus abi.TokenAmount + deals []dealSpec + exit exitcode.ExitCode + error string + }{{ + name: "one sector", + batchSize: 1, + balanceSurplus: big.Zero(), + }, { + name: "max sectors", + batchSize: 32, + balanceSurplus: big.Zero(), + }, { + name: "one deal", + batchSize: 3, + balanceSurplus: big.Zero(), + deals: []dealSpec{{ + size: 32 << 30, + verifiedSize: 0, + IDs: []abi.DealID{1}, + }}, + }, { + name: "many deals", + batchSize: 3, + balanceSurplus: big.Zero(), + deals: []dealSpec{{ + size: 32 << 30, + verifiedSize: 0, + IDs: []abi.DealID{1}, + }, { + size: 0, + verifiedSize: 32 << 30, + IDs: []abi.DealID{1}, + }, { + size: 16 << 30, + verifiedSize: 16 << 30, + IDs: []abi.DealID{1, 2}, + }}, + }, { + name: "empty batch", + batchSize: 0, + balanceSurplus: big.Zero(), + exit: exitcode.ErrIllegalArgument, + error: "batch empty", + }, { + name: "too many sectors", + batchSize: 33, + balanceSurplus: big.Zero(), + exit: exitcode.ErrIllegalArgument, + error: "batch of 33 too large", + }, { + name: "insufficient balance", + batchSize: 10, + balanceSurplus: abi.NewTokenAmount(1).Neg(), + exit: exitcode.ErrInsufficientFunds, + error: "insufficient funds", + }} { + t.Run(test.name, func(t *testing.T) { + actor := newHarness(t, periodOffset) + rt := builderForHarness(actor). + Build(t) + precommitEpoch := periodOffset + 1 + rt.SetEpoch(precommitEpoch) + actor.constructAndVerify(rt) + dlInfo := actor.deadline(rt) + + batchSize := test.batchSize + sectorNos := make([]abi.SectorNumber, batchSize) + sectorNoAsUints := make([]uint64, batchSize) + for i := 0; i < batchSize; i++ { + sectorNos[i] = abi.SectorNumber(100 + i) + sectorNoAsUints[i] = uint64(100 + i) + } + sectorExpiration := dlInfo.PeriodEnd() + defaultSectorExpiration*miner.WPoStProvingPeriod // on deadline boundary but > 180 days + proveCommitEpoch := precommitEpoch + miner.PreCommitChallengeDelay + 1 + dealLifespan := sectorExpiration - proveCommitEpoch + + sectors := make([]*miner0.SectorPreCommitInfo, batchSize) + conf := preCommitBatchConf{ + sectorWeights: make([]market.SectorWeights, batchSize), + } + deposits := make([]big.Int, batchSize) + for i := 0; i < batchSize; i++ { + deals := dealSpec{} + if len(test.deals) > i { + deals = test.deals[i] + } + sectors[i] = actor.makePreCommit(sectorNos[i], precommitEpoch-1, sectorExpiration, deals.IDs) + + dealSpace := deals.size + deals.verifiedSize + dealWeight := big.Mul(big.NewIntUnsigned(deals.size), big.NewInt(int64(dealLifespan))) + verifiedDealWeight := big.Mul(big.NewIntUnsigned(deals.verifiedSize), big.NewInt(int64(dealLifespan))) + conf.sectorWeights[i] = market.SectorWeights{ + DealSpace: dealSpace, + DealWeight: dealWeight, + VerifiedDealWeight: verifiedDealWeight, + } + pwrEstimate := miner.QAPowerForWeight(actor.sectorSize, sectors[i].Expiration-precommitEpoch, dealWeight, verifiedDealWeight) + deposits[i] = miner.PreCommitDepositForPower(actor.epochRewardSmooth, actor.epochQAPowerSmooth, pwrEstimate) + } + totalDeposit := big.Sum(deposits...) + rt.SetBalance(big.Add(totalDeposit, test.balanceSurplus)) + + if test.exit != exitcode.Ok { + rt.ExpectAbortContainsMessage(test.exit, test.error, func() { + actor.preCommitSectorBatch(rt, &miner.PreCommitSectorBatchParams{Sectors: sectors}, conf, true) + + // State untouched. + st := getState(rt) + assert.True(t, st.PreCommitDeposits.IsZero()) + expirations := actor.collectPrecommitExpirations(rt, st) + assert.Equal(t, map[abi.ChainEpoch][]uint64{}, expirations) + }) + return + } + precommits := actor.preCommitSectorBatch(rt, &miner.PreCommitSectorBatchParams{Sectors: sectors}, conf, true) + + // Check precommits + st := getState(rt) + for i := 0; i < batchSize; i++ { + assert.Equal(t, precommitEpoch, precommits[i].PreCommitEpoch) + assert.Equal(t, conf.sectorWeights[i].DealWeight, precommits[i].DealWeight) + assert.Equal(t, conf.sectorWeights[i].VerifiedDealWeight, precommits[i].VerifiedDealWeight) + + assert.Equal(t, sectorNos[i], precommits[i].Info.SectorNumber) + assert.Equal(t, sectors[i].SealProof, precommits[i].Info.SealProof) + assert.Equal(t, sectors[i].SealedCID, precommits[i].Info.SealedCID) + assert.Equal(t, sectors[i].SealRandEpoch, precommits[i].Info.SealRandEpoch) + assert.Equal(t, sectors[i].DealIDs, precommits[i].Info.DealIDs) + assert.Equal(t, sectors[i].Expiration, precommits[i].Info.Expiration) + + pwrEstimate := miner.QAPowerForWeight(actor.sectorSize, precommits[i].Info.Expiration-precommitEpoch, + conf.sectorWeights[i].DealWeight, conf.sectorWeights[i].VerifiedDealWeight) + expectedDeposit := miner.PreCommitDepositForPower(actor.epochRewardSmooth, actor.epochQAPowerSmooth, pwrEstimate) + assert.Equal(t, expectedDeposit, precommits[i].PreCommitDeposit) + } + + assert.True(t, totalDeposit.GreaterThan(big.Zero())) + assert.Equal(t, totalDeposit, st.PreCommitDeposits) + + expirations := actor.collectPrecommitExpirations(rt, st) + expectedPrecommitExpiration := st.QuantSpecEveryDeadline().QuantizeUp(precommitEpoch + miner.MaxProveCommitDuration[actor.sealProofType] + 1) + assert.Equal(t, map[abi.ChainEpoch][]uint64{expectedPrecommitExpiration: sectorNoAsUints}, expirations) + }) + } + + t.Run("one bad apple ruins batch", func(t *testing.T) { + // This test does not enumerate all the individual conditions that could cause a single precommit + // to be rejected. Those are covered in the PreCommitSector tests, and we know that that + // method is implemented in terms of a batch of one. + actor := newHarness(t, periodOffset) + rt := builderForHarness(actor). + WithBalance(bigBalance, big.Zero()). + Build(t) + precommitEpoch := periodOffset + 1 + rt.SetEpoch(precommitEpoch) + actor.constructAndVerify(rt) + dlInfo := actor.deadline(rt) + + sectorExpiration := dlInfo.PeriodEnd() + defaultSectorExpiration*miner.WPoStProvingPeriod + sectors := []*miner0.SectorPreCommitInfo{ + actor.makePreCommit(100, precommitEpoch-1, sectorExpiration, nil), + actor.makePreCommit(101, precommitEpoch-1, sectorExpiration, nil), + actor.makePreCommit(102, precommitEpoch-1, rt.Epoch(), nil), // Expires too soon + } + + rt.ExpectAbortContainsMessage(exitcode.ErrIllegalArgument, "sector expiration", func() { + actor.preCommitSectorBatch(rt, &miner.PreCommitSectorBatchParams{Sectors: sectors}, preCommitBatchConf{}, true) + }) + }) + + t.Run("duplicate sector rejects batch", func(t *testing.T) { + actor := newHarness(t, periodOffset) + rt := builderForHarness(actor). + WithBalance(bigBalance, big.Zero()). + Build(t) + precommitEpoch := periodOffset + 1 + rt.SetEpoch(precommitEpoch) + actor.constructAndVerify(rt) + dlInfo := actor.deadline(rt) + + sectorExpiration := dlInfo.PeriodEnd() + defaultSectorExpiration*miner.WPoStProvingPeriod + sectors := []*miner0.SectorPreCommitInfo{ + actor.makePreCommit(100, precommitEpoch-1, sectorExpiration, nil), + actor.makePreCommit(101, precommitEpoch-1, sectorExpiration, nil), + actor.makePreCommit(100, precommitEpoch-1, sectorExpiration, nil), + } + rt.ExpectAbortContainsMessage(exitcode.ErrIllegalArgument, "duplicate sector number 100", func() { + actor.preCommitSectorBatch(rt, &miner.PreCommitSectorBatchParams{Sectors: sectors}, preCommitBatchConf{}, true) + }) + }) +} + func TestProveCommit(t *testing.T) { periodOffset := abi.ChainEpoch(100) - actor := newHarness(t, periodOffset) - builder := builderForHarness(actor). - WithBalance(bigBalance, big.Zero()) - t.Run("valid precommit then provecommit", func(t *testing.T) { + t.Run("prove single sector", func(t *testing.T) { actor := newHarness(t, periodOffset) rt := builderForHarness(actor). WithBalance(bigBalance, big.Zero()). @@ -366,16 +663,14 @@ func TestProveCommit(t *testing.T) { verifiedDealWeight: verifiedDealWeight, }, true) - // assert precommit exists and meets expectations - onChainPrecommit := actor.getPreCommit(rt, sectorNo) - + // Check precommit // deal weights must be set in precommit onchain info - assert.Equal(t, dealWeight, onChainPrecommit.DealWeight) - assert.Equal(t, verifiedDealWeight, onChainPrecommit.VerifiedDealWeight) + assert.Equal(t, dealWeight, precommit.DealWeight) + assert.Equal(t, verifiedDealWeight, precommit.VerifiedDealWeight) - pwrEstimate := miner.QAPowerForWeight(actor.sectorSize, precommit.Info.Expiration-precommitEpoch, onChainPrecommit.DealWeight, onChainPrecommit.VerifiedDealWeight) + pwrEstimate := miner.QAPowerForWeight(actor.sectorSize, precommit.Info.Expiration-precommitEpoch, precommit.DealWeight, precommit.VerifiedDealWeight) expectedDeposit := miner.PreCommitDepositForPower(actor.epochRewardSmooth, actor.epochQAPowerSmooth, pwrEstimate) - assert.Equal(t, expectedDeposit, onChainPrecommit.PreCommitDeposit) + assert.Equal(t, expectedDeposit, precommit.PreCommitDeposit) // expect total precommit deposit to equal our new deposit st := getState(rt) @@ -384,7 +679,15 @@ func TestProveCommit(t *testing.T) { // run prove commit logic rt.SetEpoch(proveCommitEpoch) rt.SetBalance(big.Mul(big.NewInt(1000), big.NewInt(1e18))) - actor.proveCommitSectorAndConfirm(rt, precommit, makeProveCommit(sectorNo), proveCommitConf{}) + sector := actor.proveCommitSectorAndConfirm(rt, precommit, makeProveCommit(sectorNo), proveCommitConf{}) + + assert.Equal(t, precommit.Info.SealProof, sector.SealProof) + assert.Equal(t, precommit.Info.SealedCID, sector.SealedCID) + assert.Equal(t, precommit.Info.DealIDs, sector.DealIDs) + assert.Equal(t, rt.Epoch(), sector.Activation) + assert.Equal(t, precommit.Info.Expiration, sector.Expiration) + assert.Equal(t, precommit.DealWeight, sector.DealWeight) + assert.Equal(t, precommit.VerifiedDealWeight, sector.VerifiedDealWeight) // expect precommit to have been removed st = getState(rt) @@ -397,27 +700,17 @@ func TestProveCommit(t *testing.T) { // The sector is exactly full with verified deals, so expect fully verified power. expectedPower := big.Mul(big.NewInt(int64(actor.sectorSize)), big.Div(builtin.VerifiedDealWeightMultiplier, builtin.QualityBaseMultiplier)) - qaPower := miner.QAPowerForWeight(actor.sectorSize, precommit.Info.Expiration-rt.Epoch(), onChainPrecommit.DealWeight, onChainPrecommit.VerifiedDealWeight) + qaPower := miner.QAPowerForWeight(actor.sectorSize, precommit.Info.Expiration-rt.Epoch(), precommit.DealWeight, precommit.VerifiedDealWeight) assert.Equal(t, expectedPower, qaPower) - expectedInitialPledge := miner.InitialPledgeForPower(qaPower, actor.baselinePower, actor.epochRewardSmooth, - actor.epochQAPowerSmooth, rt.TotalFilCircSupply()) - assert.Equal(t, expectedInitialPledge, st.InitialPledge) - - // expect new onchain sector - sector := actor.getSector(rt, sectorNo) sectorPower := miner.NewPowerPair(big.NewIntUnsigned(uint64(actor.sectorSize)), qaPower) // expect deal weights to be transferred to on chain info - assert.Equal(t, onChainPrecommit.DealWeight, sector.DealWeight) - assert.Equal(t, onChainPrecommit.VerifiedDealWeight, sector.VerifiedDealWeight) - - // expect activation epoch to be current epoch - assert.Equal(t, rt.Epoch(), sector.Activation) + assert.Equal(t, precommit.DealWeight, sector.DealWeight) + assert.Equal(t, precommit.VerifiedDealWeight, sector.VerifiedDealWeight) - // expect initial plege of sector to be set + // expect initial plege of sector to be set, and be total pledge requirement + expectedInitialPledge := miner.InitialPledgeForPower(qaPower, actor.baselinePower, actor.epochRewardSmooth, actor.epochQAPowerSmooth, rt.TotalFilCircSupply()) assert.Equal(t, expectedInitialPledge, sector.InitialPledge) - - // expect locked initial pledge of sector to be the same as pledge requirement assert.Equal(t, expectedInitialPledge, st.InitialPledge) // expect sector to be assigned a deadline/partition @@ -454,6 +747,96 @@ func TestProveCommit(t *testing.T) { assert.Equal(t, miner.NewPowerPairZero(), entry.FaultyPower) }) + t.Run("prove sectors from batch pre-commit", func(t *testing.T) { + actor := newHarness(t, periodOffset) + rt := builderForHarness(actor). + WithBalance(bigBalance, big.Zero()). + Build(t) + precommitEpoch := periodOffset + 1 + rt.SetEpoch(precommitEpoch) + actor.constructAndVerify(rt) + dlInfo := actor.deadline(rt) + + sectorExpiration := dlInfo.PeriodEnd() + defaultSectorExpiration*miner.WPoStProvingPeriod + + sectors := []*miner0.SectorPreCommitInfo{ + actor.makePreCommit(100, precommitEpoch-1, sectorExpiration, nil), + actor.makePreCommit(101, precommitEpoch-1, sectorExpiration, []abi.DealID{1}), // 1 * 32GiB verified deal + actor.makePreCommit(102, precommitEpoch-1, sectorExpiration, []abi.DealID{2, 3}), // 2 * 16GiB verified deals + } + + dealSpace := uint64(32 << 30) + dealWeight := big.Zero() + proveCommitEpoch := precommitEpoch + miner.PreCommitChallengeDelay + 1 + dealLifespan := sectorExpiration - proveCommitEpoch + verifiedDealWeight := big.Mul(big.NewIntUnsigned(dealSpace), big.NewInt(int64(dealLifespan))) + + // Power estimates made a pre-commit time + noDealPowerEstimate := miner.QAPowerForWeight(actor.sectorSize, sectorExpiration-precommitEpoch, big.Zero(), big.Zero()) + fullDealPowerEstimate := miner.QAPowerForWeight(actor.sectorSize, sectorExpiration-precommitEpoch, dealWeight, verifiedDealWeight) + + deposits := []big.Int{ + miner.PreCommitDepositForPower(actor.epochRewardSmooth, actor.epochQAPowerSmooth, noDealPowerEstimate), + miner.PreCommitDepositForPower(actor.epochRewardSmooth, actor.epochQAPowerSmooth, fullDealPowerEstimate), + miner.PreCommitDepositForPower(actor.epochRewardSmooth, actor.epochQAPowerSmooth, fullDealPowerEstimate), + } + conf := preCommitBatchConf{ + sectorWeights: []market.SectorWeights{ + {DealSpace: 0, DealWeight: big.Zero(), VerifiedDealWeight: big.Zero()}, + {DealSpace: dealSpace, DealWeight: dealWeight, VerifiedDealWeight: verifiedDealWeight}, + {DealSpace: dealSpace, DealWeight: dealWeight, VerifiedDealWeight: verifiedDealWeight}, + }, + } + + precommits := actor.preCommitSectorBatch(rt, &miner.PreCommitSectorBatchParams{Sectors: sectors}, conf, true) + + rt.SetEpoch(proveCommitEpoch) + noDealPower := miner.QAPowerForWeight(actor.sectorSize, sectorExpiration-proveCommitEpoch, big.Zero(), big.Zero()) + noDealPledge := miner.InitialPledgeForPower(noDealPower, actor.baselinePower, actor.epochRewardSmooth, actor.epochQAPowerSmooth, rt.TotalFilCircSupply()) + fullDealPower := miner.QAPowerForWeight(actor.sectorSize, sectorExpiration-proveCommitEpoch, dealWeight, verifiedDealWeight) + assert.Equal(t, big.Mul(big.NewInt(int64(actor.sectorSize)), big.Div(builtin.VerifiedDealWeightMultiplier, builtin.QualityBaseMultiplier)), fullDealPower) + fullDealPledge := miner.InitialPledgeForPower(fullDealPower, actor.baselinePower, actor.epochRewardSmooth, actor.epochQAPowerSmooth, rt.TotalFilCircSupply()) + + // Prove just the first sector, with no deals + { + precommit := precommits[0] + sector := actor.proveCommitSectorAndConfirm(rt, precommit, makeProveCommit(precommit.Info.SectorNumber), proveCommitConf{}) + assert.Equal(t, rt.Epoch(), sector.Activation) + st := getState(rt) + expectedDeposit := big.Sum(deposits[1:]...) // First sector deposit released + assert.Equal(t, expectedDeposit, st.PreCommitDeposits) + + // Expect power/pledge for a sector with no deals + assert.Equal(t, noDealPledge, sector.InitialPledge) + assert.Equal(t, noDealPledge, st.InitialPledge) + } + // Prove the next, with one deal + { + precommit := precommits[1] + sector := actor.proveCommitSectorAndConfirm(rt, precommit, makeProveCommit(precommit.Info.SectorNumber), proveCommitConf{}) + assert.Equal(t, rt.Epoch(), sector.Activation) + st := getState(rt) + expectedDeposit := big.Sum(deposits[2:]...) // First and second sector deposits released + assert.Equal(t, expectedDeposit, st.PreCommitDeposits) + + // Expect power/pledge for the two sectors (only this one having any deal weight) + assert.Equal(t, fullDealPledge, sector.InitialPledge) + assert.Equal(t, big.Add(noDealPledge, fullDealPledge), st.InitialPledge) + } + // Prove the last + { + precommit := precommits[2] + sector := actor.proveCommitSectorAndConfirm(rt, precommit, makeProveCommit(precommit.Info.SectorNumber), proveCommitConf{}) + assert.Equal(t, rt.Epoch(), sector.Activation) + st := getState(rt) + assert.Equal(t, big.Zero(), st.PreCommitDeposits) + + // Expect power/pledge for the three sectors + assert.Equal(t, fullDealPledge, sector.InitialPledge) + assert.Equal(t, big.Sum(noDealPledge, fullDealPledge, fullDealPledge), st.InitialPledge) + } + }) + t.Run("invalid proof rejected", func(t *testing.T) { actor := newHarness(t, periodOffset) rt := builderForHarness(actor). @@ -528,7 +911,10 @@ func TestProveCommit(t *testing.T) { }) t.Run("prove commit aborts if pledge requirement not met", func(t *testing.T) { - rt := builder.Build(t) + actor := newHarness(t, periodOffset) + rt := builderForHarness(actor). + WithBalance(bigBalance, big.Zero()). + Build(t) actor.constructAndVerify(rt) // Set the circulating supply high and expected reward low in order to coerce // pledge requirements (BR + share of money supply, but capped at 1FIL) @@ -566,7 +952,10 @@ func TestProveCommit(t *testing.T) { }) t.Run("drop invalid prove commit while processing valid one", func(t *testing.T) { - rt := builder.Build(t) + actor := newHarness(t, periodOffset) + rt := builderForHarness(actor). + WithBalance(bigBalance, big.Zero()). + Build(t) actor.constructAndVerify(rt) // make two precommits diff --git a/actors/builtin/miner/miner_state_test.go b/actors/builtin/miner/miner_state_test.go index 95a59d9d3..2de5a1f8a 100644 --- a/actors/builtin/miner/miner_state_test.go +++ b/actors/builtin/miner/miner_state_test.go @@ -1017,11 +1017,6 @@ func newPreCommitOnChain(sectorNo abi.SectorNumber, sealed cid.Cid, deposit abi. } } -const ( - sectorSealRandEpochValue = abi.ChainEpoch(1) - sectorExpiration = abi.ChainEpoch(1) -) - // returns a unique SectorOnChainInfo with each invocation with SectorNumber set to `sectorNo`. func newSectorOnChainInfo(sectorNo abi.SectorNumber, sealed cid.Cid, weight big.Int, activation abi.ChainEpoch) *miner.SectorOnChainInfo { return &miner.SectorOnChainInfo{ @@ -1030,7 +1025,7 @@ func newSectorOnChainInfo(sectorNo abi.SectorNumber, sealed cid.Cid, weight big. SealedCID: sealed, DealIDs: nil, Activation: activation, - Expiration: sectorExpiration, + Expiration: abi.ChainEpoch(1), DealWeight: weight, VerifiedDealWeight: weight, InitialPledge: abi.NewTokenAmount(0), @@ -1047,8 +1042,8 @@ func newSectorPreCommitInfo(sectorNo abi.SectorNumber, sealed cid.Cid) *miner.Se SealProof: abi.RegisteredSealProof_StackedDrg32GiBV1_1, SectorNumber: sectorNo, SealedCID: sealed, - SealRandEpoch: sectorSealRandEpochValue, + SealRandEpoch: abi.ChainEpoch(1), DealIDs: nil, - Expiration: sectorExpiration, + Expiration: abi.ChainEpoch(1), } } diff --git a/actors/builtin/miner/miner_test.go b/actors/builtin/miner/miner_test.go index 72ec96bc1..18dc5effd 100644 --- a/actors/builtin/miner/miner_test.go +++ b/actors/builtin/miner/miner_test.go @@ -15,6 +15,7 @@ import ( "github.com/filecoin-project/go-state-types/dline" "github.com/filecoin-project/go-state-types/exitcode" "github.com/filecoin-project/go-state-types/network" + miner0 "github.com/filecoin-project/specs-actors/actors/builtin/miner" cid "github.com/ipfs/go-cid" "github.com/minio/blake2b-simd" "github.com/stretchr/testify/assert" @@ -4438,6 +4439,19 @@ func (h *actorHarness) collectSectors(rt *mock.Runtime) map[abi.SectorNumber]*mi return sectors } +func (h *actorHarness) collectPrecommitExpirations(rt *mock.Runtime, st *miner.State) map[abi.ChainEpoch][]uint64 { + queue, err := miner.LoadBitfieldQueue(rt.AdtStore(), st.PreCommittedSectorsExpiry, miner.NoQuantization, miner.PrecommitExpiryAmtBitwidth) + require.NoError(h.t, err) + expirations := map[abi.ChainEpoch][]uint64{} + _ = queue.ForEach(func(epoch abi.ChainEpoch, bf bitfield.BitField) error { + expanded, err := bf.All(miner.AddressedSectorsMax) + require.NoError(h.t, err) + expirations[epoch] = expanded + return nil + }) + return expirations +} + func (h *actorHarness) collectDeadlineExpirations(rt *mock.Runtime, deadline *miner.Deadline) map[abi.ChainEpoch][]uint64 { queue, err := miner.LoadBitfieldQueue(rt.AdtStore(), deadline.ExpirationsEpochs, miner.NoQuantization, miner.DeadlineExpirationAmtBitwidth) require.NoError(h.t, err) @@ -4579,7 +4593,6 @@ type preCommitConf struct { dealWeight abi.DealWeight verifiedDealWeight abi.DealWeight dealSpace abi.SectorSize - pledgeDelta *abi.TokenAmount } func (h *actorHarness) preCommitSector(rt *mock.Runtime, params *miner.PreCommitSectorParams, conf preCommitConf, first bool) *miner.SectorPreCommitOnChainInfo { @@ -4590,7 +4603,6 @@ func (h *actorHarness) preCommitSector(rt *mock.Runtime, params *miner.PreCommit expectQueryNetworkInfo(rt, h) } if len(params.DealIDs) > 0 { - // If there are any deal IDs, allocate half the weight to non-verified and half to verified. vdParams := market.VerifyDealsForActivationParams{ Sectors: []market.SectorDeals{{ SectorExpiry: params.Expiration, @@ -4612,6 +4624,11 @@ func (h *actorHarness) preCommitSector(rt *mock.Runtime, params *miner.PreCommit }}, } rt.ExpectSend(builtin.StorageMarketActorAddr, builtin.MethodsMarket.VerifyDealsForActivation, &vdParams, big.Zero(), &vdReturn, exitcode.Ok) + } else { + // Ensure the deal IDs and configured deal weight returns are consistent. + require.Equal(h.t, abi.SectorSize(0), conf.dealSpace, "no deals but positive deal space configured") + require.True(h.t, conf.dealWeight.NilOrZero(), "no deals but positive deal weight configured") + require.True(h.t, conf.verifiedDealWeight.NilOrZero(), "no deals but positive deal weight configured") } st := getState(rt) if st.FeeDebt.GreaterThan(big.Zero()) { @@ -4624,11 +4641,7 @@ func (h *actorHarness) preCommitSector(rt *mock.Runtime, params *miner.PreCommit rt.ExpectSend(builtin.StoragePowerActorAddr, builtin.MethodsPower.EnrollCronEvent, cronParams, big.Zero(), nil, exitcode.Ok) } - if conf.pledgeDelta != nil { - if !conf.pledgeDelta.IsZero() { - rt.ExpectSend(builtin.StoragePowerActorAddr, builtin.MethodsPower.UpdatePledgeTotal, conf.pledgeDelta, big.Zero(), nil, exitcode.Ok) - } - } else if rt.NetworkVersion() < network.Version7 { + if rt.NetworkVersion() < network.Version7 { pledgeDelta := immediatelyVestingFunds(rt, st).Neg() if !pledgeDelta.IsZero() { rt.ExpectSend(builtin.StoragePowerActorAddr, builtin.MethodsPower.UpdatePledgeTotal, &pledgeDelta, big.Zero(), nil, exitcode.Ok) @@ -4640,6 +4653,70 @@ func (h *actorHarness) preCommitSector(rt *mock.Runtime, params *miner.PreCommit return h.getPreCommit(rt, params.SectorNumber) } +type preCommitBatchConf struct { + sectorWeights []market.SectorWeights +} + +func (h *actorHarness) preCommitSectorBatch(rt *mock.Runtime, params *miner.PreCommitSectorBatchParams, conf preCommitBatchConf, first bool) []*miner.SectorPreCommitOnChainInfo { + rt.SetCaller(h.worker, builtin.AccountActorCodeID) + rt.ExpectValidateCallerAddr(append(h.controlAddrs, h.owner, h.worker)...) + { + expectQueryNetworkInfo(rt, h) + } + sectorDeals := make([]market.SectorDeals, len(params.Sectors)) + sectorWeights := make([]market.SectorWeights, len(params.Sectors)) + anyDeals := false + for i, sector := range params.Sectors { + sectorDeals[i] = market.SectorDeals{ + SectorExpiry: sector.Expiration, + DealIDs: sector.DealIDs, + } + + if len(conf.sectorWeights) > i { + sectorWeights[i] = conf.sectorWeights[i] + } else { + sectorWeights[i] = market.SectorWeights{ + DealSpace: 0, + DealWeight: big.Zero(), + VerifiedDealWeight: big.Zero(), + } + } + // Sanity check on expectations + sectorHasDeals := len(sector.DealIDs) > 0 + dealTotalWeight := big.Add(sectorWeights[i].DealWeight, sectorWeights[i].VerifiedDealWeight) + require.True(h.t, sectorHasDeals == !dealTotalWeight.Equals(big.Zero()), "sector deals inconsistent with configured weight") + require.True(h.t, sectorHasDeals == (sectorWeights[i].DealSpace != 0), "sector deals inconsistent with configured space") + anyDeals = anyDeals || sectorHasDeals + } + if anyDeals { + vdParams := market.VerifyDealsForActivationParams{ + Sectors: sectorDeals, + } + vdReturn := market.VerifyDealsForActivationReturn{ + Sectors: sectorWeights, + } + rt.ExpectSend(builtin.StorageMarketActorAddr, builtin.MethodsMarket.VerifyDealsForActivation, &vdParams, big.Zero(), &vdReturn, exitcode.Ok) + } + st := getState(rt) + if st.FeeDebt.GreaterThan(big.Zero()) { + rt.ExpectSend(builtin.BurntFundsActorAddr, builtin.MethodSend, nil, st.FeeDebt, nil, exitcode.Ok) + } + + if first { + dlInfo := miner.NewDeadlineInfoFromOffsetAndEpoch(st.ProvingPeriodStart, rt.Epoch()) + cronParams := makeDeadlineCronEventParams(h.t, dlInfo.Last()) + rt.ExpectSend(builtin.StoragePowerActorAddr, builtin.MethodsPower.EnrollCronEvent, cronParams, big.Zero(), nil, exitcode.Ok) + } + + rt.Call(h.a.PreCommitSectorBatch, params) + rt.Verify() + precommits := make([]*miner.SectorPreCommitOnChainInfo, len(params.Sectors)) + for i, sector := range params.Sectors { + precommits[i] = h.getPreCommit(rt, sector.SectorNumber) + } + return precommits +} + // Options for proveCommitSector behaviour. // Default zero values should let everything be ok. type proveCommitConf struct { @@ -5509,7 +5586,7 @@ func (h *actorHarness) powerPairForSectors(sectors []*miner.SectorOnChainInfo) m return miner.NewPowerPair(rawPower, qaPower) } -func (h *actorHarness) makePreCommit(sectorNo abi.SectorNumber, challenge, expiration abi.ChainEpoch, dealIDs []abi.DealID) *miner.PreCommitSectorParams { +func (h *actorHarness) makePreCommit(sectorNo abi.SectorNumber, challenge, expiration abi.ChainEpoch, dealIDs []abi.DealID) *miner0.SectorPreCommitInfo { return &miner.PreCommitSectorParams{ SealProof: h.sealProofType, SectorNumber: sectorNo, diff --git a/actors/builtin/miner/policy.go b/actors/builtin/miner/policy.go index 155c7af55..5d9ad61da 100644 --- a/actors/builtin/miner/policy.go +++ b/actors/builtin/miner/policy.go @@ -178,6 +178,10 @@ var MaxProveCommitDuration = map[abi.RegisteredSealProof]abi.ChainEpoch{ abi.RegisteredSealProof_StackedDrg64GiBV1_1: builtin.EpochsInDay + PreCommitChallengeDelay, } +// The maximum number of sector pre-commitments in a single batch. +// 32 sectors per epoch would support a single miner onboarding 1EiB of 32GiB sectors in 1 year. +const PreCommitSectorBatchMaxSize = 32 + // Maximum delay between challenge and pre-commitment. // This prevents a miner sealing sectors far in advance of committing them to the chain, thus committing to a // particular chain. diff --git a/gen/gen.go b/gen/gen.go index 85fd2be87..139d25e11 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -200,6 +200,7 @@ func main() { //miner.CompactSectorNumbersParams{}, // Aliased from v0 //miner.CronEventPayload{}, // Aliased from v0 // miner.DisputeWindowedPoStParams{}, // Aliased from v3 + miner.PreCommitSectorBatchParams{}, // other types //miner.FaultDeclaration{}, // Aliased from v0 //miner.RecoveryDeclaration{}, // Aliased from v0