From 179773fdb94b9ccf85e338018d0a261bfe6b508a Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sun, 28 Apr 2024 16:03:00 +0200 Subject: [PATCH 1/3] triggerforceclose: make cmd compatible with all nodes --- README.md | 7 +- cmd/chantools/triggerforceclose.go | 129 +++++++++++++++++------------ doc/chantools.md | 2 +- doc/chantools_deletepayments.md | 2 +- doc/chantools_dropchannelgraph.md | 2 +- doc/chantools_dropgraphzombies.md | 2 +- doc/chantools_migratedb.md | 2 +- doc/chantools_recoverloopin.md | 1 + doc/chantools_removechannel.md | 2 +- doc/chantools_triggerforceclose.md | 11 ++- 10 files changed, 93 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 042e20c..e55613f 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,8 @@ Scenarios: Another reason might be that the peer is a CLN node with a specific version that doesn't react to force close requests normally. You can use the [`chantools triggerforceclose` command](doc/chantools_triggerforceclose.md) in - that case (ONLY works with CLN peers of a certain version). + that case (should work with CLN peers of a certain version that don't respond + to normal force close requests). ## What should I NEVER do? @@ -437,7 +438,7 @@ Available Commands: sweeptimelock Sweep the force-closed state after the time lock has expired sweeptimelockmanual Sweep the force-closed state of a single channel manually if only a channel backup file is available sweepremoteclosed Go through all the addresses that could have funds of channels that were force-closed by the remote party. A public block explorer is queried for each address and if any balance is found, all funds are swept to a given address - triggerforceclose Connect to a peer and send a custom message to trigger a force close of the specified channel + triggerforceclose Connect to a peer and send request to trigger a force close of the specified channel vanitygen Generate a seed with a custom lnd node identity public key that starts with the given prefix walletinfo Shows info about an lnd wallet.db file and optionally extracts the BIP32 HD root key zombierecovery Try rescuing funds stuck in channels with zombie nodes @@ -499,7 +500,7 @@ Legend: | [sweepremoteclosed](doc/chantools_sweepremoteclosed.md) | :pencil: Find channel funds from remotely force closed channels and sweep them | | [sweeptimelock](doc/chantools_sweeptimelock.md) | :pencil: Sweep funds in locally force closed channels once time lock has expired (requires `channel.db`) | | [sweeptimelockmanual](doc/chantools_sweeptimelockmanual.md) | :pencil: Manually sweep funds in a locally force closed channel where no `channel.db` file is available | -| [triggerforceclose](doc/chantools_triggerforceclose.md) | :pencil: (:pushpin:) Request certain CLN peers to force close a channel that don't react to normal SCB recovery requests | +| [triggerforceclose](doc/chantools_triggerforceclose.md) | :pencil: (:pushpin:) Request a peer to force close a channel | | [vanitygen](doc/chantools_vanitygen.md) | Generate an `lnd` seed for a node public key that starts with a certain sequence of hex digits | | [walletinfo](doc/chantools_walletinfo.md) | Show information from a `wallet.db` file, requires access to the wallet password | | [zombierecovery](doc/chantools_zombierecovery.md) | :pencil: Cooperatively rescue funds from channels where normal recovery is not possible (see [full guide here][zombie-recovery]) | diff --git a/cmd/chantools/triggerforceclose.go b/cmd/chantools/triggerforceclose.go index 382a61e..53492ec 100644 --- a/cmd/chantools/triggerforceclose.go +++ b/cmd/chantools/triggerforceclose.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/connmgr" "github.com/btcsuite/btcd/wire" @@ -37,13 +38,13 @@ func newTriggerForceCloseCommand() *cobra.Command { cc := &triggerForceCloseCommand{} cc.cmd = &cobra.Command{ Use: "triggerforceclose", - Short: "Connect to a CLN peer and send a custom message to " + - "trigger a force close of the specified channel", - Long: `Certain versions of CLN didn't properly react to error -messages sent by peers and therefore didn't follow the DLP protocol to recover -channel funds using SCB. This command can be used to trigger a force close with -those earlier versions of CLN (this command will not work for lnd peers or CLN -peers of a different version).`, + Short: "Connect to a Lightning Network peer and send " + + "specific messages to trigger a force close of the " + + "specified channel", + Long: `Asks the specified remote peer to force close a specific +channel by first sending a channel re-establish message, and if that doesn't +work, a custom error message (in case the peer is a specific version of CLN that +does not properly respond to a Data Loss Protection re-establish message).'`, Example: `chantools triggerforceclose \ --peer 03abce...@xx.yy.zz.aa:9735 \ --channel_point abcdef01234...:x`, @@ -88,101 +89,125 @@ func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error { PrivKey: identityPriv, } + outPoint, err := parseOutPoint(c.ChannelPoint) + if err != nil { + return fmt.Errorf("error parsing channel point: %w", err) + } + + err = requestForceClose(c.Peer, pubKey, outPoint, identityECDH) + if err != nil { + return fmt.Errorf("error requesting force close: %w", err) + } + + log.Infof("Message sent, waiting for force close transaction to " + + "appear in mempool") + + api := newExplorerAPI(c.APIURL) + channelAddress, err := api.Address(c.ChannelPoint) + if err != nil { + return fmt.Errorf("error getting channel address: %w", err) + } + + spends, err := api.Spends(channelAddress) + if err != nil { + return fmt.Errorf("error getting spends: %w", err) + } + for len(spends) == 0 { + log.Infof("No spends found yet, waiting 5 seconds...") + time.Sleep(5 * time.Second) + spends, err = api.Spends(channelAddress) + if err != nil { + return fmt.Errorf("error getting spends: %w", err) + } + } + + log.Infof("Found force close transaction %v", spends[0].TXID) + log.Infof("You can now use the sweepremoteclosed command to sweep " + + "the funds from the channel") + + return nil +} + +func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress, + netCfg tor.Net, timeout time.Duration) (*brontide.Conn, error) { + + return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial) +} + +func requestForceClose(peerHost string, peerPubKey *btcec.PublicKey, + channelPoint *wire.OutPoint, identity keychain.SingleKeyECDH) error { + peerAddr, err := lncfg.ParseLNAddressString( - c.Peer, "9735", net.ResolveTCPAddr, + peerHost, "9735", net.ResolveTCPAddr, ) if err != nil { return fmt.Errorf("error parsing peer address: %w", err) } - outPoint, err := parseOutPoint(c.ChannelPoint) - if err != nil { - return fmt.Errorf("error parsing channel point: %w", err) - } - channelID := lnwire.NewChanIDFromOutPoint(outPoint) + channelID := lnwire.NewChanIDFromOutPoint(channelPoint) conn, err := noiseDial( - identityECDH, peerAddr, &tor.ClearNet{}, dialTimeout, + identity, peerAddr, &tor.ClearNet{}, dialTimeout, ) if err != nil { return fmt.Errorf("error dialing peer: %w", err) } log.Infof("Attempting to connect to peer %x, dial timeout is %v", - pubKey.SerializeCompressed(), dialTimeout) + peerPubKey.SerializeCompressed(), dialTimeout) req := &connmgr.ConnReq{ Addr: peerAddr, Permanent: false, } - p, err := lnd.ConnectPeer(conn, req, chainParams, identityECDH) + p, err := lnd.ConnectPeer(conn, req, chainParams, identity) if err != nil { return fmt.Errorf("error connecting to peer: %w", err) } log.Infof("Connection established to peer %x", - pubKey.SerializeCompressed()) + peerPubKey.SerializeCompressed()) // We'll wait until the peer is active. select { case <-p.ActiveSignal(): case <-p.QuitSignal(): return fmt.Errorf("peer %x disconnected", - pubKey.SerializeCompressed()) + peerPubKey.SerializeCompressed()) } // Channel ID (32 byte) + u16 for the data length (which will be 0). data := make([]byte, 34) copy(data[:32], channelID[:]) - log.Infof("Sending channel error message to peer to trigger force "+ - "close of channel %v", c.ChannelPoint) + log.Infof("Sending channel re-establish to peer to trigger force "+ + "close of channel %v", channelPoint) - _ = lnwire.SetCustomOverrides([]uint16{lnwire.MsgError}) - msg, err := lnwire.NewCustom(lnwire.MsgError, data) + err = p.SendMessageLazy(true, &lnwire.ChannelReestablish{ + ChanID: channelID, + }) if err != nil { return err } - err = p.SendMessageLazy(true, msg) - if err != nil { - return fmt.Errorf("error sending message: %w", err) - } - - log.Infof("Message sent, waiting for force close transaction to " + - "appear in mempool") + log.Infof("Sending channel error message to peer to trigger force "+ + "close of channel %v", channelPoint) - api := newExplorerAPI(c.APIURL) - channelAddress, err := api.Address(c.ChannelPoint) + _ = lnwire.SetCustomOverrides([]uint16{ + lnwire.MsgError, lnwire.MsgChannelReestablish, + }) + msg, err := lnwire.NewCustom(lnwire.MsgError, data) if err != nil { - return fmt.Errorf("error getting channel address: %w", err) + return err } - spends, err := api.Spends(channelAddress) + err = p.SendMessageLazy(true, msg) if err != nil { - return fmt.Errorf("error getting spends: %w", err) - } - for len(spends) == 0 { - log.Infof("No spends found yet, waiting 5 seconds...") - time.Sleep(5 * time.Second) - spends, err = api.Spends(channelAddress) - if err != nil { - return fmt.Errorf("error getting spends: %w", err) - } + return fmt.Errorf("error sending message: %w", err) } - log.Infof("Found force close transaction %v", spends[0].TXID) - log.Infof("You can now use the sweepremoteclosed command to sweep " + - "the funds from the channel") - return nil } -func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress, - netCfg tor.Net, timeout time.Duration) (*brontide.Conn, error) { - - return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial) -} - func parseOutPoint(s string) (*wire.OutPoint, error) { split := strings.Split(s, ":") if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 { diff --git a/doc/chantools.md b/doc/chantools.md index 544c8e2..cffdb64 100644 --- a/doc/chantools.md +++ b/doc/chantools.md @@ -51,7 +51,7 @@ https://github.com/lightninglabs/chantools/. * [chantools sweepremoteclosed](chantools_sweepremoteclosed.md) - Go through all the addresses that could have funds of channels that were force-closed by the remote party. A public block explorer is queried for each address and if any balance is found, all funds are swept to a given address * [chantools sweeptimelock](chantools_sweeptimelock.md) - Sweep the force-closed state after the time lock has expired * [chantools sweeptimelockmanual](chantools_sweeptimelockmanual.md) - Sweep the force-closed state of a single channel manually if only a channel backup file is available -* [chantools triggerforceclose](chantools_triggerforceclose.md) - Connect to a CLN peer and send a custom message to trigger a force close of the specified channel +* [chantools triggerforceclose](chantools_triggerforceclose.md) - Connect to a Lightning Network peer and send specific messages to trigger a force close of the specified channel * [chantools vanitygen](chantools_vanitygen.md) - Generate a seed with a custom lnd node identity public key that starts with the given prefix * [chantools walletinfo](chantools_walletinfo.md) - Shows info about an lnd wallet.db file and optionally extracts the BIP32 HD root key * [chantools zombierecovery](chantools_zombierecovery.md) - Try rescuing funds stuck in channels with zombie nodes diff --git a/doc/chantools_deletepayments.md b/doc/chantools_deletepayments.md index 67b6129..1a85fcb 100644 --- a/doc/chantools_deletepayments.md +++ b/doc/chantools_deletepayments.md @@ -10,7 +10,7 @@ If only the failed payments should be deleted (and not the successful ones), the CAUTION: Running this command will make it impossible to use the channel DB with an older version of lnd. Downgrading is not possible and you'll need to -run lnd v0.17.0-beta or later after using this command!' +run lnd v0.17.4-beta or later after using this command!' ``` chantools deletepayments [flags] diff --git a/doc/chantools_dropchannelgraph.md b/doc/chantools_dropchannelgraph.md index cd58359..303861b 100644 --- a/doc/chantools_dropchannelgraph.md +++ b/doc/chantools_dropchannelgraph.md @@ -12,7 +12,7 @@ without removing any other data. CAUTION: Running this command will make it impossible to use the channel DB with an older version of lnd. Downgrading is not possible and you'll need to -run lnd v0.17.0-beta or later after using this command!' +run lnd v0.17.4-beta or later after using this command!' ``` chantools dropchannelgraph [flags] diff --git a/doc/chantools_dropgraphzombies.md b/doc/chantools_dropgraphzombies.md index b9cd152..05d96f2 100644 --- a/doc/chantools_dropgraphzombies.md +++ b/doc/chantools_dropgraphzombies.md @@ -12,7 +12,7 @@ be helpful to fix a graph that is out of sync with the network. CAUTION: Running this command will make it impossible to use the channel DB with an older version of lnd. Downgrading is not possible and you'll need to -run lnd v0.17.0-beta or later after using this command!' +run lnd v0.17.4-beta or later after using this command!' ``` chantools dropgraphzombies [flags] diff --git a/doc/chantools_migratedb.md b/doc/chantools_migratedb.md index 6a26bed..f79001e 100644 --- a/doc/chantools_migratedb.md +++ b/doc/chantools_migratedb.md @@ -11,7 +11,7 @@ needs to read the database content. CAUTION: Running this command will make it impossible to use the channel DB with an older version of lnd. Downgrading is not possible and you'll need to -run lnd v0.17.0-beta or later after using this command!' +run lnd v0.17.4-beta or later after using this command!' ``` chantools migratedb [flags] diff --git a/doc/chantools_recoverloopin.md b/doc/chantools_recoverloopin.md index a7dabc9..4725faf 100644 --- a/doc/chantools_recoverloopin.md +++ b/doc/chantools_recoverloopin.md @@ -30,6 +30,7 @@ chantools recoverloopin \ --output_amt uint amount of the output to sweep --publish publish sweep TX to the chain API instead of just printing the TX --rootkey string BIP32 HD root key of the wallet to use for deriving starting key; leave empty to prompt for lnd 24 word aezeed + --sqlite_file string optional path to the loop sqlite database file, if not specified, the default location will be loaded from --loop_db_dir --start_key_index int start key index to try to find the correct key index --swap_hash string swap hash of the loop in swap --sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically diff --git a/doc/chantools_removechannel.md b/doc/chantools_removechannel.md index ad878b3..dcdc25e 100644 --- a/doc/chantools_removechannel.md +++ b/doc/chantools_removechannel.md @@ -11,7 +11,7 @@ channel was never confirmed on chain! CAUTION: Running this command will make it impossible to use the channel DB with an older version of lnd. Downgrading is not possible and you'll need to -run lnd v0.17.0-beta or later after using this command! +run lnd v0.17.4-beta or later after using this command! ``` chantools removechannel [flags] diff --git a/doc/chantools_triggerforceclose.md b/doc/chantools_triggerforceclose.md index c9672a2..58bd4e3 100644 --- a/doc/chantools_triggerforceclose.md +++ b/doc/chantools_triggerforceclose.md @@ -1,14 +1,13 @@ ## chantools triggerforceclose -Connect to a CLN peer and send a custom message to trigger a force close of the specified channel +Connect to a Lightning Network peer and send specific messages to trigger a force close of the specified channel ### Synopsis -Certain versions of CLN didn't properly react to error -messages sent by peers and therefore didn't follow the DLP protocol to recover -channel funds using SCB. This command can be used to trigger a force close with -those earlier versions of CLN (this command will not work for lnd peers or CLN -peers of a different version). +Asks the specified remote peer to force close a specific +channel by first sending a channel re-establish message, and if that doesn't +work, a custom error message (in case the peer is a specific version of CLN that +does not properly respond to a Data Loss Protection re-establish message).' ``` chantools triggerforceclose [flags] From e3285daf5b18e80c8a32b1b49ba24fe5f2b19976 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sun, 28 Apr 2024 16:03:01 +0200 Subject: [PATCH 2/3] triggerforceclose: support Tor connections --- cmd/chantools/triggerforceclose.go | 67 +++++++++++++++++++++++------- doc/chantools_triggerforceclose.md | 1 + 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/cmd/chantools/triggerforceclose.go b/cmd/chantools/triggerforceclose.go index 53492ec..366a046 100644 --- a/cmd/chantools/triggerforceclose.go +++ b/cmd/chantools/triggerforceclose.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "net" "strconv" "strings" "time" @@ -16,12 +15,15 @@ import ( "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/peer" "github.com/lightningnetwork/lnd/tor" "github.com/spf13/cobra" ) var ( dialTimeout = time.Minute + + defaultTorDNSHostPort = "soa.nodes.lightning.directory:53" ) type triggerForceCloseCommand struct { @@ -30,6 +32,8 @@ type triggerForceCloseCommand struct { APIURL string + TorProxy string + rootKey *rootKey cmd *cobra.Command } @@ -63,6 +67,10 @@ does not properly respond to a Data Loss Protection re-establish message).'`, &cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+ "be esplora compatible)", ) + cc.cmd.Flags().StringVar( + &cc.TorProxy, "torproxy", "", "SOCKS5 proxy to use for Tor "+ + "connections (to .onion addresses)", + ) cc.rootKey = newRootKey(cc.cmd, "deriving the identity key") return cc.cmd @@ -94,7 +102,9 @@ func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error { return fmt.Errorf("error parsing channel point: %w", err) } - err = requestForceClose(c.Peer, pubKey, outPoint, identityECDH) + err = requestForceClose( + c.Peer, c.TorProxy, pubKey, outPoint, identityECDH, + ) if err != nil { return fmt.Errorf("error requesting force close: %w", err) } @@ -134,34 +144,44 @@ func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress, return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial) } -func requestForceClose(peerHost string, peerPubKey *btcec.PublicKey, - channelPoint *wire.OutPoint, identity keychain.SingleKeyECDH) error { +func connectPeer(peerHost, torProxy string, peerPubKey *btcec.PublicKey, + identity keychain.SingleKeyECDH, + dialTimeout time.Duration) (*peer.Brontide, error) { + + var dialNet tor.Net = &tor.ClearNet{} + if torProxy != "" { + dialNet = &tor.ProxyNet{ + SOCKS: torProxy, + DNS: defaultTorDNSHostPort, + StreamIsolation: false, + SkipProxyForClearNetTargets: true, + } + } + log.Debugf("Attempting to resolve peer address %v", peerHost) peerAddr, err := lncfg.ParseLNAddressString( - peerHost, "9735", net.ResolveTCPAddr, + peerHost, "9735", dialNet.ResolveTCPAddr, ) if err != nil { - return fmt.Errorf("error parsing peer address: %w", err) + return nil, fmt.Errorf("error parsing peer address: %w", err) } - channelID := lnwire.NewChanIDFromOutPoint(channelPoint) - - conn, err := noiseDial( - identity, peerAddr, &tor.ClearNet{}, dialTimeout, - ) + log.Debugf("Attempting to dial resolved peer address %v", + peerAddr.String()) + conn, err := noiseDial(identity, peerAddr, dialNet, dialTimeout) if err != nil { - return fmt.Errorf("error dialing peer: %w", err) + return nil, fmt.Errorf("error dialing peer: %w", err) } - log.Infof("Attempting to connect to peer %x, dial timeout is %v", - peerPubKey.SerializeCompressed(), dialTimeout) + log.Infof("Attempting to establish p2p connection to peer %x, dial"+ + "timeout is %v", peerPubKey.SerializeCompressed(), dialTimeout) req := &connmgr.ConnReq{ Addr: peerAddr, Permanent: false, } p, err := lnd.ConnectPeer(conn, req, chainParams, identity) if err != nil { - return fmt.Errorf("error connecting to peer: %w", err) + return nil, fmt.Errorf("error connecting to peer: %w", err) } log.Infof("Connection established to peer %x", @@ -171,10 +191,25 @@ func requestForceClose(peerHost string, peerPubKey *btcec.PublicKey, select { case <-p.ActiveSignal(): case <-p.QuitSignal(): - return fmt.Errorf("peer %x disconnected", + return nil, fmt.Errorf("peer %x disconnected", peerPubKey.SerializeCompressed()) } + return p, nil +} + +func requestForceClose(peerHost, torProxy string, peerPubKey *btcec.PublicKey, + channelPoint *wire.OutPoint, identity keychain.SingleKeyECDH) error { + + p, err := connectPeer( + peerHost, torProxy, peerPubKey, identity, dialTimeout, + ) + if err != nil { + return fmt.Errorf("error connecting to peer: %w", err) + } + + channelID := lnwire.NewChanIDFromOutPoint(channelPoint) + // Channel ID (32 byte) + u16 for the data length (which will be 0). data := make([]byte, 34) copy(data[:32], channelID[:]) diff --git a/doc/chantools_triggerforceclose.md b/doc/chantools_triggerforceclose.md index 58bd4e3..695752c 100644 --- a/doc/chantools_triggerforceclose.md +++ b/doc/chantools_triggerforceclose.md @@ -30,6 +30,7 @@ chantools triggerforceclose \ -h, --help help for triggerforceclose --peer string remote peer address (@[:]) --rootkey string BIP32 HD root key of the wallet to use for deriving the identity key; leave empty to prompt for lnd 24 word aezeed + --torproxy string SOCKS5 proxy to use for Tor connections (to .onion addresses) --walletdb string read the seed/master root key to use fro deriving the identity key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag ``` From 6acc81815e835bb167c6912148ac13353ac7368d Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 29 Apr 2024 09:54:07 +0200 Subject: [PATCH 3/3] root: bump version to v0.13.1 --- cmd/chantools/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/chantools/root.go b/cmd/chantools/root.go index 1166cb1..2e3a3b8 100644 --- a/cmd/chantools/root.go +++ b/cmd/chantools/root.go @@ -30,7 +30,7 @@ const ( // version is the current version of the tool. It is set during build. // NOTE: When changing this, please also update the version in the // download link shown in the README. - version = "0.13.0" + version = "0.13.1" na = "n/a" // lndVersion is the current version of lnd that we support. This is