Compare commits

...

106 Commits

Author SHA1 Message Date
Oliver Gugger 85f207c58f
Merge pull request #137 from hieblmi/gitignore-idea
gitignore: .idea folder
1 week ago
Slyghtning da904ae1d7
gitignore: .idea folder 1 week ago
Oliver Gugger 14aa06fa41
Merge pull request #134 from lightninglabs/dependabot/go_modules/tools/github.com/btcsuite/btcd-0.24.0
build(deps): bump github.com/btcsuite/btcd from 0.23.4 to 0.24.0 in /tools
4 weeks ago
dependabot[bot] 601789f445
build(deps): bump github.com/btcsuite/btcd in /tools
Bumps [github.com/btcsuite/btcd](https://github.com/btcsuite/btcd) from 0.23.4 to 0.24.0.
- [Release notes](https://github.com/btcsuite/btcd/releases)
- [Changelog](https://github.com/btcsuite/btcd/blob/master/CHANGES)
- [Commits](https://github.com/btcsuite/btcd/compare/v0.23.4...v0.24.0)

---
updated-dependencies:
- dependency-name: github.com/btcsuite/btcd
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
4 weeks ago
Oliver Gugger cc284baa67
Merge pull request #132 from lightninglabs/triggerforceclose
triggerforceclose: make compatible with all nodes, add Tor support
1 month ago
Oliver Gugger 6acc81815e
root: bump version to v0.13.1 1 month ago
Oliver Gugger e3285daf5b
triggerforceclose: support Tor connections 1 month ago
Oliver Gugger 179773fdb9
triggerforceclose: make cmd compatible with all nodes 1 month ago
Oliver Gugger 0fd58ee7eb
Merge pull request #130 from lightninglabs/dependabot/go_modules/golang.org/x/net-0.23.0
build(deps): bump golang.org/x/net from 0.21.0 to 0.23.0
1 month ago
dependabot[bot] 7c405057bd
build(deps): bump golang.org/x/net from 0.21.0 to 0.23.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.21.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.21.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
1 month ago
Oliver Gugger d07251df9c
Merge pull request #129 from lightninglabs/fix-signpsbt
signpsbt+lnd: fix signing for P2WKH
2 months ago
Oliver Gugger 7f840cf03b
signpsbt+lnd: fix signing for P2WKH 2 months ago
Oliver Gugger f062b53a21
Merge pull request #127 from lightninglabs/dependabot/go_modules/github.com/docker/docker-24.0.9incompatible
build(deps): bump github.com/docker/docker from 24.0.7+incompatible to 24.0.9+incompatible
2 months ago
Oliver Gugger 24cd530c65
make: update build targets due to sqlite 2 months ago
dependabot[bot] c54184b8d0
build(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 24.0.7+incompatible to 24.0.9+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v24.0.7...v24.0.9)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2 months ago
Oliver Gugger 997d86cd84
Merge pull request #124 from lightninglabs/createwallet
Add `createwallet` and `signpsbt` subcommands
2 months ago
Oliver Gugger 676ba60197
Merge pull request #113 from sputn1ck/recoverloopin_sqlite
`recoverloopin`: Sqlite option
2 months ago
sputn1ck 3e419da317
recoverloopin: add sqlite option
This commit will allow to recover loop ins that have been made with
sqlite.
2 months ago
sputn1ck 6a81614b1b
go.mod: update loop to v0.26.6-beta 2 months ago
Oliver Gugger 71b824e105
README+doc: update all docs 2 months ago
Oliver Gugger 1a46f9099f
root: bump version to v0.13.0 2 months ago
Oliver Gugger 9f8484bb89
cmd/chantools: add signpsbt subcommand 2 months ago
Oliver Gugger e80dcbfb67
lnd+cmd/chantools: add AddPartialSignatureForPrivateKey to signer 2 months ago
Oliver Gugger 5c39df02d3
cmd/chantools: allow root key to be read from wallet DB
With this commit we allow a third option for reading the master root key
for any command that requires access to it: Reading and decrypting it
directly from an lnd wallet password.
2 months ago
Oliver Gugger 37179e5215
lnd+cmd/chantools: extract functions from walletinfo 2 months ago
Oliver Gugger b169634d85
cmd/chantools: add new createwallet subcommand
This commit adds a new subcommand for creating a new lnd compatible
wallet.db file from an existing aezeed, master root key (xprv) or by
generating a new aezeed.
2 months ago
Oliver Gugger a3a00d410a
lnd: extract ReadPassphrase into own function 2 months ago
Oliver Gugger 450c2777af
Merge pull request #126 from lightninglabs/dependabot/go_modules/tools/google.golang.org/protobuf-1.33.0
build(deps): bump google.golang.org/protobuf from 1.28.0 to 1.33.0 in /tools
3 months ago
dependabot[bot] c4162303b7
build(deps): bump google.golang.org/protobuf in /tools
Bumps google.golang.org/protobuf from 1.28.0 to 1.33.0.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
3 months ago
Oliver Gugger 486af2e99a
Merge pull request #125 from lightninglabs/dependabot/go_modules/github.com/jackc/pgx/v4-4.18.2
build(deps): bump github.com/jackc/pgx/v4 from 4.18.1 to 4.18.2
3 months ago
dependabot[bot] 3b3daddfee
build(deps): bump github.com/jackc/pgx/v4 from 4.18.1 to 4.18.2
Bumps [github.com/jackc/pgx/v4](https://github.com/jackc/pgx) from 4.18.1 to 4.18.2.
- [Changelog](https://github.com/jackc/pgx/blob/v4.18.2/CHANGELOG.md)
- [Commits](https://github.com/jackc/pgx/compare/v4.18.1...v4.18.2)

---
updated-dependencies:
- dependency-name: github.com/jackc/pgx/v4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
3 months ago
Oliver Gugger c75f9ff91a
Merge pull request #121 from sputn1ck/signmessage
signmessage: add signmessage cmd
4 months ago
sputn1ck 3fbf8d0bd2
signmessage: add signmessage cmd
This commit adds the signmessage command which allows a user to sign a
message with the nodes identity key, similiar to `lncli signmessage`.
4 months ago
Oliver Gugger cf4cabbd2a
Merge pull request #120 from ziggie1984/master
Fix Partial Signature Signing.
4 months ago
ziggie 78c41b4acf
zombierecovery-makeoffer: fix witness data for psbt package.
We need to make sure we populate all the necessary
witness/nonwitness because for taproot inputs we need the prevout
so we check for it in the hashcash creation.
4 months ago
Oliver Gugger a0e5f0613d
Merge pull request #118 from Tetrix42/doublespend_rbf
doublespendinputs: allow RBF per default
4 months ago
Felix Passenberg d9af0e36e5
doublespendinputs: remove RBF argument, RBF always on 4 months ago
Felix Passenberg 3b50a5ce16
doublespendinputs: allow RBF per default 4 months ago
Oliver Gugger d5d5a91430
Merge pull request #117 from lightninglabs/zombie-matching
zombierecovery: add --matchonly flag to makeoffer, --numkeys to preparekeys
4 months ago
Oliver Gugger 82a03a65ef
README+root: bump version to v0.12.2 4 months ago
Oliver Gugger 65cc3fdf6e
zombierecovery add --numkeys to preparekeys
With this new flag it will be possible to specify the number of keys to
add to the file when running the preparekeys command.
4 months ago
Oliver Gugger 79f65bb1a1
zombierecovery: add --matchonly flag to makeoffer
With this commit we make it possible to just check whether two lists of
public keys can match the given channels and derive the 2-of-2 multisig
channel funding address.
4 months ago
Oliver Gugger 341d3af108
README: fix broken links 5 months ago
Oliver Gugger 3865a7757e
Merge pull request #114 from sputn1ck/chantools_external_amt
`recoverloopin`: allow setting output value
5 months ago
sputn1ck ad3c1ad2de
recoverloopin: allow setting output value
This commit adds the ability to manually set the output value for a
loop in swap. This is useful for recovering funds from a loop in swap
that has been fund externaly and was sent to with a different amount
than the one specified in the loop in swap.
5 months ago
Oliver Gugger 399a23adba
Merge pull request #107 from lightninglabs/cli-cleanup
Support P2TR as sweep/change address everywhere
5 months ago
Oliver Gugger a05962e03e
multi: standardize sweep/change addr support 5 months ago
Oliver Gugger 7e3ea44fd4
multi: standardize address checks 5 months ago
Oliver Gugger 53085d34d0
btc: make API errors more verbose 5 months ago
Oliver Gugger d830ebe57a
multi: use default API URLs for testnet and regtest 5 months ago
Oliver Gugger 858995a317
Merge pull request #109 from ziggie1984/bug-fix-anchor-amt
pullanchor: account for all anchor outputs.
5 months ago
ziggie b777d4436d
pullanchor: account for all anchor outputs. 5 months ago
Oliver Gugger 5cf7fd60c4
multi: clean up after PRs 5 months ago
Oliver Gugger 92fdb156e0
Merge pull request #100 from lightninglabs/pullanchor
multi: add new pullanchor command
5 months ago
Oliver Gugger 2abc29d01d
Merge pull request #106 from lightninglabs/sweepremoteclosed-taproot-channels
sweepremoteclosed: add support for simple taproot channels
5 months ago
Oliver Gugger fd18186a82
Merge pull request #91 from lightninglabs/sweeptimelockmanual-backup-file
sweeptimelockmanual: allow specifying the backup file directly
5 months ago
Oliver Gugger 7227c7f101
multi: add new pullanchor command 5 months ago
Oliver Gugger 801f881274
sweepremoteclosed: add support for simple taproot channels 5 months ago
Oliver Gugger c89cede963
Merge pull request #103 from lightninglabs/dependabot/go_modules/golang.org/x/crypto-0.17.0
build(deps): bump golang.org/x/crypto from 0.14.0 to 0.17.0
6 months ago
Oliver Gugger 00c7f7eb98
Merge pull request #104 from lightninglabs/dependabot/go_modules/tools/golang.org/x/crypto-0.17.0
build(deps): bump golang.org/x/crypto from 0.5.0 to 0.17.0 in /tools
6 months ago
dependabot[bot] 798a6d0927
build(deps): bump golang.org/x/crypto from 0.5.0 to 0.17.0 in /tools
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.5.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.5.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
6 months ago
dependabot[bot] 7e110d8d46
build(deps): bump golang.org/x/crypto from 0.14.0 to 0.17.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.14.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.14.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
6 months ago
Oliver Gugger 0ebb732576
rescueclosed: clarify instructions
Fixes #102.
6 months ago
Oliver Gugger 8d28e1b2f6
fakechanbackup: use correct JSON decoder 6 months ago
Oliver Gugger a5a884bff9
Merge pull request #92 from YuckFouBTC/patch-1
Update zombierecovery.md
7 months ago
Oliver Gugger bed41c1533
Merge pull request #93 from lightninglabs/dependabot/go_modules/go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc-0.46.0
build(deps): bump go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc from 0.25.0 to 0.46.0
7 months ago
dependabot[bot] a44746912c
build(deps): bump go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
Bumps [go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc](https://github.com/open-telemetry/opentelemetry-go-contrib) from 0.25.0 to 0.46.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.25.0...zpages/v0.46.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
7 months ago
Yuck Fou 55208e0218
Update zombierecovery.md
Fix typo: Hou -> You
7 months ago
Oliver Gugger a13262f2ff
sweeptimelockmanual: allow using channel backup file
Instead of needing to manually dump the channel backup file, look for
the remote revocation base point and then have the CSV delay and channel
derivation index being brute forced, we can extract all that info
directly from the channel backup file.
7 months ago
Oliver Gugger dee18ed80c
sweeptimelockmanual: rename variable 7 months ago
Oliver Gugger 5bc49376a3
sweeptimelock: make start CSV timeout+channels configurable 7 months ago
Oliver Gugger 3044d9f796
lnd: add ExtractChannel function 7 months ago
Oliver Gugger f9343e5c3d
Merge pull request #88 from lightninglabs/dependabot/go_modules/google.golang.org/grpc-1.56.3
build(deps): bump google.golang.org/grpc from 1.53.0 to 1.56.3
7 months ago
dependabot[bot] abb0343059
build(deps): bump google.golang.org/grpc from 1.53.0 to 1.56.3
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.53.0 to 1.56.3.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.53.0...v1.56.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
7 months ago
Oliver Gugger bc69de77e1
multi: use go 1.21.x 8 months ago
Oliver Gugger 3fc529f747
README+root: bump version to v0.12.0 8 months ago
Oliver Gugger a2f8a0e27e
dump+root: fix hard coded version and linter 8 months ago
Oliver Gugger fac990905f
Merge pull request #86 from lightninglabs/dependabot/go_modules/golang.org/x/net-0.17.0
build(deps): bump golang.org/x/net from 0.10.0 to 0.17.0
8 months ago
dependabot[bot] d9e3365e1c
build(deps): bump golang.org/x/net from 0.10.0 to 0.17.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.10.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.10.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
8 months ago
Oliver Gugger b124006af7
Merge pull request #85 from starius/lnd-v0.17.0-final
multi: bump to lnd version v0.17.0-beta
8 months ago
bitromortac 4f083e1c1d
multi: bump to lnd version v0.17.0-beta 8 months ago
Oliver Gugger 04a4354efd
Merge pull request #84 from lightninglabs/dependabot/go_modules/golang.org/x/net-0.17.0
build(deps): bump golang.org/x/net from 0.7.0 to 0.17.0
8 months ago
dependabot[bot] 9b7f200176
build(deps): bump golang.org/x/net from 0.7.0 to 0.17.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.7.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.7.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
8 months ago
Oliver Gugger fe356a4648
dumpchannels: fix linter error 9 months ago
Oliver Gugger f191d1bb91
multi: add new dropgraphzombies command 9 months ago
Oliver Gugger 909163b260
doc: update after adding signet 9 months ago
Oliver Gugger 84158971ca
root: add signet support 9 months ago
Oliver Gugger bae1bff7e2
dump+dumpchannels: add additional debug info to channels 9 months ago
Oliver Gugger 4926ae2db8
doc+triggerforceclose: make command help more explicit 9 months ago
Oliver Gugger b0097ad132
Merge pull request #71 from lightninglabs/dependabot/go_modules/google.golang.org/grpc-1.53.0
build(deps): bump google.golang.org/grpc from 1.41.0 to 1.53.0
11 months ago
dependabot[bot] 6f0cca33c8
build(deps): bump google.golang.org/grpc from 1.41.0 to 1.53.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.41.0 to 1.53.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.41.0...v1.53.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
11 months ago
Oliver Gugger ebe9d8903e
README: bump link to v0.11.3 1 year ago
Oliver Gugger 9fbec87dff
Merge pull request #69 from lightninglabs/handover-fixups
Rename and add more documentation
1 year ago
Oliver Gugger e6a9395a00
mod+cmd: pin lnd version in command help texts 1 year ago
Oliver Gugger f2ee1d3ce8
README+root: bump install link to latest version 1 year ago
Oliver Gugger 6295ba9b88
root: bump version to 0.11.3 1 year ago
Oliver Gugger 88a715fca8
doc: update after changes in recoverloopin 1 year ago
Oliver Gugger 4e0c5ada63
multi: update repo/module from github.com/{guggero => lightninglabs}/chantools 1 year ago
Oliver Gugger a84a59ed76
README: add more information 1 year ago
Oliver Gugger 2a1997844d
Merge pull request #70 from sputn1ck/rbfInputs
Add rbfinputs command
1 year ago
sputn1ck aad46d3e64
rbfinputs: add rbfinputs command
This commit adds a new command to the chantools tool which allows
users to RBF inputs. In case a channel opening transaction is stuck
in the mempool, this command can be used to RBF spend the inputs
of the channel opening transaction to a different address.
1 year ago
sputn1ck 4b804f7024
multi: fix overflow on feerate
As we multiply the feerate by 1000, if the uint16 feerate is set higher
than 66 we would encounter an overflow and as such we could never
have a higher feerate than 65.
1 year ago
sputn1ck 2c2de636a8
signer: allow signing of an output with privkey
This commit adds the functionality to sign an output with an already
obtained privatekey.
1 year ago
Oliver Gugger 22a4644f4b
zombierecovery: add date to message.txt 1 year ago
Oliver Gugger 20a684d0ff
recoverloopin: fix loop db dir, validate params 1 year ago
Oliver Gugger e7c12e4335
zombierecovery: make matches stable 1 year ago

@ -16,7 +16,7 @@ env:
# go needs absolute directories, using the $HOME variable doesn't work here.
GOCACHE: /home/runner/work/go/pkg/build
GOPATH: /home/runner/work/go
GO_VERSION: 1.20.3
GO_VERSION: 1.21.3
jobs:
########################

1
.gitignore vendored

@ -1,3 +1,4 @@
.idea
/chantools
results
/chantools-v*

@ -19,6 +19,11 @@ linters-settings:
staticcheck:
go: "1.18"
checks: ["-SA1019"]
gomoddirectives:
replace-allow-list:
# See go.mod for the explanation why these are needed.
- google.golang.org/protobuf
linters:
enable-all: true
@ -63,4 +68,4 @@ issues:
exclude-rules:
- path: cmd/chantools
linters:
- lll
- lll

@ -1,4 +1,4 @@
PKG := github.com/guggero/chantools
PKG := github.com/lightninglabs/chantools
TOOLS_DIR := tools
GOTEST := GO111MODULE=on go test -v
@ -20,14 +20,13 @@ VERSION_TAG = $(shell git describe --tags)
VERSION_CHECK = @$(call print, "Building master with date version tag")
BUILD_SYSTEM = darwin-amd64 \
darwin-arm64 \
linux-386 \
linux-amd64 \
linux-armv6 \
linux-armv7 \
linux-arm64 \
windows-386 \
windows-amd64 \
windows-arm
windows-amd64
# By default we will build all systems. But with the 'sys' tag, a specific
# system can be specified. This is useful to release for a subset of

@ -23,13 +23,14 @@ a private API URL with `--apiurl`.
## Installation
The easiest way to install `chantools` is to [download a pre-built binary for
your operating system and architecture](https://github.com/guggero/chantools/releases).
your operating system and
architecture](https://github.com/lightninglabs/chantools/releases).
Example (make sure you always use the latest version!):
```shell
$ cd /tmp
$ wget -O chantools.tar.gz https://github.com/guggero/chantools/releases/download/v0.10.7/chantools-linux-amd64-v0.10.7.tar.gz
$ wget -O chantools.tar.gz https://github.com/lightninglabs/chantools/releases/download/v0.12.2/chantools-linux-amd64-v0.12.2.tar.gz
$ tar -zxvf chantools.tar.gz
$ sudo mv chantools-*/chantools /usr/local/bin/
```
@ -38,15 +39,114 @@ $ sudo mv chantools-*/chantools /usr/local/bin/
If there isn't a pre-built binary for your operating system or architecture
available or you want to build `chantools` from source for another reason, you
need to make sure you have `go 1.19.x` (or later) and `make` installed and can
need to make sure you have `go 1.21.x` (or later) and `make` installed and can
then run the following commands:
```bash
git clone https://github.com/guggero/chantools.git
git clone https://github.com/lightninglabs/chantools.git
cd chantools
make install
```
## When should I use what command?
This list contains a list of scenarios that users seem to run into sometimes.
**Before you start running any `chantools` command, you MUST read the
["What should I NEVER do?"](#what-should-i-never-do) section below!**
Scenarios:
- **My node/disk/database crashed and I only have the seed and `channel.backup`
file.**
This is the "normal" recovery scenario for which you don't need `chantools`.
Just follow the [`lnd` recovery guide][recovery].
All channels will be closed to recover funds, so you should still try to avoid
This scenario. You only need `chantools` if you had [zombie
channels][safety-zombie] or a channel that did not confirm in time (see
below).
- **My node/disk/database crashed and I only have the seed.**
This is very bad and recovery will take manual steps and might not be
successful for private channels. If you do not have _any_ data left from your
node, you need to follow the [`chantools fakechanbackup` command
](doc/chantools_fakechanbackup.md) help text. If you do have an old version of
your `channel.db` file, DO NOT UNDER ANY CIRCUMSTANCES start your node with
it. Instead, try to extract a `channel.backup` from it using the [`chantools
chanbackup`](doc/chantools_chanbackup.md) command. If that is successful,
follow the steps in the [`lnd` recovery guide][recovery].
This will not cover new channels opened after the backup of the `channel.db`
file was created. You might still need to create the fake channel backup.
- **I suspect my channel.db file to be corrupt.**
This can happen due to unclean shutdowns or power outages. Try running
[`chantools compactdb`](doc/chantools_compactdb.md). If there are NO ERRORS
during the execution of that command, things should be back to normal, and you
can continue running your node. If you get errors, you should probably follow
the [recovery scenario described below](#channel-recovery-scenario) to avoid
future issues. This will close all channels, however.
- **I don't have a `channel.backup` file but all my peers force closed my
channels, why don't I see the funds with just my seed?**
When a channel is force closed by the remote party, the funds don't
automatically go to a normal on-chain address. You need to sweep those funds
using the [`chantools sweepremoteclosed`](doc/chantools_sweepremoteclosed.md)
command.
- **My channel peer is online, but they don't force close a channel when using
a `channel.backup` file**.
This can have many reasons. Often it means the channels is a legacy channel
type (not an anchor output channel) and the force close transaction the peer
has doesn't have enough fees to make it into the mempool. In that case waiting
for an empty mempool might be the only option.
Another reason might be that the peer is a CLN node with a specific version
that doesn't react to force close requests normally. You can use the
[`chantools triggerforceclose` command](doc/chantools_triggerforceclose.md) in
that case (should work with CLN peers of a certain version that don't respond
to normal force close requests).
## What should I NEVER do?
- You should never panic. There are extremely few situations in which doing
nothing makes things worse. On the contrary, most cases where users actually
lost funds it was due to them running commands they did not understand in a
rush of panic. So stay calm, try to find out what the reason for the problem
is, ask for help (see [Slack][slack], [`lnd` discussions][discussions]) or use
Google.
Create a backup of all your files in the `lnd` data directory (just in case,
but never [start a node from a file based backup][safety-file-backup])
before running _any_ command. Also read the [`lnd` Operational Safety
Guidelines][safety].
- Whatever you might read in any issue, you should never use
`lncli abandonchannel` on a channel that was confirmed on chain. Even if you
have an SCB (Static Channel Backup, unfortunately poorly named) file
(`channel.backup`) or export from `lncli exportchanbackup`. Those files DO NOT
contain enough information to close a channel if your peer does not have the
channel data either (which might happen if the channel took longer than 2
weeks to confirm). If the channel confirmed on chain, you need to force close
it from your node if it does not operate normally. Running `abandonchannel`
deletes the information needed to be able to force close.
- When running Umbrel, NEVER just uninstall the Lightning App when encountering
a problem. Uninstalling the app deletes important data that might be needed
for recovery in edge cases. The channel backup (SCB) in the cloud does NOT
cover "expired" channels (channels that took longer than 2 weeks to confirm)
or [zombie channels][safety-zombie].
- The term "backup" in SCB (Static Channel Backup) or the `channel.backup` file
or the output of `lncli exportchanbackup` is not optimal as it implies the
channels can be fully restored or brought back to an operational state. But
the content of those files are for absolute emergencies only. Channels are
always closed when using such a file (by asking the remote peer to issue their
latest force close transaction they have). So chain fees occur. And there are
some edge cases where funds are not covered by those files, for example when
a channel funding transaction is not confirmed in time. Or for channels where
the peer is no longer online. So deleting your `lnd` data directory should
never ever be something to be done lightly (see Umbrel above).
## Channel recovery scenario
The following flow chart shows the main recovery scenario this tool was built
@ -63,177 +163,208 @@ compacting the DB).
**Explanation:**
1. **Node crashed**: For some reason your `lnd` node crashed and isn't starting
anymore. If you get errors similar to
[this](https://github.com/lightningnetwork/lnd/issues/4449),
[this](https://github.com/lightningnetwork/lnd/issues/3473) or
[this](https://github.com/lightningnetwork/lnd/issues/4102), it is possible
that a simple compaction (a full copy in safe mode) can solve your problem.
See [`chantools compactdb`](doc/chantools_compactdb.md).
<br/><br/>
If that doesn't work and you need to continue the recovery, make sure you can
at least extract the `channel.backup` file and if somehow possible any version
of the `channel.db` from the node.
<br/><br/>
Whatever you do, do **never, ever** replace your `channel.db` file with an old
version (from a file based backup) and start your node that way.
[Read this explanation why that can lead to loss of funds.](https://github.com/lightningnetwork/lnd/blob/master/docs/safety.md#file-based-backups)
anymore. If you get errors similar to
[this](https://github.com/lightningnetwork/lnd/issues/4449),
[this](https://github.com/lightningnetwork/lnd/issues/3473) or
[this](https://github.com/lightningnetwork/lnd/issues/4102), it is possible
that a simple compaction (a full copy in safe mode) can solve your problem.
See [`chantools compactdb`](doc/chantools_compactdb.md).
<br/><br/>
If that doesn't work and you need to continue the recovery, make sure you can
at least extract the `channel.backup` file and if somehow possible any
version
of the `channel.db` from the node.
<br/><br/>
Whatever you do, do **never, ever** replace your `channel.db` file with an
old
version (from a file based backup) and start your node that way.
[Read this explanation why that can lead to loss of
funds.][safety-file-backup]
2. **Rescue on-chain balance**: To start the recovery process, we are going to
re-create the node from scratch. To make sure we don't overwrite any old data
in the process, make sure the old data directory of your node (usually `.lnd`
in the user's home directory) is safely moved away (or the whole folder
renamed) before continuing.<br/>
To start the on-chain recovery, [follow the sub step "Starting On-Chain Recovery" of this guide](https://github.com/lightningnetwork/lnd/blob/master/docs/recovery.md#starting-on-chain-recovery).
Don't follow the whole guide, only this single chapter!
<br/><br/>
This step is completed once the `lncli getinfo` command shows both
`"synced_to_chain": true` and `"synced_to_graph": true` which can take several
hours depending on the speed of your hardware. **Do not be alarmed** that the
`lncli getinfo` command shows 0 channels. This is normal as we haven't started
the off-chain recovery yet.
re-create the node from scratch. To make sure we don't overwrite any old data
in the process, make sure the old data directory of your node (usually `.lnd`
in the user's home directory) is safely moved away (or the whole folder
renamed) before continuing.<br/>
To start the on-chain recovery, [follow the sub step "Starting On-Chain
Recovery" of this guide][recovery].
Don't follow the whole guide, only this single chapter!
<br/><br/>
This step is completed once the `lncli getinfo` command shows both
`"synced_to_chain": true` and `"synced_to_graph": true` which can take
several
hours depending on the speed of your hardware. **Do not be alarmed** that the
`lncli getinfo` command shows 0 channels. This is normal as we haven't
started
the off-chain recovery yet.
3. **Recover channels using SCB**: Now that the node is fully synced, we can try
to recover the channels using the [Static Channel Backups (SCB)](https://github.com/lightningnetwork/lnd/blob/master/docs/safety.md#static-channel-backups-scbs).
For this, you need a file called `channel.backup`. Simply run the command
`lncli restorechanbackup --multi_file <path-to-your-channel.backup>`. **This
will take a while!**. The command itself can take several minutes to complete,
depending on the number of channels. The recovery can easily take a day or
two as a lot of chain rescanning needs to happen. It is recommended to wait at
least one full day. You can watch the progress with the `lncli pendingchannels`
command. If the list is empty, congratulations, you've recovered all channels!
If the list stays un-changed for several hours, it means not all channels
could be restored using this method.
[One explanation can be found here.](https://github.com/lightningnetwork/lnd/blob/master/docs/safety.md#zombie-channels)
to recover the channels using the [Static Channel Backups (SCB)][safety-scb].
For this, you need a file called `channel.backup`. Simply run the command
`lncli restorechanbackup --multi_file <path-to-your-channel.backup>`. **This
will take a while!**. The command itself can take several minutes to
complete,
depending on the number of channels. The recovery can easily take a day or
two as a lot of chain rescanning needs to happen. It is recommended to wait
at
least one full day. You can watch the progress with
the `lncli pendingchannels`
command. If the list is empty, congratulations, you've recovered all
channels!
If the list stays un-changed for several hours, it means not all channels
could be restored using this method.
[One explanation can be found here.][safety-zombie]
4. **Install chantools**: To try to recover the remaining channels, we are going
to use `chantools`. Simply [follow the installation instructions.](#installation)
The recovery can only be continued if you have access to some version of the
crashed node's `channel.db`. This could be the latest state as recovered from
the crashed file system, or a version from a regular file based backup. If you
do not have any version of a channel DB, `chantools` won't be able to help
with the recovery. See step 11 for some possible manual steps.
to use `chantools`.
Simply [follow the installation instructions.](#installation)
The recovery can only be continued if you have access to some version of the
crashed node's `channel.db`. This could be the latest state as recovered from
the crashed file system, or a version from a regular file based backup. If
you
do not have any version of a channel DB, `chantools` won't be able to help
with the recovery. See step 11 for some possible manual steps.
5. **Create copy of channel DB**: To make sure we can read the channel DB, we
are going to create a copy in safe mode (called compaction). Simply run
<br/><br/>
`chantools compactdb --sourcedb <recovered-channel.db> --destdb ./results/compacted.db`
<br/><br/>
We are going to assume that the compacted copy of the channel DB is located in
`./results/compacted.db` in the following commands.
are going to create a copy in safe mode (called compaction). Simply run
<br/><br/>
`chantools compactdb --sourcedb <recovered-channel.db> --destdb ./results/compacted.db`
<br/><br/>
We are going to assume that the compacted copy of the channel DB is located
in
`./results/compacted.db` in the following commands.
6. **chantools summary**: First, `chantools` needs to find out the state of each
channel on chain. For this, a blockchain API (by default [blockstream.info](https://blockstream.info))
is queried. The result will be written to a file called
`./results/summary-yyyy-mm-dd.json`. This result file will be needed for the
next command.
<br/><br/>
`chantools --fromchanneldb ./results/compacted.db summary`
channel on chain. For this, a blockchain API (by
default [blockstream.info](https://blockstream.info))
is queried. The result will be written to a file called
`./results/summary-yyyy-mm-dd.json`. This result file will be needed for the
next command.
<br/><br/>
`chantools --fromchanneldb ./results/compacted.db summary`
7. **chantools rescueclosed**: It is possible that by now the remote peers have
force-closed some of the remaining channels. What we now do is try to find the
private keys to sweep our balance of those channels. For this we need a shared
secret which is called the `commit_point` and is changed whenever a channel is
updated. We do have the latest known version of this point in the channel DB.
The following command tries to find all private keys for channels that have
been closed by the other party. The command needs to know what channels it is
operating on, so we have to supply the `summary-yyy-mm-dd.json` created by the
previous command:
<br/><br/>
`chantools --fromsummary ./results/<summary-file-created-in-last-step>.json rescueclosed --channeldb ./results/compacted.db`
<br/><br/>
This will create a new file called `./results/rescueclosed-yyyy-mm-dd.json`
which will contain any found private keys and will also be needed for the next
command. Use `bitcoind` or Electrum Wallet to sweep all of the private keys.
force-closed some of the remaining channels. What we now do is try to find
the
private keys to sweep our balance of those channels. For this we need a
shared
secret which is called the `commit_point` and is changed whenever a channel
is
updated. We do have the latest known version of this point in the channel DB.
The following command tries to find all private keys for channels that have
been closed by the other party. The command needs to know what channels it is
operating on, so we have to supply the `summary-yyy-mm-dd.json` created by
the
previous command:
<br/><br/>
`chantools --fromsummary ./results/<summary-file-created-in-last-step>.json rescueclosed --channeldb ./results/compacted.db`
<br/><br/>
This will create a new file called `./results/rescueclosed-yyyy-mm-dd.json`
which will contain any found private keys and will also be needed for the
next
command. Use `bitcoind` or Electrum Wallet to sweep all of the private keys.
8. **chantools forceclose**: This command will now close all channels that
`chantools` thinks are still open. This is achieved by publishing the latest
known channel state of the `channel.db` file.
<br/>**Please read the full warning text of the
[`forceclose` command below](doc/chantools_forceclose.md) as this command can put
your funds at risk** if the state in the channel DB is not the most recent
one. This command should only be executed for channels where the remote peer
is not online anymore.
<br/><br/>
`chantools --fromsummary ./results/<rescueclosed-file-created-in-last-step>.json forceclose --channeldb ./results/compacted.db --publish`
<br/><br/>
This will create a new file called `./results/forceclose-yyyy-mm-dd.json`
which will be needed for the next command.
<br/><br/>
If you get the error `non-mandatory-script-verify-flag (Signature must be zero
for failed CHECK(MULTI)SIG operation)`, you might be affected by an old bug
of `lnd` that was fixed in the meantime. But it means the signature in the
force-close transaction is invalid and needs to be fixed. There is [a guide
on how to do exactly that here](doc/fix-commitment-tx.md).
`chantools` thinks are still open. This is achieved by publishing the latest
known channel state of the `channel.db` file.
<br/>**Please read the full warning text of the
[`forceclose` command below](doc/chantools_forceclose.md) as this command can
put
your funds at risk** if the state in the channel DB is not the most recent
one. This command should only be executed for channels where the remote peer
is not online anymore.
<br/><br/>
`chantools --fromsummary ./results/<rescueclosed-file-created-in-last-step>.json forceclose --channeldb ./results/compacted.db --publish`
<br/><br/>
This will create a new file called `./results/forceclose-yyyy-mm-dd.json`
which will be needed for the next command.
<br/><br/>
If you get the
error `non-mandatory-script-verify-flag (Signature must be zero
for failed CHECK(MULTI)SIG operation)`, you might be affected by an old bug
of `lnd` that was fixed in the meantime. But it means the signature in the
force-close transaction is invalid and needs to be fixed. There is [a guide
on how to do exactly that here](doc/fix-commitment-tx.md).
9. **Wait for timelocks**: The previous command closed the remaining open
channels by publishing your node's state of the channel. By design of the
Lightning Network, you now have to wait until the channel funds belonging to
you are not time locked any longer. Depending on the size of the channel, you
have to wait for somewhere between 144 and 2000 confirmations of the
force-close transactions. Only continue with the next step after the channel
with the highest `csv_delay` has reached that many confirmations of its
closing transaction. You can check this by looking up each force closed
channel transaction on a block explorer (like
[blockstream.info](https://blockstream.info) for example). Open the result
JSON file of the last command (`./results/forceclose-yyyy-mm-dd.json`) and
look up every TXID in `"force_close" -> "txid"` on the explorer. If the number
of confirmations is equal to or greater to the value shown in
`"force_close" -> "csv_delay"` for each of the channels, you can proceed.
channels by publishing your node's state of the channel. By design of the
Lightning Network, you now have to wait until the channel funds belonging to
you are not time locked any longer. Depending on the size of the channel, you
have to wait for somewhere between 144 and 2000 confirmations of the
force-close transactions. Only continue with the next step after the channel
with the highest `csv_delay` has reached that many confirmations of its
closing transaction. You can check this by looking up each force closed
channel transaction on a block explorer (like
[blockstream.info](https://blockstream.info) for example). Open the result
JSON file of the last command (`./results/forceclose-yyyy-mm-dd.json`) and
look up every TXID in `"force_close" -> "txid"` on the explorer. If the
number
of confirmations is equal to or greater to the value shown in
`"force_close" -> "csv_delay"` for each of the channels, you can proceed.
10. **chantools sweeptimelock**: Once all force-close transactions have reached
the number of transactions as the `csv_timeout` in the JSON demands, these
time locked funds can now be swept. Use the following command to sweep all the
channel funds to an address of your wallet:
<br/><br/>
`chantools --fromsummary ./results/<forceclose-file-created-in-last-step>.json sweeptimelock --publish --sweepaddr <bech32-address-from-your-wallet>`
the number of transactions as the `csv_timeout` in the JSON demands, these
time locked funds can now be swept. Use the following command to sweep all
the
channel funds to an address of your wallet:
<br/><br/>
`chantools --fromsummary ./results/<forceclose-file-created-in-last-step>.json sweeptimelock --publish --sweepaddr <bech32-address-from-your-wallet>`
11. **Manual intervention necessary**: You got to this step because you either
don't have a `channel.db` file or because `chantools` couldn't rescue all your
node's channels. There are a few things you can try manually that have some
chance of working:
don't have a `channel.db` file or because `chantools` couldn't rescue all
your
node's channels. There are a few things you can try manually that have some
chance of working:
- Make sure you can connect to all nodes when restoring from SCB: It happens
all the time that nodes change their IP addresses. When restoring from a
static channel backup, your node tries to connect to the node using the IP
address encoded in the backup file. If the address changed, the SCB restore
process doesn't work. You can use block explorers like [1ml.com](https://1ml.com)
to try to find an IP address that is up-to-date. Just run
`lncli connect <node-pubkey>@<updated-ip-address>:<port>` in the recovered
`lnd` node from step 3 and wait a few hours to see if the channel is now
being force closed by the remote node.
- Find out who the node belongs to: Maybe you opened the channel with someone
you know. Or maybe their node alias contains some information about who the
node belongs to. If you can find out who operates the remote node, you can
ask them to force-close the channel from their end. If the channel was opened
with the `option_static_remote_key`, (`lnd v0.8.0` and later), the funds can
be swept by your node.
all the time that nodes change their IP addresses. When restoring from a
static channel backup, your node tries to connect to the node using the IP
address encoded in the backup file. If the address changed, the SCB
restore
process doesn't work. You can use block explorers
like [1ml.com](https://1ml.com)
to try to find an IP address that is up-to-date. Just run
`lncli connect <node-pubkey>@<updated-ip-address>:<port>` in the recovered
`lnd` node from step 3 and wait a few hours to see if the channel is now
being force closed by the remote node.
- Find out who the node belongs to: Maybe you opened the channel with
someone
you know. Or maybe their node alias contains some information about who
the
node belongs to. If you can find out who operates the remote node, you can
ask them to force-close the channel from their end. If the channel was
opened
with the `option_static_remote_key`, (`lnd v0.8.0` and later), the funds
can
be swept by your node.
12. **Use Zombie Channel Recovery Matcher**: As a final, last resort, you can
go to [node-recovery.com](https://www.node-recovery.com/) and register your
node's ID for being matched up against other nodes with the same problem.
<br/><br/>
Once you were contacted with a match, follow the instructions on the
[Zombie Channel Recovery Guide](doc/zombierecovery.md) page.
<br/><br/>
If you know the peer of a zombie channel and have a way to contact them, you
can also skip the registration/matching process and [create your own match
file](doc/zombierecovery.md#file-format).
go to [node-recovery.com](https://www.node-recovery.com/) and register your
node's ID for being matched up against other nodes with the same problem.
<br/><br/>
Once you were contacted with a match, follow the instructions on the
[Zombie Channel Recovery Guide](doc/zombierecovery.md) page.
<br/><br/>
If you know the peer of a zombie channel and have a way to contact them, you
can also skip the registration/matching process and [create your own match
file](doc/zombierecovery.md#file-format).
## Seed and passphrase input
All commands that require the seed (and, if set, the seed's passphrase) offer
three distinct possibilities to specify it:
1. **Enter manually on the terminal**: This is the safest option as it makes
sure that the seed isn't stored in the terminal's command history.
sure that the seed isn't stored in the terminal's command history.
2. **Pass the extened master root key as parameter**: This is added as an option
for users who don't have the full seed anymore, possibly because they used
`lnd`'s `--noseedbackup` flag and extracted the `xprv` from the wallet
for users who don't have the full seed anymore, possibly because they used
`lnd`'s `--noseedbackup` flag and extracted the `xprv` from the wallet
database with the `walletinfo` command. Those users can specify the master
root key by passing the `--rootkey` command line flag to each command that
requires the seed.
3. **Use environment variables**: This option makes it easy to automate usage of
`chantools` by removing the need to type into the terminal. There are three
environment variables that can be set to skip entering values through the
terminal:
`chantools` by removing the need to type into the terminal. There are three
environment variables that can be set to skip entering values through the
terminal:
- `AEZEED_MNEMONIC`: Specifies the 24 word `lnd` aezeed.
- `AEZEED_PASSPHRASE`: Specifies the passphrase for the aezeed. If no
passphrase was used during the creation of the seed, the special value
@ -254,12 +385,13 @@ $ export AEZEED_MNEMONIC="abandon able ... ... ..."
$ export AEZEED_PASSPHRASE="-"
$ chantools showrootkey
2020-10-29 20:22:42.329 [INF] CHAN: chantools version v0.6.0 commit v0.6.0-3
2020-10-29 20:22:42.329 [INF] CHAN: chantools version v0.12.0 commit v0.12.0
Your BIP32 HD root key is: xprv9s21ZrQH1...
```
### Are my funds safe?
Some commands require the seed. But your seed will never leave your computer.
Most commands don't require an internet connection: you can and should
@ -270,7 +402,7 @@ run them on a computer with a firewall that blocks outgoing connections.
```text
This tool provides helper functions that can be used rescue
funds locked in lnd channels in case lnd itself cannot run properly anymore.
Complete documentation is available at https://github.com/guggero/chantools/.
Complete documentation is available at https://github.com/lightninglabs/chantools/.
Usage:
chantools [command]
@ -279,9 +411,12 @@ Available Commands:
chanbackup Create a channel.backup file from a channel database
closepoolaccount Tries to close a Pool account that has expired
compactdb Create a copy of a channel.db file in safe/read-only mode
createwallet Create a new lnd compatible wallet.db file from an existing seed or by generating a new one
deletepayments Remove all (failed) payments from a channel DB
derivekey Derive a key with a specific derivation path
doublespendinputs Tries to double spend the given inputs by deriving the private for the address and sweeping the funds to the given address. This can only be used with inputs that belong to an lnd wallet.
dropchannelgraph Remove all graph related data from a channel DB
dropgraphzombies Remove all channels identified as zombies from the graph to force a re-sync of the graph
dumpbackup Dump the content of a channel.backup file
dumpchannels Dump all channel information from an lnd channel database
fakechanbackup Fake a channel backup file to attempt fund recovery
@ -290,17 +425,20 @@ Available Commands:
forceclose Force-close the last state that is in the channel.db provided
genimportscript Generate a script containing the on-chain keys of an lnd wallet that can be imported into other software like bitcoind
migratedb Apply all recent lnd channel database migrations
pullanchor Attempt to CPFP an anchor output of a channel
removechannel Remove a single channel from the given channel DB
rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels
rescuefunding Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the initiator of the channel needs to run
rescuetweakedkey Attempt to rescue funds locked in an address with a key that was affected by a specific bug in lnd
showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed
signmessage Sign a message with the nodes identity pubkey.
signpsbt Sign a Partially Signed Bitcoin Transaction (PSBT)
signrescuefunding Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the remote node (the non-initiator) of the channel needs to run
summary Compile a summary about the current state of channels
sweeptimelock Sweep the force-closed state after the time lock has expired
sweeptimelockmanual Sweep the force-closed state of a single channel manually if only a channel backup file is available
sweepremoteclosed Go through all the addresses that could have funds of channels that were force-closed by the remote party. A public block explorer is queried for each address and if any balance is found, all funds are swept to a given address
triggerforceclose Connect to a peer and send a custom message to trigger a force close of the specified channel
triggerforceclose Connect to a peer and send request to trigger a force close of the specified channel
vanitygen Generate a seed with a custom lnd node identity public key that starts with the given prefix
walletinfo Shows info about an lnd wallet.db file and optionally extracts the BIP32 HD root key
zombierecovery Try rescuing funds stuck in channels with zombie nodes
@ -319,32 +457,66 @@ Use "chantools [command] --help" for more information about a command.
Detailed documentation for each sub command is available in the
[docs](doc/chantools.md) folder.
Quick access:
+ [chanbackup](doc/chantools_chanbackup.md)
+ [closepoolaccount](doc/chantools_closepoolaccount.md)
+ [compactdb](doc/chantools_compactdb.md)
+ [deletepayments](doc/chantools_deletepayments.md)
+ [derivekey](doc/chantools_derivekey.md)
+ [dropchannelgraph](doc/chantools_dropchannelgraph.md)
+ [dumpbackup](doc/chantools_dumpbackup.md)
+ [dumpchannels](doc/chantools_dumpchannels.md)
+ [fakechanbackup](doc/chantools_fakechanbackup.md)
+ [filterbackup](doc/chantools_filterbackup.md)
+ [fixoldbackup](doc/chantools_fixoldbackup.md)
+ [forceclose](doc/chantools_forceclose.md)
+ [genimportscript](doc/chantools_genimportscript.md)
+ [migratedb](doc/chantools_migratedb.md)
+ [recoverloopin](doc/chantools_recoverloopin.md)
+ [removechannel](doc/chantools_removechannel.md)
+ [rescueclosed](doc/chantools_rescueclosed.md)
+ [rescuefunding](doc/chantools_rescuefunding.md)
+ [showrootkey](doc/chantools_showrootkey.md)
+ [signrescuefunding](doc/chantools_signrescuefunding.md)
+ [summary](doc/chantools_summary.md)
+ [sweepremoteclosed](doc/chantools_sweepremoteclosed.md)
+ [sweeptimelock](doc/chantools_sweeptimelock.md)
+ [sweeptimelockmanual](doc/chantools_sweeptimelockmanual.md)
+ [triggerforceclose](doc/chantools_triggerforceclose.md)
+ [vanitygen](doc/chantools_vanitygen.md)
+ [walletinfo](doc/chantools_walletinfo.md)
+ [zombierecovery](doc/chantools_zombierecovery.md)
The following table provides quick access to each command's documentation.
Legend:
- :pencil: This command requires the seed to be entered (see [seed and
passphrase input](#seed-and-passphrase-input)).
- :warning: Should not be used unless no other option exists, can lead to
malfunction of the node.
- :skull: Danger of loss of funds, only use when instructed to.
- :pushpin: Command was created for a very specific version or use case and most
likely does not apply to 99.9% of users
| Command | Use when |
|-------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|
| [chanbackup](doc/chantools_chanbackup.md) | :pencil: Extract a `channel.backup` file from a `channel.db` file |
| [closepoolaccount](doc/chantools_closepoolaccount.md) | :pencil: Manually close an expired Lightning Pool account |
| [compactdb](doc/chantools_compactdb.md) | Run database compaction manually to reclaim space |
| [createwallet](doc/chantools_createwallet.md) | :pencil: Create a new lnd compatible wallet.db file from an existing seed or by generating a new one |
| [deletepayments](doc/chantools_deletepayments.md) | Remove ALL payments from a `channel.db` file to reduce size |
| [derivekey](doc/chantools_derivekey.md) | :pencil: Derive a single private/public key from `lnd`'s seed, use to test seed |
| [doublespendinputs](doc/chantools_doublespendinputs.md) | :pencil: Tries to double spend the given inputs by deriving the private for the address and sweeping the funds to the given address |
| [dropchannelgraph](doc/chantools_dropchannelgraph.md) | (:warning:) Completely drop the channel graph from a `channel.db` to force re-sync |
| [dropgraphzombies](doc/chantools_dropgraphzombies.md) | Drop all zombie channels from a `channel.db` to force a graph re-sync |
| [dumpbackup](doc/chantools_dumpbackup.md) | :pencil: Show the content of a `channel.backup` file as text |
| [dumpchannels](doc/chantools_dumpchannels.md) | Show the content of a `channel.db` file as text |
| [fakechanbackup](doc/chantools_fakechanbackup.md) | :pencil: Create a fake `channel.backup` file from public information |
| [filterbackup](doc/chantools_filterbackup.md) | :pencil: Remove a channel from a `channel.backup` file |
| [fixoldbackup](doc/chantools_fixoldbackup.md) | :pencil: (:pushpin:) Fixes an issue with old `channel.backup` files |
| [forceclose](doc/chantools_forceclose.md) | :pencil: (:skull: :warning:) Publish an old channel state from a `channel.db` file |
| [genimportscript](doc/chantools_genimportscript.md) | :pencil: Create a script/text file that can be used to import `lnd` keys into other software |
| [migratedb](doc/chantools_migratedb.md) | Upgrade the `channel.db` file to the latest version |
| [pullanchor](doc/chantools_pullanchor.md) | :pencil: Attempt to CPFP an anchor output of a channel |
| [recoverloopin](doc/chantools_recoverloopin.md) | :pencil: Recover funds from a failed Lightning Loop inbound swap |
| [removechannel](doc/chantools_removechannel.md) | (:skull: :warning:) Remove a single channel from a `channel.db` file |
| [rescueclosed](doc/chantools_rescueclosed.md) | :pencil: (:pushpin:) Rescue funds in a legacy (pre `STATIC_REMOTE_KEY`) channel output |
| [rescuefunding](doc/chantools_rescuefunding.md) | :pencil: (:pushpin:) Rescue funds from a funding transaction. Deprecated, use [zombierecovery](doc/chantools_zombierecovery.md) instead |
| [showrootkey](doc/chantools_showrootkey.md) | :pencil: Display the master root key (`xprv`) from your seed (DO NOT SHARE WITH ANYONE) |
| [signmessage](doc/chantools_signmessage.md) | :pencil: Sign a message with the nodes identity pubkey. |
| [signpsbt](doc/chantools_signpsbt.md) | :pencil: Sign a Partially Signed Bitcoin Transaction (PSBT) |
| [signrescuefunding](doc/chantools_signrescuefunding.md) | :pencil: (:pushpin:) Sign to funds from a funding transaction. Deprecated, use [zombierecovery](doc/chantools_zombierecovery.md) instead |
| [summary](doc/chantools_summary.md) | Create a summary of channel funds from a `channel.db` file |
| [sweepremoteclosed](doc/chantools_sweepremoteclosed.md) | :pencil: Find channel funds from remotely force closed channels and sweep them |
| [sweeptimelock](doc/chantools_sweeptimelock.md) | :pencil: Sweep funds in locally force closed channels once time lock has expired (requires `channel.db`) |
| [sweeptimelockmanual](doc/chantools_sweeptimelockmanual.md) | :pencil: Manually sweep funds in a locally force closed channel where no `channel.db` file is available |
| [triggerforceclose](doc/chantools_triggerforceclose.md) | :pencil: (:pushpin:) Request a peer to force close a channel |
| [vanitygen](doc/chantools_vanitygen.md) | Generate an `lnd` seed for a node public key that starts with a certain sequence of hex digits |
| [walletinfo](doc/chantools_walletinfo.md) | Show information from a `wallet.db` file, requires access to the wallet password |
| [zombierecovery](doc/chantools_zombierecovery.md) | :pencil: Cooperatively rescue funds from channels where normal recovery is not possible (see [full guide here][zombie-recovery]) |
[safety]: https://github.com/lightningnetwork/lnd/blob/master/docs/safety.md
[safety-zombie]: https://github.com/lightningnetwork/lnd/blob/master/docs/safety.md#zombie-channels
[safety-file-backup]: https://github.com/lightningnetwork/lnd/blob/master/docs/safety.md#file-based-backups
[safety-scb]: https://github.com/lightningnetwork/lnd/blob/master/docs/safety.md#static-channel-backups-scbs
[recovery]: https://github.com/lightningnetwork/lnd/blob/master/docs/recovery.md
[slack]: https://lightning.engineering/slack.html
[discussions]: https://github.com/lightningnetwork/lnd/discussions
[zombie-recovery]: doc/zombierecovery.md

@ -12,7 +12,7 @@ import (
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/guggero/chantools/bip39"
"github.com/lightninglabs/chantools/bip39"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/ssh/terminal"
)

@ -10,7 +10,7 @@ import (
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
)
const (

@ -196,13 +196,17 @@ func (a *ExplorerAPI) PublishTx(rawTxHex string) (string, error) {
url := fmt.Sprintf("%s/tx", a.BaseURL)
resp, err := http.Post(url, "text/plain", strings.NewReader(rawTxHex))
if err != nil {
return "", err
return "", fmt.Errorf("error posting data to API '%s', "+
"server might be experiencing temporary issues, try "+
"again later; error details: %w", url, err)
}
defer resp.Body.Close()
body := new(bytes.Buffer)
_, err = body.ReadFrom(resp.Body)
if err != nil {
return "", err
return "", fmt.Errorf("error fetching data from API '%s', "+
"server might be experiencing temporary issues, try "+
"again later; error details: %w", url, err)
}
return body.String(), nil
}
@ -210,20 +214,29 @@ func (a *ExplorerAPI) PublishTx(rawTxHex string) (string, error) {
func fetchJSON(url string, target interface{}) error {
resp, err := http.Get(url)
if err != nil {
return err
return fmt.Errorf("error fetching data from API '%s', "+
"server might be experiencing temporary issues, try "+
"again later; error details: %w", url, err)
}
defer resp.Body.Close()
body := new(bytes.Buffer)
_, err = body.ReadFrom(resp.Body)
if err != nil {
return err
return fmt.Errorf("error fetching data from API '%s', "+
"server might be experiencing temporary issues, try "+
"again later; error details: %w", url, err)
}
err = json.Unmarshal(body.Bytes(), target)
if err != nil {
if body.String() == "Transaction not found" {
return ErrTxNotFound
}
return fmt.Errorf("error decoding data from API '%s', "+
"server might be experiencing temporary issues, try "+
"again later; error details: %w", url, err)
}
return err
return nil
}

@ -4,16 +4,15 @@ import (
"errors"
"github.com/btcsuite/btclog"
"github.com/guggero/chantools/dataformat"
"github.com/lightninglabs/chantools/dataformat"
)
func SummarizeChannels(apiURL string, channels []*dataformat.SummaryEntry,
func SummarizeChannels(api *ExplorerAPI, channels []*dataformat.SummaryEntry,
log btclog.Logger) (*dataformat.SummaryEntryFile, error) {
summaryFile := &dataformat.SummaryEntryFile{
Channels: channels,
}
api := &ExplorerAPI{BaseURL: apiURL}
for idx, channel := range channels {
tx, err := api.Transaction(channel.FundingTXID)

@ -3,7 +3,7 @@ package main
import (
"fmt"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/chanbackup"
"github.com/spf13/cobra"
)

@ -10,8 +10,7 @@ import (
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightninglabs/pool/account"
"github.com/lightninglabs/pool/poolscript"
"github.com/lightningnetwork/lnd/input"
@ -42,7 +41,7 @@ type closePoolAccountCommand struct {
AuctioneerKey string
Publish bool
SweepAddr string
FeeRate uint16
FeeRate uint32
MinExpiry uint32
MaxNumBlocks uint32
@ -89,9 +88,11 @@ obtained by running 'pool accounts list' `,
"API instead of just printing the TX",
)
cc.cmd.Flags().StringVar(
&cc.SweepAddr, "sweepaddr", "", "address to sweep the funds to",
&cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
"derive a new address from the seed automatically",
)
cc.cmd.Flags().Uint16Var(
cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
"use for the sweep transaction in sat/vByte",
)
@ -125,8 +126,12 @@ func (c *closePoolAccountCommand) Execute(_ *cobra.Command, _ []string) error {
}
// Make sure sweep addr is set.
if c.SweepAddr == "" {
return fmt.Errorf("sweep addr is required")
err = lnd.CheckAddress(
c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
}
// Parse account outpoint and auctioneer key.
@ -158,14 +163,24 @@ func (c *closePoolAccountCommand) Execute(_ *cobra.Command, _ []string) error {
func closePoolAccount(extendedKey *hdkeychain.ExtendedKey, apiURL string,
outpoint *wire.OutPoint, auctioneerKey *btcec.PublicKey,
sweepAddr string, publish bool, feeRate uint16, minExpiry,
sweepAddr string, publish bool, feeRate uint32, minExpiry,
maxNumBlocks, maxNumAccounts, maxNumBatchKeys uint32) error {
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
var (
estimator input.TxWeightEstimator
signer = &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
api = newExplorerAPI(apiURL)
)
sweepScript, err := lnd.PrepareWalletAddress(
sweepAddr, chainParams, &estimator, extendedKey, "sweep",
)
if err != nil {
return err
}
api := &btc.ExplorerAPI{BaseURL: apiURL}
tx, err := api.Transaction(outpoint.Hash.String())
if err != nil {
@ -241,7 +256,6 @@ func closePoolAccount(extendedKey *hdkeychain.ExtendedKey, apiURL string,
// Calculate the fee based on the given fee rate and our weight
// estimation.
var (
estimator input.TxWeightEstimator
prevOutFetcher = txscript.NewCannedPrevOutputFetcher(
pkScript, sweepValue,
)
@ -277,15 +291,10 @@ func closePoolAccount(extendedKey *hdkeychain.ExtendedKey, apiURL string,
signDesc.HashType = txscript.SigHashDefault
signDesc.SignMethod = input.TaprootScriptSpendSignMethod
}
estimator.AddP2WKHOutput()
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
// Add our sweep destination output.
sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams)
if err != nil {
return err
}
sweepTx.TxOut = []*wire.TxOut{{
Value: sweepValue - int64(totalFee),
PkScript: sweepScript,

@ -7,7 +7,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightninglabs/pool/poolscript"
"github.com/lightningnetwork/lnd/keychain"
"github.com/stretchr/testify/require"

@ -0,0 +1,232 @@
package main
import (
"bytes"
"fmt"
"os"
"strings"
"time"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/aezeed"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
"github.com/spf13/cobra"
)
type createWalletCommand struct {
WalletDBDir string
GenerateSeed bool
rootKey *rootKey
cmd *cobra.Command
}
func newCreateWalletCommand() *cobra.Command {
cc := &createWalletCommand{}
cc.cmd = &cobra.Command{
Use: "createwallet",
Short: "Create a new lnd compatible wallet.db file from an " +
"existing seed or by generating a new one",
Long: `Creates a new wallet that can be used with lnd or with
chantools. The wallet can be created from an existing seed or a new one can be
generated (use --generateseed).`,
Example: `chantools createwallet \
--walletdbdir ~/.lnd/data/chain/bitcoin/mainnet`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.WalletDBDir, "walletdbdir", "", "the folder to create the "+
"new wallet.db file in",
)
cc.cmd.Flags().BoolVar(
&cc.GenerateSeed, "generateseed", false, "generate a new "+
"seed instead of using an existing one",
)
cc.rootKey = newRootKey(cc.cmd, "creating the new wallet")
return cc.cmd
}
func (c *createWalletCommand) Execute(_ *cobra.Command, _ []string) error {
var (
publicWalletPw = lnwallet.DefaultPublicPassphrase
privateWalletPw = lnwallet.DefaultPrivatePassphrase
masterRootKey *hdkeychain.ExtendedKey
birthday time.Time
err error
)
// Check that we have a wallet DB.
if c.WalletDBDir == "" {
return fmt.Errorf("wallet DB directory is required")
}
// Make sure the directory (and parents) exists.
if err := os.MkdirAll(c.WalletDBDir, 0700); err != nil {
return fmt.Errorf("error creating wallet DB directory '%s': %w",
c.WalletDBDir, err)
}
// Check if we should create a new seed or read if from the console or
// environment.
if c.GenerateSeed {
fmt.Printf("Generating new lnd compatible aezeed...\n")
seed, err := aezeed.New(
keychain.KeyDerivationVersionTaproot, nil, time.Now(),
)
if err != nil {
return fmt.Errorf("error creating new seed: %w", err)
}
birthday = seed.BirthdayTime()
// Derive the master extended key from the seed.
masterRootKey, err = hdkeychain.NewMaster(
seed.Entropy[:], chainParams,
)
if err != nil {
return fmt.Errorf("failed to derive master extended "+
"key: %w", err)
}
passphrase, err := lnd.ReadPassphrase("shouldn't use")
if err != nil {
return fmt.Errorf("error reading passphrase: %w", err)
}
mnemonic, err := seed.ToMnemonic(passphrase)
if err != nil {
return fmt.Errorf("error converting seed to "+
"mnemonic: %w", err)
}
fmt.Println("Generated new seed")
printCipherSeedWords(mnemonic[:])
} else {
masterRootKey, birthday, err = c.rootKey.readWithBirthday()
if err != nil {
return err
}
}
// To automate things with chantools, we also offer reading the wallet
// password from environment variables.
pw := []byte(strings.TrimSpace(os.Getenv(lnd.PasswordEnvName)))
// Because we cannot differentiate between an empty and a non-existent
// environment variable, we need a special character that indicates that
// no password should be used. We use a single dash (-) for that as that
// would be too short for an explicit password anyway.
switch {
// The user indicated in the environment variable that no passphrase
// should be used. We don't set any value.
case string(pw) == "-":
// The environment variable didn't contain anything, we'll read the
// passphrase from the terminal.
case len(pw) == 0:
fmt.Printf("\n\nThe wallet password is used to encrypt the " +
"wallet.db file itself and is unrelated to the seed.\n")
pw, err = lnd.PasswordFromConsole("Input new wallet password: ")
if err != nil {
return err
}
pw2, err := lnd.PasswordFromConsole(
"Confirm new wallet password: ",
)
if err != nil {
return err
}
if !bytes.Equal(pw, pw2) {
return fmt.Errorf("passwords don't match")
}
if len(pw) > 0 {
publicWalletPw = pw
privateWalletPw = pw
}
// There was a password in the environment, just use it directly.
default:
publicWalletPw = pw
privateWalletPw = pw
}
// Try to create the wallet.
loader, err := btcwallet.NewWalletLoader(
chainParams, 0, btcwallet.LoaderWithLocalWalletDB(
c.WalletDBDir, true, 0,
),
)
if err != nil {
return fmt.Errorf("error creating wallet loader: %w", err)
}
_, err = loader.CreateNewWalletExtendedKey(
publicWalletPw, privateWalletPw, masterRootKey, birthday,
)
if err != nil {
return fmt.Errorf("error creating new wallet: %w", err)
}
if err := loader.UnloadWallet(); err != nil {
return fmt.Errorf("error unloading wallet: %w", err)
}
fmt.Printf("Wallet created successfully at %v\n", c.WalletDBDir)
return nil
}
func printCipherSeedWords(mnemonicWords []string) {
fmt.Println("!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
"RESTORE THE WALLET!!!")
fmt.Println()
fmt.Println("---------------BEGIN LND CIPHER SEED---------------")
numCols := 4
colWords := monoWidthColumns(mnemonicWords, numCols)
for i := 0; i < len(colWords); i += numCols {
fmt.Printf("%2d. %3s %2d. %3s %2d. %3s %2d. %3s\n",
i+1, colWords[i], i+2, colWords[i+1], i+3,
colWords[i+2], i+4, colWords[i+3])
}
fmt.Println("---------------END LND CIPHER SEED-----------------")
fmt.Println("\n!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
"RESTORE THE WALLET!!!")
}
// monoWidthColumns takes a set of words, and the number of desired columns,
// and returns a new set of words that have had white space appended to the
// word in order to create a mono-width column.
func monoWidthColumns(words []string, ncols int) []string {
// Determine max size of words in each column.
colWidths := make([]int, ncols)
for i, word := range words {
col := i % ncols
curWidth := colWidths[col]
if len(word) > curWidth {
colWidths[col] = len(word)
}
}
// Append whitespace to each word to make columns mono-width.
finalWords := make([]string, len(words))
for i, word := range words {
col := i % ncols
width := colWidths[col]
diff := width - len(word)
finalWords[i] = word + strings.Repeat(" ", diff)
}
return finalWords
}

@ -3,7 +3,7 @@ package main
import (
"fmt"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/spf13/cobra"
)
@ -25,7 +25,7 @@ If only the failed payments should be deleted (and not the successful ones), the
CAUTION: Running this command will make it impossible to use the channel DB
with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.16.0-beta or later after using this command!'`,
run lnd ` + lndVersion + ` or later after using this command!'`,
Example: `chantools deletepayments --failedonly \
--channeldb ~/.lnd/data/graph/mainnet/channel.db`,
RunE: cc.Execute,

@ -5,7 +5,7 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/spf13/cobra"
)

@ -3,8 +3,8 @@ package main
import (
"testing"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd"
"github.com/stretchr/testify/require"
)

@ -0,0 +1,364 @@
package main
import (
"bytes"
"encoding/hex"
"fmt"
"strconv"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/mempool"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/spf13/cobra"
)
type doubleSpendInputs struct {
APIURL string
InputOutpoints []string
Publish bool
SweepAddr string
FeeRate uint32
RecoveryWindow uint32
rootKey *rootKey
cmd *cobra.Command
}
func newDoubleSpendInputsCommand() *cobra.Command {
cc := &doubleSpendInputs{}
cc.cmd = &cobra.Command{
Use: "doublespendinputs",
Short: "Replace a transaction by double spending its input",
Long: `Tries to double spend the given inputs by deriving the
private for the address and sweeping the funds to the given address. This can
only be used with inputs that belong to an lnd wallet.`,
Example: `chantools doublespendinputs \
--inputoutpoints xxxxxxxxx:y,xxxxxxxxx:y \
--sweepaddr bc1q..... \
--feerate 10 \
--publish`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
"be esplora compatible)",
)
cc.cmd.Flags().StringSliceVar(
&cc.InputOutpoints, "inputoutpoints", []string{},
"list of outpoints to double spend in the format txid:vout",
)
cc.cmd.Flags().StringVar(
&cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
"derive a new address from the seed automatically",
)
cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
"use for the sweep transaction in sat/vByte",
)
cc.cmd.Flags().Uint32Var(
&cc.RecoveryWindow, "recoverywindow", defaultRecoveryWindow,
"number of keys to scan per internal/external branch; output "+
"will consist of double this amount of keys",
)
cc.cmd.Flags().BoolVar(
&cc.Publish, "publish", false, "publish replacement TX to "+
"the chain API instead of just printing the TX",
)
cc.rootKey = newRootKey(cc.cmd, "deriving the input keys")
return cc.cmd
}
func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
extendedKey, err := c.rootKey.read()
if err != nil {
return fmt.Errorf("error reading root key: %w", err)
}
// Make sure sweep addr is set.
err = lnd.CheckAddress(
c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
}
// Make sure we have at least one input.
if len(c.InputOutpoints) == 0 {
return fmt.Errorf("inputoutpoints are required")
}
api := newExplorerAPI(c.APIURL)
addresses := make([]btcutil.Address, 0, len(c.InputOutpoints))
outpoints := make([]*wire.OutPoint, 0, len(c.InputOutpoints))
privKeys := make([]*secp256k1.PrivateKey, 0, len(c.InputOutpoints))
// Get the addresses for the inputs.
for _, inputOutpoint := range c.InputOutpoints {
addrString, err := api.Address(inputOutpoint)
if err != nil {
return err
}
addr, err := btcutil.DecodeAddress(addrString, chainParams)
if err != nil {
return err
}
addresses = append(addresses, addr)
txHash, err := chainhash.NewHashFromStr(inputOutpoint[:64])
if err != nil {
return err
}
vout, err := strconv.Atoi(inputOutpoint[65:])
if err != nil {
return err
}
outpoint := wire.NewOutPoint(txHash, uint32(vout))
outpoints = append(outpoints, outpoint)
}
// Create the paths for the addresses.
p2wkhPath, err := lnd.ParsePath(lnd.WalletDefaultDerivationPath)
if err != nil {
return err
}
p2trPath, err := lnd.ParsePath(lnd.WalletBIP86DerivationPath)
if err != nil {
return err
}
// Start with the txweight estimator.
var estimator input.TxWeightEstimator
sweepScript, err := lnd.PrepareWalletAddress(
c.SweepAddr, chainParams, &estimator, extendedKey, "sweep",
)
if err != nil {
return err
}
// Find the key for the given addresses and add their
// output weight to the tx estimator.
for _, addr := range addresses {
var key *hdkeychain.ExtendedKey
switch addr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
key, err = iterateOverPath(
extendedKey, addr, p2wkhPath, c.RecoveryWindow,
)
if err != nil {
return err
}
estimator.AddP2WKHInput()
case *btcutil.AddressTaproot:
key, err = iterateOverPath(
extendedKey, addr, p2trPath, c.RecoveryWindow,
)
if err != nil {
return err
}
estimator.AddTaprootKeySpendInput(
txscript.SigHashDefault,
)
default:
return fmt.Errorf("address type %T not supported", addr)
}
// Get the private key.
privKey, err := key.ECPrivKey()
if err != nil {
return err
}
privKeys = append(privKeys, privKey)
}
// Now that we have the keys, we can create the transaction.
prevOuts := make(map[wire.OutPoint]*wire.TxOut)
// Next get the full value of the inputs.
var totalInput btcutil.Amount
for _, outpoint := range outpoints {
// Get the transaction.
tx, err := api.Transaction(outpoint.Hash.String())
if err != nil {
return err
}
value := tx.Vout[outpoint.Index].Value
// Get the output index.
totalInput += btcutil.Amount(value)
scriptPubkey, err := hex.DecodeString(
tx.Vout[outpoint.Index].ScriptPubkey,
)
if err != nil {
return err
}
// Add the output to the map.
prevOuts[*outpoint] = &wire.TxOut{
Value: int64(value),
PkScript: scriptPubkey,
}
}
// Calculate the fee.
feeRateKWeight := chainfee.SatPerKVByte(1000 * c.FeeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
// Create the transaction.
tx := wire.NewMsgTx(2)
// Add the inputs.
for _, outpoint := range outpoints {
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: *outpoint,
Sequence: mempool.MaxRBFSequence,
})
}
tx.AddTxOut(wire.NewTxOut(int64(totalInput-totalFee), sweepScript))
// Calculate the signature hash.
prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
// Sign the inputs depending on the address type.
for i, outpoint := range outpoints {
switch addresses[i].(type) {
case *btcutil.AddressWitnessPubKeyHash:
witness, err := txscript.WitnessSignature(
tx, sigHashes, i, prevOuts[*outpoint].Value,
prevOuts[*outpoint].PkScript,
txscript.SigHashAll, privKeys[i], true,
)
if err != nil {
return err
}
tx.TxIn[i].Witness = witness
case *btcutil.AddressTaproot:
rawTxSig, err := txscript.RawTxInTaprootSignature(
tx, sigHashes, i,
prevOuts[*outpoint].Value,
prevOuts[*outpoint].PkScript,
[]byte{}, txscript.SigHashDefault, privKeys[i],
)
if err != nil {
return err
}
tx.TxIn[i].Witness = wire.TxWitness{
rawTxSig,
}
default:
return fmt.Errorf("address type %T not supported",
addresses[i])
}
}
// Serialize the transaction.
var txBuf bytes.Buffer
if err := tx.Serialize(&txBuf); err != nil {
return err
}
// Print the transaction.
fmt.Printf("Sweeping transaction:\n%x\n", txBuf.Bytes())
// Publish the transaction.
if c.Publish {
txid, err := api.PublishTx(hex.EncodeToString(txBuf.Bytes()))
if err != nil {
return err
}
fmt.Printf("Published transaction with txid %s\n", txid)
}
return nil
}
// iterateOverPath iterates over the given key path and tries to find the
// private key that corresponds to the given address.
func iterateOverPath(baseKey *hdkeychain.ExtendedKey, addr btcutil.Address,
path []uint32, maxTries uint32) (*hdkeychain.ExtendedKey, error) {
for i := uint32(0); i < maxTries; i++ {
// Check for both the external and internal branch.
for _, branch := range []uint32{0, 1} {
// Create the path to derive the key.
addrPath := append(path, branch, i) //nolint:gocritic
// Derive the key.
derivedKey, err := lnd.DeriveChildren(baseKey, addrPath)
if err != nil {
return nil, err
}
var address btcutil.Address
switch addr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
// Get the address for the derived key.
derivedAddr, err := derivedKey.Address(chainParams)
if err != nil {
return nil, err
}
address, err = btcutil.NewAddressWitnessPubKeyHash(
derivedAddr.ScriptAddress(), chainParams,
)
if err != nil {
return nil, err
}
case *btcutil.AddressTaproot:
pubkey, err := derivedKey.ECPubKey()
if err != nil {
return nil, err
}
pubkey = txscript.ComputeTaprootKeyNoScript(pubkey)
address, err = btcutil.NewAddressTaproot(
schnorr.SerializePubKey(pubkey), chainParams,
)
if err != nil {
return nil, err
}
}
// Compare the addresses.
if address.String() == addr.String() {
return derivedKey, nil
}
}
}
return nil, fmt.Errorf("could not find key for address %s", addr.String())
}

@ -9,7 +9,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/chainreg"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/keychain"
@ -46,7 +46,7 @@ without removing any other data.
CAUTION: Running this command will make it impossible to use the channel DB
with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.16.0-beta or later after using this command!'`,
run lnd ` + lndVersion + ` or later after using this command!'`,
Example: `chantools dropchannelgraph \
--channeldb ~/.lnd/data/graph/mainnet/channel.db \
--node_identity_key 03......
@ -58,7 +58,7 @@ chantools dropchannelgraph \
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.ChannelDB, "channeldb", "", "lnd channel.db file to dump "+
&cc.ChannelDB, "channeldb", "", "lnd channel.db file to drop "+
"channels from",
)
cc.cmd.Flags().Uint64Var(

@ -0,0 +1,88 @@
package main
import (
"fmt"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/spf13/cobra"
)
var (
zombieBucket = []byte("zombie-index")
)
type dropGraphZombiesCommand struct {
ChannelDB string
NodeIdentityKey string
FixOnly bool
SingleChannel uint64
cmd *cobra.Command
}
func newDropGraphZombiesCommand() *cobra.Command {
cc := &dropGraphZombiesCommand{}
cc.cmd = &cobra.Command{
Use: "dropgraphzombies",
Short: "Remove all channels identified as zombies from the " +
"graph to force a re-sync of the graph",
Long: `This command removes all channels that were identified as
zombies from the local graph.
This will cause lnd to re-download all those channels from the network and can
be helpful to fix a graph that is out of sync with the network.
CAUTION: Running this command will make it impossible to use the channel DB
with an older version of lnd. Downgrading is not possible and you'll need to
run lnd ` + lndVersion + ` or later after using this command!'`,
Example: `chantools dropgraphzombies \
--channeldb ~/.lnd/data/graph/mainnet/channel.db`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.ChannelDB, "channeldb", "", "lnd channel.db file to drop "+
"zombies from",
)
return cc.cmd
}
func (c *dropGraphZombiesCommand) Execute(_ *cobra.Command, _ []string) error {
// Check that we have a channel DB.
if c.ChannelDB == "" {
return fmt.Errorf("channel DB is required")
}
db, err := lnd.OpenDB(c.ChannelDB, false)
if err != nil {
return fmt.Errorf("error opening rescue DB: %w", err)
}
defer func() { _ = db.Close() }()
log.Infof("Dropping zombie channel bucket")
rwTx, err := db.BeginReadWriteTx()
if err != nil {
return err
}
success := false
defer func() {
if !success {
_ = rwTx.Rollback()
}
}()
edges := rwTx.ReadWriteBucket(edgeBucket)
if edges == nil {
return channeldb.ErrGraphNoEdgesFound
}
if err := edges.DeleteNestedBucket(zombieBucket); err != nil {
return err
}
success = true
return rwTx.Commit()
}

@ -4,8 +4,8 @@ import (
"fmt"
"github.com/davecgh/go-spew/spew"
"github.com/guggero/chantools/dump"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/dump"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/keychain"
"github.com/spf13/cobra"

@ -1,11 +1,12 @@
package main
import (
"errors"
"fmt"
"github.com/davecgh/go-spew/spew"
"github.com/guggero/chantools/dump"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/dump"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/spf13/cobra"
)
@ -107,7 +108,31 @@ func dumpClosedChannelInfo(chanDb *channeldb.ChannelStateDB) error {
return err
}
dumpChannels, err := dump.ClosedChannelDump(channels, chainParams)
historicalChannels := make([]*channeldb.OpenChannel, len(channels))
for idx := range channels {
closedChan := channels[idx]
histChan, err := chanDb.FetchHistoricalChannel(
&closedChan.ChanPoint,
)
switch {
// The channel was closed in a pre-historic version of lnd.
// Ignore the error.
case errors.Is(err, channeldb.ErrNoHistoricalBucket):
case errors.Is(err, channeldb.ErrChannelNotFound):
case err == nil:
historicalChannels[idx] = histChan
// Non-nil error not due to older versions of lnd.
default:
return err
}
}
dumpChannels, err := dump.ClosedChannelDump(
channels, historicalChannels, chainParams,
)
if err != nil {
return fmt.Errorf("error converting to dump format: %w", err)
}

@ -13,8 +13,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/gogo/protobuf/jsonpb"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/keychain"
@ -138,8 +137,9 @@ func (c *fakeChanBackupCommand) Execute(_ *cobra.Command, _ []string) error {
return fmt.Errorf("error reading graph JSON file %s: "+
"%v", c.FromChannelGraph, err)
}
graph := &lnrpc.ChannelGraph{}
err = jsonpb.UnmarshalString(string(graphBytes), graph)
err = lnrpc.ProtoJSONUnmarshalOpts.Unmarshal(graphBytes, graph)
if err != nil {
return fmt.Errorf("error parsing graph JSON: %w", err)
}

@ -6,7 +6,7 @@ import (
"strings"
"time"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/keychain"
"github.com/spf13/cobra"

@ -6,7 +6,7 @@ import (
"os"
"time"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/keychain"
"github.com/spf13/cobra"

@ -11,9 +11,8 @@ import (
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/txscript"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/dataformat"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/dataformat"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input"
"github.com/spf13/cobra"
@ -105,7 +104,7 @@ func forceCloseChannels(apiURL string, extendedKey *hdkeychain.ExtendedKey,
if err != nil {
return err
}
api := &btc.ExplorerAPI{BaseURL: apiURL}
api := newExplorerAPI(apiURL)
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,

@ -5,8 +5,8 @@ import (
"os"
"time"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd"
"github.com/spf13/cobra"
)

@ -3,7 +3,7 @@ package main
import (
"fmt"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/spf13/cobra"
)
@ -25,7 +25,7 @@ needs to read the database content.
CAUTION: Running this command will make it impossible to use the channel DB
with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.16.0-beta or later after using this command!'`,
run lnd ` + lndVersion + ` or later after using this command!'`,
Example: `chantools migratedb \
--channeldb ~/.lnd/data/graph/mainnet/channel.db`,
RunE: cc.Execute,

@ -0,0 +1,530 @@
package main
import (
"bytes"
"encoding/hex"
"fmt"
"math"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/mempool"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/spf13/cobra"
)
type pullAnchorCommand struct {
APIURL string
SponsorInput string
AnchorAddrs []string
ChangeAddr string
FeeRate uint32
rootKey *rootKey
cmd *cobra.Command
}
func newPullAnchorCommand() *cobra.Command {
cc := &pullAnchorCommand{}
cc.cmd = &cobra.Command{
Use: "pullanchor",
Short: "Attempt to CPFP an anchor output of a channel",
Long: `Use this command to confirm a channel force close
transaction of an anchor output channel type. This will attempt to CPFP the
330 byte anchor output created for your node.`,
Example: `chantools pullanchor \
--sponsorinput txid:vout \
--anchoraddr bc1q..... \
--changeaddr bc1q..... \
--feerate 30`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
"be esplora compatible)",
)
cc.cmd.Flags().StringVar(
&cc.SponsorInput, "sponsorinput", "", "the input to use to "+
"sponsor the CPFP transaction; must be owned by the "+
"lnd node that owns the anchor output",
)
cc.cmd.Flags().StringArrayVar(
&cc.AnchorAddrs, "anchoraddr", nil, "the address of the "+
"anchor output (p2wsh or p2tr output with 330 "+
"satoshis) that should be pulled; can be specified "+
"multiple times per command to pull multiple anchors "+
"with a single transaction",
)
cc.cmd.Flags().StringVar(
&cc.ChangeAddr, "changeaddr", "", "the change address to "+
"send the remaining funds back to; specify '"+
lnd.AddressDeriveFromWallet+"' to derive a new "+
"address from the seed automatically",
)
cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
"use for the sweep transaction in sat/vByte",
)
cc.rootKey = newRootKey(cc.cmd, "deriving keys")
return cc.cmd
}
func (c *pullAnchorCommand) Execute(_ *cobra.Command, _ []string) error {
extendedKey, err := c.rootKey.read()
if err != nil {
return fmt.Errorf("error reading root key: %w", err)
}
// Make sure all input is provided.
if c.SponsorInput == "" {
return fmt.Errorf("sponsor input is required")
}
if len(c.AnchorAddrs) == 0 {
return fmt.Errorf("at least one anchor addr is required")
}
for _, anchorAddr := range c.AnchorAddrs {
err = lnd.CheckAddress(
anchorAddr, chainParams, true, "anchor",
lnd.AddrTypeP2WSH, lnd.AddrTypeP2TR,
)
if err != nil {
return err
}
}
err = lnd.CheckAddress(
c.ChangeAddr, chainParams, true, "change", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
}
outpoint, err := lnd.ParseOutpoint(c.SponsorInput)
if err != nil {
return fmt.Errorf("error parsing sponsor input outpoint: %w",
err)
}
// Set default values.
if c.FeeRate == 0 {
c.FeeRate = defaultFeeSatPerVByte
}
return createPullTransactionTemplate(
extendedKey, c.APIURL, outpoint, c.AnchorAddrs, c.ChangeAddr,
c.FeeRate,
)
}
type targetAnchor struct {
addr string
keyDesc *keychain.KeyDescriptor
outpoint wire.OutPoint
utxo *wire.TxOut
script []byte
scriptTree *input.AnchorScriptTree
}
func createPullTransactionTemplate(rootKey *hdkeychain.ExtendedKey,
apiURL string, sponsorOutpoint *wire.OutPoint, anchorAddrs []string,
changeAddr string, feeRate uint32) error {
var (
signer = &lnd.Signer{
ExtendedKey: rootKey,
ChainParams: chainParams,
}
api = newExplorerAPI(apiURL)
estimator input.TxWeightEstimator
)
changeScript, err := lnd.PrepareWalletAddress(
changeAddr, chainParams, &estimator, rootKey, "change",
)
if err != nil {
return err
}
// Make sure the sponsor input is a P2WPKH or P2TR input and is known
// to the block explorer, so we can fetch the witness utxo.
sponsorTx, err := api.Transaction(sponsorOutpoint.Hash.String())
if err != nil {
return fmt.Errorf("error fetching sponsor tx: %w", err)
}
sponsorTxOut := sponsorTx.Vout[sponsorOutpoint.Index]
sponsorPkScript, err := hex.DecodeString(sponsorTxOut.ScriptPubkey)
if err != nil {
return fmt.Errorf("error decoding sponsor pkscript: %w", err)
}
sponsorType, err := txscript.ParsePkScript(sponsorPkScript)
if err != nil {
return fmt.Errorf("error parsing sponsor pkscript: %w", err)
}
var sponsorSigHashType txscript.SigHashType
switch sponsorType.Class() {
case txscript.WitnessV0PubKeyHashTy:
estimator.AddP2WKHInput()
sponsorSigHashType = txscript.SigHashAll
case txscript.WitnessV1TaprootTy:
sponsorSigHashType = txscript.SigHashDefault
estimator.AddTaprootKeySpendInput(sponsorSigHashType)
default:
return fmt.Errorf("unsupported sponsor input type: %v",
sponsorType.Class())
}
tx := wire.NewMsgTx(2)
packet, err := psbt.NewFromUnsignedTx(tx)
if err != nil {
return fmt.Errorf("error creating PSBT: %w", err)
}
// Let's add the sponsor input to the PSBT.
sponsorUtxo := &wire.TxOut{
Value: int64(sponsorTxOut.Value),
PkScript: sponsorPkScript,
}
packet.UnsignedTx.TxIn = append(packet.UnsignedTx.TxIn, &wire.TxIn{
PreviousOutPoint: *sponsorOutpoint,
Sequence: mempool.MaxRBFSequence,
})
packet.Inputs = append(packet.Inputs, psbt.PInput{
WitnessUtxo: sponsorUtxo,
SighashType: sponsorSigHashType,
})
targets, err := addAnchorInputs(
anchorAddrs, packet, api, &estimator, rootKey,
)
if err != nil {
return fmt.Errorf("error adding anchor inputs: %w", err)
}
// Now we can calculate the fee and add the change output.
anchorAmt := uint64(len(anchorAddrs)) * 330
totalOutputValue := btcutil.Amount(sponsorTxOut.Value + anchorAmt)
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
log.Infof("Fee %d sats of %d total amount (estimated weight %d)",
totalFee, totalOutputValue, estimator.Weight())
packet.UnsignedTx.TxOut = append(packet.UnsignedTx.TxOut, &wire.TxOut{
Value: int64(totalOutputValue - totalFee),
PkScript: changeScript,
})
packet.Outputs = append(packet.Outputs, psbt.POutput{})
prevOutFetcher := txscript.NewMultiPrevOutFetcher(
map[wire.OutPoint]*wire.TxOut{
*sponsorOutpoint: sponsorUtxo,
},
)
for idx := range targets {
prevOutFetcher.AddPrevOut(
targets[idx].outpoint, targets[idx].utxo,
)
}
// And now we sign the anchor inputs.
for idx := range targets {
target := targets[idx]
signDesc := &input.SignDescriptor{
KeyDesc: *target.keyDesc,
WitnessScript: target.script,
Output: target.utxo,
PrevOutputFetcher: prevOutFetcher,
InputIndex: idx + 1,
}
var anchorWitness wire.TxWitness
switch {
// Simple Taproot Channel:
case target.scriptTree != nil:
signDesc.SignMethod = input.TaprootKeySpendSignMethod
signDesc.HashType = txscript.SigHashDefault
signDesc.TapTweak = target.scriptTree.TapscriptRoot
anchorSig, err := signer.SignOutputRaw(
packet.UnsignedTx, signDesc,
)
if err != nil {
return fmt.Errorf("error signing anchor "+
"input: %w", err)
}
anchorWitness = wire.TxWitness{
anchorSig.Serialize(),
}
// Anchor Channel:
default:
signDesc.SignMethod = input.WitnessV0SignMethod
signDesc.HashType = txscript.SigHashAll
anchorSig, err := signer.SignOutputRaw(
packet.UnsignedTx, signDesc,
)
if err != nil {
return fmt.Errorf("error signing anchor "+
"input: %w", err)
}
anchorWitness = make(wire.TxWitness, 2)
anchorWitness[0] = append(
anchorSig.Serialize(),
byte(txscript.SigHashAll),
)
anchorWitness[1] = target.script
}
var witnessBuf bytes.Buffer
err = psbt.WriteTxWitness(&witnessBuf, anchorWitness)
if err != nil {
return fmt.Errorf("error serializing witness: %w", err)
}
packet.Inputs[idx+1].FinalScriptWitness = witnessBuf.Bytes()
}
packetBase64, err := packet.B64Encode()
if err != nil {
return fmt.Errorf("error encoding PSBT: %w", err)
}
log.Infof("Prepared PSBT follows, please now call\n" +
"'lncli wallet psbt finalize <psbt>' to finalize the\n" +
"transaction, then publish it manually or by using\n" +
"'lncli wallet publishtx <final_tx>':\n\n" + packetBase64 +
"\n")
return nil
}
func addAnchorInputs(anchorAddrs []string, packet *psbt.Packet,
api *btc.ExplorerAPI, estimator *input.TxWeightEstimator,
rootKey *hdkeychain.ExtendedKey) ([]targetAnchor, error) {
// Fetch the additional info we need for the anchor output as well.
results := make([]targetAnchor, len(anchorAddrs))
for idx, anchorAddr := range anchorAddrs {
anchorTx, anchorIndex, err := api.Outpoint(anchorAddr)
if err != nil {
return nil, fmt.Errorf("error fetching anchor "+
"outpoint: %w", err)
}
anchorTxHash, err := chainhash.NewHashFromStr(anchorTx.TXID)
if err != nil {
return nil, fmt.Errorf("error decoding anchor txid: %w",
err)
}
addr, err := btcutil.DecodeAddress(anchorAddr, chainParams)
if err != nil {
return nil, fmt.Errorf("error decoding address: %w",
err)
}
anchorPkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
return nil, fmt.Errorf("error creating pk script: %w",
err)
}
target := targetAnchor{
addr: anchorAddr,
utxo: &wire.TxOut{
Value: 330,
PkScript: anchorPkScript,
},
outpoint: wire.OutPoint{
Hash: *anchorTxHash,
Index: uint32(anchorIndex),
},
}
switch addr.(type) {
case *btcutil.AddressWitnessScriptHash:
estimator.AddWitnessInput(input.AnchorWitnessSize)
anchorKeyDesc, anchorWitnessScript, err := findAnchorKey(
rootKey, anchorPkScript,
)
if err != nil {
return nil, fmt.Errorf("could not find "+
"key for anchor address %v: %w",
anchorAddr, err)
}
target.keyDesc = anchorKeyDesc
target.script = anchorWitnessScript
case *btcutil.AddressTaproot:
estimator.AddTaprootKeySpendInput(
txscript.SigHashDefault,
)
anchorKeyDesc, scriptTree, err := findTaprootAnchorKey(
rootKey, anchorPkScript,
)
if err != nil {
return nil, fmt.Errorf("could not find "+
"key for anchor address %v: %w",
anchorAddr, err)
}
target.keyDesc = anchorKeyDesc
target.scriptTree = scriptTree
default:
return nil, fmt.Errorf("unsupported address type: %T",
addr)
}
log.Infof("Found multisig key %x for anchor pk script %x",
target.keyDesc.PubKey.SerializeCompressed(),
anchorPkScript)
packet.UnsignedTx.TxIn = append(
packet.UnsignedTx.TxIn, &wire.TxIn{
PreviousOutPoint: target.outpoint,
Sequence: mempool.MaxRBFSequence,
},
)
packet.Inputs = append(packet.Inputs, psbt.PInput{
WitnessUtxo: target.utxo,
WitnessScript: target.script,
})
results[idx] = target
}
return results, nil
}
func findAnchorKey(rootKey *hdkeychain.ExtendedKey,
targetScript []byte) (*keychain.KeyDescriptor, []byte, error) {
family := keychain.KeyFamilyMultiSig
localMultisig, err := lnd.DeriveChildren(rootKey, []uint32{
lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose),
lnd.HardenedKeyStart + chainParams.HDCoinType,
lnd.HardenedKeyStart + uint32(family),
0,
})
if err != nil {
return nil, nil, fmt.Errorf("could not derive local "+
"multisig key: %w", err)
}
// Loop through the local multisig keys to find the target anchor
// script.
for index := uint32(0); index < math.MaxInt16; index++ {
currentKey, err := localMultisig.DeriveNonStandard(index)
if err != nil {
return nil, nil, fmt.Errorf("error deriving child "+
"key: %w", err)
}
currentPubkey, err := currentKey.ECPubKey()
if err != nil {
return nil, nil, fmt.Errorf("error deriving public "+
"key: %w", err)
}
script, err := input.CommitScriptAnchor(currentPubkey)
if err != nil {
return nil, nil, fmt.Errorf("error deriving script: "+
"%w", err)
}
pkScript, err := input.WitnessScriptHash(script)
if err != nil {
return nil, nil, fmt.Errorf("error deriving script "+
"hash: %w", err)
}
if !bytes.Equal(pkScript, targetScript) {
continue
}
return &keychain.KeyDescriptor{
PubKey: currentPubkey,
KeyLocator: keychain.KeyLocator{
Family: family,
Index: index,
},
}, script, nil
}
return nil, nil, fmt.Errorf("no matching pubkeys found")
}
func findTaprootAnchorKey(rootKey *hdkeychain.ExtendedKey,
targetScript []byte) (*keychain.KeyDescriptor, *input.AnchorScriptTree,
error) {
family := keychain.KeyFamilyPaymentBase
localPayment, err := lnd.DeriveChildren(rootKey, []uint32{
lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose),
lnd.HardenedKeyStart + chainParams.HDCoinType,
lnd.HardenedKeyStart + uint32(family),
0,
})
if err != nil {
return nil, nil, fmt.Errorf("could not derive local "+
"multisig key: %w", err)
}
// Loop through the local multisig keys to find the target anchor
// script.
for index := uint32(0); index < math.MaxInt16; index++ {
currentKey, err := localPayment.DeriveNonStandard(index)
if err != nil {
return nil, nil, fmt.Errorf("error deriving child "+
"key: %w", err)
}
currentPubkey, err := currentKey.ECPubKey()
if err != nil {
return nil, nil, fmt.Errorf("error deriving public "+
"key: %w", err)
}
scriptTree, err := input.NewAnchorScriptTree(currentPubkey)
if err != nil {
return nil, nil, fmt.Errorf("error deriving taproot "+
"key: %w", err)
}
pkScript, err := input.PayToTaprootScript(scriptTree.TaprootKey)
if err != nil {
return nil, nil, fmt.Errorf("error deriving pk "+
"script: %w", err)
}
if !bytes.Equal(pkScript, targetScript) {
continue
}
return &keychain.KeyDescriptor{
PubKey: currentPubkey,
KeyLocator: keychain.KeyLocator{
Family: family,
Index: index,
},
}, scriptTree, nil
}
return nil, nil, fmt.Errorf("no matching pubkeys found")
}

@ -2,37 +2,47 @@ package main
import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"path/filepath"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/spf13/cobra"
)
var (
errSwapNotFound = fmt.Errorf("loop in swap not found")
)
type recoverLoopInCommand struct {
TxID string
Vout uint32
SwapHash string
SweepAddr string
FeeRate uint16
OutputAmt uint64
FeeRate uint32
StartKeyIndex int
NumTries int
APIURL string
Publish bool
LoopDbPath string
LoopDbDir string
SqliteFile string
rootKey *rootKey
cmd *cobra.Command
@ -48,7 +58,7 @@ func newRecoverLoopInCommand() *cobra.Command {
--txid abcdef01234... \
--vout 0 \
--swap_hash abcdef01234... \
--loop_db_path /path/to/loop.db \
--loop_db_dir /path/to/loop/db/dir \
--sweep_addr bc1pxxxxxxx \
--feerate 10`,
RunE: cc.Execute,
@ -66,14 +76,15 @@ func newRecoverLoopInCommand() *cobra.Command {
"swap",
)
cc.cmd.Flags().StringVar(
&cc.LoopDbPath, "loop_db_path", "", "path to the loop "+
"database file",
&cc.LoopDbDir, "loop_db_dir", "", "path to the loop "+
"database directory, where the loop.db file is located",
)
cc.cmd.Flags().StringVar(
&cc.SweepAddr, "sweep_addr", "", "address to recover "+
"the funds to",
&cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
"derive a new address from the seed automatically",
)
cc.cmd.Flags().Uint16Var(
cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", 0, "fee rate to "+
"use for the sweep transaction in sat/vByte",
)
@ -93,6 +104,14 @@ func newRecoverLoopInCommand() *cobra.Command {
&cc.Publish, "publish", false, "publish sweep TX to the chain "+
"API instead of just printing the TX",
)
cc.cmd.Flags().Uint64Var(
&cc.OutputAmt, "output_amt", 0, "amount of the output to sweep",
)
cc.cmd.Flags().StringVar(
&cc.SqliteFile, "sqlite_file", "", "optional path to the loop "+
"sqlite database file, if not specified, the default "+
"location will be loaded from --loop_db_dir",
)
cc.rootKey = newRootKey(cc.cmd, "deriving starting key")
@ -105,38 +124,99 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
return fmt.Errorf("error reading root key: %w", err)
}
api := &btc.ExplorerAPI{BaseURL: c.APIURL}
if c.TxID == "" {
return fmt.Errorf("txid is required")
}
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
if c.SwapHash == "" {
return fmt.Errorf("swap_hash is required")
}
// Try to fetch the swap from the database.
store, err := loopdb.NewBoltSwapStore(c.LoopDbPath, chainParams)
if err != nil {
return err
if c.LoopDbDir == "" {
return fmt.Errorf("loop_db_dir is required")
}
defer store.Close()
swaps, err := store.FetchLoopInSwaps()
err = lnd.CheckAddress(
c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
}
var loopIn *loopdb.LoopIn
for _, s := range swaps {
if s.Hash.String() == c.SwapHash {
loopIn = s
break
api := newExplorerAPI(c.APIURL)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
// Try to fetch the swap from the boltdb.
var (
store loopdb.SwapStore
loopIn *loopdb.LoopIn
)
// First check if a boltdb file exists.
if lnrpc.FileExists(filepath.Join(c.LoopDbDir, "loop.db")) {
store, err = loopdb.NewBoltSwapStore(c.LoopDbDir, chainParams)
if err != nil {
return err
}
defer store.Close()
loopIn, err = findLoopInSwap(ctx, store, c.SwapHash)
if err != nil && !errors.Is(err, errSwapNotFound) {
return err
}
}
// If the loopin is not found yet, try to fetch it from the sqlite db.
if loopIn == nil {
return fmt.Errorf("swap not found")
if c.SqliteFile == "" {
c.SqliteFile = filepath.Join(
c.LoopDbDir, "loop_sqlite.db",
)
}
sqliteDb, err := loopdb.NewSqliteStore(
&loopdb.SqliteConfig{
DatabaseFileName: c.SqliteFile,
SkipMigrations: true,
}, chainParams,
)
if err != nil {
return err
}
defer sqliteDb.Close()
loopIn, err = findLoopInSwap(ctx, sqliteDb, c.SwapHash)
if err != nil && !errors.Is(err, errSwapNotFound) {
return err
}
}
// If the loopin is still not found, return an error.
if loopIn == nil {
return errSwapNotFound
}
// If the swap is an external htlc, we require the output amount to be
// set, as a lot of failure cases steam from the output amount being
// wrong.
if loopIn.Contract.ExternalHtlc && c.OutputAmt == 0 {
return fmt.Errorf("output_amt is required for external htlc")
}
fmt.Println("Loop expires at block height", loopIn.Contract.CltvExpiry)
outputValue := loopIn.Contract.AmountRequested
if c.OutputAmt != 0 {
outputValue = btcutil.Amount(c.OutputAmt)
}
// Get the swaps htlc.
htlc, err := loop.GetHtlc(
loopIn.Hash, &loopIn.Contract.SwapContract, chainParams,
@ -146,30 +226,23 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
}
// Get the destination address.
sweepAddr, err := btcutil.DecodeAddress(c.SweepAddr, chainParams)
var estimator input.TxWeightEstimator
sweepScript, err := lnd.PrepareWalletAddress(
c.SweepAddr, chainParams, &estimator, extendedKey, "sweep",
)
if err != nil {
return err
}
// Calculate the sweep fee.
estimator := &input.TxWeightEstimator{}
err = htlc.AddTimeoutToEstimator(estimator)
err = htlc.AddTimeoutToEstimator(&estimator)
if err != nil {
return err
}
switch sweepAddr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
estimator.AddP2WKHOutput()
case *btcutil.AddressTaproot:
estimator.AddP2TROutput()
default:
return fmt.Errorf("unsupported address type")
}
feeRateKWeight := chainfee.SatPerKVByte(1000 * c.FeeRate).FeePerKWeight()
feeRateKWeight := chainfee.SatPerKVByte(
1000 * c.FeeRate,
).FeePerKWeight()
fee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
txID, err := chainhash.NewHashFromStr(c.TxID)
@ -195,14 +268,9 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
})
// Add output for the destination address.
sweepPkScript, err := txscript.PayToAddrScript(sweepAddr)
if err != nil {
return err
}
sweepTx.AddTxOut(&wire.TxOut{
PkScript: sweepPkScript,
Value: int64(loopIn.Contract.AmountRequested) - int64(fee),
PkScript: sweepScript,
Value: int64(outputValue) - int64(fee),
})
// If the htlc is version 2, we need to brute force the key locator, as
@ -212,8 +280,9 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
fmt.Println("Brute forcing key index...")
for i := c.StartKeyIndex; i < c.StartKeyIndex+c.NumTries; i++ {
rawTx, err = getSignedTx(
signer, loopIn, sweepTx, htlc,
signer, sweepTx, htlc,
keychain.KeyFamily(swap.KeyFamily), uint32(i),
outputValue,
)
if err == nil {
break
@ -221,13 +290,15 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
}
if rawTx == nil {
return fmt.Errorf("failed to brute force key index, " +
"please try again with a higher start key index")
"please try again with a higher start key " +
"index")
}
} else {
rawTx, err = getSignedTx(
signer, loopIn, sweepTx, htlc,
signer, sweepTx, htlc,
loopIn.Contract.HtlcKeys.ClientScriptKeyLocator.Family,
loopIn.Contract.HtlcKeys.ClientScriptKeyLocator.Index,
outputValue,
)
if err != nil {
return err
@ -245,22 +316,22 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
log.Infof("Published TX %s, response: %s",
sweepTx.TxHash().String(), response)
} else {
fmt.Printf("Success, we successfully created the sweep transaction. "+
"Please publish this using any bitcoin node:\n\n%x\n\n",
rawTx)
fmt.Printf("Success, we successfully created the sweep "+
"transaction. Please publish this using any bitcoin "+
"node:\n\n%x\n\n", rawTx)
}
return nil
}
func getSignedTx(signer *lnd.Signer, loopIn *loopdb.LoopIn, sweepTx *wire.MsgTx,
htlc *swap.Htlc, keyFamily keychain.KeyFamily,
keyIndex uint32) ([]byte, error) {
func getSignedTx(signer *lnd.Signer, sweepTx *wire.MsgTx, htlc *swap.Htlc,
keyFamily keychain.KeyFamily, keyIndex uint32,
outputValue btcutil.Amount) ([]byte, error) {
// Create the sign descriptor.
prevTxOut := &wire.TxOut{
PkScript: htlc.PkScript,
Value: int64(loopIn.Contract.AmountRequested),
Value: int64(outputValue),
}
prevOutputFetcher := txscript.NewCannedPrevOutputFetcher(
prevTxOut.PkScript, prevTxOut.Value,
@ -324,6 +395,23 @@ func getSignedTx(signer *lnd.Signer, loopIn *loopdb.LoopIn, sweepTx *wire.MsgTx,
return rawTx, nil
}
func findLoopInSwap(ctx context.Context, store loopdb.SwapStore,
swapHash string) (*loopdb.LoopIn, error) {
swaps, err := store.FetchLoopInSwaps(ctx)
if err != nil {
return nil, err
}
for _, s := range swaps {
if s.Hash.String() == swapHash {
return s, nil
}
}
return nil, errSwapNotFound
}
// encodeTx encodes a tx to raw bytes.
func encodeTx(tx *wire.MsgTx) ([]byte, error) {
var buffer bytes.Buffer

@ -7,7 +7,7 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/spf13/cobra"
)
@ -31,7 +31,7 @@ channel was never confirmed on chain!
CAUTION: Running this command will make it impossible to use the channel DB
with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.16.0-beta or later after using this command!`,
run lnd ` + lndVersion + ` or later after using this command!`,
Example: `chantools removechannel \
--channeldb ~/.lnd/data/graph/mainnet/channel.db \
--channel 3149764effbe82718b280de425277e5e7b245a4573aa4a0203ac12cee1c37816:0`,

@ -13,8 +13,8 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/guggero/chantools/dataformat"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/dataformat"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
@ -60,6 +60,10 @@ 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.
NOTE: Unless your channel was opened before 2019, you very likely don't need to
use this command as things were simplified. Use 'chantools sweepremoteclosed'
instead if the remote party has already closed the channel.
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 (see
https://github.com/guggero/lnd/releases for a binary release) in combination
@ -88,7 +92,8 @@ chantools rescueclosed --fromsummary results/summary-xxxxxx.json \
)
cc.cmd.Flags().StringVar(
&cc.Addr, "force_close_addr", "", "the address the channel "+
"was force closed to",
"was force closed to, look up in block explorer by "+
"following funding txid",
)
cc.cmd.Flags().StringVar(
&cc.CommitPoint, "commit_point", "", "the commit point that "+

@ -9,8 +9,7 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/wire"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
@ -45,7 +44,7 @@ type rescueFundingCommand struct {
RemotePubKey string
SweepAddr string
FeeRate uint16
FeeRate uint32
APIURL string
rootKey *rootKey
@ -113,9 +112,11 @@ chantools rescuefunding \
"specified manually",
)
cc.cmd.Flags().StringVar(
&cc.SweepAddr, "sweepaddr", "", "address to sweep the funds to",
&cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
"derive a new address from the seed automatically",
)
cc.cmd.Flags().Uint16Var(
cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
"use for the sweep transaction in sat/vByte",
)
@ -208,7 +209,7 @@ func (c *rescueFundingCommand) Execute(_ *cobra.Command, _ []string) error {
Index: c.LocalKeyIndex,
},
}
privKey, err := signer.FetchPrivKey(localKeyDesc)
privKey, err := signer.FetchPrivateKey(localKeyDesc)
if err != nil {
return fmt.Errorf("error deriving local key: %w", err)
}
@ -227,35 +228,46 @@ func (c *rescueFundingCommand) Execute(_ *cobra.Command, _ []string) error {
}
}
// Make sure the sweep addr is a P2WKH address so we can do accurate
// fee estimation.
sweepScript, err := lnd.GetP2WPKHScript(c.SweepAddr, chainParams)
err = lnd.CheckAddress(
c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return fmt.Errorf("error parsing sweep addr: %w", err)
return err
}
return rescueFunding(
localKeyDesc, remotePubKey, signer, chainOp,
sweepScript, btcutil.Amount(c.FeeRate), c.APIURL,
localKeyDesc, remotePubKey, signer, chainOp, c.SweepAddr,
btcutil.Amount(c.FeeRate), c.APIURL,
)
}
func rescueFunding(localKeyDesc *keychain.KeyDescriptor,
remoteKey *btcec.PublicKey, signer *lnd.Signer,
chainPoint *wire.OutPoint, sweepPKScript []byte, feeRate btcutil.Amount,
chainPoint *wire.OutPoint, sweepAddr string, feeRate btcutil.Amount,
apiURL string) error {
var (
estimator input.TxWeightEstimator
api = newExplorerAPI(apiURL)
)
sweepScript, err := lnd.PrepareWalletAddress(
sweepAddr, chainParams, &estimator, signer.ExtendedKey, "sweep",
)
if err != nil {
return err
}
// Prepare the wire part of the PSBT.
txIn := &wire.TxIn{
PreviousOutPoint: *chainPoint,
Sequence: 0,
}
txOut := &wire.TxOut{
PkScript: sweepPKScript,
PkScript: sweepScript,
}
// Locate the output in the funding TX.
api := &btc.ExplorerAPI{BaseURL: apiURL}
tx, err := api.Transaction(chainPoint.Hash.String())
if err != nil {
return fmt.Errorf("error fetching UTXO info for outpoint %s: "+
@ -294,17 +306,15 @@ func rescueFunding(localKeyDesc *keychain.KeyDescriptor,
WitnessScript: witnessScript,
Unknowns: []*psbt.Unknown{{
// We add the public key the other party needs to sign
// with as a proprietary field so we can easily read it
// with as a proprietary field, so we can easily read it
// out with the signrescuefunding command.
Key: PsbtKeyTypeOutputMissingSigPubkey,
Value: remoteKey.SerializeCompressed(),
}},
}
// Estimate the transaction weight so we can do the fee estimation.
var estimator input.TxWeightEstimator
// Estimate the transaction weight, so we can do the fee estimation.
estimator.AddWitnessInput(MultiSigWitnessSize)
estimator.AddP2WKHOutput()
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
txOut.Value = utxo.Value - int64(totalFee)

@ -10,7 +10,7 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/spf13/cobra"
)

@ -1,34 +1,41 @@
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"syscall"
"time"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btclog"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/dataformat"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/dataformat"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/peer"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
)
const (
defaultAPIURL = "https://blockstream.info/api"
version = "0.11.2"
na = "n/a"
defaultAPIURL = "https://blockstream.info/api"
defaultTestnetAPIURL = "https://blockstream.info/testnet/api"
defaultRegtestAPIURL = "http://localhost:3004"
// version is the current version of the tool. It is set during build.
// NOTE: When changing this, please also update the version in the
// download link shown in the README.
version = "0.13.1"
na = "n/a"
// lndVersion is the current version of lnd that we support. This is
// shown in some commands that affect the database and its migrations.
lndVersion = "v0.17.4-beta"
Commit = ""
)
@ -36,6 +43,7 @@ const (
var (
Testnet bool
Regtest bool
Signet bool
logWriter = build.NewRotatingLogWriter()
log = build.NewSubLogger("CHAN", genSubLogger(logWriter))
@ -47,7 +55,8 @@ var rootCmd = &cobra.Command{
Short: "Chantools helps recover funds from lightning channels",
Long: `This tool provides helper functions that can be used rescue
funds locked in lnd channels in case lnd itself cannot run properly anymore.
Complete documentation is available at https://github.com/guggero/chantools/.`,
Complete documentation is available at
https://github.com/lightninglabs/chantools/.`,
Version: fmt.Sprintf("v%s, commit %s", version, Commit),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
switch {
@ -57,6 +66,9 @@ Complete documentation is available at https://github.com/guggero/chantools/.`,
case Regtest:
chainParams = &chaincfg.RegressionNetParams
case Signet:
chainParams = &chaincfg.SigNetParams
default:
chainParams = &chaincfg.MainNetParams
}
@ -78,14 +90,21 @@ func main() {
&Regtest, "regtest", "r", false, "Indicates if regtest "+
"parameters should be used",
)
rootCmd.PersistentFlags().BoolVarP(
&Signet, "signet", "s", false, "Indicates if the public "+
"signet parameters should be used",
)
rootCmd.AddCommand(
newChanBackupCommand(),
newClosePoolAccountCommand(),
newCreateWalletCommand(),
newCompactDBCommand(),
newDeletePaymentsCommand(),
newDeriveKeyCommand(),
newDoubleSpendInputsCommand(),
newDropChannelGraphCommand(),
newDropGraphZombiesCommand(),
newDumpBackupCommand(),
newDumpChannelsCommand(),
newDocCommand(),
@ -95,13 +114,16 @@ func main() {
newForceCloseCommand(),
newGenImportScriptCommand(),
newMigrateDBCommand(),
newPullAnchorCommand(),
newRecoverLoopInCommand(),
newRemoveChannelCommand(),
newRescueClosedCommand(),
newRescueFundingCommand(),
newRescueTweakedKeyCommand(),
newShowRootKeyCommand(),
newSignMessageCommand(),
newSignRescueFundingCommand(),
newSignPSBTCommand(),
newSummaryCommand(),
newSweepTimeLockCommand(),
newSweepTimeLockManualCommand(),
@ -119,8 +141,9 @@ func main() {
}
type rootKey struct {
RootKey string
BIP39 bool
RootKey string
BIP39 bool
WalletDB string
}
func newRootKey(cmd *cobra.Command, desc string) *rootKey {
@ -135,6 +158,12 @@ func newRootKey(cmd *cobra.Command, desc string) *rootKey {
"passphrase from the terminal instead of asking for "+
"lnd seed format or providing the --rootkey flag",
)
cmd.Flags().StringVar(
&r.WalletDB, "walletdb", "", "read the seed/master root key "+
"to use fro "+desc+" from an lnd wallet.db file "+
"instead of asking for a seed or providing the "+
"--rootkey flag",
)
return r
}
@ -157,6 +186,39 @@ func (r *rootKey) readWithBirthday() (*hdkeychain.ExtendedKey, time.Time,
extendedKey, err := btc.ReadMnemonicFromTerminal(chainParams)
return extendedKey, time.Unix(0, 0), err
case r.WalletDB != "":
wallet, pw, cleanup, err := lnd.OpenWallet(
r.WalletDB, chainParams,
)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("error "+
"opening wallet '%s': %w", r.WalletDB, err)
}
defer func() {
if err := cleanup(); err != nil {
log.Errorf("error closing wallet: %v", err)
}
}()
extendedKeyBytes, err := lnd.DecryptWalletRootKey(
wallet.Database(), pw,
)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("error "+
"decrypting wallet root key: %w", err)
}
extendedKey, err := hdkeychain.NewKeyFromString(
string(extendedKeyBytes),
)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("error "+
"parsing master key: %w", err)
}
return extendedKey, wallet.Manager.Birthday(), nil
default:
return lnd.ReadAezeed(chainParams)
}
@ -241,27 +303,6 @@ func readInput(input string) ([]byte, error) {
return ioutil.ReadFile(input)
}
func passwordFromConsole(userQuery string) ([]byte, error) {
// Read from terminal (if there is one).
if terminal.IsTerminal(int(syscall.Stdin)) { //nolint
fmt.Print(userQuery)
pw, err := terminal.ReadPassword(int(syscall.Stdin)) //nolint
if err != nil {
return nil, err
}
fmt.Println()
return pw, nil
}
// Read from stdin as a fallback.
reader := bufio.NewReader(os.Stdin)
pw, err := reader.ReadBytes('\n')
if err != nil {
return nil, err
}
return pw, nil
}
func setupLogging() {
setSubLogger("CHAN", log)
addSubLogger("CHDB", channeldb.UseLogger)
@ -304,6 +345,21 @@ func setSubLogger(subsystem string, logger btclog.Logger,
}
}
func noConsole() ([]byte, error) {
return nil, fmt.Errorf("wallet db requires console access")
func newExplorerAPI(apiURL string) *btc.ExplorerAPI {
// Override for testnet if default is used.
if apiURL == defaultAPIURL &&
chainParams.Name == chaincfg.TestNet3Params.Name {
return &btc.ExplorerAPI{BaseURL: defaultTestnetAPIURL}
}
// Also override for regtest if default is used.
if apiURL == defaultAPIURL &&
chainParams.Name == chaincfg.RegressionNetParams.Name {
return &btc.ExplorerAPI{BaseURL: defaultRegtestAPIURL}
}
// Otherwise use the provided URL.
return &btc.ExplorerAPI{BaseURL: apiURL}
}

@ -3,8 +3,8 @@ package main
import (
"testing"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd"
"github.com/stretchr/testify/require"
)

@ -0,0 +1,90 @@
package main
import (
"fmt"
chantools_lnd "github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/keychain"
"github.com/spf13/cobra"
"github.com/tv42/zbase32"
)
var (
signedMsgPrefix = []byte("Lightning Signed Message:")
)
type signMessageCommand struct {
Msg string
rootKey *rootKey
cmd *cobra.Command
}
func newSignMessageCommand() *cobra.Command {
cc := &signMessageCommand{}
cc.cmd = &cobra.Command{
Use: "signmessage",
Short: "Sign a message with the node's private key.",
Long: `Sign msg with the resident node's private key.
Returns the signature as a zbase32 string.`,
Example: `chantools signmessage --msg=foobar`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.Msg, "msg", "", "the message to sign",
)
cc.rootKey = newRootKey(cc.cmd, "decrypting the backup")
return cc.cmd
}
func (c *signMessageCommand) Execute(_ *cobra.Command, _ []string) error {
if c.Msg == "" {
return fmt.Errorf("please enter a valid msg")
}
extendedKey, err := c.rootKey.read()
if err != nil {
return fmt.Errorf("error reading root key: %w", err)
}
signer := &chantools_lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
// Create the key locator for the node key.
keyLocator := keychain.KeyLocator{
Family: keychain.KeyFamilyNodeKey,
Index: 0,
}
// Fetch the private key for node key.
privKey, err := signer.FetchPrivateKey(&keychain.KeyDescriptor{
KeyLocator: keyLocator,
})
if err != nil {
return err
}
// Create a new signer.
privKeyMsgSigner := keychain.NewPrivKeyMessageSigner(
privKey, keyLocator,
)
// Prepend the special lnd prefix.
// See: https://github.com/lightningnetwork/lnd/blob/63e698ec4990e678089533561fd95cfd684b67db/rpcserver.go#L1576 .
msg := []byte(c.Msg)
msg = append(signedMsgPrefix, msg...)
sigBytes, err := privKeyMsgSigner.SignMessageCompact(msg, true)
if err != nil {
return err
}
// Encode the signature.
sig := zbase32.EncodeToString(sigBytes)
fmt.Println(sig)
return nil
}

@ -0,0 +1,226 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/binary"
"fmt"
"os"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/lightninglabs/chantools/lnd"
"github.com/spf13/cobra"
)
type signPSBTCommand struct {
Psbt string
FromRawPsbtFile string
ToRawPsbtFile string
rootKey *rootKey
cmd *cobra.Command
}
func newSignPSBTCommand() *cobra.Command {
cc := &signPSBTCommand{}
cc.cmd = &cobra.Command{
Use: "signpsbt",
Short: "Sign a Partially Signed Bitcoin Transaction (PSBT)",
Long: `Sign a PSBT with a master root key. The PSBT must contain
an input that is owned by the master root key.`,
Example: `chantools signpsbt \
--psbt <the_base64_encoded_psbt>
chantools signpsbt --fromrawpsbtfile <file_with_psbt>`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.Psbt, "psbt", "", "Partially Signed Bitcoin Transaction "+
"to sign",
)
cc.cmd.Flags().StringVar(
&cc.FromRawPsbtFile, "fromrawpsbtfile", "", "the file containing "+
"the raw, binary encoded PSBT packet to sign",
)
cc.cmd.Flags().StringVar(
&cc.ToRawPsbtFile, "torawpsbtfile", "", "the file to write "+
"the resulting signed raw, binary encoded PSBT packet "+
"to",
)
cc.rootKey = newRootKey(cc.cmd, "signing the PSBT")
return cc.cmd
}
func (c *signPSBTCommand) Execute(_ *cobra.Command, _ []string) error {
extendedKey, err := c.rootKey.read()
if err != nil {
return fmt.Errorf("error reading root key: %w", err)
}
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
var packet *psbt.Packet
// Decode the PSBT, either from the command line or the binary file.
switch {
case c.Psbt != "":
packet, err = psbt.NewFromRawBytes(
bytes.NewReader([]byte(c.Psbt)), true,
)
if err != nil {
return fmt.Errorf("error decoding PSBT: %w", err)
}
case c.FromRawPsbtFile != "":
f, err := os.Open(c.FromRawPsbtFile)
if err != nil {
return fmt.Errorf("error opening PSBT file '%s': %w",
c.FromRawPsbtFile, err)
}
packet, err = psbt.NewFromRawBytes(f, false)
if err != nil {
return fmt.Errorf("error decoding PSBT from file "+
"'%s': %w", c.FromRawPsbtFile, err)
}
default:
return fmt.Errorf("either the PSBT or the raw PSBT file " +
"must be set")
}
err = signPsbt(extendedKey, packet, signer)
if err != nil {
return fmt.Errorf("error signing PSBT: %w", err)
}
switch {
case c.ToRawPsbtFile != "":
f, err := os.Create(c.ToRawPsbtFile)
if err != nil {
return fmt.Errorf("error creating PSBT file '%s': %w",
c.ToRawPsbtFile, err)
}
if err := packet.Serialize(f); err != nil {
return fmt.Errorf("error serializing PSBT to file "+
"'%s': %w", c.ToRawPsbtFile, err)
}
fmt.Printf("Successfully signed PSBT and wrote it to file "+
"'%s'\n", c.ToRawPsbtFile)
default:
var buf bytes.Buffer
if err := packet.Serialize(&buf); err != nil {
return fmt.Errorf("error serializing PSBT: %w", err)
}
fmt.Printf("Successfully signed PSBT:\n\n%s\n",
base64.StdEncoding.EncodeToString(buf.Bytes()))
}
return nil
}
func signPsbt(rootKey *hdkeychain.ExtendedKey,
packet *psbt.Packet, signer *lnd.Signer) error {
// Check that we have an input with a derivation path that belongs to
// the root key.
derivationPath, inputIndex, err := findMatchingDerivationPath(
rootKey, packet,
)
if err != nil {
return fmt.Errorf("could not find matching derivation path: %w",
err)
}
if len(derivationPath) < 5 {
return fmt.Errorf("invalid derivation path, expected at least "+
"5 elements, got %d", len(derivationPath))
}
localKey, err := lnd.DeriveChildren(rootKey, derivationPath)
if err != nil {
return fmt.Errorf("could not derive local key: %w", err)
}
if packet.Inputs[inputIndex].WitnessUtxo == nil {
return fmt.Errorf("invalid PSBT, input %d is missing witness "+
"UTXO", inputIndex)
}
utxo := packet.Inputs[inputIndex].WitnessUtxo
// The signing is a bit different for P2WPKH, we need to specify the
// pk script as the witness script.
var witnessScript []byte
if txscript.IsPayToWitnessPubKeyHash(utxo.PkScript) {
witnessScript = utxo.PkScript
} else {
if len(packet.Inputs[inputIndex].WitnessScript) == 0 {
return fmt.Errorf("invalid PSBT, input %d is missing "+
"witness script", inputIndex)
}
witnessScript = packet.Inputs[inputIndex].WitnessScript
}
localPrivateKey, err := localKey.ECPrivKey()
if err != nil {
return fmt.Errorf("error getting private key: %w", err)
}
err = signer.AddPartialSignatureForPrivateKey(
packet, localPrivateKey, utxo, witnessScript, inputIndex,
)
if err != nil {
return fmt.Errorf("error adding partial signature: %w", err)
}
return nil
}
func findMatchingDerivationPath(rootKey *hdkeychain.ExtendedKey,
packet *psbt.Packet) ([]uint32, int, error) {
pubKey, err := rootKey.ECPubKey()
if err != nil {
return nil, 0, fmt.Errorf("error getting public key: %w", err)
}
pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
fingerprint := binary.LittleEndian.Uint32(pubKeyHash[:4])
for idx, input := range packet.Inputs {
if len(input.Bip32Derivation) == 0 {
continue
}
for _, derivation := range input.Bip32Derivation {
// A special case where there is only a single
// derivation path and the master key fingerprint is not
// set, we assume we are the correct signer... This
// might not be correct, but we have no way of knowing.
if derivation.MasterKeyFingerprint == 0 &&
len(input.Bip32Derivation) == 1 {
return derivation.Bip32Path, idx, nil
}
// The normal case, where a derivation path has the
// master fingerprint set.
if derivation.MasterKeyFingerprint == fingerprint {
return derivation.Bip32Path, idx, nil
}
}
}
return nil, 0, fmt.Errorf("no matching derivation path found")
}

@ -7,7 +7,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/keychain"
"github.com/spf13/cobra"
)

@ -6,8 +6,8 @@ import (
"io/ioutil"
"time"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/dataformat"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/dataformat"
"github.com/spf13/cobra"
)
@ -53,7 +53,8 @@ func (c *summaryCommand) Execute(_ *cobra.Command, _ []string) error {
func summarizeChannels(apiURL string,
channels []*dataformat.SummaryEntry) error {
summaryFile, err := btc.SummarizeChannels(apiURL, channels, log)
api := newExplorerAPI(apiURL)
summaryFile, err := btc.SummarizeChannels(api, channels, log)
if err != nil {
return fmt.Errorf("error running summary: %w", err)
}

@ -11,8 +11,8 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
@ -29,7 +29,7 @@ type sweepRemoteClosedCommand struct {
APIURL string
Publish bool
SweepAddr string
FeeRate uint16
FeeRate uint32
rootKey *rootKey
cmd *cobra.Command
@ -53,6 +53,7 @@ funds can be swept after the force-close transaction was confirmed.
Supported remote force-closed channel types are:
- STATIC_REMOTE_KEY (a.k.a. tweakless channels)
- ANCHOR (a.k.a. anchor output channels)
- SIMPLE_TAPROOT (a.k.a. simple taproot channels)
`,
Example: `chantools sweepremoteclosed \
--recoverywindow 300 \
@ -75,9 +76,11 @@ Supported remote force-closed channel types are:
"API instead of just printing the TX",
)
cc.cmd.Flags().StringVar(
&cc.SweepAddr, "sweepaddr", "", "address to sweep the funds to",
&cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
"derive a new address from the seed automatically",
)
cc.cmd.Flags().Uint16Var(
cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
"use for the sweep transaction in sat/vByte",
)
@ -94,8 +97,12 @@ func (c *sweepRemoteClosedCommand) Execute(_ *cobra.Command, _ []string) error {
}
// Make sure sweep addr is set.
if c.SweepAddr == "" {
return fmt.Errorf("sweep addr is required")
err = lnd.CheckAddress(
c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
}
// Set default values.
@ -113,21 +120,30 @@ func (c *sweepRemoteClosedCommand) Execute(_ *cobra.Command, _ []string) error {
}
type targetAddr struct {
addr btcutil.Address
pubKey *btcec.PublicKey
path string
keyDesc *keychain.KeyDescriptor
vouts []*btc.Vout
script []byte
addr btcutil.Address
pubKey *btcec.PublicKey
path string
keyDesc *keychain.KeyDescriptor
vouts []*btc.Vout
script []byte
scriptTree *input.CommitScriptTree
}
func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
sweepAddr string, recoveryWindow uint32, feeRate uint16,
sweepAddr string, recoveryWindow uint32, feeRate uint32,
publish bool) error {
var estimator input.TxWeightEstimator
sweepScript, err := lnd.PrepareWalletAddress(
sweepAddr, chainParams, &estimator, extendedKey, "sweep",
)
if err != nil {
return err
}
var (
targets []*targetAddr
api = &btc.ExplorerAPI{BaseURL: apiURL}
api = newExplorerAPI(apiURL)
)
for index := uint32(0); index < recoveryWindow; index++ {
path := fmt.Sprintf("m/1017'/%d'/%d'/0/%d",
@ -169,7 +185,6 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
// Create estimator and transaction template.
var (
estimator input.TxWeightEstimator
signDescs []*input.SignDescriptor
sweepTx = wire.NewMsgTx(2)
totalOutputValue = uint64(0)
@ -196,18 +211,6 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
err)
}
sequence := wire.MaxTxInSequenceNum
switch target.addr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
estimator.AddP2WKHInput()
case *btcutil.AddressWitnessScriptHash:
estimator.AddWitnessInput(
input.ToRemoteConfirmedWitnessSize,
)
sequence = 1
}
prevOutPoint := wire.OutPoint{
Hash: *txHash,
Index: uint32(vout.Outspend.Vin),
@ -217,18 +220,76 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
Value: int64(vout.Value),
}
prevOutFetcher.AddPrevOut(prevOutPoint, prevTxOut)
sweepTx.TxIn = append(sweepTx.TxIn, &wire.TxIn{
txIn := &wire.TxIn{
PreviousOutPoint: prevOutPoint,
Sequence: sequence,
})
Sequence: wire.MaxTxInSequenceNum,
}
sweepTx.TxIn = append(sweepTx.TxIn, txIn)
inputIndex := len(sweepTx.TxIn) - 1
signDescs = append(signDescs, &input.SignDescriptor{
KeyDesc: *target.keyDesc,
WitnessScript: target.script,
Output: prevTxOut,
HashType: txscript.SigHashAll,
PrevOutputFetcher: prevOutFetcher,
})
var signDesc *input.SignDescriptor
switch target.addr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
estimator.AddP2WKHInput()
signDesc = &input.SignDescriptor{
KeyDesc: *target.keyDesc,
WitnessScript: target.script,
Output: prevTxOut,
HashType: txscript.SigHashAll,
PrevOutputFetcher: prevOutFetcher,
InputIndex: inputIndex,
}
case *btcutil.AddressWitnessScriptHash:
estimator.AddWitnessInput(
input.ToRemoteConfirmedWitnessSize,
)
txIn.Sequence = 1
signDesc = &input.SignDescriptor{
KeyDesc: *target.keyDesc,
WitnessScript: target.script,
Output: prevTxOut,
HashType: txscript.SigHashAll,
PrevOutputFetcher: prevOutFetcher,
InputIndex: inputIndex,
}
case *btcutil.AddressTaproot:
estimator.AddWitnessInput(
input.TaprootToRemoteWitnessSize,
)
txIn.Sequence = 1
tree := target.scriptTree
controlBlock, err := tree.CtrlBlockForPath(
input.ScriptPathSuccess,
)
if err != nil {
return err
}
controlBlockBytes, err := controlBlock.ToBytes()
if err != nil {
return err
}
script := tree.SettleLeaf.Script
signMethod := input.TaprootScriptSpendSignMethod
signDesc = &input.SignDescriptor{
KeyDesc: *target.keyDesc,
WitnessScript: script,
Output: prevTxOut,
HashType: txscript.SigHashDefault,
PrevOutputFetcher: prevOutFetcher,
ControlBlock: controlBlockBytes,
InputIndex: inputIndex,
SignMethod: signMethod,
TapTweak: tree.TapscriptRoot,
}
}
signDescs = append(signDescs, signDesc)
}
}
@ -238,13 +299,6 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
len(targets), totalOutputValue, sweepDustLimit)
}
// Add our sweep destination output.
sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams)
if err != nil {
return err
}
estimator.AddP2WKHOutput()
// Calculate the fee based on the given fee rate and our weight
// estimation.
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
@ -270,7 +324,19 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
desc.SigHashes = sigHashes
desc.InputIndex = idx
if len(desc.WitnessScript) > 0 {
switch {
// Simple Taproot Channels.
case desc.SignMethod == input.TaprootScriptSpendSignMethod:
witness, err := input.TaprootCommitSpendSuccess(
signer, desc, sweepTx, nil,
)
if err != nil {
return err
}
sweepTx.TxIn[idx].Witness = witness
// Anchor Channels.
case len(desc.WitnessScript) > 0:
witness, err := input.CommitSpendToRemoteConfirmed(
signer, desc, sweepTx,
)
@ -278,7 +344,9 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
return err
}
sweepTx.TxIn[idx].Witness = witness
} else {
// Static Remote Key Channels.
default:
// The txscript library expects the witness script of a
// P2WKH descriptor to be set to the pkScript of the
// output...
@ -320,7 +388,9 @@ func queryAddressBalances(pubKey *btcec.PublicKey, path string,
error) {
var targets []*targetAddr
queryAddr := func(address btcutil.Address, script []byte) error {
queryAddr := func(address btcutil.Address, script []byte,
scriptTree *input.CommitScriptTree) error {
unspent, err := api.Unspent(address.EncodeAddress())
if err != nil {
return fmt.Errorf("could not query unspent: %w", err)
@ -330,12 +400,13 @@ func queryAddressBalances(pubKey *btcec.PublicKey, path string,
log.Infof("Found %d unspent outputs for address %v",
len(unspent), address.EncodeAddress())
targets = append(targets, &targetAddr{
addr: address,
pubKey: pubKey,
path: path,
keyDesc: keyDesc,
vouts: unspent,
script: script,
addr: address,
pubKey: pubKey,
path: path,
keyDesc: keyDesc,
vouts: unspent,
script: script,
scriptTree: scriptTree,
})
}
@ -346,7 +417,7 @@ func queryAddressBalances(pubKey *btcec.PublicKey, path string,
if err != nil {
return nil, err
}
if err := queryAddr(p2wkh, nil); err != nil {
if err := queryAddr(p2wkh, nil, nil); err != nil {
return nil, err
}
@ -354,7 +425,15 @@ func queryAddressBalances(pubKey *btcec.PublicKey, path string,
if err != nil {
return nil, err
}
if err := queryAddr(p2anchor, script); err != nil {
if err := queryAddr(p2anchor, script, nil); err != nil {
return nil, err
}
p2tr, scriptTree, err := lnd.P2TaprootStaticRemote(pubKey, chainParams)
if err != nil {
return nil, err
}
if err := queryAddr(p2tr, nil, scriptTree); err != nil {
return nil, err
}

@ -10,9 +10,8 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/dataformat"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/dataformat"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
@ -29,7 +28,7 @@ type sweepTimeLockCommand struct {
Publish bool
SweepAddr string
MaxCsvLimit uint16
FeeRate uint16
FeeRate uint32
rootKey *rootKey
inputs *inputFlags
@ -65,13 +64,15 @@ parameter to 144.`,
"API instead of just printing the TX",
)
cc.cmd.Flags().StringVar(
&cc.SweepAddr, "sweepaddr", "", "address to sweep the funds to",
&cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
"derive a new address from the seed automatically",
)
cc.cmd.Flags().Uint16Var(
&cc.MaxCsvLimit, "maxcsvlimit", defaultCsvLimit, "maximum CSV "+
"limit to use",
)
cc.cmd.Flags().Uint16Var(
cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
"use for the sweep transaction in sat/vByte",
)
@ -89,8 +90,12 @@ func (c *sweepTimeLockCommand) Execute(_ *cobra.Command, _ []string) error {
}
// Make sure sweep addr is set.
if c.SweepAddr == "" {
return fmt.Errorf("sweep addr is required")
err = lnd.CheckAddress(
c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
}
// Parse channel entries from any of the possible input files.
@ -125,7 +130,7 @@ type sweepTarget struct {
func sweepTimeLockFromSummary(extendedKey *hdkeychain.ExtendedKey, apiURL string,
entries []*dataformat.SummaryEntry, sweepAddr string,
maxCsvTimeout uint16, publish bool, feeRate uint16) error {
maxCsvTimeout uint16, publish bool, feeRate uint32) error {
targets := make([]*sweepTarget, 0, len(entries))
for _, entry := range entries {
@ -213,21 +218,29 @@ func sweepTimeLockFromSummary(extendedKey *hdkeychain.ExtendedKey, apiURL string
func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
targets []*sweepTarget, sweepAddr string, maxCsvTimeout uint16,
publish bool, feeRate uint16) error {
publish bool, feeRate uint32) error {
// Create signer and transaction template.
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
var (
estimator input.TxWeightEstimator
signer = &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
api = newExplorerAPI(apiURL)
)
sweepScript, err := lnd.PrepareWalletAddress(
sweepAddr, chainParams, &estimator, extendedKey, "sweep",
)
if err != nil {
return err
}
api := &btc.ExplorerAPI{BaseURL: apiURL}
var (
sweepTx = wire.NewMsgTx(2)
totalOutputValue = int64(0)
signDescs = make([]*input.SignDescriptor, 0)
prevOutFetcher = txscript.NewMultiPrevOutFetcher(nil)
estimator input.TxWeightEstimator
)
for _, target := range targets {
// We can't rely on the CSV delay of the channel DB to be
@ -239,11 +252,11 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
), input.DeriveRevocationPubkey(
target.revocationBasePoint,
target.commitPoint,
), target.lockScript, maxCsvTimeout,
), target.lockScript, 0, maxCsvTimeout,
)
if err != nil {
log.Errorf("Could not create matching script for %s "+
"or csv too high: %w", target.channelPoint, err)
log.Errorf("could not create matching script for %s "+
"or csv too high: %v", target.channelPoint, err)
continue
}
@ -283,13 +296,6 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
estimator.AddWitnessInput(input.ToLocalTimeoutWitnessSize)
}
// Add our sweep destination output.
sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams)
if err != nil {
return err
}
estimator.AddP2WKHOutput()
// Calculate the fee based on the given fee rate and our weight
// estimation.
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
@ -346,14 +352,14 @@ func pubKeyFromHex(pubKeyHex string) (*btcec.PublicKey, error) {
}
func bruteForceDelay(delayPubkey, revocationPubkey *btcec.PublicKey,
targetScript []byte, maxCsvTimeout uint16) (int32, []byte, []byte,
error) {
targetScript []byte, startCsvTimeout, maxCsvTimeout uint16) (int32,
[]byte, []byte, error) {
if len(targetScript) != 34 {
return 0, nil, nil, fmt.Errorf("invalid target script: %s",
targetScript)
}
for i := uint16(0); i <= maxCsvTimeout; i++ {
for i := startCsvTimeout; i <= maxCsvTimeout; i++ {
s, err := input.CommitScriptToSelf(
uint32(i), delayPubkey, revocationPubkey,
)

@ -10,8 +10,7 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
@ -30,12 +29,15 @@ type sweepTimeLockManualCommand struct {
Publish bool
SweepAddr string
MaxCsvLimit uint16
FeeRate uint16
FeeRate uint32
TimeLockAddr string
RemoteRevocationBasePoint string
MaxNumChansTotal uint16
MaxNumChanUpdates uint64
MaxNumChannelsTotal uint16
MaxNumChanUpdates uint64
ChannelBackup string
ChannelPoint string
rootKey *rootKey
inputs *inputFlags
@ -56,6 +58,9 @@ and only the channel.backup file is available.
To get the value for --remoterevbasepoint you must use the dumpbackup command,
then look up the value for RemoteChanCfg -> RevocationBasePoint -> PubKey.
Alternatively you can directly use the --frombackup and --channelpoint flags to
pull the required information from the given channel.backup file automatically.
To get the value for --timelockaddr you must look up the channel's funding
output on chain, then follow it to the force close output. The time locked
address is always the one that's longer (because it's P2WSH and not P2PKH).`,
@ -64,6 +69,14 @@ address is always the one that's longer (because it's P2WSH and not P2PKH).`,
--timelockaddr bc1q............ \
--remoterevbasepoint 03xxxxxxx \
--feerate 10 \
--publish
chantools sweeptimelockmanual \
--sweepaddr bc1q..... \
--timelockaddr bc1q............ \
--frombackup channel.backup \
--channelpoint f39310xxxxxxxxxx:1 \
--feerate 10 \
--publish`,
RunE: cc.Execute,
}
@ -76,14 +89,16 @@ address is always the one that's longer (because it's P2WSH and not P2PKH).`,
"API instead of just printing the TX",
)
cc.cmd.Flags().StringVar(
&cc.SweepAddr, "sweepaddr", "", "address to sweep the funds to",
&cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
"derive a new address from the seed automatically",
)
cc.cmd.Flags().Uint16Var(
&cc.MaxCsvLimit, "maxcsvlimit", defaultCsvLimit, "maximum CSV "+
"limit to use",
)
cc.cmd.Flags().Uint16Var(
&cc.MaxNumChansTotal, "maxnumchanstotal", maxKeys, "maximum "+
&cc.MaxNumChannelsTotal, "maxnumchanstotal", maxKeys, "maximum "+
"number of keys to try, set to maximum number of "+
"channels the local node potentially has or had",
)
@ -92,7 +107,7 @@ address is always the one that's longer (because it's P2WSH and not P2PKH).`,
"maximum number of channel updates to try, set to maximum "+
"number of times the channel was used",
)
cc.cmd.Flags().Uint16Var(
cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
"use for the sweep transaction in sat/vByte",
)
@ -105,6 +120,16 @@ address is always the one that's longer (because it's P2WSH and not P2PKH).`,
"remote node's revocation base point, can be found "+
"in a channel.backup file",
)
cc.cmd.Flags().StringVar(
&cc.ChannelBackup, "frombackup", "", "channel backup file to "+
"read the channel information from",
)
cc.cmd.Flags().StringVar(
&cc.ChannelPoint, "channelpoint", "", "channel point to use "+
"for locating the channel in the channel backup file "+
"specified in the --frombackup flag, "+
"format: txid:index",
)
cc.rootKey = newRootKey(cc.cmd, "deriving keys")
cc.inputs = newInputFlags(cc.cmd)
@ -119,16 +144,84 @@ func (c *sweepTimeLockManualCommand) Execute(_ *cobra.Command, _ []string) error
}
// Make sure the sweep and time lock addrs are set.
if c.SweepAddr == "" {
return fmt.Errorf("sweep addr is required")
err = lnd.CheckAddress(
c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
}
if c.TimeLockAddr == "" {
return fmt.Errorf("time lock addr is required")
err = lnd.CheckAddress(
c.TimeLockAddr, chainParams, true, "time lock",
lnd.AddrTypeP2WSH,
)
if err != nil {
return err
}
var (
startCsvLimit uint16
maxCsvLimit = c.MaxCsvLimit
startNumChannelsTotal uint16
maxNumChannelsTotal = c.MaxNumChannelsTotal
remoteRevocationBasePoint = c.RemoteRevocationBasePoint
)
// We either support specifying the remote revocation base point
// manually, in which case the CSV limit and number of channels are not
// known, or we can use the channel backup file to get the required
// information from there directly.
switch {
case c.RemoteRevocationBasePoint != "":
// Nothing to do here but continue below with the info provided
// by the user.
case c.ChannelBackup != "":
if c.ChannelPoint == "" {
return fmt.Errorf("channel point is required with " +
"--frombackup")
}
backupChan, err := lnd.ExtractChannel(
extendedKey, chainParams, c.ChannelBackup,
c.ChannelPoint,
)
if err != nil {
return fmt.Errorf("error extracting channel: %w", err)
}
remoteCfg := backupChan.RemoteChanCfg
remoteRevocationBasePoint = remoteCfg.RevocationBasePoint.PubKey
startCsvLimit = remoteCfg.CsvDelay
maxCsvLimit = startCsvLimit + 1
delayPath, err := lnd.ParsePath(
backupChan.LocalChanCfg.DelayBasePoint.Path,
)
if err != nil {
return fmt.Errorf("error parsing delay path: %w", err)
}
if len(delayPath) != 5 {
return fmt.Errorf("invalid delay path '%v'", delayPath)
}
startNumChannelsTotal = uint16(delayPath[4])
maxNumChannelsTotal = startNumChannelsTotal + 1
case c.ChannelBackup != "" && c.RemoteRevocationBasePoint != "":
return fmt.Errorf("cannot use both --frombackup and " +
"--remoterevbasepoint at the same time")
default:
return fmt.Errorf("either --frombackup or " +
"--remoterevbasepoint is required")
}
// The remote revocation base point must also be set and a valid EC
// point.
remoteRevPoint, err := pubKeyFromHex(c.RemoteRevocationBasePoint)
remoteRevPoint, err := pubKeyFromHex(remoteRevocationBasePoint)
if err != nil {
return fmt.Errorf("invalid remote revocation base point: %w",
err)
@ -136,22 +229,49 @@ func (c *sweepTimeLockManualCommand) Execute(_ *cobra.Command, _ []string) error
return sweepTimeLockManual(
extendedKey, c.APIURL, c.SweepAddr, c.TimeLockAddr,
remoteRevPoint, c.MaxCsvLimit, c.MaxNumChansTotal,
remoteRevPoint, startCsvLimit, maxCsvLimit,
startNumChannelsTotal, maxNumChannelsTotal,
c.MaxNumChanUpdates, c.Publish, c.FeeRate,
)
}
func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
sweepAddr, timeLockAddr string, remoteRevPoint *btcec.PublicKey,
maxCsvTimeout, maxNumChannels uint16, maxNumChanUpdates uint64,
publish bool, feeRate uint16) error {
startCsvTimeout, maxCsvTimeout, startNumChannels, maxNumChannels uint16,
maxNumChanUpdates uint64, publish bool, feeRate uint32) error {
log.Debugf("Starting to brute force the time lock script, using: "+
"remote_rev_base_point=%x, start_csv_limit=%d, "+
"max_csv_limit=%d, start_num_channels=%d, "+
"max_num_channels=%d, max_num_chan_updates=%d",
remoteRevPoint.SerializeCompressed(), startCsvTimeout,
maxCsvTimeout, startNumChannels, maxNumChannels,
maxNumChanUpdates)
// Create signer and transaction template.
var (
estimator input.TxWeightEstimator
signer = &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
api = newExplorerAPI(apiURL)
)
// First of all, we need to parse the lock addr and make sure we can
// brute force the script with the information we have. If not, we can't
// continue anyway.
lockScript, err := lnd.GetP2WSHScript(timeLockAddr, chainParams)
lockScript, err := lnd.PrepareWalletAddress(
sweepAddr, chainParams, nil, extendedKey, "time lock",
)
if err != nil {
return err
}
sweepScript, err := lnd.PrepareWalletAddress(
sweepAddr, chainParams, &estimator, extendedKey, "sweep",
)
if err != nil {
return fmt.Errorf("invalid time lock addr: %w", err)
return err
}
// We need to go through a lot of our keys so it makes sense to
@ -179,10 +299,10 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
delayDesc *keychain.KeyDescriptor
commitPoint *btcec.PublicKey
)
for i := uint16(0); i < maxNumChannels; i++ {
for i := startNumChannels; i < maxNumChannels; i++ {
csvTimeout, script, scriptHash, commitPoint, delayDesc, err = tryKey(
baseKey, remoteRevPoint, maxCsvTimeout, lockScript,
uint32(i), maxNumChanUpdates,
baseKey, remoteRevPoint, startCsvTimeout, maxCsvTimeout,
lockScript, uint32(i), maxNumChanUpdates,
)
if err == nil {
@ -201,13 +321,6 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
return fmt.Errorf("target script not derived")
}
// Create signer and transaction template.
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
api := &btc.ExplorerAPI{BaseURL: apiURL}
// We now know everything we need to construct the sweep transaction,
// except for what outpoint to sweep. We'll ask the chain API to give
// us this information.
@ -237,17 +350,11 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
// Calculate the fee based on the given fee rate and our weight
// estimation.
var estimator input.TxWeightEstimator
estimator.AddWitnessInput(input.ToLocalTimeoutWitnessSize)
estimator.AddP2WKHOutput()
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
// Add our sweep destination output.
sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams)
if err != nil {
return err
}
sweepTx.TxOut = []*wire.TxOut{{
Value: sweepValue - int64(totalFee),
PkScript: sweepScript,
@ -305,7 +412,7 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
}
func tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey,
maxCsvTimeout uint16, lockScript []byte, idx uint32,
startCsvTimeout, maxCsvTimeout uint16, lockScript []byte, idx uint32,
maxNumChanUpdates uint64) (int32, []byte, []byte, *btcec.PublicKey,
*keychain.KeyDescriptor, error) {
@ -338,7 +445,7 @@ func tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey,
// points and CSV values.
csvTimeout, script, scriptHash, commitPoint, err := bruteForceDelayPoint(
delayPrivKey.PubKey(), remoteRevPoint, revRoot, lockScript,
maxCsvTimeout, maxNumChanUpdates,
startCsvTimeout, maxCsvTimeout, maxNumChanUpdates,
)
if err == nil {
return csvTimeout, script, scriptHash, commitPoint,
@ -403,7 +510,7 @@ func tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey,
csvTimeout, script, scriptHash, commitPoint, err = bruteForceDelayPoint(
delayPrivKey.PubKey(), remoteRevPoint, revRoot2, lockScript,
maxCsvTimeout, maxNumChanUpdates,
startCsvTimeout, maxCsvTimeout, maxNumChanUpdates,
)
if err == nil {
return csvTimeout, script, scriptHash, commitPoint,
@ -444,7 +551,7 @@ func tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey,
csvTimeout, script, scriptHash, commitPoint, err = bruteForceDelayPoint(
delayPrivKey.PubKey(), remoteRevPoint, revRoot3, lockScript,
maxCsvTimeout, maxNumChanUpdates,
startCsvTimeout, maxCsvTimeout, maxNumChanUpdates,
)
if err == nil {
return csvTimeout, script, scriptHash, commitPoint,
@ -462,8 +569,8 @@ func tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey,
func bruteForceDelayPoint(delayBase, revBase *btcec.PublicKey,
revRoot *shachain.RevocationProducer, lockScript []byte,
maxCsvTimeout uint16, maxChanUpdates uint64) (int32, []byte, []byte,
*btcec.PublicKey, error) {
startCsvTimeout, maxCsvTimeout uint16, maxChanUpdates uint64) (int32,
[]byte, []byte, *btcec.PublicKey, error) {
for i := uint64(0); i < maxChanUpdates; i++ {
revPreimage, err := revRoot.AtIndex(i)
@ -475,7 +582,7 @@ func bruteForceDelayPoint(delayBase, revBase *btcec.PublicKey,
csvTimeout, script, scriptHash, err := bruteForceDelay(
input.TweakPubKey(delayBase, commitPoint),
input.DeriveRevocationPubkey(revBase, commitPoint),
lockScript, maxCsvTimeout,
lockScript, startCsvTimeout, maxCsvTimeout,
)
if err != nil {

@ -7,7 +7,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/stretchr/testify/require"
)
@ -86,7 +86,7 @@ func TestSweepTimeLockManual(t *testing.T) {
revPubKey, _ := btcec.ParsePubKey(revPubKeyBytes)
_, _, _, _, _, err = tryKey(
baseKey, revPubKey, defaultCsvLimit, lockScript,
baseKey, revPubKey, 0, defaultCsvLimit, lockScript,
tc.keyIndex, 500,
)
require.NoError(t, err)

Binary file not shown.

@ -2,26 +2,28 @@ package main
import (
"fmt"
"net"
"strconv"
"strings"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/connmgr"
"github.com/btcsuite/btcd/wire"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/brontide"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/peer"
"github.com/lightningnetwork/lnd/tor"
"github.com/spf13/cobra"
)
var (
dialTimeout = time.Minute
defaultTorDNSHostPort = "soa.nodes.lightning.directory:53"
)
type triggerForceCloseCommand struct {
@ -30,6 +32,8 @@ type triggerForceCloseCommand struct {
APIURL string
TorProxy string
rootKey *rootKey
cmd *cobra.Command
}
@ -38,8 +42,13 @@ func newTriggerForceCloseCommand() *cobra.Command {
cc := &triggerForceCloseCommand{}
cc.cmd = &cobra.Command{
Use: "triggerforceclose",
Short: "Connect to a peer and send a custom message to " +
"trigger a force close of the specified channel",
Short: "Connect to a Lightning Network peer and send " +
"specific messages to trigger a force close of the " +
"specified channel",
Long: `Asks the specified remote peer to force close a specific
channel by first sending a channel re-establish message, and if that doesn't
work, a custom error message (in case the peer is a specific version of CLN that
does not properly respond to a Data Loss Protection re-establish message).'`,
Example: `chantools triggerforceclose \
--peer 03abce...@xx.yy.zz.aa:9735 \
--channel_point abcdef01234...:x`,
@ -58,6 +67,10 @@ func newTriggerForceCloseCommand() *cobra.Command {
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
"be esplora compatible)",
)
cc.cmd.Flags().StringVar(
&cc.TorProxy, "torproxy", "", "SOCKS5 proxy to use for Tor "+
"connections (to .onion addresses)",
)
cc.rootKey = newRootKey(cc.cmd, "deriving the identity key")
return cc.cmd
@ -84,101 +97,152 @@ func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
PrivKey: identityPriv,
}
peerAddr, err := lncfg.ParseLNAddressString(
c.Peer, "9735", net.ResolveTCPAddr,
outPoint, err := parseOutPoint(c.ChannelPoint)
if err != nil {
return fmt.Errorf("error parsing channel point: %w", err)
}
err = requestForceClose(
c.Peer, c.TorProxy, pubKey, outPoint, identityECDH,
)
if err != nil {
return fmt.Errorf("error parsing peer address: %w", err)
return fmt.Errorf("error requesting force close: %w", err)
}
outPoint, err := parseOutPoint(c.ChannelPoint)
log.Infof("Message sent, waiting for force close transaction to " +
"appear in mempool")
api := newExplorerAPI(c.APIURL)
channelAddress, err := api.Address(c.ChannelPoint)
if err != nil {
return fmt.Errorf("error parsing channel point: %w", err)
return fmt.Errorf("error getting channel address: %w", err)
}
channelID := lnwire.NewChanIDFromOutPoint(outPoint)
conn, err := noiseDial(
identityECDH, peerAddr, &tor.ClearNet{}, dialTimeout,
spends, err := api.Spends(channelAddress)
if err != nil {
return fmt.Errorf("error getting spends: %w", err)
}
for len(spends) == 0 {
log.Infof("No spends found yet, waiting 5 seconds...")
time.Sleep(5 * time.Second)
spends, err = api.Spends(channelAddress)
if err != nil {
return fmt.Errorf("error getting spends: %w", err)
}
}
log.Infof("Found force close transaction %v", spends[0].TXID)
log.Infof("You can now use the sweepremoteclosed command to sweep " +
"the funds from the channel")
return nil
}
func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress,
netCfg tor.Net, timeout time.Duration) (*brontide.Conn, error) {
return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial)
}
func connectPeer(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
identity keychain.SingleKeyECDH,
dialTimeout time.Duration) (*peer.Brontide, error) {
var dialNet tor.Net = &tor.ClearNet{}
if torProxy != "" {
dialNet = &tor.ProxyNet{
SOCKS: torProxy,
DNS: defaultTorDNSHostPort,
StreamIsolation: false,
SkipProxyForClearNetTargets: true,
}
}
log.Debugf("Attempting to resolve peer address %v", peerHost)
peerAddr, err := lncfg.ParseLNAddressString(
peerHost, "9735", dialNet.ResolveTCPAddr,
)
if err != nil {
return fmt.Errorf("error dialing peer: %w", err)
return nil, fmt.Errorf("error parsing peer address: %w", err)
}
log.Infof("Attempting to connect to peer %x, dial timeout is %v",
pubKey.SerializeCompressed(), dialTimeout)
log.Debugf("Attempting to dial resolved peer address %v",
peerAddr.String())
conn, err := noiseDial(identity, peerAddr, dialNet, dialTimeout)
if err != nil {
return nil, fmt.Errorf("error dialing peer: %w", err)
}
log.Infof("Attempting to establish p2p connection to peer %x, dial"+
"timeout is %v", peerPubKey.SerializeCompressed(), dialTimeout)
req := &connmgr.ConnReq{
Addr: peerAddr,
Permanent: false,
}
p, err := lnd.ConnectPeer(conn, req, chainParams, identityECDH)
p, err := lnd.ConnectPeer(conn, req, chainParams, identity)
if err != nil {
return fmt.Errorf("error connecting to peer: %w", err)
return nil, fmt.Errorf("error connecting to peer: %w", err)
}
log.Infof("Connection established to peer %x",
pubKey.SerializeCompressed())
peerPubKey.SerializeCompressed())
// We'll wait until the peer is active.
select {
case <-p.ActiveSignal():
case <-p.QuitSignal():
return fmt.Errorf("peer %x disconnected",
pubKey.SerializeCompressed())
return nil, fmt.Errorf("peer %x disconnected",
peerPubKey.SerializeCompressed())
}
return p, nil
}
func requestForceClose(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
channelPoint *wire.OutPoint, identity keychain.SingleKeyECDH) error {
p, err := connectPeer(
peerHost, torProxy, peerPubKey, identity, dialTimeout,
)
if err != nil {
return fmt.Errorf("error connecting to peer: %w", err)
}
channelID := lnwire.NewChanIDFromOutPoint(channelPoint)
// Channel ID (32 byte) + u16 for the data length (which will be 0).
data := make([]byte, 34)
copy(data[:32], channelID[:])
log.Infof("Sending channel error message to peer to trigger force "+
"close of channel %v", c.ChannelPoint)
log.Infof("Sending channel re-establish to peer to trigger force "+
"close of channel %v", channelPoint)
_ = lnwire.SetCustomOverrides([]uint16{lnwire.MsgError})
msg, err := lnwire.NewCustom(lnwire.MsgError, data)
err = p.SendMessageLazy(true, &lnwire.ChannelReestablish{
ChanID: channelID,
})
if err != nil {
return err
}
err = p.SendMessageLazy(true, msg)
if err != nil {
return fmt.Errorf("error sending message: %w", err)
}
log.Infof("Message sent, waiting for force close transaction to " +
"appear in mempool")
log.Infof("Sending channel error message to peer to trigger force "+
"close of channel %v", channelPoint)
api := &btc.ExplorerAPI{BaseURL: c.APIURL}
channelAddress, err := api.Address(c.ChannelPoint)
_ = lnwire.SetCustomOverrides([]uint16{
lnwire.MsgError, lnwire.MsgChannelReestablish,
})
msg, err := lnwire.NewCustom(lnwire.MsgError, data)
if err != nil {
return fmt.Errorf("error getting channel address: %w", err)
return err
}
spends, err := api.Spends(channelAddress)
err = p.SendMessageLazy(true, msg)
if err != nil {
return fmt.Errorf("error getting spends: %w", err)
}
for len(spends) == 0 {
log.Infof("No spends found yet, waiting 5 seconds...")
time.Sleep(5 * time.Second)
spends, err = api.Spends(channelAddress)
if err != nil {
return fmt.Errorf("error getting spends: %w", err)
}
return fmt.Errorf("error sending message: %w", err)
}
log.Infof("Found force close transaction %v", spends[0].TXID)
log.Infof("You can now use the sweepremoteclosed command to sweep " +
"the funds from the channel")
return nil
}
func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress,
netCfg tor.Net, timeout time.Duration) (*brontide.Conn, error) {
return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial)
}
func parseOutPoint(s string) (*wire.OutPoint, error) {
split := strings.Split(s, ":")
if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {

@ -11,8 +11,8 @@ import (
"sync"
"time"
"github.com/guggero/chantools/btc/fasthd"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/btc/fasthd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/aezeed"
"github.com/lightningnetwork/lnd/keychain"
"github.com/spf13/cobra"

@ -1,28 +1,19 @@
package main
import (
"errors"
"fmt"
"os"
"strings"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcwallet/snacl"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/walletdb"
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/spf13/cobra"
"go.etcd.io/bbolt"
)
const (
passwordEnvName = "WALLET_PASSWORD"
walletInfoFormat = `
Identity Pubkey: %x
BIP32 HD extended root key: %s
@ -38,19 +29,7 @@ Scope: m/%d'/%d'
)
var (
// Namespace from github.com/btcsuite/btcwallet/wallet/wallet.go.
waddrmgrNamespaceKey = []byte("waddrmgr")
// Bucket names from github.com/btcsuite/btcwallet/waddrmgr/db.go.
mainBucketName = []byte("main")
masterPrivKeyName = []byte("mpriv")
cryptoPrivKeyName = []byte("cpriv")
masterHDPrivName = []byte("mhdpriv")
defaultAccount = uint32(waddrmgr.DefaultAccountNum)
openCallbacks = &waddrmgr.OpenCallbacks{
ObtainSeed: noConsole,
ObtainPrivatePass: noConsole,
}
defaultAccount = uint32(waddrmgr.DefaultAccountNum)
)
type walletInfoCommand struct {
@ -98,75 +77,24 @@ or simply press <enter> without entering a password when being prompted.`,
}
func (c *walletInfoCommand) Execute(_ *cobra.Command, _ []string) error {
var (
publicWalletPw = lnwallet.DefaultPublicPassphrase
privateWalletPw = lnwallet.DefaultPrivatePassphrase
err error
)
// Check that we have a wallet DB.
if c.WalletDB == "" {
return fmt.Errorf("wallet DB is required")
}
// To automate things with chantools, we also offer reading the wallet
// password from environment variables.
pw := []byte(strings.TrimSpace(os.Getenv(passwordEnvName)))
// Because we cannot differentiate between an empty and a non-existent
// environment variable, we need a special character that indicates that
// no password should be used. We use a single dash (-) for that as that
// would be too short for an explicit password anyway.
switch {
// The user indicated in the environment variable that no passphrase
// should be used. We don't set any value.
case string(pw) == "-":
// The environment variable didn't contain anything, we'll read the
// passphrase from the terminal.
case len(pw) == 0:
pw, err = passwordFromConsole("Input wallet password: ")
if err != nil {
return err
}
if len(pw) > 0 {
publicWalletPw = pw
privateWalletPw = pw
}
// There was a password in the environment, just use it directly.
default:
publicWalletPw = pw
privateWalletPw = pw
}
// Try to load and open the wallet.
db, err := walletdb.Open(
"bdb", lncfg.CleanAndExpandPath(c.WalletDB), false,
lnd.DefaultOpenTimeout,
w, privateWalletPw, cleanup, err := lnd.OpenWallet(
c.WalletDB, chainParams,
)
if errors.Is(err, bbolt.ErrTimeout) {
return fmt.Errorf("error opening wallet database, make sure " +
"lnd is not running and holding the exclusive lock " +
"on the wallet")
}
if err != nil {
return fmt.Errorf("error opening wallet database: %w", err)
return fmt.Errorf("error opening wallet file '%s': %w",
c.WalletDB, err)
}
defer func() { _ = db.Close() }()
w, err := wallet.Open(db, publicWalletPw, openCallbacks, chainParams, 0)
if err != nil {
return err
}
// Start and unlock the wallet.
w.Start()
defer w.Stop()
err = w.Unlock(privateWalletPw, nil)
if err != nil {
return err
}
defer func() {
if err := cleanup(); err != nil {
log.Errorf("error closing wallet: %v", err)
}
}()
// Print the wallet info and if requested the root key.
identityKey, scopeInfo, err := walletInfo(w, c.DumpAddrs)
@ -175,7 +103,9 @@ func (c *walletInfoCommand) Execute(_ *cobra.Command, _ []string) error {
}
rootKey := na
if c.WithRootKey {
masterHDPrivKey, err := decryptRootKey(db, privateWalletPw)
masterHDPrivKey, err := lnd.DecryptWalletRootKey(
w.Database(), privateWalletPw,
)
if err != nil {
return err
}
@ -259,7 +189,7 @@ func walletInfo(w *wallet.Wallet, dumpAddrs bool) (*btcec.PublicKey, string,
err = walletdb.View(
w.Database(), func(tx walletdb.ReadTx) error {
waddrmgrNs := tx.ReadBucket(
waddrmgrNamespaceKey,
lnd.WaddrmgrNamespaceKey,
)
return mgr.ForEachAccountAddress(
@ -304,64 +234,3 @@ func printScopeInfo(name string, w *wallet.Wallet,
return scopeInfo, nil
}
func decryptRootKey(db walletdb.DB, privPassphrase []byte) ([]byte, error) {
// Step 1: Load the encryption parameters and encrypted keys from the
// database.
var masterKeyPrivParams []byte
var cryptoKeyPrivEnc []byte
var masterHDPrivEnc []byte
err := walletdb.View(db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
if ns == nil {
return fmt.Errorf("namespace '%s' does not exist",
waddrmgrNamespaceKey)
}
mainBucket := ns.NestedReadBucket(mainBucketName)
if mainBucket == nil {
return fmt.Errorf("bucket '%s' does not exist",
mainBucketName)
}
val := mainBucket.Get(masterPrivKeyName)
if val != nil {
masterKeyPrivParams = make([]byte, len(val))
copy(masterKeyPrivParams, val)
}
val = mainBucket.Get(cryptoPrivKeyName)
if val != nil {
cryptoKeyPrivEnc = make([]byte, len(val))
copy(cryptoKeyPrivEnc, val)
}
val = mainBucket.Get(masterHDPrivName)
if val != nil {
masterHDPrivEnc = make([]byte, len(val))
copy(masterHDPrivEnc, val)
}
return nil
})
if err != nil {
return nil, err
}
// Step 2: Unmarshal the master private key parameters and derive
// key from passphrase.
var masterKeyPriv snacl.SecretKey
if err := masterKeyPriv.Unmarshal(masterKeyPrivParams); err != nil {
return nil, err
}
if err := masterKeyPriv.DeriveKey(&privPassphrase); err != nil {
return nil, err
}
// Step 3: Decrypt the keys in the correct order.
cryptoKeyPriv := &snacl.CryptoKey{}
cryptoKeyPrivBytes, err := masterKeyPriv.Decrypt(cryptoKeyPrivEnc)
if err != nil {
return nil, err
}
copy(cryptoKeyPriv[:], cryptoKeyPrivBytes)
return cryptoKeyPriv.Decrypt(masterHDPrivEnc)
}

@ -3,6 +3,7 @@ package main
import (
"testing"
"github.com/lightninglabs/chantools/lnd"
"github.com/stretchr/testify/require"
)
@ -20,7 +21,7 @@ func TestWalletInfo(t *testing.T) {
WithRootKey: true,
}
t.Setenv(passwordEnvName, testPassPhrase)
t.Setenv(lnd.PasswordEnvName, testPassPhrase)
err := info.Execute(nil, nil)
require.NoError(t, err)

@ -6,12 +6,13 @@ import (
"fmt"
"os"
"regexp"
"sort"
"strconv"
"strings"
"text/template"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/guggero/chantools/btc"
"github.com/hasura/go-graphql-client"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
@ -50,7 +51,7 @@ Contact: {{$peer.Contact}}
{{end}}
The document that describes what to do exactly is located here:
https://github.com/guggero/chantools/blob/master/doc/zombierecovery.md
https://github.com/lightninglabs/chantools/blob/master/doc/zombierecovery.md
Good luck!
@ -186,7 +187,7 @@ func (c *zombieRecoveryFindMatchesCommand) Execute(_ *cobra.Command,
log.Infof("%s: %s", groups[1], groups[2])
}
api := &btc.ExplorerAPI{BaseURL: c.APIURL}
api := newExplorerAPI(c.APIURL)
src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.AmbossKey})
httpClient := oauth2.NewClient(context.Background(), src)
client := graphql.NewClient(
@ -270,8 +271,18 @@ func (c *zombieRecoveryFindMatchesCommand) Execute(_ *cobra.Command,
}
}
// To achieve a stable order, we sort the matches lexicographically by
// their node key.
node1IDs := make([]string, 0, len(matches))
for node1 := range matches {
node1IDs = append(node1IDs, node1)
}
sort.Strings(node1IDs)
// Write the matches to files.
for node1, node1map := range matches {
for _, node1 := range node1IDs {
node1map := matches[node1]
tpl, err := template.New("initial").Parse(initialTemplate)
if err != nil {
return fmt.Errorf("error parsing template: %w", err)
@ -288,19 +299,20 @@ func (c *zombieRecoveryFindMatchesCommand) Execute(_ *cobra.Command,
}
folder := fmt.Sprintf("results/match-%s", node1)
err = os.MkdirAll(folder, 0755)
if err != nil {
return err
}
today := time.Now().Format("2006-01-02")
for node2, match := range node1map {
err = os.MkdirAll(folder, 0755)
if err != nil {
return err
}
matchBytes, err := json.MarshalIndent(match, "", " ")
if err != nil {
return err
}
fileName := fmt.Sprintf("%s/%s-%s.json",
folder, node2, time.Now().Format("2006-01-02"))
folder, node2, today)
log.Infof("Writing result to %s", fileName)
err = os.WriteFile(fileName, matchBytes, 0644)
if err != nil {
@ -315,7 +327,7 @@ func (c *zombieRecoveryFindMatchesCommand) Execute(_ *cobra.Command,
continue
}
textFileName := fmt.Sprintf("%s/message.txt", folder)
textFileName := fmt.Sprintf("%s/message-%s.txt", folder, today)
file, err := os.OpenFile(
textFileName, os.O_RDWR|os.O_CREATE, 0644,
)
@ -349,6 +361,11 @@ func fetchChannels(client *graphql.Client, pubkey string) ([]*gqChannel,
var query gqGetNodeQuery
err := client.Query(context.Background(), &query, variables)
if err != nil {
if strings.Contains(err.Error(), "Too many requests") {
time.Sleep(1 * time.Second)
continue
}
return nil, err
}

@ -6,7 +6,6 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
@ -16,7 +15,7 @@ import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/wallet/txrules"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
@ -26,7 +25,9 @@ import (
type zombieRecoveryMakeOfferCommand struct {
Node1 string
Node2 string
FeeRate uint16
FeeRate uint32
MatchOnly bool
rootKey *rootKey
cmd *cobra.Command
@ -60,10 +61,14 @@ a counter offer.`,
&cc.Node2, "node2_keys", "", "the JSON file generated in the"+
"previous step ('preparekeys') command of node 2",
)
cc.cmd.Flags().Uint16Var(
cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
"use for the sweep transaction in sat/vByte",
)
cc.cmd.Flags().BoolVar(
&cc.MatchOnly, "matchonly", false, "only match the keys, "+
"don't create an offer",
)
cc.rootKey = newRootKey(cc.cmd, "signing the offer")
@ -82,12 +87,12 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
c.FeeRate = defaultFeeSatPerVByte
}
node1Bytes, err := ioutil.ReadFile(c.Node1)
node1Bytes, err := os.ReadFile(c.Node1)
if err != nil {
return fmt.Errorf("error reading node1 key file %s: %w",
c.Node1, err)
}
node2Bytes, err := ioutil.ReadFile(c.Node2)
node2Bytes, err := os.ReadFile(c.Node2)
if err != nil {
return fmt.Errorf("error reading node2 key file %s: %w",
c.Node2, err)
@ -153,6 +158,22 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
}
}
// If we're only matching, we can stop here.
if c.MatchOnly {
ourPubKeys, err := parseKeys(keys1.Node1.MultisigKeys)
if err != nil {
return fmt.Errorf("error parsing their keys: %w", err)
}
theirPubKeys, err := parseKeys(keys2.Node2.MultisigKeys)
if err != nil {
return fmt.Errorf("error parsing our keys: %w", err)
}
return matchKeys(
keys1.Channels, ourPubKeys, theirPubKeys, chainParams,
)
}
// Make sure one of the nodes is ours.
_, pubKey, _, err := lnd.DeriveKey(
extendedKey, lnd.IdentityPath(chainParams), chainParams,
@ -206,52 +227,19 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
return fmt.Errorf("payout address missing")
}
ourPubKeys := make([]*btcec.PublicKey, len(ourKeys))
theirPubKeys := make([]*btcec.PublicKey, len(theirKeys))
for idx, pubKeyHex := range ourKeys {
ourPubKeys[idx], err = pubKeyFromHex(pubKeyHex)
if err != nil {
return fmt.Errorf("error parsing our pubKey: %w", err)
}
}
for idx, pubKeyHex := range theirKeys {
theirPubKeys[idx], err = pubKeyFromHex(pubKeyHex)
if err != nil {
return fmt.Errorf("error parsing their pubKey: %w", err)
}
ourPubKeys, err := parseKeys(ourKeys)
if err != nil {
return fmt.Errorf("error parsing their keys: %w", err)
}
// Loop through all channels and all keys now, this will definitely take
// a while.
channelLoop:
for _, channel := range keys1.Channels {
for ourKeyIndex, ourKey := range ourPubKeys {
for _, theirKey := range theirPubKeys {
match, witnessScript, err := matchScript(
channel.Address, ourKey, theirKey,
chainParams,
)
if err != nil {
return fmt.Errorf("error matching "+
"keys to script: %w", err)
}
if match {
channel.ourKeyIndex = uint32(ourKeyIndex)
channel.ourKey = ourKey
channel.theirKey = theirKey
channel.witnessScript = witnessScript
log.Infof("Found keys for channel %s",
channel.ChanPoint)
continue channelLoop
}
}
}
theirPubKeys, err := parseKeys(theirKeys)
if err != nil {
return fmt.Errorf("error parsing our keys: %w", err)
}
return fmt.Errorf("didn't find matching multisig keys for "+
"channel %s", channel.ChanPoint)
err = matchKeys(keys1.Channels, ourPubKeys, theirPubKeys, chainParams)
if err != nil {
return err
}
// Let's now sum up the tally of how much of the rescued funds should
@ -381,10 +369,8 @@ channelLoop:
return fmt.Errorf("error creating PSBT from TX: %w", err)
}
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
// First we add the necessary information to the psbt package so that
// we can sign the transaction with SIGHASH_ALL.
for idx, txIn := range inputs {
channel := keys1.Channels[idx]
@ -411,6 +397,16 @@ channelLoop:
Value: channel.theirKey.SerializeCompressed(),
},
)
}
// Loop a second time through the inputs and sign each input. We now
// have all the witness/nonwitness data filled in the psbt package.
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
for idx, txIn := range inputs {
channel := keys1.Channels[idx]
keyDesc := keychain.KeyDescriptor{
PubKey: channel.ourKey,
@ -444,6 +440,64 @@ channelLoop:
return nil
}
// parseKeys parses a list of string keys into public keys.
func parseKeys(keys []string) ([]*btcec.PublicKey, error) {
pubKeys := make([]*btcec.PublicKey, 0, len(keys))
for _, key := range keys {
pubKey, err := pubKeyFromHex(key)
if err != nil {
return nil, err
}
pubKeys = append(pubKeys, pubKey)
}
return pubKeys, nil
}
// matchKeys tries to match the keys from the two nodes. It updates the channels
// with the correct keys and witness scripts.
func matchKeys(channels []*channel, ourPubKeys, theirPubKeys []*btcec.PublicKey,
chainParams *chaincfg.Params) error {
// Loop through all channels and all keys now, this will definitely take
// a while.
channelLoop:
for _, channel := range channels {
for ourKeyIndex, ourKey := range ourPubKeys {
for _, theirKey := range theirPubKeys {
match, witnessScript, err := matchScript(
channel.Address, ourKey, theirKey,
chainParams,
)
if err != nil {
return fmt.Errorf("error matching "+
"keys to script: %w", err)
}
if match {
channel.ourKeyIndex = uint32(ourKeyIndex)
channel.ourKey = ourKey
channel.theirKey = theirKey
channel.witnessScript = witnessScript
log.Infof("Found keys for channel %s: "+
"our key %x, their key %x",
channel.ChanPoint,
ourKey.SerializeCompressed(),
theirKey.SerializeCompressed())
continue channelLoop
}
}
}
return fmt.Errorf("didn't find matching multisig keys for "+
"channel %s", channel.ChanPoint)
}
return nil
}
func matchScript(address string, key1, key2 *btcec.PublicKey,
params *chaincfg.Params) (bool, []byte, error) {

@ -6,9 +6,10 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"time"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/spf13/cobra"
)
@ -20,6 +21,8 @@ type zombieRecoveryPrepareKeysCommand struct {
MatchFile string
PayoutAddr string
NumKeys uint32
rootKey *rootKey
cmd *cobra.Command
}
@ -47,7 +50,12 @@ correct ones for the matched channels.`,
cc.cmd.Flags().StringVar(
&cc.PayoutAddr, "payout_addr", "", "the address where this "+
"node's rescued funds should be sent to, must be a "+
"P2WPKH (native SegWit) address")
"P2WPKH (native SegWit) address",
)
cc.cmd.Flags().Uint32Var(
&cc.NumKeys, "num_keys", numMultisigKeys, "the number of "+
"multisig keys to derive",
)
cc.rootKey = newRootKey(cc.cmd, "deriving the multisig keys")
@ -108,9 +116,9 @@ func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command,
}
// Derive all 2500 keys now, this might take a while.
for index := 0; index < numMultisigKeys; index++ {
for index := uint32(0); index < c.NumKeys; index++ {
_, pubKey, _, err := lnd.DeriveKey(
extendedKey, lnd.MultisigPath(chainParams, index),
extendedKey, lnd.MultisigPath(chainParams, int(index)),
chainParams,
)
if err != nil {
@ -134,5 +142,5 @@ func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command,
fileName := fmt.Sprintf("results/preparedkeys-%s-%s.json",
time.Now().Format("2006-01-02"), pubKeyStr)
log.Infof("Writing result to %s", fileName)
return ioutil.WriteFile(fileName, matchBytes, 0644)
return os.WriteFile(fileName, matchBytes, 0644)
}

@ -18,7 +18,7 @@ func newZombieRecoveryCommand() *cobra.Command {
Long: `A sub command that hosts a set of further sub commands
to help with recovering funds tuck in zombie channels.
Please visit https://github.com/guggero/chantools/blob/master/doc/zombierecovery.md
Please visit https://github.com/lightninglabs/chantools/blob/master/doc/zombierecovery.md
for more information on how to use these commands.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {

@ -10,7 +10,7 @@ import (
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/guggero/chantools/lnd"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/keychain"
"github.com/spf13/cobra"
)

@ -6,13 +6,15 @@ Chantools helps recover funds from lightning channels
This tool provides helper functions that can be used rescue
funds locked in lnd channels in case lnd itself cannot run properly anymore.
Complete documentation is available at https://github.com/guggero/chantools/.
Complete documentation is available at
https://github.com/lightninglabs/chantools/.
### Options
```
-h, --help help for chantools
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```
@ -21,9 +23,12 @@ Complete documentation is available at https://github.com/guggero/chantools/.
* [chantools chanbackup](chantools_chanbackup.md) - Create a channel.backup file from a channel database
* [chantools closepoolaccount](chantools_closepoolaccount.md) - Tries to close a Pool account that has expired
* [chantools compactdb](chantools_compactdb.md) - Create a copy of a channel.db file in safe/read-only mode
* [chantools createwallet](chantools_createwallet.md) - Create a new lnd compatible wallet.db file from an existing seed or by generating a new one
* [chantools deletepayments](chantools_deletepayments.md) - Remove all (failed) payments from a channel DB
* [chantools derivekey](chantools_derivekey.md) - Derive a key with a specific derivation path
* [chantools doublespendinputs](chantools_doublespendinputs.md) - Replace a transaction by double spending its input
* [chantools dropchannelgraph](chantools_dropchannelgraph.md) - Remove all graph related data from a channel DB
* [chantools dropgraphzombies](chantools_dropgraphzombies.md) - Remove all channels identified as zombies from the graph to force a re-sync of the graph
* [chantools dumpbackup](chantools_dumpbackup.md) - Dump the content of a channel.backup file
* [chantools dumpchannels](chantools_dumpchannels.md) - Dump all channel information from an lnd channel database
* [chantools fakechanbackup](chantools_fakechanbackup.md) - Fake a channel backup file to attempt fund recovery
@ -32,18 +37,21 @@ Complete documentation is available at https://github.com/guggero/chantools/.
* [chantools forceclose](chantools_forceclose.md) - Force-close the last state that is in the channel.db provided
* [chantools genimportscript](chantools_genimportscript.md) - Generate a script containing the on-chain keys of an lnd wallet that can be imported into other software like bitcoind
* [chantools migratedb](chantools_migratedb.md) - Apply all recent lnd channel database migrations
* [chantools pullanchor](chantools_pullanchor.md) - Attempt to CPFP an anchor output of a channel
* [chantools recoverloopin](chantools_recoverloopin.md) - Recover a loop in swap that the loop daemon is not able to sweep
* [chantools removechannel](chantools_removechannel.md) - Remove a single channel from the given channel DB
* [chantools rescueclosed](chantools_rescueclosed.md) - Try finding the private keys for funds that are in outputs of remotely force-closed channels
* [chantools rescuefunding](chantools_rescuefunding.md) - Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the initiator of the channel needs to run
* [chantools rescuetweakedkey](chantools_rescuetweakedkey.md) - Attempt to rescue funds locked in an address with a key that was affected by a specific bug in lnd
* [chantools showrootkey](chantools_showrootkey.md) - Extract and show the BIP32 HD root key from the 24 word lnd aezeed
* [chantools signmessage](chantools_signmessage.md) - Sign a message with the node's private key.
* [chantools signpsbt](chantools_signpsbt.md) - Sign a Partially Signed Bitcoin Transaction (PSBT)
* [chantools signrescuefunding](chantools_signrescuefunding.md) - Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the remote node (the non-initiator) of the channel needs to run
* [chantools summary](chantools_summary.md) - Compile a summary about the current state of channels
* [chantools sweepremoteclosed](chantools_sweepremoteclosed.md) - Go through all the addresses that could have funds of channels that were force-closed by the remote party. A public block explorer is queried for each address and if any balance is found, all funds are swept to a given address
* [chantools sweeptimelock](chantools_sweeptimelock.md) - Sweep the force-closed state after the time lock has expired
* [chantools sweeptimelockmanual](chantools_sweeptimelockmanual.md) - Sweep the force-closed state of a single channel manually if only a channel backup file is available
* [chantools triggerforceclose](chantools_triggerforceclose.md) - Connect to a peer and send a custom message to trigger a force close of the specified channel
* [chantools triggerforceclose](chantools_triggerforceclose.md) - Connect to a Lightning Network peer and send specific messages to trigger a force close of the specified channel
* [chantools vanitygen](chantools_vanitygen.md) - Generate a seed with a custom lnd node identity public key that starts with the given prefix
* [chantools walletinfo](chantools_walletinfo.md) - Shows info about an lnd wallet.db file and optionally extracts the BIP32 HD root key
* [chantools zombierecovery](chantools_zombierecovery.md) - Try rescuing funds stuck in channels with zombie nodes

@ -27,12 +27,14 @@ chantools chanbackup \
-h, --help help for chanbackup
--multi_file string lnd channel.backup file to create
--rootkey string BIP32 HD root key of the wallet to use for creating the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro creating the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -32,7 +32,7 @@ chantools closepoolaccount \
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
--auctioneerkey string the auctioneer's static public key (default "028e87bdd134238f8347f845d9ecc827b843d0d1e27cdcb46da704d916613f4fce")
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--feerate uint16 fee rate to use for the sweep transaction in sat/vByte (default 30)
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
-h, --help help for closepoolaccount
--maxnumaccounts uint32 the number of account indices to try at most (default 20)
--maxnumbatchkeys uint32 the number of batch keys to try at most (default 500)
@ -41,13 +41,15 @@ chantools closepoolaccount \
--outpoint string last account outpoint of the account to close (<txid>:<txindex>)
--publish publish sweep TX to the chain API instead of just printing the TX
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to sweep the funds to
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--walletdb string read the seed/master root key to use fro deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -32,6 +32,7 @@ chantools compactdb \
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -0,0 +1,44 @@
## chantools createwallet
Create a new lnd compatible wallet.db file from an existing seed or by generating a new one
### Synopsis
Creates a new wallet that can be used with lnd or with
chantools. The wallet can be created from an existing seed or a new one can be
generated (use --generateseed).
```
chantools createwallet [flags]
```
### Examples
```
chantools createwallet \
--walletdbdir ~/.lnd/data/chain/bitcoin/mainnet
```
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--generateseed generate a new seed instead of using an existing one
-h, --help help for createwallet
--rootkey string BIP32 HD root key of the wallet to use for creating the new wallet; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro creating the new wallet from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
--walletdbdir string the folder to create the new wallet.db file in
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```
### SEE ALSO
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels

@ -10,7 +10,7 @@ If only the failed payments should be deleted (and not the successful ones), the
CAUTION: Running this command will make it impossible to use the channel DB
with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.16.0-beta or later after using this command!'
run lnd v0.17.4-beta or later after using this command!'
```
chantools deletepayments [flags]
@ -35,6 +35,7 @@ chantools deletepayments --failedonly \
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -23,18 +23,20 @@ chantools derivekey --identity
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for derivekey
--identity derive the lnd identity_pubkey
--neuter don't output private key(s), only public key(s)
--path string BIP32 derivation path to derive; must start with "m/"
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for derivekey
--identity derive the lnd identity_pubkey
--neuter don't output private key(s), only public key(s)
--path string BIP32 derivation path to derive; must start with "m/"
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -0,0 +1,51 @@
## chantools doublespendinputs
Replace a transaction by double spending its input
### Synopsis
Tries to double spend the given inputs by deriving the
private for the address and sweeping the funds to the given address. This can
only be used with inputs that belong to an lnd wallet.
```
chantools doublespendinputs [flags]
```
### Examples
```
chantools doublespendinputs \
--inputoutpoints xxxxxxxxx:y,xxxxxxxxx:y \
--sweepaddr bc1q..... \
--feerate 10 \
--publish
```
### Options
```
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
-h, --help help for doublespendinputs
--inputoutpoints strings list of outpoints to double spend in the format txid:vout
--publish publish replacement TX to the chain API instead of just printing the TX
--recoverywindow uint32 number of keys to scan per internal/external branch; output will consist of double this amount of keys (default 2500)
--rootkey string BIP32 HD root key of the wallet to use for deriving the input keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--walletdb string read the seed/master root key to use fro deriving the input keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```
### SEE ALSO
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels

@ -12,7 +12,7 @@ without removing any other data.
CAUTION: Running this command will make it impossible to use the channel DB
with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.16.0-beta or later after using this command!'
run lnd v0.17.4-beta or later after using this command!'
```
chantools dropchannelgraph [flags]
@ -34,7 +34,7 @@ chantools dropchannelgraph \
### Options
```
--channeldb string lnd channel.db file to dump channels from
--channeldb string lnd channel.db file to drop channels from
--fix_only fix an already empty graph by re-adding the own node's channels
-h, --help help for dropchannelgraph
--node_identity_key string your node's identity public key
@ -45,6 +45,7 @@ chantools dropchannelgraph \
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -0,0 +1,46 @@
## chantools dropgraphzombies
Remove all channels identified as zombies from the graph to force a re-sync of the graph
### Synopsis
This command removes all channels that were identified as
zombies from the local graph.
This will cause lnd to re-download all those channels from the network and can
be helpful to fix a graph that is out of sync with the network.
CAUTION: Running this command will make it impossible to use the channel DB
with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.17.4-beta or later after using this command!'
```
chantools dropgraphzombies [flags]
```
### Examples
```
chantools dropgraphzombies \
--channeldb ~/.lnd/data/graph/mainnet/channel.db
```
### Options
```
--channeldb string lnd channel.db file to drop zombies from
-h, --help help for dropgraphzombies
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```
### SEE ALSO
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels

@ -25,12 +25,14 @@ chantools dumpbackup \
-h, --help help for dumpbackup
--multi_file string lnd channel.backup file to dump
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -32,6 +32,7 @@ chantools dumpchannels \
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -61,16 +61,18 @@ chantools fakechanbackup --from_channel_graph lncli_describegraph.json \
--channelpoint string funding transaction outpoint of the channel to rescue (<txid>:<txindex>) as it is displayed on 1ml.com
--from_channel_graph string the full LN channel graph in the JSON format that the 'lncli describegraph' returns
-h, --help help for fakechanbackup
--multi_file string the fake channel backup file to create (default "results/fake-2023-04-11-16-33-35.backup")
--multi_file string the fake channel backup file to create (default "results/fake-2024-01-26-02-27-52.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>
--walletdb string read the seed/master root key to use fro encrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -27,12 +27,14 @@ chantools filterbackup \
-h, --help help for filterbackup
--multi_file string lnd channel.backup file to filter
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -28,12 +28,14 @@ chantools fixoldbackup \
-h, --help help for fixoldbackup
--multi_file string lnd channel.backup file to fix
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -43,12 +43,14 @@ chantools forceclose \
--pendingchannels string channel input is in the format of lncli's pendingchannels format; specify '-' to read from stdin
--publish publish force-closing TX to the chain API instead of just printing the TX
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -51,12 +51,14 @@ chantools genimportscript --format bitcoin-cli \
--rescanfrom uint32 block number to rescan from; will be set automatically from the wallet birthday if the lnd 24 word aezeed is entered (default 500000)
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--stdout write generated import script to standard out instead of writing it to a file
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -11,7 +11,7 @@ needs to read the database content.
CAUTION: Running this command will make it impossible to use the channel DB
with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.16.0-beta or later after using this command!'
run lnd v0.17.4-beta or later after using this command!'
```
chantools migratedb [flags]
@ -35,6 +35,7 @@ chantools migratedb \
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -0,0 +1,50 @@
## chantools pullanchor
Attempt to CPFP an anchor output of a channel
### Synopsis
Use this command to confirm a channel force close
transaction of an anchor output channel type. This will attempt to CPFP the
330 byte anchor output created for your node.
```
chantools pullanchor [flags]
```
### Examples
```
chantools pullanchor \
--sponsorinput txid:vout \
--anchoraddr bc1q..... \
--changeaddr bc1q..... \
--feerate 30
```
### Options
```
--anchoraddr stringArray the address of the anchor output (p2wsh or p2tr output with 330 satoshis) that should be pulled; can be specified multiple times per command to pull multiple anchors with a single transaction
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--changeaddr string the change address to send the remaining funds back to; specify 'fromseed' to derive a new address from the seed automatically
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
-h, --help help for pullanchor
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sponsorinput string the input to use to sponsor the CPFP transaction; must be owned by the lnd node that owns the anchor output
--walletdb string read the seed/master root key to use fro deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```
### SEE ALSO
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels

@ -13,7 +13,7 @@ chantools recoverloopin \
--txid abcdef01234... \
--vout 0 \
--swap_hash abcdef01234... \
--loop_db_path /path/to/loop.db \
--loop_db_dir /path/to/loop/db/dir \
--sweep_addr bc1pxxxxxxx \
--feerate 10
```
@ -23,23 +23,27 @@ chantools recoverloopin \
```
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--feerate uint16 fee rate to use for the sweep transaction in sat/vByte
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte
-h, --help help for recoverloopin
--loop_db_path string path to the loop database file
--loop_db_dir string path to the loop database directory, where the loop.db file is located
--num_tries int number of tries to try to find the correct key index (default 1000)
--output_amt uint amount of the output to sweep
--publish publish sweep TX to the chain API instead of just printing the TX
--rootkey string BIP32 HD root key of the wallet to use for deriving starting key; leave empty to prompt for lnd 24 word aezeed
--sqlite_file string optional path to the loop sqlite database file, if not specified, the default location will be loaded from --loop_db_dir
--start_key_index int start key index to try to find the correct key index
--swap_hash string swap hash of the loop in swap
--sweep_addr string address to recover the funds to
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--txid string transaction id of the on-chain transaction that created the HTLC
--vout uint32 output index of the on-chain transaction that created the HTLC
--walletdb string read the seed/master root key to use fro deriving starting key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -11,7 +11,7 @@ channel was never confirmed on chain!
CAUTION: Running this command will make it impossible to use the channel DB
with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.16.0-beta or later after using this command!
run lnd v0.17.4-beta or later after using this command!
```
chantools removechannel [flags]
@ -37,6 +37,7 @@ chantools removechannel \
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -12,6 +12,10 @@ 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.
NOTE: Unless your channel was opened before 2019, you very likely don't need to
use this command as things were simplified. Use 'chantools sweepremoteclosed'
instead if the remote party has already closed the channel.
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 (see
https://github.com/guggero/lnd/releases for a binary release) in combination
@ -48,7 +52,7 @@ chantools rescueclosed --fromsummary results/summary-xxxxxx.json \
--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
--force_close_addr string the address the channel was force closed to, look up in block explorer by following funding txid
--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
@ -56,12 +60,14 @@ chantools rescueclosed --fromsummary results/summary-xxxxxx.json \
--lnd_log string the lnd log file to read to get the commit_point values when rescuing multiple channels at the same time
--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
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -44,18 +44,20 @@ chantools rescuefunding \
--channeldb string lnd channel.db file to rescue a channel from; must contain the pending channel specified with --channelpoint
--confirmedchannelpoint string channel outpoint that got confirmed on chain (<txid>:<txindex>); normally this is the same as the --dbchannelpoint so it will be set to that value ifthis is left empty
--dbchannelpoint string funding transaction outpoint of the channel to rescue (<txid>:<txindex>) as it is recorded in the DB
--feerate uint16 fee rate to use for the sweep transaction in sat/vByte (default 30)
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
-h, --help help for rescuefunding
--localkeyindex uint32 in case a channel DB is not available (but perhaps a channel backup file), the derivation index of the local multisig public key can be specified manually
--remotepubkey string in case a channel DB is not available (but perhaps a channel backup file), the remote multisig public key can be specified manually
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to sweep the funds to
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--walletdb string read the seed/master root key to use fro deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -29,12 +29,14 @@ chantools rescuetweakedkey \
--path string BIP32 derivation path to derive the starting key from; must start with "m/"
--rootkey string BIP32 HD root key of the wallet to use for deriving starting key; leave empty to prompt for lnd 24 word aezeed
--targetaddr string address the funds are locked in
--walletdb string read the seed/master root key to use fro deriving starting key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -21,15 +21,17 @@ chantools showrootkey
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for showrootkey
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for showrootkey
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -0,0 +1,41 @@
## chantools signmessage
Sign a message with the node's private key.
### Synopsis
Sign msg with the resident node's private key.
Returns the signature as a zbase32 string.
```
chantools signmessage [flags]
```
### Examples
```
chantools signmessage --msg=foobar
```
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for signmessage
--msg string the message to sign
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```
### SEE ALSO
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels

@ -0,0 +1,46 @@
## chantools signpsbt
Sign a Partially Signed Bitcoin Transaction (PSBT)
### Synopsis
Sign a PSBT with a master root key. The PSBT must contain
an input that is owned by the master root key.
```
chantools signpsbt [flags]
```
### Examples
```
chantools signpsbt \
--psbt <the_base64_encoded_psbt>
chantools signpsbt --fromrawpsbtfile <file_with_psbt>
```
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--fromrawpsbtfile string the file containing the raw, binary encoded PSBT packet to sign
-h, --help help for signpsbt
--psbt string Partially Signed Bitcoin Transaction to sign
--rootkey string BIP32 HD root key of the wallet to use for signing the PSBT; leave empty to prompt for lnd 24 word aezeed
--torawpsbtfile string the file to write the resulting signed raw, binary encoded PSBT packet to
--walletdb string read the seed/master root key to use fro signing the PSBT from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```
### SEE ALSO
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels

@ -26,16 +26,18 @@ chantools signrescuefunding \
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for signrescuefunding
--psbt string Partially Signed Bitcoin Transaction that was provided by the initiator of the channel to rescue
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for signrescuefunding
--psbt string Partially Signed Bitcoin Transaction that was provided by the initiator of the channel to rescue
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -34,6 +34,7 @@ chantools summary --fromchanneldb ~/.lnd/data/graph/mainnet/channel.db
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -13,6 +13,7 @@ funds can be swept after the force-close transaction was confirmed.
Supported remote force-closed channel types are:
- STATIC_REMOTE_KEY (a.k.a. tweakless channels)
- ANCHOR (a.k.a. anchor output channels)
- SIMPLE_TAPROOT (a.k.a. simple taproot channels)
```
@ -34,18 +35,20 @@ chantools sweepremoteclosed \
```
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--feerate uint16 fee rate to use for the sweep transaction in sat/vByte (default 30)
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
-h, --help help for sweepremoteclosed
--publish publish sweep TX to the chain API instead of just printing the TX
--recoverywindow uint32 number of keys to scan per derivation path (default 200)
--rootkey string BIP32 HD root key of the wallet to use for sweeping the wallet; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to sweep the funds to
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--walletdb string read the seed/master root key to use fro sweeping the wallet from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -31,7 +31,7 @@ chantools sweeptimelock \
```
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--feerate uint16 fee rate to use for the sweep transaction in sat/vByte (default 30)
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
--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 sweeptimelock
@ -40,13 +40,15 @@ chantools sweeptimelock \
--pendingchannels string channel input is in the format of lncli's pendingchannels format; specify '-' to read from stdin
--publish publish sweep TX to the chain API instead of just printing the TX
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to sweep the funds to
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--walletdb string read the seed/master root key to use fro deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -12,6 +12,9 @@ and only the channel.backup file is available.
To get the value for --remoterevbasepoint you must use the dumpbackup command,
then look up the value for RemoteChanCfg -> RevocationBasePoint -> PubKey.
Alternatively you can directly use the --frombackup and --channelpoint flags to
pull the required information from the given channel.backup file automatically.
To get the value for --timelockaddr you must look up the channel's funding
output on chain, then follow it to the force close output. The time locked
address is always the one that's longer (because it's P2WSH and not P2PKH).
@ -29,6 +32,14 @@ chantools sweeptimelockmanual \
--remoterevbasepoint 03xxxxxxx \
--feerate 10 \
--publish
chantools sweeptimelockmanual \
--sweepaddr bc1q..... \
--timelockaddr bc1q............ \
--frombackup channel.backup \
--channelpoint f39310xxxxxxxxxx:1 \
--feerate 10 \
--publish
```
### Options
@ -36,7 +47,9 @@ chantools sweeptimelockmanual \
```
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--feerate uint16 fee rate to use for the sweep transaction in sat/vByte (default 30)
--channelpoint string channel point to use for locating the channel in the channel backup file specified in the --frombackup flag, format: txid:index
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
--frombackup string channel backup file to read the channel information from
--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 sweeptimelockmanual
@ -48,14 +61,16 @@ chantools sweeptimelockmanual \
--publish publish sweep TX to the chain API instead of just printing the TX
--remoterevbasepoint string remote node's revocation base point, can be found in a channel.backup file
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to sweep the funds to
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--timelockaddr string address of the time locked commitment output where the funds are stuck in
--walletdb string read the seed/master root key to use fro deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -1,6 +1,13 @@
## chantools triggerforceclose
Connect to a peer and send a custom message to trigger a force close of the specified channel
Connect to a Lightning Network peer and send specific messages to trigger a force close of the specified channel
### Synopsis
Asks the specified remote peer to force close a specific
channel by first sending a channel re-establish message, and if that doesn't
work, a custom error message (in case the peer is a specific version of CLN that
does not properly respond to a Data Loss Protection re-establish message).'
```
chantools triggerforceclose [flags]
@ -23,12 +30,15 @@ chantools triggerforceclose \
-h, --help help for triggerforceclose
--peer string remote peer address (<pubkey>@<host>[:<port>])
--rootkey string BIP32 HD root key of the wallet to use for deriving the identity key; leave empty to prompt for lnd 24 word aezeed
--torproxy string SOCKS5 proxy to use for Tor connections (to .onion addresses)
--walletdb string read the seed/master root key to use fro deriving the identity key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -42,6 +42,7 @@ chantools vanitygen --prefix 022222 --threads 8
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -39,6 +39,7 @@ chantools walletinfo --withrootkey \
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -7,7 +7,7 @@ Try rescuing funds stuck in channels with zombie nodes
A sub command that hosts a set of further sub commands
to help with recovering funds tuck in zombie channels.
Please visit https://github.com/guggero/chantools/blob/master/doc/zombierecovery.md
Please visit https://github.com/lightninglabs/chantools/blob/master/doc/zombierecovery.md
for more information on how to use these commands.
```
@ -24,6 +24,7 @@ chantools zombierecovery [flags]
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -37,6 +37,7 @@ chantools zombierecovery findmatches \
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -29,17 +29,20 @@ chantools zombierecovery makeoffer \
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--feerate uint16 fee rate to use for the sweep transaction in sat/vByte (default 30)
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
-h, --help help for makeoffer
--matchonly only match the keys, don't create an offer
--node1_keys string the JSON file generated in theprevious step ('preparekeys') command of node 1
--node2_keys string the JSON file generated in theprevious step ('preparekeys') command of node 2
--rootkey string BIP32 HD root key of the wallet to use for signing the offer; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro signing the offer from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -28,14 +28,17 @@ chantools zombierecovery preparekeys \
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for preparekeys
--match_file string the match JSON file that was sent to both nodes by the match maker
--num_keys uint32 the number of multisig keys to derive (default 2500)
--payout_addr string the address where this node's rescued funds should be sent to, must be a P2WPKH (native SegWit) address
--rootkey string BIP32 HD root key of the wallet to use for deriving the multisig keys; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro deriving the multisig keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -21,16 +21,18 @@ chantools zombierecovery signoffer \
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for signoffer
--psbt string the base64 encoded PSBT that the other party sent as an offer to rescue funds
--rootkey string BIP32 HD root key of the wallet to use for signing the offer; leave empty to prompt for lnd 24 word aezeed
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for signoffer
--psbt string the base64 encoded PSBT that the other party sent as an offer to rescue funds
--rootkey string BIP32 HD root key of the wallet to use for signing the offer; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro signing the offer from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

@ -8,7 +8,7 @@ known anymore: no one knows what the balances were (there is no channel DB,
static channel backup did not work, force closing was not possible).
This means:
1. Hou have to find the peer (which, if you want to do a zombie recovery, was
1. You have to find the peer (which, if you want to do a zombie recovery, was
probably offline if you tried to recover)
2. You have to find a way to contact this peer (twitter, email, ...)
3. You and your peer will have to negotiate a closing state: you only know the
@ -46,7 +46,7 @@ Below image is a simplified version of the steps described below the image.
node, continue:
3. Send/upload the JSON file(s) to your node. If you open the JSON file(s), you
will see your own node ID (and contact info) and the peers'. [Download or
install chantools](https://github.com/guggero/chantools#installation).
install chantools](https://github.com/lightninglabs/chantools#installation).
Technically, you do not _need_ to install `chantools` on the same machine as
your node. Maybe you do not feel confident entering your seed words on your
node and want to do this someplace else.

@ -10,10 +10,12 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
)
@ -21,14 +23,14 @@ const (
lndInternalDerivationPath = "m/1017'/%d'/%d'/0/%d"
)
// BackupSingle is the information we want to dump from an lnd channel backup
// BackupMulti is the information we want to dump from a lnd channel backup
// multi file. See `chanbackup.Multi` for information about the fields.
type BackupMulti struct {
Version chanbackup.MultiBackupVersion
StaticBackups []BackupSingle
}
// BackupSingle is the information we want to dump from an lnd channel backup.
// BackupSingle is the information we want to dump from a lnd channel backup.
// See `chanbackup.Single` for information about the fields.
type BackupSingle struct {
Version chanbackup.SingleBackupVersion
@ -57,6 +59,7 @@ type OpenChannel struct {
FundingBroadcastHeight uint32
NumConfsRequired uint16
ChannelFlags lnwire.FundingFlag
ThawHeight uint32
IdentityPub string
Capacity btcutil.Amount
TotalMSatSent lnwire.MilliSatoshi
@ -66,6 +69,8 @@ type OpenChannel struct {
RemoteChanCfg ChannelConfig
LocalCommitment channeldb.ChannelCommitment
RemoteCommitment channeldb.ChannelCommitment
LocalCommitmentDebug ChannelDebugInfo
RemoteCommitmentDebug ChannelDebugInfo
RemoteCurrentRevocation string
RemoteNextRevocation string
FundingTxn string
@ -73,24 +78,38 @@ type OpenChannel struct {
RemoteShutdownScript lnwire.DeliveryAddress
}
// ChannelDebugInfo is a struct that holds additional information about an open
// or pending channel that is useful for debugging.
type ChannelDebugInfo struct {
ToLocalScript string
ToLocalAddr string
ToRemoteScript string
ToRemoteAddr string
}
// ClosedChannel is the information we want to dump from a closed channel in
// lnd's channel DB. See `channeldb.ChannelCloseSummary` for information about
// the fields.
type ClosedChannel struct {
ChanPoint string
ShortChanID lnwire.ShortChannelID
ChainHash chainhash.Hash
ClosingTXID string
RemotePub string
Capacity btcutil.Amount
CloseHeight uint32
SettledBalance btcutil.Amount
TimeLockedBalance btcutil.Amount
CloseType string
IsPending bool
RemoteCurrentRevocation string
RemoteNextRevocation string
LocalChanConfig ChannelConfig
ChanPoint string
ShortChanID lnwire.ShortChannelID
ChainHash chainhash.Hash
ClosingTXID string
RemotePub string
Capacity btcutil.Amount
CloseHeight uint32
SettledBalance btcutil.Amount
TimeLockedBalance btcutil.Amount
CloseType string
IsPending bool
RemoteCurrentRevocation string
RemoteNextRevocation string
LocalChanConfig ChannelConfig
NextLocalCommitHeight uint64
RemoteCommitTailHeight uint64
LastRemoteCommitSecret string
LocalUnrevokedCommitPoint string
HistoricalChannel *OpenChannel
}
// ChannelConfig is the information we want to dump from a channel
@ -119,68 +138,198 @@ func OpenChannelDump(channels []*channeldb.OpenChannel,
dumpChannels := make([]OpenChannel, len(channels))
for idx, channel := range channels {
var buf bytes.Buffer
if channel.FundingTxn != nil {
err := channel.FundingTxn.Serialize(&buf)
if err != nil {
return nil, err
}
openChan, err := openChannelDump(channel, params)
if err != nil {
return nil, fmt.Errorf("error converting to dump "+
"format: %w", err)
}
revPreimage, err := channel.RevocationProducer.AtIndex(
channel.LocalCommitment.CommitHeight,
)
dumpChannels[idx] = *openChan
}
return dumpChannels, nil
}
func openChannelDump(channel *channeldb.OpenChannel,
params *chaincfg.Params) (*OpenChannel, error) {
var buf bytes.Buffer
if channel.FundingTxn != nil {
err := channel.FundingTxn.Serialize(&buf)
if err != nil {
return nil, err
}
perCommitPoint := input.ComputeCommitmentPoint(revPreimage[:])
dumpChannels[idx] = OpenChannel{
ChanType: channel.ChanType,
ChainHash: channel.ChainHash,
FundingOutpoint: channel.FundingOutpoint.String(),
ShortChannelID: channel.ShortChannelID,
IsPending: channel.IsPending,
IsInitiator: channel.IsInitiator,
ChanStatus: channel.ChanStatus(),
FundingBroadcastHeight: channel.FundingBroadcastHeight,
NumConfsRequired: channel.NumConfsRequired,
ChannelFlags: channel.ChannelFlags,
IdentityPub: PubKeyToString(
channel.IdentityPub,
),
Capacity: channel.Capacity,
TotalMSatSent: channel.TotalMSatSent,
TotalMSatReceived: channel.TotalMSatReceived,
PerCommitPoint: PubKeyToString(perCommitPoint),
LocalChanCfg: ToChannelConfig(
params, channel.LocalChanCfg,
),
RemoteChanCfg: ToChannelConfig(
params, channel.RemoteChanCfg,
),
LocalCommitment: channel.LocalCommitment,
RemoteCommitment: channel.RemoteCommitment,
RemoteCurrentRevocation: PubKeyToString(
channel.RemoteCurrentRevocation,
),
RemoteNextRevocation: PubKeyToString(
channel.RemoteNextRevocation,
),
FundingTxn: hex.EncodeToString(buf.Bytes()),
LocalShutdownScript: channel.LocalShutdownScript,
RemoteShutdownScript: channel.RemoteShutdownScript,
}
}
return dumpChannels, nil
revPreimage, err := channel.RevocationProducer.AtIndex(
channel.LocalCommitment.CommitHeight,
)
if err != nil {
return nil, err
}
perCommitPoint := input.ComputeCommitmentPoint(revPreimage[:])
openChan := &OpenChannel{
ChanType: channel.ChanType,
ChainHash: channel.ChainHash,
FundingOutpoint: channel.FundingOutpoint.String(),
ShortChannelID: channel.ShortChannelID,
IsPending: channel.IsPending,
IsInitiator: channel.IsInitiator,
ChanStatus: channel.ChanStatus(),
FundingBroadcastHeight: channel.FundingBroadcastHeight,
NumConfsRequired: channel.NumConfsRequired,
ChannelFlags: channel.ChannelFlags,
ThawHeight: channel.ThawHeight,
IdentityPub: PubKeyToString(
channel.IdentityPub,
),
Capacity: channel.Capacity,
TotalMSatSent: channel.TotalMSatSent,
TotalMSatReceived: channel.TotalMSatReceived,
PerCommitPoint: PubKeyToString(perCommitPoint),
LocalChanCfg: ToChannelConfig(
params, channel.LocalChanCfg,
),
RemoteChanCfg: ToChannelConfig(
params, channel.RemoteChanCfg,
),
LocalCommitment: channel.LocalCommitment,
RemoteCommitment: channel.RemoteCommitment,
RemoteCurrentRevocation: PubKeyToString(
channel.RemoteCurrentRevocation,
),
RemoteNextRevocation: PubKeyToString(
channel.RemoteNextRevocation,
),
FundingTxn: hex.EncodeToString(buf.Bytes()),
LocalShutdownScript: channel.LocalShutdownScript,
RemoteShutdownScript: channel.RemoteShutdownScript,
}
localDebug, err := CollectDebugInfo(
channel, perCommitPoint, true, channel.IsInitiator, params,
)
if err != nil {
return nil, fmt.Errorf("error collecting local debug info: %w",
err)
}
remoteDebug, err := CollectDebugInfo(
channel, channel.RemoteCurrentRevocation, false,
!channel.IsInitiator, params,
)
if err != nil {
return nil, fmt.Errorf("error collecting remote debug info: %w",
err)
}
openChan.LocalCommitmentDebug = *localDebug
openChan.RemoteCommitmentDebug = *remoteDebug
return openChan, nil
}
// CollectDebugInfo collects the additional debug information for the given
// channel.
func CollectDebugInfo(channel *channeldb.OpenChannel,
commitPoint *btcec.PublicKey, ourCommit, initiator bool,
params *chaincfg.Params) (*ChannelDebugInfo, error) {
chanType := channel.ChanType
ourChanCfg := &channel.LocalChanCfg
theirChanCfg := &channel.RemoteChanCfg
leaseExpiry := channel.ThawHeight
keyRing := lnwallet.DeriveCommitmentKeys(
commitPoint, ourCommit, chanType, ourChanCfg, theirChanCfg,
)
// First, we create the script for the delayed "pay-to-self" output.
// This output has 2 main redemption clauses: either we can redeem the
// output after a relative block delay, or the remote node can claim
// the funds with the revocation key if we broadcast a revoked
// commitment transaction.
toLocalScript, err := lnwallet.CommitScriptToSelf(
chanType, initiator, keyRing.ToLocalKey, keyRing.RevocationKey,
uint32(ourChanCfg.CsvDelay), leaseExpiry,
)
if err != nil {
return nil, err
}
// Next, we create the script paying to the remote.
toRemoteScript, _, err := lnwallet.CommitScriptToRemote(
chanType, initiator, keyRing.ToRemoteKey, leaseExpiry,
)
if err != nil {
return nil, err
}
toLocalPkScript, err := txscript.ParsePkScript(toLocalScript.PkScript())
if err != nil {
return nil, err
}
toLocalAddr, err := toLocalPkScript.Address(params)
if err != nil {
return nil, err
}
toRemotePkScript, err := txscript.ParsePkScript(
toRemoteScript.PkScript(),
)
if err != nil {
return nil, err
}
toRemoteAddr, err := toRemotePkScript.Address(params)
if err != nil {
return nil, err
}
return &ChannelDebugInfo{
ToLocalScript: hex.EncodeToString(
toLocalScript.WitnessScriptToSign(),
),
ToLocalAddr: toLocalAddr.String(),
ToRemoteScript: hex.EncodeToString(
toRemoteScript.WitnessScriptToSign(),
),
ToRemoteAddr: toRemoteAddr.String(),
}, nil
}
// ClosedChannelDump converts the closed channels in the given channel DB into a
// dumpable format.
func ClosedChannelDump(channels []*channeldb.ChannelCloseSummary,
historicalChannels []*channeldb.OpenChannel,
params *chaincfg.Params) ([]ClosedChannel, error) {
dumpChannels := make([]ClosedChannel, len(channels))
for idx, channel := range channels {
var (
nextLocalHeight, remoteTailHeight uint64
lastRemoteSecret string
localUnrevokedCommitPoint *btcec.PublicKey
historicalChannel *OpenChannel
)
if channel.LastChanSyncMsg != nil {
msg := channel.LastChanSyncMsg
nextLocalHeight = msg.NextLocalCommitHeight
remoteTailHeight = msg.RemoteCommitTailHeight
lastRemoteSecret = hex.EncodeToString(
msg.LastRemoteCommitSecret[:],
)
localUnrevokedCommitPoint = msg.LocalUnrevokedCommitPoint
}
histChan := historicalChannels[idx]
if histChan != nil {
openChan, err := openChannelDump(histChan, params)
if err != nil {
return nil, fmt.Errorf("error converting to "+
"dump format: %w", err)
}
historicalChannel = openChan
}
dumpChannels[idx] = ClosedChannel{
ChanPoint: channel.ChanPoint.String(),
ShortChanID: channel.ShortChanID,
@ -204,6 +353,13 @@ func ClosedChannelDump(channels []*channeldb.ChannelCloseSummary,
LocalChanConfig: ToChannelConfig(
params, channel.LocalChanConfig,
),
NextLocalCommitHeight: nextLocalHeight,
RemoteCommitTailHeight: remoteTailHeight,
LastRemoteCommitSecret: lastRemoteSecret,
LocalUnrevokedCommitPoint: PubKeyToString(
localUnrevokedCommitPoint,
),
HistoricalChannel: historicalChannel,
}
}
return dumpChannels, nil

189
go.mod

@ -1,41 +1,50 @@
module github.com/guggero/chantools
module github.com/lightninglabs/chantools
go 1.19
go 1.21
require (
github.com/btcsuite/btcd v0.23.5-0.20230125025938-be056b0a0b2f
github.com/btcsuite/btcd v0.24.1-0.20240123000108-62e6af035ec5
github.com/btcsuite/btcd/btcec/v2 v2.3.2
github.com/btcsuite/btcd/btcutil v1.1.3
github.com/btcsuite/btcd/btcutil/psbt v1.1.5
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2
github.com/btcsuite/btcd/btcutil v1.1.5
github.com/btcsuite/btcd/btcutil/psbt v1.1.8
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
github.com/btcsuite/btcwallet v0.16.7
github.com/btcsuite/btcwallet v0.16.10-0.20240127010340-16b422a2e8bf
github.com/btcsuite/btcwallet/wallet/txrules v1.2.0
github.com/btcsuite/btcwallet/walletdb v1.4.0
github.com/coreos/bbolt v1.3.3
github.com/davecgh/go-spew v1.1.1
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
github.com/gogo/protobuf v1.3.2
github.com/gogo/protobuf v1.3.2 // indirect
github.com/hasura/go-graphql-client v0.9.1
github.com/lightninglabs/loop v0.23.0-beta
github.com/lightninglabs/loop v0.26.6-beta
github.com/lightninglabs/pool v0.6.2-beta.0.20230329135228-c3bffb52df3a
github.com/lightningnetwork/lnd v0.16.0-beta
github.com/lightningnetwork/lnd/kvdb v1.4.1
github.com/lightningnetwork/lnd/queue v1.1.0
github.com/lightningnetwork/lnd/ticker v1.1.0
github.com/lightningnetwork/lnd/tor v1.1.0
// The current version of lnd we are compatible with, mostly affects the
// commands that touch the channel DB and has an impact on the DB schema.
// NOTE: When updating this version, make sure to also update the string in
// cmd/chantools/root.go.
github.com/lightningnetwork/lnd v0.17.4-beta
github.com/lightningnetwork/lnd/kvdb v1.4.4
github.com/lightningnetwork/lnd/queue v1.1.1
github.com/lightningnetwork/lnd/ticker v1.1.1
github.com/lightningnetwork/lnd/tor v1.1.2
github.com/spf13/cobra v1.1.3
github.com/stretchr/testify v1.8.1
go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.1.0
golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1
github.com/stretchr/testify v1.8.4
go.etcd.io/bbolt v1.3.7
golang.org/x/crypto v0.21.0
golang.org/x/oauth2 v0.11.0
)
require github.com/tv42/zbase32 v0.0.0-20220222190657-f76a9fc892fa
require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/aead/siphash v1.0.1 // indirect
github.com/andybalholm/brotli v1.0.3 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2 // indirect
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 // indirect
@ -43,93 +52,106 @@ require (
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
github.com/btcsuite/winsvc v1.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/decred/dcrd/lru v1.0.0 // indirect
github.com/docker/cli v20.10.17+incompatible // indirect
github.com/docker/docker v24.0.9+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fergusstrange/embedded-postgres v1.10.0 // indirect
github.com/frankban/quicktest v1.11.2 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/golang-migrate/migrate/v4 v4.16.1 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.10.0 // indirect
github.com/jackc/pgconn v1.14.3 // indirect
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.1.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.8.1 // indirect
github.com/jackc/pgx/v4 v4.13.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.2 // indirect
github.com/jessevdk/go-flags v1.4.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/jrick/logrotate v1.0.0 // indirect
github.com/json-iterator/go v1.1.11 // indirect
github.com/juju/clock v1.0.0 // indirect
github.com/juju/collections v1.0.0 // indirect
github.com/juju/errors v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4 // indirect
github.com/juju/mgo/v2 v2.0.0 // indirect
github.com/juju/retry v1.0.0 // indirect
github.com/juju/utils/v3 v3.0.0 // indirect
github.com/juju/version/v2 v2.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kkdai/bstream v1.0.0 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/lib/pq v1.10.3 // indirect
github.com/lightninglabs/aperture v0.1.20-beta // indirect
github.com/lib/pq v1.10.7 // indirect
github.com/lightninglabs/aperture v0.1.21-beta.0.20230705004936-87bb996a4030 // indirect
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
github.com/lightninglabs/lndclient v0.16.0-10 // indirect
github.com/lightninglabs/loop/swapserverrpc v1.0.4 // indirect
github.com/lightninglabs/neutrino v0.15.0 // indirect
github.com/lightninglabs/lndclient v0.17.4-1 // indirect
github.com/lightninglabs/loop/swapserverrpc v1.0.5 // indirect
github.com/lightninglabs/neutrino v0.16.0 // indirect
github.com/lightninglabs/neutrino/cache v1.1.1 // indirect
github.com/lightninglabs/pool/auctioneerrpc v1.0.7 // indirect
github.com/lightningnetwork/lightning-onion v1.2.1-0.20221202012345-ca23184850a1 // indirect
github.com/lightningnetwork/lnd/clock v1.1.0 // indirect
github.com/lightningnetwork/lnd/healthcheck v1.2.2 // indirect
github.com/lightningnetwork/lnd/tlv v1.1.0 // indirect
github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f // indirect
github.com/lightningnetwork/lnd/clock v1.1.1 // indirect
github.com/lightningnetwork/lnd/healthcheck v1.2.3 // indirect
github.com/lightningnetwork/lnd/tlv v1.1.1 // indirect
github.com/ltcsuite/ltcd v0.0.0-20191228044241-92166e412499 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mholt/archiver/v3 v3.5.0 // indirect
github.com/miekg/dns v1.1.43 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nwaples/rardecode v1.1.2 // indirect
github.com/pierrec/lz4/v4 v4.1.8 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.1.12 // indirect
github.com/ory/dockertest/v3 v3.10.0 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.11.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.30.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rogpeppe/fastuuid v1.2.0 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.7.0 // indirect
github.com/sirupsen/logrus v1.9.2 // indirect
github.com/soheilhy/cmux v0.1.5 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
@ -140,31 +162,35 @@ require (
go.etcd.io/etcd/pkg/v3 v3.5.7 // indirect
go.etcd.io/etcd/raft/v3 v3.5.7 // indirect
go.etcd.io/etcd/server/v3 v3.5.7 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.25.0 // indirect
go.opentelemetry.io/otel v1.6.3 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.0.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.0.1 // indirect
go.opentelemetry.io/otel/sdk v1.0.1 // indirect
go.opentelemetry.io/otel/trace v1.6.3 // indirect
go.opentelemetry.io/proto/otlp v0.9.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 // indirect
go.opentelemetry.io/otel v1.20.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0 // indirect
go.opentelemetry.io/otel/metric v1.20.0 // indirect
go.opentelemetry.io/otel/sdk v1.3.0 // indirect
go.opentelemetry.io/otel/trace v1.20.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.17.0 // indirect
golang.org/x/exp v0.0.0-20221111094246-ab4555d3164f // indirect
golang.org/x/mod v0.6.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/term v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
golang.org/x/tools v0.2.0 // indirect
google.golang.org/appengine v1.6.6 // indirect
google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced // indirect
google.golang.org/grpc v1.41.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.9.1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/errgo.v1 v1.0.1 // indirect
gopkg.in/macaroon-bakery.v2 v2.0.1 // indirect
gopkg.in/macaroon-bakery.v2 v2.1.0 // indirect
gopkg.in/macaroon.v2 v2.1.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
@ -182,3 +208,8 @@ require (
nhooyr.io/websocket v1.8.7 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
)
// We want to format raw bytes as hex instead of base64. The forked version
// allows us to specify that as an option. This is required for the
// taproot-assets dependency to function properly.
replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display

1121
go.sum

File diff suppressed because it is too large Load Diff

@ -2,6 +2,7 @@ package lnd
import (
"bufio"
"errors"
"fmt"
"os"
"regexp"
@ -11,20 +12,47 @@ import (
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcwallet/snacl"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/lightningnetwork/lnd/aezeed"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnwallet"
"go.etcd.io/bbolt"
"golang.org/x/crypto/ssh/terminal"
)
const (
MnemonicEnvName = "AEZEED_MNEMONIC"
PassphraseEnvName = "AEZEED_PASSPHRASE"
PasswordEnvName = "WALLET_PASSWORD"
)
var (
numberDotsRegex = regexp.MustCompile(`[\d.\-\n\r\t]*`)
multipleSpaces = regexp.MustCompile(" [ ]+")
openCallbacks = &waddrmgr.OpenCallbacks{
ObtainSeed: noConsole,
ObtainPrivatePass: noConsole,
}
// Namespace from github.com/btcsuite/btcwallet/wallet/wallet.go.
WaddrmgrNamespaceKey = []byte("waddrmgr")
// Bucket names from github.com/btcsuite/btcwallet/waddrmgr/db.go.
mainBucketName = []byte("main")
masterPrivKeyName = []byte("mpriv")
cryptoPrivKeyName = []byte("cpriv")
masterHDPrivName = []byte("mhdpriv")
)
func noConsole() ([]byte, error) {
return nil, fmt.Errorf("wallet db requires console access")
}
// ReadAezeed reads an aezeed from the console or the environment variable.
func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time,
error) {
@ -67,6 +95,32 @@ func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time,
len(cipherSeedMnemonic), 24)
}
passphraseBytes, err := ReadPassphrase("doesn't have")
if err != nil {
return nil, time.Unix(0, 0), err
}
var mnemonic aezeed.Mnemonic
copy(mnemonic[:], cipherSeedMnemonic)
// If we're unable to map it back into the ciphertext, then either the
// mnemonic is wrong, or the passphrase is wrong.
cipherSeed, err := mnemonic.ToCipherSeed(passphraseBytes)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("failed to decrypt "+
"seed with passphrase: %w", err)
}
rootKey, err := hdkeychain.NewMaster(cipherSeed.Entropy[:], params)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("failed to derive " +
"master extended key")
}
return rootKey, cipherSeed.BirthdayTime(), nil
}
// ReadPassphrase reads a cipher seed passphrase from the console or the
// environment variable.
func ReadPassphrase(verb string) ([]byte, error) {
// Additionally, the user may have a passphrase, that will also need to
// be provided so the daemon can properly decipher the cipher seed.
// Try the environment variable first.
@ -85,14 +139,14 @@ func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time,
// The environment variable didn't contain anything, we'll read the
// passphrase from the terminal.
case passphrase == "":
fmt.Printf("Input your cipher seed passphrase (press enter " +
"if your seed doesn't have a passphrase): ")
fmt.Printf("Input your cipher seed passphrase (press enter "+
"if your seed %s a passphrase): ", verb)
var err error
passphraseBytes, err = terminal.ReadPassword(
int(syscall.Stdin), //nolint
)
if err != nil {
return nil, time.Unix(0, 0), err
return nil, err
}
fmt.Println()
@ -101,20 +155,176 @@ func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time,
passphraseBytes = []byte(passphrase)
}
var mnemonic aezeed.Mnemonic
copy(mnemonic[:], cipherSeedMnemonic)
return passphraseBytes, nil
}
// If we're unable to map it back into the ciphertext, then either the
// mnemonic is wrong, or the passphrase is wrong.
cipherSeed, err := mnemonic.ToCipherSeed(passphraseBytes)
// PasswordFromConsole reads a password from the console or stdin.
func PasswordFromConsole(userQuery string) ([]byte, error) {
// Read from terminal (if there is one).
if terminal.IsTerminal(int(syscall.Stdin)) { //nolint
fmt.Print(userQuery)
pw, err := terminal.ReadPassword(int(syscall.Stdin)) //nolint
if err != nil {
return nil, err
}
fmt.Println()
return pw, nil
}
// Read from stdin as a fallback.
reader := bufio.NewReader(os.Stdin)
pw, err := reader.ReadBytes('\n')
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("failed to decrypt "+
"seed with passphrase: %w", err)
return nil, err
}
return pw, nil
}
// OpenWallet opens a lnd compatible wallet and returns it, along with the
// private wallet password.
func OpenWallet(walletDbPath string,
chainParams *chaincfg.Params) (*wallet.Wallet, []byte, func() error,
error) {
var (
publicWalletPw = lnwallet.DefaultPublicPassphrase
privateWalletPw = lnwallet.DefaultPrivatePassphrase
err error
)
// To automate things with chantools, we also offer reading the wallet
// password from environment variables.
pw := []byte(strings.TrimSpace(os.Getenv(PasswordEnvName)))
// Because we cannot differentiate between an empty and a non-existent
// environment variable, we need a special character that indicates that
// no password should be used. We use a single dash (-) for that as that
// would be too short for an explicit password anyway.
switch {
// The user indicated in the environment variable that no passphrase
// should be used. We don't set any value.
case string(pw) == "-":
// The environment variable didn't contain anything, we'll read the
// passphrase from the terminal.
case len(pw) == 0:
pw, err = PasswordFromConsole("Input wallet password: ")
if err != nil {
return nil, nil, nil, err
}
if len(pw) > 0 {
publicWalletPw = pw
privateWalletPw = pw
}
// There was a password in the environment, just use it directly.
default:
publicWalletPw = pw
privateWalletPw = pw
}
// Try to load and open the wallet.
db, err := walletdb.Open(
"bdb", lncfg.CleanAndExpandPath(walletDbPath), false,
DefaultOpenTimeout,
)
if errors.Is(err, bbolt.ErrTimeout) {
return nil, nil, nil, fmt.Errorf("error opening wallet " +
"database, make sure lnd is not running and holding " +
"the exclusive lock on the wallet")
}
rootKey, err := hdkeychain.NewMaster(cipherSeed.Entropy[:], params)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("failed to derive " +
"master extended key")
return nil, nil, nil, fmt.Errorf("error opening wallet "+
"database: %w", err)
}
return rootKey, cipherSeed.BirthdayTime(), nil
w, err := wallet.Open(db, publicWalletPw, openCallbacks, chainParams, 0)
if err != nil {
_ = db.Close()
return nil, nil, nil, fmt.Errorf("error opening wallet %w", err)
}
// Start and unlock the wallet.
w.Start()
err = w.Unlock(privateWalletPw, nil)
if err != nil {
w.Stop()
_ = db.Close()
return nil, nil, nil, err
}
cleanup := func() error {
w.Stop()
if err := db.Close(); err != nil {
return err
}
return nil
}
return w, privateWalletPw, cleanup, nil
}
// DecryptWalletRootKey decrypts a lnd compatible wallet's root key.
func DecryptWalletRootKey(db walletdb.DB,
privatePassphrase []byte) ([]byte, error) {
// Step 1: Load the encryption parameters and encrypted keys from the
// database.
var masterKeyPrivParams []byte
var cryptoKeyPrivEnc []byte
var masterHDPrivEnc []byte
err := walletdb.View(db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(WaddrmgrNamespaceKey)
if ns == nil {
return fmt.Errorf("namespace '%s' does not exist",
WaddrmgrNamespaceKey)
}
mainBucket := ns.NestedReadBucket(mainBucketName)
if mainBucket == nil {
return fmt.Errorf("bucket '%s' does not exist",
mainBucketName)
}
val := mainBucket.Get(masterPrivKeyName)
if val != nil {
masterKeyPrivParams = make([]byte, len(val))
copy(masterKeyPrivParams, val)
}
val = mainBucket.Get(cryptoPrivKeyName)
if val != nil {
cryptoKeyPrivEnc = make([]byte, len(val))
copy(cryptoKeyPrivEnc, val)
}
val = mainBucket.Get(masterHDPrivName)
if val != nil {
masterHDPrivEnc = make([]byte, len(val))
copy(masterHDPrivEnc, val)
}
return nil
})
if err != nil {
return nil, err
}
// Step 2: Unmarshal the master private key parameters and derive
// key from passphrase.
var masterKeyPriv snacl.SecretKey
if err := masterKeyPriv.Unmarshal(masterKeyPrivParams); err != nil {
return nil, err
}
if err := masterKeyPriv.DeriveKey(&privatePassphrase); err != nil {
return nil, err
}
// Step 3: Decrypt the keys in the correct order.
cryptoKeyPriv := &snacl.CryptoKey{}
cryptoKeyPrivBytes, err := masterKeyPriv.Decrypt(cryptoKeyPrivEnc)
if err != nil {
return nil, err
}
copy(cryptoKeyPriv[:], cryptoKeyPrivBytes)
return cryptoKeyPriv.Decrypt(masterHDPrivEnc)
}

@ -113,10 +113,8 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
NotifyWhenOffline: func(peerPubKey [33]byte) <-chan struct{} {
return make(chan struct{})
},
SelfNodeAnnouncement: func(bool) (lnwire.NodeAnnouncement,
error) {
return lnwire.NodeAnnouncement{}, nil
FetchSelfAnnouncement: func() lnwire.NodeAnnouncement {
return lnwire.NodeAnnouncement{}
},
ProofMatureDelta: 0,
TrickleDelay: time.Millisecond * 50,
@ -183,7 +181,7 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
key.SerializeCompressed())
return nil
},
GenNodeAnnouncement: func(b bool,
GenNodeAnnouncement: func(
modifier ...netann.NodeAnnModifier) (
lnwire.NodeAnnouncement, error) {

@ -4,6 +4,9 @@ import (
"bytes"
"fmt"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightninglabs/chantools/dump"
"github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/keychain"
@ -35,3 +38,32 @@ func CreateChannelBackup(db *channeldb.DB, multiFile *chanbackup.MultiFile,
}
return nil
}
// ExtractChannel extracts a single channel from the given backup file and
// returns it as a dump.BackupSingle struct.
func ExtractChannel(extendedKey *hdkeychain.ExtendedKey,
chainParams *chaincfg.Params, multiFilePath,
channelPoint string) (*dump.BackupSingle, error) {
multiFile := chanbackup.NewMultiFile(multiFilePath)
keyRing := &HDKeyRing{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
multi, err := multiFile.ExtractMulti(keyRing)
if err != nil {
return nil, fmt.Errorf("could not extract multi file: %w", err)
}
channels := dump.BackupDump(multi, chainParams)
for _, channel := range channels {
channel := channel
if channel.FundingOutpoint == channelPoint {
return &channel, nil
}
}
return nil, fmt.Errorf("channel %s not found in backup", channelPoint)
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save