Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}

Expand Down Expand Up @@ -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)
Expand All @@ -82,6 +86,9 @@ def mine_and_process_block(chain, mempool, miner_pk):
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,
)

Expand Down
72 changes: 48 additions & 24 deletions minichain/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,36 @@
from collections.abc import Sequence

from .transaction import Transaction
from .receipt import Receipt
from .serialization import canonical_json_hash, canonical_json_bytes


def _sha256(data: str) -> str:
return hashlib.sha256(data.encode()).hexdigest()

# <-- Updated to Sequence to accept the frozen tuple
def _calculate_merkle_root(transactions: Sequence[Transaction]) -> Optional[str]:
if not transactions:
def _calculate_merkle_tree(hashes: Sequence[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

hashes_list = list(hashes)
while len(hashes_list) > 1:
if len(hashes_list) % 2 != 0:
hashes_list.append(hashes_list[-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_list), 2):
combined = hashes_list[i] + hashes_list[i + 1]
new_level.append(_sha256(combined))
hashes_list = new_level
return hashes_list[0]

tx_hashes = new_level
# <-- Updated to Sequence to accept the frozen tuple
def _calculate_merkle_root(transactions: Sequence[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: Sequence[Receipt]) -> Optional[str]:
if not receipts:
return None
return _calculate_merkle_tree([canonical_json_hash(r.to_dict()) for r in receipts])

class Block:
def __init__(
Expand All @@ -43,12 +43,16 @@ def __init__(
transactions: Optional[Sequence[Transaction]] = None,
timestamp: Optional[float] = None,
difficulty: Optional[int] = None,
miner: Optional[str] = None
state_root: Optional[str] = None,
receipt_root: Optional[str] = None,
receipts: Optional[Sequence[Receipt]] = None,
miner: Optional[str] = None,
):
self.index = index
self.previous_hash = previous_hash
# Freeze transactions into an immutable tuple to prevent header/body mismatch
self.transactions = tuple(transactions) if transactions else ()
self.receipts = tuple(receipts) if receipts else ()
self.miner = miner
# Deterministic timestamp (ms)
self.timestamp: int = (
Expand All @@ -60,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)
Expand All @@ -74,10 +83,10 @@ 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,
"miner": self.miner,
}
# Include miner in header only when present (optional field) <-- Reworded comment
if self.miner is not None:
Expand All @@ -91,6 +100,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
]
}

Expand All @@ -115,6 +127,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", [])
]

# Safely extract and cast difficulty if it exists
raw_diff = payload.get("difficulty")
Expand All @@ -123,13 +139,15 @@ def from_dict(cls, payload: dict):
# Safely extract and cast timestamp if it exists <-- Added explicit timestamp casting
raw_ts = payload.get("timestamp")
parsed_ts = int(raw_ts) if raw_ts is not None else None

block = cls(
index=int(payload["index"]),
previous_hash=payload["previous_hash"],
transactions=transactions,
timestamp=parsed_ts, # <-- Passed the casted timestamp
difficulty=parsed_diff,
state_root=payload.get("state_root"),
receipt_root=payload.get("receipt_root"),
receipts=receipts,
miner=payload.get("miner"),
)
block.nonce = int(payload.get("nonce", 0))
Expand All @@ -143,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
Expand Down
32 changes: 23 additions & 9 deletions minichain/chain.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -47,18 +47,18 @@ 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
alloc = config.get("alloc", {})
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
Expand All @@ -72,15 +72,17 @@ 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())
config_hash = config.get("hash")

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:
Expand Down Expand Up @@ -111,17 +113,29 @@ 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)

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
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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():
Expand Down
4 changes: 2 additions & 2 deletions minichain/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Loading