QUIC Transaction Submission

This guide introduces how to connect to the high-performance transaction landing service using the QUIC protocol, ensuring your transactions are packaged and confirmed quickly and reliably.

Overview

QUIC transaction submission service provides the following features:

  • High Performance: Submit transactions through private QUIC endpoints for lower latency and higher throughput
  • Secure & Reliable: mTLS mutual authentication + API Key authorization ensure connection security
  • Auto-Reconnect: Built-in connection health checks and automatic reconnection mechanisms
  • No Subscription Required: Pay-as-you-go based on Tips mode

Prerequisites

1. Obtain Your API Key

Contact us to get your dedicated API Key.

2. Prepare Your Transaction Signing Account

You need a real account holding SOL to sign transactions. This account must:

  • Hold sufficient SOL balance (recommended at least 0.0003 SOL)

3. Ensure Tip Account Funding

Each transaction must include a System Program transfer (≥0.0003 SOL = 300,000 lamports) to one of the following Tip accounts:

7HkiWXe5deJvzn4D6kgMUFCADwX9Z4DMrdjNSSxN6bPp

zanrUknLZXzT9JPj968A7RfgCjp77Lx1W1xKRAtfshb
zanHbk2UsiT3jKsKjD7UuEqS5Vgpmcd4pG9HycAAV8g
zanNazKCXNRoKnPS9BBbFTELTpNwUDJxeKEb1JtZJer
zan3gbFhXCjGLHhRe2vaXRDta5fCrYiYr3Dq4RLvpfU
zan6WoE3DX5aK7FMQT1vSGsGrgZG1ngns3oCsFMnBHU
zan8Nb9fB4zMDsuTRP9R65QZbc9v2Cjn5a4Hjwnj8D3
zanJgoR7ALJAJ6ohoKs6aS9T71D9ZkNN9gYM5xUsi3u
zanAtYifQP7Bo6kStB97mJvzqSDW1toKNibWibwcKDd

Endpoints

RegionEndpoint Address
Frankfurt (Recommended)fra.zan.top:21000
Singaporesgp.zan.top:21000

Quick Start

Rust Environment Requirements

  • Rust 1.70+
  • Cargo

Cargo.toml Configuration

Add the following dependencies to your project's Cargo.toml:

[dependencies]
# QUIC Protocol
quinn = "0.11.3"
rustls = { version = "0.23.15", default-features = false }

# Solana Related
solana-sdk = "3.0.0"
solana-client = "3.0.0"
solana-tls-utils = "3.0.0"
solana-system-interface = { version = "3.0.0", features = ["bincode"] }
solana-perf = "3.1.5"

# Async Runtime
tokio = { version = "1.39", features = ["macros", "rt-multi-thread", "net", "time", "sync"] }

# Cryptography & Certificates
sha2 = "0.10"
rcgen = "0.13"

# Thread Safety
arc-swap = "1.6"

# Serialization
bincode = "1.3.3"
serde = { version = "1.0", features = ["derive"] }

# Error Handling
anyhow = "1.0.98"

1. Client Example

//! Zan-style QUIC Client Example (mTLS + API Key Authentication)
//!
//! Usage: cargo run --bin client_quic_demo -- [BOOSTER_QUIC_ADDR] [KEYPAIR_PATH] [API_KEY] [RPC_URL]
//!
//! Features:
//! - Reads keypair from file for transaction signing (real account with SOL)
//! - Generates mTLS certificate using api_key, CN = api_key for authentication
//! - Supports automatic reconnection

use anyhow::{Context, Result};
use arc_swap::ArcSwap;
use quinn::{
    crypto::rustls::QuicClientConfig, ClientConfig, Connection, Endpoint, IdleTimeout,
    TransportConfig,
};
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
    message::Message,
    signature::{read_keypair_file, Keypair, Signer},
    transaction::Transaction,
};
use solana_system_interface::instruction;
use solana_perf::packet::PACKET_DATA_SIZE;
use std::{net::SocketAddr, sync::Arc, time::Duration};
use tokio::sync::Mutex;

const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
const ZAN_SERVER: &str = "zan-node";
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(10);
const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(30);

/// Zan QUIC client with automatic reconnection.
///
/// Thread-safe: can be shared across tasks via `Arc<ZanClient>`.
pub struct ZanClient {
    endpoint: Endpoint,
    client_config: ClientConfig,
    addr: SocketAddr,
    connection: ArcSwap<Connection>,
    reconnect: Mutex<()>,
}

