Storage Market Actor
-
State
reliable
-
Theory Audit
done
-
Edit this section
-
section-systems.filecoin_markets.onchain_storage_market.storage_market_actor
-
State
reliable
-
Theory Audit
done
- Edit this section
-
section-systems.filecoin_markets.onchain_storage_market.storage_market_actor
The StorageMarketActor
is responsible for processing and managing on-chain deals. This is also the entry point of all storage deals and data into the system. It maintains a mapping of StorageDealID
to StorageDeal
and keeps track of locked balances of StorageClient
and StorageProvider
. When a deal is posted on chain through the StorageMarketActor
, it will first check if both transacting parties have sufficient balances locked up and include the deal on chain.
StorageMarketActor
implementation
-
State
reliable
-
Theory Audit
done
-
Edit this section
-
section-systems.filecoin_markets.onchain_storage_market.storage_market_actor.storagemarketactor-implementation
-
State
reliable
-
Theory Audit
done
- Edit this section
-
section-systems.filecoin_markets.onchain_storage_market.storage_market_actor.storagemarketactor-implementation
package market
import (
"sort"
addr "github.com/filecoin-project/go-address"
"github.com/filecoin-project/go-bitfield"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/go-state-types/cbor"
"github.com/filecoin-project/go-state-types/exitcode"
rtt "github.com/filecoin-project/go-state-types/rt"
market0 "github.com/filecoin-project/specs-actors/actors/builtin/market"
market3 "github.com/filecoin-project/specs-actors/v3/actors/builtin/market"
market5 "github.com/filecoin-project/specs-actors/v5/actors/builtin/market"
market6 "github.com/filecoin-project/specs-actors/v6/actors/builtin/market"
"github.com/ipfs/go-cid"
cbg "github.com/whyrusleeping/cbor-gen"
"golang.org/x/xerrors"
"github.com/filecoin-project/specs-actors/v8/actors/builtin"
"github.com/filecoin-project/specs-actors/v8/actors/builtin/power"
"github.com/filecoin-project/specs-actors/v8/actors/builtin/reward"
"github.com/filecoin-project/specs-actors/v8/actors/builtin/verifreg"
"github.com/filecoin-project/specs-actors/v8/actors/runtime"
"github.com/filecoin-project/specs-actors/v8/actors/util/adt"
)
type Actor struct{}
type Runtime = runtime.Runtime
func (a Actor) Exports() []interface{} {
return []interface{}{
builtin.MethodConstructor: a.Constructor,
2: a.AddBalance,
3: a.WithdrawBalance,
4: a.PublishStorageDeals,
5: a.VerifyDealsForActivation,
6: a.ActivateDeals,
7: a.OnMinerSectorsTerminate,
8: a.ComputeDataCommitment,
9: a.CronTick,
}
}
func (a Actor) Code() cid.Cid {
return builtin.StorageMarketActorCodeID
}
func (a Actor) IsSingleton() bool {
return true
}
func (a Actor) State() cbor.Er {
return new(State)
}
var _ runtime.VMActor = Actor{}
////////////////////////////////////////////////////////////////////////////////
// Actor methods
////////////////////////////////////////////////////////////////////////////////
func (a Actor) Constructor(rt Runtime, _ *abi.EmptyValue) *abi.EmptyValue {
rt.ValidateImmediateCallerIs(builtin.SystemActorAddr)
st, err := ConstructState(adt.AsStore(rt))
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to create state")
rt.StateCreate(st)
return nil
}
//type WithdrawBalanceParams struct {
// ProviderOrClientAddress addr.Address
// Amount abi.TokenAmount
//}
type WithdrawBalanceParams = market0.WithdrawBalanceParams
// Attempt to withdraw the specified amount from the balance held in escrow.
// If less than the specified amount is available, yields the entire available balance.
// Returns the amount withdrawn.
func (a Actor) WithdrawBalance(rt Runtime, params *WithdrawBalanceParams) *abi.TokenAmount {
if params.Amount.LessThan(big.Zero()) {
rt.Abortf(exitcode.ErrIllegalArgument, "negative amount %v", params.Amount)
}
nominal, recipient, approvedCallers := escrowAddress(rt, params.ProviderOrClientAddress)
// for providers -> only corresponding owner or worker can withdraw
// for clients -> only the client i.e the recipient can withdraw
rt.ValidateImmediateCallerIs(approvedCallers...)
amountExtracted := abi.NewTokenAmount(0)
var st State
rt.StateTransaction(&st, func() {
msm, err := st.mutator(adt.AsStore(rt)).withEscrowTable(WritePermission).
withLockedTable(WritePermission).build()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load state")
// The withdrawable amount might be slightly less than nominal
// depending on whether or not all relevant entries have been processed
// by cron
minBalance, err := msm.lockedTable.Get(nominal)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to get locked balance")
ex, err := msm.escrowTable.SubtractWithMinimum(nominal, params.Amount, minBalance)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to subtract from escrow table")
err = msm.commitState()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to flush state")
amountExtracted = ex
})
code := rt.Send(recipient, builtin.MethodSend, nil, amountExtracted, &builtin.Discard{})
builtin.RequireSuccess(rt, code, "failed to send funds")
return &amountExtracted
}
// Deposits the received value into the balance held in escrow.
func (a Actor) AddBalance(rt Runtime, providerOrClientAddress *addr.Address) *abi.EmptyValue {
msgValue := rt.ValueReceived()
builtin.RequireParam(rt, msgValue.GreaterThan(big.Zero()), "balance to add must be greater than zero")
// only signing parties can add balance for client AND provider.
rt.ValidateImmediateCallerType(builtin.CallerTypesSignable...)
nominal, _, _ := escrowAddress(rt, *providerOrClientAddress)
var st State
rt.StateTransaction(&st, func() {
msm, err := st.mutator(adt.AsStore(rt)).withEscrowTable(WritePermission).
withLockedTable(WritePermission).build()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load state")
err = msm.escrowTable.Add(nominal, msgValue)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to add balance to escrow table")
err = msm.commitState()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to flush state")
})
return nil
}
type PublishStorageDealsParams struct {
Deals []ClientDealProposal
}
//type PublishStorageDealsReturn struct {
// IDs []abi.DealID
// ValidDeals bitfield.BitField
//}
type PublishStorageDealsReturn = market6.PublishStorageDealsReturn
// Publish a new set of storage deals (not yet included in a sector).
func (a Actor) PublishStorageDeals(rt Runtime, params *PublishStorageDealsParams) *PublishStorageDealsReturn {
// Deal message must have a From field identical to the provider of all the deals.
// This allows us to retain and verify only the client's signature in each deal proposal itself.
rt.ValidateImmediateCallerType(builtin.CallerTypesSignable...)
if len(params.Deals) == 0 {
rt.Abortf(exitcode.ErrIllegalArgument, "empty deals parameter")
}
// All deals should have the same provider so get worker once
providerRaw := params.Deals[0].Proposal.Provider
provider, ok := rt.ResolveAddress(providerRaw)
if !ok {
rt.Abortf(exitcode.ErrNotFound, "failed to resolve provider address %v", providerRaw)
}
codeID, ok := rt.GetActorCodeCID(provider)
builtin.RequireParam(rt, ok, "no codeId for address %v", provider)
if !codeID.Equals(builtin.StorageMinerActorCodeID) {
rt.Abortf(exitcode.ErrIllegalArgument, "deal provider is not a StorageMinerActor")
}
caller := rt.Caller()
_, worker, controllers := builtin.RequestMinerControlAddrs(rt, provider)
callerOk := caller == worker
for _, controller := range controllers {
if callerOk {
break
}
callerOk = caller == controller
}
if !callerOk {
rt.Abortf(exitcode.ErrForbidden, "caller %v is not worker or control address of provider %v", caller, provider)
}
resolvedAddrs := make(map[addr.Address]addr.Address, len(params.Deals))
baselinePower := requestCurrentBaselinePower(rt)
networkRawPower, networkQAPower := requestCurrentNetworkPower(rt)
// Drop invalid deals
var st State
proposalCidLookup := make(map[cid.Cid]struct{})
validProposalCids := make([]cid.Cid, 0)
validDeals := make([]ClientDealProposal, 0, len(params.Deals))
totalClientLockup := make(map[addr.Address]abi.TokenAmount)
totalProviderLockup := abi.NewTokenAmount(0)
validInputBf := bitfield.New()
rt.StateReadonly(&st)
msm, err := st.mutator(adt.AsStore(rt)).withPendingProposals(ReadOnlyPermission).
withEscrowTable(ReadOnlyPermission).withLockedTable(ReadOnlyPermission).build()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load state")
for di, deal := range params.Deals {
/*
drop malformed deals
*/
if err := validateDeal(rt, deal, networkRawPower, networkQAPower, baselinePower); err != nil {
rt.Log(rtt.INFO, "invalid deal %d: %s", di, err)
continue
}
if deal.Proposal.Provider != provider && deal.Proposal.Provider != providerRaw {
rt.Log(rtt.INFO, "invalid deal %d: cannot publish deals from multiple providers in one batch", di)
continue
}
client, ok := rt.ResolveAddress(deal.Proposal.Client)
if !ok {
rt.Log(rtt.INFO, "invalid deal %d: failed to resolve proposal.Client address %v for deal ", di, deal.Proposal.Client)
continue
}
/*
drop deals with insufficient lock up to cover costs
*/
if _, ok := totalClientLockup[client]; !ok {
totalClientLockup[client] = abi.NewTokenAmount(0)
}
totalClientLockup[client] = big.Sum(totalClientLockup[client], deal.Proposal.ClientBalanceRequirement())
clientBalanceOk, err := msm.balanceCovered(client, totalClientLockup[client])
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to check client balance coverage")
if !clientBalanceOk {
rt.Log(rtt.INFO, "invalid deal: %d: insufficient client funds to cover proposal cost", di)
continue
}
totalProviderLockup = big.Sum(totalProviderLockup, deal.Proposal.ProviderCollateral)
providerBalanceOk, err := msm.balanceCovered(provider, totalProviderLockup)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to check provider balance coverage")
if !providerBalanceOk {
rt.Log(rtt.INFO, "invalid deal: %d: insufficient provider funds to cover proposal cost", di)
continue
}
/*
drop duplicate deals
*/
// Normalise provider and client addresses in the proposal stored on chain.
// Must happen after signature verification and before taking cid.
deal.Proposal.Provider = provider
resolvedAddrs[deal.Proposal.Client] = client
deal.Proposal.Client = client
pcid, err := deal.Proposal.Cid()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalArgument, "failed to take cid of proposal %d", di)
// check proposalCids for duplication within message batch
// check state PendingProposals for duplication across messages
duplicateInState, err := msm.pendingDeals.Has(abi.CidKey(pcid))
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to check for existence of deal proposal")
_, duplicateInMessage := proposalCidLookup[pcid]
if duplicateInState || duplicateInMessage {
rt.Log(rtt.INFO, "invalid deal %d: cannot publish duplicate deal proposal %s", di)
continue
}
/*
check VerifiedClient allowed cap and deduct PieceSize from cap
drop deals with a DealSize that cannot be fully covered by VerifiedClient's available DataCap
*/
if deal.Proposal.VerifiedDeal {
code := rt.Send(
builtin.VerifiedRegistryActorAddr,
builtin.MethodsVerifiedRegistry.UseBytes,
&verifreg.UseBytesParams{
Address: client,
DealSize: big.NewIntUnsigned(uint64(deal.Proposal.PieceSize)),
},
abi.NewTokenAmount(0),
&builtin.Discard{},
)
if code.IsError() {
rt.Log(rtt.INFO, "invalid deal %d: failed to acquire datacap exitcode: %d", di, code)
continue
}
}
// update valid deal state
proposalCidLookup[pcid] = struct{}{}
validProposalCids = append(validProposalCids, pcid)
validDeals = append(validDeals, deal)
validInputBf.Set(uint64(di))
}
validDealCount, err := validInputBf.Count()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to count valid deals in bitfield")
builtin.RequirePredicate(rt, len(validDeals) == len(validProposalCids), exitcode.ErrIllegalState,
"%d valid deals but %d valid proposal cids", len(validDeals), len(validProposalCids))
builtin.RequirePredicate(rt, uint64(len(validDeals)) == validDealCount, exitcode.ErrIllegalState,
"%d valid deals but validDealCount=%d", len(validDeals), validDealCount)
builtin.RequireParam(rt, validDealCount > 0, "All deal proposals invalid")
var newDealIds []abi.DealID
rt.StateTransaction(&st, func() {
msm, err := st.mutator(adt.AsStore(rt)).withPendingProposals(WritePermission).
withDealProposals(WritePermission).withDealsByEpoch(WritePermission).withEscrowTable(WritePermission).
withLockedTable(WritePermission).build()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load state")
// All storage dealProposals will be added in an atomic transaction; this operation will be unrolled if any of them fails.
// This should only fail on programmer error because all expected invalid conditions should be filtered in the first set of checks.
for vdi, validDeal := range validDeals {
err := msm.lockClientAndProviderBalances(&validDeal.Proposal)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to lock balance")
id := msm.generateStorageDealID()
pcid := validProposalCids[vdi]
err = msm.pendingDeals.Put(abi.CidKey(pcid))
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to set pending deal")
err = msm.dealProposals.Set(id, &validDeal.Proposal)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to set deal")
// We randomize the first epoch for when the deal will be processed so an attacker isn't able to
// schedule too many deals for the same tick.
processEpoch := GenRandNextEpoch(validDeal.Proposal.StartEpoch, id)
err = msm.dealsByEpoch.Put(processEpoch, id)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to set deal ops by epoch")
newDealIds = append(newDealIds, id)
}
err = msm.commitState()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to flush state")
})
return &PublishStorageDealsReturn{
IDs: newDealIds,
ValidDeals: validInputBf,
}
}
// Changed in v3:
// - Array of sectors rather than just one
// - Removed SectorStart (which is unknown at call time)
//type VerifyDealsForActivationParams struct {
// Sectors []SectorDeals
//}
type VerifyDealsForActivationParams = market3.VerifyDealsForActivationParams
//type SectorDeals struct {
// SectorExpiry abi.ChainEpoch
// DealIDs []abi.DealID
//}
type SectorDeals = market3.SectorDeals
// Changed in v3:
// - Array of sectors weights
//type VerifyDealsForActivationReturn struct {
// Sectors []SectorWeights
//}
type VerifyDealsForActivationReturn = market3.VerifyDealsForActivationReturn
//type SectorWeights struct {
// DealSpace uint64 // Total space in bytes of submitted deals.
// DealWeight abi.DealWeight // Total space*time of submitted deals.
// VerifiedDealWeight abi.DealWeight // Total space*time of submitted verified deals.
//}
type SectorWeights = market3.SectorWeights
// Computes the weight of deals proposed for inclusion in a number of sectors.
// Deal weight is defined as the sum, over all deals in the set, of the product of deal size and duration.
//
// This method performs some light validation on the way in order to fail early if deals can be
// determined to be invalid for the proposed sector properties.
// Full deal validation is deferred to deal activation since it depends on the activation epoch.
func (a Actor) VerifyDealsForActivation(rt Runtime, params *VerifyDealsForActivationParams) *VerifyDealsForActivationReturn {
rt.ValidateImmediateCallerType(builtin.StorageMinerActorCodeID)
minerAddr := rt.Caller()
currEpoch := rt.CurrEpoch()
var st State
rt.StateReadonly(&st)
store := adt.AsStore(rt)
proposals, err := AsDealProposalArray(store, st.Proposals)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load deal proposals")
weights := make([]SectorWeights, len(params.Sectors))
for i, sector := range params.Sectors {
// Pass the current epoch as the activation epoch for validation.
// The sector activation epoch isn't yet known, but it's still more helpful to fail now if the deal
// is so late that a sector activating now couldn't include it.
dealWeight, verifiedWeight, dealSpace, err := validateAndComputeDealWeight(proposals, sector.DealIDs, minerAddr, sector.SectorExpiry, currEpoch)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to validate deal proposals for activation")
weights[i] = SectorWeights{
DealSpace: dealSpace,
DealWeight: dealWeight,
VerifiedDealWeight: verifiedWeight,
}
}
return &VerifyDealsForActivationReturn{
Sectors: weights,
}
}
//type ActivateDealsParams struct {
// DealIDs []abi.DealID
// SectorExpiry abi.ChainEpoch
//}
type ActivateDealsParams = market0.ActivateDealsParams
// Verify that a given set of storage deals is valid for a sector currently being ProveCommitted,
// update the market's internal state accordingly.
func (a Actor) ActivateDeals(rt Runtime, params *ActivateDealsParams) *abi.EmptyValue {
rt.ValidateImmediateCallerType(builtin.StorageMinerActorCodeID)
minerAddr := rt.Caller()
currEpoch := rt.CurrEpoch()
var st State
store := adt.AsStore(rt)
// Update deal dealStates.
rt.StateTransaction(&st, func() {
_, _, _, err := ValidateDealsForActivation(&st, store, params.DealIDs, minerAddr, params.SectorExpiry, currEpoch)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to validate dealProposals for activation")
msm, err := st.mutator(adt.AsStore(rt)).withDealStates(WritePermission).
withPendingProposals(ReadOnlyPermission).withDealProposals(ReadOnlyPermission).build()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load state")
for _, dealID := range params.DealIDs {
// This construction could be replaced with a single "update deal state" state method, possibly batched
// over all deal ids at once.
_, found, err := msm.dealStates.Get(dealID)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to get state for dealId %d", dealID)
if found {
rt.Abortf(exitcode.ErrIllegalArgument, "deal %d already included in another sector", dealID)
}
proposal, err := getDealProposal(msm.dealProposals, dealID)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to get dealId %d", dealID)
propc, err := proposal.Cid()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to calculate proposal CID")
has, err := msm.pendingDeals.Has(abi.CidKey(propc))
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to get pending proposal %v", propc)
if !has {
rt.Abortf(exitcode.ErrIllegalState, "tried to activate deal that was not in the pending set (%s)", propc)
}
err = msm.dealStates.Set(dealID, &DealState{
SectorStartEpoch: currEpoch,
LastUpdatedEpoch: EpochUndefined,
SlashEpoch: EpochUndefined,
})
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to set deal state %d", dealID)
}
err = msm.commitState()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to flush state")
})
return nil
}
//type SectorDataSpec struct {
// DealIDs []abi.DealID
// SectorType abi.RegisteredSealProof
//}
type SectorDataSpec = market5.SectorDataSpec
//type ComputeDataCommitmentParams struct {
// Inputs []*SectorDataSpec
//}
type ComputeDataCommitmentParams = market5.ComputeDataCommitmentParams
//type ComputeDataCommitmentReturn struct {
// CommDs []cbg.CborCid
//}
type ComputeDataCommitmentReturn = market5.ComputeDataCommitmentReturn
func (a Actor) ComputeDataCommitment(rt Runtime, params *ComputeDataCommitmentParams) *ComputeDataCommitmentReturn {
rt.ValidateImmediateCallerType(builtin.StorageMinerActorCodeID)
var st State
rt.StateReadonly(&st)
proposals, err := AsDealProposalArray(adt.AsStore(rt), st.Proposals)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load deal dealProposals")
commDs := make([]cbg.CborCid, len(params.Inputs))
for i, commInput := range params.Inputs {
pieces := make([]abi.PieceInfo, 0)
for _, dealID := range commInput.DealIDs {
deal, err := getDealProposal(proposals, dealID)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to get dealId %d", dealID)
pieces = append(pieces, abi.PieceInfo{
PieceCID: deal.PieceCID,
Size: deal.PieceSize,
})
}
commD, err := rt.ComputeUnsealedSectorCID(commInput.SectorType, pieces)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalArgument, "failed to compute unsealed sectorCID: %s", err)
commDs[i] = (cbg.CborCid)(commD)
}
return &ComputeDataCommitmentReturn{
CommDs: commDs,
}
}
//type OnMinerSectorsTerminateParams struct {
// Epoch abi.ChainEpoch
// DealIDs []abi.DealID
//}
type OnMinerSectorsTerminateParams = market0.OnMinerSectorsTerminateParams
// Terminate a set of deals in response to their containing sector being terminated.
// Slash provider collateral, refund client collateral, and refund partial unpaid escrow
// amount to client.
func (a Actor) OnMinerSectorsTerminate(rt Runtime, params *OnMinerSectorsTerminateParams) *abi.EmptyValue {
rt.ValidateImmediateCallerType(builtin.StorageMinerActorCodeID)
minerAddr := rt.Caller()
var st State
rt.StateTransaction(&st, func() {
msm, err := st.mutator(adt.AsStore(rt)).withDealStates(WritePermission).
withDealProposals(ReadOnlyPermission).build()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load deal state")
for _, dealID := range params.DealIDs {
deal, found, err := msm.dealProposals.Get(dealID)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to get deal proposal %v", dealID)
// The deal may have expired and been deleted before the sector is terminated.
// Log the dealID for the dealProposal and continue execution for other deals
if !found {
rt.Log(rtt.INFO, "couldn't find deal %d", dealID)
continue
}
builtin.RequireState(rt, deal.Provider == minerAddr, "caller %v is not the provider %v of deal %v",
minerAddr, deal.Provider, dealID)
// do not slash expired deals
if deal.EndEpoch <= params.Epoch {
rt.Log(rtt.INFO, "deal %d expired, not slashing", dealID)
continue
}
state, found, err := msm.dealStates.Get(dealID)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to get deal state %v", dealID)
if !found {
// A deal with a proposal but no state is not activated, but then it should not be
// part of a sector that is terminating.
rt.Abortf(exitcode.ErrIllegalArgument, "no state for deal %v", dealID)
}
// if a deal is already slashed, we don't need to do anything here.
if state.SlashEpoch != EpochUndefined {
rt.Log(rtt.INFO, "deal %d already slashed", dealID)
continue
}
// mark the deal for slashing here.
// actual releasing of locked funds for the client and slashing of provider collateral happens in CronTick.
state.SlashEpoch = params.Epoch
err = msm.dealStates.Set(dealID, state)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to set deal state %v", dealID)
}
err = msm.commitState()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to flush state")
})
return nil
}
func (a Actor) CronTick(rt Runtime, _ *abi.EmptyValue) *abi.EmptyValue {
rt.ValidateImmediateCallerIs(builtin.CronActorAddr)
amountSlashed := big.Zero()
var timedOutVerifiedDeals []*DealProposal
var st State
rt.StateTransaction(&st, func() {
updatesNeeded := make(map[abi.ChainEpoch][]abi.DealID)
msm, err := st.mutator(adt.AsStore(rt)).withDealStates(WritePermission).
withLockedTable(WritePermission).withEscrowTable(WritePermission).withDealsByEpoch(WritePermission).
withDealProposals(WritePermission).withPendingProposals(WritePermission).build()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load state")
for i := st.LastCron + 1; i <= rt.CurrEpoch(); i++ {
err = msm.dealsByEpoch.ForEach(i, func(dealID abi.DealID) error {
deal, err := getDealProposal(msm.dealProposals, dealID)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to get dealId %d", dealID)
dcid, err := deal.Cid()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to calculate CID for proposal %v", dealID)
state, found, err := msm.dealStates.Get(dealID)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to get deal state")
// deal has been published but not activated yet -> terminate it as it has timed out
if !found {
// Not yet appeared in proven sector; check for timeout.
builtin.RequireState(rt, rt.CurrEpoch() >= deal.StartEpoch, "deal %d processed before start epoch %d",
dealID, deal.StartEpoch)
slashed := msm.processDealInitTimedOut(rt, deal)
if !slashed.IsZero() {
amountSlashed = big.Add(amountSlashed, slashed)
}
if deal.VerifiedDeal {
timedOutVerifiedDeals = append(timedOutVerifiedDeals, deal)
}
// Delete the proposal (but not state, which doesn't exist).
err = msm.dealProposals.Delete(dealID)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to delete deal proposal %d", dealID)
err = msm.pendingDeals.Delete(abi.CidKey(dcid))
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to delete pending proposal %d (%v)", dealID, dcid)
return nil
}
// if this is the first cron tick for the deal, it should be in the pending state.
if state.LastUpdatedEpoch == EpochUndefined {
pdErr := msm.pendingDeals.Delete(abi.CidKey(dcid))
builtin.RequireNoErr(rt, pdErr, exitcode.ErrIllegalState, "failed to delete pending proposal %v", dcid)
}
slashAmount, nextEpoch, removeDeal := msm.updatePendingDealState(rt, state, deal, rt.CurrEpoch())
builtin.RequireState(rt, slashAmount.GreaterThanEqual(big.Zero()), "computed negative slash amount %v for deal %d", slashAmount, dealID)
if removeDeal {
builtin.RequireState(rt, nextEpoch == EpochUndefined, "removed deal %d should have no scheduled epoch (got %d)", dealID, nextEpoch)
amountSlashed = big.Add(amountSlashed, slashAmount)
// Delete proposal and state simultaneously.
err = msm.dealStates.Delete(dealID)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to delete deal state %d", dealID)
err = msm.dealProposals.Delete(dealID)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to delete deal proposal %d", dealID)
} else {
builtin.RequireState(rt, nextEpoch > rt.CurrEpoch(), "continuing deal %d next epoch %d should be in future", dealID, nextEpoch)
builtin.RequireState(rt, slashAmount.IsZero(), "continuing deal %d should not be slashed", dealID)
// Update deal's LastUpdatedEpoch in DealStates
state.LastUpdatedEpoch = rt.CurrEpoch()
err = msm.dealStates.Set(dealID, state)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to set deal state")
updatesNeeded[nextEpoch] = append(updatesNeeded[nextEpoch], dealID)
}
return nil
})
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to iterate deal ops")
err = msm.dealsByEpoch.RemoveAll(i)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to delete deal ops for epoch %v", i)
}
// Iterate changes in sorted order to ensure that loads/stores
// are deterministic. Otherwise, we could end up charging an
// inconsistent amount of gas.
changedEpochs := make([]abi.ChainEpoch, 0, len(updatesNeeded))
for epoch := range updatesNeeded { //nolint:nomaprange
changedEpochs = append(changedEpochs, epoch)
}
sort.Slice(changedEpochs, func(i, j int) bool { return changedEpochs[i] < changedEpochs[j] })
for _, epoch := range changedEpochs {
err = msm.dealsByEpoch.PutMany(epoch, updatesNeeded[epoch])
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to reinsert deal IDs for epoch %v", epoch)
}
st.LastCron = rt.CurrEpoch()
err = msm.commitState()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to flush state")
})
for _, d := range timedOutVerifiedDeals {
code := rt.Send(
builtin.VerifiedRegistryActorAddr,
builtin.MethodsVerifiedRegistry.RestoreBytes,
&verifreg.RestoreBytesParams{
Address: d.Client,
DealSize: big.NewIntUnsigned(uint64(d.PieceSize)),
},
abi.NewTokenAmount(0),
&builtin.Discard{},
)
if !code.IsSuccess() {
rt.Log(rtt.ERROR, "failed to send RestoreBytes call to the VerifReg actor for timed-out verified deal, client: %s, dealSize: %v, "+
"provider: %v, got code %v", d.Client, d.PieceSize, d.Provider, code)
}
}
if !amountSlashed.IsZero() {
e := rt.Send(builtin.BurntFundsActorAddr, builtin.MethodSend, nil, amountSlashed, &builtin.Discard{})
builtin.RequireSuccess(rt, e, "expected send to burnt funds actor to succeed")
}
return nil
}
func GenRandNextEpoch(startEpoch abi.ChainEpoch, dealID abi.DealID) abi.ChainEpoch {
offset := abi.ChainEpoch(uint64(dealID) % uint64(DealUpdatesInterval))
q := builtin.NewQuantSpec(DealUpdatesInterval, 0)
prevDay := q.QuantizeDown(startEpoch)
if prevDay+offset >= startEpoch {
return prevDay + offset
}
nextDay := q.QuantizeUp(startEpoch)
return nextDay + offset
}
//
// Exported functions
//
// Validates a collection of deal dealProposals for activation, and returns their combined weight,
// split into regular deal weight and verified deal weight.
func ValidateDealsForActivation(
st *State, store adt.Store, dealIDs []abi.DealID, minerAddr addr.Address, sectorExpiry, currEpoch abi.ChainEpoch,
) (big.Int, big.Int, uint64, error) {
proposals, err := AsDealProposalArray(store, st.Proposals)
if err != nil {
return big.Int{}, big.Int{}, 0, xerrors.Errorf("failed to load dealProposals: %w", err)
}
return validateAndComputeDealWeight(proposals, dealIDs, minerAddr, sectorExpiry, currEpoch)
}
////////////////////////////////////////////////////////////////////////////////
// Checks
////////////////////////////////////////////////////////////////////////////////
func validateAndComputeDealWeight(proposals *DealArray, dealIDs []abi.DealID, minerAddr addr.Address,
sectorExpiry abi.ChainEpoch, sectorActivation abi.ChainEpoch) (big.Int, big.Int, uint64, error) {
seenDealIDs := make(map[abi.DealID]struct{}, len(dealIDs))
totalDealSpace := uint64(0)
totalDealSpaceTime := big.Zero()
totalVerifiedSpaceTime := big.Zero()
for _, dealID := range dealIDs {
// Make sure we don't double-count deals.
if _, seen := seenDealIDs[dealID]; seen {
return big.Int{}, big.Int{}, 0, exitcode.ErrIllegalArgument.Wrapf("deal ID %d present multiple times", dealID)
}
seenDealIDs[dealID] = struct{}{}
proposal, found, err := proposals.Get(dealID)
if err != nil {
return big.Int{}, big.Int{}, 0, xerrors.Errorf("failed to load deal %d: %w", dealID, err)
}
if !found {
return big.Int{}, big.Int{}, 0, exitcode.ErrNotFound.Wrapf("no such deal %d", dealID)
}
if err = validateDealCanActivate(proposal, minerAddr, sectorExpiry, sectorActivation); err != nil {
return big.Int{}, big.Int{}, 0, xerrors.Errorf("cannot activate deal %d: %w", dealID, err)
}
// Compute deal weight
totalDealSpace += uint64(proposal.PieceSize)
dealSpaceTime := DealWeight(proposal)
if proposal.VerifiedDeal {
totalVerifiedSpaceTime = big.Add(totalVerifiedSpaceTime, dealSpaceTime)
} else {
totalDealSpaceTime = big.Add(totalDealSpaceTime, dealSpaceTime)
}
}
return totalDealSpaceTime, totalVerifiedSpaceTime, totalDealSpace, nil
}
func validateDealCanActivate(proposal *DealProposal, minerAddr addr.Address, sectorExpiration, sectorActivation abi.ChainEpoch) error {
if proposal.Provider != minerAddr {
return exitcode.ErrForbidden.Wrapf("proposal has provider %v, must be %v", proposal.Provider, minerAddr)
}
if sectorActivation > proposal.StartEpoch {
return exitcode.ErrIllegalArgument.Wrapf("proposal start epoch %d has already elapsed at %d", proposal.StartEpoch, sectorActivation)
}
if proposal.EndEpoch > sectorExpiration {
return exitcode.ErrIllegalArgument.Wrapf("proposal expiration %d exceeds sector expiration %d", proposal.EndEpoch, sectorExpiration)
}
return nil
}
func validateDeal(rt Runtime, deal ClientDealProposal, networkRawPower, networkQAPower, baselinePower abi.StoragePower) error {
if err := dealProposalIsInternallyValid(rt, deal); err != nil {
return xerrors.Errorf("Invalid deal proposal %w", err)
}
proposal := deal.Proposal
if proposal.Label.Length() > DealMaxLabelSize {
return xerrors.Errorf("deal label can be at most %d bytes, is %d", DealMaxLabelSize, proposal.Label.Length())
}
if err := proposal.PieceSize.Validate(); err != nil {
return xerrors.Errorf("proposal piece size is invalid: %w", err)
}
if !proposal.PieceCID.Defined() {
return xerrors.Errorf("proposal PieceCid undefined")
}
if proposal.PieceCID.Prefix() != PieceCIDPrefix {
return xerrors.Errorf("proposal PieceCID had wrong prefix")
}
if proposal.EndEpoch <= proposal.StartEpoch {
return xerrors.Errorf("proposal end before proposal start")
}
if rt.CurrEpoch() > proposal.StartEpoch {
return xerrors.Errorf("Deal start epoch has already elapsed")
}
minDuration, maxDuration := DealDurationBounds(proposal.PieceSize)
if proposal.Duration() < minDuration || proposal.Duration() > maxDuration {
return xerrors.Errorf("Deal duration out of bounds")
}
minPrice, maxPrice := DealPricePerEpochBounds(proposal.PieceSize, proposal.Duration())
if proposal.StoragePricePerEpoch.LessThan(minPrice) || proposal.StoragePricePerEpoch.GreaterThan(maxPrice) {
return xerrors.Errorf("Storage price out of bounds")
}
minProviderCollateral, maxProviderCollateral := DealProviderCollateralBounds(proposal.PieceSize, proposal.VerifiedDeal,
networkRawPower, networkQAPower, baselinePower, rt.TotalFilCircSupply())
if proposal.ProviderCollateral.LessThan(minProviderCollateral) || proposal.ProviderCollateral.GreaterThan(maxProviderCollateral) {
return xerrors.Errorf("Provider collateral out of bounds")
}
minClientCollateral, maxClientCollateral := DealClientCollateralBounds(proposal.PieceSize, proposal.Duration())
if proposal.ClientCollateral.LessThan(minClientCollateral) || proposal.ClientCollateral.GreaterThan(maxClientCollateral) {
return xerrors.Errorf("Client collateral out of bounds")
}
return nil
}
//
// Helpers
//
// Resolves a provider or client address to the canonical form against which a balance should be held, and
// the designated recipient address of withdrawals (which is the same, for simple account parties).
func escrowAddress(rt Runtime, address addr.Address) (nominal addr.Address, recipient addr.Address, approved []addr.Address) {
// Resolve the provided address to the canonical form against which the balance is held.
nominal, ok := rt.ResolveAddress(address)
if !ok {
rt.Abortf(exitcode.ErrIllegalArgument, "failed to resolve address %v", address)
}
codeID, ok := rt.GetActorCodeCID(nominal)
if !ok {
rt.Abortf(exitcode.ErrIllegalArgument, "no code for address %v", nominal)
}
if codeID.Equals(builtin.StorageMinerActorCodeID) {
// Storage miner actor entry; implied funds recipient is the associated owner address.
ownerAddr, workerAddr, _ := builtin.RequestMinerControlAddrs(rt, nominal)
return nominal, ownerAddr, []addr.Address{ownerAddr, workerAddr}
}
return nominal, nominal, []addr.Address{nominal}
}
func getDealProposal(proposals *DealArray, dealID abi.DealID) (*DealProposal, error) {
proposal, found, err := proposals.Get(dealID)
if err != nil {
return nil, xerrors.Errorf("failed to load proposal: %w", err)
}
if !found {
return nil, exitcode.ErrNotFound.Wrapf("no such deal %d", dealID)
}
return proposal, nil
}
// Requests the current epoch target block reward from the reward actor.
func requestCurrentBaselinePower(rt Runtime) abi.StoragePower {
var ret reward.ThisEpochRewardReturn
code := rt.Send(builtin.RewardActorAddr, builtin.MethodsReward.ThisEpochReward, nil, big.Zero(), &ret)
builtin.RequireSuccess(rt, code, "failed to check epoch baseline power")
return ret.ThisEpochBaselinePower
}
// Requests the current network total power and pledge from the power actor.
func requestCurrentNetworkPower(rt Runtime) (rawPower, qaPower abi.StoragePower) {
var pwr power.CurrentTotalPowerReturn
code := rt.Send(builtin.StoragePowerActorAddr, builtin.MethodsPower.CurrentTotalPower, nil, big.Zero(), &pwr)
builtin.RequireSuccess(rt, code, "failed to check current power")
return pwr.RawBytePower, pwr.QualityAdjPower
}
StorageMarketActorState
implementation
-
State
reliable
-
Theory Audit
done
-
Edit this section
-
section-systems.filecoin_markets.onchain_storage_market.storage_market_actor.storagemarketactorstate-implementation
-
State
reliable
-
Theory Audit
done
- Edit this section
-
section-systems.filecoin_markets.onchain_storage_market.storage_market_actor.storagemarketactorstate-implementation
Storage Market Actor Statuses
package market
import (
"bytes"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/go-state-types/exitcode"
"github.com/ipfs/go-cid"
xerrors "golang.org/x/xerrors"
"github.com/filecoin-project/specs-actors/v8/actors/builtin"
"github.com/filecoin-project/specs-actors/v8/actors/util/adt"
)
const EpochUndefined = abi.ChainEpoch(-1)
// BalanceLockingReason is the reason behind locking an amount.
type BalanceLockingReason int
const (
ClientCollateral BalanceLockingReason = iota
ClientStorageFee
ProviderCollateral
)
// Bitwidth of AMTs determined empirically from mutation patterns and projections of mainnet data.
const ProposalsAmtBitwidth = 5
const StatesAmtBitwidth = 6
type State struct {
// Proposals are deals that have been proposed and not yet cleaned up after expiry or termination.
Proposals cid.Cid // AMT[DealID]DealProposal
// States contains state for deals that have been activated and not yet cleaned up after expiry or termination.
// After expiration, the state exists until the proposal is cleaned up too.
// Invariant: keys(States) ⊆ keys(Proposals).
States cid.Cid // AMT[DealID]DealState
// PendingProposals tracks dealProposals that have not yet reached their deal start date.
// We track them here to ensure that miners can't publish the same deal proposal twice
PendingProposals cid.Cid // Set[DealCid]
// Total amount held in escrow, indexed by actor address (including both locked and unlocked amounts).
EscrowTable cid.Cid // BalanceTable
// Amount locked, indexed by actor address.
// Note: the amounts in this table do not affect the overall amount in escrow:
// only the _portion_ of the total escrow amount that is locked.
LockedTable cid.Cid // BalanceTable
NextID abi.DealID
// Metadata cached for efficient iteration over deals.
DealOpsByEpoch cid.Cid // SetMultimap, HAMT[epoch]Set
LastCron abi.ChainEpoch
// Total Client Collateral that is locked -> unlocked when deal is terminated
TotalClientLockedCollateral abi.TokenAmount
// Total Provider Collateral that is locked -> unlocked when deal is terminated
TotalProviderLockedCollateral abi.TokenAmount
// Total storage fee that is locked in escrow -> unlocked when payments are made
TotalClientStorageFee abi.TokenAmount
}
func ConstructState(store adt.Store) (*State, error) {
emptyProposalsArrayCid, err := adt.StoreEmptyArray(store, ProposalsAmtBitwidth)
if err != nil {
return nil, xerrors.Errorf("failed to create empty array: %w", err)
}
emptyStatesArrayCid, err := adt.StoreEmptyArray(store, StatesAmtBitwidth)
if err != nil {
return nil, xerrors.Errorf("failed to create empty states array: %w", err)
}
emptyPendingProposalsMapCid, err := adt.StoreEmptyMap(store, builtin.DefaultHamtBitwidth)
if err != nil {
return nil, xerrors.Errorf("failed to create empty map: %w", err)
}
emptyDealOpsHamtCid, err := StoreEmptySetMultimap(store, builtin.DefaultHamtBitwidth)
if err != nil {
return nil, xerrors.Errorf("failed to create empty multiset: %w", err)
}
emptyBalanceTableCid, err := adt.StoreEmptyMap(store, adt.BalanceTableBitwidth)
if err != nil {
return nil, xerrors.Errorf("failed to create empty balance table: %w", err)
}
return &State{
Proposals: emptyProposalsArrayCid,
States: emptyStatesArrayCid,
PendingProposals: emptyPendingProposalsMapCid,
EscrowTable: emptyBalanceTableCid,
LockedTable: emptyBalanceTableCid,
NextID: abi.DealID(0),
DealOpsByEpoch: emptyDealOpsHamtCid,
LastCron: abi.ChainEpoch(-1),
TotalClientLockedCollateral: abi.NewTokenAmount(0),
TotalProviderLockedCollateral: abi.NewTokenAmount(0),
TotalClientStorageFee: abi.NewTokenAmount(0),
}, nil
}
////////////////////////////////////////////////////////////////////////////////
// Deal state operations
////////////////////////////////////////////////////////////////////////////////
func (m *marketStateMutation) updatePendingDealState(rt Runtime, state *DealState, deal *DealProposal, epoch abi.ChainEpoch) (amountSlashed abi.TokenAmount, nextEpoch abi.ChainEpoch, removeDeal bool) {
amountSlashed = abi.NewTokenAmount(0)
everUpdated := state.LastUpdatedEpoch != EpochUndefined
everSlashed := state.SlashEpoch != EpochUndefined
builtin.RequireState(rt, !everUpdated || (state.LastUpdatedEpoch <= epoch), "deal updated at future epoch %d", state.LastUpdatedEpoch)
// This would be the case that the first callback somehow triggers before it is scheduled to
// This is expected not to be able to happen
if deal.StartEpoch > epoch {
return amountSlashed, EpochUndefined, false
}
paymentEndEpoch := deal.EndEpoch
if everSlashed {
builtin.RequireState(rt, epoch >= state.SlashEpoch, "current epoch less than deal slash epoch %d", state.SlashEpoch)
builtin.RequireState(rt, state.SlashEpoch <= deal.EndEpoch, "deal slash epoch %d after deal end %d", state.SlashEpoch, deal.EndEpoch)
paymentEndEpoch = state.SlashEpoch
} else if epoch < paymentEndEpoch {
paymentEndEpoch = epoch
}
paymentStartEpoch := deal.StartEpoch
if everUpdated && state.LastUpdatedEpoch > paymentStartEpoch {
paymentStartEpoch = state.LastUpdatedEpoch
}
numEpochsElapsed := paymentEndEpoch - paymentStartEpoch
{
// Process deal payment for the elapsed epochs.
totalPayment := big.Mul(big.NewInt(int64(numEpochsElapsed)), deal.StoragePricePerEpoch)
// the transfer amount can be less than or equal to zero if a deal is slashed before or at the deal's start epoch.
if totalPayment.GreaterThan(big.Zero()) {
err := m.transferBalance(deal.Client, deal.Provider, totalPayment)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to transfer %v from %v to %v",
totalPayment, deal.Client, deal.Provider)
}
}
if everSlashed {
// unlock client collateral and locked storage fee
paymentRemaining, err := dealGetPaymentRemaining(deal, state.SlashEpoch)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to compute remaining payment")
// unlock remaining storage fee
err = m.unlockBalance(deal.Client, paymentRemaining, ClientStorageFee)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to unlock remaining client storage fee")
// unlock client collateral
err = m.unlockBalance(deal.Client, deal.ClientCollateral, ClientCollateral)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to unlock client collateral")
// slash provider collateral
amountSlashed = deal.ProviderCollateral
err = m.slashBalance(deal.Provider, amountSlashed, ProviderCollateral)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "slashing balance")
return amountSlashed, EpochUndefined, true
}
if epoch >= deal.EndEpoch {
m.processDealExpired(rt, deal, state)
return amountSlashed, EpochUndefined, true
}
// We're explicitly not inspecting the end epoch and may process a deal's expiration late, in order to prevent an outsider
// from loading a cron tick by activating too many deals with the same end epoch.
nextEpoch = epoch + DealUpdatesInterval
return amountSlashed, nextEpoch, false
}
// Deal start deadline elapsed without appearing in a proven sector.
// Slash a portion of provider's collateral, and unlock remaining collaterals
// for both provider and client.
func (m *marketStateMutation) processDealInitTimedOut(rt Runtime, deal *DealProposal) abi.TokenAmount {
if err := m.unlockBalance(deal.Client, deal.TotalStorageFee(), ClientStorageFee); err != nil {
rt.Abortf(exitcode.ErrIllegalState, "failure unlocking client storage fee: %s", err)
}
if err := m.unlockBalance(deal.Client, deal.ClientCollateral, ClientCollateral); err != nil {
rt.Abortf(exitcode.ErrIllegalState, "failure unlocking client collateral: %s", err)
}
amountSlashed := CollateralPenaltyForDealActivationMissed(deal.ProviderCollateral)
amountRemaining := big.Sub(deal.ProviderBalanceRequirement(), amountSlashed)
if err := m.slashBalance(deal.Provider, amountSlashed, ProviderCollateral); err != nil {
rt.Abortf(exitcode.ErrIllegalState, "failed to slash balance: %s", err)
}
if err := m.unlockBalance(deal.Provider, amountRemaining, ProviderCollateral); err != nil {
rt.Abortf(exitcode.ErrIllegalState, "failed to unlock deal provider balance: %s", err)
}
return amountSlashed
}
// Normal expiration. Unlock collaterals for both provider and client.
func (m *marketStateMutation) processDealExpired(rt Runtime, deal *DealProposal, state *DealState) {
builtin.RequireState(rt, state.SectorStartEpoch != EpochUndefined, "sector start epoch undefined")
// Note: payment has already been completed at this point (_rtProcessDealPaymentEpochsElapsed)
err := m.unlockBalance(deal.Provider, deal.ProviderCollateral, ProviderCollateral)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed unlocking deal provider balance")
err = m.unlockBalance(deal.Client, deal.ClientCollateral, ClientCollateral)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed unlocking deal client balance")
}
func (m *marketStateMutation) generateStorageDealID() abi.DealID {
ret := m.nextDealId
m.nextDealId = m.nextDealId + abi.DealID(1)
return ret
}
////////////////////////////////////////////////////////////////////////////////
// State utility functions
////////////////////////////////////////////////////////////////////////////////
func dealProposalIsInternallyValid(rt Runtime, proposal ClientDealProposal) error {
// Note: we do not verify the provider signature here, since this is implicit in the
// authenticity of the on-chain message publishing the deal.
buf := bytes.Buffer{}
err := proposal.Proposal.MarshalCBOR(&buf)
if err != nil {
return xerrors.Errorf("proposal signature verification failed to marshal proposal: %w", err)
}
err = rt.VerifySignature(proposal.ClientSignature, proposal.Proposal.Client, buf.Bytes())
if err != nil {
return xerrors.Errorf("signature proposal invalid: %w", err)
}
return nil
}
func dealGetPaymentRemaining(deal *DealProposal, slashEpoch abi.ChainEpoch) (abi.TokenAmount, error) {
if slashEpoch > deal.EndEpoch {
return big.Zero(), xerrors.Errorf("deal slash epoch %d after end epoch %d", slashEpoch, deal.EndEpoch)
}
// Payments are always for start -> end epoch irrespective of when the deal is slashed.
if slashEpoch < deal.StartEpoch {
slashEpoch = deal.StartEpoch
}
durationRemaining := deal.EndEpoch - slashEpoch
if durationRemaining < 0 {
return big.Zero(), xerrors.Errorf("deal remaining duration negative: %d", durationRemaining)
}
return big.Mul(big.NewInt(int64(durationRemaining)), deal.StoragePricePerEpoch), nil
}
// MarketStateMutationPermission is the mutation permission on a state field
type MarketStateMutationPermission int
const (
// Invalid means NO permission
Invalid MarketStateMutationPermission = iota
// ReadOnlyPermission allows reading but not mutating the field
ReadOnlyPermission
// WritePermission allows mutating the field
WritePermission
)
type marketStateMutation struct {
st *State
store adt.Store
proposalPermit MarketStateMutationPermission
dealProposals *DealArray
statePermit MarketStateMutationPermission
dealStates *DealMetaArray
escrowPermit MarketStateMutationPermission
escrowTable *adt.BalanceTable
pendingPermit MarketStateMutationPermission
pendingDeals *adt.Set
dpePermit MarketStateMutationPermission
dealsByEpoch *SetMultimap
lockedPermit MarketStateMutationPermission
lockedTable *adt.BalanceTable
totalClientLockedCollateral abi.TokenAmount
totalProviderLockedCollateral abi.TokenAmount
totalClientStorageFee abi.TokenAmount
nextDealId abi.DealID
}
func (s *State) mutator(store adt.Store) *marketStateMutation {
return &marketStateMutation{st: s, store: store}
}
func (m *marketStateMutation) build() (*marketStateMutation, error) {
if m.proposalPermit != Invalid {
proposals, err := AsDealProposalArray(m.store, m.st.Proposals)
if err != nil {
return nil, xerrors.Errorf("failed to load deal proposals: %w", err)
}
m.dealProposals = proposals
}
if m.statePermit != Invalid {
states, err := AsDealStateArray(m.store, m.st.States)
if err != nil {
return nil, xerrors.Errorf("failed to load deal state: %w", err)
}
m.dealStates = states
}
if m.lockedPermit != Invalid {
lt, err := adt.AsBalanceTable(m.store, m.st.LockedTable)
if err != nil {
return nil, xerrors.Errorf("failed to load locked table: %w", err)
}
m.lockedTable = lt
m.totalClientLockedCollateral = m.st.TotalClientLockedCollateral.Copy()
m.totalClientStorageFee = m.st.TotalClientStorageFee.Copy()
m.totalProviderLockedCollateral = m.st.TotalProviderLockedCollateral.Copy()
}
if m.escrowPermit != Invalid {
et, err := adt.AsBalanceTable(m.store, m.st.EscrowTable)
if err != nil {
return nil, xerrors.Errorf("failed to load escrow table: %w", err)
}
m.escrowTable = et
}
if m.pendingPermit != Invalid {
pending, err := adt.AsSet(m.store, m.st.PendingProposals, builtin.DefaultHamtBitwidth)
if err != nil {
return nil, xerrors.Errorf("failed to load pending proposals: %w", err)
}
m.pendingDeals = pending
}
if m.dpePermit != Invalid {
dbe, err := AsSetMultimap(m.store, m.st.DealOpsByEpoch, builtin.DefaultHamtBitwidth, builtin.DefaultHamtBitwidth)
if err != nil {
return nil, xerrors.Errorf("failed to load deals by epoch: %w", err)
}
m.dealsByEpoch = dbe
}
m.nextDealId = m.st.NextID
return m, nil
}
func (m *marketStateMutation) withDealProposals(permit MarketStateMutationPermission) *marketStateMutation {
m.proposalPermit = permit
return m
}
func (m *marketStateMutation) withDealStates(permit MarketStateMutationPermission) *marketStateMutation {
m.statePermit = permit
return m
}
func (m *marketStateMutation) withEscrowTable(permit MarketStateMutationPermission) *marketStateMutation {
m.escrowPermit = permit
return m
}
func (m *marketStateMutation) withLockedTable(permit MarketStateMutationPermission) *marketStateMutation {
m.lockedPermit = permit
return m
}
func (m *marketStateMutation) withPendingProposals(permit MarketStateMutationPermission) *marketStateMutation {
m.pendingPermit = permit
return m
}
func (m *marketStateMutation) withDealsByEpoch(permit MarketStateMutationPermission) *marketStateMutation {
m.dpePermit = permit
return m
}
func (m *marketStateMutation) commitState() error {
var err error
if m.proposalPermit == WritePermission {
if m.st.Proposals, err = m.dealProposals.Root(); err != nil {
return xerrors.Errorf("failed to flush deal dealProposals: %w", err)
}
}
if m.statePermit == WritePermission {
if m.st.States, err = m.dealStates.Root(); err != nil {
return xerrors.Errorf("failed to flush deal states: %w", err)
}
}
if m.lockedPermit == WritePermission {
if m.st.LockedTable, err = m.lockedTable.Root(); err != nil {
return xerrors.Errorf("failed to flush locked table: %w", err)
}
m.st.TotalClientLockedCollateral = m.totalClientLockedCollateral.Copy()
m.st.TotalProviderLockedCollateral = m.totalProviderLockedCollateral.Copy()
m.st.TotalClientStorageFee = m.totalClientStorageFee.Copy()
}
if m.escrowPermit == WritePermission {
if m.st.EscrowTable, err = m.escrowTable.Root(); err != nil {
return xerrors.Errorf("failed to flush escrow table: %w", err)
}
}
if m.pendingPermit == WritePermission {
if m.st.PendingProposals, err = m.pendingDeals.Root(); err != nil {
return xerrors.Errorf("failed to flush pending deals: %w", err)
}
}
if m.dpePermit == WritePermission {
if m.st.DealOpsByEpoch, err = m.dealsByEpoch.Root(); err != nil {
return xerrors.Errorf("failed to flush deals by epoch: %w", err)
}
}
m.st.NextID = m.nextDealId
return nil
}
Storage Market Actor Balance states and mutations
package market
import (
addr "github.com/filecoin-project/go-address"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/go-state-types/exitcode"
"golang.org/x/xerrors"
)
func (m *marketStateMutation) lockClientAndProviderBalances(proposal *DealProposal) error {
if err := m.maybeLockBalance(proposal.Client, proposal.ClientBalanceRequirement()); err != nil {
return xerrors.Errorf("failed to lock client funds: %w", err)
}
if err := m.maybeLockBalance(proposal.Provider, proposal.ProviderCollateral); err != nil {
return xerrors.Errorf("failed to lock provider funds: %w", err)
}
m.totalClientLockedCollateral = big.Add(m.totalClientLockedCollateral, proposal.ClientCollateral)
m.totalClientStorageFee = big.Add(m.totalClientStorageFee, proposal.TotalStorageFee())
m.totalProviderLockedCollateral = big.Add(m.totalProviderLockedCollateral, proposal.ProviderCollateral)
return nil
}
func (m *marketStateMutation) unlockBalance(addr addr.Address, amount abi.TokenAmount, lockReason BalanceLockingReason) error {
if amount.LessThan(big.Zero()) {
return xerrors.Errorf("unlock negative amount %v", amount)
}
err := m.lockedTable.MustSubtract(addr, amount)
if err != nil {
return xerrors.Errorf("subtracting from locked balance: %w", err)
}
switch lockReason {
case ClientCollateral:
m.totalClientLockedCollateral = big.Sub(m.totalClientLockedCollateral, amount)
case ClientStorageFee:
m.totalClientStorageFee = big.Sub(m.totalClientStorageFee, amount)
case ProviderCollateral:
m.totalProviderLockedCollateral = big.Sub(m.totalProviderLockedCollateral, amount)
}
return nil
}
// move funds from locked in client to available in provider
func (m *marketStateMutation) transferBalance(fromAddr addr.Address, toAddr addr.Address, amount abi.TokenAmount) error {
if amount.LessThan(big.Zero()) {
return xerrors.Errorf("transfer negative amount %v", amount)
}
if err := m.escrowTable.MustSubtract(fromAddr, amount); err != nil {
return xerrors.Errorf("subtract from escrow: %w", err)
}
if err := m.unlockBalance(fromAddr, amount, ClientStorageFee); err != nil {
return xerrors.Errorf("subtract from locked: %w", err)
}
if err := m.escrowTable.Add(toAddr, amount); err != nil {
return xerrors.Errorf("add to escrow: %w", err)
}
return nil
}
func (m *marketStateMutation) slashBalance(addr addr.Address, amount abi.TokenAmount, reason BalanceLockingReason) error {
if amount.LessThan(big.Zero()) {
return xerrors.Errorf("negative amount to slash: %v", amount)
}
if err := m.escrowTable.MustSubtract(addr, amount); err != nil {
return xerrors.Errorf("subtract from escrow: %v", err)
}
return m.unlockBalance(addr, amount, reason)
}
func (m *marketStateMutation) maybeLockBalance(addr addr.Address, amount abi.TokenAmount) error {
if amount.LessThan(big.Zero()) {
return xerrors.Errorf("cannot lock negative amount %v", amount)
}
prevLocked, err := m.lockedTable.Get(addr)
if err != nil {
return xerrors.Errorf("failed to get locked balance: %w", err)
}
escrowBalance, err := m.escrowTable.Get(addr)
if err != nil {
return xerrors.Errorf("failed to get escrow balance: %w", err)
}
if big.Add(prevLocked, amount).GreaterThan(escrowBalance) {
return exitcode.ErrInsufficientFunds.Wrapf("insufficient balance for addr %s: escrow balance %s < locked %s + required %s",
addr, escrowBalance, prevLocked, amount)
}
if err := m.lockedTable.Add(addr, amount); err != nil {
return xerrors.Errorf("failed to add locked balance: %w", err)
}
return nil
}
// Return true when the funds in escrow for the input address can cover an additional lockup of amountToLock
func (m *marketStateMutation) balanceCovered(addr addr.Address, amountToLock abi.TokenAmount) (bool, error) {
prevLocked, err := m.lockedTable.Get(addr)
if err != nil {
return false, xerrors.Errorf("failed to get locked balance: %w", err)
}
escrowBalance, err := m.escrowTable.Get(addr)
if err != nil {
return false, xerrors.Errorf("failed to get escrow balance: %w", err)
}
return big.Add(prevLocked, amountToLock).LessThanEqual(escrowBalance), nil
}
Storage Deal Collateral
-
State
reliable
-
Theory Audit
done
-
Edit this section
-
section-systems.filecoin_markets.onchain_storage_market.storage_market_actor.storage-deal-collateral
-
State
reliable
-
Theory Audit
done
- Edit this section
-
section-systems.filecoin_markets.onchain_storage_market.storage_market_actor.storage-deal-collateral
Apart from
Initial Pledge Collateral and Block Reward Collateral discussed earlier, the third form of collateral is provided by the storage provider to collateralize deals, is called Storage Deal Collateral and is held in the StorageMarketActor
.
There is a minimum amount of collateral required by the protocol to provide a minimum level of guarantee, which is agreed upon by the storage provider and client off-chain. However, miners can offer a higher deal collateral to imply a higher level of service and reliability to potential clients. Given the increased stakes, clients may associate additional provider deal collateral beyond the minimum with an increased likelihood that their data will be reliably stored.
Provider deal collateral is only slashed when a sector is terminated before the deal expires. If a miner enters Temporary Fault for a sector and later recovers from it, no deal collateral will be slashed.
This collateral is returned to the storage provider when all deals in the sector successfully conclude. Upon graceful deal expiration, storage providers must wait for finality number of epochs (as defined in
Finality) before being able to withdraw their StorageDealCollateral
from the StorageMarketActor
.
$$MinimumProviderDealCollateral = 1\% \times FILCirculatingSupply \times \frac{DealRawByte}{max(NetworkBaseline, NetworkRawBytePower)}$$