Skip to content
This repository has been archived by the owner on Jun 11, 2024. It is now read-only.

Latest commit

Β 

History

History
2122 lines (1706 loc) Β· 96.4 KB

lip-0057.md

File metadata and controls

2122 lines (1706 loc) Β· 96.4 KB
LIP: 0057
Title: Define state and state transitions of PoS module
Author: Maxime Gagnebin <[email protected]>
        Nazar Hussain <[email protected]>
        Mehmet Egemen Albayrak <[email protected]>
        Grigorios Koumoutsos <[email protected]>
Discussions-To: https://research.lisk.com/t/define-state-and-state-transitions-of-pos-module/320
Status: Active
Type: Standards Track
Created: 2021-09-03
Updated: 2024-01-04
Required: 0022, 0023, 0024, 0040, 0044, 0046, 0058, 0059

Abstract

The PoS (proof-of-stake) module is responsible for handling validator registration, staking, and computing the validator weight. In this LIP, we specify the properties of the PoS module, along with their serialization and initial values. Furthermore, we specify the state transitions logic defined within this module, i.e. the commands, the protocol logic injected during the block lifecycle, and the functions that can be called from other modules or off-chain services. We also specify the events emitted by the PoS module.

Copyright

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

Motivation

The PoS module handles all aspects of the generator selection, this includes the registration of accounts as validators, the staking process, and potential reports of misbehavior.

In this LIP we specify the properties, serialization, initialization, and exposed functions of the PoS module, as well as the protocol logic processed during a block life cycle and the module commands.

Rationale

This LIP does not introduce significant protocol changes to the generator selection mechanism proposed in LIP 0022 and LIP 0023. It only defines how the commands and processes defined in those LIPs are integrated in the state model used in Lisk. Please see LIP 0022 and LIP 0023 for a thorough rationale regarding the choice of staking system and the inclusion of standby validators. LIP 0022 defines a selection mechanism for 2 standby validators. In this LIP, we slightly extend the specifications to support 0 or 1 standby validator, however, we do not specify how to extend the protocol to more than 2 validators. Introducing more standby validators might require a different source of randomness and it is not the aim of this LIP to describe this topic.

PoS Store

Staker Substore

This part of the state store is used to maintain the stakes and recent unstakes of users (the ones for which the unstaked amount has not been unlocked). The entries are keyed by address and contain an array of the current stakes as well as an array of objects representing the tokens waiting to be unlocked.

Validator Substore

This part of the state store is used to maintain all information regarding the registered validators. It is keyed by address and contains values for the validator name, last generated height, total stake (i.e., the sum of all amounts staked with that validator), self-stake (i.e., the sum of all self-staked amounts of that validator), misbehavior report heights, and a flag asserting if the validator is banned or not.

Name Substore

This part of the state store is used to maintain a list of all validator names already registered. It allows the protocol to efficiently process the validator registration transaction. The entries are keyed by validator name and the value contains the address of the corresponding validator.

Eligible Validators Substore

This part of the state store is used to maintain the list of all non-banned validators that have more validator weight than a specified threshold. The validators from this list with the most weight are active validators, and the others are standby validators.

Snapshot Substore

This part of the state store is used to maintain the needed snapshots of the eligible validators. The entries are keyed by round number and contain the addresses and weights of eligible validators at the end of the corresponding round. Based on this information, the sets of active and standby validators are selected two rounds later. Entries for older rounds which are no longer necessary are removed.

Genesis Data Substore

This part of the state store is used to maintain information from the genesis block. This information is used to compute if a block is at the end of a round, and to generate the generator list during the bootstrap period.

Previous Timestamp Substore

This part of the state store is used to maintain the timestamp of the last block added to the chain. This is used when calling the function computing missed blocks from the Validators module.

Setting BFT Weights

In this LIP, we set the BFT weights of active validators to be proportional to their validator weight. Additionally, to avoid a large concentration of BFT weight in one or only very few validators, we introduce a capping threshold for the BFT weight, i.e., we do not allow one validator to get more than a certain percentage of the total BFT weight. This threshold is specified by the constant MAX_BFT_WEIGHT_CAP; for the Lisk mainchain, it is set to 10%.

The capping of BFT weights ensures a minimum of decentralization in the validator set, even in the case of a very unbalanced validator weight distribution. In practice, we expect the capping to only effect very few validators, if any, in particular as long as the reward system incentivizes decentralization and does not lead to concentration of huge staked amounts to few validators (this is the case for both reward modules proposed, see LIP 0042 and LIP 0071).

Note that further we choose floor(2/3 * aggregateBFTWeight) + 1 as precommit and certificate threshold for PoS chains, see LIP 0058 for an explanation of these parameters. Here aggregateBFTWeight is the sum of BFT weights of the active validators. This choice of BFT parameters means that the safety and liveness of the BFT consensus protocol is ensured if <1/3 * aggregateBFTWeight is malicious (for the case of changing validators the guarantees are slightly weaker). Developers might choose to modify the threshold values for their sidechains by updating the computeThresholds function. We crucially note, however, that modifying the chosen parameters has consequences for the security guarantees of the chain according to the security-liveness tradeoff proven in Theorem 4.4 of the Lisk-BFT paper. Therefore, we highly recommend not updating those parameters, unless the tradeoff is well-understood and there is some very clear reason to do so for a specific chain.

By setting the BFT weights proportional to validator weight instead of uniform BFT weights for all active validators as done before, we can improve the security guarantees of the BFT consensus protocol. If we neglect the effect of capping, which will likely effect only very few validators by a small percentage, then more than 1/3 of the active validator weight are needed to have more than 1/3 of the BFT weight for an attack of the network. Assuming validators generally have at least 10 % self-staking (i.e., no validator has total stake greater than 10 times their self-stake), this translates to at least 1/3 of the total staked amount for active validators would have to be controlled by the attacker.

In the current system of uniform BFT weights this is not the case. The set of 34 lowest-weight validators may have significantly less than 1/3 of the total validator weight although their total BFT weight is more than 1/3. This means that quite a lot less than 1/3 of the total staked amount for active validators is sufficient to control more than 1/3 of BFT weight, which would be sufficient to perform an attack on the network. Choosing BFT weights proportional to validator weight avoids this issue.

Bootstrap and Hybrid Period

The bootstrap period, defined in LIP 0034, is the initial period when the PoS validator selection mechanism is not active and blocks are generated by a fixed set of validators (specified in genesis block), called initial validators. In LIP 0034, we assumed that after the bootstrap period ends, all validators are selected according to the PoS selection mechanism. One drawback of adopting this approach here is that after the end of the bootstrap period, the change of validators might be too abrupt, possibly leading to breaking the BFT security assumptions and finalizing contradicting blocks or inability of the chain to produce certificates. To avoid those issues, a possible solution would be to not allow blocks to be finalized during the bootstrap period, as in LIP 34. This approach has some drawbacks however, for example, it does not allow sidechains to be interoperable during their bootstrap period, which is not desirable.

To circumvent the aforementioned issues and allow sidechains to produce certificates at any time, we introduce a hybrid period which starts just after the end of the bootstrap period. During the hybrid period, the set of validators is gradually shifting from initial validators to PoS-selected validators. In particular, in each round we decrease the number of initial validators by 1 and increase the number of validators selected from the PoS selection; therefore the hybrid phase lasts for NUMBER_ACTIVE_VALIDATORS - 1 rounds. This way, the change of validators between subsequent rounds will not be too abrupt and the sidechains would be able to finalize blocks and produce certificates during both bootstrap and hybrid periods without additional security risks.

Specification

Notation and Constants

For the rest of this proposal we define the following constants:

