Solutions
No Results
Solution

IoT on the Algorand Blockchain - Interfacing with an ESP32

Overview

Introduction

This solution will outline an approach to enable low resource IoT devices built on the Espressif ESP32 chip to interact with the Algorand Blockchain. Key elements of the Algorand specification have been solved to lay a foundation for you to build more advanced solutions targeted at low resource devices. A basic C/C++ library supports features like Ed25519 key generation, retrieving account information and transactions. Finally, we will use a common blinking LED example to show how a device may respond to commands sent by users on the blockchain.

What You Need

This overview assumes you have working knowledge of common development tasks (GIT repositories, IDEs, compilers, linking, etc.). Knowledge of microcontrollers and the C/C++ language is helpful, but it should not be required to complete this example. Items you will need:

  1. ESP32-DevKitC-v1 development board. We use the v1 board for its built in LED. Anyone familiar with prototyping electronics using breadboards, LED diodes, etc. can also construct a prototype using other ESP32 development kits. The board used with this example can be found at Amazon.
  2. Visual Studio Code with PlatformIO. We will use the PlatformIO plugin to build code, flash firmware and monitor serial output.
  3. WIFI Access Point. We will connect the ESP32 to your local WIFI for interacting with the blockchain.
  4. MyAlgo.com wallet. We will use the wallet to manage our TESTNET account and send commands and Algos to the IoT device.
  5. Algorand TestNet Dispenser. (https://bank.testnet.algorand.network/)
  6. PureStake.io account. We will use the PureStake APIs to interact with TESTNET from our device. Please register for a developer account at (https://developer.purestake.io)

Basic Use Case

By the end of this project, we expect to have an IoT device with the following basic functions:

  1. Connect to a local WIFI access point
  2. Ability to create its own Algorand account by generating a public/private key pair
  3. Store the address (public key) in non-volatile storage to ensure we persist between reboots
  4. Monitor its account for a funding event.
  5. Monitor its account for commands sent to control the device LEDs

The Solution

Development Environment

  1. Install Visual Studio Code (https://code.visualstudio.com/)
  2. Install the PlatformIO IDE plugin for VS Code. Instructions found here: (https://platformio.org/install/ide?install=vscode)

Info

If you are new to the ESP32, interfacing with it over USB may require an additional driver. More information can be found at (https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/establish-serial-connection.html)

Get the Code

Open a terminal, ‘cd’ to your chosen directory, and clone the project with:

$ git clone https://github.com/jdl5711/algorand-esp32-example

You can now launch VS Code and open the project.

Review Project Source

Step 1: Platformio.ini

A few custom settings were added to the platformio.ini file. We updated the standard project to use C++ v17 and included two libraries to ease integration with non-volatile storage and parsing JSON. See build_flags, build_unflags and lib_deps below:

    [env:esp32dev]
    platform = espressif32
    board = esp32dev
    framework = arduino
    monitor_speed = 115200
    monitor_filters = colorize, default
    build_unflags = ${common.build_unflags}
    build_flags = ${common.build_flags}
    lib_deps = 
        rpolitex/ArduinoNvs@^2.5
        bblanchon/ArduinoJson@^6.17.3

    [common]
    build_unflags = -std=gnu++11
    build_flags = 
        -std=c++17
        -std=gnu++17

Step 2: WIFI Connection

We will not go into detail on setting up WIFI for an ESP32. If you are interested, you can review wifi_setup.h and wifi_setup.cpp. There are also many helpful Espressif tutorials online. The only step for you here is to open src/Constants.h and enter values for MY_SSID and MY_PASSWORD. Save and close the file.

Step 3: Create an Algorand Account (Account.cpp)

Our goal was to have a device that could stand alone on the blockchain by creating it’s own account, querying the blockchain and (eventually) sending it’s own transactions. The primary function of the Account object is to generate properly formatted Algorand private and public keys.

init()

The init function is called by the constructor. It’s primary responsibility is to find keys in non-volatile storage (NVS). If those keys don’t exist it starts the process to generate keys.

void Account::init(void) {
    // 1) look in non-volitale storage for the account. 
    NVS.begin();
    this->publicKey = NVS.getString(PUB_KEY);
    this->privateKey = NVS.getString(PRI_KEY);
    //If we don't have both the public and private key, regenerate.
    if(this->publicKey == NULL || this->privateKey == NULL) {
        this->publicKey = generateKeys();
    } else {
        Serial.println("Public:");
        Serial.println(publicKey);
        Serial.println("Private:");
        Serial.println(privateKey);
    } 
}

generateKeys() and Ed25519

The ESP32 bundles the libsodium library which provides a convenient method to generate an Ed25519 key pair.

// First, generate the Ed25519 public/private key pair
unsigned char seed[crypto_sign_SEEDBYTES];
randombytes(seed, crypto_sign_SEEDBYTES);
crypto_sign_ed25519_seed_keypair(public_key, private_key, seed);

After the key pairs have been created, we need to apply the Algorand rules to properly format keys.

// Second, evolve the keys into Algorand keys.
createAlgorandPrivateKey(private_key, public_key);
String pubKey = createAlgorandPublicKey(public_key);

createAlgorandPrivateKey()

Formatting the private key is straightforward and easily done with standard C/C++/ESP32 support. We concatenate the private and public keys, base64 encode and finally persist to NVS.

createAlgorandPublicKey()

The process of transforming the public key was a bit more complex primarily due to supporting the hash. Per Algorand documentation, the rules for public key creation include:

  1. Create a SHA-512/256 hash from the public key
  2. Append the last four bytes of the hash to the end of the public key
  3. Base32 encode the resulting byte array.

String Account::createAlgorandPublicKey(const uint8_t *public_key){
    // Step 1: generate a SHA-512/256 hash from the public key
    uint8_t public_hash[32];
    cppcrypto::sha512 sha512(256);
    sha512.init();
    sha512.update(public_key, 32);
    sha512.final(public_hash);

    // Step 2: Algorand expects the last 4 bytes of the hash to be 
    // appended to the public_key making it 36 bytes
    uint8_t *tmp_hash = public_hash + sizeof(public_hash) - 4;
    uint8_t public_key_final[36];  
    memcpy(public_key_final, public_key, 32);
    memcpy(public_key_final+32, tmp_hash, 4);

    // Step 3: The public_key is then base32 encoded  
    Base32 b32encoder;
    uint8_t *public_key_encoded = NULL;
    b32encoder.toBase32(public_key_final, 36, public_key_encoded, true);

    //Step 4: Need only the 58 chars (terminated by EOL) which is the Algorand Base32 encoded key
    String pubk = (String(reinterpret_cast< char const* >(public_key_encoded))).substring(0,58);

    Serial.println(F("PublicKey:"));
    Serial.println(pubk);
    this->publicKey = pubk;
    NVS.setString(PUB_KEY, pubk);
    return pubk;
}

It was particularly difficult to find support for SHA512/256 hashing on embedded devices and writing our own cryptography support was a show stopper. We found several libraries purporting to offer this capability, but it appears most simply truncate existing SHA512 hashes. Unfortunately, this does not meet or follow the SHA512/256 standard and caused invalid public keys. Ultimately, we decided to extract the necessary functions from the cppcrypto project on SourceForge. This project is not meant for embedded devices; however, the SHA code (once refactored) worked perfectly for key generation. More detail on cppcrypto at the end.

Account.cpp Summary

At this point the device can generate its public and private keys and store them to NVS to ensure we can successfully resume after a reboot or when new firmware is flashed.

Step 4: Interacting with the Blockchain (agloclient.cpp)

The next class to review is the AlgoClient located at “src/algorand/algoclient.cpp”. The AlgoClient is currently fairly simple. It’s primary responsibility is to interact with the blockchain through the V2 Indexer and Testnet APIs. It contains a private get() method responsible for executing the REST API call and extracting the JSON results from the response payload.

getAccountBalance()

The role of this method is to execute an API call to account information to retrieve the current account balance. We want to report back ALGOs and perform the appropriate conversion from microAlgos.

/**
 * Retrieve account information and extract the balance
 * String publicKey
 * return double balance
 */ 
double AlgoClient::getAccountBalance(String pubKey) {
    String serverPath = Constants::BASE_URL + "accounts/"+pubKey;
    DynamicJsonDocument doc = get(serverPath);
    double balance = doc["amount"];
    if(balance > 0) {
        balance = balance/1000000;
    }
    return balance;
}

getTransactions()

Another simple method calling the Indexer API to get a list of transactions bounded by the limit parameter.

/**
 * Retrieve transactions from the Indexer
 * String publicKey
 * int limit -- the number of results to return
 */ 
DynamicJsonDocument AlgoClient::getTransactions(String pubKey, int limit){
    String serverPath = Constants::IDX_BASE_URL + "accounts/"+ pubKey 
                        + "/transactions?limit=" + limit;
    return get(serverPath);
}

Step 5: Processing LED Commands (CommandProcessor.cpp)

The other primary feature of our project it to monitor the blockchain for “commands” received in a transaction from other users. This is the role of the CommandProcessor. Our approach is to watch the transaction note field for a JSON object that requests the device to blink it’s LED fast or slow. The general flow is as follows:

  1. The CommandProcessor uses the AlgoClient to retrieve the latest transaction (limit=1)
    DynamicJsonDocument doc = client->getTransactions(pubKey, 1);

  2. Checks the note field for a base64 encoded JSON object with an LED command. For example, {“led”:”blink-fast”}.

String note64 = doc["transactions"][0]["note"];
if(note64 != NULL) {
    unsigned char* decodedNote = base64_decode(((const unsigned char *)note64.c_str()), note64.length(), &outlen);

  1. If a command is found, spawn a FreeRTOs task in processLedCmd() to facilitate the LED blinking until the next command is received.

TaskHandle_t ledTaskHandle = NULL;
void toggleLed(void * parameter){
  // Start an infinite loop. 
  // This will run until the the task is killed by a new transaction/note
  for(;;){ 
    // Turn the LED on then pause
    digitalWrite(LED_PIN, HIGH);
    vTaskDelay(*((int*)parameter) / portTICK_PERIOD_MS);
    // Turn the LED off then pause
    digitalWrite(LED_PIN, LOW);
    vTaskDelay(*((int*)parameter) / portTICK_PERIOD_MS);
  }
}

void CommandProcessor::processLedCmd(String cmd){
  //first kill any running task and make sure the light is off.
  if(ledTaskHandle != NULL) {    
    vTaskDelete(ledTaskHandle);
    ledTaskHandle = NULL;
    digitalWrite(LED_PIN, LOW);
  }

  if(cmd.equalsIgnoreCase("blink-fast")){ 
    Serial.println("Received command to blink-fast");
    xTaskCreate(toggleLed, "ToggleFastLED", 1000, (void*)&FAST_LED, 1, &ledTaskHandle);
  } else if(cmd.equalsIgnoreCase("blink-slow")) {
    Serial.println("Received command to blink-slow");
    xTaskCreate(toggleLed, "ToggleSlowLED", 1000, (void*)&SLOW_LED, 1, &ledTaskHandle);
    //function, name, stack size, parameter, task priority, handle
  } else if(!cmd.equalsIgnoreCase("stop")) {
    Serial.println("Try again. Received unrecognized LED command: " + cmd);
  } else {
    Serial.println("Stopped the blinking");
  }
}

Step 6: main.cpp

The final file to review is main.cpp. This file is the heart of the device firmware and where all the pieces come together. You will notice two primary functions setup() and loop().

setup()

Setup is executed one time after the device boots. Anything you need to initialize for the device should be placed in this method. In this project, we use it to initialize WIFI and the Algorand Account. We then grab the public key to use in future processing.

String PUBLIC_KEY;
AlgoClient *CLIENT = new AlgoClient();
CommandProcessor *CMD_PROC = new CommandProcessor(CLIENT);

/*
* The setup function will be called once after boot.  Here we initialize
* WIFI and retrieve our public key. 
*/
void setup() {
  // Setting up serial output. 
  // This number should match monitor_speed in platformio.ini
  Serial.begin(115200);  

  setupWifi(Constants::MY_SSID.c_str(), Constants::MY_PASSWORD.c_str());

  //Create an account object and retrieve the public key
  Account *account = new Account();
  PUBLIC_KEY = account->getPublicKey();

}

loop()

This method will be called repeatedly until your program exists. All of your recurring tasks will originate from this method. It is here that the device checks its latest account balance and looks for any new commands that have been sent by users on the blockchain.

/*
*  This will run in a loop.  All recurring tasks go here. 
*  In this method we will monitor our Algorand account 
*  for balance changes and device commands. 
*/
void loop() {
  //1) Lets check our balance for more ALGOS.  
  double balance = CLIENT->getAccountBalance(PUBLIC_KEY);
  Serial.print("Balance = ");
  Serial.print(balance);
  Serial.println(" ALGOS");

  /* 
    2) Check for any new commands
        In our scenario, a command is an instruction from a 
        user on the Algorand blockchain sent to our IoT 
        device via the note field.  
  */
  CMD_PROC->processCommands(PUBLIC_KEY);
  delay(15000);
}

Test the Solution

Let’s run the project. We need to update Constants.h with the API and WIFI details. If you have not already created your accounts and updated this file, do so now.

You can create a PureStake account at PureStake

namespace Constants {
    //Your local WIFI SSID and password
     const String MY_SSID = "[ssid]";
     const String MY_PASSWORD =  "[passwd]";

    //Base URL for Algod TESTNET
     const String BASE_URL = "https://testnet-algorand.api.purestake.io/ps2/v2/";

    //Base URL for Indexer TESTNET
     const String IDX_BASE_URL = "https://testnet-algorand.api.purestake.io/idx2/v2/";

    //PureStake API Key
     const String API_KEY = "dIpTd0xmwG2******************";

};

Testnet Wallet and Disperse ALGOs

We will use a MyAlgo account to send transactions to our device. Create your testnet wallet at MyAlgo.com.

Copy your wallet address from MyAlgo and use the dispenser to send yourself ALGOs.

Build the Project

In VS Code, select the PlatformIO icon in the left menu (it looks like an ant). This will bring up a list of commands. Under “esp32dev->General” select “Build” as shown in the following image.
EditorImages/2021/03/11 23:10/platformio-build.jpg

This will launch a terminal in VS Code and compile the project. If all goes well, you will see “Success” at the end of the build.

Run on Device

Make sure your ESP32 is connected via USB. In the same menu used to build the firmware, select “Upload and Monitor”. This process will flash new firmware to your device and start the serial monitor.

Pay attention to the console as it may need your input on which USB port to use for monitoring. If you have multiple ports in use you will need to select your serial port. Here is an example:

--- Available ports:
---  1: /dev/cu.Bluetooth-Incoming-Port 'n/a'
---  2: /dev/cu.SLAB_USBtoUART 'CP2102 USB to UART Bridge Controller - CP2102 USB to UART Bridge Controller'
---  3: /dev/cu.URT1         'n/a'
---  4: /dev/cu.URT2         'n/a'
---  5: /dev/cu.usbserial-0001 'CP2102 USB to UART Bridge Controller - CP2102 USB to UART Bridge Controller'
--- Enter port index or full name: 5

Dependent upon how quickly you make that selection, you will begin to see serial output from the project, including confirmation of WIFI connection, your private and public keys and balance.

Fund Your Device

At this point, you can copy your public Algorand Address from the serial output and fund your device. Go back to your MyAlgo wallet and send ALGOs to your device’s new address. Within a few seconds of the transaction being confirmed, you should see serial output with a new ALGO balance.

Send LED Commands to Your Device

Now test the LED commands. Using your wallet again, send another transaction with or without ALGOs. In this transaction, select the “Advanced” option as shown in the below picture. The project currently watches for three different commands:

{"led":"blink-fast"}
{"led":"blink-slow"}
{"led":"stop"}

Important

Note that we are using the “Text” option to enter JSON for the note field. The JSON tab is currently mislabeled and should be labeled Message Pack.

EditorImages/2021/03/11 23:04/myalgo-transaction.jpg

Summary

You should now have a working ESP32 device that can manage its own public/private keys and interact with the blockchain.

Thanks to the Following Projects

NetRat/Base32 Encoding
Found in the “encoding” directory. Used for Base32 encoding of the Public Key.
https://github.com/NetRat/Base32

CppCrypto
Thank you to the cppcrypto project on sourceforge. The files under the sha/* directory were adapted from CppCrypto for use with this solution. There does not appear to be strong support for SHA-512/256 implementations for C/C++ on embedded systems. Upon refactoring a few classes from cppcrypto to operate on the ESP32, key generation worked perfectly.
http://cppcrypto.sourceforge.net/

Running the Project

Video tutorial of running the project and monitoring output.

C

PureStake

testnet

ed25519

Hardware

dispenser

APIs

VS Code

IoT

March 25, 2021