Transport Tutorial Part 1: Track a packet on the blockchain

The goal of this part is to implement a simple application that tracks sensor measurements on the blockchain. That means, once the IoT application will be started, it will immediately send an LightAlarmTransaction to the network, whenever the sensor detects light.

You will learn here:

  • How to implement the LightAlarmTransaction

  • How to register the new transaction type with the node application

  • How to create the IoT script, and how to put it on the Raspberry Pi

  • How to use the client app to initialize the packet account and to track the alarm transactions in the network

You can also require the solutions directly in your app, just change

const LightAlarmTransaction = require('../transactions/light-alarm');

To

const LightAlarmTransaction = require('../transactions/solutions/light-alarm');

But let’s try not to cheat too much ;-)

Project Architecture

Three different kind of applications need to be developed, to create the decentralized supply chain system:

A node application

which accepts the application-specific transaction types. This application needs to be installed on different independent nodes and will setup and maintain the blockchain which is used to store the data about the packets, carrier and users.

A client application

which is displaying information from the blockchain to the user. It needs a frontend, which should be listing at least a list of packetIDs, the carrier, sender, recipient, and a status field (pending | ongoing | alarm | success | fail). It should also provide an easy way to create and send the different transaction types to the network.

An IoT application

which is stored on a microcontroller/raspberry pi. This application will track that the packet is not manipulated during the delivery. To do this, certain sensors will be connected to it, that track information like light, temperature and/or humidity inside of the packet. If something unexpected is detected by the IoT app, it will create a transaction object, sign it, and send it to the network.

The basic file structure looks as following (contents of lisk-sdk-examples/transport):

.
├── README.adoc
├── Workshop.adoc
├── client                                          (1)
│   ├── accounts.json
│   ├── app.js
│   ├── package.json
│   ├── scripts
│   └── views
├── iot                                             (2)
│   ├── README.md
│   ├── lisk_rpi_ldr_and_temperature_sensors_wiring.png
│   ├── light_alarm
│   │   ├── package.json
│   │   └── index.js
├── node                                            (3)
│   ├── index.js
│   └── package.json
└── transactions                                    (4)
    ├── finish-transport.js
    ├── light-alarm.js
    ├── register-packet.js
    └── start-transport.js
1 Contains the code for the client application
2 Contains the code for the IoT application
3 Contains the code for the node application
4 Contains the custom transactions, that are used by the node and client app.

1.1 Implement the LightAlarm Transaction

For the very simple version of the packet tracking, only one custom transaction type needs to be implemented: the LightAlarmTransaction. This transaction will be sent by the IoT device inside of the packet when it detects anomalies with its connected photoresistor (light detection).

The only thing you need to implement in this step yourself is the validateAsset function. For more details how to do this, check the explanation below. The code you see below represents the current state for which you’ll have to implement the validateAsset() function.

Contents of /transactions/light-alarm.js
const {
    BaseTransaction,
    TransactionError,
} = require('@liskhq/lisk-transactions');

/**
 * Send light alarm transaction when the packet has been opened (accepts timestamp).
 * Self-signed by packet.
 * The `LightAlarmTransaction` is extended from the `BaseTransaction` interface.
 */
class LightAlarmTransaction extends BaseTransaction {

    /* Static property that defines the transaction `type` (has to be unique in the network). */
    static get TYPE () {
        return 23;
    }

    /* The transaction `fee`. This needs to be paid by the sender when posting the transaction to the network.
       It is set to `0`, so the packet doesn't need any funds to send an alarm transaction. */
    static get FEE () {
        return '0';
    };

    /* Data from the packet account is cached from the databse. */
    async prepare(store) {
        await store.account.cache([
            {
                address: this.senderId,
            }
        ]);
    }

    /* Static checks for presence and correct datatype of `timestamp`, which holds the timestamp of when the alarm was triggered. */
    validateAsset() {
        const errors = [];
        /*
        Implement your own logic here.
        Static checks for presence of `timestamp` which holds the timestamp of when the alarm was triggered
        */

        return errors;
    }

