From 51a388a3b93cd6e9f2327829d15fb45f2a5a0ce9 Mon Sep 17 00:00:00 2001 From: shuse2 Date: Wed, 3 Nov 2021 10:07:04 +0100 Subject: [PATCH] wip --- framework/src/modules/dpos_v2/module.ts | 68 ++++++++++++++++- framework/src/modules/dpos_v2/types.ts | 9 +++ framework/src/modules/dpos_v2/utils.ts | 98 ++++++++++++++++++++++++- 3 files changed, 172 insertions(+), 3 deletions(-) diff --git a/framework/src/modules/dpos_v2/module.ts b/framework/src/modules/dpos_v2/module.ts index 44ade214c0f..4cc8f882cda 100644 --- a/framework/src/modules/dpos_v2/module.ts +++ b/framework/src/modules/dpos_v2/module.ts @@ -42,7 +42,12 @@ import { SnapshotStoreData, } from './types'; import { Rounds } from './rounds'; -import { isCurrentlyPunished } from './utils'; +import { + isCurrentlyPunished, + selectStandbyDelegates, + shuffleDelegateList, + validtorsEqual, +} from './utils'; export class DPoSModule extends BaseModule { public id = MODULE_ID_DPOS; @@ -117,6 +122,7 @@ export class DPoSModule extends BaseModule { public async afterBlockExecute(context: BlockAfterExecuteContext): Promise { await this._createVoteWeightSnapshot(context); + await this._updateValidators(context); } private async _createVoteWeightSnapshot(context: BlockAfterExecuteContext): Promise { @@ -214,4 +220,64 @@ export class DPoSModule extends BaseModule { await snapshotStore.del(key); } } + + private async _updateValidators(context: BlockAfterExecuteContext): Promise { + 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( + 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); + } } diff --git a/framework/src/modules/dpos_v2/types.ts b/framework/src/modules/dpos_v2/types.ts index af6629eb2c2..f54bd33d869 100644 --- a/framework/src/modules/dpos_v2/types.ts +++ b/framework/src/modules/dpos_v2/types.ts @@ -43,6 +43,15 @@ export interface BFTAPI { certificateThreshold: number, validators: Validator[], ): Promise; + getBFTParameters( + context: ImmutableAPIContext, + height: number, + ): Promise<{ + prevoteThreshold: bigint; + precommitThreshold: bigint; + certificateThreshold: bigint; + validators: Validator[]; + }>; } export interface RandomAPI { diff --git a/framework/src/modules/dpos_v2/utils.ts b/framework/src/modules/dpos_v2/utils.ts index b95fd9e74d0..04a1d7fa667 100644 --- a/framework/src/modules/dpos_v2/utils.ts +++ b/framework/src/modules/dpos_v2/utils.ts @@ -12,9 +12,9 @@ * 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 { SnapshotStoreData, UnlockingObject, VoterData } from './types'; import { PUNISHMENT_PERIOD, SELF_VOTE_PUNISH_TIME, @@ -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) => { @@ -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, + 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[] => { + 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); + 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; +};