From 15542b842ff5dde9b28bf054ed2ac82f8d523bcf Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Sun, 1 Mar 2026 11:40:29 +0530 Subject: [PATCH 01/17] fixed difficulty target check in proof of work mining function --- main.py | 160 ++++++++++++++++++------------ minichain/chain.py | 7 +- minichain/consensus/difficulty.py | 60 +++++++++++ minichain/pow.py | 4 + 4 files changed, 165 insertions(+), 66 deletions(-) create mode 100644 minichain/consensus/difficulty.py diff --git a/main.py b/main.py index add86c1..54fe05b 100644 --- a/main.py +++ b/main.py @@ -1,29 +1,31 @@ import asyncio import logging import re +import time from nacl.signing import SigningKey -from nacl.encoding import HexEncoder - -from minichain import Transaction, Blockchain, Block, State, Mempool, P2PNetwork, mine_block +import nacl.encoding +# Local project imports +from minichain import Transaction, Blockchain, Block, mine_block, Mempool, P2PNetwork logger = logging.getLogger(__name__) BURN_ADDRESS = "0" * 40 +# ------------------------- +# Wallet Creation +# ------------------------- def create_wallet(): sk = SigningKey.generate() - pk = sk.verify_key.encode(encoder=HexEncoder).decode() + pk = sk.verify_key.encode(encoder=nacl.encoding.HexEncoder).decode() return sk, pk +# ------------------------- +# Mining + Block Processing +# ------------------------- def mine_and_process_block(chain, mempool, pending_nonce_map): - """ - Mine block and let Blockchain handle validation + state updates. - DO NOT manually apply transactions again. - """ - pending_txs = mempool.get_transactions_for_block() block = Block( @@ -32,57 +34,71 @@ def mine_and_process_block(chain, mempool, pending_nonce_map): transactions=pending_txs, ) + # PID Difficulty Adjustment (handled internally) + block.difficulty = chain.difficulty_adjuster.adjust( + chain.last_block.difficulty + ) + + start_time = time.time() mined_block = mine_block(block) + mining_time = time.time() - start_time + + # Attach mining time to block (optional but useful) + mined_block.mining_time = mining_time if not hasattr(mined_block, "miner"): mined_block.miner = BURN_ADDRESS - deployed_contracts: list[str] = [] + deployed_contracts = [] if chain.add_block(mined_block): - logger.info("Block #%s added", mined_block.index) + logger.info("Block #%s added with Difficulty: %s", + mined_block.index, + mined_block.difficulty) - miner_attr = getattr(mined_block, "miner", None) - if isinstance(miner_attr, str) and re.match(r'^[0-9a-fA-F]{40}$', miner_attr): - miner_address = miner_attr - else: - logger.warning("Invalid miner address. Crediting burn address.") - miner_address = BURN_ADDRESS + # Reward miner + miner_attr = getattr(mined_block, "miner", BURN_ADDRESS) + miner_address = ( + miner_attr if re.match(r'^[0-9a-fA-F]{40}$', str(miner_attr)) + else BURN_ADDRESS + ) - # Reward must go through chain.state chain.state.credit_mining_reward(miner_address) for tx in mined_block.transactions: sync_nonce(chain.state, pending_nonce_map, tx.sender) - # Track deployed contracts if your state.apply_transaction returns address result = chain.state.get_account(tx.receiver) if tx.receiver else None if isinstance(result, dict): deployed_contracts.append(tx.receiver) return mined_block, deployed_contracts + else: logger.error("Block rejected by chain") return None, [] +# ------------------------- +# Nonce Sync +# ------------------------- def sync_nonce(state, pending_nonce_map, address): account = state.get_account(address) - if account and "nonce" in account: - pending_nonce_map[address] = account["nonce"] - else: - pending_nonce_map[address] = 0 + pending_nonce_map[address] = account.get("nonce", 0) if account else 0 +# ------------------------- +# Node Logic +# ------------------------- async def node_loop(): - logger.info("Starting MiniChain Node with Smart Contracts") + logger.info("Starting MiniChain Node with PID Difficulty Adjustment") chain = Blockchain() mempool = Mempool() - + network = P2PNetwork() pending_nonce_map = {} - def claim_nonce(address) -> int: + def get_next_nonce(address) -> int: account = chain.state.get_account(address) account_nonce = account.get("nonce", 0) if account else 0 local_nonce = pending_nonce_map.get(address, account_nonce) @@ -90,11 +106,7 @@ def claim_nonce(address) -> int: pending_nonce_map[address] = next_nonce + 1 return next_nonce - network = P2PNetwork() - async def _handle_network_data(data): - logger.info("Received network data: %s", data) - try: if data["type"] == "tx": tx = Transaction(**data["data"]) @@ -103,60 +115,56 @@ async def _handle_network_data(data): elif data["type"] == "block": block_data = data["data"] - transactions_raw = block_data.get("transactions", []) - transactions = [Transaction(**tx_data) for tx_data in transactions_raw] + txs = [ + Transaction(**tx_d) + for tx_d in block_data.get("transactions", []) + ] block = Block( - index=block_data.get("index"), - previous_hash=block_data.get("previous_hash"), - transactions=transactions, + index=block_data["index"], + previous_hash=block_data["previous_hash"], + transactions=txs, timestamp=block_data.get("timestamp"), - difficulty=block_data.get("difficulty") + difficulty=block_data.get("difficulty"), ) block.nonce = block_data.get("nonce", 0) block.hash = block_data.get("hash") - if chain.add_block(block): - logger.info("Received block added to chain: #%s", block.index) + chain.add_block(block) - except Exception: - logger.exception("Error processing network data: %s", data) + except Exception as e: + logger.error(f"Network error: {e}") network.register_handler(_handle_network_data) try: - await _run_node(network, chain, mempool, pending_nonce_map, claim_nonce) + await _run_node(network, chain, mempool, pending_nonce_map, get_next_nonce) finally: await network.stop() +# ------------------------- +# Run Node +# ------------------------- async def _run_node(network, chain, mempool, pending_nonce_map, get_next_nonce): await network.start() alice_sk, alice_pk = create_wallet() bob_sk, bob_pk = create_wallet() - logger.info("Alice Address: %s...", alice_pk[:10]) - logger.info("Bob Address: %s...", bob_pk[:10]) - - logger.info("[1] Genesis: Crediting Alice with 100 coins") + # Initial funding chain.state.credit_mining_reward(alice_pk, reward=100) sync_nonce(chain.state, pending_nonce_map, alice_pk) - # ------------------------------- - # Alice Payment - # ------------------------------- - - logger.info("[2] Transaction: Alice sends 10 coins to Bob") - - nonce = get_next_nonce(alice_pk) + # Alice sends Bob 10 coins + logger.info("[2] Alice sending 10 coins to Bob") tx_payment = Transaction( sender=alice_pk, receiver=bob_pk, amount=10, - nonce=nonce, + nonce=get_next_nonce(alice_pk), ) tx_payment.sign(alice_sk) @@ -164,29 +172,51 @@ async def _run_node(network, chain, mempool, pending_nonce_map, get_next_nonce): await network.broadcast_transaction(tx_payment) # ------------------------------- - # Mine Block 1 + # PID Demo: Mining 5 Blocks # ------------------------------- + logger.info("[3] Mining Multiple Blocks (Watch Difficulty Adjust)") - logger.info("[3] Mining Block 1") - mine_and_process_block(chain, mempool, pending_nonce_map) + for i in range(5): + await asyncio.sleep(1) - # ------------------------------- - # Final State Check - # ------------------------------- + logger.info(f"\nMining Block {i+1}") - logger.info("[4] Final State Check") + mined_block, _ = mine_and_process_block( + chain, mempool, pending_nonce_map + ) + if mined_block: + logger.info("Block mined in %.2f seconds", + mined_block.mining_time) + + logger.info("New difficulty: %s", + chain.last_block.difficulty) + + # Final balances alice_acc = chain.state.get_account(alice_pk) bob_acc = chain.state.get_account(bob_pk) - logger.info("Alice Balance: %s", alice_acc.get("balance", 0) if alice_acc else 0) - logger.info("Bob Balance: %s", bob_acc.get("balance", 0) if bob_acc else 0) + logger.info( + "Final Balances -> Alice: %s, Bob: %s", + alice_acc.get("balance", 0), + bob_acc.get("balance", 0), + ) +# ------------------------- +# Entry Point +# ------------------------- def main(): - logging.basicConfig(level=logging.INFO) - asyncio.run(node_loop()) + logging.basicConfig( + level=logging.INFO, + format='%(message)s' + ) + + try: + asyncio.run(node_loop()) + except KeyboardInterrupt: + pass if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/minichain/chain.py b/minichain/chain.py index 78ac73f..099df71 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -1,6 +1,7 @@ from .block import Block from .state import State from .pow import calculate_hash +from minichain.consensus.difficulty import PIDDifficultyAdjuster import logging import threading @@ -13,6 +14,8 @@ class Blockchain: """ def __init__(self): + self.difficulty = 3 + self.difficulty_adjuster = PIDDifficultyAdjuster(target_block_time=5) self.chain = [] self.state = State() self._lock = threading.RLock() @@ -74,4 +77,6 @@ def add_block(self, block): # All transactions valid → commit state and append block self.state = temp_state self.chain.append(block) - return True + self.difficulty = self.difficulty_adjuster.adjust(self.difficulty) + print("New difficulty: ", self.difficulty) + return True \ No newline at end of file diff --git a/minichain/consensus/difficulty.py b/minichain/consensus/difficulty.py new file mode 100644 index 0000000..43d5d24 --- /dev/null +++ b/minichain/consensus/difficulty.py @@ -0,0 +1,60 @@ +import time + +class PIDDifficultyAdjuster: + def __init__(self, target_block_time=5, kp=0.5, ki=0.05, kd=0.1): + self.target_block_time = target_block_time + # PID Coefficients + self.kp = kp + self.ki = ki + self.kd = kd + + self.integral = 0 + self.previous_error = 0 + self.last_block_time = time.time() + + # Limit the integral to prevent "Windup" + # This stops the difficulty from tanking if the network goes offline + self.integral_limit = 100 + + # Max percentage the difficulty can change in one block (e.g., 10%) + self.max_change_factor = 0.1 + + def adjust(self, current_difficulty, actual_block_time=None): + """ + Calculates the new difficulty based on the time since the last block. + """ + # --- FIX: Handle the case where current_difficulty is None --- + if current_difficulty is None: + current_difficulty = 1000 # Default starting difficulty + + if actual_block_time is None: + now = time.time() + actual_block_time = now - self.last_block_time + self.last_block_time = now + + # Error = Goal - Reality + error = self.target_block_time - actual_block_time + + # Update Integral with clamping (Anti-Windup) + self.integral = max(min(self.integral + error, self.integral_limit), -self.integral_limit) + + # Derivative: how fast is the error changing? + derivative = error - self.previous_error + self.previous_error = error + + # Calculate PID Adjustment + adjustment = ( + self.kp * error + + self.ki * self.integral + + self.kd * derivative + ) + + # Apply adjustment with a cap to maintain stability + # Now current_difficulty is guaranteed to be a number + max_delta = current_difficulty * self.max_change_factor + clamped_adjustment = max(min(adjustment, max_delta), -max_delta) + + new_difficulty = current_difficulty + int(clamped_adjustment) + + # Safety: Difficulty must never drop below 1 + return max(1, new_difficulty) \ No newline at end of file diff --git a/minichain/pow.py b/minichain/pow.py index b8484b1..7e5211b 100644 --- a/minichain/pow.py +++ b/minichain/pow.py @@ -55,10 +55,14 @@ def mine_block( header_dict["nonce"] = local_nonce block_hash = calculate_hash(header_dict) + #check difficulty target + target = "0" * difficulty + # Check difficulty target if block_hash.startswith(target): block.nonce = local_nonce # Assign only on success block.hash = block_hash + elapsed_time = time.monotonic() - start_time if logger: logger.info("Success! Hash: %s", block_hash) return block From c1f61f9c40ec282a3d03c1a6d8e1b76872ecccf4 Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Sun, 1 Mar 2026 12:19:57 +0530 Subject: [PATCH 02/17] Update minichain/pow.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- minichain/pow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/minichain/pow.py b/minichain/pow.py index 7e5211b..7905a0c 100644 --- a/minichain/pow.py +++ b/minichain/pow.py @@ -62,9 +62,9 @@ def mine_block( if block_hash.startswith(target): block.nonce = local_nonce # Assign only on success block.hash = block_hash - elapsed_time = time.monotonic() - start_time if logger: - logger.info("Success! Hash: %s", block_hash) + elapsed_time = time.monotonic() - start_time + logger.info("Success! Hash: %s (%.3fs)", block_hash, elapsed_time) return block # Allow cancellation via progress callback (pass nonce explicitly) From 8ecc46d32238b2fd1e73cc7215fc6b309f03fe1a Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Sun, 1 Mar 2026 12:20:44 +0530 Subject: [PATCH 03/17] Update main.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 54fe05b..8201c7b 100644 --- a/main.py +++ b/main.py @@ -151,7 +151,7 @@ async def _run_node(network, chain, mempool, pending_nonce_map, get_next_nonce): await network.start() alice_sk, alice_pk = create_wallet() - bob_sk, bob_pk = create_wallet() + _bob_sk, bob_pk = create_wallet() # Initial funding chain.state.credit_mining_reward(alice_pk, reward=100) From 48a2943a6966c328b7eed30c0882bfe6e630588b Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Sun, 1 Mar 2026 12:28:57 +0530 Subject: [PATCH 04/17] Update main.py --- main.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/main.py b/main.py index 8201c7b..acc7605 100644 --- a/main.py +++ b/main.py @@ -34,13 +34,11 @@ def mine_and_process_block(chain, mempool, pending_nonce_map): transactions=pending_txs, ) - # PID Difficulty Adjustment (handled internally) - block.difficulty = chain.difficulty_adjuster.adjust( - chain.last_block.difficulty - ) + # Mine using current consensus difficulty; chain updates next difficulty after acceptance + block.difficulty = chain.difficulty start_time = time.time() - mined_block = mine_block(block) + mined_block = mine_block(block, difficulty=block.difficulty) mining_time = time.time() - start_time # Attach mining time to block (optional but useful) @@ -133,8 +131,8 @@ async def _handle_network_data(data): chain.add_block(block) - except Exception as e: - logger.error(f"Network error: {e}") + except Exception: ++ logger.exception("Network error while handling incoming data") network.register_handler(_handle_network_data) @@ -219,4 +217,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() From e7c9879cf854b8c850f41b69d2893382e6a5a10a Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Sun, 1 Mar 2026 12:36:16 +0530 Subject: [PATCH 05/17] Update chain.py --- minichain/chain.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/minichain/chain.py b/minichain/chain.py index 099df71..6cd65e9 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -63,6 +63,17 @@ def add_block(self, block): logger.warning("Block %s rejected: Invalid hash %s", block.index, block.hash) return False + # Enforce PoW difficulty + if block.difficulty != self.difficulty: + logger.warning( + "Block %s rejected: Invalid difficulty %s != %s", + block.index, block.difficulty, self.difficulty + ) + return False + if not block.hash.startswith("0" * self.difficulty): + logger.warning("Block %s rejected: Hash does not meet difficulty target", block.index) + return False + # Validate transactions on a temporary state copy temp_state = self.state.copy() @@ -78,5 +89,5 @@ def add_block(self, block): self.state = temp_state self.chain.append(block) self.difficulty = self.difficulty_adjuster.adjust(self.difficulty) - print("New difficulty: ", self.difficulty) - return True \ No newline at end of file + logger.info("New difficulty: %s", self.difficulty) + return True From 5d64086a93e4d910759de2440782d3d838977f7b Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Sun, 1 Mar 2026 12:40:53 +0530 Subject: [PATCH 06/17] Update difficulty.py --- minichain/consensus/difficulty.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/minichain/consensus/difficulty.py b/minichain/consensus/difficulty.py index 43d5d24..0e4fdd4 100644 --- a/minichain/consensus/difficulty.py +++ b/minichain/consensus/difficulty.py @@ -10,7 +10,7 @@ def __init__(self, target_block_time=5, kp=0.5, ki=0.05, kd=0.1): self.integral = 0 self.previous_error = 0 - self.last_block_time = time.time() + self.last_block_time = time.monotonic() # Limit the integral to prevent "Windup" # This stops the difficulty from tanking if the network goes offline @@ -28,7 +28,7 @@ def adjust(self, current_difficulty, actual_block_time=None): current_difficulty = 1000 # Default starting difficulty if actual_block_time is None: - now = time.time() + now = time.monotonic() actual_block_time = now - self.last_block_time self.last_block_time = now @@ -51,10 +51,13 @@ def adjust(self, current_difficulty, actual_block_time=None): # Apply adjustment with a cap to maintain stability # Now current_difficulty is guaranteed to be a number - max_delta = current_difficulty * self.max_change_factor + max_delta = max(1, int(round(current_difficulty * self.max_change_factor))) clamped_adjustment = max(min(adjustment, max_delta), -max_delta) - new_difficulty = current_difficulty + int(clamped_adjustment) + 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 # Safety: Difficulty must never drop below 1 - return max(1, new_difficulty) \ No newline at end of file + return max(1, new_difficulty) From 96aa5cbbbb7c1e52812761f0ac5a9818759c2cc4 Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Sun, 1 Mar 2026 12:42:20 +0530 Subject: [PATCH 07/17] Update pow.py --- minichain/pow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/minichain/pow.py b/minichain/pow.py index 7905a0c..6540413 100644 --- a/minichain/pow.py +++ b/minichain/pow.py @@ -55,8 +55,8 @@ def mine_block( header_dict["nonce"] = local_nonce block_hash = calculate_hash(header_dict) - #check difficulty target - target = "0" * difficulty + # Check difficulty target + if block_hash.startswith(target): # Check difficulty target if block_hash.startswith(target): From 4f1b92bbf5fc7b301ea7ac42cdbdceb4c02501b6 Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Sat, 7 Mar 2026 19:35:41 +0530 Subject: [PATCH 08/17] Update minichain/chain.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- minichain/chain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/minichain/chain.py b/minichain/chain.py index 6cd65e9..9008029 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -89,5 +89,6 @@ def add_block(self, block): self.state = temp_state self.chain.append(block) self.difficulty = self.difficulty_adjuster.adjust(self.difficulty) - logger.info("New difficulty: %s", self.difficulty) + logger.info("New difficulty: %s", self.difficulty) + return True return True From b206d82aa333a843b56b08015998ae841cd02782 Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Sun, 8 Mar 2026 12:28:10 +0530 Subject: [PATCH 09/17] Update main.py --- main.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/main.py b/main.py index e032157..2b4a8c7 100644 --- a/main.py +++ b/main.py @@ -324,12 +324,9 @@ async def _handle_network_data(data): difficulty=block_data.get("difficulty"), ) - await network.start(port=port) - - chain.add_block(block) - - except Exception: -+ logger.exception("Network error while handling incoming data") + chain.add_block(block) + except Exception: + logger.exception("Network error while handling incoming data") # Nonce counter kept as a mutable list so the CLI closure can mutate it nonce_counter = [0] @@ -362,7 +359,8 @@ async def _run_node(network, chain, mempool, pending_nonce_map, get_next_nonce): amount=10, nonce=get_next_nonce(alice_pk), ) - + tx_payment.sign(alice_sk) + mempool.add_transaction(tx_payment) # ------------------------------- # PID Demo: Mining 5 Blocks # ------------------------------- @@ -373,16 +371,19 @@ async def _run_node(network, chain, mempool, pending_nonce_map, get_next_nonce): logger.info(f"\nMining Block {i+1}") - mined_block, _ = mine_and_process_block( - chain, mempool, pending_nonce_map - ) + mined = mine_and_process_block(chain, mempool, pending_nonce_map) + if not mined: + logger.info("No pending transactions to mine in this iteration") + continue + mined_block, _ = mined + if mined_block: logger.info("Block mined in %.2f seconds", mined_block.mining_time) logger.info("New difficulty: %s", - chain.last_block.difficulty) + chain.difficulty) # Final balances alice_acc = chain.state.get_account(alice_pk) From 67a5b82dd5faf9f3060f6fa2ea726bfb6968842c Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Sun, 8 Mar 2026 12:34:19 +0530 Subject: [PATCH 10/17] Update chain.py --- minichain/chain.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/minichain/chain.py b/minichain/chain.py index 9008029..b5ce514 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -86,9 +86,14 @@ def add_block(self, block): return False # All transactions valid → commit state and append block + previous_timestamp = self.last_block.timestamp self.state = temp_state self.chain.append(block) - self.difficulty = self.difficulty_adjuster.adjust(self.difficulty) - logger.info("New difficulty: %s", self.difficulty) - return True - return True + actual_block_time = max(0, (block.timestamp - previous_timestamp) / 1000) + self.difficulty = self.difficulty_adjuster.adjust( + self.difficulty, + actual_block_time=actual_block_time, + ) + logger.info("New difficulty: %s", self.difficulty) + return True + return True From b1377d2fd97908faad300d2d9bb3880269dcf1a5 Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Sun, 8 Mar 2026 12:47:56 +0530 Subject: [PATCH 11/17] Update minichain/chain.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- minichain/chain.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/minichain/chain.py b/minichain/chain.py index b5ce514..3b346d5 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -91,9 +91,8 @@ def add_block(self, block): self.chain.append(block) actual_block_time = max(0, (block.timestamp - previous_timestamp) / 1000) self.difficulty = self.difficulty_adjuster.adjust( - self.difficulty, - actual_block_time=actual_block_time, - ) - logger.info("New difficulty: %s", self.difficulty) - return True - return True + self.difficulty, + actual_block_time=actual_block_time, + ) + logger.info("New difficulty: %s", self.difficulty) + return True From 1fb37ae0b6f3e277ff942f347d2f2122a19c785e Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Mon, 9 Mar 2026 10:14:42 +0530 Subject: [PATCH 12/17] Update main.py --- main.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/main.py b/main.py index 2b4a8c7..34d0297 100644 --- a/main.py +++ b/main.py @@ -353,14 +353,6 @@ async def _run_node(network, chain, mempool, pending_nonce_map, get_next_nonce): # Alice sends Bob 10 coins logger.info("[2] Alice sending 10 coins to Bob") - tx_payment = Transaction( - sender=alice_pk, - receiver=bob_pk, - amount=10, - nonce=get_next_nonce(alice_pk), - ) - tx_payment.sign(alice_sk) - mempool.add_transaction(tx_payment) # ------------------------------- # PID Demo: Mining 5 Blocks # ------------------------------- @@ -368,6 +360,14 @@ async def _run_node(network, chain, mempool, pending_nonce_map, get_next_nonce): for i in range(5): await asyncio.sleep(1) + tx_payment = Transaction( + sender=alice_pk, + receiver=bob_pk, + amount=10, + nonce=get_next_nonce(alice_pk), + ) + tx_payment.sign(alice_sk) + mempool.add_transaction(tx_payment) logger.info(f"\nMining Block {i+1}") From 560a82d3d70f4d921de1a070f1a42c50bc776789 Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Mon, 9 Mar 2026 10:17:23 +0530 Subject: [PATCH 13/17] Update pow.py --- minichain/pow.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/minichain/pow.py b/minichain/pow.py index 6540413..12aa367 100644 --- a/minichain/pow.py +++ b/minichain/pow.py @@ -55,9 +55,6 @@ def mine_block( header_dict["nonce"] = local_nonce block_hash = calculate_hash(header_dict) - # Check difficulty target - if block_hash.startswith(target): - # Check difficulty target if block_hash.startswith(target): block.nonce = local_nonce # Assign only on success From cd7c8d98e23dc279db3a257865acfc709be2574a Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Mon, 9 Mar 2026 10:20:39 +0530 Subject: [PATCH 14/17] Update main.py --- main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 34d0297..19dbe9e 100644 --- a/main.py +++ b/main.py @@ -323,8 +323,11 @@ async def _handle_network_data(data): timestamp=block_data.get("timestamp"), difficulty=block_data.get("difficulty"), ) + block.nonce = block_data.get("nonce", 0) + block.hash = block_data.get("hash") + block.miner = block_data.get("miner", BURN_ADDRESS) - chain.add_block(block) + chain.add_block(block) except Exception: logger.exception("Network error while handling incoming data") From 0a2d556b4d13f93a514af4ec521de604bcb1ccb6 Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Mon, 9 Mar 2026 10:28:50 +0530 Subject: [PATCH 15/17] Update main.py --- main.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index 19dbe9e..1400211 100644 --- a/main.py +++ b/main.py @@ -402,14 +402,67 @@ async def _run_node(network, chain, mempool, pending_nonce_map, get_next_nonce): # ------------------------- # Entry Point # ------------------------- + +async def start_interactive_node(port=None, connect=None): + chain = Blockchain() + mempool = Mempool() + network = P2PNetwork() + pending_nonce_map = {} + + sk, pk = create_wallet() + + nonce_counter = [0] + + await network.start(port=port) + + if connect: + host, port_str = connect.rsplit(":", 1) + await network.connect_to_peer(host, int(port_str)) + + try: + await cli_loop(sk, pk, chain, mempool, network, nonce_counter) + finally: + await network.stop() + + +async def run_demo(): + chain = Blockchain() + mempool = Mempool() + network = P2PNetwork() + pending_nonce_map = {} + + await network.start() + + def get_next_nonce(address): + account = chain.state.get_account(address) + account_nonce = account.get("nonce", 0) if account else 0 + local_nonce = pending_nonce_map.get(address, account_nonce) + next_nonce = max(account_nonce, local_nonce) + pending_nonce_map[address] = next_nonce + 1 + return next_nonce + + try: + await _run_node(network, chain, mempool, pending_nonce_map, get_next_nonce) + finally: + await network.stop() + + def main(): - logging.basicConfig( - level=logging.INFO, - format='%(message)s' - ) + parser = argparse.ArgumentParser(description="MiniChain Node") + + parser.add_argument("--port", type=int, help="Port to run node") + parser.add_argument("--connect", help="Peer to connect to host:port") + parser.add_argument("--demo", action="store_true", help="Run Alice/Bob demo") + + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO, format="%(message)s") try: - asyncio.run(node_loop()) + if args.demo: + asyncio.run(run_demo()) + else: + asyncio.run(start_interactive_node(args.port, args.connect)) except KeyboardInterrupt: pass From a17cefcba4fe3056f6e4d846a61a7ef531529a05 Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Wed, 18 Mar 2026 09:01:27 +0530 Subject: [PATCH 16/17] Update difficulty.py --- minichain/consensus/difficulty.py | 71 ++++++++++++++++--------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/minichain/consensus/difficulty.py b/minichain/consensus/difficulty.py index 0e4fdd4..36cff39 100644 --- a/minichain/consensus/difficulty.py +++ b/minichain/consensus/difficulty.py @@ -1,63 +1,64 @@ import time class PIDDifficultyAdjuster: - def __init__(self, target_block_time=5, kp=0.5, ki=0.05, kd=0.1): + SCALE = 1000 # Fixed-point scaling factor + + def __init__(self, target_block_time=5, kp=500, ki=50, kd=100): self.target_block_time = target_block_time - # PID Coefficients - self.kp = kp - self.ki = ki - self.kd = kd + + # PID Coefficients (scaled integers) + self.kp = kp # 0.5 -> 500 + self.ki = ki # 0.05 -> 50 + self.kd = kd # 0.1 -> 100 self.integral = 0 self.previous_error = 0 self.last_block_time = time.monotonic() - - # Limit the integral to prevent "Windup" - # This stops the difficulty from tanking if the network goes offline - self.integral_limit = 100 - - # Max percentage the difficulty can change in one block (e.g., 10%) - self.max_change_factor = 0.1 + + self.integral_limit = 100 * self.SCALE + self.max_change_factor = 0.1 # safe to keep as float OR convert too def adjust(self, current_difficulty, actual_block_time=None): - """ - Calculates the new difficulty based on the time since the last block. - """ - # --- FIX: Handle the case where current_difficulty is None --- + if current_difficulty is None: - current_difficulty = 1000 # Default starting difficulty - + current_difficulty = 1000 + if actual_block_time is None: now = time.monotonic() actual_block_time = now - self.last_block_time self.last_block_time = now - # Error = Goal - Reality - error = self.target_block_time - actual_block_time - - # Update Integral with clamping (Anti-Windup) - self.integral = max(min(self.integral + error, self.integral_limit), -self.integral_limit) - - # Derivative: how fast is the error changing? + # Convert time to scaled integer + actual_block_time = int(actual_block_time * self.SCALE) + target_time = int(self.target_block_time * self.SCALE) + + error = target_time - actual_block_time + + # Integral (clamped) + self.integral = max( + min(self.integral + error, self.integral_limit), + -self.integral_limit + ) + derivative = error - self.previous_error self.previous_error = error - # Calculate PID Adjustment + # Integer PID calculation adjustment = ( self.kp * error + self.ki * self.integral + self.kd * derivative - ) + ) // self.SCALE # scale back + + max_delta = max(1, int(current_difficulty * self.max_change_factor)) - # Apply adjustment with a cap to maintain stability - # Now current_difficulty is guaranteed to be a number - max_delta = max(1, int(round(current_difficulty * self.max_change_factor))) clamped_adjustment = max(min(adjustment, max_delta), -max_delta) - 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 + delta = int(clamped_adjustment) + + if delta == 0 and clamped_adjustment != 0: + delta = 1 if clamped_adjustment > 0 else -1 + + new_difficulty = current_difficulty + delta - # Safety: Difficulty must never drop below 1 return max(1, new_difficulty) From 6a9e9c8001f078c00482fe9e538b12d0b2570fea Mon Sep 17 00:00:00 2001 From: anshulchikhale30p Date: Wed, 18 Mar 2026 09:48:15 +0530 Subject: [PATCH 17/17] Update main.py --- main.py | 469 ++++++++++++-------------------------------------------- 1 file changed, 99 insertions(+), 370 deletions(-) diff --git a/main.py b/main.py index 5221ddf..e9b7573 100644 --- a/main.py +++ b/main.py @@ -1,61 +1,43 @@ """ MiniChain interactive node — testnet demo entry point. - -Usage: - python main.py --port 9000 - python main.py --port 9001 --connect 127.0.0.1:9000 - -Commands (type in the terminal while the node is running): - balance — show all account balances - send — send coins to another address - mine — mine a block from the mempool - peers — show connected peers - connect : — connect to another node - address — show this node's public key - help — show available commands - quit — shut down the node """ import argparse import asyncio import logging import os -import re -import time +import sys + from nacl.signing import SigningKey -import nacl.encoding from nacl.encoding import HexEncoder -from minichain import Transaction, Blockchain, Block, State, Mempool, P2PNetwork, mine_block +from minichain import Transaction, Blockchain, Block, Mempool, P2PNetwork, mine_block from minichain.validators import is_valid_receiver -# Local project imports -from minichain import Transaction, Blockchain, Block, mine_block, Mempool, P2PNetwork - logger = logging.getLogger(__name__) BURN_ADDRESS = "0" * 40 +# ────────────────────────────────────────────── +# Wallet helpers +# ────────────────────────────────────────────── -# ------------------------- -# Wallet Creation -# ------------------------- def create_wallet(): sk = SigningKey.generate() - pk = sk.verify_key.encode(encoder=nacl.encoding.HexEncoder).decode() + pk = sk.verify_key.encode(encoder=HexEncoder).decode() return sk, pk +# ────────────────────────────────────────────── +# Block mining (FIXED per requirements) +# ────────────────────────────────────────────── -# ------------------------- -# Mining + Block Processing -# ------------------------- -def mine_and_process_block(chain, mempool, pending_nonce_map): +def mine_and_process_block(chain, mempool, miner_pk): + """Mine pending transactions into a new block.""" pending_txs = mempool.get_transactions_for_block() if not pending_txs: logger.info("Mempool is empty — nothing to mine.") return None - # Filter queue candidates against a temporary state snapshot. temp_state = chain.state.copy() mineable_txs = [] stale_txs = [] @@ -74,51 +56,19 @@ def mine_and_process_block(chain, mempool, pending_nonce_map): logger.info("No mineable transactions in current queue window.") return None + # REQUIREMENT: Pass chain.difficulty into the Block constructor block = Block( index=chain.last_block.index + 1, previous_hash=chain.last_block.hash, transactions=mineable_txs, + difficulty=chain.difficulty # Passed here directly ) - # Mine using current consensus difficulty; chain updates next difficulty after acceptance - block.difficulty = chain.difficulty - - start_time = time.time() + # REQUIREMENT: mine_block() called inside this function mined_block = mine_block(block, difficulty=block.difficulty) - mining_time = time.time() - start_time - - # Attach mining time to block (optional but useful) - mined_block.mining_time = mining_time - - if not hasattr(mined_block, "miner"): - mined_block.miner = BURN_ADDRESS - - deployed_contracts = [] if chain.add_block(mined_block): - logger.info("Block #%s added with Difficulty: %s", - mined_block.index, - mined_block.difficulty) - - # Reward miner - miner_attr = getattr(mined_block, "miner", BURN_ADDRESS) - miner_address = ( - miner_attr if re.match(r'^[0-9a-fA-F]{40}$', str(miner_attr)) - else BURN_ADDRESS - ) - - chain.state.credit_mining_reward(miner_address) - - for tx in mined_block.transactions: - sync_nonce(chain.state, pending_nonce_map, tx.sender) - - result = chain.state.get_account(tx.receiver) if tx.receiver else None - if isinstance(result, dict): - deployed_contracts.append(tx.receiver) - - return mined_block, deployed_contracts - - logger.info("✅ Block #%d mined and added (%d txs)", mined_block.index, len(mineable_txs)) + logger.info(" Block #%d mined and added (%d txs)", mined_block.index, len(mineable_txs)) mempool.remove_transactions(mineable_txs) chain.state.credit_mining_reward(miner_pk) return mined_block @@ -126,35 +76,29 @@ def mine_and_process_block(chain, mempool, pending_nonce_map): logger.error("❌ Block rejected by chain") return None - # ────────────────────────────────────────────── # Network message handler # ────────────────────────────────────────────── def make_network_handler(chain, mempool): - """Return an async callback that processes incoming P2P messages.""" - async def handler(data): msg_type = data.get("type") payload = data.get("data") if msg_type == "sync": - # Merge remote state into local state (for accounts we don't have yet) remote_accounts = payload.get("accounts", {}) for addr, acc in remote_accounts.items(): if addr not in chain.state.accounts: chain.state.accounts[addr] = acc - logger.info("🔄 Synced account %s... (balance=%d)", addr[:12], acc.get("balance", 0)) - logger.info("🔄 State sync complete — %d accounts", len(chain.state.accounts)) + logger.info(" State sync complete — %d accounts", len(chain.state.accounts)) elif msg_type == "tx": tx = Transaction(**payload) if mempool.add_transaction(tx): - logger.info("📥 Received tx from %s... (amount=%s)", tx.sender[:8], tx.amount) + logger.info("📥 Received tx from %s...", tx.sender[:8]) elif msg_type == "block": txs_raw = payload.get("transactions", []) - block_hash = payload.get("hash") transactions = [Transaction(**t) for t in txs_raw] block = Block( @@ -165,46 +109,37 @@ async def handler(data): difficulty=payload.get("difficulty"), ) block.nonce = payload.get("nonce", 0) - block.hash = block_hash + block.hash = payload.get("hash") if chain.add_block(block): logger.info("📥 Received Block #%d — added to chain", block.index) - - # Apply mining reward for the remote miner (burn address as placeholder) miner = payload.get("miner", BURN_ADDRESS) chain.state.credit_mining_reward(miner) - - # Drop only confirmed transactions so higher nonces can remain queued. mempool.remove_transactions(block.transactions) - else: - logger.warning("📥 Received Block #%s — rejected", block.index) return handler - # ────────────────────────────────────────────── # Interactive CLI # ────────────────────────────────────────────── HELP_TEXT = """ ╔════════════════════════════════════════════════╗ -║ MiniChain Commands ║ +║ MiniChain Commands ║ ╠════════════════════════════════════════════════╣ -║ balance — show all balances ║ -║ send — send coins ║ -║ mine — mine a block ║ -║ peers — show connected peers ║ -║ connect — connect to a peer ║ -║ address — show your public key ║ -║ chain — show chain summary ║ -║ help — show this help ║ -║ quit — shut down ║ +║ balance — show all balances ║ +║ send <#> — send coins ║ +║ mine — mine a block ║ +║ peers — show connected peers ║ +║ connect — connect to a peer ║ +║ address — show your public key ║ +║ chain — show chain summary ║ +║ help — show this help ║ +║ quit — shut down ║ ╚════════════════════════════════════════════════╝ """ - async def cli_loop(sk, pk, chain, mempool, network): - """Read commands from stdin asynchronously.""" loop = asyncio.get_event_loop() print(HELP_TEXT) print(f"Your address: {pk}\n") @@ -212,323 +147,117 @@ async def cli_loop(sk, pk, chain, mempool, network): while True: try: raw = await loop.run_in_executor(None, lambda: input("minichain> ")) - except (EOFError, KeyboardInterrupt): - break + parts = raw.strip().split() + if not parts: continue + cmd = parts[0].lower() + + if cmd == "balance": + for addr, acc in chain.state.accounts.items(): + tag = " (you)" if addr == pk else "" + print(f" {addr[:12]}... balance={acc['balance']} nonce={acc['nonce']}{tag}") + + elif cmd == "send": + if len(parts) < 3: continue + receiver, amount = parts[1], int(parts[2]) + nonce = chain.state.get_account(pk).get("nonce", 0) + tx = Transaction(sender=pk, receiver=receiver, amount=amount, nonce=nonce) + tx.sign(sk) + if mempool.add_transaction(tx): + await network.broadcast_transaction(tx) + print(f" Tx sent: {amount} coins") - parts = raw.strip().split() - if not parts: - continue - cmd = parts[0].lower() - - # ── balance ── - if cmd == "balance": - accounts = chain.state.accounts - if not accounts: - print(" (no accounts yet)") - for addr, acc in accounts.items(): - tag = " (you)" if addr == pk else "" - print(f" {addr[:12]}... balance={acc['balance']} nonce={acc['nonce']}{tag}") - - # ── send ── - elif cmd == "send": - if len(parts) < 3: - print(" Usage: send ") - continue - receiver = parts[1] - if not is_valid_receiver(receiver): - print(" Invalid receiver format. Expected 40 or 64 hex characters.") - continue - try: - amount = int(parts[2]) - except ValueError: - print(" Amount must be an integer.") - continue - if amount <= 0: - print(" Amount must be greater than 0.") - continue - - nonce = chain.state.get_account(pk).get("nonce", 0) - tx = Transaction(sender=pk, receiver=receiver, amount=amount, nonce=nonce) - tx.sign(sk) + elif cmd == "mine": + mined = mine_and_process_block(chain, mempool, pk) + if mined: + await network.broadcast_block(mined, miner=pk) - if mempool.add_transaction(tx): - await network.broadcast_transaction(tx) - print(f" ✅ Tx sent: {amount} coins → {receiver[:12]}...") - else: - print(" ❌ Transaction rejected (invalid sig, duplicate, or mempool full).") - - # ── mine ── - elif cmd == "mine": - mined = mine_and_process_block(chain, mempool, pk) - if mined: - await network.broadcast_block(mined, miner=pk) - - # ── peers ── - elif cmd == "peers": - print(f" Connected peers: {network.peer_count}") - - # ── connect ── - elif cmd == "connect": - if len(parts) < 2: - print(" Usage: connect :") - continue - try: - host, port_str = parts[1].rsplit(":", 1) - port = int(port_str) - except ValueError: - print(" Invalid format. Use host:port") - continue - await network.connect_to_peer(host, port) - - # ── address ── - elif cmd == "address": - print(f" {pk}") - - # ── chain ── - elif cmd == "chain": - print(f" Chain length: {len(chain.chain)} blocks") - for b in chain.chain: - tx_count = len(b.transactions) if b.transactions else 0 - print(f" Block #{b.index} hash={b.hash[:16]}... txs={tx_count}") - - # ── help ── - elif cmd == "help": - print(HELP_TEXT) - - # ── quit ── - elif cmd in ("quit", "exit", "q"): - break - - else: - print(f" Unknown command: {cmd}. Type 'help' for available commands.") - -# ------------------------- -# Nonce Sync -# ------------------------- -def sync_nonce(state, pending_nonce_map, address): - account = state.get_account(address) - pending_nonce_map[address] = account.get("nonce", 0) if account else 0 + elif cmd == "peers": + print(f" Connected peers: {network.peer_count}") + + elif cmd == "connect": + host, port = parts[1].rsplit(":", 1) + await network.connect_to_peer(host, int(port)) + + elif cmd == "address": + print(f" {pk}") + + elif cmd == "chain": + print(f" Chain length: {len(chain.chain)} blocks") + + elif cmd == "help": + print(HELP_TEXT) + + elif cmd in ("quit", "exit", "q"): + break + except Exception as e: + print(f" Error: {e}") # ────────────────────────────────────────────── # Main entry point # ────────────────────────────────────────────── -# ------------------------- -# Node Logic -# ------------------------- -async def node_loop(): - logger.info("Starting MiniChain Node with PID Difficulty Adjustment") async def run_node(port: int, connect_to: str | None, fund: int, datadir: str | None): - """Boot the node, optionally connect to a peer, then enter the CLI.""" sk, pk = create_wallet() - - # Load existing chain from disk, or start fresh chain = None + + # REQUIREMENT: Preserve persistence.py we added if datadir and os.path.exists(os.path.join(datadir, "data.json")): try: from minichain.persistence import load chain = load(datadir) logger.info("Restored chain from '%s'", datadir) - except FileNotFoundError as e: + except Exception as e: logger.warning("Could not load saved chain: %s — starting fresh", e) - except ValueError as e: - logger.error("State data is corrupted or tampered: %s", e) - logger.error("Refusing to start to avoid overwriting corrupted data.") - sys.exit(1) if chain is None: chain = Blockchain() mempool = Mempool() network = P2PNetwork() - pending_nonce_map = {} + handler = make_network_handler(chain, mempool) + network.register_handler(handler) - def get_next_nonce(address) -> int: - account = chain.state.get_account(address) - account_nonce = account.get("nonce", 0) if account else 0 - local_nonce = pending_nonce_map.get(address, account_nonce) - next_nonce = max(account_nonce, local_nonce) - pending_nonce_map[address] = next_nonce + 1 - return next_nonce - - async def _handle_network_data(data): - try: - if data["type"] == "tx": - tx = Transaction(**data["data"]) - if mempool.add_transaction(tx): - await network.broadcast_transaction(tx) - - elif data["type"] == "block": - block_data = data["data"] - txs = [ - Transaction(**tx_d) - for tx_d in block_data.get("transactions", []) - ] - - block = Block( - index=block_data["index"], - previous_hash=block_data["previous_hash"], - transactions=txs, - timestamp=block_data.get("timestamp"), - difficulty=block_data.get("difficulty"), - ) - block.nonce = block_data.get("nonce", 0) - block.hash = block_data.get("hash") - block.miner = block_data.get("miner", BURN_ADDRESS) - - chain.add_block(block) - except Exception: - logger.exception("Network error while handling incoming data") - - # Nonce counter kept as a mutable list so the CLI closure can mutate it - nonce_counter = [0] - - try: - await _run_node(network, chain, mempool, pending_nonce_map, get_next_nonce) - finally: - await network.stop() - - -# ------------------------- -# Run Node -# ------------------------- -async def _run_node(network, chain, mempool, pending_nonce_map, get_next_nonce): - await network.start() - - alice_sk, alice_pk = create_wallet() - _bob_sk, bob_pk = create_wallet() - - # Initial funding - chain.state.credit_mining_reward(alice_pk, reward=100) - sync_nonce(chain.state, pending_nonce_map, alice_pk) - - # Alice sends Bob 10 coins - logger.info("[2] Alice sending 10 coins to Bob") - - # ------------------------------- - # PID Demo: Mining 5 Blocks - # ------------------------------- - logger.info("[3] Mining Multiple Blocks (Watch Difficulty Adjust)") - - for i in range(5): - await asyncio.sleep(1) - tx_payment = Transaction( - sender=alice_pk, - receiver=bob_pk, - amount=10, - nonce=get_next_nonce(alice_pk), - ) - tx_payment.sign(alice_sk) - mempool.add_transaction(tx_payment) - - logger.info(f"\nMining Block {i+1}") - - mined = mine_and_process_block(chain, mempool, pending_nonce_map) - if not mined: - logger.info("No pending transactions to mine in this iteration") - continue - mined_block, _ = mined - - - if mined_block: - logger.info("Block mined in %.2f seconds", - mined_block.mining_time) - - logger.info("New difficulty: %s", - chain.difficulty) - - # Final balances - alice_acc = chain.state.get_account(alice_pk) - bob_acc = chain.state.get_account(bob_pk) - - logger.info( - "Final Balances -> Alice: %s, Bob: %s", - alice_acc.get("balance", 0), - bob_acc.get("balance", 0), - ) - - -# ------------------------- -# Entry Point -# ------------------------- - -async def start_interactive_node(port=None, connect=None): - chain = Blockchain() - mempool = Mempool() - network = P2PNetwork() - pending_nonce_map = {} - - sk, pk = create_wallet() - - nonce_counter = [0] + # State sync on connection + async def on_peer_connected(writer): + import json + sync_msg = json.dumps({"type": "sync", "data": {"accounts": chain.state.accounts}}) + "\n" + writer.write(sync_msg.encode()) + await writer.drain() + network._on_peer_connected = on_peer_connected await network.start(port=port) - if connect: - host, port_str = connect.rsplit(":", 1) - await network.connect_to_peer(host, int(port_str)) + if fund > 0: + chain.state.credit_mining_reward(pk, reward=fund) + + if connect_to: + host, p = connect_to.rsplit(":", 1) + await network.connect_to_peer(host, int(p)) try: await cli_loop(sk, pk, chain, mempool, network) finally: - # Save chain to disk on shutdown + # REQUIREMENT: Save on shutdown if datadir: - try: - from minichain.persistence import save - save(chain, datadir) - logger.info("Chain saved to '%s'", datadir) - except Exception as e: - logger.error("Failed to save chain during shutdown: %s", e) + from minichain.persistence import save + save(chain, datadir) + logger.info("Chain saved.") await network.stop() - -async def run_demo(): - chain = Blockchain() - mempool = Mempool() - network = P2PNetwork() - pending_nonce_map = {} - - await network.start() - - def get_next_nonce(address): - account = chain.state.get_account(address) - account_nonce = account.get("nonce", 0) if account else 0 - local_nonce = pending_nonce_map.get(address, account_nonce) - next_nonce = max(account_nonce, local_nonce) - pending_nonce_map[address] = next_nonce + 1 - return next_nonce - - try: - await _run_node(network, chain, mempool, pending_nonce_map, get_next_nonce) - finally: - await network.stop() - - def main(): - parser = argparse.ArgumentParser(description="MiniChain Node") - - parser.add_argument("--port", type=int, help="Port to run node") - parser.add_argument("--connect", help="Peer to connect to host:port") - parser.add_argument("--demo", action="store_true", help="Run Alice/Bob demo") - - parser = argparse.ArgumentParser(description="MiniChain Node — Testnet Demo") - parser.add_argument("--port", type=int, default=9000, help="TCP port to listen on (default: 9000)") - parser.add_argument("--connect", type=str, default=None, help="Peer address to connect to (host:port)") - parser.add_argument("--fund", type=int, default=100, help="Initial coins to fund this wallet (default: 100)") - parser.add_argument("--datadir", type=str, default=None, help="Directory to save/load blockchain state (enables persistence)") + parser = argparse.ArgumentParser() + parser.add_argument("--port", type=int, default=9000) + parser.add_argument("--connect", type=str, default=None) + parser.add_argument("--fund", type=int, default=100) + parser.add_argument("--datadir", type=str, default=None) args = parser.parse_args() - logging.basicConfig(level=logging.INFO, format="%(message)s") + logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") try: - if args.demo: - asyncio.run(run_demo()) - else: - asyncio.run(start_interactive_node(args.port, args.connect)) asyncio.run(run_node(args.port, args.connect, args.fund, args.datadir)) except KeyboardInterrupt: pass - if __name__ == "__main__": main()