package sealing import ( "bytes" "errors" "math/rand" "sort" "testing" "time" "golang.org/x/net/context" "golang.org/x/xerrors" "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/crypto" "github.com/filecoin-project/go-state-types/exitcode" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/chain/actors/builtin/market" evtmock "github.com/filecoin-project/lotus/chain/events/state/mock" "github.com/filecoin-project/lotus/chain/types" market2 "github.com/filecoin-project/specs-actors/v2/actors/builtin/market" tutils "github.com/filecoin-project/specs-actors/v2/support/testing" "github.com/ipfs/go-cid" "github.com/stretchr/testify/require" ) var errNotFound = errors.New("could not find") func TestGetCurrentDealInfo(t *testing.T) { ctx := context.Background() dummyCid, _ := cid.Parse("bafkqaaa") dummyCid2, _ := cid.Parse("bafkqaab") zeroDealID := abi.DealID(0) earlierDealID := abi.DealID(9) successDealID := abi.DealID(10) proposal := market.DealProposal{ PieceCID: dummyCid, PieceSize: abi.PaddedPieceSize(100), Client: tutils.NewActorAddr(t, "client"), Provider: tutils.NewActorAddr(t, "provider"), StoragePricePerEpoch: abi.NewTokenAmount(1), ProviderCollateral: abi.NewTokenAmount(1), ClientCollateral: abi.NewTokenAmount(1), Label: "success", } otherProposal := market.DealProposal{ PieceCID: dummyCid2, PieceSize: abi.PaddedPieceSize(100), Client: tutils.NewActorAddr(t, "client"), Provider: tutils.NewActorAddr(t, "provider"), StoragePricePerEpoch: abi.NewTokenAmount(1), ProviderCollateral: abi.NewTokenAmount(1), ClientCollateral: abi.NewTokenAmount(1), Label: "other", } successDeal := &api.MarketDeal{ Proposal: proposal, State: market.DealState{ SectorStartEpoch: 1, LastUpdatedEpoch: 2, }, } earlierDeal := &api.MarketDeal{ Proposal: otherProposal, State: market.DealState{ SectorStartEpoch: 1, LastUpdatedEpoch: 2, }, } type testCaseData struct { searchMessageLookup *MsgLookup searchMessageErr error marketDeals map[abi.DealID]*api.MarketDeal publishCid cid.Cid targetProposal *market.DealProposal expectedDealID abi.DealID expectedMarketDeal *api.MarketDeal expectedError error } testCases := map[string]testCaseData{ "deal lookup succeeds": { publishCid: dummyCid, searchMessageLookup: &MsgLookup{ Receipt: MessageReceipt{ ExitCode: exitcode.Ok, Return: makePublishDealsReturnBytes(t, []abi.DealID{successDealID}), }, }, marketDeals: map[abi.DealID]*api.MarketDeal{ successDealID: successDeal, }, targetProposal: &proposal, expectedDealID: successDealID, expectedMarketDeal: successDeal, }, "deal lookup succeeds two return values": { publishCid: dummyCid, searchMessageLookup: &MsgLookup{ Receipt: MessageReceipt{ ExitCode: exitcode.Ok, Return: makePublishDealsReturnBytes(t, []abi.DealID{earlierDealID, successDealID}), }, }, marketDeals: map[abi.DealID]*api.MarketDeal{ earlierDealID: earlierDeal, successDealID: successDeal, }, targetProposal: &proposal, expectedDealID: successDealID, expectedMarketDeal: successDeal, }, "deal lookup fails proposal mis-match": { publishCid: dummyCid, searchMessageLookup: &MsgLookup{ Receipt: MessageReceipt{ ExitCode: exitcode.Ok, Return: makePublishDealsReturnBytes(t, []abi.DealID{earlierDealID}), }, }, marketDeals: map[abi.DealID]*api.MarketDeal{ earlierDealID: earlierDeal, }, targetProposal: &proposal, expectedDealID: zeroDealID, expectedError: xerrors.Errorf("could not find deal in publish deals message %s", dummyCid), }, "deal lookup fails mismatch count of deals and return values": { publishCid: dummyCid, searchMessageLookup: &MsgLookup{ Receipt: MessageReceipt{ ExitCode: exitcode.Ok, Return: makePublishDealsReturnBytes(t, []abi.DealID{earlierDealID}), }, }, marketDeals: map[abi.DealID]*api.MarketDeal{ earlierDealID: earlierDeal, successDealID: successDeal, }, targetProposal: &proposal, expectedDealID: zeroDealID, expectedError: xerrors.Errorf("deal index 1 out of bounds of deals (len 1) in publish deals message %s", dummyCid), }, "deal lookup succeeds, target proposal nil, single deal in message": { publishCid: dummyCid, searchMessageLookup: &MsgLookup{ Receipt: MessageReceipt{ ExitCode: exitcode.Ok, Return: makePublishDealsReturnBytes(t, []abi.DealID{successDealID}), }, }, marketDeals: map[abi.DealID]*api.MarketDeal{ successDealID: successDeal, }, targetProposal: nil, expectedDealID: successDealID, expectedMarketDeal: successDeal, }, "deal lookup fails, multiple deals in return value but target proposal nil": { publishCid: dummyCid, searchMessageLookup: &MsgLookup{ Receipt: MessageReceipt{ ExitCode: exitcode.Ok, Return: makePublishDealsReturnBytes(t, []abi.DealID{earlierDealID, successDealID}), }, }, marketDeals: map[abi.DealID]*api.MarketDeal{ earlierDealID: earlierDeal, successDealID: successDeal, }, targetProposal: nil, expectedDealID: zeroDealID, expectedError: xerrors.Errorf("getting deal ID from publish deal message %s: no deal proposal supplied but message return value has more than one deal (2 deals)", dummyCid), }, "search message fails": { publishCid: dummyCid, searchMessageErr: errors.New("something went wrong"), targetProposal: &proposal, expectedDealID: zeroDealID, expectedError: xerrors.Errorf("looking for publish deal message %s: search msg failed: something went wrong", dummyCid), }, "search message not found": { publishCid: dummyCid, targetProposal: &proposal, expectedDealID: zeroDealID, expectedError: xerrors.Errorf("looking for publish deal message %s: not found", dummyCid), }, "return code not ok": { publishCid: dummyCid, searchMessageLookup: &MsgLookup{ Receipt: MessageReceipt{ ExitCode: exitcode.ErrIllegalState, }, }, targetProposal: &proposal, expectedDealID: zeroDealID, expectedError: xerrors.Errorf("looking for publish deal message %s: non-ok exit code: %s", dummyCid, exitcode.ErrIllegalState), }, "unable to unmarshal params": { publishCid: dummyCid, searchMessageLookup: &MsgLookup{ Receipt: MessageReceipt{ ExitCode: exitcode.Ok, Return: []byte("applesauce"), }, }, targetProposal: &proposal, expectedDealID: zeroDealID, expectedError: xerrors.Errorf("looking for publish deal message %s: unmarshalling message return: cbor input should be of type array", dummyCid), }, } runTestCase := func(testCase string, data testCaseData) { t.Run(testCase, func(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() ts, err := evtmock.MockTipset(address.TestAddress, rand.Uint64()) require.NoError(t, err) marketDeals := make(map[marketDealKey]*api.MarketDeal) for dealID, deal := range data.marketDeals { marketDeals[marketDealKey{dealID, ts.Key()}] = deal } mockApi := &CurrentDealInfoMockAPI{ SearchMessageLookup: data.searchMessageLookup, SearchMessageErr: data.searchMessageErr, MarketDeals: marketDeals, } dealInfoMgr := CurrentDealInfoManager{mockApi} res, err := dealInfoMgr.GetCurrentDealInfo(ctx, ts.Key().Bytes(), data.targetProposal, data.publishCid) require.Equal(t, data.expectedDealID, res.DealID) require.Equal(t, data.expectedMarketDeal, res.MarketDeal) if data.expectedError == nil { require.NoError(t, err) } else { require.EqualError(t, err, data.expectedError.Error()) } }) } for testCase, data := range testCases { runTestCase(testCase, data) } } type marketDealKey struct { abi.DealID types.TipSetKey } type CurrentDealInfoMockAPI struct { SearchMessageLookup *MsgLookup SearchMessageErr error MarketDeals map[marketDealKey]*api.MarketDeal } func (mapi *CurrentDealInfoMockAPI) ChainGetMessage(ctx context.Context, c cid.Cid) (*types.Message, error) { var dealIDs []abi.DealID var deals []market2.ClientDealProposal for k, dl := range mapi.MarketDeals { dealIDs = append(dealIDs, k.DealID) deals = append(deals, market2.ClientDealProposal{ Proposal: market2.DealProposal(dl.Proposal), ClientSignature: crypto.Signature{ Data: []byte("foo bar cat dog"), Type: crypto.SigTypeBLS, }, }) } sort.SliceStable(deals, func(i, j int) bool { return dealIDs[i] < dealIDs[j] }) buf := new(bytes.Buffer) params := market2.PublishStorageDealsParams{Deals: deals} err := params.MarshalCBOR(buf) if err != nil { panic(err) } return &types.Message{ Params: buf.Bytes(), }, nil } func (mapi *CurrentDealInfoMockAPI) StateLookupID(ctx context.Context, addr address.Address, token TipSetToken) (address.Address, error) { return addr, nil } func (mapi *CurrentDealInfoMockAPI) StateMarketStorageDeal(ctx context.Context, dealID abi.DealID, tok TipSetToken) (*api.MarketDeal, error) { tsk, err := types.TipSetKeyFromBytes(tok) if err != nil { return nil, err } deal, ok := mapi.MarketDeals[marketDealKey{dealID, tsk}] if !ok { return nil, errNotFound } return deal, nil } func (mapi *CurrentDealInfoMockAPI) StateSearchMsg(ctx context.Context, c cid.Cid) (*MsgLookup, error) { if mapi.SearchMessageLookup == nil { return mapi.SearchMessageLookup, mapi.SearchMessageErr } return mapi.SearchMessageLookup, mapi.SearchMessageErr } func makePublishDealsReturnBytes(t *testing.T, dealIDs []abi.DealID) []byte { buf := new(bytes.Buffer) dealsReturn := market.PublishStorageDealsReturn{ IDs: dealIDs, } err := dealsReturn.MarshalCBOR(buf) require.NoError(t, err) return buf.Bytes() }