Skip to content

Commit 5810cce

Browse files
committed
sweepremoteclosed: make command CLN compatible
1 parent b141033 commit 5810cce

File tree

5 files changed

+210
-33
lines changed

5 files changed

+210
-33
lines changed

cln/signer.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,17 @@ import (
1515
)
1616

1717
type Signer struct {
18+
*input.MusigSessionManager
19+
1820
HsmSecret [32]byte
21+
22+
// SwapDescKeyAfterDerive is a boolean that indicates that after
23+
// deriving the private key from the key descriptor (which interprets
24+
// the public key as the peer's public key), we should swap the public
25+
// key in the key descriptor to the actual derived public key. This is
26+
// required for P2WKH signatures that need to have the public key in the
27+
// witness stack.
28+
SwapDescKeyAfterDerive bool
1929
}
2030

2131
func (s *Signer) SignOutputRaw(tx *wire.MsgTx,
@@ -28,9 +38,22 @@ func (s *Signer) SignOutputRaw(tx *wire.MsgTx,
2838
return nil, err
2939
}
3040

41+
if s.SwapDescKeyAfterDerive {
42+
// If we need to swap the public key in the descriptor, we do so
43+
// now. This is required for P2WKH signatures that need to have
44+
// the public key in the witness stack.
45+
signDesc.KeyDesc.PubKey = privKey.PubKey()
46+
}
47+
3148
return lnd.SignOutputRawWithPrivateKey(tx, signDesc, privKey)
3249
}
3350

51+
func (s *Signer) ComputeInputScript(_ *wire.MsgTx, _ *input.SignDescriptor) (
52+
*input.Script, error) {
53+
54+
return nil, errors.New("unimplemented")
55+
}
56+
3457
func (s *Signer) FetchPrivateKey(
3558
descriptor *keychain.KeyDescriptor) (*btcec.PrivateKey, error) {
3659

cmd/chantools/sweepremoteclosed.go

Lines changed: 169 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/json"
88
"errors"
99
"fmt"
10+
"strings"
1011

1112
"github.com/btcsuite/btcd/btcec/v2"
1213
"github.com/btcsuite/btcd/btcutil"
@@ -16,6 +17,7 @@ import (
1617
"github.com/btcsuite/btcd/txscript"
1718
"github.com/btcsuite/btcd/wire"
1819
"github.com/lightninglabs/chantools/btc"
20+
"github.com/lightninglabs/chantools/cln"
1921
"github.com/lightninglabs/chantools/lnd"
2022
"github.com/lightningnetwork/lnd/input"
2123
"github.com/lightningnetwork/lnd/keychain"
@@ -38,6 +40,9 @@ type sweepRemoteClosedCommand struct {
3840
SweepAddr string
3941
FeeRate uint32
4042

43+
HsmSecret string
44+
PeerPubKeys string
45+
4146
rootKey *rootKey
4247
cmd *cobra.Command
4348
}
@@ -92,19 +97,27 @@ Supported remote force-closed channel types are:
9297
"use for the sweep transaction in sat/vByte",
9398
)
9499

100+
cc.cmd.Flags().StringVar(
101+
&cc.HsmSecret, "hsm_secret", "", "the hex encoded HSM secret "+
102+
"to use for deriving the multisig keys for a CLN "+
103+
"node; obtain by running 'xxd -p -c32 "+
104+
"~/.lightning/bitcoin/hsm_secret'",
105+
)
106+
cc.cmd.Flags().StringVar(
107+
&cc.PeerPubKeys, "peers", "", "comma separated list of "+
108+
"hex encoded public keys of the remote peers "+
109+
"to recover funds from, only required when using "+
110+
"--hsm_secret to derive the keys",
111+
)
112+
95113
cc.rootKey = newRootKey(cc.cmd, "sweeping the wallet")
96114

97115
return cc.cmd
98116
}
99117

