Lightning Network Daemon ⚡️

RAW Doc

DOCKER

Docker Instructions

There are two flavors of Dockerfiles available:
- Dockerfile: Used for production builds. Checks out the source code from
GitHub during build. The build argument --build-arg checkout=v0.x.x-beta
can be used to specify what git tag or commit to git checkout
before building.
- dev.Dockerfile Used for development or testing builds. Uses the local code
when building and allows local changes to be tested more easily.

Development/testing

To build a standalone development image from the local source directory, use the
following command:

shell
$  docker build --tag=myrepository/lnd-dev -f dev.Dockerfile .

There is also a docker-compose setup available for development or testing that
spins up a btcd backend alongside lnd. Check out the documentation at
docker/README.md to learn more about how to use that
setup to create a small local Lightning Network.

Production (manual build)

To use Docker in a production environment, you can run lnd by creating a
Docker container, adding the appropriate command-line options as parameters.

You first need to build the lnd docker image:

shell
$  docker build --tag=myrepository/lnd --build-arg checkout=v0.14.1-beta .

It is recommended that you check out the latest released tag.

You can continue by creating and running the container:

shell
$  docker run myrepository/lnd [command-line options]

Production (official images)

Starting with lnd v0.12.0-beta, there are official, automatically built docker
images of lnd available in the
lightninglabs/lnd repository on Docker Hub.

You can just pull those images by specifying a release tag:

shell
$  docker pull lightninglabs/lnd:v0.14.1-beta
$ docker run lightninglabs/lnd [command-line options]

Note that daily-* tags are unstable and not for production use.
They are only suitable for development and pre-release testing.

Verifying docker images

To verify the lnd and lncli binaries inside the docker images against the
signed, reproducible release binaries, there is a verification
script in the image that can be called (before starting the container for
example):

shell
$  docker run --rm --entrypoint="" lightninglabs/lnd:v0.14.1-beta /verify-install.sh v0.14.1-beta
$ OK=$?
$ if [ "$OK" -ne "0" ]; then echo "Verification failed!"; exit 1; done
$ docker run lightninglabs/lnd:v0.14.1-beta [command-line options]

Volumes

A Docker volume will be created with your .lnd directory automatically, and will
persist through container restarts.

You can also optionally manually specify a local folder to be used as a volume:

shell
$  docker create --name=mylndcontainer -v /media/lnd-docker/:/root/.lnd myrepository/lnd [command-line options]

Example

Here is an example testnet lnd that uses Neutrino:

shell
$  docker run --name lnd-testnet myrepository/lnd --bitcoin.active --bitcoin.testnet --bitcoin.node=neutrino --neutrino.connect=faucet.lightning.community

Create a wallet (and write down the seed):

shell
$  docker exec -it lnd-testnet lncli create

Confirm lnd has begun to synchronize:

shell
$  docker logs lnd-testnet
[snipped]
2018-05-01 02:28:01.201 [INF] RPCS: RPC server listening on 127.0.0.1:10009
2018-05-01 02:28:01.201 [INF] LTND: Waiting for chain backend to finish sync, start_height=2546
2018-05-01 02:28:01.201 [INF] RPCS: gRPC proxy started at 127.0.0.1:8080
2018-05-01 02:28:08.999 [INF] LNWL: Caught up to height 10000
2018-05-01 02:28:09.872 [INF] BTCN: Processed 10547 blocks in the last 10.23s (height 10547, 2012-05-28 05:02:32 +0000 UTC)

This is a simple example, it is possible to use any command-line options necessary
to expose RPC ports, use btcd or bitcoind, or add additional chains.

LND Development and Testing

To test the Docker production image locally, run the following from the project root:

shell
$  docker build . -t myrepository/lnd:master

To choose a specific branch or tag instead, use the checkout build-arg. For example, to build the latest tagged commit:

shell
$  docker build . --build-arg checkout=v0.14.1-beta -t myrepository/lnd:v0.14.1-beta

To build the image using the most current tag:

shell
$  docker build . --build-arg checkout=$(git describe --tags git rev-list --tags --max-count=1) -t myrepository/lnd:latest-tag

