Skip to content

Readjusting the difficulty dynamically using PID #52

Open
anshulchikhale30-p wants to merge 16 commits intoStabilityNexus:mainfrom
anshulchikhale30-p:Difficulty/adjust
Open

Readjusting the difficulty dynamically using PID #52
anshulchikhale30-p wants to merge 16 commits intoStabilityNexus:mainfrom
anshulchikhale30-p:Difficulty/adjust

Conversation

@anshulchikhale30-p
Copy link
Contributor

@anshulchikhale30-p anshulchikhale30-p commented Mar 1, 2026

Add Dynamic Difficulty Adjustment Mechanism to Improve Network Stability #26 close
I test it locally

Screen Recording 2026-03-01 114726 (1)

Summary by CodeRabbit

  • New Features

    • PID-based dynamic difficulty adjustment; mining records per-block mining time.
    • New node loop driving multi-block demo and final balance reporting.
  • Improvements

    • Stronger PoW enforcement during block acceptance.
    • Per-address nonce synchronization and validated miner reward assignment with safe fallback.
    • Clearer mining and network operation logging.
  • Refactor

    • Node lifecycle and network handling reorganized for more robust orchestration.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a PID-based difficulty adjuster and enforces PoW checks in block validation; restructures node startup into node_loop/_run_node with per-address nonce syncing (sync_nonce, nested get_next_nonce), updates mining flow (pending_nonce_map, mining_time), and refactors network data handling.

Changes

Cohort / File(s) Summary
Node & CLI / Mining Loop
main.py
Replaces previous run flow with node_loop and internal _run_node; adds _handle_network_data; demo mining cadence and final balance reporting; main() now calls node_loop.
Nonce, Wallet & Mining API
main.py
Adds sync_nonce and nested get_next_nonce; changes mine_and_process_block signature to accept pending_nonce_map; wallet/public key encoding now uses explicit nacl.encoding.HexEncoder.
Consensus: PID difficulty
minichain/consensus/difficulty.py, minichain/chain.py
Adds PIDDifficultyAdjuster; Blockchain gains difficulty and difficulty_adjuster; add_block enforces block.difficulty and PoW prefix, updates difficulty after commit.
PoW / Mining internals
minichain/pow.py
Mining success log now includes elapsed mining time; an extra duplicate prefix check was introduced (likely accidental).
Misc / Manifest
manifest_file
Minor manifest/requirements adjustments recorded.

Sequence Diagram(s)

sequenceDiagram
    participant Client as "Client"
    participant Network as "Network"
    participant Node as "Node"
    participant Mempool as "Mempool"
    participant Chain as "Chain"
    participant Miner as "Miner"

    Client-->>Network: submit tx / connect
    Network-->>Node: deliver data
    Node->>Node: _handle_network_data(data)
    alt transaction
        Node->>Chain: sync_nonce(state, pending_nonce_map, address)
        Node->>Mempool: add transaction (uses pending_nonce_map)
    else block
        Node->>Chain: validate block (check difficulty & PoW)
    end
    loop mining cycle
        Node->>Miner: request mine (use pending_nonce_map & chain.difficulty)
        Miner-->>Node: mined block (includes mining_time)
        Node->>Chain: mine_and_process_block(..., pending_nonce_map)
        Chain-->>Chain: difficulty_adjuster.adjust(...)
        Node->>Network: broadcast new block
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

Python Lang

Suggested reviewers

  • Zahnentferner

Poem

🐰 I hopped through code at break of dawn,

