A comprehensive, step-by-step tutorial that explains blockchain concepts as you build your first blockchain with ChainForge.
In this tutorial, you'll:
- Understand what blockchains are and how they work
- Create a blockchain instance
- Mine blocks with Proof of Work
- Explore chain integrity and immutability
- Experiment with different mining difficulties
- Learn how tampering is detected
Time Required: 15-20 minutes
Prerequisites:
- ChainForge installed (Installation Guide)
- Basic understanding of HTTP requests
curlor API client (Postman, Insomnia)
A blockchain is a distributed ledger - a digital record of transactions organized into blocks and linked together in a chain. Think of it as a digital notebook where:
- Each page is a "block"
- Pages are numbered sequentially (index)
- Each page references the previous page's fingerprint (hash)
- You can only add pages, never remove or modify them
- Everyone can verify the pages haven't been tampered with
Block: A container for data with metadata
index: Position in the chain (0, 1, 2, ...)data: Information stored in the blocktimestamp: When the block was createdhash: Cryptographic fingerprint of the blockprevious_hash: Link to the previous block's hashnonce: Number used to meet Proof of Work requirementdifficulty: Mining difficulty (how many leading zeros)
Hash: A cryptographic fingerprint (SHA256)
- Fixed length (64 characters)
- Unique for each input
- Changing even 1 character completely changes the hash
- Impossible to reverse (can't get data from hash)
Proof of Work (PoW): Mining algorithm that requires computational work
- Must find a nonce that produces a valid hash
- Valid hash starts with required number of zeros
- Higher difficulty = more zeros = more attempts = more secure
Chain Integrity: Validation that ensures data hasn't been tampered with
- Each block links to the previous block
- Changing any data invalidates the hash
- Invalid hash breaks the chain
# Using Docker
docker-compose up
# Or local installation
ruby main.rb -p 1910Verify it's running:
curl http://localhost:1910
# Output: Hello to ChainForge!curl -X POST http://localhost:1910/api/v1/chainResponse:
{
"id": "674c8a1b2e4f5a0012345678"
}What just happened?
- ChainForge created a new MongoDB collection for this blockchain
- Generated a genesis block (the first block):
{ "index": 0, "data": "Genesis Block", "hash": "calculated_hash_here", "previous_hash": null, "nonce": 0, "difficulty": 0, "timestamp": 1699564821 } - Genesis blocks are special:
- Always index 0
- No previous_hash (they're first)
- NOT mined (no Proof of Work requirement)
- Auto-created when blockchain is instantiated
Save the blockchain ID for subsequent requests!
Let's start with difficulty 1 (easy mining):
curl -X POST http://localhost:1910/api/v1/chain/674c8a1b2e4f5a0012345678/block \
-H 'Content-Type: application/json' \
-d '{"data": "My first transaction", "difficulty": 1}'Response:
{
"chain_id": "674c8a1b2e4f5a0012345678",
"block_id": "674c8b2c3e5f6a0012345679",
"block_hash": "0a1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef012345678",
"nonce": 7,
"difficulty": 1
}Mining process breakdown:
The system performed these steps:
-
Validation: Checked that
datais provided anddifficultyis 1-10 ✓ -
Block Creation: Created a new block with:
- index: 1 (next available)
- data: "My first transaction"
- previous_hash: (genesis block's hash)
- difficulty: 1
- nonce: 0 (starting point)
-
Mining Algorithm (Proof of Work):
Target: Hash must start with "0" (one leading zero) Attempt 1: nonce=0 hash = SHA256("1" + timestamp + "My first transaction" + previous_hash + "0") hash = "a1b2c3d4..." ✗ (doesn't start with "0") Attempt 2: nonce=1 hash = SHA256("1" + timestamp + "My first transaction" + previous_hash + "1") hash = "9f8e7d6c..." ✗ (doesn't start with "0") ... Attempt 7: nonce=7 hash = SHA256("1" + timestamp + "My first transaction" + previous_hash + "7") hash = "0a1b2c3d..." ✓ (starts with "0"!) -
Block Saved: Block stored in MongoDB with nonce=7
Why different nonce values each time? The timestamp changes every second, so even the same data produces different hashes!
curl http://localhost:1910/api/v1/chain/674c8a1b2e4f5a0012345678/block/674c8b2c3e5f6a0012345679Response:
{
"chain_id": "674c8a1b2e4f5a0012345678",
"block": {
"id": "674c8b2c3e5f6a0012345679",
"index": 1,
"data": "My first transaction",
"hash": "0a1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef012345678",
"previous_hash": "genesis_block_hash_here",
"nonce": 7,
"difficulty": 1,
"timestamp": 1699564821,
"valid_hash": true
}
}Field explanations:
index: 1- Second block in the chain (0 = genesis)hash: 0a1b2...- Starts with "0" (meets difficulty 1)previous_hash: genesis_hash- Links to genesis blocknonce: 7- Took 7 attempts to find valid hashvalid_hash: true- Hash meets difficulty requirement
Now let's try difficulty 2 (requires "00"):
curl -X POST http://localhost:1910/api/v1/chain/674c8a1b2e4f5a0012345678/block \
-H 'Content-Type: application/json' \
-d '{"data": "Second block with higher difficulty", "difficulty": 2}'Response:
{
"chain_id": "674c8a1b2e4f5a0012345678",
"block_id": "674c8c3d4e6f7a001234567a",
"block_hash": "00abc123def456789abcdef0123456789abcdef0123456789abcdef012345678",
"nonce": 142,
"difficulty": 2
}Notice:
- Hash starts with "00" (2 leading zeros)
- Nonce: 142 (many more attempts than difficulty 1)
- Mining took longer (~1-2 seconds)
Why exponentially harder?
| Difficulty | Target Pattern | Probability per Attempt | Avg Attempts |
|---|---|---|---|
| 1 | 0* | 1/16 (6.25%) | ~16 |
| 2 | 00* | 1/256 (0.39%) | ~256 |
| 3 | 000* | 1/4096 (0.024%) | ~4096 |
| 4 | 0000* | 1/65536 (0.0015%) | ~65536 |
Each additional zero multiplies attempts by 16!
Let's push it further:
curl -X POST http://localhost:1910/api/v1/chain/674c8a1b2e4f5a0012345678/block \
-H 'Content-Type: application/json' \
-d '{"data": "Third block - even harder", "difficulty": 3}'This will take several seconds! Watch the process:
{
"chain_id": "674c8a1b2e4f5a0012345678",
"block_id": "674c8d4e5f7g8a001234567b",
"block_hash": "000def123abc456789abcdef0123456789abcdef0123456789abcdef012345678",
"nonce": 3847,
"difficulty": 3
}Observations:
- Hash: "000..." (3 leading zeros)
- Nonce: 3847 (thousands of attempts!)
- Time: Several seconds of CPU work
Bitcoin comparison:
- Bitcoin uses ~19 leading zeros (as of 2023)
- Requires specialized hardware (ASICs)
- Difficulty adjusts every 2 weeks to maintain ~10 min block time
- ChainForge difficulty is fixed (educational simplification)
Your blockchain now looks like this:
Blockchain: 674c8a1b2e4f5a0012345678
Block 0 (Genesis)
├─ index: 0
├─ hash: abc123...
├─ previous_hash: null
└─ nonce: 0 (not mined)
│
↓ (previous_hash link)
│
Block 1
├─ index: 1
├─ hash: 0a1b2c... (difficulty 1)
├─ previous_hash: abc123... ← links to Block 0
└─ nonce: 7
│
↓ (previous_hash link)
│
Block 2
├─ index: 2
├─ hash: 00abc1... (difficulty 2)
├─ previous_hash: 0a1b2c... ← links to Block 1
└─ nonce: 142
│
↓ (previous_hash link)
│
Block 3
├─ index: 3
├─ hash: 000def... (difficulty 3)
├─ previous_hash: 00abc1... ← links to Block 2
└─ nonce: 3847
Verify Block 1's data is unchanged:
curl -X POST http://localhost:1910/api/v1/chain/674c8a1b2e4f5a0012345678/block/674c8b2c3e5f6a0012345679/valid \
-H 'Content-Type: application/json' \
-d '{"data": "My first transaction"}'Response:
{
"chain_id": "674c8a1b2e4f5a0012345678",
"block_id": "674c8b2c3e5f6a0012345679",
"valid": true
}Validation process:
- Retrieve block from database
- Recalculate hash using provided data:
SHA256(1 + timestamp + "My first transaction" + previous_hash + 7) - Compare calculated hash with stored hash
- If match:
valid: true✓
Now try with wrong data to see tampering detection:
curl -X POST http://localhost:1910/api/v1/chain/674c8a1b2e4f5a0012345678/block/674c8b2c3e5f6a0012345679/valid \
-H 'Content-Type: application/json' \
-d '{"data": "Tampered data!"}'Response:
{
"chain_id": "674c8a1b2e4f5a0012345678",
"block_id": "674c8b2c3e5f6a0012345679",
"valid": false
}Why invalid?
- Recalculates hash:
SHA256(1 + timestamp + "Tampered data!" + previous_hash + 7) - Calculated hash:
9xyz...(completely different!) - Stored hash:
0a1b2c... - Hashes don't match →
valid: false✗
Key insight: Changing even one character produces a completely different hash!
Scenario: An attacker wants to change Block 1's data from "My first transaction" to "Fraudulent transaction"
What would need to happen:
-
Change Block 1 data:
Old: "My first transaction" → hash: 0a1b2c... New: "Fraudulent transaction" → hash: 9xyz... ✗Problem: Hash no longer starts with "0" (doesn't meet difficulty 1)
-
Re-mine Block 1: Find new nonce to get valid hash starting with "0"
After re-mining: hash: 0def456... -
Block 2 is now invalid:
Block 2: ├─ previous_hash: 0a1b2c... (old Block 1 hash) └─ Stored hash: 00abc1... Problem: previous_hash doesn't match Block 1's new hash (0def456) -
Re-mine Block 2: Update previous_hash and re-mine
After re-mining: hash: 00ghi789... -
Block 3 is now invalid: Same problem - previous_hash doesn't match
-
Re-mine ALL subsequent blocks: Must re-mine Block 3, Block 4, Block 5, ... (entire chain!)
Computational Security:
- Each block took time to mine (PoW)
- Attacker must re-mine faster than network adds new blocks
- In real blockchains, this requires >51% of network computing power
- Practically impossible for established blockchains like Bitcoin
ChainForge validates chain integrity before adding new blocks:
# Pseudocode from src/blockchain.rb
def valid?
blocks.each_with_index do |block, index|
# Check 1: Hash matches calculated hash
return false unless block.hash == block.calculate_hash
# Check 2: Hash meets difficulty requirement
return false unless block.valid_hash?
# Check 3: Links to previous block (skip genesis)
if index > 0
return false unless block.previous_hash == blocks[index - 1].hash
end
end
true
endIf validation fails, ChainForge rejects new blocks until integrity is restored.
Omit the difficulty parameter to use default (from DEFAULT_DIFFICULTY env var):
curl -X POST http://localhost:1910/api/v1/chain/674c8a1b2e4f5a0012345678/block \
-H 'Content-Type: application/json' \
-d '{"data": "Using default difficulty"}'Response:
{
"chain_id": "674c8a1b2e4f5a0012345678",
"block_id": "674c8e5f6g8h9a001234567c",
"block_hash": "00abc...",
"nonce": 234,
"difficulty": 2
}Default difficulty is 2 (set in .env file).
Create multiple blockchains to see they're independent:
# Create first blockchain
curl -X POST http://localhost:1910/api/v1/chain
# Response: {"id": "blockchain_1_id"}
# Create second blockchain
curl -X POST http://localhost:1910/api/v1/chain
# Response: {"id": "blockchain_2_id"}
# Add block to first blockchain
curl -X POST http://localhost:1910/api/v1/chain/blockchain_1_id/block \
-H 'Content-Type: application/json' \
-d '{"data": "Blockchain 1 data"}'
# Add block to second blockchain
curl -X POST http://localhost:1910/api/v1/chain/blockchain_2_id/block \
-H 'Content-Type: application/json' \
-d '{"data": "Blockchain 2 data"}'Each blockchain has its own genesis block and independent chain!
Try exceeding rate limits to see protection in action:
# Create many blockchains rapidly (limit: 10/minute)
for i in {1..15}; do
curl -X POST http://localhost:1910/api/v1/chain
doneResponse (after 10 requests):
{
"error": "Rate limit exceeded. Please try again later."
}HTTP Status: 429 (Too Many Requests)
Try invalid inputs:
Missing data:
curl -X POST http://localhost:1910/api/v1/chain/674c8a1b2e4f5a0012345678/block \
-H 'Content-Type: application/json' \
-d '{"difficulty": 2}'Response:
{
"errors": {
"data": ["must be filled"]
}
}HTTP Status: 400 (Bad Request)
Invalid difficulty:
curl -X POST http://localhost:1910/api/v1/chain/674c8a1b2e4f5a0012345678/block \
-H 'Content-Type: application/json' \
-d '{"data": "test", "difficulty": 15}'Response:
{
"errors": {
"difficulty": ["must be between 1 and 10"]
}
}✅ Blockchain Basics:
- Blocks contain data and link via hashes
- Genesis blocks start each chain
- Chains are append-only (immutable)
✅ Proof of Work:
- Mining finds nonce producing valid hash
- Difficulty determines leading zeros required
- Higher difficulty = exponentially more work
✅ Chain Integrity:
- Hash chaining links blocks together
- Tampering invalidates hashes
- Breaking chain requires re-mining all subsequent blocks
✅ Security Features:
- Rate limiting prevents abuse
- Input validation prevents malformed data
- PoW makes tampering computationally expensive
- Immutability through computation: PoW makes changing history expensive
- Hash chaining: Each block depends on all previous blocks
- Verifiability: Anyone can validate block data
- Decentralization (in real blockchains): No single point of control
Explore Further:
- API Reference - Complete endpoint documentation
- Proof of Work Deep Dive - Mining algorithm details
- Architecture Overview - System design
- API Examples - Code examples in multiple languages
Build Something:
- Create a simple blockchain explorer web app
- Implement a transaction system on top of ChainForge
- Write scripts to automate blockchain creation
- Build a visualization tool for the blockchain
Learn More:
- Study Bitcoin's whitepaper (Satoshi Nakamoto, 2008)
- Research Ethereum's smart contracts
- Explore consensus mechanisms (PoW, PoS, etc.)
- Learn about Merkle trees and their role in blockchains
Q: Mining is taking forever! A: Lower the difficulty (1-3 for development). Difficulty 5+ can take minutes.
Q: I get "Blockchain not found" errors A: Double-check your blockchain ID. Each blockchain has a unique ID.
Q: Rate limit errors A: Wait 1 minute for the rate limit window to reset.
Q: MongoDB connection errors
A: Ensure MongoDB is running: mongosh --eval "db.version()"
For more help, see Troubleshooting Guide.
Congratulations! You've completed the comprehensive blockchain tutorial. You now understand how blockchains work and how ChainForge implements these concepts!