Consideration of the unsettled balance

We add the amounts of pending htlcs to the local and remote balance of
a channel. Incoming htlcs are added to the remote balance and outgoing
htlcs to the local balance.
This commit is contained in:
feelancer21 2024-05-28 23:47:12 +02:00
parent 2b103c8d57
commit 2499f743e5
No known key found for this signature in database
GPG key ID: 1F7071EE8449729C
3 changed files with 135 additions and 45 deletions

View file

@ -5,6 +5,7 @@ import grpc
import sys
import re
import time
from types import SimpleNamespace
from .grpc_generated import lightning_pb2_grpc as lnrpc, lightning_pb2 as ln
from .grpc_generated import router_pb2_grpc as routerrpc, router_pb2 as router
@ -19,6 +20,81 @@ def debug(message):
sys.stderr.write(message + "\n")
class ChannelMetrics(SimpleNamespace):
local_balance_settled: int = 0
local_balance_unsettled: int = 0
remote_balance_settled: int = 0
remote_balance_unsettled: int = 0
def local_balance_total(self):
return self.local_balance_settled + self.local_balance_unsettled
def remote_balance_total(self):
return self.remote_balance_settled + self.remote_balance_unsettled
class PeerMetrics(SimpleNamespace):
channels_active: int = 0
channels_inactive: int = 0
local_active_balance_settled: int = 0
local_active_balance_unsettled: int = 0
local_inactive_balance_settled: int = 0
local_inactive_balance_unsettled: int = 0
remote_active_balance_settled: int = 0
remote_active_balance_unsettled: int = 0
remote_inactive_balance_settled: int = 0
remote_inactive_balance_unsettled: int = 0
def local_active_balance_total(self):
return self.local_active_balance_settled + self.local_active_balance_unsettled
def remote_active_balance_total(self):
return self.remote_active_balance_settled + self.remote_active_balance_unsettled
def local_inactive_balance_total(self):
return self.local_inactive_balance_settled + self.local_inactive_balance_unsettled
def remote_inactive_balance_total(self):
return self.remote_inactive_balance_settled + self.remote_inactive_balance_unsettled
def active_balance_total(self):
return self.local_active_balance_total() + self.remote_active_balance_total()
def inactive_balance_total(self):
return self.local_inactive_balance_total() + self.remote_inactive_balance_total()
def channel_metrics(channel):
return ChannelMetrics(
local_balance_settled=channel.local_balance,
local_balance_unsettled=sum(h.amount for h in channel.pending_htlcs if not h.incoming),
remote_balance_settled=channel.remote_balance,
remote_balance_unsettled=sum(h.amount for h in channel.pending_htlcs if h.incoming)
)
def peer_metrics(channels):
pm = PeerMetrics()
for channel in channels:
cm = channel_metrics(channel)
if channel.active:
pm.local_active_balance_settled += cm.local_balance_settled
pm.local_active_balance_unsettled += cm.local_balance_unsettled
pm.remote_active_balance_settled += cm.remote_balance_settled
pm.remote_active_balance_unsettled += cm.remote_balance_unsettled
pm.channels_active += 1
else:
pm.local_inactive_balance_settled += cm.local_balance_settled
pm.local_inactive_balance_unsettled += cm.local_balance_unsettled
pm.remote_inactive_balance_settled += cm.remote_balance_settled
pm.remote_inactive_balance_unsettled += cm.remote_balance_unsettled
pm.channels_inactive += 1
return pm
class Lnd:
def __init__(self, lnd_dir, server, tls_cert_path=None, macaroon_path=None):
os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA'
@ -41,7 +117,10 @@ class Lnd:
self.chan_info = {}
self.fwdhistory = {}
self.valid = True
self.dict_channels = None
self.peer_channels = {}
self.chan_metrics = {}
self.peer_metrics = {}
try:
self.feereport = self.get_feereport()
except grpc._channel._InactiveRpcError:
@ -205,15 +284,31 @@ class Lnd:
request = ln.ListChannelsRequest()
self.channels = self.lnstub.ListChannels(request).channels
return self.channels
def get_dict_channels(self):
if self.dict_channels is None:
channels = self.get_channels()
self.dict_channels = {}
for c in channels:
self.dict_channels[c.chan_id] = c
return self.dict_channels
def get_chan_metrics(self, chanid):
if not chanid in self.chan_metrics:
self.chan_metrics[chanid] = channel_metrics(self.get_dict_channels()[chanid])
return self.chan_metrics[chanid]
# Get all channels shared with a node
def get_shared_channels(self, peerid):
# See example: https://github.com/lightningnetwork/lnd/issues/3930#issuecomment-596041700
byte_peerid=bytes.fromhex(peerid)
if peerid not in self.peer_channels:
request = ln.ListChannelsRequest(peer=byte_peerid)
self.peer_channels[peerid] = self.lnstub.ListChannels(request).channels
def get_peer_channels(self, peerid):
if not peerid in self.peer_channels:
channels = self.get_channels()
self.peer_channels[peerid] = [c for c in channels if c.remote_pubkey == peerid]
return self.peer_channels[peerid]
def get_peer_metrics(self, peerid):
if not peerid in self.peer_metrics:
self.peer_metrics[peerid] = peer_metrics(self.get_peer_channels(peerid))
return self.peer_metrics[peerid]
def min_version(self, major, minor, patch=0):
p = re.compile("(\d+)\.(\d+)\.(\d+).*")

