MoonBlokz series part II. — Technologies & Architecture
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.
Programming language
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:
C/C++: These languages have the longest history in embedded development, offering extensive support, numerous libraries, and unmatched performance. You can do everything with these languages; however, they provide limited assistance in avoiding common bugs and poor design patterns.
MicroPython: MicroPython is a popular programming language that works on many microcontrollers, but it is not the optimal choice to squeeze every bit of performance from the hardware.
Other languages, like Java, do not provide real solutions for effective embedded development or are unsuitable for general usage (run even on computers), such as the Arduino platform.
RUST: Rust is a new player in the embedded scene but is gaining momentum rapidly. Its performance and available optimization options are comparable to C/C++, and Rust has much more support for creating bug-free software with idiomatic design patterns. This is the primary reason I chose Rust for MoonBlokz.
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:
First, I can only select a prototype device from the microcontrollers supported by Rust. Fortunately, there are many devices available. I will start with solutions based on RP2040 (the chip from the Raspberry Pi Pico). There are devices with integrated LoRa radio (https://www.adafruit.com/product/5714) or LoRa radio available as an add-on for the official Pico (https://www.waveshare.com/wiki/Pico-LoRa-SX1262-868M).
Second, I can only distribute MoonBlokz as source code. This limitation stems from Rust, and it is also a common practice in the embedded world. However, this does not automatically mean that MoonBlokz will be open-source; I have not yet made my decision on that.
Architecture
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:
BlockChain: Responsible for chain management and algorithms.
MoonBlokzNetwork: This is the library’s central facade, with a local API to access MoonBlokz’s functionality. It glues the other parts together.
Communication: Responsible for implementation-independent parts of the radio communication (like relaying algorithms).
Utils: Utility functions used by other parts of the library.
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.
In the simple model (for instance, on RP2040), flash storage can be accessed through a memory-mapped array for reading and via SPI communication for writing 4k pages at a time.
In the file system model, as the Adafruit Adalogger uses, flash storage can be accessed using a file-system-based API, allowing for reading and writing directories and files.
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:
If we can access a true random number generator provided by the microcontroller, we can use these generated random numbers in all of our algorithms.
If a true random number generator is unavailable, we can utilize pseudo-random numbers in all algorithms except for the initial key generation. (A separate article will discuss the details of the cryptographic functions.) In this case, the key pair must be generated externally and then copied to the node.
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:
send_message: add a message to the output buffer.
get_received_message: retrieve a message from the input buffer if available (non-blocking).
process: we channel the main loop into this module to maintain the input and output buffers.
Main loop management
Our program will change states based on the following events:
An incoming radio message arrives
The program calls library functions (such as creating a new transaction).
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:
The library handles the event loop, allowing the embedding program to register callbacks for its custom logic.
The embedding program provides the event loop and passes it to the library. This could be done by calling a processing function during each loop iteration.
I chose the second option because it is simpler and gives the embedding program greater control.
We have finalized the high-level architecture. Next, we will explore the algorithms for blockchain management. You can read it here.
If you are interested in our solutions, contact us via the form below.
Dorsum is proud to benefit from the European Commission’s Brexit Adjusment Reserve.
Learn more about our project aimed at aligning our Wealth Management Suite to the UK market. (Hungarian content)
What is a cookie?
A cookie is a small file of letters and numbers which we store in your browser or the hard drive of your computer (with your agreement). Cookies contain information which is transferred to your computer’s hard drive.
Why we use cookies?
Our website uses cookies to distinguish you from other users of our website. This helps us to provide you with a superb user experience when you browse our website and allows us to improve our internet presence
Choose whether to allow cookies or related technologies - webmasters, pixel tags, and Flash objects (cookies) - for the website as follows. Read our Privacy Policy below to learn more about how this website uses cookies and related technologies.
Before sending your approval, please set your preference in the below categories.
Required
These cookies are required for the basic functionality of the web site, such as secure login and tracking how long you have been in the processes.
If you disable this cookie, we will not be able to save your preferences. This means that every time you visit this website you will need to enable or disable cookies again.
Functional
These cookies allow the website to remember choices you make and provide enhanced functionality and personal features.
Please enable Strictly Necessary Cookies first so that we can save your preferences!
Performance
These cookies help to improve the performance of our website. For example, they collect information about which pages visitors go to most often and help us to provide a better user experience.
Please enable Strictly Necessary Cookies first so that we can save your preferences!