    applyAsset(store) {
        /* Insert the logic for applyAsset() here */
    }

    undoAsset(store) {
        const errors = [];
        const packet = store.account.get(this.senderId);

        /* --- Revert packet status --- */
        packet.asset.status = null;
        packet.asset.alarms.light.pop();

        store.account.set(packet.address, packet);
        return errors;
    }

}

module.exports = LightAlarmTransaction;
Go to the Lisk Documentation, to get an overview about the required methods for custom transactions

1.1.1 Task: Implement validateAsset()

Implement your own logic for the validateAsset() function here at line 31. The code will validate the timestamp that has been sent by the LightAlarmTransaction. In case an error is found, push a new TransactionError into the errors array and return it at the end of the function.

All data, that is sent with the transaction is available through the this variable. So, to access the timestamp of the transaction, use this.timestamp.

The snippet below describes how to create an TransactionError object. Try to add a fitting TransactionError to the errors list of validateAsset(), in case the timestamp is not present, or if it has the wrong format.

The expected data type for the timestamp is number!
Example: How to create a TransactionError object:
new TransactionError(
	'Invalid "asset.hello" defined on transaction',
	this.id,
	'.asset.hello',
	this.asset.hello,
	'A string value no longer than 64 characters',
)
In case you need some inspiration how to implement the validateAsset() function, check out the other examples like hello_world inside of the lisk-sdk-examples repository, or check the tutorials in the Lisk documentation.
To verify your implementation of validateAsset(), compare it with the solution.

1.1.2 Task: Implement applyAsset()

The applyAsset function tells the blockchain what changes it should make and how to change a user’s account. Basically, it holds the core business logic of your custom transaction. The magic happens here! You can find a possible implementation of applyAsset for the LightAlarmTransaction below.

TASK

Copy the snippet below and replace the applyAsset function in light-alarm.js with it in order to complete the implementation of the lightAlarmTransaction.

/*Inside of `applyAsset`, we can make use of the cached data from the `prepare` function,
 * which is stored inside of the `store` parameter.*/
applyAsset(store) {
    const errors = [];

    /* With `store.account.get(ADDRESS)` we now get the account data of the packet account.
     * We specify `this.senderId` as address, because the light alarm is always signed and sent by the packet itself. */
    const packet = store.account.get(this.senderId);

    /**
     * Update the Packet account:
     * - set packet status to "alarm"
     * - add current timestamp to light alarms list
     */
    packet.asset.status = 'alarm';
    packet.asset.alarms = packet.asset.alarms ? packet.asset.alarms : {};
    packet.asset.alarms.light = packet.asset.alarms.light ? packet.asset.alarms.light : [];
    packet.asset.alarms.light.push(this.timestamp);

    /* When all changes have been made, they are applied to the database by executing `store.account.set(ADDRESS, DATA)`; */
    store.account.set(packet.address, packet);

    /* Unlike in `validateAsset`, the `store` parameter is present here.
     * That means, inside of `applyAsset` it is possible to make dynamic checks against the existing data in the database.
     *  As we do not need to this here, an empty `errors` array is returned at the end of the function. */
    return errors;
}

1.1.3 Register the transaction with the application

Now, that we have created the new custom transaction type LightAlarmTransaction, it needs to be registered with the node application. Without this step, the nodes won’t have the logic to validate a LightAlarmTransaction and the transaction will be discarded.

Check out the code at node/index.js which registers the LightAlarmTransaction to the blockchain application:
const { Application, genesisBlockDevnet, configDevnet } = require('lisk-sdk');
const LightAlarmTransaction = require('../transactions/light-alarm');           (1)

configDevnet.app.label = 'lisk-transport';

const app = new Application(genesisBlockDevnet, configDevnet);

app.registerTransaction(LightAlarmTransaction);                                 (2)

