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 }