package paychmgr import ( "bytes" "context" "fmt" "math" cborutil "github.com/filecoin-project/go-cbor-util" "github.com/filecoin-project/specs-actors/actors/builtin" "github.com/filecoin-project/specs-actors/actors/builtin/paych" "golang.org/x/xerrors" logging "github.com/ipfs/go-log/v2" "go.uber.org/fx" "github.com/filecoin-project/go-address" "github.com/filecoin-project/lotus/chain/actors" "github.com/filecoin-project/lotus/chain/stmgr" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/lib/sigs" "github.com/filecoin-project/lotus/node/impl/full" ) var log = logging.Logger("paych") type ManagerApi struct { fx.In full.MpoolAPI full.WalletAPI full.StateAPI } type Manager struct { store *Store sm *stmgr.StateManager mpool full.MpoolAPI wallet full.WalletAPI state full.StateAPI } func NewManager(sm *stmgr.StateManager, pchstore *Store, api ManagerApi) *Manager { return &Manager{ store: pchstore, sm: sm, mpool: api.MpoolAPI, wallet: api.WalletAPI, state: api.StateAPI, } } func maxLaneFromState(st *actors.PaymentChannelActorState) (uint64, error) { maxLane := uint64(math.MaxInt64) for _, state := range st.LaneStates { if (state.ID)+1 > maxLane+1 { maxLane = state.ID } } 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 } maxLane, err := maxLaneFromState(st) if err != nil { return err } return pm.store.TrackChannel(&ChannelInfo{ Channel: ch, Control: st.To, Target: st.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 } return &ChannelInfo{ Channel: ch, Control: st.From, Target: st.To, Direction: DirOutbound, NextLane: maxLane + 1, }, nil } func (pm *Manager) TrackOutboundChannel(ctx context.Context, ch address.Address) error { ci, err := pm.loadOutboundChannelInfo(ctx, ch) if err != nil { return err } return pm.store.TrackChannel(ci) } func (pm *Manager) ListChannels() ([]address.Address, error) { return pm.store.ListChannels() } 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) func (pm *Manager) CheckVoucherValid(ctx context.Context, ch address.Address, sv *types.SignedVoucher) error { act, pca, err := pm.loadPaychState(ctx, ch) if err != nil { return err } // verify signature vb, err := sv.SigningBytes() if err != nil { return 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, pca.From, vb); err != nil { return 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) } // 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") } if len(sv.Merges) != 0 { return fmt.Errorf("dont currently support paych lane merges") } return nil } // checks if the given voucher is currently spendable func (pm *Manager) CheckVoucherSpendable(ctx context.Context, ch address.Address, sv *types.SignedVoucher, secret []byte, proof []byte) (bool, error) { owner, err := pm.getPaychOwner(ctx, ch) if err != nil { return false, err } if sv.Extra != nil && proof == nil { known, err := pm.ListVouchers(ctx, ch) if err != nil { return false, err } for _, v := range known { eq, err := cborutil.Equals(v.Voucher, sv) if err != nil { return false, err } if v.Proof != nil && eq { log.Info("CheckVoucherSpendable: using stored proof") proof = v.Proof break } } if proof == nil { log.Warn("CheckVoucherSpendable: nil proof for voucher with validation") } } enc, err := actors.SerializeParams(&paych.UpdateChannelStateParams{ Sv: *sv, Secret: secret, Proof: proof, }) if err != nil { return false, err } ret, err := pm.sm.Call(ctx, &types.Message{ From: owner, To: ch, Method: builtin.MethodsPaych.UpdateChannelState, Params: enc, }, nil) if err != nil { return false, err } if ret.ExitCode != 0 { return false, nil } return true, nil } func (pm *Manager) getPaychOwner(ctx context.Context, ch address.Address) (address.Address, error) { var state paych.State if _, err := pm.sm.LoadActorState(ctx, ch, &state, nil); err != nil { return address.Address{}, err } return state.From, nil } func (pm *Manager) AddVoucher(ctx context.Context, ch address.Address, sv *types.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() ci, err := pm.store.getChannelInfo(ch) if err != nil { 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 for i, v := range ci.Vouchers { eq, err := cborutil.Equals(sv, v) if err != nil { return types.BigInt{}, err } if !eq { continue } if v.Proof != nil { if !bytes.Equal(v.Proof, proof) { log.Warnf("AddVoucher: multiple proofs for single voucher, storing both") break } log.Warnf("AddVoucher: voucher re-added with matching proof") return types.NewInt(0), nil } log.Warnf("AddVoucher: adding proof to stored voucher") ci.Vouchers[i] = &VoucherInfo{ Voucher: v.Voucher, Proof: proof, } return types.NewInt(0), pm.store.putChannelInfo(ci) } delta := types.BigSub(sv.Amount, laneState.Redeemed) 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) } ci.Vouchers = append(ci.Vouchers, &VoucherInfo{ Voucher: sv, Proof: proof, }) if ci.NextLane <= (sv.Lane) { ci.NextLane = sv.Lane + 1 } return delta, pm.store.putChannelInfo(ci) } func (pm *Manager) AllocateLane(ch address.Address) (uint64, error) { return pm.store.AllocateLane(ch) } func (pm *Manager) ListVouchers(ctx context.Context, ch address.Address) ([]*VoucherInfo, error) { // TODO: just having a passthrough method like this feels odd. Seems like // there should be some filtering we're doing here return pm.store.VouchersForPaych(ch) } func (pm *Manager) OutboundChanTo(from, to address.Address) (address.Address, error) { pm.store.lk.Lock() defer pm.store.lk.Unlock() return pm.store.findChan(func(ci *ChannelInfo) bool { if ci.Direction != DirOutbound { return false } return ci.Control == from && ci.Target == to }) } func (pm *Manager) NextNonceForLane(ctx context.Context, ch address.Address, lane uint64) (uint64, error) { vouchers, err := pm.store.VouchersForPaych(ch) if err != nil { return 0, err } var maxnonce uint64 for _, v := range vouchers { if uint64(v.Voucher.Lane) == lane { if uint64(v.Voucher.Nonce) > maxnonce { maxnonce = uint64(v.Voucher.Nonce) } } } return maxnonce + 1, nil }