feat: PaychAvailableFunds API method

This commit is contained in:
Dirk McCormick 2020-09-01 16:33:44 +02:00
parent b8bbbf3ea3
commit 2c98bf0cc7
14 changed files with 675 additions and 226 deletions

View File

@ -418,6 +418,7 @@ type FullNode interface {
PaychGet(ctx context.Context, from, to address.Address, amt types.BigInt) (*ChannelInfo, error)
PaychGetWaitReady(context.Context, cid.Cid) (address.Address, error)
PaychAvailableFunds(from, to address.Address) (*ChannelAvailableFunds, error)
PaychList(context.Context) ([]address.Address, error)
PaychStatus(context.Context, address.Address) (*PaychStatus, error)
PaychSettle(context.Context, address.Address) (cid.Cid, error)
@ -426,7 +427,7 @@ type FullNode interface {
PaychNewPayment(ctx context.Context, from, to address.Address, vouchers []VoucherSpec) (*PaymentInfo, error)
PaychVoucherCheckValid(context.Context, address.Address, *paych.SignedVoucher) error
PaychVoucherCheckSpendable(context.Context, address.Address, *paych.SignedVoucher, []byte, []byte) (bool, error)
PaychVoucherCreate(context.Context, address.Address, types.BigInt, uint64) (*paych.SignedVoucher, error)
PaychVoucherCreate(context.Context, address.Address, types.BigInt, uint64) (*VoucherCreateResult, 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, []byte, []byte) (cid.Cid, error)
@ -535,6 +536,23 @@ type ChannelInfo struct {
WaitSentinel cid.Cid
}
type ChannelAvailableFunds struct {
Channel *address.Address
// ConfirmedAmt is the amount of funds that have been confirmed on-chain
// for the channel
ConfirmedAmt types.BigInt
// PendingAmt is the amount of funds that are pending confirmation on-chain
PendingAmt types.BigInt
// PendingWaitSentinel can be used with PaychGetWaitReady to wait for
// confirmation of pending funds
PendingWaitSentinel *cid.Cid
// QueuedAmt is the amount that is queued up behind a pending request
QueuedAmt types.BigInt
// VoucherRedeemedAmt is the amount that is redeemed by vouchers on-chain
// and in the local datastore
VoucherReedeemedAmt types.BigInt
}
type PaymentInfo struct {
Channel address.Address
WaitSentinel cid.Cid
@ -550,6 +568,16 @@ type VoucherSpec struct {
Extra *paych.ModVerifyParams
}
// VoucherCreateResult is the response to calling PaychVoucherCreate
type VoucherCreateResult struct {
// Voucher that was created, or nil if there was an error or if there
// were insufficient funds in the channel
Voucher *paych.SignedVoucher
// Shortfall is the additional amount that would be needed in the channel
// in order to be able to create the voucher
Shortfall types.BigInt
}
type MinerPower struct {
MinerPower power.Claim
TotalPower power.Claim

View File

@ -203,6 +203,7 @@ type FullNodeStruct struct {
PaychGet func(ctx context.Context, from, to address.Address, amt types.BigInt) (*api.ChannelInfo, error) `perm:"sign"`
PaychGetWaitReady func(context.Context, cid.Cid) (address.Address, error) `perm:"sign"`
PaychAvailableFunds func(address.Address, address.Address) (*api.ChannelAvailableFunds, error) `perm:"sign"`
PaychList func(context.Context) ([]address.Address, error) `perm:"read"`
PaychStatus func(context.Context, address.Address) (*api.PaychStatus, error) `perm:"read"`
PaychSettle func(context.Context, address.Address) (cid.Cid, error) `perm:"sign"`
@ -213,7 +214,7 @@ type FullNodeStruct struct {
PaychVoucherCheckValid func(context.Context, address.Address, *paych.SignedVoucher) error `perm:"read"`
PaychVoucherCheckSpendable func(context.Context, address.Address, *paych.SignedVoucher, []byte, []byte) (bool, error) `perm:"read"`
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) (*api.VoucherCreateResult, error) `perm:"sign"`
PaychVoucherList func(context.Context, address.Address) ([]*paych.SignedVoucher, error) `perm:"write"`
PaychVoucherSubmit func(context.Context, address.Address, *paych.SignedVoucher, []byte, []byte) (cid.Cid, error) `perm:"sign"`
}
@ -882,6 +883,10 @@ func (c *FullNodeStruct) PaychGetWaitReady(ctx context.Context, sentinel cid.Cid
return c.Internal.PaychGetWaitReady(ctx, sentinel)
}
func (c *FullNodeStruct) PaychAvailableFunds(from address.Address, to address.Address) (*api.ChannelAvailableFunds, error) {
return c.Internal.PaychAvailableFunds(from, to)
}
func (c *FullNodeStruct) PaychList(ctx context.Context) ([]address.Address, error) {
return c.Internal.PaychList(ctx)
}
@ -902,7 +907,7 @@ func (c *FullNodeStruct) PaychVoucherAdd(ctx context.Context, addr address.Addre
return c.Internal.PaychVoucherAdd(ctx, addr, sv, proof, minDelta)
}
func (c *FullNodeStruct) PaychVoucherCreate(ctx context.Context, pch address.Address, amt types.BigInt, lane uint64) (*paych.SignedVoucher, error) {
func (c *FullNodeStruct) PaychVoucherCreate(ctx context.Context, pch address.Address, amt types.BigInt, lane uint64) (*api.VoucherCreateResult, error) {
return c.Internal.PaychVoucherCreate(ctx, pch, amt, lane)
}

View File

@ -96,18 +96,24 @@ func TestPaymentChannels(t *testing.T, b APIBuilder, blocktime time.Duration) {
if err != nil {
t.Fatal(err)
}
if vouch1.Voucher == nil {
t.Fatal(fmt.Errorf("Not enough funds to create voucher: missing %d", vouch1.Shortfall))
}
vouch2, err := paymentCreator.PaychVoucherCreate(ctx, channel, abi.NewTokenAmount(2000), lane)
if err != nil {
t.Fatal(err)
}
delta1, err := paymentReceiver.PaychVoucherAdd(ctx, channel, vouch1, nil, abi.NewTokenAmount(1000))
if vouch2.Voucher == nil {
t.Fatal(fmt.Errorf("Not enough funds to create voucher: missing %d", vouch2.Shortfall))
}
delta1, err := paymentReceiver.PaychVoucherAdd(ctx, channel, vouch1.Voucher, nil, abi.NewTokenAmount(1000))
if err != nil {
t.Fatal(err)
}
if !delta1.Equals(abi.NewTokenAmount(1000)) {
t.Fatal("voucher didn't have the right amount")
}
delta2, err := paymentReceiver.PaychVoucherAdd(ctx, channel, vouch2, nil, abi.NewTokenAmount(1000))
delta2, err := paymentReceiver.PaychVoucherAdd(ctx, channel, vouch2.Voucher, nil, abi.NewTokenAmount(1000))
if err != nil {
t.Fatal(err)
}

View File

@ -217,9 +217,9 @@ var paychVoucherCreateCmd = &cli.Command{
return err
}
amt, err := types.BigFromString(cctx.Args().Get(1))
amt, err := types.ParseFIL(cctx.Args().Get(1))
if err != nil {
return err
return ShowHelp(cctx, fmt.Errorf("parsing amount failed: %s", err))
}
lane := cctx.Int("lane")
@ -232,12 +232,16 @@ var paychVoucherCreateCmd = &cli.Command{
ctx := ReqContext(cctx)
sv, err := api.PaychVoucherCreate(ctx, ch, amt, uint64(lane))
v, err := api.PaychVoucherCreate(ctx, ch, types.BigInt(amt), uint64(lane))
if err != nil {
return err
}
enc, err := EncodedString(sv)
if v.Voucher == nil {
return fmt.Errorf("Could not create voucher: insufficient funds in channel, shortfall: %d", v.Shortfall)
}
enc, err := EncodedString(v.Voucher)
if err != nil {
return err
}

View File

@ -6,6 +6,7 @@ import (
"flag"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"testing"
@ -248,6 +249,50 @@ func TestPaymentChannelVouchers(t *testing.T) {
checkVoucherOutput(t, bestSpendable, bestVouchers)
}
// TestPaymentChannelVoucherCreateShortfall verifies that if a voucher amount
// is greater than what's left in the channel, voucher create fails
func TestPaymentChannelVoucherCreateShortfall(t *testing.T) {
_ = os.Setenv("BELLMAN_NO_GPU", "1")
blocktime := 5 * time.Millisecond
ctx := context.Background()
nodes, addrs := startTwoNodesOneMiner(ctx, t, blocktime)
paymentCreator := nodes[0]
creatorAddr := addrs[0]
receiverAddr := addrs[1]
// Create mock CLI
mockCLI := newMockCLI(t)
creatorCLI := mockCLI.client(paymentCreator.ListenAddr)
// creator: paych get <creator> <receiver> <amount>
channelAmt := 100
cmd := []string{creatorAddr.String(), receiverAddr.String(), fmt.Sprintf("%d", channelAmt)}
chstr := creatorCLI.runCmd(paychGetCmd, cmd)
chAddr, err := address.NewFromString(chstr)
require.NoError(t, err)
// creator: paych voucher create <channel> <amount> --lane=1
voucherAmt1 := 60
lane1 := "--lane=1"
cmd = []string{lane1, chAddr.String(), strconv.Itoa(voucherAmt1)}
voucher1 := creatorCLI.runCmd(paychVoucherCreateCmd, cmd)
fmt.Println(voucher1)
// creator: paych voucher create <channel> <amount> --lane=2
lane2 := "--lane=2"
voucherAmt2 := 70
cmd = []string{lane2, chAddr.String(), strconv.Itoa(voucherAmt2)}
_, err = creatorCLI.runCmdRaw(paychVoucherCreateCmd, cmd)
// Should fail because channel doesn't have required amount
require.Error(t, err)
shortfall := voucherAmt1 + voucherAmt2 - channelAmt
require.Regexp(t, regexp.MustCompile(fmt.Sprintf("shortfall: %d", shortfall)), err.Error())
}
func checkVoucherOutput(t *testing.T, list string, vouchers []voucherSpec) {
lines := strings.Split(list, "\n")
listVouchers := make(map[string]string)
@ -350,6 +395,13 @@ type mockCLIClient struct {
}
func (c *mockCLIClient) runCmd(cmd *cli.Command, input []string) string {
out, err := c.runCmdRaw(cmd, input)
require.NoError(c.t, err)
return out
}
func (c *mockCLIClient) runCmdRaw(cmd *cli.Command, input []string) (string, error) {
// prepend --api=<node api listener address>
apiFlag := "--api=" + c.addr.String()
input = append([]string{apiFlag}, input...)
@ -359,12 +411,11 @@ func (c *mockCLIClient) runCmd(cmd *cli.Command, input []string) string {
require.NoError(c.t, err)
err = cmd.Action(cli.NewContext(c.cctx.App, fs, c.cctx))
require.NoError(c.t, err)
// Get the output
str := strings.TrimSpace(c.out.String())
c.out.Reset()
return str
return str, err
}
func (c *mockCLIClient) flagSet(cmd *cli.Command) *flag.FlagSet {

View File

@ -3,11 +3,14 @@ package retrievaladapter
import (
"context"
"golang.org/x/xerrors"
"github.com/filecoin-project/specs-actors/actors/builtin/paych"
"github.com/filecoin-project/go-address"
"github.com/filecoin-project/go-fil-markets/retrievalmarket"
"github.com/filecoin-project/go-fil-markets/shared"
"github.com/filecoin-project/specs-actors/actors/abi"
"github.com/filecoin-project/specs-actors/actors/builtin/paych"
"github.com/ipfs/go-cid"
"github.com/multiformats/go-multiaddr"
@ -56,7 +59,10 @@ func (rcn *retrievalClientNode) CreatePaymentVoucher(ctx context.Context, paymen
if err != nil {
return nil, err
}
return voucher, nil
if voucher.Voucher == nil {
return nil, xerrors.Errorf("Could not create voucher - shortfall: %d", voucher.Shortfall)
}
return voucher.Voucher, nil
}
func (rcn *retrievalClientNode) GetChainHead(ctx context.Context) (shared.TipSetToken, abi.ChainEpoch, error) {

View File

@ -3,9 +3,10 @@ package paych
import (
"context"
"golang.org/x/xerrors"
"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/paych"
@ -38,6 +39,10 @@ func (a *PaychAPI) PaychGet(ctx context.Context, from, to address.Address, amt t
}, nil
}
func (a *PaychAPI) PaychAvailableFunds(from, to address.Address) (*api.ChannelAvailableFunds, error) {
return a.PaychMgr.AvailableFunds(from, to)
}
func (a *PaychAPI) PaychGetWaitReady(ctx context.Context, sentinel cid.Cid) (address.Address, error) {
return a.PaychMgr.GetPaychWaitReady(ctx, sentinel)
}
@ -64,9 +69,7 @@ func (a *PaychAPI) PaychNewPayment(ctx context.Context, from, to address.Address
svs := make([]*paych.SignedVoucher, len(vouchers))
for i, v := range vouchers {
sv, err := a.paychVoucherCreate(ctx, ch.Channel, paych.SignedVoucher{
ChannelAddr: ch.Channel,
sv, err := a.PaychMgr.CreateVoucher(ctx, ch.Channel, paych.SignedVoucher{
Amount: v.Amount,
Lane: lane,
@ -78,8 +81,11 @@ func (a *PaychAPI) PaychNewPayment(ctx context.Context, from, to address.Address
if err != nil {
return nil, err
}
if sv.Voucher == nil {
return nil, xerrors.Errorf("Could not create voucher - shortfall of %d", sv.Shortfall)
}
svs[i] = sv
svs[i] = sv.Voucher
}
return &api.PaymentInfo{
@ -129,41 +135,10 @@ func (a *PaychAPI) PaychVoucherAdd(ctx context.Context, ch address.Address, sv *
// that will be used to create the voucher, so if previous vouchers exist, the
// actual additional value of this voucher will only be the difference between
// the two.
func (a *PaychAPI) PaychVoucherCreate(ctx context.Context, pch address.Address, amt types.BigInt, lane uint64) (*paych.SignedVoucher, error) {
return a.paychVoucherCreate(ctx, pch, paych.SignedVoucher{ChannelAddr: pch, Amount: amt, Lane: lane})
}
func (a *PaychAPI) paychVoucherCreate(ctx context.Context, pch address.Address, voucher paych.SignedVoucher) (*paych.SignedVoucher, error) {
ci, err := a.PaychMgr.GetChannelInfo(pch)
if err != nil {
return nil, xerrors.Errorf("get channel info: %w", err)
}
nonce, err := a.PaychMgr.NextNonceForLane(ctx, pch, voucher.Lane)
if err != nil {
return nil, xerrors.Errorf("getting next nonce for lane: %w", err)
}
sv := &voucher
sv.Nonce = nonce
vb, err := sv.SigningBytes()
if err != nil {
return nil, err
}
sig, err := a.WalletSign(ctx, ci.Control, vb)
if err != nil {
return nil, err
}
sv.Signature = sig
if _, err := a.PaychMgr.AddVoucherOutbound(ctx, pch, sv, nil, types.NewInt(0)); err != nil {
return nil, xerrors.Errorf("failed to persist voucher: %w", err)
}
return sv, nil
// If there are insufficient funds in the channel to create the voucher,
// returns a nil voucher and the shortfall.
func (a *PaychAPI) PaychVoucherCreate(ctx context.Context, pch address.Address, amt types.BigInt, lane uint64) (*api.VoucherCreateResult, error) {
return a.PaychMgr.CreateVoucher(ctx, pch, paych.SignedVoucher{Amount: amt, Lane: lane})
}
func (a *PaychAPI) PaychVoucherList(ctx context.Context, pch address.Address) ([]*paych.SignedVoucher, error) {

View File

@ -60,7 +60,7 @@ func (pm *Manager) accessorCacheKey(from address.Address, to address.Address) st
// access a channel use the same lock (the lock on the accessor)
func (pm *Manager) addAccessorToCache(from address.Address, to address.Address) *channelAccessor {
key := pm.accessorCacheKey(from, to)
ca := newChannelAccessor(pm)
ca := newChannelAccessor(pm, from, to)
// TODO: Use LRU
pm.channels[key] = ca
return ca

View File

@ -4,6 +4,8 @@ import (
"context"
"sync"
"github.com/filecoin-project/specs-actors/actors/crypto"
"github.com/filecoin-project/lotus/node/modules/helpers"
"github.com/ipfs/go-datastore"
@ -49,6 +51,7 @@ type paychAPI interface {
StateWaitMsg(ctx context.Context, msg cid.Cid, confidence uint64) (*api.MsgLookup, error)
MpoolPushMessage(ctx context.Context, msg *types.Message, maxFee *api.MessageSendSpec) (*types.SignedMessage, error)
WalletHas(ctx context.Context, addr address.Address) (bool, error)
WalletSign(ctx context.Context, k address.Address, msg []byte) (*crypto.Signature, error)
}
// managerAPI defines all methods needed by the manager
@ -138,6 +141,15 @@ func (pm *Manager) GetPaych(ctx context.Context, from, to address.Address, amt t
return chanAccessor.getPaych(ctx, from, to, amt)
}
func (pm *Manager) AvailableFunds(from address.Address, to address.Address) (*api.ChannelAvailableFunds, error) {
chanAccessor, err := pm.accessorByFromTo(from, to)
if err != nil {
return nil, err
}
return chanAccessor.availableFunds()
}
// GetPaychWaitReady waits until the create channel / add funds message with the
// given message CID arrives.
// The returned channel address can safely be used against the Manager methods.
@ -179,6 +191,15 @@ func (pm *Manager) GetChannelInfo(addr address.Address) (*ChannelInfo, error) {
return ca.getChannelInfo(addr)
}
func (pm *Manager) CreateVoucher(ctx context.Context, ch address.Address, voucher paych.SignedVoucher) (*api.VoucherCreateResult, error) {
ca, err := pm.accessorByAddress(ch)
if err != nil {
return nil, err
}
return ca.createVoucher(ctx, ch, voucher)
}
// CheckVoucherValid checks if the given voucher is valid (is or could become spendable at some point).
// If the channel is not in the store, fetches the channel from state (and checks that
// the channel To address is owned by the wallet).
@ -309,14 +330,6 @@ func (pm *Manager) ListVouchers(ctx context.Context, ch address.Address) ([]*Vou
return ca.listVouchers(ctx, ch)
}
func (pm *Manager) NextNonceForLane(ctx context.Context, ch address.Address, lane uint64) (uint64, error) {
ca, err := pm.accessorByAddress(ch)
if err != nil {
return 0, err
}
return ca.nextNonceForLane(ctx, ch, lane)
}
func (pm *Manager) Settle(ctx context.Context, addr address.Address) (cid.Cid, error) {
ca, err := pm.accessorByAddress(addr)
if err != nil {

View File

@ -5,6 +5,10 @@ import (
"fmt"
"sync"
"github.com/filecoin-project/lotus/lib/sigs"
"github.com/filecoin-project/specs-actors/actors/crypto"
cbornode "github.com/ipfs/go-ipld-cbor"
"github.com/filecoin-project/specs-actors/actors/util/adt"
@ -132,6 +136,7 @@ type mockPaychAPI struct {
waitingCalls map[cid.Cid]*waitingCall
waitingResponses map[cid.Cid]*waitingResponse
wallet map[address.Address]struct{}
signingKey []byte
}
func newMockPaychAPI() *mockPaychAPI {
@ -240,3 +245,17 @@ func (pchapi *mockPaychAPI) addWalletAddress(addr address.Address) {
pchapi.wallet[addr] = struct{}{}
}
func (pchapi *mockPaychAPI) WalletSign(ctx context.Context, k address.Address, msg []byte) (*crypto.Signature, error) {
pchapi.lk.Lock()
defer pchapi.lk.Unlock()
return sigs.Sign(crypto.SigTypeSecp256k1, pchapi.signingKey, msg)
}
func (pchapi *mockPaychAPI) addSigningKey(key []byte) {
pchapi.lk.Lock()
defer pchapi.lk.Unlock()
pchapi.signingKey = key
}

View File

@ -5,6 +5,8 @@ import (
"context"
"fmt"
"github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/specs-actors/actors/util/adt"
"github.com/ipfs/go-cid"
@ -21,11 +23,36 @@ import (
xerrors "golang.org/x/xerrors"
)
// insufficientFundsErr indicates that there are not enough funds in the
// channel to create a voucher
type insufficientFundsErr interface {
Shortfall() types.BigInt
}
type ErrInsufficientFunds struct {
shortfall types.BigInt
}
func newErrInsufficientFunds(shortfall types.BigInt) *ErrInsufficientFunds {
return &ErrInsufficientFunds{shortfall: shortfall}
}
func (e *ErrInsufficientFunds) Error() string {
return fmt.Sprintf("not enough funds in channel to cover voucher - shortfall: %d", e.shortfall)
}
func (e *ErrInsufficientFunds) Shortfall() types.BigInt {
return e.shortfall
}
// channelAccessor is used to simplify locking when accessing a channel
type channelAccessor struct {
// waitCtx is used by processes that wait for things to be confirmed
// on chain
waitCtx context.Context
from address.Address
to address.Address
// chctx is used by background processes (eg when waiting for things to be
// confirmed on chain)
chctx context.Context
sa *stateAccessor
api managerAPI
store *Store
@ -34,14 +61,16 @@ type channelAccessor struct {
msgListeners msgListeners
}
func newChannelAccessor(pm *Manager) *channelAccessor {
func newChannelAccessor(pm *Manager, from address.Address, to address.Address) *channelAccessor {
return &channelAccessor{
lk: &channelLock{globalLock: &pm.lk},
from: from,
to: to,
chctx: pm.ctx,
sa: pm.sa,
api: pm.pchapi,
store: pm.store,
lk: &channelLock{globalLock: &pm.lk},
msgListeners: newMsgListeners(),
waitCtx: pm.ctx,
}
}
@ -52,6 +81,69 @@ func (ca *channelAccessor) getChannelInfo(addr address.Address) (*ChannelInfo, e
return ca.store.ByAddress(addr)
}
// createVoucher creates a voucher with the given specification, setting its
// nonce, signing the voucher and storing it in the local datastore.
// If there are not enough funds in the channel to create the voucher, returns
// the shortfall in funds.
func (ca *channelAccessor) createVoucher(ctx context.Context, ch address.Address, voucher paych.SignedVoucher) (*api.VoucherCreateResult, error) {
ca.lk.Lock()
defer ca.lk.Unlock()
// Find the channel for the voucher
ci, err := ca.store.ByAddress(ch)
if err != nil {
return nil, xerrors.Errorf("failed to get channel info by address: %w", err)
}
// Set the voucher channel
sv := &voucher
sv.ChannelAddr = ch
// Get the next nonce on the given lane
sv.Nonce = ca.nextNonceForLane(ci, voucher.Lane)
// Sign the voucher
vb, err := sv.SigningBytes()
if err != nil {
return nil, xerrors.Errorf("failed to get voucher signing bytes: %w", err)
}
sig, err := ca.api.WalletSign(ctx, ci.Control, vb)
if err != nil {
return nil, xerrors.Errorf("failed to sign voucher: %w", err)
}
sv.Signature = sig
// Store the voucher
if _, err := ca.addVoucherUnlocked(ctx, ch, sv, nil, types.NewInt(0)); err != nil {
// If there are not enough funds in the channel to cover the voucher,
// return a voucher create result with the shortfall
var ife insufficientFundsErr
if xerrors.As(err, &ife) {
return &api.VoucherCreateResult{
Shortfall: ife.Shortfall(),
}, nil
}
return nil, xerrors.Errorf("failed to persist voucher: %w", err)
}
return &api.VoucherCreateResult{Voucher: sv, Shortfall: types.NewInt(0)}, nil
}
func (ca *channelAccessor) nextNonceForLane(ci *ChannelInfo, lane uint64) uint64 {
var maxnonce uint64
for _, v := range ci.Vouchers {
if v.Voucher.Lane == lane {
if v.Voucher.Nonce > maxnonce {
maxnonce = v.Voucher.Nonce
}
}
}
return maxnonce + 1
}
func (ca *channelAccessor) checkVoucherValid(ctx context.Context, ch address.Address, sv *paych.SignedVoucher) (map[uint64]*paych.LaneState, error) {
ca.lk.Lock()
defer ca.lk.Unlock()
@ -133,7 +225,7 @@ func (ca *channelAccessor) checkVoucherValidUnlocked(ctx context.Context, ch add
// Must not exceed actor balance
newTotal := types.BigAdd(totalRedeemed, pchState.ToSend)
if act.Balance.LessThan(newTotal) {
return nil, fmt.Errorf("not enough funds in channel to cover voucher")
return nil, newErrInsufficientFunds(types.BigSub(newTotal, act.Balance))
}
if len(sv.Merges) != 0 {
@ -221,6 +313,10 @@ func (ca *channelAccessor) addVoucher(ctx context.Context, ch address.Address, s
ca.lk.Lock()
defer ca.lk.Unlock()
return ca.addVoucherUnlocked(ctx, ch, sv, proof, minDelta)
}
func (ca *channelAccessor) addVoucherUnlocked(ctx context.Context, ch address.Address, sv *paych.SignedVoucher, proof []byte, minDelta types.BigInt) (types.BigInt, error) {
ci, err := ca.store.ByAddress(ch)
if err != nil {
return types.BigInt{}, err
@ -382,28 +478,6 @@ func (ca *channelAccessor) listVouchers(ctx context.Context, ch address.Address)
return ca.store.VouchersForPaych(ch)
}
func (ca *channelAccessor) nextNonceForLane(ctx context.Context, ch address.Address, lane uint64) (uint64, error) {
ca.lk.Lock()
defer ca.lk.Unlock()
// TODO: should this take into account lane state?
vouchers, err := ca.store.VouchersForPaych(ch)
if err != nil {
return 0, err
}
var maxnonce uint64
for _, v := range vouchers {
if v.Voucher.Lane == lane {
if v.Voucher.Nonce > maxnonce {
maxnonce = v.Voucher.Nonce
}
}
}
return maxnonce + 1, nil
}
// laneState gets the LaneStates from chain, then applies all vouchers in
// the data store over the chain state
func (ca *channelAccessor) laneState(ctx context.Context, state *paych.State, ch address.Address) (map[uint64]*paych.LaneState, error) {
@ -479,7 +553,7 @@ func (ca *channelAccessor) totalRedeemedWithVoucher(laneStates map[uint64]*paych
lane, ok := laneStates[sv.Lane]
if ok {
// If the voucher is for an existing lane, and the voucher nonce
// and is higher than the lane nonce
// is higher than the lane nonce
if sv.Nonce > lane.Nonce {
// Add the delta between the redeemed amount and the voucher
// amount to the total

View File

@ -381,11 +381,76 @@ func TestCheckVoucherValidCountingAllLanes(t *testing.T) {
require.NoError(t, err)
}
func TestCreateVoucher(t *testing.T) {
ctx := context.Background()
// Set up a manager with a single payment channel
s := testSetupMgrWithChannel(ctx, t)
// Create a voucher in lane 1
voucherLane1Amt := big.NewInt(5)
voucher := paych.SignedVoucher{
Lane: 1,
Amount: voucherLane1Amt,
}
res, err := s.mgr.CreateVoucher(ctx, s.ch, voucher)
require.NoError(t, err)
require.NotNil(t, res.Voucher)
require.Equal(t, s.ch, res.Voucher.ChannelAddr)
require.Equal(t, voucherLane1Amt, res.Voucher.Amount)
require.EqualValues(t, 0, res.Shortfall.Int64())
nonce := res.Voucher.Nonce
// Create a voucher in lane 1 again, with a higher amount
voucherLane1Amt = big.NewInt(8)
voucher = paych.SignedVoucher{
Lane: 1,
Amount: voucherLane1Amt,
}
res, err = s.mgr.CreateVoucher(ctx, s.ch, voucher)
require.NoError(t, err)
require.NotNil(t, res.Voucher)
require.Equal(t, s.ch, res.Voucher.ChannelAddr)
require.Equal(t, voucherLane1Amt, res.Voucher.Amount)
require.EqualValues(t, 0, res.Shortfall.Int64())
require.Equal(t, nonce+1, res.Voucher.Nonce)
// Create a voucher in lane 2 that covers all the remaining funds
// in the channel
voucherLane2Amt := big.Sub(s.amt, voucherLane1Amt)
voucher = paych.SignedVoucher{
Lane: 2,
Amount: voucherLane2Amt,
}
res, err = s.mgr.CreateVoucher(ctx, s.ch, voucher)
require.NoError(t, err)
require.NotNil(t, res.Voucher)
require.Equal(t, s.ch, res.Voucher.ChannelAddr)
require.Equal(t, voucherLane2Amt, res.Voucher.Amount)
require.EqualValues(t, 0, res.Shortfall.Int64())
// Create a voucher in lane 2 that exceeds the remaining funds in the
// channel
voucherLane2Amt = big.Add(voucherLane2Amt, big.NewInt(1))
voucher = paych.SignedVoucher{
Lane: 2,
Amount: voucherLane2Amt,
}
res, err = s.mgr.CreateVoucher(ctx, s.ch, voucher)
require.NoError(t, err)
// Expect a shortfall value equal to the amount required to add the voucher
// to the channel
require.Nil(t, res.Voucher)
require.EqualValues(t, 1, res.Shortfall.Int64())
}
func TestAddVoucherDelta(t *testing.T) {
ctx := context.Background()
// Set up a manager with a single payment channel
mgr, _, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t)
s := testSetupMgrWithChannel(ctx, t)
voucherLane := uint64(1)
@ -393,23 +458,23 @@ func TestAddVoucherDelta(t *testing.T) {
minDelta := big.NewInt(2)
nonce := uint64(1)
voucherAmount := big.NewInt(1)
sv := createTestVoucher(t, ch, voucherLane, nonce, voucherAmount, fromKeyPrivate)
_, err := mgr.AddVoucherOutbound(ctx, ch, sv, nil, minDelta)
sv := createTestVoucher(t, s.ch, voucherLane, nonce, voucherAmount, s.fromKeyPrivate)
_, err := s.mgr.AddVoucherOutbound(ctx, s.ch, sv, nil, minDelta)
require.Error(t, err)
// Expect success when adding a voucher whose amount is equal to minDelta
nonce++
voucherAmount = big.NewInt(2)
sv = createTestVoucher(t, ch, voucherLane, nonce, voucherAmount, fromKeyPrivate)
delta, err := mgr.AddVoucherOutbound(ctx, ch, sv, nil, minDelta)
sv = createTestVoucher(t, s.ch, voucherLane, nonce, voucherAmount, s.fromKeyPrivate)
delta, err := s.mgr.AddVoucherOutbound(ctx, s.ch, sv, nil, minDelta)
require.NoError(t, err)
require.EqualValues(t, delta.Int64(), 2)
// Check that delta is correct when there's an existing voucher
nonce++
voucherAmount = big.NewInt(5)
sv = createTestVoucher(t, ch, voucherLane, nonce, voucherAmount, fromKeyPrivate)
delta, err = mgr.AddVoucherOutbound(ctx, ch, sv, nil, minDelta)
sv = createTestVoucher(t, s.ch, voucherLane, nonce, voucherAmount, s.fromKeyPrivate)
delta, err = s.mgr.AddVoucherOutbound(ctx, s.ch, sv, nil, minDelta)
require.NoError(t, err)
require.EqualValues(t, delta.Int64(), 3)
@ -417,8 +482,8 @@ func TestAddVoucherDelta(t *testing.T) {
nonce = uint64(1)
voucherAmount = big.NewInt(6)
voucherLane = uint64(2)
sv = createTestVoucher(t, ch, voucherLane, nonce, voucherAmount, fromKeyPrivate)
delta, err = mgr.AddVoucherOutbound(ctx, ch, sv, nil, minDelta)
sv = createTestVoucher(t, s.ch, voucherLane, nonce, voucherAmount, s.fromKeyPrivate)
delta, err = s.mgr.AddVoucherOutbound(ctx, s.ch, sv, nil, minDelta)
require.NoError(t, err)
require.EqualValues(t, delta.Int64(), 6)
}
@ -427,7 +492,7 @@ func TestAddVoucherNextLane(t *testing.T) {
ctx := context.Background()
// Set up a manager with a single payment channel
mgr, _, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t)
s := testSetupMgrWithChannel(ctx, t)
minDelta := big.NewInt(0)
voucherAmount := big.NewInt(2)
@ -435,40 +500,40 @@ func TestAddVoucherNextLane(t *testing.T) {
// Add a voucher in lane 2
nonce := uint64(1)
voucherLane := uint64(2)
sv := createTestVoucher(t, ch, voucherLane, nonce, voucherAmount, fromKeyPrivate)
_, err := mgr.AddVoucherOutbound(ctx, ch, sv, nil, minDelta)
sv := createTestVoucher(t, s.ch, voucherLane, nonce, voucherAmount, s.fromKeyPrivate)
_, err := s.mgr.AddVoucherOutbound(ctx, s.ch, sv, nil, minDelta)
require.NoError(t, err)
ci, err := mgr.GetChannelInfo(ch)
ci, err := s.mgr.GetChannelInfo(s.ch)
require.NoError(t, err)
require.EqualValues(t, ci.NextLane, 3)
// Allocate a lane (should be lane 3)
lane, err := mgr.AllocateLane(ch)
lane, err := s.mgr.AllocateLane(s.ch)
require.NoError(t, err)
require.EqualValues(t, lane, 3)
ci, err = mgr.GetChannelInfo(ch)
ci, err = s.mgr.GetChannelInfo(s.ch)
require.NoError(t, err)
require.EqualValues(t, ci.NextLane, 4)
// Add a voucher in lane 1
voucherLane = uint64(1)
sv = createTestVoucher(t, ch, voucherLane, nonce, voucherAmount, fromKeyPrivate)
_, err = mgr.AddVoucherOutbound(ctx, ch, sv, nil, minDelta)
sv = createTestVoucher(t, s.ch, voucherLane, nonce, voucherAmount, s.fromKeyPrivate)
_, err = s.mgr.AddVoucherOutbound(ctx, s.ch, sv, nil, minDelta)
require.NoError(t, err)
ci, err = mgr.GetChannelInfo(ch)
ci, err = s.mgr.GetChannelInfo(s.ch)
require.NoError(t, err)
require.EqualValues(t, ci.NextLane, 4)
// Add a voucher in lane 7
voucherLane = uint64(7)
sv = createTestVoucher(t, ch, voucherLane, nonce, voucherAmount, fromKeyPrivate)
_, err = mgr.AddVoucherOutbound(ctx, ch, sv, nil, minDelta)
sv = createTestVoucher(t, s.ch, voucherLane, nonce, voucherAmount, s.fromKeyPrivate)
_, err = s.mgr.AddVoucherOutbound(ctx, s.ch, sv, nil, minDelta)
require.NoError(t, err)
ci, err = mgr.GetChannelInfo(ch)
ci, err = s.mgr.GetChannelInfo(s.ch)
require.NoError(t, err)
require.EqualValues(t, ci.NextLane, 8)
}
@ -477,15 +542,15 @@ func TestAllocateLane(t *testing.T) {
ctx := context.Background()
// Set up a manager with a single payment channel
mgr, _, ch, _ := testSetupMgrWithChannel(ctx, t)
s := testSetupMgrWithChannel(ctx, t)
// First lane should be 0
lane, err := mgr.AllocateLane(ch)
lane, err := s.mgr.AllocateLane(s.ch)
require.NoError(t, err)
require.EqualValues(t, lane, 0)
// Next lane should be 1
lane, err = mgr.AllocateLane(ch)
lane, err = s.mgr.AllocateLane(s.ch)
require.NoError(t, err)
require.EqualValues(t, lane, 1)
}
@ -553,7 +618,7 @@ func TestAddVoucherProof(t *testing.T) {
ctx := context.Background()
// Set up a manager with a single payment channel
mgr, _, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t)
s := testSetupMgrWithChannel(ctx, t)
nonce := uint64(1)
voucherAmount := big.NewInt(1)
@ -563,34 +628,34 @@ func TestAddVoucherProof(t *testing.T) {
// Add a voucher with no proof
var proof []byte
sv := createTestVoucher(t, ch, voucherLane, nonce, voucherAmount, fromKeyPrivate)
_, err := mgr.AddVoucherOutbound(ctx, ch, sv, nil, minDelta)
sv := createTestVoucher(t, s.ch, voucherLane, nonce, voucherAmount, s.fromKeyPrivate)
_, err := s.mgr.AddVoucherOutbound(ctx, s.ch, sv, nil, minDelta)
require.NoError(t, err)
// Expect one voucher with no proof
ci, err := mgr.GetChannelInfo(ch)
ci, err := s.mgr.GetChannelInfo(s.ch)
require.NoError(t, err)
require.Len(t, ci.Vouchers, 1)
require.Len(t, ci.Vouchers[0].Proof, 0)
// Add same voucher with no proof
voucherLane = uint64(1)
_, err = mgr.AddVoucherOutbound(ctx, ch, sv, proof, minDelta)
_, err = s.mgr.AddVoucherOutbound(ctx, s.ch, sv, proof, minDelta)
require.NoError(t, err)
// Expect one voucher with no proof
ci, err = mgr.GetChannelInfo(ch)
ci, err = s.mgr.GetChannelInfo(s.ch)
require.NoError(t, err)
require.Len(t, ci.Vouchers, 1)
require.Len(t, ci.Vouchers[0].Proof, 0)
// Add same voucher with proof
proof = []byte{1}
_, err = mgr.AddVoucherOutbound(ctx, ch, sv, proof, minDelta)
_, err = s.mgr.AddVoucherOutbound(ctx, s.ch, sv, proof, minDelta)
require.NoError(t, err)
// Should add proof to existing voucher
ci, err = mgr.GetChannelInfo(ch)
ci, err = s.mgr.GetChannelInfo(s.ch)
require.NoError(t, err)
require.Len(t, ci.Vouchers, 1)
require.Len(t, ci.Vouchers[0].Proof, 1)
@ -663,47 +728,47 @@ func TestBestSpendable(t *testing.T) {
ctx := context.Background()
// Set up a manager with a single payment channel
mgr, mock, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t)
s := 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)
svL1V1 := createTestVoucher(t, s.ch, voucherLane, nonce, voucherAmount, s.fromKeyPrivate)
_, err := s.mgr.AddVoucherInbound(ctx, s.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)
svL1V2 := createTestVoucher(t, s.ch, voucherLane, nonce, voucherAmount, s.fromKeyPrivate)
_, err = s.mgr.AddVoucherInbound(ctx, s.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)
svL1V3 := createTestVoucher(t, s.ch, voucherLane, nonce, voucherAmount, s.fromKeyPrivate)
_, err = s.mgr.AddVoucherInbound(ctx, s.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)
svL2V1 := createTestVoucher(t, s.ch, voucherLane, nonce, voucherAmount, s.fromKeyPrivate)
_, err = s.mgr.AddVoucherInbound(ctx, s.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{
bsapi := newMockBestSpendableAPI(s.mgr)
s.mock.setCallResponse(&api.InvocResult{
MsgRct: &types.MessageReceipt{
ExitCode: 0,
},
})
// Verify best spendable vouchers on each lane
vouchers, err := BestSpendableByLane(ctx, bsapi, ch)
vouchers, err := BestSpendableByLane(ctx, bsapi, s.ch)
require.NoError(t, err)
require.Len(t, vouchers, 2)
@ -716,21 +781,21 @@ func TestBestSpendable(t *testing.T) {
require.EqualValues(t, 2, vchr.Amount.Int64())
// Submit voucher from lane 2
_, err = mgr.SubmitVoucher(ctx, ch, svL2V1, nil, nil)
_, err = s.mgr.SubmitVoucher(ctx, s.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)
vouchers, err = BestSpendableByLane(ctx, bsapi, s.ch)
require.NoError(t, err)
require.Len(t, vouchers, 1)
// Submit first voucher from lane 1
_, err = mgr.SubmitVoucher(ctx, ch, svL1V1, nil, nil)
_, err = s.mgr.SubmitVoucher(ctx, s.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)
vouchers, err = BestSpendableByLane(ctx, bsapi, s.ch)
require.NoError(t, err)
require.Len(t, vouchers, 1)
@ -743,18 +808,18 @@ func TestCheckSpendable(t *testing.T) {
ctx := context.Background()
// Set up a manager with a single payment channel
mgr, mock, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t)
s := 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)
voucher := createTestVoucherWithExtra(t, s.ch, voucherLane, nonce, voucherAmount, s.fromKeyPrivate)
// Add voucher with proof
minDelta := big.NewInt(0)
proof := []byte("proof")
_, err := mgr.AddVoucherInbound(ctx, ch, voucher, proof, minDelta)
_, err := s.mgr.AddVoucherInbound(ctx, s.ch, voucher, proof, minDelta)
require.NoError(t, err)
// Return success exit code from VM call, which indicates that voucher is
@ -764,17 +829,17 @@ func TestCheckSpendable(t *testing.T) {
ExitCode: 0,
},
}
mock.setCallResponse(successResponse)
s.mock.setCallResponse(successResponse)
// Check that spendable is true
secret := []byte("secret")
otherProof := []byte("other proof")
spendable, err := mgr.CheckVoucherSpendable(ctx, ch, voucher, secret, otherProof)
spendable, err := s.mgr.CheckVoucherSpendable(ctx, s.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()
lastCall := s.mock.getLastCall()
var p paych.UpdateChannelStateParams
err = p.UnmarshalCBOR(bytes.NewReader(lastCall.Params))
require.NoError(t, err)
@ -784,11 +849,11 @@ func TestCheckSpendable(t *testing.T) {
// 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)
spendable, err = s.mgr.CheckVoucherSpendable(ctx, s.ch, voucher, secret2, nil)
require.NoError(t, err)
require.True(t, spendable)
lastCall = mock.getLastCall()
lastCall = s.mock.getLastCall()
var p2 paych.UpdateChannelStateParams
err = p2.UnmarshalCBOR(bytes.NewReader(lastCall.Params))
require.NoError(t, err)
@ -796,26 +861,26 @@ func TestCheckSpendable(t *testing.T) {
require.Equal(t, secret2, p2.Secret)
// Check that if VM call returns non-success exit code, spendable is false
mock.setCallResponse(&api.InvocResult{
s.mock.setCallResponse(&api.InvocResult{
MsgRct: &types.MessageReceipt{
ExitCode: 1,
},
})
spendable, err = mgr.CheckVoucherSpendable(ctx, ch, voucher, secret, nil)
spendable, err = s.mgr.CheckVoucherSpendable(ctx, s.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)
s.mock.setCallResponse(successResponse)
spendable, err = s.mgr.CheckVoucherSpendable(ctx, s.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)
_, err = s.mgr.SubmitVoucher(ctx, s.ch, voucher, nil, nil)
require.NoError(t, err)
spendable, err = mgr.CheckVoucherSpendable(ctx, ch, voucher, secret, nil)
spendable, err = s.mgr.CheckVoucherSpendable(ctx, s.ch, voucher, secret, nil)
require.NoError(t, err)
require.False(t, spendable)
}
@ -824,28 +889,28 @@ func TestSubmitVoucher(t *testing.T) {
ctx := context.Background()
// Set up a manager with a single payment channel
mgr, mock, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t)
s := 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)
voucher := createTestVoucherWithExtra(t, s.ch, voucherLane, nonce, voucherAmount, s.fromKeyPrivate)
// Add voucher with proof
minDelta := big.NewInt(0)
addVoucherProof := []byte("proof")
_, err := mgr.AddVoucherInbound(ctx, ch, voucher, addVoucherProof, minDelta)
_, err := s.mgr.AddVoucherInbound(ctx, s.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)
submitCid, err := s.mgr.SubmitVoucher(ctx, s.ch, voucher, secret, submitProof)
require.NoError(t, err)
// Check that the secret and proof were passed through correctly
msg := mock.pushedMessages(submitCid)
msg := s.mock.pushedMessages(submitCid)
var p paych.UpdateChannelStateParams
err = p.UnmarshalCBOR(bytes.NewReader(msg.Message.Params))
require.NoError(t, err)
@ -858,14 +923,14 @@ func TestSubmitVoucher(t *testing.T) {
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)
voucher = createTestVoucherWithExtra(t, s.ch, voucherLane, nonce, voucherAmount, s.fromKeyPrivate)
_, err = s.mgr.AddVoucherInbound(ctx, s.ch, voucher, addVoucherProof2, minDelta)
require.NoError(t, err)
submitCid, err = mgr.SubmitVoucher(ctx, ch, voucher, secret2, nil)
submitCid, err = s.mgr.SubmitVoucher(ctx, s.ch, voucher, secret2, nil)
require.NoError(t, err)
msg = mock.pushedMessages(submitCid)
msg = s.mock.pushedMessages(submitCid)
var p2 paych.UpdateChannelStateParams
err = p2.UnmarshalCBOR(bytes.NewReader(msg.Message.Params))
require.NoError(t, err)
@ -877,11 +942,11 @@ func TestSubmitVoucher(t *testing.T) {
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)
voucher = createTestVoucherWithExtra(t, s.ch, voucherLane, nonce, voucherAmount, s.fromKeyPrivate)
submitCid, err = s.mgr.SubmitVoucher(ctx, s.ch, voucher, secret3, proof3)
require.NoError(t, err)
msg = mock.pushedMessages(submitCid)
msg = s.mock.pushedMessages(submitCid)
var p3 paych.UpdateChannelStateParams
err = p3.UnmarshalCBOR(bytes.NewReader(msg.Message.Params))
require.NoError(t, err)
@ -889,7 +954,7 @@ func TestSubmitVoucher(t *testing.T) {
require.Equal(t, secret3, p3.Secret)
// Verify that vouchers are marked as submitted
vis, err := mgr.ListVouchers(ctx, ch)
vis, err := s.mgr.ListVouchers(ctx, s.ch)
require.NoError(t, err)
require.Len(t, vis, 3)
@ -898,55 +963,20 @@ func TestSubmitVoucher(t *testing.T) {
}
// Attempting to submit the same voucher again should fail
_, err = mgr.SubmitVoucher(ctx, ch, voucher, secret2, nil)
_, err = s.mgr.SubmitVoucher(ctx, s.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)
// Expect next nonce for non-existent lane to be 1
next, err := mgr.NextNonceForLane(ctx, ch, 1)
require.NoError(t, err)
require.EqualValues(t, next, 1)
voucherAmount := big.NewInt(1)
minDelta := big.NewInt(0)
voucherAmount = big.NewInt(2)
// Add vouchers such that we have
// lane 1: nonce 2
// lane 1: nonce 4
voucherLane := uint64(1)
for _, nonce := range []uint64{2, 4} {
voucherAmount = big.Add(voucherAmount, big.NewInt(1))
sv := createTestVoucher(t, ch, voucherLane, nonce, voucherAmount, key)
_, err := mgr.AddVoucherOutbound(ctx, ch, sv, nil, minDelta)
require.NoError(t, err)
type testScaffold struct {
mgr *Manager
mock *mockManagerAPI
ch address.Address
amt big.Int
fromAcct address.Address
fromKeyPrivate []byte
}
// lane 2: nonce 7
voucherLane = uint64(2)
nonce := uint64(7)
sv := createTestVoucher(t, ch, voucherLane, nonce, voucherAmount, key)
_, err = mgr.AddVoucherOutbound(ctx, ch, sv, nil, minDelta)
require.NoError(t, err)
// Expect next nonce for lane 1 to be 5
next, err = mgr.NextNonceForLane(ctx, ch, 1)
require.NoError(t, err)
require.EqualValues(t, next, 5)
// Expect next nonce for lane 2 to be 8
next, err = mgr.NextNonceForLane(ctx, ch, 2)
require.NoError(t, err)
require.EqualValues(t, next, 8)
}
func testSetupMgrWithChannel(ctx context.Context, t *testing.T) (*Manager, *mockManagerAPI, address.Address, []byte) {
func testSetupMgrWithChannel(ctx context.Context, t *testing.T) *testScaffold {
fromKeyPrivate, fromKeyPublic := testGenerateKeyPair(t)
ch := tutils.NewIDAddr(t, 100)
@ -962,11 +992,12 @@ func testSetupMgrWithChannel(ctx context.Context, t *testing.T) (*Manager, *mock
mock.setAccountState(toAcct, account.State{Address: to})
// Create channel in state
balance := big.NewInt(20)
act := &types.Actor{
Code: builtin.AccountActorCodeID,
Head: cid.Cid{},
Nonce: 0,
Balance: big.NewInt(20),
Balance: balance,
}
mock.setPaychState(ch, act, paych.State{
From: fromAcct,
@ -991,7 +1022,17 @@ func testSetupMgrWithChannel(ctx context.Context, t *testing.T) (*Manager, *mock
err = mgr.store.putChannelInfo(ci)
require.NoError(t, err)
return mgr, mock, ch, fromKeyPrivate
// Add the from signing key to the wallet
mock.addSigningKey(fromKeyPrivate)
return &testScaffold{
mgr: mgr,
mock: mock,
ch: ch,
amt: balance,
fromAcct: fromAcct,
fromKeyPrivate: fromKeyPrivate,
}
}
func testGenerateKeyPair(t *testing.T) ([]byte, []byte) {

View File

@ -6,6 +6,12 @@ import (
"testing"
"time"
"github.com/filecoin-project/specs-actors/actors/builtin/account"
"github.com/filecoin-project/specs-actors/actors/util/adt"
"github.com/filecoin-project/specs-actors/actors/abi"
"github.com/filecoin-project/specs-actors/actors/builtin/paych"
cborrpc "github.com/filecoin-project/go-cbor-util"
init_ "github.com/filecoin-project/specs-actors/actors/builtin/init"
@ -650,8 +656,6 @@ func TestPaychGetMergeAddFunds(t *testing.T) {
require.NoError(t, err)
// Queue up two add funds requests behind create channel
//var addFundsQueuedUp sync.WaitGroup
//addFundsQueuedUp.Add(2)
var addFundsSent sync.WaitGroup
addFundsSent.Add(2)
@ -662,7 +666,6 @@ func TestPaychGetMergeAddFunds(t *testing.T) {
var addFundsMcid1 cid.Cid
var addFundsMcid2 cid.Cid
go func() {
//go addFundsQueuedUp.Done()
defer addFundsSent.Done()
// Request add funds - should block until create channel has completed
@ -671,7 +674,6 @@ func TestPaychGetMergeAddFunds(t *testing.T) {
}()
go func() {
//go addFundsQueuedUp.Done()
defer addFundsSent.Done()
// Request add funds again - should merge with waiting add funds request
@ -899,6 +901,168 @@ func TestPaychGetMergeAddFundsCtxCancelAll(t *testing.T) {
require.Equal(t, createAmt, createMsg.Message.Value)
}
// TestPaychAvailableFunds tests that PaychAvailableFunds returns the correct
// channel state
func TestPaychAvailableFunds(t *testing.T) {
ctx := context.Background()
store := NewStore(ds_sync.MutexWrap(ds.NewMapDatastore()))
fromKeyPrivate, fromKeyPublic := testGenerateKeyPair(t)
ch := tutils.NewIDAddr(t, 100)
from := tutils.NewSECP256K1Addr(t, string(fromKeyPublic))
to := tutils.NewIDAddr(t, 102)
fromAcct := tutils.NewActorAddr(t, "fromAct")
toAcct := tutils.NewActorAddr(t, "toAct")
mock := newMockManagerAPI()
defer mock.close()
mgr, err := newManager(store, mock)
require.NoError(t, err)
// No channel created yet so available funds should be all zeroes
av, err := mgr.AvailableFunds(from, to)
require.NoError(t, err)
require.Nil(t, av.Channel)
require.Nil(t, av.PendingWaitSentinel)
require.EqualValues(t, 0, av.ConfirmedAmt.Int64())
require.EqualValues(t, 0, av.PendingAmt.Int64())
require.EqualValues(t, 0, av.QueuedAmt.Int64())
require.EqualValues(t, 0, av.VoucherReedeemedAmt.Int64())
// Send create message for a channel with value 10
createAmt := big.NewInt(10)
_, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt)
require.NoError(t, err)
// Available funds should reflect create channel message sent
av, err = mgr.AvailableFunds(from, to)
require.NoError(t, err)
require.Nil(t, av.Channel)
require.EqualValues(t, 0, av.ConfirmedAmt.Int64())
require.EqualValues(t, createAmt, av.PendingAmt)
require.EqualValues(t, 0, av.QueuedAmt.Int64())
require.EqualValues(t, 0, av.VoucherReedeemedAmt.Int64())
// Should now have a pending wait sentinel
require.NotNil(t, av.PendingWaitSentinel)
// Queue up an add funds request behind create channel
var addFundsSent sync.WaitGroup
addFundsSent.Add(1)
addFundsAmt := big.NewInt(5)
var addFundsMcid cid.Cid
go func() {
defer addFundsSent.Done()
// Request add funds - should block until create channel has completed
_, addFundsMcid, err = mgr.GetPaych(ctx, from, to, addFundsAmt)
require.NoError(t, err)
}()
// Wait for add funds request to be queued up
waitForQueueSize(t, mgr, from, to, 1)
// Available funds should now include queued funds
av, err = mgr.AvailableFunds(from, to)
require.NoError(t, err)
require.Nil(t, av.Channel)
require.NotNil(t, av.PendingWaitSentinel)
require.EqualValues(t, 0, av.ConfirmedAmt.Int64())
// create amount is still pending
require.EqualValues(t, createAmt, av.PendingAmt)
// queued amount now includes add funds amount
require.EqualValues(t, addFundsAmt, av.QueuedAmt)
require.EqualValues(t, 0, av.VoucherReedeemedAmt.Int64())
// Create channel in state
arr, err := adt.MakeEmptyArray(mock.store).Root()
require.NoError(t, err)
mock.setAccountState(fromAcct, account.State{Address: from})
mock.setAccountState(toAcct, account.State{Address: to})
act := &types.Actor{
Code: builtin.AccountActorCodeID,
Head: cid.Cid{},
Nonce: 0,
Balance: createAmt,
}
mock.setPaychState(ch, act, paych.State{
From: fromAcct,
To: toAcct,
ToSend: big.NewInt(0),
SettlingAt: abi.ChainEpoch(0),
MinSettleHeight: abi.ChainEpoch(0),
LaneStates: arr,
})
// Send create channel response
response := testChannelResponse(t, ch)
mock.receiveMsgResponse(createMsgCid, response)
// Wait for create channel response
chres, err := mgr.GetPaychWaitReady(ctx, *av.PendingWaitSentinel)
require.NoError(t, err)
require.Equal(t, ch, chres)
// Wait for add funds request to be sent
addFundsSent.Wait()
// Available funds should now include the channel and also a wait sentinel
// for the add funds message
av, err = mgr.AvailableFunds(from, to)
require.NoError(t, err)
require.NotNil(t, av.Channel)
require.NotNil(t, av.PendingWaitSentinel)
// create amount is now confirmed
require.EqualValues(t, createAmt, av.ConfirmedAmt)
// add funds amount it now pending
require.EqualValues(t, addFundsAmt, av.PendingAmt)
require.EqualValues(t, 0, av.QueuedAmt.Int64())
require.EqualValues(t, 0, av.VoucherReedeemedAmt.Int64())
// Send success add funds response
mock.receiveMsgResponse(addFundsMcid, types.MessageReceipt{
ExitCode: 0,
Return: []byte{},
})
// Wait for add funds response
_, err = mgr.GetPaychWaitReady(ctx, *av.PendingWaitSentinel)
require.NoError(t, err)
// Available funds should no longer have a wait sentinel
av, err = mgr.AvailableFunds(from, to)
require.NoError(t, err)
require.NotNil(t, av.Channel)
require.Nil(t, av.PendingWaitSentinel)
// confirmed amount now includes create and add funds amounts
require.EqualValues(t, types.BigAdd(createAmt, addFundsAmt), av.ConfirmedAmt)
require.EqualValues(t, 0, av.PendingAmt.Int64())
require.EqualValues(t, 0, av.QueuedAmt.Int64())
require.EqualValues(t, 0, av.VoucherReedeemedAmt.Int64())
// Add some vouchers
voucherAmt1 := types.NewInt(3)
voucher := createTestVoucher(t, ch, 1, 1, voucherAmt1, fromKeyPrivate)
_, err = mgr.AddVoucherOutbound(ctx, ch, voucher, nil, types.NewInt(0))
require.NoError(t, err)
voucherAmt2 := types.NewInt(2)
voucher = createTestVoucher(t, ch, 2, 1, voucherAmt2, fromKeyPrivate)
_, err = mgr.AddVoucherOutbound(ctx, ch, voucher, nil, types.NewInt(0))
require.NoError(t, err)
av, err = mgr.AvailableFunds(from, to)
require.NoError(t, err)
require.NotNil(t, av.Channel)
require.Nil(t, av.PendingWaitSentinel)
require.EqualValues(t, types.BigAdd(createAmt, addFundsAmt), av.ConfirmedAmt)
require.EqualValues(t, 0, av.PendingAmt.Int64())
require.EqualValues(t, 0, av.QueuedAmt.Int64())
// voucher redeemed amount now includes vouchers
require.EqualValues(t, types.BigAdd(voucherAmt1, voucherAmt2), av.VoucherReedeemedAmt)
}
// waitForQueueSize waits for the funds request queue to be of the given size
func waitForQueueSize(t *testing.T, mgr *Manager, from address.Address, to address.Address, size int) {
ca, err := mgr.accessorByFromTo(from, to)

View File

@ -6,6 +6,8 @@ import (
"fmt"
"sync"
"github.com/filecoin-project/lotus/api"
"golang.org/x/sync/errgroup"
"github.com/filecoin-project/specs-actors/actors/abi/big"
@ -191,17 +193,17 @@ func (ca *channelAccessor) getPaych(ctx context.Context, from, to address.Addres
}
}
// Queue up an add funds operations
// Queue up an add funds operation
func (ca *channelAccessor) enqueue(task *fundsReq) {
ca.lk.Lock()
defer ca.lk.Unlock()
ca.fundsReqQueue = append(ca.fundsReqQueue, task)
go ca.processQueue()
go ca.processQueue() // nolint: errcheck
}
// Run the operations in the queue
func (ca *channelAccessor) processQueue() {
func (ca *channelAccessor) processQueue() (*api.ChannelAvailableFunds, error) {
ca.lk.Lock()
defer ca.lk.Unlock()
@ -210,7 +212,7 @@ func (ca *channelAccessor) processQueue() {
// If there's nothing in the queue, bail out
if len(ca.fundsReqQueue) == 0 {
return
return ca.currentAvailableFunds(types.NewInt(0))
}
// Merge all pending requests into one.
@ -221,7 +223,7 @@ func (ca *channelAccessor) processQueue() {
if amt.IsZero() {
// Note: The amount can be zero if requests are cancelled as we're
// building the mergedFundsReq
return
return ca.currentAvailableFunds(amt)
}
res := ca.processTask(merged.ctx, merged.from(), merged.to(), amt)
@ -231,7 +233,7 @@ func (ca *channelAccessor) processQueue() {
if res == nil {
// Stop processing the fundsReqQueue and wait. When the event occurs it will
// call processQueue() again
return
return ca.currentAvailableFunds(amt)
}
// Finished processing so clear the queue
@ -239,6 +241,8 @@ func (ca *channelAccessor) processQueue() {
// Call the task callback with its results
merged.onComplete(res)
return ca.currentAvailableFunds(types.NewInt(0))
}
// filterQueue filters cancelled requests out of the queue
@ -291,10 +295,65 @@ func (ca *channelAccessor) msgWaitComplete(mcid cid.Cid, err error) {
// The queue may have been waiting for msg completion to proceed, so
// process the next queue item
if len(ca.fundsReqQueue) > 0 {
go ca.processQueue()
go ca.processQueue() // nolint: errcheck
}
}
func (ca *channelAccessor) currentAvailableFunds(queuedAmt types.BigInt) (*api.ChannelAvailableFunds, error) {
channelInfo, err := ca.store.OutboundActiveByFromTo(ca.from, ca.to)
if err != nil && err != ErrChannelNotTracked {
return nil, err
}
if channelInfo == nil {
// Channel does not exist
return &api.ChannelAvailableFunds{
Channel: nil,
ConfirmedAmt: types.NewInt(0),
PendingAmt: types.NewInt(0),
PendingWaitSentinel: nil,
QueuedAmt: queuedAmt,
VoucherReedeemedAmt: types.NewInt(0),
}, nil
}
// The channel may have a pending create or add funds message
waitSentinel := channelInfo.CreateMsg
if waitSentinel == nil {
waitSentinel = channelInfo.AddFundsMsg
}
// Get the total amount redeemed by vouchers.
// This includes vouchers that have been submitted, and vouchers that are
// in the datastore but haven't yet been submitted.
totalRedeemed := types.NewInt(0)
if channelInfo.Channel != nil {
ch := *channelInfo.Channel
_, pchState, err := ca.sa.loadPaychActorState(ca.chctx, ch)
if err != nil {
return nil, err
}
laneStates, err := ca.laneState(ca.chctx, pchState, ch)
if err != nil {
return nil, err
}
for _, ls := range laneStates {
totalRedeemed = types.BigAdd(totalRedeemed, ls.Redeemed)
}
}
return &api.ChannelAvailableFunds{
Channel: channelInfo.Channel,
ConfirmedAmt: channelInfo.Amount,
PendingAmt: channelInfo.PendingAmount,
PendingWaitSentinel: waitSentinel,
QueuedAmt: queuedAmt,
VoucherReedeemedAmt: totalRedeemed,
}, nil
}
// processTask checks the state of the channel and takes appropriate action
// (see description of getPaych).
// Note that processTask may be called repeatedly in the same state, and should
@ -397,7 +456,7 @@ func (ca *channelAccessor) waitForPaychCreateMsg(channelID string, mcid cid.Cid)
}
func (ca *channelAccessor) waitPaychCreateMsg(channelID string, mcid cid.Cid) error {
mwait, err := ca.api.StateWaitMsg(ca.waitCtx, mcid, build.MessageConfidence)
mwait, err := ca.api.StateWaitMsg(ca.chctx, mcid, build.MessageConfidence)
if err != nil {
log.Errorf("wait msg: %w", err)
return err
@ -480,7 +539,7 @@ func (ca *channelAccessor) waitForAddFundsMsg(channelID string, mcid cid.Cid) {
}
func (ca *channelAccessor) waitAddFundsMsg(channelID string, mcid cid.Cid) error {
mwait, err := ca.api.StateWaitMsg(ca.waitCtx, mcid, build.MessageConfidence)
mwait, err := ca.api.StateWaitMsg(ca.chctx, mcid, build.MessageConfidence)
if err != nil {
log.Error(err)
return err
@ -669,3 +728,7 @@ func (ca *channelAccessor) msgPromise(ctx context.Context, mcid cid.Cid) chan on
return promise
}
func (ca *channelAccessor) availableFunds() (*api.ChannelAvailableFunds, error) {
return ca.processQueue()
}