Skip to content

Commit

Permalink
fix: duplicate transaction entry in keysend payments (#385)
Browse files Browse the repository at this point in the history
* fix: duplicate transaction entry in keysend payments

* chore: update ldk-node-go dependency

* fix: tests

* fix: remove pay_keysend supported method in breez LNClient

* feat: include fees paid in pay_keysend and multi_pay_keysend response

* chore: add extra multi_keysend test

* chore: add extra assertions in keysend tests
  • Loading branch information
rolznz committed Aug 2, 2024
1 parent cc0f9b8 commit c79e11e
Show file tree
Hide file tree
Showing 17 changed files with 264 additions and 121 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/davrux/echo-logrus/v4 v4.0.3
github.com/elnosh/gonuts v0.1.1-0.20240602162005-49da741613e4
github.com/getAlby/glalby-go v0.0.0-20240621192717-95673c864d59
github.com/getAlby/ldk-node-go v0.0.0-20240730160541-be661a91409b
github.com/getAlby/ldk-node-go v0.0.0-20240801181008-94e3b8403ad3
github.com/go-gormigrate/gormigrate/v2 v2.1.2
github.com/gorilla/sessions v1.3.0
github.com/labstack/echo-contrib v0.17.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwV
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/getAlby/glalby-go v0.0.0-20240621192717-95673c864d59 h1:fSqdXE9uKhLcOOQaLtzN+D8RN3oEcZQkGX5E8PyiKy0=
github.com/getAlby/glalby-go v0.0.0-20240621192717-95673c864d59/go.mod h1:ViyJvjlvv0GCesTJ7mb3fBo4G+/qsujDAFN90xZ7a9U=
github.com/getAlby/ldk-node-go v0.0.0-20240730160541-be661a91409b h1:60D6iN0//asM9NErwW0q3wqHv3gjzGnBRVJh1J2Q5/8=
github.com/getAlby/ldk-node-go v0.0.0-20240730160541-be661a91409b/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg=
github.com/getAlby/ldk-node-go v0.0.0-20240801181008-94e3b8403ad3 h1:0V68vTnGZITuZeGVo1zqYrc4MP6cGyM3maWEZbWyW8k=
github.com/getAlby/ldk-node-go v0.0.0-20240801181008-94e3b8403ad3/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg=
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
Expand Down
10 changes: 6 additions & 4 deletions lnclient/breez/breez.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,9 @@ func (bs *BreezService) SendPaymentSync(ctx context.Context, payReq string) (*ln

}

func (bs *BreezService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) {
extraTlvs := []breez_sdk.TlvEntry{}
func (bs *BreezService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) {
// TODO: re-enable when passing custom preimage is possible
/*extraTlvs := []breez_sdk.TlvEntry{}
for _, record := range custom_records {
decodedValue, err := hex.DecodeString(record.Value)
if err != nil {
Expand All @@ -144,7 +145,8 @@ func (bs *BreezService) SendKeysend(ctx context.Context, amount uint64, destinat
if resp.Payment.Details != nil {
lnDetails, _ = resp.Payment.Details.(breez_sdk.PaymentDetailsLn)
}
return lnDetails.Data.PaymentHash, lnDetails.Data.PaymentPreimage, resp.Payment.FeeMsat, nil
return lnDetails.Data.PaymentHash, lnDetails.Data.PaymentPreimage, resp.Payment.FeeMsat, nil*/
return nil, errors.New("not supported")
}

func (bs *BreezService) GetBalance(ctx context.Context) (balance int64, err error) {
Expand Down Expand Up @@ -481,7 +483,7 @@ func (bs *BreezService) DisconnectPeer(ctx context.Context, peerId string) error
}

func (bs *BreezService) GetSupportedNIP47Methods() []string {
return []string{"pay_invoice", "pay_keysend", "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message"}
return []string{"pay_invoice" /*"pay_keysend",*/, "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message"}
}

func (bs *BreezService) GetSupportedNIP47NotificationTypes() []string {
Expand Down
4 changes: 2 additions & 2 deletions lnclient/cashu/cashu.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ func (cs *CashuService) SendPaymentSync(ctx context.Context, invoice string) (re
}, nil
}

func (cs *CashuService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) {
return "", "", 0, errors.New("keysend not supported")
func (cs *CashuService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) {
return nil, errors.New("keysend not supported")
}

func (cs *CashuService) GetBalance(ctx context.Context) (balance int64, err error) {
Expand Down
10 changes: 6 additions & 4 deletions lnclient/greenlight/greenlight.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,10 @@ func (gs *GreenlightService) SendPaymentSync(ctx context.Context, payReq string)
}, nil
}

func (gs *GreenlightService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) {
func (gs *GreenlightService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) {

extraTlvs := []glalby.TlvEntry{}
// TODO: re-enable when passing custom preimage is possible
/*extraTlvs := []glalby.TlvEntry{}
for _, customRecord := range custom_records {
Expand All @@ -153,7 +154,8 @@ func (gs *GreenlightService) SendKeysend(ctx context.Context, amount uint64, des
// TODO: get payment hash from response
return "", response.PaymentPreimage, 0, nil
return "", response.PaymentPreimage, 0, nil*/
return nil, errors.New("not supported")
}

func (gs *GreenlightService) GetBalance(ctx context.Context) (balance int64, err error) {
Expand Down Expand Up @@ -682,7 +684,7 @@ func (gs *GreenlightService) DisconnectPeer(ctx context.Context, peerId string)
}

func (gs *GreenlightService) GetSupportedNIP47Methods() []string {
return []string{"pay_invoice", "pay_keysend", "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message"}
return []string{"pay_invoice" /*"pay_keysend",*/, "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message"}
}

func (gs *GreenlightService) GetSupportedNIP47NotificationTypes() []string {
Expand Down
44 changes: 15 additions & 29 deletions lnclient/ldk/ldk.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
"035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226@170.75.163.209:9735", // WoS
"02fcc5bfc48e83f06c04483a2985e1c390cb0f35058baa875ad2053858b8e80dbd@35.239.148.251:9735", // Blink
"027100442c3b79f606f80f322d98d499eefcb060599efc5d4ecb00209c2cb54190@3.230.33.224:9735", // c=
"038a9e56512ec98da2b5789761f7af8f280baf98a09282360cd6ff1381b5e889bf@64.23.162.51:9735", // Megalith LSP
"038a9e56512ec98da2b5789761f7af8f280baf98a09282360cd6ff1381b5e889bf@64.23.162.51:9735", // Megalith LSP
}
logger.Logger.Info("Connecting to some peers to retrieve P2P gossip data")
for _, peer := range peers {
Expand Down Expand Up @@ -515,14 +515,14 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnc
}, nil
}

func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) {
func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) {
paymentStart := time.Now()
customTlvs := []ldk_node.TlvEntry{}

for _, customRecord := range custom_records {
decodedValue, err := hex.DecodeString(customRecord.Value)
if err != nil {
return "", "", 0, err
return nil, err
}
customTlvs = append(customTlvs, ldk_node.TlvEntry{
Type: customRecord.Type,
Expand All @@ -533,12 +533,13 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destinatio
ldkEventSubscription := ls.ldkEventBroadcaster.Subscribe()
defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription)

paymentHash, err = ls.node.SpontaneousPayment().Send(amount, destination, customTlvs)
paymentHash, err := ls.node.SpontaneousPayment().Send(amount, destination, customTlvs, &preimage)
if err != nil {
logger.Logger.WithError(err).Error("Keysend failed")
return paymentHash, "", 0, err
return nil, err
}

fee := uint64(0)
paid := false
for start := time.Now(); time.Since(start) < time.Second*60; {
event := <-ldkEventSubscription

Expand All @@ -547,25 +548,8 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destinatio

if isEventPaymentSuccessfulEvent && eventPaymentSuccessful.PaymentHash == paymentHash {
logger.Logger.Info("Got payment success event")
payment := ls.node.Payment(paymentHash)
if payment == nil {
logger.Logger.Errorf("Couldn't find payment by payment hash: %v", paymentHash)
return paymentHash, "", 0, errors.New("Payment not found")
}

spontaneousPaymentKind, ok := payment.Kind.(ldk_node.PaymentKindSpontaneous)

if !ok {
logger.Logger.WithFields(logrus.Fields{
"payment": payment,
}).Error("Payment is not a spontaneous kind")
}

if spontaneousPaymentKind.Preimage == nil {
logger.Logger.Errorf("No payment preimage for payment hash: %v", paymentHash)
return paymentHash, "", 0, errors.New("Payment preimage not found")
}
preimage = *spontaneousPaymentKind.Preimage
paid = true

if eventPaymentSuccessful.FeePaidMsat != nil {
fee = *eventPaymentSuccessful.FeePaidMsat
Expand All @@ -581,21 +565,23 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destinatio
"reason": failureReasonMessage,
}).Error("Received payment failed event")

return paymentHash, "", 0, fmt.Errorf("payment failed event: %s", failureReasonMessage)
return nil, fmt.Errorf("payment failed event: %s", failureReasonMessage)
}
}
if preimage == "" {
if !paid {
logger.Logger.WithFields(logrus.Fields{
"paymentHash": paymentHash,
"payment_hash": paymentHash,
}).Warn("Timed out waiting for keysend to be sent")
return paymentHash, "", 0, lnclient.NewTimeoutError()
return nil, lnclient.NewTimeoutError()
}

logger.Logger.WithFields(logrus.Fields{
"duration": time.Since(paymentStart).Milliseconds(),
"fee": fee,
}).Info("Successful keysend payment")
return paymentHash, preimage, fee, nil
return &lnclient.PayKeysendResponse{
Fee: fee,
}, nil
}

func (ls *LDKService) GetBalance(ctx context.Context) (balance int64, err error) {
Expand Down
44 changes: 14 additions & 30 deletions lnclient/lnd/lnd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package lnd

import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
Expand Down Expand Up @@ -300,37 +299,29 @@ func (svc *LNDService) SendPaymentSync(ctx context.Context, payReq string) (*lnc
}, nil
}

func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) {
func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) {
destBytes, err := hex.DecodeString(destination)
if err != nil {
return "", "", 0, err
return nil, err
}
var preImageBytes []byte

preImageBytes, err = makePreimageHex()
preimage = hex.EncodeToString(preImageBytes)

preImageBytes, err := hex.DecodeString(preimage)
if err != nil || len(preImageBytes) != 32 {
logger.Logger.WithFields(logrus.Fields{
"amount": amount,
"destination": destination,
"preimage": preimage,
"customRecords": custom_records,
"error": err,
}).Errorf("Invalid preimage")
return "", "", 0, err
"preimage": preimage,
}).WithError(err).Error("Invalid preimage")
return nil, err
}

paymentHash256 := sha256.New()
paymentHash256.Write(preImageBytes)
paymentHashBytes := paymentHash256.Sum(nil)
paymentHash = hex.EncodeToString(paymentHashBytes)
paymentHash := hex.EncodeToString(paymentHashBytes)

destCustomRecords := map[uint64][]byte{}
for _, record := range custom_records {
decodedValue, err := hex.DecodeString(record.Value)
if err != nil {
return "", "", 0, err
return nil, err
}
destCustomRecords[record.Type] = decodedValue
}
Expand All @@ -354,7 +345,7 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destinati
"customRecords": custom_records,
"error": err,
}).Errorf("Failed to send keysend payment")
return paymentHash, "", 0, err
return nil, err
}
if resp.PaymentError != "" {
logger.Logger.WithFields(logrus.Fields{
Expand All @@ -365,7 +356,7 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destinati
"customRecords": custom_records,
"paymentError": resp.PaymentError,
}).Errorf("Keysend payment has payment error")
return paymentHash, "", 0, errors.New(resp.PaymentError)
return nil, errors.New(resp.PaymentError)
}
respPreimage := hex.EncodeToString(resp.PaymentPreimage)
if respPreimage != preimage {
Expand All @@ -377,7 +368,7 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destinati
"customRecords": custom_records,
"paymentError": resp.PaymentError,
}).Errorf("Preimage in keysend response does not match")
return paymentHash, "", 0, errors.New("preimage in keysend response does not match")
return nil, errors.New("preimage in keysend response does not match")
}
logger.Logger.WithFields(logrus.Fields{
"amount": amount,
Expand All @@ -388,16 +379,9 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destinati
"respPreimage": respPreimage,
}).Info("Keysend payment successful")

