The DPoS module

The DPoS module is responsible for handling all DPoS related logics. Specifically:

  • Snapshotting vote weights

  • Calculating productivity

  • Handling registerDelegate, voteDelegate, unlockToken and reportDelegateMisbehavior transaction assets

  • Setting the next delegates set

Name Property

Name

dpos

ID

5

Assets

  • RegisterTransactionAsset

    • AssetID: 0

  • VoteTransactionAsset

    • AssetID: 1

  • UnlockTransactionAsset

    • AssetID: 2

  • PomTransactionAsset

    • AssetID: 3

Reducers

none

Actions

  • getAllDelegates()

  • getUnlockings()

Events

none

Account schema

The token module adds a new property balance under the key token to every account in the network as follows:

{
    type: 'object',
    properties: {
        delegate: {
            type: 'object',
            fieldNumber: 1,
            properties: {
                username: { dataType: 'string', fieldNumber: 1 },
                pomHeights: {
                    type: 'array',
                    items: { dataType: 'uint32' },
                    fieldNumber: 2,
                },
                consecutiveMissedBlocks: { dataType: 'uint32', fieldNumber: 3 },
                lastForgedHeight: { dataType: 'uint32', fieldNumber: 4 },
                isBanned: { dataType: 'boolean', fieldNumber: 5 },
                totalVotesReceived: { dataType: 'uint64', fieldNumber: 6 },
            },
            required: [
                'username',
                'pomHeights',
                'consecutiveMissedBlocks',
                'lastForgedHeight',
                'isBanned',
                'totalVotesReceived',
            ],
        },
        sentVotes: {
            type: 'array',
            fieldNumber: 2,
            items: {
                type: 'object',
                properties: {
                    delegateAddress: {
                        dataType: 'bytes',
                        fieldNumber: 1,
                    },
                    amount: {
                        dataType: 'uint64',
                        fieldNumber: 2,
                    },
                },
                required: ['delegateAddress', 'amount'],
            },
        },
        unlocking: {
            type: 'array',
            fieldNumber: 3,
            items: {
                type: 'object',
                properties: {
                    delegateAddress: {
                        dataType: 'bytes',
                        fieldNumber: 1,
                    },
                    amount: {
                        dataType: 'uint64',
                        fieldNumber: 2,
                    },
                    unvoteHeight: {
                        dataType: 'uint32',
                        fieldNumber: 3,
                    },
                },
                required: ['delegateAddress', 'amount', 'unvoteHeight'],
            },
        },
    },
	default: {
		delegate: {
			username: '',
			pomHeights: [],
			consecutiveMissedBlocks: 0,
			lastForgedHeight: 0,
			isBanned: false,
			totalVotesReceived: BigInt(0),
		},
		sentVotes: [],
		unlocking: [],
	},
};

Transactions

The following transaction assets are provided by the token module.

RegisterTransactionAsset

Allows to send a register delegate transaction, which registers a delegate for the sender account with a given username.

Name

registerDelegate

ID

0

Base fee

10 LSK

Schema
{
    $id: 'lisk/dpos/register',
    type: 'object',
    required: ['username'],
    properties: {
        username: {
            dataType: 'string',
            fieldNumber: 1,
            minLength: 1,
            maxLength: 20,
        },
    },
}

VoteTransactionAsset

Allows to send a vote transaction, which casts votes and unvotes for delegates.

Each token can only be used once for voting, therefore the sender locks a certain amount of tokens for each vote. After unvoting a delegate, the user is able to unlock the token again with the unlock transaction.

Name

voteDelegate

ID

1

Schema
{
    $id: 'lisk/dpos/vote',
    type: 'object',
    required: ['votes'],
    properties: {
        votes: {
            type: 'array',
            minItems: 1,
            maxItems: 20,
            items: {
                type: 'object',
                required: ['delegateAddress', 'amount'],
                properties: {
                    delegateAddress: {
                        dataType: 'bytes',
                        fieldNumber: 1,
                        minLength: 20,
                        maxLength: 20,
                    },
                    amount: {
                        dataType: 'sint64',
                        fieldNumber: 2,
                    },
                },
            },
            fieldNumber: 1,
        },
    },
}

UnlockTransactionAsset

Allows to send an unlock transaction, which unlocks token that have been locked after voting for a delegate, after unvoting this delegate.

