Skip to content

Commit 138a0dc

Browse files
committed
swap: probe payment as sanity check
Replace sanity check with probe payment in SpendableMsat. ProbePayment trying to pay via a route with a random payment hash that the receiver doesn't have the preimage of. The receiver node aren't able to settle the payment. When the probe is successful, the receiver will return a incorrect_or_unknown_payment_details error to the sender. If the result of ProbePayment is assumed to be a payment failure, log the reason as known by ProbePayment
1 parent e77ec03 commit 138a0dc

File tree

6 files changed

+129
-82
lines changed

6 files changed

+129
-82
lines changed

clightning/clightning.go

+57-29
Original file line numberDiff line numberDiff line change
@@ -259,35 +259,6 @@ func (cl *ClightningClient) getMaxHtlcAmtMsat(scid, nodeId string) (uint64, erro
259259
return htlcMaximumMilliSatoshis, nil
260260
}
261261

262-
// SpendableMsat returns an estimate of the total we could send through the
263-
// channel with given scid. Falls back to the owned amount in the channel.
264-
func (cl *ClightningClient) SpendableMsat(scid string) (uint64, error) {
265-
scid = lightning.Scid(scid).ClnStyle()
266-
var res ListPeerChannelsResponse
267-
err := cl.glightning.Request(ListPeerChannelsRequest{}, &res)
268-
if err != nil {
269-
return 0, err
270-
}
271-
for _, ch := range res.Channels {
272-
if ch.ShortChannelId == scid {
273-
if err = cl.checkChannel(ch); err != nil {
274-
return 0, err
275-
}
276-
maxHtlcAmtMsat, err := cl.getMaxHtlcAmtMsat(scid, cl.nodeId)
277-
if err != nil {
278-
return 0, err
279-
}
280-
// since the max htlc limit is not always set reliably,
281-
// the check is skipped if it is not set.
282-
if maxHtlcAmtMsat == 0 {
283-
return ch.GetSpendableMsat(), nil
284-
}
285-
return min(maxHtlcAmtMsat, ch.GetSpendableMsat()), nil
286-
}
287-
}
288-
return 0, fmt.Errorf("could not find a channel with scid: %s", scid)
289-
}
290-
291262
func min(x, y uint64) uint64 {
292263
if x < y {
293264
return x
@@ -619,6 +590,63 @@ func (cl *ClightningClient) GetPeers() []string {
619590
return peerlist
620591
}
621592

593+
// ProbePayment trying to pay via a route with a random payment hash
594+
// that the receiver doesn't have the preimage of.
595+
// The receiver node aren't able to settle the payment.
596+
// When the probe is successful, the receiver will return
597+
// a incorrect_or_unknown_payment_details error to the sender.
598+
func (cl *ClightningClient) ProbePayment(scid string, amountMsat uint64) (bool, string, error) {
599+
var res ListPeerChannelsResponse
600+
err := cl.glightning.Request(ListPeerChannelsRequest{}, &res)
601+
if err != nil {
602+
return false, "", fmt.Errorf("ListPeerChannelsRequest() %w", err)
603+
}
604+
var channel PeerChannel
605+
for _, ch := range res.Channels {
606+
if ch.ShortChannelId == lightning.Scid(scid).ClnStyle() {
607+
if err := cl.checkChannel(ch); err != nil {
608+
return false, "", err
609+
}
610+
channel = ch
611+
}
612+
}
613+
614+
route, err := cl.glightning.GetRoute(channel.PeerId, amountMsat, 1, 0, cl.nodeId, 0, nil, 1)
615+
if err != nil {
616+
return false, "", fmt.Errorf("GetRoute() %w", err)
617+
}
618+
preimage, err := lightning.GetPreimage()
619+
if err != nil {
620+
return false, "", fmt.Errorf("GetPreimage() %w", err)
621+
}
622+
paymentHash := preimage.Hash().String()
623+
_, err = cl.glightning.SendPay(
624+
route,
625+
paymentHash,
626+
"",
627+
amountMsat,
628+
"",
629+
"",
630+
0,
631+
)
632+
if err != nil {
633+
return false, "", fmt.Errorf("SendPay() %w", err)
634+
}
635+
_, err = cl.glightning.WaitSendPay(paymentHash, 0)
636+
if err != nil {
637+
pe, ok := err.(*glightning.PaymentError)
638+
if !ok {
639+
return false, "", fmt.Errorf("WaitSendPay() %w", err)
640+
}
641+
faiCodeWireIncorrectOrUnknownPaymentDetails := 203
642+
if pe.RpcError.Code != faiCodeWireIncorrectOrUnknownPaymentDetails {
643+
log.Debugf("send pay would be failed. reason:%w", err)
644+
return false, pe.Error(), nil
645+
}
646+
}
647+
return true, "", nil
648+
}
649+
622650
type Glightninglogger struct {
623651
plugin *glightning.Plugin
624652
}

lnd/client.go

+58-37
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/hex"
66
"errors"
77
"fmt"
8+
"strings"
89
"sync"
910

1011
"github.com/elementsproject/peerswap/log"
@@ -103,43 +104,6 @@ func min(x, y uint64) uint64 {
103104
return y
104105
}
105106

106-
// SpendableMsat returns an estimate of the total we could send through the
107-
// channel with given scid.
108-
func (l *Client) SpendableMsat(scid string) (uint64, error) {
109-
s := lightning.Scid(scid)
110-
r, err := l.lndClient.ListChannels(context.Background(), &lnrpc.ListChannelsRequest{
111-
ActiveOnly: false,
112-
InactiveOnly: false,
113-
PublicOnly: false,
114-
PrivateOnly: false,
115-
})
116-
if err != nil {
117-
return 0, err
118-
}
119-
for _, ch := range r.Channels {
120-
channelShortId := lnwire.NewShortChanIDFromInt(ch.ChanId)
121-
if channelShortId.String() == s.LndStyle() {
122-
if err = l.checkChannel(ch); err != nil {
123-
return 0, err
124-
}
125-
maxHtlcAmtMsat, err := l.getMaxHtlcAmtMsat(ch.ChanId, l.pubkey)
126-
if err != nil {
127-
return 0, err
128-
}
129-
spendable := (uint64(ch.GetLocalBalance()) -
130-
ch.GetLocalConstraints().GetChanReserveSat()*1000)
131-
// since the max htlc limit is not always set reliably,
132-
// the check is skipped if it is not set.
133-
if maxHtlcAmtMsat == 0 {
134-
return spendable, nil
135-
}
136-
return min(maxHtlcAmtMsat, spendable), nil
137-
138-
}
139-
}
140-
return 0, fmt.Errorf("could not find a channel with scid: %s", scid)
141-
}
142-
143107
// ReceivableMsat returns an estimate of the total we could receive through the
144108
// channel with given scid.
145109
func (l *Client) ReceivableMsat(scid string) (uint64, error) {
@@ -373,6 +337,63 @@ func (l *Client) GetPeers() []string {
373337
return peerlist
374338
}
375339

340+
// ProbePayment trying to pay via a route with a random payment hash
341+
// that the receiver doesn't have the preimage of.
342+
// The receiver node aren't able to settle the payment.
343+
// When the probe is successful, the receiver will return
344+
// a incorrect_or_unknown_payment_details error to the sender.
345+
func (l *Client) ProbePayment(scid string, amountMsat uint64) (bool, string, error) {
346+
chsRes, err := l.lndClient.ListChannels(context.Background(), &lnrpc.ListChannelsRequest{})
347+
if err != nil {
348+
return false, "", fmt.Errorf("ListChannels() %w", err)
349+
}
350+
var channel *lnrpc.Channel
351+
for _, ch := range chsRes.GetChannels() {
352+
channelShortId := lnwire.NewShortChanIDFromInt(ch.ChanId)
353+
if channelShortId.String() == lightning.Scid(scid).LndStyle() {
354+
channel = ch
355+
}
356+
}
357+
if channel.GetChanId() == 0 {
358+
return false, "", fmt.Errorf("could not find a channel with scid: %s", scid)
359+
}
360+
v, err := route.NewVertexFromStr(channel.GetRemotePubkey())
361+
if err != nil {
362+
return false, "", fmt.Errorf("NewVertexFromStr() %w", err)
363+
}
364+
365+
route, err := l.routerClient.BuildRoute(context.Background(), &routerrpc.BuildRouteRequest{
366+
AmtMsat: int64(amountMsat),
367+
FinalCltvDelta: 9,
368+
OutgoingChanId: channel.GetChanId(),
369+
HopPubkeys: [][]byte{v[:]},
370+
})
371+
if err != nil {
372+
return false, "", fmt.Errorf("BuildRoute() %w", err)
373+
}
374+
preimage, err := lightning.GetPreimage()
375+
if err != nil {
376+
return false, "", fmt.Errorf("GetPreimage() %w", err)
377+
}
378+
pHash, err := hex.DecodeString(preimage.Hash().String())
379+
if err != nil {
380+
return false, "", fmt.Errorf("DecodeString() %w", err)
381+
}
382+
383+
res2, err := l.lndClient.SendToRouteSync(context.Background(), &lnrpc.SendToRouteRequest{
384+
PaymentHash: pHash,
385+
Route: route.GetRoute(),
386+
})
387+
if err != nil {
388+
return false, "", fmt.Errorf("SendToRouteSync() %w", err)
389+
}
390+
if !strings.Contains(res2.PaymentError, "IncorrectOrUnknownPaymentDetails") {
391+
log.Debugf("send pay would be failed. reason:%w", res2.PaymentError)
392+
return false, res2.PaymentError, nil
393+
}
394+
return true, "", nil
395+
}
396+
376397
func LndShortChannelIdToCLShortChannelId(lndCI lnwire.ShortChannelID) string {
377398
return fmt.Sprintf("%dx%dx%d", lndCI.BlockHeight, lndCI.TxIndex, lndCI.TxPosition)
378399
}

swap/actions.go

+3-5
Original file line numberDiff line numberDiff line change
@@ -578,14 +578,12 @@ func (r *PayFeeInvoiceAction) Execute(services *SwapServices, swap *SwapData) Ev
578578
if err != nil {
579579
return swap.HandleError(err)
580580
}
581-
582-
sp, err := ll.SpendableMsat(swap.SwapOutRequest.Scid)
581+
success, failureReason, err := ll.ProbePayment(swap.SwapOutRequest.Scid, swap.SwapOutRequest.Amount*1000)
583582
if err != nil {
584583
return swap.HandleError(err)
585584
}
586-
587-
if sp <= swap.SwapOutRequest.Amount*1000 {
588-
return swap.HandleError(err)
585+
if !success {
586+
return swap.HandleError(fmt.Errorf("the prepayment probe was unsuccessful: %s", failureReason))
589587
}
590588

591589
swap.OpeningTxFee = msatAmt / 1000

swap/service.go

+6-10
Original file line numberDiff line numberDiff line change
@@ -383,13 +383,12 @@ func (s *SwapService) SwapOut(peer string, chain string, channelId string, initi
383383
return nil, err
384384
}
385385

386-
sp, err := s.swapServices.lightning.SpendableMsat(channelId)
386+
success, failureReason, err := s.swapServices.lightning.ProbePayment(channelId, amtSat*1000)
387387
if err != nil {
388388
return nil, err
389389
}
390-
391-
if sp <= amtSat*1000 {
392-
return nil, fmt.Errorf("exceeding spendable amount_msat: %d", sp)
390+
if !success {
391+
return nil, fmt.Errorf("the prepayment probe was unsuccessful: %s", failureReason)
393392
}
394393

395394
swap := newSwapOutSenderFSM(s.swapServices, initiator, peer)
@@ -507,7 +506,7 @@ func (s *SwapService) OnSwapInRequestReceived(swapId *SwapId, peerId string, mes
507506
return err
508507
}
509508

510-
sp, err := s.swapServices.lightning.SpendableMsat(message.Scid)
509+
success, failureReason, err := s.swapServices.lightning.ProbePayment(message.Scid, message.Amount*1000)
511510
if err != nil {
512511
msg := fmt.Sprintf("from the %s peer: %s", s.swapServices.lightning.Implementation(), err.Error())
513512
// We want to tell our peer why we can not do this swap.
@@ -518,14 +517,11 @@ func (s *SwapService) OnSwapInRequestReceived(swapId *SwapId, peerId string, mes
518517
s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType)
519518
return err
520519
}
521-
522-
if sp <= message.Amount*1000 {
523-
err = fmt.Errorf("exceeding spendable amount_msat: %d", sp)
524-
msg := fmt.Sprintf("from the %s peer: %s", s.swapServices.lightning.Implementation(), err.Error())
520+
if !success {
525521
// We want to tell our peer why we can not do this swap.
526522
msgBytes, msgType, err := MarshalPeerswapMessage(&CancelMessage{
527523
SwapId: swapId,
528-
Message: msg,
524+
Message: "The prepayment probe was unsuccessful." + failureReason,
529525
})
530526
s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType)
531527
return err

swap/services.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ type LightningClient interface {
4949
RebalancePayment(payreq string, channel string) (preimage string, err error)
5050
CanSpend(amountMsat uint64) error
5151
Implementation() string
52-
SpendableMsat(scid string) (uint64, error)
5352
ReceivableMsat(scid string) (uint64, error)
53+
ProbePayment(scid string, amountMsat uint64) (bool, string, error)
5454
}
5555

5656
type TxWatcher interface {

swap/swap_out_sender_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,10 @@ func (d *dummyLightningClient) PayInvoiceViaChannel(payreq, scid string) (preima
352352
return pi.String(), nil
353353
}
354354

355+
func (d *dummyLightningClient) ProbePayment(scid string, amountMsat uint64) (bool, string, error) {
356+
return true, "", nil
357+
}
358+
355359
type dummyPolicy struct {
356360
isPeerSuspiciousReturn bool
357361
isPeerSuspiciousParam string

0 commit comments

Comments
 (0)