impl ZanClient {
    /// Connect to a Zan endpoint.
    ///
    /// # Arguments
    /// * `endpoint_addr` - Server address (e.g., "fra.zan.top:21000")
    /// * `api_key` - Your Zan API key (e.g., "bs_2294d1eb...")
    pub async fn connect(endpoint_addr: &str, api_key: &str) -> Result<Self> {
        // Derive keypair from API key using SHA256 hash
        use sha2::{Digest, Sha256};
        let mut hasher = Sha256::new();
        hasher.update(api_key.as_bytes());
        let seed: [u8; 32] = hasher.finalize().into();

        // Generate keypair from seed
        let keypair = Keypair::new_from_array(seed);
        let (cert, key) = new_dummy_x509_certificate(&keypair, api_key);

        let mut crypto = rustls::ClientConfig::builder()
            .dangerous()
            .with_custom_certificate_verifier(SkipServerVerification::new())
            .with_client_auth_cert(vec![cert], key)
            .context("failed to configure client certificate")?;

        crypto.alpn_protocols = vec![ALPN_TPU_PROTOCOL_ID.to_vec()];

        let client_crypto = QuicClientConfig::try_from(crypto)
            .context("failed to convert rustls config into quinn crypto config")?;

        let mut client_config = ClientConfig::new(Arc::new(client_crypto));
        let mut transport = TransportConfig::default();
        transport.keep_alive_interval(Some(KEEP_ALIVE_INTERVAL));
        transport.max_idle_timeout(Some(IdleTimeout::try_from(MAX_IDLE_TIMEOUT)?));
        client_config.transport_config(Arc::new(transport));

        let mut endpoint = Endpoint::client("0.0.0.0:0".parse().context("parse bind address")?)?
        ;
        endpoint.set_default_client_config(client_config.clone());

        let addr = resolve(endpoint_addr).await?;
        let connection = endpoint.connect(addr, ZAN_SERVER)?.await?;

        Ok(Self {
            endpoint,
            client_config,
            addr,
            connection: ArcSwap::from_pointee(connection),
            reconnect: Mutex::new(()),
        })
    }

    /// Send a serialized transaction to the network.
    ///
    /// Automatically reconnects on failure and retries once.
    pub async fn send_transaction(&self, transaction_bytes: &[u8]) -> Result<()> {
        let connection = self.connection.load_full();
        if Self::try_send_bytes(&connection, transaction_bytes)
            .await
            .is_ok()
        {
            return Ok(());
        }

        // Connection lost, reconnect and retry
        self.reconnect().await?;
        let connection = self.connection.load_full();
        Self::try_send_bytes(&connection, transaction_bytes).await
    }

    async fn reconnect(&self) -> Result<()> {
        let _guard = self.reconnect.try_lock()?;
        let connection = self
            .endpoint
            .connect_with(self.client_config.clone(), self.addr, ZAN_SERVER)?
            .await?;
        self.connection.store(Arc::new(connection));
        Ok(())
    }

    async fn try_send_bytes(connection: &Connection, payload: &[u8]) -> Result<()> {
        let mut stream = connection.open_uni().await?;
        stream.write_all(payload).await?;
        stream.finish()?;
        Ok(())
    }
}

/// 解析地址字符串,支持域名和 IP 地址
/// 如果是域名,会先解析为 IP 地址
fn resolve_addr(addr: &str) -> Result<std::net::SocketAddr> {
    // 尝试直接解析(IP:端口 格式)
    if let Ok(socket_addr) = addr.parse::<std::net::SocketAddr>() {
        return Ok(socket_addr);
    }

    if let Some((hostname, port_str)) = addr.rsplit_once(':') {
        let port: u16 = port_str.parse()
            .map_err(|_| anyhow::anyhow!("invalid port: {}", port_str))?;

        // DNS 解析
        let ips = std::net::ToSocketAddrs::to_socket_addrs(&(hostname, port))
            .map_err(|e| anyhow::anyhow!("failed to resolve hostname '{}': {}", hostname, e))?;

        let ip = ips.fold(None, |acc: Option<std::net::IpAddr>, addr| {
            Some(acc.unwrap_or(addr.ip()))
        }).ok_or_else(|| anyhow::anyhow!("no IP found for hostname"))?;

        return Ok(std::net::SocketAddr::new(ip, port));
    }

    anyhow::bail!("invalid address format: {}", addr)
}

