Skip to content

Feat/libp2p#102

Open
SIDDHANTCOOKIE wants to merge 8 commits into
mainfrom
feat/libp2p
Open

Feat/libp2p#102
SIDDHANTCOOKIE wants to merge 8 commits into
mainfrom
feat/libp2p

Conversation

@SIDDHANTCOOKIE

@SIDDHANTCOOKIE SIDDHANTCOOKIE commented Jun 21, 2026

Copy link
Copy Markdown
Member

Addressed Issues:

Implement libp2p

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

    • Added an HTTP JSON-RPC server for chain queries and transaction submission.
    • Upgraded P2P networking to libp2p with multiaddress-based peer connections and improved peer signaling.
    • Implemented chunked chain synchronization with total-work conflict resolution, reorg handling, and automatic catch-up.
    • Smart contracts can now emit outbound transfers to external addresses.
    • CLI networking accepts a single multiaddress for connections.
  • Bug Fixes

    • Enforced chain-id protection for transactions.
    • Added atomic rollback when contract transfers can’t be funded.
  • Tests

    • Added/expanded coverage for contract transfers, reorg resolution, transaction signing, protocol hardening, and JSON-RPC.

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR replaces the asyncio TCP P2P transport with libp2p/trio, adds a JSON-RPC HTTP server, rewrites chain conflict resolution to use total-work comparison with full state replay, adds contract-to-address outbound transfer support with atomic rollback, and refactors the mempool from a nested dict to a single sorted list.

Changes

MiniChain Protocol Upgrade

Layer / File(s) Summary
Transaction chain_id field and state verification
minichain/transaction.py, minichain/chain.py, minichain/state.py, genesis.json, tests/test_transaction_signing.py
Transaction now includes chain_id in serialization and signing (defaulting to "minichain-default"); Blockchain reads and propagates chain_id from genesis config throughout the chain and state; State.verify_transaction_logic() rejects transactions with mismatched chain_id; cross-chain rejection is tested.
Mempool sorted-list refactor
minichain/mempool.py
Mempool storage changes from a per-sender/per-nonce dict to a single sorted list; add_transaction performs nonce-window and fee-based insertion; get_transactions_for_block returns a list slice; remove_transactions filters the list; __len__ reports list size.
State snapshot/restore and contract transfer application
minichain/state.py, minichain/contract.py
State.snapshot() and State.restore() enable transactional rollback; State.apply_transaction() processes contract-emitted transfers, validates receiver balance coverage, commits storage, and applies debit/credit accounting with rollback on insufficiency; contract worker collects transfer_out calls and returns them in the success payload; ContractMachine.execute no longer commits storage directly.
Contract transfer end-to-end tests
tests/test_contract_transfers.py
Three test cases validate successful transfers, insufficient-balance rollback with atomic account preservation and storage non-update, and incoming-value forwarding via msg['value'].
Chain reorg: total-work selection and state replay
minichain/chain.py, tests/test_reorg.py
Blockchain stores _genesis_state_snapshot for deterministic reorg rebuilds; get_total_work() sums 2^difficulty across a chain; resolve_conflicts() rejects lighter chains, verifies incoming genesis hash, replays all post-genesis blocks against a fresh state, validates receipt/state roots and miner rewards including gas fees, then swaps chain+state and returns orphaned transactions; three pytest tests cover adoption, orphan detection, and lighter-chain rejection.
P2P transport: libp2p/trio replacement
minichain/p2p.py, requirements.txt, tests/test_protocol_hardening.py
P2PNetwork uses libp2p with a trio background thread bridged to asyncio via queues; lifecycle methods launch trio and handle stop/connect via multiaddr strings; de-duplication uses per-type seen sets; broadcast_chain_request and send_chain_response are added; peer_count reflects libp2p stream count; protocol-hardening tests are updated to validate object-level deserialization; new dependencies libp2p, aiohttp, and multiaddr are added.
JSON-RPC server
minichain/rpc.py, tests/test_rpc.py
New JSONRPCServer backed by aiohttp handles POST requests with single or batch JSON-RPC 2.0 payloads; implements mc_blockNumber, mc_getBlockByNumber (with "latest"), mc_getBalance, and mc_sendTransaction (with signature verification and async broadcast); returns standard error codes for parse errors, invalid requests, unknown methods, and internal errors; five HTTP async tests verify all methods and error cases.
main.py: handler, RPC startup, multiaddr CLI, and chain sync
main.py
make_network_handler gains a network parameter and wraps tx/block parsing in try/except; chain_request/chain_response flows are added to invoke resolve_conflicts and re-admit orphans; CLI help and commands use multiaddr format; interactive send/deploy/call transactions include chain_id parameter; run_node instantiates and starts JSONRPCServer on a derived port with a hello-handshake peer callback; shutdown cancels RPC task before stopping the P2P network.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant JSONRPCServer
  participant Blockchain
  participant Mempool
  participant P2PNetwork

  rect rgba(70, 130, 180, 0.5)
    Note over Client,JSONRPCServer: JSON-RPC HTTP POST
    Client->>JSONRPCServer: POST {method: mc_sendTransaction, params: [tx_dict], id}
    JSONRPCServer->>JSONRPCServer: Transaction.from_dict / verify()
    JSONRPCServer->>Mempool: add_transaction(tx)
    Mempool-->>JSONRPCServer: accepted
    JSONRPCServer->>P2PNetwork: broadcast_transaction(tx) [async]
    JSONRPCServer-->>Client: {result: tx_id, id}
  end

  rect rgba(60, 179, 113, 0.5)
    Note over P2PNetwork,Blockchain: Chain sync via chain_request/response
    P2PNetwork->>P2PNetwork: receives chain_request
    P2PNetwork->>P2PNetwork: send_chain_response(blocks)
    P2PNetwork->>Blockchain: resolve_conflicts(new_chain_list)
    Blockchain->>Blockchain: get_total_work comparison
    Blockchain->>Blockchain: replay blocks on temp_state
    Blockchain-->>P2PNetwork: (True, orphans)
    P2PNetwork->>Mempool: add_transaction(orphan) per orphan
  end

  rect rgba(176, 196, 222, 0.5)
    Note over Mempool,Blockchain: Contract transfer with rollback
    Mempool->>Blockchain: include tx in block
    Blockchain->>Blockchain: apply_transaction(tx)
    Blockchain->>Blockchain: contract execution → transfers
    Blockchain->>Blockchain: validate transfer total vs balance
    alt insufficient balance
      Blockchain->>Blockchain: restore(snapshot) / rollback
    else valid
      Blockchain->>Blockchain: debit receiver, credit recipients
    end
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • StabilityNexus/MiniChain#58: Modifies transaction serialization and signing (to_signing_dict, tx_id) and P2P message de-duplication via canonical payload hashing, which directly overlaps with this PR's chain_id field integration and libp2p message validation changes.
  • StabilityNexus/MiniChain#91: Introduces receipt/state root validation, miner reward calculation including transaction gas fees, which this PR's resolve_conflicts implementation builds upon during block replay and validation.
  • StabilityNexus/MiniChain#92: Overhauled contract execution and state/receipt handling; this PR extends that foundation by adding contract-emitted transfer_out tracking and atomic transfer application with rollback.

