From 328d2cea87cb925ebf4de0d71191c833ecb09df4 Mon Sep 17 00:00:00 2001 From: siddhant Date: Tue, 7 Jul 2026 23:01:09 +0530 Subject: [PATCH 1/4] feat: decouple fee into gas_limit and max_fee_per_gas, configurable genesis supply --- genesis.json | 1 + main.py | 52 +++++++++++++++++--------------- minichain/chain.py | 2 +- minichain/mempool.py | 4 +-- minichain/state.py | 24 ++++++++++----- minichain/transaction.py | 17 ++++++----- tests/test_contract.py | 30 +++++++++--------- tests/test_contract_transfers.py | 12 ++++---- tests/test_core.py | 2 +- tests/test_reorg.py | 10 +++--- 10 files changed, 84 insertions(+), 70 deletions(-) diff --git a/genesis.json b/genesis.json index eb966c5..ed1f2b5 100644 --- a/genesis.json +++ b/genesis.json @@ -4,6 +4,7 @@ "difficulty": 4, "target_block_time": 10000, "alpha": 0.1, + "initial_supply": 1500000000, "alloc": { "0000000000000000000000000000000000000001": { "balance": 1000000000 diff --git a/main.py b/main.py index cfb4605..c63ff11 100644 --- a/main.py +++ b/main.py @@ -7,7 +7,9 @@ Commands (type in the terminal while the node is running): balance — show all account balances - send — send coins to another address + send - send coins + deploy - deploy a contract + call - call a contract mine — mine a block from the mempool peers — show connected peers connect — connect to another node @@ -85,19 +87,20 @@ def request_chain(network, start_index, limit): asyncio.create_task(network._broadcast_raw(req)) -def parse_amount_fee(parts, start): - """Parse optional non-negative amount/fee at parts[start] and parts[start+1]. - Returns (amount, fee), or None if invalid (a message is printed for the user).""" +def parse_tx_params(parts, start): + """Parse optional non-negative amount, gas_limit, max_fee_per_gas. + Returns (amount, gas_limit, max_fee), or None if invalid.""" try: amount = int(parts[start]) if len(parts) > start else 0 - fee = int(parts[start + 1]) if len(parts) > start + 1 else 0 + gas_limit = int(parts[start + 1]) if len(parts) > start + 1 else 0 + max_fee = int(parts[start + 2]) if len(parts) > start + 2 else 0 except ValueError: - print(" Amount and fee must be integers.") + print(" Values must be integers.") return None - if amount < 0 or fee < 0: - print(" Amount and fee cannot be negative.") + if amount < 0 or gas_limit < 0 or max_fee < 0: + print(" Values cannot be negative.") return None - return amount, fee + return amount, gas_limit, max_fee async def submit_and_broadcast(mempool, network, tx, ok_msg, reject_msg): @@ -143,7 +146,7 @@ def mine_and_process_block(chain, mempool, miner_pk): logger.info("No mineable transactions in current queue window.") return None - total_fees = sum(getattr(r, 'gas_used', 0) for r in receipts) + total_fees = sum(getattr(r, 'gas_used', 0) * getattr(tx, 'max_fee_per_gas', 0) for r, tx in zip(receipts, mineable_txs)) temp_state.credit_mining_reward(miner_pk, reward=temp_state.DEFAULT_MINING_REWARD + total_fees) block = Block( @@ -399,7 +402,7 @@ def print_prompt_info(current_pk): # ── send ── elif cmd == "send": if len(parts) < 3: - print(" Usage: send [fee]") + print(" Usage: send [gas_limit] [max_fee_per_gas]") continue receiver = parts[1] if not is_valid_receiver(receiver): @@ -407,19 +410,20 @@ def print_prompt_info(current_pk): continue try: amount = int(parts[2]) - fee = int(parts[3]) if len(parts) > 3 else 0 + gas_limit = int(parts[3]) if len(parts) > 3 else 0 + max_fee = int(parts[4]) if len(parts) > 4 else 0 except ValueError: - print(" Amount and fee must be integers.") + print(" Values must be integers.") continue if amount <= 0: print(" Amount must be greater than 0.") continue - if fee < 0: - print(" Fee cannot be negative.") + if gas_limit < 0 or max_fee < 0: + print(" Gas limit and max fee cannot be negative.") continue nonce = chain.state.get_account(pk).get("nonce", 0) - tx = Transaction(sender=pk, receiver=receiver, amount=amount, nonce=nonce, fee=fee, chain_id=chain.chain_id) + tx = Transaction(sender=pk, receiver=receiver, amount=amount, nonce=nonce, gas_limit=gas_limit, max_fee_per_gas=max_fee, chain_id=chain.chain_id) tx.sign(sk) await submit_and_broadcast( @@ -431,7 +435,7 @@ def print_prompt_info(current_pk): # ── deploy ── elif cmd == "deploy": if len(parts) < 2: - print(" Usage: deploy [amount] [fee]") + print(" Usage: deploy [amount] [gas_limit] [max_fee_per_gas]") continue filepath = parts[1] try: @@ -441,13 +445,13 @@ def print_prompt_info(current_pk): print(f" File not found: {filepath}") continue - parsed = parse_amount_fee(parts, 2) + parsed = parse_tx_params(parts, 2) if parsed is None: continue - amount, fee = parsed + amount, gas_limit, max_fee = parsed nonce = chain.state.get_account(pk).get("nonce", 0) - tx = Transaction(sender=pk, receiver=None, amount=amount, nonce=nonce, fee=fee, data=code, chain_id=chain.chain_id) + tx = Transaction(sender=pk, receiver=None, amount=amount, nonce=nonce, gas_limit=gas_limit, max_fee_per_gas=max_fee, data=code, chain_id=chain.chain_id) tx.sign(sk) await submit_and_broadcast( @@ -459,7 +463,7 @@ def print_prompt_info(current_pk): # ── call ── elif cmd == "call": if len(parts) < 3: - print(" Usage: call [amount] [fee]") + print(" Usage: call [amount] [gas_limit] [max_fee_per_gas]") continue receiver = parts[1] if not is_valid_receiver(receiver): @@ -467,13 +471,13 @@ def print_prompt_info(current_pk): continue payload = parts[2] - parsed = parse_amount_fee(parts, 3) + parsed = parse_tx_params(parts, 3) if parsed is None: continue - amount, fee = parsed + amount, gas_limit, max_fee = parsed nonce = chain.state.get_account(pk).get("nonce", 0) - tx = Transaction(sender=pk, receiver=receiver, amount=amount, nonce=nonce, fee=fee, data=payload, chain_id=chain.chain_id) + tx = Transaction(sender=pk, receiver=receiver, amount=amount, nonce=nonce, gas_limit=gas_limit, max_fee_per_gas=max_fee, data=payload, chain_id=chain.chain_id) tx.sign(sk) await submit_and_broadcast( diff --git a/minichain/chain.py b/minichain/chain.py index f3e2bf1..25db030 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -168,7 +168,7 @@ def _apply_block(self, prev_block, block, state, difficulty, avg_block_time): return status, difficulty, avg_block_time receipts.append(receipt) - total_fees = sum(getattr(r, 'gas_used', 0) for r in receipts) + total_fees = sum(getattr(r, 'gas_used', 0) * getattr(tx, 'max_fee_per_gas', 0) for r, tx in zip(receipts, block.transactions)) if block.miner: state.credit_mining_reward(block.miner, reward=state.DEFAULT_MINING_REWARD + total_fees) diff --git a/minichain/mempool.py b/minichain/mempool.py index a6e7630..f42de6c 100644 --- a/minichain/mempool.py +++ b/minichain/mempool.py @@ -52,10 +52,10 @@ def add_transaction(self, tx): i_min = min(i_min, i_max) - # Insert before the first tx in [i_min, i_max] that has a lower fee + # Insert before the first tx in [i_min, i_max] that has a lower max_fee_per_gas insert_idx = i_max for j in range(i_min, i_max): - if getattr(self._list[j], 'fee', 0) < getattr(tx, 'fee', 0): + if getattr(self._list[j], 'max_fee_per_gas', 0) < getattr(tx, 'max_fee_per_gas', 0): insert_idx = j break diff --git a/minichain/state.py b/minichain/state.py index 92431dd..29ff7fc 100644 --- a/minichain/state.py +++ b/minichain/state.py @@ -54,7 +54,7 @@ def verify_transaction_logic(self, tx): sender_acc = self.get_account(tx.sender) - total_cost = tx.amount + getattr(tx, 'fee', 0) + total_cost = tx.amount + (getattr(tx, 'gas_limit', 0) * getattr(tx, 'max_fee_per_gas', 0)) if sender_acc['balance'] < total_cost: logger.warning("Invalid tx %s: insufficient balance", tx.tx_id) return ValidationStatus.FAILED @@ -91,8 +91,9 @@ def _amounts_well_formed(tx): """Semantic guard: amount and fee must be non-negative integers.""" if not isinstance(tx.amount, int) or tx.amount < 0: return False - fee = getattr(tx, "fee", 0) - return isinstance(fee, int) and fee >= 0 + gas_limit = getattr(tx, "gas_limit", 0) + max_fee = getattr(tx, "max_fee_per_gas", 0) + return isinstance(gas_limit, int) and gas_limit >= 0 and isinstance(max_fee, int) and max_fee >= 0 def validate_and_apply_with_status(self, tx): """ @@ -128,7 +129,7 @@ def _apply_validated_tx(self, tx): """ sender = self.accounts[tx.sender] - total_cost = tx.amount + getattr(tx, 'fee', 0) + total_cost = tx.amount + (getattr(tx, 'gas_limit', 0) * getattr(tx, 'max_fee_per_gas', 0)) # Deduct funds and increment nonce sender['balance'] -= total_cost @@ -137,7 +138,10 @@ def _apply_validated_tx(self, tx): # LOGIC BRANCH 1: Contract Deployment if tx.receiver is None or tx.receiver == "": contract_address = self.derive_contract_address(tx.sender, tx.nonce) - gas_used = getattr(tx, 'fee', 0) + gas_used = getattr(tx, 'gas_limit', 0) + gas_refund = tx.gas_limit - gas_used + if gas_refund > 0: + sender['balance'] += (gas_refund * tx.max_fee_per_gas) # Prevent redeploy collision existing = self.accounts.get(contract_address) @@ -153,7 +157,7 @@ def _apply_validated_tx(self, tx): # If data is provided (non-empty), treat as contract call if tx.data: receiver = self.accounts.get(tx.receiver) - gas_limit = getattr(tx, 'fee', 0) + gas_limit = getattr(tx, 'gas_limit', 0) # Fail if contract does not exist or has no code if not receiver or not receiver.get("code"): @@ -180,7 +184,7 @@ def revert_transfer(): gas_used = result.get("gas_used", gas_limit) gas_refund = gas_limit - gas_used if gas_refund > 0: - sender['balance'] += gas_refund + sender['balance'] += (gas_refund * getattr(tx, 'max_fee_per_gas', 0)) if not result.get("success"): revert_transfer() @@ -206,7 +210,11 @@ def revert_transfer(): # LOGIC BRANCH 3: Regular Transfer receiver = self.get_account(tx.receiver) receiver['balance'] += tx.amount - return Receipt(tx.tx_id, status=1, gas_used=getattr(tx, 'fee', 0)) + gas_used = getattr(tx, 'gas_limit', 0) + gas_refund = tx.gas_limit - gas_used + if gas_refund > 0: + sender['balance'] += (gas_refund * tx.max_fee_per_gas) + return Receipt(tx.tx_id, status=1, gas_used=gas_used) def derive_contract_address(self, sender, nonce): raw = f"{sender}:{nonce}".encode() diff --git a/minichain/transaction.py b/minichain/transaction.py index 7754dcd..c80a41e 100644 --- a/minichain/transaction.py +++ b/minichain/transaction.py @@ -6,7 +6,7 @@ class Transaction: - _TX_FIELDS = frozenset({"sender", "receiver", "amount", "fee", "nonce", "data", "timestamp", "chain_id", "signature"}) + _TX_FIELDS = frozenset({"sender", "receiver", "amount", "gas_limit", "max_fee_per_gas", "nonce", "data", "timestamp", "chain_id", "signature"}) def __setattr__(self, name, value) -> None: if name in self._TX_FIELDS and getattr(self, "_sealed", False): @@ -23,11 +23,12 @@ def _normalize_ts(ts) -> int: # If it's already in milliseconds (>= 1e12), just ensure it's an integer return int(ts) - def __init__(self, sender, receiver, amount, nonce, fee=0, data=None, chain_id="minichain-default", signature=None, timestamp=None): + def __init__(self, sender, receiver, amount, nonce, gas_limit=0, max_fee_per_gas=0, data=None, chain_id="minichain-default", signature=None, timestamp=None): self.sender = sender self.receiver = receiver self.amount = amount - self.fee = fee + self.gas_limit = gas_limit + self.max_fee_per_gas = max_fee_per_gas self.nonce = nonce self.data = data self.chain_id = chain_id @@ -37,18 +38,18 @@ def __init__(self, sender, receiver, amount, nonce, fee=0, data=None, chain_id=" self._sealed = False def to_dict(self): - return {"sender": self.sender, "receiver": self.receiver, "amount": self.amount, "fee": self.fee, - "nonce": self.nonce, "data": self.data, "chain_id": self.chain_id, "timestamp": self.timestamp, + return {"sender": self.sender, "receiver": self.receiver, "amount": self.amount, "gas_limit": self.gas_limit, + "max_fee_per_gas": self.max_fee_per_gas, "nonce": self.nonce, "data": self.data, "chain_id": self.chain_id, "timestamp": self.timestamp, "signature": self.signature} def to_signing_dict(self): - return {"sender": self.sender, "receiver": self.receiver, "amount": self.amount, "fee": self.fee, - "nonce": self.nonce, "data": self.data, "chain_id": self.chain_id, "timestamp": self.timestamp} + return {"sender": self.sender, "receiver": self.receiver, "amount": self.amount, "gas_limit": self.gas_limit, + "max_fee_per_gas": self.max_fee_per_gas, "nonce": self.nonce, "data": self.data, "chain_id": self.chain_id, "timestamp": self.timestamp} @classmethod def from_dict(cls, payload: dict): return cls(sender=payload["sender"], receiver=payload.get("receiver"), - amount=payload["amount"], nonce=payload["nonce"], fee=payload["fee"], + amount=payload["amount"], nonce=payload["nonce"], gas_limit=payload.get("gas_limit", 0), max_fee_per_gas=payload.get("max_fee_per_gas", 0), data=payload.get("data"), chain_id=payload.get("chain_id", "minichain-default"), signature=payload.get("signature"), timestamp=payload.get("timestamp")) diff --git a/tests/test_contract.py b/tests/test_contract.py index c8a5571..bdf2843 100644 --- a/tests/test_contract.py +++ b/tests/test_contract.py @@ -23,7 +23,7 @@ def test_deploy_and_execute(self): storage['counter'] = storage.get('counter', 0) + 1 """ - tx_deploy = Transaction(self.pk, None, 0, 0, fee=500, data=code) + tx_deploy = Transaction(self.pk, None, 0, 0, gas_limit=500, max_fee_per_gas=1, data=code) tx_deploy.sign(self.sk) receipt_deploy = self.state.apply_transaction(tx_deploy) @@ -32,7 +32,7 @@ def test_deploy_and_execute(self): contract_addr = receipt_deploy.contract_address self.assertTrue(isinstance(contract_addr, str)) - tx_call = Transaction(self.pk, contract_addr, 0, 1, fee=1000, data="increment") + tx_call = Transaction(self.pk, contract_addr, 0, 1, gas_limit=1000, max_fee_per_gas=1, data="increment") tx_call.sign(self.sk) receipt_call = self.state.apply_transaction(tx_call) @@ -50,7 +50,7 @@ def test_deploy_insufficient_balance(self): code = "storage['x'] = 1" - tx = Transaction(poor_pk, None, 1000, 0, fee=500, data=code) + tx = Transaction(poor_pk, None, 1000, 0, gas_limit=500, max_fee_per_gas=1, data=code) tx.sign(poor_sk) receipt = self.state.apply_transaction(tx) @@ -63,7 +63,7 @@ def test_call_non_existent_contract(self): fake_sk = SigningKey.generate() fake_receiver = fake_sk.verify_key.encode(encoder=HexEncoder).decode() - tx = Transaction(self.pk, fake_receiver, 0, 0, fee=500, data="increment") + tx = Transaction(self.pk, fake_receiver, 0, 0, gas_limit=500, max_fee_per_gas=1, data="increment") tx.sign(self.sk) receipt = self.state.apply_transaction(tx) @@ -78,7 +78,7 @@ def test_contract_runtime_exception(self): raise Exception("boom") """ - tx_deploy = Transaction(self.pk, None, 0, 0, fee=500, data=code) + tx_deploy = Transaction(self.pk, None, 0, 0, gas_limit=500, max_fee_per_gas=1, data=code) tx_deploy.sign(self.sk) receipt_deploy = self.state.apply_transaction(tx_deploy) @@ -87,7 +87,7 @@ def test_contract_runtime_exception(self): contract_addr = receipt_deploy.contract_address self.assertTrue(isinstance(contract_addr, str)) - tx_call = Transaction(self.pk, contract_addr, 0, 1, fee=1000, data="anything") + tx_call = Transaction(self.pk, contract_addr, 0, 1, gas_limit=1000, max_fee_per_gas=1, data="anything") tx_call.sign(self.sk) receipt_call = self.state.apply_transaction(tx_call) @@ -104,7 +104,7 @@ def test_redeploy_same_address(self): code = "storage['x'] = 1" # First deploy - tx1 = Transaction(self.pk, None, 0, 0, fee=500, data=code) + tx1 = Transaction(self.pk, None, 0, 0, gas_limit=500, max_fee_per_gas=1, data=code) tx1.sign(self.sk) receipt1 = self.state.apply_transaction(tx1) @@ -121,7 +121,7 @@ def test_redeploy_same_address(self): self.state.create_contract(collision_addr, "storage['y'] = 2") # Attempt redeploy - tx2 = Transaction(self.pk, None, 0, next_nonce, fee=500, data=code) + tx2 = Transaction(self.pk, None, 0, next_nonce, gas_limit=500, max_fee_per_gas=1, data=code) tx2.sign(self.sk) receipt2 = self.state.apply_transaction(tx2) @@ -138,7 +138,7 @@ def test_balance_and_nonce_updates(self): code = "storage['x'] = 1" - tx_deploy = Transaction(self.pk, None, 10, initial_nonce, fee=500, data=code) + tx_deploy = Transaction(self.pk, None, 10, initial_nonce, gas_limit=500, max_fee_per_gas=1, data=code) tx_deploy.sign(self.sk) receipt = self.state.apply_transaction(tx_deploy) @@ -159,14 +159,14 @@ def test_out_of_gas(self): code = "while True: pass" # 1. Deploy code - tx_deploy = Transaction(self.pk, None, 0, 0, fee=500, data=code) + tx_deploy = Transaction(self.pk, None, 0, 0, gas_limit=500, max_fee_per_gas=1, data=code) tx_deploy.sign(self.sk) receipt_deploy = self.state.apply_transaction(tx_deploy) self.assertEqual(receipt_deploy.status, 1) contract_addr = receipt_deploy.contract_address # 2. Call code with specific fee (gas limit) - tx_call = Transaction(self.pk, contract_addr, 0, 1, fee=1000, data="loop") + tx_call = Transaction(self.pk, contract_addr, 0, 1, gas_limit=1000, max_fee_per_gas=1, data="loop") tx_call.sign(self.sk) balance_before = self.state.get_account(self.pk)["balance"] @@ -184,13 +184,13 @@ def test_out_of_gas(self): def test_malicious_import(self): """Contract attempting to import a module should fail AST validation.""" code = "import os\nstorage['x'] = 1" - tx_deploy = Transaction(self.pk, None, 0, 0, fee=500, data=code) + tx_deploy = Transaction(self.pk, None, 0, 0, gas_limit=500, max_fee_per_gas=1, data=code) tx_deploy.sign(self.sk) receipt_deploy = self.state.apply_transaction(tx_deploy) self.assertEqual(receipt_deploy.status, 1) # Deploy succeeds, saves code - tx_call = Transaction(self.pk, receipt_deploy.contract_address, 0, 1, fee=500, data="call") + tx_call = Transaction(self.pk, receipt_deploy.contract_address, 0, 1, gas_limit=500, max_fee_per_gas=1, data="call") tx_call.sign(self.sk) receipt_call = self.state.apply_transaction(tx_call) @@ -202,13 +202,13 @@ def test_malicious_file_deletion(self): """Contract attempting to use open() or file IO should fail at runtime due to missing builtins.""" # Using open() which is stripped from __builtins__ code = "f = open('critical_file.txt', 'w')\nf.write('hacked')" - tx_deploy = Transaction(self.pk, None, 0, 0, fee=500, data=code) + tx_deploy = Transaction(self.pk, None, 0, 0, gas_limit=500, max_fee_per_gas=1, data=code) tx_deploy.sign(self.sk) receipt_deploy = self.state.apply_transaction(tx_deploy) self.assertEqual(receipt_deploy.status, 1) - tx_call = Transaction(self.pk, receipt_deploy.contract_address, 0, 1, fee=500, data="call") + tx_call = Transaction(self.pk, receipt_deploy.contract_address, 0, 1, gas_limit=500, max_fee_per_gas=1, data="call") tx_call.sign(self.sk) receipt_call = self.state.apply_transaction(tx_call) diff --git a/tests/test_contract_transfers.py b/tests/test_contract_transfers.py index 30dc1b4..6615ee0 100644 --- a/tests/test_contract_transfers.py +++ b/tests/test_contract_transfers.py @@ -26,7 +26,7 @@ def test_successful_transfer_out(self): transfer_out(target, 50) transfer_out(target, 25) """ - deploy_tx = self._sign(Transaction(self.sender_pk, None, amount=100, nonce=0, data=code, fee=1000)) + deploy_tx = self._sign(Transaction(self.sender_pk, None, amount=100, nonce=0, data=code, gas_limit=1000, max_fee_per_gas=1)) receipt = self.state.apply_transaction(deploy_tx) self.assertEqual(receipt.status, 1) contract_addr = receipt.contract_address @@ -36,7 +36,7 @@ def test_successful_transfer_out(self): self.assertEqual(self.state.get_account(self.target_pk)['balance'], 0) # 2. Call Contract to transfer out 75 coins - call_tx = self._sign(Transaction(self.sender_pk, contract_addr, amount=0, nonce=1, data={"target": self.target_pk}, fee=1000)) + call_tx = self._sign(Transaction(self.sender_pk, contract_addr, amount=0, nonce=1, data={"target": self.target_pk}, gas_limit=1000, max_fee_per_gas=1)) receipt2 = self.state.apply_transaction(call_tx) self.assertEqual(receipt2.status, 1) @@ -55,13 +55,13 @@ def test_failed_transfer_out_insufficient_balance(self): transfer_out(target, 500) storage['malicious_state'] = 'corrupted' """ - deploy_tx = self._sign(Transaction(self.sender_pk, None, amount=100, nonce=0, data=code, fee=1000)) + deploy_tx = self._sign(Transaction(self.sender_pk, None, amount=100, nonce=0, data=code, gas_limit=1000, max_fee_per_gas=1)) receipt = self.state.apply_transaction(deploy_tx) self.assertEqual(receipt.status, 1) contract_addr = receipt.contract_address # 2. Call Contract - call_tx = self._sign(Transaction(self.sender_pk, contract_addr, amount=50, nonce=1, data={"target": self.target_pk}, fee=1000)) + call_tx = self._sign(Transaction(self.sender_pk, contract_addr, amount=50, nonce=1, data={"target": self.target_pk}, gas_limit=1000, max_fee_per_gas=1)) receipt2 = self.state.apply_transaction(call_tx) # Should fail with status 0 @@ -87,7 +87,7 @@ def test_transfer_with_incoming_funds(self): # We use the incoming funds to instantly transfer out! transfer_out(target, msg['value']) """ - deploy_tx = self._sign(Transaction(self.sender_pk, None, amount=0, nonce=0, data=code, fee=1000)) + deploy_tx = self._sign(Transaction(self.sender_pk, None, amount=0, nonce=0, data=code, gas_limit=1000, max_fee_per_gas=1)) receipt = self.state.apply_transaction(deploy_tx) self.assertEqual(receipt.status, 1) contract_addr = receipt.contract_address @@ -95,7 +95,7 @@ def test_transfer_with_incoming_funds(self): self.assertEqual(self.state.get_account(contract_addr)['balance'], 0) # 2. Call Contract sending 50 coins - call_tx = self._sign(Transaction(self.sender_pk, contract_addr, amount=50, nonce=1, data={"target": self.target_pk}, fee=1000)) + call_tx = self._sign(Transaction(self.sender_pk, contract_addr, amount=50, nonce=1, data={"target": self.target_pk}, gas_limit=1000, max_fee_per_gas=1)) receipt2 = self.state.apply_transaction(call_tx) self.assertEqual(receipt2.status, 1) diff --git a/tests/test_core.py b/tests/test_core.py index 93d68d6..e4236c6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -66,7 +66,7 @@ def test_transaction_fee(self): """Test that transaction fees are properly deducted and credited.""" self.state.credit_mining_reward(self.alice_pk, 100) - tx = Transaction(self.alice_pk, self.bob_pk, 40, 0, fee=5) + tx = Transaction(self.alice_pk, self.bob_pk, 40, 0, gas_limit=5, max_fee_per_gas=1) tx.sign(self.alice_sk) receipt = self.state.validate_and_apply(tx) diff --git a/tests/test_reorg.py b/tests/test_reorg.py index e931b47..8fe5940 100644 --- a/tests/test_reorg.py +++ b/tests/test_reorg.py @@ -39,7 +39,7 @@ def test_resolve_conflicts_heavier_chain(genesis_file): assert node_a.get_total_work() == node_b.get_total_work() pool_b = Mempool() - tx = Transaction(sender=pk, receiver="b"*64, amount=10, nonce=0, fee=1) + tx = Transaction(sender=pk, receiver="b"*64, amount=10, nonce=0, gas_limit=1, max_fee_per_gas=1) tx.sign(sk) pool_b.add_transaction(tx) @@ -65,19 +65,19 @@ def test_resolve_conflicts_reorg_with_orphans(genesis_file): pool_b = Mempool() # Node A mines tx1 (nonce 0) - tx1 = Transaction(sender=pk, receiver="a"*64, amount=10, nonce=0, fee=1) + tx1 = Transaction(sender=pk, receiver="a"*64, amount=10, nonce=0, gas_limit=1, max_fee_per_gas=1) tx1.sign(sk) pool_a.add_transaction(tx1) mine_and_process_block(node_a, pool_a, pk) # Node B mines tx2 (nonce 0, competing transaction) - tx2 = Transaction(sender=pk, receiver="b"*64, amount=20, nonce=0, fee=1) + tx2 = Transaction(sender=pk, receiver="b"*64, amount=20, nonce=0, gas_limit=1, max_fee_per_gas=1) tx2.sign(sk) pool_b.add_transaction(tx2) mine_and_process_block(node_b, pool_b, pk) # Node B mines tx3 (nonce 1) to become the heavier chain - tx3 = Transaction(sender=pk, receiver="c"*64, amount=30, nonce=1, fee=1) + tx3 = Transaction(sender=pk, receiver="c"*64, amount=30, nonce=1, gas_limit=1, max_fee_per_gas=1) tx3.sign(sk) pool_b.add_transaction(tx3) block_b2 = mine_and_process_block(node_b, pool_b, pk) @@ -103,7 +103,7 @@ def test_resolve_conflicts_rejects_lighter_chain(genesis_file): pool_a = Mempool() # Node A mines a block - tx1 = Transaction(sender=pk, receiver="a"*64, amount=10, nonce=0, fee=1) + tx1 = Transaction(sender=pk, receiver="a"*64, amount=10, nonce=0, gas_limit=1, max_fee_per_gas=1) tx1.sign(sk) pool_a.add_transaction(tx1) mine_and_process_block(node_a, pool_a, pk) From a15d2280a5308fda560e48d3c22f8f4f6ba3d987 Mon Sep 17 00:00:00 2001 From: siddhant Date: Wed, 8 Jul 2026 18:17:13 +0530 Subject: [PATCH 2/4] fix: address PR review feedback on genesis validation, send CLI, and state gas refunds --- main.py | 13 ++++--------- minichain/chain.py | 7 +++++++ minichain/state.py | 6 ------ 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/main.py b/main.py index c63ff11..ffc5f24 100644 --- a/main.py +++ b/main.py @@ -408,19 +408,14 @@ def print_prompt_info(current_pk): if not is_valid_receiver(receiver): print(" Invalid receiver format. Expected 40 or 64 hex characters.") continue - try: - amount = int(parts[2]) - gas_limit = int(parts[3]) if len(parts) > 3 else 0 - max_fee = int(parts[4]) if len(parts) > 4 else 0 - except ValueError: - print(" Values must be integers.") + parsed = parse_tx_params(parts, 2) + if parsed is None: continue + amount, gas_limit, max_fee = parsed + if amount <= 0: print(" Amount must be greater than 0.") continue - if gas_limit < 0 or max_fee < 0: - print(" Gas limit and max fee cannot be negative.") - continue nonce = chain.state.get_account(pk).get("nonce", 0) tx = Transaction(sender=pk, receiver=receiver, amount=amount, nonce=nonce, gas_limit=gas_limit, max_fee_per_gas=max_fee, chain_id=chain.chain_id) diff --git a/minichain/chain.py b/minichain/chain.py index 25db030..623b949 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -68,6 +68,7 @@ def _create_genesis_block(self, genesis_path): # Apply genesis allocations alloc = config.get("alloc", {}) + total_alloc = 0 for address, data in alloc.items(): balance = data.get("balance", 0) if not isinstance(balance, int) or balance < 0: @@ -75,6 +76,12 @@ def _create_genesis_block(self, genesis_path): sys.exit(1) account = self.state.get_account(address) account['balance'] = balance + total_alloc += balance + + initial_supply = config.get("initial_supply") + if initial_supply is not None and initial_supply != total_alloc: + logger.error("Genesis allocation mismatch: initial_supply is %s but alloc sum is %s", initial_supply, total_alloc) + sys.exit(1) self.chain_id = config.get("chain_id", "minichain-default") self.state.chain_id = self.chain_id diff --git a/minichain/state.py b/minichain/state.py index 29ff7fc..5b10f1b 100644 --- a/minichain/state.py +++ b/minichain/state.py @@ -139,9 +139,6 @@ def _apply_validated_tx(self, tx): if tx.receiver is None or tx.receiver == "": contract_address = self.derive_contract_address(tx.sender, tx.nonce) gas_used = getattr(tx, 'gas_limit', 0) - gas_refund = tx.gas_limit - gas_used - if gas_refund > 0: - sender['balance'] += (gas_refund * tx.max_fee_per_gas) # Prevent redeploy collision existing = self.accounts.get(contract_address) @@ -211,9 +208,6 @@ def revert_transfer(): receiver = self.get_account(tx.receiver) receiver['balance'] += tx.amount gas_used = getattr(tx, 'gas_limit', 0) - gas_refund = tx.gas_limit - gas_used - if gas_refund > 0: - sender['balance'] += (gas_refund * tx.max_fee_per_gas) return Receipt(tx.tx_id, status=1, gas_used=gas_used) def derive_contract_address(self, sender, nonce): From a521a2331295b9ad1450dcda0b23745d31b77996 Mon Sep 17 00:00:00 2001 From: siddhant Date: Wed, 8 Jul 2026 20:46:02 +0530 Subject: [PATCH 3/4] fix: resolve critical mempool, state, and sandbox vulnerabilities --- main.py | 51 ++++++++++++++++++++++++++++--------------- minichain/block.py | 7 +++++- minichain/chain.py | 42 ++++++++++++++++++++--------------- minichain/contract.py | 5 ++++- minichain/mempool.py | 7 ++++++ minichain/rpc.py | 3 +++ minichain/state.py | 3 ++- 7 files changed, 80 insertions(+), 38 deletions(-) diff --git a/main.py b/main.py index ffc5f24..44d50b9 100644 --- a/main.py +++ b/main.py @@ -103,8 +103,11 @@ def parse_tx_params(parts, start): return amount, gas_limit, max_fee -async def submit_and_broadcast(mempool, network, tx, ok_msg, reject_msg): +async def submit_and_broadcast(chain, mempool, network, tx, ok_msg, reject_msg): """Add a signed tx to the mempool and broadcast it, printing the outcome.""" + if chain.state.verify_transaction_logic(tx) != ValidationStatus.VALID: + print(" ❌ Transaction failed state validation (e.g. insufficient balance or invalid nonce).") + return if mempool.add_transaction(tx): await network.broadcast_transaction(tx) print(ok_msg) @@ -138,6 +141,8 @@ def mine_and_process_block(chain, mempool, miner_pk): if receipt is not None: mineable_txs.append(tx) receipts.append(receipt) + else: + stale_txs.append(tx) if stale_txs: mempool.remove_transactions(stale_txs) @@ -220,6 +225,9 @@ async def handler(data): logger.warning("Invalid chain_id in tx from %s", peer_addr) return ValidationStatus.INVALID + if chain.state.verify_transaction_logic(tx) != ValidationStatus.VALID: + return ValidationStatus.INVALID + if mempool.add_transaction(tx): logger.info("📥 Received tx from %s... (amount=%s)", tx.sender[:8], tx.amount) return ValidationStatus.VALID @@ -244,8 +252,7 @@ async def handler(data): request_chain(network, chain.last_block.index + 1, 500) else: logger.warning("📥 Received Block #%s — rejected. Fork detected, trigger reorg sync.", block.index) - # For a fork, request the full chain to use resolve_conflicts - request_chain(network, 0, 1000000) + request_chain(network, max(0, block.index - 50), 50) return status elif msg_type == "chain_request": @@ -276,15 +283,8 @@ async def handler(data): return if new_chain: - # Distinguish between linear catch-up vs full reorg based on whether we received block 0 - if new_chain[0].index == 0: - # Fork / Reorg sync - success, orphans = chain.resolve_conflicts(new_chain) - if success: - logger.info("🔄 Reorg complete! Restoring %d orphaned txs to mempool.", len(orphans)) - for tx in orphans: - mempool.add_transaction(tx) - else: + # Differentiate between linear catchup and reorg + if new_chain[0].index == chain.last_block.index + 1 and new_chain[0].previous_hash == chain.last_block.hash: # Linear Catch-up all_added = True for block in new_chain: @@ -294,16 +294,31 @@ async def handler(data): logger.info("📥 Synced Block #%d", block.index) mempool.remove_transactions(block.transactions) else: - logger.warning("❌ Sync failed at Block #%d. Fork detected. Requesting full chain.", block.index) - request_chain(network, 0, 1000000) + logger.warning("❌ Sync failed at Block #%d. Fork detected. Requesting chunk backward.", block.index) + request_chain(network, max(0, block.index - 50), 50) all_added = False break - # If we added all blocks and we hit the limit, request next batch if all_added and len(new_chain) == requested_limit: next_index = chain.last_block.index + 1 logger.info("📡 Requesting next batch from index %d", next_index) request_chain(network, next_index, requested_limit) + else: + # Fork / Reorg sync + success, orphans = chain.resolve_conflicts(new_chain) + if success: + logger.info("🔄 Reorg complete! Restoring %d orphaned txs to mempool.", len(orphans)) + for tx in orphans: + mempool.add_transaction(tx) + + if len(new_chain) == requested_limit: + next_index = chain.last_block.index + 1 + request_chain(network, next_index, requested_limit) + else: + logger.warning("❌ Reorg failed. Fetching earlier blocks to find fork point...") + earlier_start = max(0, new_chain[0].index - 50) + if new_chain[0].index > 0: + request_chain(network, earlier_start, 50) return handler @@ -422,7 +437,7 @@ def print_prompt_info(current_pk): tx.sign(sk) await submit_and_broadcast( - mempool, network, tx, + chain, mempool, network, tx, f" {C_GREEN}✅ Tx sent:{C_RESET} {amount} coins → {receiver[:12]}...", f" {C_RED}❌ Transaction rejected{C_RESET} (invalid sig, duplicate, or mempool full).", ) @@ -450,7 +465,7 @@ def print_prompt_info(current_pk): tx.sign(sk) await submit_and_broadcast( - mempool, network, tx, + chain, mempool, network, tx, f" ✅ Deploy Tx sent (nonce={nonce}). Mine a block to confirm.", " ❌ Deploy Transaction rejected.", ) @@ -476,7 +491,7 @@ def print_prompt_info(current_pk): tx.sign(sk) await submit_and_broadcast( - mempool, network, tx, + chain, mempool, network, tx, f" ✅ Call Tx sent to {receiver[:12]}... (payload='{payload}'). Mine a block to confirm.", " ❌ Call Transaction rejected.", ) diff --git a/minichain/block.py b/minichain/block.py index 96923e5..c81dbe3 100644 --- a/minichain/block.py +++ b/minichain/block.py @@ -132,7 +132,12 @@ def from_dict(cls, payload: dict): # Safely extract and cast difficulty and timestamp if they exist raw_diff = payload.get("difficulty") - parsed_diff = int(raw_diff) if raw_diff is not None else None + if raw_diff is not None: + parsed_diff = int(raw_diff) + if parsed_diff > 256: + raise ValueError(f"Difficulty too large: {parsed_diff}") + else: + parsed_diff = None raw_ts = payload.get("timestamp") parsed_ts = int(raw_ts) if raw_ts is not None else None diff --git a/minichain/chain.py b/minichain/chain.py index 623b949..a89394d 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -221,7 +221,7 @@ def add_block(self, block): def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]: """ - Evaluates a competing chain. If it has strictly greater cumulative work, + Evaluates a competing partial or full chain. If it has strictly greater cumulative work, attempts a reorg. Rebuilds state from genesis to guarantee validity. Returns: (success_bool, list_of_orphaned_transactions) """ @@ -231,46 +231,54 @@ def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]: return False, [] with self._lock: + first_block = new_chain_list[0] + fork_idx = first_block.index + + if fork_idx == 0: + if first_block.hash != self.chain[0].hash: + logger.warning("Reorg failed: Genesis hash mismatch.") + return False, [] + else: + if fork_idx > len(self.chain): + logger.warning("Reorg failed: Partial chain does not connect to our history.") + return False, [] + if first_block.previous_hash != self.chain[fork_idx - 1].hash: + logger.warning("Reorg failed: Partial chain hash mismatch at fork point.") + return False, [] + + proposed_chain = self.chain[:fork_idx] + new_chain_list + current_work = self.get_total_work() - new_work = self.get_total_work(new_chain_list) + new_work = self.get_total_work(proposed_chain) if new_work <= current_work: logger.debug("Incoming chain (work: %s) is not heavier than local chain (work: %s). Rejecting.", new_work, current_work) return False, [] - # 1. Verify genesis block matches - if new_chain_list[0].hash != self.chain[0].hash: - logger.warning("Reorg failed: Genesis hash mismatch.") - return False, [] - logger.info("Incoming chain is heavier (%s > %s). Attempting reorg...", new_work, current_work) - # 2. Snapshot current chain in case reorg fails validation original_chain = list(self.chain) - # 3. Rebuild state entirely from genesis using the new chain temp_state = State() temp_state.chain_id = self.chain_id temp_state.restore(self._genesis_state_snapshot) - temp_difficulty = new_chain_list[0].difficulty + temp_difficulty = proposed_chain[0].difficulty temp_avg_block_time = self.target_block_time - # Verify and apply blocks 1 to N through the shared pipeline - for i in range(1, len(new_chain_list)): + for i in range(1, len(proposed_chain)): status, temp_difficulty, temp_avg_block_time = self._apply_block( - new_chain_list[i - 1], new_chain_list[i], temp_state, temp_difficulty, temp_avg_block_time + proposed_chain[i - 1], proposed_chain[i], temp_state, temp_difficulty, temp_avg_block_time ) if status != ValidationStatus.VALID: - logger.warning("Reorg failed at block %s", new_chain_list[i].index) + logger.warning("Reorg failed at block %s", proposed_chain[i].index) return False, [] - # 4. Success! Compute orphaned transactions. old_txs = {tx.tx_id: tx for b in original_chain[1:] for tx in b.transactions} - new_tx_ids = {tx.tx_id for b in new_chain_list[1:] for tx in b.transactions} + new_tx_ids = {tx.tx_id for b in proposed_chain[1:] for tx in b.transactions} orphans = [tx for tx_id, tx in old_txs.items() if tx_id not in new_tx_ids] - self.chain = new_chain_list + self.chain = proposed_chain self.state = temp_state self.current_difficulty = temp_difficulty self.avg_block_time = temp_avg_block_time diff --git a/minichain/contract.py b/minichain/contract.py index 87799a7..9e68e96 100644 --- a/minichain/contract.py +++ b/minichain/contract.py @@ -169,7 +169,7 @@ def execute(self, contract_address, sender_address, payload, amount, gas_limit): args=(code, globals_for_exec, context, queue, gas_limit) ) p.start() - p.join(timeout=10) # 10 second timeout + p.join(timeout=1) # 1 second timeout if p.is_alive(): p.kill() @@ -223,6 +223,9 @@ def _validate_code_ast(self, code): if isinstance(node, ast.JoinedStr): # f-strings logger.warning("Rejected f-string usage.") return False + if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Mult, ast.Pow, ast.MatMult)): + logger.warning("Rejected contract code with potentially unbounded operator (*, **, @).") + return False return True except SyntaxError: return False diff --git a/minichain/mempool.py b/minichain/mempool.py index f42de6c..19b3442 100644 --- a/minichain/mempool.py +++ b/minichain/mempool.py @@ -40,6 +40,13 @@ def add_transaction(self, tx): logger.warning("Mempool: Ignoring older replacement %s", tx.tx_id) return False + # Enforce RBF: new fee must be at least 10% higher + old_fee = getattr(existing_tx, 'max_fee_per_gas', 0) + new_fee = getattr(tx, 'max_fee_per_gas', 0) + if new_fee <= old_fee * 1.1: + logger.warning("Mempool: Replacement fee too low for %s", tx.tx_id) + return False + self._list.pop(existing_idx) if i_max > existing_idx: i_max -= 1 diff --git a/minichain/rpc.py b/minichain/rpc.py index c2d8b2c..fafc2f8 100644 --- a/minichain/rpc.py +++ b/minichain/rpc.py @@ -81,6 +81,9 @@ async def _process_single(self, req): tx = Transaction.from_dict(tx_data) if not tx.verify(): raise ValueError("Invalid signature") + from .validators import ValidationStatus + if self.chain.state.verify_transaction_logic(tx) != ValidationStatus.VALID: + raise ValueError("Transaction failed state validation") if self.mempool.add_transaction(tx): asyncio.create_task(self.network.broadcast_transaction(tx)) result = tx.tx_id diff --git a/minichain/state.py b/minichain/state.py index 5b10f1b..ec5d0f1 100644 --- a/minichain/state.py +++ b/minichain/state.py @@ -215,8 +215,9 @@ def derive_contract_address(self, sender, nonce): return sha256(raw, encoder=HexEncoder).decode()[:40] def create_contract(self, contract_address, code, initial_balance=0): + existing_balance = self.accounts.get(contract_address, {}).get('balance', 0) self.accounts[contract_address] = { - 'balance': initial_balance, + 'balance': existing_balance + initial_balance, 'nonce': 0, 'code': code, 'storage': {} From 505b28180b02f8b6c43ffcbc3c53bca760d2e42d Mon Sep 17 00:00:00 2001 From: siddhant Date: Thu, 9 Jul 2026 16:14:01 +0530 Subject: [PATCH 4/4] Fix contract timeout on Windows and mempool RBF test fee --- minichain/contract.py | 2 +- tests/test_protocol_hardening.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/minichain/contract.py b/minichain/contract.py index 9e68e96..aa2669b 100644 --- a/minichain/contract.py +++ b/minichain/contract.py @@ -169,7 +169,7 @@ def execute(self, contract_address, sender_address, payload, amount, gas_limit): args=(code, globals_for_exec, context, queue, gas_limit) ) p.start() - p.join(timeout=1) # 1 second timeout + p.join(timeout=5) # 5 seconds timeout (increased for Windows compatibility) if p.is_alive(): p.kill() diff --git a/tests/test_protocol_hardening.py b/tests/test_protocol_hardening.py index dcb651f..1fa98b5 100644 --- a/tests/test_protocol_hardening.py +++ b/tests/test_protocol_hardening.py @@ -31,13 +31,14 @@ def setUp(self): self.receiver_pk = SigningKey.generate().verify_key.encode(encoder=HexEncoder).decode() self.state.credit_mining_reward(self.sender_pk, 100) - def _signed_tx(self, nonce, amount=1, timestamp=None) -> Transaction: + def _signed_tx(self, nonce, amount=1, timestamp=None, max_fee_per_gas=0) -> Transaction: tx = Transaction( sender=self.sender_pk, receiver=self.receiver_pk, amount=amount, nonce=nonce, timestamp=timestamp, + max_fee_per_gas=max_fee_per_gas, ) tx.sign(self.sender_sk) return tx @@ -58,8 +59,8 @@ def test_transactions_for_block_are_sorted_and_capped(self): def test_same_nonce_replaces_pending_transaction(self): mempool = Mempool() - original_tx = self._signed_tx(0, amount=1, timestamp=1000) - replacement_tx = self._signed_tx(0, amount=2, timestamp=2000) + original_tx = self._signed_tx(0, amount=1, timestamp=1000, max_fee_per_gas=10) + replacement_tx = self._signed_tx(0, amount=2, timestamp=2000, max_fee_per_gas=15) self.assertTrue(mempool.add_transaction(original_tx)) self.assertTrue(mempool.add_transaction(replacement_tx))