Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 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
2 changes: 1 addition & 1 deletion minichain/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def from_dict(cls, payload: dict):

# Verify the block hash
expected_hash = block.compute_hash()
if block.hash is not None and block.hash != expected_hash:
if block.hash != expected_hash:
raise ValueError("block hash does not match header")

# Recalculate and verify the Merkle root!
Expand Down
57 changes: 41 additions & 16 deletions minichain/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@
logger = logging.getLogger(__name__)


def validate_block_link_and_hash(previous_block, block):
class InvalidProofOfWorkError(ValueError):
pass


def validate_difficulty(difficulty, max_difficulty):
Comment thread
Jiyacodex marked this conversation as resolved.
if type(difficulty) is not int or difficulty < 1 or difficulty > max_difficulty:
raise ValueError(f"invalid difficulty {difficulty}")

def validate_block(previous_block, block, expected_difficulty):
if block.previous_hash != previous_block.hash:
raise ValueError(
f"invalid previous hash {block.previous_hash} != {previous_block.hash}"
Expand All @@ -26,17 +34,26 @@ def validate_block_link_and_hash(previous_block, block):
if block.hash != expected_hash:
raise ValueError(f"invalid hash {block.hash}")

target = "0" * (block.difficulty or 1)
if not block.hash.startswith(target):
raise ValueError(f"invalid Proof of Work: hash {block.hash} does not satisfy difficulty {block.difficulty}")
if block.difficulty != expected_difficulty:
raise ValueError(
f"invalid difficulty {block.difficulty} != expected {expected_difficulty}"
)

Comment thread
Jiyacodex marked this conversation as resolved.
if not block.hash.startswith("0" * expected_difficulty):
raise InvalidProofOfWorkError(
f"invalid PoW: hash {block.hash} does not satisfy difficulty {expected_difficulty}"
)

if block.timestamp <= previous_block.timestamp:
raise ValueError(f"invalid timestamp: {block.timestamp} is not strictly greater than previous block timestamp {previous_block.timestamp}")
raise ValueError(
f"invalid timestamp: {block.timestamp} is not strictly greater than previous block timestamp {previous_block.timestamp}"
)

max_allowed_time = int(time.time() * 1000) + 15000
if block.timestamp > max_allowed_time:
raise ValueError(f"invalid timestamp: {block.timestamp} is too far in the future (max allowed: {max_allowed_time})")

raise ValueError(
f"invalid timestamp: {block.timestamp} is too far in the future (max allowed: {max_allowed_time})"
)

class Blockchain:
"""
Expand Down Expand Up @@ -130,7 +147,13 @@ def get_total_work(self, chain_list=None):
if chain_list is None:
with self._lock:
chain_list = self.chain
return sum(2 ** (block.difficulty or 1) for block in chain_list)

for block in chain_list:
if not isinstance(block.hash, str):
raise ValueError(f"invalid or missing hash on block {block.index}")
validate_difficulty(block.difficulty, len(block.hash))

return sum(2 ** block.difficulty for block in chain_list)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def _next_difficulty(self, difficulty, avg_block_time):
"""Advance the EMA difficulty control after a block, returning the new value."""
Expand All @@ -150,16 +173,15 @@ def _apply_block(self, prev_block, block, state, difficulty, avg_block_time):
from .validators import ValidationStatus

try:
validate_block_link_and_hash(prev_block, block)
validate_block(prev_block, block, difficulty)
except InvalidProofOfWorkError as exc:
logger.warning("Block %s rejected: %s", block.index, exc)
return ValidationStatus.INVALID, difficulty, avg_block_time
except ValueError as exc:
logger.warning("Block %s rejected: %s", block.index, exc)
status = ValidationStatus.INVALID if "hash" in str(exc) else ValidationStatus.FAILED
return status, difficulty, avg_block_time

if block.difficulty != difficulty:
logger.warning("Block %s rejected: Invalid difficulty. Expected %s, got %s", block.index, difficulty, block.difficulty)
return ValidationStatus.INVALID, difficulty, avg_block_time

receipts = []
for tx in block.transactions:
status, receipt = state.validate_and_apply_with_status(tx)
Expand Down Expand Up @@ -224,9 +246,12 @@ def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]:
return False, []

with self._lock:
current_work = self.get_total_work()
new_work = self.get_total_work(new_chain_list)

try:
current_work = self.get_total_work()
new_work = self.get_total_work(new_chain_list)
except ValueError as exc:
logger.warning("Reorg rejected: %s", exc)
return False, []
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, []
Expand Down
79 changes: 46 additions & 33 deletions minichain/persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from typing import Any

from .block import Block
from .chain import Blockchain, validate_block_link_and_hash
from .chain import Blockchain

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -64,7 +64,13 @@ def save(blockchain: Blockchain, path: str = ".") -> None:


def load(path: str = ".") -> Blockchain:
"""Restore a Blockchain from SQLite inside *path* (with legacy JSON fallback)."""
"""Restore a Blockchain from SQLite inside *path* (with legacy JSON fallback).

Blocks are replayed through the canonical ``Blockchain._apply_block()``
pipeline so that difficulty adjustment, PoW, receipt validation, state-root
validation and transaction validation are all performed by the same code
path used at runtime. No parallel validation logic is maintained here.
"""
db_path = os.path.join(path, _DB_FILE)
legacy_path = os.path.join(path, _LEGACY_DATA_FILE)

Expand All @@ -90,6 +96,9 @@ def load(path: str = ".") -> Blockchain:
raise ValueError(f"Invalid or empty chain data in '{path}'")
if not isinstance(raw_accounts, dict):
raise ValueError(f"Invalid accounts data in '{path}'")
for address, account in raw_accounts.items():
if not isinstance(address, str) or not isinstance(account, dict):
raise ValueError(f"Invalid accounts data in '{path}'")

blocks = []
for raw_block in raw_blocks:
Expand All @@ -100,17 +109,44 @@ def load(path: str = ".") -> Blockchain:
except (KeyError, TypeError, ValueError) as exc:
raise ValueError(f"Invalid chain data in '{path}'") from exc

normalized_accounts = {}
for address, account in raw_accounts.items():
if not isinstance(address, str) or not isinstance(account, dict):
raise ValueError(f"Invalid accounts data in '{path}'")
normalized_accounts[address] = account
blockchain = Blockchain()

from .pow import calculate_hash
genesis = blocks[0]
if genesis.index != 0:
raise ValueError("Invalid genesis block")
if genesis.hash != calculate_hash(genesis.to_header_dict()):
raise ValueError("Invalid genesis block hash")
if genesis.hash != blockchain.chain[0].hash:
raise ValueError(
f"Persisted genesis hash {genesis.hash!r} does not match "
f"local genesis {blockchain.chain[0].hash!r}"
)

_verify_chain_integrity(blocks)
from .validators import ValidationStatus
from .state import State

temp_state = State()
temp_state.chain_id = blockchain.chain_id
temp_state.restore(blockchain._genesis_state_snapshot)

temp_difficulty = blocks[0].difficulty
temp_avg_block_time = blockchain.target_block_time

for i in range(1, len(blocks)):
status, temp_difficulty, temp_avg_block_time = blockchain._apply_block(
blocks[i - 1], blocks[i], temp_state, temp_difficulty, temp_avg_block_time
)
if status != ValidationStatus.VALID:
raise ValueError(
f"Block #{blocks[i].index} failed validation during load "
f"(status={status.name})"
)

blockchain = Blockchain()
blockchain.chain = blocks
blockchain.state.accounts = normalized_accounts
blockchain.state = temp_state
blockchain.current_difficulty = temp_difficulty
blockchain.avg_block_time = temp_avg_block_time

logger.info(
"Loaded %d blocks and %d accounts from '%s'",
Expand All @@ -121,29 +157,6 @@ def load(path: str = ".") -> Blockchain:
return blockchain


# ---------------------------------------------------------------------------
# Integrity verification
# ---------------------------------------------------------------------------


def _verify_chain_integrity(blocks: list[Block]) -> None:
"""Verify genesis, hash linkage, and block hashes."""
genesis = blocks[0]
if genesis.index != 0:
raise ValueError("Invalid genesis block")
from .pow import calculate_hash
if genesis.hash != calculate_hash(genesis.to_header_dict()):
raise ValueError("Invalid genesis block hash")

for i in range(1, len(blocks)):
block = blocks[i]
prev = blocks[i - 1]
try:
validate_block_link_and_hash(prev, block)
except ValueError as exc:
raise ValueError(f"Block #{block.index}: {exc}") from exc


# ---------------------------------------------------------------------------
# SQLite helpers
# ---------------------------------------------------------------------------
Expand Down
Loading