NEU: Helius übernimmt Light Protocol
Solana Geyser-Plugins
Blog/Grundlagen

Solana Geyser-Plugins: Datenstreaming mit Lichtgeschwindigkeit

Developer Experience Engineer0xIchigo auf X0xIchigo auf LinkedIn0xIchigo auf GitHub
13 Min. Lesezeit

Worum geht es in diesem Artikel?

Geyser-Plugins sind modulare Komponenten, die Daten zu Accounts, Slots, Blöcken und Transaktionen an externe Datenspeicher übertragen. So können Entwickler die RPC-Last (Remote-Prozeduraufrufe) von einem Validator nehmen. Geyser-Plugins bieten eine flexible Lösung für Entwickler, die das Streaming und die Verarbeitung ihrer Daten individuell anpassen möchten.

In diesem Artikel gehen wir auf die Feinheiten von Solana Geyser-Plugins ein. Zunächst betrachten wir AccountsDB-Replikate. Dieser vorgeschlagene Ansatz für Datenreplikation und Lastmanagement wurde letztlich zugunsten von Geyser-Plugins aufgegeben.

Danach erklären wir, was Geyser-Plugins sind, wie sie funktionieren und wie sie über das Plugin Interface strukturiert werden.

Anschließend stellen wir gängige Geyser-Plugins vor und führen dich durch den komplexen Prozess, ein eigenes zu entwickeln. Zum Schluss zeigen wir, wie Helius das Datenstreaming auf Solana vereinfacht.

AccountsDB-Replikate: Ein aufgegebener Ansatz für Datenreplikation und RPC-Last

Solana untersuchte mehrere Möglichkeiten, um hohe RPC-Last und Datenreplikation zu bewältigen. Ein vielversprechender Ansatz waren AccountsDB-Replikate. Sie sollten Anfragen zum Scannen von Accounts vom Haupt-Validator auf AccountsDB-Replikate auslagern. Das System war zwar vielversprechend, aber von Natur aus komplex. Außerdem erforderte es neue Dienste, um den Haupt-Validator und die Replikate zu synchronisieren. Letztlich wurde dieser Vorschlag zugunsten des Geyser Plugin Systems aufgegeben. Diese Lösung ließ sich einfacher vom Validator-Client unterstützen und gab Entwicklern mehr Flexibilität bei der Implementierung ihrer Anwendungen.

Was genau sind also Solana Geyser-Plugins?

Was sind Solana Geyser-Plugins?

Solana Geyser-Plugins bieten latenzarmen Zugriff auf Solana-Daten und können Anwendungen versorgen, sodass keine RPC-Aufrufe an Validatoren nötig sind. Muss ein Validator beispielsweise zahlreiche getProgramAccounts-Aufrufe kurz hintereinander bearbeiten, kann er durch diese hohe Last hinter das Netzwerk zurückfallen.

Geyser-Plugins lösen dieses Problem, indem sie Informationen zu Accounts, Blöcken, Slots und Transaktionen an externe Datenspeicher wie relationale Datenbanken, NoSQL-Datenbanken oder Kafka weiterleiten.

Durch diese Weiterleitung können RPC-Dienste flexiblere und gezieltere Optimierungen wie Caching und Indizierung für Abfragen aus diesen externen Speichern anbieten.

Geyser-Plugins bilden eine Brücke zwischen Solana und externen Datenspeicherlösungen. Entwickler können damit einen erheblichen Teil der Datenverwaltung von Validatoren auslagern. Das verbessert die Performance und reduziert das Risiko möglicher Engpässe.

Geyser-Plugins sorgen dafür, dass Validatoren unabhängig vom RPC-Datenverkehr mit dem Netzwerk synchron bleiben.

Das Geyser Plugin Interface

Entwickler können Geyser-Plugins mit dem Solana Geyser Plugin Interface erstellen. Das Interface bietet Zugriff auf Accounts, Transaktionen, Slots, Blockmetadaten und Einträge. Es wird im solana-geyser-plugin-interface-Crate deklariert und durch den GeyserPlugin-Trait definiert.

Der Trait definiert Methoden, denen jeweils update_ vorangestellt ist. Sie werden aufgerufen, wenn neue Daten erstellt oder bestehende Daten aktualisiert werden. Geyser-Plugins müssen außerdem ihr Verhalten beim Laden und Entladen festlegen. Der Trait beschreibt die wesentlichen Methoden, die ein Geyser-Plugin implementieren sollte, um Daten entsprechend dem gewünschten Plugin-Verhalten effizient zu streamen.

Quellcode

