Skip to content

Commit

Permalink
feat(lnd): use lnd 0.6.1 w/ hold invoices
Browse files Browse the repository at this point in the history
This is a major commit that changes the way hashes are resolved and
swaps are performed with lnd. Here we switch from using a fork of lnd
to the main lnd repository.

Previously, we used the hash resolver service which made a resolve gRPC
call from lnd to xud when lnd accepted an htlc with a payment hash it
did not recognize. Now, when we prepare for a swap xud will tell lnd
about the hash via `AddHoldInvoice` and subscribe to updates to that
invoice via `SubscribeSingleInvoice`. The subscription will alert xud
when lnd accepts an htlc for that invoice, at which point xud will
attempt to resolve the hash by completing the swap and provide the
preimage to lnd via `SettleInvoice`. If a swap fails, the invoice in
lnd is canceled via `CancelInvoice`.

A new SanitySwapAck packet is necessary for this new approach.
Previously, the sanity swap payment and the SanitySwap packet (which
informs the peer of the hash for the swap would) be sent at the same
time. A node that received a resolve request for an amount of 1 satoshi
and a hash it did not recognize would wait a short while to see if it
received a SanitySwap packet for that hash. With hold invoices, lnd
must be informed of the payment hash via `AddInvoice` before it receives
the htlc, otherwise it will fail the htlc immediately. Thus, the
SanitySwapAck packet is used to inform the peer that it is ready to
accept the sanity swap payment.

Closes #798.
  • Loading branch information
sangaman committed May 14, 2019
1 parent 956c7a6 commit 35ae4f6
Show file tree
Hide file tree
Showing 48 changed files with 18,859 additions and 10,641 deletions.
19 changes: 18 additions & 1 deletion docs/api.md

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

2 changes: 1 addition & 1 deletion lib/grpc/GrpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import GrpcService from './GrpcService';
import Service from '../service/Service';
import errors from './errors';
import { XudService } from '../proto/xudrpc_grpc_pb';
import { HashResolverService } from '../proto/lndrpc_grpc_pb';
import { promises as fs } from 'fs';
import serverProxy from './serverProxy';
import { HashResolverService } from '../proto/hash_resolver_grpc_pb';