Name Type Value Description
Global constants
ADDRESS_LENGTH uint32 20 Length in bytes of type Address.
BLS_PUBLIC_KEY_LENGTH uint32 48 Length in bytes of type PublicKeyBLS.
BLS_POP_LENGTH uint32 96 Length in bytes of type ProofOfPossession.
ED25519_PUBLIC_KEY_LENGTH uint32 32 Length in bytes of type PublicKeyEd25519.
SEED_LENGTH uint32 16 Length in bytes of a valid seed revealed.
OWN_CHAIN_ID bytes The chain ID of the chain.
TOKEN_ID_LENGTH uint32 8 Length in bytes of type TokenID
INVALID_BLS_KEY bytes 48 bytes all set to 0x00 The byte value associated with validators that did not register a BLS key.
PoS store constants
SUBSTORE_PREFIX_STAKER bytes 0x0000 The substore prefix of the staker substore.
SUBSTORE_PREFIX_VALIDATOR bytes 0x8000 The substore prefix of the validator substore.
SUBSTORE_PREFIX_NAME bytes 0x4000 The substore prefix of the name substore.
SUBSTORE_PREFIX_SNAPSHOT bytes 0xc000 The substore prefix of the snapshot substore.
SUBSTORE_PREFIX_GENESIS_DATA bytes 0x2000 The substore prefix of the genesis data substore.
SUBSTORE_PREFIX_PREVIOUS_TIMESTAMP bytes 0xa000 The substore prefix of the previous timestamp substore.
SUBSTORE_PREFIX_ELIGIBLE_VALIDATORS bytes 0x6000 The substore prefix of the eligible validators substore.
PoS constants
MODULE_NAME_POS string "pos" The module name of the PoS module.
COMMAND_NAME_REGISTER_VALIDATOR string "registerValidator" The command name of the validator registration transaction.
COMMAND_NAME_STAKE string "stake" The command name of the stake transaction.
COMMAND_NAME_UNLOCK string "unlock" The command name of the unlock transaction.
COMMAND_NAME_REPORT_MISBEHAVIOR string "reportMisbehavior" The command name of the report misbehavior transaction.
MIN_INIT_ROUNDS uint32 3 The minimum number of rounds for the bootstrap period.
MAX_NUM_BYTES_Q96 uint32 24 The maximal number of bytes of a serialized fractional number in Q96 format (see LIP 0070).
Configurable Constants Mainchain Value
TOKEN_ID_POS bytes OWN_CHAIN_ID[0:1] + '00000000000000' The token ID of the token used for staking.
FACTOR_SELF_STAKING uint32 10 The factor multiplying the self-staked amount of a validator for the validator weight computation.
BASE_STAKING_AMOUNT uint32 10 * (10)^8 The minimum staking amount. All staked amounts should be multiples of this value.
MAX_LENGTH_NAME uint32 20 The maximum allowed name length for validators.
MAX_NUMBER_STAKING_SLOTS uint32 10 The maximum size of the stakes array of a staker substore entry.
MAX_NUMBER_PENDING_UNLOCKS uint32 20 The maximum size of the pendingUnlocks array of a staker substore entry.
FAIL_SAFE_MISSED_BLOCKS uint32 50 The number of consecutive missed blocks used in the fail safe banning mechanism.
FAIL_SAFE_INACTIVE_WINDOW uint32 120,960 = 14 * 24 * 3600 // BLOCK_TIME The length of the inactivity window used in the fail safe banning mechanism, measured in number of blocks.
LOCKING_PERIOD_STAKING uint32 25,920 = 3 * 24 * 3600 // BLOCK_TIME The locking period for regular staked tokens, measured in number of blocks.
LOCKING_PERIOD_SELF_STAKING uint32 241,920 = 28 * 24 * 3600 // BLOCK_TIME The locking period for self-staked tokens.
PUNISHMENT_WINDOW_STAKING uint32 241,920 = 28 * 24 * 3600 // BLOCK_TIME The punishment period for regular stakes, i.e., how many blocks after misbehavior report stakers can unlock tokens unstaked from a punished validator.
PUNISHMENT_WINDOW_SELF_STAKING uint32 725,760 = 3 * PUNISHMENT_WINDOW_STAKING The punishment period for self-stakes, i.e., how many blocks after misbehavior report punished validators can unlock their self-unstaked tokens.
REPORT_MISBEHAVIOR_REWARD uint32 10^8 The reward (in TOKEN_ID_POS) for a sender of a report misbehavior transaction.
REPORT_MISBEHAVIOR_LIMIT_BANNED uint32 5 The number of report misbehavior transactions against a validator for getting banned.
MIN_WEIGHT uint64 1000*(10^8) The minimum validator weight required to be selected as a block generator. It should have a non-zero value.
NUMBER_ACTIVE_VALIDATORS uint32 101 The number of active validators.To be compatible with the interoperability module, the value should be at most MAX_NUM_VALIDATORS ( constant defined in LIP 0045), due to the fact that BLS keys of all active validators should fit into a Cross-Chain Update Transaction (CCU).
NUMBER_STANDBY_VALIDATORS uint32 2 The number of standby validators. This LIP is specified for the number of standby validators being 0, 1 or 2.
VALIDATOR_REGISTRATION_FEE uint64 10*(10^8) The extra command fee of the validator registration.
WEIGHT_SCALE_FACTOR uint32 1000 * (10)^8 It determines the factor by which BFT weights are divided.
MAX_BFT_WEIGHT_CAP uint32 1000 It determines the maximum BFT weight percentage for a single validator. The percentage is obtained by dividing this value by 100, i.e., a value of 1000 corresponds to 10%.
INVALID_BLS_KEYS_IN_GENESIS_BLOCK uint32 True This value determines if the function registerValidatorKeys or registerValidatorWithoutBLSKey from the validators module should be used when registering validators listed in a genesis block. This value must be set to True for the v4 re-genesis block of the Lisk mainchain. For all other genesis blocks, it should be set to False.

We also define ROUND_LENGTH to be the length of a round, i.e., ROUND_LENGTH = NUMBER_ACTIVE_VALIDATORS + NUMBER_STANDBY_VALIDATORS.

Event Names and Results

Name Type Value Description
Event names
EVENT_NAME_VALIDATOR_REGISTERED string "validatorRegistered" Used for events during validator registration.
EVENT_NAME_AMOUNT_STAKED string "amountStaked" Used for events related to staking with a validator.
EVENT_NAME_VALIDATOR_PUNISHED string "validatorPunished" Used for events related to punishing a validator.
EVENT_NAME_VALIDATOR_BANNED string "validatorBanned" Used for events related to banning a validator.
Result codes
STAKE_SUCCESSFUL uint32 0 Used when a stake succeeds.
STAKE_FAILED_NON_REGISTERED_VALIDATOR uint32 1 Used when a stake transaction fails because the staked account has not registered a validator.
STAKE_FAILED_INVALID_UNSTAKE_PARAMETERS uint32 2 Used when a stake transaction fails because the unstaked amount exceeds the total stakes sent from staker to validator.
STAKE_FAILED_TOO_MANY_PENDING_UNLOCKS uint32 3 Used when a stake transaction fails because it the total number of pending unlocks of staker exceeds MAX_NUMBER_PENDING_UNLOCKS.
STAKE_FAILED_TOO_MANY_VALIDATORS uint32 4 Used when a stake transaction fails because the total number of validators that the user has staked with exceeds MAX_NUMBER_STAKING_SLOTS.

Type Definitions

Name Type Validation Description
BlockHeader object Must follow the blockHeaderSchema schema defined in LIP 0055. An object representing a block header.
Address bytes Must be of length ADDRESS_LENGTH. Address of an account.
TokenID bytes Must be of length TOKEN_ID_LENGTH. Used for token identifiers.
UnlockObject object Contains 3 elements (address, amount, unstakeHeight) of types Address, uint64 and uint32 respectively (same as the items in the pendingUnlocks array of the stakerStoreSchema). An object containing information regarding unstaking a validator.
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.
PublicKeyEd25519 bytes Must be of length ED25519_PUBLIC_KEY_LENGTH. Used for Ed25519 public keys.
StakerStoreObject object Must follow the stakerStoreSchema schema. Deserialized version of staker substore values.
ValidatorStoreObject object Must follow the validatorStoreSchema schema. Deserialized version of validator substore values.
EligibleValidatorObject object Contains 2 elements (address, weight) of types Address and uint64 respectively (same as the items in the validatorWeightSnapshot array of the snapshotStoreSchema). An object containing information regarding an eligible validator.
ValidatorObject object Contains 2 elements (address, bftWeight) of types Address and uint64 respectively. An object containing information regarding a validator.

Functions from Other Modules

Calling a function fct from another module (named module) is represented by module.fct(required inputs).

PoS Module Store

The store keys and values of the PoS store are set as follows:

Staker Substore

Store Prefix, Store Key, and Store Value
  • The store prefix is set to SUBSTORE_PREFIX_STAKER.
  • Each store key is a ADDRESS_LENGTH-byte address, representing a user address.
  • Each store value is the serialization of an object following stakerStoreSchema.
  • Notation: For the rest of this proposal let stakerStore(address) be the value stored in the staker substore with store key address, deserialized using stakerStoreSchema.
JSON Schema
stakerStoreSchema = {
    "type": "object",
    "required": ["stakes", "pendingUnlocks"],
    "properties": {
        "stakes": {
            "type": "array",
            "fieldNumber": 1,
            "items": {
                "type": "object",
                "required": ["validatorAddress", "amount", "sharingCoefficients"],
                "properties": {
                    "validatorAddress": {
                        "dataType": "bytes",
                        "length": ADDRESS_LENGTH,
                        "fieldNumber": 1
                    },
                    "amount": {
                        "dataType": "uint64",
                        "fieldNumber": 2
                    },
                    "sharingCoefficients": {
                        "type": "array",
                        "fieldNumber": 3,
                        "items":{
                            "type": "object",
                            "required": ["tokenID", "coefficient"],
                            "properties": {
                                "tokenID": {
                                    "dataType": "bytes",
                                    "length": TOKEN_ID_LENGTH,
                                    "fieldNumber": 1
                                },
                                "coefficient":{
                                    "dataType": "bytes",
                                    "maxLength": MAX_NUM_BYTES_Q96,
                                    "fieldNumber": 2
                                }
                            }
                        }
                    }
                }
            }
        },
        "pendingUnlocks": {
            "type": "array",
            "fieldNumber": 2,
            "items": {
                "type": "object",
                "required": ["validatorAddress", "amount", "unstakeHeight"],
                "properties": {
                    "validatorAddress": {
                        "dataType": "bytes",
                        "length": ADDRESS_LENGTH,
                        "fieldNumber": 1
                    },
                    "amount": {
                        "dataType": "uint64",
                        "fieldNumber": 2
                    },
                    "unstakeHeight": {
                        "dataType": "uint32",
                        "fieldNumber": 3
                    }
                }
            }
        }
    }
}
Properties

In this section, we describe the properties of the staker substore.

  • stakes: stores an array of the current staking information of a user. This array was called votes in LIP 0023. For each validator that the user has staked with, we store their address and the amount of tokens that the user had staked with them; in case reward sharing is enabled, we also store the sharing coefficients at the time of staking (or at the last time rewards for this stake were claimed). This array is updated with a stake command. The stakes array is always kept ordered in lexicographical order of validatorAddress. Its size is at most MAX_NUMBER_STAKING_SLOTS, any state transition that would increase it to above MAX_NUMBER_STAKING_SLOTS is invalid. Any element with amount == 0 is removed from the array. For all elements of this array, the sharingCoefficients array is always kept ordered in lexicographical order of tokenID
  • pendingUnlocks: stores an array representing the tokens that have been unstaked, but not yet unlocked. Each unstake generates an object in this array containing the address of the unstaked validator, the amount of the unstake and the height at which the unstake was included in the chain. Objects in this array get removed when the corresponding unlock command is executed. This array was called unlocking in LIP 0023. This array is updated with stake and unlock commands. The pendingUnlocks array is always kept ordered by lexicographical order of validatorAddress, ties broken by increasing amount, ties broken by increasing unstakeHeight. The size of the pendingUnlocks array is at most MAX_NUMBER_PENDING_UNLOCKS, any state transition that would increase it to above MAX_NUMBER_PENDING_UNLOCKS is invalid. NB: by construction, all elements of this array will have amount != 0.
  • If any state transition would result in a staker substore entry to have stakes == [] and pendingUnlocks == [], the entry is removed from the store.