Tuned PID whiskers till difficulty shone,
Nonces lined neatly, miners hummed in time,
Each block got a stamp — mining_time in rhyme,
Carrots for CI, and tests that pass fine.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately reflects the main change: implementing a PID-based dynamic difficulty adjustment mechanism. It clearly summarizes the primary objective and is concise and specific.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@main.py`:
- Line 154: The variable bob_sk returned from create_wallet() is never used; to
remove lint noise either discard it by assigning to an anonymous variable (e.g.,
replace bob_sk with _ in the assignment) or update the call to only return the
public key if create_wallet can be changed; modify the assignment bob_sk, bob_pk
= create_wallet() to drop the unused secret key (use _ , bob_pk =
create_wallet() or adjust create_wallet to return (bob_pk,) and update callers
accordingly).
- Around line 136-137: The except Exception as e block currently logs with an
eager f-string and drops the traceback; update it to call the logger with
exception context instead (use logger.exception("Network error") or
logger.error("Network error", exc_info=True)) so the full stack trace is
retained and avoid interpolating e into an f-string; locate the except block
that references logger and replace the logging call accordingly.
- Around line 37-43: The PID-adjusted difficulty is being computed into
block.difficulty but mine_block(block) is still running with the default since
the computed value isn't passed/used, and the controller is being advanced twice
because chain.add_block() also adjusts difficulty; update the flow so mining
uses the PID output and the controller advances only once — compute adjusted =
chain.difficulty_adjuster.adjust(chain.last_block.difficulty), set
block.difficulty = adjusted and call mine_block with that adjusted difficulty
(or a mine_block signature that accepts a difficulty), and remove or guard the
duplicate adjust call inside chain.add_block() (or make add_block() accept a
flag to skip re-adjusting) so the PID controller isn't stepped twice.

In `@minichain/chain.py`:
- Line 81: Replace the direct print call "print('New difficulty: ',
self.difficulty)" with a proper logger call: obtain a module/class logger (e.g.,
logger = logging.getLogger(__name__) or use an existing self.logger) and emit
the message at an appropriate level (logger.info or logger.debug) including the
difficulty value; if logging is not yet imported, add "import logging" and
prefer structured message like logger.info("New difficulty: %s",
self.difficulty).
- Around line 80-82: add_block currently updates self.difficulty via
self.difficulty_adjuster.adjust(self.difficulty) before enforcing PoW, allowing
low-work blocks to be accepted; change add_block to first compute the expected
target from the current self.difficulty, verify the incoming block's hash meets
that target (reject/return False if it doesn't), then only after accepting the
block call self.difficulty = self.difficulty_adjuster.adjust(self.difficulty)
(or otherwise update difficulty) so that the difficulty enforcement uses the
correct pre-adjustment value; reference add_block, self.difficulty, and
difficulty_adjuster.adjust when making this change.

In `@minichain/consensus/difficulty.py`:
- Line 13: Replace uses of the wall-clock time() with a monotonic clock for PID
interval measurements: change the assignment to self.last_block_time (and any
subsequent reads at the PID/update code around the lines that reference
time.time(), e.g., the methods calculating dt at lines ~31-33) to use
time.monotonic() instead of time.time(), ensuring all interval math uses the
same monotonic source and units; keep the existing time module import and only
swap the call sites (self.last_block_time = time.monotonic(), and use
time.monotonic() when computing dt).
- Around line 54-58: The PID adjustment is being truncated to int which makes
small non-zero adjustments round to 0 (when current_difficulty *
self.max_change_factor < 1) and can freeze difficulty; change the logic around
max_delta, clamped_adjustment and new_difficulty to apply the clamped adjustment
as a float and then convert to an integer with a non-zero minimum step (e.g.,
round and if rounded result is 0 but clamped_adjustment != 0, set delta to +1 or
-1 depending on sign) so that small adjustments still move the difficulty;
update usage of current_difficulty, max_delta, clamped_adjustment and
new_difficulty accordingly.

In `@minichain/pow.py`:
- Around line 65-67: The variable elapsed_time is computed but never used;
update the success logging to include elapsed_time (e.g., change the logger.info
call that currently logs block_hash to also log elapsed_time) so that the
computed duration from start_time is not dead code, or remove the elapsed_time
assignment entirely; look for the logger.info("Success! Hash: %s", block_hash)
call and the elapsed_time/start_time variables in this proof-of-work/mine
routine and either incorporate elapsed_time into that log message or delete the
unused assignment.
- Around line 58-60: The code recomputes the constant target string ("target =
'0' * difficulty") on every nonce attempt inside the mining/proof-of-work loop,
causing unnecessary overhead; move the computation of target (based on
difficulty) out of the nonce loop so it is calculated once before the loop
starts and remove the redundant assignment inside the loop, keeping the rest of
the nonce handling and hash checks unchanged.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c41261f and 15542b8.

📒 Files selected for processing (4)
  • main.py
  • minichain/chain.py
  • minichain/consensus/difficulty.py
  • minichain/pow.py

anshulchikhale30-p and others added 6 commits March 1, 2026 12:19
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor Author

@anshulchikhale30-p anshulchikhale30-p left a comment

Choose a reason for hiding this comment

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

Readjusting the difficulty dynamically using PID

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (1)
minichain/consensus/difficulty.py (1)

57-60: ⚠️ Potential issue | 🔴 Critical

Fix the over-indented delta branch.

Lines 58-60 still have one extra leading space, so the new PID adjuster cannot be imported.

🐛 Proposed fix
         delta = int(round(clamped_adjustment))
-         if delta == 0 and clamped_adjustment != 0:
-             delta = 1 if clamped_adjustment > 0 else -1
-         new_difficulty = current_difficulty + delta
+        if delta == 0 and clamped_adjustment != 0:
+            delta = 1 if clamped_adjustment > 0 else -1
+        new_difficulty = current_difficulty + delta

Run this to confirm the file parses again:

#!/bin/bash
python - <<'PY'
import ast, pathlib
path = pathlib.Path("minichain/consensus/difficulty.py")
ast.parse(path.read_text(), filename=str(path))
print("minichain/consensus/difficulty.py: syntax OK")
PY
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@minichain/consensus/difficulty.py` around lines 57 - 60, Remove the stray
extra leading space before the delta branch so the lines "if delta == 0 and
clamped_adjustment != 0:", "delta = 1 if clamped_adjustment > 0 else -1", and
"new_difficulty = current_difficulty + delta" align with the preceding "delta =
int(round(clamped_adjustment))" line; locate the block that computes delta
(references: delta, clamped_adjustment, current_difficulty, new_difficulty) and
fix the indentation so the module parses cleanly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@main.py`:
- Around line 384-385: The log is printing the stale accepted block difficulty
via chain.last_block.difficulty; change the logging to report the updated chain
difficulty (chain.difficulty) after calling chain.add_block() so the message
reflects the new target for the next block; locate the logger.info call near the
chain.add_block() invocation and replace or update its argument to use
chain.difficulty instead of chain.last_block.difficulty and keep the same log
format.
- Around line 356-378: The loop fails because tx_payment is never signed or
enqueued so mine_and_process_block(chain, mempool, pending_nonce_map) can return
None and unpacking into mined_block, _ raises TypeError; fix by signing
tx_payment (use whatever signing API your Transaction class requires) and
inserting it into mempool before the mining loop (e.g., mempool.enqueue or
mempool.append), and also defensively handle mine_and_process_block returning
None by checking the return value before unpacking (only unpack/process when
result is not None). Reference symbols: tx_payment, Transaction, get_next_nonce,
mempool, pending_nonce_map, and mine_and_process_block.
- Around line 305-332: The async handler _handle_network_data is malformed:
remove the stray await network.start(port=port) call from inside the handler
(network is started in _run_node), fix indentation so chain.add_block(block) is
inside the try block, remove the stray '+' and replace the malformed except with
a proper except Exception: logger.exception("Network error while handling
incoming data") clause; additionally, after the code that constructs tx_payment
ensure you call mempool.add_transaction(tx_payment) so the payment is enqueued,
and change the mining log to reference chain.difficulty (the next target)
instead of chain.last_block.difficulty. Use the symbols _handle_network_data,
chain.add_block, logger.exception, _run_node, tx_payment,
mempool.add_transaction, and chain.difficulty to locate the edits.

In `@minichain/chain.py`:
- Line 91: The difficulty is currently recomputed using
difficulty_adjuster.adjust(self.difficulty) which falls back to time.monotonic()
when actual_block_time is omitted, causing node-local wall-clock variance;
change the call to pass the canonical block time from the accepted chain (e.g.
the new block's timestamp or the chain-provided interval) into
difficulty_adjuster.adjust (use the block/header timestamp or an explicit
actual_block_time argument) so the adjust function uses chain data rather than
local receipt time and avoids time.monotonic() fallback when computing
self.difficulty.
- Around line 91-92: The logger.info call is misindented relative to the
surrounding block in the method that updates difficulty (the lines calling
self.difficulty = self.difficulty_adjuster.adjust(self.difficulty) and
logger.info("New difficulty: %s", self.difficulty)); fix it by adjusting the
indentation of the logger.info line so it matches the enclosing block (same
indentation level as the previous line) to eliminate the IndentationError and
allow the module to import.

In `@minichain/pow.py`:
- Around line 58-60: There is a stray, empty duplicate conditional "if
block_hash.startswith(target):" that causes an IndentationError; remove this
lone line so only the intended difficulty check remains (the subsequent
populated check that evaluates block_hash.startswith(target) and handles
success/failure). Locate the duplicate occurrence of
block_hash.startswith(target) in the mining/proof-of-work routine and delete the
empty if-statement so parsing and mining continue normally.

---

Duplicate comments:
In `@minichain/consensus/difficulty.py`:
- Around line 57-60: Remove the stray extra leading space before the delta
branch so the lines "if delta == 0 and clamped_adjustment != 0:", "delta = 1 if
clamped_adjustment > 0 else -1", and "new_difficulty = current_difficulty +
delta" align with the preceding "delta = int(round(clamped_adjustment))" line;
locate the block that computes delta (references: delta, clamped_adjustment,
current_difficulty, new_difficulty) and fix the indentation so the module parses
cleanly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 88b96c91-8297-443e-aee5-6761abdefc10

📥 Commits

Reviewing files that changed from the base of the PR and between 15542b8 and 6e3b6b6.

📒 Files selected for processing (4)
  • main.py
  • minichain/chain.py
  • minichain/consensus/difficulty.py
  • minichain/pow.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@minichain/chain.py`:
- Around line 93-94: The function in minichain/chain.py contains a duplicated
unreachable "return True" (two consecutive return True statements); remove the
second/duplicate return so only a single "return True" remains in that function
(i.e., delete the redundant return in the block containing the duplicate returns
to restore clean control flow).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 40c0e8b5-56c1-4c83-9cb6-2ab124e5f686

📥 Commits

Reviewing files that changed from the base of the PR and between 6e3b6b6 and 4f1b92b.

📒 Files selected for processing (1)
  • minichain/chain.py

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
main.py (1)

305-329: ⚠️ Potential issue | 🔴 Critical

Fix the handler indentation; main.py does not parse.

chain.add_block(block) is dedented out of the try/elif, and the except clause is misindented. Python rejects the module before any of this code can run.

This parses main.py and prints the affected range. It should currently report a SyntaxError in this block.

#!/bin/bash
python - <<'PY'
import ast
from pathlib import Path

path = Path("main.py")
source = path.read_text()
try:
    ast.parse(source, filename=str(path))
    print("main.py: syntax OK")
except SyntaxError as e:
    print(f"{e.filename}:{e.lineno}:{e.offset}: {e.msg}")
PY

sed -n '305,329p' main.py | nl -ba
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.py` around lines 305 - 329, The block in async function
_handle_network_data has incorrect indentation: chain.add_block(block) is
dedented out of the elif/try and the except: logger.exception(...) is
misindented causing a SyntaxError; fix by indenting chain.add_block(block) to
align inside the elif (so it executes after constructing block) and align the
except: and logger.exception(...) with the try: (same indentation level as try)
so the try/except wraps the whole handler; verify _handle_network_data,
chain.add_block, and logger.exception are in correct nested indentation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@main.py`:
- Around line 289-345: The entry path now always runs the Alice/Bob demo and
tears down the network immediately, breaking the interactive node behavior;
restore the original CLI/daemon flow by reintroducing argument parsing and
conditional startup: in main (and node_loop) detect --port/--connect flags and
only run the Alice/Bob demo when an explicit demo flag is passed, otherwise
start the network via _run_node and then call cli_loop() to keep the node
interactive; ensure network.stop() is only called when the node is actually
shutting down (i.e., after cli_loop exits or on signal), and remove any
premature shutdown logic that stops the network right after _run_node() returns
so the node remains running for CLI/peer operations.
- Around line 356-363: The demo only queues a single transaction (tx_payment)
before repeatedly calling mine_and_process_block, so after the first block the
mempool is empty and subsequent iterations skip mining; fix by ensuring a
mineable transaction is added for each mining iteration—inside the loop that
calls mine_and_process_block, create and sign a new Transaction (use
Transaction(..., sender=alice_pk, receiver=bob_pk, amount=10,
nonce=get_next_nonce(alice_pk))), call tx.sign(alice_sk), and
mempool.add_transaction(tx) each iteration so mine_and_process_block has work
for all five blocks (also apply the same change to the second loop referenced
around lines 369-378).
- Around line 319-327: The received block is missing PoW fields so
Chain.add_block rejects it; when constructing Block from block_data (in the
Block(...) call) populate the proof-of-work fields nonce and hash from
block_data (e.g., set nonce=block_data.get("nonce") and
hash=block_data.get("hash") or equivalent) before calling chain.add_block so
calculate_hash(block.to_header_dict()) and difficulty checks in Chain.add_block
validate correctly; update the Block construction (or immediately assign those
attributes on the Block instance) using the keys "nonce" and "hash" from
block_data.
- Around line 362-363: The reported indentation errors are caused by extra
leading spaces on the statements tx_payment.sign(alice_sk) and
mempool.add_transaction(tx_payment) and on the block inside the for loop (the if
not mined, its logger.info/continue, and the mined_block assignment); fix them
by aligning those lines to the surrounding block levels—reduce the two lines
after tx_payment = Transaction(...) to the same 4-space indent as that block,
and adjust the if not mined block and its continuations to the proper 8-space
and 12-space indents respectively so logger.info and continue match the if-body
and mined_block, _ = mined aligns with the for-loop body.

