Coding a P2P blockchain in Rust: Part 1

Blocks of connected buildings in a city with some of them lit up.
Photo by bert sz on Unsplash

This is an attempt to learn how to build a new blockchain that is customised for a specific purpose, with open source libraries. Objective is learning and strengthening of concepts. However beyond that, the main incentive for me is that I get to code in Rust, a programming language that I fell in love with, at first sight, earlier in the year. I also noticed recently that several high-profile blockchain platforms are being quietly built in Rust — Facebook Libra, Hyperledger projects (Sawtooth, Aries, Indy and Ursa), Parity (Ethereum client), and Exonum, so there are indeed several (smart!) guys and gals who have seen the value of Rust in blockchain protocol & core modules development.

As an aside, even though I don’t particularly have a great track record of successfully predicting which tech/language/framework will win in future, it doesn’t stop me from making one more prediction. Mark my words: Rust will rule the world of systems programming and high-performance server development within the next 10 years. If you don’t believe me, contact me after 10 years.

Now with the proclamation out of the way, I need to make some disclaimers.

Let’s first define the problem we are trying to solve, which has an impact on the design of blockchain. Three of the major groups of blockchain use-cases are :

We are going to focus on the third scenario for this tutorial. Here are some sample use cases that are applicable to the shared-data scenario:

In all these cases ,we don’t necessarily need to define an asset with financial value, we are only concerned with recording and sharing data among a group of participants in a manner where no single entity/participant owns the data. So, we can adopt a slightly different , light-weight approach here to design this.

Let’s first define what features of blockchain are needed to implement a simple but immutable shared-data blockchain :

Design constraints for this tutorial:

To recap and reiterate , we are building a blockchain because we want to store identical copies of shared data across different nodes on a peer-to-peer network, managed by a common protocol, and ensure the data once stored, is immutable. The participants of this network can include multiple individuals, businesses and institutions spanning across geographical and organisational boundaries.

This is a rather modest but important goal, which cannot be achieved with a regular centralised database.

Now that we know the what and why, let’s go to the how.

Let’s start by defining a few data structures:

pub struct Transaction {
pub transaction_id: String,
pub transaction_timestamp: i64,
pub transaction_details: String,
}
pub struct Block {
pub block_number: u64,
block_timestamp: i64,
pub block_nonce: u64,
pub transaction_list: Vec<Transaction>,
previous_block_hash: String,
}

We are defining a simple Transaction struct, which does not have sender and receiver account details. There is a transaction_details field, which can store transaction text. We will use this field to store the shared data.

Similarly, the Block struct does not have a separate header and body, just the header fields (block_number, block_timestamp, previous_block_hash) and and a transaction list. This should suffice for now.

Let’s add the following dependencies to Cargo.toml file:

serde = "1.0.98"serde_json = "1.0.40"serde_derive = "1.0.98"crypto-hash = "0.3.3"chrono = "0.4"

Serde-related crates are for serializing and deserializing block data. SHA256 digest comes from crypto-hash crate. Chrono is for block and transaction timestamps.

We will now define the following methods on the Block struct:

2. serialize_block(): Serialize block contents to json format for i/o (network, storage) purposes.

3. generate_hash(): Generate SHA256 hash of a given block.

4. is_block_valid(): Function that checks if the blockhash meets the difficulty level set (i.e. if the block has contains the required number of leading zeros).

5. new(): Create a new block, when a new transaction arrives.

6. mine_new_block(): Do proof-of-work calculations to determine the nonce.

impl Block {
pub fn genesis() -> Self {
let transaction = Transaction {
transaction_id: String::from("1"),
transaction_details: String::from("This is dummy transaction as genesis block has no transactions"),
transaction_timestamp: Utc::now().timestamp(),
};
Block {
block_number: 1,
block_timestamp: Utc::now().timestamp(),
block_nonce: 0,
transaction_list: vec![transaction],
previous_block_hash: String::from("0"),
}
}
pub fn serialize_block(&self) -> String {
serde_json::to_string(&self).unwrap()
}
pub fn generate_hash(block: &Block) -> String {
hex_digest(Algorithm::SHA256, block.serialize_block().as_bytes())
}
pub fn is_block_valid(hash: &str, prefix: &str) -> bool {
hash.starts_with(prefix)
}
pub fn new(transactions: Vec<Transaction>, previous_block: &Block) -> Block {
Block {
block_number: previous_block.block_number + 1,
block_timestamp: Utc::now().timestamp(),
block_nonce: 0,
transaction_list: transactions,
previous_block_hash: Self::generate_hash(previous_block),
}
}
pub fn mine_new_block(block_candidate: &mut Block, prefix: &str) {
while !Self::is_block_valid(&Self::generate_hash(block_candidate), prefix) {
println!("{}", block_candidate.block_nonce);
block_candidate.block_nonce += 1
}
}
}

Let’s now write some unit tests:

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_genesis_block() {
//create blockchain
let p2p_bc: Vec<Block> = vec![Block::genesis()];
assert_eq!(p2p_bc[0].block_number , 1);
assert_eq!(p2p_bc[0].transaction_list[0].transaction_details, "This is dummy transaction as genesis block has no transactions");
}
#[test]
fn test_new_block() {
let mut p2p_bc: Vec<Block> = vec![Block::genesis()];
let new_txn = Transaction {
transaction_id: String::from("1"),
transaction_timestamp: 0,
transaction_details: String::from("Testing a new transaction"),
};
let mut new_block = Block::new(vec![new_txn], &p2p_bc[p2p_bc.len() - 1]);
Block::mine_new_block(&mut new_block, &PREFIX);
p2p_bc.push(new_block);
assert_eq!(p2p_bc.len(),2);
assert_eq!(p2p_bc[1].transaction_list[0].transaction_details,"Testing a new transaction");
}
}

