montana/Русский/Бот/presence_proof.py

397 lines
15 KiB
Python
Raw Normal View History

#!/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,
}