From 51d6f663cfd4ca4177f464bbc1a36289e5b43be6 Mon Sep 17 00:00:00 2001 From: gkoumout <48478060+gkoumout@users.noreply.github.com> Date: Wed, 21 Sep 2022 13:00:44 +0300 Subject: [PATCH 1/4] Paste staging version --- proposals/lip-0057.md | 1552 +++++++++++++++++++++++++++-------------- 1 file changed, 1014 insertions(+), 538 deletions(-) diff --git a/proposals/lip-0057.md b/proposals/lip-0057.md index 1d72af370..395ff5d68 100644 --- a/proposals/lip-0057.md +++ b/proposals/lip-0057.md @@ -15,7 +15,7 @@ Required: 0022, 0023, 0024, 0040, 0044, 0046, 0058, 0059 ## Abstract The DPoS (delegated proof-of-stake) module is responsible for handling delegate registration, votes, and computing the delegate weight. In this LIP, we specify the properties of the DPoS 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. +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 DPos module. ## Copyright @@ -29,15 +29,17 @@ In this LIP we specify the properties, serialization, initialization, and expose ## Rationale -This new LIP does not introduce significant protocol changes to the generator selection mechanism proposed in [LIP 0022][lip-0022] and [LIP 0023][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][lip-0022] and [LIP 0023][lip-0023] for a thorough rationale regarding the choice of voting system and the inclusion of standby delegates. +This LIP does not introduce significant protocol changes to the generator selection mechanism proposed in [LIP 0022][lip-0022] and [LIP 0023][lip-0023]. It only defines how the commands and processes defined in those LIPs are integrated in the [state model][lip-0040] used in Lisk. Please see [LIP 0022][lip-0022] and [LIP 0023][lip-0023] for a thorough rationale regarding the choice of voting system and the inclusion of standby delegates. [LIP 0022][lip-0022] defines a selection mechanism for 2 standby delegates. In this LIP, we slightly extend the specifications to support 0 or 1 standby delegate, however, we do not specify how to extend the protocol to more than 2 delegates. Introducing more standby delegates might require a different source of randomness and it is not the aim of this LIP to describe this topic. +[LIP 0023][lip-0023] defines a [locking period][lip-0023#locking-period] for unvoted amounts and sets its duration to roughly 5.5 hours (2000 blocks). The sole protocol change in this LIP is to change this value to roughly 3 days (26.000 blocks). + ### DPoS Store #### Voter Substore -This part of the state store is used to maintain the votes and recent unvotes of users. The entries are keyed by address and contain an array of the current votes as well as an array of objects representing the tokens waiting to be unlocked. +This part of the state store is used to maintain the votes and recent unvotes of users (the ones for which the unvoted amount has not been unlocked). The entries are keyed by address and contain an array of the current votes as well as an array of objects representing the tokens waiting to be unlocked. #### Delegate Substore @@ -47,9 +49,13 @@ This part of the state store is used to maintain all information regarding the r This part of the state store is used to maintain a list of all names already registered. It allows the protocol to efficiently process the delegate registration transaction. The entries are keyed by delegate name and the value contains the address of the corresponding delegate. +#### Eligible Delegates Substore + +This part of the state store is used to maintain the list of all non-banned delegates that have more delegate weight than a specified threshold. The delegates from this list with the most weight are active delegates, and the others are standby delegates. + #### Snapshot Substore -This part of the state store is used to maintain the needed snapshots of validator weights. The entries are keyed by round number and contain the active delegates, the delegate weight of potential standby delegates. Entries for older rounds which are no longer necessary are removed. +This part of the state store is used to maintain the needed snapshots of the eligible delegates. The entries are keyed by round number and contain the addresses and weights of eligible delegates at the end of the corresponding round. Based on this information, the sets of active and standby delegates are selected two rounds later. Entries for older rounds which are no longer necessary are removed. #### Genesis Data Substore @@ -65,48 +71,94 @@ This part of the state store is used to maintain the timestamp of the last block For the rest of this proposal we define the following constants: -Name | Type | Value | Description ----------------------------------- | ------ | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- -**DPoS store constants** | | | -`STORE_PREFIX_VOTER` | bytes | 0x0000 | The store prefix of the voter substore. -`STORE_PREFIX_DELEGATE` | bytes | 0x4000 | The store prefix of the delegate substore. -`STORE_PREFIX_NAME` | bytes | 0x8000 | The store prefix of the name substore. -`STORE_PREFIX_SNAPSHOT` | bytes | 0xd000 | The store prefix of the snapshot substore. -`STORE_PREFIX_GENESIS_DATA` | bytes | 0xc000 | The store prefix of the genesis data substore. -`STORE_PREFIX_PREVIOUS_TIMESTAMP` | bytes | 0xe000 | The store prefix of the previous timestamp substore. -**DPoS constants** | | | -`MODULE_ID_DPOS` | uint32 | TBD | The module ID of the DPoS module. -`COMMAND_ID_DELEGATE_REGISTRATION` | uint32 | `0` | The command ID of the delegate registration transaction. -`COMMAND_ID_VOTE` | uint32 | `1` | The command ID of the vote transaction. -`COMMAND_ID_UNLOCK` | uint32 | `2` | The command ID of the unlock transaction. -`COMMAND_ID_POM` | uint32 | `3` | The command ID of the proof-of-misbehavior transaction. -`COMMAND_ID_UPDATE_GENERATOR_KEY` | uint32 | `4` | The command ID of the update generator key transaction. -**Configurable Constants** | | **Mainchain Value** | -`FACTOR_SELF_VOTES` | uint32 | `10` | The factor multiplying the self-votes of a delegate for the delegate weight computation. -`MAX_LENGTH_NAME` | uint32 | `20` | The maximum allowed name length for delegates. -`MAX_NUMBER_SENT_VOTES` | uint32 | `10` | The maximum size of the sentVotes array of a voter substore entry. -`MAX_NUMBER_PENDING_UNLOCKS` | uint32 | `20` | The maximum size of the pendingUnlocks array of a voter 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 | `260,000` | The length of the inactivity window used in the fail safe banning mechanism. -`PUNISHMENT_WINDOW` | uint32 | `780,000` | The punishment time for punished delegates. -`ROUND_LENGTH` | uint32 | `103` | The round length. -`BFT_THRESHOLD` | uint32 | `68` | The precommit and certificate thresholds used by the BFT module. -`MIN_WEIGHT_STANDBY` | uint64 | `1000*(10^8)` | The minimum delegate weight required to be eligible as a standby delegate. -`NUMBER_ACTIVE_DELEGATES` | uint32 | `101` | The number of active delegates. -`NUMBER_STANDBY_DELEGATES` | uint32 | `2` | The number of standby delegates. This LIP is specified for the number of standby delegates being 0, 1 or 2. -`TOKEN_ID_DPOS` | object | `TOKEN_ID_LSK` = {
-`"chainID": 0`,
-`"localID": 0`
-} | The token ID of the token used to cast votes. -`DELEGATE_REGISTRATION_FEE` | uint64 | `10*(10^8)` | The extra command fee of the delegate registration. - -#### uint32be Function - -The 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. +| 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][lip-0037#chain-identifiers] of the chain. | +| **DPoS store constants** | | | | +| `STORE_PREFIX_VOTER` | bytes | 0x0000 | The store prefix of the voter substore. | +| `STORE_PREFIX_DELEGATE` | bytes | 0x4000 | The store prefix of the delegate substore. | +| `STORE_PREFIX_NAME` | bytes | 0x8000 | The store prefix of the name substore. | +| `STORE_PREFIX_SNAPSHOT` | bytes | 0xd000 | The store prefix of the snapshot substore. | +| `STORE_PREFIX_GENESIS_DATA` | bytes | 0xc000 | The store prefix of the genesis data substore. | +| `STORE_PREFIX_PREVIOUS_TIMESTAMP` | bytes | 0xe000 | The store prefix of the previous timestamp substore. | +| `STORE_PREFIX_ELIGIBLE_DELEGATES` | bytes | 0xf000 | The store prefix of the eligible delegates substore. | +| **DPoS constants** | | | | +| `MODULE_NAME_DPOS` | string | "dpos" | The module name of the DPoS module. | +| `COMMAND_NAME_DELEGATE_REGISTRATION` | string | "delegateRegistration"| The command name of the delegate registration transaction. | +| `COMMAND_NAME_VOTE` | string | "vote" | The command name of the vote transaction. | +| `COMMAND_NAME_UNLOCK` | string | "unlock" | The command name of the unlock transaction. | +| `COMMAND_NAME_POM` | string | "pom" | The command name of the proof-of-misbehavior transaction. | +| **Configurable Constants** | | **Mainchain Value** | | +| `FACTOR_SELF_VOTES` | uint32 | `10` | The factor multiplying the self-votes of a delegate for the delegate weight computation. | +| `BASE_VOTE_AMOUNT` | uint32 | `10 * (10)^8` | The minimum voting amount. All voted amounts should be multiples of this value. | +| `MAX_LENGTH_NAME` | uint32 | `20` | The maximum allowed name length for delegates. | +| `MAX_NUMBER_SENT_VOTES` | uint32 | `10` | The maximum size of the sentVotes array of a voter substore entry. | +| `MAX_NUMBER_PENDING_UNLOCKS` | uint32 | `20` | The maximum size of the pendingUnlocks array of a voter 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 | `130,000` | The length of the inactivity window used in the fail safe banning mechanism. | +| `LOCKING_PERIOD_VOTES` | uint32 | `26,000` | The [locking period][lip-0023#explicit-unlock-mechanism] for regular votes. | +| `LOCKING_PERIOD_SELF_VOTES` | uint32 | `260,000` | The [locking period][lip-0023#explicit-unlock-mechanism] for self-votes. | +| `PUNISHMENT_WINDOW_VOTES` | uint32 | `260,000` | The punishment time for votes on punished delegates. | +| `PUNISHMENT_WINDOW_SELF_VOTES` | uint32 | `780,000` | The punishment time for self-votes of punished delegates. | +| `POM_LIMIT_BANNED` | uint32 | `5` | The number of proof-of-misbehavior transactions against a delegate for getting banned. | +| `MIN_INIT_ROUNDS` | uint32 | `3` | The minimum number of rounds for the [bootstrap period][lip-0034#bootstrap-period] | +| `BFT_THRESHOLD` | uint32 | `68` | The precommit and certificate thresholds used by the BFT module. This constant must equal `floor(2/3 * NUMBER_ACTIVE_DELEGATES) + 1` | +| `MIN_WEIGHT` | uint64 | `1000*(10^8)` | The minimum delegate weight required to be selected as a block generator. | +| `NUMBER_ACTIVE_DELEGATES` | uint32 | `101` | The number of active delegates. | +| `NUMBER_STANDBY_DELEGATES` | uint32 | `2` | The number of standby delegates. This LIP is specified for the number of standby delegates being 0, 1 or 2. | +| `ROUND_LENGTH` | uint32 | `103` | The round length. Is equal to `NUMBER_ACTIVE_DELEGATES` + `NUMBER_STANDBY_DELEGATES` | +| `TOKEN_ID_DPOS` | bytes | `TOKEN_ID_LSK = 0x 00 00 00 00 00 00 00 00` | The [token ID][lip-0051#tokenID] of the token used to cast votes. | +| `TOKEN_ID_FEE` | bytes | `TOKEN_ID_LSK = 0x 00 00 00 00 00 00 00 00` | The [token ID][lip-0051#tokenID] of the token used to pay the transaction fees. Defined in the fee module. | +| `TOKEN_ID_REWARD` | bytes |`TOKEN_ID_LSK = 0x 00 00 00 00 00 00 00 00`| The [token ID][lip-0051#tokenID] of the token used for block rewards. Specified as part of the [reward module][lip-0042] configuration. | +| `DELEGATE_REGISTRATION_FEE` | uint64 | `10*(10^8)` | The extra command fee of the delegate registration. | +| `INVALID_BLS_KEY ` | bytes | 48 bytes all set to 0x00 | The byte value associated with validators that did not register a BLS key. | + + + +### Event Names and Results + +| Name | Type | Value | Description | +|-----------------------------------|--------|--------|-------------| +| **Event names** | | | | +| `EVENT_NAME_REGISTER_DELEGATE` | string | "registerDelegate" | Used for events during delegate registration. | +| `EVENT_NAME_VOTE_DELEGATE` | string | "voteDelegate" | Used for events related to voting a delegate. | +| `EVENT_NAME_DELEGATE_PUNISHED` | string | "delegatePunished" | Used for events related to punishing a delegate. | +| `EVENT_NAME_DELEGATE_BANNED` | string | "delegateBanned" | Used for events related to banning a delegate. | +| **Result codes** | | | | +| `VOTE_SUCCESSFUL` | uint32 | 0 | Used when a vote succeeds. | +| `VOTE_FAILED_NON_REGISTERED_DELEGATE`| uint32 | 1 | Used when a vote fails because the voted account has not registered a delegate. | +| `VOTE_FAILED_INVALID_UNVOTE_PARAMETERS`| uint32 | 2 | Used when an unvote fails because the unvoted amount exceeds the total votes sent from voter to delegate. | +| `VOTE_FAILED_TOO_MANY_PENDING_UNLOCKS` | uint32 | 3 | Used when a vote fails because it the total number of pending unlocks of voter exceeds `MAX_NUMBER_PENDING_UNLOCKS`. | +| `VOTE_FAILED_TOO_MANY_SENT_VOTES` | uint32 | 4 | Used when a vote fails because the total number of delegates voted by the voter exceeds `MAX_NUMBER_SENT_VOTES`. | + + + + +### Type Definition + +| Name | Type | Validation | Description | +|---------------------|--------|--------------------------------------------------|----------------------------------------------------------------------------------------| +| `Address` | bytes | Must be of length `ADDRESS_LENGTH`. | Address of an account. | +| `Transaction` | object | Must follow the `transactionSchema` schema defined in [LIP 0068][lip-0068] with the only difference that `params` property is not serialized and contains the values of parameters of `paramsSchema` for the corresponding transaction. | An object representing a non-serialized transaction. | +| `UnlockObject` | object | Contains 3 elements (`address`, `amount`, `unvoteHeight`) of types `Address`, `uint64` and `uint32` respecively (same as the items in the `pendingUnlocks` array of the [voterStoreSchema](#json-schema)). | An object containing information regarding unvoting a delegate. | +| `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][lip-0038#public-key-registration]. | +| `PublicKeyEd25519` | bytes | Must be of length `ED25519_PUBLIC_KEY_LENGTH`. | Used for Ed25519 public keys. | +| `VoterStoreObject` | object | Must follow the [`voterStoreSchema` schema](#json-schema). | Deserialized version of voter substore values. | +| `DelegateStoreObject` | object | Must follow the [`delegateStoreSchema` schema](#json-schema-1). | Deserialized version of delegate substore values. | + + + #### Functions from Other Modules -Calling a function `fct` from another module (named `moduleName`) is represented by `moduleName.fct(required inputs)`. +Calling a function `fct` from another module (named `module`) is represented by `module.fct(required inputs)`. ### DPoS Module Store @@ -117,9 +169,9 @@ The store keys and values of the DPoS store are set as follows: ##### Store Prefix, Store Key, and Store Value * The store prefix is set to `STORE_PREFIX_VOTER`. -* Each store key is a 20-byte `address`, representing a user address. +* Each store key is a `ADDRESS_LENGTH`-byte `address`, representing a user address. * Each store value is the serialization of an object following `voterStoreSchema`. -* Notation: For the rest of this proposal let `voterStore(address)` be the entry in the voter substore with store key `address`. +* Notation: For the rest of this proposal let `voterStore(address)` be the object stored in the voter substore with store key `address`. ##### JSON Schema @@ -137,6 +189,7 @@ voterStoreSchema = { "properties": { "delegateAddress": { "dataType": "bytes", + "length": ADDRESS_LENGTH, "fieldNumber": 1 }, "amount": { @@ -155,6 +208,7 @@ voterStoreSchema = { "properties": { "delegateAddress": { "dataType": "bytes", + "length": ADDRESS_LENGTH, "fieldNumber": 1 }, "amount": { @@ -172,21 +226,34 @@ voterStoreSchema = { } ``` -##### Properties and Default values - -In this section, we describe the properties of the voter substore and specify their default values. - -* `sentVotes`: stores an array of the current votes of a user. Each vote is represented by the address of the voted delegate and the amount of tokens that have been used to vote for the delegate. This array was called `votes` in LIP 0023. This array is updated with a vote command. The `sentVotes` array is always kept ordered in lexicographical order of `delegateAddress`. Its size is at most `MAX_NUMBER_SENT_VOTES`, any state transition that would increase it to above `MAX_NUMBER_SENT_VOTES` is invalid. -* `pendingUnlocks`: stores an array representing the tokens that have been unvoted, but not yet unlocked. Each unvote generates an object in this array containing the address of the unvoted delegate, the amount of the unvote and the height at which the unvote 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 vote and unlock commands. The `pendingUnlocks` array is always kept ordered by lexicographical order of `delegateAddress`, ties broken by increasing `amount`, ties broken by increasing `unvoteHeight`. 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. +##### Properties + +In this section, we describe the properties of the voter substore. + +* `sentVotes`: stores an array of the current votes of a user. + Each vote is represented by the address of the voted delegate and the amount of tokens that have been used to vote for the delegate. + This array was called `votes` in [LIP 0023][lip-0023]. + This array is updated with a [vote command](#vote). + The `sentVotes` array is always kept ordered in lexicographical order of `delegateAddress`. + Its size is at most `MAX_NUMBER_SENT_VOTES`, any state transition that would increase it to above `MAX_NUMBER_SENT_VOTES` is invalid. + Any element with `amount == 0` is removed from the array. +* `pendingUnlocks`: stores an array representing the tokens that have been unvoted, but not yet unlocked. + Each unvote generates an object in this array containing the address of the unvoted delegate, the amount of the unvote and the height at which the unvote was included in the chain. + Objects in this array get removed when the corresponding [unlock command](#unlock) is executed. This array was called `unlocking` in [LIP 0023][lip-0023]. + This array is updated with [vote](#vote) and [unlock](#unlock) commands. + The `pendingUnlocks` array is always kept ordered by lexicographical order of `delegateAddress`, ties broken by increasing `amount`, ties broken by increasing `unvoteHeight`. + 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 voter substore entry to have `sentVotes == []` and `pendingUnlocks == []`, the entry is removed from the store. #### Delegate Substore ##### Store Prefix, Store Key, and Store Value * The store prefix is set to `STORE_PREFIX_DELEGATE`. -* Each store key is a 20-byte `address`, representing a delegate address. +* Each store key is a `ADDRESS_LENGTH`-byte `address`, representing a delegate address. * Each store value is the serialization of an object following `delegateStoreSchema`. -* Notation: For the rest of this proposal let `delegateStore(address)` be the entry in the delegate substore with store key `address`. +* Notation: For the rest of this proposal let `delegateStore(address)` be the object stored in the delegate substore with store key `address`. ##### JSON Schema @@ -236,11 +303,11 @@ delegateStoreSchema = { } ``` -##### Properties and Default values +##### Properties -In this section, we describe the properties of the delegate substore and specify their default values. 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 [delegate registration command](#delegate-registration) and its value is set during the command execution. It contains information about the delegate whose address is the store key. +In this section, we describe the properties of the delegate 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 [delegate registration command](#delegate-registration) and its value is set during the command execution. It contains information about the delegate whose address is the store key. -* `name`: a string representing the delegate name, with a minimum length of `1` character and a maximum length of `MAX_LENGTH_NAME` +* `name`: a string representing the delegate name, with a minimum length of `1` character and a maximum length of `MAX_LENGTH_NAME`. * `totalVotesReceived`: the sum of all votes received by a delegate. * `selfVotes` : the sum of all votes the delegate cast for its own account. * `lastGeneratedHeight`: the height at which the delegate last generated a block. @@ -255,7 +322,7 @@ In this section, we describe the properties of the delegate substore and specify * The store prefix is set to `STORE_PREFIX_NAME`. * Each store key is a utf8-encoded string `name`, representing a delegate name. * Each store value is the serialization of an object following `nameStoreSchema`. -* Notation: For the rest of this proposal let `nameStore(name)` be the entry in the name substore with store key `name`. +* Notation: For the rest of this proposal let `nameStore(name)` be the object stored in the name substore with store key `name`. ##### JSON Schema @@ -266,49 +333,55 @@ nameStoreSchema = { "properties": { "delegateAddress": { "dataType": "bytes", + "length": ADDRESS_LENGTH, "fieldNumber": 1 } } } ``` -##### Properties and Default values +##### Properties -The name substore maintains all registered names, using the name as store key and the address of the validator that registered that name as the corresponding store value. The name substore is initially empty, i.e. it does not contain any key-value pairs. +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 [delegate registration command](#delegate-registration). + +#### Eligible Delegates Substore + +##### Store Prefix, Store Key, and Store Value + +* The store prefix is set to `STORE_PREFIX_ELIGIBLE_DELEGATES`. +* This store only maintains an entry for delegates that have weight more than `MIN_WEIGHT`, and that are not banned. +* For the entry corresponding to a delegate, the store key is the concatenation of the delegate weight (represented using the big endian uint64 serialization) and the delegate address, i.e., `weight.to_bytes(8,'big') + address`. +* The store value is set to empty bytes. #### Snapshot Substore ##### Store Prefix, Store Key, and Store Value * The store prefix is set to `STORE_PREFIX_SNAPSHOT`. -* Each store key is `uint32be(roundNumber)`, where `roundNumber` is the number of the round at the end of which the active delegates and weights are computed. These values will be used to compute the validator set for round `roundNumber + 2`. +* 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 at the end of which the active delegates and weights are computed. These values will be used to compute the validator set for round `roundNumber + 2`. * Each store value is the serialization of an object following `snapshotStoreSchema`. -* Notation: For the rest of this proposal let `snapshotStore(roundNumber)` be the entry in the snapshot substore with store key `uint32be(roundNumber)`. +* Notation: For the rest of this proposal let `snapshotStore(roundNumber)` be the object stored in the snapshot substore with store key `roundNumber.to_bytes(4,'big')`. ##### JSON Schema ```java snapshotStoreSchema = { "type": "object", - "required": ["activeDelegates", "delegateWeightSnapshot"], + "required": ["delegateWeightSnapshot"], "properties": { - "activeDelegates": { - "type": "array", - "fieldNumber": 1, - "items": {"dataType": "bytes"} - }, "delegateWeightSnapshot": { "type": "array", - "fieldNumber": 2, + "fieldNumber": 1, "items": { "type": "object", - "required": ["delegateAddress", "delegateWeight"], + "required": ["address", "weight"], "properties": { - "delegateAddress": { + "address": { "dataType": "bytes", + "length": ADDRESS_LENGTH, "fieldNumber": 1 }, - "delegateWeight": { + "weight": { "dataType": "uint64", "fieldNumber": 2 } @@ -319,14 +392,13 @@ snapshotStoreSchema = { } ``` -The `delegateWeightSnapshot` array is ordered lexicographically by `address`. +The `delegateWeightSnapshot` array is ordered by decreasing `weight`, ties broken by reverse lexicographical ordering of `address`. -##### Properties and Default values +##### Properties -In this section, we describe the properties of the snapshot substore and specify their default values. +In this section, we describe the properties of the snapshot substore. -* `activeDelegates`: the addresses of the top `NUMBER_ACTIVE_DELEGATES` delegates by delegate weight at the end of round `roundNumber`. -* `delegateWeightSnapshot`: all delegate addresses and weights of delegates (not in the active delegates array) with more than `MIN_WEIGHT_STANDBY` delegate weight. This array is completed with delegates with less weight in the case there are not enough delegates with weight above `MIN_WEIGHT_STANDBY`. +* `delegateWeightSnapshot`: all delegate addresses and weights of all non-banned delegates with more than `MIN_WEIGHT` delegate weight for the given round number. The snapshot substore is initially empty. @@ -335,12 +407,12 @@ The snapshot substore is initially empty. ##### Store Prefix, Store Key, and Store Value * The store prefix is set to `STORE_PREFIX_GENESIS_DATA`. -* The store key is the empty bytes. +* 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: - * `genesisHeight` be the `height` property of the entry in the genesis data substore. - * `initRounds` be the `initRounds` property of the entry in the genesis data substore. - * `initDelegates` be the `initDelegates` property of the entry in the genesis data substore. + * `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.initDelegates` be the `initDelegates` property of the entry in the genesis data substore. ##### JSON Schema @@ -364,18 +436,21 @@ genesisDataStoreSchema = { "initDelegates": { "type": "array", "fieldNumber": 3, - "items": {"dataType": "bytes"} + "items": { + "dataType": "bytes", + "length": ADDRESS_LENGTH + } } } } ``` -##### Properties and Default values +##### 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][lip-0034#bootstrap-period], also called initial rounds. This property must have a value greater than 3. +* `initRounds`: the length of the [bootstrap period][lip-0034#bootstrap-period], also called initial rounds. `initRounds` must be at least `MIN_INIT_ROUNDS`. * `initDelegates`: the addresses of the validators to be used during the bootstrap period. This property must have a non-empty value. #### Previous Timestamp Substore @@ -383,7 +458,7 @@ The genesis data substore stores information from the genesis block. It is initi ##### Store Prefix, Store Key, and Store Value * The store prefix is set to `STORE_PREFIX_PREVIOUS_TIMESTAMP`. -* The store key is the empty bytes. +* 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. @@ -402,26 +477,20 @@ previousTimestampStoreSchema = { } ``` -##### Properties and Default values +##### Properties `timestamp`: The timestamp of the last block added to the chain. -### Command - -#### Delegate Registration -Transaction executing this command have: +### Commands -* `moduleID = MODULE_ID_DPOS`, -* `commandID = COMMAND_ID_DELEGATE_REGISTRATION`. +#### Delegate Registration -##### Fee +Transactions executing this command have: -This command has an extra command fee: +* `module = MODULE_NAME_DPOS`, +* `command = COMMAND_NAME_DELEGATE_REGISTRATION`. -```python -extraCommandFee(MODULE_ID_DPOS, COMMAND_ID_DELEGATE_REGISTRATION) = DELEGATE_REGISTRATION_FEE -``` ##### Parameters @@ -432,7 +501,8 @@ delegateRegistrationTransactionParams = { "name", "blsKey", "proofOfPossession", - "generatorKey" + "generatorKey", + "delegateRegistrationFee" ], "properties": { "name": { @@ -441,101 +511,109 @@ delegateRegistrationTransactionParams = { }, "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 + }, + "delegateRegistrationFee": { + "type": "uint64", + "fieldNumber": 5 } } } ``` +The property `delegateRegistrationFee` corresponds to the special fee for registering a delegate. The token used for this fee is `TOKEN_ID_FEE`. + ##### Verification -The params property of a delegate registration command is valid if: +```python +def verify(trs: Transaction) -> None: + delegateAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # derive delegateAddress from trs.senderPublicKey + + if trs.params.delegateRegistrationFee != DELEGATE_REGISTRATION_FEE: + raise Exception('Invalid delegate registration fee.') + if Token.getAvailableBalance(delegateAddress, TOKEN_ID_FEE) < DELEGATE_REGISTRATION_FEE: + raise Exception('Not sufficient amount for delegate registration fee.') + if there exists an entry delegateStore(delegateAddress) in delegate substore: + raise Exception('This address has already registered a delegate.') + if not isDelegateNameValid(trs.params.name):. + raise Exception('Invalid name') + if there exists an entry nameStore(delegateName) in name substore: + raise Exception('Name already used by a delegate.') +``` -* there exists no entry in the name substore with store key `name`. -* `name` contains only characters from the set `abcdefghijklmnopqrstuvwxyz0123456789!@$&_.` (lower case letters, numbers and symbols `!@$&_.`), is at least `1` character long and at most `MAX_LENGTH_NAME` characters long. -* There exists no entry in the delegate substore with store key `delegateAddress`, `delegateAddress` being derived from the transaction sender public key. -* `generatorKey` must have length 32. -* `blsKey` must have length 48. -* `proofOfPossession` must have length 96. ##### Execution When a transaction `trs` executing a delegate registration command included in a block `b`, the logic below is followed: ```python -derive delegateAddress from trs.senderPublicKey - -validators.registerValidatorKeys(delegateAddress, - proofOfPossession, - generatorKey, - blsKey) - -if the above function returns False, the execution fails - -create an entry in the delegate substore with - storeKey = delegateAddress, - storeValue = { - "name": trs.params.name, - "totalVotesReceived": 0, - "selfVotes": 0, - "lastGeneratedHeight": b.header.height, - "isBanned": False, - "pomHeights": [], - "consecutiveMissedBlocks": 0 - } serialized using delegateStoreSchema - -create an entry in the name substore with - storeKey = trs.params.name encoded as utf8, - storeValue = {"delegateAddress": delegateAddress} serialized using nameStoreSchema -``` -#### Update Generator Key +def execute(trs: Transaction) -> None: + b = block including trs + delegateAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # derive delegateAddress from trs.senderPublicKey + delegateName = trs.params.name -This command is used to update the generator key (from the [validators module][lip-0044]) for a specific validator. Transaction executing this command have: + # this step also checks that the BLS key has not been used from another delegate + Validators.registerValidatorKeys(delegateAddress, + trs.params.proofOfPossession, + trs.params.generatorKey, + trs.params.blsKey) -* `moduleID = MODULE_ID_DPOS`, -* `commandID = COMMAND_ID_UPDATE_GENERATOR_KEY`. -##### Parameters -```java -updateGeneratorKeyParams = { - "type": "object", - "required": ["generatorKey"], - "properties": { - "generatorKey": { - "dataType": "bytes", - "fieldNumber": 1 - } - } -} -``` - -##### Verification + # the new delegate pays the registration fee; this fee is burned. + Token.burn(delegateAddress, TOKEN_ID_FEE, trs.params.delegateRegistrationFee) -An update generator key transaction `trs` must fulfill the following to be valid: -* Let `address` be the 20-byte address derived from `trs.senderPublicKey`. Then the delegate substore must have an entry for the store key `address`. -* `trs.params.generatorKey` must have length 32. - -##### Execution + # update delegate substore + delegateState = { + "name": delegateName, + "totalVotesReceived": 0, + "selfVotes": 0, + "lastGeneratedHeight": b.header.height, + "isBanned": False, + "pomHeights": [], + "consecutiveMissedBlocks": 0 + } -Executing an update generator key transaction `trs` is done by calling `validators.setValidatorGeneratorKey(address, trs.params.generatorKey)` where `address` is the 20-byte address derived from trs.senderPublicKey. `trs` is invalid if this function returns `False`. + create an entry in the delegate substore with + storeKey = delegateAddress, + storeValue = encode(delegateStoreSchema, delegateState) + + # update name substore + create an entry in the name substore with + storeKey = delegateName encoded as utf8, + storeValue = encode(nameStoreSchema, {"delegateAddress": delegateAddress}) + + # emit event for the successfull delegate registration. + emitEvent( + module=MODULE_NAME_DPOS, + name=EVENT_NAME_REGISTER_DELEGATE, + data={ + "address": delegateAddress, + "name": delegateName + }, + topics=[delegateAddress] + ) +``` #### Vote Transactions executing this command have: -* `moduleID = MODULE_ID_DPOS` -* `commandID = COMMAND_ID_VOTE` +* `module = MODULE_NAME_DPOS` +* `command = COMMAND_NAME_VOTE` ##### Parameters @@ -553,6 +631,7 @@ voteTransactionParams = { "properties": { "delegateAddress" : { "dataType": "bytes", + "length": ADDRESS_LENGTH, "fieldNumber": 1 }, "amount": { @@ -566,99 +645,189 @@ voteTransactionParams = { } ``` -The verification and execution of this transaction is specified in [LIP 0023][lip-0023#new-vote-transaction]. This specification is followed to implement the vote command with the following additional point: +##### Verification + +The `params` property of a vote transaction is valid if: -* when executing a self-vote (a vote with the `delegateAddress` equal to the address derived from the transaction public key), modify the `delegateStore(delegateAddress).selfVotes` property and the `voterStore(delegateAddress).sentVotes` entry corresponding to `delegateAddress` identically. +* `params.votes` has at most `2 * MAX_NUMBER_SENT_VOTES` elements. The reason of choosing this bound on the size is to allow a voter to unvote all voted delegates (which are at most `MAX_NUMBER_SENT_VOTES`) and vote for `MAX_NUMBER_SENT_VOTES` new ones in the same transaction. +* A given `delegateAddress` is included in at most one vote from the list of votes (regardless of the associated amounts). +* For all votes included in `params.votes`, we have: + * `amount` value is a multiple of `BASE_VOTE_AMOUNT`, i.e., `amount % BASE_VOTE_AMOUNT == 0`. For the Lisk mainchain, where `BASE_VOTE_AMOUNT = 10^9` and `TOKEN_ID_DPOS = TOKEN_ID_LSK`, this corresponds to multiples of 10 LSK. + * `amount != 0`. + +##### Execution + +When executing a vote transaction `trs`, the logic below is followed + +```python +def execute(trs: Transaction) -> None: + b = block including trs + height = b.header.height + senderAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # derive address from trs.senderPublicKey + + + # sorting the votes guarantees that we first apply the votes with negative amounts + sortedVotes = trs.params.votes ordered by increasing value of amount + + for vote in sortedVotes: + votedAddress = vote.delegateAddress + + if delegateStore(votedAddress) does not exist: + emitVoteEvent(senderAddress, votedAddress, vote.amount, height, VOTE_FAILED_NON_REGISTERED_DELEGATE) + raise Exception('Invalid vote: no registered delegate with the specified address') + + if vote.amount < 0: # case of unvote + sentVote = element in voterStore(senderAddress).sentVotes with sentVote.delegateAddress == votedAddress + if not sentVote or abs(vote.amount) > sentVote.amount: + emitVoteEvent(senderAddress, votedAddress, vote.amount, height, VOTE_FAILED_INVALID_UNVOTE_PARAMETERS) + raise Exception('Invalid unvote: The unvote amount exceeds the voted amount for this delegate') + + # update voter substore + i = index of sentVote in voterStore(senderAddress).sentVotes + voterStore(senderAddress).sentVotes[i].amount += vote.amount + + if voterStore(senderAddress).sentVotes[i].amount == 0: + remove sentVote from voterStore(senderAddress).sentVotes + + unlockObject = { + "delegateAddress" : votedAddress, + "amount" : abs(vote.amount), + "unvoteHeight" : height + } + add unlockOject to voterStore(senderAddress).pendingUnlocks, keeping the array ordered by lexicographical order of delegateAddress, + ties broken by increasing amount, + ties broken by increasing unvoteHeight + + if len(voterStore(senderAddress).pendingUnlocks) > MAX_NUMBER_PENDING_UNLOCKS: + emitVoteEvent(senderAddress, votedAddress, vote.amount, height, VOTE_FAILED_TOO_MANY_PENDING_UNLOCKS) + raise Exception('Sender has reached the maximum number of pending unlocks.') + + if vote.amount > 0: #case of regular vote + Token.lock(senderAddress, MODULE_NAME_DPOS, TOKEN_ID_DPOS, vote.amount) # lock the voted amount + #update user substore + if there exist an entry oldVote in voterStore(senderAddress).sentVotes with oldVote.delegateAddress = votedAddress: + i = index of oldVote in voterStore(senderAddress).sentVotes + voterStore(senderAddress).sentVotes[i].amount += vote.amount + else: + add {"delegateAddress": votedAddress, "amount": vote.amount} to voterStore(senderAddress).sentVotes + keeping the array ordered in lexicographical order of delegateAddress + + if len(voterStore(senderAddress).sentVotes) > MAX_NUMBER_SENT_VOTES: + emitVoteEvent(senderAddress, votedAddress, vote.amount, height, VOTE_FAILED_TOO_MANY_SENT_VOTES) + raise Exception('This address has reached the maximum number of voted delegates.') + + + # update delegate substore + previousDelegateWeight = getDelegateWeight(votedAddress) + + delegateStore(votedAddress).totalVotesReceived += vote.amount + if senderAddress == votedAddress: + delegateStore(votedAddress).selfVotes += vote.amount + + emitVoteEvent(senderAddress, votedAddress, vote.amount, height, VOTE_SUCCESSFUL) + # update eligible delegates substore + updateDelegateEligibility(votedAddress, previousDelegateWeight) + + +def emitVoteEvent(senderAddress: Address, delegateAddress: Address, amount: uint64, height: uint32, result: uint32) -> None: + if result == VOTE_SUCCESSFUL: + emitEvent( + module = MODULE_NAME_DPOS, + name = EVENT_NAME_VOTE_DELEGATE, + data={ + "senderAddress": senderAddress, + "delegateAddress": delegateAddress, + "amount": amount, + "height": height, + "result": VOTE_SUCCESSFUL + }, + topics = [senderAddress, delegateAddress] + ) + else: + emitPersistentEvent( + module = MODULE_NAME_DPOS, + name = EVENT_NAME_VOTE_DELEGATE, + data={ + "senderAddress": senderAddress, + "delegateAddress": delegateAddress, + "amount": amount, + "height": height, + "result": result + }, + topics = [senderAddress, delegateAddress] + ) +``` + +Here, the [`updateDelegateEligibility` function](#updatedelegateeligibility) updates the eligible delegates substore according to the new weight of the voted delegate. #### Unlock Transactions executing this command have: -* `moduleID = MODULE_ID_DPOS` -* `commandID = COMMAND_ID_UNLOCK` +* `module = MODULE_NAME_DPOS` +* `command = COMMAND_NAME_UNLOCK` ##### Parameters The `params` property of unlock transactions is empty. -#### Verification +#### Execution -An unlock transaction `trs` is valid if the following returns `True`: ```python -senderAddress = address corresponding to trs.senderPublicKey -height = height of the block including trs - -for each unlockObject in voterStore(senderAddress).pendingUnlocks: - if (hasWaited(unlockObject, senderAddress, height) - and not isPunished(unlockObject, senderAddress, height) - and isCertificateGenerated(unlockObject)): - return True - -return False +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 voterStore(senderAddress).pendingUnlocks: + # check if unvoted amount can be unlocked + if (isUnlockable(unlockObject, senderAddress, height) + and isCertificateGenerated(unlockObject)): + + delete unlockObject from voterStore(senderAddress).pendingUnlocks + Token.unlock(senderAddress, MODULE_NAME_DPOS, TOKEN_ID_DPOS, 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 "Introduce unlocking condition for incentivizing certificate generation"][lip-0059]. The `hasWaited` and `isPunished` functions are defined below and are rationalized in [LIP 0023][lip-0023#explicit-unlock-mechanism] and [LIP 0024][lip-0024#rationale] respectively. Both functions have the following input parameters: +The definition and rationale for the `isCertificateGenerated` function is part of [LIP 0059][lip-0059]. The function `isUnlockable` is defined below. It's logic is the concatenation of the functions `hasWaited` and `isPunished` that are rationalized in [LIP 0023][lip-0023#explicit-unlock-mechanism] and [LIP 0024][lip-0024#rationale] respectively. This function has the following input parameters: * `unlockObject`: an object with properties `delegateAddress` (the address of the previously voted delegate), `amount` (the unvote amount) and `unvoteHeight` (the height of the unvote). -* `senderAddress`: 20-byte address of the user sending the unlock transaction. +* `senderAddress`: Address of the user sending the unlock transaction. * `height`: the height of the block including the unlock transaction. ```python -hasWaited(unlockObject, senderAddress, height): - if unlockObject.delegateAddress == senderAddress: - # This is a self-unvote - delayedAvailability = 260,000 - else: - delayedAvailability = 2000 - - if height - unlockObject.unvoteHeight < delayedAvailability: - return False - else: - return True -``` - -```python -isPunished(unlockObject, senderAddress, height): - if delegateStore(unlockObject.delegateAddress).pomHeights is empty: - return false - else: - let lastPomHeight be the last element of delegateStore(unlockObject.delegateAddress).pomHeights +def isUnlockable(unlockObject: UnlockObject, senderAddress: Address, height: uint32) -> bool: + #first consider the case that delegate is not punished + delegateAddress = unlockObject.delegateAddress + lockingPeriod = LOCKING_PERIOD_SELF_VOTES if delegateAddress == senderAddress else LOCKING_PERIOD_VOTES + + # if delegate is not punished, normal locking period for votes/selfvotes applies. + if not isPunished(delegateAddress, height): + if height - unlockObject.unvoteHeight < lockingPeriod: + return False + + else: #delegate is punished + let lastPomHeight be the last element of delegateStore(delegateAddress).pomHeights # lastPomHeight is also the largest element of the pomHeights array - - if unlockObject.address == senderAddress: - # This is a self-unvote - if height – lastPomHeight < 780,000 and lastPomHeight < unlockObject.unvoteHeight + 260,000: - return True - else: - if height – lastPomHeight < 260,000 and lastPomHeight < unlockObject.unvoteHeight + 2000: - return True - - return False + punishmentWindow = PUNISHMENT_WINDOW_SELF_VOTES if delegateAddress == senderAddress else PUNISHMENT_WINDOW_VOTES + + if height – lastPomHeight < punishmentWindow and lastPomHeight < unlockObject.unvoteHeight + lockingPeriod: + return False + + return True ``` -#### Execution -When executing an unlock transaction `trs`, the following is done: -```python -senderAddress = address corresponding to trs.senderPublicKey -height = height of the block including trs - -for each unlockObject in voterStore(senderAddress).pendingUnlocks: - if (hasWaited(unlockObject, senderAddress, height) - and not isPunished(unlockObject, senderAddress, height) - and isCertificateGenerated(unlockObject)): - delete unlockObject from voterStore(senderAddress).pendingUnlocks - token.unlock(senderAddress, MODULE_ID_DPOS, TOKEN_ID_DPOS, unlockObject.amount) -``` #### Proof of Misbehavior Transactions executing this command have: -* `moduleID = MODULE_ID_DPOS` -* `commandID = COMMAND_ID_POM` +* `module = MODULE_NAME_DPOS` +* `command = COMMAND_NAME_POM` ##### Parameters @@ -681,11 +850,238 @@ pomParams = { ##### Verification -Both properties of the parameters must follow the block header schema as defined in [LIP "New block header and block asset schema"][lip-0055]. Validity of this transaction is then specified in [LIP 0024][lip-0024#validity-of-a-pom-transaction]. +Both properties of the parameters must follow the [block header schema `blockHeaderSchema`][lip-0055#block-header-json-schema] defined in LIP 0055. Validity of this transaction was previously specified in [LIP 0024][lip-0024#validity-of-a-pom-transaction]. For completeness, we include the pseudocode here. + + +```python +def verify(trs: Transaction) -> None: + b = block including trs + + header1 = trs.params.header1 + header2 = trs.params.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_VOTES: + raise Exception('Locking period has expired.') + if isPunished(header1.address, b.header.height): + raise Exception('Delegate is already punished.') + if delegateStore(header1.address).isBanned: + raise Exception('Delegate is banned.') + if verifyBlockSignature(header1) == False or verifyBlockSignature(header2) == False: + raise Exception('Invalid block signature') + if Bft.areHeadersContradicting(header1, header2) == False: + raise Exception('Block headers are not contradicting.') + +``` + +The [verifyBlockSignature function](#verifyblocksignature) is an internal function defined below. + + ##### Execution -Execution of this transaction is specified in [LIP 0024][lip-0024#applying-a-pom-transaction]. +Execution of this transaction was previously specified in [LIP 0024][lip-0024#applying-a-pom-transaction]. Here we update the specifications to be integrated in the [state model][lip-0040] used in Lisk. + +```python +def execute(trs: Transaction) -> None: + b = block including trs + h = b.header.height + senderAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # derive address from trs.senderPublicKey + + header1 = decode(blockHeaderSchema, trs.params.header1) + header2 = decode(blockHeaderSchema, trs.params.header2) + + punishedAddress = trs.params.header1.generatorAddress + + #update delegate substore + delegateStore(punishedAddress).pomHeights.append(h) + + # emit event for the delegate punishment. + emitEvent( + module=MODULE_NAME_DPOS, + name=EVENT_NAME_DELEGATE_PUNISHED, + data={ + "address": delegateAddress, + "height": h + }, + topics=[delegateAddress] + ) + + # check if the delegate should be banned. + if len(delegateStore(punishedAddress).pomHeights) == POM_LIMIT_BANNED: + delegateStore(punishedAddress).isBanned = True + # emit event for the delegate banning + emitEvent( + module=MODULE_NAME_DPOS, + name=EVENT_NAME_DELEGATE_BANNED, + data={ + "address": delegateAddress, + "height": h + }, + topics=[delegateAddress] + ) + + currentWeight = getDelegateWeight(punishedAddress) + updateDelegateEligibility(votedAddress, currentWeight) + + # assign the block reward to the sender of the transaction. + # the amount is taken from the punished delegate account. + # if punished delegate has less balance than the reward, all their balance is provided to the sender. + senderReward = min(reward.getBlockReward(b.header), Token.getAvailableBalance(punishedAddress,TOKEN_ID_REWARD)) + Token.transfer(punishedAddress, + senderAddress, + TOKEN_ID_REWARD, + senderReward) +``` + + + +### Events + +#### delegateRegistered + +This event has `name = EVENT_NAME_REGISTER_DELEGATE`. This event is emitted when a new delegate gets registered. + +##### Topics + +* `delegateAddress`: the address of the account registering a delegate. + +##### Data + +```java +delegateRegisteredDataSchema = { + "type": "object", + "required" = [ + "address", + "name" + ], + "properties": { + "address": { + "dataType": "bytes", + "length": ADDRESS_LENGTH, + "fieldNumber": 1 + }, + "name": { + "dataType": "string", + "fieldNumber": 2 + } + } +} +``` + + + +#### vote + +This event has `name = EVENT_NAME_VOTE_DELEGATE`. This event is emitted during the processing of each vote included in a vote transaction. + +##### Topics + +* `senderAddress`: the address of the account submitting the vote transaction. +* `delegateAddress`: the address of the account of the voted delegate. + +```java +voteDelegateDataSchema = { + "type": "object", + "required" = [ + "senderAddress", + "delegateAddress", + "amount", + "result" + ], + "properties": { + "senderAddress": { + "dataType": "bytes", + "length": ADDRESS_LENGTH, + "fieldNumber": 1 + }, + "delegateAddress": { + "dataType": "bytes", + "length": ADDRESS_LENGTH, + "fieldNumber": 2 + }, + "amount": { + "dataType": "uint64", + "fieldNumber": 3 + }, + "result": { + "dataType": "uint32", + "fieldNumber": 5 + } + } +} +``` + +#### delegatePunished + +This event has `name = EVENT_NAME_DELEGATE_PUNISHED`. This event is emitted when a delegate gets punished. + +##### Topics + +* `delegateAddress`: the address of the account of the punished delegate. + +##### Data + +```java +delegatePunishedDataSchema = { + "type": "object", + "required" = [ + "address", + "height" + ], + "properties": { + "address": { + "dataType": "bytes", + "length": ADDRESS_LENGTH, + "fieldNumber": 1 + }, + "height": { + "dataType": "uint32", + "fieldNumber": 2 + } + } +} +``` + +#### delegateBanned + +This event has `name = EVENT_NAME_DELEGATE_BANNED`. This event is emitted when a delegate gets banned. + +##### Topics + +* `delegateAddress`: the address of the account of the banned delegate. + +##### Data + +```java +delegatePunishedDataSchema = { + "type": "object", + "required" = [ + "address", + "height" + ], + "properties": { + "address": { + "dataType": "bytes", + "length": ADDRESS_LENGTH, + "fieldNumber": 1 + }, + "height": { + "dataType": "uint32", + "fieldNumber": 2 + } + } +} +``` + + + + ### Internal Functions @@ -698,8 +1094,8 @@ All blocks (with the exception of the genesis block) are part of a round. The fi This function returns the round number to which the input height belongs. ```python -roundNumber(h): - return ceiling((h - genesisHeight) / ROUND_LENGTH) +def roundNumber(h: uint32) -> uint32: + return math.ceil((h - genesisDataStore.height) / ROUND_LENGTH) ``` ##### isEndOfRound @@ -708,67 +1104,285 @@ This function returns a boolean indicating if the input height is at the end of ```python -isEndOfRound(h): - if (h - genesisHeight) % ROUND_LENGTH == 0: +def isEndOfRound(h: uint32) -> bool: + if (h - genesisDataStore.height) % ROUND_LENGTH == 0: return True else: return False ``` -#### delegateWeight +#### getDelegateWeight + +This function returns the weight of a given delegate (specified by the address). + +```python +def getDelegateWeight(address: Address) -> uint64: + return min(delegateStore(address).selfVotes * FACTOR_SELF_VOTES, + delegateStore(address).totalVotesReceived) +``` + +#### shuffleValidatorsList -The delegate weight is always a function of the votes, the potential misbehaviors and the block height. +A function to reorder the list of validators as specified in [LIP 0003][lip-0003]. ##### Parameters The function has the following input parameters in the order given below: -* `address`: A 20-byte addresses of a delegate. -* `height`: The height for which the weight is computed. +* `validatorsAddresses`: An array of pairwise distinct `Address` items (i.e., `ADDRESS_LENGTH`-byte addresses). +* `randomSeed`: A `SEED_LENGTH`-byte value representing a random seed. ##### Returns -This function returns the delegate weight. +This function returns an array of `Address` items, which is a re-ordered list of the input addresses. ##### Execution ```python -delegateWeight(address, height): - if there exist h in delegateStore(address).pomHeights with 0 < height - h < PUNISHMENT_WINDOW: - return 0 - else: - return min(delegateStore(address).selfVotes * FACTOR_SELF_VOTES, - delegateStore(address).totalVotesReceived) +def shuffleValidatorsList(validatorsAddresses: list[Address], randomSeed: bytes) -> list[Address]: + # checking pairwise distinct property + if validatorsAddresses != set(validatorsAddresses): + raise Exception('Validators list invalid (duplicate values detected)') + + roundHash = {} + for address in validatorsAddresses: + roundHash[address] = SHA256(randomSeed + address) # hashing concatenation of randomSeed and address + + + # Reorder the validator list + shuffledValidatorAddresses = sort validatorsAddresses where address1 < address2 if (roundHash(address1) < roundHash(address2)) + or ((roundHash[address1] == roundHash[address2]) and address1 < address2) + + return shuffledValidatorAddresses ``` -#### shuffleValidatorsList +#### updateDelegateEligibility -A function to reorder the list of validators as specified in [LIP 0003][lip-0003]. +A function that updates the eligible delegates substore to account for new changes of delegate properties. ##### Parameters -The function has the following input parameters in the order given below: +The function has the following input parameters: -* `validatorsAddresses`: An array of pairwise distinct 20-byte addresses. -* `randomSeed`: A 32-byte value representing a random seed. +* `address`: the address of a delegate. +* `oldWeight`: the weight of the delegate before the eligibility update. ##### Returns -This function returns an array of bytes with the re-ordered list of addresses. +This function does not return. ##### Execution ```python -shuffleValidatorsList(validatorsAddresses, randomSeed): - roundHash = {} - for address in validatorsAddresses: - roundHash[address] = hash(randomSeed || address) +def updateDelegateEligibility(address: Address, oldWeight: uint64) -> None: + # Always start by removing the old entry from the eligible + # delegate substore + oldKey = oldWeight.to_bytes(8,'big') + address + if the eligible delegate substore contains an entry for key = oldKey: + remove this entry from the store + + # If the delegate is eligible, add an entry to the eligible + # delegate substore. + weight = getDelegateWeight(address) + if (weight >= MIN_WEIGHT + and delegateStore(address).isBanned == False + and Validators.getValidatorAccount(address).blsKey != INVALID_BLS_KEY + and Validators.getValidatorAccount(address).generatorKey != INVALID_ED25519_KEY): + newKey = weight.to_bytes(8,'big') + address + create an entry in the eligible delegate substore with key = newKey +``` - # Reorder the validator list - shuffledValidatorAddresses = sort validatorsAddresses where address1 < address2 if (roundHash(address1) < roundHash(address2)) - or ((roundHash(address1) == roundHash(address2)) and address1 < address2) +#### verifyBlockSignature - return shuffledValidatorAddresses +Checks whether a block header is validly signed. + +##### Execution + +```python +def verifyBlockSignature(header: Header) -> bool: + generatorKey = Validators.getValidatorAccount(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][lip-0062#specification]. + +#### isDelegateNameValid + +Checks whether a given string would be a valid delegate name. + +##### Execution + +```python +def isDelegateNameValid(delegateName: 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 delegateName)) + or len(delegateName) < 1 + or len(delegateName) > MAX_LENGTH_NAME): + + return False + + return True +``` + +##### isPunished + +This function returns a boolean indicating if a delegate is punished at a certain height of not. + + +```python +def isPunished(address: Address, height: uint32) -> bool: + if delegateStore(address).pomHeights is empty: + return False + + let lastPomHeight be the last element of delegateStore(address).pomHeights + + if height <= lastPomHeight + PUNISHMENT_WINDOW_SELF_VOTES: + return True + + return False +``` + + +#### getActiveDelegates + +This function computes the set of active delegates based on the input set of eligible delegates. + +```python +def getActiveDelegates(validatorsTwoRoundsAgo: list[Address], roundNumber: uint32) -> list[Address]: + initRounds = genesisDataStore.initRounds + initDelegates = genesisDataStore.initDelegates + + # During the first NUMBER_ACTIVE_DELEGATES rounds after the bootstrap period + # the initial delegates are only partly replaced by elected delegates. + # During this phase, there are no selected standby delegates. + + if roundNumber < initRounds + NUMBER_ACTIVE_DELEGATES: + nbrInitValidators = initRounds + NUMBER_ACTIVE_DELEGATES - roundNumber + nbrElectedValidators = NUMBER_ACTIVE_DELEGATES - nbrInitValidators + + # In the definition below, recall that validatorsTwoRoundsAgo is sorted by delegate weight. + electedValidators = validatorsTwoRoundsAgo[:nbrElectedValidators] + + remainingInitDelegates = [address for address in initDelegates if address not in electedValidators] + + # concatenation of elected validators and remaining initial delegates + activeDelegates = electedValidators + remainingInitDelegates[:nbrInitValidators] + + else: + # If validatorsTwoRoundsAgo contains less than NUMBER_ACTIVE_DELEGATES entries + # there will be less than NUMBER_ACTIVE_DELEGATES active delegates. + # Recall that validatorsTwoRoundsAgo is sorted by delegate weight. + if len(validatorsTwoRoundsAgo) <= NUMBER_ACTIVE_DELEGATES: + activeDelegates = validatorsTwoRoundsAgo + else: + activeDelegates = validatorsTwoRoundsAgo[:NUMBER_ACTIVE_DELEGATES] + + return activeDelegates +``` + +#### getSelectedStandbyDelegates + +This function selects the standby delegates for a round. + +```python +def getSelectedStandbyDelegates(standbyDelegates: list[dict[str,bytes]], height: uint32) -> list[Address]: + # We now compute the randomness used for selecting the first standby delegate. + randomSeed1 = random.getRandomBytes( + height +1 - (ROUND_LENGTH*3)//2, + ROUND_LENGTH + ) + + if NUMBER_STANDBY_DELEGATES == 2 and len(standbyDelegates) >=2: + randomSeed2 = random.getRandomBytes( + height +1 - 2*ROUND_LENGTH, + ROUND_LENGTH + ) + selectedStandbyDelegates = select 2 address from standbyDelegates + as specified in LIP 0022, using the seeds randomSeed1 and randomSeed2 + elif NUMBER_STANDBY_DELEGATES >= 1 and len(standbyDelegates) >= 1: + selectedStandbyDelegates = select 1 address from standbyDelegates + as specified in LIP 0022, using the seed randomSeed1 + else: # No standby delegates + selectedStandbyDelegates = empty + + return selectedStandbyDelegates +``` + +### Protocol Logic for Other Modules + +More functions might be made available during implementation. + +#### getVoter + +Returns the stored information relative to the given address. + +```python +def getVoter(address: Address)-> VoterStoreObject: + return voterStore(address) +``` + +#### getDelegate + +Returns the stored information relative to the given address. + +```python +def getDelegate(address: Address)-> DelegateStoreObject: + return delegateStore(address) +``` + +### Endpoints for Off-Chain Services + +#### isNameAvailable + +Asserts the availability of a given name for delegate registration. + +```python +def isNameAvailable(name: str) -> bool: + + if (not isDelegateNameValid(name)) + or (nameStore(name) exists): + return False + else: + return True +``` + + +#### getVoter + +Same as the [getVoter function](#getvoter) of the previous section. + +#### getDelegate + +Same as the [getDelegate function](#getdelegate) of the previous section. + +#### getAllDelegates + +Returns information of all delegates. + +##### Execution + +```python +def getAllDelegates()-> list[DelegateStoreObject]: + return [decode(delegateStoreSchema,delegateStore(address)) for delegateStore(address) in delegate substore] +``` + +#### getPendingUnlocks + +Returns the list of pending unlocks for the given address. + +##### Execution + +```python +def getPendingUnlocks(address: Address)-> UnlockObject: + return voterStore(address).pendingUnlocks ``` ### Genesis Block Processing @@ -778,7 +1392,7 @@ shuffleValidatorsList(validatorsAddresses, randomSeed): ```java genesisDPoSStoreSchema = { "type": "object", - "required": ["validators", "voters", "snapshots", "genesisData"], + "required": ["validators", "voters", "genesisData"], "properties": { "validators": { "type": "array", @@ -799,6 +1413,7 @@ genesisDPoSStoreSchema = { "properties": { "address": { "dataType": "bytes", + "length": ADDRESS_LENGTH, "fieldNumber": 1 }, "name": { @@ -807,14 +1422,17 @@ genesisDPoSStoreSchema = { }, "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": { @@ -857,6 +1475,7 @@ genesisDPoSStoreSchema = { "properties": { "delegateAddress": { "dataType": "bytes", + "length": ADDRESS_LENGTH, "fieldNumber": 1 }, "amount": { @@ -879,6 +1498,7 @@ genesisDPoSStoreSchema = { "properties": { "delegateAddress": { "dataType": "bytes", + "length": ADDRESS_LENGTH, "fieldNumber": 1 }, "amount": { @@ -895,47 +1515,6 @@ genesisDPoSStoreSchema = { } } }, - "snapshots": { - "type": "array", - "fieldNumber": 3, - "items": { - "type": "object", - "required": [ - "roundNumber", - "activeDelegates", - "delegateWeightSnapshot" - ], - "properties": { - "roundNumber": { - "dataType": "uint32", - "fieldNumber": 1 - }, - "activeDelegates": { - "type": "array", - "fieldNumber": 2, - "items": { "dataType": "bytes" } - }, - "delegateWeightSnapshot": { - "type": "array", - "fieldNumber": 3, - "items": { - "type": "object", - "required": ["delegateAddress", "delegateWeight"], - "properties": { - "delegateAddress": { - "dataType": "bytes", - "fieldNumber": 1 - }, - "delegateWeight": { - "dataType": "uint64", - "fieldNumber": 2 - } - } - } - } - } - } - }, "genesisData": { "type": "object", "fieldNumber": 4, @@ -948,7 +1527,10 @@ genesisDPoSStoreSchema = { "initDelegates": { "type": "array", "fieldNumber": 2, - "items": { "dataType": "bytes" } + "items": { + "dataType": "bytes", + "length": ADDRESS_LENGTH + } } } } @@ -962,32 +1544,25 @@ During the genesis state initialization stage, the following steps are executed. Let `genesisBlockAssetBytes` be the `data` bytes included in the block assets for the DPoS module and let `genesisBlockAssetObject` be the deserialization of `genesisBlockAssetBytes` according to the `genesisDPoSStoreSchema` schema, given above. + * Initial checks on the properties of `genesisBlockAssetObject`: + * `genesisBlockAssetObject` should satisfy the [`genesisDPoSStoreSchema` schema](#genesis-assets-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: - * `address` values must have length `ADDRESS_LENGTH`. - * `name` values must have length between `1` and `MAX_LENGTH_NAME` (included), and must contain only characters from the set `abcdefghijklmnopqrstuvwxyz0123456789!@$&_.`. - * `generatorKey` values must have length 32. - * `blsKey` values must have length 48. - * `proofOfPossession` values must have length 96. - * Across elements of the `voters` array, all `address` values must be unique, and must have length `ADDRESS_LENGTH`. + * For all elements of the `validators` array, `name` values must statisfy `isDelegateNameValid(name) == True`. + * Across elements of the `voters` array, all `address` values must be unique. * For all elements of the `voters` array: - * Across elements of the `sentVotes` array, all `delegateAddress` values must be unique, and must have length `ADDRESS_LENGTH`. + * Either `sentVotes != []` or `pendingUnlocks != []`. + * All `amounts` properties in elements of the `sentVotes` or the `pendingUnlocks` arrays must be non-zero. + * Across elements of the `sentVotes` array, all `delegateAddress` values must be unique. * For each element `sentVote` in the `sentVotes` array, there is an element `validator` in the `validators` array with `validator.address == sentVote.delegateAddress`. * `sentVotes` has size is at most `MAX_NUMBER_SENT_VOTES`. * `sentVotes` must be in lexicographic order of `delegateAddress`. * `pendingUnlocks` has size is at most `MAX_NUMBER_PENDING_UNLOCKS`. * `pendingUnlocks` must be ordered by lexicographical order of `delegateAddress`, ties then broken by increasing `amount`, ties finally broken by increasing `unvoteHeight`. * For each element `pendingUnlock` in the `pendingUnlocks` array, there is an element `validator` in the `validators` array with `validator.address == pendingUnlock.delegateAddress`. - * The `snapshots` array must have length less than or equal to 3. - * Across elements of the `snapshots` array, `roundNumber` values must be unique. - * Across elements of the `snapshots` array, - * All values of the `activeDelegates` array must be unique, and must have length `ADDRESS_LENGTH`. - * For each element `activeDelegate` in the `activeDelegates` array, there is an element `validator` in the `validators` array with `validator.address == activeAddress`. - * Across elements of the `delegateWeightSnapshot` array, all values for the `delegateAddress` property must be unique, must have length `ADDRESS_LENGTH`. - * For each element `weightSnapshot` in the `delegateWeightSnapshot` array, there is an element `validator` in the `validators` array with `validator.address == weightSnapshot.delegateAddress`. - * All values of the `genesisData.initDelegates` array must be unique, must have length `ADDRESS_LENGTH`, and must be equal to `validator.address` for `validator` an element of `validators`. - * The `genesisData.initDelegates` array must have length less than or equal to `NUMBER_ACTIVE_DELEGATES`. + * All values of the `genesisData.initDelegates` array must be unique and must be equal to `validator.address` for a `validator` element of `validators`. + * The `genesisData.initDelegates` array must have length equal to `NUMBER_ACTIVE_DELEGATES` 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 delegate substore with: ```python totalVotesReceived = 0 @@ -997,60 +1572,64 @@ Let `genesisBlockAssetBytes` be the `data` bytes included in the block assets fo totalVotesReceived += sentVote.amount if voter.address == validator.address: selfVotes = sentVote.amount - if totalVotesReceived >= 2^64: - fail - storeKey = validator.address - storeValue = { + delegateState = { "name": validator.name, "totalVotesReceived": totalVotesReceived, "selfVotes": selfVotes, "lastGeneratedHeight": validator.lastGeneratedHeight, "isBanned": validator.isBanned, - "pomHeights": validator.pomHeight, + "pomHeights": validator.pomHeights, "consecutiveMissedBlocks": validator.consecutiveMissedBlocks - } serialized using delegateStoreSchema. + } + storeKey = validator.address + storeValue = encode(delegateStoreSchema, delegateState) ``` + Further, for every entry `validator` in `genesisBlockAssetObject.validators`, also create an entry in the name substore with: ```python - storeKey = validator.name utf8-encoded - storeValue = { + + delegateState = { "delegateAddress": validator.address - } serialized using nameStoreSchema. + } + + storeKey = validator.name utf8-encoded + storeValue = encode(nameStoreSchema, delegateState) ``` + + For each entry `validator` in `genesisBlockAssetObject.validators`, call `updateDelegateEligibility(validator.address, 0)`. + * For each entry `voter` in `genesisBlockAssetObject.voters`, create an entry in the voter substore with: ```python - storeKey = voter.address - storeValue = { + + voterState = { "sentVotes": voter.sentVotes, "pendingUnlocks": voter.pendingUnlocks - } serialized using voterStoreSchema. - ``` -* For each entry `snapshot` in `genesisBlockAssetObject.snapshots`, create an entry in the snapshot substore with: - ```python - storeKey = uint32be(snapshot.roundNumber) - storeValue = { - "activeDelegates": snapshot.activeDelegates, - "delegateWeightSnapshot": snapshot.delegateWeightSnapshot - } serialized using snapshotStoreSchema. + } + storeKey = voter.address + storeValue = encode(voterStoreSchema, voterState) ``` * Create an entry in the genesis data substore with: ```python - storeKey = EMPTY_BYTES - storeValue = { + genesisState = { "height": block header height of the genesis block, "initRounds": genesisBlockAssetObject.genesisData.initRounds, "initDelegates": genesisBlockAssetObject.genesisData.initDelegates - } serialized using genesisDataStoreSchema. + } + + storeKey = EMPTY_BYTES + storeValue = endcode(genesisDataStoreSchema, genesisState) ``` * Create an entry in the previous timestamp substore with: ```python - storeKey = EMPTY_BYTES - storeValue = { + + timestampState = { "timestamp": block header height of the genesis block - } serialized using previousTimestampStoreSchema + } + storeKey = EMPTY_BYTES + storeValue = encode(previousTimestampStoreSchema, timestampState) ``` #### Genesis State Finalization @@ -1066,23 +1645,19 @@ As in the previous point, let `genesisBlockAssetBytes` be the `data` bytes inclu # this snapshot block do not have a BLS key. if the chain is the mainchain and b is the snapshot block for the migration: for validator in genesisBlockAssetObject.validators: - validators.registerValidatorWithoutBLSKey( + Validators.registerValidatorWithoutBLSKey( validator.address, validator.generatorKey ) - if the above returns False: - fail else: # For any other genesis block, register validators with a BLS key for validator in genesisBlockAssetObject.validators: - validators.registerValidatorKeys( + Validators.registerValidatorKeys( validator.address, validator.proofOfPossession, validator.generatorKey, validator.blsKey ) - if the above returns False: - fail # Check that all sentVotes and pendingUnlocks correspond to locked tokens for address a key of the voter substore: @@ -1092,34 +1667,24 @@ for address a key of the voter substore: for pendingUnlock in voter(address).pendingUnlocks: votedAmount += pendingUnlock.amount - if token.getLockedAmount(address, MODULE_ID_DPOS, TOKEN_ID_DPOS) != votedAmount: - fail + if Token.getLockedAmount(address, MODULE_NAME_DPOS, TOKEN_ID_DPOS) != votedAmount: + raise Exception('Locked values do not match') -# Set the initial delegates in the BFT module +# set the initial delegates in the BFT module +# recall that initDelegate is always in lexicographical order initDelegates = genesisBlockAssetObject.genesisData.initDelegates bftWeights = [ {"address": address, "bftWeight": 1} - for address in initDelegates sorted by lexicographically by address + for address in initDelegates ] -# Compute the initial BFT threshold -initBFTThreshold = floor(2/3 * length(initDelegates)) + 1 - # Initialize the BFT module store -bft.setBFTParameters(initBFTThreshold, - initBFTThreshold, +bft.setBFTParameters(BFT_THRESHOLD, + BFT_THRESHOLD, bftWeights) # Set the initial delegates in the validators module -validators.setGeneratorList(initDelegates) - -# Check that the snapshot only correspond to the last three rounds -# note that if the genesis height == 0 and the snapshot store is non-empty, this will fail -let h be the header height of the genesis block -genesisRound = roundNumber(h) -snapshotKeys = array of the store keys (converted to uint32) of the snapshot substore ordered increasingly -if snapshotKeys is not an incrementing array or snapshotKeys[-1] != genesisRound: - fail +Validators.setGeneratorList(initDelegates) ``` ### Block Processing @@ -1129,193 +1694,93 @@ if snapshotKeys is not an incrementing array or snapshotKeys[-1] != genesisRound After the transactions in a block `b` are executed, the properties related to missed blocks are updated according to [Delegate Productivity][lip-0023#delegate-productivity]. This logic is recapitulated below: ```python -newHeight = b.header.height -# previousTimestamp is the value in the previous timestamp substore -missedBlocks = validators.getGeneratorsBetweenTimestamps(previousTimestamp, b.header.timestamp) - -for address in missedBlocks: - delegateStore(address).consecutiveMissedBlocks += missedBlocks[address] - - # The rule below was introduced in LIP 0023 - if (delegateStore(address).consecutiveMissedBlocks > FAIL_SAFE_MISSED_BLOCKS - and newHeight - delegateStore(address).lastGeneratedHeight > FAIL_SAFE_INACTIVE_WINDOW): - delegateStore(address).isBanned = True - -delegateStore(b.header.generatorAddress).consecutiveMissedBlocks = 0 -delegateStore(b.header.generatorAddress).lastGeneratedHeight = newHeight - -previousTimestamp = b.header.timestamp -``` - -If the block `b` is an end-of-round block (`isEndOfRound(b.header.height) == True`), the following logic is executed (this must be done after the properties related to missed blocks are updated): - -```python -roundNumber = roundNumber(b.header.height - genesisHeight) -currentWeights = {} -for address being a storeKey in delegate substore and delegateStore(address).isBanned == False: - currentWeights[address] = delegateWeight(address, b.header.height) - -activeDelegates = array of the top 101 address by decreasing delegateWeight from currentWeights, ties broken by lexicographical ordering of the address - -# If currentWeights contains less than 101 entries -# there will be less than 101 activeDelegates -remove all entries from currentWeights with address in activeDelegates - -sort currentWeights by decreasing delegateWeight, ties broken by lexicographical order of the delegateAddress - -weightSnapshot = [] -for each address being a key of currentWeights (keys taken in order): - if currentWeights[address] >= MIN_WEIGHT_STANDBY: - weightSnapshot.append({"delegateAddress": address, - "delegateWeight": currentWeights[address]}) - else: - # Only triggered if there not enough addresses with weight MIN_WEIGHT_STANDBY as currentWeights is sorted - if length(weightSnapshot) < 2: - weightSnapshot.append({"delegateAddress": address, - "delegateWeight": currentWeights[address]}) - -create an entry in the snapshot substore with - storeKey = uint32be(roundNumber), - storeValue = { - "activeDelegates": activeDelegates, - "delegateWeightSnapshot": weightSnapshot - } serialized using snapshotStoreSchema -delete any entries from the snapshot substore with storeKey <= uint32be(roundNumber-3) - -# Updates to Validators and BFT module are only done after the bootstrap period -if roundNumber > initRounds: - validatorsTwoRoundsAgo = deserialized value of the snapshot substore entry with storeKey == uint32be(roundNumber-2) - activeDelegates = validatorsTwoRoundsAgo.activeDelegates - +def afterTransactionsExecute(b: Block) -> 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: + delegateStore(address).consecutiveMissedBlocks += missedBlocks[address] + + # The rule below was introduced in LIP 0023 + if (delegateStore(address).consecutiveMissedBlocks > FAIL_SAFE_MISSED_BLOCKS + and height - delegateStore(address).lastGeneratedHeight > FAIL_SAFE_INACTIVE_WINDOW): + delegateStore(address).isBanned = True + updateDelegateEligibility(address) + + delegateStore(b.header.generatorAddress).consecutiveMissedBlocks = 0 + delegateStore(b.header.generatorAddress).lastGeneratedHeight = height + + # update previousTimestamp substore + previousTimestamp = b.header.timestamp + + if isEndOfRound(height) == False: + return + + # block b is an end-of-round block + # this must be done after the properties related to missed blocks are updated + roundNumber = roundNumber(height) + + eligibleDelegates = [ + {"address": key[-ADDRESS_LENGTH:], "weight":key[:-ADDRESS_LENGTH]} + for key a substore key of the eligible substore + ] 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 = { + "delegateWeightSnapshot": eligibleDelegates + } + create an entry in the snapshot substore with + storeKey = roundNumber.to_bytes(4,'big'), + storeValue = encode(snapshotStoreSchema, snapshotState) + delete any entries from the snapshot substore snapshotStore(x) for x <= roundNumber-3 + + # Updates to Validators and BFT module are only done after the bootstrap period + if roundNumber <= genesisDataStore.initRounds: + return + + # Calculate the active delegates. Exclude punished ones from the snapshot + validatorsTwoRoundsAgo = [item for item in snapshotStore(roundNumber-2) if isPunished(item["address"], height)==False] + activeDelegates = getActiveDelegates([item["address"] for item in validatorsTwoRoundsAgo], roundNumber) + validators = activeDelegates + + # update BFT bftWeights = [{ - "address": address, + "address": item["address"], "bftWeight": 1 - } for address in activeDelegates, sorted by lexicographically by address] + } for item in activeDelegates] - # Get the last stored BFT parameters, and update them if needed - currentBFTParameters = BFT.getBFTParameters(b.header.height) + # Get the last stored BFT parameters, and update them if needed. + currentBFTParameters = BFT.getBFTParameters(height) if (currentBFTParameters.validators != bftWeights or currentBFTParameters.precommitThreshold != BFT_THRESHOLD or currentBFTParameters.certificateThreshold != BFT_THRESHOLD): BFT.setBFTParameters(BFT_THRESHOLD, - BFT_THRESHOLD, - bftWeights) - - if NUMBER_STANDBY_DELEGATES == 2: - randomSeed1 = random.getRandomBytes(b.header.height +1 - (ROUND_LENGTH*3)//2, - ROUND_LENGTH) - randomSeed2 = random.getRandomBytes(b.header.height +1 - 2*ROUND_LENGTH, - ROUND_LENGTH) - delegate1, delegate2 = addresses of the standby delegates selected from validatorsTwoRoundsAgo.delegateWeightSnapshot - as specified in LIP 0022, using the seeds randomSeed1 and randomSeed2 - # In the above, if validatorsTwoRoundsAgo.delegateWeightSnapshot is empty, then no standby delegates are selected - validators = union of activeDelegates and {delegate1, delegate2} - - elif NUMBER_STANDBY_DELEGATES == 1: - randomSeed1 = random.getRandomBytes(b.header.height +1 - (ROUND_LENGTH*3)//2, - ROUND_LENGTH) - delegate1 = address of the standby delegates selected from validatorsTwoRoundsAgo.delegateWeightSnapshot - as specified in LIP 0022, using the seed randomSeed1 - validators = union of activeDelegates and {delegate1} - else: # No standby delegates - randomSeed1 = random.getRandomBytes(b.header.height +1 - (ROUND_LENGTH*3)//2, - ROUND_LENGTH) - validators = activeDelegates - - nextValidators = shuffleValidatorsList(validators, randomSeed1) - validators.setGeneratorList(nextValidators) -``` - -### Protocol Logic for Other Modules - -More functions might be made available during implementation. - -#### isNameAvailable - -Asserts the availability of a given name for delegate registration. - -##### Parameters - -* `name`: A string being asserted for availability. - -##### Returns - -A boolean asserting the availability of the given name for delegate registration. - -##### Execution - -```python -isNameAvailable(name): - if (nameStore(name) exists - or name includes symbols not in "abcdefghijklmnopqrstuvwxyz0123456789!@$&_." - or length(name) > MAX_LENGTH_NAME - or length(name) < 1): - return False - else: - return True + BFT_THRESHOLD, + bftWeights) + + # select standby delegates if relevant. + if roundNumber > initRounds + NUMBER_ACTIVE_DELEGATES: + standbyDelegates = [validator for validator in validatorsTwoRoundsAgo + if validator["address"] not in activeDelegates] + + selectedStandbyDelegates = getSelectedStandbyDelegates(standbyDelegates, height) + # add selected standby delegates to validators + validators += selectedStandbyDelegates + + # random seed to shuffle validators + randomSeed = random.getRandomBytes( + height +1 - (ROUND_LENGTH*3)//2, + ROUND_LENGTH + ) + + if validators is not empty: + nextValidators = shuffleValidatorsList(validators, randomSeed) + Validators.setGeneratorList(nextValidators) ``` -#### getVoter - -Returns the stored information relative to the given address. - -##### Parameters - -* `address`: A 20-byte value identifying the voter. - -##### Returns - -This functions returns `voterStore(address)` deserialized using `voterStoreSchema`. - -#### getDelegate - -Returns the stored information relative to the given address. - -##### Parameters - -* `address`: A 20-byte value identifying the delegate. - -##### Returns - -This functions returns `delegateStore(address)` deserialized using `delegateStoreSchema`. - -### Endpoints for Off-Chain Services - -#### getVoter - -Returns voter information for the given address. - -##### Parameters - -* `address`: A 20-byte value identifying the voter. - -##### Returns - -This function returns `voterStore(address)` deserialized using `voterStoreSchema` - -#### getDelegate - -Returns delegate information for the given address. - -##### Parameters - -* `address`: A 20-byte value identifying the delegate. - -##### Returns - -This functions returns `delegateStore(address)` deserialized using `delegateStoreSchema`. - -#### getAllDelegates - -Returns information of all delegates. - -##### Parameters - -This function has no input parameter. - -##### Returns - -This function returns all `delegateStore` items deserialized using `delegateStoreSchema`. ## Backwards Compatibility @@ -1329,6 +1794,7 @@ TBA [lip-0022]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0022.md [lip-0023]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0023.md [lip-0023#delegate-productivity]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0023.md#delegate-productivity-1 +[lip-0023#locking-period]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0023.md#voting-by-locking-tokens [lip-0023#explicit-unlock-mechanism]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0023.md#explicit-unlock-mechanism [lip-0023#new-unlock-transaction]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0023.md#new-unlock-transaction [lip-0023#new-vote-transaction]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0023.md#new-vote-transaction-1 @@ -1336,7 +1802,17 @@ TBA [lip-0024#rationale]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0024.md#rationale [lip-0024#validity-of-a-pom-transaction]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0024.md#validity-of-a-pom-transaction [lip-0034#bootstrap-period]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0034.md#bootstrap-period +[lip-0037#chain-identifiers]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0037.md#chain-identifiers +[lip-0038#public-key-registration]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0038.md#public-key-registration-and-proof-of-possession +[lip-0040]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0040.md +[lip-0042]:https://github.com/LiskHQ/lips/blob/main/proposals/lip-0042.md +[lip-0043#chainid]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0043.md#chain-id [lip-0044]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0044.md [lip-0045#constants]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0045.md#notation-and-constants -[lip-0059]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0059.md +[lip-0051]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0051.md +[lip-0051#tokenID]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0051.md#token-id-and-native-tokens [lip-0055]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0055.md +[lip-0055#block-header-json-schema]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0055.md#block-header-json-schema +[lip-0059]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0059.md +[lip-0062#specification]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0062.md#specification +[lip-0068]: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0068.md From c45ea1c4794a19d3f010c2cda3006cf421a92adc Mon Sep 17 00:00:00 2001 From: gkoumout <48478060+gkoumout@users.noreply.github.com> Date: Wed, 21 Sep 2022 13:20:38 +0300 Subject: [PATCH 2/4] add check for empty validators list --- proposals/lip-0057.md | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/proposals/lip-0057.md b/proposals/lip-0057.md index 395ff5d68..72da77499 100644 --- a/proposals/lip-0057.md +++ b/proposals/lip-0057.md @@ -1746,6 +1746,26 @@ def afterTransactionsExecute(b: Block) -> None: activeDelegates = getActiveDelegates([item["address"] for item in validatorsTwoRoundsAgo], roundNumber) validators = activeDelegates + # select standby delegates if relevant. + if roundNumber > initRounds + NUMBER_ACTIVE_DELEGATES: + standbyDelegates = [validator for validator in validatorsTwoRoundsAgo + if validator["address"] not in activeDelegates] + + selectedStandbyDelegates = getSelectedStandbyDelegates(standbyDelegates, height) + # add selected standby delegates to validators + validators += selectedStandbyDelegates + + # random seed to shuffle validators + randomSeed = random.getRandomBytes( + height +1 - (ROUND_LENGTH*3)//2, + ROUND_LENGTH + ) + + # if there are no eligible delegates, validators array is empty + # in this case, bft parameters and validators are not updated. + if validators is empty: + return + # update BFT bftWeights = [{ "address": item["address"], @@ -1761,24 +1781,9 @@ def afterTransactionsExecute(b: Block) -> None: BFT_THRESHOLD, bftWeights) - # select standby delegates if relevant. - if roundNumber > initRounds + NUMBER_ACTIVE_DELEGATES: - standbyDelegates = [validator for validator in validatorsTwoRoundsAgo - if validator["address"] not in activeDelegates] - - selectedStandbyDelegates = getSelectedStandbyDelegates(standbyDelegates, height) - # add selected standby delegates to validators - validators += selectedStandbyDelegates - - # random seed to shuffle validators - randomSeed = random.getRandomBytes( - height +1 - (ROUND_LENGTH*3)//2, - ROUND_LENGTH - ) - - if validators is not empty: - nextValidators = shuffleValidatorsList(validators, randomSeed) - Validators.setGeneratorList(nextValidators) + # update validators list + nextValidators = shuffleValidatorsList(validators, randomSeed) + Validators.setGeneratorList(nextValidators) ``` From afbb4ef8963f5e76d69937af84810c047c482783 Mon Sep 17 00:00:00 2001 From: Oliver Beddows Date: Mon, 26 Sep 2022 12:46:19 +0200 Subject: [PATCH 3/4] :nail_care: Apply standards --- proposals/lip-0057.md | 417 +++++++++++++++++++----------------------- 1 file changed, 185 insertions(+), 232 deletions(-) diff --git a/proposals/lip-0057.md b/proposals/lip-0057.md index 72da77499..ae8c2ded8 100644 --- a/proposals/lip-0057.md +++ b/proposals/lip-0057.md @@ -71,90 +71,82 @@ This part of the state store is used to maintain the timestamp of the last block 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][lip-0037#chain-identifiers] of the chain. | -| **DPoS store constants** | | | | -| `STORE_PREFIX_VOTER` | bytes | 0x0000 | The store prefix of the voter substore. | -| `STORE_PREFIX_DELEGATE` | bytes | 0x4000 | The store prefix of the delegate substore. | -| `STORE_PREFIX_NAME` | bytes | 0x8000 | The store prefix of the name substore. | -| `STORE_PREFIX_SNAPSHOT` | bytes | 0xd000 | The store prefix of the snapshot substore. | -| `STORE_PREFIX_GENESIS_DATA` | bytes | 0xc000 | The store prefix of the genesis data substore. | -| `STORE_PREFIX_PREVIOUS_TIMESTAMP` | bytes | 0xe000 | The store prefix of the previous timestamp substore. | -| `STORE_PREFIX_ELIGIBLE_DELEGATES` | bytes | 0xf000 | The store prefix of the eligible delegates substore. | -| **DPoS constants** | | | | -| `MODULE_NAME_DPOS` | string | "dpos" | The module name of the DPoS module. | -| `COMMAND_NAME_DELEGATE_REGISTRATION` | string | "delegateRegistration"| The command name of the delegate registration transaction. | -| `COMMAND_NAME_VOTE` | string | "vote" | The command name of the vote transaction. | -| `COMMAND_NAME_UNLOCK` | string | "unlock" | The command name of the unlock transaction. | -| `COMMAND_NAME_POM` | string | "pom" | The command name of the proof-of-misbehavior transaction. | -| **Configurable Constants** | | **Mainchain Value** | | -| `FACTOR_SELF_VOTES` | uint32 | `10` | The factor multiplying the self-votes of a delegate for the delegate weight computation. | -| `BASE_VOTE_AMOUNT` | uint32 | `10 * (10)^8` | The minimum voting amount. All voted amounts should be multiples of this value. | -| `MAX_LENGTH_NAME` | uint32 | `20` | The maximum allowed name length for delegates. | -| `MAX_NUMBER_SENT_VOTES` | uint32 | `10` | The maximum size of the sentVotes array of a voter substore entry. | -| `MAX_NUMBER_PENDING_UNLOCKS` | uint32 | `20` | The maximum size of the pendingUnlocks array of a voter 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 | `130,000` | The length of the inactivity window used in the fail safe banning mechanism. | -| `LOCKING_PERIOD_VOTES` | uint32 | `26,000` | The [locking period][lip-0023#explicit-unlock-mechanism] for regular votes. | -| `LOCKING_PERIOD_SELF_VOTES` | uint32 | `260,000` | The [locking period][lip-0023#explicit-unlock-mechanism] for self-votes. | -| `PUNISHMENT_WINDOW_VOTES` | uint32 | `260,000` | The punishment time for votes on punished delegates. | -| `PUNISHMENT_WINDOW_SELF_VOTES` | uint32 | `780,000` | The punishment time for self-votes of punished delegates. | -| `POM_LIMIT_BANNED` | uint32 | `5` | The number of proof-of-misbehavior transactions against a delegate for getting banned. | -| `MIN_INIT_ROUNDS` | uint32 | `3` | The minimum number of rounds for the [bootstrap period][lip-0034#bootstrap-period] | -| `BFT_THRESHOLD` | uint32 | `68` | The precommit and certificate thresholds used by the BFT module. This constant must equal `floor(2/3 * NUMBER_ACTIVE_DELEGATES) + 1` | -| `MIN_WEIGHT` | uint64 | `1000*(10^8)` | The minimum delegate weight required to be selected as a block generator. | -| `NUMBER_ACTIVE_DELEGATES` | uint32 | `101` | The number of active delegates. | -| `NUMBER_STANDBY_DELEGATES` | uint32 | `2` | The number of standby delegates. This LIP is specified for the number of standby delegates being 0, 1 or 2. | -| `ROUND_LENGTH` | uint32 | `103` | The round length. Is equal to `NUMBER_ACTIVE_DELEGATES` + `NUMBER_STANDBY_DELEGATES` | -| `TOKEN_ID_DPOS` | bytes | `TOKEN_ID_LSK = 0x 00 00 00 00 00 00 00 00` | The [token ID][lip-0051#tokenID] of the token used to cast votes. | -| `TOKEN_ID_FEE` | bytes | `TOKEN_ID_LSK = 0x 00 00 00 00 00 00 00 00` | The [token ID][lip-0051#tokenID] of the token used to pay the transaction fees. Defined in the fee module. | -| `TOKEN_ID_REWARD` | bytes |`TOKEN_ID_LSK = 0x 00 00 00 00 00 00 00 00`| The [token ID][lip-0051#tokenID] of the token used for block rewards. Specified as part of the [reward module][lip-0042] configuration. | -| `DELEGATE_REGISTRATION_FEE` | uint64 | `10*(10^8)` | The extra command fee of the delegate registration. | -| `INVALID_BLS_KEY ` | bytes | 48 bytes all set to 0x00 | The byte value associated with validators that did not register a BLS key. | - - +| 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][lip-0037#chain-identifiers] of the chain. | +| **DPoS store constants** | | | | +| `STORE_PREFIX_VOTER` | bytes | 0x0000 | The store prefix of the voter substore. | +| `STORE_PREFIX_DELEGATE` | bytes | 0x4000 | The store prefix of the delegate substore. | +| `STORE_PREFIX_NAME` | bytes | 0x8000 | The store prefix of the name substore. | +| `STORE_PREFIX_SNAPSHOT` | bytes | 0xd000 | The store prefix of the snapshot substore. | +| `STORE_PREFIX_GENESIS_DATA` | bytes | 0xc000 | The store prefix of the genesis data substore. | +| `STORE_PREFIX_PREVIOUS_TIMESTAMP` | bytes | 0xe000 | The store prefix of the previous timestamp substore. | +| `STORE_PREFIX_ELIGIBLE_DELEGATES` | bytes | 0xf000 | The store prefix of the eligible delegates substore. | +| **DPoS constants** | | | | +| `MODULE_NAME_DPOS` | string | "dpos" | The module name of the DPoS module. | +| `COMMAND_NAME_DELEGATE_REGISTRATION` | string | "delegateRegistration"| The command name of the delegate registration transaction. | +| `COMMAND_NAME_VOTE` | string | "vote" | The command name of the vote transaction. | +| `COMMAND_NAME_UNLOCK` | string | "unlock" | The command name of the unlock transaction. | +| `COMMAND_NAME_POM` | string | "pom" | The command name of the proof-of-misbehavior transaction. | +| **Configurable Constants** | | **Mainchain Value** | | +| `FACTOR_SELF_VOTES` | uint32 | `10` | The factor multiplying the self-votes of a delegate for the delegate weight computation. | +| `BASE_VOTE_AMOUNT` | uint32 | `10 * (10)^8` | The minimum voting amount. All voted amounts should be multiples of this value. | +| `MAX_LENGTH_NAME` | uint32 | `20` | The maximum allowed name length for delegates. | +| `MAX_NUMBER_SENT_VOTES` | uint32 | `10` | The maximum size of the sentVotes array of a voter substore entry. | +| `MAX_NUMBER_PENDING_UNLOCKS` | uint32 | `20` | The maximum size of the pendingUnlocks array of a voter 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 | `130,000` | The length of the inactivity window used in the fail safe banning mechanism. | +| `LOCKING_PERIOD_VOTES` | uint32 | `26,000` | The [locking period][lip-0023#explicit-unlock-mechanism] for regular votes. | +| `LOCKING_PERIOD_SELF_VOTES` | uint32 | `260,000` | The [locking period][lip-0023#explicit-unlock-mechanism] for self-votes. | +| `PUNISHMENT_WINDOW_VOTES` | uint32 | `260,000` | The punishment time for votes on punished delegates. | +| `PUNISHMENT_WINDOW_SELF_VOTES` | uint32 | `780,000` | The punishment time for self-votes of punished delegates. | +| `POM_LIMIT_BANNED` | uint32 | `5` | The number of proof-of-misbehavior transactions against a delegate for getting banned. | +| `MIN_INIT_ROUNDS` | uint32 | `3` | The minimum number of rounds for the [bootstrap period][lip-0034#bootstrap-period] | +| `BFT_THRESHOLD` | uint32 | `68` | The precommit and certificate thresholds used by the BFT module. This constant must equal `floor(2/3 * NUMBER_ACTIVE_DELEGATES) + 1` | +| `MIN_WEIGHT` | uint64 | `1000*(10^8)` | The minimum delegate weight required to be selected as a block generator. | +| `NUMBER_ACTIVE_DELEGATES` | uint32 | `101` | The number of active delegates. | +| `NUMBER_STANDBY_DELEGATES` | uint32 | `2` | The number of standby delegates. This LIP is specified for the number of standby delegates being 0, 1 or 2. | +| `ROUND_LENGTH` | uint32 | `103` | The round length. Is equal to `NUMBER_ACTIVE_DELEGATES` + `NUMBER_STANDBY_DELEGATES` | +| `TOKEN_ID_DPOS` | bytes | `TOKEN_ID_LSK = 0x 00 00 00 00 00 00 00 00` | The [token ID][lip-0051#tokenID] of the token used to cast votes. | +| `TOKEN_ID_FEE` | bytes | `TOKEN_ID_LSK = 0x 00 00 00 00 00 00 00 00` | The [token ID][lip-0051#tokenID] of the token used to pay the transaction fees. Defined in the fee module. | +| `TOKEN_ID_REWARD` | bytes |`TOKEN_ID_LSK = 0x 00 00 00 00 00 00 00 00`| The [token ID][lip-0051#tokenID] of the token used for block rewards. Specified as part of the [reward module][lip-0042] configuration. | +| `DELEGATE_REGISTRATION_FEE` | uint64 | `10*(10^8)` | The extra command fee of the delegate registration. | +| `INVALID_BLS_KEY ` | bytes | 48 bytes all set to 0x00 | The byte value associated with validators that did not register a BLS key. | ### Event Names and Results -| Name | Type | Value | Description | -|-----------------------------------|--------|--------|-------------| -| **Event names** | | | | -| `EVENT_NAME_REGISTER_DELEGATE` | string | "registerDelegate" | Used for events during delegate registration. | -| `EVENT_NAME_VOTE_DELEGATE` | string | "voteDelegate" | Used for events related to voting a delegate. | -| `EVENT_NAME_DELEGATE_PUNISHED` | string | "delegatePunished" | Used for events related to punishing a delegate. | -| `EVENT_NAME_DELEGATE_BANNED` | string | "delegateBanned" | Used for events related to banning a delegate. | -| **Result codes** | | | | -| `VOTE_SUCCESSFUL` | uint32 | 0 | Used when a vote succeeds. | +| Name | Type | Value | Description | +|------|------|-------|-------------| +| **Event names** | | | | +| `EVENT_NAME_REGISTER_DELEGATE` | string | "registerDelegate" | Used for events during delegate registration. | +| `EVENT_NAME_VOTE_DELEGATE` | string | "voteDelegate" | Used for events related to voting a delegate. | +| `EVENT_NAME_DELEGATE_PUNISHED` | string | "delegatePunished" | Used for events related to punishing a delegate. | +| `EVENT_NAME_DELEGATE_BANNED` | string | "delegateBanned" | Used for events related to banning a delegate. | +| **Result codes** | | | | +| `VOTE_SUCCESSFUL` | uint32 | 0 | Used when a vote succeeds. | | `VOTE_FAILED_NON_REGISTERED_DELEGATE`| uint32 | 1 | Used when a vote fails because the voted account has not registered a delegate. | | `VOTE_FAILED_INVALID_UNVOTE_PARAMETERS`| uint32 | 2 | Used when an unvote fails because the unvoted amount exceeds the total votes sent from voter to delegate. | | `VOTE_FAILED_TOO_MANY_PENDING_UNLOCKS` | uint32 | 3 | Used when a vote fails because it the total number of pending unlocks of voter exceeds `MAX_NUMBER_PENDING_UNLOCKS`. | -| `VOTE_FAILED_TOO_MANY_SENT_VOTES` | uint32 | 4 | Used when a vote fails because the total number of delegates voted by the voter exceeds `MAX_NUMBER_SENT_VOTES`. | - - - +| `VOTE_FAILED_TOO_MANY_SENT_VOTES` | uint32 | 4 | Used when a vote fails because the total number of delegates voted by the voter exceeds `MAX_NUMBER_SENT_VOTES`. | ### Type Definition -| Name | Type | Validation | Description | -|---------------------|--------|--------------------------------------------------|----------------------------------------------------------------------------------------| -| `Address` | bytes | Must be of length `ADDRESS_LENGTH`. | Address of an account. | -| `Transaction` | object | Must follow the `transactionSchema` schema defined in [LIP 0068][lip-0068] with the only difference that `params` property is not serialized and contains the values of parameters of `paramsSchema` for the corresponding transaction. | An object representing a non-serialized transaction. | -| `UnlockObject` | object | Contains 3 elements (`address`, `amount`, `unvoteHeight`) of types `Address`, `uint64` and `uint32` respecively (same as the items in the `pendingUnlocks` array of the [voterStoreSchema](#json-schema)). | An object containing information regarding unvoting a delegate. | -| `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][lip-0038#public-key-registration]. | -| `PublicKeyEd25519` | bytes | Must be of length `ED25519_PUBLIC_KEY_LENGTH`. | Used for Ed25519 public keys. | -| `VoterStoreObject` | object | Must follow the [`voterStoreSchema` schema](#json-schema). | Deserialized version of voter substore values. | -| `DelegateStoreObject` | object | Must follow the [`delegateStoreSchema` schema](#json-schema-1). | Deserialized version of delegate substore values. | - - - +| Name | Type | Validation | Description | +|------|------|------------|-------------| +| `Address` | bytes | Must be of length `ADDRESS_LENGTH`. | Address of an account. | +| `Transaction` | object | Must follow the `transactionSchema` schema defined in [LIP 0068][lip-0068] with the only difference that `params` property is not serialized and contains the values of parameters of `paramsSchema` for the corresponding transaction. | An object representing a non-serialized transaction. | +| `UnlockObject` | object | Contains 3 elements (`address`, `amount`, `unvoteHeight`) of types `Address`, `uint64` and `uint32` respecively (same as the items in the `pendingUnlocks` array of the [voterStoreSchema](#json-schema)). | An object containing information regarding unvoting a delegate. | +| `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][lip-0038#public-key-registration]. | +| `PublicKeyEd25519` | bytes | Must be of length `ED25519_PUBLIC_KEY_LENGTH`. | Used for Ed25519 public keys. | +| `VoterStoreObject` | object | Must follow the [`voterStoreSchema` schema](#json-schema). | Deserialized version of voter substore values. | +| `DelegateStoreObject` | object | Must follow the [`delegateStoreSchema` schema](#json-schema-1). | Deserialized version of delegate substore values. | #### Functions from Other Modules @@ -226,22 +218,22 @@ voterStoreSchema = { } ``` -##### Properties +##### Properties In this section, we describe the properties of the voter substore. -* `sentVotes`: stores an array of the current votes of a user. - Each vote is represented by the address of the voted delegate and the amount of tokens that have been used to vote for the delegate. - This array was called `votes` in [LIP 0023][lip-0023]. - This array is updated with a [vote command](#vote). - The `sentVotes` array is always kept ordered in lexicographical order of `delegateAddress`. - Its size is at most `MAX_NUMBER_SENT_VOTES`, any state transition that would increase it to above `MAX_NUMBER_SENT_VOTES` is invalid. +* `sentVotes`: stores an array of the current votes of a user. + Each vote is represented by the address of the voted delegate and the amount of tokens that have been used to vote for the delegate. + This array was called `votes` in [LIP 0023][lip-0023]. + This array is updated with a [vote command](#vote). + The `sentVotes` array is always kept ordered in lexicographical order of `delegateAddress`. + Its size is at most `MAX_NUMBER_SENT_VOTES`, any state transition that would increase it to above `MAX_NUMBER_SENT_VOTES` is invalid. Any element with `amount == 0` is removed from the array. -* `pendingUnlocks`: stores an array representing the tokens that have been unvoted, but not yet unlocked. - Each unvote generates an object in this array containing the address of the unvoted delegate, the amount of the unvote and the height at which the unvote was included in the chain. - Objects in this array get removed when the corresponding [unlock command](#unlock) is executed. This array was called `unlocking` in [LIP 0023][lip-0023]. - This array is updated with [vote](#vote) and [unlock](#unlock) commands. - The `pendingUnlocks` array is always kept ordered by lexicographical order of `delegateAddress`, ties broken by increasing `amount`, ties broken by increasing `unvoteHeight`. +* `pendingUnlocks`: stores an array representing the tokens that have been unvoted, but not yet unlocked. + Each unvote generates an object in this array containing the address of the unvoted delegate, the amount of the unvote and the height at which the unvote was included in the chain. + Objects in this array get removed when the corresponding [unlock command](#unlock) is executed. This array was called `unlocking` in [LIP 0023][lip-0023]. + This array is updated with [vote](#vote) and [unlock](#unlock) commands. + The `pendingUnlocks` array is always kept ordered by lexicographical order of `delegateAddress`, ties broken by increasing `amount`, ties broken by increasing `unvoteHeight`. 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 voter substore entry to have `sentVotes == []` and `pendingUnlocks == []`, the entry is removed from the store. @@ -538,8 +530,8 @@ The property `delegateRegistrationFee` corresponds to the special fee for regist ```python def verify(trs: Transaction) -> None: - delegateAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # derive delegateAddress from trs.senderPublicKey - + delegateAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # Derive delegateAddress from trs.senderPublicKey. + if trs.params.delegateRegistrationFee != DELEGATE_REGISTRATION_FEE: raise Exception('Invalid delegate registration fee.') if Token.getAvailableBalance(delegateAddress, TOKEN_ID_FEE) < DELEGATE_REGISTRATION_FEE: @@ -550,33 +542,28 @@ def verify(trs: Transaction) -> None: raise Exception('Invalid name') if there exists an entry nameStore(delegateName) in name substore: raise Exception('Name already used by a delegate.') -``` - +``` ##### Execution When a transaction `trs` executing a delegate registration command included in a block `b`, the logic below is followed: ```python - def execute(trs: Transaction) -> None: b = block including trs - delegateAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # derive delegateAddress from trs.senderPublicKey - delegateName = trs.params.name + delegateAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # Derive delegateAddress from trs.senderPublicKey. + delegateName = trs.params.name - # this step also checks that the BLS key has not been used from another delegate + # This step also checks that the BLS key has not been used from another delegate. Validators.registerValidatorKeys(delegateAddress, trs.params.proofOfPossession, trs.params.generatorKey, trs.params.blsKey) - - - # the new delegate pays the registration fee; this fee is burned. + # The new delegate pays the registration fee; this fee is burned. Token.burn(delegateAddress, TOKEN_ID_FEE, trs.params.delegateRegistrationFee) - - # update delegate substore + # Update delegate substore. delegateState = { "name": delegateName, "totalVotesReceived": 0, @@ -591,12 +578,12 @@ def execute(trs: Transaction) -> None: storeKey = delegateAddress, storeValue = encode(delegateStoreSchema, delegateState) - # update name substore + # Update name substore. create an entry in the name substore with storeKey = delegateName encoded as utf8, - storeValue = encode(nameStoreSchema, {"delegateAddress": delegateAddress}) + storeValue = encode(nameStoreSchema, {"delegateAddress": delegateAddress}) - # emit event for the successfull delegate registration. + # Emit event for the successful delegate registration. emitEvent( module=MODULE_NAME_DPOS, name=EVENT_NAME_REGISTER_DELEGATE, @@ -649,11 +636,11 @@ voteTransactionParams = { The `params` property of a vote transaction is valid if: -* `params.votes` has at most `2 * MAX_NUMBER_SENT_VOTES` elements. The reason of choosing this bound on the size is to allow a voter to unvote all voted delegates (which are at most `MAX_NUMBER_SENT_VOTES`) and vote for `MAX_NUMBER_SENT_VOTES` new ones in the same transaction. -* A given `delegateAddress` is included in at most one vote from the list of votes (regardless of the associated amounts). -* For all votes included in `params.votes`, we have: - * `amount` value is a multiple of `BASE_VOTE_AMOUNT`, i.e., `amount % BASE_VOTE_AMOUNT == 0`. For the Lisk mainchain, where `BASE_VOTE_AMOUNT = 10^9` and `TOKEN_ID_DPOS = TOKEN_ID_LSK`, this corresponds to multiples of 10 LSK. - * `amount != 0`. +* `params.votes` has at most `2 * MAX_NUMBER_SENT_VOTES` elements. The reason of choosing this bound on the size is to allow a voter to unvote all voted delegates (which are at most `MAX_NUMBER_SENT_VOTES`) and vote for `MAX_NUMBER_SENT_VOTES` new ones in the same transaction. +* A given `delegateAddress` is included in at most one vote from the list of votes (regardless of the associated amounts). +* For all votes included in `params.votes`, we have: + * `amount` value is a multiple of `BASE_VOTE_AMOUNT`, i.e., `amount % BASE_VOTE_AMOUNT == 0`. For the Lisk mainchain, where `BASE_VOTE_AMOUNT = 10^9` and `TOKEN_ID_DPOS = TOKEN_ID_LSK`, this corresponds to multiples of 10 LSK. + * `amount != 0`. ##### Execution @@ -663,10 +650,9 @@ When executing a vote transaction `trs`, the logic below is followed def execute(trs: Transaction) -> None: b = block including trs height = b.header.height - senderAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # derive address from trs.senderPublicKey - + senderAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # Derive address from trs.senderPublicKey. - # sorting the votes guarantees that we first apply the votes with negative amounts + # Sorting the votes guarantees that we first apply the votes with negative amounts. sortedVotes = trs.params.votes ordered by increasing value of amount for vote in sortedVotes: @@ -674,18 +660,18 @@ def execute(trs: Transaction) -> None: if delegateStore(votedAddress) does not exist: emitVoteEvent(senderAddress, votedAddress, vote.amount, height, VOTE_FAILED_NON_REGISTERED_DELEGATE) - raise Exception('Invalid vote: no registered delegate with the specified address') + raise Exception('Invalid vote: no registered delegate with the specified address') - if vote.amount < 0: # case of unvote + if vote.amount < 0: # Case of unvote. sentVote = element in voterStore(senderAddress).sentVotes with sentVote.delegateAddress == votedAddress if not sentVote or abs(vote.amount) > sentVote.amount: emitVoteEvent(senderAddress, votedAddress, vote.amount, height, VOTE_FAILED_INVALID_UNVOTE_PARAMETERS) raise Exception('Invalid unvote: The unvote amount exceeds the voted amount for this delegate') - - # update voter substore + + # Update voter substore. i = index of sentVote in voterStore(senderAddress).sentVotes - voterStore(senderAddress).sentVotes[i].amount += vote.amount - + voterStore(senderAddress).sentVotes[i].amount += vote.amount + if voterStore(senderAddress).sentVotes[i].amount == 0: remove sentVote from voterStore(senderAddress).sentVotes @@ -694,17 +680,17 @@ def execute(trs: Transaction) -> None: "amount" : abs(vote.amount), "unvoteHeight" : height } - add unlockOject to voterStore(senderAddress).pendingUnlocks, keeping the array ordered by lexicographical order of delegateAddress, + add unlockOject to voterStore(senderAddress).pendingUnlocks, keeping the array ordered by lexicographical order of delegateAddress, ties broken by increasing amount, - ties broken by increasing unvoteHeight + ties broken by increasing unvoteHeight if len(voterStore(senderAddress).pendingUnlocks) > MAX_NUMBER_PENDING_UNLOCKS: emitVoteEvent(senderAddress, votedAddress, vote.amount, height, VOTE_FAILED_TOO_MANY_PENDING_UNLOCKS) raise Exception('Sender has reached the maximum number of pending unlocks.') - - if vote.amount > 0: #case of regular vote - Token.lock(senderAddress, MODULE_NAME_DPOS, TOKEN_ID_DPOS, vote.amount) # lock the voted amount - #update user substore + + if vote.amount > 0: # Case of regular vote. + Token.lock(senderAddress, MODULE_NAME_DPOS, TOKEN_ID_DPOS, vote.amount) # Lock the voted amount. + # Update user substore. if there exist an entry oldVote in voterStore(senderAddress).sentVotes with oldVote.delegateAddress = votedAddress: i = index of oldVote in voterStore(senderAddress).sentVotes voterStore(senderAddress).sentVotes[i].amount += vote.amount @@ -715,20 +701,18 @@ def execute(trs: Transaction) -> None: if len(voterStore(senderAddress).sentVotes) > MAX_NUMBER_SENT_VOTES: emitVoteEvent(senderAddress, votedAddress, vote.amount, height, VOTE_FAILED_TOO_MANY_SENT_VOTES) raise Exception('This address has reached the maximum number of voted delegates.') - - - # update delegate substore + + # Update delegate substore. previousDelegateWeight = getDelegateWeight(votedAddress) delegateStore(votedAddress).totalVotesReceived += vote.amount if senderAddress == votedAddress: delegateStore(votedAddress).selfVotes += vote.amount - + emitVoteEvent(senderAddress, votedAddress, vote.amount, height, VOTE_SUCCESSFUL) - # update eligible delegates substore + # Update eligible delegates substore. updateDelegateEligibility(votedAddress, previousDelegateWeight) - def emitVoteEvent(senderAddress: Address, delegateAddress: Address, amount: uint64, height: uint32, result: uint32) -> None: if result == VOTE_SUCCESSFUL: emitEvent( @@ -773,22 +757,20 @@ The `params` property of unlock transactions is empty. #### Execution - ```python def execute(trs: Transaction) -> None: - senderAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # derive address from trs.senderPublicKey + senderAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # Derive address from trs.senderPublicKey. b = block including trs height = b.header.height - for each unlockObject in voterStore(senderAddress).pendingUnlocks: - # check if unvoted amount can be unlocked + # Check if unvoted amount can be unlocked. if (isUnlockable(unlockObject, senderAddress, height) and isCertificateGenerated(unlockObject)): delete unlockObject from voterStore(senderAddress).pendingUnlocks Token.unlock(senderAddress, MODULE_NAME_DPOS, TOKEN_ID_DPOS, unlockObject.amount) - # token module has its own event for successful/failed unlock so no need to add event here. + # 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][lip-0059]. The function `isUnlockable` is defined below. It's logic is the concatenation of the functions `hasWaited` and `isPunished` that are rationalized in [LIP 0023][lip-0023#explicit-unlock-mechanism] and [LIP 0024][lip-0024#rationale] respectively. This function has the following input parameters: @@ -799,29 +781,26 @@ The definition and rationale for the `isCertificateGenerated` function is part o ```python def isUnlockable(unlockObject: UnlockObject, senderAddress: Address, height: uint32) -> bool: - #first consider the case that delegate is not punished + # First consider the case that delegate is not punished. delegateAddress = unlockObject.delegateAddress lockingPeriod = LOCKING_PERIOD_SELF_VOTES if delegateAddress == senderAddress else LOCKING_PERIOD_VOTES - # if delegate is not punished, normal locking period for votes/selfvotes applies. + # If delegate is not punished, normal locking period for votes/selfvotes applies. if not isPunished(delegateAddress, height): if height - unlockObject.unvoteHeight < lockingPeriod: return False - else: #delegate is punished + else: # Delegate is punished. let lastPomHeight be the last element of delegateStore(delegateAddress).pomHeights - # lastPomHeight is also the largest element of the pomHeights array - punishmentWindow = PUNISHMENT_WINDOW_SELF_VOTES if delegateAddress == senderAddress else PUNISHMENT_WINDOW_VOTES - + # lastPomHeight is also the largest element of the pomHeights array. + punishmentWindow = PUNISHMENT_WINDOW_SELF_VOTES if delegateAddress == senderAddress else PUNISHMENT_WINDOW_VOTES + if height – lastPomHeight < punishmentWindow and lastPomHeight < unlockObject.unvoteHeight + lockingPeriod: return False - + return True ``` - - - #### Proof of Misbehavior Transactions executing this command have: @@ -850,8 +829,7 @@ pomParams = { ##### Verification -Both properties of the parameters must follow the [block header schema `blockHeaderSchema`][lip-0055#block-header-json-schema] defined in LIP 0055. Validity of this transaction was previously specified in [LIP 0024][lip-0024#validity-of-a-pom-transaction]. For completeness, we include the pseudocode here. - +Both properties of the parameters must follow the [block header schema `blockHeaderSchema`][lip-0055#block-header-json-schema] defined in LIP 0055. Validity of this transaction was previously specified in [LIP 0024][lip-0024#validity-of-a-pom-transaction]. For completeness, we include the pseudocode here. ```python def verify(trs: Transaction) -> None: @@ -881,8 +859,6 @@ def verify(trs: Transaction) -> None: The [verifyBlockSignature function](#verifyblocksignature) is an internal function defined below. - - ##### Execution Execution of this transaction was previously specified in [LIP 0024][lip-0024#applying-a-pom-transaction]. Here we update the specifications to be integrated in the [state model][lip-0040] used in Lisk. @@ -891,17 +867,17 @@ Execution of this transaction was previously specified in [LIP 0024][lip-0024#ap def execute(trs: Transaction) -> None: b = block including trs h = b.header.height - senderAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # derive address from trs.senderPublicKey + senderAddress = SHA256(trs.senderPublicKey)[:ADDRESS_LENGTH] # Derive address from trs.senderPublicKey. header1 = decode(blockHeaderSchema, trs.params.header1) header2 = decode(blockHeaderSchema, trs.params.header2) punishedAddress = trs.params.header1.generatorAddress - #update delegate substore + # Update delegate substore. delegateStore(punishedAddress).pomHeights.append(h) - # emit event for the delegate punishment. + # Emit event for the delegate punishment. emitEvent( module=MODULE_NAME_DPOS, name=EVENT_NAME_DELEGATE_PUNISHED, @@ -912,10 +888,10 @@ def execute(trs: Transaction) -> None: topics=[delegateAddress] ) - # check if the delegate should be banned. + # Check if the delegate should be banned. if len(delegateStore(punishedAddress).pomHeights) == POM_LIMIT_BANNED: delegateStore(punishedAddress).isBanned = True - # emit event for the delegate banning + # Emit event for the delegate banning. emitEvent( module=MODULE_NAME_DPOS, name=EVENT_NAME_DELEGATE_BANNED, @@ -925,13 +901,13 @@ def execute(trs: Transaction) -> None: }, topics=[delegateAddress] ) - + currentWeight = getDelegateWeight(punishedAddress) updateDelegateEligibility(votedAddress, currentWeight) - # assign the block reward to the sender of the transaction. - # the amount is taken from the punished delegate account. - # if punished delegate has less balance than the reward, all their balance is provided to the sender. + # Assign the block reward to the sender of the transaction. + # The amount is taken from the punished delegate account. + # If punished delegate has less balance than the reward, all their balance is provided to the sender. senderReward = min(reward.getBlockReward(b.header), Token.getAvailableBalance(punishedAddress,TOKEN_ID_REWARD)) Token.transfer(punishedAddress, senderAddress, @@ -939,8 +915,6 @@ def execute(trs: Transaction) -> None: senderReward) ``` - - ### Events #### delegateRegistered @@ -974,8 +948,6 @@ delegateRegisteredDataSchema = { } ``` - - #### vote This event has `name = EVENT_NAME_VOTE_DELEGATE`. This event is emitted during the processing of each vote included in a vote transaction. @@ -1079,10 +1051,6 @@ delegatePunishedDataSchema = { } ``` - - - - ### Internal Functions #### Round Number and End of Rounds @@ -1102,7 +1070,6 @@ def roundNumber(h: uint32) -> uint32: This function returns a boolean indicating if the input height is at the end of a round or not. - ```python def isEndOfRound(h: uint32) -> bool: if (h - genesisDataStore.height) % ROUND_LENGTH == 0: @@ -1118,7 +1085,7 @@ This function returns the weight of a given delegate (specified by the address). ```python def getDelegateWeight(address: Address) -> uint64: return min(delegateStore(address).selfVotes * FACTOR_SELF_VOTES, - delegateStore(address).totalVotesReceived) + delegateStore(address).totalVotesReceived) ``` #### shuffleValidatorsList @@ -1140,16 +1107,15 @@ This function returns an array of `Address` items, which is a re-ordered list of ```python def shuffleValidatorsList(validatorsAddresses: list[Address], randomSeed: bytes) -> list[Address]: - # checking pairwise distinct property + # Checking pairwise distinct property. if validatorsAddresses != set(validatorsAddresses): raise Exception('Validators list invalid (duplicate values detected)') roundHash = {} for address in validatorsAddresses: - roundHash[address] = SHA256(randomSeed + address) # hashing concatenation of randomSeed and address - + roundHash[address] = SHA256(randomSeed + address) # Hashing concatenation of randomSeed and address. - # Reorder the validator list + # Reorder the validator list. shuffledValidatorAddresses = sort validatorsAddresses where address1 < address2 if (roundHash(address1) < roundHash(address2)) or ((roundHash[address1] == roundHash[address2]) and address1 < address2) @@ -1175,17 +1141,15 @@ This function does not return. ```python def updateDelegateEligibility(address: Address, oldWeight: uint64) -> None: - # Always start by removing the old entry from the eligible - # delegate substore + # Always start by removing the old entry from the eligible delegate substore. oldKey = oldWeight.to_bytes(8,'big') + address if the eligible delegate substore contains an entry for key = oldKey: - remove this entry from the store + remove this entry from the store - # If the delegate is eligible, add an entry to the eligible - # delegate substore. + # If the delegate is eligible, add an entry to the eligible delegate substore. weight = getDelegateWeight(address) if (weight >= MIN_WEIGHT - and delegateStore(address).isBanned == False + and delegateStore(address).isBanned == False and Validators.getValidatorAccount(address).blsKey != INVALID_BLS_KEY and Validators.getValidatorAccount(address).generatorKey != INVALID_ED25519_KEY): newKey = weight.to_bytes(8,'big') + address @@ -1194,7 +1158,7 @@ def updateDelegateEligibility(address: Address, oldWeight: uint64) -> None: #### verifyBlockSignature -Checks whether a block header is validly signed. +Checks whether a block header is validly signed. ##### Execution @@ -1203,9 +1167,9 @@ def verifyBlockSignature(header: Header) -> bool: generatorKey = Validators.getValidatorAccount(header.generatorAddress).generatorKey signature = block.header.signature - # Remove the signature from the block header + # Remove the signature from the block header. delete header.signature - # Serialize the block header without signature + # Serialize the block header without signature. serializedUnsignedBlockHeader = encode(unsignedBlockHeaderSchema, header) return verifyEd25519(generatorKey, "LSK_BH_", OWN_CHAIN_ID, serializedUnsignedBlockHeader, signature) @@ -1215,20 +1179,20 @@ Here, the function `verifyEd25519` verifies the validity of a signature as speci #### isDelegateNameValid -Checks whether a given string would be a valid delegate name. +Checks whether a given string would be a valid delegate name. ##### Execution ```python def isDelegateNameValid(delegateName: 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 + # 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 delegateName)) or len(delegateName) < 1 or len(delegateName) > MAX_LENGTH_NAME): return False - + return True ``` @@ -1236,7 +1200,6 @@ def isDelegateNameValid(delegateName: str) -> bool: This function returns a boolean indicating if a delegate is punished at a certain height of not. - ```python def isPunished(address: Address, height: uint32) -> bool: if delegateStore(address).pomHeights is empty: @@ -1246,11 +1209,10 @@ def isPunished(address: Address, height: uint32) -> bool: if height <= lastPomHeight + PUNISHMENT_WINDOW_SELF_VOTES: return True - + return False ``` - #### getActiveDelegates This function computes the set of active delegates based on the input set of eligible delegates. @@ -1263,7 +1225,6 @@ def getActiveDelegates(validatorsTwoRoundsAgo: list[Address], roundNumber: uint3 # During the first NUMBER_ACTIVE_DELEGATES rounds after the bootstrap period # the initial delegates are only partly replaced by elected delegates. # During this phase, there are no selected standby delegates. - if roundNumber < initRounds + NUMBER_ACTIVE_DELEGATES: nbrInitValidators = initRounds + NUMBER_ACTIVE_DELEGATES - roundNumber nbrElectedValidators = NUMBER_ACTIVE_DELEGATES - nbrInitValidators @@ -1273,10 +1234,9 @@ def getActiveDelegates(validatorsTwoRoundsAgo: list[Address], roundNumber: uint3 remainingInitDelegates = [address for address in initDelegates if address not in electedValidators] - # concatenation of elected validators and remaining initial delegates + # Concatenation of elected validators and remaining initial delegates. activeDelegates = electedValidators + remainingInitDelegates[:nbrInitValidators] - - else: + else: # If validatorsTwoRoundsAgo contains less than NUMBER_ACTIVE_DELEGATES entries # there will be less than NUMBER_ACTIVE_DELEGATES active delegates. # Recall that validatorsTwoRoundsAgo is sorted by delegate weight. @@ -1290,7 +1250,7 @@ def getActiveDelegates(validatorsTwoRoundsAgo: list[Address], roundNumber: uint3 #### getSelectedStandbyDelegates -This function selects the standby delegates for a round. +This function selects the standby delegates for a round. ```python def getSelectedStandbyDelegates(standbyDelegates: list[dict[str,bytes]], height: uint32) -> list[Address]: @@ -1306,11 +1266,11 @@ def getSelectedStandbyDelegates(standbyDelegates: list[dict[str,bytes]], height: ROUND_LENGTH ) selectedStandbyDelegates = select 2 address from standbyDelegates - as specified in LIP 0022, using the seeds randomSeed1 and randomSeed2 + as specified in LIP 0022, using the seeds randomSeed1 and randomSeed2 elif NUMBER_STANDBY_DELEGATES >= 1 and len(standbyDelegates) >= 1: selectedStandbyDelegates = select 1 address from standbyDelegates - as specified in LIP 0022, using the seed randomSeed1 - else: # No standby delegates + as specified in LIP 0022, using the seed randomSeed1 + else: # No standby delegates. selectedStandbyDelegates = empty return selectedStandbyDelegates @@ -1346,15 +1306,13 @@ Asserts the availability of a given name for delegate registration. ```python def isNameAvailable(name: str) -> bool: - - if (not isDelegateNameValid(name)) + if (not isDelegateNameValid(name)) or (nameStore(name) exists): return False else: return True ``` - #### getVoter Same as the [getVoter function](#getvoter) of the previous section. @@ -1529,7 +1487,7 @@ genesisDPoSStoreSchema = { "fieldNumber": 2, "items": { "dataType": "bytes", - "length": ADDRESS_LENGTH + "length": ADDRESS_LENGTH } } } @@ -1544,7 +1502,6 @@ During the genesis state initialization stage, the following steps are executed. Let `genesisBlockAssetBytes` be the `data` bytes included in the block assets for the DPoS module and let `genesisBlockAssetObject` be the deserialization of `genesisBlockAssetBytes` according to the `genesisDPoSStoreSchema` schema, given above. - * Initial checks on the properties of `genesisBlockAssetObject`: * `genesisBlockAssetObject` should satisfy the [`genesisDPoSStoreSchema` schema](#genesis-assets-schema). * Across elements of the `validators` array, all `address` values must be unique, all `name` values must also be unique. @@ -1586,11 +1543,9 @@ Let `genesisBlockAssetBytes` be the `data` bytes included in the block assets fo storeValue = encode(delegateStoreSchema, delegateState) ``` - Further, for every entry `validator` in `genesisBlockAssetObject.validators`, also create an entry in the name substore with: ```python - delegateState = { "delegateAddress": validator.address } @@ -1624,7 +1579,6 @@ Let `genesisBlockAssetBytes` be the `data` bytes included in the block assets fo ``` * Create an entry in the previous timestamp substore with: ```python - timestampState = { "timestamp": block header height of the genesis block } @@ -1650,7 +1604,7 @@ if the chain is the mainchain and b is the snapshot block for the migration: validator.generatorKey ) else: -# For any other genesis block, register validators with a BLS key +# For any other genesis block, register validators with a BLS key. for validator in genesisBlockAssetObject.validators: Validators.registerValidatorKeys( validator.address, @@ -1659,7 +1613,7 @@ else: validator.blsKey ) -# Check that all sentVotes and pendingUnlocks correspond to locked tokens +# Check that all sentVotes and pendingUnlocks correspond to locked tokens. for address a key of the voter substore: votedAmount = 0 for sentVote in voter(address).sentVotes: @@ -1670,20 +1624,20 @@ for address a key of the voter substore: if Token.getLockedAmount(address, MODULE_NAME_DPOS, TOKEN_ID_DPOS) != votedAmount: raise Exception('Locked values do not match') -# set the initial delegates in the BFT module -# recall that initDelegate is always in lexicographical order +# Set the initial delegates in the BFT module. +# Recall that initDelegate is always in lexicographical order. initDelegates = genesisBlockAssetObject.genesisData.initDelegates bftWeights = [ {"address": address, "bftWeight": 1} for address in initDelegates ] -# Initialize the BFT module store +# Initialize the BFT module store. bft.setBFTParameters(BFT_THRESHOLD, BFT_THRESHOLD, bftWeights) -# Set the initial delegates in the validators module +# Set the initial delegates in the validators module. Validators.setGeneratorList(initDelegates) ``` @@ -1697,13 +1651,13 @@ After the transactions in a block `b` are executed, the properties related to mi def afterTransactionsExecute(b: Block) -> None: height = b.header.height - # previousTimestamp is the value in the previous timestamp substore + # previousTimestamp is the value in the previous timestamp substore. missedBlocks = Validators.getGeneratorsBetweenTimestamps(previousTimestamp, b.header.timestamp) for address in missedBlocks: delegateStore(address).consecutiveMissedBlocks += missedBlocks[address] - # The rule below was introduced in LIP 0023 + # The below rule was introduced in LIP 0023. if (delegateStore(address).consecutiveMissedBlocks > FAIL_SAFE_MISSED_BLOCKS and height - delegateStore(address).lastGeneratedHeight > FAIL_SAFE_INACTIVE_WINDOW): delegateStore(address).isBanned = True @@ -1712,61 +1666,61 @@ def afterTransactionsExecute(b: Block) -> None: delegateStore(b.header.generatorAddress).consecutiveMissedBlocks = 0 delegateStore(b.header.generatorAddress).lastGeneratedHeight = height - # update previousTimestamp substore + # Update previousTimestamp substore. previousTimestamp = b.header.timestamp if isEndOfRound(height) == False: return - # block b is an end-of-round block - # this must be done after the properties related to missed blocks are updated + # Block b is an end-of-round block. + # This must be done after the properties related to missed blocks are updated. roundNumber = roundNumber(height) - + eligibleDelegates = [ - {"address": key[-ADDRESS_LENGTH:], "weight":key[:-ADDRESS_LENGTH]} + {"address": key[-ADDRESS_LENGTH:], "weight":key[:-ADDRESS_LENGTH]} for key a substore key of the eligible substore - ] ordered by weight, ties broken by reverse lexicographical ordering of address - # notice that the keys in the substore naturally have the right ordering + ] 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 = { "delegateWeightSnapshot": eligibleDelegates - } + } create an entry in the snapshot substore with storeKey = roundNumber.to_bytes(4,'big'), storeValue = encode(snapshotStoreSchema, snapshotState) delete any entries from the snapshot substore snapshotStore(x) for x <= roundNumber-3 - # Updates to Validators and BFT module are only done after the bootstrap period + # Updates to Validators and BFT module are only done after the bootstrap period. if roundNumber <= genesisDataStore.initRounds: return - # Calculate the active delegates. Exclude punished ones from the snapshot + # Calculate the active delegates. Exclude punished ones from the snapshot. validatorsTwoRoundsAgo = [item for item in snapshotStore(roundNumber-2) if isPunished(item["address"], height)==False] activeDelegates = getActiveDelegates([item["address"] for item in validatorsTwoRoundsAgo], roundNumber) validators = activeDelegates - # select standby delegates if relevant. + # Select standby delegates if relevant. if roundNumber > initRounds + NUMBER_ACTIVE_DELEGATES: - standbyDelegates = [validator for validator in validatorsTwoRoundsAgo + standbyDelegates = [validator for validator in validatorsTwoRoundsAgo if validator["address"] not in activeDelegates] - + selectedStandbyDelegates = getSelectedStandbyDelegates(standbyDelegates, height) - # add selected standby delegates to validators + # Add selected standby delegates to validators. validators += selectedStandbyDelegates - # random seed to shuffle validators + # Random seed to shuffle validators. randomSeed = random.getRandomBytes( height +1 - (ROUND_LENGTH*3)//2, ROUND_LENGTH ) - # if there are no eligible delegates, validators array is empty - # in this case, bft parameters and validators are not updated. + # If there are no eligible delegates, validators array is empty. + # In this case, bft parameters and validators are not updated. if validators is empty: return - # update BFT + # Update BFT. bftWeights = [{ "address": item["address"], "bftWeight": 1 @@ -1781,12 +1735,11 @@ def afterTransactionsExecute(b: Block) -> None: BFT_THRESHOLD, bftWeights) - # update validators list + # Update validators list. nextValidators = shuffleValidatorsList(validators, randomSeed) Validators.setGeneratorList(nextValidators) ``` - ## Backwards Compatibility This LIP defines a new store interface for the DPoS 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. From cd9c60cab9ea06b6fcf428d87d209ae9bf1bc1fa Mon Sep 17 00:00:00 2001 From: Oliver Beddows Date: Mon, 26 Sep 2022 12:46:39 +0200 Subject: [PATCH 4/4] :pencil: Update header --- proposals/lip-0057.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/lip-0057.md b/proposals/lip-0057.md index ae8c2ded8..58d0367ca 100644 --- a/proposals/lip-0057.md +++ b/proposals/lip-0057.md @@ -8,7 +8,7 @@ Discussions-To: https://research.lisk.com/t/define-state-and-state-transitions-o Status: Draft Type: Standards Track Created: 2021-09-03 -Updated: 2022-05-10 +Updated: 2022-09-26 Required: 0022, 0023, 0024, 0040, 0044, 0046, 0058, 0059 ```