mirror of https://github.com/guggero/chantools
add new command to clean height hint cache.
It was observed that the height hint cache is poisoned leading to unresolved contracts in lnd. This command is a temporary fix for node runners until the real reason for this behaviour is found.pull/80/head
parent
fe356a4648
commit
2d3fec3513
@ -0,0 +1,211 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/chantools/btc"
|
||||
"github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var spendHintBucket = []byte("spend-hints")
|
||||
|
||||
type dropHeightHintCacheCommand struct {
|
||||
APIURL string
|
||||
ChannelDB string
|
||||
ChanPoint string
|
||||
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func newDropHeightHintCacheCommand() *cobra.Command {
|
||||
cc := &dropHeightHintCacheCommand{}
|
||||
cc.cmd = &cobra.Command{
|
||||
Use: "dropheighthintcache",
|
||||
Short: "Remove all height hints used for spend notifications",
|
||||
Long: `Removes either all spent height hint entries for
|
||||
channels remaining in the __waiting_force_close__ state or for an explicit
|
||||
outpoint which leads to an internal rescan resolving all contracts already due.`,
|
||||
Example: `chantools dropheighthintcache \
|
||||
--channeldb ~/.lnd/data/graph/mainnet/channel.db \
|
||||
-chan_point bd278162f98...ecbab00764c8a1:0`,
|
||||
RunE: cc.Execute,
|
||||
}
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.ChannelDB, "channeldb", "", "lnd channel.db file to dump "+
|
||||
"channels from",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.ChanPoint, "chan_point", "", "outpoint for which the "+
|
||||
"height should be removed ",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
|
||||
"be esplora compatible)",
|
||||
)
|
||||
return cc.cmd
|
||||
}
|
||||
|
||||
func (c *dropHeightHintCacheCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||
if c.ChannelDB == "" {
|
||||
return fmt.Errorf("channel DB is required")
|
||||
}
|
||||
|
||||
db, err := lnd.OpenDB(c.ChannelDB, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening rescue DB: %w", err)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
if c.ChanPoint != "" {
|
||||
return dropHeightHintOutpoint(db, c.ChanPoint, c.APIURL)
|
||||
}
|
||||
|
||||
// In case no channel point is selected we will only remove the spent
|
||||
// hint for channels which are borked and in the state
|
||||
// __waiting_close__ (fundingTx not yet confirmed).
|
||||
err = dropHeightHintFundingTx(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// dropHeightHintFundingTx queries the underlying channel.db for channels which
|
||||
// are in the __waiting_close_channels__ bucket. This means the channel is
|
||||
// already borked but the funding tx has still not been spent. We observed in
|
||||
// some cases that the relevant height hint cache was poisoned leading to an
|
||||
// unrecognized closed channel. Deleting the underlying height hint should
|
||||
// tigger a rescan form an earlier blockheight and therefore finding the
|
||||
// confirmed fundingTx.
|
||||
func dropHeightHintFundingTx(db *channeldb.DB) error {
|
||||
// We only fetch the waiting force close channels.
|
||||
channels, err := db.ChannelStateDB().FetchWaitingCloseChannels()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
spendRequests := make([]*chainntnfs.SpendRequest, 0, len(channels))
|
||||
|
||||
for _, channel := range channels {
|
||||
spendRequests = append(spendRequests, &chainntnfs.SpendRequest{
|
||||
OutPoint: channel.FundingOutpoint,
|
||||
// We index the SpendRequest entry in the db by the
|
||||
// outpoint value (for the channel close observer at
|
||||
// least).
|
||||
PkScript: txscript.PkScript{},
|
||||
})
|
||||
}
|
||||
|
||||
// We resolve all the waiting force close channels which might have
|
||||
// a poisoned height hint cache.
|
||||
return kvdb.Batch(db.Backend, func(tx kvdb.RwTx) error {
|
||||
spendHints := tx.ReadWriteBucket(spendHintBucket)
|
||||
if spendHints == nil {
|
||||
return chainntnfs.ErrCorruptedHeightHintCache
|
||||
}
|
||||
|
||||
for _, request := range spendRequests {
|
||||
var outpoint bytes.Buffer
|
||||
err := channeldb.WriteElement(
|
||||
&outpoint, request.OutPoint,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
spendKey := outpoint.Bytes()
|
||||
if err := spendHints.Delete(spendKey); err != nil {
|
||||
log.Debugf("outpoint not found in the height "+
|
||||
"hint cache: "+
|
||||
"%v", request.OutPoint.String())
|
||||
|
||||
return err
|
||||
}
|
||||
log.Infof("deleted height hint for outpoint: "+
|
||||
"%v \n", request.OutPoint.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// dropHeightHintOutpoint deletes the height hint cache for a specific outpoint.
|
||||
// Sometimes a channel is stuck in a pending state because the spend of a
|
||||
// channel contract was not recognized. In other words the height hint cache
|
||||
// for this outpoint was poisoned and we need to delete its value so we trigger
|
||||
// a clean rescan from the initial height of the channel contract.
|
||||
func dropHeightHintOutpoint(db *channeldb.DB, chanPoint, apiURL string) error {
|
||||
api := &btc.ExplorerAPI{BaseURL: apiURL}
|
||||
// Check that the outpoint is really spent
|
||||
addr, err := api.Address(chanPoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
spends, err := api.Spends(addr)
|
||||
if err != nil || len(spends) == 0 {
|
||||
return fmt.Errorf("outpoint is not spend yet")
|
||||
}
|
||||
outPoint, err := parseChanPoint(chanPoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return kvdb.Update(db.Backend, func(tx kvdb.RwTx) error {
|
||||
spendHints := tx.ReadWriteBucket(spendHintBucket)
|
||||
if spendHints == nil {
|
||||
return chainntnfs.ErrCorruptedHeightHintCache
|
||||
}
|
||||
|
||||
var outPointBytes bytes.Buffer
|
||||
err := channeldb.WriteElement(
|
||||
&outPointBytes, outPoint,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
spendKey := outPointBytes.Bytes()
|
||||
if err := spendHints.Delete(spendKey); err != nil {
|
||||
log.Debugf("outpoint not found in the height "+
|
||||
"hint cache: "+
|
||||
"%v", outPoint.String())
|
||||
|
||||
return err
|
||||
}
|
||||
log.Infof("deleted height hint for outpoint: "+
|
||||
"%v \n", outPoint.String())
|
||||
|
||||
return nil
|
||||
}, func() {})
|
||||
}
|
||||
|
||||
func parseChanPoint(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")
|
||||
}
|
||||
|
||||
index, err := strconv.ParseInt(split[1], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode output index: %w", err)
|
||||
}
|
||||
|
||||
txid, err := chainhash.NewHashFromStr(split[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse hex string: %w", err)
|
||||
}
|
||||
|
||||
return &wire.OutPoint{Hash: *txid,
|
||||
Index: uint32(index)}, nil
|
||||
}
|
Loading…
Reference in New Issue