UNPKG

@node-lightning/wire

Version:
438 lines (382 loc) 15.8 kB
import { BitField } from "@node-lightning/core"; import { ILogger } from "@node-lightning/logger"; import * as noise from "@node-lightning/noise"; import { NoiseSocket } from "@node-lightning/noise"; import assert from "assert"; import { Readable } from "stream"; import { InitFeatureFlags } from "./flags/InitFeatureFlags"; import * as MessageFactory from "./MessageFactory"; import { InitMessage } from "./messages/InitMessage"; import { IWireMessage } from "./messages/IWireMessage"; import { PeerState } from "./PeerState"; import { PingPongState } from "./PingPongState"; export interface IMessageSender { send(buf: Buffer): void; sendMessage(msg: IWireMessage): void; } export interface IMessageReceiver { read(): IWireMessage; on(event: "readable", listener: () => void): this; off(event: "readable", listener: () => void): this; } export type IMessageSenderReceiver = IMessageSender & IMessageReceiver; export interface IPeer extends IMessageSenderReceiver { send(buf: Buffer): void; sendMessage(msg: IWireMessage): void; disconnect(): void; read(): IWireMessage; on(event: "readable", listener: () => void): this; on(event: "error", listener: (err: Error) => void): this; off(event: "readable", listener: () => void): this; off(event: "error", listener: (err: Error) => void): this; } /** * Peer is an EventEmitter that layers the Lightning Network wire * protocol ontop of an @node-lightning/noise NoiseSocket. * * Peer itself is a state-machine with three states: * 1. pending * 2. awaiting_peer_init * 3. ready * * The Peer instance starts in `pending` until the underlying NoiseSocket * has connected. * * It then immediately sends the InitMessage as specified in the Peer * constructor. * * At this point, the Peer transitions to `awaiting_peer_init`. * * Once the remote peer has sent its InitMessage, the state is * transitioned to `ready` and the Peer can be begin sending and * receiving messages. * * Once the peer is in the `ready` state it will begin emitting `message` * events when it receives new messages from the peer. * * The Peer will also start a PingPong state machine to manage sending * and receiving Pings and Pongs as defined in BOLT01 * * A choice (probably wrongly) was made to make Peer an EventEmitter * instead of a DuplexStream operating in object mode. We need to keep * the noise socket in flowing mode (instead of paused) because we will * not know the length of messages until after we have deserialized the * message. This makes it a challenge to implement a DuplexStream that * emits objects (such as messages). * * @emits ready the underlying socket has performed its handshake and * initialization message swap has occurred. * * @emits message a new message has been received. Only sent after the * `ready` event has fired. * * @emits rawmessage outputs the message as a raw buffer instead of * a deserialized message. * * @emits error emitted when there is an error processing a message. * The underlying socket will be closed after this event is emitted. * * @emits close emitted when the connection to the peer has completedly * closed. * * @emits open emmited when the connection to the peer has been established * after the handshake has been performed * * @emits end emitted when the connection to the peer is ending. */ export class Peer extends Readable implements IPeer { public static states = PeerState; public state: PeerState = PeerState.Disconnected; public socket: NoiseSocket; public messageCounter: number = 0; public pingPongState: PingPongState; public logger: ILogger; public remoteFeatures: BitField<InitFeatureFlags>; public remoteChains: Buffer[]; public isInitiator: boolean = false; public reconnectTimeoutMs = 15000; private _id: string; private _rpk: Buffer; private _host: string; private _port: number; private _reconnectHandle: NodeJS.Timeout; constructor( readonly ls: Buffer, readonly localFeatures: BitField<InitFeatureFlags>, readonly localChains: Buffer[], logger: ILogger, readonly highWaterMark: number = 2048, ) { super({ objectMode: true, highWaterMark }); this.pingPongState = new PingPongState(this); this.logger = logger; } public get id(): string { return this._id; } public get pubkey(): Buffer { return this._rpk; } public get pubkeyHex(): string { return this._rpk ? this._rpk.toString("hex") : undefined; } /** * Connect to the remote peer and binds socket events into the Peer. */ public connect(rpk: Buffer, host: string, port: number) { // construct logger specific to the peer this._rpk = rpk; this._id = this._rpk.slice(0, 8).toString("hex"); this.logger = this.logger.sub("peer", this._id); this.logger.info("connecting to peer"); this.isInitiator = true; // store these values if we need to use them for a reconnection event. this._host = host; this._port = port; // create the socket and initiate the connection this.socket = noise.connect({ ls: this.ls, host: this._host, port: this._port, rpk: this._rpk, logger: this.logger, }); this.socket.on("readable", this._onSocketReadable.bind(this)); this.socket.on("ready", this._onSocketReady.bind(this)); this.socket.on("close", this._onSocketClose.bind(this)); this.socket.on("error", this._onSocketError.bind(this)); } /** * * @param socket */ public attach(socket: NoiseSocket) { this.isInitiator = false; this.socket = socket; this.socket.on("readable", this._onSocketReadable.bind(this)); this.socket.on("ready", this._onSocketReady.bind(this)); this.socket.on("close", this._onSocketClose.bind(this)); this.socket.on("error", this._onSocketError.bind(this)); // Once the socket is ready, we have performed the handshake which allows // us to ascertain the identity of the connecting node. Now that we have // that identity ascertained we can construct the instance specific logger this.socket.once("ready", () => { this._rpk = socket.rpk; this._id = this._rpk.slice(0, 8).toString("hex"); this.logger = this.logger.sub("peer", this._id); this.logger.info("peer connected"); }); } /** * Writes data on the NoiseSocket. This method allows custom * serialization of methods. Use `sendMessage` to send a message * using the default message serialization. * @param buf */ public send(buf: Buffer): boolean { assert.ok(this.state === PeerState.Ready, new Error("Peer is not ready")); this.emit("sending", buf); return this.socket.write(buf); } /** * Writes the message on the NoiseSocket using the default * serialization properties */ public sendMessage(m: IWireMessage): boolean { assert.ok(this.state === PeerState.Ready, new Error("Peer is not ready")); const buf = m.serialize() as Buffer; this.emit("sending", buf); return this.socket.write(buf); } /** * Closes the socket */ public disconnect() { this.logger.info("disconnecting"); this.state = PeerState.Disconnecting; this.socket.end(); } /** * Reconnects the socket */ public reconnect() { this.socket.end(); } ///////////////////////////////////////////////////////// private _onSocketReady() { // now that we're connected, we need to wait for the remote reply // before any other messages can be receieved or sent this.state = PeerState.AwaitingPeerInit; this.logger.debug("state: awaiting_peer_init"); // blast off our init message this.emit("open"); this._sendInitMessage(); } private _onSocketClose() { this.logger.debug("socket closed"); // Clear any existing reconnection handles. We want the logic in // this method, and the current state of the peer to dictate // what should happen. clearTimeout(this._reconnectHandle); // Clear the ping/pong status if (this.pingPongState) this.pingPongState.onDisconnecting(); // If socket closed because there was a request to disconnect // the underlying socket, we will emit a close event and mark // the state as disconnected. if (this.state === PeerState.Disconnecting) { this.emit("close"); this.state = PeerState.Disconnected; this.logger.debug("permanently disconnected"); return; } // Update the state to indicate that the peer is no longer connected. // If we don't do this then upon reconnection the Peer will be in // invalid state and will not send the initialization message. this.state = PeerState.Disconnected; this.logger.debug("state: disconnected"); // If the disconnection was not intentional, we will initiate // a reconnection event by creating a reconnection handle // and delaying the connection event by the reconnectTimeoutMs // value. // This is likely to originate from two sources: // 1) A reconnection was requeted by the ping/pong manager because // receipt of a pong message timed out // 2) A network error occurred on a subsequent connection event // which triggered a socket close event. This can happen if a connection // is disrupted for a longer period of time. The reconnection logic // should continue to fire until a connection can be established. if (this.isInitiator) { this.logger.debug(`reconnecting in ${(this.reconnectTimeoutMs / 1000).toFixed(1)}s`); this._reconnectHandle = setTimeout(() => { this.connect(this._rpk, this._host, this._port); }, this.reconnectTimeoutMs); } } private _onSocketError(err) { // emit what error we recieved this.emit("error", err); } public _onSocketReadable() { this.logger.trace("socket is readable"); try { let cont = true; while (cont) { const raw: Buffer = this.socket.read() as Buffer; if (!raw) return; if (this.state === PeerState.AwaitingPeerInit) { this._processPeerInitMessage(raw); } else { const m = this._processMessage(raw); cont = this.push(m); } } } catch (err) { // we have a problem, kill connectinon with the client this.logger.error(err); this.socket.end(); this.emit("error", err); return; } } public _read() { // Trigger a read but wait until the end of the event loop. // This is necessary when reading in paused mode where // _read was triggered by stream.read() originating inside // a "readable" event handler. Attempting to push more data // synchronously will not trigger another "readable" event. setImmediate(() => this._onSocketReadable()); } /** * Sends the initialization message to the peer. This message * does not matter if it is sent before or after the peer sends * there message. */ private _sendInitMessage() { // construct the init message const msg = new InitMessage(); msg.features = this.localFeatures; msg.chainHashes = this.localChains; // fire off the init message to the peer const payload = msg.serialize(); this.emit("sending", payload); this.socket.write(payload); this.logger.debug("init message sent"); } /** * Processes the initialization message sent by the remote peer. * Once this is successfully completed, the state is transitioned * to `active` */ private _processPeerInitMessage(raw: Buffer) { // deserialize message const m = MessageFactory.deserialize(raw) as InitMessage; if (this.logger) { const features: InitFeatureFlags[] = m.features.flags(); this.logger.info("peer initialized with features", features); } // ensure we got an InitMessagee assert.ok(m instanceof InitMessage, new Error("Expecting InitMessage")); // store the init messagee in case we need to refer to it this.remoteFeatures = m.features; // capture the local chains this.remoteChains = m.chainHashes; // validate remote chains and if we are unawares of any. We do this by // first looking to see if there is any match on the chains. If there is // no match and both the remote and our local node declare that we are // monitoring a specific chain then we will abort the connnection let hasChain = false; for (const remoteChain of this.remoteChains) { for (const localChain of this.localChains) { if (remoteChain.equals(localChain)) hasChain = true; } } if (!hasChain && this.remoteChains.length && this.localChains.length) { this.logger.trace("remote chains", ...this.remoteChains); this.logger.trace("local chains", ...this.localChains); this.logger.warn("remote node does not support any known chains, aborting"); this.disconnect(); return; } // we need to be sure that the remote node supports required features // that we care about. If the remote node does not support these feature // we will disconnect. for (const feature of this.localFeatures.flags()) { // we can skip odd features since they are optional if (feature % 2 === 1) continue; // for even (compulsory) features, we check if the remote node is // signalling either optional or compulsory support. This code // makes the assumption that the even features are always first if (!(this.remoteFeatures.isSet(feature) || this.remoteFeatures.isSet(feature + 1))) { this.disconnect(); return; } } // start other state now that peer is initialized this.pingPongState.start(); // transition state to ready this.state = PeerState.Ready; // emit ready event this.emit("ready"); } /** * Process the raw message sent by the peer. These messages are * processed after the initialization message has been received. */ private _processMessage(raw: Buffer): IWireMessage { // increment counter first so we know exactly how many messages // have been received by the peer regardless of whether they // could be processed this.messageCounter += 1; // emit the rawmessage event first so that if there is a // deserialization problem there is a chance that we were // able to capture the raw message for further testing this.emit("rawmessage", raw); // deserialize the message const m = MessageFactory.deserialize(raw); // ensure pingpong state is updated if (m) { this.pingPongState.onMessage(m); } return m; } }