Guides
intermediate

Building an Energy-Efficient Smart Grid that Rewards Responsible Users with W3bstream, and the IoTeX Blockchain.

Learn how to create a DePIN project that enables the creation of a simple, smart energy grid. You'll be using cutting-edge technologies like W3bstream and IoTeX Blockchain to build a system that rewards users for their energy efficiency. By the end of this tutorial, you will have gained valuable insights into building an energy-efficient infrastructure that promotes responsible energy use.

Post Header Image

Updated:


SHARE

Edit on Github

Introduction

Welcome to this tutorial on creating a DePIN project using W3bstream and blockchain. We will build a simple smart energy grid that rewards energy-efficient users. With the increasing demand for energy worldwide, it has become increasingly important to find innovative ways to conserve and manage energy. A smart energy grid is one such solution that can help promote responsible energy use and reduce wastage.

In this tutorial, we will guide you through the process of building a smart energy grid from scratch. We will start by creating three smart contracts on the IoTeX blockchain that will authorize smart energy meters to send energy consumption data, keep track of device-owner bindings, and implement an ERC20 token used for the actual crypto rewards.

We will then show you how to create a W3bstream project composed of an Applet, in the form of a WASM module built in AssemblyScript, that validates and stores incoming IoT data. The module will process the data every 24 hours to evaluate users behavior and trigger rewards on the blockchain. Finally, we will build a smart energy meter device simulator using Node.js to create some data and send it to our W3bstream project.

By the end of this tutorial, you will have gained valuable insights into DePIN and the skills needed to build a simple project that rewards users to promote responsible behaviors. With these skills, you will be able to expand upon the system in new and exciting ways. So let's get started!

Setting up the development environment

Before you can begin creating the project, you'll need to make sure you have the following:

  • A blockchain wallet app that is compatible with the IoTeX or Ethereum network, such as Metamask or ioPay
  • Some test tokens on one of the blockchains supported by W3bstream (IoTeX, Ethereum, Polygon)
  • Node.js installed on your system
  • Git installed on your system

To start your W3bstream project, you can use the create-w3bstream-project package, and open your favorite code editor:

npx create-w3bstream-project simple-smart-grid
code simple-smart-grid

The package creates the skeleton of a simple w3bstream application, which will include the folliwng directories:

  1. The blockchain subfolder contains the smart contracts needed for the IoTeX or Ethereum blockchain, including the authorization contract, device-owner binding contract, and ERC20 token contract.
  2. The w3bstream subfolder contains the W3bstream applet source code. The applet is written in AssemblyScript and validates and stores incoming IoT data, and processes the data every 24 hours to trigger rewards on the blockchain.
  3. The simulator subfolder contains the W3bstream-compatible smart-meter device simulator written in Node.js. This simulator is used to create fake data and send it to the W3bstream project for testing purposes.

You can find the full project repository at https://github.com/simonerom/w3bstream-power-meter.

Creating the smart contracts

The blockchain folder will contain a series of smart contracts to handle the device registration and binding process needed in our application. When creating an application with the package, you'll also be able to choose what token you'd like to use in your application as user incentive. In this application, we'll leverage the sample ERC20 contract.

Device Identity and Binding

Each DePIN project must implement some sort of device identity and ownership binding mechanisms. The device identity consists of managing a list of devices that are allowed to contribute their data to the project via a unique device ID. Like in any IoT ecosystem, device binding is a required mechanism that allows identifying what account owns a certain device and is therefore allowed to communicate and manage it. In addition, in a DePIN application, device binding is also required to identify the address of the device owner, who is usually the recipient of rewards, NFT tokens, and other on-chain assets minted as a result of the data generated by their devices.

To learn more about device management in a DePIN application built with W3bstream, check out this tutorial.

Token economy

We will perform all the rewards calculations inside the W3bstream logic, which will then query the device registry and binding contracts on-chain to mint rewards to the respective device owners by directly triggering the ERC20 token mentioned above.

