├── itest ├── docker │ ├── .gitignore │ ├── .env │ ├── run-cmd.sh │ └── compose.sh ├── log.go ├── integration_test.go ├── cmd_scbforceclose_test.go ├── cmd_sweepremoteclosed_test.go ├── itest.sh ├── cmd_triggerforceclose_test.go ├── standalone_test.go ├── bitcoind_harness.go ├── README.md └── chantools_harness.go ├── doc ├── command-generator │ ├── .gitignore │ ├── src │ │ ├── main.js │ │ ├── App.vue │ │ └── components │ │ │ ├── CommandGenerator.vue │ │ │ ├── TriggerForceClose.vue │ │ │ └── CommandSelector.vue │ ├── vite.config.js │ ├── index.html │ └── package.json ├── rescue-flow.png ├── zombierecovery-flow.png ├── rescue-flow.plantuml ├── chantools_dumpchannels.md ├── chantools_compactdb.md ├── chantools_migratedb.md ├── chantools_signmessage.md ├── chantools_deletepayments.md ├── chantools_showrootkey.md ├── chantools_dropgraphzombies.md ├── chantools_dumpbackup.md ├── chantools_chanbackup.md ├── chantools_removechannel.md ├── chantools_zombierecovery_findmatches.md ├── chantools_vanitygen.md ├── chantools_createwallet.md ├── chantools_zombierecovery.md ├── chantools_filterbackup.md ├── chantools_signpsbt.md ├── chantools_fixoldbackup.md ├── chantools_rescuetweakedkey.md ├── chantools_walletinfo.md ├── chantools_derivekey.md ├── chantools_dropchannelgraph.md ├── chantools_signrescuefunding.md ├── chantools_summary.md ├── chantools_zombierecovery_signoffer.md ├── chantools_zombierecovery_preparekeys.md ├── chantools_pullanchor.md ├── chantools_doublespendinputs.md ├── chantools_zombierecovery_makeoffer.md ├── chantools_triggerforceclose.md ├── chantools_recoverloopin.md ├── chantools_scbforceclose.md ├── chantools_closepoolaccount.md ├── chantools_forceclose.md ├── chantools_sweeptimelock.md ├── chantools_sweepremoteclosed.md ├── chantools_rescuefunding.md ├── zombierecovery-flow.drawio ├── chantools_genimportscript.md └── chantools_fakechanbackup.md ├── .gitignore ├── cmd └── chantools │ ├── testdata │ ├── wallet.db │ ├── channel.db │ └── scbforceclose_testdata.json │ ├── dumpbackup_test.go │ ├── dumpchannels_test.go │ ├── doc.go │ ├── walletinfo_test.go │ ├── chanbackup_test.go │ ├── compactdb_test.go │ ├── showrootkey.go │ ├── zombierecovery_root.go │ ├── zombierecovery_makeoffer_test.go │ ├── showrootkey_test.go │ ├── migratedb.go │ ├── deletepayments.go │ ├── rescuetweakedkey_test.go │ ├── chanbackup.go │ ├── dumpbackup.go │ ├── signmessage.go │ ├── dropgraphzombies.go │ ├── closepoolaccount_test.go │ ├── filterbackup.go │ ├── scbforceclose_test.go │ ├── sweepremoteclosed_test.go │ ├── removechannel.go │ └── root_test.go ├── docker ├── docker-entrypoint.sh ├── umbrel.Dockerfile └── bash-wrapper.sh ├── btc ├── descriptors_test.go ├── descriptors.go └── fasthd │ └── extendedkey.go ├── LICENSE ├── lnd ├── graph.go ├── mock.go ├── chanbackup.go └── channel_test.go ├── .golangci.yml ├── Dockerfile ├── .github └── workflows │ └── main.yml ├── Makefile ├── cln ├── derivation_test.go └── derivation.go ├── release.sh └── scbforceclose └── sign_close_tx_test.go /itest/docker/.gitignore: -------------------------------------------------------------------------------- 1 | node-data 2 | -------------------------------------------------------------------------------- /doc/command-generator/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /chantools 3 | results 4 | /chantools-v* 5 | .idea 6 | -------------------------------------------------------------------------------- /doc/rescue-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightninglabs/chantools/HEAD/doc/rescue-flow.png -------------------------------------------------------------------------------- /doc/zombierecovery-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightninglabs/chantools/HEAD/doc/zombierecovery-flow.png -------------------------------------------------------------------------------- /cmd/chantools/testdata/wallet.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightninglabs/chantools/HEAD/cmd/chantools/testdata/wallet.db -------------------------------------------------------------------------------- /cmd/chantools/testdata/channel.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightninglabs/chantools/HEAD/cmd/chantools/testdata/channel.db -------------------------------------------------------------------------------- /cmd/chantools/dumpbackup_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This file is empty for now, the dumpbackup command is covered by the test in 4 | // chanbackup_test.go. 5 | -------------------------------------------------------------------------------- /cmd/chantools/dumpchannels_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This file is empty for now, the dumpchannels command is covered by the test 4 | // in compactdb_test.go. 5 | -------------------------------------------------------------------------------- /doc/command-generator/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import 'bootstrap/dist/css/bootstrap.css' 4 | import 'bootstrap/dist/js/bootstrap.bundle.js' 5 | 6 | createApp(App).mount('#app') 7 | -------------------------------------------------------------------------------- /docker/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Starting background shell, this container doesn't do anything on its own." 4 | echo "You can connect to it using: docker exec -it bash" 5 | 6 | sleep infinity 7 | -------------------------------------------------------------------------------- /doc/command-generator/vite.config.js: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import {viteSingleFile} from 'vite-plugin-singlefile' 4 | 5 | // See https://vitejs.dev/config/ for more details. 6 | export default defineConfig({ 7 | plugins: [vue(), viteSingleFile()], 8 | }) 9 | -------------------------------------------------------------------------------- /itest/docker/.env: -------------------------------------------------------------------------------- 1 | BITCOIND_VERSION=29 2 | LND_LATEST_VERSION=v0.19.3-beta 3 | CLN_LATEST_VERSION=v25.09 4 | TIMEOUT=15 5 | 6 | CLN_NODES="rusty|nifty|snyke" 7 | 8 | # Change the values in /itest/helpers.go as well when changing these! 9 | ELECTRS_EXPORTED_PORT=3004 10 | DAVE_EXPORTED_PORT=9700 11 | SNYKE_EXPORTED_PORT=9701 12 | -------------------------------------------------------------------------------- /itest/log.go: -------------------------------------------------------------------------------- 1 | package itest 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/btcsuite/btclog/v2" 7 | ) 8 | 9 | var log btclog.Logger 10 | 11 | //nolint:gochecknoinits 12 | func init() { 13 | logger := btclog.NewSLogger(btclog.NewDefaultHandler(os.Stdout)) 14 | logger.SetLevel(btclog.LevelTrace) 15 | log = logger.SubSystem("ITEST") 16 | } 17 | -------------------------------------------------------------------------------- /doc/command-generator/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | chantools command generator 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /cmd/chantools/doc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/spf13/cobra/doc" 6 | ) 7 | 8 | func newDocCommand() *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "doc", 11 | Short: "Generate the markdown documentation of all commands", 12 | Hidden: true, 13 | RunE: func(_ *cobra.Command, _ []string) error { 14 | return doc.GenMarkdownTree(rootCmd, "./doc") 15 | }, 16 | } 17 | 18 | return cmd 19 | } 20 | -------------------------------------------------------------------------------- /itest/docker/run-cmd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The run-cmd.sh file can be used to call any helper functions directly from the 4 | # command line to call any function defined in compose.sh or network.sh. 5 | # For example: 6 | # $ ./run-cmd.sh compose-up 7 | 8 | # DIR is set to the directory of this script. 9 | DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 10 | 11 | source "$DIR/.env" 12 | source "$DIR/compose.sh" 13 | source "$DIR/network.sh" 14 | 15 | CMD=$1 16 | shift 17 | $CMD "$@" 18 | -------------------------------------------------------------------------------- /doc/command-generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chantools-command-generator", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@popperjs/core": "^2.11.8", 13 | "bootstrap": "^5.3.8", 14 | "vue": "^3.2.37" 15 | }, 16 | "devDependencies": { 17 | "@vitejs/plugin-vue": "^6.0.0", 18 | "vite": "^7.1.11", 19 | "vite-plugin-singlefile": "^2.3.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /btc/descriptors_test.go: -------------------------------------------------------------------------------- 1 | package btc 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | var testCases = []struct { 10 | descriptor string 11 | expectedSum string 12 | }{{ 13 | descriptor: "addr(mkmZxiEcEd8ZqjQWVZuC6so5dFMKEFpN2j)", 14 | expectedSum: "#02wpgw69", 15 | }, { 16 | descriptor: "tr(cRhCT5vC5NdnSrQ2Jrah6NPCcth41uT8DWFmA6uD8R4x2ufucnYX)", 17 | expectedSum: "#gwfmkgga", 18 | }} 19 | 20 | func TestDescriptorSum(t *testing.T) { 21 | for _, tc := range testCases { 22 | sum := DescriptorSumCreate(tc.descriptor) 23 | require.Equal(t, tc.descriptor+tc.expectedSum, sum) 24 | 25 | DescriptorSumCheck(sum, true) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docker/umbrel.Dockerfile: -------------------------------------------------------------------------------- 1 | # Start with the chantools base image 2 | ARG VERSION 3 | FROM guggero/chantools:${VERSION:-latest} AS golangbuilder 4 | 5 | FROM tsl0922/ttyd:1.7.7-alpine@sha256:e17d5420fa78ea6271e32a06eec334adda6f54077e56e3969340fb47e604c24c AS final 6 | 7 | # Define a root volume for data persistence. 8 | VOLUME /chantools 9 | WORKDIR /chantools 10 | 11 | # We'll expect the lnd data directory to be mounted here. 12 | VOLUME /lnd 13 | 14 | # Copy the binaries and entrypoint from the builder image. 15 | COPY --from=golangbuilder /bin/chantools /bin/ 16 | COPY ./docker/bash-wrapper.sh /bash-wrapper.sh 17 | 18 | # Add bash. 19 | RUN apk add --no-cache \ 20 | bash \ 21 | jq \ 22 | ca-certificates \ 23 | -------------------------------------------------------------------------------- /cmd/chantools/walletinfo_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/lightninglabs/chantools/lnd" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | const ( 11 | walletContent = "03b99ab108e39e9e4cf565c1b706480180a70a4fdc4828e44c50" + 12 | "4530c056be5b5f" 13 | ) 14 | 15 | func TestWalletInfo(t *testing.T) { 16 | h := newHarness(t) 17 | 18 | // Dump the wallet information. 19 | info := &walletInfoCommand{ 20 | WalletDB: h.testdataFile("wallet.db"), 21 | WithRootKey: true, 22 | } 23 | 24 | t.Setenv(lnd.PasswordEnvName, testPassPhrase) 25 | 26 | err := info.Execute(nil, nil) 27 | require.NoError(t, err) 28 | 29 | h.assertLogContains(walletContent) 30 | h.assertLogContains(rootKeyAezeed) 31 | } 32 | -------------------------------------------------------------------------------- /docker/bash-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Welcome to the chantools command line interface." 4 | echo "Type 'chantools --help' (without quotes) to see available commands." 5 | echo "" 6 | echo "You might also want to visit https://www.node-recovery.com/chantools/" 7 | echo "for the reference documentation and an interactive tool that helps you" 8 | echo "build the command you might want to run." 9 | echo "" 10 | echo "Also, keep in mind that you don't have to run chantools inside this" 11 | echo "container. You can also install it directly on any machine you want," 12 | echo "since there is no direct interaction with your Lightning node required" 13 | echo "and for most command you don't even need anything other than your seed" 14 | echo "phrase." 15 | echo "" 16 | 17 | exec /bin/bash 18 | -------------------------------------------------------------------------------- /cmd/chantools/chanbackup_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | const ( 10 | backupContent = "FundingOutpoint: (string) (len=66) \"10279f626196340" + 11 | "58b6133cb7ac6c1693a8e6df7caa91c6263ca3d0bf704ad4d:0\"" 12 | ) 13 | 14 | func TestChanBackupAndDumpBackup(t *testing.T) { 15 | h := newHarness(t) 16 | 17 | // Create a channel backup from a channel DB file. 18 | makeBackup := &chanBackupCommand{ 19 | ChannelDB: h.testdataFile("channel.db"), 20 | MultiFile: h.tempFile("extracted.backup"), 21 | rootKey: &rootKey{RootKey: rootKeyAezeed}, 22 | } 23 | 24 | err := makeBackup.Execute(nil, nil) 25 | require.NoError(t, err) 26 | 27 | // Decrypt and dump the channel backup file. 28 | dumpBackup := &dumpBackupCommand{ 29 | MultiFile: makeBackup.MultiFile, 30 | rootKey: &rootKey{RootKey: rootKeyAezeed}, 31 | } 32 | 33 | err = dumpBackup.Execute(nil, nil) 34 | require.NoError(t, err) 35 | 36 | h.assertLogContains(backupContent) 37 | } 38 | -------------------------------------------------------------------------------- /cmd/chantools/testdata/scbforceclose_testdata.json: -------------------------------------------------------------------------------- 1 | { 2 | "remote_pubkey": "029e5f4d86d9d6c845fbcf37b09ac7d59c25c19932ab34a2757e8ea88437a876c3", 3 | "transaction_hex": "020000000001011f644a3f04139c2c3b1036f9deb924f7c8101e5825a2bf4a379579beea24bf320100000000b2448780044a010000000000002200202661eee6d24eaf71079b96f8df4dd88aa6280b61845dacdb10d8b0bcc51257af4a0100000000000022002074bcb8019840e0ac7abb16be6c8408fbbebd519cb86193965b33e8e69648865e971f0000000000002200209a1c8e727820d673859049f9305c02c39eb0a718f9219dc2e48a2621243d7dc8b8110c00000000002200205a596aa125a8a39e73f70dcf279cb06295eed49950c9e1f239b47ce41ab0e9320400483045022100ef18b0fe8d34f21ef13316d03cbb72445b61033489a8df81f163ebd60f430637022075a25aa0dc0a08e361540bd831430fc816b0a4ca9ca0169fb95de4a64c297cde01483045022100f8d7b5eee968157f0e06a65c389b6d1f5ca68a3189440b7638ab341c5ac77fdd022069db71847c48b1f762242b99b2fa254b1bce8f44a160293fe3b36ed2d2e32f650147522103ae9df242881bb10a2400e7812fc8cfe437f0f869538584d39d96f52cb2dbaf622103e71742ef40d136884a1f7368fb096cc5897fd697b41a3b481def37b60188c49152aebf573f20" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Oliver Gugger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /doc/rescue-flow.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | (*) --> "1: Node crashed" 4 | --> "2: Rescue on-chain balance" 5 | --> "3: Recover channels using SCB" 6 | if "Pending/Open\nchannels left?" then 7 | -->[yes] "4: Install chantools" 8 | if "5: Is channel DB \navailable?" then 9 | -->[yes] "5: Create copy of channel DB" 10 | --> "6: chantools summary" 11 | --> "7: chantools rescueclosed" 12 | if "Pending/Open\nchannels left?" then 13 | -->[yes] "8: chantools forceclose" 14 | --> "9: Wait for timelocks" 15 | --> "10: chantools sweeptimelock" 16 | if "Pending/Open\nchannels left?" then 17 | -->[yes] ===MANUAL=== 18 | else 19 | -->[no] ===DONE=== 20 | endif 21 | else 22 | -->[no] ===DONE=== 23 | endif 24 | else 25 | -->[no] ===MANUAL=== 26 | --> "11: Manual intervention necessary" 27 | --> "12: Use Zombie Channel Recovery Matcher" 28 | --> (*) 29 | endif 30 | else 31 | -->[no] ===DONE=== 32 | note right 33 | Recovery complete 34 | end note 35 | endif 36 | 37 | --> (*) 38 | 39 | @enduml -------------------------------------------------------------------------------- /itest/integration_test.go: -------------------------------------------------------------------------------- 1 | package itest 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type testCase struct { 8 | name string 9 | fn func(t *testing.T) 10 | } 11 | 12 | var testCases = []testCase{ 13 | { 14 | name: "zombie recovery lnd <-> lnd", 15 | fn: runZombieRecoveryLndLnd, 16 | }, 17 | { 18 | name: "zombie recovery lnd <-> cln", 19 | fn: runZombieRecoveryLndCln, 20 | }, 21 | { 22 | name: "zombie recovery cln <-> lnd", 23 | fn: runZombieRecoveryClnLnd, 24 | }, 25 | { 26 | name: "zombie recovery cln <-> cln", 27 | fn: runZombieRecoveryClnCln, 28 | }, 29 | { 30 | name: "sweep remote closed lnd", 31 | fn: runSweepRemoteClosedLnd, 32 | }, 33 | { 34 | name: "sweep remote closed cln", 35 | fn: runSweepRemoteClosedCln, 36 | }, 37 | { 38 | name: "trigger force close lnd", 39 | fn: runTriggerForceCloseLnd, 40 | }, 41 | { 42 | name: "trigger force close cln", 43 | fn: runTriggerForceCloseCln, 44 | }, 45 | { 46 | name: "scb force close", 47 | fn: runScbForceClose, 48 | }, 49 | } 50 | 51 | // TestIntegration runs all integration test cases. 52 | func TestIntegration(t *testing.T) { 53 | for _, tc := range testCases { 54 | t.Run(tc.name, tc.fn) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /cmd/chantools/compactdb_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestCompactDBAndDumpChannels(t *testing.T) { 10 | h := newHarness(t) 11 | 12 | // Compact the test DB. 13 | compact := &compactDBCommand{ 14 | SourceDB: h.testdataFile("channel.db"), 15 | DestDB: h.tempFile("compacted.db"), 16 | } 17 | 18 | err := compact.Execute(nil, nil) 19 | require.NoError(t, err) 20 | 21 | require.FileExists(t, compact.DestDB) 22 | 23 | // Compacting small DBs actually increases the size slightly. But we 24 | // just want to make sure the contents match. 25 | require.GreaterOrEqual( 26 | t, h.fileSize(compact.DestDB), h.fileSize(compact.SourceDB), 27 | ) 28 | 29 | // Compare the content of the source and destination DB by looking at 30 | // the logged dump. 31 | dump := &dumpChannelsCommand{ 32 | ChannelDB: compact.SourceDB, 33 | } 34 | h.clearLog() 35 | err = dump.Execute(nil, nil) 36 | require.NoError(t, err) 37 | sourceDump := h.getLog() 38 | 39 | h.clearLog() 40 | dump.ChannelDB = compact.DestDB 41 | err = dump.Execute(nil, nil) 42 | require.NoError(t, err) 43 | destDump := h.getLog() 44 | 45 | h.assertLogEqual(sourceDump, destDump) 46 | } 47 | -------------------------------------------------------------------------------- /lnd/graph.go: -------------------------------------------------------------------------------- 1 | package lnd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lightningnetwork/lnd/lnrpc" 7 | ) 8 | 9 | func AllNodeChannels(graph *lnrpc.ChannelGraph, 10 | nodePubKey string) []*lnrpc.ChannelEdge { 11 | 12 | var result []*lnrpc.ChannelEdge //nolint:prealloc 13 | for _, edge := range graph.Edges { 14 | if edge.Node1Pub != nodePubKey && edge.Node2Pub != nodePubKey { 15 | continue 16 | } 17 | 18 | result = append(result, edge) 19 | } 20 | 21 | return result 22 | } 23 | 24 | func FindCommonEdges(graph *lnrpc.ChannelGraph, node1, 25 | node2 string) []*lnrpc.ChannelEdge { 26 | 27 | var result []*lnrpc.ChannelEdge //nolint:prealloc 28 | for _, edge := range graph.Edges { 29 | if edge.Node1Pub != node1 && edge.Node2Pub != node1 { 30 | continue 31 | } 32 | 33 | if edge.Node1Pub != node2 && edge.Node2Pub != node2 { 34 | continue 35 | } 36 | 37 | result = append(result, edge) 38 | } 39 | 40 | return result 41 | } 42 | 43 | func FindNode(graph *lnrpc.ChannelGraph, 44 | nodePubKey string) (*lnrpc.LightningNode, error) { 45 | 46 | for _, node := range graph.Nodes { 47 | if node.PubKey == nodePubKey { 48 | return node, nil 49 | } 50 | } 51 | 52 | return nil, fmt.Errorf("node %s not found in graph", nodePubKey) 53 | } 54 | -------------------------------------------------------------------------------- /lnd/mock.go: -------------------------------------------------------------------------------- 1 | package lnd 2 | 3 | import ( 4 | "github.com/lightningnetwork/lnd/htlcswitch" 5 | "github.com/lightningnetwork/lnd/lnwallet" 6 | "github.com/lightningnetwork/lnd/lnwire" 7 | ) 8 | 9 | // mockMessageSwitch is a mock implementation of the messageSwitch interface 10 | // used for testing without relying on a *htlcswitch.Switch in unit tests. 11 | type mockMessageSwitch struct { 12 | links []htlcswitch.ChannelUpdateHandler 13 | } 14 | 15 | // BestHeight currently returns a dummy value. 16 | func (m *mockMessageSwitch) BestHeight() uint32 { 17 | return 0 18 | } 19 | 20 | // CircuitModifier currently returns a dummy value. 21 | func (m *mockMessageSwitch) CircuitModifier() htlcswitch.CircuitModifier { 22 | return nil 23 | } 24 | 25 | // RemoveLink currently does nothing. 26 | func (m *mockMessageSwitch) RemoveLink(lnwire.ChannelID) {} 27 | 28 | // CreateAndAddLink currently returns a dummy value. 29 | func (m *mockMessageSwitch) CreateAndAddLink(htlcswitch.ChannelLinkConfig, 30 | *lnwallet.LightningChannel) error { 31 | 32 | return nil 33 | } 34 | 35 | // GetLinksByInterface returns the active links. 36 | func (m *mockMessageSwitch) GetLinksByInterface([33]byte) ( 37 | []htlcswitch.ChannelUpdateHandler, error) { 38 | 39 | return m.links, nil 40 | } 41 | -------------------------------------------------------------------------------- /itest/cmd_scbforceclose_test.go: -------------------------------------------------------------------------------- 1 | package itest 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/lightningnetwork/lnd/lnrpc" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func runScbForceClose(t *testing.T) { 12 | charlieChannels := readChannelsJSON(t, "charlie") 13 | snykeIdentity := getNodeIdentityKeyCln(t, "snyke") 14 | 15 | var charlieSnykeChannel *lnrpc.Channel 16 | for _, c := range charlieChannels { 17 | if c.RemotePubkey == snykeIdentity { 18 | charlieSnykeChannel = c 19 | } 20 | } 21 | require.NotNil( 22 | t, charlieSnykeChannel, "charlie-snyke channel not found", 23 | ) 24 | 25 | scbFile := fmt.Sprintf(scbFilePattern, "charlie") 26 | txHex, fullOutput := getScbForceClose( 27 | t, "charlie", tempDir, scbFile, 28 | charlieSnykeChannel.ChannelPoint, 29 | ) 30 | 31 | // Outputs on a force-close transaction are always ordered by amount. 32 | require.Contains( 33 | t, fullOutput, "Possible anchor: idx=0 amount=330 sat", 34 | ) 35 | require.Contains( 36 | t, fullOutput, "Possible anchor: idx=1 amount=330 sat", 37 | ) 38 | require.Contains(t, fullOutput, "Output to_remote: idx=2 amount=") 39 | require.Contains(t, fullOutput, "Possible to_local/htlc: idx=3 amount=") 40 | 41 | backend := connectBitcoind(t) 42 | publishTx(t, txHex, backend) 43 | } 44 | -------------------------------------------------------------------------------- /cmd/chantools/showrootkey.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | const showRootKeyFormat = ` 10 | Your BIP32 HD root key is: %v 11 | ` 12 | 13 | type showRootKeyCommand struct { 14 | rootKey *rootKey 15 | cmd *cobra.Command 16 | } 17 | 18 | func newShowRootKeyCommand() *cobra.Command { 19 | cc := &showRootKeyCommand{} 20 | cc.cmd = &cobra.Command{ 21 | Use: "showrootkey", 22 | Short: "Extract and show the BIP32 HD root key from the 24 " + 23 | "word lnd aezeed", 24 | Long: `This command converts the 24 word lnd aezeed phrase and 25 | password to the BIP32 HD root key that is used as the --rootkey flag in other 26 | commands of this tool.`, 27 | Example: `chantools showrootkey`, 28 | RunE: cc.Execute, 29 | } 30 | 31 | cc.rootKey = newRootKey(cc.cmd, "decrypting the backup") 32 | 33 | return cc.cmd 34 | } 35 | 36 | func (c *showRootKeyCommand) Execute(_ *cobra.Command, _ []string) error { 37 | extendedKey, err := c.rootKey.read() 38 | if err != nil { 39 | return fmt.Errorf("error reading root key: %w", err) 40 | } 41 | 42 | result := fmt.Sprintf(showRootKeyFormat, extendedKey) 43 | fmt.Println(result) 44 | 45 | // For the tests, also log as trace level which is disabled by default. 46 | log.Tracef(result) 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /cmd/chantools/zombierecovery_root.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type zombieRecoveryCommand struct { 10 | cmd *cobra.Command 11 | } 12 | 13 | func newZombieRecoveryCommand() *cobra.Command { 14 | cc := &zombieRecoveryCommand{} 15 | cc.cmd = &cobra.Command{ 16 | Use: "zombierecovery", 17 | Short: "Try rescuing funds stuck in channels with zombie nodes", 18 | Long: `A sub command that hosts a set of further sub commands 19 | to help with recovering funds stuck in zombie channels. 20 | 21 | Please visit https://github.com/lightninglabs/chantools/blob/master/doc/zombierecovery.md 22 | for more information on how to use these commands. 23 | 24 | Check out https://guggero.github.io/chantools/doc/command-generator.html for an 25 | interactive GUI that guides you through the different steps. 26 | `, 27 | Run: func(cmd *cobra.Command, args []string) { 28 | if len(args) == 0 { 29 | _ = cmd.Help() 30 | os.Exit(0) 31 | } 32 | }, 33 | } 34 | 35 | cobra.EnableCommandSorting = false 36 | cc.cmd.AddCommand( 37 | // Here the order matters, we don't want them to be 38 | // alphabetically sorted but by step number. 39 | newZombieRecoveryFindMatchesCommand(), 40 | newZombieRecoveryPrepareKeysCommand(), 41 | newZombieRecoveryMakeOfferCommand(), 42 | newZombieRecoverySignOfferCommand(), 43 | ) 44 | 45 | return cc.cmd 46 | } 47 | -------------------------------------------------------------------------------- /itest/cmd_sweepremoteclosed_test.go: -------------------------------------------------------------------------------- 1 | package itest 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "testing" 7 | 8 | "github.com/btcsuite/btcd/wire" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func runSweepRemoteClosedLnd(t *testing.T) { 13 | sweepAddr := randTaprootAddr(t) 14 | txHex := getSweepRemoteClosed( 15 | t, "charlie", tempDir, localElectrsAddr, sweepAddr, 16 | ) 17 | 18 | txBytes, err := hex.DecodeString(txHex) 19 | require.NoError(t, err) 20 | 21 | var tx wire.MsgTx 22 | err = tx.Deserialize(bytes.NewReader(txBytes)) 23 | require.NoError(t, err) 24 | 25 | backend := connectBitcoind(t) 26 | txHash, err := backend.SendRawTransaction(&tx, false) 27 | require.NoError(t, err) 28 | t.Logf("Sweep transaction sent: %v", txHash.String()) 29 | } 30 | 31 | func runSweepRemoteClosedCln(t *testing.T) { 32 | aliceIdentity := getNodeIdentityKey(t, "alice") 33 | sweepAddr := randTaprootAddr(t) 34 | txHex := getSweepRemoteClosedCln( 35 | t, "rusty", tempDir, localElectrsAddr, aliceIdentity, sweepAddr, 36 | ) 37 | 38 | txBytes, err := hex.DecodeString(txHex) 39 | require.NoError(t, err) 40 | 41 | var tx wire.MsgTx 42 | err = tx.Deserialize(bytes.NewReader(txBytes)) 43 | require.NoError(t, err) 44 | 45 | backend := connectBitcoind(t) 46 | txHash, err := backend.SendRawTransaction(&tx, false) 47 | require.NoError(t, err) 48 | t.Logf("Sweep transaction sent: %v", txHash.String()) 49 | } 50 | -------------------------------------------------------------------------------- /itest/itest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script sets up the dockerized LN cluster and runs the integration tests. 4 | 5 | # Stop the script if an error is returned by any step. 6 | set -e 7 | 8 | # ITEST_DIR is set to the directory of this script. 9 | ITEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 10 | 11 | source "$ITEST_DIR/docker/compose.sh" 12 | source "$ITEST_DIR/docker/network.sh" 13 | source "$ITEST_DIR/docker/.env" 14 | 15 | # We make sure the cluster is always stopped in case it was left running from a 16 | # previous run. 17 | compose_down || true 18 | 19 | # Ensure that the cluster is shut down when the script errors out due to the 20 | # set -e flag above. 21 | trap compose_down ERR 22 | 23 | # If the user doesn't intend to debug the tests (i.e. by keeping the containers 24 | # running after the tests), then we ensure that the cluster is torn down at the 25 | # end of the script. 26 | if [[ -z "$DEBUG" ]] && [[ -z "$debug" ]]; then 27 | trap compose_down EXIT 28 | else 29 | echo "⚠️ Debug mode enabled, not stopping cluster at the end of the tests." 30 | fi 31 | 32 | "$ITEST_DIR"/docker/setup-test-network.sh 33 | 34 | # The network setup part was successful, we don't need that trap anymore. 35 | trap - ERR 36 | 37 | # Make sure we're in the itest directory before running the tests. 38 | cd "$ITEST_DIR" 39 | 40 | # Run the integration tests. 41 | go test -v -count=1 -test.run ^TestIntegration${CASE} ./... 42 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | run: 4 | timeout: 5m 5 | go: "1.23" 6 | 7 | linters: 8 | default: all 9 | disable: 10 | - gochecknoglobals 11 | - gosec 12 | - funlen 13 | - varnamelen 14 | - wrapcheck 15 | - testpackage 16 | - err113 17 | - exhaustruct 18 | - forbidigo 19 | - gocognit 20 | - nestif 21 | - wsl 22 | - cyclop 23 | - gocyclo 24 | - nlreturn 25 | - paralleltest 26 | - ireturn 27 | - maintidx 28 | - noctx 29 | - exhaustive 30 | - protogetter 31 | - depguard 32 | - mnd 33 | - gomoddirectives 34 | - nolintlint 35 | - errcheck 36 | - funcorder 37 | - godoclint 38 | - noinlineerr 39 | - revive 40 | - wsl_v5 41 | 42 | settings: 43 | govet: 44 | disable: 45 | # Don't report about shadowed variables. 46 | - shadow 47 | whitespace: 48 | multi-func: true 49 | multi-if: true 50 | tagliatelle: 51 | case: 52 | rules: 53 | json: snake 54 | staticcheck: 55 | checks: [ "-SA1019" ] 56 | gomoddirectives: 57 | replace-allow-list: 58 | # See go.mod for the explanation why these are needed. 59 | - google.golang.org/protobuf 60 | 61 | exclusions: 62 | rules: 63 | - path: cmd/chantools 64 | linters: 65 | - lll 66 | - path: itest 67 | linters: 68 | - dupl 69 | - thelper 70 | -------------------------------------------------------------------------------- /doc/chantools_dumpchannels.md: -------------------------------------------------------------------------------- 1 | ## chantools dumpchannels 2 | 3 | Dump all channel information from an lnd channel database 4 | 5 | ### Synopsis 6 | 7 | This command dumps all open and pending channels from the 8 | given lnd channel.db gile in a human readable format. 9 | 10 | ``` 11 | chantools dumpchannels [flags] 12 | ``` 13 | 14 | ### Examples 15 | 16 | ``` 17 | chantools dumpchannels \ 18 | --channeldb ~/.lnd/data/graph/mainnet/channel.db 19 | ``` 20 | 21 | ### Options 22 | 23 | ``` 24 | --channeldb string lnd channel.db file to dump channels from 25 | --closed dump closed channels instead of open 26 | -h, --help help for dumpchannels 27 | --pending dump pending channels instead of open 28 | --waiting_close dump waiting close channels instead of open 29 | ``` 30 | 31 | ### Options inherited from parent commands 32 | 33 | ``` 34 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 35 | -r, --regtest Indicates if regtest parameters should be used 36 | --resultsdir string Directory where results should be stored (default "./results") 37 | -s, --signet Indicates if the public signet parameters should be used 38 | -t, --testnet Indicates if testnet parameters should be used 39 | ``` 40 | 41 | ### SEE ALSO 42 | 43 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 44 | 45 | -------------------------------------------------------------------------------- /doc/chantools_compactdb.md: -------------------------------------------------------------------------------- 1 | ## chantools compactdb 2 | 3 | Create a copy of a channel.db file in safe/read-only mode 4 | 5 | ### Synopsis 6 | 7 | This command opens a database in read-only mode and tries 8 | to create a copy of it to a destination file, compacting it in the process. 9 | 10 | ``` 11 | chantools compactdb [flags] 12 | ``` 13 | 14 | ### Examples 15 | 16 | ``` 17 | chantools compactdb \ 18 | --sourcedb ~/.lnd/data/graph/mainnet/channel.db \ 19 | --destdb ./results/compacted.db 20 | ``` 21 | 22 | ### Options 23 | 24 | ``` 25 | --destdb string new lnd channel.db file to copy the compacted database to 26 | -h, --help help for compactdb 27 | --sourcedb string lnd channel.db file to create the database backup from 28 | --txmaxsize int maximum transaction size (default 65536) 29 | ``` 30 | 31 | ### Options inherited from parent commands 32 | 33 | ``` 34 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 35 | -r, --regtest Indicates if regtest parameters should be used 36 | --resultsdir string Directory where results should be stored (default "./results") 37 | -s, --signet Indicates if the public signet parameters should be used 38 | -t, --testnet Indicates if testnet parameters should be used 39 | ``` 40 | 41 | ### SEE ALSO 42 | 43 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 44 | 45 | -------------------------------------------------------------------------------- /cmd/chantools/zombierecovery_makeoffer_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/btcsuite/btcd/btcec/v2" 8 | "github.com/btcsuite/btcd/chaincfg" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestMatchScript(t *testing.T) { 13 | testCases := []struct { 14 | key1 string 15 | key2 string 16 | addr string 17 | params *chaincfg.Params 18 | }{{ 19 | key1: "0201943d78d61c8ad50ba57164830f536c156d8d89d979448bef3e67f564ea0ab6", 20 | key2: "038b88de18064024e9da4dfc9c804283b3077a265dcd73ad3615b50badcbdebd5b", 21 | addr: "bc1qp5jnhnavt32fjwhnf5ttpvvym7e0syp79q5l9skz545q62d8u2uq05ul63", 22 | params: &chaincfg.MainNetParams, 23 | }, { 24 | key1: "03585d8e760bd0925da67d9c22a69dcad9f51f90a39f9a681971268555975ea30d", 25 | key2: "0326a2171c97673cc8cd7a04a043f0224c59591fc8c9de320a48f7c9b68ab0ae2b", 26 | addr: "bcrt1qhcn39q6jc0krkh9va230y2z6q96zadt8fhxw3erv92fzlrw83cyq40nwek", 27 | params: &chaincfg.RegressionNetParams, 28 | }} 29 | 30 | for _, tc := range testCases { 31 | key1Bytes, err := hex.DecodeString(tc.key1) 32 | require.NoError(t, err) 33 | key1, err := btcec.ParsePubKey(key1Bytes) 34 | require.NoError(t, err) 35 | 36 | key2Bytes, err := hex.DecodeString(tc.key2) 37 | require.NoError(t, err) 38 | key2, err := btcec.ParsePubKey(key2Bytes) 39 | require.NoError(t, err) 40 | 41 | ok, _, _, err := matchScript(tc.addr, key1, key2, tc.params) 42 | require.NoError(t, err) 43 | require.True(t, ok) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cmd/chantools/showrootkey_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/lightninglabs/chantools/btc" 7 | "github.com/lightninglabs/chantools/lnd" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestShowRootKey(t *testing.T) { 12 | h := newHarness(t) 13 | 14 | // Derive the root key from the aezeed. 15 | show := &showRootKeyCommand{ 16 | rootKey: &rootKey{}, 17 | } 18 | 19 | t.Setenv(lnd.MnemonicEnvName, seedAezeedNoPassphrase) 20 | t.Setenv(lnd.PassphraseEnvName, "-") 21 | 22 | err := show.Execute(nil, nil) 23 | require.NoError(t, err) 24 | 25 | h.assertLogContains(rootKeyAezeed) 26 | } 27 | 28 | func TestShowRootKeyBIP39(t *testing.T) { 29 | h := newHarness(t) 30 | 31 | // Derive the root key from the BIP39 seed. 32 | show := &showRootKeyCommand{ 33 | rootKey: &rootKey{BIP39: true}, 34 | } 35 | 36 | t.Setenv(btc.BIP39MnemonicEnvName, seedBip39) 37 | t.Setenv(btc.BIP39PassphraseEnvName, "-") 38 | 39 | err := show.Execute(nil, nil) 40 | require.NoError(t, err) 41 | 42 | h.assertLogContains(rootKeyBip39) 43 | } 44 | 45 | func TestShowRootKeyBIP39WithPassphrase(t *testing.T) { 46 | h := newHarness(t) 47 | 48 | // Derive the root key from the BIP39 seed. 49 | show := &showRootKeyCommand{ 50 | rootKey: &rootKey{BIP39: true}, 51 | } 52 | 53 | t.Setenv(btc.BIP39MnemonicEnvName, seedBip39) 54 | t.Setenv(btc.BIP39PassphraseEnvName, testPassPhrase) 55 | 56 | err := show.Execute(nil, nil) 57 | require.NoError(t, err) 58 | 59 | h.assertLogContains(rootKeyBip39Passphrase) 60 | } 61 | -------------------------------------------------------------------------------- /itest/cmd_triggerforceclose_test.go: -------------------------------------------------------------------------------- 1 | package itest 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/lightningnetwork/lnd/lnrpc" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func runTriggerForceCloseLnd(t *testing.T) { 12 | daveChannels := readChannelsJSON(t, "dave") 13 | charlieIdentity := getNodeIdentityKey(t, "charlie") 14 | daveIdentity := readNodeIdentityFromFile(t, "dave") 15 | daveURI := fmt.Sprintf(nodeURIPattern, daveIdentity, localDaveAddr) 16 | 17 | var charlieDaveChannel *lnrpc.Channel 18 | for _, c := range daveChannels { 19 | if c.RemotePubkey == charlieIdentity { 20 | charlieDaveChannel = c 21 | } 22 | } 23 | require.NotNil(t, charlieDaveChannel, "charlie-dave channel not found") 24 | 25 | txid := getTriggerForceClose( 26 | t, "charlie", tempDir, localElectrsAddr, daveURI, 27 | charlieDaveChannel.ChannelPoint, 28 | ) 29 | 30 | t.Logf("Force close TX found: %v", txid) 31 | } 32 | 33 | func runTriggerForceCloseCln(t *testing.T) { 34 | aliceChannels := readChannelsJSON(t, "alice") 35 | snykeIdentity := getNodeIdentityKeyCln(t, "snyke") 36 | snykeURI := fmt.Sprintf(nodeURIPattern, snykeIdentity, localSnykeAddr) 37 | 38 | var aliceSnykeChannel *lnrpc.Channel 39 | for _, c := range aliceChannels { 40 | if c.RemotePubkey == snykeIdentity { 41 | aliceSnykeChannel = c 42 | } 43 | } 44 | require.NotNil(t, aliceSnykeChannel, "alice-snyke channel not found") 45 | 46 | txid := getTriggerForceClose( 47 | t, "alice", tempDir, localElectrsAddr, snykeURI, 48 | aliceSnykeChannel.ChannelPoint, 49 | ) 50 | 51 | t.Logf("Force close TX found: %v", txid) 52 | } 53 | -------------------------------------------------------------------------------- /cmd/chantools/migratedb.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/lightninglabs/chantools/lnd" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type migrateDBCommand struct { 12 | ChannelDB string 13 | 14 | cmd *cobra.Command 15 | } 16 | 17 | func newMigrateDBCommand() *cobra.Command { 18 | cc := &migrateDBCommand{} 19 | cc.cmd = &cobra.Command{ 20 | Use: "migratedb", 21 | Short: "Apply all recent lnd channel database migrations", 22 | Long: `This command opens an lnd channel database in write mode 23 | and applies all recent database migrations to it. This can be used to update 24 | an old database file to be compatible with the current version that chantools 25 | needs to read the database content. 26 | 27 | CAUTION: Running this command will make it impossible to use the channel DB 28 | with an older version of lnd. Downgrading is not possible and you'll need to 29 | run lnd ` + lndVersion + ` or later after using this command!'`, 30 | Example: `chantools migratedb \ 31 | --channeldb ~/.lnd/data/graph/mainnet/channel.db`, 32 | RunE: cc.Execute, 33 | } 34 | cc.cmd.Flags().StringVar( 35 | &cc.ChannelDB, "channeldb", "", "lnd channel.db file to "+ 36 | "migrate", 37 | ) 38 | 39 | return cc.cmd 40 | } 41 | 42 | func (c *migrateDBCommand) Execute(_ *cobra.Command, _ []string) error { 43 | // Check that we have a channel DB. 44 | if c.ChannelDB == "" { 45 | return errors.New("channel DB is required") 46 | } 47 | db, _, err := lnd.OpenDB(c.ChannelDB, false) 48 | if err != nil { 49 | return fmt.Errorf("error opening DB: %w", err) 50 | } 51 | 52 | return db.Close() 53 | } 54 | -------------------------------------------------------------------------------- /doc/chantools_migratedb.md: -------------------------------------------------------------------------------- 1 | ## chantools migratedb 2 | 3 | Apply all recent lnd channel database migrations 4 | 5 | ### Synopsis 6 | 7 | This command opens an lnd channel database in write mode 8 | and applies all recent database migrations to it. This can be used to update 9 | an old database file to be compatible with the current version that chantools 10 | needs to read the database content. 11 | 12 | CAUTION: Running this command will make it impossible to use the channel DB 13 | with an older version of lnd. Downgrading is not possible and you'll need to 14 | run lnd v0.19.0-beta or later after using this command!' 15 | 16 | ``` 17 | chantools migratedb [flags] 18 | ``` 19 | 20 | ### Examples 21 | 22 | ``` 23 | chantools migratedb \ 24 | --channeldb ~/.lnd/data/graph/mainnet/channel.db 25 | ``` 26 | 27 | ### Options 28 | 29 | ``` 30 | --channeldb string lnd channel.db file to migrate 31 | -h, --help help for migratedb 32 | ``` 33 | 34 | ### Options inherited from parent commands 35 | 36 | ``` 37 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 38 | -r, --regtest Indicates if regtest parameters should be used 39 | --resultsdir string Directory where results should be stored (default "./results") 40 | -s, --signet Indicates if the public signet parameters should be used 41 | -t, --testnet Indicates if testnet parameters should be used 42 | ``` 43 | 44 | ### SEE ALSO 45 | 46 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 47 | 48 | -------------------------------------------------------------------------------- /doc/chantools_signmessage.md: -------------------------------------------------------------------------------- 1 | ## chantools signmessage 2 | 3 | Sign a message with the node's private key. 4 | 5 | ### Synopsis 6 | 7 | Sign msg with the resident node's private key. 8 | Returns the signature as a zbase32 string. 9 | 10 | ``` 11 | chantools signmessage [flags] 12 | ``` 13 | 14 | ### Examples 15 | 16 | ``` 17 | chantools signmessage --msg=foobar 18 | ``` 19 | 20 | ### Options 21 | 22 | ``` 23 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 24 | -h, --help help for signmessage 25 | --msg string the message to sign 26 | --rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed 27 | --walletdb string read the seed/master root key to use for decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 28 | ``` 29 | 30 | ### Options inherited from parent commands 31 | 32 | ``` 33 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 34 | -r, --regtest Indicates if regtest parameters should be used 35 | --resultsdir string Directory where results should be stored (default "./results") 36 | -s, --signet Indicates if the public signet parameters should be used 37 | -t, --testnet Indicates if testnet parameters should be used 38 | ``` 39 | 40 | ### SEE ALSO 41 | 42 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 43 | 44 | -------------------------------------------------------------------------------- /doc/chantools_deletepayments.md: -------------------------------------------------------------------------------- 1 | ## chantools deletepayments 2 | 3 | Remove all (failed) payments from a channel DB 4 | 5 | ### Synopsis 6 | 7 | This command removes all payments from a channel DB. 8 | If only the failed payments should be deleted (and not the successful ones), the 9 | --failedonly flag can be specified. 10 | 11 | CAUTION: Running this command will make it impossible to use the channel DB 12 | with an older version of lnd. Downgrading is not possible and you'll need to 13 | run lnd v0.19.0-beta or later after using this command!' 14 | 15 | ``` 16 | chantools deletepayments [flags] 17 | ``` 18 | 19 | ### Examples 20 | 21 | ``` 22 | chantools deletepayments --failedonly \ 23 | --channeldb ~/.lnd/data/graph/mainnet/channel.db 24 | ``` 25 | 26 | ### Options 27 | 28 | ``` 29 | --channeldb string lnd channel.db file to dump channels from 30 | --failedonly don't delete all payments, only failed ones 31 | -h, --help help for deletepayments 32 | ``` 33 | 34 | ### Options inherited from parent commands 35 | 36 | ``` 37 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 38 | -r, --regtest Indicates if regtest parameters should be used 39 | --resultsdir string Directory where results should be stored (default "./results") 40 | -s, --signet Indicates if the public signet parameters should be used 41 | -t, --testnet Indicates if testnet parameters should be used 42 | ``` 43 | 44 | ### SEE ALSO 45 | 46 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 47 | 48 | -------------------------------------------------------------------------------- /doc/chantools_showrootkey.md: -------------------------------------------------------------------------------- 1 | ## chantools showrootkey 2 | 3 | Extract and show the BIP32 HD root key from the 24 word lnd aezeed 4 | 5 | ### Synopsis 6 | 7 | This command converts the 24 word lnd aezeed phrase and 8 | password to the BIP32 HD root key that is used as the --rootkey flag in other 9 | commands of this tool. 10 | 11 | ``` 12 | chantools showrootkey [flags] 13 | ``` 14 | 15 | ### Examples 16 | 17 | ``` 18 | chantools showrootkey 19 | ``` 20 | 21 | ### Options 22 | 23 | ``` 24 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 25 | -h, --help help for showrootkey 26 | --rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed 27 | --walletdb string read the seed/master root key to use for decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 28 | ``` 29 | 30 | ### Options inherited from parent commands 31 | 32 | ``` 33 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 34 | -r, --regtest Indicates if regtest parameters should be used 35 | --resultsdir string Directory where results should be stored (default "./results") 36 | -s, --signet Indicates if the public signet parameters should be used 37 | -t, --testnet Indicates if testnet parameters should be used 38 | ``` 39 | 40 | ### SEE ALSO 41 | 42 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 43 | 44 | -------------------------------------------------------------------------------- /doc/chantools_dropgraphzombies.md: -------------------------------------------------------------------------------- 1 | ## chantools dropgraphzombies 2 | 3 | Remove all channels identified as zombies from the graph to force a re-sync of the graph 4 | 5 | ### Synopsis 6 | 7 | This command removes all channels that were identified as 8 | zombies from the local graph. 9 | 10 | This will cause lnd to re-download all those channels from the network and can 11 | be helpful to fix a graph that is out of sync with the network. 12 | 13 | CAUTION: Running this command will make it impossible to use the channel DB 14 | with an older version of lnd. Downgrading is not possible and you'll need to 15 | run lnd v0.19.0-beta or later after using this command!' 16 | 17 | ``` 18 | chantools dropgraphzombies [flags] 19 | ``` 20 | 21 | ### Examples 22 | 23 | ``` 24 | chantools dropgraphzombies \ 25 | --channeldb ~/.lnd/data/graph/mainnet/channel.db 26 | ``` 27 | 28 | ### Options 29 | 30 | ``` 31 | --channeldb string lnd channel.db file to drop zombies from 32 | -h, --help help for dropgraphzombies 33 | ``` 34 | 35 | ### Options inherited from parent commands 36 | 37 | ``` 38 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 39 | -r, --regtest Indicates if regtest parameters should be used 40 | --resultsdir string Directory where results should be stored (default "./results") 41 | -s, --signet Indicates if the public signet parameters should be used 42 | -t, --testnet Indicates if testnet parameters should be used 43 | ``` 44 | 45 | ### SEE ALSO 46 | 47 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 48 | 49 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Start with a Golang builder image. 2 | FROM golang:1.24.10-alpine3.22@sha256:12c199a889439928e36df7b4c5031c18bfdad0d33cdeae5dd35b2de369b5fbf5 AS golangbuilder 3 | 4 | # Pass a tag, branch or a commit using build-arg. This allows a docker image to 5 | # be built from a specified Git state. 6 | ARG checkout="master" 7 | 8 | # Install dependencies and install/build chantools. 9 | RUN apk add --no-cache --update alpine-sdk make \ 10 | && git clone https://github.com/lightninglabs/chantools /go/src/github.com/lightninglabs/chantools \ 11 | && cd /go/src/github.com/lightninglabs/chantools \ 12 | && git checkout $checkout \ 13 | && make install 14 | 15 | # Start a new, final image to reduce size. 16 | FROM alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 AS final 17 | 18 | # Define a root volume for data persistence. 19 | VOLUME /chantools 20 | WORKDIR /chantools 21 | 22 | # We'll use the default / directory as the home directory, since the /chantools 23 | # folder will be overwritten if a volume is mounted there. 24 | ENV HOME=/ 25 | 26 | # We'll expect the lnd data directory to be mounted here. 27 | VOLUME /lnd 28 | 29 | # Copy the binaries and entrypoint from the builder image. 30 | COPY ./docker/docker-entrypoint.sh /bin/ 31 | COPY ./docker/bash-wrapper.sh /usr/local/bin/bash 32 | COPY --from=golangbuilder /go/bin/chantools /bin/ 33 | 34 | # Make the wrapper executable. 35 | RUN chmod 0777 /usr/local/bin/bash 36 | 37 | # Add bash. 38 | RUN apk add --no-cache \ 39 | bash \ 40 | jq \ 41 | ca-certificates 42 | 43 | # We'll want to just start a shell, but also give the user some info on how to 44 | # use this image, which we do with a shell script. 45 | ENTRYPOINT ["/bin/docker-entrypoint.sh"] 46 | -------------------------------------------------------------------------------- /doc/chantools_dumpbackup.md: -------------------------------------------------------------------------------- 1 | ## chantools dumpbackup 2 | 3 | Dump the content of a channel.backup file 4 | 5 | ### Synopsis 6 | 7 | This command dumps all information that is inside a 8 | channel.backup file in a human readable format. 9 | 10 | ``` 11 | chantools dumpbackup [flags] 12 | ``` 13 | 14 | ### Examples 15 | 16 | ``` 17 | chantools dumpbackup \ 18 | --multi_file ~/.lnd/data/chain/bitcoin/mainnet/channel.backup 19 | ``` 20 | 21 | ### Options 22 | 23 | ``` 24 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 25 | -h, --help help for dumpbackup 26 | --multi_file string lnd channel.backup file to dump 27 | --rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed 28 | --walletdb string read the seed/master root key to use for decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 29 | ``` 30 | 31 | ### Options inherited from parent commands 32 | 33 | ``` 34 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 35 | -r, --regtest Indicates if regtest parameters should be used 36 | --resultsdir string Directory where results should be stored (default "./results") 37 | -s, --signet Indicates if the public signet parameters should be used 38 | -t, --testnet Indicates if testnet parameters should be used 39 | ``` 40 | 41 | ### SEE ALSO 42 | 43 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 44 | 45 | -------------------------------------------------------------------------------- /doc/chantools_chanbackup.md: -------------------------------------------------------------------------------- 1 | ## chantools chanbackup 2 | 3 | Create a channel.backup file from a channel database 4 | 5 | ### Synopsis 6 | 7 | This command creates a new channel.backup from a 8 | channel.db file. 9 | 10 | ``` 11 | chantools chanbackup [flags] 12 | ``` 13 | 14 | ### Examples 15 | 16 | ``` 17 | chantools chanbackup \ 18 | --channeldb ~/.lnd/data/graph/mainnet/channel.db \ 19 | --multi_file new_channel_backup.backup 20 | ``` 21 | 22 | ### Options 23 | 24 | ``` 25 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 26 | --channeldb string lnd channel.db file to create the backup from 27 | -h, --help help for chanbackup 28 | --multi_file string lnd channel.backup file to create 29 | --rootkey string BIP32 HD root key of the wallet to use for creating the backup; leave empty to prompt for lnd 24 word aezeed 30 | --walletdb string read the seed/master root key to use for creating the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 31 | ``` 32 | 33 | ### Options inherited from parent commands 34 | 35 | ``` 36 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 37 | -r, --regtest Indicates if regtest parameters should be used 38 | --resultsdir string Directory where results should be stored (default "./results") 39 | -s, --signet Indicates if the public signet parameters should be used 40 | -t, --testnet Indicates if testnet parameters should be used 41 | ``` 42 | 43 | ### SEE ALSO 44 | 45 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 46 | 47 | -------------------------------------------------------------------------------- /cmd/chantools/deletepayments.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/lightninglabs/chantools/lnd" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type deletePaymentsCommand struct { 12 | ChannelDB string 13 | FailedOnly bool 14 | 15 | cmd *cobra.Command 16 | } 17 | 18 | func newDeletePaymentsCommand() *cobra.Command { 19 | cc := &deletePaymentsCommand{} 20 | cc.cmd = &cobra.Command{ 21 | Use: "deletepayments", 22 | Short: "Remove all (failed) payments from a channel DB", 23 | Long: `This command removes all payments from a channel DB. 24 | If only the failed payments should be deleted (and not the successful ones), the 25 | --failedonly flag can be specified. 26 | 27 | CAUTION: Running this command will make it impossible to use the channel DB 28 | with an older version of lnd. Downgrading is not possible and you'll need to 29 | run lnd ` + lndVersion + ` or later after using this command!'`, 30 | Example: `chantools deletepayments --failedonly \ 31 | --channeldb ~/.lnd/data/graph/mainnet/channel.db`, 32 | RunE: cc.Execute, 33 | } 34 | cc.cmd.Flags().StringVar( 35 | &cc.ChannelDB, "channeldb", "", "lnd channel.db file to dump "+ 36 | "channels from", 37 | ) 38 | cc.cmd.Flags().BoolVar( 39 | &cc.FailedOnly, "failedonly", false, "don't delete all "+ 40 | "payments, only failed ones", 41 | ) 42 | 43 | return cc.cmd 44 | } 45 | 46 | func (c *deletePaymentsCommand) Execute(_ *cobra.Command, _ []string) error { 47 | // Check that we have a channel DB. 48 | if c.ChannelDB == "" { 49 | return errors.New("channel DB is required") 50 | } 51 | db, _, err := lnd.OpenDB(c.ChannelDB, false) 52 | if err != nil { 53 | return fmt.Errorf("error opening rescue DB: %w", err) 54 | } 55 | defer func() { _ = db.Close() }() 56 | 57 | _, err = db.DeletePayments(c.FailedOnly, false) 58 | 59 | return err 60 | } 61 | -------------------------------------------------------------------------------- /doc/chantools_removechannel.md: -------------------------------------------------------------------------------- 1 | ## chantools removechannel 2 | 3 | Remove a single channel from the given channel DB 4 | 5 | ### Synopsis 6 | 7 | Opens the given channel DB in write mode and removes one 8 | single channel from it. This means giving up on any state (and therefore coins) 9 | of that channel and should only be used if the funding transaction of the 10 | channel was never confirmed on chain! 11 | 12 | CAUTION: Running this command will make it impossible to use the channel DB 13 | with an older version of lnd. Downgrading is not possible and you'll need to 14 | run lnd v0.19.0-beta or later after using this command! 15 | 16 | ``` 17 | chantools removechannel [flags] 18 | ``` 19 | 20 | ### Examples 21 | 22 | ``` 23 | chantools removechannel \ 24 | --channeldb ~/.lnd/data/graph/mainnet/channel.db \ 25 | --channel 3149764effbe82718b280de425277e5e7b245a4573aa4a0203ac12cee1c37816:0 26 | ``` 27 | 28 | ### Options 29 | 30 | ``` 31 | --channel string channel to remove from the DB file, identified by its channel point (:) 32 | --channeldb string lnd channel.backup file to remove the channel from 33 | -h, --help help for removechannel 34 | ``` 35 | 36 | ### Options inherited from parent commands 37 | 38 | ``` 39 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 40 | -r, --regtest Indicates if regtest parameters should be used 41 | --resultsdir string Directory where results should be stored (default "./results") 42 | -s, --signet Indicates if the public signet parameters should be used 43 | -t, --testnet Indicates if testnet parameters should be used 44 | ``` 45 | 46 | ### SEE ALSO 47 | 48 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 49 | 50 | -------------------------------------------------------------------------------- /doc/chantools_zombierecovery_findmatches.md: -------------------------------------------------------------------------------- 1 | ## chantools zombierecovery findmatches 2 | 3 | [0/3] Matchmaker only: Find matches between registered nodes 4 | 5 | ### Synopsis 6 | 7 | Matchmaker only: Runs through all the nodes that have 8 | registered their ID on https://www.node-recovery.com and checks whether there 9 | are any matches of channels between them by looking at the whole channel graph. 10 | 11 | This command will be run by guggero and the result will be sent to the 12 | registered nodes. 13 | 14 | ``` 15 | chantools zombierecovery findmatches [flags] 16 | ``` 17 | 18 | ### Examples 19 | 20 | ``` 21 | chantools zombierecovery findmatches \ 22 | --registrations data.txt \ 23 | --ambosskey 24 | ``` 25 | 26 | ### Options 27 | 28 | ``` 29 | --ambossdelay duration the delay between each query to the Amboss GraphQL API (default 4s) 30 | --ambosskey string the API key for the Amboss GraphQL API 31 | --apiurl string API URL to use (must be esplora compatible) (default "https://api.node-recovery.com") 32 | -h, --help help for findmatches 33 | --registrations string the raw data.txt where the registrations are stored in 34 | ``` 35 | 36 | ### Options inherited from parent commands 37 | 38 | ``` 39 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 40 | -r, --regtest Indicates if regtest parameters should be used 41 | --resultsdir string Directory where results should be stored (default "./results") 42 | -s, --signet Indicates if the public signet parameters should be used 43 | -t, --testnet Indicates if testnet parameters should be used 44 | ``` 45 | 46 | ### SEE ALSO 47 | 48 | * [chantools zombierecovery](chantools_zombierecovery.md) - Try rescuing funds stuck in channels with zombie nodes 49 | 50 | -------------------------------------------------------------------------------- /doc/chantools_vanitygen.md: -------------------------------------------------------------------------------- 1 | ## chantools vanitygen 2 | 3 | Generate a seed with a custom lnd node identity public key that starts with the given prefix 4 | 5 | ### Synopsis 6 | 7 | Try random lnd compatible seeds until one is found that 8 | produces a node identity public key that starts with the given prefix. 9 | 10 | Example output: 11 | 12 |
13 | Running vanitygen on 8 threads. Prefix bit length is 17, expecting to approach
14 | probability p=1.0 after 131,072 seeds.
15 | Tested 185k seeds, p=1.41296, speed=14k/s, elapsed=13s                          
16 | Looking for 022222, found pubkey: 022222f015540ddde9bdf7c95b24f1d44f7ea6ab69bec83d6fbe622296d64b51d6
17 | with seed: [ability roast pear stomach wink cable tube trumpet shy caught hunt
18 | someone border organ spoon only prepare calm silent million tobacco chaos normal
19 | phone]
20 | 
21 | 22 | 23 | ``` 24 | chantools vanitygen [flags] 25 | ``` 26 | 27 | ### Examples 28 | 29 | ``` 30 | chantools vanitygen --prefix 022222 --threads 8 31 | ``` 32 | 33 | ### Options 34 | 35 | ``` 36 | -h, --help help for vanitygen 37 | --prefix string hex encoded prefix to find in node public key 38 | --threads uint8 number of parallel threads (default 4) 39 | ``` 40 | 41 | ### Options inherited from parent commands 42 | 43 | ``` 44 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 45 | -r, --regtest Indicates if regtest parameters should be used 46 | --resultsdir string Directory where results should be stored (default "./results") 47 | -s, --signet Indicates if the public signet parameters should be used 48 | -t, --testnet Indicates if testnet parameters should be used 49 | ``` 50 | 51 | ### SEE ALSO 52 | 53 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 54 | 55 | -------------------------------------------------------------------------------- /cmd/chantools/rescuetweakedkey_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/btcsuite/btcd/btcec/v2" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | var ( 12 | privKeyBytes, _ = hex.DecodeString( 13 | "571e2fc5e99f91596f7561da9f605cbf2e2342a166593eef041862b6a8b7" + 14 | "4f35", 15 | ) 16 | pubKeyOrigBytes, _ = hex.DecodeString( 17 | "032ec305fb12642fd3b1091d1cba88ebb7b1a8dbc256b35789b7e223a1b3" + 18 | "75f0b7", 19 | ) 20 | pubKeyNegBytes, _ = hex.DecodeString( 21 | "022ec305fb12642fd3b1091d1cba88ebb7b1a8dbc256b35789b7e223a1b3" + 22 | "75f0b7", 23 | ) 24 | pubKeyNegTweakBytes, _ = hex.DecodeString( 25 | "0322b5c94ec4dc3a8843edc7448a0aad389d43e0f8d1b35b546dd1aad70f" + 26 | "b2c45b", 27 | ) 28 | pubKeyNegTweakTweakBytes, _ = hex.DecodeString( 29 | "03f4cd1ff9efa8198e33e5a110dc690c1472d56c01287893c2f8ed55f61e" + 30 | "a767d1", 31 | ) 32 | ) 33 | 34 | func TestTweak(t *testing.T) { 35 | privKey, pubKey := btcec.PrivKeyFromBytes(privKeyBytes) 36 | require.Equal(t, pubKeyOrigBytes, pubKey.SerializeCompressed()) 37 | 38 | privKeyCopy := copyPrivKey(privKey) 39 | require.Equal(t, privKey, privKeyCopy) 40 | 41 | mutateWithSign(privKeyCopy) 42 | require.NotEqual(t, privKey, privKeyCopy) 43 | require.Equalf( 44 | t, pubKeyNegBytes, privKeyCopy.PubKey().SerializeCompressed(), 45 | "%x", privKeyCopy.PubKey().SerializeCompressed(), 46 | ) 47 | 48 | mutateWithTweak(privKeyCopy) 49 | require.NotEqual(t, privKey, privKeyCopy) 50 | require.Equalf( 51 | t, pubKeyNegTweakBytes, 52 | privKeyCopy.PubKey().SerializeCompressed(), 53 | "%x", privKeyCopy.PubKey().SerializeCompressed(), 54 | ) 55 | 56 | mutateWithTweak(privKeyCopy) 57 | require.NotEqual(t, privKey, privKeyCopy) 58 | require.Equalf( 59 | t, pubKeyNegTweakTweakBytes, 60 | privKeyCopy.PubKey().SerializeCompressed(), 61 | "%x", privKeyCopy.PubKey().SerializeCompressed(), 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /doc/chantools_createwallet.md: -------------------------------------------------------------------------------- 1 | ## chantools createwallet 2 | 3 | Create a new lnd compatible wallet.db file from an existing seed or by generating a new one 4 | 5 | ### Synopsis 6 | 7 | Creates a new wallet that can be used with lnd or with 8 | chantools. The wallet can be created from an existing seed or a new one can be 9 | generated (use --generateseed). 10 | 11 | ``` 12 | chantools createwallet [flags] 13 | ``` 14 | 15 | ### Examples 16 | 17 | ``` 18 | chantools createwallet \ 19 | --walletdbdir ~/.lnd/data/chain/bitcoin/mainnet 20 | ``` 21 | 22 | ### Options 23 | 24 | ``` 25 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 26 | --generateseed generate a new seed instead of using an existing one 27 | -h, --help help for createwallet 28 | --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 29 | --walletdb string read the seed/master root key to use for creating the new wallet from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 30 | --walletdbdir string the folder to create the new wallet.db file in 31 | ``` 32 | 33 | ### Options inherited from parent commands 34 | 35 | ``` 36 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 37 | -r, --regtest Indicates if regtest parameters should be used 38 | --resultsdir string Directory where results should be stored (default "./results") 39 | -s, --signet Indicates if the public signet parameters should be used 40 | -t, --testnet Indicates if testnet parameters should be used 41 | ``` 42 | 43 | ### SEE ALSO 44 | 45 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 46 | 47 | -------------------------------------------------------------------------------- /doc/chantools_zombierecovery.md: -------------------------------------------------------------------------------- 1 | ## chantools zombierecovery 2 | 3 | Try rescuing funds stuck in channels with zombie nodes 4 | 5 | ### Synopsis 6 | 7 | A sub command that hosts a set of further sub commands 8 | to help with recovering funds stuck in zombie channels. 9 | 10 | Please visit https://github.com/lightninglabs/chantools/blob/master/doc/zombierecovery.md 11 | for more information on how to use these commands. 12 | 13 | Check out https://guggero.github.io/chantools/doc/command-generator.html for an 14 | interactive GUI that guides you through the different steps. 15 | 16 | 17 | ``` 18 | chantools zombierecovery [flags] 19 | ``` 20 | 21 | ### Options 22 | 23 | ``` 24 | -h, --help help for zombierecovery 25 | ``` 26 | 27 | ### Options inherited from parent commands 28 | 29 | ``` 30 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 31 | -r, --regtest Indicates if regtest parameters should be used 32 | --resultsdir string Directory where results should be stored (default "./results") 33 | -s, --signet Indicates if the public signet parameters should be used 34 | -t, --testnet Indicates if testnet parameters should be used 35 | ``` 36 | 37 | ### SEE ALSO 38 | 39 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 40 | * [chantools zombierecovery findmatches](chantools_zombierecovery_findmatches.md) - [0/3] Matchmaker only: Find matches between registered nodes 41 | * [chantools zombierecovery makeoffer](chantools_zombierecovery_makeoffer.md) - [2/3] Make an offer on how to split the funds to recover 42 | * [chantools zombierecovery preparekeys](chantools_zombierecovery_preparekeys.md) - [1/3] Prepare all public keys for a recovery attempt 43 | * [chantools zombierecovery signoffer](chantools_zombierecovery_signoffer.md) - [3/3] Sign an offer sent by the remote peer to recover funds 44 | 45 | -------------------------------------------------------------------------------- /doc/chantools_filterbackup.md: -------------------------------------------------------------------------------- 1 | ## chantools filterbackup 2 | 3 | Filter an lnd channel.backup file and remove certain channels 4 | 5 | ### Synopsis 6 | 7 | Filter an lnd channel.backup file by removing certain 8 | channels (identified by their funding transaction outpoints). 9 | 10 | ``` 11 | chantools filterbackup [flags] 12 | ``` 13 | 14 | ### Examples 15 | 16 | ``` 17 | chantools filterbackup \ 18 | --multi_file ~/.lnd/data/chain/bitcoin/mainnet/channel.backup \ 19 | --discard 2abcdef2b2bffaaa...db0abadd:1,4abcdef2b2bffaaa...db8abadd:0 20 | ``` 21 | 22 | ### Options 23 | 24 | ``` 25 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 26 | --discard string comma separated list of channel funding outpoints (format :) to remove from the backup file 27 | -h, --help help for filterbackup 28 | --multi_file string lnd channel.backup file to filter 29 | --rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed 30 | --walletdb string read the seed/master root key to use for decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 31 | ``` 32 | 33 | ### Options inherited from parent commands 34 | 35 | ``` 36 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 37 | -r, --regtest Indicates if regtest parameters should be used 38 | --resultsdir string Directory where results should be stored (default "./results") 39 | -s, --signet Indicates if the public signet parameters should be used 40 | -t, --testnet Indicates if testnet parameters should be used 41 | ``` 42 | 43 | ### SEE ALSO 44 | 45 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 46 | 47 | -------------------------------------------------------------------------------- /doc/chantools_signpsbt.md: -------------------------------------------------------------------------------- 1 | ## chantools signpsbt 2 | 3 | Sign a Partially Signed Bitcoin Transaction (PSBT) 4 | 5 | ### Synopsis 6 | 7 | Sign a PSBT with a master root key. The PSBT must contain 8 | an input that is owned by the master root key. 9 | 10 | ``` 11 | chantools signpsbt [flags] 12 | ``` 13 | 14 | ### Examples 15 | 16 | ``` 17 | chantools signpsbt \ 18 | --psbt 19 | 20 | chantools signpsbt --fromrawpsbtfile 21 | ``` 22 | 23 | ### Options 24 | 25 | ``` 26 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 27 | --fromrawpsbtfile string the file containing the raw, binary encoded PSBT packet to sign 28 | -h, --help help for signpsbt 29 | --psbt string Partially Signed Bitcoin Transaction to sign 30 | --rootkey string BIP32 HD root key of the wallet to use for signing the PSBT; leave empty to prompt for lnd 24 word aezeed 31 | --torawpsbtfile string the file to write the resulting signed raw, binary encoded PSBT packet to 32 | --walletdb string read the seed/master root key to use for signing the PSBT from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 33 | ``` 34 | 35 | ### Options inherited from parent commands 36 | 37 | ``` 38 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 39 | -r, --regtest Indicates if regtest parameters should be used 40 | --resultsdir string Directory where results should be stored (default "./results") 41 | -s, --signet Indicates if the public signet parameters should be used 42 | -t, --testnet Indicates if testnet parameters should be used 43 | ``` 44 | 45 | ### SEE ALSO 46 | 47 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 48 | 49 | -------------------------------------------------------------------------------- /doc/chantools_fixoldbackup.md: -------------------------------------------------------------------------------- 1 | ## chantools fixoldbackup 2 | 3 | Fixes an old channel.backup file that is affected by the lnd issue #3881 (unable to derive shachain root key) 4 | 5 | ### Synopsis 6 | 7 | Fixes an old channel.backup file that is affected by the 8 | lnd issue [#3881](https://github.com/lightningnetwork/lnd/issues/3881) 9 | ([lncli] unable to restore chan backups: rpc error: code = Unknown desc = 10 | unable to unpack chan backup: unable to derive shachain root key: unable to 11 | derive private key). 12 | 13 | ``` 14 | chantools fixoldbackup [flags] 15 | ``` 16 | 17 | ### Examples 18 | 19 | ``` 20 | chantools fixoldbackup \ 21 | --multi_file ~/.lnd/data/chain/bitcoin/mainnet/channel.backup 22 | ``` 23 | 24 | ### Options 25 | 26 | ``` 27 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 28 | -h, --help help for fixoldbackup 29 | --multi_file string lnd channel.backup file to fix 30 | --rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed 31 | --walletdb string read the seed/master root key to use for decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 32 | ``` 33 | 34 | ### Options inherited from parent commands 35 | 36 | ``` 37 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 38 | -r, --regtest Indicates if regtest parameters should be used 39 | --resultsdir string Directory where results should be stored (default "./results") 40 | -s, --signet Indicates if the public signet parameters should be used 41 | -t, --testnet Indicates if testnet parameters should be used 42 | ``` 43 | 44 | ### SEE ALSO 45 | 46 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 47 | 48 | -------------------------------------------------------------------------------- /cmd/chantools/chanbackup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/lightninglabs/chantools/lnd" 8 | "github.com/lightningnetwork/lnd/chanbackup" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | type chanBackupCommand struct { 13 | ChannelDB string 14 | MultiFile string 15 | 16 | rootKey *rootKey 17 | cmd *cobra.Command 18 | } 19 | 20 | func newChanBackupCommand() *cobra.Command { 21 | cc := &chanBackupCommand{} 22 | cc.cmd = &cobra.Command{ 23 | Use: "chanbackup", 24 | Short: "Create a channel.backup file from a channel database", 25 | Long: `This command creates a new channel.backup from a 26 | channel.db file.`, 27 | Example: `chantools chanbackup \ 28 | --channeldb ~/.lnd/data/graph/mainnet/channel.db \ 29 | --multi_file new_channel_backup.backup`, 30 | RunE: cc.Execute, 31 | } 32 | cc.cmd.Flags().StringVar( 33 | &cc.ChannelDB, "channeldb", "", "lnd channel.db file to "+ 34 | "create the backup from", 35 | ) 36 | cc.cmd.Flags().StringVar( 37 | &cc.MultiFile, "multi_file", "", "lnd channel.backup file to "+ 38 | "create", 39 | ) 40 | 41 | cc.rootKey = newRootKey(cc.cmd, "creating the backup") 42 | 43 | return cc.cmd 44 | } 45 | 46 | func (c *chanBackupCommand) Execute(_ *cobra.Command, _ []string) error { 47 | extendedKey, err := c.rootKey.read() 48 | if err != nil { 49 | return fmt.Errorf("error reading root key: %w", err) 50 | } 51 | 52 | // Check that we have a backup file. 53 | if c.MultiFile == "" { 54 | return errors.New("backup file is required") 55 | } 56 | 57 | // Check that we have a channel DB. 58 | if c.ChannelDB == "" { 59 | return errors.New("channel DB is required") 60 | } 61 | db, _, err := lnd.OpenDB(c.ChannelDB, true) 62 | if err != nil { 63 | return fmt.Errorf("error opening rescue DB: %w", err) 64 | } 65 | multiFile := chanbackup.NewMultiFile(c.MultiFile, noBackupArchive) 66 | keyRing := &lnd.HDKeyRing{ 67 | ExtendedKey: extendedKey, 68 | ChainParams: chainParams, 69 | } 70 | return lnd.CreateChannelBackup(db, multiFile, keyRing) 71 | } 72 | -------------------------------------------------------------------------------- /doc/chantools_rescuetweakedkey.md: -------------------------------------------------------------------------------- 1 | ## chantools rescuetweakedkey 2 | 3 | Attempt to rescue funds locked in an address with a key that was affected by a specific bug in lnd 4 | 5 | ### Synopsis 6 | 7 | There very likely is no reason to run this command 8 | unless you exactly know why or were told by the author of this tool to use it. 9 | 10 | 11 | ``` 12 | chantools rescuetweakedkey [flags] 13 | ``` 14 | 15 | ### Examples 16 | 17 | ``` 18 | chantools rescuetweakedkey \ 19 | --path "m/1017'/0'/5'/0/0'" \ 20 | --targetaddr bc1pxxxxxxx 21 | ``` 22 | 23 | ### Options 24 | 25 | ``` 26 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 27 | -h, --help help for rescuetweakedkey 28 | --numtries uint the number of mutations to try (default 10000000) 29 | --path string BIP32 derivation path to derive the starting key from; must start with "m/" 30 | --rootkey string BIP32 HD root key of the wallet to use for deriving starting key; leave empty to prompt for lnd 24 word aezeed 31 | --targetaddr string address the funds are locked in 32 | --walletdb string read the seed/master root key to use for deriving starting key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 33 | ``` 34 | 35 | ### Options inherited from parent commands 36 | 37 | ``` 38 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 39 | -r, --regtest Indicates if regtest parameters should be used 40 | --resultsdir string Directory where results should be stored (default "./results") 41 | -s, --signet Indicates if the public signet parameters should be used 42 | -t, --testnet Indicates if testnet parameters should be used 43 | ``` 44 | 45 | ### SEE ALSO 46 | 47 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 48 | 49 | -------------------------------------------------------------------------------- /doc/chantools_walletinfo.md: -------------------------------------------------------------------------------- 1 | ## chantools walletinfo 2 | 3 | Shows info about an lnd wallet.db file and optionally extracts the BIP32 HD root key 4 | 5 | ### Synopsis 6 | 7 | Shows some basic information about an lnd wallet.db file, 8 | like the node identity the wallet belongs to, how many on-chain addresses are 9 | used and, if enabled with --withrootkey the BIP32 HD root key of the wallet. The 10 | latter can be useful to recover funds from a wallet if the wallet password is 11 | still known but the seed was lost. **The 24 word seed phrase itself cannot be 12 | extracted** because it is hashed into the extended HD root key before storing it 13 | in the wallet.db. 14 | In case lnd was started with "--noseedbackup=true" your wallet has the default 15 | password. To unlock the wallet set the environment variable WALLET_PASSWORD="-" 16 | or simply press without entering a password when being prompted. 17 | 18 | ``` 19 | chantools walletinfo [flags] 20 | ``` 21 | 22 | ### Examples 23 | 24 | ``` 25 | chantools walletinfo --withrootkey \ 26 | --walletdb ~/.lnd/data/chain/bitcoin/mainnet/wallet.db 27 | ``` 28 | 29 | ### Options 30 | 31 | ``` 32 | --dumpaddrs print all addresses, including private keys 33 | -h, --help help for walletinfo 34 | --walletdb string lnd wallet.db file to dump the contents from 35 | --withrootkey print BIP32 HD root key of wallet to standard out 36 | ``` 37 | 38 | ### Options inherited from parent commands 39 | 40 | ``` 41 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 42 | -r, --regtest Indicates if regtest parameters should be used 43 | --resultsdir string Directory where results should be stored (default "./results") 44 | -s, --signet Indicates if the public signet parameters should be used 45 | -t, --testnet Indicates if testnet parameters should be used 46 | ``` 47 | 48 | ### SEE ALSO 49 | 50 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 51 | 52 | -------------------------------------------------------------------------------- /doc/chantools_derivekey.md: -------------------------------------------------------------------------------- 1 | ## chantools derivekey 2 | 3 | Derive a key with a specific derivation path 4 | 5 | ### Synopsis 6 | 7 | This command derives a single key with the given BIP32 8 | derivation path from the root key and prints it to the console. 9 | 10 | ``` 11 | chantools derivekey [flags] 12 | ``` 13 | 14 | ### Examples 15 | 16 | ``` 17 | chantools derivekey --path "m/1017'/0'/5'/0/0'" \ 18 | --neuter 19 | 20 | chantools derivekey --identity 21 | ``` 22 | 23 | ### Options 24 | 25 | ``` 26 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 27 | -h, --help help for derivekey 28 | --hsm_secret string the hex encoded HSM secret to use for deriving the multisig keys for a CLN node; obtain by running 'xxd -p -c32 ~/.lightning/bitcoin/hsm_secret' 29 | --identity derive the node's identity public key 30 | --neuter don't output private key(s), only public key(s) 31 | --path string BIP32 derivation path to derive; must start with "m/" 32 | --rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed 33 | --walletdb string read the seed/master root key to use for decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 34 | ``` 35 | 36 | ### Options inherited from parent commands 37 | 38 | ``` 39 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 40 | -r, --regtest Indicates if regtest parameters should be used 41 | --resultsdir string Directory where results should be stored (default "./results") 42 | -s, --signet Indicates if the public signet parameters should be used 43 | -t, --testnet Indicates if testnet parameters should be used 44 | ``` 45 | 46 | ### SEE ALSO 47 | 48 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 49 | 50 | -------------------------------------------------------------------------------- /doc/command-generator/src/App.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 62 | 63 | 70 | -------------------------------------------------------------------------------- /itest/docker/compose.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This file contains all the common docker-compose related functions. 4 | 5 | # DIR is set to the directory of this script. 6 | DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 7 | 8 | export LOCAL_USER_ID=$(id -u) 9 | export LOCAL_GROUP_ID=$(id -g) 10 | 11 | # The way we call docker-compose depends on the installation. 12 | if which docker-compose > /dev/null; then 13 | COMPOSE_CMD="docker-compose" 14 | else 15 | COMPOSE_CMD="docker compose" 16 | fi 17 | 18 | # Common arguments that we want to pass to docker-compose. 19 | # By default, this only includes the main docker-compose file 20 | # and not the override file. Use the `compose_upgrade` method 21 | # to load both docker compose files. 22 | COMPOSE_ARGS="-f $DIR/docker-compose.yaml -p regtest" 23 | COMPOSE="$COMPOSE_CMD $COMPOSE_ARGS" 24 | 25 | # compose_upgrade sets COMPOSE_ARGS and COMPOSE such that 26 | # both the main docker-compose file and the override file 27 | # are loaded. 28 | function compose_upgrade() { 29 | export COMPOSE_ARGS="-p regtest" 30 | export COMPOSE="$COMPOSE_CMD $COMPOSE_ARGS" 31 | } 32 | 33 | # compose_up starts the docker-compose cluster. 34 | function compose_up() { 35 | echo "🐳 Starting the cluster" 36 | $COMPOSE up -d --quiet-pull 37 | } 38 | 39 | # compose_down tears down the docker-compose cluster 40 | # and removes all volumes and orphans. 41 | function compose_down() { 42 | echo "🐳 Tearing down the cluster" 43 | $COMPOSE down --volumes --remove-orphans 44 | } 45 | 46 | # compose_stop stops a specific service in the cluster. 47 | function compose_stop() { 48 | local service="$1" 49 | echo "🐳 Stopping $service" 50 | $COMPOSE stop "$service" 51 | } 52 | 53 | # compose_start starts a specific service in the cluster. 54 | function compose_start() { 55 | local service="$1" 56 | echo "🐳 Starting $service" 57 | $COMPOSE up -d $service 58 | } 59 | 60 | # compose_rebuild forces the rebuild of the image for a 61 | # specific service in the cluster. 62 | function compose_rebuild() { 63 | local service="$1" 64 | echo "🐳 Rebuilding $service" 65 | $COMPOSE build --no-cache $service 66 | } 67 | -------------------------------------------------------------------------------- /doc/chantools_dropchannelgraph.md: -------------------------------------------------------------------------------- 1 | ## chantools dropchannelgraph 2 | 3 | Remove all graph related data from a channel DB 4 | 5 | ### Synopsis 6 | 7 | This command removes all graph data from a channel DB, 8 | forcing the lnd node to do a full graph sync. 9 | 10 | Or if a single channel is specified, that channel is purged from the graph 11 | without removing any other data. 12 | 13 | CAUTION: Running this command will make it impossible to use the channel DB 14 | with an older version of lnd. Downgrading is not possible and you'll need to 15 | run lnd v0.19.0-beta or later after using this command!' 16 | 17 | ``` 18 | chantools dropchannelgraph [flags] 19 | ``` 20 | 21 | ### Examples 22 | 23 | ``` 24 | chantools dropchannelgraph \ 25 | --channeldb ~/.lnd/data/graph/mainnet/channel.db \ 26 | --node_identity_key 03...... 27 | 28 | chantools dropchannelgraph \ 29 | --channeldb ~/.lnd/data/graph/mainnet/channel.db \ 30 | --single_channel 726607861215512345 31 | --node_identity_key 03...... 32 | ``` 33 | 34 | ### Options 35 | 36 | ``` 37 | --channeldb string lnd channel.db file to drop channels from 38 | --fix_only fix an already empty graph by re-adding the own node's channels 39 | -h, --help help for dropchannelgraph 40 | --node_identity_key string your node's identity public key 41 | --single_channel uint the single channel identified by its short channel ID (CID) to remove from the graph 42 | ``` 43 | 44 | ### Options inherited from parent commands 45 | 46 | ``` 47 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 48 | -r, --regtest Indicates if regtest parameters should be used 49 | --resultsdir string Directory where results should be stored (default "./results") 50 | -s, --signet Indicates if the public signet parameters should be used 51 | -t, --testnet Indicates if testnet parameters should be used 52 | ``` 53 | 54 | ### SEE ALSO 55 | 56 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 57 | 58 | -------------------------------------------------------------------------------- /doc/chantools_signrescuefunding.md: -------------------------------------------------------------------------------- 1 | ## chantools signrescuefunding 2 | 3 | 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 4 | 5 | ### Synopsis 6 | 7 | This is part 2 of a two phase process to rescue a channel 8 | funding output that was created on chain by accident but never resulted in a 9 | proper channel and no commitment transactions exist to spend the funds locked in 10 | the 2-of-2 multisig. 11 | 12 | If successful, this will create a final on-chain transaction that can be 13 | broadcast by any Bitcoin node. 14 | 15 | ``` 16 | chantools signrescuefunding [flags] 17 | ``` 18 | 19 | ### Examples 20 | 21 | ``` 22 | chantools signrescuefunding \ 23 | --psbt 24 | ``` 25 | 26 | ### Options 27 | 28 | ``` 29 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 30 | -h, --help help for signrescuefunding 31 | --psbt string Partially Signed Bitcoin Transaction that was provided by the initiator of the channel to rescue 32 | --rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed 33 | --walletdb string read the seed/master root key to use for deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 34 | ``` 35 | 36 | ### Options inherited from parent commands 37 | 38 | ``` 39 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 40 | -r, --regtest Indicates if regtest parameters should be used 41 | --resultsdir string Directory where results should be stored (default "./results") 42 | -s, --signet Indicates if the public signet parameters should be used 43 | -t, --testnet Indicates if testnet parameters should be used 44 | ``` 45 | 46 | ### SEE ALSO 47 | 48 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 49 | 50 | -------------------------------------------------------------------------------- /doc/chantools_summary.md: -------------------------------------------------------------------------------- 1 | ## chantools summary 2 | 3 | Compile a summary about the current state of channels 4 | 5 | ### Synopsis 6 | 7 | From a list of channels, find out what their state is by 8 | querying the funding transaction on a block explorer API. 9 | 10 | ``` 11 | chantools summary [flags] 12 | ``` 13 | 14 | ### Examples 15 | 16 | ``` 17 | lncli listchannels | chantools summary --listchannels - 18 | 19 | chantools summary --fromchanneldb ~/.lnd/data/graph/mainnet/channel.db 20 | ``` 21 | 22 | ### Options 23 | 24 | ``` 25 | --ancient Create summary of ancient channel closes with un-swept outputs 26 | --ancientstats string Create summary of ancient channel closes with un-swept outputs and print stats for the given list of channels 27 | --apiurl string API URL to use (must be esplora compatible) (default "https://api.node-recovery.com") 28 | --fromchanneldb string channel input is in the format of an lnd channel.db file 29 | --fromchanneldump string channel input is in the format of a channel dump file 30 | --fromsummary string channel input is in the format of chantool's channel summary; specify '-' to read from stdin 31 | -h, --help help for summary 32 | --listchannels string channel input is in the format of lncli's listchannels format; specify '-' to read from stdin 33 | --pendingchannels string channel input is in the format of lncli's pendingchannels format; specify '-' to read from stdin 34 | ``` 35 | 36 | ### Options inherited from parent commands 37 | 38 | ``` 39 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 40 | -r, --regtest Indicates if regtest parameters should be used 41 | --resultsdir string Directory where results should be stored (default "./results") 42 | -s, --signet Indicates if the public signet parameters should be used 43 | -t, --testnet Indicates if testnet parameters should be used 44 | ``` 45 | 46 | ### SEE ALSO 47 | 48 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 49 | 50 | -------------------------------------------------------------------------------- /lnd/chanbackup.go: -------------------------------------------------------------------------------- 1 | package lnd 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/btcsuite/btcd/btcutil/hdkeychain" 8 | "github.com/btcsuite/btcd/chaincfg" 9 | "github.com/lightninglabs/chantools/dump" 10 | "github.com/lightningnetwork/lnd/chanbackup" 11 | "github.com/lightningnetwork/lnd/channeldb" 12 | "github.com/lightningnetwork/lnd/keychain" 13 | ) 14 | 15 | // CreateChannelBackup creates a channel backup file from all channels found in 16 | // the given DB file, encrypted with the key in the key ring. 17 | func CreateChannelBackup(db *channeldb.DB, multiFile *chanbackup.MultiFile, 18 | ring keychain.KeyRing) error { 19 | 20 | singles, err := chanbackup.FetchStaticChanBackups( 21 | db.ChannelStateDB(), db, 22 | ) 23 | if err != nil { 24 | return fmt.Errorf("error extracting channel backup: %w", err) 25 | } 26 | multi := &chanbackup.Multi{ 27 | Version: chanbackup.DefaultMultiVersion, 28 | StaticBackups: singles, 29 | } 30 | var b bytes.Buffer 31 | err = multi.PackToWriter(&b, ring) 32 | if err != nil { 33 | return fmt.Errorf("unable to pack backup: %w", err) 34 | } 35 | err = multiFile.UpdateAndSwap(b.Bytes()) 36 | if err != nil { 37 | return fmt.Errorf("unable to write backup file: %w", err) 38 | } 39 | return nil 40 | } 41 | 42 | // ExtractChannel extracts a single channel from the given backup file and 43 | // returns it as a dump.BackupSingle struct. 44 | func ExtractChannel(extendedKey *hdkeychain.ExtendedKey, 45 | chainParams *chaincfg.Params, multiFilePath, 46 | channelPoint string) (*dump.BackupSingle, error) { 47 | 48 | const noBackupArchive = false 49 | multiFile := chanbackup.NewMultiFile(multiFilePath, noBackupArchive) 50 | keyRing := &HDKeyRing{ 51 | ExtendedKey: extendedKey, 52 | ChainParams: chainParams, 53 | } 54 | 55 | multi, err := multiFile.ExtractMulti(keyRing) 56 | if err != nil { 57 | return nil, fmt.Errorf("could not extract multi file: %w", err) 58 | } 59 | 60 | channels := dump.BackupDump(multi, chainParams) 61 | for _, channel := range channels { 62 | if channel.FundingOutpoint == channelPoint { 63 | return &channel, nil 64 | } 65 | } 66 | 67 | return nil, fmt.Errorf("channel %s not found in backup", channelPoint) 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | concurrency: 12 | # Cancel any previous workflows if they are from a PR or push. 13 | group: ${{ github.event.pull_request.number || github.ref }} 14 | cancel-in-progress: true 15 | 16 | defaults: 17 | run: 18 | shell: bash 19 | 20 | env: 21 | GO_VERSION: 1.24.10 22 | LINT_VERSION: v2.6.2 23 | 24 | jobs: 25 | ######################## 26 | # lint code 27 | ######################## 28 | lint: 29 | name: lint code 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: git checkout 33 | uses: actions/checkout@v5 34 | with: 35 | fetch-depth: 0 36 | 37 | - name: setup go ${{ env.GO_VERSION }} 38 | uses: actions/setup-go@v5 39 | with: 40 | go-version: '${{ env.GO_VERSION }}' 41 | 42 | - name: golangci-lint 43 | uses: golangci/golangci-lint-action@v9 44 | with: 45 | version: '${{ env.LINT_VERSION }}' 46 | 47 | ######################## 48 | # run unit tests 49 | ######################## 50 | unit-test: 51 | name: run unit tests 52 | runs-on: ubuntu-latest 53 | steps: 54 | - name: git checkout 55 | uses: actions/checkout@v5 56 | 57 | - name: setup go ${{ env.GO_VERSION }} 58 | uses: actions/setup-go@v5 59 | with: 60 | go-version: '${{ env.GO_VERSION }}' 61 | 62 | - name: Install chantools 63 | run: make install 64 | 65 | - name: run unit tests 66 | run: make unit 67 | 68 | ######################## 69 | # run integration tests 70 | ######################## 71 | integration-test: 72 | name: run integration tests 73 | runs-on: ubuntu-latest 74 | steps: 75 | - name: git checkout 76 | uses: actions/checkout@v5 77 | 78 | - name: setup go ${{ env.GO_VERSION }} 79 | uses: actions/setup-go@v5 80 | with: 81 | go-version: '${{ env.GO_VERSION }}' 82 | 83 | - name: Install chantools 84 | run: make install 85 | 86 | - name: run integration tests 87 | run: make itest 88 | -------------------------------------------------------------------------------- /btc/descriptors.go: -------------------------------------------------------------------------------- 1 | package btc 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | var ( 8 | inputCharset = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ" + 9 | "&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\\\"\\\\ " 10 | checksumCharset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" 11 | generator = []uint64{ 12 | 0xf5dee51989, 0xa9fdca3312, 0x1bab10e32d, 0x3706b1677a, 13 | 0x644d626ffd, 14 | } 15 | ) 16 | 17 | func descriptorSumPolymod(symbols []uint64) uint64 { 18 | chk := uint64(1) 19 | for _, value := range symbols { 20 | top := chk >> 35 21 | chk = (chk&0x7ffffffff)<<5 ^ value 22 | for i := range 5 { 23 | if (top>>i)&1 != 0 { 24 | chk ^= generator[i] 25 | } 26 | } 27 | } 28 | return chk 29 | } 30 | 31 | func descriptorSumExpand(s string) []uint64 { 32 | groups := []uint64{} 33 | symbols := []uint64{} 34 | for _, c := range s { 35 | v := strings.IndexRune(inputCharset, c) 36 | if v < 0 { 37 | return nil 38 | } 39 | symbols = append(symbols, uint64(v&31)) 40 | groups = append(groups, uint64(v>>5)) 41 | if len(groups) == 3 { 42 | symbols = append( 43 | symbols, groups[0]*9+groups[1]*3+groups[2], 44 | ) 45 | groups = []uint64{} 46 | } 47 | } 48 | if len(groups) == 1 { 49 | symbols = append(symbols, groups[0]) 50 | } else if len(groups) == 2 { 51 | symbols = append(symbols, groups[0]*3+groups[1]) 52 | } 53 | return symbols 54 | } 55 | 56 | func DescriptorSumCreate(s string) string { 57 | symbols := append(descriptorSumExpand(s), 0, 0, 0, 0, 0, 0, 0, 0) 58 | checksum := descriptorSumPolymod(symbols) ^ 1 59 | builder := strings.Builder{} 60 | for i := range 8 { 61 | builder.WriteByte(checksumCharset[(checksum>>(5*(7-i)))&31]) 62 | } 63 | return s + "#" + builder.String() 64 | } 65 | 66 | func DescriptorSumCheck(s string, require bool) bool { 67 | if !strings.Contains(s, "#") { 68 | return !require 69 | } 70 | if s[len(s)-9] != '#' { 71 | return false 72 | } 73 | for _, c := range s[len(s)-8:] { 74 | if !strings.ContainsRune(checksumCharset, c) { 75 | return false 76 | } 77 | } 78 | symbols := append( 79 | descriptorSumExpand(s[:len(s)-9]), 80 | uint64(strings.Index(checksumCharset, s[len(s)-8:])), 81 | ) 82 | return descriptorSumPolymod(symbols) == 1 83 | } 84 | -------------------------------------------------------------------------------- /itest/standalone_test.go: -------------------------------------------------------------------------------- 1 | package itest 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | const ( 10 | testXPriv = "xprv9s21ZrQH143K2ZzuN99NjD7oJruBomAVbNDXzPmhHYcwf8WXCsML" + 11 | "63azyS1rzzfpUsLifeDkM4Q6U9PF9RP7frSGKkMfDTDiiyQjH2PUj2z" 12 | 13 | testMnemonic = "about wolf boost other battle asthma refuse wedding " + 14 | "few purchase track one smooth tunnel immune glass infant " + 15 | "tag manual multiply diagram orient wrist agent" 16 | 17 | testIdentityKey = "03c0da201eedf786d432b6b93fc054355478aee2a97448971c" + 18 | "dfcfa8f13953c58c" 19 | 20 | testHsmSecret = "f85e33c27dc7a87c81ee1f9d8ae15c6d756e53089d75aa6dc480" + 21 | "3d23b4af4b2b" 22 | 23 | testClnIdentityKey = "026672d7c7bce3ffb9cdc7fe42c433dc78f113732d8459e" + 24 | "608ae301409ba1a6f05" 25 | ) 26 | 27 | func TestChantoolsShowRootKeyXPriv(t *testing.T) { 28 | proc := StartChantools(t, "showrootkey", "--rootkey", testXPriv) 29 | defer proc.Wait(t) 30 | 31 | output := proc.ReadAvailableOutput(t, defaultTimeout) 32 | require.Contains(t, output, "Your BIP32 HD root key is: "+testXPriv) 33 | } 34 | 35 | func TestChantoolsShowRootKeyMnemonic(t *testing.T) { 36 | proc := StartChantools(t, "showrootkey") 37 | defer proc.Wait(t) 38 | 39 | mnemonicPrompt := proc.ReadAvailableOutput(t, readTimeout) 40 | require.Contains(t, mnemonicPrompt, "Input your 24-word mnemonic") 41 | proc.WriteInput(t, testMnemonic+"\n") 42 | 43 | passphrasePrompt := proc.ReadAvailableOutput(t, readTimeout) 44 | require.Contains( 45 | t, passphrasePrompt, "Input your cipher seed passphrase", 46 | ) 47 | proc.WriteInput(t, "\n") 48 | 49 | output := proc.ReadAvailableOutput(t, defaultTimeout) 50 | require.Contains(t, output, "Your BIP32 HD root key is: "+testXPriv) 51 | } 52 | 53 | func TestDeriveKey(t *testing.T) { 54 | cmdOutput := invokeCmdDeriveKey( 55 | t, nil, "--identity", "--rootkey", testXPriv, 56 | ) 57 | pubKey := extractRowContent(cmdOutput, rowPublicKey) 58 | require.Equal(t, testIdentityKey, pubKey) 59 | } 60 | 61 | func TestDeriveKeyCln(t *testing.T) { 62 | cmdOutput := invokeCmdDeriveKey( 63 | t, nil, "--identity", "--hsm_secret", testHsmSecret, 64 | ) 65 | pubKey := extractRowContent(cmdOutput, rowPublicKey) 66 | require.Equal(t, testClnIdentityKey, pubKey) 67 | } 68 | -------------------------------------------------------------------------------- /cmd/chantools/dumpbackup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/davecgh/go-spew/spew" 8 | "github.com/lightninglabs/chantools/dump" 9 | "github.com/lightninglabs/chantools/lnd" 10 | "github.com/lightningnetwork/lnd/chanbackup" 11 | "github.com/lightningnetwork/lnd/keychain" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | type dumpBackupCommand struct { 16 | MultiFile string 17 | 18 | rootKey *rootKey 19 | cmd *cobra.Command 20 | } 21 | 22 | func newDumpBackupCommand() *cobra.Command { 23 | cc := &dumpBackupCommand{} 24 | cc.cmd = &cobra.Command{ 25 | Use: "dumpbackup", 26 | Short: "Dump the content of a channel.backup file", 27 | Long: `This command dumps all information that is inside a 28 | channel.backup file in a human readable format.`, 29 | Example: `chantools dumpbackup \ 30 | --multi_file ~/.lnd/data/chain/bitcoin/mainnet/channel.backup`, 31 | RunE: cc.Execute, 32 | } 33 | cc.cmd.Flags().StringVar( 34 | &cc.MultiFile, "multi_file", "", "lnd channel.backup file to "+ 35 | "dump", 36 | ) 37 | 38 | cc.rootKey = newRootKey(cc.cmd, "decrypting the backup") 39 | 40 | return cc.cmd 41 | } 42 | 43 | func (c *dumpBackupCommand) Execute(_ *cobra.Command, _ []string) error { 44 | extendedKey, err := c.rootKey.read() 45 | if err != nil { 46 | return fmt.Errorf("error reading root key: %w", err) 47 | } 48 | 49 | // Check that we have a backup file. 50 | if c.MultiFile == "" { 51 | return errors.New("backup file is required") 52 | } 53 | multiFile := chanbackup.NewMultiFile(c.MultiFile, noBackupArchive) 54 | keyRing := &lnd.HDKeyRing{ 55 | ExtendedKey: extendedKey, 56 | ChainParams: chainParams, 57 | } 58 | return dumpChannelBackup(multiFile, keyRing) 59 | } 60 | 61 | func dumpChannelBackup(multiFile *chanbackup.MultiFile, 62 | ring keychain.KeyRing) error { 63 | 64 | multi, err := multiFile.ExtractMulti(ring) 65 | if err != nil { 66 | return fmt.Errorf("could not extract multi file: %w", err) 67 | } 68 | content := dump.BackupMulti{ 69 | Version: multi.Version, 70 | StaticBackups: dump.BackupDump(multi, chainParams), 71 | } 72 | spew.Dump(content) 73 | 74 | // For the tests, also log as trace level which is disabled by default. 75 | log.Tracef(spew.Sdump(content)) 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /itest/bitcoind_harness.go: -------------------------------------------------------------------------------- 1 | package itest 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/btcsuite/btcd/rpcclient" 9 | "github.com/btcsuite/btcd/txscript" 10 | "github.com/btcsuite/btcd/wire" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func connectBitcoind(t *testing.T) *rpcclient.Client { 15 | t.Helper() 16 | 17 | rpcCfg := rpcclient.ConnConfig{ 18 | Host: "127.0.0.1:18443", 19 | User: "lightning", 20 | Pass: "lightning", 21 | DisableConnectOnNew: true, 22 | DisableAutoReconnect: false, 23 | DisableTLS: true, 24 | HTTPPostMode: true, 25 | } 26 | 27 | client, err := rpcclient.New(&rpcCfg, nil) 28 | require.NoError(t, err) 29 | 30 | return client 31 | } 32 | 33 | func addressOfOutpoint(t *testing.T, client *rpcclient.Client, 34 | op string) string { 35 | 36 | t.Helper() 37 | 38 | channelOp, err := wire.NewOutPointFromString(op) 39 | require.NoError(t, err) 40 | 41 | channelTx, err := client.GetRawTransaction(&channelOp.Hash) 42 | require.NoError(t, err) 43 | 44 | channelScript := channelTx.MsgTx().TxOut[channelOp.Index].PkScript 45 | _, addrs, _, err := txscript.ExtractPkScriptAddrs( 46 | channelScript, &testParams, 47 | ) 48 | require.NoError(t, err) 49 | require.Len(t, addrs, 1) 50 | 51 | return addrs[0].EncodeAddress() 52 | } 53 | 54 | func addrAndOpFromShortChannelID(t *testing.T, client *rpcclient.Client, 55 | shortChanID string) (string, wire.OutPoint) { 56 | 57 | t.Helper() 58 | 59 | parts := strings.Split(shortChanID, "x") 60 | require.Len(t, parts, 3) 61 | 62 | blockHeight, err := strconv.Atoi(parts[0]) 63 | require.NoError(t, err) 64 | txIndex, err := strconv.Atoi(parts[1]) 65 | require.NoError(t, err) 66 | outputIndex, err := strconv.Atoi(parts[2]) 67 | require.NoError(t, err) 68 | 69 | blockHash, err := client.GetBlockHash(int64(blockHeight)) 70 | require.NoError(t, err) 71 | 72 | block, err := client.GetBlock(blockHash) 73 | require.NoError(t, err) 74 | require.Greater(t, len(block.Transactions), outputIndex) 75 | 76 | tx := block.Transactions[txIndex] 77 | op := wire.OutPoint{ 78 | Hash: tx.TxHash(), 79 | Index: uint32(outputIndex), 80 | } 81 | 82 | return addressOfOutpoint(t, client, op.String()), op 83 | } 84 | -------------------------------------------------------------------------------- /cmd/chantools/signmessage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | chantools_lnd "github.com/lightninglabs/chantools/lnd" 8 | "github.com/lightningnetwork/lnd/keychain" 9 | "github.com/spf13/cobra" 10 | "github.com/tv42/zbase32" 11 | ) 12 | 13 | var ( 14 | signedMsgPrefix = []byte("Lightning Signed Message:") 15 | ) 16 | 17 | type signMessageCommand struct { 18 | Msg string 19 | 20 | rootKey *rootKey 21 | cmd *cobra.Command 22 | } 23 | 24 | func newSignMessageCommand() *cobra.Command { 25 | cc := &signMessageCommand{} 26 | cc.cmd = &cobra.Command{ 27 | Use: "signmessage", 28 | Short: "Sign a message with the node's private key.", 29 | Long: `Sign msg with the resident node's private key. 30 | Returns the signature as a zbase32 string.`, 31 | Example: `chantools signmessage --msg=foobar`, 32 | RunE: cc.Execute, 33 | } 34 | cc.cmd.Flags().StringVar( 35 | &cc.Msg, "msg", "", "the message to sign", 36 | ) 37 | 38 | cc.rootKey = newRootKey(cc.cmd, "decrypting the backup") 39 | 40 | return cc.cmd 41 | } 42 | 43 | func (c *signMessageCommand) Execute(_ *cobra.Command, _ []string) error { 44 | if c.Msg == "" { 45 | return errors.New("please enter a valid msg") 46 | } 47 | 48 | extendedKey, err := c.rootKey.read() 49 | if err != nil { 50 | return fmt.Errorf("error reading root key: %w", err) 51 | } 52 | 53 | signer := &chantools_lnd.Signer{ 54 | ExtendedKey: extendedKey, 55 | ChainParams: chainParams, 56 | } 57 | 58 | // Create the key locator for the node key. 59 | keyLocator := keychain.KeyLocator{ 60 | Family: keychain.KeyFamilyNodeKey, 61 | Index: 0, 62 | } 63 | 64 | // Fetch the private key for node key. 65 | privKey, err := signer.FetchPrivateKey(&keychain.KeyDescriptor{ 66 | KeyLocator: keyLocator, 67 | }) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | // Create a new signer. 73 | privKeyMsgSigner := keychain.NewPrivKeyMessageSigner( 74 | privKey, keyLocator, 75 | ) 76 | 77 | // Prepend the special lnd prefix. 78 | // See: https://github.com/lightningnetwork/lnd/blob/63e698ec4990e678089533561fd95cfd684b67db/rpcserver.go#L1576 . 79 | msg := []byte(c.Msg) 80 | msg = append(signedMsgPrefix, msg...) 81 | sigBytes, err := privKeyMsgSigner.SignMessageCompact(msg, true) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | // Encode the signature. 87 | sig := zbase32.EncodeToString(sigBytes) 88 | fmt.Println(sig) 89 | 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /doc/chantools_zombierecovery_signoffer.md: -------------------------------------------------------------------------------- 1 | ## chantools zombierecovery signoffer 2 | 3 | [3/3] Sign an offer sent by the remote peer to recover funds 4 | 5 | ### Synopsis 6 | 7 | Inspect and sign an offer that was sent by the remote 8 | peer to recover funds from one or more channels. 9 | 10 | ``` 11 | chantools zombierecovery signoffer [flags] 12 | ``` 13 | 14 | ### Examples 15 | 16 | ``` 17 | chantools zombierecovery signoffer \ 18 | --psbt 19 | ``` 20 | 21 | ### Options 22 | 23 | ``` 24 | --apiurl string API URL to use for publishing the final transaction (must be esplora compatible) (default "https://api.node-recovery.com") 25 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 26 | -h, --help help for signoffer 27 | --hsm_secret string the hex encoded HSM secret to use for deriving the multisig keys for a CLN node; obtain by running 'xxd -p -c32 ~/.lightning/bitcoin/hsm_secret' 28 | --psbt string the base64 encoded PSBT that the other party sent as an offer to rescue funds 29 | --publish if set, the final PSBT will be published to the network after signing, otherwise it will just be printed to stdout 30 | --remote_peer string the hex encoded remote peer node identity key, only required when running 'signoffer' on the CLN side 31 | --rootkey string BIP32 HD root key of the wallet to use for signing the offer; leave empty to prompt for lnd 24 word aezeed 32 | --walletdb string read the seed/master root key to use for signing the offer from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 33 | ``` 34 | 35 | ### Options inherited from parent commands 36 | 37 | ``` 38 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 39 | -r, --regtest Indicates if regtest parameters should be used 40 | --resultsdir string Directory where results should be stored (default "./results") 41 | -s, --signet Indicates if the public signet parameters should be used 42 | -t, --testnet Indicates if testnet parameters should be used 43 | ``` 44 | 45 | ### SEE ALSO 46 | 47 | * [chantools zombierecovery](chantools_zombierecovery.md) - Try rescuing funds stuck in channels with zombie nodes 48 | 49 | -------------------------------------------------------------------------------- /cmd/chantools/dropgraphzombies.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/lightninglabs/chantools/lnd" 8 | graphdb "github.com/lightningnetwork/lnd/graph/db" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var ( 13 | edgeBucket = []byte("graph-edge") 14 | zombieBucket = []byte("zombie-index") 15 | ) 16 | 17 | type dropGraphZombiesCommand struct { 18 | ChannelDB string 19 | NodeIdentityKey string 20 | FixOnly bool 21 | 22 | SingleChannel uint64 23 | 24 | cmd *cobra.Command 25 | } 26 | 27 | func newDropGraphZombiesCommand() *cobra.Command { 28 | cc := &dropGraphZombiesCommand{} 29 | cc.cmd = &cobra.Command{ 30 | Use: "dropgraphzombies", 31 | Short: "Remove all channels identified as zombies from the " + 32 | "graph to force a re-sync of the graph", 33 | Long: `This command removes all channels that were identified as 34 | zombies from the local graph. 35 | 36 | This will cause lnd to re-download all those channels from the network and can 37 | be helpful to fix a graph that is out of sync with the network. 38 | 39 | CAUTION: Running this command will make it impossible to use the channel DB 40 | with an older version of lnd. Downgrading is not possible and you'll need to 41 | run lnd ` + lndVersion + ` or later after using this command!'`, 42 | Example: `chantools dropgraphzombies \ 43 | --channeldb ~/.lnd/data/graph/mainnet/channel.db`, 44 | RunE: cc.Execute, 45 | } 46 | cc.cmd.Flags().StringVar( 47 | &cc.ChannelDB, "channeldb", "", "lnd channel.db file to drop "+ 48 | "zombies from", 49 | ) 50 | 51 | return cc.cmd 52 | } 53 | 54 | func (c *dropGraphZombiesCommand) Execute(_ *cobra.Command, _ []string) error { 55 | // Check that we have a channel DB. 56 | if c.ChannelDB == "" { 57 | return errors.New("channel DB is required") 58 | } 59 | db, _, err := lnd.OpenDB(c.ChannelDB, false) 60 | if err != nil { 61 | return fmt.Errorf("error opening rescue DB: %w", err) 62 | } 63 | defer func() { _ = db.Close() }() 64 | 65 | log.Infof("Dropping zombie channel bucket") 66 | 67 | rwTx, err := db.BeginReadWriteTx() 68 | if err != nil { 69 | return err 70 | } 71 | 72 | success := false 73 | defer func() { 74 | if !success { 75 | _ = rwTx.Rollback() 76 | } 77 | }() 78 | 79 | edges := rwTx.ReadWriteBucket(edgeBucket) 80 | if edges == nil { 81 | return graphdb.ErrGraphNoEdgesFound 82 | } 83 | 84 | if err := edges.DeleteNestedBucket(zombieBucket); err != nil { 85 | return err 86 | } 87 | 88 | success = true 89 | 90 | return rwTx.Commit() 91 | } 92 | -------------------------------------------------------------------------------- /doc/chantools_zombierecovery_preparekeys.md: -------------------------------------------------------------------------------- 1 | ## chantools zombierecovery preparekeys 2 | 3 | [1/3] Prepare all public keys for a recovery attempt 4 | 5 | ### Synopsis 6 | 7 | Takes a match file, validates it against the seed and 8 | then adds the first 2500 multisig pubkeys to it. 9 | This must be run by both parties of a channel for a successful recovery. The 10 | next step (makeoffer) takes two such key enriched files and tries to find the 11 | correct ones for the matched channels. 12 | 13 | ``` 14 | chantools zombierecovery preparekeys [flags] 15 | ``` 16 | 17 | ### Examples 18 | 19 | ``` 20 | chantools zombierecovery preparekeys \ 21 | --match_file match-xxxx-xx-xx--.json \ 22 | --payout_addr bc1q... 23 | ``` 24 | 25 | ### Options 26 | 27 | ``` 28 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 29 | -h, --help help for preparekeys 30 | --hsm_secret string the hex encoded HSM secret to use for deriving the multisig keys for a CLN node; obtain by running 'xxd -p -c32 ~/.lightning/bitcoin/hsm_secret' 31 | --match_file string the match JSON file that was sent to both nodes by the match maker 32 | --num_keys uint32 the number of multisig keys to derive (default 2500) 33 | --payout_addr string the address where this node's rescued funds should be sent to, must be a P2WPKH (native SegWit) or P2TR (Taproot) address 34 | --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 35 | --walletdb string read the seed/master root key to use for deriving the multisig keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 36 | ``` 37 | 38 | ### Options inherited from parent commands 39 | 40 | ``` 41 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 42 | -r, --regtest Indicates if regtest parameters should be used 43 | --resultsdir string Directory where results should be stored (default "./results") 44 | -s, --signet Indicates if the public signet parameters should be used 45 | -t, --testnet Indicates if testnet parameters should be used 46 | ``` 47 | 48 | ### SEE ALSO 49 | 50 | * [chantools zombierecovery](chantools_zombierecovery.md) - Try rescuing funds stuck in channels with zombie nodes 51 | 52 | -------------------------------------------------------------------------------- /doc/chantools_pullanchor.md: -------------------------------------------------------------------------------- 1 | ## chantools pullanchor 2 | 3 | Attempt to CPFP an anchor output of a channel 4 | 5 | ### Synopsis 6 | 7 | Use this command to confirm a channel force close 8 | transaction of an anchor output channel type. This will attempt to CPFP the 9 | 330 byte anchor output created for your node. 10 | 11 | ``` 12 | chantools pullanchor [flags] 13 | ``` 14 | 15 | ### Examples 16 | 17 | ``` 18 | chantools pullanchor \ 19 | --sponsorinput txid:vout \ 20 | --anchoraddr bc1q..... \ 21 | --changeaddr bc1q..... \ 22 | --feerate 30 23 | ``` 24 | 25 | ### Options 26 | 27 | ``` 28 | --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 29 | --apiurl string API URL to use (must be esplora compatible) (default "https://api.node-recovery.com") 30 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 31 | --changeaddr string the change address to send the remaining funds back to; specify 'fromseed' to derive a new address from the seed automatically 32 | --feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30) 33 | -h, --help help for pullanchor 34 | --rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed 35 | --sponsorinput string the input to use to sponsor the CPFP transaction; must be owned by the lnd node that owns the anchor output 36 | --walletdb string read the seed/master root key to use for deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 37 | ``` 38 | 39 | ### Options inherited from parent commands 40 | 41 | ``` 42 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 43 | -r, --regtest Indicates if regtest parameters should be used 44 | --resultsdir string Directory where results should be stored (default "./results") 45 | -s, --signet Indicates if the public signet parameters should be used 46 | -t, --testnet Indicates if testnet parameters should be used 47 | ``` 48 | 49 | ### SEE ALSO 50 | 51 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 52 | 53 | -------------------------------------------------------------------------------- /doc/chantools_doublespendinputs.md: -------------------------------------------------------------------------------- 1 | ## chantools doublespendinputs 2 | 3 | Replace a transaction by double spending its input 4 | 5 | ### Synopsis 6 | 7 | Tries to double spend the given inputs by deriving the 8 | private for the address and sweeping the funds to the given address. This can 9 | only be used with inputs that belong to an lnd wallet. 10 | 11 | ``` 12 | chantools doublespendinputs [flags] 13 | ``` 14 | 15 | ### Examples 16 | 17 | ``` 18 | chantools doublespendinputs \ 19 | --inputoutpoints xxxxxxxxx:y,xxxxxxxxx:y \ 20 | --sweepaddr bc1q..... \ 21 | --feerate 10 \ 22 | --publish 23 | ``` 24 | 25 | ### Options 26 | 27 | ``` 28 | --apiurl string API URL to use (must be esplora compatible) (default "https://api.node-recovery.com") 29 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 30 | --feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30) 31 | -h, --help help for doublespendinputs 32 | --inputoutpoints strings list of outpoints to double spend in the format txid:vout 33 | --publish publish replacement TX to the chain API instead of just printing the TX 34 | --recoverywindow uint32 number of keys to scan per internal/external branch; output will consist of double this amount of keys (default 2500) 35 | --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 36 | --sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically 37 | --walletdb string read the seed/master root key to use for deriving the input keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 38 | ``` 39 | 40 | ### Options inherited from parent commands 41 | 42 | ``` 43 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 44 | -r, --regtest Indicates if regtest parameters should be used 45 | --resultsdir string Directory where results should be stored (default "./results") 46 | -s, --signet Indicates if the public signet parameters should be used 47 | -t, --testnet Indicates if testnet parameters should be used 48 | ``` 49 | 50 | ### SEE ALSO 51 | 52 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 53 | 54 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG := github.com/lightninglabs/chantools 2 | 3 | GOFILES_NOVENDOR = $(shell find . -type f -name '*.go' -not -path "./vendor/*") 4 | GOLIST := go list $(PKG)/... | grep -v '/vendor/' 5 | 6 | GOBUILD := CGO_ENABLED=0 go build -v 7 | GOINSTALL := CGO_ENABLED=0 go install -v 8 | GOTEST := CGO_ENABLED=0 go test -v 9 | XARGS := xargs -L 1 10 | 11 | VERSION_TAG = $(shell git describe --tags) 12 | VERSION_CHECK = @$(call print, "Building master with date version tag") 13 | 14 | BUILD_SYSTEM = darwin-amd64 \ 15 | darwin-arm64 \ 16 | linux-386 \ 17 | linux-amd64 \ 18 | linux-armv6 \ 19 | linux-armv7 \ 20 | linux-arm64 \ 21 | windows-amd64 22 | 23 | # By default we will build all systems. But with the 'sys' tag, a specific 24 | # system can be specified. This is useful to release for a subset of 25 | # systems/architectures. 26 | ifneq ($(sys),) 27 | BUILD_SYSTEM = $(sys) 28 | endif 29 | 30 | TEST_FLAGS = -test.timeout=20m 31 | 32 | UNIT := $(GOLIST) | grep -v "/itest" | $(XARGS) env $(GOTEST) $(TEST_FLAGS) 33 | LDFLAGS := -X main.Commit=$(shell git describe --tags) 34 | RELEASE_LDFLAGS := -s -w -buildid= $(LDFLAGS) 35 | 36 | GREEN := "\\033[0;32m" 37 | NC := "\\033[0m" 38 | define print 39 | echo $(GREEN)$1$(NC) 40 | endef 41 | 42 | default: build 43 | 44 | unit: 45 | @$(call print, "Running unit tests.") 46 | $(UNIT) 47 | 48 | itest: install 49 | @$(call print, "Running integration tests.") 50 | cd itest; ./itest.sh 51 | 52 | build: 53 | @$(call print, "Building chantools.") 54 | $(GOBUILD) -ldflags "$(LDFLAGS)" ./... 55 | 56 | install: 57 | @$(call print, "Installing chantools.") 58 | $(GOINSTALL) -ldflags "$(LDFLAGS)" ./... 59 | 60 | release: 61 | @$(call print, "Creating release of chantools.") 62 | rm -rf chantools-v* 63 | ./release.sh build-release "$(VERSION_TAG)" "$(BUILD_SYSTEM)" "$(RELEASE_LDFLAGS)" 64 | 65 | docker-release: 66 | @$(call print, "Creating docker release of chantools.") 67 | ./release.sh docker-release "$(VERSION_TAG)" 68 | 69 | command-generator-build: 70 | @$(call print, "Building command generator.") 71 | cd doc/command-generator; npm install && npm run build 72 | mv doc/command-generator/dist/index.html doc/command-generator.html 73 | 74 | fmt: $(GOIMPORTS_BIN) 75 | @$(call print, "Fixing imports.") 76 | go tool gosimports -w $(GOFILES_NOVENDOR) 77 | @$(call print, "Formatting source.") 78 | gofmt -l -w -s $(GOFILES_NOVENDOR) 79 | 80 | lint: 81 | @$(call print, "Linting source.") 82 | go tool golangci-lint run -v $(LINT_WORKERS) 83 | 84 | docs: install command-generator-build 85 | @$(call print, "Rendering docs.") 86 | chantools doc 87 | 88 | .PHONY: unit itest build install release fmt lint docs 89 | -------------------------------------------------------------------------------- /doc/chantools_zombierecovery_makeoffer.md: -------------------------------------------------------------------------------- 1 | ## chantools zombierecovery makeoffer 2 | 3 | [2/3] Make an offer on how to split the funds to recover 4 | 5 | ### Synopsis 6 | 7 | After both parties have prepared their keys with the 8 | 'preparekeys' command and have exchanged the files generated from that step, 9 | one party has to create an offer on how to split the funds that are in the 10 | channels to be rescued. 11 | If the other party agrees with the offer, they can sign and publish the offer 12 | with the 'signoffer' command. If the other party does not agree, they can create 13 | a counter offer. 14 | 15 | ``` 16 | chantools zombierecovery makeoffer [flags] 17 | ``` 18 | 19 | ### Examples 20 | 21 | ``` 22 | chantools zombierecovery makeoffer \ 23 | --node1_keys preparedkeys-xxxx-xx-xx-.json \ 24 | --node2_keys preparedkeys-xxxx-xx-xx-.json \ 25 | --feerate 15 26 | ``` 27 | 28 | ### Options 29 | 30 | ``` 31 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 32 | --feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30) 33 | -h, --help help for makeoffer 34 | --hsm_secret string the hex encoded HSM secret to use for deriving the multisig keys for a CLN node; obtain by running 'xxd -p -c32 ~/.lightning/bitcoin/hsm_secret' 35 | --matchonly only match the keys, don't create an offer 36 | --node1_keys string the JSON file generated in theprevious step ('preparekeys') command of node 1 37 | --node2_keys string the JSON file generated in theprevious step ('preparekeys') command of node 2 38 | --rootkey string BIP32 HD root key of the wallet to use for signing the offer; leave empty to prompt for lnd 24 word aezeed 39 | --walletdb string read the seed/master root key to use for signing the offer from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 40 | ``` 41 | 42 | ### Options inherited from parent commands 43 | 44 | ``` 45 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 46 | -r, --regtest Indicates if regtest parameters should be used 47 | --resultsdir string Directory where results should be stored (default "./results") 48 | -s, --signet Indicates if the public signet parameters should be used 49 | -t, --testnet Indicates if testnet parameters should be used 50 | ``` 51 | 52 | ### SEE ALSO 53 | 54 | * [chantools zombierecovery](chantools_zombierecovery.md) - Try rescuing funds stuck in channels with zombie nodes 55 | 56 | -------------------------------------------------------------------------------- /doc/chantools_triggerforceclose.md: -------------------------------------------------------------------------------- 1 | ## chantools triggerforceclose 2 | 3 | Connect to a Lightning Network peer and send specific messages to trigger a force close of the specified channel 4 | 5 | ### Synopsis 6 | 7 | Asks the specified remote peer to force close a specific 8 | channel by first sending a channel re-establish message, and if that doesn't 9 | work, a custom error message (in case the peer is a specific version of CLN that 10 | does not properly respond to a Data Loss Protection re-establish message).' 11 | 12 | ``` 13 | chantools triggerforceclose [flags] 14 | ``` 15 | 16 | ### Examples 17 | 18 | ``` 19 | chantools triggerforceclose \ 20 | --peer 03abce...@xx.yy.zz.aa:9735 \ 21 | --channel_point abcdef01234...:x 22 | ``` 23 | 24 | ### Options 25 | 26 | ``` 27 | --all_public_channels query all public channels from the Amboss API and attempt to trigger a force close for each of them 28 | --apiurl string API URL to use (must be esplora compatible) (default "https://api.node-recovery.com") 29 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 30 | --channel_point string funding transaction outpoint of the channel to trigger the force close of (:) 31 | -h, --help help for triggerforceclose 32 | --hsm_secret string the hex encoded HSM secret to use for deriving the node key for a CLN node; obtain by running 'xxd -p -c32 ~/.lightning/bitcoin/hsm_secret' 33 | --peer string remote peer address (@[:]) 34 | --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 35 | --torproxy string SOCKS5 proxy to use for Tor connections (to .onion addresses) 36 | --walletdb string read the seed/master root key to use for deriving the identity key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 37 | ``` 38 | 39 | ### Options inherited from parent commands 40 | 41 | ``` 42 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 43 | -r, --regtest Indicates if regtest parameters should be used 44 | --resultsdir string Directory where results should be stored (default "./results") 45 | -s, --signet Indicates if the public signet parameters should be used 46 | -t, --testnet Indicates if testnet parameters should be used 47 | ``` 48 | 49 | ### SEE ALSO 50 | 51 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 52 | 53 | -------------------------------------------------------------------------------- /doc/chantools_recoverloopin.md: -------------------------------------------------------------------------------- 1 | ## chantools recoverloopin 2 | 3 | Recover a loop in swap that the loop daemon is not able to sweep 4 | 5 | ``` 6 | chantools recoverloopin [flags] 7 | ``` 8 | 9 | ### Examples 10 | 11 | ``` 12 | chantools recoverloopin \ 13 | --txid abcdef01234... \ 14 | --vout 0 \ 15 | --swap_hash abcdef01234... \ 16 | --loop_db_dir /path/to/loop/db/dir \ 17 | --sweep_addr bc1pxxxxxxx \ 18 | --feerate 10 19 | ``` 20 | 21 | ### Options 22 | 23 | ``` 24 | --apiurl string API URL to use (must be esplora compatible) (default "https://api.node-recovery.com") 25 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 26 | --feerate uint32 fee rate to use for the sweep transaction in sat/vByte 27 | -h, --help help for recoverloopin 28 | --loop_db_dir string path to the loop database directory, where the loop.db file is located 29 | --num_tries int number of tries to try to find the correct key index (default 1000) 30 | --output_amt uint amount of the output to sweep 31 | --publish publish sweep TX to the chain API instead of just printing the TX 32 | --rootkey string BIP32 HD root key of the wallet to use for deriving starting key; leave empty to prompt for lnd 24 word aezeed 33 | --sqlite_file string optional path to the loop sqlite database file, if not specified, the default location will be loaded from --loop_db_dir 34 | --start_key_index int start key index to try to find the correct key index 35 | --swap_hash string swap hash of the loop in swap 36 | --sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically 37 | --txid string transaction id of the on-chain transaction that created the HTLC 38 | --vout uint32 output index of the on-chain transaction that created the HTLC 39 | --walletdb string read the seed/master root key to use for deriving starting key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 40 | ``` 41 | 42 | ### Options inherited from parent commands 43 | 44 | ``` 45 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 46 | -r, --regtest Indicates if regtest parameters should be used 47 | --resultsdir string Directory where results should be stored (default "./results") 48 | -s, --signet Indicates if the public signet parameters should be used 49 | -t, --testnet Indicates if testnet parameters should be used 50 | ``` 51 | 52 | ### SEE ALSO 53 | 54 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 55 | 56 | -------------------------------------------------------------------------------- /doc/chantools_scbforceclose.md: -------------------------------------------------------------------------------- 1 | ## chantools scbforceclose 2 | 3 | Force-close the last state that is in the SCB provided 4 | 5 | ### Synopsis 6 | 7 | 8 | If you are certain that a node is offline for good (AFTER you've tried SCB!) 9 | and a channel is still open, you can use this method to force-close your 10 | latest state that you have in your channel.db. 11 | 12 | **!!! WARNING !!! DANGER !!! WARNING !!!** 13 | 14 | If you do this and the state that you publish is *not* the latest state, then 15 | the remote node *could* punish you by taking the whole channel amount *if* they 16 | come online before you can sweep the funds from the time locked (144 - 2000 17 | blocks) transaction *or* they have a watch tower looking out for them. 18 | 19 | **This should absolutely be the last resort and you have been warned!** 20 | 21 | ``` 22 | chantools scbforceclose [flags] 23 | ``` 24 | 25 | ### Examples 26 | 27 | ``` 28 | chantools scbforceclose --multi_file channel.backup 29 | ``` 30 | 31 | ### Options 32 | 33 | ``` 34 | --apiurl string API URL to use (must be esplora compatible) (default "https://api.node-recovery.com") 35 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 36 | -h, --help help for scbforceclose 37 | --multi_backup string a hex encoded multi-channel backup obtained from exportchanbackup for force-closing channels 38 | --multi_file string the path to a single-channel backup file (channel.backup) 39 | --publish publish force-closing TX to the chain API instead of just printing the TX 40 | --rootkey string BIP32 HD root key of the wallet to use for decrypting the backup and signing tx; leave empty to prompt for lnd 24 word aezeed 41 | --single_backup string a hex encoded single channel backup obtained from exportchanbackup for force-closing channels 42 | --single_file string the path to a single-channel backup file 43 | --walletdb string read the seed/master root key to use for decrypting the backup and signing tx from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 44 | ``` 45 | 46 | ### Options inherited from parent commands 47 | 48 | ``` 49 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 50 | -r, --regtest Indicates if regtest parameters should be used 51 | --resultsdir string Directory where results should be stored (default "./results") 52 | -s, --signet Indicates if the public signet parameters should be used 53 | -t, --testnet Indicates if testnet parameters should be used 54 | ``` 55 | 56 | ### SEE ALSO 57 | 58 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 59 | 60 | -------------------------------------------------------------------------------- /cmd/chantools/closepoolaccount_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/btcsuite/btcd/btcec/v2" 8 | "github.com/btcsuite/btcd/btcutil/hdkeychain" 9 | "github.com/btcsuite/btcd/chaincfg" 10 | "github.com/lightninglabs/chantools/lnd" 11 | "github.com/lightninglabs/pool/poolscript" 12 | "github.com/lightningnetwork/lnd/keychain" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | type testAccount struct { 17 | name string 18 | rootKey string 19 | pkScript string 20 | minExpiry uint32 21 | } 22 | 23 | var ( 24 | auctioneerKeyBytes, _ = hex.DecodeString( 25 | "0353c7c0d3258c4957331b86af335568232e9af8df61330cee3a7488b61c" + 26 | "f6c298", 27 | ) 28 | auctioneerKey, _ = btcec.ParsePubKey(auctioneerKeyBytes) 29 | 30 | testAccounts = []testAccount{{ 31 | name: "regtest taproot (v1)", 32 | rootKey: "tprv8ZgxMBicQKsPdkvdLKn7HG2hhZ9Ewsgze1Yj3KDEcvb6H5U" + 33 | "519UtfoPPP3hYVgFTn7hXmvE41qaugbaYiZN8wM1HoQHhs3AzSwg" + 34 | "xGYdD8gM", 35 | pkScript: "512001e8d17b83358476534aae4eae2062ea9025dfd858cd81" + 36 | "7bac5f439969da92a6", 37 | minExpiry: 1600, 38 | }, { 39 | name: "regtest taproot (v2)", 40 | rootKey: "tprv8ZgxMBicQKsPdkvdLKn7HG2hhZ9Ewsgze1Yj3KDEcvb6H5U" + 41 | "519UtfoPPP3hYVgFTn7hXmvE41qaugbaYiZN8wM1HoQHhs3AzSwg" + 42 | "xGYdD8gM", 43 | pkScript: "51209dfee24b87f5c35d5a310496a64fab70641bd03d40d5cc" + 44 | "3720f6061f7435778a", 45 | minExpiry: 2060, 46 | }, { 47 | name: "regtest segwit (v0)", 48 | rootKey: "tprv8ZgxMBicQKsPdkvdLKn7HG2hhZ9Ewsgze1Yj3KDEcvb6H5U" + 49 | "519UtfoPPP3hYVgFTn7hXmvE41qaugbaYiZN8wM1HoQHhs3AzSwg" + 50 | "xGYdD8gM", 51 | pkScript: "00201acfd449370aca0f744141bc6fe1f9fe326aa57a9cd35f" + 52 | "bc2f8f15af4c0f4597", 53 | minExpiry: 1600, 54 | }} 55 | ) 56 | 57 | func TestClosePoolAccount(t *testing.T) { 58 | t.Parallel() 59 | 60 | path := []uint32{ 61 | lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose), 62 | lnd.HardenedKeyStart + chaincfg.RegressionNetParams.HDCoinType, 63 | lnd.HardenedKeyStart + uint32(poolscript.AccountKeyFamily), 64 | 0, 65 | } 66 | const ( 67 | maxBlocks = 50 68 | maxAccounts = 5 69 | maxBatchKeys = 10 70 | ) 71 | 72 | for _, tc := range testAccounts { 73 | t.Run(tc.name, func(tt *testing.T) { 74 | tt.Parallel() 75 | 76 | extendedKey, err := hdkeychain.NewKeyFromString( 77 | tc.rootKey, 78 | ) 79 | require.NoError(tt, err) 80 | accountBaseKey, err := lnd.DeriveChildren( 81 | extendedKey, path, 82 | ) 83 | require.NoError(tt, err) 84 | targetScriptBytes, err := hex.DecodeString(tc.pkScript) 85 | require.NoError(tt, err) 86 | 87 | acct, err := bruteForceAccountScript( 88 | accountBaseKey, auctioneerKey, tc.minExpiry, 89 | tc.minExpiry+maxBlocks, maxAccounts, 90 | maxBatchKeys, targetScriptBytes, 91 | ) 92 | require.NoError(tt, err) 93 | t.Logf("Found account: %v", acct) 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /itest/README.md: -------------------------------------------------------------------------------- 1 | # Chantools Integration Test (itest) 2 | 3 | This directory contains the integration test (itest) setup and test cases for 4 | chantools. The test network is created using Docker Compose scripts and consists 5 | of multiple Lightning Network nodes connected in a specific topology to create 6 | multiple channels with non-zero channel update indexes (by sending some payments 7 | around the network). 8 | 9 | ## Test Network Topology 10 | 11 | The network is set up as follows: 12 | 13 | ``` 14 | Alice ◄──► Bob ◄──► Charlie ◄──► Dave 15 | └───────►└──► Rusty ◄──┘ 16 | | Nifty ◄──┘ | 17 | └► Snyke ◄─────────────┘ 18 | ``` 19 | 20 | - Channel **Alice** - **Bob**: Remains open, used by `runZombieRecoveryLndLnd`. 21 | - Channel **Alice** - **Rusty**: Is force closed by Alice, used by 22 | `runSweepRemoteClosedCln`. 23 | - Channel **Alice** - **Snyke**: Remains open, used by 24 | `runTriggerForceCloseCln`. 25 | - Channel **Bob** - **Charlie**: Is force closed by Bob, used by 26 | `runSweepRemoteClosedLnd`. 27 | - Channel **Charlie** - **Dave**: Remains open, used by 28 | `runTriggerForceCloseLnd`. 29 | - Channel **Charlie** - **Snyke**: Remains open, used by `runSCBForceClose`. 30 | - Channel **Bob** - **Rusty**: Remains open, used by `runZombieRecoveryLndCln`. 31 | - Channel **Rusty** - **Charlie**: Remains open, used by 32 | `runZombieRecoveryClnLnd`. 33 | - Channel **Rusty** - **Nifty**: Remains open, used by 34 | `runZombieRecoveryClnCln`. 35 | 36 | All nodes except for Dave and Snyke are stopped before the integration tests 37 | are run. 38 | 39 | Multiple channels are opened between the nodes, and several multi-hop payments 40 | are executed to ensure the network is fully operational and synchronized. After 41 | the setup, each node's channel information is exported to a JSON file in the 42 | `node-data/chantools/` directory. 43 | 44 | ## Running the Integration Tests 45 | 46 | To start the integration tests, run the following command from the project root: 47 | 48 | ```sh 49 | make itest 50 | ``` 51 | 52 | Which executes the script `itest/itest.sh`. 53 | 54 | This script will: 55 | - Build and start the Docker-based test network. 56 | - Set up the channels and perform payments between nodes. 57 | - Export channel data for each node. 58 | - Run the Go integration tests against the prepared network. 59 | 60 | ## Debugging the Test Network 61 | 62 | By default, the test network is torn down automatically after the tests finish 63 | or if an error occurs. If you want to keep the Docker containers running for 64 | debugging purposes, set the `DEBUG` environment variable before running the 65 | script: 66 | 67 | ```sh 68 | make itest DEBUG=1 69 | ``` 70 | 71 | With `DEBUG` set, the containers will not be stopped automatically, allowing you 72 | to inspect the state of the network and containers for troubleshooting and 73 | run the Golang based integration tests manually several times. 74 | 75 | --- 76 | 77 | For more details on the network setup, see `docker/setup-test-network.sh`. 78 | -------------------------------------------------------------------------------- /btc/fasthd/extendedkey.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2016 The btcsuite developers 2 | // Use of this source code is governed by an ISC 3 | // license that can be found in the LICENSE file. 4 | 5 | package fasthd 6 | 7 | import ( 8 | "crypto/hmac" 9 | "crypto/sha512" 10 | "encoding/binary" 11 | "errors" 12 | "math/big" 13 | 14 | "github.com/btcsuite/btcd/btcec/v2" 15 | "github.com/btcsuite/btcd/chaincfg" 16 | ) 17 | 18 | const ( 19 | HardenedKeyStart = 0x80000000 // 2^31 20 | keyLen = 33 21 | ) 22 | 23 | var ( 24 | ErrInvalidChild = errors.New("the extended key at this index is invalid") 25 | ErrUnusableSeed = errors.New("unusable seed") 26 | masterKey = []byte("Bitcoin seed") 27 | ) 28 | 29 | type FastDerivation struct { 30 | key []byte 31 | chainCode []byte 32 | version []byte 33 | scratch [keyLen + 4]byte 34 | } 35 | 36 | func (k *FastDerivation) PubKeyBytes() []byte { 37 | _, pubKey := btcec.PrivKeyFromBytes(k.key) 38 | return pubKey.SerializeCompressed() 39 | } 40 | 41 | func (k *FastDerivation) Child(i uint32) error { 42 | isChildHardened := i >= HardenedKeyStart 43 | if isChildHardened { 44 | copy(k.scratch[1:], k.key) 45 | } else { 46 | copy(k.scratch[:], k.PubKeyBytes()) 47 | } 48 | binary.BigEndian.PutUint32(k.scratch[keyLen:], i) 49 | 50 | hmac512 := hmac.New(sha512.New, k.chainCode) 51 | _, _ = hmac512.Write(k.scratch[:]) 52 | ilr := hmac512.Sum(nil) 53 | 54 | il := ilr[:len(ilr)/2] 55 | childChainCode := ilr[len(ilr)/2:] 56 | 57 | ilNum := new(big.Int).SetBytes(il) 58 | if ilNum.Cmp(btcec.S256().N) >= 0 || ilNum.Sign() == 0 { 59 | return ErrInvalidChild 60 | } 61 | 62 | keyNum := new(big.Int).SetBytes(k.key) 63 | ilNum.Add(ilNum, keyNum) 64 | ilNum.Mod(ilNum, btcec.S256().N) 65 | 66 | k.key = ilNum.Bytes() 67 | k.chainCode = childChainCode 68 | 69 | return nil 70 | } 71 | 72 | func (k *FastDerivation) ChildPath(path []uint32) error { 73 | for _, pathPart := range path { 74 | if err := k.Child(pathPart); err != nil { 75 | return err 76 | } 77 | } 78 | return nil 79 | } 80 | 81 | func NewFastDerivation(seed []byte, net *chaincfg.Params) (*FastDerivation, error) { 82 | // First take the HMAC-SHA512 of the master key and the seed data: 83 | // I = HMAC-SHA512(Key = "Bitcoin seed", Data = S) 84 | hmac512 := hmac.New(sha512.New, masterKey) 85 | _, _ = hmac512.Write(seed) 86 | lr := hmac512.Sum(nil) 87 | 88 | // Split "I" into two 32-byte sequences Il and Ir where: 89 | // Il = master secret key 90 | // Ir = master chain code 91 | secretKey := lr[:len(lr)/2] 92 | chainCode := lr[len(lr)/2:] 93 | 94 | // Ensure the key in usable. 95 | secretKeyNum := new(big.Int).SetBytes(secretKey) 96 | if secretKeyNum.Cmp(btcec.S256().N) >= 0 || secretKeyNum.Sign() == 0 { 97 | return nil, ErrUnusableSeed 98 | } 99 | 100 | return &FastDerivation{ 101 | key: secretKey, 102 | chainCode: chainCode, 103 | version: net.HDPrivateKeyID[:], 104 | }, nil 105 | } 106 | -------------------------------------------------------------------------------- /doc/chantools_closepoolaccount.md: -------------------------------------------------------------------------------- 1 | ## chantools closepoolaccount 2 | 3 | Tries to close a Pool account that has expired 4 | 5 | ### Synopsis 6 | 7 | In case a Pool account cannot be closed normally with the 8 | poold daemon it can be closed with this command. The account **MUST** have 9 | expired already, otherwise this command doesn't work since a signature from the 10 | auctioneer is necessary. 11 | 12 | You need to know the account's last unspent outpoint. That can either be 13 | obtained by running 'pool accounts list' 14 | 15 | ``` 16 | chantools closepoolaccount [flags] 17 | ``` 18 | 19 | ### Examples 20 | 21 | ``` 22 | chantools closepoolaccount \ 23 | --outpoint xxxxxxxxx:y \ 24 | --sweepaddr bc1q..... \ 25 | --feerate 10 \ 26 | --publish 27 | ``` 28 | 29 | ### Options 30 | 31 | ``` 32 | --apiurl string API URL to use (must be esplora compatible) (default "https://api.node-recovery.com") 33 | --auctioneerkey string the auctioneer's static public key (default "028e87bdd134238f8347f845d9ecc827b843d0d1e27cdcb46da704d916613f4fce") 34 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 35 | --feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30) 36 | -h, --help help for closepoolaccount 37 | --maxnumaccounts uint32 the number of account indices to try at most (default 20) 38 | --maxnumbatchkeys uint32 the number of batch keys to try at most (default 500) 39 | --maxnumblocks uint32 the maximum number of blocks to try when brute forcing the expiry (default 200000) 40 | --minexpiry uint32 the block to start brute forcing the expiry from (default 648168) 41 | --outpoint string last account outpoint of the account to close (:) 42 | --publish publish sweep TX to the chain API instead of just printing the TX 43 | --rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed 44 | --sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically 45 | --walletdb string read the seed/master root key to use for deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 46 | ``` 47 | 48 | ### Options inherited from parent commands 49 | 50 | ``` 51 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 52 | -r, --regtest Indicates if regtest parameters should be used 53 | --resultsdir string Directory where results should be stored (default "./results") 54 | -s, --signet Indicates if the public signet parameters should be used 55 | -t, --testnet Indicates if testnet parameters should be used 56 | ``` 57 | 58 | ### SEE ALSO 59 | 60 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 61 | 62 | -------------------------------------------------------------------------------- /cln/derivation_test.go: -------------------------------------------------------------------------------- 1 | package cln 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/btcsuite/btcd/btcec/v2" 8 | "github.com/lightningnetwork/lnd/keychain" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | var ( 13 | hsmSecret = [32]byte{ 14 | 0x3f, 0x0a, 0x06, 0xc6, 0x38, 0x5b, 0x74, 0x93, 15 | 0xf7, 0x5a, 0xa0, 0x08, 0x9f, 0x31, 0x6a, 0x13, 16 | 0xbf, 0x72, 0xbe, 0xb4, 0x30, 0xe5, 0x9e, 0x71, 17 | 0xb5, 0xac, 0x5a, 0x73, 0x58, 0x1a, 0x62, 0x70, 18 | } 19 | nodeKeyBytes, _ = hex.DecodeString( 20 | "035149629152c1bee83f1e148a51400b5f24bf3e2ca53384dd801418446e" + 21 | "1f53fe", 22 | ) 23 | 24 | peerPubKeyBytes, _ = hex.DecodeString( 25 | "02678187ca43e6a6f62f9185be98a933bf485313061e6a05578bbd83c54e" + 26 | "88d460", 27 | ) 28 | peerPubKey, _ = btcec.ParsePubKey(peerPubKeyBytes) 29 | 30 | expectedFundingKeyBytes, _ = hex.DecodeString( 31 | "0326a2171c97673cc8cd7a04a043f0224c59591fc8c9de320a48f7c9b68a" + 32 | "b0ae2b", 33 | ) 34 | ) 35 | 36 | func TestNodeKey(t *testing.T) { 37 | nodeKey, _, err := NodeKey(hsmSecret) 38 | require.NoError(t, err) 39 | 40 | require.Equal(t, nodeKeyBytes, nodeKey.SerializeCompressed()) 41 | } 42 | 43 | func TestFundingKey(t *testing.T) { 44 | fundingKey, _, err := DeriveKeyPair(hsmSecret, &keychain.KeyDescriptor{ 45 | PubKey: peerPubKey, 46 | KeyLocator: keychain.KeyLocator{ 47 | Family: keychain.KeyFamilyMultiSig, 48 | Index: 1, 49 | }, 50 | }) 51 | require.NoError(t, err) 52 | 53 | require.Equal( 54 | t, expectedFundingKeyBytes, fundingKey.SerializeCompressed(), 55 | ) 56 | } 57 | 58 | func TestPaymentBasePointSecret(t *testing.T) { 59 | hsmSecret2, _ := hex.DecodeString( 60 | "665b09e6fc86391f0141d957eb14ec30f8f8a58a876842792474cacc2448" + 61 | "9456", 62 | ) 63 | 64 | basePointPeerBytes, _ := hex.DecodeString( 65 | "0350aeef9f33a157953d3c3c1ef464bdf421204461959524e52e530c17f1" + 66 | "66f541", 67 | ) 68 | 69 | expectedPaymentBasePointBytes, _ := hex.DecodeString( 70 | "0339c93ca896829672510f8a4e51caef4b5f6a26f880acf5a120725a7f02" + 71 | "7b56b4", 72 | ) 73 | 74 | var hsmSecret [32]byte 75 | copy(hsmSecret[:], hsmSecret2) 76 | 77 | basepointPeer, err := btcec.ParsePubKey(basePointPeerBytes) 78 | require.NoError(t, err) 79 | 80 | nk, _, err := NodeKey(hsmSecret) 81 | require.NoError(t, err) 82 | 83 | t.Logf("Node key: %x", nk.SerializeCompressed()) 84 | 85 | fk, _, err := DeriveKeyPair(hsmSecret, &keychain.KeyDescriptor{ 86 | PubKey: basepointPeer, 87 | KeyLocator: keychain.KeyLocator{ 88 | Family: keychain.KeyFamilyMultiSig, 89 | Index: 1, 90 | }, 91 | }) 92 | require.NoError(t, err) 93 | 94 | t.Logf("Funding key: %x", fk.SerializeCompressed()) 95 | 96 | paymentBasePoint, _, err := DeriveKeyPair( 97 | hsmSecret, &keychain.KeyDescriptor{ 98 | PubKey: basepointPeer, 99 | KeyLocator: keychain.KeyLocator{ 100 | Family: keychain.KeyFamilyPaymentBase, 101 | Index: 1, 102 | }, 103 | }, 104 | ) 105 | require.NoError(t, err) 106 | 107 | require.Equal( 108 | t, expectedPaymentBasePointBytes, 109 | paymentBasePoint.SerializeCompressed(), 110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /doc/chantools_forceclose.md: -------------------------------------------------------------------------------- 1 | ## chantools forceclose 2 | 3 | Force-close the last state that is in the channel.db provided 4 | 5 | ### Synopsis 6 | 7 | 8 | If you are certain that a node is offline for good (AFTER you've tried SCB!) 9 | and a channel is still open, you can use this method to force-close your 10 | latest state that you have in your channel.db. 11 | 12 | **!!! WARNING !!! DANGER !!! WARNING !!!** 13 | 14 | If you do this and the state that you publish is *not* the latest state, then 15 | the remote node *could* punish you by taking the whole channel amount *if* they 16 | come online before you can sweep the funds from the time locked (144 - 2000 17 | blocks) transaction *or* they have a watch tower looking out for them. 18 | 19 | **This should absolutely be the last resort and you have been warned!** 20 | 21 | ``` 22 | chantools forceclose [flags] 23 | ``` 24 | 25 | ### Examples 26 | 27 | ``` 28 | chantools forceclose \ 29 | --fromsummary results/summary-xxxx-yyyy.json 30 | --channeldb ~/.lnd/data/graph/mainnet/channel.db \ 31 | --publish 32 | ``` 33 | 34 | ### Options 35 | 36 | ``` 37 | --apiurl string API URL to use (must be esplora compatible) (default "https://api.node-recovery.com") 38 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 39 | --channeldb string lnd channel.db file to use for force-closing channels 40 | --fromchanneldb string channel input is in the format of an lnd channel.db file 41 | --fromchanneldump string channel input is in the format of a channel dump file 42 | --fromsummary string channel input is in the format of chantool's channel summary; specify '-' to read from stdin 43 | -h, --help help for forceclose 44 | --listchannels string channel input is in the format of lncli's listchannels format; specify '-' to read from stdin 45 | --pendingchannels string channel input is in the format of lncli's pendingchannels format; specify '-' to read from stdin 46 | --publish publish force-closing TX to the chain API instead of just printing the TX 47 | --rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed 48 | --walletdb string read the seed/master root key to use for decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 49 | ``` 50 | 51 | ### Options inherited from parent commands 52 | 53 | ``` 54 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 55 | -r, --regtest Indicates if regtest parameters should be used 56 | --resultsdir string Directory where results should be stored (default "./results") 57 | -s, --signet Indicates if the public signet parameters should be used 58 | -t, --testnet Indicates if testnet parameters should be used 59 | ``` 60 | 61 | ### SEE ALSO 62 | 63 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 64 | 65 | -------------------------------------------------------------------------------- /doc/chantools_sweeptimelock.md: -------------------------------------------------------------------------------- 1 | ## chantools sweeptimelock 2 | 3 | Sweep the force-closed state after the time lock has expired 4 | 5 | ### Synopsis 6 | 7 | Use this command to sweep the funds from channels that 8 | you force-closed with the forceclose command. You **MUST** use the result file 9 | that was created with the forceclose command, otherwise it won't work. You also 10 | have to wait until the highest time lock (can be up to 2016 blocks which is more 11 | than two weeks) of all the channels has passed. If you only want to sweep 12 | channels that have the default CSV limit of 1 day, you can set the --maxcsvlimit 13 | parameter to 144. 14 | 15 | ``` 16 | chantools sweeptimelock [flags] 17 | ``` 18 | 19 | ### Examples 20 | 21 | ``` 22 | chantools sweeptimelock \ 23 | --fromsummary results/forceclose-xxxx-yyyy.json \ 24 | --sweepaddr bc1q..... \ 25 | --feerate 10 \ 26 | --publish 27 | ``` 28 | 29 | ### Options 30 | 31 | ``` 32 | --apiurl string API URL to use (must be esplora compatible) (default "https://api.node-recovery.com") 33 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 34 | --feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30) 35 | --fromchanneldb string channel input is in the format of an lnd channel.db file 36 | --fromchanneldump string channel input is in the format of a channel dump file 37 | --fromsummary string channel input is in the format of chantool's channel summary; specify '-' to read from stdin 38 | -h, --help help for sweeptimelock 39 | --listchannels string channel input is in the format of lncli's listchannels format; specify '-' to read from stdin 40 | --maxcsvlimit uint16 maximum CSV limit to use (default 2016) 41 | --pendingchannels string channel input is in the format of lncli's pendingchannels format; specify '-' to read from stdin 42 | --publish publish sweep TX to the chain API instead of just printing the TX 43 | --rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed 44 | --sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically 45 | --walletdb string read the seed/master root key to use for deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 46 | ``` 47 | 48 | ### Options inherited from parent commands 49 | 50 | ``` 51 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 52 | -r, --regtest Indicates if regtest parameters should be used 53 | --resultsdir string Directory where results should be stored (default "./results") 54 | -s, --signet Indicates if the public signet parameters should be used 55 | -t, --testnet Indicates if testnet parameters should be used 56 | ``` 57 | 58 | ### SEE ALSO 59 | 60 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 61 | 62 | -------------------------------------------------------------------------------- /cmd/chantools/filterbackup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/lightninglabs/chantools/lnd" 11 | "github.com/lightningnetwork/lnd/chanbackup" 12 | "github.com/lightningnetwork/lnd/keychain" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | type filterBackupCommand struct { 17 | MultiFile string 18 | Discard string 19 | 20 | rootKey *rootKey 21 | cmd *cobra.Command 22 | } 23 | 24 | func newFilterBackupCommand() *cobra.Command { 25 | cc := &filterBackupCommand{} 26 | cc.cmd = &cobra.Command{ 27 | Use: "filterbackup", 28 | Short: "Filter an lnd channel.backup file and remove certain " + 29 | "channels", 30 | Long: `Filter an lnd channel.backup file by removing certain 31 | channels (identified by their funding transaction outpoints).`, 32 | Example: `chantools filterbackup \ 33 | --multi_file ~/.lnd/data/chain/bitcoin/mainnet/channel.backup \ 34 | --discard 2abcdef2b2bffaaa...db0abadd:1,4abcdef2b2bffaaa...db8abadd:0`, 35 | RunE: cc.Execute, 36 | } 37 | cc.cmd.Flags().StringVar( 38 | &cc.MultiFile, "multi_file", "", "lnd channel.backup file to "+ 39 | "filter", 40 | ) 41 | cc.cmd.Flags().StringVar( 42 | &cc.Discard, "discard", "", "comma separated list of channel "+ 43 | "funding outpoints (format :) to "+ 44 | "remove from the backup file", 45 | ) 46 | 47 | cc.rootKey = newRootKey(cc.cmd, "decrypting the backup") 48 | 49 | return cc.cmd 50 | } 51 | 52 | func (c *filterBackupCommand) Execute(_ *cobra.Command, _ []string) error { 53 | extendedKey, err := c.rootKey.read() 54 | if err != nil { 55 | return fmt.Errorf("error reading root key: %w", err) 56 | } 57 | 58 | // Parse discard filter. 59 | discard := strings.Split(c.Discard, ",") 60 | 61 | // Check that we have a backup file. 62 | if c.MultiFile == "" { 63 | return errors.New("backup file is required") 64 | } 65 | multiFile := chanbackup.NewMultiFile(c.MultiFile, noBackupArchive) 66 | keyRing := &lnd.HDKeyRing{ 67 | ExtendedKey: extendedKey, 68 | ChainParams: chainParams, 69 | } 70 | return filterChannelBackup(multiFile, keyRing, discard) 71 | } 72 | 73 | func filterChannelBackup(multiFile *chanbackup.MultiFile, ring keychain.KeyRing, 74 | discard []string) error { 75 | 76 | multi, err := multiFile.ExtractMulti(ring) 77 | if err != nil { 78 | return fmt.Errorf("could not extract multi file: %w", err) 79 | } 80 | 81 | keep := make([]chanbackup.Single, 0, len(multi.StaticBackups)) 82 | for _, single := range multi.StaticBackups { 83 | found := false 84 | for _, discardChanPoint := range discard { 85 | if single.FundingOutpoint.String() == discardChanPoint { 86 | found = true 87 | } 88 | } 89 | if found { 90 | continue 91 | } 92 | keep = append(keep, single) 93 | } 94 | multi.StaticBackups = keep 95 | 96 | fileName := fmt.Sprintf("%s/backup-filtered-%s.backup", ResultsDir, 97 | time.Now().Format("2006-01-02-15-04-05")) 98 | log.Infof("Writing result to %s", fileName) 99 | f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 100 | if err != nil { 101 | return err 102 | } 103 | err = multi.PackToWriter(f, ring) 104 | _ = f.Close() 105 | if err != nil { 106 | return err 107 | } 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /cmd/chantools/scbforceclose_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "encoding/hex" 7 | "encoding/json" 8 | "testing" 9 | 10 | "github.com/btcsuite/btcd/btcec/v2" 11 | "github.com/btcsuite/btcd/wire" 12 | "github.com/btcsuite/btclog/v2" 13 | "github.com/lightningnetwork/lnd/chanbackup" 14 | "github.com/lightningnetwork/lnd/keychain" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | // TestClassifyOutputs_RealData verifies we can identify the to_remote output 19 | // using lnwallet.CommitScriptToRemote with real world data provided. 20 | func TestClassifyOutputs_RealData(t *testing.T) { 21 | h := newHarness(t) 22 | h.logger.SetLevel(btclog.LevelTrace) 23 | 24 | // Load test data from embedded file. 25 | testDataBytes := h.readTestdataFile("scbforceclose_testdata.json") 26 | 27 | var testData struct { 28 | RemotePubkey string `json:"remote_pubkey"` 29 | TransactionHex string `json:"transaction_hex"` 30 | } 31 | err := json.Unmarshal(testDataBytes, &testData) 32 | require.NoError(t, err) 33 | 34 | // Remote payment basepoint (compressed) provided by user. 35 | remoteBytes, err := hex.DecodeString(testData.RemotePubkey) 36 | require.NoError(t, err) 37 | remoteKey, err := btcec.ParsePubKey(remoteBytes) 38 | require.NoError(t, err) 39 | 40 | // Example transaction hex from a real world channel. 41 | txBytes, err := hex.DecodeString(testData.TransactionHex) 42 | require.NoError(t, err) 43 | var tx wire.MsgTx 44 | require.NoError(t, tx.Deserialize(bytes.NewReader(txBytes))) 45 | 46 | // Build a minimal Single with the remote payment basepoint. 47 | makeSingle := func(version chanbackup.SingleBackupVersion, 48 | initiator bool) chanbackup.Single { 49 | 50 | s := chanbackup.Single{ 51 | Version: version, 52 | IsInitiator: initiator, 53 | } 54 | s.RemoteChanCfg.PaymentBasePoint = keychain.KeyDescriptor{ 55 | PubKey: remoteKey, 56 | } 57 | 58 | return s 59 | } 60 | 61 | // Try a set of plausible SCB versions and initiator roles to find 62 | // a match. 63 | versions := []chanbackup.SingleBackupVersion{ 64 | chanbackup.AnchorsCommitVersion, 65 | chanbackup.AnchorsZeroFeeHtlcTxCommitVersion, 66 | chanbackup.ScriptEnforcedLeaseVersion, 67 | chanbackup.TweaklessCommitVersion, 68 | chanbackup.DefaultSingleVersion, 69 | } 70 | 71 | found := false 72 | var lastClass outputClassification 73 | for _, v := range versions { 74 | for _, initiator := range []bool{true, false} { 75 | s := makeSingle(v, initiator) 76 | class, err := classifyOutputs(s, &tx) 77 | require.NoError(t, err) 78 | if class.toRemoteIdx >= 0 { 79 | found = true 80 | lastClass = class 81 | t.Logf("Matched with version=%v initiator=%v", 82 | v, initiator) 83 | 84 | break 85 | } 86 | } 87 | if found { 88 | break 89 | } 90 | } 91 | 92 | require.True(t, found, "to_remote output not identified for "+ 93 | "provided data") 94 | 95 | // Log the results. 96 | printOutputClassification(lastClass, &tx) 97 | 98 | // Verify the logged classification. 99 | h.assertLogContains("Output to_remote: idx=3 amount=790968 sat") 100 | h.assertLogContains("Possible anchor: idx=0 amount=330 sat") 101 | h.assertLogContains("Possible anchor: idx=1 amount=330 sat") 102 | h.assertLogContains("Possible to_local/htlc: idx=2 amount=8087 sat") 103 | } 104 | -------------------------------------------------------------------------------- /lnd/channel_test.go: -------------------------------------------------------------------------------- 1 | package lnd 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/btcsuite/btcd/btcutil/hdkeychain" 8 | "github.com/btcsuite/btcd/chaincfg" 9 | "github.com/btcsuite/btcd/chaincfg/chainhash" 10 | "github.com/btcsuite/btcd/wire" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | var ( 15 | rootKey = "tprv8ZgxMBicQKsPejNXQLJKe3dBBs9Zrt53EZrsBzVLQ8rZji3" + 16 | "hVb3wcoRvgrjvTmjPG2ixoGUUkCyC6yBEy9T5gbLdvD2a5VmJbcFd5Q9pkAs" 17 | 18 | staticRand = [32]byte{ 19 | 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 20 | 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 21 | 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 22 | 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 23 | } 24 | 25 | staticChanPoint = &wire.OutPoint{ 26 | Hash: chainhash.Hash{ 27 | 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 28 | }, 29 | Index: 123, 30 | } 31 | 32 | testNetParams = &chaincfg.TestNet3Params 33 | mainNetParams = &chaincfg.MainNetParams 34 | 35 | staticPubNonceHex = "0275757be33335347132895c3cf7c9d5d4c6dbfbc2b8090b" + 36 | "0c311929b5b3304629026f5183811ea44bd60110f9d4c30525bb1e8c72f9" + 37 | "19b766464e91db7739d4123a" 38 | ) 39 | 40 | func TestGenerateMuSig2Nonces(t *testing.T) { 41 | extendedKey, err := hdkeychain.NewKeyFromString(rootKey) 42 | require.NoError(t, err) 43 | 44 | staticNonces, err := GenerateMuSig2Nonces( 45 | extendedKey, staticRand, staticChanPoint, testNetParams, nil, 46 | ) 47 | require.NoError(t, err) 48 | 49 | require.Equal( 50 | t, staticPubNonceHex, 51 | hex.EncodeToString(staticNonces.PubNonce[:]), 52 | ) 53 | 54 | testCases := []struct { 55 | name string 56 | randomness [32]byte 57 | chanPoint *wire.OutPoint 58 | chainParams *chaincfg.Params 59 | pubNonce string 60 | }{{ 61 | name: "mainnet", 62 | randomness: staticRand, 63 | chanPoint: staticChanPoint, 64 | chainParams: mainNetParams, 65 | pubNonce: "02045795da7cffa1e2d8e64c4dfe606cd54b9d727e93f8c277" + 66 | "5b1d5442b80f605c024471a42dae0583f08262dcd09162d692bd" + 67 | "2ceb44178f37599c925b6465e92786", 68 | }, { 69 | name: "channel point", 70 | randomness: staticRand, 71 | chanPoint: &wire.OutPoint{ 72 | Hash: chainhash.Hash{ 73 | 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 74 | }, 75 | Index: 124, 76 | }, 77 | chainParams: testNetParams, 78 | pubNonce: "025c22d8bc5fd0605fa007db40977da4caff2d5312bd865b62" + 79 | "b7db6a184ea1b0d803278833480a3b7005cd1fad18c9a2740407" + 80 | "8a325d3c85f33b0370663d2943e44d", 81 | }, { 82 | name: "randomness", 83 | randomness: [32]byte{0x1}, 84 | chanPoint: staticChanPoint, 85 | chainParams: testNetParams, 86 | pubNonce: "02a0d0b3281e92130e64a454ad122b37c8fd771647eb442769" + 87 | "103b583db5d73753030ed0c6e04f4fb729b2db5a34331a4e5283" + 88 | "b3872004222c401a4b9ec6d0540f64", 89 | }} 90 | 91 | for idx := range testCases { 92 | tc := testCases[idx] 93 | 94 | t.Run(tc.name, func(t *testing.T) { 95 | nonces, err := GenerateMuSig2Nonces( 96 | extendedKey, tc.randomness, tc.chanPoint, 97 | tc.chainParams, nil, 98 | ) 99 | require.NoError(t, err) 100 | 101 | require.NotEqual( 102 | t, staticNonces.PubNonce, nonces.PubNonce, 103 | ) 104 | 105 | require.Equal( 106 | t, tc.pubNonce, 107 | hex.EncodeToString(nonces.PubNonce[:]), 108 | ) 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /cmd/chantools/sweepremoteclosed_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "encoding/json" 7 | "os" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/btcsuite/btcd/btcutil/hdkeychain" 12 | "github.com/btcsuite/btcd/chaincfg" 13 | "github.com/btcsuite/btcd/chaincfg/chainhash" 14 | "github.com/btcsuite/btcd/wire" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | var ( 19 | oldNodeRootKey = "tprv8ZgxMBicQKsPew1XiTmCmnZNSqTpy1eqSKg2ZjcFojcFtSM" + 20 | "bB5HUxQA5LyCfLgBB9aqWPqtPNLeMo3CR6uMASfbyQiUfTcAN6ECvE9F9qUy" 21 | ) 22 | 23 | func TestAncientKeyDerivation(t *testing.T) { 24 | chainParams = &chaincfg.RegressionNetParams 25 | 26 | jsonBytes, err := os.ReadFile("./sweepremoteclosed_ancient.json") 27 | require.NoError(t, err) 28 | 29 | var ancients []ancientChannel 30 | err = json.Unmarshal(jsonBytes, &ancients) 31 | require.NoError(t, err) 32 | 33 | oldRootKey, err := hdkeychain.NewKeyFromString(oldNodeRootKey) 34 | require.NoError(t, err) 35 | 36 | oldChans, err := findAncientChannels(ancients, 5, oldRootKey) 37 | require.NoError(t, err) 38 | 39 | require.NotEmpty(t, oldChans) 40 | } 41 | 42 | func unhex(t *testing.T, str string) []byte { 43 | t.Helper() 44 | 45 | str = strings.ReplaceAll(str, " ", "") 46 | b, err := hex.DecodeString(str) 47 | require.NoError(t, err) 48 | return b 49 | } 50 | 51 | func hash(t *testing.T, str string) chainhash.Hash { 52 | t.Helper() 53 | 54 | h, err := chainhash.NewHashFromStr(str) 55 | require.NoError(t, err) 56 | return *h 57 | } 58 | 59 | func TestCreateTx(t *testing.T) { 60 | tx := &wire.MsgTx{ 61 | Version: 2, 62 | TxIn: []*wire.TxIn{{ 63 | PreviousOutPoint: wire.OutPoint{ 64 | Hash: hash(t, "7fc0278c11f2ac4773caa3dcea56d"+ 65 | "2c7883b124c06588651216255e0f9c9a24f"), 66 | Index: 0, 67 | }, 68 | Sequence: 2157913601, 69 | Witness: [][]byte{ 70 | nil, 71 | unhex(t, "30 45 02 21 00 c0 88 6a 4c f4 77 24"+ 72 | " 68 45 50 c1 ec e8 94 29 fe bc c5 a3"+ 73 | " 0c f7 1a 7b a9 37 28 14 92 90 04 5d"+ 74 | " aa 02 20 2c 52 69 32 a1 da a7 84 58"+ 75 | " 95 b4 84 f7 46 1c 44 1e f7 b5 3c 59"+ 76 | " f3 9e a2 6a b0 5a b3 a6 be 53 0d 01"), 77 | unhex(t, "30 44 02 20 3b 00 4d 82 fe c6 3c fe"+ 78 | " ba 09 73 66 59 9b 25 c7 21 da 48 06"+ 79 | " 2c 86 12 09 9e 1b 15 e0 f3 49 a9 6c"+ 80 | " 02 20 1f 77 34 ce 2f 49 db f1 44 00"+ 81 | " 0b 9a 41 94 68 2a 86 b2 45 a9 68 74"+ 82 | " bf eb 5d 1a ce 83 ed 2a 46 06 01"), 83 | unhex(t, "52 21 02 34 99 0e f6 80 ff 3b e9 1e"+ 84 | " 86 2c 1b 5f 97 3c 7d 21 ef 3e a1 e4"+ 85 | " e9 40 f3 cb bb e3 dc 94 1b d3 61 21"+ 86 | " 02 ea 1f 0d f7 3d 6a 3a 8e c9 ae 81"+ 87 | " c7 5f 01 4b 2b b6 7c 17 36 e0 9b 70"+ 88 | " c7 4e a6 58 5f 55 9b 40 c7 52 ae"), 89 | }, 90 | }}, 91 | TxOut: []*wire.TxOut{{ 92 | Value: 15000, 93 | PkScript: unhex(t, "00 14 ac 56 01 6d 51 e9 fe d2 81 "+ 94 | "a1 48 7c 9c f1 74 26 14 f1 af 2e"), 95 | }, { 96 | Value: 4975950, 97 | PkScript: unhex(t, "00 20 d2 38 19 91 40 3a b2 aa 5a "+ 98 | "79 27 5a 29 d5 0e cf aa 53 4c ca 00 49 f9 "+ 99 | "cf fd ca 07 7c 17 4f af f1"), 100 | }}, 101 | LockTime: 553223869, 102 | } 103 | t.Logf("%v", tx.TxHash()) 104 | 105 | var buf bytes.Buffer 106 | err := tx.Serialize(&buf) 107 | require.NoError(t, err) 108 | t.Logf("%x", buf.Bytes()) 109 | } 110 | -------------------------------------------------------------------------------- /cmd/chantools/removechannel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/btcsuite/btcd/chaincfg/chainhash" 10 | "github.com/btcsuite/btcd/wire" 11 | "github.com/lightninglabs/chantools/lnd" 12 | "github.com/lightningnetwork/lnd/channeldb" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | type removeChannelCommand struct { 17 | ChannelDB string 18 | Channel string 19 | 20 | cmd *cobra.Command 21 | } 22 | 23 | func newRemoveChannelCommand() *cobra.Command { 24 | cc := &removeChannelCommand{} 25 | cc.cmd = &cobra.Command{ 26 | Use: "removechannel", 27 | Short: "Remove a single channel from the given channel DB", 28 | Long: `Opens the given channel DB in write mode and removes one 29 | single channel from it. This means giving up on any state (and therefore coins) 30 | of that channel and should only be used if the funding transaction of the 31 | channel was never confirmed on chain! 32 | 33 | CAUTION: Running this command will make it impossible to use the channel DB 34 | with an older version of lnd. Downgrading is not possible and you'll need to 35 | run lnd ` + lndVersion + ` or later after using this command!`, 36 | Example: `chantools removechannel \ 37 | --channeldb ~/.lnd/data/graph/mainnet/channel.db \ 38 | --channel 3149764effbe82718b280de425277e5e7b245a4573aa4a0203ac12cee1c37816:0`, 39 | RunE: cc.Execute, 40 | } 41 | cc.cmd.Flags().StringVar( 42 | &cc.ChannelDB, "channeldb", "", "lnd channel.backup file to "+ 43 | "remove the channel from", 44 | ) 45 | cc.cmd.Flags().StringVar( 46 | &cc.Channel, "channel", "", "channel to remove from the DB "+ 47 | "file, identified by its channel point "+ 48 | "(:)", 49 | ) 50 | 51 | return cc.cmd 52 | } 53 | 54 | func (c *removeChannelCommand) Execute(_ *cobra.Command, _ []string) error { 55 | // Check that we have a channel DB. 56 | if c.ChannelDB == "" { 57 | return errors.New("channel DB is required") 58 | } 59 | db, _, err := lnd.OpenDB(c.ChannelDB, false) 60 | if err != nil { 61 | return fmt.Errorf("error opening channel DB: %w", err) 62 | } 63 | defer func() { 64 | if err := db.Close(); err != nil { 65 | log.Errorf("Error closing DB: %w", err) 66 | } 67 | }() 68 | 69 | parts := strings.Split(c.Channel, ":") 70 | if len(parts) != 2 { 71 | return fmt.Errorf("invalid channel point format: %v", c.Channel) 72 | } 73 | hash, err := chainhash.NewHashFromStr(parts[0]) 74 | if err != nil { 75 | return err 76 | } 77 | index, err := strconv.ParseUint(parts[1], 10, 64) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | return removeChannel(db.ChannelStateDB(), &wire.OutPoint{ 83 | Hash: *hash, 84 | Index: uint32(index), 85 | }) 86 | } 87 | 88 | func removeChannel(db *channeldb.ChannelStateDB, 89 | chanPoint *wire.OutPoint) error { 90 | 91 | dbChan, err := db.FetchChannel(*chanPoint) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | if err := dbChan.MarkBorked(); err != nil { 97 | return err 98 | } 99 | 100 | // Abandoning a channel is a three step process: remove from the open 101 | // channel state, remove from the graph, remove from the contract 102 | // court. Between any step it's possible that the users restarts the 103 | // process all over again. As a result, each of the steps below are 104 | // intended to be idempotent. 105 | return db.AbandonChannel(chanPoint, uint32(100000)) 106 | } 107 | -------------------------------------------------------------------------------- /doc/chantools_sweepremoteclosed.md: -------------------------------------------------------------------------------- 1 | ## chantools sweepremoteclosed 2 | 3 | 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 4 | 5 | ### Synopsis 6 | 7 | This command helps users sweep funds that are in 8 | outputs of channels that were force-closed by the remote party. This command 9 | only needs to be used if no channel.backup file is available. By manually 10 | contacting the remote peers and asking them to force-close the channels, the 11 | funds can be swept after the force-close transaction was confirmed. 12 | 13 | Supported remote force-closed channel types are: 14 | - STATIC_REMOTE_KEY (a.k.a. tweakless channels) 15 | - ANCHOR (a.k.a. anchor output channels) 16 | - SIMPLE_TAPROOT (a.k.a. simple taproot channels) 17 | 18 | 19 | ``` 20 | chantools sweepremoteclosed [flags] 21 | ``` 22 | 23 | ### Examples 24 | 25 | ``` 26 | chantools sweepremoteclosed \ 27 | --recoverywindow 300 \ 28 | --feerate 20 \ 29 | --sweepaddr bc1q..... \ 30 | --publish 31 | ``` 32 | 33 | ### Options 34 | 35 | ``` 36 | --apiurl string API URL to use (must be esplora compatible) (default "https://api.node-recovery.com") 37 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 38 | --feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30) 39 | -h, --help help for sweepremoteclosed 40 | --hsm_secret string the hex encoded HSM secret to use for deriving the multisig keys for a CLN node; obtain by running 'xxd -p -c32 ~/.lightning/bitcoin/hsm_secret' 41 | --known_outputs string a comma separated list of known output addresses to use for matching against, instead of querying the API; can also be a file name to a file that contains the known outputs, one per line 42 | --peers string comma separated list of hex encoded public keys of the remote peers to recover funds from, only required when using --hsm_secret to derive the keys; can also be a file name to a file that contains the public keys, one per line 43 | --publish publish sweep TX to the chain API instead of just printing the TX 44 | --recoverywindow uint32 number of keys to scan per derivation path (default 200) 45 | --rootkey string BIP32 HD root key of the wallet to use for sweeping the wallet; leave empty to prompt for lnd 24 word aezeed 46 | --sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically 47 | --walletdb string read the seed/master root key to use for sweeping the wallet from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 48 | ``` 49 | 50 | ### Options inherited from parent commands 51 | 52 | ``` 53 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 54 | -r, --regtest Indicates if regtest parameters should be used 55 | --resultsdir string Directory where results should be stored (default "./results") 56 | -s, --signet Indicates if the public signet parameters should be used 57 | -t, --testnet Indicates if testnet parameters should be used 58 | ``` 59 | 60 | ### SEE ALSO 61 | 62 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 63 | 64 | -------------------------------------------------------------------------------- /doc/command-generator/src/components/CommandGenerator.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 120 | 121 | 127 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Simple bash script to build basic chantools for all the platforms 4 | # we support with the golang cross-compiler. 5 | # 6 | # Copyright (c) 2016 Company 0, LLC. 7 | # Use of this source code is governed by the ISC 8 | # license. 9 | 10 | set -e 11 | 12 | PKG="github.com/lightninglabs/chantools" 13 | DOCKER_USER="guggero" 14 | PACKAGE=chantools 15 | 16 | # green prints one line of green text (if the terminal supports it). 17 | function green() { 18 | echo -e "\e[0;32m${1}\e[0m" 19 | } 20 | 21 | # red prints one line of red text (if the terminal supports it). 22 | function red() { 23 | echo -e "\e[0;31m${1}\e[0m" 24 | } 25 | 26 | # build_release builds the actual release binaries. 27 | # arguments: 28 | function build_release() { 29 | local tag=$1 30 | local sys=$2 31 | local ldflags=$3 32 | 33 | green " - Packaging vendor" 34 | go mod vendor 35 | tar -czf vendor.tar.gz vendor 36 | 37 | maindir=$PACKAGE-$tag 38 | mkdir -p $maindir 39 | 40 | cp vendor.tar.gz $maindir/ 41 | rm vendor.tar.gz 42 | rm -r vendor 43 | 44 | package_source="${maindir}/${PACKAGE}-source-${tag}.tar" 45 | git archive -o "${package_source}" HEAD 46 | gzip -f "${package_source}" >"${package_source}.gz" 47 | 48 | cd "${maindir}" 49 | 50 | for i in $sys; do 51 | os=$(echo $i | cut -f1 -d-) 52 | arch=$(echo $i | cut -f2 -d-) 53 | arm= 54 | 55 | if [[ $arch == "armv6" ]]; then 56 | arch=arm 57 | arm=6 58 | elif [[ $arch == "armv7" ]]; then 59 | arch=arm 60 | arm=7 61 | fi 62 | 63 | dir="${PACKAGE}-${i}-${tag}" 64 | mkdir "${dir}" 65 | pushd "${dir}" 66 | 67 | green " - Building: ${os} ${arch} ${arm}" 68 | env CGO_ENABLED=0 GOOS=$os GOARCH=$arch GOARM=$arm go build -v -trimpath -ldflags="${ldflags}" ${PKG}/cmd/chantools 69 | popd 70 | 71 | if [[ $os == "windows" ]]; then 72 | zip -r "${dir}.zip" "${dir}" 73 | else 74 | tar -cvzf "${dir}.tar.gz" "${dir}" 75 | fi 76 | 77 | rm -r "${dir}" 78 | done 79 | 80 | shasum -a 256 * >manifest-$tag.txt 81 | } 82 | 83 | # docker_release builds the Docker images for all supported platforms. 84 | # arguments: 85 | function docker_release() { 86 | local tag=$1 87 | docker buildx build \ 88 | --platform linux/arm64,linux/amd64 \ 89 | --tag $DOCKER_USER/$PACKAGE:$tag \ 90 | --tag $DOCKER_USER/$PACKAGE:latest --output "type=registry" . 91 | 92 | docker buildx build \ 93 | -f docker/umbrel.Dockerfile \ 94 | --platform linux/arm64,linux/amd64 \ 95 | --build-arg VERSION=$tag \ 96 | --tag $DOCKER_USER/$PACKAGE:$tag-umbrel \ 97 | --tag $DOCKER_USER/$PACKAGE:latest-umbrel --output "type=registry" . 98 | } 99 | 100 | # usage prints the usage of the whole script. 101 | function usage() { 102 | red "Usage: " 103 | red "release.sh build-release " 104 | } 105 | 106 | # Whatever sub command is passed in, we need at least 2 arguments. 107 | if [ "$#" -lt 2 ]; then 108 | usage 109 | exit 1 110 | fi 111 | 112 | # Extract the sub command and remove it from the list of parameters by shifting 113 | # them to the left. 114 | SUBCOMMAND=$1 115 | shift 116 | 117 | # Call the function corresponding to the specified sub command or print the 118 | # usage if the sub command was not found. 119 | case $SUBCOMMAND in 120 | build-release) 121 | green "Building release" 122 | build_release "$@" 123 | ;; 124 | docker-release) 125 | green "Building docker release" 126 | docker_release "$@" 127 | ;; 128 | *) 129 | usage 130 | exit 1 131 | ;; 132 | esac 133 | -------------------------------------------------------------------------------- /doc/chantools_rescuefunding.md: -------------------------------------------------------------------------------- 1 | ## chantools rescuefunding 2 | 3 | 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 4 | 5 | ### Synopsis 6 | 7 | This is part 1 of a two phase process to rescue a channel 8 | funding output that was created on chain by accident but never resulted in a 9 | proper channel and no commitment transactions exist to spend the funds locked in 10 | the 2-of-2 multisig. 11 | 12 | **You need the cooperation of the channel partner (remote node) for this to 13 | work**! They need to run the second command of this process: signrescuefunding 14 | 15 | If successful, this will create a PSBT that then has to be sent to the channel 16 | partner (remote node operator). 17 | 18 | ``` 19 | chantools rescuefunding [flags] 20 | ``` 21 | 22 | ### Examples 23 | 24 | ``` 25 | chantools rescuefunding \ 26 | --channeldb ~/.lnd/data/graph/mainnet/channel.db \ 27 | --dbchannelpoint xxxxxxx:xx \ 28 | --sweepaddr bc1qxxxxxxxxx \ 29 | --feerate 10 30 | 31 | chantools rescuefunding \ 32 | --confirmedchannelpoint xxxxxxx:xx \ 33 | --localkeyindex x \ 34 | --remotepubkey 0xxxxxxxxxxxxxxxx \ 35 | --sweepaddr bc1qxxxxxxxxx \ 36 | --feerate 10 37 | ``` 38 | 39 | ### Options 40 | 41 | ``` 42 | --apiurl string API URL to use (must be esplora compatible) (default "https://api.node-recovery.com") 43 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 44 | --channeldb string lnd channel.db file to rescue a channel from; must contain the pending channel specified with --channelpoint 45 | --confirmedchannelpoint string channel outpoint that got confirmed on chain (:); normally this is the same as the --dbchannelpoint so it will be set to that value ifthis is left empty 46 | --dbchannelpoint string funding transaction outpoint of the channel to rescue (:) as it is recorded in the DB 47 | --feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30) 48 | -h, --help help for rescuefunding 49 | --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 50 | --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 51 | --rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed 52 | --sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically 53 | --walletdb string read the seed/master root key to use for deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 54 | ``` 55 | 56 | ### Options inherited from parent commands 57 | 58 | ``` 59 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 60 | -r, --regtest Indicates if regtest parameters should be used 61 | --resultsdir string Directory where results should be stored (default "./results") 62 | -s, --signet Indicates if the public signet parameters should be used 63 | -t, --testnet Indicates if testnet parameters should be used 64 | ``` 65 | 66 | ### SEE ALSO 67 | 68 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 69 | 70 | -------------------------------------------------------------------------------- /doc/command-generator/src/components/TriggerForceClose.vue: -------------------------------------------------------------------------------- 1 | 86 | 87 | 115 | -------------------------------------------------------------------------------- /cln/derivation.go: -------------------------------------------------------------------------------- 1 | package cln 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/binary" 6 | "fmt" 7 | 8 | "github.com/btcsuite/btcd/btcec/v2" 9 | "github.com/lightningnetwork/lnd/keychain" 10 | "golang.org/x/crypto/hkdf" 11 | ) 12 | 13 | const ( 14 | KeyOffsetFunding = 0 15 | KeyOffsetRevocation = 1 16 | KeyOffsetHtlc = 2 17 | KeyOffsetPayment = 3 18 | KeyOffsetDelayed = 4 19 | ) 20 | 21 | var ( 22 | InfoNodeID = []byte("nodeid") 23 | InfoPeerSeed = []byte("peer seed") 24 | InfoPerPeer = []byte("per-peer seed") 25 | InfoCLightning = []byte("c-lightning") 26 | ) 27 | 28 | // NodeKey derives a CLN node key from the given HSM secret. 29 | func NodeKey(hsmSecret [32]byte) (*btcec.PublicKey, *btcec.PrivateKey, error) { 30 | salt := make([]byte, 4) 31 | privKeyBytes, err := HkdfSha256(hsmSecret[:], salt, InfoNodeID) 32 | if err != nil { 33 | return nil, nil, err 34 | } 35 | 36 | privKey, pubKey := btcec.PrivKeyFromBytes(privKeyBytes[:]) 37 | return pubKey, privKey, nil 38 | } 39 | 40 | // DeriveKeyPair derives a channel key pair from the given HSM secret, and the 41 | // key descriptor. The public key in the key descriptor is used as the peer's 42 | // public key, the family is converted to the CLN key type, and the index is 43 | // used as the channel's database index. 44 | func DeriveKeyPair(hsmSecret [32]byte, 45 | desc *keychain.KeyDescriptor) (*btcec.PublicKey, *btcec.PrivateKey, 46 | error) { 47 | 48 | var offset int 49 | switch desc.Family { 50 | case keychain.KeyFamilyMultiSig: 51 | offset = KeyOffsetFunding 52 | 53 | case keychain.KeyFamilyRevocationBase: 54 | offset = KeyOffsetRevocation 55 | 56 | case keychain.KeyFamilyHtlcBase: 57 | offset = KeyOffsetHtlc 58 | 59 | case keychain.KeyFamilyPaymentBase: 60 | offset = KeyOffsetPayment 61 | 62 | case keychain.KeyFamilyDelayBase: 63 | offset = KeyOffsetDelayed 64 | 65 | case keychain.KeyFamilyNodeKey: 66 | return NodeKey(hsmSecret) 67 | 68 | default: 69 | return nil, nil, fmt.Errorf("unsupported key family for CLN: "+ 70 | "%v", desc.Family) 71 | } 72 | 73 | channelBase, err := HkdfSha256(hsmSecret[:], nil, InfoPeerSeed) 74 | if err != nil { 75 | return nil, nil, err 76 | } 77 | 78 | peerAndChannel := make([]byte, 33+8) 79 | copy(peerAndChannel[:33], desc.PubKey.SerializeCompressed()) 80 | binary.LittleEndian.PutUint64(peerAndChannel[33:], uint64(desc.Index)) 81 | 82 | channelSeed, err := HkdfSha256( 83 | channelBase[:], peerAndChannel, InfoPerPeer, 84 | ) 85 | if err != nil { 86 | return nil, nil, err 87 | } 88 | 89 | fundingKey, err := HkdfSha256WithSkip( 90 | channelSeed[:], nil, InfoCLightning, offset*32, 91 | ) 92 | if err != nil { 93 | return nil, nil, err 94 | } 95 | 96 | privKey, pubKey := btcec.PrivKeyFromBytes(fundingKey[:]) 97 | return pubKey, privKey, nil 98 | } 99 | 100 | // HkdfSha256 derives a 32-byte key from the given input key material, salt, and 101 | // info using the HKDF-SHA256 key derivation function. 102 | func HkdfSha256(key, salt, info []byte) ([32]byte, error) { 103 | return HkdfSha256WithSkip(key, salt, info, 0) 104 | } 105 | 106 | // HkdfSha256WithSkip derives a 32-byte key from the given input key material, 107 | // salt, and info using the HKDF-SHA256 key derivation function and skips the 108 | // first `skip` bytes of the output. 109 | func HkdfSha256WithSkip(key, salt, info []byte, skip int) ([32]byte, error) { 110 | expander := hkdf.New(sha256.New, key, salt, info) 111 | 112 | if skip > 0 { 113 | skippedBytes := make([]byte, skip) 114 | _, err := expander.Read(skippedBytes) 115 | if err != nil { 116 | return [32]byte{}, err 117 | } 118 | } 119 | 120 | var outputKey [32]byte 121 | _, err := expander.Read(outputKey[:]) 122 | if err != nil { 123 | return [32]byte{}, err 124 | } 125 | 126 | return outputKey, nil 127 | } 128 | -------------------------------------------------------------------------------- /doc/zombierecovery-flow.drawio: -------------------------------------------------------------------------------- 1 | 7V1bc+K4Ev41VM0+TArf4TEwM7tbNTknO5mqPfu0JWwBPjEWJSsh7K9fSb5gWzIWMUYk8dQUwW1ZNupPn1qtbnlkzTcvv2KwXd+hAEYjcxy8jKwvI9OcOC79ZIJ9KvCm01SwwmGQioyD4CH8B2bCcSZ9CgOYVAoShCISbqtCH8Ux9ElFBjBGu2qxJYqqd92CFRQEDz6IROmfYUDW+c8aH+S/wXC1zu9sjLMzG5AXzgTJGgRoVxJZX0fWHCNE0m+blzmMWNvl7ZJe963hbPFgGMZE5YJdCL/dff/t7x8mwkuUPAfPf1ifzbSWZxA9ZT/49zgkISAIs8ohZH9GJlWiRTVizT5lIuOX7EeRfd5SyS7cRCCmR7OEAEwyXVrssggsYDQD/uMKo6c4mKOI1m99iREvvgyjKBfRGwUOnAQ2rwajR1g6MzEXluvSM9lTQ0zgS2NzGEUjU3BCtIEE72mR7IICYRkwDTc73h3UbEwz2bqk4ukkE4IMWqui7kPr0y+ZAk5QxlRoUxhQMGaHCJM1WqEYRF8P0hlvT8hqZe18KPMdoS0VGlT4f0jIPtMGeCKIitZkE2Vn03uyGzU25DHkJOgJ+/BIOSfrtACv4LH6PLm2MIwACZ+rD3f2lneEbvDAICzoY4vCmPCbOzP6f3xj2PTSefF35NBSc34mPa7KJo68tMGl9Rry0tnfWt1GTZY/iVC6Wjf9TzvWGmzZL9q8rBhp3yy2m/gGPnOFl7HBOlhIufA76773KKHMgGJ6boEIQZvmfk276pL/K9VxG4Urdi1huJyB7Min96SEYs22EIdUl+z7F6owSu/w/iCaoScShTHlgpzlGdpBsk0PluEL6wJ5KSqhDBQHADNhst8sEPtFKxhDDCJV+mhGfSOnWE6FUmyRUSwJoVh98YknoPoHXIUJ4QwOKLrHMR2uP2PoI9oW+3a0mzJQy2SeKEwxbrYDP5V5dcx7MtDLZKZEKK1S1ulqD9ncWwhIHum53Tok8GELOAXu6GlGyRSUOWkX9DxLUgLmoyi7+A7gR472BMZBj5isD3OGCMqJBJRuX6A07fc4yhkTxWHOtHSOc/ljVijBh+y+7BFyHhhTiwwOdKCFDnCmj4sxQjH+aGMEcRLyQOCWzTLozCNiNsmCjljuin379N8tM0JAJJmA8OlCPkszqwRQ19oyQjt/TW28GxDHiABW6d9myTLB6S9PTZyS7RPBJRPzzgDxV2Y0JS3m0iYMAk5jdVuoONGXqj+7VV1LLBJbour+dG0ZOth/ieJ8OmpcYjRISV5lNJjoHA3yx6yMBs8h3EkGA+s2m/WP52vaY7iTJ6RaoWoxxwEE9D7fSn4Ck5V8gPDgRPCpDoBP+GVLNAwuWgaXp4TPpi40sliKtua0N7bRYmtenG1UbU/b1so2ou35e0zn6fxKRirMr5sMxPABiME21YjB6YsYbOsjEIOlaobYDdpTJgZ+6S3GYF8qkPXcQ833TFAChV1FhePWFjBq5d2jxemX9AEOqCh+SYcRRDSR7jFkGmLtumafj3CfDFNmrR40H4db0it91fhrqsZfvU2jbHHKPC/s68zgNm/oxy2hwnWB1W2K3aCEWVbqHqMtSlgBwKTcbMeAcEHMDHy/sPnpgE3YNQPQdQC9Z1dxdfnCmYgoN1xHhLnXF8ydbsO08bphOgDJurieHdwDQiCOuYQ5pS4/ktu24khuaXUv549ZoqVfIaOkRUh8yhDsnkGAYTKY+e92pLyugdI1BaRdgEEuTQ/OVJEe3LFcexeKspgK9HBYfULLJTNbBlrQQAsXX3PyJAsRMl7oLdTKda+BF+BLSP7H/AkUA+nRX1lJ9v3LS+Zq4Af70kEpMobL+uaXnJ7b+cXpyC+vdCRUDdeJdZIjoVa8H0dCfs/yjG0N/cdiipWwJUHaxBjECZ3HsZXDgQzfp42kQIbeJcnQ67Yoe2YyNEpUeCDGKyJD1YhWt+vi7uvIsDaLn0yOk2G9/LQeRl4rP7GOFe+JPB2BPCn+QOCDhAyceQWc2ffyUQ3Sjpr12N+s8lgY3ZK2FnOuothfg3BApB5EgkVCacEvIriak0+kc/XeZ0BTyaBf2LEXiQ4XV7b+U40GL4J5bny0EWB8LN1HqQ2Pp+e4VrW9pOk50mh6o68Gy1nobUQu567z1sjlPKuuzZwxtEaP5I/ZELnM0howD+IcXMtv14fUsHiRZ9fUpk2ajQDDe5d0oLrSZLha6UBcaXrwAXOgLHnm6j9oswgZNWQr1wMrvFlnykmkYOieGhiuxKwi4ZI9MgvMGID4RkMnToKh9pwakR5/wA0iRTz+3BzdjiV5/eZpef31zH0AJ0t/JMncd/0JXCzPMzWwzao7QDo1kMaZ9+ZBzeccb8MWyKPiWz2diqaA1iQWkW+H1P3RkLrfgPpmAq9yiu7UfZmDcUjdf3dGxXFMXlvqvjXRMcqdK5hKddQzVIOprK7BDt1MjmPBVEMq/zXQwzlcYicxhPZph6VlC6tLM0RuK7QzhFYfWf6YQ3r3RyGczsvwJ7GN9vTu3Eh/52yjOg23tG6hZ4kT8SG9+2MSg/b0bqdbQOOZiEEhj6x37vAUuSNVhTbuEHcqHLKsr41DzrCUdxqL6M4dc8xOLHL92aeqLGKrbjCjl0XsY1GZbFaCN2CIEf443hBZ/rqMQPpLX+/mLz1XXkVMf0uaZeY5+fFfmZnDDw65FfxoXz46V3aFKtfkNmE713QNR3xddkVtncht2bPGqAUGuJfYtCZvwxIR3oHHIcn23dtP7fwnyyvrj/8kQXqQJztmOBx/un+Y/WS+REpihStRnLwNAH1/q5muJH63iOS5TBbPsRW0IYvnGiCpIYvnJBBLs3hkIO4tUMToNkl9pavrYFNWDMobpyeTMh8S2qxHVXeX0RBheJrxKFh79qQKD2tcU3v6YNlVB82faoUWQ3Z+H/u4FerUn+vE8uYlrFZDy56cF8w6VwWxqrflPCA+FXsClsZXiKXinWuaWPGm+0T7sDfMeGxXwOlavewQowpPQ3U90tCy/4Fb36uzZTMYx+5W3mrxAAhcfRH8a1kA6wf/10PM6sifDsin9Tk6kC9O6rKXyYiG8nt/YczxNBqjph5bMiGXvRymv+BB0W2ZqU60DAfVVTx9iu/16S8Sy2hSnfgGjkF1FTeYoup6S12zxdiLTHXOiL/6F2x41g//pBJx48RBoWWFepIYbJlCjf5yP99KELaq3ZXvdt/+Hgu9mYjdjO5XLvee1ejuUYn5YlD7wm5X47mbEsVtkTI69AoujBfJtmicgQgbiHA61myUuI0jmxiXMaiurDpjrKi7/rYk7bascbl921XpT33XUK3054n095PH337iS/Yw+GWUvQOG1ZTwX76Bn8X3tJJ1GK9G2cI+WGGeK4T4DseINsAaYvbimYcw9tmJGIZMNCpiAB5jtGO1p8G/wCdPIDrc+Zt4vwWIAK9rzmCMnjh2+e9dA76gW3kQfkAfMak+Tx1xaTL7SL6UWcJND2uTxzusW19Ml/RX6RYYRn1F6nzIEYM9xAaNg1uM0e7QNpVo11oUbDXw1Rrl24qUWbra5V/ff1u7pdew/0upwR1Je+eyjguL1rQ2yahPHlL+ERYWRZ9czYfn1XdLPNMKZZECUpsVNW7JXi9fXVXqx1foiQmSbwiy5wul1IttrxYiOalDUhXb3qSlkzRgux1O9BAjRMrFmSF4hwLISvwL -------------------------------------------------------------------------------- /cmd/chantools/root_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "path" 8 | "regexp" 9 | "testing" 10 | 11 | "github.com/btcsuite/btcd/chaincfg" 12 | "github.com/btcsuite/btclog/v2" 13 | "github.com/lightningnetwork/lnd/chanbackup" 14 | "github.com/lightningnetwork/lnd/channeldb" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | const ( 19 | seedAezeedNoPassphrase = "abandon kangaroo tribe spell brass entry " + 20 | "argue buzz muffin total rug title autumn wish use bubble " + 21 | "alarm rent machine hockey fork slam gaze tobacco" 22 | seedAezeedWithPassphrase = "able pause keen exhibit duck olympic " + 23 | "foot donor hire omit earth ribbon rotate cruise door orbit " + 24 | "nephew mixture machine hockey fork scorpion shell door" 25 | testPassPhrase = "testnet3" 26 | seedBip39 = "uncover bargain diesel boss local host over divide " + 27 | "orient cradle good crumble" 28 | 29 | rootKeyAezeed = "tprv8ZgxMBicQKsPejNXQLJKe3dBBs9Zrt53EZrsBzVLQ8rZji3" + 30 | "hVb3wcoRvgrjvTmjPG2ixoGUUkCyC6yBEy9T5gbLdvD2a5VmJbcFd5Q9pkAs" 31 | rootKeyBip39 = "tprv8ZgxMBicQKsPdoVEZRN2MyzEgxGTqJepzhMc66b26zL1siLi" + 32 | "WRQAGh9rAgPPJuQeHWWpgcDcS45yi6KBTFeGkQMEb2RNTrP11evJcB4UVSh" 33 | rootKeyBip39Passphrase = "" 34 | ) 35 | 36 | var ( 37 | datePattern = regexp.MustCompile( 38 | `\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} `, 39 | ) 40 | addressPattern = regexp.MustCompile(`\(0x[0-9a-f]{10}\)`) 41 | ) 42 | 43 | type harness struct { 44 | t *testing.T 45 | logBuffer *bytes.Buffer 46 | logger btclog.Logger 47 | tempDir string 48 | } 49 | 50 | func newHarness(t *testing.T) *harness { 51 | t.Helper() 52 | 53 | buf := &bytes.Buffer{} 54 | logBackend := btclog.NewDefaultHandler(buf) 55 | 56 | h := &harness{ 57 | t: t, 58 | logBuffer: buf, 59 | logger: btclog.NewSLogger(logBackend.SubSystem("CHAN")), 60 | tempDir: t.TempDir(), 61 | } 62 | 63 | h.logger.SetLevel(btclog.LevelTrace) 64 | log = h.logger 65 | channeldb.UseLogger(h.logger) 66 | chanbackup.UseLogger(h.logger) 67 | 68 | os.Clearenv() 69 | chainParams = &chaincfg.RegressionNetParams 70 | 71 | return h 72 | } 73 | 74 | func (h *harness) getLog() string { 75 | return h.logBuffer.String() 76 | } 77 | 78 | func (h *harness) clearLog() { 79 | h.logBuffer.Reset() 80 | } 81 | 82 | func (h *harness) assertLogContains(format string) { 83 | h.t.Helper() 84 | 85 | require.Contains(h.t, h.logBuffer.String(), format) 86 | } 87 | 88 | func (h *harness) assertLogEqual(a, b string) { 89 | // Remove all timestamps and all memory addresses from dumps as those 90 | // are always different. 91 | a = datePattern.ReplaceAllString(a, "") 92 | a = addressPattern.ReplaceAllString(a, "") 93 | 94 | b = datePattern.ReplaceAllString(b, "") 95 | b = addressPattern.ReplaceAllString(b, "") 96 | 97 | require.Equal(h.t, a, b) 98 | } 99 | 100 | func (h *harness) testdataFile(name string) string { 101 | workingDir, err := os.Getwd() 102 | require.NoError(h.t, err) 103 | 104 | origFile := path.Join(workingDir, "testdata", name) 105 | 106 | fileCopy := path.Join(h.t.TempDir(), name) 107 | 108 | src, err := os.Open(origFile) 109 | require.NoError(h.t, err) 110 | defer src.Close() 111 | dst, err := os.Create(fileCopy) 112 | require.NoError(h.t, err) 113 | defer dst.Close() 114 | _, err = io.Copy(dst, src) 115 | require.NoError(h.t, err) 116 | 117 | return fileCopy 118 | } 119 | 120 | func (h *harness) readTestdataFile(name string) []byte { 121 | workingDir, err := os.Getwd() 122 | require.NoError(h.t, err) 123 | 124 | origFile := path.Join(workingDir, "testdata", name) 125 | 126 | data, err := os.ReadFile(origFile) 127 | require.NoError(h.t, err) 128 | 129 | return data 130 | } 131 | 132 | func (h *harness) tempFile(name string) string { 133 | return path.Join(h.tempDir, name) 134 | } 135 | 136 | func (h *harness) fileSize(name string) int64 { 137 | stat, err := os.Stat(name) 138 | require.NoError(h.t, err) 139 | 140 | return stat.Size() 141 | } 142 | -------------------------------------------------------------------------------- /scbforceclose/sign_close_tx_test.go: -------------------------------------------------------------------------------- 1 | package scbforceclose 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "encoding/hex" 7 | "encoding/json" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/btcsuite/btcd/btcutil/hdkeychain" 12 | "github.com/btcsuite/btcd/chaincfg" 13 | "github.com/btcsuite/btcd/txscript" 14 | "github.com/lightninglabs/chantools/lnd" 15 | "github.com/lightningnetwork/lnd/aezeed" 16 | "github.com/lightningnetwork/lnd/chanbackup" 17 | "github.com/lightningnetwork/lnd/input" 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | //go:embed testdata/channel_backups.json 22 | var channelBackupsJSON []byte 23 | 24 | // TestSignCloseTx tests that SignCloseTx produces valid transactions. 25 | func TestSignCloseTx(t *testing.T) { 26 | // Load prepared channel backups with seeds and passwords. 27 | type TestCase struct { 28 | Name string `json:"name"` 29 | RootKey string `json:"rootkey"` 30 | Password string `json:"password"` 31 | Mnemonic string `json:"mnemonic"` 32 | Single bool `json:"single"` 33 | ChannelBackup string `json:"channel_backup"` 34 | PkScript string `json:"pk_script"` 35 | AmountSats int64 `json:"amount_sats"` 36 | } 37 | 38 | var testdata struct { 39 | Cases []TestCase `json:"cases"` 40 | } 41 | require.NoError(t, json.Unmarshal(channelBackupsJSON, &testdata)) 42 | 43 | chainParams := &chaincfg.RegressionNetParams 44 | 45 | for _, tc := range testdata.Cases { 46 | t.Run(tc.Name, func(t *testing.T) { 47 | var extendedKey *hdkeychain.ExtendedKey 48 | if tc.RootKey != "" { 49 | // Parse root key. 50 | var err error 51 | extendedKey, err = hdkeychain.NewKeyFromString( 52 | tc.RootKey, 53 | ) 54 | require.NoError(t, err) 55 | } else { 56 | // Generate root key from seed and password. 57 | words := strings.Split(tc.Mnemonic, " ") 58 | require.Len(t, words, 24) 59 | var mnemonic aezeed.Mnemonic 60 | copy(mnemonic[:], words) 61 | cipherSeed, err := mnemonic.ToCipherSeed( 62 | []byte(tc.Password), 63 | ) 64 | require.NoError(t, err) 65 | extendedKey, err = hdkeychain.NewMaster( 66 | cipherSeed.Entropy[:], chainParams, 67 | ) 68 | require.NoError(t, err) 69 | } 70 | 71 | // Make key ring and signer. 72 | keyRing := &lnd.HDKeyRing{ 73 | ExtendedKey: extendedKey, 74 | ChainParams: chainParams, 75 | } 76 | 77 | signer := &lnd.Signer{ 78 | ExtendedKey: extendedKey, 79 | ChainParams: chainParams, 80 | } 81 | musigSessionManager := input.NewMusigSessionManager( 82 | signer.FetchPrivateKey, 83 | ) 84 | signer.MusigSessionManager = musigSessionManager 85 | 86 | // Unpack channel.backup. 87 | backup, err := hex.DecodeString( 88 | tc.ChannelBackup, 89 | ) 90 | require.NoError(t, err) 91 | r := bytes.NewReader(backup) 92 | 93 | var s chanbackup.Single 94 | if tc.Single { 95 | err := s.UnpackFromReader(r, keyRing) 96 | require.NoError(t, err) 97 | } else { 98 | var m chanbackup.Multi 99 | err := m.UnpackFromReader(r, keyRing) 100 | require.NoError(t, err) 101 | 102 | // Extract a single channel backup from 103 | // multi backup. 104 | require.Len(t, m.StaticBackups, 1) 105 | s = m.StaticBackups[0] 106 | } 107 | 108 | // Sign force close transaction. 109 | sweepTx, err := SignCloseTx( 110 | s, keyRing, signer, signer, 111 | ) 112 | require.NoError(t, err) 113 | 114 | // Check if the transaction is valid. 115 | pkScript, err := hex.DecodeString(tc.PkScript) 116 | require.NoError(t, err) 117 | fetcher := txscript.NewCannedPrevOutputFetcher( 118 | pkScript, tc.AmountSats, 119 | ) 120 | 121 | sigHashes := txscript.NewTxSigHashes(sweepTx, fetcher) 122 | 123 | vm, err := txscript.NewEngine( 124 | pkScript, sweepTx, 0, 125 | txscript.StandardVerifyFlags, 126 | nil, sigHashes, tc.AmountSats, fetcher, 127 | ) 128 | require.NoError(t, err) 129 | 130 | require.NoError(t, vm.Execute()) 131 | }) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /doc/chantools_genimportscript.md: -------------------------------------------------------------------------------- 1 | ## chantools genimportscript 2 | 3 | Generate a script containing the on-chain keys of an lnd wallet that can be imported into other software like bitcoind 4 | 5 | ### Synopsis 6 | 7 | Generates a script that contains all on-chain private (or 8 | public) keys derived from an lnd 24 word aezeed wallet. That script can then be 9 | imported into other software like bitcoind. 10 | 11 | The following script formats are currently supported: 12 | * bitcoin-cli: Creates a list of bitcoin-cli importprivkey commands that can 13 | be used in combination with a bitcoind full node to recover the funds locked 14 | in those private keys. NOTE: This will only work for legacy wallets and only 15 | for legacy, p2sh-segwit and bech32 (p2pkh, np2wkh and p2wkh) addresses. Use 16 | bitcoin-descriptors and a descriptor wallet for bech32m (p2tr). 17 | * bitcoin-cli-watchonly: Does the same as bitcoin-cli but with the 18 | bitcoin-cli importpubkey command. That means, only the public keys are 19 | imported into bitcoind to watch the UTXOs of those keys. The funds cannot be 20 | spent that way as they are watch-only. 21 | * bitcoin-importwallet: Creates a text output that is compatible with 22 | bitcoind's importwallet command. 23 | * electrum: Creates a text output that contains one private key per line with 24 | the address type as the prefix, the way Electrum expects them. 25 | * bitcoin-descriptors: Create a list of bitcoin-cli importdescriptors commands 26 | that can be used in combination with a bitcoind full node that has a 27 | descriptor wallet to recover the funds locked in those private keys. 28 | NOTE: This will only work for descriptor wallets and only for 29 | p2sh-segwit, bech32 and bech32m (np2wkh, p2wkh and p2tr) addresses. 30 | 31 | ``` 32 | chantools genimportscript [flags] 33 | ``` 34 | 35 | ### Examples 36 | 37 | ``` 38 | chantools genimportscript --format bitcoin-cli \ 39 | --recoverywindow 5000 40 | ``` 41 | 42 | ### Options 43 | 44 | ``` 45 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 46 | --derivationpath string use one specific derivation path; specify the first levels of the derivation path before any internal/external branch; Cannot be used in conjunction with --lndpaths 47 | --format string format of the generated import script; currently supported are: bitcoin-importwallet, bitcoin-cli, bitcoin-cli-watchonly, bitcoin-descriptors and electrum (default "bitcoin-importwallet") 48 | -h, --help help for genimportscript 49 | --lndpaths use all derivation paths that lnd used; results in a large number of results; cannot be used in conjunction with --derivationpath 50 | --recoverywindow uint32 number of keys to scan per internal/external branch; output will consist of double this amount of keys (default 2500) 51 | --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) 52 | --rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed 53 | --stdout write generated import script to standard out instead of writing it to a file 54 | --walletdb string read the seed/master root key to use for decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 55 | ``` 56 | 57 | ### Options inherited from parent commands 58 | 59 | ``` 60 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 61 | -r, --regtest Indicates if regtest parameters should be used 62 | --resultsdir string Directory where results should be stored (default "./results") 63 | -s, --signet Indicates if the public signet parameters should be used 64 | -t, --testnet Indicates if testnet parameters should be used 65 | ``` 66 | 67 | ### SEE ALSO 68 | 69 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 70 | 71 | -------------------------------------------------------------------------------- /doc/command-generator/src/components/CommandSelector.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 89 | 90 | 122 | -------------------------------------------------------------------------------- /itest/chantools_harness.go: -------------------------------------------------------------------------------- 1 | package itest 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "io" 8 | "io/fs" 9 | "os" 10 | "os/exec" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | // ChantoolsProcess wraps a running chantools process for integration testing. 19 | type ChantoolsProcess struct { 20 | cmd *exec.Cmd 21 | stdin io.WriteCloser 22 | stdout *os.File 23 | stdoutReader *bufio.Reader 24 | stderr *bufio.Reader 25 | } 26 | 27 | // StartChantools starts the chantools binary with the given arguments. 28 | func StartChantools(t *testing.T, args ...string) *ChantoolsProcess { 29 | t.Helper() 30 | 31 | log.Debugf("Calling chantools with args: %v", args) 32 | 33 | args = append([]string{"--nologfile"}, args...) 34 | cmd := exec.Command("chantools", args...) 35 | stdin, err := cmd.StdinPipe() 36 | require.NoError(t, err) 37 | 38 | stdoutPipe, err := cmd.StdoutPipe() 39 | require.NoError(t, err) 40 | stderrPipe, err := cmd.StderrPipe() 41 | require.NoError(t, err) 42 | 43 | stdoutFile, ok := stdoutPipe.(*os.File) 44 | require.True(t, ok) 45 | 46 | require.NoError(t, cmd.Start()) 47 | 48 | proc := &ChantoolsProcess{ 49 | cmd: cmd, 50 | stdin: stdin, 51 | stdout: stdoutFile, 52 | stdoutReader: bufio.NewReader(stdoutFile), 53 | stderr: bufio.NewReader(stderrPipe), 54 | } 55 | 56 | proc.AssertNoStderr(t) 57 | return proc 58 | } 59 | 60 | func (p *ChantoolsProcess) AssertNoStderr(t *testing.T) { 61 | t.Helper() 62 | 63 | require.Never(t, func() bool { 64 | stderrBytes, err := io.ReadAll(p.stderr) 65 | if isProcessExitErr(err) { 66 | return false 67 | } 68 | require.NoError(t, err) 69 | 70 | if len(stderrBytes) > 0 { 71 | t.Logf("Stderr has unexpected output: %s", stderrBytes) 72 | return true 73 | } 74 | 75 | return false 76 | }, shortTimeout, testTick) 77 | } 78 | 79 | // WriteInput writes input to the process's stdin. 80 | func (p *ChantoolsProcess) WriteInput(t *testing.T, input string) { 81 | t.Helper() 82 | 83 | _, err := io.WriteString(p.stdin, input) 84 | require.NoError(t, err, "failed to write input to chantools") 85 | } 86 | 87 | // ReadAllOutput reads all output from the process's stdout until EOF. 88 | func (p *ChantoolsProcess) ReadAllOutput(t *testing.T) string { 89 | t.Helper() 90 | 91 | resp, err := io.ReadAll(p.stdout) 92 | require.NoError(t, err, "failed to read chantools output") 93 | 94 | log.Debugf("[CHANTOOLS]: %s", strings.TrimSpace(string(resp))) 95 | 96 | return string(resp) 97 | } 98 | 99 | // ReadAvailableOutput reads as many bytes as possible from stdout until the 100 | // timeout elapses. 101 | func (p *ChantoolsProcess) ReadAvailableOutput(t *testing.T, 102 | timeout time.Duration) string { 103 | 104 | t.Helper() 105 | 106 | err := p.stdout.SetReadDeadline(time.Now().Add(timeout)) 107 | require.NoError(t, err, "failed to set read deadline") 108 | 109 | defer func() { 110 | _ = p.stdout.SetReadDeadline(time.Time{}) 111 | }() 112 | 113 | var out bytes.Buffer 114 | for { 115 | buf := make([]byte, 1024) 116 | n, err := p.stdoutReader.Read(buf) 117 | if n > 0 { 118 | chunk := string(buf[:n]) 119 | out.WriteString(chunk) 120 | } 121 | if err != nil { 122 | if errors.Is(err, io.EOF) || 123 | errors.Is(err, os.ErrDeadlineExceeded) { 124 | 125 | break 126 | } 127 | 128 | time.Sleep(50 * time.Millisecond) 129 | } 130 | } 131 | 132 | log.Debugf("[CHANTOOLS]: %s", strings.TrimSpace(out.String())) 133 | return out.String() 134 | } 135 | 136 | // Wait waits for the process to exit. 137 | func (p *ChantoolsProcess) Wait(t *testing.T) { 138 | t.Helper() 139 | 140 | require.NoError(t, p.cmd.Wait()) 141 | } 142 | 143 | // Kill kills the process. 144 | func (p *ChantoolsProcess) Kill(t *testing.T) { 145 | t.Helper() 146 | 147 | require.NoError(t, p.cmd.Process.Kill()) 148 | } 149 | 150 | func isProcessExitErr(err error) bool { 151 | var pathError *fs.PathError 152 | if err != nil && errors.As(err, &pathError) { 153 | return errors.Is(pathError.Err, fs.ErrClosed) 154 | } 155 | 156 | return false 157 | } 158 | -------------------------------------------------------------------------------- /doc/chantools_fakechanbackup.md: -------------------------------------------------------------------------------- 1 | ## chantools fakechanbackup 2 | 3 | Fake a channel backup file to attempt fund recovery 4 | 5 | ### Synopsis 6 | 7 | If for any reason a node suffers from data loss and there is no 8 | channel.backup for one or more channels, then the funds in the channel would 9 | theoretically be lost forever. 10 | If the remote node is still online and still knows about the channel, there is 11 | hope. We can initiate DLP (Data Loss Protocol) and ask the remote node to 12 | force-close the channel and to provide us with the per_commit_point that is 13 | needed to derive the private key for our part of the force-close transaction 14 | output. But to initiate DLP, we would need to have a channel.backup file. 15 | Fortunately, if we have enough information about the channel, we can create a 16 | faked/skeleton channel.backup file that at least lets us talk to the other node 17 | and ask them to do their part. Then we can later brute-force the private key for 18 | the transaction output of our part of the funds (see rescueclosed command). 19 | 20 | There are two versions of this command: The first one is to create a fake 21 | backup for a single channel where all flags (except --from_channel_graph) need 22 | to be set. This is the easiest to use since it only relies on data that is 23 | publicly available (for example on 1ml.com) but involves more manual work. 24 | The second version of the command only takes the --from_channel_graph and 25 | --multi_file flags and tries to assemble all channels found in the public 26 | network graph (must be provided in the JSON format that the 27 | 'lncli describegraph' command returns) into a fake backup file. This is the 28 | most convenient way to use this command but requires one to have a fully synced 29 | lnd node. 30 | 31 | Any fake channel backup _needs_ to be used with the custom fork of lnd 32 | specifically built for this purpose: https://github.com/guggero/lnd/releases 33 | Also the debuglevel must be set to debug (lnd.conf, set 'debuglevel=debug') when 34 | running the above lnd for it to produce the correct log file that will be needed 35 | for the rescueclosed command. 36 | 37 | 38 | ``` 39 | chantools fakechanbackup [flags] 40 | ``` 41 | 42 | ### Examples 43 | 44 | ``` 45 | chantools fakechanbackup \ 46 | --capacity 123456 \ 47 | --channelpoint f39310xxxxxxxxxx:1 \ 48 | --remote_node_addr 022c260xxxxxxxx@213.174.150.1:9735 \ 49 | --short_channel_id 566222x300x1 \ 50 | --multi_file fake.backup 51 | 52 | chantools fakechanbackup --from_channel_graph lncli_describegraph.json \ 53 | --multi_file fake.backup 54 | ``` 55 | 56 | ### Options 57 | 58 | ``` 59 | --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag 60 | --capacity uint the channel's capacity in satoshis 61 | --channelpoint string funding transaction outpoint of the channel to rescue (:) as it is displayed on 1ml.com 62 | --from_channel_graph string the full LN channel graph in the JSON format that the 'lncli describegraph' returns 63 | -h, --help help for fakechanbackup 64 | --multi_file string the fake channel backup file to create (default "results/fake-2025-07-06-14-25-48.backup") 65 | --remote_node_addr string the remote node connection information in the format pubkey@host:port 66 | --rootkey string BIP32 HD root key of the wallet to use for encrypting the backup; leave empty to prompt for lnd 24 word aezeed 67 | --short_channel_id string the short channel ID in the format xx 68 | --walletdb string read the seed/master root key to use for encrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag 69 | ``` 70 | 71 | ### Options inherited from parent commands 72 | 73 | ``` 74 | --nologfile If set, no log file will be created. This is useful for testing purposes where we don't want to create a log file. 75 | -r, --regtest Indicates if regtest parameters should be used 76 | --resultsdir string Directory where results should be stored (default "./results") 77 | -s, --signet Indicates if the public signet parameters should be used 78 | -t, --testnet Indicates if testnet parameters should be used 79 | ``` 80 | 81 | ### SEE ALSO 82 | 83 | * [chantools](chantools.md) - Chantools helps recover funds from lightning channels 84 | 85 | --------------------------------------------------------------------------------