Introduce Validators module

Hello everyone,

Today I would like to propose a LIP for the roadmap objective “Define state model and state root”. This LIP specifies the validators module.

Looking forward to your feedback!

Here is the LIP draft:

LIP: 0044
Title: Introduce Validators module
Author: Alessandro Ricottone <alessandro.ricottone@lightcurve.io>
        Andreas Kendziorra <andreas.kendziorra@lightcurve.io>
        Rishi Mittal <rishi.mittal@lightcurve.io>
Discussions-To: https://research.lisk.com/t/introduce-validators-module/317
Status: Draft
Type: Standards Track
Created: 2021-08-06
Updated: 2021-09-17
Requires: 0040

Abstract

The Validators module is responsible for validating the eligibility of a validator for generating a block and the block signature. Furthermore, it maintains information about the registered validators in its module store and provides the generator list. In this LIP, we specify the properties of the Validators module, along with their serialization and default values. Furthermore, we specify the state transitions logic defined within this module, i.e. the protocol logic injected during the block processing and the functions that can be called from other modules or off-chain services.

Copyright

This LIP is licensed under the Creative Commons Zero 1.0 Universal.

Motivation

Validators in Lisk DPoS and Lisk PoA chains share many common properties, like the generator and BLS keys. It is therefore desirable to handle these properties and their associated logic in a single module, the Validators module.

The Validators module handles parts of the block validation. In particular, it verifies that a validator is eligible for generating a block in a certain block slot and the validity of the block signature. Furthermore, it maintains the generator and BLS keys of all registered validators in its store and exposes functions to register new keys during a validator registration, to update the generator key, and to get the list of current validators (the generator list).

In this LIP we specify the properties, serialization, and initialization of the Validators module, as well as the protocol logic processed during a block processing and the functions exposed to other modules and to off-chain services.

Rationale

Generator Key

To be able to create block signatures, the secret key of the validator account needs to be accessible for a generator node. The most common approach is to store the encrypted passphrase that yields the secret key (or the encrypted secret key) on the node, where the encryption key is derived from a password-based key derivation function. This results in a small security risk: If an attacker is able to get the encrypted passphrase (or encrypted secret key) and the password for the encryption key derivation, the attacker has full control over the validator account. This may be a bigger concern for validators running a Lisk node on a remote data center.

To mitigate this risk, we propose to add an extra key pair to a validator account that is used for creating block signatures. Then, a generator node only requires access to the secret generator key, but not to the secret that is used for signing transactions. To increase security even further, validators should be able to update the generator key pair at any time. This is done by calling the setValidatorGeneratorKey function, described below.

Ensuring Uniqueness of validator BLS Keys

The Validators module store maintains an account for each validator registered in the chain. In particular, it stores the BLS key associated with the validator, used to sign commits. BLS keys have to be unique across the chain, i.e. two validators are not allowed to register the same BLS key. To easily check whether a BLS key has been previously registered, we use a registered BLS keys substore, to store all the registered validator BLS keys. Store keys are set to the BLS key and the corresponding store value to the address of the validator that registered the key. This allows to check for the existence of a certain BLS key in constant time.

Block Slots

In the Lisk protocol, time is divided into intervals of fixed length. These intervals are called block slots and at most one block can be added to the blockchain during each slot. The length of a block slot is a constant called block time. The validator module provides the functions to calculate the block slot given the timestamp and viceversa. Furthermore, it maintains the generator list, an array of addresses corresponding to the accounts that are eligible to generate a block in the current round. Each entry of the generator list is assigned to a block slot.

Specification

In this section, we specify the substores that are part of the Validators module store and the protocol logic called during the block processing. The Validators module has module ID MODULE_ID_VALIDATORS (see the table below).

Notation and Constants

We define the following constants:

Name Type Value Description
MODULE_ID_VALIDATORS uint32 TBD ID of the Validators module.
STORE_PREFIX_VALIDATORS_DATA bytes 0x0000 Store prefix of the validators data substore.
STORE_PREFIX_GENERATOR_LIST bytes 0x4000 Store prefix of the generator list substore.
STORE_PREFIX_BLS_KEYS bytes 0x8000 Store prefix of the registered BLS keys substore.
STORE_PREFIX_GENESIS_DATA bytes 0xc000 Store prefix of the genesis data substore.
INVALID_BLS_KEY bytes 48 bytes all set to 0x00 An invalid BLS key, used as a placeholder before a valid BLS key is registered.
BLOCK_TIME integer 10 (value for the Lisk mainchain) Block time (in seconds) set in the chain configuration.

Validators Module Store

