@@ -6,14 +6,19 @@ import (
6
6
"os"
7
7
"time"
8
8
9
+ "github.com/btcsuite/btcd/btcutil"
9
10
"github.com/lightninglabs/chantools/btc"
10
11
"github.com/lightninglabs/chantools/dataformat"
12
+ "github.com/lightninglabs/chantools/lnd"
11
13
"github.com/spf13/cobra"
12
14
)
13
15
14
16
type summaryCommand struct {
15
17
APIURL string
16
18
19
+ Ancient bool
20
+ AncientStats string
21
+
17
22
inputs * inputFlags
18
23
cmd * cobra.Command
19
24
}
@@ -35,18 +40,36 @@ chantools summary --fromchanneldb ~/.lnd/data/graph/mainnet/channel.db`,
35
40
& cc .APIURL , "apiurl" , defaultAPIURL , "API URL to use (must " +
36
41
"be esplora compatible)" ,
37
42
)
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
+ )
38
52
39
53
cc .inputs = newInputFlags (cc .cmd )
40
54
41
55
return cc .cmd
42
56
}
43
57
44
58
func (c * summaryCommand ) Execute (_ * cobra.Command , _ []string ) error {
59
+ if c .AncientStats != "" {
60
+ return summarizeAncientChannelOutputs (c .APIURL , c .AncientStats )
61
+ }
62
+
45
63
// Parse channel entries from any of the possible input files.
46
64
entries , err := c .inputs .parseInputType ()
47
65
if err != nil {
48
66
return err
49
67
}
68
+
69
+ if c .Ancient {
70
+ return summarizeAncientChannels (c .APIURL , entries )
71
+ }
72
+
50
73
return summarizeChannels (c .APIURL , entries )
51
74
}
52
75
@@ -90,3 +113,130 @@ func summarizeChannels(apiURL string,
90
113
log .Infof ("Writing result to %s" , fileName )
91
114
return os .WriteFile (fileName , summaryBytes , 0644 )
92
115
}
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