397 lines
15 KiB
Python
397 lines
15 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
presence_proof.py — Цепочка доказательств присутствия
|
||
Montana Protocol v3.2
|
||
|
||
ACP (Adaptive Cognitive Presence) — консенсус Montana.
|
||
|
||
Каждый τ₁ узел подписывает:
|
||
MONTANA_PRESENCE_V1:{timestamp}:{prev_hash}:{pubkey}:{t2_index}
|
||
|
||
Доказательства образуют свою цепочку.
|
||
Сеть принимает только 1 подпись на τ₁.
|
||
Невозможно ускорить время.
|
||
|
||
Post-quantum security: ML-DSA-65 (FIPS 204) signatures.
|
||
"""
|
||
|
||
import hashlib
|
||
import sqlite3
|
||
import time
|
||
import threading
|
||
import logging
|
||
from dataclasses import dataclass
|
||
from typing import List, Optional, Tuple, Dict
|
||
|
||
from node_crypto import sign_message, verify_signature
|
||
|
||
logger = logging.getLogger("presence_proof")
|
||
|
||
GENESIS_HASH = "0" * 64
|
||
PRESENCE_VERSION = "MONTANA_PRESENCE_V1"
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# PRESENCE PROOF
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
@dataclass
|
||
class PresenceProof:
|
||
"""
|
||
Доказательство присутствия узла.
|
||
|
||
Формат сообщения:
|
||
MONTANA_PRESENCE_V1:{timestamp}:{prev_hash}:{pubkey}:{t2_index}
|
||
|
||
Хеш доказательства:
|
||
SHA256(message || signature)
|
||
|
||
Цепочка: genesis (prev_hash = 64 нуля) → proof#1 → proof#2 → …
|
||
"""
|
||
message: str # MONTANA_PRESENCE_V1:...
|
||
signature: str # ML-DSA-65 подпись message
|
||
proof_hash: str # SHA256(message || signature)
|
||
prev_proof_hash: str # Хеш предыдущего proof (genesis = GENESIS_HASH)
|
||
timestamp: int # Наносекунды UTC
|
||
pubkey: str # Публичный ключ узла (hex)
|
||
t2_index: int # Номер текущего τ₂ окна
|
||
proof_number: int # Последовательный номер
|
||
|
||
def to_dict(self) -> Dict:
|
||
return {
|
||
"message": self.message,
|
||
"signature": self.signature,
|
||
"proof_hash": self.proof_hash,
|
||
"prev_proof_hash": self.prev_proof_hash,
|
||
"timestamp": self.timestamp,
|
||
"pubkey": self.pubkey,
|
||
"t2_index": self.t2_index,
|
||
"proof_number": self.proof_number,
|
||
}
|
||
|
||
@classmethod
|
||
def from_dict(cls, d: Dict) -> "PresenceProof":
|
||
return cls(
|
||
message=d["message"],
|
||
signature=d["signature"],
|
||
proof_hash=d["proof_hash"],
|
||
prev_proof_hash=d["prev_proof_hash"],
|
||
timestamp=d["timestamp"],
|
||
pubkey=d["pubkey"],
|
||
t2_index=d["t2_index"],
|
||
proof_number=d["proof_number"],
|
||
)
|
||
|
||
|
||
def compute_proof_hash(message: str, signature: str) -> str:
|
||
"""SHA256(message || signature) — хеш доказательства"""
|
||
combined = message + signature
|
||
return hashlib.sha256(combined.encode("utf-8")).hexdigest()
|
||
|
||
|
||
def format_presence_message(
|
||
timestamp: int, prev_hash: str, pubkey: str, t2_index: int,
|
||
proof_number: int = -1,
|
||
) -> str:
|
||
"""
|
||
Форматирует сообщение для подписи.
|
||
|
||
ANTI-REPLAY: proof_number включается в сообщение.
|
||
Если proof_number == -1, используется legacy формат (без номера).
|
||
"""
|
||
if proof_number >= 0:
|
||
return f"{PRESENCE_VERSION}:{timestamp}:{prev_hash}:{pubkey}:{t2_index}:{proof_number}"
|
||
return f"{PRESENCE_VERSION}:{timestamp}:{prev_hash}:{pubkey}:{t2_index}"
|
||
|
||
|
||
def parse_presence_message(message: str) -> Optional[Dict]:
|
||
"""
|
||
Парсит сообщение presence proof.
|
||
|
||
Поддерживает оба формата:
|
||
- V1 (5 полей): version:timestamp:prev_hash:pubkey:t2_index
|
||
- V1+ (6 полей): version:timestamp:prev_hash:pubkey:t2_index:proof_number
|
||
|
||
Returns:
|
||
{"version": str, "timestamp": int, "prev_hash": str, "pubkey": str,
|
||
"t2_index": int, "proof_number": int}
|
||
или None при ошибке формата
|
||
"""
|
||
parts = message.split(":")
|
||
if len(parts) not in (5, 6):
|
||
return None
|
||
if parts[0] != PRESENCE_VERSION:
|
||
return None
|
||
try:
|
||
result = {
|
||
"version": parts[0],
|
||
"timestamp": int(parts[1]),
|
||
"prev_hash": parts[2],
|
||
"pubkey": parts[3],
|
||
"t2_index": int(parts[4]),
|
||
"proof_number": -1,
|
||
}
|
||
if len(parts) == 6:
|
||
result["proof_number"] = int(parts[5])
|
||
return result
|
||
except (ValueError, IndexError):
|
||
return None
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# PRESENCE CHAIN
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
class PresenceChain:
|
||
"""
|
||
Цепочка доказательств присутствия узла.
|
||
|
||
Каждый τ₁ узел создаёт одно доказательство.
|
||
Цепочка верифицируема: формат, подписи, prev_hash, timestamps.
|
||
"""
|
||
|
||
def __init__(self, node_id: str, private_key: str, pubkey: str, db_path: str):
|
||
"""
|
||
Args:
|
||
node_id: mt... адрес узла
|
||
private_key: hex приватный ключ ML-DSA-65
|
||
pubkey: hex публичный ключ ML-DSA-65
|
||
db_path: путь к SQLite
|
||
"""
|
||
self.node_id = node_id
|
||
self.private_key = private_key
|
||
self.pubkey = pubkey
|
||
self.db_path = db_path
|
||
self._lock = threading.Lock()
|
||
self._init_db()
|
||
|
||
# Загружаем последний proof
|
||
self.last_proof_hash = self._load_last_hash()
|
||
self.proof_count = self._load_count()
|
||
self.last_timestamp = self._load_last_timestamp()
|
||
|
||
def _init_db(self):
|
||
with sqlite3.connect(self.db_path) as conn:
|
||
# IMMUTABILITY: WAL mode + crash protection
|
||
conn.execute("PRAGMA journal_mode=WAL")
|
||
conn.execute("PRAGMA synchronous=FULL")
|
||
|
||
conn.execute("""
|
||
CREATE TABLE IF NOT EXISTS presence_proofs (
|
||
proof_number INTEGER PRIMARY KEY,
|
||
message TEXT NOT NULL,
|
||
signature TEXT NOT NULL,
|
||
proof_hash TEXT UNIQUE NOT NULL,
|
||
prev_proof_hash TEXT NOT NULL,
|
||
pubkey TEXT NOT NULL,
|
||
timestamp INTEGER NOT NULL,
|
||
t2_index INTEGER NOT NULL
|
||
)
|
||
""")
|
||
conn.execute("""
|
||
CREATE INDEX IF NOT EXISTS idx_proof_hash
|
||
ON presence_proofs(proof_hash)
|
||
""")
|
||
conn.commit()
|
||
|
||
def _conn(self) -> sqlite3.Connection:
|
||
conn = sqlite3.connect(self.db_path)
|
||
conn.row_factory = sqlite3.Row
|
||
return conn
|
||
|
||
def _load_last_hash(self) -> str:
|
||
with self._conn() as conn:
|
||
row = conn.execute(
|
||
"SELECT proof_hash FROM presence_proofs ORDER BY proof_number DESC LIMIT 1"
|
||
).fetchone()
|
||
return row["proof_hash"] if row else GENESIS_HASH
|
||
|
||
def _load_count(self) -> int:
|
||
with self._conn() as conn:
|
||
row = conn.execute(
|
||
"SELECT COUNT(*) as cnt FROM presence_proofs"
|
||
).fetchone()
|
||
return row["cnt"]
|
||
|
||
def _load_last_timestamp(self) -> int:
|
||
with self._conn() as conn:
|
||
row = conn.execute(
|
||
"SELECT timestamp FROM presence_proofs ORDER BY proof_number DESC LIMIT 1"
|
||
).fetchone()
|
||
return row["timestamp"] if row else 0
|
||
|
||
# ─── Creation ─────────────────────────────────────────────────────────────
|
||
|
||
def create_proof(self, t2_index: int) -> PresenceProof:
|
||
"""
|
||
Создать новое доказательство присутствия.
|
||
|
||
Вызывается каждый τ₁ (1 минута).
|
||
Подписывает ML-DSA-65, связывает с предыдущим proof.
|
||
|
||
Args:
|
||
t2_index: номер текущего τ₂ окна
|
||
|
||
Returns:
|
||
PresenceProof
|
||
"""
|
||
ts = time.time_ns()
|
||
|
||
# Формируем сообщение (с proof_number для anti-replay)
|
||
message = format_presence_message(
|
||
ts, self.last_proof_hash, self.pubkey, t2_index,
|
||
proof_number=self.proof_count,
|
||
)
|
||
|
||
# Подписываем ML-DSA-65
|
||
signature = sign_message(self.private_key, message)
|
||
|
||
# Хеш доказательства
|
||
proof_hash = compute_proof_hash(message, signature)
|
||
|
||
proof = PresenceProof(
|
||
message=message,
|
||
signature=signature,
|
||
proof_hash=proof_hash,
|
||
prev_proof_hash=self.last_proof_hash,
|
||
timestamp=ts,
|
||
pubkey=self.pubkey,
|
||
t2_index=t2_index,
|
||
proof_number=self.proof_count,
|
||
)
|
||
|
||
# Сохраняем
|
||
self._save_proof(proof)
|
||
|
||
return proof
|
||
|
||
def _save_proof(self, proof: PresenceProof):
|
||
with self._lock:
|
||
with self._conn() as conn:
|
||
conn.execute(
|
||
"""INSERT INTO presence_proofs
|
||
(proof_number, message, signature, proof_hash,
|
||
prev_proof_hash, pubkey, timestamp, t2_index)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
||
(
|
||
proof.proof_number,
|
||
proof.message,
|
||
proof.signature,
|
||
proof.proof_hash,
|
||
proof.prev_proof_hash,
|
||
proof.pubkey,
|
||
proof.timestamp,
|
||
proof.t2_index,
|
||
),
|
||
)
|
||
conn.commit()
|
||
|
||
self.last_proof_hash = proof.proof_hash
|
||
self.proof_count = proof.proof_number + 1
|
||
self.last_timestamp = proof.timestamp
|
||
|
||
# ─── Verification ─────────────────────────────────────────────────────────
|
||
|
||
def verify_chain(self) -> Tuple[bool, str]:
|
||
"""
|
||
Полная верификация цепочки presence proofs.
|
||
|
||
Проверки:
|
||
1. Формат MONTANA_PRESENCE_V1:... корректен
|
||
2. ML-DSA-65 подпись валидна
|
||
3. Временные метки строго возрастают
|
||
4. prev_hash совпадает с proof_hash предыдущего
|
||
5. proof_hash = SHA256(message || signature)
|
||
"""
|
||
count = self._load_count()
|
||
if count == 0:
|
||
return True, "Empty chain"
|
||
|
||
prev_hash = GENESIS_HASH
|
||
prev_timestamp = 0
|
||
|
||
with self._conn() as conn:
|
||
rows = conn.execute(
|
||
"SELECT * FROM presence_proofs ORDER BY proof_number"
|
||
).fetchall()
|
||
|
||
for row in rows:
|
||
proof = PresenceProof(
|
||
message=row["message"],
|
||
signature=row["signature"],
|
||
proof_hash=row["proof_hash"],
|
||
prev_proof_hash=row["prev_proof_hash"],
|
||
timestamp=row["timestamp"],
|
||
pubkey=row["pubkey"],
|
||
t2_index=row["t2_index"],
|
||
proof_number=row["proof_number"],
|
||
)
|
||
|
||
# 1. Формат
|
||
parsed = parse_presence_message(proof.message)
|
||
if parsed is None:
|
||
return False, f"Proof #{proof.proof_number}: invalid message format"
|
||
if parsed["version"] != PRESENCE_VERSION:
|
||
return False, f"Proof #{proof.proof_number}: wrong version {parsed['version']}"
|
||
|
||
# 2. ML-DSA-65 подпись
|
||
if not verify_signature(proof.pubkey, proof.message, proof.signature):
|
||
return False, f"Proof #{proof.proof_number}: invalid ML-DSA-65 signature"
|
||
|
||
# 3. Timestamps строго возрастают
|
||
if proof.timestamp <= prev_timestamp and proof.proof_number > 0:
|
||
return False, (
|
||
f"Proof #{proof.proof_number}: timestamp not increasing "
|
||
f"({proof.timestamp} <= {prev_timestamp})"
|
||
)
|
||
|
||
# 4. prev_hash совпадает
|
||
if proof.prev_proof_hash != prev_hash:
|
||
return False, (
|
||
f"Proof #{proof.proof_number}: prev_hash mismatch "
|
||
f"(expected {prev_hash[:16]}..., got {proof.prev_proof_hash[:16]}...)"
|
||
)
|
||
|
||
# 5. proof_hash = SHA256(message || signature)
|
||
expected_hash = compute_proof_hash(proof.message, proof.signature)
|
||
if proof.proof_hash != expected_hash:
|
||
return False, (
|
||
f"Proof #{proof.proof_number}: proof_hash mismatch "
|
||
f"(expected {expected_hash[:16]}..., got {proof.proof_hash[:16]}...)"
|
||
)
|
||
|
||
prev_hash = proof.proof_hash
|
||
prev_timestamp = proof.timestamp
|
||
|
||
return True, f"OK ({count} proofs verified)"
|
||
|
||
# ─── Query ────────────────────────────────────────────────────────────────
|
||
|
||
def get_proof(self, proof_number: int) -> Optional[PresenceProof]:
|
||
with self._conn() as conn:
|
||
row = conn.execute(
|
||
"SELECT * FROM presence_proofs WHERE proof_number = ?",
|
||
(proof_number,),
|
||
).fetchone()
|
||
if row:
|
||
return PresenceProof.from_dict(dict(row))
|
||
return None
|
||
|
||
def get_proofs_for_t2(self, t2_index: int) -> List[PresenceProof]:
|
||
"""Все proofs для данного τ₂ окна"""
|
||
with self._conn() as conn:
|
||
rows = conn.execute(
|
||
"SELECT * FROM presence_proofs WHERE t2_index = ? ORDER BY proof_number",
|
||
(t2_index,),
|
||
).fetchall()
|
||
return [PresenceProof.from_dict(dict(r)) for r in rows]
|
||
|
||
def get_stats(self) -> Dict:
|
||
return {
|
||
"proof_count": self.proof_count,
|
||
"last_proof_hash": self.last_proof_hash[:16] + "..." if self.last_proof_hash != GENESIS_HASH else "genesis",
|
||
"last_timestamp": self.last_timestamp,
|
||
}
|