pool/funding/manager.go

1279 lines
40 KiB
Go

package funding
import (
"context"
"encoding/hex"
"fmt"
"io"
"net"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/pool/auctioneerrpc"
"github.com/lightninglabs/pool/chaninfo"
"github.com/lightninglabs/pool/clientdb"
"github.com/lightninglabs/pool/order"
"github.com/lightninglabs/pool/sidecar"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/subscribe"
"github.com/lightningnetwork/lnd/tor"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var (
// rpcCodeFundingFailed is the error code we send if the channel funding
// fails because of a timeout or another problem.
rpcCodeFundingFailed = auctioneerrpc.OrderReject_CHANNEL_FUNDING_FAILED
)
// MatchRejectErr is an error type that is returned from the funding manager if
// the trader rejects certain orders instead of the whole batch.
type MatchRejectErr struct {
// RejectedOrders is the map of matches we reject, keyed with our order
// nonce.
RejectedOrders map[order.Nonce]*auctioneerrpc.OrderReject
}
// Error returns the underlying error string.
//
// NOTE: This is part of the error interface.
func (e *MatchRejectErr) Error() string {
return fmt.Sprintf("trader rejected orders: %v", e.RejectedOrders)
}
// BaseClient is an interface that contains all methods necessary to open a
// channel with a funding shim and query peer connections.
type BaseClient interface {
// FundingStateStep is an advanced funding related call that allows the
// caller to either execute some preparatory steps for a funding
// workflow, or manually progress a funding workflow.
FundingStateStep(ctx context.Context, req *lnrpc.FundingTransitionMsg,
opts ...grpc.CallOption) (*lnrpc.FundingStateStepResp, error)
// OpenChannel attempts to open a singly funded channel specified in the
// request to a remote peer.
OpenChannel(ctx context.Context, req *lnrpc.OpenChannelRequest,
opts ...grpc.CallOption) (lnrpc.Lightning_OpenChannelClient,
error)
// ListPeers returns a verbose listing of all currently active peers.
ListPeers(ctx context.Context, req *lnrpc.ListPeersRequest,
opts ...grpc.CallOption) (*lnrpc.ListPeersResponse, error)
// SubscribePeerEvents creates a uni-directional stream from the server
// to the client in which any events relevant to the state of peers are
// sent over. Events include peers going online and offline.
SubscribePeerEvents(ctx context.Context, r *lnrpc.PeerEventSubscription,
opts ...grpc.CallOption) (
lnrpc.Lightning_SubscribePeerEventsClient, error)
// AbandonChannel removes all channel state from the database except for
// a close summary. This method can be used to get rid of permanently
// unusable channels due to bugs fixed in newer versions of lnd. This
// method can also be used to remove externally funded channels where
// the funding transaction was never broadcast. Only available for
// non-externally funded channels in dev build.
AbandonChannel(ctx context.Context, in *lnrpc.AbandonChannelRequest,
opts ...grpc.CallOption) (*lnrpc.AbandonChannelResponse, error)
// SubscribeChannelEvents creates a uni-directional stream from the
// server to the client in which any updates relevant to the state of
// the channels are sent over. Events include new active channels,
// inactive channels, and closed channels.
SubscribeChannelEvents(ctx context.Context,
in *lnrpc.ChannelEventSubscription, opts ...grpc.CallOption) (
lnrpc.Lightning_SubscribeChannelEventsClient, error)
}
// ManagerConfig holds all the items passed into the funding manager externally.
type ManagerConfig struct {
// DB is the client database.
DB *clientdb.DB
// WalletKit is an lndclient wrapped walletrpc client.
WalletKit lndclient.WalletKitClient
// LightningClient is an lndclient wrapped lnrpc client.
LightningClient lndclient.LightningClient
// SignerClient is an lndclient wrapped signrpc client.
SignerClient lndclient.SignerClient
// BaseClient is a raw lnrpc client that implements all methods the
// funding manager needs.
BaseClient BaseClient
// NodePubKey is the connected lnd node's identity public key.
NodePubKey *btcec.PublicKey
// NewNodesOnly specifies if the funding manager should only accept
// matched orders with channels from new nodes that the connected lnd
// node doesn't already have channels with.
NewNodesOnly bool
// BatchStepTimeout is the timeout the manager uses when executing a
// single batch step.
BatchStepTimeout time.Duration
// NotifyShimCreated is a function that should be called whenever a
// funding shim is created for a bid order where we expect an incoming
// channel at any moment.
NotifyShimCreated func(ourBid *order.Bid, pendingChanID [32]byte)
}
// Manager is responsible for everything channel funding related during the
// match making process.
type Manager struct {
started uint32 // To be used atomically.
stopped uint32 // To be used atomically.
cfg *ManagerConfig
wg sync.WaitGroup
quit chan struct{}
pendingOpenChanCancel func()
pendingOpenChanServer *subscribe.Server
pendingOpenChanClient *subscribe.Client
}
// NewManager creates a new funding manager from the given config.
func NewManager(cfg *ManagerConfig) *Manager {
return &Manager{
cfg: cfg,
quit: make(chan struct{}),
pendingOpenChanServer: subscribe.NewServer(),
}
}
// Start starts the rpcServer, making it ready to accept incoming requests.
func (m *Manager) Start() error {
if !atomic.CompareAndSwapUint32(&m.started, 0, 1) {
return nil
}
log.Infof("Starting funding manager")
// Subscribe to pending open channel notifications. This will be useful
// when we're creating channels with a matched order as part of a batch.
streamCtx, streamCancel := context.WithCancel(context.Background())
m.pendingOpenChanCancel = streamCancel
subStream, err := m.cfg.BaseClient.SubscribeChannelEvents(
streamCtx, &lnrpc.ChannelEventSubscription{},
)
if err != nil {
return err
}
if err := m.pendingOpenChanServer.Start(); err != nil {
return fmt.Errorf("error starting pending chan subscription "+
"server: %v", err)
}
// We want to make sure we don't miss any channel updates as long as we
// are running. But we might not be the only manager interested in the
// updates, that's why we are a client to our own server.
m.pendingOpenChanClient, err = m.SubscribePendingOpenChan()
if err != nil {
return fmt.Errorf("error subscribing to pending open "+
"channel events: %v", err)
}
m.wg.Add(1)
go m.consumePendingOpenChannels(subStream)
log.Infof("Funding manager is now active")
return nil
}
// Stop stops the server.
func (m *Manager) Stop() error {
if !atomic.CompareAndSwapUint32(&m.stopped, 0, 1) {
return nil
}
log.Info("Funding manager stopping")
close(m.quit)
// We call this before Wait to ensure the goroutine is stopped by this
// call.
m.pendingOpenChanClient.Cancel()
if err := m.pendingOpenChanServer.Stop(); err != nil {
return fmt.Errorf("error stopping pending chan subscription "+
"server: %v", err)
}
m.pendingOpenChanCancel()
m.wg.Wait()
log.Info("Stopped funding manager")
return nil
}
// consumePendingOpenChannels consumes pending open channel events from the
// stream and notifies them if the trader currently has an ongoing batch.
func (m *Manager) consumePendingOpenChannels(
subStream lnrpc.Lightning_SubscribeChannelEventsClient) {
defer m.wg.Done()
for {
select {
case <-m.quit:
return
default:
}
msg, err := subStream.Recv()
if err != nil {
select {
case <-m.quit:
return
default:
}
log.Errorf("Unable to read channel event: %v", err)
// If the lnd node shut down, there's no use continuing.
if err == io.EOF || err == io.ErrUnexpectedEOF ||
status.Code(err) == codes.Unavailable {
return
}
continue
}
// Skip any events other than the pending open channel one.
channel, ok := msg.Channel.(*lnrpc.ChannelEventUpdate_PendingOpenChannel)
if !ok {
continue
}
update := channel.PendingOpenChannel
if err := m.pendingOpenChanServer.SendUpdate(update); err != nil {
log.Errorf("Error sending open channel update: %v", err)
}
}
}
// SubscribePendingOpenChan creates a new subscription client to receive events
// for pending open channels from lnd.
func (m *Manager) SubscribePendingOpenChan() (*subscribe.Client, error) {
return m.pendingOpenChanServer.Subscribe()
}
// deriveFundingShim generates the proper funding shim that should be used by
// the maker or taker to properly make a channel that stems off the main batch
// funding transaction.
func (m *Manager) deriveFundingShim(ourOrder order.Order,
matchedOrder *order.MatchedOrder,
batchTx *wire.MsgTx) (*lnrpc.FundingShim, [32]byte, error) {
log.Infof("Registering funding shim for Order(type=%v, amt=%v, "+
"nonce=%v", ourOrder.Type(),
matchedOrder.UnitsFilled.ToSatoshis(), ourOrder.Nonce())
// First, we'll compute the pending channel ID key which will be unique
// to this order pair.
var (
askNonce, bidNonce order.Nonce
thawHeight uint32
selfChanBalance btcutil.Amount
)
ourOrderBid, ourOrderIsBid := ourOrder.(*order.Bid)
ourOrderIsSidecar := ourOrderIsBid && ourOrderBid.SidecarTicket != nil
if ourOrderIsBid {
bidNonce = ourOrder.Nonce()
askNonce = matchedOrder.Order.Nonce()
thawHeight = ourOrderBid.LeaseDuration
selfChanBalance = ourOrderBid.SelfChanBalance
} else {
bidNonce = matchedOrder.Order.Nonce()
askNonce = ourOrder.Nonce()
thawHeight = matchedOrder.Order.(*order.Bid).LeaseDuration
selfChanBalance = matchedOrder.Order.(*order.Bid).SelfChanBalance
}
pendingChanID := order.PendingChanKey(
askNonce, bidNonce,
)
chanSize := matchedOrder.UnitsFilled.ToSatoshis()
// Next, we'll need to find the location of this channel output on the
// funding transaction, so we'll re-compute the funding script from
// scratch. If we're a taker or otherwise uninvolved in a sidecar order
// we have to re-derive our key from the locator.
var ourMultiSigKey *keychain.KeyDescriptor
if ourOrderIsSidecar && ourOrderBid.SidecarTicket.Recipient != nil {
recipient := ourOrderBid.SidecarTicket.Recipient
ourMultiSigKey = &keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamilyMultiSig,
Index: recipient.MultiSigKeyIndex,
},
PubKey: recipient.MultiSigPubKey,
}
} else {
var err error
ctxb := context.Background()
ourKeyLocator := ourOrder.Details().MultiSigKeyLocator
ourMultiSigKey, err = m.cfg.WalletKit.DeriveKey(
ctxb, &ourKeyLocator,
)
if err != nil {
return nil, [32]byte{}, err
}
}
_, fundingOutput, err := input.GenFundingPkScript(
ourMultiSigKey.PubKey.SerializeCompressed(),
matchedOrder.MultiSigKey[:], int64(chanSize),
)
if err != nil {
return nil, [32]byte{}, err
}
// Now that we have the funding script, we'll find the output index
// within the batch execution transaction. We ignore the first
// argument, as earlier during validation, we would've rejected the
// batch if it wasn't found.
batchTxID := batchTx.TxHash()
_, chanOutputIndex := input.FindScriptOutputIndex(
batchTx, fundingOutput.PkScript,
)
chanPoint := &lnrpc.ChannelPoint{
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
FundingTxidBytes: batchTxID[:],
},
OutputIndex: chanOutputIndex,
}
// With all the components assembled, we'll now create the chan point
// shim, and register it so we use the proper funding key when we
// receive the marker's incoming funding request.
chanPointShim := &lnrpc.ChanPointShim{
Amt: int64(chanSize + selfChanBalance),
ChanPoint: chanPoint,
LocalKey: &lnrpc.KeyDescriptor{
RawKeyBytes: ourMultiSigKey.PubKey.SerializeCompressed(),
KeyLoc: &lnrpc.KeyLocator{
KeyFamily: int32(ourMultiSigKey.Family),
KeyIndex: int32(ourMultiSigKey.Index),
},
},
RemoteKey: matchedOrder.MultiSigKey[:],
PendingChanId: pendingChanID[:],
ThawHeight: thawHeight,
}
return &lnrpc.FundingShim{
Shim: &lnrpc.FundingShim_ChanPointShim{
ChanPointShim: chanPointShim,
},
}, pendingChanID, nil
}
// registerFundingShim is used when we're on the taker (our bid was executed)
// side of a new matched order. To prepare ourselves for their incoming funding
// request, we'll register a shim with all the expected parameters.
func (m *Manager) registerFundingShim(ourBid *order.Bid,
matchedOrder *order.MatchedOrder, batchTx *wire.MsgTx) error {
ctxb := context.Background()
fundingShim, pendingChanID, err := m.deriveFundingShim(
ourBid, matchedOrder, batchTx,
)
if err != nil {
return err
}
_, err = m.cfg.BaseClient.FundingStateStep(
ctxb, &lnrpc.FundingTransitionMsg{
Trigger: &lnrpc.FundingTransitionMsg_ShimRegister{
ShimRegister: fundingShim,
},
},
)
if err != nil {
return fmt.Errorf("unable to register funding shim: %v", err)
}
// In case there is a self chan balance involved, we need our channel
// acceptor to be aware of the incoming order so it can verify the push
// amount accordingly.
if m.cfg.NotifyShimCreated != nil {
m.cfg.NotifyShimCreated(ourBid, pendingChanID)
}
return nil
}
// PrepChannelFunding preps the backing node to either receive or initiate a
// channel funding based on the items in the order batch.
func (m *Manager) PrepChannelFunding(batch *order.Batch,
getOrder order.Fetcher) error {
log.Infof("Batch(%x): preparing channel funding for %v orders",
batch.ID[:], len(batch.MatchedOrders))
// As we need to change our behavior if the node has any Tor addresses,
// we'll fetch the current state of our advertised addrs now.
nodeInfo, err := m.cfg.LightningClient.GetInfo(context.Background())
if err != nil {
log.Errorf("error in GetInfo: %v", err)
return err
}
traderBehindTor := nodeHasTorAddrs(nodeInfo.Uris)
// We need to make sure our whole process doesn't take too long overall
// so we create a context that is valid for the whole funding step and
// use that everywhere.
setupCtx, cancel := context.WithTimeout(
context.Background(), m.cfg.BatchStepTimeout,
)
defer cancel()
// Before we connect out to peers, we check that we don't get any new
// channels from peers we already have channels with, in case this is
// requested by the trader.
if m.cfg.NewNodesOnly {
fundingRejects, err := m.rejectDuplicateChannels(batch)
if err != nil {
return err
}
// In case we have any orders we don't like, tell the auctioneer
// now. They'll hopefully prepare and send us another batch.
if len(fundingRejects) > 0 {
return &MatchRejectErr{
RejectedOrders: fundingRejects,
}
}
}
// Now that we know this batch passes our sanity checks, we'll register
// all the funding shims we need to be able to respond
connsInitiated := make(map[route.Vertex]struct{})
for ourOrderNonce, matchedOrders := range batch.MatchedOrders {
ourOrder, err := getOrder(ourOrderNonce)
if err != nil {
return err
}
orderIsAsk := ourOrder.Type() == order.TypeAsk
// Depending on if this is a bid or not, we'll either try to
// connect out and register the full funding shim or do nothing
// at all.
for _, matchedOrder := range matchedOrders {
// To allow bidders to be mobile phones or Tor only
// nodes (which means, they aren't reachable directly
// through clearnet), we only force the asker to be
// reachable. For that to work, the connection has to
// be established from the bidder to the asker. But the
// channel itself will be opened from the asker to the
// bidder which will succeed once the connection is
// open. But that means, as an asker we don't have
// anything to do here.
//
// However, if we're currently running with a single
// Tor address, or all Tor addresses, then we'll also
// try to connect out to the taker, as they may not be
// able to connect to hidden services.
if orderIsAsk && !traderBehindTor {
continue
}
// If we are the provider for a sidecar ticket, we don't
// have to do anything either in this step.
ourOrderBid, ourOrderIsBid := ourOrder.(*order.Bid)
if ourOrderIsBid && ourOrderBid.SidecarTicket != nil {
r := ourOrderBid.SidecarTicket.Recipient
if r != nil &&
!m.cfg.NodePubKey.IsEqual(r.NodePubKey) {
continue
}
}
// As the bidder, we're the one that needs to make the
// connection as we're possibly not reachable from the
// outside. Let's kick off the connection now. However
// since we might be matching multiple orders from the
// same remote node, we want to de-duplicate the peers
// to not run into a problem in lnd when connecting to
// the same node twice in a very short interval.
//
// TODO(roasbeef): info leaks?
nodeKey := matchedOrder.NodeKey
log.Debugf("Connecting to node=%x for order_nonce="+
"%v", nodeKey[:], matchedOrder.Order.Nonce())
_, initiated := connsInitiated[nodeKey]
if !initiated {
// Since we don't want to block on the
// connection attempt to this trader, we start
// it in a new go routine, and ignore the error
// for now. Instead we will observe later
// whether the peer gets connected before the
// batch timeout.
go m.connectToMatchedTrader(
setupCtx, nodeKey,
matchedOrder.NodeAddrs,
)
connsInitiated[nodeKey] = struct{}{}
}
// Only bid orders need to register for the shim.
if orderIsAsk {
continue
}
// At this point, one of our bids was matched with a
// series of asks, so we'll now register all the
// expected funding shims so we can execute the next
// phase w/o any issues and accept the incoming channel
// from the asker.
err := m.registerFundingShim(
ourOrderBid, matchedOrder, batch.BatchTX,
)
if err != nil {
return fmt.Errorf("unable to register funding "+
"shim: %v", err)
}
}
}
// We need to wait for all connections to be established now. Otherwise
// the asker won't be able to open the channel as it doesn't know the
// connection details of the bidder.
return m.waitForPeerConnections(setupCtx, connsInitiated, batch)
}
// BatchChannelSetup will attempt to establish new funding flows with all
// matched takers (people buying our channels) in the passed batch. This method
// will block until the channel is considered pending. Once this phase is
// complete, and the batch execution transaction broadcast, the channel will be
// finalized and locked in.
func (m *Manager) BatchChannelSetup(
batch *order.Batch) (map[wire.OutPoint]*chaninfo.ChannelInfo, error) {
var (
eg errgroup.Group
chanPoints = make(map[wire.OutPoint]order.Nonce)
fundingRejects = make(map[order.Nonce]*auctioneerrpc.OrderReject)
fundingRejectsMtx sync.Mutex
)
partialReject := func(nonce order.Nonce, reason string,
chanPoint wire.OutPoint) {
fundingRejectsMtx.Lock()
defer fundingRejectsMtx.Unlock()
fundingRejects[nonce] = &auctioneerrpc.OrderReject{
ReasonCode: rpcCodeFundingFailed,
Reason: reason,
}
// Also remove the channel from the map that tracks pending
// channels, we won't receive any update on it if we failed
// before opening the channel.
delete(chanPoints, chanPoint)
}
// We need to make sure our whole process doesn't take too long overall
// so we create a context that is valid for the whole funding step and
// use that everywhere.
setupCtx, cancel := context.WithTimeout(
context.Background(), m.cfg.BatchStepTimeout,
)
defer cancel()
log.Infof("Batch(%x): opening channels for %v matched orders",
batch.ID[:], len(batch.MatchedOrders))
// For each ask order of ours that's matched, we'll make a new funding
// flow, blocking until they all progress to the final state.
batchTxHash := batch.BatchTX.TxHash()
for ourOrderNonce, matchedOrders := range batch.MatchedOrders {
ourOrder, err := m.cfg.DB.GetOrder(ourOrderNonce)
if err != nil {
return nil, err
}
bid, orderIsBid := ourOrder.(*order.Bid)
// If our order is a sidecar channel, we don't need to do
// anything at all in this step. The channel will be opened to
// another node and we don't need to wait for it here. If we are
// the receiver of the sidecar channel, this code won't be hit
// as that's separated out into the ChannelAcceptor type.
if orderIsBid && bid.SidecarTicket != nil {
continue
}
// We'll obtain the expected channel point for each matched
// order, and complete the funding flow for each one in which
// our order was the ask.
for _, matchedOrder := range matchedOrders {
fundingShim, _, err := m.deriveFundingShim(
ourOrder, matchedOrder, batch.BatchTX,
)
if err != nil {
return nil, err
}
chanPoint := wire.OutPoint{
Hash: batchTxHash,
Index: fundingShim.GetChanPointShim().ChanPoint.
OutputIndex,
}
// Some goroutines are already running from previous
// iterations so we need to acquire the lock here.
fundingRejectsMtx.Lock()
chanPoints[chanPoint] = matchedOrder.Order.Nonce()
fundingRejectsMtx.Unlock()
// If this is a bid order, then we don't need to do
// anything, as we should've already connected and
// registered the funding shim during the prior phase.
// The asker is going to open the channel.
if orderIsBid {
continue
}
// We know the matched order must be a bid now since our
// order is an ask.
matchedOrderBid := matchedOrder.Order.(*order.Bid)
// Otherwise, we'll now initiate the funding request to
// establish all the channels generated by this order
// with the remote parties. It's the bidder's job to
// connect to the asker. So if we get here we know they
// connected and we'll launch off the request to
// initiate channel funding with the remote peer.
//
// TODO(roasbeef): sat per byte from order?
// * also other params to set as well
chanAmt := matchedOrder.UnitsFilled.ToSatoshis()
chanAmt += matchedOrderBid.SelfChanBalance
fundingReq := &lnrpc.OpenChannelRequest{
NodePubkey: matchedOrder.NodeKey[:],
LocalFundingAmount: int64(chanAmt),
FundingShim: fundingShim,
PushSat: int64(
matchedOrderBid.SelfChanBalance,
),
}
chanStream, err := m.cfg.BaseClient.OpenChannel(
setupCtx, fundingReq,
)
// If we can't open the channel, it could be for a
// number of reasons. One of them is that we still
// aren't connected to the remote peer because it either
// refuses connections or is offline. We want to track
// these errors on the server side so we can track and
// investigate/intervene. And this is also no reason to
// fail the whole batch. So we just mark this order pair
// as rejected.
nonce := matchedOrder.Order.Nonce()
nodeKey := matchedOrder.NodeKey
if err != nil {
log.Warnf("Error when trying to open "+
"channel to node %x, going to reject "+
"channel: %v", nodeKey[:], err)
partialReject(nonce, err.Error(), chanPoint)
continue
}
// We'll launch a new goroutine to wait until chan
// pending (funding flow finished) update has been
// sent.
eg.Go(func() error {
for {
select {
case <-m.quit:
return fmt.Errorf("server " +
"shutting down")
default:
}
msg, err := chanStream.Recv()
if err != nil {
log.Errorf("unable to read "+
"chan open update "+
"event from node %x, "+
"going to reject "+
"channel: %v",
nodeKey[:], err)
partialReject(
nonce, err.Error(),
chanPoint,
)
return err
}
_, ok := msg.Update.(*lnrpc.OpenStatusUpdate_ChanPending)
if ok {
return nil
}
}
})
}
}
// There can only be two kinds of errors: Either we're shutting down, in
// which case we'll just return that error. Or we had a problem opening
// the channel which should have resulted in an entry in the rejects
// map. We're not going to fail yet in the second case but wait for the
// channel openings to proceed first. In case a timeout happens as well
// we can report that together with the other errors.
if err := eg.Wait(); err != nil {
select {
case <-m.quit:
return nil, err
default:
}
}
// If the only order that we were involved in is a sidecar channel and
// we are the provider, not the ultimate recipient, then there's nothing
// to wait for since the channel is going to be opened to another node.
if len(chanPoints) == 0 {
log.Debugf("Don't need to wait for channel events, only " +
"sidecar channels matched")
return nil, nil
}
// We've kicked off all channel open streams. We'll now need to wait for
// either completion of the funding process or a timeout.
return m.waitForChannelOpen(
setupCtx, chanPoints, m.pendingOpenChanClient, fundingRejects,
)
}
// SidecarBatchChannelSetup will attempt to establish new funding flows with all
// matched takers (people buying our channels) in the passed batch. This method
// will block until the channel is considered pending. Once this phase is
// complete, and the batch execution transaction broadcast, the channel will be
// finalized and locked in.
func (m *Manager) SidecarBatchChannelSetup(batch *order.Batch,
chanUpdates *subscribe.Client,
getOrder order.Fetcher) (map[wire.OutPoint]*chaninfo.ChannelInfo,
error) {
var (
chanPoints = make(map[wire.OutPoint]order.Nonce)
fundingRejects = make(map[order.Nonce]*auctioneerrpc.OrderReject)
)
// We need to make sure our whole process doesn't take too long overall
// so we create a context that is valid for the whole funding step and
// use that everywhere.
setupCtx, cancel := context.WithTimeout(
context.Background(), m.cfg.BatchStepTimeout,
)
defer cancel()
log.Infof("Batch(%x): opening channels for %v matched orders",
batch.ID[:], len(batch.MatchedOrders))
// For each ask order of ours that's matched, we'll make a new funding
// flow, blocking until they all progress to the final state.
batchTxHash := batch.BatchTX.TxHash()
for ourOrderNonce, matchedOrders := range batch.MatchedOrders {
ourOrder, err := getOrder(ourOrderNonce)
if err != nil {
// There can be sidecar and non-sidecar orders in the
// same batch. If we get an error here, it means it's
// not a sidecar order. The "normal" funding flow will
// take care of it.
log.Debugf("unexpected channel funding "+
"for order %v as it's not a sidecar order: %v",
ourOrderNonce.String(), err)
continue
}
// We'll obtain the expected channel point for each matched
// order, and complete the funding flow for each one in which
// our order was the ask.
for _, matchedOrder := range matchedOrders {
fundingShim, _, err := m.deriveFundingShim(
ourOrder, matchedOrder, batch.BatchTX,
)
if err != nil {
return nil, err
}
chanPoint := wire.OutPoint{
Hash: batchTxHash,
Index: fundingShim.GetChanPointShim().ChanPoint.
OutputIndex,
}
chanPoints[chanPoint] = matchedOrder.Order.Nonce()
}
}
// We've kicked off all channel open streams. We'll now need to wait for
// either completion of the funding process or a timeout.
return m.waitForChannelOpen(
setupCtx, chanPoints, chanUpdates, fundingRejects,
)
}
// waitForChannelOpen waits until we get a pending open channel message for each
// of the channel outpoints provided. If we don't get all of them in time, we'll
// instead return a reject error with all the orders for which the funding
// failed.
func (m *Manager) waitForChannelOpen(ctx context.Context,
chanPoints map[wire.OutPoint]order.Nonce, chanUpdates *subscribe.Client,
fundingRejects map[order.Nonce]*auctioneerrpc.OrderReject) (
map[wire.OutPoint]*chaninfo.ChannelInfo, error) {
// Once we've waited for the operations to complete, we'll wait to
// receive each channel's pending open notification in order to retrieve
// some keys from their SCB we'll need to submit to the auctioneer in
// order for them to enforce the channel's service lifetime. We don't
// need to acquire any lock anymore because all previously started
// goroutines have now exited.
channelKeys := make(
map[wire.OutPoint]*chaninfo.ChannelInfo, len(chanPoints),
)
log.Debugf("Waiting for pending open events for %v channel(s)",
len(chanPoints))
for {
var chanPoint wire.OutPoint
select {
case channel := <-chanUpdates.Updates():
pendingChannel, ok := channel.(*lnrpc.PendingUpdate)
if !ok || pendingChannel == nil {
continue
}
var hash chainhash.Hash
copy(hash[:], pendingChannel.Txid)
chanPoint = wire.OutPoint{
Hash: hash,
Index: pendingChannel.OutputIndex,
}
case <-ctx.Done():
// One or more of our peers timed out. At this point the
// chanPoints map should only contain channels that
// failed/timed out. So we can partially reject all of
// them.
const timeoutErrStr = "timed out waiting for pending " +
"open channel notification"
for _, nonce := range chanPoints {
fundingRejects[nonce] = &auctioneerrpc.OrderReject{
ReasonCode: rpcCodeFundingFailed,
Reason: timeoutErrStr,
}
}
return nil, &MatchRejectErr{
RejectedOrders: fundingRejects,
}
case <-m.pendingOpenChanClient.Quit():
return nil, fmt.Errorf("server shutting down")
case <-m.quit:
return nil, fmt.Errorf("server shutting down")
}
// If the notification is for a channel we're not interested in,
// skip it. This can happen if a channel was opened out-of-band
// at the same time the batch channels were.
if _, ok := chanPoints[chanPoint]; !ok {
continue
}
log.Debugf("Retrieving info for channel %v", chanPoint)
chanInfo, err := chaninfo.GatherChannelInfo(
ctx, m.cfg.LightningClient, m.cfg.WalletKit, chanPoint,
)
if err != nil {
return nil, err
}
// Once we've retrieved the keys for all channels, we can exit.
channelKeys[chanPoint] = chanInfo
delete(chanPoints, chanPoint)
if len(chanPoints) == 0 {
break
}
}
// If we got to this point with entries in the reject map it means some
// channels failed directly when opening but the rest succeeded without
// a timeout. We still want to reject the batch because of the failures.
if len(fundingRejects) > 0 {
return nil, &MatchRejectErr{
RejectedOrders: fundingRejects,
}
}
return channelKeys, nil
}
// DeletePendingBatch removes all references to the current pending batch
// without applying its staged updates to accounts and orders. If no pending
// batch exists, this acts as a no-op.
//
// NOTE: This is part of the auctioneer.BatchCleaner interface.
func (m *Manager) DeletePendingBatch() error {
return m.cfg.DB.DeletePendingBatch()
}
// RemovePendingBatchArtifacts removes any funding shims or pending channels
// from a batch that was never finalized. Some non-terminal errors are logged
// only and not returned. Therefore if this method returns an error, it should
// be handled as terminal error.
//
// NOTE: This is part of the auctioneer.BatchCleaner interface.
func (m *Manager) RemovePendingBatchArtifacts(
matchedOrders map[order.Nonce][]*order.MatchedOrder,
batchTx *wire.MsgTx) error {
err := CancelPendingFundingShims(
matchedOrders, m.cfg.BaseClient, m.cfg.DB.GetOrder,
)
if err != nil {
// CancelPendingFundingShims only returns hard errors that
// justify us rejecting the batch or logging an actual error.
return fmt.Errorf("error canceling funding shims "+
"from previous pending batch: %v", err)
}
// Also abandon any channels that might still be pending
// from a previous round of the same batch or a previous
// batch that we didn't make it into the final round.
err = AbandonCanceledChannels(
matchedOrders, batchTx, m.cfg.WalletKit, m.cfg.BaseClient,
m.cfg.DB.GetOrder,
)
if err != nil {
// AbandonCanceledChannels also only returns hard errors that
// justify us rejecting the batch or logging an actual error.
return fmt.Errorf("error abandoning channels from "+
"previous pending batch: %v", err)
}
return nil
}
// OfferSidecar creates a sidecar channel offer and embeds it in a new sidecar
// ticket. The offer is signed with the local lnd's node public key. If a bid
// is passed along, then this indicates that the ticket is intended to be used
// for autonated sidecar negotiation.
func (m *Manager) OfferSidecar(ctx context.Context, capacity,
pushAmt btcutil.Amount, duration uint32,
acctPubKey *keychain.KeyDescriptor,
bid *order.Bid, auto bool) (*sidecar.Ticket, error) {
// Make sure the capacity and push amounts are sane.
err := sidecar.CheckOfferParams(capacity, pushAmt, order.BaseSupplyUnit)
if err != nil {
return nil, err
}
// So far everything looks good. Let's create the ticket with the offer
// now.
ticket, err := sidecar.NewTicket(
sidecar.VersionDefault, capacity, pushAmt, duration,
acctPubKey.PubKey, auto,
)
if err != nil {
return nil, fmt.Errorf("error creating sidecar ticket: %v", err)
}
// Let's sign the offer part of the ticket with our node's identity key
// now.
if err := sidecar.SignOffer(
ctx, ticket, acctPubKey.KeyLocator, m.cfg.SignerClient,
); err != nil {
return nil, fmt.Errorf("error signing offer: %v", err)
}
// Let's now store and return the ticket with the signed offer. If a
// bid was specified, then we'll commit that as well so we can submit
// it to the auctioneer later once we communicate with the recipient.
if bid == nil {
err = m.cfg.DB.AddSidecar(ticket)
} else {
err = m.cfg.DB.AddSidecarWithBid(ticket, bid)
}
if err != nil {
return nil, fmt.Errorf("error storing sidecar ticket: %v", err)
}
return ticket, nil
}
// connectToMatchedTrader attempts to connect to a trader that we've had an
// order matched with, on all available addresses.
func (m *Manager) connectToMatchedTrader(ctx context.Context,
nodeKey [33]byte, addrs []net.Addr) {
for _, addr := range addrs {
err := m.cfg.LightningClient.Connect(
ctx, nodeKey, addr.String(), false,
)
if err != nil {
// If we're already connected, then we can stop now.
if strings.Contains(err.Error(), "already connected") {
return
}
log.Warnf("unable to connect to trader at %x@%v",
nodeKey[:], addr)
continue
}
// We connected successfully, no need to try any of the
// other addresses.
break
}
}
// waitForPeerConnections makes sure the connected lnd has a persistent
// connection to each of the peers in the given map. This method blocks until
// either all connections are established successfully or the passed context is
// canceled.
//
// NOTE: The passed context MUST have a timeout applied to it, otherwise this
// method will block forever in case a connection doesn't succeed.
func (m *Manager) waitForPeerConnections(ctx context.Context,
peers map[route.Vertex]struct{}, batch *order.Batch) error {
// First of all, subscribe to new peer events so we certainly don't miss
// an update while we look for the already connected peers.
ctxc, cancel := context.WithCancel(ctx)
defer cancel()
subscription, err := m.cfg.BaseClient.SubscribePeerEvents(
ctxc, &lnrpc.PeerEventSubscription{},
)
if err != nil {
return fmt.Errorf("unable to subscribe to peer events: %v", err)
}
// Query all connected peers. This only returns active peers so once a
// node key appears in this list, we can be reasonably sure the
// connection is established (flapping peers notwithstanding).
resp, err := m.cfg.BaseClient.ListPeers(
ctx, &lnrpc.ListPeersRequest{},
)
if err != nil {
return fmt.Errorf("unable to query peers: %v", err)
}
// Remove all connected peers from our previously de-duplicated map.
for _, peer := range resp.Peers {
peerKeyBytes, err := hex.DecodeString(peer.PubKey)
if err != nil {
return fmt.Errorf("error parsing node key: %v", err)
}
var peerKey [33]byte
copy(peerKey[:], peerKeyBytes)
delete(peers, peerKey)
}
// Some connection attempts are still ongoing, let's now wait for peer
// events or the cancellation of the context.
for {
// Great, we're done now, all connections established!
if len(peers) == 0 {
return nil
}
select {
// We can't wait forever, otherwise the auctioneer will kick us
// out of the batch for not responding. In case we weren't able
// to open all required connections, we reject the orders that
// came from the peers with the failed connections.
case <-ctx.Done():
return &MatchRejectErr{
RejectedOrders: m.rejectFailedConnections(
peers, batch,
),
}
// We're shutting down, nothing more to do here.
case <-m.quit:
return nil
default:
}
// Block on reading the next peer event now.
msg, err := subscription.Recv()
if err != nil {
// We must inspect the gprc status to uncover the
// actual error.
st, ok := status.FromError(err)
if !ok {
return fmt.Errorf("error reading peer event: "+
"%v", err)
}
// We've run into the timeout. Let the code above return
// the error, we should now run into the ctx.Done()
// case.
switch st.Code() {
case codes.Canceled, codes.DeadlineExceeded:
continue
default:
return fmt.Errorf("error reading peer events, "+
"unhandled status: %v", err)
}
}
// We're only interested in online events, skip everything else.
if msg.Type != lnrpc.PeerEvent_PEER_ONLINE {
continue
}
// Great, a new peer is online, let's remove it from the wait
// list now.
peerKeyBytes, err := hex.DecodeString(msg.PubKey)
if err != nil {
return fmt.Errorf("error parsing node key: %v", err)
}
var peerKey [33]byte
copy(peerKey[:], peerKeyBytes)
delete(peers, peerKey)
}
}
// rejectDuplicateChannels gathers a list of matched orders that should be
// rejected because they would create channels to peers that the trader already
// has channels with.
func (m *Manager) rejectDuplicateChannels(
batch *order.Batch) (map[order.Nonce]*auctioneerrpc.OrderReject, error) {
// We gather all peers from the open and pending channels.
ctxb := context.Background()
peers := make(map[route.Vertex]struct{})
openChans, err := m.cfg.LightningClient.ListChannels(ctxb, false, false)
if err != nil {
return nil, fmt.Errorf("error listing open channels: %v", err)
}
pendingChans, err := m.cfg.LightningClient.PendingChannels(ctxb)
if err != nil {
return nil, fmt.Errorf("error listing pending channels: %v",
err)
}
for _, openChan := range openChans {
peers[openChan.PubKeyBytes] = struct{}{}
}
for _, pendingChan := range pendingChans.PendingOpen {
peers[pendingChan.PubKeyBytes] = struct{}{}
}
// Gather the list of matches we reject because they are to peers we
// already have channels with.
const haveChannel = "already have open/pending channel with peer"
const rejectCode = auctioneerrpc.OrderReject_DUPLICATE_PEER
fundingRejects := make(map[order.Nonce]*auctioneerrpc.OrderReject)
for _, matchedOrders := range batch.MatchedOrders {
for _, matchedOrder := range matchedOrders {
_, ok := peers[matchedOrder.NodeKey]
if !ok {
continue
}
log.Debugf("Rejecting channel to node %x: %v",
matchedOrder.NodeKey[:], haveChannel)
otherNonce := matchedOrder.Order.Nonce()
fundingRejects[otherNonce] = &auctioneerrpc.OrderReject{
ReasonCode: rejectCode,
Reason: haveChannel,
}
}
}
return fundingRejects, nil
}
// rejectFailedConnections gathers a list of all matched orders that are to
// peers to which we couldn't connect and want to reject because of that.
func (m *Manager) rejectFailedConnections(peers map[route.Vertex]struct{},
batch *order.Batch) map[order.Nonce]*auctioneerrpc.OrderReject {
// Gather the list of matches we reject because the connection to them
// failed.
const connFailed = "connection not established before timeout"
const rejectCode = auctioneerrpc.OrderReject_CHANNEL_FUNDING_FAILED
fundingRejects := make(map[order.Nonce]*auctioneerrpc.OrderReject)
for _, matchedOrders := range batch.MatchedOrders {
for _, matchedOrder := range matchedOrders {
_, ok := peers[matchedOrder.NodeKey]
if !ok {
continue
}
log.Debugf("Rejecting channel to node %x: %v",
matchedOrder.NodeKey[:], connFailed)
otherNonce := matchedOrder.Order.Nonce()
fundingRejects[otherNonce] = &auctioneerrpc.OrderReject{
ReasonCode: rejectCode,
Reason: connFailed,
}
}
}
return fundingRejects
}
// nodeHasTorAddrs returns true if there exists a Tor address amongst the set
// of active Uris for a node.
func nodeHasTorAddrs(nodeAddrs []string) bool {
for _, nodeAddr := range nodeAddrs {
// Obtain the host to determine if this is a Tor address.
host, _, err := net.SplitHostPort(nodeAddr)
if err != nil {
host = nodeAddr
}
if tor.IsOnionHost(host) {
return true
}
}
return false
}