mirror of
https://github.com/lightningequipment/circuitbreaker.git
synced 2026-08-13 12:33:02 +02:00
362 lines
7.2 KiB
Go
362 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
var (
|
|
rpcTimeout = 10 * time.Second
|
|
ctxb = context.Background()
|
|
)
|
|
|
|
type lndclient interface {
|
|
getIdentity() (route.Vertex, error)
|
|
|
|
listChannels() (map[uint64]*channel, error)
|
|
|
|
getNodeAlias(key route.Vertex) (string, error)
|
|
|
|
subscribeHtlcEvents(ctx context.Context,
|
|
in *routerrpc.SubscribeHtlcEventsRequest) (
|
|
routerrpc.Router_SubscribeHtlcEventsClient, error)
|
|
|
|
htlcInterceptor(ctx context.Context) (
|
|
routerrpc.Router_HtlcInterceptorClient, error)
|
|
|
|
getPendingIncomingHtlcs(ctx context.Context) (
|
|
map[circuitKey]struct{}, error)
|
|
}
|
|
|
|
type circuitKey struct {
|
|
channel uint64
|
|
htlc uint64
|
|
}
|
|
|
|
type interceptEvent struct {
|
|
circuitKey
|
|
valueMsat int64
|
|
resume func(bool) error
|
|
}
|
|
|
|
type process struct {
|
|
client lndclient
|
|
cfg *config
|
|
|
|
interceptChan chan interceptEvent
|
|
resolveChan chan circuitKey
|
|
|
|
identity route.Vertex
|
|
chanMap map[uint64]*channel
|
|
aliasMap map[route.Vertex]string
|
|
|
|
peerCtrls map[route.Vertex]*peerController
|
|
|
|
// Testing hook
|
|
resolvedCallback func()
|
|
}
|
|
|
|
func newProcess(client lndclient, cfg *config) *process {
|
|
return &process{
|
|
interceptChan: make(chan interceptEvent),
|
|
resolveChan: make(chan circuitKey),
|
|
chanMap: make(map[uint64]*channel),
|
|
aliasMap: make(map[route.Vertex]string),
|
|
client: client,
|
|
cfg: cfg,
|
|
peerCtrls: make(map[route.Vertex]*peerController),
|
|
}
|
|
}
|
|
|
|
func (p *process) run(ctx context.Context) error {
|
|
log.Info("CircuitBreaker started")
|
|
|
|
var err error
|
|
p.identity, err = p.client.getIdentity()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Infow("Connected to lnd node",
|
|
"pubkey", p.identity.String())
|
|
|
|
group, ctx := errgroup.WithContext(ctx)
|
|
|
|
stream, err := p.client.subscribeHtlcEvents(
|
|
ctx, &routerrpc.SubscribeHtlcEventsRequest{},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
interceptor, err := p.client.htlcInterceptor(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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.eventLoop(ctx)
|
|
})
|
|
|
|
return group.Wait()
|
|
}
|
|
|
|
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]struct{})
|
|
|
|
return p.createPeerController(ctx, peer, startGo, htlcs)
|
|
}
|
|
|
|
func (p *process) createPeerController(ctx context.Context, peer route.Vertex,
|
|
startGo func(func() error), htlcs map[circuitKey]struct{}) *peerController {
|
|
|
|
peerCfg := p.cfg.forPeer(peer)
|
|
|
|
alias := p.getNodeAlias(peer)
|
|
|
|
logger := log.With(
|
|
"peer_alias", alias,
|
|
"peer", peer.String(),
|
|
)
|
|
|
|
ctrl := newPeerController(logger, peerCfg, htlcs)
|
|
|
|
startGo(func() error {
|
|
return ctrl.run(ctx)
|
|
})
|
|
|
|
p.peerCtrls[peer] = ctrl
|
|
|
|
return ctrl
|
|
}
|
|
|
|
func (p *process) eventLoop(ctx context.Context) error {
|
|
// Create a group to attach peer goroutines to.
|
|
group, ctx := errgroup.WithContext(ctx)
|
|
defer func() {
|
|
_ = group.Wait()
|
|
}()
|
|
|
|
// Retrieve all pending htlcs from lnd.
|
|
allHtlcs, err := p.client.getPendingIncomingHtlcs(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Arrange htlcs per peer.
|
|
htlcsPerPeer := make(map[route.Vertex]map[circuitKey]struct{})
|
|
for h := range allHtlcs {
|
|
peer, err := p.getChanInfo(h.channel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
htlcs := htlcsPerPeer[peer.peer]
|
|
if htlcs == nil {
|
|
htlcs = make(map[circuitKey]struct{})
|
|
htlcsPerPeer[peer.peer] = htlcs
|
|
}
|
|
|
|
htlcs[h] = struct{}{}
|
|
}
|
|
|
|
// 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 resolvedKey := <-p.resolveChan:
|
|
chanInfo, err := p.getChanInfo(resolvedKey.channel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctrl := p.getPeerController(ctx, chanInfo.peer, group.Go)
|
|
|
|
if err := ctrl.resolved(ctx, resolvedKey); err != nil {
|
|
return err
|
|
}
|
|
|
|
if p.resolvedCallback != nil {
|
|
p.resolvedCallback()
|
|
}
|
|
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *process) processHtlcEvents(ctx context.Context,
|
|
stream routerrpc.Router_SubscribeHtlcEventsClient) error {
|
|
|
|
for {
|
|
event, err := stream.Recv()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if event.EventType != routerrpc.HtlcEvent_FORWARD {
|
|
continue
|
|
}
|
|
|
|
switch event.Event.(type) {
|
|
case *routerrpc.HtlcEvent_SettleEvent:
|
|
case *routerrpc.HtlcEvent_ForwardFailEvent:
|
|
case *routerrpc.HtlcEvent_LinkFailEvent:
|
|
|
|
default:
|
|
continue
|
|
}
|
|
|
|
select {
|
|
case p.resolveChan <- circuitKey{
|
|
channel: event.IncomingChannelId,
|
|
htlc: event.IncomingHtlcId,
|
|
}:
|
|
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *process) processInterceptor(ctx context.Context,
|
|
interceptor routerrpc.Router_HtlcInterceptorClient) error {
|
|
|
|
for {
|
|
event, err := interceptor.Recv()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
key := circuitKey{
|
|
channel: event.IncomingCircuitKey.ChanId,
|
|
htlc: event.IncomingCircuitKey.HtlcId,
|
|
}
|
|
|
|
resume := func(resume bool) error {
|
|
response := &routerrpc.ForwardHtlcInterceptResponse{
|
|
IncomingCircuitKey: &routerrpc.CircuitKey{
|
|
ChanId: key.channel,
|
|
HtlcId: key.htlc,
|
|
},
|
|
}
|
|
if resume {
|
|
response.Action = routerrpc.ResolveHoldForwardAction_RESUME
|
|
} else {
|
|
response.Action = routerrpc.ResolveHoldForwardAction_FAIL
|
|
}
|
|
|
|
return interceptor.Send(response)
|
|
}
|
|
|
|
select {
|
|
case p.interceptChan <- interceptEvent{
|
|
circuitKey: key,
|
|
valueMsat: int64(event.OutgoingAmountMsat),
|
|
resume: resume,
|
|
}:
|
|
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *process) getNodeAlias(key route.Vertex) string {
|
|
alias, ok := p.aliasMap[key]
|
|
if ok {
|
|
return alias
|
|
}
|
|
|
|
alias, err := p.client.getNodeAlias(key)
|
|
if err != nil {
|
|
log.Warnw("cannot get node alias",
|
|
"err", err)
|
|
|
|
return ""
|
|
}
|
|
|
|
p.aliasMap[key] = alias
|
|
|
|
return alias
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Channel not found.
|
|
return nil, fmt.Errorf("incoming channel %v not found", channel)
|
|
}
|