Code
pub trait GeyserPlugin:Any +Send +Sync +Debug {
    // Required method
    fn name(&self) -> &'static str;

    // Provided methods
    fn on_load(&mut self, _config_file: &str) ->Result<()> { ... }
    fn on_unload(&mut self) { ... }
    fn update_account(
        &self,
        account:ReplicaAccountInfoVersions<'_>,
        slot: Slot,
        is_startup:bool
    ) ->Result<()> { ... }
    fn notify_end_of_startup(&self) ->Result<()> { ... }
    fn update_slot_status(
        &self,
        slot: Slot,
        parent:Option,
        status:SlotStatus
    ) ->Result<()> { ... }
    fn notify_transaction(
        &self,
        transaction:ReplicaTransactionInfoVersions<'_>,
        slot: Slot
    ) ->Result<()> { ... }
    fn notify_entry(&self, entry:ReplicaEntryInfoVersions<'_>) ->Result<()> { ... }
    fn notify_block_metadata(
        &self,
        blockinfo:ReplicaBlockInfoVersions<'_>
    ) ->Result<()> { ... }
    fn account_data_notifications_enabled(&self) ->bool { ... }
    fn transaction_notifications_enabled(&self) ->bool { ... }
    fn entry_notifications_enabled(&self) ->bool { ... }
}

Trait-Deklaration

Der GeyserPlugin-Trait bildet das grundlegende Interface für alle Plugins im Solana Geyser Plugin-Ökosystem. Er wird als öffentlicher Trait mit den Trait-Bounds Any, Send, Sync und Debug aus der Rust-Standardbibliothek deklariert. Die Trait-Bounds funktionieren wie folgt:

  • Any ermöglicht Typreflexion und damit das Downcasting auf einen konkreten Typ
  • Send gibt an, dass sich der Besitz des Typs, der diesen Trait implementiert, zwischen Threads übertragen lässt
  • Sync bedeutet, dass Referenzen auf den Typ, der diesen Trait implementiert, zwischen Threads geteilt werden können
  • Debug ermöglicht die Formatierung des Typs für die Ausgabe, insbesondere zu Debugging-Zwecken

Any und Debug sind für uns nicht besonders wichtig.

Wirklich entscheidend ist, dass GeyserPlugin die Traits Send und Sync benötigt, damit das Programm threadsicher ist.

Erforderliche Methode

Code
fn name(&self) -> &'static str;

Die Methode name ist für jeden Typ erforderlich, der GeyserPlugin implementiert. Sie dient als Kennung für das Geyser-Plugin. Die Methode gibt einen statischen String-Slice zurück, der den Namen des Geyser-Plugins repräsentiert.

Dass diese Methode und alle anderen Methoden außer on_load und on_unload den Typ &self statt &mut self verwenden, ist seit Solanas Update 1.16 neu. Das verbessert die Performance erheblich: Das Geyser-Plugin muss nicht mehr in einen Read-Write Lock eingebunden und bei jedem Aufruf einer seiner Funktionen mit einem Write Lock gesperrt werden.

Bereitgestellte Methoden

Der Trait stellt mehrere Methoden mit Standardimplementierungen bereit. Implementierungen von GeyserPlugin können sie überschreiben.

Code
fn on_load(&mut self, _config_file: &str) ->Result<()> { ... }

Die Methode on_load ist der Callback, der aufgerufen wird, wenn das System ein Plugin lädt. Sie dient zur erforderlichen Initialisierung des Plugins. Die Methode akzeptiert eine Referenz auf einen string, der den Pfad zu einer Konfigurationsdatei repräsentiert. Die Konfiguration muss im JSON5-Format vorliegen und ein Feld libpath enthalten, das den vollständigen Pfad der Shared Library angibt, die dieses Interface implementiert.

Code
fn on_unload(&mut self) { ... }

Die Methode on_unload ist ein Callback, der vor dem Entladen eines Plugins durch das System erforderliche Bereinigungen ausführt.

Code
fn update_account(
        &self,
        account:ReplicaAccountInfoVersions<'_>,
        slot: Slot,
        is_startup:bool
    ) ->Result<()> { ... }

Die Methode update_account wird aufgerufen, wenn ein Account auf der Bestätigungsstufe „processed“ aktualisiert wird. Das kann innerhalb eines Slots mehrmals geschehen. Dabei ist es entscheidend, bestätigte Slots nachzuverfolgen, um die Account-Aktualisierungen zu erhalten, die in die kanonische Chain übernommen werden.

Die Struct ReplicaAccountInfoVersions enthält die Metadaten und Daten des gestreamten Accounts.

Der Parameter slot verweist auf den Slot, in dem der Account aktualisiert wird.

Wenn is_startup true ist, wird der Account beim Start des Validators aus Snapshots geladen. Wenn is_startup false ist, wird der Account während der Transaktionsverarbeitung aktualisiert.

Code
fn notify_end_of_startup(&self) ->Result<()> { ... }

Die Methode notify_end_of_startup signalisiert das Ende der Startphase. Dies geschieht, wenn der Validator die Account-Datenbank aus Snapshots wiederhergestellt und alle Accounts entsprechend aktualisiert hat.

Code
fn update_slot_status(
        &self,
        slot: Slot,
        parent:Option,
        status:SlotStatus
    ) ->Result<()> { ... }

Die Methode update_slot_status wird aufgerufen, wenn sich der Status eines Slots ändert. Sie akzeptiert einen Slot, einen Option<u64> für den übergeordneten Slot und eine SlotStatus enum-Instanz.

SlotStatus definiert die drei Zustände eines Slots in Solana:

  • Processed – der höchste Slot, an dem der Node gearbeitet hat. Solange der Slot weder bestätigt noch finalisiert ist, gehört er zu der Chain, die der Validator am wahrscheinlichsten für die künftige kanonische Chain hält
  • Confirmed – der Slot hat genügend Stimmen erhalten, um als sicherer Teil der Chain zu gelten. Eine Supermehrheit der Solana-Validatoren unterstützt diesen Slot
  • Rooted – der Slot ist jetzt ein dauerhafter Bestandteil der Blockchain. Alle anderen Versionen oder Forks der Chain müssen auf diesem Slot aufbauen. Das bedeutet, dass alle Zweige im Netzwerk von diesem Block abstammen
Code
fn notify_transaction(
        &self,
        transaction:ReplicaTransactionInfoVersions<'_>,
        slot: Slot
    ) ->Result<()> { ... }

Die Methode notify_transaction wird aufgerufen, wenn eine Transaktion in einem Slot verarbeitet wird. Sie übermittelt dem Plugin die Details der Transaktion.

ReplicaTransactionInfoVersions ist ein enum-Wrapper, der ReplicaTransactionInfo verarbeitet. Würde sich die Struktur von RepicaTransactionInfo ändern, gäbe es für die neuere Version einen neuen enum-Eintrag. Plugin-Implementierungen müssten diese Änderung durch einen neuen enum-Eintrag berücksichtigen. Derzeit umschließt enum zwei Varianten:

  1. V0_0_1(&'a ReplicaTransactionInfo<'a>)
  2. V0_0_2(&'a ReplicaTransactionInfoV2<'a>)
Code
pub struct ReplicaTransactionInfo<'a> {
    pub signature: &'a Signature,
    pub is_vote: bool,
    pub transaction: &'a SanitizedTransaction,
    pub transaction_status_meta: &'a TransactionStatusMeta,
}

pub struct ReplicaTransactionInfoV2<'a> {
    pub signature: &'a Signature,
    pub is_vote: bool,
    pub transaction: &'a SanitizedTransaction,
    pub transaction_status_meta: &'a TransactionStatusMeta,
    pub index: usize,
}

Der wichtigste Unterschied zwischen den Varianten ist, dass die zweite den Index der Transaktion im Block speichert.

