Did you lose funds on the Lightning Network because of a disk crash? Here’s a way you might get them back.

EDIT: This article is quite old and only applies to situations where you have no data from your crashed node but the seed. If you have any recovery data (for example a channel.backup file or an old copy of the channel.db file), please follow my more recent recovery guide.

EDIT: There is now a way to get another chance with Zombie Channels. Visit https://www.node-recovery.com for more information.

If you’ve been using the Lightning Network at any point prior to this article being written, you probably know that doing so is still considered #reckless and that there’s a real possibility of losing your bitcoins if anything goes wrong (with your hardware for example).
This is due to a protocol design decision that was only mitigated very recently with the introduction of Static Channel Backups (SCB).
Even though SCBs are great, work as advertised and will help many users being safe in the future, there are a lot of people trying out the Lightning Network that don’t read the warnings and aren’t aware SCBs even exist.

I’ve recently been approached by two people independently who have wiped their systems and wanted to reinstall everything, thinking they would be safe by having the 24 word mnemonic only. They did not back up their channel.backup file containing the SCBs and would have lost all bitcoins that they used to open their channels. So I decided to try and help them recover their funds, that’s how this article and the modified version of LND described here were created.

But before we start, here a short disclaimer: I’ve only really tested this once and was able to recover a few million satoshis from one channel. The other person that approached me is still gathering the information that is needed to start the recovery process.

So to not give you false hope, here are the conditions that need to be met for this to even be possible:

  1. You used LND. This method might be ported to other implementations but it currently only works for LND.
  2. The remote node you opened the channel with is still online with the same public key and can be looked up on 1ml.com. This is absolutely crucial. If the other node is not online and/or also suffered from data loss, there is simply no way of recovery, sorry.
  3. You do have the 24 word seed of the LND wallet that you got when setting it up. If you set a seed passphrase you also need to know that.
  4. You know exactly which channel(s) you want to recover funds from. And the channels can be seen on 1ml.com too (therefore, no private channels, unless you know the remote node ID and the channel funding TXID from somewhere).
  5. It hasn’t been a long time since you had the accident. I’m not sure how important this is but I guess the longer you haven’t been re-establishing the channel with the remote node the more likely it is they simply “forgot” the information we need.
  6. It’s an old, legacy channel, not one you opened with lnd v0.8.0 or later (those might be even easier to recover, see my new guide linked at the top).

If you can answer all of this with yes, then there is hope! Then you should at least try this guide/tutorial. This will be quite technical and it might not work in all cases. Maybe I’ll also need to tweak the code a bit more. You can DM me on the Lightning Network Community Slack if you need assistance though please bear in mind that I’m very busy and might not answer right away.

Ok, let’s get to it. The following things you will need in order to do this:

  1. Everything you need to compile LND (git, make, go), follow this tutorial.
  2. A bitcoind or btcd Bitcoin full node to connect LND to. It should also work over Neutrino.
  3. The latest version of my Channel Tools (chantools).

Now that we have everything set up, here is what you have to do:

  1. Clone and build my modified version of LND (based on 0.12.0-beta-rc6).
$ git clone https://github.com/guggero/lnd.git
$ cd lnd
$ git checkout fund-recovery
$ make && make install

All further commands need to be run inside this directory that was just created.

2. Run the following command and have a look at the parameters you have to provide:

$ chantools fakechanbackup --help
If for any reason a node suffers from data loss and there is no
channel.backup for one or more channels, then the funds in the channel would
theoretically be lost forever.
If the remote node is still online and still knows about the channel, there is
hope. We can initiate DLP (Data Loss Protocol) and ask the remote node to
force-close the channel and to provide us with the per_commit_point that is
needed to derive the private key for our part of the force-close transaction
output. But to initiate DLP, we would need to have a channel.backup file.
Fortunately, if we have enough information about the channel, we can create a
faked/skeleton channel.backup file that at least lets us talk to the other node
and ask them to do their part. Then we can later brute-force the private key for
the transaction output of our part of the funds (see rescueclosed command).
chantools fakechanbackup [flags]
chantools fakechanbackup --rootkey xprvxxxxxxxxxx \
--capacity 123456 \
--channelpoint f39310xxxxxxxxxx:1 \
--initiator \
--remote_node_addr 022c260xxxxxxxx@ \
--short_channel_id 566222x300x1 \
--multi_file fake.backup
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--capacity uint the channel's capacity in satoshis
--channelpoint string funding transaction outpoint of the channel to rescue (<txid>:<txindex>) as it is displayed on 1ml.com
-h, --help help for fakechanbackup
--initiator whether our node was the initiator (funder) of the channel
--multi_file string the fake channel backup file to create (default "results/fake-2021-01-23-14-41-03.backup")
--remote_node_addr string the remote node connection information in the format pubkey@host:port
--rootkey string BIP32 HD root key of the wallet to use for encrypting the backup; leave empty to prompt for lnd 24 word aezeed
--short_channel_id string the short channel ID in the format <blockheight>x<transactionindex>x<outputindex>
Global Flags:
-r, --regtest Indicates if regtest parameters should be used
-t, --testnet Indicates if testnet parameters should be used

The root key you can get using chantools. Simply run “chantools showrootkey” and enter the seed (and passhprase if one was used).

