multi: add triggerforceclose command

pull/59/head v0.10.7
Oliver Gugger 1 year ago
parent a01dd32e55
commit 083453ef5d
No known key found for this signature in database
GPG Key ID: 8E4256593F177720

@ -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
@ -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)

@ -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
}
@ -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{}

@ -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.

@ -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 = ""
@ -104,6 +105,7 @@ func main() {
newSweepTimeLockCommand(),
newSweepTimeLockManualCommand(),
newSweepRemoteClosedCommand(),
newTriggerForceCloseCommand(),
newVanityGenCommand(),
newWalletInfoCommand(),
newZombieRecoveryCommand(),
@ -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)

@ -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
}

@ -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

@ -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>

@ -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

@ -0,0 +1,235 @@
package lnd
import (
"fmt"
"os"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/connmgr"
"github.com/lightningnetwork/lnd/aliasmgr"
"github.com/lightningnetwork/lnd/brontide"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/discovery"
"github.com/lightningnetwork/lnd/feature"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/htlcswitch/hodl"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnpeer"
"github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/netann"
"github.com/lightningnetwork/lnd/peer"
"github.com/lightningnetwork/lnd/pool"
"github.com/lightningnetwork/lnd/queue"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/ticker"
)
const (
defaultChannelCommitBatchSize = 10
defaultCoopCloseTargetConfs = 6
)
var (
chanEnableTimeout = 19 * time.Minute
defaultChannelCommitInterval = 50 * time.Millisecond
defaultPendingCommitInterval = 1 * time.Minute
)
func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
netParams *chaincfg.Params,
identityECDH keychain.SingleKeyECDH) (*peer.Brontide, error) {
featureMgr, err := feature.NewManager(feature.Config{})
if err != nil {
return nil, err
}
initFeatures := featureMgr.Get(feature.SetInit)
legacyFeatures := featureMgr.Get(feature.SetLegacyGlobal)
addr := conn.RemoteAddr()
pubKey := conn.RemotePub()
peerAddr := &lnwire.NetAddress{
IdentityKey: pubKey,
Address: addr,
ChainNet: netParams.Net,
}
errBuffer, err := queue.NewCircularBuffer(500)
if err != nil {
return nil, err
}
pongBuf := make([]byte, lnwire.MaxPongBytes)
writeBufferPool := pool.NewWriteBuffer(
pool.DefaultWriteBufferGCInterval,
pool.DefaultWriteBufferExpiryInterval,
)
writePool := pool.NewWrite(
writeBufferPool, lncfg.DefaultWriteWorkers,
pool.DefaultWorkerTimeout,
)
readBufferPool := pool.NewReadBuffer(
pool.DefaultReadBufferGCInterval,
pool.DefaultReadBufferExpiryInterval,
)
readPool := pool.NewRead(
readBufferPool, lncfg.DefaultWriteWorkers,
pool.DefaultWorkerTimeout,
)
commitFee := chainfee.SatPerKVByte(
lnwallet.DefaultAnchorsCommitMaxFeeRateSatPerVByte * 1000,
)
if err := writePool.Start(); err != nil {
return nil, fmt.Errorf("unable to start write pool: %v", err)
}
if err := readPool.Start(); err != nil {
return nil, fmt.Errorf("unable to start read pool: %v", err)
}
channelDB, err := channeldb.Open(os.TempDir())
if err != nil {
return nil, err
}
gossiper := discovery.New(discovery.Config{
ChainHash: *netParams.GenesisHash,
Broadcast: func(skips map[route.Vertex]struct{},
msg ...lnwire.Message) error {
return nil
},
NotifyWhenOnline: func(peerPubKey [33]byte,
peerChan chan<- lnpeer.Peer) {
},
NotifyWhenOffline: func(peerPubKey [33]byte) <-chan struct{} {
return make(chan struct{})
},
SelfNodeAnnouncement: func(
refresh bool) (lnwire.NodeAnnouncement, error) {
return lnwire.NodeAnnouncement{}, nil
},
ProofMatureDelta: 0,
TrickleDelay: time.Millisecond * 50,
RetransmitTicker: ticker.New(time.Minute * 30),
RebroadcastInterval: time.Hour * 24,
RotateTicker: ticker.New(discovery.DefaultSyncerRotationInterval),
HistoricalSyncTicker: ticker.New(discovery.DefaultHistoricalSyncInterval),
NumActiveSyncers: 0,
MinimumBatchSize: 10,
SubBatchDelay: discovery.DefaultSubBatchDelay,
IgnoreHistoricalFilters: true,
PinnedSyncers: make(map[route.Vertex]struct{}),
MaxChannelUpdateBurst: discovery.DefaultMaxChannelUpdateBurst,
ChannelUpdateInterval: discovery.DefaultChannelUpdateInterval,
IsAlias: aliasmgr.IsAlias,
SignAliasUpdate: func(u *lnwire.ChannelUpdate) (*ecdsa.Signature, error) {
return nil, fmt.Errorf("unimplemented")
},
FindBaseByAlias: func(alias lnwire.ShortChannelID) (lnwire.ShortChannelID, error) {
return lnwire.ShortChannelID{}, fmt.Errorf("unimplemented")
},
GetAlias: func(id lnwire.ChannelID) (lnwire.ShortChannelID, error) {
return lnwire.ShortChannelID{}, fmt.Errorf("unimplemented")
},
FindChannel: func(node *btcec.PublicKey,
chanID lnwire.ChannelID) (*channeldb.OpenChannel, error) {
return nil, fmt.Errorf("unimplemented")
},
}, &keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{},
PubKey: identityECDH.PubKey(),
})
pCfg := peer.Config{
Conn: conn,
ConnReq: connReq,
Addr: peerAddr,
Inbound: false,
Features: initFeatures,
LegacyFeatures: legacyFeatures,
OutgoingCltvRejectDelta: lncfg.DefaultOutgoingCltvRejectDelta,
ChanActiveTimeout: chanEnableTimeout,
ErrorBuffer: errBuffer,
WritePool: writePool,
ReadPool: readPool,
ChannelDB: channelDB.ChannelStateDB(),
AuthGossiper: gossiper,
ChainNotifier: &mock.ChainNotifier{},
DisconnectPeer: func(key *btcec.PublicKey) error {
fmt.Printf("Peer %x disconnected\n",
key.SerializeCompressed())
return nil
},
GenNodeAnnouncement: func(b bool,
modifier ...netann.NodeAnnModifier) (
lnwire.NodeAnnouncement, error) {
return lnwire.NodeAnnouncement{},
fmt.Errorf("unimplemented")
},
PongBuf: pongBuf,
PrunePersistentPeerConnection: func(bytes [33]byte) {},
FetchLastChanUpdate: func(id lnwire.ShortChannelID) (
*lnwire.ChannelUpdate, error) {
return nil, fmt.Errorf("unimplemented")
},
Hodl: &hodl.Config{},
UnsafeReplay: false,
MaxOutgoingCltvExpiry: htlcswitch.DefaultMaxOutgoingCltvExpiry,
MaxChannelFeeAllocation: htlcswitch.DefaultMaxLinkFeeAllocation,
CoopCloseTargetConfs: defaultCoopCloseTargetConfs,
MaxAnchorsCommitFeeRate: commitFee.FeePerKWeight(),
ChannelCommitInterval: defaultChannelCommitInterval,
PendingCommitInterval: defaultPendingCommitInterval,
ChannelCommitBatchSize: defaultChannelCommitBatchSize,
HandleCustomMessage: func(peer [33]byte,
msg *lnwire.Custom) error {
fmt.Printf("Received custom message from %x: %v\n",
peer[:], msg)
return nil
},
GetAliases: func(
base lnwire.ShortChannelID) []lnwire.ShortChannelID {
return nil
},
RequestAlias: func() (lnwire.ShortChannelID, error) {
return lnwire.ShortChannelID{}, nil
},
AddLocalAlias: func(alias, base lnwire.ShortChannelID,
gossip bool) error {
return nil
},
Quit: make(chan struct{}),
}
copy(pCfg.PubKeyBytes[:], peerAddr.IdentityKey.SerializeCompressed())
copy(pCfg.ServerPubKey[:], identityECDH.PubKey().SerializeCompressed())
p := peer.NewBrontide(pCfg)
if err := p.Start(); err != nil {
return nil, err
}
return p, nil
}
Loading…
Cancel
Save