diff --git a/cli/paych.go b/cli/paych.go index cf00be72c..79613f7d6 100644 --- a/cli/paych.go +++ b/cli/paych.go @@ -4,6 +4,10 @@ import ( "bytes" "encoding/base64" "fmt" + "io" + "sort" + + "github.com/filecoin-project/lotus/paychmgr" "github.com/filecoin-project/go-address" "github.com/filecoin-project/lotus/build" @@ -322,7 +326,7 @@ var paychVoucherListCmd = &cli.Command{ Flags: []cli.Flag{ &cli.BoolFlag{ Name: "export", - Usage: "Print export strings", + Usage: "Print voucher as serialized string", }, }, Action: func(cctx *cli.Context) error { @@ -348,16 +352,11 @@ var paychVoucherListCmd = &cli.Command{ return err } - for _, v := range vouchers { - if cctx.Bool("export") { - enc, err := EncodedString(v) - if err != nil { - return err - } - - fmt.Fprintf(cctx.App.Writer, "Lane %d, Nonce %d: %s; %s\n", v.Lane, v.Nonce, v.Amount.String(), enc) - } else { - fmt.Fprintf(cctx.App.Writer, "Lane %d, Nonce %d: %s\n", v.Lane, v.Nonce, v.Amount.String()) + for _, v := range sortVouchers(vouchers) { + export := cctx.Bool("export") + err := outputVoucher(cctx.App.Writer, v, export) + if err != nil { + return err } } @@ -367,8 +366,14 @@ var paychVoucherListCmd = &cli.Command{ var paychVoucherBestSpendableCmd = &cli.Command{ Name: "best-spendable", - Usage: "Print voucher with highest value that is currently spendable", + Usage: "Print vouchers with highest value that is currently spendable for each lane", ArgsUsage: "[channelAddress]", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "export", + Usage: "Print voucher as serialized string", + }, + }, Action: func(cctx *cli.Context) error { if cctx.Args().Len() != 1 { return ShowHelp(cctx, fmt.Errorf("must pass payment channel address")) @@ -387,37 +392,53 @@ var paychVoucherBestSpendableCmd = &cli.Command{ ctx := ReqContext(cctx) - vouchers, err := api.PaychVoucherList(ctx, ch) + vouchersByLane, err := paychmgr.BestSpendableByLane(ctx, api, ch) if err != nil { return err } - var best *paych.SignedVoucher - for _, v := range vouchers { - spendable, err := api.PaychVoucherCheckSpendable(ctx, ch, v, nil, nil) + var vouchers []*paych.SignedVoucher + for _, vchr := range vouchersByLane { + vouchers = append(vouchers, vchr) + } + for _, best := range sortVouchers(vouchers) { + export := cctx.Bool("export") + err := outputVoucher(cctx.App.Writer, best, export) if err != nil { return err } - if spendable { - if best == nil || v.Amount.GreaterThan(best.Amount) { - best = v - } - } } - if best == nil { - return fmt.Errorf("No spendable vouchers for that channel") - } + return nil + }, +} - enc, err := EncodedString(best) +func sortVouchers(vouchers []*paych.SignedVoucher) []*paych.SignedVoucher { + sort.Slice(vouchers, func(i, j int) bool { + if vouchers[i].Lane == vouchers[j].Lane { + return vouchers[i].Nonce < vouchers[j].Nonce + } + return vouchers[i].Lane < vouchers[j].Lane + }) + return vouchers +} + +func outputVoucher(w io.Writer, v *paych.SignedVoucher, export bool) error { + var enc string + if export { + var err error + enc, err = EncodedString(v) if err != nil { return err } + } - fmt.Fprintln(cctx.App.Writer, enc) - fmt.Fprintf(cctx.App.Writer, "Amount: %s\n", best.Amount) - return nil - }, + fmt.Fprintf(w, "Lane %d, Nonce %d: %s", v.Lane, v.Nonce, v.Amount.String()) + if export { + fmt.Fprintf(w, "; %s", enc) + } + fmt.Fprintln(w) + return nil } var paychVoucherSubmitCmd = &cli.Command{ diff --git a/cli/paych_test.go b/cli/paych_test.go index ed883c228..396db905f 100644 --- a/cli/paych_test.go +++ b/cli/paych_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "flag" + "fmt" "os" "strconv" "strings" @@ -49,6 +50,139 @@ func TestPaymentChannels(t *testing.T) { blocktime := 5 * time.Millisecond ctx := context.Background() + nodes, addrs := startTwoNodesOneMiner(ctx, t, blocktime) + paymentCreator := nodes[0] + paymentReceiver := nodes[0] + creatorAddr := addrs[0] + receiverAddr := addrs[1] + + // Create mock CLI + mockCLI := newMockCLI(t) + creatorCLI := mockCLI.client(paymentCreator.ListenAddr) + receiverCLI := mockCLI.client(paymentReceiver.ListenAddr) + + // creator: paych get + channelAmt := "100000" + cmd := []string{creatorAddr.String(), receiverAddr.String(), channelAmt} + chstr := creatorCLI.runCmd(paychGetCmd, cmd) + + chAddr, err := address.NewFromString(chstr) + require.NoError(t, err) + + // creator: paych voucher create + voucherAmt := 100 + vamt := strconv.Itoa(voucherAmt) + cmd = []string{chAddr.String(), vamt} + voucher := creatorCLI.runCmd(paychVoucherCreateCmd, cmd) + + // receiver: paych voucher add + cmd = []string{chAddr.String(), voucher} + receiverCLI.runCmd(paychVoucherAddCmd, cmd) + + // creator: paych settle + cmd = []string{chAddr.String()} + creatorCLI.runCmd(paychSettleCmd, cmd) + + // Wait for the chain to reach the settle height + chState := getPaychState(ctx, t, paymentReceiver, chAddr) + waitForHeight(ctx, t, paymentReceiver, chState.SettlingAt) + + // receiver: paych collect + cmd = []string{chAddr.String()} + receiverCLI.runCmd(paychCloseCmd, cmd) +} + +type voucherSpec struct { + serialized string + amt int + lane int +} + +// TestPaymentChannelVouchers does a basic test to exercise some payment +// channel voucher commands +func TestPaymentChannelVouchers(t *testing.T) { + _ = os.Setenv("BELLMAN_NO_GPU", "1") + + blocktime := 5 * time.Millisecond + ctx := context.Background() + nodes, addrs := startTwoNodesOneMiner(ctx, t, blocktime) + paymentCreator := nodes[0] + creatorAddr := addrs[0] + receiverAddr := addrs[1] + + // Create mock CLI + mockCLI := newMockCLI(t) + creatorCLI := mockCLI.client(paymentCreator.ListenAddr) + + // creator: paych get + channelAmt := "100000" + cmd := []string{creatorAddr.String(), receiverAddr.String(), channelAmt} + chstr := creatorCLI.runCmd(paychGetCmd, cmd) + + chAddr, err := address.NewFromString(chstr) + require.NoError(t, err) + + var vouchers []voucherSpec + + // creator: paych voucher create + // Note: implied --lane=0 + voucherAmt1 := 100 + vamt1 := strconv.Itoa(voucherAmt1) + cmd = []string{chAddr.String(), vamt1} + voucher1 := creatorCLI.runCmd(paychVoucherCreateCmd, cmd) + vouchers = append(vouchers, voucherSpec{serialized: voucher1, lane: 0, amt: voucherAmt1}) + + // creator: paych voucher create --lane=5 + lane5 := "--lane=5" + voucherAmt2 := 50 + vamt2 := strconv.Itoa(voucherAmt2) + cmd = []string{lane5, chAddr.String(), vamt2} + voucher2 := creatorCLI.runCmd(paychVoucherCreateCmd, cmd) + vouchers = append(vouchers, voucherSpec{serialized: voucher2, lane: 5, amt: voucherAmt2}) + + // creator: paych voucher create --lane=5 + voucherAmt3 := 70 + vamt3 := strconv.Itoa(voucherAmt3) + cmd = []string{lane5, chAddr.String(), vamt3} + voucher3 := creatorCLI.runCmd(paychVoucherCreateCmd, cmd) + vouchers = append(vouchers, voucherSpec{serialized: voucher3, lane: 5, amt: voucherAmt3}) + + // creator: paych voucher list --export + cmd = []string{"--export", chAddr.String()} + list := creatorCLI.runCmd(paychVoucherListCmd, cmd) + + // Check that voucher list output is correct + checkVoucherOutput(t, list, vouchers) + + // creator: paych voucher best-spendable + cmd = []string{"--export", chAddr.String()} + bestSpendable := creatorCLI.runCmd(paychVoucherBestSpendableCmd, cmd) + + // Check that best spendable output is correct + bestVouchers := []voucherSpec{ + {serialized: voucher1, lane: 0, amt: voucherAmt1}, + {serialized: voucher3, lane: 5, amt: voucherAmt3}, + } + checkVoucherOutput(t, bestSpendable, bestVouchers) +} + +func checkVoucherOutput(t *testing.T, list string, vouchers []voucherSpec) { + lines := strings.Split(list, "\n") + listVouchers := make(map[string]string) + for _, line := range lines { + parts := strings.Split(line, ";") + serialized := strings.TrimSpace(parts[1]) + listVouchers[serialized] = strings.TrimSpace(parts[0]) + } + for _, vchr := range vouchers { + res, ok := listVouchers[vchr.serialized] + require.True(t, ok) + require.Regexp(t, fmt.Sprintf("Lane %d", vchr.lane), res) + require.Regexp(t, fmt.Sprintf("%d", vchr.amt), res) + } +} + +func startTwoNodesOneMiner(ctx context.Context, t *testing.T, blocktime time.Duration) ([]test.TestNode, []address.Address) { n, sn := builder.RPCMockSbBuilder(t, 2, test.OneMiner) paymentCreator := n[0] @@ -88,39 +222,7 @@ func TestPaymentChannels(t *testing.T) { } // Create mock CLI - mockCLI := newMockCLI(t) - creatorCLI := mockCLI.client(paymentCreator.ListenAddr) - receiverCLI := mockCLI.client(paymentReceiver.ListenAddr) - - // creator: paych get - channelAmt := "100000" - cmd := []string{creatorAddr.String(), receiverAddr.String(), channelAmt} - chstr := creatorCLI.runCmd(paychGetCmd, cmd) - - chAddr, err := address.NewFromString(chstr) - require.NoError(t, err) - - // creator: paych voucher create - voucherAmt := 100 - vamt := strconv.Itoa(voucherAmt) - cmd = []string{chAddr.String(), vamt} - voucher := creatorCLI.runCmd(paychVoucherCreateCmd, cmd) - - // receiver: paych voucher add - cmd = []string{chAddr.String(), voucher} - receiverCLI.runCmd(paychVoucherAddCmd, cmd) - - // creator: paych settle - cmd = []string{chAddr.String()} - creatorCLI.runCmd(paychSettleCmd, cmd) - - // Wait for the chain to reach the settle height - chState := getPaychState(ctx, t, paymentReceiver, chAddr) - waitForHeight(ctx, t, paymentReceiver, chState.SettlingAt) - - // receiver: paych collect - cmd = []string{chAddr.String()} - receiverCLI.runCmd(paychCloseCmd, cmd) + return n, []address.Address{creatorAddr, receiverAddr} } type mockCLI struct { diff --git a/paychmgr/settler/settler.go b/paychmgr/settler/settler.go index 636684118..cc12068ec 100644 --- a/paychmgr/settler/settler.go +++ b/paychmgr/settler/settler.go @@ -4,6 +4,8 @@ import ( "context" "sync" + "github.com/filecoin-project/lotus/paychmgr" + "go.uber.org/fx" "github.com/ipfs/go-cid" @@ -72,23 +74,10 @@ func (pcs *paymentChannelSettler) check(ts *types.TipSet) (done bool, more bool, } func (pcs *paymentChannelSettler) messageHandler(msg *types.Message, rec *types.MessageReceipt, ts *types.TipSet, curH abi.ChainEpoch) (more bool, err error) { - vouchers, err := pcs.api.PaychVoucherList(pcs.ctx, msg.To) + bestByLane, err := paychmgr.BestSpendableByLane(pcs.ctx, pcs.api, msg.To) if err != nil { return true, err } - - bestByLane := make(map[uint64]*paych.SignedVoucher) - for _, voucher := range vouchers { - spendable, err := pcs.api.PaychVoucherCheckSpendable(pcs.ctx, msg.To, voucher, nil, nil) - if err != nil { - return true, err - } - if spendable { - if bestByLane[voucher.Lane] == nil || voucher.Amount.GreaterThan(bestByLane[voucher.Lane].Amount) { - bestByLane[voucher.Lane] = voucher - } - } - } var wg sync.WaitGroup wg.Add(len(bestByLane)) for _, voucher := range bestByLane { diff --git a/paychmgr/util.go b/paychmgr/util.go new file mode 100644 index 000000000..0509f8a24 --- /dev/null +++ b/paychmgr/util.go @@ -0,0 +1,34 @@ +package paychmgr + +import ( + "context" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/specs-actors/actors/builtin/paych" +) + +type BestSpendableAPI interface { + PaychVoucherList(context.Context, address.Address) ([]*paych.SignedVoucher, error) + PaychVoucherCheckSpendable(context.Context, address.Address, *paych.SignedVoucher, []byte, []byte) (bool, error) +} + +func BestSpendableByLane(ctx context.Context, api BestSpendableAPI, ch address.Address) (map[uint64]*paych.SignedVoucher, error) { + vouchers, err := api.PaychVoucherList(ctx, ch) + if err != nil { + return nil, err + } + + bestByLane := make(map[uint64]*paych.SignedVoucher) + for _, voucher := range vouchers { + spendable, err := api.PaychVoucherCheckSpendable(ctx, ch, voucher, nil, nil) + if err != nil { + return nil, err + } + if spendable { + if bestByLane[voucher.Lane] == nil || voucher.Amount.GreaterThan(bestByLane[voucher.Lane].Amount) { + bestByLane[voucher.Lane] = voucher + } + } + } + return bestByLane, nil +}