Skip to content

Commit 4b79ab4

Browse files
committed
new command: scbforceclose
1 parent 64fd6e4 commit 4b79ab4

File tree

2 files changed

+354
-0
lines changed

2 files changed

+354
-0
lines changed

cmd/chantools/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ func main() {
113113
newFilterBackupCommand(),
114114
newFixOldBackupCommand(),
115115
newForceCloseCommand(),
116+
newScbForceCloseCommand(),
116117
newGenImportScriptCommand(),
117118
newMigrateDBCommand(),
118119
newPullAnchorCommand(),

cmd/chantools/scbforceclose.go

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/hex"
6+
"errors"
7+
"fmt"
8+
"os"
9+
"strings"
10+
11+
"github.com/btcsuite/btcd/btcutil/hdkeychain"
12+
"github.com/btcsuite/btcd/txscript"
13+
"github.com/btcsuite/btcd/wire"
14+
"github.com/lightninglabs/chantools/btc"
15+
"github.com/lightninglabs/chantools/lnd"
16+
"github.com/lightningnetwork/lnd/chanbackup"
17+
"github.com/lightningnetwork/lnd/channeldb"
18+
"github.com/lightningnetwork/lnd/input"
19+
"github.com/lightningnetwork/lnd/lnwallet"
20+
"github.com/lightningnetwork/lnd/shachain"
21+
"github.com/spf13/cobra"
22+
)
23+
24+
type scbForceCloseCommand struct {
25+
APIURL string
26+
Publish bool
27+
28+
// channel.backup.
29+
SingleBackup string
30+
SingleFile string
31+
MultiBackup string
32+
MultiFile string
33+
34+
rootKey *rootKey
35+
cmd *cobra.Command
36+
}
37+
38+
func newScbForceCloseCommand() *cobra.Command {
39+
cc := &scbForceCloseCommand{}
40+
cc.cmd = &cobra.Command{
41+
Use: "scbforceclose",
42+
Short: "Force-close the last state that is in the SCB " +
43+
"provided",
44+
Long: forceCloseWarning,
45+
Example: `chantools scbforceclose --multi_file channel.backup`,
46+
RunE: cc.Execute,
47+
}
48+
cc.cmd.Flags().StringVar(
49+
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
50+
"be esplora compatible)",
51+
)
52+
53+
cc.cmd.Flags().StringVar(
54+
&cc.SingleBackup, "single_backup", "", "a hex encoded single "+
55+
"channel backup obtained from exportchanbackup for "+
56+
"force-closing channels",
57+
)
58+
cc.cmd.Flags().StringVar(
59+
&cc.MultiBackup, "multi_backup", "", "a hex encoded "+
60+
"multi-channel backup obtained from exportchanbackup "+
61+
"for force-closing channels",
62+
)
63+
cc.cmd.Flags().StringVar(
64+
&cc.SingleFile, "single_file", "", "the path to a "+
65+
"single-channel backup file",
66+
)
67+
cc.cmd.Flags().StringVar(
68+
&cc.MultiFile, "multi_file", "", "the path to a "+
69+
"single-channel backup file (channel.backup)",
70+
)
71+
72+
cc.cmd.Flags().BoolVar(
73+
&cc.Publish, "publish", false, "publish force-closing TX to "+
74+
"the chain API instead of just printing the TX",
75+
)
76+
77+
cc.rootKey = newRootKey(cc.cmd, "decrypting the backup and signing tx")
78+
79+
return cc.cmd
80+
}
81+
82+
func (c *scbForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
83+
extendedKey, err := c.rootKey.read()
84+
if err != nil {
85+
return fmt.Errorf("error reading root key: %w", err)
86+
}
87+
88+
api := &btc.ExplorerAPI{BaseURL: c.APIURL}
89+
keyRing := &lnd.HDKeyRing{
90+
ExtendedKey: extendedKey,
91+
ChainParams: chainParams,
92+
}
93+
var backups []chanbackup.Single
94+
if c.SingleBackup != "" || c.SingleFile != "" {
95+
if c.SingleBackup != "" && c.SingleFile != "" {
96+
return errors.New("must not pass --single_backup and " +
97+
"--single_file together")
98+
}
99+
var singleBackupBytes []byte
100+
if c.SingleBackup != "" {
101+
singleBackupBytes, err = hex.DecodeString(
102+
c.SingleBackup,
103+
)
104+
} else if c.SingleFile != "" {
105+
singleBackupBytes, err = os.ReadFile(c.SingleFile)
106+
}
107+
if err != nil {
108+
return fmt.Errorf("failed to get single backup: %w",
109+
err)
110+
}
111+
var s chanbackup.Single
112+
r := bytes.NewReader(singleBackupBytes)
113+
if err := s.UnpackFromReader(r, keyRing); err != nil {
114+
return fmt.Errorf("failed to unpack single backup: %w",
115+
err)
116+
}
117+
backups = append(backups, s)
118+
}
119+
if c.MultiBackup != "" || c.MultiFile != "" {
120+
if len(backups) != 0 {
121+
return errors.New("must not pass single and multi " +
122+
"backups together")
123+
}
124+
if c.MultiBackup != "" && c.MultiFile != "" {
125+
return errors.New("must not pass --multi_backup and " +
126+
"--multi_file together")
127+
}
128+
var multiBackupBytes []byte
129+
if c.MultiBackup != "" {
130+
multiBackupBytes, err = hex.DecodeString(c.MultiBackup)
131+
} else if c.MultiFile != "" {
132+
multiBackupBytes, err = os.ReadFile(c.MultiFile)
133+
}
134+
if err != nil {
135+
return fmt.Errorf("failed to get multi backup: %w", err)
136+
}
137+
var m chanbackup.Multi
138+
r := bytes.NewReader(multiBackupBytes)
139+
if err := m.UnpackFromReader(r, keyRing); err != nil {
140+
return fmt.Errorf("failed to unpack multi backup: %w",
141+
err)
142+
}
143+
backups = append(backups, m.StaticBackups...)
144+
}
145+
146+
backupsWithInputs := make([]chanbackup.Single, 0, len(backups))
147+
for _, s := range backups {
148+
if s.CloseTxInputs != nil {
149+
backupsWithInputs = append(backupsWithInputs, s)
150+
}
151+
}
152+
153+
fmt.Println()
154+
fmt.Printf("Found %d channel backups, %d of them have close tx.\n",
155+
len(backups), len(backupsWithInputs))
156+
157+
if len(backupsWithInputs) == 0 {
158+
fmt.Println("No channel backups that can be used for force " +
159+
"close.")
160+
return nil
161+
}
162+
163+
fmt.Println()
164+
fmt.Println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
165+
fmt.Println(strings.TrimSpace(forceCloseWarning))
166+
fmt.Println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
167+
fmt.Println()
168+
169+
fmt.Printf("Type YES to proceed: ")
170+
var userInput string
171+
if _, err := fmt.Scan(&userInput); err != nil {
172+
return errors.New("failed to read user input")
173+
}
174+
if strings.TrimSpace(userInput) != "YES" {
175+
return errors.New("cancelled by user")
176+
}
177+
178+
if c.Publish {
179+
fmt.Println("Signed transactions will be broadcasted " +
180+
"automatically.")
181+
fmt.Printf("Type YES again to proceed: ")
182+
if _, err := fmt.Scan(&userInput); err != nil {
183+
return errors.New("failed to read user input")
184+
}
185+
if strings.TrimSpace(userInput) != "YES" {
186+
return errors.New("cancelled by user")
187+
}
188+
}
189+
190+
for _, s := range backupsWithInputs {
191+
signedTx, err := signCloseTx(s, extendedKey)
192+
if err != nil {
193+
return fmt.Errorf("signCloseTx failed for %s: %w",
194+
s.FundingOutpoint, err)
195+
}
196+
var buf bytes.Buffer
197+
if err := signedTx.Serialize(&buf); err != nil {
198+
return fmt.Errorf("failed to serialize signed %s: %w",
199+
s.FundingOutpoint, err)
200+
}
201+
txHex := hex.EncodeToString(buf.Bytes())
202+
fmt.Println(s.FundingOutpoint)
203+
fmt.Println(txHex)
204+
fmt.Println()
205+
206+
// Publish TX.
207+
if c.Publish {
208+
response, err := api.PublishTx(txHex)
209+
if err != nil {
210+
return err
211+
}
212+
log.Infof("Published TX %s, response: %s",
213+
signedTx.TxHash(), response)
214+
}
215+
}
216+
217+
return nil
218+
}
219+
220+
func signCloseTx(s chanbackup.Single, extendedKey *hdkeychain.ExtendedKey) (
221+
*wire.MsgTx, error) {
222+
223+
if s.CloseTxInputs == nil {
224+
return nil, errors.New("channel backup does not have data " +
225+
"needed to sign force sloe tx")
226+
}
227+
228+
// Each of the keys in our local channel config only have their
229+
// locators populate, so we'll re-derive the raw key now.
230+
keyRing := &lnd.HDKeyRing{
231+
ExtendedKey: extendedKey,
232+
ChainParams: chainParams,
233+
}
234+
var err error
235+
s.LocalChanCfg.MultiSigKey, err = keyRing.DeriveKey(
236+
s.LocalChanCfg.MultiSigKey.KeyLocator,
237+
)
238+
if err != nil {
239+
return nil, fmt.Errorf("unable to derive multi sig key: %w",
240+
err)
241+
}
242+
243+
signDesc, err := createSignDesc(s)
244+
if err != nil {
245+
return nil, fmt.Errorf("failed to create signDesc: %w", err)
246+
}
247+
248+
inputs := lnwallet.SignedCommitTxInputs{
249+
CommitTx: s.CloseTxInputs.CommitTx,
250+
CommitSig: s.CloseTxInputs.CommitSig,
251+
OurKey: s.LocalChanCfg.MultiSigKey,
252+
TheirKey: s.RemoteChanCfg.MultiSigKey,
253+
SignDesc: signDesc,
254+
}
255+
256+
if s.Version == chanbackup.SimpleTaprootVersion {
257+
p, err := createTaprootNonceProducer(s, extendedKey)
258+
if err != nil {
259+
return nil, err
260+
}
261+
inputs.Taproot = &lnwallet.TaprootSignedCommitTxInputs{
262+
CommitHeight: s.CloseTxInputs.CommitHeight,
263+
TaprootNonceProducer: p,
264+
}
265+
}
266+
267+
signer := &lnd.Signer{
268+
ExtendedKey: extendedKey,
269+
ChainParams: chainParams,
270+
}
271+
signer.MusigSessionManager = input.NewMusigSessionManager(
272+
signer.FetchPrivateKey,
273+
)
274+
275+
return lnwallet.GetSignedCommitTx(inputs, signer)
276+
}
277+
278+
func createSignDesc(s chanbackup.Single) (*input.SignDescriptor, error) {
279+
// See LightningChannel.createSignDesc on how signDesc is produced.
280+
281+
var fundingPkScript, multiSigScript []byte
282+
283+
localKey := s.LocalChanCfg.MultiSigKey.PubKey
284+
remoteKey := s.RemoteChanCfg.MultiSigKey.PubKey
285+
286+
var err error
287+
if s.Version == chanbackup.SimpleTaprootVersion {
288+
fundingPkScript, _, err = input.GenTaprootFundingScript(
289+
localKey, remoteKey, int64(s.Capacity),
290+
)
291+
if err != nil {
292+
return nil, err
293+
}
294+
} else {
295+
multiSigScript, err = input.GenMultiSigScript(
296+
localKey.SerializeCompressed(),
297+
remoteKey.SerializeCompressed(),
298+
)
299+
if err != nil {
300+
return nil, err
301+
}
302+
303+
fundingPkScript, err = input.WitnessScriptHash(multiSigScript)
304+
if err != nil {
305+
return nil, err
306+
}
307+
}
308+
309+
return &input.SignDescriptor{
310+
KeyDesc: s.LocalChanCfg.MultiSigKey,
311+
WitnessScript: multiSigScript,
312+
Output: &wire.TxOut{
313+
PkScript: fundingPkScript,
314+
Value: int64(s.Capacity),
315+
},
316+
HashType: txscript.SigHashAll,
317+
PrevOutputFetcher: txscript.NewCannedPrevOutputFetcher(
318+
fundingPkScript, int64(s.Capacity),
319+
),
320+
InputIndex: 0,
321+
}, nil
322+
}
323+
324+
func createTaprootNonceProducer(
325+
s chanbackup.Single,
326+
extendedKey *hdkeychain.ExtendedKey,
327+
) (shachain.Producer, error) {
328+
329+
revPathStr := fmt.Sprintf("m/1017'/%d'/%d'/0/%d",
330+
chainParams.HDCoinType,
331+
s.ShaChainRootDesc.KeyLocator.Family,
332+
s.ShaChainRootDesc.KeyLocator.Index,
333+
)
334+
revPath, err := lnd.ParsePath(revPathStr)
335+
if err != nil {
336+
return nil, err
337+
}
338+
339+
if s.ShaChainRootDesc.PubKey != nil {
340+
return nil, errors.New("taproot channels always use ECDH, " +
341+
"but legacy ShaChainRootDesc with pubkey found")
342+
}
343+
revocationProducer, err := lnd.ShaChainFromPath(
344+
extendedKey, revPath, s.LocalChanCfg.MultiSigKey.PubKey,
345+
)
346+
if err != nil {
347+
return nil, fmt.Errorf("lnd.ShaChainFromPath(extendedKey, %v, "+
348+
"%v) failed: %w", revPath, s.ShaChainRootDesc.PubKey,
349+
err)
350+
}
351+
352+
return channeldb.DeriveMusig2Shachain(revocationProducer)
353+
}

0 commit comments

Comments
 (0)