Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,9 @@ async def handler(data):
║ connect <host>:<port> - connect to a peer ║
║ address - show your public key ║
║ chain - show chain summary ║
║ list-banned - show banned peers ║
║ ban <peer_id> - ban a peer ║
║ unban <peer_id> - unban a peer ║
║ help - show this help ║
║ quit - shut down ║
╚════════════════════════════════════════════════╝
Expand Down Expand Up @@ -420,6 +423,37 @@ async def cli_loop(sk, pk, chain, mempool, network):
tx_count = len(b.transactions) if b.transactions else 0
print(f" Block #{b.index} hash={b.hash[:16]}... txs={tx_count}")

# ── list-banned ──
elif cmd == "list-banned":
from minichain.persistence import get_banned_peers
banned = get_banned_peers()
if not banned:
print(" No peers are currently banned.")
else:
print(f" {len(banned)} banned peer(s):")
for p in banned:
print(f" - {p['peer_id']} (Reason: {p['reason']}, Time: {p['timestamp']})")

# ── ban ──
elif cmd == "ban":
if len(parts) < 2:
print(" Usage: ban <peer_id>")
continue
peer_id = parts[1]
from minichain.persistence import ban_peer
ban_peer(peer_id, reason="Manual ban via CLI")
print(f" ✅ Peer {peer_id} banned.")

# ── unban ──
elif cmd == "unban":
if len(parts) < 2:
print(" Usage: unban <peer_id>")
continue
peer_id = parts[1]
from minichain.persistence import unban_peer
unban_peer(peer_id)
print(f" ✅ Peer {peer_id} unbanned.")

# ── help ──
elif cmd == "help":
print(HELP_TEXT)
Expand Down
26 changes: 14 additions & 12 deletions minichain/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,30 +125,31 @@ def add_block(self, block):
Validates and adds a block to the chain if all transactions succeed.
Uses a copied State to ensure atomic validation.
"""
from .validators import ValidationStatus

with self._lock:
try:
validate_block_link_and_hash(self.last_block, block)
except ValueError as exc:
logger.warning("Block %s rejected: %s", block.index, exc)
return False
return ValidationStatus.INVALID if "hash" in str(exc) else ValidationStatus.FAILED

if block.difficulty != self.current_difficulty:
logger.warning("Block %s rejected: Invalid difficulty. Expected %s, got %s", block.index, self.current_difficulty, block.difficulty)
return False
return ValidationStatus.INVALID

# Validate transactions on a temporary state copy
temp_state = self.state.copy()
temp_state.chain_id = self.chain_id
receipts = []

for tx in block.transactions:
receipt = temp_state.validate_and_apply(tx)
status, receipt = temp_state.validate_and_apply_with_status(tx)

# Reject block if any transaction fails mathematical validation (None)
if receipt is None:
# Reject block if any transaction fails mathematical validation
if status != ValidationStatus.VALID:
logger.warning("Block %s rejected: Transaction failed validation", block.index)
return False
return status

receipts.append(receipt)

Expand All @@ -159,16 +160,16 @@ def add_block(self, block):
computed_receipt_root = calculate_receipt_root(receipts)
if block.receipt_root != computed_receipt_root:
logger.warning("Block %s rejected: Invalid receipt root. Expected %s, got %s", block.index, computed_receipt_root, block.receipt_root)
return False
return ValidationStatus.INVALID

if [r.to_dict() for r in block.receipts] != [r.to_dict() for r in receipts]:
logger.warning("Block %s rejected: Receipts payload mismatch", block.index)
return False
return ValidationStatus.INVALID

# Verify state root
if block.state_root != temp_state.state_root():
logger.warning("Block %s rejected: Invalid state root. Expected %s, got %s", block.index, temp_state.state_root(), block.state_root)
return False
return ValidationStatus.INVALID

# Update EMA difficulty state
time_diff = block.timestamp - self.last_block.timestamp
Expand All @@ -182,7 +183,7 @@ def add_block(self, block):
# All transactions valid → commit state and append block
self.state = temp_state
self.chain.append(block)
return True
return ValidationStatus.VALID

def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]:
"""
Expand Down Expand Up @@ -236,8 +237,9 @@ def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]:

receipts = []
for tx in block.transactions:
receipt = temp_state.validate_and_apply(tx)
if receipt is None:
from .validators import ValidationStatus
status, receipt = temp_state.validate_and_apply_with_status(tx)
if status != ValidationStatus.VALID:
logger.warning("Reorg failed: Transaction validation failed in block %s", block.index)
return False, []
receipts.append(receipt)
Expand Down
62 changes: 62 additions & 0 deletions minichain/persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,68 @@ def _load_snapshot_from_sqlite(db_path: str) -> dict[str, Any]:
return {"chain": chain, "state": state}


# ---------------------------------------------------------------------------
# Banned Peers (Track 1)
# ---------------------------------------------------------------------------

import time