The key-value pairs in the module store are organized in the following substores.

Validators Data Substore

Store Prefix, Store Key, and Store Value
  • The store prefix is set to STORE_PREFIX_VALIDATORS_DATA.
  • Store keys are set to 20 bytes addresses, representing a user address.
  • Store values are set to validator account data structures, holding the properties indicated below, serialized using the JSON schema validatorAccountSchema, presented below.
  • Notation: For the rest of this proposal let validatorAccount(address) be an entry in the validators data substore identified by the store key address.
JSON Schema
validatorAccountSchema = {
  "type": "object",
  "required": ["generatorKey", "blsKey"],
  "properties": {
    "generatorKey": {
      "dataType": "bytes",
      "fieldNumber": 1
    },
    "blsKey": {
      "dataType": "bytes",
      "fieldNumber": 2
    }
  }
}
Properties and Default values

The validator account holds the generator and BLS keys of a registered validator. In this section, we describe the properties of a validator account. These properties are set by the registerValidatorKeys function, called for instance during the processing of the delegate registration command or the authority registration command.

  • generatorKey: The public key whose corresponding private key is used to sign blocks generated by the validator. A valid generator key is 32 bytes long.
  • blsKey: The validator BLS key is the public BLS key whose corresponding private key is used to sign certificates. A valid BLS key is 48 bytes long.

Generator List Substore

Store Prefix, Store Key, and Store Value
  • The store prefix is set to STORE_PREFIX_GENERATOR_LIST.
  • The store key is set to empty bytes.
  • The store value is set to the generator list data structure, serialized using the JSON schema generatorListSchema, presented below.
  • Notation: For the rest of this proposal let generatorList be the entry in the generator list substore.
JSON Schema
generatorListSchema = {
  "type": "object",
  "required": ["addresses"],
  "properties": {
    "addresses": {
      "type": "array",
      "fieldNumber": 1,
      "items": {
        "dataType": "bytes"
      }
    }
  }
}
Properties and Default values

This substore stores an array of addresses, corresponding to the ordered list of eligible generators for the current round. An address is 20 bytes long. The array is initialized to the initial generator list specified in the genesis block.

Registered BLS Keys Substore

Store Prefix, Store Key, and Store Value
  • The store prefix is set to STORE_PREFIX_BLS_KEYS.
  • Store keys are set to 48 bytes BLS keys.
  • Store values are set to the addresses of the validators corresponding to the store keys, serialized using the validatorAddressSchema schema presented below.
JSON Schema
validatorAddressSchema = {
  "type": "object",
  "required": ["address"],
  "properties": {
    "address": {
      "dataType": "bytes",
      "fieldNumber": 1
    }
  }
}
Properties and Default values

The registered BLS keys substore maintains all registered validator BLS keys, using the BLS key as store key and the address of the validator that registered the BLS key as the corresponding store value. The registered BLS keys substore is initially empty, i.e. it does not contain any key-value pairs.

Genesis Data Substore

Store Prefix, Store Key, and Store Value
  • The store prefix is set to STORE_PREFIX_GENESIS_DATA.
  • The store key is set to empty bytes.
  • The store value is set to a data structure holding values from the genesis block of the chain, serialized using the genesisDataSchema schema presented below.
  • Notation: For the rest of this proposal let genesisData be the entry in the genesis data substore.
JSON Schema
genesisDataSchema = {
  "type": "object",
  "required": ["timestamp"],
  "properties": {
    "timestamp": {
      "dataType": "uint64",
      "fieldNumber": 1
    }
  }
}
Properties and Default values

The genesis data substore stores the timestamp of the genesis block.

Commands

The Validators module does not specify any commands.

Protocol Logic for Other Modules

registerValidatorKeys

This function creates a new validator account in the validators data substore. It is called as part of the delegate registration and authority registration commands that are part of the DPoS and PoA module.

Parameters

This function has the following input parameters in the order given below:

  • validatorAddress: A 20 bytes value identifying the validator account in the validators data substore.
  • proofOfPossession: The proof of possession for blsKey.
  • generatorKey: A 32 bytes value, specifying the generator key of the validator.
  • blsKey: A 48 bytes value, specifying the BLS key of the validator.
Returns
  • success: A boolean, indicating success or failure of the command execution.
Execution

This function checks that there is no account already registered for the input validatorAddress, that the input BLS key has not been registered before in the chain, and that the proof of possession for the BLS key is valid. Finally, it creates a new validator account in the validators data substore.

