Resilienz und Selbstheilung

Ein dezentrales Netzwerk ist nur so robust wie seine Fähigkeit, sich von Teilausfällen zu erholen. Server gehen offline. Neue Server treten bei und müssen ihre Daten bootstrappen. Netzwerk-Partitionen verursachen Divergenz. Hashiverse adressiert das durch Selbstheilungs­protokolle, die ohne zentralen Koordinator arbeiten — getrieben von Clients und Servern, die Inkonsistenz erkennen und beheben.

Sharding und Lastverteilung

Soziale Medien folgen Potenzgesetzen. Eine kleine Anzahl Nutzer und Hashtags zieht den weitaus größten Teil der Follower an und erzeugt die Mehrheit der Beiträge. In einer naiven DHT würde das bedeuten, dass eine kleine Anzahl Knoten die Mehrheit der Last trägt — und damit zu Performance-Engpässen und hochwertigen DDoS-Zielen aufgrund der Inhalte wird, die sie hosten. Hashiverse adressiert das auf Datenmodell­ebene über zeit- und frequenzbasiertes Sharding.

Bucket-Schlüssel auf Epoch-Basis

Der Inhalt eines Nutzers oder Hashtags wird nicht unter einem festen DHT-Schlüssel gespeichert. Der Schlüssel ist eine Funktion der öffentlichen ID des Nutzers (oder des Hashtag-Texts) und einer Zeit-Epoche. Das bedeutet, dass sich der DHT-Standort der Beiträge eines Nutzers über die Zeit verschiebt und die Verantwortung für deren Hosting in verschiedenen Epochen auf verschiedene Knoten verteilt.

Für Nutzer und Hashtags mit hohem Volumen wird der Schlüsselraum weiter unterteilt: Je geschäftiger eine Identität in Beitragsvolumen ist, desto granularer wird die Bucket-Unterteilung. Der Inhalt eines vielschreibenden Nutzers wird auf mehr, kleinere Buckets aufgeteilt — jedes von einer anderen Region des DHT-Rings gehostet. Erfasst wird das im Typ BucketLocation, der Identität, Epoche und Bucket-Granularität gemeinsam codiert.

Das Ergebnis: Kein einzelner Knoten wird dauerhaft für den Verkehr eines stark genutzten Accounts verantwortlich, und kein Knoten wird zu einem stabilen Ziel für einen DDoS-Angriff, der einen bestimmten Account oder Hashtag stumm machen will.

Quelle: buckets.rsBucketLocation

Bedarfs­getriebenes Caching

Über die Heilung hinaus (die fehlende Daten nachträglich repariert) implementiert Hashiverse bedarfs­getriebenes Caching, um die Last auf den der DHT am nächsten liegenden Servern für beliebte Buckets zu senken. Vermittelnde Server cachen Beitrags-Bundles und Feedback-Bundles im Auftrag der dem Inhalt nächst­gelegenen Knoten, bedienen Clients direkt und beenden den Kademlia-Walk frühzeitig. Der Cache weitet sich unter anhaltender Nachfrage nach außen aus und zieht sich automatisch zusammen, wenn das Interesse nachlässt — vollständig selbstregulierend, keine Koordination erforderlich.

Hit-Schwellen-Token-Mechanismus

Jeder Server hält einen Bundle-Cache und einen Feedback-Cache (jeweils ein gewichteter Moka-Cache pro Typ). Bei jeder eingehenden Anfrage GetPostBundleV1 oder GetPostBundleFeedbackV1 erhöht der Server einen Hit-Zähler für diesen Bucket-Standort. Sobald der Zähler den Schwellwert erreicht (derzeit 10 Hits innerhalb des Idle-Fensters), gibt der Server einen CacheRequestTokenV1 aus — ein kurzlebiges, vom Server signiertes Token, beschränkt auf diesen Bucket-Standort.

Der Client sammelt alle während seines Kademlia-Walks ausgegebenen Tokens. Nach erfolgreichem Abruf und Verifikation des Bundles von einem verantwortlichen Server lädt er das Bundle asynchron via CachePostBundleV1 / CachePostBundleFeedbackV1 zu jedem token-ausgebenden Server hoch. Der Server validiert das Token (Signatur, Ablauf, Standort), parst das Bundle und legt es im In-Memory-Cache ab. Nachfolgende Clients, die durch diesen Server laufen, bekommen das gecachte Bundle direkt, ohne den Walk weiter nach innen fortzusetzen.

Ein In-Flight-Deduplizierungs­cache (mit Schlüssel auf Standort-ID, TTL passend zur Token-Lebenszeit) verhindert, dass der Server doppelte Tokens an mehrere gleichzeitige Walker für denselben Bucket ausgibt.

