@node-lightning/wire
Version:
Lightning Network Wire Protocol
272 lines • 11.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GossipManager = exports.PeerGossipState = void 0;
const MessageType_1 = require("../MessageType");
const Peer_1 = require("../Peer");
const GossipManager_1 = require("../gossip/GossipManager");
const GossipPeer_1 = require("./GossipPeer");
const GossipRelay_1 = require("./GossipRelay");
const ExtendedChannelAnnouncementMessage_1 = require("../messages/ExtendedChannelAnnouncementMessage");
const core_1 = require("@node-lightning/core");
const WireError_1 = require("../WireError");
class PeerGossipState {
}
exports.PeerGossipState = PeerGossipState;
/**
* GossipManager provides is a facade for many parts of gossip. It
* orchestrates for validating, storing, and emitting
* routing gossip traffic obtained by peers.
*/
class GossipManager {
constructor(logger, gossipFilter, chainClient) {
this.gossipFilter = gossipFilter;
this.chainClient = chainClient;
this.logger = logger.sub(GossipManager.name);
this.peers = new Map();
this.syncState = GossipManager_1.SyncState.Unsynced;
this.gossipRelay = new GossipRelay_1.GossipRelay(logger.sub(GossipRelay_1.GossipRelay.name), 60000, 2000);
}
/**
* Starts the gossip manager. This method will load information
* from the gossip store, determine when the last information
* was obtained, validate the existing messages (to see if any
* channels have closed), and finally emit all messages that
* exist in the system.
*/
async start() {
this.logger.info("starting gossip manager");
// wait for chain sync to complete
if (this.chainClient) {
this.logger.info("waiting for chain sync");
await this.chainClient.waitForSync();
this.logger.info("chain sync complete");
}
// Restore the state
await this._restoreState();
// start the gossip relay manager
this.gossipRelay.start();
// flag that the manager has now started
this.started = true;
}
onPeerReady(peer) {
const gossipPeer = new GossipPeer_1.GossipPeer(this.logger, peer);
this.peers.set(gossipPeer.key, gossipPeer);
// Add the peer to the relay manager
this.gossipRelay.addPeer(peer);
// If we have not yet performed a full synchronization then we can
// perform the full gossip state restore from this node
if (this.syncState === GossipManager_1.SyncState.Unsynced) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this._syncPeer(gossipPeer);
}
// If we've already synced, simply enable gossip receiving for the peer
else {
gossipPeer.enableGossip();
}
}
/**
* Handles when a peer closes
* @param gossipPeer
*/
onPeerClose(peer) {
const gossipPeer = this.findPeer(peer);
if (this.gossipRelay) {
this.gossipRelay.removePeer(peer);
}
this.peers.delete(gossipPeer.key);
}
/**
* TODO: Refactor this out of here. It should be part of the PeerManager!
* Uses a dns seed to discover and add peers to be managed by the GossipManager.
*/
async bootstrapPeers(ls, localFeatures, localChains, logger, dnsSeed) {
if (!this.started)
throw new WireError_1.WireError(WireError_1.WireErrorCode.gossipManagerNotStarted);
const peerRecords = await this.dnsPeerQuery.query({ dnsSeed: dnsSeed });
for (const peerRecord of peerRecords) {
const peer = new Peer_1.Peer(ls, localFeatures, localChains, logger);
peer.once("ready", this.onPeerReady.bind(this));
peer.connect(peerRecord.publicKey, peerRecord.address, peerRecord.port);
}
}
/**
* TODO: Refactor this. It should be event based, not imperative.
* Removes the channel from storage by the gossip manager. This
* will likely be called by a chain-monitoring service.
*/
async removeChannel(scid) {
this.logger.debug("removing channel %s", scid.toString());
await this.gossipFilter.gossipStore.deleteChannelAnnouncement(scid);
}
/**
* TODO: Refactor this. It should be event based, not imperative.
* Removes the channel from storage by the gossip manager. This will
* likely be called by a chain-monitoring service.
* @param outpoint
*/
async removeChannelByOutpoint(outpoint) {
const chanAnn = await this.gossipFilter.gossipStore.findChannelAnnouncementByOutpoint(outpoint);
if (!chanAnn)
return;
await this.removeChannel(chanAnn.shortChannelId);
}
/**
* TODO: Refactor this. It should be event based, not imperative.
* Retrieves the valid chan_ann, chan_update, node_ann messages
* while making sure to not send duplicate node_ann messages.
*
* @remarks
* For now we are going to buffer messages into memory. We could
* return a stream and yield messages as they are streamed from
* the gossip_store.
*/
async *allMessages() {
this.logger.debug("fetching all messages");
// maintain a set of node ids that we have already seen so that
// we do no rebroadcast node announcements. This set stores the
// nodeid pubkey as a hex string, which through testing is the
// fastest way to perfrom set operations.
const seenNodeIds = new Set();
// obtain full list of channel announcements
const chanAnns = await this.gossipFilter.gossipStore.findChannelAnnouncemnts();
for (const chanAnn of chanAnns) {
yield chanAnn;
// load and add the node1 channel_update
const update1 = await this.gossipFilter.gossipStore.findChannelUpdate(chanAnn.shortChannelId, 0);
if (update1)
yield update1;
// load and add the nod2 channel_update
const update2 = await this.gossipFilter.gossipStore.findChannelUpdate(chanAnn.shortChannelId, 1);
if (update2)
yield update2;
// optionally load node1 announcement
const nodeId1 = chanAnn.nodeId1.toString("hex");
if (!seenNodeIds.has(nodeId1)) {
seenNodeIds.add(nodeId1);
const nodeAnn = await this.gossipFilter.gossipStore.findNodeAnnouncement(chanAnn.nodeId1);
if (nodeAnn)
yield nodeAnn;
}
// optionally load node2 announcement
const nodeId2 = chanAnn.nodeId2.toString("hex");
if (!seenNodeIds.has(nodeId2)) {
seenNodeIds.add(nodeId2);
const nodeAnn = await this.gossipFilter.gossipStore.findNodeAnnouncement(chanAnn.nodeId2);
if (nodeAnn)
yield nodeAnn;
}
}
// Broadcast unattached node announcements. These may have been orphaned
// from previously closed channels, or if the node allows node_ann messages
// without channels.
const nodeAnns = await this.gossipFilter.gossipStore.findNodeAnnouncements();
for (const nodeAnn of nodeAnns) {
if (!seenNodeIds.has(nodeAnn.nodeId.toString("hex")))
yield nodeAnn;
}
}
findPeer(peer) {
return this.peers.get(peer.id);
}
async handlePeerMessage(peer, msg) {
// process any sync task as well
const gossipPeer = this.findPeer(peer);
if (gossipPeer.syncTask) {
gossipPeer.syncTask.handleWireMessage(msg);
}
// process inbound messages
if (msg.type === MessageType_1.MessageType.ChannelAnnouncement ||
msg.type === MessageType_1.MessageType.ChannelUpdate ||
msg.type === MessageType_1.MessageType.NodeAnnouncement) {
try {
const result = await this.gossipFilter.validateMessage(msg);
// TODO handle the result
if (result.isOk) {
this.logger.info(result.value);
}
else {
// Handled error should be emitted to the caller
// but we prevent the transform stream from
// stopping by calling the callback without an
// error.
}
// Unhandled error is something unexpected and our peer
// is now in a broken state and we need to disconnect.
}
catch (err) {
//
}
}
// process valid message
if (msg.type === MessageType_1.MessageType.ChannelAnnouncement) {
this.blockHeight = Math.max(this.blockHeight, msg.shortChannelId.block);
}
// enqueue the message into the relayer
this.gossipRelay.enqueue(msg);
}
/**
* Synchronize the peer using the peer's synchronization mechanism.
* @param peer
*/
async _syncPeer(peer) {
// Disable gossip relay
this.gossipRelay.stop();
this.logger.trace("sync status now 'syncing'");
this.syncState = GossipManager_1.SyncState.Syncing;
try {
// perform synchronization
await peer.syncRange();
// finally transition to sync complete status
this.logger.trace("sync status now 'synced'");
this.syncState = GossipManager_1.SyncState.Synced;
// enable gossip for all the peers
this.logger.trace("enabling gossip for all peers");
for (const gossipPeer of this.peers.values()) {
gossipPeer.enableGossip();
}
}
catch (ex) {
// TODO select next peer
this.syncState = GossipManager_1.SyncState.Unsynced;
}
// Enable gossip relay now that sync is complete
this.gossipRelay.start();
}
async _restoreState() {
this.logger.info("retrieving gossip state from store");
this.blockHeight = 0;
const chanAnns = await this.gossipFilter.gossipStore.findChannelAnnouncemnts();
// find best block height
for (const chanAnn of chanAnns) {
this.blockHeight = Math.max(this.blockHeight, chanAnn.shortChannelId.block);
}
this.logger.info("highest block %d found from %d channels", this.blockHeight, chanAnns.length); // prettier-ignore
// validate all utxos
await this._validateUtxos(chanAnns);
}
async _validateUtxos(chanAnns) {
if (!this.chainClient) {
this.logger.info("skipping utxo validation, no chain_client configured");
return;
}
const extChanAnnCount = chanAnns.reduce((acc, msg) => acc + (msg instanceof ExtendedChannelAnnouncementMessage_1.ExtendedChannelAnnouncementMessage ? 1 : 0), 0);
this.logger.info("validating %d utxos", extChanAnnCount);
if (!extChanAnnCount)
return;
const oct = Math.trunc(extChanAnnCount / 8);
for (let i = 0; i < chanAnns.length; i++) {
const chanAnn = chanAnns[i];
if ((i + 1) % oct === 0) {
this.logger.info("validating utxos %s% complete", (((i + 1) / extChanAnnCount) * 100).toFixed(2));
}
if (chanAnn instanceof ExtendedChannelAnnouncementMessage_1.ExtendedChannelAnnouncementMessage) {
const utxo = await this.chainClient.getUtxo(chanAnn.outpoint.txid.toString(core_1.HashByteOrder.RPC), chanAnn.outpoint.outputIndex);
if (!utxo) {
await this.removeChannel(chanAnn.shortChannelId);
}
}
}
}
}
exports.GossipManager = GossipManager;
//# sourceMappingURL=GossipManager.js.map