From 5733441cf791b9cc93c8a6fa1357418bb5697331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boban=20Milo=C5=A1evi=C4=87?= Date: Wed, 7 Jun 2023 15:49:03 +0200 Subject: [PATCH 1/3] Align getRandomSeed() implementation to protocol changes from LIP-46 --- framework/src/modules/random/utils.ts | 12 +- .../test/unit/modules/random/method.spec.ts | 136 +++++++++--------- .../test/unit/modules/random/module.spec.ts | 2 +- .../modules/random/pos_random_seed_generation | 1 - ...m_seed_generation_invalid_seed_reveal.json | 2 +- ...om_seed_generation_not_forged_earlier.json | 2 +- ...s_random_seed_generation_other_rounds.json | 10 +- .../pos_random_seed_generation/README.md | 2 +- .../pos_random_seed_generation/index.js | 2 +- 9 files changed, 82 insertions(+), 87 deletions(-) delete mode 120000 framework/test/unit/modules/random/pos_random_seed_generation diff --git a/framework/src/modules/random/utils.ts b/framework/src/modules/random/utils.ts index 4d9bae67b41..1c22676716c 100644 --- a/framework/src/modules/random/utils.ts +++ b/framework/src/modules/random/utils.ts @@ -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'; @@ -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 = ( @@ -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 = ( @@ -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, 16); 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); } } diff --git a/framework/test/unit/modules/random/method.spec.ts b/framework/test/unit/modules/random/method.spec.ts index 49e8747bcac..ee532a30b2b 100644 --- a/framework/test/unit/modules/random/method.spec.ts +++ b/framework/test/unit/modules/random/method.spec.ts @@ -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, @@ -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); @@ -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); @@ -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); @@ -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, emptyBytes, { 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); + }); + }, + ); }); }); diff --git a/framework/test/unit/modules/random/module.spec.ts b/framework/test/unit/modules/random/module.spec.ts index 023775c00cd..cc12119f634 100644 --- a/framework/test/unit/modules/random/module.spec.ts +++ b/framework/test/unit/modules/random/module.spec.ts @@ -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(); diff --git a/framework/test/unit/modules/random/pos_random_seed_generation b/framework/test/unit/modules/random/pos_random_seed_generation deleted file mode 120000 index 83b38c2de61..00000000000 --- a/framework/test/unit/modules/random/pos_random_seed_generation +++ /dev/null @@ -1 +0,0 @@ -../../../../../protocol-specs/generator_outputs/dpos_random_seed_generation \ No newline at end of file diff --git a/protocol-specs/generator_outputs/dpos_random_seed_generation/pos_random_seed_generation_invalid_seed_reveal.json b/protocol-specs/generator_outputs/dpos_random_seed_generation/pos_random_seed_generation_invalid_seed_reveal.json index 636ec813cf2..c73287e764a 100644 --- a/protocol-specs/generator_outputs/dpos_random_seed_generation/pos_random_seed_generation_invalid_seed_reveal.json +++ b/protocol-specs/generator_outputs/dpos_random_seed_generation/pos_random_seed_generation_invalid_seed_reveal.json @@ -1459,7 +1459,7 @@ ] }, "output": { - "randomSeed1": "d900abc349e48a4ab42922f8a55725d4", + "randomSeed1": "d42310bf046b9b6568b0a12650d774e7", "randomSeed2": "1ebc2c57112bc594c7ca32a76a89f70e" } } diff --git a/protocol-specs/generator_outputs/dpos_random_seed_generation/pos_random_seed_generation_not_forged_earlier.json b/protocol-specs/generator_outputs/dpos_random_seed_generation/pos_random_seed_generation_not_forged_earlier.json index 21799168a37..9d78cdbe061 100644 --- a/protocol-specs/generator_outputs/dpos_random_seed_generation/pos_random_seed_generation_not_forged_earlier.json +++ b/protocol-specs/generator_outputs/dpos_random_seed_generation/pos_random_seed_generation_not_forged_earlier.json @@ -1459,7 +1459,7 @@ ] }, "output": { - "randomSeed1": "73997b60c3b9c86aab326e2aed125dc2", + "randomSeed1": "7ebac01c8e36d94577abedf418920cf1", "randomSeed2": "1ebc2c57112bc594c7ca32a76a89f70e" } } diff --git a/protocol-specs/generator_outputs/dpos_random_seed_generation/pos_random_seed_generation_other_rounds.json b/protocol-specs/generator_outputs/dpos_random_seed_generation/pos_random_seed_generation_other_rounds.json index 77a80f49e39..996e736c4e8 100644 --- a/protocol-specs/generator_outputs/dpos_random_seed_generation/pos_random_seed_generation_other_rounds.json +++ b/protocol-specs/generator_outputs/dpos_random_seed_generation/pos_random_seed_generation_other_rounds.json @@ -1459,7 +1459,7 @@ ] }, "output": { - "randomSeed1": "283f543e68fea3c08e976ef66acd3586", + "randomSeed1": "251cef422571b2ef520eed289f4d64b5", "randomSeed2": "1ebc2c57112bc594c7ca32a76a89f70e" } }, @@ -3636,8 +3636,8 @@ ] }, "output": { - "randomSeed1": "3258f0441e5d12360684a9c965ce37f0", - "randomSeed2": "354c87fa7674a8061920b9daafce92af" + "randomSeed1": "e2da98258bc71904b892a5c076fb0a35", + "randomSeed2": "cf19a137331cd3a23a4ce47ff8e63906" } }, { @@ -7255,8 +7255,8 @@ ] }, "output": { - "randomSeed1": "16b7720b20b50ef49b9bd66b6a2034e7", - "randomSeed2": "7838d2b53d7250254d5a2ce5ff36dc5a" + "randomSeed1": "f81be5b77f53a8c23c362ed635739a2c", + "randomSeed2": "09a929cf02f58d22db53873b33ebf5b4" } } ] diff --git a/protocol-specs/generators/pos_random_seed_generation/README.md b/protocol-specs/generators/pos_random_seed_generation/README.md index edab3afc380..2abd568b37a 100644 --- a/protocol-specs/generators/pos_random_seed_generation/README.md +++ b/protocol-specs/generators/pos_random_seed_generation/README.md @@ -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 diff --git a/protocol-specs/generators/pos_random_seed_generation/index.js b/protocol-specs/generators/pos_random_seed_generation/index.js index 7d4c930f156..e7324eec595 100644 --- a/protocol-specs/generators/pos_random_seed_generation/index.js +++ b/protocol-specs/generators/pos_random_seed_generation/index.js @@ -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); From 8333be169b5237b17f8126cb4ce222ff6da0a218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boban=20Milo=C5=A1evi=C4=87?= Date: Mon, 12 Jun 2023 08:31:15 +0200 Subject: [PATCH 2/3] Replace number literal 16 with constant SEED_LENGTH Co-authored-by: shuse2 --- framework/src/modules/random/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/modules/random/utils.ts b/framework/src/modules/random/utils.ts index 1c22676716c..6e17b41b2c7 100644 --- a/framework/src/modules/random/utils.ts +++ b/framework/src/modules/random/utils.ts @@ -73,7 +73,7 @@ export const getRandomSeed = ( throw new Error('Number of seeds cannot be greater than 1000.'); } const initRandomBuffer = utils.intToBuffer(height + numberOfSeeds, 4); - let randomSeed = utils.hash(initRandomBuffer).slice(0, 16); + let randomSeed = utils.hash(initRandomBuffer).slice(0, SEED_LENGTH); let isInFuture = true; const currentSeeds = []; From d8b0eadb7a76451065f054c7acb503494aef6d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boban=20Milo=C5=A1evi=C4=87?= Date: Tue, 13 Jun 2023 04:27:58 +0200 Subject: [PATCH 3/3] Update test case which excludes invalid seed reveal --- .../test/unit/modules/random/method.spec.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/framework/test/unit/modules/random/method.spec.ts b/framework/test/unit/modules/random/method.spec.ts index ee532a30b2b..a9e22fe6259 100644 --- a/framework/test/unit/modules/random/method.spec.ts +++ b/framework/test/unit/modules/random/method.spec.ts @@ -39,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[] = []; @@ -68,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), }); }); @@ -110,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, @@ -128,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 = { @@ -151,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')]; @@ -174,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')]; @@ -197,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 = @@ -261,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 () => { @@ -362,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); @@ -452,7 +452,7 @@ describe('RandomModuleMethod', () => { ); context = createTransientMethodContext({}); randomStore = randomModule.stores.get(ValidatorRevealsStore); - await randomStore.set(context, emptyBytes, { validatorReveals: validators }); + await randomStore.set(context, EMPTY_BYTES, { validatorReveals: validators }); }); it('should generate correct random seeds', async () => {