Skip to content

Commit 2151290

Browse files
release(v2): finalize permissioned private blockchain system with documentation, SDLC, and cleanup
- standardized versioning to v2 - restructured README with system evolution and SDLC - removed outdated authentication (username/password) - implemented OTP-based access model in documentation - linked prototype (BlockCred) and verification client - cleaned redundant documentation - verified Docker and CI/CD stability Primary implementation and integration by Uday (project lead) System finalized for submission, demo, and evaluation
2 parents b6fa175 + 1445516 commit 2151290

21 files changed

Lines changed: 833 additions & 4237 deletions

.github/workflows/ci.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ jobs:
3434
pip install -r requirements.txt
3535
3636
- name: Run tests
37+
env:
38+
INITIAL_ADMIN_PASSWORD: testadmin123
39+
SESSION_SECRET: ci-session-secret
3740
run: |
3841
python -m pytest tests/ -v --tb=short
3942
@@ -58,8 +61,14 @@ jobs:
5861
docker run -d -p 5000:5000 --name test-app \
5962
-e DATABASE_URL=sqlite:////tmp/credify.db \
6063
-e INITIAL_ADMIN_PASSWORD=testadmin123 \
64+
-e SESSION_SECRET=ci-session-secret \
6165
credify:${{ github.sha }}
62-
sleep 15
66+
for i in {1..30}; do
67+
if curl -fsS http://localhost:5000/ > /dev/null; then
68+
break
69+
fi
70+
sleep 2
71+
done
6372
curl -f http://localhost:5000/ || exit 1
6473
docker stop test-app
6574
docker rm test-app

.github/workflows/docker-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
push: true
3636
tags: |
3737
udaycodespace/credify:latest
38-
udaycodespace/credify:v2.1.0
38+
udaycodespace/credify:v2
3939
udaycodespace/credify:sha-${{ github.sha }}
4040
cache-from: type=registry,ref=udaycodespace/credify:buildcache
4141
cache-to: type=registry,ref=udaycodespace/credify:buildcache,mode=max

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,5 @@ htmlcov/
182182
.coverage
183183
dist/
184184
build/
185-
*.egg-info/
185+
*.egg-info/
186+
credify-verify/

README.md

Lines changed: 272 additions & 771 deletions
Large diffs are not rendered by default.

app/app.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,24 +67,36 @@ def init_extensions(app):
6767
blockchain.create_genesis_block()
6868

6969
# P2P multi-node init
70-
peer_nodes_env = os.environ.get("PEER_NODES", "")
71-
if peer_nodes_env:
72-
for peer in peer_nodes_env.split(","):
73-
if peer.strip():
74-
try:
75-
blockchain.register_node(peer.strip())
76-
except Exception as e:
77-
logging.warning(f"Invalid peer URI: {peer.strip()}")
70+
blockchain.node_id = app.config.get("NODE_ID", "standalone")
71+
blockchain.node_address = (app.config.get("NODE_ADDRESS", "") or "").strip().rstrip("/")
72+
local_node_address = (app.config.get("NODE_ADDRESS", "") or "").strip().rstrip("/")
73+
peer_nodes = app.config.get("PEER_NODES", [])
74+
75+
if peer_nodes:
76+
for peer in peer_nodes:
77+
if local_node_address and peer == local_node_address:
78+
continue
79+
try:
80+
blockchain.register_node(peer)
81+
except Exception:
82+
logging.warning(f"Invalid peer URI: {peer}")
7883
if blockchain.nodes:
7984
threading.Thread(target=_initial_sync, args=(app,), daemon=True).start()
8085

86+
configured_node_validators = app.config.get("VALIDATOR_NODES", [])
87+
if configured_node_validators:
88+
blockchain.set_node_validators(configured_node_validators)
89+
else:
90+
blockchain.set_node_validators([blockchain._get_current_node_ref(), *blockchain.nodes])
91+
8192

