Blocks and Transactions

Blocks

Blocks following the Lisk protocol have three main properties:

  1. Block header: includes properties relevant to the consensus domain.

  2. Block assets: an array of objects containing data injected during the block creation.

  3. Transactions: The transactions included in the block. Each block can include a maximum of 15 kB of transactions.

Anatomy of a Lisk block
Figure 1. Anatomy of a block

Detailed descriptions of the main properties of a block are provided in the following sections.

Block JSON schema

Blocks are serialized and deserialized accordingly to the following JSON schema.

blockSchema = {
  "type": "object",
  "required": ["header", "transactions", "assets"],
  "properties": {
    "header": {
      "dataType": "bytes",
      "fieldNumber": 1
    },
    "transactions": {
      "type": "array",
      "fieldNumber": 2,
      "items": {
        "dataType": "bytes"
      }
    },
    "assets": {
      "type": "array",
      "fieldNumber": 3,
      "items": {
        "dataType": "bytes"
      }
    }
  }
}

Block header

Properties handled by the consensus domain are added to the block header.

Block ID
The block ID is calculated by hashing the complete block header of a signed block.

The most important properties of the block header are:

  • timestamp: Time the block was created as Unix timestamp.

  • height: The height of a block is always = height of the previous block + 1.

  • previousBlockID: The ID of the previous block.

  • generatorAddress: The address of the block generator[1].

  • transactionRoot: The transaction root is the root of the Merkle tree built from the ID of the transactions contained in the block.

  • stateRoot: The root of the Sparse Merkle Tree that is computed from the state of the blockchain. The state root is the root of the Sparse Merkle Tree built from the state of the chain after the block has been processed[2].

Besides the above properties, each block header contains various additional properties, required for deeper technical aspects of the Lisk blockchain. Expand below boxes for a description of the additional properties, and see the JSON schema for an overview of all properties included in the header. Check LIP 0055 for additional information about the block header.

Remaining properties of the block header
  • version: The block header version must be equal the value of a block of the previous protocol plus one.

  • assetRoot: The root of the Merkle tree computed from the Block assets array.

  • eventRoot: The root of the Sparse Merkle Tree that is computed from the events emitted during the block processing[3].

  • maxHeightPrevoted: This property is related to the Lisk-BFT protocol and is used for the fork choice rule.

  • maxHeightGenerated: This property is related to the Lisk-BFT protocol and is used to check for contradicting block headers.

  • validatorsHash: This property authenticates the set of validators active from the next block onward. It is important for cross-chain certification and included in certificates.

  • aggregateCommit: This property contains the aggregate BLS signature for a certificate and the height of the certified block. It attests that all signing validators consider the corresponding block final. Based on this, any node can create a certificate for the given height[4].

  • signature: Signature of the validator who created the block.

Block header JSON schema

Block headers are serialized and deserialized accordingly to the following JSON schema.

blockHeaderSchema = {
  "type": "object",
  "required": [
    "version",
    "timestamp",
    "height",
    "previousBlockID",
    "generatorAddress",
    "transactionRoot",
    "assetRoot",
    "eventRoot",
    "stateRoot",
    "maxHeightPrevoted",
    "maxHeightGenerated",
    "validatorsHash",
    "aggregateCommit",
    "signature"
  ],
  "properties": {
    "version": {
      "dataType": "uint32",
      "fieldNumber": 1
    },
    "timestamp": {
      "dataType": "uint32",
      "fieldNumber": 2
    },
    "height": {
      "dataType": "uint32",
      "fieldNumber": 3
    },
    "previousBlockID": {
      "dataType": "bytes",
      "fieldNumber": 4
    },
    "generatorAddress": {
      "dataType": "bytes",
      "fieldNumber": 5
    },
    "transactionRoot": {
      "dataType": "bytes",
      "fieldNumber": 6
    },
    "assetRoot": {
      "dataType": "bytes",
      "fieldNumber": 7
    },
    "eventRoot": {
      "dataType": "bytes",
      "fieldNumber": 8
    },
    "stateRoot": {
      "dataType": "bytes",
      "fieldNumber": 9
    },
    "maxHeightPrevoted": {
      "dataType": "uint32",
      "fieldNumber": 10
    },
    "maxHeightGenerated": {
      "dataType": "uint32",
      "fieldNumber": 11
    },
    "validatorsHash": {
      "dataType": "bytes",
      "fieldNumber": 12
    },
    "aggregateCommit": {
      "type": "object",
      "fieldNumber": 13,
      "required": [
        "height",
        "aggregationBits",
        "certificateSignature"
      ],
      "properties": {
        "height": {
          "dataType": "uint32",
          "fieldNumber": 1
        },
        "aggregationBits": {
          "dataType": "bytes",
          "fieldNumber": 2
        },
        "certificateSignature": {
          "dataType": "bytes",
          "fieldNumber": 3
        }
      }
    },
    "signature": {
      "dataType": "bytes",
      "fieldNumber": 14
    }
  }
}

