From 53f886cf43d62d6e8e728178872e83e00b06151a Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sat, 1 May 2021 12:54:48 +0200 Subject: [PATCH] fakechanbackup: create fake backup from graph data --- btc/summary.go | 6 + cmd/chantools/fakechanbackup.go | 211 ++++++++++++++++++++++++++------ dataformat/summary.go | 1 + doc/chantools_fakechanbackup.md | 34 +++-- go.mod | 1 + lnd/graph.go | 34 +++++ lnd/hdkeychain.go | 18 ++- 7 files changed, 254 insertions(+), 51 deletions(-) create mode 100644 lnd/graph.go diff --git a/btc/summary.go b/btc/summary.go index 9a79bc4..20ecb33 100644 --- a/btc/summary.go +++ b/btc/summary.go @@ -97,6 +97,12 @@ func reportOutspend(api *ExplorerAPI, entry.ClosingTX.AllOutsSpent = false summaryFile.ChannelsWithUnspent++ + for _, o := range utxo { + if o.ScriptPubkeyType == "v0_p2wpkh" { + entry.ClosingTX.ToRemoteAddr = o.ScriptPubkeyAddr + } + } + if couldBeOurs(entry, utxo) { summaryFile.ChannelsWithPotential++ summaryFile.FundsForceClose += utxo[0].Value diff --git a/cmd/chantools/fakechanbackup.go b/cmd/chantools/fakechanbackup.go index e2fdcef..215141f 100644 --- a/cmd/chantools/fakechanbackup.go +++ b/cmd/chantools/fakechanbackup.go @@ -4,17 +4,22 @@ import ( "bytes" "encoding/hex" "fmt" + "github.com/lightningnetwork/lnd/tor" + "io/ioutil" "net" "strconv" "strings" "time" "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/gogo/protobuf/jsonpb" "github.com/guggero/chantools/lnd" "github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnwire" "github.com/spf13/cobra" ) @@ -23,9 +28,11 @@ type fakeChanBackupCommand struct { NodeAddr string ChannelPoint string ShortChanID string - Initiator bool Capacity uint64 - MultiFile string + + FromChannelGraph string + + MultiFile string rootKey *rootKey cmd *cobra.Command @@ -47,13 +54,27 @@ 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).`, +the transaction output of our part of the funds (see rescueclosed command). + +There are two versions of this command: The first one is to create a fake +backup for a single channel where all flags (except --from_channel_graph) need +to be set. This is the easiest to use since it only relies on data that is +publicly available (for example on 1ml.com) but involves more manual work. +The second version of the command only takes the --from_channel_graph and +--multi_file flags and tries to assemble all channels found in the public +network graph (must be provided in the JSON format that the +'lncli describegraph' command returns) into a fake backup file. This is the +most convenient way to use this command but requires one to have a fully synced +lnd node.`, 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 + +chantools fakechanbackup --rootkey xprvxxxxxxxxxx \ + --from_channel_graph lncli_describegraph.json \ --multi_file fake.backup`, RunE: cc.Execute, } @@ -76,9 +97,10 @@ the transaction output of our part of the funds (see rescueclosed command).`, &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", + cc.cmd.Flags().StringVar( + &cc.FromChannelGraph, "from_channel_graph", "", "the full "+ + "LN channel graph in the JSON format that the "+ + "'lncli describegraph' returns", ) multiFileName := fmt.Sprintf("results/fake-%s.backup", time.Now().Format("2006-01-02-15-04-05")) @@ -104,6 +126,21 @@ func (c *fakeChanBackupCommand) Execute(_ *cobra.Command, _ []string) error { ChainParams: chainParams, } + if c.FromChannelGraph != "" { + graphBytes, err := ioutil.ReadFile(c.FromChannelGraph) + if err != nil { + return fmt.Errorf("error reading graph JSON file %s: "+ + "%v", c.FromChannelGraph, err) + } + graph := &lnrpc.ChannelGraph{} + err = jsonpb.UnmarshalString(string(graphBytes), graph) + if err != nil { + return fmt.Errorf("error parsing graph JSON: %v", err) + } + + return backupFromGraph(graph, keyRing, multiFile) + } + // Parse channel point of channel to fake. chanOp, err := lnd.ParseOutpoint(c.ChannelPoint) if err != nil { @@ -160,8 +197,134 @@ func (c *fakeChanBackupCommand) Execute(_ *cobra.Command, _ []string) error { "be equal to index on --channelpoint") } - // Create some fake channel config. - chanCfg := channeldb.ChannelConfig{ + singles := []chanbackup.Single{newSingle( + *chanOp, shortChanID, nodePubkey, []net.Addr{addr}, + btcutil.Amount(c.Capacity), + )} + return writeBackups(singles, keyRing, multiFile) +} + +func backupFromGraph(graph *lnrpc.ChannelGraph, keyRing *lnd.HDKeyRing, + multiFile *chanbackup.MultiFile) error { + + // Since we have the master root key, we can find out our local node's + // identity pubkey by just deriving it. + nodePubKey, err := keyRing.NodePubKey() + if err != nil { + return fmt.Errorf("error deriving node pubkey: %v", err) + } + nodePubKeyStr := hex.EncodeToString(nodePubKey.SerializeCompressed()) + + // Let's now find all channels in the graph that our node is part of. + channels := lnd.AllNodeChannels(graph, nodePubKeyStr) + + // Let's create a single backup entry for each channel. + singles := make([]chanbackup.Single, len(channels)) + for idx, channel := range channels { + var peerPubKeyStr string + if channel.Node1Pub == nodePubKeyStr { + peerPubKeyStr = channel.Node2Pub + } else { + peerPubKeyStr = channel.Node1Pub + } + + peerPubKeyBytes, err := hex.DecodeString(peerPubKeyStr) + if err != nil { + return fmt.Errorf("error parsing hex: %v", err) + } + peerPubKey, err := btcec.ParsePubKey( + peerPubKeyBytes, btcec.S256(), + ) + if err != nil { + return fmt.Errorf("error parsing pubkey: %v", err) + } + + peer, err := lnd.FindNode(graph, peerPubKeyStr) + if err != nil { + return err + } + peerAddresses := make([]net.Addr, len(peer.Addresses)) + for idx, peerAddr := range peer.Addresses { + var err error + if strings.Contains(peerAddr.Addr, ".onion") { + peerAddresses[idx], err = tor.ParseAddr( + peerAddr.Addr, "", + ) + if err != nil { + return fmt.Errorf("error parsing "+ + "tor address: %v", err) + } + + continue + } + peerAddresses[idx], err = net.ResolveTCPAddr( + "tcp", peerAddr.Addr, + ) + if err != nil { + return fmt.Errorf("could not parse addr: %s", + err) + } + } + + shortChanID := lnwire.NewShortChanIDFromInt(channel.ChannelId) + chanOp, err := lnd.ParseOutpoint(channel.ChanPoint) + if err != nil { + return fmt.Errorf("error parsing channel point: %v", + err) + } + + singles[idx] = newSingle( + *chanOp, shortChanID, peerPubKey, peerAddresses, + btcutil.Amount(channel.Capacity), + ) + } + + return writeBackups(singles, keyRing, multiFile) +} + +func writeBackups(singles []chanbackup.Single, keyRing keychain.KeyRing, + multiFile *chanbackup.MultiFile) error { + + newMulti := chanbackup.Multi{ + Version: chanbackup.DefaultMultiVersion, + StaticBackups: singles, + } + 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()) +} + +func newSingle(fundingOutPoint wire.OutPoint, shortChanID lnwire.ShortChannelID, + nodePubKey *btcec.PublicKey, addrs []net.Addr, + capacity btcutil.Amount) chanbackup.Single { + + return chanbackup.Single{ + Version: chanbackup.DefaultSingleVersion, + IsInitiator: true, + ChainHash: *chainParams.GenesisHash, + FundingOutpoint: fundingOutPoint, + ShortChannelID: shortChanID, + RemoteNodePub: nodePubKey, + Addresses: addrs, + Capacity: capacity, + LocalChanCfg: fakeChanCfg(nodePubKey), + RemoteChanCfg: fakeChanCfg(nodePubKey), + ShaChainRootDesc: keychain.KeyDescriptor{ + PubKey: nodePubKey, + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamilyRevocationRoot, + Index: 1, + }, + }, + } +} + +func fakeChanCfg(nodePubkey *btcec.PublicKey) channeldb.ChannelConfig { + return channeldb.ChannelConfig{ ChannelConstraints: channeldb.ChannelConstraints{ DustLimit: 500, ChanReserve: 5000, @@ -206,34 +369,4 @@ func (c *fakeChanBackupCommand) Execute(_ *cobra.Command, _ []string) error { }, }, } - - 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/dataformat/summary.go b/dataformat/summary.go index 6365c22..1c4cd35 100644 --- a/dataformat/summary.go +++ b/dataformat/summary.go @@ -9,6 +9,7 @@ type ClosingTX struct { ForceClose bool `json:"force_close"` AllOutsSpent bool `json:"all_outputs_spent"` OurAddr string `json:"our_addr"` + ToRemoteAddr string `json:"to_remote_addr"` SweepPrivkey string `json:"sweep_privkey"` ConfHeight uint32 `json:"conf_height"` } diff --git a/doc/chantools_fakechanbackup.md b/doc/chantools_fakechanbackup.md index c92979c..6f6fab5 100644 --- a/doc/chantools_fakechanbackup.md +++ b/doc/chantools_fakechanbackup.md @@ -17,6 +17,17 @@ 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). +There are two versions of this command: The first one is to create a fake +backup for a single channel where all flags (except --from_channel_graph) need +to be set. This is the easiest to use since it only relies on data that is +publicly available (for example on 1ml.com) but involves more manual work. +The second version of the command only takes the --from_channel_graph and +--multi_file flags and tries to assemble all channels found in the public +network graph (must be provided in the JSON format that the +'lncli describegraph' command returns) into a fake backup file. This is the +most convenient way to use this command but requires one to have a fully synced +lnd node. + ``` chantools fakechanbackup [flags] ``` @@ -27,24 +38,27 @@ chantools fakechanbackup [flags] 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 + +chantools fakechanbackup --rootkey xprvxxxxxxxxxx \ + --from_channel_graph lncli_describegraph.json \ + --multi_file fake.backup ``` ### Options ``` - --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag - --capacity uint the channel's capacity in satoshis - --channelpoint string funding transaction outpoint of the channel to rescue (:) as it is displayed on 1ml.com - -h, --help help for fakechanbackup - --initiator whether our node was the initiator (funder) of the channel - --multi_file string the fake channel backup file to create (default "results/fake-2021-03-01-10-12-23.backup") - --remote_node_addr string the remote node connection information in the format pubkey@host:port - --rootkey string BIP32 HD root key of the wallet to use for encrypting the backup; leave empty to prompt for lnd 24 word aezeed - --short_channel_id string the short channel ID in the format xx + --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag + --capacity uint the channel's capacity in satoshis + --channelpoint string funding transaction outpoint of the channel to rescue (:) as it is displayed on 1ml.com + --from_channel_graph string the full LN channel graph in the JSON format that the 'lncli describegraph' returns + -h, --help help for fakechanbackup + --multi_file string the fake channel backup file to create (default "results/fake-2021-05-01-22-08-48.backup") + --remote_node_addr string the remote node connection information in the format pubkey@host:port + --rootkey string BIP32 HD root key of the wallet to use for encrypting the backup; leave empty to prompt for lnd 24 word aezeed + --short_channel_id string the short channel ID in the format xx ``` ### Options inherited from parent commands diff --git a/go.mod b/go.mod index 15d0550..63b0e04 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/btcsuite/btcwallet/walletdb v1.3.4 github.com/coreos/bbolt v1.3.3 github.com/davecgh/go-spew v1.1.1 + github.com/gogo/protobuf v1.2.1 github.com/gohugoio/hugo v0.79.1 // indirect github.com/jessevdk/go-flags v1.4.0 // indirect github.com/lightningnetwork/lnd v0.11.1-beta diff --git a/lnd/graph.go b/lnd/graph.go new file mode 100644 index 0000000..c369527 --- /dev/null +++ b/lnd/graph.go @@ -0,0 +1,34 @@ +package lnd + +import ( + "fmt" + + "github.com/lightningnetwork/lnd/lnrpc" +) + +func AllNodeChannels(graph *lnrpc.ChannelGraph, + nodePubKey string) []*lnrpc.ChannelEdge { + + var result []*lnrpc.ChannelEdge + for _, edge := range graph.Edges { + if edge.Node1Pub != nodePubKey && edge.Node2Pub != nodePubKey { + continue + } + + result = append(result, edge) + } + + return result +} + +func FindNode(graph *lnrpc.ChannelGraph, + nodePubKey string) (*lnrpc.LightningNode, error) { + + for _, node := range graph.Nodes { + if node.PubKey == nodePubKey { + return node, nil + } + } + + return nil, fmt.Errorf("node %s not found in graph", nodePubKey) +} \ No newline at end of file diff --git a/lnd/hdkeychain.go b/lnd/hdkeychain.go index c946a03..3d08077 100644 --- a/lnd/hdkeychain.go +++ b/lnd/hdkeychain.go @@ -293,8 +293,8 @@ func (r *HDKeyRing) DeriveKey(keyLoc keychain.KeyLocator) ( }, nil } -// Check if a key descriptor is correct by making sure that we can derive the -// key that it describes. +// CheckDescriptor checks if a key descriptor is correct by making sure that we +// can derive the key that it describes. func (r *HDKeyRing) CheckDescriptor( keyDesc keychain.KeyDescriptor) error { @@ -335,3 +335,17 @@ func (r *HDKeyRing) CheckDescriptor( // derivable with the given information. return keychain.ErrCannotDerivePrivKey } + +// NodePubKey returns the public key that represents an lnd node's public +// network identity. +func (r *HDKeyRing) NodePubKey() (*btcec.PublicKey, error) { + keyDesc, err := r.DeriveKey(keychain.KeyLocator{ + Family: keychain.KeyFamilyNodeKey, + Index: 0, + }) + if err != nil { + return nil, err + } + + return keyDesc.PubKey, nil +}