Skip to content

Feat/fork choice#100

Open
SIDDHANTCOOKIE wants to merge 2 commits into
mainfrom
feat/fork-choice
Open

Feat/fork choice#100
SIDDHANTCOOKIE wants to merge 2 commits into
mainfrom
feat/fork-choice

Conversation

@SIDDHANTCOOKIE

@SIDDHANTCOOKIE SIDDHANTCOOKIE commented Jun 16, 2026

Copy link
Copy Markdown
Member

Addressed Issues:

Fixes #(TODO:issue number)

Screenshots/Recordings:

TODO: If applicable, add screenshots or recordings that demonstrate the interface before and after the changes.

Additional Notes:

AI Usage Disclosure:

We encourage contributors to use AI tools responsibly when creating Pull Requests. While AI can be a valuable aid, it is essential to ensure that your contributions meet the task requirements, build successfully, include relevant tests, and pass all linters. Submissions that do not meet these standards may be closed without warning to maintain the quality and integrity of the project. Please take the time to understand the changes you are proposing and their impact. AI slop is strongly discouraged and may lead to banning and blocking. Do not spam our repos with AI slop.

Check one of the checkboxes below:

  • This PR does not contain AI-generated code at all.
  • This PR contains AI-generated code. I have read the AI Usage Policy and this PR complies with this policy. I have tested the code locally and I am responsible for it.

I have used the following AI models and tools: TODO

Checklist

  • My PR addresses a single issue, fixes a single bug or makes a single improvement.
  • My code follows the project's code style and conventions
  • If applicable, I have made corresponding changes or additions to the documentation
  • If applicable, I have made corresponding changes or additions to tests
  • My changes generate no new warnings or errors
  • I have joined the Discord server and I will share a link to this PR with the project maintainers there
  • I have read the Contribution Guidelines
  • Once I submit my PR, CodeRabbit AI will automatically review it and I will address CodeRabbit's comments.
  • I have filled this PR template completely and carefully, and I understand that my PR may be closed without review otherwise.

Summary by CodeRabbit

Release Notes

  • New Features

    • Nodes now automatically request chain synchronization when receiving blocks ahead of their local chain.
    • Nodes can resolve blockchain forks by comparing chain work and reorganizing to the heavier chain, re-adding orphaned transactions to the transaction pool.
  • Tests

    • Added comprehensive test suite for fork resolution and chain reorganization scenarios.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds a full chain-sync and fork-resolution subsystem: State gains snapshot/restore methods; Blockchain stores a genesis snapshot, computes cumulative PoW via get_total_work, and reorgs via resolve_conflicts; Mempool is refactored to a sorted list; P2PNetwork gains chain_request/chain_response message types; and make_network_handler orchestrates sync by triggering requests on block rejection and replaying orphans on successful reorg.

Changes

Chain Synchronization and Fork Resolution