In Rust , unit tests can be included as part of the source (.rs files). Unit test scripts are indicated by #[cfg(test)] attribute and the individual unit tests are listed within the tests module (mod tests). Here we have declared two unit tests one to test genesis block and the second to test creation of new block.

For test 1 (test_genesis_block), we can instantiate a new blockchain with the genesis block as follows:

let p2p_bc: Vec<Block> = vec![Block::genesis()];

We then make two checks for this test- one to check if the block number of genesis block is 1, and the second to check whether the transaction details are stored correctly for the genesis block. (Note: In a ‘real’ public blockchain, the genesis block contains block reward for the miner, but here we don’t have any mining rewards, so we are just adding a dummy transaction).

For the second test (test_new_block) we have to create a new block. for which four steps are needed:

The four steps are shown below:

let new_txn = Transaction {
transaction_id: String::from("1"),
transaction_timestamp: 0,
transaction_details: String::from("Testing a new transaction"),
};

We are hardcoding the transaction id to “1” as we will have only one transaction per block in this tutorial, for simplicity (like how ganache does it).

2. Create a new block as follows:

let mut new_block = Block::new(vec![new_txn], &p2p_bc[p2p_bc.len() - 1]);

3. Mine the new block:

Block::mine_new_block(&mut new_block, &PREFIX);
p2p_bc.push(new_block);

Note here, for mine_new_block method, we are passing the newly created block (mutable borrow in rust terminology) to the mining method which will directly update the nonce in new block struct once Proof-of-work is concluded. So there is no return value in this method. There is also a second parameter to this method, &PREFIX, which is a constant specifying the difficulty level for proof-of-work. Declare this constant as follows :

pub const PREFIX: &str = "00";

4. Append the newly mined block to the blockchain:

p2p_bc.push(new_block);

We can now check with assert_eq! macro whether the transaction details are correctly recorded in the newly created block.

Here is the complete code split across two files — blockchain.rs and main.rs

Create a new project using cargo new <project_name>, and under the src directory, populate the blockchain.rs and main.rs files as below:

Blockchain.rs

extern crate crypto_hash;
extern crate serde_json;
extern crate chrono;
use crypto_hash::{hex_digest, Algorithm};
use chrono::prelude::*;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Transaction {
pub transaction_id: String,
pub transaction_timestamp: i64,
pub transaction_details: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Block {
pub block_number: u64,
block_timestamp: i64,
pub block_nonce: u64,
pub transaction_list: Vec<Transaction>,
previous_block_hash: String,
}
pub const PREFIX: &str = "00";impl Block {
pub fn genesis() -> Self {
let transaction = Transaction {
transaction_id: String::from("1"),
transaction_details: String::from("This is dummy transaction as genesis block has no transactions"),
transaction_timestamp: Utc::now().timestamp(),
};
Block {
block_number: 1,
block_timestamp: Utc::now().timestamp(),
block_nonce: 0,
transaction_list: vec![transaction],
previous_block_hash: String::from("0"),
}
}
pub fn serialize_block(&self) -> String {
serde_json::to_string(&self).unwrap()
}
pub fn generate_hash(block: &Block) -> String {
hex_digest(Algorithm::SHA256, block.serialize_block().as_bytes())
}
pub fn is_block_valid(hash: &str, prefix: &str) -> bool {
hash.starts_with(prefix)
}
pub fn new(transactions: Vec<Transaction>, previous_block: &Block) -> Block {
Block {
block_number: previous_block.block_number + 1,
block_timestamp: Utc::now().timestamp(),
block_nonce: 0,
transaction_list: transactions,
previous_block_hash: Self::generate_hash(previous_block),
}
}
pub fn mine_new_block(block_candidate: &mut Block, prefix: &str) {
while !Self::is_block_valid(&Self::generate_hash(block_candidate), prefix) {
println!("{}", block_candidate.block_nonce);
block_candidate.block_nonce += 1
}
}
}

main.rs

#[macro_use]
extern crate serde_derive;
extern crate chrono;
mod blockchain;use blockchain::*;fn main() {
println!("Welcome to P2P Rust Blockchain experiment");
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_genesis_block() {
//create blockchain
let p2p_bc: Vec<Block> = vec![Block::genesis()];
assert_eq!(p2p_bc[0].block_number , 1);
assert_eq!(p2p_bc[0].transaction_list[0].transaction_details, "This is dummy transaction as genesis block has no transactions");
}
#[test]
fn test_new_block() {
let mut p2p_bc: Vec<Block> = vec![Block::genesis()];
let new_txn = Transaction {
transaction_id: String::from("1"),
transaction_timestamp: 0,
transaction_details: String::from("Testing a new transaction"),
};
let mut new_block = Block::new(vec![new_txn], &p2p_bc[p2p_bc.len() - 1]);
Block::mine_new_block(&mut new_block, &PREFIX);
p2p_bc.push(new_block);
assert_eq!(p2p_bc.len(),2);
assert_eq!(p2p_bc[1].transaction_list[0].transaction_details,"Testing a new transaction");
}
}

From the home directory of project, run Cargo test.

You will get the message that 2 tests have passed successfully.

So far , so good. But where is the P2P part? How can additional nodes be spun up and how will they discover and communicate with each other?

We will look at it in Part 2. (if you don’t see the link here , it means I haven’t yet published it).

If you like what you read, show it with claps. If you didn’t like it, feel free to critique it. If you have any suggestions, leave them in comments section.

Happy Rust-ing!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store