mirror of
https://github.com/lightningequipment/circuitbreaker.git
synced 2026-08-13 12:33:02 +02:00
Update error handling for outgoing channel not found to catch the case where an outgoing channel was not found for a failed HTLC. Unlike incoming HTLCs, where the HTLC arrived on the channel so we know it exists, we have not yet performed any existence validation on the outgoing channel (because interception happens before we check that it exists). Since we only need the outgoing channel for record keeping, we just log the case where a HTLC was failed back and we don't know the channel (since this is just a bogus channel). We don't store this HTLC in the DB, as it will be instantly failed back.
565 lines
12 KiB
Go
565 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
"go.uber.org/zap"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
var (
|
|
rpcTimeout = 10 * time.Second
|
|
defaultPeerRefreshInterval = 10 * time.Minute
|
|
|
|
errChannelNotFound = errors.New("channel not found")
|
|
)
|
|
|
|
const burstSize = 10
|
|
|
|
type lndclient interface {
|
|
getInfo() (*info, error)
|
|
|
|
listChannels() (map[uint64]*channel, error)
|
|
|
|
listClosedChannels() (map[uint64]*channel, error)
|
|
|
|
getNodeAlias(key route.Vertex) (string, error)
|
|
|
|
subscribeHtlcEvents(ctx context.Context) (htlcEventsClient, error)
|
|
|
|
htlcInterceptor(ctx context.Context) (htlcInterceptorClient, error)
|
|
|
|
getPendingIncomingHtlcs(ctx context.Context, peer *route.Vertex) (
|
|
map[route.Vertex]map[circuitKey]*inFlightHtlc, error)
|
|
}
|
|
|
|
type circuitKey struct {
|
|
channel uint64
|
|
htlc uint64
|
|
}
|
|
|
|
type interceptEvent struct {
|
|
circuitKey
|
|
incomingMsat lnwire.MilliSatoshi
|
|
outgoingMsat lnwire.MilliSatoshi
|
|
resume func(bool) error
|
|
}
|
|
|
|
type resolvedEvent struct {
|
|
incomingCircuitKey circuitKey
|
|
outgoingCircuitKey circuitKey
|
|
settled bool
|
|
timestamp time.Time
|
|
}
|
|
|
|
type rateCounters struct {
|
|
counters map[route.Vertex]*peerState
|
|
}
|
|
|
|
type rateCountersRequest struct {
|
|
counters chan *rateCounters
|
|
}
|
|
|
|
type process struct {
|
|
db *Db
|
|
client lndclient
|
|
limits *Limits
|
|
log *zap.SugaredLogger
|
|
|
|
interceptChan chan interceptEvent
|
|
resolveChan chan resolvedEvent
|
|
updateLimitChan chan updateLimitEvent
|
|
rateCountersRequestChan chan rateCountersRequest
|
|
newPeerChan chan route.Vertex
|
|
|
|
identity route.Vertex
|
|
chanMap map[uint64]*channel
|
|
aliasMap map[route.Vertex]string
|
|
|
|
peerCtrls map[route.Vertex]*peerController
|
|
|
|
burstSize int
|
|
peerRefreshInterval time.Duration
|
|
|
|
// Testing hook
|
|
resolvedCallback func()
|
|
}
|
|
|
|
func NewProcess(client lndclient, log *zap.SugaredLogger, limits *Limits, db *Db) *process {
|
|
return &process{
|
|
db: db,
|
|
log: log,
|
|
client: client,
|
|
interceptChan: make(chan interceptEvent),
|
|
resolveChan: make(chan resolvedEvent),
|
|
updateLimitChan: make(chan updateLimitEvent),
|
|
rateCountersRequestChan: make(chan rateCountersRequest),
|
|
newPeerChan: make(chan route.Vertex),
|
|
chanMap: make(map[uint64]*channel),
|
|
aliasMap: make(map[route.Vertex]string),
|
|
peerCtrls: make(map[route.Vertex]*peerController),
|
|
limits: limits,
|
|
burstSize: burstSize,
|
|
peerRefreshInterval: defaultPeerRefreshInterval,
|
|
}
|
|
}
|
|
|
|
type updateLimitEvent struct {
|
|
limit *Limit
|
|
peer *route.Vertex
|
|
}
|
|
|
|
func (p *process) UpdateLimit(ctx context.Context, peer *route.Vertex,
|
|
limit *Limit) error {
|
|
|
|
if peer == nil && limit == nil {
|
|
return errors.New("cannot clear default limit")
|
|
}
|
|
|
|
update := updateLimitEvent{
|
|
limit: limit,
|
|
peer: peer,
|
|
}
|
|
|
|
select {
|
|
case p.updateLimitChan <- update:
|
|
return nil
|
|
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
func (p *process) Run(ctx context.Context) error {
|
|
p.log.Info("CircuitBreaker started")
|
|
|
|
info, err := p.client.getInfo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.identity = info.nodeKey
|
|
|
|
p.log.Infow("Connected to lnd node",
|
|
"pubkey", p.identity.String())
|
|
|
|
group, ctx := errgroup.WithContext(ctx)
|
|
|
|
stream, err := p.client.subscribeHtlcEvents(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
interceptor, err := p.client.htlcInterceptor(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p.log.Info("Interceptor/notification handlers registered")
|
|
|
|
group.Go(func() error {
|
|
err := p.processHtlcEvents(ctx, stream)
|
|
if err != nil {
|
|
return fmt.Errorf("htlc events error: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
group.Go(func() error {
|
|
err := p.processInterceptor(ctx, interceptor)
|
|
if err != nil {
|
|
return fmt.Errorf("interceptor error: %w", err)
|
|
}
|
|
|
|
return err
|
|
})
|
|
|
|
group.Go(func() error {
|
|
return p.peerRefreshLoop(ctx)
|
|
})
|
|
|
|
group.Go(func() error {
|
|
return p.runEventLoop(ctx)
|
|
})
|
|
|
|
return group.Wait()
|
|
}
|
|
|
|
func (p *process) peerRefreshLoop(ctx context.Context) error {
|
|
notifiedPeers := make(map[route.Vertex]struct{})
|
|
|
|
for {
|
|
// Get all peers.
|
|
channels, err := p.client.listChannels()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Notify the main event loop of the new ones.
|
|
for _, ch := range channels {
|
|
if _, ok := notifiedPeers[ch.peer]; ok {
|
|
continue
|
|
}
|
|
|
|
notifiedPeers[ch.peer] = struct{}{}
|
|
|
|
select {
|
|
case p.newPeerChan <- ch.peer:
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
// Poll delay.
|
|
select {
|
|
case <-time.After(p.peerRefreshInterval):
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *process) getPeerController(ctx context.Context, peer route.Vertex,
|
|
startGo func(func() error)) *peerController {
|
|
|
|
ctrl, ok := p.peerCtrls[peer]
|
|
if ok {
|
|
return ctrl
|
|
}
|
|
|
|
// If the peer does not yet exist, initialize it with no pending htlcs.
|
|
htlcs := make(map[circuitKey]*inFlightHtlc)
|
|
|
|
return p.createPeerController(ctx, peer, startGo, htlcs)
|
|
}
|
|
|
|
func (p *process) createPeerController(ctx context.Context, peer route.Vertex,
|
|
startGo func(func() error),
|
|
htlcs map[circuitKey]*inFlightHtlc) *peerController {
|
|
|
|
peerCfg, ok := p.limits.PerPeer[peer]
|
|
if !ok {
|
|
peerCfg = p.limits.Default
|
|
}
|
|
|
|
cfg := &peerControllerCfg{
|
|
logger: p.log,
|
|
limit: peerCfg,
|
|
burstSize: p.burstSize,
|
|
htlcs: htlcs,
|
|
lnd: p.client,
|
|
pubKey: peer,
|
|
now: time.Now,
|
|
htlcCompleted: func(ctx context.Context, htlc *HtlcInfo) error {
|
|
// If the add time of a htlc is zero, it was resumed after a LND
|
|
// restart. We don't store these htlcs because they have
|
|
// incomplete information (missing add time and amounts).
|
|
if htlc.addTime.IsZero() {
|
|
log.Debug("Not storing incomplete htlc resumed after "+
|
|
"restart: %v (%v) -> %v (%v)",
|
|
htlc.incomingCircuit.channel,
|
|
htlc.incomingCircuit.htlc,
|
|
htlc.outgoingCircuit.channel,
|
|
htlc.outgoingCircuit.htlc)
|
|
|
|
return nil
|
|
}
|
|
|
|
return p.db.RecordHtlcResolution(ctx, htlc)
|
|
},
|
|
}
|
|
ctrl := newPeerController(cfg)
|
|
|
|
startGo(func() error {
|
|
return ctrl.run(ctx)
|
|
})
|
|
|
|
p.peerCtrls[peer] = ctrl
|
|
|
|
return ctrl
|
|
}
|
|
|
|
func (p *process) runEventLoop(ctx context.Context) error {
|
|
group, ctx := errgroup.WithContext(ctx)
|
|
|
|
// Event loop will spin up new goroutines using the group that is passed in here.
|
|
// We run it in the same group so that both errors in eventLoop and those in
|
|
// the goroutines that is spins will will prompt exit.
|
|
group.Go(func() error {
|
|
return p.eventLoop(ctx, group)
|
|
})
|
|
|
|
return group.Wait()
|
|
}
|
|
|
|
func (p *process) eventLoop(ctx context.Context, group *errgroup.Group) error {
|
|
// Retrieve all pending htlcs from lnd.
|
|
htlcsPerPeer, err := p.client.getPendingIncomingHtlcs(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Initialize peer controllers with currently pending htlcs.
|
|
for peer, htlcs := range htlcsPerPeer {
|
|
p.createPeerController(ctx, peer, group.Go, htlcs)
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case interceptEvent := <-p.interceptChan:
|
|
chanInfo, err := p.getChanInfo(interceptEvent.channel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctrl := p.getPeerController(ctx, chanInfo.peer, group.Go)
|
|
|
|
peerEvent := peerInterceptEvent{
|
|
interceptEvent: interceptEvent,
|
|
peerInitiated: !chanInfo.initiator,
|
|
}
|
|
if err := ctrl.process(ctx, peerEvent); err != nil {
|
|
return err
|
|
}
|
|
|
|
case resolvedEvent := <-p.resolveChan:
|
|
chanInfo, err := p.getChanInfo(
|
|
resolvedEvent.incomingCircuitKey.channel,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctrl := p.getPeerController(ctx, chanInfo.peer, group.Go)
|
|
|
|
// Lookup the outgoing peer to supplement the information on the
|
|
// resolved event. Here we handle a channel lookup error
|
|
// differently to the incoming channel, because it's possible
|
|
// we were forwarded a HTLC with a bogus outgoing channel. If
|
|
// this is the case, LND would have failed the HTLC back even if
|
|
// we let it through. We catch and log that error, rather than
|
|
// exiting like we do with incoming channels (where we reasonably
|
|
// expect to find the channel). We still enforce channel lookup
|
|
// for successful HTLCs, because then we know that the channel
|
|
// does exist and should be found.
|
|
var outgoingPeer *route.Vertex
|
|
chanInfo, err = p.getChanInfo(
|
|
resolvedEvent.outgoingCircuitKey.channel,
|
|
)
|
|
switch {
|
|
case errors.Is(err, errChannelNotFound) && !resolvedEvent.settled:
|
|
log.Debugf("Channel not found for failed htlc: %v",
|
|
resolvedEvent.outgoingCircuitKey.channel)
|
|
|
|
case err != nil:
|
|
return err
|
|
|
|
default:
|
|
outgoingPeer = &chanInfo.peer
|
|
}
|
|
|
|
if err := ctrl.resolved(ctx, peerResolvedEvent{
|
|
resolvedEvent: resolvedEvent,
|
|
outgoingPeer: outgoingPeer,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
if p.resolvedCallback != nil {
|
|
p.resolvedCallback()
|
|
}
|
|
|
|
case update := <-p.updateLimitChan:
|
|
switch {
|
|
// Update sets default limit.
|
|
case update.peer == nil:
|
|
p.limits.Default = *update.limit
|
|
|
|
// Update all controllers that have no specific limit.
|
|
for node, ctrl := range p.peerCtrls {
|
|
_, ok := p.limits.PerPeer[node]
|
|
if ok {
|
|
continue
|
|
}
|
|
|
|
err := ctrl.updateLimit(ctx, *update.limit)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Update sets specific limit.
|
|
case update.limit != nil:
|
|
p.limits.PerPeer[*update.peer] = *update.limit
|
|
|
|
// Update specific controller if it exists.
|
|
ctrl, ok := p.peerCtrls[*update.peer]
|
|
if ok {
|
|
err := ctrl.updateLimit(ctx, *update.limit)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Update clears limit.
|
|
case update.limit == nil:
|
|
delete(p.limits.PerPeer, *update.peer)
|
|
|
|
// Apply default limit to peer controller.
|
|
ctrl, ok := p.peerCtrls[*update.peer]
|
|
if ok {
|
|
err := ctrl.updateLimit(ctx, p.limits.Default)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
case req := <-p.rateCountersRequestChan:
|
|
allCounts := make(map[route.Vertex]*peerState)
|
|
for node, ctrl := range p.peerCtrls {
|
|
state, err := ctrl.state(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
allCounts[node] = state
|
|
}
|
|
|
|
req.counters <- &rateCounters{
|
|
counters: allCounts,
|
|
}
|
|
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
|
|
// A new or existing peer has been reported.
|
|
case newPeer := <-p.newPeerChan:
|
|
p.log.Infow("New peer notification received", "peer", newPeer)
|
|
|
|
// Try to get the existing peer controller. If it doesn't exist, it
|
|
// will be created. This causes the peer to be reported over grpc.
|
|
_ = p.getPeerController(ctx, newPeer, group.Go)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *process) getRateCounters(ctx context.Context) (
|
|
map[route.Vertex]*peerState, error) {
|
|
|
|
replyChan := make(chan *rateCounters)
|
|
|
|
select {
|
|
case p.rateCountersRequestChan <- rateCountersRequest{
|
|
counters: replyChan,
|
|
}:
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
|
|
select {
|
|
case reply := <-replyChan:
|
|
return reply.counters, nil
|
|
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
|
|
func (p *process) processHtlcEvents(ctx context.Context,
|
|
stream htlcEventsClient) error {
|
|
|
|
for {
|
|
event, err := stream.recv()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
select {
|
|
case p.resolveChan <- *event:
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *process) processInterceptor(ctx context.Context,
|
|
interceptor htlcInterceptorClient) error {
|
|
|
|
for {
|
|
event, err := interceptor.recv()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
key := event.circuitKey
|
|
|
|
resume := func(resume bool) error {
|
|
return interceptor.send(&interceptResponse{
|
|
key: key,
|
|
resume: resume,
|
|
})
|
|
}
|
|
|
|
select {
|
|
case p.interceptChan <- interceptEvent{
|
|
circuitKey: key,
|
|
incomingMsat: event.incomingMsat,
|
|
outgoingMsat: event.outgoingMsat,
|
|
resume: resume,
|
|
}:
|
|
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *process) getChanInfo(channel uint64) (*channel, error) {
|
|
// Try to look up from the cache.
|
|
ch, ok := p.chanMap[channel]
|
|
if ok {
|
|
return ch, nil
|
|
}
|
|
|
|
// Cache miss. Retrieve all channels and update the cache.
|
|
channels, err := p.client.listChannels()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for chanId, ch := range channels {
|
|
p.chanMap[chanId] = ch
|
|
}
|
|
|
|
// Try looking up the channel again.
|
|
ch, ok = p.chanMap[channel]
|
|
if ok {
|
|
return ch, nil
|
|
}
|
|
|
|
// If the channel is not open, fall back to checking our closed
|
|
// channels.
|
|
closedChannels, err := p.client.listClosedChannels()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Add to cache and try again.
|
|
for chanId, ch := range closedChannels {
|
|
p.chanMap[chanId] = ch
|
|
}
|
|
|
|
ch, ok = p.chanMap[channel]
|
|
if ok {
|
|
return ch, nil
|
|
}
|
|
|
|
// Channel not found.
|
|
return nil, fmt.Errorf("%w: %v", errChannelNotFound, channel)
|
|
}
|