Suggested labels

Python Lang

Suggested reviewers

  • Zahnentferner

Poem

🐇 Hoppity-hop through libp2p's threads,
Trio carries messages while asyncio spreads,
Chain reorgs weighed by total work's might,
Contracts send coins—all transfers done right!
JSON-RPC lights guide the way,
Mempool sorted for a faster day,
The protocol hops forward—all safe, all bright! 🌟

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The pull request title 'Feat/libp2p' is vague and uses a generic format that does not clearly convey what specific functionality was added or changed. Replace with a more descriptive title that summarizes the main change, such as 'Replace TCP networking with libp2p-based P2P protocol' or 'Implement libp2p network layer with chain synchronization'.
✅ 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/libp2p

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 OpenGrep (1.23.0)
main.py

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

[00.25][ERROR]: unable to find a config; path .coderabbit-opengrep-fallback.yml does not exist

minichain/contract.py

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

[00.27][ERROR]: unable to find a config; path .coderabbit-opengrep-fallback.yml does not exist

minichain/p2p.py

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

[00.42][ERROR]: unable to find a config; path .coderabbit-opengrep-fallback.yml does not exist

  • 1 others

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.

@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: 17

Caution

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

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

13-16: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject invalid amount/fee values before admitting transactions.

add_transaction() is now a shared ingress for P2P/RPC, but it only checks the signature. A signed transaction with a negative or non-integer amount/fee can enter the sorted list and poison block assembly; validate these cheap invariants before insertion.

🛡️ Proposed validation
 def add_transaction(self, tx):
+    amount = getattr(tx, "amount", None)
+    fee = getattr(tx, "fee", 0)
+    if not isinstance(amount, int) or amount < 0:
+        logger.warning("Mempool: Invalid amount rejected")
+        return False
+    if not isinstance(fee, int) or fee < 0:
+        logger.warning("Mempool: Invalid fee rejected")
+        return False
+
     if not tx.verify():
         logger.warning("Mempool: Invalid signature rejected")
         return False