/// Generate a self-signed X.509 certificate from a Solana keypair.
/// The CommonName (CN) is set to the api_key for authentication.
fn new_dummy_x509_certificate(
    keypair: &Keypair,
    api_key: &str,
) -> (CertificateDer<'static>, PrivateKeyDer<'static>) {
    use rcgen::{CertificateParams, KeyPair, PKCS_ED25519};

    let secret = keypair.secret_bytes();
    let public = keypair.pubkey().to_bytes();

    let mut keypair_bytes = [0u8; 64];
    keypair_bytes[..32].copy_from_slice(secret);
    keypair_bytes[32..].copy_from_slice(&public);

    let pkcs8_der = keypair_to_pkcs8(&keypair_bytes);
    let key_pair = KeyPair::from_pkcs8_der_and_sign_algo(
        &rustls::pki_types::PrivatePkcs8KeyDer::from(pkcs8_der.clone()),
        &PKCS_ED25519,
    )
    .expect("valid keypair");

    let mut params = CertificateParams::new(vec!["localhost".into()]).expect("valid params");
    params.distinguished_name = rcgen::DistinguishedName::new();
    // CN = api_key for mTLS authentication (like client_quic)
    params.distinguished_name.push(rcgen::DnType::CommonName, api_key);

    let cert = params.self_signed(&key_pair).expect("valid cert");
    let key_der = PrivateKeyDer::try_from(pkcs8_der).expect("valid key");

    (cert.der().clone(), key_der)
}

fn keypair_to_pkcs8(keypair_bytes: &[u8; 64]) -> Vec<u8> {
    let mut pkcs8 = vec![
        0x30, 0x53, 0x02, 0x01, 0x01, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04,
        0x20,
    ];
    pkcs8.extend_from_slice(&keypair_bytes[..32]);
    pkcs8.extend_from_slice(&[0xa1, 0x23, 0x03, 0x21, 0x00]);
    pkcs8.extend_from_slice(&keypair_bytes[32..]);
    pkcs8
}

/// Server certificate verifier that skips verification (for self-signed certs).
#[derive(Debug)]
struct SkipServerVerification(Arc<rustls::crypto::CryptoProvider>);

impl SkipServerVerification {
    fn new() -> Arc<Self> {
        Arc::new(Self(Arc::new(rustls::crypto::ring::default_provider())))
    }
}

impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
    fn verify_server_cert(
        &self,
        _: &CertificateDer<'_>,
        _: &[CertificateDer<'_>],
        _: &rustls::pki_types::ServerName<'_>,
        _: &[u8],
        _: rustls::pki_types::UnixTime,
    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
        Ok(rustls::client::danger::ServerCertVerified::assertion())
    }

    fn verify_tls12_signature(
        &self,
        message: &[u8],
        cert: &CertificateDer<'_>,
        dss: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        rustls::crypto::verify_tls12_signature(
            message,
            cert,
            dss,
            &self.0.signature_verification_algorithms,
        )
    }

    fn verify_tls13_signature(
        &self,
        message: &[u8],
        cert: &CertificateDer<'_>,
        dss: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        rustls::crypto::verify_tls13_signature(
            message,
            cert,
            dss,
            &self.0.signature_verification_algorithms,
        )
    }

    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
        self.0.signature_verification_algorithms.supported_schemes()
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let args: Vec<String> = std::env::args().collect();
    let addr = args.get(1).cloned().unwrap_or_else(|| "fra.zan.top:21000".to_string());
    let keypair_path = args.get(2).cloned().unwrap_or_else(|| "client_id.json".to_string());
    let api_key = args.get(3).cloned().unwrap_or_else(|| "00f7xxxc088".to_string());
    let rpc_url = args.get(4).cloned().unwrap_or_else(|| "https://api.zan.top/node/v1/solana/mainnet/00f7xxxc088".to_string());

    // Read real account with SOL from file (for transaction signing)
    let keypair = read_keypair_file(&keypair_path)
        .map_err(|e| anyhow::anyhow!("read keypair: {}", e))?;
    let rpc = RpcClient::new(rpc_url);
    let blockhash = rpc.get_latest_blockhash().context("get blockhash")?;
    let msg = Message::new(
        &[instruction::transfer(
            &keypair.pubkey(),
            &keypair.pubkey(),
            1_000,
        )],
        Some(&keypair.pubkey()),
    );
    let tx = Transaction::new(&[&keypair], msg, blockhash);
    let serialized = bincode::serialize(&tx).context("serialize tx")?;

    if serialized.len() > PACKET_DATA_SIZE {
        anyhow::bail!(
            "tx size {} > {}",
            serialized.len(),
            PACKET_DATA_SIZE
        );
    }

    // Send transaction bytes directly (mTLS cert CN contains api_key for authentication)
    let payload = &serialized;

    println!(
        "Connecting to Zan QUIC server: {} (keypair: {}, api_key: {})...",
        addr,
        keypair.pubkey(),
        &api_key[..8.min(api_key.len())]
    );

    let client = ZanClient::connect(&addr, &api_key).await?;
    println!("Connected!");

    println!("Sending transaction via uni stream ({} bytes, mTLS auth)...", payload.len());
    let start = std::time::Instant::now();
    client.send_transaction(&payload).await?;
    let elapsed = start.elapsed();

    let sig = tx.signatures.first().map(|s| s.to_string()).unwrap_or_default();
    println!(
        "Transaction sent successfully! signature: {}, latency: {:.2} ms",
        sig,
        elapsed.as_secs_f64() * 1000.0
    );

    Ok(())
}

