feat: account for other vouchers when calculating voucher validity

This commit is contained in:
Dirk McCormick 2020-07-10 14:06:52 -04:00
parent 6a6bcd09ee
commit f07c7377b6
3 changed files with 258 additions and 65 deletions

View File

@ -5,6 +5,8 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/filecoin-project/specs-actors/actors/abi/big"
"github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/api"
cborutil "github.com/filecoin-project/go-cbor-util" cborutil "github.com/filecoin-project/go-cbor-util"
@ -99,14 +101,14 @@ func (pm *Manager) CheckVoucherValid(ctx context.Context, ch address.Address, sv
return err return err
} }
func (pm *Manager) checkVoucherValid(ctx context.Context, ch address.Address, sv *paych.SignedVoucher) (*paych.State, error) { func (pm *Manager) checkVoucherValid(ctx context.Context, ch address.Address, sv *paych.SignedVoucher) (map[uint64]*paych.LaneState, error) {
act, pca, err := pm.loadPaychState(ctx, ch) act, pchState, err := pm.loadPaychState(ctx, ch)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var account account.State var account account.State
_, err = pm.sm.LoadActorState(ctx, pca.From, &account, nil) _, err = pm.sm.LoadActorState(ctx, pchState.From, &account, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -125,29 +127,47 @@ func (pm *Manager) checkVoucherValid(ctx context.Context, ch address.Address, sv
return nil, err return nil, err
} }
sendAmount := sv.Amount
// Check the voucher against the highest known voucher nonce / value // Check the voucher against the highest known voucher nonce / value
ls, err := pm.laneState(pca, ch, sv.Lane) laneStates, err := pm.laneState(pchState, ch)
if err != nil { if err != nil {
return nil, err 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 the new voucher nonce value is less than the highest known
if ls.Redeemed.Int64() > 0 && sv.Nonce <= ls.Nonce { // nonce for the lane
ls, lsExists := laneStates[sv.Lane]
if lsExists && sv.Nonce <= ls.Nonce {
return nil, fmt.Errorf("nonce too low") return nil, fmt.Errorf("nonce too low")
} }
// If the voucher amount is less than the highest known voucher amount // If the voucher amount is less than the highest known voucher amount
if sv.Amount.LessThanEqual(ls.Redeemed) { if lsExists && sv.Amount.LessThanEqual(ls.Redeemed) {
return nil, fmt.Errorf("voucher amount is lower than amount for voucher with lower nonce") 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 // Total redeemed is the total redeemed amount for all lanes, including
// been redeemed // the new voucher
sendAmount = types.BigSub(sv.Amount, ls.Redeemed) // eg
//
// lane 1 redeemed: 3
// lane 2 redeemed: 2
// voucher for lane 1: 5
//
// Voucher supersedes lane 1 redeemed, therefore
// effective lane 1 redeemed: 5
//
// lane 1: 5
// lane 2: 2
// -
// total: 7
totalRedeemed, err := pm.totalRedeemedWithVoucher(laneStates, sv)
if err != nil {
return nil, err
}
// TODO: also account for vouchers on other lanes we've received // Total required balance = total redeemed + toSend
newTotal := types.BigAdd(sendAmount, pca.ToSend) // Must not exceed actor balance
newTotal := types.BigAdd(totalRedeemed, pchState.ToSend)
if act.Balance.LessThan(newTotal) { if act.Balance.LessThan(newTotal) {
return nil, fmt.Errorf("not enough funds in channel to cover voucher") return nil, fmt.Errorf("not enough funds in channel to cover voucher")
} }
@ -156,7 +176,7 @@ func (pm *Manager) checkVoucherValid(ctx context.Context, ch address.Address, sv
return nil, fmt.Errorf("dont currently support paych lane merges") return nil, fmt.Errorf("dont currently support paych lane merges")
} }
return pca, nil return laneStates, nil
} }
// CheckVoucherSpendable checks if the given voucher is currently spendable // CheckVoucherSpendable checks if the given voucher is currently spendable
@ -260,21 +280,22 @@ func (pm *Manager) AddVoucher(ctx context.Context, ch address.Address, sv *paych
} }
// Check voucher validity // Check voucher validity
pchState, err := pm.checkVoucherValid(ctx, ch, sv) laneStates, err := pm.checkVoucherValid(ctx, ch, sv)
if err != nil { if err != nil {
return types.NewInt(0), err return types.NewInt(0), err
} }
// The change in value is the delta between the voucher amount and // The change in value is the delta between the voucher amount and
// the highest previous voucher amount // the highest previous voucher amount for the lane
laneState, err := pm.laneState(pchState, ch, sv.Lane) laneState, exists := laneStates[sv.Lane]
if err != nil { redeemed := big.NewInt(0)
return types.NewInt(0), err if exists {
redeemed = laneState.Redeemed
} }
delta := types.BigSub(sv.Amount, laneState.Redeemed) delta := types.BigSub(sv.Amount, redeemed)
if minDelta.GreaterThan(delta) { if minDelta.GreaterThan(delta) {
return delta, xerrors.Errorf("addVoucher: supplied token amount too low; minD=%s, D=%s; laneAmt=%s; v.Amt=%s", minDelta, delta, laneState.Redeemed, sv.Amount) return delta, xerrors.Errorf("addVoucher: supplied token amount too low; minD=%s, D=%s; laneAmt=%s; v.Amt=%s", minDelta, delta, redeemed, sv.Amount)
} }
ci.Vouchers = append(ci.Vouchers, &VoucherInfo{ ci.Vouchers = append(ci.Vouchers, &VoucherInfo{

View File

@ -262,21 +262,42 @@ func TestCheckVoucherValid(t *testing.T) {
toSend: big.NewInt(9), toSend: big.NewInt(9),
voucherAmount: big.NewInt(2), voucherAmount: big.NewInt(2),
}, { }, {
// required balance = toSend + (voucher - redeemed) // voucher supersedes lane 1 redeemed so
// = 0 + (11 - 2) // lane 1 effective redeemed = voucher amount
// = 9 //
// So required balance: 9 < actor balance: 10 // required balance = toSend + total redeemed
name: "passes when voucher - redeemed < balance", // = 1 + 6 (lane1)
// = 7
// So required balance: 7 < actor balance: 10
name: "passes when voucher + total redeemed <= balance",
key: fromKeyPrivate, key: fromKeyPrivate,
actorBalance: big.NewInt(10), actorBalance: big.NewInt(10),
toSend: big.NewInt(0), toSend: big.NewInt(1),
voucherAmount: big.NewInt(11), voucherAmount: big.NewInt(6),
voucherLane: 1, voucherLane: 1,
voucherNonce: 3, voucherNonce: 2,
laneStates: []*paych.LaneState{{ laneStates: []*paych.LaneState{{
ID: 1, ID: 1, // Lane 1 (same as voucher lane 1)
Redeemed: big.NewInt(2), Redeemed: big.NewInt(4),
Nonce: 2, Nonce: 1,
}},
}, {
// required balance = toSend + total redeemed
// = 1 + 4 (lane 2) + 6 (voucher lane 1)
// = 11
// So required balance: 11 > actor balance: 10
name: "fails when voucher + total redeemed > balance",
expectError: true,
key: fromKeyPrivate,
actorBalance: big.NewInt(10),
toSend: big.NewInt(1),
voucherAmount: big.NewInt(6),
voucherLane: 1,
voucherNonce: 1,
laneStates: []*paych.LaneState{{
ID: 2, // Lane 2 (different from voucher lane 1)
Redeemed: big.NewInt(4),
Nonce: 1,
}}, }},
}} }}
@ -316,6 +337,139 @@ func TestCheckVoucherValid(t *testing.T) {
} }
} }
func TestCheckVoucherValidCountingAllLanes(t *testing.T) {
ctx := context.Background()
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")
minDelta := big.NewInt(0)
sm := newMockStateManager()
sm.setAccountState(fromAcct, account.State{Address: from})
sm.setAccountState(toAcct, account.State{Address: to})
store := NewStore(ds_sync.MutexWrap(ds.NewMapDatastore()))
actorBalance := big.NewInt(10)
toSend := big.NewInt(1)
laneStates := []*paych.LaneState{{
ID: 1,
Nonce: 1,
Redeemed: big.NewInt(3),
}, {
ID: 2,
Nonce: 1,
Redeemed: big.NewInt(4),
}}
act := &types.Actor{
Code: builtin.AccountActorCodeID,
Head: cid.Cid{},
Nonce: 0,
Balance: actorBalance,
}
sm.setPaychState(ch, act, paych.State{
From: fromAcct,
To: toAcct,
ToSend: toSend,
SettlingAt: abi.ChainEpoch(0),
MinSettleHeight: abi.ChainEpoch(0),
LaneStates: laneStates,
})
mgr := newManager(sm, store)
err := mgr.TrackInboundChannel(ctx, ch)
require.NoError(t, err)
//
// Should not be possible to add a voucher with a value such that
// <total lane Redeemed> + toSend > <actor balance>
//
// lane 1 redeemed: 3
// voucher amount (lane 1): 6
// lane 1 redeemed (with voucher): 6
//
// Lane 1: 6
// Lane 2: 4
// toSend: 1
// --
// total: 11
//
// actor balance is 10 so total is too high.
//
voucherLane := uint64(1)
voucherNonce := uint64(2)
voucherAmount := big.NewInt(6)
sv := testCreateVoucher(t, voucherLane, voucherNonce, voucherAmount, fromKeyPrivate)
err = mgr.CheckVoucherValid(ctx, ch, sv)
require.Error(t, err)
//
// lane 1 redeemed: 3
// voucher amount (lane 1): 4
// lane 1 redeemed (with voucher): 4
//
// Lane 1: 4
// Lane 2: 4
// toSend: 1
// --
// total: 9
//
// actor balance is 10 so total is ok.
//
voucherAmount = big.NewInt(4)
sv = testCreateVoucher(t, voucherLane, voucherNonce, voucherAmount, fromKeyPrivate)
err = mgr.CheckVoucherValid(ctx, ch, sv)
require.NoError(t, err)
// Add voucher to lane 1, so Lane 1 effective redeemed
// (with first voucher) is now 4
_, err = mgr.AddVoucher(ctx, ch, sv, nil, minDelta)
require.NoError(t, err)
//
// lane 1 redeemed: 4
// voucher amount (lane 1): 6
// lane 1 redeemed (with voucher): 6
//
// Lane 1: 6
// Lane 2: 4
// toSend: 1
// --
// total: 11
//
// actor balance is 10 so total is too high.
//
voucherNonce++
voucherAmount = big.NewInt(6)
sv = testCreateVoucher(t, voucherLane, voucherNonce, voucherAmount, fromKeyPrivate)
err = mgr.CheckVoucherValid(ctx, ch, sv)
require.Error(t, err)
//
// lane 1 redeemed: 4
// voucher amount (lane 1): 5
// lane 1 redeemed (with voucher): 5
//
// Lane 1: 5
// Lane 2: 4
// toSend: 1
// --
// total: 10
//
// actor balance is 10 so total is ok.
//
voucherAmount = big.NewInt(5)
sv = testCreateVoucher(t, voucherLane, voucherNonce, voucherAmount, fromKeyPrivate)
err = mgr.CheckVoucherValid(ctx, ch, sv)
require.NoError(t, err)
}
func TestAddVoucherDelta(t *testing.T) { func TestAddVoucherDelta(t *testing.T) {
ctx := context.Background() ctx := context.Background()
@ -524,7 +678,7 @@ func testSetupMgrWithChannel(ctx context.Context, t *testing.T) (*Manager, addre
Code: builtin.AccountActorCodeID, Code: builtin.AccountActorCodeID,
Head: cid.Cid{}, Head: cid.Cid{},
Nonce: 0, Nonce: 0,
Balance: big.NewInt(10), Balance: big.NewInt(20),
} }
sm.setPaychState(ch, act, paych.State{ sm.setPaychState(ch, act, paych.State{
From: fromAcct, From: fromAcct,

View File

@ -3,6 +3,8 @@ package paychmgr
import ( import (
"context" "context"
"github.com/filecoin-project/specs-actors/actors/abi/big"
"github.com/filecoin-project/specs-actors/actors/builtin/account" "github.com/filecoin-project/specs-actors/actors/builtin/account"
"github.com/filecoin-project/go-address" "github.com/filecoin-project/go-address"
@ -71,52 +73,42 @@ func nextLaneFromState(st *paych.State) uint64 {
return maxLane + 1 return maxLane + 1
} }
func findLane(states []*paych.LaneState, lane uint64) *paych.LaneState { // laneState gets the LaneStates from chain, then applies all vouchers in
var ls *paych.LaneState // the data store over the chain state
for _, laneState := range states { func (pm *Manager) laneState(state *paych.State, ch address.Address) (map[uint64]*paych.LaneState, error) {
if laneState.ID == lane {
ls = laneState
break
}
}
return ls
}
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 // TODO: we probably want to call UpdateChannelState with all vouchers to be fully correct
// (but technically dont't need to) // (but technically dont't need to)
// TODO: make sure this is correct laneStates := make(map[uint64]*paych.LaneState, len(state.LaneStates))
// Get the lane state from the chain // Get the lane state from the chain
ls := findLane(state.LaneStates, lane) for _, laneState := range state.LaneStates {
if ls == nil { laneStates[laneState.ID] = laneState
ls = &paych.LaneState{
ID: lane,
Redeemed: types.NewInt(0),
Nonce: 0,
}
} }
// Apply locally stored vouchers // Apply locally stored vouchers
vouchers, err := pm.store.VouchersForPaych(ch) vouchers, err := pm.store.VouchersForPaych(ch)
if err != nil { if err != nil && err != ErrChannelNotTracked {
if err == ErrChannelNotTracked { return nil, err
return *ls, nil
}
return paych.LaneState{}, err
} }
for _, v := range vouchers { for _, v := range vouchers {
for range v.Voucher.Merges { for range v.Voucher.Merges {
return paych.LaneState{}, xerrors.Errorf("paych merges not handled yet") return nil, xerrors.Errorf("paych merges not handled yet")
} }
if v.Voucher.Lane != lane { // If there's a voucher for a lane that isn't in chain state just
continue // create it
ls, ok := laneStates[v.Voucher.Lane]
if !ok {
ls = &paych.LaneState{
ID: v.Voucher.Lane,
Redeemed: types.NewInt(0),
Nonce: 0,
}
laneStates[v.Voucher.Lane] = ls
} }
if v.Voucher.Nonce < ls.Nonce { if v.Voucher.Nonce < ls.Nonce {
log.Warnf("Found outdated voucher: ch=%s, lane=%d, v.nonce=%d lane.nonce=%d", ch, lane, v.Voucher.Nonce, ls.Nonce)
continue continue
} }
@ -124,5 +116,31 @@ func (pm *Manager) laneState(state *paych.State, ch address.Address, lane uint64
ls.Redeemed = v.Voucher.Amount ls.Redeemed = v.Voucher.Amount
} }
return *ls, nil return laneStates, nil
}
// Get the total redeemed amount across all lanes, after applying the voucher
func (pm *Manager) totalRedeemedWithVoucher(laneStates map[uint64]*paych.LaneState, sv *paych.SignedVoucher) (big.Int, error) {
total := big.NewInt(0)
for _, ls := range laneStates {
total = big.Add(total, ls.Redeemed)
}
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
if sv.Nonce > lane.Nonce {
// Add the delta between the redeemed amount and the voucher
// amount to the total
delta := big.Sub(sv.Amount, lane.Redeemed)
total = big.Add(total, delta)
}
} else {
// If the voucher is *not* for an existing lane, just add its
// value (implicitly a new lane will be created for the voucher)
total = big.Add(total, sv.Amount)
}
return total, nil
} }