You can also specify a different repo than the default (https://github.com/lightningnetwork/lnd) using the git_url build-arg:

shell
$  docker build . --build-arg git_url=https://git.example.com/lnd.git --build-arg checkout=mybranch -t myrepository/lnd:v0.13.0-beta

Once the image has been built and tagged locally, start the container:

shell
$  docker run --name=lnd-testnet -it myrepository/lnd:latest-tag --bitcoin.active --bitcoin.testnet --bitcoin.node=neutrino --neutrino.connect=faucet.lightning.community

---

INSTALL

Table of Contents


- Table of Contents
- Installation
- Installing a binary release
- Building a tagged version with Docker
- Building a development version from source
- Installing Go
- Go modules
- Installing lnd from source
- Available Backend Operating Modes
- btcd Options
- Neutrino Options
- Bitcoind Options
- Using btcd
- Installing btcd
- Starting btcd
- Running lnd using the btcd backend
- Using Neutrino
- Using bitcoind
- Creating a wallet
- Macaroons
- Network Reachability
- Simnet vs. Testnet Development
- Creating an lnd.conf (Optional)

Installation

There are multiple ways to install lnd. For most users the easiest way is to
download and install an official release binary.
Those release binaries are always built with production in mind and have all
RPC subservers enabled.

More advanced users that want to build lnd from source also have multiple
options. To build a tagged version, there is a docker build helper script that
allows users to
build lnd from source without needing to install golang.
That is also the preferred way to build and verify the reproducible builds that
are released by the team. See
release.md for more information about reproducible builds.

Finally, there is the option to build lnd fully manually. This requires more
tooling to be set up first but allows producing non-production (debug,
development) builds.

Installing a binary release

Downloading and installing an official release binary is recommended for use on
mainnet.
Visit the release page on GitHub
and select the latest version that does not have the "Pre-release" label set
(unless you explicitly want to help test a Release Candidate, RC).

Choose the package that best fits your operating system and system architecture.
It is recommended to choose 64bit versions over 32bit ones, if your operating
system supports both.

Extract the package and place the two binaries (lnd and lncli or lnd.exe
and lncli.exe on Windows) somewhere where the operating system can find them.

Building a tagged version with Docker

To use the Docker build helper, you need to have the following software
installed and set up on your machine:
- Docker
- make
- bash

To build a specific git tag of lnd, simply run the following steps (assuming
v0.x.y-beta is the tagged version to build):

shell
git clone https://github.com/lightningnetwork/lnd
cd lnd
git checkout v0.x.y-beta
make docker-release tag=v0.x.y-beta

This will create a directory called lnd-v0.x.y-beta that contains the release
binaries for all operating system and architecture pairs. A single pair can also
be selected by specifying the sys=linux-amd64 flag for example. See
release.md for more information on reproducible builds.

Building a development version from source

Building and installing lnd from source is only recommended for advanced users
and/or developers. Running the latest commit from the master branch is not
recommended for mainnet. The master branch can at times be unstable and
running your node off of it can prevent it to go back to a previous, stable
version if there are database migrations present.

In order to work with lnd, the
following build dependencies are required:

Installing Go

lnd is written in Go, with a minimum version of 1.25.11 (or, in case this
document gets out of date, whatever the Go version in the main go.mod file
requires). To install, run one of the following commands for your OS:

<details>
<summary>Linux (x86-64)</summary>

text
wget https://dl.google.com/go/go1.25.11.linux-amd64.tar.gz
echo "34f14304e856893f4ba30c2cacfe93906e9de7915c5f6aaaf3a81cdccd7ba30b go1.25.11.linux-amd64.tar.gz" | sha256sum --check

The command above should output go1.25.11.linux-amd64.tar.gz: OK. If it
doesn't, then the target REPO HAS BEEN MODIFIED, and you shouldn't install
this version of Go. If it matches, then proceed to install Go:

text
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.25.11.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

</details>

<details>
<summary>Linux (ARMv6)</summary>

text
wget https://dl.google.com/go/go1.25.11.linux-armv6l.tar.gz
echo "492d69badee59cae12e9a36282dfce94041bd4aac88fdddea575a7d99a2bd05d go1.25.11.linux-armv6l.tar.gz" | sha256sum --check

The command above should output go1.25.11.linux-armv6l.tar.gz: OK. If it
isn't, then the target REPO HAS BEEN MODIFIED, and you shouldn't install
this version of Go. If it matches, then proceed to install Go:

text
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.25.11.linux-armv6l.tar.gz
export PATH=$PATH:/usr/local/go/bin

</details>

<details>
<summary>macOS</summary>

First, install Homebrew if you don't already have it.

Then

text
brew install go

</details>

<details>
<summary>FreeBSD</summary>

text
pkg install go

Alternatively, one can download the pre-compiled binaries hosted on the
Golang download page. If one seeks to install
from source, then more detailed installation instructions can be found
here.
</details>

Important

At this point, you should set your $GOPATH environment variable, which
represents the path to your workspace. By default, $GOPATH is set to
~/go. You will also need to add $GOPATH/bin to your PATH. This ensures
that your shell will be able to detect the binaries you install.

shell
export GOPATH=~/go
export PATH=$PATH:$GOPATH/bin

---

We recommend placing the above in your .bashrc, .zshrc or in a setup script
so that you can avoid typing this every time you open a new terminal window.

Go modules

This project uses Go modules
to manage dependencies as well as to provide reproducible builds.

Usage of Go modules (with Go 1.13) means that you no longer need to clone
lnd into your $GOPATH for development purposes. Instead, your lnd
repo can now live anywhere!

---
Note: For mobile development, having the source code in $GOPATH is still
required due to a current limitation in
Go mobile. Take a look at the
documentation for building mobile libraries to learn more.

---

Installing lnd from source

With the preliminary steps completed, to install lnd, lncli, and all
related dependencies run the following commands:

shell
git clone https://github.com/lightningnetwork/lnd
cd lnd
make install

The command above will install the current _master_ branch of lnd. If you
wish to install a tagged release of lnd (as the master branch can at times be
unstable), then visit the release page to locate the latest
release
. Assuming the name
of the release is v0.x.x, then you can compile this release from source with
a small modification to the above command:

shell
git clone https://github.com/lightningnetwork/lnd
cd lnd
git checkout v0.x.x
make install

NOTE: Our instructions still use the $GOPATH directory from prior
versions of Go, but with Go 1.13, it's now possible for lnd to live
_anywhere_ on your file system.

For Windows WSL users, make will need to be referenced directly via
/usr/bin/make/, or alternatively by wrapping quotation marks around make,
like so:

shell
/usr/bin/make && /usr/bin/make install

"make" && "make" install

On FreeBSD, use gmake instead of make.

Alternatively, if one doesn't wish to use make, then the go commands can be
used directly:

shell
go install -v ./...

Tags

Release binaries and installations from source using make release-install
will have the following tags:

- autopilotrpc
- signrpc
- walletrpc
- chainrpc
- invoicesrpc
- neutrinorpc
- routerrpc
- watchtowerrpc
- monitoring (for Prometheus integration)
- peersrpc
- kvdb_postrgres
- kvdb_sqlite

The dev tag is used for development builds, and is not included in the
release builds & installation.

You can specify a custom set of tags when installing from source using the tags="" parameter. For example:

shell
make install tags="signrpc walletrpc routerrpc invoicesrpc"

Updating

To update your version of lnd to the latest version run the following
commands:

shell
cd $GOPATH/src/github.com/lightningnetwork/lnd
git pull
make clean && make && make install

On FreeBSD, use gmake instead of make.

Alternatively, if one doesn't wish to use make, then the go commands can be
used directly:

shell
cd $GOPATH/src/github.com/lightningnetwork/lnd
git pull
go install -v ./...

Tests

To check that lnd was installed properly run the following command:

shell
make check

This command requires bitcoind (almost any version should do) to be available
in the system's $PATH variable. Otherwise, some tests will fail.

Command-line completion for lncli

_Bash_: See contrib/lncli.bash-completion
_Fish_: Run: lncli fish-completion > $HOME/.config/fish/completions/lncli.fish

Available Backend Operating Modes

In order to run, lnd requires, that the user specify a chain backend. At the
time of writing of this document, there are three available chain backends:
btcd, neutrino, bitcoind. All including neutrino can run on mainnet with
an out of the box lnd instance. We don't require --txindex when running
with bitcoind or btcd but activating the txindex will generally make
lnd run faster. Note that since version 0.13 pruned nodes are supported,
although they cause performance penalty and higher network usage.

The set of arguments for each of the backend modes is as follows:

btcd Options


text
btcd:
--btcd.dir= The base directory that contains the node's data, logs, configuration file, etc. (default: /Users/<username>/Library/Application Support/Btcd)
--btcd.rpchost= The daemon's rpc listening address. If a port is omitted, then the default port for the selected chain parameters will be used. (default: localhost)
--btcd.rpcuser= Username for RPC connections
--btcd.rpcpass= Password for RPC connections
--btcd.rpccert= File containing the daemon's certificate file (default: /Users/<username>/Library/Application Support/Btcd/rpc.cert)
--btcd.rawrpccert= The raw bytes of the daemon's PEM-encoded certificate chain which will be used to authenticate the RPC connection.

Neutrino Options


text
neutrino:
-a, --neutrino.addpeer= Add a peer to connect with at startup
--neutrino.connect= Connect only to the specified peers at startup
--neutrino.maxpeers= Max number of inbound and outbound peers
--neutrino.banduration= How long to ban misbehaving peers. Valid time units are {s, m, h}. Minimum 1 second
--neutrino.banthreshold= Maximum allowed ban score before disconnecting and banning misbehaving peers.
--neutrino.useragentname= Used to help identify ourselves to other bitcoin peers.
--neutrino.useragentversion= Used to help identify ourselves to other bitcoin peers.

Bitcoind Options


text
bitcoind:
--bitcoind.dir= The base directory that contains the node's data, logs, configuration file, etc. (default: /Users/<username>/Library/Application Support/Bitcoin)
--bitcoind.rpchost= The daemon's rpc listening address. If a port is omitted, then the default port for the selected chain parameters will be used. (default: localhost)
--bitcoind.rpcuser= Username for RPC connections
--bitcoind.rpcpass= Password for RPC connections
--bitcoind.zmqpubrawblock= The address listening for ZMQ connections to deliver raw block notifications
--bitcoind.zmqpubrawtx= The address listening for ZMQ connections to deliver raw transaction notifications
--bitcoind.estimatemode= The fee estimate mode. Must be either "ECONOMICAL" or "CONSERVATIVE". (default: CONSERVATIVE)

Using btcd

Installing btcd

On FreeBSD, use gmake instead of make.

In order to be able to utilize the latest Taproot features, btcd version
v0.23.5
MUST be used.

To install btcd, run the following commands:

Install btcd:

shell
make btcd

Alternatively, you can install btcd directly from its
repo
.

Starting btcd

Running the following command will create rpc.cert and default btcd.conf.

shell
btcd --testnet --rpcuser=REPLACEME --rpcpass=REPLACEME

If you want to use lnd on testnet, btcd needs to first fully sync the
testnet blockchain. Depending on your hardware, this may take up to a few
hours. Note that adding --txindex is optional, as it will take longer to sync
the node, but then lnd will generally operate faster as it can hit the index
directly, rather than scanning blocks or BIP 158 filters for relevant items.

(NOTE: It may take several minutes to find segwit-enabled peers.)

While btcd is syncing you can check on its progress using btcd's getinfo
RPC command:

shell
btcctl --testnet --rpcuser=REPLACEME --rpcpass=REPLACEME getinfo
{
"version": 120000,
"protocolversion": 70002,
"blocks": 1114996,
"timeoffset": 0,
"connections": 7,
"proxy": "",
"difficulty": 422570.58270815,
"testnet": true,
"relayfee": 0.00001,
"errors": ""
}

Additionally, you can monitor btcd's logs to track its syncing progress in real
time.

You can test your btcd node's connectivity using the getpeerinfo command:

shell
btcctl --testnet --rpcuser=REPLACEME --rpcpass=REPLACEME getpeerinfo | more

Running lnd using the btcd backend

If you are on testnet, run this command after btcd has finished syncing.
Otherwise, replace --bitcoin.testnet with --bitcoin.simnet. If you are
installing lnd in preparation for the
tutorial, you may skip this step.

shell
lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug \
--btcd.rpcuser=kek --btcd.rpcpass=kek --externalip=X.X.X.X

Using Neutrino

In order to run lnd in its light client mode, you'll need to locate a
full-node which is capable of serving this new light client mode. lnd uses
BIP 157 and BIP
158
for its light client
mode. A public instance of such a node can be found at
faucet.lightning.community.

To run lnd in neutrino mode, run lnd with the following arguments, (swapping
in --bitcoin.simnet if needed), and also your own btcd node if available:

shell
lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug \
--bitcoin.node=neutrino --neutrino.connect=faucet.lightning.community


Using bitcoind

Note that adding -txindex is optional, as it will take longer to sync the
node, but then lnd will generally operate faster as it can hit the index
directly, rather than scanning blocks or BIP 158 filters for relevant items.

To configure your bitcoind backend for use with lnd, first complete and verify
the following:

- Since lnd uses
ZeroMQ to
interface with bitcoind, *your bitcoind installation must be compiled with
ZMQ*. Note that if you installed bitcoind from source and ZMQ was not present,
then ZMQ support will be disabled, and lnd will quit on a connection refused error.
If you installed bitcoind via Homebrew in the past ZMQ may not be included
(this has now been fixed
in the latest Homebrew recipe for bitcoin)
- Configure the bitcoind instance for ZMQ with -zmqpubrawblock and
-zmqpubrawtx. These options must each use their own unique address in order
to provide a reliable delivery of notifications (e.g.
-zmqpubrawblock=tcp://127.0.0.1:28332 and
-zmqpubrawtx=tcp://127.0.0.1:28333).
- Make sure the config setting -rpcserialversion in bitcoind is either set
to 1 or NOT used because bitcoind's default behaviour is already correct
(see bitcoin/issues/28730
for more info). Lightning depends on segwit transactions therefore we need
the witness data when querying the bitcoind backend for transaction details.

- Start bitcoind running against testnet, and let it complete a full sync with
the testnet chain (alternatively, use --bitcoind.regtest instead).

Here's a sample bitcoin.conf for use with lnd:

text
testnet=1
server=1
daemon=1
zmqpubrawblock=tcp://127.0.0.1:28332
zmqpubrawtx=tcp://127.0.0.1:28333

Once all of the above is complete, and you've confirmed bitcoind is fully
updated with the latest blocks on testnet, run the command below to launch
lnd with bitcoind as your backend (as with bitcoind, you can create an
lnd.conf to save these options, more info on that is described further
below):

shell
lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug \
--bitcoin.node=bitcoind --bitcoind.rpcuser=REPLACEME \
--bitcoind.rpcpass=REPLACEME \
--bitcoind.zmqpubrawblock=tcp://127.0.0.1:28332 \
--bitcoind.zmqpubrawtx=tcp://127.0.0.1:28333 \
--externalip=X.X.X.X

NOTE:
- The auth parameters rpcuser and rpcpass parameters can typically be
determined by lnd for a bitcoind instance running under the same user,
including when using cookie auth. In this case, you can exclude them from the
lnd options entirely.
- If you DO choose to explicitly pass the auth parameters in your lnd.conf or
command line options for lnd (bitcoind.rpcuser and bitcoind.rpcpass as
shown in example command above), you must also specify the
bitcoind.zmqpubrawblock and bitcoind.zmqpubrawtx options. Otherwise, lnd
will attempt to get the configuration from your bitcoin.conf.
- You must ensure the same addresses are used for the bitcoind.zmqpubrawblock
and bitcoind.zmqpubrawtx options passed to lnd as for the zmqpubrawblock
and zmqpubrawtx passed in the bitcoind options respectively.
- When running lnd and bitcoind on the same Windows machine, ensure you use
127.0.0.1, not localhost, for all configuration options that require a TCP/IP
host address. If you use "localhost" as the host name, you may see extremely
slow inter-process-communication between lnd and the bitcoind backend. If lnd
is experiencing this issue, you'll see "Waiting for chain backend to finish
sync, start_height=XXXXXX" as the last entry in the console or log output, and
lnd will appear to hang. Normal lnd output will quickly show multiple
messages like this as lnd consumes blocks from bitcoind.
- Don't connect more than two or three instances of lnd to bitcoind. With
the default bitcoind settings, having more than one instance of lnd, or
lnd plus any application that consumes the RPC could cause lnd to miss
crucial updates from the backend.
- The default fee estimate mode in bitcoind is CONSERVATIVE. You can set
bitcoind.estimatemode=ECONOMICAL to change it into ECONOMICAL. Furthermore,
if you start bitcoind in regtest, this configuration won't take any effect.


Creating a wallet


If lnd is being run for the first time, create a new wallet with:
shell
lncli create

This will prompt for a wallet password, and optionally a cipher seed
passphrase.

lnd will then print a 24 word cipher seed mnemonic, which can be used to
recover the wallet in case of data loss. The user should write this down and
keep in a safe place.

More information about managing wallets can be found in the wallet management
document
.

Macaroons

lnd's authentication system is called macaroons, which are decentralized
bearer credentials allowing for delegation, attenuation, and other cool
features. You can learn more about them in Alex Akselrod's writeup on
GitHub
.

Running lncli create to create a wallet, will by default generate
the admin.macaroon, read_only.macaroon, and macaroons.db
files that are used to authenticate
into lnd. They will be stored in the network directory (default:
lnddir/data/chain/bitcoin/mainnet) so that it's possible to use a distinct
password for mainnet, testnet, simnet, etc. Note that if you specified an
alternative data directory (via the --datadir argument), you will have to
additionally pass the updated location of the admin.macaroon file into lncli
using the --macaroonpath argument.

To disable macaroons for testing, pass the --no-macaroons flag into both
lnd and lncli.

Network Reachability

If you'd like to signal to other nodes on the network that you'll accept
incoming channels (as peers need to connect inbound to initiate a channel
funding workflow), then the --externalip flag should be set to your publicly
reachable IP address.

Simnet vs. Testnet Development

If you are doing local development, such as for the tutorial, you'll want to
start both btcd and lnd in the simnet mode. Simnet is similar to regtest
in that you'll be able to instantly mine blocks as needed to test lnd
locally. In order to start either daemon in the simnet mode use simnet
instead of testnet, adding the --bitcoin.simnet flag instead of the
--bitcoin.testnet flag.

Another relevant command line flag for local testing of new lnd developments
is the --debughtlc flag. When starting lnd with this flag, it'll be able to
automatically settle a special type of HTLC sent to it. This means that you
won't need to manually insert invoices in order to test payment connectivity.
To send this "special" HTLC type, include the --debugsend command at the end
of your sendpayment commands.


There are currently two primary ways to run lnd: one requires a local btcd
instance with the RPC service exposed, and the other uses a fully integrated
light client powered by neutrino.

For testing scenarios like integration tests where wallet security is not
important, the itest/lnd-itest binary can be used which uses very weak
password stretching for the wallet encryption and therefore starts up faster
than a production/mainnet/release build. The binary can be built by running
make build-itest.

Creating an lnd.conf (Optional)

Optionally, if you'd like to have a persistent configuration between lnd
launches, allowing you to simply type lnd --bitcoin.testnet --bitcoin.active
at the command line, you can create an lnd.conf.

On macOS, located at:
/Users/<username>/Library/Application Support/Lnd/lnd.conf

On Linux, located at:
~/.lnd/lnd.conf

Here's a sample lnd.conf for btcd to get you started:

text
[Application Options]
debuglevel=trace
maxpendingchannels=10

[Bitcoin]
bitcoin.active=1

Notice the [Bitcoin] section. This section houses the parameters for the
Bitcoin chain. See a more detailed sample config file available
here
and explore the other sections for node configuration, including [Btcd],
[Bitcoind] and [Neutrino] depending on which chain and node type you're
using.

---

MAKEFILE

Makefile
========

To build, verify, and install lnd from source, use the following
commands:

shell
$  make
$ make check
$ make install

The command make check requires bitcoind (almost any version should do) to
be available in the system's $PATH variable. Otherwise, some tests will
fail.

Developers
==========

This document specifies all commands available from lnd's Makefile.
The commands included handle:
- Installation of all go-related dependencies.
- Compilation and installation of lnd and lncli.
- Compilation and installation of btcd and btcctl.
- Running unit and integration suites.
- Testing, debugging, and flake hunting.
- Formatting and linting.

Commands
========

- all
- btcd
- build
- check
- clean
- default
- dep
- flake-unit
- flakehunter
- fmt
- install
- itest
- lint
- list
- rpc
- scratch
- travis
- unit
- unit-cover
- unit-race

all
-----
Compiles, tests, and installs lnd and lncli. Equivalent to
scratch check install.

btcd
------
Ensures that the [github.com/btcsuite/btcd][btcd] repository is checked out
locally. Lastly, installs the version of
[github.com/btcsuite/btcd][btcd] specified in Gopkg.toml

build
-------
Compiles the current source and vendor trees, creating ./lnd and
./lncli.

check
-------
Installs the version of [github.com/btcsuite/btcd][btcd] specified
in Gopkg.toml, then runs the unit tests followed by the integration
tests.

Related: unit itest

clean
-------
Removes compiled versions of both ./lnd and ./lncli, and removes the
vendor tree.

default
---------
Alias for scratch.

flake-unit
------------
Runs the unit test endlessly until a failure is detected.

Arguments:
- pkg=<package>
- case=<testcase>
- timeout=<timeout>

Related: unit

flakehunter
-------------
Runs the integration test suite endlessly until a failure is detected.

Arguments:
- icase=<itestcase>
- timeout=<timeout>

Related: itest

fmt
-----
Runs go fmt on the entire project.

install
---------
Copies the compiled lnd and lncli binaries into $GOPATH/bin.

itest
-------
Installs the version of [github.com/btcsuite/btcd][btcd] specified in
Gopkg.toml, builds the ./lnd and ./lncli binaries, then runs the
integration test suite.

Arguments:
- icase=<itestcase> (the snake_case version of the testcase name field in the testCases slice (i.e. sweep_coins), not the test func name)
- timeout=<timeout>

itest-parallel
------
Does the same as itest but splits the total set of tests into
NUM_ITEST_TRANCHES tranches (currently set to 6 by default, can be overwritten
by setting tranches=Y) and runs them in parallel.

Arguments:
- icase=<itestcase>: The snake_case version of the testcase name field in the
testCases slice (i.e. sweep_coins, not the test func name) or any regular
expression describing a set of tests.
- timeout=<timeout>
- tranches=<number_of_tranches>: The number of parts/tranches to split the
total set of tests into.
- parallel=<number_of_threads>: The number of threads to run in parallel. Must
be greater or equal to tranches, otherwise undefined behavior is expected.

flakehunter-parallel
------
Runs the test specified by icase simultaneously parallel (default=6) times
until an error occurs. Useful for hunting flakes.

Example:

shell
$  make flakehunter-parallel icase='(data_loss_protection|channel_backup)' backend=neutrino

lint
------
Ensures that [gopkg.in/alecthomas/gometalinter.v1][gometalinter] is
installed, then lints the project.

list
------
Lists all known make targets.

rpc
-----
Compiles the lnrpc proto files.

sample-conf-check
-------------------
Checks whether all required options of lnd --help are included in sample-lnd.conf and that the default values of lnd --help are also mentioned correctly.

scratch
---------
Compiles all dependencies and builds the ./lnd and ./lncli binaries.
Equivalent to lint btcd
unit-race.

unit
------
Runs the unit test suite. By default, this will run all known unit tests.

Arguments:
- pkg=<package>
- case=<testcase>
- timeout=<timeout>
- log="stdlog[ <log-level>]" prints logs to stdout
- <log-level> can be info (default), debug, trace, warn, error, critical, or off

unit-cover
------------
Runs the unit test suite with test coverage, compiling the statistics in
profile.cov.

Arguments:
- pkg=<package>
- case=<testcase>
- timeout=<timeout>
- log="stdlog[ <log-level>]" prints logs to stdout
- <log-level> can be info (default), debug, trace, warn, error, critical, or off

Related: unit

unit-race
-----------
Runs the unit test suite with go's race detector.

Arguments:
- pkg=<package>
- case=<testcase>
- timeout=<timeout>
- log="stdlog[ <log-level>]" prints logs to stdout
- <log-level> can be info (default), debug, trace, warn, error, critical, or off

Related: unit

[btcd]: https://github.com/btcsuite/btcd (github.com/btcsuite/btcd")
[gometalinter]: https://gopkg.in/alecthomas/gometalinter.v1 (gopkg.in/alecthomas/gometalinter.v1)

---

SphinxReplayDB

Sphinx Onion Routing in Lightning Network

The Lightning Network uses a Sphinx-based onion message protocol to send
messages across the Lightning Network. These messages have the property that a
node which is part of such a message and for example forwards such an onion
message cannot learn about the destination of this whole packet. It only knows
the predecessor and the successor of the message. In other words, it only knows
where the message came from and where it needs to be forwarded. This makes the
message protocol of the Lightning Network very private. Only the sender
(creator of the whole onion packet) knows the whole route of the packet. Also,
the receiver has no idea from which node the message originated.

This diagram illustrates how Sphinx onion routing works in the Lightning
Network, showing the privacy properties at each hop:

ascii
Alice (Sender)
|
| Knows Full Route: Alice → Bob → Carol → David → Eve
|
v
Bob (Hop 1)
|
| Knows: Alice → Carol
|
v
Carol (Hop 2)
|
| Knows: Bob → David
|
v
David (Hop 3)
|
| Knows: Carol → Eve
|
v
Eve (Receiver)
|
| Knows: From David
|
v

Privacy Properties:
- Each hop only knows its immediate neighbors
- Receiver doesn't know the original sender
- Only sender knows the complete route
- Each hop peels one layer of the onion

Privacy Properties

1. Hop Privacy: Each intermediate node only knows:
- The previous hop (where the packet came from)
- The next hop (where to forward the packet)
- Cannot see the full route or final destination

2. Sender Privacy: Only the sender (Alice) knows:
- The complete route
- All intermediate nodes
- The final destination

3. Receiver Privacy: The receiver (Eve) only knows:
- The immediate previous hop (David)
- Cannot determine the original sender

4. Onion Encryption: Each hop peels one layer of encryption, revealing only
the next hop's information


The detailed mechanics are described in BOLT 04


Replaying Onion Packets

The sphinx protocol
clearly states that implementations of the protocol should guard against
replay attacks.
Replaying (resending) onion packets into the network can compromise the
privacy guarantees promised by the protocol. So it is crucial for node
participants to not forward replayed onion packets to guard the privacy of all
network participants. Compared to the original Sphinx protocol, the Lightning
Network has an improved replay protection in place especially when it comes to
forwarding HTLCs, which are different from Onion Messages introduced later on
because HTLCs lock a payment to the particular onion packet. Therefore, sending
HTLCs packets comes with a cost. Moreover, every HTLC has an expiry date, also
called CLTV (absolute locktime), which prevents the replay of packets that have
already expired. In addition to the CLTV expiry of a packet, every HTLC onion
packet commits to the payment hash in the HMAC of the message (to be precise,
the associated data), so this prevents an attacker from attaching an old onion
packet to a new payment hash, which now also comes with the risk for the
attacker that he does not only have to lock funds when replaying an onion
packet but he also risks that the next node settles the HTLC because it already
knows the preimage of the HTLC. Although the attack comes with a high cost,
Lightning implementations should prevent replayed onion packets from
propagating through the network to safeguard the privacy for every network
participant.

Replay attack

An attacker could execute a re-injection attack by strategically positioning
well-connected nodes within the network, ensuring they participate in a high
volume of payment forwarding. In this scenario, the attacker's nodes act as
forwarding intermediaries. These malicious nodes would collect passing onion
packets and concurrently monitor network gossip. Upon observing a successful
payment forward, the attacker could re-inject the captured onion message. By
then monitoring channel updates or closures from the neighboring node, the
attacker aims to determine if that channel was part of the payment path.
Furthermore, if the neighboring node, lacking a suitable outgoing channel,
instead settles the payment (i.e., acts as the final recipient), it becomes
highly probable that it was the ultimate receiving node. It's important to note,
however, that such an attack carries significant costs.

Replay Protection in LND

In LND there are two interchanging DB names which save information of
those onion packets to prevent replays from happening. They are called
Sphinx-Replay-DB or Decayed-Log-DB.
Currently LND only implements onion messages which are tied to payments
(i.e. HTLCs), and as explained above, HTLCs expire after an absolute lock time
and therefore can be garbage collected because they will not be forwarded by
nodes anyways as that would entail a risk of losing funds.

Sphinx replay protection storage for the different backends:

1. When running LND with the BBolt backend the db is called: sphinxreplay.db.

2. For Postgres the table is called: decayedlogdb_kv.

3. For SQLite the table is called decayedlogdb_kv and is part of the
channel.sqlite file.

LND employs an internal stores to prevent replay attacks. The internal store
maintains a record of each onion packet's shared secret alongside its absolute
timelock (CLTV). This enables LND to efficiently identify and discard replayed
packets that present an already-seen shared secret.

This store is subject to garbage collection, ensuring it does not impose a
sustained burden on memory resources. After the CLTV values expiry, the entries
can be removed as described earlier.

#### What happens if LND encounters a replayed onion HTLC packet?

When LND encounters a replay, it will reject the HTLC and it will not signal a
specific error that a replay occurred which would reveal that the node was
indeed part of the route in the onion packet. LND will reject the HTLC and
respond with the message invalid_onion_version. See
BOLT 04


Other attacks

In the Lightning Network, the Sphinx protocol effectively mitigates
tagging attacks. This is due to the fact that each onion packet incorporates
a cryptographic HMAC (Hash-based Message Authentication Code), which is derived
from the packet's encrypted contents. Consequently, any attempt to tamper with
the packet's data would render its HMAC invalid, causing the packet to be
discarded by honest nodes.

---

Backport Workflow

Automated Backport Workflow

This document describes the automated backport workflow for the LND project.

Table of Contents

1. Overview
2. How to Use
3. Workflow Triggers
4. Label Format
5. Workflow Steps
6. Handling Conflicts
7. Multiple Backports
8. Technical Details
9. Troubleshooting

Overview

The automated backport workflow simplifies the process of backporting merged PRs from the master branch to release branches (e.g., v0.20.x-branch, v0.19.x-branch).

Instead of manually creating branches, cherry-picking commits, and creating PRs, maintainers can simply add a label to the master PR, and the workflow handles the rest.

How to Use

Basic Usage

1. Merge a PR to master (or have it already merged)
2. Add a backport label in the format: backport-v<version>-branch
- Example: backport-v0.20.x-branch
3. The workflow automatically:
- Validates the target branch exists
- Cherry-picks the commits
- Creates a new PR targeting the release branch
- Adds the no-changelog label (since release notes are in the master PR)

Example Scenario

text
Day 1, 10:00 - PR #1234 "Fix critical bug" merged to master
Day 1, 10:30 - Add label: backport-v0.20.x-branch
Day 1, 10:31 - Workflow creates PR #1235 automatically
Title: "[v0.20.x-branch] Backport #1234: Fix critical bug"
Base: v0.20.x-branch
Labels: no-changelog
Day 1, 14:00 - Maintainer reviews and merges PR #1235

Workflow Triggers

The backport workflow triggers in two scenarios:

Scenario 1: Label Before Merge

text
1. Open PR #1234
2. Add label: backport-v0.20.x-branch
3. Review and approve PR
4. Merge PR #1234
5. → Workflow triggers on PR close event
6. → Backport PR #1235 created immediately

Scenario 2: Label After Merge

text
1. Open PR #1234
2. Review, approve, and merge PR #1234
3. Later... decide it needs backporting
4. Add label: backport-v0.20.x-branch
5. → Workflow triggers on label event
6. → Backport PR #1235 created immediately

Both scenarios work identically.

Label Format

Valid Labels

Labels must start with backport-v to trigger the workflow:

- ✅ backport-v0.20.x-branch → backports to v0.20.x-branch
- ✅ backport-v0.19.x-branch → backports to v0.19.x-branch
- ✅ backport-v0.18.x-beta-branch → backports to v0.18.x-beta-branch

Invalid Labels (Will NOT Trigger)

These labels are ignored by the workflow:

- ❌ backport candidate - discussion label only
- ❌ backport-candidate - doesn't start with backport-v
- ❌ backport-needed - doesn't start with backport-v
- ❌ needs-backport - wrong prefix

This allows you to use discussion labels without accidentally triggering backports.

Label to Branch Mapping

The label format directly maps to the target branch:

text
Label: backport-v0.20.x-branch
↓ (removes "backport-" prefix)
Branch: v0.20.x-branch

Workflow Steps

The workflow executes the following steps when triggered:

Step 1: Checkout Repository

yaml
- Fetches the full git history
- Checks out the base branch (usually master)

Step 2: Validate Target Branches

bash
For each backport label:
1. Extract branch name from label
backport-v0.20.x-branch → v0.20.x-branch

2. Check if branch exists in remote repository
git ls-remote --heads origin v0.20.x-branch

3. If branch doesn't exist:
- Log error message
- Add branch to missing_branches list

4. After checking all labels:
- If any branches are missing → FAIL workflow
- If all branches exist → Continue

Example validation output:

text
All labels: ["backport-v0.20.x-branch", "bug-fix", "backport-v0.19.x-branch"]
Found backport labels:
backport-v0.20.x-branch
backport-v0.19.x-branch

Checking if branch exists: v0.20.x-branch
✓ Branch 'v0.20.x-branch' exists

Checking if branch exists: v0.19.x-branch
✓ Branch 'v0.19.x-branch' exists

✓ All target branches validated successfully

Step 3: Create Backport PRs

For each valid backport label, the workflow:

1. Creates a new branch
- Branch name: backport-<pr-number>-to-<target-branch>
- Example: backport-1234-to-v0.20.x-branch
- Based on: the target release branch

2. Cherry-picks commits
- Uses git cherry-pick (not merge or rebase)
- Cherry-picks all commits from the original PR
- Preserves commit messages and authorship
- Skips merge commits

3. Creates a new PR
- Title: [v0.20.x-branch] Backport #1234: <original-title>
- Base branch: v0.20.x-branch
- Head branch: backport-1234-to-v0.20.x-branch
- Labels: no-changelog (automatically added)

4. PR Description

markdown
Backport of #1234

Original PR: https://github.com/lightningnetwork/lnd/pull/1234

---

[Original PR description here]

Handling Conflicts

The workflow handles merge conflicts gracefully using the draft_commit_conflicts strategy.

When Cherry-pick Succeeds

text
1. Cherry-pick completes cleanly
2. Creates regular PR (ready for review)
3. PR is NOT in draft mode
4. Maintainer can review and merge immediately

When Cherry-pick Has Conflicts

text
1. Cherry-pick encounters conflicts
2. Workflow commits the conflict markers:
<<<<<<< HEAD
[code from release branch]
=======
[code from master PR]
>>>>>>> commit-hash

3. Creates DRAFT PR
4. PR description indicates there were conflicts
5. Manual resolution required:
a. git fetch origin
b. git checkout backport-1234-to-v0.20.x-branch
c. Resolve conflicts in affected files
d. git add <resolved-files>
e. git commit -m "Resolve backport conflicts"
f. git push origin backport-1234-to-v0.20.x-branch
g. Mark PR as "Ready for review" in GitHub UI
6. Maintainer reviews and merges

Conflict Resolution Best Practices

- Review the original PR: Understand what changed
- Check the release branch: Understand why conflicts occurred
- Test after resolving: Run tests locally before pushing
- Update commit message: Explain what conflicts were resolved and how
- Request review: Don't merge without review, even after resolving conflicts

Multiple Backports

You can backport to multiple release branches simultaneously by adding multiple labels.

Example: Backport to Two Branches

text
PR #1234 merged with labels:
- backport-v0.20.x-branch
- backport-v0.19.x-branch

Workflow creates TWO backport PRs:

PR #1235:
Title: [v0.20.x-branch] Backport #1234: Original title
Base: v0.20.x-branch
Labels: no-changelog

PR #1236:
Title: [v0.19.x-branch] Backport #1234: Original title
Base: v0.19.x-branch
Labels: no-changelog

Independent Processing

Each backport is processed independently:

- One backport may succeed while another has conflicts
- One backport may fail validation while another succeeds
- Each backport creates a separate branch and PR
- Each backport PR is reviewed and merged independently

Example with Mixed Results

text
PR #1234 with labels:
- backport-v0.20.x-branch → ✓ Clean cherry-pick, regular PR created
- backport-v0.19.x-branch → ✗ Conflicts, draft PR created
- backport-v0.99.x-branch → ✗ Branch doesn't exist, workflow fails

In this case:
1. PR #1235 to v0.20.x-branch is ready for review
2. PR #1236 to v0.19.x-branch needs conflict resolution
3. No PR created for v0.99.x-branch (validation failed)
4. Remove the incorrect label and add the correct one. The workflow will
re-trigger when you add the new label.

Technical Details

Workflow File

Location: .github/workflows/backport.yml

Trigger Events

yaml
on:
pull_request_target:
types: [closed, labeled]

- closed: Triggers when PR is closed (checks if merged)
- labeled: Triggers when any label is added

Permissions Required

yaml
permissions:
contents: write # Create branches and commits
pull-requests: write # Create and manage PRs
issues: read # Read PR metadata

Workflow Condition

yaml
if: |
github.event.pull_request.merged == true &&
contains(join(github.event.pull_request.labels.*.name, ','), 'backport-v')

Only runs when:
1. PR is actually merged (not just closed)
2. At least one label contains backport-v

Label Pattern

yaml
label_pattern: '^backport-(v.+)$'

Regex explanation:
- ^ - Start of string
- backport- - Literal text
- (v.+) - Capture group: "v" followed by one or more characters
- $ - End of string

Examples:
- backport-v0.20.x-branch → matches, captures v0.20.x-branch
- backport-v0.19.x-branch → matches, captures v0.19.x-branch
- backport-candidate → doesn't match (no "v" after dash)

Cherry-pick Strategy

yaml
merge_commits: skip

- Uses git cherry-pick for clean history
- Skips merge commits (only cherry-picks actual changes)
- Preserves original commit messages and authorship
- Maintains PGP signatures where present

Conflict Resolution Strategy

yaml
experimental: |
conflict_resolution: draft_commit_conflicts

- Creates draft PR with conflict markers
- Allows manual resolution
- Preserves all context and metadata

Troubleshooting

Problem: Workflow Doesn't Trigger

Symptoms:
- Added backport-v0.20.x-branch label
- No workflow run appears in Actions tab

Possible causes:

1. Label format is wrong
- ❌ backport-0.20.x-branch (missing "v")
- ✅ backport-v0.20.x-branch

2. PR is not merged
- Workflow only runs on merged PRs
- Check PR status

3. Workflow is disabled
- Check .github/workflows/backport.yml exists
- Check workflow is enabled in Settings → Actions

Problem: Workflow Fails with "Branch doesn't exist"

Error message:

text
Error: Target branch 'v0.21.x-branch' does not exist (from label 'backport-v0.21.x-branch')
Error: The following target branches do not exist: v0.21.x-branch
Error: Please ensure the branch exists before adding the backport label

Solution:

1. Verify branch name:

bash
git ls-remote --heads origin | grep v0.21

2. Check available release branches:

bash
git branch -r | grep origin/v0 | grep -v fork

3. Fix the label:
- Remove incorrect label
- Add correct label with existing branch name

Problem: Cherry-pick Has Conflicts

Symptoms:
- Backport PR created as DRAFT
- PR description mentions conflicts
- Branch has files with conflict markers

Solution:

1. Fetch and checkout the branch:

bash
git fetch origin
git checkout backport-1234-to-v0.20.x-branch

2. Find conflicted files:

bash
grep -r "<<<<<<< HEAD" .

3. Resolve each conflict:
- Open the file in an editor
- Review both versions:

text
<<<<<<< HEAD
[Release branch version]
=======
[Master PR version]
>>>>>>> commit-hash

- Choose the correct code or merge both
- Remove conflict markers

4. Commit the resolution:

bash
git add <resolved-files>
git commit -m "Resolve backport conflicts for PR #1234

Conflicts occurred due to [explanation].
Resolution: [describe what you did]"
git push origin backport-1234-to-v0.20.x-branch

5. Mark PR ready for review:
- Go to PR on GitHub
- Click "Ready for review"

Problem: Multiple Labels but Only One Backport Created

Symptoms:
- Added backport-v0.20.x-branch and backport-v0.19.x-branch
- Only one PR created

Possible causes:

1. One label format is wrong
- Check both labels start with backport-v
- Fix incorrect label, workflow will retry

2. One branch doesn't exist
- Check workflow logs for validation errors
- Verify both branches exist

3. Workflow still running
- Check Actions tab for in-progress runs
- Wait for workflow to complete

Problem: Backport PR Missing no-changelog Label

Symptoms:
- Backport PR created successfully
- CI fails on changelog check

Solution:

1. Manually add the label:
- Add no-changelog label to the backport PR

2. Check workflow configuration:
- Verify .github/workflows/backport.yml has:

yaml
add_labels: no-changelog

3. Re-run the workflow:
- Remove and re-add the backport label on original PR
- New backport PR will have correct label

Getting Help

If you encounter issues not covered here:

1. Check workflow logs:
- Go to Actions tab
- Click on the failed workflow run
- Review step-by-step logs

2. Check workflow file:
- .github/workflows/backport.yml
- Verify configuration matches this documentation

3. Manual backport:
- If automated backport fails repeatedly, you can backport manually:

bash
# Create branch from target release branch
git checkout v0.20.x-branch
git checkout -b manual-backport-123-to-v0.20.x

# Cherry-pick commits from the original PR
git cherry-pick <commit-hash>

# Resolve any conflicts, then:
git add .
git commit
git push origin manual-backport-123-to-v0.20.x

# Create PR targeting v0.20.x-branch with no-changelog label
gh pr create --base v0.20.x-branch --label no-changelog

4. Report issues:
- If you find a bug in the workflow
- Open an issue with workflow logs and details

---

Benchmark Perf Loop

The Go Performance Optimization Loop: From Benchmarks to Zero Allocations

When optimizing Go code for performance, particularly in hot paths like
cryptographic operations or protocol handling, the journey from identifying
bottlenecks to achieving zero-allocation code follows a well-defined
methodology. This document walks through the complete optimization loop using
Go's built-in tooling, demonstrating how to systematically eliminate allocations
and improve performance.

Understanding the Performance Baseline

The first step in any optimization effort is establishing a measurable baseline.
Go's benchmark framework provides the foundation for this measurement. When
writing benchmarks for allocation-sensitive code, always include a call to
b.ReportAllocs() before b.ResetTimer(). This ensures the benchmark reports
both timing and allocation statistics without including setup costs in the
measurements.

Consider a benchmark that exercises a cryptographic write path with the largest
possible message size to stress test allocations:

go
func BenchmarkWriteMessage(b *testing.B) {
// Setup code here...

b.ReportAllocs() // Essential for tracking allocations
b.ResetTimer()

for i := 0; i < b.N; i++ {
// Hot path being measured
}
}

Running the benchmark with go test -bench=BenchmarkWriteMessage -benchmem
-count=10
provides statistical confidence through multiple runs. The
-benchmem flag is redundant if you've called b.ReportAllocs(), but it
doesn't hurt to include it explicitly. The output reveals three critical
metrics: nanoseconds per operation, bytes allocated per operation, and the
number of distinct allocations per operation.

Profiling Memory Allocations

Once you have baseline measurements showing undesirable allocations, the next
phase involves profiling to understand where these allocations originate.
Generate memory profiles during benchmark execution using:

text
go test -bench=BenchmarkWriteMessage -memprofile=mem.prof -cpuprofile=cpu.prof -count=1

The resulting profile can be analyzed through several lenses. To see which
functions allocate the most memory by total bytes, use:
go tool pprof -alloc_space -top mem.prof.

However, for understanding allocation frequency rather than size, go tool pprof -alloc_objects -top mem.prof often provides more actionable insights, especially when hunting small but frequent allocations.

Here's what the allocation object analysis might reveal:

text
$ go tool pprof -alloc_objects -top mem.prof | head -20
File: brontide.test
Type: alloc_objects
Time: Aug 30, 2024 at 2:07pm (WEST)
Showing nodes accounting for 39254, 100% of 39272 total
Dropped 32 nodes (cum <= 196)
flat flat% sum% cum cum%
32768 83.44% 83.44% 32768 83.44% github.com/lightningnetwork/lnd/brontide.(*cipherState).Encrypt
5461 13.91% 97.34% 5461 13.91% runtime.acquireSudog
1025 2.61% 100% 1025 2.61% runtime.allocm

This output immediately shows that cipherState.Encrypt is responsible for 83%
of allocations by count, focusing our investigation.

The most powerful profiling technique involves examining allocations at the
source line level. Running go tool pprof -list 'FunctionName' mem.prof shows
exactly which lines within a function trigger heap allocations:

text
$ go tool pprof -list 'cipherState.*Encrypt' mem.prof
Total: 8.73MB
ROUTINE ======================== github.com/lightningnetwork/lnd/brontide.(*cipherState).Encrypt
512.01kB 512.01kB (flat, cum) 5.73% of Total
. . 111:func (c *cipherState) Encrypt(associatedData, cipherText, plainText []byte) []byte {
. . 112: defer func() {
. . 113: c.nonce++
. . 114:
. . 115: if c.nonce == keyRotationInterval {
. . 116: c.rotateKey()
. . 117: }
. . 118: }()
. . 119:
512.01kB 512.01kB 120: var nonce [12]byte
. . 121: binary.LittleEndian.PutUint64(nonce[4:], c.nonce)
. . 122:
. . 123: return c.cipher.Seal(cipherText, nonce[:], plainText, associatedData)

This granular view reveals that line 120, a seemingly innocent stack array
declaration, is allocating 512KB total across all benchmark iterations.

CPU Profiling for Hot Spots

While memory allocations often dominate optimization efforts, CPU profiling
reveals where computational time is spent. The CPU profile generated alongside
the memory profile provides complementary insights:

text
$ go tool pprof -top cpu.prof | head -15
File: brontide.test
Type: cpu
Time: Aug 30, 2024 at 2:07pm (WEST)
Duration: 1.8s, Total samples = 1.71s (94.40%)
Showing nodes accounting for 1.65s, 96.49% of 1.71s total
flat flat% sum% cum cum%
0.51s 29.82% 29.82% 0.51s 29.82% vendor/golang.org/x/crypto/chacha20poly1305.(*chacha20poly1305).sealGeneric
0.28s 16.37% 46.20% 0.28s 16.37% vendor/golang.org/x/crypto/internal/poly1305.updateGeneric
0.24s 14.04% 60.23% 0.24s 14.04% vendor/golang.org/x/crypto/chacha20.(*Cipher).XORKeyStream
0.19s 11.11% 71.35% 0.19s 11.11% runtime.memmove
0.12s 7.02% 78.36% 0.86s 50.29% github.com/lightningnetwork/lnd/brontide.(*cipherState).Encrypt

This profile shows that cryptographic operations dominate CPU usage, which is
expected. However, note the presence of runtime.memmove at 11% - this often
indicates unnecessary copying that could be eliminated through careful buffer
management.

For line-level CPU analysis of a specific function:

text
$ go tool pprof -list 'WriteMessage' cpu.prof
Total: 1.71s
ROUTINE ======================== github.com/lightningnetwork/lnd/brontide.(*Machine).WriteMessage
10ms 1.21s (flat, cum) 70.76% of Total
. . 734:func (b *Machine) WriteMessage(p []byte) error {
. . 735: if len(p) > math.MaxUint16 {
. . 736: return ErrMaxMessageLengthExceeded
. . 737: }
. . 738:
. 10ms 739: if len(b.nextHeaderSend) > 0 || len(b.nextBodySend) > 0 {
. . 740: return ErrMessageNotFlushed
. . 741: }
. . 742:
10ms 10ms 743: fullLength := uint16(len(p))
. . 744: var pktLen [2]byte
. 10ms 745: binary.BigEndian.PutUint16(pktLen[:], fullLength)
. . 746:
. 580ms 747: b.nextHeaderSend = b.sendCipher.Encrypt(nil, nil, pktLen[:])
. 600ms 748: b.nextBodySend = b.sendCipher.Encrypt(nil, nil, p)

This shows that the two Encrypt calls consume virtually all the CPU time in
WriteMessage, confirming that cryptographic operations are the bottleneck
rather than the message handling logic itself.

Understanding Escape Analysis

When the profiler indicates that seemingly stack-local variables are being heap
allocated, escape analysis becomes your next investigative tool. The Go
compiler's escape analysis determines whether variables can remain on the stack
or must be moved to the heap. Variables escape to the heap when their lifetime
extends beyond the function that creates them or when the compiler cannot prove
they remain local.

To see the compiler's escape analysis decisions, build with verbose flags:

text
go build -gcflags="-m" ./...

For more detailed output including the reasons for escape, use -m=2. The
output reveals escape flows, showing exactly why variables move to the heap.
When investigating specific escapes, you can grep for the variable in question:

text
$ go build -gcflags="-m=2" ./... 2>&1 | grep -A2 -B2 "nonce escapes"
./noise.go:183:17: &errors.errorString{...} does not escape
./noise.go:183:17: new(chacha20poly1305.chacha20poly1305) escapes to heap
./noise.go:120:6: nonce escapes to heap:
./noise.go:120:6: flow: {heap} = &nonce:
./noise.go:120:6: from nonce (address-of) at ./noise.go:123:40
--
./noise.go:469:21: &keychain.PrivKeyECDH{...} escapes to heap
./noise.go:483:40: []byte{} escapes to heap
./noise.go:138:6: nonce escapes to heap:
./noise.go:138:6: flow: {heap} = &nonce:
./noise.go:138:6: from nonce (address-of) at ./noise.go:141:39

This output shows the exact flow analysis: the nonce array escapes because its
address is taken when creating a slice (nonce[:]) and passed to a function
that the compiler cannot fully analyze.

Common causes include passing pointers to interfaces, storing references in
heap-allocated structures, or passing slices of stack arrays to functions that
might retain them. A particularly instructive example is the seemingly innocent
pattern of passing a stack array to a function:

go
var nonce [12]byte
binary.LittleEndian.PutUint64(nonce[4:], counter)
return cipher.Seal(ciphertext, nonce[:], plaintext, nil)

Here, nonce[:] creates a slice backed by the stack array, but if the compiler
cannot prove that cipher.Seal won't retain a reference to this slice, the
entire array escapes to the heap.

The Optimization Strategy

Armed with profiling data and escape analysis insights, the optimization phase
begins. The general strategy for eliminating allocations follows a predictable
pattern: move temporary buffers from function scope to longer-lived structures,
typically as fields in the enclosing type. This transformation changes
allocation from per-operation to per-instance.

For the nonce example above, the optimization involves adding a buffer field to
the containing struct:

go
type cipherState struct {
// ... other fields ...
nonceBuffer [12]byte // Reusable buffer to avoid allocations
}

func (c *cipherState) Encrypt(...) []byte {
binary.LittleEndian.PutUint64(c.nonceBuffer[4:], c.nonce)
return c.cipher.Seal(ciphertext, c.nonceBuffer[:], plaintext, nil)
}

This pattern extends to any temporary buffer. When dealing with variable-sized
data up to a known maximum, pre-allocate buffers at that maximum size and slice
into them as needed. The key insight is using the three-index slice notation to
control capacity separately from length:

go
// Pre-allocated: var buffer [maxSize]byte

// Creating a zero-length slice with full capacity for append:
slice := buffer[:0] // length=0, capacity=maxSize

Verification and Iteration

After implementing optimizations, the cycle returns to benchmarking. Run the
same benchmark to measure improvement, but don't stop at the aggregate numbers.
Generate new profiles to verify that specific allocations have been eliminated
and to identify any remaining allocation sites.

The benchstat tool provides statistical comparison between runs:

text
go test -bench=BenchmarkWriteMessage -count=10 > old.txt

Make optimizations


go test -bench=BenchmarkWriteMessage -count=10 > new.txt
benchstat old.txt new.txt

This comparison reveals not just whether performance improved, but whether the
improvement is statistically significant. A typical benchstat output after
successful optimization looks like:

text
goos: darwin
goarch: arm64
pkg: github.com/lightningnetwork/lnd/brontide
cpu: Apple M4 Max
│ old.txt │ new.txt │
│ sec/op │ sec/op vs base │
WriteMessage-16 50.34µ ± 1% 46.48µ ± 0% -7.68% (p=0.000 n=10)

│ old.txt │ new.txt │
│ B/op │ B/op vs base │
WriteMessage-16 73788.000 ± 0% 2.000 ± 0% -100.00% (p=0.000 n=10)

│ old.txt │ new.txt │
│ allocs/op │ allocs/op vs base │
WriteMessage-16 5.000 ± 0% 0.000 ± 0% -100.00% (p=0.000 n=10)

The key metrics to examine are:
- The percentage change (vs base column) showing the magnitude of improvement

- The p-value (p=0.000) indicating statistical significance - values below 0.05
suggest real improvements rather than noise

- The variance (± percentages) showing consistency across runs

This output confirms both a 7.68% speed improvement and complete elimination of
allocations, with high statistical confidence.

If allocations remain, the cycle continues. Profile again, identify the source,
understand why the allocation occurs through escape analysis, and apply the
appropriate optimization pattern. Each iteration should show measurable progress
toward the goal of zero allocations in the hot path.

Advanced Techniques

When standard profiling doesn't reveal the allocation source, more advanced
techniques come into play. Memory profiling with different granularities can
help. Instead of looking at total allocations, examine the profile with go tool
pprof -sample_index=alloc_objects
to focus on allocation count rather than
size. This distinction matters when hunting for small, frequent allocations that
might not show up prominently in byte-focused views.

Additional pprof commands that prove invaluable during optimization:

bash

Interactive mode for exploring the profile


go tool pprof mem.prof
(pprof) top10 # Show top 10 memory consumers
(pprof) list regexp # List functions matching regexp
(pprof) web # Open visual graph in browser

Generate a flame graph for visual analysis


go tool pprof -http=:8080 mem.prof

Compare two profiles directly


go tool pprof -base=old.prof new.prof

Show allocations only from specific packages


go tool pprof -focus=github.com/lightningnetwork/lnd/brontide mem.prof

Check for specific small allocations


go tool pprof -alloc_space -inuse_space mem.prof

When dealing with elusive allocations, checking what might be escaping to heap
can be done more surgically:

bash

Check specific function or type for escapes


go build -gcflags="-m" 2>&1 | grep -E "(YourType|yourFunc)"

See all heap allocations in a package


go build -gcflags="-m" 2>&1 | grep "moved to heap"

Check which variables are confirmed to stay on the stack


go build -gcflags="-m=2" 2>&1 | grep "does not escape"

For particularly elusive allocations, instrumenting the code with runtime memory
statistics can provide real-time feedback:

go
var m runtime.MemStats
runtime.ReadMemStats(&m)
before := m.Alloc
// Operation being measured
runtime.ReadMemStats(&m)
allocated := m.Alloc - before

While this approach adds overhead and shouldn't be used in production, it can
help isolate allocations to specific code sections during development.

The Zero-Allocation Goal

Achieving zero allocations in hot paths represents more than just a performance
optimization. It provides predictable latency, reduces garbage collection
pressure, and improves overall system behavior under load. In systems handling
thousands of operations per second, the difference between five allocations per
operation and zero can mean the difference between smooth operation and periodic
latency spikes during garbage collection.

The journey from initial benchmark to zero-allocation code demonstrates the
power of Go's built-in tooling. By systematically applying the
benchmark-profile-optimize loop, even complex code paths can be transformed into
allocation-free implementations. The key lies not in guessing or premature
optimization, but in measuring, understanding, and methodically addressing each
allocation source.

It's best to focus optimization efforts on true hot paths identified through
production profiling or realistic load testing. The techniques described here
provide the tools to achieve zero-allocation code when it matters, but the
judgment of when to apply them remains a critical engineering decision.

---

Code Contribution Guidelines

Table of Contents


1. Overview
2. Minimum Recommended Skillset
3. Required Reading
4. Substantial contributions only
5. New Contributors
6. Development Practices
1. Share Early, Share Often
1. Development Guidelines
7. Code Approval Process
1. Code Review
1. Rework Code (if needed)
1. Acceptance
1. Backporting Changes
1. Review Bot
8. Contribution Standards
1. Contribution Checklist
1. Licensing of Contributions

Overview

Developing cryptocurrencies is an exciting endeavor that touches a wide variety
of areas such as wire protocols, peer-to-peer networking, databases,
cryptography, language interpretation (transaction scripts), adversarial
threat-modeling, and RPC systems. They also represent a radical shift to the
current monetary system and as a result provide an opportunity to help reshape
the entire financial system. With the advent of the Lightning Network
(LN)
, new layers are being constructed upon the
base blockchain layer which have the potential to alleviate many of the
limitations and constraints inherent in the design of blockchains. There are
few projects that offer this level of diversity and impact all in one code
base.

However, as exciting as it is, one must keep in mind that cryptocurrencies
represent real money and introducing bugs and security vulnerabilities can have
far more dire consequences than in typical projects where having a small bug is
minimal by comparison. In the world of cryptocurrencies, even the smallest bug
in the wrong area can cost people a significant amount of money. For this
reason, the Lightning Network Daemon (lnd) has a formalized and rigorous
development process (heavily inspired by
btcsuite) which is outlined on this page.

We highly encourage code contributions, however it is imperative that you adhere
to the guidelines established on this page.

Minimum Recommended Skillset

The following list is a set of core competencies that we recommend you possess
before you really start attempting to contribute code to the project. These are
not hard requirements as we will gladly accept code contributions as long as
they follow the guidelines set forth on this page. That said, if you don't have
the following basic qualifications you will likely find it quite difficult to
contribute to the core layers of Lightning. However, there are still a number
of low-hanging fruit which can be tackled without having full competency in the
areas mentioned below.

- A reasonable understanding of bitcoin at a high level (see the
Required Reading section for the original white paper)
- A reasonable understanding of the Lightning Network at a high level
- Experience in some type of C-like language
- An understanding of data structures and their performance implications
- Familiarity with unit testing
- Debugging experience
- Ability to understand not only the area you are making a change in, but also
the code your change relies on, and the code which relies on your changed code

Building on top of those core competencies, the recommended skill set largely
depends on the specific areas you are looking to contribute to. For example,
if you wish to contribute to the cryptography code, you should have a good
understanding of the various aspects involved with cryptography such as the
security and performance implications.

Required Reading

- Effective Go - The entire lnd
project follows the guidelines in this document. For your code to be accepted,
it must follow the guidelines therein.
- Original Satoshi Whitepaper - This is the white paper that started it all. Having a solid
foundation to build on will make the code much more comprehensible.
- Lightning Network Whitepaper - This is the white paper that kicked off the Layer 2 revolution. Having a good grasp of the concepts of Lightning will make the core logic within the daemon much more comprehensible: Bitcoin Script, off-chain blockchain protocols, payment channels, bidirectional payment channels, relative and absolute time-locks, commitment state revocations, and Segregated Witness.
- The original LN was written for a rather narrow audience, the paper may be a bit unapproachable to many. Thanks to the Bitcoin community, there exist many easily accessible supplemental resources which can help one see how all the pieces fit together from double-spend protection all the way up to commitment state transitions and Hash Time Locked Contracts (HTLCs):
- Lightning Network Summary
- Understanding the Lightning Network 3-Part series
- Deployable Lightning


Note that the core design of the Lightning Network has shifted over time as
concrete implementation and design has expanded our knowledge beyond the
original white paper. Therefore, specific information outlined in the resources
above may be a bit out of date. Many implementers are currently working on an
initial Lightning Network Specifications.
Once the specification is finalized, it will be the most up-to-date
comprehensive document explaining the Lightning Network. As a result, it will
be recommended for newcomers to read first in order to get up to speed.

Substantial contributions only

Due to the prevalence of automated analysis and pull request authoring tools
and online competitions that incentivize creating commits in popular
repositories, the maintainers of this project are flooded with trivial pull
requests that only change some typos or other insubstantial content (e.g. the
year in the license file).
If you are an honest user that wants to contribute to this project, please
consider that every pull request takes precious time from the maintainers to
review and consider the impact of changes. Time that could be spent writing
features or fixing bugs.
If you really want to contribute, consider reviewing and testing other users'
pull requests instead
.
First-time reviewer friendly pull requests can be found
here
.
Once you are familiar with the project's code style, testing and review
procedure, your own pull requests will likely require less guidance and fewer
maintainer review cycles, resulting in potentially faster merges.
Also, consider increasing the test coverage of the code by writing more unit
tests first, which is also a very valuable way to contribute and learn more
about the code base.

New Contributors


Due to the ubiquity of LLM coding tools, pull requests
from new contributors are not prioritized for review. If you're a
new contributor with aspirations to contribute to LND, we recommend starting
with issue triage and PR reviews. These are a better avenue to demonstrate
your knowledge and desire to contribute to open source than new code in this
era of easy AI-assisted code generation. This should also help build a track
record that makes your future PRs easier to prioritize.

If you spot a glaring issue, we may still merge the fix or take it over
ourselves. And if you're a new developer who notices an issue with the code,
consider opening a detailed issue instead of a PR.

Development Practices

Developers are expected to work in their own trees and submit pull requests when
they feel their feature or bug fix is ready for integration into the master
branch.

Share Early, Share Often

We firmly believe in the share early, share often approach. The basic premise
of the approach is to announce your plans before you start work, and once
you have started working, craft your changes into a stream of small and easily
reviewable commits.

This approach has several benefits:

- Announcing your plans to work on a feature before you begin work avoids
duplicate work
- It permits discussions which can help you achieve your goals in a way that is
consistent with the existing architecture
- It minimizes the chances of you spending time and energy on a change that
might not fit with the consensus of the community or existing architecture and
potentially be rejected as a result
- The quicker your changes are merged to master, the less time you will need to
spend rebasing and otherwise trying to keep up with the main code base

Development Guidelines

The lnd project emphasizes code readability and maintainability through
specific development guidelines. Key aspects include: thorough code
documentation with clear function comments and meaningful inline comments;
consistent code spacing to separate logical blocks; adherence to an 80-character
line limit with specific rules for wrapping function calls, definitions, and log
messages (including structured logging); comprehensive unit and integration
testing for all changes; well-structured Git commit messages with package
prefixes and atomic commits; signing Git commits; proper handling of Go module
dependencies and submodules; and appropriate use of log levels. Developers are
encouraged to configure their editors to align with these standards.

For the complete set of rules and examples, please refer to the detailed
Development Guidelines.


Code Approval Process

This section describes the code approval process that is used for code
contributions. This is how to get your changes into lnd.

Code Review

All code which is submitted will need to be reviewed before inclusion into the
master branch. This process is performed by the project maintainers and usually
other committers who are interested in the area you are working in as well.

Code Review Timeframe

The timeframe for a code review will vary greatly depending on factors such as
the number of other pull requests which need to be reviewed, the size and
complexity of the contribution, how well you followed the guidelines presented
on this page, and how easy it is for the reviewers to digest your commits. For
example, if you make one monolithic commit that makes sweeping changes to things
in multiple subsystems, it will obviously take much longer to review. You will
also likely be asked to split the commit into several smaller, and hence more
manageable, commits.

Keeping the above in mind, most small changes will be reviewed within a few
days, while large or far-reaching changes may take weeks. This is a good reason
to stick with the Share Early, Share Often
development practice outlined above.

What is the review looking for?

The review is mainly ensuring the code follows the
Development Practices and
Code Contribution Standards. However, there are a few
other checks which are generally performed as follows:

- The code is stable and has no stability or security concerns
- The code is properly using existing APIs and generally fits well into the
overall architecture
- The change is not something which is deemed inappropriate by community
consensus

Rework Code (if needed)

After the code review, the change will be accepted immediately if no issues are
found. If there are any concerns or questions, you will be provided with
feedback along with the next steps needed to get your contribution merged with
master. In certain cases the code reviewer(s) or interested committers may help
you rework the code, but generally you will simply be given feedback for you to
make the necessary changes.

During the process of responding to review comments, we prefer that changes be
made with fixup commits.
The reason for this is twofold: it makes it easier for the reviewer to see
what changes have been made between versions (since Github doesn't easily show
prior versions like Critique) and it makes it easier on the PR author as they
can set it to auto squash the fix up commits on rebase.

This process will continue until the code is finally accepted.

Acceptance

Before your code is accepted, the release notes we keep in-tree for the next
upcoming milestone should be extended to describe the changes contained in your
PR
.
Unless otherwise mentioned by the reviewers of your PR, the description of your
changes should live in the document set for the _next_ major release.

Once your code is accepted, it will be integrated with the master branch. After
2+ (sometimes 1) LGTM's (approvals) are given on a PR, it's eligible to land in
master. At this final phase, it may be necessary to rebase the PR in order to
resolve any conflicts and also squash fix up commits. Ideally, the set of
commits by new contributors are PGP signed,
although this isn't a strong requirement (but we prefer it!). In order to keep
these signatures intact, we prefer using merge commits. PR proposers can use
git rebase --signoff to sign and rebase at the same time as a final step.

Rejoice as you will now be listed as a contributor!

Backporting Changes

After a PR is merged to master, it may need to be backported to release branches
(e.g., v0.20.x-branch) to include the fix or feature in upcoming patch releases.

The project uses an automated backport workflow to simplify this process. Simply
add a label like backport-v0.20.x-branch to your merged PR, and a GitHub Action
will automatically create a backport PR for you.

For complete documentation on the automated backport workflow, including:
- How to use backport labels
- Handling merge conflicts
- Multiple backports
- Troubleshooting

See backport-workflow.md

Review Bot

In order to keep the review flow going, Lightning Labs uses a bot to remind
PR reviewers about their outstanding reviews or to remind authors to address
recent reviews. Here are some important things to know about the bot and some
controls for adjusting its behaviour:

####🤖 Expected Behaviour:
- The bot will not do anything if your PR is in draft mode.
- It will ping a pending reviewer if they have not reviewed or commented on the
PR in x days since the last update or the last time the bot pinged them.
(default x = 3)
- It will ping the author of the PR if they have not addressed a review on a PR
after x days since last review or the last time the bot pinged them. It will
also ping them to remind them to re-request review if needed. (default x = 3)

####🤖 Controls:
To control the bot, you need to add a comment on the PR starting with
!lightninglabs-deploy followed by the command. There are 2 control types:
mute/unmute & cadence. Only the latest comment for each control type will be
used. This also means you don't need to keep adding new control comments, just
edit the latest comment for that control type.

- !lightninglabs-deploy mute will mute the bot on the PR completely.
- !lightninglabs-deploy mute 72h30m will mute the bot for the given duration.
- !lightninglabs-deploy mute 2022-Feb-02 will mute the bot until the given
date (must be in this format!).
- !lightninglabs-deploy mute #4 will mute the bot until the given PR of the
same repo has been merged.
- !lightninglabs-deploy unmute will unmute the bot (or just delete the comment
that was muting it)
- !lightninglabs-deploy cadence 60h change the cadence of the bot from the
default of 3 days to the given duration.
- it will auto-mute if the PR is in Draft mode

Contribution Standards

Contribution Checklist

See template.

Licensing of Contributions



All contributions must be licensed with the
MIT license. This is
the same license as all of the code found within lnd.


Acknowledgements


This document was heavily inspired by a similar document outlining the code
contribution

guidelines for btcd.

---

Configuring Tor

Table of Contents


1. Overview
2. Getting Started
3. Tor Stream Isolation
4. Authentication
5. Listening for Inbound Connections

Overview

lnd currently has complete support for using Lightning over
Tor. Usage of Lightning over Tor is valuable as
routing nodes no longer need to potentially expose their location via their
advertised IP address. Additionally, leaf nodes can also protect their location
by using Tor for anonymous networking to establish connections.

With widespread usage of Onion Services within the network, concerns about the
difficulty of proper NAT traversal are alleviated, as usage of onion services
allows nodes to accept inbound connections even if they're behind a NAT. lnd
supports v3 onion services only; legacy v2 onion service support has been
removed.

Before following the remainder of this documentation, you should ensure that
you already have Tor installed locally. Make sure that you run at least
version 0.3.3.6 of Tor in order to use v3 Onion Services.
Official instructions to install the latest release of Tor can be found
here.

NOTE: This documentation covers how to ensure that lnd's _Lightning
protocol traffic_ is tunneled over Tor. Users must ensure that when also running
a Bitcoin full-node, that it is also proxying all traffic over Tor. If using the
neutrino backend for lnd, then it will automatically also default to Tor
usage if active within lnd.

Getting Started

First, you'll want to run tor locally before starting up lnd. Depending on
how you installed Tor, you'll find the configuration file at
/usr/local/etc/tor/torrc. Here's an example configuration file that we'll be
using for the remainder of the tutorial:

text
SOCKSPort 9050
Log notice stdout
ControlPort 9051
CookieAuthentication 1

With the configuration file created, you'll then want to start the Tor daemon:

shell
$  tor
Feb 05 17:02:06.501 [notice] Tor 0.3.1.8 (git-ad5027f7dc790624) running on Darwin with Libevent 2.1.8-stable, OpenSSL 1.0.2l, Zlib 1.2.8, Liblzma N/A, and Libzstd N/A.
Feb 05 17:02:06.502 [notice] Tor can't help you if you use it wrong! Learn how to be safe at https://www.torproject.org/download/download#warning
Feb 05 17:02:06.502 [notice] Read configuration file "/usr/local/etc/tor/torrc".
Feb 05 17:02:06.506 [notice] Opening Socks listener on 127.0.0.1:9050
Feb 05 17:02:06.506 [notice] Opening Control listener on 127.0.0.1:9051

Once the tor daemon has started and it has finished bootstrapping, you'll see this in the logs:

text
Feb 05 17:02:06.000 [notice] Bootstrapped 0%: Starting
Feb 05 17:02:07.000 [notice] Starting with guard context "default"
Feb 05 17:02:07.000 [notice] Bootstrapped 80%: Connecting to the Tor network
Feb 05 17:02:07.000 [notice] Bootstrapped 85%: Finishing handshake with first hop
Feb 05 17:02:08.000 [notice] Bootstrapped 90%: Establishing a Tor circuit
Feb 05 17:02:11.000 [notice] Tor has successfully opened a circuit. Looks like client functionality is working.
Feb 05 17:02:11.000 [notice] Bootstrapped 100%: Done

This indicates the daemon is fully bootstrapped and ready to proxy connections.
At this point, we can now start lnd with the relevant arguments:

shell
$  ./lnd -h

<snip>

Tor:
--tor.active Allow outbound and inbound connections to be routed through Tor
--tor.socks= The host:port that Tor's exposed SOCKS5 proxy is listening on (default: localhost:9050)
--tor.dns= The DNS server as host:port that Tor will use for SRV queries - NOTE must have TCP resolution enabled (default: soa.nodes.lightning.directory:53)
--tor.streamisolation Enable Tor stream isolation by randomizing user credentials for each connection.
--tor.control= The host:port that Tor is listening on for Tor control connections (default: localhost:9051)
--tor.targetipaddress= IP address that Tor should use as the target of the hidden service
--tor.password= The password used to arrive at the HashedControlPassword for the control port. If provided, the HASHEDPASSWORD authentication method will be used instead of the SAFECOOKIE one.
--tor.v3 Automatically set up a v3 onion service to listen for inbound connections
--tor.privatekeypath= The path to the private key of the onion service being created

There are a couple of things here, so let's dissect them. The --tor.active
flag allows lnd to route all outbound and inbound connections through Tor.

Outbound connections are possible with the use of the --tor.socks and
--tor.dns arguments. The --tor.socks argument should point to the interface
that the Tor daemon is listening on to proxy connections. The --tor.dns flag
is required in order to be able to properly automatically bootstrap a set of
peer connections. The tor daemon doesn't currently support proxying SRV
queries over Tor. So instead, we need to connect directly to the authoritative
DNS server over TCP, in order query for SRV records that we can use to
bootstrap our connections.

Inbound connections are possible due to lnd automatically creating an onion
service. A path to save the onion service's private key can be specified with
the --tor.privatekeypath flag.

Most of these arguments have defaults, so as long as they apply to you, routing
all outbound and inbound connections through Tor can simply be done with v3
onion services:

shell
$  ./lnd --tor.active --tor.v3

See Listening for Inbound Connections for
more info about allowing inbound connections via Tor.

Outbound support only can also be used with:

shell
$  ./lnd --tor.active

This will allow you to make all outgoing connections over Tor. Listening is
disabled to prevent inadvertent leaks.

Tor Stream Isolation

Our support for Tor also has an additional privacy enhancing modified: stream
isolation. Usage of this mode means that Tor will always use _new circuit_ for
each connection. This added features means that it's harder to correlate
connections. As otherwise, several applications using Tor might share the same
circuit.

Activating stream isolation is very straightforward, we only require the
specification of an additional argument:

shell
$  ./lnd --tor.active --tor.streamisolation

Authentication

In order for lnd to communicate with the Tor daemon securely, it must first
establish an authenticated connection. lnd supports the following Tor control
authentication methods (arguably, from most to least secure):

* SAFECOOKIE: This authentication method relies on a cookie created and
stored by the Tor daemon and is the default assuming the Tor daemon supports
it by specifying CookieAuthentication 1 in its configuration file.
* HASHEDPASSWORD: This authentication method is stateless as it relies on a
password hash scheme and may be useful if the Tor daemon is operating under a
separate host from the lnd node. The password hash can be obtained through
the Tor daemon with tor --hash-password PASSWORD, which should then be
specified in Tor's configuration file with HashedControlPassword
PASSWORD_HASH
. Finally, to use it within lnd, the --tor.password flag
should be provided with the corresponding password.
* NULL: To bypass any authentication at all, this scheme can be used instead.
It doesn't require any additional flags to lnd or configuration options to
the Tor daemon.

Listening for Inbound Connections

In order to listen for inbound connections through Tor, an onion service must be
created. lnd supports v3 onion services, the latest generation of onion
services. To learn more about these, see
Intro to Next Gen Onion Services.

v3 onion services are created and used automatically by lnd via the tor.v3
flag. To prevent unintentional leaking of identifying information, it is also
necessary to add the flag listen=localhost.

For example, v3 onion services can be used with the following flags:

shell
$  ./lnd --tor.active --tor.v3 --listen=localhost

This will automatically create a hidden service for your node to use to listen
for inbound connections and advertise itself to the network. The onion service's
private key is saved to a file named v3_onion_private_key in lnd's base
directory. This will allow lnd to recreate the same hidden service upon
restart. If you wish to generate a new onion service, you can simply delete this
file. The path to this private key file can also be modified with the
--tor.privatekeypath argument.

You can optionally encrypt the Tor private key by using the --tor.encryptkey
flag. This will still write to the same private key files. However instead of
writing the plaintext private key, lnd encrypts the private key using the
wallet's seed and writes the encrypted blob to the file.

---

Db Migration Guide

LND Database Upgrade Guide: A Two-Stage Migration for Node Operators

Table of Contents
- Overview
- Operation Modes
- Stage 1: Migration from bbolt to SQLite/Postgres (kvdb)
- Choosing Your Target Backend
- Postgres kvdb Migration
- SQLite kvdb Migration
- Stage 2: Migration from kvdb to Relational Database
- Subsystem Readiness
- Known Limitations and Edge Cases
- Best Practices for Node Operators
- Choosing the Right Path
- Timing Your Migration
- Validation
- Implementation Examples
- Migrating to SQLite kvdb
- Migrating Invoices to Relational Backend
- Future Improvements
- Conclusion

---

Overview

The LND Database Upgrade process enables node operators to migrate from the legacy bbolt key-value store to modern SQLite or PostgreSQL backends—first in kvdb mode, then ultimately to a relational database format. This two-stage migration addresses long-standing performance bottlenecks, and lays the foundation for scalable, maintainable node operations.

This guide explains the migration path, backend trade-offs, subsystem readiness, and best practices for safely upgrading your LND database.

---

Operation Modes

The LND database upgrade operates in two sequential stages, each with distinct tooling and implications:

text
/ Detailed source-code truncated for AI context efficiency. /

- Stage 1: Migrate from bbolt to a SQL-based kvdb backend using the lndinit tool.
- Stage 2: Incrementally migrate subsystem data (invoices, graph, payments, etc.) from kvdb to SQL native relational tables as support becomes available.

---

Stage 1: Migration from bbolt to SQLite/Postgres (kvdb)

LND cannot run with mixed backends, so all users must first leave bbolt behind. This stage uses the lndinit utility to perform an offline migration.

Choosing Your Target Backend

| Backend | Performance (in kvdb mode) | Default in LND? | Long-Term Viability |
|-----------|--------------------------|------------------|----------------------|
| Postgres | Mediocre | No | ✅ |
| SQLite | Good | Yes (future) | ✅ |

💡 Recommendation: Unless you require Postgres for infrastructure reasons,

migrate to SQLite kvdb as your backend.

Postgres kvdb Migration

A migration script is available via lndinit:

- Postgres migration script
- Caveat: Users report degraded performance in kvdb mode. Only proceed if you plan to immediately follow with Stage 2 to migrate the available data stores to relational DB.
This will mitigate the poor Postgres performance on kvdb.

SQLite kvdb Migration

- SQLite migration script
- Advantage: Maintains good performance while waiting for full relational migration.

---

Stage 2: Migration from kvdb to Relational Database

This stage unlocks true SQL performance by restructuring data into relational tables. Migration is per-subsystem and incremental.

The migration steps are automatically applied when LND is restarted after step 1 was successfully completed and the config value db.use-native-sql=true is set.
You will see log lines from the SQLD subsystem about the migration, such as Starting migration of invoices from KV to SQL.

Subsystem Readiness

| Subsystem | Relational Backend | Migration Script | Status |
|---------------------|--------------------|------------------|--------|
| Invoices | ✅ Available | ✅ | Available with v0.19 |
| Graph | ✅ Available | ✅ | Available with v0.20 |
| Payments | ✅ Available | ✅ | Available with v0.21 |
| Btcwallet | 🚧 In Progress | Planned | Targeted with v0.22|
| Channel State | 🚧 In Progress | Planned | Targeted with v0.22|

---

Known Limitations and Edge Cases

- Single database engine required: LND requires a single consistent backend.
You cannot run invoices in relational mode while graph remain in kvdb unless
both are on the same SQL engine (e.g., SQLite).
- Data loss risk: Always back up your data/ directory before migration.
- Downtime required: Stage 1 requires LND to be offline. Stage 2 is done at startup, requiring a LND restart.
- Postgres kvdb performance: Postgres performance on kvdb is sub-optimal. It is
recommended to make the stage 2 migration immediately to avoid performance bottlenecks. Certain RPCs like listpayments may not perform well on Postgres if the node has a lot of payments data. As of v0.21, the payments relational
backend is available, so payment-heavy nodes can migrate payments to relational
mode to restore good listpayments performance.
- No migration path between SQL backend: Once migrated to either Postgres or
SQLite, it is not possible to switch to the other, so choose your target backend carefully.
---

Best Practices for Node Operators

Choosing the Right Path

- For most users: Choose SQLite, then migrate. Later, adopt relational backends subsystem-by-subsystem.
- Enterprise/Postgres users: With the payments relational backend available
as of v0.21, you can now perform Stage 1 + Stage 2 in quick succession.

Timing Your Migration

- Perform migrations during low-activity periods.
- Monitor LND release notes for relational DB support of different subsystems.

Validation

1. Stop LND.
2. Run migration with lndinit.
3. Start LND with new backend flags, to execute stage 2 migrations.
4. Validate node health: channels, balance, invoice/payment history.

---

Implementation Examples

Migrating to SQLite kvdb

bash

Stop LND


lnd --shutdown

Backup


cp -r ~/.lnd ~/lnd-backup-$(date +%Y%m%d)

Run migration (e.g. sqlite)


lndinit --debuglevel info migrate-db \
--source.bolt.data-dir ~/.lnd/data \
--dest.backend sqlite \
--dest.sqlite.data-dir ~/.lnd/data --network mainnet

Start LND with SQLite backend


lnd --db.backend=sqlite

📝 Add db.backend=sqlite to your lnd.conf to make it persistent.

Migrating Invoices to Relational Backend

Once on LND v0.19+ with SQLite/Postgres:

bash

Ensure backend is set


echo "db.backend=sqlite" >> ~/.lnd/lnd.conf

Start LND — invoice migration runs automatically


lnd

Check logs for:

text
Migrating invoices from kvdb to relational format...
Invoice migration completed successfully.

---

Future Improvements

The LND team is actively working on:

- Btcwallet relational backend and migration tooling (Stage 2)
- Channel state relational backend and migration tooling (Stage 2)
- Automatic detection of migration readiness in lnd

Node operators should monitor:
- LND GitHub Releases
- lndinit repository

---

Conclusion

The LND database upgrade is a strategic two-stage process designed to eliminate bbolt performance limitations while ensuring data integrity and operational continuity.

By first migrating to SQLite/Postgres kvdb and then adopting relational backends incrementally, node operators can achieve significant performance gains—especially for payment-heavy workloads—without rushing into unstable configurations.

Choose your path wisely, back up rigorously, and stay informed. The future of LND is relational, and this guide ensures you get there safely.

---

Debugging Lnd

Table of Contents


1. Overview
1. Debug Logging
1. LND's built-in profiler

Overview

lnd ships with a few useful features for debugging, such as a built-in
profiler and tunable logging levels. If you need to submit a bug report
for lnd, it may be helpful to capture debug logging and performance
data ahead of time.

Debug Logging

LND supports different logging levels and you can also specify different logging
levels per subsystem. This makes it easy to focus on a particular subsystem
without clogging up the logs with a lot of noise. One can either set the logging
in the lnd.conf file or pass the flag --debuglevel with the specified level
when starting lnd.

LND supports the following logging levels (see log.go and
sample-lnd.conf for more info):

- trace
- debug
- info
- warn
- error
- critical
- off

LND is composed of many subsystems, those subsystems can be listed either by
setting the starting flag --debuglevel or by using the lncli program.

Show all subsystems:

shell
$  lnd --debuglevel=show
$ lncli debuglevel --show

For more details see log.go.

You may also specify logging per-subsystem, like this:

shell
$  lnd --debuglevel=<subsystem>=<level>,<subsystem2>=<level>,...
$ lncli debuglevel --level=<subsystem>=<level>,<subsystem2>=<level>,...

The default global logging level is info. So if one wants to change the
global logging level and in addition also set a more detailed logging for a
particular subsystem the command would look like this (using HSWC
(htlcswitch) as an example subsystem):

shell
$ lnd --debuglevel=critical,HSWC=debug
$ lncli debuglevel --level=critical,HSWC=debug

The subsystem names are case-sensitive and must be all uppercase.

To identify the subsystems defined by an abbreviated name, you can search for
the abbreviation in the log.go file. Each subsystem
declares a btclog.Logger instance locally which is then assigned via the
UseLogger function call in the SetupLoggers function.

Example HSWC:

For the HSWC subsystem a new sublogger is injected into the htlcswitch
package via the UseLogger function call in the SetupLoggers function. So
the HSWC subsystem handles the logging in the htlcswitch package.

go
AddSubLogger(root, "HSWC", interceptor, htlcswitch.UseLogger)

Caution: Some logger subsystems are overwritten during the instanziation. An
example here is the neutrino/query package which instead of using the BTCN
prefix is overwritten by the LNWL subsystem.

Moreover when using the lncli command the return value will provide the
updated list of all subsystems and their associated logging levels. This makes
it easy to get an overview of the current logging level for the whole system.

Example:

shell
$ lncli debuglevel --level=critical,HSWC=debug
{
"sub_systems": "ARPC=INF, ATPL=INF, BLPT=INF, BRAR=INF, BTCN=INF, BTWL=INF, CHAC=INF, CHBU=INF, CHCL=INF, CHDB=INF, CHFD=INF, CHFT=INF, CHNF=INF, CHRE=INF, CLUS=INF, CMGR=INF, CNCT=INF, CNFG=INF, CRTR=INF, DISC=INF, DRPC=INF, FNDG=INF, GRPH=INF, HLCK=INF, HSWC=DBG, INVC=INF, IRPC=INF, LNWL=INF, LTND=INF, NANN=INF, NRPC=INF, NTFN=INF, NTFR=INF, PEER=INF, PRNF=INF, PROM=INF, PRPC=INF, RPCP=INF, RPCS=INF, RPWL=INF, RRPC=INF, SGNR=INF, SPHX=INF, SRVR=INF, SWPR=INF, TORC=INF, UTXN=INF, VRPC=INF, WLKT=INF, WTCL=INF, WTWR=INF"
}


Built-in profiler in LND

LND has a built-in feature which allows you to capture profiling data at
runtime using pprof, a profiler for
Go. It is recommended to enable the profiling server so that an analyis can be
triggered during runtime. There is only little overhead in enabling this
feature, because profiling is only started when calling the server endpoints.
However LND also allows to specify a cpu profile file via the cpuprofile flag
which triggers a cpu profile when LND starts and stops it when LND shuts down.
This is only recommended for debugging purposes, because the overhead is much
higher. To enable the profile server, start lnd with the --profile option
using a free port. As soon as the server is up different profiles can be
fetched from the debug/pprof endpoint using either the web interface or for
example curl.

Example port 9736 is used for the profile server in the following examples.

shell
$  lnd --profile=9736

NOTE: The --profile flag of the lncli program does not relate to profiling and
the profiling server. It has a different context and allows a node operator to
manage different LND daemons without providing all the cmd flags every time.
For more details see lncli profile.

Different types of profiles

#### CPU profile

A cpu profile can be used to analyze the CPU usage of the program. When
obtaining it via the profile http endpoint you can specify the time duration as
a query parameter.

shell
$ curl http://localhost:9736/debug/pprof/profile?seconds=10 > cpu.prof

#### Goroutine profile

The goroutine profile is very useful when analyzing deadlocks and lock
contention. It can be obtained via the web interface or the following endpoint:

shell
$ curl http://localhost:9736/debug/pprof/goroutine?debug=2 > goroutine.prof

The query parameter debug=2 is optional but recommended and referes to the
format of the output file. Only this format has the necessary information to
identify goroutines deadlocks. Otherwise go tool pprof needs to be used to
visualize the data and interpret the results.

#### Heap profile

The heap profile is useful to analyze memory allocations. It can be obtained
via the following endpoint:

shell
$ curl http://localhost:9736/debug/pprof/heap > heap.prof

The documentation of the pprof package states that a gc can be triggered before
obtaining the heap profile. This can be done by setting the gc query parameter
(gc=1).

#### Other profiles

There are several other options available like a mutex profile or a block
profile which gives insights into contention and bottlenecks of your program.
The web interface lists all the available profiles/endpoints which can be
obtained.

However mutex and block profiling need to be enabled separately by setting the
sampling rate via the config values BlockingProfile and MutexProfile. They
are off by default (0). These values represent sampling rates meaning that a
value of 1 will record every event leading to a significant overhead whereas
a sample rate of n will only record 1 out of nth events decreasing the
aggressiveness of the profiler.

Fetching the block and mutex profile:

shell
$ curl http://localhost:9736/debug/pprof/mutex?debug=2

$ curl http://localhost:9736/debug/pprof/block?debug=2

The full programm command can also be fetched which shows how LND was started
and which flags were provided to the program.

shell
$ curl http://localhost:9736/debug/pprof/cmdline > cmdline.prof

There are also other endpoints available see the
pprof documentation for more details.


#### Visualizing the profile dumps

It can be hard to make sense of the profile dumps by just looking at them
therefore the Golang ecosystem provides tools to analyze those profile dumps
either via the terminal or by visualizing them. One of the tools is
go tool pprof.

Assuming the profile was fetched via curl as in the examples above a nice
svg visualization can be generated for the cpu profile like this:

shell
$ go tool pprof -svg cpu.prof > cpu.svg

Details how to interpret these visualizations can be found in the
pprof documentation.

---

Development Guidelines

Development Guidelines


1. Code Documentation and Commenting
1. Code Spacing and Formatting
1. Additional Style Constraints
1. Recommended settings for your editor
1. Testing
1. Model Git Commit Messages
1. Ideal Git Commit Structure
1. Sign Your Git Commits
1. Pointing to Remote Dependent Branches in Go Modules
1. Use of Log Levels
1. Use of Golang submodules

Why this emphasis on formatting?

Code in general (and Open Source code specifically) is _read_ by developers many
more times during its lifecycle than it is modified. With this fact in mind, the
Golang language was designed for readability (among other goals).
While the enforced formatting of go fmt and some best practices already
eliminate many discussions, the resulting code can still look and feel very
differently among different developers.

We aim to enforce a few additional rules to unify the look and feel of all code
in lnd to help improve the overall readability.

Code Documentation and Commenting

- At a minimum every function must be commented with its intended purpose and
any assumptions that it makes
- Function comments must always begin with the name of the function per
Effective Go
- Function comments should be complete sentences since they allow a wide
variety of automated presentations such as godoc.org
- The general rule of thumb is to look at it as if you were completely
unfamiliar with the code and ask yourself, would this give me enough
information to understand what this function does and how I'd probably want to
use it?
- Exported functions should also include detailed information the caller of the
function will likely need to know and/or understand:<br /><br />

WRONG

go
// generates a revocation key
func DeriveRevocationPubkey(commitPubKey *btcec.PublicKey,
revokePreimage []byte) *btcec.PublicKey {

RIGHT
go
// DeriveRevocationPubkey derives the revocation public key given the
// counterparty's commitment key, and revocation preimage derived via a
// pseudo-random-function. In the event that we (for some reason) broadcast a
// revoked commitment transaction, then if the other party knows the revocation
// preimage, then they'll be able to derive the corresponding private key to
// this private key by exploiting the homomorphism in the elliptic curve group:
// * https://en.wikipedia.org/wiki/Group_homomorphism#Homomorphisms_of_abelian_groups
//
// The derivation is performed as follows:
//
// revokeKey := commitKey + revokePoint
// := Gk + Gh
// := G * (k+h)
//
// Therefore, once we divulge the revocation preimage, the remote peer is able to
// compute the proper private key for the revokeKey by computing:
// revokePriv := commitPriv + revokePreimge mod N
//
// Where N is the order of the sub-group.
func DeriveRevocationPubkey(commitPubKey *btcec.PublicKey,
revokePreimage []byte) *btcec.PublicKey {

- Comments in the body of the code are highly encouraged, but they should
explain the intention of the code as opposed to just calling out the
obvious<br /><br />

WRONG

go
// return err if amt is less than 546
if amt < 546 {
return err
}

RIGHT
go
// Treat transactions with amounts less than the amount which is considered dust
// as non-standard.
if amt < 546 {
return err
}

NOTE: The above should really use a constant as opposed to a magic number,
but it was left as a magic number to show how much of a difference a good
comment can make.

Code Spacing and formatting

Code in general (and Open Source code specifically) is _read_ by developers many
more times during its lifecycle than it is modified. With this fact in mind, the
Golang language was designed for readability (among other goals).
While the enforced formatting of go fmt and some best practices already
eliminate many discussions, the resulting code can still look and feel very
differently among different developers.

We aim to enforce a few additional rules to unify the look and feel of all code
in lnd to help improve the overall readability.

Blocks of code within lnd should be segmented into logical stanzas of
operation. Such spacing makes the code easier to follow at a skim, and reduces
unnecessary line noise. Coupled with the commenting scheme specified in the
contribution guide,
proper spacing allows readers to quickly scan code, extracting semantics quickly.
Functions should _not_ just be laid out as a bare contiguous block of code.

WRONG

go
witness := make([][]byte, 4)
witness[0] = nil
if bytes.Compare(pubA, pubB) == -1 {
witness[1] = sigB
witness[2] = sigA
} else {
witness[1] = sigA
witness[2] = sigB
}
witness[3] = witnessScript
return witness

RIGHT
go
witness := make([][]byte, 4)

// When spending a p2wsh multi-sig script, rather than an OP_0, we add
// a nil stack element to eat the extra pop.
witness[0] = nil

// When initially generating the witnessScript, we sorted the serialized
// public keys in descending order. So we do a quick comparison in order
// to ensure the signatures appear on the Script Virtual Machine stack in
// the correct order.
if bytes.Compare(pubA, pubB) == -1 {
witness[1] = sigB
witness[2] = sigA
} else {
witness[1] = sigA
witness[2] = sigB
}

// Finally, add the preimage as the last witness element.
witness[3] = witnessScript

return witness

Additionally, we favor spacing between stanzas within syntax like: switch case
statements and select statements.

WRONG

go
switch {
case a:
<code block>
case b:
<code block>
case c:
<code block>
case d:
<code block>
default:
<code block>
}

RIGHT
go
switch {
// Brief comment detailing instances of this case (repeat below).
case a:
<code block>

case b:
<code block>

case c:
<code block>

case d:
<code block>

default:
<code block>
}

Additional Style Constraints

Before a PR is submitted, the proposer should ensure that the file passes the
set of linting scripts run by make lint. These include gofmt. In addition
to gofmt we've opted to enforce the following style guidelines.

80 character line length

ALL columns (on a best effort basis) should be wrapped to 80 line columns.
Editors should be set to treat a tab as 8 spaces.

WRONG

go
myKey := "0214cd678a565041d00e6cf8d62ef8add33b4af4786fb2beb87b366a2e151fcee7"

RIGHT

go
myKey := "0214cd678a565041d00e6cf8d62ef8add33b4af4786fb2beb87b366a2e1" +
"51fcee7"

Wrapping long function calls

When wrapping a line that contains a function call as the unwrapped line exceeds
the column limit, the close parenthesis should be placed on its own line.
Additionally, all arguments should begin in a new line after the open parenthesis.

WRONG

go
value, err := bar(a,
a, b, c)

RIGHT

go
value, err := bar(
a, a, b, c,
)

As long as the visual symmetry of the opening and closing parentheses (or curly
braces) is preserved, arguments that would otherwise introduce a new level of
indentation are allowed to be written in a more compact form.
Visual symmetry here means that when two or more opening parentheses or curly
braces are on the same line, then they must also be closed on the same line.
And the closing line needs to have the same indentation level as the opening
line.

Example with inline struct creation:

ACCEPTABLE

go
response, err := node.AddInvoice(
ctx, &lnrpc.Invoice{
Memo: "invoice",
ValueMsat: int64(oneUnitMilliSat - 1),
},
)

PREFERRED

go
response, err := node.AddInvoice(ctx, &lnrpc.Invoice{
Memo: "invoice",
ValueMsat: int64(oneUnitMilliSat - 1),
})

WRONG

go
response, err := node.AddInvoice(ctx, &lnrpc.Invoice{
Memo: "invoice",
ValueMsat: int64(oneUnitMilliSat - 1)})

Example with nested function call:

ACCEPTABLE:

go
payInvoiceWithSatoshi(
t.t, dave, invoiceResp2, withFailure(
lnrpc.Payment_FAILED, failureNoRoute,
),
)

PREFERRED:

go
payInvoiceWithSatoshi(t.t, dave, invoiceResp2, withFailure(
lnrpc.Payment_FAILED, failureNoRoute,
))

#### Exception for log and error message formatting

Note that the above guidelines don't apply to log or error messages. For
log and error messages, committers should attempt to minimize the number of
lines utilized, while still adhering to the 80-character column limit. For
example:

WRONG

go
return fmt.Errorf(
"this is a long error message with a couple (%d) place holders",
len(things),
)

log.Debugf(
"Something happened here that we need to log: %v",
longVariableNameHere,
)

RIGHT

go
return fmt.Errorf("this is a long error message with a couple (%d) place "+
"holders", len(things))

log.Debugf("Something happened here that we need to log: %v",
longVariableNameHere)

This helps to visually distinguish those formatting statements (where nothing
of consequence happens except for formatting an error message or writing
to a log) from actual method or function calls. This compact formatting should
be used for calls to formatting functions like fmt.Errorf,
log.(Trace|Debug|Info|Warn|Error)f and fmt.Printf.
But not for statements that are important for the flow or logic of the code,
like require.NoErrorf().

#### Exceptions and additional styling for structured logging

When making use of structured logging calls (there are any btclog.Logger
methods ending in S), a few different rules and exceptions apply.

1) Static messages: Structured log calls take a context.Context as a first
parameter and a _static_ string as the second parameter (the msg parameter).
Formatted strings should ideally not be used for the construction of the msg
parameter. Instead, key-value pairs (or slog attributes) should be used to
provide additional variables to the log line.

WRONG

go
log.DebugS(ctx, fmt.Sprintf("User %d just spent %.8f to open a channel", userID, 0.0154))

RIGHT

go
log.InfoS(ctx, "Channel open performed",
slog.Int("user_id", userID),
btclog.Fmt("amount", "%.8f", 0.00154))

2) Key-value attributes: The third parameter in any structured log method is
a variadic list of the any type but it is required that these are provided in
key-value pairs such that an associated slog.Attr variable can be created for
each key-value pair. The simplest way to specify this is to directly pass in the
key-value pairs as raw literals as follows:

go
log.InfoS(ctx, "Channel open performed", "user_id", userID, "amount", 0.00154)

This does work, but it becomes easy to make a mistake and accidentally leave out
a value for each key provided leading to a nonsensical log line. To avoid this,
it is suggested to make use of the various slog.Attr helper functions as
follows:

go
log.InfoS(ctx, "Channel open performed",
slog.Int("user_id", userID),
btclog.Fmt("amount", "%.8f", 0.00154))

3) Line wrapping: Structured log lines are an exception to the 80-character
line wrapping rule. This is so that the key-value pairs can be easily read and
reasoned about. If it is the case that there is only a single key-value pair
and the entire log line is still less than 80 characters, it is acceptable to
have the key-value pair on the same line as the log message. However, if there
are multiple key-value pairs, it is suggested to use the one line per key-value
pair format. Due to this suggestion, it is acceptable for any single key-value
pair line to exceed 80 characters for the sake of readability.

WRONG

go
// Example 1.
log.InfoS(ctx, "User connected",
"user_id", userID)

// Example 2.
log.InfoS(ctx, "Channel open performed", "user_id", userID,
btclog.Fmt("amount", "%.8f", 0.00154), "channel_id", channelID)

// Example 3.
log.InfoS(ctx, "Bytes received",
"user_id", userID,
btclog.Hex("peer_id", peerID.SerializeCompressed()),
btclog.Hex("message", []bytes{
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
})))

RIGHT

go
// Example 1.
log.InfoS(ctx, "User connected", "user_id", userID)

// Example 2.
log.InfoS(ctx, "Channel open performed",
slog.Int("user_id", userID),
btclog.Fmt("amount", "%.8f", 0.00154),
slog.String("channel_id", channelID))

// Example 3.
log.InfoS(ctx, "Bytes received",
"user_id", userID,
btclog.Hex("peer_id", peerID.SerializeCompressed()),
btclog.Hex("message", []bytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08})))