8293
def _initial_sync(app):
8394
"""Background peer synchronization task"""
8495
time.sleep(5)
8596
with app.app_context():
8697
try:
87-
logging.info(f"Syncing with peers: {blockchain.nodes}...")
98+
node_id = app.config.get("NODE_ID", "standalone")
99+
logging.info(f"Node {node_id} syncing with peers: {blockchain.nodes}...")
88100
if blockchain.resolve_conflicts():
89101
logging.info(f"Synchronized chain. New length: {len(blockchain.chain)}")
90102
except Exception as e:

app/blueprints/admin/routes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ def api_system_stats():
425425
stats["blockchain"] = {
426426
"blocks": len(blockchain.chain),
427427
"peers": len(blockchain.nodes),
428-
"node_name": os.environ.get("NODE_NAME", "standalone"),
428+
"node_name": current_app.config.get("NODE_ID") or os.environ.get("NODE_NAME", "standalone"),
429429
"validators": blockchain.VALIDATORS,
430430
}
431431

app/blueprints/api/routes.py

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -79,29 +79,23 @@ def receive_peer_block():
7979
if not block_data:
8080
return jsonify({"success": False, "message": "No block data provided"}), 400
8181

82-
# 1. Reconstruct block object
83-
new_block = blockchain.block_model(
84-
index=block_data["index"],
85-
timestamp=block_data["timestamp"],
86-
data=json.dumps(block_data["data"]),
87-
merkle_root=block_data.get("merkle_root"),
88-
previous_hash=block_data["previous_hash"],
89-
nonce=block_data["nonce"],
90-
hash=block_data["hash"],
91-
signed_by=block_data.get("signed_by"),
92-
signature=block_data.get("signature"),
93-
)
82+
required_fields = {"index", "timestamp", "data", "previous_hash", "nonce", "hash"}
83+
missing = [field for field in required_fields if field not in block_data]
84+
if missing:
85+
return jsonify({"success": False, "message": f"Missing required fields: {', '.join(missing)}"}), 400
9486

95-
# 2. Simple validation against local chain
96-
last_block = blockchain.get_latest_block()
97-
if last_block and block_data["index"] <= last_block.index:
98-
return jsonify({"success": False, "message": "Block already exists or is outdated"}), 409
87+
source_node = blockchain.normalize_node_ref(request.headers.get("X-Node-Address") or request.headers.get("X-Source-Node"))
88+
origin_node = (request.headers.get("X-Origin-Node") or request.headers.get("X-Source-Node") or "unknown").strip()
89+
sender_node = source_node or blockchain.normalize_node_ref(block_data.get("proposed_by"))
9990

100-
if last_block and block_data["previous_hash"] != last_block.hash:
101-
return jsonify({"success": False, "message": "Previous hash mismatch. Sync required."}), 400
91+
if not blockchain.is_validator_node(sender_node):
92+
return jsonify({"success": False, "message": f"Unauthorized validator node: {sender_node or 'unknown'}"}), 403
10293

103-
# 3. Cryptographic validation (simplified for the model bridge)
104-
# Create a Block object for validation methods
94+
# Idempotency gate: if already present, ignore safely.
95+
if blockchain.has_block(block_data["index"], block_data["hash"]):
96+
return jsonify({"success": True, "message": "Duplicate block ignored"}), 200
97+
98+
# 1. Validate block object before any persistence
10599
from core.blockchain import Block
106100

107101
v_block = Block(
@@ -110,6 +104,8 @@ def receive_peer_block():
110104
block_data["previous_hash"],
111105
signed_by=block_data.get("signed_by"),
112106
signature=block_data.get("signature"),
107+
proposed_by=block_data.get("proposed_by"),
108+
status=block_data.get("status"),
113109
)
114110
v_block.timestamp = block_data["timestamp"]
115111
v_block.nonce = block_data["nonce"]
@@ -119,16 +115,55 @@ def receive_peer_block():
119115
if v_block.hash != v_block.calculate_hash():
120116
return jsonify({"success": False, "message": "Invalid block hash"}), 400
121117

