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
| Setting | Value |
|---|---|
| Address | UDP port 443 of a regional host or sol.zerocopy.sh |
| ALPN | zerocopy-swqos/1 |
| TLS | 1.3 only |
| Server certificate | Publicly trusted, valid for the regional name and sol.zerocopy.sh. Verify it against the hostname you connect to. |
| Client certificate | Required. Any certificate whose first subject common name is your API key. A self-signed certificate works; the chain is not validated. |
| Idle timeout | 30 seconds. The server sends no keep-alives, so configure your client to send them. |
| Streams | At most 64 bidirectional and 64 unidirectional streams open at once per connection. |
| Handshake | Must 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 code | Reason text | Meaning |
|---|---|---|
| 0 | shutdown | The server is shutting down. Reconnect. |
| 1 | connection limit | A connection limit was reached. Keep at most 32 open connections per key in one region. |
| 8 | unauthorized | No 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.
| Channel | Request | Reply | Preflight |
|---|---|---|---|
| Datagram | One payload: a transaction or a bundle. | None | No |
| Unidirectional stream | One payload, up to 16,403 bytes, then the end of the stream. | None | No |
Bidirectional stream, kind 0x01 | [0x01][flags][payload], a transaction. | Transaction frame | Flag 0x01 |
Bidirectional stream, kind 0x03 | [0x03][payload], a bundle envelope with flag 0x02. | Bundle frame | No |
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.
| Frame | Layout |
|---|---|
| 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.
Accepted transaction 01 00 00 00
Refused transaction 01 03 LL LL {"reason":"missing_tip",...}
Accepted bundle 03 00 ID...(32 bytes) 00 00Reply codes
A code groups reasons. Read reason in the detail for the exact cause.
| Code | Name | Reasons |
|---|---|---|
| 0 | Accepted | none |
| 1 | RateLimited | rate_limited |
| 2 | Unsigned | unsigned |
| 3 | MissingTip | missing_tip, credit_exhausted |
| 4 | Malformed | malformed, invalid_request, unsupported |
| 5 | TooLarge | too_large |
| 6 | ForwardFailed | forward_failed |
| 7 | SignatureCount | signature_count |
| 8 | Unauthorized | unauthorized |
| 9 | Simulation | simulation_failed |
| 10 | BundleInvalid | bundle_size, bundle_control |
| 11 | NotReady | not_ready, overloaded |
| 12 | Internal | preflight_unavailable, internal |
| 13 | PaymentRequired | payment_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.
[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"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.