Search docs

Find a documentation page

Rust SDK

staked-quic-client-sdk and staked-quic-tpu-client: the native Rust path for sending transactions over a leased SWQoS lane.

The Rust integration is two crates from the nodexpert-labs/staked-quic-connection-provider repo. The repo is currently private on GitHub, so both the web page and anonymous Git fetches require repo access. They are the canonical implementation: the TypeScript and Python SDKs wrap the same crates via native bindings.

The two crates

CrateRole
staked-quic-client-sdkThin tonic gRPC client for the TEE signing service running next to the leased validator. Exposes StakedQuicSignerClient with connect, get_certificate, sign_certificate_verify, and health, and re-exports the generated protos as staked_quic_client_sdk::proto.
staked-quic-tpu-clientSolana TPU client that sends transactions to leaders over QUIC. In CertMode::Staked the TLS 1.3 client-certificate handshake is signed by the TEE holding the validator identity key, so leaders apply SWQoS staked priority. CertMode::Unstaked is the best-effort baseline with a local ephemeral key.

Neither crate is a REST client for the marketplace API. Browsing validators, booking, and minting JWTs stay plain HTTPS calls, see the trader integration guide and the API reference.

Install

The crates are not published on crates.io, and the repo has no release tags yet. Depend on the Git repo over SSH, tracking master (the same form the marketplace itself uses):

[dependencies]
staked-quic-tpu-client = { git = "ssh://git@github.com/nodexpert-labs/staked-quic-connection-provider.git", branch = "master" }

staked-quic-tpu-client re-exports StakedQuicSignerClient, so for the send-transaction path one dependency is enough. Add staked-quic-client-sdk = { git = "...", branch = "master" } only if you drive the signing oracle directly.

The ssh + branch = "master" form requires GitHub SSH auth with access to the private repo, and it floats on master. There is no alternative today: an anonymous https://github.com/... fetch is rejected because the repo is private, and there are no release tags to pin. Once the repo is public and tagged, prefer an https + tag = "<release-tag>" dependency for anything you deploy.

Prerequisites: lease JWT and CA PEM

