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

Commit

Permalink
Align getRandomSeed() implementation to protocol changes from LIP-46 (#…
Browse files Browse the repository at this point in the history
…8570)

* Align getRandomSeed() implementation to protocol changes from LIP-46

* Replace number literal 16 with constant SEED_LENGTH

* Update test case which excludes invalid seed reveal
  • Loading branch information
bobanm authored Jun 14, 2023
1 parent fe67ea1 commit c225b00
Show file tree
Hide file tree
Showing 9 changed files with 92 additions and 97 deletions.
12 changes: 4 additions & 8 deletions framework/src/modules/random/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
* Removal or modification of this copyright notice is prohibited.
*/

import * as cryptography from '@liskhq/lisk-cryptography';
import { utils } from '@liskhq/lisk-cryptography';
import { SEED_LENGTH } from './constants';
import { ValidatorSeedReveal } from './stores/validator_reveals';
Expand All @@ -35,7 +34,7 @@ export const isSeedValidInput = (
if (!lastSeed) {
return false;
}
return lastSeed.seedReveal.equals(cryptography.utils.hash(seedReveal).slice(0, SEED_LENGTH));
return lastSeed.seedReveal.equals(utils.hash(seedReveal).slice(0, SEED_LENGTH));
};

export const getSeedRevealValidity = (
Expand All @@ -56,10 +55,7 @@ export const getSeedRevealValidity = (
}
}

return (
!lastSeed ||
lastSeed.seedReveal.equals(cryptography.utils.hash(seedReveal).slice(0, SEED_LENGTH))
);
return !lastSeed || lastSeed.seedReveal.equals(utils.hash(seedReveal).slice(0, SEED_LENGTH));
};

