package cli import ( "bytes" "context" "flag" "fmt" "os" "regexp" "strconv" "strings" "testing" "time" "github.com/filecoin-project/lotus/build" "github.com/filecoin-project/go-state-types/big" saminer "github.com/filecoin-project/specs-actors/actors/builtin/miner" v0power "github.com/filecoin-project/specs-actors/actors/builtin/power" "github.com/filecoin-project/specs-actors/actors/builtin/verifreg" "github.com/multiformats/go-multiaddr" "github.com/filecoin-project/lotus/chain/events" "github.com/filecoin-project/lotus/api/apibstore" "github.com/filecoin-project/specs-actors/actors/builtin/paych" cbor "github.com/ipfs/go-ipld-cbor" "github.com/filecoin-project/go-address" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/lotus/api/test" "github.com/filecoin-project/lotus/chain/wallet" builder "github.com/filecoin-project/lotus/node/test" "github.com/stretchr/testify/require" "github.com/urfave/cli/v2" ) func init() { v0power.ConsensusMinerMinPower = big.NewInt(2048) saminer.SupportedProofTypes = map[abi.RegisteredSealProof]struct{}{ abi.RegisteredSealProof_StackedDrg2KiBV1: {}, } verifreg.MinVerifiedDealSize = big.NewInt(256) } // TestPaymentChannels does a basic test to exercise the payment channel CLI // commands func TestPaymentChannels(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] paymentReceiver := nodes[1] creatorAddr := addrs[0] receiverAddr := addrs[1] // Create mock CLI mockCLI := newMockCLI(t) creatorCLI := mockCLI.client(paymentCreator.ListenAddr) receiverCLI := mockCLI.client(paymentReceiver.ListenAddr) // creator: paych add-funds channelAmt := "100000" cmd := []string{creatorAddr.String(), receiverAddr.String(), channelAmt} chstr := creatorCLI.runCmd(paychAddFundsCmd, 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 } // TestPaymentChannelStatus tests the payment channel status CLI command func TestPaymentChannelStatus(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) cmd := []string{creatorAddr.String(), receiverAddr.String()} out := creatorCLI.runCmd(paychStatusByFromToCmd, cmd) fmt.Println(out) noChannelState := "Channel does not exist" require.Regexp(t, regexp.MustCompile(noChannelState), out) channelAmt := uint64(100) create := make(chan string) go func() { // creator: paych add-funds cmd = []string{creatorAddr.String(), receiverAddr.String(), fmt.Sprintf("%d", channelAmt)} create <- creatorCLI.runCmd(paychAddFundsCmd, cmd) }() // Wait for the output to stop being "Channel does not exist" for regexp.MustCompile(noChannelState).MatchString(out) { cmd = []string{creatorAddr.String(), receiverAddr.String()} out = creatorCLI.runCmd(paychStatusByFromToCmd, cmd) } fmt.Println(out) // The next state should be creating channel or channel created, depending // on timing stateCreating := regexp.MustCompile("Creating channel").MatchString(out) stateCreated := regexp.MustCompile("Channel exists").MatchString(out) require.True(t, stateCreating || stateCreated) channelAmtAtto := types.BigMul(types.NewInt(channelAmt), types.NewInt(build.FilecoinPrecision)) channelAmtStr := fmt.Sprintf("%d", channelAmtAtto) if stateCreating { // If we're in the creating state (most likely) the amount should be pending require.Regexp(t, regexp.MustCompile("Pending.*"+channelAmtStr), out) } // Wait for create channel to complete chstr := <-create cmd = []string{chstr} out = creatorCLI.runCmd(paychStatusCmd, cmd) fmt.Println(out) // Output should have the channel address require.Regexp(t, regexp.MustCompile("Channel.*"+chstr), out) // Output should have confirmed amount require.Regexp(t, regexp.MustCompile("Confirmed.*"+channelAmtStr), out) chAddr, err := address.NewFromString(chstr) require.NoError(t, err) // creator: paych voucher create voucherAmt := uint64(10) cmd = []string{chAddr.String(), fmt.Sprintf("%d", voucherAmt)} creatorCLI.runCmd(paychVoucherCreateCmd, cmd) cmd = []string{chstr} out = creatorCLI.runCmd(paychStatusCmd, cmd) fmt.Println(out) voucherAmtAtto := types.BigMul(types.NewInt(voucherAmt), types.NewInt(build.FilecoinPrecision)) voucherAmtStr := fmt.Sprintf("%d", voucherAmtAtto) // Output should include voucher amount require.Regexp(t, regexp.MustCompile("Voucher.*"+voucherAmtStr), out) } // 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] paymentReceiver := nodes[1] creatorAddr := addrs[0] receiverAddr := addrs[1] // Create mock CLI mockCLI := newMockCLI(t) creatorCLI := mockCLI.client(paymentCreator.ListenAddr) receiverCLI := mockCLI.client(paymentReceiver.ListenAddr) // creator: paych add-funds channelAmt := "100000" cmd := []string{creatorAddr.String(), receiverAddr.String(), channelAmt} chstr := creatorCLI.runCmd(paychAddFundsCmd, cmd) chAddr, err := address.NewFromString(chstr) require.NoError(t, err) var vouchers []voucherSpec // creator: paych voucher create // Note: implied --lane=0 voucherAmt1 := 100 cmd = []string{chAddr.String(), strconv.Itoa(voucherAmt1)} 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 cmd = []string{lane5, chAddr.String(), strconv.Itoa(voucherAmt2)} voucher2 := creatorCLI.runCmd(paychVoucherCreateCmd, cmd) vouchers = append(vouchers, voucherSpec{serialized: voucher2, lane: 5, amt: voucherAmt2}) // creator: paych voucher create --lane=5 voucherAmt3 := 70 cmd = []string{lane5, chAddr.String(), strconv.Itoa(voucherAmt3)} voucher3 := creatorCLI.runCmd(paychVoucherCreateCmd, cmd) vouchers = append(vouchers, voucherSpec{serialized: voucher3, lane: 5, amt: voucherAmt3}) // creator: paych voucher create --lane=5 voucherAmt4 := 80 cmd = []string{lane5, chAddr.String(), strconv.Itoa(voucherAmt4)} voucher4 := creatorCLI.runCmd(paychVoucherCreateCmd, cmd) vouchers = append(vouchers, voucherSpec{serialized: voucher4, lane: 5, amt: voucherAmt4}) // creator: paych voucher list --export cmd = []string{"--export", chAddr.String()} list := creatorCLI.runCmd(paychVoucherListCmd, cmd) // Check that voucher list output is correct on creator 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 on creator bestVouchers := []voucherSpec{ {serialized: voucher1, lane: 0, amt: voucherAmt1}, {serialized: voucher4, lane: 5, amt: voucherAmt4}, } checkVoucherOutput(t, bestSpendable, bestVouchers) // receiver: paych voucher add cmd = []string{chAddr.String(), voucher1} receiverCLI.runCmd(paychVoucherAddCmd, cmd) // receiver: paych voucher add cmd = []string{chAddr.String(), voucher2} receiverCLI.runCmd(paychVoucherAddCmd, cmd) // receiver: paych voucher add cmd = []string{chAddr.String(), voucher3} receiverCLI.runCmd(paychVoucherAddCmd, cmd) // receiver: paych voucher add cmd = []string{chAddr.String(), voucher4} receiverCLI.runCmd(paychVoucherAddCmd, cmd) // receiver: paych voucher list --export cmd = []string{"--export", chAddr.String()} list = receiverCLI.runCmd(paychVoucherListCmd, cmd) // Check that voucher list output is correct on receiver checkVoucherOutput(t, list, vouchers) // receiver: paych voucher best-spendable cmd = []string{"--export", chAddr.String()} bestSpendable = receiverCLI.runCmd(paychVoucherBestSpendableCmd, cmd) // Check that best spendable output is correct on receiver bestVouchers = []voucherSpec{ {serialized: voucher1, lane: 0, amt: voucherAmt1}, {serialized: voucher4, lane: 5, amt: voucherAmt4}, } checkVoucherOutput(t, bestSpendable, bestVouchers) // receiver: paych voucher submit cmd = []string{chAddr.String(), voucher1} receiverCLI.runCmd(paychVoucherSubmitCmd, cmd) // receiver: paych voucher best-spendable cmd = []string{"--export", chAddr.String()} bestSpendable = receiverCLI.runCmd(paychVoucherBestSpendableCmd, cmd) // Check that best spendable output no longer includes submitted voucher bestVouchers = []voucherSpec{ {serialized: voucher4, lane: 5, amt: voucherAmt4}, } checkVoucherOutput(t, bestSpendable, bestVouchers) // There are three vouchers in lane 5: 50, 70, 80 // Submit the voucher for 50. Best spendable should still be 80. // receiver: paych voucher submit cmd = []string{chAddr.String(), voucher2} receiverCLI.runCmd(paychVoucherSubmitCmd, cmd) // receiver: paych voucher best-spendable cmd = []string{"--export", chAddr.String()} bestSpendable = receiverCLI.runCmd(paychVoucherBestSpendableCmd, cmd) // Check that best spendable output still includes the voucher for 80 bestVouchers = []voucherSpec{ {serialized: voucher4, lane: 5, amt: voucherAmt4}, } checkVoucherOutput(t, bestSpendable, bestVouchers) // Submit the voucher for 80 // receiver: paych voucher submit cmd = []string{chAddr.String(), voucher4} receiverCLI.runCmd(paychVoucherSubmitCmd, cmd) // receiver: paych voucher best-spendable cmd = []string{"--export", chAddr.String()} bestSpendable = receiverCLI.runCmd(paychVoucherBestSpendableCmd, cmd) // Check that best spendable output no longer includes submitted voucher bestVouchers = []voucherSpec{} checkVoucherOutput(t, bestSpendable, bestVouchers) } // TestPaymentChannelVoucherCreateShortfall verifies that if a voucher amount // is greater than what's left in the channel, voucher create fails func TestPaymentChannelVoucherCreateShortfall(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 add-funds channelAmt := 100 cmd := []string{creatorAddr.String(), receiverAddr.String(), fmt.Sprintf("%d", channelAmt)} chstr := creatorCLI.runCmd(paychAddFundsCmd, cmd) chAddr, err := address.NewFromString(chstr) require.NoError(t, err) // creator: paych voucher create --lane=1 voucherAmt1 := 60 lane1 := "--lane=1" cmd = []string{lane1, chAddr.String(), strconv.Itoa(voucherAmt1)} voucher1 := creatorCLI.runCmd(paychVoucherCreateCmd, cmd) fmt.Println(voucher1) // creator: paych voucher create --lane=2 lane2 := "--lane=2" voucherAmt2 := 70 cmd = []string{lane2, chAddr.String(), strconv.Itoa(voucherAmt2)} _, err = creatorCLI.runCmdRaw(paychVoucherCreateCmd, cmd) // Should fail because channel doesn't have required amount require.Error(t, err) shortfall := voucherAmt1 + voucherAmt2 - channelAmt require.Regexp(t, regexp.MustCompile(fmt.Sprintf("shortfall: %d", shortfall)), err.Error()) } 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, ";") if len(parts) == 2 { 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) delete(listVouchers, vchr.serialized) } for _, vchr := range listVouchers { require.Fail(t, "Extra voucher "+vchr) } } 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] paymentReceiver := n[1] miner := sn[0] // Get everyone connected addrs, err := paymentCreator.NetAddrsListen(ctx) if err != nil { t.Fatal(err) } if err := paymentReceiver.NetConnect(ctx, addrs); err != nil { t.Fatal(err) } if err := miner.NetConnect(ctx, addrs); err != nil { t.Fatal(err) } // Start mining blocks bm := test.NewBlockMiner(ctx, t, miner, blocktime) bm.MineBlocks() // Send some funds to register the receiver receiverAddr, err := paymentReceiver.WalletNew(ctx, wallet.ActSigType("secp256k1")) if err != nil { t.Fatal(err) } test.SendFunds(ctx, t, paymentCreator, receiverAddr, abi.NewTokenAmount(1e18)) // Get the creator's address creatorAddr, err := paymentCreator.WalletDefaultAddress(ctx) if err != nil { t.Fatal(err) } // Create mock CLI return n, []address.Address{creatorAddr, receiverAddr} } type mockCLI struct { t *testing.T cctx *cli.Context out *bytes.Buffer } func newMockCLI(t *testing.T) *mockCLI { // Create a CLI App with an --api flag so that we can specify which node // the command should be executed against app := cli.NewApp() app.Flags = []cli.Flag{ &cli.StringFlag{ Name: "api", Hidden: true, }, } var out bytes.Buffer app.Writer = &out app.Setup() cctx := cli.NewContext(app, &flag.FlagSet{}, nil) return &mockCLI{t: t, cctx: cctx, out: &out} } func (c *mockCLI) client(addr multiaddr.Multiaddr) *mockCLIClient { return &mockCLIClient{t: c.t, addr: addr, cctx: c.cctx, out: c.out} } // mockCLIClient runs commands against a particular node type mockCLIClient struct { t *testing.T addr multiaddr.Multiaddr cctx *cli.Context out *bytes.Buffer } func (c *mockCLIClient) runCmd(cmd *cli.Command, input []string) string { out, err := c.runCmdRaw(cmd, input) require.NoError(c.t, err) return out } func (c *mockCLIClient) runCmdRaw(cmd *cli.Command, input []string) (string, error) { // prepend --api= apiFlag := "--api=" + c.addr.String() input = append([]string{apiFlag}, input...) fs := c.flagSet(cmd) err := fs.Parse(input) require.NoError(c.t, err) err = cmd.Action(cli.NewContext(c.cctx.App, fs, c.cctx)) // Get the output str := strings.TrimSpace(c.out.String()) c.out.Reset() return str, err } func (c *mockCLIClient) flagSet(cmd *cli.Command) *flag.FlagSet { // Apply app level flags (so we can process --api flag) fs := &flag.FlagSet{} for _, f := range c.cctx.App.Flags { err := f.Apply(fs) if err != nil { c.t.Fatal(err) } } // Apply command level flags for _, f := range cmd.Flags { err := f.Apply(fs) if err != nil { c.t.Fatal(err) } } return fs } // waitForHeight waits for the node to reach the given chain epoch func waitForHeight(ctx context.Context, t *testing.T, node test.TestNode, height abi.ChainEpoch) { atHeight := make(chan struct{}) chainEvents := events.NewEvents(ctx, node) err := chainEvents.ChainAt(func(ctx context.Context, ts *types.TipSet, curH abi.ChainEpoch) error { close(atHeight) return nil }, nil, 1, height) if err != nil { t.Fatal(err) } select { case <-atHeight: case <-ctx.Done(): } } // getPaychState gets the state of the payment channel with the given address func getPaychState(ctx context.Context, t *testing.T, node test.TestNode, chAddr address.Address) paych.State { act, err := node.StateGetActor(ctx, chAddr, types.EmptyTSK) require.NoError(t, err) store := cbor.NewCborStore(apibstore.NewAPIBlockstore(node)) var chState paych.State err = store.Get(ctx, act.Head, &chState) require.NoError(t, err) return chState }