📥 Recibir transacciones decodificadas
Un Decoded Shred Stream se envía por UDP a la IP y el puerto que configures en la página de detalle del stream — sin conexión, sin handshake, sin retransmisión. Cada datagrama tiene como máximo 1.408 bytes y transporta una transacción completa: un header binario de 16 bytes, el slot y la transacción en formato wire estándar de Solana.
📦 El header del sobre (16 bytes)
Todos los datagramas comienzan con el mismo header fijo. Todos los enteros son little-endian.
| Offset | Tamaño | Campo | Valor / semántica |
|---|---|---|---|
| 0 | 2 | magic | 0x5AE7 (u16 LE) — cualquier otro valor ⇒ rechaza el datagrama |
| 2 | 1 | version | 2 — rechaza cualquier versión desconocida |
| 3 | 1 | msg_type | 2 = DecodedTx (1 = reservado) |
| 4 | 1 | flags | Reservado, 0 |
| 5 | 1 | frag_index | Índice del fragmento, desde 0 — siempre 0 para transacciones decodificadas |
| 6 | 1 | frag_count | Número de fragmentos — siempre 1 para transacciones decodificadas (nunca fragmentadas) |
| 7 | 1 | _pad | 0 |
| 8 | 8 | seq | u64 LE, monótonamente creciente por flujo de producto |
🧾 El payload (msg_type = 2)
Inmediatamente después del header:
| Campo | Tamaño | Semántica |
|---|---|---|
slot | 8 | u64 LE — el slot al que pertenece la transacción |
transaction | resto del datagrama | Formato wire estándar de Solana (VersionedTransaction serializada con bincode) |
Los bytes de la transacción son exactamente lo que espera cualquier SDK de Solana — pásalos a tu parser existente sin cambios. Las transacciones decodificadas nunca se fragmentan: un datagrama siempre es una transacción completa. Las transacciones de voto están excluidas por defecto.
📉 Detectar pérdidas con seq
UDP no retransmite. Cada datagrama lleva un seq que aumenta exactamente en 1 dentro de tu stream. Un salto en seq significa que se perdieron esos datagramas:
seq recibido = 1042, anterior = 1039 → 2 datagramas perdidos
Registra el último seq que hayas visto y alerta ante los saltos — una pérdida sostenida suele indicar que tu buffer de recepción es demasiado pequeño (abajo) o un cuello de botella en la ruta.
🔧 Dimensionar el buffer
El flujo llega en ráfagas — un slot cargado puede entregar muchas transacciones seguidas. Dale al socket un buffer de recepción generoso (SO_RCVBUF, varios megabytes) para que las ráfagas cortas se absorban en lugar de que el kernel las descarte, y vacía el socket desde un bucle dedicado y rápido.
💻 Decodificador mínimo (Python)
import socket, structsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 8 * 1024 * 1024)sock.bind(("0.0.0.0", 5000)) # el puerto que configuraste en el dashboardexpected = Nonewhile True:dgram = sock.recv(2048)if len(dgram) < 24:continuemagic, version, msg_type = struct.unpack_from("<HBB", dgram, 0)if magic != 0x5AE7 or version != 2:continue # rechaza magic/version desconocidosif msg_type != 2:continue # no es un datagrama DecodedTx(seq,) = struct.unpack_from("<Q", dgram, 8)if expected is not None and seq != expected:print(f"salto: {seq - expected} datagrama(s) perdido(s)")expected = seq + 1(slot,) = struct.unpack_from("<Q", dgram, 16)tx_bytes = dgram[24:] # formato wire estándar — pásalo a cualquier parser de tx Solana
🦀 Clientes oficiales y el módulo codec
No hace falta que escribas el decodificador anterior: los clientes oficiales decoded-shredstream (Rust, Go, JavaScript/TypeScript, Python) escuchan en el puerto, parsean el sobre, detectan los saltos de seq y te entregan cada transacción con su slot y sus firmas — por UDP y por gRPC, con la misma API.
// cargo add decoded-shredstream tokio --features tokio/macros,tokio/rt-multi-threaduse decoded_shredstream::{UdpClient, UdpConfig};#[tokio::main]async fn main() -> std::io::Result<()> {let mut client = UdpClient::bind(UdpConfig { port: 8002, ..Default::default() })?;while let Some(update) = client.next_update().await {println!("slot={} sig={} {}B", update.slot(), update.signature(), update.bytes().len());}Ok(())}
El crate Rust también expone el framing como módulo codec (constantes FRAME_MAGIC = 0x5AE7, FRAME_VERSION = 2, FRAME_HEADER_LEN = 16, MAX_DATAGRAM = 1408, parse_header, StreamDecoder), para que puedas decodificar datagramas leídos desde un socket que gestiones tú mismo — el cliente Go expone lo mismo a través de ParseHeader / StreamDecoder:
use std::net::UdpSocket;use decoded_shredstream::codec::{Push, StreamDecoder, MAX_DATAGRAM};fn decode_loop(socket: &UdpSocket) -> std::io::Result<()> {let mut decoder = StreamDecoder::new();let mut buf = [0u8; MAX_DATAGRAM + 64];loop {let n = socket.recv(&mut buf)?;if let Push::Update(update) = decoder.push(&buf[..n]) {println!("slot={} sig={}", update.slot(), update.signature());}}}
Instalación: npm install decoded-shredstream · pip install decoded-shredstream · cargo add decoded-shredstream · go get github.com/shredstream/decoded-shredstream-go.
➡️ Próximos pasos
- Decoded Shred Stream — posicionamiento, latencia y modos de entrega.
- Entrega gRPC — las mismas transacciones en un flujo ordenado, con filtrado de cuentas en el servidor.