Block assets

Block assets allow the blockchain to store specific data inside of each block.

Each entry of the block assets is then inserted in a Merkle tree, whose root is included in the Block header as the assetRoot property.

Inserting the assets root rather than the full assets allows to bound the size of the block header to a fixed size, while still authenticating the content of the block assets.

As an example, blockchains created with the Lisk SDK that implements the Random module will insert the seed reveal property in the block assets.

JSON schema

The schema for the block assets allows each module to include its serialized data individually, which makes the inclusion of module data very flexible.

Each module can insert a single entry in the assets. This entry is an object containing a module property, indicating the name of the module handling it, and a generic data property that can contain arbitrary serialized data.

Block asset schema
blockAssetSchema = {
	$id: '/block/asset/3',
	type: 'object',
	required: ['module', 'data'],
	properties: {
		module: {
			dataType: 'string',
			fieldNumber: 1,
		},
		data: {
			dataType: 'bytes',
			fieldNumber: 2,
		},
	},
};

Transactions

Transactions are sent to the blockchain by its users to trigger state mutations on the blockchain.

To be accepted by the blockchain, the transactions must be transmitted in the expected format, including all the required properties of a transaction, and pass the transaction & command verification steps explained in the Block execution process description.

Valid transactions trigger the corresponding command of a module that accepts this transaction type. Therefore, each transaction always needs to include the IDs of the module and command that the transaction wants to trigger. If any specific data input from the user is needed to complete the command, they are included under the params property of a transaction. Besides this, there are a few additional properties that every transaction should contain, which are described in image Figure 3 and below.

After a transaction is sent to a node, it is first added to the transaction pool, waiting to be included in a block. The transactions to be included in the block are then always picked from there.

Transaction properties
Figure 2. Properties of a transaction object
  • module: A string identifying the module name the transaction is addressing.

  • command: A string identifying the specific command name in the module.

  • nonce: An integer that is unique for each transaction from the account corresponding to the senderPublicKey. Increments by +1 for each transaction.

  • fee: An integer that specifies the fee in Beddows to be spent by the transaction.

  • senderPublicKey: The public key of the account issuing the transaction. A valid public key is 32 bytes long.

  • params: The serialized parameters of the module command.

  • signatures: An array with the signatures of the transaction. A transaction is signed by the sender account to verify its correctness. In the case of a multi-signature transaction, several accounts need to sign a transaction, before it is accepted by a node.

How many transactions fit in a block?

How many transactions can actually fit into a block? The answer to this question very much depends on the size of the particular transactions. As every transaction type expects a different set of params to be included in the transaction, the size of transactions can vary significantly between different transaction types.

Let’s make an example of simple token transfer transactions. If you assume all transactions are the simplest token transfers (Alice sends 5LSK to Bob etc.) then the size of each transaction is 153 Bytes. Each block can include a maximum of 15 kB of transactions. This results in maximum 100 token transfer transactions per block:

Total transactions size  = 15360 (15 x 1024)
transaction size = 153
15360/153 = 100.39 maximum token transfer transactions per block

Transaction fees

In order to send and process a transaction, it is required to pay a small fee.

Fees can be defined dynamically for every transaction. A higher fee will create an incentive for validators to include the transaction in a block as quickly as possible, because most validators prioritize transactions with higher fees in the transaction pool, because it means a personal higher reward for them, if they include these transactions in a block. So by choosing a higher fee, it can be ensured that the transaction is executed as quickly as possible, in case the network is crowded.

This mechanism can also be used to overwrite existing transactions in the pool, by using the same nonce, but a higher fee than the transaction that should be overwritten.

For every transaction, there is a minimum fee that needs to be paid in order for the transaction to be successfully processed. The minimum fee required is calculated in the following way:

minFee = transactionBytes.length * minFeePerByte

