@node-lightning/wire
Version:
Lightning Network Wire Protocol
451 lines (385 loc) • 16.5 kB
text/typescript
// tslint:disable: max-classes-per-file
// tslint:disable: no-unused-expression
import { BitField } from "@node-lightning/core";
import { ILogger } from "@node-lightning/logger";
import * as noise from "@node-lightning/noise";
import { expect } from "chai";
import sinon from "sinon";
import { Duplex } from "stream";
import { IWireMessage } from "../lib";
import { InitFeatureFlags } from "../lib/flags/InitFeatureFlags";
import { Peer } from "../lib/Peer";
import { PeerState } from "../lib/PeerState";
import { PingPongState } from "../lib/PingPongState";
import { createFakeLogger } from "./_test-utils";
class FakeSocket extends Duplex {
[x: string]: any;
public outgoing: Buffer[] = [];
constructor() {
super();
this.end = sinon.spy(this.end.bind(this));
this.rpk = Buffer.alloc(33);
}
public _write(data: any, encoding: BufferEncoding, cb: (err?: Error) => void) {
this.outgoing.push(data);
cb();
}
public _read() {
// nada
}
public simReceive(data: Buffer) {
this.push(data);
}
}
class FakeMessage implements IWireMessage {
public type = 3;
[x: string]: any;
constructor(msg) {
this.msg = msg;
}
public serialize() {
return Buffer.from(this.msg);
}
}
describe("Peer", () => {
let chainHashes: Buffer[];
let sut: Peer;
let ls: Buffer;
let rpk: Buffer;
let logger: ILogger;
let sandbox: sinon.SinonSandbox;
let socket: FakeSocket;
let localFeatures: BitField<InitFeatureFlags>;
beforeEach(() => {
chainHashes = [Buffer.alloc(32, 0xff)];
localFeatures = new BitField<InitFeatureFlags>();
localFeatures.set(InitFeatureFlags.optionDataLossProtectOptional);
ls = Buffer.alloc(32, 0);
rpk = Buffer.alloc(32, 1);
logger = createFakeLogger();
sut = new Peer(ls, localFeatures, chainHashes, logger, 1);
socket = new FakeSocket();
sut.pingPongState = sinon.createStubInstance(PingPongState) as any;
sandbox = sinon.createSandbox();
});
afterEach(() => {
sandbox.restore();
});
describe(".connect()", () => {
beforeEach(() => {
sandbox.stub(noise, "connect").returns(socket as any);
sandbox.stub(sut, "_onSocketReady" as any);
sandbox.stub(sut, "_onSocketClose" as any);
sandbox.stub(sut, "_onSocketError" as any);
sandbox.stub(sut, "_onSocketReadable" as any);
sut.connect(rpk, "127.0.0.1", 9735);
});
it("should be initiator", () => {
expect(sut.isInitiator).to.equal(true);
});
it("should bind to ready", () => {
socket.emit("ready");
expect((sut as any)._onSocketReady.called).to.be.true;
});
it("should bind close", () => {
socket.emit("close");
expect((sut as any)._onSocketClose.called).to.be.true;
});
it("should bind error", () => {
socket.emit("error");
expect((sut as any)._onSocketError.called).to.be.true;
});
it("should bind readable", () => {
socket.emit("readable");
expect((sut as any)._onSocketReadable.called).to.be.true;
});
});
describe(".attach()", () => {
beforeEach(() => {
sandbox.stub(sut, "_onSocketReady" as any);
sandbox.stub(sut, "_onSocketClose" as any);
sandbox.stub(sut, "_onSocketError" as any);
sandbox.stub(sut, "_onSocketReadable" as any);
sut.attach(socket as any);
});
it("should not be initiator", () => {
expect(sut.isInitiator).to.equal(false);
});
it("should bind to ready", () => {
socket.emit("ready");
expect((sut as any)._onSocketReady.called).to.be.true;
});
it("should bind close", () => {
socket.emit("close");
expect((sut as any)._onSocketClose.called).to.be.true;
});
it("should bind error", () => {
socket.emit("error");
expect((sut as any)._onSocketError.called).to.be.true;
});
it("should bind readable", () => {
socket.emit("readable");
expect((sut as any)._onSocketReadable.called).to.be.true;
});
});
describe("when connected", () => {
beforeEach(() => {
sut.attach(socket as any);
});
describe(".sendMessage()", () => {
it("should throw when not ready", () => {
expect(() => sut.sendMessage(new FakeMessage("hello") as any)).to.throw();
});
it("should send the serialized message", () => {
const input = new FakeMessage("test");
sut.state = Peer.states.Ready;
sut.sendMessage(input);
expect((socket as any).outgoing[0]).to.deep.equal(Buffer.from("test"));
});
it("should emit a sending message", done => {
const input = new FakeMessage("test");
sut.state = Peer.states.Ready;
sut.on("sending", () => done());
sut.sendMessage(input);
});
});
describe(".disconnect()", () => {
it("should stop the socket", () => {
sut.disconnect();
expect((sut.socket as any).end.called).to.be.true;
});
it("should change the peer state to disconnecting", () => {
sut.disconnect();
expect(sut.state).to.equal(PeerState.Disconnecting);
});
});
describe(".reconnect()", () => {
it("should stop the socket", () => {
sut.reconnect();
expect((sut.socket as any).end.called).to.be.true;
});
it("should retain the peer state", () => {
const beforeState = sut.state;
sut.reconnect();
expect(sut.state).to.equal(beforeState);
});
});
describe("._onSocketReady()", () => {
it("should transition state to awaiting_peer_init", () => {
(sut as any)._onSocketReady();
expect(sut.state).to.equal(Peer.states.AwaitingPeerInit);
});
it("should send the init message to the peer", () => {
(sut as any)._onSocketReady();
expect((socket as any).outgoing[0]).to.deep.equal(
Buffer.from(
"001000000001020120ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"hex",
),
);
});
});
describe("_onSocketClose", () => {
it("should stop the ping pong state", () => {
sut.state = PeerState.Disconnecting;
(sut as any)._onSocketClose();
expect((sut as any).pingPongState.onDisconnecting.called).to.be.true;
});
describe("when disconnecting", () => {
it("should emit the close event", done => {
sut.state = PeerState.Disconnecting;
sut.on("close", () => done());
(sut as any)._onSocketClose();
});
});
describe("when initiator", () => {
it("should trigger reconnect", done => {
sut.state = PeerState.Ready;
sut.reconnectTimeoutMs = 0;
sut.connect = sandbox.stub(sut, "connect");
sut.isInitiator = true;
(sut as any)._onSocketClose();
setTimeout(() => {
expect((sut.connect as any).called).to.be.true;
done();
}, 50);
});
});
describe("when not initiator", () => {
it("should not trigger reconnect", done => {
sut.state = PeerState.Ready;
sut.reconnectTimeoutMs = 0;
sut.connect = sandbox.stub(sut, "connect");
sut.isInitiator = false;
(sut as any)._onSocketClose();
setTimeout(() => {
expect((sut.connect as any).called).to.be.false;
done();
}, 50);
});
});
});
describe("_onSocketError", () => {
it("should emit error event", done => {
sut.on("error", () => done());
(sut as any)._onSocketError();
});
});
describe("_sendInitMessage", () => {
it("should send the initialization message", () => {
(sut as any)._sendInitMessage();
expect((socket as any).outgoing[0]).to.deep.equal(
Buffer.from(
"001000000001020120ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"hex",
),
);
});
});
describe("_processPeerInitMessage", () => {
let input;
beforeEach(() => {
input = Buffer.from("001000000000", "hex");
});
it("it should fail if not init message", () => {
input = Buffer.from("001100000000", "hex");
expect(() => (sut as any)._processPeerInitMessage(input)).to.throw();
});
it("should start ping state", () => {
(sut as any)._processPeerInitMessage(input);
expect((sut as any).pingPongState.start.called).to.be.true;
});
it("should change the state to ready", () => {
(sut as any)._processPeerInitMessage(input);
expect(sut.state).to.equal(Peer.states.Ready);
});
it("should capture the init features", () => {
input = Buffer.from("00100000000109", "hex");
(sut as any)._processPeerInitMessage(input);
expect(sut.remoteFeatures).to.be.instanceof(BitField);
expect(sut.remoteFeatures.isSet(InitFeatureFlags.optionDataLossProtectRequired));
expect(sut.remoteFeatures.isSet(InitFeatureFlags.initialRoutingSyncOptional));
});
it("should emit ready", done => {
sut.on("ready", () => done());
(sut as any)._processPeerInitMessage(input);
});
it("should be ok with no remote chainhash", () => {
input = Buffer.from("00100000000109", "hex");
(sut as any)._processPeerInitMessage(input);
});
it("should be ok with single chain_hash", () => {
input = Buffer.from(
"00100000000109" +
"0120" +
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"hex",
);
(sut as any)._processPeerInitMessage(input);
expect(sut.remoteChains.length).to.equal(1);
expect(sut.remoteChains[0].toString("hex")).to.equal("ff".repeat(32));
});
it("should be ok with multiple chain_hashes", () => {
input = Buffer.from(
"00100000000109" +
"0140" +
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" +
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"hex",
);
(sut as any)._processPeerInitMessage(input);
expect(sut.remoteChains.length).to.equal(2);
expect(sut.remoteChains[0].toString("hex")).to.equal("ff".repeat(32));
expect(sut.remoteChains[1].toString("hex")).to.equal("ee".repeat(32));
});
it("should disconnect with no chainhash match", () => {
input = Buffer.from(
"00100000000109" +
"0120" +
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"hex",
);
(sut as any)._processPeerInitMessage(input);
expect(sut.state).to.equal(PeerState.Disconnecting);
});
it("should stay connected when local odd no remote feature", () => {
// enable compulsory (bit 0)
(sut as any).localFeatures = new BitField<InitFeatureFlags>();
sut.localFeatures.set(InitFeatureFlags.optionDataLossProtectOptional);
// remote optional (bit 1)
input = Buffer.from("00100000000100", "hex");
(sut as any)._processPeerInitMessage(input);
expect(sut.state).to.equal(PeerState.Ready);
});
it("should stay connected when local even/remote even feature", () => {
// enable compulsory (bit 0)
sut.localFeatures.set(InitFeatureFlags.optionDataLossProtectRequired);
// remote optional (bit 1)
input = Buffer.from("00100000000102", "hex");
(sut as any)._processPeerInitMessage(input);
expect(sut.state).to.equal(PeerState.Ready);
});
it("should stay connected with local even/remote even feature", () => {
// enable compulsory (bit 0)
sut.localFeatures.set(InitFeatureFlags.optionDataLossProtectRequired);
// remote optional (bit 1)
input = Buffer.from("00100000000101", "hex");
(sut as any)._processPeerInitMessage(input);
expect(sut.state).to.equal(PeerState.Ready);
});
it("should disconnect with local even missing remote feature", () => {
// enable compulsory (bit 0)
sut.localFeatures.set(InitFeatureFlags.optionDataLossProtectRequired);
// remote optional (bit 1)
input = Buffer.from("00100000000100", "hex");
(sut as any)._processPeerInitMessage(input);
expect(sut.state).to.equal(PeerState.Disconnecting);
});
});
describe("_processMessage", () => {
let input;
let msg;
beforeEach(() => {
input = Buffer.from("001000000000", "hex");
});
describe("when valid message", () => {
it("should log with ping service", () => {
msg = (sut as any)._processMessage(input);
expect((sut as any).pingPongState.onMessage.called).to.be.true;
});
it("should emit the message", () => {
expect(msg.type).to.equal(16);
});
});
});
describe("_onSocketReadable", () => {
it("should read peer init message when awaiting_peer_init state", done => {
sut.state = Peer.states.AwaitingPeerInit;
sut.on("ready", () => done());
socket.simReceive(Buffer.from("001000000000", "hex"));
});
it("should process message when in ready state", done => {
sut.state = Peer.states.Ready;
sut.on("readable", () => {
done();
});
socket.simReceive(Buffer.from("001000000000", "hex"));
});
describe("on error", () => {
it("should close the socket", done => {
sut.state = Peer.states.Ready;
sut.on("error", () => {
expect((socket as any).end.called).to.be.true;
done();
});
socket.simReceive(Buffer.from("0010", "hex"));
});
it("should emit an error event", done => {
sut.state = Peer.states.Ready;
sut.on("error", () => done());
socket.simReceive(Buffer.from("0010", "hex"));
});
});
});
});
});