Blockchain 0x400 – Multi node Blockchains
So far, our blockchain runs on a single node — one machine holding all the blocks and transactions. That’s fine for learning, but it completely defeats the purpose of a decentralized ledger. Remember the whole point from our very first article? Alice, Bob, Trudy, and Eve each keep their own copy of the ledger. If only one person has the notebook, we’re back to a centralized system.
A real blockchain like Bitcoin or Ethereum runs on thousands of nodes, each keeping a copy of the ledger and syncing automatically. When Alice mines a block on her node, every other node in the network receives it and updates their chain.
In this tutorial, we’re going to make that happen. We’ll turn our single-machine blockchain into a multi-node distributed system. To achieve this, we need to build:
- A network layer to connect multiple nodes.
- A way to broadcast new blocks and transactions.
- A consensus mechanism to keep everyone in sync when chains diverge.
We’ll keep things simple — nodes will communicate using HTTP (REST). You can evolve this into WebSockets or raw TCP later, but REST gives us a clean, easy-to-understand starting point.
The Node Concept
Before we write any code, let’s think about what each node needs to do:
- Maintain its own copy of the blockchain.
- Listen on a specific port for incoming requests.
- Know about a few peers (other nodes in the network).
- Share blocks and transactions with those peers.
Each node is essentially a small server running its own blockchain instance. When something happens on one node — a new transaction or a mined block — it broadcasts that event to all its peers. Those peers do the same, and the information propagates through the network.
Let’s build it.
Setting Up SparkJava
We’ll use SparkJava — a minimalistic Java HTTP framework that’s perfect for this kind of thing. No bloated Spring Boot, no XML config files. Just simple, clean routes.
Add this dependency to your build.gradle:
dependencies {
implementation ‘com.sparkjava:spark-core:2.9.4’
implementation ‘com.google.code.gson:gson:2.10.1’
}
Spark gives us the HTTP server, and Gson handles JSON serialization so our nodes can exchange data in a standard format.
The NodeServer Class
This is the heart of our networking layer. The NodeServer class wraps a blockchain instance and exposes it over HTTP:
import static spark.Spark.*;
import com.google.gson.Gson;
import java.util.ArrayList;
import java.util.List;
public class NodeServer {
private Chain blockchain;
private List<String> peers = new ArrayList<>();
private Gson gson = new Gson();
private int port;
public NodeServer(int port) {
this.port = port;
this.blockchain = new Chain();
}
public void start() {
port(port);
// Get the full blockchain
get("/chain", (req, res) -> {
res.type("application/json");
return gson.toJson(blockchain);
});
// Submit a new transaction
post("/transaction", (req, res) -> {
// Parse and add transaction to the pool
// In a full implementation, you’d deserialize the transaction here
res.type("application/json");
return "{\"status\": \"Transaction added to pool\"}";
});
// Mine pending transactions
post("/mine", (req, res) -> {
blockchain.minePendingTransactions();
broadcastChain();
res.type("application/json");
return "{\"status\": \"Block mined and broadcast\"}";
});
// Receive a chain from a peer
post("/chain/update", (req, res) -> {
// Handle incoming chain from peers
res.type("application/json");
return "{\"status\": \"Chain received\"}";
});
// Register a new peer
post("/peers", (req, res) -> {
String peerUrl = req.body();
if (!peers.contains(peerUrl)) {
peers.add(peerUrl);
}
res.type("application/json");
return "{\"status\": \"Peer registered\"}";
});
// List all known peers
get("/peers", (req, res) -> {
res.type("application/json");
return gson.toJson(peers);
});
System.out.println("Node started on port " + port);
}
}
What’s going on here? Each node exposes a handful of REST endpoints:
GET /chain— returns the full blockchain as JSON. This is how other nodes can fetch your chain.POST /transaction— accepts a new transaction and adds it to the pool.POST /mine— mines all pending transactions into a new block, then broadcasts the updated chain to all peers.POST /chain/update— receives a chain from a peer (used during synchronization).GET /peersandPOST /peers— manage the list of known peers.
The key method here is broadcastChain() — after mining a block, the node tells all its peers about the new chain.
Broadcasting to Peers
When a node mines a new block, it needs to tell everyone else. Here’s how we broadcast:
private void broadcastChain() {
for (String peer : peers) {
try {
HttpClient client = HttpClient.newHttpClient();
String chainJson = gson.toJson(blockchain);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(peer + "/chain/update"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(chainJson))
.build();
client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Broadcast to peer: " + peer);
} catch (Exception e) {
System.out.println("Failed to reach peer: " + peer);
}
}
}
Simple enough — we loop through all known peers and send them our full chain via an HTTP POST. If a peer is down, we catch the exception and move on. No crashing, no blocking.
In a production blockchain, you wouldn’t send the entire chain every time — you’d just broadcast the new block. But for our purposes, sending the full chain keeps things simple and sets us up nicely for the consensus mechanism.
Consensus — The Longest Chain Rule
Here’s where it gets interesting. What happens when two nodes mine a block at roughly the same time? Now you have two different versions of the chain floating around the network. Which one is correct?
This is the fork problem, and blockchain solves it with a simple rule: the longest valid chain wins.
The logic is straightforward — the longest chain represents the most computational work. If someone has a longer chain than yours, and it’s valid, you replace your chain with theirs.
Let’s add a resolveConflicts() method to our Chain class:
public boolean replaceChain(ArrayList<Block> newChain) {
if (newChain.size() > blockchain.size() && isValidChain(newChain)) {
System.out.println("Replacing chain with longer valid chain.");
blockchain = newChain;
return true;
}
return false;
}
private boolean isValidChain(ArrayList<Block> chain) {
for (int i = 1; i < chain.size(); i++) {
Block currentBlock = chain.get(i);
Block previousBlock = chain.get(i - 1);
if (!currentBlock.getHash().equals(currentBlock.calculateHash())) {
return false;
}
if (!currentBlock.getPrevHash().equals(previousBlock.getHash())) {
return false;
}
}
return true;
}
Two checks happen before we accept a new chain:
- It must be longer than our current chain — if it’s the same length or shorter, we ignore it.
- It must be valid — every block’s hash must check out, and every link between blocks must be intact. We’re not going to blindly trust data from the network.
If both conditions are met, we swap out our chain. Otherwise, we keep ours. This is exactly how Bitcoin handles forks — eventually, one branch outpaces the other, and the entire network converges on a single truth.
Now we wire this into the /chain/update endpoint:
post("/chain/update", (req, res) -> {
// Deserialize the incoming chain
ArrayList<Block> receivedChain = deserializeChain(req.body());
if (blockchain.replaceChain(receivedChain)) {
res.type("application/json");
return "{\"status\": \"Chain replaced\"}";
} else {
res.type("application/json");
return "{\"status\": \"Chain kept — ours is longer or received chain is invalid\"}";
}
});
Running Multiple Nodes
Time to see it all in action. Let’s update our Main class to spin up multiple nodes:
public class Main {
public static void main(String[] args) {
int port = Integer.parseInt(args[0]);
NodeServer node = new NodeServer(port);
node.start();
}
}
Now you can launch multiple nodes in separate terminal windows:
# Terminal 1 — Node on port 4000
./gradlew run --args="4000"
# Terminal 2 — Node on port 4001
./gradlew run --args="4001"
# Terminal 3 — Node on port 4002
./gradlew run --args="4002"
Three nodes, each running their own blockchain. Now let’s connect them. Register peers using curl:
# Tell node 4000 about node 4001
curl -X POST http://localhost:4000/peers -d "http://localhost:4001"
# Tell node 4000 about node 4002
curl -X POST http://localhost:4000/peers -d "http://localhost:4002"
# Tell node 4001 about node 4000
curl -X POST http://localhost:4001/peers -d "http://localhost:4000"
Now when node 4000 mines a block, it broadcasts the chain to nodes 4001 and 4002. They validate it, and if it’s longer than theirs, they adopt it. The network stays in sync.
# Mine on node 4000
curl -X POST http://localhost:4000/mine
# Check chain on node 4001 — it should have the new block
curl http://localhost:4001/chain
That’s a distributed blockchain running on your local machine. Three independent nodes, communicating over HTTP, reaching consensus through the longest chain rule.
What’s Really Happening Under the Hood
Let’s trace through a full cycle to make sure the concept is clear:
- Alice submits a transaction to Node 4000.
- The transaction enters Node 4000’s transaction pool.
- Someone triggers mining on Node 4000 —
POST /mine. - Node 4000 mines a new block containing all pending transactions.
- Node 4000 broadcasts its updated chain to peers (4001 and 4002).
- Nodes 4001 and 4002 receive the chain, validate it, and — since it’s longer — replace their chains.
- All three nodes now have identical ledgers.
If Node 4001 also mined a block at the same time, creating a fork, the longest chain rule kicks in. Whichever chain grows faster (gets the next block first) wins, and the other nodes converge on it.
Wrapping Up
Look at how far we’ve come. We started with a simple linked list of blocks, and now we have:
- Blocks with Proof of Work mining
- Transactions with digital signatures
- Wallets with public-private key pairs
- Multiple nodes communicating over HTTP
- Consensus via the longest chain rule
Our blockchain is no longer a toy running on one machine. It’s a distributed system where multiple independent nodes maintain the same truth — without trusting each other.
The full source code is available on GitHub: distributed-blockchain (branch 0x400)
There’s still a lot we could improve — peer discovery (so nodes don’t need manual registration), persistent storage (right now everything lives in memory), balance checking (preventing overdrafts), and mining rewards. But the foundation is solid. Every major concept that powers Bitcoin and Ethereum — hashing, mining, transactions, signatures, distributed consensus — we’ve built from scratch.
Not bad for four friends who just wanted to send money to each other.