github.com/filecoin-project/bacalhau@v0.3.23-0.20230228154132-45c989550ace/pkg/publisher/filecoin_lotus/publisher.go (about)

     1  package filecoinlotus
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/filecoin-project/bacalhau/pkg/ipfs/car"
    12  	"github.com/filecoin-project/bacalhau/pkg/job"
    13  	"github.com/filecoin-project/bacalhau/pkg/model"
    14  	"github.com/filecoin-project/bacalhau/pkg/publisher"
    15  	"github.com/filecoin-project/bacalhau/pkg/publisher/filecoin_lotus/api"
    16  	"github.com/filecoin-project/bacalhau/pkg/publisher/filecoin_lotus/api/storagemarket"
    17  	"github.com/filecoin-project/bacalhau/pkg/storage/util"
    18  	"github.com/filecoin-project/bacalhau/pkg/system"
    19  	"github.com/filecoin-project/go-address"
    20  	big2 "github.com/filecoin-project/go-state-types/big"
    21  	"github.com/hashicorp/go-multierror"
    22  	"github.com/ipfs/go-cid"
    23  	"github.com/rs/zerolog/log"
    24  )
    25  
    26  type PublisherConfig struct {
    27  	// How long the deal for the data should be created for
    28  	StorageDuration time.Duration
    29  	// Location of the Lotus configuration directory - either $LOTUS_PATH or ~/.lotus
    30  	PathDir string
    31  	// Directory to use when uploading content to Lotus - optional
    32  	UploadDir string
    33  	// How close miner should be when selecting the cheapest
    34  	MaximumPing time.Duration
    35  }
    36  
    37  type Publisher struct {
    38  	config PublisherConfig
    39  	client api.Client
    40  }
    41  
    42  func NewPublisher(
    43  	ctx context.Context,
    44  	cm *system.CleanupManager,
    45  	config PublisherConfig,
    46  ) (*Publisher, error) {
    47  	if config.StorageDuration == time.Duration(0) {
    48  		return nil, errors.New("StorageDuration is required")
    49  	}
    50  	if config.PathDir == "" {
    51  		return nil, errors.New("PathDir is required")
    52  	}
    53  	if config.MaximumPing == time.Duration(0) {
    54  		return nil, errors.New("MaximumPing is required")
    55  	}
    56  
    57  	client, err := api.NewClientFromConfigDir(ctx, config.PathDir)
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  	cm.RegisterCallback(client.Close)
    62  
    63  	return newPublisher(config, client), nil
    64  }
    65  
    66  func newPublisher(
    67  	config PublisherConfig,
    68  	client api.Client,
    69  ) *Publisher {
    70  	return &Publisher{
    71  		config: config,
    72  		client: client,
    73  	}
    74  }
    75  
    76  func (l *Publisher) IsInstalled(ctx context.Context) (bool, error) {
    77  	if _, err := l.client.Version(ctx); err != nil {
    78  		return false, err
    79  	}
    80  	return true, nil
    81  }
    82  
    83  func (l *Publisher) PublishShardResult(
    84  	ctx context.Context,
    85  	shard model.JobShard,
    86  	hostID string,
    87  	shardResultPath string,
    88  ) (model.StorageSpec, error) {
    89  	log.Ctx(ctx).Debug().
    90  		Stringer("shard", shard).
    91  		Str("host", hostID).
    92  		Str("shardResultPath", shardResultPath).
    93  		Msg("Uploading results folder to filecoin lotus")
    94  
    95  	carFile, err := l.carResultsDir(ctx, shardResultPath)
    96  	if err != nil {
    97  		return model.StorageSpec{}, err
    98  	}
    99  
   100  	contentCid, err := l.importData(ctx, carFile)
   101  	if err != nil {
   102  		return model.StorageSpec{}, err
   103  	}
   104  
   105  	dealCid, err := l.createDeal(ctx, contentCid)
   106  	if err != nil {
   107  		return model.StorageSpec{}, err
   108  	}
   109  
   110  	spec := job.GetPublishedStorageSpec(shard, model.StorageSourceFilecoin, hostID, contentCid.String())
   111  	spec.Metadata["deal_cid"] = dealCid
   112  	return spec, nil
   113  }
   114  
   115  func (l *Publisher) carResultsDir(ctx context.Context, resultsDir string) (string, error) {
   116  	tempFile, err := os.CreateTemp(l.config.UploadDir, "results-*.car")
   117  	if err != nil {
   118  		return "", err
   119  	}
   120  
   121  	// Temporary files will have 0600 as their permissions, which could cause issues when sharing with a Lotus node
   122  	// running inside a container.
   123  	if err := tempFile.Chmod(util.OS_ALL_RW); err != nil { //nolint:govet
   124  		return "", err
   125  	}
   126  
   127  	// Just need the filename
   128  	if err := tempFile.Close(); err != nil {
   129  		return "", err
   130  	}
   131  
   132  	if _, err := car.CreateCar(ctx, resultsDir, tempFile.Name(), 1); err != nil {
   133  		return "", err
   134  	}
   135  
   136  	return tempFile.Name(), nil
   137  }
   138  
   139  func (l *Publisher) importData(ctx context.Context, filePath string) (cid.Cid, error) {
   140  	res, err := l.client.ClientImport(ctx, api.FileRef{
   141  		Path:  filePath,
   142  		IsCAR: true,
   143  	})
   144  	if err != nil {
   145  		return cid.Cid{}, err
   146  	}
   147  	return res.Root, nil
   148  }
   149  
   150  func (l *Publisher) createDeal(ctx context.Context, contentCid cid.Cid) (string, error) {
   151  	dataSize, err := l.client.ClientDealPieceCID(ctx, contentCid)
   152  	if err != nil {
   153  		return "", err
   154  	}
   155  
   156  	params, err := l.client.StateGetNetworkParams(ctx)
   157  	if err != nil {
   158  		return "", err
   159  	}
   160  
   161  	epochs := api.ChainEpoch(l.config.StorageDuration / (time.Duration(params.BlockDelaySecs) * time.Second))
   162  
   163  	wallet, err := l.client.WalletDefaultAddress(ctx)
   164  	if err != nil {
   165  		return "", err
   166  	}
   167  
   168  	miners, err := l.client.StateListMiners(ctx, api.TipSetKey{})
   169  	if err != nil {
   170  		return "", err
   171  	}
   172  
   173  	log.Ctx(ctx).Debug().Int("count", len(miners)).Msg("Initial list of miners")
   174  
   175  	asks, errs := throttledMap(miners, func(miner address.Address) (*ask, error) {
   176  		return l.queryMiner(ctx, dataSize, miner)
   177  	}, parallelMinerQueries)
   178  	if len(asks) == 0 {
   179  		log.Ctx(ctx).
   180  			Err(multierror.Append(nil, errs...)).
   181  			Msg("Couldn't find a miner")
   182  		return "", fmt.Errorf("unable to find a miner")
   183  	}
   184  
   185  	cheapest := asks[0]
   186  	for _, a := range asks {
   187  		if a.epochPrice.LessThan(cheapest.epochPrice) {
   188  			cheapest = a
   189  		}
   190  	}
   191  
   192  	deal, err := l.client.ClientStartDeal(ctx, &api.StartDealParams{
   193  		Data: &api.DataRef{
   194  			TransferType: "graphsync", // storagemarket.TTGraphsync
   195  			Root:         contentCid,
   196  			PieceCid:     &dataSize.PieceCID,
   197  			PieceSize:    dataSize.PieceSize.Unpadded(),
   198  		},
   199  		Wallet:            wallet,
   200  		Miner:             cheapest.miner,
   201  		EpochPrice:        cheapest.epochPrice,
   202  		MinBlocksDuration: uint64(epochs),
   203  	})
   204  	if err != nil {
   205  		return "", err
   206  	}
   207  
   208  	log.Ctx(ctx).Info().Stringer("cid", deal).Msg("Deal started")
   209  
   210  	if err := l.waitUntilDealIsReady(ctx, deal); err != nil {
   211  		return "", err
   212  	}
   213  
   214  	return deal.String(), nil
   215  }
   216  
   217  func (l *Publisher) waitUntilDealIsReady(ctx context.Context, deal *cid.Cid) error {
   218  	// The go-jsonrpc library that the `client` uses relies on the context to know when to stop writing to the info channel
   219  	ctx, cancel := context.WithCancel(ctx)
   220  	defer cancel()
   221  
   222  	infoChan, err := l.client.ClientGetDealUpdates(ctx)
   223  	if err != nil {
   224  		return err
   225  	}
   226  
   227  	t := time.NewTicker(3 * time.Second)
   228  	defer t.Stop()
   229  
   230  	// The documentation recommends that, at least for lite nodes, we should wait until the deal's state is `StorageDealActive`.
   231  	// This can take a long time, an hour or so, with the test image.
   232  	// Additional states after `StorageDealCheckForAcceptance` are:
   233  	// * `StorageDealAwaitingPreCommit` is reached once the sector available for sealing - a.k.a. no more data allowed
   234  	// * `StorageDealSealing` is reached after PreCommit has happened (150 epochs?)
   235  	// * `StorageDealActive` - sector has been sealed and everything is ready
   236  	var currentState storagemarket.StorageDealStatus
   237  	wanted := storagemarket.StorageDealCheckForAcceptance
   238  	for {
   239  		select {
   240  		case <-ctx.Done():
   241  			return ctx.Err()
   242  		case info := <-infoChan:
   243  			if deal.Equals(info.ProposalCid) {
   244  				currentState = info.State
   245  
   246  				if currentState == wanted {
   247  					log.Ctx(ctx).Info().
   248  						Stringer("deal", deal).
   249  						Str("current", storagemarket.DealStates[currentState]).
   250  						Str("expected", storagemarket.DealStates[wanted]).
   251  						Msg("Deal in expected state")
   252  					return nil
   253  				}
   254  
   255  				if currentState == storagemarket.StorageDealFailing || currentState == storagemarket.StorageDealError {
   256  					return fmt.Errorf("deal not accepted: %s", info.Message)
   257  				}
   258  			}
   259  		case <-t.C:
   260  			log.Ctx(ctx).Info().
   261  				Stringer("deal", deal).
   262  				Str("current", storagemarket.DealStates[currentState]).
   263  				Str("expected", storagemarket.DealStates[wanted]).
   264  				Msg("Deal not currently in expected state")
   265  		}
   266  	}
   267  }
   268  
   269  func (l *Publisher) queryMiner(ctx context.Context, dataSize api.DataCIDSize, miner address.Address) (*ask, error) {
   270  	minerInfo, err := l.client.StateMinerInfo(ctx, miner, api.TipSetKey{})
   271  	if err != nil {
   272  		return nil, fmt.Errorf("failed to get miner %s info: %w", miner, err)
   273  	}
   274  
   275  	power, err := l.client.StateMinerPower(ctx, miner, api.TipSetKey{})
   276  	if err != nil {
   277  		return nil, fmt.Errorf("failed to get miner %s power: %w", miner, err)
   278  	}
   279  	if !power.HasMinPower {
   280  		return nil, fmt.Errorf("miner %s doesn't have min power", miner)
   281  	}
   282  
   283  	start := time.Now()
   284  	query, err := l.client.ClientQueryAsk(ctx, *minerInfo.PeerId, miner)
   285  	if err != nil {
   286  		return nil, fmt.Errorf("failed to query miner %s: %w", miner, err)
   287  	}
   288  	ping := time.Since(start)
   289  
   290  	if ping > l.config.MaximumPing {
   291  		return nil, fmt.Errorf("ping for miner %s (%s) is too large", miner, ping)
   292  	}
   293  
   294  	if query.Response.MinPieceSize > dataSize.PieceSize {
   295  		return nil, fmt.Errorf("data size (%v) is too small for miner %s (%v)", dataSize.PieceSize, miner, query.Response.MinPieceSize)
   296  	}
   297  	if query.Response.MaxPieceSize < dataSize.PieceSize {
   298  		return nil, fmt.Errorf("data size (%v) is too big for miner %s (%v)", dataSize.PieceSize, miner, query.Response.MaxPieceSize)
   299  	}
   300  
   301  	epochPrice := big2.Div(big2.Mul(query.Response.Price, big2.NewIntUnsigned(uint64(dataSize.PieceSize))), big2.NewInt(oneGibibyte))
   302  
   303  	return &ask{
   304  		miner:      miner,
   305  		epochPrice: epochPrice,
   306  	}, nil
   307  }
   308  
   309  type ask struct {
   310  	miner      address.Address
   311  	epochPrice big2.Int
   312  }
   313  
   314  var _ publisher.Publisher = &Publisher{}
   315  
   316  const oneGibibyte = 1 << 30
   317  const parallelMinerQueries = 50
   318  
   319  func throttledMap[T any, V comparable](ts []T, f func(T) (V, error), concurrent int) ([]V, []error) {
   320  	throttle := make(chan struct{}, concurrent)
   321  	mu := sync.Mutex{}
   322  	var wg sync.WaitGroup
   323  
   324  	var errs []error
   325  	var vs []V
   326  
   327  	var empty V
   328  
   329  	for _, t := range ts {
   330  		t := t
   331  		wg.Add(1)
   332  		throttle <- struct{}{}
   333  		go func() {
   334  			defer func() {
   335  				<-throttle
   336  			}()
   337  			defer wg.Done()
   338  
   339  			v, err := f(t)
   340  			mu.Lock()
   341  			defer mu.Unlock()
   342  			if err != nil {
   343  				errs = append(errs, err)
   344  			} else if v != empty {
   345  				vs = append(vs, v)
   346  			}
   347  		}()
   348  	}
   349  
   350  	wg.Wait()
   351  
   352  	return vs, errs
   353  }