diff --git a/cmd/chantools/fakechanbackup.go b/cmd/chantools/fakechanbackup.go new file mode 100644 index 0000000..e2fdcef --- /dev/null +++ b/cmd/chantools/fakechanbackup.go @@ -0,0 +1,239 @@ +package main + +import ( + "bytes" + "encoding/hex" + "fmt" + "net" + "strconv" + "strings" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcutil" + "github.com/guggero/chantools/lnd" + "github.com/lightningnetwork/lnd/chanbackup" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/spf13/cobra" +) + +type fakeChanBackupCommand struct { + NodeAddr string + ChannelPoint string + ShortChanID string + Initiator bool + Capacity uint64 + MultiFile string + + rootKey *rootKey + cmd *cobra.Command +} + +func newFakeChanBackupCommand() *cobra.Command { + cc := &fakeChanBackupCommand{} + cc.cmd = &cobra.Command{ + Use: "fakechanbackup", + Short: "Fake a channel backup file to attempt fund recovery", + Long: `If for any reason a node suffers from data loss and there is no +channel.backup for one or more channels, then the funds in the channel would +theoretically be lost forever. +If the remote node is still online and still knows about the channel, there is +hope. We can initiate DLP (Data Loss Protocol) and ask the remote node to +force-close the channel and to provide us with the per_commit_point that is +needed to derive the private key for our part of the force-close transaction +output. But to initiate DLP, we would need to have a channel.backup file. +Fortunately, if we have enough information about the channel, we can create a +faked/skeleton channel.backup file that at least lets us talk to the other node +and ask them to do their part. Then we can later brute-force the private key for +the transaction output of our part of the funds (see rescueclosed command).`, + Example: `chantools fakechanbackup --rootkey xprvxxxxxxxxxx \ + --capacity 123456 \ + --channelpoint f39310xxxxxxxxxx:1 \ + --initiator \ + --remote_node_addr 022c260xxxxxxxx@213.174.150.1:9735 \ + --short_channel_id 566222x300x1 \ + --multi_file fake.backup`, + RunE: cc.Execute, + } + cc.cmd.Flags().StringVar( + &cc.NodeAddr, "remote_node_addr", "", "the remote node "+ + "connection information in the format pubkey@host:"+ + "port", + ) + cc.cmd.Flags().StringVar( + &cc.ChannelPoint, "channelpoint", "", "funding transaction "+ + "outpoint of the channel to rescue (:) "+ + "as it is displayed on 1ml.com", + ) + cc.cmd.Flags().StringVar( + &cc.ShortChanID, "short_channel_id", "", "the short channel "+ + "ID in the format xx"+ + "", + ) + cc.cmd.Flags().Uint64Var( + &cc.Capacity, "capacity", 0, "the channel's capacity in "+ + "satoshis", + ) + cc.cmd.Flags().BoolVar( + &cc.Initiator, "initiator", false, "whether our node was the "+ + "initiator (funder) of the channel", + ) + multiFileName := fmt.Sprintf("results/fake-%s.backup", + time.Now().Format("2006-01-02-15-04-05")) + cc.cmd.Flags().StringVar( + &cc.MultiFile, "multi_file", multiFileName, "the fake channel "+ + "backup file to create", + ) + + cc.rootKey = newRootKey(cc.cmd, "encrypting the backup") + + return cc.cmd +} + +func (c *fakeChanBackupCommand) Execute(_ *cobra.Command, _ []string) error { + extendedKey, err := c.rootKey.read() + if err != nil { + return fmt.Errorf("error reading root key: %v", err) + } + + multiFile := chanbackup.NewMultiFile(c.MultiFile) + keyRing := &lnd.HDKeyRing{ + ExtendedKey: extendedKey, + ChainParams: chainParams, + } + + // Parse channel point of channel to fake. + chanOp, err := lnd.ParseOutpoint(c.ChannelPoint) + if err != nil { + return fmt.Errorf("error parsing channel point: %v", err) + } + + // Now parse the remote node info. + splitNodeInfo := strings.Split(c.NodeAddr, "@") + if len(splitNodeInfo) != 2 { + return fmt.Errorf("--remote_node_addr expected in format: " + + "pubkey@host:port") + } + pubKeyBytes, err := hex.DecodeString(splitNodeInfo[0]) + if err != nil { + return fmt.Errorf("could not parse pubkey hex string: %s", err) + } + nodePubkey, err := btcec.ParsePubKey(pubKeyBytes, btcec.S256()) + if err != nil { + return fmt.Errorf("could not parse pubkey: %s", err) + } + addr, err := net.ResolveTCPAddr("tcp", splitNodeInfo[1]) + if err != nil { + return fmt.Errorf("could not parse addr: %s", err) + } + + // Parse the short channel ID. + splitChanId := strings.Split(c.ShortChanID, "x") + if len(splitChanId) != 3 { + return fmt.Errorf("--short_channel_id expected in format: " + + "xx", + ) + } + blockHeight, err := strconv.ParseInt(splitChanId[0], 10, 32) + if err != nil { + return fmt.Errorf("could not parse block height: %s", err) + } + txIndex, err := strconv.ParseInt(splitChanId[1], 10, 32) + if err != nil { + return fmt.Errorf("could not parse transaction index: %s", err) + } + chanOutputIdx, err := strconv.ParseInt(splitChanId[2], 10, 32) + if err != nil { + return fmt.Errorf("could not parse output index: %s", err) + } + shortChanID := lnwire.ShortChannelID{ + BlockHeight: uint32(blockHeight), + TxIndex: uint32(txIndex), + TxPosition: uint16(chanOutputIdx), + } + + // Is the outpoint and/or short channel ID correct? + if uint32(chanOutputIdx) != chanOp.Index { + return fmt.Errorf("output index of --short_channel_id must " + + "be equal to index on --channelpoint") + } + + // Create some fake channel config. + chanCfg := channeldb.ChannelConfig{ + ChannelConstraints: channeldb.ChannelConstraints{ + DustLimit: 500, + ChanReserve: 5000, + MaxPendingAmount: 1, + MinHTLC: 1, + MaxAcceptedHtlcs: 200, + CsvDelay: 144, + }, + MultiSigKey: keychain.KeyDescriptor{ + PubKey: nodePubkey, + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamilyMultiSig, + Index: 0, + }, + }, + RevocationBasePoint: keychain.KeyDescriptor{ + PubKey: nodePubkey, + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamilyRevocationBase, + Index: 0, + }, + }, + PaymentBasePoint: keychain.KeyDescriptor{ + PubKey: nodePubkey, + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamilyPaymentBase, + Index: 0, + }, + }, + DelayBasePoint: keychain.KeyDescriptor{ + PubKey: nodePubkey, + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamilyDelayBase, + Index: 0, + }, + }, + HtlcBasePoint: keychain.KeyDescriptor{ + PubKey: nodePubkey, + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamilyHtlcBase, + Index: 0, + }, + }, + } + + newMulti := chanbackup.Multi{ + Version: chanbackup.DefaultMultiVersion, + StaticBackups: []chanbackup.Single{{ + Version: chanbackup.DefaultSingleVersion, + IsInitiator: c.Initiator, + ChainHash: *chainParams.GenesisHash, + FundingOutpoint: *chanOp, + ShortChannelID: shortChanID, + RemoteNodePub: nodePubkey, + Addresses: []net.Addr{addr}, + Capacity: btcutil.Amount(c.Capacity), + LocalChanCfg: chanCfg, + RemoteChanCfg: chanCfg, + ShaChainRootDesc: keychain.KeyDescriptor{ + PubKey: nodePubkey, + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamilyRevocationRoot, + Index: 1, + }, + }, + }}, + } + var packed bytes.Buffer + err = newMulti.PackToWriter(&packed, keyRing) + if err != nil { + return fmt.Errorf("unable to multi-pack backups: %v", err) + } + + return multiFile.UpdateAndSwap(packed.Bytes()) +} diff --git a/cmd/chantools/root.go b/cmd/chantools/root.go index be0e706..c98ac14 100644 --- a/cmd/chantools/root.go +++ b/cmd/chantools/root.go @@ -26,7 +26,7 @@ import ( const ( defaultAPIURL = "https://blockstream.info/api" - version = "0.8.1" + version = "0.8.2" na = "n/a" Commit = "" @@ -86,6 +86,7 @@ func main() { newDumpBackupCommand(), newDumpChannelsCommand(), newDocCommand(), + newFakeChanBackupCommand(), newFilterBackupCommand(), newFixOldBackupCommand(), newForceCloseCommand(),