Skip to content

Commit bdf316e

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 bdf316e

File tree

3 files changed

+216
-0
lines changed

3 files changed

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

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)