diff --git a/channelAcceptor.go b/channelAcceptor.go index 5fb7cea..5656eb5 100644 --- a/channelAcceptor.go +++ b/channelAcceptor.go @@ -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 { diff --git a/config.go b/config.go index 07d0bce..47cd2d8 100644 --- a/config.go +++ b/config.go @@ -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) } diff --git a/config.yaml.example b/config.yaml.example index 7a1f047..fc5b587 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -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" \ No newline at end of file +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" \ No newline at end of file diff --git a/htlcInterceptor.go b/htlcInterceptor.go index 533bcc4..95c66be 100644 --- a/htlcInterceptor.go +++ b/htlcInterceptor.go @@ -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 +// } diff --git a/main.go b/main.go index 245d028..1da3470 100644 --- a/main.go +++ b/main.go @@ -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() - }