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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,12 @@ TSWLatexianTemp*
#*Notes.bib

# Python
venv/
.venv/
__pycache__/
*.py[cod]
*$py.class
*.so
*bore.zip
*bore_bin
*.egg-info/
4 changes: 2 additions & 2 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import sys
import os
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
107 changes: 83 additions & 24 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@
import re
import sys

from nacl.signing import SigningKey
from nacl.encoding import HexEncoder
from nacl.signing import SigningKey

from minichain import Transaction, Blockchain, Block, State, Mempool, P2PNetwork, mine_block
from minichain import (Block, Blockchain, Mempool, P2PNetwork, State,
Transaction, mine_block)
from minichain.validators import is_valid_receiver


logger = logging.getLogger(__name__)

BURN_ADDRESS = "0" * 40
Expand All @@ -41,6 +41,7 @@
# Wallet helpers
# ──────────────────────────────────────────────


def create_wallet():
sk = SigningKey.generate()
pk = sk.verify_key.encode(encoder=HexEncoder).decode()
Expand All @@ -51,6 +52,7 @@ def create_wallet():
# Block mining
# ──────────────────────────────────────────────


def mine_and_process_block(chain, mempool, miner_pk):
"""Mine pending transactions into a new block."""
pending_txs = mempool.get_transactions_for_block()
Expand Down Expand Up @@ -86,7 +88,11 @@ def mine_and_process_block(chain, mempool, miner_pk):
mined_block = mine_block(block)

if chain.add_block(mined_block):
logger.info("✅ Block #%d mined and added (%d txs)", mined_block.index, len(mineable_txs))
logger.info(
"✅ Block #%d mined and added (%d txs)",
mined_block.index,
len(mineable_txs),
)
mempool.remove_transactions(mineable_txs)
chain.state.credit_mining_reward(miner_pk)
return mined_block
Expand All @@ -96,14 +102,17 @@ def mine_and_process_block(chain, mempool, miner_pk):
for tx in pending_txs:
if mempool.add_transaction(tx):
restored += 1
logger.info("Mempool: Restored %d/%d txs after rejection", restored, len(pending_txs))
logger.info(
"Mempool: Restored %d/%d txs after rejection", restored, len(pending_txs)
)
return None


# ──────────────────────────────────────────────
# Network message handler
# ──────────────────────────────────────────────


def make_network_handler(chain, mempool):
"""Return an async callback that processes incoming P2P messages."""

Expand All @@ -122,24 +131,40 @@ async def handler(data):
return

# Merge remote state into local state (for accounts we don't have yet)
remote_accounts = payload.get("accounts") if isinstance(payload, dict) else None
remote_accounts = (
payload.get("accounts") if isinstance(payload, dict) else None
)
if not isinstance(remote_accounts, dict):
logger.warning("🔒 Rejected sync from %s with invalid accounts payload", peer_addr)
logger.warning(
"🔒 Rejected sync from %s with invalid accounts payload", peer_addr
)
return

for addr, acc in remote_accounts.items():
if not isinstance(acc, dict):
logger.warning("🔒 Skipping malformed account %r from %s", addr, peer_addr)
logger.warning(
"🔒 Skipping malformed account %r from %s", addr, peer_addr
)
continue
if addr not in chain.state.accounts:
chain.state.accounts[addr] = acc
logger.info("🔄 Synced account %s... (balance=%d)", addr[:12], acc.get("balance", 0))
logger.info("🔄 Accepted state sync from %s — %d accounts", peer_addr, len(chain.state.accounts))
logger.info(
"🔄 Synced account %s... (balance=%d)",
addr[:12],
acc.get("balance", 0),
)
logger.info(
"🔄 Accepted state sync from %s — %d accounts",
peer_addr,
len(chain.state.accounts),
)

elif msg_type == "tx":
tx = Transaction.from_dict(payload)
if mempool.add_transaction(tx):
logger.info("📥 Received tx from %s... (amount=%s)", tx.sender[:8], tx.amount)
logger.info(
"📥 Received tx from %s... (amount=%s)", tx.sender[:8], tx.amount
)

elif msg_type == "block":
block = Block.from_dict(payload)
Expand Down Expand Up @@ -204,7 +229,9 @@ async def cli_loop(sk, pk, chain, mempool, network):
print(" (no accounts yet)")
for addr, acc in accounts.items():
tag = " (you)" if addr == pk else ""
print(f" {addr[:12]}... balance={acc['balance']} nonce={acc['nonce']}{tag}")
print(
f" {addr[:12]}... balance={acc['balance']} nonce={acc['nonce']}{tag}"
)

# ── send ──
elif cmd == "send":
Expand Down Expand Up @@ -232,7 +259,9 @@ async def cli_loop(sk, pk, chain, mempool, network):
await network.broadcast_transaction(tx)
print(f" ✅ Tx sent: {amount} coins → {receiver[:12]}...")
else:
print(" ❌ Transaction rejected (invalid sig, duplicate, or mempool full).")
print(
" ❌ Transaction rejected (invalid sig, duplicate, or mempool full)."
)

# ── mine ──
elif cmd == "mine":
Expand Down Expand Up @@ -288,7 +317,10 @@ async def cli_loop(sk, pk, chain, mempool, network):
# Main entry point
# ──────────────────────────────────────────────

