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

Commit

Permalink
Merge pull request #6870 from LiskHQ/6726_unlock_command
Browse files Browse the repository at this point in the history
Implement DPoS unlocking command - Closes #6726
  • Loading branch information
shuse2 authored Nov 5, 2021
2 parents 579c465 + dd3bded commit 5789be8
Show file tree
Hide file tree
Showing 7 changed files with 563 additions and 82 deletions.
67 changes: 62 additions & 5 deletions framework/src/modules/dpos_v2/commands/unlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,71 @@

import { CommandExecuteContext } from '../../../node/state_machine/types';
import { BaseCommand } from '../../base_command';
import { COMMAND_ID_UNLOCK } from '../constants';
import { unlockCommandParamsSchema } from '../schemas';
import {
COMMAND_ID_UNLOCK,
STORE_PREFIX_DELEGATE,
STORE_PREFIX_VOTER,
MODULE_ID_DPOS,
} from '../constants';
import { delegateStoreSchema, voterStoreSchema } from '../schemas';
import {
DelegateAccount,
TokenAPI,
TokenIDDPoS,
UnlockCommandDependencies,
VoterData,
} from '../types';
import { hasWaited, isPunished } from '../utils';

export class UnlockCommand extends BaseCommand {
public id = COMMAND_ID_UNLOCK;
public name = 'unlockToken';
public schema = unlockCommandParamsSchema;

// eslint-disable-next-line @typescript-eslint/no-empty-function
public async execute(_context: CommandExecuteContext<Record<string, unknown>>): Promise<void> {}
private _tokenAPI!: TokenAPI;
private _tokenIDDPoS!: TokenIDDPoS;

public addDependencies(args: UnlockCommandDependencies) {
this._tokenAPI = args.tokenAPI;
this._tokenIDDPoS = args.tokenIDDPoS;
}

public async execute(context: CommandExecuteContext): Promise<void> {
const {
transaction: { senderAddress },
getStore,
getAPIContext,
header: { height },
} = context;
const delegateSubstore = getStore(this.moduleID, STORE_PREFIX_DELEGATE);
const voterSubstore = getStore(this.moduleID, STORE_PREFIX_VOTER);
const voterData = await voterSubstore.getWithSchema<VoterData>(senderAddress, voterStoreSchema);
const ineligibleUnlocks = [];

for (const unlockObject of voterData.pendingUnlocks) {
const { pomHeights } = await delegateSubstore.getWithSchema<DelegateAccount>(
unlockObject.delegateAddress,
delegateStoreSchema,
);

if (
hasWaited(unlockObject, senderAddress, height) &&
!isPunished(unlockObject, pomHeights, senderAddress, height)
) {
await this._tokenAPI.unlock(
getAPIContext(),
senderAddress,
MODULE_ID_DPOS,
this._tokenIDDPoS,
unlockObject.amount,
);
continue;
}
ineligibleUnlocks.push(unlockObject);
}
if (voterData.pendingUnlocks.length === ineligibleUnlocks.length) {
throw new Error('No eligible voter data was found for unlocking');
}
voterData.pendingUnlocks = ineligibleUnlocks;
await voterSubstore.setWithSchema(senderAddress, voterData, voterStoreSchema);
}
}
6 changes: 0 additions & 6 deletions framework/src/modules/dpos_v2/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,12 +273,6 @@ export const pomCommandParamsSchema = {
},
};

export const unlockCommandParamsSchema = {
$id: '/dpos/command/unlockTokenParams',
type: 'object',
properties: {},
};

export const configSchema = {
$id: '/dpos/config',
type: 'object',
Expand Down
12 changes: 12 additions & 0 deletions framework/src/modules/dpos_v2/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ export interface TokenAPI {
tokenID: TokenIDDPoS,
amount: bigint,
): Promise<void>;
unlock(
apiContext: APIContext,
address: Buffer,
moduleID: number,
tokenID: TokenIDDPoS,
amount: bigint,
): Promise<void>;
}

