From 99da332abb1349c5d4b1b3d7cc9a7623313299f3 Mon Sep 17 00:00:00 2001 From: siddhant Date: Wed, 29 Jul 2026 09:13:46 +0530 Subject: [PATCH 1/2] refactor: change PoW difficulty to numeric target hash threshold --- genesis.json | 2 +- main.py | 2 +- minichain/block.py | 20 +++--- minichain/chain.py | 66 +++++++++++--------- minichain/network_config.py | 3 + minichain/pow.py | 17 +++-- tests/test_core.py | 2 +- tests/test_persistence.py | 4 +- tests/test_persistence_runtime.py | 4 +- tests/test_protocol_hardening.py | 6 +- tests/test_reorg.py | 2 +- tests/test_serialization.py | 6 +- tests/{test_difficulty.py => test_target.py} | 45 ++++++------- 13 files changed, 92 insertions(+), 87 deletions(-) rename tests/{test_difficulty.py => test_target.py} (55%) diff --git a/genesis.json b/genesis.json index ed1f2b5..e4d8e5d 100644 --- a/genesis.json +++ b/genesis.json @@ -1,7 +1,7 @@ { "chain_id": "minichain-default", "timestamp": 1716880000000, - "difficulty": 4, + "target": "0x0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", "target_block_time": 10000, "alpha": 0.1, "initial_supply": 1500000000, diff --git a/main.py b/main.py index 079f6cd..b37fce4 100644 --- a/main.py +++ b/main.py @@ -162,7 +162,7 @@ def mine_and_process_block(chain, mempool, miner_pk): receipt_root=calculate_receipt_root(receipts), receipts=receipts, miner=miner_pk, - difficulty=chain.current_difficulty, + target=chain.current_target, ) mined_block = mine_block(block) diff --git a/minichain/block.py b/minichain/block.py index c81dbe3..7b38117 100644 --- a/minichain/block.py +++ b/minichain/block.py @@ -41,7 +41,7 @@ def __init__( previous_hash: str, transactions: Optional[Sequence[Transaction]] = None, timestamp: Optional[float] = None, - difficulty: Optional[int] = None, + target: Optional[int] = None, state_root: Optional[str] = None, receipt_root: Optional[str] = None, receipts: Optional[Sequence[Receipt]] = None, @@ -59,7 +59,7 @@ def __init__( if timestamp is None else int(timestamp) ) - self.difficulty: Optional[int] = difficulty + self.target: Optional[int] = target self.nonce: int = 0 self.hash: Optional[str] = None self.state_root: Optional[str] = state_root @@ -83,7 +83,7 @@ def to_header_dict(self): "state_root": self.state_root, "receipt_root": self.receipt_root, "timestamp": self.timestamp, - "difficulty": self.difficulty, + "target": hex(self.target) if self.target is not None else None, "nonce": self.nonce, } # Include miner in header only when present (optional field) @@ -130,14 +130,12 @@ def from_dict(cls, payload: dict): for r_payload in payload.get("receipts", []) ] - # Safely extract and cast difficulty and timestamp if they exist - raw_diff = payload.get("difficulty") - if raw_diff is not None: - parsed_diff = int(raw_diff) - if parsed_diff > 256: - raise ValueError(f"Difficulty too large: {parsed_diff}") + # Safely extract and cast target and timestamp if they exist + raw_target = payload.get("target") + if raw_target is not None: + parsed_target = int(raw_target, 16) if isinstance(raw_target, str) else int(raw_target) else: - parsed_diff = None + parsed_target = None raw_ts = payload.get("timestamp") parsed_ts = int(raw_ts) if raw_ts is not None else None @@ -146,7 +144,7 @@ def from_dict(cls, payload: dict): previous_hash=payload["previous_hash"], transactions=transactions, timestamp=parsed_ts, - difficulty=parsed_diff, + target=parsed_target, state_root=payload.get("state_root"), receipt_root=payload.get("receipt_root"), receipts=receipts, diff --git a/minichain/chain.py b/minichain/chain.py index cf46ce2..d7ad6ab 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -27,9 +27,8 @@ def validate_block_link_and_hash(previous_block, block): if block.hash != expected_hash: raise ValueError(f"invalid hash {block.hash}") - target = "0" * (block.difficulty or 1) - if not block.hash.startswith(target): - raise ValueError(f"invalid Proof of Work: hash {block.hash} does not satisfy difficulty {block.difficulty}") + if block.target is None or int(block.hash, 16) >= block.target: + raise ValueError(f"invalid Proof of Work: hash {block.hash} does not satisfy target {block.target}") if block.timestamp <= previous_block.timestamp: raise ValueError(f"invalid timestamp: {block.timestamp} is not strictly greater than previous block timestamp {previous_block.timestamp}") @@ -88,11 +87,15 @@ def _create_genesis_block(self, genesis_path): self.state.chain_id = self.chain_id timestamp = config.get("timestamp") - difficulty = config.get("difficulty") + raw_target = config.get("target") + if raw_target is not None: + self.current_target = int(raw_target, 16) if isinstance(raw_target, str) else int(raw_target) + else: + from .network_config import MAX_TARGET + self.current_target = MAX_TARGET self.target_block_time = config.get("target_block_time", 10000) self.alpha = config.get("alpha", 0.1) - self.current_difficulty = difficulty self.avg_block_time = self.target_block_time genesis_block = Block( @@ -100,7 +103,7 @@ def _create_genesis_block(self, genesis_path): previous_hash="0", transactions=[], timestamp=timestamp, - difficulty=difficulty, + target=self.current_target, state_root=self.state.state_root(), receipt_root=None, receipts=[] @@ -133,27 +136,28 @@ def last_block(self): def get_total_work(self, chain_list=None): """ Calculates the cumulative PoW of a chain. - Work is proportional to 2^difficulty. + Work is inversely proportional to target. """ if chain_list is None: with self._lock: chain_list = self.chain - return sum(2 ** (block.difficulty or 1) for block in chain_list) + return sum((1 << 256) // (block.target or 1) for block in chain_list) - def _next_difficulty(self, difficulty, avg_block_time): - """Advance the EMA difficulty control after a block, returning the new value.""" + def _next_target(self, target, avg_block_time): + """Advance the EMA target control after a block, returning the new value.""" + from .network_config import MAX_TARGET, MIN_TARGET if avg_block_time > self.target_block_time: - return max(1, difficulty - 1) + return min(MAX_TARGET, target + 1) if avg_block_time < self.target_block_time: - return difficulty + 1 - return difficulty + return max(MIN_TARGET, target - 1) + return target - def _apply_block(self, prev_block, block, state, difficulty, avg_block_time): + def _apply_block(self, prev_block, block, state, target, avg_block_time): """ Canonical block-application pipeline shared by add_block and resolve_conflicts. Validates `block` against `prev_block` and applies its transactions to `state` (mutated in place). On any non-VALID status the caller must discard `state`. - Returns: (ValidationStatus, new_difficulty, new_avg_block_time) + Returns: (ValidationStatus, new_target, new_avg_block_time) """ from .validators import ValidationStatus @@ -162,18 +166,18 @@ def _apply_block(self, prev_block, block, state, difficulty, avg_block_time): except ValueError as exc: logger.warning("Block %s rejected: %s", block.index, exc) status = ValidationStatus.INVALID if "hash" in str(exc) else ValidationStatus.FAILED - return status, difficulty, avg_block_time + return status, target, avg_block_time - if block.difficulty != difficulty: - logger.warning("Block %s rejected: Invalid difficulty. Expected %s, got %s", block.index, difficulty, block.difficulty) - return ValidationStatus.INVALID, difficulty, avg_block_time + if block.target != target: + logger.warning("Block %s rejected: Invalid target. Expected %s, got %s", block.index, target, block.target) + return ValidationStatus.INVALID, target, avg_block_time receipts = [] for tx in block.transactions: status, receipt = state.validate_and_apply_with_status(tx) if status != ValidationStatus.VALID: logger.warning("Block %s rejected: Transaction failed validation", block.index) - return status, difficulty, avg_block_time + return status, target, avg_block_time receipts.append(receipt) total_fees = sum(getattr(r, 'gas_used', 0) * getattr(tx, 'fee_per_gas', 0) for r, tx in zip(receipts, block.transactions)) @@ -183,19 +187,19 @@ def _apply_block(self, prev_block, block, state, difficulty, avg_block_time): 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 ValidationStatus.INVALID, difficulty, avg_block_time + return ValidationStatus.INVALID, target, avg_block_time 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 ValidationStatus.INVALID, difficulty, avg_block_time + return ValidationStatus.INVALID, target, avg_block_time computed_state_root = state.state_root() if block.state_root != computed_state_root: logger.warning("Block %s rejected: Invalid state root. Expected %s, got %s", block.index, computed_state_root, block.state_root) - return ValidationStatus.INVALID, difficulty, avg_block_time + return ValidationStatus.INVALID, target, avg_block_time new_avg = self.alpha * (block.timestamp - prev_block.timestamp) + (1 - self.alpha) * avg_block_time - return ValidationStatus.VALID, self._next_difficulty(difficulty, new_avg), new_avg + return ValidationStatus.VALID, self._next_target(target, new_avg), new_avg def add_block(self, block): """ @@ -207,15 +211,15 @@ def add_block(self, block): with self._lock: temp_state = self.state.copy() temp_state.chain_id = self.chain_id - status, new_difficulty, new_avg = self._apply_block( - self.last_block, block, temp_state, self.current_difficulty, self.avg_block_time + status, new_target, new_avg = self._apply_block( + self.last_block, block, temp_state, self.current_target, self.avg_block_time ) if status != ValidationStatus.VALID: return status # All transactions valid → commit state and append block self.state = temp_state - self.current_difficulty = new_difficulty + self.current_target = new_target self.avg_block_time = new_avg self.chain.append(block) return ValidationStatus.VALID @@ -264,12 +268,12 @@ def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]: temp_state.chain_id = self.chain_id temp_state.restore(self._genesis_state_snapshot) - temp_difficulty = proposed_chain[0].difficulty + temp_target = proposed_chain[0].target temp_avg_block_time = self.target_block_time for i in range(1, len(proposed_chain)): - status, temp_difficulty, temp_avg_block_time = self._apply_block( - proposed_chain[i - 1], proposed_chain[i], temp_state, temp_difficulty, temp_avg_block_time + status, temp_target, temp_avg_block_time = self._apply_block( + proposed_chain[i - 1], proposed_chain[i], temp_state, temp_target, temp_avg_block_time ) if status != ValidationStatus.VALID: logger.warning("Reorg failed at block %s", proposed_chain[i].index) @@ -281,7 +285,7 @@ def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]: self.chain = proposed_chain self.state = temp_state - self.current_difficulty = temp_difficulty + self.current_target = temp_target self.avg_block_time = temp_avg_block_time logger.info("Reorg successful! Switched to new chain tip: Block %s", self.last_block.index) return True, orphans diff --git a/minichain/network_config.py b/minichain/network_config.py index 3786459..5fc5371 100644 --- a/minichain/network_config.py +++ b/minichain/network_config.py @@ -14,3 +14,6 @@ MAX_FUTURE_BLOCK_TIME_MS = 15000 # Max allowed ms in the future for a block timestamp GAS_PER_BYTE = 10 # Cost per byte of state storage written MAX_CALL_DEPTH = 10 # Maximum depth for cross-contract calls +MAX_TARGET = int("F" * 64, 16) +MIN_TARGET = 1 + diff --git a/minichain/pow.py b/minichain/pow.py index 6b3846b..18a481a 100644 --- a/minichain/pow.py +++ b/minichain/pow.py @@ -14,7 +14,7 @@ def calculate_hash(block_dict): def mine_block( block, - difficulty=None, + target=None, max_nonce=None, timeout_seconds=None, logger=None, @@ -23,20 +23,19 @@ def mine_block( """Mines a block using Proof-of-Work without mutating input block until success.""" max_nonce = max_nonce if max_nonce is not None else MINING_MAX_NONCE - difficulty = difficulty if difficulty is not None else block.difficulty - if not isinstance(difficulty, int) or difficulty <= 0: - raise ValueError("Difficulty must be a positive integer.") + target = target if target is not None else block.target + if not isinstance(target, int) or target <= 0: + raise ValueError("Target must be a positive integer.") - target = "0" * difficulty local_nonce = 0 header_dict = block.to_header_dict() # Construct header dict once outside loop start_time = time.monotonic() if logger: logger.info( - "Mining block %s (Difficulty: %s)", + "Mining block %s (Target: %s)", block.index, - difficulty, + target, ) while True: @@ -56,8 +55,8 @@ def mine_block( header_dict["nonce"] = local_nonce block_hash = calculate_hash(header_dict) - # Check difficulty target - if block_hash.startswith(target): + # Check target + if int(block_hash, 16) < target: block.nonce = local_nonce # Assign only on success block.hash = block_hash if logger: diff --git a/tests/test_core.py b/tests/test_core.py index bbf079f..d3fc357 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -86,7 +86,7 @@ def test_transaction_fee(self): index=1, previous_hash="0", transactions=[tx], - difficulty=1, + target=int("F"*64, 16), state_root=self.state.state_root(), receipt_root=calculate_receipt_root([receipt]), receipts=[receipt], diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 989af77..47862dc 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -50,7 +50,7 @@ def _chain_with_tx(self): index=1, previous_hash=bc.last_block.hash, transactions=[tx], - difficulty=bc.current_difficulty, + target=bc.current_target, state_root=temp_state.state_root(), receipt_root=calculate_receipt_root([receipt]), receipts=[receipt], @@ -245,7 +245,7 @@ def test_loaded_chain_can_add_new_block(self): index=len(restored.chain), previous_hash=restored.last_block.hash, transactions=[tx2], - difficulty=restored.current_difficulty, + target=restored.current_target, state_root=temp_state.state_root(), receipt_root=calculate_receipt_root([receipt2]), receipts=[receipt2], diff --git a/tests/test_persistence_runtime.py b/tests/test_persistence_runtime.py index 73265e5..ba172ee 100644 --- a/tests/test_persistence_runtime.py +++ b/tests/test_persistence_runtime.py @@ -71,12 +71,12 @@ def _chain_with_tx(self): index=1, previous_hash=bc.last_block.hash, transactions=[tx], - difficulty=1, + target=int("F"*64, 16), state_root=temp_state.state_root(), receipt_root=calculate_receipt_root([receipt]), receipts=[receipt], ) - mine_block(block, difficulty=1) + mine_block(block, target=int("F"*64, 16)) bc.add_block(block) return bc diff --git a/tests/test_protocol_hardening.py b/tests/test_protocol_hardening.py index 5ab706c..b5f3a04 100644 --- a/tests/test_protocol_hardening.py +++ b/tests/test_protocol_hardening.py @@ -17,7 +17,7 @@ def test_canonical_json_is_order_independent(self): def test_block_hash_matches_compute_hash(self): block = Block(index=1, previous_hash="abc", transactions=[], timestamp=1234567890) - block.difficulty = 2 + block.target = 2 block.nonce = 7 self.assertEqual(block.compute_hash(), calculate_hash(block.to_header_dict())) @@ -117,7 +117,7 @@ async def test_block_schema_accepts_current_block_wire_format(self): previous_hash="0" * 64, transactions=[tx], timestamp=1600000000000, - difficulty=2, + target=int("F"*64, 16), state_root="0"*64, receipts=[receipt], receipt_root=calculate_receipt_root([receipt]) @@ -150,7 +150,7 @@ async def test_duplicate_tx_and_block_detection(self): "previous_hash": "0" * 64, "transactions": [tx_message["data"]], "timestamp": 123, - "difficulty": 2, + "target": int("F"*64, 16), "nonce": 1, "hash": "f" * 64, }, diff --git a/tests/test_reorg.py b/tests/test_reorg.py index e5637fc..12d558c 100644 --- a/tests/test_reorg.py +++ b/tests/test_reorg.py @@ -21,7 +21,7 @@ def genesis_file(tmp_path): pk = sk.verify_key.encode(encoder=HexEncoder).decode() data = { "timestamp": int(time.time()), - "difficulty": 1, + "target": int("F"*64, 16), "alloc": { pk: {"balance": 1000} } diff --git a/tests/test_serialization.py b/tests/test_serialization.py index aa5f2b7..2172c0c 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -38,8 +38,8 @@ def test_block_serialization_determinism(): tx2 = Transaction(**tx_params) # Add the miner field - block1 = Block(index=1, previous_hash="0"*64, transactions=[tx1], difficulty=2, timestamp=999999, miner="a" * 40) - block2 = Block(index=1, previous_hash="0"*64, transactions=[tx2], difficulty=2, timestamp=999999, miner="a" * 40) + block1 = Block(index=1, previous_hash="0"*64, transactions=[tx1], target=int("F"*64, 16), timestamp=999999, miner="a" * 40) + block2 = Block(index=1, previous_hash="0"*64, transactions=[tx2], target=int("F"*64, 16), timestamp=999999, miner="a" * 40) # Pre-compute the hashes before asserting block1.hash = block1.compute_hash() @@ -55,7 +55,7 @@ def test_block_from_dict_rejects_tampered_payload(): tx = Transaction(sender="A", receiver="B", amount=10, nonce=5, timestamp=1000) block = Block( index=1, previous_hash="0"*64, transactions=[tx], - difficulty=2, timestamp=999999, miner="a"*40 + target=int("F"*64, 16), timestamp=999999, miner="a"*40 ) block.hash = block.compute_hash() diff --git a/tests/test_difficulty.py b/tests/test_target.py similarity index 55% rename from tests/test_difficulty.py rename to tests/test_target.py index d15c853..57966d8 100644 --- a/tests/test_difficulty.py +++ b/tests/test_target.py @@ -2,63 +2,64 @@ from minichain import Blockchain, Block from minichain.pow import mine_block from minichain.validators import ValidationStatus +from minichain.network_config import MAX_TARGET -class TestEMADifficulty(unittest.TestCase): - def test_difficulty_adjustment(self): +class TestEMATarget(unittest.TestCase): + def test_target_adjustment(self): chain = Blockchain() chain.target_block_time = 1000 chain.alpha = 0.5 chain.avg_block_time = 1000 - chain.current_difficulty = 3 - chain.chain[0].difficulty = 3 + chain.current_target = MAX_TARGET - 10 + chain.chain[0].target = MAX_TARGET - 10 # Fast mining: timestamps only 1ms apart - # avg = 0.5 * 1 + 0.5 * 1000 = 500.5 (which is < 1000) => difficulty increments to 4 + # avg = 0.5 * 1 + 0.5 * 1000 = 500.5 (which is < 1000) => target decrements by 1 ts = chain.last_block.timestamp + 1 - block1 = Block(index=1, previous_hash=chain.last_block.hash, transactions=[], timestamp=ts, difficulty=chain.current_difficulty, state_root=chain.state.state_root()) + block1 = Block(index=1, previous_hash=chain.last_block.hash, transactions=[], timestamp=ts, target=chain.current_target, state_root=chain.state.state_root()) mined_block1 = mine_block(block1) self.assertEqual(chain.add_block(mined_block1), ValidationStatus.VALID) - self.assertEqual(chain.current_difficulty, 4) + self.assertEqual(chain.current_target, MAX_TARGET - 11) # Slow mining: timestamp 5000ms apart - # avg = 0.5 * 5000 + 0.5 * 500.5 = 2750.25 (which is > 1000) => difficulty decrements to 3 + # avg = 0.5 * 5000 + 0.5 * 500.5 = 2750.25 (which is > 1000) => target increments by 1 ts = chain.last_block.timestamp + 5000 - block2 = Block(index=2, previous_hash=chain.last_block.hash, transactions=[], timestamp=ts, difficulty=chain.current_difficulty, state_root=chain.state.state_root()) + block2 = Block(index=2, previous_hash=chain.last_block.hash, transactions=[], timestamp=ts, target=chain.current_target, state_root=chain.state.state_root()) mined_block2 = mine_block(block2) self.assertEqual(chain.add_block(mined_block2), ValidationStatus.VALID) - self.assertEqual(chain.current_difficulty, 3) + self.assertEqual(chain.current_target, MAX_TARGET - 10) - def test_reorg_difficulty_validation(self): + def test_reorg_target_validation(self): chain1 = Blockchain() chain1.target_block_time = 1000 chain1.alpha = 0.5 chain1.avg_block_time = 1000 - chain1.current_difficulty = 1 - chain1.chain[0].difficulty = 1 + chain1.current_target = MAX_TARGET - 10 + chain1.chain[0].target = MAX_TARGET - 10 chain2 = Blockchain() chain2.target_block_time = 1000 chain2.alpha = 0.5 chain2.avg_block_time = 1000 - chain2.current_difficulty = 1 - chain2.chain[0].difficulty = 1 + chain2.current_target = MAX_TARGET - 10 + chain2.chain[0].target = MAX_TARGET - 10 - # Chain 2 mines a fast block, difficulty goes to 2 - block1 = Block(1, chain2.last_block.hash, [], timestamp=chain2.last_block.timestamp + 1, difficulty=chain2.current_difficulty, state_root=chain2.state.state_root()) + # Chain 2 mines a fast block, target goes to MAX_TARGET - 11 + block1 = Block(1, chain2.last_block.hash, [], timestamp=chain2.last_block.timestamp + 1, target=chain2.current_target, state_root=chain2.state.state_root()) mine_block(block1) chain2.add_block(block1) - self.assertEqual(chain2.current_difficulty, 2) + self.assertEqual(chain2.current_target, MAX_TARGET - 11) # Reorg chain1 to chain2 success, orphans = chain1.resolve_conflicts(chain2.chain) self.assertTrue(success) - self.assertEqual(chain1.current_difficulty, 2) + self.assertEqual(chain1.current_target, MAX_TARGET - 11) - # Forging a chain with wrong difficulty should be rejected + # Forging a chain with wrong target should be rejected forged_chain = list(chain2.chain) - forged_block = Block(2, chain2.last_block.hash, [], timestamp=chain2.last_block.timestamp + 1000, difficulty=1, state_root=chain2.state.state_root()) + forged_block = Block(2, chain2.last_block.hash, [], timestamp=chain2.last_block.timestamp + 1000, target=MAX_TARGET - 10, state_root=chain2.state.state_root()) mine_block(forged_block) forged_chain.append(forged_block) success, _ = chain1.resolve_conflicts(forged_chain) - self.assertFalse(success) # Rejected because difficulty should have been 2! + self.assertFalse(success) # Rejected because target should have been MAX_TARGET - 11! From eb58853b647812a9dc0eb015df5f2a64dd03a22b Mon Sep 17 00:00:00 2001 From: siddhant Date: Thu, 30 Jul 2026 01:13:47 +0530 Subject: [PATCH 2/2] address copderabit copmments --- minichain/chain.py | 52 ++++++++++++++++-- minichain/pow.py | 1 + minichain/state.py | 90 +++++++++++++++++++++++++++---- tests/test_persistence_runtime.py | 4 +- tests/test_target.py | 5 +- 5 files changed, 135 insertions(+), 17 deletions(-) diff --git a/minichain/chain.py b/minichain/chain.py index d7ad6ab..99437de 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -27,7 +27,10 @@ def validate_block_link_and_hash(previous_block, block): if block.hash != expected_hash: raise ValueError(f"invalid hash {block.hash}") - if block.target is None or int(block.hash, 16) >= block.target: + from .network_config import MAX_TARGET + if not isinstance(block.target, int) or block.target <= 0 or block.target > MAX_TARGET: + raise ValueError(f"invalid target: {block.target}") + if int(block.hash, 16) >= block.target: raise ValueError(f"invalid Proof of Work: hash {block.hash} does not satisfy target {block.target}") if block.timestamp <= previous_block.timestamp: @@ -48,6 +51,9 @@ def __init__(self, genesis_path="genesis.json"): self.state = State() self.chain_id = "minichain-default" self._lock = threading.RLock() + import collections + self._state_snapshots = collections.OrderedDict() + self._max_snapshots = 10 self._create_genesis_block(genesis_path) def _create_genesis_block(self, genesis_path): @@ -90,6 +96,10 @@ def _create_genesis_block(self, genesis_path): raw_target = config.get("target") if raw_target is not None: self.current_target = int(raw_target, 16) if isinstance(raw_target, str) else int(raw_target) + from .network_config import MAX_TARGET + if not isinstance(self.current_target, int) or self.current_target <= 0 or self.current_target > MAX_TARGET: + logger.error("Genesis target out of bounds: %s", self.current_target) + sys.exit(1) else: from .network_config import MAX_TARGET self.current_target = MAX_TARGET @@ -124,6 +134,7 @@ def _create_genesis_block(self, genesis_path): # Snapshot the state exactly after genesis allocation for clean reorg rebuilds self._genesis_state_snapshot = self.state.snapshot() + self._state_snapshots[genesis_block.hash] = self.state.snapshot() @property def last_block(self): @@ -218,10 +229,18 @@ def add_block(self, block): return status # All transactions valid → commit state and append block + if hasattr(temp_state.accounts, 'commit'): + temp_state.accounts.commit() + temp_state.accounts = temp_state.accounts.backing self.state = temp_state self.current_target = new_target self.avg_block_time = new_avg self.chain.append(block) + + self._state_snapshots[block.hash] = self.state.snapshot() + while len(self._state_snapshots) > self._max_snapshots: + self._state_snapshots.popitem(last=False) + return ValidationStatus.VALID def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]: @@ -266,15 +285,34 @@ def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]: temp_state = State() temp_state.chain_id = self.chain_id - temp_state.restore(self._genesis_state_snapshot) - + + fork_base_hash = self.chain[fork_idx - 1].hash if fork_idx > 0 else None + temp_target = proposed_chain[0].target temp_avg_block_time = self.target_block_time + + if fork_base_hash and fork_base_hash in self._state_snapshots: + logger.info("Reorg optimization: Restoring state from in-memory snapshot at block %s", fork_idx - 1) + temp_state.restore(self._state_snapshots[fork_base_hash]) + + # Fast forward target and avg_block_time without executing state + for i in range(1, fork_idx): + block_time = proposed_chain[i].timestamp - proposed_chain[i-1].timestamp + temp_avg_block_time = self.alpha * block_time + (1 - self.alpha) * temp_avg_block_time + temp_target = self._next_target(temp_target, temp_avg_block_time) + + start_idx = fork_idx + else: + temp_state.restore(self._genesis_state_snapshot) + start_idx = 1 - for i in range(1, len(proposed_chain)): + for i in range(start_idx, len(proposed_chain)): status, temp_target, temp_avg_block_time = self._apply_block( proposed_chain[i - 1], proposed_chain[i], temp_state, temp_target, temp_avg_block_time ) + if hasattr(temp_state.accounts, 'commit'): + temp_state.accounts.commit() + temp_state.accounts = temp_state.accounts.backing if status != ValidationStatus.VALID: logger.warning("Reorg failed at block %s", proposed_chain[i].index) return False, [] @@ -287,5 +325,11 @@ def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]: self.state = temp_state self.current_target = temp_target self.avg_block_time = temp_avg_block_time + + # Repopulate snapshots for the new chain tip + self._state_snapshots[self.last_block.hash] = self.state.snapshot() + while len(self._state_snapshots) > self._max_snapshots: + self._state_snapshots.popitem(last=False) + logger.info("Reorg successful! Switched to new chain tip: Block %s", self.last_block.index) return True, orphans diff --git a/minichain/pow.py b/minichain/pow.py index 18a481a..1dbcf57 100644 --- a/minichain/pow.py +++ b/minichain/pow.py @@ -26,6 +26,7 @@ def mine_block( target = target if target is not None else block.target if not isinstance(target, int) or target <= 0: raise ValueError("Target must be a positive integer.") + block.target = target local_nonce = 0 header_dict = block.to_header_dict() # Construct header dict once outside loop diff --git a/minichain/state.py b/minichain/state.py index 0d80764..9cc0e2d 100644 --- a/minichain/state.py +++ b/minichain/state.py @@ -10,6 +10,66 @@ logger = logging.getLogger(__name__) +class StateJournal: + """ + An in-memory proxy dictionary that caches reads and writes to avoid + expensive deep copies of the entire state dictionary during transactions. + """ + def __init__(self, backing_dict): + self.backing = backing_dict + self.cache = {} + + def __getitem__(self, key): + if key not in self.cache: + if key in self.backing: + import copy + self.cache[key] = copy.deepcopy(self.backing[key]) + else: + raise KeyError(key) + return self.cache[key] + + def __setitem__(self, key, value): + self.cache[key] = value + + def __delitem__(self, key): + raise NotImplementedError("Account deletion not supported in StateJournal") + + def __contains__(self, key): + return key in self.cache or key in self.backing + + def get(self, key, default=None): + try: + return self.__getitem__(key) + except KeyError: + return default + + def items(self): + res = self.backing.copy() + res.update(self.cache) + return res.items() + + def update(self, other_dict): + if hasattr(other_dict, 'items'): + for k, v in other_dict.items(): + self[k] = v + else: + for k, v in other_dict: + self[k] = v + + def copy(self): + res = self.backing.copy() + res.update(self.cache) + return res + + def commit(self): + """Flushes cached modifications to the backing dictionary.""" + self.backing.update(self.cache) + self.cache.clear() + + def rollback(self): + """Discards modifications.""" + self.cache.clear() + class State: def __init__(self): @@ -69,9 +129,11 @@ def verify_transaction_logic(self, tx): def copy(self): """ Return an independent copy of state for transactional validation. + Uses StateJournal for O(1) cloning instead of deepcopy. """ - new_state = copy.deepcopy(self) - new_state.contract_machine = ContractMachine(new_state) # Reinitialize contract_machine + new_state = State() + new_state.accounts = StateJournal(self.accounts) + new_state.contract_machine = ContractMachine(new_state) new_state.chain_id = self.chain_id return new_state @@ -124,22 +186,22 @@ def apply_transaction(self, tx): def _apply_validated_tx(self, tx): + original_accounts = self.accounts + journal = StateJournal(original_accounts) + self.accounts = journal + sender = self.accounts[tx.sender] total_cost = tx.amount + (getattr(tx, 'gas_limit', 0) * getattr(tx, 'fee_per_gas', 0)) sender['balance'] -= total_cost sender['nonce'] += 1 - import copy - state_snapshot = copy.deepcopy(self.accounts) - def rollback_and_refund(error_message, gas_used): - self.accounts = copy.deepcopy(state_snapshot) + journal.rollback() + self.accounts = original_accounts refund_acc = self.accounts[tx.sender] - refund_acc['balance'] += tx.amount - gas_refund = getattr(tx, 'gas_limit', 0) - gas_used - if gas_refund > 0: - refund_acc['balance'] += (gas_refund * getattr(tx, 'fee_per_gas', 0)) + refund_acc['balance'] -= (gas_used * getattr(tx, 'fee_per_gas', 0)) + refund_acc['nonce'] += 1 return Receipt(tx.tx_id, status=0, error_message=error_message, gas_used=gas_used) # LOGIC BRANCH 1: Contract Deployment @@ -162,6 +224,9 @@ def rollback_and_refund(error_message, gas_used): gas_refund = gas_used - code_gas if gas_refund > 0: self.accounts[tx.sender]['balance'] += (gas_refund * getattr(tx, 'fee_per_gas', 0)) + + journal.commit() + self.accounts = original_accounts return Receipt(tx.tx_id, status=1, contract_address=contract_address, gas_used=code_gas) # LOGIC BRANCH 2: Contract Call @@ -187,12 +252,17 @@ def rollback_and_refund(error_message, gas_used): if gas_refund > 0: self.accounts[tx.sender]['balance'] += (gas_refund * getattr(tx, 'fee_per_gas', 0)) + journal.commit() + self.accounts = original_accounts return Receipt(tx.tx_id, status=1, gas_used=gas_used) # LOGIC BRANCH 3: Regular Transfer receiver = self.get_account(tx.receiver) receiver['balance'] += tx.amount gas_used = getattr(tx, 'gas_limit', 0) + + journal.commit() + self.accounts = original_accounts return Receipt(tx.tx_id, status=1, gas_used=gas_used) def execute_internal_call(self, sender, receiver_address, amount, payload, gas_limit, depth, is_top_level=False): diff --git a/tests/test_persistence_runtime.py b/tests/test_persistence_runtime.py index ba172ee..fa1dcca 100644 --- a/tests/test_persistence_runtime.py +++ b/tests/test_persistence_runtime.py @@ -71,12 +71,12 @@ def _chain_with_tx(self): index=1, previous_hash=bc.last_block.hash, transactions=[tx], - target=int("F"*64, 16), + target=bc.current_target, state_root=temp_state.state_root(), receipt_root=calculate_receipt_root([receipt]), receipts=[receipt], ) - mine_block(block, target=int("F"*64, 16)) + mine_block(block, target=bc.current_target) bc.add_block(block) return bc diff --git a/tests/test_target.py b/tests/test_target.py index 57966d8..f58aff1 100644 --- a/tests/test_target.py +++ b/tests/test_target.py @@ -12,6 +12,7 @@ def test_target_adjustment(self): chain.avg_block_time = 1000 chain.current_target = MAX_TARGET - 10 chain.chain[0].target = MAX_TARGET - 10 + chain.chain[0].hash = chain.chain[0].compute_hash() # Fast mining: timestamps only 1ms apart # avg = 0.5 * 1 + 0.5 * 1000 = 500.5 (which is < 1000) => target decrements by 1 @@ -36,6 +37,7 @@ def test_reorg_target_validation(self): chain1.avg_block_time = 1000 chain1.current_target = MAX_TARGET - 10 chain1.chain[0].target = MAX_TARGET - 10 + chain1.chain[0].hash = chain1.chain[0].compute_hash() chain2 = Blockchain() chain2.target_block_time = 1000 @@ -43,6 +45,7 @@ def test_reorg_target_validation(self): chain2.avg_block_time = 1000 chain2.current_target = MAX_TARGET - 10 chain2.chain[0].target = MAX_TARGET - 10 + chain2.chain[0].hash = chain2.chain[0].compute_hash() # Chain 2 mines a fast block, target goes to MAX_TARGET - 11 block1 = Block(1, chain2.last_block.hash, [], timestamp=chain2.last_block.timestamp + 1, target=chain2.current_target, state_root=chain2.state.state_root()) @@ -51,7 +54,7 @@ def test_reorg_target_validation(self): self.assertEqual(chain2.current_target, MAX_TARGET - 11) # Reorg chain1 to chain2 - success, orphans = chain1.resolve_conflicts(chain2.chain) + success, _ = chain1.resolve_conflicts(chain2.chain) self.assertTrue(success) self.assertEqual(chain1.current_target, MAX_TARGET - 11)