Layer / File(s) Summary
State snapshot/restore primitives
minichain/state.py
State.snapshot() and State.restore() provide deep-copy save and load of account state, enabling deterministic reorg replays.
Blockchain genesis snapshot, PoW accumulation, and resolve_conflicts
minichain/chain.py
Genesis state is snapshotted on init; get_total_work() computes cumulative PoW; resolve_conflicts() validates and replays a competing chain, swaps local chain/state on success, and returns orphaned transactions.
Mempool list-based storage refactor
minichain/mempool.py
Backing storage changes from a per-sender map to a single ordered _list; add_transaction, get_transactions_for_block, remove_transactions, and __len__ are rewritten against the new structure.
P2P protocol extension: chain_request and chain_response
minichain/p2p.py
SUPPORTED_MESSAGE_TYPES is extended; payload validators for chain_request and chain_response are added and wired into the dispatch map; P2PNetwork gains broadcast_chain_request() and send_chain_response().
Message handler wiring and chain-sync orchestration
main.py
make_network_handler gains the network parameter; rejected ahead-of-tip blocks trigger broadcast_chain_request; chain_request replies with the local chain; chain_response calls resolve_conflicts and re-adds orphans to the mempool; run_node passes network into the handler.
Reorg test suite
tests/test_reorg.py
Three tests cover heavier-chain adoption with zero orphans, reorg with one orphaned transaction, and rejection of a lighter competing chain, backed by a genesis_file fixture.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(173, 216, 230, 0.5)
    Note over NodeA,NodeB: Block received ahead of local tip
    NodeB->>NodeA: block message (rejected – ahead of tip)
    NodeA->>NodeB: broadcast_chain_request()
  end
  rect rgba(144, 238, 144, 0.5)
    Note over NodeA,NodeB: Chain exchange
    NodeB->>NodeA: chain_request
    NodeA->>NodeB: send_chain_response(local blocks)
    NodeB->>NodeA: chain_response(blocks)
  end
  rect rgba(255, 215, 0, 0.5)
    Note over NodeA,Blockchain: Fork resolution
    NodeA->>Blockchain: resolve_conflicts(new_chain_list)
    Blockchain->>State: restore(_genesis_state_snapshot)
    Blockchain->>Blockchain: replay & validate each block
    Blockchain-->>NodeA: (True, orphans)
    NodeA->>Mempool: add_transaction per orphan
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • StabilityNexus/MiniChain#70: Overlapping refactor of minichain/mempool.py changing the same add_transaction, get_transactions_for_block, remove_transactions, and __len__ methods.
  • StabilityNexus/MiniChain#60: Also refactors minichain/mempool.py to use a sorted list and changes replacement semantics for (sender, nonce) transactions.
  • StabilityNexus/MiniChain#46: Modifies the same minichain/p2p.py message-dispatch and broadcast layer that this PR extends with chain_request/chain_response.

Suggested labels

Python Lang

Suggested reviewers

  • Zahnentferner

Poem

🐇 Hop, hop, the chains collide,
Two forks racing, who will guide?
The heavier one wins the race,
Orphaned txs find their place.
Snapshot, restore, replay — done!
The longest chain has finally won! 🏆

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Feat/fork choice' is partially related to the changeset. It correctly identifies the fork-choice rule implementation as a main focus, but uses a vague 'Feat/' prefix and doesn't clearly summarize what the changes accomplish. Consider revising to a more descriptive title like 'Add fork-choice rule with chain conflict resolution' to better convey the specific functionality being implemented.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/fork-choice

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@SIDDHANTCOOKIE SIDDHANTCOOKIE self-assigned this Jun 16, 2026
@SIDDHANTCOOKIE SIDDHANTCOOKIE removed their assignment Jun 16, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
minichain/p2p.py (1)

272-277: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

chain_request messages bypass duplicate detection, enabling spam attacks.

_message_id returns None for chain_request and chain_response, so _is_duplicate always returns False for these types. A malicious peer can repeatedly send chain_request messages, causing the node to serialize and broadcast its entire chain each time, wasting CPU and bandwidth.

