Fairly Electin' Validators!

Good morrow amigos! Q3 of 2022 is almost over, but that doesn’t mean we’re done with our work! No sir, we here at Bearmint are workin’ tirelessly to ensure that we get everythin’ ready fer when we launch - it ain’t easy work, but we can assure you that we’re more than up to the task!

In Proof-of-Stake (PoS) blockchains, as well as the variants of PoS, validators play a crucial role in securin’ and runnin’ the network. But just how do validators get selected? Well, to put it simply, they get elected! That means that delegators (or voters) need to vote fer validators that they believe are up to the task and capable of runnin’ the blockchain in a responsible and efficient manner.

Now in blockchains, you can’t just show up to a pollin’ station and cast a vote or anythin’ like that! Nope, validators need to be elected by delegators who stake their tokens as a show of support for the validator of their choice. And as you can imagine, the ability to stake tokens and elect validators doesn’t just happen out of thin air. Fer this reason you’ll need to build and implement a validator elector module that’ll take care of all of this fer you!

So in today’s extremely helpful (and fairly lengthy) guide, we’ll show you how to create a validator elector mechanism so that the various participants in yer blockchain can vote fer their favorite validators and do their part to make sure that consensus is reached without any issues! As usual, before we get into the guide, we ask that you briefly look over the followin’ definitions so that when you see these terms pop up in the code, you’ll know exactly what they refer to! Here we go!

Can You Run Them Terms by Me Again, Good Sir?

  • @bearmint/bep13 is an NPM package that offers an implementation of BEP-013 which allows for the sharing of a set of types between all the modules that exist within Bearmint
  • @bearmint/bep16 is an NPM package that offers an implementation of BEP-016 which contains a standardized method of bootstrapping the plugins and application via service providers
  • @bearmint/bep109 is an NPM package that offers an implementation of BEP-109 which provides a single module that exposes various exceptions and allows for sharing between modules without circular dependencies arising

Checking if Power Falls Within a Configured Range

Bearmint can limit the amount of power a validator is allowed to hold or the minimum amount of power it has to hold. The following function validates this range with any validators falling outside of said range being denied the right to participate in consensus.

function inPowerRange({
account,
range,
}: {
account: AccountWithValidator;
range: { min: number; max: number };
}) {
if (range.max === 0) {
return account.validator.power.isGreaterThanEqual(range.min);
}
return account.validator.power.isBetween(range.min, range.max);
}

Electing Validators Within a Configured Power Range

Once we have the capacity to ensure that the power level of validators falls within the acceptable range, we can apply this to all validators.

async function gatherEligibleValidators({
range,
state,
}: {
range: { min: number; max: number };
state: StateStore;
}) {
const result: AccountWithValidator[] = [];
for (const address of await state.getAccountTrie().allValidatorAddresses()) {
const account = await state.getAccountTrie().findByValidatorAddress(address);
// Permanently banned from block production
if (isString(account.validator.renunciationHash)) {
continue;
}
// Permanently banned from block production
if (account.validator.slashing?.tombstoned === true) {
continue;
}
// Temporarily banned from block production
if (account.validator.slashing?.jailed?.end !== undefined) {
continue;
}
if (inPowerRange({ account, range })) {
result.push(account);
}
}
return result;
}

Sorting Validators

This step is essential if we want to create any kind of determinism. The default mode of sorting is according to the power of validators in descending order (highest to lowest). While it’s possible to modify the sorting method, this is the most common approach.

function sortValidators(validators: AccountWithValidator[]) {
return validators.sort((a, b) => {
const result = b.validator.power.comparedTo(a.validator.power);
if (result !== 0) {
return result;
}
if (a.validator.publicKey === b.validator.publicKey) {
throw new Error(
`The balance and public key of both validators are identical. Validator [${a.name}] appears twice in the list.`,
);
}
return a.validator.publicKey.localeCompare(b.validator.publicKey, 'en');
});
}

Electing Validators Based On All of the Above

Now that we can elect validators and sort them according to their power, we need to create a small function that neatly abstracts the task of the election process. This includes the sorting process as well as limiting the number of validators the protocol returns.

async function electValidators({
accounts,
validatorCount,
}: {
accounts: AccountWithValidator[];
validatorCount: { min: number; max: number };
}) {
if (accounts.length < validatorCount.min) {
throw new Exception(
`Expected at least (${validatorCount.min}) validators but only got (${accounts.length})`,
);
}
if (validatorCount.max === 0) {
return sortValidators(accounts).slice(0, validatorCount.min);
}
accounts = sortValidators(accounts).slice(0, validatorCount.max);
if (accounts.length > validatorCount.max) {
throw new Exception(
`Expected at most (${validatorCount.max}) validators but got (${accounts.length})`,
);
}
return accounts;
}

Creating the Actual Elector

