package storageadapter import ( "context" "testing" "github.com/filecoin-project/lotus/chain/events" "golang.org/x/sync/errgroup" cbornode "github.com/ipfs/go-ipld-cbor" adt2 "github.com/filecoin-project/specs-actors/v2/actors/util/adt" "github.com/ipfs/go-cid" "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/abi" test "github.com/filecoin-project/lotus/chain/events/state/mock" bstore "github.com/filecoin-project/lotus/lib/blockstore" builtin2 "github.com/filecoin-project/specs-actors/v2/actors/builtin" market2 "github.com/filecoin-project/specs-actors/v2/actors/builtin/market" "github.com/stretchr/testify/require" "github.com/filecoin-project/lotus/chain/events/state" "github.com/filecoin-project/lotus/chain/types" ) func TestDealStateMatcher(t *testing.T) { ctx := context.Background() bs := bstore.NewTemporarySync() store := adt2.WrapStore(ctx, cbornode.NewCborStore(bs)) deal1 := &market2.DealState{ SectorStartEpoch: 1, LastUpdatedEpoch: 2, } deal2 := &market2.DealState{ SectorStartEpoch: 4, LastUpdatedEpoch: 5, } deal3 := &market2.DealState{ SectorStartEpoch: 7, LastUpdatedEpoch: 8, } deals1 := map[abi.DealID]*market2.DealState{ abi.DealID(1): deal1, } deals2 := map[abi.DealID]*market2.DealState{ abi.DealID(1): deal2, } deals3 := map[abi.DealID]*market2.DealState{ abi.DealID(1): deal3, } deal1StateC := createMarketState(ctx, t, store, deals1) deal2StateC := createMarketState(ctx, t, store, deals2) deal3StateC := createMarketState(ctx, t, store, deals3) minerAddr, err := address.NewFromString("t00") require.NoError(t, err) ts1, err := test.MockTipset(minerAddr, 1) require.NoError(t, err) ts2, err := test.MockTipset(minerAddr, 2) require.NoError(t, err) ts3, err := test.MockTipset(minerAddr, 3) require.NoError(t, err) api := test.NewMockAPI(bs) api.SetActor(ts1.Key(), &types.Actor{Code: builtin2.StorageMarketActorCodeID, Head: deal1StateC}) api.SetActor(ts2.Key(), &types.Actor{Code: builtin2.StorageMarketActorCodeID, Head: deal2StateC}) api.SetActor(ts3.Key(), &types.Actor{Code: builtin2.StorageMarketActorCodeID, Head: deal3StateC}) t.Run("caching", func(t *testing.T) { dsm := newDealStateMatcher(state.NewStatePredicates(api)) matcher := dsm.matcher(ctx, abi.DealID(1)) // Call matcher with tipsets that have the same state ok, stateChange, err := matcher(ts1, ts1) require.NoError(t, err) require.False(t, ok) require.Nil(t, stateChange) // Should call StateGetActor once for each tipset require.Equal(t, 2, api.StateGetActorCallCount()) // Call matcher with tipsets that have different state api.ResetCallCounts() ok, stateChange, err = matcher(ts1, ts2) require.NoError(t, err) require.True(t, ok) require.NotNil(t, stateChange) // Should call StateGetActor once for each tipset require.Equal(t, 2, api.StateGetActorCallCount()) // Call matcher again with the same tipsets as above, should be cached api.ResetCallCounts() ok, stateChange, err = matcher(ts1, ts2) require.NoError(t, err) require.True(t, ok) require.NotNil(t, stateChange) // Should not call StateGetActor (because it should hit the cache) require.Equal(t, 0, api.StateGetActorCallCount()) // Call matcher with different tipsets, should not be cached api.ResetCallCounts() ok, stateChange, err = matcher(ts2, ts3) require.NoError(t, err) require.True(t, ok) require.NotNil(t, stateChange) // Should call StateGetActor once for each tipset require.Equal(t, 2, api.StateGetActorCallCount()) }) t.Run("parallel", func(t *testing.T) { api.ResetCallCounts() dsm := newDealStateMatcher(state.NewStatePredicates(api)) matcher := dsm.matcher(ctx, abi.DealID(1)) // Call matcher with lots of go-routines in parallel var eg errgroup.Group res := make([]struct { ok bool stateChange events.StateChange }, 20) for i := 0; i < len(res); i++ { i := i eg.Go(func() error { ok, stateChange, err := matcher(ts1, ts2) res[i].ok = ok res[i].stateChange = stateChange return err }) } err := eg.Wait() require.NoError(t, err) // All go-routines should have got the same (cached) result for i := 1; i < len(res); i++ { require.Equal(t, res[i].ok, res[i-1].ok) require.Equal(t, res[i].stateChange, res[i-1].stateChange) } // Only one go-routine should have called StateGetActor // (once for each tipset) require.Equal(t, 2, api.StateGetActorCallCount()) }) } func createMarketState(ctx context.Context, t *testing.T, store adt2.Store, deals map[abi.DealID]*market2.DealState) cid.Cid { dealRootCid := test.CreateDealAMT(ctx, t, store, deals) state := test.CreateEmptyMarketState(t, store) state.States = dealRootCid stateC, err := store.Put(ctx, state) require.NoError(t, err) return stateC }