fix: check voucher spendable should take into account submitted vouchers

This commit is contained in:
Dirk McCormick 2020-08-20 12:09:52 -04:00
parent 4c71182c6b
commit 340d11be38
12 changed files with 644 additions and 75 deletions

View File

@ -427,7 +427,7 @@ type FullNode interface {
PaychVoucherCreate(context.Context, address.Address, types.BigInt, uint64) (*paych.SignedVoucher, error) PaychVoucherCreate(context.Context, address.Address, types.BigInt, uint64) (*paych.SignedVoucher, error)
PaychVoucherAdd(context.Context, address.Address, *paych.SignedVoucher, []byte, types.BigInt) (types.BigInt, error) PaychVoucherAdd(context.Context, address.Address, *paych.SignedVoucher, []byte, types.BigInt) (types.BigInt, error)
PaychVoucherList(context.Context, address.Address) ([]*paych.SignedVoucher, 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 { type FileRef struct {

View File

@ -214,7 +214,7 @@ type FullNodeStruct struct {
PaychVoucherAdd func(context.Context, address.Address, *paych.SignedVoucher, []byte, types.BigInt) (types.BigInt, error) `perm:"write"` 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"` 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"` 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) return c.Internal.PaychNewPayment(ctx, from, to, vouchers)
} }
func (c *FullNodeStruct) PaychVoucherSubmit(ctx context.Context, ch address.Address, sv *paych.SignedVoucher) (cid.Cid, error) { 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) return c.Internal.PaychVoucherSubmit(ctx, ch, sv, secret, proof)
} }
// StorageMinerStruct // StorageMinerStruct

View File

@ -468,7 +468,7 @@ var paychVoucherSubmitCmd = &cli.Command{
ctx := ReqContext(cctx) ctx := ReqContext(cctx)
mcid, err := api.PaychVoucherSubmit(ctx, ch, sv) mcid, err := api.PaychVoucherSubmit(ctx, ch, sv, nil, nil)
if err != nil { if err != nil {
return err return err
} }

View File

@ -107,12 +107,14 @@ func TestPaymentChannelVouchers(t *testing.T) {
ctx := context.Background() ctx := context.Background()
nodes, addrs := startTwoNodesOneMiner(ctx, t, blocktime) nodes, addrs := startTwoNodesOneMiner(ctx, t, blocktime)
paymentCreator := nodes[0] paymentCreator := nodes[0]
paymentReceiver := nodes[1]
creatorAddr := addrs[0] creatorAddr := addrs[0]
receiverAddr := addrs[1] receiverAddr := addrs[1]
// Create mock CLI // Create mock CLI
mockCLI := newMockCLI(t) mockCLI := newMockCLI(t)
creatorCLI := mockCLI.client(paymentCreator.ListenAddr) creatorCLI := mockCLI.client(paymentCreator.ListenAddr)
receiverCLI := mockCLI.client(paymentReceiver.ListenAddr)
// creator: paych get <creator> <receiver> <amount> // creator: paych get <creator> <receiver> <amount>
channelAmt := "100000" channelAmt := "100000"
@ -127,43 +129,123 @@ func TestPaymentChannelVouchers(t *testing.T) {
// creator: paych voucher create <channel> <amount> // creator: paych voucher create <channel> <amount>
// Note: implied --lane=0 // Note: implied --lane=0
voucherAmt1 := 100 voucherAmt1 := 100
vamt1 := strconv.Itoa(voucherAmt1) cmd = []string{chAddr.String(), strconv.Itoa(voucherAmt1)}
cmd = []string{chAddr.String(), vamt1}
voucher1 := creatorCLI.runCmd(paychVoucherCreateCmd, cmd) voucher1 := creatorCLI.runCmd(paychVoucherCreateCmd, cmd)
vouchers = append(vouchers, voucherSpec{serialized: voucher1, lane: 0, amt: voucherAmt1}) vouchers = append(vouchers, voucherSpec{serialized: voucher1, lane: 0, amt: voucherAmt1})
// creator: paych voucher create <channel> <amount> --lane=5 // creator: paych voucher create <channel> <amount> --lane=5
lane5 := "--lane=5" lane5 := "--lane=5"
voucherAmt2 := 50 voucherAmt2 := 50
vamt2 := strconv.Itoa(voucherAmt2) cmd = []string{lane5, chAddr.String(), strconv.Itoa(voucherAmt2)}
cmd = []string{lane5, chAddr.String(), vamt2}
voucher2 := creatorCLI.runCmd(paychVoucherCreateCmd, cmd) voucher2 := creatorCLI.runCmd(paychVoucherCreateCmd, cmd)
vouchers = append(vouchers, voucherSpec{serialized: voucher2, lane: 5, amt: voucherAmt2}) vouchers = append(vouchers, voucherSpec{serialized: voucher2, lane: 5, amt: voucherAmt2})
// creator: paych voucher create <channel> <amount> --lane=5 // creator: paych voucher create <channel> <amount> --lane=5
voucherAmt3 := 70 voucherAmt3 := 70
vamt3 := strconv.Itoa(voucherAmt3) cmd = []string{lane5, chAddr.String(), strconv.Itoa(voucherAmt3)}
cmd = []string{lane5, chAddr.String(), vamt3}
voucher3 := creatorCLI.runCmd(paychVoucherCreateCmd, cmd) voucher3 := creatorCLI.runCmd(paychVoucherCreateCmd, cmd)
vouchers = append(vouchers, voucherSpec{serialized: voucher3, lane: 5, amt: voucherAmt3}) vouchers = append(vouchers, voucherSpec{serialized: voucher3, lane: 5, amt: voucherAmt3})
// creator: paych voucher create <channel> <amount> --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 <channel> --export // creator: paych voucher list <channel> --export
cmd = []string{"--export", chAddr.String()} cmd = []string{"--export", chAddr.String()}
list := creatorCLI.runCmd(paychVoucherListCmd, cmd) 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) checkVoucherOutput(t, list, vouchers)
// creator: paych voucher best-spendable <channel> // creator: paych voucher best-spendable <channel>
cmd = []string{"--export", chAddr.String()} cmd = []string{"--export", chAddr.String()}
bestSpendable := creatorCLI.runCmd(paychVoucherBestSpendableCmd, cmd) bestSpendable := creatorCLI.runCmd(paychVoucherBestSpendableCmd, cmd)
// Check that best spendable output is correct // Check that best spendable output is correct on creator
bestVouchers := []voucherSpec{ bestVouchers := []voucherSpec{
{serialized: voucher1, lane: 0, amt: voucherAmt1}, {serialized: voucher1, lane: 0, amt: voucherAmt1},
{serialized: voucher3, lane: 5, amt: voucherAmt3}, {serialized: voucher4, lane: 5, amt: voucherAmt4},
} }
checkVoucherOutput(t, bestSpendable, bestVouchers) checkVoucherOutput(t, bestSpendable, bestVouchers)
// receiver: paych voucher add <voucher>
cmd = []string{chAddr.String(), voucher1}
receiverCLI.runCmd(paychVoucherAddCmd, cmd)
// receiver: paych voucher add <voucher>
cmd = []string{chAddr.String(), voucher2}
receiverCLI.runCmd(paychVoucherAddCmd, cmd)
// receiver: paych voucher add <voucher>
cmd = []string{chAddr.String(), voucher3}
receiverCLI.runCmd(paychVoucherAddCmd, cmd)
// receiver: paych voucher add <voucher>
cmd = []string{chAddr.String(), voucher4}
receiverCLI.runCmd(paychVoucherAddCmd, cmd)
// receiver: paych voucher list <channel> --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 <channel>
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 <channel> <voucher>
cmd = []string{chAddr.String(), voucher1}
receiverCLI.runCmd(paychVoucherSubmitCmd, cmd)
// receiver: paych voucher best-spendable <channel>
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 <channel> <voucher>
cmd = []string{chAddr.String(), voucher2}
receiverCLI.runCmd(paychVoucherSubmitCmd, cmd)
// receiver: paych voucher best-spendable <channel>
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 <channel> <voucher>
cmd = []string{chAddr.String(), voucher4}
receiverCLI.runCmd(paychVoucherSubmitCmd, cmd)
// receiver: paych voucher best-spendable <channel>
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) { 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) listVouchers := make(map[string]string)
for _, line := range lines { for _, line := range lines {
parts := strings.Split(line, ";") parts := strings.Split(line, ";")
serialized := strings.TrimSpace(parts[1]) if len(parts) == 2 {
listVouchers[serialized] = strings.TrimSpace(parts[0]) serialized := strings.TrimSpace(parts[1])
listVouchers[serialized] = strings.TrimSpace(parts[0])
}
} }
for _, vchr := range vouchers { for _, vchr := range vouchers {
res, ok := listVouchers[vchr.serialized] res, ok := listVouchers[vchr.serialized]
require.True(t, ok) require.True(t, ok)
require.Regexp(t, fmt.Sprintf("Lane %d", vchr.lane), res) require.Regexp(t, fmt.Sprintf("Lane %d", vchr.lane), res)
require.Regexp(t, fmt.Sprintf("%d", vchr.amt), res) require.Regexp(t, fmt.Sprintf("%d", vchr.amt), res)
delete(listVouchers, vchr.serialized)
}
for _, vchr := range listVouchers {
require.Fail(t, "Extra voucher "+vchr)
} }
} }

View File

@ -2,18 +2,15 @@ package paych
import ( import (
"context" "context"
"fmt"
"github.com/ipfs/go-cid" "github.com/ipfs/go-cid"
"go.uber.org/fx" "go.uber.org/fx"
"golang.org/x/xerrors" "golang.org/x/xerrors"
"github.com/filecoin-project/go-address" "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/specs-actors/actors/builtin/paych"
"github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/chain/actors"
"github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types"
full "github.com/filecoin-project/lotus/node/impl/full" full "github.com/filecoin-project/lotus/node/impl/full"
"github.com/filecoin-project/lotus/paychmgr" "github.com/filecoin-project/lotus/paychmgr"
@ -183,35 +180,6 @@ func (a *PaychAPI) PaychVoucherList(ctx context.Context, pch address.Address) ([
return out, nil return out, nil
} }
func (a *PaychAPI) PaychVoucherSubmit(ctx context.Context, ch address.Address, sv *paych.SignedVoucher) (cid.Cid, error) { func (a *PaychAPI) PaychVoucherSubmit(ctx context.Context, ch address.Address, sv *paych.SignedVoucher, secret []byte, proof []byte) (cid.Cid, error) {
ci, err := a.PaychMgr.GetChannelInfo(ch) return a.PaychMgr.SubmitVoucher(ctx, ch, sv, secret, proof)
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
} }

View File

@ -19,7 +19,7 @@ func (t *VoucherInfo) MarshalCBOR(w io.Writer) error {
_, err := w.Write(cbg.CborNull) _, err := w.Write(cbg.CborNull)
return err return err
} }
if _, err := w.Write([]byte{162}); err != nil { if _, err := w.Write([]byte{163}); err != nil {
return err return err
} }
@ -64,6 +64,22 @@ func (t *VoucherInfo) MarshalCBOR(w io.Writer) error {
if _, err := w.Write(t.Proof[:]); err != nil { if _, err := w.Write(t.Proof[:]); err != nil {
return err 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 return nil
} }
@ -142,6 +158,24 @@ func (t *VoucherInfo) UnmarshalCBOR(r io.Reader) error {
if _, err := io.ReadFull(br, t.Proof[:]); err != nil { if _, err := io.ReadFull(br, t.Proof[:]); err != nil {
return err 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: default:
return fmt.Errorf("unknown struct field %d: '%s'", i, name) return fmt.Errorf("unknown struct field %d: '%s'", i, name)

View File

@ -285,6 +285,14 @@ func (pm *Manager) trackInboundChannel(ctx context.Context, ch address.Address)
return pm.store.TrackChannel(stateCi) 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) { func (pm *Manager) AllocateLane(ch address.Address) (uint64, error) {
ca, err := pm.accessorByAddress(ch) ca, err := pm.accessorByAddress(ch)
if err != nil { if err != nil {

View File

@ -40,6 +40,7 @@ type mockStateManager struct {
paychState map[address.Address]mockPchState paychState map[address.Address]mockPchState
store adt.Store store adt.Store
response *api.InvocResult response *api.InvocResult
lastCall *types.Message
} }
func newMockStateManager() *mockStateManager { 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)) 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) { 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 return sm.response, nil
} }

View File

@ -152,24 +152,32 @@ func (ca *channelAccessor) checkVoucherSpendable(ctx context.Context, ch address
return false, err 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 { if sv.Extra != nil && proof == nil {
known, err := ca.store.VouchersForPaych(ch) vi, err := ci.infoForVoucher(sv)
if err != nil { if err != nil {
return false, err return false, err
} }
for _, v := range known { if vi.Proof != nil {
eq, err := cborutil.Equals(v.Voucher, sv) log.Info("CheckVoucherSpendable: using stored proof")
if err != nil { proof = vi.Proof
return false, err } else {
}
if v.Proof != nil && eq {
log.Info("CheckVoucherSpendable: using stored proof")
proof = v.Proof
break
}
}
if proof == nil {
log.Warn("CheckVoucherSpendable: nil proof for voucher with validation") 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) 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) { func (ca *channelAccessor) allocateLane(ch address.Address) (uint64, error) {
ca.lk.Lock() ca.lk.Lock()
defer ca.lk.Unlock() defer ca.lk.Unlock()

View File

@ -1,9 +1,12 @@
package paychmgr package paychmgr
import ( import (
"bytes"
"context" "context"
"testing" "testing"
"github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/specs-actors/actors/builtin" "github.com/filecoin-project/specs-actors/actors/builtin"
"github.com/filecoin-project/specs-actors/actors/util/adt" "github.com/filecoin-project/specs-actors/actors/util/adt"
"github.com/ipfs/go-cid" "github.com/ipfs/go-cid"
@ -382,7 +385,7 @@ func TestAddVoucherDelta(t *testing.T) {
ctx := context.Background() ctx := context.Background()
// Set up a manager with a single payment channel // Set up a manager with a single payment channel
mgr, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t) mgr, _, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t)
voucherLane := uint64(1) voucherLane := uint64(1)
@ -424,7 +427,7 @@ func TestAddVoucherNextLane(t *testing.T) {
ctx := context.Background() ctx := context.Background()
// Set up a manager with a single payment channel // 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) minDelta := big.NewInt(0)
voucherAmount := big.NewInt(2) voucherAmount := big.NewInt(2)
@ -474,7 +477,7 @@ func TestAllocateLane(t *testing.T) {
ctx := context.Background() ctx := context.Background()
// Set up a manager with a single payment channel // Set up a manager with a single payment channel
mgr, ch, _ := testSetupMgrWithChannel(ctx, t) mgr, _, ch, _ := testSetupMgrWithChannel(ctx, t)
// First lane should be 0 // First lane should be 0
lane, err := mgr.AllocateLane(ch) lane, err := mgr.AllocateLane(ch)
@ -550,7 +553,7 @@ func TestAddVoucherProof(t *testing.T) {
ctx := context.Background() ctx := context.Background()
// Set up a manager with a single payment channel // Set up a manager with a single payment channel
mgr, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t) mgr, _, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t)
nonce := uint64(1) nonce := uint64(1)
voucherAmount := big.NewInt(1) voucherAmount := big.NewInt(1)
@ -656,11 +659,254 @@ func TestAddVoucherInboundWalletKey(t *testing.T) {
require.NoError(t, err) 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) { func TestNextNonceForLane(t *testing.T) {
ctx := context.Background() ctx := context.Background()
// Set up a manager with a single payment channel // 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 // Expect next nonce for non-existent lane to be 1
next, err := mgr.NextNonceForLane(ctx, ch, 1) next, err := mgr.NextNonceForLane(ctx, ch, 1)
@ -700,7 +946,7 @@ func TestNextNonceForLane(t *testing.T) {
require.EqualValues(t, next, 8) 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) fromKeyPrivate, fromKeyPublic := testGenerateKeyPair(t)
ch := tutils.NewIDAddr(t, 100) ch := tutils.NewIDAddr(t, 100)
@ -745,7 +991,7 @@ func testSetupMgrWithChannel(ctx context.Context, t *testing.T) (*Manager, addre
err = mgr.store.putChannelInfo(ci) err = mgr.store.putChannelInfo(ci)
require.NoError(t, err) require.NoError(t, err)
return mgr, ch, fromKeyPrivate return mgr, mock, ch, fromKeyPrivate
} }
func testGenerateKeyPair(t *testing.T) ([]byte, []byte) { 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 sv.Signature = sig
return sv 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}
}

View File

@ -40,7 +40,7 @@ type settlerAPI interface {
PaychStatus(context.Context, address.Address) (*api.PaychStatus, error) PaychStatus(context.Context, address.Address) (*api.PaychStatus, error)
PaychVoucherCheckSpendable(context.Context, address.Address, *paych.SignedVoucher, []byte, []byte) (bool, error) PaychVoucherCheckSpendable(context.Context, address.Address, *paych.SignedVoucher, []byte, []byte) (bool, error)
PaychVoucherList(context.Context, address.Address) ([]*paych.SignedVoucher, 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) 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 var wg sync.WaitGroup
wg.Add(len(bestByLane)) wg.Add(len(bestByLane))
for _, voucher := range 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 { if err != nil {
return true, err return true, err
} }

View File

@ -5,10 +5,13 @@ import (
"errors" "errors"
"fmt" "fmt"
"golang.org/x/xerrors"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/filecoin-project/lotus/chain/types" "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/filecoin-project/specs-actors/actors/builtin/paych"
"github.com/ipfs/go-cid" "github.com/ipfs/go-cid"
"github.com/ipfs/go-datastore" "github.com/ipfs/go-datastore"
@ -45,8 +48,9 @@ const (
) )
type VoucherInfo struct { type VoucherInfo struct {
Voucher *paych.SignedVoucher Voucher *paych.SignedVoucher
Proof []byte Proof []byte
Submitted bool
} }
// ChannelInfo keeps track of information about a channel // ChannelInfo keeps track of information about a channel
@ -82,6 +86,64 @@ type ChannelInfo struct {
Settling bool 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 // TrackChannel stores a channel, returning an error if the channel was already
// being tracked // being tracked
func (ps *Store) TrackChannel(ci *ChannelInfo) (*ChannelInfo, error) { 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 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 // ByAddress gets the channel that matches the given address
func (ps *Store) ByAddress(addr address.Address) (*ChannelInfo, error) { func (ps *Store) ByAddress(addr address.Address) (*ChannelInfo, error) {
return ps.findChan(func(ci *ChannelInfo) bool { return ps.findChan(func(ci *ChannelInfo) bool {