SDK Migration Guide

The Lisk SDK v6 introduces many breaking changes and essential improvements to the Lisk Protocol and the Lisk Framework.

This migration guide summarizes the most important changes from Lisk SDK v5 to Lisk SDK v6, and explains why the changes are introduced.

1. Terminology changes

To improve understanding, we introduce several terminology changes.

An overview of the most important terminology updates is summarized in the table below.

Table 1. Terminology changes SDKv6 vs SDKv5
SDKv6 (new) SDKv5 (old) Description

Block generation

Forging

The process of creating a new block in the Lisk ecosystem.

Chain ID

Network ID

Unique identifier of a chain account. It is used when sending tokens to a specific chain and is also prepended for signing to protect against replay attacks.

Module method

Module reducer

An interface for module-to-module communication.

Module/Plugin endpoint

Module/Plugin action

An interface between a module/plugin and an external system via an RPC endpoint.

PoS

DPoS

The validator selection algorithm of the Lisk Mainchain. In fact, the algorithm used by Lisk can be considered as a combination of DPoS and PoS. As the similarities with PoS (Proof of Stake) are quite big, it was decided to rename the DPoS module to PoS and to replace DPoS-specific terminology with more general terms.

RPC event

Application event

Events that are natively included in the Lisk Framework and follow the publish/subscribe pattern.

RPC node

Non-forging node

A type of node which allows users to query information from a blockchain network.

Web3 app, blockchain application

Blockchain application

An application that utilizes blockchain technology. The application typically includes business logic running on blockchain clients (modules or smart contracts), middleware and UI.

Lisk app, app on Lisk

Blockchain application

A Web3 app built on Lisk.

Blockchain client

A client is an implementation of the blockchain protocol that verifies data (blocks and transactions) against the protocol rules and keeps the network secure. In Lisk, Lisk Core is the client for the Lisk Mainchain and clients for other blockchains in the Lisk ecosystem can be built using the Lisk SDK.

Blockchain node

A node is an instance of a blockchain client that runs on a computer and is connected to other computers as part of the decentralized blockchain network.

Sidechain

A separate, independent blockchain that runs in parallel to a main blockchain. It is typically interoperable with the main blockchain. In Lisk, a sidechain is a blockchain registered on the Lisk mainchain following the Lisk Interoperability Protocol.

Staker

Voter

A user staking LSK to "vote"/increase the delegate weight of their favourite validator(s).

Staking

Voting

Process of locking tokens to support a specific validator.

Validator

Delegate

Participants in charge of generating, processing and finalizing blocks in a PoS blockchain.

Validator node

Forging node

A type of node which has the capability of generating a block.

2. Failed transactions are included in blocks

In the event of a failed execution of a transaction’s business logic, the transaction will be assigned the status of fail. In Lisk SDK v5, transactions were only included in a block, if they could be executed successfully. In Lisk SDK v6, this changes now: Failed transactions are included in blocks as well.

As you know, the transaction fee is paid for every transaction included in a block. This has the following consequence: By including failed transactions in blocks, the transaction fee is always paid even if it failed and didn’t introduce any state changes on the blockchain.

Main benefits of this change
Increased rewards for validators

By doing this, validators will still be rewarded for executing the logic of the transaction until the point where it failed.

Increased security

Additionally, it mitigates the danger of DDoSing blockchain networks by spamming transactions that will fail, because the transaction fee has to be paid in any case.

To verify, if a transaction that is included in a block was executed successfully, Lisk SDK v6 introduces the Addition of blockchain events.

3. Addition of blockchain events

Blockchain events are newly introduced, and Module pub/sub events are completely removed in Lisk SDK v6.

Although similar in name, Blockchain events follow a completely different approach. To avoid confusion between the two events, please read the summary below, which is comparing the new and old events inside modules.

Main benefits of this change
Required, if failed transactions are included in blocks

As explained above, Failed transactions are included in blocks in v6.

This means, it cannot be assumed, that a transaction was successfully executed, just by checking that the transaction is included in a finalized block.

It could happen that the transaction inside a block has failed, and wasn’t executed on the blockchain.

