Skip to content

Commit 6ae653d

Browse files
committed
fix(lnd): wait for macaroon before wallet created
This fixes the logic around initializing `LndClient` for a new lnd instance (before wallet creation) and new xud instance (before node key creation). The lnd macaroon file is not created until after its wallet is created, so we relax the check that disables the client if a macaroon is not found during initialization. Instead, we mark the client as `WaitingUnlock` and wait for a successful xud `CreateNode` call to initialize the lnd wallet. After we've successfully loaded the admin macaroon, we then continue with attempting to verify the connection.
1 parent cdf23aa commit 6ae653d

File tree

5 files changed

+158
-60
lines changed

5 files changed

+158
-60
lines changed

lib/Xud.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,22 @@ class Xud extends EventEmitter {
7272
this.unitConverter = new UnitConverter();
7373
this.unitConverter.init();
7474

75+
const nodeKeyPath = NodeKey.getPath(this.config.xudir, this.config.instanceid);
76+
const nodeKeyExists = await fs.access(nodeKeyPath).then(() => true).catch(() => false);
77+
const awaitingCreate = !nodeKeyExists && !this.config.noencrypt;
78+
7579
this.swapClientManager = new SwapClientManager(this.config, loggers, this.unitConverter);
76-
await this.swapClientManager.init(this.db.models);
80+
await this.swapClientManager.init(this.db.models, awaitingCreate);
7781

78-
const nodeKeyPath = NodeKey.getPath(this.config.xudir, this.config.instanceid);
7982
let nodeKey: NodeKey | undefined;
8083
if (this.config.noencrypt) {
81-
nodeKey = await NodeKey.load(nodeKeyPath);
84+
if (nodeKeyExists) {
85+
nodeKey = await NodeKey.fromFile(nodeKeyPath);
86+
} else {
87+
nodeKey = await NodeKey.generate();
88+
await nodeKey.toFile(nodeKeyPath);
89+
}
8290
} else {
83-
const nodeKeyExists = await fs.access(nodeKeyPath).then(() => true).catch(() => false);
8491
const initService = new InitService(this.swapClientManager, nodeKeyPath, nodeKeyExists);
8592

8693
const initRpcServer = new GrpcServer(loggers.rpc);

lib/lndclient/LndClient.ts

Lines changed: 135 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import { InvoicesClient } from '../proto/lndinvoices_grpc_pb';
88
import * as lndrpc from '../proto/lndrpc_pb';
99
import * as lndinvoices from '../proto/lndinvoices_pb';
1010
import assert from 'assert';
11-
import { promises as fs } from 'fs';
11+
import { promises as fs, watch } from 'fs';
1212
import { SwapState, SwapRole, SwapClientType } from '../constants/enums';
1313
import { SwapDeal } from '../swaps/types';
1414
import { base64ToHex, hexToUint8Array } from '../utils/utils';
1515
import { LndClientConfig, LndInfo, ChannelCount, Chain } from './types';
16+
import path from 'path';
1617

1718
interface LightningMethodIndex extends LightningClient {
1819
[methodName: string]: Function;
@@ -34,6 +35,7 @@ interface LndClient {
3435
}
3536

3637
const MAXFEE = 0.03;
38+
3739
/** A class representing a client to interact with lnd. */
3840
class LndClient extends SwapClient {
3941
public readonly type = SwapClientType.Lnd;
@@ -44,7 +46,8 @@ class LndClient extends SwapClient {
4446
private lightning?: LightningClient | LightningMethodIndex;
4547
private walletUnlocker?: WalletUnlockerClient | InvoicesMethodIndex;
4648
private invoices?: InvoicesClient | InvoicesMethodIndex;
47-
private meta!: grpc.Metadata;
49+
private macaroonpath?: string;
50+
private meta = new grpc.Metadata();
4851
private uri!: string;
4952
private credentials!: ChannelCredentials;
5053
/** The identity pub key for this lnd instance. */
@@ -56,6 +59,8 @@ class LndClient extends SwapClient {
5659
private channelSubscription?: ClientReadableStream<lndrpc.ChannelEventUpdate>;
5760
private invoiceSubscriptions = new Map<string, ClientReadableStream<lndrpc.Invoice>>();
5861
private maximumOutboundAmount = 0;
62+
private initWalletResolve?: (value: boolean) => void;
63+
private watchMacaroonResolve?: (value: boolean) => void;
5964

6065
private static MINUTES_PER_BLOCK_BY_CURRENCY: { [key: string]: number } = {
6166
BTC: 10,
@@ -77,12 +82,27 @@ class LndClient extends SwapClient {
7782
this.finalLock = Math.round(400 / LndClient.MINUTES_PER_BLOCK_BY_CURRENCY[currency]);
7883
}
7984

85+
private static waitForClientReady = (client: grpc.Client) => {
86+
return new Promise((resolve, reject) => {
87+
client.waitForReady(Number.POSITIVE_INFINITY, (err) => {
88+
if (err) {
89+
reject(err);
90+
} else {
91+
resolve();
92+
}
93+
});
94+
});
95+
}
96+
8097
public get minutesPerBlock() {
8198
return LndClient.MINUTES_PER_BLOCK_BY_CURRENCY[this.currency];
8299
}
83100

84-
/** Initializes the client for calls to lnd and verifies that we can connect to it. */
85-
public init = async () => {
101+
/**
102+
* Initializes the client for calls to lnd and verifies that we can connect to it.
103+
* @param awaitingCreate whether xud is waiting for its node key to be created
104+
*/
105+
public init = async (awaitingCreate = false) => {
86106
assert(this.lockBuffer > 0, `lnd-${this.currency}: lock buffer must be a positive number`);
87107

88108
const { disable, certpath, macaroonpath, nomacaroons, host, port } = this.config;
@@ -94,24 +114,28 @@ class LndClient extends SwapClient {
94114
try {
95115
const lndCert = await fs.readFile(certpath);
96116
this.credentials = grpc.credentials.createSsl(lndCert);
117+
this.logger.debug(`loaded tls cert from ${certpath}`);
97118
} catch (err) {
98-
this.logger.error('could not load lnd certificate, is lnd installed?');
119+
this.logger.error(`could not load tls cert from ${certpath}, is lnd installed?`);
99120
await this.setStatus(ClientStatus.Disabled);
100121
return;
101122
}
102123

103-
this.meta = new grpc.Metadata();
104124
if (!nomacaroons) {
125+
this.macaroonpath = macaroonpath;
105126
try {
106-
const adminMacaroon = await fs.readFile(macaroonpath);
107-
this.meta.add('macaroon', adminMacaroon.toString('hex'));
127+
await this.loadMacaroon();
108128
} catch (err) {
109-
this.logger.error('could not load lnd macaroon, is lnd installed?');
110-
await this.setStatus(ClientStatus.Disabled);
111-
return;
129+
if (!awaitingCreate) {
130+
// unless we are waiting for the xud nodekey and lnd wallet to be created
131+
// we expect the macaroon to exist and disable this client otherwise
132+
this.logger.error(`expected macaroon not found at ${macaroonpath}`);
133+
await this.setStatus(ClientStatus.Disabled);
134+
return;
135+
}
112136
}
113137
} else {
114-
this.logger.info('macaroons are disabled for lnd');
138+
this.logger.info('macaroons are disabled');
115139
}
116140

117141
this.uri = `${host}:${port}`;
@@ -159,6 +183,14 @@ class LndClient extends SwapClient {
159183
});
160184
}
161185

186+
private loadMacaroon = async () => {
187+
if (this.macaroonpath) {
188+
const adminMacaroon = await fs.readFile(this.macaroonpath);
189+
this.meta.add('macaroon', adminMacaroon.toString('hex'));
190+
this.logger.debug(`loaded macaroon from ${this.macaroonpath}`);
191+
}
192+
}
193+
162194
private unaryInvoiceCall = <T, U>(methodName: string, params: T): Promise<U> => {
163195
return new Promise((resolve, reject) => {
164196
if (this.isDisabled()) {
@@ -232,27 +264,87 @@ class LndClient extends SwapClient {
232264
};
233265
}
234266

267+
/**
268+
* Waits for the lnd wallet to be initialized and for its macaroons to be created then attempts
269+
* to verify the connection to lnd.
270+
*/
271+
private awaitWalletInit = async () => {
272+
// we are waiting for lnd to be initialized by xud and for the lnd macaroons to be created
273+
this.logger.info('waiting for wallet to be initialized...');
274+
275+
const isWalletInitialized = await new Promise<boolean>((resolve) => {
276+
this.initWalletResolve = resolve;
277+
});
278+
279+
if (isWalletInitialized) {
280+
if (this.walletUnlocker) {
281+
this.walletUnlocker.close();
282+
this.walletUnlocker = undefined;
283+
}
284+
285+
// admin.macaroon will not necessarily be created by the time lnd responds to a successful
286+
// InitWallet call, so we watch the folder that we expect it to be in for it to be created
287+
const watchMacaroonPromise = new Promise<boolean>((resolve) => {
288+
this.watchMacaroonResolve = resolve;
289+
});
290+
const macaroonDir = path.join(this.macaroonpath!, '..');
291+
const fsWatcher = watch(macaroonDir, (_, filename) => {
292+
if (filename === 'admin.macaroon') {
293+
this.logger.debug('admin.macaroon was created');
294+
if (this.watchMacaroonResolve) {
295+
this.watchMacaroonResolve(true);
296+
}
297+
}
298+
});
299+
this.logger.debug(`watching ${macaroonDir} for admin.macaroon to be created`);
300+
const macaroonCreated = await watchMacaroonPromise;
301+
fsWatcher.close();
302+
this.watchMacaroonResolve = undefined;
303+
304+
if (macaroonCreated) {
305+
try {
306+
await this.loadMacaroon();
307+
308+
// once we've loaded the macaroon we can attempt to verify the conneciton
309+
this.verifyConnection().catch(this.logger.error);
310+
} catch (err) {
311+
this.logger.error(`could not load macaroon from ${this.macaroonpath}`);
312+
await this.setStatus(ClientStatus.Disabled);
313+
}
314+
}
315+
}
316+
}
317+
235318
protected verifyConnection = async () => {
236319
if (this.isDisabled()) {
237320
throw(errors.LND_IS_DISABLED);
238321
}
239322

323+
if (this.macaroonpath && this.meta.get('macaroon').length === 0) {
324+
// we have not loaded the macaroon yet - it is not created until the lnd wallet is initialized
325+
if (!this.isWaitingUnlock()) { // check that we are not already waiting for wallet init & unlock
326+
this.walletUnlocker = new WalletUnlockerClient(this.uri, this.credentials);
327+
await LndClient.waitForClientReady(this.walletUnlocker);
328+
await this.setStatus(ClientStatus.WaitingUnlock);
329+
330+
if (this.reconnectionTimer) {
331+
// we don't need scheduled attempts to retry the connection while waiting on the wallet
332+
clearTimeout(this.reconnectionTimer);
333+
this.reconnectionTimer = undefined;
334+
}
335+
336+
this.awaitWalletInit().catch(this.logger.error);
337+
}
338+
return;
339+
}
340+
240341
if (!this.isConnected()) {
241342
this.logger.info(`trying to verify connection to lnd at ${this.uri}`);
242-
const lightningClient = new LightningClient(this.uri, this.credentials);
243-
const clientReadyPromise = new Promise((resolve, reject) => {
244-
lightningClient.waitForReady(Number.POSITIVE_INFINITY, (err) => {
245-
if (err) {
246-
reject(err);
247-
} else {
248-
resolve();
249-
}
250-
});
251-
});
343+
this.lightning = new LightningClient(this.uri, this.credentials);
252344

253-
this.lightning = lightningClient;
254345
try {
255-
await clientReadyPromise;
346+
await LndClient.waitForClientReady(this.lightning);
347+
256348
const getInfoResponse = await this.getInfo();
257349
if (getInfoResponse.getSyncedToChain()) {
258350
// mark connection as active
@@ -585,19 +677,22 @@ class LndClient extends SwapClient {
585677
public initWallet = async (walletPassword: string, seedMnemonic: string[]): Promise<lndrpc.InitWalletResponse.AsObject> => {
586678
const request = new lndrpc.InitWalletRequest();
587679
request.setCipherSeedMnemonicList(seedMnemonic);
588-
request.setWalletPassword(walletPassword);
680+
request.setWalletPassword(Uint8Array.from(Buffer.from(walletPassword, 'utf8')));
589681
const initWalletResponse = await this.unaryWalletUnlockerCall<lndrpc.InitWalletRequest, lndrpc.InitWalletResponse>(
590-
'initWallet', new lndrpc.InitWalletRequest(),
682+
'initWallet', request,
591683
);
684+
if (this.initWalletResolve) {
685+
this.initWalletResolve(true);
686+
}
592687
this.logger.info('wallet initialized');
593688
return initWalletResponse.toObject();
594689
}
595690

596691
public unlockWallet = async (walletPassword: string): Promise<lndrpc.UnlockWalletResponse.AsObject> => {
597692
const request = new lndrpc.UnlockWalletRequest();
598-
request.setWalletPassword(walletPassword);
693+
request.setWalletPassword(Uint8Array.from(Buffer.from(walletPassword, 'utf8')));
599694
const unlockWalletResponse = await this.unaryWalletUnlockerCall<lndrpc.UnlockWalletRequest, lndrpc.UnlockWalletResponse>(
600-
'unlockWallet', new lndrpc.UnlockWalletRequest(),
695+
'unlockWallet', request,
601696
);
602697
this.logger.info('wallet unlocked');
603698
return unlockWalletResponse.toObject();
@@ -730,7 +825,18 @@ class LndClient extends SwapClient {
730825
this.invoices.close();
731826
this.invoices = undefined;
732827
}
733-
await this.setStatus(ClientStatus.Disconnected);
828+
if (this.initWalletResolve) {
829+
this.initWalletResolve(false);
830+
this.initWalletResolve = undefined;
831+
}
832+
if (this.watchMacaroonResolve) {
833+
this.watchMacaroonResolve(false);
834+
this.watchMacaroonResolve = undefined;
835+
}
836+
837+
if (!this.isDisabled()) {
838+
await this.setStatus(ClientStatus.Disconnected);
839+
}
734840
}
735841
}
736842

lib/nodekey/NodeKey.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -64,25 +64,6 @@ class NodeKey {
6464
}
6565
}
6666

67-
/**
68-
* Loads a node key from a file or creates one if none exists. See [[fromFile]] and [[generate]].
69-
*/
70-
public static load = async (path: string): Promise<NodeKey> => {
71-
let nodeKey: NodeKey;
72-
try {
73-
nodeKey = await NodeKey.fromFile(path);
74-
} catch (err) {
75-
if (err.code === 'ENOENT') {
76-
// node key file does not exist, so create one
77-
nodeKey = await NodeKey.generate();
78-
await nodeKey.toFile(path);
79-
} else {
80-
throw err;
81-
}
82-
}
83-
return nodeKey;
84-
}
85-
8667
public static getPath = (xudir: string, instanceId = 0) => {
8768
return instanceId > 0
8869
? `${xudir}/nodekey_${instanceId}.dat`

lib/service/InitService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ class InitService extends EventEmitter {
1919

2020
public createNode = async (args: { password: string }) => {
2121
const { password } = args;
22+
if (password.length < 8) {
23+
// lnd requires 8+ character passwords, so we must as well
24+
throw errors.INVALID_ARGUMENT('password must be at least 8 characters');
25+
}
2226
if (this.nodeKeyExists) {
2327
throw errors.UNIMPLEMENTED;
2428
}

lib/swaps/SwapClientManager.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class SwapClientManager extends EventEmitter {
6262
* and waits for the swap clients to initialize.
6363
* @returns A promise that resolves upon successful initialization, rejects otherwise.
6464
*/
65-
public init = async (models: Models): Promise<void> => {
65+
public init = async (models: Models, awaitingCreate = false): Promise<void> => {
6666
const initPromises = [];
6767
// setup configured LND clients and initialize them
6868
for (const currency in this.config.lnd) {
@@ -75,7 +75,7 @@ class SwapClientManager extends EventEmitter {
7575
logger: this.loggers.lnd.createSubLogger(currency),
7676
});
7777
this.swapClients.set(currency, lndClient);
78-
initPromises.push(lndClient.init());
78+
initPromises.push(lndClient.init(awaitingCreate));
7979
}
8080
}
8181
// setup Raiden
@@ -109,9 +109,9 @@ class SwapClientManager extends EventEmitter {
109109
* Generates a cryptographically random 24 word seed mnemonic from an lnd client.
110110
*/
111111
public genSeed = async () => {
112-
// loop through swap clients until we find a connected lnd client
112+
// loop through swap clients until we find an lnd client awaiting unlock
113113
for (const swapClient of this.swapClients.values()) {
114-
if (isLndClient(swapClient) && swapClient.isConnected()) {
114+
if (isLndClient(swapClient) && swapClient.isWaitingUnlock()) {
115115
try {
116116
return swapClient.genSeed();
117117
} catch (err) {
@@ -135,8 +135,8 @@ class SwapClientManager extends EventEmitter {
135135
if (isLndClient(swapClient) && swapClient.isWaitingUnlock()) {
136136
const initWalletPromise = swapClient.initWallet(walletPassword, seedMnemonic).then(() => {
137137
createdLndWallets.push(swapClient.currency);
138-
}).catch(() => {
139-
this.loggers.lnd.debug(`could not initialize ${swapClient.currency} client`);
138+
}).catch((err) => {
139+
this.loggers.lnd.debug(`could not initialize ${swapClient.currency} client: ${err.message}`);
140140
});
141141
initWalletPromises.push(initWalletPromise);
142142
}
@@ -162,8 +162,8 @@ class SwapClientManager extends EventEmitter {
162162
if (isLndClient(swapClient) && swapClient.isWaitingUnlock()) {
163163
const unlockWalletPromise = swapClient.unlockWallet(walletPassword).then(() => {
164164
unlockedLndClients.push(swapClient.currency);
165-
}).catch(() => {
166-
this.loggers.lnd.debug(`could not unlock ${swapClient.currency} client`);
165+
}).catch((err) => {
166+
this.loggers.lnd.debug(`could not unlock ${swapClient.currency} client: ${err.message}`);
167167
});
168168
unlockWalletPromises.push(unlockWalletPromise);
169169
}

0 commit comments

Comments
 (0)