Protect your transactions from MEV with Yellowstone Shield and the TPU client

Protect your transactions from MEV with Yellowstone Shield and the TPU client

TL;DR

Solana’s deterministic leader schedule means you often know exactly which validator will process your transaction. If that leader is a bad actor, your transaction still routes to them by default, leaving you exposed to sandwiching, frontrunning, censorship, and slow leaders.

Yellowstone Shield fixes this by introducing on-chain allowlists and blocklists for validators. When combined with the Yellowstone Jet TPU Client, your application connects directly to validator TPUs, checks the current leader against Shield policies, and only sends transactions to validators you trust. 

The result is MEV-resistant, RPC-independent transaction routing with full control, direct QUIC delivery, and production-grade performance on Solana.

Introduction: MEV in Solana transaction routing

When you send a transaction on Solana, it needs to reach the current block leader for processing. But what happens when that leader is a bad actor? Some validators engage in harmful practices like:

  • Sandwich attacks - Inserting transactions before and after yours to extract value
  • Frontrunning - Copying your transaction and executing it first for profit
  • Transaction censorship - Selectively dropping transactions
  • Poor performance - Slow block production that delays your transactions

Traditional transaction sending gives you no control over which validators process your transactions. If the current leader is a bad actor, your transaction goes to them anyway.

That's where Yellowstone Shield comes in.

How Yellowstone Shield prevents validator-level MEV

Yellowstone Shield is an on-chain access control framework that lets you decide which validators can (and can't) handle your transactions. It's a Solana program that manages:

  • Allowlists - Only send to trusted validators
  • Blocklists - Never send to known bad actors

Each policy lives on-chain as a Program Derived Address (PDA) and can be used by anyone who knows the policy address.

Community-maintained validator allow- and blocklists

The community has already created several Shield policies. Check out validators.app/yellowstone-shield to see policies like:

Policy

Type

Validators

Purpose

Top 25 Validators by Stake (TV25)

Allow

25

Only send to top validators

Sandwiched.me Sandwicher List (DROPS)

Deny

3

Block known MEV sandwichers

Jito Drop List (DROPJ)

Deny

17

Block Jito blocklist validators

Marinade Drop List (DROPM)

Deny

73

Block Marinade's blocklist

Slow Block Producers (SLOWAF)

Deny

22

Avoid slow validators

You can use these existing policies or create your own!

Limitations of RPC-based Shield (Cascade feature)

If you're using Triton RPC with Cascade, you can already use Shield by adding a policy to your sendTransaction RPC call:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "sendTransaction",
  "params": [
    "<base64_transaction>",
    {
      "encoding": "base64",
      "forwardingPolicies": ["<your_policy_pda>"]
    }
  ]
}

But this approach has limitations:

  • Requires RPC provider to support Shield
  • You're dependent on the RPC's forwarding logic
  • No direct control over transaction routing
  • Can't use with custom transaction senders

You can check full comparison with TPU client Shield here.

Why use the Shield with Yellowstone Jet TPU client

The Yellowstone Jet TPU Client (yellowstone-jet-tpu-client) gives you direct control. Instead of relying on an RPC, your application:

  1. Connects directly to the validators' TPU (Transaction Processing Unit)
  2. Checks the current leader against your Shield policies
  3. Only sends transactions to allowed validators
  4. Automatically skips blocked validators
  5. Works with any Solana network (mainnet, devnet, testnet)

This is the state-of-the-art approach for sending transactions with MEV protection.

Part 1: Create a Shield policy (or use existing)

Option A: Use an existing policy

The easiest way to get started is to use a community policy. For example, to block known MEV sandwichers:

use solana_pubkey::Pubkey;
use std::str::FromStr;

// Sandwiched.me Sandwicher List (DROPS) - blocks 3 known MEV attackers
let shield_policy = Pubkey::from_str("xMTozeQTEX2MR9KUon8vjg17U5Q9459RSASE3wy5eNB")
    .expect("valid pubkey");

Browse more policies at: validators.app/yellowstone-shield

Option B: Create your own policy