Wrapping long function definitions

If one is forced to wrap lines of function arguments that exceed the
80-character limit, then indentation must be kept on the following lines. Also,
lines should not end with an open parenthesis if the function definition isn't
finished yet.

WRONG

go
func foo(a, b, c,
) (d, error) {

func bar(a, b, c) (
d, error,
) {

func baz(a, b, c) (
d, error) {


RIGHT
go
func foo(a, b,
c) (d, error) {

func baz(a, b, c) (d,
error) {

func longFunctionName(
a, b, c) (d, error) {

If a function declaration spans multiple lines the body should start with an
empty line to help visually distinguishing the two elements.

WRONG

go
func foo(a, b, c,
d, e) error {
var a int
}

RIGHT
go
func foo(a, b, c,
d, e) error {

var a int
}

Inline slice definitions

In Go a list of slices can be initialized with values directly, using curly
braces. Whenever possible, the more verbose/indented style should be used for
better readability and easier git diff handling. Because that results in more
levels of code indentation, the more compact version is allowed in situations
where the remaining space would otherwise be too restricted, resulting in too
long lines (or excessive use of the // nolint: ll directive).

ACCEPTABLE

go
testCases := []testCase{{
name: "spend exactly all",
coins: []wallet.Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * btcutil.SatoshiPerBitcoin,
},
}},
}, {
name: "spend more",
coins: []wallet.Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * btcutil.SatoshiPerBitcoin,
},
}},
}}

PREFERRED

go
coin := btcutil.SatoshiPerBitcoin
testCases := []testCase{
{
name: "spend exactly all",
coins: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * coin,
},
},
},
},
{
name: "spend more",
coins: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * coin,
},
},
},
},
}

