-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
fe356a4
commit 2d3fec3
Showing
3 changed files
with
213 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters