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
1 change: 1 addition & 0 deletions genesis.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"difficulty": 4,
"target_block_time": 10000,
"alpha": 0.1,
"initial_supply": 1500000000,

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Wire initial_supply into genesis loading or remove it.

minichain/chain.py still initializes balances from alloc only, so this new top-level field has no runtime effect and can mislead operators about the actual genesis issuance.

🤖 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 `@genesis.json` at line 7, The new top-level initial_supply field is not used
by the genesis loading path, so either wire it into the chain initialization
flow or remove it from the genesis schema. Update the genesis handling in
minichain/chain.py, especially the code that currently reads alloc during
startup, so initial supply is actually applied at runtime; otherwise, delete the
unused initial_supply entry to keep the genesis config accurate.

"alloc": {
"0000000000000000000000000000000000000001": {
"balance": 1000000000
Expand Down
106 changes: 60 additions & 46 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

Commands (type in the terminal while the node is running):
balance — show all account balances
send <to> <amount> — send coins to another address
send <to> <amount> - send coins
deploy <file> - deploy a contract
call <addr> <data> - call a contract
mine — mine a block from the mempool
peers — show connected peers
connect <multiaddr> — connect to another node
Expand Down Expand Up @@ -85,23 +87,27 @@ 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):
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)
Expand Down Expand Up @@ -135,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)
Expand All @@ -143,7 +151,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(
Expand Down Expand Up @@ -217,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
Expand All @@ -241,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":
Expand Down Expand Up @@ -273,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:
Expand All @@ -291,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

Expand Down Expand Up @@ -399,39 +417,35 @@ def print_prompt_info(current_pk):
# ── send ──
elif cmd == "send":
if len(parts) < 3:
print(" Usage: send <receiver_address> <amount> [fee]")
print(" Usage: send <receiver_address> <amount> [gas_limit] [max_fee_per_gas]")
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 and fee 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 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, 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(
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).",
)

# ── deploy ──
elif cmd == "deploy":
if len(parts) < 2:
print(" Usage: deploy <filepath> [amount] [fee]")
print(" Usage: deploy <filepath> [amount] [gas_limit] [max_fee_per_gas]")
continue
filepath = parts[1]
try:
Expand All @@ -441,43 +455,43 @@ 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(
mempool, network, tx,
chain, mempool, network, tx,
f" ✅ Deploy Tx sent (nonce={nonce}). Mine a block to confirm.",
" ❌ Deploy Transaction rejected.",
)

# ── call ──
elif cmd == "call":
if len(parts) < 3:
print(" Usage: call <contract_address> <payload> [amount] [fee]")
print(" Usage: call <contract_address> <payload> [amount] [gas_limit] [max_fee_per_gas]")
continue
receiver = parts[1]
if not is_valid_receiver(receiver):
print(" Invalid receiver format. Expected 40 or 64 hex characters.")
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(
mempool, network, tx,
chain, mempool, network, tx,
f" ✅ Call Tx sent to {receiver[:12]}... (payload='{payload}'). Mine a block to confirm.",
" ❌ Call Transaction rejected.",
)
Expand Down
7 changes: 6 additions & 1 deletion minichain/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 33 additions & 18 deletions minichain/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,20 @@ 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:
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
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
Expand Down Expand Up @@ -168,7 +175,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))

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.

Why is "max_fee_per_gas" call "max"? It seems that it is just a "fee_per_gas". Isn't it?

Isn't the user being charged exactly gas_used * fee_per_gas?

if block.miner:
state.credit_mining_reward(block.miner, reward=state.DEFAULT_MINING_REWARD + total_fees)

Expand Down Expand Up @@ -214,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)
"""
Expand All @@ -224,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
Expand Down
5 changes: 4 additions & 1 deletion minichain/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=5) # 5 seconds timeout (increased for Windows compatibility)

if p.is_alive():
p.kill()
Expand Down Expand Up @@ -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
Loading