Visualizing Lightning Network payments
Tags: lightningnetwork, datascience, python, todayilearned
Published: 2022-07-25 13:38:00

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](https://twitter.com/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:

```python
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()
```

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

<table><tr>
<td> <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uz6lkdvd3nlfb4l9lb33.png" alt="Networkx representation of LN failed payment" style="width: 250px;"/> <figcaption>Figure 1: Failed payment attempt</figcaption> </td>
<td> <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/181zu0eze395knw68i74.png" alt="Networkx representation of LN successful payment" style="width: 250px;"/> <figcaption>Figure 2: Successful payment attempt</figcaption> </td>
</tr></table>

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.

<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/67t7ibnzaq9ofgf8kach.jpg" alt="Many payment attempts"/> <figcaption>Figure 3: Many payment attempts aggregated in one visualization</figcaption>

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.

<table><tr>
<td> <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7imqkdk2i2f9q26tpj8f.png" alt="Undirected structure" style="width: 250px;"/> <figcaption>Figure 4: Undirected graph structure</figcaption> </td>
<td> <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ksyf6oc3gwnyluxum7jq.png" alt="Directed graph structure" style="width: 250px;"/> <figcaption>Figure 5: Directed graph structure with curved edges</figcaption> </td>
</tr></table>

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:

<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jsysflw6oou4esmk7dwd.png" alt="Directed graph" /> <figcaption>Figure 6: More readable payment attempt visualization</figcaption>

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.

<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ln2prosh0sr5nf1v04w2.png" alt="Directed graph with more features" /> <figcaption>Figure 7: Visual representation of a payment attempt</figcaption>

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:
```python
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)
```

## 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](https://altair-viz.github.io/index.html) 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:

<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w974exwjtzd8ycpajdrd.png" alt="Interactive visualization" /> <figcaption>Figure 8: How the interactive visualization looks like</figcaption>

### Characteristics of the interactive visualization

**Zooming and panning**

![Zoom](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jvh1uc3cawnsc1qdcdp1.gif)

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

![Graph data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cnux1dbysnq0npfpoeis.gif)

**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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b1amjheuz63zqusrbpjj.gif)

**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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/buca1314k04axvox669n.gif)

---
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 :)