Skip to content

Commit

Permalink
cln: Sanity check max htlc amount msat
Browse files Browse the repository at this point in the history
Added logic to check the maximum htlc limit
set per node for each channel.
Additionally checking maxhtlc would be
more strict than CLN's own limitations.

Also, to ensure that the receivable amout is also taken into account.
  • Loading branch information
YusukeShimizu committed Sep 29, 2023
1 parent 226f607 commit 9c5c35b
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 67 deletions.
64 changes: 56 additions & 8 deletions clightning/clightning.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ type ListPeerChannelsResponse struct {
}

type PeerChannel struct {
PeerId string `json:"peer_id"`
PeerConnected bool `json:"peer_connected"`
State string `json:"state"`
ShortChannelId string `json:"short_channel_id,omitempty"`
Expand All @@ -226,6 +227,36 @@ type PeerChannel struct {
SpendableMsat glightning.Amount `json:"spendable_msat,omitempty"`
}

func (ch *PeerChannel) GetSpendableMsat() uint64 {
if ch.SpendableMsat.MSat() > 0 {
return ch.SpendableMsat.MSat()
} else {
return ch.ToUsMsat.MSat()
}
}

func (ch *PeerChannel) GetReceivableMsat() uint64 {
if ch.ReceivableMsat.MSat() > 0 {
return ch.ReceivableMsat.MSat()
} else {
return ch.TotalMsat.MSat() - ch.ToUsMsat.MSat()
}
}

func (cl *ClightningClient) getMaxHtlcAmtMsat(scid, nodeId string) (uint64, error) {
var htlcMaximumMilliSatoshis uint64
chgs, err := cl.glightning.GetChannel(scid)
if err != nil {
return htlcMaximumMilliSatoshis, nil
}
for _, c := range chgs {
if c.Source == nodeId {
htlcMaximumMilliSatoshis = c.HtlcMaximumMilliSatoshis.MSat()
}
}
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) {
Expand All @@ -240,16 +271,28 @@ func (cl *ClightningClient) SpendableMsat(scid string) (uint64, error) {
if err = cl.checkChannel(ch); err != nil {
return 0, err
}
if ch.SpendableMsat.MSat() > 0 {
return ch.SpendableMsat.MSat(), nil
} else {
return ch.ToUsMsat.MSat(), nil
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
}
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 All @@ -264,11 +307,16 @@ func (cl *ClightningClient) ReceivableMsat(scid string) (uint64, error) {
if err = cl.checkChannel(ch); err != nil {
return 0, err
}
if ch.ReceivableMsat.MSat() > 0 {
return ch.ReceivableMsat.MSat(), nil
} else {
return ch.TotalMsat.MSat() - ch.ToUsMsat.MSat(), nil
maxHtlcAmtMsat, err := cl.getMaxHtlcAmtMsat(scid, ch.PeerId)
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.GetReceivableMsat(), nil
}
return min(maxHtlcAmtMsat, ch.GetReceivableMsat()), nil
}
}
return 0, fmt.Errorf("could not find a channel with scid: %s", scid)
Expand Down
12 changes: 11 additions & 1 deletion lnd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,17 @@ func (l *Client) ReceivableMsat(scid string) (uint64, error) {
if err = l.checkChannel(ch); err != nil {
return 0, err
}
return uint64(ch.RemoteBalance * 1000), nil
maxHtlcAmtMsat, err := l.getMaxHtlcAmtMsat(ch.ChanId, ch.GetRemotePubkey())
if err != nil {
return 0, err
}
receivable := uint64(ch.RemoteBalance * 1000)
// since the max htlc limit is not always set reliably,
// the check is skipped if it is not set.
if maxHtlcAmtMsat == 0 {
return receivable, nil
}
return min(maxHtlcAmtMsat, receivable), nil
}
}
return 0, fmt.Errorf("could not find a channel with scid: %s", scid)
Expand Down
247 changes: 190 additions & 57 deletions test/bitcoin_cln_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -942,69 +942,202 @@ func Test_ClnLnd_Bitcoin_SwapOut(t *testing.T) {

func Test_ClnCln_ExcessiveAmount(t *testing.T) {
IsIntegrationTest(t)

t.Parallel()
require := require.New(t)

bitcoind, lightningds, scid := clnclnSetup(t, uint64(math.Pow10(9)))
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,
},
)
t.Run("excessive", func(t *testing.T) {
require := require.New(t)

bitcoind, lightningds, scid := clnclnSetup(t, uint64(math.Pow10(9)))
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)
}
}()

var channelBalances []uint64
var walletBalances []uint64
for _, lightningd := range lightningds {
b, err := lightningd.GetBtcBalanceSat()
require.NoError(err)
walletBalances = append(walletBalances, b)
params := &testParams{
swapAmt: 2 * 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_OUT,
}
asset := "btc"

b, err = lightningd.GetChannelBalanceSat(scid)
require.NoError(err)
channelBalances = append(channelBalances, b)
}
// Swap out should fail as the swap_amt is to high.
var response map[string]interface{}
err := lightningds[0].Rpc.Request(&clightning.SwapOut{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response)
assert.Error(t, err)

params := &testParams{
swapAmt: 2 * 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_OUT,
}
asset := "btc"
// Swap in should fail as the swap_amt is to high.
err = lightningds[1].Rpc.Request(&clightning.SwapIn{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response)
assert.Error(t, err)
})
t.Run("swapout_maxhtlc", func(t *testing.T) {
t.Parallel()
require := require.New(t)

// Swap out should fail as the swap_amt is to high.
var response map[string]interface{}
err := lightningds[0].Rpc.Request(&clightning.SwapOut{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response)
assert.Error(t, err)
bitcoind, lightningds, scid := clnclnSetup(t, uint64(math.Pow10(9)))
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,
filter: filter,
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] / 2,
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,
}
asset := "btc"

_, err := lightningds[0].SetHtlcMaximumMilliSatoshis(scid, channelBalances[0]*1000/2-1)
assert.NoError(t, err)
// Swap out should fail as the swap_amt is to high.
var response map[string]interface{}
err = lightningds[0].Rpc.Request(&clightning.SwapOut{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response)
assert.Error(t, err)
})
t.Run("swapin_maxhtlc", func(t *testing.T) {
t.Parallel()
require := require.New(t)

bitcoind, lightningds, scid := clnclnSetup(t, uint64(math.Pow10(9)))
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,
filter: filter,
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] / 2,
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,
}
asset := "btc"

_, err := lightningds[0].SetHtlcMaximumMilliSatoshis(scid, channelBalances[0]*1000/2-1)
assert.NoError(t, err)
// Swap in should fail as the swap_amt is to high.
var response map[string]interface{}
err = lightningds[1].Rpc.Request(&clightning.SwapIn{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response)
assert.Error(t, err)
})

// Swap in should fail as the swap_amt is to high.
err = lightningds[1].Rpc.Request(&clightning.SwapIn{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response)
assert.Error(t, err)
}
Loading

0 comments on commit 9c5c35b

Please sign in to comment.