Skip to content

Commit 083453e

Browse files
committed
multi: add triggerforceclose command
1 parent a01dd32 commit 083453e

File tree

9 files changed

+516
-5
lines changed

9 files changed

+516
-5
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,27 +277,34 @@ Usage:
277277
278278
Available Commands:
279279
chanbackup Create a channel.backup file from a channel database
280+
closepoolaccount Tries to close a Pool account that has expired
280281
compactdb Create a copy of a channel.db file in safe/read-only mode
282+
deletepayments Remove all (failed) payments from a channel DB
281283
derivekey Derive a key with a specific derivation path
282284
dropchannelgraph Remove all graph related data from a channel DB
283285
dumpbackup Dump the content of a channel.backup file
284286
dumpchannels Dump all channel information from an lnd channel database
287+
fakechanbackup Fake a channel backup file to attempt fund recovery
285288
filterbackup Filter an lnd channel.backup file and remove certain channels
286289
fixoldbackup Fixes an old channel.backup file that is affected by the lnd issue #3881 (unable to derive shachain root key)
287290
forceclose Force-close the last state that is in the channel.db provided
288291
genimportscript Generate a script containing the on-chain keys of an lnd wallet that can be imported into other software like bitcoind
289-
help Help about any command
290292
migratedb Apply all recent lnd channel database migrations
291293
removechannel Remove a single channel from the given channel DB
292294
rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels
293295
rescuefunding Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the initiator of the channel needs to run
296+
rescuetweakedkey Attempt to rescue funds locked in an address with a key that was affected by a specific bug in lnd
294297
showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed
295298
signrescuefunding Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the remote node (the non-initiator) of the channel needs to run
296299
summary Compile a summary about the current state of channels
297300
sweeptimelock Sweep the force-closed state after the time lock has expired
298301
sweeptimelockmanual Sweep the force-closed state of a single channel manually if only a channel backup file is available
302+
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
303+
triggerforceclose Connect to a peer and send a custom message to trigger a force close of the specified channel
299304
vanitygen Generate a seed with a custom lnd node identity public key that starts with the given prefix
300305
walletinfo Shows info about an lnd wallet.db file and optionally extracts the BIP32 HD root key
306+
zombierecovery Try rescuing funds stuck in channels with zombie nodes
307+
help Help about any command
301308
302309
Flags:
303310
-h, --help help for chantools
@@ -336,6 +343,7 @@ Quick access:
336343
+ [sweepremoteclosed](doc/chantools_sweepremoteclosed.md)
337344
+ [sweeptimelock](doc/chantools_sweeptimelock.md)
338345
+ [sweeptimelockmanual](doc/chantools_sweeptimelockmanual.md)
346+
+ [triggerforceclose](doc/chantools_triggerforceclose.md)
339347
+ [vanitygen](doc/chantools_vanitygen.md)
340348
+ [walletinfo](doc/chantools_walletinfo.md)
341349
+ [zombierecovery](doc/chantools_zombierecovery.md)

