Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ yarn run start
# You can change the host of CodeChain and DB host on the config/dev.json
```

### Check CCCChanges

If you run the Indexer with the environment `ENABLE_CCC_CHANGES_CHECK` variable,
the Indexer checks to see if CCCChanges is well calculated.
If you want to receive an email when an error is found, please set the `SENDGRID_API_KEY` and `SENDGRID_TO` variables.
If you want to receive a slack notification, please set the `SLACK_WEBHOOK` variable.

## Run (for production)

```
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"yarn": "^1.10.0"
},
"dependencies": {
"@sendgrid/mail": "^6.4.0",
"@slack/client": "^5.0.1",
"async-lock": "^1.1.4",
"bignumber.js": "^7.2.1",
"buffer": "^5.2.1",
Expand Down
99 changes: 99 additions & 0 deletions src/checker/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as sendgrid from "@sendgrid/mail";

export interface Email {
sendError(msg: string): void;
sendWarning(text: string): void;
sendInfo(title: string, msg: string): void;
}

class NullEmail implements Email {
public sendError(_: string): void {
// empty
}
public sendWarning(_: string): void {
// empty
}
public sendInfo(_: string, __: string): void {
// empty
}
}

const from = "no-reply+indexer-CCCChanges-checker@devop.codechain.io";

function createTitle(params: {
title: string;
tag: string;
level: string;
}): string {
const { title, tag, level } = params;
return `[${level}]${tag} ${title}`;
}

// tslint:disable-next-line:max-classes-per-file
class Sendgrid implements Email {
private readonly tag: string;
private readonly to: string;

public constructor(params: {
tag: string;
sendgridApiKey: string;
to: string;
}) {
const { tag, sendgridApiKey, to } = params;
this.tag = tag;
sendgrid.setApiKey(sendgridApiKey);
this.to = to;
}

public sendError(text: string): void {
const subject = createTitle({
tag: this.tag,
title: "has a problem.",
level: "error"
});
this.send(subject, text);
}

public sendWarning(text: string): void {
const subject = createTitle({
tag: this.tag,
title: "finds a problem.",
level: "warn"
});
this.send(subject, text);
}

public sendInfo(title: string, text: string): void {
const subject = createTitle({ tag: this.tag, title, level: "info" });
this.send(subject, text);
}

private send(subject: string, value: string): void {
sendgrid
.send({
subject,
from,
to: this.to,
content: [{ type: "text/html", value }]
})
.catch(console.error);
}
}

export function createEmail(params: {
tag: string;
to?: string;
sendgridApiKey?: string;
}): Email {
const { tag, to, sendgridApiKey } = params;
if (sendgridApiKey != null) {
if (to == null) {
throw Error("The email destination is not set");
}
console.log("Sendgrid key is set");
return new Sendgrid({ tag, sendgridApiKey, to });
} else {
console.log("Donot use sendgrid");
return new NullEmail();
}
}
184 changes: 184 additions & 0 deletions src/checker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { SDK } from "codechain-sdk";
import { IndexerConfig } from "../config";
import * as BlockModel from "../models/logic/block";
import * as CCCChangeModel from "../models/logic/cccChange";
import { createEmail, Email } from "./email";
import { createSlack, Slack } from "./slack";

export async function run(sdk: SDK, options: IndexerConfig) {
console.log("Start to check CCCChanges");
const prevBlockInstance = await BlockModel.getLatestBlock();
let lastCheckedBlockNumber: number | "NotExist";

const email = createEmail({
tag: `[${options.codechain.networkId}][indexer-cccchanges-checker]`,
sendgridApiKey: process.env.SENDGRID_API_KEY,
to: process.env.SENDGRID_TO
});

const slack = createSlack(
`[${options.codechain.networkId}][indexer-cccchanges-checker]`,
process.env.SLACK_WEBHOOK
);

if (prevBlockInstance) {
lastCheckedBlockNumber = prevBlockInstance.get({ plain: true }).number;
} else {
lastCheckedBlockNumber = "NotExist";
}
for (;;) {
await new Promise(resolve => setTimeout(resolve, 5 * 1000));
if (lastCheckedBlockNumber === "NotExist") {
const blockInstance = await BlockModel.getByNumber(0);
if (blockInstance === null) {
continue;
} else {
lastCheckedBlockNumber = -1;
continue;
}
}

const latestBlock = (await BlockModel.getLatestBlock())!;
const latestBlockNumber: number = latestBlock.get("number");

const checkFrom = lastCheckedBlockNumber + 1;
const checkTo = latestBlockNumber - 1;
if (checkTo > checkFrom) {
await checkBlocks(checkFrom, checkTo, sdk, email, slack);
lastCheckedBlockNumber = checkTo;
} else {
continue;
}
}
}

async function checkBlocks(
fromBlockNumber: number,
toBlockNumber: number,
sdk: SDK,
email: Email,
slack: Slack
) {
if (fromBlockNumber >= toBlockNumber) {
throw new Error(
`Invalid fromBlockNumber(${fromBlockNumber}) and toBlockNumber(${toBlockNumber})`
);
}

let beforeBlockNumber = fromBlockNumber;
let afterBlockNumber = fromBlockNumber + 1;

for (;;) {
const cccChanges = (await CCCChangeModel.getByBlockNumber(
afterBlockNumber
)).map(instance => instance.get({ plain: true }));

const balanceChangeMap: Map<string, number> = new Map();
cccChanges.forEach(cccChange => {
/// Total CCC does not exceed Number.MAX_SAFE_INTEGER;
const change = parseInt(cccChange.change, 10);
const address = cccChange.address;

if (balanceChangeMap.has(cccChange.address)) {
balanceChangeMap.set(
address,
balanceChangeMap.get(address)! + change
);
} else {
balanceChangeMap.set(address, change);
}
});

const promises = Array.from(balanceChangeMap).map(
async ([address, change]) => {
const beforeBalanceUInt = await sdk.rpc.chain.getBalance(
address,
beforeBlockNumber
);
const afterBalanceUInt = await sdk.rpc.chain.getBalance(
address,
afterBlockNumber
);

const beforeBalance = parseInt(
beforeBalanceUInt.toString(10),
10
);
const afterBalance = parseInt(
afterBalanceUInt.toString(10),
10
);
const expected = afterBalance - beforeBalance;
const actual = change;

if (actual !== expected) {
sendAlarm({
address,
beforeBlockNumber,
afterBlockNumber,
beforeBalance,
afterBalance,
actual,
expected,
email,
slack
});
}
}
);

await Promise.all(promises);
if (afterBlockNumber === toBlockNumber) {
return;
}
beforeBlockNumber += 1;
afterBlockNumber += 1;
}
}

function sendAlarm({
address,
beforeBlockNumber,
afterBlockNumber,
actual,
expected,
beforeBalance,
afterBalance,
email,
slack
}: {
address: string;
beforeBlockNumber: number;
afterBlockNumber: number;
actual: number;
expected: number;
beforeBalance: number;
afterBalance: number;
email: Email;
slack: Slack;
}) {
/// TODO Send an email.
const firstLine = "Mismatch found";
console.group(firstLine);

const lines = [
`Address: ${address}`,
`Balance at ${beforeBlockNumber}: ${beforeBalance}`,
`Balance at ${afterBlockNumber}: ${afterBalance}`,
`Actual CCCChanges: ${actual}`,
`Exepcted CCCChanges: ${expected}`
];
lines.forEach(line => {
console.error(line);
});
console.groupEnd();

email.sendError(`
<p>${firstLine}</p>
<ul>
${lines.map(line => `<li>${line}</li>`).join("\n")}
</ul>
`);

slack.sendError([firstLine, ...lines].join("\n"));
}
85 changes: 85 additions & 0 deletions src/checker/slack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { IncomingWebhook, MessageAttachment } from "@slack/client";
import * as _ from "lodash";

export interface Slack {
sendError(msg: string): void;
sendWarning(text: string): void;
sendInfo(title: string, text: string): void;
}

class NullSlack implements Slack {
public sendError(__: string) {
// empty
}
public sendWarning(__: string) {
// empty
}
public sendInfo(__: string, ___: string) {
// empty
}
}

// tslint:disable-next-line:max-classes-per-file
class SlackWebhook implements Slack {
private readonly tag: string;
private readonly webhook: IncomingWebhook;
private unsentAttachments: MessageAttachment[] = [];

public constructor(tag: string, slackWebhookUrl: string) {
this.tag = tag;
this.webhook = new IncomingWebhook(slackWebhookUrl, {});
}

public sendError(text: string) {
const title = `[error]${this.tag} has a problem`;
this.unsentAttachments.push({ title, text, color: "danger" });
this.send();
}

public sendWarning(text: string) {
console.log(`Warning: ${text}`);
this.unsentAttachments.push({
title: `[warn]${this.tag} finds a problem`,
text,
color: "warning"
});
this.send();
}

public sendInfo(title: string, text: string) {
console.log(`Info: ${text}`);
this.unsentAttachments.push({
title: `[info]${this.tag} ${title}`,
text,
color: "good"
});
this.send();
}

private send() {
this.webhook
.send({
attachments: this.unsentAttachments
})
.catch((err: Error) => {
if (err) {
console.error("IncomingWebhook failed!", err);
return;
}
});
this.unsentAttachments = [];
}
}

export function createSlack(
tag: string,
slackWebhookUrl: string | undefined
): Slack {
if (slackWebhookUrl) {
console.log("Slack connected");
return new SlackWebhook(tag, slackWebhookUrl);
} else {
console.log("Slack not connected");
return new NullSlack();
}
}
Loading