In this tutorial, I will show you how to create a basic custom module and then walk you through all the different ways in which modules can interact with each other.

After the overview and initial setup, this tutorial is divided into 2 main parts:

Node level functionality : To show how modules can interact with each other locally on the same host/node.

: To show how modules can interact with each other locally on the same host/node. Network level functionality: To show how modules can interact with each other remotely over the network using the Lisk Network module.

Note that the Network module was introduced in Lisk Core 2.0 and was significantly improved in Lisk Core 2.1 — For the purpose of this guide, it is recommended that you use the latest version of the Lisk Core (v2.1.x).

Overview

The main purpose of a custom module is to extend the behaviour of a Lisk node. A custom module can communicate with other modules that are running on the same host/node. In addition to this, it’s possible for a custom module to communicate with other modules which are running on remote peers across the network (using the Network module for P2P communication).

Unlike a regular Node.js module which you need to require or import as a dependency into your code, a custom module in Lisk is stand-alone and is only loosely coupled to the Lisk framework and other modules; communication is done via message passing (asynchronously).

You can put essentially any kind of logic or behavior inside a custom module; for example, the custom module could gather and analyze data from other modules or it could act as an independent ledger built on top of a Blockchain or DAG. For example, most of the Lisk blockchain logic is contained within a module called chain.

One of the biggest advantages of the custom modules feature in Lisk core is that it allows you to build dapps without having to write any networking or P2P logic; this means that you don’t need to worry about things like:

Peer discovery

Peer state management

Connection management

RPCs and remote events

Decentralized message routing

Blacklisting and whitelisting of peers

Rate limiting and banning

This is made possible by the Network module API which is exposed to all modules on the same node. This internal Network module API is also what the Lisk chain module uses to interact with and build consensus with the rest of the network.

For a more detailed explanation of the custom modules architecture, see LIP5 by Nazar Hussain.

Creating a custom module

In order to attach a custom module to a Lisk node, you will need to run the node from source. To do this, you should clone the lisk-core repository from GitHub:

git clone git@github.com:LiskHQ/lisk-core.git

Then install dependencies:

cd lisk-core && npm install

You can then create your custom module in a new directory under the main lisk-core directory. For convenience, you may want to publish your custom module to npm; in that case, it should go under lisk-core/node_modules/ (or you can link to it using the npm link command).

Once you’ve created the custom module directory (e.g. lisk-core/node_modules/custom_module/ ), you will need to create the main index.js file directly at the top level. The main index file (entry point) for a custom module should look like this:

Most of your logic should start inside the load method.

The channel instance which is passed to the load method allows your module to invoke actions and listen to events on other modules.

Node level functionality

Custom modules can interact with other modules which are running on the same node/host without restriction. Because of this, you should not install modules which you don’t trust on important nodes or nodes that store sensitive data.

Exposing module actions

You can expose your custom module actions to other modules like this:

This will allow other modules to invoke your incrementNumber action with some parameters as input (in this case, params require a value property) and they will get a response when your handler returns. The handler function can be declared as async ; in that case you can use await inside it and return the result back to the caller asynchronously.

Invoking module actions

You can call actions on other modules from inside your own custom module (e.g. inside the load method):

The snippet above means that your module will call the getForgingStatusForAllDelegates action on the chain module which is running on your node and pass it an empty object {} as parameters. Note that channel invoke returns a Promise so in this case, you need to await the result.

You can see the list of actions exposed by the chain module here:

https://github.com/LiskHQ/lisk-sdk/blob/864b2b44ffc1033e21608c7487b40332af40f3c6/framework/src/modules/chain/index.js#L73-L137

Listening to module events

The format for events is similar to actions except that the namespace refers to the source module instead of the target module (I.e. sourceModuleName:eventName ). In the following case, the source module is chain and the event name is blocks:change :

Publishing module events

Your custom module can publish an event like this:

Other modules which are interested in our event can subscribe to it just like we did in the previous ‘listening to module events’ step with the ‘chain:blocks:change’ event.

Network level functionality

Thanks to the new Network module in SDK v2, you can use the channel instance to call actions on modules which are running on other peers across the network.

Your custom module can either invoke an action on a completely different module (for example the chain module) or it can invoke an action on the same module but a different instance of it (for example, your custom_module can call actions on other custom_module instances which are running on remote peers in the network).

Exposing remote module actions

You can publicly expose module actions to remote peers across the network like this:

Declaring an action as isPublic: true like above will allow remote peers to invoke this action directly on your module so you need to be careful about what kinds of actions are safe to expose publicly. By default (or with isPublic: false ), an action can only be invoked from another module which is running on the same node.

Invoking remote module actions

You can call a getTransactions action on the chain module of a remote peer like this:

Note that unlike an internal module action which can always succeed, a remote action will only succeed if:

At least one of your node’s peers have the necessary target module.

The remote module exposes the action with an isPublic property set to true as shown in the previous section.

If the remote peer does not have the target module or action, the invoke call will throw an error; so you should always add a try-catch block around a remote channel.invoke(...) call. It’s expected that remote actions will fail from time to time so you need to make sure that you are handling errors properly.

Note that when you invoke an action on a remote peer/module, your Lisk node’s peer selection (routing) algorithm will decide which peer(s) should handle the request. In this case, the default Lisk peer selection algorithm will simply choose a random peer from your node’s peer list. The default peer selection algorithm can be exchanged for a different algorithm (for example lisk-interchain).

Listening to remote module events

If a module from a different peer sends an event to your node, you can handle it like this:

All remote events from the network module will be passed to the subscribe handler as shown above; you need to check yourself what kind of event it is.

Hopefully this API will be improved in the future. Right now your custom module will receive all remote events from the Network module.

Emitting remote module events

You need to invoke an action on the Network module and tell it to emit the event remotely to the rest of the network. The event name in this case contains custom_module which should be the alias of the source module where the event originated:

Note that when you emit a remote event to the network, your Lisk node’s peer selection (routing) algorithm will decide which peer(s) should receive the event. In this case, the default Lisk peer selection algorithm will simply pick a random sample of 24 peers from your node’s peer list. The default peer selection algorithm can be exchanged for a different algorithm (for example lisk-interchain).

Registering your custom module on a Lisk node

After you’ve created your custom module, you need to register it on a Lisk core node by adding a couple of lines of code to lisk-core/src/index.ts .

Firstly, you need to import the custom module which you defined earlier:

Then, just before app.run() is called, register the module like this:

Overview of lisk-interchain

Note that lisk-interchain is an independent community project for the Lisk ecosystem and it is not affiliated with Lightcurve or Lisk Foundation.

Allowing multiple instances of a module to invoke actions on each other from different peers across the network is useful if you want to establish a decentralized consensus about some kind of data within your module. For example, your module could represent a separate blockchain which runs alongside the Lisk chain on a small subset of nodes.

Making modules communicate with each other in this way allows them to form independent ‘subnets’ which run on top of the main Lisk network.

Unfortunately, the default peer selection functions in Lisk core select peers according to a fully random algorithm. In order to allow modules to efficiently form subnets with each other (based on features and modules), a smarter routing algorithm is needed.

This is what the lisk-interchain plugin does; it uses a feature of the Network module to do 2 things: Firstly, it ensures that your node is more likely to connect to peers which have the same custom modules as itself (but it always leaves enough room for regular Lisk nodes and selection is still random within any given peer group). Secondly, all actions and events which affect a specific module will only be routed to peers which actually have that module; this will prevent custom actions and events from slowing down regular Lisk nodes which are not part of any subnet or are part of a different subnet.