Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

probe payment as sanity check #260

Merged
merged 7 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 57 additions & 29 deletions clightning/clightning.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,35 +259,6 @@ func (cl *ClightningClient) getMaxHtlcAmtMsat(scid, nodeId string) (uint64, erro
return htlcMaximumMilliSatoshis, nil
}

// SpendableMsat returns an estimate of the total we could send through the
// channel with given scid. Falls back to the owned amount in the channel.
func (cl *ClightningClient) SpendableMsat(scid string) (uint64, error) {
scid = lightning.Scid(scid).ClnStyle()
var res ListPeerChannelsResponse
err := cl.glightning.Request(ListPeerChannelsRequest{}, &res)
if err != nil {
return 0, err
}
for _, ch := range res.Channels {
if ch.ShortChannelId == scid {
if err = cl.checkChannel(ch); err != nil {
return 0, err
}
maxHtlcAmtMsat, err := cl.getMaxHtlcAmtMsat(scid, cl.nodeId)
if err != nil {
return 0, err
}
// since the max htlc limit is not always set reliably,
// the check is skipped if it is not set.
if maxHtlcAmtMsat == 0 {
return ch.GetSpendableMsat(), nil
}
return min(maxHtlcAmtMsat, ch.GetSpendableMsat()), nil
}
}
return 0, fmt.Errorf("could not find a channel with scid: %s", scid)
}