return paymentHash, respPreimage, uint64(resp.PaymentRoute.TotalFeesMsat), nil
}

func makePreimageHex() ([]byte, error) {
bytes := make([]byte, 32) // 32 bytes * 8 bits/byte = 256 bits
_, err := rand.Read(bytes)
if err != nil {
return nil, err
}
return bytes, nil
return &lnclient.PayKeysendResponse{
Fee: uint64(resp.PaymentRoute.TotalFeesMsat),
}, nil
}

func NewLNDService(ctx context.Context, eventPublisher events.EventPublisher, lndAddress, lndCertHex, lndMacaroonHex string) (result lnclient.LNClient, err error) {
Expand Down
6 changes: 5 additions & 1 deletion lnclient/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type NodeConnectionInfo struct {

type LNClient interface {
SendPaymentSync(ctx context.Context, payReq string) (*PayInvoiceResponse, error)
SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []TLVRecord) (paymentHash string, preimage string, fee uint64, err error)
SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []TLVRecord, preimage string) (*PayKeysendResponse, error)
GetBalance(ctx context.Context) (balance int64, err error)
GetPubkey() string
GetInfo(ctx context.Context) (info *NodeInfo, err error)
Expand Down Expand Up @@ -157,6 +157,10 @@ type PayInvoiceResponse struct {
Fee uint64 `json:"fee"`
}

type PayKeysendResponse struct {
Fee uint64 `json:"fee"`
}

type BalancesResponse struct {
Onchain OnchainBalanceResponse `json:"onchain"`
Lightning LightningBalanceResponse `json:"lightning"`
Expand Down
4 changes: 2 additions & 2 deletions lnclient/phoenixd/phoenixd.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,8 +431,8 @@ func (svc *PhoenixService) SendPaymentSync(ctx context.Context, payReq string) (
}, nil
}

func (svc *PhoenixService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) {
return "", "", 0, errors.New("not implemented")
func (svc *PhoenixService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) {
return nil, errors.New("not implemented")
}

func (svc *PhoenixService) RedeemOnchainFunds(ctx context.Context, toAddress string) (txId string, err error) {
Expand Down
7 changes: 7 additions & 0 deletions nip47/controllers/multi_pay_invoice_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,13 @@ func TestHandleMultiPayInvoiceEvent_LNClient_OnePaymentFailed(t *testing.T) {
HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)

assert.Equal(t, 2, len(responses))
assert.Equal(t, 2, len(dTags))
// we can't guarantee which request was processed first
// so swap them if they are back to front
if responses[0].Result == nil {
responses[0], responses[1] = responses[1], responses[0]
dTags[0], dTags[1] = dTags[1], dTags[0]
}

assert.Equal(t, "320c2c5a1492ccfd5bc7aa4ad9b657d6aaec3cfcc0d1d98413a29af4ac772ccf", dTags[0].GetFirst([]string{"d"}).Value())
assert.Equal(t, "123preimage", responses[0].Result.(payResponse).Preimage)
Expand Down
Loading

0 comments on commit c79e11e

Please sign in to comment.