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
4 changes: 1 addition & 3 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
from nacl.encoding import HexEncoder

from minichain import Transaction, Blockchain, Block, State, Mempool, P2PNetwork, mine_block
from minichain.validators import is_valid_receiver

from minichain.p2p import is_valid_receiver

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -327,7 +326,6 @@ async def on_peer_connected(writer):
logger.info("🔄 Sent state sync to new peer")

network.register_on_peer_connected(on_peer_connected)

await network.start(port=port, host=host)

# Fund this node's wallet so it can transact in the demo
Expand Down
61 changes: 13 additions & 48 deletions minichain/chain.py
Original file line number Diff line number Diff line change
@@ -1,84 +1,49 @@
import logging
import threading
from .block import Block
from .state import State
from .pow import calculate_hash
import logging
import threading

logger = logging.getLogger(__name__)


def validate_block_link_and_hash(previous_block, block):
if block.previous_hash != previous_block.hash:
raise ValueError(
f"invalid previous hash {block.previous_hash} != {previous_block.hash}"
)

raise ValueError("Invalid previous hash")
if block.index != previous_block.index + 1:
raise ValueError(
f"invalid index {block.index} != {previous_block.index + 1}"
)

expected_hash = calculate_hash(block.to_header_dict())
if block.hash != expected_hash:
raise ValueError(f"invalid hash {block.hash}")

raise ValueError("Invalid index")
if block.hash != calculate_hash(block.to_header_dict()):
raise ValueError("Invalid hash")

class Blockchain:
"""
Manages the blockchain, validates blocks, and commits state transitions.
"""

def __init__(self):
self.chain = []
self.state = State()
self._lock = threading.RLock()
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)
genesis = Block(index=0, previous_hash="0", transactions=[])
genesis.hash = "0" * 64
self.chain.append(genesis)