The Rust SDK does not mint the lease JWT for you. You need three inputs before calling connect:

  1. TEE endpoint URL (https://...): returned once by the credentials reveal, POST /v1/leases/{lease_id}/credentials, as tee_endpoint_url.
  2. CA PEM: the host-proxy serves a self-signed certificate generated at deploy time. Get it from the validator operator as the cert.pem that deploy.sh prints at deploy time. The credentials reveal also returns a tls_ca_cert field, but in v1 it is a hardcoded placeholder string, not a usable PEM, so do not pin it (the TLS handshake would fail).
  3. Lease JWT: a short-lived ES256 token minted per call from the marketplace API.

Mint the JWT with your trader API token (also from the one-shot credentials reveal, or rotated via POST /v1/leases/{lease_id}/api-keys, see authentication):

curl -s -X POST "https://api.swqos.dev/v1/leases/$LEASE_ID/token" \
  -H "Authorization: Bearer $API_TOKEN"
{
  "data": {
    "jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9...",
    "expires_at": 1781234567
  },
  "meta": { "request_id": "01J...", "version": "v1" }
}

Facts that matter for your refresh loop:

  • Default TTL is 600 seconds (expires_at is a Unix timestamp). Re-mint before expiry, the other SDKs refresh at exp - 60s.
  • Each POST mints a fresh token. The endpoint is rate limited at 12 requests per minute per API key.
  • Errors: 403 lease-not-active, 403 lease-epoch-out-of-range (the current Solana epoch is outside your booked range), 404 lease-not-found, 429 rate-limit-exceeded. See errors.
  • The JWT is audience-bound to your tee_endpoint_url and carries the leased validator's pubkey, so it only works against your lease's TEE.

The TypeScript and Python LeaderClients mint and refresh the JWT automatically. In Rust you call the token endpoint yourself (curl, reqwest, anything) and pass the jwt string to connect.

Quickstart: send a transaction over the staked lane

This mirrors staked-quic-tpu-client/examples/send_one_staked.rs:

use staked_quic_tpu_client::{CertMode, StakedQuicSignerClient, TpuClient, TpuConfig};

#[tokio::main(flavor = "multi_thread")]
async fn main() -> anyhow::Result<()> {
    // The deploy.sh-printed CA cert from the validator operator
    // (the reveal's tls_ca_cert field is a v1 placeholder, do not use it).
    let ca_pem = std::fs::read("cert.pem")?;

    // Minted via POST /v1/leases/{id}/token (see above).
    let lease_jwt = "<lease-jwt>";

    // https:// is mandatory; the JWT rides as Bearer metadata on every RPC.
    let sdk = StakedQuicSignerClient::connect(
        "https://tee.example.com:443",
        lease_jwt,
        &ca_pem,
    )
    .await?;

    let tpu = TpuClient::new(
        "https://api.testnet.solana.com", // RPC is used for leader discovery only
        CertMode::Staked(sdk),
        TpuConfig::default(),
    )
    .await?;

    // Sanity check: in Staked mode this is the leased validator's identity pubkey.
    println!("cert pubkey: {}", hex::encode(tpu.cert_pubkey()));

    let tx = build_my_signed_tx(); // your fully signed solana_sdk Transaction
    let signature = tpu.send_transaction(&tx).await?;
    println!("sent {signature}");
    Ok(())
}

What each call does:

  • StakedQuicSignerClient::connect(endpoint, lease_jwt, ca_pem) opens a TLS-pinned gRPC channel to the host-proxy. It rejects any non-https:// endpoint (a plaintext channel would leak the JWT and signing traffic), pins the given CA PEM, and installs an interceptor that attaches authorization: Bearer <jwt> to every request, flagged sensitive so it stays out of HPACK tables and header dumps. The client is Clone over a shared pooled channel.
  • TpuClient::new(rpc_url, cert_mode, config) builds the QUIC endpoint. In CertMode::Staked it fetches the TEE certificate up front and wires the rustls client-cert resolver to the signing oracle. Transactions never go through the RPC node, it only serves leader discovery.
  • tpu.cert_pubkey() returns the 32-byte Ed25519 pubkey embedded in the client cert: the validator identity in Staked mode, an ephemeral local key in Unstaked mode. Compare it against your lease's validator before sending real traffic.
  • tpu.send_transaction(&tx) bincode-serializes the transaction and fans it out to the next 4 distinct upcoming leaders, then returns the transaction signature.

CertMode::Staked requires a multi-threaded tokio runtime (the default #[tokio::main]). The TEE signing path bridges rustls's synchronous Signer::sign to the async gRPC client over a channel, which would deadlock on a current-thread runtime, so TpuClient::new fails loudly at construction on that flavor.

TpuClient behavior

  • Fan-out: send_transaction targets the next 4 distinct upcoming leaders (about 16 slots, roughly 6.4 seconds of coverage). The banking stage dedupes by signature, so duplicate arrivals are harmless. It errors only if every send failed.
  • Send-only: the client returns the signature once the QUIC streams are written. Confirmation is your job, poll getSignatureStatuses or use your own strategy.
  • Pinned target: send_transaction_to(&tx, addr) skips leader discovery and hits one specific tpu_quic endpoint. The leader module ships upcoming_unique_leaders and resolve_validator helpers.
  • Persistent connections: tpu.connect(addr) (or connect_to(&leader)) returns a TpuConnection, a cheaply cloneable QUIC connection reusable across many sends without re-handshaking. This is how you keep the TEE-signed handshake cost (30-80 ms, cold connections only) off the hot path.
  • Wire limit: serialize_tx enforces SOLANA_TX_WIRE_LIMIT (1232 bytes). send_raw / send_raw_to accept pre-serialized payloads.

TpuConfig defaults:

FieldDefaultMeaning
bind_addr0.0.0.0:0Local UDP bind for the QUIC endpoint
keep_alive1500 msQUIC keep-alive interval
max_idle_timeout10 sIdle timeout before the connection drops
fanout_count4Distinct upcoming leaders per send_transaction
rpc_commitmentprocessedCommitment for leader-discovery RPC calls

Low-level: the signing oracle client

If you run your own QUIC/TLS stack, use staked-quic-client-sdk directly:

let health = sdk.health().await?;
// HealthStatus { provisioned: bool, pubkey_sha256: Vec<u8> }

let cert = sdk.get_certificate().await?;
// CertificateData { certificate: Vec<u8>, public_key: Vec<u8> }

let sig = sdk.sign_certificate_verify(&payload).await?;
// 64-byte Ed25519 signature
  • get_certificate returns the 249-byte Firedancer-compatible mock X.509 certificate to present during the TLS handshake with the leader, plus the 32-byte validator pubkey.
  • sign_certificate_verify signs a TLS 1.3 CertificateVerify payload. The enclave's keyguard accepts only payloads of exactly 130, 146, or 162 bytes matching the CertificateVerify transcript format and rejects everything else, which is what keeps the validator key unable to sign votes or transactions on a trader's behalf.
  • health reports whether the enclave is provisioned and the SHA-256 of the loaded validator pubkey.

The client attaches your lease JWT to every call. The host-proxy enforces it on SignCertificateVerify and answers expired or invalid tokens with gRPC UNAUTHENTICATED. The JWT is fixed at connect time, so when you mint a fresh token, reconnect with it.

Runnable examples

From a checkout of staked-quic-connection-provider:

# Staked lane, requires a reachable TEE proxy + active lease.
# --keypair is the TRADER wallet that signs and pays the TX;
# the validator identity never leaves the TEE.
cargo run --example send_one_staked -- \
    --keypair /path/to/trader.json \
    --tee-endpoint https://tee.example.com:443 \
    --tee-token "<lease-jwt>" \
    --tee-ca-pem cert.pem \
    --rpc-url https://api.testnet.solana.com

# Unstaked baseline, no TEE required.
cargo run --example send_one_unstaked -- \
    --keypair /path/to/trader.json \
    --rpc-url https://api.testnet.solana.com

--rpc-url defaults to https://api.testnet.solana.com when omitted.

Next steps