async def run_node(port: int, host: str, connect_to: str | None, fund: int, datadir: str | None):

async def run_node(
port: int, host: str, connect_to: str | None, fund: int, datadir: str | None
):
"""Boot the node, optionally connect to a peer, then enter the CLI."""
sk, pk = create_wallet()

Expand All @@ -297,6 +329,7 @@ async def run_node(port: int, host: str, connect_to: str | None, fund: int, data
if datadir and os.path.exists(os.path.join(datadir, "data.json")):
try:
from minichain.persistence import load

chain = load(datadir)
logger.info("Restored chain from '%s'", datadir)
except FileNotFoundError as e:
Expand All @@ -318,10 +351,11 @@ async def run_node(port: int, host: str, connect_to: str | None, fund: int, data
# When a new peer connects, send our state so they can sync
async def on_peer_connected(writer):
import json as _json
sync_msg = _json.dumps({
"type": "sync",
"data": {"accounts": chain.state.accounts}
}) + "\n"

sync_msg = (
_json.dumps({"type": "sync", "data": {"accounts": chain.state.accounts}})
+ "\n"
)
writer.write(sync_msg.encode())
await writer.drain()
logger.info("🔄 Sent state sync to new peer")
Expand Down Expand Up @@ -350,6 +384,7 @@ async def on_peer_connected(writer):
if datadir:
try:
from minichain.persistence import save

save(chain, datadir)
logger.info("Chain saved to '%s'", datadir)
except Exception as e:
Expand All @@ -359,11 +394,33 @@ async def on_peer_connected(writer):

def main():
parser = argparse.ArgumentParser(description="MiniChain Node — Testnet Demo")
parser.add_argument("--host", type=str, default="127.0.0.1", help="Host/IP to bind the P2P server (default: 127.0.0.1)")
parser.add_argument("--port", type=int, default=9000, help="TCP port to listen on (default: 9000)")
parser.add_argument("--connect", type=str, default=None, help="Peer address to connect to (host:port)")
parser.add_argument("--fund", type=int, default=100, help="Initial coins to fund this wallet (default: 100)")
parser.add_argument("--datadir", type=str, default=None, help="Directory to save/load blockchain state (enables persistence)")
parser.add_argument(
"--host",
type=str,
default="127.0.0.1",
help="Host/IP to bind the P2P server (default: 127.0.0.1)",
)
parser.add_argument(
"--port", type=int, default=9000, help="TCP port to listen on (default: 9000)"
)
parser.add_argument(
"--connect",
type=str,
default=None,
help="Peer address to connect to (host:port)",
)
parser.add_argument(
"--fund",
type=int,
default=100,
help="Initial coins to fund this wallet (default: 100)",
)
parser.add_argument(
"--datadir",
type=str,
default=None,
help="Directory to save/load blockchain state (enables persistence)",
)
args = parser.parse_args()

logging.basicConfig(
Expand All @@ -373,7 +430,9 @@ def main():
)

try:
asyncio.run(run_node(args.port, args.host, args.connect, args.fund, args.datadir))
asyncio.run(
run_node(args.port, args.host, args.connect, args.fund, args.datadir)
)
except KeyboardInterrupt:
print("\nNode shut down.")

Expand Down
10 changes: 5 additions & 5 deletions minichain/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from .pow import mine_block, calculate_hash, MiningExceededError
from .block import Block
from .chain import Blockchain
from .transaction import Transaction
from .state import State
from .contract import ContractMachine
from .p2p import P2PNetwork
from .mempool import Mempool
from .persistence import save, load
from .p2p import P2PNetwork
from .persistence import load, save
from .pow import MiningExceededError, calculate_hash, mine_block
from .state import State
from .transaction import Transaction

__all__ = [
"mine_block",
Expand Down
21 changes: 7 additions & 14 deletions minichain/block.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import time
import hashlib
import time
from typing import List, Optional
from .transaction import Transaction

from .serialization import canonical_json_hash
from .transaction import Transaction


def _sha256(data: str) -> str:
return hashlib.sha256(data.encode()).hexdigest()
Expand All @@ -13,10 +15,7 @@ def _calculate_merkle_root(transactions: List[Transaction]) -> Optional[str]:
return None

# Hash each transaction deterministically
tx_hashes = [
tx.tx_id
for tx in transactions
]
tx_hashes = [tx.tx_id for tx in transactions]

# Build Merkle tree
while len(tx_hashes) > 1:
Expand Down Expand Up @@ -48,9 +47,7 @@ def __init__(

# Deterministic timestamp (ms)
self.timestamp: int = (
round(time.time() * 1000)
if timestamp is None
else int(timestamp)
round(time.time() * 1000) if timestamp is None else int(timestamp)
)

self.difficulty: Optional[int] = difficulty
Expand All @@ -77,11 +74,7 @@ def to_header_dict(self):
# BODY (transactions only)
# -------------------------
def to_body_dict(self):
return {
"transactions": [
tx.to_dict() for tx in self.transactions
]
}
return {"transactions": [tx.to_dict() for tx in self.transactions]}

# -------------------------
# FULL BLOCK
Expand Down
Loading
Loading