Skip to content

rust ​

Send transactions over QUIC for the lowest possible latency.

Dependencies ​

toml
[dependencies]
tokio = { version = "1", features = ["full"] }
quinn = "0.11"
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
rcgen = "0.13"
solana-sdk = "2.0"
arc-swap = "1.7"
anyhow = "1.0"
sha2 = "0.10"
ed25519-dalek = "2.1"
bincode = "1"  # serializes legacy/v0 transactions in the Usage example

Full Example ​

rust
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_sdk::{signature::Keypair, signer::Signer};
use std::{net::SocketAddr, sync::Arc, time::Duration};
use tokio::sync::Mutex;

const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
const BLOCKSPRINT_SERVER: &str = "blocksprint-node";
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25);
const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(30);

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

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

        // Generate keypair from seed
        use ed25519_dalek::SigningKey;
        let secret = SigningKey::from_bytes(&seed);
        let mut keypair_bytes = [0u8; 64];
        keypair_bytes[..32].copy_from_slice(&seed);
        keypair_bytes[32..].copy_from_slice(secret.verifying_key().as_bytes());
        let keypair = Keypair::from_bytes(&keypair_bytes)
            .context("Failed to create keypair from seed")?;
        let (cert, key) = new_dummy_x509_certificate(&keypair);

        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()?)?;
        endpoint.set_default_client_config(client_config.clone());

        let addr = resolve(endpoint_addr).await?;
        let connection = endpoint.connect(addr, BLOCKSPRINT_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.lock().await;
        if self.connection.load().close_reason().is_none() {
            return Ok(()); // still open, or another task has already reconnected
        }
        let connection = self
            .endpoint
            .connect_with(self.client_config.clone(), self.addr, BLOCKSPRINT_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.
fn new_dummy_x509_certificate(keypair: &Keypair) -> (CertificateDer<'static>, PrivateKeyDer<'static>) {
    use rcgen::{CertificateParams, KeyPair, PKCS_ED25519};

    let secret = keypair.secret().as_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();
    params
        .distinguished_name
        .push(rcgen::DnType::CommonName, "Solana node");

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

Usage ​

rust
mod full_example; // the client above
use full_example::BlocksprintClient;
use solana_sdk::transaction::Transaction;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = BlocksprintClient::connect(
        "fra.blocksprint.io:10000",
        "YOUR_API_KEY",
    ).await?;

    // Build your transaction
    let transaction: Transaction = /* your signed transaction */;
    let tx_bytes = bincode::serialize(&transaction)?;

    // Send via QUIC
    client.send_transaction(&tx_bytes).await?;

    // Give the transaction a moment to leave before the program exits
    tokio::time::sleep(std::time::Duration::from_secs(1)).await;

    Ok(())
}

INFO

Notes:

  • The client automatically reconnects on connection failures
  • Thread-safe: wrap in Arc to share across tasks
  • Uses the same API key as HTTP endpoints
  • Transactions must be fully signed before sending
  • Transaction V1 (up to 4,096 bytes) is sent the same way. Serialize it with wincode, not bincode.