Guides
intermediate

Decentralized IoT Data access authorization

Learn how to create a decentralized marketplace where the data stream from smart devices may be traded between device owners and subscribers.

06/15/2022

Updated: 09/02/2022


SHARE

Edit on Github

In this guide, we share the implementation details for a smart contract that realizes an authorization mechanism to a fully decentralized IoT data marketplace.

Overview

This contract allows a user to register their devices that can provide some sort of real-time IoT data, and generate revenue as other users subscribe to the device's data stream. The logic of this marketplace can be divided into three steps:

  1. First, the contract owner needs to set up the device registration and subscription fees, and pre-register (or "whitelist") each authorized device into the contract.

  2. Next, each device's owner will be able to register, and also update their device data stream configuration.

  3. Finally, each of these smart devices will produce its unique data stream which can be purchased by third party users for a certain amount of time.

This has the potential to generate revenue for both the device's owner (who can claim the balances generated by their devices) and the contract owner (who accumulates the registration and subscription fees).

Coding the contract

The code for this contract can be found in this repository.

Start by creating a new file in your chosen project directory called Marketplace.sol.

Contract, imports, and inheritance

Let's start by creating the contract and setting up its inheritance and global scope.

Copy and paste the following code in Marketplace.sol.

pragma solidity <6.0 >=0.4.24;
 
import "./Pausable.sol";
import "./SafeMath.sol";
 
contract Marketplace is Pausable {
  using SafeMath for uint256;
 
  /// EVENTS
  event Registered(bytes32 indexed deviceID, address indexed owner); 
  event Subscribed(bytes32 indexed deviceID, address indexed subscriber, uint256 startHeight, uint256 duration, uint256 income);
  event Claimed(bytes32 indexed deviceID, address indexed claimer, uint256 amount);
  event Updated(bytes32 indexed deviceID, address indexed owner); 
  
  /// STORAGE MAPPINGS
  bytes32[] public deviceIDs;
  mapping (bytes32 => Device) public devices;
  mapping (bytes32 => Order) public orders;
  mapping (bytes32 => bool) public allowedIDHashes; 
 
  /// GLOBAL VARIABLES
  uint256 public registrationFee;
  uint256 public subscriptionFee;
  uint256 public registrationFeeTotal;
  uint256 public subscriptionFeeTotal;
 
  uint256 public constant maxDuration = 86400 / 5 * 90; // 90 days
 
  /// TYPES
  struct Device {
      // Intrinsic properties
      address owner;          // owner's address
      bool    hasOrder;       // order placed
      uint32  freq;           // how many seconds per data point
      uint256 pricePerBlock;  // in terms of IOTX, in RAUL/Wei (which 1e-18)
      uint256 settledBalance; // balance ready to claim
      uint256 pendingBalance; // balance in pending
      string  spec;           // link to the spec
      bytes   rsaPubkeyN;     // RSA public key N
      bytes   rsaPubkeyE;     // RSA public key E
  }
 
  struct Order {
      // Order info
      uint256 startHeight;  // the height starting from which this device's data stream is bought
      uint256 duration;     // how many blocks this order lasts
      string storageEPoint; // storage endpoint buyer provides
      string storageToken;  // access token to the storage endpoint, encrypted by devicePublicKey
  }
 
  /// CONSTRUCTOR
  constructor () public {}
  
  }

Let's now look at the logic in more detail.

For this contract we only import SafeMath and Pausable to get some security-related benefits.

Pausable is also inheriting from Ownable, which gives us the onlyOwer modifier. The SafeMath library will be used when dealing with unsigned integers. If you'd like to dig deeper into the Pausable and Ownable anchestor contracts you can check out the Openzeppelin documentation.

Events

Lines 10 to 13 are describing the events that will be emitted appropriately by some of the functions we'll be adding to the contract in just a few minutes. The events will be emitted when a new device is Registered by a device owner, when a registered device is Updated, when a user has Subscribed to a device's data stream, and when a device's owner has Claimed their funds.

Mappings

Lines 16 to 19 handle storage. There is an array of bytes32 elements called deviceIDs, while the other mappings handle the storage of any authorized device id, registered devices, and data orders.

