diff --git a/Makefile b/Makefile index 5ea13f9e0..f0e7dba11 100644 --- a/Makefile +++ b/Makefile @@ -89,6 +89,12 @@ boost: $(BUILD_DEPS) .PHONY: boost BINS+=boost boostx boostd +booster-http: $(BUILD_DEPS) + rm -f booster-http + $(GOCC) build $(GOFLAGS) -o booster-http ./cmd/booster-http +.PHONY: booster-http +BINS+=booster-http + devnet: $(BUILD_DEPS) rm -f devnet $(GOCC) build $(GOFLAGS) -o devnet ./cmd/devnet diff --git a/api/api.go b/api/api.go index 2dc9e0146..711f62183 100644 --- a/api/api.go +++ b/api/api.go @@ -15,6 +15,7 @@ import ( "github.com/google/uuid" "github.com/ipfs/go-cid" "github.com/libp2p/go-libp2p-core/peer" + "github.com/multiformats/go-multihash" ) // MODIFYING THE API INTERFACE @@ -43,8 +44,8 @@ type Boost interface { BoostDagstoreInitializeAll(ctx context.Context, params DagstoreInitializeAllParams) (<-chan DagstoreInitializeAllEvent, error) //perm:admin BoostDagstoreRecoverShard(ctx context.Context, key string) error //perm:admin BoostDagstoreGC(ctx context.Context) ([]DagstoreShardResult, error) //perm:admin - - BoostDagstoreListShards(ctx context.Context) ([]DagstoreShardInfo, error) //perm:read + BoostDagstorePiecesContainingMultihash(ctx context.Context, mh multihash.Multihash) ([]cid.Cid, error) //perm:read + BoostDagstoreListShards(ctx context.Context) ([]DagstoreShardInfo, error) //perm:read // RuntimeSubsystems returns the subsystems that are enabled // in this instance. @@ -67,6 +68,7 @@ type Boost interface { PiecesListCidInfos(ctx context.Context) ([]cid.Cid, error) //perm:read PiecesGetPieceInfo(ctx context.Context, pieceCid cid.Cid) (*piecestore.PieceInfo, error) //perm:read PiecesGetCIDInfo(ctx context.Context, payloadCid cid.Cid) (*piecestore.CIDInfo, error) //perm:read + PiecesGetMaxOffset(ctx context.Context, pieceCid cid.Cid) (uint64, error) //perm:read // MethodGroup: Actor ActorSectorSize(context.Context, address.Address) (abi.SectorSize, error) //perm:read diff --git a/api/proxy_gen.go b/api/proxy_gen.go index ade455970..e476313f6 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -23,6 +23,7 @@ import ( "github.com/libp2p/go-libp2p-core/network" "github.com/libp2p/go-libp2p-core/peer" "github.com/libp2p/go-libp2p-core/protocol" + "github.com/multiformats/go-multihash" ) var ErrNotSupported = errors.New("method not supported") @@ -43,6 +44,8 @@ type BoostStruct struct { BoostDagstoreListShards func(p0 context.Context) ([]DagstoreShardInfo, error) `perm:"read"` + BoostDagstorePiecesContainingMultihash func(p0 context.Context, p1 multihash.Multihash) ([]cid.Cid, error) `perm:"read"` + BoostDagstoreRecoverShard func(p0 context.Context, p1 string) error `perm:"admin"` BoostDagstoreRegisterShard func(p0 context.Context, p1 string) error `perm:"admin"` @@ -109,6 +112,8 @@ type BoostStruct struct { PiecesGetCIDInfo func(p0 context.Context, p1 cid.Cid) (*piecestore.CIDInfo, error) `perm:"read"` + PiecesGetMaxOffset func(p0 context.Context, p1 cid.Cid) (uint64, error) `perm:"read"` + PiecesGetPieceInfo func(p0 context.Context, p1 cid.Cid) (*piecestore.PieceInfo, error) `perm:"read"` PiecesListCidInfos func(p0 context.Context) ([]cid.Cid, error) `perm:"read"` @@ -281,6 +286,17 @@ func (s *BoostStub) BoostDagstoreListShards(p0 context.Context) ([]DagstoreShard return *new([]DagstoreShardInfo), ErrNotSupported } +func (s *BoostStruct) BoostDagstorePiecesContainingMultihash(p0 context.Context, p1 multihash.Multihash) ([]cid.Cid, error) { + if s.Internal.BoostDagstorePiecesContainingMultihash == nil { + return *new([]cid.Cid), ErrNotSupported + } + return s.Internal.BoostDagstorePiecesContainingMultihash(p0, p1) +} + +func (s *BoostStub) BoostDagstorePiecesContainingMultihash(p0 context.Context, p1 multihash.Multihash) ([]cid.Cid, error) { + return *new([]cid.Cid), ErrNotSupported +} + func (s *BoostStruct) BoostDagstoreRecoverShard(p0 context.Context, p1 string) error { if s.Internal.BoostDagstoreRecoverShard == nil { return ErrNotSupported @@ -644,6 +660,17 @@ func (s *BoostStub) PiecesGetCIDInfo(p0 context.Context, p1 cid.Cid) (*piecestor return nil, ErrNotSupported } +func (s *BoostStruct) PiecesGetMaxOffset(p0 context.Context, p1 cid.Cid) (uint64, error) { + if s.Internal.PiecesGetMaxOffset == nil { + return 0, ErrNotSupported + } + return s.Internal.PiecesGetMaxOffset(p0, p1) +} + +func (s *BoostStub) PiecesGetMaxOffset(p0 context.Context, p1 cid.Cid) (uint64, error) { + return 0, ErrNotSupported +} + func (s *BoostStruct) PiecesGetPieceInfo(p0 context.Context, p1 cid.Cid) (*piecestore.PieceInfo, error) { if s.Internal.PiecesGetPieceInfo == nil { return nil, ErrNotSupported diff --git a/build/openrpc/boost.json.gz b/build/openrpc/boost.json.gz index 9c7bc4afc..91b3f1065 100644 Binary files a/build/openrpc/boost.json.gz and b/build/openrpc/boost.json.gz differ diff --git a/cmd/boostd/run.go b/cmd/boostd/run.go index 686c772fe..5505850ac 100644 --- a/cmd/boostd/run.go +++ b/cmd/boostd/run.go @@ -28,6 +28,10 @@ var runCmd = &cli.Command{ Name: "pprof", Usage: "run pprof web server on localhost:6060", }, + &cli.BoolFlag{ + Name: "nosync", + Usage: "dont wait for the full node to sync with the chain", + }, }, Action: func(cctx *cli.Context) error { if cctx.Bool("pprof") { diff --git a/cmd/booster-http/main.go b/cmd/booster-http/main.go new file mode 100644 index 000000000..27e2a405e --- /dev/null +++ b/cmd/booster-http/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "os" + + "github.com/filecoin-project/boost/build" + cliutil "github.com/filecoin-project/boost/cli/util" + logging "github.com/ipfs/go-log/v2" + "github.com/urfave/cli/v2" +) + +var log = logging.Logger("booster") + +func main() { + app := &cli.App{ + Name: "booster-http", + Usage: "HTTP endpoint for retrieval from Filecoin", + EnableBashCompletion: true, + Version: build.UserVersion(), + Flags: []cli.Flag{ + cliutil.FlagVeryVerbose, + }, + Commands: []*cli.Command{ + runCmd, + }, + } + app.Setup() + + if err := app.Run(os.Args); err != nil { + os.Stderr.WriteString("Error: " + err.Error() + "\n") + } +} + +func before(cctx *cli.Context) error { + _ = logging.SetLogLevel("booster", "INFO") + + if cliutil.IsVeryVerbose { + _ = logging.SetLogLevel("booster", "DEBUG") + } + + return nil +} diff --git a/cmd/booster-http/run.go b/cmd/booster-http/run.go new file mode 100644 index 000000000..bed04629f --- /dev/null +++ b/cmd/booster-http/run.go @@ -0,0 +1,270 @@ +package main + +import ( + "context" + "errors" + "fmt" + "net/http" + _ "net/http/pprof" + "strings" + + "github.com/filecoin-project/boost/api" + bclient "github.com/filecoin-project/boost/api/client" + cliutil "github.com/filecoin-project/boost/cli/util" + "github.com/filecoin-project/dagstore/mount" + "github.com/filecoin-project/go-fil-markets/piecestore" + "github.com/filecoin-project/go-jsonrpc" + "github.com/filecoin-project/go-state-types/abi" + lapi "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/api/client" + "github.com/filecoin-project/lotus/api/v0api" + "github.com/filecoin-project/lotus/api/v1api" + lcli "github.com/filecoin-project/lotus/cli" + sectorstorage "github.com/filecoin-project/lotus/extern/sector-storage" + "github.com/filecoin-project/lotus/extern/sector-storage/stores" + "github.com/filecoin-project/lotus/markets/dagstore" + "github.com/filecoin-project/lotus/markets/sectoraccessor" + lotus_modules "github.com/filecoin-project/lotus/node/modules" + "github.com/filecoin-project/lotus/node/modules/dtypes" + "github.com/filecoin-project/lotus/node/repo" + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multihash" + "github.com/urfave/cli/v2" +) + +var runCmd = &cli.Command{ + Name: "run", + Usage: "Start a booster-http process", + Before: before, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "pprof", + Usage: "run pprof web server on localhost:6070", + }, + &cli.StringFlag{ + Name: "base-path", + Usage: "the base path at which to run the web server", + Value: "", + }, + &cli.UintFlag{ + Name: "port", + Usage: "the port the web server listens on", + Value: 7777, + }, + &cli.StringFlag{ + Name: "api-boost", + Usage: "the endpoint for the boost API", + Required: true, + }, + &cli.StringFlag{ + Name: "api-fullnode", + Usage: "the endpoint for the full node API", + Required: true, + }, + &cli.StringFlag{ + Name: "api-sealer", + Usage: "the endpoint for the sealer API", + Required: true, + }, + }, + Action: func(cctx *cli.Context) error { + if cctx.Bool("pprof") { + go func() { + err := http.ListenAndServe("localhost:6070", nil) + if err != nil { + log.Error(err) + } + }() + } + + // Connect to the Boost API + ctx := lcli.ReqContext(cctx) + boostApiInfo := cctx.String("api-boost") + bapi, bcloser, err := getBoostApi(ctx, boostApiInfo) + if err != nil { + return fmt.Errorf("getting boost API: %w", err) + } + defer bcloser() + + // Connect to the full node API + fnApiInfo := cctx.String("api-fullnode") + fullnodeApi, ncloser, err := getFullNodeApi(ctx, fnApiInfo) + if err != nil { + return fmt.Errorf("getting full node API: %w", err) + } + defer ncloser() + + // Connect to the sealing API + sealingApiInfo := cctx.String("api-sealer") + sauth, err := storageAuthWithURL(sealingApiInfo) + if err != nil { + return fmt.Errorf("parsing sealing API endpoint: %w", err) + } + sealingService, sealerCloser, err := getMinerApi(ctx, sealingApiInfo) + if err != nil { + return fmt.Errorf("getting miner API: %w", err) + } + defer sealerCloser() + + maddr, err := sealingService.ActorAddress(ctx) + if err != nil { + return fmt.Errorf("getting miner actor address: %w", err) + } + log.Infof("Miner address: %s", maddr) + + // Use an in-memory repo because we don't need any functions + // of a real repo, we just need to supply something that satisfies + // the LocalStorage interface to the store + memRepo := repo.NewMemory(nil) + lr, err := memRepo.Lock(repo.StorageMiner) + if err != nil { + return fmt.Errorf("locking mem repo: %w", err) + } + defer lr.Close() + + // Create the store interface + var urls []string + lstor, err := stores.NewLocal(ctx, lr, sealingService, urls) + if err != nil { + return fmt.Errorf("creating new local store: %w", err) + } + storage := lotus_modules.RemoteStorage(lstor, sealingService, sauth, sectorstorage.Config{ + // TODO: Not sure if I need this, or any of the other fields in this struct + ParallelFetchLimit: 1, + }) + // Create the piece provider and sector accessors + pp := sectorstorage.NewPieceProvider(storage, sealingService, sealingService) + sa := sectoraccessor.NewSectorAccessor(dtypes.MinerAddress(maddr), sealingService, pp, fullnodeApi) + // Create the server API + sapi := serverApi{ctx: ctx, bapi: bapi, sa: sa} + server := NewHttpServer(cctx.String("base-path"), cctx.Int("port"), sapi) + + // Start the server + log.Infof("Starting booster-http node on port %d with base path '%s'", + cctx.Int("port"), cctx.String("base-path")) + server.Start(ctx) + + // Monitor for shutdown. + <-ctx.Done() + + log.Info("Shutting down...") + + err = server.Stop() + if err != nil { + return err + } + log.Info("Graceful shutdown successful") + + // Sync all loggers. + _ = log.Sync() //nolint:errcheck + + return nil + }, +} + +func storageAuthWithURL(apiInfo string) (sectorstorage.StorageAuth, error) { + s := strings.Split(apiInfo, ":") + if len(s) != 2 { + return nil, errors.New("unexpected format of `apiInfo`") + } + headers := http.Header{} + headers.Add("Authorization", "Bearer "+s[0]) + return sectorstorage.StorageAuth(headers), nil +} + +type serverApi struct { + ctx context.Context + bapi api.Boost + sa dagstore.SectorAccessor +} + +var _ HttpServerApi = (*serverApi)(nil) + +func (s serverApi) PiecesContainingMultihash(mh multihash.Multihash) ([]cid.Cid, error) { + return s.bapi.BoostDagstorePiecesContainingMultihash(s.ctx, mh) +} + +func (s serverApi) GetMaxPieceOffset(pieceCid cid.Cid) (uint64, error) { + return s.bapi.PiecesGetMaxOffset(s.ctx, pieceCid) +} + +func (s serverApi) GetPieceInfo(pieceCID cid.Cid) (*piecestore.PieceInfo, error) { + return s.bapi.PiecesGetPieceInfo(s.ctx, pieceCID) +} + +func (s serverApi) IsUnsealed(ctx context.Context, sectorID abi.SectorNumber, offset abi.UnpaddedPieceSize, length abi.UnpaddedPieceSize) (bool, error) { + return s.sa.IsUnsealed(ctx, sectorID, offset, length) +} + +func (s serverApi) UnsealSectorAt(ctx context.Context, sectorID abi.SectorNumber, offset abi.UnpaddedPieceSize, length abi.UnpaddedPieceSize) (mount.Reader, error) { + return s.sa.UnsealSectorAt(ctx, sectorID, offset, length) +} + +func getBoostApi(ctx context.Context, ai string) (api.Boost, jsonrpc.ClientCloser, error) { + ai = strings.TrimPrefix(strings.TrimSpace(ai), "BOOST_API_INFO=") + info := cliutil.ParseApiInfo(ai) + addr, err := info.DialArgs("v0") + if err != nil { + return nil, nil, fmt.Errorf("could not get DialArgs: %w", err) + } + + log.Infof("Using boost API at %s", addr) + api, closer, err := bclient.NewBoostRPCV0(ctx, addr, info.AuthHeader()) + if err != nil { + return nil, nil, fmt.Errorf("creating full node service API: %w", err) + } + + return api, closer, nil +} + +func getFullNodeApi(ctx context.Context, ai string) (v1api.FullNode, jsonrpc.ClientCloser, error) { + ai = strings.TrimPrefix(strings.TrimSpace(ai), "FULLNODE_API_INFO=") + info := cliutil.ParseApiInfo(ai) + addr, err := info.DialArgs("v1") + if err != nil { + return nil, nil, fmt.Errorf("could not get DialArgs: %w", err) + } + + log.Infof("Using full node API at %s", addr) + api, closer, err := client.NewFullNodeRPCV1(ctx, addr, info.AuthHeader()) + if err != nil { + return nil, nil, fmt.Errorf("creating full node service API: %w", err) + } + + v, err := api.Version(ctx) + if err != nil { + return nil, nil, fmt.Errorf("checking full node service API version: %w", err) + } + + if !v.APIVersion.EqMajorMinor(lapi.FullAPIVersion1) { + return nil, nil, fmt.Errorf("full node service API version didn't match (expected %s, remote %s)", lapi.FullAPIVersion1, v.APIVersion) + } + + return api, closer, nil +} + +func getMinerApi(ctx context.Context, ai string) (v0api.StorageMiner, jsonrpc.ClientCloser, error) { + ai = strings.TrimPrefix(strings.TrimSpace(ai), "MINER_API_INFO=") + info := cliutil.ParseApiInfo(ai) + addr, err := info.DialArgs("v0") + if err != nil { + return nil, nil, fmt.Errorf("could not get DialArgs: %w", err) + } + + log.Infof("Using sealing API at %s", addr) + api, closer, err := client.NewStorageMinerRPCV0(ctx, addr, info.AuthHeader()) + if err != nil { + return nil, nil, fmt.Errorf("creating miner service API: %w", err) + } + + v, err := api.Version(ctx) + if err != nil { + return nil, nil, fmt.Errorf("checking miner service API version: %w", err) + } + + if !v.APIVersion.EqMajorMinor(lapi.MinerAPIVersion0) { + return nil, nil, fmt.Errorf("miner service API version didn't match (expected %s, remote %s)", lapi.MinerAPIVersion0, v.APIVersion) + } + + return api, closer, nil +} diff --git a/cmd/booster-http/server.go b/cmd/booster-http/server.go new file mode 100644 index 000000000..eb16ce39b --- /dev/null +++ b/cmd/booster-http/server.go @@ -0,0 +1,433 @@ +package main + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + "strings" + "time" + + "github.com/fatih/color" + "github.com/filecoin-project/dagstore/mount" + "github.com/filecoin-project/go-fil-markets/piecestore" + "github.com/filecoin-project/go-fil-markets/retrievalmarket" + "github.com/filecoin-project/go-state-types/abi" + "github.com/hashicorp/go-multierror" + "github.com/ipfs/go-cid" + "github.com/ipfs/go-datastore" + "github.com/multiformats/go-multihash" + "github.com/multiformats/go-varint" +) + +var ErrNotFound = errors.New("not found") + +type HttpServer struct { + path string + port int + api HttpServerApi + + ctx context.Context + cancel context.CancelFunc + server *http.Server +} + +type HttpServerApi interface { + PiecesContainingMultihash(mh multihash.Multihash) ([]cid.Cid, error) + GetMaxPieceOffset(pieceCid cid.Cid) (uint64, error) + GetPieceInfo(pieceCID cid.Cid) (*piecestore.PieceInfo, error) + IsUnsealed(ctx context.Context, sectorID abi.SectorNumber, offset abi.UnpaddedPieceSize, length abi.UnpaddedPieceSize) (bool, error) + UnsealSectorAt(ctx context.Context, sectorID abi.SectorNumber, pieceOffset abi.UnpaddedPieceSize, length abi.UnpaddedPieceSize) (mount.Reader, error) +} + +func NewHttpServer(path string, port int, api HttpServerApi) *HttpServer { + return &HttpServer{path: path, port: port, api: api} +} + +func (s *HttpServer) payloadBasePath() string { + return s.path + "/payload/" +} + +func (s *HttpServer) pieceBasePath() string { + return s.path + "/piece/" +} + +func (s *HttpServer) Start(ctx context.Context) { + s.ctx, s.cancel = context.WithCancel(ctx) + + listenAddr := fmt.Sprintf(":%d", s.port) + handler := http.NewServeMux() + handler.HandleFunc(s.payloadBasePath(), s.handleByPayloadCid) + handler.HandleFunc(s.pieceBasePath(), s.handleByPieceCid) + handler.HandleFunc("/", s.handleIndex) + handler.HandleFunc("/index.html", s.handleIndex) + s.server = &http.Server{ + Addr: listenAddr, + Handler: handler, + // This context will be the parent of the context associated with all + // incoming requests + BaseContext: func(listener net.Listener) context.Context { + return s.ctx + }, + } + + go func() { + if err := s.server.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("http.ListenAndServe(): %v", err) + } + }() +} + +func (s *HttpServer) Stop() error { + s.cancel() + return s.server.Close() +} + +const idxPage = ` + +
++ Download a CAR file by payload CID + | +
+ /payload/ |
+
+ Download a CAR file by piece CID + | +
+ /piece/ |
+