Validator Substore

Store Prefix, Store Key, and Store Value
  • The store prefix is set to SUBSTORE_PREFIX_VALIDATOR.
  • Each store key is a ADDRESS_LENGTH-byte address, representing a validator address.
  • Each store value is the serialization of an object following validatorStoreSchema.
  • Notation: For the rest of this proposal let validatorStore(address) be the value stored in the validator substore with store key address, deserialized using validatorStoreSchema.
JSON Schema
validatorStoreSchema = {
    "type": "object",
    "required": [
        "name",
        "totalStake",
        "selfStake",
        "lastGeneratedHeight",
        "isBanned",
        "reportMisbehaviorHeights",
        "consecutiveMissedBlocks",
        "commission",
        "lastCommissionIncreaseHeight",
        "sharingCoefficients"
    ],
    "properties": {
        "name": {
            "dataType": "string",
            "fieldNumber": 1
        },
        "totalStake": {
            "dataType": "uint64",
            "fieldNumber": 2
        },
        "selfStake": {
            "dataType": "uint64",
            "fieldNumber": 3
        },
        "lastGeneratedHeight": {
            "dataType": "uint32",
            "fieldNumber": 4
        },
        "isBanned": {
            "dataType": "boolean",
            "fieldNumber": 5
        },
        "reportMisbehaviorHeights": {
            "type": "array",
            "fieldNumber": 6,
            "items": {"dataType": "uint32"}
        },
        "consecutiveMissedBlocks": {
            "dataType": "uint32",
            "fieldNumber": 7
        },
        "commission": {
            "dataType": "uint32",
            "fieldNumber": 8
        },
        "lastCommissionIncreaseHeight": {
            "dataType": "uint32",
            "fieldNumber": 9
        },
        "sharingCoefficients": {
            "type": "array",
            "fieldNumber": 10,
            "items":{
                "type": "object",
                "required": ["tokenID", "coefficient"],
                "properties": {
                    "tokenID": {
                        "dataType": "bytes",
                        "length": TOKEN_ID_LENGTH,
                        "fieldNumber": 1
                    },
                    "coefficient":{
                        "dataType": "bytes",
                        "maxLength": MAX_NUM_BYTES_Q96,
                        "fieldNumber": 2
                    }
                }
            }
        }
    }
}
Properties

In this section, we describe the properties of the validator substore. Entries in this substore can be created during the execution of the genesis block. When the chain is running, entries in this substore are created by a validator registration command and its value is set during the command execution. It contains information about the validator whose address is the store key.

  • name: a string representing the validator name, with a minimum length of 1 character and a maximum length of MAX_LENGTH_NAME.
  • totalStake: the total stake of a validator.
  • selfStake : the total self-stake of a validator.
  • lastGeneratedHeight: the height at which the validator last generated a block.
  • isBanned: a Boolean value indicating if the validator is banned or not. Banned validators are never chosen to generate new blocks.
  • reportMisbehaviorHeights: the heights at which a report misbehavior command was successfully executed with blocks generated by the validator.
  • consecutiveMissedBlocks: the number of consecutive missed blocks by the validator. This value resets to 0 whenever a block generated by the validator is included in the blockchain.

The following three properties are relevant only in case reward sharing is enabled. See LIP 0070 for more details.

  • commission: a number specifying the commission of the validator. We use two decimals precision, i.e., the number will be an integer from 0 to 10000, where the commission percentage is commission/100.
  • lastCommissionIncreaseHeight : the height at which the validator last increased the commission.
  • sharingCoefficients: an array containing the value of the sharing coefficients of the validator for each token.

Name Substore

Store Prefix, Store Key, and Store Value
  • The store prefix is set to SUBSTORE_PREFIX_NAME.
  • Each store key is a utf8-encoded string name, representing a validator name.
  • Each store value is the serialization of an object following nameStoreSchema.
  • Notation: For the rest of this proposal let nameStore(name) be the value stored in the name substore with store key name, deserialized using nameStoreSchema.
JSON Schema
nameStoreSchema = {
    "type": "object",
    "required": ["validatorAddress"],
    "properties": {
        "validatorAddress": {
            "dataType": "bytes",
            "length": ADDRESS_LENGTH,
            "fieldNumber": 1
        }
    }
}
Properties

The name substore maintains all registered names, using the name as store key and storing the address of the validator that registered that name in the corresponding store value. Entries in this substore are created during the execution of validator registration command.

Eligible Validators Substore

Store Prefix, Store Key, and Store Value
  • The store prefix is set to SUBSTORE_PREFIX_ELIGIBLE_VALIDATORS.
  • For the entry corresponding to a validator, the store key is the concatenation of the validator weight (represented using the big endian uint64 serialization) and the validator address, i.e., weight.to_bytes(8,'big') + address.
  • Each store value is the serialization of an object following eligibleValidatorsStoreSchema.
  • Notation: For the rest of this proposal let eligibleValidatorsStore(key) be the value stored in the eligible validators substore with store key key, deserialized using eligibleValidatorsStoreSchema.
JSON Schema
eligibleValidatorsStoreSchema = {
    "type": "object",
    "required": ["lastReportMisbehaviorHeight"],
    "properties": {
        "lastReportMisbehaviorHeight": {
            "dataType": "uint32",
            "fieldNumber": 1
        }
    }
}
Properties

In this section, we describe the properties of the eligible validators substore.

  • lastReportMisbehaviorHeight: the height of the last report misbehavior transaction against the specified validator. It's default value is 0.

This substore only maintains an entry for validators that have weight more than MIN_WEIGHT, and that are not banned. It is updated via the updateValidatorEligibility function.

Snapshot Substore

Store Prefix, Store Key, and Store Value
  • The store prefix is set to SUBSTORE_PREFIX_SNAPSHOT.
  • Each store key is roundNumber.to_bytes(4,'big'), i.e., the big endian uint32 serialization of roundNumber, where roundNumber is the number of the round for which the snapshot is supposed to be used for determining the validators. This snapshot is defined at the end of round roundNumber -3 and is used at the end of round roundNumber -1 to determine the validators for round roundNumber.
  • Each store value is the serialization of an object following snapshotStoreSchema.
  • Notation: For the rest of this proposal let snapshotStore(roundNumber) be the value stored in the snapshot substore with store key roundNumber.to_bytes(4,'big'), deserialized using snapshotStoreSchema.
JSON Schema
snapshotStoreSchema = {
    "type": "object",
    "required": ["validatorWeightSnapshot"],
    "properties": {
        "validatorWeightSnapshot": {
            "type": "array",
            "fieldNumber": 1,
            "items": {
                "type": "object",
                "required": ["address", "weight"],
                "properties": {
                    "address": {
                        "dataType": "bytes",
                        "length": ADDRESS_LENGTH,
                        "fieldNumber": 1
                    },
                    "weight": {
                        "dataType": "uint64",
                        "fieldNumber": 2
                    }
                }
            }
        }
    }
}

The validatorWeightSnapshot array is ordered by decreasing weight, ties broken by reverse lexicographical ordering of address.

Properties

In this section, we describe the properties of the snapshot substore.

  • validatorWeightSnapshot: all validator addresses and weights of all non-banned validators with more than MIN_WEIGHT validator weight for the given round number.

The snapshot substore is initially empty.

Genesis Data Substore

Store Prefix, Store Key, and Store Value
  • The store prefix is set to SUBSTORE_PREFIX_GENESIS_DATA.
  • The store key is set to empty bytes.
  • The store value is the serialization of an object following genesisDataStoreSchema.
  • Notation: For the rest of this proposal let:
    • genesisDataStore.height be the height property of the entry in the genesis data substore.
    • genesisDataStore.initRounds be the initRounds property of the entry in the genesis data substore.
    • genesisDataStore.initValidators be the initValidators property of the entry in the genesis data substore.
JSON Schema
genesisDataStoreSchema = {
    "type": "object",
    "required": [
        "height",
        "initRounds",
        "initValidators"
    ],
    "properties": {
        "height": {
            "dataType": "uint32",
            "fieldNumber": 1
        },
        "initRounds": {
            "dataType": "uint32",
            "fieldNumber": 2
        },
        "initValidators": {
            "type": "array",
            "fieldNumber": 3,
            "items": {
                "dataType": "bytes",
                      "length": ADDRESS_LENGTH
             }
        }
    }
}
Properties

The genesis data substore stores information from the genesis block. It is initialized when processing the genesis block.

  • height: height of the genesis block.
  • initRounds: the length of the bootstrap period, also called initial rounds. initRounds must be at least MIN_INIT_ROUNDS.
  • initValidators: the addresses of the validators to be used during the bootstrap period. This property must have a non-empty value.

Previous Timestamp Substore

Store Prefix, Store Key, and Store Value
  • The store prefix is set to SUBSTORE_PREFIX_PREVIOUS_TIMESTAMP.
  • The store key is set to empty bytes.
  • The store value is the serialization of an object following previousTimestampStoreSchema
  • Notation: For the rest of this proposal, let previousTimestamp be the timestamp property of the entry in the previous timestamp substore, deserialized using the previousTimestampStoreSchema schema.
JSON Schema
previousTimestampStoreSchema = {
    "type": "object",
    "required": ["timestamp"],
    "properties": {
        "timestamp": {
            "dataType": "uint32",
            "fieldNumber": 1
        }
    }
}
Properties

timestamp: The timestamp of the last block added to the chain.

Commands

Validator Registration

Transactions executing this command have:

  • module = MODULE_NAME_POS,
  • command = COMMAND_NAME_REGISTER_VALIDATOR.
