Merge pull request #2346 from filecoin-project/feat/paychmgr-tst
Refactor payment channel manager
This commit is contained in:
commit
6a6bcd09ee
@ -4,7 +4,8 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/filecoin-project/lotus/api"
|
||||
|
||||
cborutil "github.com/filecoin-project/go-cbor-util"
|
||||
"github.com/filecoin-project/specs-actors/actors/builtin"
|
||||
@ -34,9 +35,14 @@ type ManagerApi struct {
|
||||
full.StateAPI
|
||||
}
|
||||
|
||||
type StateManagerApi interface {
|
||||
LoadActorState(ctx context.Context, a address.Address, out interface{}, ts *types.TipSet) (*types.Actor, error)
|
||||
Call(ctx context.Context, msg *types.Message, ts *types.TipSet) (*api.InvocResult, error)
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
store *Store
|
||||
sm *stmgr.StateManager
|
||||
sm StateManagerApi
|
||||
|
||||
mpool full.MpoolAPI
|
||||
wallet full.WalletAPI
|
||||
@ -54,84 +60,24 @@ func NewManager(sm *stmgr.StateManager, pchstore *Store, api ManagerApi) *Manage
|
||||
}
|
||||
}
|
||||
|
||||
func maxLaneFromState(st *paych.State) (uint64, error) {
|
||||
maxLane := uint64(math.MaxInt64)
|
||||
for _, state := range st.LaneStates {
|
||||
if (state.ID)+1 > maxLane+1 {
|
||||
maxLane = state.ID
|
||||
}
|
||||
// Used by the tests to supply mocks
|
||||
func newManager(sm StateManagerApi, pchstore *Store) *Manager {
|
||||
return &Manager{
|
||||
store: pchstore,
|
||||
sm: sm,
|
||||
}
|
||||
return maxLane, nil
|
||||
}
|
||||
|
||||
func (pm *Manager) TrackInboundChannel(ctx context.Context, ch address.Address) error {
|
||||
_, st, err := pm.loadPaychState(ctx, ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var account account.State
|
||||
_, err = pm.sm.LoadActorState(ctx, st.From, &account, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
from := account.Address
|
||||
_, err = pm.sm.LoadActorState(ctx, st.To, &account, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
to := account.Address
|
||||
|
||||
maxLane, err := maxLaneFromState(st)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return pm.store.TrackChannel(&ChannelInfo{
|
||||
Channel: ch,
|
||||
Control: to,
|
||||
Target: from,
|
||||
|
||||
Direction: DirInbound,
|
||||
NextLane: maxLane + 1,
|
||||
})
|
||||
}
|
||||
|
||||
func (pm *Manager) loadOutboundChannelInfo(ctx context.Context, ch address.Address) (*ChannelInfo, error) {
|
||||
_, st, err := pm.loadPaychState(ctx, ch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxLane, err := maxLaneFromState(st)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var account account.State
|
||||
_, err = pm.sm.LoadActorState(ctx, st.From, &account, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
from := account.Address
|
||||
_, err = pm.sm.LoadActorState(ctx, st.To, &account, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
to := account.Address
|
||||
|
||||
return &ChannelInfo{
|
||||
Channel: ch,
|
||||
Control: from,
|
||||
Target: to,
|
||||
|
||||
Direction: DirOutbound,
|
||||
NextLane: maxLane + 1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (pm *Manager) TrackOutboundChannel(ctx context.Context, ch address.Address) error {
|
||||
ci, err := pm.loadOutboundChannelInfo(ctx, ch)
|
||||
return pm.trackChannel(ctx, ch, DirOutbound)
|
||||
}
|
||||
|
||||
func (pm *Manager) TrackInboundChannel(ctx context.Context, ch address.Address) error {
|
||||
return pm.trackChannel(ctx, ch, DirInbound)
|
||||
}
|
||||
|
||||
func (pm *Manager) trackChannel(ctx context.Context, ch address.Address, dir uint64) error {
|
||||
ci, err := pm.loadStateChannelInfo(ctx, ch, dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -147,62 +93,73 @@ func (pm *Manager) GetChannelInfo(addr address.Address) (*ChannelInfo, error) {
|
||||
return pm.store.getChannelInfo(addr)
|
||||
}
|
||||
|
||||
// checks if the given voucher is valid (is or could become spendable at some point)
|
||||
// CheckVoucherValid checks if the given voucher is valid (is or could become spendable at some point)
|
||||
func (pm *Manager) CheckVoucherValid(ctx context.Context, ch address.Address, sv *paych.SignedVoucher) error {
|
||||
_, err := pm.checkVoucherValid(ctx, ch, sv)
|
||||
return err
|
||||
}
|
||||
|
||||
func (pm *Manager) checkVoucherValid(ctx context.Context, ch address.Address, sv *paych.SignedVoucher) (*paych.State, error) {
|
||||
act, pca, err := pm.loadPaychState(ctx, ch)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var account account.State
|
||||
_, err = pm.sm.LoadActorState(ctx, pca.From, &account, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
from := account.Address
|
||||
|
||||
// verify signature
|
||||
vb, err := sv.SigningBytes()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: technically, either party may create and sign a voucher.
|
||||
// However, for now, we only accept them from the channel creator.
|
||||
// More complex handling logic can be added later
|
||||
if err := sigs.Verify(sv.Signature, from, vb); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sendAmount := sv.Amount
|
||||
|
||||
// now check the lane state
|
||||
// TODO: should check against vouchers in our local store too
|
||||
// there might be something conflicting
|
||||
ls := findLane(pca.LaneStates, uint64(sv.Lane))
|
||||
if ls == nil {
|
||||
} else {
|
||||
if (ls.Nonce) >= sv.Nonce {
|
||||
return fmt.Errorf("nonce too low")
|
||||
}
|
||||
|
||||
sendAmount = types.BigSub(sv.Amount, ls.Redeemed)
|
||||
// Check the voucher against the highest known voucher nonce / value
|
||||
ls, err := pm.laneState(pca, ch, sv.Lane)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If there has been at least once voucher redeemed, and the voucher
|
||||
// nonce value is less than the highest known nonce
|
||||
if ls.Redeemed.Int64() > 0 && sv.Nonce <= ls.Nonce {
|
||||
return nil, fmt.Errorf("nonce too low")
|
||||
}
|
||||
// If the voucher amount is less than the highest known voucher amount
|
||||
if sv.Amount.LessThanEqual(ls.Redeemed) {
|
||||
return nil, fmt.Errorf("voucher amount is lower than amount for voucher with lower nonce")
|
||||
}
|
||||
|
||||
// Only send the difference between the voucher amount and what has already
|
||||
// been redeemed
|
||||
sendAmount = types.BigSub(sv.Amount, ls.Redeemed)
|
||||
|
||||
// TODO: also account for vouchers on other lanes we've received
|
||||
newTotal := types.BigAdd(sendAmount, pca.ToSend)
|
||||
if act.Balance.LessThan(newTotal) {
|
||||
return fmt.Errorf("not enough funds in channel to cover voucher")
|
||||
return nil, fmt.Errorf("not enough funds in channel to cover voucher")
|
||||
}
|
||||
|
||||
if len(sv.Merges) != 0 {
|
||||
return fmt.Errorf("dont currently support paych lane merges")
|
||||
return nil, fmt.Errorf("dont currently support paych lane merges")
|
||||
}
|
||||
|
||||
return nil
|
||||
return pca, nil
|
||||
}
|
||||
|
||||
// checks if the given voucher is currently spendable
|
||||
// CheckVoucherSpendable checks if the given voucher is currently spendable
|
||||
func (pm *Manager) CheckVoucherSpendable(ctx context.Context, ch address.Address, sv *paych.SignedVoucher, secret []byte, proof []byte) (bool, error) {
|
||||
owner, err := pm.getPaychOwner(ctx, ch)
|
||||
if err != nil {
|
||||
@ -267,10 +224,6 @@ func (pm *Manager) getPaychOwner(ctx context.Context, ch address.Address) (addre
|
||||
}
|
||||
|
||||
func (pm *Manager) AddVoucher(ctx context.Context, ch address.Address, sv *paych.SignedVoucher, proof []byte, minDelta types.BigInt) (types.BigInt, error) {
|
||||
if err := pm.CheckVoucherValid(ctx, ch, sv); err != nil {
|
||||
return types.NewInt(0), err
|
||||
}
|
||||
|
||||
pm.store.lk.Lock()
|
||||
defer pm.store.lk.Unlock()
|
||||
|
||||
@ -279,16 +232,7 @@ func (pm *Manager) AddVoucher(ctx context.Context, ch address.Address, sv *paych
|
||||
return types.NewInt(0), err
|
||||
}
|
||||
|
||||
laneState, err := pm.laneState(ctx, ch, uint64(sv.Lane))
|
||||
if err != nil {
|
||||
return types.NewInt(0), err
|
||||
}
|
||||
|
||||
if minDelta.GreaterThan(types.NewInt(0)) && laneState.Nonce > sv.Nonce {
|
||||
return types.NewInt(0), xerrors.Errorf("already storing voucher with higher nonce; %d > %d", laneState.Nonce, sv.Nonce)
|
||||
}
|
||||
|
||||
// look for duplicates
|
||||
// Check if the voucher has already been added
|
||||
for i, v := range ci.Vouchers {
|
||||
eq, err := cborutil.Equals(sv, v.Voucher)
|
||||
if err != nil {
|
||||
@ -297,22 +241,35 @@ func (pm *Manager) AddVoucher(ctx context.Context, ch address.Address, sv *paych
|
||||
if !eq {
|
||||
continue
|
||||
}
|
||||
if v.Proof != nil {
|
||||
if !bytes.Equal(v.Proof, proof) {
|
||||
log.Warnf("AddVoucher: multiple proofs for single voucher, storing both")
|
||||
break
|
||||
|
||||
// This is a duplicate voucher.
|
||||
// Update the proof on the existing voucher
|
||||
if len(proof) > 0 && !bytes.Equal(v.Proof, proof) {
|
||||
log.Warnf("AddVoucher: adding proof to stored voucher")
|
||||
ci.Vouchers[i] = &VoucherInfo{
|
||||
Voucher: v.Voucher,
|
||||
Proof: proof,
|
||||
}
|
||||
log.Warnf("AddVoucher: voucher re-added with matching proof")
|
||||
return types.NewInt(0), nil
|
||||
|
||||
return types.NewInt(0), pm.store.putChannelInfo(ci)
|
||||
}
|
||||
|
||||
log.Warnf("AddVoucher: adding proof to stored voucher")
|
||||
ci.Vouchers[i] = &VoucherInfo{
|
||||
Voucher: v.Voucher,
|
||||
Proof: proof,
|
||||
}
|
||||
// Otherwise just ignore the duplicate voucher
|
||||
log.Warnf("AddVoucher: voucher re-added with matching proof")
|
||||
return types.NewInt(0), nil
|
||||
}
|
||||
|
||||
return types.NewInt(0), pm.store.putChannelInfo(ci)
|
||||
// Check voucher validity
|
||||
pchState, err := pm.checkVoucherValid(ctx, ch, sv)
|
||||
if err != nil {
|
||||
return types.NewInt(0), err
|
||||
}
|
||||
|
||||
// The change in value is the delta between the voucher amount and
|
||||
// the highest previous voucher amount
|
||||
laneState, err := pm.laneState(pchState, ch, sv.Lane)
|
||||
if err != nil {
|
||||
return types.NewInt(0), err
|
||||
}
|
||||
|
||||
delta := types.BigSub(sv.Amount, laneState.Redeemed)
|
||||
@ -325,7 +282,7 @@ func (pm *Manager) AddVoucher(ctx context.Context, ch address.Address, sv *paych
|
||||
Proof: proof,
|
||||
})
|
||||
|
||||
if ci.NextLane <= (sv.Lane) {
|
||||
if ci.NextLane <= sv.Lane {
|
||||
ci.NextLane = sv.Lane + 1
|
||||
}
|
||||
|
||||
@ -333,6 +290,7 @@ func (pm *Manager) AddVoucher(ctx context.Context, ch address.Address, sv *paych
|
||||
}
|
||||
|
||||
func (pm *Manager) AllocateLane(ch address.Address) (uint64, error) {
|
||||
// TODO: should this take into account lane state?
|
||||
return pm.store.AllocateLane(ch)
|
||||
}
|
||||
|
||||
@ -355,6 +313,7 @@ func (pm *Manager) OutboundChanTo(from, to address.Address) (address.Address, er
|
||||
}
|
||||
|
||||
func (pm *Manager) NextNonceForLane(ctx context.Context, ch address.Address, lane uint64) (uint64, error) {
|
||||
// TODO: should this take into account lane state?
|
||||
vouchers, err := pm.store.VouchersForPaych(ch)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@ -362,9 +321,9 @@ func (pm *Manager) NextNonceForLane(ctx context.Context, ch address.Address, lan
|
||||
|
||||
var maxnonce uint64
|
||||
for _, v := range vouchers {
|
||||
if uint64(v.Voucher.Lane) == lane {
|
||||
if uint64(v.Voucher.Nonce) > maxnonce {
|
||||
maxnonce = uint64(v.Voucher.Nonce)
|
||||
if v.Voucher.Lane == lane {
|
||||
if v.Voucher.Nonce > maxnonce {
|
||||
maxnonce = v.Voucher.Nonce
|
||||
}
|
||||
}
|
||||
}
|
||||
|
566
paychmgr/paych_test.go
Normal file
566
paychmgr/paych_test.go
Normal file
@ -0,0 +1,566 @@
|
||||
package paychmgr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/filecoin-project/specs-actors/actors/builtin"
|
||||
"github.com/ipfs/go-cid"
|
||||
|
||||
"github.com/filecoin-project/lotus/lib/sigs"
|
||||
"github.com/filecoin-project/specs-actors/actors/crypto"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/filecoin-project/specs-actors/actors/abi/big"
|
||||
|
||||
"github.com/filecoin-project/specs-actors/actors/abi"
|
||||
tutils "github.com/filecoin-project/specs-actors/support/testing"
|
||||
|
||||
"github.com/filecoin-project/specs-actors/actors/builtin/paych"
|
||||
|
||||
"github.com/filecoin-project/specs-actors/actors/builtin/account"
|
||||
|
||||
"github.com/filecoin-project/go-address"
|
||||
"github.com/filecoin-project/lotus/api"
|
||||
"github.com/filecoin-project/lotus/chain/types"
|
||||
|
||||
ds "github.com/ipfs/go-datastore"
|
||||
ds_sync "github.com/ipfs/go-datastore/sync"
|
||||
)
|
||||
|
||||
type testPchState struct {
|
||||
actor *types.Actor
|
||||
state paych.State
|
||||
}
|
||||
|
||||
type mockStateManager struct {
|
||||
lk sync.Mutex
|
||||
accountState map[address.Address]account.State
|
||||
paychState map[address.Address]testPchState
|
||||
response *api.InvocResult
|
||||
}
|
||||
|
||||
func newMockStateManager() *mockStateManager {
|
||||
return &mockStateManager{
|
||||
accountState: make(map[address.Address]account.State),
|
||||
paychState: make(map[address.Address]testPchState),
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *mockStateManager) setAccountState(a address.Address, state account.State) {
|
||||
sm.lk.Lock()
|
||||
defer sm.lk.Unlock()
|
||||
sm.accountState[a] = state
|
||||
}
|
||||
|
||||
func (sm *mockStateManager) setPaychState(a address.Address, actor *types.Actor, state paych.State) {
|
||||
sm.lk.Lock()
|
||||
defer sm.lk.Unlock()
|
||||
sm.paychState[a] = testPchState{actor, state}
|
||||
}
|
||||
|
||||
func (sm *mockStateManager) LoadActorState(ctx context.Context, a address.Address, out interface{}, ts *types.TipSet) (*types.Actor, error) {
|
||||
sm.lk.Lock()
|
||||
defer sm.lk.Unlock()
|
||||
|
||||
if outState, ok := out.(*account.State); ok {
|
||||
*outState = sm.accountState[a]
|
||||
return nil, nil
|
||||
}
|
||||
if outState, ok := out.(*paych.State); ok {
|
||||
info := sm.paychState[a]
|
||||
*outState = info.state
|
||||
return info.actor, nil
|
||||
}
|
||||
panic(fmt.Sprintf("unexpected state type %v", out))
|
||||
}
|
||||
|
||||
func (sm *mockStateManager) Call(ctx context.Context, msg *types.Message, ts *types.TipSet) (*api.InvocResult, error) {
|
||||
return sm.response, nil
|
||||
}
|
||||
|
||||
func TestPaychOutbound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := NewStore(ds_sync.MutexWrap(ds.NewMapDatastore()))
|
||||
|
||||
ch := tutils.NewIDAddr(t, 100)
|
||||
from := tutils.NewIDAddr(t, 101)
|
||||
to := tutils.NewIDAddr(t, 102)
|
||||
fromAcct := tutils.NewIDAddr(t, 201)
|
||||
toAcct := tutils.NewIDAddr(t, 202)
|
||||
|
||||
sm := newMockStateManager()
|
||||
sm.setAccountState(fromAcct, account.State{Address: from})
|
||||
sm.setAccountState(toAcct, account.State{Address: to})
|
||||
sm.setPaychState(ch, nil, paych.State{
|
||||
From: fromAcct,
|
||||
To: toAcct,
|
||||
ToSend: big.NewInt(0),
|
||||
SettlingAt: abi.ChainEpoch(0),
|
||||
MinSettleHeight: abi.ChainEpoch(0),
|
||||
LaneStates: []*paych.LaneState{},
|
||||
})
|
||||
|
||||
mgr := newManager(sm, store)
|
||||
err := mgr.TrackOutboundChannel(ctx, ch)
|
||||
require.NoError(t, err)
|
||||
|
||||
ci, err := mgr.GetChannelInfo(ch)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ci.Channel, ch)
|
||||
require.Equal(t, ci.Control, from)
|
||||
require.Equal(t, ci.Target, to)
|
||||
require.EqualValues(t, ci.Direction, DirOutbound)
|
||||
require.EqualValues(t, ci.NextLane, 0)
|
||||
require.Len(t, ci.Vouchers, 0)
|
||||
}
|
||||
|
||||
func TestPaychInbound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := NewStore(ds_sync.MutexWrap(ds.NewMapDatastore()))
|
||||
|
||||
ch := tutils.NewIDAddr(t, 100)
|
||||
from := tutils.NewIDAddr(t, 101)
|
||||
to := tutils.NewIDAddr(t, 102)
|
||||
fromAcct := tutils.NewIDAddr(t, 201)
|
||||
toAcct := tutils.NewIDAddr(t, 202)
|
||||
|
||||
sm := newMockStateManager()
|
||||
sm.setAccountState(fromAcct, account.State{Address: from})
|
||||
sm.setAccountState(toAcct, account.State{Address: to})
|
||||
sm.setPaychState(ch, nil, paych.State{
|
||||
From: fromAcct,
|
||||
To: toAcct,
|
||||
ToSend: big.NewInt(0),
|
||||
SettlingAt: abi.ChainEpoch(0),
|
||||
MinSettleHeight: abi.ChainEpoch(0),
|
||||
LaneStates: []*paych.LaneState{},
|
||||
})
|
||||
|
||||
mgr := newManager(sm, store)
|
||||
err := mgr.TrackInboundChannel(ctx, ch)
|
||||
require.NoError(t, err)
|
||||
|
||||
ci, err := mgr.GetChannelInfo(ch)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ci.Channel, ch)
|
||||
require.Equal(t, ci.Control, to)
|
||||
require.Equal(t, ci.Target, from)
|
||||
require.EqualValues(t, ci.Direction, DirInbound)
|
||||
require.EqualValues(t, ci.NextLane, 0)
|
||||
require.Len(t, ci.Vouchers, 0)
|
||||
}
|
||||
|
||||
func TestCheckVoucherValid(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
fromKeyPrivate, fromKeyPublic := testGenerateKeyPair(t)
|
||||
randKeyPrivate, _ := testGenerateKeyPair(t)
|
||||
|
||||
ch := tutils.NewIDAddr(t, 100)
|
||||
from := tutils.NewSECP256K1Addr(t, string(fromKeyPublic))
|
||||
to := tutils.NewSECP256K1Addr(t, "secpTo")
|
||||
fromAcct := tutils.NewActorAddr(t, "fromAct")
|
||||
toAcct := tutils.NewActorAddr(t, "toAct")
|
||||
|
||||
sm := newMockStateManager()
|
||||
sm.setAccountState(fromAcct, account.State{Address: from})
|
||||
sm.setAccountState(toAcct, account.State{Address: to})
|
||||
|
||||
tcases := []struct {
|
||||
name string
|
||||
expectError bool
|
||||
key []byte
|
||||
actorBalance big.Int
|
||||
toSend big.Int
|
||||
voucherAmount big.Int
|
||||
voucherLane uint64
|
||||
voucherNonce uint64
|
||||
laneStates []*paych.LaneState
|
||||
}{{
|
||||
name: "passes when voucher amount < balance",
|
||||
key: fromKeyPrivate,
|
||||
actorBalance: big.NewInt(10),
|
||||
toSend: big.NewInt(0),
|
||||
voucherAmount: big.NewInt(5),
|
||||
}, {
|
||||
name: "fails when funds too low",
|
||||
expectError: true,
|
||||
key: fromKeyPrivate,
|
||||
actorBalance: big.NewInt(5),
|
||||
toSend: big.NewInt(0),
|
||||
voucherAmount: big.NewInt(10),
|
||||
}, {
|
||||
name: "fails when invalid signature",
|
||||
expectError: true,
|
||||
key: randKeyPrivate,
|
||||
actorBalance: big.NewInt(10),
|
||||
toSend: big.NewInt(0),
|
||||
voucherAmount: big.NewInt(5),
|
||||
}, {
|
||||
name: "fails when nonce too low",
|
||||
expectError: true,
|
||||
key: fromKeyPrivate,
|
||||
actorBalance: big.NewInt(10),
|
||||
toSend: big.NewInt(0),
|
||||
voucherAmount: big.NewInt(5),
|
||||
voucherLane: 1,
|
||||
voucherNonce: 2,
|
||||
laneStates: []*paych.LaneState{{
|
||||
ID: 1,
|
||||
Redeemed: big.NewInt(2),
|
||||
Nonce: 3,
|
||||
}},
|
||||
}, {
|
||||
name: "passes when nonce higher",
|
||||
key: fromKeyPrivate,
|
||||
actorBalance: big.NewInt(10),
|
||||
toSend: big.NewInt(0),
|
||||
voucherAmount: big.NewInt(5),
|
||||
voucherLane: 1,
|
||||
voucherNonce: 3,
|
||||
laneStates: []*paych.LaneState{{
|
||||
ID: 1,
|
||||
Redeemed: big.NewInt(2),
|
||||
Nonce: 2,
|
||||
}},
|
||||
}, {
|
||||
name: "passes when nonce for different lane",
|
||||
key: fromKeyPrivate,
|
||||
actorBalance: big.NewInt(10),
|
||||
toSend: big.NewInt(0),
|
||||
voucherAmount: big.NewInt(5),
|
||||
voucherLane: 2,
|
||||
voucherNonce: 2,
|
||||
laneStates: []*paych.LaneState{{
|
||||
ID: 1,
|
||||
Redeemed: big.NewInt(2),
|
||||
Nonce: 3,
|
||||
}},
|
||||
}, {
|
||||
name: "fails when voucher has higher nonce but lower value than lane state",
|
||||
expectError: true,
|
||||
key: fromKeyPrivate,
|
||||
actorBalance: big.NewInt(10),
|
||||
toSend: big.NewInt(0),
|
||||
voucherAmount: big.NewInt(5),
|
||||
voucherLane: 1,
|
||||
voucherNonce: 3,
|
||||
laneStates: []*paych.LaneState{{
|
||||
ID: 1,
|
||||
Redeemed: big.NewInt(6),
|
||||
Nonce: 2,
|
||||
}},
|
||||
}, {
|
||||
name: "fails when voucher + ToSend > balance",
|
||||
expectError: true,
|
||||
key: fromKeyPrivate,
|
||||
actorBalance: big.NewInt(10),
|
||||
toSend: big.NewInt(9),
|
||||
voucherAmount: big.NewInt(2),
|
||||
}, {
|
||||
// required balance = toSend + (voucher - redeemed)
|
||||
// = 0 + (11 - 2)
|
||||
// = 9
|
||||
// So required balance: 9 < actor balance: 10
|
||||
name: "passes when voucher - redeemed < balance",
|
||||
key: fromKeyPrivate,
|
||||
actorBalance: big.NewInt(10),
|
||||
toSend: big.NewInt(0),
|
||||
voucherAmount: big.NewInt(11),
|
||||
voucherLane: 1,
|
||||
voucherNonce: 3,
|
||||
laneStates: []*paych.LaneState{{
|
||||
ID: 1,
|
||||
Redeemed: big.NewInt(2),
|
||||
Nonce: 2,
|
||||
}},
|
||||
}}
|
||||
|
||||
for _, tcase := range tcases {
|
||||
tcase := tcase
|
||||
t.Run(tcase.name, func(t *testing.T) {
|
||||
store := NewStore(ds_sync.MutexWrap(ds.NewMapDatastore()))
|
||||
|
||||
act := &types.Actor{
|
||||
Code: builtin.AccountActorCodeID,
|
||||
Head: cid.Cid{},
|
||||
Nonce: 0,
|
||||
Balance: tcase.actorBalance,
|
||||
}
|
||||
sm.setPaychState(ch, act, paych.State{
|
||||
From: fromAcct,
|
||||
To: toAcct,
|
||||
ToSend: tcase.toSend,
|
||||
SettlingAt: abi.ChainEpoch(0),
|
||||
MinSettleHeight: abi.ChainEpoch(0),
|
||||
LaneStates: tcase.laneStates,
|
||||
})
|
||||
|
||||
mgr := newManager(sm, store)
|
||||
err := mgr.TrackInboundChannel(ctx, ch)
|
||||
require.NoError(t, err)
|
||||
|
||||
sv := testCreateVoucher(t, tcase.voucherLane, tcase.voucherNonce, tcase.voucherAmount, tcase.key)
|
||||
|
||||
err = mgr.CheckVoucherValid(ctx, ch, sv)
|
||||
if tcase.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddVoucherDelta(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Set up a manager with a single payment channel
|
||||
mgr, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t)
|
||||
|
||||
voucherLane := uint64(1)
|
||||
|
||||
// Expect error when adding a voucher whose amount is less than minDelta
|
||||
minDelta := big.NewInt(2)
|
||||
nonce := uint64(1)
|
||||
voucherAmount := big.NewInt(1)
|
||||
sv := testCreateVoucher(t, voucherLane, nonce, voucherAmount, fromKeyPrivate)
|
||||
_, err := mgr.AddVoucher(ctx, 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 = testCreateVoucher(t, voucherLane, nonce, voucherAmount, fromKeyPrivate)
|
||||
delta, err := mgr.AddVoucher(ctx, 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 = testCreateVoucher(t, voucherLane, nonce, voucherAmount, fromKeyPrivate)
|
||||
delta, err = mgr.AddVoucher(ctx, ch, sv, nil, minDelta)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, delta.Int64(), 3)
|
||||
|
||||
// Check that delta is correct when voucher added to a different lane
|
||||
nonce = uint64(1)
|
||||
voucherAmount = big.NewInt(6)
|
||||
voucherLane = uint64(2)
|
||||
sv = testCreateVoucher(t, voucherLane, nonce, voucherAmount, fromKeyPrivate)
|
||||
delta, err = mgr.AddVoucher(ctx, ch, sv, nil, minDelta)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, delta.Int64(), 6)
|
||||
}
|
||||
|
||||
func TestAddVoucherNextLane(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Set up a manager with a single payment channel
|
||||
mgr, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t)
|
||||
|
||||
minDelta := big.NewInt(0)
|
||||
voucherAmount := big.NewInt(2)
|
||||
|
||||
// Add a voucher in lane 2
|
||||
nonce := uint64(1)
|
||||
voucherLane := uint64(2)
|
||||
sv := testCreateVoucher(t, voucherLane, nonce, voucherAmount, fromKeyPrivate)
|
||||
_, err := mgr.AddVoucher(ctx, ch, sv, nil, minDelta)
|
||||
require.NoError(t, err)
|
||||
|
||||
ci, err := mgr.GetChannelInfo(ch)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, ci.NextLane, 3)
|
||||
|
||||
// Add a voucher in lane 1
|
||||
voucherLane = uint64(1)
|
||||
sv = testCreateVoucher(t, voucherLane, nonce, voucherAmount, fromKeyPrivate)
|
||||
_, err = mgr.AddVoucher(ctx, ch, sv, nil, minDelta)
|
||||
require.NoError(t, err)
|
||||
|
||||
ci, err = mgr.GetChannelInfo(ch)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, ci.NextLane, 3)
|
||||
|
||||
// Add a voucher in lane 5
|
||||
voucherLane = uint64(5)
|
||||
sv = testCreateVoucher(t, voucherLane, nonce, voucherAmount, fromKeyPrivate)
|
||||
_, err = mgr.AddVoucher(ctx, ch, sv, nil, minDelta)
|
||||
require.NoError(t, err)
|
||||
|
||||
ci, err = mgr.GetChannelInfo(ch)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, ci.NextLane, 6)
|
||||
}
|
||||
|
||||
func TestAddVoucherProof(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Set up a manager with a single payment channel
|
||||
mgr, ch, fromKeyPrivate := testSetupMgrWithChannel(ctx, t)
|
||||
|
||||
nonce := uint64(1)
|
||||
voucherAmount := big.NewInt(1)
|
||||
minDelta := big.NewInt(0)
|
||||
voucherAmount = big.NewInt(2)
|
||||
voucherLane := uint64(1)
|
||||
|
||||
// Add a voucher with no proof
|
||||
var proof []byte
|
||||
sv := testCreateVoucher(t, voucherLane, nonce, voucherAmount, fromKeyPrivate)
|
||||
_, err := mgr.AddVoucher(ctx, ch, sv, nil, minDelta)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Expect one voucher with no proof
|
||||
ci, err := mgr.GetChannelInfo(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.AddVoucher(ctx, ch, sv, proof, minDelta)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Expect one voucher with no proof
|
||||
ci, err = mgr.GetChannelInfo(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.AddVoucher(ctx, ch, sv, proof, minDelta)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should add proof to existing voucher
|
||||
ci, err = mgr.GetChannelInfo(ch)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ci.Vouchers, 1)
|
||||
require.Len(t, ci.Vouchers[0].Proof, 1)
|
||||
}
|
||||
|
||||
func TestAllocateLane(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Set up a manager with a single payment channel
|
||||
mgr, ch, _ := testSetupMgrWithChannel(ctx, t)
|
||||
|
||||
// First lane should be 0
|
||||
lane, err := mgr.AllocateLane(ch)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, lane, 0)
|
||||
|
||||
// Next lane should be 1
|
||||
lane, err = mgr.AllocateLane(ch)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, lane, 1)
|
||||
}
|
||||
|
||||
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
|
||||
// lane 2: nonce 7
|
||||
voucherLane := uint64(1)
|
||||
for _, nonce := range []uint64{2, 4} {
|
||||
voucherAmount = big.Add(voucherAmount, big.NewInt(1))
|
||||
sv := testCreateVoucher(t, voucherLane, nonce, voucherAmount, key)
|
||||
_, err := mgr.AddVoucher(ctx, ch, sv, nil, minDelta)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
voucherLane = uint64(2)
|
||||
nonce := uint64(7)
|
||||
sv := testCreateVoucher(t, voucherLane, nonce, voucherAmount, key)
|
||||
_, err = mgr.AddVoucher(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, address.Address, []byte) {
|
||||
fromKeyPrivate, fromKeyPublic := testGenerateKeyPair(t)
|
||||
|
||||
ch := tutils.NewIDAddr(t, 100)
|
||||
from := tutils.NewSECP256K1Addr(t, string(fromKeyPublic))
|
||||
to := tutils.NewSECP256K1Addr(t, "secpTo")
|
||||
fromAcct := tutils.NewActorAddr(t, "fromAct")
|
||||
toAcct := tutils.NewActorAddr(t, "toAct")
|
||||
|
||||
sm := newMockStateManager()
|
||||
sm.setAccountState(fromAcct, account.State{Address: from})
|
||||
sm.setAccountState(toAcct, account.State{Address: to})
|
||||
|
||||
act := &types.Actor{
|
||||
Code: builtin.AccountActorCodeID,
|
||||
Head: cid.Cid{},
|
||||
Nonce: 0,
|
||||
Balance: big.NewInt(10),
|
||||
}
|
||||
sm.setPaychState(ch, act, paych.State{
|
||||
From: fromAcct,
|
||||
To: toAcct,
|
||||
ToSend: big.NewInt(0),
|
||||
SettlingAt: abi.ChainEpoch(0),
|
||||
MinSettleHeight: abi.ChainEpoch(0),
|
||||
LaneStates: []*paych.LaneState{},
|
||||
})
|
||||
|
||||
store := NewStore(ds_sync.MutexWrap(ds.NewMapDatastore()))
|
||||
mgr := newManager(sm, store)
|
||||
err := mgr.TrackInboundChannel(ctx, ch)
|
||||
require.NoError(t, err)
|
||||
return mgr, ch, fromKeyPrivate
|
||||
}
|
||||
|
||||
func testGenerateKeyPair(t *testing.T) ([]byte, []byte) {
|
||||
priv, err := sigs.Generate(crypto.SigTypeSecp256k1)
|
||||
require.NoError(t, err)
|
||||
pub, err := sigs.ToPublic(crypto.SigTypeSecp256k1, priv)
|
||||
require.NoError(t, err)
|
||||
return priv, pub
|
||||
}
|
||||
|
||||
func testCreateVoucher(t *testing.T, voucherLane uint64, nonce uint64, voucherAmount big.Int, key []byte) *paych.SignedVoucher {
|
||||
sv := &paych.SignedVoucher{
|
||||
Lane: voucherLane,
|
||||
Nonce: nonce,
|
||||
Amount: voucherAmount,
|
||||
}
|
||||
|
||||
signingBytes, err := sv.SigningBytes()
|
||||
require.NoError(t, err)
|
||||
sig, err := sigs.Sign(crypto.SigTypeSecp256k1, key, signingBytes)
|
||||
require.NoError(t, err)
|
||||
sv.Signature = sig
|
||||
return sv
|
||||
}
|
@ -75,7 +75,7 @@ func (pm *Manager) waitForPaychCreateMsg(ctx context.Context, mcid cid.Cid) {
|
||||
}
|
||||
paychaddr := decodedReturn.RobustAddress
|
||||
|
||||
ci, err := pm.loadOutboundChannelInfo(ctx, paychaddr)
|
||||
ci, err := pm.loadStateChannelInfo(ctx, paychaddr, DirOutbound)
|
||||
if err != nil {
|
||||
log.Errorf("loading channel info: %w", err)
|
||||
return
|
||||
|
@ -3,6 +3,8 @@ package paychmgr
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/filecoin-project/specs-actors/actors/builtin/account"
|
||||
|
||||
"github.com/filecoin-project/go-address"
|
||||
"github.com/filecoin-project/specs-actors/actors/builtin/paych"
|
||||
xerrors "golang.org/x/xerrors"
|
||||
@ -20,10 +22,59 @@ func (pm *Manager) loadPaychState(ctx context.Context, ch address.Address) (*typ
|
||||
return act, &pcast, nil
|
||||
}
|
||||
|
||||
func (pm *Manager) loadStateChannelInfo(ctx context.Context, ch address.Address, dir uint64) (*ChannelInfo, error) {
|
||||
_, st, err := pm.loadPaychState(ctx, ch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var account account.State
|
||||
_, err = pm.sm.LoadActorState(ctx, st.From, &account, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
from := account.Address
|
||||
_, err = pm.sm.LoadActorState(ctx, st.To, &account, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
to := account.Address
|
||||
|
||||
ci := &ChannelInfo{
|
||||
Channel: ch,
|
||||
Direction: dir,
|
||||
NextLane: nextLaneFromState(st),
|
||||
}
|
||||
|
||||
if dir == DirOutbound {
|
||||
ci.Control = from
|
||||
ci.Target = to
|
||||
} else {
|
||||
ci.Control = to
|
||||
ci.Target = from
|
||||
}
|
||||
|
||||
return ci, nil
|
||||
}
|
||||
|
||||
func nextLaneFromState(st *paych.State) uint64 {
|
||||
if len(st.LaneStates) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
maxLane := st.LaneStates[0].ID
|
||||
for _, state := range st.LaneStates {
|
||||
if state.ID > maxLane {
|
||||
maxLane = state.ID
|
||||
}
|
||||
}
|
||||
return maxLane + 1
|
||||
}
|
||||
|
||||
func findLane(states []*paych.LaneState, lane uint64) *paych.LaneState {
|
||||
var ls *paych.LaneState
|
||||
for _, laneState := range states {
|
||||
if uint64(laneState.ID) == lane {
|
||||
if laneState.ID == lane {
|
||||
ls = laneState
|
||||
break
|
||||
}
|
||||
@ -31,16 +82,12 @@ func findLane(states []*paych.LaneState, lane uint64) *paych.LaneState {
|
||||
return ls
|
||||
}
|
||||
|
||||
func (pm *Manager) laneState(ctx context.Context, ch address.Address, lane uint64) (paych.LaneState, error) {
|
||||
_, state, err := pm.loadPaychState(ctx, ch)
|
||||
if err != nil {
|
||||
return paych.LaneState{}, err
|
||||
}
|
||||
|
||||
func (pm *Manager) laneState(state *paych.State, ch address.Address, lane uint64) (paych.LaneState, error) {
|
||||
// TODO: we probably want to call UpdateChannelState with all vouchers to be fully correct
|
||||
// (but technically dont't need to)
|
||||
// TODO: make sure this is correct
|
||||
|
||||
// Get the lane state from the chain
|
||||
ls := findLane(state.LaneStates, lane)
|
||||
if ls == nil {
|
||||
ls = &paych.LaneState{
|
||||
@ -50,6 +97,7 @@ func (pm *Manager) laneState(ctx context.Context, ch address.Address, lane uint6
|
||||
}
|
||||
}
|
||||
|
||||
// Apply locally stored vouchers
|
||||
vouchers, err := pm.store.VouchersForPaych(ch)
|
||||
if err != nil {
|
||||
if err == ErrChannelNotTracked {
|
||||
|
@ -60,7 +60,7 @@ func TestStore(t *testing.T) {
|
||||
require.Len(t, vouchers, 1)
|
||||
|
||||
// Requesting voucher for non-existent channel should error
|
||||
vouchers, err = store.VouchersForPaych(tutils.NewIDAddr(t, 300))
|
||||
_, err = store.VouchersForPaych(tutils.NewIDAddr(t, 300))
|
||||
require.Equal(t, err, ErrChannelNotTracked)
|
||||
|
||||
// Allocate lane for channel
|
||||
@ -74,7 +74,7 @@ func TestStore(t *testing.T) {
|
||||
require.Equal(t, lane, uint64(1))
|
||||
|
||||
// Allocate next lane for non-existent channel should error
|
||||
lane, err = store.AllocateLane(tutils.NewIDAddr(t, 300))
|
||||
_, err = store.AllocateLane(tutils.NewIDAddr(t, 300))
|
||||
require.Equal(t, err, ErrChannelNotTracked)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user