Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b531c45
done
anshulchikhale30-p Mar 21, 2026
34deb5d
Update minichain/chain.py
anshulchikhale30-p Mar 21, 2026
25ee67d
Update minichain/chain.py
anshulchikhale30-p Mar 21, 2026
0fde886
Update minichain/chain.py
anshulchikhale30-p Mar 21, 2026
258342b
Update minichain/pid.py
anshulchikhale30-p Mar 21, 2026
46ae32e
Update test_pid_integration.py
anshulchikhale30-p Mar 21, 2026
d911642
Update minichain/pow.py
anshulchikhale30-p Mar 21, 2026
af63105
Update tests/test_difficulty.py
anshulchikhale30-p Mar 21, 2026
879a9c1
Update tests/test_difficulty.py
anshulchikhale30-p Mar 21, 2026
08e1cb3
Update test_pid_integration.py
anshulchikhale30-p Mar 21, 2026
88ac5ac
test file under test folder fix
anshulchikhale30-p Mar 22, 2026
7205ce0
Merge branch 'pid-app' of https://github.com/anshulchikhale30-p/MiniC…
anshulchikhale30-p Mar 22, 2026
1b8b76a
fixed
anshulchikhale30-p Mar 22, 2026
57e163a
Update chain.py
anshulchikhale30-p Mar 22, 2026
e91af2d
Update test_pid_integration.py
anshulchikhale30-p Mar 22, 2026
d83df01
Update test_difficulty.py
anshulchikhale30-p Mar 22, 2026
3a75cdf
Update chain.py
anshulchikhale30-p Mar 22, 2026
5bbc9c6
Update tests/test_difficulty.py
anshulchikhale30-p Mar 22, 2026
1da0818
Update tests/test_pid_integration.py
anshulchikhale30-p Mar 22, 2026
e18cfa6
Update tests/test_pid_integration.py
anshulchikhale30-p Mar 22, 2026
5dfeac5
Update tests/test_pid_integration.py
anshulchikhale30-p Mar 22, 2026
70abc1e
Merge branch 'StabilityNexus:main' into pid-app
anshulchikhale30-p Mar 23, 2026
ff7fea5
Update test_pid_integration.py
anshulchikhale30-p Mar 23, 2026
504ded0
remove duplicate block of code from 100 to 113
anshulchikhale30-p Mar 24, 2026
a329455
Update chain.py
anshulchikhale30-p Mar 24, 2026
046f4f0
Update pow.py
anshulchikhale30-p Mar 24, 2026
0a1cfa9
Update pid.py
anshulchikhale30-p Mar 24, 2026
3f70d4f
Update test_pid_integration.py
anshulchikhale30-p Mar 24, 2026
b5b855f
Merge branch 'StabilityNexus:main' into pid-app
anshulchikhale30-p Mar 26, 2026
df90e3d
Removed line 66
anshulchikhale30-p Mar 26, 2026
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
1 change: 1 addition & 0 deletions minichain/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ def from_dict(cls, payload: dict):
transactions=transactions,
timestamp=payload.get("timestamp"),
difficulty=payload.get("difficulty"),
mining_time=payload.get("mining_time"),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since you removed mining_time from here this will cause a type error and breaks p2p sync

)
block.nonce = payload.get("nonce", 0)
block.hash = payload.get("hash")
Expand Down
64 changes: 56 additions & 8 deletions minichain/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .pow import calculate_hash
import logging
import threading

from minichain.pid import PIDDifficultyAdjuster
logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -32,6 +32,9 @@ def __init__(self):
self.chain = []
self.state = State()
self._lock = threading.RLock()
self.difficulty_adjuster = PIDDifficultyAdjuster(target_block_time=10)
# Use reasonable initial difficulty (4 leading zeros)
self.current_difficulty = 4
self._create_genesis_block()

def _create_genesis_block(self):
Expand All @@ -44,41 +47,86 @@ def _create_genesis_block(self):
transactions=[]
)
genesis_block.hash = "0" * 64
genesis_block.difficulty = self.current_difficulty
self.chain.append(genesis_block)