Name

unlockToken

ID

2

Schema
{
    $id: 'lisk/dpos/unlock',
    type: 'object',
    required: ['unlockObjects'],
    properties: {
        unlockObjects: {
            type: 'array',
            minItems: 1,
            maxItems: 20,
            items: {
                type: 'object',
                required: ['delegateAddress', 'amount', 'unvoteHeight'],
                properties: {
                    delegateAddress: {
                        dataType: 'bytes',
                        fieldNumber: 1,
                        minLength: 20,
                        maxLength: 20,
                    },
                    amount: {
                        dataType: 'uint64',
                        fieldNumber: 2,
                    },
                    unvoteHeight: {
                        dataType: 'uint32',
                        fieldNumber: 3,
                    },
                },
            },
            fieldNumber: 1,
        },
    },
}

PomTransactionAsset

Allows to send a proof-of-misbehavior transaction, reports violations of the BFT protocol by a particular delegate.

Name

reportDelegateMisbehavior

ID

3

Schema
{
    $id: 'lisk/dpos/pom',
    type: 'object',
    required: ['header1', 'header2'],
    properties: {
        header1: {
            ...blockHeaderSchema,
            fieldNumber: 1,
        },
        header2: {
            ...blockHeaderSchema,
            fieldNumber: 2,
        },
    },
}

Actions

getAllDelegates

Returns a list of all registered delegates, including their username and address. The address is returned as hex string.

Input

none

Returns

{
    username: string, (1)
    address: string, (2)
}[]
1 Username of the delegate
2 Address of the delegate as hex string.

getUnlockings

Returns a list of delegate unvotes of a certain account, the height of the unvote, and the minimum height for unlocking the tokens again.

Input

{
  address: string; (1)
}
1 Address of the account as hex string.

Returns

{
    delegateAddress: string, (1)
    amount: string, (2)
    unvoteHeight: number, (3)
    minUnlockHeight: number, (4)
}[]

Delegated Proof of Stake (DPoS)

The Lisk SDK bootstraps a blockchain network that is based on the Lisk DPoS consensus algorithm.

In DPoS based blockchains, the consensus regarding who can forge the next block is reached by users according to the votes cast.

The DPoS used by Lisk is in fact more of a middle ground between PoS and DPoS.

The DPoS related characteristic is the ability of users to register as delegate and then receive votes from other users, in order to increase their delegate weight.

The PoS related characteristic is the requirement for delegates to self-vote a certain amount of tokens, in order to increase their delegate weight. Another PoS characteristic is the mechanism for the selection of the two random standby delegates, who are selected every forging round. The higher the delegate weight, the higher the chance to be selected in one of the two random spots available for standby delegates.

Forging

The process of adding new blocks to a blockchain that uses the PoS or DPoS consensus algorithm is called forging.

The 101 active delegates and 2 additional standby delegates are selected to forge during a forging round.

During a forging round, no new calculations are required, which makes DPoS an extremely energy-friendly consensus algorithm, compared to "mining", which is the analog process for blockchains that run with Proof of Work (PoW). This technique enables a very energy efficient process of adding new blocks, which allows forging nodes to run even on machines with very limited processing capabilities, such as a Raspberry Pi.

101(amount of the active delegates) + 2(random standby delegates) = 103(Number of blocks of a forging round)

More information about the delegate selection mechanism can be found in the Lisk Protocol.

Delegate weight

The 101 delegates with the highest delegate weight are selected for the active forging positions.

The delegate weight is defined as shown below:

delegate weight = minimum { 10 * delegate self-vote , sum of all votes for the delegate }

Where delegate self-vote is the amount the delegate voted for its own account.

The sum of all votes for the delegate includes the self-votes.

Registering as delegate

In a DPoS system, each account that has an adequate enough balance to send a register delegate transaction can register a new delegate on the network. Other accounts can vote for delegates to support them with their stake. As a reward for securing the network, the forging delegates receive the transaction fees and block rewards of the forged blocks, and the transactions included in the forged blocks.

The chosen delegate name has to be unique in the network.
Example: Creating a delegate registration transaction
const tx = await APIClient.transaction.create({
    moduleID: 5,
    assetID: 0,
    fee: 1100000000,
    asset: {
        username: 'lightcurve',
    },
}, passphrase);

Voting for a delegate

