Zum Hauptinhalt springen

Komprimierte Filter (Kuckucksfilter)

Aktualisiert am
Sep 03, 2026
Solana gRPC is included with Scale plans and up. Learn more on our pricing page.

Übersicht

Mit komprimierten Filtern (auch als „Cuckoo-Filter“ bekannt) können Sie Millionen von Konten in einem einzigengRPC verfolgen, ohne die Obergrenzen für öffentliche Schlüssel zu erreichen. Anstatt eine explizite Liste öffentlicher Schlüssel zu senden, übermitteln Sie einen kompakten binären Filter, den der Server verwendet, um eingehende Aktualisierungen abzugleichen.

EingRPC enthält eine explizite Liste der öffentlichen Schlüssel, die abgeglichen werden sollen, und diese Liste wächst schnell an: Eine Million öffentliche Schlüssel entsprechen etwa 44 MB pro Anfrage. Die Verfolgung von Hunderten oder Tausenden von Konten (z. B. 500 Wallets plus 700 Token-Konten) führt schnell dazu, dass diese Grenzen überschritten werden.

Ein komprimierter Filter ersetzt diese explizite Liste durch eine probabilistische Datenstruktur, die dieselbe Menge auf einem Bruchteil des Speicherplatzes kodiert:


  • Umgeht die Beschränkungen für öffentliche Schlüssel: Die einzige Begrenzung ist die Nachrichtengröße (~99 MiB, was mehrere zehn Millionen öffentliche Schlüssel ermöglicht).
  • Reduziert die Nutzdatengröße im Vergleich zu einer expliziten Liste öffentlicher Schlüssel um das ~10-Fache.
  • Unterstützt Einfüge- und Löschvorgänge in O(1)-Komplexität, sodass Sie eine große Menge direkt aktualisieren können, anstatt sie von Grund auf neu aufzubauen.

Was ist ein Kompressionsfilter?

Ein komprimierter Filter ist eine probabilistische Datenstruktur, die prüft, ob ein öffentlicher Schlüssel zu einer Menge gehört, ohne die gesamte Menge zu speichern. Anstatt jeden 32-Byte-Schlüssel zu speichern, wird für jeden Schlüssel ein kurzer Fingerabdruck (ein kleiner Hash) in einer Bucket-Tabelle abgelegt, wodurch sich der zur Darstellung der Menge benötigte Speicherplatz um etwa das Zehnfache reduziert.

Anzahl der KontenKompressionsfilterListe der expliziten öffentlichen Schlüssel
1,000~4 KiB~44 KB
10,000~32 KiB~440 KB
100,000~256 KiB~4,4 MB
1,000,000~4 MiB~44 MB
2,000,000~8 MiB~84 MB

So funktioniert es

Das Erstellen und Verwenden eines komprimierten Filters folgt unabhängig von der Sprache demselben Lebenszyklus:


  1. Der Client erstellt den Filter: Füge jeden öffentlichen Schlüssel, den du verfolgen möchtest, in eine CompressedAccountFilterSet. Das Set speichert lokal eine exakte Kopie Ihrer öffentlichen Schlüssel und verfügt über einen kompakten Cuckoo-Filter für die Datenübertragung.
  2. Der Filter wird serialisiert: Der Cuckoo-Filter wird als Binär-Blob samt Metadaten (Anzahl der Buckets, Einträge pro Bucket, Fingerabdruck-Bits und Hash-Seed) kodiert und Ihrer Abonnementanfrage angehängt.
  3. Der Server führt einen probabilistischen Abgleich durch: Bei jeder eingehenden Aktualisierung berechnet der Server einen Hashwert des öffentlichen Schlüssels und prüft, ob dessen Fingerabdruck in Ihrem Filter vorhanden ist; bei Übereinstimmung leitet er die Aktualisierung weiter.
  4. Der Client überprüft die Übereinstimmungen: Da die serverseitige Überprüfung eine Falsch-Positiv-Rate von etwa 1 % aufweist, vergleicht Ihr Client jede Übereinstimmung noch einmal mit dem exakten öffentlichen Schlüsselsatz, bevor er darauf reagiert.

Filterstruktur