But how to check if the transaction failed, or was executed successfully? To transmit this information, the standard event is emitted for every transaction included in the particular block. It informs if that particular transaction was successfully executed, or failed.

By adding events, it is therefore possible to check if a transaction was executed successfully.

Enhanced developer experience

Events can store various additional information on-chain, which can be valuable for other services. Additional events can be defined per module by the blockchain developer.

  • Blockchain events - SDKv6

  • Module events - SDKv5

Description

Blockchain events are introduced newly in Lisk SDK v6. Blockchain events are still defined and emitted inside the module, but it is not possible to subscribe to them via the RPC API of a node.

Instead, blockchain events are logged per block, i.e. directly included in the block header and can be queried through the RPC API of a node by requesting the RPC endpoint chain_getEvents.

Blockchain events in Lisk SDK v6 are following a concept comparable to the Ethereum event log.

Besides the newly introduced blockchain events, there is still a couple of RPC events included in the Lisk Framework, which can be retrieved via public-subscribe, as in v5.

The blockchain developer can define new blockchain events per module as desired, but the RPC events are not customizable any more. Only new blockchain events can be included in the blockchain client by the developer.

Purpose

Blockchain events are a way for modules to store important information which is not included in the transactions or block assets, verifiable using eventRoot property present inside the block header.

They are part of the overall state of the blockchain, as an event root of all events included in a particular block is stored in the block header.

They can include a lot of additional data if required, as the events themselves can be removed from the stores of the node after a certain time, and therefore don’t "pollute" the blockchain itself.

Definition

public constructor() {
    super();
    // registration of stores and events
    this.events.register(NewHelloEvent, new NewHelloEvent(this.name));
}

For more information on how to create the corresponding event class, please check out the guide: How to create a blockchain event

Publishing

const newHelloEvent = this.events.get(NewHelloEvent);
newHelloEvent.add(context, {
    senderAddress: context.transaction.senderAddress,
    message: context.params.message
},[ context.transaction.senderAddress ]);

Retrieving

In Lisk SDK v6, events are requested per block height after an event is emitted.

Retrieving events from a node:

curl --location --request POST 'http://localhost:7887/rpc' \
--header 'Content-Type: application/json' \
--data-raw '{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "chain_getEvents",
    "params": {
        "height": 123
    }
}'

Description

Analog to the application events from v5, module events could be subscribed to via the RPC API of a node.

An event informs services that subscribed to it, if a certain event happened (e.g. a transaction was executed), and often contain additional data, providing more information or context about the event.

Module events in Lisk SDK v5 follow the publish-subscribe-pattern.

Purpose

Events are used to communicate about certain events in real time, and to prevent reoccuring RPC request, just to check if there are any changes.

Definition

public events = ['newHello'];

For more information how to create the corresponding event class, please check out the guide How to create a blockchain event

Publishing

this._channel.publish('hello:newHello', {
    sender: tx.senderAddress.toString('hex'),
    hello: helloAsset.helloString
});

Retrieving

In Lisk SDK v5, events could be subscribed directly via the API client.

If an event was missed, there was no way of retrieving the event, after it was emitted by a node.

client.subscribe('app:block:new', ( data ) => {
  console.log('new block:',data);
});

4. Chain ID replaces network ID

Chain identifiers (or chain IDs) for transaction signatures and block signatures were already introduced in Lisk SDK v5 as "network identifiers" to prevent replay attacks on other chains. In Lisk SDK v6, the chain ID is now additionally used to identify chains for making cross chain transactions in the Lisk ecosystem.

Besides getting a more descriptive name, the chain ID is also constructed differently now:

Chain ID (Lisk SDK v6)

Network ID (Lisk SDK v5)

Description

Unique identifier of a blockchain network for transactions and blocks to prevent replay attacks on other chains.

Unique identifier of a blockchain network for transactions and blocks to prevent replay attacks on other chains.

Bytes

4

32

Creation

Defined by the blockchain developer

Randomly generated

Read the LIP 0037 for more information about the chain ID.
  • Chain ID example

  • Network ID example