To make it easier to follow the rules outlined above, we recommend setting up
your editor with at least the following two settings:

1. Set your tabulator width (also called "tab size") to 8 spaces.
2. Set a ruler or visual guide at 80 character.

Note that the two above settings are automatically applied in editors that
support the EditorConfig scheme (for example GoLand, GitHub, GitLab,
VisualStudio). In addition, specific settings for Visual Studio Code are checked
into the code base as well.

Other editors (for example Atom, Notepad++, Vim, Emacs and so on) might install
a plugin to understand the rules in the .editorconfig file.

In Vim, you might want to use set colorcolumn=80.


Testing

One of the major design goals of all of lnd's packages and the daemon itself is
to aim for a high degree of test coverage. This is financial software so bugs
and regressions in the core logic can cost people real money. For this reason
every effort must be taken to ensure the code is as accurate and bug-free as
possible. Thorough testing is a good way to help achieve that goal.

Unless a new feature you submit is completely trivial, it will probably be
rejected unless it is also accompanied by adequate test coverage for both
positive and negative conditions. That is to say, the tests must ensure your
code works correctly when it is fed correct data as well as incorrect data
(error paths).


Go provides an excellent test framework that makes writing test code and
checking coverage statistics straightforward. For more information about the
test coverage tools, see the golang cover blog post.

