Skip to content

Commit 521d4c8

Browse files
committed
new command: scbforceclose
1 parent dbcbcae commit 521d4c8

File tree

2 files changed

+225
-0
lines changed

2 files changed

+225
-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: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/hex"
6+
"errors"
7+
"fmt"
8+
"os"
9+
"strings"
10+
11+
"github.com/lightninglabs/chantools/btc"
12+
"github.com/lightninglabs/chantools/lnd"
13+
"github.com/lightningnetwork/lnd/chanbackup"
14+
"github.com/lightningnetwork/lnd/input"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
type scbForceCloseCommand struct {
19+
APIURL string
20+
Publish bool
21+
22+
// channel.backup.
23+
SingleBackup string
24+
SingleFile string
25+
MultiBackup string
26+
MultiFile string
27+
28+
rootKey *rootKey
29+
cmd *cobra.Command
30+
}
31+
32+
func newScbForceCloseCommand() *cobra.Command {
33+
cc := &scbForceCloseCommand{}
34+
cc.cmd = &cobra.Command{
35+
Use: "scbforceclose",
36+
Short: "Force-close the last state that is in the SCB " +
37+
"provided",
38+
Long: forceCloseWarning,
39+
Example: `chantools scbforceclose --multi_file channel.backup`,
40+
RunE: cc.Execute,
41+
}
42+
cc.cmd.Flags().StringVar(
43+
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
44+
"be esplora compatible)",
45+
)
46+
47+
cc.cmd.Flags().StringVar(
48+
&cc.SingleBackup, "single_backup", "", "a hex encoded single "+
49+
"channel backup obtained from exportchanbackup for "+
50+
"force-closing channels",
51+
)
52+
cc.cmd.Flags().StringVar(
53+
&cc.MultiBackup, "multi_backup", "", "a hex encoded "+
54+
"multi-channel backup obtained from exportchanbackup "+
55+
"for force-closing channels",
56+
)
57+
cc.cmd.Flags().StringVar(
58+
&cc.SingleFile, "single_file", "", "the path to a "+
59+
"single-channel backup file",
60+
)
61+
cc.cmd.Flags().StringVar(
62+
&cc.MultiFile, "multi_file", "", "the path to a "+
63+
"single-channel backup file (channel.backup)",
64+
)
65+
66+
cc.cmd.Flags().BoolVar(
67+
&cc.Publish, "publish", false, "publish force-closing TX to "+
68+
"the chain API instead of just printing the TX",
69+
)
70+
71+
cc.rootKey = newRootKey(cc.cmd, "decrypting the backup and signing tx")
72+
73+
return cc.cmd
74+
}
75+
76+
func (c *scbForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
77+
extendedKey, err := c.rootKey.read()
78+
if err != nil {
79+
return fmt.Errorf("error reading root key: %w", err)
80+
}
81+
82+
api := &btc.ExplorerAPI{BaseURL: c.APIURL}
83+
84+
keyRing := &lnd.HDKeyRing{
85+
ExtendedKey: extendedKey,
86+
ChainParams: chainParams,
87+
}
88+
89+
signer := &lnd.Signer{
90+
ExtendedKey: extendedKey,
91+
ChainParams: chainParams,
92+
}
93+
signer.MusigSessionManager = input.NewMusigSessionManager(
94+
signer.FetchPrivateKey,
95+
)
96+
97+
var backups []chanbackup.Single
98+
if c.SingleBackup != "" || c.SingleFile != "" {
99+
if c.SingleBackup != "" && c.SingleFile != "" {
100+
return errors.New("must not pass --single_backup and " +
101+
"--single_file together")
102+
}
103+
var singleBackupBytes []byte
104+
if c.SingleBackup != "" {
105+
singleBackupBytes, err = hex.DecodeString(
106+
c.SingleBackup,
107+
)
108+
} else if c.SingleFile != "" {
109+
singleBackupBytes, err = os.ReadFile(c.SingleFile)
110+
}
111+
if err != nil {
112+
return fmt.Errorf("failed to get single backup: %w",
113+
err)
114+
}
115+
var s chanbackup.Single
116+
r := bytes.NewReader(singleBackupBytes)
117+
if err := s.UnpackFromReader(r, keyRing); err != nil {
118+
return fmt.Errorf("failed to unpack single backup: %w",
119+
err)
120+
}
121+
backups = append(backups, s)
122+
}
123+
if c.MultiBackup != "" || c.MultiFile != "" {
124+
if len(backups) != 0 {
125+
return errors.New("must not pass single and multi " +
126+
"backups together")
127+
}
128+
if c.MultiBackup != "" && c.MultiFile != "" {
129+
return errors.New("must not pass --multi_backup and " +
130+
"--multi_file together")
131+
}
132+
var multiBackupBytes []byte
133+
if c.MultiBackup != "" {
134+
multiBackupBytes, err = hex.DecodeString(c.MultiBackup)
135+
} else if c.MultiFile != "" {
136+
multiBackupBytes, err = os.ReadFile(c.MultiFile)
137+
}
138+
if err != nil {
139+
return fmt.Errorf("failed to get multi backup: %w", err)
140+
}
141+
var m chanbackup.Multi
142+
r := bytes.NewReader(multiBackupBytes)
143+
if err := m.UnpackFromReader(r, keyRing); err != nil {
144+
return fmt.Errorf("failed to unpack multi backup: %w",
145+
err)
146+
}
147+
backups = append(backups, m.StaticBackups...)
148+
}
149+
150+
backupsWithInputs := make([]chanbackup.Single, 0, len(backups))
151+
for _, s := range backups {
152+
if s.CloseTxInputs != nil {
153+
backupsWithInputs = append(backupsWithInputs, s)
154+
}
155+
}
156+
157+
fmt.Println()
158+
fmt.Printf("Found %d channel backups, %d of them have close tx.\n",
159+
len(backups), len(backupsWithInputs))
160+
161+
if len(backupsWithInputs) == 0 {
162+
fmt.Println("No channel backups that can be used for force " +
163+
"close.")
164+
return nil
165+
}
166+
167+
fmt.Println()
168+
fmt.Println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
169+
fmt.Println(strings.TrimSpace(forceCloseWarning))
170+
fmt.Println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
171+
fmt.Println()
172+
173+
fmt.Printf("Type YES to proceed: ")
174+
var userInput string
175+
if _, err := fmt.Scan(&userInput); err != nil {
176+
return errors.New("failed to read user input")
177+
}
178+
if strings.TrimSpace(userInput) != "YES" {
179+
return errors.New("cancelled by user")
180+
}
181+
182+
if c.Publish {
183+
fmt.Println("Signed transactions will be broadcasted " +
184+
"automatically.")
185+
fmt.Printf("Type YES again to proceed: ")
186+
if _, err := fmt.Scan(&userInput); err != nil {
187+
return errors.New("failed to read user input")
188+
}
189+
if strings.TrimSpace(userInput) != "YES" {
190+
return errors.New("cancelled by user")
191+
}
192+
}
193+
194+
for _, s := range backupsWithInputs {
195+
signedTx, err := chanbackup.SignCloseTx(
196+
s, keyRing, signer, signer,
197+
)
198+
if err != nil {
199+
return fmt.Errorf("signCloseTx failed for %s: %w",
200+
s.FundingOutpoint, err)
201+
}
202+
var buf bytes.Buffer
203+
if err := signedTx.Serialize(&buf); err != nil {
204+
return fmt.Errorf("failed to serialize signed %s: %w",
205+
s.FundingOutpoint, err)
206+
}
207+
txHex := hex.EncodeToString(buf.Bytes())
208+
fmt.Println(s.FundingOutpoint)
209+
fmt.Println(txHex)
210+
fmt.Println()
211+
212+
// Publish TX.
213+
if c.Publish {
214+
response, err := api.PublishTx(txHex)
215+
if err != nil {
216+
return err
217+
}
218+
log.Infof("Published TX %s, response: %s",
219+
signedTx.TxHash(), response)
220+
}
221+
}
222+
223+
return nil
224+
}

0 commit comments

Comments
 (0)