Build State Machine Using Rust | Part 2 | System Module

Abhijit Roy
Rustaceans
Published in
6 min readJan 2, 2024
Alice transfers token to Bob leading to update in System pallet’s metadata

Hello folks! 👋

Hope you were successful in getting through the part 1.

If so, let’s proceed to the next module, which will further enhance the capabilities of our state machine.

So far, with balances pallet added, it’s just a barebone which doesn’t have any data persistence over a period.

There comes the System module which is all about ensuring data persistence between each transaction made like sending money to and fro as we saw in Balances pallet.

In this whole series, ‘module’ and ‘pallet’ terms would be used interchangeably. The term ‘Pallet’ is commonly used in the world of substrate (whole new class of blockchains).

Now, how to ensure data persistence?

Simply by tracking every transaction or activity made by the users. Right?

If you guessed it right, you are on the right track, else don’t worry! By the end of this series you will surely be able to understand every nitty gritties.

Now, let’s think over 🤔 for a bit and decide which parameters/fields should we have inside the System pallet struct.

  1. Nonce: means “a number used once”. If you are familiar with EVM (Ethereum) based blockchains, you might have come across this field. If not then, consider this as a counter that is incremented after every transaction is successful.

Now, how do we know if a transaction is successful or not?

For this, we need to have some kind of database. Now, a traditional database won’t make sense. Right? Because that has been around for years and we know the problems related to security.

Therefore, we would prefer something like decentralized database than a centralized counterpart. And that has been possible with the advent of consensus algorithms in a distributed systems with “blockchain technology” which just not have the p2p nodes as component, but also give incentives to the participating nodes, which was a glaring miss in BitTorrent.

This brings to the next field — “Database” which is effectively chain of blocks where the transactions are supposed to be added in order to be considered as successful.

2. Block Number: means “latest block number”. So, after each epoch/duration/period, there is a block added to the database. And each block carries some transactions.

Here, imagine if a block is added every 12 seconds and by then, if we have 10 transactions (sending money via Balances pallet) requested/sent to the network. Then, all the 10 transactions are likely to get added to the block unless it hits the block limit. Finally, the block number is incremented from say 0 to 1 and so on and so forth with more transactions requested thereafter.

Now, what is this block limit?

Well, the block limit is defined by either:

  • The size of the data (or transaction bytes to be added), or
  • Compute unit (measured in Network bandwidth & CPU bandwidth in a p2p setup) needed to verify if the data (transactions) sent are legit i.e. the public-key/address of the sender/caller matches with the computed address of signer (from given signature & hash of original message/data). or

This is normally the case in case of smart contract platforms like EVM, EOS, etc.

We could also have a system where if there is no transaction requested, the no block is produced like Bitcoin. Each miner/validator tries to maximize their earning by including as much transaction as possible to earn more fees from the transactions. But still there is a parameter called difficulty which is adjusted to bring the block time to 10 mins. Although some blocks took around an hour to get added to the blockchain database waiting for more transactions to meet the 1 MB block size limit.

  • May be some other that I am not aware of.

Whew, that’s a lot to take in, right? 🤯 No worries though, take a deep breath and relax. 🧘 We’ve got this!

In simple words, the attempt is to add the data in the shortest time possible for scalability & also ensuring security to avoid clogging the network with enormous amount of data thrown at a time to get added to the next block, leading to network congestion.

Now, let’s code the struct for System pallet in a separate file — system.rs which would represent a rust module:

struct Pallet {
block_number: u32,
nonce: BTreeMap<String, u32>
}

Here, each file is representing a module which is linked in the main.rs file. Like system.rs represents System Pallet. Similarly, balances.rs represents the Balances Pallet.

So, we have to update the main.rs file as:

mod balances;
+ mod system;

fn main() {}

To keep things simple as of now, we just added the block_number which maintains the current/latest no. of the blockchain’s block added/considered. And nonce field maintains the no. of transactions sent by an account/user till the no. of blocks represented by block_number. Here, the user’s account is represented by simple name (no collision allowed, kind of like username). So suppose if Alice sends money to Bob, then it leads to incrementing nonce of Alice by 1.

