Skip to content

Commit 53f886c

Browse files
committed
fakechanbackup: create fake backup from graph data
1 parent 5aa69d3 commit 53f886c

File tree

7 files changed

+254
-51
lines changed

7 files changed

+254
-51
lines changed

btc/summary.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ func reportOutspend(api *ExplorerAPI,
9797
entry.ClosingTX.AllOutsSpent = false
9898
summaryFile.ChannelsWithUnspent++
9999

100+
for _, o := range utxo {
101+
if o.ScriptPubkeyType == "v0_p2wpkh" {
102+
entry.ClosingTX.ToRemoteAddr = o.ScriptPubkeyAddr
103+
}
104+
}
105+
100106
if couldBeOurs(entry, utxo) {
101107
summaryFile.ChannelsWithPotential++
102108
summaryFile.FundsForceClose += utxo[0].Value

cmd/chantools/fakechanbackup.go

Lines changed: 172 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@ import (
44
"bytes"
55
"encoding/hex"
66
"fmt"
7+
"github.com/lightningnetwork/lnd/tor"
8+
"io/ioutil"
79
"net"
810
"strconv"
911
"strings"
1012
"time"
1113

1214
"github.com/btcsuite/btcd/btcec"
15+
"github.com/btcsuite/btcd/wire"
1316
"github.com/btcsuite/btcutil"
17+
"github.com/gogo/protobuf/jsonpb"
1418
"github.com/guggero/chantools/lnd"
1519
"github.com/lightningnetwork/lnd/chanbackup"
1620
"github.com/lightningnetwork/lnd/channeldb"
1721
"github.com/lightningnetwork/lnd/keychain"
22+
"github.com/lightningnetwork/lnd/lnrpc"
1823
"github.com/lightningnetwork/lnd/lnwire"
1924
"github.com/spf13/cobra"
2025
)
@@ -23,9 +28,11 @@ type fakeChanBackupCommand struct {
2328
NodeAddr string
2429
ChannelPoint string
2530
ShortChanID string
26-
Initiator bool
2731
Capacity uint64
28-
MultiFile string
32+
33+
FromChannelGraph string
34+
35+
MultiFile string
2936

3037
rootKey *rootKey
3138
cmd *cobra.Command
@@ -47,13 +54,27 @@ output. But to initiate DLP, we would need to have a channel.backup file.
4754
Fortunately, if we have enough information about the channel, we can create a
4855
faked/skeleton channel.backup file that at least lets us talk to the other node
4956
and ask them to do their part. Then we can later brute-force the private key for
50-
the transaction output of our part of the funds (see rescueclosed command).`,
57+
the transaction output of our part of the funds (see rescueclosed command).
58+
59+
There are two versions of this command: The first one is to create a fake
60+
backup for a single channel where all flags (except --from_channel_graph) need
61+
to be set. This is the easiest to use since it only relies on data that is
62+
publicly available (for example on 1ml.com) but involves more manual work.
63+
The second version of the command only takes the --from_channel_graph and
64+
--multi_file flags and tries to assemble all channels found in the public
65+
network graph (must be provided in the JSON format that the
66+
'lncli describegraph' command returns) into a fake backup file. This is the
67+
most convenient way to use this command but requires one to have a fully synced
68+
lnd node.`,
5169
Example: `chantools fakechanbackup --rootkey xprvxxxxxxxxxx \
5270
--capacity 123456 \
5371
--channelpoint f39310xxxxxxxxxx:1 \
54-
--initiator \
5572
--remote_node_addr [email protected]:9735 \
5673
--short_channel_id 566222x300x1 \
74+
--multi_file fake.backup
75+
76+
chantools fakechanbackup --rootkey xprvxxxxxxxxxx \
77+
--from_channel_graph lncli_describegraph.json \
5778
--multi_file fake.backup`,
5879
RunE: cc.Execute,
5980
}
@@ -76,9 +97,10 @@ the transaction output of our part of the funds (see rescueclosed command).`,
7697
&cc.Capacity, "capacity", 0, "the channel's capacity in "+
7798
"satoshis",
7899
)
79-
cc.cmd.Flags().BoolVar(
80-
&cc.Initiator, "initiator", false, "whether our node was the "+
81-
"initiator (funder) of the channel",
100+
cc.cmd.Flags().StringVar(
101+
&cc.FromChannelGraph, "from_channel_graph", "", "the full "+
102+
"LN channel graph in the JSON format that the "+
103+
"'lncli describegraph' returns",
82104
)
83105
multiFileName := fmt.Sprintf("results/fake-%s.backup",
84106
time.Now().Format("2006-01-02-15-04-05"))
@@ -104,6 +126,21 @@ func (c *fakeChanBackupCommand) Execute(_ *cobra.Command, _ []string) error {
104126
ChainParams: chainParams,
105127
}
106128

129+
if c.FromChannelGraph != "" {
130+
graphBytes, err := ioutil.ReadFile(c.FromChannelGraph)
131+
if err != nil {
132+
return fmt.Errorf("error reading graph JSON file %s: "+
133+
"%v", c.FromChannelGraph, err)
134+
}
135+
graph := &lnrpc.ChannelGraph{}
136+
err = jsonpb.UnmarshalString(string(graphBytes), graph)
137+
if err != nil {
138+
return fmt.Errorf("error parsing graph JSON: %v", err)
139+
}
140+
141+
return backupFromGraph(graph, keyRing, multiFile)
142+
}
143+
107144
// Parse channel point of channel to fake.
108145
chanOp, err := lnd.ParseOutpoint(c.ChannelPoint)
109146
if err != nil {
@@ -160,8 +197,134 @@ func (c *fakeChanBackupCommand) Execute(_ *cobra.Command, _ []string) error {
160197
"be equal to index on --channelpoint")
161198
}
162199

163-
// Create some fake channel config.
164-
chanCfg := channeldb.ChannelConfig{
200+
singles := []chanbackup.Single{newSingle(
201+
*chanOp, shortChanID, nodePubkey, []net.Addr{addr},
202+
btcutil.Amount(c.Capacity),
203+
)}
204+
return writeBackups(singles, keyRing, multiFile)
205+
}
206+
207+
func backupFromGraph(graph *lnrpc.ChannelGraph, keyRing *lnd.HDKeyRing,
208+
multiFile *chanbackup.MultiFile) error {
209+
210+
// Since we have the master root key, we can find out our local node's
211+
// identity pubkey by just deriving it.
212+
nodePubKey, err := keyRing.NodePubKey()
213+
if err != nil {
214+
return fmt.Errorf("error deriving node pubkey: %v", err)
215+
}
216+
nodePubKeyStr := hex.EncodeToString(nodePubKey.SerializeCompressed())
217+
218+
// Let's now find all channels in the graph that our node is part of.
219+
channels := lnd.AllNodeChannels(graph, nodePubKeyStr)
220+
221+
// Let's create a single backup entry for each channel.
222+
singles := make([]chanbackup.Single, len(channels))
223+
for idx, channel := range channels {
224+
var peerPubKeyStr string
225+
if channel.Node1Pub == nodePubKeyStr {
226+
peerPubKeyStr = channel.Node2Pub
227+
} else {
228+
peerPubKeyStr = channel.Node1Pub
229+
}
230+
231+
peerPubKeyBytes, err := hex.DecodeString(peerPubKeyStr)
232+
if err != nil {
233+
return fmt.Errorf("error parsing hex: %v", err)
234+
}
235+
peerPubKey, err := btcec.ParsePubKey(
236+
peerPubKeyBytes, btcec.S256(),
237+
)
238+
if err != nil {
239+
return fmt.Errorf("error parsing pubkey: %v", err)
240+
}
241+
242+
peer, err := lnd.FindNode(graph, peerPubKeyStr)
243+
if err != nil {
244+
return err
245+
}
246+
peerAddresses := make([]net.Addr, len(peer.Addresses))
247+
for idx, peerAddr := range peer.Addresses {
248+
var err error
249+
if strings.Contains(peerAddr.Addr, ".onion") {
250+
peerAddresses[idx], err = tor.ParseAddr(
251+
peerAddr.Addr, "",
252+
)
253+
if err != nil {
254+
return fmt.Errorf("error parsing "+
255+
"tor address: %v", err)
256+
}
257+
258+
continue
259+
}
260+
peerAddresses[idx], err = net.ResolveTCPAddr(
261+
"tcp", peerAddr.Addr,
262+
)
263+
if err != nil {
264+
return fmt.Errorf("could not parse addr: %s",
265+
err)
266+
}
267+
}
268+
269+
shortChanID := lnwire.NewShortChanIDFromInt(channel.ChannelId)
270+
chanOp, err := lnd.ParseOutpoint(channel.ChanPoint)
271+
if err != nil {
272+
return fmt.Errorf("error parsing channel point: %v",
273+
err)
274+
}
275+
276+
singles[idx] = newSingle(
277+
*chanOp, shortChanID, peerPubKey, peerAddresses,
278+
btcutil.Amount(channel.Capacity),
279+
)
280+
}
281+
282+
return writeBackups(singles, keyRing, multiFile)
283+
}
284+
285+
func writeBackups(singles []chanbackup.Single, keyRing keychain.KeyRing,
286+
multiFile *chanbackup.MultiFile) error {
287+
288+
newMulti := chanbackup.Multi{
289+
Version: chanbackup.DefaultMultiVersion,
290+
StaticBackups: singles,
291+
}
292+
var packed bytes.Buffer
293+
err := newMulti.PackToWriter(&packed, keyRing)
294+
if err != nil {
295+
return fmt.Errorf("unable to multi-pack backups: %v", err)
296+
}
297+
298+
return multiFile.UpdateAndSwap(packed.Bytes())
299+
}
300+
301+
func newSingle(fundingOutPoint wire.OutPoint, shortChanID lnwire.ShortChannelID,
302+
nodePubKey *btcec.PublicKey, addrs []net.Addr,
303+
capacity btcutil.Amount) chanbackup.Single {
304+
305+
return chanbackup.Single{
306+
Version: chanbackup.DefaultSingleVersion,
307+
IsInitiator: true,
308+
ChainHash: *chainParams.GenesisHash,
309+
FundingOutpoint: fundingOutPoint,
310+
ShortChannelID: shortChanID,
311+
RemoteNodePub: nodePubKey,
312+
Addresses: addrs,
313+
Capacity: capacity,
314+
LocalChanCfg: fakeChanCfg(nodePubKey),
315+
RemoteChanCfg: fakeChanCfg(nodePubKey),
316+
ShaChainRootDesc: keychain.KeyDescriptor{
317+
PubKey: nodePubKey,
318+
KeyLocator: keychain.KeyLocator{
319+
Family: keychain.KeyFamilyRevocationRoot,
320+
Index: 1,
321+
},
322+
},
323+
}
324+
}
325+
326+
func fakeChanCfg(nodePubkey *btcec.PublicKey) channeldb.ChannelConfig {
327+
return channeldb.ChannelConfig{
165328
ChannelConstraints: channeldb.ChannelConstraints{
166329
DustLimit: 500,
167330
ChanReserve: 5000,
@@ -206,34 +369,4 @@ func (c *fakeChanBackupCommand) Execute(_ *cobra.Command, _ []string) error {
206369
},
207370
},
208371
}
209-
210-
newMulti := chanbackup.Multi{
211-
Version: chanbackup.DefaultMultiVersion,
212-
StaticBackups: []chanbackup.Single{{
213-
Version: chanbackup.DefaultSingleVersion,
214-
IsInitiator: c.Initiator,
215-
ChainHash: *chainParams.GenesisHash,
216-
FundingOutpoint: *chanOp,
217-
ShortChannelID: shortChanID,
218-
RemoteNodePub: nodePubkey,
219-
Addresses: []net.Addr{addr},
220-
Capacity: btcutil.Amount(c.Capacity),
221-
LocalChanCfg: chanCfg,
222-
RemoteChanCfg: chanCfg,
223-
ShaChainRootDesc: keychain.KeyDescriptor{
224-
PubKey: nodePubkey,
225-
KeyLocator: keychain.KeyLocator{
226-
Family: keychain.KeyFamilyRevocationRoot,
227-
Index: 1,
228-
},
229-
},
230-
}},
231-
}
232-
var packed bytes.Buffer
233-
err = newMulti.PackToWriter(&packed, keyRing)
234-
if err != nil {
235-
return fmt.Errorf("unable to multi-pack backups: %v", err)
236-
}
237-
238-
return multiFile.UpdateAndSwap(packed.Bytes())
239372
}

dataformat/summary.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type ClosingTX struct {
99
ForceClose bool `json:"force_close"`
1010
AllOutsSpent bool `json:"all_outputs_spent"`
1111
OurAddr string `json:"our_addr"`
12+
ToRemoteAddr string `json:"to_remote_addr"`
1213
SweepPrivkey string `json:"sweep_privkey"`
1314
ConfHeight uint32 `json:"conf_height"`
1415
}

doc/chantools_fakechanbackup.md

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ faked/skeleton channel.backup file that at least lets us talk to the other node
1717
and ask them to do their part. Then we can later brute-force the private key for
1818
the transaction output of our part of the funds (see rescueclosed command).
1919

20+
There are two versions of this command: The first one is to create a fake
21+
backup for a single channel where all flags (except --from_channel_graph) need
22+
to be set. This is the easiest to use since it only relies on data that is
23+
publicly available (for example on 1ml.com) but involves more manual work.
24+
The second version of the command only takes the --from_channel_graph and
25+
--multi_file flags and tries to assemble all channels found in the public
26+
network graph (must be provided in the JSON format that the
27+
'lncli describegraph' command returns) into a fake backup file. This is the
28+
most convenient way to use this command but requires one to have a fully synced
29+
lnd node.
30+
2031
```
2132
chantools fakechanbackup [flags]
2233
```
@@ -27,24 +38,27 @@ chantools fakechanbackup [flags]
2738
chantools fakechanbackup --rootkey xprvxxxxxxxxxx \
2839
--capacity 123456 \
2940
--channelpoint f39310xxxxxxxxxx:1 \
30-
--initiator \
3141
--remote_node_addr [email protected]:9735 \
3242
--short_channel_id 566222x300x1 \
3343
--multi_file fake.backup
44+
45+
chantools fakechanbackup --rootkey xprvxxxxxxxxxx \
46+
--from_channel_graph lncli_describegraph.json \
47+
--multi_file fake.backup
3448
```
3549

3650
### Options
3751

3852
```
39-
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
40-
--capacity uint the channel's capacity in satoshis
41-
--channelpoint string funding transaction outpoint of the channel to rescue (<txid>:<txindex>) as it is displayed on 1ml.com
42-
-h, --help help for fakechanbackup
43-
--initiator whether our node was the initiator (funder) of the channel
44-
--multi_file string the fake channel backup file to create (default "results/fake-2021-03-01-10-12-23.backup")
45-
--remote_node_addr string the remote node connection information in the format pubkey@host:port
46-
--rootkey string BIP32 HD root key of the wallet to use for encrypting the backup; leave empty to prompt for lnd 24 word aezeed
47-
--short_channel_id string the short channel ID in the format <blockheight>x<transactionindex>x<outputindex>
53+
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
54+
--capacity uint the channel's capacity in satoshis
55+
--channelpoint string funding transaction outpoint of the channel to rescue (<txid>:<txindex>) as it is displayed on 1ml.com
56+
--from_channel_graph string the full LN channel graph in the JSON format that the 'lncli describegraph' returns
57+
-h, --help help for fakechanbackup
58+
--multi_file string the fake channel backup file to create (default "results/fake-2021-05-01-22-08-48.backup")
59+
--remote_node_addr string the remote node connection information in the format pubkey@host:port
60+
--rootkey string BIP32 HD root key of the wallet to use for encrypting the backup; leave empty to prompt for lnd 24 word aezeed
61+
--short_channel_id string the short channel ID in the format <blockheight>x<transactionindex>x<outputindex>
4862
```
4963

5064
### Options inherited from parent commands

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/btcsuite/btcwallet/walletdb v1.3.4
1212
github.com/coreos/bbolt v1.3.3
1313
github.com/davecgh/go-spew v1.1.1
14+
github.com/gogo/protobuf v1.2.1
1415
github.com/gohugoio/hugo v0.79.1 // indirect
1516
github.com/jessevdk/go-flags v1.4.0 // indirect
1617
github.com/lightningnetwork/lnd v0.11.1-beta

lnd/graph.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package lnd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/lightningnetwork/lnd/lnrpc"
7+
)
8+
9+
func AllNodeChannels(graph *lnrpc.ChannelGraph,
10+
nodePubKey string) []*lnrpc.ChannelEdge {
11+
12+
var result []*lnrpc.ChannelEdge
13+
for _, edge := range graph.Edges {
14+
if edge.Node1Pub != nodePubKey && edge.Node2Pub != nodePubKey {
15+
continue
16+
}
17+
18+
result = append(result, edge)
19+
}
20+
21+
return result
22+
}
23+
24+
func FindNode(graph *lnrpc.ChannelGraph,
25+
nodePubKey string) (*lnrpc.LightningNode, error) {
26+
27+
for _, node := range graph.Nodes {
28+
if node.PubKey == nodePubKey {
29+
return node, nil
30+
}
31+
}
32+
33+
return nil, fmt.Errorf("node %s not found in graph", nodePubKey)
34+
}

0 commit comments

Comments
 (0)