ZYCORD Doku
Deutsch
ZycordDokuWire-Protokoll

Wire-Protokoll

Was Zycord-Nodes einander sagen, und die Regeln, die ein Peer-Protokoll davon abhalten, zu einem Verstärker zu werden. Ein Leitfaden zur normativen Spezifikation, kein Ersatz für sie.

Diese Seite ist ein Leitfaden, nicht die Spezifikation

Die Spezifikation ist spec/: die Parameterdateien und das Vektorkorpus. Wo diese Seite und der Baum sich widersprechen, hat der Baum recht — eine Implementierung ist konform, wenn sie die Genesis-Id reproduziert und die Vektoren besteht, und nichts hier ändert daran etwas. Was folgt, ist eine Karte davon, wo die Anforderungen stehen.

Transport#

TCP, dann TLS 1.3. Die Peer-Identität ist ein Ed25519-Schlüssel, getragen in einem selbstsignierten X.509-Zertifikat, das beide Seiten vorlegen. Zertifikatsketten werden nicht validiert — es gibt keine Zertifizierungsstelle und nichts, was eine solche bezeugen könnte —, die Prüfung verlangt also genau ein Zertifikat, verlangt, dass sein öffentlicher Schlüssel ein Ed25519-Schlüssel ist, und entnimmt diesen Schlüssel als Identität des Peers.

Die gewonnene Eigenschaft ist Kanalbindung an einen Schlüssel, nicht Autorisierung: TLS garantiert, dass derjenige, der den Handshake abgeschlossen hat, den privaten Schlüssel zur vorgelegten Identität hält und dass der Datenstrom unterwegs weder gelesen noch verändert werden kann. Es sagt nichts darüber, ob dieser Peer ehrlich ist. Nichts im Protokoll gewährt Befugnisse aufgrund von Identität.

Die Gültigkeitsdaten des Zertifikats sind absichtlich feste Konstanten und kein Fenster um die aktuelle Zeit: ein relatives Fenster macht die Annahme eines Zertifikats von der Übereinstimmung der Uhren abhängig, und ein Netzwerk, das entlang der Uhrenabweichung zerfällt, ist ein Netzwerk, das aus einem Grund zerfällt, der mit dem Konsens nichts zu tun hat.

Implementierungen dürfen keinen Schlüssel aus einer anderen Schicht als Peer-Identität wiederverwenden. Der Node erzeugt bei jedem Start einen frischen und schreibt ihn nie auf die Platte.

Rahmung#

offset  size  field
0       4     length   uint32, little-endian - payload length, excluding this header
4       1     kind     uint8  - see below
5       n     payload
  • length muss gegen MaxMessageBytes = 8 MiB geprüft werden, bevor irgendetwas reserviert wird. Ein Empfänger, der auf eine behauptete Länge hin reserviert, hat einen Fehler zur Speichererschöpfung aus der Ferne, unabhängig davon, was danach kommt.
  • kind muss in [1, 9] liegen. Null und alles oberhalb der höchsten bekannten Art sind ein Protokollverstoß, keine unbekannte Erweiterung: bei Protokoll 1 gibt es keine Hintertür für Vorwärtskompatibilität, weil ein beim Handshake geprüftes Versionsfeld eine solche überflüssig macht.

Nachrichtenarten#

WertNameRichtungNutzlast
1hellobeide, zuerstHandshake
2certificateGossipssz(Certificate)
3block-announceGossipHeader plus Zertifikats-IDs
4get-blockAnfrage32-Byte-Block-ID ‖ u32-Chunk-Index
5blockAntwortEin Chunk von ssz(Block)
6get-headersAnfrageLocator
7headersAntwortHeader-Folge
8get-peersAnfrageleer
9peersAntwortAdressliste

Es gibt keine netzwerkspezifische Kodierung eines Konsensobjekts, und genau das macht “

Jede eingehende Nachricht kostet ihren Absender#

Das ist der Teil der Spezifikation, dessen vollständige Lektüre sich am meisten lohnt, denn hier leckt ein Peer-Protokoll üblicherweise. Zwei Regeln beherrschen ihn.