💭 Does this mean that Alice can have only 1 transaction added to a block?

Not actually!

This rather means that Alice may send money to Bob multiple times or to multiple persons like Charlie, Eve, etc. Here, each transaction leads to increase in nonce & thereby difference in nonce value from the previous block vs current block can be the no. of transactions sent by the caller/sender/signer in the latest block. So, if Alice’s nonce as per the last block was 5 and if Alice decides to send 10 transactions, then assuming all the 10 transactions gets added to the next block, the new nonce of Alice becomes 15.

Now, let’s code some functions to increment the block_number, nonce like this:

  • new: Initialize the System Pallet. In blockchain, this is pretty much the case at the genesis block i.e. block #0.
// previous code

impl Pallet {
fn new() -> Self {
Self {
block_number: 0,
nonce: BTreeMap::new()
}
}
}
  • block_number: Get the latest block number (last updated one).
// previous code

impl Pallet {
// previous code

fn block_number(&self) -> u32 {
self.block
}
}

self represents the struct i.e. Pallet itself, for which the functions are defined inherently. And &self represents the immutably borrowed instantiated Pallet struct.

  • inc_block_number: Increment the latest block number by 1.
// previous code
impl Pallet {
// previous code

fn inc_block_number(&mut self) {
self.block += 1
}
}

Here &mut self represents the mutably borrowed instantiated Pallet struct. This means that the struct could be modified.

  • inc_nonce: Increment the user’s nonce by 1. This helps us keep track of how many transactions each account has made.
// previous code
impl Pallet {
// previous code

fn inc_nonce(&mut self, who: &String) {
let old_nonce = *self.nonce.get(who).unwrap_or(&0);
let new_nonce = old_nonce + 1;
self.nonce.insert(who.clone(), new_nonce);
}
}

Now, all the inherent functions are defined for the System pallet’s struct/type.

Let’s jump onto the next part i.e. write tests for System pallet:

  • init_system: This test is mainly for checking if the struct’s fields are properly updated when incrementing the block number & nonce, considering a transaction like Alice sends 10 tokens to Bob.
// previous code

#[cfg(tests)]
mod tests {
#[test]
fn init_system() {
// instantiate the system pallet
let mut system = super::Pallet::new();

// increment the block number
system.inc_block_number();

// increment the alice's nonce
system.inc_nonce(&"alice".to_string());

// assert the current block number
assert_eq!(system.block_number(), 1);

// assert the alice, bob nonce
assert_eq!(system.nonce.get(&"alice".to_string()), Some(&1));
assert_eq!(system.nonce.get(&"bob".to_string()), None);

}
}

Here, super::Pallet::new() indicates calling new function of Pallet that is defined just outside tests module.

NOTE:
Here, we haven’t defined any inherent function called nonce like block_number, so we depend on using get() function of BTreeMap collection.
Now, if you can recall, in the balances pallet previously, we defined an inherent function called balance which unwrapped as 0 in place of None. Similarly, we could also do here that would result into 0 nonce for accounts which didn’t make any transaction like Bob here. But, that is not the intent here, so we haven’t defined any inherent function like that. Hence, Bob has nonce as None.

Now, you can run the related test like this on terminal:

# for running test for system pallet
cargo test -- system::tests::init_system

# for running all tests
cargo test

Full code 👨‍💻

Summary

The system pallet is more like metadata of the state machine which allows to have some transactions/activities. This metadata keeps track of the activity over a course of time.

I give a huge shoutout 📢 to some of the works already done by engineers at Parity. My goal has been to simplify this as much as possible. And also to add my perspective wherever I thought as required.

That’s all for now!

Rustaceans 🚀

Thank you for being a part of the Rustaceans community! Before you go:

  • Show your appreciation with a clap and follow the publication
  • Discover how you can contribute your own insights to Rustaceans.
  • Connect with us: X | Weekly Rust Newsletter

--

--

Abhijit Roy
Rustaceans

Blockchain (Rust, Solidity, C++, JS/TS) || Data Science (Python) || Bot (Python) || Backend (JS, TS)