If you want custom control, create your own policy using the Yellowstone Shield CLI.

Step 1: Install Shield CLI

git clone https://github.com/rpcpool/yellowstone-shield
cd yellowstone-shield
cargo build --release --bin yellowstone-shield-cli

Step 2: Prepare policy metadata

Create a JSON file describing your policy:

{
  "name": "My MEV Blocklist",
  "symbol": "MEVBLOCK",
  "description": "Blocks validators known for sandwich attacks",
  "image": "https://the-image-url.com/shield.png",
  "external_url": "https://your-website.com"
}

Upload to IPFS/Arweave and save the URL.

Step 3: Create the policy

# For a blocklist (deny these validators)
./target/release/yellowstone-shield-cli policy create \
  --strategy Deny \
  --name "My MEV Blocklist" \
  --symbol "MEVBLOCK" \
  --uri "https://the-metadata-url.json"

# Output: Policy Mint Address: <YOUR_POLICY_PDA>

Save the policy PDA - you'll need it!

Step 4: Add validators to your policy

Create validators-to-block.txt with one validator pubkey per line:

ValidatorPubkey1...
ValidatorPubkey2...
ValidatorPubkey3...

Add them to your policy:

./target/release/yellowstone-shield-cli identities add \
  --mint <YOUR_POLICY_PDA> \
  --identities-path validators-to-block.txt

Your policy is now live on-chain and ready to use!


Part 2: Setting up the TPU client with Shield

Now let's integrate Shield with the Yellowstone TPU Client.

Project setup

Due to Solana version requirements, you need to set up a Cargo workspace with patches. The yellowstone-jet-tpu-client requires Triton's patched Solana crates, and the workspace structure ensures all dependencies (including transitive ones from yellowstone-shield-store) use the same patched versions.

Create this structure:

Workspace root Cargo.toml:

[workspace]
members = ["example"]
resolver = "2"

[workspace.dependencies]
yellowstone-jet-tpu-client = { version = "0.1.0-rc4", features = ["yellowstone-grpc", "shield"] }
yellowstone-shield-store = "0.9.1"
yellowstone-grpc-client = "10.2.0"
solana-client = "3.0"
solana-keypair = "3.0"
solana-pubkey = "3.0"
solana-signature = "3.0"
solana-transaction = "3.0"
solana-message = "3.0"
solana-system-interface = { version = "3.0", features = ["bincode"] }
solana-commitment-config = "3.0"
solana-signer = "3.0"
tokio = { version = "1", features = ["full"] }
bincode = "1"
anyhow = "1"

[patch.crates-io]
# Required patches from Triton's Solana fork
solana-rpc-client = { git = "https://github.com/rpcpool/solana-public.git", tag = "v3.0.6-triton-public" }
solana-rpc-client-api = { git = "https://github.com/rpcpool/solana-public.git", tag = "v3.0.6-triton-public" }
solana-account-decoder = { git = "https://github.com/rpcpool/solana-public.git", tag = "v3.0.6-triton-public" }
solana-account-decoder-client-types = { git = "https://github.com/rpcpool/solana-public.git", tag = "v3.0.6-triton-public" }
solana-client = { git = "https://github.com/rpcpool/solana-public.git", tag = "v3.0.6-triton-public" }
solana-streamer = { git = "https://github.com/rpcpool/solana-public.git", tag = "v3.0.6-triton-public" }
solana-transaction-context = { git = "https://github.com/rpcpool/solana-public.git", tag = "v3.0.6-triton-public" }
solana-transaction-status = { git = "https://github.com/rpcpool/solana-public.git", tag = "v3.0.6-triton-public" }
solana-transaction-status-client-types = { git = "https://github.com/rpcpool/solana-public.git", tag = "v3.0.6-triton-public" }
solana-net-utils = { git = "https://github.com/rpcpool/solana-public.git", tag = "v3.0.6-triton-public" }
solana-tpu-client = { git = "https://github.com/rpcpool/solana-public.git", tag = "v3.0.6-triton-public" }
solana-quic-client = { git = "https://github.com/rpcpool/solana-public.git", tag = "v3.0.6-triton-public" }