Die Regel zur Kostenreihenfolge#

Arbeit wird in aufsteigender Kostenreihenfolge erledigt, und eine Nachricht wird vor dem teuren Schritt abgerechnet, der auf sie folgt. Ein Empfänger, der zuerst verifiziert und danach bewertet, hat einen Verstärker gebaut: das Billige zu senden ist das Teure zu prüfen.

Jeder Ausgang ist bepreist#

Es gibt kein unbenanntes Free. Jede Nachrichtenart, gekreuzt mit jedem Ausgang, den sie hervorbringen kann, steht in einer Tabelle mit einer zugeordneten Bewertung. Ein Ausgang, der ohne Abrechnung durch die Tabelle fällt, ist der Mangel, den diese Regel verhindern soll — und er wurde in der Praxis über engere Wege als eine unbepreiste Zeile erreicht: eine Implementierung, die einen fehlerhaften Frame an ihrem einzigen Bewerter vorbeileitete, machte diesen Frame so lange kostenlos, wie eine Anfrage offen war, ohne dass irgendeiner Tabelle eine Zeile hinzugefügt worden wäre.

Auch das Ausliefern wird gemessen. Blockbytes sind die eine Antwort, die um Größenordnungen umfangreicher ist als die Anfrage, die nach ihr fragt, also tragen sie zusätzlich zur Anfragenzahl ihr eigenes Bytebudget.

Verbindungsmanagement und die Eclipse-Abwehr#

Dieser Abschnitt trägt mehr gemessene Fehlschläge als jeder andere, und jede Anforderung unten existiert, weil eine Implementierung ohne sie in einer Messung eclipsed wurde.

  • Ausgehende Ziele müssen mit Adressvielfalt ausgewählt werden, damit ein einzelner Hosting-Bereich die ausgehenden Slots eines Nodes nicht füllen kann.
  • Ausgehende Ziele müssen zudem je Gossip-Quelle begrenzt sein. Adressvielfalt allein genügt nicht, aus einem Grund, der Arithmetik ist und keine Ermessensfrage: eine Adressgruppe ist eine Eigenschaft einer Adresse, die ein Angreifer besitzt, aber eine Adresse, die ein Peer behauptet, sind Bytes, die er erfunden hat, und beliebige vier Bytes sind ein gültiger IPv4-Host. Ein Angreifer ganz ohne Adressen prägt zum Preis eines Frames je Zeichenkette eine frische Vielfaltsgruppe. Was er nicht erfinden kann, ist die Verbindung, über die die Behauptung eintraf.
  • Der Socket, über den eine eingehende Verbindung eintraf, darf nicht zu einem ausgehenden Ziel werden. Es ist die Quelladresse des Peers — ein flüchtiger Port, den sein Betriebssystem gewählt hat, und nichts, worauf er lauscht —, ein Slot, der aufs Anwählen verwendet wird, ist also ein Slot für eine Adresse, die nicht antworten kann.
  • Beide Schranken müssen gegen die Verbindungen gezählt werden, die ein Node hält, nicht gegen einen einzelnen Auswahlaufruf. Sonst baut eine Wählschleife, die bereits verbundene Peers ausschließt, beide Budgets jede Runde wieder voll auf, und die Schranke verzögert einen Angreifer um eine Runde je Kontingent, statt ihn zu begrenzen. Gemessen: ein Erzähler nahm über vier Runden hinweg 2, dann 4, dann 6, dann 8 von 8 ausgehenden Slots.
  • Der Peer-Speicher muss dauerhaft sein und muss begrenzt sein. Ein Node, der nach jedem Neustart leer beginnt, gibt einem Angreifer bei jedem Neustart eine frische Gelegenheit; einer, der mit dem Bestand des Angreifers beginnt, gibt ihm dieselbe Gelegenheit dauerhaft.
  • Ein begrenzter Speicher darf eine wohlgeformte Adresse nicht ablehnen, weil er voll ist; er verdrängt für sie. Eine nie kontaktierte ehrliche Adresse ist nie besser als eine nie kontaktierte erfundene, ein Angreifer, der den Speicher zuerst füllt, verriegelt ihn also gegen alles, was danach angeboten wird — die eigene Bootstrap-Liste des Betreibers eingeschlossen.
  • Was die Verdrängung wählt, zählt mehr als dass sie geschieht. “Nimm das Opfer aus der größten Population” liest sich als eine Flut verdrängt sich selbst und ist das nur, solange die Flut das Größte im Speicher ist. Auf einem Node, der von einem hilfsbereiten Peer aus gebootstrappt wurde, ist die größte Population das Adressbuch dieses Peers. Gemessen an einer so geordneten Implementierung: 200 erfundene Adressen aus einer Quelle verdrängten 200 ehrliche, ohne Kosten für den Angreifer.
  • Unter ununterscheidbaren Einträgen darf der letzte Stichentscheid nichts sein, was der gossipende Peer wählt — die Adresse eingeschlossen —, und das gilt für die Auswahl genauso wie für die Verdrängung. Gemessen an einer Implementierung, deren Auswahl auf die Adresszeichenkette durchfiel: 8 ehrliche Adressen gegen 8 erfundene ergaben 8 von 8 erfundenen und über zehn Runden null ehrliche ausgehende Verbindungen.

