diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 63929f315..b2a1551b7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,6 +61,7 @@ jobs: # to support resource intensive jobs. runners: | { + "itest-niporep_manual": ["self-hosted", "linux", "x64", "4xlarge"], "itest-sector_pledge": ["self-hosted", "linux", "x64", "4xlarge"], "itest-worker": ["self-hosted", "linux", "x64", "4xlarge"], "itest-manual_onboarding": ["self-hosted", "linux", "x64", "4xlarge"], @@ -118,6 +119,7 @@ jobs: "itest-direct_data_onboard_verified", "itest-direct_data_onboard", "itest-manual_onboarding", + "itest-niporep_manual", "itest-net", "itest-path_detach_redeclare", "itest-sealing_resources", diff --git a/itests/kit/node_unmanaged.go b/itests/kit/node_unmanaged.go index 2cab93071..53ccb342b 100644 --- a/itests/kit/node_unmanaged.go +++ b/itests/kit/node_unmanaged.go @@ -455,7 +455,7 @@ func (tm *TestUnmanagedMiner) SubmitProveCommit( tm.t.Log("Submitting ProveCommitSector ...") var provingDeadline uint64 = 7 - if tm.IsUmmutableDeadline(ctx, provingDeadline) { + if tm.IsImmutableDeadline(ctx, provingDeadline) { // avoid immutable deadlines provingDeadline = 5 } @@ -1144,7 +1144,7 @@ func (tm *TestUnmanagedMiner) AssertDisputeFails(ctx context.Context, sector abi require.Contains(tm.t, err.Error(), "(RetCode=16)") } -func (tm *TestUnmanagedMiner) IsUmmutableDeadline(ctx context.Context, deadlineIndex uint64) bool { +func (tm *TestUnmanagedMiner) IsImmutableDeadline(ctx context.Context, deadlineIndex uint64) bool { di, err := tm.FullNode.StateMinerProvingDeadline(ctx, tm.ActorAddr, types.EmptyTSK) require.NoError(tm.t, err) // don't rely on di.Index because if we haven't enrolled in cron it won't be ticking diff --git a/itests/niporep_manual_test.go b/itests/niporep_manual_test.go new file mode 100644 index 000000000..e5ac41e6d --- /dev/null +++ b/itests/niporep_manual_test.go @@ -0,0 +1,260 @@ +package itests + +import ( + "context" + "fmt" + "math" + "testing" + "time" + + "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/datamodel" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/multiformats/go-multicodec" + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/builtin" + miner14 "github.com/filecoin-project/go-state-types/builtin/v14/miner" + "github.com/filecoin-project/go-state-types/network" + + "github.com/filecoin-project/lotus/build" + "github.com/filecoin-project/lotus/chain/actors/builtin/miner" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/itests/kit" + "github.com/filecoin-project/lotus/lib/must" +) + +func TestManualNISectorOnboarding(t *testing.T) { + req := require.New(t) + + const defaultSectorSize = abi.SectorSize(2 << 10) // 2KiB + sealProofType, err := miner.SealProofTypeFromSectorSize(defaultSectorSize, network.Version23, miner.SealProofVariant_NonInteractive) + req.NoError(err) + + for _, withMockProofs := range []bool{true, false} { + testName := "WithRealProofs" + if withMockProofs { + testName = "WithMockProofs" + } + t.Run(testName, func(t *testing.T) { + if !withMockProofs { + kit.Expensive(t) + } + kit.QuietMiningLogs() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var ( + // need to pick a balance value so that the test is not racy on CI by running through it's WindowPostDeadlines too fast + blocktime = 2 * time.Millisecond + client kit.TestFullNode + minerA kit.TestMiner // A is a standard genesis miner + ) + + // Setup and begin mining with a single miner (A) + // Miner A will only be a genesis Miner with power allocated in the genesis block and will not onboard any sectors from here on + ens := kit.NewEnsemble(t, kit.MockProofs(withMockProofs)). + FullNode(&client, kit.SectorSize(defaultSectorSize)). + // preseal more than the default number of sectors to ensure that the genesis miner has power + // because our unmanaged miners won't produce blocks so we may get null rounds + Miner(&minerA, &client, kit.PresealSectors(5), kit.SectorSize(defaultSectorSize), kit.WithAllSubsystems()). + Start(). + InterconnectAll() + blockMiners := ens.BeginMiningMustPost(blocktime) + req.Len(blockMiners, 1) + blockMiner := blockMiners[0] + + // Instantiate MinerB to manually handle sector onboarding and power acquisition through sector activation. + // Unlike other miners managed by the Lotus Miner storage infrastructure, MinerB operates independently, + // performing all related tasks manually. Managed by the TestKit, MinerB has the capability to utilize actual proofs + // for the processes of sector onboarding and activation. + nodeOpts := []kit.NodeOpt{kit.SectorSize(defaultSectorSize), kit.OwnerAddr(client.DefaultKey)} + minerB, ens := ens.UnmanagedMiner(&client, nodeOpts...) + + ens.Start() + + build.Clock.Sleep(time.Second) + + t.Log("Checking initial power ...") + + // Miner A should have power as it has already onboarded sectors in the genesis block + head, err := client.ChainHead(ctx) + req.NoError(err) + p, err := client.StateMinerPower(ctx, minerA.ActorAddr, head.Key()) + req.NoError(err) + t.Logf("MinerA RBP: %v, QaP: %v", p.MinerPower.QualityAdjPower.String(), p.MinerPower.RawBytePower.String()) + + // Miner B should have no power as it has yet to onboard and activate any sectors + minerB.AssertNoPower(ctx) + + // Verify that ProveCommitSectorsNI rejects messages with invalid parameters + verifyProveCommitSectorsNIErrorConditions(ctx, t, minerB, sealProofType) + + // ---- Miner B onboards a CC sector + var bSectorNum abi.SectorNumber + var bRespCh chan kit.WindowPostResp + var bWdPostCancelF context.CancelFunc + + // Onboard a CC sector with Miner B using NI-PoRep + bSectorNum, bRespCh, bWdPostCancelF = minerB.OnboardCCSector(ctx, sealProofType) + // Miner B should still not have power as power can only be gained after sector is activated i.e. the first WindowPost is submitted for it + minerB.AssertNoPower(ctx) + + // Check that the sector-activated event was emitted + { + expectedEntries := []types.EventEntry{ + {Flags: 0x03, Codec: uint64(multicodec.Cbor), Key: "$type", Value: must.One(ipld.Encode(basicnode.NewString("sector-activated"), dagcbor.Encode))}, + {Flags: 0x03, Codec: uint64(multicodec.Cbor), Key: "sector", Value: must.One(ipld.Encode(basicnode.NewInt(int64(bSectorNum)), dagcbor.Encode))}, + {Flags: 0x03, Codec: uint64(multicodec.Cbor), Key: "unsealed-cid", Value: must.One(ipld.Encode(datamodel.Null, dagcbor.Encode))}, + } + from := head.Height() + recentEvents, err := client.FullNode.GetActorEventsRaw(ctx, &types.ActorEventFilter{FromHeight: &from}) + req.NoError(err) + req.Len(recentEvents, 1) + req.Equal(expectedEntries, recentEvents[0].Entries) + } + + // Ensure that the block miner checks for and waits for posts during the appropriate proving window from our new miner with a sector + blockMiner.WatchMinerForPost(minerB.ActorAddr) + + // Wait till both miners' sectors have had their first post and are activated and check that this is reflected in miner power + minerB.WaitTillActivatedAndAssertPower(ctx, bRespCh, bSectorNum) + + head, err = client.ChainHead(ctx) + req.NoError(err) + + // Miner B has activated the CC sector -> upgrade it with snapdeals + snapPieces := minerB.SnapDeal(ctx, kit.TestSpt, bSectorNum) + // cancel the WdPost for the CC sector as the corresponding CommR is no longer valid + bWdPostCancelF() + + // Check "sector-updated" event happned after snap + { + expectedEntries := []types.EventEntry{ + {Flags: 0x03, Codec: uint64(multicodec.Cbor), Key: "$type", Value: must.One(ipld.Encode(basicnode.NewString("sector-updated"), dagcbor.Encode))}, + {Flags: 0x03, Codec: uint64(multicodec.Cbor), Key: "sector", Value: must.One(ipld.Encode(basicnode.NewInt(int64(bSectorNum)), dagcbor.Encode))}, + {Flags: 0x03, Codec: uint64(multicodec.Cbor), Key: "unsealed-cid", Value: must.One(ipld.Encode(basicnode.NewLink(cidlink.Link{Cid: snapPieces[0].PieceCID}), dagcbor.Encode))}, + {Flags: 0x03, Codec: uint64(multicodec.Cbor), Key: "piece-cid", Value: must.One(ipld.Encode(basicnode.NewLink(cidlink.Link{Cid: snapPieces[0].PieceCID}), dagcbor.Encode))}, + {Flags: 0x01, Codec: uint64(multicodec.Cbor), Key: "piece-size", Value: must.One(ipld.Encode(basicnode.NewInt(int64(snapPieces[0].Size)), dagcbor.Encode))}, + } + from := head.Height() + recentEvents, err := client.FullNode.GetActorEventsRaw(ctx, &types.ActorEventFilter{FromHeight: &from}) + req.NoError(err) + req.Len(recentEvents, 1) + req.Equal(expectedEntries, recentEvents[0].Entries) + } + }) + } +} + +func verifyProveCommitSectorsNIErrorConditions(ctx context.Context, t *testing.T, miner *kit.TestUnmanagedMiner, sealProofType abi.RegisteredSealProof) { + req := require.New(t) + + head, err := miner.FullNode.ChainHead(ctx) + req.NoError(err) + + actorIdNum, err := address.IDFromAddress(miner.ActorAddr) + req.NoError(err) + actorId := abi.ActorID(actorIdNum) + + var provingDeadline uint64 = 7 + if miner.IsImmutableDeadline(ctx, provingDeadline) { + // avoid immutable deadlines + provingDeadline = 5 + } + + submitAndFail := func(params *miner14.ProveCommitSectorsNIParams, errMsg string, errCode int) { + t.Helper() + r, err := miner.SubmitMessage(ctx, params, 1, builtin.MethodsMiner.ProveCommitSectorsNI) + req.Error(err) + req.Contains(err.Error(), errMsg) + if errCode > 0 { + req.Contains(err.Error(), fmt.Sprintf("(RetCode=%d)", errCode)) + } + req.Nil(r) + } + + sn := abi.SectorNumber(5000) + mkSai := func() miner14.SectorNIActivationInfo { + sn++ + return miner14.SectorNIActivationInfo{ + SealingNumber: sn, + SealerID: actorId, + SealedCID: cid.MustParse("bagboea4b5abcatlxechwbp7kjpjguna6r6q7ejrhe6mdp3lf34pmswn27pkkiekz"), + SectorNumber: sn, + SealRandEpoch: head.Height() - 10, + Expiration: 2880 * 300, + } + } + mkParams := func() miner14.ProveCommitSectorsNIParams { + return miner14.ProveCommitSectorsNIParams{ + Sectors: []miner14.SectorNIActivationInfo{mkSai(), mkSai()}, + AggregateProof: []byte{0xca, 0xfe, 0xbe, 0xef}, + SealProofType: sealProofType, + AggregateProofType: abi.RegisteredAggregationProof_SnarkPackV2, + ProvingDeadline: provingDeadline, + RequireActivationSuccess: true, + } + } + + // Test message rejection on no sectors + params := mkParams() + params.Sectors = []miner14.SectorNIActivationInfo{} + submitAndFail(¶ms, "too few sectors", 16) + + // Test message rejection on too many sectors + sectorInfos := make([]miner14.SectorNIActivationInfo, 66) + for i := range sectorInfos { + sectorInfos[i] = mkSai() + } + params = mkParams() + params.Sectors = sectorInfos + submitAndFail(¶ms, "too many sectors", 16) + + // Test bad aggregation proof type + params = mkParams() + params.AggregateProofType = abi.RegisteredAggregationProof_SnarkPackV1 + submitAndFail(¶ms, "aggregate proof type", 16) + + // Test bad SealerID + params = mkParams() + params.Sectors[1].SealerID = 1234 + submitAndFail(¶ms, "invalid NI commit 1 while requiring activation success", 16) + + // Test bad SealingNumber + params = mkParams() + params.Sectors[1].SealingNumber = 1234 + submitAndFail(¶ms, "invalid NI commit 1 while requiring activation success", 16) + + // Test bad SealedCID + params = mkParams() + params.Sectors[1].SealedCID = cid.MustParse("baga6ea4seaqjtovkwk4myyzj56eztkh5pzsk5upksan6f5outesy62bsvl4dsha") + submitAndFail(¶ms, "invalid NI commit 1 while requiring activation success", 16) + + // Test bad SealRandEpoch + head, err = miner.FullNode.ChainHead(ctx) + req.NoError(err) + params = mkParams() + params.Sectors[1].SealRandEpoch = head.Height() + builtin.EpochsInDay + submitAndFail(¶ms, fmt.Sprintf("seal challenge epoch %d must be before now", params.Sectors[1].SealRandEpoch), 16) + params.Sectors[1].SealRandEpoch = head.Height() - 190*builtin.EpochsInDay + submitAndFail(¶ms, "invalid NI commit 1 while requiring activation success", 16) + + // Immutable/bad deadlines + di, err := miner.FullNode.StateMinerProvingDeadline(ctx, miner.ActorAddr, head.Key()) + req.NoError(err) + currentDeadlineIdx := uint64(math.Abs(float64((di.CurrentEpoch - di.PeriodStart) / di.WPoStChallengeWindow))) + req.Less(currentDeadlineIdx, di.WPoStPeriodDeadlines) + params = mkParams() + params.ProvingDeadline = currentDeadlineIdx + submitAndFail(¶ms, fmt.Sprintf("proving deadline %d must not be the current or next deadline", currentDeadlineIdx), 18) + params.ProvingDeadline = currentDeadlineIdx + 1 + submitAndFail(¶ms, fmt.Sprintf("proving deadline %d must not be the current or next deadline", currentDeadlineIdx+1), 18) + params.ProvingDeadline = di.WPoStPeriodDeadlines // too big + submitAndFail(¶ms, fmt.Sprintf("proving deadline index %d invalid", di.WPoStPeriodDeadlines), 16) +}