Example project example/Cargo.toml:

[package]
name = "shield-tpu-example"
version = "0.1.0"
edition = "2021"

[dependencies]
yellowstone-jet-tpu-client = { workspace = true }
yellowstone-shield-store = { workspace = true }
yellowstone-grpc-client = { workspace = true }
solana-client = { workspace = true }
solana-keypair = { workspace = true }
solana-pubkey = { workspace = true }
solana-signature = { workspace = true }
solana-transaction = { workspace = true }
solana-message = { workspace = true }
solana-system-interface = { workspace = true }
solana-commitment-config = { workspace = true }
solana-signer = { workspace = true }
tokio = { workspace = true }
bincode = { workspace = true }
anyhow = { workspace = true }

Example source code example/src/main.rs: (see complete code below in Part 3)

Architecture overview

┌────────────────────────────────────────────────────────┐
│                  Your Application                      │
│                                                        │
│  1. Create Shield PolicyStore                          │
│  2. Create TPU Sender                                  │
│  3. Build & sign transaction                           │
│  4. Call send_txn_with_shield_policies()               │
└─────────────────┬──────────────────────────────────────┘
                  │
                  ▼
┌────────────────────────────────────────────────────────┐
│          Yellowstone TPU Client (Jet)                  │
│                                                        │
│  • Tracks current slot                                 │
│  • Knows leader schedule                               │
│  • Checks: Is current leader allowed?                  │
│    ├─ YES → Send transaction via QUIC                  │
│    └─ NO  → Skip (don't send)                          │
└────────────────────────────────────────────────────────┘

Part 3: Complete working example

Here's a full example that sends SOL transfers while blocking validators on your Shield policy.

use {
    solana_client::nonblocking::rpc_client::RpcClient,
    solana_commitment_config::CommitmentConfig,
    solana_keypair::Keypair,
    solana_message::{v0, VersionedMessage},
    solana_pubkey::Pubkey,
    solana_signer::Signer,
    solana_system_interface::instruction::transfer,
    solana_transaction::versioned::VersionedTransaction,
    std::{str::FromStr, sync::Arc},
    yellowstone_jet_tpu_client::{
        yellowstone_grpc::sender::{
            create_yellowstone_tpu_sender, Endpoints, ShieldBlockList,
        },
    },
    yellowstone_shield_store::{
        PolicyStore, PolicyStoreConfig, PolicyStoreRpcConfig, PolicyStoreGrpcConfig,
    },
};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Configuration
    let rpc_endpoint = "https://api.mainnet-beta.solana.com".to_string();
    let grpc_endpoint = "https://grpc.triton.one".to_string();
    let grpc_x_token = Some("the-triton-api-key".to_string());

    // Your identity keypair
    let identity = Keypair::new(); // In production: read from file
    println!("Using identity: {}", identity.pubkey());

    // Recipient for test transfer
    let recipient = Pubkey::new_unique();

    // STEP 1: Set Up Shield Policy Store

    // Configure the policy store
    let config = PolicyStoreConfig {
        rpc: PolicyStoreRpcConfig {
            endpoint: rpc_endpoint.clone(),
        },
        grpc: PolicyStoreGrpcConfig {
            endpoint: grpc_endpoint.clone(),
            x_token: grpc_x_token.clone(),
            commitment: None, // defaults to Confirmed
            timeout: std::time::Duration::from_secs(60),
            connect_timeout: std::time::Duration::from_secs(10),
            tcp_nodelay: true,
            http2_adaptive_window: true,
            http2_keep_alive: false,
            http2_keep_alive_interval: None,
            http2_keep_alive_timeout: None,
            http2_keep_alive_while_idle: None,
            max_decoding_message_size: Some(16 * 1024 * 1024), // 16 MiB
            initial_connection_window_size: None,
            initial_stream_window_size: None,
        },
    };

    let policy_store = PolicyStore::build()
        .config(config)
        .run()
        .await?;

    // Use existing community policy (Sandwiched.me MEV blocklist)
    let shield_policy_pubkey = Pubkey::from_str(
        "xMTozeQTEX2MR9KUon8vjg17U5Q9459RSASE3wy5eNB"
    )?;
 
    // STEP 2: Create TPU Sender

    let endpoints = Endpoints {
        rpc: rpc_endpoint.clone(),
        grpc: grpc_endpoint,
        grpc_x_token,
    };

    let tpu_sender_result = create_yellowstone_tpu_sender(
        Default::default(),
        identity.insecure_clone(),
        endpoints,
    )
    .await?;

    let mut sender = tpu_sender_result.sender;
    let _related_objects_jh = tpu_sender_result.related_objects_jh;

    // STEP 3: Create RPC Client

    let rpc_client = Arc::new(RpcClient::new_with_commitment(
        rpc_endpoint,
        CommitmentConfig::confirmed(),
    ));

    // STEP 4: Build Transaction

    const LAMPORTS: u64 = 1000;
    let instructions = vec![transfer(
        &identity.pubkey(),
        &recipient,
        LAMPORTS,
    )];

    let latest_blockhash = rpc_client
        .get_latest_blockhash()
        .await?;

    let transaction = VersionedTransaction::try_new(
        VersionedMessage::V0(
            v0::Message::try_compile(
                &identity.pubkey(),
                &instructions,
                &[],
                latest_blockhash,
            )?,
        ),
        &[&identity],
    )?;

    let signature = transaction.signatures[0];
    let bincoded_txn = bincode::serialize(&transaction)?;

    // STEP 5: Send with Shield Protection

    let shield_blocklist = ShieldBlockList {
        policy_store: &policy_store,
        shield_policy_addresses: &[shield_policy_pubkey],
        default_return_value: true, // Allow sending when policy check fails
    };

    match sender
        .send_txn_with_shield_policies(
            signature,
            bincoded_txn,
            shield_blocklist,
        )
        .await
    {
        Ok(_) => {
            println!("Transaction sent successfully!");
            println!("Signature: {}", signature);
            println!("Current leader was NOT on blocklist");
        }
        Err(e) => {
            println!("Transaction NOT sent: {:?}", e);
            println!("Current leader was blocked by Shield policy");
            println!("Transaction was dropped (not forwarded)");
        }
    }

    Ok(())
}

