diff --git a/api/api_full.go b/api/api_full.go index 2f146267b..4aaa9bb01 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -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 diff --git a/api/apistruct/struct.go b/api/apistruct/struct.go index 33b02933f..6634436a4 100644 --- a/api/apistruct/struct.go +++ b/api/apistruct/struct.go @@ -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) } diff --git a/api/test/paych.go b/api/test/paych.go index faa0bf8d9..b0ccc0a5c 100644 --- a/api/test/paych.go +++ b/api/test/paych.go @@ -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) } diff --git a/cli/paych.go b/cli/paych.go index ff4d769da..ce92af0bc 100644 --- a/cli/paych.go +++ b/cli/paych.go @@ -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 } diff --git a/cli/paych_test.go b/cli/paych_test.go index 11a08fb2c..31d41e36a 100644 --- a/cli/paych_test.go +++ b/cli/paych_test.go @@ -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 + 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 --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 --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= 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 { diff --git a/markets/retrievaladapter/client.go b/markets/retrievaladapter/client.go index 59eca595c..e57a11bd1 100644 --- a/markets/retrievaladapter/client.go +++ b/markets/retrievaladapter/client.go @@ -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) { diff --git a/node/impl/paych/paych.go b/node/impl/paych/paych.go index 5ce88c501..e998f969f 100644 --- a/node/impl/paych/paych.go +++ b/node/impl/paych/paych.go @@ -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) { diff --git a/paychmgr/accessorcache.go b/paychmgr/accessorcache.go index 43223200d..176fbdd11 100644 --- a/paychmgr/accessorcache.go +++ b/paychmgr/accessorcache.go @@ -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 diff --git a/paychmgr/manager.go b/paychmgr/manager.go index b1755a7d3..093afc0e9 100644 --- a/paychmgr/manager.go +++ b/paychmgr/manager.go @@ -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 { diff --git a/paychmgr/mock_test.go b/paychmgr/mock_test.go index 54519bae2..d2aa047ee 100644 --- a/paychmgr/mock_test.go +++ b/paychmgr/mock_test.go @@ -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 +} diff --git a/paychmgr/paych.go b/paychmgr/paych.go index 0f12adb11..be43aaf9b 100644 --- a/paychmgr/paych.go +++ b/paychmgr/paych.go @@ -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 diff --git a/paychmgr/paych_test.go b/paychmgr/paych_test.go index 4439fb2fb..e1ae487e1 100644 --- a/paychmgr/paych_test.go +++ b/paychmgr/paych_test.go @@ -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) - } - - // 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) +type testScaffold struct { + mgr *Manager + mock *mockManagerAPI + ch address.Address + amt big.Int + fromAcct address.Address + fromKeyPrivate []byte } -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) { diff --git a/paychmgr/paychget_test.go b/paychmgr/paychget_test.go index 64ef6f9d7..8eff08bdd 100644 --- a/paychmgr/paychget_test.go +++ b/paychmgr/paychget_test.go @@ -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) diff --git a/paychmgr/simple.go b/paychmgr/simple.go index b2f3c4404..99fd23334 100644 --- a/paychmgr/simple.go +++ b/paychmgr/simple.go @@ -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() +}