Lightning Network Development for Modern Applications
In-depth guide to help you kickstart and streamline your Lightning Application development workflow
Introduction
I had the privilege to participate in the Chaincode Labs Lightning Residency 2018, along with 11 other amazing Lightning Network (LN) enthusiasts from around the globe. We learnt about deep technical concepts regarding the BOLTs specifications and went into detail about the various node implementations available to date, their advantages and disadvantages.
We were also tasked with building an application — extension, plugin, web app, mobile app, you name it. Throughout that week, while building the first version of the Lightwork web application, I encountered some architectural issues, mostly relating to the handling of BOLT11 invoices (core to LN) between the LN node and my web app. Having gone through (most of) these hurdles, I figured I’d write about the major pain points about developing an LN-powered application and how to mitigate them.
Application Stack
There are usually two main entities to most modern applications: a Client and a Server. The Client can be a mobile application or a JavaScript SPA for example, and the Server can be comprised of a NodeJS/Go/Rust backend, which exposes a RESTful or GraphQL-based API.
Adding Lightning Network support for your application, will usually introduce two new entities to your application stack: a Bitcoin node and a Lightning Network node.
For Bitcoin there are a couple node implementations available to use, with the two most widely accepted ones being:
- Bitcoin Core —
bitcoind
- BTCD —
btcd
Based on community consensus, number of active nodes on the network, and maintenance stats from the respective code repositories, it’s clear that Bitcoin Core is the most widely used and better-maintained implementation — and the one we’re going to use.
On the Lightning Network side of things, there are (currently) three compatible clients that all adhere to the same BOLT specifications:
- C-Lightning (written in C)
- LND (written in Go)
- Eclair (written in Scala)
You should evaluate each of the clients and decide what makes the most sense for you. I’ve personally used C-Lightning and LND before, but have also heard great things about the Eclair client. For Lightwork I decided to go with LND as it provides a friendly and easy-to-use gRPC/REST API, as well as Neutrino light client support out-of-the-box (testnet for now).
Time to get our nodes up and running.
Bitcoin Core Setup
The current mainnet Bitcoin blockchain sits at roughly ~260GB of data with around 550,000 blocks mined. Since our LND node depends on having access to the underlying chain for opening and closing channel transactions, running a full node on mainnet in a local machine/laptop seems like a huge undertaking. Not only is it 1/4TB of data, but it also takes a couple hours to fully sync (depending on your connection it can take many days).
Bitcoin also has a testnet3 network, which allows for developers to test new features and changes to the protocol without affecting mainnet. This is also great for developers building applications and services upon Bitcoin, which are able to test end-to-end flows using valueless testnet BTC coins. The testnet is lighter in size, sitting at around ~25GB with roughly 144,000 blocks. Syncing testnet3 still seems like a big hurdle if all you want to do is mess around with an application that interfaces with the Lightning Network. That is why Bitcoin Core gives us yet another network, this one called regtest (simnet for btcd
users).
Regtest is a version of the blockchain that you essentially start from scratch on your machine. Given that its sole purpose is to allow for faster development on the protocol, as the admin of that node, you can mine as many blocks as you need (almost) instantaneously. And the best part is that it’s only as large of a chain as you make it. I’ve used my regtest network extensively when developing and it still sits at ~37MB. This is more along the lines of what we want.
Installing Bitcoin Core Node
These following instructions are for running
bitcoind
on macOS systems. For Windows and Linux instructions, please see https://bitcoin.org/en/full-node
If you haven’t already, install Bitcoin Core on your environment by running:
curl -O https://bitcoin.org/bin/bitcoin-core-0.17.0/bitcoin-0.17.0-osx64.tar.gz
If you are familiar with PGP, it’s also advised that you check the signed hash for the release: 01EA 5486 DE18 A882 D4C2 6845 90C8 019E 36C2 E964
(more info here).
Now you can unpack the tarball with:
tar -zxf bitcoin-0.17.0-osx64.tar.gz
And subsequently make the bitcoind
binary available system wide with:
sudo mkdir -p /usr/local/bin
sudo cp bitcoin-0.17.0/bin/bitcoin* /usr/local/bin/
To clean things up you can delete the downloaded folder with:
rm -rf bitcoin-0.17.0*
You should now have full access to using the bitcoind
and bitcoin-cli
commands on your machine.
Node Configuration
A Bitcoin Core full node has many configuration options, so instead of passing all of these parameters on the command line every time you run the binaries, it is advised that you use a bitcoin.conf
config file.
For a list of all commands supported by
bitcoind
runbitcoind --help
.
Below is an example of a config file with the (necessary) parameters to run a regtest node (and connect to an LND node):
# Daemon Options
server=1# Network Options
regtest=1# RPC Options
rpcport=8332
rpcuser=USERNAME_HERE
rpcpassword=PASSWORD_HERE# ZMQ Options
zmqpubrawblock=tcp://127.0.0.1:28332
zmqpubrawtx=tcp://127.0.0.1:28333
To keep things simple, you could create your bitcoin.conf
file in the main Bitcoin folder that’s created in your system by the full node. In macOS systems, that folder is located at ~/Library/Application Support/Bitcoin
.
Make note of the RPC username and password you choose as we’ll need it during the LND node configuration.
You can then run the following to step into that same Bitcoin directory and start your node daemon with the newly created configuration file:
cd ~/Library/Application Support/Bitcoinbitcoind -datadir=./ -conf=./bitcoin.conf
You should see an output similar to:
In a separate terminal window, you can run bitcoin-cli getblockchaininfo
and you will see the latest data sets from your regtest Bitcoin chain, including the current block height, difficulty level and size on disk. For a full list of bitcoin-cli
commands, run bitcoin-cli --help
.
When you run bitcoind
for the first time, it automatically creates a wallet for you. So at this point, any BTC earned through mining rewards will be sent to this wallet. To mine regtest blocks simply run bitcoin-cli generate 100
, where 100 is the number of blocks to be mined. If you run bitcoin-cli getwalletinfo
you’ll now have a value under ‘balance’. These are regtest BTC coins that are perfect for testing Lightning Network applications. Now that we have a Bitcoin Core daemon running and we know how to mine regtest blocks, it’s time to setup our LND nodes.
You will most likely need to mine upwards of 100 blocks in the beginning of your regtest network in order to see mining rewards appear in your wallet.
LND Node(s) Setup
To connect to the Lighting Network, all you need is an LND node connected to an underlying chain. However, in order to send funds around the network, you must have another node to send to, or request from. One approach would be to spin up multiple VPS boxes with different Bitcoin Core and LND nodes, and then open channels between the two. A simpler approach would be to have multiple LND instances running in your machine, connected to a single bitcoind
regtest node that you have full control over. A user can run as many LND nodes they’d like in a single environment, there are only three requirements:
- Each node must have its own dedicated data directory
- Each node must have its own gRPC/REST ports
- Each node must have its own set of TLS certificates and admin macaroons
LND creates all of the necessary data files needed to run a node on startup, including the authentication macaroon files. With that in mind, after spinning up the first node, you can simply clone it (and delete the authentication macaroons/TLS certs so they’re recreated on initialization of the second node). For macOS users, the default LND folder is at ~/Library/Application Support/Lnd
(NODE A), so I just cloned it into ~/Library/Application Support/Lnd Test
(NODE B).
You may place your node data directories wherever in your file system. This is not a requirements for the nodes to function properly.
Node Configuration
Similar to Bitcoin Core, the LND node takes in a configuration file known as lnd.conf
. In order to run two nodes, we’ll need two files, placed inside each of the respective data directories, configuring the nodes to connect to the correct bitcoind
instance.
NODE A will be running on the default data directory and the default settings (RPC/REST/gRPC ports). We’ll use the following conf file:
# LND Settings
debuglevel=debug
debughtlc=true
alias=YOUR_NODE_NAME
maxpendingchannels=10
color=#eeeeee
rpclisten=0.0.0.0:10009
restlisten=0.0.0.0:8080
listen=9735
externalip=127.0.0.1:9735
datadir=./
datatlscertpath=./tls.cert
tlskeypath=./tls.key
adminmacaroonpath=./data/chain/regtest/admin.macaroon# Bitcoin
bitcoin.active=1
bitcoin.regtest=1
bitcoin.node=bitcoind# Bitcoind
bitcoind.rpcuser=USERNAME_HERE
bitcoind.rpcpass=PASSWORD_HERE
bitcoind.zmqpubrawblock=tcp://127.0.0.1:28332
bitcoind.zmqpubrawtx=tcp://127.0.0.1:28333
Note that the ZMQ options and the RPC username and passwords must match the previously configured
bitcoind.conf
settings.
NODE B will have essentially the same configuration settings, apart from the connecting ports. More specifically, the rpclisten
, restlisten
, listen
, and externalip
properties all receive new ports.
# LND Settings
debuglevel=debug
debughtlc=true
alias=YOUR_NODE_NAME
maxpendingchannels=10
color=#eeeeee
rpclisten=0.0.0.0:10008
restlisten=0.0.0.0:8090
listen=9736
externalip=127.0.0.1:9736
datadir=./
datatlscertpath=./tls.cert
tlskeypath=./tls.key
adminmacaroonpath=./data/chain/regtest/admin.macaroon# Bitcoin
bitcoin.active=1
bitcoin.regtest=1
bitcoin.node=bitcoind# Bitcoind
bitcoind.rpcuser=USERNAME_HERE
bitcoind.rpcpass=PASSWORD_HERE
bitcoind.zmqpubrawblock=tcp://127.0.0.1:28332
bitcoind.zmqpubrawtx=tcp://127.0.0.1:28333
At this point we have two LND nodes running on different data directories, with their own set of TLS certs and authentication macaroons, and running on/against different ports. Time to start the network. In two separate terminal shells, start both NODE A and NODE B with:
lnd --configfile=./lnd.conf
In two more shells, use the lncli
tool to send commands to your nodes. Run the following to create a wallet. For NODE A, we simply need to run the create
command, like so:
lncli --network=regtest create
For NODE B, we must pass a few more properties:
lncli --network=regtest --rpcserver=localhost:10008 --lnddir=./ --tlscertpath=./tls.cert create
The added properties are to ensure that the second lncli
call is talking to NODE B running on port 10008. Run through the wallet creation process and create yourself a password for each of the wallets.
Note that once you’ve created your wallet, you can simply run the
unlock
command to get things going.
There is a whole lot that an LND node actually does, and to learn all of its capabilities head over to LND’s docs or run lncli --help
to see a couple of the commands. A few of the more widely used ones are:
# Wallet balance, in Satoshis
lncli walletbalance# Balance of all channels combined, in Satoshis
lncli channelbalance# Generate Invoice
lncli addinvoice --amt=1000 --memo=testing# Pay Invoice
lncli payinvoice lnbcrt10u1pdlyp6fpp50xvlxjg...# Connect to specific node URI
lncli connect 03fbe39af6166273...@127.0.0.1:9735# Open channel with to specific node
lncli openchannel 03fbe39af6166273...1af2 100000
We’ll use each of these commands to send LN invoices between our two nodes, but before we do that we need some actual regtest BTC in one of our wallets.
Sending regtest BTC from Bitcoin Core to LND node
You can use the bitcoin-cli
tool to send mined regtest BTC to NODE A. First generate a new address from inside NODE A:
lncli --network=regtest newaddress np2wkh
Then head on over to Bitcoin Core and send some regtest BTC to that address. Make sure to generate a couple blocks to mine and confirm the transaction.
# Send 250 BTC to the LND Address
bitcoin-cli sendtoaddress 2MuKLyJQn2UQ2BaVotWpCKAfXpkrsa96eL7 250# Mine Blocks
bitcoin-cli generate 10
Now, back in NODE A, we can get our wallet balance and see that our BTC has transferred correctly.
Opening channel between the two LND nodes
Now that we have BTC inside of NODE A, we can get NODE B to create some payment requests for NODE A. As part of the Lightning Network specification, nodes need to have channels open with other nodes if they’d like to send and receive satoshis (routing invoices through other indirect nodes is of course possible in a real network scenario, just not in our two-node LND regtest network).
To connect two nodes you need to get the node’s public URIs. On NODE B, run the following command:
lncli --network=regtest --rpcserver=localhost:10008 --lnddir=./ --tlscertpath=./tls.cert getinfo
The highlighted portion is the URI that we’ll need to use to connect to this node. If you don’t see a URI listed, it’s probably because you don’t have the externalip
property set on your LND configuration files.
To open a channel between the nodes, first we need to connect them. Run the following on NODE A, with NODE B’s URI.
lncli --network=regtest connect 0394ed661ff...92f446@127.0.0.1:9736
We still have no channels opened, so even if we were to try and pay an invoice, we’d run into routing issues:
As part of opening a channel, LND will create a funding transaction onchain, setting an amount of satoshis to lock in this newly created channel.
lncli --network=regtest openchannel NODE_PUB_KEY amount
For the funding transaction to be valid, you will need to run bitcoin-cli generate 10
in order to mine a couple more blocks in Bitcoin Core. At this point you can run listchannels
inside NODE A to see the opened channel between the two nodes:
lncli --network=regtest listchannels
Handling Invoices (BOLT11 Payment Requests)
Now that NODE A has been funded with some regtest BTC, and we have an active opened channel between the two nodes, we can create an invoice to transact some BTC.
Invoices are at the center of any Lightning Network transaction. To learn more about LN Invoices (Payment Requests) and the BOLT11 specification, read my other article on it.
From NODE B, generate an invoice for 300 satoshis, with a description of ‘testing’:
lncli --network=regtest --rpcserver=localhost:10008 --lnddir=./ --tlscertpath=./tls.cert addinvoice --amt=300 --memo=testing
The highlighted portion is the BOLT11 Payment Request hash. To learn a little more about this invoice, and the types of data we can decode from it, head on over to Lightning Decoder and paste your invoice in.
From NODE A, we can pay this newly created invoice, and have the payment routed over the Lightning Network through the channel we have just opened.
lncli --network=regtest payinvoice lnbcrt3u1pdlygj221k0x...c0h503tpr
NODE B should now have 300 satoshis listed on its side of the channel. We can check it with the channelbalance
command.
lncli --network=regtest --rpcserver=localhost:10008 --lnddir=./ --tlscertpath=./tls.cert channelbalance
Lo’ and behold, our 300 satoshis have been transferred.
You have successfully connected two LND nodes running on the same regtest BTC network, opened a channel between them, and transacted BTC through the payment of BOLT11 invoices. With this setup, you can create endless real-world scenarios of payments through these two LND nodes. This will allow for much easier testing, iteration and troubleshooting of the backend server logic of your Lightning Network-powered application.
Now, to interface with the node from within your application, look into LND’s gPRC or RESTful APIs. You may also leverage an abstraction layer such as LN Service by Alex Bosworth, which may provide a friendlier API.
LNET
When developing LN apps, it’s common to follow a regtest → testnet → mainnet approach, but depending on your application size and requirements, it might make sense to add an extra step.
On the last day of the Chaincode Lightning Residency 2018, Christian Decker from Blockstream demo’ed lnet
. The goal behind lnet
is to simplify the process of initializing a LN network topology for application testing. All you need to do is describe the network in the graphviz dot
format, and lnet
will take care of the rest. This library was built upon C-Lightning instead of LND and does not (yet?) provide as much control, customization and configuration as spinning up your own nodes — but it does facilitate testing with lots of nodes.
More info here: https://github.com/cdecker/lnet
Conclusion
I’ll continue to share notes like these as I continue to develop Lightwork and further my understanding of the Lightning Network. In the next post I hope to walk through actual code snippets/implementations that were used to build Lightwork’s support for LN payments and withdrawals — real world source code is more valuable than 1000 words.
Hashtag Reckless