Key points in the code:

  1. PolicyStoreConfig (lines 258-278): Must be manually configured with RPC and gRPC endpoints
  2. PolicyStore (lines 280-283): Caches Shield policies from on-chain data and keeps them updated via gRPC
  3. ShieldBlockList (lines 159-163): Links policy store with your policy PDAs
  4. send_txn_with_shield_policies() (lines 166-174): Sends transaction ONLY if current leader is allowed
  5. default_return_value: true (line 162): If policy check fails (network issue, etc.), allow sending (fail-open)

Part 4: How it works under the hood

Transaction flow with Shield

1. You call: send_txn_with_shield_policies(sig, txn, shield)
                              │
                              ▼
2. TPU Client checks current slot
                              │
                              ▼
3. Gets current leader from schedule
                              │
                              ▼
4. Shield check: Is leader allowed?
   ├─ Policy Store → snapshot()
   └─ Calls: is_allowed([policies], &leader_pubkey)
                              │
                ┌─────────────┴─────────────┐
                │                           │
                ▼                           ▼
                YES                         NO
   5a. Send transaction          5b. Return SendError
       via QUIC to leader            RemotePeerBlocked
                                     (transaction dropped)

Shield policy checking logic

From yellowstone-jet/crates/tpu-client/src/yellowstone_grpc/sender.rs:437-445:

impl Blocklist for ShieldBlockList<'_> {
    fn is_blocked(&self, peer_address: &Pubkey) -> bool {
        // Check if validator is allowed by ANY of the policies
        !self
            .policy_store
            .snapshot()  // Get latest cached policy data
            .is_allowed(self.shield_policy_addresses, peer_address)
            .unwrap_or(self.default_return_value)
    }
}

