Skip to content

Commit

Permalink
multi: add triggerforceclose command
Browse files Browse the repository at this point in the history
  • Loading branch information
guggero committed Feb 25, 2023
1 parent a01dd32 commit 083453e
Show file tree
Hide file tree
Showing 9 changed files with 516 additions and 5 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,27 +277,34 @@ Usage:
Available Commands:
chanbackup Create a channel.backup file from a channel database
closepoolaccount Tries to close a Pool account that has expired
compactdb Create a copy of a channel.db file in safe/read-only mode
deletepayments Remove all (failed) payments from a channel DB
derivekey Derive a key with a specific derivation path
dropchannelgraph Remove all graph related data from a channel DB
dumpbackup Dump the content of a channel.backup file
dumpchannels Dump all channel information from an lnd channel database
fakechanbackup Fake a channel backup file to attempt fund recovery
filterbackup Filter an lnd channel.backup file and remove certain channels
fixoldbackup Fixes an old channel.backup file that is affected by the lnd issue #3881 (unable to derive shachain root key)
forceclose Force-close the last state that is in the channel.db provided
genimportscript Generate a script containing the on-chain keys of an lnd wallet that can be imported into other software like bitcoind
help Help about any command
migratedb Apply all recent lnd channel database migrations
removechannel Remove a single channel from the given channel DB
rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels
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
rescuetweakedkey Attempt to rescue funds locked in an address with a key that was affected by a specific bug in lnd
showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed
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
summary Compile a summary about the current state of channels
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
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
help Help about any command
Flags:
-h, --help help for chantools
Expand Down Expand Up @@ -336,6 +343,7 @@ Quick access:
+ [sweepremoteclosed](doc/chantools_sweepremoteclosed.md)
+ [sweeptimelock](doc/chantools_sweeptimelock.md)
+ [sweeptimelockmanual](doc/chantools_sweeptimelockmanual.md)
+ [triggerforceclose](doc/chantools_triggerforceclose.md)
+ [vanitygen](doc/chantools_vanitygen.md)
+ [walletinfo](doc/chantools_walletinfo.md)
+ [zombierecovery](doc/chantools_zombierecovery.md)
26 changes: 25 additions & 1 deletion btc/explorer_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ func (a *ExplorerAPI) Transaction(txid string) (*TX, error) {

func (a *ExplorerAPI) Outpoint(addr string) (*TX, int, error) {
var txs []*TX
err := fetchJSON(fmt.Sprintf("%s/address/%s/txs", a.BaseURL, addr), &txs)
err := fetchJSON(
fmt.Sprintf("%s/address/%s/txs", a.BaseURL, addr), &txs,
)
if err != nil {
return nil, 0, err
}
Expand All @@ -104,6 +106,28 @@ func (a *ExplorerAPI) Outpoint(addr string) (*TX, int, error) {
return nil, 0, fmt.Errorf("no tx found")
}

func (a *ExplorerAPI) Spends(addr string) ([]*TX, error) {
var txs []*TX
err := fetchJSON(
fmt.Sprintf("%s/address/%s/txs", a.BaseURL, addr), &txs,
)
if err != nil {
return nil, err
}

var spends []*TX
for txIndex := range txs {
tx := txs[txIndex]
for _, vin := range tx.Vin {
if vin.Prevout.ScriptPubkeyAddr == addr {
spends = append(spends, tx)
}
}
}

return spends, nil
}

func (a *ExplorerAPI) Unspent(addr string) ([]*Vout, error) {
var (
stats = &AddressStats{}
Expand Down
2 changes: 1 addition & 1 deletion cmd/chantools/dropchannelgraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func newChanAnnouncement(localPubKey, remotePubKey *btcec.PublicKey,

// Our channel update message flags will signal that we support the
// max_htlc field.
msgFlags := lnwire.ChanUpdateOptionMaxHtlc
msgFlags := lnwire.ChanUpdateRequiredMaxHtlc

// We announce the channel with the default values. Some of
// these values can later be changed by crafting a new ChannelUpdate.
Expand Down
5 changes: 4 additions & 1 deletion cmd/chantools/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ import (
"github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/peer"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
)

const (
defaultAPIURL = "https://blockstream.info/api"
version = "0.10.6"
version = "0.10.7"
na = "n/a"

Commit = ""
Expand Down Expand Up @@ -104,6 +105,7 @@ func main() {
newSweepTimeLockCommand(),
newSweepTimeLockManualCommand(),
newSweepRemoteClosedCommand(),
newTriggerForceCloseCommand(),
newVanityGenCommand(),
newWalletInfoCommand(),
newZombieRecoveryCommand(),
Expand Down Expand Up @@ -263,6 +265,7 @@ func setupLogging() {
setSubLogger("CHAN", log)
addSubLogger("CHDB", channeldb.UseLogger)
addSubLogger("BCKP", chanbackup.UseLogger)
addSubLogger("PEER", peer.UseLogger)
err := logWriter.InitLogRotator("./results/chantools.log", 10, 3)
if err != nil {
panic(err)
Expand Down
202 changes: 202 additions & 0 deletions cmd/chantools/triggerforceclose.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package main

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

"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/connmgr"
"github.com/btcsuite/btcd/wire"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/lnd"
"github.com/lightningnetwork/lnd/brontide"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tor"
"github.com/spf13/cobra"
)

var (
dialTimeout = time.Minute
)

type triggerForceCloseCommand struct {
Peer string
ChannelPoint string

APIURL string

rootKey *rootKey
cmd *cobra.Command
}

func newTriggerForceCloseCommand() *cobra.Command {
cc := &triggerForceCloseCommand{}
cc.cmd = &cobra.Command{
Use: "triggerforceclose",
Short: "Connect to a peer and send a custom message to " +
"trigger a force close of the specified channel",
Example: `chantools triggerforceclose \
--peer 03abce...@xx.yy.zz.aa:9735 \
--channel_point abcdef01234...:x`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.Peer, "peer", "", "remote peer address "+
"(<pubkey>@<host>[:<port>])",
)
cc.cmd.Flags().StringVar(
&cc.ChannelPoint, "channel_point", "", "funding transaction "+
"outpoint of the channel to trigger the force close "+
"of (<txid>:<txindex>)",
)
cc.cmd.Flags().StringVar(
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
"be esplora compatible)",
)
cc.rootKey = newRootKey(cc.cmd, "deriving the identity key")

return cc.cmd
}

func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
extendedKey, err := c.rootKey.read()
if err != nil {
return fmt.Errorf("error reading root key: %w", err)
}

identityPath := lnd.IdentityPath(chainParams)
child, pubKey, _, err := lnd.DeriveKey(
extendedKey, identityPath, chainParams,
)
if err != nil {
return fmt.Errorf("could not derive identity key: %w", err)
}
identityPriv, err := child.ECPrivKey()
if err != nil {
return fmt.Errorf("could not get identity private key: %w", err)
}
identityECDH := &keychain.PrivKeyECDH{
PrivKey: identityPriv,
}

peerAddr, err := lncfg.ParseLNAddressString(
c.Peer, "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)

conn, err := noiseDial(
identityECDH, 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)
req := &connmgr.ConnReq{
Addr: peerAddr,
Permanent: false,
}
p, err := lnd.ConnectPeer(conn, req, chainParams, identityECDH)
if err != nil {
return fmt.Errorf("error connecting to peer: %w", err)
}

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

// We'll wait until the peer is active.
select {
case <-p.ActiveSignal():
case <-p.QuitSignal():
return fmt.Errorf("peer %x disconnected",
pubKey.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)

_ = lnwire.SetCustomOverrides([]uint16{lnwire.MsgError})
msg, err := lnwire.NewCustom(lnwire.MsgError, data)
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")

api := &btc.ExplorerAPI{BaseURL: 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 parseOutPoint(s string) (*wire.OutPoint, error) {
split := strings.Split(s, ":")
if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
return nil, fmt.Errorf("invalid channel point format: %v", s)
}

index, err := strconv.ParseInt(split[1], 10, 64)
if err != nil {
return nil, fmt.Errorf("unable to decode output index: %v", err)
}

txid, err := chainhash.NewHashFromStr(split[0])
if err != nil {
return nil, fmt.Errorf("unable to parse hex string: %v", err)
}

return &wire.OutPoint{
Hash: *txid,
Index: uint32(index),
}, nil
}
1 change: 1 addition & 0 deletions doc/chantools.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Complete documentation is available at https://github.com/guggero/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 peer and send a custom message 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_fakechanbackup.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ chantools fakechanbackup --from_channel_graph lncli_describegraph.json \
--channelpoint string funding transaction outpoint of the channel to rescue (<txid>:<txindex>) as it is displayed on 1ml.com
--from_channel_graph string the full LN channel graph in the JSON format that the 'lncli describegraph' returns
-h, --help help for fakechanbackup
--multi_file string the fake channel backup file to create (default "results/fake-2022-09-11-19-20-32.backup")
--multi_file string the fake channel backup file to create (default "results/fake-2023-02-25-14-15-10.backup")
--remote_node_addr string the remote node connection information in the format pubkey@host:port
--rootkey string BIP32 HD root key of the wallet to use for encrypting the backup; leave empty to prompt for lnd 24 word aezeed
--short_channel_id string the short channel ID in the format <blockheight>x<transactionindex>x<outputindex>
Expand Down
38 changes: 38 additions & 0 deletions doc/chantools_triggerforceclose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## chantools triggerforceclose

Connect to a peer and send a custom message to trigger a force close of the specified channel

```
chantools triggerforceclose [flags]
```

### Examples

```
chantools triggerforceclose \
--peer 03abce...@xx.yy.zz.aa:9735 \
--channel_point abcdef01234...:x
```

### Options

```
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--channel_point string funding transaction outpoint of the channel to trigger the force close of (<txid>:<txindex>)
-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
```

### Options inherited from parent commands

```
-r, --regtest Indicates if regtest parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```

### SEE ALSO

* [chantools](chantools.md) - Chantools helps recover funds from lightning channels

Loading

0 comments on commit 083453e

Please sign in to comment.