atproto utils for zig zat.dev

test: deterministic simulation campaigns over a simulated network, and the fixes they found #34

Open zzstoatzz.io
targeting main from codex/zat-dst
Open opened by and assigned to zzstoatzz.io
targeting main from codex/zat-dst

zat's clients now run under deterministic simulation: pbt/sim_io.zig puts an in-process network behind std.Io, so the real XRPC, identity, firehose, and OAuth code, together with zttp, websocket.zig, and std.http, runs against scripted hosts that refuse, reset, truncate, and split reads. The campaigns found seven defects in zat, fixed here with regressions, and two in dependencies, fixed on their own branches.

Found Fix
DidResolver accepted a document whose id names another DID reject with DidDocumentIdMismatch
a DoH A/AAAA answer that is a hostname passed the safety preflight and became the dial host require an IP literal of the record's family
3fff::/20 (RFC 9637 documentation space) passed as routable add it to the non-routable ranges
Retry-After in its HTTP-date form was ignored honor it after the delta form, before RateLimit-Reset
a synthesized firehose commit CAR was rooted at a record, or at nothing root it at the commit CID; the encoder has no commit block to include, so producers of verifiable commits still pass blocks
TCP keepalive setup panicked when the peer reset right after the handshake call setsockopt through errno and give up on keepalive
a generated TID with a leading a or b was rejected by the parser parse the full 53-bit timestamp range

DPoP and client-assertion JWTs also escape string claims now (89049c9), found by the earlier runtime campaign.

campaigns

zig build pbt -Dhegel runs every campaign; zig build pbt-<name> -Dhegel runs one. Ordinary builds and zig build test do not need Hegel.

  • xrpc: wire encoding, bearer auth, and a retry model (its own backoff schedule, not the policy's) layered over zttp's dead-connection replay, with Retry-After and reset hints, refused, reset, and truncated connections, and keep-alive reuse.
  • identity: PLC and did:web resolution, the DoH preflight against 19 hand-classified answers (IPv4 and IPv6, routable, special-purpose per the IANA registries, and non-literal) judged alone and in mixes, a preflight pass that must reach TLS setup, and handle TXT did= selection.
  • firehose: FirehoseClient over simulated relays and websocket.zig: the cursor on every reconnect, the exact delivered sequence, backoff with its reset and cap, host rotation, and the Host header. LabelClient shares this code.
  • oauth: dpopRequest and refreshAccessToken: nonce challenges in body and header, missing nonces, exhaustion, each proof's claims, the returned nonce, and leaks under a checking allocator.
  • mst: one-shot block reader failures during lazy reads, checked for atomicity.
  • codecs, runtime: the earlier codec, syntax, crypto, and virtual-clock properties.

zig build mutants -Dhegel plants the 20 entries of pbt/mutants.zon, including the inverse of every fix above, and counts the seeds that catch each. Two new oracles were checked against the harness before them: the block-reader mutant is caught on 0 of 10 seeds before reader faults and 10 of 10 after; a backoff mutant on 0 of 10 when the XRPC model called delayMillisAt and 10 of 10 with its own schedule.

findings outside this PR

pbt/findings.md records each finding with its replay blob.

  • zttp returns a body cut short of Content-Length as complete. Fixed on zttp fix/content-length-truncation; pbt/xrpc.zig counts the divergence behind transport_reports_truncation until zat pins a release with it.
  • websocket.zig drops a handshake header whose CRLF splits across reads. Fixed on websocket.zig fix/handshake-split-crlf; the simulator avoids that split behind split_upgrade_crlf and counts it.
  • open: zttp's dead-connection replay resends a single-use DPoP proof (RFC 9449 §11.1). Fixing it needs a per-request replay opt-out in zttp or a zat retry with a fresh proof.
gaps

The simulator runs async inline and has no concurrent, so zttp's deadline and stall guards and idle_timeout_ms are untested. TLS is not simulated: wss, https well-known handle lookups, did:web document fetches, and JetstreamClient, which always connects over TLS, are not covered.

verification

Each of the 24 commits passes zig build check, zig build test, and, where campaigns exist, a seeded zig build pbt -Dhegel. The tip compiles the tests for arm-linux-musleabihf. The JWT isExpiredAt and RFC 6979 commits are their own PR (33, cell-extras) and are not part of this one.

🤖 Generated with Claude Code

Commits 0