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

Implement DPoS module Endpoint & API - Closes #6720 #6719 #6833

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion framework/src/modules/dpos_v2/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,51 @@
* Removal or modification of this copyright notice is prohibited.
*/

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

export class DPoSAPI extends BaseAPI {}
export class DPoSAPI extends BaseAPI {
public async isNameAvailable(apiContext: ImmutableAPIContext, name: string): Promise<boolean> {
const nameSubStore = apiContext.getStore(MODULE_ID_DPOS, STORE_PREFIX_NAME);
const regex = /^[a-z=0-9!@$&_.]*$/;
if (
(await nameSubStore.has(Buffer.from(name, 'hex'))) ||
Incede marked this conversation as resolved.
Show resolved Hide resolved
name.length > MAX_LENGTH_NAME ||
Incede marked this conversation as resolved.
Show resolved Hide resolved
name.length < 1 ||
!regex.test(name)
) {
return false;
}

return true;
}

public async getVoter(apiContext: ImmutableAPIContext, address: Buffer): Promise<VoterData> {
const voterSubStore = apiContext.getStore(MODULE_ID_DPOS, STORE_PREFIX_VOTER);
Incede marked this conversation as resolved.
Show resolved Hide resolved
const voterData = await voterSubStore.getWithSchema<VoterData>(address, voterStoreSchema);

return voterData;
Incede marked this conversation as resolved.
Show resolved Hide resolved
}

public async getDelegate(
apiContext: ImmutableAPIContext,
address: Buffer,
): Promise<DelegateAccount> {
const delegateSubStore = apiContext.getStore(MODULE_ID_DPOS, STORE_PREFIX_DELEGATE);
Incede marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Up @@ -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;
70 changes: 69 additions & 1 deletion framework/src/modules/dpos_v2/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,74 @@
* Removal or modification of this copyright notice is prohibited.
*/

import { ModuleEndpointContext } from '../..';
import { BaseEndpoint } from '../base_endpoint';
import { MODULE_ID_DPOS, 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(MODULE_ID_DPOS, STORE_PREFIX_VOTER);
Incede marked this conversation as resolved.
Show resolved Hide resolved
const address = ctx.params.address as string;
Incede marked this conversation as resolved.
Show resolved Hide resolved
const voterData = await voterSubStore.getWithSchema<VoterData>(
Buffer.from(address, 'hex'),
voterStoreSchema,
);
const voterDataJSON = { sentVotes: [], pendingUnlocks: [] } as VoterDataJSON;
Incede marked this conversation as resolved.
Show resolved Hide resolved
voterData.sentVotes.map(sentVote =>
voterDataJSON.sentVotes.push({
delegateAddress: sentVote.delegateAddress.toString('hex'),
amount: sentVote.amount.toString(),
}),
);
Incede marked this conversation as resolved.
Show resolved Hide resolved
voterData.pendingUnlocks.map(pendingUnlock =>
voterDataJSON.pendingUnlocks.push({
...pendingUnlock,
delegateAddress: pendingUnlock.delegateAddress.toString('hex'),
amount: pendingUnlock.amount.toString(),
}),
);

return voterDataJSON;
}

public async getDelegate(ctx: ModuleEndpointContext): Promise<DelegateAccountJSON> {
const delegateSubStore = ctx.getStore(MODULE_ID_DPOS, STORE_PREFIX_DELEGATE);
const address = ctx.params.address as string;
Incede marked this conversation as resolved.
Show resolved Hide resolved
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(MODULE_ID_DPOS, STORE_PREFIX_DELEGATE);
Incede marked this conversation as resolved.
Show resolved Hide resolved
const startBuf = Buffer.alloc(20);
const endBuf = Buffer.alloc(20);
endBuf.fill(255);
Incede marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Up @@ -13,6 +13,7 @@
*/

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

export const nameStoreSchema = {
$id: '/dpos/name',
type: 'object',
required: ['delegateAddress'],
properties: {
Expand Down
34 changes: 34 additions & 0 deletions framework/src/modules/dpos_v2/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}[];
Incede marked this conversation as resolved.
Show resolved Hide resolved
}

export interface VoterDataJSON {
sentVotes: {
delegateAddress: string;
amount: string;
}[];
pendingUnlocks: {
delegateAddress: string;
amount: string;
unvoteHeight: number;
}[];
}
179 changes: 179 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,179 @@
/*
* 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 { DPoSModule } from '../../../../src/modules/dpos_v2';
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 dposModule: DPoSModule;
let apiContext: APIContext;
let stateStore: StateStore;
let voterSubStore: StateStore;
let delegateSubStore: StateStore;
let nameSubStore: StateStore;
const address = getRandomBytes(48);
const voterData = {
sentVotes: [
{
delegateAddress: getRandomBytes(48),
Incede marked this conversation as resolved.
Show resolved Hide resolved
amount: BigInt(0),
},
],
pendingUnlocks: [
{
delegateAddress: getRandomBytes(48),
amount: BigInt(0),
unvoteHeight: 0,
},
],
};

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

beforeAll(async () => {
dposModule = new DPoSModule();
});

beforeEach(() => {
dposAPI = new DPoSAPI(MODULE_ID_DPOS);
Incede marked this conversation as resolved.
Show resolved Hide resolved
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, 'hex'),
Incede marked this conversation as resolved.
Show resolved Hide resolved
{},
nameStoreSchema,
);
apiContext = new APIContext({ stateStore, eventQueue: new EventQueue() });
Incede marked this conversation as resolved.
Show resolved Hide resolved
await expect(
dposModule.api.isNameAvailable(apiContext, delegateData.name),
Incede marked this conversation as resolved.
Show resolved Hide resolved
).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(
dposModule.api.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(dposModule.api.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(
dposModule.api.isNameAvailable(apiContext, 'Ajldnfdf-_.dv$%&^#'),
Incede marked this conversation as resolved.
Show resolved Hide resolved
).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(
dposModule.api.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 dposModule.api.getVoter(apiContext, address);

expect(
voterDataReturned.sentVotes[0].delegateAddress.equals(
voterData.sentVotes[0].delegateAddress,
),
).toBeTrue();
expect(voterDataReturned.sentVotes[0].amount).toBe(voterData.sentVotes[0].amount);
expect(
voterDataReturned.pendingUnlocks[0].delegateAddress.equals(
voterData.pendingUnlocks[0].delegateAddress,
),
).toBeTrue();
expect(voterDataReturned.pendingUnlocks[0].amount).toBe(voterData.pendingUnlocks[0].amount);
expect(voterDataReturned.pendingUnlocks[0].unvoteHeight).toBe(
voterData.pendingUnlocks[0].unvoteHeight,
);
});
});
});

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 dposModule.api.getDelegate(apiContext, address);

expect(delegateDataReturned.name).toBe(delegateData.name);
expect(delegateDataReturned.totalVotesReceived).toBe(delegateData.totalVotesReceived);
expect(delegateDataReturned.selfVotes).toBe(delegateData.selfVotes);
expect(delegateDataReturned.lastGeneratedHeight).toBe(delegateData.lastGeneratedHeight);
expect(delegateDataReturned.isBanned).toBe(delegateData.isBanned);
expect(delegateDataReturned.pomHeights).toStrictEqual(delegateData.pomHeights);
expect(delegateDataReturned.consecutiveMissedBlocks).toBe(
delegateData.consecutiveMissedBlocks,
);
});
});
});
});
Loading