@@ -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
1416type 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
4458func (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