Compare commits

...

80 Commits

Author SHA1 Message Date
Oliver Gugger d173f8ca1c
Merge pull request #138 from starius/update-lnd-v0.18.0-beta
update dependencies (LND v0.18.0-beta and co)
1 week ago
Boris Nagaev 65d6e52299
test: don't modify test data
Copy a file to temp directory before opening it.

Fix https://github.com/lightninglabs/chantools/issues/139
1 week ago
Boris Nagaev 551e2a056a
lint: update configs, golangci-lint to v1.59.0
Remove deprecated and deactivated linters from .golangci.yml.

Ignore too noisy linters: protogetter, depguard, mnd.

Fix linter warnings.

Use timeout instead of deadline in .golangci.yml.
1 week ago
Boris Nagaev 7bd21fba58
update dependencies (LND v0.18.0-beta and co)
Updated versions:

  - lnd: v0.17.4-beta -> v0.18.0-beta
  - lnd/kvdb: v1.4.4 -> v1.4.8
  - lnd/tor: v1.1.2 -> v1.1.3
  - lnd/healthcheck: v1.2.3 -> v1.2.4
  - lnd/tlv: v1.1.1 -> v1.2.6
  - neutrino: v0.16.0 -> v0.16.1-0.20240425105051-602843d34ffd
  - neutrino/cache: v1.1.1 -> v1.1.2
  - lndclient: v0.17.4-1 -> v0.18.0-2
  - loop: v0.26.6-beta -> v0.28.3
  - loop/swapserverrpc: v1.0.5 -> v1.0.8
  - pool: v0.6.2-beta.0.20230329135228-c3bffb52df3a ->
        v0.6.5-beta.0.20240531084722-4000ec802aaa
  - pool/auctioneerrpc: v1.0.7 -> v1.1.2
  - aperture: v0.1.21-beta.0.20230705004936-87bb996a4030 -> v0.3.2-beta
  - replace of protobuf: v1.30.0-hex-display -> v1.33.0-hex-display
  - Go: 1.21 -> 1.22.3

Pool version v0.6.5-beta.0.20240531084722-4000ec802aaa was used because it uses
LND v0.18.0-beta, not v0.18.0-beta.rc3.

Fixed imports and API changes.

Updated Go version in README.