def _ensure_banned_peers_table(conn: sqlite3.Connection) -> None:
conn.execute(
"CREATE TABLE IF NOT EXISTS banned_peers (peer_id TEXT PRIMARY KEY, reason TEXT, timestamp REAL)"
)

def ban_peer(peer_id: str, reason: str, path: str = ".") -> None:
db_path = os.path.join(path, _DB_FILE)
os.makedirs(path, exist_ok=True)
conn = _connect(db_path)
try:
_ensure_banned_peers_table(conn)
with conn:
conn.execute(
"INSERT OR REPLACE INTO banned_peers (peer_id, reason, timestamp) VALUES (?, ?, ?)",
(peer_id, reason, time.time())
)
finally:
conn.close()

def unban_peer(peer_id: str, path: str = ".") -> None:
db_path = os.path.join(path, _DB_FILE)
if not os.path.exists(db_path):
return
conn = _connect(db_path)
try:
_ensure_banned_peers_table(conn)
with conn:
conn.execute("DELETE FROM banned_peers WHERE peer_id = ?", (peer_id,))
finally:
conn.close()

def is_peer_banned(peer_id: str, path: str = ".") -> bool:
db_path = os.path.join(path, _DB_FILE)
if not os.path.exists(db_path):
return False
conn = _connect(db_path)
try:
_ensure_banned_peers_table(conn)
row = conn.execute("SELECT peer_id FROM banned_peers WHERE peer_id = ?", (peer_id,)).fetchone()
return row is not None
finally:
conn.close()

def get_banned_peers(path: str = ".") -> list[dict[str, Any]]:
db_path = os.path.join(path, _DB_FILE)
if not os.path.exists(db_path):
return []
conn = _connect(db_path)
try:
_ensure_banned_peers_table(conn)
rows = conn.execute("SELECT peer_id, reason, timestamp FROM banned_peers ORDER BY timestamp DESC").fetchall()
return [{"peer_id": r["peer_id"], "reason": r["reason"], "timestamp": r["timestamp"]} for r in rows]
finally:
conn.close()


# ---------------------------------------------------------------------------
# Legacy JSON helpers
# ---------------------------------------------------------------------------
Expand Down
36 changes: 26 additions & 10 deletions minichain/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,27 @@ def get_account(self, address):
return self.accounts[address]

def verify_transaction_logic(self, tx):
from .validators import ValidationStatus
if not tx.verify():
logger.error("Error: Invalid signature for tx from %s...", tx.sender[:8])
return False
return ValidationStatus.INVALID

if getattr(tx, "chain_id", None) != self.chain_id:
logger.error("Error: Invalid chain_id in tx from %s...", tx.sender[:8])
return False
return ValidationStatus.INVALID

sender_acc = self.get_account(tx.sender)

total_cost = tx.amount + getattr(tx, 'fee', 0)
if sender_acc['balance'] < total_cost:
logger.warning("Invalid tx %s: insufficient balance", tx.tx_id)
return False
return ValidationStatus.FAILED

if sender_acc['nonce'] != tx.nonce:
logger.error("Error: Invalid nonce. Expected %s, got %s", sender_acc['nonce'], tx.nonce)
return False
return ValidationStatus.FAILED

return True
return ValidationStatus.VALID

def copy(self):
"""
Expand All @@ -88,22 +89,37 @@ def restore(self, snapshot_data):
def validate_and_apply(self, tx):
"""
Validate and apply a transaction.
Returns the same success/failure shape as apply_transaction().
NOTE: Delegates to apply_transaction. Callers should use this for
semantic validation entry points.
Returns: Receipt|None
"""
# Semantic validation: amount must be an integer and non-negative
if not isinstance(tx.amount, int) or tx.amount < 0:
return None
# Further checks can be added here
return self.apply_transaction(tx)

def validate_and_apply_with_status(self, tx):
"""
Validate and apply a transaction, bubbling up the precise ValidationStatus.
Returns: (ValidationStatus, Receipt|None)
"""
from .validators import ValidationStatus
if not isinstance(tx.amount, int) or tx.amount < 0:
return ValidationStatus.MALFORMED, None

status = self.verify_transaction_logic(tx)
if status != ValidationStatus.VALID:
return status, None

# We know it's valid, so apply_transaction will succeed and return a Receipt
return ValidationStatus.VALID, self.apply_transaction(tx)

def apply_transaction(self, tx):
"""
Applies transaction and mutates state.
Returns: Receipt object if mathematically valid, None if invalid.
"""
if not self.verify_transaction_logic(tx):
from .validators import ValidationStatus
status = self.verify_transaction_logic(tx)
if status != ValidationStatus.VALID:
return None

sender = self.accounts[tx.sender]
Expand Down
9 changes: 9 additions & 0 deletions minichain/validators.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import re
from enum import Enum, auto


class ValidationStatus(Enum):
VALID = auto()
INVALID = auto()
FAILED = auto()
MALFORMED = auto()


def is_valid_receiver(receiver):
return bool(re.fullmatch(r"[0-9a-fA-F]{40}|[0-9a-fA-F]{64}", receiver))