-
-
Notifications
You must be signed in to change notification settings - Fork 16
Modular Core Implementation & Smart Contracts and contract tests #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5d58c2c
da8d108
dfbcd95
8fb795d
c51717a
d06f2b9
b1639cb
2d43e2a
1349d3a
a07c68c
ff89df6
d2135fd
a9c965a
d0e44a6
1c46fca
6fbb613
d49a985
b32d077
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import sys | ||
| import os | ||
|
|
||
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from .pow import mine_block, calculate_hash, MiningExceededError | ||
|
|
||
| __all__ = ["mine_block", "calculate_hash", "MiningExceededError"] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,73 @@ | ||||||||||||||||
| import json | ||||||||||||||||
| import time | ||||||||||||||||
| from nacl.hash import sha256 | ||||||||||||||||
| from nacl.encoding import HexEncoder | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| class MiningExceededError(Exception): | ||||||||||||||||
| """Raised when max_nonce, timeout, or cancellation is exceeded during mining.""" | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| def calculate_hash(block_dict): | ||||||||||||||||
| """Calculates SHA256 hash of a block header.""" | ||||||||||||||||
| block_string = json.dumps(block_dict, sort_keys=True).encode("utf-8") | ||||||||||||||||
| return sha256(block_string, encoder=HexEncoder).decode("utf-8") | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| def mine_block( | ||||||||||||||||
| block, | ||||||||||||||||
| difficulty=4, | ||||||||||||||||
| max_nonce=10_000_000, | ||||||||||||||||
| timeout_seconds=None, | ||||||||||||||||
| logger=None, | ||||||||||||||||
| progress_callback=None | ||||||||||||||||
| ): | ||||||||||||||||
| """Mines a block using Proof-of-Work without mutating input block until success.""" | ||||||||||||||||
|
|
||||||||||||||||
| target = "0" * difficulty | ||||||||||||||||
| local_nonce = 0 | ||||||||||||||||
| start_time = time.time() | ||||||||||||||||
|
|
||||||||||||||||
| if logger: | ||||||||||||||||
| logger.info( | ||||||||||||||||
| "Mining block %s (Difficulty: %s)", | ||||||||||||||||
| block.index, | ||||||||||||||||
| difficulty, | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| while True: | ||||||||||||||||
|
|
||||||||||||||||
| # Enforce max_nonce limit before hashing | ||||||||||||||||
| if local_nonce >= max_nonce: | ||||||||||||||||
| if logger: | ||||||||||||||||
| logger.warning("Max nonce exceeded during mining.") | ||||||||||||||||
| raise MiningExceededError("Mining failed: max_nonce exceeded") | ||||||||||||||||
|
|
||||||||||||||||
| # Enforce timeout if specified | ||||||||||||||||
| if timeout_seconds is not None and (time.time() - start_time) > timeout_seconds: | ||||||||||||||||
| if logger: | ||||||||||||||||
| logger.warning("Mining timeout exceeded.") | ||||||||||||||||
| raise MiningExceededError("Mining failed: timeout exceeded") | ||||||||||||||||
|
|
||||||||||||||||
| # Temporarily set nonce for hashing only | ||||||||||||||||
| block.nonce = local_nonce | ||||||||||||||||
| block_hash = calculate_hash(block.to_header_dict()) | ||||||||||||||||
|
Comment on lines
+52
to
+54
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Docstring claims no mutation until success, but Line 25 says "without mutating input block until success," but line 53 sets Option A: Fix the docstring- """Mines a block using Proof-of-Work without mutating input block until success."""
+ """Mines a block using Proof-of-Work. Note: block.nonce is mutated during mining."""Option B: Avoid mutating the block during the loop- # Temporarily set nonce for hashing only
- block.nonce = local_nonce
- block_hash = calculate_hash(block.to_header_dict())
+ header = block.to_header_dict()
+ header["nonce"] = local_nonce
+ block_hash = calculate_hash(header)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
| # Allow cancellation via progress callback (pass nonce explicitly) | ||||||||||||||||
| if progress_callback: | ||||||||||||||||
| should_continue = progress_callback(local_nonce, block_hash) | ||||||||||||||||
| if should_continue is False: | ||||||||||||||||
| if logger: | ||||||||||||||||
| logger.info("Mining cancelled via progress_callback.") | ||||||||||||||||
| raise MiningExceededError("Mining cancelled") | ||||||||||||||||
|
|
||||||||||||||||
| # Check difficulty target | ||||||||||||||||
| if block_hash.startswith(target): | ||||||||||||||||
| block.nonce = local_nonce | ||||||||||||||||
| block.hash = block_hash | ||||||||||||||||
| if logger: | ||||||||||||||||
| logger.info("Success! Hash: %s", block_hash) | ||||||||||||||||
| return block | ||||||||||||||||
|
|
||||||||||||||||
| # Increment nonce after attempt | ||||||||||||||||
| local_nonce += 1 | ||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| from .block import Block | ||
| from .chain import Blockchain | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| from .transaction import Transaction | ||
| from .state import State | ||
| from .contract import ContractMachine | ||
|
|
||
| __all__ = [ | ||
| "Block", | ||
| "Blockchain", | ||
| "Transaction", | ||
| "State", | ||
| "ContractMachine", | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import time | ||
|
|
||
| class Block: | ||
| def __init__(self, index, previous_hash, transactions, timestamp=None, difficulty=None): | ||
| self.index = index | ||
| self.previous_hash = previous_hash | ||
| self.transactions = transactions | ||
| self.timestamp = time.time() if timestamp is None else timestamp | ||
| self.nonce = 0 | ||
| self.hash = None | ||
| self.difficulty = difficulty | ||
|
|
||
| def to_dict(self): | ||
| """Full block data for serialization/transport.""" | ||
| return { | ||
| "index": self.index, | ||
| "previous_hash": self.previous_hash, | ||
| "transactions": [tx.to_dict() for tx in self.transactions], | ||
| "timestamp": self.timestamp, | ||
| "difficulty": self.difficulty, | ||
| "nonce": self.nonce, | ||
| "hash": self.hash | ||
| } | ||
|
|
||
| def to_header_dict(self): | ||
| """Data used for mining (consensus).""" | ||
| return { | ||
| "index": self.index, | ||
| "previous_hash": self.previous_hash, | ||
| "transactions": [tx.to_dict() for tx in self.transactions], | ||
| "timestamp": self.timestamp, | ||
| "difficulty": self.difficulty, | ||
| "nonce": self.nonce | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| from core.block import Block | ||
| from core.state import State | ||
| from consensus import calculate_hash | ||
| import logging | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class Blockchain: | ||
| """ | ||
| Manages the blockchain, validates blocks, and commits state transitions. | ||
| """ | ||
|
|
||
| def __init__(self): | ||
| self.chain = [] | ||
| self.state = State() | ||
| self._create_genesis_block() | ||
|
|
||
| def _create_genesis_block(self): | ||
| """ | ||
| Creates the genesis block with a fixed hash. | ||
| """ | ||
| genesis_block = Block( | ||
| index=0, | ||
| previous_hash="0", | ||
| transactions=[] | ||
| ) | ||
| genesis_block.hash = "0" * 64 | ||
| self.chain.append(genesis_block) | ||
|
|
||
| @property | ||
| def last_block(self): | ||
| """ | ||
| Returns the most recent block in the chain. | ||
| """ | ||
| 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. | ||
| """ | ||
|
|
||
| # Check previous hash linkage | ||
| if block.previous_hash != self.last_block.hash: | ||
| logger.warning("Block %s rejected: Invalid previous hash %s != %s", block.index, block.previous_hash, self.last_block.hash) | ||
| return False | ||
|
|
||
| # Check index linkage | ||
| if block.index != self.last_block.index + 1: | ||
| logger.warning("Block %s rejected: Invalid index %s != %s", block.index, block.index, self.last_block.index + 1) | ||
| return False | ||
|
|
||
| # Verify block hash | ||
| if block.hash != calculate_hash(block.to_dict()): | ||
| logger.warning("Block %s rejected: Invalid hash %s", block.index, block.hash) | ||
| return False | ||
|
Comment on lines
+54
to
+57
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify the hash verification bug by checking to_dict and to_header_dict implementations
echo "=== to_dict in block.py ==="
rg -n 'def to_dict' -A 20 core/block.py
echo ""
echo "=== to_header_dict in block.py ==="
rg -n 'def to_header_dict' -A 20 core/block.py
echo ""
echo "=== mine_block hash computation ==="
rg -n 'def mine_block' -A 30 consensus/pow.py
echo ""
echo "=== add_block method in chain.py ==="
rg -n 'def add_block' -A 40 core/chain.pyRepository: StabilityNexus/MiniChain Length of output: 3959 🏁 Script executed: #!/bin/bash
# Get the full mine_block function to see hash computation
rg -n 'calculate_hash.*to_' consensus/pow.py
echo ""
echo "=== Full context around hash computation in mine_block ==="
rg -n 'block.hash' consensus/pow.py -B 3 -A 3
echo ""
echo "=== Check if difficulty is validated anywhere in add_block ==="
rg -n 'difficulty' core/chain.pyRepository: StabilityNexus/MiniChain Length of output: 1149 Critical: Hash verification uses Mining computes the block hash via Additionally, the PoW difficulty target is not verified in Fix: Use Proposed fix # Verify block hash
- if block.hash != calculate_hash(block.to_dict()):
+ computed_hash = calculate_hash(block.to_header_dict())
+ if block.hash != computed_hash:
logger.warning("Block %s rejected: Invalid hash %s", block.index, block.hash)
return False
+
+ # Verify PoW difficulty target
+ if block.difficulty and not block.hash.startswith("0" * block.difficulty):
+ logger.warning("Block %s rejected: Hash does not meet difficulty %s", block.index, block.difficulty)
+ return False🤖 Prompt for AI Agents |
||
|
|
||
| # 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 result is False or result is None: | ||
| logger.warning("Block %s rejected: Transaction failed validation", block.index) | ||
| return False | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # All transactions valid → commit state and append block | ||
| self.state = temp_state | ||
| self.chain.append(block) | ||
| return True | ||
|
Comment on lines
+38
to
+73
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Atomic validation is a good pattern, but the block's PoW hash is not verified.
🛡️ Proposed fix — add PoW verification def add_block(self, block):
+ # Verify PoW
+ from consensus.pow import calculate_hash
+ computed_hash = calculate_hash(block.to_header_dict())
+ if computed_hash != block.hash:
+ return False
+ if block.difficulty and not block.hash.startswith("0" * block.difficulty):
+ return False
+
# Check previous hash linkage
if block.previous_hash != self.last_block.hash:
return False🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| import logging | ||
| import multiprocessing | ||
| import ast | ||
| import sys | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| def _safe_exec_worker(code, globals_dict, context_dict, result_queue): | ||
| """ | ||
| Worker function to execute contract code in a separate process. | ||
| """ | ||
| try: | ||
| # Attempt to set resource limits (Unix only) | ||
| try: | ||
| import resource | ||
| # Limit CPU time (seconds) and memory (bytes) - example values | ||
| resource.setrlimit(resource.RLIMIT_CPU, (1, 1)) | ||
| # resource.setrlimit(resource.RLIMIT_AS, (100 * 1024 * 1024, 100 * 1024 * 1024)) | ||
| except ImportError as e: | ||
| logger.error(f"Resource limits not enforced: {e}") | ||
| raise RuntimeError(f"Resource limits not enforced: {e}") | ||
|
Comment on lines
+12
to
+21
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Contract execution always fails on Windows due to When 🐛 Proposed fix — degrade gracefully on non-Unix try:
import resource
resource.setrlimit(resource.RLIMIT_CPU, (1, 1))
- except ImportError as e:
- raise RuntimeError(f"Resource limits not enforced: {e}")
+ except ImportError:
+ pass # Resource limits not available on this platform (e.g., Windows)🧰 Tools🪛 Ruff (0.15.0)[warning] 19-19: Within an (B904) [warning] 19-19: Avoid specifying long messages outside the exception class (TRY003) 🤖 Prompt for AI Agents |
||
|
|
||
| exec(code, globals_dict, context_dict) | ||
| # Return the updated storage | ||
| result_queue.put({"status": "success", "storage": context_dict.get("storage")}) | ||
| except Exception as e: | ||
| result_queue.put({"status": "error", "error": str(e)}) | ||
|
|
||
| class ContractMachine: | ||
| """ | ||
| A minimal execution environment for Python-based smart contracts. | ||
| WARNING: Still not production-safe. For educational use only. | ||
| """ | ||
|
|
||
| def __init__(self, state): | ||
| self.state = state | ||
|
|
||
| def execute(self, contract_address, sender_address, payload, amount): | ||
| """ | ||
| Executes the contract code associated with the contract_address. | ||
| """ | ||
|
|
||
| account = self.state.get_account(contract_address) | ||
| if not account: | ||
| return False | ||
|
Comment on lines
+43
to
+45
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
♻️ Proposed fix account = self.state.get_account(contract_address)
- if not account:
- return False
-
code = account.get("code")🤖 Prompt for AI Agents |
||
|
|
||
| code = account.get("code") | ||
|
|
||
| # Defensive copy of storage to prevent direct mutation | ||
| storage = dict(account.get("storage", {})) | ||
|
Comment on lines
+49
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shallow copy doesn't protect nested mutable values in storage.
🐛 Proposed fix — use deep copy+import copy
+
class ContractMachine:- storage = dict(account.get("storage", {}))
+ storage = copy.deepcopy(account.get("storage", {}))🤖 Prompt for AI Agents |
||
|
|
||
| if not code: | ||
| return False | ||
|
|
||
| # AST Validation to prevent introspection | ||
| if not self._validate_code_ast(code): | ||
| return False | ||
|
|
||
| # Restricted builtins (explicit allowlist) | ||
| safe_builtins = { | ||
| "True": True, | ||
| "False": False, | ||
| "None": None, | ||
| "range": range, | ||
| "len": len, | ||
| "min": min, | ||
| "max": max, | ||
| "abs": abs, | ||
| } | ||
|
Comment on lines
+60
to
+69
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Safe builtins allowlist still permits certain bypasses.
🤖 Prompt for AI Agents |
||
|
|
||
| globals_for_exec = { | ||
| "__builtins__": safe_builtins | ||
| } | ||
|
|
||
| # Execution context (locals) | ||
| context = { | ||
| "storage": storage, | ||
| "msg": { | ||
| "sender": sender_address, | ||
| "value": amount, | ||
| "data": payload, | ||
| }, | ||
| "print": print, # Explicitly allowed for debugging | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| try: | ||
| # Execute in a subprocess with timeout | ||
| queue = multiprocessing.Queue() | ||
| p = multiprocessing.Process( | ||
| target=_safe_exec_worker, | ||
| args=(code, globals_for_exec, context, queue) | ||
| ) | ||
| p.start() | ||
| p.join(timeout=2) # 2 second timeout | ||
|
|
||
| if p.is_alive(): | ||
| p.kill() | ||
| logger.error("Contract execution timed out") | ||
| return False | ||
|
|
||
| if queue.empty(): | ||
| logger.error("Contract execution crashed without result") | ||
| return False | ||
|
|
||
| result = queue.get() | ||
| if result["status"] != "success": | ||
| logger.error(f"Contract Execution Failed: {result.get('error')}") | ||
| return False | ||
|
|
||
| # Commit updated storage only after successful execution | ||
| self.state.update_contract_storage( | ||
| contract_address, | ||
| result["storage"] | ||
| ) | ||
|
|
||
| return True | ||
aniket866 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| except Exception as e: | ||
| logger.error("Contract Execution Failed", exc_info=True) | ||
| return False | ||
|
Comment on lines
+118
to
+120
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Use Per Ruff F841 and G201, ♻️ Proposed fix- except Exception as e:
- logger.error("Contract Execution Failed", exc_info=True)
+ except Exception:
+ logger.exception("Contract Execution Failed")
return False🧰 Tools🪛 Ruff (0.15.0)[error] 116-116: Local variable Remove assignment to unused variable (F841) [warning] 117-117: Logging (G201) 🤖 Prompt for AI Agents |
||
|
|
||
| def _validate_code_ast(self, code): | ||
| """Reject code that uses double underscores or introspection.""" | ||
| try: | ||
| tree = ast.parse(code) | ||
| for node in ast.walk(tree): | ||
| if isinstance(node, ast.Attribute) and node.attr.startswith("__"): | ||
| logger.warning("Rejected contract code with double-underscore attribute access.") | ||
| return False | ||
| if isinstance(node, ast.Name) and node.id.startswith("__"): | ||
| logger.warning("Rejected contract code with double-underscore name.") | ||
| return False | ||
| return True | ||
| except SyntaxError: | ||
| return False | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider using an installable package instead of
sys.pathmanipulation.This
sys.path.inserthack is fragile and can mask import issues. If the project adopts apyproject.toml(orsetup.py) withpip install -e ., pytest will discover all modules without path manipulation, and this file becomes unnecessary.For the current project scope this is acceptable, but it won't scale well if the project grows or CI environments differ.
🤖 Prompt for AI Agents