Global Variables

The global variables help to keep track of the registration and subscription fees set only by the contract owner. These variables also track the total fees, which comes in handy when the contract owner decides to claim their funds. Also note that we established the maximum length allowed for a subscription to 90 days, set in the constant maxDuration. Since in this contract we measure duration in IoTeX blocks, given a 5-second block time we simply calculate how many blocks are contained in 90 days to find the maxDuration in blocks.

Structs

There are two custom data types representing a Device and a data Order. Note that upon registration of a device, the user will be able to input its data frequency freq, its price per block pricePerBlock in terms of IOTX tokens, and more importantly, the two components of the device RSA public key: rsaPubkeyN and rsaPubkeyE. We are assuming that the device is capable of secure RSA keys generation, digital signature and decryption. An example of such device is Pebble Tracker.

When a new subscriber decides to create an Order for a device, they will use this public key to encrypt the storage endpoint where the data is supposed to be sent and any token required to get access to the endpoint. The device, will then be able to decrypt these values with its private key, and will know where to send the data (storageEPoint) as well as the required "password" (storageToken).

Constructor

The constructor in this case is empty and may be omitted.

 Marketplace Owner's Functions

Let's now add the functions that allow the contract owner to set the device registration fee, set the device subscription fee, and pre-register the allowed devices (usually, during or right after manufacturing).

Add the following code to the contract:

contract Marketplace is Pausable {
 
 ...
 
    function setRegistrationFee(uint256 fee) public onlyOwner {
    registrationFee = fee;
    }
 
    function setSubscriptionFee(uint256 fee) public onlyOwner {
     subscriptionFee = fee;
    }
    
    function preRegisterDevice(bytes32 _deviceIdHash)
    public onlyOwner whenNotPaused returns (bool)
    {
      require(!allowedIDHashes[_deviceIdHash], "already whitelisted");
 
      allowedIDHashes[_deviceIdHash] = true;
      return true;
    }   
}

setRegistrationFee() and setSubscriptionFee allow to optionally set a fee to be paid to the contract owner by device owners each time they register a new device, and by data consumers each time they subscribe to a device's data stream. Of course, only the contract owner can set these fees.

preRegisterDevice() is also exclusive to the contract owner. This is how the contract owner can "authorize" devices, i.e. set which device is actually allowed to be used in the data marketplace (for example, because the contract owner has control over, or trusts, the firmware of these devices).

To prevent an observer that would look at the contract data from "stealing" the ids of authorized devices before they are assigned to the device's actual owner, this function expectes the hash of the device id to be provided instead of the actual device id (whatever has been chosen as the unique id for these devices).

In allowedIDHashes we paired each device id hash with a boolean as an efficient way to check if an hash has already been pre-registered or not. The execution is halted if the id hash is found as a key inside allowedIDHashes and it corresponds to a "true" value, meaning that it has already been registered. Otherwise, the mapping is updated with a true value for that hash to mark it as a valid device.

Registering and updating a device

Registering a newdevice

Let's now cover how a device owner is supposed to register a new device. Device's owners act as "data sellers" in this marketplace, and the device registration is supposed to happen only once per device, as it is basically intended to assign an owner account to a specific device. In the same operation, the device and data stream configuration have to be specified as well.

contract Marketplace is Pausable {
 
 ...
 
   function registerDevice(
    bytes32 _deviceId,
    uint32 _freq,
    uint256 _price,
    string memory _spec,
    bytes memory _rsaPubkeyN,
    bytes memory _rsaPubkeyE
    )
    public whenNotPaused payable returns (bool)
    {
      require(allowedIDHashes[keccak256(abi.encodePacked(_deviceId))], "id not allowed");
      require(devices[_deviceId].rsaPubkeyN.length == 0, "already registered");
      require(devices[_deviceId].rsaPubkeyE.length == 0, "already registered");
      require(_rsaPubkeyN.length != 0, "RSA public key N required");
      require(_rsaPubkeyE.length != 0, "RSA public key E required");
      require(_freq >= 0, "frequence needs to be positive");
      require(bytes(_spec).length > 0, "spec url is required");
      require(msg.value >= registrationFee, "not enough fee");
 
      registrationFeeTotal += msg.value;
      allowedIDHashes[keccak256(abi.encodePacked(_deviceId))] = false;
      devices[_deviceId] = Device(msg.sender, false, _freq, _price, 0, 0, _spec, _rsaPubkeyN, _rsaPubkeyE);
      deviceIDs.push(_deviceId);
      emit Registered(_deviceId, msg.sender);
      return true;
    }
}