app
    .run()
    .then(() => app.logger.info('App started...'))
    .catch(error => {
        console.error('Faced error in application', error);
        process.exit(1);
    });
1 Requires the custom transaction type.
2 Registers the custom transaction type with the application.
After the registration of a new transaction type, the node needs to be restarted to apply the changes with node index.js | npx bunyan -o short. Make sure you are executing this command inside the node/ folder.

1.2 The IoT application

In this step we are going to create the script that will run on the Raspberry Pi to track if the packet has been manipulated.

1.2.1 Connect to the Raspberry Pi

For simplifying the network topology for the workshop we configured a DHCP server in the Raspberry Pi that will assign an IP address to your computer using a virtual ethernet through USB. The Raspberry Pi will have the hostname raspberrypi.local by default.

Connect a micro usb cable with the Raspberry and then connect the other end to a computer.

Make sure you connect the micro usb cable to the port that has a small label usb above it.

How to connect to your Pi

Next, to be able to log in using ssh from a terminal run the below ping command. This will start pinging the Raspberry Pi and you’ll eventually get responses back.

ping raspberrypi.local

Example output from pinging the Raspberry Pi:

Request timeout for icmp_seq 79
Request timeout for icmp_seq 80
Request timeout for icmp_seq 81
Request timeout for icmp_seq 82
Request timeout for icmp_seq 83
Request timeout for icmp_seq 84
64 bytes from raspberrypi.local: icmp_seq=85 ttl=64 time=0.952 ms
64 bytes from raspberrypi.local: icmp_seq=86 ttl=64 time=0.677 ms

When you start to get lines like the last one you can execute:

ssh pi@raspberrypi.local

If prompted with a warning just hit enter to accept the default (Yes).

Following, it will prompt for a password, enter the password in the label of the box of your Raspberry.

Your terminal should now be connected to the Raspberry Pi. In the next step, we will be working on the Raspberry Pi in order to prepare the device.

1.2.2 Create the tracking script

Execute the below commands for creating the tracking script:

mkdir light_alarm #Create a folder to hold the tracking script.
cd light_alarm
npm init --yes #Creates the `package.json` file.
npm i @liskhq/lisk-transactions @liskhq/lisk-api-client @liskhq/lisk-constants rpi-pins #Install dependencies.

Now, create a new file called light-alarm.js.

touch light-alarm.js

Next, copy the code from your local computer at transport/transactions/light-alarm.js (which we prepared in step 1.1) to the Raspberry Pi. First, let’s open the file with the nano editor.

nano light-alarm.js

Next, insert here the code of the LightAlarmTransaction. You can use CMD+V to paste the contents in the file. In order to save and exit nano, use:

CMD+O

ENTER

CMD+X

The second file you need to create is the actual tracking script. Create a new file index.js that will hold our tracking script.

touch index.js

Next, insert the code snippet below and save the index.js file. You can reuse the above commands with the nano editor.

const PIN = require("rpi-pins");
const GPIO = new PIN.GPIO();
// Rpi-pins uses the WiringPi pin numbering system (check https://pinout.xyz/pinout/pin16_gpio23).
GPIO.setPin(4, PIN.MODE.INPUT);
const LightAlarmTransaction = require('./light-alarm');
const { APIClient } = require('@liskhq/lisk-api-client');

// Replace `localhost` with the IP of the node you want to reach for API requests.
const api = new APIClient(['http://localhost:4000']);

// Check config file or visit localhost:4000/api/node/constants to verify your epoc time (OK when using /transport/node/index.js)
const dateToLiskEpochTimestamp = date => (
    Math.floor(new Date(date).getTime() / 1000) - Math.floor(new Date(Date.UTC(2016, 4, 24, 17, 0, 0, 0)).getTime() / 1000)
);

const packetCredentials = { /* Insert the credentials of the packet here in step 1.3 */ }

