Merge pull request #132 from filecoin-project/feat/paych-manager

Implement payment channel manager
This commit is contained in:
Whyrusleeping 2019-08-13 12:13:23 -07:00 committed by GitHub
commit e050d56594
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 973 additions and 65 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -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

59
chain/vm/call.go Normal file
View File

@ -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
}

View File

@ -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,
}

342
cli/paych.go Normal file
View File

@ -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> <to> <amount>")
}
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: <channel> <amount>")
}
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
},
}

1
go.mod
View File

@ -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

3
go.sum
View File

@ -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=

View File

@ -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

View File

@ -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{}

15
node/modules/paych.go Normal file
View File

@ -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
}

219
paych/paych.go Normal file
View File

@ -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
}

133
paych/store.go Normal file
View File

@ -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
}