diff --git a/packages/cli/src/commands/account/isvalidator.ts b/packages/cli/src/commands/account/isvalidator.ts deleted file mode 100644 index 316514dfa8e..00000000000 --- a/packages/cli/src/commands/account/isvalidator.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { eqAddress } from '@celo/utils/lib/address' -import { BaseCommand } from '../../base' -import { Args } from '../../utils/command' - -export default class IsValidator extends BaseCommand { - static description = - 'Check whether a given address is elected to be validating in the current epoch' - - static flags = { - ...BaseCommand.flags, - } - - static args = [Args.address('address')] - - static examples = ['isvalidator 0x5409ed021d9299bf6814279a6a1411a7e866a631'] - - async run() { - const { args } = this.parse(IsValidator) - - const election = await this.kit.contracts.getElection() - const numberValidators = await election.numberValidatorsInCurrentSet() - - for (let i = 0; i < numberValidators; i++) { - const validatorAddress = await election.validatorAddressFromCurrentSet(i) - if (eqAddress(validatorAddress, args.address)) { - console.log(`${args.address} is in the current validator set`) - return - } - } - - console.log(`${args.address} is not currently in the validator set`) - } -} diff --git a/packages/cli/src/commands/election/current.ts b/packages/cli/src/commands/election/current.ts index 765a632409c..b56c8e209ac 100644 --- a/packages/cli/src/commands/election/current.ts +++ b/packages/cli/src/commands/election/current.ts @@ -4,14 +4,12 @@ import { validatorTable } from '../validator/list' export default class ElectionCurrent extends BaseCommand { static description = - 'Outputs the set of validators currently participating in BFT to create blocks. The validator set is re-elected at the end of every epoch.' + 'Outputs the set of validators currently participating in BFT to create blocks. An election is run to select the validator set at the end of every epoch.' static flags = { ...BaseCommand.flags, } - static examples = ['current'] - async run() { cli.action.start('Fetching currently elected Validators') const election = await this.kit.contracts.getElection() diff --git a/packages/cli/src/commands/election/run.ts b/packages/cli/src/commands/election/run.ts index 209f4ccac0b..34a9fba3b6f 100644 --- a/packages/cli/src/commands/election/run.ts +++ b/packages/cli/src/commands/election/run.ts @@ -10,13 +10,11 @@ export default class ElectionRun extends BaseCommand { ...BaseCommand.flags, } - static examples = ['run'] - async run() { cli.action.start('Running mock election') const election = await this.kit.contracts.getElection() const validators = await this.kit.contracts.getValidators() - const signers = await election.getCurrentValidatorSigners() + const signers = await election.electValidatorSigners() const validatorList = await Promise.all( signers.map((addr) => validators.getValidatorFromSigner(addr)) ) diff --git a/packages/cli/src/commands/validator/status.ts b/packages/cli/src/commands/validator/status.ts new file mode 100644 index 00000000000..bcf81dc417b --- /dev/null +++ b/packages/cli/src/commands/validator/status.ts @@ -0,0 +1,111 @@ +import { Address } from '@celo/contractkit' +import { eqAddress } from '@celo/utils/lib/address' +import { concurrentMap } from '@celo/utils/lib/async' +import { bitIsSet, parseBlockExtraData } from '@celo/utils/lib/istanbul' +import { flags } from '@oclif/command' +import { cli } from 'cli-ux' +import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' +import { Flags } from '../../utils/command' + +export default class ValidatorStatus extends BaseCommand { + static description = + 'Show information about whether the validator signer is elected and validating. This command will check that the validator meets the registration requirements, and its signer is currently elected and actively signing blocks.' + + static flags = { + ...BaseCommand.flags, + signer: Flags.address({ + description: 'address of the signer to check if elected and validating', + exclusive: ['validator'], + }), + validator: Flags.address({ + description: 'address of the validator to check if elected and validating', + exclusive: ['signer'], + }), + lookback: flags.integer({ + description: 'how many blocks to look back for signer activity', + default: 100, + }), + } + + static examples = [ + 'status --validator 0x5409ED021D9299bf6814279A6A1411A7e866A631', + 'status --signer 0x738337030fAeb1E805253228881d844b5332fB4c', + 'status --signer 0x738337030fAeb1E805253228881d844b5332fB4c --lookback 100', + ] + + requireSynced = true + + async run() { + const res = this.parse(ValidatorStatus) + + // Check that the specified validator or signer meets the validator requirements. + const checker = newCheckBuilder(this, res.flags.signer) + if (res.flags.validator) { + const account = res.flags.validator + checker + .isAccount(account) + .isValidator(account) + .meetsValidatorBalanceRequirements(account) + } else if (res.flags.signer) { + checker + .isSignerOrAccount() + .signerMeetsValidatorBalanceRequirements() + .signerAccountIsValidator() + } else { + this.error('Either validator or signer must be specified') + } + await checker.runChecks() + + // Get the signer from the validator account if not provided. + let signer: Address = res.flags.signer || '' + if (!signer) { + const accounts = await this.kit.contracts.getAccounts() + signer = await accounts.getValidatorSigner(res.flags.validator!) + console.info(`Identified ${signer} as the authorized validator signer`) + } + + // Determine if the signer is elected, and get their index in the validator set. + const election = await this.kit.contracts.getElection() + const signers = await election.getCurrentValidatorSigners() + const signerIndex = signers.map((a) => eqAddress(a, signer)).indexOf(true) + if (signerIndex < 0) { + // Determine whether the signer will be elected at the next epoch to provide a helpful error. + const frontrunners = await election.electValidatorSigners() + if (frontrunners.some((a) => eqAddress(a, signer))) { + this.error( + `Signer ${signer} is not elected for this epoch, but would be elected if an election were to be held now. Please wait until the next epoch.` + ) + } else { + this.error( + `Signer ${signer} is not elected for this epoch, and would not be elected if an election were to be held now.` + ) + } + } + console.info('Signer has been elected for this epoch') + + if (((res.flags && res.flags.lookback) || 0) <= 0) { + return + } + + // Retrieve blocks to examine for the singers signature. + cli.action.start(`Retreiving the last ${res.flags.lookback} blocks`) + const latest = await this.web3.eth.getBlock('latest') + const blocks = await concurrentMap(10, [...Array(res.flags.lookback).keys()].slice(1), (i) => + this.web3.eth.getBlock(latest.number - i) + ) + blocks.splice(0, 0, latest) + cli.action.stop() + + const signedCount = blocks.filter((b) => + bitIsSet(parseBlockExtraData(b.extraData).parentAggregatedSeal.bitmap, signerIndex) + ).length + if (signedCount === 0) { + this.error(`Signer has not signed any of the last ${res.flags.lookback} blocks`) + } + console.info(`Signer has signed ${signedCount} of the last ${res.flags.lookback} blocks`) + + const proposedCount = blocks.filter((b) => b.miner === signer).length + console.info(`Signer has proposed ${proposedCount} of the last ${res.flags.lookback} blocks`) + } +} diff --git a/packages/cli/src/utils/checks.ts b/packages/cli/src/utils/checks.ts index 180d50a4b8f..38f21cb435f 100644 --- a/packages/cli/src/utils/checks.ts +++ b/packages/cli/src/utils/checks.ts @@ -70,7 +70,7 @@ class CheckBuilder { } } - withAccounts(f: (lockedGold: AccountsWrapper) => A): () => Promise> { + withAccounts(f: (accounts: AccountsWrapper) => A): () => Promise> { return async () => { const accounts = await this.kit.contracts.getAccounts() return f(accounts) as Resolve @@ -122,12 +122,24 @@ class CheckBuilder { signerMeetsValidatorGroupBalanceRequirements = () => this.addCheck( - `Signer's account has enough locked gold for registration`, + `Signer's account has enough locked gold for group registration`, this.withValidators((v, _signer, account) => v.meetsValidatorGroupBalanceRequirements(account) ) ) + meetsValidatorBalanceRequirements = (account: Address) => + this.addCheck( + `${account} has enough locked gold for registration`, + this.withValidators((v) => v.meetsValidatorBalanceRequirements(account)) + ) + + meetsValidatorGroupBalanceRequirements = (account: Address) => + this.addCheck( + `${account} has enough locked gold for group registration`, + this.withValidators((v) => v.meetsValidatorGroupBalanceRequirements(account)) + ) + isNotAccount = (address: Address) => this.addCheck( `${address} is not an Account`, diff --git a/packages/docs/command-line-interface/account.md b/packages/docs/command-line-interface/account.md index d110155a6d0..38a86485e41 100644 --- a/packages/docs/command-line-interface/account.md +++ b/packages/docs/command-line-interface/account.md @@ -191,20 +191,6 @@ EXAMPLE _See code: [packages/cli/src/commands/account/get-metadata.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/get-metadata.ts)_ -### Isvalidator - -Check whether a given address is elected to be validating in the current epoch - -``` -USAGE - $ celocli account:isvalidator ADDRESS - -EXAMPLE - isvalidator 0x5409ed021d9299bf6814279a6a1411a7e866a631 -``` - -_See code: [packages/cli/src/commands/account/isvalidator.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/isvalidator.ts)_ - ### New Creates a new account locally and print out the key information. Save this information for local transaction signing or import into a Celo node. diff --git a/packages/docs/command-line-interface/election.md b/packages/docs/command-line-interface/election.md index 0c2cfc870f0..6c295bcbfcb 100644 --- a/packages/docs/command-line-interface/election.md +++ b/packages/docs/command-line-interface/election.md @@ -25,14 +25,11 @@ _See code: [packages/cli/src/commands/election/activate.ts](https://github.com/c ### Current -Outputs the set of validators currently participating in BFT to create blocks. The validator set is re-elected at the end of every epoch. +Outputs the set of validators currently participating in BFT to create blocks. An election is run to select the validator set at the end of every epoch. ``` USAGE $ celocli election:current - -EXAMPLE - current ``` _See code: [packages/cli/src/commands/election/current.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/election/current.ts)_ @@ -78,9 +75,6 @@ Runs a "mock" election and prints out the validators that would be elected if th ``` USAGE $ celocli election:run - -EXAMPLE - run ``` _See code: [packages/cli/src/commands/election/run.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/election/run.ts)_ diff --git a/packages/docs/command-line-interface/validator.md b/packages/docs/command-line-interface/validator.md index ab080c9d0f6..f1d45a1e68c 100644 --- a/packages/docs/command-line-interface/validator.md +++ b/packages/docs/command-line-interface/validator.md @@ -131,6 +131,30 @@ EXAMPLE _See code: [packages/cli/src/commands/validator/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/show.ts)_ +### Status + +Show information about whether the validator signer is elected and validating. This command will check that the validator meets the registration requirements, and its signer is currently elected and actively signing blocks. + +``` +USAGE + $ celocli validator:status + +OPTIONS + --lookback=lookback [default: 100] how many blocks to look back for signer + activity + + --signer=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d address of the signer to check if elected and validating + + --validator=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d address of the validator to check if elected and validating + +EXAMPLES + status --validator 0x5409ED021D9299bf6814279A6A1411A7e866A631 + status --signer 0x738337030fAeb1E805253228881d844b5332fB4c + status --signer 0x738337030fAeb1E805253228881d844b5332fB4c --lookback 100 +``` + +_See code: [packages/cli/src/commands/validator/status.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/status.ts)_ + ### Update-bls-public-key Update the BLS public key for a Validator to be used in consensus. Regular (ECDSA and BLS) key rotation is recommended for Validator operational security. diff --git a/packages/docs/getting-started/running-a-validator.md b/packages/docs/getting-started/running-a-validator.md index 76bdae5b427..5bce83f832d 100644 --- a/packages/docs/getting-started/running-a-validator.md +++ b/packages/docs/getting-started/running-a-validator.md @@ -445,11 +445,11 @@ celocli election:list If you find your Validator still not getting elected you may need to faucet yourself more funds and lock more gold in order to be able to cast more votes for your Validator Group! -At any moment you can check the currently elected validators by running the following command: +You can check the status of your validator, including whether it is elected and signing blocks, by running: ```bash # On your local machine -celocli election:current +celocli validator:status --validator $CELO_VALIDATOR_ADDRESS ``` ### Running the Attestation Service diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 6809c109f4d..657c7c661cd 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -7,3 +7,4 @@ export * from './dappkit' export { ECIES } from './ecies' export { PhoneNumberUtils } from './phoneNumbers' export { SignatureUtils } from './signatureUtils' +export { IstanbulUtils } from './istanbul' diff --git a/packages/utils/src/istanbul.test.ts b/packages/utils/src/istanbul.test.ts new file mode 100644 index 00000000000..9b8a89cd1f7 --- /dev/null +++ b/packages/utils/src/istanbul.test.ts @@ -0,0 +1,64 @@ +import BigNumber from 'bignumber.js' +import { bitIsSet, Bitmap, IstanbulExtra, parseBlockExtraData } from './istanbul' + +describe('Istanbul utilities', () => { + describe('parseBlockExtraData', () => { + const testExtraData = + '0xd983010817846765746888676f312e31312e358664617277696e000000000000f90127d594fd0893e334' + + 'c6401188ae77072546979b94d91813f862b8604fa3f67fc913878b068d1fa1cdddc54913d3bf988dbe5a36' + + 'a20fa888f20d4894c408a6773f3d7bde11154f2a3076b700d345a42fd25a0e5e83f4db5586ac7979ac2053' + + 'cd95d8f2efd3e959571ceccaa743e02cf4be3f5d7aaddb0b06fc9aff0001b84188022a71c12a801a4318e2' + + '7eeb5c82aa923160632c63b0eae4457ed120356ddb549fb7c4e4865728478aa61c19b9abe10ec7db34c866' + + '2b003b139188e99edcd400f30db040c083f6b6e29a6a2cab4498f50d37d458a2458b5438c9faeae8598cd4' + + '7f4ed6e17ca10e1f87c6faa14d5e3e393f0e0080f30db06107252c187052f8212ef5cfc9052fe59c7af040' + + 'e77a09b762fd51060220511e93d1c681be8883043f8a93ea637492818080' + + const expected: IstanbulExtra = { + addedValidators: ['0xFd0893E334C6401188Ae77072546979B94d91813'], + addedValidatorsPublicKeys: [ + '0x4fa3f67fc913878b068d1fa1cdddc54913d3bf988dbe5a36a20fa888f20d4894c408a6773f3d7bde11154f2a3076b700d345a42fd25a0e5e83f4db5586ac7979ac2053cd95d8f2efd3e959571ceccaa743e02cf4be3f5d7aaddb0b06fc9aff00', + ], + removedValidators: new BigNumber(1), + seal: + '0x88022a71c12a801a4318e27eeb5c82aa923160632c63b0eae4457ed120356ddb549fb7c4e4865728478aa61c19b9abe10ec7db34c8662b003b139188e99edcd400', + aggregatedSeal: { + bitmap: new BigNumber(13), + signature: + '0x40c083f6b6e29a6a2cab4498f50d37d458a2458b5438c9faeae8598cd47f4ed6e17ca10e1f87c6faa14d5e3e393f0e00', + round: new BigNumber(0), + }, + parentAggregatedSeal: { + bitmap: new BigNumber(13), + signature: + '0x6107252c187052f8212ef5cfc9052fe59c7af040e77a09b762fd51060220511e93d1c681be8883043f8a93ea63749281', + round: new BigNumber(0), + }, + } + + it('should decode the Istanbul extra data correctly', () => { + expect(parseBlockExtraData(testExtraData)).toEqual(expected) + }) + }) + + describe('bitIsSet', () => { + const testBitmap: Bitmap = new BigNumber('0x40d1', 16) + const testBitmapAsBinary = ('0100' + '0000' + '1101' + '0001') + .split('') + .map((b) => b === '1') + .reverse() + + it('should correctly identify set bits within expected index', () => { + for (let i = 0; i < testBitmapAsBinary.length; i++) { + expect(bitIsSet(testBitmap, i)).toBe(testBitmapAsBinary[i]) + } + }) + + it('should return false when the index is too large', () => { + expect(bitIsSet(testBitmap, 1000)).toBe(false) + }) + + it('should throw an error when the index is negative', () => { + expect(() => bitIsSet(testBitmap, -1)).toThrow() + }) + }) +}) diff --git a/packages/utils/src/istanbul.ts b/packages/utils/src/istanbul.ts new file mode 100644 index 00000000000..36f2be2ae6d --- /dev/null +++ b/packages/utils/src/istanbul.ts @@ -0,0 +1,69 @@ +import BigNumber from 'bignumber.js' +import { toChecksumAddress } from 'ethereumjs-util' +import * as rlp from 'rlp' +import { Address } from './address' + +// This file contains utilities that help with istanbul-specific block information. +// See https://github.com/celo-org/celo-blockchain/blob/master/core/types/istanbul.go + +const ISTANBUL_EXTRA_VANITY_BYTES = 32 + +export type Bitmap = BigNumber + +// Aggregated BLS signatures for a block. +export interface Seal { + bitmap: Bitmap + signature: string + round: BigNumber +} + +// Extra data in the block header to support Istanbul BFT. +export interface IstanbulExtra { + addedValidators: Address[] + addedValidatorsPublicKeys: string[] + removedValidators: Bitmap + seal: string + aggregatedSeal: Seal + parentAggregatedSeal: Seal +} + +function bigNumberFromBuffer(data: Buffer): BigNumber { + return new BigNumber('0x' + (data.toString('hex') || '0'), 16) +} + +function sealFromBuffers(data: Buffer[]): Seal { + return { + bitmap: bigNumberFromBuffer(data[0]), + signature: '0x' + data[1].toString('hex'), + round: bigNumberFromBuffer(data[2]), + } +} + +// Parse RLP encoded block extra data into an IstanbulExtra object. +export function parseBlockExtraData(data: string): IstanbulExtra { + const buffer = Buffer.from(data.replace(/^0x/, ''), 'hex') + const decode: any = rlp.decode('0x' + buffer.slice(ISTANBUL_EXTRA_VANITY_BYTES).toString('hex')) + return { + addedValidators: decode[0].map((addr: Buffer) => toChecksumAddress(addr.toString('hex'))), + addedValidatorsPublicKeys: decode[1].map((key: Buffer) => '0x' + key.toString('hex')), + removedValidators: bigNumberFromBuffer(decode[2]), + seal: '0x' + decode[3].toString('hex'), + aggregatedSeal: sealFromBuffers(decode[4]), + parentAggregatedSeal: sealFromBuffers(decode[5]), + } +} + +export function bitIsSet(bitmap: Bitmap, index: number): boolean { + if (index < 0) { + throw new Error(`bit index must be greater than zero: got ${index}`) + } + return bitmap + .idiv('1' + '0'.repeat(index), 2) + .mod(2) + .gt(0) +} + +export const IstanbulUtils = { + parseBlockExtraData, + bitIsSet, +}