export interface UnlockingObject {
Expand Down Expand Up @@ -150,6 +157,11 @@ export interface VoteCommandDependencies {
tokenAPI: TokenAPI;
}

export interface UnlockCommandDependencies {
tokenIDDPoS: TokenIDDPoS;
tokenAPI: TokenAPI;
}

export interface SnapshotStoreData {
activeDelegates: Buffer[];
delegateWeightSnapshot: {
Expand Down
114 changes: 50 additions & 64 deletions framework/src/modules/dpos_v2/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { NotFoundError } from '@liskhq/lisk-chain';
import { UnlockingObject, VoterData } from './types';
import {
PUNISHMENT_PERIOD,
SELF_VOTE_PUNISH_TIME,
VOTER_PUNISH_TIME,
WAIT_TIME_SELF_VOTE,
WAIT_TIME_VOTE,
Expand Down Expand Up @@ -45,57 +44,6 @@ export const sortUnlocking = (unlocks: UnlockingObject[]): void => {
});
};

export const getMinPunishedHeight = (
senderAddress: Buffer,
delegateAddress: Buffer,
pomHeights: number[],
): number => {
if (pomHeights.length === 0) {
return 0;
}

const lastPomHeight = Math.max(...pomHeights);

// https://github.com/LiskHQ/lips/blob/master/proposals/lip-0024.md#update-to-validity-of-unlock-transaction
return senderAddress.equals(delegateAddress)
? lastPomHeight + SELF_VOTE_PUNISH_TIME
: lastPomHeight + VOTER_PUNISH_TIME;
};

export const getPunishmentPeriod = (
senderAddress: Buffer,
delegateAddress: Buffer,
pomHeights: number[],
lastBlockHeight: number,
): number => {
const currentHeight = lastBlockHeight + 1;
const minPunishedHeight = getMinPunishedHeight(senderAddress, delegateAddress, pomHeights);
const remainingBlocks = minPunishedHeight - currentHeight;

return remainingBlocks < 0 ? 0 : remainingBlocks;
};

export const getMinWaitingHeight = (
senderAddress: Buffer,
delegateAddress: Buffer,
unlockObject: UnlockingObject,
): number =>
unlockObject.unvoteHeight +
(senderAddress.equals(delegateAddress) ? WAIT_TIME_SELF_VOTE : WAIT_TIME_VOTE);

export const getWaitingPeriod = (
senderAddress: Buffer,
delegateAddress: Buffer,
lastBlockHeight: number,
unlockObject: UnlockingObject,
): number => {
const currentHeight = lastBlockHeight + 1;
const minWaitingHeight = getMinWaitingHeight(senderAddress, delegateAddress, unlockObject);
const remainingBlocks = minWaitingHeight - currentHeight;

return remainingBlocks < 0 ? 0 : remainingBlocks;
};

export const isNullCharacterIncluded = (input: string): boolean =>
new RegExp(/\\0|\\u0000|\\x00/).test(input);

Expand All @@ -119,18 +67,6 @@ export const validateSignature = (
bytes: Buffer,
): boolean => verifyData(tag, networkIdentifier, bytes, signature, publicKey);

export const isCurrentlyPunished = (height: number, pomHeights: ReadonlyArray<number>): boolean => {
if (pomHeights.length === 0) {
return false;
}
const lastPomHeight = Math.max(...pomHeights);
if (height - lastPomHeight < PUNISHMENT_PERIOD) {
return true;
}

return false;
};

export const getVoterOrDefault = async (voterStore: SubStore, address: Buffer) => {
try {
const voterData = await voterStore.getWithSchema<VoterData>(address, voterStoreSchema);
Expand All @@ -147,3 +83,53 @@ export const getVoterOrDefault = async (voterStore: SubStore, address: Buffer) =
return voterData;
}
};

export const isCurrentlyPunished = (height: number, pomHeights: ReadonlyArray<number>): boolean => {
if (pomHeights.length === 0) {
return false;
}
const lastPomHeight = Math.max(...pomHeights);
if (height - lastPomHeight < PUNISHMENT_PERIOD) {
return true;
}

return false;
};

export const hasWaited = (
unlockingObject: UnlockingObject,
senderAddress: Buffer,
height: number,
) => {
const delayedAvailability = unlockingObject.delegateAddress.equals(senderAddress)
? WAIT_TIME_SELF_VOTE
: WAIT_TIME_VOTE;

return !(height - unlockingObject.unvoteHeight < delayedAvailability);
};

export const isPunished = (
unlockingObject: UnlockingObject,
pomHeights: ReadonlyArray<number>,
senderAddress: Buffer,
height: number,
) => {
if (!pomHeights.length) {
return false;
}

const lastPomHeight = pomHeights[pomHeights.length - 1];

// If self-vote
if (unlockingObject.delegateAddress.equals(senderAddress)) {
return (
height - lastPomHeight < PUNISHMENT_PERIOD &&
lastPomHeight < unlockingObject.unvoteHeight + WAIT_TIME_SELF_VOTE
);
}

return (
height - lastPomHeight < VOTER_PUNISH_TIME &&
lastPomHeight < unlockingObject.unvoteHeight + WAIT_TIME_VOTE
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,24 @@ import { StateStore, Transaction } from '@liskhq/lisk-chain';
import { codec } from '@liskhq/lisk-codec';
import { getRandomBytes } from '@liskhq/lisk-cryptography';
import { InMemoryKVStore, KVStore } from '@liskhq/lisk-db';
import * as testing from '../../../../src/testing';
import { DelegateRegistrationCommand } from '../../../../src/modules/dpos_v2/commands/delegate_registration';
import * as testing from '../../../../../src/testing';
import { DelegateRegistrationCommand } from '../../../../../src/modules/dpos_v2/commands/delegate_registration';
import {
COMMAND_ID_DELEGATE_REGISTRATION,
MODULE_ID_DPOS,
STORE_PREFIX_DELEGATE,
STORE_PREFIX_NAME,
} from '../../../../src/modules/dpos_v2/constants';
} from '../../../../../src/modules/dpos_v2/constants';
import {
delegateStoreSchema,
delegateRegistrationCommandParamsSchema,
nameStoreSchema,
} from '../../../../src/modules/dpos_v2/schemas';
import { DelegateRegistrationParams, ValidatorsAPI } from '../../../../src/modules/dpos_v2/types';
import { VerifyStatus } from '../../../../src/node/state_machine';
} from '../../../../../src/modules/dpos_v2/schemas';
import {
DelegateRegistrationParams,
ValidatorsAPI,
} from '../../../../../src/modules/dpos_v2/types';
import { VerifyStatus } from '../../../../../src/node/state_machine';

describe('Delegate registration command', () => {
let delegateRegistrationCommand: DelegateRegistrationCommand;
Expand Down
Loading

0 comments on commit 5789be8

Please sign in to comment.