View file

@ -170,26 +170,17 @@ class Policies:
# Consider multiple channels per node policies
if any(map(lambda n: "node." + n in config, multiple_chans_props)):
shared_chans=self.lnd.get_shared_channels(channel.remote_pubkey)
metrics = self.lnd.get_peer_metrics(channel.remote_pubkey)
local_active_balance = remote_active_balance = active_total = 0
local_inactive_balance = remote_inactive_balance = inactive_total = 0
channels_active = channels_inactive = 0
for chan in (shared_chans):
if chan.active:
local_active_balance += chan.local_balance
remote_active_balance += chan.remote_balance
channels_active += 1
else:
local_inactive_balance += chan.local_balance
remote_inactive_balance += chan.remote_balance
channels_inactive += 1
active_total = local_active_balance + remote_active_balance
inactive_total = local_inactive_balance + remote_inactive_balance
channels_active = metrics.channels_active
channels_inactive = metrics.channels_inactive
local_active_balance = metrics.local_active_balance_total()
local_inactive_balance = metrics.local_inactive_balance_total()
active_total = metrics.active_balance_total()
inactive_total = metrics.inactive_balance_total()
all_total = active_total + inactive_total
ratio_all = (local_active_balance + local_inactive_balance) / all_total
# Cannot calculate the active ratio if the active total is 0
@ -284,7 +275,11 @@ class Policies:
if 'chan.private' in config and not channel.private == config.getboolean('chan.private'):
return False
ratio = channel.local_balance/(channel.local_balance + channel.remote_balance)
metrics = self.lnd.get_chan_metrics(channel.chan_id)
local_balance = metrics.local_balance_total()
remote_balance = metrics.remote_balance_total()
ratio = local_balance/(local_balance + remote_balance)
if 'chan.max_ratio' in config and not config.getfloat('chan.max_ratio') >= ratio:
return False
if 'chan.min_ratio' in config and not config.getfloat('chan.min_ratio') <= ratio:
@ -293,13 +288,13 @@ class Policies:
return False
if 'chan.min_capacity' in config and not config.getint('chan.min_capacity') <= channel.capacity:
return False
if 'chan.max_local_balance' in config and not config.getint('chan.max_local_balance') >= channel.local_balance:
if 'chan.max_local_balance' in config and not config.getint('chan.max_local_balance') >= local_balance:
return False
if 'chan.min_local_balance' in config and not config.getint('chan.min_local_balance') <= channel.local_balance:
if 'chan.min_local_balance' in config and not config.getint('chan.min_local_balance') <= local_balance:
return False
if 'chan.max_remote_balance' in config and not config.getint('chan.max_remote_balance') >= channel.remote_balance:
if 'chan.max_remote_balance' in config and not config.getint('chan.max_remote_balance') >= remote_balance:
return False
if 'chan.min_remote_balance' in config and not config.getint('chan.min_remote_balance') <= channel.remote_balance:
if 'chan.min_remote_balance' in config and not config.getint('chan.min_remote_balance') <= remote_balance:
return False
chan_info = self.lnd.get_chan_info(channel.chan_id)

View file

@ -120,6 +120,8 @@ def strategy_static(channel, policy, **kwargs):
@strategy(name = 'proportional')
def strategy_proportional(channel, policy, **kwargs):
lnd = kwargs['lnd']
if policy.getint('min_fee_ppm_delta',-1) < 0:
policy.set('min_fee_ppm_delta', 10) # set delta to 10 if not defined
ppm_min = policy.getint('min_fee_ppm')
@ -128,27 +130,25 @@ def strategy_proportional(channel, policy, **kwargs):
raise Exception('proportional strategy requires min_fee_ppm and max_fee_ppm properties')
if policy.getbool('sum_peer_chans', False):
lnd = kwargs['lnd']
shared_chans=lnd.get_shared_channels(channel.remote_pubkey)
local_balance = 0
remote_balance = 0
for c in (shared_chans):
# Include balance of all active channels with peer
if c.active:
local_balance += c.local_balance
remote_balance += c.remote_balance
metrics = lnd.get_peer_metrics(channel.remote_pubkey)
local_balance = metrics.local_active_balance_total()
remote_balance = metrics.remote_active_balance_total()
total_balance = local_balance + remote_balance
if total_balance == 0:
if metrics.channels_active == 0:
# Sum inactive channels because the node is likely offline with no active channels.
# When they come back online their fees won't be changed.
for c in (shared_chans):
if not c.active:
local_balance += c.local_balance
remote_balance += c.remote_balance
local_balance += metrics.local_inactive_balance_total()
remote_balance += metrics.remote_inactive_balance_total()
total_balance = local_balance + remote_balance
ratio = local_balance/total_balance
else:
ratio = channel.local_balance/(channel.local_balance + channel.remote_balance)
metrics = lnd.get_chan_metrics(channel.chan_id)
local_balance = metrics.local_balance_total()
remote_balance = metrics.remote_balance_total()
ratio = local_balance/(local_balance + remote_balance)
ppm = int(ppm_min + (1.0 - ratio) * (ppm_max - ppm_min))
# clamp to 0..inf