Quelle: post_bundle_caching.rs, post_bundle_feedback_caching.rs

Live- vs. versiegelte Buckets

Der Cache unterscheidet zwischen Live- und versiegelten Daten:

Discovery-Radius auf Client-Seite

Um den expandierenden Cache zu nutzen, hält jeder Client einen Discovery-Radius pro Bucket: die XOR-Distanz zum entferntesten Server, der beim vorherigen Walk erfolgreich ein Bundle zurückgegeben hat. Beim nächsten Walk überspringt der PeerIterator Server, die näher als dieser Radius sind, und beginnt mit der Abfrage von der Frontier nach außen — geht also direkt dahin, wo der Cache zuletzt gefunden wurde, und umgeht die verantwortlichen (innersten) Server vollständig.

Der Radius wird nach jedem Walk auf die XOR-Distanz des entferntesten positiven Antworters aktualisiert. Wenn äußere Cache-Schichten inzwischen abgelaufen sind und kein Server an der Frontier antwortet, fällt der Walk auf den nächst-näheren Peer zurück und der Radius zieht sich natürlich auf die aktuelle Cache-Tiefe zusammen. Der Radius wird in einem Time-to-Idle-Cache gehalten, sodass Buckets, die eine Weile nicht besucht wurden, beim nächsten Zugriff wieder bei den verantwortlichen Servern starten.

Cache-Eviction und Kapazität

Beide Caches sind nach tatsächlicher Bundle-Größe in Bytes gewichtet, gestützt auf Mokas W-TinyLFU-Aufnahmerichtlinie. Platzhalter-Einträge (Standorte, die abgefragt, aber noch nicht mit einem Bundle befüllt wurden) verwenden ein kleines festes Gewicht, sodass sie an der Häufigkeitsskizze teilnehmen, ohne echte Kapazität zu verbrauchen. Wenn die Byte-High-Water-Mark erreicht ist, werden zuerst die am seltensten genutzten Standorte geräumt. Jeder Standort hat zudem ein Time-to-Idle von 60 Sekunden: Ein Standort, der keine Anfragen mehr empfängt, wird leise geräumt, sein Hit-Zähler zurückgesetzt und der effektive Cache-Radius zieht sich zusammen.

Der Bundle-Cache speichert pro Standort bis zu einer konfigurierbaren Anzahl von Originatoren (derzeit 5). Erscheint ein neuer Originator, wenn die Pro-Standort-Obergrenze voll ist, wird das Bundle ersetzt, das am ehesten abläuft. Versiegelte Bundles (kein Ablauf) werden so behandelt, als liefen sie zuletzt ab, und werden nur von anderen versiegelten Bundles verdrängt.

Quelle: post_bundle_caching_shared.rs, config.rsSERVER_POST_BUNDLE_CACHE_MAX_BYTES u. a.

Heilung von Beitrags-Bundles

Wenn ein Client Beiträge für einen Bucket abruft, fragt er mehrere Server ab und sammelt deren Antworten. Dann vergleicht er: Welcher Server hat Beiträge, die einem anderen fehlen? Für jedes Geber-Empfänger-Paar, in dem der Geber Beiträge hat, die dem Empfänger fehlen, organisiert der Client eine zweistufige Heilung:

  1. Claim-Phase: Der Client fragt den Empfangs-Server, welche der Beiträge des Gebers er braucht, mittels einer HealPostBundleClaimV1-Anfrage. Der Server gibt an, welche Beitrags-IDs ihm fehlen.
  2. Commit-Phase: Der Client (oder der Geber als Proxy) sendet diese spezifischen Beitrags-Bytes per HealPostBundleCommitV1 an den Empfänger.

Das läuft in Hintergrund-Tasks, die nach Abschluss des primären Abrufs des Clients gestartet werden. Der Nutzer sieht seinen Inhalt, ohne auf die Heilung zu warten; das Netzwerk repariert sich parallel.

Quelle: post_bundle_healing.rs, test_healing_post_bundles.rs

Feedback-Heilung

Feedback-Signale — Likes, Meldungen, Dislikes — divergieren ebenfalls zwischen Servern, während sie sich durch das Netzwerk verbreiten. Eine Meldung kann Server A erreichen, aber Server B nicht. Die Feedback-Heilung adressiert das getrennt von der Beitrags-Heilung, weil Feedback eine andere Datenform hat (der 50-Byte EncodedPostFeedbackV1-Datensatz) und eine andere Merge-Regel.