100118
func (c *sweepRemoteClosedCommand) Execute(_ *cobra.Command, _ []string) error {
101-
extendedKey, err := c.rootKey.read()
102-
if err != nil {
103-
return fmt.Errorf("error reading root key: %w", err)
104-
}
105-
106119
// Make sure sweep addr is set.
107-
err = lnd.CheckAddress(
120+
err := lnd.CheckAddress(
108121
c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
109122
lnd.AddrTypeP2TR,
110123
)
@@ -120,9 +133,93 @@ func (c *sweepRemoteClosedCommand) Execute(_ *cobra.Command, _ []string) error {
120133
c.FeeRate = defaultFeeSatPerVByte
121134
}
122135

136+
var (
137+
signer lnd.ChannelSigner
138+
estimator input.TxWeightEstimator
139+
sweepScript []byte
140+
targets []*targetAddr
141+
)
142+
switch {
143+
case c.HsmSecret != "":
144+
secretBytes, err := hex.DecodeString(c.HsmSecret)
145+
if err != nil {
146+
return fmt.Errorf("error decoding HSM secret: %w", err)
147+
}
148+
149+
var hsmSecret [32]byte
150+
copy(hsmSecret[:], secretBytes)
151+
152+
if c.PeerPubKeys == "" {
153+
return errors.New("invalid peer public keys, must be " +
154+
"a comma separated list of hex encoded " +
155+
"public keys")
156+
}
157+
158+
var pubKeys []*btcec.PublicKey
159+
for _, pubKeyHex := range strings.Split(c.PeerPubKeys, ",") {
160+
pkHex, err := hex.DecodeString(pubKeyHex)
161+
if err != nil {
162+
return fmt.Errorf("error decoding peer "+
163+
"public key hex %s: %w", pubKeyHex, err)
164+
}
165+
166+
pk, err := btcec.ParsePubKey(pkHex)
167+
if err != nil {
168+
return fmt.Errorf("error parsing peer public "+
169+
"key hex %s: %w", pubKeyHex, err)
170+
}
171+
172+
pubKeys = append(pubKeys, pk)
173+
}
174+
175+
signer = &cln.Signer{
176+
HsmSecret: hsmSecret,
177+
}
178+
179+
targets, err = findTargetsCln(
180+
hsmSecret, pubKeys, c.APIURL, c.RecoveryWindow,
181+
)
182+
if err != nil {
183+
return fmt.Errorf("error finding targets: %w", err)
184+
}
185+
186+
sweepScript, err = lnd.CheckAndEstimateAddress(
187+
c.SweepAddr, chainParams, &estimator, "sweep",
188+
)
189+
if err != nil {
190+
return err
191+
}
192+
193+
default:
194+
extendedKey, err := c.rootKey.read()
195+
if err != nil {
196+
return fmt.Errorf("error reading root key: %w", err)
197+
}
198+
199+
signer = &lnd.Signer{
200+
ExtendedKey: extendedKey,
201+
ChainParams: chainParams,
202+
}
203+
204+
targets, err = findTargetsLnd(
205+
extendedKey, c.APIURL, c.RecoveryWindow,
206+
)
207+
if err != nil {
208+
return fmt.Errorf("error finding targets: %w", err)
209+
}
210+
211+
sweepScript, err = lnd.PrepareWalletAddress(
212+
c.SweepAddr, chainParams, &estimator, extendedKey,
213+
"sweep",
214+
)
215+
if err != nil {
216+
return err
217+
}
218+
}
219+
123220
return sweepRemoteClosed(
124-
extendedKey, c.APIURL, c.SweepAddr, c.RecoveryWindow, c.FeeRate,
125-
c.Publish,
221+
signer, &estimator, sweepScript, targets,
222+
newExplorerAPI(c.APIURL), c.FeeRate, c.Publish,
126223
)
127224
}
128225

@@ -135,17 +232,8 @@ type targetAddr struct {
135232
scriptTree *input.CommitScriptTree
136233
}
137234

138-
func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
139-
sweepAddr string, recoveryWindow uint32, feeRate uint32,
140-
publish bool) error {
141-
142-
var estimator input.TxWeightEstimator
143-
sweepScript, err := lnd.PrepareWalletAddress(
144-
sweepAddr, chainParams, &estimator, extendedKey, "sweep",
145-
)
146-
if err != nil {
147-
return err
148-
}
235+
func findTargetsLnd(extendedKey *hdkeychain.ExtendedKey, apiURL string,
236+
recoveryWindow uint32) ([]*targetAddr, error) {
149237

150238
var (
151239
targets []*targetAddr
@@ -157,17 +245,18 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
157245
index)
158246
parsedPath, err := lnd.ParsePath(path)
159247
if err != nil {
160-
return fmt.Errorf("error parsing path: %w", err)
248+
return nil, fmt.Errorf("error parsing path: %w", err)
161249
}
162250

163251
hdKey, err := lnd.DeriveChildren(extendedKey, parsedPath)
164252
if err != nil {
165-
return fmt.Errorf("eror deriving children: %w", err)
253+
return nil, fmt.Errorf("eror deriving children: %w",
254+
err)
166255
}
167256

168257
privKey, err := hdKey.ECPrivKey()
169258
if err != nil {
170-
return fmt.Errorf("could not derive private "+
259+
return nil, fmt.Errorf("could not derive private "+
171260
"key: %w", err)
172261
}
173262

@@ -181,7 +270,7 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
181270
}, api,
182271
)
183272
if err != nil {
184-
return fmt.Errorf("could not query API for "+
273+
return nil, fmt.Errorf("could not query API for "+
185274
"addresses with funds: %w", err)
186275
}
187276
targets = append(targets, foundTargets...)
@@ -193,14 +282,60 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
193282
api, recoveryWindow, extendedKey,
194283
)
195284
if err != nil && !errors.Is(err, errAddrNotFound) {
196-
return fmt.Errorf("could not check ancient channel points: %w",
197-
err)
285+
return nil, fmt.Errorf("could not check ancient channel "+
286+
"points: %w", err)
198287
}
199288

200289
if len(ancientChannelTargets) > 0 {
201290
targets = append(targets, ancientChannelTargets...)
202291
}
203292

293+
return targets, nil
294+
}
295+
296+
func findTargetsCln(hsmSecret [32]byte, pubKeys []*btcec.PublicKey,
297+
apiURL string, recoveryWindow uint32) ([]*targetAddr, error) {
298+
299+
var (
300+
targets []*targetAddr
301+
api = newExplorerAPI(apiURL)
302+
)
303+
for _, pubKey := range pubKeys {
304+
for index := range recoveryWindow {
305+
desc := &keychain.KeyDescriptor{
306+
PubKey: pubKey,
307+
KeyLocator: keychain.KeyLocator{
308+
Family: keychain.KeyFamilyPaymentBase,
309+
Index: index,
310+
},
311+
}
312+
_, privKey, err := cln.DeriveKeyPair(hsmSecret, desc)
313+
if err != nil {
314+
return nil, fmt.Errorf("could not derive "+
315+
"private key: %w", err)
316+
}
317+
318+
foundTargets, err := queryAddressBalances(
319+
privKey.PubKey(), desc, api,
320+
)
321+
if err != nil {
322+
return nil, fmt.Errorf("could not query API "+
323+
"for addresses with funds: %w", err)
324+
}
325+
targets = append(targets, foundTargets...)
326+
}
327+
}
328+
329+
log.Infof("Found %d addresses with funds to sweep.", len(targets))
330+
331+
return targets, nil
332+
}
333+
334+
func sweepRemoteClosed(signer lnd.ChannelSigner,
335+
estimator *input.TxWeightEstimator, sweepScript []byte,
336+
targets []*targetAddr, api *btc.ExplorerAPI, feeRate uint32,
337+
publish bool) error {
338+
204339
// Create estimator and transaction template.
205340
var (
206341
signDescs []*input.SignDescriptor
@@ -332,13 +467,7 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
332467
}}
333468

