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

Update validators from DPoS module - Closes #6876 #6878

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
68 changes: 67 additions & 1 deletion framework/src/modules/dpos_v2/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ import {
SnapshotStoreData,
} from './types';
import { Rounds } from './rounds';
import { isCurrentlyPunished } from './utils';
import {
isCurrentlyPunished,
selectStandbyDelegates,
shuffleDelegateList,
validtorsEqual,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

} from './utils';

export class DPoSModule extends BaseModule {
public id = MODULE_ID_DPOS;
Expand Down Expand Up @@ -117,6 +122,7 @@ export class DPoSModule extends BaseModule {

public async afterBlockExecute(context: BlockAfterExecuteContext): Promise<void> {
await this._createVoteWeightSnapshot(context);
await this._updateValidators(context);
}

private async _createVoteWeightSnapshot(context: BlockAfterExecuteContext): Promise<void> {
Expand Down Expand Up @@ -214,4 +220,64 @@ export class DPoSModule extends BaseModule {
await snapshotStore.del(key);
}
}

private async _updateValidators(context: BlockAfterExecuteContext): Promise<void> {
const round = new Rounds({ blocksPerRound: this._moduleConfig.roundLength });
const { height } = context.header;
const nextRound = round.calcRound(height) + 1;
context.logger.debug(nextRound, 'Updating delegate list for');

const snapshotStore = context.getStore(this.id, STORE_PREFIX_SNAPSHOT);
const snapshot = await snapshotStore.getWithSchema<SnapshotStoreData>(
intToBuffer(nextRound, 4),
snapshotStoreSchema,
);

const apiContext = context.getAPIContext();

// get the last stored BFT parameters, and update them if needed
const currentBFTParams = await this._bftAPI.getBFTParameters(apiContext, height);
// snapshot.activeDelegates order should not be changed here to use it below
const bftWeight = [...snapshot.activeDelegates]
.sort((a, b) => a.compare(b))
.map(address => ({ address, bftWeight: BigInt(1) }));
if (
!validtorsEqual(currentBFTParams.validators, bftWeight) ||
currentBFTParams.precommitThreshold !== BigInt(this._moduleConfig.bftThreshold) ||
currentBFTParams.certificateThreshold !== BigInt(this._moduleConfig.bftThreshold)
) {
await this._bftAPI.setBFTParameters(
apiContext,
this._moduleConfig.bftThreshold,
this._moduleConfig.bftThreshold,
bftWeight,
);
}

// Update the validators
const validators = [...snapshot.activeDelegates];
const randomSeed1 = await this._randomAPI.getRandomBytes(
apiContext,
height + 1 - Math.floor((this._moduleConfig.roundLength * 3) / 2),
this._moduleConfig.roundLength,
);
if (this._moduleConfig.numberStandbyDelegates === 2) {
const randomSeed2 = await this._randomAPI.getRandomBytes(
apiContext,
height + 1 - 2 * this._moduleConfig.roundLength,
this._moduleConfig.roundLength,
);
const standbyDelegates = selectStandbyDelegates(
snapshot.delegateWeightSnapshot,
randomSeed1,
randomSeed2,
);
validators.push(...standbyDelegates);
} else if (this._moduleConfig.numberStandbyDelegates === 1) {
const standbyDelegates = selectStandbyDelegates(snapshot.delegateWeightSnapshot, randomSeed1);
validators.push(...standbyDelegates);
}
const shuffledValidators = shuffleDelegateList(randomSeed1, validators);
await this._validatorsAPI.setGeneratorList(apiContext, shuffledValidators);
}
}
9 changes: 9 additions & 0 deletions framework/src/modules/dpos_v2/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ export interface BFTAPI {
certificateThreshold: number,
validators: Validator[],
): Promise<void>;
getBFTParameters(
context: ImmutableAPIContext,
height: number,
): Promise<{
prevoteThreshold: bigint;
precommitThreshold: bigint;
certificateThreshold: bigint;
validators: Validator[];
}>;
}

