From 1558636890cfd77242338b2991064ce5f60ae381 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Fri, 16 Jun 2023 11:00:13 +0200 Subject: [PATCH] notifications: add integration tests --- go.mod | 2 +- itest/breez_client.go | 74 +++++--- itest/cln_breez_client.go | 14 +- itest/cln_lspd_node.go | 24 ++- itest/lnd_breez_client.go | 4 + itest/lnd_lspd_node.go | 24 ++- itest/lspd_node.go | 2 + itest/lspd_test.go | 12 ++ itest/notification_service.go | 59 +++++++ itest/notification_test.go | 309 ++++++++++++++++++++++++++++++++++ 10 files changed, 474 insertions(+), 50 deletions(-) create mode 100644 itest/notification_service.go create mode 100644 itest/notification_test.go diff --git a/go.mod b/go.mod index daa6930..d46b271 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.21 + github.com/breez/lntest v0.0.23 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/itest/breez_client.go b/itest/breez_client.go index 7b96ca2..80f1fa8 100644 --- a/itest/breez_client.go +++ b/itest/breez_client.go @@ -3,6 +3,7 @@ package itest import ( "crypto/sha256" "log" + "testing" "github.com/breez/lntest" "github.com/btcsuite/btcd/btcec/v2" @@ -19,6 +20,7 @@ type BreezClient interface { Start() Stop() error SetHtlcAcceptor(totalMsat uint64) + ResetHtlcAcceptor() } type generateInvoicesRequest struct { @@ -39,24 +41,55 @@ func GenerateInvoices(n BreezClient, req generateInvoicesRequest) (invoice, invo preimage, err := GenerateRandomBytes(32) lntest.CheckError(n.Harness().T, err) - lspNodeId, err := btcec.ParsePubKey(req.lsp.NodeId()) - lntest.CheckError(n.Harness().T, err) - innerInvoice := n.Node().CreateBolt11Invoice(&lntest.CreateInvoiceOptions{ AmountMsat: req.innerAmountMsat, Description: &req.description, Preimage: &preimage, }) - outerInvoiceRaw, err := zpay32.Decode(innerInvoice.Bolt11, &chaincfg.RegressionNetParams) + outerInvoice := AddHopHint(n, innerInvoice.Bolt11, req.lsp, lntest.ShortChannelID{ + BlockHeight: 1, + TxIndex: 0, + OutputIndex: 0, + }, &req.outerAmountMsat) + + inner := invoice{ + bolt11: innerInvoice.Bolt11, + paymentHash: innerInvoice.PaymentHash, + paymentSecret: innerInvoice.PaymentSecret, + paymentPreimage: preimage, + } + outer := invoice{ + bolt11: outerInvoice, + paymentHash: innerInvoice.PaymentHash[:], + paymentSecret: innerInvoice.PaymentSecret, + paymentPreimage: preimage, + } + + return inner, outer +} + +func ContainsHopHint(t *testing.T, invoice string) bool { + rawInvoice, err := zpay32.Decode(invoice, &chaincfg.RegressionNetParams) + lntest.CheckError(t, err) + + return len(rawInvoice.RouteHints) > 0 +} + +func AddHopHint(n BreezClient, invoice string, lsp LspNode, chanid lntest.ShortChannelID, amountMsat *uint64) string { + rawInvoice, err := zpay32.Decode(invoice, &chaincfg.RegressionNetParams) lntest.CheckError(n.Harness().T, err) - milliSat := lnwire.MilliSatoshi(req.outerAmountMsat) - outerInvoiceRaw.MilliSat = &milliSat - fakeChanId := &lnwire.ShortChannelID{BlockHeight: 1, TxIndex: 0, TxPosition: 0} - outerInvoiceRaw.RouteHints = append(outerInvoiceRaw.RouteHints, []zpay32.HopHint{ + if amountMsat != nil { + milliSat := lnwire.MilliSatoshi(*amountMsat) + rawInvoice.MilliSat = &milliSat + } + + lspNodeId, err := btcec.ParsePubKey(lsp.NodeId()) + lntest.CheckError(n.Harness().T, err) + rawInvoice.RouteHints = append(rawInvoice.RouteHints, []zpay32.HopHint{ { NodeID: lspNodeId, - ChannelID: fakeChanId.ToUint64(), + ChannelID: chanid.ToUint64(), FeeBaseMSat: lspBaseFeeMsat, FeeProportionalMillionths: lspFeeRatePpm, CLTVExpiryDelta: lspCltvDelta, @@ -64,12 +97,12 @@ func GenerateInvoices(n BreezClient, req generateInvoicesRequest) (invoice, invo }) log.Printf( - "Encoding outer invoice. privkey: '%x', invoice: '%+v', original bolt11: '%s'", + "Encoding invoice. privkey: '%x', invoice: '%+v', original bolt11: '%s'", n.Node().PrivateKey().Serialize(), - outerInvoiceRaw, - innerInvoice.Bolt11, + rawInvoice, + invoice, ) - outerInvoice, err := outerInvoiceRaw.Encode(zpay32.MessageSigner{ + newInvoice, err := rawInvoice.Encode(zpay32.MessageSigner{ SignCompact: func(msg []byte) ([]byte, error) { hash := sha256.Sum256(msg) sig, err := ecdsa.SignCompact(n.Node().PrivateKey(), hash[:], true) @@ -85,18 +118,5 @@ func GenerateInvoices(n BreezClient, req generateInvoicesRequest) (invoice, invo }) lntest.CheckError(n.Harness().T, err) - inner := invoice{ - bolt11: innerInvoice.Bolt11, - paymentHash: innerInvoice.PaymentHash, - paymentSecret: innerInvoice.PaymentSecret, - paymentPreimage: preimage, - } - outer := invoice{ - bolt11: outerInvoice, - paymentHash: outerInvoiceRaw.PaymentHash[:], - paymentSecret: innerInvoice.PaymentSecret, - paymentPreimage: preimage, - } - - return inner, outer + return newInvoice } diff --git a/itest/cln_breez_client.go b/itest/cln_breez_client.go index 369ba3b..ee2ad66 100644 --- a/itest/cln_breez_client.go +++ b/itest/cln_breez_client.go @@ -123,6 +123,9 @@ func (c *clnBreezClient) Start() { c.startHtlcAcceptor() } +func (c *clnBreezClient) ResetHtlcAcceptor() { + c.htlcAcceptor = nil +} func (c *clnBreezClient) SetHtlcAcceptor(totalMsat uint64) { c.htlcAcceptor = func(htlc *proto.HtlcAccepted) *proto.HtlcResolution { origPayload, err := hex.DecodeString(htlc.Onion.Payload) @@ -230,13 +233,12 @@ func (c *clnBreezClient) startHtlcAcceptor() { } client := proto.NewClnPluginClient(conn) + acceptor, err := client.HtlcStream(ctx) + if err != nil { + log.Printf("%s: client.HtlcStream() error: %v", c.name, err) + break + } for { - acceptor, err := client.HtlcStream(ctx) - if err != nil { - log.Printf("%s: client.HtlcStream() error: %v", c.name, err) - break - } - htlc, err := acceptor.Recv() if err != nil { log.Printf("%s: acceptor.Recv() error: %v", c.name, err) diff --git a/itest/cln_lspd_node.go b/itest/cln_lspd_node.go index f5f94bc..fcfa646 100644 --- a/itest/cln_lspd_node.go +++ b/itest/cln_lspd_node.go @@ -12,6 +12,7 @@ import ( "github.com/breez/lntest" "github.com/breez/lspd/config" + "github.com/breez/lspd/notifications" lspd "github.com/breez/lspd/rpc" "github.com/btcsuite/btcd/btcec/v2" ecies "github.com/ecies/go/v2" @@ -36,10 +37,11 @@ type ClnLspNode struct { } type clnLspNodeRuntime struct { - logFile *os.File - cmd *exec.Cmd - rpc lspd.ChannelOpenerClient - cleanups []*lntest.Cleanup + logFile *os.File + cmd *exec.Cmd + rpc lspd.ChannelOpenerClient + notificationRpc notifications.NotificationsClient + cleanups []*lntest.Cleanup } func NewClnLspdNode(h *lntest.TestHarness, m *lntest.Miner, mem *mempoolApi, name string, nodeConfig *config.NodeConfig) LspNode { @@ -170,11 +172,13 @@ func (c *ClnLspNode) Start() { }) client := lspd.NewChannelOpenerClient(conn) + notif := notifications.NewNotificationsClient(conn) c.runtime = &clnLspNodeRuntime{ - logFile: logFile, - cmd: cmd, - rpc: client, - cleanups: cleanups, + logFile: logFile, + cmd: cmd, + rpc: client, + notificationRpc: notif, + cleanups: cleanups, } } @@ -207,6 +211,10 @@ func (c *ClnLspNode) Rpc() lspd.ChannelOpenerClient { return c.runtime.rpc } +func (c *ClnLspNode) NotificationsRpc() notifications.NotificationsClient { + return c.runtime.notificationRpc +} + func (l *ClnLspNode) NodeId() []byte { return l.lightningNode.NodeId() } diff --git a/itest/lnd_breez_client.go b/itest/lnd_breez_client.go index 66fe7fb..c95febd 100644 --- a/itest/lnd_breez_client.go +++ b/itest/lnd_breez_client.go @@ -78,6 +78,10 @@ func (c *lndBreezClient) Stop() error { return c.node.Stop() } +func (c *lndBreezClient) ResetHtlcAcceptor() { + +} + func (c *lndBreezClient) SetHtlcAcceptor(totalMsat uint64) { // No need for a htlc acceptor in the LND breez client } diff --git a/itest/lnd_lspd_node.go b/itest/lnd_lspd_node.go index 4864815..745b14b 100644 --- a/itest/lnd_lspd_node.go +++ b/itest/lnd_lspd_node.go @@ -14,6 +14,7 @@ import ( "github.com/breez/lntest" "github.com/breez/lspd/config" + "github.com/breez/lspd/notifications" lspd "github.com/breez/lspd/rpc" "github.com/btcsuite/btcd/btcec/v2" ecies "github.com/ecies/go/v2" @@ -32,10 +33,11 @@ type LndLspNode struct { } type lndLspNodeRuntime struct { - logFile *os.File - cmd *exec.Cmd - rpc lspd.ChannelOpenerClient - cleanups []*lntest.Cleanup + logFile *os.File + cmd *exec.Cmd + rpc lspd.ChannelOpenerClient + notificationRpc notifications.NotificationsClient + cleanups []*lntest.Cleanup } func NewLndLspdNode(h *lntest.TestHarness, m *lntest.Miner, mem *mempoolApi, name string, nodeConfig *config.NodeConfig) LspNode { @@ -193,11 +195,13 @@ func (c *LndLspNode) Start() { }) client := lspd.NewChannelOpenerClient(conn) + notifyClient := notifications.NewNotificationsClient(conn) c.runtime = &lndLspNodeRuntime{ - logFile: logFile, - cmd: cmd, - rpc: client, - cleanups: cleanups, + logFile: logFile, + cmd: cmd, + rpc: client, + notificationRpc: notifyClient, + cleanups: cleanups, } } @@ -230,6 +234,10 @@ func (c *LndLspNode) Rpc() lspd.ChannelOpenerClient { return c.runtime.rpc } +func (c *LndLspNode) NotificationsRpc() notifications.NotificationsClient { + return c.runtime.notificationRpc +} + func (l *LndLspNode) NodeId() []byte { return l.lightningNode.NodeId() } diff --git a/itest/lspd_node.go b/itest/lspd_node.go index cd926de..fcbb5ec 100644 --- a/itest/lspd_node.go +++ b/itest/lspd_node.go @@ -15,6 +15,7 @@ import ( "github.com/breez/lntest" "github.com/breez/lspd/config" + "github.com/breez/lspd/notifications" lspd "github.com/breez/lspd/rpc" "github.com/btcsuite/btcd/btcec/v2" "github.com/decred/dcrd/dcrec/secp256k1/v4" @@ -45,6 +46,7 @@ type LspNode interface { PublicKey() *btcec.PublicKey EciesPublicKey() *ecies.PublicKey Rpc() lspd.ChannelOpenerClient + NotificationsRpc() notifications.NotificationsClient NodeId() []byte LightningNode() lntest.LightningNode PostgresBackend() *PostgresContainer diff --git a/itest/lspd_test.go b/itest/lspd_test.go index 4f403de..098d68a 100644 --- a/itest/lspd_test.go +++ b/itest/lspd_test.go @@ -151,4 +151,16 @@ var allTestCases = []*testCase{ name: "testDynamicFeeFlow", test: testDynamicFeeFlow, }, + { + name: "testOfflineNotificationPaymentRegistered", + test: testOfflineNotificationPaymentRegistered, + }, + { + name: "testOfflineNotificationRegularForward", + test: testOfflineNotificationRegularForward, + }, + { + name: "testOfflineNotificationZeroConfChannel", + test: testOfflineNotificationZeroConfChannel, + }, } diff --git a/itest/notification_service.go b/itest/notification_service.go new file mode 100644 index 0000000..85c856a --- /dev/null +++ b/itest/notification_service.go @@ -0,0 +1,59 @@ +package itest + +import ( + "context" + "net" + "net/http" +) + +type PaymentReceivedPayload struct { + Template string `json:"template" binding:"required,eq=payment_received"` + Data struct { + PaymentHash string `json:"payment_hash" binding:"required"` + } `json:"data"` +} + +type TxConfirmedPayload struct { + Template string `json:"template" binding:"required,eq=tx_confirmed"` + Data struct { + TxID string `json:"tx_id" binding:"required"` + } `json:"data"` +} + +type AddressTxsChangedPayload struct { + Template string `json:"template" binding:"required,eq=address_txs_changed"` + Data struct { + Address string `json:"address" binding:"required"` + } `json:"data"` +} + +type notificationDeliveryService struct { + addr string + handleFunc func(resp http.ResponseWriter, req *http.Request) +} + +func newNotificationDeliveryService( + addr string, + handleFunc func(resp http.ResponseWriter, req *http.Request), +) *notificationDeliveryService { + return ¬ificationDeliveryService{ + addr: addr, + handleFunc: handleFunc, + } +} + +func (s *notificationDeliveryService) Start(ctx context.Context) error { + mux := http.NewServeMux() + mux.HandleFunc("/api/v1/notify", s.handleFunc) + lis, err := net.Listen("tcp", s.addr) + if err != nil { + return err + } + + go func() { + <-ctx.Done() + lis.Close() + }() + + return http.Serve(lis, mux) +} diff --git a/itest/notification_test.go b/itest/notification_test.go new file mode 100644 index 0000000..d4c0daf --- /dev/null +++ b/itest/notification_test.go @@ -0,0 +1,309 @@ +package itest + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/breez/lntest" + "github.com/breez/lspd/notifications" + lspd "github.com/breez/lspd/rpc" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/stretchr/testify/assert" +) + +func testOfflineNotificationPaymentRegistered(p *testParams) { + alice := lntest.NewClnNode(p.h, p.m, "Alice") + alice.Start() + alice.Fund(10000000) + p.lsp.LightningNode().Fund(10000000) + + log.Print("Opening channel between Alice and the lsp") + channel := alice.OpenChannel(p.lsp.LightningNode(), &lntest.OpenChannelOptions{ + AmountSat: publicChanAmount, + }) + channelId := alice.WaitForChannelReady(channel) + + log.Printf("Adding bob's invoices") + outerAmountMsat := uint64(2100000) + innerAmountMsat := calculateInnerAmountMsat(p.lsp, outerAmountMsat, nil) + description := "Please pay me" + innerInvoice, outerInvoice := GenerateInvoices(p.BreezClient(), + generateInvoicesRequest{ + innerAmountMsat: innerAmountMsat, + outerAmountMsat: outerAmountMsat, + description: description, + lsp: p.lsp, + }) + + log.Print("Connecting bob to lspd") + p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode()) + + log.Printf("Registering payment with lsp") + RegisterPayment(p.lsp, &lspd.PaymentInformation{ + PaymentHash: innerInvoice.paymentHash, + PaymentSecret: innerInvoice.paymentSecret, + Destination: p.BreezClient().Node().NodeId(), + IncomingAmountMsat: int64(outerAmountMsat), + OutgoingAmountMsat: int64(innerAmountMsat), + }, false) + + // Kill the mobile client + log.Printf("Stopping breez client") + p.BreezClient().Stop() + + port, err := lntest.GetPort() + if err != nil { + assert.FailNow(p.t, "failed to get port for deliveeery service") + } + addr := fmt.Sprintf("127.0.0.1:%d", port) + delivered := make(chan struct{}) + + notify := newNotificationDeliveryService(addr, func(resp http.ResponseWriter, req *http.Request) { + var body PaymentReceivedPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(p.t, err) + + ph := hex.EncodeToString(innerInvoice.paymentHash) + assert.Equal(p.t, ph, body.Data.PaymentHash) + close(delivered) + }) + go notify.Start(p.h.Ctx) + go func() { + <-delivered + log.Printf("Starting breez client again") + p.BreezClient().SetHtlcAcceptor(innerAmountMsat) + p.BreezClient().Start() + p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode()) + }() + + // TODO: Fix race waiting for htlc interceptor. + log.Printf("Waiting %v to allow htlc interceptor to activate.", htlcInterceptorDelay) + <-time.After(htlcInterceptorDelay) + + url := "http://" + addr + "/api/v1/notify" + first := sha256.Sum256([]byte(url)) + second := sha256.Sum256(first[:]) + sig, err := ecdsa.SignCompact(p.BreezClient().Node().PrivateKey(), second[:], true) + assert.NoError(p.t, err) + + p.lsp.NotificationsRpc().SubscribeNotifications(p.h.Ctx, ¬ifications.SubscribeNotificationsRequest{ + Url: url, + Signature: sig, + }) + log.Printf("Alice paying") + route := constructRoute(p.lsp.LightningNode(), p.BreezClient().Node(), channelId, lntest.NewShortChanIDFromString("1x0x0"), outerAmountMsat) + _, err = alice.PayViaRoute(outerAmountMsat, outerInvoice.paymentHash, outerInvoice.paymentSecret, route) + assert.Nil(p.t, err) +} + +func testOfflineNotificationRegularForward(p *testParams) { + alice := lntest.NewClnNode(p.h, p.m, "Alice") + alice.Start() + alice.Fund(10000000) + p.lsp.LightningNode().Fund(10000000) + p.BreezClient().Node().Fund(100000) + + log.Print("Opening channel between Alice and the lsp") + channelAL := alice.OpenChannel(p.lsp.LightningNode(), &lntest.OpenChannelOptions{ + AmountSat: publicChanAmount, + IsPublic: true, + }) + + log.Print("Opening channel between lsp and Breez client") + channelLB := p.lsp.LightningNode().OpenChannel(p.BreezClient().Node(), &lntest.OpenChannelOptions{ + AmountSat: 200000, + IsPublic: false, + }) + + log.Print("Waiting for channel between Alice and the lsp to be ready.") + alice.WaitForChannelReady(channelAL) + log.Print("Waiting for channel between LSP and Bob to be ready.") + p.lsp.LightningNode().WaitForChannelReady(channelLB) + p.BreezClient().Node().WaitForChannelReady(channelLB) + + port, err := lntest.GetPort() + if err != nil { + assert.FailNow(p.t, "failed to get port for deliveeery service") + } + addr := fmt.Sprintf("127.0.0.1:%d", port) + delivered := make(chan struct{}) + + notify := newNotificationDeliveryService(addr, func(resp http.ResponseWriter, req *http.Request) { + var body PaymentReceivedPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(p.t, err) + + close(delivered) + }) + go notify.Start(p.h.Ctx) + go func() { + <-delivered + log.Printf("Notification was delivered. Starting breez client again") + p.BreezClient().Start() + p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode()) + }() + + url := "http://" + addr + "/api/v1/notify" + first := sha256.Sum256([]byte(url)) + second := sha256.Sum256(first[:]) + sig, err := ecdsa.SignCompact(p.BreezClient().Node().PrivateKey(), second[:], true) + assert.NoError(p.t, err) + + p.lsp.NotificationsRpc().SubscribeNotifications(p.h.Ctx, ¬ifications.SubscribeNotificationsRequest{ + Url: url, + Signature: sig, + }) + + <-time.After(time.Second * 2) + log.Printf("Adding bob's invoice") + amountMsat := uint64(2100000) + bobInvoice := p.BreezClient().Node().CreateBolt11Invoice(&lntest.CreateInvoiceOptions{ + AmountMsat: amountMsat, + IncludeHopHints: true, + }) + log.Printf(bobInvoice.Bolt11) + + log.Printf("Bob going offline") + p.BreezClient().Stop() + + // TODO: Fix race waiting for htlc interceptor. + log.Printf("Waiting %v to allow htlc interceptor to activate.", htlcInterceptorDelay) + <-time.After(htlcInterceptorDelay) + + log.Printf("Alice paying") + payResp := alice.Pay(bobInvoice.Bolt11) + invoiceResult := p.BreezClient().Node().GetInvoice(bobInvoice.PaymentHash) + + assert.Equal(p.t, payResp.PaymentPreimage, invoiceResult.PaymentPreimage) + assert.Equal(p.t, amountMsat, invoiceResult.AmountReceivedMsat) +} + +func testOfflineNotificationZeroConfChannel(p *testParams) { + alice := lntest.NewClnNode(p.h, p.m, "Alice") + alice.Start() + alice.Fund(10000000) + p.lsp.LightningNode().Fund(10000000) + + log.Print("Opening channel between Alice and the lsp") + channel := alice.OpenChannel(p.lsp.LightningNode(), &lntest.OpenChannelOptions{ + AmountSat: publicChanAmount, + IsPublic: true, + }) + channelId := alice.WaitForChannelReady(channel) + + log.Printf("Adding bob's invoices") + outerAmountMsat := uint64(2100000) + innerAmountMsat := calculateInnerAmountMsat(p.lsp, outerAmountMsat, nil) + description := "Please pay me" + innerInvoice, outerInvoice := GenerateInvoices(p.BreezClient(), + generateInvoicesRequest{ + innerAmountMsat: innerAmountMsat, + outerAmountMsat: outerAmountMsat, + description: description, + lsp: p.lsp, + }) + + log.Print("Connecting bob to lspd") + p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode()) + p.BreezClient().SetHtlcAcceptor(innerAmountMsat) + + // TODO: Fix race waiting for htlc interceptor. + log.Printf("Waiting %v to allow htlc interceptor to activate.", htlcInterceptorDelay) + <-time.After(htlcInterceptorDelay) + + log.Printf("Registering payment with lsp") + RegisterPayment(p.lsp, &lspd.PaymentInformation{ + PaymentHash: innerInvoice.paymentHash, + PaymentSecret: innerInvoice.paymentSecret, + Destination: p.BreezClient().Node().NodeId(), + IncomingAmountMsat: int64(outerAmountMsat), + OutgoingAmountMsat: int64(innerAmountMsat), + }, false) + + expectedheight := p.Miner().GetBlockHeight() + log.Printf("Alice paying") + route := constructRoute(p.lsp.LightningNode(), p.BreezClient().Node(), channelId, lntest.NewShortChanIDFromString("1x0x0"), outerAmountMsat) + _, err := alice.PayViaRoute(outerAmountMsat, outerInvoice.paymentHash, outerInvoice.paymentSecret, route) + assert.Nil(p.t, err) + + <-time.After(time.Second * 2) + log.Printf("Adding bob's invoice for zero conf payment") + amountMsat := uint64(2100000) + + bobInvoice := p.BreezClient().Node().CreateBolt11Invoice(&lntest.CreateInvoiceOptions{ + AmountMsat: amountMsat, + IncludeHopHints: true, + }) + + 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: %s", invoiceWithHint) + + // Kill the mobile client + log.Printf("Stopping breez client") + p.BreezClient().Stop() + p.BreezClient().ResetHtlcAcceptor() + + port, err := lntest.GetPort() + if err != nil { + assert.FailNow(p.t, "failed to get port for delivery service") + } + addr := fmt.Sprintf("127.0.0.1:%d", port) + delivered := make(chan struct{}) + + notify := newNotificationDeliveryService(addr, func(resp http.ResponseWriter, req *http.Request) { + var body PaymentReceivedPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(p.t, err) + + close(delivered) + }) + go notify.Start(p.h.Ctx) + go func() { + <-delivered + log.Printf("Starting breez client again") + p.BreezClient().Start() + p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode()) + }() + + url := "http://" + addr + "/api/v1/notify" + first := sha256.Sum256([]byte(url)) + second := sha256.Sum256(first[:]) + sig, err := ecdsa.SignCompact(p.BreezClient().Node().PrivateKey(), second[:], true) + assert.NoError(p.t, err) + + p.lsp.NotificationsRpc().SubscribeNotifications(p.h.Ctx, ¬ifications.SubscribeNotificationsRequest{ + Url: url, + Signature: sig, + }) + + log.Printf("Alice paying zero conf invoice") + payResp := alice.Pay(invoiceWithHint) + invoiceResult := p.BreezClient().Node().GetInvoice(bobInvoice.PaymentHash) + + assert.Equal(p.t, payResp.PaymentPreimage, invoiceResult.PaymentPreimage) + assert.Equal(p.t, amountMsat, invoiceResult.AmountReceivedMsat) + + // Make sure we haven't accidentally mined blocks in between. + actualheight := p.Miner().GetBlockHeight() + assert.Equal(p.t, expectedheight, actualheight) +}