Updated LND version in cmd/chantools/root.go.
Ran `make docs`.
1 week ago
Oliver Gugger 1f43c4a0ad
Merge pull request #142 from lightninglabs/signpsbt-multi
signpsbt: allow signing multiple inputs
1 week ago
Oliver Gugger 32fd44dbe1
signpsbt: allow signing multiple inputs
This commit fixes an issue where the signpsbt sub command was only able
to sign for a single input.
To avoid attempting to double sign an input that already has a partial
signature, we also add a check instead of erroring out.
1 week ago
Oliver Gugger 85f207c58f
Merge pull request #137 from hieblmi/gitignore-idea
gitignore: .idea folder
1 month ago
Slyghtning da904ae1d7
gitignore: .idea folder 1 month 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
2 months 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>
2 months ago
Oliver Gugger cc284baa67
Merge pull request #132 from lightninglabs/triggerforceclose
triggerforceclose: make compatible with all nodes, add Tor support
2 months ago
Oliver Gugger 6acc81815e
root: bump version to v0.13.1 2 months ago
Oliver Gugger e3285daf5b
triggerforceclose: support Tor connections 2 months ago
Oliver Gugger 179773fdb9
triggerforceclose: make cmd compatible with all nodes 2 months 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
2 months 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>
2 months ago
Oliver Gugger d07251df9c
Merge pull request #129 from lightninglabs/fix-signpsbt
signpsbt+lnd: fix signing for P2WKH
3 months ago
Oliver Gugger 7f840cf03b
signpsbt+lnd: fix signing for P2WKH 3 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
3 months ago
Oliver Gugger 24cd530c65
make: update build targets due to sqlite 3 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>
3 months ago
Oliver Gugger 997d86cd84
Merge pull request #124 from lightninglabs/createwallet
Add `createwallet` and `signpsbt` subcommands
3 months ago
Oliver Gugger 676ba60197
Merge pull request #113 from sputn1ck/recoverloopin_sqlite
`recoverloopin`: Sqlite option
3 months ago
sputn1ck 3e419da317
recoverloopin: add sqlite option
This commit will allow to recover loop ins that have been made with
sqlite.
3 months ago
sputn1ck 6a81614b1b
go.mod: update loop to v0.26.6-beta 3 months ago
Oliver Gugger 71b824e105
README+doc: update all docs 3 months ago
Oliver Gugger 1a46f9099f
root: bump version to v0.13.0 3 months ago
Oliver Gugger 9f8484bb89
cmd/chantools: add signpsbt subcommand 3 months ago
Oliver Gugger e80dcbfb67
lnd+cmd/chantools: add AddPartialSignatureForPrivateKey to signer 3 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.
3 months ago
Oliver Gugger 37179e5215
lnd+cmd/chantools: extract functions from walletinfo 3 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.
3 months ago
Oliver Gugger a3a00d410a
lnd: extract ReadPassphrase into own function 3 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
4 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>
4 months ago
Oliver Gugger c75f9ff91a
Merge pull request #121 from sputn1ck/signmessage
signmessage: add signmessage cmd
5 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`.
5 months ago
Oliver Gugger cf4cabbd2a
Merge pull request #120 from ziggie1984/master
Fix Partial Signature Signing.
5 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.
5 months ago
Oliver Gugger a0e5f0613d
Merge pull request #118 from Tetrix42/doublespend_rbf
doublespendinputs: allow RBF per default
5 months ago
Felix Passenberg d9af0e36e5
doublespendinputs: remove RBF argument, RBF always on 5 months ago
Felix Passenberg 3b50a5ce16
doublespendinputs: allow RBF per default 5 months ago
Oliver Gugger d5d5a91430
Merge pull request #117 from lightninglabs/zombie-matching
zombierecovery: add --matchonly flag to makeoffer, --numkeys to preparekeys
5 months ago
Oliver Gugger 82a03a65ef
README+root: bump version to v0.12.2 5 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.
5 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.
5 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
6 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.
6 months ago
Oliver Gugger 399a23adba
Merge pull request #107 from lightninglabs/cli-cleanup
Support P2TR as sweep/change address everywhere
6 months ago
Oliver Gugger a05962e03e
multi: standardize sweep/change addr support 6 months ago
Oliver Gugger 7e3ea44fd4
multi: standardize address checks 6 months ago
Oliver Gugger 53085d34d0
btc: make API errors more verbose 6 months ago
Oliver Gugger d830ebe57a
multi: use default API URLs for testnet and regtest 6 months ago
Oliver Gugger 858995a317
Merge pull request #109 from ziggie1984/bug-fix-anchor-amt
pullanchor: account for all anchor outputs.
6 months ago
ziggie b777d4436d
pullanchor: account for all anchor outputs. 6 months ago
Oliver Gugger 5cf7fd60c4
multi: clean up after PRs 6 months ago
Oliver Gugger 92fdb156e0
Merge pull request #100 from lightninglabs/pullanchor
multi: add new pullanchor command
6 months ago
Oliver Gugger 2abc29d01d
Merge pull request #106 from lightninglabs/sweepremoteclosed-taproot-channels
sweepremoteclosed: add support for simple taproot channels
6 months ago
Oliver Gugger fd18186a82
Merge pull request #91 from lightninglabs/sweeptimelockmanual-backup-file
sweeptimelockmanual: allow specifying the backup file directly
6 months ago
Oliver Gugger 7227c7f101
multi: add new pullanchor command 6 months ago
Oliver Gugger 801f881274
sweepremoteclosed: add support for simple taproot channels 6 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>
8 months ago
Yuck Fou 55208e0218
Update zombierecovery.md
Fix typo: Hou -> You
8 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.
8 months ago
Oliver Gugger dee18ed80c
sweeptimelockmanual: rename variable 8 months ago
Oliver Gugger 5bc49376a3
sweeptimelock: make start CSV timeout+channels configurable 8 months ago
Oliver Gugger 3044d9f796
lnd: add ExtractChannel function 8 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
8 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>
8 months ago

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

1
.gitignore vendored

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

@ -1,6 +1,6 @@
run: run:
# timeout for analysis # timeout for analysis
deadline: 4m timeout: 4m
linters-settings: linters-settings:
govet: govet:
@ -12,7 +12,7 @@ linters-settings:
whitespace: whitespace:
multi-func: true multi-func: true
multi-if: true multi-if: true
tagliatelle: tagliatelle:
case: case:
rules: rules:
json: snake json: snake
@ -31,17 +31,15 @@ linters:
- gochecknoglobals - gochecknoglobals
- gosec - gosec
- funlen - funlen
- maligned
- varnamelen - varnamelen
- wrapcheck - wrapcheck
- testpackage - testpackage
- gomnd - gomnd
- goerr113 - err113
- exhaustruct - exhaustruct
- forbidigo - forbidigo
- gocognit - gocognit
- nestif - nestif
- ifshort
- wsl - wsl
- cyclop - cyclop
- gocyclo - gocyclo
@ -53,16 +51,9 @@ linters:
- noctx - noctx
- gofumpt - gofumpt
- exhaustive - exhaustive
- protogetter
# deprecated - depguard
- interfacer - mnd
- scopelint
- golint
- exhaustivestruct
- nosnakecase
- deadcode
- structcheck
- varcheck
issues: issues:
exclude-rules: exclude-rules:

@ -20,14 +20,13 @@ VERSION_TAG = $(shell git describe --tags)
VERSION_CHECK = @$(call print, "Building master with date version tag") VERSION_CHECK = @$(call print, "Building master with date version tag")
BUILD_SYSTEM = darwin-amd64 \ BUILD_SYSTEM = darwin-amd64 \
darwin-arm64 \
linux-386 \ linux-386 \
linux-amd64 \ linux-amd64 \
linux-armv6 \ linux-armv6 \
linux-armv7 \ linux-armv7 \
linux-arm64 \ linux-arm64 \
windows-386 \ windows-amd64
windows-amd64 \
windows-arm
# By default we will build all systems. But with the 'sys' tag, a specific # 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 # system can be specified. This is useful to release for a subset of

@ -30,7 +30,7 @@ Example (make sure you always use the latest version!):
```shell ```shell
$ cd /tmp $ cd /tmp
$ wget -O chantools.tar.gz https://github.com/lightninglabs/chantools/releases/download/v0.12.0/chantools-linux-amd64-v0.12.0.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 $ tar -zxvf chantools.tar.gz
$ sudo mv chantools-*/chantools /usr/local/bin/ $ sudo mv chantools-*/chantools /usr/local/bin/
``` ```
@ -39,7 +39,7 @@ $ sudo mv chantools-*/chantools /usr/local/bin/
If there isn't a pre-built binary for your operating system or architecture 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 available or you want to build `chantools` from source for another reason, you
need to make sure you have `go 1.21.x` (or later) and `make` installed and can need to make sure you have `go 1.22.3` (or later) and `make` installed and can
then run the following commands: then run the following commands:
```bash ```bash
@ -107,7 +107,8 @@ Scenarios:
Another reason might be that the peer is a CLN node with a specific version 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 that doesn't react to force close requests normally. You can use the
[`chantools triggerforceclose` command](doc/chantools_triggerforceclose.md) in [`chantools triggerforceclose` command](doc/chantools_triggerforceclose.md) in
that case (ONLY works with CLN peers of a certain version). 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? ## What should I NEVER do?
@ -186,7 +187,7 @@ compacting the DB).
in the user's home directory) is safely moved away (or the whole folder in the user's home directory) is safely moved away (or the whole folder
renamed) before continuing.<br/> renamed) before continuing.<br/>
To start the on-chain recovery, [follow the sub step "Starting On-Chain To start the on-chain recovery, [follow the sub step "Starting On-Chain
Recovery" of this guide][2]. Recovery" of this guide][recovery].
Don't follow the whole guide, only this single chapter! Don't follow the whole guide, only this single chapter!
<br/><br/> <br/><br/>
This step is completed once the `lncli getinfo` command shows both This step is completed once the `lncli getinfo` command shows both
@ -212,7 +213,7 @@ compacting the DB).
channels! channels!
If the list stays un-changed for several hours, it means not all channels If the list stays un-changed for several hours, it means not all channels
could be restored using this method. could be restored using this method.
[One explanation can be found here.][1] [One explanation can be found here.][safety-zombie]
4. **Install chantools**: To try to recover the remaining channels, we are going 4. **Install chantools**: To try to recover the remaining channels, we are going
to use `chantools`. to use `chantools`.
@ -410,6 +411,7 @@ Available Commands:
chanbackup Create a channel.backup file from a channel database chanbackup Create a channel.backup file from a channel database
closepoolaccount Tries to close a Pool account that has expired closepoolaccount Tries to close a Pool account that has expired
compactdb Create a copy of a channel.db file in safe/read-only mode 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 deletepayments Remove all (failed) payments from a channel DB
derivekey Derive a key with a specific derivation path 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. 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.
@ -423,17 +425,20 @@ Available Commands:
forceclose Force-close the last state that is in the channel.db provided 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 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 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 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 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 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 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 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 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 summary Compile a summary about the current state of channels
sweeptimelock Sweep the force-closed state after the time lock has expired 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 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 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 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 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 zombierecovery Try rescuing funds stuck in channels with zombie nodes
@ -468,6 +473,7 @@ Legend:
| [chanbackup](doc/chantools_chanbackup.md) | :pencil: Extract a `channel.backup` file from a `channel.db` file | | [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 | | [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 | | [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 | | [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 | | [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 | | [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 |
@ -481,17 +487,20 @@ Legend:
| [forceclose](doc/chantools_forceclose.md) | :pencil: (:skull: :warning:) Publish an old channel state from a `channel.db` file | | [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 | | [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 | | [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 | | [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 | | [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 | | [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 | | [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) | | [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 | | [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 | | [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 | | [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`) | | [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 | | [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 certain CLN peers to force close a channel that don't react to normal SCB recovery requests | | [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 | | [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 | | [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]) | | [zombierecovery](doc/chantools_zombierecovery.md) | :pencil: Cooperatively rescue funds from channels where normal recovery is not possible (see [full guide here][zombie-recovery]) |
@ -510,4 +519,4 @@ Legend:
[discussions]: https://github.com/lightningnetwork/lnd/discussions [discussions]: https://github.com/lightningnetwork/lnd/discussions
[zombie-recovery]: doc/zombierecovery.md [zombie-recovery]: doc/zombierecovery.md

@ -3,8 +3,8 @@
package bip39 package bip39
import ( import (
"fmt"
"hash/crc32" "hash/crc32"
"strconv"
"strings" "strings"
) )
@ -14,7 +14,7 @@ func init() { //nolint:gochecknoinits
// $ crc32 english.txt // $ crc32 english.txt
// c1dbd296 // c1dbd296
checksum := crc32.ChecksumIEEE([]byte(english)) checksum := crc32.ChecksumIEEE([]byte(english))
if fmt.Sprintf("%x", checksum) != "c1dbd296" { if strconv.FormatUint(uint64(checksum), 16) != "c1dbd296" {
panic("english checksum invalid") panic("english checksum invalid")
} }
} }

@ -25,9 +25,9 @@ const (
type KeyExporter interface { type KeyExporter interface {
Header() string Header() string
Format(*hdkeychain.ExtendedKey, *chaincfg.Params, string, uint32, Format(hdKey *hdkeychain.ExtendedKey, params *chaincfg.Params,
uint32) (string, error) path string, branch, index uint32) (string, error)
Trailer(uint32) string Trailer(birthdayBlock uint32) string
} }
// ParseFormat parses the given format name and returns its associated print // ParseFormat parses the given format name and returns its associated print
@ -67,7 +67,7 @@ func ExportKeys(extendedKey *hdkeychain.ExtendedKey, strPaths []string,
path := paths[idx] path := paths[idx]
// External branch first (<DerivationPath>/0/i). // External branch first (<DerivationPath>/0/i).
for i := uint32(0); i < recoveryWindow; i++ { for i := range recoveryWindow {
path := append(path, 0, i) path := append(path, 0, i)
derivedKey, err := lnd.DeriveChildren(extendedKey, path) derivedKey, err := lnd.DeriveChildren(extendedKey, path)
if err != nil { if err != nil {
@ -83,7 +83,7 @@ func ExportKeys(extendedKey *hdkeychain.ExtendedKey, strPaths []string,
} }
// Now the internal branch (<DerivationPath>/1/i). // Now the internal branch (<DerivationPath>/1/i).
for i := uint32(0); i < recoveryWindow; i++ { for i := range recoveryWindow {
path := append(path, 1, i) path := append(path, 1, i)
derivedKey, err := lnd.DeriveChildren(extendedKey, path) derivedKey, err := lnd.DeriveChildren(extendedKey, path)
if err != nil { if err != nil {
@ -254,7 +254,7 @@ func (p *Electrum) Header() string {
} }
func (p *Electrum) Format(hdKey *hdkeychain.ExtendedKey, func (p *Electrum) Format(hdKey *hdkeychain.ExtendedKey,
params *chaincfg.Params, path string, branch, index uint32) (string, params *chaincfg.Params, path string, _, _ uint32) (string,
error) { error) {
privKey, err := hdKey.ECPrivKey() privKey, err := hdKey.ECPrivKey()
@ -285,7 +285,7 @@ func (d *Descriptors) Header() string {
} }
func (d *Descriptors) Format(hdKey *hdkeychain.ExtendedKey, func (d *Descriptors) Format(hdKey *hdkeychain.ExtendedKey,
params *chaincfg.Params, path string, branch, index uint32) (string, params *chaincfg.Params, _ string, _, _ uint32) (string,
error) { error) {
privKey, err := hdKey.ECPrivKey() privKey, err := hdKey.ECPrivKey()

@ -19,7 +19,7 @@ func descriptorSumPolymod(symbols []uint64) uint64 {
for _, value := range symbols { for _, value := range symbols {
top := chk >> 35 top := chk >> 35
chk = (chk&0x7ffffffff)<<5 ^ value chk = (chk&0x7ffffffff)<<5 ^ value
for i := 0; i < 5; i++ { for i := range 5 {
if (top>>i)&1 != 0 { if (top>>i)&1 != 0 {
chk ^= generator[i] chk ^= generator[i]
} }
@ -57,7 +57,7 @@ func DescriptorSumCreate(s string) string {
symbols := append(descriptorSumExpand(s), 0, 0, 0, 0, 0, 0, 0, 0) symbols := append(descriptorSumExpand(s), 0, 0, 0, 0, 0, 0, 0, 0)
checksum := descriptorSumPolymod(symbols) ^ 1 checksum := descriptorSumPolymod(symbols) ^ 1
builder := strings.Builder{} builder := strings.Builder{}
for i := 0; i < 8; i++ { for i := range 8 {
builder.WriteByte(checksumCharset[(checksum>>(5*(7-i)))&31]) builder.WriteByte(checksumCharset[(checksum>>(5*(7-i)))&31])
} }
return s + "#" + builder.String() return s + "#" + builder.String()

@ -103,7 +103,7 @@ func (a *ExplorerAPI) Outpoint(addr string) (*TX, int, error) {
} }
} }
return nil, 0, fmt.Errorf("no tx found") return nil, 0, errors.New("no tx found")
} }
func (a *ExplorerAPI) Spends(addr string) ([]*TX, error) { func (a *ExplorerAPI) Spends(addr string) ([]*TX, error) {
@ -193,16 +193,20 @@ func (a *ExplorerAPI) Address(outpoint string) (string, error) {
} }
func (a *ExplorerAPI) PublishTx(rawTxHex string) (string, error) { func (a *ExplorerAPI) PublishTx(rawTxHex string) (string, error) {
url := fmt.Sprintf("%s/tx", a.BaseURL) url := a.BaseURL + "/tx"
resp, err := http.Post(url, "text/plain", strings.NewReader(rawTxHex)) resp, err := http.Post(url, "text/plain", strings.NewReader(rawTxHex))
if err != nil { 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() defer resp.Body.Close()
body := new(bytes.Buffer) body := new(bytes.Buffer)
_, err = body.ReadFrom(resp.Body) _, err = body.ReadFrom(resp.Body)
if err != nil { 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 return body.String(), nil
} }
@ -210,20 +214,29 @@ func (a *ExplorerAPI) PublishTx(rawTxHex string) (string, error) {
func fetchJSON(url string, target interface{}) error { func fetchJSON(url string, target interface{}) error {
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { 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() defer resp.Body.Close()
body := new(bytes.Buffer) body := new(bytes.Buffer)
_, err = body.ReadFrom(resp.Body) _, err = body.ReadFrom(resp.Body)
if err != nil { 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) err = json.Unmarshal(body.Bytes(), target)
if err != nil { if err != nil {
if body.String() == "Transaction not found" { if body.String() == "Transaction not found" {
return ErrTxNotFound 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
} }

@ -7,13 +7,12 @@ import (
"github.com/lightninglabs/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) { log btclog.Logger) (*dataformat.SummaryEntryFile, error) {
summaryFile := &dataformat.SummaryEntryFile{ summaryFile := &dataformat.SummaryEntryFile{
Channels: channels, Channels: channels,
} }
api := &ExplorerAPI{BaseURL: apiURL}
for idx, channel := range channels { for idx, channel := range channels {
tx, err := api.Transaction(channel.FundingTXID) tx, err := api.Transaction(channel.FundingTXID)

@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
@ -50,12 +51,12 @@ func (c *chanBackupCommand) Execute(_ *cobra.Command, _ []string) error {
// Check that we have a backup file. // Check that we have a backup file.
if c.MultiFile == "" { if c.MultiFile == "" {
return fmt.Errorf("backup file is required") return errors.New("backup file is required")
} }
// Check that we have a channel DB. // Check that we have a channel DB.
if c.ChannelDB == "" { if c.ChannelDB == "" {
return fmt.Errorf("channel DB is required") return errors.New("channel DB is required")
} }
db, err := lnd.OpenDB(c.ChannelDB, true) db, err := lnd.OpenDB(c.ChannelDB, true)
if err != nil { if err != nil {

@ -3,6 +3,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
@ -10,7 +11,6 @@ import (
"github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightninglabs/pool/account" "github.com/lightninglabs/pool/account"
"github.com/lightninglabs/pool/poolscript" "github.com/lightninglabs/pool/poolscript"
@ -89,7 +89,9 @@ obtained by running 'pool accounts list' `,
"API instead of just printing the TX", "API instead of just printing the TX",
) )
cc.cmd.Flags().StringVar( 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().Uint32Var( cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+ &cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
@ -125,8 +127,12 @@ func (c *closePoolAccountCommand) Execute(_ *cobra.Command, _ []string) error {
} }
// Make sure sweep addr is set. // Make sure sweep addr is set.
if c.SweepAddr == "" { err = lnd.CheckAddress(
return fmt.Errorf("sweep addr is required") c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
} }
// Parse account outpoint and auctioneer key. // Parse account outpoint and auctioneer key.
@ -161,11 +167,21 @@ func closePoolAccount(extendedKey *hdkeychain.ExtendedKey, apiURL string,
sweepAddr string, publish bool, feeRate uint32, minExpiry, sweepAddr string, publish bool, feeRate uint32, minExpiry,
maxNumBlocks, maxNumAccounts, maxNumBatchKeys uint32) error { maxNumBlocks, maxNumAccounts, maxNumBatchKeys uint32) error {
signer := &lnd.Signer{ var (
ExtendedKey: extendedKey, estimator input.TxWeightEstimator
ChainParams: chainParams, 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()) tx, err := api.Transaction(outpoint.Hash.String())
if err != nil { if err != nil {
@ -241,7 +257,6 @@ func closePoolAccount(extendedKey *hdkeychain.ExtendedKey, apiURL string,
// Calculate the fee based on the given fee rate and our weight // Calculate the fee based on the given fee rate and our weight
// estimation. // estimation.
var ( var (
estimator input.TxWeightEstimator
prevOutFetcher = txscript.NewCannedPrevOutputFetcher( prevOutFetcher = txscript.NewCannedPrevOutputFetcher(
pkScript, sweepValue, pkScript, sweepValue,
) )
@ -277,15 +292,10 @@ func closePoolAccount(extendedKey *hdkeychain.ExtendedKey, apiURL string,
signDesc.HashType = txscript.SigHashDefault signDesc.HashType = txscript.SigHashDefault
signDesc.SignMethod = input.TaprootScriptSpendSignMethod signDesc.SignMethod = input.TaprootScriptSpendSignMethod
} }
estimator.AddP2WKHOutput()
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight())) totalFee := feeRateKWeight.FeeForWeight(estimator.Weight())
// Add our sweep destination output. // Add our sweep destination output.
sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams)
if err != nil {
return err
}
sweepTx.TxOut = []*wire.TxOut{{ sweepTx.TxOut = []*wire.TxOut{{
Value: sweepValue - int64(totalFee), Value: sweepValue - int64(totalFee),
PkScript: sweepScript, PkScript: sweepScript,
@ -358,7 +368,7 @@ func bruteForceAccountScript(accountBaseKey *hdkeychain.ExtendedKey,
maxNumBatchKeys uint32, targetScript []byte) (*poolAccount, error) { maxNumBatchKeys uint32, targetScript []byte) (*poolAccount, error) {
// The outermost loop is over the possible accounts. // The outermost loop is over the possible accounts.
for i := uint32(0); i < maxNumAccounts; i++ { for i := range maxNumAccounts {
accountExtendedKey, err := accountBaseKey.DeriveNonStandard(i) accountExtendedKey, err := accountBaseKey.DeriveNonStandard(i)
if err != nil { if err != nil {
return nil, fmt.Errorf("error deriving account key: "+ return nil, fmt.Errorf("error deriving account key: "+
@ -421,7 +431,7 @@ func bruteForceAccountScript(accountBaseKey *hdkeychain.ExtendedKey,
log.Debugf("Tried account index %d of %d", i, maxNumAccounts) log.Debugf("Tried account index %d of %d", i, maxNumAccounts)
} }
return nil, fmt.Errorf("account script not derived") return nil, errors.New("account script not derived")
} }
func fastScript(keyIndex, expiryFrom, expiryTo uint32, traderKey, auctioneerKey, func fastScript(keyIndex, expiryFrom, expiryTo uint32, traderKey, auctioneerKey,
@ -433,7 +443,7 @@ func fastScript(keyIndex, expiryFrom, expiryTo uint32, traderKey, auctioneerKey,
return nil, err return nil, err
} }
if script.Class() != txscript.WitnessV0ScriptHashTy { if script.Class() != txscript.WitnessV0ScriptHashTy {
return nil, fmt.Errorf("incompatible script class") return nil, errors.New("incompatible script class")
} }
traderKeyTweak := poolscript.TraderKeyTweak(batchKey, secret, traderKey) traderKeyTweak := poolscript.TraderKeyTweak(batchKey, secret, traderKey)
@ -483,7 +493,7 @@ func fastScript(keyIndex, expiryFrom, expiryTo uint32, traderKey, auctioneerKey,
}, nil }, nil
} }
return nil, fmt.Errorf("account script not derived") return nil, errors.New("account script not derived")
} }
func fastScriptTaproot(scriptVersion poolscript.Version, keyIndex, expiryFrom, func fastScriptTaproot(scriptVersion poolscript.Version, keyIndex, expiryFrom,
@ -495,7 +505,7 @@ func fastScriptTaproot(scriptVersion poolscript.Version, keyIndex, expiryFrom,
return nil, err return nil, err
} }
if parsedScript.Class() != txscript.WitnessV1TaprootTy { if parsedScript.Class() != txscript.WitnessV1TaprootTy {
return nil, fmt.Errorf("incompatible script class") return nil, errors.New("incompatible script class")
} }
traderKeyTweak := poolscript.TraderKeyTweak(batchKey, secret, traderKey) traderKeyTweak := poolscript.TraderKeyTweak(batchKey, secret, traderKey)
@ -592,5 +602,5 @@ func fastScriptTaproot(scriptVersion poolscript.Version, keyIndex, expiryFrom,
}, nil }, nil
} }
return nil, fmt.Errorf("account script not derived") return nil, errors.New("account script not derived")
} }

@ -70,8 +70,6 @@ func TestClosePoolAccount(t *testing.T) {
) )
for _, tc := range testAccounts { for _, tc := range testAccounts {
tc := tc
t.Run(tc.name, func(tt *testing.T) { t.Run(tc.name, func(tt *testing.T) {
tt.Parallel() tt.Parallel()

@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"github.com/coreos/bbolt" "github.com/coreos/bbolt"
@ -52,10 +53,10 @@ to create a copy of it to a destination file, compacting it in the process.`,
func (c *compactDBCommand) Execute(_ *cobra.Command, _ []string) error { func (c *compactDBCommand) Execute(_ *cobra.Command, _ []string) error {
// Check that we have a source and destination channel DB. // Check that we have a source and destination channel DB.
if c.SourceDB == "" { if c.SourceDB == "" {
return fmt.Errorf("source channel DB is required") return errors.New("source channel DB is required")
} }
if c.DestDB == "" { if c.DestDB == "" {
return fmt.Errorf("destination channel DB is required") return errors.New("destination channel DB is required")
} }
if c.TxMaxSize <= 0 { if c.TxMaxSize <= 0 {
c.TxMaxSize = defaultTxMaxSize c.TxMaxSize = defaultTxMaxSize

@ -0,0 +1,233 @@
package main
import (
"bytes"
"errors"
"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 errors.New("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 errors.New("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
}

@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
@ -45,7 +46,7 @@ run lnd ` + lndVersion + ` or later after using this command!'`,
func (c *deletePaymentsCommand) Execute(_ *cobra.Command, _ []string) error { func (c *deletePaymentsCommand) Execute(_ *cobra.Command, _ []string) error {
// Check that we have a channel DB. // Check that we have a channel DB.
if c.ChannelDB == "" { if c.ChannelDB == "" {
return fmt.Errorf("channel DB is required") return errors.New("channel DB is required")
} }
db, err := lnd.OpenDB(c.ChannelDB, false) db, err := lnd.OpenDB(c.ChannelDB, false)
if err != nil { if err != nil {

@ -10,7 +10,7 @@ func newDocCommand() *cobra.Command {
Use: "doc", Use: "doc",
Short: "Generate the markdown documentation of all commands", Short: "Generate the markdown documentation of all commands",
Hidden: true, Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
return doc.GenMarkdownTree(rootCmd, "./doc") return doc.GenMarkdownTree(rootCmd, "./doc")
}, },
} }

@ -3,6 +3,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"strconv" "strconv"
@ -10,10 +11,10 @@ import (
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/mempool"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
@ -35,16 +36,16 @@ type doubleSpendInputs struct {
func newDoubleSpendInputsCommand() *cobra.Command { func newDoubleSpendInputsCommand() *cobra.Command {
cc := &doubleSpendInputs{} cc := &doubleSpendInputs{}
cc.cmd = &cobra.Command{ cc.cmd = &cobra.Command{
Use: "doublespendinputs", Use: "doublespendinputs",
Short: "Tries to double spend the given inputs by deriving the " + Short: "Replace a transaction by double spending its input",
"private for the address and sweeping the funds to the given " + Long: `Tries to double spend the given inputs by deriving the
"address. This can only be used with inputs that belong to " + private for the address and sweeping the funds to the given address. This can
"an lnd wallet.", only be used with inputs that belong to an lnd wallet.`,
Example: `chantools doublespendinputs \ Example: `chantools doublespendinputs \
--inputoutpoints xxxxxxxxx:y,xxxxxxxxx:y \ --inputoutpoints xxxxxxxxx:y,xxxxxxxxx:y \
--sweepaddr bc1q..... \ --sweepaddr bc1q..... \
--feerate 10 \ --feerate 10 \
--publish`, --publish`,
RunE: cc.Execute, RunE: cc.Execute,
} }
cc.cmd.Flags().StringVar( cc.cmd.Flags().StringVar(
@ -56,7 +57,9 @@ func newDoubleSpendInputsCommand() *cobra.Command {
"list of outpoints to double spend in the format txid:vout", "list of outpoints to double spend in the format txid:vout",
) )
cc.cmd.Flags().StringVar( 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().Uint32Var( cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+ &cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
@ -84,24 +87,28 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
} }
// Make sure sweep addr is set. // Make sure sweep addr is set.
if c.SweepAddr == "" { err = lnd.CheckAddress(
return fmt.Errorf("sweep addr is required") c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
} }
// Make sure we have at least one input. // Make sure we have at least one input.
if len(c.InputOutpoints) == 0 { if len(c.InputOutpoints) == 0 {
return fmt.Errorf("inputoutpoints are required") return errors.New("inputoutpoints are required")
} }
api := &btc.ExplorerAPI{BaseURL: c.APIURL} api := newExplorerAPI(c.APIURL)
addresses := make([]btcutil.Address, 0, len(c.InputOutpoints)) addresses := make([]btcutil.Address, 0, len(c.InputOutpoints))
outpoints := make([]*wire.OutPoint, 0, len(c.InputOutpoints)) outpoints := make([]*wire.OutPoint, 0, len(c.InputOutpoints))
privKeys := make([]*secp256k1.PrivateKey, 0, len(c.InputOutpoints)) privKeys := make([]*secp256k1.PrivateKey, 0, len(c.InputOutpoints))
// Get the addresses for the inputs. // Get the addresses for the inputs.
for _, input := range c.InputOutpoints { for _, inputOutpoint := range c.InputOutpoints {
addrString, err := api.Address(input) addrString, err := api.Address(inputOutpoint)
if err != nil { if err != nil {
return err return err
} }
@ -113,12 +120,12 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
addresses = append(addresses, addr) addresses = append(addresses, addr)
txHash, err := chainhash.NewHashFromStr(input[:64]) txHash, err := chainhash.NewHashFromStr(inputOutpoint[:64])
if err != nil { if err != nil {
return err return err
} }
vout, err := strconv.Atoi(input[65:]) vout, err := strconv.Atoi(inputOutpoint[65:])
if err != nil { if err != nil {
return err return err
} }
@ -139,7 +146,13 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
} }
// Start with the txweight estimator. // Start with the txweight estimator.
estimator := input.TxWeightEstimator{} 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 // Find the key for the given addresses and add their
// output weight to the tx estimator. // output weight to the tx estimator.
@ -164,7 +177,9 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
return err return err
} }
estimator.AddTaprootKeySpendInput(txscript.SigHashDefault) estimator.AddTaprootKeySpendInput(
txscript.SigHashDefault,
)
default: default:
return fmt.Errorf("address type %T not supported", addr) return fmt.Errorf("address type %T not supported", addr)
@ -184,63 +199,45 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
// Next get the full value of the inputs. // Next get the full value of the inputs.
var totalInput btcutil.Amount var totalInput btcutil.Amount
for _, input := range outpoints { for _, outpoint := range outpoints {
// Get the transaction. // Get the transaction.
tx, err := api.Transaction(input.Hash.String()) tx, err := api.Transaction(outpoint.Hash.String())
if err != nil { if err != nil {
return err return err
} }
value := tx.Vout[input.Index].Value value := tx.Vout[outpoint.Index].Value
// Get the output index. // Get the output index.
totalInput += btcutil.Amount(value) totalInput += btcutil.Amount(value)
scriptPubkey, err := hex.DecodeString(tx.Vout[input.Index].ScriptPubkey) scriptPubkey, err := hex.DecodeString(
tx.Vout[outpoint.Index].ScriptPubkey,
)
if err != nil { if err != nil {
return err return err
} }
// Add the output to the map. // Add the output to the map.
prevOuts[*input] = &wire.TxOut{ prevOuts[*outpoint] = &wire.TxOut{
Value: int64(value), Value: int64(value),
PkScript: scriptPubkey, PkScript: scriptPubkey,
} }
} }
// Calculate the fee.
sweepAddr, err := btcutil.DecodeAddress(c.SweepAddr, chainParams)
if err != nil {
return err
}
switch sweepAddr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
estimator.AddP2WKHOutput()
case *btcutil.AddressTaproot:
estimator.AddP2TROutput()
default:
return fmt.Errorf("address type %T not supported", sweepAddr)
}
// Calculate the fee. // Calculate the fee.
feeRateKWeight := chainfee.SatPerKVByte(1000 * c.FeeRate).FeePerKWeight() feeRateKWeight := chainfee.SatPerKVByte(1000 * c.FeeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight())) totalFee := feeRateKWeight.FeeForWeight(estimator.Weight())
// Create the transaction. // Create the transaction.
tx := wire.NewMsgTx(2) tx := wire.NewMsgTx(2)
// Add the inputs. // Add the inputs.
for _, input := range outpoints { for _, outpoint := range outpoints {
tx.AddTxIn(wire.NewTxIn(input, nil, nil)) tx.AddTxIn(&wire.TxIn{
} PreviousOutPoint: *outpoint,
Sequence: mempool.MaxRBFSequence,
// Add the output. })
sweepScript, err := txscript.PayToAddrScript(sweepAddr)
if err != nil {
return err
} }
tx.AddTxOut(wire.NewTxOut(int64(totalInput-totalFee), sweepScript)) tx.AddTxOut(wire.NewTxOut(int64(totalInput-totalFee), sweepScript))
@ -280,7 +277,8 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
} }
default: default:
return fmt.Errorf("address type %T not supported", addresses[i]) return fmt.Errorf("address type %T not supported",
addresses[i])
} }
} }
@ -291,7 +289,7 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
} }
// Print the transaction. // Print the transaction.
fmt.Printf("Sweeping transaction:\n%s\n", hex.EncodeToString(txBuf.Bytes())) fmt.Printf("Sweeping transaction:\n%x\n", txBuf.Bytes())
// Publish the transaction. // Publish the transaction.
if c.Publish { if c.Publish {
@ -311,7 +309,7 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
func iterateOverPath(baseKey *hdkeychain.ExtendedKey, addr btcutil.Address, func iterateOverPath(baseKey *hdkeychain.ExtendedKey, addr btcutil.Address,
path []uint32, maxTries uint32) (*hdkeychain.ExtendedKey, error) { path []uint32, maxTries uint32) (*hdkeychain.ExtendedKey, error) {
for i := uint32(0); i < maxTries; i++ { for i := range maxTries {
// Check for both the external and internal branch. // Check for both the external and internal branch.
for _, branch := range []uint32{0, 1} { for _, branch := range []uint32{0, 1} {
// Create the path to derive the key. // Create the path to derive the key.

@ -3,6 +3,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"time" "time"
@ -12,6 +13,7 @@ import (
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/chainreg"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -81,7 +83,7 @@ chantools dropchannelgraph \
func (c *dropChannelGraphCommand) Execute(_ *cobra.Command, _ []string) error { func (c *dropChannelGraphCommand) Execute(_ *cobra.Command, _ []string) error {
// Check that we have a channel DB. // Check that we have a channel DB.
if c.ChannelDB == "" { if c.ChannelDB == "" {
return fmt.Errorf("channel DB is required") return errors.New("channel DB is required")
} }
db, err := lnd.OpenDB(c.ChannelDB, false) db, err := lnd.OpenDB(c.ChannelDB, false)
if err != nil { if err != nil {
@ -90,7 +92,7 @@ func (c *dropChannelGraphCommand) Execute(_ *cobra.Command, _ []string) error {
defer func() { _ = db.Close() }() defer func() { _ = db.Close() }()
if c.NodeIdentityKey == "" { if c.NodeIdentityKey == "" {
return fmt.Errorf("node identity key is required") return errors.New("node identity key is required")
} }
idKeyBytes, err := hex.DecodeString(c.NodeIdentityKey) idKeyBytes, err := hex.DecodeString(c.NodeIdentityKey)
@ -174,8 +176,8 @@ func newChanAnnouncement(localPubKey, remotePubKey *btcec.PublicKey,
localFundingKey *keychain.KeyDescriptor, localFundingKey *keychain.KeyDescriptor,
remoteFundingKey *btcec.PublicKey, shortChanID lnwire.ShortChannelID, remoteFundingKey *btcec.PublicKey, shortChanID lnwire.ShortChannelID,
fwdMinHTLC, fwdMaxHTLC lnwire.MilliSatoshi, capacity btcutil.Amount, fwdMinHTLC, fwdMaxHTLC lnwire.MilliSatoshi, capacity btcutil.Amount,
channelPoint wire.OutPoint) (*channeldb.ChannelEdgeInfo, channelPoint wire.OutPoint) (*models.ChannelEdgeInfo,
*channeldb.ChannelEdgePolicy, error) { *models.ChannelEdgePolicy, error) {
chainHash := *chainParams.GenesisHash chainHash := *chainParams.GenesisHash
@ -226,7 +228,7 @@ func newChanAnnouncement(localPubKey, remotePubKey *btcec.PublicKey,
return nil, nil, err return nil, nil, err
} }
edge := &channeldb.ChannelEdgeInfo{ edge := &models.ChannelEdgeInfo{
ChannelID: chanAnn.ShortChannelID.ToUint64(), ChannelID: chanAnn.ShortChannelID.ToUint64(),
ChainHash: chanAnn.ChainHash, ChainHash: chanAnn.ChainHash,
NodeKey1Bytes: chanAnn.NodeID1, NodeKey1Bytes: chanAnn.NodeID1,
@ -264,7 +266,7 @@ func newChanAnnouncement(localPubKey, remotePubKey *btcec.PublicKey,
FeeRate: uint32(chainreg.DefaultBitcoinFeeRate), FeeRate: uint32(chainreg.DefaultBitcoinFeeRate),
} }
update := &channeldb.ChannelEdgePolicy{ update := &models.ChannelEdgePolicy{
SigBytes: chanUpdateAnn.Signature.ToSignatureBytes(), SigBytes: chanUpdateAnn.Signature.ToSignatureBytes(),
ChannelID: chanAnn.ShortChannelID.ToUint64(), ChannelID: chanAnn.ShortChannelID.ToUint64(),
LastUpdate: time.Now(), LastUpdate: time.Now(),

@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
@ -52,7 +53,7 @@ run lnd ` + lndVersion + ` or later after using this command!'`,
func (c *dropGraphZombiesCommand) Execute(_ *cobra.Command, _ []string) error { func (c *dropGraphZombiesCommand) Execute(_ *cobra.Command, _ []string) error {
// Check that we have a channel DB. // Check that we have a channel DB.
if c.ChannelDB == "" { if c.ChannelDB == "" {
return fmt.Errorf("channel DB is required") return errors.New("channel DB is required")
} }
db, err := lnd.OpenDB(c.ChannelDB, false) db, err := lnd.OpenDB(c.ChannelDB, false)
if err != nil { if err != nil {

@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
@ -47,7 +48,7 @@ func (c *dumpBackupCommand) Execute(_ *cobra.Command, _ []string) error {
// Check that we have a backup file. // Check that we have a backup file.
if c.MultiFile == "" { if c.MultiFile == "" {
return fmt.Errorf("backup file is required") return errors.New("backup file is required")
} }
multiFile := chanbackup.NewMultiFile(c.MultiFile) multiFile := chanbackup.NewMultiFile(c.MultiFile)
keyRing := &lnd.HDKeyRing{ keyRing := &lnd.HDKeyRing{

@ -55,7 +55,7 @@ given lnd channel.db gile in a human readable format.`,
func (c *dumpChannelsCommand) Execute(_ *cobra.Command, _ []string) error { func (c *dumpChannelsCommand) Execute(_ *cobra.Command, _ []string) error {
// Check that we have a channel DB. // Check that we have a channel DB.
if c.ChannelDB == "" { if c.ChannelDB == "" {
return fmt.Errorf("channel DB is required") return errors.New("channel DB is required")
} }
db, err := lnd.OpenDB(c.ChannelDB, true) db, err := lnd.OpenDB(c.ChannelDB, true)
if err != nil { if err != nil {
@ -67,7 +67,7 @@ func (c *dumpChannelsCommand) Execute(_ *cobra.Command, _ []string) error {
(c.Pending && c.WaitingClose) || (c.Pending && c.WaitingClose) ||
(c.Closed && c.Pending && c.WaitingClose) { (c.Closed && c.Pending && c.WaitingClose) {
return fmt.Errorf("can only specify one flag at a time") return errors.New("can only specify one flag at a time")
} }
if c.Closed { if c.Closed {

@ -3,6 +3,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "net"
@ -13,7 +14,6 @@ import (
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/gogo/protobuf/jsonpb"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
@ -138,8 +138,9 @@ func (c *fakeChanBackupCommand) Execute(_ *cobra.Command, _ []string) error {
return fmt.Errorf("error reading graph JSON file %s: "+ return fmt.Errorf("error reading graph JSON file %s: "+
"%v", c.FromChannelGraph, err) "%v", c.FromChannelGraph, err)
} }
graph := &lnrpc.ChannelGraph{} graph := &lnrpc.ChannelGraph{}
err = jsonpb.UnmarshalString(string(graphBytes), graph) err = lnrpc.ProtoJSONUnmarshalOpts.Unmarshal(graphBytes, graph)
if err != nil { if err != nil {
return fmt.Errorf("error parsing graph JSON: %w", err) return fmt.Errorf("error parsing graph JSON: %w", err)
} }
@ -156,7 +157,7 @@ func (c *fakeChanBackupCommand) Execute(_ *cobra.Command, _ []string) error {
// Now parse the remote node info. // Now parse the remote node info.
splitNodeInfo := strings.Split(c.NodeAddr, "@") splitNodeInfo := strings.Split(c.NodeAddr, "@")
if len(splitNodeInfo) != 2 { if len(splitNodeInfo) != 2 {
return fmt.Errorf("--remote_node_addr expected in format: " + return errors.New("--remote_node_addr expected in format: " +
"pubkey@host:port") "pubkey@host:port")
} }
pubKeyBytes, err := hex.DecodeString(splitNodeInfo[0]) pubKeyBytes, err := hex.DecodeString(splitNodeInfo[0])
@ -192,7 +193,7 @@ func (c *fakeChanBackupCommand) Execute(_ *cobra.Command, _ []string) error {
// Parse the short channel ID. // Parse the short channel ID.
splitChanID := strings.Split(c.ShortChanID, "x") splitChanID := strings.Split(c.ShortChanID, "x")
if len(splitChanID) != 3 { if len(splitChanID) != 3 {
return fmt.Errorf("--short_channel_id expected in format: " + return errors.New("--short_channel_id expected in format: " +
"<blockheight>x<transactionindex>x<outputindex>", "<blockheight>x<transactionindex>x<outputindex>",
) )
} }
@ -216,7 +217,7 @@ func (c *fakeChanBackupCommand) Execute(_ *cobra.Command, _ []string) error {
// Is the outpoint and/or short channel ID correct? // Is the outpoint and/or short channel ID correct?
if uint32(chanOutputIdx) != chanOp.Index { if uint32(chanOutputIdx) != chanOp.Index {
return fmt.Errorf("output index of --short_channel_id must " + return errors.New("output index of --short_channel_id must " +
"be equal to index on --channelpoint") "be equal to index on --channelpoint")
} }

@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@ -59,7 +60,7 @@ func (c *filterBackupCommand) Execute(_ *cobra.Command, _ []string) error {
// Check that we have a backup file. // Check that we have a backup file.
if c.MultiFile == "" { if c.MultiFile == "" {
return fmt.Errorf("backup file is required") return errors.New("backup file is required")
} }
multiFile := chanbackup.NewMultiFile(c.MultiFile) multiFile := chanbackup.NewMultiFile(c.MultiFile)
keyRing := &lnd.HDKeyRing{ keyRing := &lnd.HDKeyRing{

@ -53,7 +53,7 @@ func (c *fixOldBackupCommand) Execute(_ *cobra.Command, _ []string) error {
// Check that we have a backup file. // Check that we have a backup file.
if c.MultiFile == "" { if c.MultiFile == "" {
return fmt.Errorf("backup file is required") return errors.New("backup file is required")
} }
multiFile := chanbackup.NewMultiFile(c.MultiFile) multiFile := chanbackup.NewMultiFile(c.MultiFile)
keyRing := &lnd.HDKeyRing{ keyRing := &lnd.HDKeyRing{

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -11,7 +12,6 @@ import (
"github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/dataformat" "github.com/lightninglabs/chantools/dataformat"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
@ -80,7 +80,7 @@ func (c *forceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
// Check that we have a channel DB. // Check that we have a channel DB.
if c.ChannelDB == "" { if c.ChannelDB == "" {
return fmt.Errorf("rescue DB is required") return errors.New("rescue DB is required")
} }
db, err := lnd.OpenDB(c.ChannelDB, true) db, err := lnd.OpenDB(c.ChannelDB, true)
if err != nil { if err != nil {
@ -105,7 +105,7 @@ func forceCloseChannels(apiURL string, extendedKey *hdkeychain.ExtendedKey,
if err != nil { if err != nil {
return err return err
} }
api := &btc.ExplorerAPI{BaseURL: apiURL} api := newExplorerAPI(apiURL)
signer := &lnd.Signer{ signer := &lnd.Signer{
ExtendedKey: extendedKey, ExtendedKey: extendedKey,
ChainParams: chainParams, ChainParams: chainParams,

@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"time" "time"
@ -145,7 +146,7 @@ func (c *genImportScriptCommand) Execute(_ *cobra.Command, _ []string) error {
paths = [][]uint32{derivationPath} paths = [][]uint32{derivationPath}
case c.LndPaths && c.DerivationPath != "": case c.LndPaths && c.DerivationPath != "":
return fmt.Errorf("cannot use --lndpaths and --derivationpath " + return errors.New("cannot use --lndpaths and --derivationpath " +
"at the same time") "at the same time")
case c.LndPaths: case c.LndPaths:

@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
@ -41,7 +42,7 @@ run lnd ` + lndVersion + ` or later after using this command!'`,
func (c *migrateDBCommand) Execute(_ *cobra.Command, _ []string) error { func (c *migrateDBCommand) Execute(_ *cobra.Command, _ []string) error {
// Check that we have a channel DB. // Check that we have a channel DB.
if c.ChannelDB == "" { if c.ChannelDB == "" {
return fmt.Errorf("channel DB is required") return errors.New("channel DB is required")
} }
db, err := lnd.OpenDB(c.ChannelDB, false) db, err := lnd.OpenDB(c.ChannelDB, false)
if err != nil { if err != nil {

@ -0,0 +1,531 @@
package main
import (
"bytes"
"encoding/hex"
"errors"
"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 errors.New("sponsor input is required")
}
if len(c.AnchorAddrs) == 0 {
return errors.New("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(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 := range uint32(math.MaxInt16) {
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, errors.New("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 := range uint32(math.MaxInt16) {
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, errors.New("no matching pubkeys found")
}

@ -2,29 +2,38 @@ package main
import ( import (
"bytes" "bytes"
"context"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"path/filepath"
"time"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var (
errSwapNotFound = errors.New("loop in swap not found")
)
type recoverLoopInCommand struct { type recoverLoopInCommand struct {
TxID string TxID string
Vout uint32 Vout uint32
SwapHash string SwapHash string
SweepAddr string SweepAddr string
OutputAmt uint64
FeeRate uint32 FeeRate uint32
StartKeyIndex int StartKeyIndex int
NumTries int NumTries int
@ -32,7 +41,8 @@ type recoverLoopInCommand struct {
APIURL string APIURL string
Publish bool Publish bool
LoopDbDir string LoopDbDir string
SqliteFile string
rootKey *rootKey rootKey *rootKey
cmd *cobra.Command cmd *cobra.Command
@ -70,8 +80,9 @@ func newRecoverLoopInCommand() *cobra.Command {
"database directory, where the loop.db file is located", "database directory, where the loop.db file is located",
) )
cc.cmd.Flags().StringVar( cc.cmd.Flags().StringVar(
&cc.SweepAddr, "sweep_addr", "", "address to recover "+ &cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
"the funds to", "to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
"derive a new address from the seed automatically",
) )
cc.cmd.Flags().Uint32Var( cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", 0, "fee rate to "+ &cc.FeeRate, "feerate", 0, "fee rate to "+
@ -93,6 +104,14 @@ func newRecoverLoopInCommand() *cobra.Command {
&cc.Publish, "publish", false, "publish sweep TX to the chain "+ &cc.Publish, "publish", false, "publish sweep TX to the chain "+
"API instead of just printing the TX", "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") cc.rootKey = newRootKey(cc.cmd, "deriving starting key")
@ -106,55 +125,100 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
} }
if c.TxID == "" { if c.TxID == "" {
return fmt.Errorf("txid is required") return errors.New("txid is required")
} }
if c.SwapHash == "" { if c.SwapHash == "" {
return fmt.Errorf("swap_hash is required") return errors.New("swap_hash is required")
} }
if c.LoopDbDir == "" { if c.LoopDbDir == "" {
return fmt.Errorf("loop_db_dir is required") return errors.New("loop_db_dir is required")
} }
if c.SweepAddr == "" { err = lnd.CheckAddress(
return fmt.Errorf("sweep_addr is required") c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
} }
api := &btc.ExplorerAPI{BaseURL: c.APIURL} api := newExplorerAPI(c.APIURL)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
signer := &lnd.Signer{ signer := &lnd.Signer{
ExtendedKey: extendedKey, ExtendedKey: extendedKey,
ChainParams: chainParams, ChainParams: chainParams,
} }
// Try to fetch the swap from the database. // Try to fetch the swap from the boltdb.
store, err := loopdb.NewBoltSwapStore(c.LoopDbDir, chainParams) var (
if err != nil { store loopdb.SwapStore
return err loopIn *loopdb.LoopIn
} )
defer store.Close()
swaps, err := store.FetchLoopInSwaps() // First check if a boltdb file exists.
if err != nil { if lnrpc.FileExists(filepath.Join(c.LoopDbDir, "loop.db")) {
return err 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
}
} }
var loopIn *loopdb.LoopIn // If the loopin is not found yet, try to fetch it from the sqlite db.
for _, s := range swaps { if loopIn == nil {
if s.Hash.String() == c.SwapHash { if c.SqliteFile == "" {
loopIn = s c.SqliteFile = filepath.Join(
break 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 { if loopIn == nil {
return fmt.Errorf("swap not found") 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 errors.New("output_amt is required for external htlc")
} }
fmt.Println("Loop expires at block height", loopIn.Contract.CltvExpiry) 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. // Get the swaps htlc.
htlc, err := loop.GetHtlc( htlc, err := utils.GetHtlc(
loopIn.Hash, &loopIn.Contract.SwapContract, chainParams, loopIn.Hash, &loopIn.Contract.SwapContract, chainParams,
) )
if err != nil { if err != nil {
@ -162,33 +226,24 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
} }
// Get the destination address. // 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 { if err != nil {
return err return err
} }
// Calculate the sweep fee. // Calculate the sweep fee.
estimator := &input.TxWeightEstimator{} err = htlc.AddTimeoutToEstimator(&estimator)
err = htlc.AddTimeoutToEstimator(estimator)
if err != nil { if err != nil {
return err 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( feeRateKWeight := chainfee.SatPerKVByte(
1000 * c.FeeRate, 1000 * c.FeeRate,
).FeePerKWeight() ).FeePerKWeight()
fee := feeRateKWeight.FeeForWeight(int64(estimator.Weight())) fee := feeRateKWeight.FeeForWeight(estimator.Weight())
txID, err := chainhash.NewHashFromStr(c.TxID) txID, err := chainhash.NewHashFromStr(c.TxID)
if err != nil { if err != nil {
@ -213,14 +268,9 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
}) })
// Add output for the destination address. // Add output for the destination address.
sweepPkScript, err := txscript.PayToAddrScript(sweepAddr)
if err != nil {
return err
}
sweepTx.AddTxOut(&wire.TxOut{ sweepTx.AddTxOut(&wire.TxOut{
PkScript: sweepPkScript, PkScript: sweepScript,
Value: int64(loopIn.Contract.AmountRequested) - int64(fee), Value: int64(outputValue) - int64(fee),
}) })
// If the htlc is version 2, we need to brute force the key locator, as // If the htlc is version 2, we need to brute force the key locator, as
@ -230,23 +280,25 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
fmt.Println("Brute forcing key index...") fmt.Println("Brute forcing key index...")
for i := c.StartKeyIndex; i < c.StartKeyIndex+c.NumTries; i++ { for i := c.StartKeyIndex; i < c.StartKeyIndex+c.NumTries; i++ {
rawTx, err = getSignedTx( rawTx, err = getSignedTx(
signer, loopIn, sweepTx, htlc, signer, sweepTx, htlc,
keychain.KeyFamily(swap.KeyFamily), uint32(i), keychain.KeyFamily(swap.KeyFamily), uint32(i),
outputValue,
) )
if err == nil { if err == nil {
break break
} }
} }
if rawTx == nil { if rawTx == nil {
return fmt.Errorf("failed to brute force key index, " + return errors.New("failed to brute force key index, " +
"please try again with a higher start key " + "please try again with a higher start key " +
"index") "index")
} }
} else { } else {
rawTx, err = getSignedTx( rawTx, err = getSignedTx(
signer, loopIn, sweepTx, htlc, signer, sweepTx, htlc,
loopIn.Contract.HtlcKeys.ClientScriptKeyLocator.Family, loopIn.Contract.HtlcKeys.ClientScriptKeyLocator.Family,
loopIn.Contract.HtlcKeys.ClientScriptKeyLocator.Index, loopIn.Contract.HtlcKeys.ClientScriptKeyLocator.Index,
outputValue,
) )
if err != nil { if err != nil {
return err return err
@ -272,14 +324,14 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
return nil return nil
} }
func getSignedTx(signer *lnd.Signer, loopIn *loopdb.LoopIn, sweepTx *wire.MsgTx, func getSignedTx(signer *lnd.Signer, sweepTx *wire.MsgTx, htlc *swap.Htlc,
htlc *swap.Htlc, keyFamily keychain.KeyFamily, keyFamily keychain.KeyFamily, keyIndex uint32,
keyIndex uint32) ([]byte, error) { outputValue btcutil.Amount) ([]byte, error) {
// Create the sign descriptor. // Create the sign descriptor.
prevTxOut := &wire.TxOut{ prevTxOut := &wire.TxOut{
PkScript: htlc.PkScript, PkScript: htlc.PkScript,
Value: int64(loopIn.Contract.AmountRequested), Value: int64(outputValue),
} }
prevOutputFetcher := txscript.NewCannedPrevOutputFetcher( prevOutputFetcher := txscript.NewCannedPrevOutputFetcher(
prevTxOut.PkScript, prevTxOut.Value, prevTxOut.PkScript, prevTxOut.Value,
@ -343,6 +395,23 @@ func getSignedTx(signer *lnd.Signer, loopIn *loopdb.LoopIn, sweepTx *wire.MsgTx,
return rawTx, nil 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. // encodeTx encodes a tx to raw bytes.
func encodeTx(tx *wire.MsgTx) ([]byte, error) { func encodeTx(tx *wire.MsgTx) ([]byte, error) {
var buffer bytes.Buffer var buffer bytes.Buffer

@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -53,7 +54,7 @@ run lnd ` + lndVersion + ` or later after using this command!`,
func (c *removeChannelCommand) Execute(_ *cobra.Command, _ []string) error { func (c *removeChannelCommand) Execute(_ *cobra.Command, _ []string) error {
// Check that we have a channel DB. // Check that we have a channel DB.
if c.ChannelDB == "" { if c.ChannelDB == "" {
return fmt.Errorf("channel DB is required") return errors.New("channel DB is required")
} }
db, err := lnd.OpenDB(c.ChannelDB, false) db, err := lnd.OpenDB(c.ChannelDB, false)
if err != nil { if err != nil {

@ -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 know about the channels any more but we still have the channel.db from the
moment they force-closed. 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 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 running the fund-recovery branch of my guggero/lnd fork (see
https://github.com/guggero/lnd/releases for a binary release) in combination 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.cmd.Flags().StringVar(
&cc.Addr, "force_close_addr", "", "the address the channel "+ &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.cmd.Flags().StringVar(
&cc.CommitPoint, "commit_point", "", "the commit point that "+ &cc.CommitPoint, "commit_point", "", "the commit point that "+
@ -168,7 +173,7 @@ func (c *rescueClosedCommand) Execute(_ *cobra.Command, _ []string) error {
return rescueClosedChannels(extendedKey, entries, commitPoints) return rescueClosedChannels(extendedKey, entries, commitPoints)
default: default:
return fmt.Errorf("you either need to specify --channeldb and " + return errors.New("you either need to specify --channeldb and " +
"--fromsummary or --force_close_addr and " + "--fromsummary or --force_close_addr and " +
"--commit_point but not a mixture of them") "--commit_point but not a mixture of them")
} }
@ -328,7 +333,7 @@ func rescueClosedChannel(extendedKey *hdkeychain.ExtendedKey,
"hash %x\n", addr.ScriptAddress()) "hash %x\n", addr.ScriptAddress())
default: default:
return fmt.Errorf("address: must be a bech32 P2WPKH address") return errors.New("address: must be a bech32 P2WPKH address")
} }
err := fillCache(extendedKey) err := fillCache(extendedKey)
@ -375,13 +380,13 @@ func addrInCache(addr string, perCommitPoint *btcec.PublicKey) (string, error) {
return "", fmt.Errorf("error parsing addr: %w", err) return "", fmt.Errorf("error parsing addr: %w", err)
} }
if scriptHash { if scriptHash {
return "", fmt.Errorf("address must be a P2WPKH address") return "", errors.New("address must be a P2WPKH address")
} }
// If the commit point is nil, we try with plain private keys to match // If the commit point is nil, we try with plain private keys to match
// static_remote_key outputs. // static_remote_key outputs.
if perCommitPoint == nil { if perCommitPoint == nil {
for i := 0; i < cacheSize; i++ { for i := range cacheSize {
cacheEntry := cache[i] cacheEntry := cache[i]
hashedPubKey := btcutil.Hash160( hashedPubKey := btcutil.Hash160(
cacheEntry.pubKey.SerializeCompressed(), cacheEntry.pubKey.SerializeCompressed(),
@ -410,7 +415,7 @@ func addrInCache(addr string, perCommitPoint *btcec.PublicKey) (string, error) {
// Loop through all cached payment base point keys, tweak each of it // Loop through all cached payment base point keys, tweak each of it
// with the per_commit_point and see if the hashed public key // with the per_commit_point and see if the hashed public key
// corresponds to the target pubKeyHash of the given address. // corresponds to the target pubKeyHash of the given address.
for i := 0; i < cacheSize; i++ { for i := range cacheSize {
cacheEntry := cache[i] cacheEntry := cache[i]
basePoint := cacheEntry.pubKey basePoint := cacheEntry.pubKey
tweakedPubKey := input.TweakPubKey(basePoint, perCommitPoint) tweakedPubKey := input.TweakPubKey(basePoint, perCommitPoint)
@ -444,7 +449,7 @@ func addrInCache(addr string, perCommitPoint *btcec.PublicKey) (string, error) {
func fillCache(extendedKey *hdkeychain.ExtendedKey) error { func fillCache(extendedKey *hdkeychain.ExtendedKey) error {
cache = make([]*cacheEntry, cacheSize) cache = make([]*cacheEntry, cacheSize)
for i := 0; i < cacheSize; i++ { for i := range cacheSize {
key, err := lnd.DeriveChildren(extendedKey, []uint32{ key, err := lnd.DeriveChildren(extendedKey, []uint32{
lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose), lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose),
lnd.HardenedKeyStart + chainParams.HDCoinType, lnd.HardenedKeyStart + chainParams.HDCoinType,

@ -3,13 +3,13 @@ package main
import ( import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
@ -113,7 +113,9 @@ chantools rescuefunding \
"specified manually", "specified manually",
) )
cc.cmd.Flags().StringVar( 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().Uint32Var( cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+ &cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
@ -152,7 +154,7 @@ func (c *rescueFundingCommand) Execute(_ *cobra.Command, _ []string) error {
case (c.ChannelDB == "" || c.DBChannelPoint == "") && case (c.ChannelDB == "" || c.DBChannelPoint == "") &&
c.RemotePubKey == "": c.RemotePubKey == "":
return fmt.Errorf("need to specify either channel DB and " + return errors.New("need to specify either channel DB and " +
"channel point or both local and remote pubkey") "channel point or both local and remote pubkey")
case c.ChannelDB != "" && c.DBChannelPoint != "": case c.ChannelDB != "" && c.DBChannelPoint != "":
@ -178,11 +180,11 @@ func (c *rescueFundingCommand) Execute(_ *cobra.Command, _ []string) error {
} }
if pendingChan.LocalChanCfg.MultiSigKey.PubKey == nil { if pendingChan.LocalChanCfg.MultiSigKey.PubKey == nil {
return fmt.Errorf("invalid channel data in DB, local " + return errors.New("invalid channel data in DB, local " +
"multisig pubkey is nil") "multisig pubkey is nil")
} }
if pendingChan.LocalChanCfg.MultiSigKey.PubKey == nil { if pendingChan.LocalChanCfg.MultiSigKey.PubKey == nil {
return fmt.Errorf("invalid channel data in DB, remote " + return errors.New("invalid channel data in DB, remote " +
"multisig pubkey is nil") "multisig pubkey is nil")
} }
@ -208,7 +210,7 @@ func (c *rescueFundingCommand) Execute(_ *cobra.Command, _ []string) error {
Index: c.LocalKeyIndex, Index: c.LocalKeyIndex,
}, },
} }
privKey, err := signer.FetchPrivKey(localKeyDesc) privKey, err := signer.FetchPrivateKey(localKeyDesc)
if err != nil { if err != nil {
return fmt.Errorf("error deriving local key: %w", err) return fmt.Errorf("error deriving local key: %w", err)
} }
@ -227,35 +229,46 @@ func (c *rescueFundingCommand) Execute(_ *cobra.Command, _ []string) error {
} }
} }
// Make sure the sweep addr is a P2WKH address so we can do accurate err = lnd.CheckAddress(
// fee estimation. c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
sweepScript, err := lnd.GetP2WPKHScript(c.SweepAddr, chainParams) lnd.AddrTypeP2TR,
)
if err != nil { if err != nil {
return fmt.Errorf("error parsing sweep addr: %w", err) return err
} }
return rescueFunding( return rescueFunding(
localKeyDesc, remotePubKey, signer, chainOp, localKeyDesc, remotePubKey, signer, chainOp, c.SweepAddr,
sweepScript, btcutil.Amount(c.FeeRate), c.APIURL, btcutil.Amount(c.FeeRate), c.APIURL,
) )
} }
func rescueFunding(localKeyDesc *keychain.KeyDescriptor, func rescueFunding(localKeyDesc *keychain.KeyDescriptor,
remoteKey *btcec.PublicKey, signer *lnd.Signer, 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 { 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. // Prepare the wire part of the PSBT.
txIn := &wire.TxIn{ txIn := &wire.TxIn{
PreviousOutPoint: *chainPoint, PreviousOutPoint: *chainPoint,
Sequence: 0, Sequence: 0,
} }
txOut := &wire.TxOut{ txOut := &wire.TxOut{
PkScript: sweepPKScript, PkScript: sweepScript,
} }
// Locate the output in the funding TX. // Locate the output in the funding TX.
api := &btc.ExplorerAPI{BaseURL: apiURL}
tx, err := api.Transaction(chainPoint.Hash.String()) tx, err := api.Transaction(chainPoint.Hash.String())
if err != nil { if err != nil {
return fmt.Errorf("error fetching UTXO info for outpoint %s: "+ return fmt.Errorf("error fetching UTXO info for outpoint %s: "+
@ -285,7 +298,7 @@ func rescueFunding(localKeyDesc *keychain.KeyDescriptor,
// Some last sanity check that we're working with the correct data. // Some last sanity check that we're working with the correct data.
if !bytes.Equal(fundingTxOut.PkScript, utxo.PkScript) { if !bytes.Equal(fundingTxOut.PkScript, utxo.PkScript) {
return fmt.Errorf("funding output script does not match UTXO") return errors.New("funding output script does not match UTXO")
} }
// Now the rest of the known data for the PSBT. // Now the rest of the known data for the PSBT.
@ -294,19 +307,17 @@ func rescueFunding(localKeyDesc *keychain.KeyDescriptor,
WitnessScript: witnessScript, WitnessScript: witnessScript,
Unknowns: []*psbt.Unknown{{ Unknowns: []*psbt.Unknown{{
// We add the public key the other party needs to sign // 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. // out with the signrescuefunding command.
Key: PsbtKeyTypeOutputMissingSigPubkey, Key: PsbtKeyTypeOutputMissingSigPubkey,
Value: remoteKey.SerializeCompressed(), Value: remoteKey.SerializeCompressed(),
}}, }},
} }
// Estimate the transaction weight so we can do the fee estimation. // Estimate the transaction weight, so we can do the fee estimation.
var estimator input.TxWeightEstimator
estimator.AddWitnessInput(MultiSigWitnessSize) estimator.AddWitnessInput(MultiSigWitnessSize)
estimator.AddP2WKHOutput()
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight())) totalFee := feeRateKWeight.FeeForWeight(estimator.Weight())
txOut.Value = utxo.Value - int64(totalFee) txOut.Value = utxo.Value - int64(totalFee)
// Let's now create the PSBT as we have everything we need so far. // Let's now create the PSBT as we have everything we need so far.

@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
@ -15,7 +16,7 @@ import (
) )
var ( var (
ErrAddrNotFound = fmt.Errorf("address not found") ErrAddrNotFound = errors.New("address not found")
) )
type rescueTweakedKeyCommand struct { type rescueTweakedKeyCommand struct {
@ -66,7 +67,7 @@ func (c *rescueTweakedKeyCommand) Execute(_ *cobra.Command, _ []string) error {
} }
if c.Path == "" { if c.Path == "" {
return fmt.Errorf("path is required") return errors.New("path is required")
} }
childKey, _, _, err := lnd.DeriveKey(extendedKey, c.Path, chainParams) childKey, _, _, err := lnd.DeriveKey(extendedKey, c.Path, chainParams)

@ -1,14 +1,13 @@
package main package main
import ( import (
"bufio"
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"strings" "strings"
"syscall"
"time" "time"
"github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/btcutil/hdkeychain"
@ -22,21 +21,22 @@ import (
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/peer" "github.com/lightningnetwork/lnd/peer"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
) )
const ( const (
defaultAPIURL = "https://blockstream.info/api" 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. // version is the current version of the tool. It is set during build.
// NOTE: When changing this, please also update the version in the // NOTE: When changing this, please also update the version in the
// download link shown in the README. // download link shown in the README.
version = "0.12.0" version = "0.13.1"
na = "n/a" na = "n/a"
// lndVersion is the current version of lnd that we support. This is // lndVersion is the current version of lnd that we support. This is
// shown in some commands that affect the database and its migrations. // shown in some commands that affect the database and its migrations.
lndVersion = "v0.17.0-beta" lndVersion = "v0.18.0-beta"
Commit = "" Commit = ""
) )
@ -56,9 +56,10 @@ var rootCmd = &cobra.Command{
Short: "Chantools helps recover funds from lightning channels", Short: "Chantools helps recover funds from lightning channels",
Long: `This tool provides helper functions that can be used rescue Long: `This tool provides helper functions that can be used rescue
funds locked in lnd channels in case lnd itself cannot run properly anymore. funds locked in lnd channels in case lnd itself cannot run properly anymore.
Complete documentation is available at https://github.com/lightninglabs/chantools/.`, Complete documentation is available at
https://github.com/lightninglabs/chantools/.`,
Version: fmt.Sprintf("v%s, commit %s", version, Commit), Version: fmt.Sprintf("v%s, commit %s", version, Commit),
PersistentPreRun: func(cmd *cobra.Command, args []string) { PersistentPreRun: func(_ *cobra.Command, _ []string) {
switch { switch {
case Testnet: case Testnet:
chainParams = &chaincfg.TestNet3Params chainParams = &chaincfg.TestNet3Params
@ -98,6 +99,7 @@ func main() {
rootCmd.AddCommand( rootCmd.AddCommand(
newChanBackupCommand(), newChanBackupCommand(),
newClosePoolAccountCommand(), newClosePoolAccountCommand(),
newCreateWalletCommand(),
newCompactDBCommand(), newCompactDBCommand(),
newDeletePaymentsCommand(), newDeletePaymentsCommand(),
newDeriveKeyCommand(), newDeriveKeyCommand(),
@ -113,13 +115,16 @@ func main() {
newForceCloseCommand(), newForceCloseCommand(),
newGenImportScriptCommand(), newGenImportScriptCommand(),
newMigrateDBCommand(), newMigrateDBCommand(),
newPullAnchorCommand(),
newRecoverLoopInCommand(), newRecoverLoopInCommand(),
newRemoveChannelCommand(), newRemoveChannelCommand(),
newRescueClosedCommand(), newRescueClosedCommand(),
newRescueFundingCommand(), newRescueFundingCommand(),
newRescueTweakedKeyCommand(), newRescueTweakedKeyCommand(),
newShowRootKeyCommand(), newShowRootKeyCommand(),
newSignMessageCommand(),
newSignRescueFundingCommand(), newSignRescueFundingCommand(),
newSignPSBTCommand(),
newSummaryCommand(), newSummaryCommand(),
newSweepTimeLockCommand(), newSweepTimeLockCommand(),
newSweepTimeLockManualCommand(), newSweepTimeLockManualCommand(),
@ -137,8 +142,9 @@ func main() {
} }
type rootKey struct { type rootKey struct {
RootKey string RootKey string
BIP39 bool BIP39 bool
WalletDB string
} }
func newRootKey(cmd *cobra.Command, desc string) *rootKey { func newRootKey(cmd *cobra.Command, desc string) *rootKey {
@ -153,6 +159,12 @@ func newRootKey(cmd *cobra.Command, desc string) *rootKey {
"passphrase from the terminal instead of asking for "+ "passphrase from the terminal instead of asking for "+
"lnd seed format or providing the --rootkey flag", "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 return r
} }
@ -175,6 +187,39 @@ func (r *rootKey) readWithBirthday() (*hdkeychain.ExtendedKey, time.Time,
extendedKey, err := btc.ReadMnemonicFromTerminal(chainParams) extendedKey, err := btc.ReadMnemonicFromTerminal(chainParams)
return extendedKey, time.Unix(0, 0), err 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: default:
return lnd.ReadAezeed(chainParams) return lnd.ReadAezeed(chainParams)
} }
@ -238,7 +283,7 @@ func (f *inputFlags) parseInputType() ([]*dataformat.SummaryEntry, error) {
return target.AsSummaryEntries() return target.AsSummaryEntries()
default: default:
return nil, fmt.Errorf("an input file must be specified") return nil, errors.New("an input file must be specified")
} }
if err != nil { if err != nil {
@ -259,27 +304,6 @@ func readInput(input string) ([]byte, error) {
return ioutil.ReadFile(input) 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() { func setupLogging() {
setSubLogger("CHAN", log) setSubLogger("CHAN", log)
addSubLogger("CHDB", channeldb.UseLogger) addSubLogger("CHDB", channeldb.UseLogger)
@ -322,6 +346,21 @@ func setSubLogger(subsystem string, logger btclog.Logger,
} }
} }
func noConsole() ([]byte, error) { func newExplorerAPI(apiURL string) *btc.ExplorerAPI {
return nil, fmt.Errorf("wallet db requires console access") // 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}
} }

@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"io"
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
@ -103,7 +104,20 @@ func (h *harness) testdataFile(name string) string {
workingDir, err := os.Getwd() workingDir, err := os.Getwd()
require.NoError(h.t, err) require.NoError(h.t, err)
return path.Join(workingDir, "testdata", name) origFile := path.Join(workingDir, "testdata", name)
fileCopy := path.Join(h.t.TempDir(), name)
src, err := os.Open(origFile)
require.NoError(h.t, err)
defer src.Close()
dst, err := os.Create(fileCopy)
require.NoError(h.t, err)
defer dst.Close()
_, err = io.Copy(dst, src)
require.NoError(h.t, err)
return fileCopy
} }
func (h *harness) tempFile(name string) string { func (h *harness) tempFile(name string) string {

@ -0,0 +1,91 @@
package main
import (
"errors"
"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 errors.New("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,255 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/binary"
"errors"
"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"
)
var (
errNoPathFound = errors.New("no matching derivation path found")
)
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 errors.New("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 {
for inputIndex := range packet.Inputs {
pIn := &packet.Inputs[inputIndex]
// Check that we have an input with a derivation path that
// belongs to the root key.
derivationPath, err := findMatchingDerivationPath(rootKey, pIn)
if errors.Is(err, errNoPathFound) {
log.Infof("No matching derivation path found for "+
"input %d, skipping", inputIndex)
continue
}
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 pIn.WitnessUtxo == nil {
return fmt.Errorf("invalid PSBT, input %d is missing "+
"witness UTXO", inputIndex)
}
utxo := pIn.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(pIn.WitnessScript) == 0 {
return fmt.Errorf("invalid PSBT, input %d is "+
"missing witness script", inputIndex)
}
witnessScript = pIn.WitnessScript
}
localPrivateKey, err := localKey.ECPrivKey()
if err != nil {
return fmt.Errorf("error getting private key: %w", err)
}
// Do we already have a partial signature for our key?
localPubKey := localPrivateKey.PubKey().SerializeCompressed()
haveSig := false
for _, partialSig := range pIn.PartialSigs {
if bytes.Equal(partialSig.PubKey, localPubKey) {
haveSig = true
}
}
if haveSig {
log.Infof("Already have a partial signature for input "+
"%d and local key %x, skipping", inputIndex,
localPubKey)
continue
}
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,
pIn *psbt.PInput) ([]uint32, error) {
pubKey, err := rootKey.ECPubKey()
if err != nil {
return nil, fmt.Errorf("error getting public key: %w", err)
}
pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
fingerprint := binary.LittleEndian.Uint32(pubKeyHash[:4])
if len(pIn.Bip32Derivation) == 0 {
return nil, errNoPathFound
}
for _, derivation := range pIn.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(pIn.Bip32Derivation) == 1 {
return derivation.Bip32Path, nil
}
// The normal case, where a derivation path has the master
// fingerprint set.
if derivation.MasterKeyFingerprint == fingerprint {
return derivation.Bip32Path, nil
}
}
return nil, errNoPathFound
}

@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
@ -115,11 +116,11 @@ func signRescueFunding(rootKey *hdkeychain.ExtendedKey,
return fmt.Errorf("could not find local multisig key: %w", err) return fmt.Errorf("could not find local multisig key: %w", err)
} }
if len(packet.Inputs[0].WitnessScript) == 0 { if len(packet.Inputs[0].WitnessScript) == 0 {
return fmt.Errorf("invalid PSBT, missing witness script") return errors.New("invalid PSBT, missing witness script")
} }
witnessScript := packet.Inputs[0].WitnessScript witnessScript := packet.Inputs[0].WitnessScript
if packet.Inputs[0].WitnessUtxo == nil { if packet.Inputs[0].WitnessUtxo == nil {
return fmt.Errorf("invalid PSBT, witness UTXO missing") return errors.New("invalid PSBT, witness UTXO missing")
} }
utxo := packet.Inputs[0].WitnessUtxo utxo := packet.Inputs[0].WitnessUtxo
@ -157,7 +158,7 @@ func findLocalMultisigKey(multisigBranch *hdkeychain.ExtendedKey,
targetPubkey *btcec.PublicKey) (*keychain.KeyDescriptor, error) { targetPubkey *btcec.PublicKey) (*keychain.KeyDescriptor, error) {
// Loop through the local multisig keys to find the target key. // Loop through the local multisig keys to find the target key.
for index := uint32(0); index < MaxChannelLookup; index++ { for index := range uint32(MaxChannelLookup) {
currentKey, err := multisigBranch.DeriveNonStandard(index) currentKey, err := multisigBranch.DeriveNonStandard(index)
if err != nil { if err != nil {
return nil, fmt.Errorf("error deriving child key: %w", return nil, fmt.Errorf("error deriving child key: %w",
@ -183,5 +184,5 @@ func findLocalMultisigKey(multisigBranch *hdkeychain.ExtendedKey,
}, nil }, nil
} }
return nil, fmt.Errorf("no matching pubkeys found") return nil, errors.New("no matching pubkeys found")
} }

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

@ -53,6 +53,7 @@ funds can be swept after the force-close transaction was confirmed.
Supported remote force-closed channel types are: Supported remote force-closed channel types are:
- STATIC_REMOTE_KEY (a.k.a. tweakless channels) - STATIC_REMOTE_KEY (a.k.a. tweakless channels)
- ANCHOR (a.k.a. anchor output channels) - ANCHOR (a.k.a. anchor output channels)
- SIMPLE_TAPROOT (a.k.a. simple taproot channels)
`, `,
Example: `chantools sweepremoteclosed \ Example: `chantools sweepremoteclosed \
--recoverywindow 300 \ --recoverywindow 300 \
@ -75,7 +76,9 @@ Supported remote force-closed channel types are:
"API instead of just printing the TX", "API instead of just printing the TX",
) )
cc.cmd.Flags().StringVar( 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().Uint32Var( cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+ &cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
@ -94,8 +97,12 @@ func (c *sweepRemoteClosedCommand) Execute(_ *cobra.Command, _ []string) error {
} }
// Make sure sweep addr is set. // Make sure sweep addr is set.
if c.SweepAddr == "" { err = lnd.CheckAddress(
return fmt.Errorf("sweep addr is required") c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
} }
// Set default values. // Set default values.
@ -113,23 +120,32 @@ func (c *sweepRemoteClosedCommand) Execute(_ *cobra.Command, _ []string) error {
} }
type targetAddr struct { type targetAddr struct {
addr btcutil.Address addr btcutil.Address
pubKey *btcec.PublicKey pubKey *btcec.PublicKey
path string path string
keyDesc *keychain.KeyDescriptor keyDesc *keychain.KeyDescriptor
vouts []*btc.Vout vouts []*btc.Vout
script []byte script []byte
scriptTree *input.CommitScriptTree
} }
func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL, func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
sweepAddr string, recoveryWindow uint32, feeRate uint32, sweepAddr string, recoveryWindow uint32, feeRate uint32,
publish bool) error { publish bool) error {
var estimator input.TxWeightEstimator
sweepScript, err := lnd.PrepareWalletAddress(
sweepAddr, chainParams, &estimator, extendedKey, "sweep",
)
if err != nil {
return err
}
var ( var (
targets []*targetAddr targets []*targetAddr
api = &btc.ExplorerAPI{BaseURL: apiURL} api = newExplorerAPI(apiURL)
) )
for index := uint32(0); index < recoveryWindow; index++ { for index := range recoveryWindow {
path := fmt.Sprintf("m/1017'/%d'/%d'/0/%d", path := fmt.Sprintf("m/1017'/%d'/%d'/0/%d",
chainParams.HDCoinType, keychain.KeyFamilyPaymentBase, chainParams.HDCoinType, keychain.KeyFamilyPaymentBase,
index) index)
@ -169,7 +185,6 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
// Create estimator and transaction template. // Create estimator and transaction template.
var ( var (
estimator input.TxWeightEstimator
signDescs []*input.SignDescriptor signDescs []*input.SignDescriptor
sweepTx = wire.NewMsgTx(2) sweepTx = wire.NewMsgTx(2)
totalOutputValue = uint64(0) totalOutputValue = uint64(0)
@ -196,18 +211,6 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
err) 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{ prevOutPoint := wire.OutPoint{
Hash: *txHash, Hash: *txHash,
Index: uint32(vout.Outspend.Vin), Index: uint32(vout.Outspend.Vin),
@ -217,18 +220,76 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
Value: int64(vout.Value), Value: int64(vout.Value),
} }
prevOutFetcher.AddPrevOut(prevOutPoint, prevTxOut) prevOutFetcher.AddPrevOut(prevOutPoint, prevTxOut)
sweepTx.TxIn = append(sweepTx.TxIn, &wire.TxIn{ txIn := &wire.TxIn{
PreviousOutPoint: prevOutPoint, PreviousOutPoint: prevOutPoint,
Sequence: sequence, Sequence: wire.MaxTxInSequenceNum,
}) }
sweepTx.TxIn = append(sweepTx.TxIn, txIn)
inputIndex := len(sweepTx.TxIn) - 1
signDescs = append(signDescs, &input.SignDescriptor{ var signDesc *input.SignDescriptor
KeyDesc: *target.keyDesc, switch target.addr.(type) {
WitnessScript: target.script, case *btcutil.AddressWitnessPubKeyHash:
Output: prevTxOut, estimator.AddP2WKHInput()
HashType: txscript.SigHashAll,
PrevOutputFetcher: prevOutFetcher, 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,17 +299,10 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
len(targets), totalOutputValue, sweepDustLimit) 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 // Calculate the fee based on the given fee rate and our weight
// estimation. // estimation.
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight())) totalFee := feeRateKWeight.FeeForWeight(estimator.Weight())
log.Infof("Fee %d sats of %d total amount (estimated weight %d)", log.Infof("Fee %d sats of %d total amount (estimated weight %d)",
totalFee, totalOutputValue, estimator.Weight()) totalFee, totalOutputValue, estimator.Weight())
@ -270,7 +324,19 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
desc.SigHashes = sigHashes desc.SigHashes = sigHashes
desc.InputIndex = idx 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( witness, err := input.CommitSpendToRemoteConfirmed(
signer, desc, sweepTx, signer, desc, sweepTx,
) )
@ -278,7 +344,9 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
return err return err
} }
sweepTx.TxIn[idx].Witness = witness sweepTx.TxIn[idx].Witness = witness
} else {
// Static Remote Key Channels.
default:
// The txscript library expects the witness script of a // The txscript library expects the witness script of a
// P2WKH descriptor to be set to the pkScript of the // P2WKH descriptor to be set to the pkScript of the
// output... // output...
@ -320,7 +388,9 @@ func queryAddressBalances(pubKey *btcec.PublicKey, path string,
error) { error) {
var targets []*targetAddr 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()) unspent, err := api.Unspent(address.EncodeAddress())
if err != nil { if err != nil {
return fmt.Errorf("could not query unspent: %w", err) 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", log.Infof("Found %d unspent outputs for address %v",
len(unspent), address.EncodeAddress()) len(unspent), address.EncodeAddress())
targets = append(targets, &targetAddr{ targets = append(targets, &targetAddr{
addr: address, addr: address,
pubKey: pubKey, pubKey: pubKey,
path: path, path: path,
keyDesc: keyDesc, keyDesc: keyDesc,
vouts: unspent, vouts: unspent,
script: script, script: script,
scriptTree: scriptTree,
}) })
} }
@ -346,7 +417,7 @@ func queryAddressBalances(pubKey *btcec.PublicKey, path string,
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := queryAddr(p2wkh, nil); err != nil { if err := queryAddr(p2wkh, nil, nil); err != nil {
return nil, err return nil, err
} }
@ -354,7 +425,15 @@ func queryAddressBalances(pubKey *btcec.PublicKey, path string,
if err != nil { if err != nil {
return nil, err 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 return nil, err
} }

@ -10,7 +10,6 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/dataformat" "github.com/lightninglabs/chantools/dataformat"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
@ -65,7 +64,9 @@ parameter to 144.`,
"API instead of just printing the TX", "API instead of just printing the TX",
) )
cc.cmd.Flags().StringVar( 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().Uint16Var(
&cc.MaxCsvLimit, "maxcsvlimit", defaultCsvLimit, "maximum CSV "+ &cc.MaxCsvLimit, "maxcsvlimit", defaultCsvLimit, "maximum CSV "+
@ -89,8 +90,12 @@ func (c *sweepTimeLockCommand) Execute(_ *cobra.Command, _ []string) error {
} }
// Make sure sweep addr is set. // Make sure sweep addr is set.
if c.SweepAddr == "" { err = lnd.CheckAddress(
return fmt.Errorf("sweep addr is required") c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
} }
// Parse channel entries from any of the possible input files. // Parse channel entries from any of the possible input files.
@ -216,18 +221,26 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
publish bool, feeRate uint32) error { publish bool, feeRate uint32) error {
// Create signer and transaction template. // Create signer and transaction template.
signer := &lnd.Signer{ var (
ExtendedKey: extendedKey, estimator input.TxWeightEstimator
ChainParams: chainParams, 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 ( var (
sweepTx = wire.NewMsgTx(2) sweepTx = wire.NewMsgTx(2)
totalOutputValue = int64(0) totalOutputValue = int64(0)
signDescs = make([]*input.SignDescriptor, 0) signDescs = make([]*input.SignDescriptor, 0)
prevOutFetcher = txscript.NewMultiPrevOutFetcher(nil) prevOutFetcher = txscript.NewMultiPrevOutFetcher(nil)
estimator input.TxWeightEstimator
) )
for _, target := range targets { for _, target := range targets {
// We can't rely on the CSV delay of the channel DB to be // 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( ), input.DeriveRevocationPubkey(
target.revocationBasePoint, target.revocationBasePoint,
target.commitPoint, target.commitPoint,
), target.lockScript, maxCsvTimeout, ), target.lockScript, 0, maxCsvTimeout,
) )
if err != nil { if err != nil {
log.Errorf("Could not create matching script for %s "+ log.Errorf("could not create matching script for %s "+
"or csv too high: %w", target.channelPoint, err) "or csv too high: %v", target.channelPoint, err)
continue continue
} }
@ -283,17 +296,10 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
estimator.AddWitnessInput(input.ToLocalTimeoutWitnessSize) 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 // Calculate the fee based on the given fee rate and our weight
// estimation. // estimation.
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight())) totalFee := feeRateKWeight.FeeForWeight(estimator.Weight())
log.Infof("Fee %d sats of %d total amount (estimated weight %d)", log.Infof("Fee %d sats of %d total amount (estimated weight %d)",
totalFee, totalOutputValue, estimator.Weight()) totalFee, totalOutputValue, estimator.Weight())
@ -346,14 +352,14 @@ func pubKeyFromHex(pubKeyHex string) (*btcec.PublicKey, error) {
} }
func bruteForceDelay(delayPubkey, revocationPubkey *btcec.PublicKey, func bruteForceDelay(delayPubkey, revocationPubkey *btcec.PublicKey,
targetScript []byte, maxCsvTimeout uint16) (int32, []byte, []byte, targetScript []byte, startCsvTimeout, maxCsvTimeout uint16) (int32,
error) { []byte, []byte, error) {
if len(targetScript) != 34 { if len(targetScript) != 34 {
return 0, nil, nil, fmt.Errorf("invalid target script: %s", return 0, nil, nil, fmt.Errorf("invalid target script: %s",
targetScript) targetScript)
} }
for i := uint16(0); i <= maxCsvTimeout; i++ { for i := startCsvTimeout; i <= maxCsvTimeout; i++ {
s, err := input.CommitScriptToSelf( s, err := input.CommitScriptToSelf(
uint32(i), delayPubkey, revocationPubkey, uint32(i), delayPubkey, revocationPubkey,
) )

@ -3,6 +3,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
@ -10,7 +11,6 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
@ -34,8 +34,11 @@ type sweepTimeLockManualCommand struct {
TimeLockAddr string TimeLockAddr string
RemoteRevocationBasePoint string RemoteRevocationBasePoint string
MaxNumChansTotal uint16 MaxNumChannelsTotal uint16
MaxNumChanUpdates uint64 MaxNumChanUpdates uint64
ChannelBackup string
ChannelPoint string
rootKey *rootKey rootKey *rootKey
inputs *inputFlags inputs *inputFlags
@ -56,6 +59,9 @@ and only the channel.backup file is available.
To get the value for --remoterevbasepoint you must use the dumpbackup command, To get the value for --remoterevbasepoint you must use the dumpbackup command,
then look up the value for RemoteChanCfg -> RevocationBasePoint -> PubKey. 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 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 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).`, address is always the one that's longer (because it's P2WSH and not P2PKH).`,
@ -64,6 +70,14 @@ address is always the one that's longer (because it's P2WSH and not P2PKH).`,
--timelockaddr bc1q............ \ --timelockaddr bc1q............ \
--remoterevbasepoint 03xxxxxxx \ --remoterevbasepoint 03xxxxxxx \
--feerate 10 \ --feerate 10 \
--publish
chantools sweeptimelockmanual \
--sweepaddr bc1q..... \
--timelockaddr bc1q............ \
--frombackup channel.backup \
--channelpoint f39310xxxxxxxxxx:1 \
--feerate 10 \
--publish`, --publish`,
RunE: cc.Execute, RunE: cc.Execute,
} }
@ -76,14 +90,16 @@ address is always the one that's longer (because it's P2WSH and not P2PKH).`,
"API instead of just printing the TX", "API instead of just printing the TX",
) )
cc.cmd.Flags().StringVar( 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().Uint16Var(
&cc.MaxCsvLimit, "maxcsvlimit", defaultCsvLimit, "maximum CSV "+ &cc.MaxCsvLimit, "maxcsvlimit", defaultCsvLimit, "maximum CSV "+
"limit to use", "limit to use",
) )
cc.cmd.Flags().Uint16Var( cc.cmd.Flags().Uint16Var(
&cc.MaxNumChansTotal, "maxnumchanstotal", maxKeys, "maximum "+ &cc.MaxNumChannelsTotal, "maxnumchanstotal", maxKeys, "maximum "+
"number of keys to try, set to maximum number of "+ "number of keys to try, set to maximum number of "+
"channels the local node potentially has or had", "channels the local node potentially has or had",
) )
@ -105,6 +121,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 "+ "remote node's revocation base point, can be found "+
"in a channel.backup file", "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.rootKey = newRootKey(cc.cmd, "deriving keys")
cc.inputs = newInputFlags(cc.cmd) cc.inputs = newInputFlags(cc.cmd)
@ -119,16 +145,84 @@ func (c *sweepTimeLockManualCommand) Execute(_ *cobra.Command, _ []string) error
} }
// Make sure the sweep and time lock addrs are set. // Make sure the sweep and time lock addrs are set.
if c.SweepAddr == "" { err = lnd.CheckAddress(
return fmt.Errorf("sweep addr is required") 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 errors.New("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 errors.New("cannot use both --frombackup and " +
"--remoterevbasepoint at the same time")
default:
return errors.New("either --frombackup or " +
"--remoterevbasepoint is required")
} }
// The remote revocation base point must also be set and a valid EC // The remote revocation base point must also be set and a valid EC
// point. // point.
remoteRevPoint, err := pubKeyFromHex(c.RemoteRevocationBasePoint) remoteRevPoint, err := pubKeyFromHex(remoteRevocationBasePoint)
if err != nil { if err != nil {
return fmt.Errorf("invalid remote revocation base point: %w", return fmt.Errorf("invalid remote revocation base point: %w",
err) err)
@ -136,22 +230,49 @@ func (c *sweepTimeLockManualCommand) Execute(_ *cobra.Command, _ []string) error
return sweepTimeLockManual( return sweepTimeLockManual(
extendedKey, c.APIURL, c.SweepAddr, c.TimeLockAddr, extendedKey, c.APIURL, c.SweepAddr, c.TimeLockAddr,
remoteRevPoint, c.MaxCsvLimit, c.MaxNumChansTotal, remoteRevPoint, startCsvLimit, maxCsvLimit,
startNumChannelsTotal, maxNumChannelsTotal,
c.MaxNumChanUpdates, c.Publish, c.FeeRate, c.MaxNumChanUpdates, c.Publish, c.FeeRate,
) )
} }
func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string, func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
sweepAddr, timeLockAddr string, remoteRevPoint *btcec.PublicKey, sweepAddr, timeLockAddr string, remoteRevPoint *btcec.PublicKey,
maxCsvTimeout, maxNumChannels uint16, maxNumChanUpdates uint64, startCsvTimeout, maxCsvTimeout, startNumChannels, maxNumChannels uint16,
publish bool, feeRate uint32) error { 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 // 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 // brute force the script with the information we have. If not, we can't
// continue anyway. // continue anyway.
lockScript, err := lnd.GetP2WSHScript(timeLockAddr, chainParams) lockScript, err := lnd.PrepareWalletAddress(
sweepAddr, chainParams, nil, extendedKey, "time lock",
)
if err != nil { if err != nil {
return fmt.Errorf("invalid time lock addr: %w", err) return err
}
sweepScript, err := lnd.PrepareWalletAddress(
sweepAddr, chainParams, &estimator, extendedKey, "sweep",
)
if err != nil {
return err
} }
// We need to go through a lot of our keys so it makes sense to // We need to go through a lot of our keys so it makes sense to
@ -179,10 +300,10 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
delayDesc *keychain.KeyDescriptor delayDesc *keychain.KeyDescriptor
commitPoint *btcec.PublicKey commitPoint *btcec.PublicKey
) )
for i := uint16(0); i < maxNumChannels; i++ { for i := startNumChannels; i < maxNumChannels; i++ {
csvTimeout, script, scriptHash, commitPoint, delayDesc, err = tryKey( csvTimeout, script, scriptHash, commitPoint, delayDesc, err = tryKey(
baseKey, remoteRevPoint, maxCsvTimeout, lockScript, baseKey, remoteRevPoint, startCsvTimeout, maxCsvTimeout,
uint32(i), maxNumChanUpdates, lockScript, uint32(i), maxNumChanUpdates,
) )
if err == nil { if err == nil {
@ -198,16 +319,9 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
// Did we find what we looked for or did we just exhaust all // Did we find what we looked for or did we just exhaust all
// possibilities? // possibilities?
if script == nil || delayDesc == nil { if script == nil || delayDesc == nil {
return fmt.Errorf("target script not derived") return errors.New("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, // 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 // except for what outpoint to sweep. We'll ask the chain API to give
// us this information. // us this information.
@ -237,17 +351,11 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
// Calculate the fee based on the given fee rate and our weight // Calculate the fee based on the given fee rate and our weight
// estimation. // estimation.
var estimator input.TxWeightEstimator
estimator.AddWitnessInput(input.ToLocalTimeoutWitnessSize) estimator.AddWitnessInput(input.ToLocalTimeoutWitnessSize)
estimator.AddP2WKHOutput()
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight())) totalFee := feeRateKWeight.FeeForWeight(estimator.Weight())
// Add our sweep destination output. // Add our sweep destination output.
sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams)
if err != nil {
return err
}
sweepTx.TxOut = []*wire.TxOut{{ sweepTx.TxOut = []*wire.TxOut{{
Value: sweepValue - int64(totalFee), Value: sweepValue - int64(totalFee),
PkScript: sweepScript, PkScript: sweepScript,
@ -305,7 +413,7 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
} }
func tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey, 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, maxNumChanUpdates uint64) (int32, []byte, []byte, *btcec.PublicKey,
*keychain.KeyDescriptor, error) { *keychain.KeyDescriptor, error) {
@ -338,7 +446,7 @@ func tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey,
// points and CSV values. // points and CSV values.
csvTimeout, script, scriptHash, commitPoint, err := bruteForceDelayPoint( csvTimeout, script, scriptHash, commitPoint, err := bruteForceDelayPoint(
delayPrivKey.PubKey(), remoteRevPoint, revRoot, lockScript, delayPrivKey.PubKey(), remoteRevPoint, revRoot, lockScript,
maxCsvTimeout, maxNumChanUpdates, startCsvTimeout, maxCsvTimeout, maxNumChanUpdates,
) )
if err == nil { if err == nil {
return csvTimeout, script, scriptHash, commitPoint, return csvTimeout, script, scriptHash, commitPoint,
@ -403,7 +511,7 @@ func tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey,
csvTimeout, script, scriptHash, commitPoint, err = bruteForceDelayPoint( csvTimeout, script, scriptHash, commitPoint, err = bruteForceDelayPoint(
delayPrivKey.PubKey(), remoteRevPoint, revRoot2, lockScript, delayPrivKey.PubKey(), remoteRevPoint, revRoot2, lockScript,
maxCsvTimeout, maxNumChanUpdates, startCsvTimeout, maxCsvTimeout, maxNumChanUpdates,
) )
if err == nil { if err == nil {
return csvTimeout, script, scriptHash, commitPoint, return csvTimeout, script, scriptHash, commitPoint,
@ -444,7 +552,7 @@ func tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey,
csvTimeout, script, scriptHash, commitPoint, err = bruteForceDelayPoint( csvTimeout, script, scriptHash, commitPoint, err = bruteForceDelayPoint(
delayPrivKey.PubKey(), remoteRevPoint, revRoot3, lockScript, delayPrivKey.PubKey(), remoteRevPoint, revRoot3, lockScript,
maxCsvTimeout, maxNumChanUpdates, startCsvTimeout, maxCsvTimeout, maxNumChanUpdates,
) )
if err == nil { if err == nil {
return csvTimeout, script, scriptHash, commitPoint, return csvTimeout, script, scriptHash, commitPoint,
@ -457,15 +565,15 @@ func tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey,
}, nil }, nil
} }
return 0, nil, nil, nil, nil, fmt.Errorf("target script not derived") return 0, nil, nil, nil, nil, errors.New("target script not derived")
} }
func bruteForceDelayPoint(delayBase, revBase *btcec.PublicKey, func bruteForceDelayPoint(delayBase, revBase *btcec.PublicKey,
revRoot *shachain.RevocationProducer, lockScript []byte, revRoot *shachain.RevocationProducer, lockScript []byte,
maxCsvTimeout uint16, maxChanUpdates uint64) (int32, []byte, []byte, startCsvTimeout, maxCsvTimeout uint16, maxChanUpdates uint64) (int32,
*btcec.PublicKey, error) { []byte, []byte, *btcec.PublicKey, error) {
for i := uint64(0); i < maxChanUpdates; i++ { for i := range maxChanUpdates {
revPreimage, err := revRoot.AtIndex(i) revPreimage, err := revRoot.AtIndex(i)
if err != nil { if err != nil {
return 0, nil, nil, nil, err return 0, nil, nil, nil, err
@ -475,7 +583,7 @@ func bruteForceDelayPoint(delayBase, revBase *btcec.PublicKey,
csvTimeout, script, scriptHash, err := bruteForceDelay( csvTimeout, script, scriptHash, err := bruteForceDelay(
input.TweakPubKey(delayBase, commitPoint), input.TweakPubKey(delayBase, commitPoint),
input.DeriveRevocationPubkey(revBase, commitPoint), input.DeriveRevocationPubkey(revBase, commitPoint),
lockScript, maxCsvTimeout, lockScript, startCsvTimeout, maxCsvTimeout,
) )
if err != nil { if err != nil {
@ -485,5 +593,5 @@ func bruteForceDelayPoint(delayBase, revBase *btcec.PublicKey,
return csvTimeout, script, scriptHash, commitPoint, nil return csvTimeout, script, scriptHash, commitPoint, nil
} }
return 0, nil, nil, nil, fmt.Errorf("target script not derived") return 0, nil, nil, nil, errors.New("target script not derived")
} }

@ -86,7 +86,7 @@ func TestSweepTimeLockManual(t *testing.T) {
revPubKey, _ := btcec.ParsePubKey(revPubKeyBytes) revPubKey, _ := btcec.ParsePubKey(revPubKeyBytes)
_, _, _, _, _, err = tryKey( _, _, _, _, _, err = tryKey(
baseKey, revPubKey, defaultCsvLimit, lockScript, baseKey, revPubKey, 0, defaultCsvLimit, lockScript,
tc.keyIndex, 500, tc.keyIndex, 500,
) )
require.NoError(t, err) require.NoError(t, err)

@ -2,26 +2,28 @@ package main
import ( import (
"fmt" "fmt"
"net"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/connmgr" "github.com/btcsuite/btcd/connmgr"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/brontide" "github.com/lightningnetwork/lnd/brontide"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/peer"
"github.com/lightningnetwork/lnd/tor" "github.com/lightningnetwork/lnd/tor"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var ( var (
dialTimeout = time.Minute dialTimeout = time.Minute
defaultTorDNSHostPort = "soa.nodes.lightning.directory:53"
) )
type triggerForceCloseCommand struct { type triggerForceCloseCommand struct {
@ -30,6 +32,8 @@ type triggerForceCloseCommand struct {
APIURL string APIURL string
TorProxy string
rootKey *rootKey rootKey *rootKey
cmd *cobra.Command cmd *cobra.Command
} }
@ -38,13 +42,13 @@ func newTriggerForceCloseCommand() *cobra.Command {
cc := &triggerForceCloseCommand{} cc := &triggerForceCloseCommand{}
cc.cmd = &cobra.Command{ cc.cmd = &cobra.Command{
Use: "triggerforceclose", Use: "triggerforceclose",
Short: "Connect to a CLN peer and send a custom message to " + Short: "Connect to a Lightning Network peer and send " +
"trigger a force close of the specified channel", "specific messages to trigger a force close of the " +
Long: `Certain versions of CLN didn't properly react to error "specified channel",
messages sent by peers and therefore didn't follow the DLP protocol to recover Long: `Asks the specified remote peer to force close a specific
channel funds using SCB. This command can be used to trigger a force close with channel by first sending a channel re-establish message, and if that doesn't
those earlier versions of CLN (this command will not work for lnd peers or CLN work, a custom error message (in case the peer is a specific version of CLN that
peers of a different version).`, does not properly respond to a Data Loss Protection re-establish message).'`,
Example: `chantools triggerforceclose \ Example: `chantools triggerforceclose \
--peer 03abce...@xx.yy.zz.aa:9735 \ --peer 03abce...@xx.yy.zz.aa:9735 \
--channel_point abcdef01234...:x`, --channel_point abcdef01234...:x`,
@ -63,6 +67,10 @@ peers of a different version).`,
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+ &cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
"be esplora compatible)", "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") cc.rootKey = newRootKey(cc.cmd, "deriving the identity key")
return cc.cmd return cc.cmd
@ -89,101 +97,152 @@ func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
PrivKey: identityPriv, PrivKey: identityPriv,
} }
peerAddr, err := lncfg.ParseLNAddressString( outPoint, err := parseOutPoint(c.ChannelPoint)
c.Peer, "9735", net.ResolveTCPAddr, if err != nil {
return fmt.Errorf("error parsing channel point: %w", err)
}
err = requestForceClose(
c.Peer, c.TorProxy, pubKey, *outPoint, identityECDH,
) )
if err != nil { 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 { 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( spends, err := api.Spends(channelAddress)
identityECDH, peerAddr, &tor.ClearNet{}, dialTimeout, 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 { 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", log.Debugf("Attempting to dial resolved peer address %v",
pubKey.SerializeCompressed(), dialTimeout) 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{ req := &connmgr.ConnReq{
Addr: peerAddr, Addr: peerAddr,
Permanent: false, Permanent: false,
} }
p, err := lnd.ConnectPeer(conn, req, chainParams, identityECDH) p, err := lnd.ConnectPeer(conn, req, chainParams, identity)
if err != nil { 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", log.Infof("Connection established to peer %x",
pubKey.SerializeCompressed()) peerPubKey.SerializeCompressed())
// We'll wait until the peer is active. // We'll wait until the peer is active.
select { select {
case <-p.ActiveSignal(): case <-p.ActiveSignal():
case <-p.QuitSignal(): case <-p.QuitSignal():
return fmt.Errorf("peer %x disconnected", return nil, fmt.Errorf("peer %x disconnected",
pubKey.SerializeCompressed()) 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). // Channel ID (32 byte) + u16 for the data length (which will be 0).
data := make([]byte, 34) data := make([]byte, 34)
copy(data[:32], channelID[:]) copy(data[:32], channelID[:])
log.Infof("Sending channel error message to peer to trigger force "+ log.Infof("Sending channel re-establish to peer to trigger force "+
"close of channel %v", c.ChannelPoint) "close of channel %v", channelPoint)
_ = lnwire.SetCustomOverrides([]uint16{lnwire.MsgError}) err = p.SendMessageLazy(true, &lnwire.ChannelReestablish{
msg, err := lnwire.NewCustom(lnwire.MsgError, data) ChanID: channelID,
})
if err != nil { if err != nil {
return err return err
} }
err = p.SendMessageLazy(true, msg) log.Infof("Sending channel error message to peer to trigger force "+
if err != nil { "close of channel %v", channelPoint)
return fmt.Errorf("error sending message: %w", err)
}
log.Infof("Message sent, waiting for force close transaction to " +
"appear in mempool")
api := &btc.ExplorerAPI{BaseURL: c.APIURL} _ = lnwire.SetCustomOverrides([]uint16{
channelAddress, err := api.Address(c.ChannelPoint) lnwire.MsgError, lnwire.MsgChannelReestablish,
})
msg, err := lnwire.NewCustom(lnwire.MsgError, data)
if err != nil { 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 { if err != nil {
return fmt.Errorf("error getting spends: %w", err) return fmt.Errorf("error sending message: %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 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) { func parseOutPoint(s string) (*wire.OutPoint, error) {
split := strings.Split(s, ":") split := strings.Split(s, ":")
if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 { if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"math" "math"
"runtime" "runtime"
@ -71,14 +72,14 @@ func (c *vanityGenCommand) Execute(_ *cobra.Command, _ []string) error {
} }
if len(prefixBytes) < 2 { if len(prefixBytes) < 2 {
return fmt.Errorf("prefix must be at least 2 bytes") return errors.New("prefix must be at least 2 bytes")
} }
if len(prefixBytes) > 8 { if len(prefixBytes) > 8 {
return fmt.Errorf("prefix too long, unlikely to find a key " + return errors.New("prefix too long, unlikely to find a key " +
"within billions of years") "within billions of years")
} }
if !(prefixBytes[0] == 0x02 || prefixBytes[0] == 0x03) { if !(prefixBytes[0] == 0x02 || prefixBytes[0] == 0x03) {
return fmt.Errorf("prefix must start with 02 or 03 because " + return errors.New("prefix must start with 02 or 03 because " +
"it's an EC public key") "it's an EC public key")
} }
@ -103,7 +104,7 @@ func (c *vanityGenCommand) Execute(_ *cobra.Command, _ []string) error {
start = time.Now() start = time.Now()
) )
for i := uint8(0); i < c.Threads; i++ { for range c.Threads {
go func() { go func() {
var ( var (
entropy [16]byte entropy [16]byte

@ -3,26 +3,18 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"strings"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcwallet/snacl"
"github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/walletdb"
_ "github.com/btcsuite/btcwallet/walletdb/bdb" _ "github.com/btcsuite/btcwallet/walletdb/bdb"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.etcd.io/bbolt"
) )
const ( const (
passwordEnvName = "WALLET_PASSWORD"
walletInfoFormat = ` walletInfoFormat = `
Identity Pubkey: %x Identity Pubkey: %x
BIP32 HD extended root key: %s BIP32 HD extended root key: %s
@ -38,19 +30,7 @@ Scope: m/%d'/%d'
) )
var ( var (
// Namespace from github.com/btcsuite/btcwallet/wallet/wallet.go. defaultAccount = uint32(waddrmgr.DefaultAccountNum)
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,
}
) )
type walletInfoCommand struct { type walletInfoCommand struct {
@ -98,75 +78,24 @@ or simply press <enter> without entering a password when being prompted.`,
} }
func (c *walletInfoCommand) Execute(_ *cobra.Command, _ []string) error { func (c *walletInfoCommand) Execute(_ *cobra.Command, _ []string) error {
var (
publicWalletPw = lnwallet.DefaultPublicPassphrase
privateWalletPw = lnwallet.DefaultPrivatePassphrase
err error
)
// Check that we have a wallet DB. // Check that we have a wallet DB.
if c.WalletDB == "" { if c.WalletDB == "" {
return fmt.Errorf("wallet DB is required") return errors.New("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. w, privateWalletPw, cleanup, err := lnd.OpenWallet(
db, err := walletdb.Open( c.WalletDB, chainParams,
"bdb", lncfg.CleanAndExpandPath(c.WalletDB), false,
lnd.DefaultOpenTimeout,
) )
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 { 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) defer func() {
if err != nil { if err := cleanup(); err != nil {
return err log.Errorf("error closing wallet: %v", err)
} }
}()
// Start and unlock the wallet.
w.Start()
defer w.Stop()
err = w.Unlock(privateWalletPw, nil)
if err != nil {
return err
}
// Print the wallet info and if requested the root key. // Print the wallet info and if requested the root key.
identityKey, scopeInfo, err := walletInfo(w, c.DumpAddrs) identityKey, scopeInfo, err := walletInfo(w, c.DumpAddrs)
@ -175,7 +104,9 @@ func (c *walletInfoCommand) Execute(_ *cobra.Command, _ []string) error {
} }
rootKey := na rootKey := na
if c.WithRootKey { if c.WithRootKey {
masterHDPrivKey, err := decryptRootKey(db, privateWalletPw) masterHDPrivKey, err := lnd.DecryptWalletRootKey(
w.Database(), privateWalletPw,
)
if err != nil { if err != nil {
return err return err
} }
@ -233,7 +164,7 @@ func walletInfo(w *wallet.Wallet, dumpAddrs bool) (*btcec.PublicKey, string,
printAddr := func(a waddrmgr.ManagedAddress) error { printAddr := func(a waddrmgr.ManagedAddress) error {
pka, ok := a.(waddrmgr.ManagedPubKeyAddress) pka, ok := a.(waddrmgr.ManagedPubKeyAddress)
if !ok { if !ok {
return fmt.Errorf("key is not a managed pubkey") return errors.New("key is not a managed pubkey")
} }
privKey, err := pka.PrivKey() privKey, err := pka.PrivKey()
@ -259,7 +190,7 @@ func walletInfo(w *wallet.Wallet, dumpAddrs bool) (*btcec.PublicKey, string,
err = walletdb.View( err = walletdb.View(
w.Database(), func(tx walletdb.ReadTx) error { w.Database(), func(tx walletdb.ReadTx) error {
waddrmgrNs := tx.ReadBucket( waddrmgrNs := tx.ReadBucket(
waddrmgrNamespaceKey, lnd.WaddrmgrNamespaceKey,
) )
return mgr.ForEachAccountAddress( return mgr.ForEachAccountAddress(
@ -304,64 +235,3 @@ func printScopeInfo(name string, w *wallet.Wallet,
return scopeInfo, nil 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 ( import (
"testing" "testing"
"github.com/lightninglabs/chantools/lnd"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -20,7 +21,7 @@ func TestWalletInfo(t *testing.T) {
WithRootKey: true, WithRootKey: true,
} }
t.Setenv(passwordEnvName, testPassPhrase) t.Setenv(lnd.PasswordEnvName, testPassPhrase)
err := info.Execute(nil, nil) err := info.Execute(nil, nil)
require.NoError(t, err) require.NoError(t, err)

@ -14,7 +14,6 @@ import (
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/hasura/go-graphql-client" "github.com/hasura/go-graphql-client"
"github.com/lightninglabs/chantools/btc"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@ -188,7 +187,7 @@ func (c *zombieRecoveryFindMatchesCommand) Execute(_ *cobra.Command,
log.Infof("%s: %s", groups[1], groups[2]) 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}) src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.AmbossKey})
httpClient := oauth2.NewClient(context.Background(), src) httpClient := oauth2.NewClient(context.Background(), src)
client := graphql.NewClient( client := graphql.NewClient(
@ -299,7 +298,7 @@ func (c *zombieRecoveryFindMatchesCommand) Execute(_ *cobra.Command,
Node1: node1, Node1: node1,
} }
folder := fmt.Sprintf("results/match-%s", node1) folder := "results/match-" + node1
today := time.Now().Format("2006-01-02") today := time.Now().Format("2006-01-02")
for node2, match := range node1map { for node2, match := range node1map {
err = os.MkdirAll(folder, 0755) err = os.MkdirAll(folder, 0755)

@ -5,8 +5,8 @@ import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@ -28,6 +28,8 @@ type zombieRecoveryMakeOfferCommand struct {
Node2 string Node2 string
FeeRate uint32 FeeRate uint32
MatchOnly bool
rootKey *rootKey rootKey *rootKey
cmd *cobra.Command cmd *cobra.Command
} }
@ -64,6 +66,10 @@ a counter offer.`,
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+ &cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
"use for the sweep transaction in sat/vByte", "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") cc.rootKey = newRootKey(cc.cmd, "signing the offer")
@ -82,12 +88,12 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
c.FeeRate = defaultFeeSatPerVByte c.FeeRate = defaultFeeSatPerVByte
} }
node1Bytes, err := ioutil.ReadFile(c.Node1) node1Bytes, err := os.ReadFile(c.Node1)
if err != nil { if err != nil {
return fmt.Errorf("error reading node1 key file %s: %w", return fmt.Errorf("error reading node1 key file %s: %w",
c.Node1, err) c.Node1, err)
} }
node2Bytes, err := ioutil.ReadFile(c.Node2) node2Bytes, err := os.ReadFile(c.Node2)
if err != nil { if err != nil {
return fmt.Errorf("error reading node2 key file %s: %w", return fmt.Errorf("error reading node2 key file %s: %w",
c.Node2, err) c.Node2, err)
@ -106,53 +112,69 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
// Make sure the key files were filled correctly. // Make sure the key files were filled correctly.
if keys1.Node1 == nil || keys1.Node2 == nil { if keys1.Node1 == nil || keys1.Node2 == nil {
return fmt.Errorf("invalid node1 file, node info missing") return errors.New("invalid node1 file, node info missing")
} }
if keys2.Node1 == nil || keys2.Node2 == nil { if keys2.Node1 == nil || keys2.Node2 == nil {
return fmt.Errorf("invalid node2 file, node info missing") return errors.New("invalid node2 file, node info missing")
} }
if keys1.Node1.PubKey != keys2.Node1.PubKey { if keys1.Node1.PubKey != keys2.Node1.PubKey {
return fmt.Errorf("invalid files, node 1 pubkey doesn't match") return errors.New("invalid files, node 1 pubkey doesn't match")
} }
if keys1.Node2.PubKey != keys2.Node2.PubKey { if keys1.Node2.PubKey != keys2.Node2.PubKey {
return fmt.Errorf("invalid files, node 2 pubkey doesn't match") return errors.New("invalid files, node 2 pubkey doesn't match")
} }
if len(keys1.Node1.MultisigKeys) == 0 && if len(keys1.Node1.MultisigKeys) == 0 &&
len(keys1.Node2.MultisigKeys) == 0 { len(keys1.Node2.MultisigKeys) == 0 {
return fmt.Errorf("invalid node1 file, missing multisig keys") return errors.New("invalid node1 file, missing multisig keys")
} }
if len(keys2.Node1.MultisigKeys) == 0 && if len(keys2.Node1.MultisigKeys) == 0 &&
len(keys2.Node2.MultisigKeys) == 0 { len(keys2.Node2.MultisigKeys) == 0 {
return fmt.Errorf("invalid node2 file, missing multisig keys") return errors.New("invalid node2 file, missing multisig keys")
} }
if len(keys1.Node1.MultisigKeys) == len(keys2.Node1.MultisigKeys) { if len(keys1.Node1.MultisigKeys) == len(keys2.Node1.MultisigKeys) {
return fmt.Errorf("invalid files, channel info incorrect") return errors.New("invalid files, channel info incorrect")
} }
if len(keys1.Node2.MultisigKeys) == len(keys2.Node2.MultisigKeys) { if len(keys1.Node2.MultisigKeys) == len(keys2.Node2.MultisigKeys) {
return fmt.Errorf("invalid files, channel info incorrect") return errors.New("invalid files, channel info incorrect")
} }
if len(keys1.Channels) != len(keys2.Channels) { if len(keys1.Channels) != len(keys2.Channels) {
return fmt.Errorf("invalid files, channels don't match") return errors.New("invalid files, channels don't match")
} }
for idx, node1Channel := range keys1.Channels { for idx, node1Channel := range keys1.Channels {
if keys2.Channels[idx].ChanPoint != node1Channel.ChanPoint { if keys2.Channels[idx].ChanPoint != node1Channel.ChanPoint {
return fmt.Errorf("invalid files, channels don't match") return errors.New("invalid files, channels don't match")
} }
if keys2.Channels[idx].Address != node1Channel.Address { if keys2.Channels[idx].Address != node1Channel.Address {
return fmt.Errorf("invalid files, channels don't match") return errors.New("invalid files, channels don't match")
} }
if keys2.Channels[idx].Address == "" || if keys2.Channels[idx].Address == "" ||
node1Channel.Address == "" { node1Channel.Address == "" {
return fmt.Errorf("invalid files, channel address " + return errors.New("invalid files, channel address " +
"missing") "missing")
} }
} }
// 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. // Make sure one of the nodes is ours.
_, pubKey, _, err := lnd.DeriveKey( _, pubKey, _, err := lnd.DeriveKey(
extendedKey, lnd.IdentityPath(chainParams), chainParams, extendedKey, lnd.IdentityPath(chainParams), chainParams,
@ -200,58 +222,25 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
theirPayoutAddr = keys1.Node1.PayoutAddr theirPayoutAddr = keys1.Node1.PayoutAddr
} }
if len(ourKeys) == 0 || len(theirKeys) == 0 { if len(ourKeys) == 0 || len(theirKeys) == 0 {
return fmt.Errorf("couldn't find necessary keys") return errors.New("couldn't find necessary keys")
} }
if ourPayoutAddr == "" || theirPayoutAddr == "" { if ourPayoutAddr == "" || theirPayoutAddr == "" {
return fmt.Errorf("payout address missing") return errors.New("payout address missing")
} }
ourPubKeys := make([]*btcec.PublicKey, len(ourKeys)) ourPubKeys, err := parseKeys(ourKeys)
theirPubKeys := make([]*btcec.PublicKey, len(theirKeys)) if err != nil {
for idx, pubKeyHex := range ourKeys { return fmt.Errorf("error parsing their keys: %w", err)
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)
}
} }
// Loop through all channels and all keys now, this will definitely take theirPubKeys, err := parseKeys(theirKeys)
// a while. if err != nil {
channelLoop: return fmt.Errorf("error parsing our keys: %w", err)
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
}
}
}
return fmt.Errorf("didn't find matching multisig keys for "+ err = matchKeys(keys1.Channels, ourPubKeys, theirPubKeys, chainParams)
"channel %s", channel.ChanPoint) if err != nil {
return err
} }
// Let's now sum up the tally of how much of the rescued funds should // Let's now sum up the tally of how much of the rescued funds should
@ -304,7 +293,7 @@ channelLoop:
estimator.AddWitnessInput(input.MultiSigWitnessSize) estimator.AddWitnessInput(input.MultiSigWitnessSize)
} }
feeRateKWeight := chainfee.SatPerKVByte(1000 * c.FeeRate).FeePerKWeight() feeRateKWeight := chainfee.SatPerKVByte(1000 * c.FeeRate).FeePerKWeight()
totalFee := int64(feeRateKWeight.FeeForWeight(int64(estimator.Weight()))) totalFee := int64(feeRateKWeight.FeeForWeight(estimator.Weight()))
fmt.Printf("Current tally (before fees):\n\t"+ fmt.Printf("Current tally (before fees):\n\t"+
"To our address (%s): %d sats\n\t"+ "To our address (%s): %d sats\n\t"+
@ -327,7 +316,7 @@ channelLoop:
theirSum -= totalFee theirSum -= totalFee
default: default:
return fmt.Errorf("error distributing fees, unhandled case") return errors.New("error distributing fees, unhandled case")
} }
// Our output. // Our output.
@ -381,10 +370,8 @@ channelLoop:
return fmt.Errorf("error creating PSBT from TX: %w", err) return fmt.Errorf("error creating PSBT from TX: %w", err)
} }
signer := &lnd.Signer{ // First we add the necessary information to the psbt package so that
ExtendedKey: extendedKey, // we can sign the transaction with SIGHASH_ALL.
ChainParams: chainParams,
}
for idx, txIn := range inputs { for idx, txIn := range inputs {
channel := keys1.Channels[idx] channel := keys1.Channels[idx]
@ -411,6 +398,16 @@ channelLoop:
Value: channel.theirKey.SerializeCompressed(), 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{ keyDesc := keychain.KeyDescriptor{
PubKey: channel.ourKey, PubKey: channel.ourKey,
@ -444,6 +441,64 @@ channelLoop:
return nil 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, func matchScript(address string, key1, key2 *btcec.PublicKey,
params *chaincfg.Params) (bool, []byte, error) { params *chaincfg.Params) (bool, []byte, error) {

@ -4,8 +4,10 @@ import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os"
"time" "time"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
@ -20,6 +22,8 @@ type zombieRecoveryPrepareKeysCommand struct {
MatchFile string MatchFile string
PayoutAddr string PayoutAddr string
NumKeys uint32
rootKey *rootKey rootKey *rootKey
cmd *cobra.Command cmd *cobra.Command
} }
@ -47,7 +51,12 @@ correct ones for the matched channels.`,
cc.cmd.Flags().StringVar( cc.cmd.Flags().StringVar(
&cc.PayoutAddr, "payout_addr", "", "the address where this "+ &cc.PayoutAddr, "payout_addr", "", "the address where this "+
"node's rescued funds should be sent to, must be a "+ "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") cc.rootKey = newRootKey(cc.cmd, "deriving the multisig keys")
@ -64,7 +73,7 @@ func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command,
_, err = lnd.GetP2WPKHScript(c.PayoutAddr, chainParams) _, err = lnd.GetP2WPKHScript(c.PayoutAddr, chainParams)
if err != nil { if err != nil {
return fmt.Errorf("invalid payout address, must be P2WPKH") return errors.New("invalid payout address, must be P2WPKH")
} }
matchFileBytes, err := ioutil.ReadFile(c.MatchFile) matchFileBytes, err := ioutil.ReadFile(c.MatchFile)
@ -82,7 +91,7 @@ func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command,
// Make sure the match file was filled correctly. // Make sure the match file was filled correctly.
if match.Node1 == nil || match.Node2 == nil { if match.Node1 == nil || match.Node2 == nil {
return fmt.Errorf("invalid match file, node info missing") return errors.New("invalid match file, node info missing")
} }
_, pubKey, _, err := lnd.DeriveKey( _, pubKey, _, err := lnd.DeriveKey(
@ -108,9 +117,9 @@ func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command,
} }
// Derive all 2500 keys now, this might take a while. // Derive all 2500 keys now, this might take a while.
for index := 0; index < numMultisigKeys; index++ { for index := range c.NumKeys {
_, pubKey, _, err := lnd.DeriveKey( _, pubKey, _, err := lnd.DeriveKey(
extendedKey, lnd.MultisigPath(chainParams, index), extendedKey, lnd.MultisigPath(chainParams, int(index)),
chainParams, chainParams,
) )
if err != nil { if err != nil {
@ -134,5 +143,5 @@ func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command,
fileName := fmt.Sprintf("results/preparedkeys-%s-%s.json", fileName := fmt.Sprintf("results/preparedkeys-%s-%s.json",
time.Now().Format("2006-01-02"), pubKeyStr) time.Now().Format("2006-01-02"), pubKeyStr)
log.Infof("Writing result to %s", fileName) log.Infof("Writing result to %s", fileName)
return ioutil.WriteFile(fileName, matchBytes, 0644) return os.WriteFile(fileName, matchBytes, 0644)
} }

@ -3,6 +3,7 @@ package main
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"errors"
"fmt" "fmt"
"os" "os"
@ -151,12 +152,12 @@ func signOffer(rootKey *hdkeychain.ExtendedKey,
"%w", err) "%w", err)
} }
if len(packet.Inputs[idx].WitnessScript) == 0 { if len(packet.Inputs[idx].WitnessScript) == 0 {
return fmt.Errorf("invalid PSBT, missing witness " + return errors.New("invalid PSBT, missing witness " +
"script") "script")
} }
witnessScript := packet.Inputs[idx].WitnessScript witnessScript := packet.Inputs[idx].WitnessScript
if packet.Inputs[idx].WitnessUtxo == nil { if packet.Inputs[idx].WitnessUtxo == nil {
return fmt.Errorf("invalid PSBT, witness UTXO missing") return errors.New("invalid PSBT, witness UTXO missing")
} }
utxo := packet.Inputs[idx].WitnessUtxo utxo := packet.Inputs[idx].WitnessUtxo

@ -6,7 +6,8 @@ Chantools helps recover funds from lightning channels
This tool provides helper functions that can be used rescue This tool provides helper functions that can be used rescue
funds locked in lnd channels in case lnd itself cannot run properly anymore. funds locked in lnd channels in case lnd itself cannot run properly anymore.
Complete documentation is available at https://github.com/lightninglabs/chantools/. Complete documentation is available at
https://github.com/lightninglabs/chantools/.
### Options ### Options
@ -22,9 +23,10 @@ Complete documentation is available at https://github.com/lightninglabs/chantool
* [chantools chanbackup](chantools_chanbackup.md) - Create a channel.backup file from a channel database * [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 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 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 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 derivekey](chantools_derivekey.md) - Derive a key with a specific derivation path
* [chantools doublespendinputs](chantools_doublespendinputs.md) - 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](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 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 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 dumpbackup](chantools_dumpbackup.md) - Dump the content of a channel.backup file
@ -35,18 +37,21 @@ Complete documentation is available at https://github.com/lightninglabs/chantool
* [chantools forceclose](chantools_forceclose.md) - Force-close the last state that is in the channel.db provided * [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 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 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 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 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 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 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 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 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 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 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 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 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 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 CLN 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 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 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 * [chantools zombierecovery](chantools_zombierecovery.md) - Try rescuing funds stuck in channels with zombie nodes

@ -27,6 +27,7 @@ chantools chanbackup \
-h, --help help for chanbackup -h, --help help for chanbackup
--multi_file string lnd channel.backup file to create --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 --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 ### Options inherited from parent commands

@ -41,7 +41,8 @@ chantools closepoolaccount \
--outpoint string last account outpoint of the account to close (<txid>:<txindex>) --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 --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 --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 ### Options inherited from parent commands

@ -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 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 with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.17.0-beta or later after using this command!' run lnd v0.18.0-beta or later after using this command!'
``` ```
chantools deletepayments [flags] chantools deletepayments [flags]

@ -23,12 +23,13 @@ chantools derivekey --identity
### Options ### Options
``` ```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag --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 -h, --help help for derivekey
--identity derive the lnd identity_pubkey --identity derive the lnd identity_pubkey
--neuter don't output private key(s), only public key(s) --neuter don't output private key(s), only public key(s)
--path string BIP32 derivation path to derive; must start with "m/" --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 --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 ### Options inherited from parent commands

@ -1,6 +1,12 @@
## chantools doublespendinputs ## chantools 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. 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] chantools doublespendinputs [flags]
@ -10,10 +16,10 @@ chantools doublespendinputs [flags]
``` ```
chantools doublespendinputs \ chantools doublespendinputs \
--inputoutpoints xxxxxxxxx:y,xxxxxxxxx:y \ --inputoutpoints xxxxxxxxx:y,xxxxxxxxx:y \
--sweepaddr bc1q..... \ --sweepaddr bc1q..... \
--feerate 10 \ --feerate 10 \
--publish --publish
``` ```
### Options ### Options
@ -27,7 +33,8 @@ chantools doublespendinputs \
--publish publish replacement TX to the chain API instead of just printing the TX --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) --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 --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 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 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 ### Options inherited from parent commands

@ -12,7 +12,7 @@ without removing any other data.
CAUTION: Running this command will make it impossible to use the channel DB 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 with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.17.0-beta or later after using this command!' run lnd v0.18.0-beta or later after using this command!'
``` ```
chantools dropchannelgraph [flags] chantools dropchannelgraph [flags]

@ -12,7 +12,7 @@ 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 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 with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.17.0-beta or later after using this command!' run lnd v0.18.0-beta or later after using this command!'
``` ```
chantools dropgraphzombies [flags] chantools dropgraphzombies [flags]

@ -25,6 +25,7 @@ chantools dumpbackup \
-h, --help help for dumpbackup -h, --help help for dumpbackup
--multi_file string lnd channel.backup file to dump --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 --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 ### Options inherited from parent commands

@ -61,10 +61,11 @@ 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 --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 --from_channel_graph string the full LN channel graph in the JSON format that the 'lncli describegraph' returns
-h, --help help for fakechanbackup -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-06-18-10-55-31.backup")
--remote_node_addr string the remote node connection information in the format pubkey@host:port --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 --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> --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 ### Options inherited from parent commands

@ -27,6 +27,7 @@ chantools filterbackup \
-h, --help help for filterbackup -h, --help help for filterbackup
--multi_file string lnd channel.backup file to filter --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 --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 ### Options inherited from parent commands

@ -28,6 +28,7 @@ chantools fixoldbackup \
-h, --help help for fixoldbackup -h, --help help for fixoldbackup
--multi_file string lnd channel.backup file to fix --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 --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 ### Options inherited from parent commands

@ -43,6 +43,7 @@ chantools forceclose \
--pendingchannels string channel input is in the format of lncli's pendingchannels format; specify '-' to read from stdin --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 --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 --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 ### Options inherited from parent commands

@ -51,6 +51,7 @@ 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) --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 --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 --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 ### Options inherited from parent commands

@ -11,7 +11,7 @@ needs to read the database content.
CAUTION: Running this command will make it impossible to use the channel DB 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 with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.17.0-beta or later after using this command!' run lnd v0.18.0-beta or later after using this command!'
``` ```
chantools migratedb [flags] chantools migratedb [flags]

@ -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

@ -27,13 +27,16 @@ chantools recoverloopin \
-h, --help help for recoverloopin -h, --help help for recoverloopin
--loop_db_dir string path to the loop database directory, where the loop.db file is located --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) --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 --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 --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 --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 --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 --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 --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 ### Options inherited from parent commands

@ -11,7 +11,7 @@ channel was never confirmed on chain!
CAUTION: Running this command will make it impossible to use the channel DB 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 with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.17.0-beta or later after using this command! run lnd v0.18.0-beta or later after using this command!
``` ```
chantools removechannel [flags] chantools removechannel [flags]

@ -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 know about the channels any more but we still have the channel.db from the
moment they force-closed. 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 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 running the fund-recovery branch of my guggero/lnd fork (see
https://github.com/guggero/lnd/releases for a binary release) in combination 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 --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 --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 --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 --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 --fromsummary string channel input is in the format of chantool's channel summary; specify '-' to read from stdin
-h, --help help for rescueclosed -h, --help help for rescueclosed
@ -56,6 +60,7 @@ 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 --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 --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 --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 ### Options inherited from parent commands

@ -49,7 +49,8 @@ chantools 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 --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 --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 --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 ### Options inherited from parent commands

@ -29,6 +29,7 @@ chantools rescuetweakedkey \
--path string BIP32 derivation path to derive the starting key from; must start with "m/" --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 --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 --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 ### Options inherited from parent commands

@ -21,9 +21,10 @@ chantools showrootkey
### Options ### Options
``` ```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag --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 -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 --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 ### Options inherited from parent commands

@ -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,10 +26,11 @@ chantools signrescuefunding \
### Options ### Options
``` ```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag --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 -h, --help help for signrescuefunding
--psbt string Partially Signed Bitcoin Transaction that was provided by the initiator of the channel to rescue --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 --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 ### Options inherited from parent commands

@ -13,6 +13,7 @@ funds can be swept after the force-close transaction was confirmed.
Supported remote force-closed channel types are: Supported remote force-closed channel types are:
- STATIC_REMOTE_KEY (a.k.a. tweakless channels) - STATIC_REMOTE_KEY (a.k.a. tweakless channels)
- ANCHOR (a.k.a. anchor output channels) - ANCHOR (a.k.a. anchor output channels)
- SIMPLE_TAPROOT (a.k.a. simple taproot channels)
``` ```
@ -39,7 +40,8 @@ chantools sweepremoteclosed \
--publish publish sweep TX to the chain API instead of just printing the TX --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) --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 --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 ### Options inherited from parent commands

@ -40,7 +40,8 @@ chantools sweeptimelock \
--pendingchannels string channel input is in the format of lncli's pendingchannels format; specify '-' to read from stdin --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 --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 --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 ### Options inherited from parent commands

@ -12,6 +12,9 @@ and only the channel.backup file is available.
To get the value for --remoterevbasepoint you must use the dumpbackup command, To get the value for --remoterevbasepoint you must use the dumpbackup command,
then look up the value for RemoteChanCfg -> RevocationBasePoint -> PubKey. 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 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 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). address is always the one that's longer (because it's P2WSH and not P2PKH).
@ -29,6 +32,14 @@ chantools sweeptimelockmanual \
--remoterevbasepoint 03xxxxxxx \ --remoterevbasepoint 03xxxxxxx \
--feerate 10 \ --feerate 10 \
--publish --publish
chantools sweeptimelockmanual \
--sweepaddr bc1q..... \
--timelockaddr bc1q............ \
--frombackup channel.backup \
--channelpoint f39310xxxxxxxxxx:1 \
--feerate 10 \
--publish
``` ```
### Options ### Options
@ -36,7 +47,9 @@ chantools sweeptimelockmanual \
``` ```
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api") --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 --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--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) --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 --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 --fromsummary string channel input is in the format of chantool's channel summary; specify '-' to read from stdin
-h, --help help for sweeptimelockmanual -h, --help help for sweeptimelockmanual
@ -48,8 +61,9 @@ chantools sweeptimelockmanual \
--publish publish sweep TX to the chain API instead of just printing the TX --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 --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 --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 --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 ### Options inherited from parent commands

@ -1,14 +1,13 @@
## chantools triggerforceclose ## chantools triggerforceclose
Connect to a CLN 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 ### Synopsis
Certain versions of CLN didn't properly react to error Asks the specified remote peer to force close a specific
messages sent by peers and therefore didn't follow the DLP protocol to recover channel by first sending a channel re-establish message, and if that doesn't
channel funds using SCB. This command can be used to trigger a force close with work, a custom error message (in case the peer is a specific version of CLN that
those earlier versions of CLN (this command will not work for lnd peers or CLN does not properly respond to a Data Loss Protection re-establish message).'
peers of a different version).
``` ```
chantools triggerforceclose [flags] chantools triggerforceclose [flags]
@ -31,6 +30,8 @@ chantools triggerforceclose \
-h, --help help for triggerforceclose -h, --help help for triggerforceclose
--peer string remote peer address (<pubkey>@<host>[:<port>]) --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 --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 ### Options inherited from parent commands

@ -31,9 +31,11 @@ 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 --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) --feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
-h, --help help for makeoffer -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 --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 --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 --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 ### Options inherited from parent commands

@ -28,8 +28,10 @@ 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 --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 -h, --help help for preparekeys
--match_file string the match JSON file that was sent to both nodes by the match maker --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 --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 --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 ### Options inherited from parent commands

@ -21,10 +21,11 @@ chantools zombierecovery signoffer \
### Options ### Options
``` ```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag --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 -h, --help help for signoffer
--psbt string the base64 encoded PSBT that the other party sent as an offer to rescue funds --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 --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 ### Options inherited from parent commands

@ -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). static channel backup did not work, force closing was not possible).
This means: 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) probably offline if you tried to recover)
2. You have to find a way to contact this peer (twitter, email, ...) 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 3. You and your peer will have to negotiate a closing state: you only know the

211
go.mod

@ -1,132 +1,157 @@
module github.com/lightninglabs/chantools module github.com/lightninglabs/chantools
go 1.21 go 1.22.3
require ( require (
github.com/btcsuite/btcd v0.23.5-0.20230905170901-80f5a0ffdf36 github.com/btcsuite/btcd v0.24.2-beta.rc1.0.20240403021926-ae5533602c46
github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/btcsuite/btcd/btcec/v2 v2.3.3
github.com/btcsuite/btcd/btcutil v1.1.4-0.20230904040416-d4f519f5dc05 github.com/btcsuite/btcd/btcutil v1.1.5
github.com/btcsuite/btcd/btcutil/psbt v1.1.8 github.com/btcsuite/btcd/btcutil/psbt v1.1.8
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
github.com/btcsuite/btcwallet v0.16.10-0.20230804184612-07be54bc22cf github.com/btcsuite/btcwallet v0.16.10-0.20240410030101-6fe19a472a62
github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 github.com/btcsuite/btcwallet/wallet/txrules v1.2.1
github.com/btcsuite/btcwallet/walletdb v1.4.0 github.com/btcsuite/btcwallet/walletdb v1.4.2
github.com/coreos/bbolt v1.3.3 github.com/coreos/bbolt v1.3.3
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
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/hasura/go-graphql-client v0.9.1
github.com/lightninglabs/loop v0.23.0-beta github.com/lightninglabs/loop v0.28.3-beta
github.com/lightninglabs/pool v0.6.2-beta.0.20230329135228-c3bffb52df3a github.com/lightninglabs/pool v0.6.5-beta.0.20240531084722-4000ec802aaa
// The current version of lnd we are compatible with, mostly affects the // 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. // 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 // NOTE: When updating this version, make sure to also update the string in
// cmd/chantools/root.go. // cmd/chantools/root.go.
github.com/lightningnetwork/lnd v0.17.0-beta github.com/lightningnetwork/lnd v0.18.0-beta.1
github.com/lightningnetwork/lnd/kvdb v1.4.4 github.com/lightningnetwork/lnd/kvdb v1.4.8
github.com/lightningnetwork/lnd/queue v1.1.1 github.com/lightningnetwork/lnd/queue v1.1.1
github.com/lightningnetwork/lnd/ticker v1.1.1 github.com/lightningnetwork/lnd/ticker v1.1.1
github.com/lightningnetwork/lnd/tor v1.1.2 github.com/lightningnetwork/lnd/tor v1.1.3
github.com/spf13/cobra v1.1.3 github.com/spf13/cobra v1.1.3
github.com/stretchr/testify v1.8.2 github.com/stretchr/testify v1.9.0
go.etcd.io/bbolt v1.3.7 go.etcd.io/bbolt v1.3.7
golang.org/x/crypto v0.14.0 golang.org/x/crypto v0.22.0
golang.org/x/oauth2 v0.4.0 golang.org/x/oauth2 v0.14.0
) )
require github.com/tv42/zbase32 v0.0.0-20220222190657-f76a9fc892fa
require ( require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // 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/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/aead/siphash v1.0.1 // indirect github.com/aead/siphash v1.0.1 // indirect
github.com/andybalholm/brotli v1.0.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2 // indirect github.com/btcsuite/btcwallet/wallet/txauthor v1.3.4 // indirect
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 // indirect github.com/btcsuite/btcwallet/wallet/txsizes v1.2.4 // indirect
github.com/btcsuite/btcwallet/wtxmgr v1.5.0 // indirect github.com/btcsuite/btcwallet/wtxmgr v1.5.3 // indirect
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
github.com/btcsuite/winsvc v1.0.0 // indirect github.com/btcsuite/winsvc v1.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // 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-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // 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/crypto/blake256 v1.0.1 // indirect
github.com/decred/dcrd/lru v1.0.0 // indirect github.com/decred/dcrd/lru v1.1.2 // indirect
github.com/dsnet/compress v0.0.1 // indirect github.com/docker/cli v20.10.17+incompatible // indirect
github.com/dustin/go-humanize v1.0.0 // indirect github.com/docker/docker v24.0.9+incompatible // indirect
github.com/fergusstrange/embedded-postgres v1.10.0 // indirect github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fergusstrange/embedded-postgres v1.25.0 // indirect
github.com/fortytw2/leaktest v1.3.0 // indirect
github.com/frankban/quicktest v1.14.3 // indirect
github.com/go-errors/errors v1.0.1 // 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/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/golang/mock v1.6.0 // indirect github.com/golang-migrate/migrate/v4 v4.17.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.0.1 // indirect github.com/google/btree v1.0.1 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/websocket v1.4.2 // indirect github.com/google/uuid v1.6.0 // 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-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.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 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/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.0 // indirect github.com/jackc/pgconn v1.14.3 // indirect
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect
github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.1 // indirect github.com/jackc/pgx/v4 v4.18.2 // indirect
github.com/jessevdk/go-flags v1.4.0 // indirect github.com/jessevdk/go-flags v1.4.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/jrick/logrotate v1.0.0 // indirect github.com/jrick/logrotate v1.0.0 // indirect
github.com/json-iterator/go v1.1.11 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4 // indirect github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kkdai/bstream v1.0.0 // indirect github.com/kkdai/bstream v1.0.0 // indirect
github.com/klauspost/compress v1.16.0 // indirect github.com/klauspost/compress v1.16.0 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/lib/pq v1.10.3 // indirect github.com/lib/pq v1.10.9 // indirect
github.com/lightninglabs/aperture v0.1.20-beta // indirect
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
github.com/lightninglabs/lndclient v0.16.0-10 // indirect github.com/lightninglabs/lndclient v0.18.0-2 // indirect
github.com/lightninglabs/loop/swapserverrpc v1.0.4 // indirect github.com/lightninglabs/loop/swapserverrpc v1.0.8 // indirect
github.com/lightninglabs/neutrino v0.16.0 // indirect github.com/lightninglabs/neutrino v0.16.1-0.20240425105051-602843d34ffd // indirect
github.com/lightninglabs/neutrino/cache v1.1.1 // indirect github.com/lightninglabs/neutrino/cache v1.1.2 // indirect
github.com/lightninglabs/pool/auctioneerrpc v1.0.7 // indirect github.com/lightninglabs/pool/auctioneerrpc v1.1.2 // indirect
github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f // 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/clock v1.1.1 // indirect
github.com/lightningnetwork/lnd/healthcheck v1.2.3 // indirect github.com/lightningnetwork/lnd/fn v1.0.8 // indirect
github.com/lightningnetwork/lnd/tlv v1.1.1 // indirect github.com/lightningnetwork/lnd/healthcheck v1.2.4 // indirect
github.com/lightningnetwork/lnd/sqldb v1.0.2 // indirect
github.com/lightningnetwork/lnd/tlv v1.2.6 // indirect
github.com/ltcsuite/ltcd v0.0.0-20191228044241-92166e412499 // indirect github.com/ltcsuite/ltcd v0.0.0-20191228044241-92166e412499 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // 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/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/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/ncruces/go-strftime v0.1.9 // 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/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.11.1 // indirect github.com/prometheus/client_golang v1.11.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.26.0 // indirect github.com/prometheus/common v0.30.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect github.com/prometheus/procfs v0.7.3 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/fastuuid v1.2.0 // indirect github.com/rogpeppe/fastuuid v1.2.0 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.2 // indirect github.com/sirupsen/logrus v1.9.2 // indirect
github.com/soheilhy/cmux v0.1.5 // indirect github.com/soheilhy/cmux v0.1.5 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // 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/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect github.com/xdg-go/stringprep v1.0.4 // 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/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
@ -137,45 +162,47 @@ require (
go.etcd.io/etcd/pkg/v3 v3.5.7 // indirect go.etcd.io/etcd/pkg/v3 v3.5.7 // indirect
go.etcd.io/etcd/raft/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.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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect
go.opentelemetry.io/otel v1.6.3 // indirect go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.0.1 // indirect go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.0.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 // indirect
go.opentelemetry.io/otel/sdk v1.0.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0 // indirect
go.opentelemetry.io/otel/trace v1.6.3 // indirect go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/proto/otlp v0.9.0 // indirect go.opentelemetry.io/otel/sdk v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
go.uber.org/atomic v1.7.0 // indirect go.uber.org/atomic v1.7.0 // indirect
go.uber.org/mock v0.4.0 // indirect
go.uber.org/multierr v1.6.0 // indirect go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.17.0 // indirect go.uber.org/zap v1.17.0 // indirect
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
golang.org/x/mod v0.10.0 // indirect golang.org/x/mod v0.16.0 // indirect
golang.org/x/net v0.17.0 // indirect golang.org/x/net v0.23.0 // indirect
golang.org/x/sync v0.2.0 // indirect golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.13.0 // indirect golang.org/x/sys v0.19.0 // indirect
golang.org/x/term v0.13.0 // indirect golang.org/x/term v0.19.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.9.1 // indirect golang.org/x/tools v0.19.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/grpc v1.53.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/protobuf v1.30.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/errgo.v1 v1.0.1 // 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/macaroon.v2 v2.1.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/cc/v3 v3.40.0 // indirect modernc.org/libc v1.49.3 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect modernc.org/mathutil v1.6.0 // indirect
modernc.org/libc v1.22.2 // indirect modernc.org/memory v1.8.0 // indirect
modernc.org/mathutil v1.5.0 // indirect modernc.org/sqlite v1.29.8 // indirect
modernc.org/memory v1.4.0 // indirect modernc.org/strutil v1.2.0 // indirect
modernc.org/opt v0.1.3 // indirect modernc.org/token v1.1.0 // indirect
modernc.org/sqlite v1.20.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
nhooyr.io/websocket v1.8.7 // indirect nhooyr.io/websocket v1.8.7 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect sigs.k8s.io/yaml v1.2.0 // indirect
) )
@ -183,4 +210,4 @@ require (
// We want to format raw bytes as hex instead of base64. The forked version // 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 // allows us to specify that as an option. This is required for the
// taproot-assets dependency to function properly. // taproot-assets dependency to function properly.
replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display

1511
go.sum

File diff suppressed because it is too large Load Diff

@ -2,6 +2,7 @@ package lnd
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
@ -11,20 +12,47 @@ import (
"github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg" "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/aezeed"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnwallet"
"go.etcd.io/bbolt"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
) )
const ( const (
MnemonicEnvName = "AEZEED_MNEMONIC" MnemonicEnvName = "AEZEED_MNEMONIC"
PassphraseEnvName = "AEZEED_PASSPHRASE" PassphraseEnvName = "AEZEED_PASSPHRASE"
PasswordEnvName = "WALLET_PASSWORD"
) )
var ( var (
numberDotsRegex = regexp.MustCompile(`[\d.\-\n\r\t]*`) numberDotsRegex = regexp.MustCompile(`[\d.\-\n\r\t]*`)
multipleSpaces = regexp.MustCompile(" [ ]+") 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, errors.New("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, func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time,
error) { error) {
@ -67,6 +95,32 @@ func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time,
len(cipherSeedMnemonic), 24) 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), errors.New("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 // Additionally, the user may have a passphrase, that will also need to
// be provided so the daemon can properly decipher the cipher seed. // be provided so the daemon can properly decipher the cipher seed.
// Try the environment variable first. // 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 // The environment variable didn't contain anything, we'll read the
// passphrase from the terminal. // passphrase from the terminal.
case passphrase == "": case passphrase == "":
fmt.Printf("Input your cipher seed passphrase (press enter " + fmt.Printf("Input your cipher seed passphrase (press enter "+
"if your seed doesn't have a passphrase): ") "if your seed %s a passphrase): ", verb)
var err error var err error
passphraseBytes, err = terminal.ReadPassword( passphraseBytes, err = terminal.ReadPassword(
int(syscall.Stdin), //nolint int(syscall.Stdin), //nolint
) )
if err != nil { if err != nil {
return nil, time.Unix(0, 0), err return nil, err
} }
fmt.Println() fmt.Println()
@ -101,20 +155,176 @@ func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time,
passphraseBytes = []byte(passphrase) passphraseBytes = []byte(passphrase)
} }
var mnemonic aezeed.Mnemonic return passphraseBytes, nil
copy(mnemonic[:], cipherSeedMnemonic) }
// If we're unable to map it back into the ciphertext, then either the // PasswordFromConsole reads a password from the console or stdin.
// mnemonic is wrong, or the passphrase is wrong. func PasswordFromConsole(userQuery string) ([]byte, error) {
cipherSeed, err := mnemonic.ToCipherSeed(passphraseBytes) // 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 { if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("failed to decrypt "+ return nil, err
"seed with passphrase: %w", 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, errors.New("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 { if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("failed to derive " + return nil, nil, nil, fmt.Errorf("error opening wallet "+
"master extended key") "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)
} }

@ -1,6 +1,7 @@
package lnd package lnd
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"time" "time"
@ -103,14 +104,14 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
gossiper := discovery.New(discovery.Config{ gossiper := discovery.New(discovery.Config{
ChainHash: *netParams.GenesisHash, ChainHash: *netParams.GenesisHash,
Broadcast: func(skips map[route.Vertex]struct{}, Broadcast: func(_ map[route.Vertex]struct{},
msg ...lnwire.Message) error { _ ...lnwire.Message) error {
return nil return nil
}, },
NotifyWhenOnline: func([33]byte, chan<- lnpeer.Peer) { NotifyWhenOnline: func([33]byte, chan<- lnpeer.Peer) {
}, },
NotifyWhenOffline: func(peerPubKey [33]byte) <-chan struct{} { NotifyWhenOffline: func(_ [33]byte) <-chan struct{} {
return make(chan struct{}) return make(chan struct{})
}, },
FetchSelfAnnouncement: func() lnwire.NodeAnnouncement { FetchSelfAnnouncement: func() lnwire.NodeAnnouncement {
@ -137,24 +138,24 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
SignAliasUpdate: func( SignAliasUpdate: func(
*lnwire.ChannelUpdate) (*ecdsa.Signature, error) { *lnwire.ChannelUpdate) (*ecdsa.Signature, error) {
return nil, fmt.Errorf("unimplemented") return nil, errors.New("unimplemented")
}, },
FindBaseByAlias: func( FindBaseByAlias: func(
lnwire.ShortChannelID) (lnwire.ShortChannelID, error) { lnwire.ShortChannelID) (lnwire.ShortChannelID, error) {
return lnwire.ShortChannelID{}, return lnwire.ShortChannelID{},
fmt.Errorf("unimplemented") errors.New("unimplemented")
}, },
GetAlias: func(id lnwire.ChannelID) (lnwire.ShortChannelID, GetAlias: func(_ lnwire.ChannelID) (lnwire.ShortChannelID,
error) { error) {
return lnwire.ShortChannelID{}, return lnwire.ShortChannelID{},
fmt.Errorf("unimplemented") errors.New("unimplemented")
}, },
FindChannel: func(*btcec.PublicKey, FindChannel: func(*btcec.PublicKey,
lnwire.ChannelID) (*channeldb.OpenChannel, error) { lnwire.ChannelID) (*channeldb.OpenChannel, error) {
return nil, fmt.Errorf("unimplemented") return nil, errors.New("unimplemented")
}, },
}, &keychain.KeyDescriptor{ }, &keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{}, KeyLocator: keychain.KeyLocator{},
@ -181,22 +182,21 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
key.SerializeCompressed()) key.SerializeCompressed())
return nil return nil
}, },
GenNodeAnnouncement: func( GenNodeAnnouncement: func(_ ...netann.NodeAnnModifier) (
modifier ...netann.NodeAnnModifier) (
lnwire.NodeAnnouncement, error) { lnwire.NodeAnnouncement, error) {
return lnwire.NodeAnnouncement{}, return lnwire.NodeAnnouncement{},
fmt.Errorf("unimplemented") errors.New("unimplemented")
}, },
PongBuf: pongBuf, PongBuf: pongBuf,
PrunePersistentPeerConnection: func(bytes [33]byte) {}, PrunePersistentPeerConnection: func(_ [33]byte) {},
FetchLastChanUpdate: func(id lnwire.ShortChannelID) ( FetchLastChanUpdate: func(_ lnwire.ShortChannelID) (
*lnwire.ChannelUpdate, error) { *lnwire.ChannelUpdate, error) {
return nil, fmt.Errorf("unimplemented") return nil, errors.New("unimplemented")
}, },
Hodl: &hodl.Config{}, Hodl: &hodl.Config{},
@ -216,16 +216,14 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
return nil return nil
}, },
GetAliases: func( GetAliases: func(
base lnwire.ShortChannelID) []lnwire.ShortChannelID { _ lnwire.ShortChannelID) []lnwire.ShortChannelID {
return nil return nil
}, },
RequestAlias: func() (lnwire.ShortChannelID, error) { RequestAlias: func() (lnwire.ShortChannelID, error) {
return lnwire.ShortChannelID{}, nil return lnwire.ShortChannelID{}, nil
}, },
AddLocalAlias: func(alias, base lnwire.ShortChannelID, AddLocalAlias: func(_, _ lnwire.ShortChannelID, _ bool) error {
gossip bool) error {
return nil return nil
}, },
Quit: make(chan struct{}), Quit: make(chan struct{}),

@ -4,6 +4,9 @@ import (
"bytes" "bytes"
"fmt" "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/chanbackup"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
@ -35,3 +38,30 @@ func CreateChannelBackup(db *channeldb.DB, multiFile *chanbackup.MultiFile,
} }
return nil 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 {
if channel.FundingOutpoint == channelPoint {
return &channel, nil
}
}
return nil, fmt.Errorf("channel %s not found in backup", channelPoint)
}

@ -1,6 +1,7 @@
package lnd package lnd
import ( import (
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -90,7 +91,7 @@ func (lc *LightningChannel) SignedCommitTx() (*wire.MsgTx, error) {
func ParseOutpoint(s string) (*wire.OutPoint, error) { func ParseOutpoint(s string) (*wire.OutPoint, error) {
split := strings.Split(s, ":") split := strings.Split(s, ":")
if len(split) != 2 { if len(split) != 2 {
return nil, fmt.Errorf("expecting channel point to be in " + return nil, errors.New("expecting channel point to be in " +
"format of: txid:index") "format of: txid:index")
} }

@ -2,6 +2,7 @@ package lnd
import ( import (
"crypto/sha256" "crypto/sha256"
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -13,6 +14,7 @@ import (
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/shachain" "github.com/lightningnetwork/lnd/shachain"
@ -24,6 +26,16 @@ const (
WalletBIP49DerivationPath = "m/49'/0'/0'" WalletBIP49DerivationPath = "m/49'/0'/0'"
WalletBIP86DerivationPath = "m/86'/0'/0'" WalletBIP86DerivationPath = "m/86'/0'/0'"
LndDerivationPath = "m/1017'/%d'/%d'" LndDerivationPath = "m/1017'/%d'/%d'"
AddressDeriveFromWallet = "fromseed"
)
type AddrType int
const (
AddrTypeP2WKH AddrType = iota
AddrTypeP2WSH
AddrTypeP2TR
) )
func DeriveChildren(key *hdkeychain.ExtendedKey, path []uint32) ( func DeriveChildren(key *hdkeychain.ExtendedKey, path []uint32) (
@ -68,10 +80,10 @@ func DeriveChildren(key *hdkeychain.ExtendedKey, path []uint32) (
func ParsePath(path string) ([]uint32, error) { func ParsePath(path string) ([]uint32, error) {
path = strings.TrimSpace(path) path = strings.TrimSpace(path)
if len(path) == 0 { if len(path) == 0 {
return nil, fmt.Errorf("path cannot be empty") return nil, errors.New("path cannot be empty")
} }
if !strings.HasPrefix(path, "m/") { if !strings.HasPrefix(path, "m/") {
return nil, fmt.Errorf("path must start with m/") return nil, errors.New("path must start with m/")
} }
parts := strings.Split(path, "/") parts := strings.Split(path, "/")
indices := make([]uint32, len(parts)-1) indices := make([]uint32, len(parts)-1)
@ -239,7 +251,7 @@ func DecodeAddressHash(addr string, chainParams *chaincfg.Params) ([]byte, bool,
targetHash = targetAddr.ScriptAddress() targetHash = targetAddr.ScriptAddress()
default: default:
return nil, false, fmt.Errorf("address: must be a bech32 " + return nil, false, errors.New("address: must be a bech32 " +
"P2WPKH or P2WSH address") "P2WPKH or P2WSH address")
} }
return targetHash, isScriptHash, nil return targetHash, isScriptHash, nil
@ -276,11 +288,7 @@ func GetWitnessAddrScript(addr btcutil.Address,
chainParams.Name) chainParams.Name)
} }
builder := txscript.NewScriptBuilder() return txscript.PayToAddrScript(addr)
builder.AddOp(txscript.OP_0)
builder.AddData(addr.ScriptAddress())
return builder.Script()
} }
// GetP2WPKHScript creates a P2WKH output script from an address. If the address // GetP2WPKHScript creates a P2WKH output script from an address. If the address
@ -387,6 +395,155 @@ func P2AnchorStaticRemote(pubKey *btcec.PublicKey,
return p2wsh, commitScript, err return p2wsh, commitScript, err
} }
func P2TaprootStaticRemote(pubKey *btcec.PublicKey,
params *chaincfg.Params) (*btcutil.AddressTaproot,
*input.CommitScriptTree, error) {
scriptTree, err := input.NewRemoteCommitScriptTree(pubKey)
if err != nil {
return nil, nil, fmt.Errorf("could not create script: %w", err)
}
addr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(scriptTree.TaprootKey), params,
)
return addr, scriptTree, err
}
func CheckAddress(addr string, chainParams *chaincfg.Params, allowDerive bool,
hint string, allowedTypes ...AddrType) error {
// We generally always want an address to be specified. If one should
// be derived from the wallet automatically, the user should specify
// "derive" as the address.
if addr == "" {
return fmt.Errorf("%s address cannot be empty", hint)
}
// If we're allowed to derive an address from the wallet, we can skip
// the rest of the checks.
if allowDerive && addr == AddressDeriveFromWallet {
return nil
}
parsedAddr, err := ParseAddress(addr, chainParams)
if err != nil {
return fmt.Errorf("%s address is invalid: %w", hint, err)
}
if !matchAddrType(parsedAddr, allowedTypes...) {
return fmt.Errorf("%s address is of wrong type, allowed "+
"types: %s", hint, addrTypesToString(allowedTypes))
}
return nil
}
func PrepareWalletAddress(addr string, chainParams *chaincfg.Params,
estimator *input.TxWeightEstimator, rootKey *hdkeychain.ExtendedKey,
hint string) ([]byte, error) {
// We already checked if deriving a new address is allowed in a previous
// step, so we can just go ahead and do it now if requested.
if addr == AddressDeriveFromWallet {
// To maximize compatibility and recoverability, we always
// derive the very first P2WKH address from the wallet.
// This corresponds to the derivation path: m/84'/0'/0'/0/0.
derivedKey, err := DeriveChildren(rootKey, []uint32{
HardenedKeyStart + waddrmgr.KeyScopeBIP0084.Purpose,
HardenedKeyStart + chainParams.HDCoinType,
HardenedKeyStart + 0, 0, 0,
})
if err != nil {
return nil, err
}
derivedPubKey, err := derivedKey.ECPubKey()
if err != nil {
return nil, err
}
p2wkhAddr, err := P2WKHAddr(derivedPubKey, chainParams)
if err != nil {
return nil, err
}
return txscript.PayToAddrScript(p2wkhAddr)
}
parsedAddr, err := ParseAddress(addr, chainParams)
if err != nil {
return nil, fmt.Errorf("%s address is invalid: %w", hint, err)
}
// Exit early if we don't need to estimate the weight.
if estimator == nil {
return txscript.PayToAddrScript(parsedAddr)
}
// These are the three address types that we support in general. We
// should have checked that we get the correct type in a previous step.
switch parsedAddr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
estimator.AddP2WKHOutput()
case *btcutil.AddressWitnessScriptHash:
estimator.AddP2WSHOutput()
case *btcutil.AddressTaproot:
estimator.AddP2TROutput()
default:
return nil, fmt.Errorf("%s address is of wrong type", hint)
}
return txscript.PayToAddrScript(parsedAddr)
}
func matchAddrType(addr btcutil.Address, allowedTypes ...AddrType) bool {
contains := func(allowedTypes []AddrType, addrType AddrType) bool {
for _, allowedType := range allowedTypes {
if allowedType == addrType {
return true
}
}
return false
}
switch addr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
return contains(allowedTypes, AddrTypeP2WKH)
case *btcutil.AddressWitnessScriptHash:
return contains(allowedTypes, AddrTypeP2WSH)
case *btcutil.AddressTaproot:
return contains(allowedTypes, AddrTypeP2TR)
default:
return false
}
}
func addrTypesToString(allowedTypes []AddrType) string {
var types []string
for _, allowedType := range allowedTypes {
switch allowedType {
case AddrTypeP2WKH:
types = append(types, "P2WKH")
case AddrTypeP2WSH:
types = append(types, "P2WSH")
case AddrTypeP2TR:
types = append(types, "P2TR")
}
}
return strings.Join(types, ", ")
}
type HDKeyRing struct { type HDKeyRing struct {
ExtendedKey *hdkeychain.ExtendedKey ExtendedKey *hdkeychain.ExtendedKey
ChainParams *chaincfg.Params ChainParams *chaincfg.Params
@ -433,7 +590,7 @@ func (r *HDKeyRing) CheckDescriptor(
// A check doesn't make sense if there is no public key set. // A check doesn't make sense if there is no public key set.
if keyDesc.PubKey == nil { if keyDesc.PubKey == nil {
return fmt.Errorf("no public key provided to check") return errors.New("no public key provided to check")
} }
// Performance fix, derive static path only once. // Performance fix, derive static path only once.
@ -448,7 +605,7 @@ func (r *HDKeyRing) CheckDescriptor(
} }
// Scan the same key range as lnd would do on channel restore. // Scan the same key range as lnd would do on channel restore.
for i := 0; i < keychain.MaxKeyRangeScan; i++ { for i := range keychain.MaxKeyRangeScan {
child, err := DeriveChildren(familyKey, []uint32{uint32(i)}) child, err := DeriveChildren(familyKey, []uint32{uint32(i)})
if err != nil { if err != nil {
return err return err

@ -2,6 +2,7 @@ package lnd
import ( import (
"crypto/sha256" "crypto/sha256"
"errors"
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
@ -30,15 +31,15 @@ func (s *Signer) SignOutputRaw(tx *wire.MsgTx,
// First attempt to fetch the private key which corresponds to the // First attempt to fetch the private key which corresponds to the
// specified public key. // specified public key.
privKey, err := s.FetchPrivKey(&signDesc.KeyDesc) privKey, err := s.FetchPrivateKey(&signDesc.KeyDesc)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return s.SignOutputRawWithPrivkey(tx, signDesc, privKey) return s.SignOutputRawWithPrivateKey(tx, signDesc, privKey)
} }
func (s *Signer) SignOutputRawWithPrivkey(tx *wire.MsgTx, func (s *Signer) SignOutputRawWithPrivateKey(tx *wire.MsgTx,
signDesc *input.SignDescriptor, signDesc *input.SignDescriptor,
privKey *secp256k1.PrivateKey) (input.Signature, error) { privKey *secp256k1.PrivateKey) (input.Signature, error) {
@ -119,10 +120,10 @@ func (s *Signer) SignOutputRawWithPrivkey(tx *wire.MsgTx,
func (s *Signer) ComputeInputScript(_ *wire.MsgTx, _ *input.SignDescriptor) ( func (s *Signer) ComputeInputScript(_ *wire.MsgTx, _ *input.SignDescriptor) (
*input.Script, error) { *input.Script, error) {
return nil, fmt.Errorf("unimplemented") return nil, errors.New("unimplemented")
} }
func (s *Signer) FetchPrivKey(descriptor *keychain.KeyDescriptor) ( func (s *Signer) FetchPrivateKey(descriptor *keychain.KeyDescriptor) (
*btcec.PrivateKey, error) { *btcec.PrivateKey, error) {
key, err := DeriveChildren(s.ExtendedKey, []uint32{ key, err := DeriveChildren(s.ExtendedKey, []uint32{
@ -181,6 +182,58 @@ func (s *Signer) AddPartialSignature(packet *psbt.Packet,
return nil return nil
} }
func (s *Signer) AddPartialSignatureForPrivateKey(packet *psbt.Packet,
privateKey *btcec.PrivateKey, utxo *wire.TxOut, witnessScript []byte,
inputIndex int) error {
// Now we add our partial signature.
prevOutFetcher := wallet.PsbtPrevOutputFetcher(packet)
signDesc := &input.SignDescriptor{
WitnessScript: witnessScript,
Output: utxo,
InputIndex: inputIndex,
HashType: txscript.SigHashAll,
PrevOutputFetcher: prevOutFetcher,
SigHashes: txscript.NewTxSigHashes(
packet.UnsignedTx, prevOutFetcher,
),
}
ourSigRaw, err := s.SignOutputRawWithPrivateKey(
packet.UnsignedTx, signDesc, privateKey,
)
if err != nil {
return fmt.Errorf("error signing with our key: %w", err)
}
ourSig := append(ourSigRaw.Serialize(), byte(txscript.SigHashAll))
// Great, we were able to create our sig, let's add it to the PSBT.
updater, err := psbt.NewUpdater(packet)
if err != nil {
return fmt.Errorf("error creating PSBT updater: %w", err)
}
// If the witness script is the pk script for a P2WPKH output, then we
// need to blank it out for the PSBT code, otherwise it interprets it as
// a P2WSH.
if txscript.IsPayToWitnessPubKeyHash(utxo.PkScript) {
witnessScript = nil
}
status, err := updater.Sign(
inputIndex, ourSig, privateKey.PubKey().SerializeCompressed(),
nil, witnessScript,
)
if err != nil {
return fmt.Errorf("error adding signature to PSBT: %w", err)
}
if status != 0 {
return fmt.Errorf("unexpected status for signature update, "+
"got %d wanted 0", status)
}
return nil
}
// maybeTweakPrivKey examines the single tweak parameters on the passed sign // maybeTweakPrivKey examines the single tweak parameters on the passed sign
// descriptor and may perform a mapping on the passed private key in order to // descriptor and may perform a mapping on the passed private key in order to
// utilize the tweaks, if populated. // utilize the tweaks, if populated.

@ -1,4 +1,4 @@
FROM golang:1.19.4-buster FROM golang:1.22.3-bookworm
RUN apt-get update && apt-get install -y git RUN apt-get update && apt-get install -y git
ENV GOCACHE=/tmp/build/.cache ENV GOCACHE=/tmp/build/.cache

@ -1,10 +1,12 @@
module github.com/lightninglabs/chantools/tools module github.com/lightninglabs/chantools/tools
go 1.18 go 1.21
toolchain go1.22.4
require ( require (
github.com/btcsuite/btcd v0.23.4 github.com/btcsuite/btcd v0.24.0
github.com/golangci/golangci-lint v1.51.2 github.com/golangci/golangci-lint v1.59.0
github.com/ory/go-acc v0.2.8 github.com/ory/go-acc v0.2.8
github.com/rinchsan/gosimports v0.1.5 github.com/rinchsan/gosimports v0.1.5
) )
@ -12,195 +14,206 @@ require (
require ( require (
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
4d63.com/gochecknoglobals v0.2.1 // indirect 4d63.com/gochecknoglobals v0.2.1 // indirect
github.com/Abirdcfly/dupword v0.0.9 // indirect github.com/4meepo/tagalign v1.3.4 // indirect
github.com/Antonboom/errname v0.1.7 // indirect github.com/Abirdcfly/dupword v0.0.14 // indirect
github.com/Antonboom/nilnil v0.1.1 // indirect github.com/Antonboom/errname v0.1.13 // indirect
github.com/BurntSushi/toml v1.2.1 // indirect github.com/Antonboom/nilnil v0.1.9 // indirect
github.com/Antonboom/testifylint v1.3.0 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/Crocmagnon/fatcontext v0.2.2 // indirect
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect
github.com/GaijinEntertainment/go-exhaustruct/v2 v2.3.0 // indirect github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0 // indirect
github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/OpenPeeDeeP/depguard v1.1.1 // indirect github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect
github.com/aead/siphash v1.0.1 // indirect github.com/aead/siphash v1.0.1 // indirect
github.com/alecthomas/go-check-sumtype v0.1.4 // indirect
github.com/alexkohler/nakedret/v2 v2.0.4 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect github.com/alingse/asasalint v0.0.11 // indirect
github.com/ashanbrown/forbidigo v1.4.0 // indirect github.com/ashanbrown/forbidigo v1.6.0 // indirect
github.com/ashanbrown/makezero v1.1.1 // indirect github.com/ashanbrown/makezero v1.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bkielbasa/cyclop v1.2.0 // indirect github.com/bkielbasa/cyclop v1.2.1 // indirect
github.com/blizzy78/varnamelen v0.8.0 // indirect github.com/blizzy78/varnamelen v0.8.0 // indirect
github.com/bombsimon/wsl/v3 v3.4.0 // indirect github.com/bombsimon/wsl/v4 v4.2.1 // indirect
github.com/breml/bidichk v0.2.3 // indirect github.com/breml/bidichk v0.2.7 // indirect
github.com/breml/errchkjson v0.3.0 // indirect github.com/breml/errchkjson v0.3.6 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect
github.com/btcsuite/btcd/btcutil v1.1.0 // indirect github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
github.com/btcsuite/winsvc v1.0.0 // indirect github.com/btcsuite/winsvc v1.0.0 // indirect
github.com/butuzov/ireturn v0.1.1 // indirect github.com/butuzov/ireturn v0.3.0 // indirect
github.com/butuzov/mirror v1.2.0 // indirect
github.com/catenacyber/perfsprint v0.7.1 // indirect
github.com/ccojocar/zxcvbn-go v1.0.2 // indirect
github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/charithe/durationcheck v0.0.9 // indirect github.com/charithe/durationcheck v0.0.10 // indirect
github.com/chavacava/garif v0.0.0-20221024190013-b3ef35877348 // indirect github.com/chavacava/garif v0.1.0 // indirect
github.com/ckaznocha/intrange v0.1.2 // indirect
github.com/curioswitch/go-reassign v0.2.0 // indirect github.com/curioswitch/go-reassign v0.2.0 // indirect
github.com/daixiang0/gci v0.9.1 // indirect github.com/daixiang0/gci v0.13.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/decred/dcrd/lru v1.0.0 // indirect github.com/decred/dcrd/lru v1.0.0 // indirect
github.com/denis-tingaikin/go-header v0.4.3 // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect
github.com/dgraph-io/ristretto v0.0.2 // indirect github.com/dgraph-io/ristretto v0.0.2 // indirect
github.com/esimonov/ifshort v1.0.4 // indirect github.com/ettle/strcase v0.2.0 // indirect
github.com/ettle/strcase v0.1.1 // indirect github.com/fatih/color v1.17.0 // indirect
github.com/fatih/color v1.14.1 // indirect
github.com/fatih/structtag v1.2.0 // indirect github.com/fatih/structtag v1.2.0 // indirect
github.com/firefart/nonamedreturns v1.0.4 // indirect github.com/firefart/nonamedreturns v1.0.5 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/go-critic/go-critic v0.6.7 // indirect github.com/ghostiam/protogetter v0.3.6 // indirect
github.com/go-critic/go-critic v0.11.4 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.0.3 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.1.0 // indirect github.com/go-toolsmith/astequal v1.2.0 // indirect
github.com/go-toolsmith/astfmt v1.1.0 // indirect github.com/go-toolsmith/astfmt v1.1.0 // indirect
github.com/go-toolsmith/astp v1.1.0 // indirect github.com/go-toolsmith/astp v1.1.0 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect
github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.8.1 // indirect github.com/gofrs/flock v0.8.1 // 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/golang/snappy v0.0.4 // indirect
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect
github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe // indirect github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e // indirect
github.com/golangci/gofmt v0.0.0-20220901101216-f2edd75033f2 // indirect github.com/golangci/misspell v0.5.1 // indirect
github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 // indirect github.com/golangci/modinfo v0.3.4 // indirect
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca // indirect github.com/golangci/plugin-module-register v0.1.1 // indirect
github.com/golangci/misspell v0.4.0 // indirect github.com/golangci/revgrep v0.5.3 // indirect
github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6 // indirect github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-cmp v0.5.9 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/gordonklaus/ineffassign v0.1.0 // indirect
github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
github.com/gostaticanalysis/comment v1.4.2 // indirect github.com/gostaticanalysis/comment v1.4.2 // indirect
github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect
github.com/gostaticanalysis/nilerr v0.1.1 // indirect github.com/gostaticanalysis/nilerr v0.1.1 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jessevdk/go-flags v1.4.0 // indirect github.com/jessevdk/go-flags v1.4.0 // indirect
github.com/jgautheron/goconst v1.5.1 // indirect github.com/jgautheron/goconst v1.7.1 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect
github.com/jjti/go-spancheck v0.6.1 // indirect
github.com/jrick/logrotate v1.0.0 // indirect github.com/jrick/logrotate v1.0.0 // indirect
github.com/julz/importas v0.1.0 // indirect github.com/julz/importas v0.1.0 // indirect
github.com/junk1tm/musttag v0.4.5 // indirect github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect
github.com/kisielk/errcheck v1.6.3 // indirect github.com/kisielk/errcheck v1.7.0 // indirect
github.com/kisielk/gotool v1.0.0 // indirect github.com/kkHAIKE/contextcheck v1.1.5 // indirect
github.com/kkHAIKE/contextcheck v1.1.3 // indirect
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 // indirect github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 // indirect
github.com/kulti/thelper v0.6.3 // indirect github.com/kulti/thelper v0.6.3 // indirect
github.com/kunwardeep/paralleltest v1.0.6 // indirect github.com/kunwardeep/paralleltest v1.0.10 // indirect
github.com/kyoh86/exportloopref v0.1.11 // indirect github.com/kyoh86/exportloopref v0.1.11 // indirect
github.com/ldez/gomoddirectives v0.2.3 // indirect github.com/lasiar/canonicalheader v1.1.1 // indirect
github.com/ldez/tagliatelle v0.4.0 // indirect github.com/ldez/gomoddirectives v0.2.4 // indirect
github.com/leonklingele/grouper v1.1.1 // indirect github.com/ldez/tagliatelle v0.5.0 // indirect
github.com/leonklingele/grouper v1.1.2 // indirect
github.com/lufeee/execinquery v1.2.1 // indirect github.com/lufeee/execinquery v1.2.1 // indirect
github.com/macabu/inamedparam v0.1.3 // indirect
github.com/magiconair/properties v1.8.6 // indirect github.com/magiconair/properties v1.8.6 // indirect
github.com/maratori/testableexamples v1.0.0 // indirect github.com/maratori/testableexamples v1.0.0 // indirect
github.com/maratori/testpackage v1.1.0 // indirect github.com/maratori/testpackage v1.1.1 // indirect
github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 // indirect github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mbilski/exhaustivestruct v1.2.0 // indirect github.com/mgechev/revive v1.3.7 // indirect
github.com/mgechev/revive v1.2.5 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moricho/tparallel v0.2.1 // indirect github.com/moricho/tparallel v0.3.1 // indirect
github.com/nakabonne/nestif v0.3.1 // indirect github.com/nakabonne/nestif v0.3.1 // indirect
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/nishanths/exhaustive v0.9.5 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect github.com/nishanths/predeclared v0.2.2 // indirect
github.com/nunnatsa/ginkgolinter v0.8.1 // indirect github.com/nunnatsa/ginkgolinter v0.16.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/ory/viper v1.7.5 // indirect github.com/ory/viper v1.7.5 // indirect
github.com/pborman/uuid v1.2.0 // indirect github.com/pborman/uuid v1.2.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polyfloyd/go-errorlint v1.1.0 // indirect github.com/polyfloyd/go-errorlint v1.5.1 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_golang v1.12.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect github.com/prometheus/procfs v0.7.3 // indirect
github.com/quasilyte/go-ruleguard v0.3.19 // indirect github.com/quasilyte/go-ruleguard v0.4.2 // indirect
github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect github.com/quasilyte/gogrep v0.5.0 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95 // indirect github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/ryancurrah/gomodguard v1.3.0 // indirect github.com/ryancurrah/gomodguard v1.3.2 // indirect
github.com/ryanrolds/sqlclosecheck v0.4.0 // indirect github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.23.0 // indirect github.com/sashamelentyev/usestdlibvars v1.25.0 // indirect
github.com/securego/gosec/v2 v2.15.0 // indirect github.com/securego/gosec/v2 v2.20.1-0.20240525090044-5f0084eb01a9 // indirect
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
github.com/sirupsen/logrus v1.9.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sivchari/containedctx v1.0.2 // indirect github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sivchari/nosnakecase v1.7.0 // indirect
github.com/sivchari/tenv v1.7.1 // indirect github.com/sivchari/tenv v1.7.1 // indirect
github.com/sonatard/noctx v0.0.1 // indirect github.com/sonatard/noctx v0.0.2 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/afero v1.8.2 // indirect github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.6.1 // indirect github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.12.0 // indirect github.com/spf13/viper v1.12.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect
github.com/stretchr/objx v0.5.0 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.8.1 // indirect github.com/stretchr/testify v1.9.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect github.com/subosito/gotenv v1.4.1 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect
github.com/tdakkota/asciicheck v0.1.1 // indirect github.com/tdakkota/asciicheck v0.2.0 // indirect
github.com/tetafro/godot v1.4.11 // indirect github.com/tetafro/godot v1.4.16 // indirect
github.com/timakin/bodyclose v0.0.0-20221125081123-e39cf3fc478e // indirect github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect
github.com/timonwong/loggercheck v0.9.3 // indirect github.com/timonwong/loggercheck v0.9.4 // indirect
github.com/tomarrell/wrapcheck/v2 v2.8.0 // indirect github.com/tomarrell/wrapcheck/v2 v2.8.3 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
github.com/ultraware/funlen v0.0.3 // indirect github.com/ultraware/funlen v0.1.0 // indirect
github.com/ultraware/whitespace v0.0.5 // indirect github.com/ultraware/whitespace v0.1.1 // indirect
github.com/uudashr/gocognit v1.0.6 // indirect github.com/uudashr/gocognit v1.1.2 // indirect
github.com/xen0n/gosmopolitan v1.2.2 // indirect
github.com/yagipy/maintidx v1.0.0 // indirect github.com/yagipy/maintidx v1.0.0 // indirect
github.com/yeya24/promlinter v0.2.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect
gitlab.com/bosi/decorder v0.2.3 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect
gitlab.com/bosi/decorder v0.4.2 // indirect
go-simpler.org/musttag v0.12.2 // indirect
go-simpler.org/sloglint v0.7.0 // indirect
go.uber.org/atomic v1.7.0 // indirect go.uber.org/atomic v1.7.0 // indirect
go.uber.org/automaxprocs v1.5.3 // indirect
go.uber.org/multierr v1.6.0 // indirect go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.17.0 // indirect go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.5.0 // indirect golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9 // indirect golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect
golang.org/x/mod v0.8.0 // indirect golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.1.0 // indirect golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.6.0 // indirect golang.org/x/text v0.15.0 // indirect
golang.org/x/tools v0.6.0 // indirect golang.org/x/tools v0.21.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
honnef.co/go/tools v0.4.2 // indirect honnef.co/go/tools v0.4.7 // indirect
mvdan.cc/gofumpt v0.4.0 // indirect mvdan.cc/gofumpt v0.6.0 // indirect
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect mvdan.cc/unparam v0.0.0-20240427195214-063aff900ca1 // indirect
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d // indirect
) )

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save