A quick summary of test practices follows:

- All new code should be accompanied by tests that ensure the code behaves
correctly when given expected values, and, perhaps even more importantly, that
it handles errors gracefully.
- When you fix a bug, it should be accompanied by tests which exercise the bug
to both prove it has been resolved and to prevent future regressions.
- Changes to publicly exported packages such as
brontide should
be accompanied by unit tests exercising the new or changed behavior.
- Changes to behavior within the daemon's interaction with the P2P protocol,
or RPC's will need to be accompanied by integration tests which use the
networkHarnessframework
contained within lnd. For example integration tests, see
lnd_test.go.
- The itest log files are automatically scanned for [ERR] lines. There
shouldn't be any of those in the logs, see Use of Log Levels.

Throughout the process of contributing to lnd, you'll likely also be
extensively using the commands within our Makefile. As a result, we recommend
perusing the make file documentation.


Before committing the changes made, you should run unit tests to validate the
changes, and provide a new integration test when necessary. The unit tests
should pass at two levels,

- Run make unit-debug log="stdlog trace" pkg=$pkg case=$case timeout=10s,
where pkg is the package that's updated, and case is the newly added or
affected unit test. This command should run against all the newly added and
affected test cases. In addition, you should pay attention to the logs added
here, and make sure they are correctly formatted and no spammy. Also notice
the timeout - 10 seconds should be more than enough to run a single unit test
case, if it takes longer than that, consider break the test to make it more
"unit".

