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 all commits
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
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ test-bitcoin-cln: test-bins
'Test_ClnCln_Bitcoin_SwapIn|'\
'Test_ClnLnd_Bitcoin_SwapOut|'\
'Test_ClnLnd_Bitcoin_SwapIn|'\
'Test_ClnCln_ExcessiveAmount)'\
'Test_ClnCln_ExcessiveAmount|'\
'Test_ClnCln_StuckChannels)'\
./test
.PHONY: test-bitoin-cln

Expand Down
77 changes: 70 additions & 7 deletions clightning/clightning.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,13 @@ func (cl *ClightningClient) getMaxHtlcAmtMsat(scid, nodeId string) (uint64, erro
return htlcMaximumMilliSatoshis, nil
}

func min(x, y uint64) uint64 {
if x < y {
return x
}
return y
}

// 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) {
Expand Down Expand Up @@ -288,13 +295,6 @@ func (cl *ClightningClient) SpendableMsat(scid string) (uint64, error) {
return 0, fmt.Errorf("could not find a channel with scid: %s", scid)
}

func min(x, y uint64) uint64 {
if x < y {
return x
}
return y
}

// ReceivableMsat returns an estimate of the total we could receive through the
// channel with given scid.
func (cl *ClightningClient) ReceivableMsat(scid string) (uint64, error) {
Expand Down Expand Up @@ -619,6 +619,69 @@ 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
}
}

