pull/95/merge
Boris Nagaev 2 months ago committed by GitHub
commit 293a8bd2ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -18,6 +18,20 @@ import (
"github.com/spf13/cobra"
)
const forceCloseWarning = `
If you are certain that a node is offline for good (AFTER you've tried SCB!)
and a channel is still open, you can use this method to force-close your
latest state that you have in your channel.db.
**!!! WARNING !!! DANGER !!! WARNING !!!**
If you do this and the state that you publish is *not* the latest state, then
the remote node *could* punish you by taking the whole channel amount *if* they
come online before you can sweep the funds from the time locked (144 - 2000
blocks) transaction *or* they have a watch tower looking out for them.
**This should absolutely be the last resort and you have been warned!**`
type forceCloseCommand struct {
APIURL string
ChannelDB string
@ -34,18 +48,7 @@ func newForceCloseCommand() *cobra.Command {
Use: "forceclose",
Short: "Force-close the last state that is in the channel.db " +
"provided",
Long: `If you are certain that a node is offline for good (AFTER
you've tried SCB!) and a channel is still open, you can use this method to
force-close your latest state that you have in your channel.db.
**!!! WARNING !!! DANGER !!! WARNING !!!**
If you do this and the state that you publish is *not* the latest state, then
the remote node *could* punish you by taking the whole channel amount *if* they
come online before you can sweep the funds from the time locked (144 - 2000
blocks) transaction *or* they have a watch tower looking out for them.
**This should absolutely be the last resort and you have been warned!**`,
Long: forceCloseWarning,
Example: `chantools forceclose \
--fromsummary results/summary-xxxx-yyyy.json
--channeldb ~/.lnd/data/graph/mainnet/channel.db \

@ -114,6 +114,7 @@ func main() {
newFilterBackupCommand(),
newFixOldBackupCommand(),
newForceCloseCommand(),
newScbForceCloseCommand(),
newGenImportScriptCommand(),
newMigrateDBCommand(),
newPullAnchorCommand(),

@ -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)
}

@ -189,3 +189,5 @@ require (
// allows us to specify that as an option. This is required for the
// taproot-assets dependency to function properly.
replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display
replace github.com/lightningnetwork/lnd => github.com/starius/lnd v0.17.0-beta-close-tx-4

@ -506,8 +506,6 @@ github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display h1:pRdza2wl
github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f h1:Pua7+5TcFEJXIIZ1I2YAUapmbcttmLj4TTi786bIi3s=
github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI=
github.com/lightningnetwork/lnd v0.17.0-beta h1:Vzl2W3ClIDdffuQD5IaZwyP7CKR/CbXxWqws72Cq51g=
github.com/lightningnetwork/lnd v0.17.0-beta/go.mod h1:8w27nArqZ1P7U6FP9U78GlaTLNm7u9GhV5Edv1C1yRU=
github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg=
github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0=
github.com/lightningnetwork/lnd/clock v1.1.1/go.mod h1:mGnAhPyjYZQJmebS7aevElXKTFDuO+uNFFfMXK1W8xQ=
@ -662,6 +660,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/starius/lnd v0.17.0-beta-close-tx-4 h1:xQNKkjT+G9oaJtCtpOl8/6KtEjXthis+9Kk3/VmGCKI=
github.com/starius/lnd v0.17.0-beta-close-tx-4/go.mod h1:8w27nArqZ1P7U6FP9U78GlaTLNm7u9GhV5Edv1C1yRU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=

@ -19,7 +19,7 @@ import (
)
type Signer struct {
input.MockSigner
*input.MusigSessionManager
ExtendedKey *hdkeychain.ExtendedKey
ChainParams *chaincfg.Params

Loading…
Cancel
Save