00000000
4c09e6a781fc4c7bdb936ee815de8f94190f8a7519becd9de2081832be309a99

Chain identifiers are 4-byte values that follow a specific format: the first byte is used to identify the network in which the chain is running (either the Lisk Mainnet, Lisk Testnet, or any other test network); the other 3 bytes identify the blockchain within the network.

The network-specific prefix is included explicitly to ensure that a chain does not use the same chain identifier in the test network as in the mainnet.
Main benefits of this change
Improved developer experience
  • The chain identifier can be directly set by the blockchain creator, which is more convenient than generating a random 32-byte value.

Improved user experience
  • By using a much shorter ID, users can easily verify that they are signing a transaction for the correct blockchain.

5. Module & command IDs are removed

The module and command IDs are removed completely in Lisk SDKv6.

Instead of IDs, the name of a module and the name of a command are now used as unique identifiers for modules and commands, respectively.

Main benefits of this change
Enhanced developer experience

Reduces the number of required properties and uses strings which are more descriptive than numbers.

6. Methods replace reducers

The module reducers are renamed to methods.

Methods in Lisk SDK v6 still have the same purpose as reducers in v5, but besides the name change, they are also defined a bit differently, as summarized below:

Main benefits of this change
Improved developer experience
  • By providing a base class for the creation of module methods, developers can follow a dedicated pattern to include methods into a module in a straightforward manner.

  • The renaming from reducers to methods was introduced to improve intuitive understanding of the meaning behind this data structure.

  • Methods - SDKv6

  • Reducers - SDKv5

Name

Method

Description

An interface for module-to-module communication.

Definition

  1. Define methods in a class which extends from the BaseMethod:

    import { BaseMethod, ImmutableMethodContext } from 'lisk-sdk';
    import { MessageStore, MessageStoreData } from './stores/message';
    
    export class HelloMethod extends BaseMethod {
    
    	public async getHello(
    		methodContext: ImmutableMethodContext,
    		address: Buffer,
    	): Promise<MessageStoreData> {
            // 1. Get message store
    		const messageSubStore = this.stores.get(MessageStore);
            // 2. Get the Hello message for the address from the message store
    		const helloMessage = await messageSubStore.get(methodContext, address);
            // 3. Return the Hello message
    		return helloMessage;
    	}
    }
  2. Assign the method attribute of the module to an instance of the Method class, which was created above:

    import { HelloMethod } from './method';
    
    export class HelloModule extends BaseModule {
    	// [...]
    	public method = new HelloMethod(this.stores, this.events);
        // [...]
    }

Usage

import { TokenMethod } from '../../../token';

export class SidechainRegistrationCommand extends BaseInteroperabilityCommand {
	public schema = sidechainRegParams;
	private _tokenMethod!: TokenMethod;

	public addDependencies(tokenMethod: TokenMethod) {
		this._tokenMethod = tokenMethod;
	}
    public async verify(
		context: CommandVerifyContext<SidechainRegistrationParams>,
	): Promise<VerificationResult> {
        // ...
        // Sender must have enough balance to pay for extra command fee.
		const availableBalance = await this._tokenMethod.getAvailableBalance(
			context.getMethodContext(),
			senderAddress,
			TOKEN_ID_LSK,
		);
		if (availableBalance < REGISTRATION_FEE) {
            // ...
		}
        // ...
	}
}

Description

An interface for module-to-module communication.

Definition

export class TokenModule extends BaseModule {
	// [...]
	public reducers = {
		credit: async (params: Record<string, unknown>, stateStore: StateStore): Promise<void> => {
			// [...]
		},
		debit: async (params: Record<string, unknown>, stateStore: StateStore): Promise<void> => {
			// [...]
		},
		getBalance: async (
			params: Record<string, unknown>,
			stateStore: StateStore,
		): Promise<bigint> => {
			const { address } = params;
			if (!Buffer.isBuffer(address)) {
				throw new Error('Address must be a buffer');
			}
			const account = await stateStore.account.getOrDefault<TokenAccount>(address);
			return account.token.balance;
		}
	};
    // [...]
}

