Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: cope with dynamic round sizes #2370

Merged
merged 21 commits into from Apr 8, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions __tests__/integration/core-blockchain/blockchain.test.ts
@@ -1,5 +1,6 @@
/* tslint:disable:max-line-length */
import { Wallet } from "@arkecosystem/core-database";
import { roundCalculator } from "@arkecosystem/core-utils";
import {
Bignum,
crypto,
Expand Down Expand Up @@ -123,8 +124,9 @@ describe("Blockchain", () => {
await __addBlocks(55);

// Pretend blockchain just started
const roundInfo = roundCalculator.calculateRound(blockchain.getLastHeight());
await blockchain.database.restoreCurrentRound(blockchain.getLastHeight());
const forgingDelegates = await blockchain.database.getActiveDelegates(blockchain.getLastHeight());
const forgingDelegates = await blockchain.database.getActiveDelegates(roundInfo);
expect(forgingDelegates).toHaveLength(51);

// Reset again and replay to round 2. In both cases the forging delegates
Expand Down Expand Up @@ -153,7 +155,8 @@ describe("Blockchain", () => {

const getNextForger = async () => {
const lastBlock = blockchain.state.getLastBlock();
const activeDelegates = await blockchain.database.getActiveDelegates(lastBlock.data.height);
const roundInfo = roundCalculator.calculateRound(lastBlock.data.height);
const activeDelegates = await blockchain.database.getActiveDelegates(roundInfo);
const nextSlot = slots.getSlotNumber(lastBlock.data.timestamp) + 1;
return activeDelegates[nextSlot % activeDelegates.length];
};
Expand Down
1 change: 1 addition & 0 deletions __tests__/unit/core-blockchain/mocks/container.ts
Expand Up @@ -9,6 +9,7 @@ export const container = {
app: {
getConfig: () => {
return {
config: { milestones: [{ activeDelegates: 51, height: 1 }] },
get: key => config[key],
getMilestone: () => ({
activeDelegates: 51,
Expand Down
2 changes: 1 addition & 1 deletion __tests__/unit/core-blockchain/state-machine.test.ts
@@ -1,11 +1,11 @@
import "../../utils";
import "./mocks/";

import { roundCalculator } from "@arkecosystem/core-utils";
import { slots } from "@arkecosystem/crypto";
import { Block } from "@arkecosystem/crypto/dist/models";
import { config as localConfig } from "../../../packages/core-blockchain/src/config";
import { stateStorage } from "../../../packages/core-blockchain/src/state-storage";
import "../../utils";
import genesisBlockJSON from "../../utils/config/testnet/genesisBlock.json";
import { blockchain } from "./mocks/blockchain";
import { config } from "./mocks/config";
Expand Down
9 changes: 6 additions & 3 deletions __tests__/unit/core-database/database-service.test.ts
Expand Up @@ -7,6 +7,7 @@ import { TransactionHandlerRegistry } from "@arkecosystem/core-transactions";
import { Address, Bignum, constants, models, Transaction, transactionBuilder } from "@arkecosystem/crypto";
import { Wallet, WalletManager } from "../../../packages/core-database/src";
import { DatabaseService } from "../../../packages/core-database/src/database-service";
import { roundCalculator } from "../../../packages/core-utils/dist";
import { genesisBlock } from "../../utils/fixtures/testnet/block-model";
import { DatabaseConnectionStub } from "./__fixtures__/database-connection-stub";
import { StateStorageStub } from "./__fixtures__/state-storage-stub";
Expand Down Expand Up @@ -205,7 +206,8 @@ describe("Database Service", () => {
};

// Beginning of round 2 with all delegates 0 vote balance.
const delegatesRound2 = walletManager.loadActiveDelegateList(initialHeight);
const roundInfo1 = roundCalculator.calculateRound(initialHeight);
const delegatesRound2 = walletManager.loadActiveDelegateList(roundInfo1);

// Prepare sender wallet
const transactionHandler = TransactionHandlerRegistry.get(TransactionTypes.Transfer);
Expand Down Expand Up @@ -254,7 +256,8 @@ describe("Database Service", () => {
}

// The delegates from round 2 are now reversed in rank in round 3.
const delegatesRound3 = walletManager.loadActiveDelegateList(initialHeight + 51);
const roundInfo2 = roundCalculator.calculateRound(initialHeight + 51);
const delegatesRound3 = walletManager.loadActiveDelegateList(roundInfo2);
for (let i = 0; i < delegatesRound3.length; i++) {
expect(delegatesRound3[i].rate).toBe(i + 1);
expect(delegatesRound3[i].publicKey).toBe(delegatesRound2[delegatesRound3.length - i - 1].publicKey);
Expand All @@ -273,7 +276,7 @@ describe("Database Service", () => {
});

// Finally recalculate the round 2 list and compare against the original list
const restoredDelegatesRound2 = await (databaseService as any).calcPreviousActiveDelegates(2);
const restoredDelegatesRound2 = await (databaseService as any).calcPreviousActiveDelegates(roundInfo2);

for (let i = 0; i < restoredDelegatesRound2.length; i++) {
expect(restoredDelegatesRound2[i].rate).toBe(i + 1);
Expand Down
1 change: 1 addition & 0 deletions __tests__/unit/core-database/mocks/core-container.ts
Expand Up @@ -5,6 +5,7 @@ jest.mock("@arkecosystem/core-container", () => {
app: {
getConfig: () => {
return {
config: { milestones: [{ activeDelegates: 51, height: 1 }] },
get: () => ({}),
getMilestone: () => ({
activeDelegates: 51,
Expand Down
1 change: 1 addition & 0 deletions __tests__/unit/core-p2p/mocks/core-container.ts
Expand Up @@ -17,6 +17,7 @@ jest.mock("@arkecosystem/core-container", () => {

return null;
},
config: { milestones: [{ activeDelegates: 51, height: 1 }] },
getMilestone: () => ({
activeDelegates: 51,
}),
Expand Down
2 changes: 2 additions & 0 deletions __tests__/unit/core-utils/mocks/core-container.ts
Expand Up @@ -3,9 +3,11 @@ jest.mock("@arkecosystem/core-container", () => {
app: {
getConfig: () => {
return {
config: { milestones: [{ activeDelegates: 51, height: 1 }] },
getMilestone: () => ({
epoch: "2017-03-21T13:00:00.000Z",
activeDelegates: 51,
height: 1,
}),
};
},
Expand Down
131 changes: 122 additions & 9 deletions __tests__/unit/core-utils/round-calculator.test.ts
@@ -1,29 +1,142 @@
import "jest-extended";
import "./mocks/core-container";

import { app } from "@arkecosystem/core-container";
import "jest-extended";
import { calculateRound, isNewRound } from "../../../packages/core-utils/src/round-calculator";

describe("Round calculator", () => {
describe("calculateRound", () => {
it("should calculate the round when nextRound is the same", () => {
const { round, nextRound } = calculateRound(1);
expect(round).toBe(1);
expect(nextRound).toBe(1);
describe("static delegate count", () => {
it("should calculate the round when nextRound is the same", () => {
for (let i = 0, height = 51; i < 1000; i++, height += 51) {
const { round, nextRound } = calculateRound(height - 1);
expect(round).toBe(i + 1);
expect(nextRound).toBe(i + 1);
}
});

it("should calculate the round when nextRound is not the same", () => {
for (let i = 0, height = 51; i < 1000; i++, height += 51) {
const { round, nextRound } = calculateRound(height);
expect(round).toBe(i + 1);
expect(nextRound).toBe(i + 2);
}
});

it("should calculate the correct round", () => {
const activeDelegates = 51;
for (let i = 0; i < 1000; i++) {
const { round, nextRound } = calculateRound(i + 1);
expect(round).toBe(Math.floor(i / activeDelegates) + 1);
expect(nextRound).toBe(Math.floor((i + 1) / activeDelegates) + 1);
}
});
});

it("should calculate the round when nextRound is not the same", () => {
const { round, nextRound } = calculateRound(51);
expect(round).toBe(1);
expect(nextRound).toBe(2);
describe("dynamic delegate count", () => {
it("should calculate the correct with dynamic delegate count", () => {
const testVector = [
{ height: 1, round: 1, roundHeight: 1, nextRound: 1, activeDelegates: 2 },
{ height: 3, round: 2, roundHeight: 3, nextRound: 2, activeDelegates: 3 },
{ height: 6, round: 3, roundHeight: 6, nextRound: 4, activeDelegates: 1 },
{ height: 10, round: 7, roundHeight: 10, nextRound: 7, activeDelegates: 51 },
{ height: 112, round: 9, roundHeight: 112, nextRound: 10, activeDelegates: 1 },
{ height: 115, round: 12, roundHeight: 115, nextRound: 12, activeDelegates: 2 },
{ height: 131, round: 20, roundHeight: 131, nextRound: 20, activeDelegates: 51 },
];

const milestones = testVector.reduce((acc, vector) => acc.set(vector.height, vector), new Map());

const backup = app.getConfig;
app.getConfig = jest.fn(() => {
return {
config: {
milestones: Array.from(milestones.values()),
},
getMilestone: height => {
return milestones.get(height);
},
};
});

testVector.forEach(({ height, round, roundHeight, nextRound, activeDelegates }) => {
const result = calculateRound(height);
expect(result.round).toBe(round);
expect(result.roundHeight).toBe(roundHeight);
expect(isNewRound(result.roundHeight)).toBeTrue();
expect(result.nextRound).toBe(nextRound);
expect(result.maxDelegates).toBe(activeDelegates);
});

app.getConfig = backup;
});
});
});

describe("isNewRound", () => {
const setMilestones = milestones => {
app.getConfig = jest.fn(() => {
return {
config: {
milestones,
},
getMilestone: height => {
for (let i = milestones.length - 1; i >= 0; i--) {
if (milestones[i].height <= height) {
return milestones[i];
}
}

return milestones[0];
},
};
});
};

it("should determine the beginning of a new round", () => {
expect(isNewRound(1)).toBeTrue();
expect(isNewRound(2)).toBeFalse();
expect(isNewRound(52)).toBeTrue();
expect(isNewRound(53)).toBeFalse();
expect(isNewRound(54)).toBeFalse();
expect(isNewRound(103)).toBeTrue();
expect(isNewRound(104)).toBeFalse();
expect(isNewRound(154)).toBeTrue();
});

it("should be ok when changing delegate count", () => {
const milestones = [
{ height: 1, activeDelegates: 2 }, // R1
{ height: 3, activeDelegates: 3 }, // R2
{ height: 6, activeDelegates: 1 }, // R3
{ height: 10, activeDelegates: 51 }, // R7
{ height: 62, activeDelegates: 51 }, // R8
];

const backup = app.getConfig;
setMilestones(milestones);

// 2 Delegates
expect(isNewRound(1)).toBeTrue();
expect(isNewRound(2)).toBeFalse();

// 3 Delegates
expect(isNewRound(3)).toBeTrue();
expect(isNewRound(4)).toBeFalse();
expect(isNewRound(5)).toBeFalse();

// 1 Delegate
expect(isNewRound(6)).toBeTrue();
expect(isNewRound(7)).toBeTrue();
expect(isNewRound(8)).toBeTrue();
expect(isNewRound(9)).toBeTrue();

// 51 Delegates
expect(isNewRound(10)).toBeTrue();
expect(isNewRound(11)).toBeFalse();
expect(isNewRound(61)).toBeTrue();

app.getConfig = backup;
});
});
});
6 changes: 0 additions & 6 deletions __tests__/unit/crypto/crypto/slots.test.ts
Expand Up @@ -88,12 +88,6 @@ describe("Slots", () => {
});
});

describe("getLastSlot", () => {
it("returns last slot", () => {
expect(slots.getLastSlot(1)).toBe(52);
});
});

describe("isForgingAllowed", () => {
it("returns boolean", () => {
expect(slots.isForgingAllowed()).toBeDefined();
Expand Down
4 changes: 3 additions & 1 deletion packages/core-api/src/versions/1/delegates/controller.ts
@@ -1,3 +1,4 @@
import { roundCalculator } from "@arkecosystem/core-utils";
import { slots } from "@arkecosystem/crypto";
import Boom from "boom";
import Hapi from "hapi";
Expand Down Expand Up @@ -95,7 +96,8 @@ export class DelegatesController extends Controller {
const delegatesCount = this.config.getMilestone(lastBlock).activeDelegates;
const currentSlot = slots.getSlotNumber(lastBlock.data.timestamp);

const activeDelegates = await this.databaseService.getActiveDelegates(lastBlock.data.height);
const roundInfo = roundCalculator.calculateRound(lastBlock.data.height);
const activeDelegates = await this.databaseService.getActiveDelegates(roundInfo);
const nextForgers = [];

for (let i = 1; i <= delegatesCount && i <= limit; i++) {
Expand Down
3 changes: 2 additions & 1 deletion packages/core-api/src/versions/1/delegates/schema.ts
@@ -1,5 +1,6 @@
import { app } from "@arkecosystem/core-container";
import { Blockchain } from "@arkecosystem/core-interfaces";
import { roundCalculator } from "@arkecosystem/core-utils";

const lastBlock = app.resolvePlugin<Blockchain.IBlockchain>("blockchain").getLastBlock();

Expand Down Expand Up @@ -63,7 +64,7 @@ export const getDelegates: object = {
limit: {
type: "integer",
minimum: 1,
maximum: lastBlock ? app.getConfig().getMilestone(lastBlock.data.height).activeDelegates : 51,
maximum: lastBlock ? roundCalculator.calculateRound(lastBlock.data.height).maxDelegates : 51,
},
offset: {
type: "integer",
Expand Down
@@ -1,6 +1,7 @@
// tslint:disable:max-classes-per-file

import { app } from "@arkecosystem/core-container";
import { roundCalculator } from "@arkecosystem/core-utils";
import { models } from "@arkecosystem/crypto";
import { Blockchain } from "../../blockchain";
import { BlockProcessorResult } from "../block-processor";
Expand Down Expand Up @@ -67,7 +68,8 @@ export class UnchainedHandler extends BlockHandler {
switch (status) {
case UnchainedBlockStatus.DoubleForging: {
const database = app.resolvePlugin("database");
const delegates = await database.getActiveDelegates(this.block.data.height);
const roundInfo = roundCalculator.calculateRound(this.block.data.height);
const delegates = await database.getActiveDelegates(roundInfo);
if (delegates.some(delegate => delegate.publicKey === this.block.data.generatorPublicKey)) {
this.blockchain.forkBlock(this.block);
}
Expand Down
22 changes: 11 additions & 11 deletions packages/core-blockchain/src/state-machine.ts
Expand Up @@ -168,6 +168,17 @@ blockchainMachine.actionMap = (blockchain: Blockchain) => ({
stateStorage.setLastBlock(block);
stateStorage.lastDownloadedBlock = block;

// NOTE: if the node is shutdown between round, the round has already been applied
if (roundCalculator.isNewRound(block.data.height + 1)) {
const { round } = roundCalculator.calculateRound(block.data.height + 1);

logger.info(
`New round ${round.toLocaleString()} detected. Cleaning calculated data before restarting!`,
);

await blockchain.database.deleteRound(round);
}

if (stateStorage.networkStart) {
await blockchain.database.buildWallets();
await blockchain.database.applyRound(block.data.height);
Expand Down Expand Up @@ -198,17 +209,6 @@ blockchainMachine.actionMap = (blockchain: Blockchain) => ({
);
}

// NOTE: if the node is shutdown between round, the round has already been applied
if (roundCalculator.isNewRound(block.data.height + 1)) {
const { round } = roundCalculator.calculateRound(block.data.height + 1);

logger.info(
`New round ${round.toLocaleString()} detected. Cleaning calculated data before restarting!`,
);

await blockchain.database.deleteRound(round);
}

await blockchain.database.restoreCurrentRound(block.data.height);
await blockchain.transactionPool.buildWallets();

Expand Down
4 changes: 3 additions & 1 deletion packages/core-blockchain/src/utils/validate-generator.ts
@@ -1,12 +1,14 @@
import { app } from "@arkecosystem/core-container";
import { Logger } from "@arkecosystem/core-interfaces";
import { roundCalculator } from "@arkecosystem/core-utils";
import { models, slots } from "@arkecosystem/crypto";

export const validateGenerator = async (block: models.Block): Promise<boolean> => {
const database = app.resolvePlugin("database");
const logger = app.resolvePlugin<Logger.ILogger>("logger");

const delegates = await database.getActiveDelegates(block.data.height);
const roundInfo = roundCalculator.calculateRound(block.data.height);
const delegates = await database.getActiveDelegates(roundInfo);
const slot = slots.getSlotNumber(block.data.timestamp);
const forgingDelegate = delegates[slot % delegates.length];

Expand Down