As shown in the formula, the minimum fee is directly connected to the size of the transaction object in bytes after it has been encoded.

Some transaction types may also require an additional fee, called the Command fee. The minimum fee for a valid transaction is in this case:

minFee = transactionBytes.length * minFeePerByte + commandFee
  • If a transaction requires a command fee, and the command fee is partially paid or not paid at all, then, the transaction will be invalid.

  • If the transaction is posted with a fee smaller than the required minimum fee, the transaction will fail.

Minimum fee per byte

The minimum fee per byte is defined in the configuration options of the Fee module.

The default value for minFeePerByte is 1000 Beddows, or 0.00001 LSK.

It is possible to configure a blockchain to have no transaction fee, by setting the minFeePerByte to 0 in the config.

Command fee

Command fees, or command execution fees, are fees that need to be paid only, if the command execution requires an additional fee - for example, if the execution of the command requires a lot of computational resources.

The following commands require an extra command fee:

Transaction JSON schema

Transaction schema
transactionSchema = {
	$id: '/lisk/transaction',
	type: 'object',
	required: ['module', 'command', 'nonce', 'fee', 'senderPublicKey', 'params'],
	properties: {
		module: {
			dataType: 'string',
			fieldNumber: 1,
			minLength: 1,
			maxLength: 32,
		},
		command: {
			dataType: 'string',
			fieldNumber: 2,
			minLength: 1,
			maxLength: 32,
		},
		nonce: {
			dataType: 'uint64',
			fieldNumber: 3,
		},
		fee: {
			dataType: 'uint64',
			fieldNumber: 4,
		},
		senderPublicKey: {
			dataType: 'bytes',
			fieldNumber: 5,
			minLength: 32,
			maxLength: 32,
		},
		params: {
			dataType: 'bytes',
			fieldNumber: 6,
		},
		signatures: {
			type: 'array',
			items: {
				dataType: 'bytes',
			},
			fieldNumber: 7,
		},
	},
};

Valid vs invalid transactions

Only valid transactions should be added to a block during the block generation, as an invalid transaction makes the whole block invalid, meaning that it would be discarded by any node in the network.

A transaction is valid, if the following stages associated with the transaction of Block execution are executed successfully without errors:

  • "transaction verification"

  • "command verification"

  • "before command execution" and

  • "after command execution"

Otherwise, a transaction is invalid.

Successful vs failed transactions

A valid transaction is executed successfully if additionally the "command execution" stage of Block execution is executed successfully without errors.

A valid transaction fails if on the other hand an error occurs during the command execution. In this case, all state transitions of the "command execution" stage are reverted. This means that the transaction has no effect except for those defined in "before command execution" and "after command execution". The result of the transaction execution is logged using an event emitted at the end of the "after transaction execution" stage, indicating whether the transaction was processed successfully or an error occurred.

Block generation

The block generation flow offers a lot of flexibility for custom business logic by providing hooks for executing additional custom logic before and after each execution of a transaction and/or command. The gradual steps make all important verification steps explicit and obvious.

Block generation steps
Figure 3. Block generation steps

The full generation of a block is organized as follows.

  1. Header initialization: Block header properties that require access to the state store before any state transitions implied by the block are executed are inserted in this stage.

    Sets the version, timestamp, height, previousBlockID, generatorAddress, maxHeightPrevoted, maxHeightGenerated, and aggregateCommit properties of the Block header.

  2. Assets insertion: Each module can insert information in the block assets.

  3. Before transactions execution: Each module can define protocol logic that is executed before the transactions contained in the block are processed. After this stage has been completed, transactions are selected one-by-one from a transaction pool.

  4. Transaction verification: Each module can define protocol logic that verifies a transaction, possibly by accessing the state store. If an error occurs, the transaction is invalid and it is not included in the block. The transaction processing stages (steps 4 to 8) are repeated for each transaction selected. If steps 4, 5, 6, and 8 are executed successfully, the transaction is valid and it is included in the block, otherwise, it is invalid and therefore discarded.

  5. Command verification: The command corresponding to the module-command combination is verified. If an error occurs, the transaction is invalid and it is not included in the block.

  6. Before command execution: Each module can define protocol logic that is processed before the command has been executed. If an error occurs, the transaction is invalid, it is not included in the block, and all state transitions induced by the transaction are reverted. In that case, the block generation continues with step 4 for another transaction from the transaction pool or step 9.

  7. Command execution: The command corresponding to the module-command combination is executed. If an error occurs, the transaction is failed and all state transitions performed in this stage are reverted. In any case, afterward, the processing continues with the next stage.

  8. After command execution: Each module can define protocol logic that is processed after the command has been executed. If an error occurs, the transaction is invalid, it is not included in the block, and all state transitions induced by the transaction performed up to this stage are reverted. In that case, the block generation continues with step 4 for another transaction from the transaction pool or step 9.

  9. After transactions execution: Each module can define protocol logic that is executed after all the transactions contained in the block have been processed.

  10. Header finalization: Block header properties, which require accessing the state store after all state transitions implied by the block have been executed, are inserted.

    Sets the transactionRoot, assetRoot, eventRoot, stateRoot, validatorsHash, and signature properties of the Block header.

  11. Block processing: The block goes through the Block execution stages.