Consider adding rate limiting or tracking seen chain_request messages per peer address.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@minichain/p2p.py` around lines 272 - 277, The _message_id method in p2p.py
returns None for chain_request and chain_response message types, bypassing
duplicate detection and allowing spam attacks. Extend the _message_id method to
handle chain_request and chain_response types by generating or extracting a
message identifier from the payload (similar to how tx uses canonical_json_hash
or block uses payload["hash"]), so that duplicate detection via _is_duplicate
can properly identify and filter repeated messages from malicious peers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@main.py`:
- Around line 183-188: The code in the chain reorganization block only logs a
message when resolve_conflicts succeeds, but provides no feedback when it fails.
Add a log statement in an else block or separate conditional that triggers when
the success variable returned from chain.resolve_conflicts(new_chain) is False,
so that reorg failures are properly recorded for debugging sync issues.
- Line 164: The asyncio.create_task call for network.broadcast_chain_request()
does not store the returned task reference, which can cause exceptions to be
silently lost and allows the task to potentially be garbage collected. Store the
task reference in a variable or maintain it in a task collection (such as a set)
to ensure the task is not garbage collected and exceptions can be properly
handled. This will allow you to await the task or add callbacks to handle any
exceptions that occur during execution.
- Around line 168-172: The chain_request handler at the elif msg_type ==
"chain_request" block is using network._broadcast_raw() to send the chain
response to all peers instead of responding only to the requesting peer. To fix
this, modify the P2P layer to pass the requesting peer's writer in the message
data alongside _peer_addr, then update the handler to receive that writer and
use send_chain_response() with that specific peer's writer to send the response
directly to the requester instead of calling network._broadcast_raw(). This
avoids unnecessary network traffic by sending the chain only to the peer that
requested it.

In `@minichain/chain.py`:
- Around line 104-113: The get_total_work method has two security and
correctness issues in the sum calculation. First, the expression 2 **
(block.difficulty or 1) treats a valid difficulty value of 0 as falsy and
incorrectly substitutes 1, when 0 should be treated as a legitimate difficulty
value. Second, the computation allows unbounded peer-controlled exponents which
can cause expensive big-integer arithmetic during chain-sync operations. Fix
this by replacing the or fallback with explicit validation: ensure difficulty
has a sensible default only when missing or None (not when it is 0), and add a
maximum cap on the difficulty exponent to prevent peer-controlled DoS attacks.
The cap should be reasonable for your consensus algorithm while rejecting
malformed or malicious difficulty values.
- Around line 233-234: The assignment `self.chain = new_chain_list` on line 233
creates an alias to the external list instead of copying it, which means
external mutations to the caller's list can modify the node's chain without
replaying state. Replace this assignment with a copy of the list (using either
the copy() method or the list() constructor) to ensure the node maintains an
independent chain that cannot be mutated by the caller's modifications to their
original list.

In `@minichain/p2p.py`:
- Line 18: The class docstring does not reflect the new message types added to
SUPPORTED_MESSAGE_TYPES. Update the docstring that documents the JSON wire
format to include the two new message types chain_request and chain_response
alongside the existing sync, tx, and block types. Ensure the docstring
accurately represents all message types currently defined in
SUPPORTED_MESSAGE_TYPES.
- Around line 417-418: In the exception handler catching Exception for the send
chain response failure, replace the logger.error call with logger.exception. The
logger.exception method automatically preserves and includes the full stack
trace in the log output, whereas logger.error only logs the error message
itself. This change will provide the traceback needed for debugging network
issues.
- Around line 236-244: The _validate_chain_response method validates the
structure of the chain response but lacks a size limit check on the blocks list,
creating a DoS vulnerability. After verifying that payload["blocks"] is a list,
add a check to ensure the list length does not exceed a reasonable maximum
threshold (define an appropriate constant for max blocks allowed). Insert this
length validation before the loop that iterates through payload["blocks"] to
validate each block.

---

Outside diff comments:
In `@minichain/p2p.py`:
- Around line 272-277: The _message_id method in p2p.py returns None for
chain_request and chain_response message types, bypassing duplicate detection
and allowing spam attacks. Extend the _message_id method to handle chain_request
and chain_response types by generating or extracting a message identifier from
the payload (similar to how tx uses canonical_json_hash or block uses
payload["hash"]), so that duplicate detection via _is_duplicate can properly
identify and filter repeated messages from malicious peers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a0c262fe-d745-4e0b-bfc7-3b1efae79478

📥 Commits

Reviewing files that changed from the base of the PR and between eb8e75d and fcb814a.

📒 Files selected for processing (6)
  • main.py
  • minichain/chain.py
  • minichain/mempool.py
  • minichain/p2p.py
  • minichain/state.py
  • tests/test_reorg.py