2. Build the Client

cargo build --release --bin client_quic_demo

3. Run the Example

# cargo run --bin client_quic_demo <QUIC_ENDPOINT> <KEYPAIR_PATH> <API_KEY> <RPC_URL>
cargo run --bin client_quic_demo -- \
  "fra.zan.top:21000" \
  "./client_id.json" \
  "00f7xxxc088" \
  "https://api.zan.top/node/v1/solana/mainnet/00f7xxxc088"

Parameter Descriptions

ParameterDescriptionExample
QUIC EndpointQUIC server addressfra.zan.top:21000
Keypair PathJSON file path for transaction signing account./client_id.json
API_KEYYour API Key00f7xxxxc088
RPC_URLSolana RPC Node URLhttps://api.zan.top/node/v1/solana/mainnet/00f7xxxxc088

Usage Example

cargo run --bin client_quic_demo -- \
  "fra.zan.top:21000" \
  "./client_id.json" \
  "00f7xxxc088" \
  "https://api.zan.top/node/v1/solana/mainnet/00f7xxxc088"

4. Output Example

Connecting to Zan QUIC server: fra.zan.top:21000 (keypair: ..., api_key: 00f76627)...
Connected!
Sending transaction via uni stream (123 bytes, mTLS auth)...
Transaction sent successfully! signature: xxx, latency: 45.32 ms

Code Integration

If you want to integrate the QUIC client into your own project, you can reference the core implementation in client_quic_demo.rs:

Main Interfaces

/// Create Zan QUIC client and connect
/// endpoint_addr: QUIC server address, e.g., "fra.zan.top:21000"
/// api_key: Your API Key
let client = ZanClient::connect(endpoint_addr, api_key).await?;

/// Send transaction (handles reconnection automatically)
client.send_transaction(&transaction_bytes).await?;

Core Features

  • mTLS Authentication: Generates self-signed certificate using API Key, CN field contains API Key for server-side authentication
  • Auto-Reconnect: Automatically reconnects and retries sending on connection loss
  • Keep-Alive Mechanism: Sends heartbeat every 10 seconds to maintain connection activity

Important Notes

  1. Tip Transaction: Each transaction must include a transfer of at least 0.0003 SOL to a Tip account
  2. Transaction Size: Serialized transaction cannot exceed 1232 bytes (PACKET_DATA_SIZE)
  3. Connection Maintenance: Proactively reconnect when sending intervals are long to avoid idle timeout
  4. Key Security: Keep your keypair file and API Key secure; do not share them with others

FAQ

Q: What should I do if connection fails?

A: Please check:

  1. Is the API Key correct?
  2. Can the network access the target endpoint?
  3. Is the keypair file path correct?

Q: What should I do if transaction submission fails?

A: Ensure:

  1. The signing account has sufficient SOL balance
  2. Tip account has sufficient balance (0.0003 SOL per transaction)
  3. Transaction size does not exceed the limit

Q: How do I choose an endpoint?

A: We recommend selecting the endpoint closest to your physical location for the lowest latency. You can test response times from different endpoints to choose the optimal one.


Did this page help you?