ZeroCopyZeroCopy
API reference

QUIC

The SWQoS QUIC protocol, with connection setup, client-certificate keys, channels, reply frames and codes.

QUIC carries the same transactions, bundles and payment rules as HTTP over long-lived connections. Each request is a datagram or a stream, so a client opens a connection once and keeps it.

Connect

SettingValue
AddressUDP port 443 of a regional host or sol.zerocopy.sh
ALPNzerocopy-swqos/1
TLS1.3 only
Server certificatePublicly trusted, valid for the regional name and sol.zerocopy.sh. Verify it against the hostname you connect to.
Client certificateRequired. Any certificate whose first subject common name is your API key. A self-signed certificate works; the chain is not validated.
Idle timeout30 seconds. The server sends no keep-alives, so configure your client to send them.
StreamsAt most 64 bidirectional and 64 unidirectional streams open at once per connection.
HandshakeMust complete within 3 seconds. Under load the server may answer a new connection with a QUIC Retry.

A missing, unknown or revoked key closes the connection with application code 8. If the key is revoked after connection, a stream request receives unauthorized before the connection closes.

Close codeReason textMeaning
0shutdownThe server is shutting down. Reconnect.
1connection limitA connection limit was reached. Keep at most 32 open connections per key in one region.
8unauthorizedNo client certificate, or the key in it is missing, unknown or revoked.

Channels

A QUIC connection carries four kinds of request. Every request body is a payload: a bare transaction or an envelope.

ChannelRequestReplyPreflight
DatagramOne payload: a transaction or a bundle.NoneNo
Unidirectional streamOne payload, up to 16,403 bytes, then the end of the stream.NoneNo
Bidirectional stream, kind 0x01[0x01][flags][payload], a transaction.Transaction frameFlag 0x01
Bidirectional stream, kind 0x03[0x03][payload], a bundle envelope with flag 0x02.Bundle frameNo

A datagram must fit in one QUIC datagram on your path; use a stream for a payload too large for that. The server reads each stream to its end, so finish your sending side after the request. The whole request must arrive within 2 seconds of the stream opening, or the server drops it without a reply.

A datagram or unidirectional stream gets no answer, and a refusal on those channels is dropped silently. A transaction sent that way counts against your send rate even when it is then refused. Use a bidirectional stream when you need the outcome.

On a kind 0x01 stream, the flags byte after the kind is 0x00, or 0x01 to run a preflight simulation at processed commitment. No other bits may be set. After the kind byte, the stream may carry up to 16,404 bytes: one flags byte and a payload of up to 16,403 bytes. A bundle payload on this stream is refused as malformed with problem expected_transaction, and a single transaction on a kind 0x03 stream with problem expected_bundle.

Kinds 0x02, 0x06 and 0x07 are reserved for the SWQoS agent; do not send them. A stream that starts with any other byte is refused as malformed, problem invalid_stream_kind, and ends without a reply.

Reply frames

The server writes one frame and finishes the stream.

FrameLayout
Transaction[0x01][code: u8][detail length: u16 LE][detail]
Bundle[0x03][code: u8][bundle id: 32 bytes][detail length: u16 LE][detail]

An accepted request has code 0 and an empty detail. An accepted bundle's frame carries its bundle id as 32 raw bytes; a refused bundle's id is all zeros. The detail of a refusal is the UTF-8 JSON rejection, the same object /binary returns: reason, its fields, message, retryable and docs. A failed simulation whose detail would exceed 65,535 bytes is shortened to err and the first 1,024 bytes of error.

Frames in hex
Accepted transaction   01 00 00 00
Refused transaction    01 03 LL LL {"reason":"missing_tip",...}
Accepted bundle        03 00 ID...(32 bytes) 00 00

Reply codes

A code groups reasons. Read reason in the detail for the exact cause.

