Web3 represents a paradigm shift in how we interact with the internet—moving from centralized platforms to decentralized, trustless systems powered by blockchain technology. At the heart of this transformation lies Ethereum, a platform that enables smart contracts and decentralized applications (dApps). To truly grasp Web3 fundamentals, there's no better exercise than building your own private Ethereum blockchain.
In this hands-on guide, you'll learn how to set up a private Proof-of-Authority (PoA) Ethereum network using Geth and puppeth, deploy a voting dApp with Truffle, and interact with it via a simple frontend interface—all while gaining practical insight into core Web3 concepts like consensus mechanisms, account management, smart contract deployment, and node communication.
Setting Up Geth for Local Blockchain Development
The first step in exploring Ethereum-based Web3 development is installing Geth, the official Go implementation of the Ethereum protocol. Geth allows you to run a full Ethereum node, interact with the network, mine blocks, and deploy smart contracts.
To install Geth on macOS using Homebrew:
brew tap ethereum/ethereum
brew install ethereumVerify the installation:
geth versionOnce installed, you're ready to create your own private blockchain—perfect for testing dApps without incurring gas fees or exposing code to public networks.
👉 Discover how blockchain developers test dApps securely before launch.
Creating a Proof-of-Authority (PoA) Private Network
For development and enterprise use cases, Proof-of-Authority (PoA) is ideal because it offers fast transaction finality and low overhead. Unlike Proof-of-Work, PoA relies on approved validators (authorities) to produce blocks.
Step 1: Initialize Node Directories
Create two separate directories for two nodes:
mkdir private-chain-node1 private-chain-node2Step 2: Generate Ethereum Accounts
Each node needs an account. Create them using:
geth --datadir private-chain-node1 account new
geth --datadir private-chain-node2 account newRecord the generated addresses—they’ll be used to define sealers (block producers) and pre-funded accounts.
Step 3: Configure Genesis Block with Puppeth
Use puppeth to generate a custom genesis configuration:
puppethFollow the prompts:
- Choose "Configure new genesis"
- Select Clique (Proof-of-Authority)
- Set block time to 15 seconds
- Add both account addresses as authorized sealers
- Pre-fund the same accounts with ETH
- Set a custom chain ID (e.g.,
123456) - Export the configuration as
genesis.json
Step 4: Initialize Nodes with Genesis File
Apply the genesis block to both nodes:
geth --datadir private-chain-node1 init genesis.json
geth --datadir private-chain-node2 init genesis.jsonStep 5: Launch Both Nodes
Start each node with unique ports:
Node 1:
geth --datadir ./ --networkid 123456 --port 8000 --rpc.allow-unprotected-txs --http --http.crossdomain="*" --allow-insecure-unlock --nodiscover consoleNode 2:
geth --datadir ./ --networkid 123456 --port 8001 --http.port=8547 --authrpc.port=8546 --rpc.allow-unprotected-txs --http --http.crossdomain="*" --allow-insecure-unlock --nodiscover consoleNote: The --http flag enables JSON-RPC access, crucial for dApp interaction.Step 6: Connect Nodes Peering
Retrieve Node 1’s enode URL:
admin.nodeInfo.enodeOn Node 2, add Node 1 as a peer:
admin.addPeer("enode://<node1-enode-url>@127.0.0.1:8000?discport=0")Confirm connection:
admin.peersYou now have a two-node PoA blockchain—laying the foundation for secure, scalable Web3 application testing.
Deploying a Voting Smart Contract Using Truffle
With the blockchain running, let’s deploy a simple voting dApp using Truffle Suite, a popular development framework for Ethereum.
Install Truffle
yarn global add truffleCreate Project
mkdir vote_dapp && cd vote_dapp
truffle unbox webpackWrite the Smart Contract (contracts/Vote.sol)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Voting {
mapping(bytes32 => uint8) public votesReceived;
bytes32[] public candidateList;
constructor(bytes32[] memory candidateNames) {
candidateList = candidateNames;
}
function totalVotesFor(bytes32 candidate) public view returns (uint8) {
require(validCandidate(candidate));
return votesReceived[candidate];
}
function voteForCandidate(bytes32 candidate) public {
require(validCandidate(candidate));
votesReceived[candidate] += 1;
}
function validCandidate(bytes32 candidate) public view returns (bool) {
for (uint i = 0; i < candidateList.length; i++) {
if (candidateList[i] == candidate) {
return true;
}
}
return false;
}
}Configure Deployment Script (migrations/2_deploy_contract.js)
var Voting = artifacts.require("Voting");
const web3 = require("web3");
module.exports = function(deployer) {
deployer.deploy(Voting, [
web3.utils.asciiToHex('Rama'),
web3.utils.asciiToHex('Nick'),
web3.utils.asciiToHex('Jose')
]);
};Update truffle-config.js
Add development network settings:
development: {
host: "127.0.0.1",
port: 8545,
network_id: "123456"
}Set Solidity compiler version:
compilers: {
solc: {
version: "0.8.0"
}
}Deploy the contract:
truffle migrate --network development👉 See how real-world dApps are deployed and audited on Ethereum.
Building a Frontend Interface for User Interaction
Now that the contract is live, build a simple HTML/JS frontend to interact with it.
Include Web3.js and connect to your local node:
<script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>Initialize Web3 provider:
window.web3 = new Web3(new Web3.providers.HttpProvider("http://127.0.0.1:8545"));Load the deployed contract and enable voting:
Voting.setProvider(web3.currentProvider);
window.voteForCandidate = async function() {
const candidateName = document.getElementById("candidate").value;
const accounts = await web3.eth.getAccounts();
Voting.deployed().then(async function(contractInstance) {
contractInstance.voteForCandidate(web3.utils.asciiToHex(candidateName), { from: accounts[0] });
// Update vote count after transaction
const voteCount = await contractInstance.totalVotesFor(web3.utils.asciiToHex(candidateName));
document.getElementById(candidates[candidateName]).textContent = voteCount.toString();
});
};This creates a functional dApp where users can cast votes recorded immutably on your private chain.
Frequently Asked Questions (FAQ)
Q: Why use Proof-of-Authority instead of Proof-of-Work?
A: PoA is faster, energy-efficient, and ideal for private or test networks where trust among participants exists. It’s widely used in enterprise blockchain solutions.
Q: Can I connect MetaMask to my private chain?
A: Yes! Add a custom RPC network in MetaMask with Chain ID 123456, RPC URL http://127.0.0.1:8545, then import accounts using their private keys.
Q: What is the purpose of the genesis.json file?
A: It defines the initial state of the blockchain, including consensus rules, block time, pre-funded accounts, and chain ID—essential for network consistency.
Q: How do nodes discover each other in a private network?
A: Since --nodiscover disables automatic discovery, you must manually connect nodes using admin.addPeer() with enode URLs.
Q: Is it safe to use --allow-insecure-unlock in production?
A: No. This flag is only for local development. In production, use secure key management tools like Hashicorp Vault or hardware wallets.
Q: How can I prevent duplicate voting?
A: Extend the contract to track voter addresses using a mapping(address => bool) to ensure one vote per account.
Conclusion
By building a private Ethereum blockchain and deploying a working dApp, you've taken a significant step toward mastering Web3 development. You now understand key components such as node setup, consensus mechanisms, smart contract lifecycle, and frontend integration—all essential skills for modern decentralized application engineers.
As Web3 continues to evolve, hands-on experience like this becomes increasingly valuable. Whether you're exploring decentralized identity, tokenomics, or layer-2 scaling solutions, starting with a private chain offers a safe sandbox to innovate.
👉 Start experimenting with blockchain tools used by top developers worldwide.