77 "encoding/json"
88 "errors"
99 "fmt"
10+ "os"
11+ "regexp"
12+ "slices"
1013 "strings"
1114
1215 "github.com/btcsuite/btcd/btcec/v2"
@@ -19,8 +22,11 @@ import (
1922 "github.com/lightninglabs/chantools/btc"
2023 "github.com/lightninglabs/chantools/cln"
2124 "github.com/lightninglabs/chantools/lnd"
25+ "github.com/lightningnetwork/lnd/fn/v2"
2226 "github.com/lightningnetwork/lnd/input"
2327 "github.com/lightningnetwork/lnd/keychain"
28+ "github.com/lightningnetwork/lnd/lncfg"
29+ "github.com/lightningnetwork/lnd/lnrpc"
2430 "github.com/lightningnetwork/lnd/lnwallet/chainfee"
2531 "github.com/spf13/cobra"
2632)
@@ -40,8 +46,9 @@ type sweepRemoteClosedCommand struct {
4046 SweepAddr string
4147 FeeRate uint32
4248
43- HsmSecret string
44- PeerPubKeys string
49+ HsmSecret string
50+ PeerPubKeys string
51+ KnownOutputs string
4552
4653 rootKey * rootKey
4754 cmd * cobra.Command
@@ -107,7 +114,16 @@ Supported remote force-closed channel types are:
107114 & cc .PeerPubKeys , "peers" , "" , "comma separated list of " +
108115 "hex encoded public keys of the remote peers " +
109116 "to recover funds from, only required when using " +
110- "--hsm_secret to derive the keys" ,
117+ "--hsm_secret to derive the keys; can also be a file " +
118+ "name to a file that contains the public keys, one " +
119+ "per line" ,
120+ )
121+ cc .cmd .Flags ().StringVar (
122+ & cc .KnownOutputs , "known_outputs" , "" , "a comma separated " +
123+ "list of known output addresses to use for matching " +
124+ "against, instead of querying the API; can also be " +
125+ "a file name to a file that contains the known " +
126+ "outputs, one per line" ,
111127 )
112128
113129 cc .rootKey = newRootKey (cc .cmd , "sweeping the wallet" )
@@ -134,11 +150,32 @@ func (c *sweepRemoteClosedCommand) Execute(_ *cobra.Command, _ []string) error {
134150 }
135151
136152 var (
137- signer lnd.ChannelSigner
138- estimator input.TxWeightEstimator
139- sweepScript []byte
140- targets []* targetAddr
153+ signer lnd.ChannelSigner
154+ estimator input.TxWeightEstimator
155+ knownOutputs []string
156+ sweepScript []byte
157+ targets []* targetAddr
141158 )
159+
160+ if c .KnownOutputs != "" {
161+ knownOutputs , err = listOrFile (c .KnownOutputs )
162+ if err != nil {
163+ return fmt .Errorf ("error reading known outputs: %w" ,
164+ err )
165+ }
166+
167+ for _ , output := range knownOutputs {
168+ _ , err = lnd .ParseAddress (output , chainParams )
169+ if err != nil {
170+ return fmt .Errorf ("error parsing known output " +
171+ "address %s: %w" , output , err )
172+ }
173+ }
174+
175+ log .Infof ("Using %d known outputs for matching." ,
176+ len (knownOutputs ))
177+ }
178+
142179 switch {
143180 case c .HsmSecret != "" :
144181 secretBytes , err := hex .DecodeString (c .HsmSecret )
@@ -152,11 +189,16 @@ func (c *sweepRemoteClosedCommand) Execute(_ *cobra.Command, _ []string) error {
152189 if c .PeerPubKeys == "" {
153190 return errors .New ("invalid peer public keys, must be " +
154191 "a comma separated list of hex encoded " +
155- "public keys" )
192+ "public keys or a file name " )
156193 }
157194
158195 var pubKeys []* btcec.PublicKey
159- for _ , pubKeyHex := range strings .Split (c .PeerPubKeys , "," ) {
196+ hexPubKeys , err := listOrFile (c .PeerPubKeys )
197+ if err != nil {
198+ return fmt .Errorf ("error reading peer public keys: %w" ,
199+ err )
200+ }
201+ for _ , pubKeyHex := range hexPubKeys {
160202 pkHex , err := hex .DecodeString (pubKeyHex )
161203 if err != nil {
162204 return fmt .Errorf ("error decoding peer " +
@@ -172,12 +214,16 @@ func (c *sweepRemoteClosedCommand) Execute(_ *cobra.Command, _ []string) error {
172214 pubKeys = append (pubKeys , pk )
173215 }
174216
217+ log .Infof ("Using %d peer public keys for recovery." ,
218+ len (pubKeys ))
219+
175220 signer = & cln.Signer {
176221 HsmSecret : hsmSecret ,
177222 }
178223
179224 targets , err = findTargetsCln (
180225 hsmSecret , pubKeys , c .APIURL , c .RecoveryWindow ,
226+ knownOutputs ,
181227 )
182228 if err != nil {
183229 return fmt .Errorf ("error finding targets: %w" , err )
@@ -202,7 +248,7 @@ func (c *sweepRemoteClosedCommand) Execute(_ *cobra.Command, _ []string) error {
202248 }
203249
204250 targets , err = findTargetsLnd (
205- extendedKey , c .APIURL , c .RecoveryWindow ,
251+ extendedKey , c .APIURL , c .RecoveryWindow , knownOutputs ,
206252 )
207253 if err != nil {
208254 return fmt .Errorf ("error finding targets: %w" , err )
@@ -233,7 +279,7 @@ type targetAddr struct {
233279}
234280
235281func findTargetsLnd (extendedKey * hdkeychain.ExtendedKey , apiURL string ,
236- recoveryWindow uint32 ) ([]* targetAddr , error ) {
282+ recoveryWindow uint32 , knownOutputs [] string ) ([]* targetAddr , error ) {
237283
238284 var (
239285 targets []* targetAddr
@@ -267,7 +313,7 @@ func findTargetsLnd(extendedKey *hdkeychain.ExtendedKey, apiURL string,
267313 Family : keychain .KeyFamilyPaymentBase ,
268314 Index : index ,
269315 },
270- }, api ,
316+ }, api , knownOutputs ,
271317 )
272318 if err != nil {
273319 return nil , fmt .Errorf ("could not query API for " +
@@ -294,7 +340,8 @@ func findTargetsLnd(extendedKey *hdkeychain.ExtendedKey, apiURL string,
294340}
295341
296342func findTargetsCln (hsmSecret [32 ]byte , pubKeys []* btcec.PublicKey ,
297- apiURL string , recoveryWindow uint32 ) ([]* targetAddr , error ) {
343+ apiURL string , recoveryWindow uint32 ,
344+ knownOutputs []string ) ([]* targetAddr , error ) {
298345
299346 var (
300347 targets []* targetAddr
@@ -316,7 +363,7 @@ func findTargetsCln(hsmSecret [32]byte, pubKeys []*btcec.PublicKey,
316363 }
317364
318365 foundTargets , err := queryAddressBalances (
319- privKey .PubKey (), desc , api ,
366+ privKey .PubKey (), desc , api , knownOutputs ,
320367 )
321368 if err != nil {
322369 return nil , fmt .Errorf ("could not query API " +
@@ -540,13 +587,19 @@ func sweepRemoteClosed(signer lnd.ChannelSigner,
540587}
541588
542589func queryAddressBalances (pubKey * btcec.PublicKey ,
543- keyDesc * keychain.KeyDescriptor , api * btc.ExplorerAPI ) ([] * targetAddr ,
544- error ) {
590+ keyDesc * keychain.KeyDescriptor , api * btc.ExplorerAPI ,
591+ knownOutputs [] string ) ([] * targetAddr , error ) {
545592
546593 var targets []* targetAddr
547594 queryAddr := func (address btcutil.Address , script []byte ,
548595 scriptTree * input.CommitScriptTree ) error {
549596
597+ if len (knownOutputs ) > 0 {
598+ if ! slices .Contains (knownOutputs , address .String ()) {
599+ return nil
600+ }
601+ }
602+
550603 unspent , err := api .Unspent (address .EncodeAddress ())
551604 if err != nil {
552605 return fmt .Errorf ("could not query unspent: %w" , err )
@@ -716,3 +769,21 @@ func checkAncientChannelPoints(api *btc.ExplorerAPI, numKeys uint32,
716769
717770 return targets , nil
718771}
772+
773+ func listOrFile (listOrPath string ) ([]string , error ) {
774+ if lnrpc .FileExists (lncfg .CleanAndExpandPath (listOrPath )) {
775+ contents , err := os .ReadFile (listOrPath )
776+ if err != nil {
777+ return nil , fmt .Errorf ("error reading file %s: %w" ,
778+ listOrPath , err )
779+ }
780+
781+ re := regexp .MustCompile (`[,\s]+` )
782+ parts := re .Split (string (contents ), - 1 )
783+ return fn .Filter (parts , func (s string ) bool {
784+ return len (strings .TrimSpace (s )) > 0
785+ }), nil
786+ }
787+
788+ return strings .Split (listOrPath , "," ), nil
789+ }
0 commit comments