For Deny Lists (Blocklists):

  • If validator IS in the deny list → is_blocked() = true → Transaction NOT sent
  • If validator is NOT in the deny list → is_blocked() = false → Transaction sent

For Allow Lists (Allowlists):

  • If validator IS in the allow list → is_blocked() = false → Transaction sent
  • If validator is NOT in the allow list → is_blocked() = true → Transaction NOT sent

PolicyStore: Efficient on-chain caching

The PolicyStore continuously syncs Shield policies from the blockchain:

// From yellowstone-shield-store
let policy_store = PolicyStore::build()
    .config(config)  // Use the config from Part 3
    .run()
    .await?;

// Later: Get current snapshot (fast, lock-free)
let snapshot = policy_store.snapshot();

// Check if validator is allowed
match snapshot.is_allowed(&[policy_pda], &validator_pubkey) {
    Ok(true) => println!("Validator allowed"),
    Ok(false) => println!("Validator blocked"),
    Err(e) => println!("Policy check failed: {:?}", e),
}

Part 5: Advanced usage

Using multiple Shield policies

You can apply multiple policies at once. The transaction is only sent if the leader passes ALL policies:

let policies = vec![
    // Deny known MEV attackers
    Pubkey::from_str("xMTozeQTEX2MR9KUon8vjg17U5Q9459RSASE3wy5eNB")?, // DROPS
    // AND deny slow validators
    Pubkey::from_str("7xTAiL3tTzgbDwsxMKi2rMXW7QKzcGrABsZc2tW6L6bj")?, // SLOWAF
];

let shield_blocklist = ShieldBlockList {
    policy_store: &policy_store,
    shield_policy_addresses: &policies,
    default_return_value: true,
};

sender.send_txn_with_shield_policies(signature, txn, shield_blocklist).await?;

Broadcast to specific validators

Send directly to specific validators (bypassing leader schedule):

let target_validators = vec![
    Pubkey::from_str("ValidatorA...")?,
    Pubkey::from_str("ValidatorB...")?,
];

sender.send_txn_many_dest(signature, bincoded_txn, &target_validators).await?;

Handling blocked leaders

When a leader is blocked, your transaction is dropped (not queued). You have several options:

Option 1: Retry with fanout

// Try current leader first
if sender.send_txn_with_shield_policies(sig, txn.clone(), shield).await.is_err() {
    // Current leader blocked, try next 2 leaders
    sender.send_txn_fanout_with_blocklist(sig, txn, 3, Some(shield)).await?;
}

Option 2: Wait for the next slot

// Wait for next slot
tokio::time::sleep(std::time::Duration::from_millis(400)).await;
// Try again with new leader
sender.send_txn_with_shield_policies(sig, txn, shield).await?;

Option 3: Use fail-open mode

// If all leaders in next N slots are blocked, send anyway
let shield_failopen = ShieldBlockList {
    policy_store: &policy_store,
    shield_policy_addresses: &policies,
    default_return_value: true,  // Send even if leader is blocked
};

Conclusion

Yellowstone Shield + TPU Client gives you state-of-the-art transaction sending with built-in MEV protection. By combining:

  1. On-chain Shield policies - Community-maintained or custom blocklists/allowlists
  2. PolicyStore - Efficient caching and checking of policies
  3. Yellowstone TPU Client - Direct validator connections with automatic blocking

You get:

  • Full control over which validators handle your transactions
  • Protection from known MEV attackers
  • Direct QUIC connections to validators
  • Automatic leader schedule tracking
  • Production-ready Rust implementation
Feature RPC Shield (Cascade) TPU Client Shield
Setup complexity Easy (just add param) Moderate (requires Rust)
RPC dependency Requires Shield-enabled RPC No RPC dependency
Direct validator connection Through RPC Direct QUIC connections
Customization Limited to RPC's logic Full control
Works without internet No Yes (with local validator)
Transaction prioritization RPC-controlled Your control
Best for Quick integration, web apps High-performance apps, MEV bots

Resources