Skip to content

Commit a42d611

Browse files
authored
Merge pull request #260 from YusukeShimizu/probe-payment
probe payment as sanity check
2 parents a729a02 + 52e2ff3 commit a42d611

15 files changed

+319
-31
lines changed

Makefile

+2-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ test-bitcoin-cln: test-bins
9090
'Test_ClnCln_Bitcoin_SwapIn|'\
9191
'Test_ClnLnd_Bitcoin_SwapOut|'\
9292
'Test_ClnLnd_Bitcoin_SwapIn|'\
93-
'Test_ClnCln_ExcessiveAmount)'\
93+
'Test_ClnCln_ExcessiveAmount|'\
94+
'Test_ClnCln_StuckChannels)'\
9495
./test
9596
.PHONY: test-bitoin-cln
9697

clightning/clightning.go

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

262+
func min(x, y uint64) uint64 {
263+
if x < y {
264+
return x
265+
}
266+
return y
267+
}
268+
262269
// SpendableMsat returns an estimate of the total we could send through the
263270
// channel with given scid. Falls back to the owned amount in the channel.
264271
func (cl *ClightningClient) SpendableMsat(scid string) (uint64, error) {
@@ -288,13 +295,6 @@ func (cl *ClightningClient) SpendableMsat(scid string) (uint64, error) {
288295
return 0, fmt.Errorf("could not find a channel with scid: %s", scid)
289296
}
290297

291-
func min(x, y uint64) uint64 {
292-
if x < y {
293-
return x
294-
}
295-
return y
296-
}
297-
298298
// ReceivableMsat returns an estimate of the total we could receive through the
299299
// channel with given scid.
300300
func (cl *ClightningClient) ReceivableMsat(scid string) (uint64, error) {
@@ -619,6 +619,69 @@ func (cl *ClightningClient) GetPeers() []string {
619619
return peerlist
620620
}
621621

622+
// ProbePayment trying to pay via a route with a random payment hash
623+
// that the receiver doesn't have the preimage of.
624+
// The receiver node aren't able to settle the payment.
625+
// When the probe is successful, the receiver will return
626+
// a incorrect_or_unknown_payment_details error to the sender.
627+
func (cl *ClightningClient) ProbePayment(scid string, amountMsat uint64) (bool, string, error) {
628+
var res ListPeerChannelsResponse
629+
err := cl.glightning.Request(ListPeerChannelsRequest{}, &res)
630+
if err != nil {
631+
return false, "", fmt.Errorf("ListPeerChannelsRequest() %w", err)
632+
}
633+
var channel PeerChannel
634+
for _, ch := range res.Channels {
635+
if ch.ShortChannelId == lightning.Scid(scid).ClnStyle() {
636+
if err := cl.checkChannel(ch); err != nil {
637+
return false, "", err
638+
}
639+
channel = ch
640+
}
641+
}
642+
643+
preimage, err := lightning.GetPreimage()
644+
if err != nil {
645+
return false, "", fmt.Errorf("GetPreimage() %w", err)
646+
}
647+
paymentHash := preimage.Hash().String()
648+
_, err = cl.glightning.SendPay(
649+
[]glightning.RouteHop{
650+
{
651+
Id: channel.PeerId,
652+
ShortChannelId: channel.ShortChannelId,
653+
AmountMsat: glightning.AmountFromMSat(amountMsat),
654+
// The total expected CLTV.
655+
// The default GetRoute value of 9 is set here.
656+
Delay: 9,
657+
Direction: 0,
658+
},
659+
},
660+
paymentHash,
661+
"",
662+
amountMsat,
663+
"",
664+
"",
665+
0,
666+
)
667+
if err != nil {
668+
return false, "", fmt.Errorf("SendPay() %w", err)
669+
}
670+
_, err = cl.glightning.WaitSendPay(paymentHash, 0)
671+
if err != nil {
672+
pe, ok := err.(*glightning.PaymentError)
673+
if !ok {
674+
return false, "", fmt.Errorf("WaitSendPay() %w", err)
675+
}
676+
failCodeWireIncorrectOrUnknownPaymentDetails := 203
677+
if pe.RpcError.Code != failCodeWireIncorrectOrUnknownPaymentDetails {
678+
log.Debugf("send pay would be failed. reason:%w", err)
679+
return false, pe.Error(), nil
680+
}
681+
}
682+
return true, "", nil
683+
}
684+
622685
type Glightninglogger struct {
623686
plugin *glightning.Plugin
624687
}

lnd/client.go

+58
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"
@@ -373,6 +374,63 @@ func (l *Client) GetPeers() []string {
373374
return peerlist
374375
}
375376

377+
// ProbePayment trying to pay via a route with a random payment hash
378+
// that the receiver doesn't have the preimage of.
379+
// The receiver node aren't able to settle the payment.
380+
// When the probe is successful, the receiver will return
381+
// a incorrect_or_unknown_payment_details error to the sender.
382+
func (l *Client) ProbePayment(scid string, amountMsat uint64) (bool, string, error) {
383+
chsRes, err := l.lndClient.ListChannels(context.Background(), &lnrpc.ListChannelsRequest{})
384+
if err != nil {
385+
return false, "", fmt.Errorf("ListChannels() %w", err)
386+
}
387+
var channel *lnrpc.Channel
388+
for _, ch := range chsRes.GetChannels() {
389+
channelShortId := lnwire.NewShortChanIDFromInt(ch.ChanId)
390+
if channelShortId.String() == lightning.Scid(scid).LndStyle() {
391+
channel = ch
392+
}
393+
}
394+
if channel.GetChanId() == 0 {
395+
return false, "", fmt.Errorf("could not find a channel with scid: %s", scid)
396+
}
397+
v, err := route.NewVertexFromStr(channel.GetRemotePubkey())
398+
if err != nil {
399+
return false, "", fmt.Errorf("NewVertexFromStr() %w", err)
400+
}
401+
402+
route, err := l.routerClient.BuildRoute(context.Background(), &routerrpc.BuildRouteRequest{
403+
AmtMsat: int64(amountMsat),
404+
FinalCltvDelta: 9,
405+
OutgoingChanId: channel.GetChanId(),
406+
HopPubkeys: [][]byte{v[:]},
407+
})
408+
if err != nil {
409+
return false, "", fmt.Errorf("BuildRoute() %w", err)
410+
}
411+
preimage, err := lightning.GetPreimage()
412+
if err != nil {
413+
return false, "", fmt.Errorf("GetPreimage() %w", err)
414+
}
415+
pHash, err := hex.DecodeString(preimage.Hash().String())
416+
if err != nil {
417+
return false, "", fmt.Errorf("DecodeString() %w", err)
418+
}
419+
420+
res2, err := l.lndClient.SendToRouteSync(context.Background(), &lnrpc.SendToRouteRequest{
421+
PaymentHash: pHash,
422+
Route: route.GetRoute(),
423+
})
424+
if err != nil {
425+
return false, "", fmt.Errorf("SendToRouteSync() %w", err)
426+
}
427+
if !strings.Contains(res2.PaymentError, "IncorrectOrUnknownPaymentDetails") {
428+
log.Debugf("send pay would be failed. reason:%w", res2.PaymentError)
429+
return false, res2.PaymentError, nil
430+
}
431+
return true, "", nil
432+
}
433+
376434
func LndShortChannelIdToCLShortChannelId(lndCI lnwire.ShortChannelID) string {
377435
return fmt.Sprintf("%dx%dx%d", lndCI.BlockHeight, lndCI.TxIndex, lndCI.TxPosition)
378436
}

lnd/paymentwatcher_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ func paymentwatcherNodeSetup(t *testing.T, dir string) (
262262
return nil, nil, nil, nil, fmt.Errorf("Could not create lnd client connection: %v", err)
263263
}
264264

265-
_, err = payer.OpenChannel(payee, uint64(math.Pow10(7)), true, true, true)
265+
_, err = payer.OpenChannel(payee, uint64(math.Pow10(7)), 0, true, true, true)
266266
if err != nil {
267267
return nil, nil, nil, nil, fmt.Errorf("Could not open channel: %v", err)
268268
}

policy/policy.go

+1-4
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,7 @@ type Policy struct {
7878
// MinSwapAmountMsat is the minimum swap amount in msat that is needed to
7979
// perform a swap. Below this amount it might be uneconomical to do a swap
8080
// due to the on-chain costs.
81-
// TODO: This can not be set in the policy by now but this is the place
82-
// where this value belongs. Eventually we might want to make this value
83-
// editable as a policy setting.
84-
MinSwapAmountMsat uint64 `json:"min_swap_amount_msat"`
81+
MinSwapAmountMsat uint64 `json:"min_swap_amount_msat" long:"min_swap_amount_msat" description:"The minimum amount in msat that is needed to perform a swap."`
8582

8683
// AllowNewSwaps can be used to disallow any new swaps. This can be useful
8784
// when we want to upgrade the node and do not want to allow for any new

swap/actions.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,6 @@ func (r *PayFeeInvoiceAction) Execute(services *SwapServices, swap *SwapData) Ev
578578
if err != nil {
579579
return swap.HandleError(err)
580580
}
581-
582581
sp, err := ll.SpendableMsat(swap.SwapOutRequest.Scid)
583582
if err != nil {
584583
return swap.HandleError(err)
@@ -587,6 +586,13 @@ func (r *PayFeeInvoiceAction) Execute(services *SwapServices, swap *SwapData) Ev
587586
if sp <= swap.SwapOutRequest.Amount*1000 {
588587
return swap.HandleError(err)
589588
}
589+
success, failureReason, err := ll.ProbePayment(swap.SwapOutRequest.Scid, swap.SwapOutRequest.Amount*1000)
590+
if err != nil {
591+
return swap.HandleError(err)
592+
}
593+
if !success {
594+
return swap.HandleError(fmt.Errorf("the prepayment probe was unsuccessful: %s", failureReason))
595+
}
590596

591597
swap.OpeningTxFee = msatAmt / 1000
592598

swap/service.go

+29-1
Original file line numberDiff line numberDiff line change
@@ -387,11 +387,18 @@ func (s *SwapService) SwapOut(peer string, chain string, channelId string, initi
387387
if err != nil {
388388
return nil, err
389389
}
390-
391390
if sp <= amtSat*1000 {
392391
return nil, fmt.Errorf("exceeding spendable amount_msat: %d", sp)
393392
}
394393

394+
success, failureReason, err := s.swapServices.lightning.ProbePayment(channelId, amtSat*1000)
395+
if err != nil {
396+
return nil, err
397+
}
398+
if !success {
399+
return nil, fmt.Errorf("the prepayment probe was unsuccessful: %s", failureReason)
400+
}
401+
395402
swap := newSwapOutSenderFSM(s.swapServices, initiator, peer)
396403
err = s.lockSwap(swap.SwapId.String(), channelId, swap)
397404
if err != nil {
@@ -531,6 +538,27 @@ func (s *SwapService) OnSwapInRequestReceived(swapId *SwapId, peerId string, mes
531538
return err
532539
}
533540

541+
success, failureReason, err := s.swapServices.lightning.ProbePayment(message.Scid, message.Amount*1000)
542+
if err != nil {
543+
msg := fmt.Sprintf("from the %s peer: %s", s.swapServices.lightning.Implementation(), err.Error())
544+
// We want to tell our peer why we can not do this swap.
545+
msgBytes, msgType, err := MarshalPeerswapMessage(&CancelMessage{
546+
SwapId: swapId,
547+
Message: msg,
548+
})
549+
s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType)
550+
return err
551+
}
552+
if !success {
553+
// We want to tell our peer why we can not do this swap.
554+
msgBytes, msgType, err := MarshalPeerswapMessage(&CancelMessage{
555+
SwapId: swapId,
556+
Message: "The prepayment probe was unsuccessful." + failureReason,
557+
})
558+
s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType)
559+
return err
560+
}
561+
534562
swap := newSwapInReceiverFSM(swapId, s.swapServices, peerId)
535563

536564
err = s.lockSwap(swap.SwapId.String(), message.Scid, swap)

swap/services.go

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type LightningClient interface {
5151
Implementation() string
5252
SpendableMsat(scid string) (uint64, error)
5353
ReceivableMsat(scid string) (uint64, error)
54+
ProbePayment(scid string, amountMsat uint64) (bool, string, error)
5455
}
5556

5657
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

test/bitcoin_cln_test.go

+74
Original file line numberDiff line numberDiff line change
@@ -1141,3 +1141,77 @@ func Test_ClnCln_ExcessiveAmount(t *testing.T) {
11411141
})
11421142

11431143
}
1144+
1145+
// Test_ClnCln_StuckChannels tests that the swap fails if the channel is stuck.
1146+
// For more information about stuck channel, please check the link.
1147+
// https://github.com/lightning/bolts/issues/728
1148+
func Test_ClnCln_StuckChannels(t *testing.T) {
1149+
IsIntegrationTest(t)
1150+
t.Parallel()
1151+
1152+
require := require.New(t)
1153+
// repro by using the push_msat in the open_channel.
1154+
// Assumption that feperkw is 253perkw in reg test.
1155+
bitcoind, lightningds, scid := clnclnSetupWithConfig(t, 3794, 3573, []string{
1156+
"--dev-bitcoind-poll=1",
1157+
"--dev-fast-gossip",
1158+
"--large-channels",
1159+
"--min-capacity-sat=1000",
1160+
})
1161+
1162+
defer func() {
1163+
if t.Failed() {
1164+
filter := os.Getenv("PEERSWAP_TEST_FILTER")
1165+
pprintFail(
1166+
tailableProcess{
1167+
p: bitcoind.DaemonProcess,
1168+
lines: defaultLines,
1169+
},
1170+
tailableProcess{
1171+
p: lightningds[0].DaemonProcess,
1172+
filter: filter,
1173+
lines: defaultLines,
1174+
},
1175+
tailableProcess{
1176+
p: lightningds[1].DaemonProcess,
1177+
lines: defaultLines,
1178+
},
1179+
)
1180+
}
1181+
}()
1182+
1183+
var channelBalances []uint64
1184+
var walletBalances []uint64
1185+
for _, lightningd := range lightningds {
1186+
b, err := lightningd.GetBtcBalanceSat()
1187+
require.NoError(err)
1188+
walletBalances = append(walletBalances, b)
1189+
1190+
b, err = lightningd.GetChannelBalanceSat(scid)
1191+
require.NoError(err)
1192+
channelBalances = append(channelBalances, b)
1193+
}
1194+
1195+
params := &testParams{
1196+
swapAmt: channelBalances[0],
1197+
scid: scid,
1198+
origTakerWallet: walletBalances[0],
1199+
origMakerWallet: walletBalances[1],
1200+
origTakerBalance: channelBalances[0],
1201+
origMakerBalance: channelBalances[1],
1202+
takerNode: lightningds[0],
1203+
makerNode: lightningds[1],
1204+
takerPeerswap: lightningds[0].DaemonProcess,
1205+
makerPeerswap: lightningds[1].DaemonProcess,
1206+
chainRpc: bitcoind.RpcProxy,
1207+
chaind: bitcoind,
1208+
confirms: BitcoinConfirms,
1209+
csv: BitcoinCsv,
1210+
swapType: swap.SWAPTYPE_IN,
1211+
}
1212+
1213+
// Swap in should fail by probing payment as the channel is stuck.
1214+
var response map[string]interface{}
1215+
err := lightningds[1].Rpc.Request(&clightning.SwapIn{SatAmt: 100, ShortChannelId: params.scid, Asset: "btc"}, &response)
1216+
assert.Error(t, err)
1217+
}

0 commit comments

Comments
 (0)