- Run make unit pkg=$pkg timeout=5m to make sure all existing unit tests still
pass.

- If there are newly added integration tests, or the changes may alter the
workflow of specific areas, run make itest icase=$icase to validate the
behavior, where icase is the affected test case.


Model Git Commit Messages

This project prefers to keep a clean commit history with well-formed commit
messages. This section illustrates a model commit message and provides a bit
of background for it. This content was originally created by Tim Pope and made
available on his website, however that website is no longer active, so it is
being provided here.

Here’s a model Git commit message:

text
Short (50 chars or less) summary of changes

More detailed explanatory text, if necessary. Wrap it to about 72
characters or so. In some contexts, the first line is treated as the
subject of an email and the rest of the text as the body. The blank
line separating the summary from the body is critical (unless you omit
the body entirely); tools like rebase can get confused if you run the
two together.

Write your commit message in the present tense: "Fix bug" and not "Fixed
bug." This convention matches up with commit messages generated by
commands like git merge and git revert.

Further paragraphs come after blank lines.

- Bullet points are okay, too
- Typically a hyphen or asterisk is used for the bullet, preceded by a
single space, with blank lines in between, but conventions vary here
- Use a hanging indent

Here are some of the reasons why wrapping your commit messages to 72 columns is
a good thing.

- git log doesn't do any special wrapping of the commit messages. With
the default pager of less -S, this means your paragraphs flow far off the edge
of the screen, making them difficult to read. On an 80 column terminal, if we
subtract 4 columns for the indent on the left and 4 more for symmetry on the
right, we’re left with 72 columns.
- git format-patch --stdout converts a series of commits to a series of emails,
using the messages for the message body. Good email netiquette dictates we
wrap our plain text emails such that there’s room for a few levels of nested
reply indicators without overflow in an 80 column terminal.

In addition to the Git commit message structure adhered to within the daemon
all short-commit messages are to be prefixed according to the convention
outlined in the Go project
. All
commits should begin with the subsystem or package primarily affected by the
change. In the case of a widespread change, the packages are to be delimited by
either a '+' or a ','. This prefix seems minor but can be extremely helpful in
determining the scope of a commit at a glance, or when bug hunting to find a
commit which introduced a bug or regression.

Ideal Git Commit Structure

Within the project we prefer small, contained commits for a pull request over a
single giant commit that touches several files/packages. Ideal commits build on
their own, in order to facilitate easy usage of tools like git bisect to git
cherry-pick
. It's preferred that commits contain an isolated change in a
single package. In this case, the commit header message should begin with the
prefix of the modified package. For example, if a commit was made to modify the
lnwallet package, it should start with lnwallet: .

In the case of changes that only build in tandem with changes made in other
packages, it is permitted for a single commit to be made which contains several
prefixes such as: lnwallet+htlcswitch. This prefix structure along with the
requirement for atomic contained commits (when possible) make things like
scanning the set of commits and debugging easier. In the case of changes that
touch several packages, and can only compile with the change across several
packages, a multi: prefix should be used.

Examples of common patterns w.r.t commit structures within the project:

* It is common that during the work on a PR, existing bugs are found and
fixed. If they can be fixed in isolation, they should have their own
commit.
* File restructuring like moving a function to another file or changing order
of functions: with a separate commit because it is much easier to review
the real changes that go on top of the restructuring.
* Preparatory refactorings that are functionally equivalent: own commit.
* Project or package wide file renamings should be in their own commit.
* Ideally if a new package/struct/sub-system is added in a PR, there should
be a single commit which adds the new functionality, with follow up
individual commits that begin to integrate the functionality within the
codebase.
* If a PR only fixes a trivial issue, such as updating documentation on a
small scale, fix typos, or any changes that do not modify the code, the
commit message of the HEAD commit of the PR should end with [skip ci] to
skip the CI checks. When pushing to such an existing PR, the latest commit
being pushed should end with [skip ci] as to not inadvertently trigger the
CI checks.

Sign your git commits

When contributing to lnd it is recommended to sign your git commits. This is
easy to do and will help in assuring the integrity of the tree. See mailing
list entry

for more information.

How to sign your commits?

Provide the -S flag (or --gpg-sign) to git commit when you commit
your changes, for example

shell
$  git commit -m "Commit message" -S

Optionally you can provide a key id after the -S option to sign with a
specific key.

To instruct git to auto-sign every commit, add the following lines to your
~/.gitconfig file:

text
[commit]
gpgsign = true

What if I forgot?

You can retroactively sign your previous commit using --amend, for example

shell
$  git commit -S --amend

If you need to go further back, you can use the interactive rebase
command with 'edit'. Replace HEAD~3 with the base commit from which
you want to start.

shell
$  git rebase -i HEAD~3

Replace 'pick' by 'edit' for the commit that you want to sign and the
rebasing will stop after that commit. Then you can amend the commit as
above. Afterwards, do

shell
$  git rebase --continue

As this will rewrite history, you cannot do this when your commit is
already merged. In that case, too bad, better luck next time.

If you rewrite history for another reason - for example when squashing
commits - make sure that you re-sign as the signatures will be lost.

Multiple commits can also be re-signed with git rebase. For example, signing
the last three commits can be done with:

shell
$  git rebase --exec 'git commit --amend --no-edit -n -S' -i HEAD~3

How to check if commits are signed?

Use git log with --show-signature,

shell
$  git log --show-signature

You can also pass the --show-signature option to git show to check a single
commit.


Pointing to Remote Dependent Branches in Go Modules

It's common that a developer may need to make a change in a dependent project
of lnd such as btcd, neutrino, btcwallet, etc. In order to test changes
without testing infrastructure, or simply make a PR into lnd that will build
without any further work, the go.mod and go.sum files will need to be
updated. Luckily, the go mod command has a handy tool to do this
automatically so developers don't need to manually edit the go.mod file:

shell
$  go mod edit -replace=IMPORT-PATH-IN-LND@LND-VERSION=DEV-FORK-IMPORT-PATH@DEV-FORK-VERSION

Here's an example replacing the lightning-onion version checked into lnd with a version in roasbeef's fork:

shell
$  go mod edit -replace=github.com/lightningnetwork/[email protected]=github.com/roasbeef/lightning-onion@2e5ae87696046298365ab43bcd1cf3a7a1d69695

Use of Log Levels

There are six log levels available: trace, debug, info, warn, error and critical.

Only use error for internal errors that are never expected to happen during
normal operation. No event triggered by external sources (rpc, chain backend,
etc) should lead to an error log.

Use of Golang submodules

Changes to packages that are their own submodules (e.g. they contain a go.mod
and go.sum file, for example tor/go.mod) require a specific process.
We want to avoid the use of local replace directives in the root go.mod,
therefore changes to a submodule are a bit involved.

The main process for updating and then using code in a submodule is as follows:
- Create a PR for the changes to the submodule itself (e.g. edit something in
the tor package)
- Wait for the PR to be merged and a new tag (for example tor/v1.0.x) to be
pushed.
- Create a second PR that bumps the updated submodule in the root go.mod and
uses the new functionality in the main module.

Of course the two PRs can be opened at the same time and be built on top of each
other. But the merge and tag push order should always be maintained.

---

Estimate Route Fee

EstimateRouteFee: A Guide for Wallet Developers

Table of Contents

- Overview
- Operation Modes
- Graph-Based Estimation
- Probe-Based Estimation
- Private Channel and Hop Hint Handling
- Hop Hint Processing
- Integration with Pathfinding
- LSP Detection and Special Handling
- The LSP Detection Heuristic
- How Probing Differs When an LSP is Detected
- Route Hint Transformation for LSP Probing
- Relationship to Zero-Conf Channels
- Fee Assembly After LSP Probing
- Known Limitations and Edge Cases
- Best Practices for Wallet Integration
- Choosing the Appropriate Mode
- Handling Timeouts
- Error Handling
- Fee Presentation
- Implementation Examples
- Basic Graph-Based Estimation
- Invoice-Based Estimation with Timeout
- Future Improvements
- Conclusion

Overview

The EstimateRouteFee RPC call provides wallet applications with fee estimates
for Lightning Network payments. Understanding its operation modes and heuristics
is essential for building reliable payment experiences, particularly when
dealing with private channels and Lightning Service Providers (LSPs).

This document explains the behavioral characteristics, assumptions, and best
practices for integrating EstimateRouteFee into wallet applications, whether
you're building directly on LND or developing third-party wallet software.

Operation Modes

EstimateRouteFee operates in two distinct modes, each optimized for different
use cases and accuracy requirements.

mermaid
flowchart TD
Start([EstimateRouteFee Called]) --> Check{Input Type?}
Check -->|Destination + Amount| Graph[Graph-Based Estimation]
Check -->|Payment Request/Invoice| Invoice[Invoice Processing]

Graph --> LocalPath[Use Local Channel Graph]
LocalPath --> Mission[Apply Mission Control Data]
Mission --> CalcFee[Calculate Route & Fee]
CalcFee --> ReturnFast([Return Fee Estimate<br/>~100ms])

Invoice --> HasHints{Has Route Hints?}
HasHints -->|No| StandardProbe[Standard Probe to Destination]
HasHints -->|Yes| CheckLSP{Check LSP Heuristic}

CheckLSP -->|Not LSP| StandardProbe
CheckLSP -->|Is LSP| LSPProbe[LSP-Aware Probe]

StandardProbe --> SendProbe1[Send Probe with Random Hash]
LSPProbe --> ModifyHints[Transform Route Hints]
ModifyHints --> SendProbe2[Probe to LSP Node]

SendProbe1 --> ProbeResult1[Wait for Probe Result]
SendProbe2 --> ProbeResult2[Calculate LSP Fees]

ProbeResult1 --> ReturnProbe([Return Fee Estimate<br/>1-60s])
ProbeResult2 --> ReturnProbe

Graph-Based Estimation

When provided with a destination public key and amount, EstimateRouteFee
performs local pathfinding using the in-memory channel graph. This mode executes
entirely locally without network interaction, making it fast but potentially
less accurate for complex routing scenarios.

This approach uses your node's view of the network topology and mission control
data (historical payment success rates) to calculate the most economical route.
A 1 BTC maximum fee limit prevents unreasonable calculations. The estimate
represents the difference between sent and received amounts.

Best for well-connected public nodes with sufficient routing information in the
public graph. Response times are typically sub-second.

Probe-Based Estimation

When provided with an invoice, EstimateRouteFee sends probe payments through
the network using a random payment hash. This ensures the payment fails with
"incorrect payment details" at the destination, confirming route viability
without transferring funds.

More accurate than graph-based estimation as it tests actual network conditions
(liquidity, availability, current fees). May take seconds to minutes, especially
for private channels or LSPs.

Private Channel and Hop Hint Handling

Private channels present unique challenges for fee estimation since they don't
exist in the public channel graph. EstimateRouteFee handles these through hop
hints provided in BOLT11 invoices.

Hop Hint Processing

When an invoice contains route hints, EstimateRouteFee treats them as
additional routing information that extends the known network graph. Each hop
hint describes a private channel that can be used to reach the destination,
including the channel's routing policies such as fees and timelock requirements.

The system makes several important assumptions about hop hints:

- Capacity assumptions: Private channels are assumed to have sufficient
capacity for pathfinding calculations. This high capacity assumption
prevents the pathfinding algorithm from prematurely rejecting routes based
on amount constraints, as the receiver is expected to have adequate inbound
liquidity when they include a hop hint for that channel.

- Policy trust: Routing policies in hop hints (base fee, proportional
fee, CLTV delta) are trusted without verification, as there's no way to
independently validate private channel information.

Integration with Pathfinding

Hop hints are treated as legitimate routing options alongside public channels,
enabling paths through both public and private channels. The pathfinding
algorithm works backward from destination to source, making hop hints critical
for reaching private destinations.

Private edges bypass normal validation and capacity checks. The system trusts
invoice creators to provide accurate routing information, as incorrect hints
prevent payment receipt.

LSP Detection and Special Handling

Lightning Service Providers require special handling due to their role as
intermediaries for nodes without direct channel connectivity. EstimateRouteFee
implements sophisticated heuristics to detect LSP scenarios and fundamentally
modifies its probing behavior when an LSP configuration is identified.

The LSP Detection Heuristic

EstimateRouteFee employs a pattern-matching algorithm to identify when a
payment destination is likely behind an LSP. This detection is crucial because
probing through an LSP requires different handling than standard payment
probing.

The heuristic examines the structure of route hints provided in the invoice to
identify characteristic LSP patterns. The detection operates on the principle
that LSPs typically maintain private channels to their users and appear as
public nodes in the network, while the final destination is private.

mermaid
flowchart TD
Start([Route Hints Received]) --> Empty{Empty Hints?}
Empty -->|Yes| NotLSP([Not LSP])
Empty -->|No| CheckTarget{Invoice Target<br/>in Graph?}