Usage

Reducers can be invoked through the reducerHandler, which is available inside the lifecycle hooks and assets of a module.

await reducerHandler.invoke("token:debit", {
  address: senderAddress,
  amount: asset.initValue,
});

7. Endpoints replace actions

The module actions are renamed to endpoints.

Endpoints in Lisk SDK v6 still have the same purpose as actions in v5, but besides the name change, they are also defined a bit differently, as summarized below:

Main benefits of this change
Improved developer experience
  • By providing a base class for the creation of module endpoints, developers can follow a dedicated pattern to include endpoints into a module in a straightforward manner.

  • The renaming from actions to endpoints was introduced to improve intuitive understanding of the meaning behind this data structure.

  • Endpoints - SDKv6

  • Actions - SDKv5

Description

An interface between a module and an external system via an RPC endpoint.

Definition

import { BaseEndpoint, ModuleEndpointContext, cryptography } from 'lisk-sdk';
import { MessageStore, MessageStoreData } from './stores/message';

export class HelloEndpoint extends BaseEndpoint {
    public async getHello(ctx: ModuleEndpointContext): Promise<MessageStoreData> {
        // 1. Get message store
        const messageSubStore = this.stores.get(MessageStore);
        // 2. Get the address from the endpoint params
        const { address } = ctx.params;
        // 3. Validate address
        if (typeof address !== 'string') {
            throw new Error('Parameter address must be a string.');
        }
        cryptography.address.validateLisk32Address(address);
        // 4. Get the Hello message for the address from the message store
        const helloMessage = await messageSubStore.get(
            ctx,
            cryptography.address.getAddressFromLisk32Address(address),
        );
        // 5. Return the Hello message
        return helloMessage;
    }
}

Usage

curl --location --request GET 'http://localhost:7887/rpc' \
--header 'Content-Type: application/json' \
--data-raw '{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "hello_getHello",
    "params": {
        "address": "lskuz5p98kz3mqzxnu68qdrjxtvdvr2o7pprtj4yv"
    }
}'
const data = await client.invoke('namespace_endpointName', input);
console.log(data);

Description

An interface between a module and an external system via an RPC endpoint.

Definition

export class HelloModule extends BaseModule {

    // ...

    public actions = {
        amountOfHellos: async () => {
            const res = await this._dataAccess.getChainState(CHAIN_STATE_HELLO_COUNTER);
            const count = codec.decode(
                helloCounterSchema,
                res
            );
            return count;
        },
    };

    // ...
}

Usage

const data = await client.invoke('app:actionName', input);

8. Migration of store from v5 to v6

A store, aka key-value store is a special kind of database that follows a data storage paradigm designed for storing, retrieving, and managing associative arrays.

In the Lisk SDK, stores are used to store the on-chain and off-chain related data of a node. Each module has its own dedicated store, which only the module itself can access.

In Lisk SDK v5, this principle was not followed consistently: There were the chain store and the account store, and the account store was accessible by every module.

In Lisk SDK v6, the account store is removed completely, and integrated into the respective module stores. Additionally, the implementation of stores into modules is improved, to store any key-value pair in the database consistently.

Main benefits of this change
Improved developer experience
  • By providing a base class for the creation of module stores, developers can follow a dedicated pattern to include stores into a module in a consistent manner.

Improved modularity
  • Confines data per module

Table 2. Stores in Lisk SDK v6

Definition

How to define a new module store
import { BaseStore } from 'lisk-sdk';

export interface MessageStoreData {
	message: String;
}

export const messageStoreSchema = {
	$id: '/hello/message',
	type: 'object',
	required: ['message'],
	properties: {
		message: {
			dataType: 'string',
			fieldNumber: 1,
		},
	},
};

export class MessageStore extends BaseStore<MessageStoreData> {
	public schema = messageStoreSchema;
}
How to register stores with the module
import { CounterStore } from './stores/counter';
import { MessageStore } from './stores/message';


export class HelloModule extends BaseModule {
    // [...]