Accounts can vote for delegates by sending a delegate vote transaction. The tokens used to vote for delegates will be locked. The locked tokens can be unlocked again if required, by unvoting the delegate again.

For instance, lets assume you hold an account with a balance of 100 tokens.

You could use only a part of your tokens for voting, but let’s assume you want to vote with all 100. It is of course possible to split your tokens among multiple delegates, or to use all of them to vote for only one delegate. For example you could use it to vote for 10 delegates with 10 tokens, or alternatively just on one delegate with the full 100 tokens.

In the example below we decide to vote for one delegate with 70 tokens, and for another one with 30 tokens.

Example: Vote for two delegates
const tx = await APIClient.transaction.create({
    moduleID: 5,
    assetID: 1,
    fee: 10000000,
    asset: {
        votes: [
            { delegateAddress:'11750255083444888021L', amount: '7000000000'}, (1)
            { delegateAddress:'64373847834494888026L', amount: '3000000000'} (2)
        ]
    },
}, passphrase);
1 Locks 70 tokens and adds 70 tokens delegate weight to the delegate with address 11750255083444888021L.
2 Locks 30 tokens and adds 30 tokens delegate weight to the delegate with address 64373847834494888026L.

Unvoting delegates and unlocking of tokens

The amount of tokens used for voting is locked and cannot be used for any other transactions. This includes but is not limited to further voting, balance transfers or transaction fees.

To use the locked tokens again, the account has to submit a delegate vote transaction, with a negative amount (also called “unvote”). This will start the unlocking procedure and the LSK will be ready for unlocking 2000 blocks later, (approximately 5 hours and 30 minutes).

To recover the locked tokens, the account has to submit two transactions:

  1. First, the tokens have to be unvoted. This is done with a new VoteTransaction, the transaction just needs to contain a negative amount. The tokens are now in an “unlocking” state. They have been unvoted but are not usable yet.

    Example: Unvote a delegate
    const tx = await APIClient.transaction.create({
        moduleID: 5,
        assetID: 1,
        fee: 10000000,
        asset: {
            votes: [
                { delegateAddress:'64373847834494888026L', amount: '-1500000000'} // 15 tokens can be unlocked in 2000 blocks
            ]
        },
    }, passphrase);
  2. After a 2000 block period, the tokens can be unlocked. This is done with a new UnlockTransaction. The token unlock transaction specifies which tokens have to be unlocked and added back to the balance. This mechanism is necessary to allow blocks to be reverted. Future improvements of the Lisk blockchain (particularly on the database level), could render this unlock transaction unnecessary.

    const tx = await APIClient.transaction.create({
        moduleID: 5,
        assetID: 3,
        fee: 10000000,
        asset: {
            unlockingObjects:[
                { delegateAddress:'64373847834494888026L', amount: '1500000000', unvoteHeight: '1234' }
            ]
        },
    }, passphrase);
A token unlock transaction can contain multiple unlock objects
This allows an account to submit multiple delegate votes and recover those tokens with a single unlock transaction. Of course, all tokens must have been in the unlocking state for at least 2000 blocks for the unlock to be valid.

BFT

The Byzantine Fault Tolerance (BFT) algorithm ensures that the network can reach consensus about the current state of the blockchain. This means that for a given height, eventually all Lisk nodes agree on the same block. This is particularly important in the case where there are different valid blocks for the same height, which can occur due to network delays or delegates forging multiple blocks in their designated time slot.

Additionally, delegates can be punished by anyone in the network if they forge contradicting blocks, i.e., two blocks with consensus votes that violate the Lisk-BFT protocol (see the Lisk protocol documentation for details). In order to avoid that a delegate is punished, a node operator has to take great care when enabling forging on a node. This means that it is very important to adhere to the following points below:

  • Never activate forging on more than one node. Otherwise, the delegate may be punished due to double-forge, i.e., producing two contradicting blocks for the same time slot.

  • Never activate forging without porting over the forger_info data . Otherwise, the maxHeightPreviouslyForged property of the forged blocks may be outdated, which can cause the delegate to forge contradicting blocks.

How to discover misbehavior of a delegate

Any misbehavior of a delegate is indicated by two contradicting block headers signed by the delegate. A misbehavior can therefore be reported by providing the two contradicting block headers. The code below checks if two contradicting block headers exist.

