fieldwitness/frontends/web/blueprints/federation.py
Aaron D. Lee 2a6900abed
Some checks failed
CI / lint (push) Failing after 1m3s
CI / typecheck (push) Failing after 32s
Implement live gossip federation server (5 phases)
Phase 1: RFC 6962 consistency proofs in merkle.py
- Implemented _build_consistency_proof() with recursive subtree
  decomposition algorithm following RFC 6962 Section 2.1.2
- Added _subproof() recursive helper and _compute_root_of()
- Added standalone verify_consistency_proof() function

Phase 2: Federation API endpoints on FastAPI server
- GET /federation/status — merkle root + log size for gossip probes
- GET /federation/records?start=N&count=M — record fetch (cap 100)
- GET /federation/consistency-proof?old_size=N — Merkle proof
- POST /federation/records — accept records with trust filtering
  and SHA-256 deduplication
- Cached storage singleton for concurrent safety
- Added FEDERATION_DIR to paths.py

Phase 3: HttpTransport implementation
- Replaced stub with real aiohttp client (lazy import for optional dep)
- Reusable ClientSession with configurable timeout
- All 4 PeerTransport methods: get_status, get_records,
  get_consistency_proof, push_records
- FederationError wrapping for all network failures
- Added record_filter callback to GossipNode for trust-store filtering

Phase 4: Peer persistence (SQLite)
- New peer_store.py: SQLite-backed peer database + sync history
- Tables: peers (url, fingerprint, health, last_seen) and
  sync_history (timestamp, records_received, success/error)
- PeerStore follows dropbox.py SQLite pattern

Phase 5: CLI commands + Web UI dashboard
- CLI: federation status, peer-add, peer-remove, peer-list,
  sync-now (asyncio), history
- Flask blueprint at /federation/ with peer table, sync history,
  add/remove peer forms, local node info cards
- CSRF tokens on all forms

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 22:20:53 -04:00

79 lines
2.2 KiB
Python

"""
Federation blueprint — peer status dashboard and management.
"""
from auth import admin_required, login_required
from flask import Blueprint, flash, redirect, render_template, request, url_for
bp = Blueprint("federation", __name__, url_prefix="/federation")
@bp.route("/")
@login_required
def status():
"""Federation status dashboard."""
from soosef.verisoo.peer_store import PeerStore
store = PeerStore()
peers = store.list_peers()
history = store.get_sync_history(limit=20)
# Get local node info
node_info = {"root": None, "size": 0}
try:
from soosef.verisoo.storage import LocalStorage
import soosef.paths as _paths
storage = LocalStorage(_paths.ATTESTATIONS_DIR)
stats = storage.get_stats()
merkle_log = storage.load_merkle_log()
node_info = {
"root": merkle_log.root_hash[:16] + "..." if merkle_log.root_hash else "empty",
"size": merkle_log.size,
"record_count": stats.record_count,
}
except Exception:
pass
return render_template(
"federation/status.html",
peers=peers,
history=history,
node_info=node_info,
)
@bp.route("/peer/add", methods=["POST"])
@admin_required
def peer_add():
"""Add a federation peer."""
from soosef.verisoo.peer_store import PeerStore
url = request.form.get("url", "").strip()
fingerprint = request.form.get("fingerprint", "").strip()
if not url or not fingerprint:
flash("URL and fingerprint are required.", "error")
return redirect(url_for("federation.status"))
store = PeerStore()
store.add_peer(url, fingerprint)
flash(f"Peer added: {url}", "success")
return redirect(url_for("federation.status"))
@bp.route("/peer/remove", methods=["POST"])
@admin_required
def peer_remove():
"""Remove a federation peer."""
from soosef.verisoo.peer_store import PeerStore
url = request.form.get("url", "").strip()
store = PeerStore()
if store.remove_peer(url):
flash(f"Peer removed: {url}", "success")
else:
flash(f"Peer not found: {url}", "error")
return redirect(url_for("federation.status"))