From 54ba2d211d9bfcf63c94f13608a7f647fedd08b9 Mon Sep 17 00:00:00 2001 From: air1one <36802613+air1one@users.noreply.github.com> Date: Wed, 17 Jun 2020 10:05:39 +0200 Subject: [PATCH] fix(core-blockchain): block schema violation (#3806) --- .../core-blockchain/blockchain.test.ts | 10 +- .../core-p2p/network-monitor.test.ts | 3 + .../unit/core-blockchain/blockchain.test.ts | 36 ++---- .../processor/block-processor.test.ts | 29 ++++- __tests__/unit/crypto/blocks/block.test.ts | 42 +++++++ .../unit/crypto/utils/is-exception.test.ts | 117 ++++++++++++++++-- packages/core-blockchain/src/blockchain.ts | 19 ++- .../src/processor/block-processor.ts | 2 +- .../src/replay/replay-blockchain.ts | 11 +- packages/core-container/src/config/index.ts | 1 + packages/core-p2p/src/network-monitor.ts | 40 +++--- packages/core-p2p/src/peer-communicator.ts | 3 + packages/crypto/src/crypto/hash.ts | 10 +- .../src/networks/devnet/exceptions.json | 108 +++++++++++----- .../src/networks/mainnet/exceptions.json | 3 + packages/crypto/src/utils/index.ts | 27 +++- 16 files changed, 352 insertions(+), 109 deletions(-) diff --git a/__tests__/integration/core-blockchain/blockchain.test.ts b/__tests__/integration/core-blockchain/blockchain.test.ts index 04d960a595..3ee89516b4 100644 --- a/__tests__/integration/core-blockchain/blockchain.test.ts +++ b/__tests__/integration/core-blockchain/blockchain.test.ts @@ -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]); } }; @@ -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); @@ -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); @@ -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)); @@ -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)); diff --git a/__tests__/integration/core-p2p/network-monitor.test.ts b/__tests__/integration/core-p2p/network-monitor.test.ts index 20d0dd1ec5..54383a6cb4 100644 --- a/__tests__/integration/core-p2p/network-monitor.test.ts +++ b/__tests__/integration/core-p2p/network-monitor.test.ts @@ -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"; @@ -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); }); }); diff --git a/__tests__/unit/core-blockchain/blockchain.test.ts b/__tests__/unit/core-blockchain/blockchain.test.ts index 9b68bb8e65..7e65bb383a 100644 --- a/__tests__/unit/core-blockchain/blockchain.test.ts +++ b/__tests__/unit/core-blockchain/blockchain.test.ts @@ -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"; @@ -113,55 +112,46 @@ 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") @@ -169,10 +159,8 @@ describe("Blockchain", () => { 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(); diff --git a/__tests__/unit/core-blockchain/processor/block-processor.test.ts b/__tests__/unit/core-blockchain/processor/block-processor.test.ts index e77ef4a005..3dea1966f1 100644 --- a/__tests__/unit/core-blockchain/processor/block-processor.test.ts +++ b/__tests__/unit/core-blockchain/processor/block-processor.test.ts @@ -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 { @@ -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"); }); }); diff --git a/__tests__/unit/crypto/blocks/block.test.ts b/__tests__/unit/crypto/blocks/block.test.ts index cdf2f4757f..7893efc03c 100644 --- a/__tests__/unit/crypto/blocks/block.test.ts +++ b/__tests__/unit/crypto/blocks/block.test.ts @@ -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(); diff --git a/__tests__/unit/crypto/utils/is-exception.test.ts b/__tests__/unit/crypto/utils/is-exception.test.ts index 2b4f160380..94dd764f8a 100644 --- a/__tests__/unit/crypto/utils/is-exception.test.ts +++ b/__tests__/unit/crypto/utils/is-exception.test.ts @@ -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(); + }); }); }); diff --git a/packages/core-blockchain/src/blockchain.ts b/packages/core-blockchain/src/blockchain.ts index 462ef357c6..abb4f7aead 100644 --- a/packages/core-blockchain/src/blockchain.ts +++ b/packages/core-blockchain/src/blockchain.ts @@ -84,15 +84,14 @@ export class Blockchain implements blockchain.IBlockchain { this.actions = stateMachine.actionMap(this); this.blockProcessor = new BlockProcessor(this); - this.queue = async.queue((blockList: { blocks: Interfaces.IBlockData[] }, cb) => { + this.queue = async.queue(async (blockList: { blocks: Interfaces.IBlockData[] }) => { try { - return this.processBlocks(blockList.blocks, cb); + return await this.processBlocks(blockList.blocks); } catch (error) { logger.error( `Failed to process ${blockList.blocks.length} blocks from height ${blockList.blocks[0].height} in queue.`, ); - logger.error(error.stack); - return cb(); + return undefined; } }, 1); @@ -298,8 +297,6 @@ export class Blockchain implements blockchain.IBlockchain { } } this.queue.push({ blocks: currentBlocksChunk }); - - this.state.lastDownloadedBlock = blocks.slice(-1)[0]; } /** @@ -402,7 +399,7 @@ export class Blockchain implements blockchain.IBlockchain { /** * Process the given block. */ - public async processBlocks(blocks: Interfaces.IBlockData[], callback): Promise { + public async processBlocks(blocks: Interfaces.IBlockData[]): Promise { const acceptedBlocks: Interfaces.IBlock[] = []; let lastProcessResult: BlockProcessorResult; @@ -414,7 +411,7 @@ export class Blockchain implements blockchain.IBlockchain { // Discard remaining blocks as it won't go anywhere anyway. this.clearQueue(); this.resetLastDownloadedBlock(); - return callback(); + return undefined; } let forkBlock: Interfaces.IBlock; @@ -426,9 +423,11 @@ export class Blockchain implements blockchain.IBlockchain { if (lastProcessResult === BlockProcessorResult.Accepted) { acceptedBlocks.push(blockInstance); + this.state.lastDownloadedBlock = blockInstance.data; } else { if (lastProcessResult === BlockProcessorResult.Rollback) { forkBlock = blockInstance; + this.state.lastDownloadedBlock = blockInstance.data; } break; // if one block is not accepted, the other ones won't be chained anyway @@ -464,7 +463,7 @@ export class Blockchain implements blockchain.IBlockchain { await this.database.deleteRound(deleteRoundsAfter + 1); await this.database.loadBlocksFromCurrentRound(); - return callback(); + return undefined; } } @@ -482,7 +481,7 @@ export class Blockchain implements blockchain.IBlockchain { this.forkBlock(forkBlock); } - return callback(acceptedBlocks); + return acceptedBlocks; } /** diff --git a/packages/core-blockchain/src/processor/block-processor.ts b/packages/core-blockchain/src/processor/block-processor.ts index 86c513a714..0bb6333fb9 100644 --- a/packages/core-blockchain/src/processor/block-processor.ts +++ b/packages/core-blockchain/src/processor/block-processor.ts @@ -36,7 +36,7 @@ export class BlockProcessor { } public async getHandler(block: Interfaces.IBlock): Promise { - if (Utils.isException(block.data)) { + if (Utils.isException({ ...block.data, transactions: block.transactions.map(tx => tx.data) })) { return new ExceptionHandler(this.blockchain, block); } diff --git a/packages/core-blockchain/src/replay/replay-blockchain.ts b/packages/core-blockchain/src/replay/replay-blockchain.ts index 36b8f2f857..1d71b63f6a 100644 --- a/packages/core-blockchain/src/replay/replay-blockchain.ts +++ b/packages/core-blockchain/src/replay/replay-blockchain.ts @@ -73,13 +73,12 @@ export class ReplayBlockchain extends Blockchain { const blocks: Interfaces.IBlockData[] = await this.fetchBatch(startHeight, batch, lastAcceptedHeight); - this.processBlocks(blocks, async (acceptedBlocks: Interfaces.IBlock[]) => { - if (acceptedBlocks.length !== blocks.length) { - throw new FailedToReplayBlocksError(); - } + const acceptedBlocks: Interfaces.IBlock[] = await this.processBlocks(blocks); + if (acceptedBlocks.length !== blocks.length) { + throw new FailedToReplayBlocksError(); + } - await replayBatch(batch + 1, acceptedBlocks[acceptedBlocks.length - 1].data.height); - }); + await replayBatch(batch + 1, acceptedBlocks[acceptedBlocks.length - 1].data.height); }; await replayBatch(1); diff --git a/packages/core-container/src/config/index.ts b/packages/core-container/src/config/index.ts index c7a172af2f..f3c534ea2e 100644 --- a/packages/core-container/src/config/index.ts +++ b/packages/core-container/src/config/index.ts @@ -53,6 +53,7 @@ export class Config { .required(), exceptions: Joi.object({ blocks: Joi.array().items(Joi.string()), + blocksTransactions: Joi.object(), transactions: Joi.array().items(Joi.string()), outlookTable: Joi.object(), transactionIdFixTable: Joi.object(), diff --git a/packages/core-p2p/src/network-monitor.ts b/packages/core-p2p/src/network-monitor.ts index b0140a8142..cfa30073e9 100644 --- a/packages/core-p2p/src/network-monitor.ts +++ b/packages/core-p2p/src/network-monitor.ts @@ -154,26 +154,32 @@ export class NetworkMonitor implements P2P.INetworkMonitor { this.logger.info(`Checking ${max} peers`); const peerErrors = {}; - await Promise.all( - peers.map(async peer => { - try { - await this.communicator.ping(peer, pingDelay, forcePing); - } catch (error) { - unresponsivePeers++; - if (peerErrors[error]) { - peerErrors[error].push(peer); - } else { - peerErrors[error] = [peer]; - } + // we use Promise.race to cut loose in case some communicator.ping() does not resolve within the delay + // in that case we want to keep on with our program execution while ping promises can finish in the background + await Promise.race([ + Promise.all( + peers.map(async peer => { + try { + await this.communicator.ping(peer, pingDelay, forcePing); + } catch (error) { + unresponsivePeers++; - this.emitter.emit("internal.p2p.disconnectPeer", { peer }); - this.emitter.emit(ApplicationEvents.PeerRemoved, peer); + if (peerErrors[error]) { + peerErrors[error].push(peer); + } else { + peerErrors[error] = [peer]; + } - return undefined; - } - }), - ); + this.emitter.emit("internal.p2p.disconnectPeer", { peer }); + this.emitter.emit(ApplicationEvents.PeerRemoved, peer); + + return undefined; + } + }), + ), + delay(pingDelay), + ]); for (const key of Object.keys(peerErrors)) { const peerCount = peerErrors[key].length; diff --git a/packages/core-p2p/src/peer-communicator.ts b/packages/core-p2p/src/peer-communicator.ts index a970ab6bb0..99236058b8 100644 --- a/packages/core-p2p/src/peer-communicator.ts +++ b/packages/core-p2p/src/peer-communicator.ts @@ -49,6 +49,9 @@ export class PeerCommunicator implements P2P.IPeerCommunicator { return this.emit(peer, "p2p.peer.postTransactions", { transactions }, postTransactionsTimeout); } + // ! do not rely on parameter timeoutMsec as guarantee that ping method will resolve within it ! + // ! peerVerifier.checkState can take more time ! + // TODO refactor on next version ? public async ping(peer: P2P.IPeer, timeoutMsec: number, force: boolean = false): Promise { const deadline = new Date().getTime() + timeoutMsec; diff --git a/packages/crypto/src/crypto/hash.ts b/packages/crypto/src/crypto/hash.ts index 8b4c2638b4..cc1750ce38 100644 --- a/packages/crypto/src/crypto/hash.ts +++ b/packages/crypto/src/crypto/hash.ts @@ -7,9 +7,17 @@ export class Hash { } public static verifyECDSA(hash: Buffer, signature: Buffer | string, publicKey: Buffer | string): boolean { + const signatureRS = secp256k1.signatureImport( + signature instanceof Buffer ? signature : Buffer.from(signature, "hex"), + ); + + if (!secp256k1.isLowS(signatureRS)) { + return false; + } + return secp256k1.verify( hash, - secp256k1.signatureImport(signature instanceof Buffer ? signature : Buffer.from(signature, "hex")), + signatureRS, publicKey instanceof Buffer ? publicKey : Buffer.from(publicKey, "hex"), ); } diff --git a/packages/crypto/src/networks/devnet/exceptions.json b/packages/crypto/src/networks/devnet/exceptions.json index 0f57248aec..721559dadf 100644 --- a/packages/crypto/src/networks/devnet/exceptions.json +++ b/packages/crypto/src/networks/devnet/exceptions.json @@ -72,34 +72,6 @@ "15836524583901486981", "12478859533758330380", "13701809340863213986", - "4296553458016414976", - "6837659293375391985", - "16540521480028827748", - "1485997193168364918", - "14159698257459587584", - "6247200360319694668", - "7363268091423233950", - "9014317427571908796", - "15519361274991733193", - "12603272471546364995", - "1944108005996955253", - "8469356042757089608", - "2997965849869498353", - "9196430932294555781", - "5806654366498055250", - "13290912469992409149", - "9502002558776276513", - "330791153715252718", - "7079194814443264009", - "15946707936026547597", - "1641736062116508620", - "5245034769798442586", - "2565729258675312304", - "12614646598841308905", - "8274406339991077743", - "1661383348822169561", - "15467742607784975524", - "3665174254391236833", "17417028847837598792", "14220651316552198137", "13101468344291730322", @@ -521,5 +493,83 @@ "acc5a085fc12c91bba07b6ebb651111fdeca1b78a226dc9f7438715ddd6b6754", "86cdb2493926befea4a55f31713378eba1839699f1c66d3fc46c909df38bc912", "06b91270143cdacc94cddf46b99d60463b96c529c75a3c04cf7101c7a44b0bf8" - ] + ], + "blocksTransactions": { + "15895730198424359628": [], + "14746174532446639362": [], + "15249141324902969334": [], + "12360802297474246584": [], + "2565729258675312304": [], + "12614646598841308905": [], + "8274406339991077743": [], + "1661383348822169561": [], + "15467742607784975524": [], + "3665174254391236833": [], + "18033869253067308940": [], + "9121030900295704150": [], + "4296553458016414976": [], + "6837659293375391985": [], + "16540521480028827748": [], + "1485997193168364918": [], + "14159698257459587584": [], + "7561147498738550191": [], + "6247200360319694668": [], + "7363268091423233950": [], + "8738693892321921533": [], + "9014317427571908796": [], + "15519361274991733193": [], + "14013227271822852495": [], + "12603272471546364995": [], + "1944108005996955253": [], + "8469356042757089608": [], + "3433946900869474802": [], + "11257633501887013743": [], + "2997965849869498353": [], + "9196430932294555781": [], + "6730395143580220680": [], + "5806654366498055250": [], + "13290912469992409149": [], + "9502002558776276513": [], + "330791153715252718": [], + "12084096509112875921": [], + "7079194814443264009": [], + "15946707936026547597": [], + "1641736062116508620": [], + "5245034769798442586": [], + "4073147595542846301": [], + "11129434526540201266": [], + "15355810214343508168": [], + "834201289153220685": [], + "4785149476172130294": [], + "9808224912335721998": [], + "11229968119222422821": [], + "6766557974469507237": [], + "2066948671330348076": [], + "13308773643111727094": [], + "15649739201370841265": [], + "17287484123727410951": [], + "1739406121453748889": [], + "16969775483726255451": [], + "5174570296595098048": [], + "10957882104586895269": [], + "16222316251056394079": [], + "11019993339496601918": [], + "7648775833276915174": [], + "5947225658884952613": [], + "17256370470460685782": [], + "5681801935518263609": [], + "6853934810393582972": [], + "621694479387726255": [], + "649083198759873217": [], + "4052333663180604671": [], + "5348794590580429562": [], + "7723209448992965570": [], + "15836524583901486981": [], + "12478859533758330380": [], + "13701809340863213986": [], + "17417028847837598792": [], + "14220651316552198137": [], + "13101468344291730322": [], + "6671890826474701031": [] + } } diff --git a/packages/crypto/src/networks/mainnet/exceptions.json b/packages/crypto/src/networks/mainnet/exceptions.json index a3a5dc0fb0..4af966f8fb 100644 --- a/packages/crypto/src/networks/mainnet/exceptions.json +++ b/packages/crypto/src/networks/mainnet/exceptions.json @@ -1,5 +1,8 @@ { "blocks": ["10370119864814436559"], + "blocksTransactions": { + "10370119864814436559": ["43223de192d61a341301cc831a325ffe21d3e99666c023749bd4b562652f6796"] + }, "transactions": [ "608c7aeba0895da4517496590896eb325a0b5d367e1b186b1c07d7651a568b9e", "43223de192d61a341301cc831a325ffe21d3e99666c023749bd4b562652f6796", diff --git a/packages/crypto/src/utils/index.ts b/packages/crypto/src/utils/index.ts index 6702f4a1e5..248aec88bd 100644 --- a/packages/crypto/src/utils/index.ts +++ b/packages/crypto/src/utils/index.ts @@ -1,5 +1,6 @@ import memoize from "fast-memoize"; import { SATOSHI } from "../constants"; +import { ITransactionData } from "../interfaces"; import { configManager } from "../managers/config"; import { Base58 } from "./base58"; import { BigNumber } from "./bignum"; @@ -42,8 +43,32 @@ export const formatSatoshi = (amount: BigNumber): string => { /** * Check if the given block or transaction id is an exception. */ -export const isException = (blockOrTransaction: { id?: string }): boolean => { +export const isException = (blockOrTransaction: { id?: string; transactions?: ITransactionData[] }): boolean => { const network: number = configManager.get("network"); + + if (typeof blockOrTransaction.id !== "string") { + return false; + } + + if (blockOrTransaction.id.length < 64) { + // old block ids, we check that the transactions inside the block are correct + const blockExceptionTxIds: string[] = (configManager.get("exceptions.blocksTransactions") || {})[ + blockOrTransaction.id + ]; + const blockTransactions = blockOrTransaction.transactions || []; + if (!blockExceptionTxIds || blockExceptionTxIds.length !== blockTransactions.length) { + return false; + } + + blockExceptionTxIds.sort(); + const blockToCheckTxIds = blockTransactions.map(tx => tx.id).sort(); + for (let i = 0; i < blockExceptionTxIds.length; i++) { + if (blockToCheckTxIds[i] !== blockExceptionTxIds[i]) { + return false; + } + } + } + return getExceptionIds(network).has(blockOrTransaction.id); };