Der komprimierte Filter wird als Teil Ihrer Abonnementanfrage übermittelt:

{
"cuckooAccountInclude": {
"data": "AAAAAAAAAACJdcGaAAAAAAAAAAAASsE3BAAAADxkwAAAAAAAAAAAA...",
"bucketCount": 16,
"entriesPerBucket": 4,
"fingerprintBits": 16,
"hashSeed": "8749487436367949345",
"hashAlgorithm": "SIP_HASH"
}
}
FeldBeschreibung
DatenBase64-kodierte Bucket-Daten
EimeranzahlAnzahl der Segmente im Filter
Einträge pro BucketSteckplätze pro Behälter (in der Regel 4)
fingerprintBitsBits pro Fingerabdruck (8, 12 oder 16)
hashSeedStartwert für die Hash-Funktion (SipHash-2-4)
Hash-AlgorithmusBezeichner des Hash-Algorithmus (derzeit SIP_HASH)

Abonnementarten

Komprimierte Filter können zwei Abonnementtypen zugeordnet werden, die jeweils bei einem anderen Ereignis ausgelöst werden:


  • Kontofilter (cuckoo_accounts_filter): Wird ausgelöst, wenn der eigene Status eines überwachten Kontos geschrieben wird. Verwenden Sie dies, wenn Sie Änderungen an Kontodaten überwachen möchten.
  • Blöcke filtern (cuckoo_account_include): Wird ausgelöst, wenn ein verfolgter öffentlicher Schlüssel in den Kontoschlüsseln einer Transaktion referenziert wird, einschließlich der über CPI verwendeten Programm-IDs. Verwenden Sie dies zur Transaktionsüberwachung.