Comment thread main.py
logger.warning("📥 Received Block #%s — rejected", block.index)
if block.index > chain.last_block.index:
logger.warning("📥 Received Block #%s — ahead of us (tip: %s). Requesting chain sync...", block.index, chain.last_block.index)
asyncio.create_task(network.broadcast_chain_request())

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 | ⚡ Quick win

Fire-and-forget asyncio.create_task may silently lose exceptions.

The task reference is not stored, so if the task fails with an exception, it will be silently swallowed. Additionally, the task could theoretically be garbage collected before completion (though CPython currently keeps a reference).

Consider storing the task reference or using a background task set:

♻️ Proposed fix
+# At module level or in handler closure
+_background_tasks = set()
+
 # In the handler:
-                    asyncio.create_task(network.broadcast_chain_request())
+                    task = asyncio.create_task(network.broadcast_chain_request())
+                    _background_tasks.add(task)
+                    task.add_done_callback(_background_tasks.discard)
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 164-164: Store a reference to the return value of asyncio.create_task

(RUF006)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main.py` at line 164, The asyncio.create_task call for
network.broadcast_chain_request() does not store the returned task reference,
which can cause exceptions to be silently lost and allows the task to
potentially be garbage collected. Store the task reference in a variable or
maintain it in a task collection (such as a set) to ensure the task is not
garbage collected and exceptions can be properly handled. This will allow you to
await the task or add callbacks to handle any exceptions that occur during
execution.

Source: Linters/SAST tools

Comment thread main.py
Comment on lines +168 to +172
elif msg_type == "chain_request":
logger.info("📡 Peer requested chain sync. Broadcasting our chain...")
blocks_dicts = [b.to_dict() for b in chain.chain]
payload = {"type": "chain_response", "data": {"blocks": blocks_dicts}}
asyncio.create_task(network._broadcast_raw(payload))

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 | 🏗️ Heavy lift

chain_request handler broadcasts to all peers instead of responding to the requester.

When a peer sends a chain_request, the handler calls network._broadcast_raw(payload) which sends the chain to all connected peers, not just the requesting peer. This is inefficient and could cause unnecessary network traffic and potential amplification.

The handler should use send_chain_response with the specific peer's writer. However, the current handler signature doesn't provide access to the peer's writer. Consider passing the writer through the message data or adding a response mechanism.

🐛 Proposed approach

One solution is to include the writer in the callback data (requires P2P layer changes):

         elif msg_type == "chain_request":
             logger.info("📡 Peer requested chain sync. Broadcasting our chain...")
             blocks_dicts = [b.to_dict() for b in chain.chain]
-            payload = {"type": "chain_response", "data": {"blocks": blocks_dicts}}
-            asyncio.create_task(network._broadcast_raw(payload))
+            writer = data.get("_writer")
+            if writer:
+                asyncio.create_task(network.send_chain_response(blocks_dicts, writer))
+            else:
+                logger.warning("Cannot respond to chain_request: no writer available")

This requires the P2P layer to pass the writer in the message data alongside _peer_addr.

🧰 Tools
🪛 Ruff (0.15.17)

[warning] 172-172: Store a reference to the return value of asyncio.create_task

(RUF006)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main.py` around lines 168 - 172, The chain_request handler at the elif
msg_type == "chain_request" block is using network._broadcast_raw() to send the
chain response to all peers instead of responding only to the requesting peer.
To fix this, modify the P2P layer to pass the requesting peer's writer in the
message data alongside _peer_addr, then update the handler to receive that
writer and use send_chain_response() with that specific peer's writer to send
the response directly to the requester instead of calling
network._broadcast_raw(). This avoids unnecessary network traffic by sending the
chain only to the peer that requested it.

