Skip to content

Commit

Permalink
Consideration of the unsettled balance
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
feelancer21 committed Jun 10, 2024
1 parent 2b103c8 commit 2499f74
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 45 deletions.
109 changes: 102 additions & 7 deletions charge_lnd/lnd.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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
Expand All @@ -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'
Expand All @@ -41,7 +117,10 @@ def __init__(self, lnd_dir, server, tls_cert_path=None, macaroon_path=None):
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:
Expand Down Expand Up @@ -205,15 +284,31 @@ def get_channels(self):
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+).*")
Expand Down
41 changes: 18 additions & 23 deletions charge_lnd/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,26 +170,17 @@ def match_by_node(self, channel, config):
# 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
Expand Down Expand Up @@ -284,7 +275,11 @@ def match_by_chan(self, channel, config):
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:
Expand All @@ -293,13 +288,13 @@ def match_by_chan(self, channel, config):
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)
Expand Down
30 changes: 15 additions & 15 deletions charge_lnd/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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
Expand Down

0 comments on commit 2499f74

Please sign in to comment.