Tamper-proof, fault-tolerant distributed ledger for file-access audit logging
5-node consensus · Raft-inspired leader election · SHA-256 hash chain · Merkle-tree integrity · RSA-SHA256 signatures
- 5-Node Cluster — Fault-tolerant distributed system
- Majority-Quorum Consensus — Propose Vote Commit over gRPC
- Tamper-Evident by Construction — SHA-256 hash chain + Merkle tree
- RSA-SHA256 Signatures — Cryptographically signed audit records
- Automatic Block Recovery — Lagging nodes self-heal in background
- Raft-Inspired Election — Term-based bully election, majority vote
- Gossip Propagation — Peer-to-peer audit distribution
- ~3,400 Lines C++17 — Thread-safe, RAII concurrency
No record can be altered or forged undetected. Altering any audit changes its hash changes the Merkle root changes the block hash breaks the chain and every peer independently verifies before committing.
flowchart TD
C["Python Client\naudit_client.py\n(RSA-signed audit)"]
subgraph CLUSTER["gRPC Cluster 5 Nodes"]
direction LR
L["Node 1 Leader\n\nheartbeat every 10s\npropose · vote · commit\nmempool management"]
N2["Node 2"]
N3["Node 3"]
N4["Node 4"]
N5["Node 5"]
end
DISK[("Disk\nblock storage")]
C -- "SubmitAudit RPC\n(verify RSA sig)" --> L
L -- "WhisperAuditRequest\ngossip to peers" --> N2 & N3 & N4 & N5
L -- "ProposeBlock\nasync fan-out" --> N2 & N3 & N4 & N5
N2 & N3 & N4 & N5 -- "vote true/false" --> L
L -- "CommitBlock\nbroadcast on majority" --> N2 & N3 & N4 & N5
N2 & N3 & N4 & N5 -- "GetBlock RPC\n(recovery)" --> L
L --> DISK
N2 --> DISK
| Component | File | Responsibility |
|---|---|---|
| Node | src/node.cc |
Leader election, heartbeat loop, block proposal loop, recovery loop |
| BlockManager | src/block_manager.cc |
Create / verify / commit blocks, Merkle root, disk persistence |
| BlockchainService | src/blockchain_service.cc |
gRPC handlers whisper, propose, vote, commit, get block |
| FileAuditService | src/file_audit_service.cc |
gRPC handler submit audit, validate fields, verify RSA signature |
| Mempool | src/mempool.cc |
Thread-safe pending audit store, content-hash dedup, sorted by timestamp |
| CryptoUtils | src/crypto_utils.cc |
SHA-256, Merkle tree, RSA-SHA256 verify (OpenSSL EVP) |
| Python Client | client/audit_client.py |
Generate RSA keypair, sign audits, submit over gRPC |
Heartbeat every 10s 3 missed heartbeats start election
Candidate fans out TriggerElection via std::async (concurrent)
Vote criteria: highest latest_block_id
largest mempool (tie-break)
largest address (final tie-break)
Winner broadcasts NotifyLeadership to all peers
Client signs audit (RSA-SHA256, PKCS1v15)
SubmitAudit RPC validate fields + verify OpenSSL signature
add to thread-safe mempool (dedup by reqId + content hash)
WhisperAuditRequest to all peers (P2P gossip)
Leader (mempool 5 audits):
1. createBlock Merkle root + SHA-256 hash chain
2. ProposeBlock async fan-out, await majority vote
3. Peers verify: id sequence · prev_hash chain · Merkle root · block hash
4. commitBlock persist to disk
5. CommitBlock broadcast to all peers
6. Remove committed audits from mempool
blockHash = SHA-256( id : previous_hash : merkle_root )
merkle_root = pairwise SHA-256 over all audit hashes (odd node duplicated)
| What changes | What breaks |
|---|---|
| Any single audit field | Its hash Merkle root block hash chain |
| Any block hash | Every subsequent block's previous_hash |
| Signature on an audit | Signature verification in FileAuditService |
Background loop (every 30s):
Compare own latest_block_id with peers (from heartbeat metadata)
If behind GetBlock RPC from most-advanced peer
Verify + commit each missing block remove audits from mempool
service FileAuditService {
rpc SubmitAudit (FileAudit) returns (FileAuditResponse);
}
service BlockChainService {
rpc WhisperAuditRequest (FileAudit) returns (WhisperResponse);
rpc ProposeBlock (Block) returns (BlockVoteResponse);
rpc CommitBlock (Block) returns (BlockCommitResponse);
rpc GetBlock (GetBlockRequest) returns (GetBlockResponse);
rpc SendHeartbeat (HeartbeatRequest) returns (HeartbeatResponse);
rpc TriggerElection (TriggerElectionRequest) returns (TriggerElectionResponse);
rpc NotifyLeadership (NotifyLeadershipRequest) returns (NotifyLeadershipResponse);
}| Mechanism | Where | Why |
|---|---|---|
std::lock_guard (RAII) |
All shared state | Exception-safe, no manual unlock |
verifyBlockNoLock |
Commit path | Prevents re-entrant deadlock inside mutex |
std::async + futures |
Vote / commit fan-out | Non-blocking parallel peer calls |
3 background std::threads |
Heartbeat · Proposal · Recovery | Clean lifecycle separation |
# Required
CMake 3.14+ | C++17 compiler | gRPC + Protocol Buffers
OpenSSL | yaml-cpp | nlohmann/json | Abseil./build.sh./start_nodes.sh
# Nodes start on ports 5005150055
# Logs: node1.log node5.logcd client
pip install grpcio grpcio-tools cryptography
python generate_proto.py
python audit_client.py \
--server localhost:50051 \
--file-id "file123" --file-name "report.pdf" \
--user-id "user456" --user-name "John Doe" \
--access-type 1 --generate-keystail -f node1.log | grep -E "CONSENSUS|COMMIT|VERIFY|ELECT"./stop_nodes.sh| Decision | Why | Known Limitation |
|---|---|---|
| Raft-inspired (not full Raft) | Simpler; sufficient for append-only audit log | No term/vote persistence across restarts |
| Majority quorum | Tolerates up to (n1)/2 failures | Requires >half nodes live to commit |
| Gossip ("whisper") propagation | Decentralized audit ingestion, no single bottleneck | Eventual consistency in mempool across peers |
| Hash chain + Merkle | O(log n) audit proof, tamper-evident at every layer | Merkle proof not exposed via API |
verifyBlockNoLock pattern |
Allows verification inside the commit mutex path | Caller must hold lock not enforced by type system |
Built with C++17 · gRPC · OpenSSL · Protocol Buffers
Distributed Systems · Consensus Protocols · Cryptographic Integrity · Fault Tolerance