Parameters
registerValidatorTransactionParams = {
    "type": "object",
    "required": [
        "name",
        "blsKey",
        "proofOfPossession",
        "generatorKey"
    ],
    "properties": {
        "name": {
            "dataType": "string",
            "fieldNumber": 1
        },
        "blsKey": {
            "dataType": "bytes",
            "length" : BLS_PUBLIC_KEY_LENGTH,
            "fieldNumber": 2
        },
        "proofOfPossession": {
            "dataType": "bytes",
            "length" : BLS_POP_LENGTH,
            "fieldNumber": 3
        },
        "generatorKey": {
            "dataType": "bytes",
            "length": ED25519_PUBLIC_KEY_LENGTH,
            "fieldNumber": 4
        }
    }
}
Verification
def verify(trs: Transaction) -> None:
    trsParams = decode(registerValidatorTransactionParams, trs.params)
    
    validatorAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # Derive validatorAddress from trs.senderPublicKey.    

    if there exists an entry validatorStore(validatorAddress) in validator substore:
        raise Exception('This address has already registered a validator.')
    if not isValidatorNameValid(trsParams.name):
        raise Exception('Invalid name')
    if there exists an entry nameStore(trsParams.name) in name substore:
        raise Exception('Name already used by a validator.')
    if trs.fee < VALIDATOR_REGISTRATION_FEE:
        raise Exception('Insufficient transaction fee.')
Execution

When a transaction trs executing a validator registration command included in a block b, the logic below is followed:

def execute(trs: Transaction) -> None:
    trsParams = decode(registerValidatorTransactionParams, trs.params)

    b = block including trs
    validatorAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # Derive validatorAddress from trs.senderPublicKey.
    validatorName = trsParams.name

    # This step also checks that the BLS key has not been used from another validator.
    Validators.registerValidatorKeys(validatorAddress,
                                    trsParams.proofOfPossession,
                                    trsParams.generatorKey,
                                    trsParams.blsKey)

    # The new validator pays the registration fee.
    Fee.payFee(VALIDATOR_REGISTRATION_FEE)

    # Update validator substore.
    validatorState = {
            "name": validatorName,
            "totalStake": 0,
            "selfStake": 0,
            "lastGeneratedHeight": b.header.height,
            "isBanned": False,
            "reportMisbehaviorHeights": [],
            "consecutiveMissedBlocks": 0,
            "commission": 10000,
            "lastCommissionIncreaseHeight": b.header.height,
            "sharingCoefficients": []
        }

    create an entry in the validator substore with
        storeKey = validatorAddress,
        storeValue =  encode(validatorStoreSchema, validatorState)

    # Update name substore.
    create an entry in the name substore with
        storeKey = validatorName encoded as utf8,
        storeValue = encode(nameStoreSchema, {"validatorAddress": validatorAddress})

    # Emit event for the successful validator registration.
    emitEvent(
            module=MODULE_NAME_POS,
            name=EVENT_NAME_VALIDATOR_REGISTERED,
            data={
                "address": validatorAddress,
                "name": validatorName
            },
            topics=[validatorAddress]
        )

Stake

Transactions executing this command have:

  • module = MODULE_NAME_POS
  • command = COMMAND_NAME_STAKE
Parameters
stakeTransactionParams = {
    "type": "object",
    "required": ["stakes"],
    "properties": {
        "stakes": {
            "type": "array",
            "fieldNumber": 1,
            "items": {
                "type": "object",
                "required": ["validatorAddress", "amount"],
                "properties": {
                    "validatorAddress" : {
                        "dataType": "bytes",
                        "length": ADDRESS_LENGTH,
                        "fieldNumber": 1
                    },
                    "amount": {
                        "dataType": "sint64",
                        "fieldNumber": 2
                    }
                }
            }
        }
    }
}
Verification

Let trsParams = decode(stakeTransactionParams, trs.params) for the given transaction trs. Then the following verification steps are performed:

  • trsParams.stakes has at most 2 * MAX_NUMBER_STAKING_SLOTS elements. The reason of choosing this bound on the size is to allow a staker to unstake all current validators (which are at most MAX_NUMBER_STAKING_SLOTS) and stake with MAX_NUMBER_STAKING_SLOTS new ones in the same transaction.
  • A given validatorAddress is included in at most one stake from the list of stakes (regardless of the associated amounts).
  • For all stakes included in trsParams.stakes, we have:
    • amount value is a multiple of BASE_STAKING_AMOUNT, i.e., amount % BASE_STAKING_AMOUNT == 0. For the Lisk mainchain, where BASE_STAKING_AMOUNT = 10^9 and TOKEN_ID_POS is the token ID of the LSK token, this corresponds to multiples of 10 LSK.
    • amount != 0.
Execution

When executing a stake transaction trs, the logic below is followed

def execute(trs: Transaction) -> None:
    trsParams = decode(stakeTransactionParams, trs.params)

    b = block including trs
    height = b.header.height
    senderAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # Derive address from trs.senderPublicKey.

    # Sorting the stakes guarantees that we first apply the stakes with negative amounts.
    sortedStakes = trsParams.stakes ordered by increasing value of amount

    for stake in sortedStakes:
        validatorAddress = stake.validatorAddress

        if validatorStore(validatorAddress) does not exist:
            emitStakeEvent(senderAddress, validatorAddress, stake.amount, STAKE_FAILED_NON_REGISTERED_VALIDATOR)
            raise Exception('Invalid stake: no registered validator with the specified address')

        if stake.amount < 0: # Case of unstake.
            sentStake = element in stakerStore(senderAddress).stakes with sentStake.validatorAddress == validatorAddress
            if not sentStake or abs(stake.amount) > sentStake.amount:
                emitStakeEvent(senderAddress, validatorAddress, stake.amount, STAKE_FAILED_INVALID_UNSTAKE_PARAMETERS)    
                raise Exception('Invalid unstake: The unstaked amount exceeds the staked amount for this validator')

            # Assign the rewards related to this stake to the sender.
            i = index of sentStake in stakerStore(senderAddress).stakes
            assignStakingRewards(senderAddress, i)

            # Update staker substore.
            stakerStore(senderAddress).stakes[i].amount += stake.amount

            if stakerStore(senderAddress).stakes[i].amount == 0:
                remove sentStake from stakerStore(senderAddress).stakes

            # Create unlock object for the unstake.
            unlockObject = {
                "validatorAddress" : validatorAddress,
                "amount" : abs(stake.amount),
                "unstakeHeight" : height
            }
            add unlockOject to stakerStore(senderAddress).pendingUnlocks, keeping the array ordered by lexicographical order of validatorAddress,
                                                                         ties broken by increasing amount,
                                                                         ties broken by increasing unstakeHeight

            if len(stakerStore(senderAddress).pendingUnlocks) > MAX_NUMBER_PENDING_UNLOCKS:
                emitStakeEvent(senderAddress, validatorAddress, stake.amount, STAKE_FAILED_TOO_MANY_PENDING_UNLOCKS)    
                raise Exception('Sender has reached the maximum number of pending unlocks.')

        if stake.amount > 0: # Case of regular stake.
            Token.lock(senderAddress, MODULE_NAME_POS, TOKEN_ID_POS, stake.amount) # Lock the staked amount.

            # User has already staked this validator.
            if there exists an entry oldStake in stakerStore(senderAddress).stakes with oldStake.validatorAddress = validatorAddress:
                i = index of oldStake in stakerStore(senderAddress).stakes
                assignStakingRewards(senderAddress, i)

                # Update staker substore.
                stakerStore(senderAddress).stakes[i].amount += stake.amount

            else: # New stake.
                add {"validatorAddress": validatorAddress, "amount": stake.amount,
                     "sharingCoefficients": validatorStore(stake.validatorAddress).sharingCoefficients}
                     to stakerStore(senderAddress).stakes
                keeping the array ordered in lexicographical order of validatorAddress

            if len(stakerStore(senderAddress).stakes) > MAX_NUMBER_STAKING_SLOTS:
                emitStakeEvent(senderAddress, validatorAddress, stake.amount, STAKE_FAILED_TOO_MANY_VALIDATORS)
                raise Exception('This address has reached the maximum number of staking slots.')

        # Update validator substore.
        previousValidatorWeight = getValidatorWeight(validatorAddress)

        validatorStore(validatorAddress).totalStake += stake.amount
        if senderAddress == validatorAddress:
            validatorStore(validatorAddress).selfStake += stake.amount

        emitStakeEvent(senderAddress, validatorAddress, stake.amount, STAKE_SUCCESSFUL)
        # Update eligible validators substore.
        updateValidatorEligibility(validatorAddress, previousValidatorWeight)


def emitStakeEvent(senderAddress: Address, validatorAddress: Address, amount: uint64, result: uint32) -> None:
    if result == STAKE_SUCCESSFUL:
        emitEvent(
            module = MODULE_NAME_POS,
            name = EVENT_NAME_AMOUNT_STAKED,
            data={
                "senderAddress": senderAddress,
                "validatorAddress": validatorAddress,
                "amount": amount,
                "result": STAKE_SUCCESSFUL
            },
            topics = [senderAddress]
        )
    else:
        emitPersistentEvent(
            module = MODULE_NAME_POS,
            name = EVENT_NAME_AMOUNT_STAKED,
            data={
                "senderAddress": senderAddress,
                "validatorAddress": validatorAddress,
                "amount": amount,
                "result": result
            },
            topics = [senderAddress]
        )

Here, the updateValidatorEligibility function updates the eligible validators substore according to the new weight of the validator the user stakes with. The assignStakingRewards function is defined in LIP 0070. The calls to this function are needed only in case reward sharing is enabled.

Unlock

Transactions executing this command have:

  • module = MODULE_NAME_POS
  • command = COMMAND_NAME_UNLOCK
Parameters
unlockTransactionParams = {
    "type": "object",
    "required": [],
    "properties": {}
}
Verification

No additional verification is performed for transactions executing this command.

Execution
def execute(trs: Transaction) -> None:
    senderAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # Derive address from trs.senderPublicKey.
    b = block including trs
    height = b.header.height

    for each unlockObject in stakerStore(senderAddress).pendingUnlocks:
        # Check if unstaked amount can be unlocked.
        if (isUnlockable(unlockObject, senderAddress, height)
            and isCertificateGenerated(unlockObject)):

            delete unlockObject from stakerStore(senderAddress).pendingUnlocks
            Token.unlock(senderAddress, MODULE_NAME_POS, TOKEN_ID_POS, unlockObject.amount)
            # Token module has its own event for successful/failed unlock so no need to add event here.

