UNPKG

@node-dlc/wire

Version:
321 lines (284 loc) 9.39 kB
import { ShortChannelId } from '@node-dlc/common'; import { ILogger } from '@node-dlc/logger'; import { ChannelUpdateChannelFlags } from '../flags/ChanneUpdateChannelFlags'; import { ChannelAnnouncementMessage } from '../messages/ChannelAnnouncementMessage'; import { ChannelUpdateMessage } from '../messages/ChannelUpdateMessage'; import { IWireMessage } from '../messages/IWireMessage'; import { NodeAnnouncementMessage } from '../messages/NodeAnnouncementMessage'; import { IPeer } from '../Peer'; /** * Interface for the sub-system for handling the gossip message relay, * also known as rumor mongering.This system is responsible for * periodically flushing messages to connected peers and makes a best * effort to not send message that have already been sent to a peer. * * The idea of rumor mongering is that a piece of information is hot. A * node attempts to infect connected peers with this information by * sending it to them. Once it has been sent, we no longer need to * infect them with information. */ export interface IGossipRelay { /** * The current state of gossip relay */ state: GossipRelayState; /** * Starts gossip relay */ start(): void; /** * Stops gossip relay */ stop(): void; /** * Adds a new peer to relay messages to * @param peer */ addPeer(peer: IPeer): void; /** * Removes the peer from relay * @param peer */ removePeer(peer: IPeer): void; /** * Enqueues a message to be broadcast to peers * @param msg */ enqueue(msg: IWireMessage): void; } /** * The state of a IGossipRelay rumor mongerer */ export enum GossipRelayState { /** * Rumor mongering is not active */ Inactive, /** * Rumor mongering is active */ Active, } /** * This is a basic implementation of IGossipRelay that enques all * messages and maintaining an index of each peer in the queue. When * messages are flushed, only messages that haven't been sent to a peer * are sent and the index position for that peer is updated. When the * queue of messages has reached a maximum length, older messages are * pruned and the index positions are updated. */ export class GossipRelay { private _queue: IWireMessage[]; private _peers: Map<IPeer, number>; private _timer: NodeJS.Timeout; private _state: GossipRelayState; constructor( readonly logger: ILogger, readonly relayPeriodMs = 60000, readonly maxQueueLen = 10000, ) { this._peers = new Map() as Map<IPeer, number>; this._queue = []; this._state = GossipRelayState.Inactive; } /** * Gets the current state of gossip relay */ public get state(): GossipRelayState { return this._state; } /** * Starts relay to peers. This enables messages to be enqueued and * periodically sent to the peers. */ public start(): void { if (this._state === GossipRelayState.Active) return; this.logger.info('starting gossip relay for all peers'); this._state = GossipRelayState.Active; // eslint-disable-next-line @typescript-eslint/no-implied-eval this._timer = setInterval(this._onTimer.bind(this), this.relayPeriodMs); } /** * Stops relay to peers. */ public stop(): void { if (this._state === GossipRelayState.Inactive) return; this.logger.info('stopping gossip relay for all peers'); clearTimeout(this._timer); this._state = GossipRelayState.Inactive; } /** * Adds a new peer to relay messages to * @param peer */ public addPeer(peer: IPeer): void { this._peers.set(peer, this._queue.length); } /** * Removes the peer from relay * @param peer */ public removePeer(peer: IPeer): void { this._peers.delete(peer); } /** * Enqueues a message to be broadcast to peers. * @param msg */ public enqueue(msg: IWireMessage): void { if (this.state !== GossipRelayState.Active) return; // For chan_ann messages there is never an update so we only // need to check if the chan_ann exists and add it if it doesn't if (msg instanceof ChannelAnnouncementMessage) { const existing = this._findChanAnn(msg.shortChannelId); // adds to the queue if there is no existing message if (!existing) { this.logger.trace( 'adding channel_announcement', msg.shortChannelId.toString(), ); this._queue.push(msg); return; } } // For chan_update messages we will add messages that don't exist // or update an existing update message if one already exists if (msg instanceof ChannelUpdateMessage) { const existing = this._findChanUpd( msg.shortChannelId, msg.channelFlags.isSet(ChannelUpdateChannelFlags.direction), ); // Adds to the queue if there is no existing message if (!existing) { this.logger.trace( 'adding channel_update', msg.shortChannelId.toString(), msg.channelFlags.isSet(ChannelUpdateChannelFlags.direction), ); this._queue.push(msg); return; } // Removes the existing message and replaces with a newer // message by adding the new message to the back of the queue if (existing && existing.timestamp < msg.timestamp) { this.logger.trace( 'updating channel_update', msg.shortChannelId.toString(), msg.channelFlags.isSet(ChannelUpdateChannelFlags.direction), ); const idx = this._queue.indexOf(existing); this._queue.splice(idx, 1); this._queue.push(msg); return; } } // For node_ann messages we look for the existing messages and // abort if the new msg is older than the current node_ann we // have in the message queue. if (msg instanceof NodeAnnouncementMessage) { const existing = this._findNodeAnn(msg.nodeId); // Adds to the queue if there is no existing message if (!existing) { this.logger.trace( 'adding node_announcement', msg.nodeId.toString('hex'), ); this._queue.push(msg); return; } // Removes the existing message and replaces with a newer // message by adding the new message to the back of the queue if (existing && existing.timestamp < msg.timestamp) { this.logger.trace( 'updating node_announcement', msg.nodeId.toString('hex'), ); const idx = this._queue.indexOf(existing); this._queue.splice(idx, 1); this._queue.push(msg); return; } } } /** * Finds a channel_announcement message based on the short_channel_id * @param scid */ private _findChanAnn(scid: ShortChannelId): ChannelAnnouncementMessage { return this._queue.find( (p) => p instanceof ChannelAnnouncementMessage && p.shortChannelId.toNumber() === scid.toNumber(), ) as ChannelAnnouncementMessage; } /** * Finds a channel_update message based on the short_channel_id and * direction. The found message can then be compared to an inbound * message to determine if the new message is newer. * @param scid * @param direction */ private _findChanUpd( scid: ShortChannelId, direction: boolean, ): ChannelUpdateMessage { return this._queue.find( (p) => p instanceof ChannelUpdateMessage && p.shortChannelId.toNumber() === scid.toNumber() && p.channelFlags.isSet(ChannelUpdateChannelFlags.direction) === direction, ) as ChannelUpdateMessage; } /** * Finds a node_announcement message based on the node_id. The * returned message can be compared to newer messages using the * timestamp. * @param nodeId */ private _findNodeAnn(nodeId: Buffer): NodeAnnouncementMessage { return this._queue.find( (p) => p instanceof NodeAnnouncementMessage && p.nodeId.equals(nodeId), ) as NodeAnnouncementMessage; } /** * Fires when the timer ticks and will flush messages to peers and * prune the queue */ private _onTimer() { this.logger.debug(`periodic flush, ${this._peers.size} peers, ${this._queue.length} hot messages`); // prettier-ignore for (const peer of this._peers.keys()) { this._flushToPeer(peer); } this._pruneQueue(); } /** * Flushes message to a peer based on the index of messages that * the peer has received. * @param peer */ private _flushToPeer(peer: IPeer) { for (let i = this._peers.get(peer); i < this._queue.length; i++) { const message = this._queue[i]; peer.sendMessage(message); this._peers.set(peer, this._queue.length); } } /** * Prunes excess message */ private _pruneQueue() { // calculate the delete count based on the current queue length // and the max allowed queue length const deleteCount = Math.max(0, this._queue.length - this.maxQueueLen); // do nothing if we don't need to prune any items if (deleteCount === 0) return; // pruning the excess items from the start of the queue this._queue.splice(0, deleteCount); // adjust all of the peers by reducing their index position by // the delete count for (const [peer, index] of this._peers.entries()) { this._peers.set(peer, index - deleteCount); } this.logger.debug(`pruned ${deleteCount} old messages`); } }