diff --git a/.github/actions/setup-clightning/action.yaml b/.github/actions/setup-clightning/action.yaml index 37b39348..e078d1f1 100644 --- a/.github/actions/setup-clightning/action.yaml +++ b/.github/actions/setup-clightning/action.yaml @@ -74,6 +74,7 @@ runs: - name: Install dependencies if: steps.cache-core-lightning.outputs.cache-hit != 'true' run: | + sudo apt-get update -y sudo apt-get install -y autoconf automake build-essential git libtool libgmp-dev libsqlite3-dev python3 python3-pip net-tools zlib1g-dev libsodium-dev gettext valgrind libpq-dev shellcheck cppcheck libsecp256k1-dev jq sudo apt-get remove -y protobuf-compiler curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v3.12.0/protoc-3.12.0-linux-x86_64.zip diff --git a/.github/workflows/integration_tests.yaml b/.github/workflows/integration_tests.yaml index 01e9c858..2295daa4 100644 --- a/.github/workflows/integration_tests.yaml +++ b/.github/workflows/integration_tests.yaml @@ -83,7 +83,7 @@ jobs: - setup-lnd-lsp - setup-cln - build-lspd - name: test ${{ matrix.implementation }} ${{ matrix.test }} + name: test ${{ matrix.lsp }}-lsp ${{ matrix.client }}-client ${{ matrix.test }} strategy: max-parallel: 6 matrix: @@ -103,10 +103,18 @@ jobs: testOfflineNotificationRegularForward, testOfflineNotificationZeroConfChannel, ] - implementation: [ + lsp: [ LND, CLN ] + client: [ + LND, + CLN + ] + exclude: + - lsp: CLN + client: LND + steps: - name: Checkout uses: actions/checkout@v3 @@ -114,8 +122,8 @@ jobs: - name: Run and Process Test State uses: ./.github/actions/test-lspd with: - TESTRE: "TestLspd/${{ matrix.implementation }}-lspd:_${{ matrix.test }}" - artifact-name: TestLspd-${{ matrix.implementation }}-lspd_${{ matrix.test }} + TESTRE: "TestLspd/${{ matrix.lsp }}-lsp-${{ matrix.client}}-client:_${{ matrix.test }}" + artifact-name: TestLspd-${{ matrix.lsp }}-lsp-${{ matrix.client}}-client_${{ matrix.test }} bitcoin-version: ${{ env.BITCOIN_VERSION }} LSP_REF: ${{ env.LSP_REF }} CLIENT_REF: ${{ env.CLIENT_REF }} diff --git a/config/config.go b/config/config.go index dd287e58..12c773aa 100644 --- a/config/config.go +++ b/config/config.go @@ -15,6 +15,11 @@ type NodeConfig struct { // configured node, so it's obvious which node an rpc call is meant for. Tokens []string `json:"tokens"` + // If the used token is in the LegacyOnionTokens array, the forwarded htlc + // will have the legacy onion format. As per the time of writing breezmobile + // requires the legacy onion format. + LegacyOnionTokens []string `json:"legacyOnionTokens"` + // The network location of the lightning node, e.g. `12.34.56.78:9012` or // `localhost:10011` Host string `json:"host"` diff --git a/go.mod b/go.mod index 4525e3eb..72405bc5 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.19 require ( github.com/aws/aws-sdk-go v1.34.0 - github.com/breez/lntest v0.0.26 + github.com/breez/lntest v0.0.28 github.com/btcsuite/btcd v0.23.5-0.20230228185050-38331963bddd github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 diff --git a/interceptor/intercept.go b/interceptor/intercept.go index 9bbb0cd1..af3b7dae 100644 --- a/interceptor/intercept.go +++ b/interceptor/intercept.go @@ -15,6 +15,7 @@ import ( "github.com/breez/lspd/lightning" "github.com/breez/lspd/notifications" "github.com/btcsuite/btcd/wire" + "golang.org/x/exp/slices" "golang.org/x/sync/singleflight" ) @@ -35,14 +36,15 @@ var ( ) type InterceptResult struct { - Action InterceptAction - FailureCode InterceptFailureCode - Destination []byte - AmountMsat uint64 - TotalAmountMsat uint64 - ChannelPoint *wire.OutPoint - ChannelId uint64 - PaymentSecret []byte + Action InterceptAction + FailureCode InterceptFailureCode + Destination []byte + AmountMsat uint64 + TotalAmountMsat uint64 + ChannelPoint *wire.OutPoint + ChannelId uint64 + PaymentSecret []byte + UseLegacyOnionBlob bool } type Interceptor struct { @@ -257,14 +259,16 @@ func (i *Interceptor) Intercept(scid *basetypes.ShortChannelID, reqPaymentHash [ channelID = uint64(chanResult.InitialChannelID) } + useLegacyOnionBlob := slices.Contains(i.config.LegacyOnionTokens, token) return InterceptResult{ - Action: INTERCEPT_RESUME_WITH_ONION, - Destination: destination, - ChannelPoint: channelPoint, - ChannelId: channelID, - PaymentSecret: paymentSecret, - AmountMsat: uint64(amt), - TotalAmountMsat: uint64(outgoingAmountMsat), + Action: INTERCEPT_RESUME_WITH_ONION, + Destination: destination, + ChannelPoint: channelPoint, + ChannelId: channelID, + PaymentSecret: paymentSecret, + AmountMsat: uint64(amt), + TotalAmountMsat: uint64(outgoingAmountMsat), + UseLegacyOnionBlob: useLegacyOnionBlob, }, nil } diff --git a/itest/cln_breez_client.go b/itest/cln_breez_client.go index 357e0515..38dbe2af 100644 --- a/itest/cln_breez_client.go +++ b/itest/cln_breez_client.go @@ -38,6 +38,9 @@ def on_openchannel(openchannel, plugin, **kwargs): plugin.log(repr(openchannel)) mindepth = int(0) + if openchannel['funding_msat'] == 200000000: + return {'result': 'continue'} + plugin.log(f"This peer is in the zeroconf allowlist, setting mindepth={mindepth}") return {'result': 'continue', 'mindepth': mindepth} diff --git a/itest/lspd_node.go b/itest/lspd_node.go index fcbb5ec9..71be5198 100644 --- a/itest/lspd_node.go +++ b/itest/lspd_node.go @@ -22,6 +22,7 @@ import ( ecies "github.com/ecies/go/v2" "github.com/golang/protobuf/proto" "github.com/jackc/pgx/v4/pgxpool" + "google.golang.org/grpc/metadata" ) var ( @@ -121,6 +122,10 @@ func newLspd(h *lntest.TestHarness, mem *mempoolApi, name string, nodeConfig *co if nodeConfig.MinConfs != nil { conf.MinConfs = nodeConfig.MinConfs } + + if nodeConfig.LegacyOnionTokens != nil { + conf.LegacyOnionTokens = nodeConfig.LegacyOnionTokens + } } log.Printf("%s: node config: %+v", name, conf) @@ -271,9 +276,10 @@ func RegisterPayment(l LspNode, paymentInfo *lspd.PaymentInformation, continueOn encrypted, err := ecies.Encrypt(l.EciesPublicKey(), serialized) lntest.CheckError(l.Harness().T, err) + ctx := metadata.AppendToOutgoingContext(l.Harness().Ctx, "authorization", "Bearer hello") log.Printf("Registering payment") _, err = l.Rpc().RegisterPayment( - l.Harness().Ctx, + ctx, &lspd.RegisterPaymentRequest{ Blob: encrypted, }, diff --git a/itest/lspd_test.go b/itest/lspd_test.go index 098d68a1..57e982ef 100644 --- a/itest/lspd_test.go +++ b/itest/lspd_test.go @@ -14,8 +14,19 @@ var defaultTimeout time.Duration = time.Second * 120 func TestLspd(t *testing.T) { testCases := allTestCases - runTests(t, testCases, "LND-lspd", lndLspFunc, lndClientFunc) - runTests(t, testCases, "CLN-lspd", clnLspFunc, clnClientFunc) + runTests(t, testCases, "LND-lsp-CLN-client", lndLspFunc, clnClientFunc) + runTests(t, testCases, "LND-lsp-LND-client", legacyOnionLndLspFunc, lndClientFunc) + runTests(t, testCases, "CLN-lsp-CLN-client", clnLspFunc, clnClientFunc) +} + +func legacyOnionLndLspFunc(h *lntest.TestHarness, m *lntest.Miner, mem *mempoolApi, c *config.NodeConfig) LspNode { + cfg := c + if cfg == nil { + cfg = &config.NodeConfig{} + } + + cfg.LegacyOnionTokens = []string{"hello"} + return lndLspFunc(h, m, mem, cfg) } func lndLspFunc(h *lntest.TestHarness, m *lntest.Miner, mem *mempoolApi, c *config.NodeConfig) LspNode { diff --git a/itest/notification_test.go b/itest/notification_test.go index d4c0daf2..78b93e99 100644 --- a/itest/notification_test.go +++ b/itest/notification_test.go @@ -168,6 +168,23 @@ func testOfflineNotificationRegularForward(p *testParams) { }) log.Printf(bobInvoice.Bolt11) + invoiceWithHint := bobInvoice.Bolt11 + if !ContainsHopHint(p.t, bobInvoice.Bolt11) { + chans := p.BreezClient().Node().GetChannels() + assert.Len(p.t, chans, 1) + + var id lntest.ShortChannelID + if chans[0].RemoteAlias != nil { + id = *chans[0].RemoteAlias + } else if chans[0].LocalAlias != nil { + id = *chans[0].LocalAlias + } else { + id = chans[0].ShortChannelID + } + invoiceWithHint = AddHopHint(p.BreezClient(), bobInvoice.Bolt11, p.Lsp(), id, nil) + } + log.Printf("invoice with hint: %v", invoiceWithHint) + log.Printf("Bob going offline") p.BreezClient().Stop() @@ -176,7 +193,7 @@ func testOfflineNotificationRegularForward(p *testParams) { <-time.After(htlcInterceptorDelay) log.Printf("Alice paying") - payResp := alice.Pay(bobInvoice.Bolt11) + payResp := alice.Pay(invoiceWithHint) invoiceResult := p.BreezClient().Node().GetInvoice(bobInvoice.PaymentHash) assert.Equal(p.t, payResp.PaymentPreimage, invoiceResult.PaymentPreimage) diff --git a/itest/regular_forward_test.go b/itest/regular_forward_test.go index cd788b6f..20f54106 100644 --- a/itest/regular_forward_test.go +++ b/itest/regular_forward_test.go @@ -45,8 +45,25 @@ func testRegularForward(p *testParams) { }) log.Printf(bobInvoice.Bolt11) + invoiceWithHint := bobInvoice.Bolt11 + if !ContainsHopHint(p.t, bobInvoice.Bolt11) { + chans := p.BreezClient().Node().GetChannels() + assert.Len(p.t, chans, 1) + + var id lntest.ShortChannelID + if chans[0].RemoteAlias != nil { + id = *chans[0].RemoteAlias + } else if chans[0].LocalAlias != nil { + id = *chans[0].LocalAlias + } else { + id = chans[0].ShortChannelID + } + invoiceWithHint = AddHopHint(p.BreezClient(), bobInvoice.Bolt11, p.Lsp(), id, nil) + } + log.Printf("invoice with hint: %v", invoiceWithHint) + log.Printf("Alice paying") - payResp := alice.Pay(bobInvoice.Bolt11) + payResp := alice.Pay(invoiceWithHint) invoiceResult := p.BreezClient().Node().GetInvoice(bobInvoice.PaymentHash) assert.Equal(p.t, payResp.PaymentPreimage, invoiceResult.PaymentPreimage) diff --git a/itest/zero_reserve_test.go b/itest/zero_reserve_test.go index fad91b35..71ecec2d 100644 --- a/itest/zero_reserve_test.go +++ b/itest/zero_reserve_test.go @@ -61,4 +61,10 @@ func testZeroReserve(p *testParams) { assert.Equal(p.t, c.RemoteReserveMsat, c.CapacityMsat/100) log.Printf("local reserve: %d, remote reserve: %d", c.LocalReserveMsat, c.RemoteReserveMsat) assert.Zero(p.t, c.LocalReserveMsat) + + lspInvoice := p.lsp.LightningNode().CreateBolt11Invoice(&lntest.CreateInvoiceOptions{ + AmountMsat: innerAmountMsat - 1, + }) + <-time.After(time.Second * 1) + p.BreezClient().Node().Pay(lspInvoice.Bolt11) } diff --git a/lnd/interceptor.go b/lnd/interceptor.go index 8d1574f8..2d4ecb17 100644 --- a/lnd/interceptor.go +++ b/lnd/interceptor.go @@ -139,23 +139,7 @@ func (i *LndHtlcInterceptor) intercept() error { interceptResult := i.interceptor.Intercept(&scid, request.PaymentHash, request.OutgoingAmountMsat, request.OutgoingExpiry, request.IncomingExpiry) switch interceptResult.Action { case interceptor.INTERCEPT_RESUME_WITH_ONION: - onion, err := i.constructOnion(interceptResult, request.OutgoingExpiry, request.PaymentHash) - if err == nil { - interceptorClient.Send(&routerrpc.ForwardHtlcInterceptResponse{ - IncomingCircuitKey: request.IncomingCircuitKey, - Action: routerrpc.ResolveHoldForwardAction_RESUME, - OutgoingAmountMsat: interceptResult.AmountMsat, - OutgoingRequestedChanId: uint64(interceptResult.ChannelId), - OnionBlob: onion, - }) - } else { - interceptorClient.Send(&routerrpc.ForwardHtlcInterceptResponse{ - IncomingCircuitKey: request.IncomingCircuitKey, - Action: routerrpc.ResolveHoldForwardAction_FAIL, - FailureCode: lnrpc.Failure_TEMPORARY_CHANNEL_FAILURE, - }) - } - + interceptorClient.Send(i.createOnionResponse(interceptResult, request)) case interceptor.INTERCEPT_FAIL_HTLC_WITH_CODE: interceptorClient.Send(&routerrpc.ForwardHtlcInterceptResponse{ IncomingCircuitKey: request.IncomingCircuitKey, @@ -257,3 +241,27 @@ func (i *LndHtlcInterceptor) constructOnion( return onionBlob.Bytes(), nil } + +func (i *LndHtlcInterceptor) createOnionResponse(interceptResult interceptor.InterceptResult, request *routerrpc.ForwardHtlcInterceptRequest) *routerrpc.ForwardHtlcInterceptResponse { + onionBlob := request.OnionBlob + + if interceptResult.UseLegacyOnionBlob { + var err error + onionBlob, err = i.constructOnion(interceptResult, request.OutgoingExpiry, request.PaymentHash) + if err != nil { + return &routerrpc.ForwardHtlcInterceptResponse{ + IncomingCircuitKey: request.IncomingCircuitKey, + Action: routerrpc.ResolveHoldForwardAction_FAIL, + FailureCode: lnrpc.Failure_TEMPORARY_CHANNEL_FAILURE, + } + } + } + + return &routerrpc.ForwardHtlcInterceptResponse{ + IncomingCircuitKey: request.IncomingCircuitKey, + Action: routerrpc.ResolveHoldForwardAction_RESUME, + OutgoingAmountMsat: interceptResult.AmountMsat, + OutgoingRequestedChanId: uint64(interceptResult.ChannelId), + OnionBlob: onionBlob, + } +}