This function starts with a series of require statements (lines 15 to 22). The function expects the actual device id to be passed by the device owner and it requires that the hash of the device id is actually inluded in the list of pre-authorized devices (line 15).

Next, the function requires that the device has not yet been registered, by simply checking that there is no valid device in the devices mapping corresponding to that id (it's enough to just check for a field like rsaPubkeyN: if there was an device already registered with the same id that field would never be "0", as specified by the next two requirements).

Line 20 requires the caller to set a positive data frequency for their device. _spec is supposed to be a url pointing to some technical specifications for that device and line 21 requires it not to be null. Finally we expect the user to pay the registration fee as defined by the contract owner.

After all the requirements are met, the function will finally register the device in the contract: line 24 updates the registration fee pool and Line 25 sets the allowedIDHashes mapping for the device ID to false, to prevent it for being registered again (as per line 15). Line 26 is finally creating the device object, that is then added to the devices mapping and can be later retrieved using its corresponding ID.

Line 27 then "pushes" the device ID to the deviceIDs array (we assume that the chosen device id is no longer than 32 bytes, e.g. an IMEI number). The deviceIDs array's length will be used to quickly tell how many devices have been registered.

Back to the code. Line 28 emits the Registered event confirming that the the caller of this function has indeed successfully registered the device with the given ID.

Update a device

The contract gives also a device owner the possibility to update their devices. Let's look at how this is accomplished:

contract Marketplace is Pausable {
 
 ...
 
   function updateDevice(
      bytes32 _deviceId,
      uint32 _freq,
      uint256 _price,
      string memory _spec,
      bytes memory _rsaPubkeyN,
      bytes memory _rsaPubkeyE
      )
      public whenNotPaused returns (bool)
      {
        require(devices[_deviceId].rsaPubkeyN.length != 0, "not yet registered");
        require(devices[_deviceId].rsaPubkeyE.length != 0, "not yet registered");
        require(devices[_deviceId].owner == msg.sender, "not owner");
        require(_rsaPubkeyN.length != 0, "RSA public key N required");
        require(_rsaPubkeyE.length != 0, "RSA public key E required");
        require(_freq != 0, "frequence cannot be zero");
        require(bytes(_spec).length > 0, "spec url is required");
 
        if (devices[_deviceId].hasOrder) {
          require(orders[_deviceId].startHeight + orders[_deviceId].duration < block.number, "device in active subscription");
        }
 
        devices[_deviceId].freq = _freq;
        devices[_deviceId].pricePerBlock = _price;
        devices[_deviceId].spec = _spec;
        devices[_deviceId].rsaPubkeyN = _rsaPubkeyN;
        devices[_deviceId].rsaPubkeyE = _rsaPubkeyE;
        emit Updated(_deviceId, msg.sender);
        return true;
      }
 } 

Based on the previous function, the require statements should be self-explanatory. It's worth noticing, however, how line 17 checks whether the function caller is indeed the device's owner.

Let's now focus on the if() statement on line 23. This line checks to see if the device used in this function has its hasOrder property set to true. This checks whether there has ever been an order associated with it (this property is set in the next function that handles a user's subscription to a device's data stream). If an order has been associated with the device in the past, then we check that it is not stil ongoing (line 14): if an order is still ongoing we don't allow the device owner to change the properties of the device, as that would affect the current order. If this requirement is met, the device mapping can be updated (lines 27 to 31) and the Updated event is emitted (line 32).

Subscribing to a device data stream

It's now time to look at how to create a function that will allow users to subscribe to a device's data stream, thus generating revenue for the device's owner (and optionally the contract owner).

The subscribe() function can be broken into smaller pieces. Let's start from the function signature:

contract Marketplace is Pausable {
 
 ...
 
    function subscribe(
    bytes32 _deviceId,
    uint256 _duration,
    string memory _storageEPoint,
    string memory _storageToken
    ) public whenNotPaused payable returns (bool){}
    
}

You can see that the user needs to specify the device Id and the duration of the subscription (in IoTeX blocks), as well as a storage endpoint (_storageEPoint), and a storage access token (_storageToken). The function is basically asking the user for the device they want to subscribe to, for how long, and where to send the data. Both _storageEPoint and _storageToken are supposed to be encrypted using the device's RSA public key, and can therefore only be decrypted using the device's private key. That's how a device will know where to send the data while keeping it private in the smart contract.

The next bit of code will handle the require statements:

   require(devices[_deviceId].rsaPubkeyN.length != 0, "no such device");
   require(devices[_deviceId].rsaPubkeyE.length != 0, "no such device");
   require(bytes(_storageEPoint).length > 0, "storage endpoint required");
   require(_duration > 0 && _duration <= maxDuration, "inappropriate duration");
   require(msg.value >= subscriptionFee + _duration.mul(devices[_deviceId].pricePerBlock), "not enough fee");

These statements check for four things:

  1. That the device the user wants to subscribe to exists (lines 2 and 3)
  2. That the storage endpoint has been provided (line 4)
  3. That the duration of the subscription does not exceed the maximum duration established in the global scope (line 5)
  4. That the value sent with the transaction can cover the subscription fee and the cost for the data set by the device's owner.

Note that line 4 is only checking for a storage endpoint to be present, and it cannot check for its validity, which is left up to the user calling this function. Being there no way for the contract to check the validity of the encrypted endpoint, a possible strategy here would be for the device to "cancel" an order if an attempt to send the data to an endpoint failed.

The next thing to do is to create the subscription order:

function subscribe(
    bytes32 _deviceId,
    uint256 _duration,
    string memory _storageEPoint,
    string memory _storageToken
    ) public whenNotPaused payable returns (bool){
    
    ... 
    
    if (devices[_deviceId].hasOrder) {
    
      require(orders[_deviceId].startHeight + orders[_deviceId].duration < block.number, "device in active subscription");
      orders[_deviceId].startHeight = block.number;
      orders[_deviceId].duration = _duration;
      orders[_deviceId].storageEPoint = _storageEPoint;
      orders[_deviceId].storageToken = _storageToken;
      
    } else {
      
      devices[_deviceId].hasOrder = true;
      orders[_deviceId] = Order(block.number, _duration, _storageEPoint, _storageToken);
    }
}

The first thing to establish is whether the device the user wants to subscribe to has already had an order in the past. Given there is no active order, in this case it's enough to just update the existing order with the new order data. If the device hasn't had any orders yet, then we must set its hasOrder property to true, create a new order, and assign it to the orders mapping for the corresponding _deviceId. For simplicity, when a new subrìscription is created it will use the current chain height as the starting block for the data stream (line 13).

Now that a new subscription has been created, it's time to update the total fees and balances, of both the contract's owner and the device owner appropriately.

function subscribe(
    bytes32 _deviceId,
    uint256 _duration,
    string memory _storageEPoint,
    string memory _storageToken
    ) public whenNotPaused payable returns (bool){
 
  ...
 
   subscriptionFeeTotal += subscriptionFee;
    if (devices[_deviceId].pendingBalance > 0) {
      devices[_deviceId].settledBalance = devices[_deviceId].settledBalance.add(devices[_deviceId].pendingBalance);
    }
    devices[_deviceId].pendingBalance = msg.value.sub(subscriptionFee);
}

The subscription fee is added to the subscription pool (line 10). Now, if the device already has a pending balance from a previous subscription, it is "archived" into the settledBalance. Otherwise, the pending balance is set to the amount paid for this new subscription (i.e. the value of the transaction minus the contract subscription fee).

Once all the balances are updated, the respective event has to be emitted:

 function subscribe(
    bytes32 _deviceId,
    uint256 _duration,
    string memory _storageEPoint,
    string memory _storageToken
    ) public whenNotPaused payable returns (bool){
 
  ...
  
    emit Subscribed(_deviceId, msg.sender, block.number, _duration, devices[_deviceId].pendingBalance);
    
    return true;
}

This function is now completed.

Claiming device data fees

The next function will allow device owners to claim the balance generated by their devices.

The claim() function only needs one parameter, which is the device's unique identification number. Let's go ahead and create the function header with this one parameter, and add some of the requirements needed to move forward:

contract Marketplace is Pausable {
 
 ...
  
  function claim(bytes32 _deviceId) public whenNotPaused returns (bool) {
  
    require(devices[_deviceId].rsaPubkeyN.length != 0, "no such device");
    require(devices[_deviceId].rsaPubkeyE.length != 0, "no such device");
    require(devices[_deviceId].owner == msg.sender, "not owner");
    require(devices[_deviceId].hasOrder, "device not yet subscribed");
    require(orders[_deviceId].startHeight + orders[_deviceId].duration < block.number, "device in active subscription");
  
  }
}

First, confirm that the device exists (lines 7 and 8). Then make sure that the person calling this function is actually the owner of the device (line 9). Now, for a device to have a claimable balance, there must have been at least one completed order associated with it (lines 10, 11). Any past order should have also been completed (line 12).

So far so good - let's move to the next bit of logic.

At this point pendingBalance stores the fee relative to the very last order, while settledBalance stores all the fees accumulated by any previous order. Therefore, we only have to add any pending balance to the settledBalance and make sure to reset both balances to "0" before actually performing the transfer (again, to prevent reentrancy attacks):

The code below reflects this logic:

function claim(bytes32 _deviceId) public whenNotPaused returns (bool) {
 
 ...
 
   if (devices[_deviceId].pendingBalance > 0) {
      devices[_deviceId].settledBalance = devices[_deviceId].settledBalance.add(devices[_deviceId].pendingBalance);
      devices[_deviceId].pendingBalance = 0;
    }
    uint256 balance = devices[_deviceId].settledBalance;
    require(balance > 0, "no balance");
 
    devices[_deviceId].settledBalance = 0;
    msg.sender.transfer(balance);
 ...
}

Finally the Claimed event is emitted and the function returns true after successful completion:

function claim(bytes32 _deviceId) public whenNotPaused returns (bool) {
 
 ...
 
    emit Claimed(_deviceId, msg.sender, balance);
    return true;
}

Claiming marketplace fees

Let's look at how the contract owner can collect marketplace fees:

function collectFees() onlyOwner public returns (bool) {
    uint256 total = registrationFeeTotal + subscriptionFeeTotal;
    if (total > 0) {
      registrationFeeTotal = 0;
      subscriptionFeeTotal = 0;
      msg.sender.transfer(total);
    }
    return true;
}

We start by creating a new unsigned integer variable called total, representing the sum of the pooled registration fees and subscription fees. There is a chance that this total may be zero, so it's better to create a conditional statement (line 3) to check for that. As usual, we change the value of both registrationFeeTotal and subscriptionFeeTotal back to zero before inititiating any token transfer.

View Functions

The logic governing the marketplace is done, but we're not 100% done quite yet: there are a few more functions that would be useful to have. One function should allow to retrieve all registered devices. Just in case we've have to deal with a large number of devices we create a pagination mechanism that allows you to query devices in batches:

function getDeviceIDs(uint256 _offset, uint8 limit)
    public view returns (uint256 count, bytes32[] memory ids) {
      require(_offset < deviceIDs.length && limit != 0);
 
      ids = new bytes32[](limit);
      for (uint256 i = 0; i < limit; i++) {
          if (_offset + i >= deviceIDs.length) {
              break;
          }
          ids[count] = deviceIDs[_offset + i];
          count++;
      }
  }

Let's add another useful function that retrieves information on a specific device, given its unique identifier:

function getDeviceInfoByID(
    bytes32 _deviceId
  ) public view returns (address, uint32, uint256, uint256, uint256, string memory, bytes memory, bytes memory) {
    require(devices[_deviceId].rsaPubkeyN.length != 0, "no such device");
    require(devices[_deviceId].rsaPubkeyE.length != 0, "no such device");
 
    Device memory d = devices[_deviceId];
    return (d.owner, d.freq, d.pricePerBlock, d.settledBalance, d.pendingBalance, d.spec, d.rsaPubkeyN, d.rsaPubkeyE);
  }

and finally, one that provides order details fo a specific device, given the device unique identifier:

function getDeviceOrderByID(
      bytes32 _deviceId
    ) public view returns (uint256, uint256, string memory, string memory) {
      require(devices[_deviceId].rsaPubkeyN.length != 0, "no such device");
      require(devices[_deviceId].rsaPubkeyE.length != 0, "no such device");
 
      if (devices[_deviceId].hasOrder) {
        Order memory o = orders[_deviceId];
        return (o.startHeight, o.duration, o.storageEPoint, o.storageToken);
      }
      return (0, 0, "", "");
    }

These last three functions are self-explanatory and, with them, the marketplace contract is now complete.

Few final considerations

This contract is to be considered just as a starting point: there are indeed a few limitations to keep in mind when thinking of this contract in production terms.
One limitation is, for example, in the "specs" argument that device's owner provides when registering a device in the marketplace. It makes sense for data consumers to be aware of the specifications of some devices compared to others. However, when trying to pick the best device to subscribe to, there is no way for a data subscriber to "trust" the specifications provided by the device's owner. This is partly a limitation in the way the "identity" of a device is registered by only recording its unique id. This id should be somehow linked to a proper decentralized identity document, signed by the manufacturer (or any trusted party) that provides the actual specifications for that device.

Another limitation of the contract lies in the lack of a "Proof of Device Ownership". The contract provides some level of protection by expecting the manufacturer to pre-register (or "whitelist") devices by their hashed Ids. This is done so that no one could infer these Ids just by looking at the contract's data or transactions, and therefore attempt to register devices without actually owning them. However, a malicious user who managed to somehow get off-chain access to a device's unique identifier before it's registered by the actual owner would still be able to register such a device and collect its subscription fees, without being in possession of the device itself. This limitation can be overcome using a challenge-response pattern based on digital signatures and secure hardware.

Conclusion

This data access authorization contract is a theoretical implementation of how real-world data could actually become a tradable good without the need for a centralized party. It would allow users to monetize on their smart devices, and buyers to directly subscribe to the more competitive data provider, based on their needs.

At this point, it's important to observe that Smart contracts only represent one component of a fully decentralized data marketplace. While they allow the delicate exchange of data and funds to be handled trustlessly, as you may have noticed, the actual data is never really sent to the smart contract. The reason is that blockchain is not a practical storage layer for big volumes of data, nor can it manage complex data post-processing that is often required in IoT applications. The fact is that the blockchain layer alone is usually never enough when trying to use real-world data with decentralized applications, especially if they provide financial benefits that could incentivize data tampering.

In this case, for example, the marketplace contract cannot really decide alone how and if the data was actually delivered to the buyers. The claim() function allows device owners to claim their balances, without knowing if the data was ever sent to the subscriber. The contract does not take into account any "proof of data delivered".

Another limitation is related to the data itself: even if a recipient did actually receive data, how can they make sure that that data actually** originated from the devices they subscribed to**, and that it hasn't been tampered with on its way from the device to them?

These and other important things like data privacy and security have to be taken into account when building what we call "MachineFi" applications, and they require to be addressed with "Layer-2" technologies using, whenever possible, "trusted hardware" capable of generating "verifiable data". This is exactly the focus of IoTeX: Not only a layer-1 blockchain built from scratch to serve machine economies, but also layer-2 components, hardware designs, and developer tools to facilitate jump-starting decentralized networks of machines, as well as the financialization of smart devices and machines' utility and data.


Docs

IoTeX Docs


IoTeX Developerslogo

devs@developers.iotex.io