CheckTarget -->|Yes| NotLSP
CheckTarget -->|No| GetFirstDest[Get First Hint's<br/>Destination Hop]

GetFirstDest --> CheckPub1{Destination Node<br/>in Graph?}
CheckPub1 -->|Yes| IsLSP([Detected as LSP])
CheckPub1 -->|No| MoreHints{More Hints?}

MoreHints -->|No| NotLSP
MoreHints -->|Yes| NextHint[Check Next Hint]

NextHint --> GetNextDest[Get Destination Hop]
GetNextDest --> CheckPub2{Destination Node<br/>in Graph?}
CheckPub2 -->|Yes| IsLSP
CheckPub2 -->|No| MoreHints

The detection follows three simple rules applied sequentially:

Rule 1: Public Invoice Target → NOT an LSP
- If the invoice target (destination) is a public node that exists in the
channel graph, the payment can be routed directly to it
- This means it's not an LSP setup, regardless of what route hints are provided
- Example: A well-connected merchant node with route hints for liquidity
signaling

Rule 2: Public Destination Hop → IS an LSP
- If at least one route hint has a destination hop (last hop in the route hint)
that is a public node in the graph, LSP detection is triggered
- This indicates the destination hop is an LSP serving a private client
- The private client is reached through the LSP's private channel

Rule 3: All Private Destination Hops → NOT an LSP
- If all destination hops in all route hints are private nodes (not in the
public graph), this is not treated as an LSP setup
- The payment will be routed directly to the invoice destination using the
route hints as additional path information
- This is the standard case for private channel payments

This pattern effectively distinguishes LSP configurations from other routing
scenarios. For instance, some Lightning implementations like CLN include route
hints even for public nodes to signal liquidity availability or preferred
routing paths. The heuristic correctly identifies these as non-LSP scenarios
by Rule 1 (detecting that the invoice target itself is public).

How Probing Differs When an LSP is Detected

When the LSP detection heuristic identifies an LSP configuration,
EstimateRouteFee fundamentally changes its probing strategy. Understanding
these differences is crucial for wallet developers to correctly interpret fee
estimates.

#### Standard Probing Behavior

When no LSP is detected, the probe targets the invoice's actual destination
using complete route hints. The probe amount matches the invoice amount with
standard timelock requirements. Works well for directly reachable destinations.

#### LSP-Aware Probing Behavior

When an LSP configuration is detected, the probing strategy undergoes several
fundamental changes that reflect the unique characteristics of LSP-mediated
payments:

- Probe destination changes to the LSP node - Instead of targeting the
final payment recipient, the probe targets the LSP itself, recognizing that
the service provider handles the final hop independently

- Route hints are modified via prepareLspRouteHints - The final hop is
stripped from all route hints, removing the LSP-to-destination segment
while preserving intermediate hops that help reach the LSP

- Probe amount increases by the LSP's maximum fee - The system calculates
the worst-case fee across all route hints and adds it to the probe amount,
ensuring the estimate accounts for the LSP's forwarding charges

- Timelock requirements switch to LSP's CLTV delta - The probe uses the
LSP's timelock requirements instead of the invoice's, with the final
destination's CLTV added to the estimate after probing

- Modified hints prevent traversal past the LSP - By removing the final
hop, the probe cannot attempt to reach the actual destination, which would
likely fail due to the possible non-existence of LSP-to-user channels in
the public graph

These modifications ensure that fee estimation accurately reflects the two-stage
nature of LSP-mediated payments: first reaching the LSP through the public
network, then the LSP's own hop to the final destination.

Route Hint Transformation for LSP Probing

When an LSP is detected, the system performs a sophisticated transformation of
the route hints to enable accurate fee estimation. This transformation, handled
by the prepareLspRouteHints function, serves three critical purposes.

mermaid
flowchart LR
subgraph "Original Route Hints"
H1[Hop A → Hop B → LSP → Destination]
H2[Hop C → LSP → Destination]
H3[Hop D → Hop E → LSP → Destination]
end

Transform[prepareLspRouteHints<br/>Transformation]

subgraph "Modified for Probing"
M1[Hop A → Hop B → LSP]
M2[Hop C → LSP]
M3[Hop D → Hop E → LSP]
end

LSPHint[Synthetic LSP Hint<br/>Max Fees & CLTV]

H1 --> Transform
H2 --> Transform
H3 --> Transform

Transform --> M1
Transform --> M2
Transform --> M3
Transform --> LSPHint

style LSPHint fill:#f9f,stroke:#333,stroke-width:2px

The transformation serves three purposes:

1. Creates a synthetic LSP hop hint with worst-case fees and CLTV across
all route hints, ensuring conservative but reliable estimates

2. Strips the final hop from all route hints, removing the
LSP-to-destination segment that would cause probe failure

3. Preserves intermediate hops that help reach the LSP

This worst-case approach prioritizes reliability over optimism, particularly
important for mobile wallets where payment success matters more than minimal
fees.

Relationship to Zero-Conf Channels

LSP detection has important interactions with zero-conf channels, which are
commonly used in LSP deployments for instant liquidity provision.

#### Why LSPs Use Zero-Conf Channels

LSPs use zero-conf channels for instant liquidity provision to new users. These
channels enable immediate routing using SCID aliases, remain private/unconfirmed
in the public graph, and rely on LSP-user trust relationships.

#### Impact on LSP Detection

Zero-conf channels align with LSP detection patterns since SCID aliases don't
appear in the public graph, always appearing as private channels. This alignment
reflects real-world LSP deployment patterns and strengthens the heuristic's
effectiveness.

#### Implications for Fee Estimation

Key considerations for zero-conf channels behind LSPs:
- Cannot verify channel existence (not in public graph)
- Capacity assumptions may be optimistic
- Route hint fees trusted without validation
- LSP handles liquidity management

Fee Assembly After LSP Probing

After successful LSP probing, the system adds:
- LSP's worst-case fee for the final hop
- Invoice's final CLTV requirement

This two-stage calculation captures both the cost to reach the LSP and the
LSP's forwarding charges.

Known Limitations and Edge Cases

Probe Success Risk

Theoretical risk of probe completion if destination has bugs or non-standard
behavior. Funds would be lost with only a warning logged. Additionally, probes
may get stuck in the network, which is why setting appropriate timeouts is
crucial. Note that liquidity may shift between the probe and the actual payment
attempt. For larger payments, Multi-Path Payments (MPP) might find better routes
than the single-path probe, potentially resulting in lower actual fees.

LSP Heuristic Accuracy

Can produce false positives (CLN nodes with liquidity hints) and false negatives
(magic routing hints from services like Boltz). Recent improvements check public
graph existence to reduce false positives.

Route Hint Validation

Route hints trusted without validation. Assumes invoice creators have incentives
for accuracy (incorrect hints prevent payment receipt).

Capacity and Liquidity Assumptions

Capacity assumptions may exceed actual availability. Payments may fail despite
successful estimation—handle failures gracefully.

Best Practices for Wallet Integration

Choosing the Appropriate Mode

Graph-based: Quick estimates for well-connected public nodes when you
have the destination key.

Probe-based: Accurate estimates for invoices, especially:
- Private or poorly connected destinations
- Large payments requiring fee accuracy
- LSP or complex routing scenarios

Handling Timeouts

Set timeouts based on UX needs: 30s standard, 60s for important payments, 15s
for responsive UIs. Show progress indicators and provide cancel options.
Consider graph-based fallback for consistent timeouts.

Error Handling

Common failures:
- NO_ROUTE: Destination offline, no liquidity, or unreachable
- INSUFFICIENT_BALANCE: Insufficient funds for payment + fees
- TIMEOUT: Probe exceeded timeout (congestion or poor connectivity)

Translate errors to user-friendly messages with suggested actions.

Fee Presentation

Consider showing fee ranges or confidence levels since estimates may be
conservative (especially for LSPs using worst-case fees). Probe-based estimates
reflect current conditions but may change before actual payment.

Implementation Examples

Basic Graph-Based Estimation

For graph-based estimation, provide the destination public key and payment
amount. The response will include the routing fee in millisatoshis and an
estimated timelock delay. This mode is ideal for quick estimates where you have
the destination's node ID.

shell

Estimate fee for 100,000 satoshi payment to a specific node


lncli estimateroutefee --dest 0266a18ed969ef95c8a5aa314b443b2b3b8d91ed1d9f8e95476f5f4647efdec079 --amt 100000

The typical flow involves calling EstimateRouteFee with just the destination
and amount parameters. The response arrives quickly (usually under 100ms) since
it uses only local data. Convert the returned fee from millisatoshis to satoshis
for display, and consider showing the timelock delay to inform users about the
maximum time their funds might be locked.

Invoice-Based Estimation with Timeout

For invoice-based estimation, provide the full payment request string and
optionally specify a timeout. This mode sends actual probe payments, so timeouts
are important to prevent long waits for poorly connected destinations.

shell

Estimate fee for an invoice with 60-second timeout


lncli estimateroutefee --pay_req lnbc100n1p3e... --timeout 60s

Shorter timeout for responsive UIs


lncli estimateroutefee --pay_req lnbc100n1p3e... --timeout 15s

When calling EstimateRouteFee with a payment request, always set a reasonable
timeout (30-60 seconds is typical). The response includes not just the fee but
also a failure reason if the probe fails. Common failure reasons include
NO_ROUTE (destination unreachable), INSUFFICIENT_BALANCE (not enough funds), or
TIMEOUT (probe took too long). Handle these gracefully in your UI to guide users
appropriately.

Future Improvements

The EstimateRouteFee implementation continues to evolve based on real-world
usage patterns. Ongoing discussions in the LND community focus on:

Multi-Path Payment Support: Extending fee estimation to support MPP
scenarios where payments split across multiple routes.

Trampoline Routing Compatibility: Adapting fee estimation for future
trampoline routing implementations where intermediate nodes handle pathfinding.

Blinded Path Integration: Ensuring fee estimation works correctly with
blinded paths as they become more prevalent in the network.

Wallet developers should monitor LND releases and participate in community
discussions to stay informed about improvements and changes to fee estimation
behavior.

Conclusion

EstimateRouteFee provides essential functionality for wallet applications to
present accurate fee information to users. By understanding its dual-mode
operation, hop hint processing, and LSP detection heuristics, developers can
build robust payment experiences that handle both simple public node payments
and complex private channel scenarios.

The key to successful integration lies in choosing the appropriate estimation
mode for each use case, handling edge cases gracefully, and presenting fee
information in a way that helps users make informed payment decisions. As the
Lightning Network evolves, staying informed about EstimateRouteFee
improvements will ensure wallets continue to provide accurate and reliable fee
estimates.

---

Forwarding History Privacy

Forwarding History Privacy Management

Introduction

The Lightning Network excels at providing fast, low-cost payments with strong
privacy properties. However, routing nodes and Lightning Service Providers
(LSPs) face a unique challenge: their operational databases accumulate
forwarding history that, if compromised or subpoenaed, could reveal sensitive
information about payment flows across the network. This document explores the
privacy implications of forwarding logs and introduces LND's solution for
implementing data retention policies without migrating to a new node instance.

Understanding Forwarding History

When your LND node routes a payment between two other nodes, it records detailed
information about that forwarding event in its database. This serves several
important operational purposes, including fee accounting, channel performance
analysis, and troubleshooting. Each forwarding event captures the incoming and
outgoing channels, amounts transferred, fees earned, and precise timestamps.

Over months or years of operation, a busy routing node accumulates millions of
these records. While this historical data provides valuable insights into node
performance, it also creates a potential privacy liability. An attacker who
gains access to this database—whether through a security breach, or physical
seizure—could potentially reconstruct payment paths across the network by
correlating forwarding events across multiple compromised nodes.

Privacy Implications for Operators

For individual routing node operators, the privacy risks of retaining unlimited
forwarding history are modest but real. If an adversary gains access to your
node's database, they could analyze your forwarding patterns to infer
information about the network topology you participate in and potentially
identify payment patterns involving your channels.

The Traditional Dilemma

Prior to this feature, routing node operators faced an uncomfortable tradeoff.
To implement a data retention policy and purge old forwarding logs, the only
practical option was to shut down the node, reset the database, and restore
channels from backups—effectively migrating to a fresh node instance. This
process carries significant operational risks, including potential channel
closures, loss of channel state, and extended downtime. For LSPs serving
customers around the clock, such maintenance windows are highly disruptive.

The DeleteForwardingHistory Solution

LND's DeleteForwardingHistory RPC addresses this challenge by providing a
safe, reversible-only-forward way to implement data retention policies. The
feature allows operators to specify a time threshold—either as a relative
duration or an absolute timestamp—and permanently delete all forwarding events
older than that threshold. The deletion operation executes in configurable
batches to avoid holding large database locks, and it returns statistics about
the deleted events, including the total fees earned during that period for
accounting purposes.

How It Works

The deletion mechanism operates at the database layer, directly manipulating the
forwarding log bucket in LND's embedded bbolt database. The forwarding log
stores events using nanosecond-precision timestamps as keys, which enables
efficient time-based range queries. When you invoke a deletion, LND constructs a
cursor-based iteration that walks through events in chronological order,
collecting keys for events older than your specified cutoff time. It then
deletes these events in batches, with each batch executed within its own
database transaction.

mermaid
sequenceDiagram
participant User
participant CLI
participant Router RPC
participant ForwardingLog
participant Database

User->>CLI: deletefwdhistory --age="-720h"
CLI->>Router RPC: DeleteForwardingHistory(duration: "-720h")

Router RPC->>Router RPC: Parse duration → absolute time
Router RPC->>Router RPC: Validate minimum age (1 hour)

loop For each batch (default: 10,000 events)
Router RPC->>ForwardingLog: DeleteForwardingEvents(endTime, batchSize)
ForwardingLog->>Database: Begin transaction
ForwardingLog->>Database: Iterate events <= endTime
ForwardingLog->>ForwardingLog: Calculate fees for batch
ForwardingLog->>Database: Delete batch of keys
ForwardingLog->>Database: Commit transaction
end

ForwardingLog->>Router RPC: Return stats (deleted count, total fees)
Router RPC->>CLI: DeleteForwardingHistoryResponse
CLI->>User: Display deletion results

This batched approach ensures that even nodes with millions of forwarding events
can safely purge old data without causing database performance issues. Each
batch completes within a separate transaction, limiting lock contention and
allowing other database operations to proceed between batches.

Security Considerations

The implementation includes several safeguards to prevent accidental data loss.
First, the RPC enforces a minimum age requirement: you cannot delete events less
than one hour old. This prevents mishaps where an operator accidentally deletes
recent forwarding history due to a timestamp parsing error or misunderstanding
the time format. The CLI command additionally requires explicit confirmation
before proceeding with the deletion.

Second, the RPC requires the "offchain:write" macaroon permission, treating
forwarding history deletion as a sensitive write operation similar to payment
deletion. This ensures that only authorized users can purge forwarding data.

Third, the operation is logged extensively. LND writes detailed log messages
before and after each deletion operation, recording the time threshold, batch
size, number of events deleted, and total fees from the deleted period. These
audit trails help operators verify that deletions executed as intended.

Fee Accounting

One critical requirement for LSPs implementing data retention policies is
maintaining accurate accounting records. Even after purging old forwarding
events for privacy reasons, operators need to know how much revenue their node
generated during those periods for tax reporting and business analytics.

The deletion operation addresses this by calculating and returning the sum of
all fees earned from the deleted events. For each event, LND computes the fee as
the difference between the incoming and outgoing amounts, then aggregates these
fees across all deleted events. The response includes this total in
millisatoshis, allowing operators to record their earnings before purging the
detailed records.

mermaid
graph TD
A[Forwarding Event] --> B{Calculate Fee}
B --> C[Fee = AmtIn - AmtOut]
C --> D[Accumulate to TotalFees]
D --> E{More Events?}
E -->|Yes| A
E -->|No| F[Return Total to User]
F --> G[Operator Records<br/>for Accounting]
G --> H[Delete Detailed Events]

This approach separates accounting data from operational surveillance data. You
can maintain aggregate financial records while minimizing the detailed
forwarding logs that pose privacy risks.

Usage Guide

Command Line Interface

The lncli deletefwdhistory command provides the primary interface for
operators. The command accepts time specifications in two formats: relative
durations for convenience, or absolute Unix timestamps for precision.

For most use cases, relative durations offer the most intuitive interface. To
implement a 90-day retention policy, you would periodically run:

bash
lncli deletefwdhistory --age="-90d"

The supported time units cover a wide range of retention policies:

- Seconds (s) and minutes (m) for testing or very short-term retention
- Hours (h) and days (d) for common operational timeframes
- Weeks (w) for weekly cleanup schedules
- Months (M, averaged to 30.44 days) for typical retention policies
- Years (y, averaged to 365.25 days) for long-term archives

The minus sign prefix indicates you're specifying how far back in time to
delete. This convention matches the relative time syntax used elsewhere in LND
and makes the intent clear: "delete events from more than X time ago."

For precise control, you can specify an absolute Unix timestamp:

bash
lncli deletefwdhistory --before=1704067200

This deletes all events before January 1, 2024 00:00:00 UTC. Absolute timestamps
are particularly useful when implementing policies tied to specific dates, such
as calendar year boundaries for accounting purposes or regulatory compliance
deadlines.

Batch Size Tuning

The --batch_size flag controls how many events are deleted per database
transaction. The default value of 10,000 provides a good balance for most nodes,
but you may want to adjust this based on your node's characteristics.

For nodes with slower disk I/O or running on resource-constrained hardware,
reducing the batch size decreases the duration of each database lock, improving
responsiveness to concurrent operations:

bash
lncli deletefwdhistory --age="-1M" --batch_size=5000

Conversely, for nodes with fast SSDs and low concurrent load, increasing the
batch size can speed up the overall deletion process:

bash
lncli deletefwdhistory --age="-1M" --batch_size=25000

The implementation caps the maximum batch size at 50,000 to prevent excessively
large transactions from degrading database performance.

Automation and Scheduling

Most operators will want to automate forwarding history cleanup rather than
running deletions manually. The command integrates naturally with cron jobs or
systemd timers. For a monthly cleanup maintaining a 90-day retention window:

bash

Run at 3 AM on the first day of each month


0 3 1 /usr/local/bin/lncli deletefwdhistory --age="-90d" --force >> /var/log/lnd/fwdhistory_cleanup.log 2>&1

The --force flag skips the interactive confirmation prompt, which is
required for unattended automation. In production you should implement
additional safeguards such as pre-deletion validation checks and alerting
on unexpected results.

For more sophisticated automation, consider implementing a script that:

1. Queries current forwarding history statistics
2. Calculates the appropriate deletion threshold based on database size and growth rate
3. Executes the deletion
4. Records the fees returned for accounting
5. Monitors the resulting database size and alerts if disk space isn't reclaimed as expected

Database Compaction

Deleting forwarding events frees space within LND's bbolt database, but this
space isn't immediately returned to the operating system. bbolt uses a
copy-on-write structure where deleted data leaves "free pages" that can be
reused for future writes, but the overall file size doesn't shrink until you
compact the database.

LND supports automatic compaction via the configuration option:

text
db.bolt.auto-compact=true

With auto-compaction enabled, LND periodically performs compaction during normal
operation, typically triggered when the amount of free space exceeds a
threshold. However, after a large deletion operation, you may want to trigger
compaction immediately to reclaim disk space.

The recommended approach is to schedule compaction shortly after your regular
deletion operations:

1. Run deletefwdhistory to purge old events
2. Restart LND with --db.bolt.auto-compact=true if not already enabled
3. Monitor database file size to confirm space reclamation

Be aware that database compaction requires free disk space equal to the current
database size during the operation, as it creates a new, compacted copy of the
database before replacing the original.

Integration with Existing Tools

Forwarding History Analysis

The deletion operation doesn't interfere with LND's existing forwardinghistory
RPC, which allows you to query and analyze forwarding events. After a deletion,
queries for time ranges that have been purged will simply return no events for
those periods, while more recent events remain accessible.

This means you can continue using analytical tools and scripts that query
forwarding history, but you should design them to handle sparse historical data
gracefully. Tools should not assume that forwarding history extends back to the
node's inception date.

Channel Analytics

Similarly, channel performance analysis tools that rely on forwarding history
will only have access to events within your retention window. When evaluating
channel performance metrics like forwarding frequency or fee revenue, be mindful
that historical data before your retention cutoff is no longer available.

For long-term performance tracking, consider aggregating statistics before
purging detailed events. You might maintain summary records showing weekly or
monthly aggregate forwarding counts and fees per channel, even after deleting
the individual event records.

Privacy Best Practices

While the deletion feature provides operators with a mechanism to implement data
retention policies, it's important to understand what it does and doesn't
protect against.

What Deletion Protects

Deleting old forwarding history reduces your node's exposure if the database is
compromised in the future. An attacker who gains access to your node after
you've implemented a 90-day retention policy can only observe the last 90 days
of forwarding activity, not the entire operational history. This limits the
window during which surveillance or correlation attacks could be performed using
your node's data.

What Deletion Doesn't Protect

The revocation log for _active_ channels contains information that can be used
to reconstruct transaction flows. Once channels are closed, this data is
automatically deleted.

The normal logs of a node also contain information that can be used to correlate
transactions. Users can set up automated systems to manually purge logs, or
configure the logging directory to a purely in-memory file system.

Defense in Depth

Forwarding history deletion should be one component of a comprehensive privacy
strategy, not your only defense. Other important measures include:

- Restricting physical and network access to the node
- Implementing strong authentication and access controls
- Regularly auditing who has access to the node and its backups
- Using channel aliases and avoiding personally identifiable information in
channel names
- Running your node over Tor to hide the network-level correlation between node
identity and IP address


The deletion feature gives you control over how long your node retains detailed
forwarding records, but it doesn't eliminate all privacy risks inherent in
operating a Lightning Network routing node.

Troubleshooting

Database Lock Timeouts

During deletion of very large numbers of events, you might encounter database
lock timeout errors if other operations are trying to access the database
concurrently. If this occurs:

1. Reduce the batch size to shorten each transaction
2. Schedule deletions during low-traffic periods
3. Temporarily pause other operations that query forwarding history frequently

Insufficient Disk Space for Compaction

Database compaction requires temporary free space roughly equal to the size of
your database. If compaction fails due to insufficient disk space, you'll need
to free up space before the compaction can proceed:

1. Delete other unnecessary files from the disk
2. Move log files or other non-critical data to alternate storage
3. Consider whether you can safely delete older database backups

Performance Considerations

Deletion performance scales linearly with the number of events being deleted.
Performance varies significantly depending on storage hardware and database
size; operators should benchmark on their own hardware before relying on
specific throughput estimates.

The operation's impact on node performance during deletion is minimal. Each
batch executes quickly, and the gaps between batches allow other database
operations to proceed. You can safely run deletions while the node is actively
routing payments, though you may want to avoid doing so during peak traffic
times on very busy nodes.

Database compaction has a more significant performance impact, as it requires
LND to copy the entire database. During compaction, expect elevated CPU and disk
I/O, and budget several minutes for the operation to complete depending on your
database size. LND remains operational during compaction, but you may observe
increased latency for database-heavy operations.

---

Fuzz

Fuzzing LND #

The following runs all fuzz tests on default settings:

shell
$  make fuzz

The following runs all fuzz tests inside the lnwire package, each for a total of 1 minute, using 4 procs.
It is recommended that processes be set to the number of processor cores in the system:
shell
$  make fuzz pkg=lnwire fuzztime=1m parallel=4

Alternatively, individual fuzz tests can be run manually by setting the working directory to the location of the .go file holding the fuzz tests.
The go test command can only test one fuzz test at a time:
shell
$  cd lnwire
$ go test -fuzz=FuzzAcceptChannel -fuzztime=1m -parallel=4

The following can be used to show all fuzz tests in the working directory:
shell
$  cd lnwire
$ go test -list=Fuzz.*

Fuzz tests can be run as normal tests, which only runs the seed corpus:

shell
$  cd lnwire
$ go test -run=FuzzAcceptChannel -parallel=4

The generated corpus values can be found in the $(go env GOCACHE)/fuzz directory.

Options ##


Several parameters can be appended to the end of the make commands to tune the build process or the way the fuzzer runs.
- fuzztime specifies how long each fuzz test runs for, corresponding to the go test -fuzztime option. The default is 30s.
- parallel specifies the number of parallel processes to use while running the harnesses, corresponding to the go test -parallel option.
- pkg specifies the lnd packages to build or fuzz. The default is to build and run all available packages (brontide lnwire watchtower/wtwire zpay32). This can be changed to build/run against individual packages.

Corpus ##


Fuzzing generally works best with a corpus that is of minimal size while achieving the maximum coverage.

Disclosure ##


If you find any crashers that affect LND security, please disclose with the information found here.

---

Gossip Rate Limiting

Gossip Rate Limiting Configuration Guide

When running a Lightning node, one of the most critical yet often overlooked
aspects is properly configuring the gossip rate limiting system. This guide will
help you understand how LND manages outbound gossip traffic and how to tune
these settings for your specific needs.

Understanding Gossip Rate Limiting

At its core, LND uses a token bucket algorithm to control how much bandwidth it
dedicates to sending gossip messages to other nodes. Think of it as a bucket
that fills with tokens at a steady rate. Each time your node sends a gossip
message, it consumes tokens equal to the message size. If the bucket runs dry,
messages must wait until enough tokens accumulate.

This system serves an important purpose: it prevents any single peer, or group
of peers, from overwhelming your node's network resources. Without rate
limiting, a misbehaving peer could request your entire channel graph repeatedly,
consuming all your bandwidth and preventing normal operation.

Core Configuration Options

The gossip rate limiting system has several configuration options that work
together to control your node's behavior.

Setting the Sustained Rate: gossip.msg-rate-bytes

The most fundamental setting is gossip.msg-rate-bytes, which determines how
many bytes per second your node will allocate to outbound gossip messages. This
rate is shared across all connected peers, not per-peer.

The default value of 102,400 bytes per second (100 KB/s) works well for most
nodes, but you may need to adjust it based on your situation. Setting this value
too low can cause serious problems. When the rate limit is exhausted, peers
waiting to synchronize must queue up, potentially waiting minutes between
messages. Values below 50 KB/s can make initial synchronization fail entirely,
as peers timeout before receiving the data they need.

Managing Burst Capacity: gossip.msg-burst-bytes

The burst capacity, configured via gossip.msg-burst-bytes, determines the
initial capacity of your token bucket. This value must be greater than
gossip.msg-rate-bytes for the rate limiter to function properly. The burst
capacity represents the maximum number of bytes that can be sent immediately
when the bucket is full.

The default of 204,800 bytes (200 KB) is set to be double the default rate
(100 KB/s), providing a good balance. This ensures that when the rate limiter
starts or after a period of inactivity, you can send up to 200 KB worth of
messages immediately before rate limiting kicks in. Any single message larger
than this value can never be sent, regardless of how long you wait.

Controlling Concurrent Operations: gossip.filter-concurrency

When peers apply gossip filters to request specific channel updates, these
operations can consume significant resources. The gossip.filter-concurrency
setting limits how many of these operations can run simultaneously. The default
value of 5 provides a reasonable balance between resource usage and
responsiveness.

Large routing nodes handling many simultaneous peer connections might benefit
from increasing this value to 10 or 15, while resource-constrained nodes should
keep it at the default or even reduce it slightly.

Preventing Spam: gossip.ban-threshold

To protect your node from spam and misbehaving peers, LND uses a ban score
system controlled by gossip.ban-threshold. Each time a peer sends a gossip
message that is considered invalid, its ban score is incremented. Once the score
reaches this threshold, the peer is banned for a default of 48 hours, and your
node will no longer process gossip messages from them.

A gossip message can be considered invalid for several reasons, including:
- Invalid signature on the announcement.
- Stale timestamp, older than what we already have.
- Too many channel updates for the same channel in a short period.
- Announcing a channel that is not found on-chain.
- Announcing a channel that has already been closed.
- Announcing a channel with an invalid proof.

The default value is 100. Setting this value to 0 disables banning completely,
which is not recommended for most operators.

Understanding Connection Limits: num-restricted-slots

The num-restricted-slots configuration deserves special attention because it
directly affects your gossip bandwidth requirements. This setting limits inbound
connections, but not in the way you might expect.

LND maintains a three-tier system for peer connections. Peers you've ever had
channels with enjoy "protected" status and can always connect. Peers currently
opening channels with you have "temporary" status. Everyone else—new peers
without channels—must compete for the limited "restricted" slots.

When a new peer without channels connects inbound, they consume one restricted
slot. If all slots are full, additional peers are turned away. However, as soon
as a restricted peer begins opening a channel, they're upgraded to temporary
status, freeing their slot. This creates breathing room for large nodes to form
new channel relationships without constantly rejecting connections.

The relationship between restricted slots and rate limiting is straightforward:
more allowed connections mean more peers requesting data, requiring more
bandwidth. A reasonable rule of thumb is to allocate at least 1 KB/s of rate
limit per restricted slot.

Calculating Appropriate Values

To set these values correctly, you need to understand your node's position in
the network and its typical workload. The fundamental question is: how much
gossip traffic does your node actually need to handle?

Start by considering how many peers typically connect to your node. A hobbyist
node might have 10-20 connections, while a well-connected routing node could
easily exceed 100. Each peer generates gossip traffic when syncing channel
updates, announcing new channels, or requesting historical data.

The calculation itself is straightforward. Take your average message size
(approximately 210 bytes for gossip messages), multiply by your peer count and
expected message frequency, then add a safety factor for traffic spikes. Since
each channel generates approximately 842 bytes of bandwidth (including both
channel announcements and updates), you can also calculate based on your
channel count. Here's the formula:

text
rate = avg_msg_size × peer_count × msgs_per_second × safety_factor

Let's walk through some real-world examples to make this concrete.

For a small node with 15 peers, you might see 10 messages per peer per second
during normal operation. With an average message size of 210 bytes and a safety
factor of 1.5, you'd need about 47 KB/s. Rounding up to 50 KB/s provides
comfortable headroom.

A medium-sized node with 75 peers faces different challenges. These nodes often
relay more traffic and handle more frequent updates. With 15 messages per peer
per second, the calculation yields about 237 KB/s. Setting the limit to 250 KB/s
ensures smooth operation without waste.

Large routing nodes require the most careful consideration. With 150 or more
peers and high message frequency, bandwidth requirements can exceed 1 MB/s.
These nodes form the backbone of the Lightning Network and need generous
allocations to serve their peers effectively.

Remember that the relationship between restricted slots and rate limiting is
direct: each additional slot potentially adds another peer requesting data. Plan
for at least 1 KB/s per restricted slot to maintain healthy synchronization.

Network Size and Geography

The Lightning Network's growth directly impacts your gossip bandwidth needs.
With over 80,000 public channels at the time of writing, each generating
multiple updates daily, the volume of gossip traffic continues to increase. A
channel update occurs whenever a node adjusts its fees, changes its routing
policy, or goes offline temporarily. During volatile market conditions or fee
market adjustments, update frequency can spike dramatically.

Geographic distribution adds another layer of complexity. If your node connects
to peers across continents, the inherent network latency affects how quickly you
can exchange messages. However, this primarily impacts initial connection
establishment rather than ongoing rate limiting.

Troubleshooting Common Issues

When rate limiting isn't configured properly, the symptoms are often subtle at
first but can cascade into serious problems.

The most common issue is slow initial synchronization. New peers attempting to
download your channel graph experience long delays between messages. You'll see
entries in your logs like "rate limiting gossip replies, responding in 30s" or
even longer delays. This happens because the rate limiter has exhausted its
tokens and must wait for refill. The solution is straightforward: increase your
msg-rate-bytes setting.

Peer disconnections present a more serious problem. When peers wait too long for
gossip responses, they may timeout and disconnect. This creates a vicious cycle
where peers repeatedly connect, attempt to sync, timeout, and reconnect. Look
for "peer timeout" errors in your logs. If you see these, you need to increase
your rate limit.

Sometimes you'll notice unusually high CPU usage from your LND process. This
often indicates that many goroutines are blocked waiting for rate limiter
tokens. The rate limiter must constantly calculate delays and manage waiting
threads. Increasing the rate limit reduces this contention and lowers CPU usage.

To debug these issues, focus on your LND logs rather than high-level commands.
Search for "rate limiting" messages to understand how often delays occur and how
long they last. Look for patterns in peer disconnections that might correlate
with rate limiting delays. The specific commands that matter are:

bash

View peer connections and sync state


lncli listpeers | grep -A5 "sync_type"

Check recent rate limiting events


grep "rate limiting" ~/.lnd/logs/bitcoin/mainnet/lnd.log | tail -20

Pay attention to log entries showing "Timestamp range queue full" if you've
implemented the queue-based approach—this indicates your system is shedding load
due to overwhelming demand.

Best Practices for Configuration

Experience has shown that starting with conservative (higher) rate limits and
reducing them if needed works better than starting too low and debugging
problems. It's much easier to notice excess bandwidth usage than to diagnose
subtle synchronization failures.

Monitor your node's actual bandwidth usage and sync times after making changes.
Most operating systems provide tools to track network usage per process. When
adjusting settings, make gradual changes of 25-50% rather than dramatic shifts.
This helps you understand the impact of each change and find the sweet spot for
your setup.

Keep your burst size at least double the largest message size you expect to
send. While the default 200 KB is usually sufficient, monitor your logs for any
"message too large" errors that would indicate a need to increase this value.

As your node grows and attracts more peers, revisit these settings periodically.
What works for 50 peers may cause problems with 150 peers. Regular review
prevents gradual degradation as conditions change.

Configuration Examples

For most users running a personal node, conservative settings provide reliable
operation without excessive resource usage:

text
[Application Options]
gossip.msg-rate-bytes=204800
gossip.msg-burst-bytes=409600
gossip.filter-concurrency=5
num-restricted-slots=100

Well-connected nodes that route payments regularly need more generous
allocations:

text
[Application Options]
gossip.msg-rate-bytes=524288
gossip.msg-burst-bytes=1048576
gossip.filter-concurrency=10
num-restricted-slots=200

Large routing nodes at the heart of the network require the most resources:

text
[Application Options]
gossip.msg-rate-bytes=1048576
gossip.msg-burst-bytes=2097152
gossip.filter-concurrency=15
num-restricted-slots=300

Critical Warning About Low Values

Setting gossip.msg-rate-bytes below 50 KB/s creates serious operational
problems that may not be immediately obvious. Initial synchronization, which
typically transfers 10-20 MB of channel graph data, can take hours or fail
entirely. Peers appear to connect but remain stuck in a synchronization loop,
never completing their initial download.

Your channel graph remains perpetually outdated, causing routing failures as you
attempt to use channels that have closed or changed their fee policies. The
gossip subsystem appears to work, but operates so slowly that it cannot keep
pace with network changes.

During normal operation, a well-connected node processes hundreds of channel
updates per minute. Each update is small, but they add up quickly. Factor in
occasional bursts during network-wide fee adjustments or major routing node
policy changes, and you need substantial headroom above the theoretical minimum.

The absolute minimum viable configuration requires at least enough bandwidth to
complete initial sync in under an hour and process ongoing updates without
falling behind. This translates to no less than 50 KB/s for even the smallest
nodes.

---

Key Import

Overview

This document serves as an introductory point for users interested in reducing
their hot-wallet risks, allowing them to maintain on-chain funds outside of
lnd but still be able to manage them within lnd. As of v0.13.0-beta, lnd
is able to import BIP-0049 and BIP-0084 extended public keys either at the
account path (m/purpose'/coin_type'/account') or at the address index path
(m/purpose'/coin_type'/account'/change/address_index) as watch-only through
the WalletKit APIs.

Note that in order to follow the rest of this document and/or use the
WalletKit APIs, users will need to obtain an lnd build compiled with the
walletrpc tag. Our release builds already include this tag by default, so this
would only be necessary when compiling from source.

lnd's Default Wallet Accounts

Upon initializing lnd, a wallet is created with four default accounts:

* A custom BIP-0049 account (more on this later) to generate NP2WKH external
addresses.
* A BIP-0084 account to generate P2WKH external and change addresses.
* A catch-all BIP-0049 account where all imported BIP-0049 address keys (NP2WKH
addresses) exist within.
* A catch-all BIP-0084 account where all imported BIP-0049 address keys (P2WKH
addresses) exist within.

Prior to v0.13.0-beta, these accounts were abstracted away from users. As part
of the key import feature, they are now exposed through the new WalletKit RPCs
(ListAccounts, ImportAccount, ImportPublicKey) and the lncli wallet
accounts
command.

shell
$ lncli wallet accounts
NAME:
lncli wallet accounts - Interact with wallet accounts.

USAGE:
lncli wallet accounts command [command options] [arguments...]

COMMANDS:
list Retrieve information of existing on-chain wallet accounts.
import Import an on-chain account into the wallet through its extended public key.
import-pubkey Import a public key as watch-only into the wallet.

OPTIONS:
--help, -h show help

Account Details

Before interacting with the new set of APIs, users will want to become familiar
with how wallet accounts are represented within lnd. The
WalletKit.ListAccounts RPC or lncli wallet accounts list command can be used
to retrieve the details of accounts.

shell
$ lncli wallet accounts list
{
"accounts": [
{
"name": "default",
"address_type": "HYBRID_NESTED_WITNESS_PUBKEY_HASH",
"extended_public_key": "upub5EbJZz2tYCpPFgDAMDnXpTeLs5EMNJAfyzRKQuUiTugSaJDjnDdk9vNcENzpw1FnxkerNW7jLuBeoxmcGMtopGExmaWqrMB7wRgU8tExTMz",
"master_key_fingerprint": null,
"derivation_path": "m/49'/0'/0'",
"external_key_count": 0,
"internal_key_count": 0,
"watch_only": false
},
{
"name": "default",
"address_type": "WITNESS_PUBKEY_HASH",
"extended_public_key": "vpub5Z9beF6NYCrHeDmKC38tM3xXMDFFSARa9sdHRPChEMGqtxiELfZB8hm6FwBpBvfPpX2HGG8edYVV9Wupe43PEJJhhfnz1egtQNNaDXyYExn",
"master_key_fingerprint": null,
"derivation_path": "m/84'/0'/0'",
"external_key_count": 0,
"internal_key_count": 0,
"watch_only": false
}
]
}

There's a lot to unpack in the response above, so let's cover each account field
in detail. As mentioned above, four default accounts should exist, though only
two are shown in the output. The catch-all imported accounts are hidden by
default until a key has been imported into them.

* name: Each account has a name it can be identified by. lnd's default
spendable accounts have the name "default". The default catch-all imported
accounts have the name "imported".
* extended_public_key: The BIP-0044 extended public key for the account. Any
addresses generated for the account are derived from this key. Each key has a
version prefix that identifies the chain and derivation scheme being used. At
the time of writing, lnd supports the following versions:
* xpub/tpub: The commonly used version prefix originally intended for
BIP-0032 mainnet/testnet extended keys. Since lnd does not support
BIP-0032 extended keys, this version serves as a catch-all for the other
versions.
* ypub/upub: The version prefix for BIP-0049 mainnet/testnet extended keys.
* zpub/vpub: The version prefix for BIP-0084 mainnet/testnet extended keys.
* address_type: The type of addresses the account can derive. There are three
supported address types:
* WITNESS_PUBKEY_HASH: The standard derivation scheme for BIP-0084 with
P2WKH for external and change addresses.
* NESTED_WITNESS_PUBKEY_HASH: The standard derivation scheme for BIP-0049
with P2WKH for external and change addresses.
* HYBRID_NESTED_WITNESS_PUBKEY_HASH A custom derivation scheme for BIP-0049
used by lnd where NP2WKH is used for external addresses and P2WKH for
change addresses.
* master_key_fingerprint: The 4 byte fingerprint of the master key
corresponding to the account. This is usually required by hardware
wallet/external signers to identify the proper signing key.
* derivation_path: The BIP-0044 derivation path used on the master key to
obtain the account key.
* external_key_count: The number of external addresses generated.
* internal_key_count: The number of change addresses generated.
* watch_only: Whether the wallet has private key information for the account.
lnd's default wallet accounts always have private key information, so this
value is false.

Key Import

An existing limitation to the key import APIs is that events (deposits/spends)
for imported keys, including those derived from an imported account, will only
be detected by lnd if they happen after the import. Rescans to detect past
events are currently not supported, but will come at a later time.

Account Key Import

The WalletKit.ImportAccount RPC and lncli wallet accounts import command can
be used to import an account. At the time of writing, importing an account has
the following request parameters:

* name (required): A name to identify the imported account with.
* extended_public_key (required): A public key that corresponds to a wallet account
represented as an extended key. It must conform to a derivation path of the
form m/purpose'/coin_type'/account'.
* master_key_fingerprint (optional): The fingerprint of the root key (also
known as the key with derivation path m/) from which the account public key
was derived from. This may be required by some hardware wallets for proper
identification and signing.
* address_type (optional): An address type is only required when the extended
account public key has a legacy version (xpub, tpub, etc.), such that the
wallet cannot detect what address scheme it belongs to.
* dry_run (optional): Whether a dry run should be attempted when importing the
account. This serves as a way to confirm whether the account is being imported
correctly by returning the first N addresses for the external and internal
branches of the account. If these addresses match as expected, then it should
be safe to import the account as is.

For the sake of simplicity, we'll present an example with two lnd nodes Alice
and Bob, where Alice acts as a signer _only_, and Bob manages Alice's on-chain
BIP-0084 account by crafting transactions and watching/spending addresses. Since
Alice will only act as a signer, we'll want to import her BIP-0084 account into
Bob's node, which will require knowledge of Alice's extended public key.

Alice's BIP-0084 extended public key can be obtained as follows.

shell
$ lncli-alice wallet accounts list --name=default --address_type=p2wkh
{
"accounts": [
{
"name": "default",
"address_type": "WITNESS_PUBKEY_HASH",
"extended_public_key": "vpub5Z9beF6NYCrHeDmKC38tM3xXMDFFSARa9sdHRPChEMGqtxiELfZB8hm6FwBpBvfPpX2HGG8edYVV9Wupe43PEJJhhfnz1egtQNNaDXyYExn",
"master_key_fingerprint": null,
"derivation_path": "m/84'/0'/0'",
"external_key_count": 0,
"internal_key_count": 0,
"watch_only": false
}
]
}

Bob can then import the account with the following command:

shell
$ lncli-bob wallet accounts import vpub5Z9beF6NYCrHeDmKC38tM3xXMDFFSARa9sdHRPChEMGqtxiELfZB8hm6FwBpBvfPpX2HGG8edYVV9Wupe43PEJJhhfnz1egtQNNaDXyYExn alice

Before Bob imports the account, they may want to confirm the account is being
imported using the correct derivation scheme. This can be done with the dry run
request parameter. When a dry run is done, the response will include the usual
account details, as well as the first 5 external and change addresses, which can
be used to confirm they match with what the account owner expects.

shell
$ lncli-bob wallet accounts import vpub5Z9beF6NYCrHeDmKC38tM3xXMDFFSARa9sdHRPChEMGqtxiELfZB8hm6FwBpBvfPpX2HGG8edYVV9Wupe43PEJJhhfnz1egtQNNaDXyYExn alice --dry_run
{
"account": {
"name": "alice",
"address_type": "WITNESS_PUBKEY_HASH",
"extended_public_key": "vpub5Z9beF6NYCrHeDmKC38tM3xXMDFFSARa9sdHRPChEMGqtxiELfZB8hm6FwBpBvfPpX2HGG8edYVV9Wupe43PEJJhhfnz1egtQNNaDXyYExn",
"master_key_fingerprint": null,
"derivation_path": "m/84'/0'/0'",
"external_key_count": 0,
"internal_key_count": 0,
"watch_only": true
},
"dry_run_external_addrs": [
"bcrt1q8zdjz2q92eh7jw9ah3upf2u9553226gq79el5l",
"bcrt1qmx2m4ngd2el0rmmcu0mz453yzzl3aq9mag0l79",
"bcrt1q904yve7yvt2t3v0s5r7rueweh4jjr3enfgam8w",
"bcrt1qa7k20jwfvsep8x0dx4jfu9xm0tlwaa8wrrgl77",
"bcrt1qzypxx35cfsl24mslqextetuc5m8vvadlqp20d8"
],
"dry_run_internal_addrs": [
"bcrt1qlstwh8ecy7szfw7k6rllc4ajkg6922xjwj6a23",
"bcrt1qdrz9glz4ld7uyxwv3jz2anx4k9pe3zm86hpy9g",
"bcrt1qfdu6tfhs85q20tf48nhtx0kjgr0t2j25apm90t",
"bcrt1qkmysm9wlnhyyc4uhfaxyafj6q3e3ujcnh97cqc",
"bcrt1qw8hhmdg3atfp7dcwjtysq4kcmnh07kjy2rd2ay"
]
}

Once Bob has confirmed the correct account derivation scheme is being used, the
account can be imported without the dry run parameter.

shell
$ lncli-bob wallet accounts import vpub5Z9beF6NYCrHeDmKC38tM3xXMDFFSARa9sdHRPChEMGqtxiELfZB8hm6FwBpBvfPpX2HGG8edYVV9Wupe43PEJJhhfnz1egtQNNaDXyYExn alice
{
"account": {
"name": "alice",
"address_type": "WITNESS_PUBKEY_HASH",
"extended_public_key": "vpub5Z9beF6NYCrHeDmKC38tM3xXMDFFSARa9sdHRPChEMGqtxiELfZB8hm6FwBpBvfPpX2HGG8edYVV9Wupe43PEJJhhfnz1egtQNNaDXyYExn",
"master_key_fingerprint": null,
"derivation_path": "m/84'/0'/0'",
"external_key_count": 0,
"internal_key_count": 0,
"watch_only": true
}
}

Generating Addresses from an Imported Account

External addresses from an imported account can be generated through the
existing Lightning.NewAddress RPC and lncli newaddress command, as they now
take an additional optional parameter to specify which account the address
should be derived from.

Following the example above, Bob is able to generate an external address for an
incoming deposit as follows:

shell
$ lncli-bob newaddress p2wkh --account=alice
{
"address": "bcrt1q8zdjz2q92eh7jw9ah3upf2u9553226gq79el5l"
}

Change addresses cannot be generated on demand, they are generated automatically
when a transaction is crafted that requires a change output.

Crafting Transactions through PSBTs from an Imported Account

Assuming a deposit of 1 tBTC was made to the address above
(bcrt1q8zdjz2q92eh7jw9ah3upf2u9553226gq79el5l), Bob should be able to craft a
transaction spending their new UTXO. Since Bob is unable to sign the transaction
themselves, they'll use PSBTs to craft the transaction, and provide it to Alice
to sign.

shell
$ lncli-bob wallet psbt fund --account=alice --outputs="{\"bcrt1qpjqr663tylcksysa4u76xvremee9k8af3pqd5h\": 500000}" --sat_per_vbyte=1
{
"psbt": "cHNidP8BAHECAAAAAYDHzEGcDW4Qf+gVbIgWpG2PVSUY6aZ3xUGk/3Ia/XnJAAAAAAD/////AiChBwAAAAAAFgAUDIA9aisn8WgSHa89ozB53nJbH6lWNf4pAQAAABYAFPwW6584J6Aku9bQ//xXsrI0VSjSAAAAAAABAKgCAAAAAAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////A1oBAf////8CAPIFKgEAAAAWABQ4myEoBVZv6Ti9vHgUq4WlIqVpAAAAAAAAAAAAJmokqiGp7eL2HD9x0d79P6mZ36NpU3VcaQaJeZlitIvr2DaXToz5ASAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAR8A8gUqAQAAABYAFDibISgFVm/pOL28eBSrhaUipWkAAQMEAQAAACIGArbCQ3C0eTrSeuEokWjN7ty25lSzNxiClZL3tnbmlDG6GAAAAABUAACAAAAAgAAAAIAAAAAAAAAAAAAAAA==",
"change_output_index": 1,
"locks": [
{
"id": "ede19a92ed321a4705f8a1cccc1d4f6182545d4bb4fae08bd5937831b7e38f98",
"outpoint": "c979fd1a72ffa441c577a6e91825558f6da416886c15e87f106e0d9c41ccc780:0",
"expiration": 1621632493
}
]
}

The PSBT can then be provided to Alice to sign:

text
/ Detailed source-code truncated for AI context efficiency. /

---