525 lines
14 KiB
Go
525 lines
14 KiB
Go
package paychmgr
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/ipfs/go-cid"
|
|
"github.com/ipfs/go-datastore"
|
|
dsq "github.com/ipfs/go-datastore/query"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/filecoin-project/go-address"
|
|
cborrpc "github.com/filecoin-project/go-cbor-util"
|
|
cborutil "github.com/filecoin-project/go-cbor-util"
|
|
"github.com/filecoin-project/go-state-types/builtin/v8/paych"
|
|
|
|
"github.com/filecoin-project/lotus/chain/types"
|
|
)
|
|
|
|
var ErrChannelNotTracked = errors.New("channel not tracked")
|
|
|
|
type Store struct {
|
|
ds datastore.Batching
|
|
}
|
|
|
|
func NewStore(ds datastore.Batching) *Store {
|
|
return &Store{
|
|
ds: ds,
|
|
}
|
|
}
|
|
|
|
const (
|
|
DirInbound = 1
|
|
DirOutbound = 2
|
|
)
|
|
|
|
const (
|
|
dsKeyChannelInfo = "ChannelInfo"
|
|
dsKeyMsgCid = "MsgCid"
|
|
)
|
|
|
|
type VoucherInfo struct {
|
|
Voucher *paych.SignedVoucher
|
|
Proof []byte // ignored
|
|
Submitted bool
|
|
}
|
|
|
|
// ChannelInfo keeps track of information about a channel
|
|
type ChannelInfo struct {
|
|
// ChannelID is a uuid set at channel creation
|
|
ChannelID string
|
|
// Channel address - may be nil if the channel hasn't been created yet
|
|
Channel *address.Address
|
|
// Control is the address of the local node
|
|
Control address.Address
|
|
// Target is the address of the remote node (on the other end of the channel)
|
|
Target address.Address
|
|
// Direction indicates if the channel is inbound (Control is the "to" address)
|
|
// or outbound (Control is the "from" address)
|
|
Direction uint64
|
|
// Vouchers is a list of all vouchers sent on the channel
|
|
Vouchers []*VoucherInfo
|
|
// NextLane is the number of the next lane that should be used when the
|
|
// client requests a new lane (eg to create a voucher for a new deal)
|
|
NextLane uint64
|
|
// Amount added to the channel.
|
|
// Note: This amount is only used by GetPaych to keep track of how much
|
|
// has locally been added to the channel. It should reflect the channel's
|
|
// Balance on chain as long as all operations occur on the same datastore.
|
|
Amount types.BigInt
|
|
// AvailableAmount indicates how much afil is non-reserved
|
|
AvailableAmount types.BigInt
|
|
// PendingAvailableAmount is available amount that we're awaiting confirmation of
|
|
PendingAvailableAmount types.BigInt
|
|
// PendingAmount is the amount that we're awaiting confirmation of
|
|
PendingAmount types.BigInt
|
|
// CreateMsg is the CID of a pending create message (while waiting for confirmation)
|
|
CreateMsg *cid.Cid
|
|
// AddFundsMsg is the CID of a pending add funds message (while waiting for confirmation)
|
|
AddFundsMsg *cid.Cid
|
|
// Settling indicates whether the channel has entered into the settling state
|
|
Settling bool
|
|
}
|
|
|
|
func (ci *ChannelInfo) from() address.Address {
|
|
if ci.Direction == DirOutbound {
|
|
return ci.Control
|
|
}
|
|
return ci.Target
|
|
}
|
|
|
|
func (ci *ChannelInfo) to() address.Address {
|
|
if ci.Direction == DirOutbound {
|
|
return ci.Target
|
|
}
|
|
return ci.Control
|
|
}
|
|
|
|
// infoForVoucher gets the VoucherInfo for the given voucher.
|
|
// returns nil if the channel doesn't have the voucher.
|
|
func (ci *ChannelInfo) infoForVoucher(sv *paych.SignedVoucher) (*VoucherInfo, error) {
|
|
for _, v := range ci.Vouchers {
|
|
eq, err := cborutil.Equals(sv, v.Voucher)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if eq {
|
|
return v, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (ci *ChannelInfo) hasVoucher(sv *paych.SignedVoucher) (bool, error) {
|
|
vi, err := ci.infoForVoucher(sv)
|
|
return vi != nil, err
|
|
}
|
|
|
|
// markVoucherSubmitted marks the voucher, and any vouchers of lower nonce
|
|
// in the same lane, as being submitted.
|
|
// Note: This method doesn't write anything to the store.
|
|
func (ci *ChannelInfo) markVoucherSubmitted(sv *paych.SignedVoucher) error {
|
|
vi, err := ci.infoForVoucher(sv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if vi == nil {
|
|
return xerrors.Errorf("cannot submit voucher that has not been added to channel")
|
|
}
|
|
|
|
// Mark the voucher as submitted
|
|
vi.Submitted = true
|
|
|
|
// Mark lower-nonce vouchers in the same lane as submitted (lower-nonce
|
|
// vouchers are superseded by the submitted voucher)
|
|
for _, vi := range ci.Vouchers {
|
|
if vi.Voucher.Lane == sv.Lane && vi.Voucher.Nonce < sv.Nonce {
|
|
vi.Submitted = true
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// wasVoucherSubmitted returns true if the voucher has been submitted
|
|
func (ci *ChannelInfo) wasVoucherSubmitted(sv *paych.SignedVoucher) (bool, error) {
|
|
vi, err := ci.infoForVoucher(sv)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if vi == nil {
|
|
return false, xerrors.Errorf("cannot submit voucher that has not been added to channel")
|
|
}
|
|
return vi.Submitted, nil
|
|
}
|
|
|
|
// TrackChannel stores a channel, returning an error if the channel was already
|
|
// being tracked
|
|
func (ps *Store) TrackChannel(ctx context.Context, ci *ChannelInfo) (*ChannelInfo, error) {
|
|
_, err := ps.ByAddress(ctx, *ci.Channel)
|
|
switch err {
|
|
default:
|
|
return nil, err
|
|
case nil:
|
|
return nil, fmt.Errorf("already tracking channel: %s", ci.Channel)
|
|
case ErrChannelNotTracked:
|
|
err = ps.putChannelInfo(ctx, ci)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ps.ByAddress(ctx, *ci.Channel)
|
|
}
|
|
}
|
|
|
|
// ListChannels returns the addresses of all channels that have been created
|
|
func (ps *Store) ListChannels(ctx context.Context) ([]address.Address, error) {
|
|
cis, err := ps.findChans(ctx, func(ci *ChannelInfo) bool {
|
|
return ci.Channel != nil
|
|
}, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addrs := make([]address.Address, 0, len(cis))
|
|
for _, ci := range cis {
|
|
addrs = append(addrs, *ci.Channel)
|
|
}
|
|
|
|
return addrs, nil
|
|
}
|
|
|
|
// findChan finds a single channel using the given filter.
|
|
// If there isn't a channel that matches the filter, returns ErrChannelNotTracked
|
|
func (ps *Store) findChan(ctx context.Context, filter func(ci *ChannelInfo) bool) (*ChannelInfo, error) {
|
|
cis, err := ps.findChans(ctx, filter, 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(cis) == 0 {
|
|
return nil, ErrChannelNotTracked
|
|
}
|
|
|
|
return &cis[0], err
|
|
}
|
|
|
|
// findChans loops over all channels, only including those that pass the filter.
|
|
// max is the maximum number of channels to return. Set to zero to return unlimited channels.
|
|
func (ps *Store) findChans(ctx context.Context, filter func(*ChannelInfo) bool, max int) ([]ChannelInfo, error) {
|
|
res, err := ps.ds.Query(ctx, dsq.Query{Prefix: dsKeyChannelInfo})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Close() //nolint:errcheck
|
|
|
|
var stored ChannelInfo
|
|
var matches []ChannelInfo
|
|
|
|
for {
|
|
res, ok := res.NextSync()
|
|
if !ok {
|
|
break
|
|
}
|
|
|
|
if res.Error != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ci, err := unmarshallChannelInfo(&stored, res.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !filter(ci) {
|
|
continue
|
|
}
|
|
|
|
matches = append(matches, *ci)
|
|
|
|
// If we've reached the maximum number of matches, return.
|
|
// Note that if max is zero we return an unlimited number of matches
|
|
// because len(matches) will always be at least 1.
|
|
if len(matches) == max {
|
|
return matches, nil
|
|
}
|
|
}
|
|
|
|
return matches, nil
|
|
}
|
|
|
|
// AllocateLane allocates a new lane for the given channel
|
|
func (ps *Store) AllocateLane(ctx context.Context, ch address.Address) (uint64, error) {
|
|
ci, err := ps.ByAddress(ctx, ch)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
out := ci.NextLane
|
|
ci.NextLane++
|
|
|
|
return out, ps.putChannelInfo(ctx, ci)
|
|
}
|
|
|
|
// VouchersForPaych gets the vouchers for the given channel
|
|
func (ps *Store) VouchersForPaych(ctx context.Context, ch address.Address) ([]*VoucherInfo, error) {
|
|
ci, err := ps.ByAddress(ctx, ch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ci.Vouchers, nil
|
|
}
|
|
|
|
func (ps *Store) MarkVoucherSubmitted(ctx context.Context, ci *ChannelInfo, sv *paych.SignedVoucher) error {
|
|
err := ci.markVoucherSubmitted(sv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ps.putChannelInfo(ctx, ci)
|
|
}
|
|
|
|
// ByAddress gets the channel that matches the given address
|
|
func (ps *Store) ByAddress(ctx context.Context, addr address.Address) (*ChannelInfo, error) {
|
|
return ps.findChan(ctx, func(ci *ChannelInfo) bool {
|
|
return ci.Channel != nil && *ci.Channel == addr
|
|
})
|
|
}
|
|
|
|
// MsgInfo stores information about a create channel / add funds message
|
|
// that has been sent
|
|
type MsgInfo struct {
|
|
// ChannelID links the message to a channel
|
|
ChannelID string
|
|
// MsgCid is the CID of the message
|
|
MsgCid cid.Cid
|
|
// Received indicates whether a response has been received
|
|
Received bool
|
|
// Err is the error received in the response
|
|
Err string
|
|
}
|
|
|
|
// The datastore key used to identify the message
|
|
func dskeyForMsg(mcid cid.Cid) datastore.Key {
|
|
return datastore.KeyWithNamespaces([]string{dsKeyMsgCid, mcid.String()})
|
|
}
|
|
|
|
// SaveNewMessage is called when a message is sent
|
|
func (ps *Store) SaveNewMessage(ctx context.Context, channelID string, mcid cid.Cid) error {
|
|
k := dskeyForMsg(mcid)
|
|
|
|
b, err := cborrpc.Dump(&MsgInfo{ChannelID: channelID, MsgCid: mcid})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return ps.ds.Put(ctx, k, b)
|
|
}
|
|
|
|
// SaveMessageResult is called when the result of a message is received
|
|
func (ps *Store) SaveMessageResult(ctx context.Context, mcid cid.Cid, msgErr error) error {
|
|
minfo, err := ps.GetMessage(ctx, mcid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
k := dskeyForMsg(mcid)
|
|
minfo.Received = true
|
|
if msgErr != nil {
|
|
minfo.Err = msgErr.Error()
|
|
}
|
|
|
|
b, err := cborrpc.Dump(minfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return ps.ds.Put(ctx, k, b)
|
|
}
|
|
|
|
// ByMessageCid gets the channel associated with a message
|
|
func (ps *Store) ByMessageCid(ctx context.Context, mcid cid.Cid) (*ChannelInfo, error) {
|
|
minfo, err := ps.GetMessage(ctx, mcid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ci, err := ps.findChan(ctx, func(ci *ChannelInfo) bool {
|
|
return ci.ChannelID == minfo.ChannelID
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ci, err
|
|
}
|
|
|
|
// GetMessage gets the message info for a given message CID
|
|
func (ps *Store) GetMessage(ctx context.Context, mcid cid.Cid) (*MsgInfo, error) {
|
|
k := dskeyForMsg(mcid)
|
|
|
|
val, err := ps.ds.Get(ctx, k)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var minfo MsgInfo
|
|
if err := minfo.UnmarshalCBOR(bytes.NewReader(val)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &minfo, nil
|
|
}
|
|
|
|
// OutboundActiveByFromTo looks for outbound channels that have not been
|
|
// settled, with the given from / to addresses
|
|
func (ps *Store) OutboundActiveByFromTo(ctx context.Context, sma stateManagerAPI, from address.Address, to address.Address) (*ChannelInfo, error) {
|
|
return ps.findChan(ctx, func(ci *ChannelInfo) bool {
|
|
if ci.Direction != DirOutbound {
|
|
return false
|
|
}
|
|
if ci.Settling {
|
|
return false
|
|
}
|
|
|
|
if ci.Channel != nil {
|
|
_, st, err := sma.GetPaychState(ctx, *ci.Channel, nil)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
sat, err := st.SettlingAt()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if sat != 0 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return ci.Control == from && ci.Target == to
|
|
})
|
|
}
|
|
|
|
// WithPendingAddFunds is used on startup to find channels for which a
|
|
// create channel or add funds message has been sent, but lotus shut down
|
|
// before the response was received.
|
|
func (ps *Store) WithPendingAddFunds(ctx context.Context) ([]ChannelInfo, error) {
|
|
return ps.findChans(ctx, func(ci *ChannelInfo) bool {
|
|
if ci.Direction != DirOutbound {
|
|
return false
|
|
}
|
|
return ci.CreateMsg != nil || ci.AddFundsMsg != nil
|
|
}, 0)
|
|
}
|
|
|
|
// ByChannelID gets channel info by channel ID
|
|
func (ps *Store) ByChannelID(ctx context.Context, channelID string) (*ChannelInfo, error) {
|
|
var stored ChannelInfo
|
|
|
|
res, err := ps.ds.Get(ctx, dskeyForChannel(channelID))
|
|
if err != nil {
|
|
if err == datastore.ErrNotFound {
|
|
return nil, ErrChannelNotTracked
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
return unmarshallChannelInfo(&stored, res)
|
|
}
|
|
|
|
// CreateChannel creates an outbound channel for the given from / to
|
|
func (ps *Store) CreateChannel(ctx context.Context, from address.Address, to address.Address, createMsgCid cid.Cid, amt, avail types.BigInt) (*ChannelInfo, error) {
|
|
ci := &ChannelInfo{
|
|
Direction: DirOutbound,
|
|
NextLane: 0,
|
|
Control: from,
|
|
Target: to,
|
|
CreateMsg: &createMsgCid,
|
|
PendingAmount: amt,
|
|
PendingAvailableAmount: avail,
|
|
}
|
|
|
|
// Save the new channel
|
|
err := ps.putChannelInfo(ctx, ci)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Save a reference to the create message
|
|
err = ps.SaveNewMessage(ctx, ci.ChannelID, createMsgCid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ci, err
|
|
}
|
|
|
|
// RemoveChannel removes the channel with the given channel ID
|
|
func (ps *Store) RemoveChannel(ctx context.Context, channelID string) error {
|
|
return ps.ds.Delete(ctx, dskeyForChannel(channelID))
|
|
}
|
|
|
|
// The datastore key used to identify the channel info
|
|
func dskeyForChannel(channelID string) datastore.Key {
|
|
return datastore.KeyWithNamespaces([]string{dsKeyChannelInfo, channelID})
|
|
}
|
|
|
|
// putChannelInfo stores the channel info in the datastore
|
|
func (ps *Store) putChannelInfo(ctx context.Context, ci *ChannelInfo) error {
|
|
if len(ci.ChannelID) == 0 {
|
|
ci.ChannelID = uuid.New().String()
|
|
}
|
|
k := dskeyForChannel(ci.ChannelID)
|
|
|
|
b, err := marshallChannelInfo(ci)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return ps.ds.Put(ctx, k, b)
|
|
}
|
|
|
|
// TODO: This is a hack to get around not being able to CBOR marshall a nil
|
|
// address.Address. It's been fixed in address.Address but we need to wait
|
|
// for the change to propagate to specs-actors before we can remove this hack.
|
|
var emptyAddr address.Address
|
|
|
|
func init() {
|
|
addr, err := address.NewActorAddress([]byte("empty"))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
emptyAddr = addr
|
|
}
|
|
|
|
func marshallChannelInfo(ci *ChannelInfo) ([]byte, error) {
|
|
// See note above about CBOR marshalling address.Address
|
|
if ci.Channel == nil {
|
|
ci.Channel = &emptyAddr
|
|
}
|
|
return cborrpc.Dump(ci)
|
|
}
|
|
|
|
func unmarshallChannelInfo(stored *ChannelInfo, value []byte) (*ChannelInfo, error) {
|
|
if err := stored.UnmarshalCBOR(bytes.NewReader(value)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// See note above about CBOR marshalling address.Address
|
|
if stored.Channel != nil && *stored.Channel == emptyAddr {
|
|
stored.Channel = nil
|
|
}
|
|
|
|
// backwards compat
|
|
if stored.AvailableAmount.Int == nil {
|
|
stored.AvailableAmount = types.NewInt(0)
|
|
stored.PendingAvailableAmount = types.NewInt(0)
|
|
}
|
|
|
|
return stored, nil
|
|
}
|