Build State Machine Using Rust | Part 3 | Runtime
Hello Rustaceans!
I hope you went successfully through the last part.
Recap 🤔
So far, we have worked on the scaffoldings/modules of the blockchain. But, we haven’t added any code in the main
program ☹️ yet.
Definitely we ran some tests to see if our functions are working as per our expectation. But in reality, when the project is run using $ cargo run
, nothing shows up.
Analogy:
Consider our progress so far as building various components/modules of a car. We’ve built the wheels, the seats, and the steering system. Now, it’s time to construct the heart ❤️ of the vehicle — the engine ⚙️ — which will empower it to function effectively and fulfill its purpose 🛣️.
Menu ☑️
So, let’s build the main program also called the “Runtime” in the world of blockchain or state machine. Thereafter, we would also run the blockchain producing blocks — 0
, 1
, 2
i.e. 3 blocks. In other words, we would be driving our car for few miles & evaluate its performance.
But how to achieve this? 🤯
1. Collate all the modules into a runtime struct/type.
2. Emulate producing/incrementing blocks. ⛓️🧊
So, we just need to complete steps — 1
& 2
.
Step-1
Following after part-2 code.
Now, let’s add the runtime struct in the main file — main.rs
:
struct Runtime {
system: system::Pallet;
balances: balances::Pallet;
}
and a new
inherent function for the runtime like this:
impl Runtime {
fn new() -> Self {
Self {
system: system::Pallet::new(),
balances: balances::Pallet::new()
}
}
Woo! 🥳 We are now all set to take our car 🚗 out for a spin.
Step-2
Now, let’s figure out where exactly we’re going to take our car 🚗 and how many miles we’ll drive.
So, let’s code the blockchain’s runtime v1.0
logic following the steps below:
a. Create a mutable instance of the runtime. Here, mut
(mutable) is used because we would modify the runtime fields inside which carries 2 components’ (System
& Balances
) fields — balances
, nonce
, block_number
.
So, select a car 🚗 of this model
v1.0
.
// previous code
fn main() {
// create a mutable runtime with genesis block
let mut runtime = Runtime::new();
assert_eq!(runtime.system.block_number(), 0);
}
We also check if the current block number is 0
i.e. the genesis block.
b. In order to do some activity with the current Balances
pallet, we have to airdrop/initiate some balances to at least 1 account (also called genesis account(s)). So, let’s make alice
rich with 100
tokens 😅 like this:
Normally, in a real blockchain network, these genesis accounts are airdropped with some initial tokens along with a treasury account that holds total/max. supply as decided by the Foundation/Labs.
Also, some/all genesis accounts hold permission of the treasury (to move funds in/out) for initial kickstart of the network to function properly. E.g. block rewards are paid automatically from the treasury account to the validators. Also, the contributors/participants/voters are rewarded with incentives (again from the treasury). Likewise there could be many more.
fn main() {
// previous code
// initiate some users/accounts
let alice = "alice".to_string();
let bob = "bob".to_string();
let charlie = "charlie".to_string();
let david = "david".to_string();
/* block #0 */
// set balance of alice as 100
runtime.balances.set_balance(&alice, 100);
}
You might wonder can anyone call/use
set_balance
function like this?
No way! 🙅♂️
In fact, these types of functions are called low-level functions that are exposed to run only by the network admins. Technically, these are automatically fed into the runtime code logic when a blockchain starts from block #0. And over the time before freezing code for the next upgrade, this type of functions are supposed to be mutually agreed upon (voted) by the community so that no random account(s) gets free tokens or assets or authority.
c. Next we create a new block where there are 2 transactions: Alice sends 101
tokens to Bob. This is supposed to fail as it doesn’t hold that much balance. Second transaction would be Alice sending 20
tokens to Bob which would be successful as the requested amount is within the Alice’s balance.
fn main() {
// previous code
/* block #1 */
let _ = runtime.system.inc_block_number();
// 1st tx: alice --101--> bob ❌ should fail, but won't panic as the error is swallowed.
match runtime.balances.transfer(&alice, &bob, 101) {
Ok(_) => {
let _ = runtime.system.inc_nonce(&alice);
},
Err(e) => eprintln!("Error: {}", e),
};
// 2nd tx: alice --20--> charlie ✅
match runtime.balances.transfer(&alice, &charlie, 20) {
Ok(_) => {
let _ = runtime.system.inc_nonce(&alice);
},
Err(e) => eprintln!("Error: {}", e),
};
}
We increment/create the block & then add the transactions. Consider this as validator/block producer’s job. They would do exactly the same as is.
Ideally, in a decentralized/peer-to-peer (p2p) setup, the block has to be accepted/rejected & based on that the block is considered as confirmed/finalized i.e. immutable or irreversible.
match
is used to ensure that the sender’s nonce is increased only if the token transfer
is successful. Otherwise, if there is any error in sending token like with 1st transaction of block #1, then it just prints (swallows) the error on terminal & moves ahead. It doesn’t panic.
Normally, in a real blockchain,
eprintln!
would not be used, insteadpanic!
with custom message is used. This would revert the transaction being sent. But, here the intent is to swallow the error & not let the blockchain stop. In some cases, the real blockchain also has smart contracts designed in such a way where the verification fails, but still the transaction gets added. In those cases the transaction fees are charged for sure in order to cover the compute used in verification of the transaction.In Rust 🦀, you would find very frequent use of
match
likeif-else
in other languages. In fact, insidematch
in one of the armsif
can also be used to cover some niche cases. Also, in some cases you would findif-let
's use instead ofmatch
if there is no need to cover all cases.
Here, you can observe that 2 low-level functions — inc_block_number()
, inc_nonce
are used. And 1 external function (a.k.a extrinsics) — transfer
is used.
💡Let me inform you that this
transfer
function would ideally be verified for correct signature by the original token holder so as to prevent any malicious person trying to transfer someone’s balance. This means the caller (sender) and the actual token holder’s account must be same.
d. Lastly, we create another block #2 to include 3 transactions like this:
fn main() {
// previous code
/* block #2 */
let _ = runtime.system.inc_block_number();
// 1st tx: charlie --5--> bob ✅
match runtime.balances.transfer(&charlie, &bob, 5) {
Ok(_) => {
let _ = runtime.system.inc_nonce(&charlie);
},
Err(e) => eprintln!("Error: {}", e),
};
// 2nd tx: bob --1--> den ✅
match runtime.balances.transfer(&bob, &den, 1) {
Ok(_) => {
let _ = runtime.system.inc_nonce(&bob);
},
Err(e) => eprintln!("Error: {}", e),
};
// 3rd tx: den --1--> alice ✅
match runtime.balances.transfer(&den, &alice, 1) {
Ok(_) => {
let _ = runtime.system.inc_nonce(&alice);
},
Err(e) => eprintln!("Error: {}", e),
};
}
This is challenge for you to understand the code yourself as the pattern remains same.
Visualize Runtime 👀
Now, let’s focus on how to view the runtime after each block is added (say). In other words, let’s inspect/view 🔍 how the car’s 🚗 engine is performing after every few miles 🛣️.
For this, we need to use Rust concepts like macros & traits.
In simple words, macros
are like magic rust code/shorthand that writes more rust code. More.
And traits
are like interfaces with function signatures that are used to add functionalities to a custom defined struct.
Here, we would add the Debug
trait on top of the runtime struct and successively we also need to add the same into the other 2 structs of both the pallets. This would result in:
In src/balances.rs
:
+ #[derive(Debug)]
struct Pallet {
balances: BTreeMap<String, u128>,
}
In src/system.rs
:
+ #[derive(Debug)]
struct Pallet {
block_number: u32
nonce: BTreeMap<String, u32>,
}
In src/main.rs
:
+ #[derive(Debug)]
struct Runtime {
system: system::Pallet;
balances: balances::Pallet;
}
Also, add this line to the end of code for each block:
println!("========================\n{:#?}\n========================", runtime);
Now, let’s inspect/view the runtime after each block:
========================
Runtime {
system: Pallet {
block_number: 0,
nonce: {},
},
balances: Pallet {
balances: {
"alice": 100,
},
},
}
========================
Error: Insufficient balance
========================
Runtime {
system: Pallet {
block_number: 1,
nonce: {
"alice": 1,
},
},
balances: Pallet {
balances: {
"alice": 80,
"charlie": 20,
},
},
}
========================
========================
Runtime {
system: Pallet {
block_number: 2,
nonce: {
"alice": 2,
"bob": 1,
"charlie": 1,
},
},
balances: Pallet {
balances: {
"alice": 81,
"bob": 4,
"charlie": 15,
"den": 0,
},
},
}
========================
What do you need to see for correctness as a part of viewing/inspecting:
- Check for block number if correctly incremented for each block.
- Check for
nonce
if correctly incremented of the sender after each block based on the transactions sent. - You can see that
den
received1
token frombob
& then transferred it toalice
. So, left out with0
. - Lastly, the blocks kept producing inspite of error thrown in block #1.
Full code 🧑🏻💻
Congratulations! 🎉🎉 You have built a runtime that literally has blocks getting produced when there are transaction(s) sent.
In modern blockchains, the blocks are sent every few seconds even if there are no transactions.
Next, we would be optimizing the runtime i.e. car’s engine so as to incorporate more modules swiftly.
That’s it for now! Happy coding 🧑🏻💻.
Stay tuned for more such medium blogs.
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 | Rust Bytes Newsletter