Skip to content

Commit 2d3fec3

Browse files
committed
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.
1 parent fe356a4 commit 2d3fec3

File tree

3 files changed

+213
-0
lines changed

3 files changed

+213
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ Available Commands:
417417
dropgraphzombies Remove all channels identified as zombies from the graph to force a re-sync of the graph
418418
dumpbackup Dump the content of a channel.backup file
419419
dumpchannels Dump all channel information from an lnd channel database
420+
dropheighthintcache Remove all height hint cache data from the channel DB.
420421
fakechanbackup Fake a channel backup file to attempt fund recovery
421422
filterbackup Filter an lnd channel.backup file and remove certain channels
422423
fixoldbackup Fixes an old channel.backup file that is affected by the lnd issue #3881 (unable to derive shachain root key)

cmd/chantools/dropheighthintcache.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"strconv"
7+
"strings"
8+
9+
"github.com/btcsuite/btcd/chaincfg/chainhash"
10+
"github.com/btcsuite/btcd/txscript"
11+
"github.com/btcsuite/btcd/wire"
12+
"github.com/lightninglabs/chantools/btc"
13+
"github.com/lightninglabs/chantools/lnd"
14+
"github.com/lightningnetwork/lnd/chainntnfs"
15+
"github.com/lightningnetwork/lnd/channeldb"
16+
"github.com/lightningnetwork/lnd/kvdb"
17+
"github.com/spf13/cobra"
18+
)
19+
20+
var spendHintBucket = []byte("spend-hints")
21+
22+
type dropHeightHintCacheCommand struct {
23+
APIURL string
24+
ChannelDB string
25+
ChanPoint string
26+
27+
cmd *cobra.Command
28+
}
29+
30+
func newDropHeightHintCacheCommand() *cobra.Command {
31+
cc := &dropHeightHintCacheCommand{}
32+
cc.cmd = &cobra.Command{
33+
Use: "dropheighthintcache",
34+
Short: "Remove all height hints used for spend notifications",
35+
Long: `Removes either all spent height hint entries for
36+
channels remaining in the __waiting_force_close__ state or for an explicit
37+
outpoint which leads to an internal rescan resolving all contracts already due.`,
38+
Example: `chantools dropheighthintcache \
39+
--channeldb ~/.lnd/data/graph/mainnet/channel.db \
40+
-chan_point bd278162f98...ecbab00764c8a1:0`,
41+
RunE: cc.Execute,
42+
}
43+
cc.cmd.Flags().StringVar(
44+
&cc.ChannelDB, "channeldb", "", "lnd channel.db file to dump "+
45+
"channels from",
46+
)
47+
cc.cmd.Flags().StringVar(
48+
&cc.ChanPoint, "chan_point", "", "outpoint for which the "+
49+
"height should be removed ",
50+
)
51+
cc.cmd.Flags().StringVar(
52+
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
53+
"be esplora compatible)",
54+
)
55+
return cc.cmd
56+
}
57+
58+
func (c *dropHeightHintCacheCommand) Execute(_ *cobra.Command, _ []string) error {
59+
if c.ChannelDB == "" {
60+
return fmt.Errorf("channel DB is required")
61+
}
62+
63+
db, err := lnd.OpenDB(c.ChannelDB, false)
64+
if err != nil {
65+
return fmt.Errorf("error opening rescue DB: %w", err)
66+
}
67+
defer func() { _ = db.Close() }()
68+
69+
if c.ChanPoint != "" {
70+
return dropHeightHintOutpoint(db, c.ChanPoint, c.APIURL)
71+
}
72+
73+
// In case no channel point is selected we will only remove the spent
74+
// hint for channels which are borked and in the state
75+
// __waiting_close__ (fundingTx not yet confirmed).
76+
err = dropHeightHintFundingTx(db)
77+
if err != nil {
78+
return err
79+
}
80+
81+
return nil
82+
}
83+
84+
// dropHeightHintFundingTx queries the underlying channel.db for channels which
85+
// are in the __waiting_close_channels__ bucket. This means the channel is
86+
// already borked but the funding tx has still not been spent. We observed in
87+
// some cases that the relevant height hint cache was poisoned leading to an
88+
// unrecognized closed channel. Deleting the underlying height hint should
89+
// tigger a rescan form an earlier blockheight and therefore finding the
90+
// confirmed fundingTx.
91+
func dropHeightHintFundingTx(db *channeldb.DB) error {
92+
// We only fetch the waiting force close channels.
93+
channels, err := db.ChannelStateDB().FetchWaitingCloseChannels()
94+
if err != nil {
95+
return err
96+
}
97+
98+
spendRequests := make([]*chainntnfs.SpendRequest, 0, len(channels))
99+
100+
for _, channel := range channels {
101+
spendRequests = append(spendRequests, &chainntnfs.SpendRequest{
102+
OutPoint: channel.FundingOutpoint,
103+
// We index the SpendRequest entry in the db by the
104+
// outpoint value (for the channel close observer at
105+
// least).
106+
PkScript: txscript.PkScript{},
107+
})
108+
}
109+
110+
// We resolve all the waiting force close channels which might have
111+
// a poisoned height hint cache.
112+
return kvdb.Batch(db.Backend, func(tx kvdb.RwTx) error {
113+
spendHints := tx.ReadWriteBucket(spendHintBucket)
114+
if spendHints == nil {
115+
return chainntnfs.ErrCorruptedHeightHintCache
116+
}
117+
118+
for _, request := range spendRequests {
119+
var outpoint bytes.Buffer
120+
err := channeldb.WriteElement(
121+
&outpoint, request.OutPoint,
122+
)
123+
if err != nil {
124+
return err
125+
}
126+
127+
spendKey := outpoint.Bytes()
128+
if err := spendHints.Delete(spendKey); err != nil {
129+
log.Debugf("outpoint not found in the height "+
130+
"hint cache: "+
131+
"%v", request.OutPoint.String())
132+
133+
return err
134+
}
135+
log.Infof("deleted height hint for outpoint: "+
136+
"%v \n", request.OutPoint.String())
137+
}
138+
139+
return nil
140+
})
141+
}
142+
143+
// dropHeightHintOutpoint deletes the height hint cache for a specific outpoint.
144+
// Sometimes a channel is stuck in a pending state because the spend of a
145+
// channel contract was not recognized. In other words the height hint cache
146+
// for this outpoint was poisoned and we need to delete its value so we trigger
147+
// a clean rescan from the initial height of the channel contract.
148+
func dropHeightHintOutpoint(db *channeldb.DB, chanPoint, apiURL string) error {
149+
api := &btc.ExplorerAPI{BaseURL: apiURL}
150+
// Check that the outpoint is really spent
151+
addr, err := api.Address(chanPoint)
152+
if err != nil {
153+
return err
154+
}
155+
spends, err := api.Spends(addr)
156+
if err != nil || len(spends) == 0 {
157+
return fmt.Errorf("outpoint is not spend yet")
158+
}
159+
outPoint, err := parseChanPoint(chanPoint)
160+
if err != nil {
161+
return err
162+
}
163+
164+
return kvdb.Update(db.Backend, func(tx kvdb.RwTx) error {
165+
spendHints := tx.ReadWriteBucket(spendHintBucket)
166+
if spendHints == nil {
167+
return chainntnfs.ErrCorruptedHeightHintCache
168+
}
169+
170+
var outPointBytes bytes.Buffer
171+
err := channeldb.WriteElement(
172+
&outPointBytes, outPoint,
173+
)
174+
if err != nil {
175+
return err
176+
}
177+
178+
spendKey := outPointBytes.Bytes()
179+
if err := spendHints.Delete(spendKey); err != nil {
180+
log.Debugf("outpoint not found in the height "+
181+
"hint cache: "+
182+
"%v", outPoint.String())
183+
184+
return err
185+
}
186+
log.Infof("deleted height hint for outpoint: "+
187+
"%v \n", outPoint.String())
188+
189+
return nil
190+
}, func() {})
191+
}
192+
193+
func parseChanPoint(s string) (*wire.OutPoint, error) {
194+
split := strings.Split(s, ":")
195+
if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
196+
return nil, fmt.Errorf("invalid channel point")
197+
}
198+
199+
index, err := strconv.ParseInt(split[1], 10, 64)
200+
if err != nil {
201+
return nil, fmt.Errorf("unable to decode output index: %w", err)
202+
}
203+
204+
txid, err := chainhash.NewHashFromStr(split[0])
205+
if err != nil {
206+
return nil, fmt.Errorf("unable to parse hex string: %w", err)
207+
}
208+
209+
return &wire.OutPoint{Hash: *txid,
210+
Index: uint32(index)}, nil
211+
}

cmd/chantools/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ func main() {
104104
newDoubleSpendInputsCommand(),
105105
newDropChannelGraphCommand(),
106106
newDropGraphZombiesCommand(),
107+
newDropHeightHintCacheCommand(),
107108
newDumpBackupCommand(),
108109
newDumpChannelsCommand(),
109110
newDocCommand(),

0 commit comments

Comments
 (0)