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(())
    }
}

async fn resolve(addr: &str) -> Result<SocketAddr> {
    if let Ok(parsed) = addr.parse() {
        return Ok(parsed);
    }
    tokio::net::lookup_host(addr)
        .await?
        .next()
        .context("failed to resolve address")
}

/// 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.