Before we can run the script to deploy our contracts, make sure you created a blokchain wallet account and funded it with some IOTX tokens. We can use the IoTeX testnet for our deployment. A quick tutorial on how to create and fund an IoTeX testnet account can be found here.

We won't be making any modifications to the smart contracts, so we'll go ahead and deploy them straight as they come out of the box. Before doing that though, we'll need to add the private key of the account we'd like to use to deploy contracts in an .env file.

echo PRIVATE_KEY="<YOUR PRIVATE KEY>" > .env 

Make sure you have enough test tokens in your account, then deploy the contracts by running:

npm run deploy:testnet

Output:

Compiled 1 Solidity file successfully
Deploying contracts with the account: 0x00e27ACAF1d3D58861DF710719fc97C43fC976f6
Account balance: 113.902296  IOTX

Deploying DevicesRegistry contract
DevicesRegistry Contract
address: 0x1b215fB19733C49bf529b2E5C225d169fFb427fc

Deploying DeviceBinding contract
DeviceBinding Contract
address: 0xD4C853aEb247fa63348D82E16D9ac51D4dbDA0f9

Deploying ECOToken contract
EcoToken Contract
address: 0x51D65c3E614Be96ebb637b960aF147d75d8711a4

Deployment completed.
Contracts deployed at height: 19918062


Configuration saved to .env file

Once the deployment of the contracts is completed, we can find all relevant address inside the .env file:

cat .env

Output:

PRIVATE_KEY="YOUR_PRIVATE_KEY"
REGISTRY_CONTRACT=0x1b215fB19733C49bf529b2E5C225d169fFb427fc
BINDING_CONTRACT=0xD4C853aEb247fa63348D82E16D9ac51D4dbDA0f9
DEPLOYED_HEIGHT=19918062
DEPLOYER_ADDRESS=0x00e27ACAF1d3D58861DF710719fc97C43fC976f6

These contract addresses will come in handy once we configure our W3bstream logic!

Building the W3bstream project

The device message protocol

The core part of any W3bstream project is the Applet, that defines the logic of our project to be executed by W3bstream. This logic will be in charge of processing the data incoming from smart energy meter devices.

We will create the applet using the W3bstream Applet Kit package for AssemblyScript, however, Before we can build the applet, we should clarify what data we expect our device to send us.

We decided to have only two types of messages that our device can send: a data message and a rewards request message.

The data message

The data message contains energy consumption information and is sent to w3bstream in short intervals (e.g. 1 minute):

{
    "data": {
        "sensor_reading": 0.421,
        "timestamp": 1682091108
    },
    "public_key": "0xabcd...321",
    "signature": "0432bef...c00"
}

where:

  • sensor_reading: is the average power consumed by the user in the last interval (let's assume it's 1 minute)
  • timestamp: is the time when the value has been computed, in UNIX timestamp format
  • public_key: is the public key of the device, whose correspoinding private key is used by the device to sign the data message above. It will also serve as the unique identity device_id of the device.
  • signature: is the digital signature of the data object performed by the device using elliptic curve cryptography with curve secp256r1: secp256r1.sign(sha256.hash({"sensor_reading": 0.421,"timestamp": 1682091108}))

The rewards request message

The rewards request message looks contains no energy data: it's only a request to periodically ping our W3bstream logic to make it process rewards. Since rewards are calculated based on data accumulated every 24h, this message could be sent every 24h or more:

{
    "data": {
        "device_id": "0xefgh...675",
        "timestamp": 1682091108
    },
    "public_key": "0xabcd...321",
    "signature": "0432bef...c00"
}

where:

  • device_id: is the the public key of the device for which the rewards processing is requested
  • timestamp: is the time when the request has been sent, in UNIX timestamp format
  • public_key: is the public key of the requester, we assume to be the device itself, that is also supposed to sign the request
  • signature: is the digital signature of the request (i.e. the data object) using elliptic curve cryptography with curve secp256r1

Creating the applet

