This commit is contained in:
callebtc
2022-07-08 15:04:39 +02:00
parent 6e4ae7f61a
commit e1d1a0d356
5 changed files with 285 additions and 76 deletions

View File

@@ -4,14 +4,16 @@ import (
"context"
"encoding/binary"
"encoding/hex"
"fmt"
"sync"
"github.com/lightningnetwork/lnd/lnrpc"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
)
func dispatchChannelAcceptor(ctx context.Context, conn *grpc.ClientConn, client lnrpc.LightningClient) {
func dispatchChannelAcceptor(ctx context.Context) {
client := ctx.Value(clientKey).(lnrpc.LightningClient)
// wait group for channel acceptor
defer ctx.Value(ctxKeyWaitGroup).(*sync.WaitGroup).Done()
// get the lnd grpc connection
@@ -26,21 +28,33 @@ func dispatchChannelAcceptor(ctx context.Context, conn *grpc.ClientConn, client
if err != nil {
log.Errorf(err.Error())
}
log.Infof("New channel request from %s", hex.EncodeToString(req.NodePubkey))
// print the incoming channel request
alias, err := getNodeAlias(ctx, hex.EncodeToString(req.NodePubkey))
if err != nil {
log.Errorf(err.Error())
}
var node_info_string string
if alias != "" {
node_info_string = fmt.Sprintf("%s (%s)", alias, hex.EncodeToString(req.NodePubkey))
} else {
node_info_string = hex.EncodeToString(req.NodePubkey)
}
log.Debugf("New channel request from %s", node_info_string)
var accept bool
if Configuration.Mode == "whitelist" {
if Configuration.ChannelMode == "whitelist" {
accept = false
for _, pubkey := range Configuration.Whitelist {
for _, pubkey := range Configuration.ChannelWhitelist {
if hex.EncodeToString(req.NodePubkey) == pubkey {
accept = true
break
}
}
} else if Configuration.Mode == "blacklist" {
} else if Configuration.ChannelMode == "blacklist" {
accept = true
for _, pubkey := range Configuration.Blacklist {
for _, pubkey := range Configuration.ChannelBlacklist {
if hex.EncodeToString(req.NodePubkey) == pubkey {
accept = false
break
@@ -48,9 +62,16 @@ func dispatchChannelAcceptor(ctx context.Context, conn *grpc.ClientConn, client
}
}
var channel_info_string string
if alias != "" {
channel_info_string = fmt.Sprintf("from %s (%s, %d sat, chan_id:%d)", alias, trimPubKey(req.NodePubkey), req.FundingAmt, binary.BigEndian.Uint64(req.PendingChanId))
} else {
channel_info_string = fmt.Sprintf("from %s (%d sat, chan_id:%d)", trimPubKey(req.NodePubkey), req.FundingAmt, binary.BigEndian.Uint64(req.PendingChanId))
}
res := lnrpc.ChannelAcceptResponse{}
if accept {
log.Infof("✅ [%s mode] Allow channel from %s (chan_id: %d)", Configuration.Mode, trimPubKey(req.NodePubkey), binary.BigEndian.Uint64(req.PendingChanId))
log.Infof("✅ [channel-mode %s] Allow channel %s", Configuration.ChannelMode, channel_info_string)
res = lnrpc.ChannelAcceptResponse{Accept: true,
PendingChanId: req.PendingChanId,
CsvDelay: req.CsvDelay,
@@ -61,9 +82,9 @@ func dispatchChannelAcceptor(ctx context.Context, conn *grpc.ClientConn, client
}
} else {
log.Infof("❌ [%s mode] Reject channel from %s", Configuration.Mode, trimPubKey(req.NodePubkey))
log.Infof("❌ [channel-mode %s] Reject channel %s", Configuration.ChannelMode, channel_info_string)
res = lnrpc.ChannelAcceptResponse{Accept: false,
Error: Configuration.RejectMessage}
Error: Configuration.ChannelRejectMessage}
}
err = acceptClient.Send(&res)
if err != nil {

View File

@@ -8,14 +8,17 @@ import (
)
var Configuration = struct {
Mode string `yaml:"mode"`
Host string `yaml:"host"`
MacaroonPath string `yaml:"macaroon_path"`
TLSPath string `yaml:"tls_path"`
Whitelist []string `yaml:"whitelist"`
Blacklist []string `yaml:"blacklist"`
RejectMessage string `yaml:"reject_message"`
Workers int `yaml:"workers"`
ChannelMode string `yaml:"channel-mode"`
Host string `yaml:"host"`
MacaroonPath string `yaml:"macaroon_path"`
TLSPath string `yaml:"tls-path"`
Debug bool `yaml:"debug"`
ChannelWhitelist []string `yaml:"channel-whitelist"`
ChannelBlacklist []string `yaml:"channel-blacklist"`
ChannelRejectMessage string `yaml:"channel-reject-message"`
ForwardMode string `yaml:"forward-mode"`
ForwardWhitelist []string `yaml:"forward-whitelist"`
ForwardBlacklist []string `yaml:"forward-blacklist"`
}{}
func init() {
@@ -27,20 +30,39 @@ func init() {
}
func checkConfig() {
setLogger(Configuration.Debug)
welcome()
if Configuration.Host == "" {
panic(fmt.Errorf("no host specified in config.yaml"))
}
if len(Configuration.Whitelist) == 0 {
panic(fmt.Errorf("no accepted pubkeys specified in config.yaml"))
if Configuration.MacaroonPath == "" {
panic(fmt.Errorf("no macaroon path specified in config.yaml"))
}
if Configuration.TLSPath == "" {
panic(fmt.Errorf("no tls path specified in config.yaml"))
}
if len(Configuration.RejectMessage) > 500 {
log.Warnf("reject message is too long. Trimming to 500 characters.")
Configuration.RejectMessage = Configuration.RejectMessage[:500]
if len(Configuration.ChannelRejectMessage) > 500 {
log.Warnf("channel reject message is too long. Trimming to 500 characters.")
Configuration.ChannelRejectMessage = Configuration.ChannelRejectMessage[:500]
}
if len(Configuration.Mode) == 0 {
Configuration.Mode = "blacklist"
if len(Configuration.ChannelMode) == 0 {
Configuration.ChannelMode = "blacklist"
}
log.Infof("Running in %s mode", Configuration.Mode)
if Configuration.ChannelMode != "whitelist" && Configuration.ChannelMode != "blacklist" {
panic(fmt.Errorf("channel mode must be either whitelist or blacklist"))
}
log.Infof("Channel acceptor running in %s mode", Configuration.ForwardMode)
if len(Configuration.ForwardMode) == 0 {
Configuration.ForwardMode = "blacklist"
}
if Configuration.ForwardMode != "whitelist" && Configuration.ForwardMode != "blacklist" {
panic(fmt.Errorf("channel mode must be either whitelist or blacklist"))
}
log.Infof("HTLC forwarder running in %s mode", Configuration.ForwardMode)
}

View File

@@ -1,17 +1,31 @@
# Mode can either be "blacklist" or "whitelist"
mode: "blacklist"
# Credentials for your node
host: "127.0.0.1:10009"
macaroon_path: "/home/bitcoin/.lnd/data/chain/bitcoin/mainnet/admin.macaroon"
tls_path: "/home/bitcoin/.lnd/tls.cert"
tls-path: "/home/bitcoin/.lnd/tls.cert"
# ----- Channel accept -----
# Mode can either be "blacklist" or "whitelist"
channel-mode: "blacklist"
# This error message will be sent to the other party upon a reject
reject_message: "Contact me at user@email.com"
channel-reject-message: "Contact me at user@email.com"
# List of nodes to whitelist or blacklist
whitelist:
channel-whitelist:
- "03de70865239e99460041e127647b37101b9eb335b3c22de95c944671f0dabc2d0"
- "0307299a290529c5ccb3a5e3bd2eb504daf64cc65c6d65b582c01cbd7e5ede14b6"
blacklist:
- "02853f9c1d15d479b433039885373b681683b84bb73e86dff861bee6697c17c1de"
channel-blacklist:
- "02853f9c1d15d479b433039885373b681683b84bb73e86dff861bee6697c17c1de"
# ----- HTLC forwarding -----
# Mode can either be "blacklist" or "whitelist"
forward-mode: "whitelist"
# List of channel IDs to whitelist or blacklist
forward-whitelist:
- "229797930270721"
forward-blacklist:
- "131941395398657"
- "195713069809665"

View File

@@ -2,15 +2,42 @@ package main
import (
"context"
"math/rand"
"errors"
"fmt"
"strconv"
"time"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/routing/route"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
)
func dispatchHTLCAcceptor(ctx context.Context, conn *grpc.ClientConn, client lnrpc.LightningClient) {
// type circuitKey struct {
// channel uint64
// htlc uint64
// }
// type interceptEvent struct {
// circuitKey
// valueMsat int64
// resume chan bool
// }
// type htlcAcceptor struct {
// interceptChan chan interceptEvent
// resolveChan chan circuitKey
// }
// func newHtlcAcceptor() *htlcAcceptor {
// return &htlcAcceptor{
// interceptChan: make(chan interceptEvent),
// resolveChan: make(chan circuitKey),
// }
// }
func dispatchHTLCAcceptor(ctx context.Context) {
conn := ctx.Value(connKey).(*grpc.ClientConn)
router := routerrpc.NewRouterClient(conn)
// htlc event subscriber, reports on incoming htlc events
@@ -20,7 +47,7 @@ func dispatchHTLCAcceptor(ctx context.Context, conn *grpc.ClientConn, client lnr
}
go func() {
err := logHtlcEvents(stream)
err := logHtlcEvents(ctx, stream)
if err != nil {
log.Error("htlc events error",
"err", err)
@@ -34,7 +61,7 @@ func dispatchHTLCAcceptor(ctx context.Context, conn *grpc.ClientConn, client lnr
}
go func() {
err := interceptHtlcEvents(interceptor)
err := interceptHtlcEvents(ctx, interceptor)
if err != nil {
log.Error("interceptor error",
"err", err)
@@ -44,7 +71,7 @@ func dispatchHTLCAcceptor(ctx context.Context, conn *grpc.ClientConn, client lnr
log.Info("Listening for incoming HTLCs")
}
func logHtlcEvents(stream routerrpc.Router_SubscribeHtlcEventsClient) error {
func logHtlcEvents(ctx context.Context, stream routerrpc.Router_SubscribeHtlcEventsClient) error {
for {
event, err := stream.Recv()
if err != nil {
@@ -58,50 +85,174 @@ func logHtlcEvents(stream routerrpc.Router_SubscribeHtlcEventsClient) error {
switch event.Event.(type) {
case *routerrpc.HtlcEvent_SettleEvent:
log.Infof("HTLC SettleEvent (chan_id:%d, htlc_id:%d)", event.IncomingChannelId, event.IncomingHtlcId)
log.Debugf("HTLC SettleEvent (chan_id:%d, htlc_id:%d)", event.IncomingChannelId, event.IncomingHtlcId)
case *routerrpc.HtlcEvent_ForwardFailEvent:
log.Infof("HTLC ForwardFailEvent (chan_id:%d, htlc_id:%d)", event.IncomingChannelId, event.IncomingHtlcId)
log.Debugf("HTLC ForwardFailEvent (chan_id:%d, htlc_id:%d)", event.IncomingChannelId, event.IncomingHtlcId)
case *routerrpc.HtlcEvent_ForwardEvent:
log.Infof("HTLC ForwardEvent (chan_id:%d, htlc_id:%d)", event.IncomingChannelId, event.IncomingHtlcId)
log.Debugf("HTLC ForwardEvent (chan_id:%d, htlc_id:%d)", event.IncomingChannelId, event.IncomingHtlcId)
case *routerrpc.HtlcEvent_LinkFailEvent:
log.Infof("HTLC LinkFailEvent (chan_id:%d, htlc_id:%d)", event.IncomingChannelId, event.IncomingHtlcId)
log.Debugf("HTLC LinkFailEvent (chan_id:%d, htlc_id:%d)", event.IncomingChannelId, event.IncomingHtlcId)
}
}
}
func interceptHtlcEvents(interceptor routerrpc.Router_HtlcInterceptorClient) error {
func interceptHtlcEvents(ctx context.Context, interceptor routerrpc.Router_HtlcInterceptorClient) error {
for {
event, err := interceptor.Recv()
if err != nil {
return err
}
go func() {
// decision for routing
decision_chan := make(chan bool, 1)
go htlcInterceptDecision(ctx, event, decision_chan)
// decision for routing
log.Infof("Received HTLC. Making random decision...")
accept := true
if rand.Intn(10) < 8 {
accept = false
}
channelEdge, err := getPubKeyFromChannel(ctx, event.IncomingCircuitKey.ChanId)
if err != nil {
log.Error("Error getting pubkey for channel %d", event.IncomingCircuitKey.ChanId)
}
alias, err := getNodeAlias(ctx, channelEdge.node1Pub.String())
if err != nil {
log.Errorf(err.Error())
}
response := &routerrpc.ForwardHtlcInterceptResponse{
IncomingCircuitKey: event.IncomingCircuitKey,
}
if accept {
log.Infof("✅ Accept HTLC (%d sat, htlc_id:%d, chan_id:%d->%d)", event.IncomingAmountMsat/1000, event.IncomingCircuitKey.HtlcId, event.IncomingCircuitKey.ChanId, event.OutgoingRequestedChanId)
response.Action = routerrpc.ResolveHoldForwardAction_RESUME
} else {
log.Infof("❌ Reject HTLC (%d sat, htlc_id:%d, chan_id:%d->%d)", event.IncomingAmountMsat/1000, event.IncomingCircuitKey.HtlcId, event.IncomingCircuitKey.ChanId, event.OutgoingRequestedChanId)
response.Action = routerrpc.ResolveHoldForwardAction_FAIL
}
err = interceptor.Send(response)
if err != nil {
return err
}
var forward_info_string string
if alias != "" {
forward_info_string = fmt.Sprintf("from %s (%d sat, htlc_id:%d, chan_id:%d->%d)", alias, event.IncomingAmountMsat/1000, event.IncomingCircuitKey.HtlcId, event.IncomingCircuitKey.ChanId, event.OutgoingRequestedChanId)
} else {
forward_info_string = fmt.Sprintf("(%d sat, htlc_id:%d, chan_id:%d->%d)", event.IncomingAmountMsat/1000, event.IncomingCircuitKey.HtlcId, event.IncomingCircuitKey.ChanId, event.OutgoingRequestedChanId)
}
response := &routerrpc.ForwardHtlcInterceptResponse{
IncomingCircuitKey: event.IncomingCircuitKey,
}
if <-decision_chan {
log.Infof("✅ [forward-mode %s] Accept HTLC %s", Configuration.ForwardMode, forward_info_string)
response.Action = routerrpc.ResolveHoldForwardAction_RESUME
} else {
log.Infof("❌ [forward-mode %s] Reject HTLC %s", Configuration.ForwardMode, forward_info_string)
response.Action = routerrpc.ResolveHoldForwardAction_FAIL
}
err = interceptor.Send(response)
if err != nil {
return
}
}()
}
}
func htlcInterceptDecision(ctx context.Context, event *routerrpc.ForwardHtlcInterceptRequest, decision_chan chan bool) {
var accept bool
if Configuration.ForwardMode == "whitelist" {
accept = false
for _, channel_id := range Configuration.ForwardWhitelist {
chan_id_int, err := strconv.ParseUint(channel_id, 10, 64)
if err != nil {
log.Error("Error parsing channel id %s", channel_id)
break
}
if event.IncomingCircuitKey.ChanId == chan_id_int {
accept = true
break
}
}
} else if Configuration.ForwardMode == "blacklist" {
accept = true
for _, channel_id := range Configuration.ForwardBlacklist {
chan_id_int, err := strconv.ParseUint(channel_id, 10, 64)
if err != nil {
log.Error("Error parsing channel id %s", channel_id)
break
}
if event.IncomingCircuitKey.ChanId == chan_id_int {
accept = false
break
}
}
}
decision_chan <- accept
}
// Heavily inspired by by Joost Jager's circuitbreaker
func getNodeAlias(ctx context.Context, pubkey string) (string, error) {
client := ctx.Value(clientKey).(lnrpc.LightningClient)
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
info, err := client.GetNodeInfo(ctx, &lnrpc.NodeInfoRequest{
PubKey: pubkey,
})
if err != nil {
return "", err
}
if info.Node == nil {
return "", errors.New("node info not available")
}
return info.Node.Alias, nil
}
type channelEdge struct {
node1Pub, node2Pub route.Vertex
}
func getPubKeyFromChannel(ctx context.Context, chan_id uint64) (*channelEdge, error) {
client := ctx.Value(clientKey).(lnrpc.LightningClient)
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
info, err := client.GetChanInfo(ctx, &lnrpc.ChanInfoRequest{
ChanId: chan_id,
})
if err != nil {
return nil, err
}
node1Pub, err := route.NewVertexFromStr(info.Node1Pub)
if err != nil {
return nil, err
}
node2Pub, err := route.NewVertexFromStr(info.Node2Pub)
if err != nil {
return nil, err
}
return &channelEdge{
node1Pub: node1Pub,
node2Pub: node2Pub,
}, nil
}
// func getPubKey(channel uint64) (route.Vertex, error) {
// pubkey, ok := p.pubkeyMap[channel]
// if ok {
// return pubkey, nil
// }
// edge, err := p.client.getChanInfo(channel)
// if err != nil {
// return route.Vertex{}, err
// }
// var remotePubkey route.Vertex
// switch {
// case edge.node1Pub == p.identity:
// remotePubkey = edge.node2Pub
// case edge.node2Pub == p.identity:
// remotePubkey = edge.node1Pub
// default:
// return route.Vertex{}, errors.New("identity not found in chan info")
// }
// p.pubkeyMap[channel] = remotePubkey
// return remotePubkey, nil
// }

19
main.go
View File

@@ -2,8 +2,6 @@ package main
import (
"context"
"encoding/hex"
"fmt"
"io/ioutil"
"sync"
@@ -22,6 +20,11 @@ const (
ctxKeyWaitGroup key = iota
)
type ContextKey string
var connKey ContextKey = "connKey"
var clientKey ContextKey = "clientKey"
// gets the lnd grpc connection
func getClientConnection(ctx context.Context) (*grpc.ClientConn, error) {
creds, err := credentials.NewClientTLSFromFile(Configuration.TLSPath, "")
@@ -54,10 +57,6 @@ func getClientConnection(ctx context.Context) (*grpc.ClientConn, error) {
}
func trimPubKey(pubkey []byte) string {
return fmt.Sprintf("%s...%s", hex.EncodeToString(pubkey)[:6], hex.EncodeToString(pubkey)[len(hex.EncodeToString(pubkey))-6:])
}
func main() {
ctx := context.Background()
conn, err := getClientConnection(ctx)
@@ -70,12 +69,14 @@ func main() {
ctx = context.WithValue(ctx, ctxKeyWaitGroup, &wg)
wg.Add(1)
ctx = context.WithValue(ctx, clientKey, client)
ctx = context.WithValue(ctx, connKey, conn)
// channel acceptor
go dispatchChannelAcceptor(ctx, conn, client)
go dispatchChannelAcceptor(ctx)
// htlc acceptor
go dispatchHTLCAcceptor(ctx, conn, client)
go dispatchHTLCAcceptor(ctx)
wg.Wait()
}