Si le module Inventory est la « mémoire » du robot, alors le module Scout est ses « yeux ». Dans le flux turbulent de Solana, où des changements d'état se produisent des dizaines de milliers par seconde, la tâche de Scout est de filtrer, décoder et sélectionner rapidement les signaux véritablement significatifs pour les stratégies d'arbitrage.
Dans le monde des MEV, la vitesse n'est pas tout, mais sans vitesse, rien n'est possible. Cet article explore en profondeur la construction d'un système d'écoute et d'analyse de transactions à faible latence et haute concurrence.
1. Philosophie de l'écoute : le bistouri vs. le grand filet
Sur Solana, nous sommes généralement confrontés à deux besoins d'écoute radicalement différents, correspondant à des approches techniques distinctes :
1.1 accountSubscribe : un scalpel précis (mode Arb)
Pour l'arbitrage inter-protocoles (Arbitrage), nous avons déjà verrouillé des pools spécifiques à travers l'Inventory. À ce moment-là, nous n'avons pas besoin d'observer l'ensemble du réseau, nous devons juste garder un œil attentif sur les changements dans le champ Data des comptes de ces pools.
Mécanisme : une fois que le solde ou le prix des tokens dans le pool change, le nœud RPC poussera immédiatement les dernières données de compte.
Avantage : le signal est extrêmement direct, contournant l'analyse de transaction complexe, c'est le chemin le plus rapide pour l'arbitrage haute fréquence.
1.2 logsSubscribe : un grand filet couvrant tout le réseau (mode Sniper)
Pour le sniper de nouveaux pools, nous ne pouvons pas prédire l'adresse des pools, nous devons nous fier à l'écoute de protocoles spécifiques (comme Raydium ou Orca) à travers les Program Logs pour capturer les signaux d'instruction de « nouveau pool » ou « injection initiale de liquidité ».
Mécanisme : scanner les journaux pour des mots-clés spécifiques (comme initialize2).
Défi : le bruit est énorme, et une fois capturé, il faut généralement effectuer un traitement « lent » pour compléter les informations sur les tokens du pool (comme demander getTransaction).
2. Architecture principale : multiplexage de flux (Stream Multiplexing)
Dans un système mature, vous pourriez avoir besoin de vous abonner simultanément à des mises à jour de plusieurs centaines de pools. Si vous ouvrez un thread pour chaque abonnement, le coût du système explosera instantanément.
2.1 Fusion de flux asynchrone (Sélectionner Tout)
Nous utilisons l'écosystème asynchrone de Rust (Tokio + Futures), en utilisant select_all pour fusionner des centaines de flux d'abonnement WebSocket en un seul flux d'événements. C'est comme rassembler les images de centaines de caméras de surveillance sur un mur d'affichage, avec une boucle principale (Event Loop) traitant et distribuant tout.
2.2 Modèle de thread et détachement du « chemin lent »
La vitesse de réponse à la boucle principale détermine la limite de latence du système.
Chemin rapide (Hot Path) : recevoir des données -> décodage en mémoire -> déclencher des calculs.
Chemin lent (Long Path) : si des informations RPC supplémentaires sont nécessaires (comme en mode Sniper), il est impératif d'utiliser tokio::spawn pour immédiatement détacher l'exécution de la tâche en arrière-plan, interdisant toute blocage de la boucle principale d'écoute.
3. Analyse extrême : sauter les informations inutiles
Les données de compte de Solana (Account Data) sont généralement une chaîne de buffer binaire. La méthode inefficace consiste à les désérialiser en objets complets, tandis que la méthode extrême est «解析按需».
3.1 Zéro copie et localisation décalée
Par exemple, lors de l'écoute d'Orca Whirlpool, nous pouvons ne nécessiter que le sqrt_price et tick_current_index.
Nous n'avons pas besoin d'analyser l'état complet du pool (des centaines d'octets), nous avons juste besoin de lire directement 16 octets à un Offset (décalage) spécifique dans le flux de données.
En Rust, en utilisant bytemuck ou un simple décalage de pointeur, nous pouvons extraire des paramètres de tarification clés à des niveaux de microsecondes.
3.2 L'art des filtres
À l'étape logsSubscribe, en utilisant le filtre mentions fourni par RPC, nous pouvons filtrer 90 % des journaux non pertinents côté nœud, réduisant considérablement la pression IO réseau du côté du Searcher.
4. Points d'optimisation des performances : de l'implémentation d'ingénierie à la milliseconde
Abonnement par fragmentation (Sharding) : face à la limitation de connexion des nœuds RPC publics, Scout fragmentera automatiquement les pools en liste blanche, recevant des mises à jour simultanément via plusieurs connexions WebSocket, évitant ainsi la contre-pression d'une seule connexion.
Mécanisme de réduction du bruit : pour les pools à haute fréquence, réaliser une logique simple de perte de paquets ou de fusion (Coalescing), si le même pool génère plusieurs mises à jour en moins de 1 ms, ne traiter que le dernier état pour économiser les ressources de calcul de la couche de stratégie.
Index de prélecture : lors de l'analyse des journaux, précharger les informations Decimals des tokens fréquemment utilisés, évitant ainsi de générer des requêtes supplémentaires lors du calcul des écarts de prix.
5. Démonstration technique : logiques de fusion d'événements multiples (simulation Python)
Bien que le cœur haute performance soit en Rust, sa logique de distribution de fusion « plusieurs à un » peut être parfaitement exprimée avec asyncio :
import asyncio
import random
async def pool_monitor(pool_id: str):
"""Simuler un flux d'abonnement de compte indépendant"""
while True:
await asyncio.sleep(random.uniform(0.01, 0.1)) # Simuler une poussée aléatoire
yield {"pool": pool_id, "data": random.random()}
async def main_scout_loop():
# Liste d'écoute simulée obtenue depuis l'Inventory
watchlist = ["Pool_A", "Pool_B", "Pool_C"]
# Fusionner tous les flux dans une seule file d'attente
queue = asyncio.Queue()
async def producer(pool_id):
async for update in pool_monitor(pool_id):
await queue.put(update)
# Démarrer toutes les tâches du producteur
for p in watchlist:
asyncio.create_task(producer(p))
print("[*] Le moteur Scout a démarré, en attente de signaux multiples...")
# Boucle de consommation principale : traitement de la distribution des stratégies
while True:
event = await queue.get()
# À ce moment-là, déclencher immédiatement le calcul asynchrone de la couche de stratégie
asyncio.create_task(execute_strategy(event))
async def execute_strategy(event):
print(f"⚡️ Signal capturé : {event['pool']} -> Déclenchement du calcul du modèle de tarification")
if name == "__main__":
asyncio.run(main_scout_loop())
6. Résumé : le radar le plus sensible
Le niveau de conception du module Scout détermine directement la « vitesse de départ » du robot. Un bon Scout devrait :
Assez large : capable de capturer de nouvelles opportunités à travers les journaux.
Assez précis : capable de verrouiller la volatilité des prix via des abonnements de comptes.
Assez rapide : utilisant une architecture asynchrone et une analyse binaire, maintenant la latence à des niveaux de microsecondes.
Annonce de la prochaine étape
Signal capturé, données brutes obtenues, quelle est la prochaine étape ? Nous devons convertir les données binaires en prix d'actifs réels. Dans le prochain article, nous entrerons dans le module AMM, révélant comment la formule de produit constant de Raydium et le modèle mathématique de liquidité concentrée d'Orca fonctionnent rapidement en mémoire.
Cet article est rédigé par Levi.eth, dédié à partager l'art de l'ingénierie extrême dans le domaine de MEV sur Solana.

