Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions fundmanager/fundmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fundmanager

import (
"context"
"errors"
"fmt"

"github.com/filecoin-project/boost/db"
Expand Down Expand Up @@ -66,10 +67,12 @@ type TagFundsResp struct {
AvailablePublishMessage abi.TokenAmount
}

var ErrInsufficientFunds = errors.New("insufficient funds")

// TagFunds tags funds for deal collateral and for the publish storage
// deals message, so those funds cannot be used for other deals.
// It fails if there are not enough funds available in the respective
// wallets to cover either of these operations.
// It returns ErrInsufficientFunds if there are not enough funds available
// in the respective wallets to cover either of these operations.
func (m *FundManager) TagFunds(ctx context.Context, dealUuid uuid.UUID, proposal market.DealProposal) (*TagFundsResp, error) {
marketBal, err := m.BalanceMarket(ctx)
if err != nil {
Expand All @@ -91,17 +94,19 @@ func (m *FundManager) TagFunds(ctx context.Context, dealUuid uuid.UUID, proposal
dealCollateral := proposal.ProviderBalanceRequirement()
availForDealCollat := big.Sub(marketBal.Available, tagged.Collateral)
if availForDealCollat.LessThan(dealCollateral) {
return nil, fmt.Errorf("available funds %d is less than collateral needed for deal %d: "+
err := fmt.Errorf("%w: available funds %d is less than collateral needed for deal %d: "+
"available = funds in escrow %d - amount reserved for other deals %d",
availForDealCollat, dealCollateral, marketBal.Available, tagged.Collateral)
ErrInsufficientFunds, availForDealCollat, dealCollateral, marketBal.Available, tagged.Collateral)
return nil, err
}

// Check that the provider has enough funds to send a PublishStorageDeals message
availForPubMsg := big.Sub(pubMsgBal, tagged.PubMsg)
if availForPubMsg.LessThan(m.cfg.PubMsgBalMin) {
return nil, fmt.Errorf("available funds %d is less than needed for publish deals message %d: "+
err := fmt.Errorf("%w: available funds %d is less than needed for publish deals message %d: "+
"available = funds in publish deals wallet %d - amount reserved for other deals %d",
availForPubMsg, m.cfg.PubMsgBalMin, pubMsgBal, tagged.PubMsg)
ErrInsufficientFunds, availForPubMsg, m.cfg.PubMsgBalMin, pubMsgBal, tagged.PubMsg)
return nil, err
}

// Provider has enough funds to make deal, so persist tagged funds
Expand Down
2 changes: 1 addition & 1 deletion itests/dummydeal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func TestDummydeal(t *testing.T) {
failingDealUuid := uuid.New()
res2, err2 := f.MakeDummyDeal(failingDealUuid, failingCarFilepath, failingRootCid, server.URL+"/"+filepath.Base(failingCarFilepath), false)
require.NoError(t, err2)
require.Equal(t, "cannot accept piece of size 2254421, on top of already allocated 2254421 bytes, because it would exceed max staging area size 4000000", res2.Reason)
require.Contains(t, res2.Reason, "no space left", res2.Reason)
log.Debugw("got response from MarketDummyDeal for failing deal", "res2", spew.Sdump(res2))

// Wait for the first deal to be added to a sector and cleaned up so space is made
Expand Down
15 changes: 11 additions & 4 deletions storagemanager/storagemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package storagemanager
import (
"context"
"database/sql"
"errors"
"fmt"
"path/filepath"

Expand Down Expand Up @@ -51,7 +52,11 @@ func (m *StorageManager) Free(ctx context.Context) (uint64, error) {
return m.cfg.MaxStagingDealsBytes - tagged, nil
}

// Tag
// ErrNoSpaceLeft indicates that there is insufficient storage to accept a deal
var ErrNoSpaceLeft = errors.New("no space left")

// Tags storage space for the deal.
// If there is not enough space left, returns ErrNoSpaceLeft.
func (m *StorageManager) Tag(ctx context.Context, dealUuid uuid.UUID, size uint64) error {
// Get the total tagged storage, so that we know how much is available.
log.Debugw("tagging", "id", dealUuid, "size", size, "maxbytes", m.cfg.MaxStagingDealsBytes)
Expand All @@ -62,12 +67,14 @@ func (m *StorageManager) Tag(ctx context.Context, dealUuid uuid.UUID, size uint6
}

if m.cfg.MaxStagingDealsBytes != 0 {
if tagged+uint64(size) >= m.cfg.MaxStagingDealsBytes {
return fmt.Errorf("cannot accept piece of size %d, on top of already allocated %d bytes, because it would exceed max staging area size %d", uint64(size), uint64(tagged), m.cfg.MaxStagingDealsBytes)
if tagged+size >= m.cfg.MaxStagingDealsBytes {
err := fmt.Errorf("%w: cannot accept piece of size %d, on top of already allocated %d bytes, because it would exceed max staging area size %d",
ErrNoSpaceLeft, size, tagged, m.cfg.MaxStagingDealsBytes)
return err
}
}

err = m.persistTagged(ctx, dealUuid, uint64(size))
err = m.persistTagged(ctx, dealUuid, size)
if err != nil {
return fmt.Errorf("saving total tagged storage: %w", err)
}
Expand Down
118 changes: 82 additions & 36 deletions storagemarket/deal_acceptance.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,66 +20,100 @@ import (

const DealMaxLabelSize = 256

// ValidateDealProposal validates a proposed deal against the provider criteria
func (p *Provider) validateDealProposal(deal types.ProviderDealState) error {
type validationError struct {
error
// The reason sent to the client for why validation failed
reason string
}

// ValidateDealProposal validates a proposed deal against the provider criteria.
// It returns a validationError. If a nicer error message should be sent to the
// client, the reason string will be set to that nicer error message.
func (p *Provider) validateDealProposal(deal types.ProviderDealState) *validationError {
head, err := p.fullnodeApi.ChainHead(p.ctx)
if err != nil {
return fmt.Errorf("node error getting most recent state id: %w", err)
return &validationError{
reason: "server error: getting chain head",
error: fmt.Errorf("node error getting most recent state id: %w", err),
}
}

tok := head.Key().Bytes()
curEpoch := head.Height()

if err := p.validateSignature(tok, deal); err != nil {
return fmt.Errorf("validateSignature failed: %w", err)
// Check that the proposal piece cid is defined before attempting signature
// validation - if it's not defined, it won't be possible to marshall the
// deal proposal to check the signature
proposal := deal.ClientDealProposal.Proposal
if !proposal.PieceCID.Defined() {
return &validationError{error: fmt.Errorf("proposal PieceCID undefined")}
}

if ok, err := p.validateSignature(tok, deal); err != nil || !ok {
if err != nil {
return &validationError{
reason: "server error: validating signature",
error: fmt.Errorf("validateSignature failed: %w", err),
}
}
return &validationError{
reason: "invalid signature",
error: fmt.Errorf("invalid signature"),
}
}

// validate deal proposal
proposal := deal.ClientDealProposal.Proposal
if proposal.Provider != p.Address {
return fmt.Errorf("incorrect provider for deal; proposal.Provider: %s; provider.Address: %s", proposal.Provider, p.Address)
err := fmt.Errorf("incorrect provider for deal; proposal.Provider: %s; provider.Address: %s", proposal.Provider, p.Address)
return &validationError{error: err}
}

if len(proposal.Label) > DealMaxLabelSize {
return fmt.Errorf("deal label can be at most %d bytes, is %d", DealMaxLabelSize, len(proposal.Label))
err := fmt.Errorf("deal label can be at most %d bytes, is %d", DealMaxLabelSize, len(proposal.Label))
return &validationError{error: err}
}

if err := proposal.PieceSize.Validate(); err != nil {
return fmt.Errorf("proposal piece size is invalid: %w", err)
}

if !proposal.PieceCID.Defined() {
return fmt.Errorf("proposal PieceCID undefined")
err := fmt.Errorf("proposal piece size is invalid: %w", err)
return &validationError{error: err}
}

if proposal.PieceCID.Prefix() != market.PieceCIDPrefix {
return fmt.Errorf("proposal PieceCID had wrong prefix")
err := fmt.Errorf("proposal PieceCID had wrong prefix")
return &validationError{error: err}
}

if proposal.EndEpoch <= proposal.StartEpoch {
return fmt.Errorf("proposal end before proposal start")
err := fmt.Errorf("proposal end %d before proposal start %d", proposal.EndEpoch, proposal.StartEpoch)
return &validationError{error: err}
}

if curEpoch > proposal.StartEpoch {
return fmt.Errorf("deal start epoch has already elapsed")
err := fmt.Errorf("deal start epoch %d has already elapsed (current epoch: %d)", proposal.StartEpoch, curEpoch)
return &validationError{error: err}
}

// Check that the delta between the start and end epochs (the deal
// duration) is within acceptable bounds
minDuration, maxDuration := market2.DealDurationBounds(proposal.PieceSize)
if proposal.Duration() < minDuration || proposal.Duration() > maxDuration {
return fmt.Errorf("deal duration out of bounds (min, max, provided): %d, %d, %d", minDuration, maxDuration, proposal.Duration())
err := fmt.Errorf("deal duration out of bounds (min, max, provided): %d, %d, %d", minDuration, maxDuration, proposal.Duration())
return &validationError{error: err}
}

// Check that the proposed end epoch isn't too far beyond the current epoch
maxEndEpoch := curEpoch + miner.MaxSectorExpirationExtension
if proposal.EndEpoch > maxEndEpoch {
return fmt.Errorf("invalid deal end epoch %d: cannot be more than %d past current epoch %d", proposal.EndEpoch, miner.MaxSectorExpirationExtension, curEpoch)
err := fmt.Errorf("invalid deal end epoch %d: cannot be more than %d past current epoch %d", proposal.EndEpoch, miner.MaxSectorExpirationExtension, curEpoch)
return &validationError{error: err}
}

bounds, err := p.fullnodeApi.StateDealProviderCollateralBounds(p.ctx, proposal.PieceSize, proposal.VerifiedDeal, ctypes.EmptyTSK)
if err != nil {
return fmt.Errorf("node error getting collateral bounds: %w", err)
return &validationError{
reason: "server error: getting collateral bounds",
error: fmt.Errorf("node error getting collateral bounds: %w", err),
}
}

// The maximum amount of collateral that the provider will put into escrow
Expand All @@ -90,50 +124,66 @@ func (p *Provider) validateDealProposal(deal types.ProviderDealState) error {
pcMax := max

if proposal.ProviderCollateral.LessThan(pcMin) {
return fmt.Errorf("proposed provider collateral below minimum: %s < %s", proposal.ProviderCollateral, pcMin)
err := fmt.Errorf("proposed provider collateral %s below minimum %s", proposal.ProviderCollateral, pcMin)
return &validationError{error: err}
}

if proposal.ProviderCollateral.GreaterThan(pcMax) {
return fmt.Errorf("proposed provider collateral above maximum: %s > %s", proposal.ProviderCollateral, pcMax)
err := fmt.Errorf("proposed provider collateral %s above maximum %s", proposal.ProviderCollateral, pcMax)
return &validationError{error: err}
}

if err := p.validateAsk(deal); err != nil {
return fmt.Errorf("validateAsk failed: %w", err)
return &validationError{error: err}
}

tsk, err := ctypes.TipSetKeyFromBytes(tok)
if err != nil {
return err
return &validationError{
reason: "server error: tip set key from bytes",
error: err,
}
}

bal, err := p.fullnodeApi.StateMarketBalance(p.ctx, proposal.Client, tsk)
if err != nil {
return fmt.Errorf("node error getting client market balance failed: %w", err)
return &validationError{
reason: "server error: getting market balance",
error: fmt.Errorf("node error getting client market balance failed: %w", err),
}
}

clientMarketBalance := utils.ToSharedBalance(bal)

// This doesn't guarantee that the client won't withdraw / lock those funds
// but it's a decent first filter
if clientMarketBalance.Available.LessThan(proposal.ClientBalanceRequirement()) {
return fmt.Errorf("clientMarketBalance.Available too small: %d < %d", clientMarketBalance.Available, proposal.ClientBalanceRequirement())
err := fmt.Errorf("client available funds in escrow %d not enough to meet storage cost for deal %d", clientMarketBalance.Available, proposal.ClientBalanceRequirement())
return &validationError{error: err}
}

// Verified deal checks
if proposal.VerifiedDeal {
// Get data cap
dataCap, err := p.fullnodeApi.StateVerifiedClientStatus(p.ctx, proposal.Client, tsk)
if err != nil {
return fmt.Errorf("node error fetching verified data cap: %w", err)
return &validationError{
reason: "server error: getting verified datacap",
error: fmt.Errorf("node error fetching verified data cap: %w", err),
}
}

if dataCap == nil {
return errors.New("node error fetching verified data cap: data cap missing -- client not verified")
return &validationError{
reason: "client is not a verified client",
error: errors.New("node error fetching verified data cap: data cap missing -- client not verified"),
}
}

pieceSize := big.NewIntUnsigned(uint64(proposal.PieceSize))
if dataCap.LessThan(pieceSize) {
return errors.New("verified deal DataCap too small for proposed piece size")
err := fmt.Errorf("verified deal DataCap %d too small for proposed piece size %d", dataCap, pieceSize)
return &validationError{error: err}
}
}

Expand Down Expand Up @@ -164,19 +214,15 @@ func (p *Provider) validateAsk(deal types.ProviderDealState) error {
return nil
}

func (p *Provider) validateSignature(tok shared.TipSetToken, deal types.ProviderDealState) error {
func (p *Provider) validateSignature(tok shared.TipSetToken, deal types.ProviderDealState) (bool, error) {
b, err := cborutil.Dump(&deal.ClientDealProposal.Proposal)
if err != nil {
return fmt.Errorf("failed to serialize client deal proposal: %w", err)
return false, fmt.Errorf("failed to serialize client deal proposal: %w", err)
}

verified, err := p.sigVerifier.VerifySignature(p.ctx, deal.ClientDealProposal.ClientSignature, deal.ClientDealProposal.Proposal.Client, b, tok)
if err != nil {
return fmt.Errorf("error verifying signature: %w", err)
}
if !verified {
return errors.New("could not verify signature")
return false, fmt.Errorf("error verifying signature: %w", err)
}

return nil
return verified, nil
}
3 changes: 1 addition & 2 deletions storagemarket/lp2pimpl/net.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,7 @@ func (p *DealProvider) handleNewDealStream(s network.Stream) {
// wait for deal execution to complete.
res, _, err := p.prov.ExecuteDeal(&proposal, s.Conn().RemotePeer())
if err != nil {
log.Warnw("executing deal proposal", "id", proposal.DealUUID, "err", err)
return
log.Warnw("deal proposal failed", "id", proposal.DealUUID, "err", err, "reason", res.Reason)
}

// Set a deadline on writing to the stream so it doesn't hang
Expand Down
8 changes: 6 additions & 2 deletions storagemarket/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,14 @@ func (p *Provider) ExecuteDeal(dp *types.DealParams, clientPeer peer.ID) (*api.P
}
// validate the deal proposal
if err := p.validateDealProposal(ds); err != nil {
p.dealLogger.Infow(dp.DealUUID, "deal proposal failed validation", "err", err.Error())
reason := err.reason
if reason == "" {
reason = err.Error()
}
p.dealLogger.Infow(dp.DealUUID, "deal proposal failed validation", "err", err.Error(), "reason", reason)

return &api.ProviderDealRejectionInfo{
Reason: fmt.Sprintf("failed validation: %s", err),
Reason: fmt.Sprintf("failed validation: %s", reason),
}, nil, nil
}

Expand Down
Loading