Code
fn notify_entry(&self, entry:ReplicaEntryInfoVersions<'_>) ->Result<()> { ... }

notify_entry benachrichtigt das Plugin über einen neuen Eintrag. Die Methode akzeptiert eine Instanz von ReplicaEntryInfoVersions. Dieser Wrapper macht die Verarbeitung von ReplicaEntryInfo zukunftssicher. Derzeit enthält er die Variante V0_0_1(&'a ReplicaEntryInfo<'a>).

Diese Variante ist eine struct. Sie enthält Informationen zum Slot des Eintrags, zu seinem Index im Block, zur Anzahl der Hashes seit dem vorherigen Eintrag, zum SHA-256-Hash des Eintrags und zur Anzahl der darin ausgeführten Transaktionen.

Code
fn notify_block_metadata(
        &self,
        blockinfo:ReplicaBlockInfoVersions<'_>
    ) ->Result<()> { ... }

Die Methode notify_block_metadata wird aufgerufen, wenn die Metadaten eines Blocks aktualisiert werden. Sie akzeptiert eine ReplicaBlockInfoVersions enum-Instanz mit den Blockinformationen. Dieses enum ist ein Wrapper für die verschiedenen Versionen von ReplicaBlockInfo. Sie enthalten Informationen über den Block, etwa Slot, Hash, Rewards, Blockzeit und Blockhöhe.

Code
fn account_data_notifications_enabled(&self) ->bool { ... }
fn transaction_notifications_enabled(&self) ->bool { ... }
fn entry_notifications_enabled(&self) ->bool { ... }

Diese Methoden geben boolesche Werte zurück. Sie zeigen an, ob das Plugin Benachrichtigungen für Account-Daten, Transaktionen beziehungsweise Einträge aktivieren möchte.

Hinweis zu Commitment-Stufen

Geyser sendet Aktualisierungen für Account-Daten und Transaktionen sofort nach deren Verarbeitung. Das verbessert die End-to-End-Geschwindigkeit der Indizierung. Es besteht jedoch das Risiko, dass ein verarbeiteter Slot übersprungen wird.

Ein übersprungener Slot ist ein vergangener Slot, der keinen Block erzeugt hat. Das kann passieren, weil der Leader offline war oder der Fork mit diesem Slot zugunsten einer besseren Alternative aufgegeben wurde. Die Datenspeichersysteme, an die gestreamt wird, müssen diese Möglichkeit unbedingt berücksichtigen und Aktualisierungen entsprechend verwalten.

Gängige Solana Geyser-Plugins

Entwickler können aus zahlreichen Solana Geyser-Plugins wählen und diese sogar forken, um sie an ihre Anforderungen anzupassen. Zu den bekanntesten Plugins gehören:

  • PostgreSQL Plugin: zum Verwalten und Abfragen von Daten mit PostgreSQL
  • gRPC Service Streaming Plugin: zum Streamen von Solana-Account-Aktualisierungen an einen gRPC-Dienst
  • RabbitMQ Producer Plugin: für Message Queuing mit RabbitMQ
  • Kafka Producer Plugin: zum Streamen von Daten mit Kafka
  • Amazon SQS Plugin: für Message Queuing mit Amazons Simple Queue Service
  • Google BigTable Plugin: zum Verwalten und Abfragen von Daten mit Google BigTable

Diese Plugins lassen sich an viele verschiedene Anwendungsfälle anpassen.

Clockwork nutzte beispielsweise ein Geyser-Plugin, um Transaktionen zu planen und automatisierte, ereignisgesteuerte Solana-Programme zu entwickeln. Das Projekt wurde zwar eingestellt, doch sein Open-Source-Code bleibt eine wertvolle Ressource und ist auf GitHub verfügbar.

Weitere mögliche Anwendungsfälle sind die Überwachung von Account-Guthaben auf einer DeFi-Plattform, das Bereitstellen von Metriken zum Netzwerkzustand oder die Echtzeitüberwachung von Ereignissen in Lieferketten.

Erstelle dein eigenes Solana Geyser-Plugin

Mit diesen Ressourcen und Komponenten kannst du dein eigenes Plugin entwickeln:

Das Solana Geyser Plugin Scaffold

Das Solana Geyser Plugin Scaffold ist der einfachste Einstieg in die Entwicklung von Solana Geyser-Plugins. Dieses Scaffold dient als minimalistische Vorlage, die Interaktionen zwischen dem Plugin Manager und dem Plugin selbst protokolliert. Es ist ein hervorragender Ausgangspunkt, um dich mit dem Plugin-Workflow und Debugging-Techniken vertraut zu machen.

Der Plugin Manager

Der Plugin Manager ist die zentrale Komponente, die den Lebenszyklus und die Interaktionen aller Geyser-Plugins steuert. Er kann Plugins zur Laufzeit dynamisch laden und entladen. Das ermöglicht mehr Flexibilität und Modularität.

Zur Laufzeit übergibt der Plugin Manager deinem Plugin den Pfad zur Konfigurationsdatei. So kannst du Einstellungen in Geyser-Plugins anpassen, ohne den Code des Plugins zu ändern.

Um ein Plugin in einen Validator zu integrieren, musst du den Pfad zur dynamischen Bibliothek mit dem Parameter --geyser-plugin-config angeben. Dadurch weiß der Validator, wo er das Plugin und die zugehörige Konfiguration findet.

Die Konfigurationsdatei muss mindestens im JSON-Format vorliegen und den Pfad zur dynamischen Bibliothek des Geyser-Plugins enthalten – unter Linux eine .so-Datei. Eine minimale Konfigurationsdatei sieht so aus:

Code
{
    "libpath": "/.so"
}

Ein Geyser-Plugin von Grund auf erstellen

Wenn du abseits der üblichen Wege ein eigenes Geyser-Plugin erstellen möchtest, ohne das Scaffold zu verwenden oder ein bestehendes Plugin anzupassen, musst du dein Plugin mit dem Geyser Plugin Interface programmieren.

Ein Plugin muss den Trait GeyserPlugin implementieren, damit es mit der Runtime funktioniert. Außerdem muss die dynamische Bibliothek eine „C“-Funktion _create_plugin exportieren, die die Implementierung des Plugins erstellt.

Ein Beispiel wäre ein Webhook-Plugin, das den Trait GeyserPlugin implementiert:

Code
#[no_mangle]
#[allow(improper_ctypes_definitions)]
/// # Safety
///
/// This function returns the WebhookPlugin pointer as trait GeyserPlugin.
pub unsafe extern "C" fn _create_plugin() -> *mut dyn GeyserPlugin {
    let plugin = WebhookPlugin::new();
    let plugin: Box = Box::new(plugin);
    Box::into_raw(plugin)
}

Hier erstellen wir eine unsichere öffentliche Funktion, die die C-Aufrufkonvention extern "C" verwendet. Dadurch ist sie mit C und anderen Sprachen kompatibel. Die Funktion fn _create*_*plugin() -> *mut dyn GeyserPlugin selbst gibt einen veränderbaren Raw Pointer auf dynGeyserPlugin zurück, also auf den Trait GeyserPlugin. Der Funktionskörper erstellt eine neue Instanz von WebhookPlugin, boxt diese Instanz als Trait-Objekt und wandelt das geboxtе Trait-Objekt anschließend in einen Raw Pointer um, damit die Funktion ihn zurückgeben kann.

So erstellst du dein eigenes Geyser-Plugin:

  • Entwickle dein Plugin, das das Solana Geyser Plugin Interface implementiert
  • Hole die dynamische Bibliothek (Datei .so) aus dem Ordner target/release oder target/debug
  • Erstelle eine Datei geyser-config.json. Sie muss im Feld „libpath“ den Pfad zur dynamischen Bibliothek des Geyser-Plugins enthalten
  • Starte deinen Validator mit dem Flag --geyser-plugin-config geyser-config.json

Diese Schritte klingen recht einfach. Ein Solana Geyser-Plugin tatsächlich zu betreiben und zu warten, kann jedoch sehr aufwendig sein.

Helius Geyser Streaming

Helius ist dafür bekannt, auf Solana eine beispiellose Developer Experience zu bieten. Durch den exklusiven Fokus auf Solana verfügt Helius über umfassende Erfahrung, hat zahlreiche Herausforderungen bewältigt und viele umfangreiche Integrationen ermöglicht. Damit ist Helius einzigartig aufgestellt, um jedes Problem zu lösen, dem Entwickler begegnen können.

Bei Helius verwalten wir Geyser-Plugins für mehrere leistungsstarke Teams im Solana-Ökosystem. Wir betreiben spezialisierte Geyser-Cluster mit zusätzlicher Redundanz und Fehlertoleranz. So musst du dir keine Sorgen über fehlende Daten oder Ausfallzeiten machen. Über unseren programmatischen API-Zugriff kannst du deine Geyser-Plugins dynamisch ändern, ohne die Zuverlässigkeit zu gefährden. Die Verwaltung von Geyser-Plugins ist oft anspruchsvoll, da du für Datenkonsistenz, Zuverlässigkeit und Verfügbarkeit verantwortlich bist. Warum überlässt du das nicht Helius?

Wenn du dich für Geyser Streaming interessierst, bestelle einen dedizierten Node in deinem Helius-Dashboard oder kontaktiere uns auf Discord, um noch heute loszulegen.

Fazit

Glückwunsch!

In diesem Artikel haben wir die komplexen Themen Datenreplikation und RPC-Lastmanagement anhand von Solana Geyser-Plugins untersucht. Dieses System zu verstehen, ist nicht einfach. Seine anspruchsvolle Architektur ist kaum dokumentiert, bietet Solana-Entwicklern aber zahlreiche Möglichkeiten zur Anpassung und Performance-Optimierung.

Das Wissen aus diesem Artikel ist äußerst wertvoll – besonders wenn du als Entwickler oder Team leistungsstarke Anwendungen auf Solana entwickeln oder verwalten möchtest. Geyser-Plugins sind ein wichtiges Thema, denn sie bieten dem Solana-Ökosystem eine skalierbare und zuverlässige Lösung.

Wenn du bis hierhin gelesen hast, anon: Danke!

Weitere Ressourcen

Helius abonnieren

Bleib bei der Solana-Entwicklung auf dem Laufenden und erhalte Updates, wenn wir neue Beiträge veröffentlichen