Skip to content

Commit cc284ba

Browse files
authored
Merge pull request #132 from lightninglabs/triggerforceclose
triggerforceclose: make compatible with all nodes, add Tor support
2 parents 0fd58ee + 6acc818 commit cc284ba

11 files changed

+136
-74
lines changed

README.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ Scenarios:
107107
Another reason might be that the peer is a CLN node with a specific version
108108
that doesn't react to force close requests normally. You can use the
109109
[`chantools triggerforceclose` command](doc/chantools_triggerforceclose.md) in
110-
that case (ONLY works with CLN peers of a certain version).
110+
that case (should work with CLN peers of a certain version that don't respond
111+
to normal force close requests).
111112

112113
## What should I NEVER do?
113114

@@ -437,7 +438,7 @@ Available Commands:
437438
sweeptimelock Sweep the force-closed state after the time lock has expired
438439
sweeptimelockmanual Sweep the force-closed state of a single channel manually if only a channel backup file is available
439440
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
440-
triggerforceclose Connect to a peer and send a custom message to trigger a force close of the specified channel
441+
triggerforceclose Connect to a peer and send request to trigger a force close of the specified channel
441442
vanitygen Generate a seed with a custom lnd node identity public key that starts with the given prefix
442443
walletinfo Shows info about an lnd wallet.db file and optionally extracts the BIP32 HD root key
443444
zombierecovery Try rescuing funds stuck in channels with zombie nodes
@@ -499,7 +500,7 @@ Legend:
499500
| [sweepremoteclosed](doc/chantools_sweepremoteclosed.md) | :pencil: Find channel funds from remotely force closed channels and sweep them |
500501
| [sweeptimelock](doc/chantools_sweeptimelock.md) | :pencil: Sweep funds in locally force closed channels once time lock has expired (requires `channel.db`) |
501502
| [sweeptimelockmanual](doc/chantools_sweeptimelockmanual.md) | :pencil: Manually sweep funds in a locally force closed channel where no `channel.db` file is available |
502-
| [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 |
503+
| [triggerforceclose](doc/chantools_triggerforceclose.md) | :pencil: (:pushpin:) Request a peer to force close a channel |
503504
| [vanitygen](doc/chantools_vanitygen.md) | Generate an `lnd` seed for a node public key that starts with a certain sequence of hex digits |
504505
| [walletinfo](doc/chantools_walletinfo.md) | Show information from a `wallet.db` file, requires access to the wallet password |
505506
| [zombierecovery](doc/chantools_zombierecovery.md) | :pencil: Cooperatively rescue funds from channels where normal recovery is not possible (see [full guide here][zombie-recovery]) |

cmd/chantools/root.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const (
3030
// version is the current version of the tool. It is set during build.
3131
// NOTE: When changing this, please also update the version in the
3232
// download link shown in the README.
33-
version = "0.13.0"
33+
version = "0.13.1"
3434
na = "n/a"
3535

3636
// lndVersion is the current version of lnd that we support. This is

cmd/chantools/triggerforceclose.go

+118-58
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ package main
22

33
import (
44
"fmt"
5-
"net"
65
"strconv"
76
"strings"
87
"time"
98

9+
"github.com/btcsuite/btcd/btcec/v2"
1010
"github.com/btcsuite/btcd/chaincfg/chainhash"
1111
"github.com/btcsuite/btcd/connmgr"
1212
"github.com/btcsuite/btcd/wire"
@@ -15,12 +15,15 @@ import (
1515
"github.com/lightningnetwork/lnd/keychain"
1616
"github.com/lightningnetwork/lnd/lncfg"
1717
"github.com/lightningnetwork/lnd/lnwire"
18+
"github.com/lightningnetwork/lnd/peer"
1819
"github.com/lightningnetwork/lnd/tor"
1920
"github.com/spf13/cobra"
2021
)
2122

2223
var (
2324
dialTimeout = time.Minute
25+
26+
defaultTorDNSHostPort = "soa.nodes.lightning.directory:53"
2427
)
2528

2629
type triggerForceCloseCommand struct {
@@ -29,6 +32,8 @@ type triggerForceCloseCommand struct {
2932

3033
APIURL string
3134

35+
TorProxy string
36+
3237
rootKey *rootKey
3338
cmd *cobra.Command
3439
}
@@ -37,13 +42,13 @@ func newTriggerForceCloseCommand() *cobra.Command {
3742
cc := &triggerForceCloseCommand{}
3843
cc.cmd = &cobra.Command{
3944
Use: "triggerforceclose",
40-
Short: "Connect to a CLN peer and send a custom message to " +
41-
"trigger a force close of the specified channel",
42-
Long: `Certain versions of CLN didn't properly react to error
43-
messages sent by peers and therefore didn't follow the DLP protocol to recover
44-
channel funds using SCB. This command can be used to trigger a force close with
45-
those earlier versions of CLN (this command will not work for lnd peers or CLN
46-
peers of a different version).`,
45+
Short: "Connect to a Lightning Network peer and send " +
46+
"specific messages to trigger a force close of the " +
47+
"specified channel",
48+
Long: `Asks the specified remote peer to force close a specific
49+
channel by first sending a channel re-establish message, and if that doesn't
50+
work, a custom error message (in case the peer is a specific version of CLN that
51+
does not properly respond to a Data Loss Protection re-establish message).'`,
4752
Example: `chantools triggerforceclose \
4853
--peer [email protected]:9735 \
4954
--channel_point abcdef01234...:x`,
@@ -62,6 +67,10 @@ peers of a different version).`,
6267
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
6368
"be esplora compatible)",
6469
)
70+
cc.cmd.Flags().StringVar(
71+
&cc.TorProxy, "torproxy", "", "SOCKS5 proxy to use for Tor "+
72+
"connections (to .onion addresses)",
73+
)
6574
cc.rootKey = newRootKey(cc.cmd, "deriving the identity key")
6675

6776
return cc.cmd
@@ -88,101 +97,152 @@ func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
8897
PrivKey: identityPriv,
8998
}
9099

91-
peerAddr, err := lncfg.ParseLNAddressString(
92-
c.Peer, "9735", net.ResolveTCPAddr,
100+
outPoint, err := parseOutPoint(c.ChannelPoint)
101+
if err != nil {
102+
return fmt.Errorf("error parsing channel point: %w", err)
103+
}
104+
105+
err = requestForceClose(
106+
c.Peer, c.TorProxy, pubKey, outPoint, identityECDH,
93107
)
94108
if err != nil {
95-
return fmt.Errorf("error parsing peer address: %w", err)
109+
return fmt.Errorf("error requesting force close: %w", err)
96110
}
97111

98-
outPoint, err := parseOutPoint(c.ChannelPoint)
112+
log.Infof("Message sent, waiting for force close transaction to " +
113+
"appear in mempool")
114+
115+
api := newExplorerAPI(c.APIURL)
116+
channelAddress, err := api.Address(c.ChannelPoint)
99117
if err != nil {
100-
return fmt.Errorf("error parsing channel point: %w", err)
118+
return fmt.Errorf("error getting channel address: %w", err)
119+
}
120+
121+
spends, err := api.Spends(channelAddress)
122+
if err != nil {
123+
return fmt.Errorf("error getting spends: %w", err)
124+
}
125+
for len(spends) == 0 {
126+
log.Infof("No spends found yet, waiting 5 seconds...")
127+
time.Sleep(5 * time.Second)
128+
spends, err = api.Spends(channelAddress)
129+
if err != nil {
130+
return fmt.Errorf("error getting spends: %w", err)
131+
}
101132
}
102-
channelID := lnwire.NewChanIDFromOutPoint(outPoint)
103133

104-
conn, err := noiseDial(
105-
identityECDH, peerAddr, &tor.ClearNet{}, dialTimeout,
134+
log.Infof("Found force close transaction %v", spends[0].TXID)
135+
log.Infof("You can now use the sweepremoteclosed command to sweep " +
136+
"the funds from the channel")
137+
138+
return nil
139+
}
140+
141+
func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress,
142+
netCfg tor.Net, timeout time.Duration) (*brontide.Conn, error) {
143+
144+
return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial)
145+
}
146+
147+
func connectPeer(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
148+
identity keychain.SingleKeyECDH,
149+
dialTimeout time.Duration) (*peer.Brontide, error) {
150+
151+
var dialNet tor.Net = &tor.ClearNet{}
152+
if torProxy != "" {
153+
dialNet = &tor.ProxyNet{
154+
SOCKS: torProxy,
155+
DNS: defaultTorDNSHostPort,
156+
StreamIsolation: false,
157+
SkipProxyForClearNetTargets: true,
158+
}
159+
}
160+
161+
log.Debugf("Attempting to resolve peer address %v", peerHost)
162+
peerAddr, err := lncfg.ParseLNAddressString(
163+
peerHost, "9735", dialNet.ResolveTCPAddr,
106164
)
107165
if err != nil {
108-
return fmt.Errorf("error dialing peer: %w", err)
166+
return nil, fmt.Errorf("error parsing peer address: %w", err)
109167
}
110168

111-
log.Infof("Attempting to connect to peer %x, dial timeout is %v",
112-
pubKey.SerializeCompressed(), dialTimeout)
169+
log.Debugf("Attempting to dial resolved peer address %v",
170+
peerAddr.String())
171+
conn, err := noiseDial(identity, peerAddr, dialNet, dialTimeout)
172+
if err != nil {
173+
return nil, fmt.Errorf("error dialing peer: %w", err)
174+
}
175+
176+
log.Infof("Attempting to establish p2p connection to peer %x, dial"+
177+
"timeout is %v", peerPubKey.SerializeCompressed(), dialTimeout)
113178
req := &connmgr.ConnReq{
114179
Addr: peerAddr,
115180
Permanent: false,
116181
}
117-
p, err := lnd.ConnectPeer(conn, req, chainParams, identityECDH)
182+
p, err := lnd.ConnectPeer(conn, req, chainParams, identity)
118183
if err != nil {
119-
return fmt.Errorf("error connecting to peer: %w", err)
184+
return nil, fmt.Errorf("error connecting to peer: %w", err)
120185
}
121186

122187
log.Infof("Connection established to peer %x",
123-
pubKey.SerializeCompressed())
188+
peerPubKey.SerializeCompressed())
124189

125190
// We'll wait until the peer is active.
126191
select {
127192
case <-p.ActiveSignal():
128193
case <-p.QuitSignal():
129-
return fmt.Errorf("peer %x disconnected",
130-
pubKey.SerializeCompressed())
194+
return nil, fmt.Errorf("peer %x disconnected",
195+
peerPubKey.SerializeCompressed())
131196
}
132197

198+
return p, nil
199+
}
200+
201+
func requestForceClose(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
202+
channelPoint *wire.OutPoint, identity keychain.SingleKeyECDH) error {
203+
204+
p, err := connectPeer(
205+
peerHost, torProxy, peerPubKey, identity, dialTimeout,
206+
)
207+
if err != nil {
208+
return fmt.Errorf("error connecting to peer: %w", err)
209+
}
210+
211+
channelID := lnwire.NewChanIDFromOutPoint(channelPoint)
212+
133213
// Channel ID (32 byte) + u16 for the data length (which will be 0).
134214
data := make([]byte, 34)
135215
copy(data[:32], channelID[:])
136216

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

140-
_ = lnwire.SetCustomOverrides([]uint16{lnwire.MsgError})
141-
msg, err := lnwire.NewCustom(lnwire.MsgError, data)
220+
err = p.SendMessageLazy(true, &lnwire.ChannelReestablish{
221+
ChanID: channelID,
222+
})
142223
if err != nil {
143224
return err
144225
}
145226

146-
err = p.SendMessageLazy(true, msg)
147-
if err != nil {
148-
return fmt.Errorf("error sending message: %w", err)
149-
}
150-
151-
log.Infof("Message sent, waiting for force close transaction to " +
152-
"appear in mempool")
227+
log.Infof("Sending channel error message to peer to trigger force "+
228+
"close of channel %v", channelPoint)
153229

154-
api := newExplorerAPI(c.APIURL)
155-
channelAddress, err := api.Address(c.ChannelPoint)
230+
_ = lnwire.SetCustomOverrides([]uint16{
231+
lnwire.MsgError, lnwire.MsgChannelReestablish,
232+
})
233+
msg, err := lnwire.NewCustom(lnwire.MsgError, data)
156234
if err != nil {
157-
return fmt.Errorf("error getting channel address: %w", err)
235+
return err
158236
}
159237

160-
spends, err := api.Spends(channelAddress)
238+
err = p.SendMessageLazy(true, msg)
161239
if err != nil {
162-
return fmt.Errorf("error getting spends: %w", err)
163-
}
164-
for len(spends) == 0 {
165-
log.Infof("No spends found yet, waiting 5 seconds...")
166-
time.Sleep(5 * time.Second)
167-
spends, err = api.Spends(channelAddress)
168-
if err != nil {
169-
return fmt.Errorf("error getting spends: %w", err)
170-
}
240+
return fmt.Errorf("error sending message: %w", err)
171241
}
172242

173-
log.Infof("Found force close transaction %v", spends[0].TXID)
174-
log.Infof("You can now use the sweepremoteclosed command to sweep " +
175-
"the funds from the channel")
176-
177243
return nil
178244
}
179245

180-
func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress,
181-
netCfg tor.Net, timeout time.Duration) (*brontide.Conn, error) {
182-
183-
return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial)
184-
}
185-
186246
func parseOutPoint(s string) (*wire.OutPoint, error) {
187247
split := strings.Split(s, ":")
188248
if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {

doc/chantools.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ https://github.com/lightninglabs/chantools/.
5151
* [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
5252
* [chantools sweeptimelock](chantools_sweeptimelock.md) - Sweep the force-closed state after the time lock has expired
5353
* [chantools sweeptimelockmanual](chantools_sweeptimelockmanual.md) - Sweep the force-closed state of a single channel manually if only a channel backup file is available
54-
* [chantools triggerforceclose](chantools_triggerforceclose.md) - Connect to a CLN peer and send a custom message to trigger a force close of the specified channel
54+
* [chantools triggerforceclose](chantools_triggerforceclose.md) - Connect to a Lightning Network peer and send specific messages to trigger a force close of the specified channel
5555
* [chantools vanitygen](chantools_vanitygen.md) - Generate a seed with a custom lnd node identity public key that starts with the given prefix
5656
* [chantools walletinfo](chantools_walletinfo.md) - Shows info about an lnd wallet.db file and optionally extracts the BIP32 HD root key
5757
* [chantools zombierecovery](chantools_zombierecovery.md) - Try rescuing funds stuck in channels with zombie nodes

doc/chantools_deletepayments.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ If only the failed payments should be deleted (and not the successful ones), the
1010

1111
CAUTION: Running this command will make it impossible to use the channel DB
1212
with an older version of lnd. Downgrading is not possible and you'll need to
13-
run lnd v0.17.0-beta or later after using this command!'
13+
run lnd v0.17.4-beta or later after using this command!'
1414

1515
```
1616
chantools deletepayments [flags]

doc/chantools_dropchannelgraph.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ without removing any other data.
1212

1313
CAUTION: Running this command will make it impossible to use the channel DB
1414
with an older version of lnd. Downgrading is not possible and you'll need to
15-
run lnd v0.17.0-beta or later after using this command!'
15+
run lnd v0.17.4-beta or later after using this command!'
1616

1717
```
1818
chantools dropchannelgraph [flags]

doc/chantools_dropgraphzombies.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ be helpful to fix a graph that is out of sync with the network.
1212

1313
CAUTION: Running this command will make it impossible to use the channel DB
1414
with an older version of lnd. Downgrading is not possible and you'll need to
15-
run lnd v0.17.0-beta or later after using this command!'
15+
run lnd v0.17.4-beta or later after using this command!'
1616

1717
```
1818
chantools dropgraphzombies [flags]

doc/chantools_migratedb.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ needs to read the database content.
1111

1212
CAUTION: Running this command will make it impossible to use the channel DB
1313
with an older version of lnd. Downgrading is not possible and you'll need to
14-
run lnd v0.17.0-beta or later after using this command!'
14+
run lnd v0.17.4-beta or later after using this command!'
1515

1616
```
1717
chantools migratedb [flags]

doc/chantools_recoverloopin.md

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ chantools recoverloopin \
3030
--output_amt uint amount of the output to sweep
3131
--publish publish sweep TX to the chain API instead of just printing the TX
3232
--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
3334
--start_key_index int start key index to try to find the correct key index
3435
--swap_hash string swap hash of the loop in swap
3536
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically

doc/chantools_removechannel.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ channel was never confirmed on chain!
1111

1212
CAUTION: Running this command will make it impossible to use the channel DB
1313
with an older version of lnd. Downgrading is not possible and you'll need to
14-
run lnd v0.17.0-beta or later after using this command!
14+
run lnd v0.17.4-beta or later after using this command!
1515

1616
```
1717
chantools removechannel [flags]

doc/chantools_triggerforceclose.md

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
## chantools triggerforceclose
22

3-
Connect to a CLN peer and send a custom message to trigger a force close of the specified channel
3+
Connect to a Lightning Network peer and send specific messages to trigger a force close of the specified channel
44

55
### Synopsis
66

7-
Certain versions of CLN didn't properly react to error
8-
messages sent by peers and therefore didn't follow the DLP protocol to recover
9-
channel funds using SCB. This command can be used to trigger a force close with
10-
those earlier versions of CLN (this command will not work for lnd peers or CLN
11-
peers of a different version).
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).'
1211

1312
```
1413
chantools triggerforceclose [flags]
@@ -31,6 +30,7 @@ chantools triggerforceclose \
3130
-h, --help help for triggerforceclose
3231
--peer string remote peer address (<pubkey>@<host>[:<port>])
3332
--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
33+
--torproxy string SOCKS5 proxy to use for Tor connections (to .onion addresses)
3434
--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
3535
```
3636

0 commit comments

Comments
 (0)