The hadlers.ts file in the w3bstream directory already contains two functions to handle the device registration and binding in our application. We'll now add the rest of the handlers needed to manage data storage, and rewards processing.

let's start with the receive_data handler which is in charge of processing data messages. This hanlder will validate and store IoT data alongside the device id that generated it.

// This handler will be executed each time a new data message 
// is sent to our W3bstream project
export function handle_data(rid: i32): i32 {
  log_start("New data message received");
  // Get the device data message from the W3bstream host
  let message_string = GetDataByRID(rid);
  // Parse the data message into a JSON object
  let message_json = JSON.parse(message_string) as JSON.Obj;
  // validate fields
  assert(validateData(message_json), "Message fields are not valid");
  // Verify device signature
  assert(validateDeviceIdentity(message_json),"Device identity validation failed");
  // make sure the device has an owner assigned
  let owner = get_device_owner(message_json);
  assert(owner != CONST.ZERO_ADDRESS,"No owner assigned for device");
  // Store the IoT data along with the device id 
  storeData(message_json);

  // For simplicity, let's evaluate rewards here (however, a dedicated
  // message should be sent periodically!)
  return handle_process_rewards(rid);
}

The second handler, that we will call handle_process_rewards, which will reward the most recentdata message in the database, based on certain energy consumption behavior:

// Simply rewards the most recent data message in the DB  
// but more complex logic could be implemented here
export function handle_process_rewards(rid: i32): i32 {
  log_start("Processing rewards");

  // Get the device data message from the W3bstream host
  let message_string = GetDataByRID(rid);
  // Parse the data message into a JSON object
  let message_json = JSON.parse(message_string) as JSON.Obj;

  // Get the public key from the message
  let public_key = getStringField(message_json, "public_key");
  // Get the latest IoT data point sent by the device
  let sql = "SELECT public_key,sensor_reading FROM data_table WHERE public_key = '"+public_key+"' ORDER BY id DESC LIMIT 1";
  let result = QuerySQL(sql);
  let result_json = JSON.parse(result) as JSON.Obj;
  if (result_json == null) {
    log("No data found for device ")
    return 1;
  }
  // Get the power consumption
  let sensor_reading = parseFloat(getStringField(result_json, "sensor_reading"));  
  if (sensor_reading < 4.0) {
    // Rewards the device owner
    let owner = get_device_owner(message_json);
    log("Rewarding " + owner + " with 3 ECO Tokens...");
    let tx_hash = mintRewards(CONST.TOKEN_CONTRACT, owner, CONST.FOUR_TOKENS_HEX);
    if (tx_hash == "") {
        log("Sending token rewards failed.")
        return 1;
    }
    log("Reward transaction hash: " + tx_hash);
  } else {
    log("Power consumption too high, no rewards sent.");
  }

  return 0;
}

Next we have two utility functions, used to read the Blockchain contracts that we deployed in the first part of this tutorial. We want to be able to query the DeviceRegistry contract for a specific device public key, to know if it's an authorized device. Additionally, we want to query the DeviceBinding contract to know if and whom a certain device is bound to:

// Verify that the device public key is authorized
function auth_device(message_json: JSON.Obj): bool {
    log("Authenticating device public key from DB...")
    // Get the public key from the message
    let public_key = getStringField(message_json, "public_key");
    // Get the device id from the message
    let device_id = publicKeyToDeviceId(public_key);
    let sql = "SELECT is_active FROM device_registry WHERE device_id = '" + device_id + "'";
    let result = QuerySQL(sql);
    assert(result != "", "Device is not registered");

    let result_json = JSON.parse(result) as JSON.Obj;
    let is_active = getStringField(result_json, "is_active");
    if (is_active == "true") log("Device is authorized"); 
    else if (is_active == "false") log("Device is banned");

    return (is_active == "true");
}