    public constructor() {
        super();
        // registration of stores and events
        this.stores.register(CounterStore, new CounterStore(this.name));
        this.stores.register(MessageStore, new MessageStore(this.name));
    }
    // [...]
 }

Usage

Example: How to get data from the store
import { BaseEndpoint, ModuleEndpointContext, cryptography } from 'lisk-sdk';
import { MessageStore, MessageStoreData } from './stores/message';

export class HelloEndpoint extends BaseEndpoint {
    public async getHello(ctx: ModuleEndpointContext): Promise<MessageStoreData> {
        // 1. Get message store
        const messageSubStore = this.stores.get(MessageStore);
        // 2. Get the address from the endpoint params
        const { address } = ctx.params;
        // 3. Validate address
        if (typeof address !== 'string') {
            throw new Error('Parameter address must be a string.');
        }
        cryptography.address.validateLisk32Address(address);
        // 4. Get the Hello message for the address from the message store
        const helloMessage = await messageSubStore.get(
            ctx,
            cryptography.address.getAddressFromLisk32Address(address),
        );
        // 5. Return the Hello message
        return helloMessage;
    }
}

9. Addition of metadata

Lisk SDK v6 introduces a new RPC endpoint to get all existing metadata related to a node. This includes metadata of all the modules which are registered on the node.

The module developer can now easily define which data should be returned by the endpoint for the particular module by adjusting the newly introduced metadata method.

Main benefits of this change
Improved user experience
  • Users can now query all the relevant metadata about a module in a consistent manner.

Improved modularity
  • Metadata for a module is now defined inside it.

  • The metadata to be returned can be defined for each module individually.

How to define metadata for a module
export class HelloModule extends BaseModule {
    // [...]

	public metadata(): ModuleMetadata {
		return {
			name: '',
			endpoints: [],
			commands: this.commands.map(command => ({
				name: command.name,
				params: command.schema,
			})),
			events: this.events.values().map(v => ({
				name: v.name,
				data: v.schema,
			})),
			assets: [],
		};
	}

    // [...]
}

Use the RPC endpoint system_getMetadata to retrieve the metadata of all modules registered to the blockchain client.

How to get the metadata
curl --location --request POST 'localhost:7887/rpc' \
--header 'Content-Type: application/json' \
--data-raw '{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "system_getMetadata",
    "params": {}
}'

10. Module dependencies registration updates

The following changes were introduced to complement the improvements made in the module structure, see Methods replace reducers, Addition of blockchain events, and Migration of store from v5 to v6.

In Lisk SDK v6, module dependencies can be registered in the following methods:

10.1. init()

init() is a method that can be implemented inside a module and/or a command to register the required dependencies for the module.

The init() method of the command can be called inside the init() function of the module to pass config options to a command, if desired.

Dependencies added inside the init() method
  • Configuration options for the module

  • The stores used by the module

  • The events emitted by the module

Stores and events can be registered in the constructor of the module alternatively.
public async init(args: ModuleInitArgs): Promise<void> {
    // registration of stores and events
    this.stores.register(CounterStore, new CounterStore(this.name));
    this.stores.register(MessageStore, new MessageStore(this.name));
    this.events.register(NewHelloEvent, new NewHelloEvent(this.name));
    // Get the module config defined in the config.json file
    const { moduleConfig } = args;
    // Overwrite the default module config with values from config.json, if set
    const config = utils.objects.mergeDeep({}, defaultConfig, moduleConfig) as ModuleConfigJSON;
    // Validate the provided config with the config schema
    validator.validate<ModuleConfigJSON>(configSchema, config);
    // Call the command init() method with config values as parameters
    this.commands[0].init(config).catch(err => {
        console.log("Error: ", err);
    });
}

10.2. addDependencies()

addDependencies() is a method that can be implemented inside a module and/or a command to register methods of other modules.

Dependencies added inside the addDependencies() method
  • The methods of other modules used by the module

By registering them in addDependencies(), methods keep their type information which improves the development experience greatly.
public addDependencies(tokenMethod: TokenMethod) {
    this._tokenMethod = tokenMethod;
}