🤖 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/mempool.py` around lines 13 - 16, The add_transaction() method
currently only validates the transaction signature but does not check if the
amount and fee values are valid. Add validation before the transaction is
admitted to check that both amount and fee are non-negative integers. If either
value fails validation, log an appropriate warning and return False to reject
the transaction, similar to the existing signature verification check.
🤖 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 441-443: The RPC port calculation using `rpc_port = 8545 + (port -
9000)` can produce invalid port numbers outside the valid range of 1-65535. Add
validation logic after the rpc_port assignment and before the
asyncio.create_task call to verify that rpc_port is within the valid range (1 to
65535). If the calculated rpc_port is invalid, raise an appropriate exception or
log an error and exit to prevent the rpc_server.start call from failing at
runtime.

In `@minichain/chain.py`:
- Around line 233-234: The assignment `self.chain = new_chain_list` at the chain
assignment line creates an alias to the caller's list instead of making an
independent copy. This causes mutations to the caller's list to affect the
object's chain without proper state replay. Replace the direct assignment with a
copy operation to ensure the chain list is independent from the caller's list.
- Around line 219-226: The reorg replay validation in resolve_conflicts() only
verifies that the computed receipt root matches the block's receipt root, but
fails to validate the actual receipt payload contents like add_block() does.
This allows blocks with mismatched receipts to be accepted through reorg when
normal block import would reject them. Add a validation check after computing
receipts to ensure block.receipts matches the computed receipts list directly,
not just their root hash. This additional payload validation should occur
alongside the existing receipt root and state root checks to maintain parity
with add_block() validation logic.
- Around line 104-112: The current implementation uses `block.difficulty or 1`
in the return statement of the get_total_work method, which treats both None and
0 as falsy values and converts them to 1. This causes difficulty-0 blocks to be
counted as 2^1 instead of 2^0, making them indistinguishable from difficulty-1
blocks. Replace the `or 1` fallback with an explicit None check using a
conditional expression (like `block.difficulty if block.difficulty is not None
else 1`) so that an explicit difficulty value of 0 is preserved and calculates
correctly as 2^0 = 1, while only defaulting to 1 when difficulty is actually
None.
- Around line 228-231: The orphans list being returned contains transactions
with stale nonces that are unmineable and should be filtered out. Before
returning the orphans list after computing it from old_txs and new_tx_ids,
filter out any orphaned transactions where the sender has already advanced to a
higher nonce in the new chain. Build a mapping of senders to their maximum nonce
in new_chain_list by iterating through all transactions in the new chain blocks,
then exclude any orphaned transactions where the sender's current nonce in the
new chain is already at or beyond the orphaned transaction's nonce value.

In `@minichain/contract.py`:
- Around line 50-55: In the transfer_out function, the address validation
currently only checks if the address is a string type, which allows malformed
addresses to be persisted. Add format validation to ensure the address follows a
canonical hex format using regex pattern matching. Import the re module at the
top of the file, then add a validation check after the isinstance type check
that uses a regex pattern to validate the address conforms to the expected
format before allowing it to be appended to the transfers list.

In `@minichain/p2p.py`:
- Around line 58-61: In the _message_id method, when msg_type is "block", the
code directly accesses payload["hash"] which raises KeyError if the key is
missing in malformed payloads. Replace the direct dictionary access
payload["hash"] with payload.get("hash") to safely extract the hash value,
returning None if the key does not exist. This prevents crashes during duplicate
message checks and allows the code to handle hostile or malformed block payloads
gracefully.
- Around line 46-47: The asyncio reader task created by
asyncio.create_task(self._asyncio_reader()) on line 47 is not stored, making it
impossible to stop during shutdown. Store the task reference in an instance
variable when creating it, then explicitly cancel or await stopping that task in
the shutdown logic around lines 52-53 along with the trio side shutdown. Apply
the same pattern to the other instances mentioned at lines 50-53 and 95-98 to
ensure all asyncio tasks are properly tracked and cancelled during graceful
shutdown.
- Around line 106-117: The PEER_CONNECTED handler is incorrectly broadcasting
callback-generated data to all peers via _broadcast_raw when it should only be
sent to the newly connected peer. Remove the _broadcast_raw call that forwards
the JSON data from the MockWriter to all peers, and instead send the response
data directly to the peer that initiated the connection. The callback's output
is intended for that specific peer only, not for network-wide distribution.
- Around line 133-142: The stream reading loop splits each 4096-byte read()
chunk by newline without handling fragmented JSON lines that span multiple
reads. This causes incomplete lines to fail parsing and be silently dropped. Fix
this by maintaining a persistent buffer across loop iterations: append newly
read data to the buffer, split by newline, parse only complete lines (those that
ended with a newline), and keep any incomplete line (without trailing newline)
in the buffer for the next read() call to complete it.

In `@minichain/rpc.py`:
- Line 84: The response handler at line 84 always returns a JSON-RPC response
regardless of whether the request is a notification or not. Check if the request
contains an `id` field before returning the response. If `req_id` is None or
missing (indicating a notification), the function should not return a response
payload. Only return the dictionary with jsonrpc, result, and id when the
request is not a notification.
- Around line 17-22: The start() method creates AppRunner and TCPSite instances
but stores them only as local variables, preventing cleanup during shutdown.
Store the runner and site instances as instance variables (self.runner and
self.site) instead of local variables, and implement an explicit stop() method
that properly shuts down these resources by calling their cleanup methods to
ensure reliable resource cleanup.
- Around line 30-34: When processing batch requests in the section where
isinstance(req_data, list) is checked, add a validation to detect empty batches
before the loop that processes individual requests. If the list is empty, return
a JSON-RPC 2.0 Invalid Request error response with error code -32600 instead of
returning an empty array, as per the JSON-RPC 2.0 specification for empty batch
requests.
- Around line 40-41: The validation check on line 40 allows req to be non-dict,
but line 41 calls req.get("id") which will raise AttributeError if req is not a
dict. Extract the id value safely by only calling req.get("id") when req is
known to be a dict, otherwise use a default value like None for the id field in
the error response.

In `@requirements.txt`:
- Around line 3-4: The aiohttp package is imported in minichain/rpc.py and
tests/test_rpc.py but is missing from requirements.txt, which will cause
ImportError on clean installations. Add aiohttp as a new dependency line in
requirements.txt alongside the existing entries like libp2p and multiaddr to
ensure the package is installed when the project dependencies are resolved.

In `@tests/test_contract_transfers.py`:
- Around line 22-102: Add a new test method after
test_transfer_with_incoming_funds to cover the malformed address case for
transfer_out. Create a test that deploys a contract with code calling
transfer_out using an invalid hex address (like "not_hex"), then assert that the
transaction fails with status 0, the error message indicates an invalid address,
and verify that the contract balance, target balance, and contract storage all
remain unchanged from their initial states to confirm proper rollback behavior.

In `@tests/test_protocol_hardening.py`:
- Around line 99-100: The test for Transaction.from_dict(invalid_payload) is
catching a generic Exception which is too broad and could mask unrelated
regressions. Replace the Exception with the specific exception type that
Transaction.from_dict actually raises when given malformed input (likely
KeyError or another specific exception), so the test only passes when the
expected failure mode occurs and fails if any other unexpected exceptions occur.

---

Outside diff comments:
In `@minichain/mempool.py`:
- Around line 13-16: The add_transaction() method currently only validates the
transaction signature but does not check if the amount and fee values are valid.
Add validation before the transaction is admitted to check that both amount and
fee are non-negative integers. If either value fails validation, log an
appropriate warning and return False to reject the transaction, similar to the
existing signature verification check.
🪄 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: da4e76ee-cda9-4e42-9fce-eac46594aa76

📥 Commits

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

📒 Files selected for processing (12)
  • main.py
  • minichain/chain.py
  • minichain/contract.py
  • minichain/mempool.py
  • minichain/p2p.py
  • minichain/rpc.py
  • minichain/state.py
  • requirements.txt
  • tests/test_contract_transfers.py
  • tests/test_protocol_hardening.py
  • tests/test_reorg.py
  • tests/test_rpc.py

Comment thread main.py Outdated
Comment on lines +441 to +443
# Start RPC server on a port correlated to the node port (e.g. 8545 if P2P is 9000)
rpc_port = 8545 + (port - 9000)
rpc_task = asyncio.create_task(rpc_server.start(host="127.0.0.1", port=rpc_port))

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

Validate the derived RPC port before startup.

rpc_port = 8545 + (port - 9000) can fall outside 1..65535 for valid CLI --port inputs, causing runtime startup failures.

🤖 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 441 - 443, The RPC port calculation using `rpc_port =
8545 + (port - 9000)` can produce invalid port numbers outside the valid range
of 1-65535. Add validation logic after the rpc_port assignment and before the
asyncio.create_task call to verify that rpc_port is within the valid range (1 to
65535). If the calculated rpc_port is invalid, raise an appropriate exception or
log an error and exit to prevent the rpc_server.start call from failing at
runtime.

Comment thread minichain/chain.py
Comment on lines +104 to +112
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

Preserve explicit difficulty 0 in total-work calculation.

Line 112 treats 0 as 1, so difficulty-0 blocks are counted as 2 ** 1 instead of 2 ** 0. That makes difficulty 0 and 1 indistinguishable for fork choice.

🧮 Proposed fix
-        return sum(2 ** (block.difficulty or 1) for block in chain_list)
+        return sum(
+            2 ** (block.difficulty if block.difficulty is not None else 1)
+            for block in chain_list
+        )
🤖 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 - 112, The current implementation uses
`block.difficulty or 1` in the return statement of the get_total_work method,
which treats both None and 0 as falsy values and converts them to 1. This causes
difficulty-0 blocks to be counted as 2^1 instead of 2^0, making them
indistinguishable from difficulty-1 blocks. Replace the `or 1` fallback with an
explicit None check using a conditional expression (like `block.difficulty if
block.difficulty is not None else 1`) so that an explicit difficulty value of 0
is preserved and calculates correctly as 2^0 = 1, while only defaulting to 1
when difficulty is actually None.

Comment thread minichain/chain.py
Comment on lines +219 to +226
computed_receipt_root = calculate_receipt_root(receipts)
if block.receipt_root != computed_receipt_root:
logger.warning("Reorg failed: Invalid receipt root at block %s. Expected %s, got %s", block.index, computed_receipt_root, block.receipt_root)
return False, []

if block.state_root != temp_state.state_root():
logger.warning("Reorg failed: Invalid state root at block %s", block.index)
return False, []

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

Validate replayed receipt payloads, not only the receipt root.

add_block() rejects blocks whose block.receipts payload differs from computed receipts, but reorg replay omits that check. A chain accepted through resolve_conflicts() can therefore store receipts that normal block import would reject.

✅ Proposed parity check
                 computed_receipt_root = calculate_receipt_root(receipts)
                 if block.receipt_root != computed_receipt_root:
                     logger.warning("Reorg failed: Invalid receipt root at block %s. Expected %s, got %s", block.index, computed_receipt_root, block.receipt_root)
                     return False, []
 
+                if [r.to_dict() for r in block.receipts] != [r.to_dict() for r in receipts]:
+                    logger.warning("Reorg failed: Receipts payload mismatch at block %s", block.index)
+                    return False, []
+
                 if block.state_root != temp_state.state_root():
                     logger.warning("Reorg failed: Invalid state root at block %s", block.index)
                     return False, []
🤖 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 219 - 226, The reorg replay validation in
resolve_conflicts() only verifies that the computed receipt root matches the
block's receipt root, but fails to validate the actual receipt payload contents
like add_block() does. This allows blocks with mismatched receipts to be
accepted through reorg when normal block import would reject them. Add a
validation check after computing receipts to ensure block.receipts matches the
computed receipts list directly, not just their root hash. This additional
payload validation should occur alongside the existing receipt root and state
root checks to maintain parity with add_block() validation logic.

Comment thread minichain/chain.py
Comment on lines +228 to +231
# 4. Success! Compute orphaned transactions.
old_txs = {tx.tx_id: tx for b in original_chain[1:] for tx in b.transactions}
new_tx_ids = {tx.tx_id for b in new_chain_list[1:] for tx in b.transactions}
orphans = [tx for tx_id, tx in old_txs.items() if tx_id not in new_tx_ids]

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

Filter stale nonce-conflicting orphans before returning them.

This returns every old-chain transaction missing from the new chain. In the reorg test shape, tx1 has nonce 0 while the accepted chain has already advanced that sender to nonce 2; main.py then re-adds it to the mempool, where only the signature is checked. That can leave an unmineable stale transaction poisoning block assembly.

🧹 Suggested direction
             old_txs = {tx.tx_id: tx for b in original_chain[1:] for tx in b.transactions}
             new_tx_ids = {tx.tx_id for b in new_chain_list[1:] for tx in b.transactions}
-            orphans = [tx for tx_id, tx in old_txs.items() if tx_id not in new_tx_ids]
+            candidate_orphans = [tx for tx_id, tx in old_txs.items() if tx_id not in new_tx_ids]
+            orphans = []
+            for tx in candidate_orphans:
+                sender_nonce = temp_state.accounts.get(tx.sender, {}).get("nonce", 0)
+                if tx.nonce < sender_nonce:
+                    logger.debug("Skipping stale orphan tx %s with nonce %s < %s", tx.tx_id, tx.nonce, sender_nonce)
+                    continue
+                orphans.append(tx)
🤖 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 228 - 231, The orphans list being returned
contains transactions with stale nonces that are unmineable and should be
filtered out. Before returning the orphans list after computing it from old_txs
and new_tx_ids, filter out any orphaned transactions where the sender has
already advanced to a higher nonce in the new chain. Build a mapping of senders
to their maximum nonce in new_chain_list by iterating through all transactions
in the new chain blocks, then exclude any orphaned transactions where the
sender's current nonce in the new chain is already at or beyond the orphaned
transaction's nonce value.

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

Do not retain the caller-owned chain list.

self.chain = new_chain_list aliases the caller’s list. In tests this means node_a.resolve_conflicts(node_b.chain) makes both nodes share the same chain object; later appends to node_b.chain can mutate node_a.chain without replaying state.

🔒 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` at the chain assignment line creates an alias to the caller's
list instead of making an independent copy. This causes mutations to the
caller's list to affect the object's chain without proper state replay. Replace
the direct assignment with a copy operation to ensure the chain list is
independent from the caller's list.