CodeNameReasons
0Acceptednone
1RateLimitedrate_limited
2Unsignedunsigned
3MissingTipmissing_tip, credit_exhausted
4Malformedmalformed, invalid_request, unsupported
5TooLargetoo_large
6ForwardFailedforward_failed
7SignatureCountsignature_count
8Unauthorizedunauthorized
9Simulationsimulation_failed
10BundleInvalidbundle_size, bundle_control
11NotReadynot_ready, overloaded
12Internalpreflight_unavailable, internal
13PaymentRequiredpayment_required

Example

This Rust client uses quinn to send one signed transaction on a kind 0x01 stream and print the reply. It reads the key from SWQOS_API_KEY and the transaction's wire bytes from the file named on the command line.

Cargo.toml
[package]
name = "swqos-quic-example"
version = "0.1.0"
edition = "2024"

[dependencies]
quinn = { version = "0.11", default-features = false, features = ["runtime-tokio", "rustls-ring"] }
rcgen = "0.14"
rustls = { version = "0.23", default-features = false, features = ["std", "ring"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }
webpki-roots = "1"
src/main.rs
use std::{net::SocketAddr, sync::Arc, time::Duration};

use quinn::crypto::rustls::QuicClientConfig;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};

const HOST: &str = "fra.sol.zerocopy.sh";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_key = std::env::var("SWQOS_API_KEY")?;
    let path = std::env::args().nth(1).ok_or("usage: swqos-quic-example SIGNED_TX_FILE")?;
    let transaction = std::fs::read(path)?;

    // A self-signed client certificate whose subject common name is the API key.
    let mut params = rcgen::CertificateParams::new(Vec::<String>::new())?;
    let mut name = rcgen::DistinguishedName::new();
    name.push(rcgen::DnType::CommonName, api_key.as_str());
    params.distinguished_name = name;
    let key_pair = rcgen::KeyPair::generate()?;
    let certificate = params.self_signed(&key_pair)?;
    let chain = vec![CertificateDer::from(certificate.der().to_vec())];
    let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der()));

    let roots = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
    let provider = Arc::new(rustls::crypto::ring::default_provider());
    let mut tls = rustls::ClientConfig::builder_with_provider(provider)
        .with_protocol_versions(&[&rustls::version::TLS13])?
        .with_root_certificates(roots)
        .with_client_auth_cert(chain, key)?;
    tls.alpn_protocols = vec![b"zerocopy-swqos/1".to_vec()];

    // The server closes a connection after 30 seconds without traffic.
    let mut transport = quinn::TransportConfig::default();
    transport.keep_alive_interval(Some(Duration::from_secs(5)));
    let mut config = quinn::ClientConfig::new(Arc::new(QuicClientConfig::try_from(tls)?));
    config.transport_config(Arc::new(transport));

    let address = tokio::net::lookup_host((HOST, 443))
        .await?
        .find(SocketAddr::is_ipv4)
        .ok_or("no IPv4 address")?;
    let mut endpoint = quinn::Endpoint::client(SocketAddr::from(([0, 0, 0, 0], 0)))?;
    endpoint.set_default_client_config(config);
    let connection = endpoint.connect(address, HOST)?.await?;

    // Kind 0x01, flags 0x00 (no preflight), then the bare signed transaction.
    let (mut send, mut recv) = connection.open_bi().await?;
    send.write_all(&[0x01, 0x00]).await?;
    send.write_all(&transaction).await?;
    send.finish()?;
    let reply = recv.read_to_end(4 + usize::from(u16::MAX)).await?;

    let [0x01, code, low, high, detail @ ..] = reply.as_slice() else {
        return Err("unexpected reply frame".into());
    };
    let length = usize::from(u16::from_le_bytes([*low, *high]));
    let detail = detail.get(..length).ok_or("short reply frame")?;
    if *code == 0 {
        println!("accepted");
    } else {
        println!("refused with code {code}: {}", String::from_utf8_lossy(detail));
    }

    connection.close(0u32.into(), b"done");
    endpoint.wait_idle().await;
    Ok(())
}

The SWQoS agent serves this protocol on your own host, with a local certificate and no key check.

On this page