Skip to content

Commit

Permalink
feat: add backup daemon
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Oct 7, 2019
1 parent aaa9568 commit 9f75a56
Show file tree
Hide file tree
Showing 5 changed files with 457 additions and 0 deletions.
74 changes: 74 additions & 0 deletions bin/xu-backup
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env node

const Backup = require('../dist/backup/Backup').default;

const { argv } = require('yargs')
.options({
backupdir: {
describe: 'Data directory for backups',
type: 'string',
alias: 'b',
},
dbpath: {
describe: 'Path to the XUD database',
type: 'string',
alias: 'x',
},
loglevel: {
describe: 'Verbosity of the logger',
type: 'string',
alias: 'l',
},
logpath: {
describe: 'Path to the log file',
type: 'string',
},
logdateformat: {
describe: 'Format of the logger date',
type: 'string',
},
'lnd.[currency].certpath': {
describe: 'Path to the SSL certificate for lnd',
type: 'string',
},
'lnd.[currency].cltvdelta': {
describe: 'CLTV delta for the final timelock',
type: 'number',
},
'lnd.[currency].disable': {
describe: 'Disable lnd integration',
type: 'boolean',
default: undefined,
},
'lnd.[currency].host': {
describe: 'Host of the lnd gRPC interface',
type: 'string',
},
'lnd.[currency].macaroonpath': {
describe: 'Path of the admin macaroon for lnd',
type: 'string',
},
'lnd.[currency].nomacaroons': {
describe: 'Whether to disable macaroons for lnd',
type: 'boolean',
default: undefined,
},
'lnd.[currency].port': {
describe: 'Port for the lnd gRPC interface',
type: 'number',
},
'raiden.database': {
describe: 'Path to the database of Raiden',
type: 'string',
},
});

// delete non-config keys from argv
delete argv._;
delete argv.version;
delete argv.help;
delete argv.$0;

const backup = new Backup();

backup.start(argv);
1 change: 1 addition & 0 deletions lib/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export enum Context {
Raiden = 'RAIDEN',
Swaps = 'SWAPS',
Http = 'HTTP',
Backup = 'BACKUP',
}