btc/explorer_api.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ func (a *ExplorerAPI) Transaction(txid string) (*TX, error) {
8989

9090
func (a *ExplorerAPI) Outpoint(addr string) (*TX, int, error) {
9191
var txs []*TX
92-
err := fetchJSON(fmt.Sprintf("%s/address/%s/txs", a.BaseURL, addr), &txs)
92+
err := fetchJSON(
93+
fmt.Sprintf("%s/address/%s/txs", a.BaseURL, addr), &txs,
94+
)
9395
if err != nil {
9496
return nil, 0, err
9597
}
@@ -104,6 +106,28 @@ func (a *ExplorerAPI) Outpoint(addr string) (*TX, int, error) {
104106
return nil, 0, fmt.Errorf("no tx found")
105107
}
106108

109+
func (a *ExplorerAPI) Spends(addr string) ([]*TX, error) {
110+
var txs []*TX
111+
err := fetchJSON(
112+
fmt.Sprintf("%s/address/%s/txs", a.BaseURL, addr), &txs,
113+
)
114+
if err != nil {
115+
return nil, err
116+
}
117+
118+
var spends []*TX
119+
for txIndex := range txs {
120+
tx := txs[txIndex]
121+
for _, vin := range tx.Vin {
122+
if vin.Prevout.ScriptPubkeyAddr == addr {
123+
spends = append(spends, tx)
124+
}
125+
}
126+
}
127+
128+
return spends, nil
129+
}
130+
107131
func (a *ExplorerAPI) Unspent(addr string) ([]*Vout, error) {
108132
var (
109133
stats = &AddressStats{}

cmd/chantools/dropchannelgraph.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ func newChanAnnouncement(localPubKey, remotePubKey *btcec.PublicKey,
242242

243243
// Our channel update message flags will signal that we support the
244244
// max_htlc field.
245-
msgFlags := lnwire.ChanUpdateOptionMaxHtlc
245+
msgFlags := lnwire.ChanUpdateRequiredMaxHtlc
246246

247247
// We announce the channel with the default values. Some of
248248
// these values can later be changed by crafting a new ChannelUpdate.

cmd/chantools/root.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ import (
2020
"github.com/lightningnetwork/lnd/build"
2121
"github.com/lightningnetwork/lnd/chanbackup"
2222
"github.com/lightningnetwork/lnd/channeldb"
23+
"github.com/lightningnetwork/lnd/peer"
2324
"github.com/spf13/cobra"
2425
"golang.org/x/crypto/ssh/terminal"
2526
)
2627

2728
const (
2829
defaultAPIURL = "https://blockstream.info/api"
29-
version = "0.10.6"
30+
version = "0.10.7"
3031
na = "n/a"
3132

3233
Commit = ""
@@ -104,6 +105,7 @@ func main() {
104105
newSweepTimeLockCommand(),
105106
newSweepTimeLockManualCommand(),
106107
newSweepRemoteClosedCommand(),
108+
newTriggerForceCloseCommand(),
107109
newVanityGenCommand(),
108110
newWalletInfoCommand(),
109111
newZombieRecoveryCommand(),
@@ -263,6 +265,7 @@ func setupLogging() {
263265
setSubLogger("CHAN", log)
264266
addSubLogger("CHDB", channeldb.UseLogger)
265267
addSubLogger("BCKP", chanbackup.UseLogger)
268+
addSubLogger("PEER", peer.UseLogger)
266269
err := logWriter.InitLogRotator("./results/chantools.log", 10, 3)
267270
if err != nil {
268271
panic(err)

cmd/chantools/triggerforceclose.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"strconv"
7+
"strings"
8+
"time"
9+
10+
"github.com/btcsuite/btcd/chaincfg/chainhash"
11+
"github.com/btcsuite/btcd/connmgr"
12+
"github.com/btcsuite/btcd/wire"
13+
"github.com/guggero/chantools/btc"
14+
"github.com/guggero/chantools/lnd"
15+
"github.com/lightningnetwork/lnd/brontide"
16+
"github.com/lightningnetwork/lnd/keychain"
17+
"github.com/lightningnetwork/lnd/lncfg"
18+
"github.com/lightningnetwork/lnd/lnwire"
19+
"github.com/lightningnetwork/lnd/tor"
20+
"github.com/spf13/cobra"
21+
)
22+
23+
var (
24+
dialTimeout = time.Minute
25+
)
26+
27+
type triggerForceCloseCommand struct {
28+
Peer string
29+
ChannelPoint string
30+
31+
APIURL string
32+
33+
rootKey *rootKey
34+
cmd *cobra.Command
35+
}
36+
37+
func newTriggerForceCloseCommand() *cobra.Command {
38+
cc := &triggerForceCloseCommand{}
39+
cc.cmd = &cobra.Command{
40+
Use: "triggerforceclose",
41+
Short: "Connect to a peer and send a custom message to " +
42+
"trigger a force close of the specified channel",
43+
Example: `chantools triggerforceclose \
44+
--peer [email protected]:9735 \
45+
--channel_point abcdef01234...:x`,
46+
RunE: cc.Execute,
47+
}
48+
cc.cmd.Flags().StringVar(
49+
&cc.Peer, "peer", "", "remote peer address "+
50+
"(<pubkey>@<host>[:<port>])",
51+
)
52+
cc.cmd.Flags().StringVar(
53+
&cc.ChannelPoint, "channel_point", "", "funding transaction "+
54+
"outpoint of the channel to trigger the force close "+
55+
"of (<txid>:<txindex>)",
56+
)
57+
cc.cmd.Flags().StringVar(
58+
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
59+
"be esplora compatible)",
60+
)
61+
cc.rootKey = newRootKey(cc.cmd, "deriving the identity key")
62+
63+
return cc.cmd
64+
}
65+
66+
func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
67+
extendedKey, err := c.rootKey.read()
68+
if err != nil {
69+
return fmt.Errorf("error reading root key: %w", err)
70+
}
71+
72+
identityPath := lnd.IdentityPath(chainParams)
73+
child, pubKey, _, err := lnd.DeriveKey(
74+
extendedKey, identityPath, chainParams,
75+
)
76+
if err != nil {
77+
return fmt.Errorf("could not derive identity key: %w", err)
78+
}
79+
identityPriv, err := child.ECPrivKey()
80+
if err != nil {
81+
return fmt.Errorf("could not get identity private key: %w", err)
82+
}
83+
identityECDH := &keychain.PrivKeyECDH{
84+
PrivKey: identityPriv,
85+
}
86+
87+
peerAddr, err := lncfg.ParseLNAddressString(
88+
c.Peer, "9735", net.ResolveTCPAddr,
89+
)
90+
if err != nil {
91+
return fmt.Errorf("error parsing peer address: %w", err)
92+
}
93+
94+
outPoint, err := parseOutPoint(c.ChannelPoint)
95+
if err != nil {
96+
return fmt.Errorf("error parsing channel point: %w", err)
97+
}
98+
channelID := lnwire.NewChanIDFromOutPoint(outPoint)
99+
100+
conn, err := noiseDial(
101+
identityECDH, peerAddr, &tor.ClearNet{}, dialTimeout,
102+
)
103+
if err != nil {
104+
return fmt.Errorf("error dialing peer: %w", err)
105+
}
106+
107+
log.Infof("Attempting to connect to peer %x, dial timeout is %v",
108+
pubKey.SerializeCompressed(), dialTimeout)
109+
req := &connmgr.ConnReq{
110+
Addr: peerAddr,
111+
Permanent: false,
112+
}
113+
p, err := lnd.ConnectPeer(conn, req, chainParams, identityECDH)
114+
if err != nil {
115+
return fmt.Errorf("error connecting to peer: %w", err)
116+
}
117+
118+
log.Infof("Connection established to peer %x",
119+
pubKey.SerializeCompressed())
120+
121+
// We'll wait until the peer is active.
122+
select {
123+
case <-p.ActiveSignal():
124+
case <-p.QuitSignal():
125+
return fmt.Errorf("peer %x disconnected",
126+
pubKey.SerializeCompressed())
127+
}
128+
129+
// Channel ID (32 byte) + u16 for the data length (which will be 0).
130+
data := make([]byte, 34)
131+
copy(data[:32], channelID[:])
132+
133+
log.Infof("Sending channel error message to peer to trigger force "+
134+
"close of channel %v", c.ChannelPoint)
135+
136+
_ = lnwire.SetCustomOverrides([]uint16{lnwire.MsgError})
137+
msg, err := lnwire.NewCustom(lnwire.MsgError, data)
138+
if err != nil {
139+
return err
140+
}
141+
142+
err = p.SendMessageLazy(true, msg)
143+
if err != nil {
144+
return fmt.Errorf("error sending message: %w", err)
145+
}
146+
147+
log.Infof("Message sent, waiting for force close transaction to " +
148+
"appear in mempool")
149+
150+
api := &btc.ExplorerAPI{BaseURL: c.APIURL}
151+
channelAddress, err := api.Address(c.ChannelPoint)
152+
if err != nil {
153+
return fmt.Errorf("error getting channel address: %w", err)
154+
}
155+
156+
spends, err := api.Spends(channelAddress)
157+
if err != nil {
158+
return fmt.Errorf("error getting spends: %w", err)
159+
}
160+
for len(spends) == 0 {
161+
log.Infof("No spends found yet, waiting 5 seconds...")
162+
time.Sleep(5 * time.Second)
163+
spends, err = api.Spends(channelAddress)
164+
if err != nil {
165+
return fmt.Errorf("error getting spends: %w", err)
166+
}
167+
}
168+
169+
log.Infof("Found force close transaction %v", spends[0].TXID)
170+
log.Infof("You can now use the sweepremoteclosed command to sweep " +
171+
"the funds from the channel")
172+
173+
return nil
174+
}
175+
176+
func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress,
177+
netCfg tor.Net, timeout time.Duration) (*brontide.Conn, error) {
178+
179+
return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial)
180+
}
181+
182+
func parseOutPoint(s string) (*wire.OutPoint, error) {
183+
split := strings.Split(s, ":")
184+
if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
185+
return nil, fmt.Errorf("invalid channel point format: %v", s)
186+
}
187+
188+
index, err := strconv.ParseInt(split[1], 10, 64)
189+
if err != nil {
190+
return nil, fmt.Errorf("unable to decode output index: %v", err)
191+
}
192+
193+
txid, err := chainhash.NewHashFromStr(split[0])
194+
if err != nil {
195+
return nil, fmt.Errorf("unable to parse hex string: %v", err)
196+
}
197+
198+
return &wire.OutPoint{
199+
Hash: *txid,
200+
Index: uint32(index),
201+
}, nil
202+
}

doc/chantools.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Complete documentation is available at https://github.com/guggero/chantools/.
4242
* [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
4343
* [chantools sweeptimelock](chantools_sweeptimelock.md) - Sweep the force-closed state after the time lock has expired
4444
* [chantools sweeptimelockmanual](chantools_sweeptimelockmanual.md) - Sweep the force-closed state of a single channel manually if only a channel backup file is available
45+
* [chantools triggerforceclose](chantools_triggerforceclose.md) - Connect to a peer and send a custom message to trigger a force close of the specified channel
4546
* [chantools vanitygen](chantools_vanitygen.md) - Generate a seed with a custom lnd node identity public key that starts with the given prefix
4647
* [chantools walletinfo](chantools_walletinfo.md) - Shows info about an lnd wallet.db file and optionally extracts the BIP32 HD root key
4748
* [chantools zombierecovery](chantools_zombierecovery.md) - Try rescuing funds stuck in channels with zombie nodes

doc/chantools_fakechanbackup.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ chantools fakechanbackup --from_channel_graph lncli_describegraph.json \
6161
--channelpoint string funding transaction outpoint of the channel to rescue (<txid>:<txindex>) as it is displayed on 1ml.com
6262
--from_channel_graph string the full LN channel graph in the JSON format that the 'lncli describegraph' returns
6363
-h, --help help for fakechanbackup
64-
--multi_file string the fake channel backup file to create (default "results/fake-2022-09-11-19-20-32.backup")
64+
--multi_file string the fake channel backup file to create (default "results/fake-2023-02-25-14-15-10.backup")
6565
--remote_node_addr string the remote node connection information in the format pubkey@host:port
6666
--rootkey string BIP32 HD root key of the wallet to use for encrypting the backup; leave empty to prompt for lnd 24 word aezeed
6767
--short_channel_id string the short channel ID in the format <blockheight>x<transactionindex>x<outputindex>

doc/chantools_triggerforceclose.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
## chantools triggerforceclose
2+
3+
Connect to a peer and send a custom message to trigger a force close of the specified channel
4+
5+
```
6+
chantools triggerforceclose [flags]
7+
```
8+
9+
### Examples
10+
11+
```
12+
chantools triggerforceclose \
13+
--peer [email protected]:9735 \
14+
--channel_point abcdef01234...:x
15+
```
16+
17+
### Options
18+
19+
```
20+
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
21+
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
22+
--channel_point string funding transaction outpoint of the channel to trigger the force close of (<txid>:<txindex>)
23+
-h, --help help for triggerforceclose
24+
--peer string remote peer address (<pubkey>@<host>[:<port>])
25+
--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
26+
```
27+
28+
### Options inherited from parent commands
29+
30+
```
31+
-r, --regtest Indicates if regtest parameters should be used
32+
-t, --testnet Indicates if testnet parameters should be used
33+
```
34+
35+
### SEE ALSO
36+
37+
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
38+

0 commit comments

Comments
 (0)