Skip to content
Open
Show file tree
Hide file tree
Changes from 16 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
47 changes: 46 additions & 1 deletion 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,8 @@ def __init__(self):
self.chain = []
self.state = State()
self._lock = threading.RLock()
self.difficulty_adjuster = PIDDifficultyAdjuster(target_block_time=10)
self.current_difficulty = 1000 # Initial difficulty
Comment thread
anshulchikhale30-p marked this conversation as resolved.
Outdated
self._create_genesis_block()
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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

@property
Expand All @@ -66,6 +69,22 @@ def add_block(self, block):
except ValueError as exc:
logger.warning("Block %s rejected: %s", block.index, exc)
return False

# Verify block meets difficulty target BEFORE mutating PID state
# Use same formula as pow.py: target = "0" * difficulty
expected_difficulty = block.difficulty if block.difficulty else self.current_difficulty
target_prefix = '0' * expected_difficulty

if not block.hash or not block.hash.startswith(target_prefix):
logger.warning(
"Block %s rejected: PoW check failed (difficulty: %d)",
block.index,
expected_difficulty
)
return False
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

# Block difficulty validation passed; block.difficulty remains as-is
# (reflects the difficulty at which it was actually mined)

# Validate transactions on a temporary state copy
temp_state = self.state.copy()
Expand All @@ -77,6 +96,32 @@ def add_block(self, block):
if not result:
logger.warning("Block %s rejected: Transaction failed validation", block.index)
return False
for tx in block.transactions:
result = temp_state.validate_and_apply(tx)

# Reject block if any transaction fails
if not result:
Comment thread
anshulchikhale30-p marked this conversation as resolved.
logger.warning("Block %s rejected: Transaction failed validation", block.index)
return False

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

# Adjust difficulty for next block (single adjustment per block)
old_difficulty = self.current_difficulty
self.current_difficulty = self.difficulty_adjuster.adjust(
self.current_difficulty,
block.mining_time if hasattr(block, 'mining_time') else None
Comment thread
anshulchikhale30-p marked this conversation as resolved.
Outdated
)

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

# All transactions valid → commit state and append block
self.state = temp_state
Expand Down
155 changes: 155 additions & 0 deletions minichain/pid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""
PID-based Difficulty Adjuster for MiniChain

Uses fixed-point integer arithmetic for deterministic behavior across all nodes.

Key Fix: Uses integer division (difficulty // 10) instead of float (difficulty * 0.1)
This prevents chain forks from CPU rounding differences.
"""

import time
from typing import Optional


class PIDDifficultyAdjuster:
"""
Adjusts blockchain difficulty using a PID controller to maintain target block time.

Uses fixed-point integer scaling (SCALE=1000) for deterministic behavior.
Ensures all nodes compute identical results regardless of CPU/platform.
"""

SCALE = 1000 # Fixed-point scaling factor

def __init__(
self,
target_block_time: float = 5.0,
kp: int = 500,
ki: int = 50,
kd: int = 100
):
"""
Initialize the PID difficulty adjuster.

Args:
target_block_time: Target time for block generation in seconds
kp: Proportional coefficient (pre-scaled by SCALE). Default 500 = 0.5
ki: Integral coefficient (pre-scaled by SCALE). Default 50 = 0.05
kd: Derivative coefficient (pre-scaled by SCALE). Default 100 = 0.1
"""
self.target_block_time = target_block_time
self.kp = kp # Proportional
self.ki = ki # Integral
self.kd = kd # Derivative
self.integral = 0
self.previous_error = 0
self.last_block_time = time.monotonic()
self.integral_limit = 100 * self.SCALE

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

Args:
current_difficulty: Current difficulty (default: 1000)
actual_block_time: Time to mine block in seconds
If None, calculated from time since last call

Returns:
New difficulty value (minimum 1)

Example:
adjuster = PIDDifficultyAdjuster(target_block_time=10)
new_difficulty = adjuster.adjust(current_difficulty=10000, actual_block_time=12.5)
"""

# Handle None difficulty
if current_difficulty is None:
current_difficulty = 1000

# Calculate actual_block_time if not provided
if actual_block_time is None:
now = time.monotonic()
actual_block_time = now - self.last_block_time
self.last_block_time = now

# ===== 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 =====
p_term = self.kp * error

# ===== Integral Term with Anti-Windup =====
self.integral += error
self.integral = max(
min(self.integral, self.integral_limit),
-self.integral_limit
)
i_term = self.ki * self.integral

# ===== Derivative Term =====
derivative = error - self.previous_error
self.previous_error = error
d_term = self.kd * derivative

# ===== PID Calculation =====
# Combine all terms and scale back to normal units
adjustment = (p_term + i_term + d_term) // self.SCALE

# ===== Safety Constraint: Limit Change to 10% =====
# ✅ FIXED: Use integer division instead of float multiplier
# Was: max_delta = max(1, int(current_difficulty * 0.1))
# Now: max_delta = max(1, current_difficulty // 10)
max_delta = max(1, current_difficulty // 10)

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

# Ensure we move at least ±1 if adjustment is non-zero
delta = clamped_adjustment

# Calculate and return new difficulty
new_difficulty = current_difficulty + delta
return max(1, new_difficulty)

def reset(self) -> None:
"""Reset PID state (integral and derivative history)."""
self.integral = 0
Comment thread
anshulchikhale30-p marked this conversation as resolved.
Outdated
self.previous_error = 0
self.last_block_time = time.monotonic()

def get_state(self) -> dict:
"""
Get current PID state for debugging or persistence.

Returns:
Dictionary containing integral, previous_error, and last update time
"""
return {
"integral": self.integral,
"previous_error": self.previous_error,
"last_block_time": self.last_block_time
}

def set_state(self, state: dict) -> None:
"""
Restore PID state from a dictionary (for recovery/persistence).

Args:
state: Dictionary with keys 'integral', 'previous_error', 'last_block_time'
"""
self.integral = state.get("integral", 0)
self.previous_error = state.get("previous_error", 0)
self.last_block_time = state.get("last_block_time", time.monotonic())
3 changes: 2 additions & 1 deletion minichain/pow.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ def mine_block(
if block_hash.startswith(target):
block.nonce = local_nonce # Assign only on success
block.hash = block_hash
block.mining_time = time.monotonic() - start_time
Comment thread
anshulchikhale30-p marked this conversation as resolved.
Outdated
if logger:
logger.info("Success! Hash: %s", block_hash)
logger.info("Success! Hash: %s, Mining time: %.2fs", block_hash, block.mining_time)
Comment thread
anshulchikhale30-p marked this conversation as resolved.
Outdated
return block

# Allow cancellation via progress callback (pass nonce explicitly)
Expand Down
Loading
Loading