export interface RandomAPI {
Expand Down
96 changes: 95 additions & 1 deletion framework/src/modules/dpos_v2/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* Removal or modification of this copyright notice is prohibited.
*/

import { verifyData } from '@liskhq/lisk-cryptography';
import { hash, verifyData } from '@liskhq/lisk-cryptography';
import { NotFoundError } from '@liskhq/lisk-chain';
import { UnlockingObject, VoterData } from './types';
import {
Expand All @@ -24,6 +24,7 @@ import {
} from './constants';
import { SubStore } from '../../node/state_machine/types';
import { voterStoreSchema } from './schemas';
import { Validator } from '../../node/consensus/types';

export const sortUnlocking = (unlocks: UnlockingObject[]): void => {
unlocks.sort((a, b) => {
Expand Down Expand Up @@ -147,3 +148,96 @@ export const getVoterOrDefault = async (voterStore: SubStore, address: Buffer) =
return voterData;
}
};

export interface DelegateWeight {
readonly delegateAddress: Buffer;
readonly delegateWeight: bigint;
}

export const pickStandByDelegate = (
delegateWeights: ReadonlyArray<DelegateWeight>,
randomSeed: Buffer,
): number => {
const seedNumber = randomSeed.readBigUInt64BE();
const totalVoteWeight = delegateWeights.reduce(
(prev, current) => prev + BigInt(current.delegateWeight),
BigInt(0),
);

let threshold = seedNumber % totalVoteWeight;
for (let i = 0; i < delegateWeights.length; i += 1) {
const voteWeight = BigInt(delegateWeights[i].delegateWeight);
if (voteWeight > threshold) {
return i;
}
threshold -= voteWeight;
}

return -1;
};

export const shuffleDelegateList = (
previousRoundSeed1: Buffer,
addresses: ReadonlyArray<Buffer>,
): Buffer[] => {
const delegateList = [...addresses].map(delegate => ({
address: delegate,
})) as { address: Buffer; roundHash: Buffer }[];

for (const delegate of delegateList) {
const seedSource = Buffer.concat([previousRoundSeed1, delegate.address]);
delegate.roundHash = hash(seedSource);
}

delegateList.sort((delegate1, delegate2) => {
const diff = delegate1.roundHash.compare(delegate2.roundHash);
if (diff !== 0) {
return diff;
}

return delegate1.address.compare(delegate2.address);
});

return delegateList.map(delegate => delegate.address);
};

export const selectStandbyDelegates = (
delegateWeights: DelegateWeight[],
randomSeed1: Buffer,
randomSeed2?: Buffer,
): Buffer[] => {
const numberOfCandidates = 1 + (randomSeed2 !== undefined ? 1 : 0);
// if delegate weights is smaller than number selecting, select all
if (delegateWeights.length <= numberOfCandidates) {
return delegateWeights.map(c => c.delegateAddress);
}
const result: Buffer[] = [];
const index = pickStandByDelegate(delegateWeights, randomSeed1);
const [selected] = delegateWeights.splice(index, 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the below is secondStandby maybe we can call this firstStandby?

result.push(selected.delegateAddress);
// if seed2 is missing, return only 1
if (!randomSeed2) {
return result;
}
const secondIndex = pickStandByDelegate(delegateWeights, randomSeed2);
const [secondStandby] = delegateWeights.splice(secondIndex, 1);
result.push(secondStandby.delegateAddress);

return result;
};

export const validtorsEqual = (v1: Validator[], v2: Validator[]): boolean => {
if (v1.length !== v2.length) {
return false;
}
for (let i = 0; i < v1.length; i += 1) {
if (!v1[i].address.equals(v2[i].address)) {
return false;
}
if (v1[i].bftWeight !== v2[i].bftWeight) {
return false;
}
}

return true;
};
Loading