Skip to content

Commit

Permalink
Merge pull request #132 from lightninglabs/triggerforceclose
Browse files Browse the repository at this point in the history
triggerforceclose: make compatible with all nodes, add Tor support
  • Loading branch information
guggero committed Apr 29, 2024
2 parents 0fd58ee + 6acc818 commit cc284ba
Show file tree
Hide file tree
Showing 11 changed files with 136 additions and 74 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]) |
Expand Down
2 changes: 1 addition & 1 deletion cmd/chantools/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
176 changes: 118 additions & 58 deletions cmd/chantools/triggerforceclose.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package main

import (
"fmt"
"net"
"strconv"
"strings"
"time"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/connmgr"
"github.com/btcsuite/btcd/wire"
Expand All @@ -15,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 {
Expand All @@ -29,6 +32,8 @@ type triggerForceCloseCommand struct {

APIURL string

TorProxy string

rootKey *rootKey
cmd *cobra.Command
}
Expand All @@ -37,13 +42,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`,
Expand All @@ -62,6 +67,10 @@ peers of a different version).`,
&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
Expand All @@ -88,101 +97,152 @@ func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
PrivKey: identityPriv,
}

peerAddr, err := lncfg.ParseLNAddressString(
c.Peer, "9735", net.ResolveTCPAddr,
outPoint, err := parseOutPoint(c.ChannelPoint)
if err != nil {
return fmt.Errorf("error parsing channel point: %w", err)
}

err = requestForceClose(
c.Peer, c.TorProxy, pubKey, outPoint, identityECDH,
)
if err != nil {
return fmt.Errorf("error parsing peer address: %w", err)
return fmt.Errorf("error requesting force close: %w", err)
}

outPoint, err := parseOutPoint(c.ChannelPoint)
log.Infof("Message sent, waiting for force close transaction to " +
"appear in mempool")

api := newExplorerAPI(c.APIURL)
channelAddress, err := api.Address(c.ChannelPoint)
if err != nil {
return fmt.Errorf("error parsing channel point: %w", err)
return fmt.Errorf("error getting channel address: %w", err)
}

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)
}
}
channelID := lnwire.NewChanIDFromOutPoint(outPoint)

conn, err := noiseDial(
identityECDH, peerAddr, &tor.ClearNet{}, dialTimeout,
log.Infof("Found force close transaction %v", spends[0].TXID)
log.Infof("You can now use the sweepremoteclosed command to sweep " +
"the funds from the channel")

return nil
}

func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress,
netCfg tor.Net, timeout time.Duration) (*brontide.Conn, error) {

return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial)
}

func connectPeer(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
identity keychain.SingleKeyECDH,
dialTimeout time.Duration) (*peer.Brontide, error) {

var dialNet tor.Net = &tor.ClearNet{}
if torProxy != "" {
dialNet = &tor.ProxyNet{
SOCKS: torProxy,
DNS: defaultTorDNSHostPort,
StreamIsolation: false,
SkipProxyForClearNetTargets: true,
}
}

log.Debugf("Attempting to resolve peer address %v", peerHost)
peerAddr, err := lncfg.ParseLNAddressString(
peerHost, "9735", dialNet.ResolveTCPAddr,
)
if err != nil {
return fmt.Errorf("error dialing peer: %w", err)
return nil, fmt.Errorf("error parsing peer address: %w", err)
}

log.Infof("Attempting to connect to peer %x, dial timeout is %v",
pubKey.SerializeCompressed(), dialTimeout)
log.Debugf("Attempting to dial resolved peer address %v",
peerAddr.String())
conn, err := noiseDial(identity, peerAddr, dialNet, dialTimeout)
if err != nil {
return nil, fmt.Errorf("error dialing peer: %w", err)
}

log.Infof("Attempting to establish p2p connection to peer %x, dial"+
"timeout is %v", peerPubKey.SerializeCompressed(), dialTimeout)
req := &connmgr.ConnReq{
Addr: peerAddr,
Permanent: false,
}
p, err := lnd.ConnectPeer(conn, req, chainParams, identityECDH)
p, err := lnd.ConnectPeer(conn, req, chainParams, identity)
if err != nil {
return fmt.Errorf("error connecting to peer: %w", err)
return nil, fmt.Errorf("error connecting to peer: %w", err)
}

log.Infof("Connection established to peer %x",
pubKey.SerializeCompressed())
peerPubKey.SerializeCompressed())

// We'll wait until the peer is active.
select {
case <-p.ActiveSignal():
case <-p.QuitSignal():
return fmt.Errorf("peer %x disconnected",
pubKey.SerializeCompressed())
return nil, fmt.Errorf("peer %x disconnected",
peerPubKey.SerializeCompressed())
}

return p, nil
}

func requestForceClose(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
channelPoint *wire.OutPoint, identity keychain.SingleKeyECDH) error {

p, err := connectPeer(
peerHost, torProxy, peerPubKey, identity, dialTimeout,
)
if err != nil {
return fmt.Errorf("error connecting to peer: %w", err)
}

channelID := lnwire.NewChanIDFromOutPoint(channelPoint)

// Channel ID (32 byte) + u16 for the data length (which will be 0).
data := make([]byte, 34)
copy(data[:32], channelID[:])

log.Infof("Sending channel error message to peer to trigger force "+
"close of channel %v", c.ChannelPoint)
log.Infof("Sending channel re-establish to peer to trigger force "+
"close of channel %v", channelPoint)

_ = lnwire.SetCustomOverrides([]uint16{lnwire.MsgError})
msg, err := lnwire.NewCustom(lnwire.MsgError, data)
err = p.SendMessageLazy(true, &lnwire.ChannelReestablish{
ChanID: channelID,
})
if err != nil {
return err
}

err = p.SendMessageLazy(true, msg)
if err != nil {
return fmt.Errorf("error sending message: %w", err)
}

log.Infof("Message sent, waiting for force close transaction to " +
"appear in mempool")
log.Infof("Sending channel error message to peer to trigger force "+
"close of channel %v", channelPoint)

api := 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 {
Expand Down
2 changes: 1 addition & 1 deletion doc/chantools.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion doc/chantools_deletepayments.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion doc/chantools_dropchannelgraph.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion doc/chantools_dropgraphzombies.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion doc/chantools_migratedb.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions doc/chantools_recoverloopin.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion doc/chantools_removechannel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
12 changes: 6 additions & 6 deletions doc/chantools_triggerforceclose.md
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -31,6 +30,7 @@ chantools triggerforceclose \
-h, --help help for triggerforceclose
--peer string remote peer address (<pubkey>@<host>[:<port>])
--rootkey string BIP32 HD root key of the wallet to use for deriving the identity key; leave empty to prompt for lnd 24 word aezeed
--torproxy string SOCKS5 proxy to use for Tor connections (to .onion addresses)
--walletdb string read the seed/master root key to use fro deriving the identity key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```

Expand Down

0 comments on commit cc284ba

Please sign in to comment.