The definition and rationale for the isCertificateGenerated function is part of LIP 0059. The function isUnlockable is defined below. This function has the following input parameters:

  • unlockObject: an object with properties validatorAddress (the address of the previously staked validator), amount (the unstaked amount) and unstakeHeight (the height of the unstake).
  • senderAddress: Address of the user sending the unlock transaction.
  • height: the height of the block including the unlock transaction.
def isUnlockable(unlockObject: UnlockObject, senderAddress: Address, height: uint32) -> bool:
    validatorAddress = unlockObject.validatorAddress
    if validatorAddress == senderAddress:
        lockingPeriod = LOCKING_PERIOD_SELF_STAKING
        punishmentWindow = PUNISHMENT_WINDOW_SELF_STAKING
    else:
        lockingPeriod = LOCKING_PERIOD_STAKING
        punishmentWindow = PUNISHMENT_WINDOW_STAKING

    if height < unlockObject.unstakeHeight + lockingPeriod:
        return False

    if len(validatorStore(validatorAddress).reportMisbehaviorHeights) > 0: # Validator might be punished.
        let lastReportMisbehaviorHeight be the last element of validatorStore(validatorAddress).reportMisbehaviorHeights
        # lastReportMisbehaviorHeight is also the largest element of the reportMisbehaviorHeights array.

        if height < lastReportMisbehaviorHeight + punishmentWindow and lastReportMisbehaviorHeight < unlockObject.unstakeHeight + lockingPeriod:
            return False

    return True

In LIP 0023 the locking period for unstaked tokens was set to roughly 5.5 hours (2000 blocks) for the lisk mainchain. In this LIP is to change this value to 3 days, by setting LOCKING_PERIOD_STAKING to 25.920 = 3 * 24 * 3600 // BLOCK_TIME blocks. Similarly, the constants LOCKING_PERIOD_SELF_STAKING, PUNISHMENT_WINDOW_STAKING are modified to be exactly 4 weeks (28 days) and PUNISHMENT_WINDOW_SELF_STAKING = 3 * PUNISHMENT_WINDOW_STAKING.

Report Misbehavior

Transactions executing this command have:

  • module = MODULE_NAME_POS
  • command = COMMAND_NAME_REPORT_MISBEHAVIOR
Parameters
reportMisbehaviorParams = {
    "type": "object",
    "required": ["header1", "header2"],
    "properties": {
        "header1": {
            "dataType": "bytes",
            "fieldNumber": 1
        },
        "header2": {
            "dataType": "bytes",
            "fieldNumber": 2
        }
    }
}
Verification

Both properties of the parameters must follow the block header schema blockHeaderSchema defined in LIP 0055. Validity of this transaction was previously specified in LIP 0024. For completeness, we include the pseudocode here.

def verify(trs: Transaction) -> None:
    trsParams = decode(reportMisbehaviorParams, trs.params)
    
    b = block including trs

    header1 = trsParams.header1
    header2 = trsParams.header2

    if header1 or header2 do not satisfy blockHeaderSchema schema:
        raise Exception('Invalid block header.')

    header1 = decode(blockHeaderSchema, header1)
    header2 = decode(blockHeaderSchema, header2)

    if max(abs(header1.height - b.header.height), abs(header2.height - b.header.height))  >= LOCKING_PERIOD_SELF_STAKING:
        raise Exception('Locking period has expired.')
    if isPunished(header1.address, b.header.height):
        raise Exception('Validator is already punished.')
    if validatorStore(header1.address).isBanned:
        raise Exception('Validator is banned.')
    if verifyBlockSignature(header1) == False or verifyBlockSignature(header2) == False:
        raise Exception('Invalid block signature')
    if areHeadersContradicting(header1, header2) == False:
        raise Exception('Block headers are not contradicting.')

The verifyBlockSignature function is an internal function defined below. The areHeadersContradicting function is defined in LIP 0058.

Execution

Execution of this transaction was previously specified in LIP 0024. Here we update the specifications to be integrated in the state model used in Lisk.

def execute(trs: Transaction) -> None:
    trsParams = decode(reportMisbehaviorParams, trs.params)

    b = block including trs
    h = b.header.height
    senderAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # Derive address from trs.senderPublicKey.

    header1 = decode(blockHeaderSchema, trsParams.header1)
    header2 = decode(blockHeaderSchema, trsParams.header2)

    punishedAddress = trsParams.header1.generatorAddress

    # Update validator substore.
    validatorStore(punishedAddress).reportMisbehaviorHeights.append(h)    

    # Emit event for the validator punishment.
    emitEvent(
            module=MODULE_NAME_POS,
            name=EVENT_NAME_VALIDATOR_PUNISHED,
            data={
                "address": validatorAddress
            },
            topics=[validatorAddress]
        )

    # Check if the validator should be banned.
    if len(validatorStore(punishedAddress).reportMisbehaviorHeights) == REPORT_MISBEHAVIOR_LIMIT_BANNED:
        validatorStore(punishedAddress).isBanned = True
        # Emit event for the validator banning.
        emitEvent(
                module=MODULE_NAME_POS,
                name=EVENT_NAME_VALIDATOR_BANNED,
                data={
                    "address": validatorAddress
                },
                topics=[validatorAddress]
            )

    # Assign the misbehavior report reward to the sender of the transaction.
    # The amount is taken from the punished validator account.
    senderReward = min(REPORT_MISBEHAVIOR_REWARD, validatorStore(punishedAddress).selfStake)

    Token.unlock(punishedAddress, MODULE_NAME_POS, TOKEN_ID_POS, senderReward)
    Token.transfer(punishedAddress,
                   senderAddress,
                   TOKEN_ID_POS,
                   senderReward)
    # Update validator and staker substores for punished validator.
    oldWeight = getValidatorWeight(punishedAddress)
    validatorStore(punishedAddress).selfStake -= senderReward
    validatorStore(punishedAddress).totalStake -= senderReward
    i = index of element in stakerStore(punishedAddress).stakes array with item.validatorAddress == punishedAddress
    stakerStore(punishedAddress).stakes[i].amount -= senderReward

    # Update eligibility of punished validator, based on new weight and potential ban.
    updateValidatorEligibility(punishedAddress, oldWeight)

Events

validatorRegistered

This event has name = EVENT_NAME_VALIDATOR_REGISTERED. This event is emitted when a new validator gets registered.

Topics
  • validatorAddress: the address of the account registering a validator.
Data
validatorRegisteredDataSchema = {
    "type": "object",
    "required" = [
        "address",
        "name"
    ],
    "properties": {
        "address": {
            "dataType": "bytes",
            "length": ADDRESS_LENGTH,
            "fieldNumber": 1
        },
        "name": {
            "dataType": "string",
            "fieldNumber": 2
        }
    }
}

stake

This event has name = EVENT_NAME_AMOUNT_STAKED. This event is emitted during the processing of each stake included in a stake transaction.

Topics
  • senderAddress: the address of the account submitting the stake transaction.
  • validatorAddress: the address of the account of the validator the user staked with.
Data
stakeDataSchema = {
    "type": "object",
    "required" = [
        "senderAddress",
        "validatorAddress",
        "amount",
        "result"
    ],
    "properties": {
        "senderAddress": {
            "dataType": "bytes",
            "length": ADDRESS_LENGTH,
            "fieldNumber": 1
        },
        "validatorAddress": {
            "dataType": "bytes",
            "length": ADDRESS_LENGTH,
            "fieldNumber": 2
        },
        "amount": {
            "dataType": "sint64",
            "fieldNumber": 3
        },
        "result": {
            "dataType": "uint32",
            "fieldNumber": 4
        }
    }
}

validatorPunished

This event has name = EVENT_NAME_VALIDATOR_PUNISHED. This event is emitted when a validator gets punished.

Topics
  • validatorAddress: the address of the account of the punished validator.
Data
validatorPunishedDataSchema = {
    "type": "object",
    "required" = [
        "address"
    ],
    "properties": {
        "address": {
            "dataType": "bytes",
            "length": ADDRESS_LENGTH,
            "fieldNumber": 1
        }
    }
}

validatorBanned

This event has name = EVENT_NAME_VALIDATOR_BANNED. This event is emitted when a validator gets banned.

Topics
  • validatorAddress: the address of the account of the banned validator.
Data
validatorPunishedDataSchema = {
    "type": "object",
    "required" = [
        "address"
    ],
    "properties": {
        "address": {
            "dataType": "bytes",
            "length": ADDRESS_LENGTH,
            "fieldNumber": 1
        }
    }
}

Internal Functions

Round Number and End of Rounds

All blocks (with the exception of the genesis block) are part of a round. The first block after the genesis block is the first block of the first round, and so on. The round length, i.e. the number of blocks in a round, is specified in a configuration file and is denoted ROUND_LENGTH. In the block lifecycle, it will be useful to compute the round number to which a block belongs and if the block is the last block of its round. For this, we will use the following functions:

getRoundNumberFromHeight

This function returns the round number to which the input height belongs.

def getRoundNumberFromHeight(h: uint32) -> uint32:
    return ceiling(h - genesisDataStore.height, ROUND_LENGTH)

Here, ceiling is an internal function defined below.

isEndOfRound

This function returns a boolean indicating if the input height is at the end of a round or not.

def isEndOfRound(h: uint32) -> bool:
    if (h - genesisDataStore.height) % ROUND_LENGTH == 0:
        return True
    else:
        return False

ceiling

Returns the ceiling of the division between two positive integers.

def ceiling(x: int,y: int) -> int:
    if y == 0:
        raise Exception('Can not divide by 0.')
    return (x+y-1)//y

getValidatorWeight

This function returns the weight of a given validator (specified by the address).

