From 7a6485db820149421e24f775efc1d97c7cea4535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Fri, 31 Jan 2025 16:01:08 +0000 Subject: [PATCH 1/5] Test nominating on behalf of pool --- .../kusamaNominationPools.e2e.test.ts.snap | 19 +++ .../polkadotNominationPools.e2e.test.ts.snap | 19 +++ packages/shared/src/nomination-pools.ts | 117 +++++++++++++++++- 3 files changed, 154 insertions(+), 1 deletion(-) diff --git a/packages/kusama/src/__snapshots__/kusamaNominationPools.e2e.test.ts.snap b/packages/kusama/src/__snapshots__/kusamaNominationPools.e2e.test.ts.snap index 002deda5..0de26b16 100644 --- a/packages/kusama/src/__snapshots__/kusamaNominationPools.e2e.test.ts.snap +++ b/packages/kusama/src/__snapshots__/kusamaNominationPools.e2e.test.ts.snap @@ -229,6 +229,25 @@ exports[`Kusama Nomination Pools > nomination pool lifecycle test > join nominat ] `; +exports[`Kusama Nomination Pools > nomination pool lifecycle test > nomination pool validator selection events 1`] = ` +[ + { + "data": { + "dispatchInfo": { + "class": "Normal", + "paysFee": "Yes", + "weight": { + "proofSize": "(rounded 37000)", + "refTime": "(rounded 1400000000)", + }, + }, + }, + "method": "ExtrinsicSuccess", + "section": "system", + }, +] +`; + exports[`Kusama Nomination Pools > nomination pool lifecycle test > set state events 1`] = ` [ { diff --git a/packages/polkadot/src/__snapshots__/polkadotNominationPools.e2e.test.ts.snap b/packages/polkadot/src/__snapshots__/polkadotNominationPools.e2e.test.ts.snap index bda1711d..c74a6ed9 100644 --- a/packages/polkadot/src/__snapshots__/polkadotNominationPools.e2e.test.ts.snap +++ b/packages/polkadot/src/__snapshots__/polkadotNominationPools.e2e.test.ts.snap @@ -229,6 +229,25 @@ exports[`Polkadot Nomination Pools > nomination pool lifecycle test > join nomin ] `; +exports[`Polkadot Nomination Pools > nomination pool lifecycle test > nomination pool validator selection events 1`] = ` +[ + { + "data": { + "dispatchInfo": { + "class": "Normal", + "paysFee": "Yes", + "weight": { + "proofSize": "(rounded 27000)", + "refTime": "(rounded 1100000000)", + }, + }, + }, + "method": "ExtrinsicSuccess", + "section": "system", + }, +] +`; + exports[`Polkadot Nomination Pools > nomination pool lifecycle test > set state events 1`] = ` [ { diff --git a/packages/shared/src/nomination-pools.ts b/packages/shared/src/nomination-pools.ts index 3b1426d0..16d98e0d 100644 --- a/packages/shared/src/nomination-pools.ts +++ b/packages/shared/src/nomination-pools.ts @@ -1,6 +1,6 @@ import { encodeAddress } from '@polkadot/util-crypto' -import { type Chain, defaultAccounts } from '@e2e-test/networks' +import { type Chain, defaultAccounts, defaultAccountsSr25199 } from '@e2e-test/networks' import { setupNetworks } from '@e2e-test/shared' import { check, checkEvents, objectCmp } from './helpers/index.js' @@ -75,6 +75,50 @@ async function createNominationPool( return createNomPoolEvents } +/** + * Select a random set of validators from among the list of validators present in the node's storage. + * + * Only the first page of validators in storage is considered - the size of the page is provided as an argument. + * + * @param api PJS client object. + * @param pageSize The size of the page to fetch from storage. + * @returns + */ +async function getRandomValidators(api: ApiPromise, pageSize: number) { + // Between 1 and 16 validators can be nominated by the pool at any time. + const min_validators = 1 + const max_validators = 16 + assert(pageSize >= max_validators) + + const count = Math.floor(Math.random() * max_validators) + min_validators + + // Query the list of validators from the `Validators` storage item in the `staking` pallet. + const validators = await api.query.staking.validators.keysPaged({ args: [], pageSize: pageSize }) + + const validatorIds = validators.map((key) => key.args[0].toString()) + + const selectedValidators: string[] = [] + + for (let i = 0; i < count; i++) { + const randomIndex = Math.floor(Math.random() * validatorIds.length) + const validatorAddr = validatorIds[randomIndex] + const validator = await api.query.staking.validators(validatorAddr) + + // The pool's nominator should only select validators who still allow for nominators + // to select them i.e. they have not blocked themselves. + if (validator.blocked.isFalse) { + selectedValidators.push(validatorIds[randomIndex]) + } + + // Remove the selected validator to avoid duplicates. Do so whether or not it was blocked. + // If it was blocked, it need not be considered in the next iteration. + // If it wasn't, it's already been selected, and must be removed to avoid duplicate validators. + validatorIds.splice(randomIndex, 1) + } + + return selectedValidators +} + /// ------- /// ------- /// ------- @@ -143,6 +187,7 @@ async function nominationPoolCreationFailureTest< * * 3.4 setting the commission claim permission to permissionless * + * 4. nominating a validator set as the pool's validator * 4. having another other account join the pool * 5. bonding additional funds from this newcomer account to the pool * 6. attempt to claim the pool's (zero) commission as a random account @@ -164,6 +209,7 @@ async function nominationPoolLifecycleTest(relayChain, addressEncoding: number) System: { account: [ [[defaultAccounts.bob.address], { providers: 1, data: { free: 10000e10 } }], + [[defaultAccounts.charlie.address], { providers: 1, data: { free: 10000e10 } }], [[defaultAccounts.dave.address], { providers: 1, data: { free: 10000e10 } }], [[defaultAccounts.eve.address], { providers: 1, data: { free: 10000e10 } }], [[ferdie.address], { providers: 1, data: { free: 10000e10 } }], @@ -321,6 +367,28 @@ async function nominationPoolLifecycleTest(relayChain, addressEncoding: number) await check(nominationPoolWithCommission.commission).toMatchObject(newCommissionData) + /** + * Nominate a validator set + */ + + const validators = await getRandomValidators(relayClient.api, 100) + + const nominateTx = relayClient.api.tx.nominationPools.nominate(nomPoolId, validators) + const nominateEvents = await sendTransaction(nominateTx.signAsync(defaultAccounts.charlie)) + + await relayClient.dev.newBlock() + + await checkEvents(nominateEvents, 'staking', 'nominationPools', 'system') + .redact({ removeKeys: /poolId/ }) + .toMatchSnapshot('nomination pool validator selection events') + + poolData = await relayClient.api.query.nominationPools.bondedPools(nomPoolId) + assert(poolData.isSome, 'Pool should still exist after validators are nominated') + + const nominationPoolAfterNomination = poolData.unwrap() + + nominationPoolCmp(nominationPoolWithCommission, nominationPoolAfterNomination, []) + /** * Have another account join the pool */ @@ -1016,6 +1084,53 @@ async function nominationPoolsUpdateRolesTest< }) } +async function quickTest< + TCustom extends Record | undefined, + TInitStoragesRelay extends Record> | undefined, +>(relayChain: Chain) { + // Create a pool + const [relayClient] = await setupNetworks(relayChain) + + const preLastPoolId = (await relayClient.api.query.nominationPools.lastPoolId()).toNumber() + + const alice = (await defaultAccountsSr25199).keyring.createFromUri('//Alice') + + const validator = '14AkAFBzukRhAFh1wyko1ZoNWnUyq7bY1XbjeTeCHimCzPU1' + + await relayClient.dev.setStorage({ + System: { + account: [[[alice.address], { providers: 1, data: { free: 10000e10 } }]], + }, + }) + + const createNomPoolEvents = await createNominationPool( + relayClient, + alice, + alice.address, + alice.address, + alice.address, + ) + + await relayClient.dev.newBlock() + + await checkEvents(createNomPoolEvents, 'staking', 'nominationPools') + .redact({ removeKeys: /poolId/ }) + .toMatchSnapshot('create nomination pool events') + + const nominateTx = relayClient.api.tx.nominationPools.nominate(preLastPoolId + 1, [validator]) + const nominateEvents = await sendTransaction(nominateTx.signAsync(alice)) + + await relayClient.dev.newBlock() + + relayClient.api.call.nominationPoolsApi.poolAccounts(preLastPoolId + 1) + + await relayClient.pause() + + await checkEvents(nominateEvents, 'staking', 'nominationPools') + .redact({ removeKeys: /poolId/ }) + .toMatchSnapshot('nominate events') +} + export function nominationPoolsE2ETests< TCustom extends Record | undefined, TInitStoragesRelay extends Record> | undefined, From 53438878de165c8d363655d1d056427b40ea56c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Fri, 31 Jan 2025 16:03:18 +0000 Subject: [PATCH 2/5] Update documentation of pool lifecycle test --- packages/shared/src/nomination-pools.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/shared/src/nomination-pools.ts b/packages/shared/src/nomination-pools.ts index 16d98e0d..98865416 100644 --- a/packages/shared/src/nomination-pools.ts +++ b/packages/shared/src/nomination-pools.ts @@ -188,14 +188,14 @@ async function nominationPoolCreationFailureTest< * 3.4 setting the commission claim permission to permissionless * * 4. nominating a validator set as the pool's validator - * 4. having another other account join the pool - * 5. bonding additional funds from this newcomer account to the pool - * 6. attempt to claim the pool's (zero) commission as a random account - * 7. unbonding the additionally bonded funds from the newcomer account - * 8. setting the pool state to blocked - * 9. kicking the newcomer account from the pool as the bouncer - * 10. setting the pool state to destroying - * 11. attempting to unbond the initial depositor's funds (should fail) + * 5. having another other account join the pool + * 6. bonding additional funds from this newcomer account to the pool + * 7. attempt to claim the pool's (zero) commission as a random account + * 8. unbonding the additionally bonded funds from the newcomer account + * 9. setting the pool state to blocked + * 10. kicking the newcomer account from the pool as the bouncer + * 11. setting the pool state to destroying + * 12. attempting to unbond the initial depositor's funds (should fail) * @param relayChain * @param addressEncoding */ @@ -378,6 +378,8 @@ async function nominationPoolLifecycleTest(relayChain, addressEncoding: number) await relayClient.dev.newBlock() + // `nominate` does not emit any events from `staking` or `nominationPools` as of + // Jan. 2025. #7377 will fix this. await checkEvents(nominateEvents, 'staking', 'nominationPools', 'system') .redact({ removeKeys: /poolId/ }) .toMatchSnapshot('nomination pool validator selection events') From ae007646d3ecf630d0f4a2da4287dce3f2e78cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Fri, 31 Jan 2025 18:56:27 +0000 Subject: [PATCH 3/5] Test the `chill` extrinsic --- .../kusamaNominationPools.e2e.test.ts.snap | 30 +++++++++++++++++-- .../polkadotNominationPools.e2e.test.ts.snap | 30 +++++++++++++++++-- packages/shared/src/nomination-pools.ts | 30 ++++++++++++++++--- 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/packages/kusama/src/__snapshots__/kusamaNominationPools.e2e.test.ts.snap b/packages/kusama/src/__snapshots__/kusamaNominationPools.e2e.test.ts.snap index 0de26b16..0585a315 100644 --- a/packages/kusama/src/__snapshots__/kusamaNominationPools.e2e.test.ts.snap +++ b/packages/kusama/src/__snapshots__/kusamaNominationPools.e2e.test.ts.snap @@ -114,6 +114,32 @@ exports[`Kusama Nomination Pools > nomination pool lifecycle test > bond extra f ] `; +exports[`Kusama Nomination Pools > nomination pool lifecycle test > chill events 1`] = ` +[ + { + "data": { + "stash": "F3opxRbN5ZavB4LTn2UYFiR65bYBcdqyfzfmkutumQadysv", + }, + "method": "Chilled", + "section": "staking", + }, + { + "data": { + "dispatchInfo": { + "class": "Normal", + "paysFee": "Yes", + "weight": { + "proofSize": "(rounded 4600)", + "refTime": "(rounded 990000000)", + }, + }, + }, + "method": "ExtrinsicSuccess", + "section": "system", + }, +] +`; + exports[`Kusama Nomination Pools > nomination pool lifecycle test > claim commission events 1`] = ` [ { @@ -237,8 +263,8 @@ exports[`Kusama Nomination Pools > nomination pool lifecycle test > nomination p "class": "Normal", "paysFee": "Yes", "weight": { - "proofSize": "(rounded 37000)", - "refTime": "(rounded 1400000000)", + "proofSize": "(rounded 15000)", + "refTime": "(rounded 1200000000)", }, }, }, diff --git a/packages/polkadot/src/__snapshots__/polkadotNominationPools.e2e.test.ts.snap b/packages/polkadot/src/__snapshots__/polkadotNominationPools.e2e.test.ts.snap index c74a6ed9..2183b727 100644 --- a/packages/polkadot/src/__snapshots__/polkadotNominationPools.e2e.test.ts.snap +++ b/packages/polkadot/src/__snapshots__/polkadotNominationPools.e2e.test.ts.snap @@ -114,6 +114,32 @@ exports[`Polkadot Nomination Pools > nomination pool lifecycle test > bond extra ] `; +exports[`Polkadot Nomination Pools > nomination pool lifecycle test > chill events 1`] = ` +[ + { + "data": { + "stash": "13UVJyLnbVp8c4FQeiGG4xVRNURXD6y3qzyjCZgS7dm34uSE", + }, + "method": "Chilled", + "section": "staking", + }, + { + "data": { + "dispatchInfo": { + "class": "Normal", + "paysFee": "Yes", + "weight": { + "proofSize": "(rounded 4600)", + "refTime": "(rounded 860000000)", + }, + }, + }, + "method": "ExtrinsicSuccess", + "section": "system", + }, +] +`; + exports[`Polkadot Nomination Pools > nomination pool lifecycle test > claim commission events 1`] = ` [ { @@ -237,8 +263,8 @@ exports[`Polkadot Nomination Pools > nomination pool lifecycle test > nomination "class": "Normal", "paysFee": "Yes", "weight": { - "proofSize": "(rounded 27000)", - "refTime": "(rounded 1100000000)", + "proofSize": "(rounded 12000)", + "refTime": "(rounded 1000000000)", }, }, }, diff --git a/packages/shared/src/nomination-pools.ts b/packages/shared/src/nomination-pools.ts index 98865416..d49c734f 100644 --- a/packages/shared/src/nomination-pools.ts +++ b/packages/shared/src/nomination-pools.ts @@ -192,10 +192,11 @@ async function nominationPoolCreationFailureTest< * 6. bonding additional funds from this newcomer account to the pool * 7. attempt to claim the pool's (zero) commission as a random account * 8. unbonding the additionally bonded funds from the newcomer account - * 9. setting the pool state to blocked - * 10. kicking the newcomer account from the pool as the bouncer - * 11. setting the pool state to destroying - * 12. attempting to unbond the initial depositor's funds (should fail) + * 9. moving the pool to the `chill` nominating state, as its nominator + * 10. setting the pool state to blocked + * 11. kicking the newcomer account from the pool as the bouncer + * 12. setting the pool state to destroying + * 13. attempting to unbond the initial depositor's funds (should fail) * @param relayChain * @param addressEncoding */ @@ -496,6 +497,27 @@ async function nominationPoolLifecycleTest(relayChain, addressEncoding: number) assert(nominationPoolPostUnbond.points.eq(depositorMinBond + minJoinBond)) nominationPoolCmp(nominationPoolWithExtraBond, nominationPoolPostUnbond, ['points']) + /** + * As the pool's nominator, call `chill` + */ + + const chillTx = relayClient.api.tx.nominationPools.chill(nomPoolId) + const chillEvents = await sendTransaction(chillTx.signAsync(defaultAccounts.charlie)) + + await relayClient.dev.newBlock() + + // Like `nominate`, `chill` also does not emit any staking/nom. pool events. #7377 also fixes this. + await checkEvents(chillEvents, 'nominationPools', 'staking', 'system') + .redact({ removeKeys: /poolId/ }) + .toMatchSnapshot('chill events') + + poolData = await relayClient.api.query.nominationPools.bondedPools(nomPoolId) + assert(poolData.isSome, 'Pool should still exist after chill') + + const nominationPoolPostChill = poolData.unwrap() + + nominationPoolCmp(nominationPoolPostUnbond, nominationPoolPostChill, []) + /** * Set pool state to blocked */ From 16e5a9e0eed88badd39ad9fe8c0562be9e4bbb10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Fri, 31 Jan 2025 19:24:38 +0000 Subject: [PATCH 4/5] Remove improperly added draft of test --- packages/shared/src/nomination-pools.ts | 51 +------------------------ 1 file changed, 2 insertions(+), 49 deletions(-) diff --git a/packages/shared/src/nomination-pools.ts b/packages/shared/src/nomination-pools.ts index d49c734f..ed3e7e74 100644 --- a/packages/shared/src/nomination-pools.ts +++ b/packages/shared/src/nomination-pools.ts @@ -1,6 +1,6 @@ import { encodeAddress } from '@polkadot/util-crypto' -import { type Chain, defaultAccounts, defaultAccountsSr25199 } from '@e2e-test/networks' +import { type Chain, defaultAccounts } from '@e2e-test/networks' import { setupNetworks } from '@e2e-test/shared' import { check, checkEvents, objectCmp } from './helpers/index.js' @@ -506,7 +506,7 @@ async function nominationPoolLifecycleTest(relayChain, addressEncoding: number) await relayClient.dev.newBlock() - // Like `nominate`, `chill` also does not emit any staking/nom. pool events. #7377 also fixes this. + // Like `nominate`, `chill` also does not emit any nomination pool events. #7377 also fixes this. await checkEvents(chillEvents, 'nominationPools', 'staking', 'system') .redact({ removeKeys: /poolId/ }) .toMatchSnapshot('chill events') @@ -1108,53 +1108,6 @@ async function nominationPoolsUpdateRolesTest< }) } -async function quickTest< - TCustom extends Record | undefined, - TInitStoragesRelay extends Record> | undefined, ->(relayChain: Chain) { - // Create a pool - const [relayClient] = await setupNetworks(relayChain) - - const preLastPoolId = (await relayClient.api.query.nominationPools.lastPoolId()).toNumber() - - const alice = (await defaultAccountsSr25199).keyring.createFromUri('//Alice') - - const validator = '14AkAFBzukRhAFh1wyko1ZoNWnUyq7bY1XbjeTeCHimCzPU1' - - await relayClient.dev.setStorage({ - System: { - account: [[[alice.address], { providers: 1, data: { free: 10000e10 } }]], - }, - }) - - const createNomPoolEvents = await createNominationPool( - relayClient, - alice, - alice.address, - alice.address, - alice.address, - ) - - await relayClient.dev.newBlock() - - await checkEvents(createNomPoolEvents, 'staking', 'nominationPools') - .redact({ removeKeys: /poolId/ }) - .toMatchSnapshot('create nomination pool events') - - const nominateTx = relayClient.api.tx.nominationPools.nominate(preLastPoolId + 1, [validator]) - const nominateEvents = await sendTransaction(nominateTx.signAsync(alice)) - - await relayClient.dev.newBlock() - - relayClient.api.call.nominationPoolsApi.poolAccounts(preLastPoolId + 1) - - await relayClient.pause() - - await checkEvents(nominateEvents, 'staking', 'nominationPools') - .redact({ removeKeys: /poolId/ }) - .toMatchSnapshot('nominate events') -} - export function nominationPoolsE2ETests< TCustom extends Record | undefined, TInitStoragesRelay extends Record> | undefined, From ca9513f1cf8e8f705d0a90dd008384bdea59c6f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Mon, 3 Feb 2025 15:04:08 +0000 Subject: [PATCH 5/5] Address PR review (validator selection) --- .../kusamaNominationPools.e2e.test.ts.snap | 4 +- .../polkadotNominationPools.e2e.test.ts.snap | 4 +- packages/shared/src/nomination-pools.ts | 55 +++++++++++-------- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/packages/kusama/src/__snapshots__/kusamaNominationPools.e2e.test.ts.snap b/packages/kusama/src/__snapshots__/kusamaNominationPools.e2e.test.ts.snap index 0585a315..239f2dcd 100644 --- a/packages/kusama/src/__snapshots__/kusamaNominationPools.e2e.test.ts.snap +++ b/packages/kusama/src/__snapshots__/kusamaNominationPools.e2e.test.ts.snap @@ -263,8 +263,8 @@ exports[`Kusama Nomination Pools > nomination pool lifecycle test > nomination p "class": "Normal", "paysFee": "Yes", "weight": { - "proofSize": "(rounded 15000)", - "refTime": "(rounded 1200000000)", + "proofSize": "(rounded 45000)", + "refTime": "(rounded 1500000000)", }, }, }, diff --git a/packages/polkadot/src/__snapshots__/polkadotNominationPools.e2e.test.ts.snap b/packages/polkadot/src/__snapshots__/polkadotNominationPools.e2e.test.ts.snap index 2183b727..dde8f93d 100644 --- a/packages/polkadot/src/__snapshots__/polkadotNominationPools.e2e.test.ts.snap +++ b/packages/polkadot/src/__snapshots__/polkadotNominationPools.e2e.test.ts.snap @@ -263,8 +263,8 @@ exports[`Polkadot Nomination Pools > nomination pool lifecycle test > nomination "class": "Normal", "paysFee": "Yes", "weight": { - "proofSize": "(rounded 12000)", - "refTime": "(rounded 1000000000)", + "proofSize": "(rounded 45000)", + "refTime": "(rounded 1300000000)", }, }, }, diff --git a/packages/shared/src/nomination-pools.ts b/packages/shared/src/nomination-pools.ts index ed3e7e74..b95e982e 100644 --- a/packages/shared/src/nomination-pools.ts +++ b/packages/shared/src/nomination-pools.ts @@ -8,7 +8,7 @@ import { sendTransaction } from '@acala-network/chopsticks-testing' import type { ApiPromise } from '@polkadot/api' import type { KeyringPair } from '@polkadot/keyring/types' import { type Option, u32 } from '@polkadot/types' -import type { PalletNominationPoolsBondedPoolInner } from '@polkadot/types/lookup' +import type { PalletNominationPoolsBondedPoolInner, PalletStakingValidatorPrefs } from '@polkadot/types/lookup' import type { Codec } from '@polkadot/types/types' import { assert, describe, test } from 'vitest' @@ -76,46 +76,54 @@ async function createNominationPool( } /** - * Select a random set of validators from among the list of validators present in the node's storage. + * Select some validators from the list present in the `Validators` storage item, in the `Staking` pallet. * - * Only the first page of validators in storage is considered - the size of the page is provided as an argument. + * To avoid fetching all validators at once (over a thousand in Jan. 2025), only the first page of validators + * in storage is considered - the size of the page is provided as an argument. + * + * If, in the validator page of the selected size, less than `validatorCount` validators are available, the function + * will get as close to `validatorCount` as possible. * * @param api PJS client object. * @param pageSize The size of the page to fetch from storage. - * @returns + * @param validatorCount The (desired) number of validators to select. + * @returns A list of at least 1 validator, and at most 16. */ -async function getRandomValidators(api: ApiPromise, pageSize: number) { +async function getValidators(api: ApiPromise, pageSize: number, validatorCount: number) { // Between 1 and 16 validators can be nominated by the pool at any time. const min_validators = 1 const max_validators = 16 - assert(pageSize >= max_validators) - const count = Math.floor(Math.random() * max_validators) + min_validators + assert(pageSize >= max_validators) + assert(min_validators <= validatorCount && validatorCount <= max_validators) // Query the list of validators from the `Validators` storage item in the `staking` pallet. - const validators = await api.query.staking.validators.keysPaged({ args: [], pageSize: pageSize }) + const validators = await api.query.staking.validators.entriesPaged({ args: [], pageSize: pageSize }) - const validatorIds = validators.map((key) => key.args[0].toString()) + const validatorIds: [string, PalletStakingValidatorPrefs][] = validators.map((tuple) => [ + tuple[0].args[0].toString(), + tuple[1], + ]) const selectedValidators: string[] = [] - for (let i = 0; i < count; i++) { - const randomIndex = Math.floor(Math.random() * validatorIds.length) - const validatorAddr = validatorIds[randomIndex] - const validator = await api.query.staking.validators(validatorAddr) + let ix = 0 + let count = 0 + while (count < validatorCount) { + const [valAddr, valData] = validatorIds[ix] // The pool's nominator should only select validators who still allow for nominators // to select them i.e. they have not blocked themselves. - if (validator.blocked.isFalse) { - selectedValidators.push(validatorIds[randomIndex]) + if (valData.blocked.isFalse) { + selectedValidators.push(valAddr) + count += 1 } - // Remove the selected validator to avoid duplicates. Do so whether or not it was blocked. - // If it was blocked, it need not be considered in the next iteration. - // If it wasn't, it's already been selected, and must be removed to avoid duplicate validators. - validatorIds.splice(randomIndex, 1) + ix += 1 } + assert(selectedValidators.length >= min_validators && selectedValidators.length <= max_validators) + return selectedValidators } @@ -372,15 +380,15 @@ async function nominationPoolLifecycleTest(relayChain, addressEncoding: number) * Nominate a validator set */ - const validators = await getRandomValidators(relayClient.api, 100) + const validators = await getValidators(relayClient.api, 100, 16) const nominateTx = relayClient.api.tx.nominationPools.nominate(nomPoolId, validators) const nominateEvents = await sendTransaction(nominateTx.signAsync(defaultAccounts.charlie)) await relayClient.dev.newBlock() - // `nominate` does not emit any events from `staking` or `nominationPools` as of - // Jan. 2025. #7377 will fix this. + // TODO: `nominate` does not emit any events from `staking` or `nominationPools` as of + // Jan. 2025. [#7377](https://github.com/paritytech/polkadot-sdk/pull/7377) will fix this. await checkEvents(nominateEvents, 'staking', 'nominationPools', 'system') .redact({ removeKeys: /poolId/ }) .toMatchSnapshot('nomination pool validator selection events') @@ -506,7 +514,8 @@ async function nominationPoolLifecycleTest(relayChain, addressEncoding: number) await relayClient.dev.newBlock() - // Like `nominate`, `chill` also does not emit any nomination pool events. #7377 also fixes this. + // TODO: Like `nominate`, `chill` also does not emit any nomination pool events. + // [#7377](https://github.com/paritytech/polkadot-sdk/pull/7377) also fixes this. await checkEvents(chillEvents, 'nominationPools', 'staking', 'system') .redact({ removeKeys: /poolId/ }) .toMatchSnapshot('chill events')