// Check the status of the sensor in a certain intervall (here: 1 second).
setInterval(() => {
	let state = GPIO.read(4);
    if(state === 0) {
        console.log('Package has been opened! Send lisk transaction!');

        // Uncomment the below code in step 1.3 of the workshop
        /*
        let tx = new LightAlarmTransaction({
            timestamp: dateToLiskEpochTimestamp(new Date())
        });

        tx.sign(packetCredentials.passphrase);

        api.transactions.broadcast(tx.toJSON()).then(res => {
            console.log("++++++++++++++++ API Response +++++++++++++++++");
            console.log(res.data);
            console.log("++++++++++++++++ Transaction Payload +++++++++++++++++");
            console.log(tx.stringify());
            console.log("++++++++++++++++ End Script +++++++++++++++++");
        }).catch(err => {
            console.log(JSON.stringify(err.errors, null, 2));
        });
        */
    } else {
        console.log('Alles gut');
    }
}, 1000);

1.2.3 Run the tracking script

To check if the script can read the sensor data, start the script by running:

node index.js

Now place the sensor in a dark place and then in a light place, and verify the correct logs are shown in the console.

If no light is detected, it should log:

Alles gut

and if light is detected, it will log:

Package has been opened! Send lisk transaction!

The code will also try to send the LightAlarmTransaction in case it detects light. This will fail, as we didn’t provide the passphrase of the packet in the script, which is needed to sign the LightAlarmTransaction.

You can cancel the script for now by stopping its execution with:

CMD+C

Next up, let’s use the client app in step 1.3 to initialize a new account for the packet.

1.3 The client application

In this step, we have to store the passphrase of the packet on the Raspberry Pi so it can sign and broadcast the LightAlarmTransaction. After that, we will start the client application to explore the sent transactions.

While your Raspberry Pi is still connected, open a local terminal window and navigate into the client app.

The complete implementation of the client is prepared for you before the workshop. In this part 1 of the workshop, we will only make use of the Initialize and Packet&Carrier pages.

1.3.1 Installation

Let’s start the client application with the following commands.

cd ../client
npm i
node app.js

Make sure your blockchain is running in order for the client to work! If not, start your blockchain by navigating to the node/ folder and running:

node index.js | npx bunyan -o short

1.3.2 Create New Package Credentials

Navigate to the Initialize page (web app running at http://localhost:3000) to create a new packet account. Every time that you refresh the page, new packet credentials are created and initialized on the network.

Initialization of the packet account

Copy the object with the credentials and paste it as packetCredentials in your tracking script on the Raspberry Pi. You have to paste it in the index.js file on the Raspberry Pi at the following line of code:

const packetCredentials = { /* Insert the credentials of the packet here in step 1.3 */ }

1.3.3 Update IP for Node API

Exchange localhost with the IP where your node application is running.

If you followed the tutorial, your node should run on your local machine. To get the IP, open a new terminal window on your machine and type ifconfig or a similar command, that displays your current IP address.

Simply copy it and replace localhost in the tracking script with it.

const api = new APIClient(['http://localhost:4000']);

Ok, we are all set to check all elements together.

1.3.4 Uncomment code that sends the light alarm transaction

1.3.5 Validating all components

To now track the light alarm with the client application, do the following:

  1. Make sure your blockchain node is running on your machine (node/ folder):

    node index.js | npx bunyan -o short
  2. Make sure the client from the client/ folder is running:

    node app.js
  3. Put the sensor of your raspberry in a dark place.

  4. Now, start the tracking script on your Raspberry Pi:

    node index.js
  5. Go to the Packet&Carrier page in the client which is running at localhost:3000 and refresh. Nothing should be shown on the page, yet.

  6. Now, shed some light on the sensor, and refresh the page again.

  7. If you refresh again, you should see a list of timestamps at which LightAlarmTransactions have been fired, sent by the Raspberry Pi.

If you see the timestamps are added to asset.alarms.light of the packet account, you have successfully completed part 1 of the workshop, congratz! \o/

packet account

You are now able to detect a packet manipulation and save the corresponding timestamp on the blockchain.