diff --git a/framework/src/modules/reward/calculate_reward.ts b/framework/src/modules/reward/calculate_reward.ts new file mode 100644 index 00000000000..d581f9fc7de --- /dev/null +++ b/framework/src/modules/reward/calculate_reward.ts @@ -0,0 +1,31 @@ +/* + * Copyright © 2021 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { CalculateDefaultRewardArgs } from './types'; + +export const calculateDefaultReward = (args: CalculateDefaultRewardArgs): bigint => { + const { height, distance, offset, brackets } = args; + + if (height < offset) { + return BigInt(0); + } + + const rewardDistance = Math.floor(distance); + const location = Math.trunc((height - offset) / rewardDistance); + const lastBracket = brackets[brackets.length - 1]; + + const bracket = location > brackets.length - 1 ? brackets.lastIndexOf(lastBracket) : location; + + return brackets[bracket]; +}; diff --git a/framework/src/modules/reward/endpoint.ts b/framework/src/modules/reward/endpoint.ts index ffbca81bc99..4d4ac5d9f0b 100644 --- a/framework/src/modules/reward/endpoint.ts +++ b/framework/src/modules/reward/endpoint.ts @@ -12,6 +12,40 @@ * Removal or modification of this copyright notice is prohibited. */ +import { ModuleEndpointContext } from '../..'; import { BaseEndpoint } from '../base_endpoint'; +import { calculateDefaultReward } from './calculate_reward'; +import { DefaultReward, EndpointInitArgs } from './types'; -export class RewardEndpoint extends BaseEndpoint {} +export class RewardEndpoint extends BaseEndpoint { + private _brackets!: ReadonlyArray; + private _offset!: number; + private _distance!: number; + + public init(args: EndpointInitArgs) { + this._brackets = args.config.brackets; + this._offset = args.config.offset; + this._distance = args.config.distance; + } + + public getDefaultRewardAtHeight(ctx: ModuleEndpointContext): DefaultReward { + const { height } = ctx.params; + + if (typeof height !== 'number') { + throw new Error('Parameter height must be a number.'); + } + + if (height < 0) { + throw new Error('Parameter height cannot be smaller than 0.'); + } + + const reward = calculateDefaultReward({ + height, + brackets: this._brackets, + distance: this._distance, + offset: this._offset, + }); + + return { reward: reward.toString() }; + } +} diff --git a/framework/src/modules/reward/module.ts b/framework/src/modules/reward/module.ts index 3f0180184bf..4d603718c6f 100644 --- a/framework/src/modules/reward/module.ts +++ b/framework/src/modules/reward/module.ts @@ -44,6 +44,14 @@ export class RewardModule extends BaseModule { const { moduleConfig } = args; this._moduleConfig = (moduleConfig as unknown) as ModuleConfig; this._tokenIDReward = this._moduleConfig.tokenIDReward; + + this.endpoint.init({ + config: { + brackets: this._moduleConfig.brackets.map(bracket => BigInt(bracket)), + offset: this._moduleConfig.offset, + distance: this._moduleConfig.distance, + }, + }); } // eslint-disable-next-line @typescript-eslint/require-await diff --git a/framework/src/modules/reward/schemas.ts b/framework/src/modules/reward/schemas.ts index e23f74c83e3..34d1bcdfdb6 100644 --- a/framework/src/modules/reward/schemas.ts +++ b/framework/src/modules/reward/schemas.ts @@ -28,6 +28,21 @@ export const configSchema = { }, required: ['chainID', 'localID'], }, + offset: { + type: 'integer', + minimum: 1, + }, + distance: { + type: 'integer', + minimum: 1, + }, + brackets: { + type: 'array', + items: { + type: 'string', + format: 'uint64', + }, + }, }, - required: ['tokenIDReward'], + required: ['tokenIDReward', 'offset', 'distance', 'brackets'], }; diff --git a/framework/src/modules/reward/types.ts b/framework/src/modules/reward/types.ts index 2f022f20b43..799fd3bfb94 100644 --- a/framework/src/modules/reward/types.ts +++ b/framework/src/modules/reward/types.ts @@ -22,6 +22,9 @@ export interface TokenIDReward { export interface ModuleConfig { tokenIDReward: TokenIDReward; + brackets: ReadonlyArray; + offset: number; + distance: number; } export interface TokenAPI { @@ -44,3 +47,22 @@ export interface RandomAPI { export interface LiskBFTAPI { impliesMaximalPrevotes(apiContext: APIContext, blockHeader: BlockHeader): Promise; } + +export interface DefaultReward { + reward: string; +} + +export interface EndpointInitArgs { + config: { + brackets: ReadonlyArray; + offset: number; + distance: number; + }; +} + +export interface CalculateDefaultRewardArgs { + brackets: ReadonlyArray; + offset: number; + distance: number; + height: number; +} diff --git a/framework/test/unit/modules/reward/endpoint.spec.ts b/framework/test/unit/modules/reward/endpoint.spec.ts new file mode 100644 index 00000000000..b3eec698b0c --- /dev/null +++ b/framework/test/unit/modules/reward/endpoint.spec.ts @@ -0,0 +1,103 @@ +/* + * Copyright © 2021 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ +import { Logger } from '../../../../src/logger'; +import { RewardModule } from '../../../../src/modules/reward'; +import { fakeLogger } from '../../../utils/node'; + +describe('RewardModuleEndpoint', () => { + const genesisConfig: any = {}; + const moduleConfig: any = { + distance: 3000000, + offset: 2160, + brackets: [ + BigInt('500000000'), // Initial Reward + BigInt('400000000'), // Milestone 1 + BigInt('300000000'), // Milestone 2 + BigInt('200000000'), // Milestone 3 + BigInt('100000000'), // Milestone 4 + ], + tokenIDReward: { chainID: 0, localID: 0 }, + }; + const generatorConfig: any = {}; + + const logger: Logger = fakeLogger; + let rewardModule: RewardModule; + + beforeAll(async () => { + rewardModule = new RewardModule(); + await rewardModule.init({ genesisConfig, moduleConfig, generatorConfig }); + rewardModule.addDependencies( + { mint: jest.fn() } as any, + { isValidSeedReveal: jest.fn() } as any, + { impliesMaximalPrevotes: jest.fn() } as any, + ); + }); + + const { brackets, offset, distance } = moduleConfig as { + brackets: ReadonlyArray; + offset: number; + distance: number; + }; + + for (const [index, rewardFromConfig] of Object.entries(brackets)) { + const nthBracket = +index; + const currentHeight = offset + nthBracket * distance; + // eslint-disable-next-line no-loop-func + it(`should getDefaultRewardAtHeight work for the ${nthBracket}th bracket`, () => { + const rewardFromEndpoint = rewardModule.endpoint.getDefaultRewardAtHeight({ + getStore: jest.fn(), + logger, + params: { + height: currentHeight, + }, + }); + expect(rewardFromEndpoint).toEqual({ reward: rewardFromConfig.toString() }); + }); + } + + it('should getDefaultRewardAtHeight work for the height below offset', () => { + const rewardFromEndpoint = rewardModule.endpoint.getDefaultRewardAtHeight({ + getStore: jest.fn(), + logger, + params: { + height: offset - 1, + }, + }); + expect(rewardFromEndpoint).toEqual({ reward: '0' }); + }); + + it('should throw an error when parameter height is not a number', () => { + expect(() => + rewardModule.endpoint.getDefaultRewardAtHeight({ + getStore: jest.fn(), + logger, + params: { + height: 'Not a number', + }, + }), + ).toThrow('Parameter height must be a number.'); + }); + + it('should throw an error when parameter height is below 0', () => { + expect(() => + rewardModule.endpoint.getDefaultRewardAtHeight({ + getStore: jest.fn(), + logger, + params: { + height: -1, + }, + }), + ).toThrow('Parameter height cannot be smaller than 0.'); + }); +}); diff --git a/framework/test/unit/modules/reward/reward_module.spec.ts b/framework/test/unit/modules/reward/reward_module.spec.ts new file mode 100644 index 00000000000..ead770d6e20 --- /dev/null +++ b/framework/test/unit/modules/reward/reward_module.spec.ts @@ -0,0 +1,49 @@ +/* + * Copyright © 2021 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ +import { RewardModule } from '../../../../src/modules/reward'; + +describe('RewardModule', () => { + const genesisConfig: any = {}; + const moduleConfig: any = { + distance: 3000000, + offset: 2160, + brackets: [ + BigInt('500000000'), // Initial Reward + BigInt('400000000'), // Milestone 1 + BigInt('300000000'), // Milestone 2 + BigInt('200000000'), // Milestone 3 + BigInt('100000000'), // Milestone 4 + ], + tokenIDReward: { chainID: 0, localID: 0 }, + }; + const generatorConfig: any = {}; + + let rewardModule: RewardModule; + + beforeAll(async () => { + rewardModule = new RewardModule(); + await rewardModule.init({ genesisConfig, moduleConfig, generatorConfig }); + rewardModule.addDependencies( + { mint: jest.fn() } as any, + { isValidSeedReveal: jest.fn() } as any, + { impliesMaximalPrevotes: jest.fn() } as any, + ); + }); + + describe('init', () => { + it('should set the moduleConfig property', () => { + expect(rewardModule['_moduleConfig']).toEqual(moduleConfig); + }); + }); +});