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

Fix bftweight zero issue #8899

Merged
merged 10 commits into from
Sep 1, 2023
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified examples/pos-mainchain/config/default/genesis_block.blob
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ export class ChainConnectorPlugin extends BasePlugin<ChainConnectorPluginConfig>

// Get validatorsData at new block header height
const bftParametersJSON = await this._sendingChainClient.invoke<BFTParametersJSON>(
'consensus_getBFTParameters',
'consensus_getBFTParametersActiveValidators',
{ height: newBlockHeader.height },
);

Expand All @@ -577,9 +577,10 @@ export class ChainConnectorPlugin extends BasePlugin<ChainConnectorPluginConfig>
);
// Save validatorsData if there is a new validatorsHash
if (validatorsDataIndex === -1) {
const activeValidators = bftParametersObj.validators;
validatorsHashPreimage.push({
certificateThreshold: bftParametersObj.certificateThreshold,
validators: bftParametersObj.validators,
validators: activeValidators,
validatorsHash: bftParametersObj.validatorsHash,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ describe('ChainConnectorPlugin', () => {
chain.BlockHeader.fromJSON(newBlockHeaderJSON).toObject(),
];
when(sendingChainAPIClientMock.invoke)
.calledWith('consensus_getBFTParameters', { height: newBlockHeaderHeight })
.calledWith('consensus_getBFTParametersActiveValidators', { height: newBlockHeaderHeight })
.mockResolvedValue({
prevoteThreshold: '2',
precommitThreshold: '2',
Expand Down
22 changes: 21 additions & 1 deletion framework/src/engine/bft/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
areDistinctHeadersContradicting,
computeValidatorsHash,
sortValidatorsByAddress,
sortValidatorsByBLSKey,
} from './utils';
import { getBFTParameters } from './bft_params';
import {
Expand Down Expand Up @@ -89,6 +90,19 @@ export class BFTMethod {
return getBFTParameters(paramsStore, height);
}

public async getBFTParametersActiveValidators(
stateStore: StateStore,
height: number,
): Promise<BFTParameters> {
const bftParams = await this.getBFTParameters(stateStore, height);

return {
...bftParams,
// Filter standby validators with bftWeight === 0
validators: bftParams.validators.filter(v => v.bftWeight > BigInt(0)),
};
}

public async getBFTHeights(stateStore: StateStore): Promise<BFTHeights> {
const votesStore = stateStore.getStore(MODULE_STORE_PREFIX_BFT, STORE_PREFIX_BFT_VOTES);
const bftVotes = await votesStore.getWithSchema<BFTVotes>(EMPTY_KEY, bftVotesSchema);
Expand Down Expand Up @@ -204,7 +218,13 @@ export class BFTMethod {
throw new Error('Invalid certificateThreshold input.');
}

const validatorsHash = computeValidatorsHash(validators, certificateThreshold);
sortValidatorsByBLSKey(validators);
const validatorsHash = computeValidatorsHash(
validators
.filter(v => v.bftWeight > BigInt(0))
.map(v => ({ bftWeight: v.bftWeight, blsKey: v.blsKey })),
certificateThreshold,
);
const bftParams: BFTParameters = {
prevoteThreshold: (BigInt(2) * aggregateBFTWeight) / BigInt(3) + BigInt(1),
precommitThreshold,
Expand Down
22 changes: 6 additions & 16 deletions framework/src/engine/bft/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,9 @@
import { BlockHeader } from '@liskhq/lisk-chain';
import { codec } from '@liskhq/lisk-codec';
import { utils } from '@liskhq/lisk-cryptography';
import { Validator } from '../../abi';
import {
BFTVotesBlockInfo,
ValidatorsHashInfo,
ValidatorsHashInput,
validatorsHashInputSchema,
} from './schemas';
import { BFTVotesBlockInfo, ValidatorsHashInput, validatorsHashInputSchema } from './schemas';
import { BFTHeader } from './types';
import { ActiveValidator } from '../consensus/types';

export const areDistinctHeadersContradicting = (b1: BFTHeader, b2: BFTHeader): boolean => {
let earlierBlock = b1;
Expand Down Expand Up @@ -78,15 +73,10 @@ export const sortValidatorsByAddress = (validators: { address: Buffer }[]) =>
export const sortValidatorsByBLSKey = (validators: { blsKey: Buffer }[]) =>
validators.sort((a, b) => a.blsKey.compare(b.blsKey));

export const computeValidatorsHash = (validators: Validator[], certificateThreshold: bigint) => {
const activeValidators: ValidatorsHashInfo[] = [];
for (const validator of validators) {
activeValidators.push({
blsKey: validator.blsKey,
bftWeight: validator.bftWeight,
});
}
sortValidatorsByBLSKey(activeValidators);
export const computeValidatorsHash = (
activeValidators: ActiveValidator[],
certificateThreshold: bigint,
) => {
const input: ValidatorsHashInput = {
activeValidators,
certificateThreshold,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,10 @@ export class CommitPool {
}

// Validation Step 5
const { validators } = await this._bftMethod.getBFTParameters(methodContext, commit.height);
const { validators } = await this._bftMethod.getBFTParametersActiveValidators(
methodContext,
commit.height,
);
const validator = validators.find(v => v.address.equals(commit.validatorAddress));
if (!validator) {
throw new Error('Commit validator was not active for its height.');
Expand Down Expand Up @@ -238,13 +241,11 @@ export class CommitPool {
aggregationBits: aggregateCommit.aggregationBits,
signature: aggregateCommit.certificateSignature,
};
const { validators, certificateThreshold } = await this._bftMethod.getBFTParameters(
stateStore,
aggregateCommit.height,
);
const { validators: activeValidators, certificateThreshold } =
await this._bftMethod.getBFTParametersActiveValidators(stateStore, aggregateCommit.height);

return verifyAggregateCertificateSignature(
validators,
activeValidators,
certificateThreshold,
this._chain.chainID,
certificate,
Expand All @@ -266,7 +267,10 @@ export class CommitPool {
const { height } = singleCommits[0];

// assuming this list of validators includes all validators corresponding to each singleCommit.validatorAddress
const { validators } = await this._bftMethod.getBFTParameters(methodContext, height);
const { validators } = await this._bftMethod.getBFTParametersActiveValidators(
methodContext,
height,
);
const addressToBlsKey: dataStructures.BufferMap<Buffer> = new dataStructures.BufferMap();
const validatorKeys: Buffer[] = [];

Expand Down Expand Up @@ -330,12 +334,13 @@ export class CommitPool {
...this._nonGossipedCommitsLocal.getByHeight(nextHeight),
...this._gossipedCommits.getByHeight(nextHeight),
];
const nextValidators = singleCommits.map(commit => commit.validatorAddress);
let aggregateBFTWeight = BigInt(0);

// Assume BFT parameters exist for next height
const { validators: bftParamValidators, certificateThreshold } =
await this._bftMethod.getBFTParameters(methodContext, nextHeight);
await this._bftMethod.getBFTParametersActiveValidators(methodContext, nextHeight);

const nextValidators = singleCommits.map(commit => commit.validatorAddress);

for (const matchingAddress of nextValidators) {
const bftParamsValidatorInfo = bftParamValidators.find(bftParamValidator =>
Expand Down Expand Up @@ -408,7 +413,10 @@ export class CommitPool {

// 2. Select commits to gossip
const nextHeight = this._chain.lastBlock.header.height + 1;
const { validators } = await this._bftMethod.getBFTParameters(methodContext, nextHeight);
const { validators } = await this._bftMethod.getBFTParametersActiveValidators(
methodContext,
nextHeight,
);

const maxSelectedCommitsLength = 2 * validators.length;
// Get a list of commits sorted by ascending order of height
Expand Down
5 changes: 5 additions & 0 deletions framework/src/engine/consensus/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export interface Validator {
blsKey: Buffer;
}

export interface ActiveValidator {
bftWeight: bigint;
blsKey: Buffer;
}

export interface PkSigPair {
publicKey: Buffer;
signature: Buffer;
Expand Down
28 changes: 28 additions & 0 deletions framework/src/engine/endpoint/consensus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,34 @@ export class ConsensusEndpoint {
};
}

public async getBFTParametersActiveValidators(ctx: RequestContext): Promise<BFTParametersJSON> {
const stateStore = new StateStore(this._blockchainDB);
const {
certificateThreshold,
precommitThreshold,
prevoteThreshold,
validators,
validatorsHash,
} = await this._bftMethod.getBFTParametersActiveValidators(
stateStore,
ctx.params.height as number,
);

const validatorsJSON = validators.map(v => ({
address: address.getLisk32AddressFromAddress(v.address),
bftWeight: v.bftWeight.toString(),
blsKey: v.blsKey.toString('hex'),
}));

return {
validators: validatorsJSON,
certificateThreshold: certificateThreshold.toString(),
precommitThreshold: precommitThreshold.toString(),
prevoteThreshold: prevoteThreshold.toString(),
validatorsHash: validatorsHash.toString('hex'),
};
}

public async getBFTHeights(_ctx: RequestContext): Promise<BFTHeights> {
const stateStore = new StateStore(this._blockchainDB);
const result = await this._bftMethod.getBFTHeights(stateStore);
Expand Down
2 changes: 1 addition & 1 deletion framework/src/engine/generator/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,7 @@ export class Generator {
blsPK: Buffer,
blsSK: Buffer,
): Promise<void> {
const params = await this._bft.method.getBFTParameters(stateStore, height);
const params = await this._bft.method.getBFTParametersActiveValidators(stateStore, height);
const registeredValidator = params.validators.find(v => v.address.equals(generatorAddress));
if (!registeredValidator) {
return;
Expand Down
8 changes: 7 additions & 1 deletion framework/src/genesis_block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Logger } from './logger';
import { computeValidatorsHash } from './engine';
import { EventQueue, GenesisBlockContext, StateMachine } from './state_machine';
import { PrefixedStateReadWriter } from './state_machine/prefixed_state_read_writer';
import { sortValidatorsByBLSKey } from './engine/bft/utils';

export interface GenesisBlockGenerateInput {
chainID: Buffer;
Expand Down Expand Up @@ -114,8 +115,13 @@ export const generateGenesisBlock = async (
}
}
header.eventRoot = await eventSMT.update(EMPTY_HASH, data);

sortValidatorsByBLSKey(blockCtx.nextValidators.validators);

header.validatorsHash = computeValidatorsHash(
blockCtx.nextValidators.validators,
blockCtx.nextValidators.validators
.filter(v => v.bftWeight > BigInt(0))
.map(v => ({ bftWeight: v.bftWeight, blsKey: v.blsKey })),
blockCtx.nextValidators.certificateThreshold,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,17 +335,25 @@ export abstract class BaseInteroperabilityInternalMethod extends BaseInternalMet
'The number of 1s in the bitmap is not equal to the number of new BFT weights.',
);
}

let bftWeightIndex = 0;
for (let i = 0; i < allBLSKeys.length; i += 1) {
// existing key does not need to be checked
if (!objects.bufferArrayIncludes(blsKeysUpdate, allBLSKeys[i])) {
continue;
}
const blsKey = allBLSKeys[i];
// Get digit of bitmap at index idx (starting from the right).
const digit = (bftWeightsUpdateBitmapBin >> BigInt(i)) & BigInt(1);
if (digit !== BigInt(1)) {
throw new Error('New validators must have a BFT weight update.');
if (objects.bufferArrayIncludes(blsKeysUpdate, blsKey)) {
// New validators must have a BFT weight update.
if (digit !== BigInt(1)) {
throw new Error('New validators must have a BFT weight update.');
}
// New validators must have a positive BFT weight.
// The BFT weight of current validator is specified by bftWeightsUpdate[bftWeightIndex].
if (bftWeightsUpdate[bftWeightIndex] === BigInt(0)) {
throw new Error('New validators must have a positive BFT weight.');
}
}
if (bftWeightsUpdate[i] === BigInt(0)) {
throw new Error('New validators must have a positive BFT weight.');
if (digit === BigInt(1)) {
bftWeightIndex += 1;
}
}

Expand Down
73 changes: 72 additions & 1 deletion framework/test/unit/engine/bft/method.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,12 @@ describe('BFT Method', () => {
generatorKey: utils.getRandomBytes(32),
blsKey: utils.getRandomBytes(48),
},
{
address: utils.getRandomBytes(20),
bftWeight: BigInt(0),
generatorKey: utils.getRandomBytes(32),
blsKey: utils.getRandomBytes(48),
},
],
validatorsHash: utils.getRandomBytes(32),
});
Expand Down Expand Up @@ -243,6 +249,71 @@ describe('BFT Method', () => {
});
});

describe('getBFTParametersActiveValidators', () => {
const createParam = () => ({
prevoteThreshold: BigInt(68),
precommitThreshold: BigInt(68),
certificateThreshold: BigInt(68),
validators: [
{
address: utils.getRandomBytes(20),
bftWeight: BigInt(1),
blsKey: utils.getRandomBytes(48),
generatorKey: utils.getRandomBytes(32),
},
{
address: utils.getRandomBytes(20),
bftWeight: BigInt(1),
generatorKey: utils.getRandomBytes(32),
blsKey: utils.getRandomBytes(48),
},
// Standby validator
{
address: utils.getRandomBytes(20),
bftWeight: BigInt(0),
generatorKey: utils.getRandomBytes(32),
blsKey: utils.getRandomBytes(48),
},
],
validatorsHash: utils.getRandomBytes(32),
});
const params20 = createParam();
const params30 = createParam();
const params20WithOnlyActiveValidators = {
...params20,
validators: params20.validators.filter(v => v.bftWeight > BigInt(0)),
};
const params30WithOnlyActiveValidators = {
...params30,
validators: params30.validators.filter(v => v.bftWeight > BigInt(0)),
};

beforeEach(async () => {
stateStore = new StateStore(new InMemoryDatabase());
const votesStore = stateStore.getStore(MODULE_STORE_PREFIX_BFT, STORE_PREFIX_BFT_PARAMETERS);
await votesStore.setWithSchema(utils.intToBuffer(20, 4), params20, bftParametersSchema);
await votesStore.setWithSchema(utils.intToBuffer(30, 4), params30, bftParametersSchema);
});

it('should return BFT parameters with only active validators if it exists for the lower height', async () => {
await expect(bftMethod.getBFTParametersActiveValidators(stateStore, 25)).resolves.toEqual(
params20WithOnlyActiveValidators,
);
});

it('should return BFT parameters with only active validators if it exists for the height', async () => {
await expect(bftMethod.getBFTParametersActiveValidators(stateStore, 30)).resolves.toEqual(
params30WithOnlyActiveValidators,
);
});

it('should throw if the BFT parameter does not exist for the height or lower', async () => {
await expect(bftMethod.getBFTParametersActiveValidators(stateStore, 19)).rejects.toThrow(
BFTParameterNotFoundError,
);
});
});

describe('getBFTHeights', () => {
const generatorAddress = utils.getRandomBytes(20);

Expand Down Expand Up @@ -1036,7 +1107,7 @@ describe('BFT Method', () => {
const validatorsHash = computeValidatorsHash(accounts, BigInt(99));

const sortedAccounts = [...accounts];
sortedAccounts.sort((a, b) => a.blsKey.compare(b.blsKey));

expect(validatorsHash).toEqual(
utils.hash(
codec.encode(validatorsHashInputSchema, {
Expand Down
Loading