Was bewusst fehlt#

FehltWarum
NAT-TraversierungDie Kosten sind benannt, und die Bedingung fürs Neuaufrollen wird gemessen statt angenommen: der Anteil erreichbarer Nodes im öffentlichen Testnet ist die erste Bedingung dafür, die Entscheidung neu aufzurollen.
KompressionEin Kompressor auf einem vom Angreifer kontrollierten Datenstrom ist eine Angriffsfläche —
Signaturen auf NachrichtenebeneDer Transport authentifiziert den Kanal; Konsensobjekte tragen ihre eigenen Signaturen. Eine dritte Schicht würde den Weiterleiter authentifizieren, wovon keine Entscheidung abhängt.
Anfrage-IDsAnfrage und Antwort werden über Verbindung und Reihenfolge zugeordnet, sodass die Synchronisation auf einer eigenen Verbindung läuft — was einen Zustandsautomaten erspart. Wo ein Peer nicht erreichbar ist, darf die Synchronisation über eine bestehende Gossip-Verbindung laufen und muss Antworten dann inhaltlich zuordnen.
Ein vorwärtskompatibler ErweiterungsmechanismusProtokoll 1 prüft seine Version beim Handshake und trennt bei Abweichung. Erweiterungen kommen als Protokoll 2.

Was eine Zertifikats-ID nicht abdeckt#

Hier noch einmal erwähnt, weil es eine Weiterleitungsregel entscheidet. Die ID eines Zertifikats bindet sich an das, was es autorisiert, und nie an das, was es lediglich belegt, Signaturen liegen also außerhalb des Urbildes der ID. Damit wird der Nachweis zu etwas, das jeder unterwegs ersetzen kann: man nehme ein Zertifikat, ersetze seine Signatur durch Unsinn und verbreite es — dieselbe ID, jetzt ungültiges Exemplar.

Zwei Regeln schließen das. Ein Block bindet sich an den Nachweis, den er trägt, über eine Wurzel über Exemplar-Hashes statt über IDs. Und die Weiterleitung behandelt Exemplare, nicht IDs: ein Node, der ein Exemplar empfängt, dessen Nachweis die Prüfung nicht besteht, verwirft dieses Exemplar ohne Nachteil für die ID — die ID wird nicht markiert, nicht als ungültig zwischengespeichert, und ein späteres Exemplar, das die Prüfung besteht, wird normal weitergeleitet. Eine verstümmelte Kopie kostet ihren Verstümmler die Bandbreite fürs Senden und kostet das Zertifikat nichts.

Dies von Grund auf implementieren#

Eine unabhängige Implementierung, die die Golden Vectors besteht, ist ein Peer, kein Fork — das ist der ganze Sinn dieser Art zu spezifizieren. Beginnen Sie mit spec/README.md für die Konsensobjekte, und prüfen Sie dann Ihre Arbeit mit zcd vectors.