from __future__ import annotations import json from typing import Any, Dict, List from sanic import response from app.core.logger import make_log from app.core._utils.b58 import b58decode from app.core.network.dht.records import DHTRecord from app.core.network.dht.store import DHTStore from app.core.network.dht.crypto import compute_node_id from app.core.network.dht.keys import MetaKey, MembershipKey, MetricKey from sqlalchemy import select from app.core.models.my_network import KnownNode def _merge_strategy_for(key: str): # Выбираем правильную стратегию merge по префиксу ключа from app.core.network.dht.replication import ReplicationState from app.core.network.dht.membership import MembershipState from app.core.network.dht.metrics import ContentMetricsState if key.startswith('meta:'): return lambda a, b: ReplicationState.from_dict(a).merge_with(ReplicationState.from_dict(b)).to_dict() if key.startswith('membership:'): # Для membership нужен node_id, но это только для локального состояния; здесь достаточно CRDT-мерджа return lambda a, b: MembershipState.from_dict('remote', None, a).merge(MembershipState.from_dict('remote', None, b)).to_dict() if key.startswith('metric:'): return lambda a, b: ContentMetricsState.from_dict('remote', a).merge(ContentMetricsState.from_dict('remote', b)).to_dict() return lambda a, b: b async def s_api_v1_dht_get(request): """Возвращает запись DHT по fingerprint или key.""" store: DHTStore = request.app.ctx.memory.dht_store fp = request.args.get('fingerprint') key = request.args.get('key') if fp: rec = store.get(fp) if not rec: return response.json({'error': 'NOT_FOUND'}, status=404) return response.json({**rec.to_payload(), 'signature': rec.signature}) if key: snap = store.snapshot() for _fp, payload in snap.items(): if payload.get('key') == key: return response.json(payload) return response.json({'error': 'NOT_FOUND'}, status=404) return response.json({'error': 'BAD_REQUEST'}, status=400) def _verify_publisher(node_id: str, public_key_b58: str) -> bool: try: derived = compute_node_id(b58decode(public_key_b58)) return derived == node_id except Exception: return False async def s_api_v1_dht_put(request): """Принимает запись(и) DHT, проверяет подпись и выполняет merge/persist. Поддерживает одиночную запись (record: {...}) и пакет (records: [{...}]). Требует поле public_key отправителя и соответствие node_id. """ mem = request.app.ctx.memory store: DHTStore = mem.dht_store data = request.json or {} public_key = data.get('public_key') if not public_key: return response.json({'error': 'MISSING_PUBLIC_KEY'}, status=400) # Determine publisher role (trusted/read-only/deny) role = None try: session = request.ctx.db_session kn = (await session.execute(select(KnownNode).where(KnownNode.public_key == public_key))).scalars().first() role = (kn.meta or {}).get('role') if kn and kn.meta else None except Exception: role = None def _process_one(payload: Dict[str, Any]) -> Dict[str, Any]: try: rec = DHTRecord.create( key=payload['key'], fingerprint=payload['fingerprint'], value=payload['value'], node_id=payload['node_id'], logical_counter=int(payload['logical_counter']), signature=payload.get('signature'), timestamp=float(payload.get('timestamp') or 0), ) except Exception as e: return {'error': f'BAD_RECORD: {e}'} if not _verify_publisher(rec.node_id, public_key): return {'error': 'NODE_ID_MISMATCH'} # Подтверждение подписи записи if not rec.verify(public_key): return {'error': 'BAD_SIGNATURE'} # Enforce ACL: untrusted nodes may not mutate meta/metric records if role != 'trusted': if rec.key.startswith('meta:') or rec.key.startswith('metric:'): return {'error': 'FORBIDDEN_NOT_TRUSTED'} merge_fn = _merge_strategy_for(rec.key) try: merged = store.merge_record(rec, merge_fn) return {'ok': True, 'fingerprint': merged.fingerprint} except Exception as e: make_log('DHT.put', f'merge failed: {e}', level='warning') return {'error': 'MERGE_FAILED'} if 'record' in data: result = _process_one(data['record']) status = 200 if 'ok' in result else 400 return response.json(result, status=status) elif 'records' in data and isinstance(data['records'], list): results: List[Dict[str, Any]] = [] ok = True for item in data['records']: res = _process_one(item) if 'error' in res: ok = False results.append(res) return response.json({'ok': ok, 'results': results}, status=200 if ok else 207) return response.json({'error': 'BAD_REQUEST'}, status=400)