@property
def last_block(self):
"""
Returns the most recent block in the chain.
"""
with self._lock: # Acquire lock for thread-safe access
with self._lock: # Acquire lock for thread-safe access
return self.chain[-1]

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.


- Validates PoW against current network difficulty
- Calculates block time from immutable timestamps (not mining_time)
- Uses stateless PID (no local memory variables)
- Prevents all hard-fork scenarios
"""

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


# Validate PoW against current network difficulty
# Cap difficulty to 64 (SHA-256 hash is 64 hex chars)
expected_difficulty = min(self.current_difficulty, 64)
target_prefix = '0' * expected_difficulty

if not block.hash or not block.hash.startswith(target_prefix):
logger.warning(
"Block %s rejected: PoW check failed (required %d leading zeros)",
block.index,
expected_difficulty
)
return False

# Validate transactions on a temporary state copy
temp_state = self.state.copy()

for tx in block.transactions:
result = temp_state.validate_and_apply(tx)

# Reject block if any transaction fails
if not result:
logger.warning("Block %s rejected: Transaction failed validation", block.index)
return False


# Calculate block time from TIMESTAMPS (immutable, secure)
previous_block = self.last_block
actual_block_time_ms = block.timestamp - previous_block.timestamp
actual_block_time = actual_block_time_ms / 1000.0 # Convert ms to seconds

# Adjust difficulty using STATELESS PID
# Same calculation across all nodes = deterministic consensus
old_difficulty = self.current_difficulty
self.current_difficulty = self.difficulty_adjuster.adjust(
self.current_difficulty,
actual_block_time
)

# Cap difficulty to prevent impossible mining
self.current_difficulty = min(self.current_difficulty, 64)

# All transactions valid → commit state and append block
self.state = temp_state
self.chain.append(block)

logger.info(
"Block %s accepted. Time: %.2fs, Difficulty: %d → %d",
block.index,
actual_block_time,
old_difficulty,
self.current_difficulty
)
return True

154 changes: 154 additions & 0 deletions minichain/pid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""
Stateless PID-based Difficulty Adjuster for MiniChain

CRITICAL: This implementation is 100% STATELESS.
No memory variables (integral, derivative) that persist between calls.

Why? If a node restarts, local variables reset to 0, causing the node to calculate
a radically different difficulty than the network, leading to a hard-fork.

Solution: Calculate difficulty purely from blockchain timestamps.
All nodes compute the same result regardless of when they started.
"""

from typing import Optional


class PIDDifficultyAdjuster:
"""
Stateless difficulty adjuster using pure calculation from block data.

100% Deterministic: Same inputs always produce same outputs
No Hard-Forks: Nodes get identical results regardless of restart
Blockchain-Based: Uses immutable timestamps, not local memory
"""

SCALE = 1000 # Fixed-point scaling factor for integer math

def __init__(
self,
target_block_time: float = 10.0,
kp: int = 50,
ki: int = 5,
kd: int = 10
):
"""
Initialize the stateless PID difficulty adjuster.

Args:
target_block_time: Target time for block generation in seconds
kp: Proportional coefficient (pre-scaled by SCALE). Default 50 = 0.05
ki: Integral coefficient (pre-scaled by SCALE). Default 5 = 0.005
kd: Derivative coefficient (pre-scaled by SCALE). Default 10 = 0.01
"""
self.target_block_time = target_block_time
self.kp = kp # Proportional
self.ki = ki # Integral (smaller - we don't accumulate state)
self.kd = kd # Derivative (smaller - we don't have history)

def adjust(
self,
current_difficulty: int,
actual_block_time: Optional[float] = None
) -> int:
"""
Calculate new difficulty based on CURRENT block time only.

STATELESS: No memory variables
- No self.integral (resets on restart → hard-fork)
- No self.previous_error (resets on restart → hard-fork)
- No self.last_block_time (resets on restart → hard-fork)

Pure calculation from current block time and fixed coefficients.

Args:
current_difficulty: Current difficulty value
actual_block_time: Time to mine this block in seconds

Returns:
New difficulty value (minimum 1)
"""

