|  | 
|  | 1 | +package main | 
|  | 2 | + | 
|  | 3 | +import ( | 
|  | 4 | +	"bytes" | 
|  | 5 | +	"encoding/hex" | 
|  | 6 | +	"fmt" | 
|  | 7 | +	"net" | 
|  | 8 | +	"strconv" | 
|  | 9 | +	"strings" | 
|  | 10 | +	"time" | 
|  | 11 | + | 
|  | 12 | +	"github.com/btcsuite/btcd/btcec" | 
|  | 13 | +	"github.com/btcsuite/btcutil" | 
|  | 14 | +	"github.com/guggero/chantools/lnd" | 
|  | 15 | +	"github.com/lightningnetwork/lnd/chanbackup" | 
|  | 16 | +	"github.com/lightningnetwork/lnd/channeldb" | 
|  | 17 | +	"github.com/lightningnetwork/lnd/keychain" | 
|  | 18 | +	"github.com/lightningnetwork/lnd/lnwire" | 
|  | 19 | +	"github.com/spf13/cobra" | 
|  | 20 | +) | 
|  | 21 | + | 
|  | 22 | +type fakeChanBackupCommand struct { | 
|  | 23 | +	NodeAddr     string | 
|  | 24 | +	ChannelPoint string | 
|  | 25 | +	ShortChanID  string | 
|  | 26 | +	Initiator    bool | 
|  | 27 | +	Capacity     uint64 | 
|  | 28 | +	MultiFile    string | 
|  | 29 | + | 
|  | 30 | +	rootKey *rootKey | 
|  | 31 | +	cmd     *cobra.Command | 
|  | 32 | +} | 
|  | 33 | + | 
|  | 34 | +func newFakeChanBackupCommand() *cobra.Command { | 
|  | 35 | +	cc := &fakeChanBackupCommand{} | 
|  | 36 | +	cc.cmd = &cobra.Command{ | 
|  | 37 | +		Use:   "fakechanbackup", | 
|  | 38 | +		Short: "Fake a channel backup file to attempt fund recovery", | 
|  | 39 | +		Long: `If for any reason a node suffers from data loss and there is no | 
|  | 40 | +channel.backup for one or more channels, then the funds in the channel would | 
|  | 41 | +theoretically be lost forever. | 
|  | 42 | +If the remote node is still online and still knows about the channel, there is | 
|  | 43 | +hope. We can initiate DLP (Data Loss Protocol) and ask the remote node to | 
|  | 44 | +force-close the channel and to provide us with the per_commit_point that is | 
|  | 45 | +needed to derive the private key for our part of the force-close transaction | 
|  | 46 | +output. But to initiate DLP, we would need to have a channel.backup file. | 
|  | 47 | +Fortunately, if we have enough information about the channel, we can create a | 
|  | 48 | +faked/skeleton channel.backup file that at least lets us talk to the other node | 
|  | 49 | +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).`, | 
|  | 51 | +		Example: `chantools fakechanbackup --rootkey xprvxxxxxxxxxx \ | 
|  | 52 | +	--capacity 123456 \ | 
|  | 53 | +	--channelpoint f39310xxxxxxxxxx:1 \ | 
|  | 54 | +	--initiator \ | 
|  | 55 | +	--remote_node_addr [email protected]:9735 \ | 
|  | 56 | +	--short_channel_id 566222x300x1 \ | 
|  | 57 | +	--multi_file fake.backup`, | 
|  | 58 | +		RunE: cc.Execute, | 
|  | 59 | +	} | 
|  | 60 | +	cc.cmd.Flags().StringVar( | 
|  | 61 | +		&cc.NodeAddr, "remote_node_addr", "", "the remote node "+ | 
|  | 62 | +			"connection information in the format pubkey@host:"+ | 
|  | 63 | +			"port", | 
|  | 64 | +	) | 
|  | 65 | +	cc.cmd.Flags().StringVar( | 
|  | 66 | +		&cc.ChannelPoint, "channelpoint", "", "funding transaction "+ | 
|  | 67 | +			"outpoint of the channel to rescue (<txid>:<txindex>) "+ | 
|  | 68 | +			"as it is displayed on 1ml.com", | 
|  | 69 | +	) | 
|  | 70 | +	cc.cmd.Flags().StringVar( | 
|  | 71 | +		&cc.ShortChanID, "short_channel_id", "", "the short channel "+ | 
|  | 72 | +			"ID in the format <blockheight>x<transactionindex>x"+ | 
|  | 73 | +			"<outputindex>", | 
|  | 74 | +	) | 
|  | 75 | +	cc.cmd.Flags().Uint64Var( | 
|  | 76 | +		&cc.Capacity, "capacity", 0, "the channel's capacity in "+ | 
|  | 77 | +			"satoshis", | 
|  | 78 | +	) | 
|  | 79 | +	cc.cmd.Flags().BoolVar( | 
|  | 80 | +		&cc.Initiator, "initiator", false, "whether our node was the "+ | 
|  | 81 | +			"initiator (funder) of the channel", | 
|  | 82 | +	) | 
|  | 83 | +	multiFileName := fmt.Sprintf("results/fake-%s.backup", | 
|  | 84 | +		time.Now().Format("2006-01-02-15-04-05")) | 
|  | 85 | +	cc.cmd.Flags().StringVar( | 
|  | 86 | +		&cc.MultiFile, "multi_file", multiFileName, "the fake channel "+ | 
|  | 87 | +			"backup file to create", | 
|  | 88 | +	) | 
|  | 89 | + | 
|  | 90 | +	cc.rootKey = newRootKey(cc.cmd, "encrypting the backup") | 
|  | 91 | + | 
|  | 92 | +	return cc.cmd | 
|  | 93 | +} | 
|  | 94 | + | 
|  | 95 | +func (c *fakeChanBackupCommand) Execute(_ *cobra.Command, _ []string) error { | 
|  | 96 | +	extendedKey, err := c.rootKey.read() | 
|  | 97 | +	if err != nil { | 
|  | 98 | +		return fmt.Errorf("error reading root key: %v", err) | 
|  | 99 | +	} | 
|  | 100 | + | 
|  | 101 | +	multiFile := chanbackup.NewMultiFile(c.MultiFile) | 
|  | 102 | +	keyRing := &lnd.HDKeyRing{ | 
|  | 103 | +		ExtendedKey: extendedKey, | 
|  | 104 | +		ChainParams: chainParams, | 
|  | 105 | +	} | 
|  | 106 | + | 
|  | 107 | +	// Parse channel point of channel to fake. | 
|  | 108 | +	chanOp, err := lnd.ParseOutpoint(c.ChannelPoint) | 
|  | 109 | +	if err != nil { | 
|  | 110 | +		return fmt.Errorf("error parsing channel point: %v", err) | 
|  | 111 | +	} | 
|  | 112 | + | 
|  | 113 | +	// Now parse the remote node info. | 
|  | 114 | +	splitNodeInfo := strings.Split(c.NodeAddr, "@") | 
|  | 115 | +	if len(splitNodeInfo) != 2 { | 
|  | 116 | +		return fmt.Errorf("--remote_node_addr expected in format: " + | 
|  | 117 | +			"pubkey@host:port") | 
|  | 118 | +	} | 
|  | 119 | +	pubKeyBytes, err := hex.DecodeString(splitNodeInfo[0]) | 
|  | 120 | +	if err != nil { | 
|  | 121 | +		return fmt.Errorf("could not parse pubkey hex string: %s", err) | 
|  | 122 | +	} | 
|  | 123 | +	nodePubkey, err := btcec.ParsePubKey(pubKeyBytes, btcec.S256()) | 
|  | 124 | +	if err != nil { | 
|  | 125 | +		return fmt.Errorf("could not parse pubkey: %s", err) | 
|  | 126 | +	} | 
|  | 127 | +	addr, err := net.ResolveTCPAddr("tcp", splitNodeInfo[1]) | 
|  | 128 | +	if err != nil { | 
|  | 129 | +		return fmt.Errorf("could not parse addr: %s", err) | 
|  | 130 | +	} | 
|  | 131 | + | 
|  | 132 | +	// Parse the short channel ID. | 
|  | 133 | +	splitChanId := strings.Split(c.ShortChanID, "x") | 
|  | 134 | +	if len(splitChanId) != 3 { | 
|  | 135 | +		return fmt.Errorf("--short_channel_id expected in format: " + | 
|  | 136 | +			"<blockheight>x<transactionindex>x<outputindex>", | 
|  | 137 | +		) | 
|  | 138 | +	} | 
|  | 139 | +	blockHeight, err := strconv.ParseInt(splitChanId[0], 10, 32) | 
|  | 140 | +	if err != nil { | 
|  | 141 | +		return fmt.Errorf("could not parse block height: %s", err) | 
|  | 142 | +	} | 
|  | 143 | +	txIndex, err := strconv.ParseInt(splitChanId[1], 10, 32) | 
|  | 144 | +	if err != nil { | 
|  | 145 | +		return fmt.Errorf("could not parse transaction index: %s", err) | 
|  | 146 | +	} | 
|  | 147 | +	chanOutputIdx, err := strconv.ParseInt(splitChanId[2], 10, 32) | 
|  | 148 | +	if err != nil { | 
|  | 149 | +		return fmt.Errorf("could not parse output index: %s", err) | 
|  | 150 | +	} | 
|  | 151 | +	shortChanID := lnwire.ShortChannelID{ | 
|  | 152 | +		BlockHeight: uint32(blockHeight), | 
|  | 153 | +		TxIndex:     uint32(txIndex), | 
|  | 154 | +		TxPosition:  uint16(chanOutputIdx), | 
|  | 155 | +	} | 
|  | 156 | + | 
|  | 157 | +	// Is the outpoint and/or short channel ID correct? | 
|  | 158 | +	if uint32(chanOutputIdx) != chanOp.Index { | 
|  | 159 | +		return fmt.Errorf("output index of --short_channel_id must " + | 
|  | 160 | +			"be equal to index on --channelpoint") | 
|  | 161 | +	} | 
|  | 162 | + | 
|  | 163 | +	// Create some fake channel config. | 
|  | 164 | +	chanCfg := channeldb.ChannelConfig{ | 
|  | 165 | +		ChannelConstraints: channeldb.ChannelConstraints{ | 
|  | 166 | +			DustLimit:        500, | 
|  | 167 | +			ChanReserve:      5000, | 
|  | 168 | +			MaxPendingAmount: 1, | 
|  | 169 | +			MinHTLC:          1, | 
|  | 170 | +			MaxAcceptedHtlcs: 200, | 
|  | 171 | +			CsvDelay:         144, | 
|  | 172 | +		}, | 
|  | 173 | +		MultiSigKey: keychain.KeyDescriptor{ | 
|  | 174 | +			PubKey: nodePubkey, | 
|  | 175 | +			KeyLocator: keychain.KeyLocator{ | 
|  | 176 | +				Family: keychain.KeyFamilyMultiSig, | 
|  | 177 | +				Index:  0, | 
|  | 178 | +			}, | 
|  | 179 | +		}, | 
|  | 180 | +		RevocationBasePoint: keychain.KeyDescriptor{ | 
|  | 181 | +			PubKey: nodePubkey, | 
|  | 182 | +			KeyLocator: keychain.KeyLocator{ | 
|  | 183 | +				Family: keychain.KeyFamilyRevocationBase, | 
|  | 184 | +				Index:  0, | 
|  | 185 | +			}, | 
|  | 186 | +		}, | 
|  | 187 | +		PaymentBasePoint: keychain.KeyDescriptor{ | 
|  | 188 | +			PubKey: nodePubkey, | 
|  | 189 | +			KeyLocator: keychain.KeyLocator{ | 
|  | 190 | +				Family: keychain.KeyFamilyPaymentBase, | 
|  | 191 | +				Index:  0, | 
|  | 192 | +			}, | 
|  | 193 | +		}, | 
|  | 194 | +		DelayBasePoint: keychain.KeyDescriptor{ | 
|  | 195 | +			PubKey: nodePubkey, | 
|  | 196 | +			KeyLocator: keychain.KeyLocator{ | 
|  | 197 | +				Family: keychain.KeyFamilyDelayBase, | 
|  | 198 | +				Index:  0, | 
|  | 199 | +			}, | 
|  | 200 | +		}, | 
|  | 201 | +		HtlcBasePoint: keychain.KeyDescriptor{ | 
|  | 202 | +			PubKey: nodePubkey, | 
|  | 203 | +			KeyLocator: keychain.KeyLocator{ | 
|  | 204 | +				Family: keychain.KeyFamilyHtlcBase, | 
|  | 205 | +				Index:  0, | 
|  | 206 | +			}, | 
|  | 207 | +		}, | 
|  | 208 | +	} | 
|  | 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()) | 
|  | 239 | +} | 
0 commit comments