Skip to content

Commit b78f22b

Browse files
committed
new command: scbforceclose
1 parent 701cc0a commit b78f22b

File tree

2 files changed

+349
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)