preimage, err := lightning.GetPreimage()
if err != nil {
return false, "", fmt.Errorf("GetPreimage() %w", err)
}
paymentHash := preimage.Hash().String()
_, err = cl.glightning.SendPay(
[]glightning.RouteHop{
{
Id: channel.PeerId,
ShortChannelId: channel.ShortChannelId,
AmountMsat: glightning.AmountFromMSat(amountMsat),
// The total expected CLTV.
// The default GetRoute value of 9 is set here.
Delay: 9,
Direction: 0,
},
},
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)
}
failCodeWireIncorrectOrUnknownPaymentDetails := 203
if pe.RpcError.Code != failCodeWireIncorrectOrUnknownPaymentDetails {
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
58 changes: 58 additions & 0 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 @@ -373,6 +374,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
5 changes: 1 addition & 4 deletions policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,7 @@ type Policy struct {
// MinSwapAmountMsat is the minimum swap amount in msat that is needed to
// perform a swap. Below this amount it might be uneconomical to do a swap
// due to the on-chain costs.
// TODO: This can not be set in the policy by now but this is the place
// where this value belongs. Eventually we might want to make this value
// editable as a policy setting.
MinSwapAmountMsat uint64 `json:"min_swap_amount_msat"`
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."`

// AllowNewSwaps can be used to disallow any new swaps. This can be useful
// when we want to upgrade the node and do not want to allow for any new
Expand Down
8 changes: 7 additions & 1 deletion swap/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,6 @@ func (r *PayFeeInvoiceAction) Execute(services *SwapServices, swap *SwapData) Ev
if err != nil {
return swap.HandleError(err)
}

sp, err := ll.SpendableMsat(swap.SwapOutRequest.Scid)
if err != nil {
return swap.HandleError(err)
Expand All @@ -587,6 +586,13 @@ func (r *PayFeeInvoiceAction) Execute(services *SwapServices, swap *SwapData) Ev
if sp <= swap.SwapOutRequest.Amount*1000 {
return swap.HandleError(err)
}
success, failureReason, err := ll.ProbePayment(swap.SwapOutRequest.Scid, swap.SwapOutRequest.Amount*1000)
if err != nil {
return swap.HandleError(err)
}
if !success {
return swap.HandleError(fmt.Errorf("the prepayment probe was unsuccessful: %s", failureReason))
}

swap.OpeningTxFee = msatAmt / 1000

Expand Down
30 changes: 29 additions & 1 deletion swap/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,11 +387,18 @@ func (s *SwapService) SwapOut(peer string, chain string, channelId string, initi
if err != nil {
return nil, err
}

if sp <= amtSat*1000 {
return nil, fmt.Errorf("exceeding spendable amount_msat: %d", sp)
}

success, failureReason, err := s.swapServices.lightning.ProbePayment(channelId, amtSat*1000)
if err != nil {
return nil, err
}
if !success {
return nil, fmt.Errorf("the prepayment probe was unsuccessful: %s", failureReason)
}

swap := newSwapOutSenderFSM(s.swapServices, initiator, peer)
err = s.lockSwap(swap.SwapId.String(), channelId, swap)
if err != nil {
Expand Down Expand Up @@ -531,6 +538,27 @@ func (s *SwapService) OnSwapInRequestReceived(swapId *SwapId, peerId string, mes
return err
}

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.
msgBytes, msgType, err := MarshalPeerswapMessage(&CancelMessage{
SwapId: swapId,
Message: msg,
})
s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType)
return err
}
if !success {
// We want to tell our peer why we can not do this swap.
msgBytes, msgType, err := MarshalPeerswapMessage(&CancelMessage{
SwapId: swapId,
Message: "The prepayment probe was unsuccessful." + failureReason,
})
s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType)
return err
}

swap := newSwapInReceiverFSM(swapId, s.swapServices, peerId)

err = s.lockSwap(swap.SwapId.String(), message.Scid, swap)
Expand Down
1 change: 1 addition & 0 deletions swap/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type LightningClient interface {
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
74 changes: 74 additions & 0 deletions test/bitcoin_cln_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1141,3 +1141,77 @@ func Test_ClnCln_ExcessiveAmount(t *testing.T) {
})

}

// Test_ClnCln_StuckChannels tests that the swap fails if the channel is stuck.
// For more information about stuck channel, please check the link.
// https://github.com/lightning/bolts/issues/728
func Test_ClnCln_StuckChannels(t *testing.T) {
IsIntegrationTest(t)
t.Parallel()

require := require.New(t)
// repro by using the push_msat in the open_channel.
// Assumption that feperkw is 253perkw in reg test.
bitcoind, lightningds, scid := clnclnSetupWithConfig(t, 3794, 3573, []string{
Copy link
Contributor

Choose a reason for hiding this comment

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

Numbers checked. They match 1% reserve 37940 msat and a CommitTx fee of 183172 msat.
This gives the funder 221112 msat for a stuck channel. The configuration leaves 221 sat on the funders side which results in a stuck channel.

"--dev-bitcoind-poll=1",
"--dev-fast-gossip",
"--large-channels",
"--min-capacity-sat=1000",
})

defer func() {
if t.Failed() {
filter := os.Getenv("PEERSWAP_TEST_FILTER")
pprintFail(
tailableProcess{
p: bitcoind.DaemonProcess,
lines: defaultLines,
},
tailableProcess{
p: lightningds[0].DaemonProcess,
filter: filter,
lines: defaultLines,
},
tailableProcess{
p: lightningds[1].DaemonProcess,
lines: defaultLines,
},
)
}
}()

var channelBalances []uint64
var walletBalances []uint64
for _, lightningd := range lightningds {
b, err := lightningd.GetBtcBalanceSat()
require.NoError(err)
walletBalances = append(walletBalances, b)

b, err = lightningd.GetChannelBalanceSat(scid)
require.NoError(err)
channelBalances = append(channelBalances, b)
}

params := &testParams{
swapAmt: channelBalances[0],
scid: scid,
origTakerWallet: walletBalances[0],
origMakerWallet: walletBalances[1],
origTakerBalance: channelBalances[0],
origMakerBalance: channelBalances[1],
takerNode: lightningds[0],
makerNode: lightningds[1],
takerPeerswap: lightningds[0].DaemonProcess,
makerPeerswap: lightningds[1].DaemonProcess,
chainRpc: bitcoind.RpcProxy,
chaind: bitcoind,
confirms: BitcoinConfirms,
csv: BitcoinCsv,
swapType: swap.SWAPTYPE_IN,
}

// Swap in should fail by probing payment as the channel is stuck.
var response map[string]interface{}
err := lightningds[1].Rpc.Request(&clightning.SwapIn{SatAmt: 100, ShortChannelId: params.scid, Asset: "btc"}, &response)
assert.Error(t, err)
}
Loading
Loading