Ir al contenido principal

Compressed Filters (Cuckoo Filters)

Actualizado el
2 de julio de 2026
Solana gRPC está incluido en los planes Scale y Business. En los planes Build y Accelerate, sigue estando disponible a través del complemento Solana gRPC.

Resumen

Compressed filters (also known as cuckoo filters) let you track millions of accounts in a single Solana gRPC subscription without hitting pubkey limits. Instead of sending an explicit list of pubkeys, you send a compact binary filter that the server uses to match incoming updates.

A standard Solana gRPC subscription carries an explicit list of the pubkeys you want to match, and that list grows large fast: one million pubkeys is roughly 44 MB per request. Tracking hundreds or thousands of accounts (e.g., 500 wallets plus 700 token accounts) quickly exceeds those limits.

A compressed filter replaces that explicit list with a probabilistic data structure that encodes the same set in a fraction of the space:


  • Bypasses pubkey limits: bounded only by message size (~99 MiB, which supports tens of millions of pubkeys).
  • Reduces payload size by ~10x compared to an explicit pubkey list.
  • Supports O(1) insert and remove operations, so you can update a large set in place instead of rebuilding it from scratch.

What Is a Compressed Filter?

A compressed filter is a probabilistic data structure that tests whether a pubkey belongs to a set without storing the full set. Instead of keeping each 32-byte pubkey, it stores a short fingerprint (a small hash) of each one in a table of buckets, cutting the space needed to represent the set by roughly 10x.

Account CountCompressed FilterExplicit Pubkey List
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

Cómo funciona

Building and using a compressed filter follows the same lifecycle regardless of language:


  1. Client builds the filter: Insert every pubkey you want to track into a CompressedAccountFilterSet. The set keeps an exact copy of your pubkeys locally and a compact cuckoo filter for the wire.
  2. Filter is serialized: The cuckoo filter is encoded as a binary blob plus metadata (bucket count, entries per bucket, fingerprint bits, and hash seed) and attached to your subscription request.
  3. Server matches probabilistically: For each incoming update, the server hashes the pubkey and checks whether its fingerprint exists in your filter, forwarding the update if it matches.
  4. Client verifies matches: Because the server-side check has a ~1% false positive rate, your client re-checks each match against its exact pubkey set before acting on it.

Filter Structure

The compressed filter is sent as part of your subscription request:

{
"cuckooAccountInclude": {
"data": "AAAAAAAAAACJdcGaAAAAAAAAAAAASsE3BAAAADxkwAAAAAAAAAAAA...",
"bucketCount": 16,
"entriesPerBucket": 4,
"fingerprintBits": 16,
"hashSeed": "8749487436367949345",
"hashAlgorithm": "SIP_HASH"
}
}
FieldDescripción
datosBase64-encoded bucket data
bucketCountNumber of buckets in the filter
entriesPerBucketSlots per bucket (typically 4)
fingerprintBitsBits per fingerprint (8, 12, or 16)
hashSeedSeed for the hash function (SipHash-2-4)
hashAlgorithmHashing algorithm identifier (currently SIP_HASH)

Subscription Types

Compressed filters can be attached to two subscription types, each firing on a different event:


  • Accounts Filter (cuckoo_accounts_filter): Fires when a tracked account's own state is written to. Use this when you want to monitor account data changes.
  • Blocks Filter (cuckoo_account_include): Fires when any tracked pubkey is referenced in a transaction's account keys, including program IDs used via CPI. Use this for transaction monitoring.

CompressedAccountFilterSet (from the cuckoo module in the yellowstone-grpc-proto crate) provides a method for each:

// 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 Example

The example below builds a cuckoo filter and attaches it to an Accounts subscription to track account-state changes for specific accounts. The server matches incoming account updates against the filter, then the client re-verifies each match against its exact pubkey set.

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 (with the :10000 port) and a token.
// Example: https://docs-demo.solana-mainnet.quiknode.pro:10000 + abcde123456789
const ENDPOINT: &str = "https://your-solana-grpc-endpoint.quiknode.pro:10000";
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())?
.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(())
}

Buenas prácticas

1. Client-Side Verification

Always re-check matches against your exact pubkey set. The server-side cuckoo match has a ~1% false positive rate, while contains checks the local exact set:

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

2. Filter Capacity

Size the filter for the number of pubkeys you track, plus a little headroom for future inserts. Capacity is the maximum number of entries the filter can hold:

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

3. Dynamic Updates

Cuckoo filters support O(1) insert and remove operations, so you can adjust a large set without rebuilding it:

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

4. Choose the Right Subscription Type


  • Utiliza el Cuentas subscription (cuckoo_accounts_filter) to track account state changes.
  • Utiliza el Bloques subscription (cuckoo_account_include) to track transaction references.

Requirements


Recursos


We Love Feedback!

Si tienes algún comentario o pregunta sobre esta documentación, háznoslo saber. ¡Nos encantaría saber tu opinión!

Comparte este documento