Skip to Content
Course content

229: Building a Simple Blockchain in Rust

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I worked with a developer named Marcus who was building a high-stakes audit log for a legacy banking system. He initially used a standard SQL table, thinking that restrictive permissions would keep the data safe. Then, during a security audit, the team discovered that a database administrator had manually updated a few transaction records to cover up a series of errors. Because the records were just rows in a table, there was no way to prove the data had been altered after the fact. Marcus realized that he didn't need a database; he needed a way to make the data computationally expensive to fake. That's when he dove into the world of cryptographic chaining.

At its core, a blockchain is just a distributed ledger, but for our purposes today, think of it as a linked list where the "link" is a cryptographic hash of the previous element. If you change a single bit in block one, its hash changes. Because block two contains that hash, block two's hash now changes, and so on, creating a domino effect that makes tampering immediately obvious.

Defining the Immutable Block Structure

In Rust, we want our block to be a self-contained unit of data. We need to track the index, a timestamp, the actual payload (the transaction or log), the hash of the previous block, and the hash of the current block. I usually recommend using the sha2 crate for this because it's the industry standard for SHA-256 hashing.

use sha2::{Sha256, Digest};
use std::time::SystemTime;

#[derive(Debug, Clone)]
pub struct Block {
    pub index: u32,
    pub timestamp: u64,
    pub data: String,
    pub previous_hash: String,
    pub hash: String,
}

impl Block {
    pub fn new(index: u32, data: String, previous_hash: String) -> Self {
        let timestamp = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .expect("Time went backwards")
            .as_secs();

        let mut block = Block {
            index,
            timestamp,
            data,
            previous_hash,
            hash: String::new(),
        };
        
        block.hash = block.calculate_hash();
        block
    }

    pub fn calculate_hash(&self) -> String {
        let input = format!("{}{}{}{}", self.index, self.timestamp, self.data, self.previous_hash);
        let mut hasher = Sha256::new();
        hasher.update(input);
        format!("{:x}", hasher.finalize())
    }
}

Notice that we calculate the hash inside the new method. In a real-world scenario, you'd likely have a "mining" process here to find a hash with specific properties, but for a simple blockchain, a direct hash of the contents is enough to establish the chain of trust.

Linking Blocks into a Verifiable Chain

Now we need a way to manage these blocks. The Blockchain struct is essentially a wrapper around a Vec<Block>. The most critical part here is the "Genesis Block"—the very first block in the chain. Since there's no previous block to reference, we have to hardcode its previous_hash. It's the anchor for everything that follows.

pub struct Blockchain {
    pub chain: Vec<Block>,
}

impl Blockchain {
    pub fn new() -> Self {
        let genesis_block = Block::new(0, "Genesis Block".to_string(), "0".to_string());
        Blockchain {
            chain: vec![genesis_block],
        }
    }

    pub fn add_block(&mut self, data: String) {
        let previous_hash = self.chain.last().unwrap().hash.clone();
        let new_block = Block::new(
            self.chain.len() as u32, 
            data, 
            previous_hash
        );
        self.chain.push(new_block);
    }
}

I like the simplicity of this approach. By pulling the hash from the last() element of the vector, we ensure that each new block is mathematically tied to the one before it. If you were to change the data in chain[0], the previous_hash stored in chain[1] would no longer match the recalculated hash of chain[0].

Verifying Chain Integrity

The real power of this structure isn't in storing the data, but in verifying it. To check if the chain is still valid, we need to iterate through the blocks and ensure two things: first, that the block's stored hash actually matches its recalculated hash, and second, that the block's previous_hash matches the hash of the block before it.

If either of these checks fails, you know the data has been tampered with. This is the "trustless" part of the system; you don't have to trust the person holding the data—you just trust the math.

impl Blockchain {
    pub fn is_chain_valid(&self) -> bool {
        for i in 1..self.chain.len() {
            let current = &self.chain[i];
            let previous = &self.chain[i - 1];

            if current.hash != current.calculate_hash() {
                return false;
            }
            if current.previous_hash != previous.hash {
                return false;
            }
        }
        true
    }
}



📋 Practical Task

Exercise: Implementing a Blockchain Tamper-Detector

Your task is to write a test suite that proves the blockchain's integrity checks actually work. You will need to:

  • Initialize a Blockchain and add at least three blocks of data (e.g., "Payment 1: $10", "Payment 2: $20", "Payment 3: $30").
  • Verify that is_chain_valid() returns true for the freshly created chain.
  • Manually mutate the data field of the second block in the chain (e.g., change "$20" to "$2000").
  • Verify that is_chain_valid() now returns false.
  • Attempt to "fix" the chain by recalculating the hash of the tampered block, and then verify that the chain is still invalid because the subsequent block's previous_hash no longer matches.

Ensure you include the sha2 crate in your Cargo.toml to handle the hashing logic.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.