mirror of https://github.com/guggero/chantools
Merge dcc9b3bb33
into 450c2777af
commit
293a8bd2ed
@ -0,0 +1,335 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightninglabs/chantools/btc"
|
||||||
|
"github.com/lightninglabs/chantools/lnd"
|
||||||
|
"github.com/lightningnetwork/lnd/chanbackup"
|
||||||
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
|
"github.com/lightningnetwork/lnd/input"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
"github.com/lightningnetwork/lnd/shachain"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type scbForceCloseCommand struct {
|
||||||
|
APIURL string
|
||||||
|
Publish bool
|
||||||
|
|
||||||
|
// channel.backup.
|
||||||
|
SingleBackup string
|
||||||
|
SingleFile string
|
||||||
|
MultiBackup string
|
||||||
|
MultiFile string
|
||||||
|
|
||||||
|
rootKey *rootKey
|
||||||
|
cmd *cobra.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
func newScbForceCloseCommand() *cobra.Command {
|
||||||
|
cc := &scbForceCloseCommand{}
|
||||||
|
cc.cmd = &cobra.Command{
|
||||||
|
Use: "scbforceclose",
|
||||||
|
Short: "Force-close the last state that is in the SCB provided",
|
||||||
|
Long: forceCloseWarning,
|
||||||
|
Example: `chantools scbforceclose --multi_file channel.backup`,
|
||||||
|
RunE: cc.Execute,
|
||||||
|
}
|
||||||
|
cc.cmd.Flags().StringVar(
|
||||||
|
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
|
||||||
|
"be esplora compatible)",
|
||||||
|
)
|
||||||
|
|
||||||
|
cc.cmd.Flags().StringVar(
|
||||||
|
&cc.SingleBackup, "single_backup", "", "a hex encoded single channel "+
|
||||||
|
"backup obtained from exportchanbackup for force-closing channels",
|
||||||
|
)
|
||||||
|
cc.cmd.Flags().StringVar(
|
||||||
|
&cc.MultiBackup, "multi_backup", "", "a hex encoded multi-channel "+
|
||||||
|
"backup obtained from exportchanbackup for force-closing channels",
|
||||||
|
)
|
||||||
|
cc.cmd.Flags().StringVar(
|
||||||
|
&cc.SingleFile, "single_file", "", "the path to a single-channel "+
|
||||||
|
"backup file",
|
||||||
|
)
|
||||||
|
cc.cmd.Flags().StringVar(
|
||||||
|
&cc.MultiFile, "multi_file", "", "the path to a single-channel "+
|
||||||
|
"backup file (channel.backup)",
|
||||||
|
)
|
||||||
|
|
||||||
|
cc.cmd.Flags().BoolVar(
|
||||||
|
&cc.Publish, "publish", false, "publish force-closing TX to "+
|
||||||
|
"the chain API instead of just printing the TX",
|
||||||
|
)
|
||||||
|
|
||||||
|
cc.rootKey = newRootKey(cc.cmd, "decrypting the backup and signing tx")
|
||||||
|
|
||||||
|
return cc.cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *scbForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||||
|
extendedKey, err := c.rootKey.read()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading root key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
api := &btc.ExplorerAPI{BaseURL: c.APIURL}
|
||||||
|
keyRing := &lnd.HDKeyRing{
|
||||||
|
ExtendedKey: extendedKey,
|
||||||
|
ChainParams: chainParams,
|
||||||
|
}
|
||||||
|
var backups []chanbackup.Single
|
||||||
|
if c.SingleBackup != "" || c.SingleFile != "" {
|
||||||
|
if c.SingleBackup != "" && c.SingleFile != "" {
|
||||||
|
return fmt.Errorf("must not pass --single_backup and " +
|
||||||
|
"--single_file together")
|
||||||
|
}
|
||||||
|
var singleBackupBytes []byte
|
||||||
|
if c.SingleBackup != "" {
|
||||||
|
singleBackupBytes, err = hex.DecodeString(c.SingleBackup)
|
||||||
|
} else if c.SingleFile != "" {
|
||||||
|
singleBackupBytes, err = os.ReadFile(c.SingleFile)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get single backup: %w", err)
|
||||||
|
}
|
||||||
|
var s chanbackup.Single
|
||||||
|
r := bytes.NewReader(singleBackupBytes)
|
||||||
|
if err := s.UnpackFromReader(r, keyRing); err != nil {
|
||||||
|
return fmt.Errorf("failed to unpack single backup: %w", err)
|
||||||
|
}
|
||||||
|
backups = append(backups, s)
|
||||||
|
}
|
||||||
|
if c.MultiBackup != "" || c.MultiFile != "" {
|
||||||
|
if len(backups) != 0 {
|
||||||
|
return fmt.Errorf("must not pass single and multi " +
|
||||||
|
"backups together")
|
||||||
|
}
|
||||||
|
if c.MultiBackup != "" && c.MultiFile != "" {
|
||||||
|
return fmt.Errorf("must not pass --multi_backup and " +
|
||||||
|
"--multi_file together")
|
||||||
|
}
|
||||||
|
var multiBackupBytes []byte
|
||||||
|
if c.MultiBackup != "" {
|
||||||
|
multiBackupBytes, err = hex.DecodeString(c.MultiBackup)
|
||||||
|
} else if c.MultiFile != "" {
|
||||||
|
multiBackupBytes, err = os.ReadFile(c.MultiFile)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get multi backup: %w", err)
|
||||||
|
}
|
||||||
|
var m chanbackup.Multi
|
||||||
|
r := bytes.NewReader(multiBackupBytes)
|
||||||
|
if err := m.UnpackFromReader(r, keyRing); err != nil {
|
||||||
|
return fmt.Errorf("failed to unpack multi backup: %w", err)
|
||||||
|
}
|
||||||
|
backups = append(backups, m.StaticBackups...)
|
||||||
|
}
|
||||||
|
|
||||||
|
backupsWithInputs := make([]chanbackup.Single, 0, len(backups))
|
||||||
|
for _, s := range backups {
|
||||||
|
if s.CloseTxInputs != nil {
|
||||||
|
backupsWithInputs = append(backupsWithInputs, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("Found %d channel backups, %d of them have close tx.\n",
|
||||||
|
len(backups), len(backupsWithInputs))
|
||||||
|
|
||||||
|
if len(backupsWithInputs) == 0 {
|
||||||
|
fmt.Println("No channel backups that can be used for force close.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
|
||||||
|
fmt.Println(strings.TrimSpace(forceCloseWarning))
|
||||||
|
fmt.Println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
fmt.Printf("Type YES to proceed: ")
|
||||||
|
var userInput string
|
||||||
|
fmt.Scan(&userInput)
|
||||||
|
if strings.TrimSpace(userInput) != "YES" {
|
||||||
|
return fmt.Errorf("cancelled by user")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Publish {
|
||||||
|
fmt.Println("Signed transactions will be broadcasted automatically.")
|
||||||
|
fmt.Printf("Type YES again to proceed: ")
|
||||||
|
fmt.Scan(&userInput)
|
||||||
|
if strings.TrimSpace(userInput) != "YES" {
|
||||||
|
return fmt.Errorf("cancelled by user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range backupsWithInputs {
|
||||||
|
signedTx, err := signCloseTx(s, extendedKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("signCloseTx failed for %s: %w",
|
||||||
|
s.FundingOutpoint, err)
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := signedTx.Serialize(&buf); err != nil {
|
||||||
|
return fmt.Errorf("failed to serialize signed %s: %w",
|
||||||
|
s.FundingOutpoint, err)
|
||||||
|
}
|
||||||
|
txHex := hex.EncodeToString(buf.Bytes())
|
||||||
|
fmt.Println(s.FundingOutpoint)
|
||||||
|
fmt.Println(txHex)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Publish TX.
|
||||||
|
if c.Publish {
|
||||||
|
response, err := api.PublishTx(txHex)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infof("Published TX %s, response: %s",
|
||||||
|
signedTx.TxHash(), response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func signCloseTx(s chanbackup.Single, extendedKey *hdkeychain.ExtendedKey) (
|
||||||
|
*wire.MsgTx, error) {
|
||||||
|
|
||||||
|
if s.CloseTxInputs == nil {
|
||||||
|
return nil, fmt.Errorf("channel backup does not have data needed " +
|
||||||
|
"to sign force sloe tx")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each of the keys in our local channel config only have their
|
||||||
|
// locators populate, so we'll re-derive the raw key now.
|
||||||
|
keyRing := &lnd.HDKeyRing{
|
||||||
|
ExtendedKey: extendedKey,
|
||||||
|
ChainParams: chainParams,
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
s.LocalChanCfg.MultiSigKey, err = keyRing.DeriveKey(
|
||||||
|
s.LocalChanCfg.MultiSigKey.KeyLocator,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to derive multi sig key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signDesc, err := createSignDesc(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create signDesc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs := lnwallet.SignedCommitTxInputs{
|
||||||
|
CommitTx: s.CloseTxInputs.CommitTx,
|
||||||
|
CommitSig: s.CloseTxInputs.CommitSig,
|
||||||
|
OurKey: s.LocalChanCfg.MultiSigKey,
|
||||||
|
TheirKey: s.RemoteChanCfg.MultiSigKey,
|
||||||
|
SignDesc: signDesc,
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Version == chanbackup.SimpleTaprootVersion {
|
||||||
|
p, err := createTaprootNonceProducer(s, extendedKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inputs.Taproot = &lnwallet.TaprootSignedCommitTxInputs{
|
||||||
|
CommitHeight: s.CloseTxInputs.CommitHeight,
|
||||||
|
TaprootNonceProducer: p,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signer := &lnd.Signer{
|
||||||
|
ExtendedKey: extendedKey,
|
||||||
|
ChainParams: chainParams,
|
||||||
|
}
|
||||||
|
musigSessionManager := input.NewMusigSessionManager(signer.FetchPrivKey)
|
||||||
|
signer.MusigSessionManager = musigSessionManager
|
||||||
|
|
||||||
|
return lnwallet.GetSignedCommitTx(inputs, signer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSignDesc(s chanbackup.Single) (*input.SignDescriptor, error) {
|
||||||
|
// See LightningChannel.createSignDesc on how signDesc is produced.
|
||||||
|
|
||||||
|
var fundingPkScript, multiSigScript []byte
|
||||||
|
|
||||||
|
localKey := s.LocalChanCfg.MultiSigKey.PubKey
|
||||||
|
remoteKey := s.RemoteChanCfg.MultiSigKey.PubKey
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if s.Version == chanbackup.SimpleTaprootVersion {
|
||||||
|
fundingPkScript, _, err = input.GenTaprootFundingScript(
|
||||||
|
localKey, remoteKey, int64(s.Capacity),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
multiSigScript, err = input.GenMultiSigScript(
|
||||||
|
localKey.SerializeCompressed(),
|
||||||
|
remoteKey.SerializeCompressed(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fundingPkScript, err = input.WitnessScriptHash(multiSigScript)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &input.SignDescriptor{
|
||||||
|
KeyDesc: s.LocalChanCfg.MultiSigKey,
|
||||||
|
WitnessScript: multiSigScript,
|
||||||
|
Output: &wire.TxOut{
|
||||||
|
PkScript: fundingPkScript,
|
||||||
|
Value: int64(s.Capacity),
|
||||||
|
},
|
||||||
|
HashType: txscript.SigHashAll,
|
||||||
|
PrevOutputFetcher: txscript.NewCannedPrevOutputFetcher(
|
||||||
|
fundingPkScript, int64(s.Capacity),
|
||||||
|
),
|
||||||
|
InputIndex: 0,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTaprootNonceProducer(
|
||||||
|
s chanbackup.Single,
|
||||||
|
extendedKey *hdkeychain.ExtendedKey,
|
||||||
|
) (shachain.Producer, error) {
|
||||||
|
|
||||||
|
revPathStr := fmt.Sprintf("m/1017'/%d'/%d'/0/%d",
|
||||||
|
chainParams.HDCoinType,
|
||||||
|
s.ShaChainRootDesc.KeyLocator.Family,
|
||||||
|
s.ShaChainRootDesc.KeyLocator.Index,
|
||||||
|
)
|
||||||
|
revPath, err := lnd.ParsePath(revPathStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ShaChainRootDesc.PubKey != nil {
|
||||||
|
return nil, fmt.Errorf("taproot channels always use ECDH, " +
|
||||||
|
"but legacy ShaChainRootDesc with pubkey found")
|
||||||
|
}
|
||||||
|
revocationProducer, err := lnd.ShaChainFromPath(
|
||||||
|
extendedKey, revPath, s.LocalChanCfg.MultiSigKey.PubKey,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("lnd.ShaChainFromPath(extendedKey, %v, %v) "+
|
||||||
|
"failed: %w", revPath, s.ShaChainRootDesc.PubKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return channeldb.DeriveMusig2Shachain(revocationProducer)
|
||||||
|
}
|
Loading…
Reference in New Issue