A multi-client blockchain simulator built in C demonstrating core OS concepts through a realistic blockchain-inspired architecture.
| Concept | Where |
|---|---|
| POSIX Threads | Handler, miner, and mempool manager threads |
| Mutex + Condition Variables | Mempool queue producer-consumer |
| Semaphores | Miner race and wakeup on new transactions |
| Named Pipe (FIFO) / IPC | handler.c → /tmp/bc_tx_pipe → mempool.c |
File Locking (fcntl) |
ledger.dat and users.dat readers-writers |
| Singleton Instance Lock | data/server.lock prevents duplicate servers |
| TCP Socket Programming | Multi-client server, full-duplex text protocol |
select() Non-Blocking I/O |
Client async push notifications (NOTIFY) |
| Proof of Work (PoW) | CPU nonce search before block commit |
| Coinbase Transactions | Miners earn BLOCK_REWARD per block mined |
| Merkle Tree | Transaction integrity + inclusion proofs |
| Role-Based Access Control | guest / user / miner permissions in auth.c |
make # builds both server and client
make clean # removes binaries and object filesRequirements: GCC, POSIX (Linux), pthreads
Terminal 1 — Server:
./server/server <port> <block_size> [difficulty]
# example: port 8891, block fills at 3 transactions, PoW difficulty 2
./server/server 8891 3 2Miners are not pre-started at launch. They join dynamically when a user with role
minerlogs in.
Terminal 2+ — Client(s):
./client/client <server_ip> <port>
# example:
./client/client 127.0.0.1 88911. Register — create account (role: guest / user / miner)
2. Login — authenticate
3. Send coins — transfer to a wallet address (user/miner)
4. View my balance — check wallet balance
5. View blockchain — print all blocks and transactions
6. Verify transaction — confirm a tx is in a block
7. Lookup wallet — get full wallet address by username
8. Verify Merkle proof — inclusion proof for a transaction
9. Logout
M. Mempool stats — pending tx count + active miner (miner only)
F. Faucet — claim 100 testnet coins (user/miner)
0. Exit
Clients ──TCP sockets──► Auth module
│
Client handler threads (1 per connection)
│ │
FIFO (IPC) fcntl RDLCK
│ │
Mempool manager ledger.dat
│
sem_post × N_active_miners (genuine race)
│
┌──────────┼──────────┐
Miner 0 Miner 1 Miner N ← spawned on miner login
(coinbase) (coinbase) (coinbase)
└──────────┼──────────┘
fcntl WRLCK
│
ledger.dat (append-only)
- Join: When a user with
role=minerlogs in, a miner thread is spawned and their wallet is registered to the slot. Mining begins immediately. - Race: When a new transaction enters the mempool,
race_semis posted once per active miner — all wake up and compete viaactive_minermutex. First to acquire the mutex mines next. - Coinbase: Every mined block automatically prepends a
coinbase → miner_wallettransaction worthBLOCK_REWARD = 10coins. - Merkle root: Computed from all transactions (including coinbase) before PoW starts — tampering with any tx invalidates both the Merkle root and the block hash.
- Wait phase: Active miner waits up to 15 s for more txs to fill the block. Mines immediately when full or on timeout.
- Leave: When a miner disconnects,
should_stop=1is set. Their thread exits cleanly; any popped-but-unmined transactions are returned to the mempool.
data/ledger.dat — one block per line (MERKLE format):
<idx>|<prev_hash>|<hash>|<merkle_root>|<ts>|<miner_id>|<nonce>|<tx_count>|<tx1>|...
Each <txN>: tx_id,from_wallet,to_wallet,amount,timestamp
Backward compatible: old blocks without merkle_root are parsed transparently.
data/users.dat — one user per line:
<username>|<password_hash>|<wallet_addr>|<role>
OS project/
├── server/
│ ├── server.c main, socket accept loop, global state init
│ ├── state.h all shared structs, constants, ServerState
│ ├── auth.c/h login, register, role permission checks
│ ├── mempool.c/h circular queue, FIFO reader thread, race_sem post
│ ├── miner.c/h dynamic miner threads, PoW, coinbase, Merkle root
│ ├── ledger.c/h append-only ledger, fcntl locking, Merkle verify
│ ├── handler.c/h client handler thread, VERIFY_MERKLE, miner spawn
│ └── hash.c/h djb2 hashing, compute_merkle_root, build_merkle_proof
├── client/
│ └── client.c TCP client with CLI menu, select() NOTIFY loop
├── data/ created at runtime
│ ├── ledger.dat blockchain ledger (always reset on server start)
│ ├── users.dat registered users
│ └── server.lock singleton process lock
├── Makefile
├── README.md
└── PROJECT_REPORT.md
| Command | Args | Auth |
|---|---|---|
REGISTER |
username password role |
public |
LOGIN |
username password |
public |
SUBMIT |
to_wallet amount |
user / miner |
BALANCE |
— | user / miner |
FAUCET |
— | user / miner |
LOOKUP |
username |
public |
VERIFY |
tx_id |
public |
VERIFY_CHAIN |
— | public |
VERIFY_MERKLE |
tx_id |
public |
VIEW_CHAIN |
— | public |
MEMPOOL_STATS |
— | miner only |
QUIT |
— | public |
# Register a miner user
Register → username: alice password: pw123 role: miner
# Alice logs in → miner thread spawned automatically, starts competing
# Register a regular user
Register → username: bob password: pw123 role: user
# Bob logs in → no miner thread
# Bob claims faucet coins (goes via FIFO → mempool → Alice mines it)
FAUCET → OK tx_id=abc123... coins appear after mining
# Bob sends coins to Alice
LOOKUP alice → Full wallet: 6865108148c207f2d531ce2e1568ca59
SUBMIT 6865108148c207f2d531ce2e1568ca59 20
# Live ticker appears, waiting for Alice's miner to mine the block
# *** TRANSACTION CONFIRMED! *** block=#3 elapsed=4.2s
# Verify Merkle inclusion proof
VERIFY_MERKLE abc123...
# → proof path printed, Result: VALID ✓
# Check Alice's balance (includes coinbase rewards)
BALANCE → balance=30.0000 (10 coinbase + 20 from Bob)