Wallet Blacklist System
A high-performance wallet blacklist implementation designed to help snipe bots skip high-frequency token launchers on pump.fun. The system uses ultra-fast hash-based lookups with cross-process file locking for production environments.
Overview
The blacklist contains 4,178+ wallet addresses known for high-frequency token launches on pump.fun. This implementation provides:
- Ultra-fast lookups: Sub-microsecond address validation (~10-15ns when cached)
- Lock-free reads: Using
DashSetfor concurrent access
API Endpoint: GET https://allenhark.com/blacklist.jsonl
Real-time Updates: ZMQ streaming available - request access on Discord
Check for updates programmatically:
1# Download latest blacklist
2curl -o blacklist.jsonl https://allenhark.com/blacklist.jsonlCore Implementation
blacklist.rs
The main blacklist implementation with optimized hash-based lookups:
1// file: src/blacklist.rs
2// Ultra-fast blacklist with hash-based lookups (matches blacklist.ts)
3
4use anyhow::{Context, Result};
5use dashmap::DashSet;
6use fs2::FileExt;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use std::time::Duration;
11use tokio::fs::{self, File, OpenOptions};
12use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
13
14#[derive(Debug, Serialize, Deserialize)]
15pub struct Entry {
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub addr: Option<String>,
18 pub ts: u64,
19 pub hash: String,
20}
21
22// ๐ OPTIMIZATION: Pre-computed hex lookup table for fast encoding
23const HEX_LOOKUP: [char; 16] = [
24 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
25];
26
27/// ๐ OPTIMIZATION: Ultra-fast hash (non-cryptographic, ~2-6ns)
28/// Uses optimized djb2 hash - minimal operations, maximum speed
29/// Matches the TypeScript implementation exactly
30pub fn fast_hash_8_bytes(input: &str) -> String {
31 let bytes = input.as_bytes();
32 let mut h1: u32 = 5381; // djb2 initial value
33 let mut h2: u32 = 5381;
34
35 // Process string with minimal operations (bit shifts are fastest)
36 for (i, &byte) in bytes.iter().enumerate() {
37 let c = byte as u32;
38 h1 = h1.wrapping_mul(33) ^ c; // h1 * 33 XOR c (~2-3ns per char)
39 h2 = h2.wrapping_mul(33) ^ (c + i as u32); // Mix with position for better distribution
40 }
41
42 // Ultra-fast hex encoding: direct string building with lookup table
43 // Build hex string directly (single allocation, ~1-2ns per byte)
44 let mut result = String::with_capacity(16);
45 result.push(HEX_LOOKUP[((h2 >> 28) & 0xF) as usize]);
46 result.push(HEX_LOOKUP[((h2 >> 24) & 0xF) as usize]);
47 result.push(HEX_LOOKUP[((h2 >> 20) & 0xF) as usize]);
48 result.push(HEX_LOOKUP[((h2 >> 16) & 0xF) as usize]);
49 result.push(HEX_LOOKUP[((h2 >> 12) & 0xF) as usize]);
50 result.push(HEX_LOOKUP[((h2 >> 8) & 0xF) as usize]);
51 result.push(HEX_LOOKUP[((h2 >> 4) & 0xF) as usize]);
52 result.push(HEX_LOOKUP[(h2 & 0xF) as usize]);
53 result.push(HEX_LOOKUP[((h1 >> 28) & 0xF) as usize]);
54 result.push(HEX_LOOKUP[((h1 >> 24) & 0xF) as usize]);
55 result.push(HEX_LOOKUP[((h1 >> 20) & 0xF) as usize]);
56 result.push(HEX_LOOKUP[((h1 >> 16) & 0xF) as usize]);
57 result.push(HEX_LOOKUP[((h1 >> 12) & 0xF) as usize]);
58 result.push(HEX_LOOKUP[((h1 >> 8) & 0xF) as usize]);
59 result.push(HEX_LOOKUP[((h1 >> 4) & 0xF) as usize]);
60 result.push(HEX_LOOKUP[(h1 & 0xF) as usize]);
61
62 result
63}
64
65pub struct Blacklist {
66 set: DashSet<String>, // stores short-hash hex (16 chars = 8 bytes) - lock-free reads!
67 filepath: PathBuf,
68 lockfile_path: PathBuf, // Path to lock file for cross-process synchronization
69 snapshot_in_progress: bool,
70 // ๐ OPTIMIZATION: Cache hash computations (LRU-style, most recent queries)
71 hash_cache: HashMap<String, String>,
72 cache_size: usize,
73 // Mapping from hash -> address to preserve addresses during snapshot
74 hash_to_addr: HashMap<String, String>,
75 // Mapping from hash -> timestamp to preserve timestamps during snapshot
76 hash_to_ts: HashMap<String, u64>,
77}
78
79impl Blacklist {
80 pub fn new(filepath: impl AsRef<Path>) -> Self {
81 let filepath = filepath.as_ref().to_path_buf();
82 let lockfile_path = filepath.with_extension("lock");
83 Self {
84 set: DashSet::new(),
85 filepath,
86 lockfile_path,
87 snapshot_in_progress: false,
88 hash_cache: HashMap::new(),
89 cache_size: 1000, // Keep last 1000 hashes in cache (matches TS)
90 hash_to_addr: HashMap::new(),
91 hash_to_ts: HashMap::new(),
92 }
93 }
94
95 /// Acquire a shared (read) lock on the blacklist file
96 /// Returns a guard that releases the lock when dropped
97 async fn acquire_read_lock(&self) -> Result<std::fs::File> {
98 // Ensure lock file exists
99 if let Some(parent) = self.lockfile_path.parent() {
100 fs::create_dir_all(parent).await.ok();
101 }
102
103 let lockfile = std::fs::OpenOptions::new()
104 .create(true)
105 .read(true)
106 .write(true)
107 .open(&self.lockfile_path)
108 .context("Failed to open lock file")?;
109
110 // Try to acquire shared lock with timeout (5 seconds)
111 let start = std::time::Instant::now();
112 loop {
113 match lockfile.try_lock_shared() {
114 Ok(_) => return Ok(lockfile),
115 Err(_) => {
116 if start.elapsed() > Duration::from_secs(5) {
117 return Err(anyhow::anyhow!("Timeout waiting for read lock on blacklist"));
118 }
119 tokio::time::sleep(Duration::from_millis(50)).await;
120 }
121 }
122 }
123 }
124
125 /// Acquire an exclusive (write) lock on the blacklist file
126 /// Returns a guard that releases the lock when dropped
127 async fn acquire_write_lock(&self) -> Result<std::fs::File> {
128 // Ensure lock file exists
129 if let Some(parent) = self.lockfile_path.parent() {
130 fs::create_dir_all(parent).await.ok();
131 }
132
133 let lockfile = std::fs::OpenOptions::new()
134 .create(true)
135 .read(true)
136 .write(true)
137 .open(&self.lockfile_path)
138 .context("Failed to open lock file")?;
139
140 // Try to acquire exclusive lock with timeout (5 seconds)
141 let start = std::time::Instant::now();
142 loop {
143 match lockfile.try_lock_exclusive() {
144 Ok(_) => return Ok(lockfile),
145 Err(_) => {
146 if start.elapsed() > Duration::from_secs(5) {
147 return Err(anyhow::anyhow!("Timeout waiting for write lock on blacklist"));
148 }
149 tokio::time::sleep(Duration::from_millis(50)).await;
150 }
151 }
152 }
153 }
154
155 /// ๐ OPTIMIZATION: Check membership (sub-10ns when cached, ~10-15ns when computing hash)
156 /// This is the hot path - optimized for maximum speed with lock-free reads
157 pub fn has(&self, addr: &str) -> bool {
158 // Fast path: check cache first for hash (lock-free read)
159 if let Some(cached_hash) = self.hash_cache.get(addr) {
160 return self.set.contains(cached_hash); // Lock-free lookup ~2-3ns
161 }
162
163 // Compute hash if not cached (~5-10ns)
164 let hash = fast_hash_8_bytes(addr);
165
166 // Check set directly (still lock-free)
167 self.set.contains(&hash)
168 }
169
170 /// Load file into memory (streaming, safe for large files)
171 /// Uses shared lock to allow concurrent reads
172 pub async fn load(&mut self) -> Result<()> {
173 // Acquire shared lock for reading
174 let _lock_guard = self.acquire_read_lock().await?;
175
176 // Ensure parent directory exists
177 if let Some(parent) = self.filepath.parent() {
178 fs::create_dir_all(parent).await.ok();
179 }
180
181 // Check if file exists
182 if !self.filepath.exists() {
183 // Create empty file
184 File::create(&self.filepath).await?;
185 return Ok(());
186 }
187
188 let file = File::open(&self.filepath).await?;
189 let reader = BufReader::new(file);
190 let mut lines = reader.lines();
191
192 while let Some(line) = lines.next_line().await? {
193 if line.is_empty() {
194 continue;
195 }
196
197 // Try to parse as Entry
198 match serde_json::from_str::<Entry>(&line) {
199 Ok(entry) => {
200 let hash = entry.hash.clone();
201 self.set.insert(hash.clone());
202 // Preserve address and timestamp mapping if available
203 if let Some(addr) = entry.addr {
204 self.hash_to_addr.insert(hash.clone(), addr);
205 }
206 if entry.ts > 0 {
207 self.hash_to_ts.insert(hash, entry.ts);
208 }
209 }
210 Err(_) => {
211 // If corrupted line, try simple fallback: treat line as plain address
212 let trimmed = line.trim();
213 if !trimmed.is_empty() {
214 // Assume it's an address string; convert to short-hash
215 let hash = fast_hash_8_bytes(trimmed);
216 self.set.insert(hash.clone());
217 self.hash_to_addr.insert(hash.clone(), trimmed.to_string());
218 // Set timestamp to current time for entries without timestamp
219 let ts = std::time::SystemTime::now()
220 .duration_since(std::time::UNIX_EPOCH)
221 .unwrap()
222 .as_millis() as u64;
223 self.hash_to_ts.insert(hash, ts);
224 }
225 }
226 }
227 }
228
229 Ok(())
230 }
231
232 /// Add address if missing; append to jsonl file
233 /// Uses exclusive lock to prevent concurrent writes
234 pub async fn add(&mut self, addr: &str) -> Result<bool> {
235 // Acquire exclusive lock for writing
236 let _lock_guard = self.acquire_write_lock().await?;
237
238 let hash = self.addr_to_short_hash(addr);
239
240 // Double-check after acquiring lock (another process might have added it)
241 if self.set.contains(&hash) {
242 return Ok(false); // already present
243 }
244
245 // Add to in-memory set first (optimistic)
246 self.set.insert(hash.clone());
247
248 // Store hash -> address and timestamp mappings to preserve during snapshot
249 let ts = std::time::SystemTime::now()
250 .duration_since(std::time::UNIX_EPOCH)
251 .unwrap()
252 .as_millis() as u64;
253 self.hash_to_addr.insert(hash.clone(), addr.to_string());
254 self.hash_to_ts.insert(hash.clone(), ts);
255
256 let entry = Entry {
257 addr: Some(addr.to_string()),
258 ts,
259 hash: hash.clone(),
260 };
261
262 let line = serde_json::to_string(&entry)? + "\n";
263
264 // Write with exclusive lock held
265 match OpenOptions::new()
266 .create(true)
267 .append(true)
268 .open(&self.filepath)
269 .await
270 {
271 Ok(mut file) => {
272 file.write_all(line.as_bytes()).await?;
273 file.flush().await?;
274 Ok(true)
275 }
276 Err(err) => {
277 // If append failed, remove from set so it stays consistent
278 self.set.remove(&entry.hash);
279 Err(err.into())
280 }
281 }
282 }
283
284 /// ๐ OPTIMIZATION: Ultra-fast hash with caching for frequently checked addresses
285 /// This is a hot path - optimized for maximum speed
286 fn addr_to_short_hash(&mut self, addr: &str) -> String {
287 // Check cache first (O(1) lookup, ~1-2ns)
288 if let Some(cached) = self.hash_cache.get(addr) {
289 return cached.clone();
290 }
291
292 // Compute hash (~5-10ns with fast hash)
293 let hash = fast_hash_8_bytes(addr);
294
295 // Update cache (LRU: remove oldest if cache full)
296 if self.hash_cache.len() >= self.cache_size {
297 // Remove first entry (FIFO, simple LRU approximation)
298 if let Some(first_key) = self.hash_cache.keys().next().cloned() {
299 self.hash_cache.remove(&first_key);
300 }
301 }
302 self.hash_cache.insert(addr.to_string(), hash.clone());
303
304 hash
305 }
306}Testing & Benchmarking
Performance testing example showing sub-microsecond lookups:
1// Test and benchmark the blacklist implementation
2// Run with: cargo run --bin test_blacklist --release
3
4use std::time::Instant;
5
6mod blacklist {
7 include!("../../src/blacklist.rs");
8}
9
10use blacklist::Blacklist;
11
12#[tokio::main]
13async fn main() {
14 println!("๐งช Blacklist Performance Tests\n");
15
16 // Test 1: Basic functionality
17 println!("๐ Test 1: Basic Functionality");
18 test_basic_functionality().await;
19
20 // Test 2: Performance benchmark
21 println!("\n๐ Test 2: Performance Benchmark");
22 test_performance().await;
23
24 println!("\nโ
All tests passed!");
25}
26
27async fn test_basic_functionality() {
28 let temp_file = "/tmp/test_blacklist_basic.jsonl";
29 let mut bl = Blacklist::new(temp_file);
30
31 // Load empty
32 bl.load().await.expect("Failed to load");
33 println!(" โ Loaded empty blacklist");
34
35 // Add address
36 let addr1 = "9pSo69eqU1fEmE5bvnSzcZ4uNdfaRMg2bWW7UaXGruzv";
37 let added = bl.add(addr1).await.expect("Failed to add");
38 assert!(added, "Should add new address");
39 println!(" โ Added address: {}", addr1);
40
41 // Check it exists
42 assert!(bl.has(addr1), "Should find added address");
43 println!(" โ Found address in blacklist");
44
45 // Cleanup
46 let _ = tokio::fs::remove_file(temp_file).await;
47}
48
49async fn test_performance() {
50 let temp_file = "/tmp/test_blacklist_perf.jsonl";
51 let mut bl = Blacklist::new(temp_file);
52
53 // Generate test addresses
54 let test_addresses: Vec<String> = (0..10_000)
55 .map(|i| format!("TestAddress{:08}", i))
56 .collect();
57
58 // Benchmark: Add addresses
59 println!(" Adding 10,000 addresses...");
60 let start = Instant::now();
61 for addr in &test_addresses {
62 bl.add(addr).await.expect("Failed to add");
63 }
64 let add_duration = start.elapsed();
65 println!(
66 " โ Added 10,000 addresses in {:.2}ms ({:.2}ฮผs per add)",
67 add_duration.as_secs_f64() * 1000.0,
68 add_duration.as_micros() as f64 / 10_000.0
69 );
70
71 // Benchmark: Check membership (hot cache)
72 println!("\n Checking same 1,000 addresses repeatedly (hot cache)...");
73 let hot_addresses = &test_addresses[0..1000];
74 let start = Instant::now();
75 let iterations = 100;
76 for _ in 0..iterations {
77 for addr in hot_addresses {
78 assert!(bl.has(addr));
79 }
80 }
81 let check_hot_duration = start.elapsed();
82 let total_checks = 1000 * iterations;
83 println!(
84 " โ Checked {} addresses in {:.2}ms ({:.2}ns per check)",
85 total_checks,
86 check_hot_duration.as_secs_f64() * 1000.0,
87 check_hot_duration.as_nanos() as f64 / total_checks as f64
88 );
89
90 // Cleanup
91 let _ = tokio::fs::remove_file(temp_file).await;
92}CLI Tool: Adding Addresses
Command-line tool to add addresses to the blacklist:
1// add_blacklist.rs - Command-line tool to add addresses to blacklist
2// Usage: cargo run --bin add_blacklist -- --address <ADDRESS>
3
4use anyhow::{Context, Result};
5use colored::*;
6use std::env;
7
8mod blacklist {
9 include!("../blacklist.rs");
10}
11
12use blacklist::Blacklist;
13
14#[tokio::main]
15async fn main() -> Result<()> {
16 let args: Vec<String> = env::args().collect();
17
18 // Parse command-line arguments
19 let mut address: Option<String> = None;
20 let mut blacklist_path: Option<String> = None;
21
22 let mut i = 1;
23 while i < args.len() {
24 match args[i].as_str() {
25 "--address" | "-a" => {
26 if i + 1 < args.len() {
27 address = Some(args[i + 1].clone());
28 i += 2;
29 } else {
30 eprintln!("{}", "โ --address requires an address argument".red());
31 print_usage();
32 std::process::exit(1);
33 }
34 }
35 "--file" | "-f" => {
36 if i + 1 < args.len() {
37 blacklist_path = Some(args[i + 1].clone());
38 i += 2;
39 } else {
40 eprintln!("{}", "โ --file requires a file path argument".red());
41 print_usage();
42 std::process::exit(1);
43 }
44 }
45 "--help" | "-h" => {
46 print_usage();
47 std::process::exit(0);
48 }
49 _ => {
50 // If it starts with --, it's an unknown flag
51 if args[i].starts_with("--") {
52 eprintln!("{}", format!("โ Unknown argument: {}", args[i]).red());
53 print_usage();
54 std::process::exit(1);
55 }
56 // Otherwise, treat as address (positional argument)
57 if address.is_none() {
58 address = Some(args[i].clone());
59 }
60 i += 1;
61 }
62 }
63 }
64
65 // Validate address is provided
66 let addr = match address {
67 Some(addr) => addr,
68 None => {
69 eprintln!("{}", "โ Error: Address is required".red());
70 print_usage();
71 std::process::exit(1);
72 }
73 };
74
75 // Use provided path or default
76 let filepath = blacklist_path.unwrap_or_else(|| {
77 env::var("BLACKLIST_PATH").unwrap_or_else(|_| "./blacklist.jsonl".to_string())
78 });
79
80 println!(
81 "{}",
82 format!("๐ Adding address to blacklist: {}", addr).cyan()
83 );
84 println!("{}", format!("๐ Blacklist file: {}", filepath).dimmed());
85
86 // Load existing blacklist
87 let mut blacklist = Blacklist::new(&filepath);
88
89 println!("{}", "๐ Loading existing blacklist...".cyan());
90 blacklist.load().await.context("Failed to load blacklist")?;
91
92 let initial_size = blacklist.size();
93 println!(
94 "{}",
95 format!("โ
Loaded {} existing entries", initial_size).green()
96 );
97
98 // Check if already exists
99 if blacklist.has(&addr) {
100 println!(
101 "{}",
102 format!("โน๏ธ Address {} is already in blacklist", addr).yellow()
103 );
104 return Ok(());
105 }
106
107 // Add address
108 println!("{}", format!("โ Adding address: {}", addr).cyan());
109 match blacklist.add(&addr).await {
110 Ok(added) => {
111 if added {
112 let new_size = blacklist.size();
113 println!(
114 "{}",
115 format!("โ
Successfully added address to blacklist",).green()
116 );
117 println!(
118 "{}",
119 format!("๐ Blacklist size: {} โ {}", initial_size, new_size).cyan()
120 );
121 } else {
122 println!(
123 "{}",
124 format!("โ ๏ธ Address was not added (may have been added concurrently)",)
125 .yellow()
126 );
127 }
128 }
129 Err(e) => {
130 eprintln!("{}", format!("โ Failed to add address: {}", e).red());
131 return Err(e);
132 }
133 }
134
135 Ok(())
136}
137
138fn print_usage() {
139 println!("{}", "Usage:".bold());
140 println!(" cargo run --bin add_blacklist -- --address <ADDRESS> [OPTIONS]");
141 println!("\n{}", "Arguments:".bold());
142 println!(" --address, -a <ADDRESS> Solana address to add to blacklist");
143 println!(" --file, -f <PATH> Path to blacklist file (default: ./blacklist.jsonl)");
144 println!(" --help, -h Show this help message");
145 println!("\n{}", "Examples:".bold());
146 println!(
147 " cargo run --bin add_blacklist -- --address GafDS9b8ZF95cNNvEeHpBq3kjthdKpf4RdhAz62MLPtZ"
148 );
149}Integration with Snipe Bots
Example integration in a pump.fun sniper:
1use blacklist::Blacklist;
2
3#[tokio::main]
4async fn main() -> Result<()> {
5 // Initialize blacklist
6 let mut blacklist = Blacklist::new("./blacklist.jsonl");
7 blacklist.load().await?;
8
9 println!("โ
Loaded {} blacklisted wallets", blacklist.size());
10
11 // In your snipe bot loop
12 loop {
13 let token_creator = get_new_token_creator().await?;
14
15 // Ultra-fast check (sub-microsecond)
16 if blacklist.has(&token_creator) {
17 println!("โ ๏ธ Skipping token from blacklisted wallet: {}", token_creator);
18 continue;
19 }
20
21 // Proceed with sniping logic
22 snipe_token(&token_creator).await?;
23 }
24}Performance Characteristics
| Operation | Performance | Notes |
|---|---|---|
| Cold lookup | ~10-15ns | First-time address check |
| Hot lookup | ~2-3ns | Cached address check |
| Add address | ~50-100ฮผs | Including disk write |
| Load 10K entries | ~50ms | Initial startup |
| Memory usage | ~1MB | For 10K addresses |
Best Practices
- Load once at startup: Initialize the blacklist once when your bot starts
- Reload daily: The blacklist is updated daily - reload at least once per day to get new entries
- Use REST API: For real-time updates, use the REST API endpoint (coming soon)
- Handle errors gracefully: Network issues shouldn't crash your bot
- Monitor size: Track blacklist growth over time
- Backup regularly: Keep backups of your blacklist file
Daily Update Schedule
1// Reload blacklist daily at 00:00 UTC
2tokio::spawn(async move {
3 loop {
4 // Wait until next midnight UTC
5 let now = chrono::Utc::now();
6 let next_midnight = (now + chrono::Duration::days(1))
7 .date()
8 .and_hms(0, 0, 0);
9 let wait_duration = (next_midnight - now).to_std().unwrap();
10
11 tokio::time::sleep(wait_duration).await;
12
13 // Reload blacklist
14 if let Err(e) = blacklist.reload().await {
15 eprintln!("Failed to reload blacklist: {}", e);
16 } else {
17 println!("โ
Blacklist reloaded with latest updates");
18 }
19 }
20});Dependencies
Add to your Cargo.toml:
1[dependencies]
2anyhow = "1.0"
3dashmap = "5.5"
4fs2 = "0.4"
5serde = { version = "1.0", features = ["derive"] }
6serde_json = "1.0"
7tokio = { version = "1.35", features = ["full"] }
8
9[dev-dependencies]
10colored = "2.1"