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

     1  package market
     2  
     3  import (
     4  	"bytes"
     5  
     6  	"github.com/filecoin-project/go-state-types/abi"
     7  	"github.com/filecoin-project/go-state-types/big"
     8  	"github.com/filecoin-project/go-state-types/exitcode"
     9  	"github.com/ipfs/go-cid"
    10  	xerrors "golang.org/x/xerrors"
    11  
    12  	"github.com/filecoin-project/specs-actors/v4/actors/builtin"
    13  	"github.com/filecoin-project/specs-actors/v4/actors/util/adt"
    14  )
    15  
    16  const epochUndefined = abi.ChainEpoch(-1)
    17  
    18  // Market mutations
    19  // add / rm balance
    20  // pub deal (always provider)
    21  // activate deal (miner)
    22  // end deal (miner terminate, expire(no activation))
    23  
    24  // BalanceLockingReason is the reason behind locking an amount.
    25  type BalanceLockingReason int
    26  
    27  const (
    28  	ClientCollateral BalanceLockingReason = iota
    29  	ClientStorageFee
    30  	ProviderCollateral
    31  )
    32  
    33  // Bitwidth of AMTs determined empirically from mutation patterns and projections of mainnet data.
    34  const ProposalsAmtBitwidth = 5
    35  const StatesAmtBitwidth = 6
    36  
    37  type State struct {
    38  	Proposals cid.Cid // AMT[DealID]DealProposal
    39  	States    cid.Cid // AMT[DealID]DealState
    40  
    41  	// PendingProposals tracks dealProposals that have not yet reached their deal start date.
    42  	// We track them here to ensure that miners can't publish the same deal proposal twice
    43  	PendingProposals cid.Cid // Set[DealCid]
    44  
    45  	// Total amount held in escrow, indexed by actor address (including both locked and unlocked amounts).
    46  	EscrowTable cid.Cid // BalanceTable
    47  
    48  	// Amount locked, indexed by actor address.
    49  	// Note: the amounts in this table do not affect the overall amount in escrow:
    50  	// only the _portion_ of the total escrow amount that is locked.
    51  	LockedTable cid.Cid // BalanceTable
    52  
    53  	NextID abi.DealID
    54  
    55  	// Metadata cached for efficient iteration over deals.
    56  	DealOpsByEpoch cid.Cid // SetMultimap, HAMT[epoch]Set
    57  	LastCron       abi.ChainEpoch
    58  
    59  	// Total Client Collateral that is locked -> unlocked when deal is terminated
    60  	TotalClientLockedCollateral abi.TokenAmount
    61  	// Total Provider Collateral that is locked -> unlocked when deal is terminated
    62  	TotalProviderLockedCollateral abi.TokenAmount
    63  	// Total storage fee that is locked in escrow -> unlocked when payments are made
    64  	TotalClientStorageFee abi.TokenAmount
    65  }
    66  
    67  func ConstructState(store adt.Store) (*State, error) {
    68  	emptyProposalsArrayCid, err := adt.StoreEmptyArray(store, ProposalsAmtBitwidth)
    69  	if err != nil {
    70  		return nil, xerrors.Errorf("failed to create empty array: %w", err)
    71  	}
    72  	emptyStatesArrayCid, err := adt.StoreEmptyArray(store, StatesAmtBitwidth)
    73  	if err != nil {
    74  		return nil, xerrors.Errorf("failed to create empty states array: %w", err)
    75  	}
    76  
    77  	emptyPendingProposalsMapCid, err := adt.StoreEmptyMap(store, builtin.DefaultHamtBitwidth)
    78  	if err != nil {
    79  		return nil, xerrors.Errorf("failed to create empty map: %w", err)
    80  	}
    81  	emptyDealOpsHamtCid, err := StoreEmptySetMultimap(store, builtin.DefaultHamtBitwidth)
    82  	if err != nil {
    83  		return nil, xerrors.Errorf("failed to create empty multiset: %w", err)
    84  	}
    85  	emptyBalanceTableCid, err := adt.StoreEmptyMap(store, adt.BalanceTableBitwidth)
    86  	if err != nil {
    87  		return nil, xerrors.Errorf("failed to create empty balance table: %w", err)
    88  	}
    89  
    90  	return &State{
    91  		Proposals:        emptyProposalsArrayCid,
    92  		States:           emptyStatesArrayCid,
    93  		PendingProposals: emptyPendingProposalsMapCid,
    94  		EscrowTable:      emptyBalanceTableCid,
    95  		LockedTable:      emptyBalanceTableCid,
    96  		NextID:           abi.DealID(0),
    97  		DealOpsByEpoch:   emptyDealOpsHamtCid,
    98  		LastCron:         abi.ChainEpoch(-1),
    99  
   100  		TotalClientLockedCollateral:   abi.NewTokenAmount(0),
   101  		TotalProviderLockedCollateral: abi.NewTokenAmount(0),
   102  		TotalClientStorageFee:         abi.NewTokenAmount(0),
   103  	}, nil
   104  }
   105  
   106  ////////////////////////////////////////////////////////////////////////////////
   107  // Deal state operations
   108  ////////////////////////////////////////////////////////////////////////////////
   109  
   110  func (m *marketStateMutation) updatePendingDealState(rt Runtime, state *DealState, deal *DealProposal, epoch abi.ChainEpoch) (amountSlashed abi.TokenAmount, nextEpoch abi.ChainEpoch, removeDeal bool) {
   111  	amountSlashed = abi.NewTokenAmount(0)
   112  
   113  	everUpdated := state.LastUpdatedEpoch != epochUndefined
   114  	everSlashed := state.SlashEpoch != epochUndefined
   115  
   116  	builtin.RequireState(rt, !everUpdated || (state.LastUpdatedEpoch <= epoch), "deal updated at future epoch %d", state.LastUpdatedEpoch)
   117  
   118  	// This would be the case that the first callback somehow triggers before it is scheduled to
   119  	// This is expected not to be able to happen
   120  	if deal.StartEpoch > epoch {
   121  		return amountSlashed, epochUndefined, false
   122  	}
   123  
   124  	paymentEndEpoch := deal.EndEpoch
   125  	if everSlashed {
   126  		builtin.RequireState(rt, epoch >= state.SlashEpoch, "current epoch less than deal slash epoch %d", state.SlashEpoch)
   127  		builtin.RequireState(rt, state.SlashEpoch <= deal.EndEpoch, "deal slash epoch %d after deal end %d", state.SlashEpoch, deal.EndEpoch)
   128  		paymentEndEpoch = state.SlashEpoch
   129  	} else if epoch < paymentEndEpoch {
   130  		paymentEndEpoch = epoch
   131  	}
   132  
   133  	paymentStartEpoch := deal.StartEpoch
   134  	if everUpdated && state.LastUpdatedEpoch > paymentStartEpoch {
   135  		paymentStartEpoch = state.LastUpdatedEpoch
   136  	}
   137  
   138  	numEpochsElapsed := paymentEndEpoch - paymentStartEpoch
   139  
   140  	{
   141  		// Process deal payment for the elapsed epochs.
   142  		totalPayment := big.Mul(big.NewInt(int64(numEpochsElapsed)), deal.StoragePricePerEpoch)
   143  
   144  		// the transfer amount can be less than or equal to zero if a deal is slashed before or at the deal's start epoch.
   145  		if totalPayment.GreaterThan(big.Zero()) {
   146  			err := m.transferBalance(deal.Client, deal.Provider, totalPayment)
   147  			builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to transfer %v from %v to %v",
   148  				totalPayment, deal.Client, deal.Provider)
   149  		}
   150  	}
   151  
   152  	if everSlashed {
   153  		// unlock client collateral and locked storage fee
   154  		paymentRemaining, err := dealGetPaymentRemaining(deal, state.SlashEpoch)
   155  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to compute remaining payment")
   156  
   157  		// unlock remaining storage fee
   158  		err = m.unlockBalance(deal.Client, paymentRemaining, ClientStorageFee)
   159  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to unlock remaining client storage fee")
   160  
   161  		// unlock client collateral
   162  		err = m.unlockBalance(deal.Client, deal.ClientCollateral, ClientCollateral)
   163  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to unlock client collateral")
   164  
   165  		// slash provider collateral
   166  		amountSlashed = deal.ProviderCollateral
   167  		err = m.slashBalance(deal.Provider, amountSlashed, ProviderCollateral)
   168  		builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "slashing balance")
   169  		return amountSlashed, epochUndefined, true
   170  	}
   171  
   172  	if epoch >= deal.EndEpoch {
   173  		m.processDealExpired(rt, deal, state)
   174  		return amountSlashed, epochUndefined, true
   175  	}
   176  
   177  	// We're explicitly not inspecting the end epoch and may process a deal's expiration late, in order to prevent an outsider
   178  	// from loading a cron tick by activating too many deals with the same end epoch.
   179  	nextEpoch = epoch + DealUpdatesInterval
   180  
   181  	return amountSlashed, nextEpoch, false
   182  }
   183  
   184  // Deal start deadline elapsed without appearing in a proven sector.
   185  // Slash a portion of provider's collateral, and unlock remaining collaterals
   186  // for both provider and client.
   187  func (m *marketStateMutation) processDealInitTimedOut(rt Runtime, deal *DealProposal) abi.TokenAmount {
   188  	if err := m.unlockBalance(deal.Client, deal.TotalStorageFee(), ClientStorageFee); err != nil {
   189  		rt.Abortf(exitcode.ErrIllegalState, "failure unlocking client storage fee: %s", err)
   190  	}
   191  	if err := m.unlockBalance(deal.Client, deal.ClientCollateral, ClientCollateral); err != nil {
   192  		rt.Abortf(exitcode.ErrIllegalState, "failure unlocking client collateral: %s", err)
   193  	}
   194  
   195  	amountSlashed := CollateralPenaltyForDealActivationMissed(deal.ProviderCollateral)
   196  	amountRemaining := big.Sub(deal.ProviderBalanceRequirement(), amountSlashed)
   197  
   198  	if err := m.slashBalance(deal.Provider, amountSlashed, ProviderCollateral); err != nil {
   199  		rt.Abortf(exitcode.ErrIllegalState, "failed to slash balance: %s", err)
   200  	}
   201  
   202  	if err := m.unlockBalance(deal.Provider, amountRemaining, ProviderCollateral); err != nil {
   203  		rt.Abortf(exitcode.ErrIllegalState, "failed to unlock deal provider balance: %s", err)
   204  	}
   205  
   206  	return amountSlashed
   207  }
   208  
   209  // Normal expiration. Unlock collaterals for both provider and client.
   210  func (m *marketStateMutation) processDealExpired(rt Runtime, deal *DealProposal, state *DealState) {
   211  	builtin.RequireState(rt, state.SectorStartEpoch != epochUndefined, "sector start epoch undefined")
   212  
   213  	// Note: payment has already been completed at this point (_rtProcessDealPaymentEpochsElapsed)
   214  	err := m.unlockBalance(deal.Provider, deal.ProviderCollateral, ProviderCollateral)
   215  	builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed unlocking deal provider balance")
   216  
   217  	err = m.unlockBalance(deal.Client, deal.ClientCollateral, ClientCollateral)
   218  	builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed unlocking deal client balance")
   219  }
   220  
   221  func (m *marketStateMutation) generateStorageDealID() abi.DealID {
   222  	ret := m.nextDealId
   223  	m.nextDealId = m.nextDealId + abi.DealID(1)
   224  	return ret
   225  }
   226  
   227  ////////////////////////////////////////////////////////////////////////////////
   228  // State utility functions
   229  ////////////////////////////////////////////////////////////////////////////////
   230  
   231  func dealProposalIsInternallyValid(rt Runtime, proposal ClientDealProposal) error {
   232  	// Note: we do not verify the provider signature here, since this is implicit in the
   233  	// authenticity of the on-chain message publishing the deal.
   234  	buf := bytes.Buffer{}
   235  	err := proposal.Proposal.MarshalCBOR(&buf)
   236  	if err != nil {
   237  		return xerrors.Errorf("proposal signature verification failed to marshal proposal: %w", err)
   238  	}
   239  	err = rt.VerifySignature(proposal.ClientSignature, proposal.Proposal.Client, buf.Bytes())
   240  	if err != nil {
   241  		return xerrors.Errorf("signature proposal invalid: %w", err)
   242  	}
   243  	return nil
   244  }
   245  
   246  func dealGetPaymentRemaining(deal *DealProposal, slashEpoch abi.ChainEpoch) (abi.TokenAmount, error) {
   247  	if slashEpoch > deal.EndEpoch {
   248  		return big.Zero(), xerrors.Errorf("deal slash epoch %d after end epoch %d", slashEpoch, deal.EndEpoch)
   249  	}
   250  
   251  	// Payments are always for start -> end epoch irrespective of when the deal is slashed.
   252  	if slashEpoch < deal.StartEpoch {
   253  		slashEpoch = deal.StartEpoch
   254  	}
   255  
   256  	durationRemaining := deal.EndEpoch - slashEpoch
   257  	if durationRemaining < 0 {
   258  		return big.Zero(), xerrors.Errorf("deal remaining duration negative: %d", durationRemaining)
   259  	}
   260  
   261  	return big.Mul(big.NewInt(int64(durationRemaining)), deal.StoragePricePerEpoch), nil
   262  }
   263  
   264  // MarketStateMutationPermission is the mutation permission on a state field
   265  type MarketStateMutationPermission int
   266  
   267  const (
   268  	// Invalid means NO permission
   269  	Invalid MarketStateMutationPermission = iota
   270  	// ReadOnlyPermission allows reading but not mutating the field
   271  	ReadOnlyPermission
   272  	// WritePermission allows mutating the field
   273  	WritePermission
   274  )
   275  
   276  type marketStateMutation struct {
   277  	st    *State
   278  	store adt.Store
   279  
   280  	proposalPermit MarketStateMutationPermission
   281  	dealProposals  *DealArray
   282  
   283  	statePermit MarketStateMutationPermission
   284  	dealStates  *DealMetaArray
   285  
   286  	escrowPermit MarketStateMutationPermission
   287  	escrowTable  *adt.BalanceTable
   288  
   289  	pendingPermit MarketStateMutationPermission
   290  	pendingDeals  *adt.Set
   291  
   292  	dpePermit    MarketStateMutationPermission
   293  	dealsByEpoch *SetMultimap
   294  
   295  	lockedPermit                  MarketStateMutationPermission
   296  	lockedTable                   *adt.BalanceTable
   297  	totalClientLockedCollateral   abi.TokenAmount
   298  	totalProviderLockedCollateral abi.TokenAmount
   299  	totalClientStorageFee         abi.TokenAmount
   300  
   301  	nextDealId abi.DealID
   302  }
   303  
   304  func (s *State) mutator(store adt.Store) *marketStateMutation {
   305  	return &marketStateMutation{st: s, store: store}
   306  }
   307  
   308  func (m *marketStateMutation) build() (*marketStateMutation, error) {
   309  	if m.proposalPermit != Invalid {
   310  		proposals, err := AsDealProposalArray(m.store, m.st.Proposals)
   311  		if err != nil {
   312  			return nil, xerrors.Errorf("failed to load deal proposals: %w", err)
   313  		}
   314  		m.dealProposals = proposals
   315  	}
   316  
   317  	if m.statePermit != Invalid {
   318  		states, err := AsDealStateArray(m.store, m.st.States)
   319  		if err != nil {
   320  			return nil, xerrors.Errorf("failed to load deal state: %w", err)
   321  		}
   322  		m.dealStates = states
   323  	}
   324  
   325  	if m.lockedPermit != Invalid {
   326  		lt, err := adt.AsBalanceTable(m.store, m.st.LockedTable)
   327  		if err != nil {
   328  			return nil, xerrors.Errorf("failed to load locked table: %w", err)
   329  		}
   330  		m.lockedTable = lt
   331  		m.totalClientLockedCollateral = m.st.TotalClientLockedCollateral.Copy()
   332  		m.totalClientStorageFee = m.st.TotalClientStorageFee.Copy()
   333  		m.totalProviderLockedCollateral = m.st.TotalProviderLockedCollateral.Copy()
   334  	}
   335  
   336  	if m.escrowPermit != Invalid {
   337  		et, err := adt.AsBalanceTable(m.store, m.st.EscrowTable)
   338  		if err != nil {
   339  			return nil, xerrors.Errorf("failed to load escrow table: %w", err)
   340  		}
   341  		m.escrowTable = et
   342  	}
   343  
   344  	if m.pendingPermit != Invalid {
   345  		pending, err := adt.AsSet(m.store, m.st.PendingProposals, builtin.DefaultHamtBitwidth)
   346  		if err != nil {
   347  			return nil, xerrors.Errorf("failed to load pending proposals: %w", err)
   348  		}
   349  		m.pendingDeals = pending
   350  	}
   351  
   352  	if m.dpePermit != Invalid {
   353  		dbe, err := AsSetMultimap(m.store, m.st.DealOpsByEpoch, builtin.DefaultHamtBitwidth, builtin.DefaultHamtBitwidth)
   354  		if err != nil {
   355  			return nil, xerrors.Errorf("failed to load deals by epoch: %w", err)
   356  		}
   357  		m.dealsByEpoch = dbe
   358  	}
   359  
   360  	m.nextDealId = m.st.NextID
   361  
   362  	return m, nil
   363  }
   364  
   365  func (m *marketStateMutation) withDealProposals(permit MarketStateMutationPermission) *marketStateMutation {
   366  	m.proposalPermit = permit
   367  	return m
   368  }
   369  
   370  func (m *marketStateMutation) withDealStates(permit MarketStateMutationPermission) *marketStateMutation {
   371  	m.statePermit = permit
   372  	return m
   373  }
   374  
   375  func (m *marketStateMutation) withEscrowTable(permit MarketStateMutationPermission) *marketStateMutation {
   376  	m.escrowPermit = permit
   377  	return m
   378  }
   379  
   380  func (m *marketStateMutation) withLockedTable(permit MarketStateMutationPermission) *marketStateMutation {
   381  	m.lockedPermit = permit
   382  	return m
   383  }
   384  
   385  func (m *marketStateMutation) withPendingProposals(permit MarketStateMutationPermission) *marketStateMutation {
   386  	m.pendingPermit = permit
   387  	return m
   388  }
   389  
   390  func (m *marketStateMutation) withDealsByEpoch(permit MarketStateMutationPermission) *marketStateMutation {
   391  	m.dpePermit = permit
   392  	return m
   393  }
   394  
   395  func (m *marketStateMutation) commitState() error {
   396  	var err error
   397  	if m.proposalPermit == WritePermission {
   398  		if m.st.Proposals, err = m.dealProposals.Root(); err != nil {
   399  			return xerrors.Errorf("failed to flush deal dealProposals: %w", err)
   400  		}
   401  	}
   402  
   403  	if m.statePermit == WritePermission {
   404  		if m.st.States, err = m.dealStates.Root(); err != nil {
   405  			return xerrors.Errorf("failed to flush deal states: %w", err)
   406  		}
   407  	}
   408  
   409  	if m.lockedPermit == WritePermission {
   410  		if m.st.LockedTable, err = m.lockedTable.Root(); err != nil {
   411  			return xerrors.Errorf("failed to flush locked table: %w", err)
   412  		}
   413  		m.st.TotalClientLockedCollateral = m.totalClientLockedCollateral.Copy()
   414  		m.st.TotalProviderLockedCollateral = m.totalProviderLockedCollateral.Copy()
   415  		m.st.TotalClientStorageFee = m.totalClientStorageFee.Copy()
   416  	}
   417  
   418  	if m.escrowPermit == WritePermission {
   419  		if m.st.EscrowTable, err = m.escrowTable.Root(); err != nil {
   420  			return xerrors.Errorf("failed to flush escrow table: %w", err)
   421  		}
   422  	}
   423  
   424  	if m.pendingPermit == WritePermission {
   425  		if m.st.PendingProposals, err = m.pendingDeals.Root(); err != nil {
   426  			return xerrors.Errorf("failed to flush pending deals: %w", err)
   427  		}
   428  	}
   429  
   430  	if m.dpePermit == WritePermission {
   431  		if m.st.DealOpsByEpoch, err = m.dealsByEpoch.Root(); err != nil {
   432  			return xerrors.Errorf("failed to flush deals by epoch: %w", err)
   433  		}
   434  	}
   435  
   436  	m.st.NextID = m.nextDealId
   437  	return nil
   438  }