github.com/filecoin-project/specs-actors/v4@v4.0.2/actors/builtin/market/testing.go (about)

     1  package market
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/binary"
     6  
     7  	"github.com/ipfs/go-cid"
     8  	"github.com/pkg/errors"
     9  	cbg "github.com/whyrusleeping/cbor-gen"
    10  
    11  	"github.com/filecoin-project/go-address"
    12  	"github.com/filecoin-project/go-state-types/abi"
    13  	"github.com/filecoin-project/go-state-types/big"
    14  
    15  	"github.com/filecoin-project/specs-actors/v4/actors/builtin"
    16  	"github.com/filecoin-project/specs-actors/v4/actors/util/adt"
    17  )
    18  
    19  type DealSummary struct {
    20  	Provider         address.Address
    21  	StartEpoch       abi.ChainEpoch
    22  	EndEpoch         abi.ChainEpoch
    23  	SectorStartEpoch abi.ChainEpoch
    24  	LastUpdatedEpoch abi.ChainEpoch
    25  	SlashEpoch       abi.ChainEpoch
    26  }
    27  
    28  type StateSummary struct {
    29  	Deals                map[abi.DealID]*DealSummary
    30  	PendingProposalCount uint64
    31  	DealStateCount       uint64
    32  	LockTableCount       uint64
    33  	DealOpEpochCount     uint64
    34  	DealOpCount          uint64
    35  }
    36  
    37  // Checks internal invariants of market state.
    38  func CheckStateInvariants(st *State, store adt.Store, balance abi.TokenAmount, currEpoch abi.ChainEpoch) (*StateSummary, *builtin.MessageAccumulator) {
    39  	acc := &builtin.MessageAccumulator{}
    40  
    41  	acc.Require(
    42  		st.TotalClientLockedCollateral.GreaterThanEqual(big.Zero()),
    43  		"negative total client locked collateral: %v", st.TotalClientLockedCollateral)
    44  
    45  	acc.Require(
    46  		st.TotalProviderLockedCollateral.GreaterThanEqual(big.Zero()),
    47  		"negative total provider locked collateral: %v", st.TotalClientLockedCollateral)
    48  
    49  	acc.Require(
    50  		st.TotalClientStorageFee.GreaterThanEqual(big.Zero()),
    51  		"negative total client storage fee: %v", st.TotalClientLockedCollateral)
    52  
    53  	//
    54  	// Proposals
    55  	//
    56  
    57  	proposalCids := make(map[cid.Cid]struct{})
    58  	maxDealID := int64(-1)
    59  	proposalStats := make(map[abi.DealID]*DealSummary)
    60  	expectedDealOps := make(map[abi.DealID]struct{})
    61  	totalProposalCollateral := abi.NewTokenAmount(0)
    62  
    63  	if proposals, err := adt.AsArray(store, st.Proposals, ProposalsAmtBitwidth); err != nil {
    64  		acc.Addf("error loading proposals: %v", err)
    65  	} else {
    66  		var proposal DealProposal
    67  		err = proposals.ForEach(&proposal, func(dealID int64) error {
    68  			pcid, err := proposal.Cid()
    69  			if err != nil {
    70  				return err
    71  			}
    72  
    73  			if proposal.StartEpoch >= currEpoch {
    74  				expectedDealOps[abi.DealID(dealID)] = struct{}{}
    75  			}
    76  
    77  			// keep some state
    78  			proposalCids[pcid] = struct{}{}
    79  			if dealID > maxDealID {
    80  				maxDealID = dealID
    81  			}
    82  			proposalStats[abi.DealID(dealID)] = &DealSummary{
    83  				Provider:         proposal.Provider,
    84  				StartEpoch:       proposal.StartEpoch,
    85  				EndEpoch:         proposal.EndEpoch,
    86  				SectorStartEpoch: abi.ChainEpoch(-1),
    87  				LastUpdatedEpoch: abi.ChainEpoch(-1),
    88  				SlashEpoch:       abi.ChainEpoch(-1),
    89  			}
    90  
    91  			totalProposalCollateral = big.Sum(totalProposalCollateral, proposal.ClientCollateral, proposal.ProviderCollateral)
    92  
    93  			acc.Require(proposal.Client.Protocol() == address.ID, "client address for deal %d is not an ID address", dealID)
    94  			acc.Require(proposal.Provider.Protocol() == address.ID, "provider address for deal %d is not an ID address", dealID)
    95  			return nil
    96  		})
    97  		acc.RequireNoError(err, "error iterating proposals")
    98  	}
    99  
   100  	// next id should be higher than any existing deal
   101  	acc.Require(int64(st.NextID) > maxDealID, "next id, %d, is not greater than highest id in proposals, %d", st.NextID, maxDealID)
   102  
   103  	//
   104  	// Deal States
   105  	//
   106  
   107  	dealStateCount := uint64(0)
   108  	if dealStates, err := adt.AsArray(store, st.States, StatesAmtBitwidth); err != nil {
   109  		acc.Addf("error loading deal states: %v", err)
   110  	} else {
   111  		var dealState DealState
   112  		err = dealStates.ForEach(&dealState, func(dealID int64) error {
   113  			acc.Require(
   114  				dealState.SectorStartEpoch >= 0,
   115  				"deal %d state start epoch undefined: %v", dealID, dealState)
   116  
   117  			acc.Require(
   118  				dealState.LastUpdatedEpoch == epochUndefined || dealState.LastUpdatedEpoch >= dealState.SectorStartEpoch,
   119  				"deal %d state last updated before sector start: %v", dealID, dealState)
   120  
   121  			acc.Require(
   122  				dealState.LastUpdatedEpoch == epochUndefined || dealState.LastUpdatedEpoch <= currEpoch,
   123  				"deal %d last updated epoch %d after current %d", dealID, dealState.LastUpdatedEpoch, currEpoch)
   124  
   125  			acc.Require(
   126  				dealState.SlashEpoch == epochUndefined || dealState.SlashEpoch >= dealState.SectorStartEpoch,
   127  				"deal %d state slashed before sector start: %v", dealID, dealState)
   128  
   129  			acc.Require(
   130  				dealState.SlashEpoch == epochUndefined || dealState.SlashEpoch <= currEpoch,
   131  				"deal %d state slashed after current epoch %d: %v", dealID, currEpoch, dealState)
   132  
   133  			stats, found := proposalStats[abi.DealID(dealID)]
   134  			if !found {
   135  				acc.Addf("deal proposal %d for deal state not found", dealID)
   136  			} else {
   137  				stats.SectorStartEpoch = dealState.SectorStartEpoch
   138  				stats.LastUpdatedEpoch = dealState.LastUpdatedEpoch
   139  				stats.SlashEpoch = dealState.SlashEpoch
   140  			}
   141  
   142  			dealStateCount++
   143  			return nil
   144  		})
   145  		acc.RequireNoError(err, "error iterating deal states")
   146  	}
   147  
   148  	//
   149  	// Pending Proposals
   150  	//
   151  
   152  	pendingProposalCount := uint64(0)
   153  	if pendingProposals, err := adt.AsMap(store, st.PendingProposals, builtin.DefaultHamtBitwidth); err != nil {
   154  		acc.Addf("error loading pending proposals: %v", err)
   155  	} else {
   156  		err = pendingProposals.ForEach(nil, func(key string) error {
   157  			proposalCID, err := cid.Parse([]byte(key))
   158  			if err != nil {
   159  				return err
   160  			}
   161  
   162  			_, found := proposalCids[proposalCID]
   163  			acc.Require(found, "pending proposal with cid %v not found within proposals %v", proposalCID, pendingProposals)
   164  
   165  			pendingProposalCount++
   166  			return nil
   167  		})
   168  		acc.RequireNoError(err, "error iterating pending proposals")
   169  	}
   170  
   171  	//
   172  	// Escrow Table and Locked Table
   173  	//
   174  
   175  	lockTableCount := uint64(0)
   176  	escrowTable, err := adt.AsBalanceTable(store, st.EscrowTable)
   177  	acc.RequireNoError(err, "error loading escrow table")
   178  	lockTable, err := adt.AsBalanceTable(store, st.LockedTable)
   179  	acc.RequireNoError(err, "error loading locked table")
   180  	if escrowTable != nil && lockTable != nil {
   181  		var lockedAmount abi.TokenAmount
   182  		lockedTotal := abi.NewTokenAmount(0)
   183  		err = (*adt.Map)(lockTable).ForEach(&lockedAmount, func(key string) error {
   184  			addr, err := address.NewFromBytes([]byte(key))
   185  			if err != nil {
   186  				return err
   187  			}
   188  			lockedTotal = big.Add(lockedTotal, lockedAmount)
   189  
   190  			// every entry in locked table should have a corresponding entry in escrow table that is at least as high
   191  			escrowAmount, err := escrowTable.Get(addr)
   192  			if err != nil {
   193  				return err
   194  			}
   195  			acc.Require(escrowAmount.GreaterThanEqual(lockedAmount),
   196  				"locked funds for %s, %s, greater than escrow amount, %s", addr, lockedAmount, escrowAmount)
   197  
   198  			lockTableCount++
   199  			return nil
   200  		})
   201  		acc.RequireNoError(err, "error iterating locked table")
   202  
   203  		// lockTable total should be sum of client and provider locked plus client storage fee
   204  		expectedLockTotal := big.Sum(st.TotalProviderLockedCollateral, st.TotalClientLockedCollateral, st.TotalClientStorageFee)
   205  		acc.Require(lockedTotal.Equals(expectedLockTotal),
   206  			"locked total, %s, does not sum to provider locked, %s, client locked, %s, and client storage fee, %s",
   207  			lockedTotal, st.TotalProviderLockedCollateral, st.TotalClientLockedCollateral, st.TotalClientStorageFee)
   208  
   209  		// assert escrow <= actor balance
   210  		// lockTable item <= escrow item and escrowTotal <= balance implies lockTable total <= balance
   211  		escrowTotal, err := escrowTable.Total()
   212  		acc.RequireNoError(err, "error calculating escrow total")
   213  		acc.Require(escrowTotal.LessThanEqual(balance), "escrow total, %v, greater than actor balance, %v", escrowTotal, balance)
   214  		acc.Require(escrowTotal.GreaterThanEqual(totalProposalCollateral), "escrow total, %v, less than sum of proposal collateral, %v", escrowTotal, totalProposalCollateral)
   215  	}
   216  
   217  	//
   218  	// Deal Ops by Epoch
   219  	//
   220  
   221  	dealOpEpochCount := uint64(0)
   222  	dealOpCount := uint64(0)
   223  	if dealOps, err := AsSetMultimap(store, st.DealOpsByEpoch, builtin.DefaultHamtBitwidth, builtin.DefaultHamtBitwidth); err != nil {
   224  		acc.Addf("error loading deal ops: %v", err)
   225  	} else {
   226  		// get into internals just to iterate through full data structure
   227  		var setRoot cbg.CborCid
   228  		err = dealOps.mp.ForEach(&setRoot, func(key string) error {
   229  			epoch, err := binary.ReadUvarint(bytes.NewReader([]byte(key)))
   230  			if err != nil {
   231  				return errors.Wrapf(err, "deal ops has key that is not an int: %s", key)
   232  			}
   233  
   234  			dealOpEpochCount++
   235  			return dealOps.ForEach(abi.ChainEpoch(epoch), func(id abi.DealID) error {
   236  				_, found := proposalStats[id]
   237  				acc.Require(found, "deal op found for deal id %d with missing proposal at epoch %d", id, epoch)
   238  				delete(expectedDealOps, id)
   239  				dealOpCount++
   240  				return nil
   241  			})
   242  		})
   243  		acc.RequireNoError(err, "error iterating deal ops")
   244  	}
   245  
   246  	acc.Require(len(expectedDealOps) == 0, "missing deal ops for proposals: %v", expectedDealOps)
   247  
   248  	return &StateSummary{
   249  		Deals:                proposalStats,
   250  		PendingProposalCount: pendingProposalCount,
   251  		DealStateCount:       dealStateCount,
   252  		LockTableCount:       lockTableCount,
   253  		DealOpEpochCount:     dealOpEpochCount,
   254  		DealOpCount:          dealOpCount,
   255  	}, acc
   256  }