def getValidatorWeight(address: Address) -> uint64:
    return min(validatorStore(address).selfStake * FACTOR_SELF_STAKING,
               validatorStore(address).totalStake)

shuffleValidatorsList

A function to reorder the list of validators as specified in LIP 0003.

Parameters

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

  • validatorList: An array of objects with of type ValidatorObject.
  • randomSeed: A SEED_LENGTH-byte value representing a random seed.
Returns

This function returns the input list shuffled using the value of randomSeed.

Execution
def shuffleValidatorsList(validatorList: list[ValidatorObject], randomSeed: bytes) -> list[ValidatorObject]:
    # Checking pairwise distinct property.
    if validatorList != set(validatorList):
        raise Exception('Validators list invalid (duplicate values detected)')

    roundHash = {}
    for item in validatorList:
        roundHash[item.address] = SHA256(randomSeed + item.address) # Hashing concatenation of randomSeed and address.

    # Reorder the validator list.
    shuffledValidatorList = sort validatorList where item1 < item2 if (roundHash(item1.address) < roundHash(item2.address))
                                 or ((roundHash[item1.address] == roundHash[item1.address]) and item1.address < item2.address)         

    return shuffledValidatorList

updateValidatorEligibility

A function that updates the eligible validators substore to account for new changes of validator properties.

Parameters

The function has the following input parameters:

  • address: the address of a validator.
  • oldWeight: the weight of the validator before the eligibility update.
Returns

This function does not return.

Execution
def updateValidatorEligibility(address: Address, oldWeight: uint64) -> None:
    # Always start by removing the old entry from the eligible validators substore.
    oldKey = oldWeight.to_bytes(8,'big') + address
    if the eligible validator substore contains an entry for key = oldKey:
        remove this entry from the store

    # If the validator is eligible, add an entry to the eligible validators substore.
    weight = getValidatorWeight(address)
    if (weight >= MIN_WEIGHT and validatorStore(address).isBanned == False):

        newKey = weight.to_bytes(8,'big') + address
        lastReportMisbehaviorHeight = validatorStore(validatorAddress).reportMisbehaviorHeights[-1] if len(validatorStore(validatorAddress).reportMisbehaviorHeights) else 0
        create an entry in the eligible validators substore with
            storeKey = newKey,
            storeValue = encode(eligibleValidatorsStoreSchema, {lastReportMisbehaviorHeight})

verifyBlockSignature

Checks whether a block header is validly signed.

Execution
def verifyBlockSignature(header: BlockHeader) -> bool:
    generatorKey = Validators.getValidatorKeys(header.generatorAddress).generatorKey
    signature = block.header.signature

    # Remove the signature from the block header.
    delete header.signature
    # Serialize the block header without signature.
    serializedUnsignedBlockHeader = encode(unsignedBlockHeaderSchema, header)

    return verifyEd25519(generatorKey, "LSK_BH_", OWN_CHAIN_ID, serializedUnsignedBlockHeader, signature)

Here, the function verifyEd25519 verifies the validity of a signature as specified in LIP 0062.

isValidatorNameValid

Checks whether a given string would be a valid validator name.

Execution
def isValidatorNameValid(validatorName: str) -> bool:
    # Name should contain only lower case letters, numbers and symbols `!@$&_.`
    # and should be at least 1 character long and at most MAX_LENGTH_NAME characters long.
    if (not(all(c.isdigit() or c.islower() or c in ['!','@','$','&','_','.'] for c in validatorName))
        or len(validatorName) < 1
        or len(validatorName) > MAX_LENGTH_NAME):

        return False

    return True
isPunished

This function returns a boolean indicating if a validator is punished at a certain height of not.

def isPunished(address: Address, height: uint32) -> bool:
    if validatorStore(address).reportMisbehaviorHeights is empty:
        return False

    let lastReportMisbehaviorHeight be the last element of validatorStore(address).reportMisbehaviorHeights

    if height < lastReportMisbehaviorHeight + PUNISHMENT_WINDOW_SELF_STAKING:
        return True

    return False

getActiveValidators

This function computes the set of active validators and their BFT weight based on the input set of eligible validators.

def getActiveValidators(roundNumber: uint32) -> list[ValidatorObject]:
    initRounds = genesisDataStore.initRounds
    initValidators = genesisDataStore.initValidators
    eligibleValidators = snapshotStore(roundNumber)

    # During the first NUMBER_ACTIVE_VALIDATORS rounds after the bootstrap period
    # the initial validators are only partly replaced by elected validators.
    # During this phase, there are no selected standby validators.
    if roundNumber < initRounds + NUMBER_ACTIVE_VALIDATORS:
        nbrInitValidators = initRounds + NUMBER_ACTIVE_VALIDATORS - roundNumber
        nbrElectedValidators = NUMBER_ACTIVE_VALIDATORS - nbrInitValidators

        # Recall that eligibleValidators is sorted by validator weight.
        electedValidatorAddresses = [item.address for item in eligibleValidators[:nbrElectedValidators]]
        chosenInitValidators = [address for address in initValidators if address not in electedValidatorAddresses][:nbrInitValidators]   

        # Determine active validators and their BFT weight.
        activeValidators = [{"address": item.address, "bftWeight": ceiling(item.weight, WEIGHT_SCALE_FACTOR) } for item in eligibleValidators[:nbrElectedValidators]]
        if len(activeValidators) > 0:
            # average BFT weight can not be 0, since MIN_WEIGHT >= 1, therefore BFT weight of elected validators is at least 1. 
            averageElectedValidatorBftWeight = sum([item.bftWeight for item in activeValidators]) // len(activeValidators)
            activeValidators+= [{"address": item, "bftWeight": averageElectedValidatorBftWeight} for item in chosenInitValidators]
        else:
            activeValidators+= [{"address": item, "bftWeight": 1} for item in chosenInitValidators]
            return activeValidators
    else:
        # If eligibleValidators contains less than NUMBER_ACTIVE_VALIDATORS entries
        # there will be less than NUMBER_ACTIVE_VALIDATORS active validators.
        # Recall that eligibleValidators is sorted by validator weight.
        activeValidators = [{"address": item.address, "bftWeight": ceiling(item.weight, WEIGHT_SCALE_FACTOR) } for item in eligibleValidators[:NUMBER_ACTIVE_VALIDATORS]]

    # Apply capping in weights if necessary.
    if len(activeValidators) >= ceiling(10000, MAX_BFT_WEIGHT_CAP):
        activeValidators = capWeights(activeValidators, MAX_BFT_WEIGHT_CAP)

    return activeValidators

getSelectedStandbyValidators

This function selects the standby validators for a round and returns them together with their BFT weight.

def getSelectedStandbyValidators(standbyValidators: list[EligibleValidatorObject], height: uint32) -> list[ValidatorObject]:
    # We now compute the randomness used for selecting the first standby validator.
    randomSeed1 = random.getRandomBytes(
        height +1 - (ROUND_LENGTH*3)//2,
        ROUND_LENGTH
    )

    if NUMBER_STANDBY_VALIDATORS == 2 and len(standbyValidators) >=2:
        randomSeed2 = random.getRandomBytes(
            height +1 - 2*ROUND_LENGTH,
            ROUND_LENGTH
        )
        selectedStandbyValidators = select 2 address from standbyValidators
                                   as specified in LIP 0022, using the seeds randomSeed1 and randomSeed2
    elif NUMBER_STANDBY_VALIDATORS >= 1 and len(standbyValidators) >= 1:
        selectedStandbyValidators = select 1 address from standbyValidators
                                   as specified in LIP 0022, using the seed randomSeed1
    else: # No standby validators.
        selectedStandbyValidators = empty

    return [{"address": item.address, "bftWeight": 0} for item in selectedStandbyValidators]

capWeights

This function caps the values of a sorted array to make sure that no element has value more than a certain percentage of the total value.

Parameters
  • validatorList: An array of objects of type ValidatorObject array of integers, sorted in decreasing order of bftWeight.
  • capValue: Specifies the percentage in which the values are capped. Takes values from 0 - 10000 corresponding to double decimal precision integers which can be obtained by dividing it by 100, i.e., 1000 means 10%.
def capWeights(validatorList: list[ValidatorObject], capValue: uint32) -> list[ValidatorObject]:
    if sorted(validatorList, reverse = True, key = lambda y:y.bftWeight) != validatorList: # List should be ordered in decreasing order of weight.
        raise Exception('List is not sorted in decreasing order.')
    if capValue == 0 or capValue >= 10000:
        raise Exception('Invalid value for capping.')

    maxNumCappedElements = ceiling(10000, capValue) - 1
    if len(validatorList) <= maxNumCappedElements:
        raise Exception('List size not enough to apply capping with specified value.')

    partialSum = 0
    for i in range(maxNumCappedElements + 1, len(validatorList)):
        partialSum += validatorList[i].bftWeight

    for i in range(maxNumCappedElements, 0, -1):
       partialSum += validatorList[i].bftWeight
       cappedWeightRemainingElements = (capValue * partialSum) // (10000 - (capValue * i))
       if cappedWeightRemainingElements < validatorList[i-1].bftWeight:
           for k in range(i):
              validatorList[k].bftWeight = cappedWeightRemainingElements
           break

    return validatorList

The idea of the function above is that for the validatorList with elements in descending order of bftWeight, then you want the following equation to hold for element i:

x <= (capValue / 10000) ( i * x + sum( [validatorList[k].bftWeight for k in range(i,len(validatorList))])

Here x is the BFT weight at which the elements validatorList[0].bftWeight, ..., validatorList[i-1].bftWeight are capped. Solving the above equation for x yields the value of cappedWeightRemainingElements in the code. Note that if cappedWeightRemainingElements > validatorList[i-1].bftWeight, then you do not need to cap validatorList[i-1].bftWeight and can reduce the index i by 1.

computeThresholds

This function computes the precommit and certificate thresholds that are forwarded to the consensus layer, see LIP 0058 for details.

def computeThresholds(validatorList: list[ValidatorObject]) -> Tuple[uint64, uint64]:
    aggregateBFTWeight = 0
    for validator in validatorList:
        aggregateBFTWeight += validator.bftWeight

    precommitThreshold = (2 * aggregateBFTWeight) // 3 + 1
    certificateThreshold = precommitThreshold

    return precommitThreshold, certificateThreshold

Protocol Logic for Other Modules

More functions might be made available during implementation.

getNumberActiveValidators

def getNumberActiveValidators()-> uint32:
    return NUMBER_ACTIVE_VALIDATORS

getRoundLength

def getRoundLength()-> uint32:
    return ROUND_LENGTH

getStaker

Returns the stored information relative to the given address.

def getStaker(address: Address)-> StakerStoreObject:
    return stakerStore(address)

getValidator

Returns the stored information relative to the given address.

def getValidator(address: Address)-> ValidatorStoreObject:
    return validatorStore(address)

unbanValidator

Removes the banning for a validator, given that the reason of banning was not due to misbehaviors. For example, this applies in cases the validator got banned due to inactivity or not registering valid BLS keys.

def unbanValidator(address: Address)-> None:
    if there does not exist an entry in validator substore with key == address:
        raise Exception('No registered validator with the specified address.')
    if  validatorStore(address).isBanned == False:
        raise Exception('The specified validator is not banned.')
    if len(validatorStore(address).reportMisbehaviorHeights) >= REPORT_MISBEHAVIOR_LIMIT_BANNED:
        raise Exception('Validator exceeded the maximum allowed number of misbehaviors and is permanently banned.')

    validatorStore(address).isBanned = False
    updateValidatorEligibility(address,0)

Endpoints for Off-Chain Services

This section specifies the non-trivial or recommended endpoints of the PoS module and does not include all endpoints.

isNameAvailable

Asserts the availability of a given name for validator registration.

def isNameAvailable(name: str) -> bool:
    if (not isValidatorNameValid(name))
        or (nameStore(name) exists):
        return False
    else:
        return True

getPoSTokenID

def getPoSTokenID() -> TokenID:
    return TOKEN_ID_POS

getStaker

Same as the getStaker function of the previous section.

getValidator

Same as the getValidator function of the previous section.

getAllValidators

Returns information of all validators.

Execution
def getAllValidators()-> list[ValidatorStoreObject]:
    return [decode(validatorStoreSchema,validatorStore(address)) for validatorStore(address) in validator substore]

getLockedStakedAmount

Returns the total amount locked due to participation in staking (active stakes and pending unlocks) for the given address.

def getLockedStakedAmount(address: Address) -> uint64:
    lockedStakedAmount = 0
    for item in stakerStore(address).stakes:
        lockedStakedAmount+= item.amount
    for item in stakerStore(address).pendingUnlocks:
        lockedStakedAmount+= item.amount

    return lockedStakedAmount

getPendingUnlocks

Returns the list of pending unlocks for the given address.

Execution
def getPendingUnlocks(address: Address)-> UnlockObject:
    return stakerStore(address).pendingUnlocks

Genesis Block Processing

The following steps are executed as part of the genesis block processing, see LIP 0060 for details.

Genesis Assets Schema

genesisPoSStoreSchema = {
    "type": "object",
    "required": ["validators", "stakers", "genesisData"],
    "properties": {
        "validators": {
            "type": "array",
            "fieldNumber": 1,
            "items": {
                "type": "object",
                "required": [
                    "address",
                    "name",
                    "blsKey",
                    "proofOfPossession",
                    "generatorKey",
                    "lastGeneratedHeight",
                    "isBanned",
                    "reportMisbehaviorHeights",
                    "consecutiveMissedBlocks",
                    "commission",
                    "lastCommissionIncreaseHeight",
                    "sharingCoefficients"
                ],
                "properties": {
                    "address": {
                        "dataType": "bytes",
                        "length": ADDRESS_LENGTH,
                        "fieldNumber": 1
                    },
                    "name": {
                        "dataType": "string",
                        "fieldNumber": 2
                    },
                    "blsKey": {
                        "dataType": "bytes",
                        "length" : BLS_PUBLIC_KEY_LENGTH,
                        "fieldNumber": 3
                    },
                    "proofOfPossession": {
                        "dataType": "bytes",
                        "length" : BLS_POP_LENGTH,
                        "fieldNumber": 4
                    },
                    "generatorKey": {
                        "dataType": "bytes",
                        "length": ED25519_PUBLIC_KEY_LENGTH,
                        "fieldNumber": 5
                    },
                    "lastGeneratedHeight": {
                        "dataType": "uint32",
                        "fieldNumber": 6
                    },
                    "isBanned": {
                        "dataType": "boolean",
                        "fieldNumber": 7
                    },
                    "reportMisbehaviorHeights": {
                        "type": "array",
                        "fieldNumber": 8,
                        "items": { "dataType": "uint32" }
                    },
                    "consecutiveMissedBlocks": {
                        "dataType": "uint32",
                        "fieldNumber": 9
                    },
                    "commission": {
                        "dataType": "uint32",
                        "fieldNumber": 10
                    },
                    "lastCommissionIncreaseHeight": {
                        "dataType": "uint32",
                        "fieldNumber": 11
                    },
                    "sharingCoefficients": {
                        "type": "array",
                        "fieldNumber": 12,
                        "items":{
                            "type": "object",
                            "required": ["tokenID", "coefficient"],
                            "properties": {
                                "tokenID": {
                                    "dataType": "bytes",
                                    "length": TOKEN_ID_LENGTH,
                                    "fieldNumber": 1
                                },
                                "coefficient":{
                                    "dataType": "bytes",
                                    "maxLength": MAX_NUM_BYTES_Q96,
                                    "fieldNumber": 2
                                }
                            }
                        }
                    }                        
                }
            }
        },
        "stakers": {
            "type": "array",
            "fieldNumber": 2,
            "items": {
                "type": "object",
                "required": ["address", "stakes", "pendingUnlocks"],
                "properties": {
                    "address": {
                        "dataType": "bytes",
                        "fieldNumber": 1
                    },
                    "stakes": {
                        "type": "array",
                        "fieldNumber": 2,
                        "items": {
                            "type": "object",
                            "required": ["validatorAddress", "amount", "sharingCoefficients"],
                            "properties": {
                                "validatorAddress": {
                                    "dataType": "bytes",
                                    "length": ADDRESS_LENGTH,
                                    "fieldNumber": 1
                                },
                                "amount": {
                                    "dataType": "uint64",
                                    "fieldNumber": 2
                                },
                                "sharingCoefficients": {
                                    "type": "array",
                                    "fieldNumber": 3,
                                    "items":{
                                        "type": "object",
                                        "required": ["tokenID", "coefficient"],
                                        "properties": {
                                            "tokenID": {
                                                "dataType": "bytes",
                                                "length": TOKEN_ID_LENGTH,
                                                "fieldNumber": 1
                                            },
                                            "coefficient":{
                                                "dataType": "bytes",
                                                "maxLength": MAX_NUM_BYTES_Q96,
                                                "fieldNumber": 2
                                            }
                                        }
                                    }
                                }                                
                            }
                        }
                    },
                    "pendingUnlocks": {
                        "type": "array",
                        "fieldNumber": 3,
                        "items": {
                            "type": "object",
                            "required": [
                                "validatorAddress",
                                "amount",
                                "unstakeHeight"
                            ],
                            "properties": {
                                "validatorAddress": {
                                    "dataType": "bytes",
                                    "length": ADDRESS_LENGTH,
                                    "fieldNumber": 1
                                },
                                "amount": {
                                    "dataType": "uint64",
                                    "fieldNumber": 2
                                },
                                "unstakeHeight": {
                                    "dataType": "uint32",
                                    "fieldNumber": 3
                                }
                            }
                        }
                    }
                }
            }
        },
        "genesisData": {
            "type": "object",
            "fieldNumber": 3,
            "required": ["initRounds", "initValidators"],
            "properties": {
                "initRounds": {
                    "dataType": "uint32",
                    "fieldNumber": 1
                },
                "initValidators": {
                    "type": "array",
                    "fieldNumber": 2,
                    "items": {
                        "dataType": "bytes",
                        "length": ADDRESS_LENGTH
                    }
                }
            }
        }
    }
}

Genesis State Initialization

During the genesis state initialization stage, the following steps are executed. If any step fails, the block is discarded and has no further effect.

Let genesisBlockAssetBytes be the data bytes included in the block assets for the PoS module and let genesisBlockAssetObject be the deserialization of genesisBlockAssetBytes according to the genesisPoSStoreSchema schema, given above.

  • Initial checks on the properties of genesisBlockAssetObject:

    • genesisBlockAssetObject should satisfy the genesisPoSStoreSchema schema.
    • Across elements of the validators array, all address values must be unique, all name values must also be unique.
    • For all elements of the validators array, name values must satisfy isValidatorNameValid(name) == True.
    • For all elements of the validators array, commission values must satisfy commission <= 10000.
    • For all elements of the validators array, lastCommissionIncreaseHeight values must satisfy lastCommissionIncreaseHeight <= block header height of the genesis block.
    • For all elements of the validators array, the sharingCoefficients array must be ordered in lexicographic order of tokenID.
    • Across elements of the stakers array, all address values must be unique.
    • For all elements of the stakers array:
      • Either stakes != [] or pendingUnlocks != [].
      • All amounts properties in elements of the stakes or the pendingUnlocks arrays must be non-zero.
      • Across elements of the stakes array, all validatorAddress values must be unique.
      • For each element sentStake in the stakes array, there is an element validator in the validators array with validator.address == sentStake.validatorAddress. Moreover, for each item in sentStake.sharingCoefficients array, there must be an entry elem in the validator.sharingCoefficients array such that item.tokenID == elem.tokenID and item.coefficient <= elem.coefficient.
      • For each element sentStake in the stakes array, the sharingCoefficients must be ordered in lexicographic order of tokenID.
      • For each element sentStake in the stakes array, for each item in sharingCoefficients array must be ordered in lexicographic order of tokenID.
      • stakes has size is at most MAX_NUMBER_STAKING_SLOTS.
      • stakes must be in lexicographic order of validatorAddress.
      • pendingUnlocks has size is at most MAX_NUMBER_PENDING_UNLOCKS.
      • pendingUnlocks must be ordered by lexicographical order of validatorAddress, ties then broken by increasing amount, ties finally broken by increasing unstakeHeight.
      • For each element pendingUnlock in the pendingUnlocks array, there is an element validator in the validators array with validator.address == pendingUnlock.validatorAddress.
      • For each element pendingUnlock in the pendingUnlocks array, unstakeHeight value must satisfy unstakeHeight <block header height of the genesis block.
    • All values of the genesisData.initValidators array must be unique and must be equal to validator.address for a validator element of validators.
    • The genesisData.initValidators array must have length equal to NUMBER_ACTIVE_VALIDATORS and be in lexicographical order.
    • genesisData.initRounds must be at least MIN_INIT_ROUNDS.
  • For each entry validator in genesisBlockAssetObject.validators, create an entry in the validator substore with:

    totalStake = 0
    for staker in genesisBlockAssetObject.stakers:
        for sentStake in staker.stakes:
            if sentStake.validatorAddress == validator.address:
                totalStake += sentStake.amount
                if staker.address == validator.address:
                    selfStake = sentStake.amount
    
    validatorState = {
        "name": validator.name,
        "totalStake": totalStake,
        "selfStake": selfStake,
        "lastGeneratedHeight": validator.lastGeneratedHeight,
        "isBanned": validator.isBanned,
        "reportMisbehaviorHeights": validator.reportMisbehaviorHeights,
        "consecutiveMissedBlocks": validator.consecutiveMissedBlocks,
        "commission" : validator.commission,
        "lastCommissionIncreaseHeight": validator.lastCommissionIncreaseHeight,
        "sharingCoefficients": validator.sharingCoefficients  
    }
    storeKey = validator.address
    storeValue = encode(validatorStoreSchema, validatorState)

    Further, for every entry validator in genesisBlockAssetObject.validators, also create an entry in the name substore with:

    validatorState = {
        "validatorAddress": validator.address
    }
    
    storeKey = validator.name utf8-encoded
    storeValue = encode(nameStoreSchema, validatorState)
  • For each entry staker in genesisBlockAssetObject.stakers, create an entry in the staker substore with:

    stakerState = {
        "stakes": staker.stakes,
        "pendingUnlocks": staker.pendingUnlocks
    }
    storeKey = staker.address
    storeValue =  encode(stakerStoreSchema, stakerState)
  • Create an entry in the genesis data substore with:

    genesisState = {
        "height": block header height of the genesis block,
        "initRounds": genesisBlockAssetObject.genesisData.initRounds,
        "initValidators": genesisBlockAssetObject.genesisData.initValidators
    }
    
    storeKey = EMPTY_BYTES
    storeValue = encode(genesisDataStoreSchema, genesisState)
  • Create an entry in the previous timestamp substore with:

    timestampState = {
        "timestamp": block header height of the genesis block
    }  
    storeKey = EMPTY_BYTES
    storeValue = encode(previousTimestampStoreSchema, timestampState)

Genesis State Finalization

To finalize the state of the genesis block the following logic is executed. If any step fails, the block is discarded and has no further effect.

As in the previous point, let genesisBlockAssetBytes be the data bytes included in the block assets for the PoS module and let genesisBlockAssetObject be the deserialization of genesisBlockAssetBytes according to the genesisPoSStoreSchema schema, given above. Moreover, let b be the genesis block. Then:

# Register all validators in the Validators module.
# For the snapshot block for the migration from Lisk Core 3 to Lisk Core 4 on
# Lisk mainnet, apply a special rule. This must be done as validators in
# this snapshot block do not have a BLS key.
if the chain is the mainchain and INVALID_BLS_KEYS_IN_GENESIS_BLOCK == True:
    for validator in genesisBlockAssetObject.validators:
        Validators.registerValidatorWithoutBLSKey(
            validator.address,
            validator.generatorKey
        )

else:
# For any other genesis block, register validators with a BLS key.
    for validator in genesisBlockAssetObject.validators:
        Validators.registerValidatorKeys(
            validator.address,
            validator.proofOfPossession,
            validator.generatorKey,
            validator.blsKey
        )
        updateValidatorEligibility(validator.address, 0)

# Check that all stakes and pendingUnlocks correspond to locked tokens.
for address a key of the staker substore:
    if there exists an entry in validator substore with key == address:
        if Token.getLockedAmount(address, MODULE_NAME_POS, TOKEN_ID_POS) < getLockedStakedAmount(address):
            raise Exception('Locked amount is incompatible with specified stakes.')
    else:
        # For non-validators, the only reason for locking is staking, so the values should match precisely.
        if Token.getLockedAmount(address, MODULE_NAME_POS, TOKEN_ID_POS) != getLockedStakedAmount(address):
            raise Exception('Locked values do not match.')

# Set the initial validators.
# Recall that initValidator is always in lexicographical order.
validatorList = [
    {"address": address, "bftWeight": 1}
    for address in genesisBlockAssetObject.genesisData.initValidators
]

# Compute the thresholds for the BFT consensus protocol.
precommitThreshold, certificateThreshold = computeThresholds(validatorList)

Validators.setValidatorParams(precommitThreshold, certificateThreshold, validatorList)

Block Processing

The following steps are executed as part of the (non-genesis) block processing, see LIP 0055 for details.

After Transactions Execution

After the transactions in a block b are executed, the properties related to missed blocks are updated according to Validator Productivity. This logic is recapitulated below:

def afterTransactionsExecute(b: Block) -> None:
    height = b.header.height
    
    updateProductivity(b.header)

    # if not last block of round no further updates are needed.
    if isEndOfRound(height) == False:
        return

    # Block b is an end-of-round block. Need to update snapshot and select validators for next round.
    # This must be done after the properties related to missed blocks are updated.
    updateSnapshotSubstore(height)

    nextRound = getRoundNumberFromHeight(height+1)

    # No updates in validators and BFT weights are performed during bootstrap period.
    if nextRound <= genesisDataStore.initRounds:
        return
    
    # Validator selection for next round.
    # Calculate the active validators for next round based on the snapshot taken two rounds ago.
    validatorList = getActiveValidators(nextRound)

    # Select standby validators if bootstrap and hybrid periods have passed.
    if nextRound >= initRounds + NUMBER_ACTIVE_VALIDATORS:
        standbyValidators = [validator for validator in snapshotStore(nextRound)
                            if validator["address"] not in validatorList]

        selectedStandbyValidators = getSelectedStandbyValidators(standbyValidators, height)
        # Add selected standby validators to validators.
        validatorList += selectedStandbyValidators

    # If there are no eligible validators, validatorList is empty.
    # In this case, the BFT parameters and validator list are not updated.
    if validatorList is empty:
        return

    shuffleAndSetValidators(validatorList, height)

We now define the functions updateProductivity, updateSnapshotSubstore and shuffleAndSetValidators.

def updateProductivity(b.header: BlockHeader) -> None:
    height = b.header.height

    # previousTimestamp is the value in the previous timestamp substore.
    missedBlocks = Validators.getGeneratorsBetweenTimestamps(previousTimestamp, b.header.timestamp)

    for address in missedBlocks:
        validatorStore(address).consecutiveMissedBlocks += missedBlocks[address]

        # The below rule was introduced in LIP 0023.
        if (validatorStore(address).consecutiveMissedBlocks > FAIL_SAFE_MISSED_BLOCKS
            and height - validatorStore(address).lastGeneratedHeight > FAIL_SAFE_INACTIVE_WINDOW):
            validatorStore(address).isBanned = True
            updateValidatorEligibility(address,getValidatorWeight(address))

    validatorStore(b.header.generatorAddress).consecutiveMissedBlocks = 0
    validatorStore(b.header.generatorAddress).lastGeneratedHeight = height

    # Update previousTimestamp substore.
    previousTimestamp = b.header.timestamp
def updateSnapshotSubstore(height: uint32) -> None:
    nextRound = getRoundNumberFromHeight(height+1)
    snapshotRound = nextRound + 2

    # no need to take a snapshot for the first initRounds
    if snapshotRound <= genesisDataStore.initRounds:
        return

    # Punished validators are excluded from the snapshot.
    eligibleValidators = [
        {"address": key[-ADDRESS_LENGTH:], "weight": int.from_bytes(key[:-ADDRESS_LENGTH], 'big')}
        for key a substore key of the eligible validators substore if
        height >=  eligibleValidatorsStore(key) + PUNISHMENT_WINDOW_SELF_STAKING or
        eligibleValidatorsStore(key) == 0
    ] ordered by weight, ties broken by reverse lexicographical ordering of address
    # Notice that the keys in the substore naturally have the right ordering
    # when being read from the end to the beginning of the store.

    snapshotState = {
            "validatorWeightSnapshot": eligibleValidators
        }
    create an entry in the snapshot substore with
        storeKey = snapshotRound.to_bytes(4,'big'),
        storeValue = encode(snapshotStoreSchema, snapshotState)

    delete any entries from the snapshot substore snapshotStore(x) for x < nextRound
def shuffleAndSetValidators(validatorList: ValidatorObject, height: uint32) -> None: 
    # Compute the thresholds for the BFT consensus protocol.
    precommitThreshold, certificateThreshold = computeThresholds(validatorList)
    
    # Random seed to shuffle validators.
    randomSeed = random.getRandomBytes(
        height +1 - (ROUND_LENGTH*3)//2,
        ROUND_LENGTH
    )
    shuffledValidatorList = shuffleValidatorsList(validatorList, randomSeed)

    Validators.setValidatorParams(precommitThreshold, certificateThreshold, shuffledValidatorList)

Backwards Compatibility

This LIP defines a new store interface for the PoS module, 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.

Reference Implementation

Update DPoS terminologies to PoS #7803