Skip to content

Commit dcc9b3b

Browse files
committed
new command: scbforceclose
1 parent f50ebcf commit dcc9b3b

File tree

2 files changed

+336
-0
lines changed

2 files changed

+336
-0
lines changed

cmd/chantools/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func main() {
111111
newFilterBackupCommand(),
112112
newFixOldBackupCommand(),
113113
newForceCloseCommand(),
114+
newScbForceCloseCommand(),
114115
newGenImportScriptCommand(),
115116
newMigrateDBCommand(),
116117
newPullAnchorCommand(),

cmd/chantools/scbforceclose.go

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

0 commit comments

Comments
 (0)