CompressedAccountFilterSet (aus dem Kuckuck Modul im grpc (crate) stellt für jedes eine Methode bereit:

// Accounts subscription - Attaches the filter to an accounts subscription
filter.insert_into_subscribe_request(&mut request, "tracked");

// Blocks subscription - Attaches the filter to a blocks subscription
filter.insert_into_block_subscribe_request(&mut request, "tracked_blocks");

Code-Beispiel

Im folgenden Beispiel wird ein Cuckoo-Filter erstellt und einem „Accounts“-Abonnement zugeordnet, um Änderungen des Kontostatus für bestimmte Konten zu verfolgen. Der Server gleicht eingehende Kontoaktualisierungen mit dem Filter ab, anschließend überprüft der Client jede Übereinstimmung anhand seines exakten öffentlichen Schlüsselsatzes erneut.

Cargo.toml
[package]
name = "compressed-filter-test"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1"
bs58 = "0.5"
futures = "0.3"
rustls = { version = "0.23", default-features = false, features = ["ring"] }
solana-pubkey = "4"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tonic = { version = "0.14", features = ["tls-native-roots"] }
yellowstone-grpc-client = "13.1.0"
yellowstone-grpc-proto = "12.4.0"

src/main.rs
use {
futures::stream::StreamExt,
solana_pubkey::Pubkey,
std::str::FromStr,
tonic::transport::channel::ClientTlsConfig,
yellowstone_grpc_client::GeyserGrpcClient,
yellowstone_grpc_proto::{
cuckoo::CompressedAccountFilterSet,
prelude::{subscribe_update::UpdateOneof, CommitmentLevel, SubscribeRequest},
},
};

// Quicknode gRPC endpoints split into a URL (using port :443) and a token.
// Example: https://docs-demo.solana-mainnet.quiknode.pro:443 + abcde123456789
const ENDPOINT: &str = "https://your-solana-grpc-endpoint.quiknode.pro:443";
const TOKEN: &str = "SOLANA_GRPC_TOKEN";

// Accounts to track. A cuckoo filter matches on account-data updates, so list
// accounts that actually change (mints, token accounts, oracles). The payoff
// grows with the set: it stays ~3 bytes per key, so tracking millions is cheap.
const TRACKED_ACCOUNTS: &[&str] = &[
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC mint
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", // USDT mint
];

#[tokio::main]
async fn main() -> anyhow::Result<()> {
// tonic pulls in rustls 0.23, which no longer auto-selects a crypto backend;
// install one before the first TLS handshake or connect() panics.
rustls::crypto::ring::default_provider()
.install_default()
.map_err(|_| anyhow::anyhow!("failed to install rustls crypto provider"))?;

let pubkeys: Vec<Pubkey> = TRACKED_ACCOUNTS
.iter()
.map(|s| Pubkey::from_str(s).map_err(|e| anyhow::anyhow!("invalid pubkey {s}: {e}")))
.collect::<anyhow::Result<_>>()?;

// Leave headroom above the entry count so inserts don't saturate the table.
let mut filter = CompressedAccountFilterSet::with_capacity(pubkeys.len().max(100))?;
for pk in &pubkeys {
filter.insert(*pk)?;
}
println!("Tracking {} accounts via cuckoo filter", filter.len());

let mut client = GeyserGrpcClient::build_from_shared(ENDPOINT.to_string())?
.x_token(Some(TOKEN))?
.tls_config(ClientTlsConfig::new().with_native_roots())?
.accept_compressed(tonic::codec::CompressionEncoding::Gzip)
// .accept_compressed(tonic::codec::CompressionEncoding::Zstd)
.connect()
.await?;

// Attach the cuckoo filter to an accounts subscription; the server matches
// incoming account updates against it.
let mut request = SubscribeRequest {
commitment: Some(CommitmentLevel::Processed as i32),
..Default::default()
};
filter.insert_into_subscribe_request(&mut request, "cuckoo_accounts");

let mut stream = client.subscribe_once(request).await?;
println!("Subscribed with cuckoo filter; waiting for matching account updates...");

while let Some(update) = stream.next().await {
match update?.update_oneof {
Some(UpdateOneof::Account(acc)) => {
let Some(info) = acc.account else { continue };

// The server-side cuckoo check has a ~1% false positive rate.
// contains() is an exact membership test, so use it to confirm.
let matched = <[u8; 32]>::try_from(info.pubkey.as_slice())
.map(|bytes| filter.contains(Pubkey::new_from_array(bytes)))
.unwrap_or(false);

if matched {
println!(
"Account update: {} (slot {})",
bs58::encode(&info.pubkey).into_string(),
acc.slot,
);
}
}
Some(UpdateOneof::Ping(_)) => println!("Ping received - connection alive"),
_ => {}
}
}

Ok(())
}

Bewährte Verfahren

1. Clientseitige Überprüfung

Überprüfen Sie die Übereinstimmungen stets noch einmal anhand Ihres genauen öffentlichen Schlüsselsatzes. Die serverseitige Cuckoo-Übereinstimmung weist eine Falsch-Positiv-Rate von ca. 1 % auf, während enthält überprüft die lokale exakte Menge:

if filter.contains(incoming_pubkey) {
// This is a real match from your tracked set.
}

2. Filterkapazität

Passen Sie die Größe des Filters an die Anzahl der von Ihnen verfolgten öffentlichen Schlüssel an und planen Sie etwas Spielraum für zukünftige Einträge ein. Die Kapazität ist die maximale Anzahl an Einträgen, die der Filter aufnehmen kann:

lass mut filter = CompressedAccountFilterSet::with_capacity(pubkeys.len().max(100))?;

3. Dynamische Aktualisierungen

Cuckoo-Filter unterstützen Einfüge- und Löschvorgänge in O(1), sodass Sie eine große Menge anpassen können, ohne sie neu aufbauen zu müssen:

filter.insert(new_pubkey)?;
filter.remove(old_pubkey);
filter.insert_into_block_subscribe_request(&mut request, "tracked_blocks");

4. Wählen Sie die richtige Abonnementart


  • Verwenden Sie die Konten Abonnement (cuckoo_accounts_filter), um Änderungen des Kontostatus zu verfolgen.
  • Verwenden Sie die Blöcke Abonnement (cuckoo_account_include), um Transaktionsreferenzen nachzuverfolgen.

Anforderungen


  • Proto-Version: grpc (v12.4.0 oder höher)
  • Client-Bibliothek: Das Rust-Crate grpc oder der TypeScript-Client grpc (Version 5.0.9 oder höher).

Ressourcen