Merge pull request #3197 from filecoin-project/fix/paych-spendable-submit-voucher

paych: fix issue where `voucher spendable` fails for submitted vouchers
This commit is contained in:
Łukasz Magiera 2020-08-28 22:05:04 +02:00 committed by GitHub
commit 4bcc71c68f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 647 additions and 76 deletions

View File

@ -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 {

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"`
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

View File

@ -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
}

View File

@ -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 <creator> <receiver> <amount>
channelAmt := "100000"
@ -127,43 +129,123 @@ func TestPaymentChannelVouchers(t *testing.T) {
// creator: paych voucher create <channel> <amount>
// 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 <channel> <amount> --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 <channel> <amount> --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 <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
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 <channel>
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 <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) {
@ -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)
}
}

View File

@ -2437,7 +2437,9 @@ Inputs:
"Type": 2,
"Data": "Ynl0ZSBhcnJheQ=="
}
}
},
"Ynl0ZSBhcnJheQ==",
"Ynl0ZSBhcnJheQ=="
]
```

View File

@ -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)
}

View File

@ -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)

View File

@ -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 {

View File

@ -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
}

View File

@ -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()

View File

@ -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}
}

View File

@ -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
}

View File

@ -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 {