Comment thread main.py
Comment on lines +183 to +188
if new_chain:
success, orphans = chain.resolve_conflicts(new_chain)
if success:
logger.info("🔄 Reorg complete! Restoring %d orphaned txs to mempool.", len(orphans))
for tx in orphans:
mempool.add_transaction(tx)

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 | ⚡ Quick win

Missing log when resolve_conflicts returns success=False.

When the reorg fails (e.g., the incoming chain has less total work), there's no log message indicating rejection. This makes debugging sync issues harder.

♻️ Proposed fix
             if new_chain:
                 success, orphans = chain.resolve_conflicts(new_chain)
                 if success:
                     logger.info("🔄 Reorg complete! Restoring %d orphaned txs to mempool.", len(orphans))
                     for tx in orphans:
                         mempool.add_transaction(tx)
+                else:
+                    logger.info("🔄 Received chain rejected (not heavier than local)")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main.py` around lines 183 - 188, The code in the chain reorganization block
only logs a message when resolve_conflicts succeeds, but provides no feedback
when it fails. Add a log statement in an else block or separate conditional that
triggers when the success variable returned from
chain.resolve_conflicts(new_chain) is False, so that reorg failures are properly
recorded for debugging sync issues.

Comment thread minichain/chain.py
Comment on lines +104 to +113
def get_total_work(self, chain_list=None):
"""
Calculates the cumulative PoW of a chain.
Work is proportional to 2^difficulty.
"""
if chain_list is None:
with self._lock:
chain_list = self.chain
return sum(2 ** (block.difficulty or 1) for block in chain_list)

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 | ⚡ Quick win

Harden cumulative-work computation against malformed peer difficulty values.

2 ** (block.difficulty or 1) has two problems: it incorrectly treats valid difficulty=0 as 1, and it allows unbounded peer-controlled exponents during conflict checks, which can cause expensive big-int work in the chain-sync path.

Proposed fix
     def get_total_work(self, chain_list=None):
         """
         Calculates the cumulative PoW of a chain.
         Work is proportional to 2^difficulty.
         """
         if chain_list is None:
             with self._lock:
                 chain_list = self.chain
-        return sum(2 ** (block.difficulty or 1) for block in chain_list)
+        total_work = 0
+        for block in chain_list:
+            difficulty = block.difficulty
+            if not isinstance(difficulty, int) or difficulty < 0 or difficulty > 64:
+                raise ValueError(f"Invalid block difficulty: {difficulty}")
+            total_work += 1 << difficulty
+        return total_work
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@minichain/chain.py` around lines 104 - 113, The get_total_work method has two
security and correctness issues in the sum calculation. First, the expression 2
** (block.difficulty or 1) treats a valid difficulty value of 0 as falsy and
incorrectly substitutes 1, when 0 should be treated as a legitimate difficulty
value. Second, the computation allows unbounded peer-controlled exponents which
can cause expensive big-integer arithmetic during chain-sync operations. Fix
this by replacing the or fallback with explicit validation: ensure difficulty
has a sensible default only when missing or None (not when it is 0), and add a
maximum cap on the difficulty exponent to prevent peer-controlled DoS attacks.
The cap should be reasonable for your consensus algorithm while rejecting
malformed or malicious difficulty values.

Comment thread minichain/chain.py
Comment on lines +233 to +234
self.chain = new_chain_list
self.state = temp_state

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 | ⚡ Quick win

Avoid aliasing external chain lists when committing a reorg.

Assigning self.chain = new_chain_list keeps a shared list reference. If the caller passed a live chain object (for example tests/test_reorg.py Line 89), later appends on that source chain can mutate this node’s chain without replaying state, breaking chain/state consistency.

Proposed fix
-            self.chain = new_chain_list
+            self.chain = list(new_chain_list)
             self.state = temp_state
