This article is part of the MoonBlokz series, which focuses on creating a hyper-local blockchain using microcontrollers and radio communication. You can find the previous articles here:
In this article, I will deconstruct the requirements, select the technology platform, and define the basic architectural elements.
Our program must run both on microcontrollers and computers. So, the language must support embedded development. There are only a few significant languages for embedded development. Let’s see them one by one:
Rust & me
I have been developing software for over 30 years and have worked with numerous programming languages before discovering Rust. I started with BASIC and have programmed in Pascal, x86 Assembly, C, C++, Java, Objective-C, Swift, and JavaScript. Each language has unique characteristics, but Rust surprised me, even after all these years.
One of the most striking features of Rust is that it functions as both a low-level and a high-level language simultaneously, which reminds me of Objective-C. Although the two languages are quite different, they both allow access to low-level functions while also providing high-level abstractions.
In most programming languages, you can quickly reach a state where your code compiles successfully, or you might not even need a compiler at all. As a beginner in Rust development, I found myself examining compiler error messages more than I had with any other language. However, once the program compiled successfully, I reached a working state much more quickly than with other languages. I appreciate this approach because achieving a compilable state is a definite milestone — you know exactly when you’re ready.
I also enjoy some handy features of Rust, such as the enums with embedded variants, as well as the power of generics and pattern matching. While it’s challenging to write it down, I even appreciate the borrow-checker.
Choosing Rust comes with some direct consequences:
Many architectural details cannot be decided without knowing the algorithms and data structures. However, we can outline the application’s basic layout at this stage:
What do we see on the diagram?
MoonBlokz will be developed as a library named moon_blokz_lib, which we can integrate into our applications. Since we have chosen Rust for our project and are targeting embedded platforms, this library will be a `no_std` library. The advantage of using `no_std` libraries is that they can be utilized in both standard and `no_std` applications, making their use quite versatile.
The library contains four main modules:
The implementation program must provide some external modules for the library to function properly. In the following section, we will examine these modules individually to discuss their importance and the key strategies for designing their APIs.
Storage
It is generally advisable to keep the required APIs as low-level as possible to simplify implementations. However, we cannot adhere to this principle because various storage access models are available, even on microcontrollers.
Creating a low-level API that efficiently accommodates both models is not feasible. Let’s see a little example:
We want to store blocks of binary data, each approximately 2500 bytes in size, and query them using a sequence identifier (u64, with the difference between the largest and smallest values being relatively small) and a hash value represented as [u8; 32].
If we have a file system, we can utilize a directory structure to store the blocks. We would name the directories according to the sequence numbers and the files according to the hash values. This approach simplifies querying a block since we can easily construct the full file path using the sequence number and hash value.
On the other hand, if we do not have access to a file system, we can allocate a fixed space on persistent storage for the blocks (a predetermined number of bytes) and create a B-Tree data structure. This option introduces additional complexity in the code, but it may offer better performance by avoiding the overhead associated with a general file system.
In both scenarios, we need to encapsulate the store and query logic within the storage methods and define a high-level API (save and query block) because we cannot share anything between the two totally different approaches.
Consequently, I will define a higher-level API and provide two distinct implementations within the library, both utilizing lower-level APIs from the program that employs the library.
Random
Generating random (or pseudo-random) numbers will be essential for various aspects of our program, including cryptography, making relay decisions, and selecting transactions to drop when the mempool is full. There are two categories of random number generation:
Otherwise, the random generator’s API will be simple. It will provide random numbers of different types.
Crypto
This module is designed for cryptographic functions, such as creating and verifying digital signatures. While this functionality can be implemented within the library itself, many chipsets offer accelerated cryptographic functions. Therefore, it’s beneficial to provide the option to utilize these hardware accelerations. The library includes a standard implementation as well.
Clock
This module provides a clock for measuring elapsed time. As mentioned in the previous article, we do not rely on a shared global time between nodes; rather, we need to measure elapsed time for each node individually for our algorithms. It is not a difficult task, as every microcontroller supports some form of tick counting. The API is straightforward, providing a method to retrieve time in milliseconds.
However, there is one tricky aspect: we must ensure that the time increases monotonically, even after rebooting. We will address this issue later.
Radio
This module manages the hardware-dependent aspects of communication, including sending and receiving messages and formatting messages to fit into radio communication frames. The API is simple:
Main loop management
Our program will change states based on the following events:
To handle these events, we need an event loop (don’t let any frameworks fool you; somewhere in the background, there is always an event loop). We have two options for managing the event loop:
I chose the second option because it is simpler and gives the embedding program greater control.