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
24 changes: 18 additions & 6 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 @@ -76,12 +80,16 @@ def mine_and_process_block(chain, mempool, miner_pk):
logger.info("No mineable transactions in current queue window.")
return None

temp_state.credit_mining_reward(miner_pk)
total_fees = sum(getattr(tx, 'fee', 0) for tx in mineable_txs)
temp_state.credit_mining_reward(miner_pk, reward=temp_state.DEFAULT_MINING_REWARD + total_fees)

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,
)

Expand Down Expand Up @@ -206,23 +214,27 @@ async def cli_loop(sk, pk, chain, mempool, network):
# ── send ──
elif cmd == "send":
if len(parts) < 3:
print(" Usage: send <receiver_address> <amount>")
print(" Usage: send <receiver_address> <amount> [fee]")
continue
receiver = parts[1]
if not is_valid_receiver(receiver):
print(" Invalid receiver format. Expected 40 or 64 hex characters.")
continue
try:
amount = int(parts[2])
fee = int(parts[3]) if len(parts) > 3 else 0
except ValueError:
print(" Amount must be an integer.")
print(" Amount and fee must be integers.")
continue
if amount <= 0:
print(" Amount must be greater than 0.")
continue
if fee < 0:
print(" Fee cannot be negative.")
continue

nonce = chain.state.get_account(pk).get("nonce", 0)
tx = Transaction(sender=pk, receiver=receiver, amount=amount, nonce=nonce)
tx = Transaction(sender=pk, receiver=receiver, amount=amount, nonce=nonce, fee=fee)
tx.sign(sk)

if mempool.add_transaction(tx):
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Duplicate assignment of self.miner.

self.miner is assigned twice: once at line 56 and again at line 68. Remove one of them.

🧹 Proposed fix
         self.transactions = tuple(transactions) if transactions else ()
         self.receipts = tuple(receipts) if receipts else ()
-        self.miner = miner
         # Deterministic timestamp (ms)
         self.timestamp: int = (
             round(time.time() * 1000)

Also applies to: 68-68

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@minichain/block.py` at line 56, In the Block class constructor remove the
duplicate assignment of self.miner so the field is only set once; locate the
__init__ (or constructor) where self.miner is assigned twice and delete one of
the assignments (keep the one that is in the correct logical place for
initialization), ensuring no other initialization logic depends on the removed
line and that the remaining assignment is the authoritative value for the miner
attribute.

# 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
35 changes: 25 additions & 10 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,30 @@ 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)

total_fees = sum(getattr(tx, 'fee', 0) for tx in block.transactions)
if block.miner:
temp_state.credit_mining_reward(block.miner)
temp_state.credit_mining_reward(block.miner, reward=temp_state.DEFAULT_MINING_REWARD + total_fees)

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

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
4 changes: 3 additions & 1 deletion minichain/mempool.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ def get_transactions_for_block(self):

for sender, txs in snapshot.items():
if txs:
if best_tx is None or (txs[0].timestamp, sender, txs[0].nonce) < (best_tx.timestamp, best_sender, best_tx.nonce):
current_criteria = (-getattr(txs[0], 'fee', 0), txs[0].timestamp, sender, txs[0].nonce)
best_criteria = (-getattr(best_tx, 'fee', 0), best_tx.timestamp, best_sender, best_tx.nonce) if best_tx else None
if best_tx is None or current_criteria < best_criteria:
best_tx = txs[0]
best_sender = sender

Expand Down
Loading