// Get the owner of a specific device id from te w3bstream DB
function get_device_owner(message_json: JSON.Obj): string {
    // Get the device id from the message
    let public_key = getStringField(message_json, "public_key");
    let device_id = publicKeyToDeviceId(public_key);
    log("Getting owner of device "+ device_id);
    let sql = "SELECT owner_address FROM device_bindings WHERE device_id = '" + device_id + "'";
    let result = QuerySQL(sql);
    if (result == "") {
        log("Device is not bound to any owner");
        return CONST.ZERO_ADDRESS;
    }
    let result_json = JSON.parse(result) as JSON.Obj;
    let owner = getStringField(result_json, "owner_address");
    log("Device owner is: " + owner)
    return (owner);
}

Next, let's look at the validateDeviceIdentity function. This function is in charge of two important steps: 1. Verifying the signature of the IoT data message, to make sure the data has not been tampered with while retrieving the public key of the device that signed the message, and 2. querying the blockchain DeviceRegistry contract to make sure that the public key does actually correspond to an authorized device:

// Verify that the message signature is correct and the device public key is authorized
function validateDeviceIdentity(message_json: JSON.Obj): bool {
    log("Validating device identity")
    // Get the public key from the message
    let public_key = getStringField(message_json, "public_key");
    // Verify that the device public key is authorized in the contract
    let authorized = auth_device(message_json)
    if (!authorized) {
        log("Device authentication failed");
        return false;
    }
    // Get the signature from the message
    let signature = getStringField(message_json, "signature");
    // Get the data object
    let data: JSON.Obj | null = message_json.getObj("data");
    if (data == null) return 0;
    // Perform signature verification
    let signature_ok = verifySig(public_key, signature, data.toString());
    if (!signature_ok) {
        log("Data signature is not valid");
        return false;
    }
    log("Data signature is valid")
    return true;
}

Next, we need to important utility functions: storeData, which is in charge of storing the IoT data along with the device public key, and validateData, which validates the data message that gets sent to our W3bstream node.

function storeData(message_json: JSON.Obj): i32 { 
    log("Storing data message in DB")
    // Get the device public key
    let public_key = getStringField(message_json, "public_key");
    // Get the device data
    let data_json = message_json.get("data") as JSON.Obj;
    // Get the sensor reading
    let sensor_reading = getFloatField(data_json, "sensor_reading");
    // Get the timestamp
    let timestamp = getIntField(data_json, "timestamp");
    // Store the data in the W3bstream SQL Database
    const query = `INSERT INTO "data_table" (public_key,sensor_reading,timestamp) VALUES (?,?,?);`;
    const value = ExecSQL(
        query, 
        [new String(public_key), new String(sensor_reading), new String(timestamp)]);
    log("Query returned: " + value.toString());

    return value;
}


function validateData(message_json: JSON.Obj): boolean { 
    log("Validating data message:\n" + message_json.toString())
    let valid: bool = true;
    // Validate the message fields
    if (!(valid = valid && message_json.has("public_key"))) log("public_key field is missing");
    if (!(valid = valid && message_json.has("signature"))) log("device_signature field is missing");
    if (!(valid = valid && message_json.has("data"))) log("data field is missing");
    
    let data_json = message_json.get("data") as JSON.Obj;
    if (!(valid = valid && data_json.has("sensor_reading"))) log("sensor_reading field is missing");
    if (!(valid = valid && data_json.has("timestamp"))) log("timestamp field is missing");

    return valid as boolean;
} 

The AssemblyScript applet is now complete. Next we'll use the simulator to generate some data messages that will be used to test our application.

Building the smart energy meter device simulator

Work in progress

how to build the device simulator using Node.js. Discuss how the simulator generates data and sends it to the W3bstream project.

Testing the system

Work in progress

Test the entire system to ensure that everything is working as intended. This should include running the device simulator, sending data to the W3bstream project, and verifying that rewards are being triggered on the blockchain.

Conclusion

Summarize what the reader has learned and provide some ideas for further exploration. Encourage the reader to experiment with the system and come up with new and innovative ways to use smart energy grids.


Docs

IoTeX Docs


IoTeX Developerslogo

[email protected]