LIP: 0061
Title: Introduce certificate generation mechanism
Author: Jan Hackfeld <[email protected]>
Discussions-To: https://research.lisk.com/t/introduce-certificate-generation-mechanism/296
Status: Active
Type: Standards Track
Created: 2021-05-22
Updated: 2024-01-04
Requires: 0044, 0055, 0058
This LIP defines the schema for certificates, how unsigned certificates can be computed from blocks and how they are signed using BLS signatures. We further specify commit messages, which are messages containing BLS signatures of certificates. We describe how validators create unsigned certificates for blocks they consider final and share the signatures of such certificates by gossiping commit messages via the P2P network. The certificate signatures shared via commit messages are subsequently aggregated and included in blocks. From the aggregate certificate signatures included in blocks, any node in the blockchain network can create signed certificates which can be used in cross-chain update transactions to facilitate interoperability.
This LIP is licensed under the Creative Commons Zero 1.0 Universal.
The interoperability solution for the Lisk ecosystem, which is based on the paradigm of cross-chain certification, requires the consensus algorithm to generate certificates which can be used in cross-chain update transactions. The certificates should attest all information required for the cross-chain update, including the state root, which allows authenticating cross-chain messages, and the validators hash, which authenticates changes of the validators and therefore signers of future certificates.
In this section, we explain the main aspects and design choices for the specifications.
In this section, we define the main terms used throughout this LIP.
- active validator: A validator that can propose blocks and contribute to block finality by casting consensus votes for blocks. Active validators are defined per round and may change from round to round.
- BFT weight: The weight with which a validator contributes to finalizing blocks and signing certificates.
- single commit: A message containing a BLS signature of a certificate, which corresponds to a block in the current chain. The single commit message signals that the signing validator considers the corresponding block final.
- aggregate commit: A message containing an aggregate BLS signature of a certificate, which corresponds to a block in the current chain. It attests that all signing validators consider the corresponding block final.
- certificate: An object that authenticates the relevant information for cross-chain communication. It uniquely corresponds to a block header of a chain and contains a subset of properties of that block header. The aggregate BLS signature provided in a certificate attests the finality of the block. Single commits and aggregate commits are messages for communicating certificate signatures.
Certificates are the key object for transferring information about the state of one chain to another chain. Every certificate is derived from a finalized block and contains the minimal subset of the block header properties required for interoperability. In this section, we explain why each of the certificate properties is required. For context, see LIP 0053 on how the properties are used in the validation and processing of cross-chain update transactions.
blockID
: This property uniquely identifies which block a certificate is derived from.height
: This property is used to check that certificates are submitted sequentially, i.e., in increasing order of height, when included in cross-chain updates.timestamp
: This property is used to check the liveness requirement as part of the cross-chain update validation. The liveness check prevents that certificates that are older than 28 days can be submitted. For this, the timestamp of the certificate is compared with the timestamp of the current block and cross-chain updates with certificates older than 28 days are rejected.stateRoot
: The state root is used to authenticate cross-chain messages.validatorsHash
: The validators hash is used to authenticate changes of the validators and the threshold required for certificate signatures.aggregationBits
andsignature
: The value ofaggregationBits
is a bitmap defining which validators signed the certificate and the signature property contains the corresponding aggregate BLS signature. These properties are used to validate that indeed the required threshold of validators signed the certificate.
Single commits are objects used to share BLS signatures of certificates via the P2P layer. Instead of including single commits on-chain as part of a block, we propose to share them off-chain via the P2P layer for the following main reasons:
- Including non-aggregated BLS signatures by every validator for every block would significantly increase the block size.
- Validating additional BLS signatures included in blocks slows down the block processing.
- The single commits can be broadcast directly as soon as a block is considered final, allowing for faster sharing of certificate signatures as they do not first have to be included in a block.
Note that it is always possible to generate certificates only from on-chain data (block headers) and the BLS signatures shared as part of the single commit messages are not required for it. This means that data availability is not an issue for this solution.
It further is important that the protocol provides enough incentives for validators to create single commits and to share them via the P2P layer. To ensure such incentives for a PoS blockchain, we propose to add an additional condition for unlocking stakes in LIP 0059. For validators and users to be able to unlock their tokens used for staking, this LIP will require that certificates are regularly created and therefore a sufficient threshold of active validators has to regularly create single commits so these can be aggregated for the certificate. For a blockchain using Proof-of-Authority, the validators have their reputation at stake and we believe that this is sufficient incentive for them to participate in the certificate creation, which is required for interoperability.
The goal of the P2P gossip mechanism for single commits is to ensure that the validators have the necessary data to generate aggregate commits which satisfy the chain of trust (see section below) as quickly and reliably as possible. In particular, we want to share single commits as fast as possible while being robust in case of adverse network conditions, such as a temporary network split, a longer time without finalized blocks or a significant number of validators not sharing single commits. The design decisions and choice of parameters explained in the list below are based on this overarching goal.
- For the
COMMIT_RANGE_STORED+1
most recent blocks of height at mostgetBFTHeights().maxHeightPrecommitted
(getBFTHeights
is defined in LIP 0058) we store and gossip all single commits, while for older blocks we only store single commits for blocks for which thevalidatorsHash
property is different from that of its parent block. The reason is that certificates or aggregate commits are only required to be generated for blocks that authenticate a change of BFT parameters, see Section "Chain of Trust" for details. For more recent blocks we want to create and gossip single commits as soon as possible so we store and gossip them for all blocks. - For blocks of height at most
getMaxRemovalHeight()
we do not need to store any single commits as they will never be required again to create an aggregate commit. This accounts for the fact that blocks may be reverted up to the finalized height and for non-finalized blocks an aggregate commit may need to be re-created. - We gossip single commits every
BLOCK_TIME/2
seconds. This means single commits are shared at a higher frequency than blocks so that ideally after a block is finalized, an aggregate commit for that block is included in the chain only one or two blocks later. - An aggregate commit requires single commits by a certain subset of the active validators, depending on the value of the certificate threshold described in the next section. One P2P message can contain up to
2*numActiveValidators
single commits, wherenumActiveValidators
is the number of active validators at that height. Such a message can therefore contain enough single commits to create two aggregate commits. Together with the gossiping frequency ofBLOCK_TIME/2
seconds this allows to share single commits four times faster than blocks are created, allowing for a significant buffer. - In normal operation, aggregate commits for a block will be generated and included in the chain shortly after that block is finalized.
This implies that the value of
getBFTHeights().maxHeightCertified
will be only a bit smaller thangetBFTHeights().maxHeightPrecommitted
. In particular, the difference between the two values will be at mostCOMMIT_RANGE_STORED
. In that case, single commits are only gossiped once and those of largest height are gossiped first so that aggregate commits for the last finalized block can be created as soon as possible. On the other hand, if the difference betweengetBFTHeights().maxHeightCertified
andgetBFTHeights().maxHeightPrecommitted
is larger thanCOMMIT_RANGE_STORED
, aggregate commits have not been generated for a significant number of blocks. In this case, single commits of smaller height are prioritized so that the aggregate commits can catch up to the current finalized height.
The commit messages can be viewed as a third round of BFT consensus votes for blocks, after prevotes and precommits. For proceeding from the prevote to the precommit phase for a block, the sum of BFT weights of validators prevoting for the block has to be at least a certain value called prevote threshold. Similarly, for proceeding to the commit phase for a block, the phase introduced by this LIP where validators generate commit messages, the sum of BFT weights of validators precommitting for the block or a descendant of it has to be at least a certain value called precommit threshold, see the LIP 0056 for details. Note that if the sum of BFT weights is at least the precommit threshold, then the respective block is considered final and will not be reverted.
During the commit phase an additional threshold is used, called certificate threshold. It is used in the following parts of the protocol:
- Commit messages are aggregated to aggregate commits, which are then included in blocks. The sum of BFT weights of the validators signing an aggregate commit has to be at least the certificate threshold value. Here the validators are those that are active at the height of the certificate and the BFT weights are the corresponding weights at that height.
- When submitting a certificate via a cross-chain update transaction, the weights of all signers have to be above the certificate threshold value that is currently known to the other chain, see LIP 0053 for details.
The certificate threshold is stored as part of the BFT store in the consensus domain, see LIP 0058 for details. The value of this parameter is set by the PoS or PoA module via the setValidatorParams
function of the Validators module and then forwarded to the consensus domain. Both for Lisk PoS and Lisk PoA, we propose to use the same values for the certificate threshold as for the precommit threshold. This means that the same threshold is required for finality as for cross-chain certification.
In general, for a height h
, the certificate threshold could be chosen within the following range:
floor(1/3*aggregateBFTWeight(h))+1 <= certificateThreshold(h) <= aggregateBFTWeight(h),
where aggregateBFTWeight(h)
is the sum of BFT weights at height h
and certificateThreshold(h)
is the certificate threshold at height h
. This allows for a sidechain to choose different trade-offs between satisfying certification liveness (generating certificates with enough signatures) and state transition validity (certificates attest a valid sidechain history):
- A sidechain prioritizing certification liveness can choose a certificate threshold of
floor(1/3* aggregateBFTWeight(h))+1
. This ensures that as long asfloor(2/3*aggregateBFTWeight(h))+1
of the sidechain validators in terms of BFT weight are always honest, no invalid state transition will be certified. On the other hand, more than half of the sidechain validators could go permanently offline (and be replaced by other validators in a Lisk PoS chain, for instance) without endangering certification liveness. - A sidechain prioritizing state transition validity may even choose a threshold higher than the precommit threshold if they view the danger of invalid state transitions being certified as higher than contradicting blocks to be finalized. Such a chain may accept that the connection to the Lisk Mainchain is terminated in case of a certification liveness failure and would have to be re-established afterwards.
Aggregate commits are created from single commits by aggregating the BLS signatures. The aggregate commits are then added to blocks in order to ensure that certificates can be obtained from on-chain data and they also enable adding the additional unlocking condition for PoS blockchains. Aggregate commits are basically the same as certificates with the properties blockID
, timestamp
, stateRoot
and validatorsHash
removed as these properties are already included in previous blocks and are thus redundant.
We say that the chain of trust property is satisfied for a sequence of certificates c1, ..., ck with c1.height < c2.height < ... < ck.height, if the following holds for any two consecutive certificates ci and ci+1 for i in {1, β¦, k-1}: If v1,...,vn are the validators authenticated by ci, w1,...,wn are the associated BFT weights and t is the threshold authenticated by ci, then the aggregate signature of ci+1 must be valid with respect to the validators v1,...,vn with BFT weights w1,...,wn and threshold t.
Let a1, ..., ak be the sequence of all aggregate commits included in a blockchain. Every aggregate commit ai uniquely corresponds to one certificate ci. We then call c1, ..., ck the certificates generated by the blockchain. We further say that the blockchain satisfies the chain of trust property, if the sequence of certificates c1, ..., ck satisfies the chain of trust property.
Intuitively, the chain of trust property means that for any validator change a subset of the previous validators with total BFT weights above the given threshold value has to sign a certificate. For example, the chain of trust would be broken if all validators could change without signing a certificate authenticating this change, as certificates by the new validators would then not be accepted by other chains as these are not aware of the change. An example of a sequence of three certificates satisfying the chain of trust is shown in Figure 1. If in that example Certificate 3, which authenticates the transition from the validator set A, B, E, F back to A, B, C, D, is never generated, then the chain of trust property does not hold. In particular, if Certificate 2 is submitted to the Lisk Mainchain, then no further certificates could be submitted to the Lisk Mainchain.
Figure 1: Example of a sequence of three certificates satisfying the chain of trust.
For maintaining interoperability via certification, it is therefore crucial that the chain of trust is always maintained. Every block and also any certificate derived from a block has a validatorsHash
property. This property is computed from the BLS keys of the active validators, their BFT weights and the certificate threshold after the block is applied, see the function computeValidatorsHash
in LIP 0058 for details. As the active validators are the same within one round, only blocks which are the last block of a round may have a different validatorsHash
property than their parent block. This means that the chain of trust property is satisfied if the certificate generation mechanism ensures that a certificate is generated for all blocks for which the validatorsHash
property is distinct from the validatorsHash
property of the parent block. This way there is a certificate authenticating any validator transition that happened in the chain.
The certificate generation specified in this LIP therefore generates an aggregate commit for any block at height h
for which existBFTParameters(h+1)
returns True
, where existBFTParameters
is defined in LIP 0058. This means that at height h+1
the BFT store contains new and possibly different BFT parameters that need to be authenticated by the block at height h
. As already mentioned, from the on-chain data, i.e., the aggregate commit and referenced block header, the corresponding certificates can be computed. Hence, the mechanism specified in this LIP guarantees that the generated certificates satisfy the chain of trust property.
Note that if possible, also aggregate commits for blocks that are not at the end of the round are created and added to blocks so that a certificate is created as soon as a block is finalized and it is not required to wait for a change of BFT parameters.
During the bootstrap period for Lisk Core v4, as defined in LIP 63, the 101 fixed initial validators will not have valid registered BLS keys from the beginning.
Hence, the certificate generation process is not guaranteed to work until sufficiently many validators have registered their BLS keys.
This is the reason why we introduce the constant MIN_CERTIFICATE_HEIGHT
and only start the certificate generation in Lisk Core v4 with an offset.
For Lisk Core v4, we set MIN_CERTIFICATE_HEIGHT
to the last block of the hybrid phase (see LIP 63) for the following reasons:
- At height
MIN_CERTIFICATE_HEIGHT
there are 100 validators selected by Proof-of-Stake (assuming there are at least 100 validators with registered BLS key and at leastMIN_WEIGHT
staked, see the PoS module) and 1 initial validator active. Only the initial validator may have an invalid BLS keys so an aggregate commit reaching the required threshold can be generated for heightMIN_CERTIFICATE_HEIGHT
. - This block is an end-of-round block and the PoS module sets new BFT parameters for height
MIN_CERTIFICATE_HEIGHT + 1
in the After Transaction Execution hook. This ensures thatexistBFTParameters(MIN_CERTIFICATE_HEIGHT + 1) == True
and therefore single commits for heightMIN_CERTIFICATE_HEIGHT
are created, pass validation and are not cleaned up until an aggregate commit at heightMIN_CERTIFICATE_HEIGHT
is added to the chain.
For any other blockchain created with the Lisk SDK, all validators should have valid BLS keys from the beginning. Therefore, MIN_CERTIFICATE_HEIGHT
can be set to genesisHeight + 1
, where genesisHeight
is the height of the genesis block of the chain. This means the certificate generation can happen directly from the first block proposed by the validators of the chain.
In this LIP, we will frequently use the following functions specified in LIP 0058 without reference to the function definition:
Name | Value | Description |
---|---|---|
BLOCK_TIME |
configurable per chain, default: 10 seconds |
Length of a block slot. |
MESSAGE_TAG_CERTIFICATE |
"LSK_CE_" |
Message tag prepended when signing a certificate object, see LIP 0037. |
COMMIT_RANGE_STORED |
100 | The commit messages at heights {getBFTHeights().maxHeightPrecommitted - COMMIT_RANGE_STORED, ..., getBFTHeights().maxHeightPrecommitted} are always stored. For smaller heights only the single commits for blocks authenticating a change of BFT parameters are stored. |
EMPTY_BYTES |
"" | The empty byte string. |
ADDRESS_LENGTH |
20 | Length in bytes of a Lisk address. |
BLS_SIGNATURE_LENGTH |
96 | Length in bytes of the BLS signatures used according to LIP 0038. |
HASH_LENGTH |
32 | Length in bytes of outputs of the SHA-256 hash function. |
MAX_NUM_VALIDATORS |
199 | The maximum number of validators that can be registered, see LIP 0045. |
MAX_LENGTH_AGGREGATION_BITS |
(MAX_NUM_VALIDATORS + 8 - 1) // 8 |
The maximum length of aggregationBits . |
MIN_CERTIFICATE_HEIGHT |
configurable per chain; for sidechains built with Lisk SDK, set to genesisHeight + 1 where genesisHeight is the height of the genesis block. For Lisk Core v4, set to the height of the last block before all active validators are selected by the PoS module based on their stake (this block needs to be a block at the end of a round). |
The smallest height for which a certificate can be produced. |
Name | Type | Validation | Description |
---|---|---|---|
AggregateCommit |
object | Must follow aggregateCommitSchema schema. |
An object representing an aggregate commit. |
BlockHeader |
object | Must follow blockHeaderSchema schema defined in LIP 0055. |
An object representing a block header. |
Certificate |
object | Must follow certificateSchema schema. |
An object representing a certificate. |
UnsignedCertificate |
object | Must follow unsignedCertificateSchema schema. |
An object representing a certificate without aggregationBits and signature property. |
SingleCommit |
object | Must follow singleCommitSchema schema. |
An object representing a single commit. |
Validator |
object | Must follow validatorSchema schema defined in LIP 0058. |
An object representing a validator with properties address , bftWeight , generatorKey and blsKey . |
Certificates are the key object for transferring information about the state of one chain to another chain. Every certificate references a finalized block via the blockID
property and contains a subset of the properties of that block, namely those properties important for interoperability.
As all schema properties need to be required, we distinguish between signed and unsigned certificates.
unsignedCertificateSchema = {
"type": "object",
"required": [
"blockID",
"height",
"timestamp",
"stateRoot",
"validatorsHash"
],
"properties": {
"blockID": {
"dataType": "bytes",
"length": HASH_LENGTH,
"fieldNumber": 1
},
"height": {
"dataType": "uint32",
"fieldNumber": 2
},
"timestamp": {
"dataType": "uint32",
"fieldNumber": 3
},
"stateRoot": {
"dataType": "bytes",
"length": HASH_LENGTH,
"fieldNumber": 4
},
"validatorsHash": {
"dataType": "bytes",
"length": HASH_LENGTH,
"fieldNumber": 5
}
}
}
certificateSchema = {
"type": "object",
"required": [...unsignedCertificateSchema.required, "aggregationBits", "signature"],
"properties": {
...unsignedCertificateSchema.properties,
"aggregationBits": {
"dataType": "bytes",
"maxLength": MAX_LENGTH_AGGREGATION_BITS,
"fieldNumber": 6
},
"signature": {
"dataType": "bytes",
"length": BLS_SIGNATURE_LENGTH,
"fieldNumber": 7
}
}
}
Here, the ...
notation, borrowed from JavaScript ES6 data destructuring, indicates that the corresponding part of the schema should be inserted in place, and it is just used for notational convenience.
A certificate is always created from a finalized block in the current chain. An unsigned certificate is computed from a block header blockHeader
in the following canonical way:
def computeUnsignedCertificateFromBlockHeader(blockHeader: BlockHeader) -> UnsignedCertificate:
unsignedCertificate = object following unsignedCertificateSchema
unsignedCertificate.blockID = block ID of blockHeader
unsignedCertificate.height = blockHeader.height
unsignedCertificate.timestamp = blockHeader.timestamp
unsignedCertificate.stateRoot = blockHeader.stateRoot
unsignedCertificate.validatorsHash = blockHeader.validatorsHash
return unsignedCertificate
In this section we describe how a signature of a certificate is computed and how single and aggregate signatures of certificates are validated. The functions signBLS()
, verifyBLS()
and verifyWeightedAggSig()
are as defined in the LIP 0062.
The following function computes a certificate signature.
sk
is the BLS secret key for signing,chainID
is the chain ID of the chain that the certificate corresponds to,unsignedCertificate
is an unsigned certificate object.
The certificate signature as byte array.
def signCertificate(sk: bytes, chainID: bytes, unsignedCertificate: UnsignedCertificate) -> bytes:
message = encode(unsignedCertificateSchema, unsignedCertificate)
tag = MESSAGE_TAG_CERTIFICATE
return signBLS(sk, tag, chainID, message)
The following function verifies that the BLS signature provided as input is a valid signature of the certificate with respect to the given public key.
pk
is the BLS public key used for validating the signature,sig
is the BLS signature,chainID
is the chain ID of the chain that the certificate corresponds to,unsignedCertificate
is an unsigned certificate object.
The functions returns True
if and only if the certificate signature is valid with respect to the provided BLS public key, signature and chain identifier.
def verifySingleCertificateSignature(pk: bytes, sig: bytes, chainID: bytes, unsignedCertificate: UnsignedCertificate) -> bool:
message = encode(unsignedCertificateSchema, unsignedCertificate)
tag = MESSAGE_TAG_CERTIFICATE
return verifyBLS(pk, tag, chainID, message, sig)
The following function verifies the aggregate BLS signature which is provided as part of the certificate object.
validatorList
is an array of objects of typeValidator
corresponding to the validators eligible to sign the certificate,threshold
is the required threshold value for the signatures,chainID
is the chain ID of the chain that the certificate corresponds to,certificate
is a certificate object.
The functions returns True
if and only if the aggregate certificate signature is valid with respect to the provided BLS public keys, weights, threshold and chain identifier.
def verifyAggregateCertificateSignature(validatorList: list[Validator], threshold: uint64, chainID: bytes, certificate: Certificate) -> bool:
sort validatorList lexicographically by blsKey property
keysList = [validator.blsKey for validator in validatorList]
bftWeights = [validator.bftWeight for validator in validatorList]
unsignedCertificate = UnsignedCertificate object derived from certificate
message = encode(unsignedCertificateSchema, unsignedCertificate)
tag = MESSAGE_TAG_CERTIFICATE
return verifyWeightedAggSig(keysList, certificate.aggregationBits, certificate.signature, tag, chainID, bftWeights, threshold, message)
If for validator v
a block b
or a descendant of it obtains precommits of at least the precommit threshold value, then validator v
creates and gossips a commit message if the validator is active at height b.header.height
. This commit message contains a certificate signature by validator v
of the certificate computed from the block b
. These commit messages are collected by all nodes, aggregated and then can be added to a block by any block proposer as part of an aggregate commit defined below. In this section, we define the schema, creation and validity for single commit messages and the peer-to-peer gossip mechanism.
singleCommitSchema = {
"type": "object",
"required": [
"blockID",
"height",
"validatorAddress",
"certificateSignature"
],
"properties": {
"blockID": {
"dataType": "bytes",
"length": HASH_LENGTH,
"fieldNumber": 1
},
"height": {
"dataType": "uint32",
"fieldNumber": 2
},
"validatorAddress": {
"dataType": "bytes",
"length": ADDRESS_LENGTH,
"fieldNumber": 3
},
"certificateSignature": {
"dataType": "bytes",
"length": BLS_SIGNATURE_LENGTH,
"fieldNumber": 4
}
}
}
The following function creates a single commit by a validator for a block.
blockHeader
: is a block header object,validatorAddress
: address of the validator,sk
is the BLS secret key of the validator for signing,chainID
is the chain ID of the chain that the block corresponds to.
A single commit object of the block corresponding to blockHeader
signed by the validator with the given BLS secret key sk
and address validatorAddress
.
def createSingleCommit(blockHeader: BlockHeader, validatorAddress: bytes, sk: bytes, chainID: bytes) -> SingleCommit:
m = single commit object following singleCommitSchema
m.blockID = block ID of blockHeader
m.height = b.header.height
m.validatorAddress = validatorAddress
unsignedCertificate = computeUnsignedCertificateFromBlockHeader(blockHeader)
m.certificateSignature = signCertificate(sk, chainID, unsignedCertificate)
return m
To reduce the storage requirements, single commits that are no longer needed are removed. This section provides a function that computes the height up to which single commits can be removed.
The height up to which single commits can be removed.
def getMaxRemovalHeight() -> uint32:
b = block at height maxHeightFinalized
return max(b.header.aggregateCommit.height, MIN_CERTIFICATE_HEIGHT - 1)
In the function above, maxHeightFinalized
is the maximum height up to which the current chain is considered final, as introduced in LIP 0056.
If a validator node starts for the first time or loses all previously created single commits due to a restart, the node should automatically re-create all recent single commits, which may be essential for the network to create aggregate commits. Let h1 = getMaxRemovalHeight()
and h2 = getBFTHeights().maxHeightPrecommitted
. If h2 > h1
, then the single commits are created as described in the next section Creation After Block Processing for these values of h1
and h2
.
If for a validator v
, after applying a block the value returned by the function getBFTHeights().maxHeightPrecommitted
increases from h1
to h2
, then the following commit messages are created by the validator v
and gossiped as described further below:
- For every height
h
in{max(MIN_CERTIFICATE_HEIGHT, h1+1), ..., h2 - 1}
such thatexistBFTParameters(h+1)
returnsTrue
(i.e.,h
is the height of a block authenticating a change of BFT parameters), validatorv
creates a commit message for the blockb
at heighth
in its chain if validatorv
was active at heighth
. - If
h2 >= MIN_CERTIFICATE_HEIGHT
,v
creates a commit message for the blockb
at heighth2
in its chain if validatorv
was active at heighth
.
Every node needs to store single commit messages for the P2P gossip mechanism and to allow for the creation of aggregate commits from single commits. The data structures storing single commits are not part of the blockchain state and can be cleared when restarting the node. For the gossip mechanism it is further important to know which commit messages have been gossiped already. The specifications below therefore assume that single commits are stored in two separate data structures, but in general a different mechanism for classification such as an additional flag can also be used.
We assume that single commits are stored in the following two data structures:
nonGossipedCommits:
This data structure stores newly received or newly created single commit messages until they are gossiped. The commit messages are organized by height.gossipedCommits:
This data structure contains commit messages that have been gossiped already. The commit messages are also organized by height.
Every new incoming single commit message m
is validated as follows:
- Discard
m
if it is already contained innonGossipedCommits
orgossipedCommits
, i.e., if there is a messagem2
innonGossipedCommits
orgossipedCommits
withm2.validatorAddress = m.validatorAddress
andm2.blockID = m.blockID
. - Discard
m
ifm.height <= getMaxRemovalHeight()
. - Discard
m
ifm.height
is not in{getBFTHeights().maxHeightPrecommitted - COMMIT_RANGE_STORED, ..., currentHeight}
andexistBFTParameters(m.height+1)
returnsFalse
(i.e.,m.height
is not the height of a block authenticating a change of BFT parameters). HerecurrentHeight
is the height of the latest block added to the blockchain. - Discard
m
ifm.blockID
is not the ID of the blockb
in the current chain at heightm.height
. - Discard
m
if the validator given bym.validatorAddress
is not active at heightm.height
. The active validators at heightm.height
can be obtained viagetBFTParametersActiveValidators(m.height).validators
. - Let
b
be the block in the current chain at heightm.height
,unsignedCertificate = computeUnsignedCertificateFromBlockHeader(b)
,pk
be the BLS public key of the validator given bym.validatorAddress
(obtained usinggetBFTParametersActiveValidators(m.height).validators
and checking for BLS key in the array entry with the corresponding address) andchainID
be the chain ID of the current chain. Check thatverifySingleCertificateSignature(pk, m.certificateSignature, chainID, unsignedCertificate)
returnsTrue
. - If all validations above pass,
m
is added tononGossipedCommits
. If steps 1 through 4 above pass, but step 5 or 6 fail, the corresponding peer receives a ban score of 100 (see LIP 0004).
Let currentHeight
be the height of the current tip of the chain. Commit messages in nonGossipedCommits
are gossiped every BLOCK_TIME/2
seconds as follows:
- Cleanup the data structures
nonGossipedCommits
andgossipedCommits
:- Remove any single commit message
m
fromnonGossipedCommits
andgossipedCommits
withm.height <= getMaxRemovalHeight()
. - For every commit message
m
innonGossipedCommits
orgossipedCommits
one of the following two conditions has to hold, otherwise it is discarded.- The value of
m.height
is in{getBFTHeights().maxHeightPrecommitted - COMMIT_RANGE_STORED, β¦, currentHeight}
. - The function
existBFTParameters(m.height+1)
returnsTrue
, which means thatm.height
is the height of a block authenticating a change of BFT parameters.
- The value of
- Remove any single commit message
- Let
numActiveValidators
be the length of the array returned bygetBFTParametersActiveValidators(h).validators
. Choose up to2*numActiveValidators
commit messages as follows:- Select any message in
nonGossipedCommits
orgossipedCommits
withm.height < getBFTHeights().maxHeightPrecommitted - COMMIT_RANGE_STORED
choosing messages with smaller height first. - Select all newly created commit messages in
nonGossipedCommits
(created by a validator node itself) choosing the ones with the largest height first. - Select among the received commit messages in
nonGossipedCommits
(created by other nodes) the ones with the largest height first.
- Select any message in
- Gossip an array of up to
2*numActiveValidators
commit messages to 16 randomly chosen connected peers with at least 8 of them being outgoing peers (same parameters as block propagation). - Move any gossiped commit message included in
nonGossipedCommits
togossipedCommits
.
From multiple single commit messages an aggregate commit message can be computed by aggregating the BLS signatures. Subsequently, an aggregate commit can be included in a block header. In this section, we describe the schema of an aggregate commit, how it is computed from single commits, how a block generator chooses an aggregate commit to include in a block and how an aggregate commit is processed as part of the block processing.
aggregateCommitSchema = {
"type": "object",
"required": ["height", "aggregationBits", "certificateSignature"],
"properties": {
"height": {
"dataType": "uint32",
"fieldNumber": 1
},
"aggregationBits": {
"dataType": "bytes",
"fieldNumber": 2
},
"certificateSignature": {
"dataType": "bytes",
"length": BLS_SIGNATURE_LENGTH,
"fieldNumber": 3
}
}
}
In this section, we define two functions that help validators compute an appropriate aggregate commit that can be added to the block. The function selectAggregateCommit
defined below is used in the header initialization phase of the block creation as described in LIP 0058.
The function aggregateSingleCommits
creates an aggregate commit from the provided single commits.
singleCommits
: This is a list of single commit messages, all for the same block. We assume that each single commit passed the validation steps 1 - 7 in the section Single Commit P2P Gossip.
The function then returns an object following the schema aggregateCommitSchema
.
def aggregateSingleCommits(singleCommits: list[SingleCommit]) -> AggregateCommit:
h = m.height for first single commit in singleCommits
validatorList = getBFTParametersActiveValidators(h).validators
addressToBlsKey = {}
for validator in validatorList:
addressToBlsKey[validator.address] = validator.blsKey
validatorKeys = addressToBlsKey.values() sorted lexicographically
pubKeySignaturePairs = []
for m in singleCommits:
pk = addressToBlsKey[m.validatorAddress]
add (pk, m.certificateSignature) to pubKeySignaturePairs
(bitmap, aggSig) = createAggSig(validatorKeys, pubKeySignaturePairs)
aggregateCommit = object following aggregateCommitSchema
aggregateCommit.height = h
aggregateCommit.aggregationBits = bitmap
aggregateCommit.certificateSignature = aggSig
return aggregateCommit
When creating a block, the validator node creating it uses the following function to select an aggregate commit to add to the block.
An object following the schema aggregateCommitSchema
, which can be added the next block.
def selectAggregateCommit() -> AggregateCommit:
try:
# Note that the BFT parameters at height maxHeightCertified+1 are already authenticated by the previous certificate.
heightNextBFTParameters = max(getNextHeightBFTParameters(getBFTHeights().maxHeightCertified+1), MIN_CERTIFICATE_HEIGHT + 1)
nextHeight = min(heightNextBFTParameters -1, getBFTHeights().maxHeightPrecommitted)
except:
# getNextHeightBFTParameters function call returns error, i.e., there are no new bft params since last certificate.
# attempt to create certificates for finalized blocks
nextHeight = getBFTHeights().maxHeightPrecommitted
while nextHeight > max(getBFTHeights().maxHeightCertified, MIN_CERTIFICATE_HEIGHT - 1):
singleCommits = commits in nonGossipedCommits or gossipedCommits with height equal to nextHeight
nextValidators = set of validator addresses appearing in the single commit messages in singleCommits
bftParams = getBFTParametersActiveValidators(nextHeight)
aggregateBFTWeight = 0
for v in nextValidators:
aggregateBFTWeight += BFT weight of object in bftParams.validators with address equal to v
if aggregateBFTWeight >= bftParams.certificationThreshold:
return aggregateSingleCommits(singleCommits)
else:
nextHeight -= 1
# Return default aggregate commit object.
aggregateCommit = object following aggregateCommitSchema
aggregateCommit.height = getBFTHeights().maxHeightCertified
aggregateCommit.aggregationBits = EMPTY_BYTES
aggregateCommit.certificateSignature = EMPTY_BYTES
return aggregateCommit
As described in the "before application processing" stage of the block processing in LIP 0055, the property block header property aggregateCommit
is verified using the function verifyAggregateCommit
defined below. Note that it is important that the validation defined below is performed before the BFT store is updated, i.e., before the "before application processing" logic defined in LIP 0058 is executed.
def verifyAggregateCommit(blockHeader: BlockHeader)-> bool:
aggregateCommit = blockHeader.aggregateCommit
# Check if the aggregate commit object is the default object with empty signature.
if aggregateCommit.aggregationBits == EMPTY_BYTES
and aggregateCommit.certificateSignature == EMPTY_BYTES
and aggregateCommit.height == getBFTHeights().maxHeightCertified:
return True
if aggregateCommit.aggregationBits == EMPTY_BYTES or aggregateCommit.certificateSignature == EMPTY_BYTES:
return False
# The heights of aggregate commits must be strictly increasing.
if aggregateCommit.height <= getBFTHeights().maxHeightCertified:
return False
# Check that the height of the aggregate commit is at most the value of
# maxHeightPrecommitted before processing blockHeader and updating the BFT store.
if aggregateCommit.height > getBFTHeights().maxHeightPrecommitted:
return False
# The heights of aggregate commits must be at least MIN_CERTIFY_HEIGHT.
if aggregateCommit.height < MIN_CERTIFICATE_HEIGHT:
return False
# If there are new BFT parameters for a height h, then the chain needs to include
# an aggregate commit for the block at height h-1. This block and the corresponding
# certificate authenticate the BFT parameters via the validators hash property.
# Note that the BFT parameters at height maxHeightCertified+1 are already authenticated
# by the previous certificate.
try:
heightNextBFTParameters = max(getNextHeightBFTParameters(getBFTHeights().maxHeightCertified+1), MIN_CERTIFICATE_HEIGHT + 1)
if aggregateCommit.height > heightNextBFTParameters-1:
return False
except:
pass
# Check the aggregate signature with respect to the BFT weights
# and certificate threshold.
blockHeader1 = block header of block at height aggregateCommit.height
unsignedCertificate = computeUnsignedCertificateFromBlockHeader(blockHeader1)
certificate = Certificate object with properties set to corresponding property of unsignedCertificate
and aggregationBits, signature set to empty bytes
certificate.aggregationBits = aggregateCommit.aggregationBits
certificate.signature = aggregateCommit.certificateSignature
chainID = chain ID of the current chain
bftParams = getBFTParametersActiveValidators(aggregateCommit.height)
return verifyAggregateCertificateSignature(bftParams.validators, bftParams.certificationThreshold, chainID, certificate)
If the validation above fails, the block is discarded and the peer sending the respective block receives a ban score of 100, see LIP 0004.
This LIP implies a hard fork of the protocol because of the following changes:
- The block processing includes the validation and processing of aggregate commits included in a block.
- An endpoint for gossiping single commit messages is added to the P2P layer.
TBD
For the interoperability solution in Lisk, it is crucial that a node running a blockchain A can provide certificates that can be used in cross-chain updates transactions, which are then submitted to another blockchain B. In particular, given the height lastCertifiedHeight
of the last certificate submitted to blockchain B as input, a node for blockchain A should be able to provide a valid certificate of height larger than lastCertifiedHeight
that will be accepted by blockchain B (if such a certificate exists). In order to minimize the fees paid for submitting cross-chain updates, the new certificate should have the maximum possible height while still maintaining the chain of trust. This means that it should not be necessary to submit intermediate certificates authenticating only minor validator changes, but skip over as many intermediate certificates as possible. This is, in particular, important if the previously submitted certificate at height lastCertifiedHeight
was created days or even weeks ago. Note that due to the liveness requirement, there has to be at least one certificate submitted in any 28 day period, see LIP 0053 for details.
In this section, we describe the following two approaches for nodes to provide certificates:
- Approach 1: This approach computes a suitable certificate (if it exists) from on-chain data from blockchain A , namely the block header data of blockchain A (including the aggregate commits) and historic sets of active validators and certificate threshold (inputs of the validators hash computation). Any node can store the block header data and the historic active validators and certificate threshold when processing the blocks of blockchain A .
- Approach 2: This approach computes a suitable certificates (if it exists) from the on-chain data used in Approach 1 and the single commit messages shared via the P2P network. For this approach, the node should have been participating in the P2P network of blockchain A from the time of the creation of the last certificate submitted to blockchain B onwards and collected all single commits shared via the P2P network. Single commits are off-chain data, not required to be stored and cannot be requested from other nodes via the P2P protocol.
The function computes a certificate object from the aggregate commit given as input and block header data.
aggregateCommit
: An object followingaggregateCommitSchema
with non-emptyaggregationBits
andcertificateSignature
property.
The certificate object corresponding to the aggregate commit object given as input.
def getCertificateFromAggregateCommit(aggregateCommit: AggregateCommit) -> Certificate:
blockHeader = block header at height aggregateCommit.height
unsignedCertificate = computeUnsignedCertificateFromBlockHeader(blockHeader)
certificate = Certificate object with properties set to corresponding property of unsignedCertificate
and aggregationBits, signature set to empty bytes
certificate.aggregationBits = aggregateCommit.aggregationBits
certificate.signature = aggregateCommit.certificateSignature
return certificate
In this section, we describe how to compute the certificate of largest height that can be submitted to a blockchain B given the data from a node running blockchain A. We let lastCertifiedHeight
be the height of the last certificate from blockchain A that has been submitted to blockchain B. For the computation, we require the following data from blockchain A:
- All block headers of blockchain A with height at least
lastCertifiedHeight
. - All aggregate commits included in blockchain A with height at least
lastCertifiedHeight
. For the specifications here, we assume that there is a key-value mapaggregateCommits
storing the aggregate commits included in blockchain A. That isaggregateCommits[h]
for a heighth
is an aggregate commit object with height property equal toh
that was included in blockchain A. There is no key-value entry inaggregateCommits
if the aggregate commit has emptyaggregationBits
andcertificateSignature
property. - The validators with BFT weight and BLS key and the certificate thresholds used for the computation of the
validatorsHash
property of the blocks from heightlastCertifiedHeight
onwards. For the specifications, we assume that there is a key-value storevalidatorsHashPreimage
such that for a validators hashvalidatorsHash
,validatorsHashPreimage[validatorsHash]
is an object followingvalidatorsHashInputSchema
which yields the value ofvalidatorsHash
given as input.
The function getNextCertificateFromAggregateCommits
which computes the certificate from the data described above is specified in the next section.
Using the aggregate commits included in blockchain A, the following function computes the certificate of largest height (if it exits) that can be submitted to another blockchain B.
lastCertifiedHeight
: The height of the last certificate from blockchain A that was submitted to blockchain B.
The function returns a valid certificate object that can be submitted to blockchain B or None
if no certificate of height larger than lastCertifiedHeight
can be obtained from the aggregate commits.
For readability, we describe the logic using two functions, the main function getNextCertificateFromAggregateCommits
and the auxiliary function checkChainOfTrust
.
def getNextCertificateFromAggregateCommits(lastCertifiedHeight: uint32) -> Certificate | None:
lastValidatorsHash = validatorsHash property of block header at height lastCertifiedHeight
lastCertifiedValidators = validatorsHashPreimage[lastValidatorsHash].validators
lastCertificateThreshold = validatorsHashPreimage[lastValidatorsHash].certificateThreshold
blsKeyToBFTWeight = {}
for validator in lastCertifiedValidators:
blsKeyToBFTWeight[validator.blsKey] = validator.bftWeight
h = getBFTHeights().maxHeightCertified
while h > lastCertifiedHeight:
if h in aggregateCommits:
# Verify whether the chain of trust is maintained, i.e., the certificate corresponding to
# aggregateCommits[h] would be accepted by blockchain B.
if checkChainOfTrust(lastValidatorsHash, blsKeyToBFTWeight, lastCertificateThreshold, aggregateCommits[h]):
return getCertificateFromAggregateCommit(aggregateCommits[h])
h -= 1
return None
def checkChainOfTrust(lastValidatorsHash: bytes, blsKeyToBFTWeight: dict[bytes, uint64], lastCertificateThreshold: uint64, aggregateCommit: AggregateCommit) -> bool:
blockHeader = block header at height aggregateCommit.height - 1
# Certificate signers and certificate threshold for aggregateCommit are those authenticated by the last certificate
if lastValidatorsHash == blockHeader.validatorsHash:
return True
aggregateBFTWeight = 0
validators = validatorsHashPreimage[blockHeader.validatorsHash].validators
for i in range(length(validators)):
if bit i of aggregateCommit.aggregationsBits is 1:
# Aggregate commit must only be signed by BLS keys known to the other chain
if not validators[i].blsKey in blsKeyToBFTWeight:
return False
aggregateBFTWeight += blsKeyToBFTWeight[validators[i].blsKey]
if aggregateBFTWeight >= lastCertificateThreshold:
return True
else:
return False
In this section, we describe how to compute the certificate of largest height that can be submitted to a blockchain B given the single commits collected from a node running blockchain A. We let lastCertifiedHeight
be the height of the last certificate from blockchain A that has been submitted to blockchain B. For the computation, we require the following data from blockchain A:
- All block headers of blockchain A with height at least
lastCertifiedHeight
. - The single commits collected by the node running blockchain A with height at least
lastCertifiedHeight
. For the specifications here, we assume that there is a key-value mapsingleCommits
storing the single commits shared via the P2P network of blockchain A and collected by the node. That issingleCommits[h]
for a heighth
is an array of valid single commit object, i.e., single commit objects that passed the validation steps 1 - 7 described in the section Single Commit P2P Gossip, each with height property equal toh
and with distinctvalidatorAddress
properties. There is no key-value entry insingleCommits
if no single commits for that height were collected. - In Step 7 of the "before application processing" stage defined in LIP 0058, no longer needed entries in the BFT Parameters substore are deleted. However, for the approach described here, we may need BFT parameters that would be deleted in this stage. Hence, the implementation should be able to provide past BFT parameters such that it is always possible to obtain them via
getBFTParametersActiveValidators(lastCertifiedHeight + 1)
. For this it is sufficient if from a heighth
onwards, whereh
is the largest integer such thath <= lastCertifiedHeight + 1
, all BFT parameters in the BFT parameters substore are maintained. In particular, the implementation could allow to maintain all past BFT parameters for nodes that allow to obtain certificates with the approach described here.
The function getNextCertificateFromSingleCommits
which computes the certificate from the data described above is specified in the next section.
Using the single commits collected by a node running blockchain A, the following function computes the certificate of largest height that can be submitted to another blockchain B.
lastCertifiedHeight
: The height of the last certificate from blockchain A that was submitted to blockchain B.
The function returns a valid certificate object that can be submitted to blockchain B or None
if no certificate of height larger than lastCertifiedHeight
can be obtained from the single commits.
For readability, we describe the logic using two functions, the main function getNextCertificateFromSingleCommits
and the auxiliary function computeEligibleSingleCommits
.
def getNextCertificateFromSingleCommits(lastCertifiedHeight: uint32) -> Certificate | None:
# Obtain the BFT Parameters that were certified by the last certificate.
bftParams = getBFTParametersActiveValidators(lastCertifiedHeight + 1)
addressToBFTWeight = {}
for validator in bftParams.validators:
addressToBFTWeight[validator.address] = validator.bftWeight
h = getBFTHeights().maxHeightCertified
while h > lastCertifiedHeight:
if h in singleCommits:
# Obtain the subset of single commits from singleCommits[h] that are signed by a validator known to blockchain B.
# Note that the following function returns [] if the sum of BFT weights of these single commits does not reach
# the certificate threshold.
eligibleSingleCommits = computeEligibleSingleCommits(addressToBFTWeight, bftParams.certificateThreshold, singleCommits[h])
if eligibleSingleCommits != []:
aggregateCommit = aggregateSingleCommits(eligibleSingleCommits)
return getCertificateFromAggregateCommit(aggregateCommit)
h -= 1
return None
def computeEligibleSingleCommits(addressToBFTWeight: dict[bytes, uint64], lastCertificateThreshold: uint64, singleCommitsArray: list[SingleCommit]) -> list[SingleCommit]:
eligibleSingleCommits = []
aggregateBFTWeight = 0
for singleCommit in singleCommitsArray:
# Certificate must only be signed by BLS keys known to the other chain.
if singleCommit.validatorAddress in addressToBFTWeight:
append singleCommit to eligibleSingleCommits
aggregateBFTWeight += addressToBFTWeight[singleCommit.validatorAddress]
if aggregateBFTWeight >= lastCertificateThreshold:
return eligibleSingleCommits
else:
return []