Comment thread minichain/rpc.py Outdated
Comment thread minichain/rpc.py
else:
return {"jsonrpc": "2.0", "error": {"code": -32601, "message": f"Method not found: {method}"}, "id": req_id}

return {"jsonrpc": "2.0", "result": result, "id": req_id}

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

Don’t return responses for JSON-RPC notifications.

Requests without id are notifications and should not emit a result/error response payload.

🧰 Tools
🪛 Ruff (0.15.17)

[warning] 84-84: Consider moving this statement to an else block

(TRY300)

🤖 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/rpc.py` at line 84, The response handler at line 84 always returns
a JSON-RPC response regardless of whether the request is a notification or not.
Check if the request contains an `id` field before returning the response. If
`req_id` is None or missing (indicating a notification), the function should not
return a response payload. Only return the dictionary with jsonrpc, result, and
id when the request is not a notification.

Comment thread requirements.txt
Comment on lines +22 to +102
def test_successful_transfer_out(self):
# 1. Deploy Contract
code = """
target = msg['data']['target']
transfer_out(target, 50)
transfer_out(target, 25)
"""
deploy_tx = self._sign(Transaction(self.sender_pk, None, amount=100, nonce=0, data=code, fee=1000))
receipt = self.state.apply_transaction(deploy_tx)
self.assertEqual(receipt.status, 1)
contract_addr = receipt.contract_address

# Sender sent 100 to contract, plus 10 fee
self.assertEqual(self.state.get_account(contract_addr)['balance'], 100)
self.assertEqual(self.state.get_account(self.target_pk)['balance'], 0)

# 2. Call Contract to transfer out 75 coins
call_tx = self._sign(Transaction(self.sender_pk, contract_addr, amount=0, nonce=1, data={"target": self.target_pk}, fee=1000))
receipt2 = self.state.apply_transaction(call_tx)

self.assertEqual(receipt2.status, 1)

# Contract balance should be 100 - 75 = 25
self.assertEqual(self.state.get_account(contract_addr)['balance'], 25)

# Target should have 75
self.assertEqual(self.state.get_account(self.target_pk)['balance'], 75)

def test_failed_transfer_out_insufficient_balance(self):
# 1. Deploy Contract
code = """
target = msg['data']['target']
# Try to transfer 500, but contract only has 100
transfer_out(target, 500)
storage['malicious_state'] = 'corrupted'
"""
deploy_tx = self._sign(Transaction(self.sender_pk, None, amount=100, nonce=0, data=code, fee=1000))
receipt = self.state.apply_transaction(deploy_tx)
self.assertEqual(receipt.status, 1)
contract_addr = receipt.contract_address

# 2. Call Contract
call_tx = self._sign(Transaction(self.sender_pk, contract_addr, amount=0, nonce=1, data={"target": self.target_pk}, fee=1000))
receipt2 = self.state.apply_transaction(call_tx)

# Should fail with status 0
self.assertEqual(receipt2.status, 0)
self.assertEqual(receipt2.error_message, "Insufficient contract balance for transfers")

# State should be completely rolled back (target balance 0, contract balance remains 100)
self.assertEqual(self.state.get_account(contract_addr)['balance'], 100)
self.assertEqual(self.state.get_account(self.target_pk)['balance'], 0)

# Storage should NOT be updated
self.assertEqual(self.state.get_account(contract_addr)['storage'], {})

def test_transfer_with_incoming_funds(self):
# 1. Deploy Contract (0 initial balance)
code = """
target = msg['data']['target']
# We use the incoming funds to instantly transfer out!
transfer_out(target, msg['value'])
"""
deploy_tx = self._sign(Transaction(self.sender_pk, None, amount=0, nonce=0, data=code, fee=1000))
receipt = self.state.apply_transaction(deploy_tx)
self.assertEqual(receipt.status, 1)
contract_addr = receipt.contract_address

self.assertEqual(self.state.get_account(contract_addr)['balance'], 0)

# 2. Call Contract sending 50 coins
call_tx = self._sign(Transaction(self.sender_pk, contract_addr, amount=50, nonce=1, data={"target": self.target_pk}, fee=1000))
receipt2 = self.state.apply_transaction(call_tx)

self.assertEqual(receipt2.status, 1)

# Contract balance should be 0 (received 50, sent 50)
self.assertEqual(self.state.get_account(contract_addr)['balance'], 0)

# Target should have exactly 50
self.assertEqual(self.state.get_account(self.target_pk)['balance'], 50)

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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add a regression test for invalid transfer_out target addresses.

Current tests miss the malformed-address path. Add a case where contract code calls transfer_out("not_hex", 1) and assert failed receipt plus unchanged balances/storage.

Suggested test shape
+    def test_transfer_out_rejects_invalid_target_address(self):
+        code = """
+transfer_out('not_hex', 1)
+"""
+        deploy_tx = self._sign(Transaction(self.sender_pk, None, amount=10, nonce=0, data=code, fee=1000))
+        receipt = self.state.apply_transaction(deploy_tx)
+        self.assertEqual(receipt.status, 1)
+        contract_addr = receipt.contract_address
+
+        call_tx = self._sign(Transaction(self.sender_pk, contract_addr, amount=0, nonce=1, data={}, fee=1000))
+        receipt2 = self.state.apply_transaction(call_tx)
+
+        self.assertEqual(receipt2.status, 0)
+        self.assertEqual(self.state.get_account(contract_addr)["balance"], 10)
🤖 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 `@tests/test_contract_transfers.py` around lines 22 - 102, Add a new test
method after test_transfer_with_incoming_funds to cover the malformed address
case for transfer_out. Create a test that deploys a contract with code calling
transfer_out using an invalid hex address (like "not_hex"), then assert that the
transaction fails with status 0, the error message indicates an invalid address,
and verify that the contract balance, target balance, and contract storage all
remain unchanged from their initial states to confirm proper rollback behavior.

Comment on lines +99 to +100
with self.assertRaises(Exception):
Transaction.from_dict(invalid_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 | 🟡 Minor | ⚡ Quick win

Assert a specific exception type for malformed tx payloads.

Using Exception is too broad; this test should assert the expected failure mode (e.g., KeyError) so unrelated regressions don’t pass silently.

🧰 Tools
🪛 Ruff (0.15.17)

[warning] 99-99: Use pytest.raises instead of unittest-style assertRaises

Replace assertRaises with pytest.raises

(PT027)


[warning] 99-99: Do not assert blind exception: Exception

(B017)

🤖 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 `@tests/test_protocol_hardening.py` around lines 99 - 100, The test for
Transaction.from_dict(invalid_payload) is catching a generic Exception which is
too broad and could mask unrelated regressions. Replace the Exception with the
specific exception type that Transaction.from_dict actually raises when given
malformed input (likely KeyError or another specific exception), so the test
only passes when the expected failure mode occurs and fails if any other
unexpected exceptions occur.

Source: Linters/SAST tools

@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: 4

Caution

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

⚠️ Outside diff range comments (1)
tests/test_contract_transfers.py (1)

64-76: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Exercise rollback with non-zero incoming value in failure case.

At Line 64 the failing call uses amount=0, so this test does not validate the sender-refund branch for failed contract execution with incoming funds. Use a non-zero amount and assert sender balance only loses fee + gas_used.

Suggested test hardening
-        call_tx = self._sign(Transaction(self.sender_pk, contract_addr, amount=0, nonce=1, data={"target": self.target_pk}, fee=1000))
+        sender_before = self.state.get_account(self.sender_pk)['balance']
+        call_tx = self._sign(Transaction(self.sender_pk, contract_addr, amount=50, nonce=1, data={"target": self.target_pk}, fee=1000))
         receipt2 = self.state.apply_transaction(call_tx)

         # Should fail with status 0
         self.assertEqual(receipt2.status, 0)
         self.assertEqual(receipt2.error_message, "Insufficient contract balance for transfers")
@@
         # Storage should NOT be updated
         self.assertEqual(self.state.get_account(contract_addr)['storage'], {})
+
+        sender_after = self.state.get_account(self.sender_pk)['balance']
+        self.assertEqual(sender_before - sender_after, call_tx.fee + receipt2.gas_used)
🤖 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 `@tests/test_contract_transfers.py` around lines 64 - 76, The test case for the
failing contract call uses amount=0 in the call_tx Transaction, which does not
exercise the refund logic for failed executions with incoming funds. Modify the
amount parameter in the Transaction constructor from 0 to a non-zero value (such
as 50), then add an assertion after the receipt is processed to verify that the
sender's balance (self.sender_pk) has decreased by only the fee amount (or fee
plus gas_used if applicable), demonstrating that the incoming amount was
properly refunded when the contract execution failed.
🤖 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 125-142: The hello message handler (and similarly the
chain_request and chain_response handlers) does not validate that payload is not
None before calling payload.get() methods on it. Since payload is extracted with
data.get("data") which returns None if the "data" key is missing, this causes an
AttributeError crash on malformed P2P messages. Add a null-check immediately
after payload extraction to validate it is not None before processing any
subsequent payload.get() calls in the message handler branches, similar to how
the tx and block handlers already implement error handling.

In `@minichain/contract.py`:
- Line 167: The `p.join(timeout=10)` call sets a 10-second timeout but the
`RLIMIT_CPU` constraint on line 41 is set to 2 seconds, causing the CPU limit to
kill the process before the timeout can take effect on Unix systems. Update the
`RLIMIT_CPU` value to match the 10-second timeout value to ensure consistent
behavior and proper timeout enforcement across all platforms.

In `@minichain/p2p.py`:
- Around line 97-99: The peer_count property in the P2P class currently returns
a hardcoded 0 because the streams list is inaccessible from the asyncio side.
Create a thread-safe counter variable _peer_count to track the actual number of
connected peers. Replace the hardcoded return value in the peer_count property
to return this _peer_count variable. Then increment _peer_count when streams are
added in the stream_handler method and decrement _peer_count when streams are
removed in the DISCONNECT handler, ensuring proper synchronization between the
trio and asyncio contexts.

In `@requirements.txt`:
- Line 4: The aiohttp dependency in requirements.txt is currently unpinned,
which creates security and reproducibility risks as multiple versions with known
vulnerabilities may be installed. Replace the unpinned aiohttp entry with a
specific version constraint such as aiohttp>=3.14.1 (for the latest stable
version) or aiohttp>=3.10.11 (to address request smuggling vulnerabilities),
aligning with the project's practice of pinning dependencies like pynacl to
ensure consistent and secure builds across environments.

---

Outside diff comments:
In `@tests/test_contract_transfers.py`:
- Around line 64-76: The test case for the failing contract call uses amount=0
in the call_tx Transaction, which does not exercise the refund logic for failed
executions with incoming funds. Modify the amount parameter in the Transaction
constructor from 0 to a non-zero value (such as 50), then add an assertion after
the receipt is processed to verify that the sender's balance (self.sender_pk)
has decreased by only the fee amount (or fee plus gas_used if applicable),
demonstrating that the incoming amount was properly refunded when the contract
execution failed.
🪄 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: 84ee31df-77a5-4704-9f0f-605e95aaf58e

📥 Commits

Reviewing files that changed from the base of the PR and between daa4ec4 and d1efe24.

📒 Files selected for processing (14)
  • genesis.json
  • main.py
  • minichain/chain.py
  • minichain/contract.py
  • minichain/mempool.py
  • minichain/p2p.py
  • minichain/rpc.py
  • minichain/state.py
  • minichain/transaction.py
  • requirements.txt
  • tests/test_contract_transfers.py
  • tests/test_reorg.py
  • tests/test_rpc.py
  • tests/test_transaction_signing.py
💤 Files with no reviewable changes (1)
  • tests/test_reorg.py

Comment thread main.py
Comment thread minichain/contract.py
Comment thread minichain/p2p.py Outdated
Comment thread requirements.txt Outdated

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

Caution

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

⚠️ Outside diff range comments (2)
main.py (1)

141-145: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Unicast the catch-up chain_request to the ahead peer instead of broadcasting.

The hello handler already knows the specific peer that is ahead (peer_addr), yet Line 145 sends the chain_request via _broadcast_raw to every connected peer. Each peer then replies with a full block batch, causing duplicate sync traffic and amplification. Target the originating peer with _unicast_raw.

Proposed fix
                 req = {"type": "chain_request", "data": {"start_index": chain.last_block.index + 1, "limit": 500}}
-                asyncio.create_task(network._broadcast_raw(req))
+                asyncio.create_task(network._unicast_raw(peer_addr, req))
🤖 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 141 - 145, The chain_request is being broadcast to all
connected peers instead of being sent only to the peer that is ahead, causing
duplicate sync traffic. In the hello handler where peer_addr is known, replace
the network._broadcast_raw call with network._unicast_raw and pass peer_addr as
the target peer parameter to send the catch-up request only to the specific peer
that is ahead rather than amplifying the request across the entire network.
minichain/p2p.py (1)

93-94: 🚀 Performance & Scalability | 🟠 Major

send_chain_response broadcasts to all peers instead of unicasting to the requesting peer.

Using _broadcast_raw sends the full block batch to every connected peer, causing unnecessary bandwidth amplification and wasted network resources. The peer_stream parameter accepts a target but is never used. Use _unicast_raw(peer_addr, payload) to send the response only to the requesting peer, matching the pattern already implemented in main.py's chain_request handler (line ~193).

Note: This method has no call sites in the codebase; it may be unused and could be removed entirely unless it's part of a public API contract.

🤖 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 93 - 94, The send_chain_response method
currently uses _broadcast_raw which sends the chain response to all connected
peers instead of unicasting to only the requesting peer. Replace the
_broadcast_raw call with _unicast_raw, extracting the peer address from the
peer_stream parameter and passing it as the first argument along with the
payload dictionary. This ensures the chain response is sent only to the
requesting peer, matching the pattern already implemented in the chain_request
handler. The peer_stream parameter is currently unused but should be utilized to
identify the target peer for unicasting.
🤖 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.

Outside diff comments:
In `@main.py`:
- Around line 141-145: The chain_request is being broadcast to all connected
peers instead of being sent only to the peer that is ahead, causing duplicate
sync traffic. In the hello handler where peer_addr is known, replace the
network._broadcast_raw call with network._unicast_raw and pass peer_addr as the
target peer parameter to send the catch-up request only to the specific peer
that is ahead rather than amplifying the request across the entire network.

In `@minichain/p2p.py`:
- Around line 93-94: The send_chain_response method currently uses
_broadcast_raw which sends the chain response to all connected peers instead of
unicasting to only the requesting peer. Replace the _broadcast_raw call with
_unicast_raw, extracting the peer address from the peer_stream parameter and
passing it as the first argument along with the payload dictionary. This ensures
the chain response is sent only to the requesting peer, matching the pattern
already implemented in the chain_request handler. The peer_stream parameter is
currently unused but should be utilized to identify the target peer for
unicasting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3d34f6e9-95e8-40b8-9313-ea72bf526bae

📥 Commits

Reviewing files that changed from the base of the PR and between d1efe24 and 32fd879.

📒 Files selected for processing (5)
  • main.py
  • minichain/contract.py
  • minichain/p2p.py
  • requirements.txt
  • tests/test_contract_transfers.py

Comment thread minichain/chain.py
temp_state.chain_id = self.chain_id
temp_state.restore(self._genesis_state_snapshot)

# Verify and apply blocks 1 to N

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.

Typically, the two chains will have a common prefix. In other words, there will be a a block N*, such that both chains will be equal to each other up to block N*.

Instead of recomputing everything from genesis, it would be more efficient to roll_back the state to N* and then recompute everything only from N*.

I would like to accept and merge this PR as it is, though. But could you create an issue to improve this aspect in a future PR?

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.

2 participants