func min(x, y uint64) uint64 {
if x < y {
return x
Expand Down Expand Up @@ -619,6 +590,63 @@ func (cl *ClightningClient) GetPeers() []string {
return peerlist
}

// 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.
func (cl *ClightningClient) ProbePayment(scid string, amountMsat uint64) (bool, string, error) {
var res ListPeerChannelsResponse
err := cl.glightning.Request(ListPeerChannelsRequest{}, &res)
if err != nil {
return false, "", fmt.Errorf("ListPeerChannelsRequest() %w", err)
}
var channel PeerChannel
for _, ch := range res.Channels {
if ch.ShortChannelId == lightning.Scid(scid).ClnStyle() {
if err := cl.checkChannel(ch); err != nil {
return false, "", err
}
channel = ch
}
}

route, err := cl.glightning.GetRoute(channel.PeerId, amountMsat, 1, 0, cl.nodeId, 0, nil, 1)
Copy link
Contributor

@nepet nepet Dec 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fail to see the reason in choosing a route that core lightning constructs for us? Should we not construct the route that uses the exact channel for probing that we also use for the payment?

Copy link
Contributor Author

@YusukeShimizu YusukeShimizu Dec 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree with your suggestion.

Like PayInvoiceViaChannel, I have modified to ensure that invoice payments are probed through a direct channel to peers.

if err != nil {
return false, "", fmt.Errorf("GetRoute() %w", err)
}
preimage, err := lightning.GetPreimage()
if err != nil {
return false, "", fmt.Errorf("GetPreimage() %w", err)
}
paymentHash := preimage.Hash().String()
_, err = cl.glightning.SendPay(
route,
paymentHash,
"",
amountMsat,
"",
"",
0,
)
if err != nil {
return false, "", fmt.Errorf("SendPay() %w", err)
}
_, err = cl.glightning.WaitSendPay(paymentHash, 0)
if err != nil {
pe, ok := err.(*glightning.PaymentError)
if !ok {
return false, "", fmt.Errorf("WaitSendPay() %w", err)
}
faiCodeWireIncorrectOrUnknownPaymentDetails := 203
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: typo failCodeWire...

if pe.RpcError.Code != faiCodeWireIncorrectOrUnknownPaymentDetails {
log.Debugf("send pay would be failed. reason:%w", err)
return false, pe.Error(), nil
}
}
return true, "", nil
}

type Glightninglogger struct {
plugin *glightning.Plugin
}
Expand Down
95 changes: 58 additions & 37 deletions lnd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"strings"
"sync"

"github.com/elementsproject/peerswap/log"
Expand Down Expand Up @@ -103,43 +104,6 @@ func min(x, y uint64) uint64 {
return y
}

// SpendableMsat returns an estimate of the total we could send through the
// channel with given scid.
func (l *Client) SpendableMsat(scid string) (uint64, error) {
s := lightning.Scid(scid)
r, err := l.lndClient.ListChannels(context.Background(), &lnrpc.ListChannelsRequest{
ActiveOnly: false,
InactiveOnly: false,
PublicOnly: false,
PrivateOnly: false,
})
if err != nil {
return 0, err
}
for _, ch := range r.Channels {
channelShortId := lnwire.NewShortChanIDFromInt(ch.ChanId)
if channelShortId.String() == s.LndStyle() {
if err = l.checkChannel(ch); err != nil {
return 0, err
}
maxHtlcAmtMsat, err := l.getMaxHtlcAmtMsat(ch.ChanId, l.pubkey)
if err != nil {
return 0, err
}
spendable := (uint64(ch.GetLocalBalance()) -
ch.GetLocalConstraints().GetChanReserveSat()*1000)
// since the max htlc limit is not always set reliably,
// the check is skipped if it is not set.
if maxHtlcAmtMsat == 0 {
return spendable, nil
}
return min(maxHtlcAmtMsat, spendable), nil

}
}
return 0, fmt.Errorf("could not find a channel with scid: %s", scid)
}

// ReceivableMsat returns an estimate of the total we could receive through the
// channel with given scid.
func (l *Client) ReceivableMsat(scid string) (uint64, error) {
Expand Down Expand Up @@ -373,6 +337,63 @@ func (l *Client) GetPeers() []string {
return peerlist
}

// 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.
func (l *Client) ProbePayment(scid string, amountMsat uint64) (bool, string, error) {
chsRes, err := l.lndClient.ListChannels(context.Background(), &lnrpc.ListChannelsRequest{})
if err != nil {
return false, "", fmt.Errorf("ListChannels() %w", err)
}
var channel *lnrpc.Channel
for _, ch := range chsRes.GetChannels() {
channelShortId := lnwire.NewShortChanIDFromInt(ch.ChanId)
if channelShortId.String() == lightning.Scid(scid).LndStyle() {
channel = ch
}
}
if channel.GetChanId() == 0 {
return false, "", fmt.Errorf("could not find a channel with scid: %s", scid)
}
v, err := route.NewVertexFromStr(channel.GetRemotePubkey())
if err != nil {
return false, "", fmt.Errorf("NewVertexFromStr() %w", err)
}

route, err := l.routerClient.BuildRoute(context.Background(), &routerrpc.BuildRouteRequest{
AmtMsat: int64(amountMsat),
FinalCltvDelta: 9,
OutgoingChanId: channel.GetChanId(),
HopPubkeys: [][]byte{v[:]},
})
if err != nil {
return false, "", fmt.Errorf("BuildRoute() %w", err)
}
preimage, err := lightning.GetPreimage()
if err != nil {
return false, "", fmt.Errorf("GetPreimage() %w", err)
}
pHash, err := hex.DecodeString(preimage.Hash().String())
if err != nil {
return false, "", fmt.Errorf("DecodeString() %w", err)
}

res2, err := l.lndClient.SendToRouteSync(context.Background(), &lnrpc.SendToRouteRequest{
PaymentHash: pHash,
Route: route.GetRoute(),
})
if err != nil {
return false, "", fmt.Errorf("SendToRouteSync() %w", err)
}
if !strings.Contains(res2.PaymentError, "IncorrectOrUnknownPaymentDetails") {
log.Debugf("send pay would be failed. reason:%w", res2.PaymentError)
return false, res2.PaymentError, nil
}
return true, "", nil
}

func LndShortChannelIdToCLShortChannelId(lndCI lnwire.ShortChannelID) string {
return fmt.Sprintf("%dx%dx%d", lndCI.BlockHeight, lndCI.TxIndex, lndCI.TxPosition)
}
2 changes: 1 addition & 1 deletion lnd/paymentwatcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ func paymentwatcherNodeSetup(t *testing.T, dir string) (
return nil, nil, nil, nil, fmt.Errorf("Could not create lnd client connection: %v", err)
}

_, err = payer.OpenChannel(payee, uint64(math.Pow10(7)), true, true, true)
_, err = payer.OpenChannel(payee, uint64(math.Pow10(7)), 0, true, true, true)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("Could not open channel: %v", err)
}
Expand Down
8 changes: 3 additions & 5 deletions swap/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,14 +578,12 @@ func (r *PayFeeInvoiceAction) Execute(services *SwapServices, swap *SwapData) Ev
if err != nil {
return swap.HandleError(err)
}

sp, err := ll.SpendableMsat(swap.SwapOutRequest.Scid)
success, failureReason, err := ll.ProbePayment(swap.SwapOutRequest.Scid, swap.SwapOutRequest.Amount*1000)
if err != nil {
return swap.HandleError(err)
}

if sp <= swap.SwapOutRequest.Amount*1000 {
return swap.HandleError(err)
if !success {
return swap.HandleError(fmt.Errorf("the prepayment probe was unsuccessful: %s", failureReason))
}

swap.OpeningTxFee = msatAmt / 1000
Expand Down
16 changes: 6 additions & 10 deletions swap/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,13 +383,12 @@ func (s *SwapService) SwapOut(peer string, chain string, channelId string, initi
return nil, err
}

sp, err := s.swapServices.lightning.SpendableMsat(channelId)
success, failureReason, err := s.swapServices.lightning.ProbePayment(channelId, amtSat*1000)
if err != nil {
return nil, err
}

if sp <= amtSat*1000 {
return nil, fmt.Errorf("exceeding spendable amount_msat: %d", sp)
if !success {
return nil, fmt.Errorf("the prepayment probe was unsuccessful: %s", failureReason)
}

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

sp, err := s.swapServices.lightning.SpendableMsat(message.Scid)
success, failureReason, err := s.swapServices.lightning.ProbePayment(message.Scid, message.Amount*1000)
if err != nil {
msg := fmt.Sprintf("from the %s peer: %s", s.swapServices.lightning.Implementation(), err.Error())
// We want to tell our peer why we can not do this swap.
Expand All @@ -518,14 +517,11 @@ func (s *SwapService) OnSwapInRequestReceived(swapId *SwapId, peerId string, mes
s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType)
return err
}

if sp <= message.Amount*1000 {
err = fmt.Errorf("exceeding spendable amount_msat: %d", sp)
msg := fmt.Sprintf("from the %s peer: %s", s.swapServices.lightning.Implementation(), err.Error())
if !success {
// We want to tell our peer why we can not do this swap.
msgBytes, msgType, err := MarshalPeerswapMessage(&CancelMessage{
SwapId: swapId,
Message: msg,
Message: "The prepayment probe was unsuccessful." + failureReason,
})
s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType)
return err
Expand Down
2 changes: 1 addition & 1 deletion swap/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ type LightningClient interface {
RebalancePayment(payreq string, channel string) (preimage string, err error)
CanSpend(amountMsat uint64) error
Implementation() string
SpendableMsat(scid string) (uint64, error)
ReceivableMsat(scid string) (uint64, error)
ProbePayment(scid string, amountMsat uint64) (bool, string, error)
}

type TxWatcher interface {
Expand Down
4 changes: 4 additions & 0 deletions swap/swap_out_sender_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,10 @@ func (d *dummyLightningClient) PayInvoiceViaChannel(payreq, scid string) (preima
return pi.String(), nil
}

func (d *dummyLightningClient) ProbePayment(scid string, amountMsat uint64) (bool, string, error) {
return true, "", nil
}

type dummyPolicy struct {
isPeerSuspiciousReturn bool
isPeerSuspiciousParam string
Expand Down