diff --git a/api/api.go b/api/api.go index 877468e25..9dbc77bf7 100644 --- a/api/api.go +++ b/api/api.go @@ -100,12 +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 + 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, 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 2e312874f..75cb45f68 100644 --- a/api/struct.go +++ b/api/struct.go @@ -76,13 +76,14 @@ type FullNodeStruct struct { 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"` + 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"` } } @@ -282,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/call.go b/chain/vm/call.go similarity index 77% rename from chain/call.go rename to chain/vm/call.go index 31fd98310..920ae2505 100644 --- a/chain/call.go +++ b/chain/vm/call.go @@ -1,4 +1,4 @@ -package chain +package vm import ( "context" @@ -6,7 +6,6 @@ import ( "github.com/filecoin-project/go-lotus/chain/actors" "github.com/filecoin-project/go-lotus/chain/store" "github.com/filecoin-project/go-lotus/chain/types" - "github.com/filecoin-project/go-lotus/chain/vm" "golang.org/x/xerrors" ) @@ -19,7 +18,7 @@ func Call(ctx context.Context, cs *store.ChainStore, msg *types.Message, ts *typ return nil, err } - vmi, err := vm.NewVM(state, ts.Height(), ts.Blocks()[0].Miner, cs) + 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) } @@ -40,10 +39,21 @@ func Call(ctx context.Context, cs *store.ChainStore, msg *types.Message, ts *typ } } + 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, err + 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/node/impl/full.go b/node/impl/full.go index 676e749c2..ceb00d2b4 100644 --- a/node/impl/full.go +++ b/node/impl/full.go @@ -16,6 +16,7 @@ import ( "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" "github.com/filecoin-project/go-lotus/chain/wallet" "github.com/filecoin-project/go-lotus/miner" "github.com/filecoin-project/go-lotus/node/client" @@ -157,7 +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) { - return chain.Call(ctx, a.Chain, msg, ts) + return vm.Call(ctx, a.Chain, msg, ts) } func (a *FullNodeAPI) stateForTs(ts *types.TipSet) (*state.StateTree, error) { @@ -479,7 +480,10 @@ 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 } @@ -491,15 +495,15 @@ func (a *FullNodeAPI) PaychStatus(ctx context.Context, pch address.Address) (*ap panic("nyi") } -func (a *FullNodeAPI) PaychClose(ctx context.Context, addr address.Address) error { +func (a *FullNodeAPI) PaychClose(ctx context.Context, addr address.Address) (cid.Cid, error) { ci, err := a.PaychMgr.GetChannelInfo(addr) if err != nil { - return err + return cid.Undef, err } nonce, err := a.MpoolGetNonce(ctx, ci.ControlAddr) if err != nil { - return err + return cid.Undef, err } msg := &types.Message{ @@ -513,23 +517,16 @@ func (a *FullNodeAPI) PaychClose(ctx context.Context, addr address.Address) erro GasPrice: types.NewInt(0), } - b, err := msg.Serialize() + smsg, err := a.WalletSignMessage(ctx, ci.ControlAddr, msg) if err != nil { - return err + return cid.Undef, err } - sig, err := a.WalletSign(ctx, ci.ControlAddr, b) - if err != nil { - return err + if err := a.MpoolPush(ctx, smsg); err != nil { + return cid.Undef, err } - smsg := &types.SignedMessage{ - Message: *msg, - Signature: *sig, - } - - // TODO: should this block and wait? - return a.MpoolPush(ctx, smsg) + return smsg.Cid(), nil } func (a *FullNodeAPI) PaychVoucherCheckValid(ctx context.Context, ch address.Address, sv *types.SignedVoucher) error { @@ -591,4 +588,50 @@ func (a *FullNodeAPI) PaychVoucherList(ctx context.Context, pch address.Address) 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/paych/paych.go b/paych/paych.go index 4af4b928f..b95e1e32d 100644 --- a/paych/paych.go +++ b/paych/paych.go @@ -4,12 +4,12 @@ import ( "context" "fmt" - "github.com/filecoin-project/go-lotus/chain" "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" ) @@ -123,7 +123,7 @@ func (pm *Manager) CheckVoucherSpendable(ctx context.Context, ch address.Address return false, err } - ret, err := chain.Call(ctx, pm.chain, &types.Message{ + ret, err := vm.Call(ctx, pm.chain, &types.Message{ From: owner, To: ch, Method: actors.PCAMethods.UpdateChannelState, @@ -166,7 +166,7 @@ func (pm *Manager) loadPaychState(ctx context.Context, ch address.Address) (*typ } func (pm *Manager) getPaychOwner(ctx context.Context, ch address.Address) (address.Address, error) { - ret, err := chain.Call(ctx, pm.chain, &types.Message{ + ret, err := vm.Call(ctx, pm.chain, &types.Message{ From: ch, To: ch, Method: actors.PCAMethods.GetOwner, @@ -211,5 +211,5 @@ func (pm *Manager) NextNonceForLane(ctx context.Context, ch address.Address, lan } } - return maxnonce, nil + return maxnonce + 1, nil } diff --git a/paych/store.go b/paych/store.go index f88bc1fec..9198208f9 100644 --- a/paych/store.go +++ b/paych/store.go @@ -2,6 +2,7 @@ package paych import ( "fmt" + "strings" "github.com/filecoin-project/go-lotus/chain/address" "github.com/filecoin-project/go-lotus/chain/types" @@ -10,6 +11,7 @@ import ( dsq "github.com/ipfs/go-datastore/query" cbor "github.com/ipfs/go-ipld-cbor" + "golang.org/x/xerrors" ) func init() { @@ -95,11 +97,15 @@ func (ps *Store) ListChannels() ([]address.Address, error) { break } - addr, err := address.NewFromString(res.Key) - if err != nil { + 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) }