Frozen interfaces
These contracts are shared by every crate. The function signatures in crates/*/src are authoritative. Change a signature only together with every caller, and record the change here.
Hosts in the devnet
| Host | Serves | DID |
|---|---|---|
zkwid.test | N (zkwid-n) | did:web:zkwid.test, service #zkwid of type ZkwidService |
h.zkwid.test | H stub (zkwid-h) | did:web:h.zkwid.test, #atproto K256 key |
pds.zkwid.test, *.pds.zkwid.test | atproto-pds binary | did:web:pds.zkwid.test |
plc.zkwid.test | atproto_testkit fake PLC | none |
Every https request to these hosts goes through the devnet CONNECT proxy. Process env HTTPS_PROXY points at it, and SSL_CERT_FILE points at the devnet CA. Any reqwest::Client built after Devnet::start picks both up.
Records
All in U's repo.
at.zkwid.workload/<rkey>iszkwid::lexicon::Workload.measurements[i].wasmis a blob in U's PDS.measurements[0]is the shim,[1..]are stage2..N.at.zkwid.key/<base32(sha256(challenge))>iszkwid::lexicon::Key. It is written only by N through N's own OAuth session (clientstore::N_CLIENT).at.zkwid.demo.log/<tid>iszkwid::lexicon::DemoLog. The demo workload writes it through N's proxy.- Reserved, defined in
zkwid::nsid::RESERVED:at.zkwid.keyandat.zkwid.workload.- A workload scope naming a reserved collection makes
client_scopefail, so its metadata returns 404. - The proxy always refuses writes to reserved collections, and refuses every
at.zkwid.*lxm.
- A workload scope naming a reserved collection makes
Write records with a $type field, using zkwid::lexicon::record(nsid, &value).
N routes (zkwid-n)
| Route | Module |
|---|---|
GET /.well-known/did.json | did |
GET /oauth-client-metadata.json, GET /login?handle=, GET /callback | oauth (N's own client, scope atproto repo:at.zkwid.key) |
GET /{handle}/{rkey}/oauth-client-metadata.json, GET /{handle}/{rkey}/login, GET /{handle}/{rkey}/callback | oauth (per-workload client, scope zkwid::scope::client_scope(workload.scope)) |
POST /xrpc/at.zkwid.createChallenge, POST /xrpc/at.zkwid.createKey, POST /xrpc/at.zkwid.deleteKey | keys |
ANY /xrpc/{nsid} (fallback, not at.zkwid.*) | proxy |
Login routes answer 302 to the PDS authorize URL. The callback answers 200 with JSON { "did": ..., "client": ... }.
XRPC errors use { "error": Name, "message": text } with status:
400forInvalidRequest,MeasurementMismatch,InvalidReceipt,ChallengeInvalid,ChallengeUsed401forAuthRequired/InvalidToken403forHostNotAllowed,NotAuthorized,ScopeMissing404forWorkloadNotFound,KeyNotFound
Store keys
Store::client_key(client_id_url)gets or creates a P-256 client assertion key per OAuth client_id.Session.clientis the workload rkey, orstore::N_CLIENT("") for N's own client.- Sessions are keyed by (did, client).
Store::put_request(state, json)stores a serializedatproto_oauth::workflow::OAuthRequest, together with the client string and the client_id.
H helpers
zkwid_h::stage_bytes(name)runscargo build --release --target wasm32-unknown-unknownforstages/<name>and returns the wasm bytes.zkwid_h::run_stage(wasm, input)runs a stage natively in wasmi 0.31, providing the sameenv.keygenimport the guest does.
Service auth
- H calls N:
atproto_xrpcs::mint_service_auth(h_key, "did:web:h.zkwid.test", "did:web:zkwid.test#zkwid", lxm, 60 s). - Instance calls the proxy: the same call with
iss = <instance did:key>, signed by the instance K256 private key (KeyData::new(K256Private, seed)).lxmis the proxied NSID.- An
atproto-proxyheader is optional.
- N verifies:
atproto_xrpcs::verify_service_authwithzkwid::resolver::DidKeyResolver { inner: DidResolver }.- The policy is
ServiceAuthPolicy::new(lxm, "did:web:zkwid.test#zkwid"). - The proxy also rejects any jti already in
Store::seen_jti.
- The policy is
Challenge
zkwid::challenge::mint(n_key, "did:web:zkwid.test", sub = H, repo = U, workload, 300 s)produces an ES256 JWT with headertyp: zkwid-challenge+jwt. Its claims areChallenge.nonce(token)issha256(token).rkey(token)isbase32-lower-nopad(sha256(token)).
ZK (zkwid-zkvm)
ProveInput { stages, seed, nonce: challenge::nonce(token), aud: "did:web:zkwid.test" }goes toprove, which returnsbincode(Receipt).- N calls
verify(receipt), which checksreceipt.verify(image_id())and returnsJournal. Journal.keyis the 33-byte compressed K256 public key,KeyData::new(K256Public, key). Its did:key isKeyData'sDisplay.Journal.measurements[i]issha256(stages[i]). It equalszkwid::measure::cid_digest(workload.measurements[i].wasm.ref.$link).- Dev mode:
RISC0_DEV_MODE=1makes fake receipts. - The
zkwid-n/prodfeature enableszkwid-zkvm/disable-dev-mode, which enablesrisc0-zkvm/disable-dev-mode. - wasmi is pinned to 0.31 in both the guest and H.
Stage ABI (stages/*, wasm32-unknown-unknown, no_std)
- Exports:
memoryinput_ptr() -> i32output_ptr() -> i32run(len: i32) -> i32(returns the output length; a trap means failure)
- Imports: module
env,keygen(seed_ptr: i32, out_ptr: i32) -> i32. It reads 32 bytes and writes a 33-byte compressed K256 public key, returning 33. - Shim: the input is a 32-byte seed. It calls
keygenand outputs the 33-byte public key. - Stage2: the input is JSON
{ "instance": s, "createdAt": t }. The output is the JSON body of anat.zkwid.demo.logrecord,{ "$type": "at.zkwid.demo.log", "text": "hello from <instance>", "createdAt": t }. - Build:
cargo build --release --target wasm32-unknown-unknown --manifest-path stages/<name>/Cargo.toml. The output isstages/<name>/target/wasm32-unknown-unknown/release/<name>.wasm.