The actual elector combines all of the above functions and executes them via the elect function that Bearmint will call when needed. This function purely exists to compute and return a list of validator accounts that will participate in consensus.

function makeValidatorElector(cradle: Cradle): ValidatorElector {
return {
async elect(state: StateStore) {
const milestone = getModuleMilestone<BEP88Milestone>({
name: '@bearmint/bep88',
serviceProviderRepository: cradle.ServiceProviderRepository,
state,
});
return electValidators({
accounts: await gatherEligibleValidators({
range: milestone.range.power,
state,
}),
validatorCount: milestone.count.validators,
});
},
};
}

Combining the Factory Functions

In the second-to-last step, by combining all of the aforementioned code, the following function, which returns an array of validator accounts, is the result.

import {
Cradle,
KeyValueStore,
StateStore,
ValidatorElector,
ValidatorElectorConfiguration,
} from '@bearmint/bep13';
import { Exception } from '@bearmint/bep109';
import { getModuleMilestone } from '@bearmint/bep21';
function sortValidators(validators: AccountWithValidator[]) {
return validators.sort((a, b) => {
const result = b.validator.power.comparedTo(a.validator.power);
if (result !== 0) {
return result;
}
if (a.validator.publicKey === b.validator.publicKey) {
throw new Error(
`The balance and public key of both validators are identical. Validator [${a.name}] appears twice in the list.`,
);
}
return a.validator.publicKey.localeCompare(b.validator.publicKey, 'en');
});
}
function inPowerRange({
account,
range,
}: {
account: AccountWithValidator;
range: { min: number; max: number };
}) {
if (range.max === 0) {
return account.validator.power.isGreaterThanEqual(range.min);
}
return account.validator.power.isBetween(range.min, range.max);
}
async function gatherEligibleValidators({
range,
state,
}: {
range: { min: number; max: number };
state: StateStore;
}) {
const result: AccountWithValidator[] = [];
for (const address of await state.getAccountTrie().allValidatorAddresses()) {
const account = await state.getAccountTrie().findByValidatorAddress(address);
// Permanently banned from block production
if (isString(account.validator.renunciationHash)) {
continue;
}
// Permanently banned from block production
if (account.validator.slashing?.tombstoned === true) {
continue;
}
// Temporarily banned from block production
if (account.validator.slashing?.jailed?.end !== undefined) {
continue;
}
if (inPowerRange({ account, range })) {
result.push(account);
}
}
return result;
}
async function electValidators({
accounts,
validatorCount,
}: {
accounts: AccountWithValidator[];
validatorCount: { min: number; max: number };
}) {
if (accounts.length < validatorCount.min) {
throw new Exception(
`Expected at least (${validatorCount.min}) validators but only got (${accounts.length})`,
);
}
if (validatorCount.max === 0) {
return sortValidators(accounts).slice(0, validatorCount.min);
}
accounts = sortValidators(accounts).slice(0, validatorCount.max);
if (accounts.length > validatorCount.max) {
throw new Exception(
`Expected at most (${validatorCount.max}) validators but got (${accounts.length})`,
);
}
return accounts;
}
function makeValidatorElector(cradle: Cradle): ValidatorElector {
return {
async elect(state: StateStore) {
const milestone = getModuleMilestone<BEP88Milestone>({
name: '@bearmint/bep88',
serviceProviderRepository: cradle.ServiceProviderRepository,
state,
});
return electValidators({
accounts: await gatherEligibleValidators({
range: milestone.range.power,
state,
}),
validatorCount: milestone.count.validators,
});
},
};
}

Registering With the Application

To finish everything off, we simply bind our newly-created function to the container as ContainerType.ValidatorElector - Bearmint will now use our validator elector whenever it is requested.

import type { Cradle, ServiceProvider } from '@bearmint/bep13';
import { ContainerType } from '@bearmint/bep13';
import { makeServiceProviderSkeleton } from '@bearmint/bep16';
import { makeValidatorElector } from './elector';
function makeServiceProvider(cradle: Cradle): ServiceProvider {
return {
...makeServiceProviderSkeleton(import.meta.url ?? __dirname),
async register() {
cradle.Container.bind(ContainerType.ValidatorElector).toFunctionSingleton(
makeValidatorElector,
);
},
};
}

You Made It! Nicely Done, Pilgrim!

That was a little more work than some of our previous guides, but there you have it - all done and dusted! You now have a way of buildin’ and implementin’ a reliable validator elector module that you can use in yer own application! Ain’t that just wonderful?

If you have any questions relatin’ to this here guide or anythin’ else that you may require, be sure to drop us a line so we can help you out and provide a solution fer you. Buckley and all of us here at Bearmint are committed to our work, and part of that is helpin’ folks create solutions fer their own unique use cases. In other words, don’t hesitate to ask if you need a hand, because we’d absolutely love to work and build with you! Until next time amigos, take care!