registerValidatorKeys(validatorAddress, proofOfPossession, generatorKey, blsKey):
    if there exists an entry in the validators data substore with storeKey == validatorAddress:
        return False
    if there exists an entry in the registered BLS keys substore with storeKey == blsKey:
        return False
    if PopVerify(blsKey, proofOfPossession) != VALID:
        return False

    validatorAccount = {
        generatorKey: generatorKey,
        blsKey: blsKey,
    }
    insert validatorAccount in the validators data substore with storeKey = validatorAddress
    insert validatorAddress in the registered BLS keys substore with storeKey = blsKey

    return True

The function PopVerify is part of the BLS signature scheme.

getValidatorAccount

This function is used to retrieve information about a validator account.

Parameters
  • address: A 20 bytes value identifying the validator account.
Returns

This function returns the validator account corresponding to the input address.

Execution

This function returns validatorAccount(address). If there is no entry corresponding to address, it returns an empty object.

getGenesisData

This function is used to retrieve information about the genesis data.

Parameters

This function has no input parameter.

Returns

This function returns the data structure stored in the genesis data substore.

setValidatorBLSKey

This function sets the BLS key of a validator account. This update is only possible if the current BLS key of the validator is equal to a predefined constant invalid BLS key.

Parameters

This function has the following input parameters in the order given below:

  • validatorAddress: A 20 bytes value identifying the validator account in the validators data substore.
  • proofOfPossession: The proof of possession for blsKey.
  • blsKey: A 48 bytes value, specifying the new BLS key of the validator.
Returns
  • success: A boolean, indicating success or failure of the command execution.
Execution

This function checks that there exists a validator account registered for the input validatorAddress, that the input BLS key has not been registered before in the chain, that the BLS key of the validator account is equal to a certain constant invalid BLS key, and that the proof of possession for the BLS key is valid. Finally, it updates the BLS key of the validator account.

setValidatorBLSKey(validatorAddress, proofOfPossession, blsKey):
    if no entry in the validators data substore exist with storeKey == validatorAddress:
        return False
    if there exists an entry in the registered BLS keys substore with storeKey == blsKey:
        return False
    if validatorAccount(validatorAddress).blsKey != INVALID_BLS_KEY:
        return False
    if PopVerify(blsKey, proofOfPossession) != VALID:
        return False

    validatorAccount(validatorAddress).blsKey = blsKey
    insert validatorAddress in the registered BLS keys substore with storeKey = blsKey
    return True

setValidatorGeneratorKey

This function sets the generator key of a validator account.

Parameters

This function has the following input parameters in the order given below:

  • validatorAddress: A 20 bytes value identifying the validator account in the validators data substore.
  • generatorKey: A 32 bytes value, specifying the new generator key of the validator.
Returns
  • success: A boolean, indicating success or failure of the command execution.
Execution

This function checks that there exists a validator account registered for the input validatorAddress and then updates the generator key.

setValidatorGeneratorKey(validatorAddress, generatorKey):
    if no entry in the validators data substore exist with storeKey == validatorAddress:
        return False

    validatorAccount(validatorAddress).generatorKey = generatorKey
    return True

isKeyRegistered

This function checks whether a certain BLS key has been already registered in the chain.

Parameters

blsKey: A 48 bytes value corresponding to a BLS key.

Returns

This function returns a boolean, indicating whether the input BLS key blsKey has been already registered or not.

Execution

This function checks if an entry with store key equal to blsKey exists in the registered BLS keys substore. If this is the case, it returns true, else it returns false.

getGeneratorList

This function returns the current generator list.

Parameters

This function has no input parameter.

Returns

This function returns the current generator list.

Execution

This function returns generatorList.

getSlotNumber

This function returns the slot number corresponding to the input timestamp.

Parameters

timestamp: An integer value corresponding to a timestamp.

Returns

This function returns an integer, indicating the generator slot number.

Execution
getSlotNumber(timestamp):
    elapsedTime = timestamp - genesisData.timestamp
    return floor(elapsedTime / BLOCK_TIME)

getSlotTime

This function returns the slot time corresponding to the input slot number.

Parameters

slotNumber: An integer value corresponding to a slot number.

Returns

This function returns an integer, indicating the slot time.

Execution
getSlotTime(slotNumber):
    slotGenesisTimeOffset = slotNumber * BLOCK_TIME
    return genesisData.timestamp + slotGenesisTimeOffset

getGeneratorAtTimestamp

This function returns the address of the generator active at the input timestamp. Notice that if the input timestamp corresponds to a time before the beginning of the current round, this function may not return the correct generator address.

Parameters

timestamp: An integer value corresponding to a timestamp.

Returns

This function returns a 20 bytes address.

