diff --git a/api/api_full.go b/api/api_full.go index 2d8a4e515..2ce7f79f2 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -427,7 +427,7 @@ type FullNode interface { PaychVoucherCreate(context.Context, address.Address, types.BigInt, uint64) (*paych.SignedVoucher, error) PaychVoucherAdd(context.Context, address.Address, *paych.SignedVoucher, []byte, types.BigInt) (types.BigInt, error) PaychVoucherList(context.Context, address.Address) ([]*paych.SignedVoucher, error) - PaychVoucherSubmit(context.Context, address.Address, *paych.SignedVoucher) (cid.Cid, error) + PaychVoucherSubmit(context.Context, address.Address, *paych.SignedVoucher, []byte, []byte) (cid.Cid, error) } type FileRef struct { diff --git a/api/apistruct/struct.go b/api/apistruct/struct.go index 0b8ba00e4..d80f067c4 100644 --- a/api/apistruct/struct.go +++ b/api/apistruct/struct.go @@ -214,7 +214,7 @@ type FullNodeStruct struct { PaychVoucherAdd func(context.Context, address.Address, *paych.SignedVoucher, []byte, types.BigInt) (types.BigInt, error) `perm:"write"` PaychVoucherCreate func(context.Context, address.Address, big.Int, uint64) (*paych.SignedVoucher, error) `perm:"sign"` PaychVoucherList func(context.Context, address.Address) ([]*paych.SignedVoucher, error) `perm:"write"` - PaychVoucherSubmit func(context.Context, address.Address, *paych.SignedVoucher) (cid.Cid, error) `perm:"sign"` + PaychVoucherSubmit func(context.Context, address.Address, *paych.SignedVoucher, []byte, []byte) (cid.Cid, error) `perm:"sign"` } } @@ -920,8 +920,8 @@ func (c *FullNodeStruct) PaychNewPayment(ctx context.Context, from, to address.A return c.Internal.PaychNewPayment(ctx, from, to, vouchers) } -func (c *FullNodeStruct) PaychVoucherSubmit(ctx context.Context, ch address.Address, sv *paych.SignedVoucher) (cid.Cid, error) { - return c.Internal.PaychVoucherSubmit(ctx, ch, sv) +func (c *FullNodeStruct) PaychVoucherSubmit(ctx context.Context, ch address.Address, sv *paych.SignedVoucher, secret []byte, proof []byte) (cid.Cid, error) { + return c.Internal.PaychVoucherSubmit(ctx, ch, sv, secret, proof) } // StorageMinerStruct diff --git a/cli/paych.go b/cli/paych.go index 79613f7d6..ff4d769da 100644 --- a/cli/paych.go +++ b/cli/paych.go @@ -468,7 +468,7 @@ var paychVoucherSubmitCmd = &cli.Command{ ctx := ReqContext(cctx) - mcid, err := api.PaychVoucherSubmit(ctx, ch, sv) + mcid, err := api.PaychVoucherSubmit(ctx, ch, sv, nil, nil) if err != nil { return err } diff --git a/cli/paych_test.go b/cli/paych_test.go index 396db905f..11a08fb2c 100644 --- a/cli/paych_test.go +++ b/cli/paych_test.go @@ -107,12 +107,14 @@ func TestPaymentChannelVouchers(t *testing.T) { 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 get channelAmt := "100000" @@ -127,43 +129,123 @@ func TestPaymentChannelVouchers(t *testing.T) { // creator: paych voucher create // Note: implied --lane=0 voucherAmt1 := 100 - vamt1 := strconv.Itoa(voucherAmt1) - cmd = []string{chAddr.String(), vamt1} + 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 - vamt2 := strconv.Itoa(voucherAmt2) - cmd = []string{lane5, chAddr.String(), vamt2} + 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 - vamt3 := strconv.Itoa(voucherAmt3) - cmd = []string{lane5, chAddr.String(), vamt3} + 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 + // 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 + // Check that best spendable output is correct on creator bestVouchers := []voucherSpec{ {serialized: voucher1, lane: 0, amt: voucherAmt1}, - {serialized: voucher3, lane: 5, amt: voucherAmt3}, + {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) } func checkVoucherOutput(t *testing.T, list string, vouchers []voucherSpec) { @@ -171,14 +253,20 @@ func checkVoucherOutput(t *testing.T, list string, vouchers []voucherSpec) { listVouchers := make(map[string]string) for _, line := range lines { parts := strings.Split(line, ";") - serialized := strings.TrimSpace(parts[1]) - listVouchers[serialized] = strings.TrimSpace(parts[0]) + 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) } } diff --git a/documentation/en/api-methods.md b/documentation/en/api-methods.md index bd395fc3e..31e5b96e4 100644 --- a/documentation/en/api-methods.md +++ b/documentation/en/api-methods.md @@ -2437,7 +2437,9 @@ Inputs: "Type": 2, "Data": "Ynl0ZSBhcnJheQ==" } - } + }, + "Ynl0ZSBhcnJheQ==", + "Ynl0ZSBhcnJheQ==" ] ``` diff --git a/node/impl/paych/paych.go b/node/impl/paych/paych.go index b9f8005d7..5ce88c501 100644 --- a/node/impl/paych/paych.go +++ b/node/impl/paych/paych.go @@ -2,18 +2,15 @@ package paych import ( "context" - "fmt" "github.com/ipfs/go-cid" "go.uber.org/fx" "golang.org/x/xerrors" "github.com/filecoin-project/go-address" - "github.com/filecoin-project/specs-actors/actors/builtin" "github.com/filecoin-project/specs-actors/actors/builtin/paych" "github.com/filecoin-project/lotus/api" - "github.com/filecoin-project/lotus/chain/actors" "github.com/filecoin-project/lotus/chain/types" full "github.com/filecoin-project/lotus/node/impl/full" "github.com/filecoin-project/lotus/paychmgr" @@ -183,35 +180,6 @@ func (a *PaychAPI) PaychVoucherList(ctx context.Context, pch address.Address) ([ return out, nil } -func (a *PaychAPI) PaychVoucherSubmit(ctx context.Context, ch address.Address, sv *paych.SignedVoucher) (cid.Cid, error) { - ci, err := a.PaychMgr.GetChannelInfo(ch) - 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(&paych.UpdateChannelStateParams{ - Sv: *sv, - }) - if err != nil { - return cid.Undef, err - } - - msg := &types.Message{ - From: ci.Control, - To: ch, - Value: types.NewInt(0), - Method: builtin.MethodsPaych.UpdateChannelState, - Params: enc, - } - - smsg, err := a.MpoolPushMessage(ctx, msg, nil) - if err != nil { - return cid.Undef, err - } - - return smsg.Cid(), nil +func (a *PaychAPI) PaychVoucherSubmit(ctx context.Context, ch address.Address, sv *paych.SignedVoucher, secret []byte, proof []byte) (cid.Cid, error) { + return a.PaychMgr.SubmitVoucher(ctx, ch, sv, secret, proof) } diff --git a/paychmgr/cbor_gen.go b/paychmgr/cbor_gen.go index 1a6d17783..4c9259f06 100644 --- a/paychmgr/cbor_gen.go +++ b/paychmgr/cbor_gen.go @@ -19,7 +19,7 @@ func (t *VoucherInfo) MarshalCBOR(w io.Writer) error { _, err := w.Write(cbg.CborNull) return err } - if _, err := w.Write([]byte{162}); err != nil { + if _, err := w.Write([]byte{163}); err != nil { return err } @@ -64,6 +64,22 @@ func (t *VoucherInfo) MarshalCBOR(w io.Writer) error { if _, err := w.Write(t.Proof[:]); err != nil { return err } + + // t.Submitted (bool) (bool) + if len("Submitted") > cbg.MaxLength { + return xerrors.Errorf("Value in field \"Submitted\" was too long") + } + + if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajTextString, uint64(len("Submitted"))); err != nil { + return err + } + if _, err := io.WriteString(w, string("Submitted")); err != nil { + return err + } + + if err := cbg.WriteBool(w, t.Submitted); err != nil { + return err + } return nil } @@ -142,6 +158,24 @@ func (t *VoucherInfo) UnmarshalCBOR(r io.Reader) error { if _, err := io.ReadFull(br, t.Proof[:]); err != nil { return err } + // t.Submitted (bool) (bool) + case "Submitted": + + maj, extra, err = cbg.CborReadHeaderBuf(br, scratch) + if err != nil { + return err + } + if maj != cbg.MajOther { + return fmt.Errorf("booleans must be major type 7") + } + switch extra { + case 20: + t.Submitted = false + case 21: + t.Submitted = true + default: + return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra) + } default: return fmt.Errorf("unknown struct field %d: '%s'", i, name) diff --git a/paychmgr/manager.go b/paychmgr/manager.go index e0e8de237..b1755a7d3 100644 --- a/paychmgr/manager.go +++ b/paychmgr/manager.go @@ -285,6 +285,14 @@ func (pm *Manager) trackInboundChannel(ctx context.Context, ch address.Address) return pm.store.TrackChannel(stateCi) } +func (pm *Manager) SubmitVoucher(ctx context.Context, ch address.Address, sv *paych.SignedVoucher, secret []byte, proof []byte) (cid.Cid, error) { + ca, err := pm.accessorByAddress(ch) + if err != nil { + return cid.Undef, err + } + return ca.submitVoucher(ctx, ch, sv, secret, proof) +} + func (pm *Manager) AllocateLane(ch address.Address) (uint64, error) { ca, err := pm.accessorByAddress(ch) if err != nil { diff --git a/paychmgr/mock_test.go b/paychmgr/mock_test.go index 1c8674ae4..54519bae2 100644 --- a/paychmgr/mock_test.go +++ b/paychmgr/mock_test.go @@ -40,6 +40,7 @@ type mockStateManager struct { paychState map[address.Address]mockPchState store adt.Store response *api.InvocResult + lastCall *types.Message } func newMockStateManager() *mockStateManager { @@ -93,7 +94,26 @@ func (sm *mockStateManager) LoadActorState(ctx context.Context, a address.Addres panic(fmt.Sprintf("unexpected state type %v", out)) } +func (sm *mockStateManager) setCallResponse(response *api.InvocResult) { + sm.lk.Lock() + defer sm.lk.Unlock() + + sm.response = response +} + +func (sm *mockStateManager) getLastCall() *types.Message { + sm.lk.Lock() + defer sm.lk.Unlock() + + return sm.lastCall +} + func (sm *mockStateManager) Call(ctx context.Context, msg *types.Message, ts *types.TipSet) (*api.InvocResult, error) { + sm.lk.Lock() + defer sm.lk.Unlock() + + sm.lastCall = msg + return sm.response, nil } diff --git a/paychmgr/paych.go b/paychmgr/paych.go index 43e4c128b..0f12adb11 100644 --- a/paychmgr/paych.go +++ b/paychmgr/paych.go @@ -152,24 +152,32 @@ func (ca *channelAccessor) checkVoucherSpendable(ctx context.Context, ch address return false, err } + ci, err := ca.store.ByAddress(ch) + if err != nil { + return false, err + } + + // Check if voucher has already been submitted + submitted, err := ci.wasVoucherSubmitted(sv) + if err != nil { + return false, err + } + if submitted { + return false, nil + } + + // If proof is needed and wasn't supplied as a parameter, get it from the + // datastore if sv.Extra != nil && proof == nil { - known, err := ca.store.VouchersForPaych(ch) + vi, err := ci.infoForVoucher(sv) if err != nil { return false, err } - for _, v := range known { - eq, err := cborutil.Equals(v.Voucher, sv) - if err != nil { - return false, err - } - if v.Proof != nil && eq { - log.Info("CheckVoucherSpendable: using stored proof") - proof = v.Proof - break - } - } - if proof == nil { + if vi.Proof != nil { + log.Info("CheckVoucherSpendable: using stored proof") + proof = vi.Proof + } else { log.Warn("CheckVoucherSpendable: nil proof for voucher with validation") } } @@ -276,6 +284,87 @@ func (ca *channelAccessor) addVoucher(ctx context.Context, ch address.Address, s return delta, ca.store.putChannelInfo(ci) } +func (ca *channelAccessor) submitVoucher(ctx context.Context, ch address.Address, sv *paych.SignedVoucher, secret []byte, proof []byte) (cid.Cid, error) { + ca.lk.Lock() + defer ca.lk.Unlock() + + ci, err := ca.store.ByAddress(ch) + if err != nil { + return cid.Undef, err + } + + // If voucher needs proof, and none was supplied, check datastore for proof + if sv.Extra != nil && proof == nil { + vi, err := ci.infoForVoucher(sv) + if err != nil { + return cid.Undef, err + } + + if vi.Proof != nil { + log.Info("SubmitVoucher: using stored proof") + proof = vi.Proof + } else { + log.Warn("SubmitVoucher: nil proof for voucher with validation") + } + } + + has, err := ci.hasVoucher(sv) + if err != nil { + return cid.Undef, err + } + + // If the channel has the voucher + if has { + // Check that the voucher hasn't already been submitted + submitted, err := ci.wasVoucherSubmitted(sv) + if err != nil { + return cid.Undef, err + } + if submitted { + return cid.Undef, xerrors.Errorf("cannot submit voucher that has already been submitted") + } + } + + enc, err := actors.SerializeParams(&paych.UpdateChannelStateParams{ + Sv: *sv, + Secret: secret, + Proof: proof, + }) + if err != nil { + return cid.Undef, err + } + + msg := &types.Message{ + From: ci.Control, + To: ch, + Value: types.NewInt(0), + Method: builtin.MethodsPaych.UpdateChannelState, + Params: enc, + } + + smsg, err := ca.api.MpoolPushMessage(ctx, msg, nil) + if err != nil { + return cid.Undef, err + } + + // If the channel didn't already have the voucher + if !has { + // Add the voucher to the channel + ci.Vouchers = append(ci.Vouchers, &VoucherInfo{ + Voucher: sv, + Proof: proof, + }) + } + + // Mark the voucher and any lower-nonce vouchers as having been submitted + err = ca.store.MarkVoucherSubmitted(ci, sv) + if err != nil { + return cid.Undef, err + } + + return smsg.Cid(), nil +} + func (ca *channelAccessor) allocateLane(ch address.Address) (uint64, error) { ca.lk.Lock() defer ca.lk.Unlock() diff --git a/paychmgr/paych_test.go b/paychmgr/paych_test.go index 455162cd3..4439fb2fb 100644 --- a/paychmgr/paych_test.go +++ b/paychmgr/paych_test.go @@ -1,9 +1,12 @@ package paychmgr import ( + "bytes" "context" "testing" + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/specs-actors/actors/builtin" "github.com/filecoin-project/specs-actors/actors/util/adt" "github.com/ipfs/go-cid" @@ -382,7 +385,7 @@ func TestAddVoucherDelta(t *testing.T) { ctx := context.Background() // Set up a manager with a single payment channel - mgr, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t) + mgr, _, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t) voucherLane := uint64(1) @@ -424,7 +427,7 @@ func TestAddVoucherNextLane(t *testing.T) { ctx := context.Background() // Set up a manager with a single payment channel - mgr, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t) + mgr, _, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t) minDelta := big.NewInt(0) voucherAmount := big.NewInt(2) @@ -474,7 +477,7 @@ func TestAllocateLane(t *testing.T) { ctx := context.Background() // Set up a manager with a single payment channel - mgr, ch, _ := testSetupMgrWithChannel(ctx, t) + mgr, _, ch, _ := testSetupMgrWithChannel(ctx, t) // First lane should be 0 lane, err := mgr.AllocateLane(ch) @@ -550,7 +553,7 @@ func TestAddVoucherProof(t *testing.T) { ctx := context.Background() // Set up a manager with a single payment channel - mgr, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t) + mgr, _, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t) nonce := uint64(1) voucherAmount := big.NewInt(1) @@ -656,11 +659,254 @@ func TestAddVoucherInboundWalletKey(t *testing.T) { require.NoError(t, err) } +func TestBestSpendable(t *testing.T) { + ctx := context.Background() + + // Set up a manager with a single payment channel + mgr, mock, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t) + + // Add vouchers to lane 1 with amounts: [1, 2, 3] + voucherLane := uint64(1) + minDelta := big.NewInt(0) + nonce := uint64(1) + voucherAmount := big.NewInt(1) + svL1V1 := createTestVoucher(t, ch, voucherLane, nonce, voucherAmount, fromKeyPrivate) + _, err := mgr.AddVoucherInbound(ctx, ch, svL1V1, nil, minDelta) + require.NoError(t, err) + + nonce++ + voucherAmount = big.NewInt(2) + svL1V2 := createTestVoucher(t, ch, voucherLane, nonce, voucherAmount, fromKeyPrivate) + _, err = mgr.AddVoucherInbound(ctx, ch, svL1V2, nil, minDelta) + require.NoError(t, err) + + nonce++ + voucherAmount = big.NewInt(3) + svL1V3 := createTestVoucher(t, ch, voucherLane, nonce, voucherAmount, fromKeyPrivate) + _, err = mgr.AddVoucherInbound(ctx, ch, svL1V3, nil, minDelta) + require.NoError(t, err) + + // Add voucher to lane 2 with amounts: [2] + voucherLane = uint64(2) + nonce = uint64(1) + voucherAmount = big.NewInt(2) + svL2V1 := createTestVoucher(t, ch, voucherLane, nonce, voucherAmount, fromKeyPrivate) + _, err = mgr.AddVoucherInbound(ctx, ch, svL2V1, nil, minDelta) + require.NoError(t, err) + + // Return success exit code from calls to check if voucher is spendable + bsapi := newMockBestSpendableAPI(mgr) + mock.setCallResponse(&api.InvocResult{ + MsgRct: &types.MessageReceipt{ + ExitCode: 0, + }, + }) + + // Verify best spendable vouchers on each lane + vouchers, err := BestSpendableByLane(ctx, bsapi, ch) + require.NoError(t, err) + require.Len(t, vouchers, 2) + + vchr, ok := vouchers[1] + require.True(t, ok) + require.EqualValues(t, 3, vchr.Amount.Int64()) + + vchr, ok = vouchers[2] + require.True(t, ok) + require.EqualValues(t, 2, vchr.Amount.Int64()) + + // Submit voucher from lane 2 + _, err = mgr.SubmitVoucher(ctx, ch, svL2V1, nil, nil) + require.NoError(t, err) + + // Best spendable voucher should no longer include lane 2 + // (because voucher has not been submitted) + vouchers, err = BestSpendableByLane(ctx, bsapi, ch) + require.NoError(t, err) + require.Len(t, vouchers, 1) + + // Submit first voucher from lane 1 + _, err = mgr.SubmitVoucher(ctx, ch, svL1V1, nil, nil) + require.NoError(t, err) + + // Best spendable voucher for lane 1 should still be highest value voucher + vouchers, err = BestSpendableByLane(ctx, bsapi, ch) + require.NoError(t, err) + require.Len(t, vouchers, 1) + + vchr, ok = vouchers[1] + require.True(t, ok) + require.EqualValues(t, 3, vchr.Amount.Int64()) +} + +func TestCheckSpendable(t *testing.T) { + ctx := context.Background() + + // Set up a manager with a single payment channel + mgr, mock, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t) + + // Create voucher with Extra + voucherLane := uint64(1) + nonce := uint64(1) + voucherAmount := big.NewInt(1) + voucher := createTestVoucherWithExtra(t, ch, voucherLane, nonce, voucherAmount, fromKeyPrivate) + + // Add voucher with proof + minDelta := big.NewInt(0) + proof := []byte("proof") + _, err := mgr.AddVoucherInbound(ctx, ch, voucher, proof, minDelta) + require.NoError(t, err) + + // Return success exit code from VM call, which indicates that voucher is + // spendable + successResponse := &api.InvocResult{ + MsgRct: &types.MessageReceipt{ + ExitCode: 0, + }, + } + mock.setCallResponse(successResponse) + + // Check that spendable is true + secret := []byte("secret") + otherProof := []byte("other proof") + spendable, err := mgr.CheckVoucherSpendable(ctx, ch, voucher, secret, otherProof) + require.NoError(t, err) + require.True(t, spendable) + + // Check that the secret and proof were passed through correctly + lastCall := mock.getLastCall() + var p paych.UpdateChannelStateParams + err = p.UnmarshalCBOR(bytes.NewReader(lastCall.Params)) + require.NoError(t, err) + require.Equal(t, otherProof, p.Proof) + require.Equal(t, secret, p.Secret) + + // Check that if no proof is supplied, the proof supplied to add voucher + // above is used + secret2 := []byte("secret2") + spendable, err = mgr.CheckVoucherSpendable(ctx, ch, voucher, secret2, nil) + require.NoError(t, err) + require.True(t, spendable) + + lastCall = mock.getLastCall() + var p2 paych.UpdateChannelStateParams + err = p2.UnmarshalCBOR(bytes.NewReader(lastCall.Params)) + require.NoError(t, err) + require.Equal(t, proof, p2.Proof) + require.Equal(t, secret2, p2.Secret) + + // Check that if VM call returns non-success exit code, spendable is false + mock.setCallResponse(&api.InvocResult{ + MsgRct: &types.MessageReceipt{ + ExitCode: 1, + }, + }) + spendable, err = mgr.CheckVoucherSpendable(ctx, ch, voucher, secret, nil) + require.NoError(t, err) + require.False(t, spendable) + + // Return success exit code (indicating voucher is spendable) + mock.setCallResponse(successResponse) + spendable, err = mgr.CheckVoucherSpendable(ctx, ch, voucher, secret, nil) + require.NoError(t, err) + require.True(t, spendable) + + // Check that voucher is no longer spendable once it has been submitted + _, err = mgr.SubmitVoucher(ctx, ch, voucher, nil, nil) + require.NoError(t, err) + + spendable, err = mgr.CheckVoucherSpendable(ctx, ch, voucher, secret, nil) + require.NoError(t, err) + require.False(t, spendable) +} + +func TestSubmitVoucher(t *testing.T) { + ctx := context.Background() + + // Set up a manager with a single payment channel + mgr, mock, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t) + + // Create voucher with Extra + voucherLane := uint64(1) + nonce := uint64(1) + voucherAmount := big.NewInt(1) + voucher := createTestVoucherWithExtra(t, ch, voucherLane, nonce, voucherAmount, fromKeyPrivate) + + // Add voucher with proof + minDelta := big.NewInt(0) + addVoucherProof := []byte("proof") + _, err := mgr.AddVoucherInbound(ctx, ch, voucher, addVoucherProof, minDelta) + require.NoError(t, err) + + // Submit voucher + secret := []byte("secret") + submitProof := []byte("submit proof") + submitCid, err := mgr.SubmitVoucher(ctx, ch, voucher, secret, submitProof) + require.NoError(t, err) + + // Check that the secret and proof were passed through correctly + msg := mock.pushedMessages(submitCid) + var p paych.UpdateChannelStateParams + err = p.UnmarshalCBOR(bytes.NewReader(msg.Message.Params)) + require.NoError(t, err) + require.Equal(t, submitProof, p.Proof) + require.Equal(t, secret, p.Secret) + + // Check that if no proof is supplied to submit voucher, the proof supplied + // to add voucher is used + nonce++ + voucherAmount = big.NewInt(2) + addVoucherProof2 := []byte("proof2") + secret2 := []byte("secret2") + voucher = createTestVoucherWithExtra(t, ch, voucherLane, nonce, voucherAmount, fromKeyPrivate) + _, err = mgr.AddVoucherInbound(ctx, ch, voucher, addVoucherProof2, minDelta) + require.NoError(t, err) + + submitCid, err = mgr.SubmitVoucher(ctx, ch, voucher, secret2, nil) + require.NoError(t, err) + + msg = mock.pushedMessages(submitCid) + var p2 paych.UpdateChannelStateParams + err = p2.UnmarshalCBOR(bytes.NewReader(msg.Message.Params)) + require.NoError(t, err) + require.Equal(t, addVoucherProof2, p2.Proof) + require.Equal(t, secret2, p2.Secret) + + // Submit a voucher without first adding it + nonce++ + voucherAmount = big.NewInt(3) + secret3 := []byte("secret2") + proof3 := []byte("proof3") + voucher = createTestVoucherWithExtra(t, ch, voucherLane, nonce, voucherAmount, fromKeyPrivate) + submitCid, err = mgr.SubmitVoucher(ctx, ch, voucher, secret3, proof3) + require.NoError(t, err) + + msg = mock.pushedMessages(submitCid) + var p3 paych.UpdateChannelStateParams + err = p3.UnmarshalCBOR(bytes.NewReader(msg.Message.Params)) + require.NoError(t, err) + require.Equal(t, proof3, p3.Proof) + require.Equal(t, secret3, p3.Secret) + + // Verify that vouchers are marked as submitted + vis, err := mgr.ListVouchers(ctx, ch) + require.NoError(t, err) + require.Len(t, vis, 3) + + for _, vi := range vis { + require.True(t, vi.Submitted) + } + + // Attempting to submit the same voucher again should fail + _, err = mgr.SubmitVoucher(ctx, ch, voucher, secret2, nil) + require.Error(t, err) +} + func TestNextNonceForLane(t *testing.T) { ctx := context.Background() // Set up a manager with a single payment channel - mgr, ch, key := testSetupMgrWithChannel(ctx, t) + mgr, _, ch, key := testSetupMgrWithChannel(ctx, t) // Expect next nonce for non-existent lane to be 1 next, err := mgr.NextNonceForLane(ctx, ch, 1) @@ -700,7 +946,7 @@ func TestNextNonceForLane(t *testing.T) { require.EqualValues(t, next, 8) } -func testSetupMgrWithChannel(ctx context.Context, t *testing.T) (*Manager, address.Address, []byte) { +func testSetupMgrWithChannel(ctx context.Context, t *testing.T) (*Manager, *mockManagerAPI, address.Address, []byte) { fromKeyPrivate, fromKeyPublic := testGenerateKeyPair(t) ch := tutils.NewIDAddr(t, 100) @@ -745,7 +991,7 @@ func testSetupMgrWithChannel(ctx context.Context, t *testing.T) (*Manager, addre err = mgr.store.putChannelInfo(ci) require.NoError(t, err) - return mgr, ch, fromKeyPrivate + return mgr, mock, ch, fromKeyPrivate } func testGenerateKeyPair(t *testing.T) ([]byte, []byte) { @@ -771,3 +1017,49 @@ func createTestVoucher(t *testing.T, ch address.Address, voucherLane uint64, non sv.Signature = sig return sv } + +func createTestVoucherWithExtra(t *testing.T, ch address.Address, voucherLane uint64, nonce uint64, voucherAmount big.Int, key []byte) *paych.SignedVoucher { + sv := &paych.SignedVoucher{ + ChannelAddr: ch, + Lane: voucherLane, + Nonce: nonce, + Amount: voucherAmount, + Extra: &paych.ModVerifyParams{ + Actor: tutils.NewActorAddr(t, "act"), + }, + } + + signingBytes, err := sv.SigningBytes() + require.NoError(t, err) + sig, err := sigs.Sign(crypto.SigTypeSecp256k1, key, signingBytes) + require.NoError(t, err) + sv.Signature = sig + + return sv +} + +type mockBestSpendableAPI struct { + mgr *Manager +} + +func (m *mockBestSpendableAPI) PaychVoucherList(ctx context.Context, ch address.Address) ([]*paych.SignedVoucher, error) { + vi, err := m.mgr.ListVouchers(ctx, ch) + if err != nil { + return nil, err + } + + out := make([]*paych.SignedVoucher, len(vi)) + for k, v := range vi { + out[k] = v.Voucher + } + + return out, nil +} + +func (m *mockBestSpendableAPI) PaychVoucherCheckSpendable(ctx context.Context, ch address.Address, voucher *paych.SignedVoucher, secret []byte, proof []byte) (bool, error) { + return m.mgr.CheckVoucherSpendable(ctx, ch, voucher, secret, proof) +} + +func newMockBestSpendableAPI(mgr *Manager) BestSpendableAPI { + return &mockBestSpendableAPI{mgr: mgr} +} diff --git a/paychmgr/settler/settler.go b/paychmgr/settler/settler.go index cc12068ec..d5f8bf54e 100644 --- a/paychmgr/settler/settler.go +++ b/paychmgr/settler/settler.go @@ -40,7 +40,7 @@ type settlerAPI interface { PaychStatus(context.Context, address.Address) (*api.PaychStatus, error) PaychVoucherCheckSpendable(context.Context, address.Address, *paych.SignedVoucher, []byte, []byte) (bool, error) PaychVoucherList(context.Context, address.Address) ([]*paych.SignedVoucher, error) - PaychVoucherSubmit(context.Context, address.Address, *paych.SignedVoucher) (cid.Cid, error) + PaychVoucherSubmit(context.Context, address.Address, *paych.SignedVoucher, []byte, []byte) (cid.Cid, error) StateWaitMsg(ctx context.Context, cid cid.Cid, confidence uint64) (*api.MsgLookup, error) } @@ -81,7 +81,7 @@ func (pcs *paymentChannelSettler) messageHandler(msg *types.Message, rec *types. var wg sync.WaitGroup wg.Add(len(bestByLane)) for _, voucher := range bestByLane { - submitMessageCID, err := pcs.api.PaychVoucherSubmit(pcs.ctx, msg.To, voucher) + submitMessageCID, err := pcs.api.PaychVoucherSubmit(pcs.ctx, msg.To, voucher, nil, nil) if err != nil { return true, err } diff --git a/paychmgr/store.go b/paychmgr/store.go index 09790d311..0ae81da57 100644 --- a/paychmgr/store.go +++ b/paychmgr/store.go @@ -5,10 +5,13 @@ import ( "errors" "fmt" + "golang.org/x/xerrors" + "github.com/google/uuid" "github.com/filecoin-project/lotus/chain/types" + cborutil "github.com/filecoin-project/go-cbor-util" "github.com/filecoin-project/specs-actors/actors/builtin/paych" "github.com/ipfs/go-cid" "github.com/ipfs/go-datastore" @@ -45,8 +48,9 @@ const ( ) type VoucherInfo struct { - Voucher *paych.SignedVoucher - Proof []byte + Voucher *paych.SignedVoucher + Proof []byte + Submitted bool } // ChannelInfo keeps track of information about a channel @@ -82,6 +86,64 @@ type ChannelInfo struct { Settling bool } +// infoForVoucher gets the VoucherInfo for the given voucher. +// returns nil if the channel doesn't have the voucher. +func (ci *ChannelInfo) infoForVoucher(sv *paych.SignedVoucher) (*VoucherInfo, error) { + for _, v := range ci.Vouchers { + eq, err := cborutil.Equals(sv, v.Voucher) + if err != nil { + return nil, err + } + if eq { + return v, nil + } + } + return nil, nil +} + +func (ci *ChannelInfo) hasVoucher(sv *paych.SignedVoucher) (bool, error) { + vi, err := ci.infoForVoucher(sv) + return vi != nil, err +} + +// markVoucherSubmitted marks the voucher, and any vouchers of lower nonce +// in the same lane, as being submitted. +// Note: This method doesn't write anything to the store. +func (ci *ChannelInfo) markVoucherSubmitted(sv *paych.SignedVoucher) error { + vi, err := ci.infoForVoucher(sv) + if err != nil { + return err + } + if vi == nil { + return xerrors.Errorf("cannot submit voucher that has not been added to channel") + } + + // Mark the voucher as submitted + vi.Submitted = true + + // Mark lower-nonce vouchers in the same lane as submitted (lower-nonce + // vouchers are superseded by the submitted voucher) + for _, vi := range ci.Vouchers { + if vi.Voucher.Lane == sv.Lane && vi.Voucher.Nonce < sv.Nonce { + vi.Submitted = true + } + } + + return nil +} + +// wasVoucherSubmitted returns true if the voucher has been submitted +func (ci *ChannelInfo) wasVoucherSubmitted(sv *paych.SignedVoucher) (bool, error) { + vi, err := ci.infoForVoucher(sv) + if err != nil { + return false, err + } + if vi == nil { + return false, xerrors.Errorf("cannot submit voucher that has not been added to channel") + } + return vi.Submitted, nil +} + // TrackChannel stores a channel, returning an error if the channel was already // being tracked func (ps *Store) TrackChannel(ci *ChannelInfo) (*ChannelInfo, error) { @@ -200,6 +262,14 @@ func (ps *Store) VouchersForPaych(ch address.Address) ([]*VoucherInfo, error) { return ci.Vouchers, nil } +func (ps *Store) MarkVoucherSubmitted(ci *ChannelInfo, sv *paych.SignedVoucher) error { + err := ci.markVoucherSubmitted(sv) + if err != nil { + return err + } + return ps.putChannelInfo(ci) +} + // ByAddress gets the channel that matches the given address func (ps *Store) ByAddress(addr address.Address) (*ChannelInfo, error) { return ps.findChan(func(ci *ChannelInfo) bool {