cmd/lncli: add new exportchanbackup and restorechanbackup cli commands

In this commit, we add two new cli commands: exportchanbackup and
restorechanbackup. These two commands allow users to export backups
(single or multi) for one or all channels, and also restore these
backups (single or multi) from a file to attempt to recover the
channels.

Additionally, we extend the `lncli create` call to also accept these
backups so users can have a single command to restore both their
on-chain and off-chain funds.
This commit is contained in:
Olaoluwa Osuntokun 2018-12-09 20:16:11 -08:00
parent 1d7e42af0a
commit 0b8131c3be
No known key found for this signature in database
GPG Key ID: CE58F7F8E20FD9A2
2 changed files with 377 additions and 15 deletions

@ -17,6 +17,7 @@ import (
"syscall" "syscall"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/golang/protobuf/jsonpb" "github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
@ -1324,7 +1325,29 @@ var createCommand = cli.Command{
was provided by the user. This should be written down as it can be used was provided by the user. This should be written down as it can be used
to potentially recover all on-chain funds, and most off-chain funds as to potentially recover all on-chain funds, and most off-chain funds as
well. well.
Finally, it's also possible to use this command and a set of static
channel backups to trigger a recover attempt for the provided Static
Channel Backups. Only one of the three parameters will be accepted. See
the restorechanbackup command for further details w.r.t the format
accepted.
`, `,
Flags: []cli.Flag{
cli.StringFlag{
Name: "single_backup",
Usage: "a hex encoded single channel backup obtained " +
"from exportchanbackup",
},
cli.StringFlag{
Name: "multi_backup",
Usage: "a hex encoded multi-channel backup obtained " +
"from exportchanbackup",
},
cli.StringFlag{
Name: "multi_file",
Usage: "the path to a multi-channel back up file",
},
},
Action: actionDecorator(create), Action: actionDecorator(create),
} }
@ -1566,6 +1589,30 @@ mnemonicCheck:
fmt.Println("\n!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " + fmt.Println("\n!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
"RESTORE THE WALLET!!!") "RESTORE THE WALLET!!!")
// We'll also check to see if they provided any static channel backups,
// if so, then we'll also tack these onto the final innit wallet
// request.
var chanBackups *lnrpc.ChanBackupSnapshot
backups, err := parseChanBackups(ctx)
if err != nil {
return fmt.Errorf("unable to parse chan "+
"backups: %v", err)
}
if backups != nil {
switch {
case backups.GetChanBackups() != nil:
singleBackup := backups.GetChanBackups()
chanBackups.SingleChanBackups = singleBackup
case backups.GetMultiChanBackup() != nil:
multiBackup := backups.GetMultiChanBackup()
chanBackups.MultiChanBackup = &lnrpc.MultiChanBackup{
MultiChanBackup: multiBackup,
}
}
}
// With either the user's prior cipher seed, or a newly generated one, // With either the user's prior cipher seed, or a newly generated one,
// we'll go ahead and initialize the wallet. // we'll go ahead and initialize the wallet.
req := &lnrpc.InitWalletRequest{ req := &lnrpc.InitWalletRequest{
@ -1573,6 +1620,7 @@ mnemonicCheck:
CipherSeedMnemonic: cipherSeedMnemonic, CipherSeedMnemonic: cipherSeedMnemonic,
AezeedPassphrase: aezeedPass, AezeedPassphrase: aezeedPass,
RecoveryWindow: recoveryWindow, RecoveryWindow: recoveryWindow,
ChannelBackups: chanBackups,
} }
if _, err := client.InitWallet(ctxb, req); err != nil { if _, err := client.InitWallet(ctxb, req); err != nil {
return err return err
@ -1646,6 +1694,8 @@ func unlock(ctx *cli.Context) error {
fmt.Println("\nlnd successfully unlocked!") fmt.Println("\nlnd successfully unlocked!")
// TODO(roasbeef): add ability to accept hex single and multi backups
return nil return nil
} }
@ -3328,6 +3378,31 @@ var updateChannelPolicyCommand = cli.Command{
Action: actionDecorator(updateChannelPolicy), Action: actionDecorator(updateChannelPolicy),
} }
func parseChanPoint(s string) (*lnrpc.ChannelPoint, error) {
split := strings.Split(s, ":")
if len(split) != 2 {
return nil, fmt.Errorf("expecting chan_point to be in format of: " +
"txid:index")
}
index, err := strconv.ParseInt(split[1], 10, 32)
if err != nil {
return nil, fmt.Errorf("unable to decode output index: %v", err)
}
txid, err := chainhash.NewHashFromStr(split[0])
if err != nil {
return nil, fmt.Errorf("unable to parse hex string: %v", err)
}
return &lnrpc.ChannelPoint{
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
FundingTxidBytes: txid[:],
},
OutputIndex: uint32(index),
}, nil
}
func updateChannelPolicy(ctx *cli.Context) error { func updateChannelPolicy(ctx *cli.Context) error {
ctxb := context.Background() ctxb := context.Background()
client, cleanUp := getClient(ctx) client, cleanUp := getClient(ctx)
@ -3396,22 +3471,9 @@ func updateChannelPolicy(ctx *cli.Context) error {
} }
if chanPointStr != "" { if chanPointStr != "" {
split := strings.Split(chanPointStr, ":") chanPoint, err = parseChanPoint(chanPointStr)
if len(split) != 2 {
return fmt.Errorf("expecting chan_point to be in format of: " +
"txid:index")
}
index, err := strconv.ParseInt(split[1], 10, 32)
if err != nil { if err != nil {
return fmt.Errorf("unable to decode output index: %v", err) return fmt.Errorf("unable to parse chan point: %v", err)
}
chanPoint = &lnrpc.ChannelPoint{
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
FundingTxidStr: split[0],
},
OutputIndex: uint32(index),
} }
} }
@ -3554,3 +3616,301 @@ func forwardingHistory(ctx *cli.Context) error {
printRespJSON(resp) printRespJSON(resp)
return nil return nil
} }
var exportChanBackupCommand = cli.Command{
Name: "exportchanbackup",
Category: "Channels",
Usage: "Obtain a static channel back up for a selected channels, " +
"or all known channels",
ArgsUsage: "[chan_point] [--all] [--output_file]",
Description: `
This command allows a user to export a Static Channel Backup (SCB) for
as selected channel. SCB's are encrypted backups of a channel's initial
state that are encrypted with a key derived from the seed of a user.In
the case of partial or complete data loss, the SCB will allow the user
to reclaim settled funds in the channel at its final state. The
exported channel backups can be restored at a later time using the
restorechanbackup command.
This command will return one of two types of channel backups depending
on the set of passed arguments:
* If a target channel point is specified, then a single channel
backup containing only the information for that channel will be
returned.
* If the --all flag is passed, then a multi-channel backup will be
returned. A multi backup is a single encrypted blob (displayed in
hex encoding) that contains several channels in a single cipher
text.
Both of the backup types can be restored using the restorechanbackup
command.
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "chan_point",
Usage: "the target channel to obtain an SCB for",
},
cli.BoolFlag{
Name: "all",
Usage: "if specified, then a multi backup of all " +
"active channels will be returned",
},
cli.StringFlag{
Name: "output_file",
Usage: `
if specified, then rather than printing a JSON output
of the static channel backup, a serialized version of
the backup (either Single or Multi) will be written to
the target file, this is the same format used by lnd in
its channels.backup file `,
},
},
Action: actionDecorator(exportChanBackup),
}
func exportChanBackup(ctx *cli.Context) error {
ctxb := context.Background()
client, cleanUp := getClient(ctx)
defer cleanUp()
// Show command help if no arguments provided
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
cli.ShowCommandHelp(ctx, "exportchanbackup")
return nil
}
var (
err error
chanPointStr string
)
args := ctx.Args()
switch {
case ctx.IsSet("chan_point"):
chanPointStr = ctx.String("chan_point")
case args.Present():
chanPointStr = args.First()
case !ctx.IsSet("all"):
return fmt.Errorf("must specify chan_point if --all isn't set")
}
if chanPointStr != "" {
chanPointRPC, err := parseChanPoint(chanPointStr)
if err != nil {
return err
}
chanBackup, err := client.ExportChannelBackup(
ctxb, &lnrpc.ExportChannelBackupRequest{
ChanPoint: chanPointRPC,
},
)
if err != nil {
return err
}
txid, err := chainhash.NewHash(
chanPointRPC.GetFundingTxidBytes(),
)
if err != nil {
return err
}
chanPoint := wire.OutPoint{
Hash: *txid,
Index: chanPointRPC.OutputIndex,
}
printJSON(struct {
ChanPoint string `json:"chan_point"`
ChanBackup string `json:"chan_backup"`
}{
ChanPoint: chanPoint.String(),
ChanBackup: hex.EncodeToString(
chanBackup.ChanBackup,
),
},
)
return nil
}
if !ctx.IsSet("all") {
return fmt.Errorf("if a channel isn't specified, -all must be")
}
chanBackup, err := client.ExportAllChannelBackups(
ctxb, &lnrpc.ChanBackupExportRequest{},
)
if err != nil {
return err
}
if ctx.IsSet("output_file") {
return ioutil.WriteFile(
ctx.String("output_file"),
chanBackup.MultiChanBackup.MultiChanBackup,
0666,
)
}
// TODO(roasbeef): support for export | restore ?
var chanPoints []string
for _, chanPoint := range chanBackup.MultiChanBackup.ChanPoints {
txid, err := chainhash.NewHash(chanPoint.GetFundingTxidBytes())
if err != nil {
return err
}
chanPoints = append(chanPoints, wire.OutPoint{
Hash: *txid,
Index: chanPoint.OutputIndex,
}.String())
}
printJSON(struct {
ChanPoints []string `json:"chan_points"`
MultiChanBackup string `json:"multi_chan_backup"`
}{
ChanPoints: chanPoints,
MultiChanBackup: hex.EncodeToString(
chanBackup.MultiChanBackup.MultiChanBackup,
),
},
)
return nil
}
var restoreChanBackupCommand = cli.Command{
Name: "restorechanbackup",
Category: "Channels",
Usage: "Restore an existing single or multi-channel static channel " +
"backup",
ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file=",
Description: `
Allows a suer to restore a Static Channel Backup (SCB) that was
obtained either via the exportchanbackup command, or from lnd's
automatically manged channels.backup file. This command should be used
if a user is attempting to restore a channel due to data loss on a
running node restored with the same seed as the node that created the
channel. If successful, this command will allows the user to recover
the settled funds stored in the recovered channels.
The command will accept backups in one of three forms:
* A single channel packed SCB, which can be obtained from
exportchanbackup. This should be passed in hex encoded format.
* A packed multi-channel SCB, which couples several individual
static channel backups in single blob.
* A file path which points to a packed multi-channel backup within a
file, using the same format that lnd does in its channels.backup
file.
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "single_backup",
Usage: "a hex encoded single channel backup obtained " +
"from exportchanbackup",
},
cli.StringFlag{
Name: "multi_backup",
Usage: "a hex encoded multi-channel backup obtained " +
"from exportchanbackup",
},
cli.StringFlag{
Name: "multi_file",
Usage: "the path to a multi-channel back up file",
},
},
Action: actionDecorator(restoreChanBackup),
}
func parseChanBackups(ctx *cli.Context) (*lnrpc.RestoreChanBackupRequest, error) {
switch {
case ctx.IsSet("single_backup"):
packedBackup, err := hex.DecodeString(
ctx.String("single_backup"),
)
if err != nil {
return nil, fmt.Errorf("unable to decode single packed "+
"backup: %v", err)
}
return &lnrpc.RestoreChanBackupRequest{
Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
ChanBackups: &lnrpc.ChannelBackups{
ChanBackups: []*lnrpc.ChannelBackup{
{
ChanBackup: packedBackup,
},
},
},
},
}, nil
case ctx.IsSet("multi_backup"):
packedMulti, err := hex.DecodeString(
ctx.String("multi_backup"),
)
if err != nil {
return nil, fmt.Errorf("unable to decode multi packed "+
"backup: %v", err)
}
return &lnrpc.RestoreChanBackupRequest{
Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
MultiChanBackup: packedMulti,
},
}, nil
case ctx.IsSet("multi_file"):
packedMulti, err := ioutil.ReadFile(ctx.String("multi_file"))
if err != nil {
return nil, fmt.Errorf("unable to decode multi packed "+
"backup: %v", err)
}
return &lnrpc.RestoreChanBackupRequest{
Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
MultiChanBackup: packedMulti,
},
}, nil
default:
return nil, nil
}
}
func restoreChanBackup(ctx *cli.Context) error {
ctxb := context.Background()
client, cleanUp := getClient(ctx)
defer cleanUp()
// Show command help if no arguments provided
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
cli.ShowCommandHelp(ctx, "restorechanbackup")
return nil
}
var req lnrpc.RestoreChanBackupRequest
backups, err := parseChanBackups(ctx)
if err != nil {
return err
}
req.Backup = backups.Backup
_, err = client.RestoreChannelBackups(ctxb, &req)
if err != nil {
return fmt.Errorf("unable to restore chan backups: %v", err)
}
return nil
}

@ -295,6 +295,8 @@ func main() {
feeReportCommand, feeReportCommand,
updateChannelPolicyCommand, updateChannelPolicyCommand,
forwardingHistoryCommand, forwardingHistoryCommand,
exportChanBackupCommand,
restoreChanBackupCommand,
} }
// Add any extra autopilot commands determined by build flags. // Add any extra autopilot commands determined by build flags.