type Loggers = {
Expand Down
179 changes: 179 additions & 0 deletions lib/backup/Backup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { createHash } from 'crypto';
import Config from '../Config';
import Logger, { Context } from '../Logger';
import LndClient from '../lndclient/LndClient';

class Backup {
private logger!: Logger;
private config = new Config();

private backupDir!: string;

private fileWatchers: fs.FSWatcher[] = [];
private lndClients: LndClient[] = [];

public start = async (args: { [argName: string]: any }) => {
await this.config.load(args);

this.backupDir = args.backupdir || this.getDefaultBackupDir();

this.logger = new Logger({
context: Context.Backup,
level: this.config.loglevel,
filename: this.config.logpath,
instanceId: this.config.instanceid,
dateFormat: this.config.logdateformat,
});

if (!fs.existsSync(this.backupDir)) {
try {
fs.mkdirSync(this.backupDir);
} catch (error) {
this.logger.error(`Could not create backup directory: ${error}`);
return;
}
}

try {
await this.startLndSubscriptions();
} catch (error) {
this.logger.error(`Could not connect to LNDs: ${error}`);
}

// Start the Raiden database filewatcher
if (args.raiden) {
this.startFilewatcher('raiden', args.raiden.database);
} else {
this.logFileNotSpecified('raiden');
}

// Start the XUD database filewatcher
if (args.raiden) {
this.startFilewatcher('xud', args.dbpath);
} else {
this.logFileNotSpecified('xud');
}

this.logger.info('Started backup daemon');
}

public stop = async () => {
this.fileWatchers.forEach((watcher) => {
watcher.close();
});

for (const lndClient of this.lndClients) {
await lndClient.close();
}
}

private startLndSubscriptions = async () => {
// Start the LND SCB subscriptions
for (const currency in this.config.lnd) {
const config = this.config.lnd[currency]!;

// Ignore the LND client if it is disabled or not configured
if (!config.disable && Object.entries(config).length !== 0) {
const lndClient = new LndClient({
config,
currency,
logger: Logger.DISABLED_LOGGER,
});

this.lndClients.push(lndClient);

await lndClient.init();

this.logger.verbose(`Writing initial ${lndClient.currency} LND channel backup`);

const channelBackup = await lndClient.exportAllChannelBackup();
this.writeLndBackup(lndClient.currency, channelBackup);

this.listenToChannelBackups(lndClient);

lndClient.subscribeChannelBackups();
this.logger.verbose(`Listening to ${currency} LND channel backups`);
}
}
}

private listenToChannelBackups = (lndClient: LndClient) => {
lndClient.on('channelBackup', (channelBackup) => {
this.logger.debug(`New ${lndClient.currency} channel backup`);
this.writeLndBackup(lndClient.currency, channelBackup);
});
}

private startFilewatcher = (client: string, dbPath: string) => {
if (fs.existsSync(dbPath)) {
let previousDatabaseHash = '';

this.logger.verbose(`Writing initial ${client} database backup`);
const { content, hash } = this.readDatabase(dbPath);

previousDatabaseHash = hash;
this.writeBackup(client, content);

this.fileWatchers.push(fs.watch(dbPath, { persistent: true, recursive: false }, (event: string) => {
if (event === 'change') {
const { content, hash } = this.readDatabase(dbPath);

// Compare the MD5 hash of the current content of the file with hash of the content when
// it was backed up the last time to ensure that the content of the file has changed
if (hash !== previousDatabaseHash) {
this.logger.debug(`${client} database changed`);

previousDatabaseHash = hash;
this.writeBackup(client, content);
}
}
}));

this.logger.verbose(`Listening for changes to the ${client} database`);
} else {
this.logger.error(`Could not find database file of ${client}: ${dbPath}`);
}
}

private readDatabase = (path: string): { content: string, hash: string } => {
const content = fs.readFileSync(path);

return {
content: content.toString('utf-8'),
hash: createHash('md5').update(content).digest('base64'),
};
}

private writeLndBackup = (currency: string, channelBackup: string) => {
this.writeBackup(`lnd-${currency}`, channelBackup);
}

private writeBackup = (fileName: string, data: string) => {
try {
fs.writeFileSync(
path.join(this.backupDir, fileName),
data,
);
} catch (error) {
this.logger.error(`Could not write backup file: ${error}`);
}
}

private getDefaultBackupDir = () => {
switch (os.platform()) {
case 'win32':
return path.join(process.env.LOCALAPPDATA!, 'Xud Backup');
default:
return path.join(process.env.HOME!, '.xud-backup');
}
}

private logFileNotSpecified = (client: string) => {
this.logger.warn(`${client} database file not specified`);
}
}

export default Backup;
30 changes: 30 additions & 0 deletions lib/lndclient/LndClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ import path from 'path';
interface LndClient {
on(event: 'connectionVerified', listener: (swapClientInfo: SwapClientInfo) => void): this;
on(event: 'htlcAccepted', listener: (rHash: string, amount: number) => void): this;
on(event: 'channelBackup', listener: (channelBackup: string) => void): this;
on(event: 'locked', listener: () => void): this;

emit(event: 'connectionVerified', swapClientInfo: SwapClientInfo): boolean;
emit(event: 'htlcAccepted', rHash: string, amount: number): boolean;
emit(event: 'channelBackup', channelBackup: string): boolean;
emit(event: 'locked'): boolean;
}

Expand All @@ -45,6 +48,7 @@ class LndClient extends SwapClient {
private urisList?: string[];
/** The identifier for the chain this lnd instance is using in the format [chain]-[network] like "bitcoin-testnet" */
private chainIdentifier?: string;
private channelBackupSubscription?: ClientReadableStream<lndrpc.ChanBackupSnapshot>;
private invoiceSubscriptions = new Map<string, ClientReadableStream<lndrpc.Invoice>>();
private maximumOutboundAmount = 0;
private initWalletResolve?: (value: boolean) => void;
Expand Down Expand Up @@ -784,6 +788,13 @@ class LndClient extends SwapClient {
return this.unaryCall<lndrpc.ListPaymentsRequest, lndrpc.ListPaymentsResponse>('listPayments', request);
}

public exportAllChannelBackup = async () => {
const request = new lndrpc.ChanBackupExportRequest();
const response = await this.unaryCall<lndrpc.ChanBackupExportRequest, lndrpc.ChanBackupSnapshot>('exportAllChannelBackups', request);

return response.getMultiChanBackup()!.getMultiChanBackup_asB64();
}

private addHoldInvoice = (request: lndinvoices.AddHoldInvoiceRequest): Promise<lndinvoices.AddHoldInvoiceResp> => {
return this.unaryInvoiceCall<lndinvoices.AddHoldInvoiceRequest, lndinvoices.AddHoldInvoiceResp>('addHoldInvoice', request);
}
Expand Down Expand Up @@ -817,6 +828,25 @@ class LndClient extends SwapClient {
this.invoiceSubscriptions.set(rHash, invoiceSubscription);
}

/**
* Subscribes to channel backups
*/
public subscribeChannelBackups = () => {
if (!this.lightning) {
throw errors.UNAVAILABLE(this.currency, this.status);
}

if (this.channelBackupSubscription) {
this.channelBackupSubscription.cancel();
}

this.channelBackupSubscription = this.lightning.subscribeChannelBackups(new lndrpc.ChannelBackupSubscription(), this.meta)
.on('data', (backupSnapshot: lndrpc.ChanBackupSnapshot) => {
const multiBackup = backupSnapshot.getMultiChanBackup()!;
this.emit('channelBackup', multiBackup.getMultiChanBackup_asB64().toString());
});
}

/**
* Attempts to close an open channel.
*/
Expand Down
Loading

0 comments on commit 9f75a56

Please sign in to comment.