diff --git a/api/api.go b/api/api.go index d6e672c0a..9dbc77bf7 100644 --- a/api/api.go +++ b/api/api.go @@ -100,11 +100,13 @@ type FullNode interface { PaychCreate(ctx context.Context, from, to address.Address, amt types.BigInt) (address.Address, error) PaychList(context.Context) ([]address.Address, error) PaychStatus(context.Context, address.Address) (*PaychStatus, error) - PaychClose(context.Context, address.Address) error - PaychVoucherCheck(context.Context, *types.SignedVoucher) error + PaychClose(context.Context, address.Address) (cid.Cid, error) + PaychVoucherCheckValid(context.Context, address.Address, *types.SignedVoucher) error + PaychVoucherCheckSpendable(context.Context, address.Address, *types.SignedVoucher, []byte, []byte) (bool, error) PaychVoucherCreate(context.Context, address.Address, types.BigInt, uint64) (*types.SignedVoucher, error) - PaychVoucherAdd(context.Context, *types.SignedVoucher) error + PaychVoucherAdd(context.Context, address.Address, *types.SignedVoucher) error PaychVoucherList(context.Context, address.Address) ([]*types.SignedVoucher, error) + PaychVoucherSubmit(context.Context, address.Address, *types.SignedVoucher) (cid.Cid, error) } // Full API is a low-level interface to the Filecoin network storage miner node diff --git a/api/struct.go b/api/struct.go index 4803dfdf4..75cb45f68 100644 --- a/api/struct.go +++ b/api/struct.go @@ -73,14 +73,17 @@ type FullNodeStruct struct { StateMinerSectors func(context.Context, address.Address) ([]*SectorInfo, error) `perm:"read"` StateMinerProvingSet func(context.Context, address.Address) ([]*SectorInfo, error) `perm:"read"` - PaychCreate func(ctx context.Context, from, to address.Address, amt types.BigInt) (address.Address, error) `perm:"sign"` - PaychList func(context.Context) ([]address.Address, error) `perm:"read"` - PaychStatus func(context.Context, address.Address) (*PaychStatus, error) `perm:"read"` - PaychClose func(context.Context, address.Address) error `perm:"sign"` - PaychVoucherCheck func(context.Context, *types.SignedVoucher) error `perm:"read"` - PaychVoucherAdd func(context.Context, *types.SignedVoucher) error `perm:"write"` - PaychVoucherCreate func(context.Context, address.Address, types.BigInt, uint64) (*types.SignedVoucher, error) `perm:"sign"` - PaychVoucherList func(context.Context, address.Address) ([]*types.SignedVoucher, error) `perm:"write"` + PaychCreate func(ctx context.Context, from, to address.Address, amt types.BigInt) (address.Address, error) `perm:"sign"` + PaychList func(context.Context) ([]address.Address, error) `perm:"read"` + PaychStatus func(context.Context, address.Address) (*PaychStatus, error) `perm:"read"` + PaychClose func(context.Context, address.Address) (cid.Cid, error) `perm:"sign"` + PaychVoucherCheck func(context.Context, *types.SignedVoucher) error `perm:"read"` + PaychVoucherCheckValid func(context.Context, address.Address, *types.SignedVoucher) error `perm:"read"` + PaychVoucherCheckSpendable func(context.Context, address.Address, *types.SignedVoucher, []byte, []byte) (bool, error) `perm:"read"` + PaychVoucherAdd func(context.Context, address.Address, *types.SignedVoucher) error `perm:"write"` + PaychVoucherCreate func(context.Context, address.Address, types.BigInt, uint64) (*types.SignedVoucher, error) `perm:"sign"` + PaychVoucherList func(context.Context, address.Address) ([]*types.SignedVoucher, error) `perm:"write"` + PaychVoucherSubmit func(context.Context, address.Address, *types.SignedVoucher) (cid.Cid, error) `perm:"sign"` } } @@ -260,12 +263,16 @@ func (c *FullNodeStruct) PaychStatus(ctx context.Context, pch address.Address) ( return c.Internal.PaychStatus(ctx, pch) } -func (c *FullNodeStruct) PaychVoucherCheck(ctx context.Context, sv *types.SignedVoucher) error { - return c.Internal.PaychVoucherCheck(ctx, sv) +func (c *FullNodeStruct) PaychVoucherCheckValid(ctx context.Context, addr address.Address, sv *types.SignedVoucher) error { + return c.Internal.PaychVoucherCheckValid(ctx, addr, sv) } -func (c *FullNodeStruct) PaychVoucherAdd(ctx context.Context, sv *types.SignedVoucher) error { - return c.Internal.PaychVoucherAdd(ctx, sv) +func (c *FullNodeStruct) PaychVoucherCheckSpendable(ctx context.Context, addr address.Address, sv *types.SignedVoucher, secret []byte, proof []byte) (bool, error) { + return c.Internal.PaychVoucherCheckSpendable(ctx, addr, sv, secret, proof) +} + +func (c *FullNodeStruct) PaychVoucherAdd(ctx context.Context, addr address.Address, sv *types.SignedVoucher) error { + return c.Internal.PaychVoucherAdd(ctx, addr, sv) } func (c *FullNodeStruct) PaychVoucherCreate(ctx context.Context, pch address.Address, amt types.BigInt, lane uint64) (*types.SignedVoucher, error) { @@ -276,10 +283,14 @@ func (c *FullNodeStruct) PaychVoucherList(ctx context.Context, pch address.Addre return c.Internal.PaychVoucherList(ctx, pch) } -func (c *FullNodeStruct) PaychClose(ctx context.Context, a address.Address) error { +func (c *FullNodeStruct) PaychClose(ctx context.Context, a address.Address) (cid.Cid, error) { return c.Internal.PaychClose(ctx, a) } +func (c *FullNodeStruct) PaychVoucherSubmit(ctx context.Context, ch address.Address, sv *types.SignedVoucher) (cid.Cid, error) { + return c.Internal.PaychVoucherSubmit(ctx, ch, sv) +} + func (c *StorageMinerStruct) ActorAddresses(ctx context.Context) ([]address.Address, error) { return c.Internal.ActorAddresses(ctx) } diff --git a/chain/types/voucher.go b/chain/types/voucher.go index c4e828c1a..5403572e5 100644 --- a/chain/types/voucher.go +++ b/chain/types/voucher.go @@ -1,6 +1,8 @@ package types import ( + "encoding/base64" + "github.com/filecoin-project/go-lotus/chain/address" cbor "github.com/ipfs/go-ipld-cbor" ) @@ -31,6 +33,29 @@ func (sv *SignedVoucher) SigningBytes() ([]byte, error) { return cbor.DumpObject(osv) } +func (sv *SignedVoucher) EncodedString() (string, error) { + data, err := cbor.DumpObject(sv) + if err != nil { + return "", err + } + + return base64.RawURLEncoding.EncodeToString(data), nil +} + +func DecodeSignedVoucher(s string) (*SignedVoucher, error) { + data, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + return nil, err + } + + var sv SignedVoucher + if err := cbor.DecodeInto(data, &sv); err != nil { + return nil, err + } + + return &sv, nil +} + type Merge struct { Lane uint64 Nonce uint64 diff --git a/chain/vm/call.go b/chain/vm/call.go new file mode 100644 index 000000000..920ae2505 --- /dev/null +++ b/chain/vm/call.go @@ -0,0 +1,59 @@ +package vm + +import ( + "context" + + "github.com/filecoin-project/go-lotus/chain/actors" + "github.com/filecoin-project/go-lotus/chain/store" + "github.com/filecoin-project/go-lotus/chain/types" + "golang.org/x/xerrors" +) + +func Call(ctx context.Context, cs *store.ChainStore, msg *types.Message, ts *types.TipSet) (*types.MessageReceipt, error) { + if ts == nil { + ts = cs.GetHeaviestTipSet() + } + state, err := cs.TipSetState(ts.Cids()) + if err != nil { + return nil, err + } + + vmi, err := NewVM(state, ts.Height(), ts.Blocks()[0].Miner, cs) + if err != nil { + return nil, xerrors.Errorf("failed to set up vm: %w", err) + } + + if msg.GasLimit == types.EmptyInt { + msg.GasLimit = types.NewInt(10000000000) + } + if msg.GasPrice == types.EmptyInt { + msg.GasPrice = types.NewInt(0) + } + if msg.Value == types.EmptyInt { + msg.Value = types.NewInt(0) + } + if msg.Params == nil { + msg.Params, err = actors.SerializeParams(struct{}{}) + if err != nil { + return nil, err + } + } + + fromActor, err := vmi.cstate.GetActor(msg.From) + if err != nil { + return nil, err + } + + msg.Nonce = fromActor.Nonce + + // TODO: maybe just use the invoker directly? + ret, err := vmi.ApplyMessage(ctx, msg) + if err != nil { + return nil, xerrors.Errorf("apply message failed: %w", err) + } + + if ret.ActorErr != nil { + log.Warnf("chain call failed: %s", ret.ActorErr) + } + return &ret.MessageReceipt, nil +} diff --git a/cli/cmd.go b/cli/cmd.go index a5c2abc95..74988cce9 100644 --- a/cli/cmd.go +++ b/cli/cmd.go @@ -116,12 +116,13 @@ func ReqContext(cctx *cli.Context) context.Context { var Commands = []*cli.Command{ chainCmd, clientCmd, + createMinerCmd, minerCmd, mpoolCmd, netCmd, + paychCmd, + sendCmd, + stateCmd, versionCmd, walletCmd, - createMinerCmd, - stateCmd, - sendCmd, } diff --git a/cli/paych.go b/cli/paych.go new file mode 100644 index 000000000..8cbf66274 --- /dev/null +++ b/cli/paych.go @@ -0,0 +1,342 @@ +package cli + +import ( + "fmt" + + "github.com/filecoin-project/go-lotus/chain/address" + types "github.com/filecoin-project/go-lotus/chain/types" + "gopkg.in/urfave/cli.v2" +) + +var paychCmd = &cli.Command{ + Name: "paych", + Usage: "Manage payment channels", + Subcommands: []*cli.Command{ + paychCreateCmd, + paychListCmd, + paychVoucherCmd, + }, +} + +var paychCreateCmd = &cli.Command{ + Name: "create", + Usage: "Create a new payment channel", + Action: func(cctx *cli.Context) error { + if cctx.Args().Len() != 3 { + return fmt.Errorf("must pass three arguments: ") + } + + from, err := address.NewFromString(cctx.Args().Get(0)) + if err != nil { + return fmt.Errorf("failed to parse from address: %s", err) + } + + to, err := address.NewFromString(cctx.Args().Get(1)) + if err != nil { + return fmt.Errorf("failed to parse to address: %s", err) + } + + amt, err := types.BigFromString(cctx.Args().Get(2)) + if err != nil { + return fmt.Errorf("parsing amount failed: %s", err) + } + + api, err := GetFullNodeAPI(cctx) + if err != nil { + return err + } + + ctx := ReqContext(cctx) + + addr, err := api.PaychCreate(ctx, from, to, amt) + if err != nil { + return err + } + + fmt.Println(addr.String()) + return nil + }, +} + +var paychListCmd = &cli.Command{ + Name: "list", + Usage: "List all locally registered payment channels", + Action: func(cctx *cli.Context) error { + api, err := GetFullNodeAPI(cctx) + if err != nil { + return err + } + + ctx := ReqContext(cctx) + + chs, err := api.PaychList(ctx) + if err != nil { + return err + } + + for _, v := range chs { + fmt.Println(v.String()) + } + return nil + }, +} + +var paychVoucherCmd = &cli.Command{ + Name: "voucher", + Usage: "Interact with payment channel vouchers", + Subcommands: []*cli.Command{ + paychVoucherCreateCmd, + paychVoucherCheckCmd, + paychVoucherAddCmd, + paychVoucherListCmd, + paychVoucherBestSpendableCmd, + paychVoucherSubmitCmd, + }, +} + +var paychVoucherCreateCmd = &cli.Command{ + Name: "create", + Usage: "Create a signed payment channel voucher", + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "lane", + Value: 0, + Usage: "specify payment channel lane to use", + }, + }, + Action: func(cctx *cli.Context) error { + if cctx.Args().Len() != 2 { + return fmt.Errorf("must pass two arguments: ") + } + + ch, err := address.NewFromString(cctx.Args().Get(0)) + if err != nil { + return err + } + + amt, err := types.BigFromString(cctx.Args().Get(1)) + if err != nil { + return err + } + + lane := cctx.Int("lane") + + api, err := GetFullNodeAPI(cctx) + if err != nil { + return err + } + + ctx := ReqContext(cctx) + + sv, err := api.PaychVoucherCreate(ctx, ch, amt, uint64(lane)) + if err != nil { + return err + } + + enc, err := sv.EncodedString() + if err != nil { + return err + } + + fmt.Println(enc) + return nil + }, +} + +var paychVoucherCheckCmd = &cli.Command{ + Name: "check", + Usage: "Check validity of payment channel voucher", + Action: func(cctx *cli.Context) error { + if cctx.Args().Len() != 2 { + return fmt.Errorf("must pass payment channel address and voucher to validate") + } + + ch, err := address.NewFromString(cctx.Args().Get(0)) + if err != nil { + return err + } + + sv, err := types.DecodeSignedVoucher(cctx.Args().Get(1)) + if err != nil { + return err + } + + api, err := GetFullNodeAPI(cctx) + if err != nil { + return err + } + + ctx := ReqContext(cctx) + + if err := api.PaychVoucherCheckValid(ctx, ch, sv); err != nil { + return err + } + + fmt.Println("voucher is valid") + return nil + }, +} + +var paychVoucherAddCmd = &cli.Command{ + Name: "add", + Usage: "Add payment channel voucher to local datastore", + Action: func(cctx *cli.Context) error { + if cctx.Args().Len() != 2 { + return fmt.Errorf("must pass payment channel address and voucher") + } + + ch, err := address.NewFromString(cctx.Args().Get(0)) + if err != nil { + return err + } + + sv, err := types.DecodeSignedVoucher(cctx.Args().Get(1)) + if err != nil { + return err + } + + api, err := GetFullNodeAPI(cctx) + if err != nil { + return err + } + + ctx := ReqContext(cctx) + + if err := api.PaychVoucherAdd(ctx, ch, sv); err != nil { + return err + } + + return nil + }, +} + +var paychVoucherListCmd = &cli.Command{ + Name: "list", + Usage: "List stored vouchers for a given payment channel", + Action: func(cctx *cli.Context) error { + if cctx.Args().Len() != 1 { + return fmt.Errorf("must pass payment channel address") + } + + ch, err := address.NewFromString(cctx.Args().Get(0)) + if err != nil { + return err + } + + api, err := GetFullNodeAPI(cctx) + if err != nil { + return err + } + + ctx := ReqContext(cctx) + + vouchers, err := api.PaychVoucherList(ctx, ch) + if err != nil { + return err + } + + for _, v := range vouchers { + fmt.Printf("Lane %d, Nonce %d: %s\n", v.Lane, v.Nonce, v.Amount.String()) + } + + return nil + }, +} + +var paychVoucherBestSpendableCmd = &cli.Command{ + Name: "best-spendable", + Usage: "Print voucher with highest value that is currently spendable", + Action: func(cctx *cli.Context) error { + if cctx.Args().Len() != 1 { + return fmt.Errorf("must pass payment channel address") + } + + ch, err := address.NewFromString(cctx.Args().Get(0)) + if err != nil { + return err + } + + api, err := GetFullNodeAPI(cctx) + if err != nil { + return err + } + + ctx := ReqContext(cctx) + + vouchers, err := api.PaychVoucherList(ctx, ch) + if err != nil { + return err + } + + var best *types.SignedVoucher + for _, v := range vouchers { + spendable, err := api.PaychVoucherCheckSpendable(ctx, ch, v, nil, nil) + if err != nil { + return err + } + if spendable { + if best == nil || types.BigCmp(v.Amount, best.Amount) > 0 { + best = v + } + } + } + + if best == nil { + return fmt.Errorf("No spendable vouchers for that channel") + } + + enc, err := best.EncodedString() + if err != nil { + return err + } + + fmt.Println(enc) + fmt.Printf("Amount: %s\n", best.Amount) + return nil + }, +} + +var paychVoucherSubmitCmd = &cli.Command{ + Name: "submit", + Usage: "Submit voucher to chain to update payment channel state", + Action: func(cctx *cli.Context) error { + if cctx.Args().Len() != 2 { + return fmt.Errorf("must pass payment channel address and voucher") + } + + ch, err := address.NewFromString(cctx.Args().Get(0)) + if err != nil { + return err + } + + sv, err := types.DecodeSignedVoucher(cctx.Args().Get(1)) + if err != nil { + return err + } + + api, err := GetFullNodeAPI(cctx) + if err != nil { + return err + } + + ctx := ReqContext(cctx) + + mcid, err := api.PaychVoucherSubmit(ctx, ch, sv) + if err != nil { + return err + } + + mwait, err := api.ChainWaitMsg(ctx, mcid) + if err != nil { + return err + } + + if mwait.Receipt.ExitCode != 0 { + return fmt.Errorf("message execution failed (exit code %d)", mwait.Receipt.ExitCode) + } + + fmt.Println("channel updated succesfully") + + return nil + }, +} diff --git a/go.mod b/go.mod index 24b63d594..265818c2c 100644 --- a/go.mod +++ b/go.mod @@ -61,6 +61,7 @@ require ( github.com/multiformats/go-multihash v0.0.6 github.com/pkg/errors v0.8.1 github.com/polydawn/refmt v0.0.0-20190809202753-05966cbd336a + github.com/prometheus/common v0.2.0 github.com/smartystreets/assertions v1.0.1 // indirect github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 // indirect github.com/stretchr/testify v1.3.0 diff --git a/go.sum b/go.sum index 8f4946451..f099e195f 100644 --- a/go.sum +++ b/go.sum @@ -19,7 +19,9 @@ github.com/Stebalien/go-bitfield v0.0.0-20180330043415-076a62f9ce6e/go.mod h1:3o github.com/Stebalien/go-bitfield v0.0.1 h1:X3kbSSPUaJK60wV2hjOPZwmpljr6VGCqdq4cBLhbQBo= github.com/Stebalien/go-bitfield v0.0.1/go.mod h1:GNjFpasyUVkHMsfEOk8EFLJ9syQ6SI+XWrX9Wf2XH0s= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/apache/thrift v0.12.0 h1:pODnxUFNcjP9UTLZGTdeh+j16A8lJbRvD3rOtrk/7bs= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= @@ -549,6 +551,7 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= diff --git a/node/builder.go b/node/builder.go index 97b260006..d541f8d2d 100644 --- a/node/builder.go +++ b/node/builder.go @@ -33,6 +33,7 @@ import ( "github.com/filecoin-project/go-lotus/node/modules/lp2p" "github.com/filecoin-project/go-lotus/node/modules/testing" "github.com/filecoin-project/go-lotus/node/repo" + "github.com/filecoin-project/go-lotus/paych" "github.com/filecoin-project/go-lotus/storage" ) @@ -214,6 +215,9 @@ func Online() Option { Override(new(*deals.Client), deals.NewClient), Override(RunDealClientKey, modules.RunDealClient), + + Override(new(*paych.Store), modules.PaychStore), + Override(new(*paych.Manager), modules.PaymentChannelManager), ), // Storage miner diff --git a/node/impl/full.go b/node/impl/full.go index 26d6ed676..607171680 100644 --- a/node/impl/full.go +++ b/node/impl/full.go @@ -3,9 +3,10 @@ package impl import ( "context" "fmt" - "github.com/filecoin-project/go-lotus/lib/bufbstore" "strconv" + "github.com/filecoin-project/go-lotus/lib/bufbstore" + "github.com/filecoin-project/go-lotus/api" "github.com/filecoin-project/go-lotus/chain" "github.com/filecoin-project/go-lotus/chain/actors" @@ -19,6 +20,7 @@ import ( "github.com/filecoin-project/go-lotus/chain/wallet" "github.com/filecoin-project/go-lotus/miner" "github.com/filecoin-project/go-lotus/node/client" + "github.com/filecoin-project/go-lotus/paych" "github.com/ipfs/go-cid" "github.com/ipfs/go-hamt-ipld" @@ -41,6 +43,7 @@ type FullNodeAPI struct { PubSub *pubsub.PubSub Mpool *chain.MessagePool Wallet *wallet.Wallet + PaychMgr *paych.Manager } func (a *FullNodeAPI) ClientStartDeal(ctx context.Context, data cid.Cid, miner address.Address, price types.BigInt, blocksDuration uint64) (*cid.Cid, error) { @@ -155,41 +158,7 @@ func (a *FullNodeAPI) ChainGetBlockReceipts(ctx context.Context, bcid cid.Cid) ( } func (a *FullNodeAPI) ChainCall(ctx context.Context, msg *types.Message, ts *types.TipSet) (*types.MessageReceipt, error) { - if ts == nil { - ts = a.Chain.GetHeaviestTipSet() - } - state, err := a.Chain.TipSetState(ts.Cids()) - if err != nil { - return nil, err - } - - vmi, err := vm.NewVM(state, ts.Height(), ts.Blocks()[0].Miner, a.Chain) - if err != nil { - return nil, xerrors.Errorf("failed to set up vm: %w", err) - } - - if msg.GasLimit == types.EmptyInt { - msg.GasLimit = types.NewInt(10000000000) - } - if msg.GasPrice == types.EmptyInt { - msg.GasPrice = types.NewInt(0) - } - if msg.Value == types.EmptyInt { - msg.Value = types.NewInt(0) - } - if msg.Params == nil { - msg.Params, err = actors.SerializeParams(struct{}{}) - if err != nil { - return nil, err - } - } - - // TODO: maybe just use the invoker directly? - ret, err := vmi.ApplyMessage(ctx, msg) - if ret.ActorErr != nil { - log.Warnf("chain call failed: %s", ret.ActorErr) - } - return &ret.MessageReceipt, err + return vm.Call(ctx, a.Chain, msg, ts) } func (a *FullNodeAPI) stateForTs(ts *types.TipSet) (*state.StateTree, error) { @@ -511,36 +480,160 @@ func (a *FullNodeAPI) PaychCreate(ctx context.Context, from, to address.Address, return address.Undef, err } - // TODO: track this somewhere? + if err := a.PaychMgr.TrackOutboundChannel(ctx, paychaddr); err != nil { + return address.Undef, err + } + return paychaddr, nil } func (a *FullNodeAPI) PaychList(ctx context.Context) ([]address.Address, error) { - panic("nyi") + return a.PaychMgr.ListChannels() } func (a *FullNodeAPI) PaychStatus(ctx context.Context, pch address.Address) (*api.PaychStatus, error) { panic("nyi") } -func (a *FullNodeAPI) PaychClose(ctx context.Context, addr address.Address) error { - panic("nyi") +func (a *FullNodeAPI) PaychClose(ctx context.Context, addr address.Address) (cid.Cid, error) { + ci, err := a.PaychMgr.GetChannelInfo(addr) + if err != nil { + return cid.Undef, err + } + + nonce, err := a.MpoolGetNonce(ctx, ci.ControlAddr) + if err != nil { + return cid.Undef, err + } + + msg := &types.Message{ + To: addr, + From: ci.ControlAddr, + Value: types.NewInt(0), + Method: actors.PCAMethods.Close, + Nonce: nonce, + + GasLimit: types.NewInt(500), + GasPrice: types.NewInt(0), + } + + smsg, err := a.WalletSignMessage(ctx, ci.ControlAddr, msg) + if err != nil { + return cid.Undef, err + } + + if err := a.MpoolPush(ctx, smsg); err != nil { + return cid.Undef, err + } + + return smsg.Cid(), nil } -func (a *FullNodeAPI) PaychVoucherCheck(ctx context.Context, sv *types.SignedVoucher) error { - panic("nyi") +func (a *FullNodeAPI) PaychVoucherCheckValid(ctx context.Context, ch address.Address, sv *types.SignedVoucher) error { + return a.PaychMgr.CheckVoucherValid(ctx, ch, sv) } -func (a *FullNodeAPI) PaychVoucherAdd(ctx context.Context, sv *types.SignedVoucher) error { - panic("nyi") +func (a *FullNodeAPI) PaychVoucherCheckSpendable(ctx context.Context, ch address.Address, sv *types.SignedVoucher, secret []byte, proof []byte) (bool, error) { + return a.PaychMgr.CheckVoucherSpendable(ctx, ch, sv, secret, proof) } +func (a *FullNodeAPI) PaychVoucherAdd(ctx context.Context, ch address.Address, sv *types.SignedVoucher) error { + if err := a.PaychVoucherCheckValid(ctx, ch, sv); err != nil { + return err + } + + return a.PaychMgr.AddVoucher(ctx, ch, sv) +} + +// PaychVoucherCreate creates a new signed voucher on the given payment channel +// with the given lane and amount. The value passed in is exactly the value +// that will be used to create the voucher, so if previous vouchers exist, the +// actual additional value of this voucher will only be the difference between +// the two. func (a *FullNodeAPI) PaychVoucherCreate(ctx context.Context, pch address.Address, amt types.BigInt, lane uint64) (*types.SignedVoucher, error) { - panic("nyi") + ci, err := a.PaychMgr.GetChannelInfo(pch) + if err != nil { + return nil, err + } + + nonce, err := a.PaychMgr.NextNonceForLane(ctx, pch, lane) + if err != nil { + return nil, err + } + + sv := &types.SignedVoucher{ + Lane: lane, + Nonce: nonce, + Amount: amt, + } + + vb, err := sv.SigningBytes() + if err != nil { + return nil, err + } + + sig, err := a.WalletSign(ctx, ci.ControlAddr, vb) + if err != nil { + return nil, err + } + + sv.Signature = sig + + if err := a.PaychMgr.AddVoucher(ctx, pch, sv); err != nil { + return nil, xerrors.Errorf("failed to persist voucher: %w", err) + } + + return sv, nil } func (a *FullNodeAPI) PaychVoucherList(ctx context.Context, pch address.Address) ([]*types.SignedVoucher, error) { - panic("nyi") + return a.PaychMgr.ListVouchers(ctx, pch) +} + +func (a *FullNodeAPI) PaychVoucherSubmit(ctx context.Context, ch address.Address, sv *types.SignedVoucher) (cid.Cid, error) { + ci, err := a.PaychMgr.GetChannelInfo(ch) + if err != nil { + return cid.Undef, err + } + + nonce, err := a.MpoolGetNonce(ctx, ci.ControlAddr) + if err != nil { + return cid.Undef, err + } + + if sv.Extra != nil || len(sv.SecretPreimage) > 0 { + return cid.Undef, fmt.Errorf("cant handle more advanced payment channel stuff yet") + } + + enc, err := actors.SerializeParams(&actors.PCAUpdateChannelStateParams{ + Sv: *sv, + }) + if err != nil { + return cid.Undef, err + } + + msg := &types.Message{ + From: ci.ControlAddr, + To: ch, + Value: types.NewInt(0), + Nonce: nonce, + Method: actors.PCAMethods.UpdateChannelState, + Params: enc, + GasLimit: types.NewInt(100000), + GasPrice: types.NewInt(0), + } + + smsg, err := a.WalletSignMessage(ctx, ci.ControlAddr, msg) + if err != nil { + return cid.Undef, err + } + + if err := a.MpoolPush(ctx, smsg); err != nil { + return cid.Undef, err + } + + // TODO: should we wait for it...? + return smsg.Cid(), nil } var _ api.FullNode = &FullNodeAPI{} diff --git a/node/modules/paych.go b/node/modules/paych.go new file mode 100644 index 000000000..4050d858a --- /dev/null +++ b/node/modules/paych.go @@ -0,0 +1,15 @@ +package modules + +import ( + "github.com/filecoin-project/go-lotus/chain/store" + "github.com/filecoin-project/go-lotus/node/modules/dtypes" + "github.com/filecoin-project/go-lotus/paych" +) + +func PaychStore(ds dtypes.MetadataDS) *paych.Store { + return paych.NewStore(ds) +} + +func PaymentChannelManager(chain *store.ChainStore, store *paych.Store) (*paych.Manager, error) { + return paych.NewManager(chain, store), nil +} diff --git a/paych/paych.go b/paych/paych.go new file mode 100644 index 000000000..43b75a8c7 --- /dev/null +++ b/paych/paych.go @@ -0,0 +1,219 @@ +package paych + +import ( + "context" + "fmt" + + "github.com/filecoin-project/go-lotus/chain/actors" + "github.com/filecoin-project/go-lotus/chain/address" + "github.com/filecoin-project/go-lotus/chain/state" + "github.com/filecoin-project/go-lotus/chain/store" + "github.com/filecoin-project/go-lotus/chain/types" + "github.com/filecoin-project/go-lotus/chain/vm" + + hamt "github.com/ipfs/go-hamt-ipld" +) + +type Manager struct { + chain *store.ChainStore + store *Store +} + +func NewManager(chain *store.ChainStore, pchstore *Store) *Manager { + return &Manager{ + chain: chain, + store: pchstore, + } +} + +func (pm *Manager) TrackInboundChannel(ctx context.Context, ch address.Address) error { + _, st, err := pm.loadPaychState(ctx, ch) + if err != nil { + return err + } + + return pm.store.TrackChannel(&ChannelInfo{ + Channel: ch, + Direction: DirInbound, + ControlAddr: st.To, + }) +} + +func (pm *Manager) TrackOutboundChannel(ctx context.Context, ch address.Address) error { + _, st, err := pm.loadPaychState(ctx, ch) + if err != nil { + return err + } + + return pm.store.TrackChannel(&ChannelInfo{ + Channel: ch, + Direction: DirOutbound, + ControlAddr: st.From, + }) +} + +func (pm *Manager) ListChannels() ([]address.Address, error) { + return pm.store.ListChannels() +} + +func (pm *Manager) GetChannelInfo(addr address.Address) (*ChannelInfo, error) { + return pm.store.getChannelInfo(addr) +} + +// checks if the given voucher is valid (is or could become spendable at some point) +func (pm *Manager) CheckVoucherValid(ctx context.Context, ch address.Address, sv *types.SignedVoucher) error { + act, pca, err := pm.loadPaychState(ctx, ch) + if err != nil { + return err + } + + // verify signature + vb, err := sv.SigningBytes() + if err != nil { + return err + } + + // TODO: technically, either party may create and sign a voucher. + // However, for now, we only accept them from the channel creator. + // More complex handling logic can be added later + if err := sv.Signature.Verify(pca.From, vb); err != nil { + return err + } + + sendAmount := sv.Amount + + // now check the lane state + // TODO: should check against vouchers in our local store too + // there might be something conflicting + ls, ok := pca.LaneStates[fmt.Sprint(sv.Lane)] + if !ok { + } else { + if ls.Closed { + return fmt.Errorf("voucher is on a closed lane") + } + if ls.Nonce >= sv.Nonce { + return fmt.Errorf("nonce too low") + } + + sendAmount = types.BigSub(sv.Amount, ls.Redeemed) + } + + // TODO: also account for vouchers on other lanes we've received + newTotal := types.BigAdd(sendAmount, pca.ToSend) + if types.BigCmp(act.Balance, newTotal) < 0 { + return fmt.Errorf("not enough funds in channel to cover voucher") + } + + if len(sv.Merges) != 0 { + return fmt.Errorf("dont currently support paych lane merges") + } + + return nil +} + +// checks if the given voucher is currently spendable +func (pm *Manager) CheckVoucherSpendable(ctx context.Context, ch address.Address, sv *types.SignedVoucher, secret []byte, proof []byte) (bool, error) { + owner, err := pm.getPaychOwner(ctx, ch) + if err != nil { + return false, err + } + + enc, err := actors.SerializeParams(&actors.PCAUpdateChannelStateParams{ + Sv: *sv, + Secret: secret, + Proof: proof, + }) + if err != nil { + return false, err + } + + ret, err := vm.Call(ctx, pm.chain, &types.Message{ + From: owner, + To: ch, + Method: actors.PCAMethods.UpdateChannelState, + Params: enc, + }, nil) + if err != nil { + return false, err + } + + if ret.ExitCode != 0 { + return false, nil + } + + return true, nil +} + +func (pm *Manager) loadPaychState(ctx context.Context, ch address.Address) (*types.Actor, *actors.PaymentChannelActorState, error) { + st, err := pm.chain.TipSetState(pm.chain.GetHeaviestTipSet().Cids()) + if err != nil { + return nil, nil, err + } + + cst := hamt.CSTFromBstore(pm.chain.Blockstore()) + tree, err := state.LoadStateTree(cst, st) + if err != nil { + return nil, nil, err + } + + act, err := tree.GetActor(ch) + if err != nil { + return nil, nil, err + } + + var pcast actors.PaymentChannelActorState + if err := cst.Get(ctx, act.Head, &pcast); err != nil { + return nil, nil, err + } + + return act, &pcast, nil +} + +func (pm *Manager) getPaychOwner(ctx context.Context, ch address.Address) (address.Address, error) { + ret, err := vm.Call(ctx, pm.chain, &types.Message{ + From: ch, + To: ch, + Method: actors.PCAMethods.GetOwner, + }, nil) + if err != nil { + return address.Undef, err + } + + if ret.ExitCode != 0 { + return address.Undef, fmt.Errorf("failed to get payment channel owner (exit code %d)", ret.ExitCode) + } + + return address.NewFromBytes(ret.Return) +} + +func (pm *Manager) AddVoucher(ctx context.Context, ch address.Address, sv *types.SignedVoucher) error { + if err := pm.CheckVoucherValid(ctx, ch, sv); err != nil { + return err + } + + return pm.store.AddVoucher(ch, sv) +} + +func (pm *Manager) ListVouchers(ctx context.Context, ch address.Address) ([]*types.SignedVoucher, error) { + // TODO: just having a passthrough method like this feels odd. Seems like + // there should be some filtering we're doing here + return pm.store.VouchersForPaych(ch) +} + +func (pm *Manager) NextNonceForLane(ctx context.Context, ch address.Address, lane uint64) (uint64, error) { + vouchers, err := pm.store.VouchersForPaych(ch) + if err != nil { + return 0, err + } + + var maxnonce uint64 + for _, v := range vouchers { + if v.Lane == lane { + if v.Nonce > maxnonce { + maxnonce = v.Nonce + } + } + } + + return maxnonce + 1, nil +} diff --git a/paych/store.go b/paych/store.go new file mode 100644 index 000000000..9198208f9 --- /dev/null +++ b/paych/store.go @@ -0,0 +1,133 @@ +package paych + +import ( + "fmt" + "strings" + + "github.com/filecoin-project/go-lotus/chain/address" + "github.com/filecoin-project/go-lotus/chain/types" + "github.com/ipfs/go-datastore" + "github.com/ipfs/go-datastore/namespace" + dsq "github.com/ipfs/go-datastore/query" + + cbor "github.com/ipfs/go-ipld-cbor" + "golang.org/x/xerrors" +) + +func init() { + cbor.RegisterCborType(ChannelInfo{}) +} + +type Store struct { + ds datastore.Batching +} + +func NewStore(ds datastore.Batching) *Store { + ds = namespace.Wrap(ds, datastore.NewKey("/paych/")) + return &Store{ + ds: ds, + } +} + +const ( + DirInbound = 1 + DirOutbound = 2 +) + +type ChannelInfo struct { + Channel address.Address + ControlAddr address.Address + Direction int + Vouchers []*types.SignedVoucher +} + +func dskeyForChannel(addr address.Address) datastore.Key { + return datastore.NewKey(addr.String()) +} + +func (ps *Store) putChannelInfo(ci *ChannelInfo) error { + k := dskeyForChannel(ci.Channel) + + b, err := cbor.DumpObject(ci) + if err != nil { + return err + } + + return ps.ds.Put(k, b) +} + +func (ps *Store) getChannelInfo(addr address.Address) (*ChannelInfo, error) { + k := dskeyForChannel(addr) + + b, err := ps.ds.Get(k) + if err != nil { + return nil, err + } + + var ci ChannelInfo + if err := cbor.DecodeInto(b, &ci); err != nil { + return nil, err + } + + return &ci, nil +} + +func (ps *Store) TrackChannel(ch *ChannelInfo) error { + _, err := ps.getChannelInfo(ch.Channel) + switch err { + default: + return err + case nil: + return fmt.Errorf("already tracking channel: %s", ch.Channel) + case datastore.ErrNotFound: + return ps.putChannelInfo(ch) + } +} + +func (ps *Store) ListChannels() ([]address.Address, error) { + res, err := ps.ds.Query(dsq.Query{KeysOnly: true}) + if err != nil { + return nil, err + } + + var out []address.Address + for { + res, ok := res.NextSync() + if !ok { + break + } + + if res.Error != nil { + return nil, err + } + + addr, err := address.NewFromString(strings.TrimPrefix(res.Key, "/")) + if err != nil { + return nil, xerrors.Errorf("failed reading paych key (%q) from datastore: %w", res.Key, err) + } + + out = append(out, addr) + } + + return out, nil +} + +func (ps *Store) AddVoucher(ch address.Address, sv *types.SignedVoucher) error { + ci, err := ps.getChannelInfo(ch) + if err != nil { + return err + } + + ci.Vouchers = append(ci.Vouchers, sv) + + return ps.putChannelInfo(ci) +} + +func (ps *Store) VouchersForPaych(ch address.Address) ([]*types.SignedVoucher, error) { + ci, err := ps.getChannelInfo(ch) + if err != nil { + return nil, err + } + + return ci.Vouchers, nil +}