Execution
getGeneratorAtTimestamp(timestamp):
    elapsedTime = timestamp - genesisData.timestamp
    slotIndex = floor(elapsedTime / BLOCK_TIME) % generatorList.addresses.length
    return generatorList.addresses[slotIndex]

getGeneratorsBetweenTimestamps

This function returns the address of the generators active between the two input timestamps and the number of block slots assigned to them. Notice that if the input timestamps corresponds to times before the beginning of the current round, this function may not return the correct generator addresses.

Parameters

This function has the following input parameters in the order given below:

  • startTimestamp: An integer value corresponding to a timestamp.
  • endTimestamp: An integer value corresponding to a timestamp.
Returns

This function returns an object whose keys are generator addresses, with value corresponding to the number of block slots assigned to them in between the input timestamps.

Execution
getGeneratorsBetweenTimestamps(startTimestamp, endTimestamp):
    if endTimestamp < startTimestamp:
        return invalid timestamps error

    result = {}

    startSlotNumber = floor((startTimestamp - genesisData.timestamp) / BLOCK_TIME)
    endSlotNumber = floor((endTimestamp - genesisData.timestamp) / BLOCK_TIME)
    totalSlots = endSlotNumber - startSlotNumber + 1

    # Quick skip to directly assign many block slots to every generator in the list
    baseSlots = floor(totalSlots / generatorList.addresses.length)
    if baseSlots > 0:
        totalSlots -= baseSlots * generatorList.addresses.length
        for generatorAddress in generatorList.addresses:
            result[generatorAddress] = baseSlots

    # Assign remaining block slots
    for slotNumber in range(startSlotNumber, startSlotNumber + totalSlots):
        slotIndex = slotNumber % generatorList.addresses.length
        generatorAddress = generatorList.addresses[slotIndex]
        if generatorAddress in result:
            result[generatorAddress] += 1
        else:
            result[generatorAddress] = 1

    return result

setGeneratorList

This function updates the value of the generator list.

Parameters
  • generatorAddresses: An array of 20 bytes values.
Returns

This function returns a boolean indicating the successful update of the generator list.

Execution

This function checks if all elements of generatorAddresses have a registered validator account in the validators data substore. If this is not the case, it returns false. Otherwise, it updates the value of generatorList.addresses to generatorAddresses and returns true.

Endpoints for Off-Chain Services

getGeneratorList

This function returns the current generator list.

Parameters

This function has no input parameter.

Returns

This function returns the current generator list.

Execution

This function returns generatorList.

validateBLSKey

This function checks that the input BLS key is valid, i.e. can be registered in a validator account.

Parameters

This function has the following input parameters in the order given below:

  • proofOfPossession: The proof of possession for blsKey.
  • blsKey: A 48 bytes value, specifying the BLS key to be checked.
Returns
  • success: A boolean, indicating the validity of the input BLS key.
Execution

This function checks that the input BLS key has not been registered before in the chain and that the proof of possession for the BLS key is valid.

validateBLSKey(proofOfPossession, blsKey):
    if there exists an entry in the registered BLS keys substore with storeKey == blsKey:
        return False

    if PopVerify(blsKey, proofOfPossession) != VALID:
        return False

    return True

Block Processing

After Genesis Block Execution

After the genesis block b is executed, the following logic is executed:

  • Create the entry in the genesis data substore. In particular, set genesisData.timestamp to b.header.timestamp.
  • Check the validity of the validators store included in the genesis block. In particular, for each entry validatorAccount in the validators data substore:
    • Check that validatorAccount.generatorKey is a 32 bytes value.
    • Check that validatorAccount.blsKey is a 48 bytes value.
    • Let proofOfPossession be the proof of possession for validatorAccount.blsKey provided in the genesis block. Check that PopVerify(validatorAccount.blsKey, proofOfPossession) returns VALID.
    • Let validatorAddress be the store key of validatorAccount. Check that there is an entry in the registered keys substore with store key equals to validatorAccount.blsKey and store value equals to the serialization of validatorAddress.
  • Check the validity of the registered BLS keys substore included in the genesis block. In particular, for each entry blsKey in the registered BLS keys substore:
    • Let validatorAddress be the store value of blsKey. Check that there is an entry validatorAccount in the validators data substore with store key equals to validatorAddress and validatorAccount.blsKey equals to blsKey.

Backwards Compatibility

This LIP defines a new module and specify its store, which in turn will become part of the state tree and will be authenticated by the state root. As such, it will induce a hardfork.

2 Likes

I created a pull request for this proposal in the LIPs repository.

The PR has been merged