@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:
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.
"""

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)
logger.warning(f"Block {block.index} rejected: {exc}")
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.

🧹 Nitpick | 🔵 Trivial

Prefer parameterized logging over f-strings.

F-strings are evaluated eagerly even when the log level would suppress the message, incurring unnecessary overhead. Use %-style formatting which defers string interpolation until the message is actually emitted.

♻️ Proposed fix
-                logger.warning(f"Block {block.index} rejected: {exc}")
+                logger.warning("Block %s rejected: %s", block.index, exc)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
logger.warning(f"Block {block.index} rejected: {exc}")
logger.warning("Block %s rejected: %s", block.index, exc)
🧰 Tools
🪛 Ruff (0.15.7)

[warning] 39-39: Logging statement uses f-string

(G004)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@minichain/chain.py` at line 39, Replace the eager f-string in the logger call
with parameterized logging to avoid unnecessary interpolation: update the
logger.warning call that currently uses f"Block {block.index} rejected: {exc}"
(the logger.warning usage handling rejected blocks) to use a format string with
placeholders and pass block.index and exc as separate arguments (e.g., "Block %s
rejected: %s", block.index, exc) so interpolation is deferred until the message
is emitted.

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)
if not temp_state.validate_and_apply(tx):
return False
Comment on lines 43 to 45
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.

🧹 Nitpick | 🔵 Trivial

Consider logging transaction validation failures for observability.

Block validation failures are logged (line 39), but transaction validation failures return False silently. This inconsistency can make debugging harder when blocks are rejected due to invalid transactions.

♻️ Proposed fix
             for tx in block.transactions:
                 if not temp_state.validate_and_apply(tx):
+                    logger.warning("Block %s rejected: transaction validation failed", block.index)
                     return False
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)
if not temp_state.validate_and_apply(tx):
return False
for tx in block.transactions:
if not temp_state.validate_and_apply(tx):
logger.warning("Block %s rejected: transaction validation failed", block.index)
return False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@minichain/chain.py` around lines 43 - 45, The loop over block.transactions
silently returns False when temp_state.validate_and_apply(tx) fails; update this
to log the failure before returning so observers know which transaction caused
rejection. Locate the transaction validation loop (the for tx in
block.transactions block and the call temp_state.validate_and_apply(tx)) and add
a log call using the same logger used for block validation (the logger used at
the earlier block validation check) that includes identifying context such as
block identifier (e.g., block.hash or block.header) and the transaction
identifier or index (e.g., tx.id or str(tx) plus its index), then return False
as before.


# All transactions valid → commit state and append block
self.state = temp_state
self.chain.append(block)
return True
return True
132 changes: 24 additions & 108 deletions minichain/contract.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
import logging
import multiprocessing
import ast
import json

import json # Moved to module-level import
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, (2, 2)) # Align with p.join timeout (2 seconds)
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.

Could you explain why you removed the setting of resource limits, @KDiamantidis ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Absolutely! The resource module is Unix-specific. Including it completely breaks cross-platform execution (e.g., if someone tries to run the node on Windows, it throws an ImportError and crashes instantly). Since we already enforce an absolute time boundary via p.join(timeout=2) and immediately terminate the process, I thought removing it helps conceptual minimality and portability.
If you'd prefer to keep it, I can wrap it in a sys.platform check or a try/except ImportError block. Let me know what you prefer!

resource.setrlimit(resource.RLIMIT_AS, (100 * 1024 * 1024, 100 * 1024 * 1024))
except ImportError:
logger.warning("Resource module not available. Contract will run without OS-level resource limits.")
except (OSError, ValueError) as e:
logger.warning("Failed to set resource limits: %s", e)

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)})
Expand All @@ -32,138 +17,69 @@ class ContractMachine:
A minimal execution environment for Python-based smart contracts.
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.

Please keep this warning.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch! I was aggressively cleaning up dead code and accidentally removed the docstring warning along with it. I will restore it right away in the next commit.

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:
if not account or not account.get("code"):
return False

code = account.get("code")

# Defensive copy of storage to prevent direct mutation
code = account["code"]
storage = dict(account.get("storage", {}))

if not code:
return False

# AST Validation to prevent introspection
if not self._validate_code_ast(code):
return False

# Restricted builtins (explicit allowlist)
# Minimal & Deterministic builtins (No floats)
safe_builtins = {
"True": True,
"False": False,
"None": None,
"range": range,
"len": len,
"min": min,
"max": max,
"abs": abs,
"str": str, # Keeping str for basic functionality, relying on AST checks for safety
"bool": bool,
"float": float,
"list": list,
"dict": dict,
"tuple": tuple,
"sum": sum,
"Exception": Exception, # Added to allow contracts to raise exceptions
"True": True, "False": False, "None": None,
"range": range, "len": len, "str": str, "int": int,
"bool": bool, "list": list, "dict": dict, "Exception": Exception
}
Comment on lines +34 to 39
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.

⚠️ Potential issue | 🟡 Minor

The docstring warning should be restored per the previous discussion.

The author acknowledged in the past review that the docstring warning was accidentally removed and agreed to restore it. The current comment "No floats" is brief but the original warning provided important context about the sandbox limitations.

Additionally, float literals are now properly rejected at the AST level (line 80) and JSON serialization enforces allow_nan=False (line 64), which addresses the earlier determinism concerns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@minichain/contract.py` around lines 33 - 38, Restore the original longer
docstring warning above the safe_builtins definition explaining the sandbox
limitations and that floats are not allowed/deterministic behavior, since the
prior discussion agreed it was accidentally removed; update the comment
surrounding safe_builtins (the "No floats" line) to the original explanatory
warning text and reference the existing AST float rejection (validate_ast/AST
visitor) and JSON serialization setting (allow_nan=False) so the warning
accurately reflects that floats are rejected at parse time and JSON enforces
non-NaN serialization.


globals_for_exec = {
"__builtins__": safe_builtins
}

# Execution context (locals)
context = {
"storage": storage,
"msg": {
"sender": sender_address,
"value": amount,
"data": payload,
},
# "print": print, # Removed for security
"msg": {"sender": sender_address, "value": amount, "data": payload}
}

try:
# Execute in a subprocess with timeout
queue = multiprocessing.Queue()
p = multiprocessing.Process(
target=_safe_exec_worker,
args=(code, globals_for_exec, context, queue)
args=(code, {"__builtins__": safe_builtins}, context, queue)
)
p.start()
p.join(timeout=2) # 2 second timeout
p.join(timeout=2)

if p.is_alive():
p.kill()
p.join()
logger.error("Contract execution timed out")
logger.error("Contract timeout")
return False
Comment on lines 46 to 59
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In Python's multiprocessing API, does Process.join(timeout=...) limit a child process's memory/CPU usage or only how long the parent waits? How is resource.setrlimit(RLIMIT_AS/RLIMIT_CPU) used to constrain spawned child processes on POSIX?

💡 Result:

Process.join(timeout=...) only limits how long the parent process waits for the child to terminate; it does not constrain the child process's memory or CPU usage. The child continues running unaffected until it finishes or is explicitly terminated (e.g., via p.terminate after timeout). If the timeout expires, join returns and the parent can check p.is_alive and act accordingly. On POSIX systems, resource.setrlimit(RLIMIT_AS, limits) or resource.setrlimit(RLIMIT_CPU, limits) called in the parent process sets limits that are inherited by child processes created via fork (including multiprocessing's 'fork' start method), as fork copies the parent's limits and they persist across exec. However, with 'spawn' or 'forkserver' start methods (now defaults on many platforms), the child starts a fresh Python interpreter, inheriting limits from the parent but potentially modified by multiprocessing setup. Limits apply per-process (not tree-wide), so each child has its own independent limits for AS (virtual memory) and CPU time (SIGXCPU on exceed). To set limits specifically for multiprocessing children, call setrlimit in the target function (runs in child) or use 'fork' start method after setting in parent. Note: RLIMIT_AS may not be fully supported on all POSIX (e.g., BSD/macOS lacks it), and requires appropriate privileges to raise hard limits.

Citations:


Restore hard resource caps via resource.setrlimit() to sandbox untrusted contract execution.

The current timeout mechanism only limits how long the parent waits for the child process; it does not constrain the child's memory or CPU usage. An untrusted contract can still allocate unbounded memory and OOM the host before the 2-second timeout expires. Add resource.setrlimit(RLIMIT_AS, ...) and/or resource.setrlimit(RLIMIT_CPU, ...) in the child process (via _safe_exec_worker target function) or in the parent before spawning to enforce hard per-process limits on virtual memory and CPU time. The timeout should remain as a fallback, but resource limits are the primary sandbox boundary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@minichain/contract.py` around lines 42 - 55, The worker process currently
only uses a parent-side timeout in the multiprocessing.Process created in the
caller (the p = multiprocessing.Process targeting _safe_exec_worker), which
doesn't prevent untrusted code from consuming unbounded CPU or memory; modify
the child setup inside _safe_exec_worker (or invoke resource.setrlimit in the
child immediately on entry) to call resource.setrlimit(resource.RLIMIT_AS,
(max_bytes, max_bytes)) and resource.setrlimit(resource.RLIMIT_CPU,
(max_seconds, max_seconds)) with appropriate sane caps, ensuring these calls run
before executing the untrusted code so they enforce per-process virtual memory
and CPU limits; keep the existing p.join()/p.kill() timeout as a fallback.


try:
result = queue.get(timeout=1)
except Exception:
logger.error("Contract execution crashed without result")
return False
result = queue.get(timeout=1)
if result["status"] != "success":
logger.error(f"Contract Execution Failed: {result.get('error')}")
return False

# Validate storage is JSON serializable
try:
json.dumps(result["storage"])
except (TypeError, ValueError):
logger.error("Contract storage not JSON serializable")
return False

# Commit updated storage only after successful execution
self.state.update_contract_storage(
contract_address,
result["storage"]
)

json.dumps(result["storage"], allow_nan=False) # Validate JSON serializability
self.state.update_contract_storage(contract_address, result["storage"])
Comment on lines +65 to +66
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.

⚠️ Potential issue | 🟠 Major

Reject non-dict storage before persisting it.

Serializability alone does not protect the storage invariant. A contract can rebind storage = 1; that passes this check, minichain/state.py:146-150 will persist it, and the next call blows up at Line 29 when dict(account.get("storage", {})) runs. Require result["storage"] to stay a dict.

Minimal guard
-            json.dumps(result["storage"], allow_nan=False) # Validate JSON serializability
-            self.state.update_contract_storage(contract_address, result["storage"])
+            if not isinstance(result["storage"], dict):
+                logger.error("Contract returned invalid storage type")
+                return False
+            json.dumps(result["storage"], allow_nan=False)  # Validate JSON serializability
+            self.state.update_contract_storage(contract_address, result["storage"])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@minichain/contract.py` around lines 65 - 66, The code currently validates
JSON serializability but doesn't enforce that result["storage"] is a dict;
before calling self.state.update_contract_storage(contract_address,
result["storage"]) (in the contract execution path that produces result), add a
guard that checks isinstance(result.get("storage"), dict) and reject non-dict
values (raise a TypeError/ValueError or return an error) so only mappings are
persisted; keep the existing json.dumps(...) check but perform the dict-type
validation first to preserve the storage invariant used by
dict(account.get("storage", {})) and reference result["storage"] and
self.state.update_contract_storage for the change location.

return True

except Exception as e:
except Exception:
logger.error("Contract Execution Failed", exc_info=True)
return False
Comment on lines +69 to 71
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.

🧹 Nitpick | 🔵 Trivial

Consider using logger.exception() for cleaner logging.

The static analysis suggests using logger.exception() instead of logger.error(..., exc_info=True). They are functionally equivalent, but exception() is more idiomatic.

♻️ Proposed fix
         except Exception:
-            logger.error("Contract Execution Failed", exc_info=True)
+            logger.exception("Contract Execution Failed")
             return False
🧰 Tools
🪛 Ruff (0.15.7)

[warning] 69-69: Logging .exception(...) should be used instead of .error(..., exc_info=True)

(G201)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@minichain/contract.py` around lines 68 - 70, Replace the idiomatic logging
call in the except block that currently uses logger.error("Contract Execution
Failed", exc_info=True) with logger.exception("Contract Execution Failed") to
log the stack trace more cleanly; keep the surrounding exception handler and the
return False intact so behavior doesn't change (look for the except block that
logs "Contract Execution Failed" in minichain/contract.py).


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
if isinstance(node, (ast.Import, ast.ImportFrom)):
logger.warning("Rejected contract code with import statement.")
return False
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Name) and node.func.id == 'type':
logger.warning("Rejected type() call.")
return False
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in {"getattr", "setattr", "delattr"}:
logger.warning(f"Rejected direct call to {node.func.id}.")
return False
if isinstance(node, ast.Constant) and isinstance(node.value, str):
if "__" in node.value:
logger.warning("Rejected string literal with double-underscore.")
return False
if isinstance(node, ast.JoinedStr): # f-strings
logger.warning("Rejected f-string usage.")
return False
for node in ast.walk(ast.parse(code)):
if isinstance(node, (ast.Import, ast.ImportFrom)): return False
if isinstance(node, ast.Attribute) and node.attr.startswith("__"): return False
if isinstance(node, ast.Name) and node.id.startswith("__"): return False
if isinstance(node, ast.Call) and getattr(node.func, "id", "") in {"type", "getattr", "setattr", "delattr"}: return False
if isinstance(node, ast.Constant) and isinstance(node.value, str) and "__" in node.value: return False
if isinstance(node, ast.Constant) and isinstance(node.value, (float, complex)): return False
if isinstance(node, ast.JoinedStr): return False
Comment on lines +75 to +82
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the contract.py file and verify its location
fd -t f "contract.py" | head -20

Repository: StabilityNexus/MiniChain

Length of output: 112


🏁 Script executed:

# Read the specific lines mentioned in the review (71-77)
cat -n minichain/contract.py | sed -n '65,80p'

Repository: StabilityNexus/MiniChain

Length of output: 1068


🏁 Script executed:

# Check if Ruff is configured and what rules are enabled
find . -maxdepth 3 -type f \( -name "ruff.toml" -o -name "pyproject.toml" -o -name ".ruff.toml" \) -exec cat {} \;

Repository: StabilityNexus/MiniChain

Length of output: 2494


Split these one-line if statements to multi-line blocks; E701 is active in Ruff.

Lines 72–77 each have the pattern if ...: return False, which violates Ruff's E701 rule. The Ruff configuration explicitly selects the "E7" rule set, which includes E701 and is not suppressed. This will cause lint failures in CI.

Example fix pattern
-                if isinstance(node, (ast.Import, ast.ImportFrom)): return False
+                if isinstance(node, (ast.Import, ast.ImportFrom)):
+                    return False
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for node in ast.walk(ast.parse(code)):
if isinstance(node, (ast.Import, ast.ImportFrom)): return False
if isinstance(node, ast.Attribute) and node.attr.startswith("__"): return False
if isinstance(node, ast.Name) and node.id.startswith("__"): return False
if isinstance(node, ast.Call) and getattr(node.func, "id", "") in {"type", "getattr", "setattr", "delattr"}: return False
if isinstance(node, ast.Constant) and isinstance(node.value, str) and "__" in node.value: return False
if isinstance(node, ast.JoinedStr): return False
for node in ast.walk(ast.parse(code)):
if isinstance(node, (ast.Import, ast.ImportFrom)):
return False
if isinstance(node, ast.Attribute) and node.attr.startswith("__"):
return False
if isinstance(node, ast.Name) and node.id.startswith("__"):
return False
if isinstance(node, ast.Call) and getattr(node.func, "id", "") in {"type", "getattr", "setattr", "delattr"}:
return False
if isinstance(node, ast.Constant) and isinstance(node.value, str) and "__" in node.value:
return False
if isinstance(node, ast.JoinedStr):
return False
🧰 Tools
🪛 Ruff (0.15.7)

[error] 72-72: Multiple statements on one line (colon)

(E701)


[error] 73-73: Multiple statements on one line (colon)

(E701)


[error] 74-74: Multiple statements on one line (colon)

(E701)


[error] 75-75: Multiple statements on one line (colon)

(E701)


[error] 76-76: Multiple statements on one line (colon)

(E701)


[error] 77-77: Multiple statements on one line (colon)

(E701)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@minichain/contract.py` around lines 71 - 77, Split each one-line conditional
that returns early into a multi-line if block to satisfy Ruff E701: replace
occurrences like `if isinstance(node, (ast.Import, ast.ImportFrom)): return
False` and the other checks for `ast.Attribute` (node.attr.startswith("__")),
`ast.Name` (node.id.startswith("__")), `ast.Call` (getattr(node.func, "id", "")
in {"type","getattr","setattr","delattr"}), `ast.Constant`
(isinstance(node.value, str) and "__" in node.value) and `ast.JoinedStr` with
the equivalent multi-line form (if <condition>:\n    return False) so that each
early-return is on its own line; locate these in the AST-walking block using the
ast.walk(ast.parse(code)) loop.

return True
except SyntaxError:
return False
return False
5 changes: 4 additions & 1 deletion minichain/p2p.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
import asyncio
import json
import logging
import re

from .serialization import canonical_json_hash
from .validators import is_valid_receiver

logger = logging.getLogger(__name__)

def is_valid_receiver(receiver):
return bool(re.fullmatch(r"[0-9a-fA-F]{40}|[0-9a-fA-F]{64}", receiver))
Comment on lines +17 to +18
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.

🧹 Nitpick | 🔵 Trivial

Consider adding defensive type checking for robustness.

re.fullmatch() raises TypeError if receiver is None or not a string. While current callers guard against this (line 151 checks for None, main.py always passes strings from CLI parsing), the function could be more defensive for future use cases.

♻️ Optional defensive improvement
 def is_valid_receiver(receiver):
+    if not isinstance(receiver, str):
+        return False
     return bool(re.fullmatch(r"[0-9a-fA-F]{40}|[0-9a-fA-F]{64}", receiver))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def is_valid_receiver(receiver):
return bool(re.fullmatch(r"[0-9a-fA-F]{40}|[0-9a-fA-F]{64}", receiver))
def is_valid_receiver(receiver):
if not isinstance(receiver, str):
return False
return bool(re.fullmatch(r"[0-9a-fA-F]{40}|[0-9a-fA-F]{64}", receiver))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@minichain/p2p.py` around lines 17 - 18, The is_valid_receiver function should
defensively handle non-string inputs to avoid re.fullmatch raising TypeError;
update is_valid_receiver to first check that receiver is an instance of str (or
not None) and immediately return False for other types, then perform the
existing re.fullmatch check (reference function is_valid_receiver).


TOPIC = "minichain-global"
SUPPORTED_MESSAGE_TYPES = {"sync", "tx", "block"}

Expand Down
5 changes: 0 additions & 5 deletions minichain/validators.py

This file was deleted.

Loading