📥 Приём декодированных транзакций
Decoded Shred Stream доставляется по UDP в режиме push на IP и порт, которые вы настраиваете на странице деталей стрима — без соединения, без handshake, без повторной передачи. Каждая датаграмма занимает не более 1 408 байт и несёт одну целую транзакцию: бинарный заголовок из 16 байт, слот и транзакцию в стандартном wire-формате Solana.
📦 Заголовок конверта (16 байт)
Каждая датаграмма начинается с одного и того же фиксированного заголовка. Все целые числа — в формате little-endian.
| Смещение | Размер | Поле | Значение / семантика |
|---|---|---|---|
| 0 | 2 | magic | 0x5AE7 (u16 LE) — любое другое значение ⇒ отбросить датаграмму |
| 2 | 1 | version | 2 — отбрасывайте любую неизвестную версию |
| 3 | 1 | msg_type | 2 = DecodedTx (1 = зарезервировано) |
| 4 | 1 | flags | Зарезервировано, 0 |
| 5 | 1 | frag_index | Индекс фрагмента, с нуля — всегда 0 для декодированных транзакций |
| 6 | 1 | frag_count | Число фрагментов — всегда 1 для декодированных транзакций (никогда не фрагментируются) |
| 7 | 1 | _pad | 0 |
| 8 | 8 | seq | u64 LE, монотонно возрастает в рамках потока продукта |
🧾 Полезная нагрузка (msg_type = 2)
Сразу после заголовка:
| Поле | Размер | Семантика |
|---|---|---|
slot | 8 | u64 LE — слот, к которому относится транзакция |
transaction | остаток датаграммы | Стандартный wire-формат Solana (сериализованный через bincode VersionedTransaction) |
Байты транзакции — ровно то, что ожидает любой SDK Solana: передайте их вашему существующему парсеру без изменений. Декодированные транзакции никогда не фрагментируются: одна датаграмма — всегда одна целая транзакция. Vote-транзакции исключены по умолчанию.
📉 Обнаружение потерь через seq
UDP не выполняет повторную передачу. Каждая датаграмма несёт seq, который увеличивается ровно на 1 в пределах вашего стрима. Пропуск в seq означает потерю соответствующего числа датаграмм:
получен seq = 1042, предыдущий = 1039 → потеряно 2 датаграммы
Отслеживайте последний увиденный seq и оповещайте о пропусках — устойчивые потери обычно означают слишком маленький приёмный буфер (см. ниже) или узкое место на пути.
🔧 Размер буфера
Поток идёт всплесками — загруженный слот может доставить много транзакций подряд. Дайте сокету щедрый приёмный буфер (SO_RCVBUF, несколько мегабайт), чтобы короткие всплески поглощались, а не отбрасывались ядром, и опустошайте сокет из выделенного быстрого цикла.
💻 Минимальный декодер (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)) # порт, который вы настроили в дашбордеexpected = 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 # отбрасываем неизвестные magic/versionif msg_type != 2:continue # не датаграмма DecodedTx(seq,) = struct.unpack_from("<Q", dgram, 8)if expected is not None and seq != expected:print(f"пропуск: потеряно {seq - expected} датаграмм")expected = seq + 1(slot,) = struct.unpack_from("<Q", dgram, 16)tx_bytes = dgram[24:] # стандартный wire-формат — передайте любому парсеру транзакций Solana
🦀 Официальные клиенты и модуль codec
Писать приведённый выше декодер самостоятельно не обязательно: официальные клиенты decoded-shredstream (Rust, Go, JavaScript/TypeScript, Python) привязывают порт, разбирают конверт, обнаруживают пропуски seq и передают вам каждую транзакцию вместе с её слотом и подписями — по UDP и по gRPC, с одним и тем же 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(())}
Rust-крейт также предоставляет фрейминг в виде модуля codec (константы FRAME_MAGIC = 0x5AE7, FRAME_VERSION = 2, FRAME_HEADER_LEN = 16, MAX_DATAGRAM = 1408, parse_header, StreamDecoder), чтобы вы могли декодировать датаграммы, прочитанные из сокета, которым управляете сами — Go-клиент предоставляет то же самое через 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());}}}
Установка: npm install decoded-shredstream · pip install decoded-shredstream · cargo add decoded-shredstream · go get github.com/shredstream/decoded-shredstream-go.
➡️ Следующие шаги
- Decoded Shred Stream — позиционирование, задержка и режимы доставки.
- Доставка по gRPC — те же транзакции по упорядоченному соединению с серверной фильтрацией по аккаунтам.