From e232e2c2d1681d75904b6713d3c395fb85f42b13 Mon Sep 17 00:00:00 2001 From: siddhant Date: Sun, 31 May 2026 04:06:14 +0530 Subject: [PATCH 1/4] feat: add receipts --- main.py | 10 +++++- minichain/block.py | 58 +++++++++++++++++++++---------- minichain/chain.py | 19 +++++++--- minichain/p2p.py | 2 ++ minichain/receipt.py | 34 ++++++++++++++++++ minichain/state.py | 33 ++++++++---------- tests/test_contract.py | 49 ++++++++++++++++++-------- tests/test_persistence.py | 10 ++++-- tests/test_persistence_runtime.py | 7 ++++ tests/test_protocol_hardening.py | 15 +++++++- 10 files changed, 177 insertions(+), 60 deletions(-) create mode 100644 minichain/receipt.py diff --git a/main.py b/main.py index 17de79a..4fb8c70 100644 --- a/main.py +++ b/main.py @@ -61,13 +61,17 @@ def mine_and_process_block(chain, mempool, miner_pk): temp_state = chain.state.copy() mineable_txs = [] stale_txs = [] + receipts = [] for tx in pending_txs: expected_nonce = temp_state.get_account(tx.sender).get("nonce", 0) if tx.nonce < expected_nonce: stale_txs.append(tx) continue - if temp_state.validate_and_apply(tx): + + receipt = temp_state.validate_and_apply(tx) + if receipt is not None: mineable_txs.append(tx) + receipts.append(receipt) if stale_txs: mempool.remove_transactions(stale_txs) @@ -78,11 +82,15 @@ def mine_and_process_block(chain, mempool, miner_pk): temp_state.credit_mining_reward(miner_pk) + from minichain.block import _calculate_receipt_root + block = Block( index=chain.last_block.index + 1, previous_hash=chain.last_block.hash, transactions=mineable_txs, state_root=temp_state.state_root(), + receipt_root=_calculate_receipt_root(receipts), + receipts=receipts, miner=miner_pk, ) diff --git a/minichain/block.py b/minichain/block.py index d68d985..e0d6805 100644 --- a/minichain/block.py +++ b/minichain/block.py @@ -2,35 +2,37 @@ import hashlib from typing import List, Optional from .transaction import Transaction +from .receipt import Receipt from .serialization import canonical_json_hash def _sha256(data: str) -> str: return hashlib.sha256(data.encode()).hexdigest() -def _calculate_merkle_root(transactions: List[Transaction]) -> Optional[str]: - if not transactions: +def _calculate_merkle_tree(hashes: List[str]) -> Optional[str]: + if not hashes: return None - - # Hash each transaction deterministically - tx_hashes = [ - tx.tx_id - for tx in transactions - ] - - # Build Merkle tree - while len(tx_hashes) > 1: - if len(tx_hashes) % 2 != 0: - tx_hashes.append(tx_hashes[-1]) # duplicate last if odd - + while len(hashes) > 1: + if len(hashes) % 2 != 0: + hashes.append(hashes[-1]) new_level = [] - for i in range(0, len(tx_hashes), 2): - combined = tx_hashes[i] + tx_hashes[i + 1] + for i in range(0, len(hashes), 2): + combined = hashes[i] + hashes[i + 1] new_level.append(_sha256(combined)) + hashes = new_level + return hashes[0] - tx_hashes = new_level +def _calculate_merkle_root(transactions: List[Transaction]) -> Optional[str]: + if not transactions: + return None + return _calculate_merkle_tree([tx.tx_id for tx in transactions]) - return tx_hashes[0] +def _calculate_receipt_root(receipts: List[Receipt]) -> Optional[str]: + if not receipts: + return None + return _calculate_merkle_tree([canonical_json_hash(r.to_dict()) for r in receipts]) + + # Logic moved to _calculate_merkle_tree class Block: @@ -42,11 +44,14 @@ def __init__( timestamp: Optional[float] = None, difficulty: Optional[int] = None, state_root: Optional[str] = None, + receipt_root: Optional[str] = None, + receipts: Optional[List[Receipt]] = None, miner: Optional[str] = None, ): self.index = index self.previous_hash = previous_hash self.transactions: List[Transaction] = transactions or [] + self.receipts: List[Receipt] = receipts or [] # Deterministic timestamp (ms) self.timestamp: int = ( @@ -59,10 +64,15 @@ def __init__( self.nonce: int = 0 self.hash: Optional[str] = None self.state_root: Optional[str] = state_root + self.receipt_root: Optional[str] = receipt_root self.miner: Optional[str] = miner - # NEW: compute merkle root once + # NEW: compute merkle roots once self.merkle_root: Optional[str] = _calculate_merkle_root(self.transactions) + + # If receipt_root is missing but we have receipts, calculate it. + if self.receipt_root is None and self.receipts: + self.receipt_root = _calculate_receipt_root(self.receipts) # ------------------------- # HEADER (used for mining) @@ -73,6 +83,7 @@ def to_header_dict(self): "previous_hash": self.previous_hash, "merkle_root": self.merkle_root, "state_root": self.state_root, + "receipt_root": self.receipt_root, "timestamp": self.timestamp, "difficulty": self.difficulty, "nonce": self.nonce, @@ -86,6 +97,9 @@ def to_body_dict(self): return { "transactions": [ tx.to_dict() for tx in self.transactions + ], + "receipts": [ + r.to_dict() for r in self.receipts ] } @@ -111,6 +125,10 @@ def from_dict(cls, payload: dict): Transaction.from_dict(tx_payload) for tx_payload in payload.get("transactions", []) ] + receipts = [ + Receipt.from_dict(r_payload) + for r_payload in payload.get("receipts", []) + ] block = cls( index=payload["index"], previous_hash=payload["previous_hash"], @@ -118,6 +136,8 @@ def from_dict(cls, payload: dict): timestamp=payload.get("timestamp"), difficulty=payload.get("difficulty"), state_root=payload.get("state_root"), + receipt_root=payload.get("receipt_root"), + receipts=receipts, miner=payload.get("miner"), ) block.nonce = payload.get("nonce", 0) diff --git a/minichain/chain.py b/minichain/chain.py index c7fe286..2d11aa5 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -72,7 +72,9 @@ def _create_genesis_block(self, genesis_path): transactions=[], timestamp=timestamp, difficulty=difficulty, - state_root=self.state.state_root() + state_root=self.state.state_root(), + receipt_root=None, + receipts=[] ) computed_hash = calculate_hash(genesis_block.to_header_dict()) @@ -111,17 +113,26 @@ def add_block(self, block): # Validate transactions on a temporary state copy temp_state = self.state.copy() + receipts = [] for tx in block.transactions: - result = temp_state.validate_and_apply(tx) + receipt = temp_state.validate_and_apply(tx) - # Reject block if any transaction fails - if not result: + # Reject block if any transaction fails mathematical validation (None) + if receipt is None: logger.warning("Block %s rejected: Transaction failed validation", block.index) return False + + receipts.append(receipt) if block.miner: temp_state.credit_mining_reward(block.miner) + + from .block import _calculate_receipt_root + computed_receipt_root = _calculate_receipt_root(receipts) + if block.receipt_root != computed_receipt_root: + logger.warning("Block %s rejected: Invalid receipt root. Expected %s, got %s", block.index, computed_receipt_root, block.receipt_root) + return False # Verify state root if block.state_root != temp_state.state_root(): diff --git a/minichain/p2p.py b/minichain/p2p.py index 7462962..2374e92 100644 --- a/minichain/p2p.py +++ b/minichain/p2p.py @@ -184,6 +184,8 @@ def _validate_block_payload(self, payload): "previous_hash": str, "merkle_root": (str, type(None)), "state_root": str, + "receipt_root": (str, type(None)), + "receipts": list, "transactions": list, "timestamp": int, "difficulty": (int, type(None)), diff --git a/minichain/receipt.py b/minichain/receipt.py new file mode 100644 index 0000000..60053c9 --- /dev/null +++ b/minichain/receipt.py @@ -0,0 +1,34 @@ +from typing import List, Optional + +class Receipt: + """ + Represents the execution result of a transaction. + """ + def __init__(self, tx_hash: str, status: int, gas_used: int = 0, error_message: Optional[str] = None, logs: Optional[List[dict]] = None, contract_address: Optional[str] = None): + self.tx_hash = tx_hash + self.status = status # 1 for success, 0 for failure + self.gas_used = gas_used + self.error_message = error_message + self.logs = logs or [] + self.contract_address = contract_address + + def to_dict(self) -> dict: + return { + "tx_hash": self.tx_hash, + "status": self.status, + "gas_used": self.gas_used, + "error_message": self.error_message, + "logs": self.logs, + "contract_address": self.contract_address + } + + @classmethod + def from_dict(cls, payload: dict) -> 'Receipt': + return cls( + tx_hash=payload["tx_hash"], + status=payload["status"], + gas_used=payload.get("gas_used", 0), + error_message=payload.get("error_message"), + logs=payload.get("logs", []), + contract_address=payload.get("contract_address") + ) diff --git a/minichain/state.py b/minichain/state.py index e718534..0d5c088 100644 --- a/minichain/state.py +++ b/minichain/state.py @@ -74,20 +74,19 @@ def validate_and_apply(self, tx): """ # Semantic validation: amount must be an integer and non-negative if not isinstance(tx.amount, int) or tx.amount < 0: - return False + return None # Further checks can be added here return self.apply_transaction(tx) def apply_transaction(self, tx): + from .receipt import Receipt + """ Applies transaction and mutates state. - Returns: - - Contract address (str) if deployment - - True if successful execution - - False if failed + Returns: Receipt object if mathematically valid, None if invalid. """ if not self.verify_transaction_logic(tx): - return False + return None sender = self.accounts[tx.sender] @@ -102,12 +101,12 @@ def apply_transaction(self, tx): # Prevent redeploy collision existing = self.accounts.get(contract_address) if existing and existing.get("code"): - # Restore sender state on failure + # Restore sender balance on failure, but keep nonce incremented sender['balance'] += tx.amount - sender['nonce'] -= 1 - return False + return Receipt(tx.tx_id, status=0, error_message="Contract collision") - return self.create_contract(contract_address, tx.data, initial_balance=tx.amount) + self.create_contract(contract_address, tx.data, initial_balance=tx.amount) + return Receipt(tx.tx_id, status=1, contract_address=contract_address) # LOGIC BRANCH 2: Contract Call # If data is provided (non-empty), treat as contract call @@ -116,10 +115,9 @@ def apply_transaction(self, tx): # Fail if contract does not exist or has no code if not receiver or not receiver.get("code"): - # Rollback sender balance and nonce on failure + # Rollback sender balance on failure, but keep nonce incremented sender['balance'] += tx.amount # Refund amount - sender['nonce'] -= 1 - return False + return Receipt(tx.tx_id, status=0, error_message="Contract not found") # Credit contract balance receiver['balance'] += tx.amount @@ -132,18 +130,17 @@ def apply_transaction(self, tx): ) if not success: - # Rollback transfer and nonce if execution fails + # Rollback transfer if execution fails, but keep nonce incremented receiver['balance'] -= tx.amount sender['balance'] += tx.amount # Refund amount - sender['nonce'] -= 1 - return False + return Receipt(tx.tx_id, status=0, error_message="Execution failed") - return True + return Receipt(tx.tx_id, status=1) # LOGIC BRANCH 3: Regular Transfer receiver = self.get_account(tx.receiver) receiver['balance'] += tx.amount - return True + return Receipt(tx.tx_id, status=1) def derive_contract_address(self, sender, nonce): raw = f"{sender}:{nonce}".encode() diff --git a/tests/test_contract.py b/tests/test_contract.py index 2ac6e9f..49431e8 100644 --- a/tests/test_contract.py +++ b/tests/test_contract.py @@ -26,14 +26,18 @@ def test_deploy_and_execute(self): tx_deploy = Transaction(self.pk, None, 0, 0, data=code) tx_deploy.sign(self.sk) - contract_addr = self.state.apply_transaction(tx_deploy) + receipt_deploy = self.state.apply_transaction(tx_deploy) + self.assertIsNotNone(receipt_deploy) + self.assertEqual(receipt_deploy.status, 1) + contract_addr = receipt_deploy.contract_address self.assertTrue(isinstance(contract_addr, str)) tx_call = Transaction(self.pk, contract_addr, 0, 1, data="increment") tx_call.sign(self.sk) - success = self.state.apply_transaction(tx_call) - self.assertTrue(success) + receipt_call = self.state.apply_transaction(tx_call) + self.assertIsNotNone(receipt_call) + self.assertEqual(receipt_call.status, 1) contract_acc = self.state.get_account(contract_addr) self.assertEqual(contract_acc["storage"]["counter"], 1) @@ -49,8 +53,9 @@ def test_deploy_insufficient_balance(self): tx = Transaction(poor_pk, None, 1000, 0, data=code) tx.sign(poor_sk) - result = self.state.apply_transaction(tx) - self.assertFalse(result) + receipt = self.state.apply_transaction(tx) + # deploy with insufficient balance should fail mathematical validation entirely + self.assertIsNone(receipt) def test_call_non_existent_contract(self): """Calling unknown contract should fail with valid hex receiver.""" @@ -61,8 +66,10 @@ def test_call_non_existent_contract(self): tx = Transaction(self.pk, fake_receiver, 0, 0, data="increment") tx.sign(self.sk) - result = self.state.apply_transaction(tx) - self.assertFalse(result) + receipt = self.state.apply_transaction(tx) + self.assertIsNotNone(receipt) + self.assertEqual(receipt.status, 0) + self.assertEqual(receipt.error_message, "Contract not found") def test_contract_runtime_exception(self): """Contract raising exception should fail and not mutate storage.""" @@ -74,14 +81,19 @@ def test_contract_runtime_exception(self): tx_deploy = Transaction(self.pk, None, 0, 0, data=code) tx_deploy.sign(self.sk) - contract_addr = self.state.apply_transaction(tx_deploy) + receipt_deploy = self.state.apply_transaction(tx_deploy) + self.assertIsNotNone(receipt_deploy) + self.assertEqual(receipt_deploy.status, 1) + contract_addr = receipt_deploy.contract_address self.assertTrue(isinstance(contract_addr, str)) tx_call = Transaction(self.pk, contract_addr, 0, 1, data="anything") tx_call.sign(self.sk) - result = self.state.apply_transaction(tx_call) - self.assertFalse(result) + receipt_call = self.state.apply_transaction(tx_call) + self.assertIsNotNone(receipt_call) + self.assertEqual(receipt_call.status, 0) + self.assertEqual(receipt_call.error_message, "Execution failed") contract_acc = self.state.get_account(contract_addr) self.assertEqual(contract_acc["storage"], {}) @@ -95,7 +107,10 @@ def test_redeploy_same_address(self): tx1 = Transaction(self.pk, None, 0, 0, data=code) tx1.sign(self.sk) - addr = self.state.apply_transaction(tx1) + receipt1 = self.state.apply_transaction(tx1) + self.assertIsNotNone(receipt1) + self.assertEqual(receipt1.status, 1) + addr = receipt1.contract_address self.assertTrue(isinstance(addr, str)) # Compute the address that a second deploy would use @@ -109,8 +124,10 @@ def test_redeploy_same_address(self): tx2 = Transaction(self.pk, None, 0, next_nonce, data=code) tx2.sign(self.sk) - result = self.state.apply_transaction(tx2) - self.assertFalse(result) + receipt2 = self.state.apply_transaction(tx2) + self.assertIsNotNone(receipt2) + self.assertEqual(receipt2.status, 0) + self.assertEqual(receipt2.error_message, "Contract collision") def test_balance_and_nonce_updates(self): """Verify sender balance and nonce after deploy and call.""" @@ -124,8 +141,10 @@ def test_balance_and_nonce_updates(self): tx_deploy = Transaction(self.pk, None, 10, initial_nonce, data=code) tx_deploy.sign(self.sk) - # Corrected typo: contract_add_ to contract_addr - contract_addr = self.state.apply_transaction(tx_deploy) + receipt = self.state.apply_transaction(tx_deploy) + self.assertIsNotNone(receipt) + self.assertEqual(receipt.status, 1) + contract_addr = receipt.contract_address self.assertTrue(isinstance(contract_addr, str)) # Verify balance and nonce after deploy diff --git a/tests/test_persistence.py b/tests/test_persistence.py index fe2d78f..a615892 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -42,14 +42,17 @@ def _chain_with_tx(self): tx.sign(alice_sk) temp_state = bc.state.copy() - temp_state.validate_and_apply(tx) + receipt = temp_state.validate_and_apply(tx) + from minichain.block import _calculate_receipt_root block = Block( index=1, previous_hash=bc.last_block.hash, transactions=[tx], difficulty=1, state_root=temp_state.state_root(), + receipt_root=_calculate_receipt_root([receipt]), + receipts=[receipt], ) mine_block(block, difficulty=1) bc.add_block(block) @@ -221,14 +224,17 @@ def test_loaded_chain_can_add_new_block(self): tx2.sign(new_sk) temp_state = restored.state.copy() - temp_state.validate_and_apply(tx2) + receipt2 = temp_state.validate_and_apply(tx2) + from minichain.block import _calculate_receipt_root block2 = Block( index=len(restored.chain), previous_hash=restored.last_block.hash, transactions=[tx2], difficulty=1, state_root=temp_state.state_root(), + receipt_root=_calculate_receipt_root([receipt2]), + receipts=[receipt2], ) mine_block(block2, difficulty=1) diff --git a/tests/test_persistence_runtime.py b/tests/test_persistence_runtime.py index 21d437b..15e9ef7 100644 --- a/tests/test_persistence_runtime.py +++ b/tests/test_persistence_runtime.py @@ -63,11 +63,18 @@ def _chain_with_tx(self): tx = Transaction(alice_pk, bob_pk, 30, 0) tx.sign(alice_sk) + temp_state = bc.state.copy() + receipt = temp_state.validate_and_apply(tx) + + from minichain.block import _calculate_receipt_root block = Block( index=1, previous_hash=bc.last_block.hash, transactions=[tx], difficulty=1, + state_root=temp_state.state_root(), + receipt_root=_calculate_receipt_root([receipt]), + receipts=[receipt], ) mine_block(block, difficulty=1) bc.add_block(block) diff --git a/tests/test_protocol_hardening.py b/tests/test_protocol_hardening.py index ba9028c..7644f38 100644 --- a/tests/test_protocol_hardening.py +++ b/tests/test_protocol_hardening.py @@ -108,7 +108,20 @@ async def test_block_schema_accepts_current_block_wire_format(self): tx = Transaction(sender_pk, receiver_pk, 1, 0, timestamp=123) tx.sign(sender_sk) - block = Block(index=1, previous_hash="0" * 64, transactions=[tx], timestamp=456, difficulty=2, state_root="0"*64) + from minichain.receipt import Receipt + from minichain.block import _calculate_receipt_root + receipt = Receipt(tx_hash=tx.tx_id, status=1) + + block = Block( + index=1, + previous_hash="0" * 64, + transactions=[tx], + timestamp=456, + difficulty=2, + state_root="0"*64, + receipts=[receipt], + receipt_root=_calculate_receipt_root([receipt]) + ) block.nonce = 9 block.hash = block.compute_hash() From 8e80004191a708615db069dcedea8bb2a15e3099 Mon Sep 17 00:00:00 2001 From: siddhant Date: Wed, 3 Jun 2026 19:29:12 +0530 Subject: [PATCH 2/4] implement trie lib, remove redundant code --- main.py | 6 +- minichain/block.py | 6 +- minichain/chain.py | 13 ++- minichain/contract.py | 4 +- minichain/mpt.py | 160 ++---------------------------- minichain/p2p.py | 2 +- minichain/persistence.py | 8 +- minichain/state.py | 17 ++-- requirements.txt | 2 +- setup.py | 2 +- tests/test_persistence.py | 8 +- tests/test_persistence_runtime.py | 4 +- tests/test_protocol_hardening.py | 4 +- 13 files changed, 42 insertions(+), 194 deletions(-) diff --git a/main.py b/main.py index 4fb8c70..9d1e774 100644 --- a/main.py +++ b/main.py @@ -27,11 +27,11 @@ from minichain import Transaction, Blockchain, Block, State, Mempool, P2PNetwork, mine_block from minichain.validators import is_valid_receiver +from minichain.block import calculate_receipt_root logger = logging.getLogger(__name__) -BURN_ADDRESS = "0" * 40 TRUSTED_PEERS = set() LOCALHOST_PEERS = {"127.0.0.1", "::1", "localhost", "0:0:0:0:0:0:0:1"} @@ -82,14 +82,12 @@ def mine_and_process_block(chain, mempool, miner_pk): temp_state.credit_mining_reward(miner_pk) - from minichain.block import _calculate_receipt_root - block = Block( index=chain.last_block.index + 1, previous_hash=chain.last_block.hash, transactions=mineable_txs, state_root=temp_state.state_root(), - receipt_root=_calculate_receipt_root(receipts), + receipt_root=calculate_receipt_root(receipts), receipts=receipts, miner=miner_pk, ) diff --git a/minichain/block.py b/minichain/block.py index e0d6805..24eaadb 100644 --- a/minichain/block.py +++ b/minichain/block.py @@ -27,13 +27,11 @@ def _calculate_merkle_root(transactions: List[Transaction]) -> Optional[str]: return None return _calculate_merkle_tree([tx.tx_id for tx in transactions]) -def _calculate_receipt_root(receipts: List[Receipt]) -> Optional[str]: +def calculate_receipt_root(receipts: List[Receipt]) -> Optional[str]: if not receipts: return None return _calculate_merkle_tree([canonical_json_hash(r.to_dict()) for r in receipts]) - # Logic moved to _calculate_merkle_tree - class Block: def __init__( @@ -72,7 +70,7 @@ def __init__( # If receipt_root is missing but we have receipts, calculate it. if self.receipt_root is None and self.receipts: - self.receipt_root = _calculate_receipt_root(self.receipts) + self.receipt_root = calculate_receipt_root(self.receipts) # ------------------------- # HEADER (used for mining) diff --git a/minichain/chain.py b/minichain/chain.py index 2d11aa5..1ce119f 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -1,4 +1,4 @@ -from .block import Block +from .block import Block, calculate_receipt_root from .state import State from .pow import calculate_hash import logging @@ -47,10 +47,10 @@ def _create_genesis_block(self, genesis_path): with open(genesis_path, "r") as f: config = json.load(f) except Exception as e: - logger.error(f"Failed to load genesis config: {e}") + logger.error("Failed to load genesis config: %s", e) sys.exit(1) else: - logger.error(f"Failed to load genesis config: file {genesis_path} does not exist.") + logger.error("Failed to load genesis config: file %s does not exist.", genesis_path) sys.exit(1) # Apply genesis allocations @@ -58,7 +58,7 @@ def _create_genesis_block(self, genesis_path): for address, data in alloc.items(): balance = data.get("balance", 0) if not isinstance(balance, int) or balance < 0: - logger.error(f"Invalid genesis balance for {address}: {balance}. Must be a non-negative integer.") + logger.error("Invalid genesis balance for %s: %s. Must be a non-negative integer.", address, balance) sys.exit(1) account = self.state.get_account(address) account['balance'] = balance @@ -82,7 +82,7 @@ def _create_genesis_block(self, genesis_path): if config_hash: if config_hash != computed_hash: - logger.error(f"Genesis hash mismatch. Config hash: {config_hash}, Computed hash: {computed_hash}") + logger.error("Genesis hash mismatch. Config hash: %s, Computed hash: %s", config_hash, computed_hash) sys.exit(1) genesis_block.hash = config_hash else: @@ -128,8 +128,7 @@ def add_block(self, block): if block.miner: temp_state.credit_mining_reward(block.miner) - from .block import _calculate_receipt_root - computed_receipt_root = _calculate_receipt_root(receipts) + computed_receipt_root = calculate_receipt_root(receipts) if block.receipt_root != computed_receipt_root: logger.warning("Block %s rejected: Invalid receipt root. Expected %s, got %s", block.index, computed_receipt_root, block.receipt_root) return False diff --git a/minichain/contract.py b/minichain/contract.py index c88a20f..e7c2fd4 100644 --- a/minichain/contract.py +++ b/minichain/contract.py @@ -114,7 +114,7 @@ def execute(self, contract_address, sender_address, payload, amount): logger.error("Contract execution crashed without result") return False if result["status"] != "success": - logger.error(f"Contract Execution Failed: {result.get('error')}") + logger.error("Contract Execution Failed: %s", result.get('error')) return False # Validate storage is JSON serializable @@ -155,7 +155,7 @@ def _validate_code_ast(self, code): logger.warning("Rejected type() call.") return False if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in {"getattr", "setattr", "delattr"}: - logger.warning(f"Rejected direct call to {node.func.id}.") + logger.warning("Rejected direct call to %s.", node.func.id) return False if isinstance(node, ast.Constant) and isinstance(node.value, str): if "__" in node.value: diff --git a/minichain/mpt.py b/minichain/mpt.py index ee30b8d..019ed50 100644 --- a/minichain/mpt.py +++ b/minichain/mpt.py @@ -1,164 +1,24 @@ -import hashlib -import json -from typing import Optional, List +from typing import Optional +from trie import HexaryTrie -def hash_data(data: bytes) -> bytes: - return hashlib.sha256(data).digest() - -def to_nibbles(key_hex: str) -> List[int]: - """Converts a hex string key into a list of integer nibbles (0-15).""" - try: - return [int(c, 16) for c in key_hex] - except ValueError: - raise ValueError(f"Invalid MPT key: '{key_hex}'. Keys must be valid hex strings.") - -class Node: - def hash(self) -> bytes: - raise NotImplementedError - -class LeafNode(Node): - def __init__(self, path: List[int], value: str): - self.path = path - self.value = value - - def hash(self) -> bytes: - data = json.dumps({"type": "leaf", "path": self.path, "value": self.value}, sort_keys=True) - return hash_data(data.encode()) - -class ExtensionNode(Node): - def __init__(self, path: List[int], child: Node): - self.path = path - self.child = child - - def hash(self) -> bytes: - child_hash = self.child.hash().hex() - data = json.dumps({"type": "extension", "path": self.path, "child": child_hash}, sort_keys=True) - return hash_data(data.encode()) - -class BranchNode(Node): - def __init__(self): - self.branches: List[Optional[Node]] = [None] * 16 - self.value: Optional[str] = None - - def hash(self) -> bytes: - b_hashes = [b.hash().hex() if b else None for b in self.branches] - data = json.dumps({"type": "branch", "branches": b_hashes, "value": self.value}, sort_keys=True) - return hash_data(data.encode()) class Trie: """ - A simplified Merkle Patricia Trie (MPT) for MiniChain. + A Merkle Patricia Trie (MPT) for MiniChain backed by the `trie` library. Provides O(log N) state verification via cryptographic state roots. """ def __init__(self): - self.root: Optional[Node] = None + self._trie = HexaryTrie({}) def root_hash(self) -> str: """Returns the 32-byte hex hash of the trie's root.""" - if not self.root: - return "0" * 64 - return self.root.hash().hex() + return self._trie.root_hash.hex() def get(self, key_hex: str) -> Optional[str]: - if not self.root: - return None - return self._get(self.root, to_nibbles(key_hex)) - - def _get(self, node: Optional[Node], path: List[int]) -> Optional[str]: - if not node: - return None - - if isinstance(node, LeafNode): - if node.path == path: - return node.value - return None - - elif isinstance(node, ExtensionNode): - if path[:len(node.path)] == node.path: - return self._get(node.child, path[len(node.path):]) - return None - - elif isinstance(node, BranchNode): - if not path: - return node.value - nibble = path[0] - return self._get(node.branches[nibble], path[1:]) - - return None + key = bytes.fromhex(key_hex) + val = self._trie.get(key) + return val.decode() if val is not None else None def put(self, key_hex: str, value: str): - path = to_nibbles(key_hex) - self.root = self._put(self.root, path, value) - - def _put(self, node: Optional[Node], path: List[int], value: str) -> Node: - if node is None: - return LeafNode(path, value) - - if isinstance(node, LeafNode): - if node.path == path: - node.value = value - return node - - # Paths diverge. Find common prefix. - common = 0 - while common < len(node.path) and common < len(path) and node.path[common] == path[common]: - common += 1 - - branch = BranchNode() - - # Handle the leaf's remaining path - leaf_remaining = node.path[common:] - if not leaf_remaining: - branch.value = node.value - else: - branch.branches[leaf_remaining[0]] = LeafNode(leaf_remaining[1:], node.value) - - # Handle the new value's remaining path - new_remaining = path[common:] - if not new_remaining: - branch.value = value - else: - branch.branches[new_remaining[0]] = LeafNode(new_remaining[1:], value) - - if common > 0: - return ExtensionNode(node.path[:common], branch) - return branch - - elif isinstance(node, ExtensionNode): - common = 0 - while common < len(node.path) and common < len(path) and node.path[common] == path[common]: - common += 1 - - if common == len(node.path): - # Path matches extension exactly, continue to child - node.child = self._put(node.child, path[common:], value) - return node - - # Divergence inside the extension node - branch = BranchNode() - ext_remaining = node.path[common:] - - # The child of the extension becomes a branch's branch - if len(ext_remaining) == 1: - branch.branches[ext_remaining[0]] = node.child - else: - branch.branches[ext_remaining[0]] = ExtensionNode(ext_remaining[1:], node.child) - - # Insert the new value - new_remaining = path[common:] - if not new_remaining: - branch.value = value - else: - branch.branches[new_remaining[0]] = LeafNode(new_remaining[1:], value) - - if common > 0: - return ExtensionNode(node.path[:common], branch) - return branch - - elif isinstance(node, BranchNode): - if not path: - node.value = value - else: - nibble = path[0] - node.branches[nibble] = self._put(node.branches[nibble], path[1:], value) - return node + key = bytes.fromhex(key_hex) + self._trie.set(key, value.encode()) diff --git a/minichain/p2p.py b/minichain/p2p.py index 2374e92..0fe1e79 100644 --- a/minichain/p2p.py +++ b/minichain/p2p.py @@ -141,7 +141,7 @@ def _validate_transaction_payload(self, payload): if not isinstance(payload.get(field), expected_type): return False - if payload["amount"] <= 0: + if payload["amount"] < 0: return False receiver = payload.get("receiver") diff --git a/minichain/persistence.py b/minichain/persistence.py index 6fa1fd6..d142879 100644 --- a/minichain/persistence.py +++ b/minichain/persistence.py @@ -93,7 +93,7 @@ def load(path: str = ".") -> Blockchain: if not isinstance(raw_block, dict): raise ValueError(f"Invalid chain data in '{path}'") try: - blocks.append(_deserialize_block(raw_block)) + blocks.append(Block.from_dict(raw_block)) except (KeyError, TypeError, ValueError) as exc: raise ValueError(f"Invalid chain data in '{path}'") from exc @@ -267,10 +267,4 @@ def _read_legacy_json(filepath: str) -> dict[str, Any]: return json.load(f) -# --------------------------------------------------------------------------- -# Block deserialisation -# --------------------------------------------------------------------------- - -def _deserialize_block(data: dict[str, Any]) -> Block: - return Block.from_dict(data) diff --git a/minichain/state.py b/minichain/state.py index 0d5c088..5c62f76 100644 --- a/minichain/state.py +++ b/minichain/state.py @@ -1,8 +1,11 @@ +import copy +import json +import logging from nacl.hash import sha256 from nacl.encoding import HexEncoder from .contract import ContractMachine -import copy -import logging +from .mpt import Trie +from .receipt import Receipt logger = logging.getLogger(__name__) @@ -18,8 +21,6 @@ def state_root(self) -> str: Dynamically builds the Merkle Patricia Trie from the current state dictionary and returns the cryptographic state root hash. """ - import json - from .mpt import Trie trie = Trie() # Sort items to ensure deterministic insertion order if necessary (though MPT is order-independent) for addr, acc in sorted(self.accounts.items()): @@ -42,17 +43,17 @@ def get_account(self, address): def verify_transaction_logic(self, tx): if not tx.verify(): - logger.error(f"Error: Invalid signature for tx from {tx.sender[:8]}...") + logger.error("Error: Invalid signature for tx from %s...", tx.sender[:8]) return False sender_acc = self.get_account(tx.sender) if sender_acc['balance'] < tx.amount: - logger.error(f"Error: Insufficient balance for {tx.sender[:8]}...") + logger.error("Error: Insufficient balance for %s...", tx.sender[:8]) return False if sender_acc['nonce'] != tx.nonce: - logger.error(f"Error: Invalid nonce. Expected {sender_acc['nonce']}, got {tx.nonce}") + logger.error("Error: Invalid nonce. Expected %s, got %s", sender_acc['nonce'], tx.nonce) return False return True @@ -79,8 +80,6 @@ def validate_and_apply(self, tx): return self.apply_transaction(tx) def apply_transaction(self, tx): - from .receipt import Receipt - """ Applies transaction and mutates state. Returns: Receipt object if mathematically valid, None if invalid. diff --git a/requirements.txt b/requirements.txt index 819e170..99cd065 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ pynacl==1.6.2 -libp2p==0.5.0 +trie>=3.1.0 diff --git a/setup.py b/setup.py index 1edff7b..c7c452a 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ py_modules=["main"], install_requires=[ "PyNaCl>=1.5.0", - "libp2p>=0.5.0", # Correct PyPI package name + "trie>=3.1.0", ], entry_points={ "console_scripts": [ diff --git a/tests/test_persistence.py b/tests/test_persistence.py index a615892..ac683c6 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -44,14 +44,14 @@ def _chain_with_tx(self): temp_state = bc.state.copy() receipt = temp_state.validate_and_apply(tx) - from minichain.block import _calculate_receipt_root + from minichain.block import calculate_receipt_root block = Block( index=1, previous_hash=bc.last_block.hash, transactions=[tx], difficulty=1, state_root=temp_state.state_root(), - receipt_root=_calculate_receipt_root([receipt]), + receipt_root=calculate_receipt_root([receipt]), receipts=[receipt], ) mine_block(block, difficulty=1) @@ -226,14 +226,14 @@ def test_loaded_chain_can_add_new_block(self): temp_state = restored.state.copy() receipt2 = temp_state.validate_and_apply(tx2) - from minichain.block import _calculate_receipt_root + from minichain.block import calculate_receipt_root block2 = Block( index=len(restored.chain), previous_hash=restored.last_block.hash, transactions=[tx2], difficulty=1, state_root=temp_state.state_root(), - receipt_root=_calculate_receipt_root([receipt2]), + receipt_root=calculate_receipt_root([receipt2]), receipts=[receipt2], ) mine_block(block2, difficulty=1) diff --git a/tests/test_persistence_runtime.py b/tests/test_persistence_runtime.py index 15e9ef7..894ccca 100644 --- a/tests/test_persistence_runtime.py +++ b/tests/test_persistence_runtime.py @@ -66,14 +66,14 @@ def _chain_with_tx(self): temp_state = bc.state.copy() receipt = temp_state.validate_and_apply(tx) - from minichain.block import _calculate_receipt_root + from minichain.block import calculate_receipt_root block = Block( index=1, previous_hash=bc.last_block.hash, transactions=[tx], difficulty=1, state_root=temp_state.state_root(), - receipt_root=_calculate_receipt_root([receipt]), + receipt_root=calculate_receipt_root([receipt]), receipts=[receipt], ) mine_block(block, difficulty=1) diff --git a/tests/test_protocol_hardening.py b/tests/test_protocol_hardening.py index 7644f38..538c8cc 100644 --- a/tests/test_protocol_hardening.py +++ b/tests/test_protocol_hardening.py @@ -109,7 +109,7 @@ async def test_block_schema_accepts_current_block_wire_format(self): tx.sign(sender_sk) from minichain.receipt import Receipt - from minichain.block import _calculate_receipt_root + from minichain.block import calculate_receipt_root receipt = Receipt(tx_hash=tx.tx_id, status=1) block = Block( @@ -120,7 +120,7 @@ async def test_block_schema_accepts_current_block_wire_format(self): difficulty=2, state_root="0"*64, receipts=[receipt], - receipt_root=_calculate_receipt_root([receipt]) + receipt_root=calculate_receipt_root([receipt]) ) block.nonce = 9 block.hash = block.compute_hash() From 1998d91ea90ba9b4abf51ab81cfcddd6b5df7aa9 Mon Sep 17 00:00:00 2001 From: siddhant Date: Thu, 4 Jun 2026 04:27:56 +0530 Subject: [PATCH 3/4] remove setup.py anf fixes --- minichain/chain.py | 4 ++++ minichain/p2p.py | 16 ++++++++++++++++ setup.py | 18 ------------------ tests/test_persistence.py | 12 ++++++++++++ 4 files changed, 32 insertions(+), 18 deletions(-) delete mode 100644 setup.py diff --git a/minichain/chain.py b/minichain/chain.py index 1ce119f..0b7f41c 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -133,6 +133,10 @@ def add_block(self, block): logger.warning("Block %s rejected: Invalid receipt root. 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("Block %s rejected: Receipts payload mismatch", block.index) + return False + # Verify state root if block.state_root != temp_state.state_root(): logger.warning("Block %s rejected: Invalid state root. Expected %s, got %s", block.index, temp_state.state_root(), block.state_root) diff --git a/minichain/p2p.py b/minichain/p2p.py index 0fe1e79..0efab33 100644 --- a/minichain/p2p.py +++ b/minichain/p2p.py @@ -208,6 +208,22 @@ def _validate_block_payload(self, payload): if "miner" in payload and not isinstance(payload["miner"], (str, type(None))): return False + for r_payload in payload.get("receipts", []): + if not isinstance(r_payload, dict): + return False + if "tx_hash" not in r_payload or not isinstance(r_payload["tx_hash"], str): + return False + if "status" not in r_payload or not isinstance(r_payload["status"], int): + return False + if "gas_used" in r_payload and not isinstance(r_payload["gas_used"], int): + return False + if "error_message" in r_payload and not isinstance(r_payload["error_message"], (str, type(None))): + return False + if "logs" in r_payload and not isinstance(r_payload["logs"], list): + return False + if "contract_address" in r_payload and not isinstance(r_payload["contract_address"], (str, type(None))): + return False + return all( self._validate_transaction_payload(tx_payload) for tx_payload in payload["transactions"] diff --git a/setup.py b/setup.py deleted file mode 100644 index c7c452a..0000000 --- a/setup.py +++ /dev/null @@ -1,18 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name="minichain", - version="0.1.0", - packages=find_packages(), - py_modules=["main"], - install_requires=[ - "PyNaCl>=1.5.0", - "trie>=3.1.0", - ], - entry_points={ - "console_scripts": [ - "minichain=main:main", - ], - }, - python_requires=">=3.9", -) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index ac683c6..c215712 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -91,6 +91,18 @@ def test_transaction_data_preserved(self): self.assertEqual(original_tx.nonce, loaded_tx.nonce) self.assertEqual(original_tx.signature, loaded_tx.signature) + def test_receipt_data_preserved(self): + bc, _, _ = self._chain_with_tx() + save(bc, path=self.tmpdir) + restored = load(path=self.tmpdir) + original_receipt = bc.chain[1].receipts[0] + loaded_receipt = restored.chain[1].receipts[0] + self.assertEqual(original_receipt.tx_hash, loaded_receipt.tx_hash) + self.assertEqual(original_receipt.status, loaded_receipt.status) + self.assertEqual(original_receipt.gas_used, loaded_receipt.gas_used) + self.assertEqual(original_receipt.error_message, loaded_receipt.error_message) + self.assertEqual(original_receipt.contract_address, loaded_receipt.contract_address) + def test_genesis_only_chain(self): bc = Blockchain() save(bc, path=self.tmpdir) From 0812e468edf592ead43158320d1d7f4141676004 Mon Sep 17 00:00:00 2001 From: siddhant Date: Thu, 4 Jun 2026 16:28:55 +0530 Subject: [PATCH 4/4] address coderabbit --- minichain/block.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/minichain/block.py b/minichain/block.py index 8ed2936..12bc141 100644 --- a/minichain/block.py +++ b/minichain/block.py @@ -87,7 +87,6 @@ def to_header_dict(self): "timestamp": self.timestamp, "difficulty": self.difficulty, "nonce": self.nonce, - "miner": self.miner, } # Include miner in header only when present (optional field) <-- Reworded comment if self.miner is not None: @@ -162,6 +161,12 @@ def from_dict(cls, payload: dict): # Recalculate and verify the Merkle root! if "merkle_root" in payload and payload["merkle_root"] != block.merkle_root: raise ValueError("merkle_root does not match transactions") + + if "receipt_root" in payload: + expected_receipt_root = calculate_receipt_root(block.receipts) + if payload["receipt_root"] != expected_receipt_root: + raise ValueError("receipt_root does not match receipts") + return block @property