Detecting Contradicting Block Headers
function checkHeadersContradicting(blockHeader1,blockHeader2) {
   // Order the two block headers such that b1 must be forged first
   let b1=blockHeader1;
   let b2=blockHeader2;
   if(b1.maxHeightPreviouslyForged>b2.maxHeightPreviouslyForged ||
     (b1.maxHeightPreviouslyForged==b2.maxHeightPreviouslyForged && b1.maxHeightPrevoted>b2.maxHeightPrevoted) ||
     (b1.maxHeightPreviouslyForged==b2.maxHeightPreviouslyForged && b1.maxHeightPrevoted==b2.maxHeightPrevoted && b1.height>b2.height)){
      b1=blockHeader2;
      b2=blockHeader1;
   }

   // The order of cases is essential here
   if(b1.delegatePubKey!=b2.delegatePubKey) {
      // Blocks by different delegates are never contradicting
      return false;
   } else if(b1.blockID==b2.blockID) {
      // No contradiction, as block headers are the same
      return false;
   } else if (b1.maxHeightPrevoted==b2.maxHeightPrevoted &&  b1.height>=b2.height) {
      // Violation of the fork choice rule as delegate moved to different chain
      // without strictly larger maxHeightPrevoted or larger height as justification.
      // This in particular happens, if a delegate is double forging.
      return true;
   } else if(b1.height>b2.maxHeightPreviouslyForged) {
      // Violates disjointness condition
      return true;
   } else if(b1.maxHeightPrevoted>b2.maxHeightPrevoted) {
      // Violates that delegate chooses branch with largest maxHeightPrevoted
      return true;
   } else {
      // No contradiction between block headers
      return false;
   }
}

(Source: LIP 14)

Reporting a delegate

A delegate misbehavior report transaction can be issued by anyone in the network who observes a violation of the Lisk-BFT consensus algorithm, as explained in How to discover misbehavior of a delegate.

Reporting delegate misbehavior
const tx = await APIClient.transaction.create({
    moduleID: 5,
    assetID: 3,
    fee: 10000000,
    asset:{
        header1: {
            blockSignature: 'e8b4768a7805bdcef097458e52b4acc5aed9816032504a57a0ae14ede0054bd916ddc0ff93a4baac91048930afde72f0e89a9fd5b07bd98620e3d5558b34b005',
            generatorPublicKey: '7a7f24c061db6a92320ba14323f814c20dbcc811a931ead3ca63c75a4de1b643',
            height: 8938,
            maxHeightPreviouslyForged: 8788,
            maxHeightPrevoted: 8868,
            numberOfTransactions: 0,
            payloadHash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
            payloadLength: 0,
            previousBlockId: '9326981395427095175',
            reward: '500000000',
            seedReveal: 'abe2a66d7a35fd7b580e977d9f7911ae',
            timestamp: 122329567,
            totalAmount: '0',
            totalFee: '0',
            version: 2
        },
        header2: {
            blockSignature: '31ccf4ce1a3a224a2a32c3f4bdc6fad0ddb8feb45b05b7d411eee1a608f9d91284d09c727bba173c882d5dc90cb951c5affc10462d650031a627e00d919cbf08',
            generatorPublicKey: '7a7f24c061db6a92320ba14323f814c20dbcc811a931ead3ca63c75a4de1b643',
            height: 8933,
            maxHeightPreviouslyForged: 8788,
            maxHeightPrevoted: 8868,
            numberOfTransactions: 0,
            payloadHash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
            payloadLength: 0,
            previousBlockId: '9326981395427095175',
            reward: '500000000',
            seedReveal: 'abe2a66d7a35fd7b580e977d9f7911ae',
            timestamp: 122329567,
            totalAmount: '0',
            totalFee: '0',
            version: 2
        }
    },
}, passphrase);

Consequences for punished delegates and their voters

If a valid delegate misbehavior report is posted to the network, the respective delegate will face the following consequences:

  • Setting the delegate weight to 0 for the next 780,000 blocks (approximately 3 months).

  • The unlocking period for self-votes is increased from 260,000 blocks to 780,000 blocks (from approximately 1 month to 3 months).

Voters of the respective delegate will receive the following punishment:

  • The unlocking period for the votes for the punished delegate is increased from 2,000 blocks to 260,000 blocks (from approximately 5 hours to 1 month).