SPT Wire Protocol
The SPT Wire protocol provides DID-addressed vector transport over QUIC. Unlike topic-based messaging systems, SPT Wire routes messages by Decentralized Identifier (DID) prefix using scope-aware longest-prefix matching. All connections are authenticated by DIDs verified against the Global Identity Chain (GIC), replacing traditional X.509/mTLS with post-quantum cryptography.
Design Principles
- DID-based addressing: Routes are resolved by DID prefix, not topic string
- Scope-aware routing: Longest-prefix matching with multi-hop capability
- Exactly-once delivery: Monotonic sequence counters per recipient
- Store-and-forward: Vectors persist for offline recipients (including persistent backends)
- QUIC transport: Multiplexed, encrypted connections via
quinn - DID authentication: QUIC handshake uses
spt-crypto-providerwith ML-KEM/ML-DSA -- no X.509 certificates - Adaptive transport: ConnectionCascade transparently falls through QUIC, NAT hole punch, TURN relay, and WebTransport
- Pub/sub: DID-prefix subscriptions for fan-out delivery
Architecture
+-------------------------------------------+
| Application Layer |
| (VectorEnvelope from spt-smartpoint) |
+-------------------------------------------+
| spt-wire |
| +------------+ +--------------------+ |
| | Address | | RoutingTable | |
| | (DID) | | (prefix -> hop) | |
| +------------+ +--------------------+ |
| +------------+ +--------------------+ |
| | Endpoint | | TransportChannel | |
| | (QUIC) | | (framed vectors) | |
| +------------+ +--------------------+ |
| +-----------------+ +---------------+ |
| | ConnectionCascade| |SubscriptionMgr| |
| | (adaptive path) | | (pub/sub) | |
| +-----------------+ +---------------+ |
| +------------------------------------+ |
| | PersistentVectorStore (S&F) | |
| +------------------------------------+ |
| +------------------------------------+ |
| | WebTransportFallback (TCP/443) | |
| +------------------------------------+ |
+-------------------------------------------+
| spt-crypto-provider (DID auth, PQ) |
+-------------------------------------------+
| quinn / QUIC / UDP |
+-------------------------------------------+Crate Names
ADR-101 renamed the transport crates for clarity:
| Previous Name | Current Name | Purpose |
|---|---|---|
spt-transport | spt-wire | The SPT Wire protocol (QUIC transport, routing, pub/sub) |
spt-wire | spt-wire-format | Binary segment codec (varint, delta, hash) |
spt-wire-bridge | spt-wire-bridge | Bridges SmartPoint envelopes to QUIC transport |
When ADR-088 says "SPT Wire," it means the spt-wire crate.
DID-Based Addressing
Every participant is identified by a TransportAddress wrapping a DID string. The routing table maps DID prefixes to next-hop endpoints, enabling hierarchical routing across Scopes.
use spt_wire::TransportAddress;
let addr = TransportAddress::from_did("did:said:1:sw:blake3_abc123");DID Authentication (spt-crypto-provider)
QUIC connections are authenticated using DIDs verified against the GIC Merkle DAG, replacing X.509 certificates entirely. The spt-crypto-provider crate implements the rustls::crypto::CryptoProvider trait with post-quantum algorithms.
Authentication flow:
- During QUIC handshake, each side presents a Raw Public Key (RFC 7250) containing its DID and public key
- The
DidVerifierextracts the DID and resolves it against the GIC - The verifier checks that the key is registered, not revoked, and meets the trust tier requirement
- ML-KEM-768 key exchange establishes the session keys
- All subsequent data is encrypted with ChaCha20-Poly1305
Crypto suite mapping:
| CryptoSuite | Key Exchange | Signature | Status |
|---|---|---|---|
pq-v1 | ML-KEM-768 | ML-DSA-65 | Default |
hybrid-v1 | X25519 + ML-KEM-768 | Ed25519 + ML-DSA-65 | Transition |
classical-v1 | X25519 | Ed25519 | Legacy compatibility |
Transport Endpoint
A TransportEndpoint binds to a local UDP socket and serves as both a QUIC server and client. Inbound connections are accepted automatically; outbound connections create a TransportChannel for bidirectional vector exchange.
use spt_wire::{TransportConfig, TransportEndpoint};
// Start a server endpoint
let server_cfg = TransportConfig::default();
let server = TransportEndpoint::bind(server_cfg).await?;
let server_addr = server.local_addr()?;
// Connect a client to the server
let client_cfg = TransportConfig::default();
let client = TransportEndpoint::bind(client_cfg).await?;
let channel = client.connect(server_addr).await?;Transport Channel
A TransportChannel is a framed, bidirectional vector pipe over a QUIC connection. It handles length-prefixed framing so that each send_vector / recv_vector call delivers exactly one complete vector.
// Send a vector
channel.send_vector(b"hello world").await?;
// Receive a vector
let data = channel.recv_vector().await?;NAT Traversal Integration
The spt-nat crate is wired into spt-wire so that TransportEndpoint automatically attempts NAT traversal when direct connectivity is unavailable. The QuicNatTraversal coordinator handles STUN binding, hole punching, and TURN relay allocation. TURN supports 401 authentication retry and IPv6 addresses.
use spt_wire::TransportEndpoint;
use spt_nat::NatTraversalManager;
let endpoint = TransportEndpoint::bind(config).await?;
let nat_mgr = NatTraversalManager::new(stun_servers, turn_credentials);
// connect_with_nat tries direct, then hole punch, then TURN
let channel = endpoint.connect_with_nat(&target_did, &nat_mgr).await?;ConnectionCascade
The ConnectionCascade provides adaptive transport selection. When a direct QUIC connection fails, it automatically falls through to the next transport mechanism:
1. Direct QUIC (UDP/443) -- fastest, works for public IPs + same LAN
| fails
2. Hole Punch (STUN + simultaneous open) -- works for most consumer NATs
| fails
3. TURN Relay -- works for symmetric NAT
| fails
4. WebTransport over HTTPS (TCP/443) -- works through corporate firewalls
| fails
5. QUIC-over-TCP -- last resort, maximum compatibilityThe caller receives a TransportChannel regardless of which path was selected. The same SmartPoint pair authentication and E2E encryption apply on every path.
WebTransport Fallback
When UDP is completely blocked (common in corporate environments), WebTransportFallback tunnels QUIC semantics over HTTP/3 WebTransport:
use spt_wire::WebTransportFallback;
let fallback = WebTransportFallback::new(https_url, pair);
let channel = fallback.connect().await?;
// channel is a standard TransportChannel -- caller doesn't know it's over HTTPSWebTransport adds approximately 15% latency compared to direct QUIC. It is used only as a fallback, never as the preferred path.
Pub/Sub (DID-Prefix Subscriptions)
SPT Wire extends point-to-point transport with DID-prefix subscriptions for fan-out delivery. The SubscriptionManager matches published vectors against subscriber prefixes using the same longest-prefix matching as the routing table.
use spt_wire::TransportEndpoint;
// Subscribe to all vectors in an organization
let handle = endpoint.subscribe("did:said:1:sw:blake3_org", |envelope| {
// Process received vector
});
// Publish a vector to all matching subscribers
endpoint.publish(envelope).await?;Subscriptions are scope-aware: a subscriber only receives vectors they are authorized to see based on their SmartPoint pair capabilities.
Store-and-Forward
When the recipient is offline, vectors are persisted in a VectorStore. The PersistentVectorStore backend (added in ADR-101) ensures store-and-forward data survives process restarts, replacing the previous in-memory-only implementation.
Each stored vector is wrapped in a StoredVector with metadata (sender DID, recipient DID, timestamp). When the recipient reconnects, pending vectors are flushed in order.
The InMemoryVectorStore remains available for testing and lightweight deployments.
Routing Table
The RoutingTable maps DID prefixes to next-hop socket addresses. Lookups use longest-prefix matching, which enables hierarchical routing across organizational boundaries.
use spt_wire::RoutingTable;
let mut table = RoutingTable::new();
table.insert("did:said:1:sw:blake3_org", "10.0.0.1:4433".parse()?);
table.insert("did:said:1:sw:blake3_org:team_a", "10.0.1.1:4433".parse()?);
// Routes to 10.0.1.1 (longest prefix match)
let hop = table.lookup("did:said:1:sw:blake3_org:team_a:alice");Wire Bridge
The spt-wire-bridge crate connects the SmartPoint envelope layer to the QUIC transport layer. Every message flowing over the wire is automatically sealed with a VectorEnvelope before sending and opened on receipt.
Application
|
v
WireBridge::send_message(content)
| VectorEnvelope::seal(pair, content)
| channel.send_vector(sealed_bytes)
v
QUIC (spt-wire)
|
v
WireBridge::recv_message()
| channel.recv_vector()
| VectorEnvelope::open(pair, sealed_bytes)
v
Applicationuse spt_wire_bridge::{WireBridge, WireSession};
let bridge = WireBridge::new(channel, smartpoint_pair);
bridge.send_message(b"encrypted payload").await?;
let decrypted = bridge.recv_message().await?;Key Types
| Type | Crate | Description |
|---|---|---|
TransportEndpoint | spt-wire | QUIC server/client endpoint |
TransportChannel | spt-wire | Framed bidirectional vector pipe |
TransportConfig | spt-wire | Endpoint configuration |
TransportAddress | spt-wire | DID-based address |
RoutingTable | spt-wire | DID prefix to next-hop mapping |
ConnectionCascade | spt-wire | Adaptive transport path selection (QUIC/punch/TURN/WT) |
SubscriptionManager | spt-wire | DID-prefix pub/sub subscription management |
PersistentVectorStore | spt-wire | Persistent store-and-forward backend |
VectorStore | spt-wire | Trait for store-and-forward backends |
InMemoryVectorStore | spt-wire | In-memory store-and-forward (testing) |
StoredVector | spt-wire | Persisted vector with metadata |
WebTransportFallback | spt-wire | HTTPS fallback when UDP is blocked |
SptCryptoProvider | spt-crypto-provider | rustls CryptoProvider with ML-KEM/ML-DSA |
DidVerifier | spt-crypto-provider | DID-based QUIC authentication against GIC |
WireBridge | spt-wire-bridge | Envelope + transport integration |
WireSession | spt-wire-bridge | Session state for a bridge |