Skip to content

Commit 5657a7a

Browse files
authored
Merge pull request #182 from lightninglabs/sweepremoteclosed-ancient
sweepremoteclosed: add commit points for ancient channels of LNBIG
2 parents 74b748e + 343cb8d commit 5657a7a

12 files changed

+12770
-26
lines changed

cmd/chantools/rescueclosed.go

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ var (
3232

3333
type cacheEntry struct {
3434
privKey *btcec.PrivateKey
35-
pubKey *btcec.PublicKey
35+
keyDesc *keychain.KeyDescriptor
3636
}
3737

3838
type rescueClosedCommand struct {
@@ -403,7 +403,7 @@ func addrInCache(numKeys uint32, addr string,
403403
for i := range numKeys {
404404
cacheEntry := cache[i]
405405
hashedPubKey := btcutil.Hash160(
406-
cacheEntry.pubKey.SerializeCompressed(),
406+
cacheEntry.keyDesc.PubKey.SerializeCompressed(),
407407
)
408408
equal := subtle.ConstantTimeCompare(
409409
targetPubKeyHash, hashedPubKey,
@@ -431,7 +431,7 @@ func addrInCache(numKeys uint32, addr string,
431431
// corresponds to the target pubKeyHash of the given address.
432432
for i := range numKeys {
433433
cacheEntry := cache[i]
434-
basePoint := cacheEntry.pubKey
434+
basePoint := cacheEntry.keyDesc.PubKey
435435
tweakedPubKey := input.TweakPubKey(basePoint, perCommitPoint)
436436
tweakBytes := input.SingleTweakBytes(perCommitPoint, basePoint)
437437
tweakedPrivKey := input.TweakPrivKey(
@@ -460,6 +460,29 @@ func addrInCache(numKeys uint32, addr string,
460460
return "", errAddrNotFound
461461
}
462462

463+
func keyInCache(numKeys uint32, targetPubKeyHash []byte,
464+
perCommitPoint *btcec.PublicKey) (*keychain.KeyDescriptor, []byte,
465+
error) {
466+
467+
for i := range numKeys {
468+
cacheEntry := cache[i]
469+
basePoint := cacheEntry.keyDesc.PubKey
470+
tweakedPubKey := input.TweakPubKey(basePoint, perCommitPoint)
471+
tweakBytes := input.SingleTweakBytes(perCommitPoint, basePoint)
472+
hashedPubKey := btcutil.Hash160(
473+
tweakedPubKey.SerializeCompressed(),
474+
)
475+
equal := subtle.ConstantTimeCompare(
476+
targetPubKeyHash, hashedPubKey,
477+
)
478+
if equal == 1 {
479+
return cacheEntry.keyDesc, tweakBytes, nil
480+
}
481+
}
482+
483+
return nil, nil, errAddrNotFound
484+
}
485+
463486
func fillCache(numKeys uint32, extendedKey *hdkeychain.ExtendedKey) error {
464487
cache = make([]*cacheEntry, numKeys)
465488

@@ -484,7 +507,13 @@ func fillCache(numKeys uint32, extendedKey *hdkeychain.ExtendedKey) error {
484507
}
485508
cache[i] = &cacheEntry{
486509
privKey: privKey,
487-
pubKey: pubKey,
510+
keyDesc: &keychain.KeyDescriptor{
511+
KeyLocator: keychain.KeyLocator{
512+
Family: keychain.KeyFamilyPaymentBase,
513+
Index: i,
514+
},
515+
PubKey: pubKey,
516+
},
488517
}
489518

490519
if i > 0 && i%10000 == 0 {

cmd/chantools/root.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const (
3131
// version is the current version of the tool. It is set during build.
3232
// NOTE: When changing this, please also update the version in the
3333
// download link shown in the README.
34-
version = "0.13.5"
34+
version = "0.13.6"
3535
na = "n/a"
3636

3737
// lndVersion is the current version of lnd that we support. This is
@@ -231,6 +231,7 @@ type inputFlags struct {
231231
PendingChannels string
232232
FromSummary string
233233
FromChannelDB string
234+
FromChannelDump string
234235
}
235236

236237
func newInputFlags(cmd *cobra.Command) *inputFlags {
@@ -250,6 +251,10 @@ func newInputFlags(cmd *cobra.Command) *inputFlags {
250251
cmd.Flags().StringVar(&f.FromChannelDB, "fromchanneldb", "", "channel "+
251252
"input is in the format of an lnd channel.db file",
252253
)
254+
cmd.Flags().StringVar(
255+
&f.FromChannelDump, "fromchanneldump", "", "channel "+
256+
"input is in the format of a channel dump file",
257+
)
253258

254259
return f
255260
}
@@ -283,6 +288,15 @@ func (f *inputFlags) parseInputType() ([]*dataformat.SummaryEntry, error) {
283288
target = &dataformat.ChannelDBFile{DB: db.ChannelStateDB()}
284289
return target.AsSummaryEntries()
285290

291+
case f.FromChannelDump != "":
292+
content, err = readInput(f.FromChannelDump)
293+
if err != nil {
294+
return nil, fmt.Errorf("error reading channel dump: %w",
295+
err)
296+
}
297+
298+
return dataformat.ExtractSummaryFromDump(string(content))
299+
286300
default:
287301
return nil, errors.New("an input file must be specified")
288302
}

cmd/chantools/summary.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@ import (
66
"os"
77
"time"
88

9+
"github.com/btcsuite/btcd/btcutil"
910
"github.com/lightninglabs/chantools/btc"
1011
"github.com/lightninglabs/chantools/dataformat"
12+
"github.com/lightninglabs/chantools/lnd"
1113
"github.com/spf13/cobra"
1214
)
1315

1416
type summaryCommand struct {
1517
APIURL string
1618

19+
Ancient bool
20+
AncientStats string
21+
1722
inputs *inputFlags
1823
cmd *cobra.Command
1924
}
@@ -35,18 +40,36 @@ chantools summary --fromchanneldb ~/.lnd/data/graph/mainnet/channel.db`,
3540
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
3641
"be esplora compatible)",
3742
)
43+
cc.cmd.Flags().BoolVar(
44+
&cc.Ancient, "ancient", false, "Create summary of ancient "+
45+
"channel closes with un-swept outputs",
46+
)
47+
cc.cmd.Flags().StringVar(
48+
&cc.AncientStats, "ancientstats", "", "Create summary of "+
49+
"ancient channel closes with un-swept outputs and "+
50+
"print stats for the given list of channels",
51+
)
3852

3953
cc.inputs = newInputFlags(cc.cmd)
4054

4155
return cc.cmd
4256
}
4357

4458
func (c *summaryCommand) Execute(_ *cobra.Command, _ []string) error {
59+
if c.AncientStats != "" {
60+
return summarizeAncientChannelOutputs(c.APIURL, c.AncientStats)
61+
}
62+
4563
// Parse channel entries from any of the possible input files.
4664
entries, err := c.inputs.parseInputType()
4765
if err != nil {
4866
return err
4967
}
68+
69+
if c.Ancient {
70+
return summarizeAncientChannels(c.APIURL, entries)
71+
}
72+
5073
return summarizeChannels(c.APIURL, entries)
5174
}
5275

@@ -90,3 +113,130 @@ func summarizeChannels(apiURL string,
90113
log.Infof("Writing result to %s", fileName)
91114
return os.WriteFile(fileName, summaryBytes, 0644)
92115
}
116+
117+
func summarizeAncientChannels(apiURL string,
118+
channels []*dataformat.SummaryEntry) error {
119+
120+
api := newExplorerAPI(apiURL)
121+
122+
var results []*ancientChannel
123+
for _, target := range channels {
124+
if target.ClosingTX == nil {
125+
continue
126+
}
127+
128+
closeTx := target.ClosingTX
129+
if !closeTx.ForceClose {
130+
continue
131+
}
132+
133+
if closeTx.AllOutsSpent {
134+
continue
135+
}
136+
137+
if closeTx.OurAddr != "" {
138+
log.Infof("Channel %s has potential funds: %d in %s",
139+
target.ChannelPoint, target.LocalBalance,
140+
closeTx.OurAddr)
141+
}
142+
143+
if target.LocalUnrevokedCommitPoint == "" {
144+
log.Warnf("Channel %s has no unrevoked commit point",
145+
target.ChannelPoint)
146+
continue
147+
}
148+
149+
if closeTx.ToRemoteAddr == "" {
150+
log.Warnf("Close TX %s has no remote address",
151+
closeTx.TXID)
152+
continue
153+
}
154+
155+
addr, err := lnd.ParseAddress(closeTx.ToRemoteAddr, chainParams)
156+
if err != nil {
157+
return fmt.Errorf("error parsing address %s of %s: %w",
158+
closeTx.ToRemoteAddr, closeTx.TXID, err)
159+
}
160+
161+
if _, ok := addr.(*btcutil.AddressWitnessPubKeyHash); !ok {
162+
log.Infof("Channel close %s has non-p2wkh output: %s",
163+
closeTx.TXID, closeTx.ToRemoteAddr)
164+
continue
165+
}
166+
167+
tx, err := api.Transaction(closeTx.TXID)
168+
if err != nil {
169+
return fmt.Errorf("error fetching transaction %s: %w",
170+
closeTx.TXID, err)
171+
}
172+
173+
for idx, txOut := range tx.Vout {
174+
if txOut.Outspend.Spent {
175+
continue
176+
}
177+
178+
if txOut.ScriptPubkeyAddr == closeTx.ToRemoteAddr {
179+
results = append(results, &ancientChannel{
180+
OP: fmt.Sprintf("%s:%d", closeTx.TXID,
181+
idx),
182+
Addr: closeTx.ToRemoteAddr,
183+
CP: target.LocalUnrevokedCommitPoint,
184+
})
185+
}
186+
}
187+
}
188+
189+
summaryBytes, err := json.MarshalIndent(results, "", " ")
190+
if err != nil {
191+
return err
192+
}
193+
fileName := fmt.Sprintf("results/summary-ancient-%s.json",
194+
time.Now().Format("2006-01-02-15-04-05"))
195+
log.Infof("Writing result to %s", fileName)
196+
return os.WriteFile(fileName, summaryBytes, 0644)
197+
}
198+
199+
func summarizeAncientChannelOutputs(apiURL, ancientFile string) error {
200+
jsonBytes, err := os.ReadFile(ancientFile)
201+
if err != nil {
202+
return fmt.Errorf("error reading file %s: %w", ancientFile, err)
203+
}
204+
205+
var ancients []ancientChannel
206+
err = json.Unmarshal(jsonBytes, &ancients)
207+
if err != nil {
208+
return fmt.Errorf("error unmarshalling ancient channels: %w",
209+
err)
210+
}
211+
212+
var (
213+
api = newExplorerAPI(apiURL)
214+
numUnspents uint32
215+
unspentSats uint64
216+
)
217+
for _, channel := range ancients {
218+
unspents, err := api.Unspent(channel.Addr)
219+
if err != nil {
220+
return fmt.Errorf("error fetching unspents for %s: %w",
221+
channel.Addr, err)
222+
}
223+
224+
if len(unspents) > 1 {
225+
log.Infof("Address %s has multiple unspents",
226+
channel.Addr)
227+
}
228+
for _, unspent := range unspents {
229+
if unspent.Outspend.Spent {
230+
continue
231+
}
232+
233+
numUnspents++
234+
unspentSats += unspent.Value
235+
}
236+
}
237+
238+
log.Infof("Found %d unspent outputs with %d sats", numUnspents,
239+
unspentSats)
240+
241+
return nil
242+
}

0 commit comments

Comments
 (0)