Skip to content

Commit

Permalink
fix(core-blockchain): block schema violation (#3806)
Browse files Browse the repository at this point in the history
  • Loading branch information
air1one committed Jun 17, 2020
1 parent 67aebfb commit 54ba2d2
Show file tree
Hide file tree
Showing 16 changed files with 352 additions and 109 deletions.
10 changes: 4 additions & 6 deletions __tests__/integration/core-blockchain/blockchain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const addBlocks = async untilHeight => {

for (let height = lastHeight + 1; height < untilHeight && height < 155; height++) {
const blockToProcess = allBlocks[height - 2];
await blockchain.processBlocks([blockToProcess], () => undefined);
await blockchain.processBlocks([blockToProcess]);
}
};

Expand Down Expand Up @@ -224,8 +224,6 @@ describe("Blockchain", () => {
};

it("should restore vote balances after a rollback", async () => {
const mockCallback = jest.fn(() => true);

// Create key pair for new voter
const keyPair = Identities.Keys.fromPassphrase("secret");
const recipient = Identities.Address.fromPublicKey(keyPair.publicKey);
Expand All @@ -241,7 +239,7 @@ describe("Blockchain", () => {
.createOne();

const transferBlock = createBlock(forgerKeys, [transfer]);
await blockchain.processBlocks([transferBlock], mockCallback);
await blockchain.processBlocks([transferBlock]);

const wallet = blockchain.database.walletManager.findByPublicKey(keyPair.publicKey);
const walletForger = blockchain.database.walletManager.findByPublicKey(forgerKeys.publicKey);
Expand All @@ -264,7 +262,7 @@ describe("Blockchain", () => {
let nextForgerWallet = delegates.find(wallet => wallet.publicKey === nextForger.publicKey);

const voteBlock = createBlock(nextForgerWallet, [vote]);
await blockchain.processBlocks([voteBlock], mockCallback);
await blockchain.processBlocks([voteBlock]);

// Wallet paid a fee of 1 and the vote has been placed.
expect(wallet.balance).toEqual(Utils.BigNumber.make(124));
Expand All @@ -287,7 +285,7 @@ describe("Blockchain", () => {
nextForgerWallet = delegates.find(wallet => wallet.publicKey === nextForger.publicKey);

const unvoteBlock = createBlock(nextForgerWallet, [unvote]);
await blockchain.processBlocks([unvoteBlock], mockCallback);
await blockchain.processBlocks([unvoteBlock]);

// Wallet paid a fee of 1 and no longer voted a delegate
expect(wallet.balance).toEqual(Utils.BigNumber.make(123));
Expand Down
3 changes: 3 additions & 0 deletions __tests__/integration/core-p2p/network-monitor.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import "./mocks/core-container";

import { P2P } from "@arkecosystem/core-interfaces";
import delay from "delay";
import { Peer } from "../../../packages/core-p2p/src/peer";
import { createPeerService, createStubPeer } from "../../helpers/peers";
import { MockSocketManager } from "./__support__/mock-socket-server/manager";
Expand Down Expand Up @@ -56,6 +57,8 @@ describe("NetworkMonitor", () => {

await monitor.cleansePeers({ fast: true });

await delay(1000); // removing peer can happen a bit after cleansePeers has resolved

expect(storage.getPeers().length).toBeLessThan(previousLength);
});
});
Expand Down
36 changes: 12 additions & 24 deletions __tests__/unit/core-blockchain/blockchain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { container } from "./mocks/container";

import * as Utils from "@arkecosystem/core-utils";
import { Blocks, Crypto, Interfaces, Managers } from "@arkecosystem/crypto";
import delay from "delay";
import { Blockchain } from "../../../packages/core-blockchain/src/blockchain";
import { stateMachine } from "../../../packages/core-blockchain/src/state-machine";
import "../../utils";
Expand Down Expand Up @@ -113,66 +112,55 @@ describe("Blockchain", () => {
});

it("should process a new chained block", async () => {
const mockCallback = jest.fn(() => true);
blockchain.state.blockchain = {};

await blockchain.processBlocks([blocks2to100[2]], mockCallback);
await delay(200);
const acceptedBlocks = await blockchain.processBlocks([blocks2to100[2]]);

expect(mockCallback.mock.calls.length).toBe(1);
expect(acceptedBlocks.length).toBe(1);
});

it("should process a valid block already known", async () => {
const mockCallback = jest.fn(() => true);
it("should handle a valid block already known", async () => {
const lastBlock = blockchain.getLastBlock().data;

await blockchain.processBlocks([lastBlock], mockCallback);
await delay(200);
const acceptedBlocks = await blockchain.processBlocks([lastBlock]);

expect(mockCallback.mock.calls.length).toBe(1);
expect(acceptedBlocks).toBeUndefined();
expect(blockchain.getLastBlock().data).toEqual(lastBlock);
});

it("should process a new block with database saveBlocks failing once", async () => {
const mockCallback = jest.fn(() => true);
it("should handle database saveBlocks failing once", async () => {
blockchain.state.blockchain = {};
database.saveBlocks = jest.fn().mockRejectedValueOnce(new Error("oops"));
jest.spyOn(blockchain, "removeTopBlocks").mockReturnValueOnce(undefined);

await blockchain.processBlocks([blocks2to100[2]], mockCallback);
await delay(200);
const acceptedBlocks = await blockchain.processBlocks([blocks2to100[2]]);

expect(mockCallback.mock.calls.length).toBe(1);
expect(acceptedBlocks).toBeUndefined();
});

it("should process a new block with database saveBlocks + getLastBlock failing once", async () => {
const mockCallback = jest.fn(() => true);
it("should handle a new block with database saveBlocks + getLastBlock failing once", async () => {
blockchain.state.blockchain = {};
jest.spyOn(database, "saveBlocks").mockRejectedValueOnce(new Error("oops saveBlocks"));
jest.spyOn(blockchain, "removeTopBlocks").mockReturnValueOnce(undefined);

await blockchain.processBlocks([blocks2to100[2]], mockCallback);
await delay(200);
const acceptedBlocks = await blockchain.processBlocks([blocks2to100[2]]);

expect(mockCallback.mock.calls.length).toBe(1);
expect(acceptedBlocks).toBeUndefined();
});

it("should broadcast a block if (Crypto.Slots.getSlotNumber() * blocktime <= block.data.timestamp)", async () => {
blockchain.state.started = true;
jest.spyOn(Utils, "isBlockChained").mockReturnValueOnce(true);

const mockCallback = jest.fn(() => true);
const lastBlock = blockchain.getLastBlock().data;
const spyGetSlotNumber = jest
.spyOn(Crypto.Slots, "getSlotNumber")
.mockReturnValue(Math.floor(lastBlock.timestamp / 8000));

const broadcastBlock = jest.spyOn(getMonitor, "broadcastBlock");

await blockchain.processBlocks([lastBlock], mockCallback);
await delay(200);
await blockchain.processBlocks([lastBlock]);

expect(mockCallback.mock.calls.length).toBe(1);
expect(broadcastBlock).toHaveBeenCalled();

spyGetSlotNumber.mockRestore();
Expand Down
29 changes: 27 additions & 2 deletions __tests__/unit/core-blockchain/processor/block-processor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import "../mocks/";
import { blockchain } from "../mocks/blockchain";
import { database } from "../mocks/database";

import { Blocks, Managers, Utils } from "@arkecosystem/crypto";
import { Blocks, Interfaces, Managers, Utils } from "@arkecosystem/crypto";
import { BlockProcessor, BlockProcessorResult } from "../../../../packages/core-blockchain/src/processor";
import * as handlers from "../../../../packages/core-blockchain/src/processor/handlers";
import {
Expand Down Expand Up @@ -45,19 +45,44 @@ describe("Block processor", () => {

describe("getHandler", () => {
it("should return ExceptionHandler if block is an exception", async () => {
Managers.configManager.setFromPreset("mainnet");
const exceptionBlock = BlockFactory.fromData(blockTemplate);
exceptionBlock.data.id = "10370119864814436559";
exceptionBlock.transactions = [
{
data: {
id: "43223de192d61a341301cc831a325ffe21d3e99666c023749bd4b562652f6796",
blockId: "10370119864814436559",
version: 1,
type: 3,
typeGroup: 1,
amount: Utils.BigNumber.ZERO,
fee: Utils.BigNumber.make("100000000"),
senderPublicKey: "0247b3911ddad3d24314afc621304755b054207abcd0493745d5469d6a986cef54",
recipientId: "AWMYLnbdVtGckhTzF8ZpMLEP3o3a24ZLBM",
signature:
"30440220538d262dc2636d3b78e4f1e903a732051c8082384290796a7b93ddcebb882d1f0220549464b55be0a64516431f25e40b7a45929f7892d8dbca9e0d3d8713b5e05f78",
asset: {
votes: ["+021f277f1e7a48c88f9c02988f06ca63d6f1781471f78dba49d58bab85eb3964c6"],
},
timestamp: 53231063,
nonce: Utils.BigNumber.ONE,
},
} as Interfaces.ITransaction,
];
exceptionBlock.data.numberOfTransactions = 1;

Managers.configManager.setFromPreset("mainnet");
expect(await blockProcessor.getHandler(exceptionBlock)).toBeInstanceOf(ExceptionHandler);
Managers.configManager.setFromPreset("testnet");
});

it("should return VerificationFailedHandler if block failed verification", async () => {
Managers.configManager.setFromPreset("mainnet");
const failedVerifBlock = BlockFactory.fromData(blockTemplate);
failedVerifBlock.verification.verified = false;

expect(await blockProcessor.getHandler(failedVerifBlock)).toBeInstanceOf(VerificationFailedHandler);
Managers.configManager.setFromPreset("testnet");
});
});

Expand Down
42 changes: 42 additions & 0 deletions __tests__/unit/crypto/blocks/block.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,48 @@ describe("Block", () => {
jest.restoreAllMocks();
});

it("should fail to verify a block with invalid S in signature (not low S value)", () => {
const block = BlockFactory.fromData({
id: "62b348a7aba2c60506929eec1311eaecb48ef232d4b154db2ede3f5e53700be9",
version: 0,
timestamp: 102041016,
height: 5470549,
reward: Utils.BigNumber.make("200000000"),
previousBlock: "2d270cae7e2bd9da27f6160b521859820f2c90315672e1774733bdd6415abb86",
numberOfTransactions: 0,
totalAmount: Utils.BigNumber.ZERO,
totalFee: Utils.BigNumber.ZERO,
payloadLength: 0,
payloadHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
generatorPublicKey: "026a423b3323de175dd82788c7eab57850c6a37ea6a470308ebadd7007baf8ceb3",
blockSignature:
"3045022100c92d7d0c3ea2ba72576f6494a81fc498d0420286896f806a7ead443d0b5d89720220501610f0d5498d028fd27676ea2597a5cb80cf5896e77fe2fa61623d31ff290c",
});

expect(block.verification.verified).toBeTrue();
expect(block.verification.errors).toEqual([]);

const blockHighS = BlockFactory.fromData({
id: "62b348a7aba2c60506929eec1311eaecb48ef232d4b154db2ede3f5e53700be9",
version: 0,
timestamp: 102041016,
height: 5470549,
reward: Utils.BigNumber.make("200000000"),
previousBlock: "2d270cae7e2bd9da27f6160b521859820f2c90315672e1774733bdd6415abb86",
numberOfTransactions: 0,
totalAmount: Utils.BigNumber.ZERO,
totalFee: Utils.BigNumber.ZERO,
payloadLength: 0,
payloadHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
generatorPublicKey: "026a423b3323de175dd82788c7eab57850c6a37ea6a470308ebadd7007baf8ceb3",
blockSignature:
"3045022100c92d7d0c3ea2ba72576f6494a81fc498d0420286896f806a7ead443d0b5d89720220afe9ef0f2ab672fd702d898915da6858ef2e0d8e18612058c570fc4f9e371835",
});

expect(blockHighS.verification.verified).toBeFalse();
expect(blockHighS.verification.errors).toEqual(["Failed to verify block signature"]);
});

it("should construct the block (header only)", () => {
const block = BlockFactory.fromHex(dummyBlock2.serialized);
const actual = block.toJson();
Expand Down
117 changes: 105 additions & 12 deletions __tests__/unit/crypto/utils/is-exception.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,114 @@ import { configManager } from "../../../../packages/crypto/src/managers";
import { isException } from "../../../../packages/crypto/src/utils";

describe("IsException", () => {
it("should return true", () => {
// @ts-ignore
configManager.get = jest.fn(() => ["1"]);
expect(isException({ id: "1" } as IBlockData)).toBeTrue();
const spyConfigGet = jest.spyOn(configManager, "get");
spyConfigGet.mockReturnValue(["d82ef1452ed61d9217c9b6a7328afa833586fdb19390c9c5e61b7801447428a5"]);

describe("when id is 64 bytes long", () => {
it("should return true when id is defined as an exception", () => {
const id = "d82ef1452ed61d9217c9b6a7328afa833586fdb19390c9c5e61b7801447428a5";

spyConfigGet.mockReturnValue([id]);

expect(isException({ id } as IBlockData)).toBeTrue();
});

it("should return false", () => {
const id = "d83ef1452ed61d9217c9b6a7328afa833586fdb19390c9c5e61b7801447428a5";
spyConfigGet.mockReturnValue(["1"]);
expect(isException({ id } as IBlockData)).toBeFalse();

spyConfigGet.mockReturnValue(undefined);
expect(isException({ id } as IBlockData)).toBeFalse();

spyConfigGet.mockReturnValue(undefined);
expect(isException({ id: undefined } as IBlockData)).toBeFalse();
});
});

it("should return false", () => {
// @ts-ignore
configManager.get = jest.fn(() => ["1"]);
expect(isException({ id: "2" } as IBlockData)).toBeFalse();
describe("when id is < 64 bytes long (old block ids)", () => {
it.each([
[
"72d9217c9b6a7328afa833586fdb19390c9c5e61b7801447428a5",
["b9fdb54370ac2334790942738784063475db70d5564598dbd714681bb02e3034"],
],
["73d9217c9b6a7328afa833586fdb19390c9c5e61b7801447428a5", []],
[
"74d9217c9b6a7328afa833586fdb19390c9c5e61b7801447428a5",
[
"b8fdb54370ac2334790942738784063475db70d5564598dbd714681bb02e3034",
"b7fdb54370ac2334790942738784063475db70d5564598dbd714681bb02e3034",
"b6fdb54370ac2334790942738784063475db70d5564598dbd714681bb02e3034",
],
],
])(
"should return true when block id is defined as an exception along with its transactions",
(blockId: string, txs: string[]) => {
spyConfigGet
.mockReturnValueOnce([blockId])
.mockReturnValueOnce({ [blockId]: txs })
.mockReturnValueOnce([blockId]);

expect(isException({ id: blockId, transactions: txs.map(id => ({ id })) } as IBlockData)).toBeTrue();
},
);

it("should return true when block exception transactions are in different order than the ones to check", () => {
const blockId = "83d9217c9b6a7328afa833586fdb19390c9c5e61b7801447428a5";
const txs = [
"b1fdb54370ac2334790942738784063475db70d5564598dbd714681bb02e3034",
"b2fdb54370ac2334790942738784063475db70d5564598dbd714681bb02e3034",
"b3fdb54370ac2334790942738784063475db70d5564598dbd714681bb02e3034",
];
const txsShuffled = [txs[1], txs[0], txs[2]];

spyConfigGet
.mockReturnValueOnce([blockId])
.mockReturnValueOnce({ [blockId]: txs })
.mockReturnValueOnce([blockId]);

expect(
isException({ id: blockId, transactions: txsShuffled.map(id => ({ id })) } as IBlockData),
).toBeTrue();
});

it("should return true when transactions is undefined and block transactions exceptions are empty array", () => {
const blockId = "63d9217c9b6a7328afa833586fdb19390c9c5e61b7801447428a5";

spyConfigGet
.mockReturnValueOnce([blockId])
.mockReturnValueOnce({ [blockId]: [] })
.mockReturnValueOnce([blockId]);

expect(isException({ id: blockId, transactions: undefined } as IBlockData)).toBeTrue();
});

it("should return false when transactions is undefined and there are transactions defined in exception", () => {
const blockId = "63d9217c9b6a7328afa833586fdb19390c9c5e61b7801447428a5";

spyConfigGet
.mockReturnValueOnce([blockId])
.mockReturnValueOnce({
[blockId]: ["b9fdb54270ac2334790942345784066875db70d5564598dbd714681bb02e3034"],
})
.mockReturnValueOnce([blockId]);

expect(isException({ id: blockId, transactions: undefined } as IBlockData)).toBeFalse();
});

it("should return false when transactions length is different than exception transactions length", () => {
const blockId = "64d9217c9b6a7328afa833586fdb19390c9c5e61b7801447428a5";
const transactions = [
"b9fdb54270ac2334790942738784066875db70d5564598dbd714681bb02e3034",
"b9fdb87970ac2334790942738784066875db70d5564598dbd714681bb02e3034",
];

configManager.get = jest.fn(() => undefined);
expect(isException({ id: "2" } as IBlockData)).toBeFalse();
spyConfigGet
.mockReturnValueOnce([blockId])
.mockReturnValueOnce({ [blockId]: transactions })
.mockReturnValueOnce([blockId]);

configManager.get = jest.fn(() => undefined);
expect(isException({ id: undefined } as IBlockData)).toBeFalse();
expect(isException({ id: blockId, transactions: [{ id: transactions[0] }] } as IBlockData)).toBeFalse();
});
});
});
Loading

0 comments on commit 54ba2d2

Please sign in to comment.