334469
// Sign the transaction now.
335-
var (
336-
signer = &lnd.Signer{
337-
ExtendedKey: extendedKey,
338-
ChainParams: chainParams,
339-
}
340-
sigHashes = txscript.NewTxSigHashes(sweepTx, prevOutFetcher)
341-
)
470+
var sigHashes = txscript.NewTxSigHashes(sweepTx, prevOutFetcher)
342471
for idx, desc := range signDescs {
343472
desc.SigHashes = sigHashes
344473
desc.InputIndex = idx
@@ -370,6 +499,13 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
370499
// P2WKH descriptor to be set to the pkScript of the
371500
// output...
372501
desc.WitnessScript = desc.Output.PkScript
502+
503+
// For CLN we need to activate a flag to make sure we
504+
// put the correct public key on the witness stack.
505+
if clnSigner, ok := signer.(*cln.Signer); ok {
506+
clnSigner.SwapDescKeyAfterDerive = true
507+
}
508+
373509
witness, err := input.CommitSpendNoDelay(
374510
signer, desc, sweepTx,
375511
len(desc.SingleTweak) == 0,
@@ -382,7 +518,7 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
382518
}
383519

384520
var buf bytes.Buffer
385-
err = sweepTx.Serialize(&buf)
521+
err := sweepTx.Serialize(&buf)
386522
if err != nil {
387523
return err
388524
}

doc/chantools_sweepremoteclosed.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ chantools sweepremoteclosed \
3737
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
3838
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
3939
-h, --help help for sweepremoteclosed
40+
--hsm_secret string the hex encoded HSM secret to use for deriving the multisig keys for a CLN node; obtain by running 'xxd -p -c32 ~/.lightning/bitcoin/hsm_secret'
41+
--peers string comma separated list of hex encoded public keys of the remote peers to recover funds from, only required when using --hsm_secret to derive the keys
4042
--publish publish sweep TX to the chain API instead of just printing the TX
4143
--recoverywindow uint32 number of keys to scan per derivation path (default 200)
4244
--rootkey string BIP32 HD root key of the wallet to use for sweeping the wallet; leave empty to prompt for lnd 24 word aezeed

lnd/hdkeychain.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,9 +477,19 @@ func PrepareWalletAddress(addr string, chainParams *chaincfg.Params,
477477
return nil, err
478478
}
479479

480+
if estimator != nil {
481+
estimator.AddP2WKHOutput()
482+
}
483+
480484
return txscript.PayToAddrScript(p2wkhAddr)
481485
}
482486

487+
return CheckAndEstimateAddress(addr, chainParams, estimator, hint)
488+
}
489+
490+
func CheckAndEstimateAddress(addr string, chainParams *chaincfg.Params,
491+
estimator *input.TxWeightEstimator, hint string) ([]byte, error) {
492+
483493
parsedAddr, err := ParseAddress(addr, chainParams)
484494
if err != nil {
485495
return nil, fmt.Errorf("%s address is invalid: %w", hint, err)

0 commit comments

Comments
 (0)