export const getRandomSeed = (
Expand All @@ -77,14 +73,14 @@ export const getRandomSeed = (
throw new Error('Number of seeds cannot be greater than 1000.');
}
const initRandomBuffer = utils.intToBuffer(height + numberOfSeeds, 4);
let randomSeed = cryptography.utils.hash(initRandomBuffer).slice(0, 16);
let randomSeed = utils.hash(initRandomBuffer).slice(0, SEED_LENGTH);

let isInFuture = true;
const currentSeeds = [];
for (const validatorReveal of validatorsReveal) {
if (validatorReveal.height >= height) {
isInFuture = false;
if (validatorReveal.height <= height + numberOfSeeds) {
if (validatorReveal.height < height + numberOfSeeds) {
currentSeeds.push(validatorReveal);
}
}
Expand Down
156 changes: 78 additions & 78 deletions framework/test/unit/modules/random/method.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ import { bitwiseXOR } from '../../../../src/modules/random/utils';
import { MethodContext } from '../../../../src/state_machine';
import { createTransientMethodContext } from '../../../../src/testing';
import * as genesisValidators from '../../../fixtures/genesis_validators.json';
import { testCases } from './pos_random_seed_generation/pos_random_seed_generation_other_rounds.json';
import * as randomSeedsMultipleRounds from '../../../fixtures/pos_random_seed_generation/pos_random_seed_generation_other_rounds.json';
import { testCases } from '../../../fixtures/pos_random_seed_generation/pos_random_seed_generation_other_rounds.json';
import { RandomModule } from '../../../../src/modules/random';
import {
ValidatorRevealsStore,
Expand All @@ -40,7 +39,7 @@ describe('RandomModuleMethod', () => {
let randomStore: ValidatorRevealsStore;

const randomModule = new RandomModule();
const emptyBytes = Buffer.alloc(0);
const EMPTY_BYTES = Buffer.alloc(0);

describe('isSeedRevealValid', () => {
const twoRoundsValidators: ValidatorSeedReveal[] = [];
Expand Down Expand Up @@ -69,7 +68,7 @@ describe('RandomModuleMethod', () => {
randomMethod = new RandomMethod(randomModule.stores, randomModule.events, randomModule.name);
context = createTransientMethodContext({});
randomStore = randomModule.stores.get(ValidatorRevealsStore);
await randomStore.set(context, emptyBytes, {
await randomStore.set(context, EMPTY_BYTES, {
validatorReveals: twoRoundsValidators.slice(0, 103),
});
});
Expand Down Expand Up @@ -111,7 +110,7 @@ describe('RandomModuleMethod', () => {

it('should return true if no last seed reveal found', async () => {
// Arrange
await randomStore.set(context, emptyBytes, { validatorReveals: [] });
await randomStore.set(context, EMPTY_BYTES, { validatorReveals: [] });
for (const [address, hashes] of Object.entries(twoRoundsValidatorsHashes)) {
const blockAsset: BlockAsset = {
module: randomModule.name,
Expand All @@ -129,7 +128,7 @@ describe('RandomModuleMethod', () => {
});

it('should return false if there is a last revealed seed by generatorAddress in validatorReveals array but it is not equal to the hash of seedReveal', async () => {
await randomStore.set(context, emptyBytes, { validatorReveals: twoRoundsValidators });
await randomStore.set(context, EMPTY_BYTES, { validatorReveals: twoRoundsValidators });
for (const [address, hashes] of Object.entries(twoRoundsValidatorsHashes)) {
// Arrange
const blockAsset: BlockAsset = {
Expand All @@ -152,7 +151,7 @@ describe('RandomModuleMethod', () => {
const { generatorAddress } = twoRoundsValidators[5];
const twoRoundsValidatorsClone1 = objects.cloneDeep(twoRoundsValidators);
twoRoundsValidatorsClone1[5].generatorAddress = Buffer.alloc(0);
await randomStore.set(context, emptyBytes, {
await randomStore.set(context, EMPTY_BYTES, {
validatorReveals: twoRoundsValidatorsClone1.slice(0, 103),
});
const hashes = twoRoundsValidatorsHashes[generatorAddress.toString('hex')];
Expand All @@ -175,7 +174,7 @@ describe('RandomModuleMethod', () => {
const { generatorAddress } = twoRoundsValidators[5];
const twoRoundsValidatorsClone2 = twoRoundsValidators;
twoRoundsValidatorsClone2[5].seedReveal = cryptography.utils.getRandomBytes(17);
await randomStore.set(context, emptyBytes, {
await randomStore.set(context, EMPTY_BYTES, {
validatorReveals: twoRoundsValidatorsClone2.slice(0, 103),
});
const hashes = twoRoundsValidatorsHashes[generatorAddress.toString('hex')];
Expand All @@ -198,7 +197,7 @@ describe('RandomModuleMethod', () => {
const generatorAddress = cryptography.utils.getRandomBytes(21);
const twoRoundsValidatorsClone3 = objects.cloneDeep(twoRoundsValidators);
twoRoundsValidatorsClone3[5].generatorAddress = generatorAddress;
await randomStore.set(context, emptyBytes, {
await randomStore.set(context, EMPTY_BYTES, {
validatorReveals: twoRoundsValidatorsClone3.slice(0, 103),
});
const hashes =
Expand Down Expand Up @@ -262,7 +261,7 @@ describe('RandomModuleMethod', () => {
randomMethod = new RandomMethod(randomModule.stores, randomModule.events, randomModule.name);
context = createTransientMethodContext({});
randomStore = randomModule.stores.get(ValidatorRevealsStore);
await randomStore.set(context, emptyBytes, { validatorReveals: validatorsData });
await randomStore.set(context, EMPTY_BYTES, { validatorReveals: validatorsData });
});

it('should throw error when height is negative', async () => {
Expand Down Expand Up @@ -319,9 +318,9 @@ describe('RandomModuleMethod', () => {
);
});

it('should return XOR random bytes as 16 bytes value for height=11, numberOfSeeds=2', async () => {
it('should return XOR random bytes as 16 bytes value for height=11, numberOfSeeds=3', async () => {
const height = 11;
const numberOfSeeds = 2;
const numberOfSeeds = 3;
// Create a buffer from height + numberOfSeeds
const randomSeed = strippedHashOfIntegerBuffer(height + numberOfSeeds);

Expand All @@ -341,9 +340,9 @@ describe('RandomModuleMethod', () => {
);
});

it('should return XOR random bytes for height=11, numberOfSeeds=3', async () => {
it('should return XOR random bytes for height=11, numberOfSeeds=4', async () => {
const height = 11;
const numberOfSeeds = 3;
const numberOfSeeds = 4;
// Create a buffer from height + numberOfSeeds
const randomSeed = strippedHashOfIntegerBuffer(height + numberOfSeeds);

Expand All @@ -363,9 +362,9 @@ describe('RandomModuleMethod', () => {
);
});

it('should return XOR random bytes for height=11, numberOfSeeds=4 excluding invalid seed reveal', async () => {
it('should return XOR random bytes for height=11, numberOfSeeds=5 excluding invalid seed reveal', async () => {
const height = 11;
const numberOfSeeds = 4;
const numberOfSeeds = 5;
// Create a buffer from height + numberOfSeeds
const randomSeed = strippedHashOfIntegerBuffer(height + numberOfSeeds);

Expand All @@ -385,9 +384,9 @@ describe('RandomModuleMethod', () => {
);
});

it('should return XOR random bytes for height=8, numberOfSeeds=3', async () => {
it('should return XOR random bytes for height=8, numberOfSeeds=4', async () => {
const height = 8;
const numberOfSeeds = 3;
const numberOfSeeds = 4;
// Create a buffer from height + numberOfSeeds
const randomSeed = strippedHashOfIntegerBuffer(height + numberOfSeeds);

Expand Down Expand Up @@ -423,67 +422,68 @@ describe('RandomModuleMethod', () => {
});
});

describe('generateRandomSeeds', () => {
describe.each(
[...randomSeedsMultipleRounds.testCases].map(testCase => [testCase.description, testCase]),
)('%s', (_description, testCase) => {
// Arrange
const { config, input, output } = testCase as any;
const validators: ValidatorSeedReveal[] = [];

for (const generator of input.blocks) {
const generatorAddress = cryptography.address.getAddressFromPublicKey(
Buffer.from(generator.generatorPublicKey, 'hex'),
);
const seedReveal = Buffer.from(generator.asset.seedReveal, 'hex');

validators.push({
generatorAddress,
seedReveal,
height: generator.height,
valid: true,
describe('getRandomBytes from protocol specs', () => {
describe.each([...testCases].map(testCase => [testCase.description, testCase]))(
'%s',
(_description, testCase) => {
// Arrange
const { config, input, output } = testCase as any;
const validators: ValidatorSeedReveal[] = [];

for (const generator of input.blocks) {
const generatorAddress = cryptography.address.getAddressFromPublicKey(
Buffer.from(generator.generatorPublicKey, 'hex'),
);
const seedReveal = Buffer.from(generator.asset.seedReveal, 'hex');

validators.push({
generatorAddress,
seedReveal,
height: generator.height,
valid: true,
});
}

beforeEach(async () => {
randomMethod = new RandomMethod(
randomModule.stores,
randomModule.events,
randomModule.name,
);
context = createTransientMethodContext({});
randomStore = randomModule.stores.get(ValidatorRevealsStore);
await randomStore.set(context, EMPTY_BYTES, { validatorReveals: validators });
});
}

beforeEach(async () => {
randomMethod = new RandomMethod(
randomModule.stores,
randomModule.events,
randomModule.name,
);
context = createTransientMethodContext({});
randomStore = randomModule.stores.get(ValidatorRevealsStore);
await randomStore.set(context, emptyBytes, { validatorReveals: validators });
});

it('should generate correct random seeds', async () => {
// Arrange
// For randomSeed 1
const round = Math.floor(
input.blocks[input.blocks.length - 1].height / config.blocksPerRound,
);
const middleThreshold = Math.floor(config.blocksPerRound / 2);
const startOfRound = config.blocksPerRound * (round - 1) + 1;
// To validate seed reveal of any block in the last round we have to check till second last round that doesn't exist for last round
const heightForSeed1 = startOfRound - (round === 2 ? 0 : middleThreshold);
// For randomSeed 2
const endOfLastRound = startOfRound - 1;
const startOfLastRound = endOfLastRound - config.blocksPerRound + 1;
// Act
const randomSeed1 = await randomMethod.getRandomBytes(
context,
heightForSeed1,
round === 2 ? middleThreshold : middleThreshold * 2,
);
// There is previous round for last round when round is 2
const randomSeed2 =
round === 2
? strippedHashOfIntegerBuffer(endOfLastRound)
: await randomMethod.getRandomBytes(context, startOfLastRound, middleThreshold * 2);
// Assert
expect(randomSeed1.toString('hex')).toEqual(output.randomSeed1);
expect(randomSeed2.toString('hex')).toEqual(output.randomSeed2);
});
});
it('should generate correct random seeds', async () => {
// Arrange
// For randomSeed 1
const round = Math.floor(
input.blocks[input.blocks.length - 1].height / config.blocksPerRound,
);
const middleThreshold = Math.floor(config.blocksPerRound / 2);
const startOfRound = config.blocksPerRound * (round - 1) + 1;
// To validate seed reveal of any block in the last round we have to check till second last round that doesn't exist for last round
const heightForSeed1 = startOfRound - (round === 2 ? 0 : middleThreshold);
// For randomSeed 2
const endOfLastRound = startOfRound - 1;
const startOfLastRound = endOfLastRound - config.blocksPerRound + 1;
// Act
const randomSeed1 = await randomMethod.getRandomBytes(
context,
heightForSeed1,
round === 2 ? middleThreshold : middleThreshold * 2,
);
// There is previous round for last round when round is 2
const randomSeed2 =
round === 2
? strippedHashOfIntegerBuffer(endOfLastRound)
: await randomMethod.getRandomBytes(context, startOfLastRound, middleThreshold * 2);
// Assert
expect(randomSeed1.toString('hex')).toEqual(output.randomSeed1);
expect(randomSeed2.toString('hex')).toEqual(output.randomSeed2);
});
},
);
});
});
2 changes: 1 addition & 1 deletion framework/test/unit/modules/random/module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('RandomModule', () => {
it('should initialize config with default value when module config is empty', async () => {
await expect(
randomModule.init({
genesisConfig: {} as any,
genesisConfig: {} as GenesisConfig,
moduleConfig: {},
}),
).toResolve();
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -1459,7 +1459,7 @@
]
},
"output": {
"randomSeed1": "d900abc349e48a4ab42922f8a55725d4",
"randomSeed1": "d42310bf046b9b6568b0a12650d774e7",
"randomSeed2": "1ebc2c57112bc594c7ca32a76a89f70e"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1459,7 +1459,7 @@
]
},
"output": {
"randomSeed1": "73997b60c3b9c86aab326e2aed125dc2",
"randomSeed1": "7ebac01c8e36d94577abedf418920cf1",
"randomSeed2": "1ebc2c57112bc594c7ca32a76a89f70e"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1459,7 +1459,7 @@
]
},
"output": {
"randomSeed1": "283f543e68fea3c08e976ef66acd3586",
"randomSeed1": "251cef422571b2ef520eed289f4d64b5",
"randomSeed2": "1ebc2c57112bc594c7ca32a76a89f70e"
}
},
Expand Down Expand Up @@ -3636,8 +3636,8 @@
]
},
"output": {
"randomSeed1": "3258f0441e5d12360684a9c965ce37f0",
"randomSeed2": "354c87fa7674a8061920b9daafce92af"
"randomSeed1": "e2da98258bc71904b892a5c076fb0a35",
"randomSeed2": "cf19a137331cd3a23a4ce47ff8e63906"
}
},
{
Expand Down Expand Up @@ -7255,8 +7255,8 @@
]
},
"output": {
"randomSeed1": "16b7720b20b50ef49b9bd66b6a2034e7",
"randomSeed2": "7838d2b53d7250254d5a2ce5ff36dc5a"
"randomSeed1": "f81be5b77f53a8c23c362ed635739a2c",
"randomSeed2": "09a929cf02f58d22db53873b33ebf5b4"
}
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A set of test generators for random seed generation by LIP-0022

#### Input

- blocksPerRound: Number of rounds per blocks
- blocksPerRound: Number of blocks per round
- blocks: Blocks of at least last 3 rounds

#### Output
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ const isValidSeedReveal = (seedReveal, previousSeedReveal) =>
const selectSeedReveal = ({ fromHeight, toHeight, blocksMap, blocksPerRound }) => {
const selected = [];

for (let i = fromHeight; i >= toHeight; i -= 1) {
for (let i = fromHeight - 1; i >= toHeight; i -= 1) {
const block = blocksMap[i];
const blockRound = calcRound(block.height, blocksPerRound);

Expand Down

0 comments on commit c225b00

Please sign in to comment.