class GrpcServer {
private server: any;
Expand Down
12 changes: 10 additions & 2 deletions lib/grpc/GrpcService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import grpc, { status } from 'grpc';
import Logger from '../Logger';
import Service from '../service/Service';
import * as xudrpc from '../proto/xudrpc_pb';
import { ResolveRequest, ResolveResponse } from '../proto/lndrpc_pb';
import { Order, isOwnOrder, OrderPortion, PlaceOrderResult, PlaceOrderEvent, PlaceOrderEventType } from '../orderbook/types';
import { errorCodes as orderErrorCodes } from '../orderbook/errors';
import { errorCodes as serviceErrorCodes } from '../service/errors';
Expand All @@ -12,6 +11,7 @@ import { errorCodes as lndErrorCodes } from '../lndclient/errors';
import { LndInfo } from '../lndclient/LndClient';
import { SwapSuccess, SwapFailure } from '../swaps/types';
import { SwapFailureReason } from '../constants/enums';
import { ResolveRequest, ResolveResponse } from '../proto/hash_resolver_pb';

/**
* Creates an xudrpc Order message from an [[Order]].
Expand Down Expand Up @@ -361,7 +361,15 @@ class GrpcService {
const getLndInfo = ((lndInfo: LndInfo): xudrpc.LndInfo => {
const lnd = new xudrpc.LndInfo();
if (lndInfo.blockheight) lnd.setBlockheight(lndInfo.blockheight);
if (lndInfo.chains) lnd.setChainsList(lndInfo.chains);
if (lndInfo.chains) {
const chains: xudrpc.Chain[] = lndInfo.chains.map((chain) => {
const xudChain = new xudrpc.Chain();
xudChain.setChain(chain.chain);
xudChain.setNetwork(chain.network);
return xudChain;
});
lnd.setChainsList(chains);
}
if (lndInfo.channels) {
const channels = new xudrpc.LndChannels();
channels.setActive(lndInfo.channels.active);
Expand Down
147 changes: 107 additions & 40 deletions lib/lndclient/LndClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ import Logger from '../Logger';
import SwapClient, { ClientStatus } from '../swaps/SwapClient';
import errors from './errors';
import { LightningClient } from '../proto/lndrpc_grpc_pb';
import { InvoicesClient } from '../proto/lndinvoices_grpc_pb';
import * as lndrpc from '../proto/lndrpc_pb';
import * as lndinvoices from '../proto/lndinvoices_pb';
import assert from 'assert';
import { promises as fs } from 'fs';
import { SwapState, SwapRole, SwapClientType } from '../constants/enums';
import { SwapDeal } from '../swaps/types';
import { LndInfo, ChannelCount, Chain } from './types';
import { base64ToHex, hexToUint8Array } from '../utils/utils';

/** The configurable options for the lnd client. */
type LndClientConfig = {
Expand All @@ -20,37 +24,26 @@ type LndClientConfig = {
nomacaroons: boolean;
};

/** General information about the state of this lnd client. */
type LndInfo = {
error?: string;
channels?: ChannelCount;
chains?: string[];
blockheight?: number;
uris?: string[];
version?: string;
alias?: string;
};

type ChannelCount = {
active: number,
inactive?: number,
pending: number,
};

interface LightningMethodIndex extends LightningClient {
[methodName: string]: Function;
}

interface InvoicesMethodIndex extends InvoicesClient {
[methodName: string]: Function;
}

/** A class representing a client to interact with lnd. */
class LndClient extends SwapClient {
public readonly type = SwapClientType.Lnd;
public readonly cltvDelta: number;
private lightning!: LightningClient | LightningMethodIndex;
private invoices!: InvoicesClient | InvoicesMethodIndex;
private meta!: grpc.Metadata;
private uri!: string;
private credentials!: ChannelCredentials;
private identityPubKey?: string;
private invoiceSubscription?: ClientReadableStream<lndrpc.InvoiceSubscription>;
private channelSubscription?: ClientReadableStream<lndrpc.ChannelEventUpdate>;
private invoiceSubscriptions = new Map<string, ClientReadableStream<lndrpc.Invoice>>();

/**
* Creates an lnd client.
Expand Down Expand Up @@ -119,9 +112,25 @@ class LndClient extends SwapClient {
});
}

private unaryInvoiceCall = <T, U>(methodName: string, params: T): Promise<U> => {
return new Promise((resolve, reject) => {
if (this.isDisabled()) {
reject(errors.LND_IS_DISABLED);
return;
}
(this.invoices as InvoicesMethodIndex)[methodName](params, this.meta, (err: grpc.ServiceError, response: U) => {
if (err) {
reject(err);
} else {
resolve(response);
}
});
});
}

public getLndInfo = async (): Promise<LndInfo> => {
let channels: ChannelCount | undefined;
let chains: string[] | undefined;
let chains: Chain[] | undefined;
let blockheight: number | undefined;
let uris: string[] | undefined;
let version: string | undefined;
Expand All @@ -138,7 +147,7 @@ class LndClient extends SwapClient {
active: lnd.getNumActiveChannels(),
pending: lnd.getNumPendingChannels(),
};
chains = lnd.getChainsList(),
chains = lnd.getChainsList().map(value => value.toObject());
blockheight = lnd.getBlockHeight(),
uris = lnd.getUrisList(),
version = lnd.getVersion();
Expand Down Expand Up @@ -167,6 +176,7 @@ class LndClient extends SwapClient {
if (!this.isConnected()) {
this.logger.info(`trying to verify connection to lnd for ${this.currency} at ${this.uri}`);
this.lightning = new LightningClient(this.uri, this.credentials);
this.invoices = new InvoicesClient(this.uri, this.credentials);

try {
const getInfoResponse = await this.getInfo();
Expand All @@ -181,7 +191,7 @@ class LndClient extends SwapClient {
this.identityPubKey = newPubKey;
}
this.emit('connectionVerified', newPubKey);
this.subscribeInvoices();
this.subscribeChannels();
} else {
await this.setStatus(ClientStatus.OutOfSync);
this.logger.warn(`lnd for ${this.currency} is out of sync with chain, retrying in ${LndClient.RECONNECT_TIMER} ms`);
Expand All @@ -202,18 +212,12 @@ class LndClient extends SwapClient {
return this.unaryCall<lndrpc.GetInfoRequest, lndrpc.GetInfoResponse>('getInfo', new lndrpc.GetInfoRequest());
}

/**
* Gets the preimage in hex format from an lnd SendResponse message.
*/
private getPreimageFromSendResponse = (response: lndrpc.SendResponse) => {
return Buffer.from(response.getPaymentPreimage_asB64(), 'base64').toString('hex');
}

public sendSmallestAmount = async (rHash: string, destination: string) => {
const sendRequest = new lndrpc.SendRequest();
sendRequest.setAmt(1);
sendRequest.setDestString(destination);
sendRequest.setPaymentHashString(rHash);
sendRequest.setFinalCltvDelta(this.cltvDelta);
let sendPaymentResponse: lndrpc.SendResponse;
try {
sendPaymentResponse = await this.sendPaymentSync(sendRequest);
Expand All @@ -225,7 +229,7 @@ class LndClient extends SwapClient {
if (paymentError) {
throw new Error(paymentError);
}
return this.getPreimageFromSendResponse(sendPaymentResponse);
return base64ToHex(sendPaymentResponse.getPaymentPreimage_asB64());
}

public sendPayment = async (deal: SwapDeal): Promise<string> => {
Expand All @@ -244,7 +248,7 @@ class LndClient extends SwapClient {
throw new Error(sendPaymentError);
}

return this.getPreimageFromSendResponse(sendToRouteResponse);
return base64ToHex(sendToRouteResponse.getPaymentPreimage_asB64());
} catch (err) {
this.logger.error(`got exception from sendToRouteSync: ${JSON.stringify(request.toObject())}`, err);
throw err;
Expand Down Expand Up @@ -272,7 +276,7 @@ class LndClient extends SwapClient {
throw new Error(sendPaymentError);
}

return this.getPreimageFromSendResponse(sendPaymentResponse);
return base64ToHex(sendPaymentResponse.getPaymentPreimage_asB64());
} catch (err) {
this.logger.error(`got exception from sendPaymentSync: ${JSON.stringify(request.toObject())}`, err);
throw err;
Expand All @@ -293,7 +297,7 @@ class LndClient extends SwapClient {
/**
* Gets a new address for the internal lnd wallet.
*/
public newAddress = (addressType: lndrpc.NewAddressRequest.AddressType): Promise<lndrpc.NewAddressResponse> => {
public newAddress = (addressType: lndrpc.AddressType): Promise<lndrpc.NewAddressResponse> => {
const request = new lndrpc.NewAddressRequest();
request.setType(addressType);
return this.unaryCall<lndrpc.NewAddressRequest, lndrpc.NewAddressResponse>('newAddress', request);
Expand Down Expand Up @@ -387,17 +391,80 @@ class LndClient extends SwapClient {
return this.unaryCall<lndrpc.SendToRouteRequest, lndrpc.SendResponse>('sendToRouteSync', request);
}

public addInvoice = async (rHash: string, amount: number) => {
const addHoldInvoiceRequest = new lndinvoices.AddHoldInvoiceRequest();
addHoldInvoiceRequest.setHash(hexToUint8Array(rHash));
addHoldInvoiceRequest.setValue(amount);
addHoldInvoiceRequest.setCltvExpiry(this.cltvDelta); // TODO: use peer's cltv delta
await this.addHoldInvoice(addHoldInvoiceRequest);
this.logger.debug(`added invoice of ${amount} for ${rHash}`);
this.subscribeSingleInvoice(rHash);
}

public settleInvoice = async (rHash: string, rPreimage: string) => {
const invoiceSubscription = this.invoiceSubscriptions.get(rHash);
if (invoiceSubscription) {
const settleInvoiceRequest = new lndinvoices.SettleInvoiceMsg();
settleInvoiceRequest.setPreimage(hexToUint8Array(rPreimage));
await this.settleInvoiceLnd(settleInvoiceRequest);
this.logger.debug(`settled invoice for ${rHash}`);
invoiceSubscription.cancel();
}
}

public removeInvoice = async (rHash: string) => {
const invoiceSubscription = this.invoiceSubscriptions.get(rHash);
if (invoiceSubscription) {
const cancelInvoiceRequest = new lndinvoices.CancelInvoiceMsg();
cancelInvoiceRequest.setPaymentHash(hexToUint8Array(rHash));
await this.cancelInvoice(cancelInvoiceRequest);
this.logger.debug(`canceled invoice for ${rHash}`);
invoiceSubscription.cancel();
}
}

private addHoldInvoice = (request: lndinvoices.AddHoldInvoiceRequest): Promise<lndinvoices.AddHoldInvoiceResp> => {
return this.unaryInvoiceCall<lndinvoices.AddHoldInvoiceRequest, lndinvoices.AddHoldInvoiceResp>('addHoldInvoice', request);
}

private cancelInvoice = (request: lndinvoices.CancelInvoiceMsg): Promise<lndinvoices.CancelInvoiceResp> => {
return this.unaryInvoiceCall<lndinvoices.CancelInvoiceMsg, lndinvoices.CancelInvoiceResp>('cancelInvoice', request);
}

private settleInvoiceLnd = (request: lndinvoices.SettleInvoiceMsg): Promise<lndinvoices.SettleInvoiceResp> => {
return this.unaryInvoiceCall<lndinvoices.SettleInvoiceMsg, lndinvoices.SettleInvoiceResp>('settleInvoice', request);
}

private subscribeSingleInvoice = (rHash: string) => {
const paymentHash = new lndrpc.PaymentHash();
// TODO: use RHashStr when bug fixed in lnd - https://github.com/lightningnetwork/lnd/pull/3019
paymentHash.setRHash(hexToUint8Array(rHash));
const invoiceSubscription = this.invoices.subscribeSingleInvoice(paymentHash, this.meta);
const deleteInvoiceSubscription = () => {
invoiceSubscription.removeAllListeners();
this.invoiceSubscriptions.delete(rHash);
this.logger.debug(`deleted invoice subscription for ${rHash}`);
};
invoiceSubscription.on('data', (invoice: lndrpc.Invoice) => {
if (invoice.getState() === lndrpc.Invoice.InvoiceState.ACCEPTED) {
// we have accepted an htlc for this invoice
this.emit('htlcAccepted', rHash, invoice.getValue());
}
}).on('end', deleteInvoiceSubscription).on('error', deleteInvoiceSubscription);
this.invoiceSubscriptions.set(rHash, invoiceSubscription);
}

/**
* Subscribes to invoices.
* Subscribes to channel events.
*/
private subscribeInvoices = (): void => {
if (this.invoiceSubscription) {
this.invoiceSubscription.cancel();
private subscribeChannels = (): void => {
if (this.channelSubscription) {
this.channelSubscription.cancel();
}

this.invoiceSubscription = this.lightning.subscribeInvoices(new lndrpc.InvoiceSubscription(), this.meta)
this.channelSubscription = this.lightning.subscribeChannelEvents(new lndrpc.ChannelEventSubscription(), this.meta)
.on('error', async (error) => {
this.invoiceSubscription = undefined;
this.channelSubscription = undefined;
this.logger.error(`lnd for ${this.currency} has been disconnected, error: ${error}`);
await this.setStatus(ClientStatus.Disconnected);
});
Expand Down Expand Up @@ -437,8 +504,8 @@ class LndClient extends SwapClient {

/** Lnd client specific cleanup. */
protected closeSpecific = () => {
if (this.invoiceSubscription) {
this.invoiceSubscription.cancel();
if (this.channelSubscription) {
this.channelSubscription.cancel();
}
}
}
Expand Down
21 changes: 21 additions & 0 deletions lib/lndclient/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/** General information about the state of this lnd client. */
export type LndInfo = {
error?: string;
channels?: ChannelCount;
chains?: Chain[];
blockheight?: number;
uris?: string[];
version?: string;
alias?: string;
};

export type ChannelCount = {
active: number,
inactive?: number,
pending: number,
};

export type Chain = {
network: string,
chain: string,
};
5 changes: 4 additions & 1 deletion lib/p2p/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ class Parser extends EventEmitter {
packetOrPbObj = packetTypes.NodesPacket.deserialize(payload);
break;
case PacketType.SanitySwap:
packetOrPbObj = packetTypes.SanitySwapPacket.deserialize(payload);
packetOrPbObj = packetTypes.SanitySwapInitPacket.deserialize(payload);
break;
case PacketType.SanitySwapAck:
packetOrPbObj = packetTypes.SanitySwapAckPacket.deserialize(payload);
break;
case PacketType.SwapRequest:
packetOrPbObj = packetTypes.SwapRequestPacket.deserialize(payload);
Expand Down
2 changes: 1 addition & 1 deletion lib/p2p/Peer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ class Peer extends EventEmitter {
* Waits for a packet to be received from peer.
* @returns A promise that is resolved once the packet is received or rejects on timeout.
*/
private wait = (reqId: string, resType: ResponseType, timeout?: number, cb?: (packet: Packet) => void): Promise<Packet> => {
public wait = (reqId: string, resType: ResponseType, timeout?: number, cb?: (packet: Packet) => void): Promise<Packet> => {
const entry = this.getOrAddPendingResponseEntry(reqId, resType);
return new Promise((resolve, reject) => {
entry.addJob(resolve, reject);
Expand Down
Loading

0 comments on commit 35ae4f6

Please sign in to comment.