from math import comb
import matplotlib.pyplot as plt
import numpy as np
from math import log2 as log
import plotly.express as px
import json
Warning: This work is currently under review. Error might be there and conclusions may change!
Operators of Lighting network nodes observe that their channels are frequently having the majority of the liquidity on one side (aka channel depletion). Similarly when the network is probed the liquidity distribution within payment channels is regularly observed to follow a bimodal distribution with the two modes at the ends of channels. Often the drain from supply and demand dynamics of liquidity as well as the fee market of routing fees as well as configuration settings like htlc_maximum_msat are believed to mitigate the phenomenon of channel depletion.
This work provides a geometrical argument why the liquidity in payment channels is likely to follow a bimodal distribution where the two modes are on the two ends of the channels as observed in reality. This particular argument works without looking at source and sink nodes, payment routing, the fee market for routing or similar features. Instead the only assumption is that all feasible wealth distributions in the polytope described by the Network of Payment channels are equally likely.
This assumption can most likely be weakened as the main argument is that the majority of the volume of the polytope of wealth distributions is located close to the surface of the polytope. Yet wealth distributions close to the surface of the polytope are only possible if some channels are depleted.
In particular only from gossip information one can diverge from the uniform prior probability to a precisely estimated bimodal prior distribution of liquidity in the channels of the network. This reduces the uncertainty about the liquidity by less than $1$ bit per channel. Also the cost function when minimizing the information content becomes non linear and non convex making the problem of planning optimally reliable payment flows np-hard. As the estimation of the more accurate bimodal prior is computationally hard and as using the prior makes min cost flow computation np hard the results are more of theoretical importance indicating that depletion of channels is expected to emerge and something that can hardly be mitigated even with optimal liquidity management.
Special thanks to Rene Treffer who made and shared a similar observation with my towards the end of 2023 when he suggested to drop the assumption that the liquidity in channels is distributed independently of each other. Thanks to Stefan Richter for a helpful discussion on a preliminary results. The work is sponsored through a grant from OpenSats and through individual patreons.
In A Mathematical Theory of Payment Channel Networks the set of feasible wealth distributions among $n$ peers on the Lightning Network was described as an $n-1$ dimesional polytope. This polytope can be embedded both in $\mathbb{R}^{n-1}$ and $\mathbb{R}^n$. For simplicity and better redability we focus on the latter embedding.
As a quick reminder we review the construction of the polytope $W$ of possible wealth distributions.
Let $w_1,\dots,w_n$ be the wealth of $n$ users, i.e. the number of coins each user owns. Given a fixed number $C$ of coins in circulation the possible wealth distributions may be described by a convex polytope. This is given as the intersection of the hyperplane derrived from the set of zeros of the equation $w_1 + \dots w_n = C$ with the half spaces $w_i \geq 0$.
Additionally the polytope of wealth distributions is reduced through the constraints that rise from the current topology of the payment channels. In particular the follwoing sets of inequalities need to be satisfied.
We take a network of 3 channels between 3 peers that form a triangle. The Polytope of wealth distributions can be depicted with the following code:
data_x = []
data_y = []
data_z = []
colors = []
for x in range(10):
for y in range(10):
for z in range(10):
if x+y+z-9 == 0:
data_x.append(x)
data_y.append(y)
data_z.append(z)
#if x - y == 0 and y - z == 0:
if x+y >= 3 and x+z >= 3 and z+y >= 3 and x <=6 and y <=6 and z <=6:
if x == y == z == 3:
colors.append("All peers own 3 coins")
else:
colors.append("LN")
else:
colors.append("onchain")
fig = px.scatter_3d(x=data_x, y=data_y, z=data_z,color = colors, width=900, height=600,
title= "Polytope W of wealth distributions on Bitcoin on Lighting Network for {} coins and channels of capacity {}, {} and {}".format(9,3,3,3))
fig.show()
The red dots (and the green one) in the diagram correspond to the feasible wealth distributions within the topology defined by the payment channels.
The green dot is an arbitrarily chosen wealth distribution in which each peer owns $3$ coins. Of course there are several configurations (aka states) within the payment channels such that each node can own 3 coins:
We investigate this more systematically:
For a payment channel $c_{ij}$ with capacity $c$ between peers $i$ and $j$ we have two projections $\pi_1(c_{ij})=l_i$ and $\pi_2(c_{ij})=l_j$ where $l_i$ is the liquidity that $i$ owns in the channel and $l_j$ is the liquidity $j$ owns in the channel. in particular $l_i+l_j=c$
Because of the last equation we can - without loss of generality - describe the liquidity state of a channel by looking at the first projection $\pi_1(c_{ij})$. The state of the channel can take $c+1$ values.
If we have a network of $m$ channels we have the space $S$ of all network states which is a subset of $\mathbb{R}^m$. In particular the space of all states is bounded and finite as the capacities of each channel is a finite integer.
Usually we have $m>>n$ as we assume that a payment channel network needs to have at least one channel per user and in order to allow for reasonable payment routing on average even more than $1$ channel per user are necessary. This means that the space of states $S$ is usually substentially larger than the space of wealth distributions.
data_x = []
data_y = []
data_z = []
colors = []
for x in range(4):
for y in range(4):
for z in range(4):
data_x.append(x)
data_y.append(y)
data_z.append(z)
if x - y == 0 and y - z == 0:
colors.append("All peers own 3 coins")
else:
colors.append("LN states")
fig = px.scatter_3d(x=data_x, y=data_y, z=data_z,color = colors, width=900, height=600,
title= "Polytope S of states in a network of {} coins and channels of capacity {}, {} and {}".format(9,3,3,3))
fig.show()
Note in comparison to the previous example the semantics of the coordinates in the diagram has changed. While previously each axis represented the wealth owned by one of the three peers in this diagram each axis representes how many coins are on one side of each channel
in particular we see that the space of states is significantly larger and higher dimensional than the space of wealth distributions.
The blue dot(s) are the states that correspond to the green dot in the polytope describing the possible wealth distributions on the lighting network. If you only see 1 blue dote use the mouse to rotate the cube to see a line of blue dots.
There is an interesting relation between the polytope $W \subset \mathbb{R}^n$ of all wealth distributions and the polytope $S\subset \mathbb{R}^m$ of all possible network stats.
There is a natural mapping $\omega: S \longrightarrow W$ which assigns a network state its wealth distribution.
We can describe $\omega$ component wise: $\omega(s)_i:=\sum_{j}\pi_1(c_{i,j})$
This means that the wealth of a peer is just the sum the liquidity that is controlled by the peer in the peer's channels.
The mapping is $\omega$ is surjective.
In some cases this mapping is an isomorphism. This happens if and only if the payment channel network has no cycles and is thus a tree.
As most payment channel networks consist of trees we are interested in the wealth distributions $\vec{w}$ that have non trivial preimages under $\omega$ e.g. $|\omega^{-1}(\{\vec{w}\})| > 1$. Let $s\in S$ be a state in $\omega^{-1}(\{\vec{w}\})$. If there is a cycle in the liquidity network one can find a different state $s'$ such that $\omega(s) = \omega(s')$. We call such to states $s$ and $s'$ equivalent.
More precisely:
$x\sim y$ with $x,y \in \mathbb{R}^m$ if and only iff ther exists a circulation in the channel graph that does not change the wealth distribution i.e. $\omega(x) = \omega(y)$
We can build the Quotientspace $S/\sim$ and by our construction we have $W \cong S/\sim$. This means we can understand the polytope of walth distributions as the polytope of channel states modulo circular rebalancing.
Let's stick to our example and count how many states can lead to the same wealth distribution in our example lightning network. First we need two helper functions:
def next_state(channels):
"""
helper function (generator) that goes through all possible states for a list of channels
"""
num_states = 1
boundaries = []
for c in channels:
num_states*=(c+1)
boundaries.append(num_states)
for idx in range(num_states):
x = idx
state = []
for c in channels:
if x < (c+1):
state.append(x)
else:
state.append(x%(c+1))
x = int(x/(c+1))
yield state
def compute_wealth_distribution(graph, channels, state):
"""
realization of the map $\omega: S ---> W that maps a state to a wealth distribution
"""
distribution = {}
for pos,s in enumerate(state):
src = graph[pos][0]
dest = graph[pos][1]
if src in distribution:
distribution[src] += s
else:
distribution[src] = s
if dest in distribution:
distribution[dest] += channels[pos] - s
else:
distribution[dest] = channels[pos] - s
return distribution
def map_states_to_wealth_distributions(graph, channels):
"""
returns a dictionary with a wealth distribution as a key and a list of states that lead to the wealth distribution
"""
states_per_wealth_distribution = {}
for state in next_state(channels):
d = compute_wealth_distribution(graph, channels, state)
d_str = json.dumps(d)
if d_str in states_per_wealth_distribution:
states_per_wealth_distribution[d_str].append(state)
else:
states_per_wealth_distribution[d_str] = [state]
return states_per_wealth_distribution
We start with a small network of 3 users that have 3 channels that build a circle. We draw the polytope of wealth distributions and color the dots depending on how many states exist that lead to this particular wealtch distribution
graph = {0:("A","B"),1:("B","C"),2:("C","A")}
channels = [6,9,12]
states_per_wealth_distribution = map_states_to_wealth_distributions(graph, channels)
def plot_polytope_of_wealth_distributions(states_per_wealth_distribution):
data_x = []
data_y = []
data_z = []
colors = []
for dist, states in states_per_wealth_distribution.items():
jsn = json.loads(dist)
data_x.append(jsn["A"])
data_y.append(jsn["B"])
data_z.append(jsn["C"])
colors.append("{} states".format(len(states)))
fig = px.scatter_3d(x=data_x, y=data_y, z=data_z,color = colors, width=900, height=600,
title= "Polytope W of wealth distributions in a network of {} coins and channels of capacity {}, {} and {}".format(sum(channels),channels[0],channels[1],channels[2]))
fig.show()
plot_polytope_of_wealth_distributions(states_per_wealth_distribution)
print(len(states_per_wealth_distribution), "wealth distributions are possible")
262 wealth distributions are possible
def plot_quotient_space(states_per_wealth_distribution,max_equivalent = 1):
data_x = []
data_y = []
data_z = []
colors = []
cnt = 0
max_states = 0
for dist, states in states_per_wealth_distribution.items():
if len(states)>max_states:
max_states = len(states)
for k,state in enumerate(states):
#each list of states has at least 1 state here we decide how many elements of the equivalent class we wish to add
if k>=max_equivalent:
break
data_x.append(state[0])
data_y.append(state[1])
data_z.append(state[2])
colors.append("{} states".format(len(states)))
cnt += 1
title = "Polytope S/~ of channel states in a network of {} coins and channels of capacity {}, {} and {}".format(sum(channels),channels[0],channels[1],channels[2])
if max_equivalent >= max_states:
title = "Polytope S channel states in a network of {} coins and channels of capacity {}, {} and {}".format(sum(channels),channels[0],channels[1],channels[2])
fig = px.scatter_3d(x=data_x, y=data_y, z=data_z,color = colors, width=900, height=600,
title= title)
fig.show()
return cnt
cnt = plot_quotient_space(states_per_wealth_distribution)
print(cnt, "non equivalent states exist in this network")
262 non equivalent states exist in this network
One can clearly see that the spaces $W$ and $S/\sim$ are isomorphic e.g.: $W\cong S/\sim$
Let us also plot the polytope of alle states (including the states equivalent states (this will fill the cube / box from the previous plot)
cnt = plot_quotient_space(states_per_wealth_distribution,7)
Note that the surface of the polytope $S$ of possible states consists of states that have at least one channel depleted. We review some facts from convex geometry and findings from this notebook
This suggests that channel depletion is the norm and not the exception on the Lightning Network. Therefor we conduct another experiment
We make two assumptions:
Given a payment channel network and these two assumptions we can assign a probability to each state. This leads to a probability distribution for the split of the capacity within each channel
#1. assumptions all wealth distributions have equal probability
p_wealth_distribution = 1 / len(states_per_wealth_distribution)
channel_states = {k:{y:0 for y in range(x+1)} for k,x in enumerate(channels)}
s = 0
for k,states in states_per_wealth_distribution.items():
#2. assumption all states in the preimage of a wealth distribution are equally likely
divisor = 1/len(states)
for state in states:
for pos,y in enumerate(state):
#print(pos,y)
tmp = p_wealth_distribution*divisor/len(state)
channel_states[pos][y] +=tmp
s+=tmp
for k, name in graph.items():
states = len(channel_states[k])
plt.figure(figsize=(6,4))
plt.title("{} channel with capacity of {} sats".format(name, channels[k]))
tmp = [channel_states[k][i] for i in range(states)]
s = sum(tmp)
y = [x/s for x in tmp]
H_uniform = log(channels[k]+1)
H_bimodal = sum(-log(x)*x for x in y)
information_gain = H_uniform - H_bimodal
print("Going from uniform to bimodal prior we gain {:4.2f} bits of information".format(information_gain))
print("H_uniform = {:4.2f}".format(H_uniform))
print("H_bimodal = {:4.2f}".format(H_bimodal))
plt.plot([i for i in range(states)], y)
plt.grid()
plt.xlabel("x = liquidity on the left side of the channel")
plt.ylabel("$P(X\geq x)$")
plt.show()
Going from uniform to bimodal prior we gain 0.04 bits of information H_uniform = 2.81 H_bimodal = 2.77
Going from uniform to bimodal prior we gain 0.06 bits of information H_uniform = 3.32 H_bimodal = 3.27
Going from uniform to bimodal prior we gain 0.05 bits of information H_uniform = 3.70 H_bimodal = 3.65
Very remarkably we observe that the liquidity in channels is not expected to follow a uniform distribution but rather follow a bimodal distribution with the two modes at the rim of the channels. This is the same behavior as observed in the real lighting network. However in this case no assumptions about fees, payment flows, source and sink nodes had to be conducted. Even within a circular economy in which peers change their wealth distribution by making payments we expect the liquidity in channels to follow a bimodal distribution.
While drain in particular channels may lead to the liquidity to be more likely in one mode than the other and while channel management (for example via control valves) may mitigate such observations it seems unlikely that the liquidity in the channel will be distributed differently
In particular it is interesting that knowing the bimodal liquidity distribution does not contain significantly more information than assuming a uniform prior. Thus it may seem reasonable to stick with the uniform prior probability distribution in minium cost flow computation when optimizing for reliability by reducing the uncertainty_cost
since the cost function for the uniform prior is convex and min cost flow computation computationally feasible.
Some observations about $n$ dimensional hypercubes
# interestingly enough we can compute the fraction of states in which at least 1 channel is depleted
# this is based on the assumption that channels are of equal size. the situation becomes worse if the channels are not of equal size
# because the surface / voume ratio will increase (surface is minimized in a cube (actually a sphere))
def surface_n_dim_hypercube(length,dim,rim_width=1):
return 1 - (1 - 2*rim_width / length)**dim
total_cap = 5_000*100_000_000
num_channels = 80_000
avg_chan_cap = total_cap / num_channels
print(avg_chan_cap," is the average channel size in sats")
print("{:4.2f}% of all states have a least one channel depleted".format(surface_n_dim_hypercube(avg_chan_cap,num_channels,50)*100))
6250000.0 is the average channel size in sats 72.20% of all states have a least one channel depleted
cap = 10
depleted_cnt = 0
states_cnt = 0
data_x = []
data_y = []
data_z = []
colors = []
for x in range(10+1):
for y in range(10+1):
for z in range(10+1):
data_x.append(x)
data_y.append(y)
data_z.append(z)
states_cnt += 1
if x == 0 or x == 10 or y == 0 or y == 10 or z == 0 or z == 10:
colors.append("State with at least 1 depleted channel")
depleted_cnt += 1
else:
colors.append("All states")
fig = px.scatter_3d(x=data_x, y=data_y, z=data_z,color = colors, width=900, height=600,
title= "Polytope S of states in a network of {} coins and channels of capacity {}, {} and {}".format(3*cap,cap,cap,cap))
fig.show()
print("{:4.2f}% of all states include depleted channels".format(100* depleted_cnt / states_cnt))
45.23% of all states include depleted channels