You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
chantools/cmd/chantools/forceclose.go

234 lines
6.1 KiB
Go

package main
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"time"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/txscript"
"github.com/lightninglabs/chantools/dataformat"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input"
"github.com/spf13/cobra"
)
type forceCloseCommand struct {
APIURL string
ChannelDB string
Publish bool
rootKey *rootKey
inputs *inputFlags
cmd *cobra.Command
}
func newForceCloseCommand() *cobra.Command {
cc := &forceCloseCommand{}
cc.cmd = &cobra.Command{
Use: "forceclose",
Short: "Force-close the last state that is in the channel.db " +
"provided",
Long: `If you are certain that a node is offline for good (AFTER
you've tried SCB!) and a channel is still open, you can use this method to
force-close your latest state that you have in your channel.db.
**!!! WARNING !!! DANGER !!! WARNING !!!**
If you do this and the state that you publish is *not* the latest state, then
the remote node *could* punish you by taking the whole channel amount *if* they
come online before you can sweep the funds from the time locked (144 - 2000
blocks) transaction *or* they have a watch tower looking out for them.
**This should absolutely be the last resort and you have been warned!**`,
Example: `chantools forceclose \
--fromsummary results/summary-xxxx-yyyy.json
--channeldb ~/.lnd/data/graph/mainnet/channel.db \
--publish`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
"be esplora compatible)",
)
cc.cmd.Flags().StringVar(
&cc.ChannelDB, "channeldb", "", "lnd channel.db file to use "+
"for force-closing channels",
)
cc.cmd.Flags().BoolVar(
&cc.Publish, "publish", false, "publish force-closing TX to "+
"the chain API instead of just printing the TX",
)
cc.rootKey = newRootKey(cc.cmd, "decrypting the backup")
cc.inputs = newInputFlags(cc.cmd)
return cc.cmd
}
func (c *forceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
extendedKey, err := c.rootKey.read()
if err != nil {
return fmt.Errorf("error reading root key: %w", err)
}
// Check that we have a channel DB.
if c.ChannelDB == "" {
return fmt.Errorf("rescue DB is required")
}
db, err := lnd.OpenDB(c.ChannelDB, true)
if err != nil {
return fmt.Errorf("error opening rescue DB: %w", err)
}
// Parse channel entries from any of the possible input files.
entries, err := c.inputs.parseInputType()
if err != nil {
return err
}
return forceCloseChannels(
c.APIURL, extendedKey, entries, db.ChannelStateDB(), c.Publish,
)
}
func forceCloseChannels(apiURL string, extendedKey *hdkeychain.ExtendedKey,
entries []*dataformat.SummaryEntry, chanDb *channeldb.ChannelStateDB,
publish bool) error {
channels, err := chanDb.FetchAllChannels()
if err != nil {
return err
}
api := newExplorerAPI(apiURL)
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
// Go through all channels in the DB, find the still open ones and
// publish their local commitment TX.
for _, channel := range channels {
channelPoint := channel.FundingOutpoint.String()
var channelEntry *dataformat.SummaryEntry
for _, entry := range entries {
if entry.ChannelPoint == channelPoint {
channelEntry = entry
}
}
// Don't try anything with closed channels.
if channelEntry == nil || channelEntry.ClosingTX != nil {
continue
}
localCommit := channel.LocalCommitment
localCommitTx := localCommit.CommitTx
if localCommitTx == nil {
log.Errorf("Cannot force-close, no local commit TX "+
"for channel %s", channelEntry.ChannelPoint)
continue
}
// Create signed transaction.
lc := &lnd.LightningChannel{
LocalChanCfg: channel.LocalChanCfg,
RemoteChanCfg: channel.RemoteChanCfg,
ChannelState: channel,
TXSigner: signer,
}
err := lc.CreateSignDesc()
if err != nil {
return err
}
// Serialize transaction.
signedTx, err := lc.SignedCommitTx()
if err != nil {
return err
}
var buf bytes.Buffer
err = signedTx.Serialize(io.Writer(&buf))
if err != nil {
return err
}
hash := signedTx.TxHash()
serialized := hex.EncodeToString(buf.Bytes())
// Calculate commit point.
basepoint := channel.LocalChanCfg.DelayBasePoint
revpoint := channel.RemoteChanCfg.RevocationBasePoint
revocationPreimage, err := channel.RevocationProducer.AtIndex(
localCommit.CommitHeight,
)
if err != nil {
return err
}
point := input.ComputeCommitmentPoint(revocationPreimage[:])
// Store all information that we collected into the channel
// entry file so we don't need to use the channel.db file for
// the next step.
channelEntry.ForceClose = &dataformat.ForceClose{
TXID: hash.String(),
Serialized: serialized,
DelayBasePoint: &dataformat.BasePoint{
Family: uint16(basepoint.Family),
Index: basepoint.Index,
PubKey: hex.EncodeToString(
basepoint.PubKey.SerializeCompressed(),
),
},
RevocationBasePoint: &dataformat.BasePoint{
PubKey: hex.EncodeToString(
revpoint.PubKey.SerializeCompressed(),
),
},
CommitPoint: hex.EncodeToString(
point.SerializeCompressed(),
),
Outs: make(
[]*dataformat.Out, len(localCommitTx.TxOut),
),
CSVDelay: channel.LocalChanCfg.CsvDelay,
}
for idx, out := range localCommitTx.TxOut {
script, err := txscript.DisasmString(out.PkScript)
if err != nil {
return err
}
channelEntry.ForceClose.Outs[idx] = &dataformat.Out{
Script: hex.EncodeToString(out.PkScript),
ScriptAsm: script,
Value: uint64(out.Value),
}
}
// Publish TX.
if publish {
response, err := api.PublishTx(serialized)
if err != nil {
return err
}
log.Infof("Published TX %s, response: %s",
hash.String(), response)
}
}
summaryBytes, err := json.MarshalIndent(&dataformat.SummaryEntryFile{
Channels: entries,
}, "", " ")
if err != nil {
return err
}
fileName := fmt.Sprintf("results/forceclose-%s.json",
time.Now().Format("2006-01-02-15-04-05"))
log.Infof("Writing result to %s", fileName)
return ioutil.WriteFile(fileName, summaryBytes, 0644)
}