📝 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
self.chain = new_chain_list
self.state = temp_state
self.chain = list(new_chain_list)
self.state = temp_state
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@minichain/chain.py` around lines 233 - 234, The assignment `self.chain =
new_chain_list` on line 233 creates an alias to the external list instead of
copying it, which means external mutations to the caller's list can modify the
node's chain without replaying state. Replace this assignment with a copy of the
list (using either the copy() method or the list() constructor) to ensure the
node maintains an independent chain that cannot be mutated by the caller's
modifications to their original list.

Comment thread minichain/p2p.py

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

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 | 💤 Low value

Stale docstring does not reflect new message types.

The class docstring at line 26-27 still documents only "sync" | "tx" | "block" for the JSON wire format, but chain_request and chain_response are now supported. Update the docstring to include the new message types.

Also applies to: 26-27

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@minichain/p2p.py` at line 18, The class docstring does not reflect the new
message types added to SUPPORTED_MESSAGE_TYPES. Update the docstring that
documents the JSON wire format to include the two new message types
chain_request and chain_response alongside the existing sync, tx, and block
types. Ensure the docstring accurately represents all message types currently
defined in SUPPORTED_MESSAGE_TYPES.

Comment thread minichain/p2p.py
Comment on lines +236 to +244
def _validate_chain_response(self, payload):
if not isinstance(payload, dict) or "blocks" not in payload:
return False
if not isinstance(payload["blocks"], list):
return False
for block_payload in payload["blocks"]:
if not self._validate_block_payload(block_payload):
return False
return True

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 | ⚡ Quick win

Missing size limit on blocks list creates potential DoS vector.

_validate_chain_response validates block structure but doesn't limit the number of blocks. A malicious peer could send a chain_response with an extremely large blocks list, causing memory exhaustion or CPU starvation during validation.

Consider adding a reasonable upper bound:

🛡️ Proposed fix
+MAX_CHAIN_RESPONSE_BLOCKS = 500  # or appropriate limit
+
 def _validate_chain_response(self, payload):
     if not isinstance(payload, dict) or "blocks" not in payload:
         return False
     if not isinstance(payload["blocks"], list):
         return False
+    if len(payload["blocks"]) > MAX_CHAIN_RESPONSE_BLOCKS:
+        return False
     for block_payload in payload["blocks"]:
         if not self._validate_block_payload(block_payload):
             return False
     return True
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 236-236: Missing return type annotation for private function _validate_chain_response

Add return type annotation: bool

(ANN202)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@minichain/p2p.py` around lines 236 - 244, The _validate_chain_response method
validates the structure of the chain response but lacks a size limit check on
the blocks list, creating a DoS vulnerability. After verifying that
payload["blocks"] is a list, add a check to ensure the list length does not
exceed a reasonable maximum threshold (define an appropriate constant for max
blocks allowed). Insert this length validation before the loop that iterates
through payload["blocks"] to validate each block.

Comment thread minichain/p2p.py
Comment on lines +417 to +418
except Exception as e:
logger.error("Network: Failed to send chain response: %s", e)

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 | ⚡ Quick win

Use logger.exception to preserve stack trace on send failure.

The broad Exception catch loses the stack trace. Using logger.exception instead of logger.error will preserve the traceback for debugging network issues.

♻️ Proposed fix
         except Exception as e:
-            logger.error("Network: Failed to send chain response: %s", e)
+            logger.exception("Network: Failed to send chain response: %s", e)
📝 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
except Exception as e:
logger.error("Network: Failed to send chain response: %s", e)
except Exception as e:
logger.exception("Network: Failed to send chain response: %s", e)
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 417-417: Do not catch blind exception: Exception

(BLE001)


[warning] 418-418: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@minichain/p2p.py` around lines 417 - 418, In the exception handler catching
Exception for the send chain response failure, replace the logger.error call
with logger.exception. The logger.exception method automatically preserves and
includes the full stack trace in the log output, whereas logger.error only logs
the error message itself. This change will provide the traceback needed for
debugging network issues.

Source: Linters/SAST tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant