From 8765c69ab89cfad9875293b6be2ff3e40024b4b6 Mon Sep 17 00:00:00 2001 From: zenground0 Date: Wed, 8 Dec 2021 12:11:19 -0500 Subject: [PATCH] Snap Deals Integration - FSM handles the actual cc upgrade process including error states - PoSting (winning and window) works over upgraded and upgrading sectors - Integration test and changes to itest framework to reduce flakes - Update CLI to handle new upgrade - Update dependencies --- api/api_full.go | 2 +- api/api_storage.go | 8 +- api/proxy_gen.go | 30 +- api/v0api/gateway.go | 4 +- api/v0api/proxy_gen.go | 12 +- api/version.go | 2 +- build/openrpc/full.json.gz | Bin 25704 -> 25709 bytes build/openrpc/miner.json.gz | Bin 11409 -> 11527 bytes build/openrpc/worker.json.gz | Bin 3696 -> 3692 bytes build/params_2k.go | 2 +- chain/actors/builtin/builtin.go | 1 + chain/actors/builtin/builtin.go.template | 1 + chain/actors/builtin/miner/actor.go.template | 3 + chain/actors/builtin/miner/miner.go | 3 + chain/consensus/filcns/filecoin.go | 17 +- chain/gen/gen.go | 10 +- chain/stmgr/actors.go | 7 +- chain/sync_test.go | 3 +- chain/vm/syscalls.go | 4 +- cmd/lotus-bench/caching_verifier.go | 5 +- cmd/lotus-bench/main.go | 53 ++-- cmd/lotus-miner/info.go | 12 + cmd/lotus-miner/sectors.go | 76 ++++- cmd/lotus-seal-worker/main.go | 16 ++ cmd/lotus-sim/simulation/mock/mock.go | 3 +- documentation/en/api-v0-methods-miner.md | 17 +- documentation/en/cli-lotus-miner.md | 62 ++-- documentation/en/cli-lotus-worker.md | 2 + .../en/default-lotus-miner-config.toml | 6 + .../sector-storage/ffiwrapper/sealer_cgo.go | 9 +- .../sector-storage/ffiwrapper/sealer_test.go | 18 +- extern/sector-storage/ffiwrapper/types.go | 17 +- .../sector-storage/ffiwrapper/verifier_cgo.go | 70 +++-- extern/sector-storage/manager.go | 47 ++- extern/sector-storage/mock/mock.go | 67 +++-- extern/sector-storage/teststorage_test.go | 14 +- extern/storage-sealing/cbor_gen.go | 272 +++++++++++++++++- extern/storage-sealing/checks.go | 32 +++ extern/storage-sealing/fsm.go | 119 +++++++- extern/storage-sealing/fsm_events.go | 94 ++++++ extern/storage-sealing/input.go | 39 +++ extern/storage-sealing/mocks/api.go | 15 + extern/storage-sealing/sealing.go | 16 +- extern/storage-sealing/sector_state.go | 123 +++++--- extern/storage-sealing/states_failed.go | 206 +++++++++---- extern/storage-sealing/states_failed_test.go | 8 +- .../storage-sealing/states_replica_update.go | 209 ++++++++++++++ extern/storage-sealing/states_sealing.go | 6 +- extern/storage-sealing/types.go | 10 +- extern/storage-sealing/upgrade_queue.go | 68 ++++- go.mod | 8 +- go.sum | 12 +- itests/ccupgrade_test.go | 143 ++++++--- itests/kit/blockminer.go | 137 +++++++++ itests/kit/deals.go | 7 +- itests/kit/ensemble.go | 37 +++ .../storageadapter/ondealsectorcommitted.go | 53 +++- miner/miner.go | 6 +- miner/warmup.go | 16 +- node/config/def.go | 21 +- node/impl/storminer.go | 13 +- storage/adapter_storage_miner.go | 9 + storage/miner.go | 3 +- storage/miner_sealing.go | 11 +- storage/wdpost_changehandler_test.go | 2 +- storage/wdpost_run.go | 46 ++- storage/wdpost_run_test.go | 6 +- 67 files changed, 1995 insertions(+), 355 deletions(-) create mode 100644 extern/storage-sealing/states_replica_update.go diff --git a/api/api_full.go b/api/api_full.go index 06aaff99c..84055877e 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -1083,7 +1083,7 @@ type CirculatingSupply struct { type MiningBaseInfo struct { MinerPower types.BigInt NetworkPower types.BigInt - Sectors []builtin.SectorInfo + Sectors []builtin.ExtendedSectorInfo WorkerKey address.Address SectorSize abi.SectorSize PrevBeaconEntry types.BeaconEntry diff --git a/api/api_storage.go b/api/api_storage.go index 92117d2fb..6e6d75c28 100644 --- a/api/api_storage.go +++ b/api/api_storage.go @@ -14,6 +14,7 @@ import ( "github.com/filecoin-project/go-address" datatransfer "github.com/filecoin-project/go-data-transfer" "github.com/filecoin-project/go-state-types/abi" + abinetwork "github.com/filecoin-project/go-state-types/network" "github.com/filecoin-project/specs-actors/v2/actors/builtin/market" "github.com/filecoin-project/specs-storage/storage" @@ -99,8 +100,8 @@ type StorageMiner interface { // Returns null if message wasn't sent SectorTerminateFlush(ctx context.Context) (*cid.Cid, error) //perm:admin // SectorTerminatePending returns a list of pending sector terminations to be sent in the next batch message - SectorTerminatePending(ctx context.Context) ([]abi.SectorID, error) //perm:admin - SectorMarkForUpgrade(ctx context.Context, id abi.SectorNumber) error //perm:admin + SectorTerminatePending(ctx context.Context) ([]abi.SectorID, error) //perm:admin + SectorMarkForUpgrade(ctx context.Context, id abi.SectorNumber, snap bool) error //perm:admin // SectorPreCommitFlush immediately sends a PreCommit message with sectors batched for PreCommit. // Returns null if message wasn't sent SectorPreCommitFlush(ctx context.Context) ([]sealiface.PreCommitBatchRes, error) //perm:admin @@ -111,6 +112,7 @@ type StorageMiner interface { SectorCommitFlush(ctx context.Context) ([]sealiface.CommitBatchRes, error) //perm:admin // SectorCommitPending returns a list of pending Commit sectors to be sent in the next aggregate message SectorCommitPending(ctx context.Context) ([]abi.SectorID, error) //perm:admin + SectorMatchPendingPiecesToOpenSectors(ctx context.Context) error //perm:admin // WorkerConnect tells the node to connect to workers RPC WorkerConnect(context.Context, string) error //perm:admin retry:true @@ -250,7 +252,7 @@ type StorageMiner interface { CheckProvable(ctx context.Context, pp abi.RegisteredPoStProof, sectors []storage.SectorRef, expensive bool) (map[abi.SectorNumber]string, error) //perm:admin - ComputeProof(ctx context.Context, ssi []builtin.SectorInfo, rand abi.PoStRandomness) ([]builtin.PoStProof, error) //perm:read + ComputeProof(ctx context.Context, ssi []builtin.ExtendedSectorInfo, rand abi.PoStRandomness, poStEpoch abi.ChainEpoch, nv abinetwork.Version) ([]builtin.PoStProof, error) //perm:read } var _ storiface.WorkerReturn = *new(StorageMiner) diff --git a/api/proxy_gen.go b/api/proxy_gen.go index 1e17d9e73..63dc4aac8 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -17,6 +17,7 @@ import ( "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/crypto" "github.com/filecoin-project/go-state-types/dline" + abinetwork "github.com/filecoin-project/go-state-types/network" apitypes "github.com/filecoin-project/lotus/api/types" "github.com/filecoin-project/lotus/chain/actors/builtin" "github.com/filecoin-project/lotus/chain/actors/builtin/miner" @@ -620,7 +621,7 @@ type StorageMinerStruct struct { CheckProvable func(p0 context.Context, p1 abi.RegisteredPoStProof, p2 []storage.SectorRef, p3 bool) (map[abi.SectorNumber]string, error) `perm:"admin"` - ComputeProof func(p0 context.Context, p1 []builtin.SectorInfo, p2 abi.PoStRandomness) ([]builtin.PoStProof, error) `perm:"read"` + ComputeProof func(p0 context.Context, p1 []builtin.ExtendedSectorInfo, p2 abi.PoStRandomness, p3 abi.ChainEpoch, p4 abinetwork.Version) ([]builtin.PoStProof, error) `perm:"read"` CreateBackup func(p0 context.Context, p1 string) error `perm:"admin"` @@ -756,7 +757,9 @@ type StorageMinerStruct struct { SectorGetSealDelay func(p0 context.Context) (time.Duration, error) `perm:"read"` - SectorMarkForUpgrade func(p0 context.Context, p1 abi.SectorNumber) error `perm:"admin"` + SectorMarkForUpgrade func(p0 context.Context, p1 abi.SectorNumber, p2 bool) error `perm:"admin"` + + SectorMatchPendingPiecesToOpenSectors func(p0 context.Context) error `perm:"admin"` SectorPreCommitFlush func(p0 context.Context) ([]sealiface.PreCommitBatchRes, error) `perm:"admin"` @@ -3706,14 +3709,14 @@ func (s *StorageMinerStub) CheckProvable(p0 context.Context, p1 abi.RegisteredPo return *new(map[abi.SectorNumber]string), ErrNotSupported } -func (s *StorageMinerStruct) ComputeProof(p0 context.Context, p1 []builtin.SectorInfo, p2 abi.PoStRandomness) ([]builtin.PoStProof, error) { +func (s *StorageMinerStruct) ComputeProof(p0 context.Context, p1 []builtin.ExtendedSectorInfo, p2 abi.PoStRandomness, p3 abi.ChainEpoch, p4 abinetwork.Version) ([]builtin.PoStProof, error) { if s.Internal.ComputeProof == nil { return *new([]builtin.PoStProof), ErrNotSupported } - return s.Internal.ComputeProof(p0, p1, p2) + return s.Internal.ComputeProof(p0, p1, p2, p3, p4) } -func (s *StorageMinerStub) ComputeProof(p0 context.Context, p1 []builtin.SectorInfo, p2 abi.PoStRandomness) ([]builtin.PoStProof, error) { +func (s *StorageMinerStub) ComputeProof(p0 context.Context, p1 []builtin.ExtendedSectorInfo, p2 abi.PoStRandomness, p3 abi.ChainEpoch, p4 abinetwork.Version) ([]builtin.PoStProof, error) { return *new([]builtin.PoStProof), ErrNotSupported } @@ -4454,14 +4457,25 @@ func (s *StorageMinerStub) SectorGetSealDelay(p0 context.Context) (time.Duration return *new(time.Duration), ErrNotSupported } -func (s *StorageMinerStruct) SectorMarkForUpgrade(p0 context.Context, p1 abi.SectorNumber) error { +func (s *StorageMinerStruct) SectorMarkForUpgrade(p0 context.Context, p1 abi.SectorNumber, p2 bool) error { if s.Internal.SectorMarkForUpgrade == nil { return ErrNotSupported } - return s.Internal.SectorMarkForUpgrade(p0, p1) + return s.Internal.SectorMarkForUpgrade(p0, p1, p2) } -func (s *StorageMinerStub) SectorMarkForUpgrade(p0 context.Context, p1 abi.SectorNumber) error { +func (s *StorageMinerStub) SectorMarkForUpgrade(p0 context.Context, p1 abi.SectorNumber, p2 bool) error { + return ErrNotSupported +} + +func (s *StorageMinerStruct) SectorMatchPendingPiecesToOpenSectors(p0 context.Context) error { + if s.Internal.SectorMatchPendingPiecesToOpenSectors == nil { + return ErrNotSupported + } + return s.Internal.SectorMatchPendingPiecesToOpenSectors(p0) +} + +func (s *StorageMinerStub) SectorMatchPendingPiecesToOpenSectors(p0 context.Context) error { return ErrNotSupported } diff --git a/api/v0api/gateway.go b/api/v0api/gateway.go index 18a5ec7d6..e3ba56899 100644 --- a/api/v0api/gateway.go +++ b/api/v0api/gateway.go @@ -8,7 +8,7 @@ import ( "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/dline" - "github.com/filecoin-project/go-state-types/network" + abinetwork "github.com/filecoin-project/go-state-types/network" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/chain/actors/builtin/miner" @@ -57,7 +57,7 @@ type Gateway interface { StateMinerInfo(ctx context.Context, actor address.Address, tsk types.TipSetKey) (miner.MinerInfo, error) StateMinerProvingDeadline(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*dline.Info, error) StateMinerPower(context.Context, address.Address, types.TipSetKey) (*api.MinerPower, error) - StateNetworkVersion(context.Context, types.TipSetKey) (network.Version, error) + StateNetworkVersion(context.Context, types.TipSetKey) (abinetwork.Version, error) StateSearchMsg(ctx context.Context, msg cid.Cid) (*api.MsgLookup, error) StateSectorGetInfo(ctx context.Context, maddr address.Address, n abi.SectorNumber, tsk types.TipSetKey) (*miner.SectorOnChainInfo, error) StateVerifiedClientStatus(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*abi.StoragePower, error) diff --git a/api/v0api/proxy_gen.go b/api/v0api/proxy_gen.go index af0687fe5..49ebad428 100644 --- a/api/v0api/proxy_gen.go +++ b/api/v0api/proxy_gen.go @@ -13,7 +13,7 @@ import ( "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/crypto" "github.com/filecoin-project/go-state-types/dline" - "github.com/filecoin-project/go-state-types/network" + abinetwork "github.com/filecoin-project/go-state-types/network" "github.com/filecoin-project/lotus/api" apitypes "github.com/filecoin-project/lotus/api/types" "github.com/filecoin-project/lotus/chain/actors/builtin/miner" @@ -451,7 +451,7 @@ type GatewayStruct struct { StateMinerProvingDeadline func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*dline.Info, error) `` - StateNetworkVersion func(p0 context.Context, p1 types.TipSetKey) (network.Version, error) `` + StateNetworkVersion func(p0 context.Context, p1 types.TipSetKey) (abinetwork.Version, error) `` StateSearchMsg func(p0 context.Context, p1 cid.Cid) (*api.MsgLookup, error) `` @@ -2703,15 +2703,15 @@ func (s *GatewayStub) StateMinerProvingDeadline(p0 context.Context, p1 address.A return nil, ErrNotSupported } -func (s *GatewayStruct) StateNetworkVersion(p0 context.Context, p1 types.TipSetKey) (network.Version, error) { +func (s *GatewayStruct) StateNetworkVersion(p0 context.Context, p1 types.TipSetKey) (abinetwork.Version, error) { if s.Internal.StateNetworkVersion == nil { - return *new(network.Version), ErrNotSupported + return *new(abinetwork.Version), ErrNotSupported } return s.Internal.StateNetworkVersion(p0, p1) } -func (s *GatewayStub) StateNetworkVersion(p0 context.Context, p1 types.TipSetKey) (network.Version, error) { - return *new(network.Version), ErrNotSupported +func (s *GatewayStub) StateNetworkVersion(p0 context.Context, p1 types.TipSetKey) (abinetwork.Version, error) { + return *new(abinetwork.Version), ErrNotSupported } func (s *GatewayStruct) StateSearchMsg(p0 context.Context, p1 cid.Cid) (*api.MsgLookup, error) { diff --git a/api/version.go b/api/version.go index 93148f28d..0be7de878 100644 --- a/api/version.go +++ b/api/version.go @@ -57,7 +57,7 @@ var ( FullAPIVersion0 = newVer(1, 4, 0) FullAPIVersion1 = newVer(2, 1, 0) - MinerAPIVersion0 = newVer(1, 2, 0) + MinerAPIVersion0 = newVer(1, 3, 0) WorkerAPIVersion0 = newVer(1, 5, 0) ) diff --git a/build/openrpc/full.json.gz b/build/openrpc/full.json.gz index 65580f678258eb4d72c14aa62ba86b2d17b2e7b4..5bf44f1a455a2cecb587e0a05fe009f7088e95c8 100644 GIT binary patch delta 23487 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%0Dwpb3Dps?Hgp+56G!+ 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 delta 3396 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 a4mlLYhO%7VF8>z*0RR7g*c9tT#{dA)sfKU> diff --git a/build/params_2k.go b/build/params_2k.go index 84023c38c..6c0918c51 100644 --- a/build/params_2k.go +++ b/build/params_2k.go @@ -17,7 +17,7 @@ import ( const BootstrappersFile = "" const GenesisFile = "" -const GenesisNetworkVersion = network.Version14 +const GenesisNetworkVersion = network.Version15 var UpgradeBreezeHeight = abi.ChainEpoch(-1) diff --git a/chain/actors/builtin/builtin.go b/chain/actors/builtin/builtin.go index d93732999..febbca479 100644 --- a/chain/actors/builtin/builtin.go +++ b/chain/actors/builtin/builtin.go @@ -61,6 +61,7 @@ const ( // These are all just type aliases across actor versions. In the future, that might change // and we might need to do something fancier. type SectorInfo = proof7.SectorInfo +type ExtendedSectorInfo = proof7.ExtendedSectorInfo type PoStProof = proof7.PoStProof type FilterEstimate = smoothing0.FilterEstimate diff --git a/chain/actors/builtin/builtin.go.template b/chain/actors/builtin/builtin.go.template index 031c05182..f5d5eb77b 100644 --- a/chain/actors/builtin/builtin.go.template +++ b/chain/actors/builtin/builtin.go.template @@ -45,6 +45,7 @@ const ( // These are all just type aliases across actor versions. In the future, that might change // and we might need to do something fancier. type SectorInfo = proof{{.latestVersion}}.SectorInfo +type ExtendedSectorInfo = proof{{.latestVersion}}.ExtendedSectorInfo type PoStProof = proof{{.latestVersion}}.PoStProof type FilterEstimate = smoothing0.FilterEstimate diff --git a/chain/actors/builtin/miner/actor.go.template b/chain/actors/builtin/miner/actor.go.template index 2b6b78ebc..74c16be36 100644 --- a/chain/actors/builtin/miner/actor.go.template +++ b/chain/actors/builtin/miner/actor.go.template @@ -23,6 +23,7 @@ import ( miner2 "github.com/filecoin-project/specs-actors/v2/actors/builtin/miner" miner3 "github.com/filecoin-project/specs-actors/v3/actors/builtin/miner" miner5 "github.com/filecoin-project/specs-actors/v5/actors/builtin/miner" + miner7 "github.com/filecoin-project/specs-actors/v7/actors/builtin/miner" {{range .versions}} builtin{{.}} "github.com/filecoin-project/specs-actors{{import .}}actors/builtin" {{end}} @@ -193,6 +194,7 @@ type SectorPreCommitOnChainInfo struct { type PoStPartition = miner0.PoStPartition type RecoveryDeclaration = miner0.RecoveryDeclaration type FaultDeclaration = miner0.FaultDeclaration +type ReplicaUpdate = miner7.ReplicaUpdate // Params type DeclareFaultsParams = miner0.DeclareFaultsParams @@ -201,6 +203,7 @@ type SubmitWindowedPoStParams = miner0.SubmitWindowedPoStParams type ProveCommitSectorParams = miner0.ProveCommitSectorParams type DisputeWindowedPoStParams = miner3.DisputeWindowedPoStParams type ProveCommitAggregateParams = miner5.ProveCommitAggregateParams +type ProveReplicaUpdatesParams = miner7.ProveReplicaUpdatesParams func PreferredSealProofTypeFromWindowPoStType(nver network.Version, proof abi.RegisteredPoStProof) (abi.RegisteredSealProof, error) { // We added support for the new proofs in network version 7, and removed support for the old diff --git a/chain/actors/builtin/miner/miner.go b/chain/actors/builtin/miner/miner.go index e60ff8da8..7889d7a4d 100644 --- a/chain/actors/builtin/miner/miner.go +++ b/chain/actors/builtin/miner/miner.go @@ -23,6 +23,7 @@ import ( miner2 "github.com/filecoin-project/specs-actors/v2/actors/builtin/miner" miner3 "github.com/filecoin-project/specs-actors/v3/actors/builtin/miner" miner5 "github.com/filecoin-project/specs-actors/v5/actors/builtin/miner" + miner7 "github.com/filecoin-project/specs-actors/v7/actors/builtin/miner" builtin0 "github.com/filecoin-project/specs-actors/actors/builtin" @@ -282,6 +283,7 @@ type SectorPreCommitOnChainInfo struct { type PoStPartition = miner0.PoStPartition type RecoveryDeclaration = miner0.RecoveryDeclaration type FaultDeclaration = miner0.FaultDeclaration +type ReplicaUpdate = miner7.ReplicaUpdate // Params type DeclareFaultsParams = miner0.DeclareFaultsParams @@ -290,6 +292,7 @@ type SubmitWindowedPoStParams = miner0.SubmitWindowedPoStParams type ProveCommitSectorParams = miner0.ProveCommitSectorParams type DisputeWindowedPoStParams = miner3.DisputeWindowedPoStParams type ProveCommitAggregateParams = miner5.ProveCommitAggregateParams +type ProveReplicaUpdatesParams = miner7.ProveReplicaUpdatesParams func PreferredSealProofTypeFromWindowPoStType(nver network.Version, proof abi.RegisteredPoStProof) (abi.RegisteredSealProof, error) { // We added support for the new proofs in network version 7, and removed support for the old diff --git a/chain/consensus/filcns/filecoin.go b/chain/consensus/filcns/filecoin.go index 883edd9a1..53dce3f17 100644 --- a/chain/consensus/filcns/filecoin.go +++ b/chain/consensus/filcns/filecoin.go @@ -26,7 +26,7 @@ import ( "github.com/filecoin-project/go-state-types/crypto" "github.com/filecoin-project/go-state-types/network" blockadt "github.com/filecoin-project/specs-actors/actors/util/adt" - proof2 "github.com/filecoin-project/specs-actors/v2/actors/runtime/proof" + "github.com/filecoin-project/specs-actors/v7/actors/runtime/proof" bstore "github.com/filecoin-project/lotus/blockstore" "github.com/filecoin-project/lotus/build" @@ -400,17 +400,26 @@ func (filec *FilecoinEC) VerifyWinningPoStProof(ctx context.Context, nv network. return xerrors.Errorf("failed to get ID from miner address %s: %w", h.Miner, err) } - sectors, err := stmgr.GetSectorsForWinningPoSt(ctx, nv, filec.verifier, filec.sm, lbst, h.Miner, rand) + xsectors, err := stmgr.GetSectorsForWinningPoSt(ctx, nv, filec.verifier, filec.sm, lbst, h.Miner, rand) if err != nil { return xerrors.Errorf("getting winning post sector set: %w", err) } - ok, err := ffiwrapper.ProofVerifier.VerifyWinningPoSt(ctx, proof2.WinningPoStVerifyInfo{ + sectors := make([]proof.SectorInfo, len(xsectors)) + for i, xsi := range xsectors { + sectors[i] = proof.SectorInfo{ + SealProof: xsi.SealProof, + SectorNumber: xsi.SectorNumber, + SealedCID: xsi.SealedCID, + } + } + + ok, err := ffiwrapper.ProofVerifier.VerifyWinningPoSt(ctx, proof.WinningPoStVerifyInfo{ Randomness: rand, Proofs: h.WinPoStProof, ChallengedSectors: sectors, Prover: abi.ActorID(mid), - }) + }, h.Height, nv) if err != nil { return xerrors.Errorf("failed to verify election post: %w", err) } diff --git a/chain/gen/gen.go b/chain/gen/gen.go index 60dd142e9..f48798c6b 100644 --- a/chain/gen/gen.go +++ b/chain/gen/gen.go @@ -461,7 +461,7 @@ func (cg *ChainGen) NextTipSetFromMinersWithMessagesAndNulls(base *types.TipSet, if et != nil { // TODO: maybe think about passing in more real parameters to this? - wpost, err := cg.eppProvs[m].ComputeProof(context.TODO(), nil, nil) + wpost, err := cg.eppProvs[m].ComputeProof(context.TODO(), nil, nil, round, network.Version0) if err != nil { return nil, err } @@ -620,7 +620,7 @@ func (mca mca) WalletSign(ctx context.Context, a address.Address, v []byte) (*cr type WinningPoStProver interface { GenerateCandidates(context.Context, abi.PoStRandomness, uint64) ([]uint64, error) - ComputeProof(context.Context, []proof5.SectorInfo, abi.PoStRandomness) ([]proof5.PoStProof, error) + ComputeProof(context.Context, []proof7.ExtendedSectorInfo, abi.PoStRandomness, abi.ChainEpoch, network.Version) ([]proof5.PoStProof, error) } type wppProvider struct{} @@ -629,7 +629,7 @@ func (wpp *wppProvider) GenerateCandidates(ctx context.Context, _ abi.PoStRandom return []uint64{0}, nil } -func (wpp *wppProvider) ComputeProof(context.Context, []proof5.SectorInfo, abi.PoStRandomness) ([]proof5.PoStProof, error) { +func (wpp *wppProvider) ComputeProof(context.Context, []proof7.ExtendedSectorInfo, abi.PoStRandomness, abi.ChainEpoch, network.Version) ([]proof5.PoStProof, error) { return ValidWpostForTesting, nil } @@ -692,11 +692,11 @@ func (m genFakeVerifier) VerifyReplicaUpdate(update proof7.ReplicaUpdateInfo) (b panic("not supported") } -func (m genFakeVerifier) VerifyWinningPoSt(ctx context.Context, info proof5.WinningPoStVerifyInfo) (bool, error) { +func (m genFakeVerifier) VerifyWinningPoSt(ctx context.Context, info proof7.WinningPoStVerifyInfo, poStEpoch abi.ChainEpoch, nv network.Version) (bool, error) { panic("not supported") } -func (m genFakeVerifier) VerifyWindowPoSt(ctx context.Context, info proof5.WindowPoStVerifyInfo) (bool, error) { +func (m genFakeVerifier) VerifyWindowPoSt(ctx context.Context, info proof7.WindowPoStVerifyInfo) (bool, error) { panic("not supported") } diff --git a/chain/stmgr/actors.go b/chain/stmgr/actors.go index 4d016b7ab..a50b1b034 100644 --- a/chain/stmgr/actors.go +++ b/chain/stmgr/actors.go @@ -116,7 +116,7 @@ func MinerSectorInfo(ctx context.Context, sm *StateManager, maddr address.Addres return mas.GetSector(sid) } -func GetSectorsForWinningPoSt(ctx context.Context, nv network.Version, pv ffiwrapper.Verifier, sm *StateManager, st cid.Cid, maddr address.Address, rand abi.PoStRandomness) ([]builtin.SectorInfo, error) { +func GetSectorsForWinningPoSt(ctx context.Context, nv network.Version, pv ffiwrapper.Verifier, sm *StateManager, st cid.Cid, maddr address.Address, rand abi.PoStRandomness) ([]builtin.ExtendedSectorInfo, error) { act, err := sm.LoadActorRaw(ctx, maddr, st) if err != nil { return nil, xerrors.Errorf("failed to load miner actor: %w", err) @@ -202,12 +202,13 @@ func GetSectorsForWinningPoSt(ctx context.Context, nv network.Version, pv ffiwra return nil, xerrors.Errorf("loading proving sectors: %w", err) } - out := make([]builtin.SectorInfo, len(sectors)) + out := make([]builtin.ExtendedSectorInfo, len(sectors)) for i, sinfo := range sectors { - out[i] = builtin.SectorInfo{ + out[i] = builtin.ExtendedSectorInfo{ SealProof: sinfo.SealProof, SectorNumber: sinfo.SectorNumber, SealedCID: sinfo.SealedCID, + SectorKey: sinfo.SectorKeyCID, } } diff --git a/chain/sync_test.go b/chain/sync_test.go index 4175ff5fa..e14601cb6 100644 --- a/chain/sync_test.go +++ b/chain/sync_test.go @@ -22,6 +22,7 @@ import ( "github.com/filecoin-project/go-state-types/abi" proof2 "github.com/filecoin-project/specs-actors/v2/actors/runtime/proof" + proof7 "github.com/filecoin-project/specs-actors/v7/actors/runtime/proof" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/build" @@ -542,7 +543,7 @@ func (wpp badWpp) GenerateCandidates(context.Context, abi.PoStRandomness, uint64 return []uint64{1}, nil } -func (wpp badWpp) ComputeProof(context.Context, []proof2.SectorInfo, abi.PoStRandomness) ([]proof2.PoStProof, error) { +func (wpp badWpp) ComputeProof(context.Context, []proof7.ExtendedSectorInfo, abi.PoStRandomness, abi.ChainEpoch, network.Version) ([]proof2.PoStProof, error) { return []proof2.PoStProof{ { PoStProof: abi.RegisteredPoStProof_StackedDrgWinning2KiBV1, diff --git a/chain/vm/syscalls.go b/chain/vm/syscalls.go index b8c027bd7..cd143279e 100644 --- a/chain/vm/syscalls.go +++ b/chain/vm/syscalls.go @@ -245,8 +245,8 @@ func (ss *syscallShim) workerKeyAtLookback(height abi.ChainEpoch) (address.Addre return ResolveToKeyAddr(ss.cstate, ss.cst, info.Worker) } -func (ss *syscallShim) VerifyPoSt(proof proof5.WindowPoStVerifyInfo) error { - ok, err := ss.verifier.VerifyWindowPoSt(context.TODO(), proof) +func (ss *syscallShim) VerifyPoSt(info proof5.WindowPoStVerifyInfo) error { + ok, err := ss.verifier.VerifyWindowPoSt(context.TODO(), info) if err != nil { return err } diff --git a/cmd/lotus-bench/caching_verifier.go b/cmd/lotus-bench/caching_verifier.go index 7d5e993a0..87c2e9e16 100644 --- a/cmd/lotus-bench/caching_verifier.go +++ b/cmd/lotus-bench/caching_verifier.go @@ -8,6 +8,7 @@ import ( proof7 "github.com/filecoin-project/specs-actors/v7/actors/runtime/proof" "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/network" "github.com/filecoin-project/lotus/extern/sector-storage/ffiwrapper" proof2 "github.com/filecoin-project/specs-actors/v2/actors/runtime/proof" "github.com/ipfs/go-datastore" @@ -86,8 +87,8 @@ func (cv *cachingVerifier) VerifySeal(svi proof2.SealVerifyInfo) (bool, error) { }, &svi) } -func (cv *cachingVerifier) VerifyWinningPoSt(ctx context.Context, info proof2.WinningPoStVerifyInfo) (bool, error) { - return cv.backend.VerifyWinningPoSt(ctx, info) +func (cv *cachingVerifier) VerifyWinningPoSt(ctx context.Context, info proof7.WinningPoStVerifyInfo, poStEpoch abi.ChainEpoch, nv network.Version) (bool, error) { + return cv.backend.VerifyWinningPoSt(ctx, info, poStEpoch, nv) } func (cv *cachingVerifier) VerifyWindowPoSt(ctx context.Context, info proof2.WindowPoStVerifyInfo) (bool, error) { return cv.withCache(func() (bool, error) { diff --git a/cmd/lotus-bench/main.go b/cmd/lotus-bench/main.go index 0b8ec6fe3..8893e7b8e 100644 --- a/cmd/lotus-bench/main.go +++ b/cmd/lotus-bench/main.go @@ -12,6 +12,8 @@ import ( "time" saproof2 "github.com/filecoin-project/specs-actors/v2/actors/runtime/proof" + "github.com/filecoin-project/specs-actors/v7/actors/runtime/proof" + saproof7 "github.com/filecoin-project/specs-actors/v7/actors/runtime/proof" "github.com/docker/go-units" logging "github.com/ipfs/go-log/v2" @@ -260,7 +262,8 @@ var sealBenchCmd = &cli.Command{ sectorNumber := c.Int("num-sectors") var sealTimings []SealingResult - var sealedSectors []saproof2.SectorInfo + var extendedSealedSectors []saproof7.ExtendedSectorInfo + var sealedSectors []saproof7.SectorInfo if robench == "" { var err error @@ -269,7 +272,7 @@ var sealBenchCmd = &cli.Command{ PreCommit2: 1, Commit: 1, } - sealTimings, sealedSectors, err = runSeals(sb, sbfs, sectorNumber, parCfg, mid, sectorSize, []byte(c.String("ticket-preimage")), c.String("save-commit2-input"), skipc2, c.Bool("skip-unseal")) + sealTimings, extendedSealedSectors, err = runSeals(sb, sbfs, sectorNumber, parCfg, mid, sectorSize, []byte(c.String("ticket-preimage")), c.String("save-commit2-input"), skipc2, c.Bool("skip-unseal")) if err != nil { return xerrors.Errorf("failed to run seals: %w", err) } @@ -296,7 +299,13 @@ var sealBenchCmd = &cli.Command{ } for _, s := range genm.Sectors { - sealedSectors = append(sealedSectors, saproof2.SectorInfo{ + extendedSealedSectors = append(extendedSealedSectors, saproof7.ExtendedSectorInfo{ + SealedCID: s.CommR, + SectorNumber: s.SectorID, + SealProof: s.ProofType, + SectorKey: nil, + }) + sealedSectors = append(sealedSectors, proof.SectorInfo{ SealedCID: s.CommR, SectorNumber: s.SectorID, SealProof: s.ProofType, @@ -325,20 +334,20 @@ var sealBenchCmd = &cli.Command{ return err } - fcandidates, err := ffiwrapper.ProofVerifier.GenerateWinningPoStSectorChallenge(context.TODO(), wipt, mid, challenge[:], uint64(len(sealedSectors))) + fcandidates, err := ffiwrapper.ProofVerifier.GenerateWinningPoStSectorChallenge(context.TODO(), wipt, mid, challenge[:], uint64(len(extendedSealedSectors))) if err != nil { return err } - candidates := make([]saproof2.SectorInfo, len(fcandidates)) + xcandidates := make([]saproof7.ExtendedSectorInfo, len(fcandidates)) for i, fcandidate := range fcandidates { - candidates[i] = sealedSectors[fcandidate] + xcandidates[i] = extendedSealedSectors[fcandidate] } gencandidates := time.Now() log.Info("computing winning post snark (cold)") - proof1, err := sb.GenerateWinningPoSt(context.TODO(), mid, candidates, challenge[:]) + proof1, err := sb.GenerateWinningPoSt(context.TODO(), mid, xcandidates, challenge[:]) if err != nil { return err } @@ -346,20 +355,29 @@ var sealBenchCmd = &cli.Command{ winningpost1 := time.Now() log.Info("computing winning post snark (hot)") - proof2, err := sb.GenerateWinningPoSt(context.TODO(), mid, candidates, challenge[:]) + proof2, err := sb.GenerateWinningPoSt(context.TODO(), mid, xcandidates, challenge[:]) if err != nil { return err } + candidates := make([]saproof7.SectorInfo, len(xcandidates)) + for i, xsi := range xcandidates { + candidates[i] = saproof7.SectorInfo{ + SealedCID: xsi.SealedCID, + SectorNumber: xsi.SectorNumber, + SealProof: xsi.SealProof, + } + } + winnningpost2 := time.Now() - pvi1 := saproof2.WinningPoStVerifyInfo{ + pvi1 := saproof7.WinningPoStVerifyInfo{ Randomness: abi.PoStRandomness(challenge[:]), Proofs: proof1, ChallengedSectors: candidates, Prover: mid, } - ok, err := ffiwrapper.ProofVerifier.VerifyWinningPoSt(context.TODO(), pvi1) + ok, err := ffiwrapper.ProofVerifier.VerifyWinningPoSt(context.TODO(), pvi1, 0, build.NewestNetworkVersion) if err != nil { return err } @@ -369,14 +387,14 @@ var sealBenchCmd = &cli.Command{ verifyWinningPost1 := time.Now() - pvi2 := saproof2.WinningPoStVerifyInfo{ + pvi2 := saproof7.WinningPoStVerifyInfo{ Randomness: abi.PoStRandomness(challenge[:]), Proofs: proof2, ChallengedSectors: candidates, Prover: mid, } - ok, err = ffiwrapper.ProofVerifier.VerifyWinningPoSt(context.TODO(), pvi2) + ok, err = ffiwrapper.ProofVerifier.VerifyWinningPoSt(context.TODO(), pvi2, 0, build.NewestNetworkVersion) if err != nil { return err } @@ -386,7 +404,7 @@ var sealBenchCmd = &cli.Command{ verifyWinningPost2 := time.Now() log.Info("computing window post snark (cold)") - wproof1, _, err := sb.GenerateWindowPoSt(context.TODO(), mid, sealedSectors, challenge[:]) + wproof1, _, err := sb.GenerateWindowPoSt(context.TODO(), mid, extendedSealedSectors, challenge[:]) if err != nil { return err } @@ -394,7 +412,7 @@ var sealBenchCmd = &cli.Command{ windowpost1 := time.Now() log.Info("computing window post snark (hot)") - wproof2, _, err := sb.GenerateWindowPoSt(context.TODO(), mid, sealedSectors, challenge[:]) + wproof2, _, err := sb.GenerateWindowPoSt(context.TODO(), mid, extendedSealedSectors, challenge[:]) if err != nil { return err } @@ -502,10 +520,10 @@ type ParCfg struct { Commit int } -func runSeals(sb *ffiwrapper.Sealer, sbfs *basicfs.Provider, numSectors int, par ParCfg, mid abi.ActorID, sectorSize abi.SectorSize, ticketPreimage []byte, saveC2inp string, skipc2, skipunseal bool) ([]SealingResult, []saproof2.SectorInfo, error) { +func runSeals(sb *ffiwrapper.Sealer, sbfs *basicfs.Provider, numSectors int, par ParCfg, mid abi.ActorID, sectorSize abi.SectorSize, ticketPreimage []byte, saveC2inp string, skipc2, skipunseal bool) ([]SealingResult, []saproof7.ExtendedSectorInfo, error) { var pieces []abi.PieceInfo sealTimings := make([]SealingResult, numSectors) - sealedSectors := make([]saproof2.SectorInfo, numSectors) + sealedSectors := make([]saproof7.ExtendedSectorInfo, numSectors) preCommit2Sema := make(chan struct{}, par.PreCommit2) commitSema := make(chan struct{}, par.Commit) @@ -579,10 +597,11 @@ func runSeals(sb *ffiwrapper.Sealer, sbfs *basicfs.Provider, numSectors int, par precommit2 := time.Now() <-preCommit2Sema - sealedSectors[i] = saproof2.SectorInfo{ + sealedSectors[i] = saproof7.ExtendedSectorInfo{ SealProof: sid.ProofType, SectorNumber: i, SealedCID: cids.Sealed, + SectorKey: nil, } seed := lapi.SealSeed{ diff --git a/cmd/lotus-miner/info.go b/cmd/lotus-miner/info.go index e50c4366e..39de942aa 100644 --- a/cmd/lotus-miner/info.go +++ b/cmd/lotus-miner/info.go @@ -470,6 +470,8 @@ var stateList = []stateMeta{ {col: color.FgBlue, state: sealing.Empty}, {col: color.FgBlue, state: sealing.WaitDeals}, {col: color.FgBlue, state: sealing.AddPiece}, + {col: color.FgBlue, state: sealing.SnapDealsWaitDeals}, + {col: color.FgBlue, state: sealing.SnapDealsAddPiece}, {col: color.FgRed, state: sealing.UndefinedSectorState}, {col: color.FgYellow, state: sealing.Packing}, @@ -488,6 +490,12 @@ var stateList = []stateMeta{ {col: color.FgYellow, state: sealing.SubmitCommitAggregate}, {col: color.FgYellow, state: sealing.CommitAggregateWait}, {col: color.FgYellow, state: sealing.FinalizeSector}, + {col: color.FgYellow, state: sealing.SnapDealsPacking}, + {col: color.FgYellow, state: sealing.UpdateReplica}, + {col: color.FgYellow, state: sealing.ProveReplicaUpdate}, + {col: color.FgYellow, state: sealing.SubmitReplicaUpdate}, + {col: color.FgYellow, state: sealing.ReplicaUpdateWait}, + {col: color.FgYellow, state: sealing.FinalizeReplicaUpdate}, {col: color.FgCyan, state: sealing.Terminating}, {col: color.FgCyan, state: sealing.TerminateWait}, @@ -495,6 +503,7 @@ var stateList = []stateMeta{ {col: color.FgCyan, state: sealing.TerminateFailed}, {col: color.FgCyan, state: sealing.Removing}, {col: color.FgCyan, state: sealing.Removed}, + {col: color.FgCyan, state: sealing.AbortUpgrade}, {col: color.FgRed, state: sealing.FailedUnrecoverable}, {col: color.FgRed, state: sealing.AddPieceFailed}, @@ -512,6 +521,9 @@ var stateList = []stateMeta{ {col: color.FgRed, state: sealing.RemoveFailed}, {col: color.FgRed, state: sealing.DealsExpired}, {col: color.FgRed, state: sealing.RecoverDealIDs}, + {col: color.FgRed, state: sealing.SnapDealsAddPieceFailed}, + {col: color.FgRed, state: sealing.SnapDealsDealsExpired}, + {col: color.FgRed, state: sealing.ReplicaUpdateFailed}, } func init() { diff --git a/cmd/lotus-miner/sectors.go b/cmd/lotus-miner/sectors.go index 43a71fd9e..3a17eac3a 100644 --- a/cmd/lotus-miner/sectors.go +++ b/cmd/lotus-miner/sectors.go @@ -20,6 +20,7 @@ import ( "github.com/filecoin-project/go-bitfield" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/big" + "github.com/filecoin-project/go-state-types/network" miner5 "github.com/filecoin-project/specs-actors/v5/actors/builtin/miner" "github.com/filecoin-project/lotus/api" @@ -50,11 +51,13 @@ var sectorsCmd = &cli.Command{ sectorsExtendCmd, sectorsTerminateCmd, sectorsRemoveCmd, + sectorsSnapUpCmd, sectorsMarkForUpgradeCmd, sectorsStartSealCmd, sectorsSealDelayCmd, sectorsCapacityCollateralCmd, sectorsBatching, + sectorsRefreshPieceMatchingCmd, }, } @@ -1476,6 +1479,44 @@ var sectorsRemoveCmd = &cli.Command{ }, } +var sectorsSnapUpCmd = &cli.Command{ + Name: "snap-up", + Usage: "Mark a committed capacity sector to be filled with deals", + ArgsUsage: "", + Action: func(cctx *cli.Context) error { + if cctx.Args().Len() != 1 { + return lcli.ShowHelp(cctx, xerrors.Errorf("must pass sector number")) + } + + nodeApi, closer, err := lcli.GetStorageMinerAPI(cctx) + if err != nil { + return err + } + defer closer() + api, nCloser, err := lcli.GetFullNodeAPI(cctx) + if err != nil { + return err + } + defer nCloser() + ctx := lcli.ReqContext(cctx) + + nv, err := api.StateNetworkVersion(ctx, types.EmptyTSK) + if err != nil { + return xerrors.Errorf("failed to get network version: %w", err) + } + if nv < network.Version15 { + return xerrors.Errorf("snap deals upgrades enabled in network v15") + } + + id, err := strconv.ParseUint(cctx.Args().Get(0), 10, 64) + if err != nil { + return xerrors.Errorf("could not parse sector number: %w", err) + } + + return nodeApi.SectorMarkForUpgrade(ctx, abi.SectorNumber(id), true) + }, +} + var sectorsMarkForUpgradeCmd = &cli.Command{ Name: "mark-for-upgrade", Usage: "Mark a committed capacity sector for replacement by a sector with deals", @@ -1490,14 +1531,28 @@ var sectorsMarkForUpgradeCmd = &cli.Command{ return err } defer closer() + + api, nCloser, err := lcli.GetFullNodeAPI(cctx) + if err != nil { + return err + } + defer nCloser() ctx := lcli.ReqContext(cctx) + nv, err := api.StateNetworkVersion(ctx, types.EmptyTSK) + if err != nil { + return xerrors.Errorf("failed to get network version: %w", err) + } + if nv >= network.Version15 { + return xerrors.Errorf("classic cc upgrades disabled v15 and beyond, use `snap-up`") + } + id, err := strconv.ParseUint(cctx.Args().Get(0), 10, 64) if err != nil { return xerrors.Errorf("could not parse sector number: %w", err) } - return nodeApi.SectorMarkForUpgrade(ctx, abi.SectorNumber(id)) + return nodeApi.SectorMarkForUpgrade(ctx, abi.SectorNumber(id), false) }, } @@ -1995,6 +2050,25 @@ var sectorsBatchingPendingPreCommit = &cli.Command{ }, } +var sectorsRefreshPieceMatchingCmd = &cli.Command{ + Name: "match-pending-pieces", + Usage: "force a refreshed match of pending pieces to open sectors without manually waiting for more deals", + Action: func(cctx *cli.Context) error { + nodeApi, closer, err := lcli.GetStorageMinerAPI(cctx) + if err != nil { + return err + } + defer closer() + ctx := lcli.ReqContext(cctx) + + if err := nodeApi.SectorMatchPendingPiecesToOpenSectors(ctx); err != nil { + return err + } + + return nil + }, +} + func yesno(b bool) string { if b { return color.GreenString("YES") diff --git a/cmd/lotus-seal-worker/main.go b/cmd/lotus-seal-worker/main.go index 5aec2f52f..e6d6c0b6f 100644 --- a/cmd/lotus-seal-worker/main.go +++ b/cmd/lotus-seal-worker/main.go @@ -163,6 +163,16 @@ var runCmd = &cli.Command{ Usage: "enable commit (32G sectors: all cores or GPUs, 128GiB Memory + 64GiB swap)", Value: true, }, + &cli.BoolFlag{ + Name: "replica-update", + Usage: "enable replica update", + Value: true, + }, + &cli.BoolFlag{ + Name: "prove-replica-update2", + Usage: "enable prove replica update 2", + Value: true, + }, &cli.IntFlag{ Name: "parallel-fetch-limit", Usage: "maximum fetch operations to run in parallel", @@ -268,6 +278,12 @@ var runCmd = &cli.Command{ if cctx.Bool("commit") { taskTypes = append(taskTypes, sealtasks.TTCommit2) } + if cctx.Bool("replicaupdate") { + taskTypes = append(taskTypes, sealtasks.TTReplicaUpdate) + } + if cctx.Bool("prove-replica-update2") { + taskTypes = append(taskTypes, sealtasks.TTProveReplicaUpdate2) + } if len(taskTypes) == 0 { return xerrors.Errorf("no task types specified") diff --git a/cmd/lotus-sim/simulation/mock/mock.go b/cmd/lotus-sim/simulation/mock/mock.go index 7656aaa28..70f9ba550 100644 --- a/cmd/lotus-sim/simulation/mock/mock.go +++ b/cmd/lotus-sim/simulation/mock/mock.go @@ -10,6 +10,7 @@ import ( "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/network" "github.com/ipfs/go-cid" logging "github.com/ipfs/go-log/v2" @@ -78,7 +79,7 @@ func (mockVerifier) VerifyReplicaUpdate(update proof7.ReplicaUpdateInfo) (bool, return false, nil } -func (mockVerifier) VerifyWinningPoSt(ctx context.Context, info proof5.WinningPoStVerifyInfo) (bool, error) { +func (mockVerifier) VerifyWinningPoSt(ctx context.Context, info proof7.WinningPoStVerifyInfo, poStEpoch abi.ChainEpoch, nv network.Version) (bool, error) { panic("should not be called") } func (mockVerifier) VerifyWindowPoSt(ctx context.Context, info proof5.WindowPoStVerifyInfo) (bool, error) { diff --git a/documentation/en/api-v0-methods-miner.md b/documentation/en/api-v0-methods-miner.md index 591a2b06b..1ff720677 100644 --- a/documentation/en/api-v0-methods-miner.md +++ b/documentation/en/api-v0-methods-miner.md @@ -118,6 +118,7 @@ * [SectorGetExpectedSealDuration](#SectorGetExpectedSealDuration) * [SectorGetSealDelay](#SectorGetSealDelay) * [SectorMarkForUpgrade](#SectorMarkForUpgrade) + * [SectorMatchPendingPiecesToOpenSectors](#SectorMatchPendingPiecesToOpenSectors) * [SectorPreCommitFlush](#SectorPreCommitFlush) * [SectorPreCommitPending](#SectorPreCommitPending) * [SectorRemove](#SectorRemove) @@ -330,7 +331,9 @@ Inputs: ```json [ null, - null + null, + 10101, + 15 ] ``` @@ -1936,12 +1939,22 @@ Perms: admin Inputs: ```json [ - 9 + 9, + true ] ``` Response: `{}` +### SectorMatchPendingPiecesToOpenSectors + + +Perms: admin + +Inputs: `null` + +Response: `{}` + ### SectorPreCommitFlush SectorPreCommitFlush immediately sends a PreCommit message with sectors batched for PreCommit. Returns null if message wasn't sent diff --git a/documentation/en/cli-lotus-miner.md b/documentation/en/cli-lotus-miner.md index dad0609ac..08fac332d 100644 --- a/documentation/en/cli-lotus-miner.md +++ b/documentation/en/cli-lotus-miner.md @@ -1500,23 +1500,25 @@ USAGE: lotus-miner sectors command [command options] [arguments...] COMMANDS: - status Get the seal status of a sector by its number - list List sectors - refs List References to sectors - update-state ADVANCED: manually update the state of a sector, this may aid in error recovery - pledge store random data in a sector - check-expire Inspect expiring sectors - expired Get or cleanup expired sectors - renew Renew expiring sectors while not exceeding each sector's max life - extend Extend sector expiration - terminate Terminate sector on-chain then remove (WARNING: This means losing power and collateral for the removed sector) - remove Forcefully remove a sector (WARNING: This means losing power and collateral for the removed sector (use 'terminate' for lower penalty)) - mark-for-upgrade Mark a committed capacity sector for replacement by a sector with deals - seal Manually start sealing a sector (filling any unused space with junk) - set-seal-delay Set the time, in minutes, that a new sector waits for deals before sealing starts - get-cc-collateral Get the collateral required to pledge a committed capacity sector - batching manage batch sector operations - help, h Shows a list of commands or help for one command + status Get the seal status of a sector by its number + list List sectors + refs List References to sectors + update-state ADVANCED: manually update the state of a sector, this may aid in error recovery + pledge store random data in a sector + check-expire Inspect expiring sectors + expired Get or cleanup expired sectors + renew Renew expiring sectors while not exceeding each sector's max life + extend Extend sector expiration + terminate Terminate sector on-chain then remove (WARNING: This means losing power and collateral for the removed sector) + remove Forcefully remove a sector (WARNING: This means losing power and collateral for the removed sector (use 'terminate' for lower penalty)) + snap-up Mark a committed capacity sector to be filled with deals + mark-for-upgrade Mark a committed capacity sector for replacement by a sector with deals + seal Manually start sealing a sector (filling any unused space with junk) + set-seal-delay Set the time, in minutes, that a new sector waits for deals before sealing starts + get-cc-collateral Get the collateral required to pledge a committed capacity sector + batching manage batch sector operations + match-pending-pieces force a refreshed match of pending pieces to open sectors without manually waiting for more deals + help, h Shows a list of commands or help for one command OPTIONS: --help, -h show help (default: false) @@ -1732,6 +1734,19 @@ OPTIONS: ``` +### lotus-miner sectors snap-up +``` +NAME: + lotus-miner sectors snap-up - Mark a committed capacity sector to be filled with deals + +USAGE: + lotus-miner sectors snap-up [command options] + +OPTIONS: + --help, -h show help (default: false) + +``` + ### lotus-miner sectors mark-for-upgrade ``` NAME: @@ -1832,6 +1847,19 @@ OPTIONS: ``` +### lotus-miner sectors match-pending-pieces +``` +NAME: + lotus-miner sectors match-pending-pieces - force a refreshed match of pending pieces to open sectors without manually waiting for more deals + +USAGE: + lotus-miner sectors match-pending-pieces [command options] [arguments...] + +OPTIONS: + --help, -h show help (default: false) + +``` + ## lotus-miner proving ``` NAME: diff --git a/documentation/en/cli-lotus-worker.md b/documentation/en/cli-lotus-worker.md index 9972128b5..0cfa38096 100644 --- a/documentation/en/cli-lotus-worker.md +++ b/documentation/en/cli-lotus-worker.md @@ -44,6 +44,8 @@ OPTIONS: --unseal enable unsealing (32G sectors: 1 core, 128GiB Memory) (default: true) --precommit2 enable precommit2 (32G sectors: all cores, 96GiB Memory) (default: true) --commit enable commit (32G sectors: all cores or GPUs, 128GiB Memory + 64GiB swap) (default: true) + --replica-update enable replica update (default: true) + --prove-replica-update2 enable prove replica update 2 (default: true) --parallel-fetch-limit value maximum fetch operations to run in parallel (default: 5) --timeout value used when 'listen' is unspecified. must be a valid duration recognized by golang's time.ParseDuration function (default: "30m") --help, -h show help (default: false) diff --git a/documentation/en/default-lotus-miner-config.toml b/documentation/en/default-lotus-miner-config.toml index d402f65ed..486ffed51 100644 --- a/documentation/en/default-lotus-miner-config.toml +++ b/documentation/en/default-lotus-miner-config.toml @@ -413,6 +413,12 @@ # env var: LOTUS_STORAGE_ALLOWUNSEAL #AllowUnseal = true + # env var: LOTUS_STORAGE_ALLOWREPLICAUPDATE + #AllowReplicaUpdate = true + + # env var: LOTUS_STORAGE_ALLOWPROVEREPLICAUPDATE2 + #AllowProveReplicaUpdate2 = true + # env var: LOTUS_STORAGE_RESOURCEFILTERING #ResourceFiltering = "hardware" diff --git a/extern/sector-storage/ffiwrapper/sealer_cgo.go b/extern/sector-storage/ffiwrapper/sealer_cgo.go index ec8554f34..e3939d3d1 100644 --- a/extern/sector-storage/ffiwrapper/sealer_cgo.go +++ b/extern/sector-storage/ffiwrapper/sealer_cgo.go @@ -714,7 +714,6 @@ func (sb *Sealer) ReplicaUpdate(ctx context.Context, sector storage.SectorRef, p if err != nil { return empty, xerrors.Errorf("failed to update replica %d with new deal data: %w", sector.ID.Number, err) } - return storage.ReplicaUpdateOut{NewSealed: sealed, NewUnsealed: unsealed}, nil } @@ -854,6 +853,14 @@ func (sb *Sealer) ReleaseUnsealed(ctx context.Context, sector storage.SectorRef, return xerrors.Errorf("not supported at this layer") } +func (sb *Sealer) ReleaseReplicaUpgrade(ctx context.Context, sector storage.SectorRef) error { + return xerrors.Errorf("not supported at this layer") +} + +func (sb *Sealer) ReleaseSectorKey(ctx context.Context, sector storage.SectorRef) error { + return xerrors.Errorf("not supported at this layer") +} + func (sb *Sealer) Remove(ctx context.Context, sector storage.SectorRef) error { return xerrors.Errorf("not supported at this layer") // happens in localworker } diff --git a/extern/sector-storage/ffiwrapper/sealer_test.go b/extern/sector-storage/ffiwrapper/sealer_test.go index 509efe532..cf8978464 100644 --- a/extern/sector-storage/ffiwrapper/sealer_test.go +++ b/extern/sector-storage/ffiwrapper/sealer_test.go @@ -19,6 +19,7 @@ import ( proof2 "github.com/filecoin-project/specs-actors/v2/actors/runtime/proof" proof5 "github.com/filecoin-project/specs-actors/v5/actors/runtime/proof" + proof7 "github.com/filecoin-project/specs-actors/v7/actors/runtime/proof" "github.com/ipfs/go-cid" @@ -180,16 +181,16 @@ func (s *seal) unseal(t *testing.T, sb *Sealer, sp *basicfs.Provider, si storage func post(t *testing.T, sealer *Sealer, skipped []abi.SectorID, seals ...seal) { randomness := abi.PoStRandomness{0, 9, 2, 7, 6, 5, 4, 3, 2, 1, 0, 9, 8, 7, 6, 45, 3, 2, 1, 0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 9, 7} - sis := make([]proof2.SectorInfo, len(seals)) + xsis := make([]proof7.ExtendedSectorInfo, len(seals)) for i, s := range seals { - sis[i] = proof2.SectorInfo{ + xsis[i] = proof7.ExtendedSectorInfo{ SealProof: s.ref.ProofType, SectorNumber: s.ref.ID.Number, SealedCID: s.cids.Sealed, } } - proofs, skp, err := sealer.GenerateWindowPoSt(context.TODO(), seals[0].ref.ID.Miner, sis, randomness) + proofs, skp, err := sealer.GenerateWindowPoSt(context.TODO(), seals[0].ref.ID.Miner, xsis, randomness) if len(skipped) > 0 { require.Error(t, err) require.EqualValues(t, skipped, skp) @@ -200,7 +201,16 @@ func post(t *testing.T, sealer *Sealer, skipped []abi.SectorID, seals ...seal) { t.Fatalf("%+v", err) } - ok, err := ProofVerifier.VerifyWindowPoSt(context.TODO(), proof2.WindowPoStVerifyInfo{ + sis := make([]proof7.SectorInfo, len(seals)) + for i, xsi := range xsis { + sis[i] = proof7.SectorInfo{ + SealProof: xsi.SealProof, + SectorNumber: xsi.SectorNumber, + SealedCID: xsi.SealedCID, + } + } + + ok, err := ProofVerifier.VerifyWindowPoSt(context.TODO(), proof7.WindowPoStVerifyInfo{ Randomness: randomness, Proofs: proofs, ChallengedSectors: sis, diff --git a/extern/sector-storage/ffiwrapper/types.go b/extern/sector-storage/ffiwrapper/types.go index 1da7ea832..78d2c6eca 100644 --- a/extern/sector-storage/ffiwrapper/types.go +++ b/extern/sector-storage/ffiwrapper/types.go @@ -4,13 +4,12 @@ import ( "context" "io" - proof7 "github.com/filecoin-project/specs-actors/v7/actors/runtime/proof" - - proof5 "github.com/filecoin-project/specs-actors/v5/actors/runtime/proof" + "github.com/filecoin-project/specs-actors/v7/actors/runtime/proof" "github.com/ipfs/go-cid" "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/network" "github.com/filecoin-project/specs-storage/storage" "github.com/filecoin-project/lotus/extern/sector-storage/ffiwrapper/basicfs" @@ -36,11 +35,11 @@ type Storage interface { } type Verifier interface { - VerifySeal(proof5.SealVerifyInfo) (bool, error) - VerifyAggregateSeals(aggregate proof5.AggregateSealVerifyProofAndInfos) (bool, error) - VerifyReplicaUpdate(update proof7.ReplicaUpdateInfo) (bool, error) - VerifyWinningPoSt(ctx context.Context, info proof5.WinningPoStVerifyInfo) (bool, error) - VerifyWindowPoSt(ctx context.Context, info proof5.WindowPoStVerifyInfo) (bool, error) + VerifySeal(proof.SealVerifyInfo) (bool, error) + VerifyAggregateSeals(aggregate proof.AggregateSealVerifyProofAndInfos) (bool, error) + VerifyReplicaUpdate(update proof.ReplicaUpdateInfo) (bool, error) + VerifyWinningPoSt(ctx context.Context, info proof.WinningPoStVerifyInfo, currEpoch abi.ChainEpoch, v network.Version) (bool, error) + VerifyWindowPoSt(ctx context.Context, info proof.WindowPoStVerifyInfo) (bool, error) GenerateWinningPoStSectorChallenge(context.Context, abi.RegisteredPoStProof, abi.ActorID, abi.PoStRandomness, uint64) ([]uint64, error) } @@ -49,7 +48,7 @@ type Verifier interface { type Prover interface { // TODO: move GenerateWinningPoStSectorChallenge from the Verifier interface to here - AggregateSealProofs(aggregateInfo proof5.AggregateSealVerifyProofAndInfos, proofs [][]byte) ([]byte, error) + AggregateSealProofs(aggregateInfo proof.AggregateSealVerifyProofAndInfos, proofs [][]byte) ([]byte, error) } type SectorProvider interface { diff --git a/extern/sector-storage/ffiwrapper/verifier_cgo.go b/extern/sector-storage/ffiwrapper/verifier_cgo.go index 66064b1f3..be38189f1 100644 --- a/extern/sector-storage/ffiwrapper/verifier_cgo.go +++ b/extern/sector-storage/ffiwrapper/verifier_cgo.go @@ -11,16 +11,17 @@ import ( ffi "github.com/filecoin-project/filecoin-ffi" "github.com/filecoin-project/go-state-types/abi" - proof5 "github.com/filecoin-project/specs-actors/v5/actors/runtime/proof" - proof7 "github.com/filecoin-project/specs-actors/v7/actors/runtime/proof" + "github.com/filecoin-project/go-state-types/network" + ffiproof "github.com/filecoin-project/specs-actors/v5/actors/runtime/proof" + "github.com/filecoin-project/specs-actors/v7/actors/runtime/proof" "github.com/filecoin-project/specs-storage/storage" "github.com/filecoin-project/lotus/extern/sector-storage/storiface" ) -func (sb *Sealer) GenerateWinningPoSt(ctx context.Context, minerID abi.ActorID, sectorInfo []proof5.SectorInfo, randomness abi.PoStRandomness) ([]proof5.PoStProof, error) { +func (sb *Sealer) GenerateWinningPoSt(ctx context.Context, minerID abi.ActorID, sectorInfo []proof.ExtendedSectorInfo, randomness abi.PoStRandomness) ([]proof.PoStProof, error) { randomness[31] &= 0x3f - privsectors, skipped, done, err := sb.pubSectorToPriv(ctx, minerID, sectorInfo, nil, abi.RegisteredSealProof.RegisteredWinningPoStProof) // TODO: FAULTS? + privsectors, skipped, done, err := sb.pubExtendedSectorToPriv(ctx, minerID, sectorInfo, nil, abi.RegisteredSealProof.RegisteredWinningPoStProof) // TODO: FAULTS? if err != nil { return nil, err } @@ -32,12 +33,13 @@ func (sb *Sealer) GenerateWinningPoSt(ctx context.Context, minerID abi.ActorID, return ffi.GenerateWinningPoSt(minerID, privsectors, randomness) } -func (sb *Sealer) GenerateWindowPoSt(ctx context.Context, minerID abi.ActorID, sectorInfo []proof5.SectorInfo, randomness abi.PoStRandomness) ([]proof5.PoStProof, []abi.SectorID, error) { +func (sb *Sealer) GenerateWindowPoSt(ctx context.Context, minerID abi.ActorID, sectorInfo []proof.ExtendedSectorInfo, randomness abi.PoStRandomness) ([]proof.PoStProof, []abi.SectorID, error) { randomness[31] &= 0x3f - privsectors, skipped, done, err := sb.pubSectorToPriv(ctx, minerID, sectorInfo, nil, abi.RegisteredSealProof.RegisteredWindowPoStProof) + privsectors, skipped, done, err := sb.pubExtendedSectorToPriv(ctx, minerID, sectorInfo, nil, abi.RegisteredSealProof.RegisteredWindowPoStProof) if err != nil { return nil, nil, xerrors.Errorf("gathering sector info: %w", err) } + defer done() if len(skipped) > 0 { @@ -53,11 +55,10 @@ func (sb *Sealer) GenerateWindowPoSt(ctx context.Context, minerID abi.ActorID, s Number: f, }) } - return proof, faultyIDs, err } -func (sb *Sealer) pubSectorToPriv(ctx context.Context, mid abi.ActorID, sectorInfo []proof5.SectorInfo, faults []abi.SectorNumber, rpt func(abi.RegisteredSealProof) (abi.RegisteredPoStProof, error)) (ffi.SortedPrivateSectorInfo, []abi.SectorID, func(), error) { +func (sb *Sealer) pubExtendedSectorToPriv(ctx context.Context, mid abi.ActorID, sectorInfo []proof.ExtendedSectorInfo, faults []abi.SectorNumber, rpt func(abi.RegisteredSealProof) (abi.RegisteredPoStProof, error)) (ffi.SortedPrivateSectorInfo, []abi.SectorID, func(), error) { fmap := map[abi.SectorNumber]struct{}{} for _, fault := range faults { fmap[fault] = struct{}{} @@ -81,14 +82,32 @@ func (sb *Sealer) pubSectorToPriv(ctx context.Context, mid abi.ActorID, sectorIn ID: abi.SectorID{Miner: mid, Number: s.SectorNumber}, ProofType: s.SealProof, } - - paths, d, err := sb.sectors.AcquireSector(ctx, sid, storiface.FTCache|storiface.FTSealed, 0, storiface.PathStorage) - if err != nil { - log.Warnw("failed to acquire sector, skipping", "sector", sid.ID, "error", err) - skipped = append(skipped, sid.ID) - continue + proveUpdate := s.SectorKey != nil + var cache string + var sealed string + if proveUpdate { + log.Debugf("Posting over updated sector for sector id: %d", s.SectorNumber) + paths, d, err := sb.sectors.AcquireSector(ctx, sid, storiface.FTUpdateCache|storiface.FTUpdate, 0, storiface.PathStorage) + if err != nil { + log.Warnw("failed to acquire FTUpdateCache and FTUpdate of sector, skipping", "sector", sid.ID, "error", err) + skipped = append(skipped, sid.ID) + continue + } + doneFuncs = append(doneFuncs, d) + cache = paths.UpdateCache + sealed = paths.Update + } else { + log.Debugf("Posting over sector key sector for sector id: %d", s.SectorNumber) + paths, d, err := sb.sectors.AcquireSector(ctx, sid, storiface.FTCache|storiface.FTSealed, 0, storiface.PathStorage) + if err != nil { + log.Warnw("failed to acquire FTCache and FTSealed of sector, skipping", "sector", sid.ID, "error", err) + skipped = append(skipped, sid.ID) + continue + } + doneFuncs = append(doneFuncs, d) + cache = paths.Cache + sealed = paths.Sealed } - doneFuncs = append(doneFuncs, d) postProofType, err := rpt(s.SealProof) if err != nil { @@ -96,11 +115,16 @@ func (sb *Sealer) pubSectorToPriv(ctx context.Context, mid abi.ActorID, sectorIn return ffi.SortedPrivateSectorInfo{}, nil, nil, xerrors.Errorf("acquiring registered PoSt proof from sector info %+v: %w", s, err) } + ffiInfo := ffiproof.SectorInfo{ + SealProof: s.SealProof, + SectorNumber: s.SectorNumber, + SealedCID: s.SealedCID, + } out = append(out, ffi.PrivateSectorInfo{ - CacheDirPath: paths.Cache, + CacheDirPath: cache, PoStProofType: postProofType, - SealedSectorPath: paths.Sealed, - SectorInfo: s, + SealedSectorPath: sealed, + SectorInfo: ffiInfo, }) } @@ -113,19 +137,19 @@ type proofVerifier struct{} var ProofVerifier = proofVerifier{} -func (proofVerifier) VerifySeal(info proof5.SealVerifyInfo) (bool, error) { +func (proofVerifier) VerifySeal(info proof.SealVerifyInfo) (bool, error) { return ffi.VerifySeal(info) } -func (proofVerifier) VerifyAggregateSeals(aggregate proof5.AggregateSealVerifyProofAndInfos) (bool, error) { +func (proofVerifier) VerifyAggregateSeals(aggregate proof.AggregateSealVerifyProofAndInfos) (bool, error) { return ffi.VerifyAggregateSeals(aggregate) } -func (proofVerifier) VerifyReplicaUpdate(update proof7.ReplicaUpdateInfo) (bool, error) { +func (proofVerifier) VerifyReplicaUpdate(update proof.ReplicaUpdateInfo) (bool, error) { return ffi.SectorUpdate.VerifyUpdateProof(update) } -func (proofVerifier) VerifyWinningPoSt(ctx context.Context, info proof5.WinningPoStVerifyInfo) (bool, error) { +func (proofVerifier) VerifyWinningPoSt(ctx context.Context, info proof.WinningPoStVerifyInfo, poStEpoch abi.ChainEpoch, version network.Version) (bool, error) { info.Randomness[31] &= 0x3f _, span := trace.StartSpan(ctx, "VerifyWinningPoSt") defer span.End() @@ -133,7 +157,7 @@ func (proofVerifier) VerifyWinningPoSt(ctx context.Context, info proof5.WinningP return ffi.VerifyWinningPoSt(info) } -func (proofVerifier) VerifyWindowPoSt(ctx context.Context, info proof5.WindowPoStVerifyInfo) (bool, error) { +func (proofVerifier) VerifyWindowPoSt(ctx context.Context, info proof.WindowPoStVerifyInfo) (bool, error) { info.Randomness[31] &= 0x3f _, span := trace.StartSpan(ctx, "VerifyWindowPoSt") defer span.End() diff --git a/extern/sector-storage/manager.go b/extern/sector-storage/manager.go index 748681544..ecabf0398 100644 --- a/extern/sector-storage/manager.go +++ b/extern/sector-storage/manager.go @@ -98,11 +98,13 @@ type SealerConfig struct { ParallelFetchLimit int // Local worker config - AllowAddPiece bool - AllowPreCommit1 bool - AllowPreCommit2 bool - AllowCommit bool - AllowUnseal bool + AllowAddPiece bool + AllowPreCommit1 bool + AllowPreCommit2 bool + AllowCommit bool + AllowUnseal bool + AllowReplicaUpdate bool + AllowProveReplicaUpdate2 bool // ResourceFiltering instructs the system which resource filtering strategy // to use when evaluating tasks against this worker. An empty value defaults @@ -144,7 +146,7 @@ func New(ctx context.Context, lstor *stores.Local, stor *stores.Remote, ls store go m.sched.runSched() localTasks := []sealtasks.TaskType{ - sealtasks.TTCommit1, sealtasks.TTFinalize, sealtasks.TTFetch, + sealtasks.TTCommit1, sealtasks.TTProveReplicaUpdate1, sealtasks.TTFinalize, sealtasks.TTFetch, } if sc.AllowAddPiece { localTasks = append(localTasks, sealtasks.TTAddPiece) @@ -161,6 +163,12 @@ func New(ctx context.Context, lstor *stores.Local, stor *stores.Remote, ls store if sc.AllowUnseal { localTasks = append(localTasks, sealtasks.TTUnseal) } + if sc.AllowReplicaUpdate { + localTasks = append(localTasks, sealtasks.TTReplicaUpdate) + } + if sc.AllowProveReplicaUpdate2 { + localTasks = append(localTasks, sealtasks.TTProveReplicaUpdate2) + } wcfg := WorkerConfig{ IgnoreResourceFiltering: sc.ResourceFiltering == ResourceFilteringDisabled, @@ -584,6 +592,23 @@ func (m *Manager) ReleaseSectorKey(ctx context.Context, sector storage.SectorRef return m.storage.Remove(ctx, sector.ID, storiface.FTSealed, true, nil) } +func (m *Manager) ReleaseReplicaUpgrade(ctx context.Context, sector storage.SectorRef) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + if err := m.index.StorageLock(ctx, sector.ID, storiface.FTNone, storiface.FTUpdateCache|storiface.FTUpdate); err != nil { + return xerrors.Errorf("acquiring sector lock: %w", err) + } + + if err := m.storage.Remove(ctx, sector.ID, storiface.FTUpdateCache, true, nil); err != nil { + return xerrors.Errorf("removing update cache: %w", err) + } + if err := m.storage.Remove(ctx, sector.ID, storiface.FTUpdate, true, nil); err != nil { + return xerrors.Errorf("removing update: %w", err) + } + return nil +} + func (m *Manager) GenerateSectorKeyFromData(ctx context.Context, sector storage.SectorRef, commD cid.Cid) error { ctx, cancel := context.WithCancel(ctx) @@ -666,7 +691,7 @@ func (m *Manager) Remove(ctx context.Context, sector storage.SectorRef) error { func (m *Manager) ReplicaUpdate(ctx context.Context, sector storage.SectorRef, pieces []abi.PieceInfo) (out storage.ReplicaUpdateOut, err error) { ctx, cancel := context.WithCancel(ctx) defer cancel() - + log.Errorf("manager is doing replica update") wk, wait, cancel, err := m.getWork(ctx, sealtasks.TTReplicaUpdate, sector, pieces) if err != nil { return storage.ReplicaUpdateOut{}, xerrors.Errorf("getWork: %w", err) @@ -677,7 +702,7 @@ func (m *Manager) ReplicaUpdate(ctx context.Context, sector storage.SectorRef, p waitRes := func() { p, werr := m.waitWork(ctx, wk) if werr != nil { - waitErr = werr + waitErr = xerrors.Errorf("waitWork: %w", werr) return } if p != nil { @@ -697,17 +722,17 @@ func (m *Manager) ReplicaUpdate(ctx context.Context, sector storage.SectorRef, p selector := newAllocSelector(m.index, storiface.FTUpdate|storiface.FTUpdateCache, storiface.PathSealing) err = m.sched.Schedule(ctx, sector, sealtasks.TTReplicaUpdate, selector, m.schedFetch(sector, storiface.FTSealed, storiface.PathSealing, storiface.AcquireCopy), func(ctx context.Context, w Worker) error { - + log.Errorf("scheduled work for replica update") err := m.startWork(ctx, w, wk)(w.ReplicaUpdate(ctx, sector, pieces)) if err != nil { - return err + return xerrors.Errorf("startWork: %w", err) } waitRes() return nil }) if err != nil { - return storage.ReplicaUpdateOut{}, err + return storage.ReplicaUpdateOut{}, xerrors.Errorf("Schedule: %w", err) } return out, waitErr } diff --git a/extern/sector-storage/mock/mock.go b/extern/sector-storage/mock/mock.go index ead4ebe26..7ef780087 100644 --- a/extern/sector-storage/mock/mock.go +++ b/extern/sector-storage/mock/mock.go @@ -10,14 +10,13 @@ import ( "math/rand" "sync" - proof7 "github.com/filecoin-project/specs-actors/v7/actors/runtime/proof" - - proof5 "github.com/filecoin-project/specs-actors/v5/actors/runtime/proof" + "github.com/filecoin-project/specs-actors/v7/actors/runtime/proof" "github.com/filecoin-project/dagstore/mount" ffiwrapper2 "github.com/filecoin-project/go-commp-utils/ffiwrapper" commcid "github.com/filecoin-project/go-fil-commcid" "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/network" "github.com/filecoin-project/specs-storage/storage" "github.com/ipfs/go-cid" logging "github.com/ipfs/go-log/v2" @@ -39,7 +38,7 @@ type SectorMgr struct { } type mockVerifProver struct { - aggregates map[string]proof5.AggregateSealVerifyProofAndInfos // used for logging bad verifies + aggregates map[string]proof.AggregateSealVerifyProofAndInfos // used for logging bad verifies } func NewMockSectorMgr(genesisSectors []abi.SectorID) *SectorMgr { @@ -336,14 +335,23 @@ func AddOpFinish(ctx context.Context) (context.Context, func()) { } } -func (mgr *SectorMgr) GenerateWinningPoSt(ctx context.Context, minerID abi.ActorID, sectorInfo []proof5.SectorInfo, randomness abi.PoStRandomness) ([]proof5.PoStProof, error) { +func (mgr *SectorMgr) GenerateWinningPoSt(ctx context.Context, minerID abi.ActorID, xSectorInfo []proof.ExtendedSectorInfo, randomness abi.PoStRandomness) ([]proof.PoStProof, error) { mgr.lk.Lock() defer mgr.lk.Unlock() + sectorInfo := make([]proof.SectorInfo, len(xSectorInfo)) + for i, xssi := range xSectorInfo { + sectorInfo[i] = proof.SectorInfo{ + SealProof: xssi.SealProof, + SectorNumber: xssi.SectorNumber, + SealedCID: xssi.SealedCID, + } + } + return generateFakePoSt(sectorInfo, abi.RegisteredSealProof.RegisteredWinningPoStProof, randomness), nil } -func (mgr *SectorMgr) GenerateWindowPoSt(ctx context.Context, minerID abi.ActorID, sectorInfo []proof5.SectorInfo, randomness abi.PoStRandomness) ([]proof5.PoStProof, []abi.SectorID, error) { +func (mgr *SectorMgr) GenerateWindowPoSt(ctx context.Context, minerID abi.ActorID, xSectorInfo []proof.ExtendedSectorInfo, randomness abi.PoStRandomness) ([]proof.PoStProof, []abi.SectorID, error) { mgr.lk.Lock() defer mgr.lk.Unlock() @@ -351,22 +359,22 @@ func (mgr *SectorMgr) GenerateWindowPoSt(ctx context.Context, minerID abi.ActorI return nil, nil, xerrors.Errorf("failed to post (mock)") } - si := make([]proof5.SectorInfo, 0, len(sectorInfo)) + si := make([]proof.ExtendedSectorInfo, 0, len(xSectorInfo)) var skipped []abi.SectorID var err error - for _, info := range sectorInfo { + for _, xsi := range xSectorInfo { sid := abi.SectorID{ Miner: minerID, - Number: info.SectorNumber, + Number: xsi.SectorNumber, } _, found := mgr.sectors[sid] if found && !mgr.sectors[sid].failed && !mgr.sectors[sid].corrupted { - si = append(si, info) + si = append(si, xsi) } else { skipped = append(skipped, sid) err = xerrors.Errorf("skipped some sectors") @@ -377,10 +385,19 @@ func (mgr *SectorMgr) GenerateWindowPoSt(ctx context.Context, minerID abi.ActorI return nil, skipped, err } - return generateFakePoSt(si, abi.RegisteredSealProof.RegisteredWindowPoStProof, randomness), skipped, nil + sectorInfo := make([]proof.SectorInfo, len(si)) + for i, xssi := range si { + sectorInfo[i] = proof.SectorInfo{ + SealProof: xssi.SealProof, + SectorNumber: xssi.SectorNumber, + SealedCID: xssi.SealedCID, + } + } + + return generateFakePoSt(sectorInfo, abi.RegisteredSealProof.RegisteredWindowPoStProof, randomness), skipped, nil } -func generateFakePoStProof(sectorInfo []proof5.SectorInfo, randomness abi.PoStRandomness) []byte { +func generateFakePoStProof(sectorInfo []proof.SectorInfo, randomness abi.PoStRandomness) []byte { randomness[31] &= 0x3f hasher := sha256.New() @@ -395,13 +412,13 @@ func generateFakePoStProof(sectorInfo []proof5.SectorInfo, randomness abi.PoStRa } -func generateFakePoSt(sectorInfo []proof5.SectorInfo, rpt func(abi.RegisteredSealProof) (abi.RegisteredPoStProof, error), randomness abi.PoStRandomness) []proof5.PoStProof { +func generateFakePoSt(sectorInfo []proof.SectorInfo, rpt func(abi.RegisteredSealProof) (abi.RegisteredPoStProof, error), randomness abi.PoStRandomness) []proof.PoStProof { wp, err := rpt(sectorInfo[0].SealProof) if err != nil { panic(err) } - return []proof5.PoStProof{ + return []proof.PoStProof{ { PoStProof: wp, ProofBytes: generateFakePoStProof(sectorInfo, randomness), @@ -465,6 +482,14 @@ func (mgr *SectorMgr) ReleaseUnsealed(ctx context.Context, sector storage.Sector return nil } +func (mgr *SectorMgr) ReleaseReplicaUpgrade(ctx context.Context, sector storage.SectorRef) error { + return nil +} + +func (mgr *SectorMgr) ReleaseSectorKey(ctx context.Context, sector storage.SectorRef) error { + return nil +} + func (mgr *SectorMgr) Remove(ctx context.Context, sector storage.SectorRef) error { mgr.lk.Lock() defer mgr.lk.Unlock() @@ -553,7 +578,7 @@ func (mgr *SectorMgr) ReturnGenerateSectorKeyFromData(ctx context.Context, callI panic("not supported") } -func (m mockVerifProver) VerifySeal(svi proof5.SealVerifyInfo) (bool, error) { +func (m mockVerifProver) VerifySeal(svi proof.SealVerifyInfo) (bool, error) { plen, err := svi.SealProof.ProofSize() if err != nil { return false, err @@ -574,7 +599,7 @@ func (m mockVerifProver) VerifySeal(svi proof5.SealVerifyInfo) (bool, error) { return true, nil } -func (m mockVerifProver) VerifyAggregateSeals(aggregate proof5.AggregateSealVerifyProofAndInfos) (bool, error) { +func (m mockVerifProver) VerifyAggregateSeals(aggregate proof.AggregateSealVerifyProofAndInfos) (bool, error) { out := make([]byte, m.aggLen(len(aggregate.Infos))) for pi, svi := range aggregate.Infos { for i := 0; i < 32; i++ { @@ -600,11 +625,11 @@ func (m mockVerifProver) VerifyAggregateSeals(aggregate proof5.AggregateSealVeri return ok, nil } -func (m mockVerifProver) VerifyReplicaUpdate(update proof7.ReplicaUpdateInfo) (bool, error) { +func (m mockVerifProver) VerifyReplicaUpdate(update proof.ReplicaUpdateInfo) (bool, error) { return true, nil } -func (m mockVerifProver) AggregateSealProofs(aggregateInfo proof5.AggregateSealVerifyProofAndInfos, proofs [][]byte) ([]byte, error) { +func (m mockVerifProver) AggregateSealProofs(aggregateInfo proof.AggregateSealVerifyProofAndInfos, proofs [][]byte) ([]byte, error) { out := make([]byte, m.aggLen(len(aggregateInfo.Infos))) // todo: figure out more real length for pi, proof := range proofs { for i := range proof[:32] { @@ -646,12 +671,12 @@ func (m mockVerifProver) aggLen(nproofs int) int { } } -func (m mockVerifProver) VerifyWinningPoSt(ctx context.Context, info proof5.WinningPoStVerifyInfo) (bool, error) { +func (m mockVerifProver) VerifyWinningPoSt(ctx context.Context, info proof.WinningPoStVerifyInfo, poStEpoch abi.ChainEpoch, nv network.Version) (bool, error) { info.Randomness[31] &= 0x3f return true, nil } -func (m mockVerifProver) VerifyWindowPoSt(ctx context.Context, info proof5.WindowPoStVerifyInfo) (bool, error) { +func (m mockVerifProver) VerifyWindowPoSt(ctx context.Context, info proof.WindowPoStVerifyInfo) (bool, error) { if len(info.Proofs) != 1 { return false, xerrors.Errorf("expected 1 proof entry") } @@ -674,7 +699,7 @@ func (m mockVerifProver) GenerateWinningPoStSectorChallenge(ctx context.Context, } var MockVerifier = mockVerifProver{ - aggregates: map[string]proof5.AggregateSealVerifyProofAndInfos{}, + aggregates: map[string]proof.AggregateSealVerifyProofAndInfos{}, } var MockProver = MockVerifier diff --git a/extern/sector-storage/teststorage_test.go b/extern/sector-storage/teststorage_test.go index 9fdb3a913..cb15184be 100644 --- a/extern/sector-storage/teststorage_test.go +++ b/extern/sector-storage/teststorage_test.go @@ -7,7 +7,7 @@ import ( "github.com/ipfs/go-cid" "github.com/filecoin-project/go-state-types/abi" - "github.com/filecoin-project/specs-actors/actors/runtime/proof" + "github.com/filecoin-project/specs-actors/v7/actors/runtime/proof" "github.com/filecoin-project/specs-storage/storage" "github.com/filecoin-project/lotus/extern/sector-storage/ffiwrapper" @@ -23,11 +23,11 @@ type testExec struct { apch chan chan apres } -func (t *testExec) GenerateWinningPoSt(ctx context.Context, minerID abi.ActorID, sectorInfo []proof.SectorInfo, randomness abi.PoStRandomness) ([]proof.PoStProof, error) { +func (t *testExec) GenerateWinningPoSt(ctx context.Context, minerID abi.ActorID, sectorInfo []proof.ExtendedSectorInfo, randomness abi.PoStRandomness) ([]proof.PoStProof, error) { panic("implement me") } -func (t *testExec) GenerateWindowPoSt(ctx context.Context, minerID abi.ActorID, sectorInfo []proof.SectorInfo, randomness abi.PoStRandomness) (proof []proof.PoStProof, skipped []abi.SectorID, err error) { +func (t *testExec) GenerateWindowPoSt(ctx context.Context, minerID abi.ActorID, sectorInfo []proof.ExtendedSectorInfo, randomness abi.PoStRandomness) (proof []proof.PoStProof, skipped []abi.SectorID, err error) { panic("implement me") } @@ -59,6 +59,14 @@ func (t *testExec) ReleaseSealed(ctx context.Context, sector storage.SectorRef) panic("implement me") } +func (t *testExec) ReleaseSectorKey(ctx context.Context, sector storage.SectorRef) error { + panic("implement me") +} + +func (t *testExec) ReleaseReplicaUpgrade(ctx context.Context, sector storage.SectorRef) error { + panic("implement me") +} + func (t *testExec) Remove(ctx context.Context, sector storage.SectorRef) error { panic("implement me") } diff --git a/extern/storage-sealing/cbor_gen.go b/extern/storage-sealing/cbor_gen.go index 1dfaf54a5..c1e2b08fa 100644 --- a/extern/storage-sealing/cbor_gen.go +++ b/extern/storage-sealing/cbor_gen.go @@ -143,7 +143,7 @@ func (t *SectorInfo) MarshalCBOR(w io.Writer) error { _, err := w.Write(cbg.CborNull) return err } - if _, err := w.Write([]byte{184, 26}); err != nil { + if _, err := w.Write([]byte{184, 32}); err != nil { return err } @@ -573,6 +573,137 @@ func (t *SectorInfo) MarshalCBOR(w io.Writer) error { return err } + // t.CCUpdate (bool) (bool) + if len("CCUpdate") > cbg.MaxLength { + return xerrors.Errorf("Value in field \"CCUpdate\" was too long") + } + + if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajTextString, uint64(len("CCUpdate"))); err != nil { + return err + } + if _, err := io.WriteString(w, string("CCUpdate")); err != nil { + return err + } + + if err := cbg.WriteBool(w, t.CCUpdate); err != nil { + return err + } + + // t.CCPieces ([]sealing.Piece) (slice) + if len("CCPieces") > cbg.MaxLength { + return xerrors.Errorf("Value in field \"CCPieces\" was too long") + } + + if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajTextString, uint64(len("CCPieces"))); err != nil { + return err + } + if _, err := io.WriteString(w, string("CCPieces")); err != nil { + return err + } + + if len(t.CCPieces) > cbg.MaxLength { + return xerrors.Errorf("Slice value in field t.CCPieces was too long") + } + + if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajArray, uint64(len(t.CCPieces))); err != nil { + return err + } + for _, v := range t.CCPieces { + if err := v.MarshalCBOR(w); err != nil { + return err + } + } + + // t.UpdateSealed (cid.Cid) (struct) + if len("UpdateSealed") > cbg.MaxLength { + return xerrors.Errorf("Value in field \"UpdateSealed\" was too long") + } + + if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajTextString, uint64(len("UpdateSealed"))); err != nil { + return err + } + if _, err := io.WriteString(w, string("UpdateSealed")); err != nil { + return err + } + + if t.UpdateSealed == nil { + if _, err := w.Write(cbg.CborNull); err != nil { + return err + } + } else { + if err := cbg.WriteCidBuf(scratch, w, *t.UpdateSealed); err != nil { + return xerrors.Errorf("failed to write cid field t.UpdateSealed: %w", err) + } + } + + // t.UpdateUnsealed (cid.Cid) (struct) + if len("UpdateUnsealed") > cbg.MaxLength { + return xerrors.Errorf("Value in field \"UpdateUnsealed\" was too long") + } + + if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajTextString, uint64(len("UpdateUnsealed"))); err != nil { + return err + } + if _, err := io.WriteString(w, string("UpdateUnsealed")); err != nil { + return err + } + + if t.UpdateUnsealed == nil { + if _, err := w.Write(cbg.CborNull); err != nil { + return err + } + } else { + if err := cbg.WriteCidBuf(scratch, w, *t.UpdateUnsealed); err != nil { + return xerrors.Errorf("failed to write cid field t.UpdateUnsealed: %w", err) + } + } + + // t.ReplicaUpdateProof (storage.ReplicaUpdateProof) (slice) + if len("ReplicaUpdateProof") > cbg.MaxLength { + return xerrors.Errorf("Value in field \"ReplicaUpdateProof\" was too long") + } + + if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajTextString, uint64(len("ReplicaUpdateProof"))); err != nil { + return err + } + if _, err := io.WriteString(w, string("ReplicaUpdateProof")); err != nil { + return err + } + + if len(t.ReplicaUpdateProof) > cbg.ByteArrayMaxLen { + return xerrors.Errorf("Byte array in field t.ReplicaUpdateProof was too long") + } + + if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajByteString, uint64(len(t.ReplicaUpdateProof))); err != nil { + return err + } + + if _, err := w.Write(t.ReplicaUpdateProof[:]); err != nil { + return err + } + + // t.ReplicaUpdateMessage (cid.Cid) (struct) + if len("ReplicaUpdateMessage") > cbg.MaxLength { + return xerrors.Errorf("Value in field \"ReplicaUpdateMessage\" was too long") + } + + if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajTextString, uint64(len("ReplicaUpdateMessage"))); err != nil { + return err + } + if _, err := io.WriteString(w, string("ReplicaUpdateMessage")); err != nil { + return err + } + + if t.ReplicaUpdateMessage == nil { + if _, err := w.Write(cbg.CborNull); err != nil { + return err + } + } else { + if err := cbg.WriteCidBuf(scratch, w, *t.ReplicaUpdateMessage); err != nil { + return xerrors.Errorf("failed to write cid field t.ReplicaUpdateMessage: %w", err) + } + } + // t.FaultReportMsg (cid.Cid) (struct) if len("FaultReportMsg") > cbg.MaxLength { return xerrors.Errorf("Value in field \"FaultReportMsg\" was too long") @@ -1166,6 +1297,145 @@ func (t *SectorInfo) UnmarshalCBOR(r io.Reader) error { } t.InvalidProofs = uint64(extra) + } + // t.CCUpdate (bool) (bool) + case "CCUpdate": + + maj, extra, err = cbg.CborReadHeaderBuf(br, scratch) + if err != nil { + return err + } + if maj != cbg.MajOther { + return fmt.Errorf("booleans must be major type 7") + } + switch extra { + case 20: + t.CCUpdate = false + case 21: + t.CCUpdate = true + default: + return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra) + } + // t.CCPieces ([]sealing.Piece) (slice) + case "CCPieces": + + maj, extra, err = cbg.CborReadHeaderBuf(br, scratch) + if err != nil { + return err + } + + if extra > cbg.MaxLength { + return fmt.Errorf("t.CCPieces: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.CCPieces = make([]Piece, extra) + } + + for i := 0; i < int(extra); i++ { + + var v Piece + if err := v.UnmarshalCBOR(br); err != nil { + return err + } + + t.CCPieces[i] = v + } + + // t.UpdateSealed (cid.Cid) (struct) + case "UpdateSealed": + + { + + b, err := br.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := br.UnreadByte(); err != nil { + return err + } + + c, err := cbg.ReadCid(br) + if err != nil { + return xerrors.Errorf("failed to read cid field t.UpdateSealed: %w", err) + } + + t.UpdateSealed = &c + } + + } + // t.UpdateUnsealed (cid.Cid) (struct) + case "UpdateUnsealed": + + { + + b, err := br.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := br.UnreadByte(); err != nil { + return err + } + + c, err := cbg.ReadCid(br) + if err != nil { + return xerrors.Errorf("failed to read cid field t.UpdateUnsealed: %w", err) + } + + t.UpdateUnsealed = &c + } + + } + // t.ReplicaUpdateProof (storage.ReplicaUpdateProof) (slice) + case "ReplicaUpdateProof": + + maj, extra, err = cbg.CborReadHeaderBuf(br, scratch) + if err != nil { + return err + } + + if extra > cbg.ByteArrayMaxLen { + return fmt.Errorf("t.ReplicaUpdateProof: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.ReplicaUpdateProof = make([]uint8, extra) + } + + if _, err := io.ReadFull(br, t.ReplicaUpdateProof[:]); err != nil { + return err + } + // t.ReplicaUpdateMessage (cid.Cid) (struct) + case "ReplicaUpdateMessage": + + { + + b, err := br.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := br.UnreadByte(); err != nil { + return err + } + + c, err := cbg.ReadCid(br) + if err != nil { + return xerrors.Errorf("failed to read cid field t.ReplicaUpdateMessage: %w", err) + } + + t.ReplicaUpdateMessage = &c + } + } // t.FaultReportMsg (cid.Cid) (struct) case "FaultReportMsg": diff --git a/extern/storage-sealing/checks.go b/extern/storage-sealing/checks.go index 74a791fcb..42425e782 100644 --- a/extern/storage-sealing/checks.go +++ b/extern/storage-sealing/checks.go @@ -35,6 +35,9 @@ type ErrInvalidProof struct{ error } type ErrNoPrecommit struct{ error } type ErrCommitWaitFailed struct{ error } +type ErrBadRU struct{ error } +type ErrBadPR struct{ error } + func checkPieces(ctx context.Context, maddr address.Address, si SectorInfo, api SealingAPI) error { tok, height, err := api.ChainHead(ctx) if err != nil { @@ -187,3 +190,32 @@ func (m *Sealing) checkCommit(ctx context.Context, si SectorInfo, proof []byte, return nil } + +// check that sector info is good after running a replica update +func checkReplicaUpdate(ctx context.Context, maddr address.Address, si SectorInfo, tok TipSetToken, api SealingAPI) error { + + if err := checkPieces(ctx, maddr, si, api); err != nil { + return err + } + if !si.CCUpdate { + return xerrors.Errorf("replica update on sector not marked for update") + } + + commD, err := api.StateComputeDataCommitment(ctx, maddr, si.SectorType, si.dealIDs(), tok) + if err != nil { + return &ErrApi{xerrors.Errorf("calling StateComputeDataCommitment: %w", err)} + } + if si.UpdateUnsealed == nil || !commD.Equals(*si.UpdateUnsealed) { + return &ErrBadRU{xerrors.Errorf("on chain CommD differs from sector: %s != %s", commD, si.CommD)} + } + + if si.UpdateSealed == nil { + return &ErrBadRU{xerrors.Errorf("nil sealed cid")} + } + if si.ReplicaUpdateProof == nil { + return ErrBadPR{xerrors.Errorf("nil PR2 proof")} + } + + return nil + +} diff --git a/extern/storage-sealing/fsm.go b/extern/storage-sealing/fsm.go index 10bec7e0b..83874e907 100644 --- a/extern/storage-sealing/fsm.go +++ b/extern/storage-sealing/fsm.go @@ -133,6 +133,44 @@ var fsmPlanners = map[SectorState]func(events []statemachine.Event, state *Secto on(SectorFinalizeFailed{}, FinalizeFailed), ), + // Snap deals + SnapDealsWaitDeals: planOne( + on(SectorAddPiece{}, SnapDealsAddPiece), + on(SectorStartPacking{}, SnapDealsPacking), + ), + SnapDealsAddPiece: planOne( + on(SectorPieceAdded{}, SnapDealsWaitDeals), + apply(SectorStartPacking{}), + apply(SectorAddPiece{}), + on(SectorAddPieceFailed{}, SnapDealsAddPieceFailed), + ), + SnapDealsPacking: planOne( + on(SectorPacked{}, UpdateReplica), + ), + UpdateReplica: planOne( + on(SectorReplicaUpdate{}, ProveReplicaUpdate), + on(SectorUpdateReplicaFailed{}, ReplicaUpdateFailed), + on(SectorDealsExpired{}, SnapDealsDealsExpired), + on(SectorInvalidDealIDs{}, SnapDealsRecoverDealIDs), + ), + ProveReplicaUpdate: planOne( + on(SectorProveReplicaUpdate{}, SubmitReplicaUpdate), + on(SectorProveReplicaUpdateFailed{}, ReplicaUpdateFailed), + on(SectorDealsExpired{}, SnapDealsDealsExpired), + on(SectorInvalidDealIDs{}, SnapDealsRecoverDealIDs), + ), + SubmitReplicaUpdate: planOne( + on(SectorReplicaUpdateSubmitted{}, ReplicaUpdateWait), + on(SectorSubmitReplicaUpdateFailed{}, ReplicaUpdateFailed), + ), + ReplicaUpdateWait: planOne( + on(SectorReplicaUpdateLanded{}, FinalizeReplicaUpdate), + on(SectorSubmitReplicaUpdateFailed{}, ReplicaUpdateFailed), + on(SectorAbortUpgrade{}, AbortUpgrade), + ), + FinalizeReplicaUpdate: planOne( + on(SectorFinalized{}, Proving), + ), // Sealing errors AddPieceFailed: planOne( @@ -188,11 +226,37 @@ var fsmPlanners = map[SectorState]func(events []statemachine.Event, state *Secto onReturning(SectorUpdateDealIDs{}), ), + // Snap Deals Errors + SnapDealsAddPieceFailed: planOne( + on(SectorRetryWaitDeals{}, SnapDealsWaitDeals), + apply(SectorStartPacking{}), + apply(SectorAddPiece{}), + ), + SnapDealsDealsExpired: planOne( + on(SectorAbortUpgrade{}, AbortUpgrade), + ), + SnapDealsRecoverDealIDs: planOne( + on(SectorUpdateDealIDs{}, SubmitReplicaUpdate), + on(SectorAbortUpgrade{}, AbortUpgrade), + ), + AbortUpgrade: planOneOrIgnore( + on(SectorRevertUpgradeToProving{}, Proving), + ), + ReplicaUpdateFailed: planOne( + on(SectorRetrySubmitReplicaUpdateWait{}, ReplicaUpdateWait), + on(SectorRetrySubmitReplicaUpdate{}, SubmitReplicaUpdate), + on(SectorRetryReplicaUpdate{}, UpdateReplica), + on(SectorRetryProveReplicaUpdate{}, ProveReplicaUpdate), + on(SectorInvalidDealIDs{}, SnapDealsRecoverDealIDs), + on(SectorDealsExpired{}, SnapDealsDealsExpired), + ), + // Post-seal Proving: planOne( on(SectorFaultReported{}, FaultReported), on(SectorFaulty{}, Faulty), + on(SectorStartCCUpdate{}, SnapDealsWaitDeals), ), Terminating: planOne( on(SectorTerminating{}, TerminateWait), @@ -209,7 +273,7 @@ var fsmPlanners = map[SectorState]func(events []statemachine.Event, state *Secto TerminateFailed: planOne( // SectorTerminating (global) ), - Removing: planOne( + Removing: planOneOrIgnore( on(SectorRemoved{}, Removed), on(SectorRemoveFailed{}, RemoveFailed), ), @@ -355,13 +419,6 @@ func (m *Sealing) plan(events []statemachine.Event, state *SectorInfo) (func(sta log.Errorw("update sector stats", "error", err) } - // todo: drop this, use Context iface everywhere - wrapCtx := func(f func(Context, SectorInfo) error) func(statemachine.Context, SectorInfo) error { - return func(ctx statemachine.Context, info SectorInfo) error { - return f(&ctx, info) - } - } - switch state.State { // Happy path case Empty: @@ -403,6 +460,24 @@ func (m *Sealing) plan(events []statemachine.Event, state *SectorInfo) (func(sta case FinalizeSector: return m.handleFinalizeSector, processed, nil + // Snap deals updates + case SnapDealsWaitDeals: + return m.handleWaitDeals, processed, nil + case SnapDealsAddPiece: + return m.handleAddPiece, processed, nil + case SnapDealsPacking: + return m.handlePacking, processed, nil + case UpdateReplica: + return m.handleReplicaUpdate, processed, nil + case ProveReplicaUpdate: + return m.handleProveReplicaUpdate, processed, nil + case SubmitReplicaUpdate: + return m.handleSubmitReplicaUpdate, processed, nil + case ReplicaUpdateWait: + return m.handleReplicaUpdateWait, processed, nil + case FinalizeReplicaUpdate: + return m.handleFinalizeReplicaUpdate, processed, nil + // Handled failure modes case AddPieceFailed: return m.handleAddPieceFailed, processed, nil @@ -426,7 +501,20 @@ func (m *Sealing) plan(events []statemachine.Event, state *SectorInfo) (func(sta case DealsExpired: return m.handleDealsExpired, processed, nil case RecoverDealIDs: - return wrapCtx(m.HandleRecoverDealIDs), processed, nil + return m.HandleRecoverDealIDs, processed, nil + + // Snap Deals failure modes + case SnapDealsAddPieceFailed: + return m.handleAddPieceFailed, processed, nil + + case SnapDealsDealsExpired: + return m.handleDealsExpiredSnapDeals, processed, nil + case SnapDealsRecoverDealIDs: + return m.handleSnapDealsRecoverDealIDs, processed, nil + case ReplicaUpdateFailed: + return m.handleSubmitReplicaUpdateFailed, processed, nil + case AbortUpgrade: + return m.handleAbortUpgrade, processed, nil // Post-seal case Proving: @@ -642,3 +730,16 @@ func planOne(ts ...func() (mut mutator, next func(*SectorInfo) (more bool, err e return uint64(len(events)), nil } } + +// planOne but ignores unhandled states without erroring, this prevents the need to handle all possible events creating +// error during forced override +func planOneOrIgnore(ts ...func() (mut mutator, next func(*SectorInfo) (more bool, err error))) func(events []statemachine.Event, state *SectorInfo) (uint64, error) { + f := planOne(ts...) + return func(events []statemachine.Event, state *SectorInfo) (uint64, error) { + cnt, err := f(events, state) + if err != nil { + log.Warnf("planOneOrIgnore: ignoring error from planOne: %s", err) + } + return cnt, nil + } +} diff --git a/extern/storage-sealing/fsm_events.go b/extern/storage-sealing/fsm_events.go index 650a81799..395c4b94a 100644 --- a/extern/storage-sealing/fsm_events.go +++ b/extern/storage-sealing/fsm_events.go @@ -295,6 +295,46 @@ type SectorFinalizeFailed struct{ error } func (evt SectorFinalizeFailed) FormatError(xerrors.Printer) (next error) { return evt.error } func (evt SectorFinalizeFailed) apply(*SectorInfo) {} +// Snap deals // CC update path + +type SectorStartCCUpdate struct{} + +func (evt SectorStartCCUpdate) apply(state *SectorInfo) { + state.CCUpdate = true + // Clear filler piece but remember in case of abort + state.CCPieces = state.Pieces + state.Pieces = nil +} + +type SectorReplicaUpdate struct { + Out storage.ReplicaUpdateOut +} + +func (evt SectorReplicaUpdate) apply(state *SectorInfo) { + state.UpdateSealed = &evt.Out.NewSealed + state.UpdateUnsealed = &evt.Out.NewUnsealed +} + +type SectorProveReplicaUpdate struct { + Proof storage.ReplicaUpdateProof +} + +func (evt SectorProveReplicaUpdate) apply(state *SectorInfo) { + state.ReplicaUpdateProof = evt.Proof +} + +type SectorReplicaUpdateSubmitted struct { + Message cid.Cid +} + +func (evt SectorReplicaUpdateSubmitted) apply(state *SectorInfo) { + state.ReplicaUpdateMessage = &evt.Message +} + +type SectorReplicaUpdateLanded struct{} + +func (evt SectorReplicaUpdateLanded) apply(state *SectorInfo) {} + // Failed state recovery type SectorRetrySealPreCommit1 struct{} @@ -351,6 +391,60 @@ func (evt SectorUpdateDealIDs) apply(state *SectorInfo) { } } +// Snap Deals failure and recovery + +type SectorRetryReplicaUpdate struct{} + +func (evt SectorRetryReplicaUpdate) apply(state *SectorInfo) {} + +type SectorRetryProveReplicaUpdate struct{} + +func (evt SectorRetryProveReplicaUpdate) apply(state *SectorInfo) {} + +type SectorUpdateReplicaFailed struct{ error } + +func (evt SectorUpdateReplicaFailed) FormatError(xerrors.Printer) (next error) { return evt.error } +func (evt SectorUpdateReplicaFailed) apply(state *SectorInfo) {} + +type SectorProveReplicaUpdateFailed struct{ error } + +func (evt SectorProveReplicaUpdateFailed) FormatError(xerrors.Printer) (next error) { + return evt.error +} +func (evt SectorProveReplicaUpdateFailed) apply(state *SectorInfo) {} + +type SectorAbortUpgrade struct{ error } + +func (evt SectorAbortUpgrade) apply(state *SectorInfo) {} +func (evt SectorAbortUpgrade) FormatError(xerrors.Printer) (next error) { + return evt.error +} + +type SectorRevertUpgradeToProving struct{} + +func (evt SectorRevertUpgradeToProving) apply(state *SectorInfo) { + // cleanup sector state so that it is back in proving + state.CCUpdate = false + state.UpdateSealed = nil + state.UpdateUnsealed = nil + state.ReplicaUpdateProof = nil + state.ReplicaUpdateMessage = nil + state.Pieces = state.CCPieces + state.CCPieces = nil +} + +type SectorRetrySubmitReplicaUpdateWait struct{} + +func (evt SectorRetrySubmitReplicaUpdateWait) apply(state *SectorInfo) {} + +type SectorRetrySubmitReplicaUpdate struct{} + +func (evt SectorRetrySubmitReplicaUpdate) apply(state *SectorInfo) {} + +type SectorSubmitReplicaUpdateFailed struct{} + +func (evt SectorSubmitReplicaUpdateFailed) apply(state *SectorInfo) {} + // Faults type SectorFaulty struct{} diff --git a/extern/storage-sealing/input.go b/extern/storage-sealing/input.go index 60c3a79e2..f3259f0cc 100644 --- a/extern/storage-sealing/input.go +++ b/extern/storage-sealing/input.go @@ -59,6 +59,8 @@ func (m *Sealing) handleWaitDeals(ctx statemachine.Context, sector SectorInfo) e return ctx.Send(SectorAddPiece{}) }, + number: sector.SectorNumber, + ccUpdate: sector.CCUpdate, } } else { // make sure we're only accounting for pieces which were correctly added @@ -329,6 +331,17 @@ func (m *Sealing) SectorAddPieceToAny(ctx context.Context, size abi.UnpaddedPiec return api.SectorOffset{Sector: res.sn, Offset: res.offset.Padded()}, res.err } +func (m *Sealing) MatchPendingPiecesToOpenSectors(ctx context.Context) error { + sp, err := m.currentSealProof(ctx) + if err != nil { + return xerrors.Errorf("failed to get current seal proof: %w", err) + } + log.Debug("pieces to sector matching waiting for lock") + m.inputLk.Lock() + defer m.inputLk.Unlock() + return m.updateInput(ctx, sp) +} + // called with m.inputLk func (m *Sealing) updateInput(ctx context.Context, sp abi.RegisteredSealProof) error { ssize, err := sp.SectorSize() @@ -356,8 +369,33 @@ func (m *Sealing) updateInput(ctx context.Context, sp abi.RegisteredSealProof) e toAssign[proposalCid] = struct{}{} + memo := make(map[abi.SectorNumber]abi.ChainEpoch) + expF := func(sn abi.SectorNumber) (abi.ChainEpoch, error) { + if exp, ok := memo[sn]; ok { + return exp, nil + } + onChainInfo, err := m.Api.StateSectorGetInfo(ctx, m.maddr, sn, TipSetToken{}) + if err != nil { + return 0, err + } + memo[sn] = onChainInfo.Expiration + return onChainInfo.Expiration, nil + } + for id, sector := range m.openSectors { avail := abi.PaddedPieceSize(ssize).Unpadded() - sector.used + // check that sector lifetime is long enough to fit deal using latest expiration from on chain + + ok, err := sector.dealFitsInLifetime(piece.deal.DealProposal.EndEpoch, expF) + if err != nil { + log.Errorf("failed to check expiration for cc Update sector %d", sector.number) + continue + } + if !ok { + exp, _ := expF(sector.number) + log.Infof("CC update sector %d cannot fit deal, expiration %d before deal end epoch %d", id, exp, piece.deal.DealProposal.EndEpoch) + continue + } if piece.size <= avail { // (note: if we have enough space for the piece, we also have enough space for inter-piece padding) matches = append(matches, match{ @@ -416,6 +454,7 @@ func (m *Sealing) updateInput(ctx context.Context, sp abi.RegisteredSealProof) e } if len(toAssign) > 0 { + log.Errorf("we are trying to create a new sector with open sectors %v", m.openSectors) if err := m.tryCreateDealSector(ctx, sp); err != nil { log.Errorw("Failed to create a new sector for deals", "error", err) } diff --git a/extern/storage-sealing/mocks/api.go b/extern/storage-sealing/mocks/api.go index cc8561dc7..95c222ecd 100644 --- a/extern/storage-sealing/mocks/api.go +++ b/extern/storage-sealing/mocks/api.go @@ -213,6 +213,21 @@ func (mr *MockSealingAPIMockRecorder) StateMarketStorageDealProposal(arg0, arg1, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateMarketStorageDealProposal", reflect.TypeOf((*MockSealingAPI)(nil).StateMarketStorageDealProposal), arg0, arg1, arg2) } +// StateMinerActiveSectors mocks base method. +func (m *MockSealingAPI) StateMinerActiveSectors(arg0 context.Context, arg1 address.Address, arg2 sealing.TipSetToken) ([]*miner.SectorOnChainInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StateMinerActiveSectors", arg0, arg1, arg2) + ret0, _ := ret[0].([]*miner.SectorOnChainInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StateMinerActiveSectors indicates an expected call of StateMinerActiveSectors. +func (mr *MockSealingAPIMockRecorder) StateMinerActiveSectors(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateMinerActiveSectors", reflect.TypeOf((*MockSealingAPI)(nil).StateMinerActiveSectors), arg0, arg1, arg2) +} + // StateMinerAvailableBalance mocks base method. func (m *MockSealingAPI) StateMinerAvailableBalance(arg0 context.Context, arg1 address.Address, arg2 sealing.TipSetToken) (big.Int, error) { m.ctrl.T.Helper() diff --git a/extern/storage-sealing/sealing.go b/extern/storage-sealing/sealing.go index 583bed052..81f6b38e9 100644 --- a/extern/storage-sealing/sealing.go +++ b/extern/storage-sealing/sealing.go @@ -63,6 +63,7 @@ type SealingAPI interface { StateMinerInfo(context.Context, address.Address, TipSetToken) (miner.MinerInfo, error) StateMinerAvailableBalance(context.Context, address.Address, TipSetToken) (big.Int, error) StateMinerSectorAllocated(context.Context, address.Address, abi.SectorNumber, TipSetToken) (bool, error) + StateMinerActiveSectors(context.Context, address.Address, TipSetToken) ([]*miner.SectorOnChainInfo, error) StateMarketStorageDeal(context.Context, abi.DealID, TipSetToken) (*api.MarketDeal, error) StateMarketStorageDealProposal(context.Context, abi.DealID, TipSetToken) (market.DealProposal, error) StateNetworkVersion(ctx context.Context, tok TipSetToken) (network.Version, error) @@ -121,11 +122,24 @@ type Sealing struct { } type openSector struct { - used abi.UnpaddedPieceSize // change to bitfield/rle when AddPiece gains offset support to better fill sectors + used abi.UnpaddedPieceSize // change to bitfield/rle when AddPiece gains offset support to better fill sectors + number abi.SectorNumber + ccUpdate bool maybeAccept func(cid.Cid) error // called with inputLk } +func (o *openSector) dealFitsInLifetime(dealEnd abi.ChainEpoch, expF func(sn abi.SectorNumber) (abi.ChainEpoch, error)) (bool, error) { + if !o.ccUpdate { + return true, nil + } + expiration, err := expF(o.number) + if err != nil { + return false, err + } + return expiration >= dealEnd, nil +} + type pendingPiece struct { size abi.UnpaddedPieceSize deal api.PieceDealInfo diff --git a/extern/storage-sealing/sector_state.go b/extern/storage-sealing/sector_state.go index b606de5ae..ba6df7ff4 100644 --- a/extern/storage-sealing/sector_state.go +++ b/extern/storage-sealing/sector_state.go @@ -3,50 +3,65 @@ package sealing type SectorState string var ExistSectorStateList = map[SectorState]struct{}{ - Empty: {}, - WaitDeals: {}, - Packing: {}, - AddPiece: {}, - AddPieceFailed: {}, - GetTicket: {}, - PreCommit1: {}, - PreCommit2: {}, - PreCommitting: {}, - PreCommitWait: {}, - SubmitPreCommitBatch: {}, - PreCommitBatchWait: {}, - WaitSeed: {}, - Committing: {}, - CommitFinalize: {}, - CommitFinalizeFailed: {}, - SubmitCommit: {}, - CommitWait: {}, - SubmitCommitAggregate: {}, - CommitAggregateWait: {}, - FinalizeSector: {}, - Proving: {}, - FailedUnrecoverable: {}, - SealPreCommit1Failed: {}, - SealPreCommit2Failed: {}, - PreCommitFailed: {}, - ComputeProofFailed: {}, - CommitFailed: {}, - PackingFailed: {}, - FinalizeFailed: {}, - DealsExpired: {}, - RecoverDealIDs: {}, - Faulty: {}, - FaultReported: {}, - FaultedFinal: {}, - Terminating: {}, - TerminateWait: {}, - TerminateFinality: {}, - TerminateFailed: {}, - Removing: {}, - RemoveFailed: {}, - Removed: {}, + Empty: {}, + WaitDeals: {}, + Packing: {}, + AddPiece: {}, + AddPieceFailed: {}, + GetTicket: {}, + PreCommit1: {}, + PreCommit2: {}, + PreCommitting: {}, + PreCommitWait: {}, + SubmitPreCommitBatch: {}, + PreCommitBatchWait: {}, + WaitSeed: {}, + Committing: {}, + CommitFinalize: {}, + CommitFinalizeFailed: {}, + SubmitCommit: {}, + CommitWait: {}, + SubmitCommitAggregate: {}, + CommitAggregateWait: {}, + FinalizeSector: {}, + Proving: {}, + FailedUnrecoverable: {}, + SealPreCommit1Failed: {}, + SealPreCommit2Failed: {}, + PreCommitFailed: {}, + ComputeProofFailed: {}, + CommitFailed: {}, + PackingFailed: {}, + FinalizeFailed: {}, + DealsExpired: {}, + RecoverDealIDs: {}, + Faulty: {}, + FaultReported: {}, + FaultedFinal: {}, + Terminating: {}, + TerminateWait: {}, + TerminateFinality: {}, + TerminateFailed: {}, + Removing: {}, + RemoveFailed: {}, + Removed: {}, + SnapDealsWaitDeals: {}, + SnapDealsAddPiece: {}, + SnapDealsPacking: {}, + UpdateReplica: {}, + ProveReplicaUpdate: {}, + SubmitReplicaUpdate: {}, + ReplicaUpdateWait: {}, + FinalizeReplicaUpdate: {}, + SnapDealsAddPieceFailed: {}, + SnapDealsDealsExpired: {}, + SnapDealsRecoverDealIDs: {}, + ReplicaUpdateFailed: {}, + AbortUpgrade: {}, } +// cmd/lotus-miner/info.go defines CLI colors corresponding to these states +// update files there when adding new states const ( UndefinedSectorState SectorState = "" @@ -79,6 +94,17 @@ const ( FinalizeSector SectorState = "FinalizeSector" Proving SectorState = "Proving" + + // snap deals / cc update + SnapDealsWaitDeals SectorState = "SnapDealsWaitDeals" + SnapDealsAddPiece SectorState = "SnapDealsAddPiece" + SnapDealsPacking SectorState = "SnapDealsPacking" + UpdateReplica SectorState = "UpdateReplica" + ProveReplicaUpdate SectorState = "ProveReplicaUpdate" + SubmitReplicaUpdate SectorState = "SubmitReplicaUpdate" + ReplicaUpdateWait SectorState = "ReplicaUpdateWait" + FinalizeReplicaUpdate SectorState = "FinalizeReplicaUpdate" + // error modes FailedUnrecoverable SectorState = "FailedUnrecoverable" AddPieceFailed SectorState = "AddPieceFailed" @@ -92,6 +118,13 @@ const ( DealsExpired SectorState = "DealsExpired" RecoverDealIDs SectorState = "RecoverDealIDs" + // snap deals error modes + SnapDealsAddPieceFailed SectorState = "SnapDealsAddPieceFailed" + SnapDealsDealsExpired SectorState = "SnapDealsDealsExpired" + SnapDealsRecoverDealIDs SectorState = "SnapDealsRecoverDealIDs" + AbortUpgrade SectorState = "AbortUpgrade" + ReplicaUpdateFailed SectorState = "ReplicaUpdateFailed" + Faulty SectorState = "Faulty" // sector is corrupted or gone for some reason FaultReported SectorState = "FaultReported" // sector has been declared as a fault on chain FaultedFinal SectorState = "FaultedFinal" // fault declared on chain @@ -108,11 +141,11 @@ const ( func toStatState(st SectorState, finEarly bool) statSectorState { switch st { - case UndefinedSectorState, Empty, WaitDeals, AddPiece, AddPieceFailed: + case UndefinedSectorState, Empty, WaitDeals, AddPiece, AddPieceFailed, SnapDealsWaitDeals, SnapDealsAddPiece: return sstStaging - case Packing, GetTicket, PreCommit1, PreCommit2, PreCommitting, PreCommitWait, SubmitPreCommitBatch, PreCommitBatchWait, WaitSeed, Committing, CommitFinalize, FinalizeSector: + case Packing, GetTicket, PreCommit1, PreCommit2, PreCommitting, PreCommitWait, SubmitPreCommitBatch, PreCommitBatchWait, WaitSeed, Committing, CommitFinalize, FinalizeSector, SnapDealsPacking, UpdateReplica, ProveReplicaUpdate, FinalizeReplicaUpdate: return sstSealing - case SubmitCommit, CommitWait, SubmitCommitAggregate, CommitAggregateWait: + case SubmitCommit, CommitWait, SubmitCommitAggregate, CommitAggregateWait, SubmitReplicaUpdate, ReplicaUpdateWait: if finEarly { // we use statSectorState for throttling storage use. With FinalizeEarly // we can consider sectors in states after CommitFinalize as finalized, so diff --git a/extern/storage-sealing/states_failed.go b/extern/storage-sealing/states_failed.go index 0c88cc384..a93cda3f5 100644 --- a/extern/storage-sealing/states_failed.go +++ b/extern/storage-sealing/states_failed.go @@ -1,11 +1,13 @@ package sealing import ( + "context" "time" "github.com/hashicorp/go-multierror" "golang.org/x/xerrors" + "github.com/filecoin-project/go-address" "github.com/filecoin-project/lotus/chain/actors/builtin/market" "github.com/filecoin-project/lotus/chain/actors/builtin/miner" @@ -181,6 +183,67 @@ func (m *Sealing) handleComputeProofFailed(ctx statemachine.Context, sector Sect return ctx.Send(SectorRetryComputeProof{}) } +func (m *Sealing) handleSubmitReplicaUpdateFailed(ctx statemachine.Context, sector SectorInfo) error { + if sector.ReplicaUpdateMessage != nil { + mw, err := m.Api.StateSearchMsg(ctx.Context(), *sector.ReplicaUpdateMessage) + if err != nil { + // API error + if err := failedCooldown(ctx, sector); err != nil { + return err + } + + return ctx.Send(SectorRetrySubmitReplicaUpdateWait{}) + } + + if mw == nil { + return ctx.Send(SectorRetrySubmitReplicaUpdateWait{}) + } + + switch mw.Receipt.ExitCode { + case exitcode.Ok: + return ctx.Send(SectorRetrySubmitReplicaUpdateWait{}) + case exitcode.SysErrOutOfGas: + return ctx.Send(SectorRetrySubmitReplicaUpdate{}) + default: + // something else went wrong + } + } + + tok, _, err := m.Api.ChainHead(ctx.Context()) + if err != nil { + log.Errorf("handleCommitting: api error, not proceeding: %+v", err) + return nil + } + + if err := checkReplicaUpdate(ctx.Context(), m.maddr, sector, tok, m.Api); err != nil { + switch err.(type) { + case *ErrApi: + log.Errorf("handleSubmitReplicaUpdateFailed: api error, not proceeding: %+v", err) + return nil + case *ErrBadRU: + log.Errorf("bad replica update: %+v", err) + return ctx.Send(SectorRetryReplicaUpdate{}) + case *ErrBadPR: + log.Errorf("bad PR1: +%v", err) + return ctx.Send(SectorRetryProveReplicaUpdate{}) + + case *ErrInvalidDeals: + return ctx.Send(SectorInvalidDealIDs{}) + case *ErrExpiredDeals: + return ctx.Send(SectorDealsExpired{xerrors.Errorf("expired dealIDs in sector: %w", err)}) + default: + log.Errorf("sanity check error, not proceeding: +%v", err) + return xerrors.Errorf("checkPieces sanity check error: %w", err) + } + } + + if err := failedCooldown(ctx, sector); err != nil { + return err + } + + return ctx.Send(SectorRetrySubmitReplicaUpdate{}) +} + func (m *Sealing) handleCommitFailed(ctx statemachine.Context, sector SectorInfo) error { tok, _, err := m.Api.ChainHead(ctx.Context()) if err != nil { @@ -319,61 +382,40 @@ func (m *Sealing) handleDealsExpired(ctx statemachine.Context, sector SectorInfo return ctx.Send(SectorRemove{}) } -func (m *Sealing) HandleRecoverDealIDs(ctx Context, sector SectorInfo) error { - tok, height, err := m.Api.ChainHead(ctx.Context()) +func (m *Sealing) handleDealsExpiredSnapDeals(ctx statemachine.Context, sector SectorInfo) error { + if !sector.CCUpdate { + // Should be impossible + return xerrors.Errorf("should never reach SnapDealsDealsExpired as a non-CCUpdate sector") + } + + return ctx.Send(SectorAbortUpgrade{xerrors.Errorf("one of upgrade deals expired")}) +} + +func (m *Sealing) handleAbortUpgrade(ctx statemachine.Context, sector SectorInfo) error { + if !sector.CCUpdate { + return xerrors.Errorf("should never reach AbortUpgrade as a non-CCUpdate sector") + } + + // Remove snap deals replica if any + if err := m.sealer.ReleaseReplicaUpgrade(ctx.Context(), m.minerSector(sector.SectorType, sector.SectorNumber)); err != nil { + return xerrors.Errorf("removing CC update files from sector storage") + } + return ctx.Send(SectorRevertUpgradeToProving{}) +} + +// failWith is a mutator or global mutator +func (m *Sealing) handleRecoverDealIDsOrFailWith(ctx statemachine.Context, sector SectorInfo, failWith interface{}) error { + toFix, paddingPieces, err := recoveryPiecesToFix(ctx.Context(), m.Api, sector, m.maddr) if err != nil { - return xerrors.Errorf("getting chain head: %w", err) + return err } - - var toFix []int - paddingPieces := 0 - - for i, p := range sector.Pieces { - // if no deal is associated with the piece, ensure that we added it as - // filler (i.e. ensure that it has a zero PieceCID) - if p.DealInfo == nil { - exp := zerocomm.ZeroPieceCommitment(p.Piece.Size.Unpadded()) - if !p.Piece.PieceCID.Equals(exp) { - return xerrors.Errorf("sector %d piece %d had non-zero PieceCID %+v", sector.SectorNumber, i, p.Piece.PieceCID) - } - paddingPieces++ - continue - } - - proposal, err := m.Api.StateMarketStorageDealProposal(ctx.Context(), p.DealInfo.DealID, tok) - if err != nil { - log.Warnf("getting deal %d for piece %d: %+v", p.DealInfo.DealID, i, err) - toFix = append(toFix, i) - continue - } - - if proposal.Provider != m.maddr { - log.Warnf("piece %d (of %d) of sector %d refers deal %d with wrong provider: %s != %s", i, len(sector.Pieces), sector.SectorNumber, p.DealInfo.DealID, proposal.Provider, m.maddr) - toFix = append(toFix, i) - continue - } - - if proposal.PieceCID != p.Piece.PieceCID { - log.Warnf("piece %d (of %d) of sector %d refers deal %d with wrong PieceCID: %s != %s", i, len(sector.Pieces), sector.SectorNumber, p.DealInfo.DealID, p.Piece.PieceCID, proposal.PieceCID) - toFix = append(toFix, i) - continue - } - - if p.Piece.Size != proposal.PieceSize { - log.Warnf("piece %d (of %d) of sector %d refers deal %d with different size: %d != %d", i, len(sector.Pieces), sector.SectorNumber, p.DealInfo.DealID, p.Piece.Size, proposal.PieceSize) - toFix = append(toFix, i) - continue - } - - if height >= proposal.StartEpoch { - // TODO: check if we are in an early enough state (before precommit), try to remove the offending pieces - // (tricky as we have to 'defragment' the sector while doing that, and update piece references for retrieval) - return xerrors.Errorf("can't fix sector deals: piece %d (of %d) of sector %d refers expired deal %d - should start at %d, head %d", i, len(sector.Pieces), sector.SectorNumber, p.DealInfo.DealID, proposal.StartEpoch, height) - } + tok, _, err := m.Api.ChainHead(ctx.Context()) + if err != nil { + return err } - failed := map[int]error{} updates := map[int]abi.DealID{} + for _, i := range toFix { p := sector.Pieces[i] @@ -430,3 +472,67 @@ func (m *Sealing) HandleRecoverDealIDs(ctx Context, sector SectorInfo) error { // Not much to do here, we can't go back in time to commit this sector return ctx.Send(SectorUpdateDealIDs{Updates: updates}) } + +func (m *Sealing) HandleRecoverDealIDs(ctx statemachine.Context, sector SectorInfo) error { + return m.handleRecoverDealIDsOrFailWith(ctx, sector, SectorRemove{}) +} + +func (m *Sealing) handleSnapDealsRecoverDealIDs(ctx statemachine.Context, sector SectorInfo) error { + return m.handleRecoverDealIDsOrFailWith(ctx, sector, SectorAbortUpgrade{}) +} + +func recoveryPiecesToFix(ctx context.Context, api SealingAPI, sector SectorInfo, maddr address.Address) ([]int, int, error) { + tok, height, err := api.ChainHead(ctx) + if err != nil { + return nil, 0, xerrors.Errorf("getting chain head: %w", err) + } + + var toFix []int + paddingPieces := 0 + + for i, p := range sector.Pieces { + // if no deal is associated with the piece, ensure that we added it as + // filler (i.e. ensure that it has a zero PieceCID) + if p.DealInfo == nil { + exp := zerocomm.ZeroPieceCommitment(p.Piece.Size.Unpadded()) + if !p.Piece.PieceCID.Equals(exp) { + return nil, 0, xerrors.Errorf("sector %d piece %d had non-zero PieceCID %+v", sector.SectorNumber, i, p.Piece.PieceCID) + } + paddingPieces++ + continue + } + + proposal, err := api.StateMarketStorageDealProposal(ctx, p.DealInfo.DealID, tok) + if err != nil { + log.Warnf("getting deal %d for piece %d: %+v", p.DealInfo.DealID, i, err) + toFix = append(toFix, i) + continue + } + + if proposal.Provider != maddr { + log.Warnf("piece %d (of %d) of sector %d refers deal %d with wrong provider: %s != %s", i, len(sector.Pieces), sector.SectorNumber, p.DealInfo.DealID, proposal.Provider, maddr) + toFix = append(toFix, i) + continue + } + + if proposal.PieceCID != p.Piece.PieceCID { + log.Warnf("piece %d (of %d) of sector %d refers deal %d with wrong PieceCID: %s != %s", i, len(sector.Pieces), sector.SectorNumber, p.DealInfo.DealID, p.Piece.PieceCID, proposal.PieceCID) + toFix = append(toFix, i) + continue + } + + if p.Piece.Size != proposal.PieceSize { + log.Warnf("piece %d (of %d) of sector %d refers deal %d with different size: %d != %d", i, len(sector.Pieces), sector.SectorNumber, p.DealInfo.DealID, p.Piece.Size, proposal.PieceSize) + toFix = append(toFix, i) + continue + } + + if height >= proposal.StartEpoch { + // TODO: check if we are in an early enough state (before precommit), try to remove the offending pieces + // (tricky as we have to 'defragment' the sector while doing that, and update piece references for retrieval) + return nil, 0, xerrors.Errorf("can't fix sector deals: piece %d (of %d) of sector %d refers expired deal %d - should start at %d, head %d", i, len(sector.Pieces), sector.SectorNumber, p.DealInfo.DealID, proposal.StartEpoch, height) + } + } + + return toFix, paddingPieces, nil +} diff --git a/extern/storage-sealing/states_failed_test.go b/extern/storage-sealing/states_failed_test.go index 22c245afd..86f69b11f 100644 --- a/extern/storage-sealing/states_failed_test.go +++ b/extern/storage-sealing/states_failed_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/filecoin-project/go-state-types/network" + statemachine "github.com/filecoin-project/go-statemachine" market0 "github.com/filecoin-project/specs-actors/actors/builtin/market" @@ -25,6 +26,7 @@ import ( ) func TestStateRecoverDealIDs(t *testing.T) { + t.Skip("Bring this back when we can correctly mock a state machine context: Issue #7867") mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() @@ -40,7 +42,7 @@ func TestStateRecoverDealIDs(t *testing.T) { sctx := mocks.NewMockContext(mockCtrl) sctx.EXPECT().Context().AnyTimes().Return(ctx) - api.EXPECT().ChainHead(ctx).Times(1).Return(nil, abi.ChainEpoch(10), nil) + api.EXPECT().ChainHead(ctx).Times(2).Return(nil, abi.ChainEpoch(10), nil) var dealId abi.DealID = 12 dealProposal := market.DealProposal{ @@ -70,7 +72,9 @@ func TestStateRecoverDealIDs(t *testing.T) { sctx.EXPECT().Send(sealing.SectorRemove{}).Return(nil) - err := fakeSealing.HandleRecoverDealIDs(sctx, sealing.SectorInfo{ + // TODO sctx should satisfy an interface so it can be useable for mocking. This will fail because we are passing in an empty context now to get this to build. + // https://github.com/filecoin-project/lotus/issues/7867 + err := fakeSealing.HandleRecoverDealIDs(statemachine.Context{}, sealing.SectorInfo{ Pieces: []sealing.Piece{ { DealInfo: &api2.PieceDealInfo{ diff --git a/extern/storage-sealing/states_replica_update.go b/extern/storage-sealing/states_replica_update.go new file mode 100644 index 000000000..28c5ede0b --- /dev/null +++ b/extern/storage-sealing/states_replica_update.go @@ -0,0 +1,209 @@ +package sealing + +import ( + "bytes" + + "github.com/filecoin-project/go-state-types/big" + "github.com/filecoin-project/go-state-types/exitcode" + statemachine "github.com/filecoin-project/go-statemachine" + api "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/chain/actors/builtin/miner" + "golang.org/x/xerrors" +) + +func (m *Sealing) handleReplicaUpdate(ctx statemachine.Context, sector SectorInfo) error { + if err := checkPieces(ctx.Context(), m.maddr, sector, m.Api); err != nil { // Sanity check state + switch err.(type) { + case *ErrApi: + log.Errorf("handleReplicaUpdate: api error, not proceeding: %+v", err) + return nil + case *ErrInvalidDeals: + log.Warnf("invalid deals in sector %d: %v", sector.SectorNumber, err) + return ctx.Send(SectorInvalidDealIDs{}) + case *ErrExpiredDeals: // Probably not much we can do here, maybe re-pack the sector? + return ctx.Send(SectorDealsExpired{xerrors.Errorf("expired dealIDs in sector: %w", err)}) + default: + return xerrors.Errorf("checkPieces sanity check error: %w", err) + } + } + out, err := m.sealer.ReplicaUpdate(sector.sealingCtx(ctx.Context()), m.minerSector(sector.SectorType, sector.SectorNumber), sector.pieceInfos()) + if err != nil { + return ctx.Send(SectorUpdateReplicaFailed{xerrors.Errorf("replica update failed: %w", err)}) + } + return ctx.Send(SectorReplicaUpdate{ + Out: out, + }) +} + +func (m *Sealing) handleProveReplicaUpdate(ctx statemachine.Context, sector SectorInfo) error { + if sector.UpdateSealed == nil || sector.UpdateUnsealed == nil { + return xerrors.Errorf("invalid sector %d with nil UpdateSealed or UpdateUnsealed output", sector.SectorNumber) + } + if sector.CommR == nil { + return xerrors.Errorf("invalid sector %d with nil CommR", sector.SectorNumber) + } + vanillaProofs, err := m.sealer.ProveReplicaUpdate1(sector.sealingCtx(ctx.Context()), m.minerSector(sector.SectorType, sector.SectorNumber), *sector.CommR, *sector.UpdateSealed, *sector.UpdateUnsealed) + if err != nil { + return ctx.Send(SectorProveReplicaUpdateFailed{xerrors.Errorf("prove replica update (1) failed: %w", err)}) + } + + proof, err := m.sealer.ProveReplicaUpdate2(sector.sealingCtx(ctx.Context()), m.minerSector(sector.SectorType, sector.SectorNumber), *sector.CommR, *sector.UpdateSealed, *sector.UpdateUnsealed, vanillaProofs) + if err != nil { + return ctx.Send(SectorProveReplicaUpdateFailed{xerrors.Errorf("prove replica update (2) failed: %w", err)}) + + } + return ctx.Send(SectorProveReplicaUpdate{ + Proof: proof, + }) +} + +func (m *Sealing) handleSubmitReplicaUpdate(ctx statemachine.Context, sector SectorInfo) error { + + tok, _, err := m.Api.ChainHead(ctx.Context()) + if err != nil { + log.Errorf("handleSubmitReplicaUpdate: api error, not proceeding: %+v", err) + return nil + } + + if err := checkReplicaUpdate(ctx.Context(), m.maddr, sector, tok, m.Api); err != nil { + return ctx.Send(SectorSubmitReplicaUpdateFailed{}) + } + + sl, err := m.Api.StateSectorPartition(ctx.Context(), m.maddr, sector.SectorNumber, tok) + if err != nil { + log.Errorf("handleSubmitReplicaUpdate: api error, not proceeding: %+v", err) + return nil + } + updateProof, err := sector.SectorType.RegisteredUpdateProof() + if err != nil { + log.Errorf("failed to get update proof type from seal proof: %+v", err) + return ctx.Send(SectorSubmitReplicaUpdateFailed{}) + } + enc := new(bytes.Buffer) + params := &miner.ProveReplicaUpdatesParams{ + Updates: []miner.ReplicaUpdate{ + { + SectorID: sector.SectorNumber, + Deadline: sl.Deadline, + Partition: sl.Partition, + NewSealedSectorCID: *sector.UpdateSealed, + Deals: sector.dealIDs(), + UpdateProofType: updateProof, + ReplicaProof: sector.ReplicaUpdateProof, + }, + }, + } + if err := params.MarshalCBOR(enc); err != nil { + log.Errorf("failed to serialize update replica params: %w", err) + return ctx.Send(SectorSubmitReplicaUpdateFailed{}) + } + + cfg, err := m.getConfig() + if err != nil { + return xerrors.Errorf("getting config: %w", err) + } + + onChainInfo, err := m.Api.StateSectorGetInfo(ctx.Context(), m.maddr, sector.SectorNumber, tok) + if err != nil { + log.Errorf("handleSubmitReplicaUpdate: api error, not proceeding: %+v", err) + return nil + } + sp, err := m.currentSealProof(ctx.Context()) + if err != nil { + log.Errorf("sealer failed to return current seal proof not proceeding: %+v", err) + return nil + } + virtualPCI := miner.SectorPreCommitInfo{ + SealProof: sp, + SectorNumber: sector.SectorNumber, + SealedCID: *sector.UpdateSealed, + //SealRandEpoch: 0, + DealIDs: sector.dealIDs(), + Expiration: onChainInfo.Expiration, + //ReplaceCapacity: false, + //ReplaceSectorDeadline: 0, + //ReplaceSectorPartition: 0, + //ReplaceSectorNumber: 0, + } + + collateral, err := m.Api.StateMinerInitialPledgeCollateral(ctx.Context(), m.maddr, virtualPCI, tok) + if err != nil { + return xerrors.Errorf("getting initial pledge collateral: %w", err) + } + + collateral = big.Sub(collateral, onChainInfo.InitialPledge) + if collateral.LessThan(big.Zero()) { + collateral = big.Zero() + } + + collateral, err = collateralSendAmount(ctx.Context(), m.Api, m.maddr, cfg, collateral) + if err != nil { + log.Errorf("collateral send amount failed not proceeding: %+v", err) + return nil + } + + goodFunds := big.Add(collateral, big.Int(m.feeCfg.MaxCommitGasFee)) + + mi, err := m.Api.StateMinerInfo(ctx.Context(), m.maddr, tok) + if err != nil { + log.Errorf("handleSubmitReplicaUpdate: api error, not proceeding: %+v", err) + return nil + } + + from, _, err := m.addrSel(ctx.Context(), mi, api.CommitAddr, goodFunds, collateral) + if err != nil { + log.Errorf("no good address to send replica update message from: %+v", err) + return ctx.Send(SectorSubmitReplicaUpdateFailed{}) + } + mcid, err := m.Api.SendMsg(ctx.Context(), from, m.maddr, miner.Methods.ProveReplicaUpdates, big.Zero(), big.Int(m.feeCfg.MaxCommitGasFee), enc.Bytes()) + if err != nil { + log.Errorf("handleSubmitReplicaUpdate: error sending message: %+v", err) + return ctx.Send(SectorSubmitReplicaUpdateFailed{}) + } + + return ctx.Send(SectorReplicaUpdateSubmitted{Message: mcid}) +} + +func (m *Sealing) handleReplicaUpdateWait(ctx statemachine.Context, sector SectorInfo) error { + if sector.ReplicaUpdateMessage == nil { + log.Errorf("handleReplicaUpdateWait: no replica update message cid recorded") + return ctx.Send(SectorSubmitReplicaUpdateFailed{}) + } + + mw, err := m.Api.StateWaitMsg(ctx.Context(), *sector.ReplicaUpdateMessage) + if err != nil { + log.Errorf("handleReplicaUpdateWait: failed to wait for message: %+v", err) + return ctx.Send(SectorSubmitReplicaUpdateFailed{}) + } + + switch mw.Receipt.ExitCode { + case exitcode.Ok: + //expected + case exitcode.SysErrInsufficientFunds: + fallthrough + case exitcode.SysErrOutOfGas: + log.Errorf("gas estimator was wrong or out of funds") + return ctx.Send(SectorSubmitReplicaUpdateFailed{}) + default: + return ctx.Send(SectorSubmitReplicaUpdateFailed{}) + } + si, err := m.Api.StateSectorGetInfo(ctx.Context(), m.maddr, sector.SectorNumber, mw.TipSetTok) + if err != nil { + log.Errorf("api err failed to get sector info: %+v", err) + return ctx.Send(SectorSubmitReplicaUpdateFailed{}) + } + if si == nil { + log.Errorf("api err sector not found") + return ctx.Send(SectorSubmitReplicaUpdateFailed{}) + } + + if !si.SealedCID.Equals(*sector.UpdateSealed) { + log.Errorf("mismatch of expected onchain sealed cid after replica update, expected %s got %s", sector.UpdateSealed, si.SealedCID) + return ctx.Send(SectorAbortUpgrade{}) + } + return ctx.Send(SectorReplicaUpdateLanded{}) +} + +func (m *Sealing) handleFinalizeReplicaUpdate(ctx statemachine.Context, sector SectorInfo) error { + return ctx.Send(SectorFinalized{}) +} diff --git a/extern/storage-sealing/states_sealing.go b/extern/storage-sealing/states_sealing.go index c6cd0bb49..2258250f4 100644 --- a/extern/storage-sealing/states_sealing.go +++ b/extern/storage-sealing/states_sealing.go @@ -280,8 +280,8 @@ func (m *Sealing) handlePreCommit2(ctx statemachine.Context, sector SectorInfo) } // TODO: We should probably invoke this method in most (if not all) state transition failures after handlePreCommitting -func (m *Sealing) remarkForUpgrade(sid abi.SectorNumber) { - err := m.MarkForUpgrade(sid) +func (m *Sealing) remarkForUpgrade(ctx context.Context, sid abi.SectorNumber) { + err := m.MarkForUpgrade(ctx, sid) if err != nil { log.Errorf("error re-marking sector %d as for upgrade: %+v", sid, err) } @@ -424,7 +424,7 @@ func (m *Sealing) handlePreCommitting(ctx statemachine.Context, sector SectorInf mcid, err := m.Api.SendMsg(ctx.Context(), from, m.maddr, miner.Methods.PreCommitSector, deposit, big.Int(m.feeCfg.MaxPreCommitGasFee), enc.Bytes()) if err != nil { if params.ReplaceCapacity { - m.remarkForUpgrade(params.ReplaceSectorNumber) + m.remarkForUpgrade(ctx.Context(), params.ReplaceSectorNumber) } return ctx.Send(SectorChainPreCommitFailed{xerrors.Errorf("pushing message to mpool: %w", err)}) } diff --git a/extern/storage-sealing/types.go b/extern/storage-sealing/types.go index aeb378f29..db53f43d3 100644 --- a/extern/storage-sealing/types.go +++ b/extern/storage-sealing/types.go @@ -73,7 +73,7 @@ type SectorInfo struct { // PreCommit2 CommD *cid.Cid - CommR *cid.Cid + CommR *cid.Cid // SectorKey Proof []byte PreCommitInfo *miner.SectorPreCommitInfo @@ -91,6 +91,14 @@ type SectorInfo struct { CommitMessage *cid.Cid InvalidProofs uint64 // failed proof computations (doesn't validate with proof inputs; can't compute) + // CCUpdate + CCUpdate bool + CCPieces []Piece + UpdateSealed *cid.Cid + UpdateUnsealed *cid.Cid + ReplicaUpdateProof storage.ReplicaUpdateProof + ReplicaUpdateMessage *cid.Cid + // Faults FaultReportMsg *cid.Cid diff --git a/extern/storage-sealing/upgrade_queue.go b/extern/storage-sealing/upgrade_queue.go index 02db41fde..aab1e67b0 100644 --- a/extern/storage-sealing/upgrade_queue.go +++ b/extern/storage-sealing/upgrade_queue.go @@ -4,6 +4,7 @@ import ( "context" "github.com/filecoin-project/lotus/chain/actors/builtin/miner" + market7 "github.com/filecoin-project/specs-actors/v7/actors/builtin/market" "golang.org/x/xerrors" @@ -18,7 +19,8 @@ func (m *Sealing) IsMarkedForUpgrade(id abi.SectorNumber) bool { return found } -func (m *Sealing) MarkForUpgrade(id abi.SectorNumber) error { +func (m *Sealing) MarkForUpgrade(ctx context.Context, id abi.SectorNumber) error { + m.upgradeLk.Lock() defer m.upgradeLk.Unlock() @@ -27,6 +29,37 @@ func (m *Sealing) MarkForUpgrade(id abi.SectorNumber) error { return xerrors.Errorf("sector %d already marked for upgrade", id) } + si, err := m.GetSectorInfo(id) + if err != nil { + return xerrors.Errorf("getting sector info: %w", err) + } + if si.State != Proving { + return xerrors.Errorf("can't mark sectors not in the 'Proving' state for upgrade") + } + if len(si.Pieces) != 1 { + return xerrors.Errorf("not a committed-capacity sector, expected 1 piece") + } + if si.Pieces[0].DealInfo != nil { + return xerrors.Errorf("not a committed-capacity sector, has deals") + } + + m.toUpgrade[id] = struct{}{} + + return nil +} + +func (m *Sealing) MarkForSnapUpgrade(ctx context.Context, id abi.SectorNumber) error { + cfg, err := m.getConfig() + if err != nil { + return xerrors.Errorf("getting storage config: %w", err) + } + + curStaging := m.stats.curStaging() + if cfg.MaxWaitDealsSectors > 0 && curStaging >= cfg.MaxWaitDealsSectors { + return xerrors.Errorf("already waiting for deals in %d >= %d (cfg.MaxWaitDealsSectors) sectors, no free resources to wait for deals in another", + curStaging, cfg.MaxWaitDealsSectors) + } + si, err := m.GetSectorInfo(id) if err != nil { return xerrors.Errorf("getting sector info: %w", err) @@ -44,11 +77,38 @@ func (m *Sealing) MarkForUpgrade(id abi.SectorNumber) error { return xerrors.Errorf("not a committed-capacity sector, has deals") } - // TODO: more checks to match actor constraints + tok, head, err := m.Api.ChainHead(ctx) + if err != nil { + return xerrors.Errorf("couldnt get chain head: %w", err) + } + onChainInfo, err := m.Api.StateSectorGetInfo(ctx, m.maddr, id, tok) + if err != nil { + return xerrors.Errorf("failed to read sector on chain info: %w", err) + } - m.toUpgrade[id] = struct{}{} + active, err := m.Api.StateMinerActiveSectors(ctx, m.maddr, tok) + if err != nil { + return xerrors.Errorf("failed to check active sectors: %w", err) + } + // Ensure the upgraded sector is active + var found bool + for _, si := range active { + if si.SectorNumber == id { + found = true + break + } + } + if !found { + return xerrors.Errorf("cannot mark inactive sector for upgrade") + } - return nil + if onChainInfo.Expiration-head < market7.DealMinDuration { + return xerrors.Errorf("pointless to upgrade sector %d, expiration %d is less than a min deal duration away from current epoch."+ + "Upgrade expiration before marking for upgrade", id, onChainInfo.Expiration) + } + + log.Errorf("updating sector number %d", id) + return m.sectors.Send(uint64(id), SectorStartCCUpdate{}) } func (m *Sealing) tryUpgradeSector(ctx context.Context, params *miner.SectorPreCommitInfo) big.Int { diff --git a/go.mod b/go.mod index 112b798fe..a580c738e 100644 --- a/go.mod +++ b/go.mod @@ -51,8 +51,8 @@ require ( github.com/filecoin-project/specs-actors/v4 v4.0.1 github.com/filecoin-project/specs-actors/v5 v5.0.4 github.com/filecoin-project/specs-actors/v6 v6.0.1 - github.com/filecoin-project/specs-actors/v7 v7.0.0-20211118013026-3dce48197cec - github.com/filecoin-project/specs-storage v0.1.1-0.20211202151826-2e51da61d454 + github.com/filecoin-project/specs-actors/v7 v7.0.0-20211230214648-aeae366b083a + github.com/filecoin-project/specs-storage v0.1.1-0.20211228030229-6d460d25a0c9 github.com/filecoin-project/test-vectors/schema v0.0.5 github.com/gbrlsnchs/jwt/v3 v3.0.1 github.com/gdamore/tcell/v2 v2.2.0 @@ -168,3 +168,7 @@ require ( replace github.com/filecoin-project/filecoin-ffi => ./extern/filecoin-ffi replace github.com/filecoin-project/test-vectors => ./extern/test-vectors + +//replace github.com/filecoin-project/specs-actors/v7 => /Users/zenground0/pl/repos/specs-actors + +// replace github.com/filecon-project/specs-storage => /Users/zenground0/pl/repos/specs-storage diff --git a/go.sum b/go.sum index f24bfc3ee..d188bd414 100644 --- a/go.sum +++ b/go.sum @@ -358,7 +358,6 @@ github.com/filecoin-project/go-padreader v0.0.1/go.mod h1:VYVPJqwpsfmtoHnAmPx6MU github.com/filecoin-project/go-paramfetch v0.0.3-0.20220111000201-e42866db1a53 h1:+nripp+UI/rhl01w9Gs4V0XDGaVPYPMGU/D/gNVLue0= github.com/filecoin-project/go-paramfetch v0.0.3-0.20220111000201-e42866db1a53/go.mod h1:1FH85P8U+DUEmWk1Jkw3Bw7FrwTVUNHk/95PSPG+dts= github.com/filecoin-project/go-state-types v0.0.0-20200903145444-247639ffa6ad/go.mod h1:IQ0MBPnonv35CJHtWSN3YY1Hz2gkPru1Q9qoaYLxx9I= -github.com/filecoin-project/go-state-types v0.0.0-20200904021452-1883f36ca2f4/go.mod h1:IQ0MBPnonv35CJHtWSN3YY1Hz2gkPru1Q9qoaYLxx9I= github.com/filecoin-project/go-state-types v0.0.0-20200928172055-2df22083d8ab/go.mod h1:ezYnPf0bNkTsDibL/psSz5dy4B5awOJ/E7P2Saeep8g= github.com/filecoin-project/go-state-types v0.0.0-20201102161440-c8033295a1fc/go.mod h1:ezYnPf0bNkTsDibL/psSz5dy4B5awOJ/E7P2Saeep8g= github.com/filecoin-project/go-state-types v0.1.0/go.mod h1:ezYnPf0bNkTsDibL/psSz5dy4B5awOJ/E7P2Saeep8g= @@ -374,7 +373,6 @@ github.com/filecoin-project/go-statestore v0.1.1 h1:ufMFq00VqnT2CAuDpcGnwLnCX1I/ github.com/filecoin-project/go-statestore v0.1.1/go.mod h1:LFc9hD+fRxPqiHiaqUEZOinUJB4WARkRfNl10O7kTnI= github.com/filecoin-project/go-storedcounter v0.0.0-20200421200003-1c99c62e8a5b h1:fkRZSPrYpk42PV3/lIXiL0LHetxde7vyYYvSsttQtfg= github.com/filecoin-project/go-storedcounter v0.0.0-20200421200003-1c99c62e8a5b/go.mod h1:Q0GQOBtKf1oE10eSXSlhN45kDBdGvEcVOqMiffqX+N8= -github.com/filecoin-project/specs-actors v0.9.4/go.mod h1:BStZQzx5x7TmCkLv0Bpa07U6cPKol6fd3w9KjMPZ6Z4= github.com/filecoin-project/specs-actors v0.9.12/go.mod h1:TS1AW/7LbG+615j4NsjMK1qlpAwaFsG9w0V2tg2gSao= github.com/filecoin-project/specs-actors v0.9.13/go.mod h1:TS1AW/7LbG+615j4NsjMK1qlpAwaFsG9w0V2tg2gSao= github.com/filecoin-project/specs-actors v0.9.14 h1:68PVstg2UB3ZsMLF+DKFTAs/YKsqhKWynkr0IqmVRQY= @@ -395,10 +393,11 @@ github.com/filecoin-project/specs-actors/v6 v6.0.0/go.mod h1:V1AYfi5GkHXipx1mnVi github.com/filecoin-project/specs-actors/v6 v6.0.1 h1:laxvHNsvrq83Y9n+W7znVCePi3oLyRf0Rkl4jFO8Wew= github.com/filecoin-project/specs-actors/v6 v6.0.1/go.mod h1:V1AYfi5GkHXipx1mnVivoICZh3wtwPxDVuds+fbfQtk= github.com/filecoin-project/specs-actors/v7 v7.0.0-20211117170924-fd07a4c7dff9/go.mod h1:p6LIOFezA1rgRLMewbvdi3Pp6SAu+q9FtJ9CAleSjrE= -github.com/filecoin-project/specs-actors/v7 v7.0.0-20211118013026-3dce48197cec h1:KV9vE+Sl2Y3qKsrpba4HcE7wHwK7v6O5U/S0xHbje6A= -github.com/filecoin-project/specs-actors/v7 v7.0.0-20211118013026-3dce48197cec/go.mod h1:p6LIOFezA1rgRLMewbvdi3Pp6SAu+q9FtJ9CAleSjrE= -github.com/filecoin-project/specs-storage v0.1.1-0.20211202151826-2e51da61d454 h1:9II9Xf+jq5xAPQiS4rVoKIiALINa3loMC+ghyFYIrqQ= -github.com/filecoin-project/specs-storage v0.1.1-0.20211202151826-2e51da61d454/go.mod h1:nJRRM7Aa9XVvygr3W9k6xGF46RWzr2zxF/iGoAIfA/g= +github.com/filecoin-project/specs-actors/v7 v7.0.0-20211222192039-c83bea50c402/go.mod h1:p6LIOFezA1rgRLMewbvdi3Pp6SAu+q9FtJ9CAleSjrE= +github.com/filecoin-project/specs-actors/v7 v7.0.0-20211230214648-aeae366b083a h1:MS1mtAhZh0iSE7OxP1bb6+UNyYKsxg8n51FpHlX1d54= +github.com/filecoin-project/specs-actors/v7 v7.0.0-20211230214648-aeae366b083a/go.mod h1:p6LIOFezA1rgRLMewbvdi3Pp6SAu+q9FtJ9CAleSjrE= +github.com/filecoin-project/specs-storage v0.1.1-0.20211228030229-6d460d25a0c9 h1:oUYOvF7EvdXS0Zmk9mNkaB6Bu0l+WXBYPzVodKMiLug= +github.com/filecoin-project/specs-storage v0.1.1-0.20211228030229-6d460d25a0c9/go.mod h1:Tb88Zq+IBJbvAn3mS89GYj3jdRThBTE/771HCVZdRJU= github.com/filecoin-project/test-vectors/schema v0.0.5 h1:w3zHQhzM4pYxJDl21avXjOKBLF8egrvwUwjpT8TquDg= github.com/filecoin-project/test-vectors/schema v0.0.5/go.mod h1:iQ9QXLpYWL3m7warwvK1JC/pTri8mnfEmKygNDqqY6E= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= @@ -750,7 +749,6 @@ github.com/ipfs/go-graphsync v0.4.3/go.mod h1:mPOwDYv128gf8gxPFgXnz4fNrSYPsWyqis github.com/ipfs/go-graphsync v0.10.0/go.mod h1:cKIshzTaa5rCZjryH5xmSKZVGX9uk1wvwGvz2WEha5Y= github.com/ipfs/go-graphsync v0.10.6 h1:GkYan4EoDslceHaqYo/hxktWtuZ7VmsyRXLdSmoCcBQ= github.com/ipfs/go-graphsync v0.10.6/go.mod h1:tQMjWNDD/vSz80YLT/VvzrUmy58aF9lR1uCwSLzjWzI= -github.com/ipfs/go-hamt-ipld v0.1.1/go.mod h1:1EZCr2v0jlCnhpa+aZ0JZYp8Tt2w16+JJOAVz17YcDk= github.com/ipfs/go-ipfs-blockstore v0.0.1/go.mod h1:d3WClOmRQKFnJ0Jz/jj/zmksX0ma1gROTlovZKBmN08= github.com/ipfs/go-ipfs-blockstore v0.1.0/go.mod h1:5aD0AvHPi7mZc6Ci1WCAhiBQu2IsfTduLl+422H6Rqw= github.com/ipfs/go-ipfs-blockstore v0.1.4/go.mod h1:Jxm3XMVjh6R17WvxFEiyKBLUGr86HgIYJW/D/MwqeYQ= diff --git a/itests/ccupgrade_test.go b/itests/ccupgrade_test.go index b5ca41416..487a15659 100644 --- a/itests/ccupgrade_test.go +++ b/itests/ccupgrade_test.go @@ -6,14 +6,16 @@ import ( "testing" "time" + "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/network" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/itests/kit" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// TODO: This needs to be repurposed into a SnapDeals test suite func TestCCUpgrade(t *testing.T) { kit.QuietMiningLogs() @@ -29,37 +31,45 @@ func TestCCUpgrade(t *testing.T) { } } -func runTestCCUpgrade(t *testing.T, upgradeHeight abi.ChainEpoch) { +func runTestCCUpgrade(t *testing.T, upgradeHeight abi.ChainEpoch) *kit.TestFullNode { ctx := context.Background() blockTime := 5 * time.Millisecond - client, miner, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.TurboUpgradeAt(upgradeHeight)) - ens.InterconnectAll().BeginMining(blockTime) + client, miner, ens := kit.EnsembleMinimal(t, kit.GenesisNetworkVersion(network.Version15)) + ens.InterconnectAll().BeginMiningMustPost(blockTime) maddr, err := miner.ActorAddress(ctx) if err != nil { t.Fatal(err) } - CC := abi.SectorNumber(kit.DefaultPresealsPerBootstrapMiner + 1) - Upgraded := CC + 1 + CCUpgrade := abi.SectorNumber(kit.DefaultPresealsPerBootstrapMiner + 1) + fmt.Printf("CCUpgrade: %d\n", CCUpgrade) + // wait for deadline 0 to pass so that committing starts after post on preseals + // this gives max time for post to complete minimizing chances of timeout + // waitForDeadline(ctx, t, 1, client, maddr) miner.PledgeSectors(ctx, 1, 0, nil) sl, err := miner.SectorsList(ctx) require.NoError(t, err) require.Len(t, sl, 1, "expected 1 sector") - require.Equal(t, CC, sl[0], "unexpected sector number") - + require.Equal(t, CCUpgrade, sl[0], "unexpected sector number") { - si, err := client.StateSectorGetInfo(ctx, maddr, CC, types.EmptyTSK) + si, err := client.StateSectorGetInfo(ctx, maddr, CCUpgrade, types.EmptyTSK) require.NoError(t, err) require.Less(t, 50000, int(si.Expiration)) } - err = miner.SectorMarkForUpgrade(ctx, sl[0]) + waitForSectorActive(ctx, t, CCUpgrade, client, maddr) + + err = miner.SectorMarkForUpgrade(ctx, sl[0], true) require.NoError(t, err) + sl, err = miner.SectorsList(ctx) + require.NoError(t, err) + require.Len(t, sl, 1, "expected 1 sector") + dh := kit.NewDealHarness(t, client, miner, miner) deal, res, inPath := dh.MakeOnlineDeal(ctx, kit.MakeFullDealParams{ Rseed: 6, @@ -68,37 +78,96 @@ func runTestCCUpgrade(t *testing.T, upgradeHeight abi.ChainEpoch) { outPath := dh.PerformRetrieval(context.Background(), deal, res.Root, false) kit.AssertFilesEqual(t, inPath, outPath) - // Validate upgrade - - { - exp, err := client.StateSectorExpiration(ctx, maddr, CC, types.EmptyTSK) - if err != nil { - require.Contains(t, err.Error(), "failed to find sector 3") // already cleaned up - } else { - require.NoError(t, err) - require.NotNil(t, exp) - require.Greater(t, 50000, int(exp.OnTime)) - } - } - { - exp, err := client.StateSectorExpiration(ctx, maddr, Upgraded, types.EmptyTSK) - require.NoError(t, err) - require.Less(t, 50000, int(exp.OnTime)) - } - - dlInfo, err := client.StateMinerProvingDeadline(ctx, maddr, types.EmptyTSK) + status, err := miner.SectorsStatus(ctx, CCUpgrade, true) require.NoError(t, err) + assert.Equal(t, 1, len(status.Deals)) + return client +} - // Sector should expire. +func waitForDeadline(ctx context.Context, t *testing.T, waitIdx uint64, node *kit.TestFullNode, maddr address.Address) { for { - // Wait for the sector to expire. - status, err := miner.SectorsStatus(ctx, CC, true) + ts, err := node.ChainHead(ctx) require.NoError(t, err) - if status.OnTime == 0 && status.Early == 0 { - break + dl, err := node.StateMinerProvingDeadline(ctx, maddr, ts.Key()) + require.NoError(t, err) + if dl.Index == waitIdx { + return } - t.Log("waiting for sector to expire") - // wait one deadline per loop. - time.Sleep(time.Duration(dlInfo.WPoStChallengeWindow) * blockTime) } } + +func waitForSectorActive(ctx context.Context, t *testing.T, sn abi.SectorNumber, node *kit.TestFullNode, maddr address.Address) { + for { + active, err := node.StateMinerActiveSectors(ctx, maddr, types.EmptyTSK) + require.NoError(t, err) + for _, si := range active { + if si.SectorNumber == sn { + fmt.Printf("ACTIVE\n") + return + } + } + + time.Sleep(time.Second) + } +} + +func TestCCUpgradeAndPoSt(t *testing.T) { + kit.QuietMiningLogs() + t.Run("upgrade and then post", func(t *testing.T) { + ctx := context.Background() + n := runTestCCUpgrade(t, 100) + ts, err := n.ChainHead(ctx) + require.NoError(t, err) + start := ts.Height() + // wait for a full proving period + n.WaitTillChain(ctx, func(ts *types.TipSet) bool { + if ts.Height() > start+abi.ChainEpoch(2880) { + return true + } + return false + }) + }) +} + +func TestTooManyMarkedForUpgrade(t *testing.T) { + kit.QuietMiningLogs() + + ctx := context.Background() + blockTime := 5 * time.Millisecond + + client, miner, ens := kit.EnsembleMinimal(t, kit.GenesisNetworkVersion(network.Version15)) + ens.InterconnectAll().BeginMining(blockTime) + + maddr, err := miner.ActorAddress(ctx) + if err != nil { + t.Fatal(err) + } + + CCUpgrade := abi.SectorNumber(kit.DefaultPresealsPerBootstrapMiner + 1) + waitForDeadline(ctx, t, 1, client, maddr) + miner.PledgeSectors(ctx, 3, 0, nil) + + sl, err := miner.SectorsList(ctx) + require.NoError(t, err) + require.Len(t, sl, 3, "expected 3 sectors") + + { + si, err := client.StateSectorGetInfo(ctx, maddr, CCUpgrade, types.EmptyTSK) + require.NoError(t, err) + require.Less(t, 50000, int(si.Expiration)) + } + + waitForSectorActive(ctx, t, CCUpgrade, client, maddr) + waitForSectorActive(ctx, t, CCUpgrade+1, client, maddr) + waitForSectorActive(ctx, t, CCUpgrade+2, client, maddr) + + err = miner.SectorMarkForUpgrade(ctx, CCUpgrade, true) + require.NoError(t, err) + err = miner.SectorMarkForUpgrade(ctx, CCUpgrade+1, true) + require.NoError(t, err) + + err = miner.SectorMarkForUpgrade(ctx, CCUpgrade+2, true) + require.Error(t, err) + assert.Contains(t, err.Error(), "no free resources to wait for deals") + +} diff --git a/itests/kit/blockminer.go b/itests/kit/blockminer.go index 2c9bd47c6..c1061b558 100644 --- a/itests/kit/blockminer.go +++ b/itests/kit/blockminer.go @@ -1,13 +1,18 @@ package kit import ( + "bytes" "context" "sync" "sync/atomic" "testing" "time" + "github.com/filecoin-project/go-bitfield" "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/lotus/api" + aminer "github.com/filecoin-project/lotus/chain/actors/builtin/miner" + "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/miner" "github.com/stretchr/testify/require" ) @@ -30,6 +35,138 @@ func NewBlockMiner(t *testing.T, miner *TestMiner) *BlockMiner { } } +type partitionTracker struct { + partitions []api.Partition + posted bitfield.BitField +} + +func newPartitionTracker(ctx context.Context, dlIdx uint64, bm *BlockMiner) *partitionTracker { + dlines, err := bm.miner.FullNode.StateMinerDeadlines(ctx, bm.miner.ActorAddr, types.EmptyTSK) + require.NoError(bm.t, err) + dl := dlines[dlIdx] + + parts, err := bm.miner.FullNode.StateMinerPartitions(ctx, bm.miner.ActorAddr, dlIdx, types.EmptyTSK) + require.NoError(bm.t, err) + return &partitionTracker{ + partitions: parts, + posted: dl.PostSubmissions, + } +} + +func (p *partitionTracker) count(t *testing.T) uint64 { + pCnt, err := p.posted.Count() + require.NoError(t, err) + return pCnt +} + +func (p *partitionTracker) done(t *testing.T) bool { + return uint64(len(p.partitions)) == p.count(t) +} + +func (p *partitionTracker) recordIfPost(t *testing.T, bm *BlockMiner, smsg *types.SignedMessage) (ret bool) { + defer func() { + ret = p.done(t) + }() + msg := smsg.Message + if !(msg.To == bm.miner.ActorAddr) { + return + } + if msg.Method != aminer.Methods.SubmitWindowedPoSt { + return + } + params := aminer.SubmitWindowedPoStParams{} + require.NoError(t, params.UnmarshalCBOR(bytes.NewReader(msg.Params))) + for _, part := range params.Partitions { + p.posted.Set(part.Index) + } + return +} + +// Like MineBlocks but refuses to mine until the window post scheduler has wdpost messages in the mempool +// and everything shuts down if a post fails +func (bm *BlockMiner) MineBlocksMustPost(ctx context.Context, blocktime time.Duration) { + + time.Sleep(time.Second) + + // wrap context in a cancellable context. + ctx, bm.cancel = context.WithCancel(ctx) + bm.wg.Add(1) + go func() { + defer bm.wg.Done() + + activeDeadlines := make(map[int]struct{}) + _ = activeDeadlines + + for { + select { + case <-time.After(blocktime): + case <-ctx.Done(): + return + } + nulls := atomic.SwapInt64(&bm.nextNulls, 0) + require.Equal(bm.t, int64(0), nulls, "Injecting > 0 null blocks while `MustPost` mining is currently unsupported") + + // Wake up and figure out if we are at the end of an active deadline + ts, err := bm.miner.FullNode.ChainHead(ctx) + require.NoError(bm.t, err) + tsk := ts.Key() + dlinfo, err := bm.miner.FullNode.StateMinerProvingDeadline(ctx, bm.miner.ActorAddr, tsk) + require.NoError(bm.t, err) + if ts.Height()+1 == dlinfo.Last() { // Last epoch in dline, we need to check that miner has posted + + tracker := newPartitionTracker(ctx, dlinfo.Index, bm) + if !tracker.done(bm.t) { // need to wait for post + bm.t.Logf("expect %d partitions proved but only see %d", len(tracker.partitions), tracker.count(bm.t)) + poolEvts, err := bm.miner.FullNode.MpoolSub(ctx) + require.NoError(bm.t, err) + + // First check pending messages we'll mine this epoch + msgs, err := bm.miner.FullNode.MpoolPending(ctx, types.EmptyTSK) + require.NoError(bm.t, err) + for _, msg := range msgs { + tracker.recordIfPost(bm.t, bm, msg) + } + + // post not yet in mpool, wait for it + if !tracker.done(bm.t) { + bm.t.Logf("post missing from mpool, block mining suspended until it arrives") + POOL: + for { + select { + case <-ctx.Done(): + return + case evt := <-poolEvts: + if evt.Type == api.MpoolAdd { + bm.t.Logf("incoming message %v", evt.Message) + if tracker.recordIfPost(bm.t, bm, evt.Message) { + break POOL + } + } + } + } + + } + + } + + } + + err = bm.miner.MineOne(ctx, miner.MineReq{ + InjectNulls: abi.ChainEpoch(nulls), + Done: func(bool, abi.ChainEpoch, error) {}, + }) + switch { + case err == nil: // wrap around + case ctx.Err() != nil: // context fired. + return + default: // log error + bm.t.Error(err) + } + } + }() + +} + func (bm *BlockMiner) MineBlocks(ctx context.Context, blocktime time.Duration) { time.Sleep(time.Second) diff --git a/itests/kit/deals.go b/itests/kit/deals.go index 651c15901..5a2121029 100644 --- a/itests/kit/deals.go +++ b/itests/kit/deals.go @@ -104,8 +104,9 @@ func (dh *DealHarness) MakeOnlineDeal(ctx context.Context, params MakeFullDealPa // TODO: this sleep is only necessary because deals don't immediately get logged in the dealstore, we should fix this time.Sleep(time.Second) + fmt.Printf("WAIT DEAL SEALEDS START\n") dh.WaitDealSealed(ctx, deal, false, false, nil) - + fmt.Printf("WAIT DEAL SEALEDS END\n") return deal, res, path } @@ -176,6 +177,7 @@ loop: cb() } } + fmt.Printf("WAIT DEAL SEALED LOOP BROKEN\n") } // WaitDealSealedQuiet waits until the deal is sealed, without logging anything. @@ -290,12 +292,11 @@ func (dh *DealHarness) WaitDealPublished(ctx context.Context, deal *cid.Cid) { func (dh *DealHarness) StartSealingWaiting(ctx context.Context) { snums, err := dh.main.SectorsList(ctx) require.NoError(dh.t, err) - for _, snum := range snums { si, err := dh.main.SectorsStatus(ctx, snum, false) require.NoError(dh.t, err) - dh.t.Logf("Sector state: %s", si.State) + dh.t.Logf("Sector state <%d>-[%d]:, %s", snum, si.SealProof, si.State) if si.State == api.SectorState(sealing.WaitDeals) { require.NoError(dh.t, dh.main.SectorStartSealing(ctx, snum)) } diff --git a/itests/kit/ensemble.go b/itests/kit/ensemble.go index 90a614645..fa983dbdf 100644 --- a/itests/kit/ensemble.go +++ b/itests/kit/ensemble.go @@ -675,6 +675,43 @@ func (n *Ensemble) Connect(from api.Net, to ...api.Net) *Ensemble { return n } +func (n *Ensemble) BeginMiningMustPost(blocktime time.Duration, miners ...*TestMiner) []*BlockMiner { + ctx := context.Background() + + // wait one second to make sure that nodes are connected and have handshaken. + // TODO make this deterministic by listening to identify events on the + // libp2p eventbus instead (or something else). + time.Sleep(1 * time.Second) + + var bms []*BlockMiner + if len(miners) == 0 { + // no miners have been provided explicitly, instantiate block miners + // for all active miners that aren't still mining. + for _, m := range n.active.miners { + if _, ok := n.active.bms[m]; ok { + continue // skip, already have a block miner + } + miners = append(miners, m) + } + } + + if len(miners) > 1 { + n.t.Fatalf("Only one active miner for MustPost, but have %d", len(miners)) + } + + for _, m := range miners { + bm := NewBlockMiner(n.t, m) + bm.MineBlocksMustPost(ctx, blocktime) + n.t.Cleanup(bm.Stop) + + bms = append(bms, bm) + + n.active.bms[m] = bm + } + + return bms +} + // BeginMining kicks off mining for the specified miners. If nil or 0-length, // it will kick off mining for all enrolled and active miners. It also adds a // cleanup function to stop all mining operations on test teardown. diff --git a/markets/storageadapter/ondealsectorcommitted.go b/markets/storageadapter/ondealsectorcommitted.go index 4cd0a2d68..94eaadef4 100644 --- a/markets/storageadapter/ondealsectorcommitted.go +++ b/markets/storageadapter/ondealsectorcommitted.go @@ -5,6 +5,7 @@ import ( "context" "sync" + "github.com/filecoin-project/go-bitfield" sealing "github.com/filecoin-project/lotus/extern/storage-sealing" "github.com/ipfs/go-cid" "golang.org/x/xerrors" @@ -110,7 +111,7 @@ func (mgr *SectorCommittedManager) OnDealSectorPreCommitted(ctx context.Context, // Watch for a pre-commit message to the provider. matchEvent := func(msg *types.Message) (bool, error) { - matched := msg.To == provider && (msg.Method == miner.Methods.PreCommitSector || msg.Method == miner.Methods.PreCommitSectorBatch) + matched := msg.To == provider && (msg.Method == miner.Methods.PreCommitSector || msg.Method == miner.Methods.PreCommitSectorBatch || msg.Method == miner.Methods.ProveReplicaUpdates) return matched, nil } @@ -145,6 +146,20 @@ func (mgr *SectorCommittedManager) OnDealSectorPreCommitted(ctx context.Context, return false, err } + // If this is a replica update method that succeeded the deal is active + if msg.Method == miner.Methods.ProveReplicaUpdates { + sn, err := dealSectorInReplicaUpdateSuccess(msg, rec, res) + if err != nil { + return false, err + } + if sn != nil { + cb(*sn, true, nil) + return false, nil + } + // Didn't find the deal ID in this message, so keep looking + return true, nil + } + // Extract the message parameters sn, err := dealSectorInPreCommitMsg(msg, res) if err != nil { @@ -264,6 +279,42 @@ func (mgr *SectorCommittedManager) OnDealSectorCommitted(ctx context.Context, pr return nil } +func dealSectorInReplicaUpdateSuccess(msg *types.Message, rec *types.MessageReceipt, res sealing.CurrentDealInfo) (*abi.SectorNumber, error) { + var params miner.ProveReplicaUpdatesParams + if err := params.UnmarshalCBOR(bytes.NewReader(msg.Params)); err != nil { + return nil, xerrors.Errorf("unmarshal prove replica update: %w", err) + } + + var seekUpdate miner.ReplicaUpdate + var found bool + for _, update := range params.Updates { + for _, did := range update.Deals { + if did == res.DealID { + seekUpdate = update + found = true + break + } + } + } + if !found { + return nil, nil + } + + // check that this update passed validation steps + var successBf bitfield.BitField + if err := successBf.UnmarshalCBOR(bytes.NewReader(rec.Return)); err != nil { + return nil, xerrors.Errorf("unmarshal return value: %w", err) + } + success, err := successBf.IsSet(uint64(seekUpdate.SectorID)) + if err != nil { + return nil, xerrors.Errorf("failed to check success of replica update: %w", err) + } + if !success { + return nil, xerrors.Errorf("replica update %d failed", seekUpdate.SectorID) + } + return &seekUpdate.SectorID, nil +} + // dealSectorInPreCommitMsg tries to find a sector containing the specified deal func dealSectorInPreCommitMsg(msg *types.Message, res sealing.CurrentDealInfo) (*abi.SectorNumber, error) { switch msg.Method { diff --git a/miner/miner.go b/miner/miner.go index 582ade723..22e4e9085 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -535,8 +535,12 @@ func (m *Miner) mineOne(ctx context.Context, base *MiningBase) (minedBlock *type prand := abi.PoStRandomness(rand) tSeed := build.Clock.Now() + nv, err := m.api.StateNetworkVersion(ctx, base.TipSet.Key()) + if err != nil { + return nil, err + } - postProof, err := m.epp.ComputeProof(ctx, mbi.Sectors, prand) + postProof, err := m.epp.ComputeProof(ctx, mbi.Sectors, prand, round, nv) if err != nil { err = xerrors.Errorf("failed to compute winning post proof: %w", err) return nil, err diff --git a/miner/warmup.go b/miner/warmup.go index 991679c09..be5ac3ea7 100644 --- a/miner/warmup.go +++ b/miner/warmup.go @@ -10,8 +10,7 @@ import ( "github.com/filecoin-project/go-bitfield" "github.com/filecoin-project/go-state-types/abi" - - proof2 "github.com/filecoin-project/specs-actors/v2/actors/runtime/proof" + proof7 "github.com/filecoin-project/specs-actors/v7/actors/runtime/proof" "github.com/filecoin-project/lotus/chain/types" ) @@ -61,13 +60,22 @@ out: return xerrors.Errorf("getting sector info: %w", err) } - _, err = m.epp.ComputeProof(ctx, []proof2.SectorInfo{ + ts, err := m.api.ChainHead(ctx) + if err != nil { + return xerrors.Errorf("getting chain head") + } + nv, err := m.api.StateNetworkVersion(ctx, ts.Key()) + if err != nil { + return xerrors.Errorf("getting network version") + } + + _, err = m.epp.ComputeProof(ctx, []proof7.ExtendedSectorInfo{ { SealProof: si.SealProof, SectorNumber: sector, SealedCID: si.SealedCID, }, - }, r) + }, r, ts.Height(), nv) if err != nil { return xerrors.Errorf("failed to compute proof: %w", err) } diff --git a/node/config/def.go b/node/config/def.go index 735107e29..e89d480b2 100644 --- a/node/config/def.go +++ b/node/config/def.go @@ -109,10 +109,11 @@ func DefaultStorageMiner() *StorageMiner { AvailableBalanceBuffer: types.FIL(big.Zero()), DisableCollateralFallback: false, - BatchPreCommits: true, - MaxPreCommitBatch: miner5.PreCommitSectorBatchMaxSize, // up to 256 sectors - PreCommitBatchWait: Duration(24 * time.Hour), // this should be less than 31.5 hours, which is the expiration of a precommit ticket - PreCommitBatchSlack: Duration(3 * time.Hour), // time buffer for forceful batch submission before sectors/deals in batch would start expiring, higher value will lower the chances for message fail due to expiration + BatchPreCommits: true, + MaxPreCommitBatch: miner5.PreCommitSectorBatchMaxSize, // up to 256 sectors + PreCommitBatchWait: Duration(24 * time.Hour), // this should be less than 31.5 hours, which is the expiration of a precommit ticket + // XXX snap deals wait deals slack if first + PreCommitBatchSlack: Duration(3 * time.Hour), // time buffer for forceful batch submission before sectors/deals in batch would start expiring, higher value will lower the chances for message fail due to expiration CommittedCapacitySectorLifetime: Duration(builtin.EpochDurationSeconds * uint64(policy.GetMaxSectorExpirationExtension()) * uint64(time.Second)), @@ -131,11 +132,13 @@ func DefaultStorageMiner() *StorageMiner { }, Storage: sectorstorage.SealerConfig{ - AllowAddPiece: true, - AllowPreCommit1: true, - AllowPreCommit2: true, - AllowCommit: true, - AllowUnseal: true, + AllowAddPiece: true, + AllowPreCommit1: true, + AllowPreCommit2: true, + AllowCommit: true, + AllowUnseal: true, + AllowReplicaUpdate: true, + AllowProveReplicaUpdate2: true, // Default to 10 - tcp should still be able to figure this out, and // it's the ratio between 10gbit / 1gbit diff --git a/node/impl/storminer.go b/node/impl/storminer.go index 39baa97bf..2fcd709b8 100644 --- a/node/impl/storminer.go +++ b/node/impl/storminer.go @@ -32,6 +32,7 @@ import ( "github.com/filecoin-project/go-fil-markets/retrievalmarket" "github.com/filecoin-project/go-fil-markets/storagemarket" "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/network" sectorstorage "github.com/filecoin-project/lotus/extern/sector-storage" "github.com/filecoin-project/lotus/extern/sector-storage/fsutil" @@ -379,8 +380,8 @@ func (sm *StorageMinerAPI) SectorPreCommitPending(ctx context.Context) ([]abi.Se return sm.Miner.SectorPreCommitPending(ctx) } -func (sm *StorageMinerAPI) SectorMarkForUpgrade(ctx context.Context, id abi.SectorNumber) error { - return sm.Miner.MarkForUpgrade(id) +func (sm *StorageMinerAPI) SectorMarkForUpgrade(ctx context.Context, id abi.SectorNumber, snap bool) error { + return sm.Miner.MarkForUpgrade(ctx, id, snap) } func (sm *StorageMinerAPI) SectorCommitFlush(ctx context.Context) ([]sealiface.CommitBatchRes, error) { @@ -391,6 +392,10 @@ func (sm *StorageMinerAPI) SectorCommitPending(ctx context.Context) ([]abi.Secto return sm.Miner.CommitPending(ctx) } +func (sm *StorageMinerAPI) SectorMatchPendingPiecesToOpenSectors(ctx context.Context) error { + return sm.Miner.SectorMatchPendingPiecesToOpenSectors(ctx) +} + func (sm *StorageMinerAPI) WorkerConnect(ctx context.Context, url string) error { w, err := connectRemoteWorker(ctx, sm, url) if err != nil { @@ -988,8 +993,8 @@ func (sm *StorageMinerAPI) Discover(ctx context.Context) (apitypes.OpenRPCDocume return build.OpenRPCDiscoverJSON_Miner(), nil } -func (sm *StorageMinerAPI) ComputeProof(ctx context.Context, ssi []builtin.SectorInfo, rand abi.PoStRandomness) ([]builtin.PoStProof, error) { - return sm.Epp.ComputeProof(ctx, ssi, rand) +func (sm *StorageMinerAPI) ComputeProof(ctx context.Context, ssi []builtin.ExtendedSectorInfo, rand abi.PoStRandomness, poStEpoch abi.ChainEpoch, nv network.Version) ([]builtin.PoStProof, error) { + return sm.Epp.ComputeProof(ctx, ssi, rand, poStEpoch, nv) } func (sm *StorageMinerAPI) RuntimeSubsystems(context.Context) (res api.MinerSubsystems, err error) { diff --git a/storage/adapter_storage_miner.go b/storage/adapter_storage_miner.go index 0b4b17f96..01ff9d8d3 100644 --- a/storage/adapter_storage_miner.go +++ b/storage/adapter_storage_miner.go @@ -112,6 +112,15 @@ func (s SealingAPIAdapter) StateMinerSectorAllocated(ctx context.Context, maddr return s.delegate.StateMinerSectorAllocated(ctx, maddr, sid, tsk) } +func (s SealingAPIAdapter) StateMinerActiveSectors(ctx context.Context, maddr address.Address, tok sealing.TipSetToken) ([]*miner.SectorOnChainInfo, error) { + tsk, err := types.TipSetKeyFromBytes(tok) + if err != nil { + return nil, xerrors.Errorf("faile dto unmarshal TipSetToken to TipSetKey: %w", err) + } + + return s.delegate.StateMinerActiveSectors(ctx, maddr, tsk) +} + func (s SealingAPIAdapter) StateWaitMsg(ctx context.Context, mcid cid.Cid) (sealing.MsgLookup, error) { wmsg, err := s.delegate.StateWaitMsg(ctx, mcid, build.MessageConfidence, api.LookbackNoLimit, true) if err != nil { diff --git a/storage/miner.go b/storage/miner.go index 0b1f66840..c52b786ee 100644 --- a/storage/miner.go +++ b/storage/miner.go @@ -86,6 +86,7 @@ type fullNodeFilteredAPI interface { StateSectorPartition(ctx context.Context, maddr address.Address, sectorNumber abi.SectorNumber, tok types.TipSetKey) (*miner.SectorLocation, error) StateMinerInfo(context.Context, address.Address, types.TipSetKey) (miner.MinerInfo, error) StateMinerAvailableBalance(ctx context.Context, maddr address.Address, tok types.TipSetKey) (types.BigInt, error) + StateMinerActiveSectors(context.Context, address.Address, types.TipSetKey) ([]*miner.SectorOnChainInfo, error) StateMinerDeadlines(context.Context, address.Address, types.TipSetKey) ([]api.Deadline, error) StateMinerPartitions(context.Context, address.Address, uint64, types.TipSetKey) ([]api.Partition, error) StateMinerProvingDeadline(context.Context, address.Address, types.TipSetKey) (*dline.Info, error) @@ -282,7 +283,7 @@ func (wpp *StorageWpp) GenerateCandidates(ctx context.Context, randomness abi.Po return cds, nil } -func (wpp *StorageWpp) ComputeProof(ctx context.Context, ssi []builtin.SectorInfo, rand abi.PoStRandomness) ([]builtin.PoStProof, error) { +func (wpp *StorageWpp) ComputeProof(ctx context.Context, ssi []builtin.ExtendedSectorInfo, rand abi.PoStRandomness, currEpoch abi.ChainEpoch, nv network.Version) ([]builtin.PoStProof, error) { if build.InsecurePoStValidation { return []builtin.PoStProof{{ProofBytes: []byte("valid proof")}}, nil } diff --git a/storage/miner_sealing.go b/storage/miner_sealing.go index 01b9546a6..d8ef26835 100644 --- a/storage/miner_sealing.go +++ b/storage/miner_sealing.go @@ -71,8 +71,15 @@ func (m *Miner) CommitPending(ctx context.Context) ([]abi.SectorID, error) { return m.sealing.CommitPending(ctx) } -func (m *Miner) MarkForUpgrade(id abi.SectorNumber) error { - return m.sealing.MarkForUpgrade(id) +func (m *Miner) SectorMatchPendingPiecesToOpenSectors(ctx context.Context) error { + return m.sealing.MatchPendingPiecesToOpenSectors(ctx) +} + +func (m *Miner) MarkForUpgrade(ctx context.Context, id abi.SectorNumber, snap bool) error { + if snap { + return m.sealing.MarkForSnapUpgrade(ctx, id) + } + return m.sealing.MarkForUpgrade(ctx, id) } func (m *Miner) IsMarkedForUpgrade(id abi.SectorNumber) bool { diff --git a/storage/wdpost_changehandler_test.go b/storage/wdpost_changehandler_test.go index a2283cb7c..2fcbe770e 100644 --- a/storage/wdpost_changehandler_test.go +++ b/storage/wdpost_changehandler_test.go @@ -117,7 +117,7 @@ func (m *mockAPI) startGeneratePoST( completeGeneratePoST CompleteGeneratePoSTCb, ) context.CancelFunc { ctx, cancel := context.WithCancel(ctx) - + log.Errorf("mock posting\n") m.statesLk.Lock() defer m.statesLk.Unlock() m.postStates[deadline.Open] = postStatusProving diff --git a/storage/wdpost_run.go b/storage/wdpost_run.go index 038ed3ac7..0ba7b21e1 100644 --- a/storage/wdpost_run.go +++ b/storage/wdpost_run.go @@ -19,8 +19,8 @@ import ( "go.opencensus.io/trace" "golang.org/x/xerrors" - proof2 "github.com/filecoin-project/specs-actors/v2/actors/runtime/proof" "github.com/filecoin-project/specs-actors/v3/actors/runtime/proof" + proof7 "github.com/filecoin-project/specs-actors/v7/actors/runtime/proof" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/build" @@ -567,7 +567,7 @@ func (s *WindowPoStScheduler) runPoStCycle(ctx context.Context, di dline.Info, t for retries := 0; ; retries++ { skipCount := uint64(0) var partitions []miner.PoStPartition - var sinfos []proof2.SectorInfo + var xsinfos []proof7.ExtendedSectorInfo for partIdx, partition := range batch { // TODO: Can do this in parallel toProve, err := bitfield.SubtractBitField(partition.LiveSectors, partition.FaultySectors) @@ -610,14 +610,14 @@ func (s *WindowPoStScheduler) runPoStCycle(ctx context.Context, di dline.Info, t continue } - sinfos = append(sinfos, ssi...) + xsinfos = append(xsinfos, ssi...) partitions = append(partitions, miner.PoStPartition{ Index: uint64(batchPartitionStartIdx + partIdx), Skipped: skipped, }) } - if len(sinfos) == 0 { + if len(xsinfos) == 0 { // nothing to prove for this batch break } @@ -636,14 +636,22 @@ func (s *WindowPoStScheduler) runPoStCycle(ctx context.Context, di dline.Info, t return nil, err } - postOut, ps, err := s.prover.GenerateWindowPoSt(ctx, abi.ActorID(mid), sinfos, append(abi.PoStRandomness{}, rand...)) + defer func() { + if r := recover(); r != nil { + log.Errorf("recover: %s", r) + } + }() + postOut, ps, err := s.prover.GenerateWindowPoSt(ctx, abi.ActorID(mid), xsinfos, append(abi.PoStRandomness{}, rand...)) elapsed := time.Since(tsStart) - log.Infow("computing window post", "batch", batchIdx, "elapsed", elapsed) - + if err != nil { + log.Errorf("error generating window post: %s", err) + } if err == nil { + // If we proved nothing, something is very wrong. if len(postOut) == 0 { + log.Errorf("len(postOut) == 0") return nil, xerrors.Errorf("received no proofs back from generate window post") } @@ -664,6 +672,14 @@ func (s *WindowPoStScheduler) runPoStCycle(ctx context.Context, di dline.Info, t } // If we generated an incorrect proof, try again. + sinfos := make([]proof7.SectorInfo, len(xsinfos)) + for i, xsi := range xsinfos { + sinfos[i] = proof7.SectorInfo{ + SealProof: xsi.SealProof, + SectorNumber: xsi.SectorNumber, + SealedCID: xsi.SealedCID, + } + } if correct, err := s.verifier.VerifyWindowPoSt(ctx, proof.WindowPoStVerifyInfo{ Randomness: abi.PoStRandomness(checkRand), Proofs: postOut, @@ -686,7 +702,7 @@ func (s *WindowPoStScheduler) runPoStCycle(ctx context.Context, di dline.Info, t } // Proof generation failed, so retry - + log.Debugf("Proof generation failed, retry") if len(ps) == 0 { // If we didn't skip any new sectors, we failed // for some other reason and we need to abort. @@ -714,10 +730,8 @@ func (s *WindowPoStScheduler) runPoStCycle(ctx context.Context, di dline.Info, t if !somethingToProve { continue } - posts = append(posts, params) } - return posts, nil } @@ -766,7 +780,7 @@ func (s *WindowPoStScheduler) batchPartitions(partitions []api.Partition, nv net return batches, nil } -func (s *WindowPoStScheduler) sectorsForProof(ctx context.Context, goodSectors, allSectors bitfield.BitField, ts *types.TipSet) ([]proof2.SectorInfo, error) { +func (s *WindowPoStScheduler) sectorsForProof(ctx context.Context, goodSectors, allSectors bitfield.BitField, ts *types.TipSet) ([]proof7.ExtendedSectorInfo, error) { sset, err := s.api.StateMinerSectors(ctx, s.actor, &goodSectors, ts.Key()) if err != nil { return nil, err @@ -776,22 +790,24 @@ func (s *WindowPoStScheduler) sectorsForProof(ctx context.Context, goodSectors, return nil, nil } - substitute := proof2.SectorInfo{ + substitute := proof7.ExtendedSectorInfo{ SectorNumber: sset[0].SectorNumber, SealedCID: sset[0].SealedCID, SealProof: sset[0].SealProof, + SectorKey: sset[0].SectorKeyCID, } - sectorByID := make(map[uint64]proof2.SectorInfo, len(sset)) + sectorByID := make(map[uint64]proof7.ExtendedSectorInfo, len(sset)) for _, sector := range sset { - sectorByID[uint64(sector.SectorNumber)] = proof2.SectorInfo{ + sectorByID[uint64(sector.SectorNumber)] = proof7.ExtendedSectorInfo{ SectorNumber: sector.SectorNumber, SealedCID: sector.SealedCID, SealProof: sector.SealProof, + SectorKey: sector.SectorKeyCID, } } - proofSectors := make([]proof2.SectorInfo, 0, len(sset)) + proofSectors := make([]proof7.ExtendedSectorInfo, 0, len(sset)) if err := allSectors.ForEach(func(sectorNo uint64) error { if info, found := sectorByID[sectorNo]; found { proofSectors = append(proofSectors, info) diff --git a/storage/wdpost_run_test.go b/storage/wdpost_run_test.go index 9ece295ca..feeaab6ed 100644 --- a/storage/wdpost_run_test.go +++ b/storage/wdpost_run_test.go @@ -116,11 +116,11 @@ func (m *mockStorageMinerAPI) GasEstimateFeeCap(context.Context, *types.Message, type mockProver struct { } -func (m *mockProver) GenerateWinningPoSt(context.Context, abi.ActorID, []proof2.SectorInfo, abi.PoStRandomness) ([]proof2.PoStProof, error) { +func (m *mockProver) GenerateWinningPoSt(context.Context, abi.ActorID, []proof7.ExtendedSectorInfo, abi.PoStRandomness) ([]proof2.PoStProof, error) { panic("implement me") } -func (m *mockProver) GenerateWindowPoSt(ctx context.Context, aid abi.ActorID, sis []proof2.SectorInfo, pr abi.PoStRandomness) ([]proof2.PoStProof, []abi.SectorID, error) { +func (m *mockProver) GenerateWindowPoSt(ctx context.Context, aid abi.ActorID, sis []proof7.ExtendedSectorInfo, pr abi.PoStRandomness) ([]proof2.PoStProof, []abi.SectorID, error) { return []proof2.PoStProof{ { PoStProof: abi.RegisteredPoStProof_StackedDrgWindow2KiBV1, @@ -132,7 +132,7 @@ func (m *mockProver) GenerateWindowPoSt(ctx context.Context, aid abi.ActorID, si type mockVerif struct { } -func (m mockVerif) VerifyWinningPoSt(ctx context.Context, info proof2.WinningPoStVerifyInfo) (bool, error) { +func (m mockVerif) VerifyWinningPoSt(ctx context.Context, info proof7.WinningPoStVerifyInfo, currEpoch abi.ChainEpoch, nv network.Version) (bool, error) { panic("implement me") }