@node-lightning/wire
Version:
Lightning Network Wire Protocol
135 lines (110 loc) • 4.75 kB
text/typescript
import { PeerHostRecord } from "../PeerHostRecord";
import { promises as dnsPromises, SrvRecord } from "dns";
import bech32 from "bech32";
import { AddressType } from "../domain/AddressType";
export interface DnsPeerQueryOptions {
/**
* The domain of the dns seed.
*/
dnsSeed: string;
/**
* The realm byte used to specify what realm the returned nodes must support.
*/
realm?: number;
/**
* The address type bit field specifies the address types that should be returned.
* It follows the format of the address descriptor type specified in BOLT #7.
*/
addressTypes?: AddressType[];
/**
* The bech32-encoded node id used to retrive the result of a single node.
*/
nodeId?: string;
/**
* The number of desired reply records.
*/
desiredReplyRecords?: number;
}
/**
* This class implements the node discovery mechanism described
* in BOLT #10.
* The purpose of this component is to assist in the initial
* node discovery process for nodes that have no known contacts,
* and to help nodes discover the current network address of previously
* known peers. A domain name server that implements BOLT #10 is
* referred to as a DNS Seed and answers incoming DNS queries of
* type A, AAAA, or SRV.
*/
export class DnsPeerQuery {
private resolver: dnsPromises.Resolver;
constructor(resolver?: dnsPromises.Resolver) {
this.resolver = resolver || new dnsPromises.Resolver();
}
/**
* The query method starts by querying a DNS seed for SRV records.
* This will result in a list of returned SRV records that each
* represent a lightning node that can be connected to. Each record
* is a subdomain of the dns seed where the first component of the
* subdomain is the bech32 encoded public key of the node (also known
* as the node_id). Each subdomain can subsequently be used to query
* the DNS seed for the A (or AAA) records of the lightning node with
* the specified node_id. The query method ends by constructing peer
* host records using the port from the SRV records and the ip
* address from the A (or AAA) records.
* @param dnsPeerQueryOptions
* @returns valid peer host records
*/
public async query(dnsPeerQueryOptions: DnsPeerQueryOptions): Promise<PeerHostRecord[]> {
const dnsSeed = this._buildUrl(dnsPeerQueryOptions);
const peerSrvRecords: SrvRecord[] = await this._getPeerSrvRecords(dnsSeed);
const peerHostRecordPromises: Promise<PeerHostRecord>[] = [];
for (const peerSrvRecord of peerSrvRecords) {
peerHostRecordPromises.push(this._createPeerHostRecord(peerSrvRecord));
}
const peerHostRecordSettlements = await Promise.allSettled(peerHostRecordPromises);
const peers = peerHostRecordSettlements
.filter(recordSettlement => recordSettlement?.status == "fulfilled")
.map((p: PromiseFulfilledResult<PeerHostRecord>) => p.value);
return peers;
}
private async _createPeerHostRecord(peerSrvRecord: SrvRecord): Promise<PeerHostRecord> {
const peerAddress = await this._resolveSrvNameToIp(peerSrvRecord.name);
return new PeerHostRecord(
this._getPublicKeyFromSrvRecord(peerSrvRecord),
peerAddress,
peerSrvRecord.port,
);
}
private _buildUrl(dnsPeerQueryOptions: DnsPeerQueryOptions): string {
let { dnsSeed } = dnsPeerQueryOptions;
const { realm, addressTypes, nodeId, desiredReplyRecords } = dnsPeerQueryOptions;
if (desiredReplyRecords != null) {
dnsSeed = `n${desiredReplyRecords}.${dnsSeed}`;
}
if (nodeId != null) {
dnsSeed = `l${nodeId}.${dnsSeed}`;
}
if (addressTypes != null) {
const a = addressTypes.reduce((bitField, addressType) => {
return bitField | (1 << addressType);
}, 0);
dnsSeed = `a${a}.${dnsSeed}`;
}
if (realm != null) {
dnsSeed = `r${realm}.${dnsSeed}`;
}
return dnsSeed;
}
private _getPublicKeyFromSrvRecord(srvRecord: SrvRecord): Buffer {
const domainComponents = srvRecord.name.split(".");
const { words } = bech32.decode(domainComponents[0]);
return Buffer.from(bech32.fromWords(words));
}
private _getPeerSrvRecords(dnsSeed: string): Promise<SrvRecord[]> {
return this.resolver.resolveSrv(dnsSeed);
}
private async _resolveSrvNameToIp(hostname: string): Promise<string> {
const ipAddresses: string[] = await this.resolver.resolve(hostname, "A");
return ipAddresses?.[0];
}
}