Skip to content

Commit

Permalink
fix(core-p2p): respond with error when blockchain is not ready (#4109)
Browse files Browse the repository at this point in the history
  • Loading branch information
air1one authored Oct 20, 2020
1 parent ce3d930 commit d44e7e1
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 1 deletion.
147 changes: 147 additions & 0 deletions __tests__/unit/core-p2p/socket-server/plugins/is-app-ready.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { Server } from "@hapi/hapi";
import Joi from "@hapi/joi";
import { Container } from "@arkecosystem/core-kernel";

import { IsAppReadyPlugin } from "@arkecosystem/core-p2p/src/socket-server/plugins/is-app-ready";

afterEach(() => {
jest.clearAllMocks();
});

describe("IsAppReadyPlugin", () => {
let isAppReadyPlugin: IsAppReadyPlugin;

const container = new Container.Container();

const responsePayload = { status: "ok" };
const mockRouteByPath = {
"/p2p/peer/mockroute": {
id: "p2p.peer.getPeers",
handler: () => responsePayload,
validation: Joi.object().max(0),
},
};
const mockRoute = {
method: "POST",
path: "/p2p/peer/mockroute",
config: {
id: mockRouteByPath["/p2p/peer/mockroute"].id,
handler: mockRouteByPath["/p2p/peer/mockroute"].handler,
},
};

const blockchainService = { isBooted: jest.fn().mockReturnValue(true) };
const app = {
isBound: jest.fn().mockReturnValue(true),
get: jest.fn().mockReturnValue(blockchainService),
resolve: jest.fn().mockReturnValue({ getRoutesConfigByPath: () => mockRouteByPath }),
};

beforeAll(() => {
container.unbindAll();
container.bind(Container.Identifiers.Application).toConstantValue(app);
});

beforeEach(() => {
isAppReadyPlugin = container.resolve<IsAppReadyPlugin>(IsAppReadyPlugin);
});

it("should register the plugin", async () => {
const server = new Server({ port: 4100 });
server.route(mockRoute);

const spyExt = jest.spyOn(server, "ext");

isAppReadyPlugin.register(server);

expect(spyExt).toBeCalledWith(expect.objectContaining({ type: "onPreHandler" }));

// try the route with a valid payload
const remoteAddress = "187.166.55.44";
const responseValid = await server.inject({
method: "POST",
url: "/p2p/peer/mockroute",
payload: {},
remoteAddress,
});
expect(JSON.parse(responseValid.payload)).toEqual(responsePayload);
expect(responseValid.statusCode).toBe(200);
expect(app.isBound).toBeCalledTimes(1);
expect(blockchainService.isBooted).toBeCalledTimes(1);
});

it("should return a forbidden error when blockchain service is not bound", async () => {
const server = new Server({ port: 4100 });
server.route(mockRoute);
isAppReadyPlugin.register(server);

app.isBound.mockReturnValueOnce(false);

// try the route with a valid payload
const remoteAddress = "187.166.55.44";
const responseForbidden = await server.inject({
method: "POST",
url: "/p2p/peer/mockroute",
payload: {},
remoteAddress,
});
expect(responseForbidden.statusCode).toBe(403);
expect(app.isBound).toBeCalledTimes(1);
expect(blockchainService.isBooted).toBeCalledTimes(0);
});

it("should return a forbidden error when blockchain service is not booted", async () => {
const server = new Server({ port: 4100 });
server.route(mockRoute);
isAppReadyPlugin.register(server);

blockchainService.isBooted.mockReturnValueOnce(false);

// try the route with a valid payload
const remoteAddress = "187.166.55.44";
const responseForbidden = await server.inject({
method: "POST",
url: "/p2p/peer/mockroute",
payload: {},
remoteAddress,
});
expect(responseForbidden.statusCode).toBe(403);
expect(app.isBound).toBeCalledTimes(1);
expect(blockchainService.isBooted).toBeCalledTimes(1);
});

it("should not be called on another route", async () => {
const testRoute = {
method: "POST",
path: "/p2p/peer/testroute",
config: {
handler: () => {
return { status: "ok" };
},
},
};

const server = new Server({ port: 4100 });
server.route(testRoute);
server.route(mockRoute);

const spyExt = jest.spyOn(server, "ext");

isAppReadyPlugin.register(server);

expect(spyExt).toBeCalledWith(expect.objectContaining({ type: "onPreHandler" }));

// try the route with a valid payload
const remoteAddress = "187.166.55.44";
const responseValid = await server.inject({
method: "POST",
url: "/p2p/peer/testroute",
payload: {},
remoteAddress,
});
expect(JSON.parse(responseValid.payload)).toEqual(responsePayload);
expect(responseValid.statusCode).toBe(200);
expect(app.isBound).toBeCalledTimes(1);
expect(blockchainService.isBooted).toBeCalledTimes(1);
});
});
7 changes: 7 additions & 0 deletions packages/core-blockchain/src/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export class Blockchain implements Contracts.Blockchain.Blockchain {

private missedBlocks: number = 0;
private lastCheckNetworkHealthTs: number = 0;
private booted: boolean = false;

@Container.postConstruct()
public initialize(): void {
Expand Down Expand Up @@ -120,9 +121,15 @@ export class Blockchain implements Contracts.Blockchain.Blockchain {

this.events.listen(Enums.RoundEvent.Applied, { handle: this.resetMissedBlocks });

this.booted = true;

return true;
}

public isBooted(): boolean {
return this.booted;
}

public async dispose(): Promise<void> {
if (!this.isStopped) {
this.logger.info("Stopping Blockchain Manager :chains:");
Expand Down
2 changes: 2 additions & 0 deletions packages/core-kernel/src/contracts/blockchain/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export interface Blockchain {
*/
boot(skipStartedCheck?: boolean): Promise<boolean>;

isBooted(): boolean;

dispose(): Promise<void>;

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/core-p2p/src/network-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export class NetworkMonitor implements Contracts.P2P.NetworkMonitor {

for (const key of Object.keys(peerErrors)) {
const peerCount = peerErrors[key].length;
this.logger.debug(`Removed ${peerCount} ${Utils.pluralize("peers", peerCount)} because of "${key}"`);
this.logger.debug(`Removed ${peerCount} ${Utils.pluralize("peer", peerCount)} because of "${key}"`);
}

if (this.initializing) {
Expand Down
24 changes: 24 additions & 0 deletions packages/core-p2p/src/socket-server/plugins/is-app-ready.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Container, Contracts } from "@arkecosystem/core-kernel";
import Boom from "@hapi/boom";

@Container.injectable()
export class IsAppReadyPlugin {
@Container.inject(Container.Identifiers.Application)
protected readonly app!: Contracts.Kernel.Application;

public register(server) {
server.ext({
type: "onPreHandler",
method: async (request, h) => {
if (
this.app.isBound(Container.Identifiers.BlockchainService) &&
this.app.get<Contracts.Blockchain.Blockchain>(Container.Identifiers.BlockchainService).isBooted()
) {
return h.continue;
}

return Boom.forbidden("App is not ready");
},
});
}
}
2 changes: 2 additions & 0 deletions packages/core-p2p/src/socket-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Server as HapiServer, ServerInjectOptions, ServerInjectResponse, Server
import { PortsOffset } from "../enums";
import { plugin } from "../hapi-nes";
import { AcceptPeerPlugin } from "./plugins/accept-peer";
import { IsAppReadyPlugin } from "./plugins/is-app-ready";
import { ValidatePlugin } from "./plugins/validate";
import { WhitelistForgerPlugin } from "./plugins/whitelist-forger";
import { BlocksRoute } from "./routes/blocks";
Expand Down Expand Up @@ -83,6 +84,7 @@ export class Server {
await this.transactionsServer.register({ plugin, options: { maxPayload: 10485760 } }); // 10 MB

for (const server of [this.peerServer, this.blocksServer, this.transactionsServer]) {
this.app.resolve(IsAppReadyPlugin).register(server);
this.app.resolve(ValidatePlugin).register(server);
}

Expand Down

0 comments on commit d44e7e1

Please sign in to comment.