fix: check voucher spendable should take into account submitted vouchers
This commit is contained in:
parent
4c71182c6b
commit
340d11be38
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user