diff --git a/.github/workflows/integration_tests.yaml b/.github/workflows/integration_tests.yaml index 699cd478..02478d25 100644 --- a/.github/workflows/integration_tests.yaml +++ b/.github/workflows/integration_tests.yaml @@ -137,7 +137,8 @@ jobs: testLsps0GetProtocolVersions, testLsps2GetVersions, testLsps2GetInfo, - testLsps2Buy + testLsps2Buy, + testLsps2HappyFlow ] implementation: [ CLN diff --git a/itest/breez_client.go b/itest/breez_client.go index f117b16b..d0df29b6 100644 --- a/itest/breez_client.go +++ b/itest/breez_client.go @@ -2,10 +2,16 @@ package itest import ( "crypto/sha256" + "encoding/hex" + "encoding/json" "log" + "math/rand" "testing" "github.com/breez/lntest" + "github.com/breez/lspd/lsps0" + "github.com/breez/lspd/lsps0/jsonrpc" + "github.com/breez/lspd/lsps2" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/chaincfg" @@ -39,6 +45,18 @@ type invoice struct { } func GenerateInvoices(n BreezClient, req generateInvoicesRequest) (invoice, invoice) { + return generateInvoices(n, req, lntest.ShortChannelID{ + BlockHeight: 1, + TxIndex: 0, + OutputIndex: 0, + }, lspCltvDelta) +} + +func GenerateLsps2Invoices(n BreezClient, req generateInvoicesRequest, scid string) (invoice, invoice) { + return generateInvoices(n, req, lntest.NewShortChanIDFromString(scid), lspCltvDelta+2) +} + +func generateInvoices(n BreezClient, req generateInvoicesRequest, scid lntest.ShortChannelID, cltvDelta uint16) (invoice, invoice) { preimage, err := GenerateRandomBytes(32) lntest.CheckError(n.Harness().T, err) @@ -47,11 +65,7 @@ func GenerateInvoices(n BreezClient, req generateInvoicesRequest) (invoice, invo Description: &req.description, Preimage: &preimage, }) - outerInvoice := AddHopHint(n, innerInvoice.Bolt11, req.lsp, lntest.ShortChannelID{ - BlockHeight: 1, - TxIndex: 0, - OutputIndex: 0, - }, &req.outerAmountMsat) + outerInvoice := AddHopHint(n, innerInvoice.Bolt11, req.lsp, scid, &req.outerAmountMsat, cltvDelta) inner := invoice{ bolt11: innerInvoice.Bolt11, @@ -76,7 +90,7 @@ func ContainsHopHint(t *testing.T, invoice string) bool { return len(rawInvoice.RouteHints) > 0 } -func AddHopHint(n BreezClient, invoice string, lsp LspNode, chanid lntest.ShortChannelID, amountMsat *uint64) string { +func AddHopHint(n BreezClient, invoice string, lsp LspNode, chanid lntest.ShortChannelID, amountMsat *uint64, cltvDelta uint16) string { rawInvoice, err := zpay32.Decode(invoice, &chaincfg.RegressionNetParams) lntest.CheckError(n.Harness().T, err) @@ -93,7 +107,7 @@ func AddHopHint(n BreezClient, invoice string, lsp LspNode, chanid lntest.ShortC ChannelID: chanid.ToUint64(), FeeBaseMSat: lspBaseFeeMsat, FeeProportionalMillionths: lspFeeRatePpm, - CLTVExpiryDelta: lspCltvDelta, + CLTVExpiryDelta: cltvDelta, }, }) @@ -121,3 +135,67 @@ func AddHopHint(n BreezClient, invoice string, lsp LspNode, chanid lntest.ShortC return newInvoice } + +func Lsps2GetInfo(c BreezClient, l LspNode, req lsps2.GetInfoRequest) lsps2.GetInfoResponse { + req.Version = lsps2.SupportedVersion + r := lsps2RequestResponse(c, l, "lsps2.get_info", req) + var resp lsps2.GetInfoResponse + err := json.Unmarshal(r, &resp) + lntest.CheckError(c.Harness().T, err) + + return resp +} + +func Lsps2Buy(c BreezClient, l LspNode, req lsps2.BuyRequest) lsps2.BuyResponse { + req.Version = lsps2.SupportedVersion + r := lsps2RequestResponse(c, l, "lsps2.buy", req) + var resp lsps2.BuyResponse + err := json.Unmarshal(r, &resp) + lntest.CheckError(c.Harness().T, err) + + return resp +} + +func lsps2RequestResponse(c BreezClient, l LspNode, method string, req interface{}) []byte { + id := RandStringBytes(32) + peerId := hex.EncodeToString(l.NodeId()) + inner, err := json.Marshal(req) + lntest.CheckError(c.Harness().T, err) + outer, err := json.Marshal(&jsonrpc.Request{ + JsonRpc: jsonrpc.Version, + Method: method, + Id: id, + Params: inner, + }) + lntest.CheckError(c.Harness().T, err) + + log.Printf(string(outer)) + c.Node().SendCustomMessage(&lntest.CustomMsgRequest{ + PeerId: peerId, + Type: lsps0.Lsps0MessageType, + Data: outer, + }) + + m := c.ReceiveCustomMessage() + log.Printf(string(m.Data)) + + var resp jsonrpc.Response + err = json.Unmarshal(m.Data, &resp) + lntest.CheckError(c.Harness().T, err) + + if resp.Id != id { + c.Harness().T.Fatalf("Received custom message, but had different id") + } + + return resp.Result +} + +const letterBytes = "abcdefghijklmnopqrstuvwxyz" + +func RandStringBytes(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = letterBytes[rand.Intn(len(letterBytes))] + } + return string(b) +} diff --git a/itest/lspd_node.go b/itest/lspd_node.go index fcbb5ec9..3df749a8 100644 --- a/itest/lspd_node.go +++ b/itest/lspd_node.go @@ -113,6 +113,8 @@ func newLspd(h *lntest.TestHarness, mem *mempoolApi, name string, nodeConfig *co ChannelMinimumFeeMsat: 2000000, AdditionalChannelCapacity: 100000, MaxInactiveDuration: 3888000, + MinPaymentSizeMsat: 600, + MaxPaymentSizeMsat: 4000000000, Lnd: lnd, Cln: cln, } diff --git a/itest/lspd_test.go b/itest/lspd_test.go index a84ab5f9..c0c69101 100644 --- a/itest/lspd_test.go +++ b/itest/lspd_test.go @@ -18,7 +18,13 @@ func TestLspd(t *testing.T) { return } testCases := allTestCases - runTests(t, testCases, "LND-lspd", lndLspFunc, lndClientFunc) + var lndTestCases []*testCase + for _, c := range testCases { + if !c.isLsps2 { + lndTestCases = append(lndTestCases, c) + } + } + runTests(t, lndTestCases, "LND-lspd", lndLspFunc, lndClientFunc) runTests(t, testCases, "CLN-lspd", clnLspFunc, clnClientFunc) } @@ -106,6 +112,7 @@ type testCase struct { name string test func(t *testParams) skipCreateLsp bool + isLsps2 bool timeout time.Duration } @@ -168,19 +175,28 @@ var allTestCases = []*testCase{ test: testOfflineNotificationZeroConfChannel, }, { - name: "testLsps0GetProtocolVersions", - test: testLsps0GetProtocolVersions, + name: "testLsps0GetProtocolVersions", + test: testLsps0GetProtocolVersions, + isLsps2: true, + }, + { + name: "testLsps2GetVersions", + test: testLsps2GetVersions, + isLsps2: true, }, { - name: "testLsps2GetVersions", - test: testLsps2GetVersions, + name: "testLsps2GetInfo", + test: testLsps2GetInfo, + isLsps2: true, }, { - name: "testLsps2GetInfo", - test: testLsps2GetInfo, + name: "testLsps2Buy", + test: testLsps2Buy, + isLsps2: true, }, { - name: "testLsps2Buy", - test: testLsps2Buy, + name: "testLsps2HappyFlow", + test: testLsps2HappyFlow, + isLsps2: true, }, } diff --git a/itest/lsps2_happy_flow_test.go b/itest/lsps2_happy_flow_test.go new file mode 100644 index 00000000..280667d9 --- /dev/null +++ b/itest/lsps2_happy_flow_test.go @@ -0,0 +1,68 @@ +package itest + +import ( + "log" + "time" + + "github.com/breez/lntest" + "github.com/breez/lspd/lsps2" + "github.com/stretchr/testify/assert" +) + +func testLsps2HappyFlow(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, + }) + alice.WaitForChannelReady(channel) + + log.Print("Connecting bob to lspd") + p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode()) + + log.Printf("Calling lsps2.get_info") + info := Lsps2GetInfo(p.BreezClient(), p.Lsp(), lsps2.GetInfoRequest{ + Token: &WorkingToken, + }) + + outerAmountMsat := uint64(2100000) + innerAmountMsat := lsps2CalculateInnerAmountMsat(p.lsp, outerAmountMsat, info.OpeningFeeParamsMenu[0]) + p.BreezClient().SetHtlcAcceptor(innerAmountMsat) + + log.Printf("Calling lsps2.buy") + buyResp := Lsps2Buy(p.BreezClient(), p.Lsp(), lsps2.BuyRequest{ + OpeningFeeParams: *info.OpeningFeeParamsMenu[0], + PaymentSizeMsat: &outerAmountMsat, + }) + + log.Printf("Adding bob's invoices") + description := "Please pay me" + _, outerInvoice := GenerateLsps2Invoices(p.BreezClient(), + generateInvoicesRequest{ + innerAmountMsat: innerAmountMsat, + outerAmountMsat: outerAmountMsat, + description: description, + lsp: p.lsp, + }, + buyResp.JitChannelScid) + + // 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(outerInvoice.bolt11) + bobInvoice := p.BreezClient().Node().GetInvoice(payResp.PaymentHash) + + assert.Equal(p.t, payResp.PaymentPreimage, bobInvoice.PaymentPreimage) + assert.Equal(p.t, innerAmountMsat, bobInvoice.AmountReceivedMsat) + + // Make sure capacity is correct + chans := p.BreezClient().Node().GetChannels() + assert.Equal(p.t, 1, len(chans)) + c := chans[0] + AssertChannelCapacity(p.t, innerAmountMsat, c.CapacityMsat) +} diff --git a/itest/notification_test.go b/itest/notification_test.go index d4c0daf2..10f698c8 100644 --- a/itest/notification_test.go +++ b/itest/notification_test.go @@ -253,7 +253,7 @@ func testOfflineNotificationZeroConfChannel(p *testParams) { } else { id = chans[0].ShortChannelID } - invoiceWithHint = AddHopHint(p.BreezClient(), bobInvoice.Bolt11, p.Lsp(), id, nil) + invoiceWithHint = AddHopHint(p.BreezClient(), bobInvoice.Bolt11, p.Lsp(), id, nil, lspCltvDelta) } log.Printf("Invoice with hint: %s", invoiceWithHint) diff --git a/itest/test_common.go b/itest/test_common.go index 76fe7e0b..3a733384 100644 --- a/itest/test_common.go +++ b/itest/test_common.go @@ -5,10 +5,13 @@ import ( "log" "testing" + "github.com/breez/lspd/lsps2" lspd "github.com/breez/lspd/rpc" "github.com/stretchr/testify/assert" ) +var WorkingToken = "hello" + func GenerateRandomBytes(n int) ([]byte, error) { b := make([]byte, n) _, err := rand.Read(b) @@ -51,4 +54,14 @@ func calculateInnerAmountMsat(lsp LspNode, outerAmountMsat uint64, params *lspd. return outerAmountMsat - fee } +func lsps2CalculateInnerAmountMsat(lsp LspNode, outerAmountMsat uint64, params *lsps2.OpeningFeeParams) uint64 { + fee := (outerAmountMsat*uint64(params.Proportional) + 999_999) / 1_000_000 + if fee < params.MinFeeMsat { + fee = params.MinFeeMsat + } + + log.Printf("outer: %v, fee: %v", outerAmountMsat, fee) + return outerAmountMsat - fee +} + var publicChanAmount uint64 = 1000183