Lightning Network is one of the most promising solutions to solve the scaling problem Bitcoin faces. It has also gained a lot of traction within the community recently, with more than 700 BTC in payment channels already and an average monthly growth of 15-30% in the number of channels and overall capacity according to 1ML, a site which displays live stats about the network.
There are three major implementations of Lightning nodes in active development and several end-user wallets are also working on supporting the protocol. With more and more developers and infrastructure engineers involved in Lightning, it’s no wonder that people want to learn more about how nodes are implemented: whether it's to develop a deeper understanding of how Lightning works, contribute to the open-source efforts behind it or be better prepared to troubleshoot some of the problems that may arise. This is the first of a series of articles focused on LND internals, as part of the research we are doing at Muun to deploy Lightning to our users.
LND is the most up-to-date Lightning node implementation as of February 2019. It is written in Go in an almost literate code style by the team at Lightning Labs, a San Francisco based company that has been pioneering research around Lightning since 2016. It’s nevertheless a complex piece of software consisting of many modules that handle key management, an on-chain wallet, channel lifecycle, payment routing, reactive security and other important tasks.
Today we are going to look at the process of opening a new payment channel and how it is implemented in LND. To get the most out of this article, you should be familiar with the Go language and have a basic understanding of the Lightning Network building blocks (for a refresher check out this page).
We will be looking at the following modules:
To set up a new channel, two nodes must exchange a series of messages as specified in the BOLT #2: Peer Protocol document of the Lightning RFC. We will call these nodes Alice and Bob, with Alice initiating the process and Bob as her counterparty. The whole process looks like this:
- Alice sends an open_channel message to Bob with the proposed channel parameters, such as capacity.
- Bob sends an accept_channel message to Alice with his funding pubkey for this channel so that Alice can craft a funding transaction with a 2-of-2 multisig output.
- Alice creates a funding transaction and sends Bob a funding_created message with her signature for Bob’s first commitment transaction and the details needed for him to sign Alice’s first commitment transaction.
- Bob sends a funding_signed message with his signature for Alice’s first commitment transaction.
- Alice broadcasts the funding transaction.
- Both Alice and Bob wait for the funding transaction to reach enough confirmations and exchange funding_locked messages, after which the channel is considered active by both and able to route payments.
Opening a new channel in LND
Normally, a request to open a new channel arrives at the LND daemon via the lncli command or by calling the gRPC server from code. Since lncli is just a thin command line wrapper around a gRPC client, in both cases the request will be handled by a function defined in rpcserver.go. Depending on which endpoint was called, that handler will be either OpenChannel or OpenChannelSync.
LND has a layered architecture. At the uppermost layer sits the gRPC server. It maps gRPC calls to an internal format and forwards them to a server defined in server.go. This avoids coupling the server functionality to any particular RPC protocol. Let’s look at the OpenChannelSync method defined in rpcserver.go then. After doing some parsing and sanity checking of the received gRPC arguments, it will repackage them in a protocol agnostic way and forward them to the OpenChannel method in server.go.
The OpenChannel method will attempt to locate the peer we are trying to open a channel with using the identity public key of the node. If it fails to do so, meaning we are not currently connected to the peer, it will fail the request. If we locate our peer it will pass everything along to a component named funding manager by calling its initFundingWorkflow method. The funding manager will be our focus for the rest of the post.
Introducing the Funding Manager
The funding manager is the LND component that handles most of the details of bringing a Lightning channel from zero to fully opened and capable of routing payments. It lives in fundingmanager.go. Inside that file you will find a data structure named fundingManager that contains:
- Current channel policies and related configuration parameters.
- Handles to other components like the wallet and the chain monitoring service.
- Intermediate state for channels in the process of being opened (activeReservations and signedReservations).
- Go channels that accept requests to be processed by the manager goroutine (fundingRequests, fundingMsgs and queries).
- Several mutexes for concurrency handling.
- A quit Go channel for shutting down all manager goroutines.
If we peek into its Start method, we see it reloads pending Lightning channels from the node database (important when our node was stopped or crashed). After it finishes it will launch a goroutine with the reservationCoordinator method. This method multiplexes the different origins from which messages can arrive at the manager and routes them to appropriate handlers. It takes advantage of Go’s select construct to listen to multiple Go channels at the same time and retrieve messages in a loop as they arrive.
Let’s now pick up where we left off before we introduced the funding manager. The initFundingWorkflow method I mentioned earlier has a really simple implementation. It will forward the provided struct through the fundingRequests channel to be consumed by the coordinator goroutine. This technique of passing messages through channels is the recommended way in Go programs to handle concurrency while minimizing the exposure to race conditions in shared memory. It’s commonly described as:
Do not communicate by sharing memory; instead, share memory by communicating.
Other modules of LND follow a similar pattern of starting a single goroutine that receives all messages sent to the component and acts upon them in a serial fashion. This works in a manner similar to the Actor model. Many of the techniques and patterns used for the funding manager are replicated in other components of LND, so understanding how they work makes it easier to explore other parts of the codebase.
- Initializes a new channel reservation with the wallet. The reservation is a structure defined in reservation.go that represents an intent to open a new channel with a counterparty. It stores information gathered through the funding process and locks resources, such as funding UTXOs to prevent them from being double-spent.As part of the creation of the funding reservation, coins will be selected from the wallet UTXO set to craft a funding transaction locking some of them in a 2-of-2 multi-sig output.
- Sends the open_channel message to Bob as described by the Lightning RFC.
The open_channel message is sent to our counterparty using the wire protocol encoding. If you would like to take a closer look at how LND communicates with other nodes, take a look at peer.go, in particular the readHandler and writeHandler methods.
Responding to open channel requests
Upon receiving an open_channel message, Bob (the channel counterparty) will forward it to its own funding manager, and will eventually be handled in the handleFundingOpen method. This handler performs the following tasks:
- Validate the parameters provided to the counterparty, making sure the channel capacity is not excessively big or small, for example.
- Initializes its own channel reservation with the wallet, filling it with information provided by Alice and by themselves.
- Sends the accept_channel message to Alice with his funding pubkey and other channel parameters.
Alice will handle the accept_channel message in handleFundingAccept by populating her channel reservation with the counterparty data provided by Bob and creating a signature for Bob’s version of the first commitment transaction. After that, the signature will be sent to Bob inside a funding_created message. The commitment transactions are generated in a deterministic way based on both ends, based only on the negotiated channel parameters, so there is no need to exchange them. Only the signatures need to be verified against the expected payloads.
Let’s look at the handleFundingCreated method in fundingmanager.go now. When Bob receives the funding_created message it stores Alice’s signature for his first commitment transaction and in turn signs Alice’s, sends the funding_signed message and starts watching for the funding transaction to confirm. In turn, when Alice receives the funding_signed message with the signature for its own commitment transaction, she will store it in the same way as Bob did previously, and publish the funding transaction to the blockchain.
At this point, both Alice and Bob will launch a separate goroutine to watch for confirmations of the funding transaction on the Bitcoin network. Once they see enough confirmations for the funding transaction they will exchange funding_locked messages which will indicate that the channel is fully open and able to route payments.
We have now reached the end of the channel setup protocol. Along the process, we have identified some of the core components of LND and how they interact together to provide the node functionality. Because LND is being actively developed, some of the details may change, but the core architecture will probably remain stable in the foreseeable future.
In the following posts, we will look at other components of LND. Subscribe to this blog and our Twitter account to stay updated.
Visit Muun Website: https://muun.com/
Follow Muun on Twitter: https://twitter.com/MuunWallet