Block execution

Block execution happens when the consensus domain receives a new block from a peer.

The block execution flow offers a lot of flexibility for custom business logic by providing hooks for executing additional custom logic before and after each execution of a transaction and/or command. The gradual steps make all important verification steps explicit and obvious.

Block execution steps
Figure 4. Block execution steps

The full processing of a block is organized as follows:

  1. Block reception: A new block is received from the P2P network.

  2. Fork choice: Upon receiving a new block, the fork choice rule determines whether the block will be discarded or if the processing continues.

  3. Static validation: Some initial static checks are done to ensure that the serialized object follows the general structure of a block. These checks are performed immediately because they do not require access to the state store and can therefore be done very quickly.

    • Validates, if:

      • the block follows the block schema.

      • the total size of the serialized transactions contained in the block is at most the maximum allowed size for transactions per block.

      • the block header is valid:

        • checks that the block header follows the block header schema.

        • validates the version, transactionRoot, and assetRoot properties.

      • the block assets are valid:

        • each entry in the assets array has a module property set to the name of a module registered in the chain.

        • the data property has a size that is at most equal to the max size of an assets entry in bytes.

        • each module can insert at most one entry in the block assets.

  4. Header verification: Block header properties that require access to the state store before any state transitions implied by the block are executed are verified in this stage.

    Verifies timestamp, height, previousBlockID, generatorAddress, maxHeightPrevoted, maxHeightGenerated, aggregateCommit, and signature properties of the Block header.

  5. Assets verification: Each module verifies the respective entry in the block assets. If any check fails, the block is discarded and has no further effect.

  6. Block forwarding: After the initial checks, the full block is forwarded to a subset of peers.

  7. Before transactions execution: Each module can define protocol logic that is executed before the transactions contained in the block are processed.

  8. Transaction verification: Each module can define protocol logic that verifies a transaction, possibly by accessing the state store. If an error occurs, the transaction is invalid and the whole block is discarded.

  9. Command verification: The command corresponding to the module-command combination is verified. If an error occurs, the transaction is invalid and the whole block is discarded.

  10. Before command execution: Each module can define protocol logic that is processed before the command has been executed. If an error occurs, the transaction is invalid and the whole block is discarded.

  11. Command execution: The command corresponding to the module-command combination is executed. If an error occurs, the transaction is failed and all state transitions performed in this stage are reverted. In any case, afterward, the processing continues with the next stage.

  12. After command execution: Each module can define protocol logic that is processed after the command has been executed. If an error occurs, the transaction is invalid and the whole block is discarded.

  13. After transactions execution: Each module can define protocol logic that is executed after all the transactions contained in the block have been processed.

  14. Result verification: Block header properties, which require accessing the state store after all state transitions implied by the block have been executed, are verified.

    Verifies the stateRoot, eventRoot, and validatorsHash properties of the Block header.

  15. Block storage: The block is persisted into the database.

  16. Peers notification: Other peers in the P2P network are notified of the new block.

Genesis block execution

The genesis block describes the very first block in the blockchain. It defines the initial state of the blockchain at the start of the network.

The genesis block is not generated by a validator, such as all the other blocks which come after the genesis block. Instead, it is defined by the developer, when creating the Application instance of the blockchain client.
Genesis Block execution steps
Figure 5. Genesis block execution steps

Each step in the application layer is repeated for each module registered in the application.


1. Previously the `generatorPublicKey` property (see LIP 0055 for more information).
2. See LIP 0040 for the reason why it needs to be included in a block header.
3. See LIP 0065 for the reason why it needs to be included in a block header.
4. See LIP 0061 for more details.