LIP: 0044
Title: Introduce Validators module
Author: Alessandro Ricottone <[email protected]>
Andreas Kendziorra <[email protected]>
Rishi Mittal <[email protected]>
Discussions-To: https://research.lisk.com/t/introduce-validators-module/317
Status: Draft
Type: Standards Track
Created: 2021-08-06
Updated: 2023-02-02
Requires: 0040
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.
This LIP is licensed under the Creative Commons Zero 1.0 Universal.
Validators in Lisk PoS 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.
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, the Validators module allows to update the generator key pair at any time. This is done by calling the setValidatorGeneratorKey
function, described below. Note, however, that the current version of the PoS module does not support updating the generator key as otherwise validators could avoid report of misbehavior.
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.
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 name MODULE_NAME_VALIDATORS
(see the table below).
We define the following constants:
Name | Type | Value | Description |
---|---|---|---|
MODULE_NAME_VALIDATORS |
string | "validators" | Name of the Validators module. |
SUBSTORE_PREFIX_VALIDATORS_KEYS |
bytes | 0x0000 | Substore prefix of the validators keys substore. |
SUBSTORE_PREFIX_VALIDATOR_PARAMS |
bytes | 0x4000 | Substore prefix of the validator params substore. |
SUBSTORE_PREFIX_BLS_KEYS |
bytes | 0x8000 | Substore prefix of the registered BLS keys substore. |
INVALID_BLS_KEY |
bytes | BLS_PUBLIC_KEY_LENGTH 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. |
EVENT_NAME_GENERATOR_KEY_REGISTRATION |
string | "generatorKeyRegistration" | Name of the generator key registration event. |
EVENT_NAME_BLS_KEY_REGISTRATION |
string | "blsKeyRegistration" | Name of the BLS key registration event. |
KEY_REG_RESULT_SUCCESS |
uint32 | 0 | Success result code of key registration events. |
KEY_REG_RESULT_NO_VALIDATOR |
uint32 | 1 | Failure result code of key registration events: address not registered as validator. |
KEY_REG_RESULT_ALREADY_VALIDATOR |
uint32 | 2 | Failure result code of key registration events: address already registered as validator. |
KEY_REG_RESULT_DUPLICATE_BLS_KEY |
uint32 | 3 | Failure result code of key registration events: BLS key already registered in the chain. |
KEY_REG_RESULT_INVALID_POP |
uint32 | 4 | Failure result code of key registration events: invalid proof of possession. |
ADDRESS_LENGTH |
uint32 | 20 | Length in bytes of type Address . |
ED25519_PUBLIC_KEY_LENGTH |
uint32 | 32 | Length in bytes of type PublicKeyEd25519 . |
BLS_PUBLIC_KEY_LENGTH |
uint32 | 48 | Length in bytes of type PublicKeyBLS . |
BLS_POP_LENGTH |
uint32 | 96 | Length in bytes of type ProofOfPossession . |
Furthermore, we use the symbol //
for integer division.
Name | Type | Validation | Description |
---|---|---|---|
Address |
bytes | Must be of length ADDRESS_LENGTH . |
Address of an account. |
PublicKeyEd25519 |
bytes | Must be of length ED25519_PUBLIC_KEY_LENGTH . |
Used for Ed25519 public keys. |
PublicKeyBLS |
bytes | Must be of length BLS_PUBLIC_KEY_LENGTH . |
Used for BLS keys. |
ProofOfPossession |
bytes | Must be of length BLS_POP_LENGTH . |
The proof of possession associated with a BLS key. |
ValidatorKeys |
object | Must follow the validatorKeysSchema schema. |
An object containing the BLS key and generator key associated to a validator. |
Validator |
object | Must follow the validatorSchema schema. |
An object containing the address, keys and BFT weight associated to a validator. |
ValidatorParams |
object | Must follow the validatorParamsSchema schema. |
An object representing the validator parameters. |
The key-value pairs in the module store are organized in the following substores.
- The substore prefix is set to
SUBSTORE_PREFIX_VALIDATORS_KEYS
. - Store keys are set to
ADDRESS_LENGTH
bytes addresses, representing a user address. - Store values are set to validator keys data structures, holding the properties indicated below, serialized using the JSON schema
validatorKeysSchema
, presented below. - Notation: For the rest of this proposal let
validatorKeys(address)
be an entry in the validators keys substore identified by the store keyaddress
, deserialized usingvalidatorKeysSchema
schema.
validatorKeysSchema = {
"type": "object",
"required": ["generatorKey", "blsKey"],
"properties": {
"generatorKey": {
"dataType": "bytes",
"length": ED25519_PUBLIC_KEY_LENGTH,
"fieldNumber": 1
},
"blsKey": {
"dataType": "bytes",
"length": BLS_PUBLIC_KEY_LENGTH,
"fieldNumber": 2
}
}
}
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 validator registration command or the authority registration command.
generatorKey
: The public key whose corresponding private key is used to sign blocks generated by the validator.blsKey
: The validator BLS key is the public BLS key whose corresponding private key is used to sign certificates.
- The substore prefix is set to
SUBSTORE_PREFIX_VALIDATOR_PARAMS
. - The store key is set to empty bytes.
- The store value is the serialization of an object following the JSON schema
validatorParamsSchema
defined below. - Notation: For the rest of this proposal let
validatorParamsStore
be the entry in the validator params substore, deserialized usingvalidatorParamsSchema
schema.
validatorParamsSchema = {
"type": "object",
"required": [
"precommitThreshold",
"certificateThreshold",
"validators"
],
"properties": {
"precommitThreshold": {
"dataType": "uint64",
"fieldNumber": 1
},
"certificateThreshold": {
"dataType": "uint64",
"fieldNumber": 2
},
"validators": {
"type": "array",
"fieldNumber": 3,
"items": {
...validatorSchema
}
}
}
}
validatorSchema = {
"type": "object",
"required": ["address", "bftWeight", "generatorKey", "blsKey"],
"properties": {
"address": {
"dataType": "bytes",
"length": ADDRESS_LENGTH,
"fieldNumber": 1
},
"bftWeight": {
"dataType": "uint64",
"fieldNumber": 2
},
"generatorKey": {
"dataType": "bytes",
"length": ED25519_PUBLIC_KEY_LENGTH,
"fieldNumber": 3
},
"blsKey": {
"dataType": "bytes",
"length": BLS_PUBLIC_KEY_LENGTH,
"fieldNumber": 4
}
}
}
This substore stores the current validator parameters as set using the setValidatorParams function:
precommitThreshold
: This property stores the current precommit threshold, see LIP 0056 for details.certificateThreshold
: This property stores the current certificate threshold, see LIP 0061 for details.validators
: This property stores an array of objects, each corresponding to a validator with the following properties:address
: The address of the validator.bftWeight
: The BFT weight of the validator, see LIP 0056 for details.generatorKey
: The Ed25519 public key of the validator whose corresponding private key is used to sign blocks generated by the validator.blsKey
: The BLS public key of the validator whose corresponding private key is used to sign certificates.
- The substore prefix is set to
SUBSTORE_PREFIX_BLS_KEYS
. - Store keys are of type
PublicKeyBLS
. - Store values are set to the addresses of the validators corresponding to the store keys, serialized using the
validatorAddressSchema
schema presented below. - Notation: For the rest of this proposal let
registeredBLSKeys(blsKey)
be the entry in the registered BLS keys substore identified by the store keyblsKey
, deserialized usingvalidatorAddressSchema
schema.
validatorAddressSchema = {
"type": "object",
"required": ["address"],
"properties": {
"address": {
"dataType": "bytes",
"length": ADDRESS_LENGTH,
"fieldNumber": 1
}
}
}
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.
This event has name = EVENT_NAME_GENERATOR_KEY_REGISTRATION
. This event is emitted when a generator key is registered. The event data contains the generator key generatorKey
and the result of the registration result
.
address
: The address for which the generator key has been registered.
generatorKeyRegDataSchema = {
"type": "object",
"required": ["generatorKey", "result"],
"properties": {
"generatorKey": {
"dataType": "bytes",
"length": ED25519_PUBLIC_KEY_LENGTH,
"fieldNumber": 1
},
"result": {
"dataType": "uint32",
"fieldNumber": 2
}
}
}
This event has name = EVENT_NAME_BLS_KEY_REGISTRATION
. This event is emitted when a BLS key is registered. The event data contains the BLS key blsKey
and the result of the registration result
.
address
: The address for which the BLS key has been registered.
blsKeyRegDataSchema = {
"type": "object",
"required": ["blsKey", "result"],
"properties": {
"blsKey": {
"dataType": "bytes",
"length": BLS_PUBLIC_KEY_LENGTH,
"fieldNumber": 1
},
"proofOfPossession": {
"dataType": "bytes",
"length": BLS_POP_LENGTH,
"fieldNumber": 2
},
"result": {
"dataType": "uint32",
"fieldNumber": 3
}
}
}
The Validators module does not specify any commands.
This function creates a new validator account in the validators keys substore. It is called as part of the validator registration and authority registration commands that are part of the PoS and PoA module. It checks that there is no account already registered for the input validatorAddress
, that the input BLS key blsKey
has not been registered before in the chain, and that the input proof of possession proofOfPossession
for the BLS key is valid. Finally, it creates a new validator account in the validators keys substore and sets its generator key to the input generatorKey
.
def registerValidatorKeys(validatorAddress: Address,
proofOfPossession: ProofOfPossession,
generatorKey: PublicKeyEd25519,
blsKey: PublicKeyBLS) -> None:
if there exists an entry in the validators keys substore with storeKey == validatorAddress:
emitPersistentEvent(
module = MODULE_NAME_VALIDATORS,
name = EVENT_NAME_GENERATOR_KEY_REGISTRATION,
data = {
"generatorKey": generatorKey,
"result": KEY_REG_RESULT_ALREADY_VALIDATOR
},
topics=[validatorAddress]
)
raise Exception('This address is already registered as validator.')
if there exists an entry in the registered BLS keys substore with storeKey == blsKey:
emitPersistentEvent(
module = MODULE_NAME_VALIDATORS,
name = EVENT_NAME_BLS_KEY_REGISTRATION,
data = {
"blsKey": blsKey,
"proofOfPossession": proofOfPossession,
"result": KEY_REG_RESULT_DUPLICATE_BLS_KEY
},
topics=[validatorAddress]
)
raise Exception(f'The BLS key {blsKey.hex()} has already been registered in the chain.')
if PopVerify(blsKey, proofOfPossession) != VALID:
emitPersistentEvent(
module = MODULE_NAME_VALIDATORS,
name = EVENT_NAME_BLS_KEY_REGISTRATION,
data = {
"blsKey": blsKey,
"proofOfPossession": proofOfPossession,
"result": KEY_REG_RESULT_INVALID_POP
},
topics=[validatorAddress]
)
raise Exception('Invalid proof of possession for the given BLS key.')
validatorKeys = {
generatorKey: generatorKey,
blsKey: blsKey,
}
create an entry in the validators keys substore with storeKey = validatorAddress and storeValue = validatorKeys
create an entry in the registered BLS keys data substore with storeKey = blsKey and storeValue = validatorAddress
emitEvent(
module = MODULE_NAME_VALIDATORS,
name = EVENT_NAME_GENERATOR_KEY_REGISTRATION,
data = {
"generatorKey": generatorKey,
"result": KEY_REG_RESULT_SUCCESS
},
topics=[validatorAddress]
)
emitEvent(
module = MODULE_NAME_VALIDATORS,
name = EVENT_NAME_BLS_KEY_REGISTRATION,
data = {
"blsKey": blsKey,
"proofOfPossession": proofOfPossession,
"result": KEY_REG_RESULT_SUCCESS
},
topics=[validatorAddress]
)
The function PopVerify
is part of the BLS signature scheme.
This function creates a new validator account in the validators keys substore, where the blsKey
is set to INVALID_BLS_KEY
. It checks that there is no account already registered for the input validatorAddress
. Then, it creates a new validator account in the validators keys substore, with blsKey
set to INVALID_BLS_KEY
and the generator key set to the input generatorKey
.
This function only exist on the mainchain. It is called as part of the genesis block processing of the PoS module for the snapshot block used for the migration from Lisk Core 3 to Lisk Core 4.
def registerValidatorWithoutBLSKey(validatorAddress: Address, generatorKey: PublicKeyEd25519) -> None:
if there exists an entry in the validators keys substore with storeKey == validatorAddress:
emitPersistentEvent(
module = MODULE_NAME_VALIDATORS,
name = EVENT_NAME_GENERATOR_KEY_REGISTRATION,
data = {
"generatorKey": generatorKey,
"result": KEY_REG_RESULT_ALREADY_VALIDATOR
},
topics=[validatorAddress]
)
raise Exception('This address is already registered as validator.')
validatorKeys = {
generatorKey: generatorKey,
blsKey: INVALID_BLS_KEY,
}
create an entry in the validators keys substore with storeKey = validatorAddress and storeValue = validatorKeys
emitEvent(
module = MODULE_NAME_VALIDATORS,
name = EVENT_NAME_GENERATOR_KEY_REGISTRATION,
data = {
"generatorKey": generatorKey,
"result": KEY_REG_RESULT_SUCCESS
},
topics=[validatorAddress]
)
This function is used to retrieve information about the validator account corresponding to the input address: Address
. It returns validatorKeys(address)
. If there is no entry corresponding to address
, it throws an error.
def getValidatorKeys(address: Address) -> ValidatorKeys:
if no entry in the validators keys substore exist with storeKey == address:
raise Exception('No validator account found for the input address.')
return validatorKeys(address)
This function sets the BLS key of a validator account. It 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, and that the input proof of possession proofOfPossession
for the BLS key is valid. Finally, it updates the BLS key of the validator account to the input blsKey
.
Note: For blockchains build with the SDK v6, this function should be not be called except from the Legacy module used on the Lisk Mainchain. The reason is that changing BLS keys is not supported by the (interoperability) protocol.
def setValidatorBLSKey(validatorAddress: Address, proofOfPossession: ProofOfPossession, blsKey: PublicKeyBLS) -> None:
if no entry in the validators keys substore exist with storeKey == validatorAddress:
emitPersistentEvent(
module = MODULE_NAME_VALIDATORS,
name = EVENT_NAME_BLS_KEY_REGISTRATION,
data = {
"blsKey": blsKey,
"proofOfPossession": proofOfPossession,
"result": KEY_REG_RESULT_NO_VALIDATOR
},
topics=[validatorAddress]
)
raise Exception('This address is not registered as validator. Only validators can register a BLS key.')
if there exists an entry in the registered BLS keys substore with storeKey == blsKey:
emitPersistentEvent(
module = MODULE_NAME_VALIDATORS,
name = EVENT_NAME_BLS_KEY_REGISTRATION,
data = {
"blsKey": blsKey,
"proofOfPossession": proofOfPossession,
"result": KEY_REG_RESULT_DUPLICATE_BLS_KEY
},
topics=[validatorAddress]
)
raise Exception(f'The BLS key {blsKey.hex()} has already been registered in the chain.')
if PopVerify(blsKey, proofOfPossession) != VALID:
emitPersistentEvent(
module = MODULE_NAME_VALIDATORS,
name = EVENT_NAME_BLS_KEY_REGISTRATION,
data = {
"blsKey": blsKey,
"proofOfPossession": proofOfPossession,
"result": KEY_REG_RESULT_INVALID_POP
},
topics=[validatorAddress]
)
raise Exception('Invalid proof of possession for the given BLS key.')
validatorKeys(validatorAddress).blsKey = blsKey
create an entry in the registered BLS keys data substore with storeKey = blsKey and storeValue = validatorAddress
emitEvent(
module = MODULE_NAME_VALIDATORS,
name = EVENT_NAME_BLS_KEY_REGISTRATION,
data = {
"blsKey": blsKey,
"proofOfPossession": proofOfPossession,
"result": KEY_REG_RESULT_SUCCESS
},
topics=[validatorAddress]
)
This function sets the generator key of a validator account. It checks that there exists a validator account registered for the input validatorAddress
and then updates the generator key to the input generatorKey
.
def setValidatorGeneratorKey(validatorAddress: Address, generatorKey: PublicKeyEd25519) -> None:
if no entry in the validators keys substore exist with storeKey == validatorAddress:
emitPersistentEvent(
module = MODULE_NAME_VALIDATORS,
name = EVENT_NAME_GENERATOR_KEY_REGISTRATION,
data = {
"generatorKey": generatorKey,
"result": KEY_REG_RESULT_NO_VALIDATOR
},
topics=[validatorAddress]
)
raise Exception('This address is not registered as validator. Only validators can register a generator key.')
validatorKeys(validatorAddress).generatorKey = generatorKey
emitEvent(
module = MODULE_NAME_VALIDATORS,
name = EVENT_NAME_GENERATOR_KEY_REGISTRATION,
data = {
"generatorKey": generatorKey,
"result": KEY_REG_RESULT_SUCCESS
},
topics=[validatorAddress]
)
This function returns the address that registered the input BLS key blsKey
. It checks if an entry with store key equal to blsKey
exists in the registered BLS keys substore. If this is the case, it returns the stored address
, else it throws an error.
def getAddressFromBLSKey(blsKey: PublicKeyBLS) -> Address:
if no entry in the registered BLS keys substore exist with storeKey == blsKey:
raise Exception(f'The BLS key {blsKey.hex()} has not been registered in the chain.')
return registeredBLSKeys(blsKey)
This function returns the value stored in the Validator Params substore.
def getValidatorParams() -> ValidatorParams:
return validatorParamsStore
This function returns the addresses of the generators active between the two input timestamps and the number of block slots assigned to them. The slots corresponding to the input timestamps are NOT counted, only the slots in between. Notice that the input timestamps must be recent enough sucht that the active validator set for the counted slots is the one stored validatorParamsStore.validators
. If the input timestamps are older, the result will most likely be incorrect. It’s the calling module’s responsibility to ensure that.
def getGeneratorsBetweenTimestamps(startTimestamp: uint32, endTimestamp: uint32) -> dict[Address, uint32]:
if endTimestamp < startTimestamp:
raise Exception('End timestamp cannot be smaller than start timestamp.')
result = {}
startSlotNumber = (startTimestamp // BLOCK_TIME) + 1
endSlotNumber = (endTimestamp // BLOCK_TIME) - 1
if startSlotNumber > endSlotNumber:
return result
totalSlots = endSlotNumber - startSlotNumber + 1
generatorList = [validator.address for validator in validatorParamsStore.validators]
# Quick skip to directly assign many block slots to every generator in the list.
baseSlots = totalSlots // len(generatorList)
if baseSlots > 0:
totalSlots -= baseSlots * len(generatorList)
for generatorAddress in generatorList:
result[generatorAddress] = baseSlots
# Assign remaining block slots.
for slotNumber in range(startSlotNumber, startSlotNumber + totalSlots):
slotIndex = slotNumber % len(generatorList)
generatorAddress = generatorList[slotIndex]
if generatorAddress in result:
result[generatorAddress] += 1
else:
result[generatorAddress] = 1
return result
This function allows to set the precommit threshold, certificate threshold, validators and associated BFT weights to be used from the next height onward. The information is not validated and simply stored in the Validators module. At the end of the function, the information is further forwarded from the application domain to the consensus domain. In the consensus domain the parameters are then verified in the function setBFTParameters, which is called during the "After Application Processing" stage of the genesis block processing or block processing.
precommitThreshold
: The precommit threshold value to be used from the next height onward. The value must be a 64-bit unsigned integer.certificateThreshold
: The certificate threshold value to be used from the next height onward. The value must be a 64-bit unsigned integer.validatorList
: The validators and their associated BFT weight to be used from the next height onward. The value must be an array of objects with anaddress
property containing the 20-byte address of the validator and abftWeight
property containing the BFT weight of the validator as 64-bit unsigned integer. The order in this array determines the block generation order as the array is forwarded in the same order to the consensus domain. See also the functiongetGeneratorAtTimestamp
in LIP 0058.
setValidatorParams(precommitThreshold: uint64, certificateThreshold: uint64, validatorList: list[object]):
params = object of type ValidatorParams
params.precommitThreshold = precommitThreshold
params.certificateThreshold = certificateThreshold
# Obtain validator keys from state store.
newValidators = []
for validator in validatorList:
newValidator = object of type Validator
newValidator.address = validator.address
newValidator.bftWeight = validator.bftWeight
validatorKeys = getValidatorKeys(validator.address)
newValidator.blsKey = validatorKeys.blsKey
newValidator.generatorKey = validatorKeys.generatorKey
newValidators.append(newValidator)
params.validators = newValidators
# Write new validator params to state store.
validatorParamsStore = params
forward params to consensus domain
This function works exactly as the function getValidatorKeys
defined above.
This function checks that the input BLS key blsKey
has not been registered before in the chain and that the proof of possession proofOfPossession
for the BLS key is valid.
def validateBLSKey(proofOfPossession: ProofOfPossession, blsKey: PublicKeyBLS) -> bool:
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
The Validators module does not execute any logic during the genesis block processing.
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.
TBA