This article provides a step-by-step breakdown of implementing a basic blockchain in Rust, from the initial setup of the block structure, including unique identifiers and cryptographic hashes, to block creation, mining, and validation, laying the groundwork. It explains the rationale behind each function, offering insights into the mechanics of blockchain technology, such as proof of work, nonce calculation, and maintaining the integrity and continuity of the blockchain.
Disclaimer: Remember that this is a bare-bones and basic Blockchain implementation for learning purposes only and is not intended for production environments! :)
Let’s dive right in, fellow Rustaceans!
The Groundwork
Here are the core concepts and key elements in handling P2P network interactions in blockchain:
Node and Peer Discovery
- Node Identity: Each node in the blockchain network has a unique identifier, often derived from a cryptographic key pair.
- Peer Discovery: Nodes must discover each other to form a network. This can be achieved through various methods, including static configuration (predefined peers), DNS-based discovery, or using protocols like mDNS for local network discovery.
- Bootstrap Nodes: New nodes often connect to known, reliable nodes (bootstrap nodes) to quickly integrate into the network.
Network Protocols
- Protocol Stacks: Blockchain P2P networks use specific protocol stacks for communication. Commonly used protocols include TCP/IP for basic transmission and cryptographic protocols (like TLS or Noise) for secure communication.
- Messaging Protocols: Protocols like
libp2p's Floodsub or Gossipsub are used for message broadcasting and propagation across the network.
Data Propagation and Synchronization
- Broadcasting: Nodes broadcast transactions and newly mined blocks to the network, ensuring that all participants receive the latest data.
- Chain Synchronization: Nodes synchronize their blockchain copies to the longest chain (commonly accepted as the valid one) to maintain consistency in the network.
- Consensus Mechanisms: Consensus algorithms like Proof of Work (PoW) or Proof of Stake (PoS) are used to agree on the state of the blockchain, especially for validating and adding new blocks.
Once these key concepts are understood, let’s now jump to the code!
cargo.toml — Add the following dependencies
[dependencies]
chrono = "0.4"
sha2 = "0.9.8"
serde = {version = "1.0", features = ["derive"] }
serde_json = "1.0"
libp2p = { version = "0.40.0", features = ["tcp-tokio", "mdns"] }
tokio = { version = "1.0", features = ["io-util", "io-std", "macros", "rt", "rt-multi-thread", "sync", "time"] }
hex = "0.4"
once_cell = "1.5"
log = "0.4"
pretty_env_logger = "0.4"
We won’t cover every lib in detail to not take much time, but it’s worth mentioning a key library for P2P implementation — the libp2p.
I wrote a full article about the libp2p here where you can get familiar with this rich Rust crate.
blockchain.rs — Event Processing and P2P Communication
Let’s create a new file named ‘blockchain.rs’ in which we will implement our event processing and p2p methods, as below:
Basic Setup
KEYS,PEER_ID,CHAIN_TOPIC,BLOCK_TOPIC: These static variables initialize cryptographic keys for identity, the peer ID for the network node, and topics for the Floodsub protocol to handle chain and block-related messages.
use super::{App, Block};
use libp2p::{
floodsub::{Floodsub, FloodsubEvent, Topic},
identity,
mdns::{Mdns, MdnsEvent},
swarm::{NetworkBehaviourEventProcess, Swarm},
NetworkBehaviour, PeerId,
};
use log::{error, info};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use tokio::sync::mpsc;
pub static KEYS: Lazy<identity::Keypair> = Lazy::new(identity::Keypair::generate_ed25519);
pub static PEER_ID: Lazy<PeerId> = Lazy::new(|| PeerId::from(KEYS.public()));
pub static CHAIN_TOPIC: Lazy<Topic> = Lazy::new(|| Topic::new("chains"));
pub static BLOCK_TOPIC: Lazy<Topic> = Lazy::new(|| Topic::new("blocks"));
Event Types
ChainResponse,LocalChainRequest,EventType: These data structures define the types of events and messages that nodes can send and receive.ChainResponseandLocalChainRequestare for responding to chain requests and requesting the local chain state.
#[derive(Debug, Serialize, Deserialize)]
pub struct ChainResponse {
pub blocks: Vec<Block>,
pub receiver: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LocalChainRequest {
pub from_peer_id: String,
}
pub enum EventType {
LocalChainResponse(ChainResponse),
Input(String),
Init,
}
AppBehaviour
NetworkBehaviourImplementation (AppBehaviour): This struct implements theNetworkBehaviourtrait, combining different behaviours like Floodsub (for pub/sub messaging) and mDNS (for local network peer discovery). It also holds channels for sending responses and initializing events and an instance of theAppstruct which contains the blockchain logic.
#[derive(NetworkBehaviour)]
#[behaviour(out_event="Event")]
pub struct AppBehaviour {
pub floodsub: Floodsub,
pub mdns: Mdns,
#[behaviour(ignore)]
pub response_sender: mpsc::UnboundedSender<ChainResponse>,
#[behaviour(ignore)]
pub init_sender: mpsc::UnboundedSender<bool>,
#[behaviour(ignore)]
pub app: App,
}
#[derive(Debug)]
pub enum Event {
ChainResponse(ChainResponse),
Floodsub(FloodsubEvent),
Mdns(MdnsEvent),
Input(String),
Init,
}
impl From<FloodsubEvent> for Event {
fn from(event: FloodsubEvent) -> Self {
Self::Floodsub(event)
}
}
impl From<MdnsEvent> for Event {
fn from(event: MdnsEvent) -> Self {
Self::Mdns(event)
}
}
impl AppBehaviour {
pub async fn new(
app: App,
response_sender: mpsc::UnboundedSender<ChainResponse>,
init_sender: mpsc::UnboundedSender<bool>,
) -> Self {
let mut behaviour = Self {
app,
floodsub: Floodsub::new(*PEER_ID),
mdns: Mdns::new(Default::default())
.await
.expect("can create mdns"),
response_sender,
init_sender,
};
behaviour.floodsub.subscribe(CHAIN_TOPIC.clone());
behaviour.floodsub.subscribe(BLOCK_TOPIC.clone());
behaviour
}
}
Event Handling
NetworkBehaviourEventProcessforFloodsubEventandMdnsEvent: These implementations define how the application reacts to different network events.- Floodsub Events: Handle incoming blockchain-related messages, such as new blocks, chain responses, or local chain requests. For example, when a new block is received, it is added to the blockchain through
try_add_block. - mDNS Events: Handle discovery of new peers or loss of existing ones in the local network. It updates the peer list in the Floodsub protocol accordingly.
// incoming event handler
impl NetworkBehaviourEventProcess<FloodsubEvent> for AppBehaviour {
fn inject_event(&mut self, event: FloodsubEvent) {
if let FloodsubEvent::Message(msg) = event {
if let Ok(resp) = serde_json::from_slice::<ChainResponse>(&msg.data) {
if resp.receiver == PEER_ID.to_string() {
info!("Response from {}:", msg.source);
resp.blocks.iter().for_each(|r| info!("{:?}", r));
self.app.blocks = self.app.choose_chain(self.app.blocks.clone(), resp.blocks);
}
} else if let Ok(resp) = serde_json::from_slice::<LocalChainRequest>(&msg.data) {
info!("sending local chain to {}", msg.source.to_string());
let peer_id = resp.from_peer_id;
if PEER_ID.to_string() == peer_id {
if let Err(e) = self.response_sender.send(ChainResponse {
blocks: self.app.blocks.clone(),
receiver: msg.source.to_string(),
}) {
error!("error sending response via channel, {}", e);
}
}
} else if let Ok(block) = serde_json::from_slice::<Block>(&msg.data) {
info!("received new block from {}", msg.source.to_string());
self.app.try_add_block(block);
}
}
}
}
impl NetworkBehaviourEventProcess<MdnsEvent> for AppBehaviour {
fn inject_event(&mut self, event: MdnsEvent) {
match event {
MdnsEvent::Discovered(discovered_list) => {
for (peer, _addr) in discovered_list {
self.floodsub.add_node_to_partial_view(peer);
}
}
MdnsEvent::Expired(expired_list) => {
for (peer, _addr) in expired_list {
if !self.mdns.has_node(&peer) {
self.floodsub.remove_node_from_partial_view(&peer);
}
}
}
}
}
}
Utility Functions
get_list_peers: Returns a list of discovered peers in the network.handle_print_peers: Logs the list of peers to the console.handle_print_chain: Logs the local blockchain state, providing a visual representation of the blockchain.handle_create_block: Handles user input to create a new block. It creates a new block with the provided data, updates the local blockchain, and broadcasts the new block to peers using Floodsub.
pub fn get_list_peers(swarm: &Swarm<AppBehaviour>) -> Vec<String> {
info!("Discovered Peers:");
let nodes = swarm.behaviour().mdns.discovered_nodes();
let mut unique_peers = HashSet::new();
for peer in nodes {
unique_peers.insert(peer);
}
unique_peers.iter().map(|p| p.to_string()).collect()
}
pub fn handle_print_peers(swarm: &Swarm<AppBehaviour>) {
let peers = get_list_peers(swarm);
peers.iter().for_each(|p| info!("{}", p));
}
pub fn handle_print_chain(swarm: &Swarm<AppBehaviour>) {
info!("Local Blockchain:");
let pretty_json =
serde_json::to_string_pretty(&swarm.behaviour().app.blocks).expect("can jsonify blocks");
info!("{}", pretty_json);
}
pub fn handle_create_block(cmd: &str, swarm: &mut Swarm<AppBehaviour>) {
if let Some(data) = cmd.strip_prefix("create b") {
let behaviour = swarm.behaviour_mut();
let latest_block = behaviour
.app
.blocks
.last()
.expect("there is at least one block");
let block = Block::new(
latest_block.id + 1,
latest_block.hash.clone(),
data.to_owned(),
);
let json = serde_json::to_string(&block).expect("can jsonify request");
behaviour.app.blocks.push(block);
info!("broadcasting new block");
behaviour
.floodsub
.publish(BLOCK_TOPIC.clone(), json.as_bytes());
}
}
main.rs — Main Loop and Block Mining
Let’s now create a new file named ‘main.rs’ which will take care of the main loop and block mining, the core logic, as below:
Block Structure
The Block struct defines the structure of a block in the blockchain. It includes:
id: A unique identifier for the block.hash: The block's hash.previous_hash: The hash of the previous block in the chain.timestamp: The creation time of the block.data: The data or payload of the block.nonce: A value used during the mining process.
use std::io::{Read, Write};
use chrono::prelude::*;
use libp2p::{
core::upgrade,
futures::StreamExt,
mplex,
noise::{Keypair, NoiseConfig, X25519Spec},
swarm::{Swarm, SwarmBuilder},
tcp::TokioTcpConfig,
Transport,
};
use log::{error, info, warn};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::time::Duration;
use tokio::{
io::{stdin, AsyncBufReadExt, BufReader},
select, spawn,
sync::mpsc,
time::sleep,
};
const DIFFICULTY_PREFIX: &str = "00";
mod blockchain;
pub struct App {
pub blocks: Vec<Block>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Block {
pub id: u64,
pub hash: String,
pub previous_hash: String,
pub timestamp: i64,
pub data: String,
pub nonce: u64,
}


