diff --git a/htlc_interceptor.go b/htlc_interceptor.go new file mode 100644 index 0000000..bb487df --- /dev/null +++ b/htlc_interceptor.go @@ -0,0 +1,6 @@ +package main + +type HtlcInterceptor interface { + Start() error + Stop() error +} diff --git a/intercept.go b/intercept.go index f9a2b95..e1f39e0 100644 --- a/intercept.go +++ b/intercept.go @@ -2,27 +2,159 @@ package main import ( "bytes" - "context" "fmt" "log" "math/big" - "os" - "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire" - "github.com/lightningnetwork/lnd/lnrpc" - "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/routing/route" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - - sphinx "github.com/lightningnetwork/lightning-onion" ) +type interceptAction int + +const ( + INTERCEPT_RESUME interceptAction = 0 + INTERCEPT_RESUME_OR_CANCEL interceptAction = 1 + INTERCEPT_FAIL_HTLC interceptAction = 2 +) + +type interceptFailureCode int + +const ( + FAILURE_TEMPORARY_CHANNEL_FAILURE interceptFailureCode = 0 + FAILURE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS interceptFailureCode = 1 +) + +type interceptResult struct { + action interceptAction + failureCode interceptFailureCode + destination []byte + amountMsat uint64 + channelPoint *wire.OutPoint + onionBlob []byte +} + +func intercept(reqPaymentHash []byte, reqOutgoingAmountMsat uint64, reqOutgoingExpiry uint32) interceptResult { + paymentHash, paymentSecret, destination, incomingAmountMsat, outgoingAmountMsat, channelPoint, err := paymentInfo(reqPaymentHash) + if err != nil { + log.Printf("paymentInfo(%x) error: %v", reqPaymentHash, err) + return interceptResult{ + action: INTERCEPT_FAIL_HTLC, + } + } + log.Printf("paymentHash:%x\npaymentSecret:%x\ndestination:%x\nincomingAmountMsat:%v\noutgoingAmountMsat:%v\n\n", + paymentHash, paymentSecret, destination, incomingAmountMsat, outgoingAmountMsat) + if paymentSecret != nil { + + if channelPoint == nil { + if bytes.Compare(paymentHash, reqPaymentHash) == 0 { + channelPoint, err = openChannel(client, reqPaymentHash, destination, incomingAmountMsat) + log.Printf("openChannel(%x, %v) err: %v", destination, incomingAmountMsat, err) + if err != nil { + return interceptResult{ + action: INTERCEPT_FAIL_HTLC, + } + } + } else { //probing + failureCode := FAILURE_TEMPORARY_CHANNEL_FAILURE + isConnected, _ := client.IsConnected(destination) + if err != nil || !*isConnected { + failureCode = FAILURE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS + } + + return interceptResult{ + action: INTERCEPT_FAIL_HTLC, + failureCode: failureCode, + } + } + } + + pubKey, err := btcec.ParsePubKey(destination) + if err != nil { + log.Printf("btcec.ParsePubKey(%x): %v", destination, err) + return interceptResult{ + action: INTERCEPT_FAIL_HTLC, + } + } + + sessionKey, err := btcec.NewPrivateKey() + if err != nil { + log.Printf("btcec.NewPrivateKey(): %v", err) + return interceptResult{ + action: INTERCEPT_FAIL_HTLC, + } + } + + var bigProd, bigAmt big.Int + amt := (bigAmt.Div(bigProd.Mul(big.NewInt(outgoingAmountMsat), big.NewInt(int64(reqOutgoingAmountMsat))), big.NewInt(incomingAmountMsat))).Int64() + + var addr [32]byte + copy(addr[:], paymentSecret) + hop := route.Hop{ + AmtToForward: lnwire.MilliSatoshi(amt), + OutgoingTimeLock: reqOutgoingExpiry, + MPP: record.NewMPP(lnwire.MilliSatoshi(outgoingAmountMsat), addr), + CustomRecords: make(record.CustomSet), + } + + var b bytes.Buffer + err = hop.PackHopPayload(&b, uint64(0)) + if err != nil { + log.Printf("hop.PackHopPayload(): %v", err) + return interceptResult{ + action: INTERCEPT_FAIL_HTLC, + } + } + + payload, err := sphinx.NewHopPayload(nil, b.Bytes()) + if err != nil { + log.Printf("sphinx.NewHopPayload(): %v", err) + return interceptResult{ + action: INTERCEPT_FAIL_HTLC, + } + } + + var sphinxPath sphinx.PaymentPath + sphinxPath[0] = sphinx.OnionHop{ + NodePub: *pubKey, + HopPayload: payload, + } + sphinxPacket, err := sphinx.NewOnionPacket( + &sphinxPath, sessionKey, reqPaymentHash, + sphinx.DeterministicPacketFiller, + ) + if err != nil { + log.Printf("sphinx.NewOnionPacket(): %v", err) + return interceptResult{ + action: INTERCEPT_FAIL_HTLC, + } + } + var onionBlob bytes.Buffer + err = sphinxPacket.Encode(&onionBlob) + if err != nil { + log.Printf("sphinxPacket.Encode(): %v", err) + return interceptResult{ + action: INTERCEPT_FAIL_HTLC, + } + } + + return interceptResult{ + action: INTERCEPT_RESUME_OR_CANCEL, + destination: destination, + channelPoint: channelPoint, + amountMsat: uint64(amt), + onionBlob: onionBlob.Bytes(), + } + } else { + return interceptResult{ + action: INTERCEPT_RESUME, + } + } +} func checkPayment(incomingAmountMsat, outgoingAmountMsat int64) error { fees := incomingAmountMsat * channelFeePermyriad / 10_000 / 1_000 * 1_000 if fees < channelMinimumFeeMsat { @@ -60,203 +192,3 @@ func openChannel(client LightningClient, paymentHash, destination []byte, incomi err = setFundingTx(paymentHash, channelPoint) return channelPoint, err } - -func failForwardSend(interceptorClient routerrpc.Router_HtlcInterceptorClient, incomingCircuitKey *routerrpc.CircuitKey) { - interceptorClient.Send(&routerrpc.ForwardHtlcInterceptResponse{ - IncomingCircuitKey: incomingCircuitKey, - Action: routerrpc.ResolveHoldForwardAction_FAIL, - }) -} - -func intercept(client *LndClient) { - for { - cancellableCtx, cancel := context.WithCancel(context.Background()) - clientCtx := metadata.AppendToOutgoingContext(cancellableCtx, "macaroon", os.Getenv("LND_MACAROON_HEX")) - interceptorClient, err := client.routerClient.HtlcInterceptor(clientCtx) - if err != nil { - log.Printf("routerClient.HtlcInterceptor(): %v", err) - cancel() - time.Sleep(1 * time.Second) - continue - } - - for { - request, err := interceptorClient.Recv() - if err != nil { - // If it is just the error result of the context cancellation - // the we exit silently. - status, ok := status.FromError(err) - if ok && status.Code() == codes.Canceled { - break - } - // Otherwise it an unexpected error, we fail the test. - log.Printf("unexpected error in interceptor.Recv() %v", err) - cancel() - break - } - fmt.Printf("htlc: %v\nchanID: %v\nincoming amount: %v\noutgoing amount: %v\nincomin expiry: %v\noutgoing expiry: %v\npaymentHash: %x\nonionBlob: %x\n\n", - request.IncomingCircuitKey.HtlcId, - request.IncomingCircuitKey.ChanId, - request.IncomingAmountMsat, - request.OutgoingAmountMsat, - request.IncomingExpiry, - request.OutgoingExpiry, - request.PaymentHash, - request.OnionBlob, - ) - - paymentHash, paymentSecret, destination, incomingAmountMsat, outgoingAmountMsat, channelPoint, err := paymentInfo(request.PaymentHash) - if err != nil { - log.Printf("paymentInfo(%x) error: %v", request.PaymentHash, err) - failForwardSend(interceptorClient, request.IncomingCircuitKey) - continue - } - log.Printf("paymentHash:%x\npaymentSecret:%x\ndestination:%x\nincomingAmountMsat:%v\noutgoingAmountMsat:%v\n\n", - paymentHash, paymentSecret, destination, incomingAmountMsat, outgoingAmountMsat) - if paymentSecret != nil { - - if channelPoint == nil { - if bytes.Compare(paymentHash, request.PaymentHash) == 0 { - channelPoint, err = openChannel(client, request.PaymentHash, destination, incomingAmountMsat) - log.Printf("openChannel(%x, %v) err: %v", destination, incomingAmountMsat, err) - if err != nil { - failForwardSend(interceptorClient, request.IncomingCircuitKey) - continue - } - } else { //probing - failureCode := lnrpc.Failure_TEMPORARY_CHANNEL_FAILURE - isConnected, _ := client.IsConnected(destination) - if err != nil || !*isConnected { - failureCode = lnrpc.Failure_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS - } - - interceptorClient.Send(&routerrpc.ForwardHtlcInterceptResponse{ - IncomingCircuitKey: request.IncomingCircuitKey, - Action: routerrpc.ResolveHoldForwardAction_FAIL, - FailureCode: failureCode, - }) - continue - } - } - - pubKey, err := btcec.ParsePubKey(destination) - if err != nil { - log.Printf("btcec.ParsePubKey(%x): %v", destination, err) - failForwardSend(interceptorClient, request.IncomingCircuitKey) - continue - } - - sessionKey, err := btcec.NewPrivateKey() - if err != nil { - log.Printf("btcec.NewPrivateKey(): %v", err) - failForwardSend(interceptorClient, request.IncomingCircuitKey) - continue - } - - var bigProd, bigAmt big.Int - amt := (bigAmt.Div(bigProd.Mul(big.NewInt(outgoingAmountMsat), big.NewInt(int64(request.OutgoingAmountMsat))), big.NewInt(incomingAmountMsat))).Int64() - - var addr [32]byte - copy(addr[:], paymentSecret) - hop := route.Hop{ - AmtToForward: lnwire.MilliSatoshi(amt), - OutgoingTimeLock: request.OutgoingExpiry, - MPP: record.NewMPP(lnwire.MilliSatoshi(outgoingAmountMsat), addr), - CustomRecords: make(record.CustomSet), - } - - var b bytes.Buffer - err = hop.PackHopPayload(&b, uint64(0)) - if err != nil { - log.Printf("hop.PackHopPayload(): %v", err) - failForwardSend(interceptorClient, request.IncomingCircuitKey) - continue - } - - payload, err := sphinx.NewHopPayload(nil, b.Bytes()) - if err != nil { - log.Printf("sphinx.NewHopPayload(): %v", err) - failForwardSend(interceptorClient, request.IncomingCircuitKey) - continue - } - - var sphinxPath sphinx.PaymentPath - sphinxPath[0] = sphinx.OnionHop{ - NodePub: *pubKey, - HopPayload: payload, - } - sphinxPacket, err := sphinx.NewOnionPacket( - &sphinxPath, sessionKey, request.PaymentHash, - sphinx.DeterministicPacketFiller, - ) - if err != nil { - log.Printf("sphinx.NewOnionPacket(): %v", err) - failForwardSend(interceptorClient, request.IncomingCircuitKey) - continue - } - var onionBlob bytes.Buffer - err = sphinxPacket.Encode(&onionBlob) - if err != nil { - log.Printf("sphinxPacket.Encode(): %v", err) - failForwardSend(interceptorClient, request.IncomingCircuitKey) - continue - } - - go resumeOrCancel( - clientCtx, interceptorClient, request.IncomingCircuitKey, destination, - *channelPoint, uint64(amt), onionBlob.Bytes(), - ) - - } else { - interceptorClient.Send(&routerrpc.ForwardHtlcInterceptResponse{ - IncomingCircuitKey: request.IncomingCircuitKey, - Action: routerrpc.ResolveHoldForwardAction_RESUME, - OutgoingAmountMsat: request.OutgoingAmountMsat, - OutgoingRequestedChanId: request.OutgoingRequestedChanId, - OnionBlob: request.OnionBlob, - }) - } - } - } -} - -func resumeOrCancel( - ctx context.Context, - interceptorClient routerrpc.Router_HtlcInterceptorClient, - incomingCircuitKey *routerrpc.CircuitKey, - destination []byte, - channelPoint wire.OutPoint, - outgoingAmountMsat uint64, - onionBlob []byte, -) { - deadline := time.Now().Add(10 * time.Second) - for { - ch, err := client.GetChannel(destination, channelPoint) - if err != nil { - failForwardSend(interceptorClient, incomingCircuitKey) - return - } - - if uint64(ch.InitialChannelID) != 0 { - interceptorClient.Send(&routerrpc.ForwardHtlcInterceptResponse{ - IncomingCircuitKey: incomingCircuitKey, - Action: routerrpc.ResolveHoldForwardAction_RESUME, - OutgoingAmountMsat: outgoingAmountMsat, - OutgoingRequestedChanId: uint64(ch.InitialChannelID), - OnionBlob: onionBlob, - }) - err := insertChannel(uint64(ch.InitialChannelID), uint64(ch.ConfirmedChannelID), channelPoint.String(), destination, time.Now()) - if err != nil { - log.Printf("insertChannel error: %v", err) - } - return - } - log.Printf("getChannel(%x, %v) returns 0", destination, channelPoint) - if time.Now().After(deadline) { - log.Printf("Stop retrying getChannel(%x, %v)", destination, channelPoint) - break - } - time.Sleep(1 * time.Second) - } - failForwardSend(interceptorClient, incomingCircuitKey) -} diff --git a/lnd_interceptor.go b/lnd_interceptor.go new file mode 100644 index 0000000..78389fe --- /dev/null +++ b/lnd_interceptor.go @@ -0,0 +1,143 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +type LndHtlcInterceptor struct { + client *LndClient +} + +func NewLndHtlcInterceptor(client *LndClient) *LndHtlcInterceptor { + return &LndHtlcInterceptor{ + client: client, + } +} + +func (i *LndHtlcInterceptor) Start() error { + go forwardingHistorySynchronize(i.client) + go channelsSynchronize(i.client) + return i.intercept() +} + +func (i *LndHtlcInterceptor) Stop() error { + return nil +} + +func (i *LndHtlcInterceptor) intercept() error { + for { + cancellableCtx, cancel := context.WithCancel(context.Background()) + clientCtx := metadata.AppendToOutgoingContext(cancellableCtx, "macaroon", os.Getenv("LND_MACAROON_HEX")) + interceptorClient, err := client.routerClient.HtlcInterceptor(clientCtx) + if err != nil { + log.Printf("routerClient.HtlcInterceptor(): %v", err) + cancel() + time.Sleep(1 * time.Second) + continue + } + + for { + request, err := interceptorClient.Recv() + if err != nil { + // If it is just the error result of the context cancellation + // the we exit silently. + status, ok := status.FromError(err) + if ok && status.Code() == codes.Canceled { + break + } + // Otherwise it an unexpected error, we fail the test. + log.Printf("unexpected error in interceptor.Recv() %v", err) + cancel() + break + } + fmt.Printf("htlc: %v\nchanID: %v\nincoming amount: %v\noutgoing amount: %v\nincomin expiry: %v\noutgoing expiry: %v\npaymentHash: %x\nonionBlob: %x\n\n", + request.IncomingCircuitKey.HtlcId, + request.IncomingCircuitKey.ChanId, + request.IncomingAmountMsat, + request.OutgoingAmountMsat, + request.IncomingExpiry, + request.OutgoingExpiry, + request.PaymentHash, + request.OnionBlob, + ) + + interceptResult := intercept(request.PaymentHash, request.OutgoingAmountMsat, request.OutgoingExpiry) + switch interceptResult.action { + case INTERCEPT_RESUME_OR_CANCEL: + go resumeOrCancel(clientCtx, interceptorClient, request.IncomingCircuitKey, + interceptResult.destination, *interceptResult.channelPoint, + interceptResult.amountMsat, interceptResult.onionBlob) + case INTERCEPT_FAIL_HTLC: + failForwardSend(interceptorClient, request.IncomingCircuitKey) + case INTERCEPT_RESUME: + fallthrough + default: + interceptorClient.Send(&routerrpc.ForwardHtlcInterceptResponse{ + IncomingCircuitKey: request.IncomingCircuitKey, + Action: routerrpc.ResolveHoldForwardAction_RESUME, + OutgoingAmountMsat: request.OutgoingAmountMsat, + OutgoingRequestedChanId: request.OutgoingRequestedChanId, + OnionBlob: request.OnionBlob, + }) + } + } + } +} + +func failForwardSend(interceptorClient routerrpc.Router_HtlcInterceptorClient, incomingCircuitKey *routerrpc.CircuitKey) { + interceptorClient.Send(&routerrpc.ForwardHtlcInterceptResponse{ + IncomingCircuitKey: incomingCircuitKey, + Action: routerrpc.ResolveHoldForwardAction_FAIL, + }) +} + +func resumeOrCancel( + ctx context.Context, + interceptorClient routerrpc.Router_HtlcInterceptorClient, + incomingCircuitKey *routerrpc.CircuitKey, + destination []byte, + channelPoint wire.OutPoint, + outgoingAmountMsat uint64, + onionBlob []byte, +) { + deadline := time.Now().Add(10 * time.Second) + for { + ch, err := client.GetChannel(destination, channelPoint) + if err != nil { + failForwardSend(interceptorClient, incomingCircuitKey) + return + } + + if uint64(ch.InitialChannelID) != 0 { + interceptorClient.Send(&routerrpc.ForwardHtlcInterceptResponse{ + IncomingCircuitKey: incomingCircuitKey, + Action: routerrpc.ResolveHoldForwardAction_RESUME, + OutgoingAmountMsat: outgoingAmountMsat, + OutgoingRequestedChanId: uint64(ch.InitialChannelID), + OnionBlob: onionBlob, + }) + err := insertChannel(uint64(ch.InitialChannelID), uint64(ch.ConfirmedChannelID), channelPoint.String(), destination, time.Now()) + if err != nil { + log.Printf("insertChannel error: %v", err) + } + return + } + log.Printf("getChannel(%x, %v) returns 0", destination, channelPoint) + if time.Now().After(deadline) { + log.Printf("Stop retrying getChannel(%x, %v)", destination, channelPoint) + break + } + time.Sleep(1 * time.Second) + } + failForwardSend(interceptorClient, incomingCircuitKey) +} diff --git a/main.go b/main.go index 749f300..7c06105 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,7 @@ func main() { } client = NewLndClient() + interceptor := NewLndHtlcInterceptor(client) info, err := client.GetInfo() if err != nil { @@ -36,7 +37,7 @@ func main() { nodePubkey = info.Pubkey } - go intercept(client) + go interceptor.Start() go forwardingHistorySynchronize(client) go channelsSynchronize(client)