Skip to content

Commit

Permalink
feat(rpc): RestoreNode call to restore from seed
Browse files Browse the repository at this point in the history
This adds a new `RestoreNode` call that accepts a 24 word seed mnemonic
to restore an xud key, lnd wallets, and a raiden keystore from the seed.
This call can be extended to also restore lnd channel backups and xud
db backups with additional parameters.

The seedutil tool is extended to support an `encipher` subcommand that
converts a mnemonic to the enciphered seed bytes, which is necessary
for deriving an xud key from aezeed.

Closes #1017. Closes #1020.
  • Loading branch information
sangaman committed Dec 12, 2019
1 parent e4ab35e commit 4357adb
Show file tree
Hide file tree
Showing 21 changed files with 948 additions and 247 deletions.
35 changes: 35 additions & 0 deletions docs/api.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions lib/Xud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,7 @@ class Xud extends EventEmitter {
const initService = new InitService(this.swapClientManager, nodeKeyPath, nodeKeyExists);

this.rpcServer.grpcInitService.setInitService(initService);
this.logger.info("Node key is encrypted, unlock using 'xucli unlock' or set password using" +
" 'xucli create' if this is the first time starting xud");
this.logger.info("Node key is encrypted, unlock with 'xucli unlock', 'xucli create', or 'xucli restore'");
nodeKey = await new Promise<NodeKey | undefined>((resolve) => {
initService.once('nodekey', resolve);
this.on('shutdown', () => {
Expand Down
36 changes: 2 additions & 34 deletions lib/cli/commands/create.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,15 @@
import { accessSync, watch } from 'fs';
import path from 'path';
import readline from 'readline';
import { Arguments } from 'yargs';
import { CreateNodeRequest, CreateNodeResponse } from '../../proto/xudrpc_pb';
import { callback, loadXudInitClient } from '../command';
import { getDefaultCertPath } from '../utils';
import { getDefaultCertPath, waitForCert } from '../utils';

export const command = 'create';

export const describe = 'use this to create a new xud instance and set a password';
export const describe = 'create a new xud instance and set a password';

export const builder = {};

const waitForCert = (certPath: string) => {
return new Promise<void>((resolve, reject) => {
try {
accessSync(certPath);
resolve();
} catch (err) {
if (err.code === 'ENOENT') {
// wait up to 5 seconds for the tls.cert file to be created in case
// this is the first time xud has been run
const certDir = path.dirname(certPath);
const certFilename = path.basename(certPath);
const fsWatcher = watch(certDir, (event, filename) => {
if (event === 'change' && filename === certFilename) {
clearTimeout(timeout);
fsWatcher.close();
resolve();
}
});
const timeout = setTimeout(() => {
fsWatcher.close();
reject(`timed out waiting for cert to be created at ${certPath}`);
}, 5000);
} else {
// we handle errors due to file not existing, otherwise reject
reject(err);
}
}
});
};

const formatOutput = (response: CreateNodeResponse.AsObject) => {
if (response.seedMnemonicList.length === 24) {
const WORDS_PER_ROW = 4;
Expand Down
86 changes: 86 additions & 0 deletions lib/cli/commands/restore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import readline from 'readline';
import { Arguments } from 'yargs';
import { RestoreNodeRequest, RestoreNodeResponse } from '../../proto/xudrpc_pb';
import { callback, loadXudInitClient } from '../command';
import { getDefaultCertPath, waitForCert } from '../utils';

export const command = 'restore';

export const describe = 'restore an xud instance from seed';

export const builder = {};

const formatOutput = (response: RestoreNodeResponse.AsObject) => {
let walletRestoredMessage = 'The following wallets were restored: ';

if (response.restoredLndsList.length) {
walletRestoredMessage += response.restoredLndsList.join(', ');
}

if (response.restoredRaiden) {
if (!walletRestoredMessage.endsWith(' ')) {
walletRestoredMessage += ', ';
}

walletRestoredMessage += 'ERC20(ETH)';
}

console.log(walletRestoredMessage);
};

export const handler = (argv: Arguments<any>) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: true,
});

console.log(`
You are restoring an xud node key and underlying wallets. All will be secured by
a single password provided below.
`);
rl.question('Enter your 24 word mnemonic separated by spaces: ', (mnemonicStr) => {
rl.close();
const rlQuiet = readline.createInterface({
input: process.stdin,
terminal: true,
});
const mnemonic = mnemonicStr.split(' ');
if (mnemonic.length !== 24) {
console.error('Mnemonic must be exactly 24 words');
process.exitCode = 1;
return;
}
process.stdout.write('Enter a password: ');
rlQuiet.question('', (password1) => {
process.stdout.write('\nRe-enter password: ');
rlQuiet.question('', async (password2) => {
process.stdout.write('\n\n');
rlQuiet.close();
if (password1 === password2) {
const request = new RestoreNodeRequest();
request.setPassword(password1);
request.setSeedMnemonicList(mnemonic);

const certPath = argv.tlscertpath ? argv.tlscertpath : getDefaultCertPath();
try {
await waitForCert(certPath);
} catch (err) {
console.error(err);
process.exitCode = 1;
return;
}

const client = loadXudInitClient(argv);
// wait up to 3 seconds for rpc server to listen before call in case xud was just started
client.waitForReady(Date.now() + 3000, () => {
client.restoreNode(request, callback(argv, formatOutput));
});
} else {
process.exitCode = 1;
console.error('Passwords do not match, please try again');
}
});
});
});
};
30 changes: 30 additions & 0 deletions lib/cli/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import colors from 'colors/safe';
import { accessSync, watch } from 'fs';
import os from 'os';
import path from 'path';

Expand Down Expand Up @@ -36,3 +37,32 @@ export const coinsToSats = (coinsQuantity: number) => {
export const satsToCoinsStr = (satsQuantity: number) => {
return (satsQuantity / SATOSHIS_PER_COIN).toFixed(8).replace(/\.?0+$/, '');
};

/** Waits up to 5 seconds for the tls.cert file to be created in case this is the first time xud has been run. */
export const waitForCert = (certPath: string) => {
return new Promise<void>((resolve, reject) => {
try {
accessSync(certPath);
resolve();
} catch (err) {
if (err.code === 'ENOENT') {
const certDir = path.dirname(certPath);
const certFilename = path.basename(certPath);
const fsWatcher = watch(certDir, (event, filename) => {
if (event === 'change' && filename === certFilename) {
clearTimeout(timeout);
fsWatcher.close();
resolve();
}
});
const timeout = setTimeout(() => {
fsWatcher.close();
reject(`timed out waiting for cert to be created at ${certPath}`);
}, 5000);
} else {
// we handle errors due to file not existing, otherwise reject
reject(err);
}
}
});
};
27 changes: 21 additions & 6 deletions lib/grpc/GrpcInitService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* tslint:disable no-null-keyword */
import grpc, { status } from 'grpc';
import InitService from 'lib/service/InitService';
import InitService from '../service/InitService';
import * as xudrpc from '../proto/xudrpc_pb';
import getGrpcError from './getGrpcError';

Expand Down Expand Up @@ -44,16 +44,13 @@ class GrpcInitService {
if (mnemonic) {
response.setSeedMnemonicList(mnemonic);
}
if (initializedLndWallets) {
response.setInitializedLndsList(initializedLndWallets);
}
response.setInitializedLndsList(initializedLndWallets);
response.setInitializedRaiden(initializedRaiden);

callback(null, response);
} catch (err) {
callback(getGrpcError(err), null);
}
this.initService.pendingCall = false;
}

/**
Expand All @@ -73,7 +70,25 @@ class GrpcInitService {
} catch (err) {
callback(getGrpcError(err), null);
}
this.initService.pendingCall = false;
}

/**
* See [[InitService.restoreNode]]
*/
public restoreNode: grpc.handleUnaryCall<xudrpc.RestoreNodeRequest, xudrpc.RestoreNodeResponse> = async (call, callback) => {
if (!this.isReady(this.initService, callback)) {
return;
}
try {
const { restoredLndWallets, restoredRaiden } = await this.initService.restoreNode(call.request.toObject());
const response = new xudrpc.RestoreNodeResponse();
response.setRestoredLndsList(restoredLndWallets);
response.setRestoredRaiden(restoredRaiden);

callback(null, response);
} catch (err) {
callback(getGrpcError(err), null);
}
}
}

Expand Down
17 changes: 17 additions & 0 deletions lib/proto/xudrpc.swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions lib/proto/xudrpc_grpc_pb.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 4357adb

Please sign in to comment.