Skip to content
This repository has been archived by the owner on Jun 11, 2024. It is now read-only.

Commit

Permalink
Showing 7 changed files with 478 additions and 2 deletions.
46 changes: 45 additions & 1 deletion framework/src/modules/dpos_v2/api.ts
Original file line number Diff line number Diff line change
@@ -12,6 +12,50 @@
* Removal or modification of this copyright notice is prohibited.
*/

import { ImmutableAPIContext } from '../../node/state_machine';
import { BaseAPI } from '../base_api';
import {
MAX_LENGTH_NAME,
STORE_PREFIX_DELEGATE,
STORE_PREFIX_NAME,
STORE_PREFIX_VOTER,
} from './constants';
import { voterStoreSchema, delegateStoreSchema } from './schemas';
import { DelegateAccount, VoterData } from './types';
import { isUsername } from './utils';

export class DPoSAPI extends BaseAPI {}
export class DPoSAPI extends BaseAPI {
public async isNameAvailable(apiContext: ImmutableAPIContext, name: string): Promise<boolean> {
const nameSubStore = apiContext.getStore(this.moduleID, STORE_PREFIX_NAME);
if (name.length > MAX_LENGTH_NAME || name.length < 1 || !isUsername(name)) {
return false;
}

const isRegistered = await nameSubStore.has(Buffer.from(name));
if (isRegistered) {
return false;
}

return true;
}

public async getVoter(apiContext: ImmutableAPIContext, address: Buffer): Promise<VoterData> {
const voterSubStore = apiContext.getStore(this.moduleID, STORE_PREFIX_VOTER);
const voterData = await voterSubStore.getWithSchema<VoterData>(address, voterStoreSchema);

return voterData;
}

public async getDelegate(
apiContext: ImmutableAPIContext,
address: Buffer,
): Promise<DelegateAccount> {
const delegateSubStore = apiContext.getStore(this.moduleID, STORE_PREFIX_DELEGATE);
const delegate = await delegateSubStore.getWithSchema<DelegateAccount>(
address,
delegateStoreSchema,
);

return delegate;
}
}
1 change: 1 addition & 0 deletions framework/src/modules/dpos_v2/constants.ts
Original file line number Diff line number Diff line change
@@ -33,3 +33,4 @@ export const VOTER_PUNISH_TIME = 260000;
export const SELF_VOTE_PUNISH_TIME = 780000;
// Punishment period is 780k block height by default
export const PUNISHMENT_PERIOD = 780000;
export const MAX_LENGTH_NAME = 20;
62 changes: 61 additions & 1 deletion framework/src/modules/dpos_v2/endpoint.ts
Original file line number Diff line number Diff line change
@@ -12,6 +12,66 @@
* Removal or modification of this copyright notice is prohibited.
*/

import { codec } from '@liskhq/lisk-codec';
import { ModuleEndpointContext } from '../..';
import { BaseEndpoint } from '../base_endpoint';
import { STORE_PREFIX_DELEGATE, STORE_PREFIX_VOTER } from './constants';
import { voterStoreSchema, delegateStoreSchema } from './schemas';
import { DelegateAccount, DelegateAccountJSON, VoterData, VoterDataJSON } from './types';

export class DPoSEndpoint extends BaseEndpoint {}
export class DPoSEndpoint extends BaseEndpoint {
public async getVoter(ctx: ModuleEndpointContext): Promise<VoterDataJSON> {
const voterSubStore = ctx.getStore(this.moduleID, STORE_PREFIX_VOTER);
const { address } = ctx.params;
if (typeof address !== 'string') {
throw new Error('Parameter address must be a string.');
}
const voterData = await voterSubStore.getWithSchema<VoterData>(
Buffer.from(address, 'hex'),
voterStoreSchema,
);

return codec.toJSON(voterStoreSchema, voterData);
}

public async getDelegate(ctx: ModuleEndpointContext): Promise<DelegateAccountJSON> {
const delegateSubStore = ctx.getStore(this.moduleID, STORE_PREFIX_DELEGATE);
const { address } = ctx.params;
if (typeof address !== 'string') {
throw new Error('Parameter address must be a string.');
}
const delegate = await delegateSubStore.getWithSchema<DelegateAccount>(
Buffer.from(address, 'hex'),
delegateStoreSchema,
);

return {
...delegate,
totalVotesReceived: delegate.totalVotesReceived.toString(),
selfVotes: delegate.selfVotes.toString(),
};
}

public async getAllDelegates(ctx: ModuleEndpointContext): Promise<DelegateAccountJSON[]> {
const delegateSubStore = ctx.getStore(this.moduleID, STORE_PREFIX_DELEGATE);
const startBuf = Buffer.alloc(20);
const endBuf = Buffer.alloc(20, 255);
const storeData = await delegateSubStore.iterate({ start: startBuf, end: endBuf });

const response = [];
for (const data of storeData) {
const delegate = await delegateSubStore.getWithSchema<DelegateAccount>(
data.key,
delegateStoreSchema,
);
const delegateJSON = {
...delegate,
totalVotesReceived: delegate.totalVotesReceived.toString(),
selfVotes: delegate.selfVotes.toString(),
};
response.push(delegateJSON);
}

return response;
}
}
2 changes: 2 additions & 0 deletions framework/src/modules/dpos_v2/schemas.ts
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@
*/

