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:
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:
- Connects directly to the validators' TPU (Transaction Processing Unit)
- Checks the current leader against your Shield policies
- Only sends transactions to allowed validators
- Automatically skips blocked validators
- 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:
- PolicyStoreConfig (lines 258-278): Must be manually configured with RPC and gRPC endpoints
- PolicyStore (lines 280-283): Caches Shield policies from on-chain data and keeps them updated via gRPC
- ShieldBlockList (lines 159-163): Links policy store with your policy PDAs
- send_txn_with_shield_policies() (lines 166-174): Sends transaction ONLY if current leader is allowed
- 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:
- On-chain Shield policies - Community-maintained or custom blocklists/allowlists
- PolicyStore - Efficient caching and checking of policies
- 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
- Yellowstone Shield GitHub: https://github.com/rpcpool/yellowstone-shield
- Yellowstone Jet GitHub: https://github.com/rpcpool/yellowstone-jet
- Shield Blog Post: Introducing Yellowstone Shield
- Policy Explorer: https://www.validators.app/yellowstone-shield
- Rust Crate: https://crates.io/crates/yellowstone-shield-store
- Triton Docs: https://docs.triton.one/project-yellowstone/shield-transaction-policies