Die Merge-Regel für Feedback lautet: Für jedes Paar (post_id, feedback_type) wird das Signal mit der höchsten PoW über alle Server hinweg behalten. Der Client berechnet, nachdem er Feedback von mehreren Servern gesammelt hat, das globale Maximum und identifiziert Server, die schwächere Signale als das globale Maximum haben. Er sendet dann die stärkeren Signale per HealPostBundleFeedbackV1 an diese Server.

Das ist eine einphasige Heilung (kein Claim-Schritt): Der Feedback-Datensatz ist klein genug, dass das unbedingte Senden günstiger ist als der Roundtrip, vorher zu fragen.

Quelle: post_bundle_feedback_healing.rs, test_healing_post_bundle_feedbacks.rs

DDoS-Resistenz

Hashiverse hat vier Schichten DDoS-Schutz, die Angriffe an immer früheren Punkten des Verbindungs­lebenszyklus abfangen, vom Kernel-Paketfilter bis zur Anwendungs­level-Ratenbegrenzung.

Kernel-Schicht: ipset-Blacklist

Bei anhaltenden oder schweren Angriffen eskaliert der Server zum Paket-Drop auf Kernel-Ebene über das Linux-ipset. Auf die Blacklist gesetzte IPs werden mit einem Fünf-Minuten-Timeout in einen Hash-Set aufgenommen; iptables (oder nftables) verwirft passende Pakete, bevor sie die Anwendungsschicht erreichen, und eliminiert so sogar die Kosten, die TCP-Verbindung anzunehmen und abzulehnen.

ipset create hashiverse_ddos_blacklist hash:ip timeout 300

Der Rust-Server fügt IPs über nicht-blockierende Command::new("ipset")-Aufrufe in dieses Set ein — die Kernel-Operation blockiert die Anfrage­verarbeitung nicht. In der Produktion erfordert das CAP_NET_ADMIN oder root, verwaltet über Linux Ambient Capabilities.

Pre-TLS: Verbindungs­obergrenze und Pro-IP-Slot-Limit

Jede neue TCP-Verbindung wird vor Beginn des TLS-Handshakes geprüft. An diesem Punkt laufen zwei Wächter:

Beide Prüfungen werden vor dem Aufruf von TlsAcceptor::accept ausgelöst, sodass eine Flut neuer Verbindungen von einer einzelnen IP keine CPU für die TLS-Verhandlung verbraucht. Der Server ist derzeit bewusst nur IPv4; die Unterstützung für IPv6 erfordert Tracking auf Präfix­ebene (/64), um wirksam zu sein, was noch nicht implementiert ist.

TLS-Handshake-Timeout

Hat eine Verbindung die Pre-TLS-Wächter passiert, wird der TLS-Handshake in einen harten Timeout eingehüllt. Ein Client, der ein ClientHello sendet, aber den Handshake nie abschließt — eine klassische langsame Verbindung auf TLS-Ebene — wird nach einer festen Anzahl Sekunden gekappt, und sein Bad-Request-Score wird erhöht, was ihn dem Anwendungs­level-Bann näherbringt.

Slow-Loris-Verteidigung: Header- und Body-Lese-Timeouts

Nach erfolgreichem TLS-Handshake schützen zwei weitere Timeouts gegen Verbindungs­halt-Angriffe auf der HTTP-Schicht:

Anwendungsschicht: IP-Reputations-Cache

Anfragen, die alle obigen Schichten passieren, werden pro IP-Adresse mit einem In-Memory-Moka-Cache verfolgt. IPs, die den Anfrage-Raten-Schwellwert innerhalb eines gleitenden Fensters überschreiten, erhalten eine Hashiverse-RPC- Fehlerantwort und sammeln einen Bad-Request-Score an. Sobald der Score eine Schwelle überschreitet, wird die IP auf die ipset-Blacklist befördert und für zukünftige Verbindungen auf die Kernel-Schicht heruntergedrückt.

Quelle: hashiverse-server-lib/src/network/transport/, config.rs

TimeProvider-Abstraktion

Der Trait TimeProvider abstrahiert Echtzeit und ermöglicht ScaledTimeProvider — eine zeitbeschleunigte Simulationsuhr, die in Tests verwendet wird. Integrationstests, die Heilungsverhalten über Zeitfenster prüfen, die normalerweise Stunden dauern würden, können in Sekunden laufen, indem die Zeit skaliert wird. So kann die Integrationstest-Suite den vollen Heilungslebenszyklus prüfen — einschließlich des Verfallens alter Inhalte — ohne Wanduhr-Verzögerungen.

Quelle: hashiverse-lib/src/tools/time_provider/