3. In this step we are going to start LND from the modified binary that you created earlier. But before we do so, we have to make sure we don’t overwrite something accidentally. If you have any previous installation of LND and there is a folder called .lnd in your home directory (or wherever LND stored its data), please move it away to a safe location. The next few commands will create a new wallet.db file in this folder and we don’t want your old one to be overwritten.

In terminal window 1 you start the modified LND and connect it to the Bitcoin full node (please follow any other tutorial you find online to do this, I won’t have time to support you on this, sorry). Your command might look something like this:

./lnd-debug --bitcoin.active --bitcoin.mainnet \
--bitcoin.node=bitcoind --bitcoind.rpchost=xxx:8332 \
--bitcoind.rpcuser=xxx --bitcoind.rpcpass=xxx

This will produce a few lines of output and then wait for you to create or unlock the wallet. You don’t need to do anything more in this window, just let the process run. But once the recovery process is successful, there will be a very important log message visible in this window. You will need this, this is very important!

In terminal window 2, run the following command:

./lncli-debug create --multi_file ./fake.backup

This will create a new wallet file and folder structure. But we want to use the original seed to recover from. So when it asks you if you already have a seed and you have to answer yes! Enter the 24 words (and the passphrase if applicable) to setup the wallet again from the original seed you lost funds on.
Once the command completes, follow the output on terminal window 1. LND will sync the whole chain, look for on-chain balances and finally try to connect to the remote node that we specified in the fake SCB file. This might take a while, even with an SSD it can take a few hours.
Once the Data Loss Protocol has been initiated, the following output should be seen in the console output of terminal window 1 (it will also be save to the default log file so you can also look it up there):

2021-01-23 14:09:44.306 [DBG] LNWL: ChannelPoint(f39310xxxxxx:1): Peer sent commit_point=020df4d7c539a63589dbb0115a49484d70096b2792bc733fc56ea02d2f3bb4a00c

If you see this, great! You will need the value of “commit_point” for the next step. Save this somewhere!

Now all we can do is wait and hope that the remote node force-closes the channel. This will put our balance of the channel back on-chain. But unfortunately not to an address that is watched by the wallet by default but to a special, tweaked address. This address is a combination of a key in our wallet (derived from the seed) and the “Commit Point” we just received. Without the “Commit Point” (that ONLY the remote node knows), we would never be able to calculate the private key that can spend the balance that is now back on-chain.

To now recover the private key for the address that our part of the channel balance should now be on, we first need to find out the address where the force-close transaction output has been locked to. For this, it’s easiest if we have a look at the channel funding transaction (found on 1ml.com, we used this in a previous step) in a block explorer like www.blockstream.info for example. We should now see that this funding transaction has been spent. If that is not the case, then the remote node has not yet force-closed the channel. There might be a way to approach the owner of the node and ask them to force-close manually.

Once we see the spending transaction, we know the address our funds went to. It should be the shorter of both addresses, usually it’s the second output.

Now that we have both parameters we need to brute force the private key, we can run the last command. The root key is the same we used in the first command.

$ chantools rescueclosed --help
If channels have already been force-closed by the remote
peer, this command tries to find the private keys to sweep the funds from the
output that belongs to our side. This can only be used if we have a channel DB
that contains the latest commit point. Normally you would use SCB to get the
funds from those channels. But this method can help if the other node doesn't
know about the channels any more but we still have the channel.db from the
moment they force-closed.
The alternative use case for this command is if you got the commit point by
running the fund-recovery branch of my guggero/lnd fork in combination with the
fakechanbackup command. Then you need to specify the --commit_point and
--force_close_addr flags instead of the --channeldb and --fromsummary flags.
chantools rescueclosed [flags]
chantools rescueclosed --rootkey xprvxxxxxxxxxx \
--fromsummary results/summary-xxxxxx.json \
--channeldb ~/.lnd/data/graph/mainnet/channel.db
chantools rescueclosed --rootkey xprvxxxxxxxxxx \
--force_close_addr bc1q... \
--commit_point 03xxxx
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--channeldb string lnd channel.db file to use for rescuing force-closed channels
--commit_point string the commit point that was obtained from the logs after running the fund-recovery branch of guggero/lnd
--force_close_addr string the address the channel was force closed to
--fromchanneldb string channel input is in the format of an lnd channel.db file
--fromsummary string channel input is in the format of chantool's channel summary; specify '-' to read from stdin
-h, --help help for rescueclosed
--listchannels string channel input is in the format of lncli's listchannels format; specify '-' to read from stdin
--pendingchannels string channel input is in the format of lncli's pendingchannels format; specify '-' to read from stdin
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
Global Flags:
-r, --regtest Indicates if regtest parameters should be used
-t, --testnet Indicates if testnet parameters should be used

If we are lucky, we should get a message that says: “Found private key ”. Followed by the WIF representation of a private key. If that is the case, you can import this private key into any wallet and sweep the funds.
Great, you just got your money back!
Feel free to send me a tip ;-) bc1qcpm6nw32ng4qv3htu8lu80ucnaw7qg276fdxqy

If you don’t get a private key out, there is a whole list of reasons what could have gone wrong. Feel free to contact me, maybe there is something we can do.

I’m glad for any feedback I receive on this guide. My goal is to help as many people as possible to recover any lost funds! Even if that makes Bitcoin less scarce because a few more satoshis are in circulation, it might make the Lightning Network a bit less #reckless.