---

Duplicate comments:
In `@main.py`:
- Around line 305-329: The block in async function _handle_network_data has
incorrect indentation: chain.add_block(block) is dedented out of the elif/try
and the except: logger.exception(...) is misindented causing a SyntaxError; fix
by indenting chain.add_block(block) to align inside the elif (so it executes
after constructing block) and align the except: and logger.exception(...) with
the try: (same indentation level as try) so the try/except wraps the whole
handler; verify _handle_network_data, chain.add_block, and logger.exception are
in correct nested indentation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e4426542-0565-4d8a-b7d7-e034c9d51c7a

📥 Commits

Reviewing files that changed from the base of the PR and between 4f1b92b and b206d82.

📒 Files selected for processing (1)
  • main.py

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@minichain/chain.py`:
- Around line 93-99: The block updating difficulty has inconsistent indentation
and a duplicated return that causes a SyntaxError; align the
difficulty_adjuster.adjust call's keyword args and following statements with the
surrounding method (ensure the args and closing paren line up with
self.difficulty = ...), indent logger.info("New difficulty: %s",
self.difficulty) to the same level, and remove the duplicate "return True" so
only a single properly-indented return remains; target the code around
self.difficulty, difficulty_adjuster.adjust, logger.info, and the return True
lines.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 56f50a59-024e-44ec-a427-07d4c292a73d

📥 Commits

Reviewing files that changed from the base of the PR and between b206d82 and 67a5b82.

📒 Files selected for processing (1)
  • minichain/chain.py

anshulchikhale30-p and others added 5 commits March 8, 2026 12:47
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant