DEV Community

Cover image for Visualizing Lightning Network payments
Leonardo Cumplido
Leonardo Cumplido

Posted on • Updated on

Visualizing Lightning Network payments

As Andreas M. Antonopoulos, Olaoluwa Osuntokun, and René Pickhardt explain in Matering the Lightning Network (2021) book:

The Lightning Network (LN) is a second layer peer-to-peer network that allows us to make Bitcoin [near-instantaneous] payments "off-chain".

If you have a LN node, then you can send, receive and route payments through the network. When you send a payment to a node you don't have a channel with, then a path is found to route the payment. Multiple paths are tried in a payment attempt until one succeeds or all fail. All payment attempts come with data related to the different routes the payment tried in order to get to its destination.

Being able to analyse this data might lead to valuable insights around what channels/nodes perform well and are reliable, to whom it might be okay to create a channel with, which directions present more activity, the effects of the chosen fee limit in a payment attempt, etc.

Working alongside LN Capital, under Summer of Bitcoin 2022 internship, mentored by Henrik Skogstrøm, we were able to produce valuable visualizations that represent payment attempts in the LN. All data used for this task is real.


Visualizing payments with networkx

Undirected graph

One tool that's often used for graph analysis and visualization with python is networkx. Networkx provides built-in algorithms for setting the position of the nodes for when the network is displayed. Let's look at two payment attempts drawn using networkx, and go through the elements of both charts.

The code for displaying the following figures goes something like this:

plt.figure(figsize=(10,10))

# G is our graph. It already has the edges and nodes attributes.
widths = np.array([w for *_, w in G.edges.data('weight')])
colors = np.array(['tab:red' if w >=1 else 'black' for *_, w in G.edges.data('fail_weight') ])

pos = nx.spring_layout(G, seed=7)  # positions for all nodes - seed for reproducibi

# nodes
nx.draw_networkx_nodes(G, pos, node_size=600, alpha=0.7)

# edges
nx.draw_networkx_edges(G, pos, width=(widths+0.1)*10, edge_color=colors)  

# labels
nx.draw_networkx_labels(G, pos, font_size=10, font_family="sans-serif")

ax = plt.gca()
ax.margins(0.08)
plt.axis("off")
plt.tight_layout()
Enter fullscreen mode Exit fullscreen mode

The source node is hidden, and the destination node is WalletOfSatoshi.com

Networkx representation of LN failed payment
Figure 1: Failed payment attempt
Networkx representation of LN successful payment
Figure 2: Successful payment attempt

Some things you may notice at first glance:

  • The edges, henceforth we'll call them channels, are coloured differently: they can be black, green or red.
  • The channels thickness varies.
  • The position of the nodes from both charts has nothing to do with each other.
  • In general, both look messy and it's difficult to interpret what they supposedly show.

The color of the channels isn't random. If a payment fails in that given hop, that channel is coloured in red. If a payment succeeds, the successful route is coloured in green. Otherwhise, the channel must be black.

The channels width also represents data from the payment. The more a channel is used in a payment attempt, the thicker that channel will be.

We want to achieve the following: combine different payment attempts aggregating their data, and visually seeing this information without struggle. So far, if we do this adapting the code used to display the above figures, it's impossible to search for insights.

Many payment attempts
Figure 3: Many payment attempts aggregated in one visualization

This tells us nothing. There're a lot of improvements to make the visualization readable and valuable, starting with the nodes position.

Directed graph

Let's improve the payments visual representation by:

  • Adding the source node (our node).
  • Colouring the source and destination nodes in another color than the rest of the nodes.
  • Placing the nodes better to enhance visualization of the routes.

As for the last point, let's compare two graph structures when the nodes are well positioned.

Undirected structure
Figure 4: Undirected graph structure
Directed graph structure
Figure 5: Directed graph structure with curved edges

Figure 4 shows an undirected graph. I've seen that for other payment attempts, some edges overlaped, making it difficult to follow the routes in those payments. That's why a directed graph with curved edges is better for the visualization.

Improved visualization:

Directed graph
Figure 6: More readable payment attempt visualization

Now, let's add the channel features Figures 1 and 2 show, plus these ones:

  • If a channel is red, meaning the payment failed there, and there're still channels in the route to get to the destination, those channels should be coloured in blue. This highlights what channels were never attempted to route the payment.
  • Place channel average fees below each channel. Showing this information might lead to useful insights regarding channels behaviour.
  • Set node size based on the number of times a node has been in a route.

Directed graph with more features
Figure 7: Visual representation of a payment attempt

This looks way better. Now you can clearly see who sent the payment, who the destination was, which routes the payment tried, where in those routes the payment failed, which channels were never tried because of a previous failure, which nodes and channels participated the most in this payment attempt, and the fee amount in msats each channel was going to charge if the payment was to be succesful.

The code for displaying Figures 6 and 7 is:

def show_graph_of_routes(g, node_positions, nodes_colors, widths=1, 
                         edges_colors='black', nodes_size=700, draw_fee_labels=False,
                         fee_labels=None):
    plt.figure(figsize=(12, 9))
    # drawing nodes
    nx.draw_networkx_nodes(g, pos=node_positions, node_size=nodes_size, 
                           node_color=nodes_colors, alpha=0.5,
                           )
    # drawing edges
    nx.draw_networkx_edges(
        g, pos=node_positions, 
        connectionstyle="arc3,rad=-0.1",  # curve the edges
        width=widths,
        edge_color=edges_colors,
    )

    # labels
    nx.draw_networkx_labels(g, pos=node_positions, labels=nodes_labels,
                            font_size=5, font_family="sans-serif",
                            font_color='purple', verticalalignment='top')
    if draw_fee_labels:
        nx.draw_networkx_edge_labels(g, pos=node_positions, edge_labels=fee_labels,
                                 font_size=4, label_pos=0.35)

    _ = plt.title('Graph Representation of the routes attempted trying to pay an invoice', size=15)
Enter fullscreen mode Exit fullscreen mode

Interactive visualization with Altair

We already have a nice way to display one payment attempt with networkx. What if we could visualize more payment attempts aggregated without loosing readability? Moreover, what if we could decide what payments, channels, or nodes to filter out based on their properties? In order to do this, we need to create an interactive visualization.

Altair is a data visualization tool for making interactive charts with Python. An advantaje to other visualization libraries is that you can produce visualizations with a minimal amount of code. The hard work is done previously cleaning the data and transforming it into a format that altair can understand (done with pandas).

Some main ideas to make an interactive visualization for the payment attempts:

  • Menu to select the payments to visualize.
  • Zoom in and out.
  • Panning.
  • Drag the pointer on top of a node to see the node alias, number of times it's been part of a route, and number of failures.
  • Drag the pointer on top of a channel to see the average fee of the channel, number of times it's been part of a route, number of failures, and the number of times it's been part of a successful route.
  • Filtering by dragging:
    • Brush to select channels based on their payment activity.
    • Brush to select nodes based on their payment activity.

This is how the visualization looks:

Interactive visualization
Figure 8: How the interactive visualization looks like

Characteristics of the interactive visualization

Zooming and panning

Zoom

Information displayed when placing the cursor on top of a node or a channel

Graph data

Filter by dragging

You can interact with the two charts at the top, by holding and dragging the cursor to filter out channels or nodes based on how many times they've been part of a route.

Dragging filter

Selecting multiple payments

The chart at the left is to select which payment/s you want to visualize. When selecting multiple payments, the channels and nodes information updates, based on the aggregated data.

Filtering by id


There are more ideas we want to explore to look for things visually. Transforming data into visual representations of it is always the best way to understand it.

Any ideas are welcome :)

Top comments (0)