export const voterStoreSchema = {
$id: '/dpos/voter',
type: 'object',
required: ['sentVotes', 'pendingUnlocks'],
properties: {
@@ -105,6 +106,7 @@ export const delegateStoreSchema = {
};

export const nameStoreSchema = {
$id: '/dpos/name',
type: 'object',
required: ['delegateAddress'],
properties: {
34 changes: 34 additions & 0 deletions framework/src/modules/dpos_v2/types.ts
Original file line number Diff line number Diff line change
@@ -80,3 +80,37 @@ export interface DelegateAccount {
pomHeights: ReadonlyArray<number>;
consecutiveMissedBlocks: number;
}

export interface DelegateAccountJSON {
name: string;
totalVotesReceived: string;
selfVotes: string;
lastGeneratedHeight: number;
isBanned: boolean;
pomHeights: ReadonlyArray<number>;
consecutiveMissedBlocks: number;
}

export interface VoterData {
sentVotes: {
delegateAddress: Buffer;
amount: bigint;
}[];
pendingUnlocks: {
delegateAddress: Buffer;
amount: bigint;
unvoteHeight: number;
}[];
}

export interface VoterDataJSON {
sentVotes: {
delegateAddress: string;
amount: string;
}[];
pendingUnlocks: {
delegateAddress: string;
amount: string;
unvoteHeight: number;
}[];
}
145 changes: 145 additions & 0 deletions framework/test/unit/modules/dpos_v2/api.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* 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 { StateStore } from '@liskhq/lisk-chain';
import { getRandomBytes } from '@liskhq/lisk-cryptography';
import { InMemoryKVStore } from '@liskhq/lisk-db';
import { DPoSAPI } from '../../../../src/modules/dpos_v2/api';
import {
MODULE_ID_DPOS,
STORE_PREFIX_DELEGATE,
STORE_PREFIX_NAME,
STORE_PREFIX_VOTER,
} from '../../../../src/modules/dpos_v2/constants';
import {
delegateStoreSchema,
nameStoreSchema,
voterStoreSchema,
} from '../../../../src/modules/dpos_v2/schemas';
import { APIContext } from '../../../../src/node/state_machine/api_context';
import { EventQueue } from '../../../../src/node/state_machine';

describe('DposModuleApi', () => {
let dposAPI: DPoSAPI;
let apiContext: APIContext;
let stateStore: StateStore;
let voterSubStore: StateStore;
let delegateSubStore: StateStore;
let nameSubStore: StateStore;
const address = getRandomBytes(20);
const voterData = {
sentVotes: [
{
delegateAddress: getRandomBytes(20),
amount: BigInt(0),
},
],
pendingUnlocks: [
{
delegateAddress: getRandomBytes(20),
amount: BigInt(0),
unvoteHeight: 0,
},
],
};

const delegateData = {
name: 'delegate1',
totalVotesReceived: BigInt(0),
selfVotes: BigInt(0),
lastGeneratedHeight: 0,
isBanned: false,
pomHeights: [0],
consecutiveMissedBlocks: 0,
};

beforeEach(() => {
dposAPI = new DPoSAPI(MODULE_ID_DPOS);
stateStore = new StateStore(new InMemoryKVStore());
voterSubStore = stateStore.getStore(dposAPI['moduleID'], STORE_PREFIX_VOTER);
delegateSubStore = stateStore.getStore(dposAPI['moduleID'], STORE_PREFIX_DELEGATE);
nameSubStore = stateStore.getStore(dposAPI['moduleID'], STORE_PREFIX_NAME);
});

describe('isNameAvailable', () => {
describe('when name already exists', () => {
it('should return false', async () => {
await nameSubStore.setWithSchema(Buffer.from(delegateData.name), {}, nameStoreSchema);
apiContext = new APIContext({ stateStore, eventQueue: new EventQueue() });
await expect(dposAPI.isNameAvailable(apiContext, delegateData.name)).resolves.toBeFalse();
});
});

describe('when name does not exist and exceeds the maximum length', () => {
it('should return false', async () => {
apiContext = new APIContext({ stateStore, eventQueue: new EventQueue() });
await expect(
dposAPI.isNameAvailable(
apiContext,
'nnwkfnwkfnkwrnfkrnfeknekerfnkjenejnfekfnekfnjkdnwknw',
),
).resolves.toBeFalse();
});
});

describe('when name does not exist and has length less than 1', () => {
it('should return false', async () => {
apiContext = new APIContext({ stateStore, eventQueue: new EventQueue() });
await expect(dposAPI.isNameAvailable(apiContext, '')).resolves.toBeFalse();
});
});

describe('when name does not exist and contains invalid symbol', () => {
it('should return false', async () => {
apiContext = new APIContext({ stateStore, eventQueue: new EventQueue() });
await expect(
dposAPI.isNameAvailable(apiContext, 'Ajldnfdf-_.dv$%&^#'),
).resolves.toBeFalse();
});
});

describe('when name does not exist and is a valid name', () => {
it('should return true', async () => {
apiContext = new APIContext({ stateStore, eventQueue: new EventQueue() });
await expect(
dposAPI.isNameAvailable(apiContext, 'abcdefghijklmnopqrstuvwxyz0123456789!@$&_.'),
).resolves.toBeFalse();
});
});
});

describe('getVoter', () => {
describe('when input address is valid', () => {
it('should return correct voter data corresponding to the input address', async () => {
await voterSubStore.setWithSchema(address, voterData, voterStoreSchema);
apiContext = new APIContext({ stateStore, eventQueue: new EventQueue() });
const voterDataReturned = await dposAPI.getVoter(apiContext, address);

expect(voterDataReturned).toStrictEqual(voterData);
});
});
});

describe('getDelegate', () => {
describe('when input address is valid', () => {
it('should return correct delegate data corresponding to the input address', async () => {
await delegateSubStore.setWithSchema(address, delegateData, delegateStoreSchema);
apiContext = new APIContext({ stateStore, eventQueue: new EventQueue() });
const delegateDataReturned = await dposAPI.getDelegate(apiContext, address);

expect(delegateDataReturned).toStrictEqual(delegateData);
});
});
});
});
190 changes: 190 additions & 0 deletions framework/test/unit/modules/dpos_v2/endpoint.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*
* 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 { StateStore } from '@liskhq/lisk-chain';
import { getRandomBytes } from '@liskhq/lisk-cryptography';
import { InMemoryKVStore } from '@liskhq/lisk-db';
import { codec } from '@liskhq/lisk-codec';
import { Logger } from '../../../../src/logger';
import {
MODULE_ID_DPOS,
STORE_PREFIX_DELEGATE,
STORE_PREFIX_VOTER,
} from '../../../../src/modules/dpos_v2/constants';
import { DPoSEndpoint } from '../../../../src/modules/dpos_v2/endpoint';
import { delegateStoreSchema, voterStoreSchema } from '../../../../src/modules/dpos_v2/schemas';
import { fakeLogger } from '../../../utils/node';

describe('DposModuleEndpoint', () => {
const logger: Logger = fakeLogger;
let dposEndpoint: DPoSEndpoint;
let stateStore: StateStore;
let voterSubStore: StateStore;
let delegateSubStore: StateStore;
const address = getRandomBytes(20);
const address1 = getRandomBytes(20);
const address2 = getRandomBytes(20);
const getStore1 = jest.fn();
const networkIdentifier = Buffer.alloc(0);
const voterData = {
sentVotes: [
{
delegateAddress: getRandomBytes(20),
amount: BigInt(0),
},
],
pendingUnlocks: [
{
delegateAddress: getRandomBytes(20),
amount: BigInt(0),
unvoteHeight: 0,
},
],
};

const delegateData = {
name: 'delegate1',
totalVotesReceived: BigInt(0),
selfVotes: BigInt(0),
lastGeneratedHeight: 0,
isBanned: false,
pomHeights: [0],
consecutiveMissedBlocks: 0,
};

beforeEach(() => {
dposEndpoint = new DPoSEndpoint(MODULE_ID_DPOS);
stateStore = new StateStore(new InMemoryKVStore());
voterSubStore = stateStore.getStore(dposEndpoint['moduleID'], STORE_PREFIX_VOTER);
delegateSubStore = stateStore.getStore(dposEndpoint['moduleID'], STORE_PREFIX_DELEGATE);
});

describe('getVoter', () => {
describe('when input address is valid', () => {
it('should return correct voter data corresponding to the input address', async () => {
await voterSubStore.setWithSchema(address, voterData, voterStoreSchema);
getStore1.mockReturnValue(voterSubStore);
const voterDataReturned = await dposEndpoint.getVoter({
getStore: getStore1,
logger,
params: {
address: address.toString('hex'),
},
networkIdentifier,
});

expect(voterDataReturned).toStrictEqual(codec.toJSON(voterStoreSchema, voterData));
});

it('should return valid JSON output', async () => {
await voterSubStore.setWithSchema(address, voterData, voterStoreSchema);
getStore1.mockReturnValue(voterSubStore);
const voterDataReturned = await dposEndpoint.getVoter({
getStore: getStore1,
logger,
params: {
address: address.toString('hex'),
},
networkIdentifier,
});

expect(voterDataReturned.sentVotes[0].delegateAddress).toBeString();
expect(voterDataReturned.sentVotes[0].amount).toBeString();
expect(voterDataReturned.pendingUnlocks[0].delegateAddress).toBeString();
expect(voterDataReturned.pendingUnlocks[0].amount).toBeString();
});
});
});

describe('getDelegate', () => {
describe('when input address is valid', () => {
it('should return correct delegate data corresponding to the input address', async () => {
await delegateSubStore.setWithSchema(address, delegateData, delegateStoreSchema);
getStore1.mockReturnValue(delegateSubStore);
const delegateDataReturned = await dposEndpoint.getDelegate({
getStore: getStore1,
logger,
params: {
address: address.toString('hex'),
},
networkIdentifier,
});

const delegateDataJSON = {
...delegateData,
totalVotesReceived: delegateData.totalVotesReceived.toString(),
selfVotes: delegateData.selfVotes.toString(),
};

expect(delegateDataReturned).toStrictEqual(delegateDataJSON);
});

it('should return valid JSON output', async () => {
await delegateSubStore.setWithSchema(address, delegateData, delegateStoreSchema);
getStore1.mockReturnValue(delegateSubStore);
const delegateDataReturned = await dposEndpoint.getDelegate({
getStore: getStore1,
logger,
params: {
address: address.toString('hex'),
},
networkIdentifier,
});

expect(delegateDataReturned.totalVotesReceived).toBeString();
expect(delegateDataReturned.selfVotes).toBeString();
});
});
});

describe('getAllDelegates', () => {
describe('when input address is valid', () => {
it('should return correct data for all delegates', async () => {
await delegateSubStore.setWithSchema(address1, delegateData, delegateStoreSchema);
await delegateSubStore.setWithSchema(address2, delegateData, delegateStoreSchema);
getStore1.mockReturnValue(delegateSubStore);
const delegatesDataReturned = await dposEndpoint.getAllDelegates({
getStore: getStore1,
logger,
params: {},
networkIdentifier,
});

expect(delegatesDataReturned[0]).toStrictEqual(
codec.toJSON(delegateStoreSchema, delegateData),
);
expect(delegatesDataReturned[1]).toStrictEqual(
codec.toJSON(delegateStoreSchema, delegateData),
);
});

it('should return valid JSON output', async () => {
await delegateSubStore.setWithSchema(address, delegateData, delegateStoreSchema);
await delegateSubStore.setWithSchema(address1, delegateData, delegateStoreSchema);
getStore1.mockReturnValue(delegateSubStore);
const delegatesDataReturned = await dposEndpoint.getAllDelegates({
getStore: getStore1,
logger,
params: {},
networkIdentifier,
});

expect(delegatesDataReturned[0].totalVotesReceived).toBeString();
expect(delegatesDataReturned[0].selfVotes).toBeString();
expect(delegatesDataReturned[1].totalVotesReceived).toBeString();
expect(delegatesDataReturned[1].selfVotes).toBeString();
});
});
});
});

0 comments on commit ca95690

Please sign in to comment.