diff --git a/api/api_full.go b/api/api_full.go index a59982a79..4dc39581f 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -227,6 +227,8 @@ type FullNode interface { ClientCalcCommP(ctx context.Context, inpath string, miner address.Address) (*CommPRet, error) // ClientGenCar generates a CAR file for the specified file. ClientGenCar(ctx context.Context, ref FileRef, outpath string) error + // ClientDealSize calculates real deal data size + ClientDealSize(ctx context.Context, root cid.Cid) (DataSize, error) // ClientUnimport removes references to the specified file from filestore //ClientUnimport(path string) @@ -666,6 +668,11 @@ type BlockTemplate struct { WinningPoStProof []abi.PoStProof } +type DataSize struct { + PayloadSize int64 + PieceSize abi.PaddedPieceSize +} + type CommPRet struct { Root cid.Cid Size abi.UnpaddedPieceSize diff --git a/api/apistruct/struct.go b/api/apistruct/struct.go index 764abe319..00e84fac3 100644 --- a/api/apistruct/struct.go +++ b/api/apistruct/struct.go @@ -129,6 +129,7 @@ type FullNodeStruct struct { ClientQueryAsk func(ctx context.Context, p peer.ID, miner address.Address) (*storagemarket.SignedStorageAsk, error) `perm:"read"` ClientCalcCommP func(ctx context.Context, inpath string, miner address.Address) (*api.CommPRet, error) `perm:"read"` ClientGenCar func(ctx context.Context, ref api.FileRef, outpath string) error `perm:"write"` + ClientDealSize func(ctx context.Context, root cid.Cid) (api.DataSize, error) `perm:"read"` StateNetworkName func(context.Context) (dtypes.NetworkName, error) `perm:"read"` StateMinerSectors func(context.Context, address.Address, *abi.BitField, bool, types.TipSetKey) ([]*api.ChainSectorInfo, error) `perm:"read"` @@ -418,6 +419,10 @@ func (c *FullNodeStruct) ClientGenCar(ctx context.Context, ref api.FileRef, outp return c.Internal.ClientGenCar(ctx, ref, outpath) } +func (c *FullNodeStruct) ClientDealSize(ctx context.Context, root cid.Cid) (api.DataSize, error) { + return c.Internal.ClientDealSize(ctx, root) +} + func (c *FullNodeStruct) GasEstimateGasPrice(ctx context.Context, nblocksincl uint64, sender address.Address, gaslimit int64, tsk types.TipSetKey) (types.BigInt, error) { return c.Internal.GasEstimateGasPrice(ctx, nblocksincl, sender, gaslimit, tsk) diff --git a/cli/client.go b/cli/client.go index 90a62127e..ab30df6b8 100644 --- a/cli/client.go +++ b/cli/client.go @@ -3,13 +3,14 @@ package cli import ( "encoding/json" "fmt" - "github.com/filecoin-project/lotus/build" "os" "path/filepath" "sort" "strconv" "text/tabwriter" + "time" + "github.com/docker/go-units" "github.com/fatih/color" "github.com/ipfs/go-cid" "github.com/ipfs/go-cidutil/cidenc" @@ -22,10 +23,12 @@ import ( "github.com/filecoin-project/go-fil-markets/storagemarket" "github.com/filecoin-project/go-multistore" "github.com/filecoin-project/specs-actors/actors/abi" + "github.com/filecoin-project/specs-actors/actors/abi/big" "github.com/filecoin-project/specs-actors/actors/builtin/market" "github.com/filecoin-project/lotus/api" lapi "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/chain/types" ) @@ -314,6 +317,10 @@ var clientDealCmd = &cli.Command{ &CidBaseFlag, }, Action: func(cctx *cli.Context) error { + if !cctx.Args().Present() { + return interactiveDeal(cctx) + } + api, closer, err := GetFullNodeAPI(cctx) if err != nil { return err @@ -435,6 +442,197 @@ var clientDealCmd = &cli.Command{ }, } +func interactiveDeal(cctx *cli.Context) error { + api, closer, err := GetFullNodeAPI(cctx) + if err != nil { + return err + } + defer closer() + ctx := ReqContext(cctx) + + state := "import" + + var data cid.Cid + var days int + var maddr address.Address + var ask storagemarket.StorageAsk + var epochPrice big.Int + var epochs abi.ChainEpoch + + var a address.Address + if from := cctx.String("from"); from != "" { + faddr, err := address.NewFromString(from) + if err != nil { + return xerrors.Errorf("failed to parse 'from' address: %w", err) + } + a = faddr + } else { + def, err := api.WalletDefaultAddress(ctx) + if err != nil { + return err + } + a = def + } + + printErr := func(err error) { + fmt.Printf("%s %s\n", color.RedString("Error:"), err.Error()) + } + + for { + // TODO: better exit handling + if err := ctx.Err(); err != nil { + return err + } + + switch state { + case "import": + fmt.Print("Data CID (from " + color.YellowString("lotus client import") + "): ") + + var cidStr string + _, err := fmt.Scan(&cidStr) + if err != nil { + printErr(xerrors.Errorf("reading cid string: %w", err)) + continue + } + + data, err = cid.Parse(cidStr) + if err != nil { + printErr(xerrors.Errorf("parsing cid string: %w", err)) + continue + } + + state = "duration" + case "duration": + fmt.Print("Deal duration (days): ") + + _, err := fmt.Scan(&days) + if err != nil { + printErr(xerrors.Errorf("parsing duration: %w", err)) + continue + } + + state = "miner" + case "miner": + fmt.Print("Miner Address (t0..): ") + var maddrStr string + + _, err := fmt.Scan(&maddrStr) + if err != nil { + printErr(xerrors.Errorf("reading miner address: %w", err)) + continue + } + + maddr, err = address.NewFromString(maddrStr) + if err != nil { + printErr(xerrors.Errorf("parsing miner address: %w", err)) + continue + } + + state = "query" + case "query": + color.Blue(".. querying miner ask") + + mi, err := api.StateMinerInfo(ctx, maddr, types.EmptyTSK) + if err != nil { + printErr(xerrors.Errorf("failed to get peerID for miner: %w", err)) + state = "miner" + continue + } + + a, err := api.ClientQueryAsk(ctx, mi.PeerId, maddr) + if err != nil { + printErr(xerrors.Errorf("failed to query ask: %w", err)) + state = "miner" + continue + } + + ask = *a.Ask + + // TODO: run more validation + state = "confirm" + case "confirm": + fromBal, err := api.WalletBalance(ctx, a) + if err != nil { + return xerrors.Errorf("checking from address balance: %w", err) + } + + color.Blue(".. calculating data size\n") + ds, err := api.ClientDealSize(ctx, data) + if err != nil { + return err + } + + dur := 24 * time.Hour * time.Duration(days) + + epochs = abi.ChainEpoch(dur / (time.Duration(build.BlockDelaySecs) * time.Second)) + // TODO: do some more or epochs math (round to miner PP, deal start buffer) + + gib := types.NewInt(1 << 30) + + // TODO: price is based on PaddedPieceSize, right? + epochPrice = types.BigDiv(types.BigMul(ask.Price, types.NewInt(uint64(ds.PieceSize))), gib) + totalPrice := types.BigMul(epochPrice, types.NewInt(uint64(epochs))) + + fmt.Printf("-----\n") + fmt.Printf("Proposing from %s\n", a) + fmt.Printf("\tBalance: %s\n", types.FIL(fromBal)) + fmt.Printf("\n") + fmt.Printf("Piece size: %s (Payload size: %s)\n", units.BytesSize(float64(ds.PieceSize)), units.BytesSize(float64(ds.PayloadSize))) + fmt.Printf("Duration: %s\n", dur) + fmt.Printf("Total price: ~%s (%s per epoch)\n", types.FIL(totalPrice), types.FIL(epochPrice)) + + state = "accept" + case "accept": + fmt.Print("\nAccept (yes/no): ") + + var yn string + _, err := fmt.Scan(&yn) + if err != nil { + return err + } + + if yn == "no" { + return nil + } + + if yn != "yes" { + fmt.Println("Type in full 'yes' or 'no'") + continue + } + + state = "execute" + case "execute": + color.Blue(".. executing") + proposal, err := api.ClientStartDeal(ctx, &lapi.StartDealParams{ + Data: &storagemarket.DataRef{ + TransferType: storagemarket.TTGraphsync, + Root: data, + }, + Wallet: a, + Miner: maddr, + EpochPrice: epochPrice, + MinBlocksDuration: uint64(epochs), + DealStartEpoch: abi.ChainEpoch(cctx.Int64("start-epoch")), + FastRetrieval: cctx.Bool("fast-retrieval"), + VerifiedDeal: false, // TODO: Allow setting + }) + if err != nil { + return err + } + + encoder, err := GetCidEncoder(cctx) + if err != nil { + return err + } + + fmt.Println("\nDeal CID:", color.GreenString(encoder.Encode(*proposal))) + return nil + default: + return xerrors.Errorf("unknown state: %s", state) + } + } +} + var clientFindCmd = &cli.Command{ Name: "find", Usage: "Find data in the network", diff --git a/go.mod b/go.mod index e4a57d7e9..d89b94f4a 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/filecoin-project/go-fil-markets v0.5.3-0.20200730194453-26fac2c92927 github.com/filecoin-project/go-jsonrpc v0.1.1-0.20200602181149-522144ab4e24 github.com/filecoin-project/go-multistore v0.0.2 + github.com/filecoin-project/go-padreader v0.0.0-20200210211231-548257017ca6 github.com/filecoin-project/go-paramfetch v0.0.2-0.20200701152213-3e0f0afdc261 github.com/filecoin-project/go-statestore v0.1.0 github.com/filecoin-project/go-storedcounter v0.0.0-20200421200003-1c99c62e8a5b diff --git a/node/impl/client/client.go b/node/impl/client/client.go index 715514c76..384450bf9 100644 --- a/node/impl/client/client.go +++ b/node/impl/client/client.go @@ -34,6 +34,7 @@ import ( "github.com/filecoin-project/go-fil-markets/shared" "github.com/filecoin-project/go-fil-markets/storagemarket" "github.com/filecoin-project/go-multistore" + "github.com/filecoin-project/go-padreader" "github.com/filecoin-project/sector-storage/ffiwrapper" "github.com/filecoin-project/specs-actors/actors/abi" "github.com/filecoin-project/specs-actors/actors/abi/big" @@ -69,7 +70,7 @@ type API struct { Imports dtypes.ClientImportMgr - RetBstore dtypes.ClientBlockstore // TODO: try to remove + CombinedBstore dtypes.ClientBlockstore // TODO: try to remove } func calcDealExpiration(minDuration uint64, md *miner.DeadlineInfo, startEpoch abi.ChainEpoch) abi.ChainEpoch { @@ -554,6 +555,31 @@ func (a *API) ClientCalcCommP(ctx context.Context, inpath string, miner address. }, nil } +type lenWriter int64 + +func (w *lenWriter) Write(p []byte) (n int, err error) { + *w += lenWriter(len(p)) + return len(p), nil +} + +func (a *API) ClientDealSize(ctx context.Context, root cid.Cid) (api.DataSize, error) { + dag := merkledag.NewDAGService(blockservice.New(a.CombinedBstore, offline.Exchange(a.CombinedBstore))) + + w := lenWriter(0) + + err := car.WriteCar(ctx, dag, []cid.Cid{root}, &w) + if err != nil { + return api.DataSize{}, err + } + + up := padreader.PaddedSize(uint64(w)) + + return api.DataSize{ + PayloadSize: int64(w), + PieceSize: up.Padded(), + }, nil +} + func (a *API) ClientGenCar(ctx context.Context, ref api.FileRef, outputPath string) error { id, st, err := a.imgr().NewStore() if err != nil { diff --git a/node/modules/client.go b/node/modules/client.go index c7e2e33c9..9545de341 100644 --- a/node/modules/client.go +++ b/node/modules/client.go @@ -2,7 +2,6 @@ package modules import ( "context" - "github.com/filecoin-project/go-fil-markets/storagemarket/impl/funds" "time" "github.com/filecoin-project/go-multistore" @@ -19,6 +18,7 @@ import ( rmnet "github.com/filecoin-project/go-fil-markets/retrievalmarket/network" "github.com/filecoin-project/go-fil-markets/storagemarket" storageimpl "github.com/filecoin-project/go-fil-markets/storagemarket/impl" + "github.com/filecoin-project/go-fil-markets/storagemarket/impl/funds" "github.com/filecoin-project/go-fil-markets/storagemarket/impl/requestvalidation" smnet "github.com/filecoin-project/go-fil-markets/storagemarket/network" "github.com/filecoin-project/go-statestore" @@ -64,7 +64,7 @@ func ClientImportMgr(mds dtypes.ClientMultiDstore, ds dtypes.MetadataDS) dtypes. func ClientBlockstore(imgr dtypes.ClientImportMgr) dtypes.ClientBlockstore { // in most cases this is now unused in normal operations -- however, it's important to preserve for the IPFS use case - return blockstore.NewTemporary() + return blockstore.WrapIDStore(imgr.Blockstore) } // RegisterClientValidator is an initialization hook that registers the client