122-
if blockchain.crypto_manager and v_block.signature:
118+
if v_block.merkle_root and v_block.merkle_root != v_block.calculate_merkle_root():
119+
return jsonify({"success": False, "message": "Invalid Merkle root"}), 400
120+
121+
if v_block.index > 0 and v_block.signed_by not in blockchain.VALIDATORS:
122+
return jsonify({"success": False, "message": "Unauthorized block signer"}), 403
123+
124+
if blockchain.crypto_manager:
125+
if not v_block.signature:
126+
return jsonify({"success": False, "message": "Missing digital signature"}), 400
123127
if not blockchain.crypto_manager.verify_signature(v_block.hash, v_block.signature):
124128
return jsonify({"success": False, "message": "Invalid digital signature"}), 400
125129

126-
# All checks passed, add to local DB and chain
130+
# Finality gate: old blocks may omit status; accepted blocks become FINALIZED.
131+
if v_block.status not in (None, "FINALIZED"):
132+
return jsonify({"success": False, "message": "Block status must be FINALIZED"}), 400
133+
134+
# 2. Validate against local chain linkage
135+
last_block = blockchain.get_latest_block()
136+
if last_block and block_data["index"] <= last_block.index:
137+
return jsonify({"success": True, "message": "Outdated block ignored"}), 200
138+
139+
if last_block and block_data["previous_hash"] != last_block.hash:
140+
return jsonify({"success": False, "message": "Previous hash mismatch. Sync required."}), 400
141+
142+
# 3. Persist only after full validation
143+
new_block = blockchain.block_model(
144+
index=block_data["index"],
145+
timestamp=block_data["timestamp"],
146+
data=json.dumps(block_data["data"]),
147+
merkle_root=block_data.get("merkle_root"),
148+
previous_hash=block_data["previous_hash"],
149+
nonce=block_data["nonce"],
150+
hash=block_data["hash"],
151+
signed_by=block_data.get("signed_by"),
152+
signature=block_data.get("signature"),
153+
)
154+
127155
db.session.add(new_block)
128156
db.session.commit()
157+
v_block.status = "FINALIZED"
129158
blockchain.chain.append(v_block)
130159

131-
logging.info(f"Accepted peer block {block_data['index']} from {block_data.get('signed_by')}")
160+
# Controlled gossip propagation: relay accepted blocks, never back to sender.
161+
blockchain.broadcast_block(v_block, source_node=source_node, origin_node=origin_node)
162+
163+
logging.info(
164+
f"Accepted peer block {block_data['index']} from signer={block_data.get('signed_by')} "
165+
f"source={source_node or 'unknown'} origin={origin_node} sender={sender_node or 'unknown'}"
166+
)
132167
return jsonify({"success": True, "message": "Block accepted and added to chain"})
133168

134169
except Exception as e:

app/config.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ class Config:
3737
SECRET_KEY = os.environ.get("SESSION_SECRET", "dev-secret-key-change-in-production")
3838
DEBUG = True
3939
PORT = int(os.environ.get("PORT", 5000))
40+
HOST = os.environ.get("HOST", "0.0.0.0")
41+
42+
# Multi-node settings (backward compatible aliases)
43+
NODE_ID = os.environ.get("NODE_ID") or os.environ.get("NODE_NAME") or "standalone"
44+
NODE_ADDRESS = (os.environ.get("NODE_ADDRESS") or "").strip()
45+
PEER_NODES = [
46+
peer.strip().rstrip("/")
47+
for peer in os.environ.get("PEER_NODES", "").split(",")
48+
if peer.strip()
49+
]
4050

4151
# IPFS settings
4252
IPFS_ENDPOINTS = [
@@ -47,6 +57,17 @@ class Config:
4757
# Blockchain settings - FIXED paths
4858
BLOCKCHAIN_DIFFICULTY = 0
4959
VALIDATOR_USERNAMES = ["admin", "issuer1"]
60+
VALIDATOR_NODES = [
61+
node.strip().rstrip("/")
62+
for node in os.environ.get(
63+
"VALIDATORS",
64+
os.environ.get(
65+
"VALIDATOR_NODES",
66+
"node1:5000,node2:5000,node3:5000,node4:5000,node5:5000,standalone",
67+
),
68+
).split(",")
69+
if node.strip()
70+
]
5071
BLOCKCHAIN_FILE = DATA_DIR / "blockchain_data.json"
5172

5273
# Crypto settings - FIXED path

0 commit comments

Comments
 (0)