Hello everyone,
I would like to propose another LIP for the roadmap objective “Introduce alternative validator selection mechanism for sidechains”. This LIP specifies the Proof of Authority mechanism in particular.
I’m looking forward to your feedback.
Here is the complete LIP draft:
LIP:
Title: Introduce PoA module
Author: Iker Alustiza <iker@lightcurve.io>
Type: Standards Track
Created: <YYYY-MM-DD>
Updated: <YYYY-MM-DD>
Requires: 0038, 0040, Introduce validators module, Introduce a BFT module
Abstract
This LIP introduces the Lisk Proof-of-Authority (PoA) mechanism for the selection of validators, known as authorities in this context, to generate blocks.
In particular, this document specifies the PoA module with its module store structure and the stored key-value pairs. Furthermore, it specifies 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.
Copyright
This LIP is licensed under the Creative Commons Zero 1.0 Universal.
Motivation
In Proof-of-Authority (PoA) blockchains only a pre-defined set of validators, called the authorities, can propose blocks and they are selected based on off-chain information such as their reputation or identity.
It trades the decentralization of the network (arbitrarily selected authorities) for efficiency and performance.
This mechanism was first proposed by Gavin Wood in 2015.
A PoA blockchain is especially attractive for small projects or blockchain applications where the project owners are expected to run the network nodes. Due to the simplicity of its validator selection algorithm, it is also suitable for applications where a high transaction per second throughput is important. That is why a self-contained PoA module seems to be a very useful feature to be added as one of the modules available for sidechain developers in the Lisk SDK.
Rationale
This LIP specifies the PoA module which defines a complete Proof-of-Authority blockchain.
Sidechain developers creating a sidechain with the Lisk SDK will have the out-of-the-box choice between this module or the DPoS module as the mechanism for validator selection in their sidechain.
As mentioned, the Lisk PoA module only sets the mechanism for the selection of the validators, which implies that the underlying algorithm to reach consensus for blocks of the chain is assumed to be given by the Lisk-BFT consensus algorithm.
The PoA module also assumes the same round system as currently specified for the Lisk Mainchain.
That is, the assignment of block forging slots is done in batches of consecutive blocks called rounds.
Typically, PoA systems do not define any reward system.
However, sidechain developers may choose to have a reward system in the chain native token to incentivize the authorities.
In this case, the rewards module in the Lisk SDK can be used to define block rewards for PoA blockchains in the same way as for DPoS blockchains.
Moreover, the banning mechanism (as defined in LIP 0023) and the punishment of BFT violations (as defined in LIP 0024 for the Lisk-BFT protocol) are not necessary for a functional PoA blockchain.
Hence, in this LIP they are not included in the specifications.
Updating the Set of Authorities
The current active authorities, i.e., those authorities eligible to forge blocks and participate in the Lisk-BFT consensus, are stored in the store of the PoA module together with their associated weight. It further contains a threshold property. The weights and threshold are used in the Lisk-BFT consensus algorithm and for the validity of the update authority command.
This command is specific to the PoA module and allows to update the mentioned parameters.
In particular, the update authority command allows PoA chains to increase (or decrease) the number of active authorities, to change their associated weight and the threshold.
This is a particularly interesting feature for blockchain applications that start with a small set of validators and nodes in the network (for example, the sidechain developers themselves).
With the success and maturity of the application, there may be an interest in opening the project to a bigger and more decentralized set of participants.
The command is only valid if a threshold of active authorities approve it by adding their signature (to be aggregated) to the command parameters.
This command can set a maximum of 199 active authorities which is the maximum number of active validators in any chain built with the Lisk SDK.
Migration from PoA to DPoS
As mentioned before, the sidechain developers using the SDK may specify their blockchain application to be deployed on a PoA or DPoS chain (assuming they do not develop a custom mechanism).
Thus, a sidechain will be either a PoA or a DPoS blockchain and both modules cannot co-exist in the same chain.
However, there may be an interest for some projects that started as a PoA chain to migrate to DPoS.
If this is the case, the developers and the future network validators have two choices:
- After launching the project, if there is a need for a more decentralized approach: Hard-fork the chain to include the DPoS module instead of PoA. This can be easened by following a snapshot mechanism similar to the one specified in LIP 0035.
- If during the development phase, it is decided that the application should start on a PoA chain and then run on a DPoS chain for the long term: The sidechain developers can define an arbitrarily long bootstrapping period for the DPoS chain in the genesis block as explained in LIP 0034. This bootstrapping period effectively mimics a PoA chain where there is a fixed set of validators given by the public keys in the
initDelegates
property of the block header asset. This will allow it to first have a preparatory phase of the application so it can mature sufficiently before transferring to a DPoS chain.
Specification
In this section, we specify the PoA module with its module store structure and the stored key-value pairs. Furthermore, we specify the state transition 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. The PoA module has module ID MODULE_ID_POA
(see the table below).
Constants and Notation
Name | Type | Value |
---|---|---|
MODULE_ID_POA |
uint32 | TBD |
STORE_PREFIX_VALIDATOR |
bytes | 0x8000 |
STORE_PREFIX_NAME |
bytes | 0xc0000 |
STORE_PREFIX_SNAPSHOT |
bytes | 0xe000 |
STORE_PREFIX_CHAIN |
bytes | 0x0000 |
COMMAND_ID_REGISTRATION_AUTHORITY |
uint32 | 0 |
COMMAND_ID_UPDATE_KEY |
uint32 | 1 |
COMMAND_ID_UPDATE_AUTHORITY |
uint32 | 2 |
MAX_LENGTH_NAME |
uint32 | 20 |
MAX_NUM_VALIDATORS |
uint32 | 199 |
MAX_UINT64 |
uint64 | 18446744073709551615 |
MESSAGE_TAG_POA |
bytes | ASCII encoded string “LSK_POA_” |
uint32be Function
uint32be(x)
returns the big endian uint32 serialization of an integer x
, with 0 <= x < 2^32
. This serialization is always 4 bytes long.
PoA Module Store
The key-value pairs in the module store are organized as in the following Figure 1.
Figure 1: The PoA module store is organized in four substores, one for the validator address, one for the names, one for the validators snapshots and the fourth to store the general chain properties.
Validator Substore
The validator names of the registered authorities are stored as distinct key-value entries in the PoA module store.
Store Prefix, Store Keys, and Store Values
- The store prefix is set to
STORE_PREFIX_VALIDATOR
. - Each substore key is a 20-byte value
address
, whereaddress
is the address of the user account registered as validator, either in the genesis block or with an authority registration command. - Each substore value is the serialization of an object following the JSON schema
validatorObjectSchema
defined below.
JSON Schema
validatorObjectSchema = {
"type": "object",
"properties": {
"name": {
"dataType": "string",
"fieldNumber": 1
}
},
"required": [
"name"
]
}
Properties and Default Values
name
is a string representing the validator name, with a minimum length of 1 character and a maximum length of MAX_LENGTH_NAME
characters. Its value is set in the genesis block or with an authority registration command
Name Substore
The name substore is an auxiliary store used to validate the authority registration command.
Store Prefix, Store Keys, and Store Values
- The store prefix is set to
STORE_PREFIX_NAME
. - Each substore key is a name of a validator as given in the genesis block or with an authority registration command, serialized as a utf-8 encoded string.
- Each substore value is set to the address of the corresponding validator, serialized according to the JSON schema
validatorAddressSchema
below.
JSON Schema
validatorAddressSchema = {
"type": "object",
"properties": {
"address": {
"dataType": "bytes",
"fieldNumber": 1
}
},
"required": [
"address"
]
}
Properties and Default Values
address
is a 20-byte array with the address of the user account registered as validator in the genesis block or with an authority registration command.
Snapshot Substore
This substore contains the snapshot of the active authorities for the current round, next round and in two rounds.
Store Prefix, Store Key, and Store Value
- The store prefix is set to
STORE_PREFIX_SNAPSHOT
. - Each store key is
uint32be(roundNumber)
, whereroundNumber
can be 0, 1 or 2 corresponding to the current round, the next round and in two rounds respectively. - Each store value is the serialization of an object following
snapshotStoreSchema
. - Notation: Let
snapshotStore(roundNumber)
be the store entry with prefix STORE_PREFIX_SNAPSHOT and keyuint32be(roundNumber)
.
JSON Schema
snapshotStoreSchema = {
"type": "object",
"properties": {
"addresses": {
"type": "array",
"fieldNumber": 1,
"items": {
"dataType": "bytes",
},
}
"weights": {
"type": "array",
"fieldNumber": 2,
"items": {
"dataType": "uint64",
},
}
"threshold": {
"dataType": "uint64",
"fieldNumber": 3,
},
},
"required": ["address", "weights", "threshold"]
}
Properties and Default Values
The properties of this schema are as follows:
-
addresses
: An array of pairwise distinct 20-byte addresses in lexicographical order.
It specifies the set of active validators in the chain.
Its initial value is set in the genesis block. -
weights
: An array of positive integers of the same size as thevalidatorsCurrentRound.addresses
property where each element is the weight of the corresponding validator invalidatorsCurrentRound.addresses
.
Its initial value is set in the genesis block. -
threshold
: An integer stating the weight threshold for finality in the BFT consensus protocol.
Its initial value is set in the genesis block.
Chain Properties Substore
This substore contains the general properties of the chain.
Store Prefix, Store Key, and Store Value
- The store prefix is set to
STORE_PREFIX_CHAIN
. - The store key is set to empty bytes.
- The store value is set to the serialization of an object following
chainPropSchema
below. - Notation: Let
chainProperties
be the entry in the chain properties substore.
JSON Schema
chainPropSchema = {
"type": "object",
"properties": {
"roundEndHeight": {
"dataType": "uint32",
"fieldNumber": 1
},
"validatorsUpdateNonce": {
"dataType": "uint32",
"fieldNumber": 2
},
}
"required": [
"roundEndHeight",
"validatorsUpdateNonce"
]
}
Properties and Default Values
The properties of this schema are as follows:
-
roundEndHeight
: An integer stating the last height of the round.
Its initial value is set after the execution of the genesis block. -
validatorsUpdateNonce
: An integer representing the number of times that the validator set has been updated with an update auhtority command.
It is initialized to 0.
Commands
Authority Registration Command
This command is equivalent to the delegate registration command in the DPoS module and has the same schema and similar validity rules.
The command ID of this transaction is COMMAND_ID_REGISTRATION_AUTHORITY
.
Parameters
registrationTransactionParamsSchema = {
"type": "object",
"properties": {
"name": {
"dataType": "string",
"fieldNumber": 1
},
"blsKey": {
"dataType": "bytes",
"fieldNumber": 2
},
"proofOfPossession": {
"dataType": "bytes",
"fieldNumber": 3
},
"generatorKey": {
"dataType": "bytes",
"fieldNumber": 4
}
},
"required": ["name", "blsKey", "proofOfPossession", "generatorKey"]
}
Verification
Let trs
be a transaction with module ID MODULE_ID_POA
and command ID COMMAND_ID_REGISTRATION_AUTHORITY
to be verified.
The list of verification conditions for trs.params
is as follows:
- The
trs.params.name
property has to contain only characters from the set[a-z0-9!@$&_.]
, must not be empty and has to be at mostMAX_LENGTH_NAME
characters long. - Let
address
be the 20-byte address derived fromtrs.senderPublicKey
. Thenaddress
must not already be registered as a validator. This is, the validator substore has no entry with store keyaddress
. - The value of
trs.params.name
must not already be registered as a validator name. This is, the name substore has no entry with store keytrs.params.name
. -
isKeyRegistered(trs.params.blsKey)
must returntrue
, where the function is defined in the validators module. -
PopVerify(trs.params.blsKey, trs.params.proofOfPossession)
must returnVALID
, wherePopVerify
is part of the BLS signature scheme. -
tx.params.generatorKey
must have length 32.
Execution
Let trs
be a transaction with module ID MODULE_ID_POA
and command ID COMMAND_ID_REGISTRATION_AUTHORITY
to be executed.
Then trs.params
implies the following execution logic:
- Create an entry in the validator substore as:
-
storeKey
:address
, whereaddress
is the address of the sender oftrs
. -
storeValue
: The serialization of the objectvalidatorObject
followingvalidatorObjectSchema
withvalidatorObject.name = trs.params.name
.
-
- Create an entry in the name substore as:
-
storeKey
:trs.params.name
serialized as a utf-8 encoded string. -
storeValue
: The serialization of the objectvalidatorAddress
followingvalidatorAddressSchema
withvalidatorAddress.address = address
whereaddress
is the address of the sender oftrs
.
-
- Call
registerValidatorKeys(address, trs.params.proofOfPossession, trs.params.generatorKey, trs.params.blsKey)
, whereaddress
is the 20-byte address derived fromtrs.senderPublicKey
.
The functionregisterValidatorKeys
is defined in the validators module.
Update Generator Key Command
This command is used to update the generator key (from the validators module) for a specific authority.
The command ID of this transaction is COMMAND_ID_UPDATE_KEY
.
Parameters
updateGeneratorKeyParamsSchema = {
"type": "object",
"properties": {
"generatorKey": {
"dataType": "bytes",
"fieldNumber": 1
}
},
"required": ["generatorKey"]
}
Verification
Let trs
be a transaction with module ID MODULE_ID_POA
and command ID COMMAND_ID_UPDATE_KEY
to be executed.
The list of verification conditions for trs.params
is as follows:
- Let
address
be the 20-byte address derived fromtrs.senderPublicKey
.
Then the validators substore must have an entry for the store keyaddress
. -
trs.params.generatorKey
must have length 32.
Execution
Let trs
be a transaction with module ID MODULE_ID_POA
and command ID COMMAND_ID_UPDATE_KEY
to be executed.
Then trs.params
implies the following execution logic:
Let address
be the 20-byte address derived from trs.senderPublicKey
.
Then, call setValidatorGeneratorKey(address, trs.params.generatorKey)
, where setValidatorGeneratorKey
is the function exposed by the validators module.
Update Authority Command
The command ID for this command is COMMAND_ID_UPDATE_AUTHORITY
.
Parameters
updateValidatorParams = {
"type": "object",
"properties": {
"validatorAddresses": {
"type": "array",
"items": {
"dataType": "bytes"
},
"fieldNumber": 1
},
"weights": {
"type": "array",
"items": {
"dataType": "uint64"
},
"fieldNumber": 2
},
"threshold": {
"dataType": "uint64",
"fieldNumber": 3
},
"validatorsUpdateNonce": {
"dataType": "uint32",
"fieldNumber": 4
},
"signature": {
"dataType": "bytes",
"fieldNumber": 5
},
"aggregationBits": {
"dataType": "bytes",
"fieldNumber": 6
}
},
"required": [
"validatorAddresses",
"weights",
"threshold",
"validatorsUpdateNonce",
"signature",
"aggregationBits"
]
}
Verification
Let trs
be a transaction with module ID MODULE_ID_POA
and command ID COMMAND_ID_UPDATE_AUTHORITY
to be verified.
The list of verification conditions for trs.params
is as follows:
-
Rules for
trs.params.validatorAddresses
array:- The array must have at least 1 element and at most
MAX_NUM_VALIDATORS
elements. - The array must be ordered lexicographically.
- Each element is a unique 20-byte address.
- For every element
address
in thetrs.params.validatorAddresses
array, there is an entry withstoreKey == address
in the validator substore.
- The array must have at least 1 element and at most
-
Rules for
trs.params.weights
array:- The array must have the same number of elements as
trs.params.validatorAddresses
. - Each element is a positive integer.
- Let
totalWeight
be the sum of every element in thetrs.params.weights
array. ThentotalWeight
has to be less than or equal toMAX_UINT64
.
Note that the elements in this array correspond one to one to the elements in the
trs.params.validatorAddresses
array in the same order. - The array must have the same number of elements as
-
Rules for
trs.params.threshold
property:- The value of
trs.params.threshold
is within the following range:- Minimum value: ⌊ ⅓ ×
totalWeight
⌋+ 1 - Maximum value:
totalWeight
- Minimum value: ⌊ ⅓ ×
where ⌊⋅⌋ is the floor function.
- The value of
-
Rules for
trs.params.validatorsUpdateNonce
property:- The value of
trs.params.validatorsUpdateNonce
has to be equal tochainProperties.validatorsUpdateNonce
.
- The value of
-
Rules for
trs.params.aggregationBits
andtrs.params.signature
properties:-
The function
verifyWeightedAggSig(keyList, tag, netID, aggregationBits, signature, m, weights, threshold)
, specified in LIP 0038, must return VALID where:- The
keyList
property is an array containinggetValidatorAccount(address).blsKey
for every address insnapshotStore(0).addresses
sorted in the same order, wheregetValidatorAccount
is the function exposed by the validators module. - The
tag
is equal toMESSAGE_TAG_POA
. - The
netID
byte array corresponds to the network ID of the chain. - The
aggregationBits
argument is the byte array given intrs.params.aggregationBits
. - The
signature
argument is the aggregate signature given intrs.params.signature
. - The
m
argument is the output bytes of the serialization, as specified in LIP 0027, oftrs.params.validatorAddresses
,trs.params.weights
,trs.params.validatorsUpdateNonce
, andtrs.params.threshold
properties according to the following schema:
validatorSignatureMessage = { "type": "object", "properties": { "validatorAddresses": { "type": "array", "items": { "dataType": "bytes", }, "fieldNumber": 1 }, "weights": { "type": "array", "items": { "dataType": "uint64", }, "fieldNumber": 2 }, "threshold": { "dataType": "uint64", "fieldNumber": 3 }, "validatorsUpdateNonce": { "dataType": "uint32", "fieldNumber": 4 }, }, "required": [ "validatorAddresses", "weights", "threshold", "validatorsUpdateNonce" ] }
- The
weights
argument is set tosnapshotStore(0).weights
. - The
threshold
argument is set tosnapshotStore(0).threshold
.
- The
-
Execution
Let trs
be a transaction with module ID MODULE_ID_POA
and command ID COMMAND_ID_UPDATE_AUTHORITY
to be processed.
Then processing trs
has the following effect:
- The array
snapshotStore(2).addresses
is set totrs.params.validatorAddresses
. - The array
snapshotStore(2).weights
is set totrs.params.weights
. - The property
snapshotStore(2).threshold
is set totrs.params.threshold
. - The property
chainProperties.validatorsUpdateNonce
is set totrs.params.validatorsUpdateNonce + 1
.
Internal Function
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:
-
validatorsList
: An array of pairwise distinct 20-byte addresses. -
randomSeed
: A 32-byte value representing a random seed.
Returns
This function returns an array of bytes with the re-ordered list of addresses.
Execution
The shuffling algorithm is defined in LIP 0003.
Protocol Logic During Block Lifecycle
After Genesis Block Execution
After the genesis block g
is executed, the following logic is executed:
chainProperties.roundEndHeight = g.header.height + len(snapshotStore(0).addresses)
# Pass the required chain properties to the BFT votes module
BFTThreshold = snapshotStore(0).threshold
BFTvalidators = []
for i in range(len(snapshotStore(0).addresses)):
validatorObject = {address: snapshotStore(0).addresses[i],
weight: snapshotStore(0).weights[i]}
BFTvalidators.append(validatorObject)
setBFTParameters(BFTThreshold, BFTThreshold, BFTvalidators)
# Pass the list of validators to the validators module
setGeneratorList(snapshotStore(0).addresses)
where:
-
setBFTParameters
is a function exposed by the BFT votes module. -
setGeneratorList
is a function exposed by the validators module.
After Block Execution
After a block b
is executed, the following logic is executed:
if b.header.height == chainProperties.roundEndHeight
# Pass the required chain properties to the BFT votes module
BFTThreshold = snapshotStore(1).threshold
BFTvalidators = []
for i in range(len(snapshotStore(1).addresses)):
validatorObject = {address: snapshotStore(1).addresses[i],
weight: snapshotStore(1).weights[i]}
BFTvalidators.append(validatorObject)
setBFTParameters(BFTThreshold, BFTThreshold, BFTvalidators)
# Reshuffle the list of validators and pass it to the validators module
roundStartHeight = chainProperties.roundEndHeight - len(snapshotStore(0).addresses) + 1
randomSeed = getRandomBytes(roundStartHeight, len(snapshotStore(0).addresses))
nextValidatorAddresses = shuffleValidatorsList(snapshotStore(1).addresses, randomSeed)
setGeneratorList(nextValidatorAddresses)
# Update the chain information for the next round
snapshotStore(0) = snapshotStore(1)
snapshotStore(1) = snapshotStore(2)
chainProperties.roundEndHeight = chainProperties.roundEndHeight + len(snapshotStore(1).addresses)
where:
-
getRandomBytes
is a function exposed by the random module.
Backwards Compatibility
This LIP introduces a new module for sidechains in the Lisk ecosystem.
As such it does not affect any existing chain, hence it does not imply any incompatibilities.