if actual_block_time is None:
# If no time provided, make no adjustment (safe default)
return current_difficulty

# ===== Fixed-Point Integer Arithmetic =====
# Convert times to scaled integers for precise calculation
actual_block_time_scaled = int(actual_block_time * self.SCALE)
target_time_scaled = int(self.target_block_time * self.SCALE)

# Calculate error (positive = too fast, negative = too slow)
error = target_time_scaled - actual_block_time_scaled

# ===== Proportional Term Only =====
# Without integral/derivative state, we use proportional adjustment
# This is simpler, deterministic, and stateless
p_adjustment = (self.kp * error) // self.SCALE

# ===== Safety Constraint: Limit Change to 10% per Block =====
# FIXED: Use integer division (// 10) not float multiplier (* 0.1)
# This ensures deterministic behavior across all CPUs
max_delta = max(1, current_difficulty // 10)

# Clamp adjustment to safety bounds
clamped_adjustment = max(
min(p_adjustment, max_delta),
-max_delta
)

# Calculate new difficulty
new_difficulty = current_difficulty + clamped_adjustment

# Return new difficulty (minimum 1)
return max(1, new_difficulty)

# NOTE: No reset(), get_state(), or set_state() methods
# These assume persistent memory, which causes hard-forks!
# All state is calculated fresh from blockchain data.


class StatelessPIDDifficultyAdjuster(PIDDifficultyAdjuster):
"""
Alternative: If you need more sophisticated PID logic, calculate from BLOCK HISTORY.

Instead of storing state in memory:
- Query the last N blocks from the blockchain
- Calculate integral as sum of recent errors
- Calculate derivative from last 2 blocks
- All nodes get identical results (no hard-fork)

This is the production-safe approach for a real blockchain.
"""

def adjust_from_history(
self,
current_difficulty: int,
recent_block_times: list # Last N block times in seconds
) -> int:
"""
Calculate adjustment using only blockchain history.

Args:
current_difficulty: Current difficulty
recent_block_times: List of recent block times (e.g., last 10 blocks)

Returns:
New difficulty based on trend analysis
"""
if not recent_block_times:
return current_difficulty

# Calculate average time (proportional term)
avg_time = sum(recent_block_times) / len(recent_block_times)
error = self.target_block_time - avg_time

# Simple adjustment based on average deviation
p_adjustment = int((self.kp * error * self.SCALE) // self.SCALE)

# Safety constraint
max_delta = max(1, current_difficulty // 10)
clamped = max(min(p_adjustment, max_delta), -max_delta)

new_difficulty = current_difficulty + clamped
return max(1, new_difficulty)
18 changes: 15 additions & 3 deletions minichain/pow.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ def mine_block(
logger=None,
progress_callback=None
):
"""Mines a block using Proof-of-Work without mutating input block until success."""
"""Mines a block using Proof-of-Work without mutating input block until success.
NOTE: Mining time is NOT tracked here.
Block time is calculated from immutable blockchain timestamps:
actual_time = block.timestamp - previous_block.timestamp
This prevents miners from lying about how long mining took."""

if not isinstance(difficulty, int) or difficulty <= 0:
raise ValueError("Difficulty must be a positive integer.")
Expand Down Expand Up @@ -57,10 +61,18 @@ def mine_block(
if block_hash.startswith(target):
block.nonce = local_nonce # Assign only on success
block.hash = block_hash
# DO NOT TRACK MINING TIME HERE
# Miners could lie about it (e.g., set to any value they want)
# Block time is calculated from immutable timestamps instead:
# actual_time = block.timestamp - previous_block.timestamp
# This is done in chain.py when the block is added

mining_time = time.monotonic() - start_time
if logger:
logger.info("Success! Hash: %s", block_hash)
logger.info("Success! Hash: %s, Mining time: %.2fs (local only)",
block_hash, mining_time)
return block

# Allow cancellation via progress callback (pass nonce explicitly)
if progress_callback:
should_continue = progress_callback(local_nonce, block_hash)
Expand Down
Loading
Loading